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 17, 2026, 11:00 PM ET / August 18, 2026, 03:00 UTC.

ClawSweeper review

What this changes

This PR replaces GitHub Skill Sync’s direct repository enrollment form with a publisher repository picker and a preview of discovered skills, replacements, conflicts, and unavailable destinations.

Merge readiness

Blocked by patch quality or review findings - 11 items remain

Keep open: this collaborator-authored draft has useful preview work but currently removes the working enrollment path, is incompatible with current main’s sync contract, and needs real behavior proof.

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

Review scores

Measure Result What it means
Overall readiness 🧂 unranked krab (1/6) PR readiness rating was derived from proof quality, review findings, security review, and reviewer confidence.
Proof confidence 🌊 off-meta tidepool Not applicable: Real behavior proof is not required for maintainer- or bot-authored pull requests.
Patch quality 🧂 unranked krab (1/6) Security review found an item that needs attention.

Verification

Check Result Evidence
Real behavior Not applicable Not applicable: Real behavior proof is not required for maintainer- or bot-authored pull requests.
Evidence reviewed 6 items Current enrollment path: Current main still exposes the direct enrollment action and settings handler that creates or updates a GitHub Skill Sync source.
Branch disables its replacement: The new preview UI renders Enable GitHub Skill Sync as disabled while the route no longer invokes the current enrollment action.
Current snapshot contract and release provenance: Current main’s snapshot helper takes an ActionCtx and runs raster validation; the contract arrived in the v0.23.3 commit, while this branch calls its former exported two-argument shape.
Findings 4 actionable findings [P1] Keep enrollment available until activation exists
[P1] Rebase the preview onto the current snapshot contract
[P1] Disable installation-token auth for arbitrary public repos
Security Needs attention Use the least-privileged GitHub credential for discovery: The default header builder attaches a GitHub App installation token unless explicitly disabled, but publisher-selected public repositories need not belong to that installation. Current main’s equivalent source helper explicitly disables App-token use for this boundary.

How this fits together

Publisher settings lets verified publishers connect public GitHub repositories to ClawHub’s Skill Sync service. The settings UI calls Convex actions to verify GitHub ownership, inspect repository archives, classify destination conflicts, and then enroll the source for scanning and synchronization.

flowchart LR
  A[Publisher settings] --> B[Repository discovery]
  B --> C[GitHub API and archive]
  C --> D[Skill and ownership preview]
  D --> E[Enrollment decision]
  E --> F[GitHub Skill Sync source]
Loading

Decision needed

Question Recommendation
Should repository preview ship as an additive pre-enrollment step while current enrollment remains available, or wait until the canonical engine provides its enrollment handoff? Keep enrollment and add preview: Retain the current enrollment action while using the preview as an optional safety step until the canonical handoff is ready.

Why: The PR intentionally leaves activation unavailable but removes the existing user-facing enrollment path, so maintainer intent is needed to choose the supported rollout shape.

Before merge

  • Keep enrollment available until activation exists (P1) - This renders the only activation control disabled, while the route has removed current main’s configurePublicGitHubSkillSource call. Existing eligible publishers can enroll sources on main; merging this preview-only replacement would remove that workflow without an equivalent handoff.
  • Rebase the preview onto the current snapshot contract (P1) - Current main made the snapshot helper private, added a leading ActionCtx parameter, and uses it for raster validation. This call still imports and invokes the old signature while its handler only declares runQuery, so the branch cannot typecheck or preserve current snapshot behavior after rebase.
  • Disable installation-token auth for arbitrary public repos (P1) - This is a late finding on code unchanged since the prior reviewed head: buildGitHubApiHeaders opts into the GitHub App installation token unless useGitHubApp: false is supplied. These endpoints must inspect arbitrary publisher-selected public repositories, which may be outside that installation and therefore fail or return incomplete results; reuse current main’s public-source header policy.
  • Resolve renamed repositories by immutable GitHub ID (P2) - This is a late finding on unchanged code: preview already obtains metadata.repositoryId, but classification only looks up by_repo. After a repository rename, a source owned by another publisher can be missed and presented as a clean destination; current main resolves by by_github_repository_id before the mutable name fallback.
  • Resolve security concern: Use the least-privileged GitHub credential for discovery - The default header builder attaches a GitHub App installation token unless explicitly disabled, but publisher-selected public repositories need not belong to that installation. Current main’s equivalent source helper explicitly disables App-token use for this boundary.
  • Resolve merge risk (P1) - Merging would replace a currently functional source-enrollment workflow with a preview whose only activation control is disabled.
  • Resolve merge risk (P1) - The stale branch cannot be safely integrated without adapting to v0.23.3’s current GitHub Skill Sync snapshot and immutable-source contracts.
  • Resolve merge risk (P1) - Using installation credentials for arbitrary public repository discovery can make repository listing and preview fail when the selected repository is outside that installation’s scope.
  • Improve patch quality - Resolve the security review concern or explain why the changed path is safe.
  • Improve patch quality - Address the highest-priority review finding and re-run the changed-surface validation.

Findings

  • [P1] Keep enrollment available until activation exists — src/components/GitHubSkillSyncConfiguration.tsx:249-257
  • [P1] Rebase the preview onto the current snapshot contract — convex/githubSkillSyncSettings.ts:475-480
  • [P1] Disable installation-token auth for arbitrary public repos — convex/githubSkillSyncSettings.ts:288-292
  • [medium] Use the least-privileged GitHub credential for discovery — convex/githubSkillSyncSettings.ts:288
Agent review details

Security

Needs attention: The new GitHub repository-discovery path applies a repository-scoped installation credential to arbitrary public-repository endpoints.

Review metrics

Metric Value Why it matters
Change size production +1,145, tests +630, generated API +4, −142 across 9 files This is a substantial new settings and backend surface, so current-main integration and end-to-end proof matter before merge.

Merge-risk options

Maintainer options:

  1. Rebase and preserve enrollment (recommended)
    Adapt the preview to current main, retain or wire the actual enrollment handoff, and use the canonical public-repository authentication and immutable-ID checks before merge.
  2. Pause until the enrollment contract lands
    Keep this draft out of merge until the canonical engine exposes the activation contract that can replace the current enrollment flow.

Technical review

Best possible solution:

Rebase the preview onto current main, keep existing enrollment available until an equivalent canonical handoff is wired, reuse the current identity and credential helpers, and prove the authenticated settings flow in a real browser.

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

Yes, source reproduction is high-confidence: rebasing onto current main exposes the changed snapshot signature, and the current settings route shows the enrollment action that this branch replaces with a disabled control.

Is this the best way to solve the issue?

No. The preview is a plausible addition, but it is not the best integration while it removes working enrollment; preserving that path or wiring the real handoff is safer.

Full review comments:

  • [P1] Keep enrollment available until activation exists — src/components/GitHubSkillSyncConfiguration.tsx:249-257
    This renders the only activation control disabled, while the route has removed current main’s configurePublicGitHubSkillSource call. Existing eligible publishers can enroll sources on main; merging this preview-only replacement would remove that workflow without an equivalent handoff.
    Confidence: 0.99
  • [P1] Rebase the preview onto the current snapshot contract — convex/githubSkillSyncSettings.ts:475-480
    Current main made the snapshot helper private, added a leading ActionCtx parameter, and uses it for raster validation. This call still imports and invokes the old signature while its handler only declares runQuery, so the branch cannot typecheck or preserve current snapshot behavior after rebase.
    Confidence: 0.99
  • [P1] Disable installation-token auth for arbitrary public repos — convex/githubSkillSyncSettings.ts:288-292
    This is a late finding on code unchanged since the prior reviewed head: buildGitHubApiHeaders opts into the GitHub App installation token unless useGitHubApp: false is supplied. These endpoints must inspect arbitrary publisher-selected public repositories, which may be outside that installation and therefore fail or return incomplete results; reuse current main’s public-source header policy.
    Confidence: 0.98
    Late finding: first raised on code an earlier review cycle already covered.
  • [P2] Resolve renamed repositories by immutable GitHub ID — convex/githubSkillSyncSettings.ts:242-245
    This is a late finding on unchanged code: preview already obtains metadata.repositoryId, but classification only looks up by_repo. After a repository rename, a source owned by another publisher can be missed and presented as a clean destination; current main resolves by by_github_repository_id before the mutable name fallback.
    Confidence: 0.97
    Late finding: first raised on code an earlier review cycle already covered.

Overall correctness: patch is incorrect
Overall confidence: 0.99

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 4117154ecac4.

Labels

Label changes:

  • add status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Not applicable: Real behavior proof is not required for maintainer- or bot-authored pull requests.
  • remove status: 📣 needs proof: Current PR status label is status: ⏳ waiting on author.

Label justifications:

  • P2: The draft is not yet user-facing, but its merge would break an existing publisher enrollment workflow.
  • merge-risk: 🚨 compatibility: The branch replaces the existing settings enrollment action with a disabled activation control.
  • merge-risk: 🚨 security-boundary: Repository discovery uses a scoped GitHub App credential where current main deliberately disables it for arbitrary public repositories.
  • merge-risk: 🚨 availability: A selected repository outside the App installation can make listing or preview fail despite being public and publisher-owned.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🌊 off-meta tidepool and patch quality is 🧂 unranked krab.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Not applicable: Real behavior proof is not required for maintainer- or bot-authored pull requests.

Evidence

Security concerns:

  • [medium] Use the least-privileged GitHub credential for discovery — convex/githubSkillSyncSettings.ts:288
    The default header builder attaches a GitHub App installation token unless explicitly disabled, but publisher-selected public repositories need not belong to that installation. Current main’s equivalent source helper explicitly disables App-token use for this boundary.
    Confidence: 0.98

What I checked:

  • Current enrollment path: Current main still exposes the direct enrollment action and settings handler that creates or updates a GitHub Skill Sync source. (src/routes/settings.tsx:317, 4117154ecac4)
  • Branch disables its replacement: The new preview UI renders Enable GitHub Skill Sync as disabled while the route no longer invokes the current enrollment action. (src/components/GitHubSkillSyncConfiguration.tsx:249, b1a8dfcbf37b)
  • Current snapshot contract and release provenance: Current main’s snapshot helper takes an ActionCtx and runs raster validation; the contract arrived in the v0.23.3 commit, while this branch calls its former exported two-argument shape. (convex/githubSkillSync.ts:2953, 87ca030c30f3)
  • Credential boundary: The branch’s settings helper leaves GitHub App credentials enabled by default, whereas current main explicitly disables repository-scoped installation tokens for arbitrary public repository selection. (convex/githubSkillSyncSettings.ts:288, b1a8dfcbf37b)
  • Immutable repository identity is available but unused: The preview fetches a canonical repository ID but classifies conflicts only by mutable repo name; current main resolves sources by immutable GitHub repository ID first. (convex/githubSkillSyncSettings.ts:242, b1a8dfcbf37b)
  • Feature-history routing: Blame attributes the branch settings and preview actions to Patrick Erichsen; the current main GitHub Skill Sync implementation and its new snapshot contract were also introduced in Patrick’s v0.23.3 commit. (convex/githubSkillSyncSettings.ts:475, 87ca030c30f3)

Likely related people:

  • Patrick-Erichsen: Authored the branch and the current-main GitHub Skill Sync implementation that established the snapshot contract now requiring integration. (role: recent GitHub Skill Sync contributor; confidence: high; commits: 87ca030c30f3; files: convex/githubSkillSync.ts, src/routes/settings.tsx)
  • steipete: The PR timeline records a recent human comment, making this person a useful routing candidate for the enrollment-versus-preview product decision. (role: recent decision participant; confidence: medium; files: src/routes/settings.tsx)

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 (2 earlier review cycles)
  • reviewed 2026-07-24T05:33:28.189Z sha b1a8dfc :: found issues before merge. :: [P1] Preserve source enrollment until activation exists
  • reviewed 2026-08-05T02:09:31.730Z sha b1a8dfc :: found issues before merge. :: [P1] Preserve source enrollment until its replacement exists | [P1] Rebase the snapshot preview onto the current contract

@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
@steipete

Copy link
Copy Markdown
Contributor

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@clawsweeper clawsweeper Bot added status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Aug 18, 2026
@github-actions github-actions Bot removed the stale label Aug 18, 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. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants