skills
CLAUDE.md1 starsChanged 30 days agoApache-2.0View on GitHub
Agent skills for the SLICC runtime. Each skill lives in skills/<name>/ with a required SKILL.md and optional scripts/*.jsh, references/, assets/. SKILL.md frontmatter: name, description, allowed-tools: bash, optionally command: + script: to wire the shell command explicitly. The description doubles as the activation prompt — make it long and trigger-phrase-rich. This file covers .jsh scripts, page observers, and sprinkle UIs. The script guidance is distilled from the slicc runtime source (ground truth), all 31 .jsh files in this repo, and 175…
We split this CLAUDE.md into the rules it gives an agent
11 rules. About 0 are common, 4 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 13
No wall-clock timeout. Scripts are killable by signal: SIGINT→130, SIGKILL→137, SIGTERM→143.
- Very specific: line 21
NOT present: process.platform/.arch/.version/.pid/.on/.nextTick, console.debug/.table/.dir, bare fs/path/exec globals (all bare capability …
- Very specific: line 26
Every other built-in throws (http/https/net/tls/dns/zlib/worker_threads, …): use fetch() for HTTP.
- Very specific: line 212
When a detail endpoint nests what a list endpoint flattens, normalize once at the fetch site ({...data, ...(data.summaryDTO || {})}, …
- Very specific: line 219
Every command supports --json → cli.out(data) with the raw response, early-return.
- Very specific: line 233
Every advertised flag must change behavior, and help text / SKILL.md / code stay in lockstep — flag-parses-but-does-nothing and doc drift …
- Distinctive: line 238
No direct pushes to main — every skill change goes through a PR (#56/#57).
- Distinctive: line 240
When parallel agents produce duplicate PRs, the first-opened, independently verified one wins — even over a more complete later duplicate …
- Very specific: line 241
Before "fixing" an assumption about the runtime, read the slicc source — the scripts/-dir "fix" was a no-op because discovery is recursive …
- Very specific: line 292
Clean up instrumentation in
finally: Remove the exact listeners/interceptors you installed, including capture listeners that call … - Distinctive: line 307
What the job proves: Every skill the PR touched has at least one tst file.
≈ “Common” is an estimate: at least 1 in 5 of the 25,680 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md — ai-ecoverse/skills
Agent skills for the SLICC runtime. Each skill lives in skills/<name>/ with a required SKILL.md and optional scripts/*.jsh, references/, assets/. SKILL.md frontmatter: name, description, allowed-tools: bash, optionally command: + script: to wire the shell command explicitly. The description doubles as the activation prompt — make it long and trigger-phrase-rich.
This file covers .jsh scripts, page observers, and sprinkle UIs. The script guidance is distilled from the slicc runtime source (ground truth), all 31 .jsh files in this repo, and 175 PRs of review history. Precedence when sources disagree: slicc source code > this file > header comments in old scripts > slicc's docs/*.md (several are drifted — shell-reference.md's QuickJS claim is wrong, and node-compat-shims.md overstates the process/crypto/assert/util shims). When in doubt about the runtime surface, probe it (typeof process.argv.parseFlags) — half the fleet's header comments fossilized a mid-migration state and are wrong.
1. Execution model
- A
.jshfile is JavaScript, not bash. It runs in a DedicatedWorker (V8), compiled as the body of anAsyncFunction— so top-levelawaitand top-levelreturnare both legal. - Command discovery: any
*.jshanywhere under the skill dir becomes a shell command named after its basename (scripts/foo.jsh→foo). Thescripts/subdir is convention, not required. First-found basename wins;/workspace/skillsis scanned first; precedence at dispatch is built-in >.jsh> saved workflow. process.argvis['node', <scriptPath>, ...args].- stdout/stderr are buffered and delivered when the run completes — don't design around streaming progress output.
- No wall-clock timeout. Scripts are killable by signal: SIGINT→130, SIGKILL→137, SIGTERM→143.
- Exit codes: clean completion → 0;
process.exit(N)/cli.die→ N; any uncaught throw → 1 with the stack on stderr; missing script → 127. process.exit()andcli.die()don't halt synchronously — they throwNodeExitErrorto unwind. See §6 for the mandatory re-throw rule.
2. Globals and require()
Available globals: process (argv + non-enumerable argv.parseFlags(), env, cwd(), exit(), one-shot buffered stdin, stdout/stderr with isTTY), console (only log/info → stdout, warn/error → stderr; objects are JSON.stringify'd, not inspected), fetch (bridged through the kernel — CORS bypass and server-side secret unmasking; not the page's fetch), Buffer (polyfill), __dirname/__filename, and the worker's web globals (URL, URLSearchParams, TextEncoder/TextDecoder, Blob, FormData, Headers/Request/Response, atob/btoa, Web Crypto, AbortController, setTimeout/setInterval/queueMicrotask).
NOT present: process.platform/.arch/.version/.pid/.on/.nextTick, console.debug/.table/.dir, bare fs/path/exec globals (all bare capability globals were hard-cut to require('sliccy:*'), issue #168).
require() resolves, in order: sliccy:* → served node builtins → everything else.
- Served builtins (
kernel/realm/node-builtins.ts):fs,fs/promises,path,crypto,child_process,process,buffer,assert,assert/strict,util,events,os,stream,url,tty,readline,readline/promises,module,vm.fsis the VFS bridge:readFile,writeFile,readFileBinary,writeFileBinary,readDir,exists,stat,mkdir,rm,fetchToFile— methods live directly on the object (not only under.promises); nowatch, no streams. Sync reads cap at 1 MB (ENOSYNC) — use asyncreadFilefor large files. - Every other built-in throws (
http/https/net/tls/dns/zlib/worker_threads, …): usefetch()for HTTP. - npm packages resolve only from ipk-installed VFS
node_modules(ipk install x) — no CDN fallback. Native packages (sharp,sqlite3,puppeteer,canvas, …) hard-throw.require('playwright')returns a CDP-backed shim. - Shim gaps vs real Node:
crypto.createHashsupports only md5/sha1/sha256;utilhas onlyformat/formatWithOptions/inspect/inherits/promisify;assertlacksrejects/match.
3. sliccy: module quick reference
const exec = require('sliccy:exec');
exec(cmd) // → {stdout, stderr, exitCode} — runs a REAL shell (injection risk, §7)
exec.spawn(argv[]) // no shell parsing — use for anything containing a variable
exec.start(cmd|argv, {stdin, stdinKind, args}) // → {kill(sig), stdin:{write,end}, done}
const agent = require('sliccy:agent');
agent(prompt, {model, thinking, schema, cwd, allowedCommands, readOnly}) // throws on non-zero exit / schema failure
agent.spawn(prompt, opts) // → {finalText, exitCode, stderr}, never throws
const skill = require('sliccy:skill');
skill.dir; skill.refs; skill.assets // script-relative paths — never hardcode /workspace/skills/...
await skill.config() // null if absent — Promise is truthy, so ALWAYS (await skill.config()) || {}
await skill.config({key: val}) // shallow-merge + persist
await skill.token(providerId) // OAuth broker
const http = require('sliccy:http');
const api = http.client({baseUrl, token, headers, retry: {on: [429, 503], maxAttempts: 3}, timeoutMs});
await api.get(path, {params, headers}) // .post/.put/.patch/.delete; body objects auto-JSON'd
await api.get(path, {raw: true}) // → {body, headers, status} instead of parsed body
// throws HttpError{status, statusText, url, body} on non-2xx; honors Retry-After; normalizes a
// missing leading slash (hand-rolled BASE+path concat produced "api.github.comuser", PR #127);
// retry shape is {on, maxAttempts} — {attempts, backoff} is silently ignored;
// repeated query params: pass an array in params — never comma-join (Gmail metadataHeaders bug)
const browser = require('sliccy:browser');
await browser.findTab({urlMatch: /strava\.com/}) // → tab | null; prefer urlMatch over {domain} —
// domain:'icloud.com' missed www.icloud.com; escape dots
await browser.ensureTab(url, {matchUrl})
await browser.eval(tab, fnOrString) // pass a real Function when possible — auto-serialized to an IIFE
await browser.evalAsync(tab, fnOrString) // async, transparent double-JSON unwrap host-side
await browser.cookie(tab, name); await browser.localStorage(tab, key)
await browser.fetch(tab, url, {method, headers, body, responseType}) // runs INSIDE the tab's origin —
// session cookies automatic; → {ok, status, headers, body, bodyEncoding?}
browser.websocket.on(tab, {urlMatch}).filter({parseAs, where, project}).forward({sink})
// sinks: webhook|scoop|vfs|log; filters are declarative JSON only
const cli = require('sliccy:cli');
cli.die(msg, {exitCode, prefix}) // "Error: msg" red to stderr, exits (default 1); prefix:'' suppresses the label
cli.out(value) // string → stdout; else pretty JSON — the --json output primitive
cli.warn(msg); cli.help(text) // help → stdout, exit 0
const color = require('sliccy:color'); // .green .red .yellow .gray .bold .cyan .dim
// identity functions when !TTY or NO_COLOR — never gate logic on color
const time = require('sliccy:time'); // parseDuration/ago/range/future — 'm' = MINUTES, 'M' = months
const fmt = require('sliccy:fmt'); // trunc/col/table (ANSI-aware), date(v, 'short'|'iso'|'human'|'locale')
const pool = require('sliccy:pool'); // pool(concurrency, items, fn) — bounded fan-out, results in order4. Canonical script skeleton
Best exemplars in-repo: skills/strava/scripts/strava.jsh, skills/swarm/scripts/swarm.jsh, skills/garmin/scripts/garmin.jsh (richest formatters + token lifecycle), skills/da-live/da-live.jsh (smallest complete one).
// mytool.jsh — one-line description (uses browser session)
const browser = require('sliccy:browser');
const cli = require('sliccy:cli');
const color = require('sliccy:color');
const fmt = require('sliccy:fmt');
const HELP = `
mytool — what it does
USAGE
mytool list [--limit N] ...
mytool get <id> ...
FLAGS
--json Output raw JSON
REQUIRES
example.com open and logged in in your browser
`.trim();
// ── args ──────────────────────────────────────────────────────────────
const parsed = process.argv.parseFlags(); // {positional, flags, subcommand, passthrough}
const subcommand = parsed.subcommand || '';
const positional = parsed.positional.slice(1); // drop the leading subcommand
const flags = parsed.flags; // booleans are real `true`; repeated flags become arrays
// ── session ───────────────────────────────────────────────────────────
let _tab = null; // memoize — SSO redirects move tabs off-domain mid-flow
async function getTab() {
if (_tab) return _tab;
_tab = await browser.findTab({ urlMatch: /example\.com/ });
if (!_tab) cli.die('open example.com in your browser first', { prefix: 'mytool' });
return _tab;
}
async function apiFetch(tab, path, opts = {}) { // ONE wrapper — uniform auth-expiry guidance for every command
const res = await browser.fetch(tab, `https://example.com${path}`, {
...opts,
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/plain, */*', ...(opts.headers || {}) },
});
if (res.status === 401 || res.status === 403)
cli.die('session expired — log in to example.com in your browser, then retry', { prefix: 'mytool' });
if (!res.ok) cli.die(`example returned ${res.status} for ${path}`, { prefix: 'mytool' });
return res.body;
}
// ── commands ──────────────────────────────────────────────────────────
async function cmdList(tab, flags) {
const parsedLimit = parseInt(flags.limit ?? flags.l, 10);
const limit = Number.isFinite(parsedLimit) ? Math.min(Math.max(parsedLimit, 1), 50) : 10;
const data = await apiFetch(tab, `/api/items?limit=${limit}`);
if (flags.json) { cli.out(data); return; }
const items = Array.isArray(data) ? data : (data.items || data.results || []);
if (!items.length) { console.log(color.dim(' No items found.')); return; }
for (const it of items) console.log(` ${color.cyan(color.bold(it.name))} ${color.dim(`id:${it.id}`)}`);
}
async function cmdGet(tab, positional, flags) {
const id = positional[0];
if (!id) cli.die('usage: mytool get <id>', { prefix: 'mytool' });
const data = await apiFetch(tab, `/api/items/${encodeURIComponent(id)}`);
if (flags.json) { cli.out(data); return; }
const item = data.item || data; // shape-shifting APIs — §9
console.log(` ${color.cyan(color.bold(item.name || 'Item'))} ${color.dim(`id:${item.id ?? id}`)}`);
}
// ── main ──────────────────────────────────────────────────────────────
async function main() {
if (flags.help || flags.h || !subcommand || subcommand === 'help') { cli.help(HELP); }
const tab = await getTab();
try {
if (subcommand === 'list') await cmdList(tab, flags);
else if (subcommand === 'get') await cmdGet(tab, positional, flags);
else cli.die(`unknown command: ${subcommand}\nRun 'mytool --help' for usage.`, { prefix: 'mytool' });
} catch (err) {
if (err?.name === 'NodeExitError') throw err; // MANDATORY — see §6
cli.die(err.message, { prefix: 'mytool' });
}
}
await main();On parseFlags: process.argv.parseFlags() is real and current (a non-enumerable helper on argv). It briefly disappeared during the bare-globals→sliccy: migration, which is why gh.jsh/strava.jsh carry local reimplementations and comments calling it "genuinely gone" while icloud.jsh calls it "a real bare global" — the latter is correct today. New scripts use the runtime helper. Don't propagate the local copies or the fossil comments.
5. Auth patterns, in order of preference
- Browser session, token never leaves the page — the house style (strava, jira, slack, suno, navan, teams, linkedin, …).
browser.fetchin the tab origin carries cookies automatically; when a bearer token must be read fromwindow/localStorage, mint and use it insideevalAsyncrather than returning it to the realm. skill.token(provider)OAuth broker (aem, gh) — pair with an upfrontcli.dielisting every credential fallback when none resolves; an empty token surfacing later as an opaque 401 was a P1 (gh.jsh).skill.config()persisted tokens with refresh + expiry skew (garmin).- Env vars (gmail's
GWS_*). - Config files on disk — least preferred; if displaying a token, truncate it.
External-CLI-plus-manual-secret-wiring skills lost historically (#52 abandoned); reuse-the-browser-session won.
6. Error handling
cli.die(msg, {prefix: '<tool>'})with an actionable message: say what the user should do ("log in to X and retry"), not what the code couldn't do.- The NodeExitError rule (most-repeated bug in repo history):
cli.die/process.exitthrowNodeExitErrorto unwind. Everycatchthat wraps command dispatch or an operation containing adiemust re-throw it first:if (err?.name === 'NodeExitError') throw err;Otherwise you reprint a confusing second error (garmin), downgrade a real auth failure to a warning (gmail), or neutralize a security guard entirely (slack's duplicate-watch check silently swallowed → leaked webhooks). awaitbefore falsy fallbacks:skill.config() || {}never falls back — a Promise is always truthy.(await skill.config()) || {}(confirmed-live garmin bug).- Validate required args at the top of each command with a usage line. Check auth errors before exit codes when shelling out —
curlreturns 0 on HTTP 401 (aem.jsh). - Exit non-zero on failure even in
--jsonmode (#76).
7. Security (every rule here has a P1 behind it)
exec()runs a real shell — proven live: a value ofx; echo INJECTEDexecuted the injected command. Preferexec.spawn([bin, ...args])for anything containing a variable. If you must build a shell string, validate each interpolated value against a strict allowlist (/^[A-Za-z0-9._-]+$/for names,/^[A-Za-z0-9._/-]+$/for paths) or single-quote it with'\''escaping (seeshellQuotein aem.jsh, validators in slack.jsh).- Embed values into page-eval JS via
JSON.stringify(value), never string concatenation. Validate IDs structurally first (urn:li:activity:<digits>, linkedin.jsh). - Never post-process
JSON.stringifyoutput with regex escaping — single-quote escaping is a no-op and backslash-doubling corrupts\". When hand-escaping is unavoidable, escape backslashes first. Escape XML metacharacters in constructed XML (&broke concur). - No hardcoded IDs, no silent fallback to a default identity. Resolve the current user/athlete/mailbox dynamically and
dieloudly on failure — the single most repeated P1 (gh, linkedin, strava, slack all shipped the author's own IDs as fallbacks). - Nothing secret to stdout — everything a
.jshprints lands in the agent transcript (#203). - Gate spending/destructive operations behind
--confirmwith a preview (navan is the reference; also--dry-run, jira). Paginate fully before any destructive operation on the list (#21). Bound fan-out withpool()—Promise.allover 100+ calls hits rate limits (#53). - Quote (don't rewrite) remote-controlled strings echoed into suggested commands — branch names are attacker-controlled (#48), but sanitizing broke legitimate
feature@2(#49).
8. The bridge rules (the jsh-specific bug class)
- Serialize exactly once across
evalAsync. Return raw values from the page; the host does a transparent double-JSON unwrap. Page-sideJSON.stringify+ realm-sideString()produced''and'[object Object]'(speck). Defensive guard on receipt:typeof raw === 'string' ? JSON.parse(raw) : raw(suno). - String-form eval must be an invoked IIFE —
"(async () => {...})()"— a bare function expression makesevalAsyncsilently return{}(apple-music). Passing a realFunctionavoids this: the bridge serializes it to a call expression for you. browser.fetchreads the response body exactly once. When an endpoint serves HTML, requestAccept: text/htmlexplicitly — a JSON Accept hint against an HTML response can trip a body-already-read error in the bridge (strava). If the bridge misbehaves on empty-bodied PATCH/DELETE responses, do the fetch manually insideevalAsyncand read the body once (apple-music).- The current bridge JSON-encodes plain-object bodies with
Content-Type: application/json; older bridge versions didn't (P1s on #193).JSON.stringifyexplicitly anyway — explicit survives runtime churn. - Page-context code resolves paths against the page origin — a
fetch('/tmp/x.png')inside the page hitshttps://host/tmp/x.png, not your filesystem (#79). File bytes cross the bridge viafs.writeFileBinaryon the realm side. Binary responses arrive base64 withbodyEncoding: 'base64'; force withresponseType: 'binary'. - Encoding traps:
btoais Latin-1-only (CJK/emoji throw — chunk throughTextEncoder); base64url needs-_→+/and padding restored beforeatob; shell argv caps at ~1 MB, so pipe large payloads through stdin in base64 chunks (concur.jshwriteFile). - Handle expected non-JSON: a login wall returns HTML where JSON was expected — detect
<html/<!DOCTYPEin the text and translate to "session may have expired" (linkedin) instead of a JSON.parse stack. - Meta-traps: help text containing the word
awaitonce tripped the top-level-await transpile detector (f1cbc32); scan for both mojibake byte families (C3A2/C382 and EF BF BD) after editing.
9. Defensive patterns for shape-shifting APIs
- Fallback chains at every boundary:
data.currentAthlete || data,Array.isArray(data) ? data : (data.items || data.results || []),data.count ?? data.num ?? data. - When a detail endpoint nests what a list endpoint flattens, normalize once at the fetch site (
{...data, ...(data.summaryDTO || {})}, garmin) so downstream code handles one shape. - Clamp numeric flags:
const n = Number.isFinite(parsed) ? Math.min(Math.max(parsed, 1), 50) : DEFAULT; try { JSON.parse } catcharound every text boundary;stripHtmlbefore rendering strings from web APIs.- Comment wire-format discoveries with the date and issue number you verified them (
captured live 2026-07-10, issue #208) — these comments are the only durable record of undocumented APIs.
10. Output conventions
- Every command supports
--json→cli.out(data)with the raw response, early-return. Human mode otherwise. - Human output: two-space indent, leading blank line per section,
color.dim(' ' + '─'.repeat(52))separators, dimmed empty-state messages (' No activities found.'),color.cyan(color.bold(name))for titles,color.dim('id:…')so the agent can chain commands,color.green('✓')for success. - Emoji as data icons is fine (strava's activity map); keep IDs in output — downstream agent calls need them.
11. Approach selection — lessons from abandoned and rewritten PRs
- Live verification against a real authenticated session is the merge bar. #187 passed a synthetic harness while a P1 broke every authenticated command; its successor #200 merged on "12/16 subcommands exercised with real data, zero bugs". State what you verified live in the PR body.
- Prove no cleaner data source exists before DOM-scraping — then scrape without guilt. The teams saga's wire test (HAR + WebSocket capture showing zero caption traffic → captions are WebRTC/DOM-only) is the template (af9efcd).
- MutationObserver per semantic element beats polling, and you must learn the DOM's actual lifecycle first (each caption element grows, then finalizes; iterate the real spans, not
list.children) — five approach revisions in #215/#216 to get there. - When a rich-text editor ignores DOM edits, drive real CDP input from the accessibility snapshot — Teams' CKEditor drops execCommand/paste as orphan nodes, and the eval context may be detached mid-meeting.
- When an API rejects your well-formed request, HAR-capture the working web client and diff payloads — LinkedIn's message-send needed
Content-Type: text/plain;charset=UTF-8and a 16-raw-bytetrackingId, discoverable no other way (#208). - Direct generation beats framework + data contract for LLM-authored artifacts: the reveal.js + JSON-contract presentations skill (3,730 lines) lost to "the agent writes complete HTML/CSS directly" (363 lines) because the contract drifted from the renderer (#65→#74). Generic npm utility deps lost to purpose-built
sliccy:APIs the same way (#27). - Keep feature logic decoupled from the I/O layer — concur's new commands survived a platform migration by transplant because they only touched
callOp()internals (#145→#158). - Injected page state must be re-armable and rollback-safe: idempotent guard (
if (window.__slicc_observer) return;), clean up webhook + state file when injection fails, and on restart delete the prior webhook only after the replacement is installed — an orphaned webhook leaked captions across meetings (#216). Flush cursors must survive buffer resets (#215). - Every advertised flag must change behavior, and help text /
SKILL.md/ code stay in lockstep — flag-parses-but-does-nothing and doc drift are reliably flagged as P1/P2 (#66, #122). - Derive paths from
skill.dir— never hardcode/workspace/skills/<name>/(three P1s). Use/shared(not/tmp) for files another process or container must read (#53).
12. Process
- No direct pushes to main — every skill change goes through a PR (#56/#57).
- Fork PRs can't reach secret-gated CI; maintainers re-push the branch in-repo rather than using
pull_request_target(#119→#125, #149). - When parallel agents produce duplicate PRs, the first-opened, independently verified one wins — even over a more complete later duplicate (#191 vs #199).
- Before "fixing" an assumption about the runtime, read the slicc source — the
scripts/-dir "fix" was a no-op because discovery is recursive (#81). - Skill self-tests that should run in CI import
tst, notnode:test. The Skill integration workflow boots a live SLICC leader, installs each affected skill, and runstst. A changed skill with no tst suite fails the check (#389).
13. .bsh observers (page-injected, not realm)
.bsh files are JavaScript injected into the target page via CDP on navigation — no realm, no sliccy: bridges, no require (bundle first: ipk add <pkg> then ipx esbuild --bundle). Filename maps to hostname: -.okta.com.bsh = *.okta.com wildcard, login.okta.com.bsh = exact; // @match <glob> directives (first 10 lines) restrict further. Scanned under /workspace and /shared. Rules from skills/secret-sauce/references/observers.md: pick the cheapest observer (webhook/SSE > PerformanceObserver > MutationObserver > polling), guard injection idempotently, post via fetch().catch(() => {}) — an observer must never throw. Shell work belongs in a .jsh, not a .bsh.
14. Keep sprinkle UIs consistent
- Start with a sibling UI. Compare the same kind of task before choosing controls or spacing. Review and Loose ends are examples of lists of work for a human. Their shared foundation covers typography, buttons, badges, icons, and focus styles. Keep those conventions aligned when editing duplicated styles; each installed skill must still work on its own.
- Use semantic theme tokens. Build on the host's
--s2-*content, background, border, font, and radius tokens. Check foreground/background pairs in both themes, including muted text, inputs, selection, hover, and disabled states. Use the existing type hierarchy and icon scale; status must also have a text label, not only a color. - Design the narrow rail first. Measure the sprinkle's actual width inside the browser window. Use wrapping,
min-width: 0, and container queries where layout depends on panel width. Long titles and paths must not push actions outside the panel. Keep action labels visible; omit decorative icons before turning understandable controls into unexplained symbols. - Give related actions a stable order. In a review/work queue, keep Preview and secondary actions together, with the primary approval/publication action at the far right. Let rows wrap cleanly when necessary. Put infrequent tools in a labeled disclosure so the work stays prominent. A clickable headline and its Preview control should open the same preview implementation; HTML, Markdown, and other renderers should share the surrounding review flow.
- Let the producer supply domain wording. Review's
primaryActionLabeldefaults to Approve; AEM sources supply Publish. Carry configurable fields through the skill instructions, CLI, source protocol, and queue upserts. Document defaults and override precedence, preserve omitted fields, and render labels viatextContent. Keep dispatch behavior explicit and separate from the displayed caption. See the Review source protocol. - Treat keyboard and state feedback as part of the design. Use native buttons and links, accessible names, visible focus, and predictable focus return after closing a preview or composer. Loading, empty, failed, pending, and completed states need readable explanations. Distinguish a saved draft from a sent request and an acknowledged result.
15. Test the rendered UI and its lifecycle
For a UI change, define the flow before testing: entry point → user action → visible result → persisted state or emitted event. Use the actual SLICC harness for integration checks; a passing unit test or standalone HTML render does not exercise the host bridge. For documentation-only edits, check the instructions, links, and diff; a harness run is unnecessary.
Harness and fixtures
- Reuse the intended harness. Inventory existing sessions and follow the user's choice; use the oldest when requested. Record the checkout, runtime, and selected browser target so tests do not drift into another session. Run one browser driver at a time against that harness. Connect the available browser tooling or Playwright/CDP to its existing browser.
Install the complete runtime bundle. A host checkout and the harness VFS are different filesystems. Sync the changed skill, including scripts/assets, then run its documented bootstrap/install command. For example,
interview-me installrefreshes both the template and its importedlib/*.jsunder/shared/sprinkles/interview-me/while preserving user configuration and recordings. If there is no installer, copy the complete browser runtime bundle to its documented location, preserving relative paths for modules, styles, and images as well as the template. Preserve existing user state/configuration.sprinkle refreshdiscovers files but does not reload an open panel. The following template-only copy applies to Review's current single-file UI, after syncing its other changed skill files into the VFS:cp /workspace/skills/review/templates/review.shtml /shared/sprinkles/review/review.shtml sprinkle close review sprinkle refresh sprinkle open reviewConfirm the new UI and its imported modules are loaded before interpreting test results. Probe APIs in the realm that calls them; verify
.jshworker and sprinkle iframe bridges separately.- Seed through the real contract without replacing user data. In a reused harness, prefer an upsert such as Review's
ensure-item, with unique test-owned IDs. Use reproducible example data with realistic titles, paths, counts, mixed statuses, long content, and relevant loading/error cases; clean up only the fixtures created by the run. Review'sload-itemsreplaces and immediately persists the entire queue; namespacing its items does not protect existing entries. Test queue replacement and empty states in isolated test state where possible. If an occupied panel must be used, first snapshot its full persisted state (including the queue, drafts, and comments), verify the backup is readable, and restore it infinally, then verify it survives reopening. Prevent concurrent edits during replacement/restoration; if that cannot be ensured, use isolated test state instead of restoring an old snapshot over newer work.
Four-mode check
For changes to a rail's layout or shared controls, check the same content in all four combinations. These widths are practical starting points; also exercise a changed breakpoint or other supported surface when relevant.
| Theme | Narrow rail | Wide rail |
| Light | 320 px | 640 px |
| Dark | 320 px | 640 px |
Set the host theme and the actual rail width. Measure the panel's bounding box; a 1440 px browser viewport can still contain a 320 px rail. After reconnecting browser automation, recheck viewport and panel dimensions. Inspect wrapping, horizontal overflow, clipped menus/composers, scroll behavior, contrast, action order, and focus visibility. Check both the initial viewport and content reached by scrolling.
Interaction and persistence checks
- Exercise the visible entry points. Click the control, test keyboard activation, and inspect the resulting DOM/state. For iframe previews, scope locators and selection to the asset frame. Test every supported format affected by the change. For annotations, include repeated phrases and a selection across inline elements, then verify the saved and dispatched quote context identifies the selected occurrence.
- Check the full state lifecycle. Save a draft, switch items, reload the source, close/reopen the panel, and confirm the draft remains attached to the correct item. Delay initialization or source reads where relevant: startup must not overwrite saved state with an empty default, and an old response must not replace a newer selection.
- Verify the action and acknowledgement. Inspect the emitted action, item ID, path, and payload. Check pending-state disabling, repeat clicks, failure/retry, and acknowledgement. Use a test recipient for dispatch checks that would otherwise publish, send messages, or invoke an editing agent; say where that test stops. It proves the UI handoff, not the external operation. Keep the authenticated integration checks from §11 when those operations change.
- Clean up instrumentation in
finally. Remove the exact listeners/interceptors you installed, including capture listeners that callstopImmediatePropagation, and remove temporary fixture commands. Restore test-modified state and leave the intended harness usable. An orphaned test receiver can silently swallow the user's next real action. - Pair browser checks with focused regression tests. Test contracts such as upsert preservation, CLI override precedence, event payloads, and duplicate dispatch with the shipped implementation. Use browser checks for DOM ranges, iframe behavior, focus, and layout. Add tests for behavior at risk; avoid tests that only repeat static markup. After a fix, rerun the failed flow and affected modes. Host-side Review tests still use
node --test skills/review/tests/*.test.jsand are skipped by the live SLICC job until they importtst(see §16). Runnpm run lint:jshwhen.jsh/.bshfiles change.
Screenshot and PR evidence
Capture before/after screenshots with the same fixtures, theme, rail width, and scroll position. Wait for observable readiness (content loaded, intended state visible) rather than an arbitrary sleep. Inspect each image before claiming the layout works. Capture the whole sprinkle, and include the interaction/error state when that is what changed. Include the surrounding host when dock placement or resizing matters. Keep temporary scripts and captures outside the repository unless they are intended fixtures.
Attach screenshots to the UI PR with captions identifying theme, rail width, and state. Uploaded image URLs must actually render on GitHub; local file links in a PR body are not attachments. State which flows and runtime were tested, whether dispatch used a test recipient, relevant console errors, and any untested behavior. Check the final PR body after upload and replace stale “after” images when the design changes. The Review work in PR #369 provides examples of the four-mode evidence and protocol tests.
16. Skill self-tests (live SLICC CI)
.github/workflows/skill-integration.yml boots a hosted SLICC leader on the GitHub runner (packages/github-workflow), copies each canonical skill the PR touched into /workspace/skills/<name>, and self-tests it with the bundled tst runner.
- Runner. There is no
node --testin SLICC (node: unsupported option '--test', exit 9) and nonode:assert. Write*.test.{js,ts}files thatimport test, { is } from 'tst'.require('node:test')files cannot load in the test realm and do not satisfy the gate (#389). - Prerequisite.
tstresolves TypeScript from disk before it runs anything, including a.js-only suite. The job runsipk add -g typescript@6.0.3once per instance. - What the job proves. Every skill the PR touched has at least one tst file; the skill installs on a live leader; every tst file exits 0. A missing suite fails the check before the leader boots. A failing canary (the runner itself) fails the check.
- Realm limits. Measured 2026-09-22, correcting an earlier over-broad claim that
fsandpathfail here. Node builtins DO load, byimportorrequire:node:fs, barefs,path, andnode:module'screateRequireall resolve.skills/slack/tests/slack-ext{,-app}.test.jsrely on this to compile the real.jshfrom disk, and run in CI (88 tests / 392 assertions and 41 / 87). The real limits are narrower:createRequireresolvesnode:builtins but NOT relative filesystem paths, so import sibling modules with a literal relative specifier and substitute them into any mockrequire.- A real bridge such as
sliccy:browserloads; a name that does not exist fails withunknown sliccy: module <name>, which is a typo, not a sandbox restriction. Bridge-using modules still want their I/O injected, so tests stay offline and deterministic. same()does not compare object values —same({a:1},{a:2})PASSES. Useis(), which is deep, or an assertion that cannot silently hold.- Presence is not function: prove new assertions RED against the unfixed code before you keep them.
In this repository71 other files it gives its agents.
- adbSkill
- aem-edge-functionsSkill
- aemSkill
- ai-writing-detectorSkill
- apple-musicSkill
- awsSkill
- bbSkill
- bluebubblesSkill
- clickhouse-cloudSkill
- cloudflareSkill
- concurSkill
- cosmosSkill
- da-liveSkill
- decide-quicklySkill
- elevenlabsSkill
- eslintSkill
- fastlySkill
- fireflySkill
- fluffyjawsSkill
- garminSkill
- gcloudSkill
- githubSkill
- gmailSkill
- icloudSkill
- immobilienscout24Skill
- interview-meSkill
- jiraSkill
- linkedinSkill
- llm-wikiSkill
- loose-endsSkill
- metaculusSkill
- mixtapeSkill
- mondaySkill
- navanSkill
- oryxSkill
- outlookSkill
- pandocSkill
- pdfSkill
- pm-prdSkill
- pptx2pdfSkill
- pptxSkill
- presentationsSkill
- printfulSkill
- remotion-clipperSkill
- replicateSkill
- reviewSkill
- save-the-catSkill
- searchSkill
- secret-sauceSkill
- seoSkill
- servicenowSkill
- sessionizeSkill
- sharepointSkill
- signalSkill
- slackSkill
- slicc-demoSkill
- speckSkill
- stravaSkill
- strudel-musicSkill
- sunoSkill
- The first 60 shown.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

