diff --git a/docs/sourcey-catalog/README.md b/docs/sourcey-catalog/README.md new file mode 100644 index 000000000..fc3f7b460 --- /dev/null +++ b/docs/sourcey-catalog/README.md @@ -0,0 +1,43 @@ +# Sourcey catalog extractor + +This directory creates a 24-page documentation catalog from Runx skill sources +at one immutable Git commit. The local checkout is never used as source content. + +## Inputs + +`catalog.json` identifies the canonical GitHub repository, the pinned commit, +and five ordered groups. Every entry declares its skill name, output slug, +group, and source path. The extractor accepts only the Runx repository, a +40-character lowercase commit SHA, the exact canonical 24 names and five +groups, slugs equal to names, and paths of the form `skills//SKILL.md`. + +## Run + +```sh +node --test docs/sourcey-catalog/extract.test.mjs +node docs/sourcey-catalog/extract.mjs +``` + +The extractor reads each file through GitHub's Contents API at the catalog's +commit. Optional authentication comes only from `GITHUB_TOKEN`, falling back to +`GH_TOKEN`, and is sent only in the Authorization header. Tokens are redacted +from errors and output. The extractor retries transient failures up to three +total attempts, caps fetch concurrency at four, and waits at most 30 seconds +for a rate-limit reset before returning the reset time and token guidance. +Secondary rate limits are recognized from GitHub's response message or +`Retry-After` header and return the same bounded-wait and token guidance. + +Each response must contain canonical base64 for a file no larger than 1 MiB. +The extractor verifies the returned Git blob SHA before rendering the page. + +Generated pages are written in catalog order to `pages/*.md`. Each one contains +the group, immutable source URL, commit, source path, authored Markdown with +only recognized leading YAML frontmatter removed. Frontmatter is validated with +the project's `yaml` parser before removal; malformed YAML is rejected. The +extractor normalizes output to UTF-8 LF endings and rejects a symlink +`outputDir`. Within a real output directory, it atomically replaces expected +page paths and safely unlinks stale Markdown files or symlinks without following +symlink targets. + +`extractCatalog` returns a `sha256:` digest of each final page in its page +metadata. The digest is not embedded in the generated Markdown body. diff --git a/docs/sourcey-catalog/build.mjs b/docs/sourcey-catalog/build.mjs new file mode 100644 index 000000000..ede880701 --- /dev/null +++ b/docs/sourcey-catalog/build.mjs @@ -0,0 +1,139 @@ +import { lstat, readFile, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const SOURCEY_VERSION = "3.6.5"; +const CONFIG_FILE = "sourcey.config.ts"; + +export function renderSourceyConfig(catalog) { + if (!catalog || !Array.isArray(catalog.groups)) throw new TypeError("catalog groups must be an array"); + + const groups = [ + { name: "Introduction", slugs: ["introduction"] }, + ...catalog.groups.map((group) => { + if (typeof group?.name !== "string" || !Array.isArray(group.entries)) { + throw new TypeError("catalog groups must have a name and entries array"); + } + return { name: group.name, slugs: group.entries.map(({ slug }) => slug) }; + }) + ]; + const seen = new Set(); + for (const { slugs } of groups.slice(1)) { + for (const slug of slugs) { + if (typeof slug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) { + throw new Error("catalog slugs must be lowercase kebab-case"); + } + if (seen.has(slug)) throw new Error(`duplicate slug: ${slug}`); + seen.add(slug); + } + } + + const renderedGroups = groups.map(({ name, slugs }) => [ + " {", + ` group: ${JSON.stringify(name)},`, + ` pages: [${slugs.map((slug) => JSON.stringify(`pages/${slug}`)).join(", ")}],`, + " }," + ].join("\n")).join("\n"); + + return `export default { + name: "Runx Governed Skill Catalog", + siteUrl: "https://github.com", + baseUrl: "/runxhq/runx", + repo: "https://github.com/runxhq/runx", + editBranch: "main", + editBasePath: "docs/sourcey-catalog", + theme: { + preset: "default", + colors: { primary: "#0f766e", light: "#14b8a6", dark: "#134e4a" }, + }, + navigation: { + tabs: [ + { + tab: "Skills", + slug: "", + groups: [ +${renderedGroups} + ], + }, + ], + }, +}; +`; +} + +export function buildCommand(catalogDir, options = {}) { + const root = path.resolve(catalogDir); + const outputDir = path.resolve(options.outputDir ?? path.join(root, "site")); + const expectedOutput = path.join(root, "site"); + if (outputDir !== expectedOutput) { + throw new Error("outputDir must be the site directory inside the catalog directory"); + } + + const args = ["build", "-o", "site", "--quiet"]; + const command = options.sourceyBin + ? [path.resolve(options.sourceyBin), ...args] + : ["npx", "-y", `sourcey@${SOURCEY_VERSION}`, ...args]; + return { command, outputDir }; +} + +export async function buildCatalog({ catalogDir, outputDir, sourceyBin } = {}) { + const root = path.resolve(catalogDir ?? path.dirname(fileURLToPath(import.meta.url))); + const plan = buildCommand(root, { outputDir, sourceyBin }); + const catalog = JSON.parse(await readFile(path.join(root, "catalog.json"), "utf8")); + const config = renderSourceyConfig(catalog); + + await writeFile(path.join(root, CONFIG_FILE), config, "utf8"); + await rm(plan.outputDir, { recursive: true, force: true }); + await run(plan.command, root); + await verifyBuildArtifacts(plan.outputDir, catalog); + + return { + page_count: catalog.groups.flatMap((group) => group.entries).length, + output_dir: plan.outputDir, + command: plan.command + }; +} + +async function verifyBuildArtifacts(outputDir, catalog) { + const required = [ + "index.html", + "search-index.json", + "sourcey.css", + "sourcey.js", + "llms.txt", + "llms-full.txt", + "pages/introduction.html", + ...catalog.groups.flatMap((group) => group.entries.map(({ slug }) => `pages/${slug}.html`)), + ]; + + for (const relativePath of required) { + const artifact = path.join(outputDir, relativePath); + try { + const metadata = await lstat(artifact); + if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size === 0) throw new Error(); + } catch { + throw new Error(`missing or empty Sourcey build artifact: ${relativePath}`); + } + } +} + +async function run(command, cwd) { + await new Promise((resolve, reject) => { + const child = spawn(command[0], command.slice(1), { cwd, stdio: "inherit" }); + child.once("error", reject); + child.once("exit", (code, signal) => { + if (code === 0) resolve(); + else reject(new Error(`Sourcey build failed with ${signal ? `signal ${signal}` : `exit code ${code}`}`)); + }); + }); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + buildCatalog().then((result) => { + process.stdout.write(`${JSON.stringify(result)}\n`); + }).catch((error) => { + process.stderr.write(`${error.message}\n`); + process.exitCode = 1; + }); +} diff --git a/docs/sourcey-catalog/build.test.mjs b/docs/sourcey-catalog/build.test.mjs new file mode 100644 index 000000000..b08f8aa7f --- /dev/null +++ b/docs/sourcey-catalog/build.test.mjs @@ -0,0 +1,124 @@ +import assert from "node:assert/strict"; +import { chmod, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { buildCatalog, buildCommand, renderSourceyConfig } from "./build.mjs"; + +const catalog = { + source_repository: "https://github.com/runxhq/runx", + source_commit: "5afc25a83edf1c1320df7ac0d78c36f1523b5677", + groups: [ + { name: "Operate", entries: [{ slug: "agency" }, { slug: "work-plan" }] }, + { name: "Research and data", entries: [{ slug: "research" }] }, + { name: "GitHub and delivery", entries: [{ slug: "issue-to-pr" }] }, + { name: "Safety and review", entries: [{ slug: "cve-audit" }] }, + { name: "Outbound and tooling", entries: [{ slug: "sourcey" }] } + ] +}; + +test("config exposes every manifest page once after introduction in catalog group order", () => { + const config = renderSourceyConfig(catalog); + const expectedGroups = ["Introduction", ...catalog.groups.map((group) => group.name)]; + + assert.match(config, /name: "Runx Governed Skill Catalog"/); + assert.match(config, /siteUrl: "https:\/\/github\.com"/); + assert.match(config, /baseUrl: "\/runxhq\/runx"/); + assert.match(config, /repo: "https:\/\/github\.com\/runxhq\/runx"/); + assert.match(config, /editBranch: "main"/); + assert.match(config, /editBasePath: "docs\/sourcey-catalog"/); + assert.doesNotMatch(config, /from "sourcey"/); + assert.match(config, /groups: \[/); + assert.deepEqual( + [...config.matchAll(/group: "([^"]+)"/g)].map((match) => match[1]), + expectedGroups + ); + assert.match(config, /group: "Introduction",\n\s+pages: \["pages\/introduction"\]/); + + for (const entry of catalog.groups.flatMap((group) => group.entries)) { + assert.equal(config.match(new RegExp(`pages/${entry.slug.replaceAll("-", "\\-")}`, "g"))?.length, 1); + } +}); + +test("build command pins Sourcey 3.6.5 and writes only inside catalog site", () => { + const plan = buildCommand("/repo/docs/sourcey-catalog"); + + assert.deepEqual(plan.command, ["npx", "-y", "sourcey@3.6.5", "build", "-o", "site", "--quiet"]); + assert.equal(plan.outputDir, "/repo/docs/sourcey-catalog/site"); + assert.throws( + () => buildCommand("/repo/docs/sourcey-catalog", { outputDir: "/repo/docs/site" }), + /inside the catalog directory/ + ); +}); + +test("config splits the public URL into the Sourcey 3.6.5 origin and base path", () => { + const config = renderSourceyConfig(catalog); + + assert.match(config, /siteUrl: "https:\/\/github\.com"/); + assert.match(config, /baseUrl: "\/runxhq\/runx"/); +}); + +test("build wrapper accepts an explicit Sourcey binary for offline execution", async (t) => { + const catalogDir = await mkdtemp(path.join(tmpdir(), "sourcey-build-")); + t.after(() => rm(catalogDir, { recursive: true, force: true })); + await writeFile(path.join(catalogDir, "catalog.json"), `${JSON.stringify(catalog)}\n`, "utf8"); + const sourceyBin = path.join(catalogDir, "sourcey-fixture.mjs"); + await writeFile(sourceyBin, [ + "#!/usr/bin/env node", + "import { mkdir, writeFile } from 'node:fs/promises';", + "await writeFile('invocation.json', JSON.stringify({ cwd: process.cwd(), args: process.argv.slice(2) }));", + "await mkdir('site/pages', { recursive: true });", + `for (const file of ${JSON.stringify([ + "index.html", + "search-index.json", + "sourcey.css", + "sourcey.js", + "llms.txt", + "llms-full.txt", + "pages/introduction.html", + ...catalog.groups.flatMap((group) => group.entries.map((entry) => `pages/${entry.slug}.html`)), + ])}) await writeFile('site/' + file, 'fixture');`, + ].join("\n"), "utf8"); + await chmod(sourceyBin, 0o755); + + const result = await buildCatalog({ catalogDir, sourceyBin }); + const invocation = JSON.parse(await readFile(path.join(catalogDir, "invocation.json"), "utf8")); + + assert.deepEqual(invocation, { + cwd: await realpath(catalogDir), + args: ["build", "-o", "site", "--quiet"] + }); + assert.equal(result.page_count, 6); + assert.equal(result.output_dir, path.join(catalogDir, "site")); + assert.deepEqual(result.command, [sourceyBin, "build", "-o", "site", "--quiet"]); + assert.match(await readFile(path.join(catalogDir, "sourcey.config.ts"), "utf8"), /pages\/agency/); +}); + +test("build wrapper refuses a successful command with missing static artifacts", async (t) => { + const catalogDir = await mkdtemp(path.join(tmpdir(), "sourcey-build-missing-")); + t.after(() => rm(catalogDir, { recursive: true, force: true })); + await writeFile(path.join(catalogDir, "catalog.json"), `${JSON.stringify(catalog)}\n`, "utf8"); + const sourceyBin = path.join(catalogDir, "sourcey-fixture.mjs"); + await writeFile(sourceyBin, "#!/usr/bin/env node\n", "utf8"); + await chmod(sourceyBin, 0o755); + + await assert.rejects( + buildCatalog({ catalogDir, sourceyBin }), + /missing or empty Sourcey build artifact/, + ); +}); + +test("build wrapper reports a nonzero Sourcey exit", async (t) => { + const catalogDir = await mkdtemp(path.join(tmpdir(), "sourcey-build-failed-")); + t.after(() => rm(catalogDir, { recursive: true, force: true })); + await writeFile(path.join(catalogDir, "catalog.json"), `${JSON.stringify(catalog)}\n`, "utf8"); + const sourceyBin = path.join(catalogDir, "sourcey-fixture.mjs"); + await writeFile(sourceyBin, "#!/usr/bin/env node\nprocess.exitCode = 7;\n", "utf8"); + await chmod(sourceyBin, 0o755); + + await assert.rejects( + buildCatalog({ catalogDir, sourceyBin }), + /exit code 7/, + ); +}); diff --git a/docs/sourcey-catalog/catalog.json b/docs/sourcey-catalog/catalog.json new file mode 100644 index 000000000..517167b70 --- /dev/null +++ b/docs/sourcey-catalog/catalog.json @@ -0,0 +1,55 @@ +{ + "source_repository": "https://github.com/runxhq/runx", + "source_commit": "5afc25a83edf1c1320df7ac0d78c36f1523b5677", + "groups": [ + { + "name": "Operate", + "entries": [ + { "name": "agency", "slug": "agency", "group": "Operate", "path": "skills/agency/SKILL.md" }, + { "name": "business-ops", "slug": "business-ops", "group": "Operate", "path": "skills/business-ops/SKILL.md" }, + { "name": "operator-inbox", "slug": "operator-inbox", "group": "Operate", "path": "skills/operator-inbox/SKILL.md" }, + { "name": "ops-desk", "slug": "ops-desk", "group": "Operate", "path": "skills/ops-desk/SKILL.md" }, + { "name": "work-plan", "slug": "work-plan", "group": "Operate", "path": "skills/work-plan/SKILL.md" } + ] + }, + { + "name": "Research and data", + "entries": [ + { "name": "deep-research", "slug": "deep-research", "group": "Research and data", "path": "skills/deep-research/SKILL.md" }, + { "name": "research", "slug": "research", "group": "Research and data", "path": "skills/research/SKILL.md" }, + { "name": "data-store", "slug": "data-store", "group": "Research and data", "path": "skills/data-store/SKILL.md" }, + { "name": "knowledge-router", "slug": "knowledge-router", "group": "Research and data", "path": "skills/knowledge-router/SKILL.md" }, + { "name": "web-fetch", "slug": "web-fetch", "group": "Research and data", "path": "skills/web-fetch/SKILL.md" } + ] + }, + { + "name": "GitHub and delivery", + "entries": [ + { "name": "github-sync", "slug": "github-sync", "group": "GitHub and delivery", "path": "skills/github-sync/SKILL.md" }, + { "name": "issue-intake", "slug": "issue-intake", "group": "GitHub and delivery", "path": "skills/issue-intake/SKILL.md" }, + { "name": "issue-triage", "slug": "issue-triage", "group": "GitHub and delivery", "path": "skills/issue-triage/SKILL.md" }, + { "name": "issue-to-pr", "slug": "issue-to-pr", "group": "GitHub and delivery", "path": "skills/issue-to-pr/SKILL.md" }, + { "name": "release", "slug": "release", "group": "GitHub and delivery", "path": "skills/release/SKILL.md" } + ] + }, + { + "name": "Safety and review", + "entries": [ + { "name": "audit-receipt", "slug": "audit-receipt", "group": "Safety and review", "path": "skills/audit-receipt/SKILL.md" }, + { "name": "cve-audit", "slug": "cve-audit", "group": "Safety and review", "path": "skills/cve-audit/SKILL.md" }, + { "name": "least-privilege", "slug": "least-privilege", "group": "Safety and review", "path": "skills/least-privilege/SKILL.md" }, + { "name": "policy-author", "slug": "policy-author", "group": "Safety and review", "path": "skills/policy-author/SKILL.md" }, + { "name": "review-receipt", "slug": "review-receipt", "group": "Safety and review", "path": "skills/review-receipt/SKILL.md" }, + { "name": "sandbox-harden", "slug": "sandbox-harden", "group": "Safety and review", "path": "skills/sandbox-harden/SKILL.md" } + ] + }, + { + "name": "Outbound and tooling", + "entries": [ + { "name": "governed-outbound", "slug": "governed-outbound", "group": "Outbound and tooling", "path": "skills/governed-outbound/SKILL.md" }, + { "name": "run-history", "slug": "run-history", "group": "Outbound and tooling", "path": "skills/run-history/SKILL.md" }, + { "name": "sourcey", "slug": "sourcey", "group": "Outbound and tooling", "path": "skills/sourcey/SKILL.md" } + ] + } + ] +} diff --git a/docs/sourcey-catalog/extract.mjs b/docs/sourcey-catalog/extract.mjs new file mode 100644 index 000000000..4752dc518 --- /dev/null +++ b/docs/sourcey-catalog/extract.mjs @@ -0,0 +1,471 @@ +import { createHash, randomUUID } from "node:crypto"; +import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { setTimeout as delay } from "node:timers/promises"; +import { fileURLToPath } from "node:url"; +import { parseDocument } from "yaml"; + +const EXPECTED_PAGE_COUNT = 24; +const MAX_CONCURRENCY = 4; +const MAX_ATTEMPTS = 3; +const MAX_CONTENT_BYTES = 1024 * 1024; +const MAX_RATE_LIMIT_WAIT_MS = 30 * 1000; +const MAINTAINED_PAGE_FILES = new Set(["introduction.md"]); +const SOURCE_COMMIT = "5afc25a83edf1c1320df7ac0d78c36f1523b5677"; +const COMMIT_PATTERN = /^[0-9a-f]{40}$/; +const SHA_PATTERN = /^[0-9a-f]{40}$/; +const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; +const SOURCE_REPOSITORY = "https://github.com/runxhq/runx"; +const CANONICAL_GROUPS = [ + { name: "Operate", names: ["agency", "business-ops", "operator-inbox", "ops-desk", "work-plan"] }, + { name: "Research and data", names: ["deep-research", "research", "data-store", "knowledge-router", "web-fetch"] }, + { name: "GitHub and delivery", names: ["github-sync", "issue-intake", "issue-triage", "issue-to-pr", "release"] }, + { name: "Safety and review", names: ["audit-receipt", "cve-audit", "least-privilege", "policy-author", "review-receipt", "sandbox-harden"] }, + { name: "Outbound and tooling", names: ["governed-outbound", "run-history", "sourcey"] } +]; + +export async function extractCatalog({ catalogPath, outputDir, fetchImpl = globalThis.fetch, concurrency = MAX_CONCURRENCY }) { + if (typeof fetchImpl !== "function") throw new TypeError("fetchImpl must be a function"); + + const catalog = JSON.parse(await readFile(catalogPath, "utf8")); + const { sourceRepository, sourceCommit, pages } = validateCatalog(catalog); + const authentication = githubAuthentication(); + const limit = normalizeConcurrency(concurrency); + const extractedPages = await mapWithConcurrency(pages, limit, async (page) => { + const sourceUrl = `${sourceRepository}/blob/${sourceCommit}/${page.path}`; + const contentUrl = toContentsUrl(sourceRepository, page.path, sourceCommit); + const source = await fetchContents(contentUrl, fetchImpl, authentication); + const content = decodeSource(source, page.path); + const blobSha = gitBlobSha(content); + + if (blobSha !== source.sha) { + throw new Error(`blob SHA mismatch for ${page.path}`); + } + + const body = renderPage({ ...page, sourceUrl, sourceCommit, sourceRepository, content }); + const outputPath = path.join(outputDir, `${page.slug}.md`); + return { + ...page, + source_url: sourceUrl, + output_path: outputPath, + blob_sha: blobSha, + content_digest: `sha256:${sha256(body)}`, + body + }; + }); + + await writePages(outputDir, extractedPages); + return { source_commit: sourceCommit, pages: extractedPages }; +} + +function validateCatalog(catalog) { + if (!catalog || typeof catalog !== "object") throw new TypeError("catalog must be an object"); + if (catalog.source_repository !== SOURCE_REPOSITORY) { + throw new Error(`source_repository must be ${SOURCE_REPOSITORY}`); + } + if (!COMMIT_PATTERN.test(catalog.source_commit ?? "") || catalog.source_commit !== SOURCE_COMMIT) { + throw new Error(`source_commit must equal the pinned source commit ${SOURCE_COMMIT}`); + } + if (!Array.isArray(catalog.groups)) throw new TypeError("groups must be an array"); + + const pages = []; + for (const group of catalog.groups) { + if (!group || typeof group !== "object" || typeof group.name !== "string" || !Array.isArray(group.entries)) { + throw new Error("groups must have a name and entries array"); + } + for (const entry of group.entries) { + if (!entry || typeof entry !== "object") throw new TypeError("catalog entry must be an object"); + if (!isSlug(entry.name) || !isSlug(entry.slug)) throw new Error("entry names and slugs must be lowercase kebab-case"); + if (entry.path !== `skills/${entry.name}/SKILL.md`) { + throw new Error(`invalid source path for ${entry.name}`); + } + pages.push({ name: entry.name, slug: entry.slug, group: entry.group, path: entry.path }); + } + } + + if (pages.length !== EXPECTED_PAGE_COUNT) { + throw new Error(`catalog must contain exactly ${EXPECTED_PAGE_COUNT} pages`); + } + ensureUnique(pages, "name"); + ensureUnique(pages, "slug"); + validateCanonicalGroups(catalog.groups); + return { sourceRepository: catalog.source_repository, sourceCommit: catalog.source_commit, pages }; +} + +function validateCanonicalGroups(groups) { + if (groups.length !== CANONICAL_GROUPS.length) { + throw new Error("catalog must use the exact canonical groups and memberships"); + } + + for (const [groupIndex, expectedGroup] of CANONICAL_GROUPS.entries()) { + const group = groups[groupIndex]; + if (group.name !== expectedGroup.name || group.entries.length !== expectedGroup.names.length) { + throw new Error("catalog must use the exact canonical groups and memberships"); + } + for (const [entryIndex, expectedName] of expectedGroup.names.entries()) { + const entry = group.entries[entryIndex]; + if (entry.name !== expectedName) { + throw new Error(`catalog entry must use canonical name ${expectedName}`); + } + if (entry.group !== expectedGroup.name) { + throw new Error("catalog must use the exact canonical groups and memberships"); + } + if (entry.slug !== entry.name) throw new Error(`slug must equal name for ${entry.name}`); + } + } +} + +function ensureUnique(pages, key) { + const values = new Set(); + for (const page of pages) { + if (values.has(page[key])) throw new Error(`duplicate ${key}: ${page[key]}`); + values.add(page[key]); + } +} + +function isSlug(value) { + return typeof value === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value); +} + +function normalizeConcurrency(concurrency) { + if (!Number.isInteger(concurrency) || concurrency < 1) { + throw new RangeError("concurrency must be a positive integer"); + } + return Math.min(concurrency, MAX_CONCURRENCY); +} + +async function mapWithConcurrency(values, concurrency, mapper) { + const results = new Array(values.length); + let nextIndex = 0; + const worker = async () => { + while (nextIndex < values.length) { + const index = nextIndex; + nextIndex += 1; + results[index] = await mapper(values[index]); + } + }; + + await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, worker)); + return results; +} + +function toContentsUrl(sourceRepository, sourcePath, sourceCommit) { + const repositoryPath = new URL(sourceRepository).pathname.slice(1); + const encodedPath = sourcePath.split("/").map(encodeURIComponent).join("/"); + return `https://api.github.com/repos/${repositoryPath}/contents/${encodedPath}?ref=${sourceCommit}`; +} + +async function fetchContents(url, fetchImpl, authentication) { + let lastError; + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) { + let response; + try { + response = await fetchImpl(url, { + headers: { + Accept: "application/vnd.github+json", + ...(authentication.token ? { Authorization: `Bearer ${authentication.token}` } : {}) + } + }); + } catch (error) { + lastError = new Error(redactError(error, authentication.secrets)); + continue; + } + + if (response.ok) { + try { + return await response.json(); + } catch (error) { + throw new Error(redactError(error, authentication.secrets)); + } + } + if (isRateLimited(response)) { + const resetSeconds = Number.parseInt(response.headers.get("x-ratelimit-reset") ?? "", 10); + const waitMs = Number.isSafeInteger(resetSeconds) ? Math.max(0, resetSeconds * 1000 - Date.now()) : Infinity; + const message = rateLimitMessage(resetSeconds, waitMs); + if (waitMs > MAX_RATE_LIMIT_WAIT_MS) throw new Error(message); + lastError = new Error(message); + if (attempt < MAX_ATTEMPTS) await delay(waitMs); + continue; + } + const secondaryLimit = await secondaryRateLimit(response, authentication.secrets); + if (secondaryLimit) { + const message = secondaryRateLimitMessage(secondaryLimit.waitMs); + if (secondaryLimit.waitMs > MAX_RATE_LIMIT_WAIT_MS) throw new Error(message); + lastError = new Error(message); + if (attempt < MAX_ATTEMPTS) await delay(secondaryLimit.waitMs); + continue; + } + if (!isTransientStatus(response.status)) { + throw new Error(`GitHub Contents API returned status ${response.status}`); + } + lastError = new Error(`GitHub Contents API returned transient status ${response.status}`); + } + throw new Error(`GitHub Contents API failed after ${MAX_ATTEMPTS} attempts: ${lastError.message}`); +} + +function githubAuthentication() { + const secrets = [process.env.GITHUB_TOKEN, process.env.GH_TOKEN].filter(Boolean); + return { token: process.env.GITHUB_TOKEN || process.env.GH_TOKEN, secrets }; +} + +function redactError(error, secrets) { + let message = error instanceof Error ? error.message : String(error); + for (const secret of secrets) message = message.replaceAll(secret, "[REDACTED]"); + return message; +} + +function isRateLimited(response) { + return response.status === 403 && response.headers?.get?.("x-ratelimit-remaining") === "0"; +} + +function rateLimitMessage(resetSeconds, waitMs) { + const reset = Number.isSafeInteger(resetSeconds) + ? new Date(resetSeconds * 1000).toISOString() + : "an unavailable reset time"; + const waitGuidance = waitMs > MAX_RATE_LIMIT_WAIT_MS + ? "The extractor cannot wait more than 30 seconds. " + : ""; + return `GitHub API rate limit exhausted; reset at ${reset}. ${waitGuidance}Rerun after reset or set GITHUB_TOKEN or GH_TOKEN.`; +} + +async function secondaryRateLimit(response, secrets) { + if (response.status !== 403) return undefined; + const retryAfter = response.headers?.get?.("retry-after"); + const waitMs = parseRetryAfter(retryAfter); + let responseMessage = ""; + if (typeof response.json === "function") { + try { + const body = await response.json(); + responseMessage = redactError(body?.message ?? "", secrets); + } catch { + responseMessage = ""; + } + } + if (waitMs === undefined && !/secondary rate limit|abuse detection/i.test(responseMessage)) return undefined; + return { waitMs: waitMs ?? Infinity }; +} + +function parseRetryAfter(value) { + if (value === null || value === undefined || value === "") return undefined; + const seconds = Number(value); + if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000; + const timestamp = Date.parse(value); + return Number.isFinite(timestamp) ? Math.max(0, timestamp - Date.now()) : undefined; +} + +function secondaryRateLimitMessage(waitMs) { + const retryGuidance = Number.isFinite(waitMs) + ? `retry after ${Math.ceil(waitMs / 1000)} seconds. ` + : "retry later; wait time is unavailable. "; + const waitGuidance = waitMs > MAX_RATE_LIMIT_WAIT_MS + ? "The extractor cannot wait more than 30 seconds. " + : ""; + return `GitHub API secondary rate limit triggered; ${retryGuidance}${waitGuidance}Rerun after waiting or set GITHUB_TOKEN or GH_TOKEN.`; +} + +function isTransientStatus(status) { + return status === 408 || status === 425 || status === 429 || status >= 500; +} + +function decodeSource(source, sourcePath) { + if (!source || source.type !== "file") throw new Error(`Contents API did not return a file for ${sourcePath}`); + if (source.encoding !== "base64" || typeof source.content !== "string") { + throw new Error(`Contents API did not return base64 content for ${sourcePath}`); + } + if (!SHA_PATTERN.test(source.sha ?? "")) throw new Error(`invalid blob SHA for ${sourcePath}`); + + const compactBase64 = source.content.replace(/\r?\n/g, ""); + if (!BASE64_PATTERN.test(compactBase64)) throw new Error(`invalid base64 content for ${sourcePath}`); + + const content = Buffer.from(compactBase64, "base64"); + if (content.toString("base64") !== compactBase64) { + throw new Error(`source is not canonical base64 for ${sourcePath}`); + } + if (content.length > MAX_CONTENT_BYTES) throw new Error(`source exceeds 1 MiB for ${sourcePath}`); + if (!Buffer.from(content.toString("utf8"), "utf8").equals(content)) { + throw new Error(`source is not valid UTF-8 for ${sourcePath}`); + } + return content; +} + +function gitBlobSha(content) { + return createHash("sha1").update(`blob ${content.length}\0`).update(content).digest("hex"); +} + +function renderPage({ name, group, path: sourcePath, sourceUrl, sourceCommit, sourceRepository, content }) { + const authoredContent = rewriteSourceRelativeLinks( + stripYamlFrontmatter(content.toString("utf8"), name).replace(/\r\n?/g, "\n"), + sourcePath, + sourceRepository, + sourceCommit, + ); + const header = [ + `# ${name}`, + "", + `- Group: ${group}`, + `- Source: [${sourcePath}](${sourceUrl})`, + `- Commit: \`${sourceCommit}\``, + `- Path: \`${sourcePath}\``, + "" + ].join("\n"); + return `${header}${authoredContent}`.replace(/\n*$/, "\n"); +} + +function rewriteSourceRelativeLinks(markdown, sourcePath, sourceRepository, sourceCommit) { + const lines = markdown.split("\n"); + let fence; + return lines.map((line) => { + const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/); + if (fence) { + if (fenceMatch + && fenceMatch[1][0] === fence.marker + && fenceMatch[1].length >= fence.length + && fenceMatch[2].trim() === "") { + fence = undefined; + } + return line; + } + if (fenceMatch && (fenceMatch[1][0] === "~" || !fenceMatch[2].includes("`"))) { + fence = { marker: fenceMatch[1][0], length: fenceMatch[1].length }; + return line; + } + return rewriteMarkdownLine(line, sourcePath, sourceRepository, sourceCommit); + }).join("\n"); +} + +function rewriteMarkdownLine(line, sourcePath, sourceRepository, sourceCommit) { + let output = ""; + let cursor = 0; + while (cursor < line.length) { + const opening = line.indexOf("](", cursor); + if (opening === -1) return output + line.slice(cursor); + output += line.slice(cursor, opening + 2); + const destination = readMarkdownDestination(line, opening + 2); + if (!destination) { + output += line.slice(opening + 2); + return output; + } + const rewritten = pinnedRelativeUrl( + destination.value, + sourcePath, + sourceRepository, + sourceCommit, + ); + output += destination.angle ? `<${rewritten}>` : rewritten; + cursor = destination.end; + } + return output; +} + +function readMarkdownDestination(line, start) { + const angle = line[start] === "<"; + let cursor = angle ? start + 1 : start; + const valueStart = cursor; + let nested = 0; + let escaped = false; + while (cursor < line.length) { + const character = line[cursor]; + if (escaped) { + escaped = false; + } else if (character === "\\") { + escaped = true; + } else if (angle && character === ">") { + return { value: line.slice(valueStart, cursor), end: cursor + 1, angle: true }; + } else if (!angle && character === "(") { + nested += 1; + } else if (!angle && character === ")") { + if (nested === 0) return { value: line.slice(valueStart, cursor), end: cursor, angle: false }; + nested -= 1; + } else if (!angle && /\s/.test(character) && nested === 0) { + return { value: line.slice(valueStart, cursor), end: cursor, angle: false }; + } + cursor += 1; + } + return undefined; +} + +function pinnedRelativeUrl(target, sourcePath, sourceRepository, sourceCommit) { + if (!target.startsWith("./") && !target.startsWith("../")) return target; + const suffixIndex = [...[target.indexOf("?"), target.indexOf("#")] + .filter((index) => index >= 0)].sort((left, right) => left - right)[0] ?? target.length; + const relativePath = target.slice(0, suffixIndex); + const suffix = target.slice(suffixIndex); + const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(sourcePath), relativePath)); + if (resolved === ".." || resolved.startsWith("../") || path.posix.isAbsolute(resolved)) { + throw new Error(`relative Markdown link escapes the source repository: ${target}`); + } + const encoded = resolved.split("/").map(encodeURIComponent).join("/"); + return `${sourceRepository}/blob/${sourceCommit}/${encoded}${suffix}`; +} + +function stripYamlFrontmatter(content, expectedName) { + const lines = content.split(/\r?\n/); + if (lines[0] !== "---") return content; + + if (!/^name\s*:/.test(lines[1] ?? "")) return content; + + const closingIndex = lines.findIndex((line, index) => index > 0 && line === "---"); + if (closingIndex === -1) { + throw new Error(`malformed YAML frontmatter for ${expectedName}: missing closing delimiter`); + } + validateYamlFrontmatter(lines.slice(1, closingIndex).join("\n"), expectedName); + return lines.slice(closingIndex + 1).join("\n"); +} + +function validateYamlFrontmatter(frontmatter, expectedName) { + const document = parseDocument(frontmatter, { prettyErrors: false, uniqueKeys: true }); + if (document.errors.length > 0) { + throw new Error(`malformed YAML frontmatter for ${expectedName}: invalid YAML`); + } + let parsed; + try { + parsed = document.toJS(); + } catch { + throw new Error(`malformed YAML frontmatter for ${expectedName}: invalid YAML`); + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || parsed.name !== expectedName) { + throw new Error(`malformed YAML frontmatter for ${expectedName}: expected matching name mapping`); + } +} + +function sha256(content) { + return createHash("sha256").update(content, "utf8").digest("hex"); +} + +async function writePages(outputDir, pages) { + await mkdir(outputDir, { recursive: true }); + const outputStats = await lstat(outputDir); + if (outputStats.isSymbolicLink()) throw new Error("output directory must not be a symbolic link"); + if (!outputStats.isDirectory()) throw new Error("output directory must be a directory"); + const expectedFiles = new Set([ + ...MAINTAINED_PAGE_FILES, + ...pages.map((page) => `${page.slug}.md`), + ]); + const existingEntries = await readdir(outputDir, { withFileTypes: true }); + await Promise.all(existingEntries + .filter((entry) => (entry.isFile() || entry.isSymbolicLink()) + && entry.name.endsWith(".md") + && !expectedFiles.has(entry.name)) + .map((entry) => rm(path.join(outputDir, entry.name)))); + for (const page of pages) await writePageAtomically(outputDir, page); +} + +async function writePageAtomically(outputDir, page) { + const temporaryPath = path.join(outputDir, `.${path.basename(page.output_path)}.${randomUUID()}.tmp`); + try { + await writeFile(temporaryPath, page.body, { encoding: "utf8", flag: "wx" }); + await rename(temporaryPath, page.output_path); + } finally { + await rm(temporaryPath, { force: true }); + } +} + +const modulePath = fileURLToPath(import.meta.url); +if (process.argv[1] && path.resolve(process.argv[1]) === modulePath) { + const directory = path.dirname(modulePath); + const result = await extractCatalog({ + catalogPath: path.join(directory, "catalog.json"), + outputDir: path.join(directory, "pages") + }); + process.stdout.write(`extracted ${result.pages.length} pages from ${result.source_commit}\n`); +} diff --git a/docs/sourcey-catalog/extract.test.mjs b/docs/sourcey-catalog/extract.test.mjs new file mode 100644 index 000000000..6eff17f49 --- /dev/null +++ b/docs/sourcey-catalog/extract.test.mjs @@ -0,0 +1,584 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { lstat, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { extractCatalog } from "./extract.mjs"; + +const sourceCommit = "5afc25a83edf1c1320df7ac0d78c36f1523b5677"; +const names = [ + "agency", "business-ops", "operator-inbox", "ops-desk", "work-plan", + "deep-research", "research", "data-store", "knowledge-router", "web-fetch", + "github-sync", "issue-intake", "issue-triage", "issue-to-pr", "release", + "audit-receipt", "cve-audit", "least-privilege", "policy-author", + "review-receipt", "sandbox-harden", "governed-outbound", "run-history", "sourcey" +]; +const groupDefinitions = [ + { name: "Operate", names: names.slice(0, 5) }, + { name: "Research and data", names: names.slice(5, 10) }, + { name: "GitHub and delivery", names: names.slice(10, 15) }, + { name: "Safety and review", names: names.slice(15, 21) }, + { name: "Outbound and tooling", names: names.slice(21) } +]; + +function blobSha(content) { + const bytes = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8"); + return createHash("sha1").update(`blob ${bytes.length}\0`).update(bytes).digest("hex"); +} + +function catalog(entries = createEntries()) { + let offset = 0; + return { + source_repository: "https://github.com/runxhq/runx", + source_commit: sourceCommit, + groups: groupDefinitions.map((group) => { + const groupEntries = entries.slice(offset, offset + group.names.length); + offset += group.names.length; + return { name: group.name, entries: groupEntries }; + }) + }; +} + +function createEntries() { + return groupDefinitions.flatMap((group) => group.names.map((name) => ({ + name, + slug: name, + group: group.name, + path: `skills/${name}/SKILL.md` + }))); +} + +function sourceResponse(content) { + const bytes = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8"); + return { + ok: true, + status: 200, + async json() { + return { + type: "file", + encoding: "base64", + content: bytes.toString("base64"), + sha: blobSha(bytes) + }; + } + }; +} + +function fixtureFetch(contentByPath) { + return async (url) => { + const parsed = new URL(url); + assert.equal(parsed.origin, "https://api.github.com"); + assert.equal(parsed.searchParams.get("ref"), sourceCommit); + const sourcePath = decodeURIComponent(parsed.pathname.replace("/repos/runxhq/runx/contents/", "")); + return sourceResponse(contentByPath.get(sourcePath)); + }; +} + +async function withTokenEnvironment({ githubToken, ghToken }, callback) { + const previousGithubToken = process.env.GITHUB_TOKEN; + const previousGhToken = process.env.GH_TOKEN; + try { + if (githubToken === undefined) delete process.env.GITHUB_TOKEN; + else process.env.GITHUB_TOKEN = githubToken; + if (ghToken === undefined) delete process.env.GH_TOKEN; + else process.env.GH_TOKEN = ghToken; + return await callback(); + } finally { + if (previousGithubToken === undefined) delete process.env.GITHUB_TOKEN; + else process.env.GITHUB_TOKEN = previousGithubToken; + if (previousGhToken === undefined) delete process.env.GH_TOKEN; + else process.env.GH_TOKEN = previousGhToken; + } +} + +async function captureRejection(callback) { + let rejection; + try { + await callback(); + } catch (error) { + rejection = error; + } + assert.ok(rejection instanceof Error); + return rejection; +} + +async function createFixture(overrides = {}) { + const directory = await mkdtemp(path.join(tmpdir(), "sourcey-catalog-")); + const document = overrides.catalog ?? catalog(); + const catalogPath = path.join(directory, "catalog.json"); + const outputDir = path.join(directory, "pages"); + const contents = new Map( + createEntries().map(({ name, path: sourcePath }, index) => [ + sourcePath, + index === 0 + ? (overrides.firstContent ?? `---\nname: ${name}\n---\n# Authored heading\n\npinned content\n`) + : `---\nname: ${name}\n---\n# ${sourcePath}\n` + ]) + ); + + await writeFile(catalogPath, `${JSON.stringify(document, null, 2)}\n`, "utf8"); + return { + catalogPath, + outputDir, + fetchImpl: overrides.fetchImpl ?? fixtureFetch(contents), + concurrency: overrides.concurrency ?? 4, + cleanup: () => rm(directory, { recursive: true, force: true }) + }; +} + +test("reads every page from the pinned GitHub URL and verifies its blob SHA", async (t) => { + const fixture = await createFixture(); + t.after(fixture.cleanup); + + const result = await extractCatalog(fixture); + + assert.equal(result.pages.length, 24); + assert.equal(result.pages[0].body.includes("pinned content"), true); + assert.equal(result.pages[0].body.includes("working tree mutation"), false); + assert.match(result.pages[0].blob_sha, /^[0-9a-f]{40}$/); + assert.equal((await readFile(result.pages[0].output_path, "utf8")).includes("# Authored heading"), true); +}); + +test("refuses duplicate slugs and fewer than 24 pages", async (t) => { + const duplicateEntries = createEntries(); + duplicateEntries[1] = { ...duplicateEntries[1], slug: duplicateEntries[0].slug }; + const duplicateFixture = await createFixture({ catalog: catalog(duplicateEntries) }); + const shortFixture = await createFixture({ catalog: catalog(createEntries().slice(0, -1)) }); + t.after(duplicateFixture.cleanup); + t.after(shortFixture.cleanup); + + await assert.rejects(extractCatalog(duplicateFixture), /duplicate slug/); + await assert.rejects(extractCatalog(shortFixture), /exactly 24/); +}); + +test("refuses a source commit other than the catalog snapshot", async (t) => { + const wrongCatalog = catalog(); + wrongCatalog.source_commit = "a".repeat(40); + const fixture = await createFixture({ catalog: wrongCatalog }); + t.after(fixture.cleanup); + + await assert.rejects(extractCatalog(fixture), /pinned source commit/); +}); + +test("writes immutable source links and deterministic content", async (t) => { + const fixture = await createFixture(); + t.after(fixture.cleanup); + + const first = await extractCatalog(fixture); + const second = await extractCatalog(fixture); + + assert.equal(first.pages[0].content_digest, second.pages[0].content_digest); + assert.match(first.pages[0].source_url, /github\.com\/runxhq\/runx\/blob\/[0-9a-f]{40}\//); + assert.match(first.pages[0].content_digest, /^sha256:[0-9a-f]{64}$/); +}); + +test("preserves the maintained introduction while removing stale generated pages", async (t) => { + const fixture = await createFixture(); + t.after(fixture.cleanup); + await mkdir(fixture.outputDir, { recursive: true }); + await writeFile(path.join(fixture.outputDir, "introduction.md"), "# Introduction\n", "utf8"); + await writeFile(path.join(fixture.outputDir, "obsolete.md"), "obsolete\n", "utf8"); + + await extractCatalog(fixture); + + assert.equal(await readFile(path.join(fixture.outputDir, "introduction.md"), "utf8"), "# Introduction\n"); + await assert.rejects(readFile(path.join(fixture.outputDir, "obsolete.md"), "utf8"), /ENOENT/); +}); + +test("rewrites source-relative Markdown links to the pinned upstream commit", async (t) => { + const [firstEntry, secondEntry] = createEntries(); + const fixture = await createFixture({ + firstContent: [ + "---", + `name: ${firstEntry.name}`, + "---", + "[schema](../../schemas/example.json)", + `[sibling](../${secondEntry.name}/SKILL.md#inputs)`, + "![diagram](./diagram.png)", + "[anchor](#local)", + "[external](https://example.com/docs)", + "```md", + "[literal](../not-a-link.md)", + "```", + "````md", + "```md", + "[four-fence-literal](../also-not-a-link.md)", + "```", + "````", + "", + ].join("\n"), + }); + t.after(fixture.cleanup); + + const [page] = (await extractCatalog(fixture)).pages; + const base = `https://github.com/runxhq/runx/blob/${catalog().source_commit}`; + assert.match(page.body, new RegExp(`${base}/schemas/example\\.json`)); + assert.match(page.body, new RegExp(`${base}/skills/${secondEntry.name}/SKILL\\.md#inputs`)); + assert.match(page.body, new RegExp(`${base}/skills/${firstEntry.name}/diagram\\.png`)); + assert.match(page.body, /\[anchor\]\(#local\)/); + assert.match(page.body, /\[external\]\(https:\/\/example\.com\/docs\)/); + assert.match(page.body, /\[literal\]\(\.\.\/not-a-link\.md\)/); + assert.match(page.body, /\[four-fence-literal\]\(\.\.\/also-not-a-link\.md\)/); +}); + +test("retries transient failures without exceeding the configured concurrency cap", async (t) => { + let calls = 0; + let active = 0; + let maximumActive = 0; + const baseFixture = await createFixture(); + t.after(baseFixture.cleanup); + const retryFixture = { + ...baseFixture, + concurrency: 99, + fetchImpl: async (url) => { + calls += 1; + active += 1; + maximumActive = Math.max(maximumActive, active); + try { + if (calls === 1) return { ok: false, status: 503, async json() { return {}; } }; + await new Promise((resolve) => setTimeout(resolve, 2)); + return baseFixture.fetchImpl(url); + } finally { + active -= 1; + } + } + }; + + const result = await extractCatalog(retryFixture); + + assert.equal(result.pages.length, 24); + assert.equal(calls, 25); + assert.equal(maximumActive <= 4, true); +}); + +test("stops after three transient status attempts", async (t) => { + let calls = 0; + const fixture = await createFixture({ + concurrency: 1, + fetchImpl: async () => { + calls += 1; + return { ok: false, status: 503, headers: new Headers() }; + } + }); + t.after(fixture.cleanup); + + await assert.rejects(extractCatalog(fixture), /failed after 3 attempts/); + assert.equal(calls, 3); +}); + +test("uses environment tokens only in a redacted authorization header", async (t) => { + const githubToken = "fixture-github-token"; + const ghToken = "fixture-gh-token"; + const fixture = await createFixture(); + t.after(fixture.cleanup); + const headerChecks = []; + const urls = []; + const fetchImpl = async (url, options) => { + urls.push(url); + headerChecks.push(options.headers.Authorization === `Bearer ${githubToken}`); + return fixture.fetchImpl(url, options); + }; + + await withTokenEnvironment({ githubToken, ghToken }, () => extractCatalog({ ...fixture, fetchImpl })); + + assert.equal(headerChecks.length, 24); + assert.equal(headerChecks.every(Boolean), true); + assert.equal(urls.every((url) => !url.includes(githubToken) && !url.includes(ghToken)), true); + + const fallbackChecks = []; + await withTokenEnvironment({ githubToken: undefined, ghToken }, () => extractCatalog({ + ...fixture, + fetchImpl: async (url, options) => { + fallbackChecks.push(options.headers.Authorization === `Bearer ${ghToken}`); + return fixture.fetchImpl(url, options); + } + })); + assert.equal(fallbackChecks.every(Boolean), true); + + const transportFailure = await createFixture({ + fetchImpl: async () => { + throw new TypeError(`transport failed ${githubToken}`); + } + }); + t.after(transportFailure.cleanup); + const error = await withTokenEnvironment( + { githubToken, ghToken: undefined }, + () => captureRejection(() => extractCatalog(transportFailure)) + ); + assert.equal(error.message.includes(githubToken), false); + assert.match(error.message, /REDACTED/); +}); + +test("identifies rate-limited 403 responses and gives actionable reset guidance", async (t) => { + let calls = 0; + const reset = Math.ceil(Date.now() / 1000) + 3600; + const fixture = await createFixture({ + concurrency: 1, + fetchImpl: async () => { + calls += 1; + return { + ok: false, + status: 403, + headers: new Headers({ + "x-ratelimit-remaining": "0", + "x-ratelimit-reset": String(reset) + }) + }; + } + }); + t.after(fixture.cleanup); + + await assert.rejects( + extractCatalog(fixture), + (error) => /rate limit/i.test(error.message) + && error.message.includes(new Date(reset * 1000).toISOString()) + && /cannot wait more than 30 seconds/i.test(error.message) + && error.message.includes("GITHUB_TOKEN") + && error.message.includes("GH_TOKEN") + ); + assert.equal(calls <= 3, true); +}); + +test("identifies a secondary rate-limit 403 from Retry-After", async (t) => { + const fixture = await createFixture({ + fetchImpl: async () => ({ + ok: false, + status: 403, + headers: new Headers({ "retry-after": "120" }), + async json() { + return { message: "request throttled" }; + } + }) + }); + t.after(fixture.cleanup); + + await assert.rejects( + extractCatalog(fixture), + (error) => /secondary rate limit/i.test(error.message) + && /120 seconds/i.test(error.message) + && /cannot wait more than 30 seconds/i.test(error.message) + && error.message.includes("GITHUB_TOKEN") + && error.message.includes("GH_TOKEN") + ); +}); + +test("identifies a secondary rate-limit 403 from its response message", async (t) => { + const fixture = await createFixture({ + fetchImpl: async () => ({ + ok: false, + status: 403, + headers: new Headers(), + async json() { + return { message: "You have exceeded a secondary rate limit. Please wait a few minutes before retrying." }; + } + }) + }); + t.after(fixture.cleanup); + + await assert.rejects( + extractCatalog(fixture), + (error) => /secondary rate limit/i.test(error.message) + && /retry later/i.test(error.message) + && error.message.includes("GITHUB_TOKEN") + && error.message.includes("GH_TOKEN") + ); +}); + +test("rejects non-canonical base64 that decodes to valid bytes", async (t) => { + const fixture = await createFixture({ + fetchImpl: async () => ({ + ok: true, + status: 200, + async json() { + return { + type: "file", + encoding: "base64", + content: "Zh==", + sha: blobSha("f") + }; + } + }) + }); + t.after(fixture.cleanup); + + await assert.rejects(extractCatalog(fixture), /canonical base64/); +}); + +test("rejects valid base64 when the returned blob SHA does not match", async (t) => { + const fixture = await createFixture({ + fetchImpl: async () => ({ + ok: true, + status: 200, + async json() { + return { + type: "file", + encoding: "base64", + content: Buffer.from("valid content", "utf8").toString("base64"), + sha: "0".repeat(40) + }; + } + }) + }); + t.after(fixture.cleanup); + + await assert.rejects(extractCatalog(fixture), /blob SHA mismatch/); +}); + +test("accepts exactly 1 MiB and rejects 1 MiB plus one byte", async (t) => { + const exactContent = Buffer.alloc(1024 * 1024, 0x61); + const exactFixture = await createFixture({ + fetchImpl: async (url) => sourceResponse(url.includes("/skills/agency/") ? exactContent : "small\n") + }); + t.after(exactFixture.cleanup); + const exactResult = await extractCatalog(exactFixture); + assert.equal(exactResult.pages.length, 24); + + const oversizedFixture = await createFixture({ + fetchImpl: async () => sourceResponse(Buffer.alloc(1024 * 1024 + 1, 0x61)) + }); + t.after(oversizedFixture.cleanup); + await assert.rejects(extractCatalog(oversizedFixture), /exceeds 1 MiB/); +}); + +test("atomically replaces an expected page symlink without writing its target", async (t) => { + const fixture = await createFixture(); + t.after(fixture.cleanup); + const externalPath = path.join(path.dirname(fixture.outputDir), "external-expected.md"); + const expectedPath = path.join(fixture.outputDir, "agency.md"); + await mkdir(fixture.outputDir, { recursive: true }); + await writeFile(externalPath, "outside expected\n", "utf8"); + await symlink(externalPath, expectedPath); + + await extractCatalog(fixture); + + assert.equal(await readFile(externalPath, "utf8"), "outside expected\n"); + assert.equal((await lstat(expectedPath)).isSymbolicLink(), false); + assert.match(await readFile(expectedPath, "utf8"), /pinned content/); +}); + +test("removes a stale page symlink without removing or changing its target", async (t) => { + const fixture = await createFixture(); + t.after(fixture.cleanup); + const externalPath = path.join(path.dirname(fixture.outputDir), "external-stale.md"); + const stalePath = path.join(fixture.outputDir, "obsolete.md"); + await mkdir(fixture.outputDir, { recursive: true }); + await writeFile(externalPath, "outside stale\n", "utf8"); + await symlink(externalPath, stalePath); + + await extractCatalog(fixture); + + assert.equal(await readFile(externalPath, "utf8"), "outside stale\n"); + await assert.rejects(lstat(stalePath), /ENOENT/); +}); + +test("rejects a symlink output directory without touching its target", async (t) => { + const fixture = await createFixture(); + t.after(fixture.cleanup); + const targetDir = path.join(path.dirname(fixture.outputDir), "outside-pages"); + const staleTarget = path.join(targetDir, "obsolete.md"); + await mkdir(targetDir, { recursive: true }); + await writeFile(staleTarget, "outside directory\n", "utf8"); + await symlink(targetDir, fixture.outputDir); + + await assert.rejects(extractCatalog(fixture), /output directory.*symbolic link/i); + + assert.deepEqual(await readdir(targetDir), ["obsolete.md"]); + assert.equal(await readFile(staleTarget, "utf8"), "outside directory\n"); +}); + +test("preserves leading thematic-break Markdown that is not source frontmatter", async (t) => { + const thematicBreak = "---\n\nAuthored prose between thematic breaks.\n\n---\n\n# Authored heading\n"; + const fixture = await createFixture({ fetchImpl: async () => sourceResponse(thematicBreak) }); + t.after(fixture.cleanup); + + const result = await extractCatalog(fixture); + + assert.equal(result.pages[0].body.includes(thematicBreak), true); +}); + +test("rejects malformed source frontmatter instead of deleting authored text", async (t) => { + const malformed = "---\nname: agency\nthis is not a mapping\n---\n# Authored heading\n"; + const fixture = await createFixture({ fetchImpl: async () => sourceResponse(malformed) }); + t.after(fixture.cleanup); + + await assert.rejects(extractCatalog(fixture), /malformed YAML frontmatter/); +}); + +test("rejects frontmatter with real YAML syntax errors", async (t) => { + const fixture = await createFixture({ + fetchImpl: async (url) => { + const skillName = /\/skills\/([^/]+)\/SKILL\.md/.exec(new URL(url).pathname)[1]; + return sourceResponse(`---\nname: ${skillName}\ndescription: \"unterminated\n---\n# Authored heading\n`); + } + }); + t.after(fixture.cleanup); + + await assert.rejects(extractCatalog(fixture), /malformed YAML frontmatter/); +}); + +test("rejects a non-canonical name even when count and shape are valid", async (t) => { + const entries = createEntries(); + entries[0] = { + name: "agency-copy", + slug: "agency-copy", + group: "Operate", + path: "skills/agency-copy/SKILL.md" + }; + const fixture = await createFixture({ + catalog: catalog(entries), + fetchImpl: async () => sourceResponse("# source\n") + }); + t.after(fixture.cleanup); + + await assert.rejects(extractCatalog(fixture), /canonical name/); +}); + +test("requires every slug to equal its canonical name", async (t) => { + const entries = createEntries(); + entries[0] = { ...entries[0], slug: "agency-copy" }; + const fixture = await createFixture({ + catalog: catalog(entries), + fetchImpl: async () => sourceResponse("# source\n") + }); + t.after(fixture.cleanup); + + await assert.rejects(extractCatalog(fixture), /slug must equal name/); +}); + +test("requires the exact canonical groups and memberships", async (t) => { + const entries = createEntries().map((entry) => ({ ...entry, group: "Operate" })); + const fixture = await createFixture({ + catalog: { + source_repository: "https://github.com/runxhq/runx", + source_commit: sourceCommit, + groups: [{ name: "Operate", entries }] + }, + fetchImpl: async () => sourceResponse("# source\n") + }); + t.after(fixture.cleanup); + + await assert.rejects(extractCatalog(fixture), /canonical groups/); +}); + +test("rejects malformed content, blob mismatches, and stale generated pages", async (t) => { + const malformedFixture = await createFixture({ + fetchImpl: async () => ({ + ok: true, + status: 200, + async json() { + return { type: "file", encoding: "base64", content: "not valid!", sha: "0".repeat(40) }; + } + }) + }); + t.after(malformedFixture.cleanup); + await assert.rejects(extractCatalog(malformedFixture), /base64/); + + const staleFixture = await createFixture(); + t.after(staleFixture.cleanup); + await mkdir(staleFixture.outputDir, { recursive: true }); + await writeFile(path.join(staleFixture.outputDir, "obsolete.md"), "obsolete\n", "utf8"); + await extractCatalog(staleFixture); + await assert.rejects(readFile(path.join(staleFixture.outputDir, "obsolete.md"), "utf8"), /ENOENT/); +}); diff --git a/docs/sourcey-catalog/gaps.md b/docs/sourcey-catalog/gaps.md new file mode 100644 index 000000000..8398be265 --- /dev/null +++ b/docs/sourcey-catalog/gaps.md @@ -0,0 +1,21 @@ +# Sourcey Catalog Documentation Gaps + +Measured from 24 generated skill pages at commit `5afc25a83edf1c1320df7ac0d78c36f1523b5677`. + +## Selected skills lack a worked example section + +- Affected source paths: `skills/work-plan/SKILL.md`, `skills/deep-research/SKILL.md`, `skills/research/SKILL.md`, `skills/knowledge-router/SKILL.md`, `skills/issue-intake/SKILL.md`, `skills/issue-triage/SKILL.md`, `skills/issue-to-pr/SKILL.md`, `skills/release/SKILL.md`, `skills/review-receipt/SKILL.md`, `skills/governed-outbound/SKILL.md` +- Measured fact: 10 of 24 selected skills do not contain the measured heading. +- Why it matters: Operators cannot validate the expected input-to-output flow from the reference page alone. + +## Selected skills lack a dedicated edge-case or stop-condition section + +- Affected source paths: `skills/work-plan/SKILL.md`, `skills/deep-research/SKILL.md`, `skills/research/SKILL.md`, `skills/knowledge-router/SKILL.md`, `skills/issue-intake/SKILL.md`, `skills/issue-triage/SKILL.md`, `skills/issue-to-pr/SKILL.md`, `skills/release/SKILL.md`, `skills/review-receipt/SKILL.md`, `skills/run-history/SKILL.md` +- Measured fact: 10 of 24 selected skills do not contain the measured heading. +- Why it matters: Operators have no single place to check refusal, escalation, and terminal behavior. + +## Selected skills lack a dedicated when-not-to-use section + +- Affected source paths: `skills/work-plan/SKILL.md`, `skills/deep-research/SKILL.md`, `skills/research/SKILL.md`, `skills/knowledge-router/SKILL.md`, `skills/issue-intake/SKILL.md`, `skills/issue-triage/SKILL.md`, `skills/issue-to-pr/SKILL.md`, `skills/release/SKILL.md`, `skills/review-receipt/SKILL.md` +- Measured fact: 9 of 24 selected skills do not contain the measured heading. +- Why it matters: Operators must infer when a different skill or workflow is the safer choice. diff --git a/docs/sourcey-catalog/pages/agency.md b/docs/sourcey-catalog/pages/agency.md new file mode 100644 index 000000000..3fe1c02f0 --- /dev/null +++ b/docs/sourcey-catalog/pages/agency.md @@ -0,0 +1,136 @@ +# agency + +- Group: Operate +- Source: [skills/agency/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/agency/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/agency/SKILL.md` + +# Agency + +Run a standing, accountable team toward a mandate, one governed turn at a time. + +An agency is the only runx skill that holds a roster, a persistent objective, and a +case that spans turns. It is a governed delegation envelope: a defined set of members +with scope ceilings, a mandate, cumulative limits, and a case whose every turn is +sealed and replayable. It composes the existing skills and reimplements none. Each +turn borrows `ops-desk` for judgment, the roster members for execution, `data-store` +for the event log, and receipts for the ledger. + +It is not a durable-execution engine and it is not an autonomous daemon. One turn is +one stateless governed act; an external driver (a human, a cron, a board poll) runs +the loop by calling `advance` until the case resolves. + +## What this skill does + +- `open` starts a case: it appends `opened` with the mandate, the roster, and the + cumulative limits snapshot, so the charter travels with the case. +- `advance` runs one turn: it folds the case from its event stream, asks `ops-desk` + for the single next move constrained to the roster, enforces the measurable gate, + records one turn event whose append is the contention lease, and names the member + to run. The member runs as a separate governed run; its outcome is fed back to the + next `advance` as `member_result`. +- `status` folds and returns the current case state. + +The case reducer is the agency's own code, because `data-store` carries events but +does not fold domain state. Everything else is delegation. + +## When to use this skill + +- A standing, consequential mandate must run for days or weeks, dispatch different + members, and leave an auditable trail sealed to a bounded authority. +- A process needs scoped delegation with a measurable ceiling and a human gate on + consequence, not an unbounded agent. + +## When not to use this skill + +- One-shot or interactive work. Call the member skills directly; the agency is + overhead when the operator is already the loop. +- To compute proposals (that is `ops-desk`) or claim and clock logic (that is + `messageboard`). Compose them. +- To bake a storage backend. The case lives in `data-store` via `data_source_ref`. +- To let the model invent the roster, the mandate, or the limits. They are operator + config, snapshotted into the case at `open`. + +## Procedure + +1. `open` the case with the mandate, roster, and limits. +2. `advance` the case. Read the turn packet: + - `advanced`: run the named member under its scope, then `advance` again with the + member's outcome as `member_result`. + - `awaiting_approval`: resolve the escalation, then `advance`. + - `resolved` or `failed`: the case is closed. +3. Repeat until the case resolves. The driver, not this skill, decides the cadence. + +## The measurable gate + +The done-check and the limit-check are measurable first. `advance` folds cumulative +totals (acts, spend) and the trusted planner overrides the model when a cap is +breached: an over-cap turn fails regardless of what `ops-desk` proposed. The narrative +judgment from `ops-desk` chooses the move within the caps; it never widens them. +Spend caps tracked in the projection are the v1 path; routing spend through `spend` +and runx-pay reservations is the stronger enforcement. + +## Contention + +Two drivers must not double-fire a member act. Each turn appends a single event keyed +`case_id:turn:driver_id` at the folded `expected_version`. Two drivers racing the same +turn carry different keys, so the loser hits a hard version conflict rather than +replaying the winner, and stops before any dispatch. The append is the lease, and it +lands before the named member runs. + +## Edge cases and stop conditions + +- No case at `case_id`: `advance` returns `needs_input`; open the case first. +- A cumulative cap is reached: the turn is `failed` with the breached predicate named. +- The best move is consequential and unapproved: `awaiting_approval` with the prompt. +- No roster member can act and nothing is escalatable: escalate to the configured + human with the missing input named. + +## Output schema + +`advance` returns one `agency_turn`: + +```yaml +agency_turn: + schema: runx.agency.turn.v1 + status: advanced | awaiting_approval | resolved | needs_input | failed + case_id: string + turn: number + dispatch: # present when status == advanced + member: string + skill: string + task: string + needed_scope: [string] + approval_prompt: string | null + resolution: object | null + predicates: object # the measurable over_limits booleans + reason: string | null + next: string +``` + +## Inputs + +- `open`: `data_source_ref`, `case_id`, `agency_ref`, `mandate`, `roster`, `limits`, + optional `signal`. +- `advance`: `data_source_ref`, `case_id`, `driver_id`, optional `member_result`. +- `status`: `data_source_ref`, `case_id`. + +## Worked example + +Open a docs case with a researcher, writer, and reviewer and a 50-turn limit. +`advance` folds an empty-but-opened case, ops-desk picks the researcher, and the turn +returns `advanced` naming the researcher. The driver runs the researcher and calls +`advance` again with its result; ops-desk now picks the writer to draft. When the +reviewer approves and the projection shows the docs current, `advance` returns +`resolved`. + +## Turn rules + +- Fold every turn from the sealed stream; never infer state the events do not show. +- Enforce the measurable gate before the model's judgment; never widen a cap. +- Name the member and the verification expectation on every dispatch; never claim + work settled, sent, paid, or done without a receipt. +- Compose ops-desk, data-store, and the members; never reimplement them, and never + invent the roster or the mandate. +- Stop cleanly with needs_input, awaiting_approval, refused, or failed; never a fake + ready. diff --git a/docs/sourcey-catalog/pages/audit-receipt.md b/docs/sourcey-catalog/pages/audit-receipt.md new file mode 100644 index 000000000..9c2249a66 --- /dev/null +++ b/docs/sourcey-catalog/pages/audit-receipt.md @@ -0,0 +1,144 @@ +# audit-receipt + +- Group: Safety and review +- Source: [skills/audit-receipt/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/audit-receipt/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/audit-receipt/SKILL.md` + +# Receipt Auditor + +Audit a sealed run for authority over-reach, using its own receipt as evidence. + +runx seals a receipt for every run: the authority proof, the acts performed, the +decisions taken, the refusals, and hashed material references. That receipt is +the evidence. This skill reads a sealed receipt and answers one governance +question: did the run stay inside the authority it was granted? It flags scopes +exercised that were never granted, mutating acts that ran without an approval +gate, refusals that were not recorded, and any raw secret material that leaked +into the receipt. It pairs with `least-privilege`: that one narrows a +grant from usage, this one verifies a run honored its grant. + +## What this skill does + +1. **Read the proof and the acts.** From the receipt, extract the granted + authority (the proof) and the scopes the acts actually exercised. +2. **Diff exercised against granted.** Any exercised scope not covered by the + proof is over-reach. +3. **Check the gates.** Every mutating act must show an approval gate in the + receipt; an ungated mutation is an anomaly. +4. **Check exposure.** The receipt must carry only hashed material references; a + raw secret in the receipt is a leak. +5. **Verdict.** `clean`, `anomaly`, or `needs_more_evidence`, with the exact + findings and a recommendation for each anomaly. + +## Core principles + +- **The receipt is the evidence.** Audit what the receipt records, not what the + skill claims it did. +- **Granted is the ceiling.** Exercised authority must be a subset of the proof; + anything beyond is over-reach, full stop. +- **Mutation needs a gate.** A mutating act with no approval gate in the receipt + is an anomaly even if it succeeded. +- **No raw material.** A receipt must reference material by hash; raw credential + material in a receipt is a leak, not a convenience. +- **Absence of evidence is not clean.** With no receipt or an unattributable + one, return `needs_more_evidence`, never `clean`. + +## When to use this skill + +- Post-run governance audit of a sealed, successful run. +- Spot-checking that a skill honored its authority bound in production. +- Before promoting a skill toward a higher trust posture. + +## When not to use this skill + +- To diagnose a failed run and propose a fix. That is `review-receipt` + (failure-to-improvement). This skill audits a sealed run for over-reach + (success-to-governance); the two are different lenses on a receipt. +- To narrow a grant from observed usage. That is `least-privilege`. + +## Diagnostics + +- `receipt.authority.over_reach` (error): an exercised scope is not covered by + the authority proof. +- `receipt.mutation.ungated` (error): a mutating act ran without an approval gate + recorded in the receipt. +- `receipt.refusal.unrecorded` (warning): a denied request is not reflected as a + sealed refusal. +- `receipt.material.exposed` (error): raw credential material appears in the + receipt instead of a hash reference. +- `receipt.clean` (info): exercised authority is within the grant, mutations are + gated, and no material is exposed. + +## Procedure + +1. Resolve the receipt from `receipt_id` or use the provided sanitized + `receipt_summary`. +2. Extract the authority proof, granted scopes, acts, approvals, refusals, + material references, and receipt signature metadata. +3. Normalize exercised scopes from the acts and compare them with the granted + scopes. Exercised must be a subset of granted. +4. Identify mutating acts and confirm each has an approval gate recorded in the + receipt. +5. Check that denied requests appear as sealed refusals when the receipt records + the attempt. +6. Scan receipt-visible material for raw credentials or secret-bearing payloads. +7. Return a verdict with findings, recommendations, and the success checkpoint. + +## Edge cases and stop conditions + +- **Missing receipt:** return `needs_more_evidence`; never infer a clean run. +- **Unattributable receipt:** return `needs_more_evidence` when the receipt + cannot be tied to the run under audit. +- **Malformed proof:** return `needs_more_evidence` unless enough normalized + grant data is supplied separately. +- **Unknown scope name:** treat it as over-reach unless the grant explicitly + covers it. +- **Mutation without recorded gate:** emit `receipt.mutation.ungated` even if the + mutation succeeded and the outcome looks correct. +- **Raw token, key, or credential in the receipt:** emit + `receipt.material.exposed` and recommend revocation/rotation. + +## Output schema (`receipt_audit`) + +```yaml +decision: ready | needs_more_evidence +run_ref: string +granted_scopes: [string] +exercised_scopes: [string] +refusals: [string] +findings: + - id: string + severity: error | warning | info + message: string +verdict: clean | anomaly | needs_more_evidence +rationale: string +recommendations: [string] +success_checkpoint: + milestone: string + description: string +``` + +A `clean` verdict requires zero `error` findings. + +## Worked example + +A sealed run was granted `repo.read`. The receipt shows the acts exercised only +`repo.read`, every act is an observation (no mutation), and material is +referenced by hash. Exercised is a subset of granted, no mutation to gate, no +exposure: `verdict: clean`. Had an act exercised `repo.write` while the proof +granted only `repo.read`, that would raise `receipt.authority.over_reach` and a +`verdict: anomaly` with a recommendation to revoke the run's grant and +investigate. + +## Inputs + +- `receipt_id` (optional): the receipt id to audit. +- `receipt_summary` (optional): a sanitized receipt or its acts/proof summary + when the full receipt is not available. +- `granted_scopes` (optional): the authority the run was granted, when not + derivable from the receipt alone. +- `objective` (optional): operator intent that focuses the audit. + +At least one of `receipt_id` or `receipt_summary` is required; with neither, the +skill returns `needs_more_evidence`. diff --git a/docs/sourcey-catalog/pages/business-ops.md b/docs/sourcey-catalog/pages/business-ops.md new file mode 100644 index 000000000..52587434b --- /dev/null +++ b/docs/sourcey-catalog/pages/business-ops.md @@ -0,0 +1,191 @@ +# business-ops + +- Group: Operate +- Source: [skills/business-ops/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/business-ops/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/business-ops/SKILL.md` + +# Business Ops + +Turn one business signal into a replayable operations graph. + +`business-ops` is the generic public example for how runx makes agentic +business work composable without giving the agent ambient authority. It is a +deterministic graph skeleton: it classifies one signal, fans it into bounded +lanes, records why each lane exists, names the real skill or provider lane that +would replace the fixture, and stops before any live send, spend, publish, +merge, deploy, or customer-visible action. + +This is not a provider integration and not an operator dashboard. It is the +small core shape that teams copy when they want one objective to fan out into a +chain of skills, then replay that chain with receipts. + +When the route itself should become durable, use `route_and_append`. That runner +classifies the signal, appends the classification packet through `data-store`, +and reads back the projection. The same graph can use local JSON, SQLite, +Postgres, D1, Redis, or a product adapter by changing the `data_source_ref` +binding. + +## What this skill does + +- Classifies one business signal before doing work. +- Fans the signal through representative lanes: docs, release, issue/PR, + outreach planning, spend quoting, and proof audit. +- Produces structured lane packets with authority, gate, handoff, evidence, and + readback fields. +- Demonstrates the runx split between proposal work and consequential action: + drafts and plans can be produced, but sends, spend, merges, publishes, and + deploys require a separate approval and execution lane. +- Gives downstream agents a clear handoff target instead of vague prose. +- Optionally persists the classified route for replay through `data-store`. + +## What this skill deliberately does not do + +- It does not call private providers, mutate a repo, post to GitHub, send email, + schedule campaigns, move money, publish releases, or deploy services. +- It does not duplicate `ops-desk`, product operator skills, `send-as`, + vendor-specific provider skills, `release`, `issue-to-pr`, `spend`, or + receipt-audit skills. +- It does not turn "outbound marketing" into a hidden side effect. Outreach is + a plan lane here; real delivery routes to `send-as` and then a provider + adapter. Branded provider skills are concrete adapters, not branches in this + core graph. +- It does not treat the graph receipt as proof that an external provider action + happened. Provider actions need provider evidence and their own receipt. + +## When to use this skill + +- To show how runx chains skills into replayable business operations. +- To prototype a team-specific ops graph before wiring private provider tools. +- To route a product signal without giving the agent blanket repo, email, + wallet, or deployment access. +- To explain why a governed workflow is more useful than a one-shot prompt: + the route, stops, handoffs, and readbacks are explicit and replayable. +- To smoke-test graph execution and child receipts with no external account. + +## When not to use this skill + +- To run a production launch, incident, release, campaign, support reply, + payout, or spend flow as-is. Replace fixture lanes with real skills first. +- To approve a live send, spend, merge, publish, deploy, or customer-visible + action. +- To hide project policy, customer lists, credentials, wallet keys, provider + dumps, or private review context in the signal. +- To claim external work completed when only this fixture graph ran. + +## Mental model + +```text +signal -> classify -> fanout lanes -> approval stops -> governed handoffs -> proof +``` + +The useful part is the chain. A single objective becomes several typed packets: +some read-only, some draft-only, some blocked until approval, and one proof lane +that states how success should be verified later. A human, agent, dashboard, or +CI loop can replay the same route and see the same stops. + +## How this maps to real runx work + +- **Docs and public proof** route to a docs skill such as `sourcey` or a + product-owned documentation lane. +- **Release preparation** routes to `release`, with publish held behind a + release approval. +- **Code work** routes to `issue-to-pr` or a project-owned implementation lane, + with merge held behind review. +- **Outreach and customer communication** route first to `send-as`, then to a + provider adapter that implements the send lane. Branded provider skills are + the right place for vendor-specific compose, test, review, schedule, or send + details. Broad outbound marketing should be its own skill or product broadcast + skill, not extra logic hidden in this graph. +- **Spend and payments** route to quote or payout skills with caps, recipient, + rail, and settlement proof separated from the planning lane. +- **Proof** routes to receipt/history/audit skills and provider readbacks. + +The fixture `ops-lane` step simply returns these packets without performing the +handoff. In a real project, replace each fixture lane with the named governed +skill runner or provider tool. + +## Procedure + +1. Receive one concise `signal`. +2. Optionally receive `operator_context` with project constraints, policy, or + the concrete business situation. +3. Run `classify` first. It decides which lanes are relevant and what authority + class each lane belongs to. +4. Fan out docs, release, issue, outreach, spend, and proof packets. +5. Mark each lane as read-only, draft-only, approval-required, or proof-only. +6. Name the exact downstream handoff that should replace the fixture in a real + workflow. +7. Seal the graph so the route itself is replayable. +8. If using `route_and_append`, append the classification packet with an + idempotency key and expected version, then read back the projection. + +## Edge cases and stop conditions + +- **Missing signal:** return `needs_input`. There is no safe route. +- **Vague objective:** return a narrow classify packet and ask for the missing + product, audience, repo, release, amount, or provider context. +- **Live send without principal, audience, consent, digest, and approval:** stop + at the outreach lane and route to `send-as`. +- **Spend without amount, cap, recipient, rail, and approval:** stop at the + spend lane and route to a quote or payment skill. +- **Merge, publish, deploy, or destructive mutation without approval:** stop at + the relevant lane and name the missing gate. +- **Provider success without provider evidence:** do not mark complete. Route to + proof audit. +- **Secret or private data in the signal:** refuse to echo it into outputs; + require redacted context or a provider-side readback instead. + +## Output schema + +The graph output contains child step receipts plus one `lane_packet` per lane: + +```yaml +lane_packet: + schema: runx.business_ops_lane.v1 + lane: string + signal: string + status: ready | awaiting_approval | needs_input | refused + decision: route | prepare | draft | quote | verify | stop + kind: router | docs | release | work | outreach | spend | proof + consequence: read_only | draft | live_mutation | public_send | money_movement | proof + summary: string + why: string + authority: + requested: [string] + provided: fixture_only + gate: + approval_required: boolean + approval_gate: string | null + stop_reason: string | null + handoff: + interface: skill | graph | cli | hosted_api | workflow | provider_tool + lane_ref: string + runner_ref: string | null + command_hint: string | null + evidence: + inputs_required: [string] + readbacks: [string] + receipt_refs: [string] + risks: [string] + next: [string] +``` + +## Worked example + +```bash +runx skill business-ops \ + -i signal="launch readiness for API v2: docs, release, customer comms, and spend checks" \ + --json +``` + +The graph classifies the launch signal, prepares docs/release/work packets, +routes customer communication to an outreach plan, stops spend at a quote gate, +and names receipt/history checks that would prove later execution. No external +provider is called. + +## Inputs + +- `signal` (required): concise business operations signal to classify and route. +- `operator_context` (optional): product policy, project topology, audience + constraints, or known provider state. Context only, not authority. diff --git a/docs/sourcey-catalog/pages/cve-audit.md b/docs/sourcey-catalog/pages/cve-audit.md new file mode 100644 index 000000000..0b3cb8523 --- /dev/null +++ b/docs/sourcey-catalog/pages/cve-audit.md @@ -0,0 +1,124 @@ +# cve-audit + +- Group: Safety and review +- Source: [skills/cve-audit/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/cve-audit/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/cve-audit/SKILL.md` + +# Exact CVE Audit + +## What this skill does + +This skill audits exact npm versions from an immutable `package-lock.json` +against the public OSV API. It emits a machine-readable audit result, +`evidence.json`, and a finding-by-finding Markdown report. The governed graph in +`X.yaml` independently replays every query and seals a delivery packet only +when the reported and replayed advisory sets match exactly. + +It does not install dependencies, execute target code, mutate the target +repository, repair packages, or publish vulnerability claims. + +## When to use this skill + +Use it when a reviewer needs checkable evidence for the advisories affecting +the exact npm versions in a public, immutable lockfile. It is suitable for +dependency review, release triage, and reproducible security evidence where a +package-name-only or loose-range match would create false positives. + +## When not to use this skill + +Do not use it for a mutable branch URL, a manifest without exact installed +versions, a private lockfile without explicit read authority, non-npm +ecosystems, exploit development, or claims about transitive coverage when the +selected scope is `direct-production`. Do not treat missing OSV data as proof +that a package is safe. + +## Inputs + +- `target_name`: display name for the audited project. +- `target_repo`: public HTTPS source repository. +- `target_commit`: full 40-character immutable Git commit. +- `lockfile_url`: public HTTPS lockfile URL containing that commit. +- `dependency_scope`: `direct-production` by default, or `all-installed`. + +The caller authorizes only public reads of the pinned lockfile and OSV. No +credential, token, local project file, or private payload is an accepted input. + +## Procedure + +1. Validate that the repository and lockfile URLs use HTTPS, the commit is a + full Git hash, and the lockfile URL is pinned to that hash. +2. Fetch the lockfile and record its SHA-256 digest before parsing it. +3. Extract exact installed versions from lockfile versions 2 or 3. Preserve the + requested scope in every artifact. +4. Query OSV with `{ ecosystem: npm, package, version }` for every inventory + entry. Exclude withdrawn advisories. +5. Record each finding with dependency, exact version, advisory ID, OSV URL, + aliases, installed path, and the exact query that produced it. +6. In the governed graph, replay every exact-version query in a separate step. + Fail closed if an advisory is unsupported, omitted, withdrawn, or changed. +7. Emit the report, evidence, verification, delivery packet, and sealed receipt. + +## Edge cases and stop conditions + +- Refuse non-HTTPS or mutable lockfile URLs. +- Refuse abbreviated commits and lockfiles that do not contain the commit. +- Refuse unsupported lockfile versions or entries without exact versions. +- Return `needs_input` when the target, commit, or lockfile evidence is not + immutable enough to support reproducible review. +- Stop if OSV times out, returns malformed data, or changes between scan and + independent replay. +- Stop if any false hit or missing hit is detected. +- Report the selected dependency scope; never imply broader coverage. +- Keep tokens, credentials, private source, and target code out of artifacts. + +## Output schema + +The scan emits: + +```json +{ + "audit_result": { + "schema": "runx.security.exact_cve_audit.v1", + "target": { + "repo": "https://github.com/owner/repo", + "commit": "40-character hash", + "lockfile_sha256": "64-character digest" + }, + "dependency_scope": "direct-production", + "inventory": [], + "findings": [], + "result": { + "exact_dependencies_queried": 0, + "advisory_findings": 0, + "source": "OSV" + } + }, + "report": { "artifact": { "path": "artifacts/report.md" } }, + "evidence": { + "summary": "human-readable audit summary", + "observations": [], + "artifact": { "path": "artifacts/evidence.json" } + } +} +``` + +The graph adds `verification.json`, `delivery.json`, and a sealed +`runx.receipt.v1` graph receipt with child receipt lineage. + +## Worked example + +For OWASP NodeGoat at commit +`c5cb68a7084e4ae7dcc60e6a98768720a81841e8`, the checked-in harness reads the +pinned lockfile, audits 16 exact direct production versions, reports the OSV +advisories returned for those versions, and independently requires zero false +and zero missing hits before sealing. + +The second harness case audits the repository's immutable clean fixture at +commit `83d554f904f9f73bd26ce9c15e3786ba7d62b1de`. It verifies that an exact +dependency with no OSV advisory produces zero findings while still completing +the independent replay and sealed-receipt path. + +If the same request points at `main/package-lock.json`, validation returns a +needs-input failure because the lockfile is mutable and does not contain the +declared immutable commit. No audit or receipt is presented as complete. diff --git a/docs/sourcey-catalog/pages/data-store.md b/docs/sourcey-catalog/pages/data-store.md new file mode 100644 index 000000000..795c6cc35 --- /dev/null +++ b/docs/sourcey-catalog/pages/data-store.md @@ -0,0 +1,294 @@ +# data-store + +- Group: Research and data +- Source: [skills/data-store/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/data-store/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/data-store/SKILL.md` + +# Data Store + +Operate a data source through a governed adapter contract. This skill gives an +agent enough context to read, append, or project state without learning provider +secrets, inventing SQL, or depending on one storage backend. + +The storage backend can be Postgres, SQLite, D1, Redis, DynamoDB, S3, a ledger, +or a product API. The runx boundary is the same: a declared data source exposes +typed operations; the graph supplies bounded params; the adapter executes the +operation; the receipt records the resource, authority, idempotency, version, +digest, and redaction evidence. + +## Adapter selection + +The operator chooses a data source at run time. The skill receives +`data_source_ref` and operation inputs; project or hosted configuration binds +that ref to the concrete adapter. A local development ref might be +`local://runx-data-store/dev-board`. A production ref might be +`tenant://acme/board` bound to `data.postgres`, `data.d1`, `data.redis`, or a +product-owned HTTP adapter. + +Do not put provider logic in the domain skill. Messageboard, CRM, support, and +business-ops skills should ask for durable facts to be read or written; the data +source binding decides whether those facts live in local JSON, SQL, Redis, D1, +object storage, or a product API. Switching providers is a binding change, not a +rewrite of the skill. + +The bundled OSS profile calls `data.source`. Unbound `local://...` refs default +to durable local SQLite under `.runx/data/local-sources/`, with one source-scoped +database file per logical ref, so stateful skills can be dogfooded without +standing up hosted infrastructure. Pass `store_id` only when a fixture +intentionally wants the deterministic `data.local` JSON store. The graph inputs +stay the same when a project later binds the source to Postgres, Redis, D1, +object storage, or a product API. + +Adapter preference is operator configuration, not model choice. To choose Redis, +SQLite, or a hosted provider, bind the same `data_source_ref` through +`RUNX_DATA_SOURCES` or `.runx/data-sources.json`; do not add provider branches to +the domain skill. + +## What this skill does + +- Reads data through named queries or read operations declared by a data-source + adapter. +- Appends state transitions with idempotency keys and expected versions. +- Reads projections, event streams, or bounded latest-stream-head pages so + loops can resume from explicit state without exporting full history. +- Produces receipt-bound evidence for data source, resource, operation, params, + row/event limits, versions, and output digests. +- Keeps product semantics outside the data layer. Messageboards, CRMs, billing + ledgers, and support desks define their own events and reducers. +- Ships a fixture adapter (`data.local`), durable local SQLite adapter + (`data.sqlite`), and Redis adapter (`data.redis`) behind the same operation + envelope. + +## When to use this skill + +- A graph needs durable state between turns, such as queue position, board + state, sync cursor, review status, or approval inbox state. +- A skill must query a bounded slice of product data before deciding the next + action. +- A workflow needs to append an auditable event or effect transition with + optimistic concurrency. +- An operator wants one provider-agnostic shape that can later move from local + JSON or SQLite to Postgres, Redis, D1, Supabase, Turso, DynamoDB, or another + store. + +## When not to use this skill + +- To let a model write arbitrary SQL, Redis commands, or database migrations. +- To export broad data sets, secrets, raw PII, or unrestricted tables. +- To hide product decisions in storage code. Domain skills still own state + machines, acceptance criteria, and business rules. +- To treat a projection as independent truth when the event stream or receipt + chain is available and required for review. +- To bypass payment, send, deploy, moderation, or human approval gates. + +## Procedure + +1. Identify the domain skill and transition first. The data store is a carrier, + not the policy owner. +2. Select the logical data source. Use `data_source_ref` to name the project or + tenant source; let the project binding choose the adapter. Do not put raw + database URLs, provider credentials, or SQL in the skill input. +3. Select a declared operation: named read query, append event, read events, + read projection, or list stream heads. Do not synthesize raw provider + commands. +4. Check authority. Reads need the narrow resource/query scope; writes need the + transition scope, idempotency key, and expected version unless the operation + is explicitly append-only without concurrency. +5. Bind typed params. Enforce row/event limits, tenant/partition keys, and + redaction rules before the adapter runs. +6. For writes, use optimistic concurrency and idempotency. A retry with the same + idempotency key and same payload returns the existing effect; a different + payload under the same key is a conflict. +7. Return the operation result with resource refs, version movement, digests, + redaction notes, and stop conditions. Receipts should link this data effect + to the domain transition that caused it. + +## Edge cases and stop conditions + +- `needs_source`: the data source, resource, query name, tenant key, or schema + summary is missing. +- `needs_input`: required operation params are incomplete, malformed, or not + specific enough to bind a declared data-source operation. +- `needs_authority`: the caller lacks the declared read/write scope or provider + grant. +- `needs_version`: a mutating operation lacks `expected_version` where the data + source requires optimistic concurrency. +- `conflict`: the current version differs from `expected_version`, or an + idempotency key is reused with different content. +- `too_broad`: the requested read lacks partition filters, exceeds limits, or + asks for raw export. +- `redaction_required`: the operation would return secrets, private PII, or + fields outside the declared projection. +- `provider_unavailable`: the adapter cannot reach the data source, times out, + or cannot prove whether a write committed. + +## Output schema + +All runners return `runx.data.operation_result.v1`: + +```json +{ + "schema": "runx.data.operation_result.v1", + "data_source_ref": "local://example", + "provider": "local-json-event-store", + "operation": "append_event", + "resource": "board_events", + "aggregate_id": "posting-123", + "status": "committed", + "before_version": 0, + "after_version": 1, + "idempotency_key": "posting-123:create", + "event_ref": "board_events:posting-123:1", + "result_digest": "sha256:...", + "projection_digest": "sha256:...", + "rows": [], + "events": [], + "redactions": [], + "stop_conditions": [] +} +``` + +Provider adapters may add provider evidence under `provider_evidence`, but they +must not expose credentials or raw secret material. + +For event streams, adapters derive a readable `event_type` in this order: +explicit `event.type`, explicit `event.event_type`, then +`event.effect_family + "." + event.operation`. Domain skills that emit the +generic `runx.effect.transition.v1` packet should include `effect_family` and +`operation` on every event so readback projections say `messageboard.accept`, +`business_ops.route`, or another meaningful transition instead of `data.event`. + +## Worked example + +A messageboard skill decides that `posting.claimed` is allowed. It emits a +domain transition packet. The graph then calls `data-store.append_event` with +resource `board_events`, aggregate id `posting-123`, expected version `2`, and +idempotency key `posting-123:claim:agent-9`. The data adapter appends the event +only if the stream is still at version `2`. The receipt proves the decision, +the data operation, and the new version. A later loop turn calls +`data-store.read_events` or `read_projection` to resume from the explicit board +state. + +## Inputs + +- `data_source_ref` (required): stable logical ref for the data source. The + project or hosted binding maps this ref to the concrete adapter and provider + profile. +- `resource` (required): declared resource, stream, table, keyspace, or + projection name. +- `operation` (required for tool-level use): `append_event`, `read_events`, + `read_projection`, or `list_stream_heads`. +- `aggregate_id` (required for event operations): stream or partition key. +- `event` (required for `append_event`): domain event or transition packet. +- `idempotency_key` (required for writes): stable retry key. +- `expected_version` (required when the source enforces concurrency): current + stream/resource version expected by the caller. +- `limit` (optional): maximum rows or events to return. +- `after_version` (optional for `read_events`): return an ascending page whose + event versions are strictly greater than this value. Omit it to retain the + existing latest-tail read. Compare the last returned event version with + `after_version` in the result envelope to know whether another page remains. +- `event_types` (optional for `list_stream_heads`): at most 20 exact latest + event types. No pattern or arbitrary field queries are accepted. +- `cursor` (optional for `list_stream_heads`): opaque cursor returned by the + previous page. Limits are capped at 100. +- `store_id` (local fixture adapter only): deterministic local store id that + opts into the bundled `data.local` proof adapter. Omit it for durable local + SQLite. Production adapters should ignore it. + +## Invocation examples + +Durable local dogfood with the bundled default: + +```bash +runx skill data-store append_event \ + -i data_source_ref=local://runx-data-store/dev-board \ + -i resource=board_events \ + -i aggregate_id=posting-123 \ + --input-json expected_version=0 \ + -i idempotency_key=posting-123:create:v1 \ + --input-json event='{"type":"posting.created","payload":{"title":"verify a receipt link"}}' \ + --json +``` + +Fixture-only dogfood can still use `store_id` to select the JSON fixture store: + +```bash +runx skill data-store append_event \ + -i data_source_ref=local://runx-data-store/dev-board \ + -i store_id=dev-board \ + -i resource=board_events \ + -i aggregate_id=posting-123 \ + --input-json expected_version=0 \ + -i idempotency_key=posting-123:create:v1 \ + --input-json event='{"type":"posting.created","payload":{"title":"fixture proof"}}' \ + --json +``` + +Production graph shape is the same at the skill boundary: + +```bash +runx skill data-store append_event \ + -i data_source_ref=tenant://acme/board \ + -i resource=board_events \ + -i aggregate_id=posting-123 \ + --input-json expected_version=2 \ + -i idempotency_key=posting-123:claim:agent-9 \ + --input-json event='{"type":"posting.claimed","payload":{"actor":"agent-9"}}' \ + --json +``` + +The second command only works once `tenant://acme/board` is bound to an +installed provider adapter. That binding is operator configuration and may name a +credential profile or hosted grant; it must not carry raw secrets. + +Project-specific SQLite uses the same command shape after binding the source: + +```json +{ + "data_sources": { + "tenant://acme/board": { + "adapter": "data.sqlite", + "database_path": ".runx/data/acme-board.sqlite", + "resources": { + "board_events": { + "kind": "event_stream", + "partition_key": "aggregate_id" + } + } + } + } +} +``` + +Pass that document through `RUNX_DATA_SOURCES` or `.runx/data-sources.json`. + +Redis uses the same skill and graph inputs. Only the binding changes: + +```json +{ + "data_sources": { + "tenant://acme/board": { + "adapter": "data.redis", + "endpoint": "redis://127.0.0.1:6379/0", + "key_prefix": "runx:{acme-board}", + "resources": { + "board_events": { + "kind": "event_stream", + "partition_key": "aggregate_id" + } + } + } + } +} +``` + +The Redis endpoint must not embed credentials. Use local unauthenticated Redis +for OSS dogfood, or put production secrets behind a runx credential profile or +hosted grant. For Redis Cluster, the binding's `key_prefix` must contain one +safe hash tag, such as `{acme-board}`, so the stream, idempotency, and head keys +touched by an append share one slot and update atomically. Stream-head pages use +stable keyset cursors rather than mutable offsets. Durable events and +dispositions must not receive TTLs; production Redis should enable persistence +and use a non-evicting policy. diff --git a/docs/sourcey-catalog/pages/deep-research.md b/docs/sourcey-catalog/pages/deep-research.md new file mode 100644 index 000000000..862809368 --- /dev/null +++ b/docs/sourcey-catalog/pages/deep-research.md @@ -0,0 +1,41 @@ +# deep-research + +- Group: Research and data +- Source: [skills/deep-research/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/deep-research/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/deep-research/SKILL.md` + +# Deep Research Brief + +This graph turns one important question into a decision-ready brief. + +It is for research that needs more than a quick answer but less than an open- +ended report. The output should feel like an operator memo: what the answer is, +what evidence supports it, what remains uncertain, and what posture the reader +should take next. + +Do not drift into a generic article, daily update, or trend recap. The point is +to help a human decide, not to narrate that research happened. + +Separate verified evidence from inference and carry unresolved questions into +the memo. The synthesis must say what the reader should monitor, do, defer, or +investigate next. Return `needs_more_evidence` when the packet cannot support a +recommendation, and `not_worth_publishing` when the answer is sound but does not +matter to the stated decision. + +## Output + +- `research_packet`: bounded evidence, confidence, inference, and open questions. +- `brief_draft`: the decision memo synthesized from that packet. +- `approval_decision`: review of the exact brief and its remaining uncertainty. +- `publish_packet`: approved brief and delivery metadata. + +## Inputs + +- `objective` (optional): specific question the brief should answer. +- `audience` (optional): primary reader for the memo. +- `channel` (optional): final delivery channel; defaults to `brief`. +- `domain` (optional): product, ecosystem, or market slice to bound the work. +- `operator_context` (optional): local decision context or evaluation lens. +- `target_entities` (optional): structured list of products, projects, + companies, or repos to keep in scope. diff --git a/docs/sourcey-catalog/pages/github-sync.md b/docs/sourcey-catalog/pages/github-sync.md new file mode 100644 index 000000000..b6c8008ef --- /dev/null +++ b/docs/sourcey-catalog/pages/github-sync.md @@ -0,0 +1,165 @@ +# github-sync + +- Group: GitHub and delivery +- Source: [skills/github-sync/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/github-sync/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/github-sync/SKILL.md` + +# GitHub Sync + +Decide exactly what state to move between a GitHub repo and the local graph, in +which direction, and whether the agent is even allowed to write. + +`github-sync` is the generic repo state connector. It turns a loose request like +"sync the open issues" into a bounded plan that names the resources, the +direction, the scope, the records it will touch, and the point where the run +must stop for a human. A pull is observation and stays inside `repo:read`. A +push is mutation and never proceeds without an explicit `repo:write` grant and +human approval. + +## What this skill does + +`github-sync` produces a sealed `sync_plan`: a scoped record of which GitHub +resources the run will pull or push, the scope it will use, the gates a write +must clear, and any blockers that stop the run cleanly. For a push it carries a +`diff_summary` described by digest and ref, never by raw body text, so a +reviewer can approve the shape of a change without leaking issue contents, +tokens, or PII into the plan or the receipt. + +The plan binds direction to scope. `pull` is read-only and lists the resources +it will fetch. `push` enumerates the mutations by ref and digest, marks +`approval_required: true`, and refuses to proceed past planning when the run +lacks a `repo:write` grant. + +This skill plans the sync; it does not perform the GitHub mutation itself. The +plan is the artifact a downstream adapter executes after the approval gate +clears. Planning and mutation stay on opposite sides of the gate so a review can +read intent before anything changes on the remote. + +When a sync loop needs a durable cursor, use `plan_and_append_cursor`. That +runner reads the cursor projection through `data-store`, plans the bounded sync, +appends the plan as a cursor event, and reads back the projection. The storage +provider is selected by `data_source_ref`, not by GitHub-specific code. + +## When to use this skill + +- An agent needs to fetch a bounded set of issues, threads, or PRs into the + local graph for triage or analysis. +- An agent needs to mirror local state back to GitHub (reopen, label, comment, + close) and the operator wants the write shape reviewed before it lands. +- A workflow must prove which repo, direction, and scope a sync used, with a + receipt that names the resources touched. +- A review needs to distinguish a read-only pull from a write that crossed an + approval gate. + +## When not to use this skill + +`github-sync` is the generic repo state connector. Reach for it when the job is +moving issue, thread, or PR state in or out, not authoring a change or composing +one comment. + +- To drive a thread through spec, build, review, and a draft PR. Use + `issue-to-pr`, which governs the full issue-to-PR lane. +- To draft one review comment on one PR. Use `pr-review-note`. +- To push without a named repo and direction. +- To carry raw issue bodies, comment text, access tokens, or contributor PII in + the plan or receipt. Reference them by digest, span, or ref only. +- To bypass the human approval gate on any write. + +## Procedure + +1. Resolve the target repo and confirm the run holds at least `repo:read`. +2. Read `direction`. `pull` is observation; `push` is mutation and changes the + gate posture. +3. Read `resources`. Bind the concrete set: issues, PRs, or threads, plus the + filters that bound it (state, label, author, range). An unbounded "all" + becomes a blocker until reconfirmed. +4. Read `scope`. A `push` requires `scope: write` backed by a real `repo:write` + grant. If a write is requested without that grant, stop and refuse rather + than downgrade to a silent pull. +5. For a `pull`, list `resources_touched` by ref and leave `diff_summary` empty. +6. For a `push`, build `diff_summary` as a list of intended mutations described + by ref and content digest, set `gates.approval_required: true`, and record + the approval reference once granted. +7. Record `scope_used` as the narrowest scope the plan actually needs. +8. Emit the smallest `sync_plan` an adapter can execute without widening + authority, and stop at the approval gate for any write. +9. For cursor-backed loops, read the cursor projection first, append one sync + plan event with an idempotency key and expected version, and read back the + projection before the next turn. + +## Edge cases and stop conditions + +- **Missing repo or direction:** return `needs_agent`; the sync target is + undefined. +- **Write requested without `repo:write`:** the request is `refused`; never + downgrade it to a silent pull. The plan stays unexecutable. +- **Unbounded resource set:** mark a blocker and require an explicit filter + before a push. +- **Approval absent or denied on a push:** keep the decision blocked and the + plan unexecutable; do not emit an executable mutation plan. +- **Raw bodies, tokens, or PII in the resource payload:** reference by digest + and ref; if redaction would remove the evidence needed to plan, return + `needs_agent`. + +## Output schema + +```yaml +sync_plan: + decision: ready | blocked | refused | needs_agent + repo: string # resolved owner/name target + direction: pull | push + resources_touched: # resources by ref; no raw bodies + - kind: issue | pr | thread + ref: string + selected_by: string # the filter that selected it + diff_summary: # push only; empty for a pull + - ref: string + op: string + digest: string + scope_used: string # narrowest scope, e.g. repo:read or repo:write + gates: + approval_required: boolean # true for any push + approval_ref: string # set once the write is approved + blockers: array # conditions that must clear before execution +``` + +`sync_plan` is a composable object. Downstream skills read it as arbitrary JSON; +the fields above are the contract a reviewer and adapter rely on. + +The receipt (`runx.receipt.v1`) carries the repo, direction, `scope_used`, the +resource refs touched, and the approval reference for a write. It carries no +issue bodies, comment text, tokens, or contributor PII; mutations appear as refs +and digests only. Default scope is `repo:read` and a `pull` never escalates; a +`push` needs an explicit `repo:write` grant plus human approval, so missing the +grant is a refusal and missing the approval keeps the plan blocked. + +## Worked example + +Input: "Sync the open triage issues into the graph" on `runxhq/runx`, with +`direction: pull`, `scope: read`, and a filter of `state:open label:triage`. + +Output: `decision: ready`; `direction: pull`; `scope_used: repo:read`; +`resources_touched` lists the two matched issues by ref and the filter that +selected each; `diff_summary` is empty and `gates.approval_required` is false. +No write grant is exercised and no approval gate is opened, because a pull is +pure observation. Had the same request asked to `push` labels without a +`repo:write` grant, the run would refuse instead of reading. + +Cursor-backed loop: + +```text +read cursor -> plan bounded pull/push -> append sync plan event -> read cursor +``` + +The cursor event stores refs, filters, digests, and gate status. It does not +store raw issue bodies, OAuth tokens, or write payload secrets. + +## Inputs + +- `repo` (required): target repository as `owner/name`. +- `direction` (required): `pull` or `push`. +- `resources` (required): structured selector for `issues`, `prs`, or `threads` + plus filters (state, label, author, range). +- `scope` (required): `read` or `write`. A `push` needs `write` backed by a real + `repo:write` grant. diff --git a/docs/sourcey-catalog/pages/governed-outbound.md b/docs/sourcey-catalog/pages/governed-outbound.md new file mode 100644 index 000000000..3c7d158b4 --- /dev/null +++ b/docs/sourcey-catalog/pages/governed-outbound.md @@ -0,0 +1,107 @@ +# governed-outbound + +- Group: Outbound and tooling +- Source: [skills/governed-outbound/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/governed-outbound/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/governed-outbound/SKILL.md` + +# Governed Outbound + +Take something from outside, make it safe to send, authorize the exact outbound +plan, and leave proof. `governed-outbound` prepares the boundary crossing; it +does not claim the configured provider delivered anything. + +It composes four catalog skills into one governed run: + +1. `web-fetch` gathers the source within an explicit host allowlist. +2. `redact-pii` scrubs personal data and returns a pass/hold verdict before any + of it can leave the boundary. +3. an approval gate holds the plan for a human, who sees the redaction verdict + and the residual risk, not the raw content. +4. `send-as` binds the scrubbed content, principal, audience, and provider lane + into an authorized plan. +5. `sign-receipt` seals the gather, scrub, approval, and plan. A separate + provider host must execute the plan, record delivery evidence, and read it + back before any caller may call the notification delivered. + +The point of the chain is the order. The scrub runs before authorization and the +human gate runs before `send-as`. This receipt proves preparation and authority, +not provider delivery. + +## What this skill does + +`governed-outbound` is a graph, not a single agent step. Each hop is a real +catalog skill with its own scope, and authority narrows at every branch: +`web-fetch` may only reach the allowlisted host, `redact-pii` may only read the +fetched content, the approval gate authorizes the plan, and `sign-receipt` may +only append to the ledger. Personal data never reaches the channel or the +receipt; the content travels by digest, and the redaction report carries class +and span offsets, never the values it found. + +## When to use this skill + +- An agent needs to prepare external information (an incident page, a changelog, + a status update, a thread) for a provider channel, and that information may + carry personal data. +- A workflow must prove that the proposed outbound content was scrubbed and the + exact plan was approved before provider execution. +- You want one receipt that links the source, the scrub verdict, the approval, + and the send plan. + +## When not to use this skill + +- To post content that was authored in-house and carries no external data. Call + `send-as` directly, then use the configured provider host. +- To gather a source with no intent to send it onward. Call `web-fetch`. +- To deliver without a human in the loop. The approval gate is the point; a + send that needs no review does not need this chain. +- To move money, change a repository, or unseal a secret. Those are other + governed lanes with their own gates. + +## How the chain is wired + +- `fetch-source` reads `url` and `allowlist` from the run inputs and returns + `fetch_result` with the content digest and extracted text. +- `scrub-content` takes `fetch-source`'s extracted text as `content`, runs in + `redact` mode, and returns `redaction_report` with the `ready` / `needs_review` + / `blocked` verdict, the detected spans, and `redacted_digest`. +- `approve-send` shows the approver the redaction `decision`, the + `residual_risk`, and the `redacted_digest`, then records an approval decision. +- `plan-notice` runs only when the approval is `true` and the redaction verdict + is `ready`; it plans the send of the scrubbed content to `channel` as + `principal`, naming the provider action a connector lane would run. +- `seal-run` attests the run, binding the source digest and the redacted digest + as evidence, and appends the receipt to the ledger. + +## Edge cases and stop conditions + +- **No `url` or `allowlist`:** the run returns `needs_agent`; there is nothing + to gather and no boundary to respect. +- **Host not allowlisted:** `web-fetch` returns `policy_denied` and the chain + stops before anything is read. +- **Redaction not `ready`:** a `needs_review` or `blocked` verdict fails the + send transition, so `plan-notice` never runs. Nothing leaves the boundary on a + hold verdict. +- **Approval denied or absent:** the send transition is not satisfied and the + chain stops at the gate, scrubbed but unsent. +- **Provider delivery fails downstream:** preserve this planning receipt, record + the provider failure in the executing host, and do not produce delivery + evidence or mark the action complete. + +## Output + +The run seals to `runx.receipt.v1`, linking each step's packet: +`fetch_result` (source + digest), `redaction_report` (verdict + spans + redacted +digest), `approval_decision` (the gate), `send_plan` (the authorization), and the +`attestation` (the seal). `send_plan` is authorization, not delivery evidence. +The receipt proves the preparation path without reconstructing the personal data +that was removed along the way. + +## Inputs + +- `url` (required): source to gather before preparing the notification. +- `allowlist` (required): hosts `web-fetch` is permitted to reach. +- `channel` (required): destination channel for the notification. +- `principal` (required): principal the notification is sent as. +- `claim` (optional): what the sealed attestation should assert about the run. +- `operator_context` (optional): boundary, audience, or compliance context. diff --git a/docs/sourcey-catalog/pages/introduction.md b/docs/sourcey-catalog/pages/introduction.md new file mode 100644 index 000000000..989b3c1c1 --- /dev/null +++ b/docs/sourcey-catalog/pages/introduction.md @@ -0,0 +1,12 @@ +--- +title: Introduction +description: Governed Runx skill catalog pinned to one upstream revision. +--- + +# Runx Governed Skill Catalog + +This catalog covers exactly 24 governed Runx skills at upstream commit `5afc25a83edf1c1320df7ac0d78c36f1523b5677`. + +The skills are organized into five groups: Operate, Research and data, GitHub and delivery, Safety and review, and Outbound and tooling. Each catalog page links to its authoritative `SKILL.md` at the pinned commit. + +This is a governed skill catalog, not a claim of complete Runx API coverage. diff --git a/docs/sourcey-catalog/pages/issue-intake.md b/docs/sourcey-catalog/pages/issue-intake.md new file mode 100644 index 000000000..baef2f10b --- /dev/null +++ b/docs/sourcey-catalog/pages/issue-intake.md @@ -0,0 +1,186 @@ +# issue-intake + +- Group: GitHub and delivery +- Source: [skills/issue-intake/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/issue-intake/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/issue-intake/SKILL.md` + +# Issue Intake + +Convert an inbound thread, support report, or operator request into one +explicit intake decision plus the parent change artifact that downstream +planning or mutation lanes must share. + +This skill does not mutate code, open tickets, or publish replies directly. Its +job is to classify the report, summarize it, draft the next helpful response, +and recommend the next governed lane. That next lane must be explicit: +`issue-to-pr`, `work-plan`, `reply-only`, or `manual-review`. + +In supervisor-style flows, `issue-intake` is also the commencement gate. It +decides whether work may start at all, whether the next step should stop at a +review comment first, and whether mutation is justified yet. A recommended lane +is not the same thing as build permission. + +Use `issue-to-pr` only when the requested change is bounded enough for one +governed remediation lane. Use `work-plan` for larger or multi-step +work. Use `reply-only` when the right answer is guidance rather than mutation. +Use `manual-review` when the report is ambiguous, risky, or missing key context. + +Ground category, severity, and routing in the visible request and supplied +product constraints. Put uncertainty in `operator_notes` instead of inventing +confidence. The suggested reply should sound like the project owner and lead +with the decision or next action, not read like a ticket macro. + +## Output Contract + +`intake_report` must contain: + +- `category`: one of `bug`, `feature_request`, `docs`, `billing`, `account`, + `question`, or `other` +- `severity`: one of `low`, `medium`, `high`, or `critical` +- `summary`: concise summary of the actual request or report +- `suggested_reply`: a user-facing reply draft or operator handoff note +- `recommended_lane`: `issue-to-pr`, `work-plan`, `reply-only`, or + `manual-review` +- `rationale`: why that lane is the right next step +- `needs_human`: boolean +- `operator_notes`: array of caveats, missing context, or escalation notes + +`intake_report` may also include supervisor-facing control fields: + +- `commence_decision`: `approve`, `hold`, `reject`, or `needs_human` +- `action_decision`: `proceed_to_build`, `proceed_to_plan`, + `request_review`, or `stop` +- `review_target`: `thread`, `outbox_entry`, or `none` +- `review_comment`: markdown comment body for the supervisor to post before the + next lane proceeds + +When present, these fields mean: + +- `commence_decision` gates whether the supervisor may start any downstream + work at all +- `action_decision=proceed_to_plan` means the supervisor may open a planning + lane such as `work-plan`, but still may not start repo mutation +- `action_decision=request_review` means the supervisor should post + `review_comment` to the chosen `review_target` and stop there until a later + approval or rerun authorizes mutation +- `review_target=outbox_entry` only makes sense when a current + outbox entry already exists. If no draft change, message surface, or + other outbox entry exists yet, the supervisor should fall back to the + source thread and say that clearly in the posted comment +- `action_decision=proceed_to_plan` should usually still result in a public + supervisor comment so the hold/plan decision is visible outside the raw + receipt stream +- `recommended_lane=issue-to-pr` alone does **not** authorize a build lane + +Always emit `change_set` alongside `intake_report`. + +Also emit `signal` when a source event is admitted. `signal` must follow +`runx.signal.v1` and carry the source reference, authenticity or trust level, +dedupe fingerprint, evidence references, and source-thread preview. This packet +is the portable world-before-action state that `work-plan`, `issue-to-pr`, +hosted queues, and source-thread projections preserve. + +Close the Runx turn after intake is complete with terminal `closure` control +metadata: a disposition, stable reason code, and concise summary. Closure is +receipt control state, not part of the issue-intake artifact, and must not +pretend the recommended downstream lane has already executed. + +When an adapter has provider context beyond the visible thread text, attach it +to `signal.evidence_refs` or a referenced artifact. Source adapters own +provider-specific fetching and redaction before calling this skill; this skill +only reasons over the supplied, reviewer-safe signal and artifacts. + +Hydration is a gate, not a best-effort decoration. If supplied signal or +artifact metadata says provider context is still needed, do not select +`action_decision=proceed_to_build`. Use `manual-review` or `request_review` and +explain the missing adapter context in `operator_notes`. If provider context is +unavailable, use the remaining signal only when it is still concrete enough for +a bounded reply, plan, or PR; otherwise stop for human review. + +The `change_set` is the parent artifact for any later planning or worker +fanout. It is what keeps multiple repo-scoped lanes aligned to one shared +objective. + +`change_set` must contain: + +- `change_set_id` +- `thread_locator` +- `summary` +- `category` +- `severity` +- `recommended_lane` +- `commence_decision` +- `action_decision` +- `target_surfaces`: array of objects with: + - `surface`: repo, product surface, or bounded target name + - `kind`: one of `repo`, `package`, `docs`, `support`, or `other` + - `mutating`: boolean + - `rationale`: why this surface is implicated +- `shared_invariants`: array of constraints that all downstream lanes must + preserve +- `success_criteria`: array of concrete outcomes that define success for the + whole change +- `outbox_entry` (optional): current outbox entry for status + updates, replies, or draft-change refreshes when the caller already knows it + +When `recommended_lane=issue-to-pr`, also include `thread_change_request` with: + +- `task_id` +- `thread_title` +- `thread_body` +- `thread_locator` +- `thread` (optional) +- `outbox_entry` (optional) +- `size`: one of `micro`, `small`, `medium`, or `large` +- `risk`: one of `low`, `medium`, or `high` + +When `recommended_lane=work-plan`, also include +`workspace_change_plan_request` with: + +- `change_set_id` +- `objective` +- `project_context` +- `thread_locator` +- `thread` (optional) +- `target_surfaces` +- `shared_invariants` +- `success_criteria` + +Do not emit both `thread_change_request` and `workspace_change_plan_request` for +the same report. + +Prefer conservative routing: + +- if the report is bounded and well-understood, use `commence_decision=approve` + and `action_decision=proceed_to_build` +- if the next step should be planning instead of mutation, use + `commence_decision=approve` and `action_decision=proceed_to_plan` +- if the likely next lane is clear but mutation or planning should wait for + maintainer confirmation, use `commence_decision=approve` and + `action_decision=request_review` +- if the report is ambiguous, under-specified, or risky, use + `commence_decision=hold` or `needs_human` + +## Inputs + +- `thread_title`: canonical thread title +- `thread_body`: canonical thread body or request text +- `thread_locator` (optional): canonical locator for the bounded thread, + such as an issue, chat thread, ticket, or local agent session +- `thread` (optional): provider-backed thread for the current + thread +- `outbox_entry` (optional): current outbox entry for replies, draft changes, + or refreshes +- `signal` (optional): provider-neutral `runx.signal.v1` observation gathered + by the source adapter before decision +- `product_context` (optional): product-specific constraints or routing hints +- `operator_context` (optional): maintainer or support posture guidance +- `source_event` (optional): admitted Slack, Sentry, GitHub, file, API, or + other provider event. Consuming repos decide source filters before calling + this skill. +- `source_policy` (optional): source admission and routing policy. Do not + hardcode channel names, Sentry projects, or owners in this skill. +- `operational_policy` (optional): `runx.operational_policy.v1` packet used by + downstream repo-changing lanes for source, target, runner, and source-thread + admission. diff --git a/docs/sourcey-catalog/pages/issue-to-pr.md b/docs/sourcey-catalog/pages/issue-to-pr.md new file mode 100644 index 000000000..fd904d2af --- /dev/null +++ b/docs/sourcey-catalog/pages/issue-to-pr.md @@ -0,0 +1,201 @@ +# issue-to-pr + +- Group: GitHub and delivery +- Source: [skills/issue-to-pr/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/issue-to-pr/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/issue-to-pr/SKILL.md` + +# Issue to PR + +Drive one bounded thread-driven change through the scafld 2.4-compatible +lifecycle and package the result as a provider-agnostic draft pull-request +packet. + +The graph separates cognition from mutation. Agent phases author the scafld +markdown spec and the bounded repo change bundle. Deterministic `fs.write` and +`fs.write_bundle` phases are the only places files are written to disk. scafld +owns the workflow kernel: `plan`, `validate`, `approve`, `build_to_review`, +`status`, `review`, `complete`, and `handoff`. runx owns the explicit authoring +boundaries, deterministic writes, receipts, and final outbox packaging. + +Branch creation and provider PR mutation are outside scafld. The caller or +adapter prepares the branch, then passes the intended branch into this lane. +The lane records that branch in the draft PR packet, and the GitHub adapter +fails closed if the workspace checkout does not match it. The final +`issue-to-pr-push-outbox` step is the only provider push boundary. + +## Lifecycle + +The graph runs: + +`scafld plan` -> author markdown spec -> write spec -> read spec -> validate -> +approve -> read approved spec -> read declared files -> author fix bundle -> +write fix bundle -> build to review -> status -> read current branch -> review +-> complete -> final status -> handoff -> package draft PR outbox -> adapter +push. + +There are no translation projection steps. `scafld handoff` is the human handoff +surface, `build_to_review` drives bounded native `scafld build` advances until +the task is review-ready, and `scafld review` is the native review boundary. + +## Thread Story + +The lane should leave one coherent source-thread story, not a stream of every +internal event. The durable milestones are: + +- source signal and the bounded request +- accountable decision that a PR is justified +- scafld spec approval and declared scope +- build and validation result +- adversarial review result +- draft PR publication +- human merge gate +- final provider outcome when observed + +Comments and PR bodies should summarize those gates with enough evidence for a +reviewer to act. They must not publish raw local paths, secrets, full command +dumps, or duplicate retry comments. User-facing labels should use plain terms +such as spec authoring, fix authoring, review, and human merge gate. + + +## Spec Authoring Contract + +The `issue-to-pr-author-spec` boundary must emit a full scafld +2.4-compatible markdown document, not YAML and not a reduced project brief. + +The document must preserve front matter with: + +- `spec_version: '2.0'` +- `task_id` +- `created`: ISO-8601 timestamp +- `updated`: ISO-8601 timestamp +- `title`: non-empty task title, normally `thread_title` +- `status: draft` +- `harden_status: not_run` +- `size`: one of `small`, `medium`, or `large` +- `risk_level` + +The body must include the standard scafld 2 sections: Current State, Summary, +Context, Objectives, Scope, Dependencies, Assumptions, Touchpoints, Risks, +Acceptance, at least one Phase section, Rollback, Review, Self Eval, +Deviations, Metadata, Origin, Harden Rounds, and Planning Log. + +The graph normalizes the front matter before writing the spec so current scafld +schema fields such as `title` and size stay deterministic even if the authoring +boundary omits or stales them. + +All changed-file declarations must use concrete repo-relative paths in +backticks under Context / Files impacted and Phase / Changes. Do not declare +scafld-managed control-plane artifacts under `.scafld/specs`, +`.scafld/reviews`, `.scafld/runs`, or old `.ai` governance paths as repo-change +scope. + +Documentation and process requests still need a concrete repo file. Prefer +existing docs surfaces supplied by `repo_snapshot.existing_files` or +`repo_context`, and declare at least one non-governance repo file for an +approved `issue-to-pr` lane. Do not leave the repo-change scope empty after the +decision layer has approved a PR. + +Validation commands must run against the current workspace state after the fix +bundle is written. Do not depend on git history ranges such as `HEAD~1` or +merge-base comparisons. Validation commands, when present, must be direct +repo-local checks such as test, lint, build, or file-content commands. Never use +runx runtime internals or `graph/scafld/run.mjs` as a validation command; +scafld is already the lifecycle runner around the task. + +For any code change, the approved spec must declare at least one targeted +test/spec file in the changed-file scope and include at least one executable +validation command that exercises that target. This applies even when the source +thread does not explicitly request coverage; code PRs are not publishable from +this lane without targeted test/spec scope or grounded scafld validation +evidence. If the source thread asks for tests, specs, regression coverage, +focused coverage, or request/service coverage, the targeted coverage requirement +cannot be softened to a generic smoke check. If no existing test/spec path is +declared but the repository layout makes a conventional path inferable, declare +that new test/spec file. If no grounded test/spec path or command can be +inferred from the repo snapshot, stop with a missing-evidence reason instead of +publishing a code-only PR. + +Preserve source-thread context in the spec's Summary, Origin, and Planning Log +so later PR packaging can explain why the lane ran and what evidence justified +the mutation. + +## Fix Authoring Contract + +The `issue-to-pr-apply-fix` boundary must emit a bounded `fix_bundle` with +`files: [{ path, contents }]` for every repo file needed to satisfy the approved +spec. For documentation or process changes, the approved spec, source thread, +repo snapshot, repo context, and declared file contents are sufficient when they +identify a narrow edit. + +When `repo_snapshot.recommended_files` contains concrete repo-relative files, +treat those files as actionable target evidence even if the generated spec is +worded conservatively. Read the recommended file and the nearest relevant test +or spec before blocking. If the source thread includes a runtime exception, +backtrace, failing command, or named behavior and the recommended file exists, +prefer the smallest conventional fix plus targeted regression coverage over an +empty bundle. + +For any production code change, `fix_bundle.files` must include the smallest +production fix and a targeted test/spec file, even when the source request does +not explicitly ask for coverage. Do not publish a code-only fix bundle from this +lane. If the approved spec, source thread, or acceptance criteria asks for +tests, specs, regression coverage, focused coverage, or request/service +coverage, the targeted test/spec file must directly cover that requested +behavior. If no test file exists, create the narrow conventional test file when +the repository structure makes that path inferable; otherwise block with the +missing path and evidence reason. + +If a declared file has `exists: false` and the approved spec intentionally +creates it, write the new file when the desired contents are inferable from the +spec and thread. Do not block solely because the file has no prior contents. + +Return `fix_bundle.status: blocked` with `files: []` only when no concrete +repo-relative target is declared, a required existing file cannot be read, or +the requested behavior cannot be inferred after inspecting the supplied target +files. The blocked reason must name the missing evidence and path because an +empty file bundle is a terminal policy denial before `write-fix`. + +## Inputs + +- `task_id`: scafld task id. +- `thread_title`: canonical title and default spec title. +- `thread_body`: full thread body or request text when available. +- `thread_locator`: canonical locator for the bounded thread. +- `thread`: portable thread for the current signal surface. +- `outbox_entry`: existing pull-request outbox entry when refreshing a draft. +- `harness`: optional `runx.harness.v1` packet for the governed run boundary. +- `signal`: optional `runx.signal.v1` packet. Preserve source references, + fingerprint, authenticity, and evidence references as stateful context + instead of reparsing source-thread prose. +- `decision`: optional `runx.decision.v1` packet. Preserve the accountable + selection rationale, selected act, and closure when the caller already made + the lane decision. +- `target_repo`: intended repository slug for PR packaging. +- `operational_policy`: optional `runx.operational_policy.v1` packet used to + admit the source, target repo, runner, and source-thread route before PR + packaging. +- `source_id`: optional operational policy source id. +- `runner_id`: optional operational policy runner id. +- `repo_snapshot`: compact structured snapshot of the target repo. +- `repo_snapshot_path`: optional path to a fuller repo snapshot artifact. +- `repo_context`: textual summary of repo shape and validation hooks. +- `size`: scafld size, default `small`. +- `risk`: scafld risk, default `low`. +- `base`: base ref for PR packaging, default `main`. +- `fixture`: workspace root containing `.scafld`. +- `scafld_bin`: explicit scafld executable path. +- `provider`, `provider_command`, `provider_binary`, `model`: optional native + scafld review provider overrides. + +## Structured Output + +On success, the lane emits: + +- `draft_pull_request`: provider-agnostic PR draft state derived from scafld + handoff, build, review, completion, status, and current git branch. +- `outbox_entry`: a `pull_request` outbox entry suitable for adapter push. +- `push`: adapter push result plus refreshed `thread` when the adapter supports + push. +- Story metadata suitable for one source-thread reviewer update that summarizes + the lifecycle gates and points at the human merge decision. diff --git a/docs/sourcey-catalog/pages/issue-triage.md b/docs/sourcey-catalog/pages/issue-triage.md new file mode 100644 index 000000000..54cc1bd50 --- /dev/null +++ b/docs/sourcey-catalog/pages/issue-triage.md @@ -0,0 +1,52 @@ +# issue-triage + +- Group: GitHub and delivery +- Source: [skills/issue-triage/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/issue-triage/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/issue-triage/SKILL.md` + +# Issue Triage + +Turn noisy issue streams into bounded, evidence-backed action. + +This skill is for issue selection and response drafting, not for silently +mutating repositories. Use it to identify which threads are worth attention, +understand the maintainer or contributor situation, and draft the next helpful +response or remediation path. + +Separate discovery from response. Discovery finds the thread worth engaging. +Response drafting turns one chosen thread into a concrete answer, escalation, +or change plan. + +Ground selection and response in the actual thread, repository facts, receipts, +and maintainer context; do not infer intent beyond what is visible. Lead with +the decision, answer, or next action in the project's own voice. Return +`needs_more_evidence` or `needs_human` when the thread is ambiguous, hostile, +underspecified, unsafe, or outside the maintainer's declared posture. + +## Output + +Discovery runner: + +- `issue_candidates`: candidate issues or discussions worth attention. +- `selection_rationale`: why one candidate should be handled next. +- `operator_notes`: constraints, caveats, or escalation triggers. + +Response runner: + +- `issue_profile`: concise summary of the chosen thread. +- `response_strategy`: recommended response posture and next action. +- `response_draft`: post-ready draft or maintainer handoff. +- `follow_up_actions`: concrete next steps after the response. + +## Inputs + +- `repository` (optional): repository slug or workspace reference. +- `query` (optional): search or queue objective for discovery. +- `issue_url` (optional): canonical issue URL for response drafting. +- `issue_snapshot` (optional): structured issue data when already fetched. +- `maintainer_context` (optional): project norms, release posture, and + response constraints. +- `operator_context` (optional): operator-supplied context used by higher-level + triage graphs. +- `objective` (optional): what the operator wants from this pass. diff --git a/docs/sourcey-catalog/pages/knowledge-router.md b/docs/sourcey-catalog/pages/knowledge-router.md new file mode 100644 index 000000000..afcb2f94d --- /dev/null +++ b/docs/sourcey-catalog/pages/knowledge-router.md @@ -0,0 +1,33 @@ +# knowledge-router + +- Group: Research and data +- Source: [skills/knowledge-router/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/knowledge-router/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/knowledge-router/SKILL.md` + +# Knowledge Router + +Route one question, source event, or support thread to the right knowledge +sources and follow-up path. + +This skill is for triage and routing, not answering the question directly. It +should tell a consuming graph where to look, who owns the domain, what evidence +is already available, and which next skill should run. + +Each route must name the supplied signal that justified its source match, +owner, escalation, and next-skill recommendation. Keep the result as a concise +dispatch note. Return `needs_more_context` when no route is supportable, and +`manual_review` for legal, billing, security, or destructive requests. + +## Output + +- `route`: selected knowledge or ownership domain and rationale. +- `source_matches`: relevant sources with the matching signal. +- `owner_recommendation`: owner or escalation target. +- `next_skill`: the bounded follow-up capability, if one is justified. + +## Inputs + +- `question` (required): user question, event, or thread summary to route. +- `available_sources` (required): source catalog, docs, systems, or owner map. +- `constraints` (optional): allowed systems, sensitivity, or preferred owner. diff --git a/docs/sourcey-catalog/pages/least-privilege.md b/docs/sourcey-catalog/pages/least-privilege.md new file mode 100644 index 000000000..c456223c6 --- /dev/null +++ b/docs/sourcey-catalog/pages/least-privilege.md @@ -0,0 +1,216 @@ +# least-privilege + +- Group: Safety and review +- Source: [skills/least-privilege/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/least-privilege/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/least-privilege/SKILL.md` + +# Least Privilege Auditor + +Turn granted authority plus observed usage into a bounded attenuation proposal. + +runx keeps a receipt of every scope a run actually exercised. This skill reads +that proof. It compares what a subject (a skill, a grant, or a principal) was +granted against what its receipts show it used, then proposes the narrowest +grant that still covers real usage. The output is a reviewable attenuation +proposal, not an automatic change. + +## What this skill does + +1. Diff granted authority against receipt-backed usage. +2. Classify each granted scope as `keep`, `narrow`, `remove`, or `defer`. +3. Propose the narrowest grant that still covers observed usage. +4. State residual risk after attenuation. +5. Emit a receipt-quality report a reviewer can apply or reject. + +## When to use this skill + +- Periodic least-privilege review of a skill, grant, or principal before + publish, renewal, or maturity promotion. +- After an incident, to identify authority that can be safely removed without + breaking observed behavior. +- Before expanding distribution of a public skill, to prove its grant is + minimal against real receipts. +- When a reviewer asks for a scope-by-scope evidence trail, not just a summary. + +## When not to use this skill + +- To grant new authority. This skill only narrows; widening is a human + decision. +- When no usable receipt evidence exists. Return `needs_more_evidence` rather + than guessing a grant down to nothing. +- For secret material handling or credential exposure. Use the appropriate + secret-leak triage flow instead of scope review. +- When the user asks for automatic permission changes. Produce a proposal and + stop unless a separate approved delivery lane exists. +- When grant semantics are unknown and cannot be normalized. Return + `needs_input` with the exact syntax or policy question. + +## Procedure + +1. Scope the audit target. + - Identify `subject`, grant source, receipt ids or receipt window, and + whether receipts are from the same principal or skill version. + - Gate: if the subject, grant list, or usage source is ambiguous, stop with + `needs_input`. + - Evidence expected: subject id or label, granted scope list, receipt ids or + an explicit statement that no receipts were available. + +2. Normalize granted scopes. + - Parse each scope into verb, resource, path or namespace, conditions, and + wildcard breadth. + - Preserve original scope strings. Do not rewrite policy syntax casually. + - Gate: if a scope cannot be parsed, keep it as `defer` and request the + missing policy semantics instead of treating it as unused. + +3. Build the usage model from receipts. + - Extract actual exercised verbs and resources from receipt steps, tool + calls, policy checks, denied checks, and completion status. + - Count successful use separately from denied or dry-run checks. + - Do not infer scope usage from a successful high-level task alone; cite the + receipt step or policy check that exercised the authority. + +4. Classify every granted scope. + - `keep`: at least one observed successful use requires the granted scope as + written, or a reserved/break-glass policy explicitly requires it. + - `narrow`: all observed uses fit a strictly smaller verb, resource, + namespace, condition, or path. + - `remove`: no observed use, denied check, or documented reserved purpose + supports the scope. + - `defer`: evidence is conflicting, receipt attribution is weak, or policy + semantics are unknown. + +5. Propose attenuation. + - Remove scopes classified as `remove`. + - Downgrade scopes classified as `narrow` only when every observed use fits + the narrower grant. + - Leave `keep` and `defer` scopes unchanged in the proposed grant. + - Gate: never produce a proposal narrower than the evidence supports. A + scope used once is used. + +6. State residual risk and reviewer action. + - Name what the proposed grant can still do. + - Name any broad scope kept despite thin evidence and why. + - Separate `applyable now` from `needs human policy decision`. + +7. Emit receipt expectations. + - A valid receipt for this skill should record input grant count, receipt + sources, classification counts, proposed removals or narrowings, stop + status, and unresolved questions. + +## Edge cases and stop conditions + +- Empty or unattributable usage evidence: return `needs_more_evidence`; do not + remove all scopes by default. +- Missing granted scopes: return `needs_input`; there is no baseline to diff. +- Receipt subject mismatch: return `needs_input` with the mismatched subject or + version. +- Conflicting receipts: classify affected scopes as `defer` and return + `needs_human` if the conflict changes the proposal. +- Wildcard grants: narrow only to observed resource prefixes when receipt + coverage is representative; otherwise keep and flag residual risk. +- Reserved, compliance, or break-glass scopes: keep unless the operator + provides explicit policy authority to remove them. +- Dry-run-only use: do not count as successful exercised authority unless the + grant exists solely for validation. +- Grant already matches usage: return `no_change` with the evidence summary. +- User asks to hide or omit unused authority: refuse that part and report the + complete scope diff. + +## Output schema + +Return a structured report with these fields: + +```yaml +status: attenuation_proposed | no_change | needs_more_evidence | needs_input | needs_human | refused +subject: string +evidence: + receipt_ids: [string] + receipt_window: string | null + grant_source: string | null + limitations: [string] +scope_diff: + - granted_scope: string + normalized: + verb: string | null + resource: string | null + conditions: object | null + observed_use: + count: number + verbs: [string] + resources: [string] + receipt_refs: [string] + classification: keep | narrow | remove | defer + proposal: string | null + rationale: string +attenuated_grant: [string] +removed_scopes: [string] +narrowed_scopes: + - from: string + to: string +kept_scopes: [string] +deferred_scopes: [string] +residual_risk: [string] +reviewer_action: applyable_now | needs_policy_decision | gather_more_receipts | none +receipt_expectations: + classification_counts: object + stop_status: string + unresolved_questions: [string] +``` + +## Worked example + +Input: + +```yaml +subject: skills/report-exporter +granted_scopes: + - drive.files.read:/reports/* + - drive.files.write:/reports/* + - drive.files.delete:/reports/* +usage_summary: + receipt_ids: [rx_101, rx_102] + observed: + - scope: drive.files.read:/reports/* + count: 8 + refs: [rx_101:step_3, rx_102:step_2] + - scope: drive.files.write:/reports/* + count: 2 + refs: [rx_101:step_6, rx_102:step_5] +``` + +Output: + +```yaml +status: attenuation_proposed +subject: skills/report-exporter +removed_scopes: + - drive.files.delete:/reports/* +narrowed_scopes: [] +kept_scopes: + - drive.files.read:/reports/* + - drive.files.write:/reports/* +attenuated_grant: + - drive.files.read:/reports/* + - drive.files.write:/reports/* +residual_risk: + - The skill can still read and write any file under /reports/*. +reviewer_action: applyable_now +``` + +The delete scope is removable because no cited receipt exercised delete +authority. The read and write scopes stay because each was used at least once. + +## Inputs + +- `subject` (optional): skill id, grant id, principal, or other label for what + is being audited. +- `granted_scopes` (required): the current scopes granted to the subject, + preferably in canonical policy syntax. +- `usage_summary` (required): receipt-derived usage. Include receipt ids, step + refs, observed verbs, resources, success or denial status, and the time + window when available. +- `objective` (optional): operator intent that focuses the review, such as + "prepare for public publish" or "post-incident attenuation". +- `policy_notes` (optional): reserved scopes, compliance constraints, or + human-approved exceptions that affect removal decisions. diff --git a/docs/sourcey-catalog/pages/operator-inbox.md b/docs/sourcey-catalog/pages/operator-inbox.md new file mode 100644 index 000000000..c461200a3 --- /dev/null +++ b/docs/sourcey-catalog/pages/operator-inbox.md @@ -0,0 +1,114 @@ +# operator-inbox + +- Group: Operate +- Source: [skills/operator-inbox/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/operator-inbox/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/operator-inbox/SKILL.md` + +# Operator Inbox + +Maintain a durable action queue without turning a connector into the owner of +operator state. + +The caller fetches bounded, grant-authorized provider pages and passes their +normalized observations to this skill. The skill owns work-item identity, +status, dispositions, replay suppression, reopen rules, and scan coverage. Every +read and write is composed through `data-store`; the skill does not call Slack, +SQLite, Postgres, or another provider directly. + +## What this skill does + +Use `local://runx/operator-inbox/default` unless the operator selects another +logical source. Unbound local refs resolve to SQLite under +`.runx/data/local-sources/`. A hosted database is opt-in through the same +`data_source_ref` binding. Runx Connect may still own OAuth, grants, and provider +execution; that does not move this queue into the hosted control plane. + +Observations and resumable checkpoints live in `operator_inbox_scans`, partitioned +by query digest. Action snapshots live in `operator_inbox_actions`, with one +stream per stable thread digest. Queue reads use bounded `list_stream_heads` +pages; no command folds or transports the complete queue. + +## When to use this skill + +- Build or revisit a local action queue from bounded connector observations. +- Preserve an explicit `resolved`, `dismissed`, `waiting`, or `followed_up` + decision across repeated provider scans. +- Reopen a completed item when a newer external occurrence arrives. +- Inspect bounded action or scan state without handing queue ownership to the + provider or hosted control plane. + +## When not to use this skill + +- Do not fetch provider data, reply, send, or mutate a remote account here. +- Do not infer that an item is complete from message text or provider state. +- Do not use it as an unbounded archive of raw messages or credentials. +- Do not place a private operator's routing policy or identity in this public + package; pass normalized observations and explicit dispositions as inputs. + +## Status rules + +Items use `open`, `waiting`, `followed_up`, `resolved`, or `dismissed`. + +- Provider observations never infer completion. +- A human disposition records actor, reason, time, the latest external + occurrence it covers, and optional HTTPS evidence. +- Replaying old search history preserves the human status. +- An external message newer than the covered occurrence reopens the item to + `open`, including unseen work that arrived before the disposition was saved. +- Scan coverage is explicit: `running`, `complete`, `truncated`, or `failed`. +- Direct mentions are actionable structural evidence. Author and keyword scans + remain observation-only unless the operator explicitly marks the query + actionable. The skill does not contain provider-specific keyword heuristics. + +The provider-neutral thread locator is the item key. Stored previews are bounded; +credentials, tokens, and full provider response envelopes are forbidden. + +## Procedure + +1. Read the latest checkpoint for the bounded query digest. +2. Resume its provider cursor when the prior scan was interrupted or truncated. +3. Fetch one bounded provider page through the caller's authorized connector. +4. Record actionable messages against their per-thread streams and append the + scan page with its next cursor. +5. On a version conflict, reload only the affected scan or action stream and + retry the idempotent transition. +6. List queue state through bounded action-head pages and use + `record_disposition` only for an explicit operator correction. + +The loop is outside the kernel. Each page or disposition remains one governed, +receipt-backed Runx turn. + +## Edge cases and stop conditions + +- `needs_input`: missing query identity, observation, disposition, + actor, reason, or scan coverage. +- `conflict`: the projection version is stale; reload before retrying. +- `provider_unavailable`: the caller cannot prove provider read coverage. +- `too_broad`: a page exceeds the bounded message count or contains unnormalized + provider data. +- `refused`: a caller asks this skill to send, reply, broaden a grant, store a + token, or silently claim complete coverage. + +## Output schema + +Write runners emit `runx.effect.transition.v1`, containing the effect family, +operation, expected projection version, idempotency key, and one normalized +event. Read and list runners return the corresponding bounded `data-store` +event result; they never synthesize provider coverage or completion. + +## Worked example + +Given a normalized direct mention from a teammate in one provider thread, +`record_action_observation` derives the stable action id from the provider-neutral +thread locator and appends an `open` action snapshot. If the operator later +records `resolved` with a reason, replaying that mention preserves `resolved`; +a newer external reply in the same thread appends a reopened `open` snapshot. + +## Inputs + +All runners require `data_source_ref`. Write runners also take the target id, +`expected_version`, and `observed_at`, plus exactly the normalized payload for +their operation: `scan` and `messages`, `message` and `triage`, `disposition`, +or an imported `action`. Reads take `action_id` or `query_digest`; list runners +take bounded `limit` and optional cursor or filter fields. diff --git a/docs/sourcey-catalog/pages/ops-desk.md b/docs/sourcey-catalog/pages/ops-desk.md new file mode 100644 index 000000000..443307a01 --- /dev/null +++ b/docs/sourcey-catalog/pages/ops-desk.md @@ -0,0 +1,325 @@ +# ops-desk + +- Group: Operate +- Source: [skills/ops-desk/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/ops-desk/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/ops-desk/SKILL.md` + +# Ops Desk + +Operate a project, workspace, or account from an agent-controlled desk. + +This skill is the generic operations desk layer. It turns a state snapshot, an +operator objective, and receipt-backed evidence into one safe ops desk packet: +what is happening, what needs attention, what can be checked read-only, what +requires approval, which governed lane should execute, and how success will be +verified. + +It is not the authority and it is not a second CLI. It does not replace +`release`, `send-as`, `ledger`, `refund`, `spend`, `messageboard`, +provider-specific adapter skills, hosted API routes, repository workflows, or +deploy commands. It routes to the existing interface with the smallest +sufficient context and stops before any consequential act that lacks the right +gate. + +## What this skill does + +`ops-desk` produces an ops desk packet for a manager dashboard, agent +session, or self-operation run. It reads projected state, classifies findings, +ranks the next action, selects the governed lane, names blockers, writes the +approval prompt when a human decision is required, and states the +receipt/effect/readback that will prove success. + +It is useful before an action and after an action: + +- before action, it turns state into proposals and approval requests; +- after action, it checks whether the expected receipt and projection appeared. + +The model may diagnose and write the operator rationale. The mutation itself +must be a deterministic handoff to an existing skill runner, CLI command, hosted +API route, workflow, or provider tool. + +When the desk should start from durable state, use `operate_from_projection`. +That runner reads a projection through `data-store` first, then passes the +projection as the dashboard snapshot. The storage provider is still selected by +the logical `data_source_ref`; ops desk does not know whether state came from +SQLite, Postgres, D1, Redis, or a product API. + +When a standing case must be advanced one move at a time toward a mandate, use +`advance`. It takes the mandate, the current `case_state`, and a fixed +`candidate_roster`, and returns a single typed `dispatch_decision`: dispatch one +roster member, escalate, or done. It applies the same ranking and the same gates as +`operate`, but it is hard-constrained to the roster and emits one move instead of a +multi-proposal plan. The caller (an agency loop) holds the case and the goal; ops +desk supplies the judgment. The chosen member is named as data; ops desk never runs +it. + +## When to use this skill + +- An operator asks an agent to manage a project, workspace, product, account, + or other bounded operating surface. +- A dashboard needs an agent-readable plan from the current projected state. +- A runbook needs to decide between read-only checks, proposals, approval-gated + actions, and post-action verification. +- A product-specific operator skill needs a generic cockpit spine instead of + inventing its own action model. +- A standing case (an agency) needs the single next governed move chosen from a + fixed roster, one turn at a time. +- Runx needs to dogfood its own release, registry, hosted, receipt, or provider + operations through the same governed lanes it exposes to users. + +## When not to use this skill + +- To execute a live mutation directly. Route to the named governed lane. +- To duplicate a CLI command, release script, GitHub workflow, hosted endpoint, + registry client, or provider SDK. +- To bypass a human gate because the agent or UI believes the action is obvious. +- To replace a domain skill such as `send-as`, `messageboard`, `release`, + `ledger`, `refund`, `spend`, `least-privilege`, or a provider + adapter. +- To operate from stale, missing, or unverifiable state while claiming readiness. +- To put secrets, private keys, raw customer lists, or provider dumps into the + ops desk packet. + +## Operating Model + +Use one loop: + +```text +snapshot -> findings -> proposals -> approval -> governed lane -> receipt -> projection +``` + +The manager dashboard and the agent must read the same state and emit the same +action families. A button click and an agent plan are different interfaces over +the same governed lane, not separate backdoors. + +## Delegation Model + +Ops desk packets name existing execution surfaces; they do not implement them. + +- `release` owns release preparation, approval, publish handoff, and + post-release verification. +- `ledger`, `audit-receipt`, and `run-history` own proof questions. +- `send-as` owns authority for live communications; provider adapter skills own + provider-specific execution details. +- `spend`, `charge`, `refund`, and branded payment skills own money movement. +- Project skills own product vocabulary and product-specific actions. +- CLI commands, hosted API routes, and GitHub workflows remain deterministic + execution interfaces. The operator skill may cite them as handoff targets but + must not clone their behavior in prose. + +If no existing lane can perform the action cleanly, return `needs_input` or a +product gap. Do not invent a private workaround. + +## Procedure + +1. Scope the objective. + - Identify the workspace, project, account, surface, time window, and whether the ask is + read-only, proposal-only, or execution-prep. + - Read `project_profile` or `operator_policy` as context, not authority. + - If the operating scope or objective is ambiguous, return `needs_input`. + +2. Classify state from evidence. + - Use `dashboard_snapshot`, `receipt_summary`, `effect_summary`, and + `provider_status` when present. + - Treat missing evidence as missing. Do not infer success from UI state alone. + - Separate health, money, communications, provider mutations, access, + deployment, and incident signals. + - For review, catalog, publication, bounty, or marketplace work, classify + whether the artifact is real, useful, complete, and valuable. A reachable + artifact with no credible user, maintainer, operator, public proof, or + marketing value is not ready. + - If using `operate_from_projection`, treat the read projection as the + dashboard snapshot. An empty projection is not an error, but it should + usually produce `needs_input` rather than fake readiness. + +3. Route to governed lanes. + - Release questions route to `release` plus the project release profile and + existing release workflow/commands. + - Audit questions route to `ledger`, `audit-receipt`, `run-history`, + or `least-privilege`. + - Live communication routes through `send-as` and then a provider adapter. + - Payment collection, payout, refund, chargeback, or target changes route to + the matching payment lane. + - Board, thread, and provider actions route to `messageboard`, a provider + adapter, `issue-intake`, `issue-to-pr`, or the product's own skill. + - Deploy and config changes route to the product-owned deploy lane. + +4. Decide gates. + - Read-only checks: no human approval. + - Drafts, dry-runs, previews, and reports: no live-action approval unless they + expose private data or broaden authority. + - Live sends, payouts, refunds, customer-visible posts, provider mutations, + target changes, credential changes, deploys, destructive actions, and broad + audience decisions: explicit approval required. + - A review verdict, recommendation, or green dry-run is not payment approval. + Money movement needs a separate approval prompt naming the amount, recipient, + rail, target class, and verification receipt expected after settlement. + - Missing approval means `awaiting_approval`, not "ready". + +5. Produce the ops desk packet. + - Lead with the few issues an operator should act on now. + - Name the exact lane for each proposed action. + - Include the existing execution interface as a handoff, not as a duplicated + implementation. + - Include approval copy only when the operator could approve it safely. + - Include verification steps that will prove the action happened. + +6. Stop cleanly. + - Return `needs_input` for missing scope, objective, identity, authority, + evidence, approval, or target. + - Return `refused` for requests to bypass gates, hide material facts, leak + secrets, spoof receipts, mark unsettled money as settled, or send without a + principal/audience/content digest. + +## Edge cases and stop conditions + +- **No project/workspace/account or objective:** return `needs_input`; there is + no safe operating frame. +- **No projection or receipt evidence:** return `needs_input` or `unknown` + status; do not convert silence into `ok`. +- **Requested action has unknown consequence:** stop at `needs_input` with the + missing lane/consequence classification. +- **Money, public send, deploy, credential, target, destructive, or provider + mutation without approval:** return `awaiting_approval`. +- **Approval text is too broad to approve safely:** return `needs_input` with the + exact missing amount, audience, target, network, provider, or effect. +- **User asks to skip a gate, hide a blocker, forge a receipt, or mark state + settled without proof:** return `refused`. + +## Reference Loading + +Load only the reference needed for the objective: + +- Payments, payouts, refunds, payment rail adapters, reconciliation: + `references/payments.md` +- Email, campaigns, notifications, customer/public communication: + `references/communications.md` +- Receipt verification, ledger, trust roots, after-action proof: + `references/receipts.md` +- Provider health, deploys, webhooks, credentials, outages: + `references/providers.md` +- Manager dashboard state, projections, and action catalog design: + `references/dashboard.md` +- Delegation, project profiles, CLI/workflow handoff, and dogfooding rules: + `references/delegation.md` + +## Output schema + +Return one `ops_desk_packet`: + +```yaml +ops_desk_packet: + decision: ready | awaiting_approval | needs_input | no_action | refused + scope_ref: string + objective: string + mode: read_only | proposal | execution_prep | post_action_review + dashboard: + health: ok | degraded | blocked | unknown + money: ok | needs_attention | blocked | unknown + communications: ok | needs_attention | blocked | unknown + providers: ok | needs_attention | blocked | unknown + receipts: ok | needs_attention | blocked | unknown + findings: + - severity: info | warning | critical + area: health | money | communications | providers | receipts | access | deploy + summary: string + evidence_refs: [string] + proposals: + - action_id: string + lane: string + reason: string + inputs_summary: object + consequence: read_only | draft | live_mutation | money_movement | public_send | deploy + approval_required: boolean + approval_prompt: string | null + blockers: [string] + verification: + expected_receipt: string + expected_effect: string | null + readback: string + execution: + interface: skill | cli | hosted_api | workflow | provider_tool | manual + lane_ref: string + profile_ref: string | null + command_ref: string | null + workflow_ref: string | null + approval_gate: string | null + verifier_ref: string | null + ordered_next_steps: + - step: string + lane: string + requires_confirmation: boolean + refused_reasons: [string] + needs_input: [string] + success_checkpoint: + milestone: string + description: string +``` + +The `advance` runner returns one `dispatch_decision`: + +```yaml +dispatch_decision: + decision: dispatch | escalate | done + reason: string + dispatch: # present when decision == dispatch + member: string # a role from candidate_roster + skill: string # that role's roster skill, echoed + task: string # what the member should do + needed_scope: [string] # subset of the member's scope ceiling + consequence: read_only | draft | live_mutation | money_movement | public_send | deploy + verification: + expected_receipt: string + readback: string + escalation: # present when decision == escalate + to: string # a roster role or "human" + trigger: string + ask: string + approval_prompt: string | null + resolution: # present when decision == done + reason: string +``` + +## Decision rules + +- Prefer one clear next action over a dashboard dump. +- Never bury a required approval in prose; put it in `approval_prompt`. +- Never expose tokens, API keys, raw customer lists, private wallet keys, or + provider response dumps. +- Never claim a state is settled, sent, deployed, paid, or refunded without a + receipt/effect/readback reference. +- Never route a public artifact, skill, bounty result, or docs deployment as + ready when it lacks a credible real-world audience or durable public evidence. +- Never widen authority because a dashboard widget would be convenient. +- Never duplicate an existing CLI command, workflow, hosted endpoint, or domain + skill in operator prose. Route to it. +- Keep product-specific policy in product context. Keep this skill generic. + +## Inputs + +- `objective` (required): operator request, e.g. "check payments and unblock + funding", "prepare a campaign send", or "review stuck receipts". +- `scope_ref` (required): the project, workspace, account, product, or bounded + surface being operated. +- `dashboard_snapshot` (optional): JSON summary of current projected state. +- `receipt_summary` (optional): JSON or prose receipt/effect summary. +- `provider_status` (optional): JSON or prose provider health/account state. +- `approval_context` (optional): existing operator approvals, denials, or + policy gates. +- `operator_policy` (optional): project-specific constraints and lane names. +- `project_profile` (optional): project topology, existing interfaces, and + verification expectations. It is context, not authority. +- `requested_action` (optional): preselected action lane or dashboard action id. + +## Worked example + +Input: "Check payment readiness and tell me what to do next" with a dashboard +snapshot showing healthy quote/readback state, three funded items, no unfunded +approved items, and one rail adapter webhook status `needs_review`. + +Output: `decision: ready`, money status `ok`, providers status +`needs_attention`, one warning finding for rail webhook readiness, and one +proposal routing to `provider.webhook_check` with no money movement. It does +not propose marking anything funded, because no unfunded approved item is +present and the latest funding receipt is already verified. diff --git a/docs/sourcey-catalog/pages/policy-author.md b/docs/sourcey-catalog/pages/policy-author.md new file mode 100644 index 000000000..78e59247a --- /dev/null +++ b/docs/sourcey-catalog/pages/policy-author.md @@ -0,0 +1,175 @@ +# policy-author + +- Group: Safety and review +- Source: [skills/policy-author/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/policy-author/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/policy-author/SKILL.md` + +# Policy Author + +Author one governed runx operational policy from intent, and prove it lints. + +Adopting governed runx means writing an operational policy: which repos may be +touched, who owns which surface, which sources are trusted, what confidence is +required before action, and which outcomes need a human. Written by hand that is +a long, error-prone document. This skill turns a plain-English governance brief +into one `runx.operational_policy.v1` proposal, or tightens an existing policy, +and runs a fail-closed lint over it before it ships. It proposes; a human +approves. + +## What this skill does + +1. **Read the intent.** Take the governance brief (and an existing policy when + tightening) and identify the target surfaces, sources, owners, and the risk + posture. +2. **Draft the policy.** Produce a complete `runx.operational_policy.v1`: target + repos, runner binding, allowed actions, trusted sources with confidence + floors, owner routes, and outcome rules. +3. **Lint fail-closed.** Run the policy checks below. Any failing check blocks + the proposal with the exact fix, rather than shipping a permissive policy. +4. **Tighten, never loosen.** When given an existing policy, only propose + changes that narrow authority (auto-merge off, human gate on, confidence up). + Widening is a separate, explicit human decision. + +## Core principles + +- **Fail closed.** Unspecified means denied. A missing owner route, source rule, + or confidence floor is a lint error, not a permissive default. +- **Human gate on mutation.** Any policy that allows repository mutation must set + `require_human_merge_gate: true` and `auto_merge: false`. +- **Named owners.** Every target surface routes to a named owner; no orphan + surfaces. +- **Bounded sources.** Each trusted source declares a minimum confidence; no + source admits work below its floor. +- **Verification before close.** A source issue closes only when the outcome is + verified. + +## When to use this skill + +- Bootstrapping a new runx deployment that needs an operational policy. +- Tightening an existing policy after a near-miss or an audit. +- Onboarding a new target repo, source, or owner into an existing policy. + +## When not to use this skill + +- To widen authority (add auto-merge, drop a human gate, lower confidence). That + is an explicit human decision, not a generated proposal. +- To write skill logic or graphs. This authors the governance envelope, not the + skills it governs. + +## The operational policy model + +The proposal fills `runx.operational_policy.v1`: + +- `target_repos`: the repositories the policy may act on. +- `runner`: the runner binding (id, kind, and required substrate, e.g. GitHub + Actions + scafld). +- `allowed_actions`: the lanes permitted (e.g. `issue-intake`, `issue-to-pr`, + `pr-review`). +- `sources`: trusted inbound sources, each with a `min_confidence` floor. +- `owner_routes`: surface-to-owner routing; every surface has a named owner. +- `outcomes`: `verification_required`, `close_source_issue`, + `require_human_merge_gate`, `auto_merge`. + +## Lint diagnostics + +The fail-closed lint emits these; any error blocks the proposal: + +- `policy.owner.unrouted` (error): a target surface has no owner route. +- `policy.mutation.no_human_gate` (error): mutation allowed without + `require_human_merge_gate: true`. +- `policy.mutation.auto_merge_on` (error): `auto_merge` is true on a mutating + policy. +- `policy.source.no_confidence_floor` (error): a source has no `min_confidence`. +- `policy.source.floor_too_low` (warning): a confidence floor below 0.7. +- `policy.close.before_verify` (error): `close_source_issue` set without + `verification_required`. +- `policy.action.unknown` (error): an allowed action is not a known lane. + +## Procedure + +1. Validate that the brief names the governed work, the target repo or surface, + and the intended owner or escalation route. +2. Extract all repos, sources, actions, owners, confidence floors, and outcome + rules from the brief and any existing policy. +3. If tightening an existing policy, diff proposed changes against the current + grant. Flag any widened action, lower confidence floor, removed owner, or + removed human gate as a separate human decision. +4. Draft the smallest complete `runx.operational_policy.v1` that allows the + stated work and denies everything else. +5. Run the lint diagnostics. Any `error` finding prevents `decision: ready`. +6. Emit the policy, lint result, rationale, blockers, and success checkpoint. + +## Edge cases and stop conditions + +- **No owner route:** return `needs_input`; an ownerless surface is never + governed by default. +- **Mutation without a human gate:** return `reject` or `needs_input`; do not + emit a ready mutating policy without `require_human_merge_gate: true`. +- **Auto-merge requested:** block the proposal unless the user explicitly + performs a separate authority-widening decision outside this skill. +- **Unknown action lane:** return `needs_input` with the unknown action names. +- **Source without confidence floor:** return `needs_input`; implicit trust is + not a policy. +- **Conflicting owner routes:** return `needs_input` and cite the conflicting + surfaces and owners. + +## Output schema (`policy_proposal`) + +```yaml +decision: ready | needs_input | reject +policy: + schema: runx.operational_policy.v1 + target_repos: [string] + runner: + id: string + kind: string + requires: [string] + allowed_actions: [string] + sources: + - provider: string + min_confidence: number + owner_routes: + - surface: string + owner: string + outcomes: + verification_required: boolean + close_source_issue: never | when_verified | always + require_human_merge_gate: boolean + auto_merge: boolean +lint: + status: pass | fail + findings: + - id: string + severity: error | warning + message: string +rationale: string +blockers: [string] +needs_input: [string] +success_checkpoint: + milestone: string + description: string +``` + +A proposal with any `error` finding must have `decision: needs_input` or +`reject`, never `ready`. + +## Worked example + +Brief: "Govern issue intake across our three repos. GitHub issues and Sentry +alerts. Kam owns the platform, Chong owns product. Never auto-merge; a human +approves every merge; close the source issue only once the fix is verified." + +The proposal binds the three repos to a GitHub-Actions + scafld runner, allows +`issue-intake`/`issue-to-pr`/`pr-review`, trusts GitHub at 0.72 and Sentry at +0.82, routes platform to Kam and product to Chong, and sets +`require_human_merge_gate: true`, `auto_merge: false`, +`verification_required: true`, `close_source_issue: when_verified`. The lint +passes, so `decision: ready`. + +## Inputs + +- `governance_brief` (required): the governance intent in prose. +- `existing_policy` (optional): a current `runx.operational_policy.v1` to tighten. +- `target_repos` (optional): explicit repo list when not in the brief. +- `objective` (optional): operator intent that focuses the pass. diff --git a/docs/sourcey-catalog/pages/release.md b/docs/sourcey-catalog/pages/release.md new file mode 100644 index 000000000..78675c2e1 --- /dev/null +++ b/docs/sourcey-catalog/pages/release.md @@ -0,0 +1,133 @@ +# release + +- Group: GitHub and delivery +- Source: [skills/release/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/release/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/release/SKILL.md` + +# Release + +Turn a proposed release into an audited publication. The skill owns the release +decision process: evidence gathering, changelog preparation, approval, publish +handoff, verification, and announcement. It does not own a project's custom +release implementation. Project-specific topology lives in a release profile +that names existing commands, workflows, registries, deploy targets, and +verification readbacks. + +Every version and changelog claim must trace to commits, tags, checks, package +metadata, or explicit operator context. Write release material for package +consumers: say why this version matters and what they should do next, without +generic launch language or positive wording that hides a blocker. Stop in +`prepare` or at approval when checks fail, versioning is unclear, evidence is +thin, or the announcement would overstate what shipped. + +Two runners: + +- **`prepare`** (read-only) — survey the commit range since the last tag, + classify commits, stage a changelog, run the declared checks, and emit a + `release_brief` describing what would ship, what is blocked, and what + remains unresolved. Safe to run unattended and in CI. +- **`release`** (default, graph) — wires `prepare` → approval gate → + `publish` → `verify`. `publish` is not exposed as a standalone runner; it is + only reachable inside the graph after the approval transition clears. + +Invoke `runx skill release prepare` for a CI dry-run. Invoke +`runx skill release` to run the governed end-to-end flow. + +## Phases + +### prepare + +The read-only phase. Reads git history, classifies each commit since the +previous semver tag (`feat`, `fix`, `refactor`, `chore`, `breaking`), +stages a changelog, reads the project release profile when supplied, and runs +the declared release checks. Emits a `release_brief` with the findings. + +The brief is the only artifact that flows forward. If it is not +`publishable`, the graph stops at the approval gate with the reasons +attached. + +### approve-publish + +A typed approval step. The gate id is `release.publish.approval`. The +brief is provided as context so the approver sees what would ship before +deciding. + +The policy transition only advances to `publish-release` when +`approve-publish.approval_decision.data.approved` is `true`. No back +channel, no implicit approval on timeout. + +### publish-release + +The destructive phase. Takes the approved `release_brief` from graph context and +hands off to the project-declared release interface: an existing CLI command, +GitHub Actions workflow, hosted API route, provider tool, or manual release +gate. Every side effect is recorded in `publish_report.side_effects[]` with its +locator and evidence; the graph receipt seals the trail. + +Refuses to act if the brief is missing, unpublishable, or not carried +through the approval gate. Refuses to act if the project profile asks the agent +to reimplement release logic instead of naming an existing execution surface. + +### verify-release + +The proof phase. Reads the `publish_report`, release brief, and project profile, +then verifies external state: registry versions, release assets, deploy health, +site/changelog readbacks, package-manager manifests, or any other project-owned +release acceptance criteria. Emits a `release_report` for operator review and +public audit. + + +## Inputs + +| Name | Required | Description | +|---|---|---| +| `project_root` | yes | Absolute path to the project being released. | +| `channel` | yes | Publishing target (`npm`, `pypi`, `github-release`). | +| `profile_ref` | no | Path or registry ref for a project-owned release profile. The profile describes existing commands/workflows and verification expectations; it is not authority. | +| `last_tag` | no | Previous release anchor. Defaults to the latest semver tag reachable from the current branch. | +| `operator_context` | no | Cadence, campaign, or posture guidance for this release. | + +## Outputs + +- `prepare` emits `release_brief_packet` carrying `release_brief`: + changelog, check results, proposed version, unresolved flags, + publishable verdict. +- The graph emits a graph receipt that links the prepare brief, the + approval decision, publish report, and verification report into one auditable + trail. +- `publish-release` (inside the graph) emits `publish_report`: registry + URL, release tag, announcement packet, and a `side_effects[]` list with + a locator and evidence per write action. +- `verify-release` emits `release_report`: expected lanes, observed readbacks, + missing artifacts, conditional skips, and final release verdict. + +## Trust boundary + +`prepare` is safe to run unattended and in CI. The destructive work is only +reachable through the graph, and the graph refuses to transition to +`publish-release` without an approved decision from `release.publish.approval`. +The graph enforces the gate; the skill does not bypass it. + +Project profiles are context, not authority. A profile may say which workflow, +command, registry, or URL should be used. It cannot grant credentials, skip +approval, or authorize a destructive release by itself. + +## Scopes + +- `runx:release:read` — required by the prepare phase. +- `runx:release:publish` — required by the publish phase; the graph grant + must include this only when the approval transition has cleared. +- `runx:release:verify` — required by the verification phase. + +## Tasks + +- `release-prepare` — the read-only phase task. Provides the + `release_brief` output shape. +- `release-publish` — the destructive phase task. Only reachable inside + the graph; requires the approved brief in context. +- `release-verify` — the proof phase task. Reads external state and reports + whether the release actually landed. + +These are managed-agent task contracts carried by the skill package and its +`X.yaml` graph definition. They are not a separate registered task catalog. diff --git a/docs/sourcey-catalog/pages/research.md b/docs/sourcey-catalog/pages/research.md new file mode 100644 index 000000000..599ec312f --- /dev/null +++ b/docs/sourcey-catalog/pages/research.md @@ -0,0 +1,51 @@ +# research + +- Group: Research and data +- Source: [skills/research/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/research/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/research/SKILL.md` + +# Research + +Research one bounded question and turn it into a decision-ready packet. + +This skill is for applied research, not open-ended browsing. It should answer +one practical question with evidence, tradeoffs, and explicit uncertainty: +which issue is worth tackling, what the ecosystem is doing, whether a proposal +is grounded, or what claims a public post can safely make. + +Keep the scope tight. Summaries without evidence are not enough, but an +undirected literature review is also wrong. Prefer a small number of verified +claims that change the operator's decision. + +## Operating rules + +- State the objective in operational terms. +- Distinguish verified evidence from inference. +- Give every important claim a source and confidence. +- Surface missing evidence instead of inventing it. +- Bound the result to a concrete deliverable: brief, issue recommendation, + content outline, or publish/no-publish decision. +- State what the finding changes: what to write, build, avoid, defer, or review. +- Return `needs_more_evidence` rather than forcing a speculative conclusion, + and `not_worth_publishing` when a true finding is irrelevant to the audience. + + +## Output + +- `research_brief`: object with `objective`, `scope`, `summary`, and + `open_questions`. +- `evidence_log`: array of evidence entries with `claim`, `source`, + `confidence`, and `relevance`. +- `decision_support`: array of options or recommendations with rationale. +- `risks`: array of research or execution risks. + +## Inputs + +- `objective` (required): the question to answer. +- `domain` (optional): ecosystem, product area, or audience context. +- `deliverable` (optional): intended artifact, for example `daily brief`, + `triage recommendation`, or `publish packet`. +- `operator_context` (optional): local constraints or strategic context. +- `target_entities` (optional): array or object naming repos, products, + competitors, communities, or issues that bound the research. diff --git a/docs/sourcey-catalog/pages/review-receipt.md b/docs/sourcey-catalog/pages/review-receipt.md new file mode 100644 index 000000000..f5f38807a --- /dev/null +++ b/docs/sourcey-catalog/pages/review-receipt.md @@ -0,0 +1,83 @@ +# review-receipt + +- Group: Safety and review +- Source: [skills/review-receipt/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/review-receipt/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/review-receipt/SKILL.md` + +# Receipt Review + +Diagnose what went wrong in a skill or graph execution and propose the +smallest change that fixes it. + +Read the receipt or failure summary. Identify what was attempted, what +succeeded, and where it broke. The receipt contains step statuses +(`sealed`, `failure`, `policy_denied`, `needs_agent`), +exit codes, stderr, scope admission decisions, +and timing. + +Distinguish root cause from symptoms. A graph may report failure at step 4, +but the root cause may be bad output from step 2 that propagated through +context passing. Trace data flow backward through context edges to find +where the problem originated. + +Classify the failure: + +- **Input error** — required input missing or malformed. Fix: input + validation or input resolution. +- **Scope denial** — step requested scopes outside the graph grant. + Fix: scope declarations or grant configuration. +- **Tool failure** — CLI tool or adapter returned an error. Fix: tool + invocation (args, env, cwd) or the tool itself. +- **Schema mismatch** — step output did not match expected shape for + downstream context. Fix: output parsing or artifact contract. +- **Timeout** — step exceeded time budget. Fix: increase timeout, + reduce work, or split the step. +- **Policy denial** — transition gate blocked the step. Fix: gate + conditions or upstream output. +- **Review rejection** — adversarial review found blocking issues. + Fix: the code or spec, not the review process. +- **Harness assertion** — fixture expectations did not match actual + output. Fix: skill logic or stale fixture expectations. + +## Agent-mediated suspension is not a failure + +A receipt with status `needs_agent` denotes a healthy +agent-mediated suspension, not a defect. The runtime yielded to the +caller for missing agent or human input. +This is a normal part of graph execution, not one of the failure +classes above. When the only evidence is `needs_agent` without +any exit code, scope denial, schema mismatch, or other concrete +failure signal, return `verdict: pass` with an empty +`improvement_proposals` array and note that the graph is paused as +designed. + +One failure, one fix. Propose the smallest change that addresses the root +cause. Do not bundle unrelated improvements. + + +## Output + +The output shape is formalised as JSON Schema at +[review-receipt-output.schema.json](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/schemas/review-receipt-output.schema.json). +Agents should self-validate before returning, and downstream +consumers (notably `write-harness`) may validate on receipt. + +- `verdict`: `pass`, `needs_update`, or `blocked`. +- `failure_summary`: which step, which failure class, what root cause. + One to three sentences. +- `improvement_proposals`: array of bounded changes. Each: + - `target`: what to change (SKILL.md, execution profile, graph step, input, fixture) + - `change`: what specifically to change + - `rationale`: why this fixes the root cause + - `risk`: what could go wrong +- `next_harness_checks`: replayable checks that should pass after the fix. + +## Inputs + +All optional — supply whichever evidence is available: + +- `receipt_id`: receipt id to inspect. +- `receipt_summary`: sanitized receipt or harness summary. +- `harness_output`: failed harness output or assertion text. +- `skill_path`: path to the skill being improved. diff --git a/docs/sourcey-catalog/pages/run-history.md b/docs/sourcey-catalog/pages/run-history.md new file mode 100644 index 000000000..afbfc819e --- /dev/null +++ b/docs/sourcey-catalog/pages/run-history.md @@ -0,0 +1,112 @@ +# run-history + +- Group: Outbound and tooling +- Source: [skills/run-history/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/run-history/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/run-history/SKILL.md` + +# Run History Analyst + +Turn runx's own run ledger into a governed, read-only report. + +Every governed runx run leaves a receipt. Over time that ledger is data: which +skills run, how often they seal versus refuse, which never graduate past alpha, +and where authority is consistently broader than usage. This skill reads that +ledger (via `runx history` and `runx list`) and reports it. It never executes a +skill, sends, or mutates; every planned call is read-only. Its recommendations +route to the governance skills, `least-privilege`, `audit-receipt`, +and the maturity promoter, so the report turns into action through the right +governed lane. + +## What this skill does + +1. **Scope the question.** Account-wide, a single skill, or a period. +2. **Pull the ledger, read-only.** Plan `runx history` and `runx list` queries; + never an execution command. +3. **Grade the signals.** Seal rate, refusal rate, maturity distribution, and + scope-usage breadth, each with an assessment, not a bare number. +4. **Recommend through governed lanes.** A high refusal rate, a skill stuck at + alpha, or a consistently-unused scope routes to a named governance skill, not + a direct mutation. + +## Core principles + +- **Read-only.** Only `runx history` and `runx list`. No execution, send, or + config call. Every planned call is `requires_confirmation: false`. +- **Grade, do not dump.** Every metric carries an assessment against a norm. +- **Route, do not act.** Recommendations name the governed lane + (`least-privilege`, `audit-receipt`, maturity promoter); this skill + does not change a grant or a tier itself. +- **Refusals are signal, not failure.** A healthy refusal rate means bounds are + working; a spike means a skill or a policy needs review. +- **Absence is not health.** With no history, return `needs_more_evidence`. + +## When to use this skill + +- Periodic platform review: what is runx actually doing across skills. +- Spotting skills with anomalous refusal rates or stuck maturity. +- Finding consistently-unused scopes worth attenuating. + +## When not to use this skill + +- For a single run's authority audit (use `audit-receipt`). +- To narrow one skill's grant from its usage (use `least-privilege`). +- For email or product analytics. This reports on runx runs, not a domain + dataset; that is a separate, product-owned analytics skill. + +## Signals and norms + +- `seal_rate`: share of runs that sealed cleanly. good >0.9, warning 0.7-0.9, + critical <0.7. +- `refusal_rate`: share of runs that hit a governed refusal. info by default; a + sharp per-skill spike is a warning worth routing. +- `maturity_distribution`: counts at alpha / beta / stable. Many skills stuck at + alpha is a warning (no harness coverage). +- `scope_usage`: scopes granted but never exercised across runs, a candidate for + attenuation. + + +## Output schema (`history_report`) + +```yaml +decision: ready | needs_more_evidence +scope: workspace | skill | all +period: string +ordered_tool_calls: + - tool: runx history | runx list + purpose: string + requires_confirmation: boolean # always false; read-only +findings: + - metric: string + value: string + assessment: good | warning | critical | info +recommendations: + - finding: string + lane: least-privilege | audit-receipt | maturity-promoter | none + action: string +blockers: [string] +needs_input: [string] +success_checkpoint: + milestone: string + description: string +``` + +## Worked example + +Question: "How is the skill catalog behaving this month?" The report plans +`runx history --since 30d` and `runx list skills --json`, then reports a 0.94 +seal rate (good), a refusal rate of 0.06 (info, bounds working), a maturity +spread of 14 alpha / 5 beta / 2 stable (warning, most skills lack harness +coverage), and one skill granted `repo.write` but never exercising it across 40 +runs. It recommends routing the alpha-heavy spread to the maturity promoter and +the unused `repo.write` to `least-privilege` for attenuation. It changes +nothing itself. + +## Inputs + +- `objective` (required): the history question. +- `scope` (optional): `workspace`, a specific `skill`, or `all`. +- `period` (optional): e.g. `30d` or `90d`. +- `history_summary` (optional): a sanitized `runx history` summary when already + fetched. +- `objective` guides which signals to lead with. diff --git a/docs/sourcey-catalog/pages/sandbox-harden.md b/docs/sourcey-catalog/pages/sandbox-harden.md new file mode 100644 index 000000000..7beac1649 --- /dev/null +++ b/docs/sourcey-catalog/pages/sandbox-harden.md @@ -0,0 +1,194 @@ +# sandbox-harden + +- Group: Safety and review +- Source: [skills/sandbox-harden/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/sandbox-harden/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/sandbox-harden/SKILL.md` + +# Sandbox Harden + +Decide the narrowest sandbox a workload can run inside without breaking it. + +## What this skill does + +Most workloads ship with the default sandbox their runtime hands them: the full +seccomp default, a broad capability set, unrestricted egress, a writable root. +That default is sized for the worst case, not for this workload. This skill reads +what a named workload actually needs and emits the tightest posture that still +lets it run: an allowed-syscall list, the capabilities to drop, an egress +allowlist, and a filesystem stance, with the residual risk named in plain terms. + +The output is a posture recommendation, not an enforced change. A runtime, an +orchestrator, or an operator applies it. This skill never executes the workload +and never widens a posture below the supplied baseline without saying why. + +How it differs from its neighbors: `least-privilege` audits API scopes, +not syscalls; `audit-receipt` reads a sealed run after the fact. This skill is +the only one that reasons about the seccomp, capability, egress, and filesystem +posture a workload should run inside before it starts. The recommendation reads +input only and writes nothing; applying it is a separate runtime act that +exercises `sandbox:configure` on the named workload and nothing wider. + +## When to use this skill + +- Before running an untrusted or third-party workload, to decide its sandbox. +- During a security review of an existing deployment whose sandbox is the broad + default. +- When promoting a workload toward production and the runtime posture must be + reviewable, not implicit. +- When an operator needs the egress allowlist and dropped capabilities written + down before a runtime applies them. + +## When not to use this skill + +- To run, build, or schedule the workload. This skill recommends a posture; a + runtime, orchestrator, or operator executes the workload under it. +- To audit which API scopes or grants a subject used. That is + `least-privilege`; it reasons about authority, this one reasons about + syscalls, capabilities, egress, and the filesystem. +- To audit a sealed receipt for over-reach after the fact. That is + `audit-receipt`. +- To handle, store, or surface the secret material a workload reads. A hardening + profile names a mount path or a secret handle, never a secret value. +- To produce a posture for a workload whose identity is unknown. Return + `needs_agent` instead of hardening an unnamed target. + +## Procedure + +1. **Resolve the workload.** + - Accept an image digest (`sha256:...`) or a skill ref. Record which form was + supplied as `hardening_profile.workload`. + - Gate: if no workload is supplied, stop with `needs_agent`. There is nothing + to harden. + +2. **Build the behavior model.** + - Combine the workload class (web service, batch job, CLI, language runtime), + the supplied `threat_context`, and the `baseline` posture. + - Distinguish known behavior from assumed behavior. A profile built on assumed + syscall need is weaker evidence than one built on an observed or documented + call set. + - Gate: if the behavior is unknown enough that the syscall set, egress, or + write paths would be a guess, stop with `needs_more_evidence` and name what + observation would resolve it (a trace, a manifest, a dry run under audit + seccomp). + +3. **Recommend the seccomp profile.** + - Default to `deny`. Add only syscalls the behavior model supports. + - Prefer a named runtime default profile plus an explicit allow delta over a + hand-rolled full list when the workload class has a known good baseline. + - Never add a syscall family with no behavioral basis. Unknown need is a stop + condition, not a blanket allow. + +4. **Drop capabilities.** + - Start from "drop all", then justify each capability kept. + - A capability is kept only when the behavior model needs it. Name the reason + per kept capability in the rationale. + +5. **Set the egress posture.** + - Default to `mode: none`. Move to `mode: allowlist` only when the workload + has a named, justified destination set. + - List hosts, not raw allow-everything. An empty allowlist means no egress. + - Never recommend open egress as a convenience. + +6. **Set the filesystem posture.** + - Default to `readonly: true` with an explicit `writable_paths` list. + - Each writable path is justified by the behavior model (scratch, cache, a + declared output dir). A writable root is a finding, not a default. + +7. **State residual risk.** + - After the controls above, name what an attacker who fully controls the + workload could still do, the `level`, and the `reason`. + - Residual risk is never "none". If the profile is built on assumed behavior, + say so here. + +8. **Honor the baseline.** + - The recommended posture must be at least as strict as the supplied baseline + on every axis. If the model would relax any control below the baseline, do + not relax it silently; either keep the baseline or, where a relaxation is + genuinely warranted, record the reason in the rationale and raise the + residual-risk level. + +The narrowness gate and the evidence gate are the two that hold authority: no +control weaker than the baseline without a stated reason and a raised +residual-risk level, and no syscall, host, or write path with no behavioral +basis. A posture no tighter than the baseline with no new evidence is not worth +emitting. + +## Edge cases and stop conditions + +- **Missing workload:** return `needs_agent`; an unnamed target cannot be + hardened. +- **Unknown behavior:** return `needs_more_evidence` with the observation that + would resolve it; do not pad the syscall set with plausible families. +- **Workload needs a privileged capability** (for example `CAP_SYS_ADMIN`): keep + it only with a stated reason and raise the residual-risk level; never drop a + capability the workload provably needs just to look tighter. +- **Egress to a dynamic or unbounded host set:** keep `mode: allowlist` with the + known hosts and flag the unbounded remainder as residual risk; do not fall back + to open egress. +- **Baseline is already tighter than the model:** keep the baseline; the + recommendation never loosens a control the operator already set. +- **Secret material in the input:** reference it by mount path or handle in the + profile and rationale; never copy a secret value into the output. +- **Conflicting threat context and baseline:** prefer the stricter control and + name the conflict in the rationale. + +## Output schema + +```yaml +hardening_profile: + decision: ready | needs_more_evidence | needs_agent + workload: + ref_form: image_digest | skill_ref + image_digest: string + skill_ref: string + class: string + seccomp: + default: deny | allow + allowed_syscalls: array + dropped_caps: array + egress: + mode: none | allowlist + hosts: array + filesystem: + readonly: boolean + writable_paths: array + residual_risk: + level: low | medium | high + reason: string + rationale: string +``` + +The single `hardening_profile` object is packet `runx.hardening.v1`. Secrets, +tokens, key material, and raw fetched content never appear in the profile; +secret-bearing inputs are referenced by mount path or handle only. The receipt +carries the workload ref form and digest, the four posture axes, the +residual-risk level, the stop status, and the quality and voice profile hashes. +It carries no secret values and no syscall trace payloads. + +## Worked example + +Input: `workload` is `{ image_digest: "sha256:1f4c...", class: "batch job" }`; +`threat_context` is "processes untrusted user uploads, no inbound network"; +`baseline` is "docker default seccomp, all caps, open egress, writable root". + +Output: `decision: ready`. `seccomp.default: deny` with an allowed set covering +file I/O, memory, and process control but not `ptrace`, `mount`, or raw socket +families. `dropped_caps` is the full default set (the job needs none). +`egress.mode: none` (no inbound or outbound network in the threat context). +`filesystem.readonly: true` with `writable_paths: ["/tmp/work"]` for upload +scratch. `residual_risk.level: low`, reason: a compromised job can still consume +CPU and fill `/tmp/work` to its quota; it cannot reach the network or escalate. +The rationale records that the syscall set is assumed from the batch-job class, +not from an observed trace, so a trace would raise confidence without widening +the posture. + +## Inputs + +- `workload` (required, json): the target to harden, as `{ image_digest }` or + `{ skill_ref }`, optionally with `class`. Without it the skill returns + `needs_agent`. +- `threat_context` (optional, string): the trust assumptions and exposure, for + example "processes untrusted uploads, no inbound network". +- `baseline` (optional, string): the current or floor posture. The + recommendation is never weaker than this without a stated reason. diff --git a/docs/sourcey-catalog/pages/sourcey.md b/docs/sourcey-catalog/pages/sourcey.md new file mode 100644 index 000000000..53c452014 --- /dev/null +++ b/docs/sourcey-catalog/pages/sourcey.md @@ -0,0 +1,383 @@ +# sourcey + +- Group: Outbound and tooling +- Source: [skills/sourcey/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/sourcey/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/sourcey/SKILL.md` + +# Sourcey + +Generate a documentation site for a project using Sourcey. Sourcey is a static +documentation generator that produces HTML sites from markdown pages, OpenAPI +specs, Doxygen XML, and MCP server snapshots. + +## What this skill does + +By default, runx executes Sourcey as a governed mixed-runner skill: + +1. discover the bounded documentation scope, evidence, and plan +2. request approval +3. author the bounded docs/config bundle +4. write the source bundle deterministically +5. build docs deterministically +6. critique the built output in one bounded pass +7. apply at most one bounded revision pass +8. rebuild and verify the output deterministically + +For already-configured projects, the same `sourcey` runner stays narrow: the +discover step can confirm existing config, the author/revise passes can return +empty bundles, and the deterministic tool steps still perform the build and +verification work. + +For repository-backed projects, Sourcey owns two separate surfaces: committed +docs source and generated site output. Keep those separate. Do not mix emitted +HTML, search indexes, or OG assets back into the authored docs tree. + +## When to use this skill + +- A project needs a maintainer-grade documentation site generated from real + repository evidence, existing docs, API specs, Doxygen XML, or MCP snapshots. +- A branded package or product needs Sourcey output with governed discovery, + approval, authoring, deterministic build, critique, revision, and receipt + proof. +- A workflow needs to separate authored docs source from generated site output + while preserving a reviewable receipt trail. +- A maintainer wants CI or deploy to rebuild docs without inventing scope, + prose, or information architecture at deploy time. + +## When not to use this skill + +- To manufacture documentation when the repository evidence is too thin. Return + `needs_more_evidence` or `needs_review` instead of confident filler. +- To write generated HTML, search indexes, or Open Graph assets back into the + source docs tree. +- To bypass approval for a new docs plan or to run open-ended critique/revision + loops. +- To document APIs by hand when an OpenAPI, Doxygen, or MCP source can be used + directly by Sourcey. + +## Documentation rules + +Sourcey output should read like native project documentation that a maintainer +would stand behind: + +- build from project evidence, but do not expose the evidence-gathering process + as page prose +- preserve the project's own terms, priorities, and level of ambition +- make fewer pages with real substance rather than many generic pages +- make a real developer action easier—install, evaluate, integrate, operate, or + contribute; a polished site with thin content is a failed run +- never use "generated by Sourcey", preview, adoption, migration, scaffold, or + demo framing unless the project itself uses that framing +- never describe pages as machine output, agent output, or AI-generated docs; + the site should read like the project maintainer wrote and stands behind it +- when publishing public docs, use a credible durable project, maintainer, + organization, product, or documentation home. Random personal domains, + placeholder parent sites, sandbox hosts, preview deploys, throwaway + subdomains, and unrelated novelty domains are not publication-quality homes +- if the repo evidence is too thin for a strong docs page, surface that as an + evidence gap instead of manufacturing confident filler + +## Canonical semantics + +Complex runx skills share a reusable phase language: + +- `scope` +- `ingest` +- `model` +- `materialize` +- `evaluate` +- `revise` +- `verify` +- `ratify` + +The current Sourcey runner deliberately uses a bounded subset: + +- `discover` folds `scope + ingest + model` +- `approve` is `ratify` +- `author + write-docs + build` form `materialize` +- `critique` is `evaluate` +- `revise + write-revisions + rebuild` form `revise` +- `verify` is `verify` + +The current slice uses exactly one bounded revision window. It never loops +until good and it never critiques indefinitely. + +When `docs_inputs` is supplied explicitly, treat that as a bounded instruction +to use the existing config target. Do not overwrite the referenced config or +invent replacement docs files merely because repository inspection evidence is +thin. Missing evidence is not the same as missing files. + +## Procedure + +1. Inspect the project and discover a bounded documentation plan from real project evidence. +2. Approve the discovered plan before authoring. +3. Author the bounded Sourcey source bundle. +4. Persist that bundle deterministically. +5. Run `sourcey build` deterministically with the discovered or authored config. +6. Critique the built output in one bounded evaluation pass. +7. Apply at most one bounded revision pass from that critique. +8. Rebuild deterministically after the revision bundle is written. +9. Verify the output directory contains `index.html`. +10. Inspect the receipt and generated site. + +The deterministic build report should carry enough rendered evidence for an +external reviewer to reason about the site without hidden file access. At +minimum that means the generated file list plus index-page title, headings, and +an excerpt when `index.html` exists. + +## Discovery contract + +`discovery_report` may include additional planning metadata, but the canonical +resolved docs inputs must live under: + +- `discovery_report.discovered.brand_name` +- `discovery_report.discovered.homepage_url` +- `discovery_report.discovered.docs_inputs` + +Downstream deterministic build steps consume that nested `discovered` object. + +## Output schema + +Sourcey build produces: HTML pages, `sourcey.css`, `sourcey.js`, +`search-index.json`, `sitemap.xml`, `llms.txt`, `llms-full.txt`, and +`_og/` directory with generated Open Graph images. + +The sealed package includes: + +```yaml +discovery_report: + discovered: + brand_name: string | null + homepage_url: string | null + docs_inputs: object | null +doc_bundle: + files: array + summary: string +sourcey_build_report: + generated_files: array + index_title: string + index_headings: array + index_excerpt: string +evaluation_report: object +revision_bundle: + files: array + summary: string +sourcey_verification_proof: + verified: boolean + index_path: string +receipt_notes: + authority: governed docs plan approval + mutation: authored docs source writes only +``` + +## Worked example + +Input: a project contains `README.md`, `package.json`, and a partial `docs/` +tree, but no Sourcey config. + +Output: `decision: ready` after approval; Sourcey discovers the project name, +homepage, and docs inputs, writes a bounded `docs/sourcey.config.ts` plus only +the highest-value missing docs pages, builds to `.sourcey/runx-docs`, critiques +the rendered `index.html`, applies at most one revision bundle, verifies the +output, and seals a receipt with the build report and verification proof. + +If the project evidence does not support a maintainer-grade site, the run stops +with `needs_more_evidence` or `needs_review` instead of producing filler. + +## Inputs + +- `project` (required): project root directory. +- `repo_root`: optional alias for the project root when Sourcey is composed inside a parent graph that already uses `repo_root`. +- `brand_name`: project name (discovered from package evidence if omitted). +- `homepage_url`: project homepage (discovered from project evidence if omitted). +- `docs_inputs`: structured docs inputs, e.g. `{"mode":"config","config":"docs/sourcey.config.ts"}` or `{"mode":"openapi","spec":"openapi.yaml"}`. Discovered if omitted and may point at authored config produced by the skill. +- `project_brief`: optional grounded brief carrying brand cues, docs audit, + IA direction, and writing constraints. When present, the authored docs should + feel like native project docs rather than generic generated scaffolding. +- `output_dir`: generated site output path (default: `/.sourcey/runx-docs`). +- `sourcey_bin`: explicit sourcey executable path (default: `SOURCEY_BIN` env or `sourcey` on PATH). + +## Repository Contract + +- Keep authored docs source in the repository, usually under `docs/` when using + `docs/sourcey.config.ts`. +- Keep generated site output in `output_dir`, separate from the source tree. +- The default generated output path is `/.sourcey/runx-docs`. +- Generated output should be gitignored unless the project explicitly chooses to + version release artifacts. +- CI or deploy may run deterministic `sourcey build` from committed source. +- Deploy must not be the step where docs scope, prose, or IA is invented. Do + discovery, authoring, and review before deploy. +- For Astro host apps, prefer the first-class `sourcey/astro` integration over + a separate prebuild script that writes into `public/docs`. Keep + `docs/sourcey.config.ts` and markdown/spec inputs as source; let `astro dev` + serve Sourcey through Vite and `astro build` write generated docs into the + final output under the configured route. +- For public publication, include enough proof for an external reviewer to + inspect the target project, source commit, Sourcey config or input source, + generated page list, deployment URL, parent domain, and durability of the + hosting choice. + +## Astro host pattern + +Use this shape when the target already uses Astro and docs should live at a +path such as `/docs`: + +```typescript +import { defineConfig } from "astro/config"; +import sourcey from "sourcey/astro"; + +export default defineConfig({ + site: "https://example.com", + integrations: [ + sourcey({ + config: "./docs/sourcey.config.ts", + routeBase: "/docs", + }), + ], +}); +``` + +Do not add `prebuild`, `build:docs`, or committed `public/docs` artifacts for +this path unless the project explicitly cannot use Astro integrations. The +generated output remains reproducible build output, not authored source. + +## Config reference + +```typescript +import { defineConfig } from "sourcey"; + +export default defineConfig({ + name: "Project Name", + theme: { + preset: "default", // "default" | "minimal" | "api-first" + colors: { + primary: "#hex", // required + light: "#hex", // optional, derived from primary + dark: "#hex", // optional, derived from primary + }, + fonts: { + sans: "Inter", // optional + mono: "monospace", // optional + }, + layout: { + sidebar: "18rem", // optional + toc: "19rem", // optional + content: "44rem", // optional + }, + css: ["path/to/custom.css"], // optional + }, + logo: "path/to/logo.png", // or { light, dark, href } + favicon: "path/to/favicon.ico", + repo: "https://github.com/org/repo", + editBranch: "main", + editBasePath: "docs", // path from repo root to docs source + codeSamples: ["curl", "javascript", "python"], // for OpenAPI tabs + navigation: { + tabs: [ + // Markdown pages tab + { + tab: "Documentation", + slug: "", // empty = default tab + groups: [ + { group: "Getting Started", pages: ["introduction", "quickstart"] }, + { group: "Guides", pages: ["configuration", "deployment"] }, + ], + }, + // OpenAPI tab + { + tab: "API Reference", + openapi: "path/to/openapi.yaml", + }, + // Doxygen tab + { + tab: "C++ API", + doxygen: { + xml: "path/to/doxygen/xml", + language: "cpp", // "cpp" | "java" + groups: true, // use doxygen groups for nav + index: "auto", // "auto"|"rich"|"structured"|"flat"|"none" + }, + }, + // MCP tab + { + tab: "Tools", + mcp: "path/to/mcp.json", + }, + ], + }, + navbar: { + links: [ + { type: "github", href: "https://github.com/org/repo" }, + // types: github, twitter, discord, linkedin, youtube, slack, + // mastodon, bluesky, reddit, npm, link + ], + primary: { type: "button", label: "Demo", href: "/demo" }, + }, + footer: { + links: [{ type: "github", href: "https://github.com/org/repo" }], + }, + search: { + featured: ["introduction", "quickstart"], // top results when empty query + }, +}); +``` + +## Page format + +Pages are markdown files resolved relative to the config file directory. +If config is at `docs/sourcey.config.ts`, then page `"quickstart"` resolves +to `docs/quickstart.md`. + +```markdown +--- +title: Page Title +description: One-line description for search and meta tags +--- + +Content here. Standard markdown with code blocks, tables, links. +``` + +## Card Icon Contract + +Sourcey card icons are Heroicons v2 outline names in kebab-case. The renderer +returns an empty icon for unknown names, so authoring must use exact names. + +Known-good names for documentation cards include: `academic-cap`, `arrow-path`, +`bell`, `bolt`, `book-open`, `chart-bar`, `check-circle`, `cloud-arrow-up`, +`code-bracket`, `command-line`, `cpu-chip`, `cube`, `document`, +`document-text`, `exclamation-triangle`, `globe-alt`, `key`, `lifebuoy`, +`light-bulb`, `lock-closed`, `magnifying-glass`, `map`, `rocket-launch`, +`server-stack`, `shield-check`, `sparkles`, and `wrench-screwdriver`. + +Invalid card icon names are a blocking quality issue. The build report includes +`icon_validation`; critique and revision must fix any +`icon_validation.status: "invalid"` result before the run is accepted. + +## Edge cases and stop conditions + +- Only create tabs for content types the project actually has. Do not add an + OpenAPI tab if there is no spec file. Do not add a Doxygen tab without XML. +- Do not document APIs by hand when a spec file exists — use the spec tab. +- Keep navigation shallow: 1-2 tabs, 2-4 groups for most projects. +- Use project brand colors if identifiable. Otherwise use a neutral palette. +- Use only exact Heroicons v2 outline names for Sourcey card `icon` + attributes; never invent icon names. +- When a grounded brief provides logo, favicon, color, or IA guidance, prefer + that over generic defaults. +- Match the project's existing voice and terminology. +- Never write docs that describe themselves as a preview, adoption, migration, + or tool-generated scaffold unless the repo's own evidence explicitly uses that + framing. +- Do not write generated HTML, search indexes, or OG assets into the authored + docs source tree. +- If `output_dir` lives under the repo root, gitignore it or call out the + missing ignore rule as an operational gap. +- Build output may be regenerated in CI or deploy, but deploy must not author + or revise docs content. +- Public deployments must be durable and socially credible. Do not treat a + throwaway preview URL, unrelated personal domain, placeholder parent site, or + sandbox subdomain as a completed public docs home. +- Do not encode open-ended critique or revision behavior. Critique is one + bounded evaluation pass. Revision is at most one explicit bounded pass. diff --git a/docs/sourcey-catalog/pages/web-fetch.md b/docs/sourcey-catalog/pages/web-fetch.md new file mode 100644 index 000000000..0f749e676 --- /dev/null +++ b/docs/sourcey-catalog/pages/web-fetch.md @@ -0,0 +1,134 @@ +# web-fetch + +- Group: Research and data +- Source: [skills/web-fetch/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/web-fetch/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/web-fetch/SKILL.md` + +# Web Fetch + +Fetch one URL, prove it was allowed, extract the part the caller asked for, and +return that slice by digest with the provenance needed to trust it later. + +## What this skill does + +`web-fetch` resolves a single URL against a host allowlist, retrieves it, +extracts text, metadata, or links, and seals the result so a downstream step can +cite the fetch without re-fetching. It checks the final host against the +allowlist before and after redirects, retrieves up to `max_bytes`, and returns +the final URL, the HTTP status, a `content_digest` over the retrieved body, the +extracted slice, and a provenance block recording when it ran, every redirect +hop, and how many bytes it read. The body is referenced by digest; only the +extracted slice is inlined. + +This is the primitive an agent reaches for when it has already decided which page +to read. The decision it makes easier is "can I read this page, and what did it +actually say", with the answer backed by a digest instead of a remembered +paraphrase. It differs from the research family: `research` and +`deep-research` decide *which* sources matter and synthesize across them, +while `web-fetch` retrieves exactly one source and refuses anything off the +allowlist. + +## When to use this skill + +- An agent has chosen a specific page and needs its content bound to a + `content_digest` so a later step can cite it without re-fetching. +- A research pass needs each source retrieved through a single bounded + `net:allowlist` fetch with a complete redirect chain and byte count. +- A review must later prove what a page said at fetch time. +- A follow-on skill (prior-art, vuln-triage, brief) needs one source extracted as + `text`, `metadata`, or `links`. + +## When not to use this skill + +- To judge, rank, or synthesize across sources. That is the research family's + job; `web-fetch` retrieves exactly one source and refuses to reason over many. +- To reach a host the caller did not declare in `allowlist`, including a host + reached only through a redirect. +- To write anything. The only scope is `net:allowlist`; there is no repo, file, + wallet, or send authority here. +- To inline a large raw body. The extracted slice is the payload; the full body + lives behind `content_digest`. +- To carry secrets. Request headers may reference a credential by `${secret}` + handle, but no header value, cookie, token, or auth string appears in the + output or the receipt. + +## Procedure + +1. Require `url` and `allowlist`. Either missing returns `needs_agent`; the + fetch cannot run without a target and a declared scope. +2. Match the URL host against `allowlist`. On a miss, return `policy_denied` + before any network call, recording the attempted host and the allowlist it + was checked against. +3. Fetch, following redirects, re-checking each redirect target's host against + the same `allowlist`. A redirect that lands off-allowlist halts the fetch, + returns `policy_denied` with the hop that failed, and discards partial + bodies. Cap the read at `max_bytes` when set. +4. Compute `content_digest` over the retrieved body. +5. Extract per `extract`: `text` (readable body text, default), `metadata` + (title, description, canonical, declared language, content type), or `links` + (absolute hrefs found in the document). +6. Return `fetch_result` with the final URL, status, digest, extracted slice, + and provenance. Flag truncated reads in provenance; never return a clipped + read as if whole. + +## Edge cases and stop conditions + +- **Missing `url` or `allowlist`:** return `needs_agent`; the fetch has no target + or no scope to check against. +- **Host off the allowlist:** stop with `policy_denied` before any network call; + record the attempted host, not a response body (there is none). +- **Redirect off the allowlist:** halt the fetch, return `policy_denied` naming + the hop that failed, and discard the partial body. +- **Read clipped by `max_bytes`:** flag `truncated: true` in provenance; the + digest is over the bytes actually retrieved. +- **Large raw body:** never inline beyond the extracted slice; anything bigger + than the requested view is reachable only through `content_digest`. +- **Credential in a header:** reference it by `${secret}` handle only; no header + value, cookie, or token reaches the output or the receipt. + +## Output schema + +```yaml +fetch_result: + decision: ready | needs_agent | policy_denied + final_url: string # URL after redirects, the one the digest is over + status: number # HTTP status of the final response + content_digest: string # digest of the retrieved body, algorithm prefix included + extract_mode: text | metadata | links + extracted: string | object | array # string for text, object for metadata, array of hrefs for links + provenance: + fetched_at: string # timestamp of the fetch + redirects: array # ordered host hops, each re-checked against the allowlist + bytes: number # bytes read + truncated: boolean # true when max_bytes clipped the read + policy: + allowlist_decision: allowed | denied + attempted_host: string # set on policy_denied + allowlist_checked: array # the hosts the request was checked against +``` + +The sealed `runx.receipt.v1` carries the final URL, status, `content_digest`, +byte count, the redirect chain, and the allowlist decision. It carries no header +values, no cookies, and no raw body beyond the digest. + +## Worked example + +Input: `url` of the HTTP Semantics RFC, an `allowlist` of `www.rfc-editor.org` +and `rfc-editor.org`, `extract: text`, and `max_bytes: 200000`. + +Output: `decision: ready`; the host matched the allowlist before the request +left; no redirects; status `200`; `content_digest` is taken over the retrieved +body; `extracted` holds the readable text slice; provenance records +`fetched_at`, an empty redirect chain, `184302` bytes, and `truncated: false`. +The receipt seals with the final URL, status, digest, byte count, and the +allowlist decision; no header value reaches it. + +## Inputs + +- `url` (required): the single URL to fetch; its host must match the allowlist. +- `allowlist` (required): permitted hosts or host patterns; the URL and every + redirect target must match an entry. +- `extract` (optional): `text`, `metadata`, or `links`. Defaults to `text`. +- `max_bytes` (optional): cap on bytes read; a clipped read is flagged + `truncated` in provenance. diff --git a/docs/sourcey-catalog/pages/work-plan.md b/docs/sourcey-catalog/pages/work-plan.md new file mode 100644 index 000000000..1115cd627 --- /dev/null +++ b/docs/sourcey-catalog/pages/work-plan.md @@ -0,0 +1,107 @@ +# work-plan + +- Group: Operate +- Source: [skills/work-plan/SKILL.md](https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/work-plan/SKILL.md) +- Commit: `5afc25a83edf1c1320df7ac0d78c36f1523b5677` +- Path: `skills/work-plan/SKILL.md` + +# Work Plan + +Turn a build or automation objective into a bounded governed work plan. + +For cross-repo or cross-surface work, the output must be a phased +`workspace_change_plan`, not just a loose list of steps. The shared plan is the +thing that keeps repo-local workers aligned when one issue fans out into +multiple mutation surfaces. + +When the objective originates from an existing thread, treat that thread +as provider-backed thread. GitHub issues, chat threads, support +tickets, and local agent sessions are adapter examples, not core nouns. The +plan should preserve the generic `thread_locator` and any supplied +`thread`. + +The central insight: split at governance boundaries, not cognitive boundaries. +A skill keeps its full context window. If two actions need the same context +but different scopes, they are two invocations of the same skill with +different scopes — not two separate skills. The graph defines where authority +changes, where mutation happens, and where a gate needs to approve. That is +where steps break. + +Work backward from the deliverable. Name the concrete artifact the objective +produces (spec, patch, PR, docs site, report). Then identify where authority +narrows: read-only analysis, write-access mutation, approval gates, review +boundaries. Each narrowing is a step boundary. Each step gets only the scopes +it needs — no step inherits from a prior step, each derives from the graph +grant independently. + +Determine data dependencies between steps. A step that consumes output from +a prior step must come after it. Steps with no data dependency are candidates +for fanout. Do not parallelize steps that share mutation targets. + +If the objective is ambiguous or required context is missing, surface open +questions explicitly rather than guessing. Open questions should name what +is missing, why it matters, and who can answer it. + +Prefer fewer steps with clear scope boundaries. Three well-scoped steps +beat seven single-purpose fragments. Every step should have a clear entry +condition, action, and exit artifact. + + +## Output + +- `change_set`: the parent change artifact inherited from intake or constructed + for the objective when intake did not already produce one. It should preserve + the shared objective, target surfaces, invariants, and success criteria. +- `harness_context`: when supplied, the same `runx.receipt.v1` packet with state + advanced to `planning_ready` or `blocked`. Preserve source events, dedupe, + and triage fields rather than reconstructing them from prose. +- `objective_summary`: one sentence capturing the deliverable. +- `workspace_change_plan`: phased plan for the whole change set. It must + contain: + - `plan_id` + - `change_set_id` + - `objective_summary` + - `shared_invariants` + - `success_criteria` + - `phases`: ordered array. Each phase: + - `id` + - `name` + - `depends_on`: prior phase ids + - `parallelizable`: boolean + - `repo_change_requests`: ordered array. Each request: + - `repo` + - `task_id` + - `objective` + - `depends_on`: sibling repo change request ids this request waits on + - `shared_context_refs`: references into the parent change set or prior + phase outputs + - `validation_commands` + - `mutating` + - `integration_checks`: cross-repo checks that must pass before the overall + change set is considered done + - `open_questions` +- `orchestration_steps`: canonical execution view of the plan as an ordered array. + Each step: + - `id`: kebab-case identifier + - `skill`: skill name or path + - `scopes`: scope strings this step requires + - `mutating`: boolean + - `inputs`: static input map + - `context_from`: `step_id.output_field` data dependency references + - `description`: what this step does and produces +- `required_skills`: skill names needed. Flag which exist vs need creation. +- `open_questions`: missing context that must be answered before mutation. + +## Inputs + +- `objective` (required): the build or skill objective to decompose. +- `project_context` (optional): repo, product, or user context that + constrains the decomposition. +- `change_set` (optional): parent change artifact from `issue-intake` or a + workspace supervisor. Prefer this when present. +- `harness_context` (optional): portable issue control-plane packet from intake. + Preserve it as state, not as a prose handoff. +- `thread_locator` (optional): canonical locator for the bounded thread the + plan is serving. +- `thread` (optional): portable thread when the objective is + grounded in an existing issue, chat, ticket, or other adapter surface. diff --git a/docs/sourcey-catalog/site/_og/pages/agency.png b/docs/sourcey-catalog/site/_og/pages/agency.png new file mode 100644 index 000000000..7a4c2ddc7 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/agency.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/audit-receipt.png b/docs/sourcey-catalog/site/_og/pages/audit-receipt.png new file mode 100644 index 000000000..4ac5a6f01 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/audit-receipt.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/business-ops.png b/docs/sourcey-catalog/site/_og/pages/business-ops.png new file mode 100644 index 000000000..6185e07df Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/business-ops.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/cve-audit.png b/docs/sourcey-catalog/site/_og/pages/cve-audit.png new file mode 100644 index 000000000..8719bf892 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/cve-audit.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/data-store.png b/docs/sourcey-catalog/site/_og/pages/data-store.png new file mode 100644 index 000000000..ed79ba9ca Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/data-store.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/deep-research.png b/docs/sourcey-catalog/site/_og/pages/deep-research.png new file mode 100644 index 000000000..c49beb677 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/deep-research.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/github-sync.png b/docs/sourcey-catalog/site/_og/pages/github-sync.png new file mode 100644 index 000000000..b77c9ecbc Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/github-sync.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/governed-outbound.png b/docs/sourcey-catalog/site/_og/pages/governed-outbound.png new file mode 100644 index 000000000..9aca5f482 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/governed-outbound.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/introduction.png b/docs/sourcey-catalog/site/_og/pages/introduction.png new file mode 100644 index 000000000..f8c5f1648 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/introduction.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/issue-intake.png b/docs/sourcey-catalog/site/_og/pages/issue-intake.png new file mode 100644 index 000000000..0f937c9ad Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/issue-intake.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/issue-to-pr.png b/docs/sourcey-catalog/site/_og/pages/issue-to-pr.png new file mode 100644 index 000000000..acfabfea3 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/issue-to-pr.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/issue-triage.png b/docs/sourcey-catalog/site/_og/pages/issue-triage.png new file mode 100644 index 000000000..6f2ba9ce5 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/issue-triage.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/knowledge-router.png b/docs/sourcey-catalog/site/_og/pages/knowledge-router.png new file mode 100644 index 000000000..375633d3a Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/knowledge-router.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/least-privilege.png b/docs/sourcey-catalog/site/_og/pages/least-privilege.png new file mode 100644 index 000000000..95e873bcf Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/least-privilege.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/operator-inbox.png b/docs/sourcey-catalog/site/_og/pages/operator-inbox.png new file mode 100644 index 000000000..41acbd85d Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/operator-inbox.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/ops-desk.png b/docs/sourcey-catalog/site/_og/pages/ops-desk.png new file mode 100644 index 000000000..3fcaf4c15 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/ops-desk.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/policy-author.png b/docs/sourcey-catalog/site/_og/pages/policy-author.png new file mode 100644 index 000000000..b905a5f8d Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/policy-author.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/release.png b/docs/sourcey-catalog/site/_og/pages/release.png new file mode 100644 index 000000000..c2f4f3a71 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/release.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/research.png b/docs/sourcey-catalog/site/_og/pages/research.png new file mode 100644 index 000000000..962688805 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/research.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/review-receipt.png b/docs/sourcey-catalog/site/_og/pages/review-receipt.png new file mode 100644 index 000000000..ac696fd67 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/review-receipt.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/run-history.png b/docs/sourcey-catalog/site/_og/pages/run-history.png new file mode 100644 index 000000000..e3122467d Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/run-history.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/sandbox-harden.png b/docs/sourcey-catalog/site/_og/pages/sandbox-harden.png new file mode 100644 index 000000000..13d73a3a0 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/sandbox-harden.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/sourcey.png b/docs/sourcey-catalog/site/_og/pages/sourcey.png new file mode 100644 index 000000000..39a80600c Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/sourcey.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/web-fetch.png b/docs/sourcey-catalog/site/_og/pages/web-fetch.png new file mode 100644 index 000000000..406f17f7b Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/web-fetch.png differ diff --git a/docs/sourcey-catalog/site/_og/pages/work-plan.png b/docs/sourcey-catalog/site/_og/pages/work-plan.png new file mode 100644 index 000000000..2b50b8490 Binary files /dev/null and b/docs/sourcey-catalog/site/_og/pages/work-plan.png differ diff --git a/docs/sourcey-catalog/site/index.html b/docs/sourcey-catalog/site/index.html new file mode 100644 index 000000000..050ed19f5 --- /dev/null +++ b/docs/sourcey-catalog/site/index.html @@ -0,0 +1,27 @@ + +Introduction - Runx Governed Skill Catalog
Introduction

Introduction

Governed Runx skill catalog pinned to one upstream revision.

Runx Governed Skill Catalog

+

This catalog covers exactly 24 governed Runx skills at upstream commit 5afc25a83edf1c1320df7ac0d78c36f1523b5677.

+

The skills are organized into five groups: Operate, Research and data, GitHub and delivery, Safety and review, and Outbound and tooling. Each catalog page links to its authoritative SKILL.md at the pinned commit.

+

This is a governed skill catalog, not a claim of complete Runx API coverage.

+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/llms-full.txt b/docs/sourcey-catalog/site/llms-full.txt new file mode 100644 index 000000000..0ae5799e7 --- /dev/null +++ b/docs/sourcey-catalog/site/llms-full.txt @@ -0,0 +1,205 @@ +# Runx Governed Skill Catalog + +Governed Runx skill catalog pinned to one upstream revision. + +## Skills + +### Introduction + +Path: `/runxhq/runx/pages/introduction.html` + +Governed Runx skill catalog pinned to one upstream revision. + +Runx Governed Skill Catalog This catalog covers exactly 24 governed Runx skills at upstream commit 5afc25a83edf1c1320df7ac0d78c36f1523b5677 . The skills are organized into five groups: Operate, Research and data, GitHub and delivery, Safety and review, and Outbound and tooling. Each catalog page links to its authoritative SKILL.md at the pinned commit. This is a governed skill catalog, not a claim of complete Runx API coverage. + +### agency + +Path: `/runxhq/runx/pages/agency.html` + +Run a standing, accountable team toward a mandate, one governed turn at a time. + +Group: Operate Source: skills/agency/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/agency/SKILL.md Agency Run a standing, accountable team toward a mandate, one governed turn at a time. An agency is the only runx skill that holds a roster, a persistent objective, and a case that spans turns. It is a governed delegation envelope: a defined set of members with scope ceilings, a mandate, cumulative limits, and a case whose every turn is sealed and replayable. It composes the existing skills and reimplements none. Each turn borrows ops-desk for judgment, the roster members for execution, data-store for the event log, and receipts for the ledger. It is not a durable-execution engine and it is not an autonomous daemon. One turn is one stateless governed act; an external driver (a human, a cron, a board poll) runs the loop by calling advance until the case resolves. What this skill does open starts a case: it appends opened with the mandate, the roster, and the cumulative limits snapshot, so the charter travels with the case. advance runs one turn: it folds the case from its event stream, asks ops-desk for the single next move constrained to the roster, enforces the measurable gate, records one turn event whose append is the contention lease, and names the member to run. The member runs as a separate governed run; its outcome is fed back to the next advance as member_result . status folds and returns the current case state. The case reducer is the agency's own code, because data-store carries events but does not fold domain state. Everything else is delegation. When to use this skill A standing, consequential mandate must run for days or weeks, dispatch different members, and leave an auditable trail sealed to a bounded authority. A process needs scoped delegation with a measurable ceiling and a human gate on consequence, not an unbounded agent. When not to use this skill One-shot or interactive work. Call the member skills directly; the agency is overhead when the operator is already the loop. To compute proposals (that is ops-desk ) or claim and clock logic (that is messageboard ). Compose them. To bake a storage backend. The case lives in data-store via data_source_ref . To let the model invent the roster, the mandate, or the limits. They are operator config, snapshotted into the case at open . Procedure open the case with the mandate, roster, and limits. advance the case. Read the turn packet: advanced : run the named member under its scope, then advance again with the member's outcome as member_result . awaiting_approval : resolve the escalation, then advance . resolved or failed : the case is closed. Repeat until the case resolves. The driver, not this skill, decides the cadence. The measurable gate The done-check and the limit-check are measurable first. advance folds cumulative totals (acts, spend) and the trusted planner overrides the model when a cap is breached: an over-cap turn fails regardless of what ops-desk proposed. The narrative judgment from ops-desk chooses the move within the caps; it never widens them. Spend caps tracked in the projection are the v1 path; routing spend through spend and runx-pay reservations is the stronger enforcement. Contention Two drivers must not double-fire a member act. Each turn appends a single event keyed case_id:turn:driver_id at the folded expected_version . Two drivers racing the same turn carry different keys, so the loser hits a hard version conflict rather than replaying the winner, and stops before any dispatch. The append is the lease, and it lands before the named member runs. Edge cases and stop conditions No case at case_id : advance returns needs_input ; open the case first. A cumulative cap is reached: the turn is failed with the breached predicate named. The best move is consequential and unapproved: awaiting_approval with the prompt. No roster member can act and nothing is escalatable: escalate to the configured human with the missing input named. Output schema advance returns one agency_turn : Copy agency_turn : schema : runx.agency.turn.v1 status : advanced | awaiting_approval | resolved | needs_input | failed case_id : string turn : number dispatch : # present when status == advanced member : string skill : string task : string needed_scope : [ string ] approval_prompt : string | null resolution : object | null predicates : object # the measurable over_limits booleans reason : string | null next : string Inputs open : data_source_ref , case_id , agency_ref , mandate , roster , limits , optional signal . advance : data_source_ref , case_id , driver_id , optional member_result . status : data_source_ref , case_id . Worked example Open a docs case with a researcher, writer, and reviewer and a 50-turn limit. advance folds an empty-but-opened case, ops-desk picks the researcher, and the turn returns advanced naming the researcher. The driver runs the researcher and calls advance again with its result; ops-desk now picks the writer to draft. When the reviewer approves and the projection shows the docs current, advance returns resolved . Turn rules Fold every turn from the sealed stream; never infer state the events do not show. Enforce the measurable gate before the model's judgment; never widen a cap. Name the member and the verification expectation on every dispatch; never claim work settled, sent, paid, or done without a receipt. Compose ops-desk, data-store, and the members; never reimplement them, and never invent the roster or the mandate. Stop cleanly with needs_input, awaiting_approval, refused, or failed; never a fake ready. + +### business-ops + +Path: `/runxhq/runx/pages/business-ops.html` + +Turn one business signal into a replayable operations graph. + +Group: Operate Source: skills/business-ops/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/business-ops/SKILL.md Business Ops Turn one business signal into a replayable operations graph. business-ops is the generic public example for how runx makes agentic business work composable without giving the agent ambient authority. It is a deterministic graph skeleton: it classifies one signal, fans it into bounded lanes, records why each lane exists, names the real skill or provider lane that would replace the fixture, and stops before any live send, spend, publish, merge, deploy, or customer-visible action. This is not a provider integration and not an operator dashboard. It is the small core shape that teams copy when they want one objective to fan out into a chain of skills, then replay that chain with receipts. When the route itself should become durable, use route_and_append . That runner classifies the signal, appends the classification packet through data-store , and reads back the projection. The same graph can use local JSON, SQLite, Postgres, D1, Redis, or a product adapter by changing the data_source_ref binding. What this skill does Classifies one business signal before doing work. Fans the signal through representative lanes: docs, release, issue/PR, outreach planning, spend quoting, and proof audit. Produces structured lane packets with authority, gate, handoff, evidence, and readback fields. Demonstrates the runx split between proposal work and consequential action: drafts and plans can be produced, but sends, spend, merges, publishes, and deploys require a separate approval and execution lane. Gives downstream agents a clear handoff target instead of vague prose. Optionally persists the classified route for replay through data-store . What this skill deliberately does not do It does not call private providers, mutate a repo, post to GitHub, send email, schedule campaigns, move money, publish releases, or deploy services. It does not duplicate ops-desk , product operator skills, send-as , vendor-specific provider skills, release , issue-to-pr , spend , or receipt-audit skills. It does not turn "outbound marketing" into a hidden side effect. Outreach is a plan lane here; real delivery routes to send-as and then a provider adapter. Branded provider skills are concrete adapters, not branches in this core graph. It does not treat the graph receipt as proof that an external provider action happened. Provider actions need provider evidence and their own receipt. When to use this skill To show how runx chains skills into replayable business operations. To prototype a team-specific ops graph before wiring private provider tools. To route a product signal without giving the agent blanket repo, email, wallet, or deployment access. To explain why a governed workflow is more useful than a one-shot prompt: the route, stops, handoffs, and readbacks are explicit and replayable. To smoke-test graph execution and child receipts with no external account. When not to use this skill To run a production launch, incident, release, campaign, support reply, payout, or spend flow as-is. Replace fixture lanes with real skills first. To approve a live send, spend, merge, publish, deploy, or customer-visible action. To hide project policy, customer lists, credentials, wallet keys, provider dumps, or private review context in the signal. To claim external work completed when only this fixture graph ran. Mental model Copy signal -> classify -> fanout lanes -> approval stops -> governed handoffs -> proof The useful part is the chain. A single objective becomes several typed packets: some read-only, some draft-only, some blocked until approval, and one proof lane that states how success should be verified later. A human, agent, dashboard, or CI loop can replay the same route and see the same stops. How this maps to real runx work Docs and public proof route to a docs skill such as sourcey or a product-owned documentation lane. Release preparation routes to release , with publish held behind a release approval. Code work routes to issue-to-pr or a project-owned implementation lane, with merge held behind review. Outreach and customer communication route first to send-as , then to a provider adapter that implements the send lane. Branded provider skills are the right place for vendor-specific compose, test, review, schedule, or send details. Broad outbound marketing should be its own skill or product broadcast skill, not extra logic hidden in this graph. Spend and payments route to quote or payout skills with caps, recipient, rail, and settlement proof separated from the planning lane. Proof routes to receipt/history/audit skills and provider readbacks. The fixture ops-lane step simply returns these packets without performing the handoff. In a real project, replace each fixture lane with the named governed skill runner or provider tool. Procedure Receive one concise signal . Optionally receive operator_context with project constraints, policy, or the concrete business situation. Run classify first. It decides which lanes are relevant and what authority class each lane belongs to. Fan out docs, release, issue, outreach, spend, and proof packets. Mark each lane as read-only, draft-only, approval-required, or proof-only. Name the exact downstream handoff that should replace the fixture in a real workflow. Seal the graph so the route itself is replayable. If using route_and_append , append the classification packet with an idempotency key and expected version, then read back the projection. Edge cases and stop conditions Missing signal: return needs_input . There is no safe route. Vague objective: return a narrow classify packet and ask for the missing product, audience, repo, release, amount, or provider context. Live send without principal, audience, consent, digest, and approval: stop at the outreach lane and route to send-as . Spend without amount, cap, recipient, rail, and approval: stop at the spend lane and route to a quote or payment skill. Merge, publish, deploy, or destructive mutation without approval: stop at the relevant lane and name the missing gate. Provider success without provider evidence: do not mark complete. Route to proof audit. Secret or private data in the signal: refuse to echo it into outputs; require redacted context or a provider-side readback instead. Output schema The graph output contains child step receipts plus one lane_packet per lane: Copy lane_packet : schema : runx.business_ops_lane.v1 lane : string signal : string status : ready | awaiting_approval | needs_input | refused decision : route | prepare | draft | quote | verify | stop kind : router | docs | release | work | outreach | spend | proof consequence : read_only | draft | live_mutation | public_send | money_movement | proof summary : string why : string authority : requested : [ string ] provided : fixture_only gate : approval_required : boolean approval_gate : string | null stop_reason : string | null handoff : interface : skill | graph | cli | hosted_api | workflow | provider_tool lane_ref : string runner_ref : string | null command_hint : string | null evidence : inputs_required : [ string ] readbacks : [ string ] receipt_refs : [ string ] risks : [ string ] next : [ string ] Worked example Copy runx skill business-ops \ -i signal="launch readiness for API v2: docs, release, customer comms, and spend checks" \ --json The graph classifies the launch signal, prepares docs/release/work packets, routes customer communication to an outreach plan, stops spend at a quote gate, and names receipt/history checks that would prove later execution. No external provider is called. Inputs signal (required): concise business operations signal to classify and route. operator_context (optional): product policy, project topology, audience constraints, or known provider state. Context only, not authority. + +### operator-inbox + +Path: `/runxhq/runx/pages/operator-inbox.html` + +Maintain a durable action queue without turning a connector into the owner of operator state. + +Group: Operate Source: skills/operator-inbox/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/operator-inbox/SKILL.md Operator Inbox Maintain a durable action queue without turning a connector into the owner of operator state. The caller fetches bounded, grant-authorized provider pages and passes their normalized observations to this skill. The skill owns work-item identity, status, dispositions, replay suppression, reopen rules, and scan coverage. Every read and write is composed through data-store ; the skill does not call Slack, SQLite, Postgres, or another provider directly. What this skill does Use local://runx/operator-inbox/default unless the operator selects another logical source. Unbound local refs resolve to SQLite under .runx/data/local-sources/ . A hosted database is opt-in through the same data_source_ref binding. Runx Connect may still own OAuth, grants, and provider execution; that does not move this queue into the hosted control plane. Observations and resumable checkpoints live in operator_inbox_scans , partitioned by query digest. Action snapshots live in operator_inbox_actions , with one stream per stable thread digest. Queue reads use bounded list_stream_heads pages; no command folds or transports the complete queue. When to use this skill Build or revisit a local action queue from bounded connector observations. Preserve an explicit resolved , dismissed , waiting , or followed_up decision across repeated provider scans. Reopen a completed item when a newer external occurrence arrives. Inspect bounded action or scan state without handing queue ownership to the provider or hosted control plane. When not to use this skill Do not fetch provider data, reply, send, or mutate a remote account here. Do not infer that an item is complete from message text or provider state. Do not use it as an unbounded archive of raw messages or credentials. Do not place a private operator's routing policy or identity in this public package; pass normalized observations and explicit dispositions as inputs. Status rules Items use open , waiting , followed_up , resolved , or dismissed . Provider observations never infer completion. A human disposition records actor, reason, time, the latest external occurrence it covers, and optional HTTPS evidence. Replaying old search history preserves the human status. An external message newer than the covered occurrence reopens the item to open , including unseen work that arrived before the disposition was saved. Scan coverage is explicit: running , complete , truncated , or failed . Direct mentions are actionable structural evidence. Author and keyword scans remain observation-only unless the operator explicitly marks the query actionable. The skill does not contain provider-specific keyword heuristics. The provider-neutral thread locator is the item key. Stored previews are bounded; credentials, tokens, and full provider response envelopes are forbidden. Procedure Read the latest checkpoint for the bounded query digest. Resume its provider cursor when the prior scan was interrupted or truncated. Fetch one bounded provider page through the caller's authorized connector. Record actionable messages against their per-thread streams and append the scan page with its next cursor. On a version conflict, reload only the affected scan or action stream and retry the idempotent transition. List queue state through bounded action-head pages and use record_disposition only for an explicit operator correction. The loop is outside the kernel. Each page or disposition remains one governed, receipt-backed Runx turn. Edge cases and stop conditions needs_input : missing query identity, observation, disposition, actor, reason, or scan coverage. conflict : the projection version is stale; reload before retrying. provider_unavailable : the caller cannot prove provider read coverage. too_broad : a page exceeds the bounded message count or contains unnormalized provider data. refused : a caller asks this skill to send, reply, broaden a grant, store a token, or silently claim complete coverage. Output schema Write runners emit runx.effect.transition.v1 , containing the effect family, operation, expected projection version, idempotency key, and one normalized event. Read and list runners return the corresponding bounded data-store event result; they never synthesize provider coverage or completion. Worked example Given a normalized direct mention from a teammate in one provider thread, record_action_observation derives the stable action id from the provider-neutral thread locator and appends an open action snapshot. If the operator later records resolved with a reason, replaying that mention preserves resolved ; a newer external reply in the same thread appends a reopened open snapshot. Inputs All runners require data_source_ref . Write runners also take the target id, expected_version , and observed_at , plus exactly the normalized payload for their operation: scan and messages , message and triage , disposition , or an imported action . Reads take action_id or query_digest ; list runners take bounded limit and optional cursor or filter fields. + +### ops-desk + +Path: `/runxhq/runx/pages/ops-desk.html` + +Operate a project, workspace, or account from an agent-controlled desk. + +Group: Operate Source: skills/ops-desk/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/ops-desk/SKILL.md Ops Desk Operate a project, workspace, or account from an agent-controlled desk. This skill is the generic operations desk layer. It turns a state snapshot, an operator objective, and receipt-backed evidence into one safe ops desk packet: what is happening, what needs attention, what can be checked read-only, what requires approval, which governed lane should execute, and how success will be verified. It is not the authority and it is not a second CLI. It does not replace release , send-as , ledger , refund , spend , messageboard , provider-specific adapter skills, hosted API routes, repository workflows, or deploy commands. It routes to the existing interface with the smallest sufficient context and stops before any consequential act that lacks the right gate. What this skill does ops-desk produces an ops desk packet for a manager dashboard, agent session, or self-operation run. It reads projected state, classifies findings, ranks the next action, selects the governed lane, names blockers, writes the approval prompt when a human decision is required, and states the receipt/effect/readback that will prove success. It is useful before an action and after an action: before action, it turns state into proposals and approval requests; after action, it checks whether the expected receipt and projection appeared. The model may diagnose and write the operator rationale. The mutation itself must be a deterministic handoff to an existing skill runner, CLI command, hosted API route, workflow, or provider tool. When the desk should start from durable state, use operate_from_projection . That runner reads a projection through data-store first, then passes the projection as the dashboard snapshot. The storage provider is still selected by the logical data_source_ref ; ops desk does not know whether state came from SQLite, Postgres, D1, Redis, or a product API. When a standing case must be advanced one move at a time toward a mandate, use advance . It takes the mandate, the current case_state , and a fixed candidate_roster , and returns a single typed dispatch_decision : dispatch one roster member, escalate, or done. It applies the same ranking and the same gates as operate , but it is hard-constrained to the roster and emits one move instead of a multi-proposal plan. The caller (an agency loop) holds the case and the goal; ops desk supplies the judgment. The chosen member is named as data; ops desk never runs it. When to use this skill An operator asks an agent to manage a project, workspace, product, account, or other bounded operating surface. A dashboard needs an agent-readable plan from the current projected state. A runbook needs to decide between read-only checks, proposals, approval-gated actions, and post-action verification. A product-specific operator skill needs a generic cockpit spine instead of inventing its own action model. A standing case (an agency) needs the single next governed move chosen from a fixed roster, one turn at a time. Runx needs to dogfood its own release, registry, hosted, receipt, or provider operations through the same governed lanes it exposes to users. When not to use this skill To execute a live mutation directly. Route to the named governed lane. To duplicate a CLI command, release script, GitHub workflow, hosted endpoint, registry client, or provider SDK. To bypass a human gate because the agent or UI believes the action is obvious. To replace a domain skill such as send-as , messageboard , release , ledger , refund , spend , least-privilege , or a provider adapter. To operate from stale, missing, or unverifiable state while claiming readiness. To put secrets, private keys, raw customer lists, or provider dumps into the ops desk packet. Operating Model Use one loop: Copy snapshot -> findings -> proposals -> approval -> governed lane -> receipt -> projection The manager dashboard and the agent must read the same state and emit the same action families. A button click and an agent plan are different interfaces over the same governed lane, not separate backdoors. Delegation Model Ops desk packets name existing execution surfaces; they do not implement them. release owns release preparation, approval, publish handoff, and post-release verification. ledger , audit-receipt , and run-history own proof questions. send-as owns authority for live communications; provider adapter skills own provider-specific execution details. spend , charge , refund , and branded payment skills own money movement. Project skills own product vocabulary and product-specific actions. CLI commands, hosted API routes, and GitHub workflows remain deterministic execution interfaces. The operator skill may cite them as handoff targets but must not clone their behavior in prose. If no existing lane can perform the action cleanly, return needs_input or a product gap. Do not invent a private workaround. Procedure Scope the objective. Identify the workspace, project, account, surface, time window, and whether the ask is read-only, proposal-only, or execution-prep. Read project_profile or operator_policy as context, not authority. If the operating scope or objective is ambiguous, return needs_input . Classify state from evidence. Use dashboard_snapshot , receipt_summary , effect_summary , and provider_status when present. Treat missing evidence as missing. Do not infer success from UI state alone. Separate health, money, communications, provider mutations, access, deployment, and incident signals. For review, catalog, publication, bounty, or marketplace work, classify whether the artifact is real, useful, complete, and valuable. A reachable artifact with no credible user, maintainer, operator, public proof, or marketing value is not ready. If using operate_from_projection , treat the read projection as the dashboard snapshot. An empty projection is not an error, but it should usually produce needs_input rather than fake readiness. Route to governed lanes. Release questions route to release plus the project release profile and existing release workflow/commands. Audit questions route to ledger , audit-receipt , run-history , or least-privilege . Live communication routes through send-as and then a provider adapter. Payment collection, payout, refund, chargeback, or target changes route to the matching payment lane. Board, thread, and provider actions route to messageboard , a provider adapter, issue-intake , issue-to-pr , or the product's own skill. Deploy and config changes route to the product-owned deploy lane. Decide gates. Read-only checks: no human approval. Drafts, dry-runs, previews, and reports: no live-action approval unless they expose private data or broaden authority. Live sends, payouts, refunds, customer-visible posts, provider mutations, target changes, credential changes, deploys, destructive actions, and broad audience decisions: explicit approval required. A review verdict, recommendation, or green dry-run is not payment approval. Money movement needs a separate approval prompt naming the amount, recipient, rail, target class, and verification receipt expected after settlement. Missing approval means awaiting_approval , not "ready". Produce the ops desk packet. Lead with the few issues an operator should act on now. Name the exact lane for each proposed action. Include the existing execution interface as a handoff, not as a duplicated implementation. Include approval copy only when the operator could approve it safely. Include verification steps that will prove the action happened. Stop cleanly. Return needs_input for missing scope, objective, identity, authority, evidence, approval, or target. Return refused for requests to bypass gates, hide material facts, leak secrets, spoof receipts, mark unsettled money as settled, or send without a principal/audience/content digest. Edge cases and stop conditions No project/workspace/account or objective: return needs_input ; there is no safe operating frame. No projection or receipt evidence: return needs_input or unknown status; do not convert silence into ok . Requested action has unknown consequence: stop at needs_input with the missing lane/consequence classification. Money, public send, deploy, credential, target, destructive, or provider mutation without approval: return awaiting_approval . Approval text is too broad to approve safely: return needs_input with the exact missing amount, audience, target, network, provider, or effect. User asks to skip a gate, hide a blocker, forge a receipt, or mark state settled without proof: return refused . Reference Loading Load only the reference needed for the objective: Payments, payouts, refunds, payment rail adapters, reconciliation: references/payments.md Email, campaigns, notifications, customer/public communication: references/communications.md Receipt verification, ledger, trust roots, after-action proof: references/receipts.md Provider health, deploys, webhooks, credentials, outages: references/providers.md Manager dashboard state, projections, and action catalog design: references/dashboard.md Delegation, project profiles, CLI/workflow handoff, and dogfooding rules: references/delegation.md Output schema Return one ops_desk_packet : Copy ops_desk_packet : decision : ready | awaiting_approval | needs_input | no_action | refused scope_ref : string objective : string mode : read_only | proposal | execution_prep | post_action_review dashboard : health : ok | degraded | blocked | unknown money : ok | needs_attention | blocked | unknown communications : ok | needs_attention | blocked | unknown providers : ok | needs_attention | blocked | unknown receipts : ok | needs_attention | blocked | unknown findings : - severity : info | warning | critical area : health | money | communications | providers | receipts | access | deploy summary : string evidence_refs : [ string ] proposals : - action_id : string lane : string reason : string inputs_summary : object consequence : read_only | draft | live_mutation | money_movement | public_send | deploy approval_required : boolean approval_prompt : string | null blockers : [ string ] verification : expected_receipt : string expected_effect : string | null readback : string execution : interface : skill | cli | hosted_api | workflow | provider_tool | manual lane_ref : string profile_ref : string | null command_ref : string | null workflow_ref : string | null approval_gate : string | null verifier_ref : string | null ordered_next_steps : - step : string lane : string requires_confirmation : boolean refused_reasons : [ string ] needs_input : [ string ] success_checkpoint : milestone : string description : string The advance runner returns one dispatch_decision : Copy dispatch_decision : decision : dispatch | escalate | done reason : string dispatch : # present when decision == dispatch member : string # a role from candidate_roster skill : string # that role's roster skill, echoed task : string # what the member should do needed_scope : [ string ] # subset of the member's scope ceiling consequence : read_only | draft | live_mutation | money_movement | public_send | deploy verification : expected_receipt : string readback : string escalation : # present when decision == escalate to : string # a roster role or "human" trigger : string ask : string approval_prompt : string | null resolution : # present when decision == done reason : string Decision rules Prefer one clear next action over a dashboard dump. Never bury a required approval in prose; put it in approval_prompt . Never expose tokens, API keys, raw customer lists, private wallet keys, or provider response dumps. Never claim a state is settled, sent, deployed, paid, or refunded without a receipt/effect/readback reference. Never route a public artifact, skill, bounty result, or docs deployment as ready when it lacks a credible real-world audience or durable public evidence. Never widen authority because a dashboard widget would be convenient. Never duplicate an existing CLI command, workflow, hosted endpoint, or domain skill in operator prose. Route to it. Keep product-specific policy in product context. Keep this skill generic. Inputs objective (required): operator request, e.g. "check payments and unblock funding", "prepare a campaign send", or "review stuck receipts". scope_ref (required): the project, workspace, account, product, or bounded surface being operated. dashboard_snapshot (optional): JSON summary of current projected state. receipt_summary (optional): JSON or prose receipt/effect summary. provider_status (optional): JSON or prose provider health/account state. approval_context (optional): existing operator approvals, denials, or policy gates. operator_policy (optional): project-specific constraints and lane names. project_profile (optional): project topology, existing interfaces, and verification expectations. It is context, not authority. requested_action (optional): preselected action lane or dashboard action id. Worked example Input: "Check payment readiness and tell me what to do next" with a dashboard snapshot showing healthy quote/readback state, three funded items, no unfunded approved items, and one rail adapter webhook status needs_review . Output: decision: ready , money status ok , providers status needs_attention , one warning finding for rail webhook readiness, and one proposal routing to provider.webhook_check with no money movement. It does not propose marking anything funded, because no unfunded approved item is present and the latest funding receipt is already verified. + +### work-plan + +Path: `/runxhq/runx/pages/work-plan.html` + +Turn a build or automation objective into a bounded governed work plan. + +Group: Operate Source: skills/work-plan/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/work-plan/SKILL.md Work Plan Turn a build or automation objective into a bounded governed work plan. For cross-repo or cross-surface work, the output must be a phased workspace_change_plan , not just a loose list of steps. The shared plan is the thing that keeps repo-local workers aligned when one issue fans out into multiple mutation surfaces. When the objective originates from an existing thread, treat that thread as provider-backed thread. GitHub issues, chat threads, support tickets, and local agent sessions are adapter examples, not core nouns. The plan should preserve the generic thread_locator and any supplied thread . The central insight: split at governance boundaries, not cognitive boundaries. A skill keeps its full context window. If two actions need the same context but different scopes, they are two invocations of the same skill with different scopes — not two separate skills. The graph defines where authority changes, where mutation happens, and where a gate needs to approve. That is where steps break. Work backward from the deliverable. Name the concrete artifact the objective produces (spec, patch, PR, docs site, report). Then identify where authority narrows: read-only analysis, write-access mutation, approval gates, review boundaries. Each narrowing is a step boundary. Each step gets only the scopes it needs — no step inherits from a prior step, each derives from the graph grant independently. Determine data dependencies between steps. A step that consumes output from a prior step must come after it. Steps with no data dependency are candidates for fanout. Do not parallelize steps that share mutation targets. If the objective is ambiguous or required context is missing, surface open questions explicitly rather than guessing. Open questions should name what is missing, why it matters, and who can answer it. Prefer fewer steps with clear scope boundaries. Three well-scoped steps beat seven single-purpose fragments. Every step should have a clear entry condition, action, and exit artifact. Output change_set : the parent change artifact inherited from intake or constructed for the objective when intake did not already produce one. It should preserve the shared objective, target surfaces, invariants, and success criteria. harness_context : when supplied, the same runx.receipt.v1 packet with state advanced to planning_ready or blocked . Preserve source events, dedupe, and triage fields rather than reconstructing them from prose. objective_summary : one sentence capturing the deliverable. workspace_change_plan : phased plan for the whole change set. It must contain: plan_id change_set_id objective_summary shared_invariants success_criteria phases : ordered array. Each phase: id name depends_on : prior phase ids parallelizable : boolean repo_change_requests : ordered array. Each request: repo task_id objective depends_on : sibling repo change request ids this request waits on shared_context_refs : references into the parent change set or prior phase outputs validation_commands mutating integration_checks : cross-repo checks that must pass before the overall change set is considered done open_questions orchestration_steps : canonical execution view of the plan as an ordered array. Each step: id : kebab-case identifier skill : skill name or path scopes : scope strings this step requires mutating : boolean inputs : static input map context_from : step_id.output_field data dependency references description : what this step does and produces required_skills : skill names needed. Flag which exist vs need creation. open_questions : missing context that must be answered before mutation. Inputs objective (required): the build or skill objective to decompose. project_context (optional): repo, product, or user context that constrains the decomposition. change_set (optional): parent change artifact from issue-intake or a workspace supervisor. Prefer this when present. harness_context (optional): portable issue control-plane packet from intake. Preserve it as state, not as a prose handoff. thread_locator (optional): canonical locator for the bounded thread the plan is serving. thread (optional): portable thread when the objective is grounded in an existing issue, chat, ticket, or other adapter surface. + +### deep-research + +Path: `/runxhq/runx/pages/deep-research.html` + +This graph turns one important question into a decision-ready brief. + +Group: Research and data Source: skills/deep-research/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/deep-research/SKILL.md Deep Research Brief This graph turns one important question into a decision-ready brief. It is for research that needs more than a quick answer but less than an open- ended report. The output should feel like an operator memo: what the answer is, what evidence supports it, what remains uncertain, and what posture the reader should take next. Do not drift into a generic article, daily update, or trend recap. The point is to help a human decide, not to narrate that research happened. Separate verified evidence from inference and carry unresolved questions into the memo. The synthesis must say what the reader should monitor, do, defer, or investigate next. Return needs_more_evidence when the packet cannot support a recommendation, and not_worth_publishing when the answer is sound but does not matter to the stated decision. Output research_packet : bounded evidence, confidence, inference, and open questions. brief_draft : the decision memo synthesized from that packet. approval_decision : review of the exact brief and its remaining uncertainty. publish_packet : approved brief and delivery metadata. Inputs objective (optional): specific question the brief should answer. audience (optional): primary reader for the memo. channel (optional): final delivery channel; defaults to brief . domain (optional): product, ecosystem, or market slice to bound the work. operator_context (optional): local decision context or evaluation lens. target_entities (optional): structured list of products, projects, companies, or repos to keep in scope. + +### research + +Path: `/runxhq/runx/pages/research.html` + +Research one bounded question and turn it into a decision-ready packet. + +Group: Research and data Source: skills/research/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/research/SKILL.md Research Research one bounded question and turn it into a decision-ready packet. This skill is for applied research, not open-ended browsing. It should answer one practical question with evidence, tradeoffs, and explicit uncertainty: which issue is worth tackling, what the ecosystem is doing, whether a proposal is grounded, or what claims a public post can safely make. Keep the scope tight. Summaries without evidence are not enough, but an undirected literature review is also wrong. Prefer a small number of verified claims that change the operator's decision. Operating rules State the objective in operational terms. Distinguish verified evidence from inference. Give every important claim a source and confidence. Surface missing evidence instead of inventing it. Bound the result to a concrete deliverable: brief, issue recommendation, content outline, or publish/no-publish decision. State what the finding changes: what to write, build, avoid, defer, or review. Return needs_more_evidence rather than forcing a speculative conclusion, and not_worth_publishing when a true finding is irrelevant to the audience. Output research_brief : object with objective , scope , summary , and open_questions . evidence_log : array of evidence entries with claim , source , confidence , and relevance . decision_support : array of options or recommendations with rationale. risks : array of research or execution risks. Inputs objective (required): the question to answer. domain (optional): ecosystem, product area, or audience context. deliverable (optional): intended artifact, for example daily brief , triage recommendation , or publish packet . operator_context (optional): local constraints or strategic context. target_entities (optional): array or object naming repos, products, competitors, communities, or issues that bound the research. + +### data-store + +Path: `/runxhq/runx/pages/data-store.html` + +Operate a data source through a governed adapter contract. This skill gives an agent enough context to read, append, or project state without learning provider secrets, inventing SQL, or depending on one storage backend. + +Group: Research and data Source: skills/data-store/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/data-store/SKILL.md Data Store Operate a data source through a governed adapter contract. This skill gives an agent enough context to read, append, or project state without learning provider secrets, inventing SQL, or depending on one storage backend. The storage backend can be Postgres, SQLite, D1, Redis, DynamoDB, S3, a ledger, or a product API. The runx boundary is the same: a declared data source exposes typed operations; the graph supplies bounded params; the adapter executes the operation; the receipt records the resource, authority, idempotency, version, digest, and redaction evidence. Adapter selection The operator chooses a data source at run time. The skill receives data_source_ref and operation inputs; project or hosted configuration binds that ref to the concrete adapter. A local development ref might be local://runx-data-store/dev-board . A production ref might be tenant://acme/board bound to data.postgres , data.d1 , data.redis , or a product-owned HTTP adapter. Do not put provider logic in the domain skill. Messageboard, CRM, support, and business-ops skills should ask for durable facts to be read or written; the data source binding decides whether those facts live in local JSON, SQL, Redis, D1, object storage, or a product API. Switching providers is a binding change, not a rewrite of the skill. The bundled OSS profile calls data.source . Unbound local://... refs default to durable local SQLite under .runx/data/local-sources/ , with one source-scoped database file per logical ref, so stateful skills can be dogfooded without standing up hosted infrastructure. Pass store_id only when a fixture intentionally wants the deterministic data.local JSON store. The graph inputs stay the same when a project later binds the source to Postgres, Redis, D1, object storage, or a product API. Adapter preference is operator configuration, not model choice. To choose Redis, SQLite, or a hosted provider, bind the same data_source_ref through RUNX_DATA_SOURCES or .runx/data-sources.json ; do not add provider branches to the domain skill. What this skill does Reads data through named queries or read operations declared by a data-source adapter. Appends state transitions with idempotency keys and expected versions. Reads projections, event streams, or bounded latest-stream-head pages so loops can resume from explicit state without exporting full history. Produces receipt-bound evidence for data source, resource, operation, params, row/event limits, versions, and output digests. Keeps product semantics outside the data layer. Messageboards, CRMs, billing ledgers, and support desks define their own events and reducers. Ships a fixture adapter ( data.local ), durable local SQLite adapter ( data.sqlite ), and Redis adapter ( data.redis ) behind the same operation envelope. When to use this skill A graph needs durable state between turns, such as queue position, board state, sync cursor, review status, or approval inbox state. A skill must query a bounded slice of product data before deciding the next action. A workflow needs to append an auditable event or effect transition with optimistic concurrency. An operator wants one provider-agnostic shape that can later move from local JSON or SQLite to Postgres, Redis, D1, Supabase, Turso, DynamoDB, or another store. When not to use this skill To let a model write arbitrary SQL, Redis commands, or database migrations. To export broad data sets, secrets, raw PII, or unrestricted tables. To hide product decisions in storage code. Domain skills still own state machines, acceptance criteria, and business rules. To treat a projection as independent truth when the event stream or receipt chain is available and required for review. To bypass payment, send, deploy, moderation, or human approval gates. Procedure Identify the domain skill and transition first. The data store is a carrier, not the policy owner. Select the logical data source. Use data_source_ref to name the project or tenant source; let the project binding choose the adapter. Do not put raw database URLs, provider credentials, or SQL in the skill input. Select a declared operation: named read query, append event, read events, read projection, or list stream heads. Do not synthesize raw provider commands. Check authority. Reads need the narrow resource/query scope; writes need the transition scope, idempotency key, and expected version unless the operation is explicitly append-only without concurrency. Bind typed params. Enforce row/event limits, tenant/partition keys, and redaction rules before the adapter runs. For writes, use optimistic concurrency and idempotency. A retry with the same idempotency key and same payload returns the existing effect; a different payload under the same key is a conflict. Return the operation result with resource refs, version movement, digests, redaction notes, and stop conditions. Receipts should link this data effect to the domain transition that caused it. Edge cases and stop conditions needs_source : the data source, resource, query name, tenant key, or schema summary is missing. needs_input : required operation params are incomplete, malformed, or not specific enough to bind a declared data-source operation. needs_authority : the caller lacks the declared read/write scope or provider grant. needs_version : a mutating operation lacks expected_version where the data source requires optimistic concurrency. conflict : the current version differs from expected_version , or an idempotency key is reused with different content. too_broad : the requested read lacks partition filters, exceeds limits, or asks for raw export. redaction_required : the operation would return secrets, private PII, or fields outside the declared projection. provider_unavailable : the adapter cannot reach the data source, times out, or cannot prove whether a write committed. Output schema All runners return runx.data.operation_result.v1 : Copy { "schema" : "runx.data.operation_result.v1" , "data_source_ref" : "local://example" , "provider" : "local-json-event-store" , "operation" : "append_event" , "resource" : "board_events" , "aggregate_id" : "posting-123" , "status" : "committed" , "before_version" : 0 , "after_version" : 1 , "idempotency_key" : "posting-123:create" , "event_ref" : "board_events:posting-123:1" , "result_digest" : "sha256:..." , "projection_digest" : "sha256:..." , "rows" : [], "events" : [], "redactions" : [], "stop_conditions" : [] } Provider adapters may add provider evidence under provider_evidence , but they must not expose credentials or raw secret material. For event streams, adapters derive a readable event_type in this order: explicit event.type , explicit event.event_type , then event.effect_family + "." + event.operation . Domain skills that emit the generic runx.effect.transition.v1 packet should include effect_family and operation on every event so readback projections say messageboard.accept , business_ops.route , or another meaningful transition instead of data.event . Worked example A messageboard skill decides that posting.claimed is allowed. It emits a domain transition packet. The graph then calls data-store.append_event with resource board_events , aggregate id posting-123 , expected version 2 , and idempotency key posting-123:claim:agent-9 . The data adapter appends the event only if the stream is still at version 2 . The receipt proves the decision, the data operation, and the new version. A later loop turn calls data-store.read_events or read_projection to resume from the explicit board state. Inputs data_source_ref (required): stable logical ref for the data source. The project or hosted binding maps this ref to the concrete adapter and provider profile. resource (required): declared resource, stream, table, keyspace, or projection name. operation (required for tool-level use): append_event , read_events , read_projection , or list_stream_heads . aggregate_id (required for event operations): stream or partition key. event (required for append_event ): domain event or transition packet. idempotency_key (required for writes): stable retry key. expected_version (required when the source enforces concurrency): current stream/resource version expected by the caller. limit (optional): maximum rows or events to return. after_version (optional for read_events ): return an ascending page whose event versions are strictly greater than this value. Omit it to retain the existing latest-tail read. Compare the last returned event version with after_version in the result envelope to know whether another page remains. event_types (optional for list_stream_heads ): at most 20 exact latest event types. No pattern or arbitrary field queries are accepted. cursor (optional for list_stream_heads ): opaque cursor returned by the previous page. Limits are capped at 100. store_id (local fixture adapter only): deterministic local store id that opts into the bundled data.local proof adapter. Omit it for durable local SQLite. Production adapters should ignore it. Invocation examples Durable local dogfood with the bundled default: Copy runx skill data-store append_event \ -i data_source_ref=local://runx-data-store/dev-board \ -i resource=board_events \ -i aggregate_id=posting-123 \ --input-json expected_version= 0 \ -i idempotency_key=posting-123:create:v1 \ --input-json event='{"type":"posting.created","payload":{"title":"verify a receipt link"}}' \ --json Fixture-only dogfood can still use store_id to select the JSON fixture store: Copy runx skill data-store append_event \ -i data_source_ref=local://runx-data-store/dev-board \ -i store_id=dev-board \ -i resource=board_events \ -i aggregate_id=posting-123 \ --input-json expected_version= 0 \ -i idempotency_key=posting-123:create:v1 \ --input-json event='{"type":"posting.created","payload":{"title":"fixture proof"}}' \ --json Production graph shape is the same at the skill boundary: Copy runx skill data-store append_event \ -i data_source_ref=tenant://acme/board \ -i resource=board_events \ -i aggregate_id=posting-123 \ --input-json expected_version= 2 \ -i idempotency_key=posting-123:claim:agent-9 \ --input-json event='{"type":"posting.claimed","payload":{"actor":"agent-9"}}' \ --json The second command only works once tenant://acme/board is bound to an installed provider adapter. That binding is operator configuration and may name a credential profile or hosted grant; it must not carry raw secrets. Project-specific SQLite uses the same command shape after binding the source: Copy { "data_sources" : { "tenant://acme/board" : { "adapter" : "data.sqlite" , "database_path" : ".runx/data/acme-board.sqlite" , "resources" : { "board_events" : { "kind" : "event_stream" , "partition_key" : "aggregate_id" } } } } } Pass that document through RUNX_DATA_SOURCES or .runx/data-sources.json . Redis uses the same skill and graph inputs. Only the binding changes: Copy { "data_sources" : { "tenant://acme/board" : { "adapter" : "data.redis" , "endpoint" : "redis://127.0.0.1:6379/0" , "key_prefix" : "runx:{acme-board}" , "resources" : { "board_events" : { "kind" : "event_stream" , "partition_key" : "aggregate_id" } } } } } The Redis endpoint must not embed credentials. Use local unauthenticated Redis for OSS dogfood, or put production secrets behind a runx credential profile or hosted grant. For Redis Cluster, the binding's key_prefix must contain one safe hash tag, such as {acme-board} , so the stream, idempotency, and head keys touched by an append share one slot and update atomically. Stream-head pages use stable keyset cursors rather than mutable offsets. Durable events and dispositions must not receive TTLs; production Redis should enable persistence and use a non-evicting policy. + +### knowledge-router + +Path: `/runxhq/runx/pages/knowledge-router.html` + +Route one question, source event, or support thread to the right knowledge sources and follow-up path. + +Group: Research and data Source: skills/knowledge-router/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/knowledge-router/SKILL.md Knowledge Router Route one question, source event, or support thread to the right knowledge sources and follow-up path. This skill is for triage and routing, not answering the question directly. It should tell a consuming graph where to look, who owns the domain, what evidence is already available, and which next skill should run. Each route must name the supplied signal that justified its source match, owner, escalation, and next-skill recommendation. Keep the result as a concise dispatch note. Return needs_more_context when no route is supportable, and manual_review for legal, billing, security, or destructive requests. Output route : selected knowledge or ownership domain and rationale. source_matches : relevant sources with the matching signal. owner_recommendation : owner or escalation target. next_skill : the bounded follow-up capability, if one is justified. Inputs question (required): user question, event, or thread summary to route. available_sources (required): source catalog, docs, systems, or owner map. constraints (optional): allowed systems, sensitivity, or preferred owner. + +### web-fetch + +Path: `/runxhq/runx/pages/web-fetch.html` + +Fetch one URL, prove it was allowed, extract the part the caller asked for, and return that slice by digest with the provenance needed to trust it later. + +Group: Research and data Source: skills/web-fetch/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/web-fetch/SKILL.md Web Fetch Fetch one URL, prove it was allowed, extract the part the caller asked for, and return that slice by digest with the provenance needed to trust it later. What this skill does web-fetch resolves a single URL against a host allowlist, retrieves it, extracts text, metadata, or links, and seals the result so a downstream step can cite the fetch without re-fetching. It checks the final host against the allowlist before and after redirects, retrieves up to max_bytes , and returns the final URL, the HTTP status, a content_digest over the retrieved body, the extracted slice, and a provenance block recording when it ran, every redirect hop, and how many bytes it read. The body is referenced by digest; only the extracted slice is inlined. This is the primitive an agent reaches for when it has already decided which page to read. The decision it makes easier is "can I read this page, and what did it actually say", with the answer backed by a digest instead of a remembered paraphrase. It differs from the research family: research and deep-research decide which sources matter and synthesize across them, while web-fetch retrieves exactly one source and refuses anything off the allowlist. When to use this skill An agent has chosen a specific page and needs its content bound to a content_digest so a later step can cite it without re-fetching. A research pass needs each source retrieved through a single bounded net:allowlist fetch with a complete redirect chain and byte count. A review must later prove what a page said at fetch time. A follow-on skill (prior-art, vuln-triage, brief) needs one source extracted as text , metadata , or links . When not to use this skill To judge, rank, or synthesize across sources. That is the research family's job; web-fetch retrieves exactly one source and refuses to reason over many. To reach a host the caller did not declare in allowlist , including a host reached only through a redirect. To write anything. The only scope is net:allowlist ; there is no repo, file, wallet, or send authority here. To inline a large raw body. The extracted slice is the payload; the full body lives behind content_digest . To carry secrets. Request headers may reference a credential by ${secret} handle, but no header value, cookie, token, or auth string appears in the output or the receipt. Procedure Require url and allowlist . Either missing returns needs_agent ; the fetch cannot run without a target and a declared scope. Match the URL host against allowlist . On a miss, return policy_denied before any network call, recording the attempted host and the allowlist it was checked against. Fetch, following redirects, re-checking each redirect target's host against the same allowlist . A redirect that lands off-allowlist halts the fetch, returns policy_denied with the hop that failed, and discards partial bodies. Cap the read at max_bytes when set. Compute content_digest over the retrieved body. Extract per extract : text (readable body text, default), metadata (title, description, canonical, declared language, content type), or links (absolute hrefs found in the document). Return fetch_result with the final URL, status, digest, extracted slice, and provenance. Flag truncated reads in provenance; never return a clipped read as if whole. Edge cases and stop conditions Missing url or allowlist : return needs_agent ; the fetch has no target or no scope to check against. Host off the allowlist: stop with policy_denied before any network call; record the attempted host, not a response body (there is none). Redirect off the allowlist: halt the fetch, return policy_denied naming the hop that failed, and discard the partial body. Read clipped by max_bytes : flag truncated: true in provenance; the digest is over the bytes actually retrieved. Large raw body: never inline beyond the extracted slice; anything bigger than the requested view is reachable only through content_digest . Credential in a header: reference it by ${secret} handle only; no header value, cookie, or token reaches the output or the receipt. Output schema Copy fetch_result : decision : ready | needs_agent | policy_denied final_url : string # URL after redirects, the one the digest is over status : number # HTTP status of the final response content_digest : string # digest of the retrieved body, algorithm prefix included extract_mode : text | metadata | links extracted : string | object | array # string for text, object for metadata, array of hrefs for links provenance : fetched_at : string # timestamp of the fetch redirects : array # ordered host hops, each re-checked against the allowlist bytes : number # bytes read truncated : boolean # true when max_bytes clipped the read policy : allowlist_decision : allowed | denied attempted_host : string # set on policy_denied allowlist_checked : array # the hosts the request was checked against The sealed runx.receipt.v1 carries the final URL, status, content_digest , byte count, the redirect chain, and the allowlist decision. It carries no header values, no cookies, and no raw body beyond the digest. Worked example Input: url of the HTTP Semantics RFC, an allowlist of www.rfc-editor.org and rfc-editor.org , extract: text , and max_bytes: 200000 . Output: decision: ready ; the host matched the allowlist before the request left; no redirects; status 200 ; content_digest is taken over the retrieved body; extracted holds the readable text slice; provenance records fetched_at , an empty redirect chain, 184302 bytes, and truncated: false . The receipt seals with the final URL, status, digest, byte count, and the allowlist decision; no header value reaches it. Inputs url (required): the single URL to fetch; its host must match the allowlist. allowlist (required): permitted hosts or host patterns; the URL and every redirect target must match an entry. extract (optional): text , metadata , or links . Defaults to text . max_bytes (optional): cap on bytes read; a clipped read is flagged truncated in provenance. + +### github-sync + +Path: `/runxhq/runx/pages/github-sync.html` + +Decide exactly what state to move between a GitHub repo and the local graph, in which direction, and whether the agent is even allowed to write. + +Group: GitHub and delivery Source: skills/github-sync/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/github-sync/SKILL.md GitHub Sync Decide exactly what state to move between a GitHub repo and the local graph, in which direction, and whether the agent is even allowed to write. github-sync is the generic repo state connector. It turns a loose request like "sync the open issues" into a bounded plan that names the resources, the direction, the scope, the records it will touch, and the point where the run must stop for a human. A pull is observation and stays inside repo:read . A push is mutation and never proceeds without an explicit repo:write grant and human approval. What this skill does github-sync produces a sealed sync_plan : a scoped record of which GitHub resources the run will pull or push, the scope it will use, the gates a write must clear, and any blockers that stop the run cleanly. For a push it carries a diff_summary described by digest and ref, never by raw body text, so a reviewer can approve the shape of a change without leaking issue contents, tokens, or PII into the plan or the receipt. The plan binds direction to scope. pull is read-only and lists the resources it will fetch. push enumerates the mutations by ref and digest, marks approval_required: true , and refuses to proceed past planning when the run lacks a repo:write grant. This skill plans the sync; it does not perform the GitHub mutation itself. The plan is the artifact a downstream adapter executes after the approval gate clears. Planning and mutation stay on opposite sides of the gate so a review can read intent before anything changes on the remote. When a sync loop needs a durable cursor, use plan_and_append_cursor . That runner reads the cursor projection through data-store , plans the bounded sync, appends the plan as a cursor event, and reads back the projection. The storage provider is selected by data_source_ref , not by GitHub-specific code. When to use this skill An agent needs to fetch a bounded set of issues, threads, or PRs into the local graph for triage or analysis. An agent needs to mirror local state back to GitHub (reopen, label, comment, close) and the operator wants the write shape reviewed before it lands. A workflow must prove which repo, direction, and scope a sync used, with a receipt that names the resources touched. A review needs to distinguish a read-only pull from a write that crossed an approval gate. When not to use this skill github-sync is the generic repo state connector. Reach for it when the job is moving issue, thread, or PR state in or out, not authoring a change or composing one comment. To drive a thread through spec, build, review, and a draft PR. Use issue-to-pr , which governs the full issue-to-PR lane. To draft one review comment on one PR. Use pr-review-note . To push without a named repo and direction. To carry raw issue bodies, comment text, access tokens, or contributor PII in the plan or receipt. Reference them by digest, span, or ref only. To bypass the human approval gate on any write. Procedure Resolve the target repo and confirm the run holds at least repo:read . Read direction . pull is observation; push is mutation and changes the gate posture. Read resources . Bind the concrete set: issues, PRs, or threads, plus the filters that bound it (state, label, author, range). An unbounded "all" becomes a blocker until reconfirmed. Read scope . A push requires scope: write backed by a real repo:write grant. If a write is requested without that grant, stop and refuse rather than downgrade to a silent pull. For a pull , list resources_touched by ref and leave diff_summary empty. For a push , build diff_summary as a list of intended mutations described by ref and content digest, set gates.approval_required: true , and record the approval reference once granted. Record scope_used as the narrowest scope the plan actually needs. Emit the smallest sync_plan an adapter can execute without widening authority, and stop at the approval gate for any write. For cursor-backed loops, read the cursor projection first, append one sync plan event with an idempotency key and expected version, and read back the projection before the next turn. Edge cases and stop conditions Missing repo or direction: return needs_agent ; the sync target is undefined. Write requested without repo:write : the request is refused ; never downgrade it to a silent pull. The plan stays unexecutable. Unbounded resource set: mark a blocker and require an explicit filter before a push. Approval absent or denied on a push: keep the decision blocked and the plan unexecutable; do not emit an executable mutation plan. Raw bodies, tokens, or PII in the resource payload: reference by digest and ref; if redaction would remove the evidence needed to plan, return needs_agent . Output schema Copy sync_plan : decision : ready | blocked | refused | needs_agent repo : string # resolved owner/name target direction : pull | push resources_touched : # resources by ref; no raw bodies - kind : issue | pr | thread ref : string selected_by : string # the filter that selected it diff_summary : # push only; empty for a pull - ref : string op : string digest : string scope_used : string # narrowest scope, e.g. repo:read or repo:write gates : approval_required : boolean # true for any push approval_ref : string # set once the write is approved blockers : array # conditions that must clear before execution sync_plan is a composable object. Downstream skills read it as arbitrary JSON; the fields above are the contract a reviewer and adapter rely on. The receipt ( runx.receipt.v1 ) carries the repo, direction, scope_used , the resource refs touched, and the approval reference for a write. It carries no issue bodies, comment text, tokens, or contributor PII; mutations appear as refs and digests only. Default scope is repo:read and a pull never escalates; a push needs an explicit repo:write grant plus human approval, so missing the grant is a refusal and missing the approval keeps the plan blocked. Worked example Input: "Sync the open triage issues into the graph" on runxhq/runx , with direction: pull , scope: read , and a filter of state:open label:triage . Output: decision: ready ; direction: pull ; scope_used: repo:read ; resources_touched lists the two matched issues by ref and the filter that selected each; diff_summary is empty and gates.approval_required is false. No write grant is exercised and no approval gate is opened, because a pull is pure observation. Had the same request asked to push labels without a repo:write grant, the run would refuse instead of reading. Cursor-backed loop: Copy read cursor -> plan bounded pull/push -> append sync plan event -> read cursor The cursor event stores refs, filters, digests, and gate status. It does not store raw issue bodies, OAuth tokens, or write payload secrets. Inputs repo (required): target repository as owner/name . direction (required): pull or push . resources (required): structured selector for issues , prs , or threads plus filters (state, label, author, range). scope (required): read or write . A push needs write backed by a real repo:write grant. + +### issue-intake + +Path: `/runxhq/runx/pages/issue-intake.html` + +Convert an inbound thread, support report, or operator request into one explicit intake decision plus the parent change artifact that downstream planning or mutation lanes must share. + +Group: GitHub and delivery Source: skills/issue-intake/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/issue-intake/SKILL.md Issue Intake Convert an inbound thread, support report, or operator request into one explicit intake decision plus the parent change artifact that downstream planning or mutation lanes must share. This skill does not mutate code, open tickets, or publish replies directly. Its job is to classify the report, summarize it, draft the next helpful response, and recommend the next governed lane. That next lane must be explicit: issue-to-pr , work-plan , reply-only , or manual-review . In supervisor-style flows, issue-intake is also the commencement gate. It decides whether work may start at all, whether the next step should stop at a review comment first, and whether mutation is justified yet. A recommended lane is not the same thing as build permission. Use issue-to-pr only when the requested change is bounded enough for one governed remediation lane. Use work-plan for larger or multi-step work. Use reply-only when the right answer is guidance rather than mutation. Use manual-review when the report is ambiguous, risky, or missing key context. Ground category, severity, and routing in the visible request and supplied product constraints. Put uncertainty in operator_notes instead of inventing confidence. The suggested reply should sound like the project owner and lead with the decision or next action, not read like a ticket macro. Output Contract intake_report must contain: category : one of bug , feature_request , docs , billing , account , question , or other severity : one of low , medium , high , or critical summary : concise summary of the actual request or report suggested_reply : a user-facing reply draft or operator handoff note recommended_lane : issue-to-pr , work-plan , reply-only , or manual-review rationale : why that lane is the right next step needs_human : boolean operator_notes : array of caveats, missing context, or escalation notes intake_report may also include supervisor-facing control fields: commence_decision : approve , hold , reject , or needs_human action_decision : proceed_to_build , proceed_to_plan , request_review , or stop review_target : thread , outbox_entry , or none review_comment : markdown comment body for the supervisor to post before the next lane proceeds When present, these fields mean: commence_decision gates whether the supervisor may start any downstream work at all action_decision=proceed_to_plan means the supervisor may open a planning lane such as work-plan , but still may not start repo mutation action_decision=request_review means the supervisor should post review_comment to the chosen review_target and stop there until a later approval or rerun authorizes mutation review_target=outbox_entry only makes sense when a current outbox entry already exists. If no draft change, message surface, or other outbox entry exists yet, the supervisor should fall back to the source thread and say that clearly in the posted comment action_decision=proceed_to_plan should usually still result in a public supervisor comment so the hold/plan decision is visible outside the raw receipt stream recommended_lane=issue-to-pr alone does not authorize a build lane Always emit change_set alongside intake_report . Also emit signal when a source event is admitted. signal must follow runx.signal.v1 and carry the source reference, authenticity or trust level, dedupe fingerprint, evidence references, and source-thread preview. This packet is the portable world-before-action state that work-plan , issue-to-pr , hosted queues, and source-thread projections preserve. Close the Runx turn after intake is complete with terminal closure control metadata: a disposition, stable reason code, and concise summary. Closure is receipt control state, not part of the issue-intake artifact, and must not pretend the recommended downstream lane has already executed. When an adapter has provider context beyond the visible thread text, attach it to signal.evidence_refs or a referenced artifact. Source adapters own provider-specific fetching and redaction before calling this skill; this skill only reasons over the supplied, reviewer-safe signal and artifacts. Hydration is a gate, not a best-effort decoration. If supplied signal or artifact metadata says provider context is still needed, do not select action_decision=proceed_to_build . Use manual-review or request_review and explain the missing adapter context in operator_notes . If provider context is unavailable, use the remaining signal only when it is still concrete enough for a bounded reply, plan, or PR; otherwise stop for human review. The change_set is the parent artifact for any later planning or worker fanout. It is what keeps multiple repo-scoped lanes aligned to one shared objective. change_set must contain: change_set_id thread_locator summary category severity recommended_lane commence_decision action_decision target_surfaces : array of objects with: surface : repo, product surface, or bounded target name kind : one of repo , package , docs , support , or other mutating : boolean rationale : why this surface is implicated shared_invariants : array of constraints that all downstream lanes must preserve success_criteria : array of concrete outcomes that define success for the whole change outbox_entry (optional): current outbox entry for status updates, replies, or draft-change refreshes when the caller already knows it When recommended_lane=issue-to-pr , also include thread_change_request with: task_id thread_title thread_body thread_locator thread (optional) outbox_entry (optional) size : one of micro , small , medium , or large risk : one of low , medium , or high When recommended_lane=work-plan , also include workspace_change_plan_request with: change_set_id objective project_context thread_locator thread (optional) target_surfaces shared_invariants success_criteria Do not emit both thread_change_request and workspace_change_plan_request for the same report. Prefer conservative routing: if the report is bounded and well-understood, use commence_decision=approve and action_decision=proceed_to_build if the next step should be planning instead of mutation, use commence_decision=approve and action_decision=proceed_to_plan if the likely next lane is clear but mutation or planning should wait for maintainer confirmation, use commence_decision=approve and action_decision=request_review if the report is ambiguous, under-specified, or risky, use commence_decision=hold or needs_human Inputs thread_title : canonical thread title thread_body : canonical thread body or request text thread_locator (optional): canonical locator for the bounded thread, such as an issue, chat thread, ticket, or local agent session thread (optional): provider-backed thread for the current thread outbox_entry (optional): current outbox entry for replies, draft changes, or refreshes signal (optional): provider-neutral runx.signal.v1 observation gathered by the source adapter before decision product_context (optional): product-specific constraints or routing hints operator_context (optional): maintainer or support posture guidance source_event (optional): admitted Slack, Sentry, GitHub, file, API, or other provider event. Consuming repos decide source filters before calling this skill. source_policy (optional): source admission and routing policy. Do not hardcode channel names, Sentry projects, or owners in this skill. operational_policy (optional): runx.operational_policy.v1 packet used by downstream repo-changing lanes for source, target, runner, and source-thread admission. + +### issue-triage + +Path: `/runxhq/runx/pages/issue-triage.html` + +Turn noisy issue streams into bounded, evidence-backed action. + +Group: GitHub and delivery Source: skills/issue-triage/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/issue-triage/SKILL.md Issue Triage Turn noisy issue streams into bounded, evidence-backed action. This skill is for issue selection and response drafting, not for silently mutating repositories. Use it to identify which threads are worth attention, understand the maintainer or contributor situation, and draft the next helpful response or remediation path. Separate discovery from response. Discovery finds the thread worth engaging. Response drafting turns one chosen thread into a concrete answer, escalation, or change plan. Ground selection and response in the actual thread, repository facts, receipts, and maintainer context; do not infer intent beyond what is visible. Lead with the decision, answer, or next action in the project's own voice. Return needs_more_evidence or needs_human when the thread is ambiguous, hostile, underspecified, unsafe, or outside the maintainer's declared posture. Output Discovery runner: issue_candidates : candidate issues or discussions worth attention. selection_rationale : why one candidate should be handled next. operator_notes : constraints, caveats, or escalation triggers. Response runner: issue_profile : concise summary of the chosen thread. response_strategy : recommended response posture and next action. response_draft : post-ready draft or maintainer handoff. follow_up_actions : concrete next steps after the response. Inputs repository (optional): repository slug or workspace reference. query (optional): search or queue objective for discovery. issue_url (optional): canonical issue URL for response drafting. issue_snapshot (optional): structured issue data when already fetched. maintainer_context (optional): project norms, release posture, and response constraints. operator_context (optional): operator-supplied context used by higher-level triage graphs. objective (optional): what the operator wants from this pass. + +### issue-to-pr + +Path: `/runxhq/runx/pages/issue-to-pr.html` + +Drive one bounded thread-driven change through the scafld 2.4-compatible lifecycle and package the result as a provider-agnostic draft pull-request packet. + +Group: GitHub and delivery Source: skills/issue-to-pr/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/issue-to-pr/SKILL.md Issue to PR Drive one bounded thread-driven change through the scafld 2.4-compatible lifecycle and package the result as a provider-agnostic draft pull-request packet. The graph separates cognition from mutation. Agent phases author the scafld markdown spec and the bounded repo change bundle. Deterministic fs.write and fs.write_bundle phases are the only places files are written to disk. scafld owns the workflow kernel: plan , validate , approve , build_to_review , status , review , complete , and handoff . runx owns the explicit authoring boundaries, deterministic writes, receipts, and final outbox packaging. Branch creation and provider PR mutation are outside scafld. The caller or adapter prepares the branch, then passes the intended branch into this lane. The lane records that branch in the draft PR packet, and the GitHub adapter fails closed if the workspace checkout does not match it. The final issue-to-pr-push-outbox step is the only provider push boundary. Lifecycle The graph runs: scafld plan -> author markdown spec -> write spec -> read spec -> validate -> approve -> read approved spec -> read declared files -> author fix bundle -> write fix bundle -> build to review -> status -> read current branch -> review -> complete -> final status -> handoff -> package draft PR outbox -> adapter push. There are no translation projection steps. scafld handoff is the human handoff surface, build_to_review drives bounded native scafld build advances until the task is review-ready, and scafld review is the native review boundary. Thread Story The lane should leave one coherent source-thread story, not a stream of every internal event. The durable milestones are: source signal and the bounded request accountable decision that a PR is justified scafld spec approval and declared scope build and validation result adversarial review result draft PR publication human merge gate final provider outcome when observed Comments and PR bodies should summarize those gates with enough evidence for a reviewer to act. They must not publish raw local paths, secrets, full command dumps, or duplicate retry comments. User-facing labels should use plain terms such as spec authoring, fix authoring, review, and human merge gate. Spec Authoring Contract The issue-to-pr-author-spec boundary must emit a full scafld 2.4-compatible markdown document, not YAML and not a reduced project brief. The document must preserve front matter with: spec_version: '2.0' task_id created : ISO-8601 timestamp updated : ISO-8601 timestamp title : non-empty task title, normally thread_title status: draft harden_status: not_run size : one of small , medium , or large risk_level The body must include the standard scafld 2 sections: Current State, Summary, Context, Objectives, Scope, Dependencies, Assumptions, Touchpoints, Risks, Acceptance, at least one Phase section, Rollback, Review, Self Eval, Deviations, Metadata, Origin, Harden Rounds, and Planning Log. The graph normalizes the front matter before writing the spec so current scafld schema fields such as title and size stay deterministic even if the authoring boundary omits or stales them. All changed-file declarations must use concrete repo-relative paths in backticks under Context / Files impacted and Phase / Changes. Do not declare scafld-managed control-plane artifacts under .scafld/specs , .scafld/reviews , .scafld/runs , or old .ai governance paths as repo-change scope. Documentation and process requests still need a concrete repo file. Prefer existing docs surfaces supplied by repo_snapshot.existing_files or repo_context , and declare at least one non-governance repo file for an approved issue-to-pr lane. Do not leave the repo-change scope empty after the decision layer has approved a PR. Validation commands must run against the current workspace state after the fix bundle is written. Do not depend on git history ranges such as HEAD~1 or merge-base comparisons. Validation commands, when present, must be direct repo-local checks such as test, lint, build, or file-content commands. Never use runx runtime internals or graph/scafld/run.mjs as a validation command; scafld is already the lifecycle runner around the task. For any code change, the approved spec must declare at least one targeted test/spec file in the changed-file scope and include at least one executable validation command that exercises that target. This applies even when the source thread does not explicitly request coverage; code PRs are not publishable from this lane without targeted test/spec scope or grounded scafld validation evidence. If the source thread asks for tests, specs, regression coverage, focused coverage, or request/service coverage, the targeted coverage requirement cannot be softened to a generic smoke check. If no existing test/spec path is declared but the repository layout makes a conventional path inferable, declare that new test/spec file. If no grounded test/spec path or command can be inferred from the repo snapshot, stop with a missing-evidence reason instead of publishing a code-only PR. Preserve source-thread context in the spec's Summary, Origin, and Planning Log so later PR packaging can explain why the lane ran and what evidence justified the mutation. Fix Authoring Contract The issue-to-pr-apply-fix boundary must emit a bounded fix_bundle with files: [{ path, contents }] for every repo file needed to satisfy the approved spec. For documentation or process changes, the approved spec, source thread, repo snapshot, repo context, and declared file contents are sufficient when they identify a narrow edit. When repo_snapshot.recommended_files contains concrete repo-relative files, treat those files as actionable target evidence even if the generated spec is worded conservatively. Read the recommended file and the nearest relevant test or spec before blocking. If the source thread includes a runtime exception, backtrace, failing command, or named behavior and the recommended file exists, prefer the smallest conventional fix plus targeted regression coverage over an empty bundle. For any production code change, fix_bundle.files must include the smallest production fix and a targeted test/spec file, even when the source request does not explicitly ask for coverage. Do not publish a code-only fix bundle from this lane. If the approved spec, source thread, or acceptance criteria asks for tests, specs, regression coverage, focused coverage, or request/service coverage, the targeted test/spec file must directly cover that requested behavior. If no test file exists, create the narrow conventional test file when the repository structure makes that path inferable; otherwise block with the missing path and evidence reason. If a declared file has exists: false and the approved spec intentionally creates it, write the new file when the desired contents are inferable from the spec and thread. Do not block solely because the file has no prior contents. Return fix_bundle.status: blocked with files: [] only when no concrete repo-relative target is declared, a required existing file cannot be read, or the requested behavior cannot be inferred after inspecting the supplied target files. The blocked reason must name the missing evidence and path because an empty file bundle is a terminal policy denial before write-fix . Inputs task_id : scafld task id. thread_title : canonical title and default spec title. thread_body : full thread body or request text when available. thread_locator : canonical locator for the bounded thread. thread : portable thread for the current signal surface. outbox_entry : existing pull-request outbox entry when refreshing a draft. harness : optional runx.harness.v1 packet for the governed run boundary. signal : optional runx.signal.v1 packet. Preserve source references, fingerprint, authenticity, and evidence references as stateful context instead of reparsing source-thread prose. decision : optional runx.decision.v1 packet. Preserve the accountable selection rationale, selected act, and closure when the caller already made the lane decision. target_repo : intended repository slug for PR packaging. operational_policy : optional runx.operational_policy.v1 packet used to admit the source, target repo, runner, and source-thread route before PR packaging. source_id : optional operational policy source id. runner_id : optional operational policy runner id. repo_snapshot : compact structured snapshot of the target repo. repo_snapshot_path : optional path to a fuller repo snapshot artifact. repo_context : textual summary of repo shape and validation hooks. size : scafld size, default small . risk : scafld risk, default low . base : base ref for PR packaging, default main . fixture : workspace root containing .scafld . scafld_bin : explicit scafld executable path. provider , provider_command , provider_binary , model : optional native scafld review provider overrides. Structured Output On success, the lane emits: draft_pull_request : provider-agnostic PR draft state derived from scafld handoff, build, review, completion, status, and current git branch. outbox_entry : a pull_request outbox entry suitable for adapter push. push : adapter push result plus refreshed thread when the adapter supports push. Story metadata suitable for one source-thread reviewer update that summarizes the lifecycle gates and points at the human merge decision. + +### release + +Path: `/runxhq/runx/pages/release.html` + +Turn a proposed release into an audited publication. The skill owns the release decision process: evidence gathering, changelog preparation, approval, publish handoff, verification, and announcement. It does not own a project's custom release implementation. Project-specific topology lives in a release profile that names existing commands, workflows, registries, deploy targets, and verification readbacks. + +Group: GitHub and delivery Source: skills/release/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/release/SKILL.md Release Turn a proposed release into an audited publication. The skill owns the release decision process: evidence gathering, changelog preparation, approval, publish handoff, verification, and announcement. It does not own a project's custom release implementation. Project-specific topology lives in a release profile that names existing commands, workflows, registries, deploy targets, and verification readbacks. Every version and changelog claim must trace to commits, tags, checks, package metadata, or explicit operator context. Write release material for package consumers: say why this version matters and what they should do next, without generic launch language or positive wording that hides a blocker. Stop in prepare or at approval when checks fail, versioning is unclear, evidence is thin, or the announcement would overstate what shipped. Two runners: prepare (read-only) — survey the commit range since the last tag, classify commits, stage a changelog, run the declared checks, and emit a release_brief describing what would ship, what is blocked, and what remains unresolved. Safe to run unattended and in CI. release (default, graph) — wires prepare → approval gate → publish → verify . publish is not exposed as a standalone runner; it is only reachable inside the graph after the approval transition clears. Invoke runx skill release prepare for a CI dry-run. Invoke runx skill release to run the governed end-to-end flow. Phases prepare The read-only phase. Reads git history, classifies each commit since the previous semver tag ( feat , fix , refactor , chore , breaking ), stages a changelog, reads the project release profile when supplied, and runs the declared release checks. Emits a release_brief with the findings. The brief is the only artifact that flows forward. If it is not publishable , the graph stops at the approval gate with the reasons attached. approve-publish A typed approval step. The gate id is release.publish.approval . The brief is provided as context so the approver sees what would ship before deciding. The policy transition only advances to publish-release when approve-publish.approval_decision.data.approved is true . No back channel, no implicit approval on timeout. publish-release The destructive phase. Takes the approved release_brief from graph context and hands off to the project-declared release interface: an existing CLI command, GitHub Actions workflow, hosted API route, provider tool, or manual release gate. Every side effect is recorded in publish_report.side_effects[] with its locator and evidence; the graph receipt seals the trail. Refuses to act if the brief is missing, unpublishable, or not carried through the approval gate. Refuses to act if the project profile asks the agent to reimplement release logic instead of naming an existing execution surface. verify-release The proof phase. Reads the publish_report , release brief, and project profile, then verifies external state: registry versions, release assets, deploy health, site/changelog readbacks, package-manager manifests, or any other project-owned release acceptance criteria. Emits a release_report for operator review and public audit. Inputs Name Required Description project_root yes Absolute path to the project being released. channel yes Publishing target ( npm , pypi , github-release ). profile_ref no Path or registry ref for a project-owned release profile. The profile describes existing commands/workflows and verification expectations; it is not authority. last_tag no Previous release anchor. Defaults to the latest semver tag reachable from the current branch. operator_context no Cadence, campaign, or posture guidance for this release. Outputs prepare emits release_brief_packet carrying release_brief : changelog, check results, proposed version, unresolved flags, publishable verdict. The graph emits a graph receipt that links the prepare brief, the approval decision, publish report, and verification report into one auditable trail. publish-release (inside the graph) emits publish_report : registry URL, release tag, announcement packet, and a side_effects[] list with a locator and evidence per write action. verify-release emits release_report : expected lanes, observed readbacks, missing artifacts, conditional skips, and final release verdict. Trust boundary prepare is safe to run unattended and in CI. The destructive work is only reachable through the graph, and the graph refuses to transition to publish-release without an approved decision from release.publish.approval . The graph enforces the gate; the skill does not bypass it. Project profiles are context, not authority. A profile may say which workflow, command, registry, or URL should be used. It cannot grant credentials, skip approval, or authorize a destructive release by itself. Scopes runx:release:read — required by the prepare phase. runx:release:publish — required by the publish phase; the graph grant must include this only when the approval transition has cleared. runx:release:verify — required by the verification phase. Tasks release-prepare — the read-only phase task. Provides the release_brief output shape. release-publish — the destructive phase task. Only reachable inside the graph; requires the approved brief in context. release-verify — the proof phase task. Reads external state and reports whether the release actually landed. These are managed-agent task contracts carried by the skill package and its X.yaml graph definition. They are not a separate registered task catalog. + +### audit-receipt + +Path: `/runxhq/runx/pages/audit-receipt.html` + +Audit a sealed run for authority over-reach, using its own receipt as evidence. + +Group: Safety and review Source: skills/audit-receipt/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/audit-receipt/SKILL.md Receipt Auditor Audit a sealed run for authority over-reach, using its own receipt as evidence. runx seals a receipt for every run: the authority proof, the acts performed, the decisions taken, the refusals, and hashed material references. That receipt is the evidence. This skill reads a sealed receipt and answers one governance question: did the run stay inside the authority it was granted? It flags scopes exercised that were never granted, mutating acts that ran without an approval gate, refusals that were not recorded, and any raw secret material that leaked into the receipt. It pairs with least-privilege : that one narrows a grant from usage, this one verifies a run honored its grant. What this skill does Read the proof and the acts. From the receipt, extract the granted authority (the proof) and the scopes the acts actually exercised. Diff exercised against granted. Any exercised scope not covered by the proof is over-reach. Check the gates. Every mutating act must show an approval gate in the receipt; an ungated mutation is an anomaly. Check exposure. The receipt must carry only hashed material references; a raw secret in the receipt is a leak. Verdict. clean , anomaly , or needs_more_evidence , with the exact findings and a recommendation for each anomaly. Core principles The receipt is the evidence. Audit what the receipt records, not what the skill claims it did. Granted is the ceiling. Exercised authority must be a subset of the proof; anything beyond is over-reach, full stop. Mutation needs a gate. A mutating act with no approval gate in the receipt is an anomaly even if it succeeded. No raw material. A receipt must reference material by hash; raw credential material in a receipt is a leak, not a convenience. Absence of evidence is not clean. With no receipt or an unattributable one, return needs_more_evidence , never clean . When to use this skill Post-run governance audit of a sealed, successful run. Spot-checking that a skill honored its authority bound in production. Before promoting a skill toward a higher trust posture. When not to use this skill To diagnose a failed run and propose a fix. That is review-receipt (failure-to-improvement). This skill audits a sealed run for over-reach (success-to-governance); the two are different lenses on a receipt. To narrow a grant from observed usage. That is least-privilege . Diagnostics receipt.authority.over_reach (error): an exercised scope is not covered by the authority proof. receipt.mutation.ungated (error): a mutating act ran without an approval gate recorded in the receipt. receipt.refusal.unrecorded (warning): a denied request is not reflected as a sealed refusal. receipt.material.exposed (error): raw credential material appears in the receipt instead of a hash reference. receipt.clean (info): exercised authority is within the grant, mutations are gated, and no material is exposed. Procedure Resolve the receipt from receipt_id or use the provided sanitized receipt_summary . Extract the authority proof, granted scopes, acts, approvals, refusals, material references, and receipt signature metadata. Normalize exercised scopes from the acts and compare them with the granted scopes. Exercised must be a subset of granted. Identify mutating acts and confirm each has an approval gate recorded in the receipt. Check that denied requests appear as sealed refusals when the receipt records the attempt. Scan receipt-visible material for raw credentials or secret-bearing payloads. Return a verdict with findings, recommendations, and the success checkpoint. Edge cases and stop conditions Missing receipt: return needs_more_evidence ; never infer a clean run. Unattributable receipt: return needs_more_evidence when the receipt cannot be tied to the run under audit. Malformed proof: return needs_more_evidence unless enough normalized grant data is supplied separately. Unknown scope name: treat it as over-reach unless the grant explicitly covers it. Mutation without recorded gate: emit receipt.mutation.ungated even if the mutation succeeded and the outcome looks correct. Raw token, key, or credential in the receipt: emit receipt.material.exposed and recommend revocation/rotation. Output schema ( receipt_audit ) Copy decision : ready | needs_more_evidence run_ref : string granted_scopes : [ string ] exercised_scopes : [ string ] refusals : [ string ] findings : - id : string severity : error | warning | info message : string verdict : clean | anomaly | needs_more_evidence rationale : string recommendations : [ string ] success_checkpoint : milestone : string description : string A clean verdict requires zero error findings. Worked example A sealed run was granted repo.read . The receipt shows the acts exercised only repo.read , every act is an observation (no mutation), and material is referenced by hash. Exercised is a subset of granted, no mutation to gate, no exposure: verdict: clean . Had an act exercised repo.write while the proof granted only repo.read , that would raise receipt.authority.over_reach and a verdict: anomaly with a recommendation to revoke the run's grant and investigate. Inputs receipt_id (optional): the receipt id to audit. receipt_summary (optional): a sanitized receipt or its acts/proof summary when the full receipt is not available. granted_scopes (optional): the authority the run was granted, when not derivable from the receipt alone. objective (optional): operator intent that focuses the audit. At least one of receipt_id or receipt_summary is required; with neither, the skill returns needs_more_evidence . + +### cve-audit + +Path: `/runxhq/runx/pages/cve-audit.html` + +This skill audits exact npm versions from an immutable `package-lock.json` against the public OSV API. It emits a machine-readable audit result, `evidence.json`, and a finding-by-finding Markdown report. The governed graph in `X.yaml` independently replays every query and seals a delivery packet only when the reported and replayed advisory sets match exactly. + +Group: Safety and review Source: skills/cve-audit/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/cve-audit/SKILL.md Exact CVE Audit What this skill does This skill audits exact npm versions from an immutable package-lock.json against the public OSV API. It emits a machine-readable audit result, evidence.json , and a finding-by-finding Markdown report. The governed graph in X.yaml independently replays every query and seals a delivery packet only when the reported and replayed advisory sets match exactly. It does not install dependencies, execute target code, mutate the target repository, repair packages, or publish vulnerability claims. When to use this skill Use it when a reviewer needs checkable evidence for the advisories affecting the exact npm versions in a public, immutable lockfile. It is suitable for dependency review, release triage, and reproducible security evidence where a package-name-only or loose-range match would create false positives. When not to use this skill Do not use it for a mutable branch URL, a manifest without exact installed versions, a private lockfile without explicit read authority, non-npm ecosystems, exploit development, or claims about transitive coverage when the selected scope is direct-production . Do not treat missing OSV data as proof that a package is safe. Inputs target_name : display name for the audited project. target_repo : public HTTPS source repository. target_commit : full 40-character immutable Git commit. lockfile_url : public HTTPS lockfile URL containing that commit. dependency_scope : direct-production by default, or all-installed . The caller authorizes only public reads of the pinned lockfile and OSV. No credential, token, local project file, or private payload is an accepted input. Procedure Validate that the repository and lockfile URLs use HTTPS, the commit is a full Git hash, and the lockfile URL is pinned to that hash. Fetch the lockfile and record its SHA-256 digest before parsing it. Extract exact installed versions from lockfile versions 2 or 3. Preserve the requested scope in every artifact. Query OSV with { ecosystem: npm, package, version } for every inventory entry. Exclude withdrawn advisories. Record each finding with dependency, exact version, advisory ID, OSV URL, aliases, installed path, and the exact query that produced it. In the governed graph, replay every exact-version query in a separate step. Fail closed if an advisory is unsupported, omitted, withdrawn, or changed. Emit the report, evidence, verification, delivery packet, and sealed receipt. Edge cases and stop conditions Refuse non-HTTPS or mutable lockfile URLs. Refuse abbreviated commits and lockfiles that do not contain the commit. Refuse unsupported lockfile versions or entries without exact versions. Return needs_input when the target, commit, or lockfile evidence is not immutable enough to support reproducible review. Stop if OSV times out, returns malformed data, or changes between scan and independent replay. Stop if any false hit or missing hit is detected. Report the selected dependency scope; never imply broader coverage. Keep tokens, credentials, private source, and target code out of artifacts. Output schema The scan emits: Copy { "audit_result" : { "schema" : "runx.security.exact_cve_audit.v1" , "target" : { "repo" : "https://github.com/owner/repo" , "commit" : "40-character hash" , "lockfile_sha256" : "64-character digest" }, "dependency_scope" : "direct-production" , "inventory" : [], "findings" : [], "result" : { "exact_dependencies_queried" : 0 , "advisory_findings" : 0 , "source" : "OSV" } }, "report" : { "artifact" : { "path" : "artifacts/report.md" } }, "evidence" : { "summary" : "human-readable audit summary" , "observations" : [], "artifact" : { "path" : "artifacts/evidence.json" } } } The graph adds verification.json , delivery.json , and a sealed runx.receipt.v1 graph receipt with child receipt lineage. Worked example For OWASP NodeGoat at commit c5cb68a7084e4ae7dcc60e6a98768720a81841e8 , the checked-in harness reads the pinned lockfile, audits 16 exact direct production versions, reports the OSV advisories returned for those versions, and independently requires zero false and zero missing hits before sealing. The second harness case audits the repository's immutable clean fixture at commit 83d554f904f9f73bd26ce9c15e3786ba7d62b1de . It verifies that an exact dependency with no OSV advisory produces zero findings while still completing the independent replay and sealed-receipt path. If the same request points at main/package-lock.json , validation returns a needs-input failure because the lockfile is mutable and does not contain the declared immutable commit. No audit or receipt is presented as complete. + +### least-privilege + +Path: `/runxhq/runx/pages/least-privilege.html` + +Turn granted authority plus observed usage into a bounded attenuation proposal. + +Group: Safety and review Source: skills/least-privilege/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/least-privilege/SKILL.md Least Privilege Auditor Turn granted authority plus observed usage into a bounded attenuation proposal. runx keeps a receipt of every scope a run actually exercised. This skill reads that proof. It compares what a subject (a skill, a grant, or a principal) was granted against what its receipts show it used, then proposes the narrowest grant that still covers real usage. The output is a reviewable attenuation proposal, not an automatic change. What this skill does Diff granted authority against receipt-backed usage. Classify each granted scope as keep , narrow , remove , or defer . Propose the narrowest grant that still covers observed usage. State residual risk after attenuation. Emit a receipt-quality report a reviewer can apply or reject. When to use this skill Periodic least-privilege review of a skill, grant, or principal before publish, renewal, or maturity promotion. After an incident, to identify authority that can be safely removed without breaking observed behavior. Before expanding distribution of a public skill, to prove its grant is minimal against real receipts. When a reviewer asks for a scope-by-scope evidence trail, not just a summary. When not to use this skill To grant new authority. This skill only narrows; widening is a human decision. When no usable receipt evidence exists. Return needs_more_evidence rather than guessing a grant down to nothing. For secret material handling or credential exposure. Use the appropriate secret-leak triage flow instead of scope review. When the user asks for automatic permission changes. Produce a proposal and stop unless a separate approved delivery lane exists. When grant semantics are unknown and cannot be normalized. Return needs_input with the exact syntax or policy question. Procedure Scope the audit target. Identify subject , grant source, receipt ids or receipt window, and whether receipts are from the same principal or skill version. Gate: if the subject, grant list, or usage source is ambiguous, stop with needs_input . Evidence expected: subject id or label, granted scope list, receipt ids or an explicit statement that no receipts were available. Normalize granted scopes. Parse each scope into verb, resource, path or namespace, conditions, and wildcard breadth. Preserve original scope strings. Do not rewrite policy syntax casually. Gate: if a scope cannot be parsed, keep it as defer and request the missing policy semantics instead of treating it as unused. Build the usage model from receipts. Extract actual exercised verbs and resources from receipt steps, tool calls, policy checks, denied checks, and completion status. Count successful use separately from denied or dry-run checks. Do not infer scope usage from a successful high-level task alone; cite the receipt step or policy check that exercised the authority. Classify every granted scope. keep : at least one observed successful use requires the granted scope as written, or a reserved/break-glass policy explicitly requires it. narrow : all observed uses fit a strictly smaller verb, resource, namespace, condition, or path. remove : no observed use, denied check, or documented reserved purpose supports the scope. defer : evidence is conflicting, receipt attribution is weak, or policy semantics are unknown. Propose attenuation. Remove scopes classified as remove . Downgrade scopes classified as narrow only when every observed use fits the narrower grant. Leave keep and defer scopes unchanged in the proposed grant. Gate: never produce a proposal narrower than the evidence supports. A scope used once is used. State residual risk and reviewer action. Name what the proposed grant can still do. Name any broad scope kept despite thin evidence and why. Separate applyable now from needs human policy decision . Emit receipt expectations. A valid receipt for this skill should record input grant count, receipt sources, classification counts, proposed removals or narrowings, stop status, and unresolved questions. Edge cases and stop conditions Empty or unattributable usage evidence: return needs_more_evidence ; do not remove all scopes by default. Missing granted scopes: return needs_input ; there is no baseline to diff. Receipt subject mismatch: return needs_input with the mismatched subject or version. Conflicting receipts: classify affected scopes as defer and return needs_human if the conflict changes the proposal. Wildcard grants: narrow only to observed resource prefixes when receipt coverage is representative; otherwise keep and flag residual risk. Reserved, compliance, or break-glass scopes: keep unless the operator provides explicit policy authority to remove them. Dry-run-only use: do not count as successful exercised authority unless the grant exists solely for validation. Grant already matches usage: return no_change with the evidence summary. User asks to hide or omit unused authority: refuse that part and report the complete scope diff. Output schema Return a structured report with these fields: Copy status : attenuation_proposed | no_change | needs_more_evidence | needs_input | needs_human | refused subject : string evidence : receipt_ids : [ string ] receipt_window : string | null grant_source : string | null limitations : [ string ] scope_diff : - granted_scope : string normalized : verb : string | null resource : string | null conditions : object | null observed_use : count : number verbs : [ string ] resources : [ string ] receipt_refs : [ string ] classification : keep | narrow | remove | defer proposal : string | null rationale : string attenuated_grant : [ string ] removed_scopes : [ string ] narrowed_scopes : - from : string to : string kept_scopes : [ string ] deferred_scopes : [ string ] residual_risk : [ string ] reviewer_action : applyable_now | needs_policy_decision | gather_more_receipts | none receipt_expectations : classification_counts : object stop_status : string unresolved_questions : [ string ] Worked example Input: Copy subject : skills/report-exporter granted_scopes : - drive.files.read:/reports/* - drive.files.write:/reports/* - drive.files.delete:/reports/* usage_summary : receipt_ids : [ rx_101 , rx_102 ] observed : - scope : drive.files.read:/reports/* count : 8 refs : [ rx_101:step_3 , rx_102:step_2 ] - scope : drive.files.write:/reports/* count : 2 refs : [ rx_101:step_6 , rx_102:step_5 ] Output: Copy status : attenuation_proposed subject : skills/report-exporter removed_scopes : - drive.files.delete:/reports/* narrowed_scopes : [] kept_scopes : - drive.files.read:/reports/* - drive.files.write:/reports/* attenuated_grant : - drive.files.read:/reports/* - drive.files.write:/reports/* residual_risk : - The skill can still read and write any file under /reports/*. reviewer_action : applyable_now The delete scope is removable because no cited receipt exercised delete authority. The read and write scopes stay because each was used at least once. Inputs subject (optional): skill id, grant id, principal, or other label for what is being audited. granted_scopes (required): the current scopes granted to the subject, preferably in canonical policy syntax. usage_summary (required): receipt-derived usage. Include receipt ids, step refs, observed verbs, resources, success or denial status, and the time window when available. objective (optional): operator intent that focuses the review, such as "prepare for public publish" or "post-incident attenuation". policy_notes (optional): reserved scopes, compliance constraints, or human-approved exceptions that affect removal decisions. + +### policy-author + +Path: `/runxhq/runx/pages/policy-author.html` + +Author one governed runx operational policy from intent, and prove it lints. + +Group: Safety and review Source: skills/policy-author/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/policy-author/SKILL.md Policy Author Author one governed runx operational policy from intent, and prove it lints. Adopting governed runx means writing an operational policy: which repos may be touched, who owns which surface, which sources are trusted, what confidence is required before action, and which outcomes need a human. Written by hand that is a long, error-prone document. This skill turns a plain-English governance brief into one runx.operational_policy.v1 proposal, or tightens an existing policy, and runs a fail-closed lint over it before it ships. It proposes; a human approves. What this skill does Read the intent. Take the governance brief (and an existing policy when tightening) and identify the target surfaces, sources, owners, and the risk posture. Draft the policy. Produce a complete runx.operational_policy.v1 : target repos, runner binding, allowed actions, trusted sources with confidence floors, owner routes, and outcome rules. Lint fail-closed. Run the policy checks below. Any failing check blocks the proposal with the exact fix, rather than shipping a permissive policy. Tighten, never loosen. When given an existing policy, only propose changes that narrow authority (auto-merge off, human gate on, confidence up). Widening is a separate, explicit human decision. Core principles Fail closed. Unspecified means denied. A missing owner route, source rule, or confidence floor is a lint error, not a permissive default. Human gate on mutation. Any policy that allows repository mutation must set require_human_merge_gate: true and auto_merge: false . Named owners. Every target surface routes to a named owner; no orphan surfaces. Bounded sources. Each trusted source declares a minimum confidence; no source admits work below its floor. Verification before close. A source issue closes only when the outcome is verified. When to use this skill Bootstrapping a new runx deployment that needs an operational policy. Tightening an existing policy after a near-miss or an audit. Onboarding a new target repo, source, or owner into an existing policy. When not to use this skill To widen authority (add auto-merge, drop a human gate, lower confidence). That is an explicit human decision, not a generated proposal. To write skill logic or graphs. This authors the governance envelope, not the skills it governs. The operational policy model The proposal fills runx.operational_policy.v1 : target_repos : the repositories the policy may act on. runner : the runner binding (id, kind, and required substrate, e.g. GitHub Actions + scafld). allowed_actions : the lanes permitted (e.g. issue-intake , issue-to-pr , pr-review ). sources : trusted inbound sources, each with a min_confidence floor. owner_routes : surface-to-owner routing; every surface has a named owner. outcomes : verification_required , close_source_issue , require_human_merge_gate , auto_merge . Lint diagnostics The fail-closed lint emits these; any error blocks the proposal: policy.owner.unrouted (error): a target surface has no owner route. policy.mutation.no_human_gate (error): mutation allowed without require_human_merge_gate: true . policy.mutation.auto_merge_on (error): auto_merge is true on a mutating policy. policy.source.no_confidence_floor (error): a source has no min_confidence . policy.source.floor_too_low (warning): a confidence floor below 0.7. policy.close.before_verify (error): close_source_issue set without verification_required . policy.action.unknown (error): an allowed action is not a known lane. Procedure Validate that the brief names the governed work, the target repo or surface, and the intended owner or escalation route. Extract all repos, sources, actions, owners, confidence floors, and outcome rules from the brief and any existing policy. If tightening an existing policy, diff proposed changes against the current grant. Flag any widened action, lower confidence floor, removed owner, or removed human gate as a separate human decision. Draft the smallest complete runx.operational_policy.v1 that allows the stated work and denies everything else. Run the lint diagnostics. Any error finding prevents decision: ready . Emit the policy, lint result, rationale, blockers, and success checkpoint. Edge cases and stop conditions No owner route: return needs_input ; an ownerless surface is never governed by default. Mutation without a human gate: return reject or needs_input ; do not emit a ready mutating policy without require_human_merge_gate: true . Auto-merge requested: block the proposal unless the user explicitly performs a separate authority-widening decision outside this skill. Unknown action lane: return needs_input with the unknown action names. Source without confidence floor: return needs_input ; implicit trust is not a policy. Conflicting owner routes: return needs_input and cite the conflicting surfaces and owners. Output schema ( policy_proposal ) Copy decision : ready | needs_input | reject policy : schema : runx.operational_policy.v1 target_repos : [ string ] runner : id : string kind : string requires : [ string ] allowed_actions : [ string ] sources : - provider : string min_confidence : number owner_routes : - surface : string owner : string outcomes : verification_required : boolean close_source_issue : never | when_verified | always require_human_merge_gate : boolean auto_merge : boolean lint : status : pass | fail findings : - id : string severity : error | warning message : string rationale : string blockers : [ string ] needs_input : [ string ] success_checkpoint : milestone : string description : string A proposal with any error finding must have decision: needs_input or reject , never ready . Worked example Brief: "Govern issue intake across our three repos. GitHub issues and Sentry alerts. Kam owns the platform, Chong owns product. Never auto-merge; a human approves every merge; close the source issue only once the fix is verified." The proposal binds the three repos to a GitHub-Actions + scafld runner, allows issue-intake / issue-to-pr / pr-review , trusts GitHub at 0.72 and Sentry at 0.82, routes platform to Kam and product to Chong, and sets require_human_merge_gate: true , auto_merge: false , verification_required: true , close_source_issue: when_verified . The lint passes, so decision: ready . Inputs governance_brief (required): the governance intent in prose. existing_policy (optional): a current runx.operational_policy.v1 to tighten. target_repos (optional): explicit repo list when not in the brief. objective (optional): operator intent that focuses the pass. + +### review-receipt + +Path: `/runxhq/runx/pages/review-receipt.html` + +Diagnose what went wrong in a skill or graph execution and propose the smallest change that fixes it. + +Group: Safety and review Source: skills/review-receipt/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/review-receipt/SKILL.md Receipt Review Diagnose what went wrong in a skill or graph execution and propose the smallest change that fixes it. Read the receipt or failure summary. Identify what was attempted, what succeeded, and where it broke. The receipt contains step statuses ( sealed , failure , policy_denied , needs_agent ), exit codes, stderr, scope admission decisions, and timing. Distinguish root cause from symptoms. A graph may report failure at step 4, but the root cause may be bad output from step 2 that propagated through context passing. Trace data flow backward through context edges to find where the problem originated. Classify the failure: Input error — required input missing or malformed. Fix: input validation or input resolution. Scope denial — step requested scopes outside the graph grant. Fix: scope declarations or grant configuration. Tool failure — CLI tool or adapter returned an error. Fix: tool invocation (args, env, cwd) or the tool itself. Schema mismatch — step output did not match expected shape for downstream context. Fix: output parsing or artifact contract. Timeout — step exceeded time budget. Fix: increase timeout, reduce work, or split the step. Policy denial — transition gate blocked the step. Fix: gate conditions or upstream output. Review rejection — adversarial review found blocking issues. Fix: the code or spec, not the review process. Harness assertion — fixture expectations did not match actual output. Fix: skill logic or stale fixture expectations. Agent-mediated suspension is not a failure A receipt with status needs_agent denotes a healthy agent-mediated suspension, not a defect. The runtime yielded to the caller for missing agent or human input. This is a normal part of graph execution, not one of the failure classes above. When the only evidence is needs_agent without any exit code, scope denial, schema mismatch, or other concrete failure signal, return verdict: pass with an empty improvement_proposals array and note that the graph is paused as designed. One failure, one fix. Propose the smallest change that addresses the root cause. Do not bundle unrelated improvements. Output The output shape is formalised as JSON Schema at review-receipt-output.schema.json . Agents should self-validate before returning, and downstream consumers (notably write-harness ) may validate on receipt. verdict : pass , needs_update , or blocked . failure_summary : which step, which failure class, what root cause. One to three sentences. improvement_proposals : array of bounded changes. Each: target : what to change (SKILL.md, execution profile, graph step, input, fixture) change : what specifically to change rationale : why this fixes the root cause risk : what could go wrong next_harness_checks : replayable checks that should pass after the fix. Inputs All optional — supply whichever evidence is available: receipt_id : receipt id to inspect. receipt_summary : sanitized receipt or harness summary. harness_output : failed harness output or assertion text. skill_path : path to the skill being improved. + +### sandbox-harden + +Path: `/runxhq/runx/pages/sandbox-harden.html` + +Decide the narrowest sandbox a workload can run inside without breaking it. + +Group: Safety and review Source: skills/sandbox-harden/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/sandbox-harden/SKILL.md Sandbox Harden Decide the narrowest sandbox a workload can run inside without breaking it. What this skill does Most workloads ship with the default sandbox their runtime hands them: the full seccomp default, a broad capability set, unrestricted egress, a writable root. That default is sized for the worst case, not for this workload. This skill reads what a named workload actually needs and emits the tightest posture that still lets it run: an allowed-syscall list, the capabilities to drop, an egress allowlist, and a filesystem stance, with the residual risk named in plain terms. The output is a posture recommendation, not an enforced change. A runtime, an orchestrator, or an operator applies it. This skill never executes the workload and never widens a posture below the supplied baseline without saying why. How it differs from its neighbors: least-privilege audits API scopes, not syscalls; audit-receipt reads a sealed run after the fact. This skill is the only one that reasons about the seccomp, capability, egress, and filesystem posture a workload should run inside before it starts. The recommendation reads input only and writes nothing; applying it is a separate runtime act that exercises sandbox:configure on the named workload and nothing wider. When to use this skill Before running an untrusted or third-party workload, to decide its sandbox. During a security review of an existing deployment whose sandbox is the broad default. When promoting a workload toward production and the runtime posture must be reviewable, not implicit. When an operator needs the egress allowlist and dropped capabilities written down before a runtime applies them. When not to use this skill To run, build, or schedule the workload. This skill recommends a posture; a runtime, orchestrator, or operator executes the workload under it. To audit which API scopes or grants a subject used. That is least-privilege ; it reasons about authority, this one reasons about syscalls, capabilities, egress, and the filesystem. To audit a sealed receipt for over-reach after the fact. That is audit-receipt . To handle, store, or surface the secret material a workload reads. A hardening profile names a mount path or a secret handle, never a secret value. To produce a posture for a workload whose identity is unknown. Return needs_agent instead of hardening an unnamed target. Procedure Resolve the workload. Accept an image digest ( sha256:... ) or a skill ref. Record which form was supplied as hardening_profile.workload . Gate: if no workload is supplied, stop with needs_agent . There is nothing to harden. Build the behavior model. Combine the workload class (web service, batch job, CLI, language runtime), the supplied threat_context , and the baseline posture. Distinguish known behavior from assumed behavior. A profile built on assumed syscall need is weaker evidence than one built on an observed or documented call set. Gate: if the behavior is unknown enough that the syscall set, egress, or write paths would be a guess, stop with needs_more_evidence and name what observation would resolve it (a trace, a manifest, a dry run under audit seccomp). Recommend the seccomp profile. Default to deny . Add only syscalls the behavior model supports. Prefer a named runtime default profile plus an explicit allow delta over a hand-rolled full list when the workload class has a known good baseline. Never add a syscall family with no behavioral basis. Unknown need is a stop condition, not a blanket allow. Drop capabilities. Start from "drop all", then justify each capability kept. A capability is kept only when the behavior model needs it. Name the reason per kept capability in the rationale. Set the egress posture. Default to mode: none . Move to mode: allowlist only when the workload has a named, justified destination set. List hosts, not raw allow-everything. An empty allowlist means no egress. Never recommend open egress as a convenience. Set the filesystem posture. Default to readonly: true with an explicit writable_paths list. Each writable path is justified by the behavior model (scratch, cache, a declared output dir). A writable root is a finding, not a default. State residual risk. After the controls above, name what an attacker who fully controls the workload could still do, the level , and the reason . Residual risk is never "none". If the profile is built on assumed behavior, say so here. Honor the baseline. The recommended posture must be at least as strict as the supplied baseline on every axis. If the model would relax any control below the baseline, do not relax it silently; either keep the baseline or, where a relaxation is genuinely warranted, record the reason in the rationale and raise the residual-risk level. The narrowness gate and the evidence gate are the two that hold authority: no control weaker than the baseline without a stated reason and a raised residual-risk level, and no syscall, host, or write path with no behavioral basis. A posture no tighter than the baseline with no new evidence is not worth emitting. Edge cases and stop conditions Missing workload: return needs_agent ; an unnamed target cannot be hardened. Unknown behavior: return needs_more_evidence with the observation that would resolve it; do not pad the syscall set with plausible families. Workload needs a privileged capability (for example CAP_SYS_ADMIN ): keep it only with a stated reason and raise the residual-risk level; never drop a capability the workload provably needs just to look tighter. Egress to a dynamic or unbounded host set: keep mode: allowlist with the known hosts and flag the unbounded remainder as residual risk; do not fall back to open egress. Baseline is already tighter than the model: keep the baseline; the recommendation never loosens a control the operator already set. Secret material in the input: reference it by mount path or handle in the profile and rationale; never copy a secret value into the output. Conflicting threat context and baseline: prefer the stricter control and name the conflict in the rationale. Output schema Copy hardening_profile : decision : ready | needs_more_evidence | needs_agent workload : ref_form : image_digest | skill_ref image_digest : string skill_ref : string class : string seccomp : default : deny | allow allowed_syscalls : array dropped_caps : array egress : mode : none | allowlist hosts : array filesystem : readonly : boolean writable_paths : array residual_risk : level : low | medium | high reason : string rationale : string The single hardening_profile object is packet runx.hardening.v1 . Secrets, tokens, key material, and raw fetched content never appear in the profile; secret-bearing inputs are referenced by mount path or handle only. The receipt carries the workload ref form and digest, the four posture axes, the residual-risk level, the stop status, and the quality and voice profile hashes. It carries no secret values and no syscall trace payloads. Worked example Input: workload is { image_digest: "sha256:1f4c...", class: "batch job" } ; threat_context is "processes untrusted user uploads, no inbound network"; baseline is "docker default seccomp, all caps, open egress, writable root". Output: decision: ready . seccomp.default: deny with an allowed set covering file I/O, memory, and process control but not ptrace , mount , or raw socket families. dropped_caps is the full default set (the job needs none). egress.mode: none (no inbound or outbound network in the threat context). filesystem.readonly: true with writable_paths: ["/tmp/work"] for upload scratch. residual_risk.level: low , reason: a compromised job can still consume CPU and fill /tmp/work to its quota; it cannot reach the network or escalate. The rationale records that the syscall set is assumed from the batch-job class, not from an observed trace, so a trace would raise confidence without widening the posture. Inputs workload (required, json): the target to harden, as { image_digest } or { skill_ref } , optionally with class . Without it the skill returns needs_agent . threat_context (optional, string): the trust assumptions and exposure, for example "processes untrusted uploads, no inbound network". baseline (optional, string): the current or floor posture. The recommendation is never weaker than this without a stated reason. + +### governed-outbound + +Path: `/runxhq/runx/pages/governed-outbound.html` + +Take something from outside, make it safe to send, authorize the exact outbound plan, and leave proof. `governed-outbound` prepares the boundary crossing; it does not claim the configured provider delivered anything. + +Group: Outbound and tooling Source: skills/governed-outbound/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/governed-outbound/SKILL.md Governed Outbound Take something from outside, make it safe to send, authorize the exact outbound plan, and leave proof. governed-outbound prepares the boundary crossing; it does not claim the configured provider delivered anything. It composes four catalog skills into one governed run: web-fetch gathers the source within an explicit host allowlist. redact-pii scrubs personal data and returns a pass/hold verdict before any of it can leave the boundary. an approval gate holds the plan for a human, who sees the redaction verdict and the residual risk, not the raw content. send-as binds the scrubbed content, principal, audience, and provider lane into an authorized plan. sign-receipt seals the gather, scrub, approval, and plan. A separate provider host must execute the plan, record delivery evidence, and read it back before any caller may call the notification delivered. The point of the chain is the order. The scrub runs before authorization and the human gate runs before send-as . This receipt proves preparation and authority, not provider delivery. What this skill does governed-outbound is a graph, not a single agent step. Each hop is a real catalog skill with its own scope, and authority narrows at every branch: web-fetch may only reach the allowlisted host, redact-pii may only read the fetched content, the approval gate authorizes the plan, and sign-receipt may only append to the ledger. Personal data never reaches the channel or the receipt; the content travels by digest, and the redaction report carries class and span offsets, never the values it found. When to use this skill An agent needs to prepare external information (an incident page, a changelog, a status update, a thread) for a provider channel, and that information may carry personal data. A workflow must prove that the proposed outbound content was scrubbed and the exact plan was approved before provider execution. You want one receipt that links the source, the scrub verdict, the approval, and the send plan. When not to use this skill To post content that was authored in-house and carries no external data. Call send-as directly, then use the configured provider host. To gather a source with no intent to send it onward. Call web-fetch . To deliver without a human in the loop. The approval gate is the point; a send that needs no review does not need this chain. To move money, change a repository, or unseal a secret. Those are other governed lanes with their own gates. How the chain is wired fetch-source reads url and allowlist from the run inputs and returns fetch_result with the content digest and extracted text. scrub-content takes fetch-source 's extracted text as content , runs in redact mode, and returns redaction_report with the ready / needs_review / blocked verdict, the detected spans, and redacted_digest . approve-send shows the approver the redaction decision , the residual_risk , and the redacted_digest , then records an approval decision. plan-notice runs only when the approval is true and the redaction verdict is ready ; it plans the send of the scrubbed content to channel as principal , naming the provider action a connector lane would run. seal-run attests the run, binding the source digest and the redacted digest as evidence, and appends the receipt to the ledger. Edge cases and stop conditions No url or allowlist : the run returns needs_agent ; there is nothing to gather and no boundary to respect. Host not allowlisted: web-fetch returns policy_denied and the chain stops before anything is read. Redaction not ready : a needs_review or blocked verdict fails the send transition, so plan-notice never runs. Nothing leaves the boundary on a hold verdict. Approval denied or absent: the send transition is not satisfied and the chain stops at the gate, scrubbed but unsent. Provider delivery fails downstream: preserve this planning receipt, record the provider failure in the executing host, and do not produce delivery evidence or mark the action complete. Output The run seals to runx.receipt.v1 , linking each step's packet: fetch_result (source + digest), redaction_report (verdict + spans + redacted digest), approval_decision (the gate), send_plan (the authorization), and the attestation (the seal). send_plan is authorization, not delivery evidence. The receipt proves the preparation path without reconstructing the personal data that was removed along the way. Inputs url (required): source to gather before preparing the notification. allowlist (required): hosts web-fetch is permitted to reach. channel (required): destination channel for the notification. principal (required): principal the notification is sent as. claim (optional): what the sealed attestation should assert about the run. operator_context (optional): boundary, audience, or compliance context. + +### run-history + +Path: `/runxhq/runx/pages/run-history.html` + +Turn runx's own run ledger into a governed, read-only report. + +Group: Outbound and tooling Source: skills/run-history/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/run-history/SKILL.md Run History Analyst Turn runx's own run ledger into a governed, read-only report. Every governed runx run leaves a receipt. Over time that ledger is data: which skills run, how often they seal versus refuse, which never graduate past alpha, and where authority is consistently broader than usage. This skill reads that ledger (via runx history and runx list ) and reports it. It never executes a skill, sends, or mutates; every planned call is read-only. Its recommendations route to the governance skills, least-privilege , audit-receipt , and the maturity promoter, so the report turns into action through the right governed lane. What this skill does Scope the question. Account-wide, a single skill, or a period. Pull the ledger, read-only. Plan runx history and runx list queries; never an execution command. Grade the signals. Seal rate, refusal rate, maturity distribution, and scope-usage breadth, each with an assessment, not a bare number. Recommend through governed lanes. A high refusal rate, a skill stuck at alpha, or a consistently-unused scope routes to a named governance skill, not a direct mutation. Core principles Read-only. Only runx history and runx list . No execution, send, or config call. Every planned call is requires_confirmation: false . Grade, do not dump. Every metric carries an assessment against a norm. Route, do not act. Recommendations name the governed lane ( least-privilege , audit-receipt , maturity promoter); this skill does not change a grant or a tier itself. Refusals are signal, not failure. A healthy refusal rate means bounds are working; a spike means a skill or a policy needs review. Absence is not health. With no history, return needs_more_evidence . When to use this skill Periodic platform review: what is runx actually doing across skills. Spotting skills with anomalous refusal rates or stuck maturity. Finding consistently-unused scopes worth attenuating. When not to use this skill For a single run's authority audit (use audit-receipt ). To narrow one skill's grant from its usage (use least-privilege ). For email or product analytics. This reports on runx runs, not a domain dataset; that is a separate, product-owned analytics skill. Signals and norms seal_rate : share of runs that sealed cleanly. good >0.9, warning 0.7-0.9, critical refusal_rate : share of runs that hit a governed refusal. info by default; a sharp per-skill spike is a warning worth routing. maturity_distribution : counts at alpha / beta / stable. Many skills stuck at alpha is a warning (no harness coverage). scope_usage : scopes granted but never exercised across runs, a candidate for attenuation. Output schema ( history_report ) Copy decision : ready | needs_more_evidence scope : workspace | skill | all period : string ordered_tool_calls : - tool : runx history | runx list purpose : string requires_confirmation : boolean # always false; read-only findings : - metric : string value : string assessment : good | warning | critical | info recommendations : - finding : string lane : least-privilege | audit-receipt | maturity-promoter | none action : string blockers : [ string ] needs_input : [ string ] success_checkpoint : milestone : string description : string Worked example Question: "How is the skill catalog behaving this month?" The report plans runx history --since 30d and runx list skills --json , then reports a 0.94 seal rate (good), a refusal rate of 0.06 (info, bounds working), a maturity spread of 14 alpha / 5 beta / 2 stable (warning, most skills lack harness coverage), and one skill granted repo.write but never exercising it across 40 runs. It recommends routing the alpha-heavy spread to the maturity promoter and the unused repo.write to least-privilege for attenuation. It changes nothing itself. Inputs objective (required): the history question. scope (optional): workspace , a specific skill , or all . period (optional): e.g. 30d or 90d . history_summary (optional): a sanitized runx history summary when already fetched. objective guides which signals to lead with. + +### sourcey + +Path: `/runxhq/runx/pages/sourcey.html` + +Generate a documentation site for a project using Sourcey. Sourcey is a static documentation generator that produces HTML sites from markdown pages, OpenAPI specs, Doxygen XML, and MCP server snapshots. + +Group: Outbound and tooling Source: skills/sourcey/SKILL.md Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677 Path: skills/sourcey/SKILL.md Sourcey Generate a documentation site for a project using Sourcey. Sourcey is a static documentation generator that produces HTML sites from markdown pages, OpenAPI specs, Doxygen XML, and MCP server snapshots. What this skill does By default, runx executes Sourcey as a governed mixed-runner skill: discover the bounded documentation scope, evidence, and plan request approval author the bounded docs/config bundle write the source bundle deterministically build docs deterministically critique the built output in one bounded pass apply at most one bounded revision pass rebuild and verify the output deterministically For already-configured projects, the same sourcey runner stays narrow: the discover step can confirm existing config, the author/revise passes can return empty bundles, and the deterministic tool steps still perform the build and verification work. For repository-backed projects, Sourcey owns two separate surfaces: committed docs source and generated site output. Keep those separate. Do not mix emitted HTML, search indexes, or OG assets back into the authored docs tree. When to use this skill A project needs a maintainer-grade documentation site generated from real repository evidence, existing docs, API specs, Doxygen XML, or MCP snapshots. A branded package or product needs Sourcey output with governed discovery, approval, authoring, deterministic build, critique, revision, and receipt proof. A workflow needs to separate authored docs source from generated site output while preserving a reviewable receipt trail. A maintainer wants CI or deploy to rebuild docs without inventing scope, prose, or information architecture at deploy time. When not to use this skill To manufacture documentation when the repository evidence is too thin. Return needs_more_evidence or needs_review instead of confident filler. To write generated HTML, search indexes, or Open Graph assets back into the source docs tree. To bypass approval for a new docs plan or to run open-ended critique/revision loops. To document APIs by hand when an OpenAPI, Doxygen, or MCP source can be used directly by Sourcey. Documentation rules Sourcey output should read like native project documentation that a maintainer would stand behind: build from project evidence, but do not expose the evidence-gathering process as page prose preserve the project's own terms, priorities, and level of ambition make fewer pages with real substance rather than many generic pages make a real developer action easier—install, evaluate, integrate, operate, or contribute; a polished site with thin content is a failed run never use "generated by Sourcey", preview, adoption, migration, scaffold, or demo framing unless the project itself uses that framing never describe pages as machine output, agent output, or AI-generated docs; the site should read like the project maintainer wrote and stands behind it when publishing public docs, use a credible durable project, maintainer, organization, product, or documentation home. Random personal domains, placeholder parent sites, sandbox hosts, preview deploys, throwaway subdomains, and unrelated novelty domains are not publication-quality homes if the repo evidence is too thin for a strong docs page, surface that as an evidence gap instead of manufacturing confident filler Canonical semantics Complex runx skills share a reusable phase language: scope ingest model materialize evaluate revise verify ratify The current Sourcey runner deliberately uses a bounded subset: discover folds scope + ingest + model approve is ratify author + write-docs + build form materialize critique is evaluate revise + write-revisions + rebuild form revise verify is verify The current slice uses exactly one bounded revision window. It never loops until good and it never critiques indefinitely. When docs_inputs is supplied explicitly, treat that as a bounded instruction to use the existing config target. Do not overwrite the referenced config or invent replacement docs files merely because repository inspection evidence is thin. Missing evidence is not the same as missing files. Procedure Inspect the project and discover a bounded documentation plan from real project evidence. Approve the discovered plan before authoring. Author the bounded Sourcey source bundle. Persist that bundle deterministically. Run sourcey build deterministically with the discovered or authored config. Critique the built output in one bounded evaluation pass. Apply at most one bounded revision pass from that critique. Rebuild deterministically after the revision bundle is written. Verify the output directory contains index.html . Inspect the receipt and generated site. The deterministic build report should carry enough rendered evidence for an external reviewer to reason about the site without hidden file access. At minimum that means the generated file list plus index-page title, headings, and an excerpt when index.html exists. Discovery contract discovery_report may include additional planning metadata, but the canonical resolved docs inputs must live under: discovery_report.discovered.brand_name discovery_report.discovered.homepage_url discovery_report.discovered.docs_inputs Downstream deterministic build steps consume that nested discovered object. Output schema Sourcey build produces: HTML pages, sourcey.css , sourcey.js , search-index.json , sitemap.xml , llms.txt , llms-full.txt , and _og/ directory with generated Open Graph images. The sealed package includes: Copy discovery_report : discovered : brand_name : string | null homepage_url : string | null docs_inputs : object | null doc_bundle : files : array summary : string sourcey_build_report : generated_files : array index_title : string index_headings : array index_excerpt : string evaluation_report : object revision_bundle : files : array summary : string sourcey_verification_proof : verified : boolean index_path : string receipt_notes : authority : governed docs plan approval mutation : authored docs source writes only Worked example Input: a project contains README.md , package.json , and a partial docs/ tree, but no Sourcey config. Output: decision: ready after approval; Sourcey discovers the project name, homepage, and docs inputs, writes a bounded docs/sourcey.config.ts plus only the highest-value missing docs pages, builds to .sourcey/runx-docs , critiques the rendered index.html , applies at most one revision bundle, verifies the output, and seals a receipt with the build report and verification proof. If the project evidence does not support a maintainer-grade site, the run stops with needs_more_evidence or needs_review instead of producing filler. Inputs project (required): project root directory. repo_root : optional alias for the project root when Sourcey is composed inside a parent graph that already uses repo_root . brand_name : project name (discovered from package evidence if omitted). homepage_url : project homepage (discovered from project evidence if omitted). docs_inputs : structured docs inputs, e.g. {"mode":"config","config":"docs/sourcey.config.ts"} or {"mode":"openapi","spec":"openapi.yaml"} . Discovered if omitted and may point at authored config produced by the skill. project_brief : optional grounded brief carrying brand cues, docs audit, IA direction, and writing constraints. When present, the authored docs should feel like native project docs rather than generic generated scaffolding. output_dir : generated site output path (default: /.sourcey/runx-docs ). sourcey_bin : explicit sourcey executable path (default: SOURCEY_BIN env or sourcey on PATH). Repository Contract Keep authored docs source in the repository, usually under docs/ when using docs/sourcey.config.ts . Keep generated site output in output_dir , separate from the source tree. The default generated output path is /.sourcey/runx-docs . Generated output should be gitignored unless the project explicitly chooses to version release artifacts. CI or deploy may run deterministic sourcey build from committed source. Deploy must not be the step where docs scope, prose, or IA is invented. Do discovery, authoring, and review before deploy. For Astro host apps, prefer the first-class sourcey/astro integration over a separate prebuild script that writes into public/docs . Keep docs/sourcey.config.ts and markdown/spec inputs as source; let astro dev serve Sourcey through Vite and astro build write generated docs into the final output under the configured route. For public publication, include enough proof for an external reviewer to inspect the target project, source commit, Sourcey config or input source, generated page list, deployment URL, parent domain, and durability of the hosting choice. Astro host pattern Use this shape when the target already uses Astro and docs should live at a path such as /docs : Copy import { defineConfig } from "astro/config" ; import sourcey from "sourcey/astro" ; export default defineConfig ({ site: "https://example.com" , integrations: [ sourcey ({ config: "./docs/sourcey.config.ts" , routeBase: "/docs" , }), ] , }) ; Do not add prebuild , build:docs , or committed public/docs artifacts for this path unless the project explicitly cannot use Astro integrations. The generated output remains reproducible build output, not authored source. Config reference Copy import { defineConfig } from "sourcey" ; export default defineConfig ({ name: "Project Name" , theme: { preset: "default" , // "default" | "minimal" | "api-first" colors: { primary: "#hex" , // required light: "#hex" , // optional, derived from primary dark: "#hex" , // optional, derived from primary }, fonts: { sans: "Inter" , // optional mono: "monospace" , // optional }, layout: { sidebar: "18rem" , // optional toc: "19rem" , // optional content: "44rem" , // optional }, css: [ "path/to/custom.css" ], // optional } , logo: "path/to/logo.png" , // or { light, dark, href } favicon: "path/to/favicon.ico" , repo: "https://github.com/org/repo" , editBranch: "main" , editBasePath: "docs" , // path from repo root to docs source codeSamples: [ "curl" , "javascript" , "python" ] , // for OpenAPI tabs navigation: { tabs: [ // Markdown pages tab { tab: "Documentation" , slug: "" , // empty = default tab groups: [ { group: "Getting Started" , pages: [ "introduction" , "quickstart" ] }, { group: "Guides" , pages: [ "configuration" , "deployment" ] }, ], }, // OpenAPI tab { tab: "API Reference" , openapi: "path/to/openapi.yaml" , }, // Doxygen tab { tab: "C++ API" , doxygen: { xml: "path/to/doxygen/xml" , language: "cpp" , // "cpp" | "java" groups: true , // use doxygen groups for nav index: "auto" , // "auto"|"rich"|"structured"|"flat"|"none" }, }, // MCP tab { tab: "Tools" , mcp: "path/to/mcp.json" , }, ], } , navbar: { links: [ { type: "github" , href: "https://github.com/org/repo" }, // types: github, twitter, discord, linkedin, youtube, slack, // mastodon, bluesky, reddit, npm, link ], primary: { type: "button" , label: "Demo" , href: "/demo" }, } , footer: { links: [{ type: "github" , href: "https://github.com/org/repo" }], } , search: { featured: [ "introduction" , "quickstart" ], // top results when empty query } , }) ; Page format Pages are markdown files resolved relative to the config file directory. If config is at docs/sourcey.config.ts , then page "quickstart" resolves to docs/quickstart.md . Copy --- title: Page Title description: One-line description for search and meta tags --- Content here. Standard markdown with code blocks, tables, links. Card Icon Contract Sourcey card icons are Heroicons v2 outline names in kebab-case. The renderer returns an empty icon for unknown names, so authoring must use exact names. Known-good names for documentation cards include: academic-cap , arrow-path , bell , bolt , book-open , chart-bar , check-circle , cloud-arrow-up , code-bracket , command-line , cpu-chip , cube , document , document-text , exclamation-triangle , globe-alt , key , lifebuoy , light-bulb , lock-closed , magnifying-glass , map , rocket-launch , server-stack , shield-check , sparkles , and wrench-screwdriver . Invalid card icon names are a blocking quality issue. The build report includes icon_validation ; critique and revision must fix any icon_validation.status: "invalid" result before the run is accepted. Edge cases and stop conditions Only create tabs for content types the project actually has. Do not add an OpenAPI tab if there is no spec file. Do not add a Doxygen tab without XML. Do not document APIs by hand when a spec file exists — use the spec tab. Keep navigation shallow: 1-2 tabs, 2-4 groups for most projects. Use project brand colors if identifiable. Otherwise use a neutral palette. Use only exact Heroicons v2 outline names for Sourcey card icon attributes; never invent icon names. When a grounded brief provides logo, favicon, color, or IA guidance, prefer that over generic defaults. Match the project's existing voice and terminology. Never write docs that describe themselves as a preview, adoption, migration, or tool-generated scaffold unless the repo's own evidence explicitly uses that framing. Do not write generated HTML, search indexes, or OG assets into the authored docs source tree. If output_dir lives under the repo root, gitignore it or call out the missing ignore rule as an operational gap. Build output may be regenerated in CI or deploy, but deploy must not author or revise docs content. Public deployments must be durable and socially credible. Do not treat a throwaway preview URL, unrelated personal domain, placeholder parent site, or sandbox subdomain as a completed public docs home. Do not encode open-ended critique or revision behavior. Critique is one bounded evaluation pass. Revision is at most one explicit bounded pass. diff --git a/docs/sourcey-catalog/site/llms.txt b/docs/sourcey-catalog/site/llms.txt new file mode 100644 index 000000000..5643f20d7 --- /dev/null +++ b/docs/sourcey-catalog/site/llms.txt @@ -0,0 +1,31 @@ +# Runx Governed Skill Catalog + +> Governed Runx skill catalog pinned to one upstream revision. + +## Skills + +- [Introduction](/runxhq/runx/pages/introduction.html): Governed Runx skill catalog pinned to one upstream revision. +- [agency](/runxhq/runx/pages/agency.html): Run a standing, accountable team toward a mandate, one governed turn at a time. +- [business-ops](/runxhq/runx/pages/business-ops.html): Turn one business signal into a replayable operations graph. +- [operator-inbox](/runxhq/runx/pages/operator-inbox.html): Maintain a durable action queue without turning a connector into the owner of operator state. +- [ops-desk](/runxhq/runx/pages/ops-desk.html): Operate a project, workspace, or account from an agent-controlled desk. +- [work-plan](/runxhq/runx/pages/work-plan.html): Turn a build or automation objective into a bounded governed work plan. +- [deep-research](/runxhq/runx/pages/deep-research.html): This graph turns one important question into a decision-ready brief. +- [research](/runxhq/runx/pages/research.html): Research one bounded question and turn it into a decision-ready packet. +- [data-store](/runxhq/runx/pages/data-store.html): Operate a data source through a governed adapter contract. This skill gives an agent enough context to read, append, or project state without learning provider secrets, inventing SQL, or depending on one storage backend. +- [knowledge-router](/runxhq/runx/pages/knowledge-router.html): Route one question, source event, or support thread to the right knowledge sources and follow-up path. +- [web-fetch](/runxhq/runx/pages/web-fetch.html): Fetch one URL, prove it was allowed, extract the part the caller asked for, and return that slice by digest with the provenance needed to trust it later. +- [github-sync](/runxhq/runx/pages/github-sync.html): Decide exactly what state to move between a GitHub repo and the local graph, in which direction, and whether the agent is even allowed to write. +- [issue-intake](/runxhq/runx/pages/issue-intake.html): Convert an inbound thread, support report, or operator request into one explicit intake decision plus the parent change artifact that downstream planning or mutation lanes must share. +- [issue-triage](/runxhq/runx/pages/issue-triage.html): Turn noisy issue streams into bounded, evidence-backed action. +- [issue-to-pr](/runxhq/runx/pages/issue-to-pr.html): Drive one bounded thread-driven change through the scafld 2.4-compatible lifecycle and package the result as a provider-agnostic draft pull-request packet. +- [release](/runxhq/runx/pages/release.html): Turn a proposed release into an audited publication. The skill owns the release decision process: evidence gathering, changelog preparation, approval, publish handoff, verification, and announcement. It does not own a project's custom release implementation. Project-specific topology lives in a release profile that names existing commands, workflows, registries, deploy targets, and verification readbacks. +- [audit-receipt](/runxhq/runx/pages/audit-receipt.html): Audit a sealed run for authority over-reach, using its own receipt as evidence. +- [cve-audit](/runxhq/runx/pages/cve-audit.html): This skill audits exact npm versions from an immutable `package-lock.json` against the public OSV API. It emits a machine-readable audit result, `evidence.json`, and a finding-by-finding Markdown report. The governed graph in `X.yaml` independently replays every query and seals a delivery packet only when the reported and replayed advisory sets match exactly. +- [least-privilege](/runxhq/runx/pages/least-privilege.html): Turn granted authority plus observed usage into a bounded attenuation proposal. +- [policy-author](/runxhq/runx/pages/policy-author.html): Author one governed runx operational policy from intent, and prove it lints. +- [review-receipt](/runxhq/runx/pages/review-receipt.html): Diagnose what went wrong in a skill or graph execution and propose the smallest change that fixes it. +- [sandbox-harden](/runxhq/runx/pages/sandbox-harden.html): Decide the narrowest sandbox a workload can run inside without breaking it. +- [governed-outbound](/runxhq/runx/pages/governed-outbound.html): Take something from outside, make it safe to send, authorize the exact outbound plan, and leave proof. `governed-outbound` prepares the boundary crossing; it does not claim the configured provider delivered anything. +- [run-history](/runxhq/runx/pages/run-history.html): Turn runx's own run ledger into a governed, read-only report. +- [sourcey](/runxhq/runx/pages/sourcey.html): Generate a documentation site for a project using Sourcey. Sourcey is a static documentation generator that produces HTML sites from markdown pages, OpenAPI specs, Doxygen XML, and MCP server snapshots. diff --git a/docs/sourcey-catalog/site/pages/agency.html b/docs/sourcey-catalog/site/pages/agency.html new file mode 100644 index 000000000..4165b030f --- /dev/null +++ b/docs/sourcey-catalog/site/pages/agency.html @@ -0,0 +1,149 @@ + +agency - Runx Governed Skill Catalog
Operate

agency

Run a standing, accountable team toward a mandate, one governed turn at a time.
    +
  • Group: Operate
  • +
  • Source: skills/agency/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/agency/SKILL.md
  • +
+

Agency

+

Run a standing, accountable team toward a mandate, one governed turn at a time.

+

An agency is the only runx skill that holds a roster, a persistent objective, and a +case that spans turns. It is a governed delegation envelope: a defined set of members +with scope ceilings, a mandate, cumulative limits, and a case whose every turn is +sealed and replayable. It composes the existing skills and reimplements none. Each +turn borrows ops-desk for judgment, the roster members for execution, data-store +for the event log, and receipts for the ledger.

+

It is not a durable-execution engine and it is not an autonomous daemon. One turn is +one stateless governed act; an external driver (a human, a cron, a board poll) runs +the loop by calling advance until the case resolves.

+

What this skill does

+
    +
  • open starts a case: it appends opened with the mandate, the roster, and the +cumulative limits snapshot, so the charter travels with the case.
  • +
  • advance runs one turn: it folds the case from its event stream, asks ops-desk +for the single next move constrained to the roster, enforces the measurable gate, +records one turn event whose append is the contention lease, and names the member +to run. The member runs as a separate governed run; its outcome is fed back to the +next advance as member_result.
  • +
  • status folds and returns the current case state.
  • +
+

The case reducer is the agency's own code, because data-store carries events but +does not fold domain state. Everything else is delegation.

+

When to use this skill

+
    +
  • A standing, consequential mandate must run for days or weeks, dispatch different +members, and leave an auditable trail sealed to a bounded authority.
  • +
  • A process needs scoped delegation with a measurable ceiling and a human gate on +consequence, not an unbounded agent.
  • +
+

When not to use this skill

+
    +
  • One-shot or interactive work. Call the member skills directly; the agency is +overhead when the operator is already the loop.
  • +
  • To compute proposals (that is ops-desk) or claim and clock logic (that is +messageboard). Compose them.
  • +
  • To bake a storage backend. The case lives in data-store via data_source_ref.
  • +
  • To let the model invent the roster, the mandate, or the limits. They are operator +config, snapshotted into the case at open.
  • +
+

Procedure

+
    +
  1. open the case with the mandate, roster, and limits.
  2. +
  3. advance the case. Read the turn packet:
      +
    • advanced: run the named member under its scope, then advance again with the +member's outcome as member_result.
    • +
    • awaiting_approval: resolve the escalation, then advance.
    • +
    • resolved or failed: the case is closed.
    • +
    +
  4. +
  5. Repeat until the case resolves. The driver, not this skill, decides the cadence.
  6. +
+

The measurable gate

+

The done-check and the limit-check are measurable first. advance folds cumulative +totals (acts, spend) and the trusted planner overrides the model when a cap is +breached: an over-cap turn fails regardless of what ops-desk proposed. The narrative +judgment from ops-desk chooses the move within the caps; it never widens them. +Spend caps tracked in the projection are the v1 path; routing spend through spend +and runx-pay reservations is the stronger enforcement.

+

Contention

+

Two drivers must not double-fire a member act. Each turn appends a single event keyed +case_id:turn:driver_id at the folded expected_version. Two drivers racing the same +turn carry different keys, so the loser hits a hard version conflict rather than +replaying the winner, and stops before any dispatch. The append is the lease, and it +lands before the named member runs.

+

Edge cases and stop conditions

+
    +
  • No case at case_id: advance returns needs_input; open the case first.
  • +
  • A cumulative cap is reached: the turn is failed with the breached predicate named.
  • +
  • The best move is consequential and unapproved: awaiting_approval with the prompt.
  • +
  • No roster member can act and nothing is escalatable: escalate to the configured +human with the missing input named.
  • +
+

Output schema

+

advance returns one agency_turn:

+
+
+ +
+
agency_turn:
+  schema: runx.agency.turn.v1
+  status: advanced | awaiting_approval | resolved | needs_input | failed
+  case_id: string
+  turn: number
+  dispatch:                 # present when status == advanced
+    member: string
+    skill: string
+    task: string
+    needed_scope: [string]
+  approval_prompt: string | null
+  resolution: object | null
+  predicates: object        # the measurable over_limits booleans
+  reason: string | null
+  next: string
+

Inputs

+
    +
  • open: data_source_ref, case_id, agency_ref, mandate, roster, limits, +optional signal.
  • +
  • advance: data_source_ref, case_id, driver_id, optional member_result.
  • +
  • status: data_source_ref, case_id.
  • +
+

Worked example

+

Open a docs case with a researcher, writer, and reviewer and a 50-turn limit. +advance folds an empty-but-opened case, ops-desk picks the researcher, and the turn +returns advanced naming the researcher. The driver runs the researcher and calls +advance again with its result; ops-desk now picks the writer to draft. When the +reviewer approves and the projection shows the docs current, advance returns +resolved.

+

Turn rules

+
    +
  • Fold every turn from the sealed stream; never infer state the events do not show.
  • +
  • Enforce the measurable gate before the model's judgment; never widen a cap.
  • +
  • Name the member and the verification expectation on every dispatch; never claim +work settled, sent, paid, or done without a receipt.
  • +
  • Compose ops-desk, data-store, and the members; never reimplement them, and never +invent the roster or the mandate.
  • +
  • Stop cleanly with needs_input, awaiting_approval, refused, or failed; never a fake +ready.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/audit-receipt.html b/docs/sourcey-catalog/site/pages/audit-receipt.html new file mode 100644 index 000000000..77f81b068 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/audit-receipt.html @@ -0,0 +1,160 @@ + +audit-receipt - Runx Governed Skill Catalog
Safety and review

audit-receipt

Audit a sealed run for authority over-reach, using its own receipt as evidence.
    +
  • Group: Safety and review
  • +
  • Source: skills/audit-receipt/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/audit-receipt/SKILL.md
  • +
+

Receipt Auditor

+

Audit a sealed run for authority over-reach, using its own receipt as evidence.

+

runx seals a receipt for every run: the authority proof, the acts performed, the +decisions taken, the refusals, and hashed material references. That receipt is +the evidence. This skill reads a sealed receipt and answers one governance +question: did the run stay inside the authority it was granted? It flags scopes +exercised that were never granted, mutating acts that ran without an approval +gate, refusals that were not recorded, and any raw secret material that leaked +into the receipt. It pairs with least-privilege: that one narrows a +grant from usage, this one verifies a run honored its grant.

+

What this skill does

+
    +
  1. Read the proof and the acts. From the receipt, extract the granted +authority (the proof) and the scopes the acts actually exercised.
  2. +
  3. Diff exercised against granted. Any exercised scope not covered by the +proof is over-reach.
  4. +
  5. Check the gates. Every mutating act must show an approval gate in the +receipt; an ungated mutation is an anomaly.
  6. +
  7. Check exposure. The receipt must carry only hashed material references; a +raw secret in the receipt is a leak.
  8. +
  9. Verdict. clean, anomaly, or needs_more_evidence, with the exact +findings and a recommendation for each anomaly.
  10. +
+

Core principles

+
    +
  • The receipt is the evidence. Audit what the receipt records, not what the +skill claims it did.
  • +
  • Granted is the ceiling. Exercised authority must be a subset of the proof; +anything beyond is over-reach, full stop.
  • +
  • Mutation needs a gate. A mutating act with no approval gate in the receipt +is an anomaly even if it succeeded.
  • +
  • No raw material. A receipt must reference material by hash; raw credential +material in a receipt is a leak, not a convenience.
  • +
  • Absence of evidence is not clean. With no receipt or an unattributable +one, return needs_more_evidence, never clean.
  • +
+

When to use this skill

+
    +
  • Post-run governance audit of a sealed, successful run.
  • +
  • Spot-checking that a skill honored its authority bound in production.
  • +
  • Before promoting a skill toward a higher trust posture.
  • +
+

When not to use this skill

+
    +
  • To diagnose a failed run and propose a fix. That is review-receipt +(failure-to-improvement). This skill audits a sealed run for over-reach +(success-to-governance); the two are different lenses on a receipt.
  • +
  • To narrow a grant from observed usage. That is least-privilege.
  • +
+

Diagnostics

+
    +
  • receipt.authority.over_reach (error): an exercised scope is not covered by +the authority proof.
  • +
  • receipt.mutation.ungated (error): a mutating act ran without an approval gate +recorded in the receipt.
  • +
  • receipt.refusal.unrecorded (warning): a denied request is not reflected as a +sealed refusal.
  • +
  • receipt.material.exposed (error): raw credential material appears in the +receipt instead of a hash reference.
  • +
  • receipt.clean (info): exercised authority is within the grant, mutations are +gated, and no material is exposed.
  • +
+

Procedure

+
    +
  1. Resolve the receipt from receipt_id or use the provided sanitized +receipt_summary.
  2. +
  3. Extract the authority proof, granted scopes, acts, approvals, refusals, +material references, and receipt signature metadata.
  4. +
  5. Normalize exercised scopes from the acts and compare them with the granted +scopes. Exercised must be a subset of granted.
  6. +
  7. Identify mutating acts and confirm each has an approval gate recorded in the +receipt.
  8. +
  9. Check that denied requests appear as sealed refusals when the receipt records +the attempt.
  10. +
  11. Scan receipt-visible material for raw credentials or secret-bearing payloads.
  12. +
  13. Return a verdict with findings, recommendations, and the success checkpoint.
  14. +
+

Edge cases and stop conditions

+
    +
  • Missing receipt: return needs_more_evidence; never infer a clean run.
  • +
  • Unattributable receipt: return needs_more_evidence when the receipt +cannot be tied to the run under audit.
  • +
  • Malformed proof: return needs_more_evidence unless enough normalized +grant data is supplied separately.
  • +
  • Unknown scope name: treat it as over-reach unless the grant explicitly +covers it.
  • +
  • Mutation without recorded gate: emit receipt.mutation.ungated even if the +mutation succeeded and the outcome looks correct.
  • +
  • Raw token, key, or credential in the receipt: emit +receipt.material.exposed and recommend revocation/rotation.
  • +
+

Output schema (receipt_audit)

+
+
+ +
+
decision: ready | needs_more_evidence
+run_ref: string
+granted_scopes: [string]
+exercised_scopes: [string]
+refusals: [string]
+findings:
+  - id: string
+    severity: error | warning | info
+    message: string
+verdict: clean | anomaly | needs_more_evidence
+rationale: string
+recommendations: [string]
+success_checkpoint:
+  milestone: string
+  description: string
+

A clean verdict requires zero error findings.

+

Worked example

+

A sealed run was granted repo.read. The receipt shows the acts exercised only +repo.read, every act is an observation (no mutation), and material is +referenced by hash. Exercised is a subset of granted, no mutation to gate, no +exposure: verdict: clean. Had an act exercised repo.write while the proof +granted only repo.read, that would raise receipt.authority.over_reach and a +verdict: anomaly with a recommendation to revoke the run's grant and +investigate.

+

Inputs

+
    +
  • receipt_id (optional): the receipt id to audit.
  • +
  • receipt_summary (optional): a sanitized receipt or its acts/proof summary +when the full receipt is not available.
  • +
  • granted_scopes (optional): the authority the run was granted, when not +derivable from the receipt alone.
  • +
  • objective (optional): operator intent that focuses the audit.
  • +
+

At least one of receipt_id or receipt_summary is required; with neither, the +skill returns needs_more_evidence.

+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/business-ops.html b/docs/sourcey-catalog/site/pages/business-ops.html new file mode 100644 index 000000000..705026038 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/business-ops.html @@ -0,0 +1,205 @@ + +business-ops - Runx Governed Skill Catalog
Operate

business-ops

Turn one business signal into a replayable operations graph.
    +
  • Group: Operate
  • +
  • Source: skills/business-ops/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/business-ops/SKILL.md
  • +
+

Business Ops

+

Turn one business signal into a replayable operations graph.

+

business-ops is the generic public example for how runx makes agentic +business work composable without giving the agent ambient authority. It is a +deterministic graph skeleton: it classifies one signal, fans it into bounded +lanes, records why each lane exists, names the real skill or provider lane that +would replace the fixture, and stops before any live send, spend, publish, +merge, deploy, or customer-visible action.

+

This is not a provider integration and not an operator dashboard. It is the +small core shape that teams copy when they want one objective to fan out into a +chain of skills, then replay that chain with receipts.

+

When the route itself should become durable, use route_and_append. That runner +classifies the signal, appends the classification packet through data-store, +and reads back the projection. The same graph can use local JSON, SQLite, +Postgres, D1, Redis, or a product adapter by changing the data_source_ref +binding.

+

What this skill does

+
    +
  • Classifies one business signal before doing work.
  • +
  • Fans the signal through representative lanes: docs, release, issue/PR, +outreach planning, spend quoting, and proof audit.
  • +
  • Produces structured lane packets with authority, gate, handoff, evidence, and +readback fields.
  • +
  • Demonstrates the runx split between proposal work and consequential action: +drafts and plans can be produced, but sends, spend, merges, publishes, and +deploys require a separate approval and execution lane.
  • +
  • Gives downstream agents a clear handoff target instead of vague prose.
  • +
  • Optionally persists the classified route for replay through data-store.
  • +
+

What this skill deliberately does not do

+
    +
  • It does not call private providers, mutate a repo, post to GitHub, send email, +schedule campaigns, move money, publish releases, or deploy services.
  • +
  • It does not duplicate ops-desk, product operator skills, send-as, +vendor-specific provider skills, release, issue-to-pr, spend, or +receipt-audit skills.
  • +
  • It does not turn "outbound marketing" into a hidden side effect. Outreach is +a plan lane here; real delivery routes to send-as and then a provider +adapter. Branded provider skills are concrete adapters, not branches in this +core graph.
  • +
  • It does not treat the graph receipt as proof that an external provider action +happened. Provider actions need provider evidence and their own receipt.
  • +
+

When to use this skill

+
    +
  • To show how runx chains skills into replayable business operations.
  • +
  • To prototype a team-specific ops graph before wiring private provider tools.
  • +
  • To route a product signal without giving the agent blanket repo, email, +wallet, or deployment access.
  • +
  • To explain why a governed workflow is more useful than a one-shot prompt: +the route, stops, handoffs, and readbacks are explicit and replayable.
  • +
  • To smoke-test graph execution and child receipts with no external account.
  • +
+

When not to use this skill

+
    +
  • To run a production launch, incident, release, campaign, support reply, +payout, or spend flow as-is. Replace fixture lanes with real skills first.
  • +
  • To approve a live send, spend, merge, publish, deploy, or customer-visible +action.
  • +
  • To hide project policy, customer lists, credentials, wallet keys, provider +dumps, or private review context in the signal.
  • +
  • To claim external work completed when only this fixture graph ran.
  • +
+

Mental model

+
+
+ +
+
signal -> classify -> fanout lanes -> approval stops -> governed handoffs -> proof
+

The useful part is the chain. A single objective becomes several typed packets: +some read-only, some draft-only, some blocked until approval, and one proof lane +that states how success should be verified later. A human, agent, dashboard, or +CI loop can replay the same route and see the same stops.

+

How this maps to real runx work

+
    +
  • Docs and public proof route to a docs skill such as sourcey or a +product-owned documentation lane.
  • +
  • Release preparation routes to release, with publish held behind a +release approval.
  • +
  • Code work routes to issue-to-pr or a project-owned implementation lane, +with merge held behind review.
  • +
  • Outreach and customer communication route first to send-as, then to a +provider adapter that implements the send lane. Branded provider skills are +the right place for vendor-specific compose, test, review, schedule, or send +details. Broad outbound marketing should be its own skill or product broadcast +skill, not extra logic hidden in this graph.
  • +
  • Spend and payments route to quote or payout skills with caps, recipient, +rail, and settlement proof separated from the planning lane.
  • +
  • Proof routes to receipt/history/audit skills and provider readbacks.
  • +
+

The fixture ops-lane step simply returns these packets without performing the +handoff. In a real project, replace each fixture lane with the named governed +skill runner or provider tool.

+

Procedure

+
    +
  1. Receive one concise signal.
  2. +
  3. Optionally receive operator_context with project constraints, policy, or +the concrete business situation.
  4. +
  5. Run classify first. It decides which lanes are relevant and what authority +class each lane belongs to.
  6. +
  7. Fan out docs, release, issue, outreach, spend, and proof packets.
  8. +
  9. Mark each lane as read-only, draft-only, approval-required, or proof-only.
  10. +
  11. Name the exact downstream handoff that should replace the fixture in a real +workflow.
  12. +
  13. Seal the graph so the route itself is replayable.
  14. +
  15. If using route_and_append, append the classification packet with an +idempotency key and expected version, then read back the projection.
  16. +
+

Edge cases and stop conditions

+
    +
  • Missing signal: return needs_input. There is no safe route.
  • +
  • Vague objective: return a narrow classify packet and ask for the missing +product, audience, repo, release, amount, or provider context.
  • +
  • Live send without principal, audience, consent, digest, and approval: stop +at the outreach lane and route to send-as.
  • +
  • Spend without amount, cap, recipient, rail, and approval: stop at the +spend lane and route to a quote or payment skill.
  • +
  • Merge, publish, deploy, or destructive mutation without approval: stop at +the relevant lane and name the missing gate.
  • +
  • Provider success without provider evidence: do not mark complete. Route to +proof audit.
  • +
  • Secret or private data in the signal: refuse to echo it into outputs; +require redacted context or a provider-side readback instead.
  • +
+

Output schema

+

The graph output contains child step receipts plus one lane_packet per lane:

+
+
+ +
+
lane_packet:
+  schema: runx.business_ops_lane.v1
+  lane: string
+  signal: string
+  status: ready | awaiting_approval | needs_input | refused
+  decision: route | prepare | draft | quote | verify | stop
+  kind: router | docs | release | work | outreach | spend | proof
+  consequence: read_only | draft | live_mutation | public_send | money_movement | proof
+  summary: string
+  why: string
+  authority:
+    requested: [string]
+    provided: fixture_only
+  gate:
+    approval_required: boolean
+    approval_gate: string | null
+    stop_reason: string | null
+  handoff:
+    interface: skill | graph | cli | hosted_api | workflow | provider_tool
+    lane_ref: string
+    runner_ref: string | null
+    command_hint: string | null
+  evidence:
+    inputs_required: [string]
+    readbacks: [string]
+    receipt_refs: [string]
+  risks: [string]
+  next: [string]
+

Worked example

+
+
+ +
+
runx skill business-ops \
+  -i signal="launch readiness for API v2: docs, release, customer comms, and spend checks" \
+  --json
+

The graph classifies the launch signal, prepares docs/release/work packets, +routes customer communication to an outreach plan, stops spend at a quote gate, +and names receipt/history checks that would prove later execution. No external +provider is called.

+

Inputs

+
    +
  • signal (required): concise business operations signal to classify and route.
  • +
  • operator_context (optional): product policy, project topology, audience +constraints, or known provider state. Context only, not authority.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/cve-audit.html b/docs/sourcey-catalog/site/pages/cve-audit.html new file mode 100644 index 000000000..2e3cbaf4b --- /dev/null +++ b/docs/sourcey-catalog/site/pages/cve-audit.html @@ -0,0 +1,132 @@ + +cve-audit - Runx Governed Skill Catalog
Safety and review

cve-audit

This skill audits exact npm versions from an immutable package-lock.json against the public OSV API. It emits a machine-readable audit result, evidence.json, and a finding-by-finding Markdown report. The governed graph in X.yaml independently replays every query and seals a delivery packet only when the reported and replayed advisory sets match exactly.
    +
  • Group: Safety and review
  • +
  • Source: skills/cve-audit/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/cve-audit/SKILL.md
  • +
+

Exact CVE Audit

+

What this skill does

+

This skill audits exact npm versions from an immutable package-lock.json +against the public OSV API. It emits a machine-readable audit result, +evidence.json, and a finding-by-finding Markdown report. The governed graph in +X.yaml independently replays every query and seals a delivery packet only +when the reported and replayed advisory sets match exactly.

+

It does not install dependencies, execute target code, mutate the target +repository, repair packages, or publish vulnerability claims.

+

When to use this skill

+

Use it when a reviewer needs checkable evidence for the advisories affecting +the exact npm versions in a public, immutable lockfile. It is suitable for +dependency review, release triage, and reproducible security evidence where a +package-name-only or loose-range match would create false positives.

+

When not to use this skill

+

Do not use it for a mutable branch URL, a manifest without exact installed +versions, a private lockfile without explicit read authority, non-npm +ecosystems, exploit development, or claims about transitive coverage when the +selected scope is direct-production. Do not treat missing OSV data as proof +that a package is safe.

+

Inputs

+
    +
  • target_name: display name for the audited project.
  • +
  • target_repo: public HTTPS source repository.
  • +
  • target_commit: full 40-character immutable Git commit.
  • +
  • lockfile_url: public HTTPS lockfile URL containing that commit.
  • +
  • dependency_scope: direct-production by default, or all-installed.
  • +
+

The caller authorizes only public reads of the pinned lockfile and OSV. No +credential, token, local project file, or private payload is an accepted input.

+

Procedure

+
    +
  1. Validate that the repository and lockfile URLs use HTTPS, the commit is a +full Git hash, and the lockfile URL is pinned to that hash.
  2. +
  3. Fetch the lockfile and record its SHA-256 digest before parsing it.
  4. +
  5. Extract exact installed versions from lockfile versions 2 or 3. Preserve the +requested scope in every artifact.
  6. +
  7. Query OSV with { ecosystem: npm, package, version } for every inventory +entry. Exclude withdrawn advisories.
  8. +
  9. Record each finding with dependency, exact version, advisory ID, OSV URL, +aliases, installed path, and the exact query that produced it.
  10. +
  11. In the governed graph, replay every exact-version query in a separate step. +Fail closed if an advisory is unsupported, omitted, withdrawn, or changed.
  12. +
  13. Emit the report, evidence, verification, delivery packet, and sealed receipt.
  14. +
+

Edge cases and stop conditions

+
    +
  • Refuse non-HTTPS or mutable lockfile URLs.
  • +
  • Refuse abbreviated commits and lockfiles that do not contain the commit.
  • +
  • Refuse unsupported lockfile versions or entries without exact versions.
  • +
  • Return needs_input when the target, commit, or lockfile evidence is not +immutable enough to support reproducible review.
  • +
  • Stop if OSV times out, returns malformed data, or changes between scan and +independent replay.
  • +
  • Stop if any false hit or missing hit is detected.
  • +
  • Report the selected dependency scope; never imply broader coverage.
  • +
  • Keep tokens, credentials, private source, and target code out of artifacts.
  • +
+

Output schema

+

The scan emits:

+
+
+ +
+
{
+  "audit_result": {
+    "schema": "runx.security.exact_cve_audit.v1",
+    "target": {
+      "repo": "https://github.com/owner/repo",
+      "commit": "40-character hash",
+      "lockfile_sha256": "64-character digest"
+    },
+    "dependency_scope": "direct-production",
+    "inventory": [],
+    "findings": [],
+    "result": {
+      "exact_dependencies_queried": 0,
+      "advisory_findings": 0,
+      "source": "OSV"
+    }
+  },
+  "report": { "artifact": { "path": "artifacts/report.md" } },
+  "evidence": {
+    "summary": "human-readable audit summary",
+    "observations": [],
+    "artifact": { "path": "artifacts/evidence.json" }
+  }
+}
+

The graph adds verification.json, delivery.json, and a sealed +runx.receipt.v1 graph receipt with child receipt lineage.

+

Worked example

+

For OWASP NodeGoat at commit +c5cb68a7084e4ae7dcc60e6a98768720a81841e8, the checked-in harness reads the +pinned lockfile, audits 16 exact direct production versions, reports the OSV +advisories returned for those versions, and independently requires zero false +and zero missing hits before sealing.

+

The second harness case audits the repository's immutable clean fixture at +commit 83d554f904f9f73bd26ce9c15e3786ba7d62b1de. It verifies that an exact +dependency with no OSV advisory produces zero findings while still completing +the independent replay and sealed-receipt path.

+

If the same request points at main/package-lock.json, validation returns a +needs-input failure because the lockfile is mutable and does not contain the +declared immutable commit. No audit or receipt is presented as complete.

+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/data-store.html b/docs/sourcey-catalog/site/pages/data-store.html new file mode 100644 index 000000000..646c61a2b --- /dev/null +++ b/docs/sourcey-catalog/site/pages/data-store.html @@ -0,0 +1,300 @@ + +data-store - Runx Governed Skill Catalog
Research and data

data-store

Operate a data source through a governed adapter contract. This skill gives an agent enough context to read, append, or project state without learning provider secrets, inventing SQL, or depending on one storage backend.
    +
  • Group: Research and data
  • +
  • Source: skills/data-store/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/data-store/SKILL.md
  • +
+

Data Store

+

Operate a data source through a governed adapter contract. This skill gives an +agent enough context to read, append, or project state without learning provider +secrets, inventing SQL, or depending on one storage backend.

+

The storage backend can be Postgres, SQLite, D1, Redis, DynamoDB, S3, a ledger, +or a product API. The runx boundary is the same: a declared data source exposes +typed operations; the graph supplies bounded params; the adapter executes the +operation; the receipt records the resource, authority, idempotency, version, +digest, and redaction evidence.

+

Adapter selection

+

The operator chooses a data source at run time. The skill receives +data_source_ref and operation inputs; project or hosted configuration binds +that ref to the concrete adapter. A local development ref might be +local://runx-data-store/dev-board. A production ref might be +tenant://acme/board bound to data.postgres, data.d1, data.redis, or a +product-owned HTTP adapter.

+

Do not put provider logic in the domain skill. Messageboard, CRM, support, and +business-ops skills should ask for durable facts to be read or written; the data +source binding decides whether those facts live in local JSON, SQL, Redis, D1, +object storage, or a product API. Switching providers is a binding change, not a +rewrite of the skill.

+

The bundled OSS profile calls data.source. Unbound local://... refs default +to durable local SQLite under .runx/data/local-sources/, with one source-scoped +database file per logical ref, so stateful skills can be dogfooded without +standing up hosted infrastructure. Pass store_id only when a fixture +intentionally wants the deterministic data.local JSON store. The graph inputs +stay the same when a project later binds the source to Postgres, Redis, D1, +object storage, or a product API.

+

Adapter preference is operator configuration, not model choice. To choose Redis, +SQLite, or a hosted provider, bind the same data_source_ref through +RUNX_DATA_SOURCES or .runx/data-sources.json; do not add provider branches to +the domain skill.

+

What this skill does

+
    +
  • Reads data through named queries or read operations declared by a data-source +adapter.
  • +
  • Appends state transitions with idempotency keys and expected versions.
  • +
  • Reads projections, event streams, or bounded latest-stream-head pages so +loops can resume from explicit state without exporting full history.
  • +
  • Produces receipt-bound evidence for data source, resource, operation, params, +row/event limits, versions, and output digests.
  • +
  • Keeps product semantics outside the data layer. Messageboards, CRMs, billing +ledgers, and support desks define their own events and reducers.
  • +
  • Ships a fixture adapter (data.local), durable local SQLite adapter +(data.sqlite), and Redis adapter (data.redis) behind the same operation +envelope.
  • +
+

When to use this skill

+
    +
  • A graph needs durable state between turns, such as queue position, board +state, sync cursor, review status, or approval inbox state.
  • +
  • A skill must query a bounded slice of product data before deciding the next +action.
  • +
  • A workflow needs to append an auditable event or effect transition with +optimistic concurrency.
  • +
  • An operator wants one provider-agnostic shape that can later move from local +JSON or SQLite to Postgres, Redis, D1, Supabase, Turso, DynamoDB, or another +store.
  • +
+

When not to use this skill

+
    +
  • To let a model write arbitrary SQL, Redis commands, or database migrations.
  • +
  • To export broad data sets, secrets, raw PII, or unrestricted tables.
  • +
  • To hide product decisions in storage code. Domain skills still own state +machines, acceptance criteria, and business rules.
  • +
  • To treat a projection as independent truth when the event stream or receipt +chain is available and required for review.
  • +
  • To bypass payment, send, deploy, moderation, or human approval gates.
  • +
+

Procedure

+
    +
  1. Identify the domain skill and transition first. The data store is a carrier, +not the policy owner.
  2. +
  3. Select the logical data source. Use data_source_ref to name the project or +tenant source; let the project binding choose the adapter. Do not put raw +database URLs, provider credentials, or SQL in the skill input.
  4. +
  5. Select a declared operation: named read query, append event, read events, +read projection, or list stream heads. Do not synthesize raw provider +commands.
  6. +
  7. Check authority. Reads need the narrow resource/query scope; writes need the +transition scope, idempotency key, and expected version unless the operation +is explicitly append-only without concurrency.
  8. +
  9. Bind typed params. Enforce row/event limits, tenant/partition keys, and +redaction rules before the adapter runs.
  10. +
  11. For writes, use optimistic concurrency and idempotency. A retry with the same +idempotency key and same payload returns the existing effect; a different +payload under the same key is a conflict.
  12. +
  13. Return the operation result with resource refs, version movement, digests, +redaction notes, and stop conditions. Receipts should link this data effect +to the domain transition that caused it.
  14. +
+

Edge cases and stop conditions

+
    +
  • needs_source: the data source, resource, query name, tenant key, or schema +summary is missing.
  • +
  • needs_input: required operation params are incomplete, malformed, or not +specific enough to bind a declared data-source operation.
  • +
  • needs_authority: the caller lacks the declared read/write scope or provider +grant.
  • +
  • needs_version: a mutating operation lacks expected_version where the data +source requires optimistic concurrency.
  • +
  • conflict: the current version differs from expected_version, or an +idempotency key is reused with different content.
  • +
  • too_broad: the requested read lacks partition filters, exceeds limits, or +asks for raw export.
  • +
  • redaction_required: the operation would return secrets, private PII, or +fields outside the declared projection.
  • +
  • provider_unavailable: the adapter cannot reach the data source, times out, +or cannot prove whether a write committed.
  • +
+

Output schema

+

All runners return runx.data.operation_result.v1:

+
+
+ +
+
{
+  "schema": "runx.data.operation_result.v1",
+  "data_source_ref": "local://example",
+  "provider": "local-json-event-store",
+  "operation": "append_event",
+  "resource": "board_events",
+  "aggregate_id": "posting-123",
+  "status": "committed",
+  "before_version": 0,
+  "after_version": 1,
+  "idempotency_key": "posting-123:create",
+  "event_ref": "board_events:posting-123:1",
+  "result_digest": "sha256:...",
+  "projection_digest": "sha256:...",
+  "rows": [],
+  "events": [],
+  "redactions": [],
+  "stop_conditions": []
+}
+

Provider adapters may add provider evidence under provider_evidence, but they +must not expose credentials or raw secret material.

+

For event streams, adapters derive a readable event_type in this order: +explicit event.type, explicit event.event_type, then +event.effect_family + "." + event.operation. Domain skills that emit the +generic runx.effect.transition.v1 packet should include effect_family and +operation on every event so readback projections say messageboard.accept, +business_ops.route, or another meaningful transition instead of data.event.

+

Worked example

+

A messageboard skill decides that posting.claimed is allowed. It emits a +domain transition packet. The graph then calls data-store.append_event with +resource board_events, aggregate id posting-123, expected version 2, and +idempotency key posting-123:claim:agent-9. The data adapter appends the event +only if the stream is still at version 2. The receipt proves the decision, +the data operation, and the new version. A later loop turn calls +data-store.read_events or read_projection to resume from the explicit board +state.

+

Inputs

+
    +
  • data_source_ref (required): stable logical ref for the data source. The +project or hosted binding maps this ref to the concrete adapter and provider +profile.
  • +
  • resource (required): declared resource, stream, table, keyspace, or +projection name.
  • +
  • operation (required for tool-level use): append_event, read_events, +read_projection, or list_stream_heads.
  • +
  • aggregate_id (required for event operations): stream or partition key.
  • +
  • event (required for append_event): domain event or transition packet.
  • +
  • idempotency_key (required for writes): stable retry key.
  • +
  • expected_version (required when the source enforces concurrency): current +stream/resource version expected by the caller.
  • +
  • limit (optional): maximum rows or events to return.
  • +
  • after_version (optional for read_events): return an ascending page whose +event versions are strictly greater than this value. Omit it to retain the +existing latest-tail read. Compare the last returned event version with +after_version in the result envelope to know whether another page remains.
  • +
  • event_types (optional for list_stream_heads): at most 20 exact latest +event types. No pattern or arbitrary field queries are accepted.
  • +
  • cursor (optional for list_stream_heads): opaque cursor returned by the +previous page. Limits are capped at 100.
  • +
  • store_id (local fixture adapter only): deterministic local store id that +opts into the bundled data.local proof adapter. Omit it for durable local +SQLite. Production adapters should ignore it.
  • +
+

Invocation examples

+

Durable local dogfood with the bundled default:

+
+
+ +
+
runx skill data-store append_event \
+  -i data_source_ref=local://runx-data-store/dev-board \
+  -i resource=board_events \
+  -i aggregate_id=posting-123 \
+  --input-json expected_version=0 \
+  -i idempotency_key=posting-123:create:v1 \
+  --input-json event='{"type":"posting.created","payload":{"title":"verify a receipt link"}}' \
+  --json
+

Fixture-only dogfood can still use store_id to select the JSON fixture store:

+
+
+ +
+
runx skill data-store append_event \
+  -i data_source_ref=local://runx-data-store/dev-board \
+  -i store_id=dev-board \
+  -i resource=board_events \
+  -i aggregate_id=posting-123 \
+  --input-json expected_version=0 \
+  -i idempotency_key=posting-123:create:v1 \
+  --input-json event='{"type":"posting.created","payload":{"title":"fixture proof"}}' \
+  --json
+

Production graph shape is the same at the skill boundary:

+
+
+ +
+
runx skill data-store append_event \
+  -i data_source_ref=tenant://acme/board \
+  -i resource=board_events \
+  -i aggregate_id=posting-123 \
+  --input-json expected_version=2 \
+  -i idempotency_key=posting-123:claim:agent-9 \
+  --input-json event='{"type":"posting.claimed","payload":{"actor":"agent-9"}}' \
+  --json
+

The second command only works once tenant://acme/board is bound to an +installed provider adapter. That binding is operator configuration and may name a +credential profile or hosted grant; it must not carry raw secrets.

+

Project-specific SQLite uses the same command shape after binding the source:

+
+
+ +
+
{
+  "data_sources": {
+    "tenant://acme/board": {
+      "adapter": "data.sqlite",
+      "database_path": ".runx/data/acme-board.sqlite",
+      "resources": {
+        "board_events": {
+          "kind": "event_stream",
+          "partition_key": "aggregate_id"
+        }
+      }
+    }
+  }
+}
+

Pass that document through RUNX_DATA_SOURCES or .runx/data-sources.json.

+

Redis uses the same skill and graph inputs. Only the binding changes:

+
+
+ +
+
{
+  "data_sources": {
+    "tenant://acme/board": {
+      "adapter": "data.redis",
+      "endpoint": "redis://127.0.0.1:6379/0",
+      "key_prefix": "runx:{acme-board}",
+      "resources": {
+        "board_events": {
+          "kind": "event_stream",
+          "partition_key": "aggregate_id"
+        }
+      }
+    }
+  }
+}
+

The Redis endpoint must not embed credentials. Use local unauthenticated Redis +for OSS dogfood, or put production secrets behind a runx credential profile or +hosted grant. For Redis Cluster, the binding's key_prefix must contain one +safe hash tag, such as {acme-board}, so the stream, idempotency, and head keys +touched by an append share one slot and update atomically. Stream-head pages use +stable keyset cursors rather than mutable offsets. Durable events and +dispositions must not receive TTLs; production Redis should enable persistence +and use a non-evicting policy.

+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/deep-research.html b/docs/sourcey-catalog/site/pages/deep-research.html new file mode 100644 index 000000000..8f51e2b0e --- /dev/null +++ b/docs/sourcey-catalog/site/pages/deep-research.html @@ -0,0 +1,59 @@ + +deep-research - Runx Governed Skill Catalog
Research and data

deep-research

This graph turns one important question into a decision-ready brief.
    +
  • Group: Research and data
  • +
  • Source: skills/deep-research/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/deep-research/SKILL.md
  • +
+

Deep Research Brief

+

This graph turns one important question into a decision-ready brief.

+

It is for research that needs more than a quick answer but less than an open- +ended report. The output should feel like an operator memo: what the answer is, +what evidence supports it, what remains uncertain, and what posture the reader +should take next.

+

Do not drift into a generic article, daily update, or trend recap. The point is +to help a human decide, not to narrate that research happened.

+

Separate verified evidence from inference and carry unresolved questions into +the memo. The synthesis must say what the reader should monitor, do, defer, or +investigate next. Return needs_more_evidence when the packet cannot support a +recommendation, and not_worth_publishing when the answer is sound but does not +matter to the stated decision.

+

Output

+
    +
  • research_packet: bounded evidence, confidence, inference, and open questions.
  • +
  • brief_draft: the decision memo synthesized from that packet.
  • +
  • approval_decision: review of the exact brief and its remaining uncertainty.
  • +
  • publish_packet: approved brief and delivery metadata.
  • +
+

Inputs

+
    +
  • objective (optional): specific question the brief should answer.
  • +
  • audience (optional): primary reader for the memo.
  • +
  • channel (optional): final delivery channel; defaults to brief.
  • +
  • domain (optional): product, ecosystem, or market slice to bound the work.
  • +
  • operator_context (optional): local decision context or evaluation lens.
  • +
  • target_entities (optional): structured list of products, projects, +companies, or repos to keep in scope.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/github-sync.html b/docs/sourcey-catalog/site/pages/github-sync.html new file mode 100644 index 000000000..364bb7fff --- /dev/null +++ b/docs/sourcey-catalog/site/pages/github-sync.html @@ -0,0 +1,173 @@ + +github-sync - Runx Governed Skill Catalog
GitHub and delivery

github-sync

Decide exactly what state to move between a GitHub repo and the local graph, in which direction, and whether the agent is even allowed to write.
    +
  • Group: GitHub and delivery
  • +
  • Source: skills/github-sync/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/github-sync/SKILL.md
  • +
+

GitHub Sync

+

Decide exactly what state to move between a GitHub repo and the local graph, in +which direction, and whether the agent is even allowed to write.

+

github-sync is the generic repo state connector. It turns a loose request like +"sync the open issues" into a bounded plan that names the resources, the +direction, the scope, the records it will touch, and the point where the run +must stop for a human. A pull is observation and stays inside repo:read. A +push is mutation and never proceeds without an explicit repo:write grant and +human approval.

+

What this skill does

+

github-sync produces a sealed sync_plan: a scoped record of which GitHub +resources the run will pull or push, the scope it will use, the gates a write +must clear, and any blockers that stop the run cleanly. For a push it carries a +diff_summary described by digest and ref, never by raw body text, so a +reviewer can approve the shape of a change without leaking issue contents, +tokens, or PII into the plan or the receipt.

+

The plan binds direction to scope. pull is read-only and lists the resources +it will fetch. push enumerates the mutations by ref and digest, marks +approval_required: true, and refuses to proceed past planning when the run +lacks a repo:write grant.

+

This skill plans the sync; it does not perform the GitHub mutation itself. The +plan is the artifact a downstream adapter executes after the approval gate +clears. Planning and mutation stay on opposite sides of the gate so a review can +read intent before anything changes on the remote.

+

When a sync loop needs a durable cursor, use plan_and_append_cursor. That +runner reads the cursor projection through data-store, plans the bounded sync, +appends the plan as a cursor event, and reads back the projection. The storage +provider is selected by data_source_ref, not by GitHub-specific code.

+

When to use this skill

+
    +
  • An agent needs to fetch a bounded set of issues, threads, or PRs into the +local graph for triage or analysis.
  • +
  • An agent needs to mirror local state back to GitHub (reopen, label, comment, +close) and the operator wants the write shape reviewed before it lands.
  • +
  • A workflow must prove which repo, direction, and scope a sync used, with a +receipt that names the resources touched.
  • +
  • A review needs to distinguish a read-only pull from a write that crossed an +approval gate.
  • +
+

When not to use this skill

+

github-sync is the generic repo state connector. Reach for it when the job is +moving issue, thread, or PR state in or out, not authoring a change or composing +one comment.

+
    +
  • To drive a thread through spec, build, review, and a draft PR. Use +issue-to-pr, which governs the full issue-to-PR lane.
  • +
  • To draft one review comment on one PR. Use pr-review-note.
  • +
  • To push without a named repo and direction.
  • +
  • To carry raw issue bodies, comment text, access tokens, or contributor PII in +the plan or receipt. Reference them by digest, span, or ref only.
  • +
  • To bypass the human approval gate on any write.
  • +
+

Procedure

+
    +
  1. Resolve the target repo and confirm the run holds at least repo:read.
  2. +
  3. Read direction. pull is observation; push is mutation and changes the +gate posture.
  4. +
  5. Read resources. Bind the concrete set: issues, PRs, or threads, plus the +filters that bound it (state, label, author, range). An unbounded "all" +becomes a blocker until reconfirmed.
  6. +
  7. Read scope. A push requires scope: write backed by a real repo:write +grant. If a write is requested without that grant, stop and refuse rather +than downgrade to a silent pull.
  8. +
  9. For a pull, list resources_touched by ref and leave diff_summary empty.
  10. +
  11. For a push, build diff_summary as a list of intended mutations described +by ref and content digest, set gates.approval_required: true, and record +the approval reference once granted.
  12. +
  13. Record scope_used as the narrowest scope the plan actually needs.
  14. +
  15. Emit the smallest sync_plan an adapter can execute without widening +authority, and stop at the approval gate for any write.
  16. +
  17. For cursor-backed loops, read the cursor projection first, append one sync +plan event with an idempotency key and expected version, and read back the +projection before the next turn.
  18. +
+

Edge cases and stop conditions

+
    +
  • Missing repo or direction: return needs_agent; the sync target is +undefined.
  • +
  • Write requested without repo:write: the request is refused; never +downgrade it to a silent pull. The plan stays unexecutable.
  • +
  • Unbounded resource set: mark a blocker and require an explicit filter +before a push.
  • +
  • Approval absent or denied on a push: keep the decision blocked and the +plan unexecutable; do not emit an executable mutation plan.
  • +
  • Raw bodies, tokens, or PII in the resource payload: reference by digest +and ref; if redaction would remove the evidence needed to plan, return +needs_agent.
  • +
+

Output schema

+
+
+ +
+
sync_plan:
+  decision: ready | blocked | refused | needs_agent
+  repo: string                       # resolved owner/name target
+  direction: pull | push
+  resources_touched:                 # resources by ref; no raw bodies
+    - kind: issue | pr | thread
+      ref: string
+      selected_by: string            # the filter that selected it
+  diff_summary:                      # push only; empty for a pull
+    - ref: string
+      op: string
+      digest: string
+  scope_used: string                 # narrowest scope, e.g. repo:read or repo:write
+  gates:
+    approval_required: boolean       # true for any push
+    approval_ref: string             # set once the write is approved
+  blockers: array                    # conditions that must clear before execution
+

sync_plan is a composable object. Downstream skills read it as arbitrary JSON; +the fields above are the contract a reviewer and adapter rely on.

+

The receipt (runx.receipt.v1) carries the repo, direction, scope_used, the +resource refs touched, and the approval reference for a write. It carries no +issue bodies, comment text, tokens, or contributor PII; mutations appear as refs +and digests only. Default scope is repo:read and a pull never escalates; a +push needs an explicit repo:write grant plus human approval, so missing the +grant is a refusal and missing the approval keeps the plan blocked.

+

Worked example

+

Input: "Sync the open triage issues into the graph" on runxhq/runx, with +direction: pull, scope: read, and a filter of state:open label:triage.

+

Output: decision: ready; direction: pull; scope_used: repo:read; +resources_touched lists the two matched issues by ref and the filter that +selected each; diff_summary is empty and gates.approval_required is false. +No write grant is exercised and no approval gate is opened, because a pull is +pure observation. Had the same request asked to push labels without a +repo:write grant, the run would refuse instead of reading.

+

Cursor-backed loop:

+
+
+ +
+
read cursor -> plan bounded pull/push -> append sync plan event -> read cursor
+

The cursor event stores refs, filters, digests, and gate status. It does not +store raw issue bodies, OAuth tokens, or write payload secrets.

+

Inputs

+
    +
  • repo (required): target repository as owner/name.
  • +
  • direction (required): pull or push.
  • +
  • resources (required): structured selector for issues, prs, or threads +plus filters (state, label, author, range).
  • +
  • scope (required): read or write. A push needs write backed by a real +repo:write grant.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/governed-outbound.html b/docs/sourcey-catalog/site/pages/governed-outbound.html new file mode 100644 index 000000000..7d15fc654 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/governed-outbound.html @@ -0,0 +1,123 @@ + +governed-outbound - Runx Governed Skill Catalog
Outbound and tooling

governed-outbound

Take something from outside, make it safe to send, authorize the exact outbound plan, and leave proof. governed-outbound prepares the boundary crossing; it does not claim the configured provider delivered anything.
    +
  • Group: Outbound and tooling
  • +
  • Source: skills/governed-outbound/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/governed-outbound/SKILL.md
  • +
+

Governed Outbound

+

Take something from outside, make it safe to send, authorize the exact outbound +plan, and leave proof. governed-outbound prepares the boundary crossing; it +does not claim the configured provider delivered anything.

+

It composes four catalog skills into one governed run:

+
    +
  1. web-fetch gathers the source within an explicit host allowlist.
  2. +
  3. redact-pii scrubs personal data and returns a pass/hold verdict before any +of it can leave the boundary.
  4. +
  5. an approval gate holds the plan for a human, who sees the redaction verdict +and the residual risk, not the raw content.
  6. +
  7. send-as binds the scrubbed content, principal, audience, and provider lane +into an authorized plan.
  8. +
  9. sign-receipt seals the gather, scrub, approval, and plan. A separate +provider host must execute the plan, record delivery evidence, and read it +back before any caller may call the notification delivered.
  10. +
+

The point of the chain is the order. The scrub runs before authorization and the +human gate runs before send-as. This receipt proves preparation and authority, +not provider delivery.

+

What this skill does

+

governed-outbound is a graph, not a single agent step. Each hop is a real +catalog skill with its own scope, and authority narrows at every branch: +web-fetch may only reach the allowlisted host, redact-pii may only read the +fetched content, the approval gate authorizes the plan, and sign-receipt may +only append to the ledger. Personal data never reaches the channel or the +receipt; the content travels by digest, and the redaction report carries class +and span offsets, never the values it found.

+

When to use this skill

+
    +
  • An agent needs to prepare external information (an incident page, a changelog, +a status update, a thread) for a provider channel, and that information may +carry personal data.
  • +
  • A workflow must prove that the proposed outbound content was scrubbed and the +exact plan was approved before provider execution.
  • +
  • You want one receipt that links the source, the scrub verdict, the approval, +and the send plan.
  • +
+

When not to use this skill

+
    +
  • To post content that was authored in-house and carries no external data. Call +send-as directly, then use the configured provider host.
  • +
  • To gather a source with no intent to send it onward. Call web-fetch.
  • +
  • To deliver without a human in the loop. The approval gate is the point; a +send that needs no review does not need this chain.
  • +
  • To move money, change a repository, or unseal a secret. Those are other +governed lanes with their own gates.
  • +
+

How the chain is wired

+
    +
  • fetch-source reads url and allowlist from the run inputs and returns +fetch_result with the content digest and extracted text.
  • +
  • scrub-content takes fetch-source's extracted text as content, runs in +redact mode, and returns redaction_report with the ready / needs_review +/ blocked verdict, the detected spans, and redacted_digest.
  • +
  • approve-send shows the approver the redaction decision, the +residual_risk, and the redacted_digest, then records an approval decision.
  • +
  • plan-notice runs only when the approval is true and the redaction verdict +is ready; it plans the send of the scrubbed content to channel as +principal, naming the provider action a connector lane would run.
  • +
  • seal-run attests the run, binding the source digest and the redacted digest +as evidence, and appends the receipt to the ledger.
  • +
+

Edge cases and stop conditions

+
    +
  • No url or allowlist: the run returns needs_agent; there is nothing +to gather and no boundary to respect.
  • +
  • Host not allowlisted: web-fetch returns policy_denied and the chain +stops before anything is read.
  • +
  • Redaction not ready: a needs_review or blocked verdict fails the +send transition, so plan-notice never runs. Nothing leaves the boundary on a +hold verdict.
  • +
  • Approval denied or absent: the send transition is not satisfied and the +chain stops at the gate, scrubbed but unsent.
  • +
  • Provider delivery fails downstream: preserve this planning receipt, record +the provider failure in the executing host, and do not produce delivery +evidence or mark the action complete.
  • +
+

Output

+

The run seals to runx.receipt.v1, linking each step's packet: +fetch_result (source + digest), redaction_report (verdict + spans + redacted +digest), approval_decision (the gate), send_plan (the authorization), and the +attestation (the seal). send_plan is authorization, not delivery evidence. +The receipt proves the preparation path without reconstructing the personal data +that was removed along the way.

+

Inputs

+
    +
  • url (required): source to gather before preparing the notification.
  • +
  • allowlist (required): hosts web-fetch is permitted to reach.
  • +
  • channel (required): destination channel for the notification.
  • +
  • principal (required): principal the notification is sent as.
  • +
  • claim (optional): what the sealed attestation should assert about the run.
  • +
  • operator_context (optional): boundary, audience, or compliance context.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/introduction.html b/docs/sourcey-catalog/site/pages/introduction.html new file mode 100644 index 000000000..fbc29dae3 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/introduction.html @@ -0,0 +1,27 @@ + +Introduction - Runx Governed Skill Catalog
Introduction

Introduction

Governed Runx skill catalog pinned to one upstream revision.

Runx Governed Skill Catalog

+

This catalog covers exactly 24 governed Runx skills at upstream commit 5afc25a83edf1c1320df7ac0d78c36f1523b5677.

+

The skills are organized into five groups: Operate, Research and data, GitHub and delivery, Safety and review, and Outbound and tooling. Each catalog page links to its authoritative SKILL.md at the pinned commit.

+

This is a governed skill catalog, not a claim of complete Runx API coverage.

+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/issue-intake.html b/docs/sourcey-catalog/site/pages/issue-intake.html new file mode 100644 index 000000000..1eb0755d4 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/issue-intake.html @@ -0,0 +1,197 @@ + +issue-intake - Runx Governed Skill Catalog
GitHub and delivery

issue-intake

Convert an inbound thread, support report, or operator request into one explicit intake decision plus the parent change artifact that downstream planning or mutation lanes must share.
    +
  • Group: GitHub and delivery
  • +
  • Source: skills/issue-intake/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/issue-intake/SKILL.md
  • +
+

Issue Intake

+

Convert an inbound thread, support report, or operator request into one +explicit intake decision plus the parent change artifact that downstream +planning or mutation lanes must share.

+

This skill does not mutate code, open tickets, or publish replies directly. Its +job is to classify the report, summarize it, draft the next helpful response, +and recommend the next governed lane. That next lane must be explicit: +issue-to-pr, work-plan, reply-only, or manual-review.

+

In supervisor-style flows, issue-intake is also the commencement gate. It +decides whether work may start at all, whether the next step should stop at a +review comment first, and whether mutation is justified yet. A recommended lane +is not the same thing as build permission.

+

Use issue-to-pr only when the requested change is bounded enough for one +governed remediation lane. Use work-plan for larger or multi-step +work. Use reply-only when the right answer is guidance rather than mutation. +Use manual-review when the report is ambiguous, risky, or missing key context.

+

Ground category, severity, and routing in the visible request and supplied +product constraints. Put uncertainty in operator_notes instead of inventing +confidence. The suggested reply should sound like the project owner and lead +with the decision or next action, not read like a ticket macro.

+

Output Contract

+

intake_report must contain:

+
    +
  • category: one of bug, feature_request, docs, billing, account, +question, or other
  • +
  • severity: one of low, medium, high, or critical
  • +
  • summary: concise summary of the actual request or report
  • +
  • suggested_reply: a user-facing reply draft or operator handoff note
  • +
  • recommended_lane: issue-to-pr, work-plan, reply-only, or +manual-review
  • +
  • rationale: why that lane is the right next step
  • +
  • needs_human: boolean
  • +
  • operator_notes: array of caveats, missing context, or escalation notes
  • +
+

intake_report may also include supervisor-facing control fields:

+
    +
  • commence_decision: approve, hold, reject, or needs_human
  • +
  • action_decision: proceed_to_build, proceed_to_plan, +request_review, or stop
  • +
  • review_target: thread, outbox_entry, or none
  • +
  • review_comment: markdown comment body for the supervisor to post before the +next lane proceeds
  • +
+

When present, these fields mean:

+
    +
  • commence_decision gates whether the supervisor may start any downstream +work at all
  • +
  • action_decision=proceed_to_plan means the supervisor may open a planning +lane such as work-plan, but still may not start repo mutation
  • +
  • action_decision=request_review means the supervisor should post +review_comment to the chosen review_target and stop there until a later +approval or rerun authorizes mutation
  • +
  • review_target=outbox_entry only makes sense when a current +outbox entry already exists. If no draft change, message surface, or +other outbox entry exists yet, the supervisor should fall back to the +source thread and say that clearly in the posted comment
  • +
  • action_decision=proceed_to_plan should usually still result in a public +supervisor comment so the hold/plan decision is visible outside the raw +receipt stream
  • +
  • recommended_lane=issue-to-pr alone does not authorize a build lane
  • +
+

Always emit change_set alongside intake_report.

+

Also emit signal when a source event is admitted. signal must follow +runx.signal.v1 and carry the source reference, authenticity or trust level, +dedupe fingerprint, evidence references, and source-thread preview. This packet +is the portable world-before-action state that work-plan, issue-to-pr, +hosted queues, and source-thread projections preserve.

+

Close the Runx turn after intake is complete with terminal closure control +metadata: a disposition, stable reason code, and concise summary. Closure is +receipt control state, not part of the issue-intake artifact, and must not +pretend the recommended downstream lane has already executed.

+

When an adapter has provider context beyond the visible thread text, attach it +to signal.evidence_refs or a referenced artifact. Source adapters own +provider-specific fetching and redaction before calling this skill; this skill +only reasons over the supplied, reviewer-safe signal and artifacts.

+

Hydration is a gate, not a best-effort decoration. If supplied signal or +artifact metadata says provider context is still needed, do not select +action_decision=proceed_to_build. Use manual-review or request_review and +explain the missing adapter context in operator_notes. If provider context is +unavailable, use the remaining signal only when it is still concrete enough for +a bounded reply, plan, or PR; otherwise stop for human review.

+

The change_set is the parent artifact for any later planning or worker +fanout. It is what keeps multiple repo-scoped lanes aligned to one shared +objective.

+

change_set must contain:

+
    +
  • change_set_id
  • +
  • thread_locator
  • +
  • summary
  • +
  • category
  • +
  • severity
  • +
  • recommended_lane
  • +
  • commence_decision
  • +
  • action_decision
  • +
  • target_surfaces: array of objects with:
      +
    • surface: repo, product surface, or bounded target name
    • +
    • kind: one of repo, package, docs, support, or other
    • +
    • mutating: boolean
    • +
    • rationale: why this surface is implicated
    • +
    +
  • +
  • shared_invariants: array of constraints that all downstream lanes must +preserve
  • +
  • success_criteria: array of concrete outcomes that define success for the +whole change
  • +
  • outbox_entry (optional): current outbox entry for status +updates, replies, or draft-change refreshes when the caller already knows it
  • +
+

When recommended_lane=issue-to-pr, also include thread_change_request with:

+
    +
  • task_id
  • +
  • thread_title
  • +
  • thread_body
  • +
  • thread_locator
  • +
  • thread (optional)
  • +
  • outbox_entry (optional)
  • +
  • size: one of micro, small, medium, or large
  • +
  • risk: one of low, medium, or high
  • +
+

When recommended_lane=work-plan, also include +workspace_change_plan_request with:

+
    +
  • change_set_id
  • +
  • objective
  • +
  • project_context
  • +
  • thread_locator
  • +
  • thread (optional)
  • +
  • target_surfaces
  • +
  • shared_invariants
  • +
  • success_criteria
  • +
+

Do not emit both thread_change_request and workspace_change_plan_request for +the same report.

+

Prefer conservative routing:

+
    +
  • if the report is bounded and well-understood, use commence_decision=approve +and action_decision=proceed_to_build
  • +
  • if the next step should be planning instead of mutation, use +commence_decision=approve and action_decision=proceed_to_plan
  • +
  • if the likely next lane is clear but mutation or planning should wait for +maintainer confirmation, use commence_decision=approve and +action_decision=request_review
  • +
  • if the report is ambiguous, under-specified, or risky, use +commence_decision=hold or needs_human
  • +
+

Inputs

+
    +
  • thread_title: canonical thread title
  • +
  • thread_body: canonical thread body or request text
  • +
  • thread_locator (optional): canonical locator for the bounded thread, +such as an issue, chat thread, ticket, or local agent session
  • +
  • thread (optional): provider-backed thread for the current +thread
  • +
  • outbox_entry (optional): current outbox entry for replies, draft changes, +or refreshes
  • +
  • signal (optional): provider-neutral runx.signal.v1 observation gathered +by the source adapter before decision
  • +
  • product_context (optional): product-specific constraints or routing hints
  • +
  • operator_context (optional): maintainer or support posture guidance
  • +
  • source_event (optional): admitted Slack, Sentry, GitHub, file, API, or +other provider event. Consuming repos decide source filters before calling +this skill.
  • +
  • source_policy (optional): source admission and routing policy. Do not +hardcode channel names, Sentry projects, or owners in this skill.
  • +
  • operational_policy (optional): runx.operational_policy.v1 packet used by +downstream repo-changing lanes for source, target, runner, and source-thread +admission.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/issue-to-pr.html b/docs/sourcey-catalog/site/pages/issue-to-pr.html new file mode 100644 index 000000000..135b5eb85 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/issue-to-pr.html @@ -0,0 +1,197 @@ + +issue-to-pr - Runx Governed Skill Catalog
GitHub and delivery

issue-to-pr

Drive one bounded thread-driven change through the scafld 2.4-compatible lifecycle and package the result as a provider-agnostic draft pull-request packet.
    +
  • Group: GitHub and delivery
  • +
  • Source: skills/issue-to-pr/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/issue-to-pr/SKILL.md
  • +
+

Issue to PR

+

Drive one bounded thread-driven change through the scafld 2.4-compatible +lifecycle and package the result as a provider-agnostic draft pull-request +packet.

+

The graph separates cognition from mutation. Agent phases author the scafld +markdown spec and the bounded repo change bundle. Deterministic fs.write and +fs.write_bundle phases are the only places files are written to disk. scafld +owns the workflow kernel: plan, validate, approve, build_to_review, +status, review, complete, and handoff. runx owns the explicit authoring +boundaries, deterministic writes, receipts, and final outbox packaging.

+

Branch creation and provider PR mutation are outside scafld. The caller or +adapter prepares the branch, then passes the intended branch into this lane. +The lane records that branch in the draft PR packet, and the GitHub adapter +fails closed if the workspace checkout does not match it. The final +issue-to-pr-push-outbox step is the only provider push boundary.

+

Lifecycle

+

The graph runs:

+

scafld plan -> author markdown spec -> write spec -> read spec -> validate -> +approve -> read approved spec -> read declared files -> author fix bundle -> +write fix bundle -> build to review -> status -> read current branch -> review +-> complete -> final status -> handoff -> package draft PR outbox -> adapter +push.

+

There are no translation projection steps. scafld handoff is the human handoff +surface, build_to_review drives bounded native scafld build advances until +the task is review-ready, and scafld review is the native review boundary.

+

Thread Story

+

The lane should leave one coherent source-thread story, not a stream of every +internal event. The durable milestones are:

+
    +
  • source signal and the bounded request
  • +
  • accountable decision that a PR is justified
  • +
  • scafld spec approval and declared scope
  • +
  • build and validation result
  • +
  • adversarial review result
  • +
  • draft PR publication
  • +
  • human merge gate
  • +
  • final provider outcome when observed
  • +
+

Comments and PR bodies should summarize those gates with enough evidence for a +reviewer to act. They must not publish raw local paths, secrets, full command +dumps, or duplicate retry comments. User-facing labels should use plain terms +such as spec authoring, fix authoring, review, and human merge gate.

+

Spec Authoring Contract

+

The issue-to-pr-author-spec boundary must emit a full scafld +2.4-compatible markdown document, not YAML and not a reduced project brief.

+

The document must preserve front matter with:

+
    +
  • spec_version: '2.0'
  • +
  • task_id
  • +
  • created: ISO-8601 timestamp
  • +
  • updated: ISO-8601 timestamp
  • +
  • title: non-empty task title, normally thread_title
  • +
  • status: draft
  • +
  • harden_status: not_run
  • +
  • size: one of small, medium, or large
  • +
  • risk_level
  • +
+

The body must include the standard scafld 2 sections: Current State, Summary, +Context, Objectives, Scope, Dependencies, Assumptions, Touchpoints, Risks, +Acceptance, at least one Phase section, Rollback, Review, Self Eval, +Deviations, Metadata, Origin, Harden Rounds, and Planning Log.

+

The graph normalizes the front matter before writing the spec so current scafld +schema fields such as title and size stay deterministic even if the authoring +boundary omits or stales them.

+

All changed-file declarations must use concrete repo-relative paths in +backticks under Context / Files impacted and Phase / Changes. Do not declare +scafld-managed control-plane artifacts under .scafld/specs, +.scafld/reviews, .scafld/runs, or old .ai governance paths as repo-change +scope.

+

Documentation and process requests still need a concrete repo file. Prefer +existing docs surfaces supplied by repo_snapshot.existing_files or +repo_context, and declare at least one non-governance repo file for an +approved issue-to-pr lane. Do not leave the repo-change scope empty after the +decision layer has approved a PR.

+

Validation commands must run against the current workspace state after the fix +bundle is written. Do not depend on git history ranges such as HEAD~1 or +merge-base comparisons. Validation commands, when present, must be direct +repo-local checks such as test, lint, build, or file-content commands. Never use +runx runtime internals or graph/scafld/run.mjs as a validation command; +scafld is already the lifecycle runner around the task.

+

For any code change, the approved spec must declare at least one targeted +test/spec file in the changed-file scope and include at least one executable +validation command that exercises that target. This applies even when the source +thread does not explicitly request coverage; code PRs are not publishable from +this lane without targeted test/spec scope or grounded scafld validation +evidence. If the source thread asks for tests, specs, regression coverage, +focused coverage, or request/service coverage, the targeted coverage requirement +cannot be softened to a generic smoke check. If no existing test/spec path is +declared but the repository layout makes a conventional path inferable, declare +that new test/spec file. If no grounded test/spec path or command can be +inferred from the repo snapshot, stop with a missing-evidence reason instead of +publishing a code-only PR.

+

Preserve source-thread context in the spec's Summary, Origin, and Planning Log +so later PR packaging can explain why the lane ran and what evidence justified +the mutation.

+

Fix Authoring Contract

+

The issue-to-pr-apply-fix boundary must emit a bounded fix_bundle with +files: [{ path, contents }] for every repo file needed to satisfy the approved +spec. For documentation or process changes, the approved spec, source thread, +repo snapshot, repo context, and declared file contents are sufficient when they +identify a narrow edit.

+

When repo_snapshot.recommended_files contains concrete repo-relative files, +treat those files as actionable target evidence even if the generated spec is +worded conservatively. Read the recommended file and the nearest relevant test +or spec before blocking. If the source thread includes a runtime exception, +backtrace, failing command, or named behavior and the recommended file exists, +prefer the smallest conventional fix plus targeted regression coverage over an +empty bundle.

+

For any production code change, fix_bundle.files must include the smallest +production fix and a targeted test/spec file, even when the source request does +not explicitly ask for coverage. Do not publish a code-only fix bundle from this +lane. If the approved spec, source thread, or acceptance criteria asks for +tests, specs, regression coverage, focused coverage, or request/service +coverage, the targeted test/spec file must directly cover that requested +behavior. If no test file exists, create the narrow conventional test file when +the repository structure makes that path inferable; otherwise block with the +missing path and evidence reason.

+

If a declared file has exists: false and the approved spec intentionally +creates it, write the new file when the desired contents are inferable from the +spec and thread. Do not block solely because the file has no prior contents.

+

Return fix_bundle.status: blocked with files: [] only when no concrete +repo-relative target is declared, a required existing file cannot be read, or +the requested behavior cannot be inferred after inspecting the supplied target +files. The blocked reason must name the missing evidence and path because an +empty file bundle is a terminal policy denial before write-fix.

+

Inputs

+
    +
  • task_id: scafld task id.
  • +
  • thread_title: canonical title and default spec title.
  • +
  • thread_body: full thread body or request text when available.
  • +
  • thread_locator: canonical locator for the bounded thread.
  • +
  • thread: portable thread for the current signal surface.
  • +
  • outbox_entry: existing pull-request outbox entry when refreshing a draft.
  • +
  • harness: optional runx.harness.v1 packet for the governed run boundary.
  • +
  • signal: optional runx.signal.v1 packet. Preserve source references, +fingerprint, authenticity, and evidence references as stateful context +instead of reparsing source-thread prose.
  • +
  • decision: optional runx.decision.v1 packet. Preserve the accountable +selection rationale, selected act, and closure when the caller already made +the lane decision.
  • +
  • target_repo: intended repository slug for PR packaging.
  • +
  • operational_policy: optional runx.operational_policy.v1 packet used to +admit the source, target repo, runner, and source-thread route before PR +packaging.
  • +
  • source_id: optional operational policy source id.
  • +
  • runner_id: optional operational policy runner id.
  • +
  • repo_snapshot: compact structured snapshot of the target repo.
  • +
  • repo_snapshot_path: optional path to a fuller repo snapshot artifact.
  • +
  • repo_context: textual summary of repo shape and validation hooks.
  • +
  • size: scafld size, default small.
  • +
  • risk: scafld risk, default low.
  • +
  • base: base ref for PR packaging, default main.
  • +
  • fixture: workspace root containing .scafld.
  • +
  • scafld_bin: explicit scafld executable path.
  • +
  • provider, provider_command, provider_binary, model: optional native +scafld review provider overrides.
  • +
+

Structured Output

+

On success, the lane emits:

+
    +
  • draft_pull_request: provider-agnostic PR draft state derived from scafld +handoff, build, review, completion, status, and current git branch.
  • +
  • outbox_entry: a pull_request outbox entry suitable for adapter push.
  • +
  • push: adapter push result plus refreshed thread when the adapter supports +push.
  • +
  • Story metadata suitable for one source-thread reviewer update that summarizes +the lifecycle gates and points at the human merge decision.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/issue-triage.html b/docs/sourcey-catalog/site/pages/issue-triage.html new file mode 100644 index 000000000..a6e9dcdc1 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/issue-triage.html @@ -0,0 +1,69 @@ + +issue-triage - Runx Governed Skill Catalog
GitHub and delivery

issue-triage

Turn noisy issue streams into bounded, evidence-backed action.
    +
  • Group: GitHub and delivery
  • +
  • Source: skills/issue-triage/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/issue-triage/SKILL.md
  • +
+

Issue Triage

+

Turn noisy issue streams into bounded, evidence-backed action.

+

This skill is for issue selection and response drafting, not for silently +mutating repositories. Use it to identify which threads are worth attention, +understand the maintainer or contributor situation, and draft the next helpful +response or remediation path.

+

Separate discovery from response. Discovery finds the thread worth engaging. +Response drafting turns one chosen thread into a concrete answer, escalation, +or change plan.

+

Ground selection and response in the actual thread, repository facts, receipts, +and maintainer context; do not infer intent beyond what is visible. Lead with +the decision, answer, or next action in the project's own voice. Return +needs_more_evidence or needs_human when the thread is ambiguous, hostile, +underspecified, unsafe, or outside the maintainer's declared posture.

+

Output

+

Discovery runner:

+
    +
  • issue_candidates: candidate issues or discussions worth attention.
  • +
  • selection_rationale: why one candidate should be handled next.
  • +
  • operator_notes: constraints, caveats, or escalation triggers.
  • +
+

Response runner:

+
    +
  • issue_profile: concise summary of the chosen thread.
  • +
  • response_strategy: recommended response posture and next action.
  • +
  • response_draft: post-ready draft or maintainer handoff.
  • +
  • follow_up_actions: concrete next steps after the response.
  • +
+

Inputs

+
    +
  • repository (optional): repository slug or workspace reference.
  • +
  • query (optional): search or queue objective for discovery.
  • +
  • issue_url (optional): canonical issue URL for response drafting.
  • +
  • issue_snapshot (optional): structured issue data when already fetched.
  • +
  • maintainer_context (optional): project norms, release posture, and +response constraints.
  • +
  • operator_context (optional): operator-supplied context used by higher-level +triage graphs.
  • +
  • objective (optional): what the operator wants from this pass.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/knowledge-router.html b/docs/sourcey-catalog/site/pages/knowledge-router.html new file mode 100644 index 000000000..99a5fe7f3 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/knowledge-router.html @@ -0,0 +1,52 @@ + +knowledge-router - Runx Governed Skill Catalog
Research and data

knowledge-router

Route one question, source event, or support thread to the right knowledge sources and follow-up path.
    +
  • Group: Research and data
  • +
  • Source: skills/knowledge-router/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/knowledge-router/SKILL.md
  • +
+

Knowledge Router

+

Route one question, source event, or support thread to the right knowledge +sources and follow-up path.

+

This skill is for triage and routing, not answering the question directly. It +should tell a consuming graph where to look, who owns the domain, what evidence +is already available, and which next skill should run.

+

Each route must name the supplied signal that justified its source match, +owner, escalation, and next-skill recommendation. Keep the result as a concise +dispatch note. Return needs_more_context when no route is supportable, and +manual_review for legal, billing, security, or destructive requests.

+

Output

+
    +
  • route: selected knowledge or ownership domain and rationale.
  • +
  • source_matches: relevant sources with the matching signal.
  • +
  • owner_recommendation: owner or escalation target.
  • +
  • next_skill: the bounded follow-up capability, if one is justified.
  • +
+

Inputs

+
    +
  • question (required): user question, event, or thread summary to route.
  • +
  • available_sources (required): source catalog, docs, systems, or owner map.
  • +
  • constraints (optional): allowed systems, sensitivity, or preferred owner.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/least-privilege.html b/docs/sourcey-catalog/site/pages/least-privilege.html new file mode 100644 index 000000000..c6a999b13 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/least-privilege.html @@ -0,0 +1,248 @@ + +least-privilege - Runx Governed Skill Catalog
Safety and review

least-privilege

Turn granted authority plus observed usage into a bounded attenuation proposal.
    +
  • Group: Safety and review
  • +
  • Source: skills/least-privilege/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/least-privilege/SKILL.md
  • +
+

Least Privilege Auditor

+

Turn granted authority plus observed usage into a bounded attenuation proposal.

+

runx keeps a receipt of every scope a run actually exercised. This skill reads +that proof. It compares what a subject (a skill, a grant, or a principal) was +granted against what its receipts show it used, then proposes the narrowest +grant that still covers real usage. The output is a reviewable attenuation +proposal, not an automatic change.

+

What this skill does

+
    +
  1. Diff granted authority against receipt-backed usage.
  2. +
  3. Classify each granted scope as keep, narrow, remove, or defer.
  4. +
  5. Propose the narrowest grant that still covers observed usage.
  6. +
  7. State residual risk after attenuation.
  8. +
  9. Emit a receipt-quality report a reviewer can apply or reject.
  10. +
+

When to use this skill

+
    +
  • Periodic least-privilege review of a skill, grant, or principal before +publish, renewal, or maturity promotion.
  • +
  • After an incident, to identify authority that can be safely removed without +breaking observed behavior.
  • +
  • Before expanding distribution of a public skill, to prove its grant is +minimal against real receipts.
  • +
  • When a reviewer asks for a scope-by-scope evidence trail, not just a summary.
  • +
+

When not to use this skill

+
    +
  • To grant new authority. This skill only narrows; widening is a human +decision.
  • +
  • When no usable receipt evidence exists. Return needs_more_evidence rather +than guessing a grant down to nothing.
  • +
  • For secret material handling or credential exposure. Use the appropriate +secret-leak triage flow instead of scope review.
  • +
  • When the user asks for automatic permission changes. Produce a proposal and +stop unless a separate approved delivery lane exists.
  • +
  • When grant semantics are unknown and cannot be normalized. Return +needs_input with the exact syntax or policy question.
  • +
+

Procedure

+
    +
  1. Scope the audit target.

    +
      +
    • Identify subject, grant source, receipt ids or receipt window, and +whether receipts are from the same principal or skill version.
    • +
    • Gate: if the subject, grant list, or usage source is ambiguous, stop with +needs_input.
    • +
    • Evidence expected: subject id or label, granted scope list, receipt ids or +an explicit statement that no receipts were available.
    • +
    +
  2. +
  3. Normalize granted scopes.

    +
      +
    • Parse each scope into verb, resource, path or namespace, conditions, and +wildcard breadth.
    • +
    • Preserve original scope strings. Do not rewrite policy syntax casually.
    • +
    • Gate: if a scope cannot be parsed, keep it as defer and request the +missing policy semantics instead of treating it as unused.
    • +
    +
  4. +
  5. Build the usage model from receipts.

    +
      +
    • Extract actual exercised verbs and resources from receipt steps, tool +calls, policy checks, denied checks, and completion status.
    • +
    • Count successful use separately from denied or dry-run checks.
    • +
    • Do not infer scope usage from a successful high-level task alone; cite the +receipt step or policy check that exercised the authority.
    • +
    +
  6. +
  7. Classify every granted scope.

    +
      +
    • keep: at least one observed successful use requires the granted scope as +written, or a reserved/break-glass policy explicitly requires it.
    • +
    • narrow: all observed uses fit a strictly smaller verb, resource, +namespace, condition, or path.
    • +
    • remove: no observed use, denied check, or documented reserved purpose +supports the scope.
    • +
    • defer: evidence is conflicting, receipt attribution is weak, or policy +semantics are unknown.
    • +
    +
  8. +
  9. Propose attenuation.

    +
      +
    • Remove scopes classified as remove.
    • +
    • Downgrade scopes classified as narrow only when every observed use fits +the narrower grant.
    • +
    • Leave keep and defer scopes unchanged in the proposed grant.
    • +
    • Gate: never produce a proposal narrower than the evidence supports. A +scope used once is used.
    • +
    +
  10. +
  11. State residual risk and reviewer action.

    +
      +
    • Name what the proposed grant can still do.
    • +
    • Name any broad scope kept despite thin evidence and why.
    • +
    • Separate applyable now from needs human policy decision.
    • +
    +
  12. +
  13. Emit receipt expectations.

    +
      +
    • A valid receipt for this skill should record input grant count, receipt +sources, classification counts, proposed removals or narrowings, stop +status, and unresolved questions.
    • +
    +
  14. +
+

Edge cases and stop conditions

+
    +
  • Empty or unattributable usage evidence: return needs_more_evidence; do not +remove all scopes by default.
  • +
  • Missing granted scopes: return needs_input; there is no baseline to diff.
  • +
  • Receipt subject mismatch: return needs_input with the mismatched subject or +version.
  • +
  • Conflicting receipts: classify affected scopes as defer and return +needs_human if the conflict changes the proposal.
  • +
  • Wildcard grants: narrow only to observed resource prefixes when receipt +coverage is representative; otherwise keep and flag residual risk.
  • +
  • Reserved, compliance, or break-glass scopes: keep unless the operator +provides explicit policy authority to remove them.
  • +
  • Dry-run-only use: do not count as successful exercised authority unless the +grant exists solely for validation.
  • +
  • Grant already matches usage: return no_change with the evidence summary.
  • +
  • User asks to hide or omit unused authority: refuse that part and report the +complete scope diff.
  • +
+

Output schema

+

Return a structured report with these fields:

+
+
+ +
+
status: attenuation_proposed | no_change | needs_more_evidence | needs_input | needs_human | refused
+subject: string
+evidence:
+  receipt_ids: [string]
+  receipt_window: string | null
+  grant_source: string | null
+  limitations: [string]
+scope_diff:
+  - granted_scope: string
+    normalized:
+      verb: string | null
+      resource: string | null
+      conditions: object | null
+    observed_use:
+      count: number
+      verbs: [string]
+      resources: [string]
+      receipt_refs: [string]
+    classification: keep | narrow | remove | defer
+    proposal: string | null
+    rationale: string
+attenuated_grant: [string]
+removed_scopes: [string]
+narrowed_scopes:
+  - from: string
+    to: string
+kept_scopes: [string]
+deferred_scopes: [string]
+residual_risk: [string]
+reviewer_action: applyable_now | needs_policy_decision | gather_more_receipts | none
+receipt_expectations:
+  classification_counts: object
+  stop_status: string
+  unresolved_questions: [string]
+

Worked example

+

Input:

+
+
+ +
+
subject: skills/report-exporter
+granted_scopes:
+  - drive.files.read:/reports/*
+  - drive.files.write:/reports/*
+  - drive.files.delete:/reports/*
+usage_summary:
+  receipt_ids: [rx_101, rx_102]
+  observed:
+    - scope: drive.files.read:/reports/*
+      count: 8
+      refs: [rx_101:step_3, rx_102:step_2]
+    - scope: drive.files.write:/reports/*
+      count: 2
+      refs: [rx_101:step_6, rx_102:step_5]
+

Output:

+
+
+ +
+
status: attenuation_proposed
+subject: skills/report-exporter
+removed_scopes:
+  - drive.files.delete:/reports/*
+narrowed_scopes: []
+kept_scopes:
+  - drive.files.read:/reports/*
+  - drive.files.write:/reports/*
+attenuated_grant:
+  - drive.files.read:/reports/*
+  - drive.files.write:/reports/*
+residual_risk:
+  - The skill can still read and write any file under /reports/*.
+reviewer_action: applyable_now
+

The delete scope is removable because no cited receipt exercised delete +authority. The read and write scopes stay because each was used at least once.

+

Inputs

+
    +
  • subject (optional): skill id, grant id, principal, or other label for what +is being audited.
  • +
  • granted_scopes (required): the current scopes granted to the subject, +preferably in canonical policy syntax.
  • +
  • usage_summary (required): receipt-derived usage. Include receipt ids, step +refs, observed verbs, resources, success or denial status, and the time +window when available.
  • +
  • objective (optional): operator intent that focuses the review, such as +"prepare for public publish" or "post-incident attenuation".
  • +
  • policy_notes (optional): reserved scopes, compliance constraints, or +human-approved exceptions that affect removal decisions.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/operator-inbox.html b/docs/sourcey-catalog/site/pages/operator-inbox.html new file mode 100644 index 000000000..fbcb9bc18 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/operator-inbox.html @@ -0,0 +1,122 @@ + +operator-inbox - Runx Governed Skill Catalog
Operate

operator-inbox

Maintain a durable action queue without turning a connector into the owner of operator state.
+

Operator Inbox

+

Maintain a durable action queue without turning a connector into the owner of +operator state.

+

The caller fetches bounded, grant-authorized provider pages and passes their +normalized observations to this skill. The skill owns work-item identity, +status, dispositions, replay suppression, reopen rules, and scan coverage. Every +read and write is composed through data-store; the skill does not call Slack, +SQLite, Postgres, or another provider directly.

+

What this skill does

+

Use local://runx/operator-inbox/default unless the operator selects another +logical source. Unbound local refs resolve to SQLite under +.runx/data/local-sources/. A hosted database is opt-in through the same +data_source_ref binding. Runx Connect may still own OAuth, grants, and provider +execution; that does not move this queue into the hosted control plane.

+

Observations and resumable checkpoints live in operator_inbox_scans, partitioned +by query digest. Action snapshots live in operator_inbox_actions, with one +stream per stable thread digest. Queue reads use bounded list_stream_heads +pages; no command folds or transports the complete queue.

+

When to use this skill

+
    +
  • Build or revisit a local action queue from bounded connector observations.
  • +
  • Preserve an explicit resolved, dismissed, waiting, or followed_up +decision across repeated provider scans.
  • +
  • Reopen a completed item when a newer external occurrence arrives.
  • +
  • Inspect bounded action or scan state without handing queue ownership to the +provider or hosted control plane.
  • +
+

When not to use this skill

+
    +
  • Do not fetch provider data, reply, send, or mutate a remote account here.
  • +
  • Do not infer that an item is complete from message text or provider state.
  • +
  • Do not use it as an unbounded archive of raw messages or credentials.
  • +
  • Do not place a private operator's routing policy or identity in this public +package; pass normalized observations and explicit dispositions as inputs.
  • +
+

Status rules

+

Items use open, waiting, followed_up, resolved, or dismissed.

+
    +
  • Provider observations never infer completion.
  • +
  • A human disposition records actor, reason, time, the latest external +occurrence it covers, and optional HTTPS evidence.
  • +
  • Replaying old search history preserves the human status.
  • +
  • An external message newer than the covered occurrence reopens the item to +open, including unseen work that arrived before the disposition was saved.
  • +
  • Scan coverage is explicit: running, complete, truncated, or failed.
  • +
  • Direct mentions are actionable structural evidence. Author and keyword scans +remain observation-only unless the operator explicitly marks the query +actionable. The skill does not contain provider-specific keyword heuristics.
  • +
+

The provider-neutral thread locator is the item key. Stored previews are bounded; +credentials, tokens, and full provider response envelopes are forbidden.

+

Procedure

+
    +
  1. Read the latest checkpoint for the bounded query digest.
  2. +
  3. Resume its provider cursor when the prior scan was interrupted or truncated.
  4. +
  5. Fetch one bounded provider page through the caller's authorized connector.
  6. +
  7. Record actionable messages against their per-thread streams and append the +scan page with its next cursor.
  8. +
  9. On a version conflict, reload only the affected scan or action stream and +retry the idempotent transition.
  10. +
  11. List queue state through bounded action-head pages and use +record_disposition only for an explicit operator correction.
  12. +
+

The loop is outside the kernel. Each page or disposition remains one governed, +receipt-backed Runx turn.

+

Edge cases and stop conditions

+
    +
  • needs_input: missing query identity, observation, disposition, +actor, reason, or scan coverage.
  • +
  • conflict: the projection version is stale; reload before retrying.
  • +
  • provider_unavailable: the caller cannot prove provider read coverage.
  • +
  • too_broad: a page exceeds the bounded message count or contains unnormalized +provider data.
  • +
  • refused: a caller asks this skill to send, reply, broaden a grant, store a +token, or silently claim complete coverage.
  • +
+

Output schema

+

Write runners emit runx.effect.transition.v1, containing the effect family, +operation, expected projection version, idempotency key, and one normalized +event. Read and list runners return the corresponding bounded data-store +event result; they never synthesize provider coverage or completion.

+

Worked example

+

Given a normalized direct mention from a teammate in one provider thread, +record_action_observation derives the stable action id from the provider-neutral +thread locator and appends an open action snapshot. If the operator later +records resolved with a reason, replaying that mention preserves resolved; +a newer external reply in the same thread appends a reopened open snapshot.

+

Inputs

+

All runners require data_source_ref. Write runners also take the target id, +expected_version, and observed_at, plus exactly the normalized payload for +their operation: scan and messages, message and triage, disposition, +or an imported action. Reads take action_id or query_digest; list runners +take bounded limit and optional cursor or filter fields.

+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/ops-desk.html b/docs/sourcey-catalog/site/pages/ops-desk.html new file mode 100644 index 000000000..cd975249a --- /dev/null +++ b/docs/sourcey-catalog/site/pages/ops-desk.html @@ -0,0 +1,343 @@ + +ops-desk - Runx Governed Skill Catalog
Operate

ops-desk

Operate a project, workspace, or account from an agent-controlled desk.
    +
  • Group: Operate
  • +
  • Source: skills/ops-desk/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/ops-desk/SKILL.md
  • +
+

Ops Desk

+

Operate a project, workspace, or account from an agent-controlled desk.

+

This skill is the generic operations desk layer. It turns a state snapshot, an +operator objective, and receipt-backed evidence into one safe ops desk packet: +what is happening, what needs attention, what can be checked read-only, what +requires approval, which governed lane should execute, and how success will be +verified.

+

It is not the authority and it is not a second CLI. It does not replace +release, send-as, ledger, refund, spend, messageboard, +provider-specific adapter skills, hosted API routes, repository workflows, or +deploy commands. It routes to the existing interface with the smallest +sufficient context and stops before any consequential act that lacks the right +gate.

+

What this skill does

+

ops-desk produces an ops desk packet for a manager dashboard, agent +session, or self-operation run. It reads projected state, classifies findings, +ranks the next action, selects the governed lane, names blockers, writes the +approval prompt when a human decision is required, and states the +receipt/effect/readback that will prove success.

+

It is useful before an action and after an action:

+
    +
  • before action, it turns state into proposals and approval requests;
  • +
  • after action, it checks whether the expected receipt and projection appeared.
  • +
+

The model may diagnose and write the operator rationale. The mutation itself +must be a deterministic handoff to an existing skill runner, CLI command, hosted +API route, workflow, or provider tool.

+

When the desk should start from durable state, use operate_from_projection. +That runner reads a projection through data-store first, then passes the +projection as the dashboard snapshot. The storage provider is still selected by +the logical data_source_ref; ops desk does not know whether state came from +SQLite, Postgres, D1, Redis, or a product API.

+

When a standing case must be advanced one move at a time toward a mandate, use +advance. It takes the mandate, the current case_state, and a fixed +candidate_roster, and returns a single typed dispatch_decision: dispatch one +roster member, escalate, or done. It applies the same ranking and the same gates as +operate, but it is hard-constrained to the roster and emits one move instead of a +multi-proposal plan. The caller (an agency loop) holds the case and the goal; ops +desk supplies the judgment. The chosen member is named as data; ops desk never runs +it.

+

When to use this skill

+
    +
  • An operator asks an agent to manage a project, workspace, product, account, +or other bounded operating surface.
  • +
  • A dashboard needs an agent-readable plan from the current projected state.
  • +
  • A runbook needs to decide between read-only checks, proposals, approval-gated +actions, and post-action verification.
  • +
  • A product-specific operator skill needs a generic cockpit spine instead of +inventing its own action model.
  • +
  • A standing case (an agency) needs the single next governed move chosen from a +fixed roster, one turn at a time.
  • +
  • Runx needs to dogfood its own release, registry, hosted, receipt, or provider +operations through the same governed lanes it exposes to users.
  • +
+

When not to use this skill

+
    +
  • To execute a live mutation directly. Route to the named governed lane.
  • +
  • To duplicate a CLI command, release script, GitHub workflow, hosted endpoint, +registry client, or provider SDK.
  • +
  • To bypass a human gate because the agent or UI believes the action is obvious.
  • +
  • To replace a domain skill such as send-as, messageboard, release, +ledger, refund, spend, least-privilege, or a provider +adapter.
  • +
  • To operate from stale, missing, or unverifiable state while claiming readiness.
  • +
  • To put secrets, private keys, raw customer lists, or provider dumps into the +ops desk packet.
  • +
+

Operating Model

+

Use one loop:

+
+
+ +
+
snapshot -> findings -> proposals -> approval -> governed lane -> receipt -> projection
+

The manager dashboard and the agent must read the same state and emit the same +action families. A button click and an agent plan are different interfaces over +the same governed lane, not separate backdoors.

+

Delegation Model

+

Ops desk packets name existing execution surfaces; they do not implement them.

+
    +
  • release owns release preparation, approval, publish handoff, and +post-release verification.
  • +
  • ledger, audit-receipt, and run-history own proof questions.
  • +
  • send-as owns authority for live communications; provider adapter skills own +provider-specific execution details.
  • +
  • spend, charge, refund, and branded payment skills own money movement.
  • +
  • Project skills own product vocabulary and product-specific actions.
  • +
  • CLI commands, hosted API routes, and GitHub workflows remain deterministic +execution interfaces. The operator skill may cite them as handoff targets but +must not clone their behavior in prose.
  • +
+

If no existing lane can perform the action cleanly, return needs_input or a +product gap. Do not invent a private workaround.

+

Procedure

+
    +
  1. Scope the objective.

    +
      +
    • Identify the workspace, project, account, surface, time window, and whether the ask is +read-only, proposal-only, or execution-prep.
    • +
    • Read project_profile or operator_policy as context, not authority.
    • +
    • If the operating scope or objective is ambiguous, return needs_input.
    • +
    +
  2. +
  3. Classify state from evidence.

    +
      +
    • Use dashboard_snapshot, receipt_summary, effect_summary, and +provider_status when present.
    • +
    • Treat missing evidence as missing. Do not infer success from UI state alone.
    • +
    • Separate health, money, communications, provider mutations, access, +deployment, and incident signals.
    • +
    • For review, catalog, publication, bounty, or marketplace work, classify +whether the artifact is real, useful, complete, and valuable. A reachable +artifact with no credible user, maintainer, operator, public proof, or +marketing value is not ready.
    • +
    • If using operate_from_projection, treat the read projection as the +dashboard snapshot. An empty projection is not an error, but it should +usually produce needs_input rather than fake readiness.
    • +
    +
  4. +
  5. Route to governed lanes.

    +
      +
    • Release questions route to release plus the project release profile and +existing release workflow/commands.
    • +
    • Audit questions route to ledger, audit-receipt, run-history, +or least-privilege.
    • +
    • Live communication routes through send-as and then a provider adapter.
    • +
    • Payment collection, payout, refund, chargeback, or target changes route to +the matching payment lane.
    • +
    • Board, thread, and provider actions route to messageboard, a provider +adapter, issue-intake, issue-to-pr, or the product's own skill.
    • +
    • Deploy and config changes route to the product-owned deploy lane.
    • +
    +
  6. +
  7. Decide gates.

    +
      +
    • Read-only checks: no human approval.
    • +
    • Drafts, dry-runs, previews, and reports: no live-action approval unless they +expose private data or broaden authority.
    • +
    • Live sends, payouts, refunds, customer-visible posts, provider mutations, +target changes, credential changes, deploys, destructive actions, and broad +audience decisions: explicit approval required.
    • +
    • A review verdict, recommendation, or green dry-run is not payment approval. +Money movement needs a separate approval prompt naming the amount, recipient, +rail, target class, and verification receipt expected after settlement.
    • +
    • Missing approval means awaiting_approval, not "ready".
    • +
    +
  8. +
  9. Produce the ops desk packet.

    +
      +
    • Lead with the few issues an operator should act on now.
    • +
    • Name the exact lane for each proposed action.
    • +
    • Include the existing execution interface as a handoff, not as a duplicated +implementation.
    • +
    • Include approval copy only when the operator could approve it safely.
    • +
    • Include verification steps that will prove the action happened.
    • +
    +
  10. +
  11. Stop cleanly.

    +
      +
    • Return needs_input for missing scope, objective, identity, authority, +evidence, approval, or target.
    • +
    • Return refused for requests to bypass gates, hide material facts, leak +secrets, spoof receipts, mark unsettled money as settled, or send without a +principal/audience/content digest.
    • +
    +
  12. +
+

Edge cases and stop conditions

+
    +
  • No project/workspace/account or objective: return needs_input; there is +no safe operating frame.
  • +
  • No projection or receipt evidence: return needs_input or unknown +status; do not convert silence into ok.
  • +
  • Requested action has unknown consequence: stop at needs_input with the +missing lane/consequence classification.
  • +
  • Money, public send, deploy, credential, target, destructive, or provider +mutation without approval: return awaiting_approval.
  • +
  • Approval text is too broad to approve safely: return needs_input with the +exact missing amount, audience, target, network, provider, or effect.
  • +
  • User asks to skip a gate, hide a blocker, forge a receipt, or mark state +settled without proof: return refused.
  • +
+

Reference Loading

+

Load only the reference needed for the objective:

+
    +
  • Payments, payouts, refunds, payment rail adapters, reconciliation: +references/payments.md
  • +
  • Email, campaigns, notifications, customer/public communication: +references/communications.md
  • +
  • Receipt verification, ledger, trust roots, after-action proof: +references/receipts.md
  • +
  • Provider health, deploys, webhooks, credentials, outages: +references/providers.md
  • +
  • Manager dashboard state, projections, and action catalog design: +references/dashboard.md
  • +
  • Delegation, project profiles, CLI/workflow handoff, and dogfooding rules: +references/delegation.md
  • +
+

Output schema

+

Return one ops_desk_packet:

+
+
+ +
+
ops_desk_packet:
+  decision: ready | awaiting_approval | needs_input | no_action | refused
+  scope_ref: string
+  objective: string
+  mode: read_only | proposal | execution_prep | post_action_review
+  dashboard:
+    health: ok | degraded | blocked | unknown
+    money: ok | needs_attention | blocked | unknown
+    communications: ok | needs_attention | blocked | unknown
+    providers: ok | needs_attention | blocked | unknown
+    receipts: ok | needs_attention | blocked | unknown
+  findings:
+    - severity: info | warning | critical
+      area: health | money | communications | providers | receipts | access | deploy
+      summary: string
+      evidence_refs: [string]
+  proposals:
+    - action_id: string
+      lane: string
+      reason: string
+      inputs_summary: object
+      consequence: read_only | draft | live_mutation | money_movement | public_send | deploy
+      approval_required: boolean
+      approval_prompt: string | null
+      blockers: [string]
+      verification:
+        expected_receipt: string
+        expected_effect: string | null
+        readback: string
+      execution:
+        interface: skill | cli | hosted_api | workflow | provider_tool | manual
+        lane_ref: string
+        profile_ref: string | null
+        command_ref: string | null
+        workflow_ref: string | null
+        approval_gate: string | null
+        verifier_ref: string | null
+  ordered_next_steps:
+    - step: string
+      lane: string
+      requires_confirmation: boolean
+  refused_reasons: [string]
+  needs_input: [string]
+  success_checkpoint:
+    milestone: string
+    description: string
+

The advance runner returns one dispatch_decision:

+
+
+ +
+
dispatch_decision:
+  decision: dispatch | escalate | done
+  reason: string
+  dispatch:                 # present when decision == dispatch
+    member: string          # a role from candidate_roster
+    skill: string           # that role's roster skill, echoed
+    task: string            # what the member should do
+    needed_scope: [string]  # subset of the member's scope ceiling
+    consequence: read_only | draft | live_mutation | money_movement | public_send | deploy
+    verification:
+      expected_receipt: string
+      readback: string
+  escalation:               # present when decision == escalate
+    to: string              # a roster role or "human"
+    trigger: string
+    ask: string
+    approval_prompt: string | null
+  resolution:               # present when decision == done
+    reason: string
+

Decision rules

+
    +
  • Prefer one clear next action over a dashboard dump.
  • +
  • Never bury a required approval in prose; put it in approval_prompt.
  • +
  • Never expose tokens, API keys, raw customer lists, private wallet keys, or +provider response dumps.
  • +
  • Never claim a state is settled, sent, deployed, paid, or refunded without a +receipt/effect/readback reference.
  • +
  • Never route a public artifact, skill, bounty result, or docs deployment as +ready when it lacks a credible real-world audience or durable public evidence.
  • +
  • Never widen authority because a dashboard widget would be convenient.
  • +
  • Never duplicate an existing CLI command, workflow, hosted endpoint, or domain +skill in operator prose. Route to it.
  • +
  • Keep product-specific policy in product context. Keep this skill generic.
  • +
+

Inputs

+
    +
  • objective (required): operator request, e.g. "check payments and unblock +funding", "prepare a campaign send", or "review stuck receipts".
  • +
  • scope_ref (required): the project, workspace, account, product, or bounded +surface being operated.
  • +
  • dashboard_snapshot (optional): JSON summary of current projected state.
  • +
  • receipt_summary (optional): JSON or prose receipt/effect summary.
  • +
  • provider_status (optional): JSON or prose provider health/account state.
  • +
  • approval_context (optional): existing operator approvals, denials, or +policy gates.
  • +
  • operator_policy (optional): project-specific constraints and lane names.
  • +
  • project_profile (optional): project topology, existing interfaces, and +verification expectations. It is context, not authority.
  • +
  • requested_action (optional): preselected action lane or dashboard action id.
  • +
+

Worked example

+

Input: "Check payment readiness and tell me what to do next" with a dashboard +snapshot showing healthy quote/readback state, three funded items, no unfunded +approved items, and one rail adapter webhook status needs_review.

+

Output: decision: ready, money status ok, providers status +needs_attention, one warning finding for rail webhook readiness, and one +proposal routing to provider.webhook_check with no money movement. It does +not propose marking anything funded, because no unfunded approved item is +present and the latest funding receipt is already verified.

+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/policy-author.html b/docs/sourcey-catalog/site/pages/policy-author.html new file mode 100644 index 000000000..402e4d3d2 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/policy-author.html @@ -0,0 +1,189 @@ + +policy-author - Runx Governed Skill Catalog
Safety and review

policy-author

Author one governed runx operational policy from intent, and prove it lints.
    +
  • Group: Safety and review
  • +
  • Source: skills/policy-author/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/policy-author/SKILL.md
  • +
+

Policy Author

+

Author one governed runx operational policy from intent, and prove it lints.

+

Adopting governed runx means writing an operational policy: which repos may be +touched, who owns which surface, which sources are trusted, what confidence is +required before action, and which outcomes need a human. Written by hand that is +a long, error-prone document. This skill turns a plain-English governance brief +into one runx.operational_policy.v1 proposal, or tightens an existing policy, +and runs a fail-closed lint over it before it ships. It proposes; a human +approves.

+

What this skill does

+
    +
  1. Read the intent. Take the governance brief (and an existing policy when +tightening) and identify the target surfaces, sources, owners, and the risk +posture.
  2. +
  3. Draft the policy. Produce a complete runx.operational_policy.v1: target +repos, runner binding, allowed actions, trusted sources with confidence +floors, owner routes, and outcome rules.
  4. +
  5. Lint fail-closed. Run the policy checks below. Any failing check blocks +the proposal with the exact fix, rather than shipping a permissive policy.
  6. +
  7. Tighten, never loosen. When given an existing policy, only propose +changes that narrow authority (auto-merge off, human gate on, confidence up). +Widening is a separate, explicit human decision.
  8. +
+

Core principles

+
    +
  • Fail closed. Unspecified means denied. A missing owner route, source rule, +or confidence floor is a lint error, not a permissive default.
  • +
  • Human gate on mutation. Any policy that allows repository mutation must set +require_human_merge_gate: true and auto_merge: false.
  • +
  • Named owners. Every target surface routes to a named owner; no orphan +surfaces.
  • +
  • Bounded sources. Each trusted source declares a minimum confidence; no +source admits work below its floor.
  • +
  • Verification before close. A source issue closes only when the outcome is +verified.
  • +
+

When to use this skill

+
    +
  • Bootstrapping a new runx deployment that needs an operational policy.
  • +
  • Tightening an existing policy after a near-miss or an audit.
  • +
  • Onboarding a new target repo, source, or owner into an existing policy.
  • +
+

When not to use this skill

+
    +
  • To widen authority (add auto-merge, drop a human gate, lower confidence). That +is an explicit human decision, not a generated proposal.
  • +
  • To write skill logic or graphs. This authors the governance envelope, not the +skills it governs.
  • +
+

The operational policy model

+

The proposal fills runx.operational_policy.v1:

+
    +
  • target_repos: the repositories the policy may act on.
  • +
  • runner: the runner binding (id, kind, and required substrate, e.g. GitHub +Actions + scafld).
  • +
  • allowed_actions: the lanes permitted (e.g. issue-intake, issue-to-pr, +pr-review).
  • +
  • sources: trusted inbound sources, each with a min_confidence floor.
  • +
  • owner_routes: surface-to-owner routing; every surface has a named owner.
  • +
  • outcomes: verification_required, close_source_issue, +require_human_merge_gate, auto_merge.
  • +
+

Lint diagnostics

+

The fail-closed lint emits these; any error blocks the proposal:

+
    +
  • policy.owner.unrouted (error): a target surface has no owner route.
  • +
  • policy.mutation.no_human_gate (error): mutation allowed without +require_human_merge_gate: true.
  • +
  • policy.mutation.auto_merge_on (error): auto_merge is true on a mutating +policy.
  • +
  • policy.source.no_confidence_floor (error): a source has no min_confidence.
  • +
  • policy.source.floor_too_low (warning): a confidence floor below 0.7.
  • +
  • policy.close.before_verify (error): close_source_issue set without +verification_required.
  • +
  • policy.action.unknown (error): an allowed action is not a known lane.
  • +
+

Procedure

+
    +
  1. Validate that the brief names the governed work, the target repo or surface, +and the intended owner or escalation route.
  2. +
  3. Extract all repos, sources, actions, owners, confidence floors, and outcome +rules from the brief and any existing policy.
  4. +
  5. If tightening an existing policy, diff proposed changes against the current +grant. Flag any widened action, lower confidence floor, removed owner, or +removed human gate as a separate human decision.
  6. +
  7. Draft the smallest complete runx.operational_policy.v1 that allows the +stated work and denies everything else.
  8. +
  9. Run the lint diagnostics. Any error finding prevents decision: ready.
  10. +
  11. Emit the policy, lint result, rationale, blockers, and success checkpoint.
  12. +
+

Edge cases and stop conditions

+
    +
  • No owner route: return needs_input; an ownerless surface is never +governed by default.
  • +
  • Mutation without a human gate: return reject or needs_input; do not +emit a ready mutating policy without require_human_merge_gate: true.
  • +
  • Auto-merge requested: block the proposal unless the user explicitly +performs a separate authority-widening decision outside this skill.
  • +
  • Unknown action lane: return needs_input with the unknown action names.
  • +
  • Source without confidence floor: return needs_input; implicit trust is +not a policy.
  • +
  • Conflicting owner routes: return needs_input and cite the conflicting +surfaces and owners.
  • +
+

Output schema (policy_proposal)

+
+
+ +
+
decision: ready | needs_input | reject
+policy:
+  schema: runx.operational_policy.v1
+  target_repos: [string]
+  runner:
+    id: string
+    kind: string
+    requires: [string]
+  allowed_actions: [string]
+  sources:
+    - provider: string
+      min_confidence: number
+  owner_routes:
+    - surface: string
+      owner: string
+  outcomes:
+    verification_required: boolean
+    close_source_issue: never | when_verified | always
+    require_human_merge_gate: boolean
+    auto_merge: boolean
+lint:
+  status: pass | fail
+  findings:
+    - id: string
+      severity: error | warning
+      message: string
+rationale: string
+blockers: [string]
+needs_input: [string]
+success_checkpoint:
+  milestone: string
+  description: string
+

A proposal with any error finding must have decision: needs_input or +reject, never ready.

+

Worked example

+

Brief: "Govern issue intake across our three repos. GitHub issues and Sentry +alerts. Kam owns the platform, Chong owns product. Never auto-merge; a human +approves every merge; close the source issue only once the fix is verified."

+

The proposal binds the three repos to a GitHub-Actions + scafld runner, allows +issue-intake/issue-to-pr/pr-review, trusts GitHub at 0.72 and Sentry at +0.82, routes platform to Kam and product to Chong, and sets +require_human_merge_gate: true, auto_merge: false, +verification_required: true, close_source_issue: when_verified. The lint +passes, so decision: ready.

+

Inputs

+
    +
  • governance_brief (required): the governance intent in prose.
  • +
  • existing_policy (optional): a current runx.operational_policy.v1 to tighten.
  • +
  • target_repos (optional): explicit repo list when not in the brief.
  • +
  • objective (optional): operator intent that focuses the pass.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/release.html b/docs/sourcey-catalog/site/pages/release.html new file mode 100644 index 000000000..e5cbfe3fa --- /dev/null +++ b/docs/sourcey-catalog/site/pages/release.html @@ -0,0 +1,130 @@ + +release - Runx Governed Skill Catalog
GitHub and delivery

release

Turn a proposed release into an audited publication. The skill owns the release decision process: evidence gathering, changelog preparation, approval, publish handoff, verification, and announcement. It does not own a project's custom release implementation. Project-specific topology lives in a release profile that names existing commands, workflows, registries, deploy targets, and verification readbacks.
    +
  • Group: GitHub and delivery
  • +
  • Source: skills/release/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/release/SKILL.md
  • +
+

Release

+

Turn a proposed release into an audited publication. The skill owns the release +decision process: evidence gathering, changelog preparation, approval, publish +handoff, verification, and announcement. It does not own a project's custom +release implementation. Project-specific topology lives in a release profile +that names existing commands, workflows, registries, deploy targets, and +verification readbacks.

+

Every version and changelog claim must trace to commits, tags, checks, package +metadata, or explicit operator context. Write release material for package +consumers: say why this version matters and what they should do next, without +generic launch language or positive wording that hides a blocker. Stop in +prepare or at approval when checks fail, versioning is unclear, evidence is +thin, or the announcement would overstate what shipped.

+

Two runners:

+
    +
  • prepare (read-only) — survey the commit range since the last tag, +classify commits, stage a changelog, run the declared checks, and emit a +release_brief describing what would ship, what is blocked, and what +remains unresolved. Safe to run unattended and in CI.
  • +
  • release (default, graph) — wires prepare → approval gate → +publishverify. publish is not exposed as a standalone runner; it is +only reachable inside the graph after the approval transition clears.
  • +
+

Invoke runx skill release prepare for a CI dry-run. Invoke +runx skill release to run the governed end-to-end flow.

+

Phases

+

prepare

+

The read-only phase. Reads git history, classifies each commit since the +previous semver tag (feat, fix, refactor, chore, breaking), +stages a changelog, reads the project release profile when supplied, and runs +the declared release checks. Emits a release_brief with the findings.

+

The brief is the only artifact that flows forward. If it is not +publishable, the graph stops at the approval gate with the reasons +attached.

+

approve-publish

+

A typed approval step. The gate id is release.publish.approval. The +brief is provided as context so the approver sees what would ship before +deciding.

+

The policy transition only advances to publish-release when +approve-publish.approval_decision.data.approved is true. No back +channel, no implicit approval on timeout.

+

publish-release

+

The destructive phase. Takes the approved release_brief from graph context and +hands off to the project-declared release interface: an existing CLI command, +GitHub Actions workflow, hosted API route, provider tool, or manual release +gate. Every side effect is recorded in publish_report.side_effects[] with its +locator and evidence; the graph receipt seals the trail.

+

Refuses to act if the brief is missing, unpublishable, or not carried +through the approval gate. Refuses to act if the project profile asks the agent +to reimplement release logic instead of naming an existing execution surface.

+

verify-release

+

The proof phase. Reads the publish_report, release brief, and project profile, +then verifies external state: registry versions, release assets, deploy health, +site/changelog readbacks, package-manager manifests, or any other project-owned +release acceptance criteria. Emits a release_report for operator review and +public audit.

+

Inputs

+
+ + + +
NameRequiredDescription
project_rootyesAbsolute path to the project being released.
channelyesPublishing target (npm, pypi, github-release).
profile_refnoPath or registry ref for a project-owned release profile. The profile describes existing commands/workflows and verification expectations; it is not authority.
last_tagnoPrevious release anchor. Defaults to the latest semver tag reachable from the current branch.
operator_contextnoCadence, campaign, or posture guidance for this release.

Outputs

+
    +
  • prepare emits release_brief_packet carrying release_brief: +changelog, check results, proposed version, unresolved flags, +publishable verdict.
  • +
  • The graph emits a graph receipt that links the prepare brief, the +approval decision, publish report, and verification report into one auditable +trail.
  • +
  • publish-release (inside the graph) emits publish_report: registry +URL, release tag, announcement packet, and a side_effects[] list with +a locator and evidence per write action.
  • +
  • verify-release emits release_report: expected lanes, observed readbacks, +missing artifacts, conditional skips, and final release verdict.
  • +
+

Trust boundary

+

prepare is safe to run unattended and in CI. The destructive work is only +reachable through the graph, and the graph refuses to transition to +publish-release without an approved decision from release.publish.approval. +The graph enforces the gate; the skill does not bypass it.

+

Project profiles are context, not authority. A profile may say which workflow, +command, registry, or URL should be used. It cannot grant credentials, skip +approval, or authorize a destructive release by itself.

+

Scopes

+
    +
  • runx:release:read — required by the prepare phase.
  • +
  • runx:release:publish — required by the publish phase; the graph grant +must include this only when the approval transition has cleared.
  • +
  • runx:release:verify — required by the verification phase.
  • +
+

Tasks

+
    +
  • release-prepare — the read-only phase task. Provides the +release_brief output shape.
  • +
  • release-publish — the destructive phase task. Only reachable inside +the graph; requires the approved brief in context.
  • +
  • release-verify — the proof phase task. Reads external state and reports +whether the release actually landed.
  • +
+

These are managed-agent task contracts carried by the skill package and its +X.yaml graph definition. They are not a separate registered task catalog.

+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/research.html b/docs/sourcey-catalog/site/pages/research.html new file mode 100644 index 000000000..ab4dc218d --- /dev/null +++ b/docs/sourcey-catalog/site/pages/research.html @@ -0,0 +1,69 @@ + +research - Runx Governed Skill Catalog
Research and data

research

Research one bounded question and turn it into a decision-ready packet.
    +
  • Group: Research and data
  • +
  • Source: skills/research/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/research/SKILL.md
  • +
+

Research

+

Research one bounded question and turn it into a decision-ready packet.

+

This skill is for applied research, not open-ended browsing. It should answer +one practical question with evidence, tradeoffs, and explicit uncertainty: +which issue is worth tackling, what the ecosystem is doing, whether a proposal +is grounded, or what claims a public post can safely make.

+

Keep the scope tight. Summaries without evidence are not enough, but an +undirected literature review is also wrong. Prefer a small number of verified +claims that change the operator's decision.

+

Operating rules

+
    +
  • State the objective in operational terms.
  • +
  • Distinguish verified evidence from inference.
  • +
  • Give every important claim a source and confidence.
  • +
  • Surface missing evidence instead of inventing it.
  • +
  • Bound the result to a concrete deliverable: brief, issue recommendation, +content outline, or publish/no-publish decision.
  • +
  • State what the finding changes: what to write, build, avoid, defer, or review.
  • +
  • Return needs_more_evidence rather than forcing a speculative conclusion, +and not_worth_publishing when a true finding is irrelevant to the audience.
  • +
+

Output

+
    +
  • research_brief: object with objective, scope, summary, and +open_questions.
  • +
  • evidence_log: array of evidence entries with claim, source, +confidence, and relevance.
  • +
  • decision_support: array of options or recommendations with rationale.
  • +
  • risks: array of research or execution risks.
  • +
+

Inputs

+
    +
  • objective (required): the question to answer.
  • +
  • domain (optional): ecosystem, product area, or audience context.
  • +
  • deliverable (optional): intended artifact, for example daily brief, +triage recommendation, or publish packet.
  • +
  • operator_context (optional): local constraints or strategic context.
  • +
  • target_entities (optional): array or object naming repos, products, +competitors, communities, or issues that bound the research.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/review-receipt.html b/docs/sourcey-catalog/site/pages/review-receipt.html new file mode 100644 index 000000000..84c97834e --- /dev/null +++ b/docs/sourcey-catalog/site/pages/review-receipt.html @@ -0,0 +1,98 @@ + +review-receipt - Runx Governed Skill Catalog
Safety and review

review-receipt

Diagnose what went wrong in a skill or graph execution and propose the smallest change that fixes it.
    +
  • Group: Safety and review
  • +
  • Source: skills/review-receipt/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/review-receipt/SKILL.md
  • +
+

Receipt Review

+

Diagnose what went wrong in a skill or graph execution and propose the +smallest change that fixes it.

+

Read the receipt or failure summary. Identify what was attempted, what +succeeded, and where it broke. The receipt contains step statuses +(sealed, failure, policy_denied, needs_agent), +exit codes, stderr, scope admission decisions, +and timing.

+

Distinguish root cause from symptoms. A graph may report failure at step 4, +but the root cause may be bad output from step 2 that propagated through +context passing. Trace data flow backward through context edges to find +where the problem originated.

+

Classify the failure:

+
    +
  • Input error — required input missing or malformed. Fix: input +validation or input resolution.
  • +
  • Scope denial — step requested scopes outside the graph grant. +Fix: scope declarations or grant configuration.
  • +
  • Tool failure — CLI tool or adapter returned an error. Fix: tool +invocation (args, env, cwd) or the tool itself.
  • +
  • Schema mismatch — step output did not match expected shape for +downstream context. Fix: output parsing or artifact contract.
  • +
  • Timeout — step exceeded time budget. Fix: increase timeout, +reduce work, or split the step.
  • +
  • Policy denial — transition gate blocked the step. Fix: gate +conditions or upstream output.
  • +
  • Review rejection — adversarial review found blocking issues. +Fix: the code or spec, not the review process.
  • +
  • Harness assertion — fixture expectations did not match actual +output. Fix: skill logic or stale fixture expectations.
  • +
+

Agent-mediated suspension is not a failure

+

A receipt with status needs_agent denotes a healthy +agent-mediated suspension, not a defect. The runtime yielded to the +caller for missing agent or human input. +This is a normal part of graph execution, not one of the failure +classes above. When the only evidence is needs_agent without +any exit code, scope denial, schema mismatch, or other concrete +failure signal, return verdict: pass with an empty +improvement_proposals array and note that the graph is paused as +designed.

+

One failure, one fix. Propose the smallest change that addresses the root +cause. Do not bundle unrelated improvements.

+

Output

+

The output shape is formalised as JSON Schema at +review-receipt-output.schema.json. +Agents should self-validate before returning, and downstream +consumers (notably write-harness) may validate on receipt.

+
    +
  • verdict: pass, needs_update, or blocked.
  • +
  • failure_summary: which step, which failure class, what root cause. +One to three sentences.
  • +
  • improvement_proposals: array of bounded changes. Each:
      +
    • target: what to change (SKILL.md, execution profile, graph step, input, fixture)
    • +
    • change: what specifically to change
    • +
    • rationale: why this fixes the root cause
    • +
    • risk: what could go wrong
    • +
    +
  • +
  • next_harness_checks: replayable checks that should pass after the fix.
  • +
+

Inputs

+

All optional — supply whichever evidence is available:

+
    +
  • receipt_id: receipt id to inspect.
  • +
  • receipt_summary: sanitized receipt or harness summary.
  • +
  • harness_output: failed harness output or assertion text.
  • +
  • skill_path: path to the skill being improved.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/run-history.html b/docs/sourcey-catalog/site/pages/run-history.html new file mode 100644 index 000000000..058991146 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/run-history.html @@ -0,0 +1,129 @@ + +run-history - Runx Governed Skill Catalog
Outbound and tooling

run-history

Turn runx's own run ledger into a governed, read-only report.
    +
  • Group: Outbound and tooling
  • +
  • Source: skills/run-history/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/run-history/SKILL.md
  • +
+

Run History Analyst

+

Turn runx's own run ledger into a governed, read-only report.

+

Every governed runx run leaves a receipt. Over time that ledger is data: which +skills run, how often they seal versus refuse, which never graduate past alpha, +and where authority is consistently broader than usage. This skill reads that +ledger (via runx history and runx list) and reports it. It never executes a +skill, sends, or mutates; every planned call is read-only. Its recommendations +route to the governance skills, least-privilege, audit-receipt, +and the maturity promoter, so the report turns into action through the right +governed lane.

+

What this skill does

+
    +
  1. Scope the question. Account-wide, a single skill, or a period.
  2. +
  3. Pull the ledger, read-only. Plan runx history and runx list queries; +never an execution command.
  4. +
  5. Grade the signals. Seal rate, refusal rate, maturity distribution, and +scope-usage breadth, each with an assessment, not a bare number.
  6. +
  7. Recommend through governed lanes. A high refusal rate, a skill stuck at +alpha, or a consistently-unused scope routes to a named governance skill, not +a direct mutation.
  8. +
+

Core principles

+
    +
  • Read-only. Only runx history and runx list. No execution, send, or +config call. Every planned call is requires_confirmation: false.
  • +
  • Grade, do not dump. Every metric carries an assessment against a norm.
  • +
  • Route, do not act. Recommendations name the governed lane +(least-privilege, audit-receipt, maturity promoter); this skill +does not change a grant or a tier itself.
  • +
  • Refusals are signal, not failure. A healthy refusal rate means bounds are +working; a spike means a skill or a policy needs review.
  • +
  • Absence is not health. With no history, return needs_more_evidence.
  • +
+

When to use this skill

+
    +
  • Periodic platform review: what is runx actually doing across skills.
  • +
  • Spotting skills with anomalous refusal rates or stuck maturity.
  • +
  • Finding consistently-unused scopes worth attenuating.
  • +
+

When not to use this skill

+
    +
  • For a single run's authority audit (use audit-receipt).
  • +
  • To narrow one skill's grant from its usage (use least-privilege).
  • +
  • For email or product analytics. This reports on runx runs, not a domain +dataset; that is a separate, product-owned analytics skill.
  • +
+

Signals and norms

+
    +
  • seal_rate: share of runs that sealed cleanly. good >0.9, warning 0.7-0.9, +critical <0.7.
  • +
  • refusal_rate: share of runs that hit a governed refusal. info by default; a +sharp per-skill spike is a warning worth routing.
  • +
  • maturity_distribution: counts at alpha / beta / stable. Many skills stuck at +alpha is a warning (no harness coverage).
  • +
  • scope_usage: scopes granted but never exercised across runs, a candidate for +attenuation.
  • +
+

Output schema (history_report)

+
+
+ +
+
decision: ready | needs_more_evidence
+scope: workspace | skill | all
+period: string
+ordered_tool_calls:
+  - tool: runx history | runx list
+    purpose: string
+    requires_confirmation: boolean      # always false; read-only
+findings:
+  - metric: string
+    value: string
+    assessment: good | warning | critical | info
+recommendations:
+  - finding: string
+    lane: least-privilege | audit-receipt | maturity-promoter | none
+    action: string
+blockers: [string]
+needs_input: [string]
+success_checkpoint:
+  milestone: string
+  description: string
+

Worked example

+

Question: "How is the skill catalog behaving this month?" The report plans +runx history --since 30d and runx list skills --json, then reports a 0.94 +seal rate (good), a refusal rate of 0.06 (info, bounds working), a maturity +spread of 14 alpha / 5 beta / 2 stable (warning, most skills lack harness +coverage), and one skill granted repo.write but never exercising it across 40 +runs. It recommends routing the alpha-heavy spread to the maturity promoter and +the unused repo.write to least-privilege for attenuation. It changes +nothing itself.

+

Inputs

+
    +
  • objective (required): the history question.
  • +
  • scope (optional): workspace, a specific skill, or all.
  • +
  • period (optional): e.g. 30d or 90d.
  • +
  • history_summary (optional): a sanitized runx history summary when already +fetched.
  • +
  • objective guides which signals to lead with.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/sandbox-harden.html b/docs/sourcey-catalog/site/pages/sandbox-harden.html new file mode 100644 index 000000000..ace10b737 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/sandbox-harden.html @@ -0,0 +1,223 @@ + +sandbox-harden - Runx Governed Skill Catalog
Safety and review

sandbox-harden

Decide the narrowest sandbox a workload can run inside without breaking it.
    +
  • Group: Safety and review
  • +
  • Source: skills/sandbox-harden/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/sandbox-harden/SKILL.md
  • +
+

Sandbox Harden

+

Decide the narrowest sandbox a workload can run inside without breaking it.

+

What this skill does

+

Most workloads ship with the default sandbox their runtime hands them: the full +seccomp default, a broad capability set, unrestricted egress, a writable root. +That default is sized for the worst case, not for this workload. This skill reads +what a named workload actually needs and emits the tightest posture that still +lets it run: an allowed-syscall list, the capabilities to drop, an egress +allowlist, and a filesystem stance, with the residual risk named in plain terms.

+

The output is a posture recommendation, not an enforced change. A runtime, an +orchestrator, or an operator applies it. This skill never executes the workload +and never widens a posture below the supplied baseline without saying why.

+

How it differs from its neighbors: least-privilege audits API scopes, +not syscalls; audit-receipt reads a sealed run after the fact. This skill is +the only one that reasons about the seccomp, capability, egress, and filesystem +posture a workload should run inside before it starts. The recommendation reads +input only and writes nothing; applying it is a separate runtime act that +exercises sandbox:configure on the named workload and nothing wider.

+

When to use this skill

+
    +
  • Before running an untrusted or third-party workload, to decide its sandbox.
  • +
  • During a security review of an existing deployment whose sandbox is the broad +default.
  • +
  • When promoting a workload toward production and the runtime posture must be +reviewable, not implicit.
  • +
  • When an operator needs the egress allowlist and dropped capabilities written +down before a runtime applies them.
  • +
+

When not to use this skill

+
    +
  • To run, build, or schedule the workload. This skill recommends a posture; a +runtime, orchestrator, or operator executes the workload under it.
  • +
  • To audit which API scopes or grants a subject used. That is +least-privilege; it reasons about authority, this one reasons about +syscalls, capabilities, egress, and the filesystem.
  • +
  • To audit a sealed receipt for over-reach after the fact. That is +audit-receipt.
  • +
  • To handle, store, or surface the secret material a workload reads. A hardening +profile names a mount path or a secret handle, never a secret value.
  • +
  • To produce a posture for a workload whose identity is unknown. Return +needs_agent instead of hardening an unnamed target.
  • +
+

Procedure

+
    +
  1. Resolve the workload.

    +
      +
    • Accept an image digest (sha256:...) or a skill ref. Record which form was +supplied as hardening_profile.workload.
    • +
    • Gate: if no workload is supplied, stop with needs_agent. There is nothing +to harden.
    • +
    +
  2. +
  3. Build the behavior model.

    +
      +
    • Combine the workload class (web service, batch job, CLI, language runtime), +the supplied threat_context, and the baseline posture.
    • +
    • Distinguish known behavior from assumed behavior. A profile built on assumed +syscall need is weaker evidence than one built on an observed or documented +call set.
    • +
    • Gate: if the behavior is unknown enough that the syscall set, egress, or +write paths would be a guess, stop with needs_more_evidence and name what +observation would resolve it (a trace, a manifest, a dry run under audit +seccomp).
    • +
    +
  4. +
  5. Recommend the seccomp profile.

    +
      +
    • Default to deny. Add only syscalls the behavior model supports.
    • +
    • Prefer a named runtime default profile plus an explicit allow delta over a +hand-rolled full list when the workload class has a known good baseline.
    • +
    • Never add a syscall family with no behavioral basis. Unknown need is a stop +condition, not a blanket allow.
    • +
    +
  6. +
  7. Drop capabilities.

    +
      +
    • Start from "drop all", then justify each capability kept.
    • +
    • A capability is kept only when the behavior model needs it. Name the reason +per kept capability in the rationale.
    • +
    +
  8. +
  9. Set the egress posture.

    +
      +
    • Default to mode: none. Move to mode: allowlist only when the workload +has a named, justified destination set.
    • +
    • List hosts, not raw allow-everything. An empty allowlist means no egress.
    • +
    • Never recommend open egress as a convenience.
    • +
    +
  10. +
  11. Set the filesystem posture.

    +
      +
    • Default to readonly: true with an explicit writable_paths list.
    • +
    • Each writable path is justified by the behavior model (scratch, cache, a +declared output dir). A writable root is a finding, not a default.
    • +
    +
  12. +
  13. State residual risk.

    +
      +
    • After the controls above, name what an attacker who fully controls the +workload could still do, the level, and the reason.
    • +
    • Residual risk is never "none". If the profile is built on assumed behavior, +say so here.
    • +
    +
  14. +
  15. Honor the baseline.

    +
      +
    • The recommended posture must be at least as strict as the supplied baseline +on every axis. If the model would relax any control below the baseline, do +not relax it silently; either keep the baseline or, where a relaxation is +genuinely warranted, record the reason in the rationale and raise the +residual-risk level.
    • +
    +
  16. +
+

The narrowness gate and the evidence gate are the two that hold authority: no +control weaker than the baseline without a stated reason and a raised +residual-risk level, and no syscall, host, or write path with no behavioral +basis. A posture no tighter than the baseline with no new evidence is not worth +emitting.

+

Edge cases and stop conditions

+
    +
  • Missing workload: return needs_agent; an unnamed target cannot be +hardened.
  • +
  • Unknown behavior: return needs_more_evidence with the observation that +would resolve it; do not pad the syscall set with plausible families.
  • +
  • Workload needs a privileged capability (for example CAP_SYS_ADMIN): keep +it only with a stated reason and raise the residual-risk level; never drop a +capability the workload provably needs just to look tighter.
  • +
  • Egress to a dynamic or unbounded host set: keep mode: allowlist with the +known hosts and flag the unbounded remainder as residual risk; do not fall back +to open egress.
  • +
  • Baseline is already tighter than the model: keep the baseline; the +recommendation never loosens a control the operator already set.
  • +
  • Secret material in the input: reference it by mount path or handle in the +profile and rationale; never copy a secret value into the output.
  • +
  • Conflicting threat context and baseline: prefer the stricter control and +name the conflict in the rationale.
  • +
+

Output schema

+
+
+ +
+
hardening_profile:
+  decision: ready | needs_more_evidence | needs_agent
+  workload:
+    ref_form: image_digest | skill_ref
+    image_digest: string
+    skill_ref: string
+    class: string
+  seccomp:
+    default: deny | allow
+    allowed_syscalls: array
+  dropped_caps: array
+  egress:
+    mode: none | allowlist
+    hosts: array
+  filesystem:
+    readonly: boolean
+    writable_paths: array
+  residual_risk:
+    level: low | medium | high
+    reason: string
+  rationale: string
+

The single hardening_profile object is packet runx.hardening.v1. Secrets, +tokens, key material, and raw fetched content never appear in the profile; +secret-bearing inputs are referenced by mount path or handle only. The receipt +carries the workload ref form and digest, the four posture axes, the +residual-risk level, the stop status, and the quality and voice profile hashes. +It carries no secret values and no syscall trace payloads.

+

Worked example

+

Input: workload is { image_digest: "sha256:1f4c...", class: "batch job" }; +threat_context is "processes untrusted user uploads, no inbound network"; +baseline is "docker default seccomp, all caps, open egress, writable root".

+

Output: decision: ready. seccomp.default: deny with an allowed set covering +file I/O, memory, and process control but not ptrace, mount, or raw socket +families. dropped_caps is the full default set (the job needs none). +egress.mode: none (no inbound or outbound network in the threat context). +filesystem.readonly: true with writable_paths: ["/tmp/work"] for upload +scratch. residual_risk.level: low, reason: a compromised job can still consume +CPU and fill /tmp/work to its quota; it cannot reach the network or escalate. +The rationale records that the syscall set is assumed from the batch-job class, +not from an observed trace, so a trace would raise confidence without widening +the posture.

+

Inputs

+
    +
  • workload (required, json): the target to harden, as { image_digest } or +{ skill_ref }, optionally with class. Without it the skill returns +needs_agent.
  • +
  • threat_context (optional, string): the trust assumptions and exposure, for +example "processes untrusted uploads, no inbound network".
  • +
  • baseline (optional, string): the current or floor posture. The +recommendation is never weaker than this without a stated reason.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/sourcey.html b/docs/sourcey-catalog/site/pages/sourcey.html new file mode 100644 index 000000000..b432b89d6 --- /dev/null +++ b/docs/sourcey-catalog/site/pages/sourcey.html @@ -0,0 +1,381 @@ + +sourcey - Runx Governed Skill Catalog
Outbound and tooling

sourcey

Generate a documentation site for a project using Sourcey. Sourcey is a static documentation generator that produces HTML sites from markdown pages, OpenAPI specs, Doxygen XML, and MCP server snapshots.
    +
  • Group: Outbound and tooling
  • +
  • Source: skills/sourcey/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/sourcey/SKILL.md
  • +
+

Sourcey

+

Generate a documentation site for a project using Sourcey. Sourcey is a static +documentation generator that produces HTML sites from markdown pages, OpenAPI +specs, Doxygen XML, and MCP server snapshots.

+

What this skill does

+

By default, runx executes Sourcey as a governed mixed-runner skill:

+
    +
  1. discover the bounded documentation scope, evidence, and plan
  2. +
  3. request approval
  4. +
  5. author the bounded docs/config bundle
  6. +
  7. write the source bundle deterministically
  8. +
  9. build docs deterministically
  10. +
  11. critique the built output in one bounded pass
  12. +
  13. apply at most one bounded revision pass
  14. +
  15. rebuild and verify the output deterministically
  16. +
+

For already-configured projects, the same sourcey runner stays narrow: the +discover step can confirm existing config, the author/revise passes can return +empty bundles, and the deterministic tool steps still perform the build and +verification work.

+

For repository-backed projects, Sourcey owns two separate surfaces: committed +docs source and generated site output. Keep those separate. Do not mix emitted +HTML, search indexes, or OG assets back into the authored docs tree.

+

When to use this skill

+
    +
  • A project needs a maintainer-grade documentation site generated from real +repository evidence, existing docs, API specs, Doxygen XML, or MCP snapshots.
  • +
  • A branded package or product needs Sourcey output with governed discovery, +approval, authoring, deterministic build, critique, revision, and receipt +proof.
  • +
  • A workflow needs to separate authored docs source from generated site output +while preserving a reviewable receipt trail.
  • +
  • A maintainer wants CI or deploy to rebuild docs without inventing scope, +prose, or information architecture at deploy time.
  • +
+

When not to use this skill

+
    +
  • To manufacture documentation when the repository evidence is too thin. Return +needs_more_evidence or needs_review instead of confident filler.
  • +
  • To write generated HTML, search indexes, or Open Graph assets back into the +source docs tree.
  • +
  • To bypass approval for a new docs plan or to run open-ended critique/revision +loops.
  • +
  • To document APIs by hand when an OpenAPI, Doxygen, or MCP source can be used +directly by Sourcey.
  • +
+

Documentation rules

+

Sourcey output should read like native project documentation that a maintainer +would stand behind:

+
    +
  • build from project evidence, but do not expose the evidence-gathering process +as page prose
  • +
  • preserve the project's own terms, priorities, and level of ambition
  • +
  • make fewer pages with real substance rather than many generic pages
  • +
  • make a real developer action easier—install, evaluate, integrate, operate, or +contribute; a polished site with thin content is a failed run
  • +
  • never use "generated by Sourcey", preview, adoption, migration, scaffold, or +demo framing unless the project itself uses that framing
  • +
  • never describe pages as machine output, agent output, or AI-generated docs; +the site should read like the project maintainer wrote and stands behind it
  • +
  • when publishing public docs, use a credible durable project, maintainer, +organization, product, or documentation home. Random personal domains, +placeholder parent sites, sandbox hosts, preview deploys, throwaway +subdomains, and unrelated novelty domains are not publication-quality homes
  • +
  • if the repo evidence is too thin for a strong docs page, surface that as an +evidence gap instead of manufacturing confident filler
  • +
+

Canonical semantics

+

Complex runx skills share a reusable phase language:

+
    +
  • scope
  • +
  • ingest
  • +
  • model
  • +
  • materialize
  • +
  • evaluate
  • +
  • revise
  • +
  • verify
  • +
  • ratify
  • +
+

The current Sourcey runner deliberately uses a bounded subset:

+
    +
  • discover folds scope + ingest + model
  • +
  • approve is ratify
  • +
  • author + write-docs + build form materialize
  • +
  • critique is evaluate
  • +
  • revise + write-revisions + rebuild form revise
  • +
  • verify is verify
  • +
+

The current slice uses exactly one bounded revision window. It never loops +until good and it never critiques indefinitely.

+

When docs_inputs is supplied explicitly, treat that as a bounded instruction +to use the existing config target. Do not overwrite the referenced config or +invent replacement docs files merely because repository inspection evidence is +thin. Missing evidence is not the same as missing files.

+

Procedure

+
    +
  1. Inspect the project and discover a bounded documentation plan from real project evidence.
  2. +
  3. Approve the discovered plan before authoring.
  4. +
  5. Author the bounded Sourcey source bundle.
  6. +
  7. Persist that bundle deterministically.
  8. +
  9. Run sourcey build deterministically with the discovered or authored config.
  10. +
  11. Critique the built output in one bounded evaluation pass.
  12. +
  13. Apply at most one bounded revision pass from that critique.
  14. +
  15. Rebuild deterministically after the revision bundle is written.
  16. +
  17. Verify the output directory contains index.html.
  18. +
  19. Inspect the receipt and generated site.
  20. +
+

The deterministic build report should carry enough rendered evidence for an +external reviewer to reason about the site without hidden file access. At +minimum that means the generated file list plus index-page title, headings, and +an excerpt when index.html exists.

+

Discovery contract

+

discovery_report may include additional planning metadata, but the canonical +resolved docs inputs must live under:

+
    +
  • discovery_report.discovered.brand_name
  • +
  • discovery_report.discovered.homepage_url
  • +
  • discovery_report.discovered.docs_inputs
  • +
+

Downstream deterministic build steps consume that nested discovered object.

+

Output schema

+

Sourcey build produces: HTML pages, sourcey.css, sourcey.js, +search-index.json, sitemap.xml, llms.txt, llms-full.txt, and +_og/ directory with generated Open Graph images.

+

The sealed package includes:

+
+
+ +
+
discovery_report:
+  discovered:
+    brand_name: string | null
+    homepage_url: string | null
+    docs_inputs: object | null
+doc_bundle:
+  files: array
+  summary: string
+sourcey_build_report:
+  generated_files: array
+  index_title: string
+  index_headings: array
+  index_excerpt: string
+evaluation_report: object
+revision_bundle:
+  files: array
+  summary: string
+sourcey_verification_proof:
+  verified: boolean
+  index_path: string
+receipt_notes:
+  authority: governed docs plan approval
+  mutation: authored docs source writes only
+

Worked example

+

Input: a project contains README.md, package.json, and a partial docs/ +tree, but no Sourcey config.

+

Output: decision: ready after approval; Sourcey discovers the project name, +homepage, and docs inputs, writes a bounded docs/sourcey.config.ts plus only +the highest-value missing docs pages, builds to .sourcey/runx-docs, critiques +the rendered index.html, applies at most one revision bundle, verifies the +output, and seals a receipt with the build report and verification proof.

+

If the project evidence does not support a maintainer-grade site, the run stops +with needs_more_evidence or needs_review instead of producing filler.

+

Inputs

+
    +
  • project (required): project root directory.
  • +
  • repo_root: optional alias for the project root when Sourcey is composed inside a parent graph that already uses repo_root.
  • +
  • brand_name: project name (discovered from package evidence if omitted).
  • +
  • homepage_url: project homepage (discovered from project evidence if omitted).
  • +
  • docs_inputs: structured docs inputs, e.g. {"mode":"config","config":"docs/sourcey.config.ts"} or {"mode":"openapi","spec":"openapi.yaml"}. Discovered if omitted and may point at authored config produced by the skill.
  • +
  • project_brief: optional grounded brief carrying brand cues, docs audit, +IA direction, and writing constraints. When present, the authored docs should +feel like native project docs rather than generic generated scaffolding.
  • +
  • output_dir: generated site output path (default: <project>/.sourcey/runx-docs).
  • +
  • sourcey_bin: explicit sourcey executable path (default: SOURCEY_BIN env or sourcey on PATH).
  • +
+

Repository Contract

+
    +
  • Keep authored docs source in the repository, usually under docs/ when using +docs/sourcey.config.ts.
  • +
  • Keep generated site output in output_dir, separate from the source tree.
  • +
  • The default generated output path is <project>/.sourcey/runx-docs.
  • +
  • Generated output should be gitignored unless the project explicitly chooses to +version release artifacts.
  • +
  • CI or deploy may run deterministic sourcey build from committed source.
  • +
  • Deploy must not be the step where docs scope, prose, or IA is invented. Do +discovery, authoring, and review before deploy.
  • +
  • For Astro host apps, prefer the first-class sourcey/astro integration over +a separate prebuild script that writes into public/docs. Keep +docs/sourcey.config.ts and markdown/spec inputs as source; let astro dev +serve Sourcey through Vite and astro build write generated docs into the +final output under the configured route.
  • +
  • For public publication, include enough proof for an external reviewer to +inspect the target project, source commit, Sourcey config or input source, +generated page list, deployment URL, parent domain, and durability of the +hosting choice.
  • +
+

Astro host pattern

+

Use this shape when the target already uses Astro and docs should live at a +path such as /docs:

+
+
+ +
+
import { defineConfig } from "astro/config";
+import sourcey from "sourcey/astro";
+
+export default defineConfig({
+  site: "https://example.com",
+  integrations: [
+    sourcey({
+      config: "./docs/sourcey.config.ts",
+      routeBase: "/docs",
+    }),
+  ],
+});
+

Do not add prebuild, build:docs, or committed public/docs artifacts for +this path unless the project explicitly cannot use Astro integrations. The +generated output remains reproducible build output, not authored source.

+

Config reference

+
+
+ +
+
import { defineConfig } from "sourcey";
+
+export default defineConfig({
+  name: "Project Name",
+  theme: {
+    preset: "default",             // "default" | "minimal" | "api-first"
+    colors: {
+      primary: "#hex",             // required
+      light: "#hex",               // optional, derived from primary
+      dark: "#hex",                // optional, derived from primary
+    },
+    fonts: {
+      sans: "Inter",               // optional
+      mono: "monospace",           // optional
+    },
+    layout: {
+      sidebar: "18rem",            // optional
+      toc: "19rem",                // optional
+      content: "44rem",            // optional
+    },
+    css: ["path/to/custom.css"],   // optional
+  },
+  logo: "path/to/logo.png",       // or { light, dark, href }
+  favicon: "path/to/favicon.ico",
+  repo: "https://github.com/org/repo",
+  editBranch: "main",
+  editBasePath: "docs",            // path from repo root to docs source
+  codeSamples: ["curl", "javascript", "python"],  // for OpenAPI tabs
+  navigation: {
+    tabs: [
+      // Markdown pages tab
+      {
+        tab: "Documentation",
+        slug: "",                  // empty = default tab
+        groups: [
+          { group: "Getting Started", pages: ["introduction", "quickstart"] },
+          { group: "Guides", pages: ["configuration", "deployment"] },
+        ],
+      },
+      // OpenAPI tab
+      {
+        tab: "API Reference",
+        openapi: "path/to/openapi.yaml",
+      },
+      // Doxygen tab
+      {
+        tab: "C++ API",
+        doxygen: {
+          xml: "path/to/doxygen/xml",
+          language: "cpp",         // "cpp" | "java"
+          groups: true,            // use doxygen groups for nav
+          index: "auto",           // "auto"|"rich"|"structured"|"flat"|"none"
+        },
+      },
+      // MCP tab
+      {
+        tab: "Tools",
+        mcp: "path/to/mcp.json",
+      },
+    ],
+  },
+  navbar: {
+    links: [
+      { type: "github", href: "https://github.com/org/repo" },
+      // types: github, twitter, discord, linkedin, youtube, slack,
+      //        mastodon, bluesky, reddit, npm, link
+    ],
+    primary: { type: "button", label: "Demo", href: "/demo" },
+  },
+  footer: {
+    links: [{ type: "github", href: "https://github.com/org/repo" }],
+  },
+  search: {
+    featured: ["introduction", "quickstart"],  // top results when empty query
+  },
+});
+

Page format

+

Pages are markdown files resolved relative to the config file directory. +If config is at docs/sourcey.config.ts, then page "quickstart" resolves +to docs/quickstart.md.

+
+
+ +
+
---
+title: Page Title
+description: One-line description for search and meta tags
+---
+
+Content here. Standard markdown with code blocks, tables, links.
+

Card Icon Contract

+

Sourcey card icons are Heroicons v2 outline names in kebab-case. The renderer +returns an empty icon for unknown names, so authoring must use exact names.

+

Known-good names for documentation cards include: academic-cap, arrow-path, +bell, bolt, book-open, chart-bar, check-circle, cloud-arrow-up, +code-bracket, command-line, cpu-chip, cube, document, +document-text, exclamation-triangle, globe-alt, key, lifebuoy, +light-bulb, lock-closed, magnifying-glass, map, rocket-launch, +server-stack, shield-check, sparkles, and wrench-screwdriver.

+

Invalid card icon names are a blocking quality issue. The build report includes +icon_validation; critique and revision must fix any +icon_validation.status: "invalid" result before the run is accepted.

+

Edge cases and stop conditions

+
    +
  • Only create tabs for content types the project actually has. Do not add an +OpenAPI tab if there is no spec file. Do not add a Doxygen tab without XML.
  • +
  • Do not document APIs by hand when a spec file exists — use the spec tab.
  • +
  • Keep navigation shallow: 1-2 tabs, 2-4 groups for most projects.
  • +
  • Use project brand colors if identifiable. Otherwise use a neutral palette.
  • +
  • Use only exact Heroicons v2 outline names for Sourcey card icon +attributes; never invent icon names.
  • +
  • When a grounded brief provides logo, favicon, color, or IA guidance, prefer +that over generic defaults.
  • +
  • Match the project's existing voice and terminology.
  • +
  • Never write docs that describe themselves as a preview, adoption, migration, +or tool-generated scaffold unless the repo's own evidence explicitly uses that +framing.
  • +
  • Do not write generated HTML, search indexes, or OG assets into the authored +docs source tree.
  • +
  • If output_dir lives under the repo root, gitignore it or call out the +missing ignore rule as an operational gap.
  • +
  • Build output may be regenerated in CI or deploy, but deploy must not author +or revise docs content.
  • +
  • Public deployments must be durable and socially credible. Do not treat a +throwaway preview URL, unrelated personal domain, placeholder parent site, or +sandbox subdomain as a completed public docs home.
  • +
  • Do not encode open-ended critique or revision behavior. Critique is one +bounded evaluation pass. Revision is at most one explicit bounded pass.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/web-fetch.html b/docs/sourcey-catalog/site/pages/web-fetch.html new file mode 100644 index 000000000..e4ab0796b --- /dev/null +++ b/docs/sourcey-catalog/site/pages/web-fetch.html @@ -0,0 +1,148 @@ + +web-fetch - Runx Governed Skill Catalog
Research and data

web-fetch

Fetch one URL, prove it was allowed, extract the part the caller asked for, and return that slice by digest with the provenance needed to trust it later.
    +
  • Group: Research and data
  • +
  • Source: skills/web-fetch/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/web-fetch/SKILL.md
  • +
+

Web Fetch

+

Fetch one URL, prove it was allowed, extract the part the caller asked for, and +return that slice by digest with the provenance needed to trust it later.

+

What this skill does

+

web-fetch resolves a single URL against a host allowlist, retrieves it, +extracts text, metadata, or links, and seals the result so a downstream step can +cite the fetch without re-fetching. It checks the final host against the +allowlist before and after redirects, retrieves up to max_bytes, and returns +the final URL, the HTTP status, a content_digest over the retrieved body, the +extracted slice, and a provenance block recording when it ran, every redirect +hop, and how many bytes it read. The body is referenced by digest; only the +extracted slice is inlined.

+

This is the primitive an agent reaches for when it has already decided which page +to read. The decision it makes easier is "can I read this page, and what did it +actually say", with the answer backed by a digest instead of a remembered +paraphrase. It differs from the research family: research and +deep-research decide which sources matter and synthesize across them, +while web-fetch retrieves exactly one source and refuses anything off the +allowlist.

+

When to use this skill

+
    +
  • An agent has chosen a specific page and needs its content bound to a +content_digest so a later step can cite it without re-fetching.
  • +
  • A research pass needs each source retrieved through a single bounded +net:allowlist fetch with a complete redirect chain and byte count.
  • +
  • A review must later prove what a page said at fetch time.
  • +
  • A follow-on skill (prior-art, vuln-triage, brief) needs one source extracted as +text, metadata, or links.
  • +
+

When not to use this skill

+
    +
  • To judge, rank, or synthesize across sources. That is the research family's +job; web-fetch retrieves exactly one source and refuses to reason over many.
  • +
  • To reach a host the caller did not declare in allowlist, including a host +reached only through a redirect.
  • +
  • To write anything. The only scope is net:allowlist; there is no repo, file, +wallet, or send authority here.
  • +
  • To inline a large raw body. The extracted slice is the payload; the full body +lives behind content_digest.
  • +
  • To carry secrets. Request headers may reference a credential by ${secret} +handle, but no header value, cookie, token, or auth string appears in the +output or the receipt.
  • +
+

Procedure

+
    +
  1. Require url and allowlist. Either missing returns needs_agent; the +fetch cannot run without a target and a declared scope.
  2. +
  3. Match the URL host against allowlist. On a miss, return policy_denied +before any network call, recording the attempted host and the allowlist it +was checked against.
  4. +
  5. Fetch, following redirects, re-checking each redirect target's host against +the same allowlist. A redirect that lands off-allowlist halts the fetch, +returns policy_denied with the hop that failed, and discards partial +bodies. Cap the read at max_bytes when set.
  6. +
  7. Compute content_digest over the retrieved body.
  8. +
  9. Extract per extract: text (readable body text, default), metadata +(title, description, canonical, declared language, content type), or links +(absolute hrefs found in the document).
  10. +
  11. Return fetch_result with the final URL, status, digest, extracted slice, +and provenance. Flag truncated reads in provenance; never return a clipped +read as if whole.
  12. +
+

Edge cases and stop conditions

+
    +
  • Missing url or allowlist: return needs_agent; the fetch has no target +or no scope to check against.
  • +
  • Host off the allowlist: stop with policy_denied before any network call; +record the attempted host, not a response body (there is none).
  • +
  • Redirect off the allowlist: halt the fetch, return policy_denied naming +the hop that failed, and discard the partial body.
  • +
  • Read clipped by max_bytes: flag truncated: true in provenance; the +digest is over the bytes actually retrieved.
  • +
  • Large raw body: never inline beyond the extracted slice; anything bigger +than the requested view is reachable only through content_digest.
  • +
  • Credential in a header: reference it by ${secret} handle only; no header +value, cookie, or token reaches the output or the receipt.
  • +
+

Output schema

+
+
+ +
+
fetch_result:
+  decision: ready | needs_agent | policy_denied
+  final_url: string            # URL after redirects, the one the digest is over
+  status: number               # HTTP status of the final response
+  content_digest: string       # digest of the retrieved body, algorithm prefix included
+  extract_mode: text | metadata | links
+  extracted: string | object | array   # string for text, object for metadata, array of hrefs for links
+  provenance:
+    fetched_at: string         # timestamp of the fetch
+    redirects: array           # ordered host hops, each re-checked against the allowlist
+    bytes: number              # bytes read
+    truncated: boolean         # true when max_bytes clipped the read
+  policy:
+    allowlist_decision: allowed | denied
+    attempted_host: string     # set on policy_denied
+    allowlist_checked: array   # the hosts the request was checked against
+

The sealed runx.receipt.v1 carries the final URL, status, content_digest, +byte count, the redirect chain, and the allowlist decision. It carries no header +values, no cookies, and no raw body beyond the digest.

+

Worked example

+

Input: url of the HTTP Semantics RFC, an allowlist of www.rfc-editor.org +and rfc-editor.org, extract: text, and max_bytes: 200000.

+

Output: decision: ready; the host matched the allowlist before the request +left; no redirects; status 200; content_digest is taken over the retrieved +body; extracted holds the readable text slice; provenance records +fetched_at, an empty redirect chain, 184302 bytes, and truncated: false. +The receipt seals with the final URL, status, digest, byte count, and the +allowlist decision; no header value reaches it.

+

Inputs

+
    +
  • url (required): the single URL to fetch; its host must match the allowlist.
  • +
  • allowlist (required): permitted hosts or host patterns; the URL and every +redirect target must match an entry.
  • +
  • extract (optional): text, metadata, or links. Defaults to text.
  • +
  • max_bytes (optional): cap on bytes read; a clipped read is flagged +truncated in provenance.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/pages/work-plan.html b/docs/sourcey-catalog/site/pages/work-plan.html new file mode 100644 index 000000000..97246a70a --- /dev/null +++ b/docs/sourcey-catalog/site/pages/work-plan.html @@ -0,0 +1,128 @@ + +work-plan - Runx Governed Skill Catalog
Operate

work-plan

Turn a build or automation objective into a bounded governed work plan.
    +
  • Group: Operate
  • +
  • Source: skills/work-plan/SKILL.md
  • +
  • Commit: 5afc25a83edf1c1320df7ac0d78c36f1523b5677
  • +
  • Path: skills/work-plan/SKILL.md
  • +
+

Work Plan

+

Turn a build or automation objective into a bounded governed work plan.

+

For cross-repo or cross-surface work, the output must be a phased +workspace_change_plan, not just a loose list of steps. The shared plan is the +thing that keeps repo-local workers aligned when one issue fans out into +multiple mutation surfaces.

+

When the objective originates from an existing thread, treat that thread +as provider-backed thread. GitHub issues, chat threads, support +tickets, and local agent sessions are adapter examples, not core nouns. The +plan should preserve the generic thread_locator and any supplied +thread.

+

The central insight: split at governance boundaries, not cognitive boundaries. +A skill keeps its full context window. If two actions need the same context +but different scopes, they are two invocations of the same skill with +different scopes — not two separate skills. The graph defines where authority +changes, where mutation happens, and where a gate needs to approve. That is +where steps break.

+

Work backward from the deliverable. Name the concrete artifact the objective +produces (spec, patch, PR, docs site, report). Then identify where authority +narrows: read-only analysis, write-access mutation, approval gates, review +boundaries. Each narrowing is a step boundary. Each step gets only the scopes +it needs — no step inherits from a prior step, each derives from the graph +grant independently.

+

Determine data dependencies between steps. A step that consumes output from +a prior step must come after it. Steps with no data dependency are candidates +for fanout. Do not parallelize steps that share mutation targets.

+

If the objective is ambiguous or required context is missing, surface open +questions explicitly rather than guessing. Open questions should name what +is missing, why it matters, and who can answer it.

+

Prefer fewer steps with clear scope boundaries. Three well-scoped steps +beat seven single-purpose fragments. Every step should have a clear entry +condition, action, and exit artifact.

+

Output

+
    +
  • change_set: the parent change artifact inherited from intake or constructed +for the objective when intake did not already produce one. It should preserve +the shared objective, target surfaces, invariants, and success criteria.
  • +
  • harness_context: when supplied, the same runx.receipt.v1 packet with state +advanced to planning_ready or blocked. Preserve source events, dedupe, +and triage fields rather than reconstructing them from prose.
  • +
  • objective_summary: one sentence capturing the deliverable.
  • +
  • workspace_change_plan: phased plan for the whole change set. It must +contain:
      +
    • plan_id
    • +
    • change_set_id
    • +
    • objective_summary
    • +
    • shared_invariants
    • +
    • success_criteria
    • +
    • phases: ordered array. Each phase:
        +
      • id
      • +
      • name
      • +
      • depends_on: prior phase ids
      • +
      • parallelizable: boolean
      • +
      • repo_change_requests: ordered array. Each request:
          +
        • repo
        • +
        • task_id
        • +
        • objective
        • +
        • depends_on: sibling repo change request ids this request waits on
        • +
        • shared_context_refs: references into the parent change set or prior +phase outputs
        • +
        • validation_commands
        • +
        • mutating
        • +
        +
      • +
      +
    • +
    • integration_checks: cross-repo checks that must pass before the overall +change set is considered done
    • +
    • open_questions
    • +
    +
  • +
  • orchestration_steps: canonical execution view of the plan as an ordered array. +Each step:
      +
    • id: kebab-case identifier
    • +
    • skill: skill name or path
    • +
    • scopes: scope strings this step requires
    • +
    • mutating: boolean
    • +
    • inputs: static input map
    • +
    • context_from: step_id.output_field data dependency references
    • +
    • description: what this step does and produces
    • +
    +
  • +
  • required_skills: skill names needed. Flag which exist vs need creation.
  • +
  • open_questions: missing context that must be answered before mutation.
  • +
+

Inputs

+
    +
  • objective (required): the build or skill objective to decompose.
  • +
  • project_context (optional): repo, product, or user context that +constrains the decomposition.
  • +
  • change_set (optional): parent change artifact from issue-intake or a +workspace supervisor. Prefer this when present.
  • +
  • harness_context (optional): portable issue control-plane packet from intake. +Preserve it as state, not as a prose handoff.
  • +
  • thread_locator (optional): canonical locator for the bounded thread the +plan is serving.
  • +
  • thread (optional): portable thread when the objective is +grounded in an existing issue, chat, ticket, or other adapter surface.
  • +
+
\ No newline at end of file diff --git a/docs/sourcey-catalog/site/search-index.json b/docs/sourcey-catalog/site/search-index.json new file mode 100644 index 000000000..f4d9fa980 --- /dev/null +++ b/docs/sourcey-catalog/site/search-index.json @@ -0,0 +1 @@ +[{"title":"Introduction","content":"Governed Runx skill catalog pinned to one upstream revision.","url":"/runxhq/runx/pages/introduction.html","tab":"Skills","category":"Pages"},{"title":"agency","content":"Run a standing, accountable team toward a mandate, one governed turn at a time.","url":"/runxhq/runx/pages/agency.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"agency - What this skill does","url":"/runxhq/runx/pages/agency.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"agency - When to use this skill","url":"/runxhq/runx/pages/agency.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"agency - When not to use this skill","url":"/runxhq/runx/pages/agency.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"Procedure","content":"agency - Procedure","url":"/runxhq/runx/pages/agency.html#procedure","tab":"Skills","category":"Sections"},{"title":"The measurable gate","content":"agency - The measurable gate","url":"/runxhq/runx/pages/agency.html#the-measurable-gate","tab":"Skills","category":"Sections"},{"title":"Contention","content":"agency - Contention","url":"/runxhq/runx/pages/agency.html#contention","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"agency - Edge cases and stop conditions","url":"/runxhq/runx/pages/agency.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"},{"title":"Output schema","content":"agency - Output schema","url":"/runxhq/runx/pages/agency.html#output-schema","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"agency - Inputs","url":"/runxhq/runx/pages/agency.html#inputs","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"agency - Worked example","url":"/runxhq/runx/pages/agency.html#worked-example","tab":"Skills","category":"Sections"},{"title":"Turn rules","content":"agency - Turn rules","url":"/runxhq/runx/pages/agency.html#turn-rules","tab":"Skills","category":"Sections"},{"title":"business-ops","content":"Turn one business signal into a replayable operations graph.","url":"/runxhq/runx/pages/business-ops.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"business-ops - What this skill does","url":"/runxhq/runx/pages/business-ops.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"What this skill deliberately does not do","content":"business-ops - What this skill deliberately does not do","url":"/runxhq/runx/pages/business-ops.html#what-this-skill-deliberately-does-not-do","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"business-ops - When to use this skill","url":"/runxhq/runx/pages/business-ops.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"business-ops - When not to use this skill","url":"/runxhq/runx/pages/business-ops.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"Mental model","content":"business-ops - Mental model","url":"/runxhq/runx/pages/business-ops.html#mental-model","tab":"Skills","category":"Sections"},{"title":"How this maps to real runx work","content":"business-ops - How this maps to real runx work","url":"/runxhq/runx/pages/business-ops.html#how-this-maps-to-real-runx-work","tab":"Skills","category":"Sections"},{"title":"Procedure","content":"business-ops - Procedure","url":"/runxhq/runx/pages/business-ops.html#procedure","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"business-ops - Edge cases and stop conditions","url":"/runxhq/runx/pages/business-ops.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"},{"title":"Output schema","content":"business-ops - Output schema","url":"/runxhq/runx/pages/business-ops.html#output-schema","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"business-ops - Worked example","url":"/runxhq/runx/pages/business-ops.html#worked-example","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"business-ops - Inputs","url":"/runxhq/runx/pages/business-ops.html#inputs","tab":"Skills","category":"Sections"},{"title":"operator-inbox","content":"Maintain a durable action queue without turning a connector into the owner of operator state.","url":"/runxhq/runx/pages/operator-inbox.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"operator-inbox - What this skill does","url":"/runxhq/runx/pages/operator-inbox.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"operator-inbox - When to use this skill","url":"/runxhq/runx/pages/operator-inbox.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"operator-inbox - When not to use this skill","url":"/runxhq/runx/pages/operator-inbox.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"Status rules","content":"operator-inbox - Status rules","url":"/runxhq/runx/pages/operator-inbox.html#status-rules","tab":"Skills","category":"Sections"},{"title":"Procedure","content":"operator-inbox - Procedure","url":"/runxhq/runx/pages/operator-inbox.html#procedure","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"operator-inbox - Edge cases and stop conditions","url":"/runxhq/runx/pages/operator-inbox.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"},{"title":"Output schema","content":"operator-inbox - Output schema","url":"/runxhq/runx/pages/operator-inbox.html#output-schema","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"operator-inbox - Worked example","url":"/runxhq/runx/pages/operator-inbox.html#worked-example","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"operator-inbox - Inputs","url":"/runxhq/runx/pages/operator-inbox.html#inputs","tab":"Skills","category":"Sections"},{"title":"ops-desk","content":"Operate a project, workspace, or account from an agent-controlled desk.","url":"/runxhq/runx/pages/ops-desk.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"ops-desk - What this skill does","url":"/runxhq/runx/pages/ops-desk.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"ops-desk - When to use this skill","url":"/runxhq/runx/pages/ops-desk.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"ops-desk - When not to use this skill","url":"/runxhq/runx/pages/ops-desk.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"Operating Model","content":"ops-desk - Operating Model","url":"/runxhq/runx/pages/ops-desk.html#operating-model","tab":"Skills","category":"Sections"},{"title":"Delegation Model","content":"ops-desk - Delegation Model","url":"/runxhq/runx/pages/ops-desk.html#delegation-model","tab":"Skills","category":"Sections"},{"title":"Procedure","content":"ops-desk - Procedure","url":"/runxhq/runx/pages/ops-desk.html#procedure","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"ops-desk - Edge cases and stop conditions","url":"/runxhq/runx/pages/ops-desk.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"},{"title":"Reference Loading","content":"ops-desk - Reference Loading","url":"/runxhq/runx/pages/ops-desk.html#reference-loading","tab":"Skills","category":"Sections"},{"title":"Output schema","content":"ops-desk - Output schema","url":"/runxhq/runx/pages/ops-desk.html#output-schema","tab":"Skills","category":"Sections"},{"title":"Decision rules","content":"ops-desk - Decision rules","url":"/runxhq/runx/pages/ops-desk.html#decision-rules","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"ops-desk - Inputs","url":"/runxhq/runx/pages/ops-desk.html#inputs","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"ops-desk - Worked example","url":"/runxhq/runx/pages/ops-desk.html#worked-example","tab":"Skills","category":"Sections"},{"title":"work-plan","content":"Turn a build or automation objective into a bounded governed work plan.","url":"/runxhq/runx/pages/work-plan.html","tab":"Skills","category":"Pages"},{"title":"Output","content":"work-plan - Output","url":"/runxhq/runx/pages/work-plan.html#output","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"work-plan - Inputs","url":"/runxhq/runx/pages/work-plan.html#inputs","tab":"Skills","category":"Sections"},{"title":"deep-research","content":"This graph turns one important question into a decision-ready brief.","url":"/runxhq/runx/pages/deep-research.html","tab":"Skills","category":"Pages"},{"title":"Output","content":"deep-research - Output","url":"/runxhq/runx/pages/deep-research.html#output","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"deep-research - Inputs","url":"/runxhq/runx/pages/deep-research.html#inputs","tab":"Skills","category":"Sections"},{"title":"research","content":"Research one bounded question and turn it into a decision-ready packet.","url":"/runxhq/runx/pages/research.html","tab":"Skills","category":"Pages"},{"title":"Operating rules","content":"research - Operating rules","url":"/runxhq/runx/pages/research.html#operating-rules","tab":"Skills","category":"Sections"},{"title":"Output","content":"research - Output","url":"/runxhq/runx/pages/research.html#output","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"research - Inputs","url":"/runxhq/runx/pages/research.html#inputs","tab":"Skills","category":"Sections"},{"title":"data-store","content":"Operate a data source through a governed adapter contract. This skill gives an agent enough context to read, append, or project state without learning provider secrets, inventing SQL, or depending on one storage backend.","url":"/runxhq/runx/pages/data-store.html","tab":"Skills","category":"Pages"},{"title":"Adapter selection","content":"data-store - Adapter selection","url":"/runxhq/runx/pages/data-store.html#adapter-selection","tab":"Skills","category":"Sections"},{"title":"What this skill does","content":"data-store - What this skill does","url":"/runxhq/runx/pages/data-store.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"data-store - When to use this skill","url":"/runxhq/runx/pages/data-store.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"data-store - When not to use this skill","url":"/runxhq/runx/pages/data-store.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"Procedure","content":"data-store - Procedure","url":"/runxhq/runx/pages/data-store.html#procedure","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"data-store - Edge cases and stop conditions","url":"/runxhq/runx/pages/data-store.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"},{"title":"Output schema","content":"data-store - Output schema","url":"/runxhq/runx/pages/data-store.html#output-schema","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"data-store - Worked example","url":"/runxhq/runx/pages/data-store.html#worked-example","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"data-store - Inputs","url":"/runxhq/runx/pages/data-store.html#inputs","tab":"Skills","category":"Sections"},{"title":"Invocation examples","content":"data-store - Invocation examples","url":"/runxhq/runx/pages/data-store.html#invocation-examples","tab":"Skills","category":"Sections"},{"title":"knowledge-router","content":"Route one question, source event, or support thread to the right knowledge sources and follow-up path.","url":"/runxhq/runx/pages/knowledge-router.html","tab":"Skills","category":"Pages"},{"title":"Output","content":"knowledge-router - Output","url":"/runxhq/runx/pages/knowledge-router.html#output","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"knowledge-router - Inputs","url":"/runxhq/runx/pages/knowledge-router.html#inputs","tab":"Skills","category":"Sections"},{"title":"web-fetch","content":"Fetch one URL, prove it was allowed, extract the part the caller asked for, and return that slice by digest with the provenance needed to trust it later.","url":"/runxhq/runx/pages/web-fetch.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"web-fetch - What this skill does","url":"/runxhq/runx/pages/web-fetch.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"web-fetch - When to use this skill","url":"/runxhq/runx/pages/web-fetch.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"web-fetch - When not to use this skill","url":"/runxhq/runx/pages/web-fetch.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"Procedure","content":"web-fetch - Procedure","url":"/runxhq/runx/pages/web-fetch.html#procedure","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"web-fetch - Edge cases and stop conditions","url":"/runxhq/runx/pages/web-fetch.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"},{"title":"Output schema","content":"web-fetch - Output schema","url":"/runxhq/runx/pages/web-fetch.html#output-schema","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"web-fetch - Worked example","url":"/runxhq/runx/pages/web-fetch.html#worked-example","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"web-fetch - Inputs","url":"/runxhq/runx/pages/web-fetch.html#inputs","tab":"Skills","category":"Sections"},{"title":"github-sync","content":"Decide exactly what state to move between a GitHub repo and the local graph, in which direction, and whether the agent is even allowed to write.","url":"/runxhq/runx/pages/github-sync.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"github-sync - What this skill does","url":"/runxhq/runx/pages/github-sync.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"github-sync - When to use this skill","url":"/runxhq/runx/pages/github-sync.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"github-sync - When not to use this skill","url":"/runxhq/runx/pages/github-sync.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"Procedure","content":"github-sync - Procedure","url":"/runxhq/runx/pages/github-sync.html#procedure","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"github-sync - Edge cases and stop conditions","url":"/runxhq/runx/pages/github-sync.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"},{"title":"Output schema","content":"github-sync - Output schema","url":"/runxhq/runx/pages/github-sync.html#output-schema","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"github-sync - Worked example","url":"/runxhq/runx/pages/github-sync.html#worked-example","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"github-sync - Inputs","url":"/runxhq/runx/pages/github-sync.html#inputs","tab":"Skills","category":"Sections"},{"title":"issue-intake","content":"Convert an inbound thread, support report, or operator request into one explicit intake decision plus the parent change artifact that downstream planning or mutation lanes must share.","url":"/runxhq/runx/pages/issue-intake.html","tab":"Skills","category":"Pages"},{"title":"Output Contract","content":"issue-intake - Output Contract","url":"/runxhq/runx/pages/issue-intake.html#output-contract","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"issue-intake - Inputs","url":"/runxhq/runx/pages/issue-intake.html#inputs","tab":"Skills","category":"Sections"},{"title":"issue-triage","content":"Turn noisy issue streams into bounded, evidence-backed action.","url":"/runxhq/runx/pages/issue-triage.html","tab":"Skills","category":"Pages"},{"title":"Output","content":"issue-triage - Output","url":"/runxhq/runx/pages/issue-triage.html#output","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"issue-triage - Inputs","url":"/runxhq/runx/pages/issue-triage.html#inputs","tab":"Skills","category":"Sections"},{"title":"issue-to-pr","content":"Drive one bounded thread-driven change through the scafld 2.4-compatible lifecycle and package the result as a provider-agnostic draft pull-request packet.","url":"/runxhq/runx/pages/issue-to-pr.html","tab":"Skills","category":"Pages"},{"title":"Lifecycle","content":"issue-to-pr - Lifecycle","url":"/runxhq/runx/pages/issue-to-pr.html#lifecycle","tab":"Skills","category":"Sections"},{"title":"Thread Story","content":"issue-to-pr - Thread Story","url":"/runxhq/runx/pages/issue-to-pr.html#thread-story","tab":"Skills","category":"Sections"},{"title":"Spec Authoring Contract","content":"issue-to-pr - Spec Authoring Contract","url":"/runxhq/runx/pages/issue-to-pr.html#spec-authoring-contract","tab":"Skills","category":"Sections"},{"title":"Fix Authoring Contract","content":"issue-to-pr - Fix Authoring Contract","url":"/runxhq/runx/pages/issue-to-pr.html#fix-authoring-contract","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"issue-to-pr - Inputs","url":"/runxhq/runx/pages/issue-to-pr.html#inputs","tab":"Skills","category":"Sections"},{"title":"Structured Output","content":"issue-to-pr - Structured Output","url":"/runxhq/runx/pages/issue-to-pr.html#structured-output","tab":"Skills","category":"Sections"},{"title":"release","content":"Turn a proposed release into an audited publication. The skill owns the release decision process: evidence gathering, changelog preparation, approval, publish handoff, verification, and announcement. It does not own a project's custom release implementation. Project-specific topology lives in a release profile that names existing commands, workflows, registries, deploy targets, and verification readbacks.","url":"/runxhq/runx/pages/release.html","tab":"Skills","category":"Pages"},{"title":"Phases","content":"release - Phases","url":"/runxhq/runx/pages/release.html#phases","tab":"Skills","category":"Sections"},{"title":"prepare","content":"release - prepare","url":"/runxhq/runx/pages/release.html#prepare","tab":"Skills","category":"Sections"},{"title":"approve-publish","content":"release - approve-publish","url":"/runxhq/runx/pages/release.html#approve-publish","tab":"Skills","category":"Sections"},{"title":"publish-release","content":"release - publish-release","url":"/runxhq/runx/pages/release.html#publish-release","tab":"Skills","category":"Sections"},{"title":"verify-release","content":"release - verify-release","url":"/runxhq/runx/pages/release.html#verify-release","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"release - Inputs","url":"/runxhq/runx/pages/release.html#inputs","tab":"Skills","category":"Sections"},{"title":"Outputs","content":"release - Outputs","url":"/runxhq/runx/pages/release.html#outputs","tab":"Skills","category":"Sections"},{"title":"Trust boundary","content":"release - Trust boundary","url":"/runxhq/runx/pages/release.html#trust-boundary","tab":"Skills","category":"Sections"},{"title":"Scopes","content":"release - Scopes","url":"/runxhq/runx/pages/release.html#scopes","tab":"Skills","category":"Sections"},{"title":"Tasks","content":"release - Tasks","url":"/runxhq/runx/pages/release.html#tasks","tab":"Skills","category":"Sections"},{"title":"audit-receipt","content":"Audit a sealed run for authority over-reach, using its own receipt as evidence.","url":"/runxhq/runx/pages/audit-receipt.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"audit-receipt - What this skill does","url":"/runxhq/runx/pages/audit-receipt.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"Core principles","content":"audit-receipt - Core principles","url":"/runxhq/runx/pages/audit-receipt.html#core-principles","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"audit-receipt - When to use this skill","url":"/runxhq/runx/pages/audit-receipt.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"audit-receipt - When not to use this skill","url":"/runxhq/runx/pages/audit-receipt.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"Diagnostics","content":"audit-receipt - Diagnostics","url":"/runxhq/runx/pages/audit-receipt.html#diagnostics","tab":"Skills","category":"Sections"},{"title":"Procedure","content":"audit-receipt - Procedure","url":"/runxhq/runx/pages/audit-receipt.html#procedure","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"audit-receipt - Edge cases and stop conditions","url":"/runxhq/runx/pages/audit-receipt.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"},{"title":"Output schema (receipt_audit)","content":"audit-receipt - Output schema (receipt_audit)","url":"/runxhq/runx/pages/audit-receipt.html#output-schema-receipt-audit","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"audit-receipt - Worked example","url":"/runxhq/runx/pages/audit-receipt.html#worked-example","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"audit-receipt - Inputs","url":"/runxhq/runx/pages/audit-receipt.html#inputs","tab":"Skills","category":"Sections"},{"title":"cve-audit","content":"This skill audits exact npm versions from an immutable `package-lock.json` against the public OSV API. It emits a machine-readable audit result, `evidence.json`, and a finding-by-finding Markdown report. The governed graph in `X.yaml` independently replays every query and seals a delivery packet only when the reported and replayed advisory sets match exactly.","url":"/runxhq/runx/pages/cve-audit.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"cve-audit - What this skill does","url":"/runxhq/runx/pages/cve-audit.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"cve-audit - When to use this skill","url":"/runxhq/runx/pages/cve-audit.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"cve-audit - When not to use this skill","url":"/runxhq/runx/pages/cve-audit.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"cve-audit - Inputs","url":"/runxhq/runx/pages/cve-audit.html#inputs","tab":"Skills","category":"Sections"},{"title":"Procedure","content":"cve-audit - Procedure","url":"/runxhq/runx/pages/cve-audit.html#procedure","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"cve-audit - Edge cases and stop conditions","url":"/runxhq/runx/pages/cve-audit.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"},{"title":"Output schema","content":"cve-audit - Output schema","url":"/runxhq/runx/pages/cve-audit.html#output-schema","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"cve-audit - Worked example","url":"/runxhq/runx/pages/cve-audit.html#worked-example","tab":"Skills","category":"Sections"},{"title":"least-privilege","content":"Turn granted authority plus observed usage into a bounded attenuation proposal.","url":"/runxhq/runx/pages/least-privilege.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"least-privilege - What this skill does","url":"/runxhq/runx/pages/least-privilege.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"least-privilege - When to use this skill","url":"/runxhq/runx/pages/least-privilege.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"least-privilege - When not to use this skill","url":"/runxhq/runx/pages/least-privilege.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"Procedure","content":"least-privilege - Procedure","url":"/runxhq/runx/pages/least-privilege.html#procedure","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"least-privilege - Edge cases and stop conditions","url":"/runxhq/runx/pages/least-privilege.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"},{"title":"Output schema","content":"least-privilege - Output schema","url":"/runxhq/runx/pages/least-privilege.html#output-schema","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"least-privilege - Worked example","url":"/runxhq/runx/pages/least-privilege.html#worked-example","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"least-privilege - Inputs","url":"/runxhq/runx/pages/least-privilege.html#inputs","tab":"Skills","category":"Sections"},{"title":"policy-author","content":"Author one governed runx operational policy from intent, and prove it lints.","url":"/runxhq/runx/pages/policy-author.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"policy-author - What this skill does","url":"/runxhq/runx/pages/policy-author.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"Core principles","content":"policy-author - Core principles","url":"/runxhq/runx/pages/policy-author.html#core-principles","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"policy-author - When to use this skill","url":"/runxhq/runx/pages/policy-author.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"policy-author - When not to use this skill","url":"/runxhq/runx/pages/policy-author.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"The operational policy model","content":"policy-author - The operational policy model","url":"/runxhq/runx/pages/policy-author.html#the-operational-policy-model","tab":"Skills","category":"Sections"},{"title":"Lint diagnostics","content":"policy-author - Lint diagnostics","url":"/runxhq/runx/pages/policy-author.html#lint-diagnostics","tab":"Skills","category":"Sections"},{"title":"Procedure","content":"policy-author - Procedure","url":"/runxhq/runx/pages/policy-author.html#procedure","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"policy-author - Edge cases and stop conditions","url":"/runxhq/runx/pages/policy-author.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"},{"title":"Output schema (policy_proposal)","content":"policy-author - Output schema (policy_proposal)","url":"/runxhq/runx/pages/policy-author.html#output-schema-policy-proposal","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"policy-author - Worked example","url":"/runxhq/runx/pages/policy-author.html#worked-example","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"policy-author - Inputs","url":"/runxhq/runx/pages/policy-author.html#inputs","tab":"Skills","category":"Sections"},{"title":"review-receipt","content":"Diagnose what went wrong in a skill or graph execution and propose the smallest change that fixes it.","url":"/runxhq/runx/pages/review-receipt.html","tab":"Skills","category":"Pages"},{"title":"Agent-mediated suspension is not a failure","content":"review-receipt - Agent-mediated suspension is not a failure","url":"/runxhq/runx/pages/review-receipt.html#agent-mediated-suspension-is-not-a-failure","tab":"Skills","category":"Sections"},{"title":"Output","content":"review-receipt - Output","url":"/runxhq/runx/pages/review-receipt.html#output","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"review-receipt - Inputs","url":"/runxhq/runx/pages/review-receipt.html#inputs","tab":"Skills","category":"Sections"},{"title":"sandbox-harden","content":"Decide the narrowest sandbox a workload can run inside without breaking it.","url":"/runxhq/runx/pages/sandbox-harden.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"sandbox-harden - What this skill does","url":"/runxhq/runx/pages/sandbox-harden.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"sandbox-harden - When to use this skill","url":"/runxhq/runx/pages/sandbox-harden.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"sandbox-harden - When not to use this skill","url":"/runxhq/runx/pages/sandbox-harden.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"Procedure","content":"sandbox-harden - Procedure","url":"/runxhq/runx/pages/sandbox-harden.html#procedure","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"sandbox-harden - Edge cases and stop conditions","url":"/runxhq/runx/pages/sandbox-harden.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"},{"title":"Output schema","content":"sandbox-harden - Output schema","url":"/runxhq/runx/pages/sandbox-harden.html#output-schema","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"sandbox-harden - Worked example","url":"/runxhq/runx/pages/sandbox-harden.html#worked-example","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"sandbox-harden - Inputs","url":"/runxhq/runx/pages/sandbox-harden.html#inputs","tab":"Skills","category":"Sections"},{"title":"governed-outbound","content":"Take something from outside, make it safe to send, authorize the exact outbound plan, and leave proof. `governed-outbound` prepares the boundary crossing; it does not claim the configured provider delivered anything.","url":"/runxhq/runx/pages/governed-outbound.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"governed-outbound - What this skill does","url":"/runxhq/runx/pages/governed-outbound.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"governed-outbound - When to use this skill","url":"/runxhq/runx/pages/governed-outbound.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"governed-outbound - When not to use this skill","url":"/runxhq/runx/pages/governed-outbound.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"How the chain is wired","content":"governed-outbound - How the chain is wired","url":"/runxhq/runx/pages/governed-outbound.html#how-the-chain-is-wired","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"governed-outbound - Edge cases and stop conditions","url":"/runxhq/runx/pages/governed-outbound.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"},{"title":"Output","content":"governed-outbound - Output","url":"/runxhq/runx/pages/governed-outbound.html#output","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"governed-outbound - Inputs","url":"/runxhq/runx/pages/governed-outbound.html#inputs","tab":"Skills","category":"Sections"},{"title":"run-history","content":"Turn runx's own run ledger into a governed, read-only report.","url":"/runxhq/runx/pages/run-history.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"run-history - What this skill does","url":"/runxhq/runx/pages/run-history.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"Core principles","content":"run-history - Core principles","url":"/runxhq/runx/pages/run-history.html#core-principles","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"run-history - When to use this skill","url":"/runxhq/runx/pages/run-history.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"run-history - When not to use this skill","url":"/runxhq/runx/pages/run-history.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"Signals and norms","content":"run-history - Signals and norms","url":"/runxhq/runx/pages/run-history.html#signals-and-norms","tab":"Skills","category":"Sections"},{"title":"Output schema (history_report)","content":"run-history - Output schema (history_report)","url":"/runxhq/runx/pages/run-history.html#output-schema-history-report","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"run-history - Worked example","url":"/runxhq/runx/pages/run-history.html#worked-example","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"run-history - Inputs","url":"/runxhq/runx/pages/run-history.html#inputs","tab":"Skills","category":"Sections"},{"title":"sourcey","content":"Generate a documentation site for a project using Sourcey. Sourcey is a static documentation generator that produces HTML sites from markdown pages, OpenAPI specs, Doxygen XML, and MCP server snapshots.","url":"/runxhq/runx/pages/sourcey.html","tab":"Skills","category":"Pages"},{"title":"What this skill does","content":"sourcey - What this skill does","url":"/runxhq/runx/pages/sourcey.html#what-this-skill-does","tab":"Skills","category":"Sections"},{"title":"When to use this skill","content":"sourcey - When to use this skill","url":"/runxhq/runx/pages/sourcey.html#when-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"When not to use this skill","content":"sourcey - When not to use this skill","url":"/runxhq/runx/pages/sourcey.html#when-not-to-use-this-skill","tab":"Skills","category":"Sections"},{"title":"Documentation rules","content":"sourcey - Documentation rules","url":"/runxhq/runx/pages/sourcey.html#documentation-rules","tab":"Skills","category":"Sections"},{"title":"Canonical semantics","content":"sourcey - Canonical semantics","url":"/runxhq/runx/pages/sourcey.html#canonical-semantics","tab":"Skills","category":"Sections"},{"title":"Procedure","content":"sourcey - Procedure","url":"/runxhq/runx/pages/sourcey.html#procedure","tab":"Skills","category":"Sections"},{"title":"Discovery contract","content":"sourcey - Discovery contract","url":"/runxhq/runx/pages/sourcey.html#discovery-contract","tab":"Skills","category":"Sections"},{"title":"Output schema","content":"sourcey - Output schema","url":"/runxhq/runx/pages/sourcey.html#output-schema","tab":"Skills","category":"Sections"},{"title":"Worked example","content":"sourcey - Worked example","url":"/runxhq/runx/pages/sourcey.html#worked-example","tab":"Skills","category":"Sections"},{"title":"Inputs","content":"sourcey - Inputs","url":"/runxhq/runx/pages/sourcey.html#inputs","tab":"Skills","category":"Sections"},{"title":"Repository Contract","content":"sourcey - Repository Contract","url":"/runxhq/runx/pages/sourcey.html#repository-contract","tab":"Skills","category":"Sections"},{"title":"Astro host pattern","content":"sourcey - Astro host pattern","url":"/runxhq/runx/pages/sourcey.html#astro-host-pattern","tab":"Skills","category":"Sections"},{"title":"Config reference","content":"sourcey - Config reference","url":"/runxhq/runx/pages/sourcey.html#config-reference","tab":"Skills","category":"Sections"},{"title":"Page format","content":"sourcey - Page format","url":"/runxhq/runx/pages/sourcey.html#page-format","tab":"Skills","category":"Sections"},{"title":"Card Icon Contract","content":"sourcey - Card Icon Contract","url":"/runxhq/runx/pages/sourcey.html#card-icon-contract","tab":"Skills","category":"Sections"},{"title":"Edge cases and stop conditions","content":"sourcey - Edge cases and stop conditions","url":"/runxhq/runx/pages/sourcey.html#edge-cases-and-stop-conditions","tab":"Skills","category":"Sections"}] \ No newline at end of file diff --git a/docs/sourcey-catalog/site/sitemap.xml b/docs/sourcey-catalog/site/sitemap.xml new file mode 100644 index 000000000..7a3f65c53 --- /dev/null +++ b/docs/sourcey-catalog/site/sitemap.xml @@ -0,0 +1,29 @@ + + + + https://github.com/runxhq/runx/pages/introduction.html + https://github.com/runxhq/runx/pages/agency.html + https://github.com/runxhq/runx/pages/business-ops.html + https://github.com/runxhq/runx/pages/operator-inbox.html + https://github.com/runxhq/runx/pages/ops-desk.html + https://github.com/runxhq/runx/pages/work-plan.html + https://github.com/runxhq/runx/pages/deep-research.html + https://github.com/runxhq/runx/pages/research.html + https://github.com/runxhq/runx/pages/data-store.html + https://github.com/runxhq/runx/pages/knowledge-router.html + https://github.com/runxhq/runx/pages/web-fetch.html + https://github.com/runxhq/runx/pages/github-sync.html + https://github.com/runxhq/runx/pages/issue-intake.html + https://github.com/runxhq/runx/pages/issue-triage.html + https://github.com/runxhq/runx/pages/issue-to-pr.html + https://github.com/runxhq/runx/pages/release.html + https://github.com/runxhq/runx/pages/audit-receipt.html + https://github.com/runxhq/runx/pages/cve-audit.html + https://github.com/runxhq/runx/pages/least-privilege.html + https://github.com/runxhq/runx/pages/policy-author.html + https://github.com/runxhq/runx/pages/review-receipt.html + https://github.com/runxhq/runx/pages/sandbox-harden.html + https://github.com/runxhq/runx/pages/governed-outbound.html + https://github.com/runxhq/runx/pages/run-history.html + https://github.com/runxhq/runx/pages/sourcey.html + \ No newline at end of file diff --git a/docs/sourcey-catalog/site/sourcey.css b/docs/sourcey-catalog/site/sourcey.css new file mode 100644 index 000000000..0dd4e50c2 --- /dev/null +++ b/docs/sourcey-catalog/site/sourcey.css @@ -0,0 +1,2474 @@ +/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:"Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-mono:ui-monospace, "SF Mono", "Cascadia Code", Consolas, "Liberation Mono", Menlo, monospace;--color-red-100:oklch(93.6% .032 17.717);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-600:oklch(57.7% .245 27.325);--color-red-800:oklch(44.4% .177 26.899);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-900:oklch(40.8% .123 38.172);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-green-100:oklch(96.2% .044 156.743);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-900:oklch(37.8% .077 168.94);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-600:oklch(51.1% .262 276.966);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-700:oklch(49.6% .265 301.924);--color-rose-50:oklch(96.9% .015 12.422);--color-rose-100:oklch(94.1% .03 12.58);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-300:oklch(81% .117 11.638);--color-rose-600:oklch(58.6% .253 17.585);--color-rose-800:oklch(45.5% .188 13.697);--color-rose-900:oklch(41% .159 10.272);--color-gray-50:245 245 250;--color-gray-100:241 241 245;--color-gray-200:225 225 229;--color-gray-300:209 209 213;--color-gray-400:161 161 165;--color-gray-500:115 115 119;--color-gray-600:83 83 87;--color-gray-700:65 65 69;--color-gray-800:40 40 44;--color-gray-900:25 25 30;--color-gray-950:13 13 17;--color-stone-50:250 250 249;--color-stone-100:245 245 244;--color-stone-200:231 229 228;--color-stone-400:168 162 158;--color-stone-500:120 113 108;--color-stone-600:87 83 78;--color-stone-700:68 64 60;--color-stone-900:oklch(21.6% .006 56.043);--color-stone-950:12 10 9;--color-white:#fff;--spacing:.25rem;--container-2xl:42rem;--container-3xl:48rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:.75rem;--sidebar-width:18rem;--header-height:7rem;--toc-width:19rem;--toc-inner-width:16.5rem;--content-padding:2.5rem;--content-max-width:44rem;--color-primary:99 102 241;--color-primary-light:129 140 248;--color-primary-dark:79 70 229;--color-primary-ink:79 70 229;--color-background-light:255 255 255;--color-background-dark:11 12 16;--color-code-block-light:255 255 255;--color-code-block-dark:11 12 14;--color-success:34 197 94;--color-overlay:0 0 0;--color-border-dark-subtle:255 255 255;--color-surface-dark-tint:255 255 255;--method-get:#16a34a;--method-post:#2563eb;--method-put:#d97706;--method-delete:#dc2626;--method-patch:#9333ea;--method-tool:#9333ea;--method-resource:#16a34a;--method-prompt:#2563eb}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.top-0{top:0}.top-3{top:calc(var(--spacing) * 3)}.top-\[2\.75rem\]{top:2.75rem}.top-full{top:100%}.right-0{right:0}.right-4{right:calc(var(--spacing) * 4)}.right-auto{right:auto}.bottom-0{bottom:0}.left-0{left:0}.left-1\/2{left:50%}.-z-10{z-index:calc(10 * -1)}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-\[21\]{z-index:21}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mx-4{margin-inline:calc(var(--spacing) * 4)}.mx-auto{margin-inline:auto}.my-1{margin-block:var(--spacing)}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);margin-top:1.2em;margin-bottom:1.2em;font-size:1.25em;line-height:1.6}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:decimal}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:disc}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.25em;font-weight:600}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em;font-style:italic;font-weight:500}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:2em;margin-bottom:1em;font-size:1.5em;font-weight:700;line-height:1.33333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em;display:block}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-kbd);box-shadow:0 0 0 1px var(--tw-prose-kbd-shadows), 0 3px 0 var(--tw-prose-kbd-shadows);padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;border-radius:.3125rem;padding-inline-start:.375em;font-family:inherit;font-size:.875em;font-weight:500}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);padding-top:.857143em;padding-inline-end:1.14286em;padding-bottom:.857143em;border-radius:.375rem;margin-top:1.71429em;margin-bottom:1.71429em;padding-inline-start:1.14286em;font-size:.875em;font-weight:400;line-height:1.71429;overflow-x:auto}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit;background-color:#0000;border-width:0;border-radius:0;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){table-layout:auto;width:100%;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.71429}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);vertical-align:bottom;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em;font-weight:600}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);margin-top:.857143em;font-size:.875em;line-height:1.42857}.prose{--tw-prose-body:oklch(37.3% .034 259.733);--tw-prose-headings:oklch(21% .034 264.665);--tw-prose-lead:oklch(44.6% .03 256.802);--tw-prose-links:oklch(21% .034 264.665);--tw-prose-bold:oklch(21% .034 264.665);--tw-prose-counters:oklch(55.1% .027 264.364);--tw-prose-bullets:oklch(87.2% .01 258.338);--tw-prose-hr:oklch(92.8% .006 264.531);--tw-prose-quotes:oklch(21% .034 264.665);--tw-prose-quote-borders:oklch(92.8% .006 264.531);--tw-prose-captions:oklch(55.1% .027 264.364);--tw-prose-kbd:oklch(21% .034 264.665);--tw-prose-kbd-shadows:oklab(21% -.00316127 -.0338527/.1);--tw-prose-code:oklch(21% .034 264.665);--tw-prose-pre-code:oklch(92.8% .006 264.531);--tw-prose-pre-bg:oklch(27.8% .033 256.848);--tw-prose-th-borders:oklch(87.2% .01 258.338);--tw-prose-td-borders:oklch(92.8% .006 264.531);--tw-prose-invert-body:oklch(87.2% .01 258.338);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.7% .022 261.325);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.7% .022 261.325);--tw-prose-invert-bullets:oklch(44.6% .03 256.802);--tw-prose-invert-hr:oklch(37.3% .034 259.733);--tw-prose-invert-quotes:oklch(96.7% .003 264.542);--tw-prose-invert-quote-borders:oklch(37.3% .034 259.733);--tw-prose-invert-captions:oklch(70.7% .022 261.325);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(87.2% .01 258.338);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .03 256.802);--tw-prose-invert-td-borders:oklch(37.3% .034 259.733);font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.571429em;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.prose-sm{font-size:.875rem;line-height:1.71429}.prose-sm :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.14286em;margin-bottom:1.14286em}.prose-sm :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.888889em;margin-bottom:.888889em;font-size:1.28571em;line-height:1.55556}.prose-sm :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.33333em;margin-bottom:1.33333em;padding-inline-start:1.11111em}.prose-sm :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:.8em;font-size:2.14286em;line-height:1.2}.prose-sm :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.6em;margin-bottom:.8em;font-size:1.42857em;line-height:1.4}.prose-sm :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.55556em;margin-bottom:.444444em;font-size:1.28571em;line-height:1.55556}.prose-sm :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.42857em;margin-bottom:.571429em;line-height:1.42857}.prose-sm :where(img):not(:where([class~=not-prose],[class~=not-prose] *)),.prose-sm :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.71429em;margin-bottom:1.71429em}.prose-sm :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose-sm :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.71429em;margin-bottom:1.71429em}.prose-sm :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.142857em;padding-inline-end:.357143em;padding-bottom:.142857em;border-radius:.3125rem;padding-inline-start:.357143em;font-size:.857143em}.prose-sm :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.857143em}.prose-sm :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.9em}.prose-sm :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.888889em}.prose-sm :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.666667em;padding-inline-end:1em;padding-bottom:.666667em;border-radius:.25rem;margin-top:1.66667em;margin-bottom:1.66667em;padding-inline-start:1em;font-size:.857143em;line-height:1.66667}.prose-sm :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)),.prose-sm :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.14286em;margin-bottom:1.14286em;padding-inline-start:1.57143em}.prose-sm :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.285714em;margin-bottom:.285714em}.prose-sm :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose-sm :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.428571em}.prose-sm :where(.prose-sm>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.571429em;margin-bottom:.571429em}.prose-sm :where(.prose-sm>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.14286em}.prose-sm :where(.prose-sm>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.14286em}.prose-sm :where(.prose-sm>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.14286em}.prose-sm :where(.prose-sm>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.14286em}.prose-sm :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.571429em;margin-bottom:.571429em}.prose-sm :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.14286em;margin-bottom:1.14286em}.prose-sm :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.14286em}.prose-sm :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.285714em;padding-inline-start:1.57143em}.prose-sm :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2.85714em;margin-bottom:2.85714em}.prose-sm :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose-sm :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose-sm :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose-sm :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.857143em;line-height:1.5}.prose-sm :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:1em;padding-bottom:.666667em;padding-inline-start:1em}.prose-sm :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose-sm :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose-sm :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.666667em;padding-inline-end:1em;padding-bottom:.666667em;padding-inline-start:1em}.prose-sm :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose-sm :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose-sm :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.71429em;margin-bottom:1.71429em}.prose-sm :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose-sm :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.666667em;font-size:.857143em;line-height:1.33333}.prose-sm :where(.prose-sm>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(.prose-sm>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.-mt-10{margin-top:calc(var(--spacing) * -10)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-16{margin-top:calc(var(--spacing) * 16)}.mr-0\.5{margin-right:calc(var(--spacing) * .5)}.mb-0{margin-bottom:0}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-14{margin-bottom:calc(var(--spacing) * 14)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.box-border{box-sizing:border-box}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-\[26px\]{width:26px;height:26px}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-3{height:calc(var(--spacing) * 3)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-\[1\.5px\]{height:1.5px}.h-\[1lh\]{height:1lh}.h-full{height:100%}.max-h-full{max-height:100%}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-3{width:calc(var(--spacing) * 3)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-8{width:calc(var(--spacing) * 8)}.w-\[16\.5rem\]{width:16.5rem}.w-\[18rem\]{width:18rem}.w-\[19rem\]{width:19rem}.w-\[28rem\]{width:28rem}.w-auto{width:auto}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-\[28rem\]{max-width:28rem}.max-w-\[92rem\]{max-width:92rem}.max-w-none{max-width:none}.min-w-0{min-width:0}.min-w-4{min-width:calc(var(--spacing) * 4)}.min-w-\[42px\]{min-width:42px}.min-w-\[43px\]{min-width:43px}.min-w-\[120px\]{min-width:120px}.min-w-\[140px\]{min-width:140px}.flex-1{flex:1}.flex-none{flex:none}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.flex-col{flex-direction:column}.flex-row-reverse{flex-direction:row-reverse}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-12{gap:calc(var(--spacing) * 12)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-px>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(1px * var(--tw-space-y-reverse));margin-block-end:calc(1px * calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}:where(.space-x-2\.5>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2.5) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 6) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-1{row-gap:var(--spacing)}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-\[var\(--radius\)\]{border-radius:var(--radius)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-\[rgb\(var\(--color-gray-100\)\)\]{border-color:rgb(var(--color-gray-100))}.border-\[rgb\(var\(--color-gray-200\)\/0\.7\)\]{border-color:rgb(var(--color-gray-200)/.7)}.border-\[rgb\(var\(--color-gray-500\)\/0\.08\)\]{border-color:rgb(var(--color-gray-500)/.08)}.border-\[rgb\(var\(--color-stone-200\)\)\]{border-color:rgb(var(--color-stone-200))}.bg-\[rgb\(var\(--color-background-light\)\)\]{background-color:rgb(var(--color-background-light))}.bg-\[rgb\(var\(--color-code-block-light\)\)\]{background-color:rgb(var(--color-code-block-light))}.bg-\[rgb\(var\(--color-gray-100\)\/0\.5\)\]{background-color:rgb(var(--color-gray-100)/.5)}.bg-\[rgb\(var\(--color-primary\)\)\]{background-color:rgb(var(--color-primary))}.bg-\[rgb\(var\(--color-primary-dark\)\)\]{background-color:rgb(var(--color-primary-dark))}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-100\/50{background-color:#fef3c680}@supports (color:color-mix(in lab, red, red)){.bg-amber-100\/50{background-color:color-mix(in oklab, var(--color-amber-100) 50%, transparent)}}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-100\/50{background-color:#dbeafe80}@supports (color:color-mix(in lab, red, red)){.bg-blue-100\/50{background-color:color-mix(in oklab, var(--color-blue-100) 50%, transparent)}}.bg-blue-400\/20{background-color:#54a2ff33}@supports (color:color-mix(in lab, red, red)){.bg-blue-400\/20{background-color:color-mix(in oklab, var(--color-blue-400) 20%, transparent)}}.bg-blue-500{background-color:var(--color-blue-500)}.bg-gray-400\/20{background-color:color-mix(in srgb, 161 161 165 20%, transparent)}@supports (color:color-mix(in lab, red, red)){.bg-gray-400\/20{background-color:color-mix(in oklab, var(--color-gray-400) 20%, transparent)}}.bg-green-100{background-color:var(--color-green-100)}.bg-green-100\/50{background-color:#dcfce780}@supports (color:color-mix(in lab, red, red)){.bg-green-100\/50{background-color:color-mix(in oklab, var(--color-green-100) 50%, transparent)}}.bg-green-400\/20{background-color:#05df7233}@supports (color:color-mix(in lab, red, red)){.bg-green-400\/20{background-color:color-mix(in oklab, var(--color-green-400) 20%, transparent)}}.bg-green-500{background-color:var(--color-green-500)}.bg-orange-100{background-color:var(--color-orange-100)}.bg-purple-400\/20{background-color:#c07eff33}@supports (color:color-mix(in lab, red, red)){.bg-purple-400\/20{background-color:color-mix(in oklab, var(--color-purple-400) 20%, transparent)}}.bg-purple-500{background-color:var(--color-purple-500)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-100\/50{background-color:#ffe2e280}@supports (color:color-mix(in lab, red, red)){.bg-red-100\/50{background-color:color-mix(in oklab, var(--color-red-100) 50%, transparent)}}.bg-gradient-to-b{--tw-gradient-position:to bottom in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-\[rgb\(var\(--color-background-light\)\)\]{--tw-gradient-from:rgb(var(--color-background-light));--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.object-contain{object-fit:contain}.p-2{padding:calc(var(--spacing) * 2)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-12{padding-inline:calc(var(--spacing) * 12)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-8{padding-block:calc(var(--spacing) * 8)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-10{padding-top:calc(var(--spacing) * 10)}.pt-\[8\.5rem\]{padding-top:8.5rem}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pb-2\.5{padding-bottom:calc(var(--spacing) * 2.5)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-10{padding-bottom:calc(var(--spacing) * 10)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-3\.5{padding-left:calc(var(--spacing) * 3.5)}.pl-10{padding-left:calc(var(--spacing) * 10)}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.55rem\]{font-size:.55rem}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.leading-\[1\.35rem\]{--tw-leading:1.35rem;line-height:1.35rem}.leading-none{--tw-leading:1;line-height:1}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-\[0\.14em\]{--tw-tracking:.14em;letter-spacing:.14em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.break-words{overflow-wrap:break-word}.\[word-break\:break-word\]{word-break:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.text-\[rgb\(var\(--color-gray-400\)\)\]{color:rgb(var(--color-gray-400))}.text-\[rgb\(var\(--color-gray-500\)\)\]{color:rgb(var(--color-gray-500))}.text-\[rgb\(var\(--color-gray-600\)\)\]{color:rgb(var(--color-gray-600))}.text-\[rgb\(var\(--color-gray-700\)\)\]{color:rgb(var(--color-gray-700))}.text-\[rgb\(var\(--color-gray-800\)\)\]{color:rgb(var(--color-gray-800))}.text-\[rgb\(var\(--color-gray-900\)\)\]{color:rgb(var(--color-gray-900))}.text-\[rgb\(var\(--color-primary\)\)\]{color:rgb(var(--color-primary))}.text-\[rgb\(var\(--color-primary-ink\)\)\]{color:rgb(var(--color-primary-ink))}.text-\[rgb\(var\(--color-stone-400\)\)\]{color:rgb(var(--color-stone-400))}.text-\[rgb\(var\(--color-stone-500\)\)\]{color:rgb(var(--color-stone-500))}.text-\[rgb\(var\(--color-stone-600\)\)\]{color:rgb(var(--color-stone-600))}.text-\[rgb\(var\(--color-stone-950\)\)\]{color:rgb(var(--color-stone-950))}.text-amber-600{color:var(--color-amber-600)}.text-amber-900{color:var(--color-amber-900)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-gray-700{color:var(--color-gray-700)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-orange-900{color:var(--color-orange-900)}.text-purple-700{color:var(--color-purple-700)}.text-red-600{color:var(--color-red-600)}.text-red-800{color:var(--color-red-800)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.ordinal{--tw-ordinal:ordinal;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-90{opacity:.9}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-\[rgb\(var\(--color-gray-400\)\/0\.3\)\]{--tw-ring-color:rgb(var(--color-gray-400)/.3)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.outline-0{outline-style:var(--tw-outline-style);outline-width:0}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.prose-gray{--tw-prose-body:oklch(37.3% .034 259.733);--tw-prose-headings:oklch(21% .034 264.665);--tw-prose-lead:oklch(44.6% .03 256.802);--tw-prose-links:oklch(21% .034 264.665);--tw-prose-bold:oklch(21% .034 264.665);--tw-prose-counters:oklch(55.1% .027 264.364);--tw-prose-bullets:oklch(87.2% .01 258.338);--tw-prose-hr:oklch(92.8% .006 264.531);--tw-prose-quotes:oklch(21% .034 264.665);--tw-prose-quote-borders:oklch(92.8% .006 264.531);--tw-prose-captions:oklch(55.1% .027 264.364);--tw-prose-kbd:oklch(21% .034 264.665);--tw-prose-kbd-shadows:oklab(21% -.00316127 -.0338527/.1);--tw-prose-code:oklch(21% .034 264.665);--tw-prose-pre-code:oklch(92.8% .006 264.531);--tw-prose-pre-bg:oklch(27.8% .033 256.848);--tw-prose-th-borders:oklch(87.2% .01 258.338);--tw-prose-td-borders:oklch(92.8% .006 264.531);--tw-prose-invert-body:oklch(87.2% .01 258.338);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.7% .022 261.325);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.7% .022 261.325);--tw-prose-invert-bullets:oklch(44.6% .03 256.802);--tw-prose-invert-hr:oklch(37.3% .034 259.733);--tw-prose-invert-quotes:oklch(96.7% .003 264.542);--tw-prose-invert-quote-borders:oklch(37.3% .034 259.733);--tw-prose-invert-captions:oklch(70.7% .022 261.325);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(87.2% .01 258.338);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .03 256.802);--tw-prose-invert-td-borders:oklch(37.3% .034 259.733)}.select-none{-webkit-user-select:none;user-select:none}.\[text-shadow\:-0\.2px_0_0_currentColor\,0\.2px_0_0_currentColor\]{text-shadow:-.2px 0,.2px 0}@media (hover:hover){.group-hover\:bg-\[rgb\(var\(--color-gray-200\)\)\]:is(:where(.group):hover *){background-color:rgb(var(--color-gray-200))}.group-hover\:bg-\[rgb\(var\(--color-stone-200\)\/0\.5\)\]:is(:where(.group):hover *){background-color:rgb(var(--color-stone-200)/.5)}.group-hover\:text-\[rgb\(var\(--color-primary\)\)\]:is(:where(.group):hover *){color:rgb(var(--color-primary))}.group-hover\:text-\[rgb\(var\(--color-primary-ink\)\)\]:is(:where(.group):hover *){color:rgb(var(--color-primary-ink))}.group-hover\:opacity-90:is(:where(.group):hover *){opacity:.9}.group-hover\/copy\:text-\[rgb\(var\(--color-stone-500\)\)\]:is(:where(.group\/copy):hover *){color:rgb(var(--color-stone-500))}.peer-hover\:opacity-100:is(:where(.peer):hover~*){opacity:1}}.first\:mt-0:first-child{margin-top:0}@media (hover:hover){.hover\:border-\[rgb\(var\(--color-primary\)\/0\.4\)\]:hover{border-color:rgb(var(--color-primary)/.4)}.hover\:bg-\[rgb\(var\(--color-stone-100\)\)\]:hover{background-color:rgb(var(--color-stone-100))}.hover\:text-\[rgb\(var\(--color-gray-600\)\)\]:hover{color:rgb(var(--color-gray-600))}.hover\:text-\[rgb\(var\(--color-gray-700\)\)\]:hover{color:rgb(var(--color-gray-700))}.hover\:text-\[rgb\(var\(--color-gray-800\)\)\]:hover{color:rgb(var(--color-gray-800))}.hover\:text-\[rgb\(var\(--color-gray-900\)\)\]:hover{color:rgb(var(--color-gray-900))}.hover\:underline:hover{text-decoration-line:underline}.hover\:ring-\[rgb\(var\(--color-gray-600\)\/0\.3\)\]:hover{--tw-ring-color:rgb(var(--color-gray-600)/.3)}}@media (width>=40rem){.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}}@media (width>=64rem){.lg\:sticky{position:sticky}.lg\:mx-0{margin-inline:0}.lg\:mt-8{margin-top:calc(var(--spacing) * 8)}.lg\:-ml-12{margin-left:calc(var(--spacing) * -12)}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:flex-row{flex-direction:row}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}.lg\:px-12{padding-inline:calc(var(--spacing) * 12)}.lg\:pt-10{padding-top:calc(var(--spacing) * 10)}.lg\:pl-\[23\.7rem\]{padding-left:23.7rem}}@media (width>=80rem){.xl\:block{display:block}.xl\:flex{display:flex}.xl\:hidden{display:none}.xl\:w-\[calc\(100\%-28rem\)\]{width:calc(100% - 28rem)}.xl\:flex-col{flex-direction:column}.xl\:flex-row{flex-direction:row}}.dark\:block:where(.dark,.dark *){display:block}.dark\:hidden:where(.dark,.dark *){display:none}.dark\:border-\[rgb\(255_255_255\/0\.1\)\]:where(.dark,.dark *){border-color:#ffffff1a}.dark\:border-\[rgb\(var\(--color-border-dark-subtle\)\/0\.1\)\]:where(.dark,.dark *){border-color:rgb(var(--color-border-dark-subtle)/.1)}.dark\:border-\[rgb\(var\(--color-gray-300\)\/0\.06\)\]:where(.dark,.dark *){border-color:rgb(var(--color-gray-300)/.06)}.dark\:border-\[rgb\(var\(--color-gray-300\)\/0\.08\)\]:where(.dark,.dark *){border-color:rgb(var(--color-gray-300)/.08)}.dark\:border-\[rgb\(var\(--color-gray-800\)\)\]:where(.dark,.dark *){border-color:rgb(var(--color-gray-800))}.dark\:border-\[rgb\(var\(--color-gray-800\)\/0\.5\)\]:where(.dark,.dark *){border-color:rgb(var(--color-gray-800)/.5)}.dark\:bg-\[rgb\(var\(--color-background-dark\)\)\]:where(.dark,.dark *){background-color:rgb(var(--color-background-dark))}.dark\:bg-\[rgb\(var\(--color-code-block-dark\)\)\]:where(.dark,.dark *){background-color:rgb(var(--color-code-block-dark))}.dark\:bg-\[rgb\(var\(--color-primary-light\)\)\]:where(.dark,.dark *){background-color:rgb(var(--color-primary-light))}.dark\:bg-\[rgb\(var\(--color-stone-900\)\)\]:where(.dark,.dark *){background-color:rgb(var(--color-stone-900))}.dark\:bg-\[rgb\(var\(--color-surface-dark-tint\)\/0\.05\)\]:where(.dark,.dark *){background-color:rgb(var(--color-surface-dark-tint)/.05)}.dark\:bg-amber-400\/10:where(.dark,.dark *){background-color:#fcbb001a}@supports (color:color-mix(in lab, red, red)){.dark\:bg-amber-400\/10:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-amber-400) 10%, transparent)}}.dark\:bg-blue-400:where(.dark,.dark *){background-color:var(--color-blue-400)}.dark\:bg-blue-400\/10:where(.dark,.dark *){background-color:#54a2ff1a}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-400\/10:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-blue-400) 10%, transparent)}}.dark\:bg-blue-400\/20:where(.dark,.dark *){background-color:#54a2ff33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-400\/20:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-blue-400) 20%, transparent)}}.dark\:bg-green-400:where(.dark,.dark *){background-color:var(--color-green-400)}.dark\:bg-green-400\/10:where(.dark,.dark *){background-color:#05df721a}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-400\/10:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-green-400) 10%, transparent)}}.dark\:bg-green-400\/20:where(.dark,.dark *){background-color:#05df7233}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-400\/20:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-green-400) 20%, transparent)}}.dark\:bg-orange-400\/20:where(.dark,.dark *){background-color:#ff8b1a33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-orange-400\/20:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-orange-400) 20%, transparent)}}.dark\:bg-purple-400:where(.dark,.dark *){background-color:var(--color-purple-400)}.dark\:bg-purple-400\/20:where(.dark,.dark *){background-color:#c07eff33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-purple-400\/20:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-purple-400) 20%, transparent)}}.dark\:bg-red-400\/10:where(.dark,.dark *){background-color:#ff65681a}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-400\/10:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-red-400) 10%, transparent)}}.dark\:bg-red-400\/20:where(.dark,.dark *){background-color:#ff656833}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-400\/20:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-red-400) 20%, transparent)}}.dark\:bg-yellow-400\/20:where(.dark,.dark *){background-color:#fac80033}@supports (color:color-mix(in lab, red, red)){.dark\:bg-yellow-400\/20:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-yellow-400) 20%, transparent)}}.dark\:from-\[rgb\(var\(--color-background-dark\)\)\]:where(.dark,.dark *){--tw-gradient-from:rgb(var(--color-background-dark));--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark\:text-\[rgb\(255_255_255\/0\.4\)\]:where(.dark,.dark *){color:#fff6}.dark\:text-\[rgb\(var\(--color-gray-50\)\)\]:where(.dark,.dark *){color:rgb(var(--color-gray-50))}.dark\:text-\[rgb\(var\(--color-gray-200\)\)\]:where(.dark,.dark *){color:rgb(var(--color-gray-200))}.dark\:text-\[rgb\(var\(--color-gray-300\)\)\]:where(.dark,.dark *){color:rgb(var(--color-gray-300))}.dark\:text-\[rgb\(var\(--color-gray-400\)\)\]:where(.dark,.dark *){color:rgb(var(--color-gray-400))}.dark\:text-\[rgb\(var\(--color-gray-500\)\)\]:where(.dark,.dark *){color:rgb(var(--color-gray-500))}.dark\:text-\[rgb\(var\(--color-primary-light\)\)\]:where(.dark,.dark *){color:rgb(var(--color-primary-light))}.dark\:text-\[rgb\(var\(--color-stone-50\)\)\]:where(.dark,.dark *){color:rgb(var(--color-stone-50))}.dark\:text-\[rgb\(var\(--color-stone-400\)\)\]:where(.dark,.dark *){color:rgb(var(--color-stone-400))}.dark\:text-amber-300:where(.dark,.dark *){color:var(--color-amber-300)}.dark\:text-blue-300:where(.dark,.dark *){color:var(--color-blue-300)}.dark\:text-blue-400:where(.dark,.dark *){color:var(--color-blue-400)}.dark\:text-gray-400:where(.dark,.dark *){color:var(--color-gray-400)}.dark\:text-green-300:where(.dark,.dark *){color:var(--color-green-300)}.dark\:text-green-400:where(.dark,.dark *){color:var(--color-green-400)}.dark\:text-orange-300:where(.dark,.dark *){color:var(--color-orange-300)}.dark\:text-purple-400:where(.dark,.dark *){color:var(--color-purple-400)}.dark\:text-red-300:where(.dark,.dark *){color:var(--color-red-300)}.dark\:text-yellow-300:where(.dark,.dark *){color:var(--color-yellow-300)}.dark\:ring-\[rgb\(var\(--color-gray-600\)\/0\.3\)\]:where(.dark,.dark *){--tw-ring-color:rgb(var(--color-gray-600)/.3)}.dark\:brightness-110:where(.dark,.dark *){--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.dark\:prose-invert:where(.dark,.dark *){--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-kbd:var(--tw-prose-invert-kbd);--tw-prose-kbd-shadows:var(--tw-prose-invert-kbd-shadows);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}@media (hover:hover){.dark\:group-hover\:bg-\[rgb\(var\(--color-gray-700\)\)\]:where(.dark,.dark *):is(:where(.group):hover *){background-color:rgb(var(--color-gray-700))}.dark\:group-hover\:bg-\[rgb\(var\(--color-stone-700\)\/0\.7\)\]:where(.dark,.dark *):is(:where(.group):hover *){background-color:rgb(var(--color-stone-700)/.7)}.dark\:group-hover\:text-\[rgb\(var\(--color-primary-light\)\)\]:where(.dark,.dark *):is(:where(.group):hover *){color:rgb(var(--color-primary-light))}.dark\:group-hover\/copy\:text-\[rgb\(255_255_255\/0\.6\)\]:where(.dark,.dark *):is(:where(.group\/copy):hover *){color:#fff9}.dark\:hover\:border-\[rgb\(var\(--color-primary-light\)\/0\.3\)\]:where(.dark,.dark *):hover{border-color:rgb(var(--color-primary-light)/.3)}.dark\:hover\:bg-\[rgb\(255_255_255\/0\.05\)\]:where(.dark,.dark *):hover{background-color:#ffffff0d}.dark\:hover\:text-\[rgb\(var\(--color-gray-200\)\)\]:where(.dark,.dark *):hover{color:rgb(var(--color-gray-200))}.dark\:hover\:text-\[rgb\(var\(--color-gray-300\)\)\]:where(.dark,.dark *):hover{color:rgb(var(--color-gray-300))}.dark\:hover\:ring-\[rgb\(var\(--color-gray-500\)\/0\.3\)\]:where(.dark,.dark *):hover{--tw-ring-color:rgb(var(--color-gray-500)/.3)}.dark\:hover\:brightness-125:where(.dark,.dark *):hover{--tw-brightness:brightness(125%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false} +/*$vite$:1*/ +/** + * Sourcey — Component styles. + * + * Layout and OpenAPI components use Tailwind classes in .tsx files. + * Markdown-generated components (code blocks, cards, steps, accordions, + * callouts, tabs) use semantic class names styled here — no Tailwind + * utilities in generated HTML. + */ + +/* ── Page Description (display font) ─────────────────────────────── */ + +#sourcey .page-description { + font-family: ui-serif, Georgia, Cambria, "Times New Roman", serif; + font-size: 1.125rem; + line-height: 1.35; + font-style: italic; + color: rgb(var(--color-gray-600)); +} +@media (min-width: 1024px) { + #sourcey .page-description { + font-size: 1.375rem; + line-height: 1.4; + } +} +.dark #sourcey .page-description { + color: rgb(var(--color-gray-400)); +} + +/* ── Page Top Gradient ────────────────────────────────────────────── */ + +#sourcey #page::before { + content: ""; + position: fixed; + top: 0; + left: 0; + right: 0; + height: 400px; + background: linear-gradient(to bottom, rgb(var(--color-primary) / 0.03), transparent); + pointer-events: none; + z-index: -1; +} +.dark #sourcey #page::before { + background: none; +} + +/* ── Scroll Offset (clears fixed navbar on anchor clicks) ──────────── */ + +[data-traverse-target], +h1[id], +h2[id], +h3[id], +h4[id], +h5[id], +h6[id] { + scroll-margin-top: var(--header-height, 7rem); +} + +/* ── Focus ─────────────────────────────────────────────────────────── */ + +#sourcey button:focus-visible { + outline: 2px solid rgb(var(--color-primary)); + outline-offset: 2px; +} + +/* ── Shiki Dual-Theme (defaultColor: false) ───────────────────────── */ +/* Shiki outputs --shiki-light and --shiki-dark CSS vars on each span. */ +/* Light mode: use --shiki-light. Dark mode: use --shiki-dark. */ + +.shiki, +.shiki span { + color: var(--shiki-light); + background-color: transparent !important; +} + +.dark .shiki, +.dark .shiki span { + color: var(--shiki-dark); +} + +.shiki pre { + background-color: transparent !important; + margin: 0; +} + +.shiki code { + font-family: inherit; + background: transparent; + border: none; + padding: 0; +} + +/* ── Responsive Tables ────────────────────────────────────────────── */ + +#sourcey .table-wrap { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} +#sourcey .table-wrap table { + margin: 0; +} +#sourcey .prose :where(thead th):not(:where([class~="not-prose"], [class~="not-prose"] *)) { + padding-top: 0.571429em; +} + +/* ── Prose Video (::video directive) ──────────────────────────────── */ + +#sourcey .prose-video { + position: relative; + width: 100%; + padding-bottom: 56.25%; /* 16:9 */ + margin: 1.5rem 0; + border-radius: var(--radius); + overflow: hidden; +} +#sourcey .prose-video iframe, +#sourcey .prose-video video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +/* ── Prose Iframe (::iframe directive) ────────────────────────────── */ + +#sourcey .prose-iframe { + width: 100%; + margin: 1.5rem 0; + border-radius: var(--radius); + overflow: hidden; + border: 1px solid rgb(var(--color-stone-950) / 0.1); +} +.dark #sourcey .prose-iframe { + border-color: rgb(255 255 255 / 0.1); +} +#sourcey .prose-iframe iframe { + width: 100%; + height: 100%; + display: block; +} + +/* ── Inline Code (not-prose containers lose Tailwind prose styling) ── */ + +#sourcey .not-prose :not(pre) > code { + font-size: 0.875em; + font-weight: 600; + color: var(--tw-prose-code); +} +#sourcey .not-prose :not(pre) > code::before, +#sourcey .not-prose :not(pre) > code::after { + content: "`"; +} + +/* ── Prose Code Block (fenced code in markdown pages) ─────────────── */ + +#sourcey .prose-code-block { + position: relative; + border-radius: var(--radius); + border: 1px solid rgb(var(--color-stone-950) / 0.1); + margin: 1.25rem 0 2rem; + overflow: hidden; + color: rgb(var(--color-stone-950)); +} +.dark #sourcey .prose-code-block { + border-color: rgb(255 255 255 / 0.1); + color: rgb(var(--color-stone-50)); +} + +#sourcey .prose-code-copy { + position: absolute; + top: 0.75rem; + right: 1rem; + z-index: 10; +} + +#sourcey .prose-code-copy .copy-btn { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border-radius: 0.375rem; + border: none; + background: transparent; + cursor: pointer; + color: rgb(var(--color-stone-400)); + transition: color 0.15s; +} +#sourcey .prose-code-copy .copy-btn:hover { + color: rgb(var(--color-stone-500)); +} +.dark #sourcey .prose-code-copy .copy-btn { + color: rgb(255 255 255 / 0.4); +} +.dark #sourcey .prose-code-copy .copy-btn:hover { + color: rgb(255 255 255 / 0.6); +} + +#sourcey .prose-code-copy .copy-btn svg { + width: 1rem; + height: 1rem; +} + +#sourcey .prose-code-content { + padding: 0.875rem 1rem; + border-radius: var(--radius); + background: rgb(var(--color-code-block-light)); + overflow-x: auto; + font-variant-ligatures: none; +} +.dark #sourcey .prose-code-content { + background: rgb(var(--color-code-block-dark)); +} + +#sourcey .prose-code-content pre { + margin: 0; + font-family: var(--font-mono); + font-size: 0.875rem; + line-height: 1.5rem; + white-space: pre; +} + +/* ── Nav Links ────────────────────────────────────────────────────── */ + +#sourcey .nav-group-label { + display: block; + padding: 0 0.75rem 0.25rem 1rem; + margin-bottom: 0.625rem; + font-size: inherit; + font-weight: 600; + color: rgb(var(--color-gray-900)); +} +.dark #sourcey .nav-group-label { + color: rgb(var(--color-gray-200)); +} +#sourcey .nav-group-link { + cursor: pointer; + border-radius: 0.75rem; + text-decoration: none; + transition: + color 0.15s, + background-color 0.15s; +} +#sourcey .nav-group-link:hover { + color: rgb(var(--color-primary-ink)); +} +.dark #sourcey .nav-group-link:hover { + color: rgb(var(--color-primary-light)); +} +#sourcey .nav-group-link.active { + color: rgb(var(--color-primary-ink)); +} +.dark #sourcey .nav-group-link.active { + color: rgb(var(--color-primary-light)); +} + +#sourcey .nav-tab-label { + display: block; + padding: 0.5rem 0.75rem 0.25rem 1rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: rgb(var(--color-gray-400)); +} +.dark #sourcey .nav-tab-label { + color: rgb(var(--color-gray-500)); +} + +#sourcey .nav-link { + display: flex; + align-items: flex-start; + padding: 0.375rem 0.75rem 0.375rem 1rem; + gap: 0.75rem; + cursor: pointer; + text-align: left; + overflow-wrap: break-word; + hyphens: auto; + border-radius: 0.75rem; + width: 100%; + color: rgb(var(--color-gray-700)); + transition: + color 0.15s, + background-color 0.15s; +} +.dark #sourcey .nav-link { + color: rgb(var(--color-gray-400)); +} +#sourcey .nav-link:hover { + color: rgb(var(--color-gray-900)); + background: rgb(var(--color-gray-100) / 0.6); +} +.dark #sourcey .nav-link:hover { + color: rgb(var(--color-gray-300)); + background: rgb(var(--color-gray-800) / 0.4); +} +#sourcey .nav-link.active { + color: rgb(var(--color-primary-ink)); + background: rgb(var(--color-primary) / 0.08); + text-shadow: + -0.2px 0 0 currentColor, + 0.2px 0 0 currentColor; +} +.dark #sourcey .nav-link.active { + color: rgb(var(--color-primary-light)); + background: rgb(var(--color-primary-light) / 0.08); +} + +/* ── TOC Active State ─────────────────────────────────────────────── */ + +#sourcey #toc .toc-item { + border-left: 2px solid rgb(var(--color-gray-200)); + padding-left: 0.75rem; + transition: + color 0.15s, + border-color 0.15s; +} +#sourcey #toc ul ul .toc-item { + margin-left: 0.75rem; + border-left: none; +} +.dark #sourcey #toc .toc-item { + border-left-color: rgb(var(--color-gray-700)); +} +#sourcey #toc .toc-item.active { + color: rgb(var(--color-primary-ink)); + border-left-color: rgb(var(--color-primary-ink)); + font-weight: 500; +} +.dark #sourcey #toc .toc-item.active { + color: rgb(var(--color-primary-light)); + border-left-color: rgb(var(--color-primary-light)); +} + +/* ── Code Block Panels (language dropdown + response tabs) ─────────── */ + +#sourcey .code-lang-panel { + display: none; +} +#sourcey .code-lang-panel.active { + display: block; +} + +#sourcey .lang-icon { + width: 0.875rem; + height: 0.875rem; + flex-shrink: 0; + vertical-align: -0.125rem; +} + +#sourcey .lang-icon[data-lang] { + display: inline-block; + background: currentColor; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; +} + +#sourcey .response-panel { + display: none; +} +#sourcey .response-panel.active { + display: block; +} + +/* ── Response Tabs (status code tabs on code block) ───────────────── */ + +#sourcey .response-tab { + color: rgb(var(--color-gray-500)); + transition: color 0.15s; +} +.dark #sourcey .response-tab { + color: rgb(var(--color-gray-400)); +} + +#sourcey .response-tab.active { + color: rgb(var(--color-primary-ink)); +} +.dark #sourcey .response-tab.active { + color: rgb(var(--color-primary-light)); +} + +/* Active response tab underline */ +#sourcey .response-tab.active::after { + content: ""; + position: absolute; + right: 0; + bottom: -0.375rem; + left: 0; + height: 2px; + border-radius: 9999px; + background: rgb(var(--color-primary)); +} +.dark #sourcey .response-tab.active::after { + background: rgb(var(--color-primary-light)); +} + +/* ── Copy Button Feedback ─────────────────────────────────────────── */ + +#sourcey .copy-btn.copied { + color: rgb(var(--color-success)); +} + +/* ── Schema Utilities (used by SchemaView.tsx) ─────────────────────── */ + +/* ── Collapsible child attributes ── */ + +#sourcey .schema-expandable { + margin-top: 1rem; + border: 1px solid rgb(var(--color-gray-200) / 0.7); + border-radius: 0.75rem; +} +.dark #sourcey .schema-expandable { + border-color: rgb(255 255 255 / 0.1); +} + +#sourcey .schema-expandable-toggle { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + font-size: 0.875rem; + font-weight: 400; + color: rgb(var(--color-gray-600)); + cursor: pointer; + list-style: none; + user-select: none; +} +#sourcey .schema-expandable-toggle::-webkit-details-marker { + display: none; +} +.dark #sourcey .schema-expandable-toggle { + color: rgb(var(--color-gray-300)); +} +#sourcey .schema-expandable-toggle:hover { + background: rgb(var(--color-gray-100) / 0.5); + color: rgb(var(--color-gray-900)); + border-radius: 0.75rem; +} +.dark #sourcey .schema-expandable-toggle:hover { + background: rgb(255 255 255 / 0.05); + color: rgb(var(--color-gray-200)); +} + +#sourcey .schema-expandable-icon { + width: 0.625rem; + height: 0.625rem; + flex-shrink: 0; + transition: transform 75ms; + color: rgb(var(--color-gray-400)); +} +#sourcey details[open] > .schema-expandable-toggle > .schema-expandable-icon { + transform: rotate(90deg); +} + +#sourcey .schema-expandable-content { + padding: 0 1rem 0.5rem; + border-top: 1px solid rgb(var(--color-gray-100)); +} +.dark #sourcey .schema-expandable-content { + border-top-color: rgb(255 255 255 / 0.1); +} + +/* Legacy schema-nested — kept for variant nesting */ +#sourcey .schema-nested { + padding-left: 1rem; + border-left: 2px solid rgb(var(--color-gray-200)); + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} +.dark #sourcey .schema-nested { + border-left-color: rgb(var(--color-gray-800)); +} + +#sourcey .schema-variant-option { + padding-left: 1rem; + border-left: 2px solid rgb(var(--color-gray-200)); + margin-bottom: 0.5rem; +} +.dark #sourcey .schema-variant-option { + border-left-color: rgb(var(--color-gray-800)); +} + +#sourcey .schema-variant-label { + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.05em; + color: rgb(var(--color-gray-400)); + margin-bottom: 0.25rem; +} + +/* ── Type Display (used by SchemaDatatype.tsx) ─────────────────────── */ + +#sourcey .json-property-type { + display: inline-flex; + align-items: center; + font-style: normal; + font-weight: 500; + font-size: 0.75rem; + line-height: 1; + white-space: nowrap; + color: rgb(var(--color-gray-600)); + background: rgb(var(--color-gray-100) / 0.5); + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; +} +.dark #sourcey .json-property-type { + background: rgb(var(--color-surface-dark-tint) / 0.05); + color: rgb(var(--color-gray-200)); +} + +#sourcey .json-property-format { + font-size: 0.75rem; + color: rgb(var(--color-gray-400)); +} + +#sourcey .json-property-enum { + display: inline-flex; + align-items: baseline; + gap: 0.25rem; + flex-wrap: wrap; +} + +#sourcey .json-property-enum-item, +#sourcey .json-property-default-value, +#sourcey .json-property-range { + display: inline-flex; + align-items: center; + font-size: 0.6875rem; + font-family: var(--font-mono); + line-height: 1; + white-space: nowrap; + color: rgb(var(--color-gray-500)); + background: rgb(var(--color-gray-100) / 0.5); + padding: 0.125rem 0.375rem; + border-radius: 9999px; +} +.dark #sourcey .json-property-enum-item, +.dark #sourcey .json-property-default-value, +.dark #sourcey .json-property-range { + background: rgb(var(--color-surface-dark-tint) / 0.05); + color: rgb(var(--color-gray-400)); +} + +#sourcey .json-property-enum-item { + color: rgb(var(--color-gray-700)); +} +.dark #sourcey .json-property-enum-item { + color: rgb(var(--color-gray-300)); +} + +#sourcey .json-property-default-value::before { + content: "= "; + color: rgb(var(--color-gray-400)); +} + +/* ── Parameter/Schema List (used by SchemaView.tsx, Parameters.tsx) ── */ + +#sourcey .param-item { + padding: 1rem 0; + border-bottom: 1px solid rgb(var(--color-gray-100)); +} +.dark #sourcey .param-item { + border-bottom-color: rgb(var(--color-gray-800)); +} + +#sourcey .param-item:last-child { + border-bottom: none; +} + +#sourcey .param-header { + display: flex; + align-items: baseline; + gap: 0.5rem; + flex-wrap: wrap; +} + +#sourcey .param-name { + font-family: var(--font-mono); + font-size: 0.875rem; + font-weight: 600; + color: rgb(var(--color-primary-ink)); + background: transparent; + border: none; + padding: 0; +} +.dark #sourcey .param-name { + color: rgb(var(--color-primary-light)); +} + +#sourcey .param-type { + display: inline-flex; + align-items: baseline; + gap: 0.375rem; +} + +#sourcey .param-in { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.5rem; + font-size: 0.75rem; + font-weight: 500; + border-radius: 0.375rem; + background: rgb(var(--color-gray-100) / 0.5); + color: rgb(var(--color-gray-600)); + margin-left: auto; +} +.dark #sourcey .param-in { + background: rgb(var(--color-surface-dark-tint) / 0.05); + color: rgb(var(--color-gray-200)); +} + +#sourcey .param-description { + padding-top: 0.5rem; + font-size: 0.875rem; + color: rgb(var(--color-gray-700)); +} +.dark #sourcey .param-description { + color: rgb(var(--color-gray-400)); +} + +/* ── Steps (numbered step list with vertical connector) ────────────── */ + +#sourcey .steps { + margin-left: 0.875rem; + margin-top: 1rem; + margin-bottom: 1.5rem; +} + +#sourcey .steps .step-item { + position: relative; + display: flex; + align-items: flex-start; + padding-bottom: 1.25rem; +} + +#sourcey .steps .step-item::before { + content: ""; + position: absolute; + left: 13px; + top: 2.75rem; + bottom: 0; + width: 1px; + background: rgb(var(--color-gray-200) / 0.7); +} +.dark #sourcey .steps .step-item::before { + background: rgb(255 255 255 / 0.1); +} + +#sourcey .steps .step-item:last-child::before { + background: linear-gradient(to bottom, rgb(var(--color-gray-200) / 0.7), transparent); +} +.dark #sourcey .steps .step-item:last-child::before { + background: linear-gradient(to bottom, rgb(255 255 255 / 0.1), transparent); +} + +#sourcey .steps .step-number { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + flex-shrink: 0; + border-radius: 9999px; + background: rgb(var(--color-gray-50)); + font-size: 0.75rem; + font-weight: 600; + color: rgb(var(--color-gray-900)); + margin-top: 0.5rem; +} +.dark #sourcey .steps .step-number { + background: rgb(255 255 255 / 0.1); + color: rgb(var(--color-gray-50)); +} + +#sourcey .steps .step-body { + padding-left: 1rem; + flex: 1; + overflow: hidden; +} + +#sourcey .steps .step-title { + margin-top: 0.5rem; + font-weight: 600; + color: rgb(var(--color-gray-900)); +} +.dark #sourcey .steps .step-title { + color: rgb(var(--color-gray-200)); +} + +#sourcey .steps .step-content { + margin-top: 0.25rem; +} + +#sourcey .steps .step-content p { + margin: 0.25rem 0; +} + +/* ── Card Group (icon cards in a grid) ─────────────────────────────── */ + +#sourcey .card-group { + display: grid; + gap: 1rem; + margin-top: 1.25rem; + margin-bottom: 2rem; +} +@media (min-width: 640px) { + #sourcey .card-group[data-cols="2"] { + grid-template-columns: repeat(2, 1fr); + } + #sourcey .card-group[data-cols="3"] { + grid-template-columns: repeat(3, 1fr); + } + #sourcey .card-group[data-cols="4"] { + grid-template-columns: repeat(4, 1fr); + } +} + +#sourcey .card-item { + display: block; + position: relative; + border: 1px solid rgb(var(--color-gray-950) / 0.1); + border-radius: var(--radius); + padding: 1.25rem 1.5rem; + background: rgb(var(--color-background-light)); + text-decoration: none; + color: inherit; + cursor: pointer; + overflow: hidden; + transition: border-color 0.15s; +} +.dark #sourcey .card-item { + border-color: rgb(255 255 255 / 0.1); + background: rgb(var(--color-background-dark)); +} + +#sourcey .card-item:hover { + border-color: rgb(var(--color-primary)); +} +.dark #sourcey .card-item:hover { + border-color: rgb(var(--color-primary-light)); +} + +#sourcey .card-icon { + width: 1.5rem; + height: 1.5rem; + flex-shrink: 0; + color: rgb(var(--color-primary)); + margin-bottom: 0.75rem; +} +.dark #sourcey .card-icon { + color: rgb(var(--color-primary-light)); +} + +#sourcey .card-item-title { + font-size: 1rem; + font-weight: 600; + color: rgb(var(--color-gray-800)); + margin: 0 0 0.25rem; +} +.dark #sourcey .card-item-title { + color: #fff; +} + +#sourcey .card-item-content { + margin-top: 0.25rem; + font-size: 1rem; + line-height: 1.5; + color: rgb(var(--color-gray-600)); +} +.dark #sourcey .card-item-content { + color: rgb(var(--color-gray-400)); +} + +#sourcey .card-item-content p { + margin: 0; +} + +/* ── Accordion (expandable details) ────────────────────────────────── */ + +#sourcey .accordion-group { + margin-top: 0; + margin-bottom: 0.75rem; + border-radius: var(--radius); + border: 1px solid rgb(var(--color-gray-200) / 0.7); + overflow: hidden; +} +.dark #sourcey .accordion-group { + border-color: rgb(var(--color-gray-800) / 0.5); +} + +#sourcey .accordion-item { + border-bottom: 1px solid rgb(var(--color-gray-200) / 0.7); + background: rgb(var(--color-background-light)); +} +.dark #sourcey .accordion-item { + border-bottom-color: rgb(var(--color-gray-800) / 0.5); + background: rgb(var(--color-code-block-dark)); +} + +#sourcey .accordion-item:last-child { + border-bottom: none; +} + +#sourcey .accordion-trigger { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 1rem 1.25rem; + cursor: pointer; + font-weight: 500; + color: rgb(var(--color-gray-900)); + list-style: none; +} +#sourcey .accordion-trigger::-webkit-details-marker { + display: none; +} +.dark #sourcey .accordion-trigger { + color: rgb(var(--color-gray-200)); +} + +#sourcey .accordion-trigger:hover { + background: rgb(var(--color-gray-100)); +} +.dark #sourcey .accordion-trigger:hover { + background: rgb(var(--color-gray-800)); +} + +#sourcey .accordion-trigger::before { + content: ""; + display: inline-block; + width: 0.75rem; + height: 0.75rem; + flex-shrink: 0; + background: rgb(var(--color-gray-700)); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 512'%3E%3Cpath d='M246.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-9.2-9.2-22.9-11.9-34.9-6.9s-19.8 16.6-19.8 29.6l0 256c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l128-128z'/%3E%3C/svg%3E"); + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + transition: transform 0.15s; +} +.dark #sourcey .accordion-trigger::before { + background: rgb(var(--color-gray-400)); +} + +#sourcey .accordion-item[open] > .accordion-trigger::before { + transform: rotate(90deg); +} + +#sourcey .accordion-content { + padding: 0 1.25rem 1rem 2.5rem; + color: rgb(var(--color-gray-700)); +} +.dark #sourcey .accordion-content { + color: rgb(var(--color-gray-400)); +} + +#sourcey .accordion-content p { + margin: 0.25rem 0; +} + +/* ── Callouts (:::note, :::warning, :::tip, :::info) ───────────────── */ + +#sourcey .callout { + margin: 1.25rem 0; + border: none; + border-left: 2px solid; + border-radius: 0; + padding: 0.875rem 1rem; + font-size: 0.875rem; + line-height: 1.6; + color: rgb(var(--color-gray-700)); + border-left-color: rgb(var(--color-gray-300)); +} +.dark #sourcey .callout { + border-left-color: rgb(var(--color-gray-700)); + color: rgb(var(--color-gray-400)); +} + +#sourcey .callout-title { + font-weight: 600; + font-size: 0.8125rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +/* Remove bottom margin when no content follows */ +#sourcey .callout-title:last-child { + margin-bottom: 0; +} + +#sourcey .callout-content { + margin-top: 0.375rem; +} + +#sourcey .callout-content p { + margin: 0.25rem 0; +} + +#sourcey .callout-content p:last-child { + margin-bottom: 0; +} + +/* Note — blue */ +#sourcey .callout-note { + border-left-color: #3b82f6; +} +.dark #sourcey .callout-note { + border-left-color: #60a5fa; +} +#sourcey .callout-note .callout-title { + color: #3b82f6; +} +.dark #sourcey .callout-note .callout-title { + color: #60a5fa; +} + +/* Warning — amber */ +#sourcey .callout-warning { + border-left-color: #f59e0b; +} +.dark #sourcey .callout-warning { + border-left-color: #fbbf24; +} +#sourcey .callout-warning .callout-title { + color: #f59e0b; +} +.dark #sourcey .callout-warning .callout-title { + color: #fbbf24; +} + +/* ── Changelog ───────────────────────────────────────────────────── */ + +#sourcey .sourcey-changelog-page { + width: 100%; +} + +#sourcey .sourcey-changelog-header { + margin-bottom: 2rem; +} + +#sourcey .sourcey-changelog-description { + margin-top: 0.75rem; + font-size: 1.05rem; + line-height: 1.7; + color: rgb(var(--color-gray-600)); + max-width: 65ch; + text-wrap: pretty; +} + +.dark #sourcey .sourcey-changelog-description { + color: rgb(var(--color-gray-400)); +} + +#sourcey .sourcey-changelog-description a { + color: rgb(var(--color-primary-ink)); + text-decoration: none; + border-bottom: 1px solid rgb(var(--color-primary-ink) / 0.3); +} + +#sourcey .sourcey-changelog-description a:hover { + border-bottom-color: rgb(var(--color-primary-ink)); +} + +.dark #sourcey .sourcey-changelog-description a { + color: rgb(var(--color-primary-light)); + border-bottom-color: rgb(var(--color-primary-light) / 0.3); +} + +.dark #sourcey .sourcey-changelog-description a:hover { + border-bottom-color: rgb(var(--color-primary-light)); +} + +#sourcey .sourcey-changelog-description code { + font-size: 0.92em; + padding: 0.05rem 0.3rem; + border-radius: 0.25rem; + background: rgb(var(--color-gray-100)); + color: rgb(var(--color-gray-800)); +} + +.dark #sourcey .sourcey-changelog-description code { + background: rgb(255 255 255 / 0.08); + color: rgb(var(--color-gray-200)); +} + +#sourcey .sourcey-changelog-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +#sourcey .sourcey-changelog-version { + border: 1px solid rgb(var(--color-gray-200) / 0.8); + border-radius: 1rem; + padding: 1.25rem 1.25rem 1rem; + background: linear-gradient(180deg, rgb(var(--color-gray-50) / 0.75), rgb(255 255 255)); +} + +.dark #sourcey .sourcey-changelog-version { + border-color: rgb(var(--color-gray-800) / 0.8); + background: linear-gradient(180deg, rgb(255 255 255 / 0.03), rgb(255 255 255 / 0.015)); +} + +#sourcey .sourcey-changelog-version-unreleased { + border-color: rgb(var(--color-primary) / 0.18); + border-left: 3px solid rgb(var(--color-primary)); + padding-left: calc(1.25rem - 2px); + background: linear-gradient( + 180deg, + rgb(var(--color-primary) / 0.04), + rgb(var(--color-primary) / 0.015) + ); +} + +.dark #sourcey .sourcey-changelog-version-unreleased { + border-color: rgb(var(--color-primary-light) / 0.22); + border-left-color: rgb(var(--color-primary-light)); + background: linear-gradient( + 180deg, + rgb(var(--color-primary-light) / 0.06), + rgb(var(--color-primary-light) / 0.02) + ); +} + +#sourcey .sourcey-changelog-version-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} + +@media (max-width: 768px) { + #sourcey .sourcey-changelog-version-header { + flex-direction: column; + } +} + +#sourcey .sourcey-changelog-version-heading { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +#sourcey .sourcey-changelog-version-link { + font-size: 1.25rem; + font-weight: 700; + color: rgb(var(--color-gray-900)); + text-decoration: none; +} + +.dark #sourcey .sourcey-changelog-version-link { + color: rgb(var(--color-gray-200)); +} + +#sourcey .sourcey-changelog-version-link:hover { + color: rgb(var(--color-primary-ink)); +} + +.dark #sourcey .sourcey-changelog-version-link:hover { + color: rgb(var(--color-primary-light)); +} + +#sourcey .sourcey-changelog-version-meta { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.925rem; + color: rgb(var(--color-gray-500)); +} + +.dark #sourcey .sourcey-changelog-version-meta { + color: rgb(var(--color-gray-400)); +} + +#sourcey .sourcey-changelog-compare-link { + color: rgb(var(--color-primary-ink)); + text-decoration: none; +} + +.dark #sourcey .sourcey-changelog-compare-link { + color: rgb(var(--color-primary-light)); +} + +#sourcey .sourcey-changelog-pill { + display: inline-flex; + align-items: center; + border-radius: 999px; + padding: 0.15rem 0.5rem; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.03em; +} + +#sourcey .sourcey-changelog-pill-yanked { + background: rgb(239 68 68 / 0.12); + color: rgb(185 28 28); +} + +.dark #sourcey .sourcey-changelog-pill-yanked { + background: rgb(248 113 113 / 0.15); + color: rgb(252 165 165); +} + +#sourcey .sourcey-changelog-pill-pre { + background: rgb(var(--color-gray-100)); + color: rgb(var(--color-gray-600)); +} + +.dark #sourcey .sourcey-changelog-pill-pre { + background: rgb(255 255 255 / 0.08); + color: rgb(var(--color-gray-300)); +} + +#sourcey .sourcey-changelog-pill-next { + background: rgb(var(--color-primary) / 0.1); + color: rgb(var(--color-primary-ink)); + text-transform: uppercase; + letter-spacing: 0.06em; + font-size: 0.68rem; +} + +.dark #sourcey .sourcey-changelog-pill-next { + background: rgb(var(--color-primary-light) / 0.14); + color: rgb(var(--color-primary-light)); +} + +#sourcey .sourcey-changelog-summary { + margin-bottom: 1rem; +} + +#sourcey .sourcey-changelog-summary p:first-child { + margin-top: 0; +} + +#sourcey .sourcey-changelog-sections { + display: flex; + flex-direction: column; + gap: 0.875rem; +} + +#sourcey .sourcey-changelog-section { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +#sourcey .sourcey-changelog-section-header { + display: flex; + align-items: center; + gap: 0.5rem; +} + +#sourcey .sourcey-changelog-entry-list { + margin: 0; + padding-left: 1.25rem; + color: rgb(var(--color-gray-700)); +} + +.dark #sourcey .sourcey-changelog-entry-list { + color: rgb(var(--color-gray-300)); +} + +#sourcey .sourcey-changelog-entry-list li { + margin: 0.35rem 0; +} + +#sourcey .sourcey-changelog-entry-list li > p { + margin: 0; +} + +#sourcey .sourcey-changelog-badge { + text-transform: none; +} + +#sourcey .sourcey-changelog-badge-added { + background: rgb(var(--color-success) / 0.12); + color: rgb(21 128 61); +} + +.dark #sourcey .sourcey-changelog-badge-added { + background: rgb(var(--color-success) / 0.16); + color: rgb(134 239 172); +} + +#sourcey .sourcey-changelog-badge-changed { + background: rgb(var(--color-primary) / 0.12); + color: rgb(var(--color-primary-ink)); +} + +.dark #sourcey .sourcey-changelog-badge-changed { + background: rgb(var(--color-primary-light) / 0.12); + color: rgb(var(--color-primary-light)); +} + +#sourcey .sourcey-changelog-badge-fixed { + background: rgb(245 158 11 / 0.14); + color: rgb(180 83 9); +} + +.dark #sourcey .sourcey-changelog-badge-fixed { + background: rgb(251 191 36 / 0.16); + color: rgb(253 224 71); +} + +#sourcey .sourcey-changelog-badge-removed, +#sourcey .sourcey-changelog-badge-security { + background: rgb(239 68 68 / 0.12); + color: rgb(185 28 28); +} + +.dark #sourcey .sourcey-changelog-badge-removed, +.dark #sourcey .sourcey-changelog-badge-security { + background: rgb(248 113 113 / 0.16); + color: rgb(252 165 165); +} + +#sourcey .sourcey-changelog-badge-deprecated, +#sourcey .sourcey-changelog-badge-other { + background: rgb(var(--color-gray-100)); + color: rgb(var(--color-gray-600)); +} + +.dark #sourcey .sourcey-changelog-badge-deprecated, +.dark #sourcey .sourcey-changelog-badge-other { + background: rgb(255 255 255 / 0.08); + color: rgb(var(--color-gray-300)); +} + +/* Tip — green */ +#sourcey .callout-tip { + border-left-color: #22c55e; +} +.dark #sourcey .callout-tip { + border-left-color: #4ade80; +} +#sourcey .callout-tip .callout-title { + color: #22c55e; +} +.dark #sourcey .callout-tip .callout-title { + color: #4ade80; +} + +/* Info — violet */ +#sourcey .callout-info { + border-left-color: #8b5cf6; +} +.dark #sourcey .callout-info { + border-left-color: #a78bfa; +} +#sourcey .callout-info .callout-title { + color: #8b5cf6; +} +.dark #sourcey .callout-info .callout-title { + color: #a78bfa; +} + +/* ── Directive Tabs (:::tabs, :::code-group) ───────────────────────── */ + +#sourcey .directive-tabs { + margin: 1.25rem 0; + border: 1px solid rgb(var(--color-stone-950) / 0.1); + border-radius: var(--radius); + overflow: hidden; +} +.dark #sourcey .directive-tabs { + border-color: rgb(255 255 255 / 0.1); +} + +#sourcey .directive-tab-bar { + display: flex; + gap: 0; + border-bottom: 1px solid rgb(var(--color-stone-200)); + background: rgb(var(--color-stone-50)); + padding: 0 0.25rem; +} +.dark #sourcey .directive-tab-bar { + border-bottom-color: rgb(255 255 255 / 0.06); + background: rgb(var(--color-code-block-dark)); +} + +#sourcey .directive-tab { + position: relative; + padding: 0.625rem 1rem; + font-size: 0.8125rem; + font-weight: 500; + color: rgb(var(--color-stone-500)); + background: transparent; + border: none; + cursor: pointer; + transition: color 0.15s; +} +#sourcey .directive-tab:hover { + color: rgb(var(--color-gray-900)); +} +.dark #sourcey .directive-tab:hover { + color: rgb(var(--color-gray-300)); +} + +#sourcey .directive-tab.active { + color: rgb(var(--color-primary-ink)); +} +.dark #sourcey .directive-tab.active { + color: rgb(var(--color-primary-light)); +} + +#sourcey .directive-tab.active::after { + content: ""; + position: absolute; + right: 0.5rem; + bottom: -1px; + left: 0.5rem; + height: 2px; + border-radius: 9999px; + background: rgb(var(--color-primary)); +} +.dark #sourcey .directive-tab.active::after { + background: rgb(var(--color-primary-light)); +} + +#sourcey .directive-tab-panel { + display: none; + padding: 1rem 1.25rem; +} + +#sourcey .directive-tab-panel.active { + display: block; +} + +/* Code group: tighter padding, code blocks fill the panel */ +#sourcey .directive-code-group .directive-tab-panel { + padding: 0; +} + +#sourcey .directive-code-group .directive-tab-panel pre { + margin: 0; + border-radius: 0; +} + +/* Strip nested code block chrome inside tab panels */ +#sourcey .directive-tabs .prose-code-block { + border: none; + border-radius: 0; + margin: 0; +} + +#sourcey .directive-tabs .prose-code-block .prose-code-copy { + display: none; +} + +/* ── Mobile navigation drawer (dialog) ────────────────────────────── */ + +.mobile-nav-dialog { + padding: 0; + border: none; + max-width: 20rem; + width: 80%; + height: 100%; + max-height: 100%; + margin: 0; + overflow-y: auto; + font-family: var(--font-sans); + font-size: 0.875rem; + line-height: 1.5rem; + background: rgb(var(--color-background-light)); + color: rgb(var(--color-gray-500)); +} +.mobile-nav-dialog[open] { + display: flex; + flex-direction: column; +} +.dark .mobile-nav-dialog { + background: rgb(var(--color-background-dark)); + color: rgb(var(--color-gray-400)); +} +.mobile-nav-dialog::backdrop { + background: rgb(0 0 0 / 0.25); + backdrop-filter: blur(2px); +} +.mobile-nav-dialog[open] { + animation: drawer-slide-in 0.2s ease-out; +} +.mobile-nav-dialog[open]::backdrop { + animation: drawer-fade-in 0.2s ease-out; +} + +/* ── Drawer group dropdown ─────────────────────────────────────────── */ + +.drawer-dropdown { + position: relative; +} + +.drawer-dropdown-trigger { + position: relative; + z-index: 11; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + width: 100%; + padding: 0.625rem 1.125rem; + border-radius: var(--radius, 0.5rem); + font-size: 1rem; + font-weight: 600; + font-family: var(--font-sans); + color: rgb(var(--color-gray-900)); + background: rgb(var(--color-background-light)); + border: none; + box-shadow: inset 0 0 0 1px rgb(var(--color-gray-200)); + cursor: pointer; + text-align: left; +} +.dark .drawer-dropdown-trigger { + color: rgb(var(--color-gray-200)); + background: rgb(var(--color-background-dark)); + box-shadow: inset 0 0 0 1px rgb(var(--color-gray-600)); +} +.drawer-dropdown-trigger[aria-expanded="true"] { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + box-shadow: + inset 1px 0 0 rgb(var(--color-gray-200)), + inset -1px 0 0 rgb(var(--color-gray-200)), + inset 0 1px 0 rgb(var(--color-gray-200)); +} +.dark .drawer-dropdown-trigger[aria-expanded="true"] { + box-shadow: + inset 1px 0 0 rgb(var(--color-gray-600)), + inset -1px 0 0 rgb(var(--color-gray-600)), + inset 0 1px 0 rgb(var(--color-gray-600)); +} + +.drawer-dropdown-chevron { + color: rgb(var(--color-gray-400)); + transition: transform 0.15s ease; + flex-shrink: 0; +} +.drawer-dropdown-trigger[aria-expanded="true"] .drawer-dropdown-chevron { + transform: rotate(180deg); +} + +.drawer-dropdown-list { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 10; + list-style: none; + margin: 0; + padding: 0.375rem 0; + border-radius: 0 0 var(--radius, 0.5rem) var(--radius, 0.5rem); + background: rgb(var(--color-background-light)); + border: none; + box-shadow: + inset 1px 0 0 rgb(var(--color-gray-200)), + inset -1px 0 0 rgb(var(--color-gray-200)), + inset 0 -1px 0 rgb(var(--color-gray-200)), + 0 6px 16px rgb(0 0 0 / 0.08); +} +.dark .drawer-dropdown-list { + background: rgb(var(--color-background-dark)); + box-shadow: + inset 1px 0 0 rgb(var(--color-gray-600)), + inset -1px 0 0 rgb(var(--color-gray-600)), + inset 0 -1px 0 rgb(var(--color-gray-600)), + 0 6px 16px rgb(0 0 0 / 0.3); +} + +.drawer-dropdown-item { + display: block; + width: 100%; + padding: 0.5rem 1.125rem; + font-size: 1rem; + font-family: var(--font-sans); + text-align: left; + color: rgb(var(--color-gray-600)); + background: none; + border: none; + cursor: pointer; +} +.drawer-dropdown-item:hover { + background: rgb(var(--color-gray-50)); +} +.dark .drawer-dropdown-item { + color: rgb(var(--color-gray-400)); +} +.dark .drawer-dropdown-item:hover { + background: rgb(var(--color-gray-800)); +} +.drawer-dropdown-item.active { + color: rgb(var(--color-primary-ink)); + font-weight: 500; +} +.dark .drawer-dropdown-item.active { + color: rgb(var(--color-primary-light)); +} + +/* Drawer nav items — larger touch targets */ +#sourcey .mobile-nav-dialog .nav-link, +#sourcey .mobile-nav-dialog .nav-link:hover, +#sourcey .mobile-nav-dialog .nav-link.active { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} +.mobile-nav-dialog ul { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +@keyframes drawer-slide-in { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} +@keyframes drawer-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* ── Code group cards (API right-hand panels) ─────────────────────── */ + +#sourcey .code-group { + position: relative; + display: flex; + flex-direction: column; + /* Visible so an open header dropdown menu is not clipped by the card; the + code body's rounded corners are clipped by .code-card-body instead. */ + overflow: visible; + border-radius: var(--radius); + border: 1px solid rgb(var(--color-stone-950) / 0.1); +} +.dark #sourcey .code-group { + border-color: rgb(255 255 255 / 0.1); +} + +#sourcey .code-group > div:first-child { + background: rgb(var(--color-stone-50)); + border-top-left-radius: var(--radius); + border-top-right-radius: var(--radius); +} +.dark #sourcey .code-group > div:first-child { + background: rgb(255 255 255 / 0.03); +} + +/* Clips the code panels' rounded bottom corners now that the card itself does + not clip. Horizontal code scrolling stays on the inner body element. */ +#sourcey .code-card-body { + overflow: hidden; + border-bottom-left-radius: var(--radius); + border-bottom-right-radius: var(--radius); +} + +#sourcey .code-lang-menu { + max-height: min(18rem, calc(100vh - var(--header-height, 0px) - 2rem)); + overflow-y: auto; + overscroll-behavior: contain; +} + +/* ── Print/PDF output ─────────────────────────────────────────────── */ + +@media print { + @page { + margin: 0.7in; + } + + html, + body, + #sourcey, + #sourcey #page, + #sourcey #docs { + background: #fff !important; + color: #111827 !important; + } + + #sourcey #page::before, + #sourcey #navbar, + #sourcey #toc, + #sourcey #search-dialog, + #sourcey .mobile-nav-dialog, + #sourcey .copy-btn, + #sourcey .prose-code-copy, + #sourcey .code-lang-dropdown { + display: none !important; + } + + #sourcey .max-w-\[92rem\], + #sourcey .max-w-3xl { + max-width: none !important; + margin: 0 !important; + padding: 0 !important; + } + + #sourcey #docs { + padding-top: 0 !important; + } + + #sourcey #docs > .flex, + #sourcey #docs [class*="lg:flex-row"], + #sourcey #docs [class*="xl:flex-row"] { + display: block !important; + } + + #sourcey #content-area { + width: 100% !important; + max-width: none !important; + margin: 0 !important; + padding: 0 !important; + } + + #sourcey #sidebar { + display: block !important; + position: static !important; + inset: auto !important; + width: auto !important; + margin: 0 0 1.5rem !important; + padding: 0 0 1rem !important; + border-bottom: 1px solid #e5e7eb; + } + + #sourcey #sidebar > div { + position: static !important; + inset: auto !important; + overflow: visible !important; + padding: 0 !important; + } + + #sourcey #sidebar .sticky { + display: none !important; + } + + #sourcey #sidebar #nav::before { + content: "Table of Contents"; + display: block; + margin: 0 0 0.75rem; + font-size: 0.875rem; + font-weight: 700; + color: #111827; + } + + #sourcey #sidebar .nav-group-label, + #sourcey #sidebar .nav-link { + color: #111827 !important; + background: transparent !important; + } + + #sourcey #sidebar .nav-link { + padding: 0.125rem 0 !important; + } + + #sourcey #docs article aside { + display: none !important; + } + + #sourcey #docs [class*="lg:hidden"], + #sourcey #docs [class*="xl:hidden"] { + display: block !important; + } + + #sourcey .code-group, + #sourcey .prose-code-block, + #sourcey .param-item, + #sourcey .schema-expandable, + #sourcey .sourcey-changelog-version { + break-inside: avoid; + } + + #sourcey .code-group, + #sourcey .prose-code-block, + #sourcey .prose-code-content, + #sourcey .code-card-body, + #sourcey .code-card-body > div, + #sourcey .code-card-body .font-mono { + background: #fff !important; + color: #111827 !important; + } + + #sourcey a { + color: #111827 !important; + text-decoration: underline; + } +} + +/* ── Selection ─────────────────────────────────────────────────────── */ + +#sourcey ::selection { + background: rgb(var(--color-primary) / 0.12); +} + +/* ── Search Dialog ────────────────────────────────────────────────── */ + +#sourcey #search-dialog { + display: none; + position: fixed; + inset: 0; + z-index: 500; + background: transparent; +} + +#sourcey #search-dialog.open { + display: block; +} + +#sourcey .search-dialog-inner { + position: absolute; + background: rgb(var(--color-background-light)); + border-radius: var(--radius); + border: 1px solid rgb(var(--color-gray-200) / 0.7); + box-shadow: 0 8px 32px rgb(var(--color-overlay) / 0.12); + overflow: hidden; +} +.dark #sourcey .search-dialog-inner { + background: rgb(var(--color-gray-900)); + border-color: rgb(var(--color-gray-700) / 0.5); + box-shadow: 0 8px 32px rgb(var(--color-overlay) / 0.4); +} + +#sourcey .search-input-row { + display: flex; + align-items: center; + gap: 0.625rem; + padding: 0.75rem 1rem; + border-bottom: 1px solid rgb(var(--color-gray-200)); +} +.dark #sourcey .search-input-row { + border-bottom-color: rgb(var(--color-gray-800)); +} + +#sourcey .search-input-icon { + width: 1.125rem; + height: 1.125rem; + flex-shrink: 0; + color: rgb(var(--color-gray-400)); +} + +#sourcey #search-input { + width: 100%; + border: none; + font-family: var(--font-sans); + font-size: 0.9375rem; + color: rgb(var(--color-gray-900)); + background: transparent; + outline: none; +} +.dark #sourcey #search-input { + color: rgb(var(--color-gray-100)); +} + +#sourcey #search-input::placeholder { + color: rgb(var(--color-gray-400)); +} + +#sourcey #search-results { + max-height: 50vh; + overflow-y: auto; + padding: 0.25rem; +} + +#sourcey .search-category { + padding: 0.5rem 0.75rem 0.25rem; + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: rgb(var(--color-gray-400)); +} + +#sourcey .search-footer { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.5rem 0.75rem; + border-top: 1px solid rgb(var(--color-gray-200)); + font-size: 0.6875rem; + color: rgb(var(--color-gray-400)); +} +.dark #sourcey .search-footer { + border-top-color: rgb(var(--color-gray-800)); +} + +#sourcey .search-footer-hint { + display: flex; + align-items: center; + gap: 0.25rem; +} + +#sourcey .search-footer kbd, +#sourcey #search-open kbd { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.25rem; + height: 1.125rem; + padding: 0 0.25rem; + font-family: var(--font-sans); + font-size: 0.625rem; + font-weight: 500; + border-radius: 0.25rem; + border: 1px solid rgb(var(--color-gray-200)); + background: rgb(var(--color-gray-50)); + color: rgb(var(--color-gray-500)); +} +.dark #sourcey .search-footer kbd, +.dark #sourcey #search-open kbd { + border-color: rgb(var(--color-gray-700)); + background: rgb(var(--color-gray-800)); + color: rgb(var(--color-gray-400)); +} + +#sourcey .search-result { + display: flex; + align-items: baseline; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + text-decoration: none; + color: rgb(var(--color-gray-700)); + border-radius: 0.375rem; + cursor: pointer; + transition: background 0.1s; +} +.dark #sourcey .search-result { + color: rgb(var(--color-gray-400)); +} + +#sourcey .search-result:hover, +#sourcey .search-result.active { + background: rgb(var(--color-primary) / 0.06); + color: rgb(var(--color-gray-900)); +} +.dark #sourcey .search-result:hover, +.dark #sourcey .search-result.active { + background: rgb(var(--color-primary-light) / 0.08); + color: rgb(var(--color-gray-200)); +} + +#sourcey .search-result-main { + display: flex; + align-items: baseline; + gap: 0.375rem; + flex: 1; + min-width: 0; +} + +#sourcey .search-result-method { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.375rem; + font-size: 0.625rem; + font-weight: 700; + line-height: 1; + letter-spacing: 0.04em; + text-transform: uppercase; + white-space: nowrap; + border: 1px solid transparent; + border-radius: 3px; + color: rgb(var(--color-background-light)); + flex-shrink: 0; +} + +#sourcey .search-result-method.method-get { + background: var(--method-get); +} +#sourcey .search-result-method.method-post { + background: var(--method-post); +} +#sourcey .search-result-method.method-put { + background: var(--method-put); +} +#sourcey .search-result-method.method-delete { + background: var(--method-delete); +} +#sourcey .search-result-method.method-patch { + background: var(--method-patch); +} +#sourcey .search-result-method.method-tool { + background: var(--method-tool); +} +#sourcey .search-result-method.method-resource { + background: var(--method-resource); +} +#sourcey .search-result-method.method-prompt { + background: var(--method-prompt); +} + +#sourcey .search-result-path { + font-family: var(--font-mono); + font-size: 0.8125rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +#sourcey .search-result-summary { + font-size: 0.8125rem; + color: rgb(var(--color-gray-500)); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +#sourcey .search-result-tag { + font-size: 0.6875rem; + color: rgb(var(--color-gray-400)); + white-space: nowrap; + flex-shrink: 0; +} + +#sourcey .search-loading { + padding: 1rem; + color: rgb(var(--color-gray-500)); + font-size: 0.8125rem; +} + +/* ── Godoc rendering ─────────────────────────────────────────────── */ + +#sourcey .godoc-import, +#sourcey .godoc-import-path { + font-size: 0.8125rem; + color: rgb(var(--color-gray-500)); + margin: 0.25rem 0 1.5rem; +} + +#sourcey .godoc-import code, +#sourcey .godoc-import-path code { + font-family: var(--font-mono); + background: rgba(var(--color-gray-100), 0.6); + padding: 0.125rem 0.375rem; + border-radius: 3px; +} + +#sourcey .godoc-source { + margin: -0.25rem 0 0.625rem; + font-size: 0.75rem; + color: rgb(var(--color-gray-500)); +} + +#sourcey .godoc-source a { + color: inherit; + text-decoration: none; +} + +#sourcey .godoc-source a:hover { + color: rgb(var(--color-primary-ink)); +} + +#sourcey .godoc-toc { + margin: 1.5rem 0 2.5rem; + padding: 0.875rem 1rem; + border: 1px solid rgb(var(--color-gray-200)); + border-radius: 6px; + background: rgba(var(--color-gray-50), 0.5); +} + +#sourcey .godoc-toc h4 { + margin: 0 0 0.5rem; + font-size: 0.6875rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: rgb(var(--color-gray-500)); +} + +#sourcey .godoc-toc ul { + list-style: none; + margin: 0; + padding: 0; + font-size: 0.8125rem; +} + +#sourcey .godoc-toc li { + margin: 0.125rem 0; +} + +#sourcey .godoc-toc li.godoc-toc-sub { + padding-left: 1rem; + font-family: var(--font-mono); + font-size: 0.75rem; + color: rgb(var(--color-gray-600)); +} + +#sourcey .godoc-toc a { + color: inherit; + text-decoration: none; + border-bottom: 1px dotted transparent; +} + +#sourcey .godoc-toc a:hover { + color: rgb(var(--color-primary)); + border-bottom-color: currentColor; +} + +#sourcey .godoc-symbol { + font-family: var(--font-mono); + font-size: 0.95rem; + font-weight: 600; + margin: 1.75rem 0 0.5rem; + color: rgb(var(--color-gray-900)); + scroll-margin-top: 5rem; +} + +#sourcey .godoc-doc { + margin: 0.5rem 0 0.75rem; + color: rgb(var(--color-gray-700)); + font-size: 0.9375rem; + line-height: 1.65; +} + +#sourcey .godoc-doc p:first-child { + margin-top: 0; +} + +#sourcey .godoc-doc p:last-child { + margin-bottom: 0; +} + +#sourcey .godoc-value, +#sourcey .godoc-func, +#sourcey .godoc-type { + margin: 1rem 0 1.5rem; + scroll-margin-top: 5rem; +} + +#sourcey .godoc-type { + border-top: 1px solid rgb(var(--color-gray-200)); + padding-top: 1.25rem; + margin-top: 2rem; +} + +#sourcey .godoc-anchor { + display: block; + height: 0; + scroll-margin-top: 5rem; +} + +#sourcey .godoc-fields { + margin: 0.75rem 0 1rem; + padding: 0; + border: 1px solid rgb(var(--color-gray-200)); + border-radius: 6px; + background: rgba(var(--color-gray-50), 0.4); +} + +#sourcey .godoc-fields > summary { + cursor: pointer; + padding: 0.5rem 0.875rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: rgb(var(--color-gray-600)); + list-style: none; +} + +#sourcey .godoc-fields > summary::-webkit-details-marker { + display: none; +} + +#sourcey .godoc-fields > ul { + list-style: none; + margin: 0; + padding: 0; + border-top: 1px solid rgb(var(--color-gray-200)); +} + +#sourcey .godoc-field { + padding: 0.625rem 0.875rem; + border-bottom: 1px solid rgb(var(--color-gray-200)); + font-size: 0.875rem; +} + +#sourcey .godoc-field:last-child { + border-bottom: none; +} + +#sourcey .godoc-field-sig { + font-family: var(--font-mono); + font-size: 0.8125rem; + font-weight: 600; + color: rgb(var(--color-gray-900)); + background: transparent; + padding: 0; +} + +#sourcey .godoc-field-type { + font-weight: 400; + color: rgb(var(--color-gray-700)); +} + +#sourcey .godoc-tag { + font-family: var(--font-mono); + font-size: 0.75rem; + color: rgb(var(--color-gray-500)); + background: transparent; + padding: 0; + margin-left: 0.5rem; +} + +#sourcey .godoc-field-doc { + margin-top: 0.25rem; + font-size: 0.8125rem; + color: rgb(var(--color-gray-600)); + line-height: 1.55; +} + +#sourcey .godoc-example { + margin: 0.75rem 0 1.25rem; + padding: 0; + border: 1px solid rgb(var(--color-gray-200)); + border-radius: 6px; + background: rgba(var(--color-gray-50), 0.4); +} + +#sourcey .godoc-example > summary { + cursor: pointer; + padding: 0.5rem 0.875rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: rgb(var(--color-gray-600)); + list-style: none; +} + +#sourcey .godoc-example > summary::-webkit-details-marker { + display: none; +} + +#sourcey .godoc-example > summary::before { + content: "▸ "; + color: rgb(var(--color-gray-400)); + margin-right: 0.25rem; +} + +#sourcey .godoc-example[open] > summary::before { + content: "▾ "; +} + +#sourcey .godoc-example > pre, +#sourcey .godoc-example > .godoc-doc, +#sourcey .godoc-example > p { + margin: 0.5rem 0.875rem; +} + +#sourcey .godoc-example-output-label { + margin: 0.5rem 0.875rem 0; + font-size: 0.6875rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: rgb(var(--color-gray-500)); +} + +#sourcey .godoc-func .godoc-symbol, +#sourcey .godoc-type .godoc-symbol { + word-break: break-word; +} + +/* ── Rust API rendering (rustdoc adapter) ────────────────────────── */ +/* Shared API primitives (api-*) introduced for rustdoc and reusable + by godoc/doxygen in a follow-on retrofit. Class names mirror + rustdoc's DOM so deep-links from a rustdoc URL line up with + sourcey output. */ + +#sourcey .rust-item { + border-top: 1px solid rgb(var(--color-gray-200)); + padding-top: 1rem; + margin-top: 1.25rem; +} +.dark #sourcey .rust-item { + border-top-color: rgb(var(--color-gray-800)); +} + +#sourcey .code-header, +#sourcey .rust-signature { + font-family: + ui-monospace, "SF Mono", "Cascadia Code", Consolas, "Liberation Mono", Menlo, monospace; + font-size: 0.9375rem; + color: rgb(var(--color-gray-900)); + white-space: pre-wrap; + word-break: break-word; +} +.dark #sourcey .code-header, +.dark #sourcey .rust-signature { + color: rgb(var(--color-gray-100)); +} + +#sourcey .rust-signature .kw { + color: rgb(var(--color-rose-600)); + font-weight: 600; +} +.dark #sourcey .rust-signature .kw { + color: rgb(var(--color-rose-300)); +} + +#sourcey .rust-signature .ident { + color: rgb(var(--color-indigo-600)); +} +.dark #sourcey .rust-signature .ident { + color: rgb(var(--color-indigo-300)); +} + +#sourcey .rust-signature .lifetime { + color: rgb(var(--color-amber-700)); + font-style: italic; +} +.dark #sourcey .rust-signature .lifetime { + color: rgb(var(--color-amber-300)); +} + +#sourcey .api-rightside, +#sourcey .rightside { + float: right; + font-size: 0.8125rem; + color: rgb(var(--color-gray-500)); +} + +#sourcey .api-since, +#sourcey .since { + font-feature-settings: "tnum"; +} + +#sourcey .api-srclink, +#sourcey .srclink { + color: rgb(var(--color-indigo-600)); + text-decoration: none; +} +#sourcey .api-srclink:hover, +#sourcey .srclink:hover { + text-decoration: underline; +} +.dark #sourcey .api-srclink, +.dark #sourcey .srclink { + color: rgb(var(--color-indigo-300)); +} + +#sourcey .docblock, +#sourcey .rust-doc { + color: rgb(var(--color-gray-700)); + margin-top: 0.5rem; +} +.dark #sourcey .docblock, +.dark #sourcey .rust-doc { + color: rgb(var(--color-gray-300)); +} + +#sourcey .stab, +#sourcey .api-stab { + display: block; + margin: 0.5rem 0; + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + font-size: 0.875rem; + border: 1px solid transparent; +} +#sourcey .stab.unstable, +#sourcey .api-stab-unstable { + background-color: rgb(var(--color-amber-50)); + border-color: rgb(var(--color-amber-200)); + color: rgb(var(--color-amber-900)); +} +#sourcey .stab.deprecated, +#sourcey .api-stab-deprecated { + background-color: rgb(var(--color-rose-50)); + border-color: rgb(var(--color-rose-200)); + color: rgb(var(--color-rose-900)); +} +#sourcey .stab.portability, +#sourcey .api-stab-portability { + background-color: rgb(var(--color-emerald-50)); + border-color: rgb(var(--color-emerald-200)); + color: rgb(var(--color-emerald-900)); +} +.dark #sourcey .stab.unstable, +.dark #sourcey .api-stab-unstable { + background-color: rgba(251, 191, 36, 0.08); + border-color: rgba(251, 191, 36, 0.25); + color: rgb(var(--color-amber-200)); +} +.dark #sourcey .stab.deprecated, +.dark #sourcey .api-stab-deprecated { + background-color: rgba(244, 63, 94, 0.08); + border-color: rgba(244, 63, 94, 0.25); + color: rgb(var(--color-rose-200)); +} +.dark #sourcey .stab.portability, +.dark #sourcey .api-stab-portability { + background-color: rgba(16, 185, 129, 0.08); + border-color: rgba(16, 185, 129, 0.25); + color: rgb(var(--color-emerald-200)); +} + +/* Collapsible trait-impl block. Chevron pattern mirrors .godoc-example so the + disclosure reads the same across adapters. */ +#sourcey .api-toggle { + margin: 0.75rem 0; + border: 1px solid rgb(var(--color-gray-200)); + border-radius: 6px; + background: rgba(var(--color-gray-50), 0.4); +} +.dark #sourcey .api-toggle { + border-color: rgb(var(--color-gray-800)); +} +#sourcey .api-toggle > summary { + cursor: pointer; + padding: 0.5rem 0.875rem; + list-style: none; + font-family: var(--font-mono); + font-size: 0.8125rem; + color: rgb(var(--color-gray-700)); +} +.dark #sourcey .api-toggle > summary { + color: rgb(var(--color-gray-300)); +} +#sourcey .api-toggle > summary::-webkit-details-marker { + display: none; +} +#sourcey .api-toggle > summary::before { + content: "▸ "; + color: rgb(var(--color-gray-400)); + margin-right: 0.25rem; +} +#sourcey .api-toggle[open] > summary::before { + content: "▾ "; +} +/* The impl header lives inside the summary; keep it inline with the chevron + and strip heading margins. */ +#sourcey .api-toggle > summary .rust-impl-header { + display: inline; + margin: 0; + font-size: inherit; +} +#sourcey .api-toggle-body { + padding: 0.25rem 0.875rem 0.5rem; + border-top: 1px solid rgb(var(--color-gray-200)); +} +.dark #sourcey .api-toggle-body { + border-top-color: rgb(var(--color-gray-800)); +} +/* Marker / auto / blanket trait impls: a quiet list, not expandable boxes. */ +#sourcey .rust-impl-rows { + margin: 0.5rem 0 0.75rem; +} +#sourcey .rust-impl-row { + padding: 0.2rem 0; +} +#sourcey .rust-impl-row .rust-impl-header { + display: inline; + margin: 0; + font-size: 0.8125rem; + font-weight: 400; + color: rgb(var(--color-gray-600)); +} +.dark #sourcey .rust-impl-row .rust-impl-header { + color: rgb(var(--color-gray-400)); +} + +#sourcey .anchor, +#sourcey .api-anchor { + margin-left: 0.4rem; + color: rgb(var(--color-gray-400)); + text-decoration: none; + opacity: 0; + transition: opacity 0.1s ease; +} +#sourcey .section-header:hover .anchor, +#sourcey .section-header:hover .api-anchor, +#sourcey .code-header:hover .anchor, +#sourcey .code-header:hover .api-anchor { + opacity: 1; +} + +/* Inherent-impl methods and trait members render inline as their own sections. + Code (signatures, doctests) flows through the shared Shiki code block, so no + bespoke code styling lives here. */ +#sourcey .rust-member { + margin: 0.75rem 0; +} +#sourcey .rust-method-header { + margin: 0.5rem 0 0.25rem; +} +#sourcey .rust-doctest { + margin: 0.75rem 0; +} + +#sourcey .rust-doctest-badges { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + margin-bottom: 0.25rem; +} +#sourcey .rust-doctest-badge { + display: inline-block; + padding: 0.05rem 0.4rem; + font-size: 0.6875rem; + font-weight: 600; + border-radius: 0.25rem; + background-color: rgb(var(--color-gray-100)); + color: rgb(var(--color-gray-700)); + text-transform: lowercase; + letter-spacing: 0.02em; +} +.dark #sourcey .rust-doctest-badge { + background-color: rgb(var(--color-gray-800)); + color: rgb(var(--color-gray-300)); +} +#sourcey .rust-doctest-badge-should_panic, +#sourcey .rust-doctest-badge-compile_fail { + background-color: rgb(var(--color-rose-100)); + color: rgb(var(--color-rose-800)); +} +#sourcey .rust-doctest-badge-no_run, +#sourcey .rust-doctest-badge-ignore { + background-color: rgb(var(--color-amber-100)); + color: rgb(var(--color-amber-800)); +} + +#sourcey .rust-doctest-controls { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; + font-size: 0.8125rem; +} +#sourcey .rust-doctest-run, +#sourcey .rust-doctest-toggle-hidden { + cursor: pointer; + padding: 0.2rem 0.6rem; + border-radius: 0.375rem; + border: 1px solid rgb(var(--color-gray-300)); + background-color: rgb(var(--color-white)); + color: rgb(var(--color-gray-800)); + text-decoration: none; + line-height: 1; +} +#sourcey .rust-doctest-run:hover, +#sourcey .rust-doctest-toggle-hidden:hover { + background-color: rgb(var(--color-gray-50)); +} +.dark #sourcey .rust-doctest-run, +.dark #sourcey .rust-doctest-toggle-hidden { + background-color: rgb(var(--color-gray-900)); + border-color: rgb(var(--color-gray-700)); + color: rgb(var(--color-gray-200)); +} + +/* On the aggregated doctests page the code block already renders as a card, so + the section itself stays borderless. Just space the entries and accent the + heading so they don't read as cards within cards. */ +#sourcey .rust-doctests-index .rust-doctest { + margin: 1.5rem 0; +} +#sourcey .rust-doctests-index .rust-doctest > h3 { + padding-left: 0.625rem; + border-left: 3px solid rgb(var(--color-emerald-500)); +} +.dark #sourcey .rust-doctests-index .rust-doctest > h3 { + border-left-color: rgb(var(--color-emerald-400)); +} diff --git a/docs/sourcey-catalog/site/sourcey.js b/docs/sourcey-catalog/site/sourcey.js new file mode 100644 index 000000000..d0dc64577 --- /dev/null +++ b/docs/sourcey-catalog/site/sourcey.js @@ -0,0 +1 @@ +(function(){(function(){function e(e){try{return window.sessionStorage.getItem(e)}catch{return null}}function t(e,t){try{window.sessionStorage.setItem(e,t)}catch{}}function n(e){try{window.sessionStorage.removeItem(e)}catch{}}function r(e){var t=(e||`/`).replace(/\/+$/,``)||`/`;return t=t.replace(/\/index(?:\.html)?$/,``)||`/`,t=t.replace(/\.html$/,``),t||`/`}function i(e){if(!e.length)return null;for(var t=e.map(function(e){return r(e).split(`/`).filter(Boolean)}),n=[],i=0;i=document.body.scrollHeight-10){var l=c[c.length-1];i=n.length?l.getAttribute(`data-traverse-target`):l.id}i&&d(i)}}var _=!1;window.addEventListener(`scroll`,function(){_||(_=!0,requestAnimationFrame(function(){g(),_=!1}))},{passive:!0});var v=window.location.hash.slice(1)||new URLSearchParams(window.location.search).get(`target`);v&&document.getElementById(v)?(d(v),requestAnimationFrame(function(){f(v,`instant`)})):g(),window.addEventListener(`hashchange`,function(){var e=window.location.hash.slice(1);e&&(d(e,!0),f(e,`smooth`))})}window.location.hash.slice(1)||new URLSearchParams(window.location.search).get(`target`)?requestAnimationFrame(e):`requestIdleCallback`in window?window.requestIdleCallback(e,{timeout:250}):window.addEventListener(`load`,e,{once:!0})})(),(function(){function e(){var e=null;document.addEventListener(`click`,function(e){var r=e.target.closest(`.code-lang-trigger`);if(document.querySelectorAll(`.code-lang-menu`).forEach(function(e){if(!r||!e.parentElement.contains(r)){t(e);var n=e.parentElement.querySelector(`.code-lang-trigger`);n&&n.setAttribute(`aria-expanded`,`false`)}}),r){e.stopPropagation();var i=r.nextElementSibling;if(i){var a=!i.classList.contains(`hidden`);a?t(i):(i.classList.remove(`hidden`),n(r,i)),r.setAttribute(`aria-expanded`,a?`false`:`true`)}}}),document.addEventListener(`click`,function(n){var r=n.target.closest(`.code-lang-option`);if(r){var a=r.closest(`.code-lang-dropdown`),o=!!(a&&a.hasAttribute(`data-lang-sync`)),s=r.textContent.trim(),c=r.closest(`.code-lang-menu`);if(c){t(c);var l=c.parentElement.querySelector(`.code-lang-trigger`);l&&l.setAttribute(`aria-expanded`,`false`)}var u=window.scrollY;if(i(r),o&&s!==e){e=s;var d=r.closest(`.code-group`);document.querySelectorAll(`.code-group`).forEach(function(e){e===d||!e.querySelector(`.code-lang-dropdown[data-lang-sync]`)||e.querySelectorAll(`.code-lang-option`).forEach(function(e){e.textContent.trim()===s&&i(e)})})}window.scrollTo(0,u)}});function t(e){e.classList.add(`hidden`),e.style.position=``,e.style.top=``,e.style.right=``,e.style.bottom=``,e.style.maxHeight=``}function n(e,t){var n=e.getBoundingClientRect(),r=4,i=8,a=window.innerHeight-n.bottom-i,o=n.top-i,s=a<160&&o>a;t.style.position=`fixed`,t.style.right=Math.max(i,window.innerWidth-n.right)+`px`,t.style.maxHeight=Math.max(96,Math.floor((s?o:a)-r))+`px`,s?(t.style.top=``,t.style.bottom=Math.max(i,window.innerHeight-n.top+r)+`px`):(t.style.bottom=``,t.style.top=Math.min(window.innerHeight-i,n.bottom+r)+`px`)}function r(e){var t=e.closest(`[data-response-dropdown]`);if(t){var n=t.getAttribute(`data-response-dropdown`),r=t.closest(`.code-group`),i=r&&r.querySelector(`.response-panel[data-response-panel="`+n+`"]`);if(i)return i}return e.closest(`.code-lang-scope`)||e.closest(`.code-group`)}function i(e){var t=e.closest(`.code-lang-dropdown`),n=r(e),i=e.getAttribute(`data-lang-index`);if(t){var a=t.querySelector(`.code-lang-label`);t.querySelectorAll(`.code-lang-option`).forEach(function(e){var n=e.getAttribute(`data-lang-index`)===i;if(e.setAttribute(`aria-selected`,n?`true`:`false`),e.className=e.className.replace(/(dark:)?text-\[rgb\([^\]]+\)\]/g,``).trim(),n){e.classList.add(`text-[rgb(var(--color-primary))]`,`dark:text-[rgb(var(--color-primary-light))]`),a&&(a.textContent=e.textContent.trim());var r=t.querySelector(`.code-lang-trigger .code-lang-icon`),o=e.querySelector(`.lang-icon`);r&&o&&(r.innerHTML=o.outerHTML)}else e.classList.add(`text-[rgb(var(--color-stone-600))]`,`dark:text-[rgb(var(--color-stone-400))]`)})}n&&n.querySelectorAll(`.code-lang-panel`).forEach(function(e){e.classList.toggle(`active`,e.getAttribute(`data-lang-panel`)===i)})}document.addEventListener(`click`,function(e){var t=e.target.closest(`.response-tab`);if(t){var n=t.closest(`.response-tabs`),r=t.getAttribute(`data-response-index`),i=window.scrollY;n.querySelectorAll(`.response-tab`).forEach(function(e){var t=e.getAttribute(`data-response-index`)===r;e.classList.toggle(`active`,t),e.setAttribute(`aria-selected`,t?`true`:`false`)}),n.querySelectorAll(`.response-panel`).forEach(function(e){e.classList.toggle(`active`,e.getAttribute(`data-response-panel`)===r)}),n.querySelectorAll(`[data-response-dropdown]`).forEach(function(e){e.classList.toggle(`hidden`,e.getAttribute(`data-response-dropdown`)!==r)}),window.scrollTo(0,i)}}),document.addEventListener(`click`,function(e){var t=e.target.closest(`.copy-btn`);if(t){var n=t.closest(`.code-group`)||t.closest(`.prose-code-block`);if(n){var r=n.querySelectorAll(`.code-lang-panel.active, .response-panel.active`),i=r.length?r[r.length-1]:null,a=i?i.querySelector(`code, .code-block, .font-mono`):n.querySelector(`code, .code-block, .font-mono`);if(a){var o=a.textContent||``;navigator.clipboard.writeText(o).then(function(){t.classList.add(`copied`);var e=t.nextElementSibling;e&&e.classList.contains(`copy-tooltip`)&&(e.textContent=`Copied!`),setTimeout(function(){t.classList.remove(`copied`),e&&e.classList.contains(`copy-tooltip`)&&(e.textContent=`Copy`)},2e3)})}}}}),document.addEventListener(`click`,function(e){var t=e.target.closest(`.rust-doctest-toggle-hidden`);if(t){var n=t.getAttribute(`data-target`),r=t.getAttribute(`data-display`),i=n?document.getElementById(n.replace(/^#/,``)):null,a=r?document.getElementById(r.replace(/^#/,``)):null;!i||!a||(i.hasAttribute(`hidden`)?(i.removeAttribute(`hidden`),a.setAttribute(`hidden`,``),t.textContent=`Hide hidden lines`):(i.setAttribute(`hidden`,``),a.removeAttribute(`hidden`),t.textContent=`Show hidden lines`))}}),document.addEventListener(`keydown`,function(e){e.key===`Escape`&&document.querySelectorAll(`.code-lang-menu`).forEach(function(e){e.classList.add(`hidden`);var t=e.parentElement.querySelector(`.code-lang-trigger`);t&&t.setAttribute(`aria-expanded`,`false`)})}),document.addEventListener(`click`,function(e){var t=e.target.closest(`.directive-tab`);if(t){var n=t.getAttribute(`data-tab-group`),r=t.getAttribute(`data-tab-index`),i=window.scrollY;document.querySelectorAll(`.directive-tab[data-tab-group="`+n+`"]`).forEach(function(e){e.classList.toggle(`active`,e.getAttribute(`data-tab-index`)===r)}),document.querySelectorAll(`.directive-tab-panel[data-tab-group="`+n+`"]`).forEach(function(e){e.classList.toggle(`active`,e.getAttribute(`data-tab-index`)===r)}),window.scrollTo(0,i)}})}`requestIdleCallback`in window?window.requestIdleCallback(e,{timeout:250}):window.addEventListener(`load`,e,{once:!0})})(),(function(){var e=`sourcey-theme`,t=document.getElementById(`theme-toggle`),n=document.documentElement;function r(){var t=localStorage.getItem(e);return t===`dark`||t===`light`?t:`light`}function i(e){e===`dark`?n.classList.add(`dark`):n.classList.remove(`dark`),n.style.colorScheme=e,t&&(t.setAttribute(`aria-label`,e===`dark`?`Switch to light mode`:`Switch to dark mode`),t.setAttribute(`title`,e===`dark`?`Light mode`:`Dark mode`))}i(r()),t&&t.addEventListener(`click`,function(){var t=n.classList.contains(`dark`)?`light`:`dark`;localStorage.setItem(e,t),i(t)})})(),(function(){function e(){var e=document.getElementById(`search-dialog`),t=document.getElementById(`search-input`),n=document.getElementById(`search-results`),r=document.getElementById(`search-open`);if(!e||!t||!n)return;var i=[],a=-1,o=[],s=!1,c=document.querySelector(`meta[name="sourcey-search"]`);function l(e){if(s){e();return}if(!c){s=!0,e();return}var t=c.getAttribute(`content`);fetch(t).then(function(e){return e.json()}).then(function(t){i=t.map(function(e){var t=e.title||``,n=e.qualifiedName||``,r=e.owner||``,i=e.tab||``;return{url:e.url,method:e.method||``,path:e.path||``,title:t,summary:n||t,tag:r||i,content:e.content||``,category:e.category||``,featured:!!e.featured,symbolKind:e.symbolKind||``,owner:r,ownerKind:e.ownerKind||``,namespace:e.namespace||``,qualifiedName:n,titleLower:t.toLowerCase(),pathLower:(e.path||``).toLowerCase(),qualifiedLower:n.toLowerCase(),ownerLower:r.toLowerCase(),searchText:[e.method||``,e.path||``,t,n,r,e.ownerKind||``,e.namespace||``,e.symbolKind||``,e.content||``,i].join(` `).toLowerCase()}}),s=!0,e()}).catch(function(){s=!0,e()})}var u=e.querySelector(`.search-dialog-inner`);function d(){if(!(!r||!u)){var e=r.getBoundingClientRect();u.style.position=`absolute`,u.style.top=e.top-4+`px`;var t=Math.min(e.width*.5,200);u.style.left=e.left-t/2+`px`,u.style.width=e.width+t+`px`,u.style.maxWidth=`none`,u.style.transform=`none`,u.style.margin=`0`}}function f(){d(),e.classList.add(`open`),t.value=``,t.focus(),s?m(``):(n.innerHTML=`
Loading…
`,l(function(){m(``)})),document.addEventListener(`keydown`,b)}function p(){e.classList.remove(`open`),document.removeEventListener(`keydown`,b)}function m(e){var t=e.toLowerCase().trim();if(t){var n=t.split(/\s+/);o=i.map(function(e){return{entry:e,score:h(e,t,n)}}).filter(function(e){return e.score>0});var r={Pages:0,Endpoints:1,Models:2,Functions:3,Types:4,Enums:5,"Enum Values":6,Variables:7,Friends:8,Properties:9,Members:10,Sections:20};o.sort(function(e,t){return t.score===e.score?(r[e.entry.category]||19)-(r[t.entry.category]||19):t.score-e.score}),o=o.map(function(e){return e.entry}).slice(0,50)}else{var s=i.filter(function(e){return e.featured}),c=i.filter(function(e){return!e.featured&&e.category!==`Sections`});o=s.concat(c).slice(0,30)}a=o.length?0:-1,g()}function h(e,t,n){if(!n.every(function(t){return e.searchText.indexOf(t)!==-1}))return 0;var r=t.replace(/\s+/g,``),i=e.qualifiedLower||``,a=e.titleLower||``,o=e.pathLower||``,s=e.ownerLower||``,c=1;return i&&i===r&&(c+=1200),a&&a===t&&(c+=900),o&&o===t&&(c+=850),i&&i.endsWith(`::`+r)&&(c+=780),s&&r===s+`::`+a&&(c+=760),i&&r.indexOf(`::`)!==-1&&i.indexOf(r)!==-1&&(c+=700),a&&a.indexOf(t)===0&&(c+=500),i&&i.indexOf(r)!==-1&&(c+=420),s&&s.indexOf(r)!==-1&&(c+=240),o&&o.indexOf(t)!==-1&&(c+=180),e.content.toLowerCase().indexOf(t)!==-1&&(c+=80),e.category===`Sections`&&(c-=70),e.symbolKind&&(c+=35),e.featured&&(c+=20),c}function g(){for(var e=``,t=``,r=0;r`+_(s)+``,t=s);var c=`search-result`+(r===a?` active`:``),l=i.method?``+i.method+` `+_(i.path)+``:``+_(i.summary)+``,u=i.tag?``+_(i.tag)+``:``,d=i.method&&i.summary?``+_(i.summary)+``:``;e+=`
`+l+d+`
`+u+`
`}n.innerHTML=e;var f=n.querySelector(`.search-result.active`);f&&f.scrollIntoView({block:`nearest`})}function _(e){var t=document.createElement(`span`);return t.textContent=e,t.innerHTML}function v(e){return _(e).replace(/"/g,`"`).replace(/'/g,`'`)}function y(e){if(!(e<0||e>=o.length)){var t=o[e];p(),window.location.href=t.url}}function b(e){if(e.key===`Escape`){p(),e.preventDefault();return}if(e.key===`ArrowDown`){e.preventDefault(),a=Math.min(a+1,o.length-1),g();return}if(e.key===`ArrowUp`){e.preventDefault(),a=Math.max(a-1,0),g();return}if(e.key===`Enter`){e.preventDefault(),a>=0&&y(a);return}}t.addEventListener(`input`,function(){m(t.value)}),n.addEventListener(`click`,function(e){var t=e.target.closest(`.search-result`);t&&(e.preventDefault(),y(parseInt(t.getAttribute(`data-index`),10)))}),r&&r.addEventListener(`click`,f);var x=document.getElementById(`search-open-mobile`);x&&x.addEventListener(`click`,f),document.addEventListener(`keydown`,function(e){(e.ctrlKey||e.metaKey)&&e.key===`k`&&(e.preventDefault(),f()),e.key===`/`&&!S(e.target)&&(e.preventDefault(),f())}),e.addEventListener(`click`,function(t){t.target===e&&p()});function S(e){var t=e.tagName;return t===`INPUT`||t===`TEXTAREA`||t===`SELECT`||e.isContentEditable}}`requestIdleCallback`in window?window.requestIdleCallback(e,{timeout:250}):window.addEventListener(`load`,e,{once:!0})})()})(); \ No newline at end of file diff --git a/docs/sourcey-catalog/sourcey.config.ts b/docs/sourcey-catalog/sourcey.config.ts new file mode 100644 index 000000000..e42b12348 --- /dev/null +++ b/docs/sourcey-catalog/sourcey.config.ts @@ -0,0 +1,46 @@ +export default { + name: "Runx Governed Skill Catalog", + siteUrl: "https://github.com", + baseUrl: "/runxhq/runx", + repo: "https://github.com/runxhq/runx", + editBranch: "main", + editBasePath: "docs/sourcey-catalog", + theme: { + preset: "default", + colors: { primary: "#0f766e", light: "#14b8a6", dark: "#134e4a" }, + }, + navigation: { + tabs: [ + { + tab: "Skills", + slug: "", + groups: [ + { + group: "Introduction", + pages: ["pages/introduction"], + }, + { + group: "Operate", + pages: ["pages/agency", "pages/business-ops", "pages/operator-inbox", "pages/ops-desk", "pages/work-plan"], + }, + { + group: "Research and data", + pages: ["pages/deep-research", "pages/research", "pages/data-store", "pages/knowledge-router", "pages/web-fetch"], + }, + { + group: "GitHub and delivery", + pages: ["pages/github-sync", "pages/issue-intake", "pages/issue-triage", "pages/issue-to-pr", "pages/release"], + }, + { + group: "Safety and review", + pages: ["pages/audit-receipt", "pages/cve-audit", "pages/least-privilege", "pages/policy-author", "pages/review-receipt", "pages/sandbox-harden"], + }, + { + group: "Outbound and tooling", + pages: ["pages/governed-outbound", "pages/run-history", "pages/sourcey"], + }, + ], + }, + ], + }, +}; diff --git a/docs/sourcey-catalog/verification.json b/docs/sourcey-catalog/verification.json new file mode 100644 index 000000000..a39fdb702 --- /dev/null +++ b/docs/sourcey-catalog/verification.json @@ -0,0 +1,295 @@ +{ + "ok": true, + "source_repository": "https://github.com/runxhq/runx", + "source_commit": "5afc25a83edf1c1320df7ac0d78c36f1523b5677", + "coverage": { + "manifest_pages": 24, + "markdown_pages": 24, + "html_pages": 24, + "navigation_pages": 24, + "search_pages": 24, + "source_links": 24, + "introduction_pages": 1 + }, + "artifacts": { + "html_files": 26, + "search_records": 202, + "llms_txt_bytes": 4747, + "llms_full_txt_bytes": 157034 + }, + "page_checks": [ + { + "slug": "agency", + "source_path": "skills/agency/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/agency/SKILL.md", + "markdown_bytes": 6042, + "html_bytes": 36772, + "navigation": true, + "search": true + }, + { + "slug": "business-ops", + "source_path": "skills/business-ops/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/business-ops/SKILL.md", + "markdown_bytes": 8446, + "html_bytes": 44522, + "navigation": true, + "search": true + }, + { + "slug": "operator-inbox", + "source_path": "skills/operator-inbox/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/operator-inbox/SKILL.md", + "markdown_bytes": 5515, + "html_bytes": 31314, + "navigation": true, + "search": true + }, + { + "slug": "ops-desk", + "source_path": "skills/ops-desk/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/ops-desk/SKILL.md", + "markdown_bytes": 14769, + "html_bytes": 59460, + "navigation": true, + "search": true + }, + { + "slug": "work-plan", + "source_path": "skills/work-plan/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/work-plan/SKILL.md", + "markdown_bytes": 4825, + "html_bytes": 28495, + "navigation": true, + "search": true + }, + { + "slug": "deep-research", + "source_path": "skills/deep-research/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/deep-research/SKILL.md", + "markdown_bytes": 1896, + "html_bytes": 25064, + "navigation": true, + "search": true + }, + { + "slug": "research", + "source_path": "skills/research/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/research/SKILL.md", + "markdown_bytes": 2209, + "html_bytes": 25754, + "navigation": true, + "search": true + }, + { + "slug": "data-store", + "source_path": "skills/data-store/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/data-store/SKILL.md", + "markdown_bytes": 12592, + "html_bytes": 60569, + "navigation": true, + "search": true + }, + { + "slug": "knowledge-router", + "source_path": "skills/knowledge-router/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/knowledge-router/SKILL.md", + "markdown_bytes": 1448, + "html_bytes": 24711, + "navigation": true, + "search": true + }, + { + "slug": "web-fetch", + "source_path": "skills/web-fetch/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/web-fetch/SKILL.md", + "markdown_bytes": 6690, + "html_bytes": 37606, + "navigation": true, + "search": true + }, + { + "slug": "github-sync", + "source_path": "skills/github-sync/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/github-sync/SKILL.md", + "markdown_bytes": 7831, + "html_bytes": 39926, + "navigation": true, + "search": true + }, + { + "slug": "issue-intake", + "source_path": "skills/issue-intake/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/issue-intake/SKILL.md", + "markdown_bytes": 8284, + "html_bytes": 33882, + "navigation": true, + "search": true + }, + { + "slug": "issue-triage", + "source_path": "skills/issue-triage/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/issue-triage/SKILL.md", + "markdown_bytes": 2224, + "html_bytes": 25447, + "navigation": true, + "search": true + }, + { + "slug": "issue-to-pr", + "source_path": "skills/issue-to-pr/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/issue-to-pr/SKILL.md", + "markdown_bytes": 9973, + "html_bytes": 35834, + "navigation": true, + "search": true + }, + { + "slug": "release", + "source_path": "skills/release/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/release/SKILL.md", + "markdown_bytes": 6106, + "html_bytes": 33531, + "navigation": true, + "search": true + }, + { + "slug": "audit-receipt", + "source_path": "skills/audit-receipt/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/audit-receipt/SKILL.md", + "markdown_bytes": 6197, + "html_bytes": 36880, + "navigation": true, + "search": true + }, + { + "slug": "cve-audit", + "source_path": "skills/cve-audit/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/cve-audit/SKILL.md", + "markdown_bytes": 5110, + "html_bytes": 37383, + "navigation": true, + "search": true + }, + { + "slug": "least-privilege", + "source_path": "skills/least-privilege/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/least-privilege/SKILL.md", + "markdown_bytes": 8327, + "html_bytes": 50473, + "navigation": true, + "search": true + }, + { + "slug": "policy-author", + "source_path": "skills/policy-author/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/policy-author/SKILL.md", + "markdown_bytes": 7262, + "html_bytes": 42097, + "navigation": true, + "search": true + }, + { + "slug": "review-receipt", + "source_path": "skills/review-receipt/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/review-receipt/SKILL.md", + "markdown_bytes": 3634, + "html_bytes": 27682, + "navigation": true, + "search": true + }, + { + "slug": "sandbox-harden", + "source_path": "skills/sandbox-harden/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/sandbox-harden/SKILL.md", + "markdown_bytes": 9160, + "html_bytes": 40169, + "navigation": true, + "search": true + }, + { + "slug": "governed-outbound", + "source_path": "skills/governed-outbound/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/governed-outbound/SKILL.md", + "markdown_bytes": 5407, + "html_bytes": 31413, + "navigation": true, + "search": true + }, + { + "slug": "run-history", + "source_path": "skills/run-history/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/run-history/SKILL.md", + "markdown_bytes": 4568, + "html_bytes": 35467, + "navigation": true, + "search": true + }, + { + "slug": "sourcey", + "source_path": "skills/sourcey/SKILL.md", + "source_url": "https://github.com/runxhq/runx/blob/5afc25a83edf1c1320df7ac0d78c36f1523b5677/skills/sourcey/SKILL.md", + "markdown_bytes": 15179, + "html_bytes": 71668, + "navigation": true, + "search": true + } + ], + "broken_links": [], + "credential_findings": [], + "gaps": [ + { + "id": "missing-worked-examples", + "title": "Selected skills lack a worked example section", + "affected_paths": [ + "skills/work-plan/SKILL.md", + "skills/deep-research/SKILL.md", + "skills/research/SKILL.md", + "skills/knowledge-router/SKILL.md", + "skills/issue-intake/SKILL.md", + "skills/issue-triage/SKILL.md", + "skills/issue-to-pr/SKILL.md", + "skills/release/SKILL.md", + "skills/review-receipt/SKILL.md", + "skills/governed-outbound/SKILL.md" + ], + "measured_fact": "10 of 24 selected skills do not contain the measured heading.", + "impact": "Operators cannot validate the expected input-to-output flow from the reference page alone." + }, + { + "id": "missing-edge-case-guidance", + "title": "Selected skills lack a dedicated edge-case or stop-condition section", + "affected_paths": [ + "skills/work-plan/SKILL.md", + "skills/deep-research/SKILL.md", + "skills/research/SKILL.md", + "skills/knowledge-router/SKILL.md", + "skills/issue-intake/SKILL.md", + "skills/issue-triage/SKILL.md", + "skills/issue-to-pr/SKILL.md", + "skills/release/SKILL.md", + "skills/review-receipt/SKILL.md", + "skills/run-history/SKILL.md" + ], + "measured_fact": "10 of 24 selected skills do not contain the measured heading.", + "impact": "Operators have no single place to check refusal, escalation, and terminal behavior." + }, + { + "id": "missing-non-use-guidance", + "title": "Selected skills lack a dedicated when-not-to-use section", + "affected_paths": [ + "skills/work-plan/SKILL.md", + "skills/deep-research/SKILL.md", + "skills/research/SKILL.md", + "skills/knowledge-router/SKILL.md", + "skills/issue-intake/SKILL.md", + "skills/issue-triage/SKILL.md", + "skills/issue-to-pr/SKILL.md", + "skills/release/SKILL.md", + "skills/review-receipt/SKILL.md" + ], + "measured_fact": "9 of 24 selected skills do not contain the measured heading.", + "impact": "Operators must infer when a different skill or workflow is the safer choice." + } + ] +} diff --git a/docs/sourcey-catalog/verify.mjs b/docs/sourcey-catalog/verify.mjs new file mode 100644 index 000000000..24912b7b5 --- /dev/null +++ b/docs/sourcey-catalog/verify.mjs @@ -0,0 +1,422 @@ +import { lstat, readFile, readdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const EXPECTED_PAGE_COUNT = 24; +const BASE_PATH = "/runxhq/runx/"; +const SOURCE_COMMIT = "5afc25a83edf1c1320df7ac0d78c36f1523b5677"; +const CREDENTIAL_MARKERS = [ + "ghp_", + "github_pat_", + "Bearer ", + "-----BEGIN PRIVATE KEY-----", + "-----BEGIN RSA PRIVATE KEY-----", + "-----BEGIN EC PRIVATE KEY-----", + "-----BEGIN OPENSSH PRIVATE KEY-----", +]; + +export async function verifyCatalog({ catalogDir } = {}) { + const root = path.resolve(catalogDir ?? path.dirname(fileURLToPath(import.meta.url))); + const pagesDir = path.join(root, "pages"); + const siteDir = path.join(root, "site"); + const catalog = JSON.parse(await readRequiredFile(path.join(root, "catalog.json"), "catalog")); + const pages = flattenCatalog(catalog); + const markdownNames = (await readdir(pagesDir)) + .filter((name) => name.endsWith(".md") && name !== "introduction.md") + .sort(); + const expectedNames = pages.map(({ slug }) => `${slug}.md`).sort(); + if (JSON.stringify(markdownNames) !== JSON.stringify(expectedNames)) { + throw new Error("missing generated page or unexpected generated page"); + } + + await readRequiredFile(path.join(pagesDir, "introduction.md"), "introduction page"); + const indexPath = path.join(siteDir, "index.html"); + const indexHtml = await readRequiredFile(indexPath, "site index"); + const indexTags = parseHtmlTags(indexHtml); + const navigationTargets = new Set(indexTags + .filter(({ name }) => name === "a") + .map(({ attributes }) => normalizeSiteTarget(attributes.href)) + .filter(Boolean)); + const searchRecords = JSON.parse(await readRequiredFile( + path.join(siteDir, "search-index.json"), + "search index", + )); + if (!Array.isArray(searchRecords)) throw new Error("search index must be an array"); + const searchTargets = new Set(searchRecords + .filter((record) => record?.category === "Pages" && typeof record.url === "string") + .map((record) => normalizeSiteTarget(record.url)) + .filter(Boolean)); + + const pageChecks = []; + const markdownBySlug = new Map(); + for (const page of pages) { + const markdownPath = path.join(pagesDir, `${page.slug}.md`); + const markdown = await readRequiredFile(markdownPath, `generated page ${page.slug}`); + const sourceUrl = `${catalog.source_repository}/blob/${catalog.source_commit}/${page.path}`; + if (!markdown.includes(sourceUrl) || !markdown.includes(`Commit: \`${catalog.source_commit}\``)) { + throw new Error(`immutable source link missing for ${page.slug}`); + } + markdownBySlug.set(page.slug, markdown); + + const htmlPath = path.join(siteDir, "pages", `${page.slug}.html`); + const html = await readRequiredFile(htmlPath, `HTML page ${page.slug}`); + const htmlLinks = parseHtmlTags(html) + .filter(({ name }) => name === "a") + .map(({ attributes }) => attributes.href); + if (!htmlLinks.includes(sourceUrl)) { + throw new Error(`immutable source link missing from HTML page ${page.slug}`); + } + const siteTarget = `pages/${page.slug}.html`; + const navigation = navigationTargets.has(siteTarget); + const search = searchTargets.has(siteTarget); + pageChecks.push({ + slug: page.slug, + source_path: page.path, + source_url: sourceUrl, + markdown_bytes: Buffer.byteLength(markdown), + html_bytes: Buffer.byteLength(html), + navigation, + search, + }); + } + + const navigationPages = pageChecks.filter(({ navigation }) => navigation).length; + if (navigationPages !== EXPECTED_PAGE_COUNT) { + throw new Error(`navigation coverage is ${navigationPages}/${EXPECTED_PAGE_COUNT}`); + } + const searchPages = pageChecks.filter(({ search }) => search).length; + if (searchPages !== EXPECTED_PAGE_COUNT) { + throw new Error(`search coverage is ${searchPages}/${EXPECTED_PAGE_COUNT}`); + } + + const llmsText = await readLlmsArtifact(siteDir, "llms.txt"); + const llmsFullText = await readLlmsArtifact(siteDir, "llms-full.txt"); + const htmlFiles = (await walkFiles(siteDir)).filter((file) => file.endsWith(".html")).sort(); + const brokenLinks = await findBrokenLocalLinks(siteDir, htmlFiles); + if (brokenLinks.length > 0) { + throw new Error(`broken local link: ${brokenLinks[0].source} -> ${brokenLinks[0].target}`); + } + + const credentialFindings = await findCredentialMarkers([ + ...(await walkFiles(pagesDir)), + ...(await walkFiles(siteDir)), + ]); + if (credentialFindings.length > 0) { + throw new Error(`credential-like string found in ${credentialFindings[0].path}`); + } + + const gaps = deriveGaps(pages, markdownBySlug); + return { + ok: true, + source_repository: catalog.source_repository, + source_commit: catalog.source_commit, + coverage: { + manifest_pages: pages.length, + markdown_pages: pageChecks.length, + html_pages: pageChecks.length, + navigation_pages: navigationPages, + search_pages: searchPages, + source_links: pageChecks.length, + introduction_pages: 1, + }, + artifacts: { + html_files: htmlFiles.length, + search_records: searchRecords.length, + llms_txt_bytes: Buffer.byteLength(llmsText), + llms_full_txt_bytes: Buffer.byteLength(llmsFullText), + }, + page_checks: pageChecks, + broken_links: brokenLinks, + credential_findings: credentialFindings, + gaps, + }; +} + +function flattenCatalog(catalog) { + if (!catalog || !Array.isArray(catalog.groups)) throw new Error("catalog groups must be an array"); + if (catalog.source_repository !== "https://github.com/runxhq/runx") { + throw new Error("catalog must use the pinned Runx source repository"); + } + if (catalog.source_commit !== SOURCE_COMMIT) { + throw new Error(`catalog must use the pinned source commit ${SOURCE_COMMIT}`); + } + const pages = catalog.groups.flatMap((group) => group.entries ?? []); + if (pages.length !== EXPECTED_PAGE_COUNT) { + throw new Error(`catalog must contain exactly ${EXPECTED_PAGE_COUNT} pages`); + } + const slugs = new Set(); + for (const page of pages) { + if (!page || typeof page.slug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(page.slug) + || page.path !== `skills/${page.name}/SKILL.md` || slugs.has(page.slug)) { + throw new Error("catalog contains an invalid or duplicate page"); + } + slugs.add(page.slug); + } + return pages; +} + +async function readRequiredFile(file, label) { + try { + const metadata = await lstat(file); + if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size === 0) throw new Error(); + return await readFile(file, "utf8"); + } catch { + throw new Error(`missing or empty ${label}`); + } +} + +async function readLlmsArtifact(siteDir, name) { + try { + return await readRequiredFile(path.join(siteDir, name), `llms artifact ${name}`); + } catch { + throw new Error(`missing or empty llms artifact: ${name}`); + } +} + +function parseHtmlTags(html) { + const tags = []; + let cursor = 0; + while (cursor < html.length) { + const start = html.indexOf("<", cursor); + if (start < 0) break; + let end = start + 1; + let quote = ""; + while (end < html.length) { + const character = html[end]; + if (quote) { + if (character === quote) quote = ""; + } else if (character === "\"" || character === "'") { + quote = character; + } else if (character === ">") { + break; + } + end += 1; + } + if (end >= html.length) throw new Error("malformed HTML tag"); + const token = html.slice(start + 1, end).trim(); + cursor = end + 1; + if (!token || token.startsWith("!") || token.startsWith("?") || token.startsWith("/")) continue; + tags.push(parseTagToken(token)); + } + return tags; +} + +function parseTagToken(token) { + let cursor = 0; + while (cursor < token.length && !/\s|\//.test(token[cursor])) cursor += 1; + const name = token.slice(0, cursor).toLowerCase(); + const attributes = {}; + while (cursor < token.length) { + while (cursor < token.length && /\s|\//.test(token[cursor])) cursor += 1; + const keyStart = cursor; + while (cursor < token.length && !/[\s=]/.test(token[cursor])) cursor += 1; + if (keyStart === cursor) break; + const key = token.slice(keyStart, cursor).toLowerCase(); + while (cursor < token.length && /\s/.test(token[cursor])) cursor += 1; + let value = ""; + if (token[cursor] === "=") { + cursor += 1; + while (cursor < token.length && /\s/.test(token[cursor])) cursor += 1; + if (token[cursor] === "\"" || token[cursor] === "'") { + const quote = token[cursor]; + cursor += 1; + const valueStart = cursor; + while (cursor < token.length && token[cursor] !== quote) cursor += 1; + value = token.slice(valueStart, cursor); + cursor += 1; + } else { + const valueStart = cursor; + while (cursor < token.length && !/\s/.test(token[cursor])) cursor += 1; + value = token.slice(valueStart, cursor); + } + } + attributes[key] = decodeHtmlEntities(value); + } + return { name, attributes }; +} + +function decodeHtmlEntities(value) { + return value + .replaceAll("&", "&") + .replaceAll(""", "\"") + .replaceAll("'", "'") + .replaceAll("<", "<") + .replaceAll(">", ">"); +} + +function normalizeSiteTarget(value) { + if (typeof value !== "string" || value.length === 0) return undefined; + const withoutFragment = value.split("#", 1)[0].split("?", 1)[0]; + if (withoutFragment.startsWith(BASE_PATH)) return withoutFragment.slice(BASE_PATH.length); + return withoutFragment.replace(/^\.\//, "").replace(/^\.\.\//, ""); +} + +async function walkFiles(root) { + const files = []; + async function visit(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name, "en"))) { + const target = path.join(directory, entry.name); + if (entry.isSymbolicLink()) throw new Error(`symlink is not allowed in generated output: ${target}`); + if (entry.isDirectory()) await visit(target); + else if (entry.isFile()) files.push(target); + } + } + await visit(root); + return files; +} + +async function findBrokenLocalLinks(siteDir, htmlFiles) { + const broken = []; + const anchorCache = new Map(); + for (const file of htmlFiles) { + const html = await readFile(file, "utf8"); + const tags = parseHtmlTags(html); + anchorCache.set(file, new Set(tags.map(({ attributes }) => attributes.id).filter(Boolean))); + for (const tag of tags) { + for (const attribute of ["href", "src"]) { + const value = tag.attributes[attribute]; + if (!value || isExternalTarget(value)) continue; + const [rawTarget, fragment] = value.split("#", 2); + let target; + if (rawTarget === "") { + target = file; + } else if (rawTarget.startsWith(BASE_PATH)) { + target = path.resolve(siteDir, decodeURIComponent(rawTarget.slice(BASE_PATH.length))); + } else if (rawTarget.startsWith("/")) { + broken.push(linkFinding(siteDir, file, value)); + continue; + } else { + target = path.resolve(path.dirname(file), decodeURIComponent(rawTarget.split("?", 1)[0])); + } + if (!isInside(siteDir, target) || !(await isRegularFile(target))) { + broken.push(linkFinding(siteDir, file, value)); + continue; + } + if (fragment) { + if (!anchorCache.has(target)) { + const targetHtml = await readFile(target, "utf8"); + anchorCache.set(target, new Set(parseHtmlTags(targetHtml) + .map(({ attributes }) => attributes.id) + .filter(Boolean))); + } + if (!anchorCache.get(target).has(decodeURIComponent(fragment))) { + broken.push(linkFinding(siteDir, file, value)); + } + } + } + } + } + return broken.sort((left, right) => `${left.source}\0${left.target}`.localeCompare( + `${right.source}\0${right.target}`, + "en", + )); +} + +function isExternalTarget(value) { + return value.startsWith("//") || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value); +} + +function isInside(root, target) { + const relative = path.relative(root, target); + return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative); +} + +async function isRegularFile(file) { + try { + const metadata = await lstat(file); + return metadata.isFile() && !metadata.isSymbolicLink(); + } catch { + return false; + } +} + +function linkFinding(siteDir, source, target) { + return { source: path.relative(siteDir, source), target }; +} + +async function findCredentialMarkers(files) { + const findings = []; + for (const file of files.sort()) { + const content = await readFile(file); + for (const marker of CREDENTIAL_MARKERS) { + if (content.includes(Buffer.from(marker))) { + findings.push({ path: file, marker }); + } + } + } + return findings; +} + +function deriveGaps(pages, markdownBySlug) { + const definitions = [ + { + id: "missing-worked-examples", + title: "Selected skills lack a worked example section", + pattern: /^## (?:Worked )?Example\b/im, + impact: "Operators cannot validate the expected input-to-output flow from the reference page alone.", + }, + { + id: "missing-output-contracts", + title: "Selected skills lack a dedicated output contract section", + pattern: /^## [^\n]*\bOutputs?\b/im, + impact: "Integrators must infer result shapes instead of comparing them against an explicit contract.", + }, + { + id: "missing-edge-case-guidance", + title: "Selected skills lack a dedicated edge-case or stop-condition section", + pattern: /^## .*?(?:Edge cases|Stop conditions)\b/im, + impact: "Operators have no single place to check refusal, escalation, and terminal behavior.", + }, + { + id: "missing-non-use-guidance", + title: "Selected skills lack a dedicated when-not-to-use section", + pattern: /^## When not to use\b/im, + impact: "Operators must infer when a different skill or workflow is the safer choice.", + }, + ]; + return definitions.map((definition) => { + const affected = pages + .filter(({ slug }) => !definition.pattern.test(markdownBySlug.get(slug))) + .map(({ path: sourcePath }) => sourcePath); + return { + id: definition.id, + title: definition.title, + affected_paths: affected, + measured_fact: `${affected.length} of ${pages.length} selected skills do not contain the measured heading.`, + impact: definition.impact, + }; + }).filter(({ affected_paths: affectedPaths }) => affectedPaths.length > 0); +} + +export function renderGapReport(packet) { + const lines = [ + "# Sourcey Catalog Documentation Gaps", + "", + `Measured from ${packet.coverage.markdown_pages} generated skill pages at commit \`${packet.source_commit}\`.`, + "", + ]; + for (const gap of packet.gaps) { + lines.push( + `## ${gap.title}`, + "", + `- Affected source paths: ${gap.affected_paths.map((sourcePath) => `\`${sourcePath}\``).join(", ") || "none"}`, + `- Measured fact: ${gap.measured_fact}`, + `- Why it matters: ${gap.impact}`, + "", + ); + } + return `${lines.join("\n").trimEnd()}\n`; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + verifyCatalog().then(async (packet) => { + const root = path.dirname(fileURLToPath(import.meta.url)); + await writeFile(path.join(root, "verification.json"), `${JSON.stringify(packet, null, 2)}\n`, "utf8"); + await writeFile(path.join(root, "gaps.md"), renderGapReport(packet), "utf8"); + process.stdout.write(`${JSON.stringify({ ok: packet.ok, coverage: packet.coverage })}\n`); + }).catch((error) => { + process.stderr.write(`${error.message}\n`); + process.exitCode = 1; + }); +} diff --git a/docs/sourcey-catalog/verify.test.mjs b/docs/sourcey-catalog/verify.test.mjs new file mode 100644 index 000000000..2b8d39ac4 --- /dev/null +++ b/docs/sourcey-catalog/verify.test.mjs @@ -0,0 +1,178 @@ +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { verifyCatalog } from "./verify.mjs"; + +const sourceCommit = "5afc25a83edf1c1320df7ac0d78c36f1523b5677"; +const slugs = Array.from({ length: 24 }, (_, index) => `skill-${index + 1}`); + +async function createFixture(t, mutate = async () => {}) { + const catalogDir = await mkdtemp(path.join(tmpdir(), "sourcey-verify-")); + t.after(() => rm(catalogDir, { recursive: true, force: true })); + await mkdir(path.join(catalogDir, "pages")); + await mkdir(path.join(catalogDir, "site", "pages"), { recursive: true }); + + const entries = slugs.map((slug) => ({ + name: slug, + slug, + group: "Fixture", + path: `skills/${slug}/SKILL.md`, + })); + const catalog = { + source_repository: "https://github.com/runxhq/runx", + source_commit: sourceCommit, + groups: [{ name: "Fixture", entries }], + }; + await writeFile(path.join(catalogDir, "catalog.json"), `${JSON.stringify(catalog, null, 2)}\n`); + await writeFile(path.join(catalogDir, "pages", "introduction.md"), "# Introduction\n"); + + const navigation = slugs + .map((slug) => `${slug}`) + .join(""); + await writeFile(path.join(catalogDir, "site", "index.html"), html(navigation)); + for (const entry of entries) { + const sourceUrl = `${catalog.source_repository}/blob/${sourceCommit}/${entry.path}`; + const markdown = [ + `# ${entry.slug}`, + "", + `- Source: [${entry.path}](${sourceUrl})`, + `- Commit: \`${sourceCommit}\``, + "", + "Maintainer documentation.", + "", + ].join("\n"); + await writeFile(path.join(catalogDir, "pages", `${entry.slug}.md`), markdown); + await writeFile( + path.join(catalogDir, "site", "pages", `${entry.slug}.html`), + pageHtml(`SourceDetails

Details

`), + ); + } + + const search = [ + { title: "Introduction", url: "/runxhq/runx/pages/introduction.html", category: "Pages" }, + ...slugs.map((slug) => ({ + title: slug, + url: `/runxhq/runx/pages/${slug}.html`, + category: "Pages", + })), + ]; + await writeFile(path.join(catalogDir, "site", "search-index.json"), `${JSON.stringify(search)}\n`); + await writeFile(path.join(catalogDir, "site", "llms.txt"), "catalog\n"); + await writeFile(path.join(catalogDir, "site", "llms-full.txt"), "full catalog\n"); + await writeFile(path.join(catalogDir, "site", "sourcey.css"), "body {}\n"); + await writeFile(path.join(catalogDir, "site", "sourcey.js"), "export {};\n"); + await writeFile(path.join(catalogDir, "site", "pages", "introduction.html"), pageHtml("Introduction")); + + await mutate(catalogDir); + return { catalogDir }; +} + +function html(body) { + return `${body}`; +} + +function pageHtml(body) { + return `${body}`; +} + +test("rejects a missing page, absent source link, and broken local href", async (t) => { + const missingPageFixture = await createFixture(t, (catalogDir) => + rm(path.join(catalogDir, "pages", `${slugs[0]}.md`)) + ); + const noSourceFixture = await createFixture(t, async (catalogDir) => { + const pagePath = path.join(catalogDir, "pages", `${slugs[0]}.md`); + const page = await readFile(pagePath, "utf8"); + await writeFile(pagePath, page.replace("/blob/", "/tree/")); + }); + const brokenHrefFixture = await createFixture(t, async (catalogDir) => { + const pagePath = path.join(catalogDir, "site", "pages", `${slugs[0]}.html`); + const page = await readFile(pagePath, "utf8"); + await writeFile(pagePath, page.replace("", 'Missing')); + }); + + await assert.rejects(verifyCatalog(missingPageFixture), /missing generated page/); + await assert.rejects(verifyCatalog(noSourceFixture), /immutable source link/); + await assert.rejects(verifyCatalog(brokenHrefFixture), /broken local link/); +}); + +test("rejects an unpinned commit, broken fragment, and broken src", async (t) => { + const wrongCommit = await createFixture(t, async (catalogDir) => { + const catalogPath = path.join(catalogDir, "catalog.json"); + const catalog = JSON.parse(await readFile(catalogPath, "utf8")); + catalog.source_commit = "a".repeat(40); + await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`); + }); + const brokenFragment = await createFixture(t, async (catalogDir) => { + const pagePath = path.join(catalogDir, "site", "pages", `${slugs[0]}.html`); + const page = await readFile(pagePath, "utf8"); + await writeFile(pagePath, page.replace("#details", "#missing-anchor")); + }); + const brokenSrc = await createFixture(t, async (catalogDir) => { + const pagePath = path.join(catalogDir, "site", "pages", `${slugs[0]}.html`); + const page = await readFile(pagePath, "utf8"); + await writeFile(pagePath, page.replace("", '')); + }); + + await assert.rejects(verifyCatalog(wrongCommit), /pinned source commit/); + await assert.rejects(verifyCatalog(brokenFragment), /broken local link/); + await assert.rejects(verifyCatalog(brokenSrc), /broken local link/); +}); + +test("requires all 24 pages in navigation and search", async (t) => { + const validFixture = await createFixture(t); + const result = await verifyCatalog(validFixture); + + assert.equal(result.coverage.markdown_pages, 24); + assert.equal(result.coverage.html_pages, 24); + assert.equal(result.coverage.navigation_pages, 24); + assert.equal(result.coverage.search_pages, 24); + assert.deepEqual(result.broken_links, []); +}); + +test("rejects missing llms artifacts, credential-like content, and missing coverage", async (t) => { + const missingLlms = await createFixture(t, (catalogDir) => + writeFile(path.join(catalogDir, "site", "llms.txt"), "") + ); + const credentialLeak = await createFixture(t, (catalogDir) => + writeFile(path.join(catalogDir, "site", "sourcey.js"), "const token = 'ghp_example';\n") + ); + const missingNavigation = await createFixture(t, async (catalogDir) => { + const indexPath = path.join(catalogDir, "site", "index.html"); + const index = await readFile(indexPath, "utf8"); + await writeFile(indexPath, index.replace(`${slugs[0]}`, "")); + }); + const missingSearch = await createFixture(t, async (catalogDir) => { + const searchPath = path.join(catalogDir, "site", "search-index.json"); + const search = JSON.parse(await readFile(searchPath, "utf8")); + await writeFile(searchPath, `${JSON.stringify(search.filter(({ title }) => title !== slugs[0]))}\n`); + }); + + await assert.rejects(verifyCatalog(missingLlms), /missing or empty llms artifact/); + await assert.rejects(verifyCatalog(credentialLeak), /credential-like string/); + await assert.rejects(verifyCatalog(missingNavigation), /navigation coverage/); + await assert.rejects(verifyCatalog(missingSearch), /search coverage/); +}); + +test("returns the same packet for unchanged inputs", async (t) => { + const fixture = await createFixture(t); + + assert.deepEqual(await verifyCatalog(fixture), await verifyCatalog(fixture)); +}); + +test("recognizes structured output headings and emits only grounded gaps", async (t) => { + const fixture = await createFixture(t, async (catalogDir) => { + for (const slug of slugs) { + const pagePath = path.join(catalogDir, "pages", `${slug}.md`); + const page = await readFile(pagePath, "utf8"); + await writeFile(pagePath, `${page}\n## Structured Output\n\nDocumented fields.\n`); + } + }); + + const result = await verifyCatalog(fixture); + + assert.equal(result.gaps.some(({ id }) => id === "missing-output-contracts"), false); + assert.equal(result.gaps.every(({ affected_paths: affectedPaths }) => affectedPaths.length > 0), true); +});