v2: install from public skill catalogs, compat verdicts, agent-first find - #4
Conversation
…-first find v1 was a read-only browser of the agent's skills. v2 keeps that reading experience and adds the write half of the skills story, built on the platform's new skills API (mobius-os/mobius#146): - list from GET /api/skills: both skill shapes (flat <name>.md and directory <name>/SKILL.md), provenance chips, 30-day usage counts - catalog screen: curated public sources scanned with one git-trees call each (through /api/proxy), background summary prefetch, full-page skill detail with breadcrumbs, install via POST /api/skills/install (manage_skills) - compat verdicts: '✓ Works with Möbius' / '⚠ N things to know' chips with plain-language notes — pre-install on catalog cards and the skill page (predicted from the git tree), post-install on installed skills' rows and detail (judged from what's actually on disk via shared-list) - agent-first Find button (prefilled chat draft; the platform's finding-skills seed skill is the playbook) and two-tap uninstall for install-provenance skills - fires POST /api/skills/catalog-index/refresh when Browse opens, keeping the agent's cached catalog index fresh Version 2.0.0; adds catalog.js (dependency-free catalog core) and its test suite (64 tests total). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hamzamerzic
left a comment
There was a problem hiding this comment.
Thorough review — round 1/2: authority, trust preview, and install consistency
Reviewed exact head 2e295a1. All 64 unit tests pass and the app validates/bundles successfully, but the install-facing flow needs changes before merge:
-
Blocker — Install is enabled before the trust/compatibility information exists. Catalog cards and the full skill page render an active Install button while
SKILL.mdis stillloading(and also after it failed), so a quick tap can install a third-party instruction set before its description, body, or “things to know” verdict was available. The app’s stated trust ritual and pre-install compatibility promise therefore do not hold on the actual action path. Disable Install until the exactSKILL.mdand verdict are ready; on fetch failure, keep it disabled and offer GitHub/Retry. For amber verdicts, make the warning visible before the installing tap rather than allowing the first action to bypass it. -
Blocker — catalog navigation has stale-response races.
openSource(A)and thenopenSource(B)leaves both scans alive; a slower A response can overwriteopen,skillList, anddescsafter B is current. Cancelling the prefetch generation only stops new worker iterations: already-runningloadDescription(A, sameDir)requests can still populate B’sdescs[dir], because caches/inflight keys are directory-only and carry no source generation. Add a source/request generation (or AbortControllers), key caches by source+ref+dir, and regression-test A→B with reversed response order for both tree and markdown requests. -
Blocker — preview and install are not one immutable revision. Tree scan, raw
SKILL.mdpreview, compatibility verdict, and the POST currently all name mutablemain. A source push between those steps can make the owner review one body/caveat set and install another. Coordinate with mobius#146: resolve one commit OID for the source scan, fetch previews from that OID, and send that exact OID as the install ref/expected revision. Displaying source provenance should retain it. -
Do not treat a truncated whole-repo scan as a reliable catalog.
treeScanUrl()scans the repository root even when the source has a subtree path, then filters client-side. This increases truncation risk and can omit valid skills arbitrarily. Use GitHub’s encoded<ref>:<path>tree spec like the backend index does, and treattruncated=trueas an incomplete/error state rather than a normal install catalog.
The manage_skills permission separation, same-origin proxy use, sanitized Markdown rendering, and two-tap removal are good foundations. The remaining issue is making the reviewed bytes and the installed bytes provably the same, with no action before review data is ready.
Thorough review — round 2/2: UI state, compatibility parity, and accessibilitySecond independent pass on exact head
|
Independent reconciliation review — HOLD on exact
|
…r scan (PR review) Addresses the round-1 review: - Install arms only once the exact SKILL.md and its compat verdict are on screen - on cards and on the skill page. A failed fetch keeps Install off and offers Retry (cards) / Try again + GitHub (page). An amber verdict auto-expands its notes on the skill page, so the warning is visible before the first installable tap. - Every scan takes a generation token (createGenerationGuard in catalog.js, unit-tested with A-then-B reversed-response races for both the tree and markdown requests); any response holding a stale token is dropped, so a slow source can never overwrite state after another became current. - One immutable revision per scan: the source ref resolves to a commit OID first; the tree listing, every SKILL.md preview, the compat verdict, the GitHub page link, and the install POST all name that OID. The reviewed bytes are provably the installed bytes (backend records the OID in provenance). - Tree scans are scoped to the source's subtree via the <oid>:<path> tree spec (paths re-prefixed to repo-relative at the scan boundary), and a truncated=true response is an error state, never a quietly incomplete catalog. 69 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All four findings addressed in 0c0759c (69 tests pass, bundle + validator clean):
🤖 Generated with Claude Code |
hamzamerzic
left a comment
There was a problem hiding this comment.
Fresh exact-head re-review — HOLD on 0c0759c
This head fixes the four authority/OID-generation findings from round 1, and the hosted/local tests are green (69/69). However, all six findings in the round-2 UI/compatibility review remain reproducible on this exact head: mouse-only catalog cards, client/backend compatibility-rule mismatch, directory-relative Markdown links treated as global skills, non-generation-gated primary loads plus unawaited post-uninstall reload, incomplete/unencoded installed traversal reported as complete, and unnormalized catalog/source identity.
One boundary is now clearer: the active catalog preview/install path consistently uses one resolved OID, but installed state discards the install response's commit (index.jsx:582-586), platform GET /api/skills omits it, and installed detail has no pinned-source review link. Preserve that evidence across the lifecycle.
Release pairing is also still blocked: v2 declares no enforced minimum platform/API contract, while the platform bootstrap consumes mutable app-skills/main. See the current core exact-head review. Please resolve the six linked items and the immutable/rolling-compatible release contract before merge.
…e-safe loads, complete-or-no-verdict (PR review round 2 + re-review)
- Catalog cards open through a real <button class="sk-cardopen"> (name +
summary are its accessible content, action buttons live outside it) -
keyboard and SR users can open every card, not just Install. Verified by
an actual render test (esbuild + react-dom/server against the shell's
frontend deps; skips cleanly where they are absent).
- assessCompat now applies the backend's _resource_rel_ok rule EXACTLY via
resourceRelOk(): max 4 path segments (the old check allowed 5), and
empty / dot-prefixed / backslash segments are dropped. Boundary cases
(depth-4 vs depth-5, .hidden, //, backslash) are locked in tests.
- Relative links inside a directory skill classify as that skill's bundled
resources: the installed view explains them in place instead of
emitting "unknown skill"; catalog previews block them; flat skills keep
the legacy cross-skill slug routing. classifyLink(href, { dirSkill }).
- Primary /api/skills loads go through createSkillsLoader (domain.js) with
newest-generation-wins - an older response can never overwrite a newer
list, error state, or app_ready count (tested with reversed completion
and with a stale failure). The post-uninstall reload is awaited behind
the truthful removeBusy state before the detail closes.
- listInstalledFiles (catalog.js) segment-encodes every URL component and
returns null - "no verdict" - whenever the walk is incomplete (failed
page, paginated directory, depth cutoff, page budget), instead of
assessing partial data as the complete skill.
- Installed checks and duplicates compare installIdOf() (the lowercased id
the server actually derives), and sources.json overrides are normalized
(installer-shape repo/path/ref, capped, sanitized labels) before
anything renders or builds URLs from them.
- The install-time commit OID now survives the whole lifecycle: rows keep
the commit + source coordinates GET /api/skills reports, and installed
detail links "source @ <oid>" to the exact reviewed revision.
80 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round-2 and the re-review items addressed in 5437897 (80 tests incl. a new render test; CI green):
Commit evidence across the lifecycle — the platform now returns Release pairing — resolved on the core side: 🤖 Generated with Claude Code |
hamzamerzic
left a comment
There was a problem hiding this comment.
Exact-head review — round 1/2: catalog identity and reviewed-byte parity (5437897)
The six prior UI/compatibility findings are largely fixed and the 80-test suite is green. The catalog identity boundary still needs changes:
-
Blocker —
treeToSkills()still admits names the installer rejects.installIdOf()only lowercases; it does not enforce the backend^[a-z0-9][a-z0-9._-]*$contract. I reproduced cards forBad Skill,a#fragment, and similar Git directories becoming fully reviewable/installable, then inevitably receiving a backend 400. Filter invalid derived IDs (or render them explicitly unsupported with Install disabled), and test spaces,#,?, percent, leading punctuation, and non-ASCII names. -
Blocker — normalization creates duplicate install identities without deduplication. A tree containing both
PDF/SKILL.mdandpdf/SKILL.mdproduces two cards with the sameid: pdf; before either is installed both actions are live, and after one install both become “Installed.” This is not a trustworthy one-card→one-install mapping. Group by normalized install id and either choose only an exact-lowercase unambiguous path or render a collision that cannot be installed. Lock case-collision and duplicate-path regressions. -
Blocker — preview/link URLs do not encode Git path segments.
rawSkillUrl()andgithubSkillUrl()interpolatedirdirectly. I reproducedskills/a#fragmentturning the remainder into a URL fragment andskills/a?download=1turning/SKILL.mdinto query text. The backend installer percent-encodes the repo path, so the app can review/fail on a different URL while install fetches the real path—breaking the exact-preview/exact-install invariant even with one OID. Encode every repo-relative segment for raw and GitHub links (preserving/separators), and test space,#,?,%, and Unicode paths end to end.
normalizeSources() has the same partial validation issue for owner-supplied paths: it omits the backend control-character check and sends path text into URL builders. Share one path normalizer or make the client at least as strict as the server.
Round 1 remains changes requested.
hamzamerzic
left a comment
There was a problem hiding this comment.
Exact-head review — round 2/2: mutation settlement and stale UI (5437897)
Independent pass over install/uninstall settlement, navigation races, accessibility render coverage, and installed provenance. All 80 tests—including the real rendered-card test—pass with shell dependencies, and the app validator bundles cleanly. Mutation state is still not truthful under failure/overlap:
-
Blocker — a successful uninstall can close onto a stale row.
uninstallCurrent()awaitsload(), butload()swallows the refresh failure intoloadErrorand returns no success outcome. The code then always closes the detail while preserving the last-known-goodskillsarray, which still contains the now-deleted skill. The comment claiming the list is “already current” is false on network/HTTP failure. Haveload()return its applied/ok result; after a confirmed DELETE, either remove that id optimistically from authoritative local state or keep a truthful settled-removal state until reconciliation succeeds. Test DELETE success + list failure and superseded reload. -
Blocker — install clears its pending state before authoritative reconciliation.
install()callsonInstalled()without awaiting it, then immediately clearsbusyDir. Until the parent load finishes, the successfully installed card becomes tappable again and still says Install; if the reload fails, it stays that way indefinitely despite the success notice. Await a result-bearing reconciliation or mark the returneddata.nameinstalled optimistically, and keep the action disabled through settlement. Cover POST success + delayed/failed list refresh. -
Mutation completions are not generation/source scoped. While install A is pending, the user can navigate back and open source B or start install B. A’s later completion writes B’s global notice/error and clears the single
busyDir, potentially re-enabling B while B is still pending (same dir names make this especially visible). Serialize installs or track per-operation ids plus a catalog generation, and drop stale presentation writes while still reconciling the global installed list.
Related race: two overlapping refresh() callers can let a superseded older call set refreshing=false while the newer request remains pending. A small ref-count/request-token owner avoids the same premature-idle pattern.
The installed source link should use the same segment encoder requested in round 1; sourcePath is provenance data, not a pre-escaped URL. Also remove the trailing blank line flagged by git diff --check in README before merge.
Together with round 1’s name/path identity defects, this remains changes requested.
hamzamerzic
left a comment
There was a problem hiding this comment.
Independent exact-head follow-up — remaining cross-contract/UI blockers (5437897)
The two fresh reviews already cover catalog identity/URL encoding and mutation settlement. A separate clean-clone pass reproduced four additional blockers on the same head; hosted CI, 80/80 tests, git diff --check, and the core validator are otherwise green.
- Compatibility is false-green above the backend size limit.
assessCompat()never checksSKILL.mdbyte size. A 300,000-byte file reports{ok:true}, while core caps the fetched skill at 256 KiB. Mirror that limit client-side and lock boundary tests; the server remains authoritative. - Installed traversal cannot be complete across the paired contracts. The installer permits nonempty segments containing spaces,
#,?, and%, while shared-list silently omits names outside its narrower path regex and returnsnext_cursor:null.listInstalledFiles()then assesses the partial inventory as complete. Align the contracts or expose the installer-owned boundedfilesinventory on/api/skills; URL encoding alone cannot detect silently omitted files. - The catalog detail header clips at 320 px. An exact-CSS Chromium probe measured
clientWidth=320,scrollWidth=398, with the breadcrumb collapsed to zero while fixed Back/compat/Install/GitHub actions occupy the nonwrapping row. Reflow or stack the controls and add 320/375/390 px coverage. - Detail content also lacks generation ownership. The installed Markdown request can complete after refresh/navigation and overwrite the newer detail. Apply the same newest-generation-wins discipline used by the primary list.
Also avoid eager raw prefetch of hundreds of skills when opening a large source, and include ref in sourceKey so distinct refs of one repo/path cannot share cache state. Core #146 remains a required release pair and is independently held; v2 must not publish first.
…omplete inventory (exact-head re-review) Addresses the three exact-head reviews on this branch: - installIdOf enforces the backend ^[a-z0-9][a-z0-9._-]*$ contract (returns null for a name the installer would 400); treeToSkills marks such cards, and duplicate normalized ids (PDF vs pdf), non-installable — rendered explicitly unsupported with Install disabled instead of firing an install that 400s or races its twin. - rawSkillUrl/githubSkillUrl percent-encode each repo path segment (the backend quotes the same path), so preview/assess and install name the identical URL under one pinned OID; normalizeSources rejects control chars in the path. - assessCompat mirrors the 256 KiB SKILL.md fetch cap (no more false-green above it), sourced from the tree blob size. - Mutation settlement is now truthful: load() returns its applied/ok outcome; a confirmed uninstall drops the id from authoritative local state even if the reconciling reload fails; install awaits the reconciliation (optimistically marking installed) and stays disabled through settlement; installs serialize; presentation writes are token-scoped so a completion in a different source view can't leak its notice/re-enable a sibling; refresh ref-counts so the spinner ends on the last overlapping call. - Installed-skill compatibility reads the authoritative installer files inventory from /api/skills (the paginated shared-list walk can silently omit names); the detail Markdown fetch is newest-generation guarded. - Catalog detail header reflows/stacks its actions instead of clipping at 320px. - Large sources warm only the first screenful of summaries (the rest load on scroll); sourceKey includes ref. Trailing README blank line removed. Tests: catalog identity/encoding/collision/size-cap + the files inventory field. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks — all three exact-head reviews addressed at new head Round 1 — catalog identity & reviewed-byte parity
Round 2 — mutation settlement
Independent follow-up
Release pairing acknowledged: v2 stays held behind core #146; it must not publish first. |
hamzamerzic
left a comment
There was a problem hiding this comment.
Exact-head architecture review — changes requested (8a6c870)
The identity, collision, and encoded catalog-link fixes are substantial, and all tests pass. Three state/contract gaps remain.
Blockers
-
A skill that cannot install is still offered for install.
assessCompat()flags aSKILL.mdover the platform’s hard 256 KiB fetch limit, but the button only requires a truthycompatobject. It remains enabled even though the backend must reject the fetch. -
Installed state has two owners.
installedLocallyonly grows andCatalogScreenremains mounted. After installing in the catalog, closing it, uninstalling from the installed view, and reopening the catalog, the local set can keep the card permanently labelledInstalledafter the authoritative set removed it. -
The installed-detail source link still splices a raw Git path.
current.sourcePathis inserted directly into the GitHub blob URL, so#,?, spaces, and similar characters can make the link identify different bytes or fail. The catalog path fixed this, but the installed path does not reuse that encoder.
Direct fix
Use one closed installability result (loading | installable | unsupported) derived from the same hard contract as the backend; a hard-limit violation is unsupported, not an amber-but-runnable action. Keep one installed-id set at the app root and update that same set from confirmed install/uninstall results and list refreshes—remove the monotonic child overlay. Reuse one segment-encoding Git URL helper everywhere.
Because v2 and its platform API are landing together, the app can require the new authoritative file inventory rather than keeping an incomplete legacy listing fallback.
Verification
- All 87 tests pass on the exact head.
- Hosted CI is green; the missing cases are lifecycle/contract counterexamples rather than test failures.
…d source link Exact-head architecture review (round 4) on 8a6c870 — three state/contract gaps. B1 — a skill that can't install was still offered: the Install button only required a truthy compat object, so a SKILL.md over the hard 256 KiB fetch cap (which the backend rejects) stayed enabled. Adds one closed installability result — installability(skill, compat) → loading | installable | unsupported — derived from the same hard contract (BLOCKING_CAVEATS). A blocking caveat, an invalid id, or a duplicate id is 'unsupported' with Install disabled; soft caveats (dropped/over-budget/scripts/frontmatter) stay installable. Every Install control (cards + detail) derives from this, never from compat presence. B2 — installed state had two owners: CatalogScreen's `installedLocally` set only grew and the screen stayed mounted, so a card could stay labelled Installed after the skill was uninstalled elsewhere. Removed the monotonic overlay; the single installed-id set is the app-root existingIds (from /api/skills), refreshed after every confirmed install and uninstall. Install awaits that refresh with busyDir held, so the card shows "Installing…" through settlement and then reflects the authoritative set both ways. B3 — the installed-detail "source @" link spliced current.sourcePath raw into a GitHub blob URL. It now reuses githubSkillUrl (the same per-segment encoder as the catalog links), so #, ?, spaces etc. can't point at different bytes. Also: v2 and the platform API land together, so both installed-compat effects now require the authoritative `files` inventory and the incomplete listInstalledFiles shared-list fallback (and its fetchJson) is removed. Tests: installability (over-cap unsupported, soft-caveat installable, loading, invalid/duplicate id), BLOCKING_CAVEATS membership, encoded installed-detail link; render-level catalog-card test (over-cap Install disabled+Unsupported, soft-caveat enabled, installed-ness prop-driven both ways). 94 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round-4 blockers fixed at B1 — a skill that can't install is still offered. Added one closed installability result — B2 — installed state had two owners. Removed the monotonic B3 — installed-detail source link spliced a raw git path. The “source @” link now reuses Legacy fallback dropped. Since v2 and the platform API land together, both installed-compat effects now require the authoritative 94 tests pass (0 skipped — the render tests run with frontend deps present); the esbuild bundle compiles. |
hamzamerzic
left a comment
There was a problem hiding this comment.
Approved after two exact-head review rounds against the platform Skills contract.
The app now treats a confirmed install result as authoritative before refresh, keeps one root-owned installed list, holds install busy state through reconciliation, and replaces timing sleeps with a deterministic prefetch completion contract. The exact head passed all 95 tests twice and the app validator reported no errors; its three URL warnings are static false positives because API/raw fetches go through the same-origin proxy and GitHub page URLs are external links.
Summary
v1 was a read-only browser of the agent's skills. v2 keeps that reading experience and adds the write half of the skills story, built on the platform's new skills API.
BOOTSTRAP_SKILLS_MANIFEST_URLtracks this repo'smain, so merging here first would ship an app that calls APIs that don't exist yet — merge the platform PR, then this one. Bootstrap is failure-tolerant either way (a skew logs and retries next boot), but the order avoids the window.What's new
GET /api/skills: both skill shapes (flat<name>.mdand directory<name>/SKILL.md), provenance chips (seed / agent / app-owned / installed-from), 30-day usage counts./api/proxy, background summary prefetch, full-page skill detail with breadcrumbs, install viaPOST /api/skills/install(gated by the newmanage_skillspermission). Sources are app data (sources.json) — adding a repo is a chat request.finding-skillsseed skill is the playbook (sources, fit, trust ritual, exact API calls).DELETE /api/skills/<name>; the server git-snapshots bytes first).POST /api/skills/catalog-index/refresh, keeping the agent's cached catalog index fresh.Structure
catalog.js: dependency-free catalog core (source list, tree filtering, summary parsing, prefetch pool, compat assessment) — network injected, fully unit-tested.npm test): existing domain + detail-nav suites kept, new catalog suite added.mobius.json→ 2.0.0, addscatalog.jstosource_filesand themanage_skillspermission.🤖 Generated with Claude Code