Skip to content

Commit 55b087e

Browse files
yoomlamclaude
andauthored
feat(lik-ui): show full skill instructions from public GitHub repo (#15)
## What Expanding a skill's **Details** on the connections page now shows its full `SKILL.md`, not just the name and description. The instructions are fetched from **GitHub** — the single source of truth (skills deploy *to* Managed Agents from `.claude/skills/<name>/`) — with a plain unauthenticated GET of the public repo's raw content. `beta.skills.versions.download` is a dead end (403 "not supported with this credential type"), so this reads the authoritative file directly. ## How - **`skill_docs.py`** (new) — `fetch_skill_instructions(name, settings, client_factory=None)` builds the raw URL, GETs with a short timeout, returns the text on 200 and `None` on *any* failure (404, non-200, timeout, network error) — never raises. `skill_source_url(...)` builds the human-facing GitHub blob URL (pure, no network). - **`/skill-details` endpoint** — after `describe_skill`, adds `instructions` (the fetched text, or `null`) and an always-present `source_url`. The existing `describe_skill`→502 path is unchanged; a failure never turns into a 5xx from this feature. - **Connections page** — renders the instructions in a `<pre>` via `textContent` (never `innerHTML`, so any Markdown/HTML in the file is shown literally), with a "view on GitHub" link. When the fetch failed, a fallback line links the file on GitHub so the user can still open it. - **Config** — `LIK_UI_SKILLS_REPO` (default `navapbc/leverage-inst-knowl`) and `LIK_UI_SKILLS_REF` (default `main`). The endpoint is top-level `/skill-details` rather than under `/connections/`: a skill is agent-independent — multiple agents can share one — so it isn't a sub-resource of a connection. ## Graceful degradation Every failure mode (repo later going private, GitHub unreachable, path typo) degrades to the "view on GitHub" fallback — never a page or endpoint error. ## Tests - `test_skill_docs.py` (new) — happy path + URL assertion, non-default repo/ref, pure `source_url`, and 404 / 500 / `ConnectError` / `TimeoutException` all returning `None`. - `test_agents.py` — endpoint returns name/description/instructions/source_url; degrades to `instructions: null` with a non-null `source_url` and 200; `describe_skill` failure 502s without attempting the fetch. - Full suite: **147 passing**. Real public-repo paths verified to return 200 for actual skill names. ## Not verified The client-side JS rendering (DOM building, `textContent` escaping) isn't exercised by an automated test — the Python tests cover the endpoint and that the template renders. Worth a manual browser check against a deployed agent before relying on it. ## Follow-ups (deferred) - Rendered Markdown instead of raw text (needs a Markdown dep + HTML-sanitization decision). - Caching the fetched file (align with the existing `describe`-caching TODO if per-expand fetches become a concern). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4a7378e commit 55b087e

8 files changed

Lines changed: 457 additions & 24 deletions

File tree

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
---
2+
title: "feat: lik-ui shows full skill instructions from GitHub"
3+
type: feat
4+
status: completed
5+
date: 2026-07-23
6+
origin: docs/brainstorms/2026-07-22-01-skill-instruction-hosting-requirements.md
7+
---
8+
9+
# feat: lik-ui shows full skill instructions from GitHub
10+
11+
## Summary
12+
13+
Make lik-ui's connections page show a skill's full `SKILL.md` on demand by fetching it from the
14+
**public** GitHub repo (raw content, no auth), rather than from Managed Agents. The existing
15+
`/connections/skill` JSON endpoint gains an `instructions` field populated from
16+
`.claude/skills/<name>/SKILL.md` on `main`; the connections page renders it (escaped) beneath the
17+
skill's name/description, with a graceful "unavailable" fallback when the fetch fails.
18+
19+
---
20+
21+
## Problem Frame
22+
23+
lik-ui can show each skill's name and description but not its full instructions. The original blocker
24+
`beta.skills.versions.download` 403s and no download-capable credential was identified — was closed
25+
as won't-fix in favor of reading from GitHub, which is the single source of truth (see origin R7 and
26+
docs/plans/2026-07-23-001-...-plan.md "Deferred to Follow-Up Work"). That plan deferred this feature
27+
pending one unknown: repo visibility. **Resolved during planning: the repo (`navapbc/leverage-inst-knowl`)
28+
is public**, so the fetch needs no token, no OAuth, and no bundled-files workaround — the deferral
29+
condition is gone.
30+
31+
---
32+
33+
## Requirements
34+
35+
- R1. lik-ui's "show full instructions" renders `SKILL.md` sourced from GitHub, not a Managed Agents download. *(origin R7)*
36+
- R2. The connections page shows a skill's full `SKILL.md` on demand, and degrades gracefully (a fallback message, never a page/endpoint error) when the file can't be fetched.
37+
- R3. No new secret or auth: the feature reads the public repo's raw content.
38+
- R4. When the fetch fails for any reason (including the repo later becoming private), the fallback shows a link to the skill's `SKILL.md` on GitHub so the user can open it themselves.
39+
40+
**Origin actors:** A5 lik-ui viewer. **Origin flows:** F3 view instructions in lik-ui.
41+
42+
---
43+
44+
## Scope Boundaries
45+
46+
- Only `SKILL.md` is shown — not the skill's reference files (those are the agent's on-demand bundle, not a lik-ui viewing concern).
47+
- No Markdown-to-HTML rendering — the raw `SKILL.md` text is shown escaped. Rich rendering is a separate nice-to-have.
48+
- Read-only viewer — no editing of instructions in lik-ui (rejected in the brainstorm).
49+
50+
### Deferred to Follow-Up Work
51+
52+
- **Rendered Markdown** (headings, lists, links) instead of raw text — needs a Markdown dependency and an HTML-sanitization decision; separate PR.
53+
- **Caching the fetched `SKILL.md`** — align with the existing "cache agent `describe` results" TODO in `lik-ui/README.md` if per-expand GitHub fetches become a concern; separate PR.
54+
55+
---
56+
57+
## Context & Research
58+
59+
### Relevant Code and Patterns
60+
61+
- `lik-ui/src/lik_ui/agents.py``describe_skill(skill_id, version) -> {name, description}` (the `name` equals the repo skill-dir name — see Key Decisions) and the `GET /connections/skill` endpoint (agents.py:136) returning that as JSON. This endpoint is the integration point.
62+
- `lik-ui/src/lik_ui/oauth_connector.py:126-127` — the **pattern to mirror**: an injected `client_factory or (lambda: httpx.AsyncClient(timeout=10))` so tests supply an `httpx.MockTransport`-backed client. `httpx` is already a lik-ui dependency (used here).
63+
- `lik-ui/src/lik_ui/templates/connections.html` (skill `<details>` list, `.skill-details-btn`, and the `fetch("/connections/skill?...")` JS at ~line 88-113) — where the fetched JSON is rendered inline; extend it to show `instructions`.
64+
- `lik-ui/src/lik_ui/settings.py``LIK_UI_`-prefixed pydantic `BaseSettings`; add the repo/ref config here.
65+
- `.claude/skills/<name>/SKILL.md` — the files to fetch; deployed from `main`.
66+
67+
### Institutional Learnings
68+
69+
- None yet (`docs/solutions/` is empty).
70+
71+
---
72+
73+
## Key Technical Decisions
74+
75+
- **Fetch raw content from the public repo; no auth.** Resolves the origin's deferred visibility question — the repo is public, so `https://raw.githubusercontent.com/<repo>/<ref>/.claude/skills/<name>/SKILL.md` is readable with a plain GET. This is the mechanism origin R7 called for, at minimum cost.
76+
- **Address the file by skill *name*, not skill_id.** The deploy pipeline enforces `display_title == SKILL.md name == directory name` (docs/plans/2026-07-23-001-...-plan.md U1), so `describe_skill(...)["name"]` maps directly to `.claude/skills/<name>/SKILL.md`. No id→path lookup needed.
77+
- **Show raw, escaped text — no Markdown rendering.** Avoids a new dependency and any HTML-injection surface; the connections JS renders via `textContent` (not `innerHTML`). Rich rendering is deferred.
78+
- **Graceful degradation, mirroring the connections page.** A failed fetch (404, non-200, timeout, network error) yields `instructions: null` and a fallback line — it never turns the endpoint into a 502 or breaks the page. `describe_skill` failures keep their existing 502 behavior.
79+
- **Always expose a human-facing GitHub link.** The endpoint returns a `source_url` (the GitHub *blob* URL — `https://github.com/{repo}/blob/{ref}/.claude/skills/{name}/SKILL.md`) regardless of fetch success. When `instructions` is present it's a "view on GitHub" affordance; when the fetch failed (e.g., the repo went private and now 404/401s for the app) the fallback links it so the user can open it themselves (R4). The blob URL renders in a browser for anyone on a public repo, and for logged-in authorized users if it's private.
80+
- **Config with public defaults.** `LIK_UI_SKILLS_REPO` (default `navapbc/leverage-inst-knowl`) and `LIK_UI_SKILLS_REF` (default `main`) let a dev point at a fork/branch without code changes.
81+
- **Injectable `client_factory` for the fetcher**, mirroring `oauth_connector.py`, so tests drive it with `httpx.MockTransport`.
82+
83+
---
84+
85+
## Open Questions
86+
87+
### Resolved During Planning
88+
89+
- **Repo visibility** (the origin's deferred blocker): public → raw fetch, no token/bundling.
90+
- **How to address the file**: by skill name, which the deploy pipeline guarantees equals the dir/path.
91+
92+
### Deferred to Implementation
93+
94+
- **Exact endpoint response shape** for `instructions` (e.g., `null` vs an explicit `{available: false}`): pick whatever the connections JS renders most simply; both satisfy R2.
95+
- **Version skew note**: the shown `SKILL.md` is current `main`, which may differ from the exact skill *version* pinned on the agent. Since agents pin `latest` and skills deploy from `main`, `main` ≈ the deployed instructions; acceptable. Surface a caption ("from `main`") if skew is ever confusing.
96+
97+
---
98+
99+
## Implementation Units
100+
101+
- U1. **GitHub `SKILL.md` fetcher**
102+
103+
**Goal:** A function that fetches a skill's `SKILL.md` text from the public repo by skill name, returning the text or `None` on any failure.
104+
105+
**Requirements:** R1, R3
106+
107+
**Dependencies:** None
108+
109+
**Files:**
110+
- Create: `lik-ui/src/lik_ui/skill_docs.py`
111+
- Modify: `lik-ui/src/lik_ui/settings.py` (add `skills_repo`, `skills_ref` with defaults)
112+
- Test: `lik-ui/tests/test_skill_docs.py`
113+
114+
**Approach:**
115+
- `fetch_skill_instructions(name, settings, client_factory=None) -> str | None`: build the *raw* URL `https://raw.githubusercontent.com/{settings.skills_repo}/{settings.skills_ref}/.claude/skills/{name}/SKILL.md`, GET with a short timeout, return `resp.text` on 200, else `None`. Catch `httpx` transport/timeout errors and return `None` — never raise.
116+
- `skill_source_url(name, settings) -> str`: build the human-facing GitHub *blob* URL `https://github.com/{settings.skills_repo}/blob/{settings.skills_ref}/.claude/skills/{name}/SKILL.md` (pure string builder, no network) for the "view on GitHub" / fallback link (R4).
117+
- `client_factory` defaults to `lambda: httpx.AsyncClient(timeout=10)`, injectable for tests (mirror `oauth_connector.py`).
118+
119+
**Patterns to follow:** `lik-ui/src/lik_ui/oauth_connector.py:126-127` (injected client factory + `httpx.MockTransport` in tests); `lik-ui/src/lik_ui/settings.py` (BaseSettings field with default).
120+
121+
**Test scenarios:** *(httpx.MockTransport-backed client)*
122+
- Happy path: transport returns 200 with a body → returns exactly that text; assert the requested URL is the expected raw path for a given name + default repo/ref.
123+
- Edge: non-default `skills_repo`/`skills_ref` → both the raw fetch URL and `skill_source_url` reflect them.
124+
- `skill_source_url`: returns the expected `github.com/.../blob/<ref>/.claude/skills/<name>/SKILL.md` for a given name (pure, no network).
125+
- Error path: 404 → returns `None`.
126+
- Error path: 500 → returns `None`.
127+
- Error path: `httpx.ConnectError` / `httpx.TimeoutException` raised by the transport → returns `None` (does not propagate).
128+
129+
**Verification:** `cd lik-ui && uv run pytest tests/test_skill_docs.py` passes; the function never raises on network/HTTP failure.
130+
131+
---
132+
133+
- U2. **Surface instructions on the connections page**
134+
135+
**Goal:** Extend `/connections/skill` to include the fetched `SKILL.md`, and render it (escaped) in the skill-details view with a graceful fallback.
136+
137+
**Requirements:** R1, R2, R4
138+
139+
**Dependencies:** U1
140+
141+
**Files:**
142+
- Modify: `lik-ui/src/lik_ui/agents.py` (the `skill_details` endpoint — add `instructions` and `source_url`)
143+
- Modify: `lik-ui/src/lik_ui/templates/connections.html` (render `instructions` beneath name/description via `textContent`; show a fallback line **with the `source_url` link** when `null`; also show a "view on GitHub" link when present)
144+
- Test: `lik-ui/tests/test_agents.py` (extend — it already fakes `AgentsClient`)
145+
146+
**Approach:**
147+
- In `skill_details`: after `describe_skill(...)` succeeds, compute `source_url = skill_source_url(details["name"], settings)` (always) and `instructions = await fetch_skill_instructions(details["name"], settings)` (`None` when unavailable); add both to the JSON. Keep the existing `describe_skill`-failure → 502 path unchanged.
148+
- Frontend: in the existing `fetch("/connections/skill?...")` success handler — when `instructions` is present, append a `<pre>` whose `textContent` is `res.d.instructions`, plus a "view on GitHub" link to `source_url`; when `null`, render a fallback line like "Full instructions unavailable — view on GitHub" linking `source_url`. Use `textContent`, never `innerHTML` (escaping); the link's `href` is `source_url`.
149+
150+
**Patterns to follow:** the existing `skill_details` endpoint and the connections.html skill-details JS (agents.py:136, connections.html ~88-113); FastAPI `TestClient` usage already in `lik-ui/tests/`.
151+
152+
**Test scenarios:**
153+
- Happy path (endpoint): `describe_skill` returns a name + a monkeypatched/injected fetch returning body → JSON has `name`, `description`, `instructions == body`, and `source_url` = the blob URL. *Covers F3.*
154+
- Edge (endpoint): fetch returns `None` → JSON has `instructions: null` **and a non-null `source_url`**, HTTP 200 (page shows the fallback link). *Covers R4.*
155+
- Error path (endpoint, unchanged): `describe_skill` raises → 502 with the existing `detail` shape, and no fetch is attempted.
156+
- Integration/frontend: rendering is JS driving `textContent` — verify manually (see Verification) that the instructions block appears on expand, that raw Markdown/HTML in `SKILL.md` is shown literally, and that the fallback link points at `source_url`.
157+
158+
**Verification:** With a real agent that has a deployed skill, expanding "Details" shows its `SKILL.md` plus a working GitHub link; pointing repo/ref at a nonexistent path shows the fallback with a clickable GitHub link and does not break the page; endpoint tests pass.
159+
160+
---
161+
162+
- U3. **Flip the lik-ui README section to DONE**
163+
164+
**Goal:** Update the "show full skill instructions" section from TODO to done, describing the public-repo raw fetch.
165+
166+
**Requirements:** R1
167+
168+
**Dependencies:** U2
169+
170+
**Files:**
171+
- Modify: `lik-ui/README.md` (the `## TODO: show full skill instructions (SKILL.md)` section)
172+
173+
**Approach:** Documentation only. Note that instructions are read from the public repo's raw `SKILL.md` (source of truth), configurable via `LIK_UI_SKILLS_REPO`/`LIK_UI_SKILLS_REF`, and that rendered Markdown/caching are deferred. Supersedes the "deferred pending repo visibility" note added in docs/plans/2026-07-23-001-...-plan.md's U3.
174+
175+
**Patterns to follow:** the existing README section style (the "DONE:" heading convention already used elsewhere in this README).
176+
177+
**Test scenarios:** Test expectation: none — documentation change, no runtime behavior.
178+
179+
**Verification:** The README section reflects the shipped behavior and points at the two config vars.
180+
181+
---
182+
183+
## System-Wide Impact
184+
185+
- **Interaction graph:** Adds one outbound HTTP dependency (GitHub raw) reached only when a user expands a skill's details. No change to auth, chat, vault, or MCP paths.
186+
- **Error propagation:** Fetch failures are contained in U1 (return `None`); the endpoint and page degrade to a fallback, never a 500/502 from this feature.
187+
- **State lifecycle risks:** None — read-only, no persistence.
188+
- **API surface parity:** Only the `/connections/skill` JSON gains a field; additive, no breaking change to the endpoint's existing consumers.
189+
- **Unchanged invariants:** `describe_skill`'s 502 behavior, login-gating on `/connections/skill`, and the connections page's existing name/description rendering are unchanged.
190+
191+
---
192+
193+
## Risks & Dependencies
194+
195+
| Risk | Mitigation |
196+
|------|------------|
197+
| GitHub raw is slow/unreachable, delaying the details view. | Short timeout in the fetcher; failure → fallback, not a hang or error. Caching is a deferred follow-up if it matters. |
198+
| Shown `SKILL.md` (from `main`) differs from the skill version pinned on the agent. | Agents pin `latest` and skills deploy from `main`, so they converge; add a "from `main`" caption only if skew confuses users (deferred detail). |
199+
| Repo becomes private later, breaking the unauthenticated fetch. | The fetch degrades to the fallback, which links the skill's GitHub URL so the user can open it themselves (R4). Documented dependency on public visibility (R3); a durable fix (server-side token) is a localized change behind the fetcher's `client_factory` seam. |
200+
| Markdown/HTML in `SKILL.md` injected into the page. | Render via `textContent`/escaped `<pre>`, never `innerHTML`. |
201+
202+
---
203+
204+
## Sources & References
205+
206+
- **Origin document:** [docs/brainstorms/2026-07-22-01-skill-instruction-hosting-requirements.md](docs/brainstorms/2026-07-22-01-skill-instruction-hosting-requirements.md) (R7)
207+
- Related plan: [docs/plans/2026-07-23-001-feat-skill-instruction-deploy-pipeline-plan.md](docs/plans/2026-07-23-001-feat-skill-instruction-deploy-pipeline-plan.md) (deferred this feature; enforces skill name == dir == path)
208+
- Related code: `lik-ui/src/lik_ui/agents.py`, `lik-ui/src/lik_ui/oauth_connector.py`, `lik-ui/src/lik_ui/templates/connections.html`

lik-ui/README.md

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -78,23 +78,26 @@ one call, but the agent definition (system prompt, model, declared servers) chan
7878
If the agent list grows, cache these results (e.g. a short TTL) rather than fetching per
7979
request.
8080

81-
## TODO: show full skill instructions (SKILL.md)
82-
83-
The connections page can show each skill's name and description, but not its full instructions
84-
(SKILL.md). `describe_skill` returns name and description only.
85-
86-
**Read the instructions from GitHub, not from Managed Agents.** GitHub is the single source of truth
87-
for skill instructions (they are deployed *to* Managed Agents from `.claude/skills/<name>/` — see
88-
[`scripts/README.md`](../scripts/README.md)). Do **not** try to fetch them back via
89-
`beta.skills.versions.download`: that endpoint returns 403 "Downloading skill content is not supported
90-
with this credential type", and the download-capable credential type was not identified — it's a
91-
dead end and unnecessary. The full-instructions view should render the authoritative
92-
`.claude/skills/<name>/SKILL.md` from GitHub instead.
93-
94-
Deferred until the fetch approach is chosen: if this repo is private, lik-ui needs a server-side
95-
read-only GitHub token (or the skill files bundled into its image at build time) to read the file.
96-
See `docs/plans/2026-07-23-001-feat-skill-instruction-deploy-pipeline-plan.md` (Deferred to Follow-Up
97-
Work).
81+
## DONE: show full skill instructions (SKILL.md)
82+
83+
Expanding a skill's "Details" on the connections page shows its full `SKILL.md` alongside the
84+
name and description. The instructions come from **GitHub**, the single source of truth (skills
85+
are deployed *to* Managed Agents from `.claude/skills/<name>/` — see
86+
[`scripts/README.md`](../scripts/README.md)), not from Managed Agents:
87+
`beta.skills.versions.download` is a dead end (it 403s with "Downloading skill content is not
88+
supported with this credential type"). `skill_docs.py` fetches the raw
89+
`.claude/skills/<name>/SKILL.md` from the **public** repo with a plain unauthenticated GET —
90+
addressed by skill *name*, which the deploy pipeline guarantees equals the directory. The repo
91+
and ref are configurable via `LIK_UI_SKILLS_REPO` (default `navapbc/leverage-inst-knowl`) and
92+
`LIK_UI_SKILLS_REF` (default `main`).
93+
94+
Any fetch failure (404, non-200, timeout, or the repo later going private) degrades gracefully:
95+
the view shows a fallback line linking the file on GitHub so the user can open it themselves,
96+
never a page or endpoint error. The text is rendered escaped (via `textContent`), so Markdown/HTML
97+
in the file is shown literally.
98+
99+
Deferred: rendering the Markdown (headings/lists/links) instead of raw text, and caching the
100+
fetched file (align with the `describe`-caching TODO above if per-expand fetches become a concern).
98101

99102
## TODO: decide how users get Anthropic API access
100103

lik-ui/src/lik_ui/agents.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import Protocol
1010

1111
from .settings import Settings
12+
from .skill_docs import fetch_skill_instructions, skill_source_url
1213
from .sources import normalize_url
1314
from .vault import VaultClient, ensure_user_vault
1415

@@ -25,8 +26,9 @@ def describe(self, agent_id: str) -> dict:
2526
def describe_skill(self, skill_id: str, version: str) -> dict:
2627
"""Return a skill version's human-readable details: ``{"name": str, "description": str}``.
2728
The agent definition only carries a skill's id/version; its name and description live on
28-
the skill version and are fetched on demand. (Full instructions/SKILL.md are not shown yet
29-
— see the README TODO on the download credential limitation.)"""
29+
the skill version and are fetched on demand. (The full SKILL.md is not part of this
30+
lookup — the ``/skill-details`` endpoint fetches it from the public GitHub repo by
31+
skill name; see ``skill_docs.py``.)"""
3032
...
3133

3234

@@ -133,11 +135,18 @@ async def connections(request: Request, agent_id: str):
133135
},
134136
)
135137

136-
@app.get("/connections/skill")
138+
@app.get("/skill-details")
137139
async def skill_details(request: Request, skill_id: str, version: str):
138140
require_user(request) # gate behind login, same as the connections page
139141
try:
140142
details = request.app.state.agents_client.describe_skill(skill_id, version)
141143
except Exception as exc: # noqa: BLE001 - surface SDK errors as JSON, not a 500
142144
return JSONResponse({"detail": f"Could not load skill: {exc}"}, status_code=502)
145+
# The full SKILL.md is read from the public GitHub repo by skill name (which equals the
146+
# skill's directory). source_url is always present (the "view on GitHub" affordance and
147+
# the fallback link); instructions is None when the fetch failed — the page degrades to
148+
# the fallback rather than erroring.
149+
settings: Settings = request.app.state.settings
150+
details["source_url"] = skill_source_url(details["name"], settings)
151+
details["instructions"] = await fetch_skill_instructions(details["name"], settings)
143152
return JSONResponse(details)

0 commit comments

Comments
 (0)