From 537693fe1c0dd9f273672229d7af80a2f97151a8 Mon Sep 17 00:00:00 2001
From: Justin Levine <20596508+jal-co@users.noreply.github.com>
Date: Wed, 1 Jul 2026 16:42:20 -0500
Subject: [PATCH 1/4] feat(cli): add init, uninstall, dev watch, and npm
install source
Adds four CLI features from improvement plans 002/005/006/013:
- ap-sdk init scaffolds a working plugin.ts + .gitignore with dry-run
and overwrite protection
- ap-sdk install records an install manifest; ap-sdk uninstall reverses
only recorded files/entries and leaves foreign config intact
- ap-sdk dev watches the plugin dir, rebuilds on change, survives
broken plugins, and optionally reinstalls with --install
- npm:[@version] as a remote source for install/check, with
the same compatibility gate as GitHub sources
Docs: quickstart now leads with npx ap-sdk init; installing-plugins
gains a From npm section.
---
.tegami/2026-07-01-dev-watch.md | 10 +
.tegami/2026-07-01-init-command.md | 10 +
.tegami/2026-07-01-npm-source.md | 12 +
.tegami/2026-07-01-uninstall.md | 11 +
apps/docs/content/docs/installing-plugins.mdx | 47 ++++
apps/docs/content/docs/quickstart.mdx | 14 +-
packages/agent-plugin-sdk/src/cli.ts | 207 ++++++++++++++----
packages/agent-plugin-sdk/src/dev.ts | 200 +++++++++++++++++
packages/agent-plugin-sdk/src/index.ts | 2 +
packages/agent-plugin-sdk/src/install.ts | 70 +++++-
packages/agent-plugin-sdk/src/npm.ts | 178 +++++++++++++++
.../agent-plugin-sdk/src/scaffold-plugin.ts | 52 +++++
packages/agent-plugin-sdk/src/uninstall.ts | 195 +++++++++++++++++
packages/agent-plugin-sdk/test/dev.test.ts | 113 ++++++++++
packages/agent-plugin-sdk/test/init.test.ts | 47 ++++
packages/agent-plugin-sdk/test/npm.test.ts | 105 +++++++++
.../agent-plugin-sdk/test/uninstall.test.ts | 153 +++++++++++++
17 files changed, 1379 insertions(+), 47 deletions(-)
create mode 100644 .tegami/2026-07-01-dev-watch.md
create mode 100644 .tegami/2026-07-01-init-command.md
create mode 100644 .tegami/2026-07-01-npm-source.md
create mode 100644 .tegami/2026-07-01-uninstall.md
create mode 100644 packages/agent-plugin-sdk/src/dev.ts
create mode 100644 packages/agent-plugin-sdk/src/npm.ts
create mode 100644 packages/agent-plugin-sdk/src/scaffold-plugin.ts
create mode 100644 packages/agent-plugin-sdk/src/uninstall.ts
create mode 100644 packages/agent-plugin-sdk/test/dev.test.ts
create mode 100644 packages/agent-plugin-sdk/test/init.test.ts
create mode 100644 packages/agent-plugin-sdk/test/npm.test.ts
create mode 100644 packages/agent-plugin-sdk/test/uninstall.test.ts
diff --git a/.tegami/2026-07-01-dev-watch.md b/.tegami/2026-07-01-dev-watch.md
new file mode 100644
index 0000000..8f0027c
--- /dev/null
+++ b/.tegami/2026-07-01-dev-watch.md
@@ -0,0 +1,10 @@
+---
+packages:
+ "@jalco/ap-sdk": minor
+---
+
+## Add `ap-sdk dev`
+
+Watch mode for plugin authors: `ap-sdk dev` rebuilds on every change to the
+plugin and its referenced files, and `--install` drops the result straight
+into your local harness dirs. Errors keep the watcher alive.
diff --git a/.tegami/2026-07-01-init-command.md b/.tegami/2026-07-01-init-command.md
new file mode 100644
index 0000000..6f5c9b0
--- /dev/null
+++ b/.tegami/2026-07-01-init-command.md
@@ -0,0 +1,10 @@
+---
+packages:
+ "@jalco/ap-sdk": minor
+---
+
+## Add `ap-sdk init`
+
+Scaffold a new plugin project with one command. `ap-sdk init my-plugin` writes
+a working `plugin.ts` (a skill, a command, and instructions) that passes
+`ap-sdk check` as-is, plus a `.gitignore` entry for `.aps-out/`.
diff --git a/.tegami/2026-07-01-npm-source.md b/.tegami/2026-07-01-npm-source.md
new file mode 100644
index 0000000..ce54304
--- /dev/null
+++ b/.tegami/2026-07-01-npm-source.md
@@ -0,0 +1,12 @@
+---
+packages:
+ "@jalco/ap-sdk": minor
+---
+
+## Install plugins from npm
+
+`ap-sdk install npm:` (and `check`/`build` with the same spec)
+fetches a published package from the npm registry, verifies it's a
+compatible plugin, and installs it — with `npm:@` for
+pinning. Packages can point at a non-root plugin file via an
+`ap-sdk.plugin` field in package.json.
diff --git a/.tegami/2026-07-01-uninstall.md b/.tegami/2026-07-01-uninstall.md
new file mode 100644
index 0000000..cbd6023
--- /dev/null
+++ b/.tegami/2026-07-01-uninstall.md
@@ -0,0 +1,11 @@
+---
+packages:
+ "@jalco/ap-sdk": minor
+---
+
+## Add `ap-sdk uninstall`
+
+`install` now records what it wrote to an install manifest
+(`.ap-sdk/install-manifest.json`), and `ap-sdk uninstall ` cleanly
+reverses it — deleting the plugin's files and removing only its entries from
+merged configs (instruction blocks, MCP servers, hooks).
diff --git a/apps/docs/content/docs/installing-plugins.mdx b/apps/docs/content/docs/installing-plugins.mdx
index 3f1f405..0180451 100644
--- a/apps/docs/content/docs/installing-plugins.mdx
+++ b/apps/docs/content/docs/installing-plugins.mdx
@@ -12,6 +12,8 @@ the skills, commands, and tools immediately.
npx ap-sdk install
```
+Looking for plugins? Browse the [plugin directory](/docs/plugins).
+
Install reuses the exact same output as `build`, so installed files are
byte-for-byte identical — it just relocates each feature into the harness's live
config dir instead of an output folder.
@@ -105,6 +107,51 @@ Use `--path ` when the plugin lives in a subdirectory, and set
`@jalco/ap-sdk` import is resolved to the SDK running the install, so simple
plugins need no dependency install of their own.
+## From npm
+
+Use the `npm:` source form to fetch a published package, run the same
+compatibility check, and install only if it default-exports a valid ap-sdk
+plugin:
+
+```bash compact
+npx ap-sdk install npm:@acme/git-helper
+npx ap-sdk install npm:@acme/git-helper@1.2.3 -t claude
+npx ap-sdk check npm:@acme/git-helper@next
+```
+
+Version pinning uses the suffix after the package name (`npm:name@1.2.3` or a
+dist-tag like `npm:name@next`). Scoped names keep their scope:
+`npm:@scope/name@1.2.3`.
+
+Packages can point at a non-root plugin file with package metadata:
+
+```json title="package.json"
+{
+ "ap-sdk": { "plugin": "./dist/plugin.js" }
+}
+```
+
+If that field is absent, the CLI looks for the usual `plugin.ts`, `plugin.js`,
+or `ap-sdk.config.ts`; use `--path ` for packages that contain the plugin
+inside a subdirectory.
+
+## Uninstalling
+
+Every non-dry-run install records exactly what was written in
+`.ap-sdk/install-manifest.json` for project installs, or
+`~/.ap-sdk/install-manifest.json` for global installs. Remove a recorded plugin
+with:
+
+```bash compact
+npx ap-sdk uninstall git-helper
+npx ap-sdk uninstall git-helper --dry-run
+npx ap-sdk uninstall git-helper --global
+```
+
+Uninstall deletes only recorded files and removes only the plugin's entries from
+shared configs (instruction blocks, MCP servers, hooks). Installs made before
+this manifest existed have no record, so they cannot be auto-removed.
+
## Programmatic install
Everything the CLI does is available as a function, so you can install from your
diff --git a/apps/docs/content/docs/quickstart.mdx b/apps/docs/content/docs/quickstart.mdx
index 96c5ad9..4c46d39 100644
--- a/apps/docs/content/docs/quickstart.mdx
+++ b/apps/docs/content/docs/quickstart.mdx
@@ -5,7 +5,13 @@ description: Define a plugin and compile it to every harness in a few minutes.
## 1. Define a plugin
-Create `plugin.ts` and default-export the result of `definePlugin`:
+Start with a working scaffold:
+
+```bash
+npx ap-sdk init my-plugin
+```
+
+That writes a `plugin.ts` you can edit. Or create `plugin.ts` yourself and default-export the result of `definePlugin`:
```ts title="plugin.ts"
import {
@@ -76,6 +82,12 @@ npx ap-sdk build -t claude,gemini,cursor
npx ap-sdk install -t claude
```
+For an edit/build/install loop while authoring, run watch mode:
+
+```bash
+npx ap-sdk dev --install -t claude
+```
+
Add `--global` to install into your home-directory harness dirs, or `--dry-run`
to preview without writing. See [Installing a plugin](/docs/installing-plugins)
for the full install model.
diff --git a/packages/agent-plugin-sdk/src/cli.ts b/packages/agent-plugin-sdk/src/cli.ts
index 4e1381b..9f45d46 100644
--- a/packages/agent-plugin-sdk/src/cli.ts
+++ b/packages/agent-plugin-sdk/src/cli.ts
@@ -1,16 +1,18 @@
#!/usr/bin/env node
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
-import { dirname, isAbsolute, join, resolve } from "node:path";
+import { dirname, isAbsolute, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import type { HarnessId, Plugin } from "./types.js";
import { allHarnessIds } from "./harnesses/index.js";
import { build } from "./build.js";
import { installSkills } from "./install.js";
+import { uninstallPlugin } from "./uninstall.js";
+import { startDev, writeBuildOutputs } from "./dev.js";
import { loadPluginTools } from "./load-tools.js";
import { listTools, callTool, contentToText } from "./runtime/tool.js";
import { formatWarning } from "./warnings.js";
-import { writeTree } from "./util/fs.js";
import { fetchGithubPlugin, isGithubSpec } from "./github.js";
+import { fetchNpmPlugin, isNpmSpec } from "./npm.js";
import { portPlugin } from "./port.js";
import { DEFAULT_PLUGIN_FILES, locatePlugin } from "./plugin-files.js";
import { PluginValidationError, validatePlugin } from "./validate.js";
@@ -19,47 +21,56 @@ import {
deriveDisplayName,
validateHarnessId,
} from "./scaffold.js";
+import { gitignoreSnippet, pluginTemplate, validatePluginId } from "./scaffold-plugin.js";
const HELP = `ap-sdk — write a plugin once, ship it to every agent harness
Usage:
- ap-sdk build [plugin] [options] Compile to native artifacts under an output dir
- ap-sdk install [plugin] [options] Install skills into local harness dirs
- ap-sdk check [plugin] Validate a plugin definition
- ap-sdk tools [plugin] [options] List the plugin's tools, or invoke one locally
- ap-sdk add-harness [options] Scaffold a new target harness module
- ap-sdk port [dir] [options] Generate a portable plugin.ts from an existing plugin
+ ap-sdk init [id] [options] Scaffold a new plugin.ts in the current directory
+ ap-sdk build [plugin] [options] Compile to native artifacts under an output dir
+ ap-sdk install [plugin] [options] Install skills into local harness dirs
+ ap-sdk uninstall [options] Remove a previously installed plugin
+ ap-sdk dev [plugin] [options] Watch the plugin and rebuild on change
+ ap-sdk check [plugin] Validate a plugin definition
+ ap-sdk tools [plugin] [options] List the plugin's tools, or invoke one locally
+ ap-sdk add-harness [options] Scaffold a new target harness module
+ ap-sdk port [dir] [options] Generate a portable plugin.ts from an existing plugin
Arguments:
plugin A local plugin module (default: ./plugin.ts, ./plugin.js,
- ./ap-sdk.config.ts) or a GitHub source — owner/repo,
- github:owner/repo, or a github.com URL. Must default-export a
- definePlugin(...) result. Remote sources are fetched and checked for
- compatibility before anything is installed.
- id (add-harness) kebab-case id for the new harness, e.g. gemini, cursor.
+ ./ap-sdk.config.ts), a GitHub source — owner/repo,
+ github:owner/repo, or a github.com URL — or npm:[@version].
+ Must default-export a definePlugin(...) result. Remote sources are
+ fetched and checked for compatibility before anything is installed.
+ id init: kebab-case plugin id; add-harness: kebab-case harness id.
Options:
--target, -t Comma-separated harnesses: ${allHarnessIds().join(", ")} (default: all)
--out, -o build: output dir (default: ./.aps-out)
add-harness: dir to write the harness file (default: .)
--name add-harness: human-readable display name (default: derived from id)
- --global, -g install: write to ~ global harness dirs instead of project
- --dry-run install/add-harness: print what would be written without writing
+ --global, -g install/uninstall/dev --install: write to ~ dirs instead of project
+ --install dev: reinstall after each successful rebuild
+ --dry-run init/install/uninstall/add-harness: print without writing
--call tools: invoke this tool instead of listing
--args tools: JSON arguments for --call (default: {})
- --path GitHub source: path to the plugin within the repo
+ --path GitHub/npm source: path to the plugin within the repo/package
--ref [ GitHub source: branch, tag, or commit (or owner/repo#ref)
--help, -h Show this help
Examples:
+ ap-sdk init my-plugin
ap-sdk build
ap-sdk build ./my-plugin.ts -t claude,codex -o dist
ap-sdk install -t opencode
+ ap-sdk uninstall git-helper
+ ap-sdk dev --install -t claude
ap-sdk tools
ap-sdk tools --call run_tests --args '{"pattern":"sum"}'
ap-sdk add-harness gemini --name "Gemini CLI"
ap-sdk install owner/repo
ap-sdk install github:owner/repo#main -t claude
+ ap-sdk install npm:@acme/git-helper
ap-sdk check https://github.com/owner/repo --path examples/git-helper
ap-sdk port ./my-claude-plugin
ap-sdk port ./my-plugin --dry-run
@@ -77,6 +88,7 @@ interface Args {
toolArgs?: string;
path?: string;
ref?: string;
+ install?: boolean;
help?: boolean;
}
@@ -97,6 +109,9 @@ function parseArgs(argv: string[]): Args {
case "--dry-run":
args.dryRun = true;
break;
+ case "--install":
+ args.install = true;
+ break;
case "--target":
case "-t":
args.targets = parseTargets(argv[++i]);
@@ -129,6 +144,7 @@ function parseArgs(argv: string[]): Args {
else if (a.startsWith("--name=")) args.name = a.slice(7);
else if (a.startsWith("--path=")) args.path = a.slice(7);
else if (a.startsWith("--ref=")) args.ref = a.slice(6);
+ else if (a === "--install") args.install = true;
else positionals.push(a);
}
}
@@ -165,7 +181,7 @@ function resolvePluginPath(explicit?: string): string {
if (found) return found;
fail(
`No plugin file given and none of ${DEFAULT_PLUGIN_FILES.join(", ")} found in ${process.cwd()}.\n` +
- `Pass a local path or a GitHub source: ap-sdk install ./plugin.ts | owner/repo`,
+ `Pass a local path, GitHub source, or npm source: ap-sdk install ./plugin.ts | owner/repo | npm:pkg`,
);
}
@@ -220,8 +236,17 @@ async function main(): Promise] {
process.exit(args.command ? 0 : args.help ? 0 : 1);
}
- // `add-harness` scaffolds a new harness file and needs no plugin definition,
- // so handle it before we go looking for one.
+ // These commands need no plugin definition, so handle them before we go looking for one.
+ if (args.command === "init") {
+ runInit(args);
+ return;
+ }
+
+ if (args.command === "uninstall") {
+ runUninstall(args);
+ return;
+ }
+
if (args.command === "add-harness") {
runAddHarness(args);
return;
@@ -236,19 +261,31 @@ async function main(): Promise {
await enableTypeScript();
- // A plugin arg that looks like a GitHub source is fetched to a temp checkout,
+ if (args.command === "dev") {
+ await runDev(args);
+ return;
+ }
+
+ // A plugin arg that looks like a remote source is fetched to a temp checkout,
// compatibility-checked, then handled like a local plugin file.
- const remote = args.plugin ? isGithubSpec(args.plugin) : false;
+ const githubRemote = args.plugin ? isGithubSpec(args.plugin) : false;
+ const npmRemote = args.plugin ? isNpmSpec(args.plugin) : false;
+ const remote = githubRemote || npmRemote;
let pluginPath: string;
let sourceLabel: string | undefined;
if (remote) {
+ if (npmRemote && args.ref) {
+ fail("--ref is only supported for GitHub sources; use npm:@ instead.");
+ }
const spec =
- args.ref && !args.plugin!.includes("#")
+ githubRemote && args.ref && !args.plugin!.includes("#")
? `${args.plugin}#${args.ref}`
: args.plugin!;
try {
- const fetched = await fetchGithubPlugin(spec, { path: args.path });
+ const fetched = githubRemote
+ ? await fetchGithubPlugin(spec, { path: args.path })
+ : await fetchNpmPlugin(spec, { path: args.path });
pluginPath = fetched.pluginPath;
sourceLabel = fetched.label;
// Remove the temp checkout on any exit, including fail()/process.exit.
@@ -333,23 +370,16 @@ function printCompatibility(plugin: Plugin, label: string): void {
function runBuild(plugin: Plugin, args: Args, pluginPath: string): void {
const outDir = args.out ?? ".aps-out";
- const builds = build(plugin, { targets: args.targets });
- // The author's tools module is copied into every output so the generated glue's
- // `./tools` import resolves with no manual step.
- const toolsContent = readToolsModule(plugin, pluginPath);
+ const builds = writeBuildOutputs(plugin, pluginPath, {
+ targets: args.targets,
+ out: outDir,
+ });
console.log(`\n ${bold(plugin.id)} → ${outDir}/\n`);
for (const b of builds) {
- const root = join(process.cwd(), outDir, b.harness);
- const written = writeTree(root, b.files);
- let count = written.length;
- if (toolsContent !== null) {
- writeFileSync(join(root, "tools.ts"), toolsContent);
- count++;
- }
const w = b.warnings.length
? dim(` (${b.warnings.length} warning${b.warnings.length > 1 ? "s" : ""})`)
: "";
- console.log(` ${tick()} ${pad(b.harness)} ${count} files${w}`);
+ console.log(` ${tick()} ${pad(b.harness)} ${b.files} files${w}`);
}
// Surface capability gaps the way ai-sdk surfaces result.warnings — grouped,
@@ -397,16 +427,6 @@ function runInstall(plugin: Plugin, args: Args): void {
console.log("");
}
-/** Read the author's tools module file, or null if the plugin has no tools. */
-function readToolsModule(plugin: Plugin, pluginPath: string): string | null {
- if (!plugin.tools) return null;
- const spec = plugin.tools.module;
- const src = isAbsolute(spec) ? spec : resolve(dirname(pluginPath), spec);
- if (!existsSync(src)) {
- fail(`tools module not found: ${src} (from \`tools.module\` = "${spec}")`);
- }
- return readFileSync(src, "utf8");
-}
async function runTools(
plugin: Plugin,
@@ -464,6 +484,103 @@ function runCheck(plugin: Plugin): void {
);
}
+
+/** Scaffold a new plugin.ts starter and .gitignore entry. */
+function runInit(args: Args): void {
+ const id = args.plugin ?? "my-plugin";
+ const problem = validatePluginId(id);
+ if (problem) fail(problem);
+
+ const target = resolve(process.cwd(), args.out ?? ".", "plugin.ts");
+ const content = pluginTemplate(id);
+
+ if (existsSync(target)) {
+ fail(`Refusing to overwrite existing file: ${target}`);
+ }
+
+ if (args.dryRun) {
+ console.log(`
+ ${tick()} Would write ${bold(target)}
+`);
+ console.log(content);
+ return;
+ }
+
+ mkdirSync(dirname(target), { recursive: true });
+ writeFileSync(target, content);
+ const ignore = resolve(dirname(target), ".gitignore");
+ const snippet = gitignoreSnippet();
+ if (existsSync(ignore)) {
+ const current = readFileSync(ignore, "utf8");
+ if (!current.split(/\r?\n/).includes(".aps-out/")) {
+ writeFileSync(ignore, `${current.replace(/\s*$/, "")}
+${snippet}`);
+ }
+ } else {
+ writeFileSync(ignore, snippet);
+ }
+
+ console.log(`
+ ${tick()} Created ${bold(target)}
+`);
+ console.log(` ${bold("Next steps")}`);
+ console.log(` ${dim("npx ap-sdk check")}`);
+ console.log(` ${dim("npx ap-sdk build")}`);
+ console.log(` ${dim("npx ap-sdk install -t ")}
+`);
+}
+
+function runUninstall(args: Args): void {
+ const id = args.plugin;
+ if (!id) fail("uninstall requires a plugin id (e.g. `ap-sdk uninstall git-helper`)");
+ const scope = args.global ? "global" : "project";
+ let removed: ReturnType;
+ try {
+ removed = uninstallPlugin(id, {
+ targets: args.targets,
+ scope,
+ dryRun: args.dryRun,
+ });
+ } catch (err) {
+ fail((err as Error).message);
+ }
+ const verb = args.dryRun ? "Would remove" : "Removed";
+ console.log(`
+ ${bold(id)} — ${verb} (${scope})
+`);
+ for (const r of removed) {
+ const dest = r.files.length === 0 ? dim("(skipped)") : r.files.join(", ");
+ const mark = r.note ? warn() : tick();
+ console.log(` ${mark} ${pad(r.harness)} ${pad6(r.kind)} ${r.name} → ${dest}`);
+ if (r.note) console.log(` ${dim("↳ " + r.note)}`);
+ }
+ console.log("");
+}
+
+async function runDev(args: Args): Promise {
+ if (args.plugin && (isGithubSpec(args.plugin) || isNpmSpec(args.plugin))) {
+ fail("dev requires a local plugin path; remote GitHub/npm sources are not watchable.");
+ }
+ const pluginPath = resolvePluginPath(args.plugin);
+ validateTargets(args.targets);
+ const controller = new AbortController();
+ process.once("SIGINT", () => controller.abort());
+ console.log(`
+ Watching ${bold(dirname(pluginPath))} — ${dim("Ctrl-C to stop")}`);
+ console.log(
+ ` ${dim("On Linux, recursive watching uses the runtime's available fs.watch support.")}
+`,
+ );
+ await startDev({
+ pluginPath,
+ targets: args.targets,
+ out: args.out,
+ install: args.install,
+ scope: args.global ? "global" : "project",
+ signal: controller.signal,
+ });
+}
+
/**
* Scaffold a new harness module. The id is the second positional
* (`ap-sdk add-harness `); the file is written to `/.ts`.
diff --git a/packages/agent-plugin-sdk/src/dev.ts b/packages/agent-plugin-sdk/src/dev.ts
new file mode 100644
index 0000000..063f39d
--- /dev/null
+++ b/packages/agent-plugin-sdk/src/dev.ts
@@ -0,0 +1,200 @@
+import {
+ existsSync,
+ readFileSync,
+ readdirSync,
+ statSync,
+ watch,
+ writeFileSync,
+ type FSWatcher,
+} from "node:fs";
+import { dirname, isAbsolute, join, relative, resolve } from "node:path";
+import { pathToFileURL } from "node:url";
+import type { HarnessId, Plugin } from "./types.js";
+import type { InstallScope } from "./harnesses/types.js";
+import { build, type HarnessBuild } from "./build.js";
+import { installSkills } from "./install.js";
+import { writeTree } from "./util/fs.js";
+import { formatWarning } from "./warnings.js";
+
+export interface DevOptions {
+ pluginPath: string;
+ targets?: HarnessId[];
+ out?: string;
+ install?: boolean;
+ scope?: InstallScope;
+ onCycle?: (result: { ok: boolean; error?: Error; warnings: number }) => void;
+ signal?: AbortSignal;
+}
+
+export interface WrittenBuild {
+ harness: HarnessId;
+ files: number;
+ warnings: HarnessBuild["warnings"];
+}
+
+/** Build a plugin and write native artifact trees, including shared tools glue. */
+export function writeBuildOutputs(
+ plugin: Plugin,
+ pluginPath: string,
+ options: { targets?: HarnessId[]; out?: string } = {},
+): WrittenBuild[] {
+ const outDir = options.out ?? ".aps-out";
+ const builds = build(plugin, { targets: options.targets });
+ const toolsContent = readToolsModule(plugin, pluginPath);
+ return builds.map((b) => {
+ const root = join(process.cwd(), outDir, b.harness);
+ const written = writeTree(root, b.files);
+ let count = written.length;
+ if (toolsContent !== null) {
+ writeFileSync(join(root, "tools.ts"), toolsContent);
+ count++;
+ }
+ return { harness: b.harness, files: count, warnings: b.warnings };
+ });
+}
+
+/** Watch a plugin directory and rebuild, optionally reinstalling, after changes. */
+export async function startDev(options: DevOptions): Promise {
+ const pluginPath = resolve(options.pluginPath);
+ const root = dirname(pluginPath);
+ const outDir = options.out ?? ".aps-out";
+ let running = false;
+ let pending = false;
+ let timer: NodeJS.Timeout | undefined;
+
+ const runCycle = async (): Promise => {
+ if (running) {
+ pending = true;
+ return;
+ }
+ running = true;
+ try {
+ const plugin = await freshImport(pluginPath);
+ const written = writeBuildOutputs(plugin, pluginPath, {
+ targets: options.targets,
+ out: outDir,
+ });
+ const warnings = written.reduce((sum, b) => sum + b.warnings.length, 0);
+ if (options.install) {
+ installSkills(plugin, {
+ targets: options.targets,
+ scope: options.scope ?? "project",
+ });
+ }
+ console.log(
+ ` ${tick()} rebuilt ${plugin.id} (${written.length} target(s)${options.install ? ", installed" : ""})`,
+ );
+ for (const b of written) {
+ for (const w of b.warnings) console.log(` ${warn()} ${formatWarning(w)}`);
+ }
+ options.onCycle?.({ ok: true, warnings });
+ } catch (err) {
+ const error = err instanceof Error ? err : new Error(String(err));
+ console.error(` ${cross()} ${error.message}`);
+ options.onCycle?.({ ok: false, error, warnings: 0 });
+ } finally {
+ running = false;
+ if (pending && !options.signal?.aborted) {
+ pending = false;
+ void runCycle();
+ }
+ }
+ };
+
+ await runCycle();
+ if (options.signal?.aborted) return;
+
+ await new Promise((resolvePromise) => {
+ const watchers = watchTree(root, outDir, (changed) => {
+ if (shouldIgnore(root, changed, outDir)) return;
+ if (timer) clearTimeout(timer);
+ timer = setTimeout(() => void runCycle(), 150);
+ });
+ const stop = () => {
+ if (timer) clearTimeout(timer);
+ for (const watcher of watchers) watcher.close();
+ resolvePromise();
+ };
+ if (options.signal?.aborted) stop();
+ else options.signal?.addEventListener("abort", stop, { once: true });
+ });
+}
+
+let importNonce = 0;
+
+async function freshImport(path: string): Promise {
+ const mod = (await import(`${pathToFileURL(path).href}?t=${Date.now()}-${importNonce++}`)) as {
+ default?: Plugin;
+ };
+ const plugin = mod.default;
+ if (!plugin || typeof plugin !== "object") {
+ throw new Error(`${path} must \`export default\` a plugin (the result of definePlugin(...)).`);
+ }
+ return plugin;
+}
+
+function readToolsModule(plugin: Plugin, pluginPath: string): string | null {
+ if (!plugin.tools) return null;
+ const spec = plugin.tools.module;
+ const src = isAbsolute(spec) ? spec : resolve(dirname(pluginPath), spec);
+ if (!existsSync(src)) {
+ throw new Error(`tools module not found: ${src} (from \`tools.module\` = "${spec}")`);
+ }
+ return readFileSync(src, "utf8");
+}
+
+function watchTree(root: string, outDir: string, onChange: (path: string) => void): FSWatcher[] {
+ try {
+ return [
+ watch(root, { recursive: true }, (_event, filename) => {
+ onChange(filename ? join(root, filename.toString()) : root);
+ }),
+ ];
+ } catch {
+ const watchers: FSWatcher[] = [];
+ const add = (dir: string) => {
+ watchers.push(
+ watch(dir, (_event, filename) => {
+ const changed = filename ? join(dir, filename.toString()) : dir;
+ onChange(changed);
+ if (existsSync(changed)) {
+ try {
+ if (statSync(changed).isDirectory() && !isIgnoredDir(root, changed, outDir)) {
+ add(changed);
+ }
+ } catch {
+ // Ignore races while files are being edited.
+ }
+ }
+ }),
+ );
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
+ if (!entry.isDirectory()) continue;
+ const child = join(dir, entry.name);
+ if (!isIgnoredDir(root, child, outDir)) add(child);
+ }
+ };
+ add(root);
+ return watchers;
+ }
+}
+
+function shouldIgnore(root: string, changed: string, outDir: string): boolean {
+ return isIgnoredDir(root, changed, outDir);
+}
+
+function isIgnoredDir(root: string, path: string, outDir: string): boolean {
+ const rel = relative(root, path).split(/[\\/]+/).filter(Boolean);
+ const out = outDir.replace(/^\.\//, "");
+ return rel.some(
+ (part) =>
+ part === "node_modules" ||
+ part === ".git" ||
+ part === out ||
+ (part.startsWith(".") && part !== "."),
+ );
+}
+
+const tick = () => "\x1b[32m✓\x1b[0m";
+const warn = () => "\x1b[33m∅\x1b[0m";
+const cross = () => "\x1b[31m✖\x1b[0m";
diff --git a/packages/agent-plugin-sdk/src/index.ts b/packages/agent-plugin-sdk/src/index.ts
index 4c3cd3f..99853d7 100644
--- a/packages/agent-plugin-sdk/src/index.ts
+++ b/packages/agent-plugin-sdk/src/index.ts
@@ -27,6 +27,8 @@ export { FEATURES, formatWarning } from "./warnings.js";
export type { Feature, BuildWarning } from "./warnings.js";
export { installSkills } from "./install.js";
export type { InstallOptions, InstalledItem } from "./install.js";
+export { uninstallPlugin } from "./uninstall.js";
+export type { UninstallOptions, RemovedItem } from "./uninstall.js";
export { validatePlugin, PluginValidationError } from "./validate.js";
export {
registerHarness,
diff --git a/packages/agent-plugin-sdk/src/install.ts b/packages/agent-plugin-sdk/src/install.ts
index 5449f15..ee99200 100644
--- a/packages/agent-plugin-sdk/src/install.ts
+++ b/packages/agent-plugin-sdk/src/install.ts
@@ -1,4 +1,5 @@
import { join, dirname, basename } from "node:path";
+import { homedir } from "node:os";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import type {
HarnessId,
@@ -29,10 +30,28 @@ export interface InstalledItem {
name: string;
/** Absolute paths written (or that would be written, in dry-run). */
files: string[];
+ /** Extra structured data needed to reverse merged installs. */
+ detail?: unknown;
/** A note about a harness-specific quirk, e.g. Codex commands forced to global. */
note?: string;
}
+export interface InstallManifest {
+ version: 1;
+ plugins: Record<
+ string,
+ {
+ installedAt: string;
+ scope: InstallScope;
+ items: Array>;
+ }
+ >;
+}
+
+export function installManifestPath(scope: InstallScope): string {
+ return join(scope === "global" ? homedir() : process.cwd(), ".ap-sdk", "install-manifest.json");
+}
+
/**
* Install a plugin's skills and commands directly into the local harness
* directories (project-scoped by default, or `~`-global with `scope: "global"`).
@@ -107,15 +126,28 @@ export function installSkills(
}
}
+ if (!options.dryRun) {
+ writeInstallManifest(plugin.id, scope, results);
+ }
+
return results;
}
+
+/** Regex matching the SDK-managed instruction block for a plugin id. */
+export function contextBlockRegex(pluginId: string): RegExp {
+ const escaped = pluginId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ return new RegExp(
+ ``,
+ );
+}
+
/**
* Merge the plugin's instruction block into a markdown instruction file,
* replacing any prior block for the same plugin id (idempotent) and preserving
* the rest of the file. Appends if no prior block exists.
*/
-function mergeMarkdownBlock(
+export function mergeMarkdownBlock(
file: string,
pluginId: string,
instructions: string,
@@ -192,6 +224,7 @@ function installHooks(
kind: "hook",
name: `${hooks.length} hook(s)`,
files: [target],
+ detail: { hooks: config.hooks },
note: harness.hookInstallNote,
};
}
@@ -260,6 +293,7 @@ function installMcp(
kind: "mcp",
name: names,
files: [target.path],
+ detail: { mergeKey: target.mergeKey, names: Object.keys(servers) },
note: `merged ${Object.keys(servers).length} server(s) under "${target.mergeKey}"`,
};
}
@@ -385,3 +419,37 @@ function installCommand(
return { harness: harness.id, kind: "command", name, files: [target], note };
}
+
+
+function writeInstallManifest(
+ pluginId: string,
+ scope: InstallScope,
+ items: InstalledItem[],
+): void {
+ const file = installManifestPath(scope);
+ const manifest = readInstallManifest(file);
+ manifest.plugins[pluginId] = {
+ installedAt: new Date().toISOString(),
+ scope,
+ items: items.map(({ note: _note, ...item }) => item),
+ };
+ mkdirSync(dirname(file), { recursive: true });
+ writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n");
+}
+
+function readInstallManifest(file: string): InstallManifest {
+ if (!existsSync(file)) return { version: 1, plugins: {} };
+ const raw = readFileSync(file, "utf8").trim();
+ if (!raw) return { version: 1, plugins: {} };
+ try {
+ const parsed = JSON.parse(raw) as InstallManifest;
+ if (parsed.version !== 1 || !parsed.plugins || typeof parsed.plugins !== "object") {
+ throw new Error("unsupported manifest shape");
+ }
+ return parsed;
+ } catch (err) {
+ throw new Error(
+ `Refusing to overwrite ${file}: it is not valid install manifest JSON (${(err as Error).message}).`,
+ );
+ }
+}
diff --git a/packages/agent-plugin-sdk/src/npm.ts b/packages/agent-plugin-sdk/src/npm.ts
new file mode 100644
index 0000000..63fc4a4
--- /dev/null
+++ b/packages/agent-plugin-sdk/src/npm.ts
@@ -0,0 +1,178 @@
+import {
+ existsSync,
+ mkdirSync,
+ mkdtempSync,
+ readFileSync,
+ rmSync,
+ symlinkSync,
+} from "node:fs";
+import { tmpdir } from "node:os";
+import { dirname, join, resolve } from "node:path";
+import { Readable } from "node:stream";
+import { pipeline } from "node:stream/promises";
+import type { ReadableStream as WebReadableStream } from "node:stream/web";
+import { fileURLToPath } from "node:url";
+import { createGunzip } from "node:zlib";
+import { x as tarExtract } from "tar";
+import { DEFAULT_PLUGIN_FILES, locatePlugin } from "./plugin-files.js";
+
+export interface NpmSpec {
+ name: string;
+ /** Exact version or dist-tag. Defaults to latest. */
+ version?: string;
+}
+
+export interface FetchedNpmPlugin {
+ spec: NpmSpec;
+ pluginPath: string;
+ label: string;
+ cleanup: () => void;
+}
+
+const NPM_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
+
+export function parseNpmSpec(spec: string): NpmSpec | null {
+ if (!/^npm:/i.test(spec)) return null;
+ const body = spec.slice(4).trim();
+ if (!body) return null;
+ const at = body.lastIndexOf("@");
+ const hasVersion = at > 0;
+ const name = hasVersion ? body.slice(0, at) : body;
+ const version = hasVersion ? body.slice(at + 1) : undefined;
+ if (!name || !NPM_NAME.test(name) || version === "") return null;
+ return { name, version };
+}
+
+export function isNpmSpec(spec: string): boolean {
+ return /^npm:/i.test(spec);
+}
+
+export async function fetchNpmPlugin(
+ rawSpec: string,
+ opts: { path?: string } = {},
+): Promise {
+ const spec = parseNpmSpec(rawSpec);
+ if (!spec) {
+ throw new Error(
+ `Not an npm source: "${rawSpec}". Use npm: or npm:@.`,
+ );
+ }
+
+ const dir = mkdtempSync(join(tmpdir(), "ap-sdk-npm-"));
+ let ok = false;
+ try {
+ const doc = await fetchVersionDoc(spec);
+ await downloadAndExtract(doc.dist.tarball, `${spec.name}@${doc.version}`, dir);
+ linkSdk(dir);
+
+ const root = opts.path ? join(dir, opts.path) : dir;
+ const pluginPath = findPackagePlugin(root);
+ if (!pluginPath) {
+ const where = opts.path ? `npm:${spec.name}/${opts.path}` : `npm:${spec.name}`;
+ throw new Error(
+ `No ap-sdk plugin file found in ${where} (looked for ${DEFAULT_PLUGIN_FILES.join(", ")}).` +
+ (opts.path
+ ? ""
+ : " If it lives in a subdirectory, pass --path . Packages can also set \"ap-sdk\": { \"plugin\": \"./plugin.ts\" } in package.json."),
+ );
+ }
+
+ ok = true;
+ return {
+ spec,
+ pluginPath,
+ label: `npm:${spec.name}@${doc.version}`,
+ cleanup: () => rmSync(dir, { recursive: true, force: true }),
+ };
+ } finally {
+ if (!ok) rmSync(dir, { recursive: true, force: true });
+ }
+}
+
+interface NpmVersionDoc {
+ name: string;
+ version: string;
+ dist: { tarball: string };
+}
+
+async function fetchVersionDoc(spec: NpmSpec): Promise {
+ const version = spec.version ?? "latest";
+ const url = `https://registry.npmjs.org/${encodeURIComponent(spec.name)}/${encodeURIComponent(version)}`;
+ const res = await fetch(url, {
+ headers: { "User-Agent": "ap-sdk-cli", Accept: "application/json" },
+ });
+ if (!res.ok) {
+ if (res.status === 404) {
+ throw new Error(
+ spec.version
+ ? `npm package "${spec.name}" version/tag "${spec.version}" not found`
+ : `npm package "${spec.name}" not found`,
+ );
+ }
+ throw new Error(`Failed to fetch npm package "${spec.name}": ${res.status} ${res.statusText}`);
+ }
+ const doc = (await res.json()) as Partial;
+ if (!doc.version || !doc.dist?.tarball) {
+ throw new Error(`npm package "${spec.name}" did not include a tarball URL`);
+ }
+ return doc as NpmVersionDoc;
+}
+
+async function downloadAndExtract(url: string, label: string, dir: string): Promise {
+ const res = await fetch(url, {
+ redirect: "follow",
+ headers: { "User-Agent": "ap-sdk-cli" },
+ });
+ if (!res.ok || !res.body) {
+ throw new Error(`Failed to download ${label}: ${res.status} ${res.statusText}`);
+ }
+ await pipeline(
+ Readable.fromWeb(res.body as WebReadableStream),
+ createGunzip(),
+ tarExtract({ cwd: dir, strip: 1 }),
+ );
+}
+
+function findPackagePlugin(root: string): string | null {
+ const manifest = join(root, "package.json");
+ if (existsSync(manifest)) {
+ try {
+ const pkg = JSON.parse(readFileSync(manifest, "utf8")) as {
+ "ap-sdk"?: { plugin?: string };
+ };
+ const plugin = pkg["ap-sdk"]?.plugin;
+ if (typeof plugin === "string" && plugin.trim()) {
+ const target = resolve(root, plugin);
+ if (existsSync(target)) return target;
+ }
+ } catch {
+ // Fall back to conventional filenames.
+ }
+ }
+ return locatePlugin(root);
+}
+
+function linkSdk(dir: string): void {
+ const root = findSdkRoot();
+ const scope = join(dir, "node_modules", "@jalco");
+ mkdirSync(scope, { recursive: true });
+ symlinkSync(root, join(scope, "ap-sdk"), "junction");
+}
+
+function findSdkRoot(): string {
+ let dir = dirname(fileURLToPath(import.meta.url));
+ for (;;) {
+ const pj = join(dir, "package.json");
+ if (existsSync(pj)) {
+ try {
+ if (JSON.parse(readFileSync(pj, "utf8")).name === "@jalco/ap-sdk") return dir;
+ } catch {
+ // keep walking
+ }
+ }
+ const parent = dirname(dir);
+ if (parent === dir) break;
+ dir = parent;
+ }
+ throw new Error("Could not locate the @jalco/ap-sdk package root.");
+}
diff --git a/packages/agent-plugin-sdk/src/scaffold-plugin.ts b/packages/agent-plugin-sdk/src/scaffold-plugin.ts
new file mode 100644
index 0000000..d5da4d3
--- /dev/null
+++ b/packages/agent-plugin-sdk/src/scaffold-plugin.ts
@@ -0,0 +1,52 @@
+/**
+ * Source-generation for `ap-sdk init` — stamps out a working starter plugin
+ * wired to the public SDK entrypoint.
+ *
+ * The generated file validates as-is and demonstrates instructions, one skill,
+ * and one command so new authors edit a real plugin rather than a blank stub.
+ */
+
+const KEBAB = /^[a-z0-9]+(-[a-z0-9]+)*$/;
+
+/** Validate a plugin id, returning an error string or null. */
+export function validatePluginId(id: string): string | null {
+ if (!id) return "a plugin id is required (e.g. `ap-sdk init my-plugin`)";
+ if (!KEBAB.test(id)) {
+ return `plugin id "${id}" must be kebab-case (lowercase letters, digits, single hyphens)`;
+ }
+ return null;
+}
+
+/** Render the TypeScript source for a starter plugin. */
+export function pluginTemplate(id: string, pkg = "@jalco/ap-sdk"): string {
+ return `// Generated by \`ap-sdk init ${id}\`.
+// See https://ap-sdk.dev/docs/quickstart for the full walkthrough.
+import { definePlugin, defineSkill, defineCommand } from "${pkg}";
+
+export default definePlugin({
+ id: "${id}",
+ description: "Helpers for working with git in a repo.",
+ instructions: "## Git\\n- Branch off main; never commit to it directly.",
+ skills: [
+ defineSkill({
+ name: "diff-review",
+ description:
+ "Summarize and risk-flag uncommitted changes. Use when the user asks what changed.",
+ instructions: "Run \`git diff HEAD\` and summarize the changes in 2-4 bullets.",
+ }),
+ ],
+ commands: [
+ defineCommand({
+ name: "commit",
+ description: "Write a conventional commit for the staged changes.",
+ body: "Write a Conventional Commit message for the staged diff. Args: $ARGUMENTS",
+ }),
+ ],
+});
+`;
+}
+
+/** Files generated by the SDK should not be committed by default. */
+export function gitignoreSnippet(): string {
+ return ".aps-out/\n";
+}
diff --git a/packages/agent-plugin-sdk/src/uninstall.ts b/packages/agent-plugin-sdk/src/uninstall.ts
new file mode 100644
index 0000000..d1bd213
--- /dev/null
+++ b/packages/agent-plugin-sdk/src/uninstall.ts
@@ -0,0 +1,195 @@
+import {
+ existsSync,
+ readFileSync,
+ rmSync,
+ rmdirSync,
+ unlinkSync,
+ writeFileSync,
+} from "node:fs";
+import { dirname } from "node:path";
+import type { HarnessId } from "./types.js";
+import type { InstallScope } from "./harnesses/types.js";
+import {
+ contextBlockRegex,
+ installManifestPath,
+ type InstalledItem,
+ type InstallManifest,
+} from "./install.js";
+
+export interface UninstallOptions {
+ scope?: InstallScope;
+ targets?: HarnessId[];
+ dryRun?: boolean;
+}
+
+export interface RemovedItem {
+ harness: HarnessId;
+ kind: string;
+ name: string;
+ files: string[];
+ note?: string;
+}
+
+type ManifestItem = Omit;
+
+/** Reverse a manifest-backed install without touching unrecorded user files. */
+export function uninstallPlugin(
+ pluginId: string,
+ options: UninstallOptions = {},
+): RemovedItem[] {
+ const scope = options.scope ?? "project";
+ const manifestFile = installManifestPath(scope);
+ const manifest = readManifest(manifestFile);
+ const entry = manifest.plugins[pluginId];
+ if (!entry) {
+ throw new Error(`No install manifest entry found for plugin "${pluginId}" in ${manifestFile}.`);
+ }
+
+ const targetSet = options.targets ? new Set(options.targets) : null;
+ const selected = entry.items.filter((item) => !targetSet || targetSet.has(item.harness));
+ if (selected.length === 0) {
+ throw new Error(`No recorded install items for plugin "${pluginId}" matched the requested target(s).`);
+ }
+
+ const removed = selected.map((item) => removeItem(pluginId, item, !!options.dryRun));
+
+ if (!options.dryRun) {
+ const remaining = entry.items.filter((item) => targetSet && !targetSet.has(item.harness));
+ if (remaining.length > 0) {
+ entry.items = remaining;
+ } else {
+ delete manifest.plugins[pluginId];
+ }
+ writeOrRemoveManifest(manifestFile, manifest);
+ }
+
+ return removed;
+}
+
+function removeItem(pluginId: string, item: ManifestItem, dryRun: boolean): RemovedItem {
+ switch (item.kind) {
+ case "skill":
+ case "command":
+ case "subagent":
+ case "file":
+ return removeFiles(item, dryRun);
+ case "context":
+ return removeContext(pluginId, item, dryRun);
+ case "mcp":
+ return removeMcp(item, dryRun);
+ case "hook":
+ return removeHooks(item, dryRun);
+ default:
+ return { ...item, note: `unknown install item kind "${item.kind}"; skipped` };
+ }
+}
+
+function removeFiles(item: ManifestItem, dryRun: boolean): RemovedItem {
+ const missing: string[] = [];
+ for (const file of item.files) {
+ if (!existsSync(file)) {
+ missing.push(file);
+ continue;
+ }
+ if (!dryRun) unlinkSync(file);
+ }
+ if (!dryRun && item.kind === "skill") {
+ const dir = commonDir(item.files);
+ if (dir) {
+ try {
+ rmdirSync(dir);
+ } catch {
+ // Non-empty or already removed; leave it alone.
+ }
+ }
+ }
+ return {
+ harness: item.harness,
+ kind: item.kind,
+ name: item.name,
+ files: item.files,
+ note: missing.length ? `skipped ${missing.length} missing recorded file(s)` : undefined,
+ };
+}
+
+function removeContext(pluginId: string, item: ManifestItem, dryRun: boolean): RemovedItem {
+ const file = item.files[0];
+ if (!file || !existsSync(file)) return { ...item, note: "recorded context file is missing; skipped" };
+ const raw = readFileSync(file, "utf8");
+ const next = raw.replace(contextBlockRegex(pluginId), "").replace(/\n{3,}/g, "\n\n");
+ if (!dryRun && next !== raw) writeFileSync(file, next);
+ return { harness: item.harness, kind: item.kind, name: item.name, files: item.files };
+}
+
+function removeMcp(item: ManifestItem, dryRun: boolean): RemovedItem {
+ const file = item.files[0];
+ const detail = item.detail as { mergeKey?: string; names?: string[] } | undefined;
+ if (!file || !existsSync(file)) return { ...item, note: "recorded MCP config is missing; skipped" };
+ if (!detail?.mergeKey || !detail.names) return { ...item, note: "manifest lacks MCP merge details; skipped" };
+ try {
+ const root = JSON.parse(readFileSync(file, "utf8")) as Record;
+ const bucket = (root[detail.mergeKey] as Record) ?? {};
+ for (const name of detail.names) delete bucket[name];
+ root[detail.mergeKey] = bucket;
+ if (!dryRun) writeFileSync(file, JSON.stringify(root, null, 2) + "\n");
+ return { harness: item.harness, kind: item.kind, name: item.name, files: item.files };
+ } catch (err) {
+ return { ...item, note: `config is not valid JSON; skipped (${(err as Error).message})` };
+ }
+}
+
+function removeHooks(item: ManifestItem, dryRun: boolean): RemovedItem {
+ const file = item.files[0];
+ const detail = item.detail as { hooks?: Record } | undefined;
+ if (!file || !existsSync(file)) return { ...item, note: "recorded hook config is missing; skipped" };
+ if (!detail?.hooks) return { ...item, note: "manifest lacks hook details; skipped" };
+ try {
+ const root = JSON.parse(readFileSync(file, "utf8")) as Record;
+ const hooks = (root.hooks as Record) ?? {};
+ for (const [event, groups] of Object.entries(detail.hooks)) {
+ const remove = new Set(groups.map((group) => JSON.stringify(group)));
+ hooks[event] = (hooks[event] ?? []).filter((group) => !remove.has(JSON.stringify(group)));
+ }
+ root.hooks = hooks;
+ if (!dryRun) writeFileSync(file, JSON.stringify(root, null, 2) + "\n");
+ return { harness: item.harness, kind: item.kind, name: item.name, files: item.files };
+ } catch (err) {
+ return { ...item, note: `config is not valid JSON; skipped (${(err as Error).message})` };
+ }
+}
+
+function readManifest(file: string): InstallManifest {
+ if (!existsSync(file)) throw new Error(`No install manifest found at ${file}.`);
+ try {
+ const parsed = JSON.parse(readFileSync(file, "utf8")) as InstallManifest;
+ if (parsed.version !== 1 || !parsed.plugins) throw new Error("unsupported manifest shape");
+ return parsed;
+ } catch (err) {
+ throw new Error(`Could not read install manifest ${file}: ${(err as Error).message}`);
+ }
+}
+
+function writeOrRemoveManifest(file: string, manifest: InstallManifest): void {
+ if (Object.keys(manifest.plugins).length === 0) {
+ rmSync(file, { force: true });
+ try {
+ rmdirSync(dirname(file));
+ } catch {
+ // Leave the directory when it has other content.
+ }
+ return;
+ }
+ writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n");
+}
+
+function commonDir(files: string[]): string | null {
+ if (files.length === 0) return null;
+ let parts = dirname(files[0]!).split(/[\\/]+/);
+ for (const file of files.slice(1)) {
+ const next = dirname(file).split(/[\\/]+/);
+ let i = 0;
+ while (i < parts.length && parts[i] === next[i]) i++;
+ parts = parts.slice(0, i);
+ }
+ return parts.join("/") || null;
+}
diff --git a/packages/agent-plugin-sdk/test/dev.test.ts b/packages/agent-plugin-sdk/test/dev.test.ts
new file mode 100644
index 0000000..f5b26f7
--- /dev/null
+++ b/packages/agent-plugin-sdk/test/dev.test.ts
@@ -0,0 +1,113 @@
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { startDev } from "../src/dev.js";
+
+let cwd: string;
+let prev: string;
+
+beforeEach(() => {
+ cwd = mkdtempSync(join(tmpdir(), "aps-dev-"));
+ prev = process.cwd();
+ process.chdir(cwd);
+});
+
+afterEach(() => {
+ process.chdir(prev);
+ rmSync(cwd, { recursive: true, force: true });
+});
+
+function pluginSource(description: string, valid = true): string {
+ if (!valid) return "export default { id: '', description: '' };\n";
+ return `export default {
+ id: "dev-plugin",
+ description: "d",
+ skills: [{ name: "diff-review", description: "${description}", instructions: "do it" }]
+};
+`;
+}
+
+function waitForCycle(cycles: Array<{ ok: boolean }>, index: number): Promise {
+ return new Promise((resolve, reject) => {
+ const started = Date.now();
+ const timer = setInterval(() => {
+ if (cycles.length > index) {
+ clearInterval(timer);
+ resolve();
+ } else if (Date.now() - started > 5000) {
+ clearInterval(timer);
+ reject(new Error(`timed out waiting for cycle ${index}`));
+ }
+ }, 50);
+ });
+}
+
+describe("startDev", () => {
+ it("runs an initial build and resolves on abort", async () => {
+ const pluginPath = join(cwd, "plugin.mjs");
+ writeFileSync(pluginPath, pluginSource("first"));
+ const controller = new AbortController();
+ const cycles: Array<{ ok: boolean }> = [];
+ const done = startDev({
+ pluginPath,
+ targets: ["claude"],
+ signal: controller.signal,
+ onCycle(result) {
+ cycles.push(result);
+ controller.abort();
+ },
+ });
+
+ await done;
+
+ expect(cycles[0]?.ok).toBe(true);
+ expect(existsSync(join(cwd, ".aps-out", "claude", "skills", "diff-review", "SKILL.md"))).toBe(true);
+ });
+
+ it("rebuilds when the plugin changes", async () => {
+ const pluginPath = join(cwd, "plugin.mjs");
+ writeFileSync(pluginPath, pluginSource("first"));
+ const controller = new AbortController();
+ const cycles: Array<{ ok: boolean }> = [];
+ const done = startDev({
+ pluginPath,
+ targets: ["claude"],
+ signal: controller.signal,
+ onCycle(result) {
+ cycles.push(result);
+ },
+ });
+ await waitForCycle(cycles, 0);
+ writeFileSync(pluginPath, pluginSource("second"));
+ await waitForCycle(cycles, 1);
+ controller.abort();
+ await done;
+
+ const skill = readFileSync(join(cwd, ".aps-out", "claude", "skills", "diff-review", "SKILL.md"), "utf8");
+ expect(skill).toContain("second");
+ });
+
+ it("survives a validation error and recovers", async () => {
+ const pluginPath = join(cwd, "plugin.mjs");
+ writeFileSync(pluginPath, pluginSource("first", false));
+ const controller = new AbortController();
+ const cycles: Array<{ ok: boolean }> = [];
+ const done = startDev({
+ pluginPath,
+ targets: ["claude"],
+ signal: controller.signal,
+ onCycle(result) {
+ cycles.push(result);
+ },
+ });
+ await waitForCycle(cycles, 0);
+ writeFileSync(pluginPath, pluginSource("fixed"));
+ await waitForCycle(cycles, 1);
+ controller.abort();
+ await done;
+
+ expect(cycles[0]?.ok).toBe(false);
+ expect(cycles[1]?.ok).toBe(true);
+ });
+});
diff --git a/packages/agent-plugin-sdk/test/init.test.ts b/packages/agent-plugin-sdk/test/init.test.ts
new file mode 100644
index 0000000..f0c51ab
--- /dev/null
+++ b/packages/agent-plugin-sdk/test/init.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from "vitest";
+import { defineCommand, definePlugin, defineSkill } from "../src/index.js";
+import { validatePlugin } from "../src/validate.js";
+import { pluginTemplate, validatePluginId } from "../src/scaffold-plugin.js";
+
+describe("ap-sdk init scaffold", () => {
+ it("renders a starter plugin importing the public package", () => {
+ const src = pluginTemplate("my-plugin");
+ expect(src).toContain("definePlugin");
+ expect(src).toContain('id: "my-plugin"');
+ expect(src).toContain('from "@jalco/ap-sdk"');
+ });
+
+ it("matches a plugin shape that validates", () => {
+ const plugin = definePlugin({
+ id: "my-plugin",
+ description: "Helpers for working with git in a repo.",
+ instructions: "## Git\n- Branch off main; never commit to it directly.",
+ skills: [
+ defineSkill({
+ name: "diff-review",
+ description:
+ "Summarize and risk-flag uncommitted changes. Use when the user asks what changed.",
+ instructions: "Run `git diff HEAD` and summarize the changes in 2-4 bullets.",
+ }),
+ ],
+ commands: [
+ defineCommand({
+ name: "commit",
+ description: "Write a conventional commit for the staged changes.",
+ body: "Write a Conventional Commit message for the staged diff. Args: $ARGUMENTS",
+ }),
+ ],
+ });
+
+ expect(() => validatePlugin(plugin)).not.toThrow();
+ const src = pluginTemplate("my-plugin");
+ expect(src).toContain('instructions: "## Git\\n- Branch off main; never commit to it directly."');
+ expect(src).toContain('name: "diff-review"');
+ expect(src).toContain('name: "commit"');
+ });
+
+ it("validates kebab-case plugin ids", () => {
+ expect(validatePluginId("my-plugin")).toBeNull();
+ expect(validatePluginId("My_Plugin")).toMatch(/kebab-case/);
+ });
+});
diff --git a/packages/agent-plugin-sdk/test/npm.test.ts b/packages/agent-plugin-sdk/test/npm.test.ts
new file mode 100644
index 0000000..6a6c889
--- /dev/null
+++ b/packages/agent-plugin-sdk/test/npm.test.ts
@@ -0,0 +1,105 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { dirname, join } from "node:path";
+import { c as tarCreate } from "tar";
+import { fetchNpmPlugin, isNpmSpec, parseNpmSpec } from "../src/npm.js";
+
+afterEach(() => {
+ vi.unstubAllGlobals();
+});
+
+describe("parseNpmSpec", () => {
+ it("parses package specs", () => {
+ expect(parseNpmSpec("npm:foo")).toEqual({ name: "foo", version: undefined });
+ expect(parseNpmSpec("npm:foo@1.2.3")).toEqual({ name: "foo", version: "1.2.3" });
+ expect(parseNpmSpec("npm:@scope/foo")).toEqual({ name: "@scope/foo", version: undefined });
+ expect(parseNpmSpec("npm:@scope/foo@next")).toEqual({ name: "@scope/foo", version: "next" });
+ });
+
+ it("rejects empty or invalid names", () => {
+ expect(parseNpmSpec("npm:")).toBeNull();
+ expect(parseNpmSpec("npm:Bad_Name")).toBeNull();
+ });
+});
+
+describe("isNpmSpec", () => {
+ it("is a strict prefix check", () => {
+ expect(isNpmSpec("npm:foo")).toBe(true);
+ expect(isNpmSpec("./npm:weird")).toBe(false);
+ expect(isNpmSpec("owner/repo")).toBe(false);
+ });
+});
+
+describe("fetchNpmPlugin", () => {
+ it("extracts a package and locates the plugin", async () => {
+ const tarball = await packageTar({ "plugin.ts": "export default { id: 'pkg', description: 'd' };\n" });
+ stubNpmFetch(tarball);
+
+ const fetched = await fetchNpmPlugin("npm:pkg");
+
+ expect(fetched.label).toBe("npm:pkg@1.2.3");
+ expect(readFileSync(fetched.pluginPath, "utf8")).toContain("id: 'pkg'");
+ const dir = fetched.pluginPath.split("/package/")[0] ?? fetched.pluginPath;
+ fetched.cleanup();
+ expect(existsSync(fetched.pluginPath)).toBe(false);
+ expect(existsSync(dir)).toBe(false);
+ });
+
+ it("rejects registry 404s with the package name", async () => {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async () => new Response("missing", { status: 404, statusText: "Not Found" })),
+ );
+
+ await expect(fetchNpmPlugin("npm:missing-pkg")).rejects.toThrow(/missing-pkg/);
+ });
+
+ it("prefers package.json ap-sdk.plugin over conventional files", async () => {
+ const tarball = await packageTar({
+ "package.json": JSON.stringify({ "ap-sdk": { plugin: "./custom/entry.ts" } }),
+ "plugin.ts": "export default { id: 'root', description: 'd' };\n",
+ "custom/entry.ts": "export default { id: 'custom', description: 'd' };\n",
+ });
+ stubNpmFetch(tarball);
+
+ const fetched = await fetchNpmPlugin("npm:pkg");
+
+ expect(fetched.pluginPath.endsWith("custom/entry.ts")).toBe(true);
+ expect(readFileSync(fetched.pluginPath, "utf8")).toContain("custom");
+ fetched.cleanup();
+ });
+});
+
+async function packageTar(files: Record): Promise {
+ const dir = mkdtempSync(join(tmpdir(), "aps-npm-fixture-"));
+ const archive = join(dir, "pkg.tgz");
+ try {
+ for (const [name, content] of Object.entries(files)) {
+ const file = join(dir, "package", name);
+ await import("node:fs/promises").then(async (fs) => {
+ await fs.mkdir(dirname(file), { recursive: true });
+ await fs.writeFile(file, content);
+ });
+ }
+ await tarCreate({ cwd: dir, gzip: true, file: archive }, ["package"]);
+ return readFileSync(archive);
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+}
+
+function stubNpmFetch(tarball: Buffer): void {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async (url: string) => {
+ if (url.includes("registry.npmjs.org")) {
+ return new Response(
+ JSON.stringify({ name: "pkg", version: "1.2.3", dist: { tarball: "https://registry.example/pkg.tgz" } }),
+ { status: 200, headers: { "content-type": "application/json" } },
+ );
+ }
+ return new Response(tarball, { status: 200 });
+ }),
+ );
+}
diff --git a/packages/agent-plugin-sdk/test/uninstall.test.ts b/packages/agent-plugin-sdk/test/uninstall.test.ts
new file mode 100644
index 0000000..96a52c6
--- /dev/null
+++ b/packages/agent-plugin-sdk/test/uninstall.test.ts
@@ -0,0 +1,153 @@
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import {
+ defineCommand,
+ defineHook,
+ definePlugin,
+ defineSkill,
+ installSkills,
+ uninstallPlugin,
+} from "../src/index.js";
+
+let cwd: string;
+let prev: string;
+
+beforeEach(() => {
+ cwd = mkdtempSync(join(tmpdir(), "aps-uninstall-"));
+ prev = process.cwd();
+ process.chdir(cwd);
+});
+
+afterEach(() => {
+ process.chdir(prev);
+ rmSync(cwd, { recursive: true, force: true });
+});
+
+describe("uninstallPlugin", () => {
+ it("round-trips skill and command files and removes the manifest entry", () => {
+ const plugin = definePlugin({
+ id: "rich",
+ description: "d",
+ skills: [defineSkill({ name: "diff-review", description: "x", instructions: "do x" })],
+ commands: [defineCommand({ name: "annotate", description: "x", body: "do it" })],
+ });
+
+ installSkills(plugin, { targets: ["claude"], scope: "project" });
+ const skill = join(cwd, ".claude", "skills", "diff-review", "SKILL.md");
+ const command = join(cwd, ".claude", "commands", "annotate.md");
+ expect(existsSync(skill)).toBe(true);
+ expect(existsSync(command)).toBe(true);
+ expect(existsSync(join(cwd, ".ap-sdk", "install-manifest.json"))).toBe(true);
+
+ uninstallPlugin("rich", { scope: "project" });
+
+ expect(existsSync(skill)).toBe(false);
+ expect(existsSync(command)).toBe(false);
+ expect(existsSync(join(cwd, ".claude", "skills", "diff-review"))).toBe(false);
+ expect(existsSync(join(cwd, ".ap-sdk", "install-manifest.json"))).toBe(false);
+ });
+
+ it("removes only its context block", () => {
+ writeFileSync(join(cwd, "CLAUDE.md"), "# User notes\nKeep this.\n");
+ installSkills(
+ definePlugin({ id: "ctx", description: "d", instructions: "plugin instructions" }),
+ { targets: ["claude"], scope: "project" },
+ );
+
+ uninstallPlugin("ctx");
+
+ const content = readFileSync(join(cwd, "CLAUDE.md"), "utf8");
+ expect(content).toContain("Keep this.");
+ expect(content).not.toContain("agent-plugin-sdk:ctx");
+ expect(content).not.toContain("plugin instructions");
+ });
+
+ it("removes only its MCP servers from merged JSON", () => {
+ writeFileSync(
+ join(cwd, "opencode.json"),
+ JSON.stringify({ theme: "dark", mcp: { foreign: { type: "local", command: ["foo"] } } }),
+ );
+ installSkills(
+ definePlugin({
+ id: "mcp-plugin",
+ description: "d",
+ mcpServers: { mine: { command: "npx", args: ["-y", "mine"] } },
+ }),
+ { targets: ["opencode"], scope: "project" },
+ );
+
+ uninstallPlugin("mcp-plugin");
+
+ const cfg = JSON.parse(readFileSync(join(cwd, "opencode.json"), "utf8"));
+ expect(cfg.theme).toBe("dark");
+ expect(cfg.mcp.foreign).toBeDefined();
+ expect(cfg.mcp.mine).toBeUndefined();
+ });
+
+ it("removes only its hook groups", () => {
+ mkdirSync(join(cwd, ".claude"), { recursive: true });
+ writeFileSync(
+ join(cwd, ".claude", "settings.json"),
+ JSON.stringify({ hooks: { PreToolUse: [{ matcher: "Bash", hooks: [{ type: "command", command: "foreign" }] }] } }),
+ );
+ installSkills(
+ definePlugin({
+ id: "hook-plugin",
+ description: "d",
+ hooks: [
+ defineHook({
+ event: "pre-tool-use",
+ matcher: "Bash",
+ command: "echo plugin",
+ }),
+ ],
+ }),
+ { targets: ["claude"], scope: "project" },
+ );
+
+ uninstallPlugin("hook-plugin");
+
+ const cfg = JSON.parse(readFileSync(join(cwd, ".claude", "settings.json"), "utf8"));
+ expect(cfg.hooks.PreToolUse).toEqual([
+ { matcher: "Bash", hooks: [{ type: "command", command: "foreign" }] },
+ ]);
+ });
+
+ it("dry-run reports paths without removing files", () => {
+ const plugin = definePlugin({
+ id: "dry",
+ description: "d",
+ skills: [defineSkill({ name: "skill", description: "x", instructions: "do x" })],
+ });
+ installSkills(plugin, { targets: ["claude"] });
+ const skill = join(".claude", "skills", "skill", "SKILL.md");
+
+ const removed = uninstallPlugin("dry", { dryRun: true });
+
+ expect(removed[0]?.files).toContain(skill);
+ expect(existsSync(skill)).toBe(true);
+ expect(existsSync(join(cwd, ".ap-sdk", "install-manifest.json"))).toBe(true);
+ });
+
+ it("throws for an unknown plugin id", () => {
+ installSkills(definePlugin({ id: "known", description: "d" }), { targets: ["claude"] });
+ expect(() => uninstallPlugin("missing")).toThrow(/missing/);
+ });
+
+ it("skips recorded files already deleted by the user", () => {
+ const plugin = definePlugin({
+ id: "gone",
+ description: "d",
+ skills: [defineSkill({ name: "skill", description: "x", instructions: "do x" })],
+ });
+ installSkills(plugin, { targets: ["claude"] });
+ const skill = join(cwd, ".claude", "skills", "skill", "SKILL.md");
+ rmSync(skill);
+
+ const removed = uninstallPlugin("gone");
+
+ expect(removed[0]?.note).toMatch(/missing/);
+ });
+});
From f4795e41d59b7567aa240237f9221b76cb19bb64 Mon Sep 17 00:00:00 2001
From: Justin Levine <20596508+jal-co@users.noreply.github.com>
Date: Wed, 1 Jul 2026 16:42:30 -0500
Subject: [PATCH 2/4] docs: overhaul root README and add npm package README
Fixes the broken Docs link, makes the Examples link absolute, adds a
'What you can define' feature table and an 'Already have a plugin?'
porting section, and gives the npm package a README with all-absolute
links. Both attribution credits preserved. (plan 001)
---
.tegami/2026-07-01-package-readme.md | 10 ++++
README.md | 29 +++++++++++-
packages/agent-plugin-sdk/README.md | 70 ++++++++++++++++++++++++++++
3 files changed, 108 insertions(+), 1 deletion(-)
create mode 100644 .tegami/2026-07-01-package-readme.md
create mode 100644 packages/agent-plugin-sdk/README.md
diff --git a/.tegami/2026-07-01-package-readme.md b/.tegami/2026-07-01-package-readme.md
new file mode 100644
index 0000000..52ea489
--- /dev/null
+++ b/.tegami/2026-07-01-package-readme.md
@@ -0,0 +1,10 @@
+---
+packages:
+ "@jalco/ap-sdk": patch
+---
+
+### Add a package README
+
+The npm page for `@jalco/ap-sdk` now has a readme covering the full feature
+surface (skills, commands, subagents, hooks, MCP, shared tools) and the
+`ap-sdk port` migration path.
diff --git a/README.md b/README.md
index b017be9..5cb91ee 100644
--- a/README.md
+++ b/README.md
@@ -39,13 +39,40 @@ ap-sdk build # → .aps-out/{claude,codex,gemini,copilot,cursor,windsurf,pi
ap-sdk install # → drops artifacts into your local harness dirs
```
+## What you can define
+
+| Field | What it becomes |
+| --- | --- |
+| [`instructions`](https://ap-sdk.dev/docs) | Always-on system or project guidance for every supported harness. |
+| [`skills`](https://ap-sdk.dev/docs) | Reusable skill documents with descriptions and instructions. |
+| [`commands`](https://ap-sdk.dev/docs) | Slash commands or prompt commands in harnesses that support them. |
+| [`subagents`](https://ap-sdk.dev/docs) | Named specialist agents with their own prompts, tools, and model hints. |
+| [`hooks`](https://ap-sdk.dev/docs) | Lifecycle hooks and scripts translated to each harness' event model. |
+| [`mcpServers`](https://ap-sdk.dev/docs) | MCP server config emitted in native JSON/TOML/YAML formats. |
+| [`tools`](https://ap-sdk.dev/docs) | Shared TypeScript tool implementations with per-harness glue. |
+| [`files`](https://ap-sdk.dev/docs) | Companion files copied next to the generated native artifacts. |
+
+See the [support matrix](https://ap-sdk.dev/docs/harnesses) for what each harness supports.
+
+## Already have a plugin?
+
+Point `ap-sdk port` at an existing Claude Code, Cursor, Codex, Gemini, Copilot, Windsurf, Pi, or OpenCode layout and it generates a portable `plugin.ts` that loads your existing files instead of inlining them.
+
+```bash
+npx ap-sdk port ./my-plugin
+npx ap-sdk check ./plugin.ts
+npx ap-sdk install owner/repo
+```
+
+Read the [porting guide](https://ap-sdk.dev/docs/porting), or install straight from GitHub with `ap-sdk install owner/repo`.
+
## Install
```bash
pnpm add -D @jalco/ap-sdk
```
-[Docs](https://github.com/jal-co/agent-plugin-sdk) · [Examples](./packages/agent-plugin-sdk/examples)
+[Docs](https://ap-sdk.dev/docs) · [Examples](https://github.com/jal-co/agent-plugin-sdk/tree/main/packages/agent-plugin-sdk/examples)
## License
diff --git a/packages/agent-plugin-sdk/README.md b/packages/agent-plugin-sdk/README.md
new file mode 100644
index 0000000..d56b26e
--- /dev/null
+++ b/packages/agent-plugin-sdk/README.md
@@ -0,0 +1,70 @@
+# @jalco/ap-sdk
+
+Write an agent plugin once, ship it to every harness.
+
+Each coding agent — Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, OpenCode — has its own plugin system, file layout, and frontmatter rules. `@jalco/ap-sdk` lets you define a plugin once in TypeScript and compile it to the native installable artifacts each harness expects.
+
+```ts
+import { definePlugin, defineSkill } from "@jalco/ap-sdk";
+
+export default definePlugin({
+ id: "git-helper",
+ description: "Helpers for reviewing and committing changes in a git repo.",
+ skills: [
+ defineSkill({
+ name: "diff-review",
+ description:
+ "Summarize and risk-flag uncommitted changes. Use when the user asks what changed.",
+ instructions:
+ "Run `git diff HEAD` and summarize the changes in 2-4 bullets.",
+ }),
+ ],
+});
+```
+
+```bash
+ap-sdk build # → .aps-out/{claude,codex,gemini,copilot,cursor,windsurf,pi,opencode}/
+ap-sdk install # → drops artifacts into your local harness dirs
+```
+
+## Install
+
+```bash
+pnpm add -D @jalco/ap-sdk
+```
+
+## What you can define
+
+- [Instructions](https://ap-sdk.dev/docs): always-on system or project guidance for every supported harness.
+- [Skills](https://ap-sdk.dev/docs): reusable skill documents with descriptions and instructions.
+- [Commands](https://ap-sdk.dev/docs): slash commands or prompt commands in harnesses that support them.
+- [Subagents](https://ap-sdk.dev/docs): named specialist agents with their own prompts, tools, and model hints.
+- [Hooks](https://ap-sdk.dev/docs): lifecycle hooks and scripts translated to each harness' event model.
+- [MCP servers](https://ap-sdk.dev/docs): MCP server config emitted in native JSON/TOML/YAML formats.
+- [Shared tools](https://ap-sdk.dev/docs): real TypeScript implementations with per-harness glue.
+- [Companion files](https://ap-sdk.dev/docs): files copied next to generated native artifacts.
+
+See the [support matrix](https://ap-sdk.dev/docs/harnesses) for what each harness supports.
+
+## Already have a plugin?
+
+Point `ap-sdk port` at an existing Claude Code, Cursor, Codex, Gemini, Copilot, Windsurf, Pi, or OpenCode layout and it generates a portable `plugin.ts` that loads your existing files instead of inlining them.
+
+```bash
+npx ap-sdk port ./my-plugin
+npx ap-sdk check ./plugin.ts
+npx ap-sdk install owner/repo
+```
+
+Read the [porting guide](https://ap-sdk.dev/docs/porting), or install straight from GitHub with `ap-sdk install owner/repo`.
+
+## Links
+
+- [Homepage](https://ap-sdk.dev)
+- [Docs](https://ap-sdk.dev/docs)
+- [GitHub](https://github.com/jal-co/agent-plugin-sdk)
+- [Examples](https://github.com/jal-co/agent-plugin-sdk/tree/main/packages/agent-plugin-sdk/examples)
+
+## License
+
+MIT · Originally by [Sahaj Jain](https://github.com/jnsahaj), continued by [Justin Levine](https://github.com/jal-co).
From 5e7e655808f8f535e9878d55ba4cef537c584da5 Mon Sep 17 00:00:00 2001
From: Justin Levine <20596508+jal-co@users.noreply.github.com>
Date: Wed, 1 Jul 2026 16:42:38 -0500
Subject: [PATCH 3/4] feat(docs): feature pages, porting hub, api reference,
registry, search + nav
Implements docs plans 003/004/009/010/011/012:
- seven Define pages (skills, commands, subagents, hooks, mcp, tools,
instructions-and-files) wired into the sidebar
- porting reframed as a harness-neutral hub with per-source subpages
matching exactly what src/port.ts detects
- generated API reference section (pnpm generate:api, runs on prebuild)
- search dialog (cmd-K) on fumadocs-core's useDocsSearch, prev/next
pagination, and edit-this-page links
- Callout + Tabs MDX components, mobile TOC + AI copy row below xl,
dead floating navbar variant removed
- /docs/plugins registry driven by the ap-sdk-plugin GitHub topic and
npm keyword, with filter/sort/channel badges and degraded/empty states
---
apps/docs/content/docs/api/harness.mdx | 44 ++++
apps/docs/content/docs/api/index.mdx | 13 +
apps/docs/content/docs/api/main.mdx | 84 +++++++
apps/docs/content/docs/api/meta.json | 4 +
apps/docs/content/docs/api/runtime.mdx | 36 +++
apps/docs/content/docs/commands.mdx | 45 ++++
apps/docs/content/docs/hooks.mdx | 53 ++++
apps/docs/content/docs/index.mdx | 7 +
.../content/docs/instructions-and-files.mdx | 37 +++
apps/docs/content/docs/mcp.mdx | 40 +++
apps/docs/content/docs/meta.json | 10 +
apps/docs/content/docs/porting.mdx | 158 ++----------
apps/docs/content/docs/porting/claude.mdx | 35 +++
apps/docs/content/docs/porting/codex.mdx | 32 +++
apps/docs/content/docs/porting/copilot.mdx | 32 +++
apps/docs/content/docs/porting/cursor.mdx | 33 +++
apps/docs/content/docs/porting/gemini.mdx | 32 +++
apps/docs/content/docs/porting/generic.mdx | 33 +++
apps/docs/content/docs/porting/meta.json | 14 ++
apps/docs/content/docs/porting/opencode.mdx | 32 +++
apps/docs/content/docs/porting/pi.mdx | 32 +++
apps/docs/content/docs/porting/windsurf.mdx | 33 +++
apps/docs/content/docs/skills.mdx | 47 ++++
apps/docs/content/docs/subagents.mdx | 44 ++++
apps/docs/content/docs/tools.mdx | 45 ++++
apps/docs/package.json | 4 +-
apps/docs/scripts/generate-api-docs.mjs | 142 +++++++++++
apps/docs/src/app/docs/[[...slug]]/page.tsx | 34 ++-
apps/docs/src/app/docs/layout.tsx | 2 +-
apps/docs/src/app/docs/plugins/page.tsx | 88 +++++++
apps/docs/src/components/callout.tsx | 77 ++++++
apps/docs/src/components/docs-pagination.tsx | 65 +++++
apps/docs/src/components/docs-toc.tsx | 41 +++-
apps/docs/src/components/mdx.tsx | 13 +
apps/docs/src/components/plugin-directory.tsx | 229 ++++++++++++++++++
apps/docs/src/components/search-dialog.tsx | 221 +++++++++++++++++
apps/docs/src/components/site-footer.tsx | 1 +
apps/docs/src/components/site-navbar.tsx | 93 +------
apps/docs/src/components/tabs.tsx | 118 +++++++++
apps/docs/src/lib/docs-nav.ts | 9 +-
apps/docs/src/lib/github-plugins.ts | 147 +++++++++++
41 files changed, 2034 insertions(+), 225 deletions(-)
create mode 100644 apps/docs/content/docs/api/harness.mdx
create mode 100644 apps/docs/content/docs/api/index.mdx
create mode 100644 apps/docs/content/docs/api/main.mdx
create mode 100644 apps/docs/content/docs/api/meta.json
create mode 100644 apps/docs/content/docs/api/runtime.mdx
create mode 100644 apps/docs/content/docs/commands.mdx
create mode 100644 apps/docs/content/docs/hooks.mdx
create mode 100644 apps/docs/content/docs/instructions-and-files.mdx
create mode 100644 apps/docs/content/docs/mcp.mdx
create mode 100644 apps/docs/content/docs/porting/claude.mdx
create mode 100644 apps/docs/content/docs/porting/codex.mdx
create mode 100644 apps/docs/content/docs/porting/copilot.mdx
create mode 100644 apps/docs/content/docs/porting/cursor.mdx
create mode 100644 apps/docs/content/docs/porting/gemini.mdx
create mode 100644 apps/docs/content/docs/porting/generic.mdx
create mode 100644 apps/docs/content/docs/porting/meta.json
create mode 100644 apps/docs/content/docs/porting/opencode.mdx
create mode 100644 apps/docs/content/docs/porting/pi.mdx
create mode 100644 apps/docs/content/docs/porting/windsurf.mdx
create mode 100644 apps/docs/content/docs/skills.mdx
create mode 100644 apps/docs/content/docs/subagents.mdx
create mode 100644 apps/docs/content/docs/tools.mdx
create mode 100644 apps/docs/scripts/generate-api-docs.mjs
create mode 100644 apps/docs/src/app/docs/plugins/page.tsx
create mode 100644 apps/docs/src/components/callout.tsx
create mode 100644 apps/docs/src/components/docs-pagination.tsx
create mode 100644 apps/docs/src/components/plugin-directory.tsx
create mode 100644 apps/docs/src/components/search-dialog.tsx
create mode 100644 apps/docs/src/components/tabs.tsx
create mode 100644 apps/docs/src/lib/github-plugins.ts
diff --git a/apps/docs/content/docs/api/harness.mdx b/apps/docs/content/docs/api/harness.mdx
new file mode 100644
index 0000000..174d74c
--- /dev/null
+++ b/apps/docs/content/docs/api/harness.mdx
@@ -0,0 +1,44 @@
+---
+title: Harness entrypoint
+description: Harness authoring toolkit for custom targets.
+---
+
+## defineHarness
+
+Identity helper for a custom target harness: support map, emit translator, install paths, and native config behavior.
+
+```ts
+export function defineHarness(harness: Harness): Harness;
+```
+
+## registerHarness
+
+Registers a custom harness id so build, install, and CLI target validation can use it.
+
+```ts
+export function registerHarness(harness: Harness): void;
+```
+
+## Harness
+
+The contract every built-in and custom harness implements: id, display name, support map, emit function, and install path helpers.
+
+```ts
+export interface Harness { id: HarnessId; displayName: string; supports: FeatureSupport; ... }
+```
+
+## EmitContext
+
+Context passed to emitters, including helper methods and target metadata.
+
+```ts
+export interface EmitContext { harness: Harness; ... }
+```
+
+## InstallScope
+
+Where install writes artifacts: project directories or home-directory global config.
+
+```ts
+export type InstallScope = 'project' | 'global';
+```
diff --git a/apps/docs/content/docs/api/index.mdx b/apps/docs/content/docs/api/index.mdx
new file mode 100644
index 0000000..2b0b447
--- /dev/null
+++ b/apps/docs/content/docs/api/index.mdx
@@ -0,0 +1,13 @@
+---
+title: API reference
+description: Generated reference for the public ap-sdk entrypoints.
+---
+
+This section is generated from source-aligned API metadata. Regenerate it with `pnpm --filter @jal-co/docs generate:api`.
+
+
+
+
+
+
+
diff --git a/apps/docs/content/docs/api/main.mdx b/apps/docs/content/docs/api/main.mdx
new file mode 100644
index 0000000..18ff518
--- /dev/null
+++ b/apps/docs/content/docs/api/main.mdx
@@ -0,0 +1,84 @@
+---
+title: Main entrypoint
+description: Public helpers and types exported by @jalco/ap-sdk.
+---
+
+## definePlugin
+
+Identity helper for a portable plugin definition. Author once, then build or install native artifacts for each harness.
+
+```ts
+export function definePlugin(plugin: Plugin): Plugin;
+```
+
+## defineSkill
+
+Defines a portable agent skill with a trigger description, instructions, optional resources, and harness metadata.
+
+```ts
+export function defineSkill(skill: Skill): Skill;
+```
+
+## defineCommand
+
+Defines a portable slash command or prompt command with argument templating and per-harness overrides.
+
+```ts
+export function defineCommand(command: Command): Command;
+```
+
+## defineSubagent
+
+Defines a specialist agent prompt with optional tools and target-specific model settings.
+
+```ts
+export function defineSubagent(subagent: Subagent): Subagent;
+```
+
+## defineHook
+
+Defines a lifecycle hook using ap-sdk's portable event names and optional native overrides.
+
+```ts
+export function defineHook(hook: Hook): Hook;
+```
+
+## Plugin
+
+The root declaration: id, description, instructions, skills, commands, MCP servers, subagents, hooks, files, tools, and marketplace metadata.
+
+```ts
+export interface Plugin { id: string; description: string; ... }
+```
+
+## Skill
+
+Reusable instructions plus metadata. Descriptions are capped to the tightest harness limit and drive skill routing.
+
+```ts
+export interface Skill { name: string; description: string; instructions: string; ... }
+```
+
+## Hook
+
+Portable hook event, matcher, command, timeout, async flag, and per-harness event or matcher overrides.
+
+```ts
+export interface Hook { event: HookEvent; command: string | HookCommand; ... }
+```
+
+## build
+
+Validates a plugin and returns in-memory output files and warnings for each requested harness.
+
+```ts
+export function build(plugin: Plugin, options?: BuildOptions): HarnessBuild[];
+```
+
+## installSkills
+
+Installs emitted skills, commands, MCP config, hooks, instructions, subagents, and files into live harness directories.
+
+```ts
+export function installSkills(plugin: Plugin, options?: InstallOptions): InstalledItem[];
+```
diff --git a/apps/docs/content/docs/api/meta.json b/apps/docs/content/docs/api/meta.json
new file mode 100644
index 0000000..a418fd9
--- /dev/null
+++ b/apps/docs/content/docs/api/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "API",
+ "pages": ["index", "main", "runtime", "harness"]
+}
diff --git a/apps/docs/content/docs/api/runtime.mdx b/apps/docs/content/docs/api/runtime.mdx
new file mode 100644
index 0000000..b58a7cc
--- /dev/null
+++ b/apps/docs/content/docs/api/runtime.mdx
@@ -0,0 +1,36 @@
+---
+title: Runtime entrypoint
+description: Runtime helpers for portable tools.
+---
+
+## defineTool
+
+Defines one executable tool with metadata, JSON schema parameters, and a single handler shared across harness adapters.
+
+```ts
+export function defineTool(tool: Tool): Tool;
+```
+
+## listTools
+
+Loads a tools module and returns the declared tool metadata for local inspection or generated adapters.
+
+```ts
+export async function listTools(modulePath: string): Promise;
+```
+
+## callTool
+
+Invokes a named tool locally with JSON arguments, matching the CLI's `ap-sdk tools --call` loop.
+
+```ts
+export async function callTool(modulePath: string, name: string, args: unknown): Promise;
+```
+
+## contentToText
+
+Converts structured tool content blocks into plain text for terminal output.
+
+```ts
+export function contentToText(content: ToolContent[]): string;
+```
diff --git a/apps/docs/content/docs/commands.mdx b/apps/docs/content/docs/commands.mdx
new file mode 100644
index 0000000..ef3dc5f
--- /dev/null
+++ b/apps/docs/content/docs/commands.mdx
@@ -0,0 +1,45 @@
+---
+title: Commands
+description: Define portable slash commands and prompt commands with argument templates.
+---
+
+Commands compile to each harness' native command or prompt file. Use them for repeatable workflows where the user explicitly invokes an action.
+
+```ts title="plugin.ts"
+import { defineCommand, definePlugin } from "@jalco/ap-sdk";
+
+export default definePlugin({
+ id: "issue-tools",
+ description: "Issue triage commands.",
+ commands: [
+ defineCommand({
+ name: "fix-issue",
+ description: "Fix a GitHub issue end to end.",
+ argumentHint: "",
+ allowedTools: ["Bash", "Read", "Edit"],
+ body: "Fix issue #$1. Use $ARGUMENTS as the raw user request.",
+ harness: { claude: { model: "sonnet" }, opencode: { model: "gpt-5" } },
+ }),
+ ],
+});
+```
+
+## Argument templates
+
+| Template | Meaning | Portability |
+| --- | --- | --- |
+| `$ARGUMENTS` | The complete argument string. | Portable everywhere. |
+| `$1`, `$2` | 1-based positional args in ap-sdk source. | Claude is rewritten to its 0-based native form. |
+| `` !`shell` `` | Native shell interpolation. | Passed through for Claude and OpenCode. |
+| `@file` | Native file reference. | Passed through for Claude and OpenCode. |
+
+## Fields
+
+- `name`, `description`, `argumentHint`, and `body` define the command.
+- `allowedTools` is honored by Claude Code.
+- `frontmatter` is honored by YAML-frontmatter harnesses; Gemini TOML and Cursor plain markdown ignore it.
+- `harness.claude.model`, `harness.opencode.model`, and `harness.copilot.model` / `agent` set native model hints.
+
+## Portability notes
+
+If a harness cannot represent command metadata, the command body still emits and the build reports a warning for the unsupported option. Next: [subagents](/docs/subagents) and the [support matrix](/docs/harnesses).
diff --git a/apps/docs/content/docs/hooks.mdx b/apps/docs/content/docs/hooks.mdx
new file mode 100644
index 0000000..36c2aaa
--- /dev/null
+++ b/apps/docs/content/docs/hooks.mdx
@@ -0,0 +1,53 @@
+---
+title: Hooks
+description: Translate lifecycle hooks and scripts across harness event models.
+---
+
+Hooks run commands around tool calls, prompts, sessions, and agent stops. ap-sdk uses a portable event vocabulary and translates native event spellings where a harness supports hooks.
+
+```ts title="plugin.ts"
+import { defineHook, definePlugin } from "@jalco/ap-sdk";
+
+export default definePlugin({
+ id: "plan-review",
+ description: "Review plans before execution.",
+ hooks: [
+ defineHook({
+ event: "pre-tool-use",
+ matcher: "ExitPlanMode",
+ command: { unix: "node hooks/plan-review.mjs", powershell: "node hooks/plan-review.mjs" },
+ timeout: 120,
+ async: true,
+ harness: { codex: { event: "stop", matcher: undefined } },
+ }),
+ ],
+});
+```
+
+## Portable events
+
+| ap-sdk event | Claude | Copilot | Gemini |
+| --- | --- | --- | --- |
+| `pre-tool-use` | `PreToolUse` | `preToolUse` | `BeforeTool` |
+| `post-tool-use` | `PostToolUse` | `postToolUse` | `AfterTool` |
+| `stop` | `Stop` | `stop` | `Stop` |
+| `user-prompt-submit` | `UserPromptSubmit` | `userPromptSubmit` | `UserPromptSubmit` |
+| `session-start` | `SessionStart` | `sessionStart` | `SessionStart` |
+| `notification` | `Notification` | `notification` | `Notification` |
+| `permission-request` | `PermissionRequest` | `permissionRequest` | `PermissionRequest` |
+| `subagent-stop` | `SubagentStop` | `subagentStop` | `SubagentStop` |
+| `pre-compact` | `PreCompact` | `preCompact` | `PreCompact` |
+| `session-end` | `SessionEnd` | `sessionEnd` | `SessionEnd` |
+
+## Fields
+
+- `event`, `matcher`, `command`, and `timeout` define the hook.
+- `async` is native in Claude and dropped with a warning elsewhere.
+- Object commands can provide `unix` and `powershell`; PowerShell is used by Copilot.
+- `harness..event` and `matcher` override native mapping. Use `matcher: undefined` to explicitly clear a matcher.
+
+
+Pi and OpenCode hooks are code-only today; builds note the limitation instead of emitting a misleading artifact.
+
+
+Next: [MCP servers](/docs/mcp) and the [support matrix](/docs/harnesses).
diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx
index 1d9f366..ca2c070 100644
--- a/apps/docs/content/docs/index.mdx
+++ b/apps/docs/content/docs/index.mdx
@@ -12,6 +12,9 @@ compiles it to the **native installable artifacts** each harness expects. No
runtime, no wrapper — the output is exactly the files those harnesses load on
their own.
+Already have a plugin for one harness? Start with [porting](/docs/porting)
+instead and let `ap-sdk port` generate the portable definition from your files.
+
```ts title="plugin.ts"
import { definePlugin, defineSkill } from "@jalco/ap-sdk";
@@ -62,6 +65,10 @@ Harnesses share a common feature set, but each also diverges. The SDK borrows
+
+
+
+
diff --git a/apps/docs/content/docs/instructions-and-files.mdx b/apps/docs/content/docs/instructions-and-files.mdx
new file mode 100644
index 0000000..ebe1194
--- /dev/null
+++ b/apps/docs/content/docs/instructions-and-files.mdx
@@ -0,0 +1,37 @@
+---
+title: Instructions and files
+description: Ship always-on instructions and companion files alongside generated artifacts.
+---
+
+A plugin can include global instructions plus arbitrary files. Instructions become native context files (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`) and installs merge them as id-keyed blocks; files are copied verbatim into build trees.
+
+```ts title="plugin.ts"
+import { definePlugin, defineSkill, readDir, readTextFrom } from "@jalco/ap-sdk";
+
+const read = readTextFrom(import.meta.url);
+
+export default definePlugin({
+ id: "repo-playbook",
+ description: "Repository-specific playbook.",
+ instructions: read("./AGENTS.md"),
+ skills: [
+ defineSkill({
+ name: "release-check",
+ description: "Use before preparing a release.",
+ instructions: read("./skills/release/SKILL.md"),
+ resources: [{ path: "checklist.md", content: read("./skills/release/checklist.md") }],
+ }),
+ ],
+ files: readDir("./hooks", import.meta.url, "hooks"),
+});
+```
+
+## Fields and helpers
+
+- `instructions` is always-on guidance. Install uses an id-keyed markdown block so re-installs update the SDK-managed section without clobbering foreign text.
+- `files` contains `PluginFile` entries emitted into every build tree; `executable` preserves script bits.
+- `SkillResource` files live inside a skill directory. `PluginFile` files live at the plugin root for scripts, templates, and shared assets.
+- `readText`, `readTextFrom`, and `readDir` keep content on disk and avoid giant inline strings.
+- Plugin-root variables such as `${CLAUDE_PLUGIN_ROOT}/hooks/notify.sh` let native configs reference emitted companion files.
+
+Next: [skills](/docs/skills), [porting](/docs/porting), and the [support matrix](/docs/harnesses).
diff --git a/apps/docs/content/docs/mcp.mdx b/apps/docs/content/docs/mcp.mdx
new file mode 100644
index 0000000..a2a6352
--- /dev/null
+++ b/apps/docs/content/docs/mcp.mdx
@@ -0,0 +1,40 @@
+---
+title: MCP servers
+description: Declare stdio and HTTP MCP servers once and emit native config.
+---
+
+MCP servers are configuration, not code. ap-sdk emits native MCP config for harnesses that support it and warns for those that do not.
+
+```ts title="plugin.ts"
+import { definePlugin } from "@jalco/ap-sdk";
+
+export default definePlugin({
+ id: "repo-context",
+ description: "Repository context MCP servers.",
+ mcpServers: {
+ github: {
+ command: "npx",
+ args: ["-y", "@modelcontextprotocol/server-github"],
+ env: { GITHUB_TOKEN: "${GITHUB_TOKEN}" },
+ },
+ docs: {
+ transport: "http",
+ url: "https://docs.example.com/mcp",
+ headers: { Authorization: "Bearer ${DOCS_TOKEN}" },
+ },
+ },
+});
+```
+
+## Fields
+
+- Stdio servers use `command`, `args`, `env`, and `cwd`.
+- HTTP servers use `transport: "http"`, `url`, and `headers`.
+- Claude and Codex receive `.mcp.json`.
+- OpenCode receives `opencode.json` under the `mcp` key.
+
+
+Pi builds emit a warning for MCP servers. The declaration still works for other targets in the same build.
+
+
+Next: [tools](/docs/tools) and the [support matrix](/docs/harnesses).
diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json
index f77e6e7..cd53dce 100644
--- a/apps/docs/content/docs/meta.json
+++ b/apps/docs/content/docs/meta.json
@@ -6,11 +6,21 @@
"index",
"installation",
"quickstart",
+ "---Define---",
+ "skills",
+ "commands",
+ "subagents",
+ "hooks",
+ "mcp",
+ "tools",
+ "instructions-and-files",
"---Guides---",
"installing-plugins",
"porting",
"harnesses",
"authoring-a-harness",
+ "---Reference---",
+ "api",
"---Project---",
"origins"
]
diff --git a/apps/docs/content/docs/porting.mdx b/apps/docs/content/docs/porting.mdx
index 498189a..ffa8c85 100644
--- a/apps/docs/content/docs/porting.mdx
+++ b/apps/docs/content/docs/porting.mdx
@@ -1,168 +1,58 @@
---
-title: Port a Claude Code plugin
-description: Turn an existing Claude Code plugin into one portable definePlugin that ships to every harness.
+title: Port an existing plugin
+description: Turn a Claude Code plugin, Cursor rules, Copilot prompts, or another native layout into one portable plugin.ts.
---
-Already have a Claude Code plugin — a `.claude-plugin/`, `agents/`, `commands/`,
-`hooks/`, skills, and companion files? Port it **once** into a portable
-`definePlugin` and compile it to Claude Code, Codex, Gemini CLI, Copilot, Cursor,
-Windsurf, Pi, and OpenCode. You keep your files on disk; the plugin definition
-just points at them.
+Already have a plugin or rules folder for one harness? Port it **once** into a portable `definePlugin` and compile it to Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode. You keep your files on disk; the generated plugin definition points at them.
## The fast path: `ap-sdk port`
-Don't write it by hand — point the CLI at your existing plugin and it generates a
-`plugin.ts` that **loads your files** (never inlines). It auto-detects the source
-layout (Claude Code, Codex, Gemini, Copilot, Cursor, Windsurf, OpenCode, Pi, or a
-generic `skills/` + `commands/` + `agents/` tree):
-
```bash
npx ap-sdk port ./my-plugin # writes ./my-plugin/plugin.ts
npx ap-sdk port ./my-plugin --dry-run # preview it first
```
-It maps the manifest, instruction file, `**/SKILL.md` skills, commands, agents
-(with their `model` and extra frontmatter), and hooks (native event names →
-portable ones), and ships companion directories with `readDir`. Then:
+`port` detects the source layout, reads manifests, instruction files, `SKILL.md` skills, commands, agents, hooks, and companion directories, then writes a portable `plugin.ts` that loads files with `readTextFrom`, `readBodyFrom`, and `readDir`.
```bash compact
-npx ap-sdk check plugin.ts # validate
-npx ap-sdk build plugin.ts # emit every harness
+npx ap-sdk check plugin.ts
+npx ap-sdk build plugin.ts
```
-Review the result and fill in anything the generator flagged with a `TODO`. The
-rest of this page explains the mapping it applies, for when you want to tweak by
-hand.
+Review any generated `TODO` comments. Unmapped native hook events are flagged in code so you can choose the right portable event.
-## The mapping
+## Source layouts
-| Claude Code | ap-sdk |
-| --- | --- |
-| `.claude-plugin/plugin.json` | `id`, `description`, `version`, `author`, `homepage`, `license`, `marketplace` |
-| `CLAUDE.md` | `instructions` |
-| skills (`SKILL.md`) | `defineSkill` (extra frontmatter → `frontmatter`) |
-| `agents/*.md` | `defineSubagent` (`model` → `harness.claude.model`; extra fields → `frontmatter`) |
-| `commands/*.md` | `defineCommand` (extra frontmatter → `frontmatter`) |
-| `hooks/hooks.json` | `defineHook` (events incl. `notification`, `permission-request`; `async` preserved) |
-| `hooks/*.sh`, `*.py`, `doctrine/`, `docs/` | `files` (companion files, via `readDir`) |
+| Source layout | Detector looks for | Guide |
+| --- | --- | --- |
+| Claude Code plugin | `.claude-plugin/plugin.json`, `CLAUDE.md`, `skills/**/SKILL.md`, `commands/`, `agents/`, `hooks/` | [Claude Code](/docs/porting/claude) |
+| Gemini CLI extension | `gemini-extension.json`, `GEMINI.md`, `.gemini/agents` | [Gemini CLI](/docs/porting/gemini) |
+| OpenCode plugin | `opencode.json` plus generic content folders | [OpenCode](/docs/porting/opencode) |
+| GitHub Copilot | `.github/copilot-instructions.md`, `.github/prompts`, `.github/agents` | [Copilot](/docs/porting/copilot) |
+| Cursor | `.cursor` plus generic content folders | [Cursor](/docs/porting/cursor) |
+| Windsurf | `.windsurf` plus generic content folders | [Windsurf](/docs/porting/windsurf) |
+| Codex | `.codex` or `.agents`, `.codex/prompts` | [Codex](/docs/porting/codex) |
+| Pi | `.pi` plus generic content folders | [Pi](/docs/porting/pi) |
+| Generic tree | `skills/`, `commands/`, `agents/`, instruction files | [Generic tree](/docs/porting/generic) |
## Keep your files on disk
-Instead of inlining prose, load it — `readText` for a single file,
-`readTextFrom` to bind a base, `readDir` to ship a whole folder. Resolve paths
-relative to the plugin file with `import.meta.url`:
-
```ts title="plugin.ts"
-import {
- definePlugin,
- defineSkill,
- defineSubagent,
- defineCommand,
- defineHook,
- readText,
- readTextFrom,
- readDir,
-} from "@jalco/ap-sdk";
+import { definePlugin, readDir, readTextFrom } from "@jalco/ap-sdk";
const read = readTextFrom(import.meta.url);
export default definePlugin({
id: "alp-river",
description: "Multi-step agent refinement pipeline.",
- version: "1.3.2",
- author: { name: "Alper Ortac" },
instructions: read("./CLAUDE.md"),
+ files: readDir("./hooks", import.meta.url, "hooks"),
});
```
-## Skills, agents, commands
-
-Load each body from its file, and use the `frontmatter` escape hatch for native
-fields the SDK doesn't model (an agent's `effort`, a nested `stage:` block):
-
-```ts
-skills: [
- defineSkill({
- name: "diff-review",
- description: "Summarize and risk-flag uncommitted changes.",
- instructions: read("./skills/diff-review/SKILL.md"),
- }),
-],
-subagents: [
- defineSubagent({
- name: "triage",
- description: "Classify the task and route it.",
- prompt: read("./agents/triage.md"),
- tools: ["Read", "Grep", "Bash"],
- harness: { claude: { model: "sonnet" } },
- frontmatter: { effort: "high", stage: { routes: ["code"] } },
- }),
-],
-commands: [
- defineCommand({
- name: "run",
- description: "Run the pipeline.",
- body: read("./commands/run.md"),
- }),
-],
-```
-
-The known fields always win a clash, so passthrough only *adds* frontmatter — it
-can't accidentally overwrite `name` or `description`.
-
-## Hooks and their scripts
-
-Hook events map to every harness's native name — including `notification` and
-`permission-request`. Ship the scripts your hooks call as **companion files**,
-and reference them via the harness's plugin-root variable:
-
-```ts
-hooks: [
- defineHook({
- event: "session-start",
- command: "${CLAUDE_PLUGIN_ROOT}/hooks/inject-workflow.sh",
- timeout: 5,
- }),
- defineHook({
- event: "notification",
- command: "${CLAUDE_PLUGIN_ROOT}/hooks/notify.sh",
- async: true, // fire-and-forget; Claude runs it without blocking the turn
- }),
-],
-// Ship the hook scripts + reference docs the instructions point at.
-files: [
- ...readDir("./hooks", import.meta.url, "hooks"),
- ...readDir("./doctrine", import.meta.url, "doctrine"),
-],
-```
-
-`readDir` preserves the executable bit, so your `.sh`/`.py` hooks stay runnable.
-A harness with no native form for an event (or for hooks at all) drops just that
-piece with a [warning](/docs/harnesses#what-happens-when-a-feature-isnt-supported),
-never a broken file. `async` is emitted where the harness models it natively
-(Claude Code) and otherwise dropped with a warning — the hook still runs, just
-synchronously within its timeout.
-
-## Build and review the gaps
-
-```bash compact
-npx ap-sdk check # validate the definition
-npx ap-sdk build # emit every harness tree under .aps-out/
-```
-
-`build` prints a warning for anything a target couldn't represent — read them to
-see exactly what each non-Claude harness dropped, and use each feature's
-`harness` override to bridge where it matters. The [support matrix](/docs/harnesses)
-shows the capabilities at a glance.
-
-## Let an agent do it
-
-Porting is mechanical, so it's a great job for an agent. Point your coding agent
-at the **Port a Claude Code plugin** skill (in the repo's `skills/`), which walks
-it through inspecting the plugin tree and generating a `plugin.ts` that loads your
-files.
+A harness with no native form for a feature drops just that piece with a [warning](/docs/harnesses#what-happens-when-a-feature-isnt-supported), never a broken file.
-
-
+
+
diff --git a/apps/docs/content/docs/porting/claude.mdx b/apps/docs/content/docs/porting/claude.mdx
new file mode 100644
index 0000000..9b5a3a2
--- /dev/null
+++ b/apps/docs/content/docs/porting/claude.mdx
@@ -0,0 +1,35 @@
+---
+title: Port a Claude Code plugin to every harness
+description: Convert a Claude Code layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode.
+---
+
+The detector recognizes this source when it finds `.claude-plugin/plugin.json`, `CLAUDE.md`, `skills/**/SKILL.md`, `commands/*.md`, `agents/*.md`, and `hooks/hooks.json`.
+
+```bash
+npx ap-sdk port ./my-plugin
+npx ap-sdk port ./my-plugin --dry-run
+```
+
+## Mapping
+
+| Claude Code source | ap-sdk output |
+| --- | --- |
+| `.claude-plugin/plugin.json` | plugin id, description, version, author |
+| `CLAUDE.md` | `instructions` |
+| `skills/**/SKILL.md` | `defineSkill` |
+| `commands/*.md` | `defineCommand` |
+| `agents/*.md` | `defineSubagent` |
+| `hooks/hooks.json` | `defineHook` |
+
+## What does not carry over
+
+The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`.
+
+## Next steps
+
+```bash compact
+npx ap-sdk check plugin.ts
+npx ap-sdk build plugin.ts
+```
+
+Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use.
diff --git a/apps/docs/content/docs/porting/codex.mdx b/apps/docs/content/docs/porting/codex.mdx
new file mode 100644
index 0000000..8990213
--- /dev/null
+++ b/apps/docs/content/docs/porting/codex.mdx
@@ -0,0 +1,32 @@
+---
+title: Port a Codex plugin to every harness
+description: Convert a Codex layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode.
+---
+
+The detector recognizes this source when it finds `.codex` or `.agents`, plus `.codex/prompts` for prompt commands.
+
+```bash
+npx ap-sdk port ./my-plugin
+npx ap-sdk port ./my-plugin --dry-run
+```
+
+## Mapping
+
+| Codex source | ap-sdk output |
+| --- | --- |
+| `.codex/prompts/*.md` | `defineCommand` |
+| `.agents` / `agents/*.md` | `defineSubagent` |
+| `AGENTS.md` | `instructions` |
+
+## What does not carry over
+
+The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`.
+
+## Next steps
+
+```bash compact
+npx ap-sdk check plugin.ts
+npx ap-sdk build plugin.ts
+```
+
+Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use.
diff --git a/apps/docs/content/docs/porting/copilot.mdx b/apps/docs/content/docs/porting/copilot.mdx
new file mode 100644
index 0000000..e80501b
--- /dev/null
+++ b/apps/docs/content/docs/porting/copilot.mdx
@@ -0,0 +1,32 @@
+---
+title: Port a GitHub Copilot plugin to every harness
+description: Convert a GitHub Copilot layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode.
+---
+
+The detector recognizes this source when it finds `.github/copilot-instructions.md`, `.github/prompts`, and `.github/agents`.
+
+```bash
+npx ap-sdk port ./my-plugin
+npx ap-sdk port ./my-plugin --dry-run
+```
+
+## Mapping
+
+| GitHub Copilot source | ap-sdk output |
+| --- | --- |
+| `.github/copilot-instructions.md` | `instructions` |
+| `.github/prompts/*.prompt.md` | `defineCommand` |
+| `.github/agents/*.agent.md` | `defineSubagent` |
+
+## What does not carry over
+
+The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`.
+
+## Next steps
+
+```bash compact
+npx ap-sdk check plugin.ts
+npx ap-sdk build plugin.ts
+```
+
+Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use.
diff --git a/apps/docs/content/docs/porting/cursor.mdx b/apps/docs/content/docs/porting/cursor.mdx
new file mode 100644
index 0000000..81974f3
--- /dev/null
+++ b/apps/docs/content/docs/porting/cursor.mdx
@@ -0,0 +1,33 @@
+---
+title: Port a Cursor plugin to every harness
+description: Convert a Cursor layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode.
+---
+
+The detector recognizes this source when it finds `.cursor` plus shared `skills/`, `commands/`, `agents/`, hooks, and instruction files.
+
+```bash
+npx ap-sdk port ./my-plugin
+npx ap-sdk port ./my-plugin --dry-run
+```
+
+## Mapping
+
+| Cursor source | ap-sdk output |
+| --- | --- |
+| `.cursor` | layout detection |
+| `skills/**/SKILL.md` | `defineSkill` |
+| `commands/*.md` | `defineCommand` |
+| `agents/*.md` | `defineSubagent` |
+
+## What does not carry over
+
+The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`.
+
+## Next steps
+
+```bash compact
+npx ap-sdk check plugin.ts
+npx ap-sdk build plugin.ts
+```
+
+Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use.
diff --git a/apps/docs/content/docs/porting/gemini.mdx b/apps/docs/content/docs/porting/gemini.mdx
new file mode 100644
index 0000000..4bb7181
--- /dev/null
+++ b/apps/docs/content/docs/porting/gemini.mdx
@@ -0,0 +1,32 @@
+---
+title: Port a Gemini CLI plugin to every harness
+description: Convert a Gemini CLI layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode.
+---
+
+The detector recognizes this source when it finds `gemini-extension.json`, `GEMINI.md`, and `.gemini/agents/*.md`.
+
+```bash
+npx ap-sdk port ./my-plugin
+npx ap-sdk port ./my-plugin --dry-run
+```
+
+## Mapping
+
+| Gemini CLI source | ap-sdk output |
+| --- | --- |
+| `gemini-extension.json` | plugin id, description, version, author |
+| `GEMINI.md` | `instructions` |
+| `.gemini/agents/*.md` | `defineSubagent` |
+
+## What does not carry over
+
+The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`.
+
+## Next steps
+
+```bash compact
+npx ap-sdk check plugin.ts
+npx ap-sdk build plugin.ts
+```
+
+Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use.
diff --git a/apps/docs/content/docs/porting/generic.mdx b/apps/docs/content/docs/porting/generic.mdx
new file mode 100644
index 0000000..b3371e5
--- /dev/null
+++ b/apps/docs/content/docs/porting/generic.mdx
@@ -0,0 +1,33 @@
+---
+title: Port a generic tree plugin to every harness
+description: Convert a generic tree layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode.
+---
+
+The detector recognizes this source when it finds no native marker; falls back to `skills/`, `commands/`, `agents/`, hooks, and instruction files.
+
+```bash
+npx ap-sdk port ./my-plugin
+npx ap-sdk port ./my-plugin --dry-run
+```
+
+## Mapping
+
+| generic tree source | ap-sdk output |
+| --- | --- |
+| `skills/**/SKILL.md` | `defineSkill` |
+| `commands/*.md` | `defineCommand` |
+| `agents/*.md` | `defineSubagent` |
+| `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` | `instructions` |
+
+## What does not carry over
+
+The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`.
+
+## Next steps
+
+```bash compact
+npx ap-sdk check plugin.ts
+npx ap-sdk build plugin.ts
+```
+
+Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use.
diff --git a/apps/docs/content/docs/porting/meta.json b/apps/docs/content/docs/porting/meta.json
new file mode 100644
index 0000000..53857de
--- /dev/null
+++ b/apps/docs/content/docs/porting/meta.json
@@ -0,0 +1,14 @@
+{
+ "title": "Porting",
+ "pages": [
+ "claude",
+ "codex",
+ "gemini",
+ "copilot",
+ "cursor",
+ "windsurf",
+ "opencode",
+ "pi",
+ "generic"
+ ]
+}
diff --git a/apps/docs/content/docs/porting/opencode.mdx b/apps/docs/content/docs/porting/opencode.mdx
new file mode 100644
index 0000000..91fcb71
--- /dev/null
+++ b/apps/docs/content/docs/porting/opencode.mdx
@@ -0,0 +1,32 @@
+---
+title: Port a OpenCode plugin to every harness
+description: Convert a OpenCode layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode.
+---
+
+The detector recognizes this source when it finds `opencode.json` plus shared content folders.
+
+```bash
+npx ap-sdk port ./my-plugin
+npx ap-sdk port ./my-plugin --dry-run
+```
+
+## Mapping
+
+| OpenCode source | ap-sdk output |
+| --- | --- |
+| `opencode.json` | plugin id, description, version, author |
+| `AGENTS.md` | `instructions` |
+| `commands/*.md` | `defineCommand` |
+
+## What does not carry over
+
+The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`.
+
+## Next steps
+
+```bash compact
+npx ap-sdk check plugin.ts
+npx ap-sdk build plugin.ts
+```
+
+Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use.
diff --git a/apps/docs/content/docs/porting/pi.mdx b/apps/docs/content/docs/porting/pi.mdx
new file mode 100644
index 0000000..c6448c8
--- /dev/null
+++ b/apps/docs/content/docs/porting/pi.mdx
@@ -0,0 +1,32 @@
+---
+title: Port a Pi plugin to every harness
+description: Convert a Pi layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode.
+---
+
+The detector recognizes this source when it finds `.pi` plus shared content folders and `AGENTS.md`.
+
+```bash
+npx ap-sdk port ./my-plugin
+npx ap-sdk port ./my-plugin --dry-run
+```
+
+## Mapping
+
+| Pi source | ap-sdk output |
+| --- | --- |
+| `.pi` | layout detection |
+| `AGENTS.md` | `instructions` |
+| `skills/**/SKILL.md` | `defineSkill` |
+
+## What does not carry over
+
+The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`.
+
+## Next steps
+
+```bash compact
+npx ap-sdk check plugin.ts
+npx ap-sdk build plugin.ts
+```
+
+Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use.
diff --git a/apps/docs/content/docs/porting/windsurf.mdx b/apps/docs/content/docs/porting/windsurf.mdx
new file mode 100644
index 0000000..2dc516a
--- /dev/null
+++ b/apps/docs/content/docs/porting/windsurf.mdx
@@ -0,0 +1,33 @@
+---
+title: Port a Windsurf plugin to every harness
+description: Convert a Windsurf layout into one portable plugin for Claude Code, Codex, Gemini CLI, Copilot, Cursor, Windsurf, Pi, and OpenCode.
+---
+
+The detector recognizes this source when it finds `.windsurf` plus shared `skills/`, `commands/`, `agents/`, hooks, and instruction files.
+
+```bash
+npx ap-sdk port ./my-plugin
+npx ap-sdk port ./my-plugin --dry-run
+```
+
+## Mapping
+
+| Windsurf source | ap-sdk output |
+| --- | --- |
+| `.windsurf` | layout detection |
+| `skills/**/SKILL.md` | `defineSkill` |
+| `commands/*.md` | `defineCommand` |
+| `agents/*.md` | `defineSubagent` |
+
+## What does not carry over
+
+The generator only claims what `src/port.ts` can read: manifests, instruction files, `SKILL.md` skills, markdown commands, markdown agents, Claude-style hook JSON, hook script folders, and non-structural companion directories. Native hook events without a portable mapping are emitted as `stop` with a `TODO` comment so you can choose the right event. Harness-specific settings outside those files remain in your source tree but are not modeled unless you add them to the generated `plugin.ts`.
+
+## Next steps
+
+```bash compact
+npx ap-sdk check plugin.ts
+npx ap-sdk build plugin.ts
+```
+
+Then review the [support matrix](/docs/harnesses) and install the plugin into the harnesses you use.
diff --git a/apps/docs/content/docs/skills.mdx b/apps/docs/content/docs/skills.mdx
new file mode 100644
index 0000000..284a253
--- /dev/null
+++ b/apps/docs/content/docs/skills.mdx
@@ -0,0 +1,47 @@
+---
+title: Skills
+description: Define reusable skill documents once and emit native skill folders for every harness.
+---
+
+Skills are reusable instructions with a short trigger description. The SDK emits each skill into the native skill format a harness understands and warns when a harness cannot represent an option.
+
+```ts title="plugin.ts"
+import { definePlugin, defineSkill } from "@jalco/ap-sdk";
+
+export default definePlugin({
+ id: "review-helper",
+ description: "Git review workflows.",
+ skills: [
+ defineSkill({
+ name: "diff-review",
+ description:
+ "Use when the user asks what changed or wants risks before committing.",
+ instructions: "Run `git diff HEAD`, summarize changes, then list risks.",
+ allowedTools: ["Bash", "Read", "Grep"],
+ resources: [{ path: "checklist.md", content: "# Review checklist
+" }],
+ frontmatter: { color: "blue" },
+ }),
+ ],
+});
+```
+
+## Fields
+
+- `name` — kebab-case id, max 64 characters.
+- `description` — max 1024 characters, the tightest harness limit; front-load trigger conditions because agents use this to decide when to load the skill.
+- `instructions` — the skill body.
+- `allowedTools` — honored by Claude Code and Pi.
+- `disableModelInvocation` — Pi-specific.
+- `license` — emitted where native skill metadata supports it.
+- `metadata` — honored by OpenCode and Pi.
+- `frontmatter` — escape hatch merged into YAML-frontmatter harnesses; SDK-owned fields win on conflicts.
+- `resources` — extra files emitted inside the skill directory.
+
+
+Keep descriptions concise and action-oriented. The first sentence should say when to use the skill.
+
+
+## Portability notes
+
+Unsupported options become structured build warnings; they are not silently dropped. See the [support matrix](/docs/harnesses), then continue with [commands](/docs/commands).
diff --git a/apps/docs/content/docs/subagents.mdx b/apps/docs/content/docs/subagents.mdx
new file mode 100644
index 0000000..b422981
--- /dev/null
+++ b/apps/docs/content/docs/subagents.mdx
@@ -0,0 +1,44 @@
+---
+title: Subagents
+description: Define specialist agents with prompts, tools, and per-harness model hints.
+---
+
+Subagents package a focused prompt under a name so the primary agent can delegate. Claude and OpenCode receive markdown agents, Codex receives developer instructions, and unsupported harnesses emit warnings.
+
+```ts title="plugin.ts"
+import { definePlugin, defineSubagent } from "@jalco/ap-sdk";
+
+export default definePlugin({
+ id: "review-team",
+ description: "Specialist review agents.",
+ subagents: [
+ defineSubagent({
+ name: "security-reviewer",
+ description: "Reviews diffs for auth, secrets, and injection risks.",
+ prompt: "You are a security reviewer. Lead with blockers.",
+ tools: ["Read", "Grep", "Bash"],
+ harness: {
+ claude: { model: "sonnet" },
+ codex: { model: "gpt-5" },
+ opencode: { model: "gpt-5", mode: "subagent" },
+ gemini: { model: "gemini-3-pro", temperature: 0.2, maxTurns: 8 },
+ copilot: { model: "gpt-5" },
+ },
+ }),
+ ],
+});
+```
+
+## Fields
+
+- `name` and `description` identify and route the agent.
+- `prompt` maps to Claude/OpenCode body text and Codex `developer_instructions`.
+- `tools` is honored by Claude Code.
+- `frontmatter` is emitted for YAML-frontmatter harnesses; Codex TOML ignores it.
+- `harness` sets target-specific model and behavior hints, including Gemini `temperature` / `maxTurns` and OpenCode `mode` (`primary`, `subagent`, `all`).
+
+
+Pi has no native subagent artifact, so builds for Pi report a structured warning rather than inventing a broken file.
+
+
+Next: [hooks](/docs/hooks) and the [support matrix](/docs/harnesses).
diff --git a/apps/docs/content/docs/tools.mdx b/apps/docs/content/docs/tools.mdx
new file mode 100644
index 0000000..c15b554
--- /dev/null
+++ b/apps/docs/content/docs/tools.mdx
@@ -0,0 +1,45 @@
+---
+title: Tools
+description: Author one TypeScript tool module and generate harness glue around it.
+---
+
+Tools let you keep real implementation code in one `tools.ts` module. The build copies the module into every output and generates the glue each harness needs: MCP server wiring for Claude/Codex and native tool/plugin glue for Pi/OpenCode.
+
+```ts title="tools.ts"
+import { defineTool } from "@jalco/ap-sdk/runtime";
+
+export default [
+ defineTool({
+ name: "echo",
+ description: "Echo text back for smoke tests.",
+ inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
+ async execute(input: { text: string }) {
+ return { content: [{ type: "text", text: input.text }] };
+ },
+ }),
+];
+```
+
+```ts title="plugin.ts"
+import { definePlugin } from "@jalco/ap-sdk";
+
+export default definePlugin({
+ id: "echo-tool",
+ description: "Tool smoke test.",
+ tools: { module: "./tools.ts", names: ["echo"] },
+});
+```
+
+## Local loop
+
+```bash
+ap-sdk tools --call echo --args '{"text":"hello"}'
+```
+
+## Fields
+
+- `tools.module` points at the TypeScript module.
+- `tools.names` limits or documents the exported tools.
+- `defineTool` describes the name, description, input schema, and execute function.
+
+See `packages/agent-plugin-sdk/examples/echo-tool/` and `examples/planreview/` for working projects. Next: [instructions and files](/docs/instructions-and-files) and the [support matrix](/docs/harnesses).
diff --git a/apps/docs/package.json b/apps/docs/package.json
index c2e278f..37ac62a 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -9,7 +9,9 @@
"typecheck": "fumadocs-mdx && next typegen && tsc --noEmit",
"postinstall": "fumadocs-mdx",
"lint": "biome check",
- "format": "biome format --write"
+ "format": "biome format --write",
+ "generate:api": "node scripts/generate-api-docs.mjs",
+ "prebuild": "pnpm generate:api"
},
"dependencies": {
"@jalco/ap-sdk": "workspace:*",
diff --git a/apps/docs/scripts/generate-api-docs.mjs b/apps/docs/scripts/generate-api-docs.mjs
new file mode 100644
index 0000000..c9686f4
--- /dev/null
+++ b/apps/docs/scripts/generate-api-docs.mjs
@@ -0,0 +1,142 @@
+import { mkdirSync, writeFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+const outDir = resolve(process.cwd(), "content/docs/api");
+mkdirSync(outDir, { recursive: true });
+
+const groups = [
+ {
+ slug: "main",
+ title: "Main entrypoint",
+ description: "Public helpers and types exported by @jalco/ap-sdk.",
+ symbols: [
+ [
+ "definePlugin",
+ "Identity helper for a portable plugin definition. Author once, then build or install native artifacts for each harness.",
+ "export function definePlugin(plugin: Plugin): Plugin;",
+ ],
+ [
+ "defineSkill",
+ "Defines a portable agent skill with a trigger description, instructions, optional resources, and harness metadata.",
+ "export function defineSkill(skill: Skill): Skill;",
+ ],
+ [
+ "defineCommand",
+ "Defines a portable slash command or prompt command with argument templating and per-harness overrides.",
+ "export function defineCommand(command: Command): Command;",
+ ],
+ [
+ "defineSubagent",
+ "Defines a specialist agent prompt with optional tools and target-specific model settings.",
+ "export function defineSubagent(subagent: Subagent): Subagent;",
+ ],
+ [
+ "defineHook",
+ "Defines a lifecycle hook using ap-sdk's portable event names and optional native overrides.",
+ "export function defineHook(hook: Hook): Hook;",
+ ],
+ [
+ "Plugin",
+ "The root declaration: id, description, instructions, skills, commands, MCP servers, subagents, hooks, files, tools, and marketplace metadata.",
+ "export interface Plugin { id: string; description: string; ... }",
+ ],
+ [
+ "Skill",
+ "Reusable instructions plus metadata. Descriptions are capped to the tightest harness limit and drive skill routing.",
+ "export interface Skill { name: string; description: string; instructions: string; ... }",
+ ],
+ [
+ "Hook",
+ "Portable hook event, matcher, command, timeout, async flag, and per-harness event or matcher overrides.",
+ "export interface Hook { event: HookEvent; command: string | HookCommand; ... }",
+ ],
+ [
+ "build",
+ "Validates a plugin and returns in-memory output files and warnings for each requested harness.",
+ "export function build(plugin: Plugin, options?: BuildOptions): HarnessBuild[];",
+ ],
+ [
+ "installSkills",
+ "Installs emitted skills, commands, MCP config, hooks, instructions, subagents, and files into live harness directories.",
+ "export function installSkills(plugin: Plugin, options?: InstallOptions): InstalledItem[];",
+ ],
+ ],
+ },
+ {
+ slug: "runtime",
+ title: "Runtime entrypoint",
+ description: "Runtime helpers for portable tools.",
+ symbols: [
+ [
+ "defineTool",
+ "Defines one executable tool with metadata, JSON schema parameters, and a single handler shared across harness adapters.",
+ "export function defineTool(tool: Tool): Tool;",
+ ],
+ [
+ "listTools",
+ "Loads a tools module and returns the declared tool metadata for local inspection or generated adapters.",
+ "export async function listTools(modulePath: string): Promise;",
+ ],
+ [
+ "callTool",
+ "Invokes a named tool locally with JSON arguments, matching the CLI's `ap-sdk tools --call` loop.",
+ "export async function callTool(modulePath: string, name: string, args: unknown): Promise;",
+ ],
+ [
+ "contentToText",
+ "Converts structured tool content blocks into plain text for terminal output.",
+ "export function contentToText(content: ToolContent[]): string;",
+ ],
+ ],
+ },
+ {
+ slug: "harness",
+ title: "Harness entrypoint",
+ description: "Harness authoring toolkit for custom targets.",
+ symbols: [
+ [
+ "defineHarness",
+ "Identity helper for a custom target harness: support map, emit translator, install paths, and native config behavior.",
+ "export function defineHarness(harness: Harness): Harness;",
+ ],
+ [
+ "registerHarness",
+ "Registers a custom harness id so build, install, and CLI target validation can use it.",
+ "export function registerHarness(harness: Harness): void;",
+ ],
+ [
+ "Harness",
+ "The contract every built-in and custom harness implements: id, display name, support map, emit function, and install path helpers.",
+ "export interface Harness { id: HarnessId; displayName: string; supports: FeatureSupport; ... }",
+ ],
+ [
+ "EmitContext",
+ "Context passed to emitters, including helper methods and target metadata.",
+ "export interface EmitContext { harness: Harness; ... }",
+ ],
+ [
+ "InstallScope",
+ "Where install writes artifacts: project directories or home-directory global config.",
+ "export type InstallScope = 'project' | 'global';",
+ ],
+ ],
+ },
+];
+
+for (const group of groups) {
+ const body = group.symbols
+ .map(
+ ([name, description, signature]) =>
+ `## ${name}\n\n${description}\n\n\`\`\`ts\n${signature}\n\`\`\``,
+ )
+ .join("\n\n");
+ writeFileSync(
+ resolve(outDir, `${group.slug}.mdx`),
+ `---\ntitle: ${group.title}\ndescription: ${group.description}\n---\n\n${body}\n`,
+ );
+}
+
+writeFileSync(
+ resolve(outDir, "index.mdx"),
+ `---\ntitle: API reference\ndescription: Generated reference for the public ap-sdk entrypoints.\n---\n\nThis section is generated from source-aligned API metadata. Regenerate it with \`pnpm --filter @jal-co/docs generate:api\`.\n\n\n \n \n \n \n \n`,
+);
diff --git a/apps/docs/src/app/docs/[[...slug]]/page.tsx b/apps/docs/src/app/docs/[[...slug]]/page.tsx
index ac55955..6419ceb 100644
--- a/apps/docs/src/app/docs/[[...slug]]/page.tsx
+++ b/apps/docs/src/app/docs/[[...slug]]/page.tsx
@@ -2,7 +2,8 @@ import { getGithubLastEdit } from "fumadocs-core/content/github";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { AiCopyButton } from "@/components/ai-copy-button";
-import { DocsToc } from "@/components/docs-toc";
+import { DocsPagination } from "@/components/docs-pagination";
+import { DocsToc, MobileDocsToc } from "@/components/docs-toc";
import { getMDXComponents } from "@/components/mdx";
import { gitConfig } from "@/lib/shared";
import { getLLMText, getPageImage, source } from "@/lib/source";
@@ -34,6 +35,7 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
const toc = page.data.toc;
const pageText = await getLLMText(page);
const lastEdit = await getLastEdit(page.path);
+ const editUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}/edit/${gitConfig.branch}/apps/docs/content/docs/${page.path}`;
return (
@@ -50,9 +52,31 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
) : null}
+
+
+
+
@@ -80,6 +104,14 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
)}
) : null}
+
+ Edit this page on GitHub
+
diff --git a/apps/docs/src/app/docs/layout.tsx b/apps/docs/src/app/docs/layout.tsx
index 5392d6f..49e7713 100644
--- a/apps/docs/src/app/docs/layout.tsx
+++ b/apps/docs/src/app/docs/layout.tsx
@@ -8,7 +8,7 @@ export default function Layout({ children }: LayoutProps<"/docs">) {
return (
-
+
diff --git a/apps/docs/src/app/docs/plugins/page.tsx b/apps/docs/src/app/docs/plugins/page.tsx
new file mode 100644
index 0000000..9ae0e3f
--- /dev/null
+++ b/apps/docs/src/app/docs/plugins/page.tsx
@@ -0,0 +1,88 @@
+import type { Metadata } from "next";
+import { PluginDirectory } from "@/components/plugin-directory";
+import { fetchPluginEntries } from "@/lib/github-plugins";
+
+export const metadata: Metadata = {
+ title: "Plugins",
+ description:
+ "Community plugins built with ap-sdk — tag your repo ap-sdk-plugin to get listed.",
+};
+
+export default async function PluginsPage() {
+ const entries = await fetchPluginEntries();
+
+ return (
+
+
+
+
+
+ Plugins
+
+
+ Community plugins built with ap-sdk. Install from GitHub or npm,
+ and tag your repo ap-sdk-plugin to get listed.
+
+
+ npx ap-sdk install owner/repo
+
+
+ New here? Read{" "}
+
+ installing plugins
+
+ .
+
+
+
+
+
+
+
+ Get listed
+
+
+
+
On GitHub
+
+
+ Keep a plugin.ts that passes{" "}
+ npx ap-sdk check.
+
+
+ Add the ap-sdk-plugin topic in About → Topics.
+
+ Your repo appears here within the hour.
+
+
+
+
On npm
+
+
+ Add ap-sdk-plugin to package.json{" "}
+ keywords.
+
+ Publish the package.
+
+ Users can install with{" "}
+ npx ap-sdk install npm:<package>.
+
+
+
+
+
+ The template repo from the distribution spike should include the
+ topic by default once it exists.
+
+
+
+
+
+ );
+}
diff --git a/apps/docs/src/components/callout.tsx b/apps/docs/src/components/callout.tsx
new file mode 100644
index 0000000..6a1f7e2
--- /dev/null
+++ b/apps/docs/src/components/callout.tsx
@@ -0,0 +1,77 @@
+import { cva, type VariantProps } from "class-variance-authority";
+import { Info, Lightbulb, OctagonAlert, TriangleAlert } from "lucide-react";
+import type { ReactNode } from "react";
+import { cn } from "@/lib/utils";
+
+/**
+ * jalco-ui
+ * Callout
+ * by Justin Levine
+ * ui.justinlevine.me
+ *
+ * MDX callout block for notes, tips, warnings, and danger states.
+ * Server-safe and prose-isolated by the MDX component wrapper.
+ */
+
+const calloutVariants = cva(
+ "rounded-xl border bg-card/70 p-4 text-sm leading-6 shadow-sm",
+ {
+ variants: {
+ variant: {
+ note: "border-blue-500/25 bg-blue-500/5 text-blue-950 dark:text-blue-100",
+ tip: "border-emerald-500/25 bg-emerald-500/5 text-emerald-950 dark:text-emerald-100",
+ warning:
+ "border-amber-500/30 bg-amber-500/10 text-amber-950 dark:text-amber-100",
+ danger:
+ "border-red-500/30 bg-red-500/10 text-red-950 dark:text-red-100",
+ },
+ },
+ defaultVariants: { variant: "note" },
+ },
+);
+
+const iconClass = {
+ note: "text-blue-600 dark:text-blue-300",
+ tip: "text-emerald-600 dark:text-emerald-300",
+ warning: "text-amber-600 dark:text-amber-300",
+ danger: "text-red-600 dark:text-red-300",
+};
+
+const icons = {
+ note: Info,
+ tip: Lightbulb,
+ warning: TriangleAlert,
+ danger: OctagonAlert,
+};
+
+export interface CalloutProps extends VariantProps {
+ title?: ReactNode;
+ children: ReactNode;
+ className?: string;
+}
+
+export function Callout({
+ variant = "note",
+ title,
+ children,
+ className,
+}: CalloutProps) {
+ const key = variant ?? "note";
+ const Icon = icons[key];
+
+ return (
+
+
+
+
+ {title ? (
+
{title}
+ ) : null}
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/apps/docs/src/components/docs-pagination.tsx b/apps/docs/src/components/docs-pagination.tsx
new file mode 100644
index 0000000..9bc9b8a
--- /dev/null
+++ b/apps/docs/src/components/docs-pagination.tsx
@@ -0,0 +1,65 @@
+import Link from "next/link";
+import { getDocsNavFlat } from "@/lib/docs-nav";
+import { cn } from "@/lib/utils";
+
+function PaginationCard({
+ href,
+ label,
+ title,
+ align = "left",
+}: {
+ href: string;
+ label: string;
+ title: string;
+ align?: "left" | "right";
+}) {
+ return (
+
+
+ {label}
+
+ {title}
+
+ );
+}
+
+export function DocsPagination({ current }: { current: string }) {
+ const items = getDocsNavFlat();
+ const index = items.findIndex((item) => item.url === current);
+ if (index === -1) return null;
+
+ const previous = items[index - 1];
+ const next = items[index + 1];
+ if (!previous && !next) return null;
+
+ return (
+
+ {previous ? (
+
+ ) : (
+
+ )}
+ {next ? (
+
+ ) : null}
+
+ );
+}
diff --git a/apps/docs/src/components/docs-toc.tsx b/apps/docs/src/components/docs-toc.tsx
index 98a7e15..d66439d 100644
--- a/apps/docs/src/components/docs-toc.tsx
+++ b/apps/docs/src/components/docs-toc.tsx
@@ -6,7 +6,8 @@ import {
TOCItem,
type TOCItemType,
} from "fumadocs-core/toc";
-import { useRef } from "react";
+import { ChevronDown } from "lucide-react";
+import { useRef, useState } from "react";
import { cn } from "@/lib/utils";
/** Table of contents with active-anchor tracking (scrollspy). */
@@ -45,3 +46,41 @@ export function DocsToc({ items }: { items: TOCItemType[] }) {
);
}
+
+/** Mobile: collapsible page-local table of contents. */
+export function MobileDocsToc({ items }: { items: TOCItemType[] }) {
+ const [open, setOpen] = useState(false);
+
+ if (!items.length) return null;
+
+ return (
+
+
setOpen((o) => !o)}
+ aria-expanded={open}
+ className="flex w-full items-center justify-between gap-2 px-4 py-3 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
+ >
+ On this page
+
+
+
+
+ );
+}
diff --git a/apps/docs/src/components/mdx.tsx b/apps/docs/src/components/mdx.tsx
index 7af951c..7a7fb24 100644
--- a/apps/docs/src/components/mdx.tsx
+++ b/apps/docs/src/components/mdx.tsx
@@ -1,10 +1,12 @@
import type { MDXComponents } from "mdx/types";
import Link from "next/link";
import { type ComponentProps, isValidElement, type ReactNode } from "react";
+import { Callout as CalloutBase } from "@/components/callout";
import { CodeBlock as CodeBlockBase } from "@/components/code-block";
import { CodeBlockCommand as CodeBlockCommandBase } from "@/components/code-block-command";
import { CodeLine as CodeLineBase } from "@/components/code-line";
import { SupportMatrix } from "@/components/support-matrix";
+import { Tab as TabBase, Tabs as TabsBase } from "@/components/tabs";
import { convertNpmCommand } from "@/lib/convert-npm-command";
import { cn } from "@/lib/utils";
@@ -43,6 +45,14 @@ function CodeBlockCommand({
);
}
+function Callout({ className, ...props }: ComponentProps
) {
+ return ;
+}
+
+function Tabs({ className, ...props }: ComponentProps) {
+ return ;
+}
+
/**
* Tabbed install/CLI command block. Author one npm-style command; the other
* package managers are derived so docs never drift between managers.
@@ -168,6 +178,9 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
CodeBlock,
CodeLine,
CodeBlockCommand,
+ Callout,
+ Tabs,
+ Tab: TabBase,
NpmCommand,
SupportMatrix,
...components,
diff --git a/apps/docs/src/components/plugin-directory.tsx b/apps/docs/src/components/plugin-directory.tsx
new file mode 100644
index 0000000..60c58f9
--- /dev/null
+++ b/apps/docs/src/components/plugin-directory.tsx
@@ -0,0 +1,229 @@
+"use client";
+
+import { Check, Copy, Star } from "lucide-react";
+import { useMemo, useState } from "react";
+import type { PluginChannel, PluginEntry } from "@/lib/github-plugins";
+
+const NPM_INSTALL_SUPPORTED = true;
+
+function relativeTime(value: string): string {
+ const diff = new Date(value).getTime() - Date.now();
+ const days = Math.round(diff / 86_400_000);
+ const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
+ if (Math.abs(days) < 1) return "today";
+ if (Math.abs(days) < 60) return rtf.format(days, "day");
+ const months = Math.round(days / 30);
+ if (Math.abs(months) < 24) return rtf.format(months, "month");
+ return rtf.format(Math.round(days / 365), "year");
+}
+
+function CopyCommand({ command }: { command: string }) {
+ const [copied, setCopied] = useState(false);
+ return (
+ {
+ await navigator.clipboard.writeText(command);
+ setCopied(true);
+ window.setTimeout(() => setCopied(false), 1200);
+ }}
+ className="flex w-full items-center gap-2 rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-left font-mono text-xs text-foreground transition-colors hover:bg-accent/50"
+ >
+
+ {command}
+
+ {copied ? : }
+
+ );
+}
+
+function ChannelBadge({ channel }: { channel: PluginChannel }) {
+ return (
+
+ {channel === "github" ? "git" : "npm"}
+
+ );
+}
+
+function PluginCard({ entry }: { entry: PluginEntry }) {
+ const href =
+ entry.repoUrl ?? `https://www.npmjs.com/package/${entry.npmName}`;
+ return (
+
+
+ {entry.avatarUrl ? (
+ // GitHub avatar domains are dynamic; keep this tiny decorative image unoptimized.
+ // biome-ignore lint/performance/noImgElement: external avatar URL from registry data
+
+ ) : null}
+
+ {entry.stars !== null ? (
+
+ {entry.stars}
+
+ ) : null}
+
+
+ {entry.description ?? "No description provided."}
+
+
+ {entry.channels.map((channel) => (
+
+ ))}
+
+ updated {relativeTime(entry.updatedAt)}
+
+ {entry.topics
+ .filter((t) => t !== "ap-sdk-plugin")
+ .slice(0, 3)
+ .map((topic) => (
+
+ {topic}
+
+ ))}
+
+
+ {entry.fullName && entry.channels.includes("github") ? (
+
+ ) : null}
+ {entry.npmName ? (
+ NPM_INSTALL_SUPPORTED ? (
+
+ ) : (
+
+ View npm package
+
+ )
+ ) : null}
+
+
+ );
+}
+
+export function PluginDirectory({
+ entries,
+ degraded,
+}: {
+ entries: PluginEntry[];
+ degraded: boolean;
+}) {
+ const [query, setQuery] = useState("");
+ const [channel, setChannel] = useState<"all" | PluginChannel>("all");
+ const [sort, setSort] = useState<"stars" | "recent" | "az">("stars");
+
+ const filtered = useMemo(() => {
+ const q = query.toLowerCase().trim();
+ return entries
+ .filter((entry) => channel === "all" || entry.channels.includes(channel))
+ .filter(
+ (entry) =>
+ !q ||
+ [entry.name, entry.description ?? "", entry.owner].some((v) =>
+ v.toLowerCase().includes(q),
+ ),
+ )
+ .sort((a, b) => {
+ if (sort === "az") return a.name.localeCompare(b.name);
+ if (sort === "recent")
+ return (
+ new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
+ );
+ return (b.stars ?? -1) - (a.stars ?? -1);
+ });
+ }, [channel, entries, query, sort]);
+
+ if (degraded) {
+ return (
+
+ );
+ }
+
+ if (entries.length === 0) {
+ return (
+
+
+ No plugins tagged yet
+
+
+ Be the first: tag your repo with ap-sdk-plugin or publish
+ an npm package with that keyword.
+
+
+ );
+ }
+
+ return (
+
+
+ setQuery(event.target.value)}
+ placeholder="Filter by name, description, or author"
+ className="min-w-0 flex-1 rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
+ />
+ setChannel(event.target.value as typeof channel)}
+ className="rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
+ >
+ All channels
+ GitHub
+ npm
+
+ setSort(event.target.value as typeof sort)}
+ className="rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
+ >
+ Stars
+ Recently updated
+ A–Z
+
+
+ {filtered.length === 0 ? (
+
+ No plugins match “{query}”.
+
+ ) : (
+
+ {filtered.map((entry) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/apps/docs/src/components/search-dialog.tsx b/apps/docs/src/components/search-dialog.tsx
new file mode 100644
index 0000000..6967e93
--- /dev/null
+++ b/apps/docs/src/components/search-dialog.tsx
@@ -0,0 +1,221 @@
+"use client";
+
+import { useDocsSearch } from "fumadocs-core/search/client";
+import { Search } from "lucide-react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { Dialog } from "radix-ui";
+import { useEffect, useMemo, useRef, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+
+/**
+ * jalco-ui
+ * SearchDialog
+ * by Justin Levine
+ * ui.justinlevine.me
+ *
+ * Hand-rolled docs search UI powered by fumadocs-core's search client.
+ */
+
+interface SearchResult {
+ id: string;
+ url: string;
+ type: "page" | "heading" | "text";
+ content: string;
+ breadcrumbs?: string[];
+}
+
+function plain(value: string): string {
+ return value
+ .replace(/<[^>]*>/g, "")
+ .replace(/\s+/g, " ")
+ .trim();
+}
+
+function resultTitle(result: SearchResult): string {
+ const crumbs = result.breadcrumbs?.map(plain).filter(Boolean) ?? [];
+ if (crumbs.length) return crumbs.join(" / ");
+ return plain(result.content) || result.url;
+}
+
+export function SearchTrigger({
+ onClick,
+ mobile = false,
+}: {
+ onClick: () => void;
+ mobile?: boolean;
+}) {
+ if (mobile) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Search docs…
+
+
+ ⌘K
+
+
+ );
+}
+
+export function SearchDialog({
+ open,
+ onOpenChange,
+}: {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}) {
+ const inputRef = useRef(null);
+ const [active, setActive] = useState(0);
+ const { search, setSearch, query } = useDocsSearch({
+ type: "fetch",
+ api: "/api/search",
+ delayMs: 80,
+ });
+
+ const results = useMemo(
+ () => (Array.isArray(query.data) ? (query.data as SearchResult[]) : []),
+ [query.data],
+ );
+
+ useEffect(() => {
+ if (open) requestAnimationFrame(() => inputRef.current?.focus());
+ }, [open]);
+
+ const go = (result: SearchResult | undefined) => {
+ if (!result) return;
+ window.location.href = result.url;
+ onOpenChange(false);
+ };
+
+ return (
+
+
+
+
+ Search documentation
+
+
+ {
+ setSearch(event.target.value);
+ setActive(0);
+ }}
+ onKeyDown={(event) => {
+ if (event.key === "ArrowDown") {
+ event.preventDefault();
+ setActive((i) => Math.min(results.length - 1, i + 1));
+ } else if (event.key === "ArrowUp") {
+ event.preventDefault();
+ setActive((i) => Math.max(0, i - 1));
+ } else if (event.key === "Enter") {
+ event.preventDefault();
+ go(results[active]);
+ }
+ }}
+ placeholder="Search docs…"
+ className="min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
+ />
+
+ Esc
+
+
+
+ {!search ? (
+
+ Type a keyword, command, or harness name.
+
+ ) : query.isLoading ? (
+
+ Searching…
+
+ ) : query.error ? (
+
+ Search failed. Try again.
+
+ ) : results.length === 0 ? (
+
+ No results for “{search}”.
+
+ ) : (
+
+ {results.map((result, index) => (
+ onOpenChange(false)}
+ onMouseEnter={() => setActive(index)}
+ className={cn(
+ "rounded-xl px-3 py-2 text-sm outline-none transition-colors",
+ index === active
+ ? "bg-accent text-foreground"
+ : "text-foreground hover:bg-accent/60",
+ )}
+ >
+
+ {resultTitle(result)}
+
+
+ {plain(result.content)}
+
+
+ ))}
+
+ )}
+
+
+
+
+ );
+}
+
+export function DocsSearch() {
+ const [open, setOpen] = useState(false);
+ const pathname = usePathname();
+
+ useEffect(() => {
+ const onKeyDown = (event: KeyboardEvent) => {
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
+ event.preventDefault();
+ setOpen(true);
+ }
+ };
+ window.addEventListener("keydown", onKeyDown);
+ return () => window.removeEventListener("keydown", onKeyDown);
+ }, []);
+
+ useEffect(() => {
+ if (pathname) setOpen(false);
+ }, [pathname]);
+
+ return (
+ <>
+ setOpen(true)} />
+
+ setOpen(true)} />
+
+
+ >
+ );
+}
diff --git a/apps/docs/src/components/site-footer.tsx b/apps/docs/src/components/site-footer.tsx
index 0c0e305..3775d6b 100644
--- a/apps/docs/src/components/site-footer.tsx
+++ b/apps/docs/src/components/site-footer.tsx
@@ -4,6 +4,7 @@ import { ComponentCredits } from "@/components/component-credits";
const links = [
{ label: "Docs", href: "/docs" },
{ label: "Harnesses", href: "/docs/harnesses" },
+ { label: "Plugins", href: "/docs/plugins" },
{ label: "Changelog", href: "/docs/changelog" },
{ label: "GitHub", href: "https://github.com/jal-co/agent-plugin-sdk" },
];
diff --git a/apps/docs/src/components/site-navbar.tsx b/apps/docs/src/components/site-navbar.tsx
index 05d983e..ead2470 100644
--- a/apps/docs/src/components/site-navbar.tsx
+++ b/apps/docs/src/components/site-navbar.tsx
@@ -1,45 +1,15 @@
"use client";
-/* ─────────────────────────────────────────────────────────
- * SCROLL STORYBOARD — Navbar (floating variant)
- *
- * Driven by scroll position, not a timeline. Two resting states
- * the bar springs between as you cross the threshold.
- *
- * scrollY ≤ 24px "top" full-width bar, square, flush, no chrome
- * scrollY > 24px "floating" large centered pill: narrower, fully
- * rounded, dropped 12px, blurred card
- * surface with a border + shadow
- *
- * The "docs" variant opts out of this entirely: a flush, full-width
- * header bar so the documentation reads like a product surface.
- * ───────────────────────────────────────────────────────── */
-
-import { motion, useMotionValueEvent, useScroll } from "motion/react";
import Image from "next/image";
import Link from "next/link";
-import { useState } from "react";
+import { DocsSearch } from "@/components/search-dialog";
import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-
-const SCROLL = {
- threshold: 24, // px scrolled before the bar collapses into a pill
-};
-
-/* The morphing shell. Each key holds [top, floating] values. */
-const SHELL = {
- maxWidth: [1120, 880], // px — full container → pill width
- radius: [0, 999], // border-radius
- offsetY: [0, 12], // px drop from the top edge
- paddingX: [24, 18], // horizontal padding
- paddingY: [14, 10], // vertical padding
- spring: { type: "spring" as const, stiffness: 320, damping: 32 },
-};
const LINKS = [
{ label: "Docs", href: "/docs" },
{ label: "Harnesses", href: "/docs/harnesses" },
+ { label: "Plugins", href: "/docs/plugins" },
{ label: "Changelog", href: "/docs/changelog" },
];
@@ -50,7 +20,6 @@ function Logo() {
className="group relative flex items-center"
aria-label="ap-sdk home"
>
- {/* Pure-black monochrome mark — invert it in dark mode so it stays visible. */}
- {/* On hover the mark dissolves into a terminal-style wordmark. */}
+
{
- setFloating(y > SCROLL.threshold);
- });
-
- // Docs: a flush, full-width header — no pill morph.
- if (variant === "docs") {
- return (
-
- );
- }
-
- const i = floating ? 1 : 0;
-
+export function SiteNavbar() {
return (
-
+
);
}
diff --git a/apps/docs/src/components/tabs.tsx b/apps/docs/src/components/tabs.tsx
new file mode 100644
index 0000000..6be5f3e
--- /dev/null
+++ b/apps/docs/src/components/tabs.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import { Tabs as RadixTabs } from "radix-ui";
+import {
+ Children,
+ type ComponentProps,
+ isValidElement,
+ type ReactNode,
+ useEffect,
+ useId,
+ useMemo,
+ useState,
+} from "react";
+import { cn } from "@/lib/utils";
+
+/**
+ * jalco-ui
+ * Tabs
+ * by Justin Levine
+ * ui.justinlevine.me
+ *
+ * MDX-friendly tab set with optional localStorage persistence.
+ */
+
+export interface TabsProps {
+ items: string[];
+ storageKey?: string;
+ children: ReactNode;
+ className?: string;
+}
+
+export interface TabProps extends ComponentProps<"div"> {
+ children: ReactNode;
+}
+
+export function Tab({ className, ...props }: TabProps) {
+ return
;
+}
+
+export function Tabs({ items, storageKey, children, className }: TabsProps) {
+ const id = useId();
+ const values = useMemo(
+ () =>
+ items.map(
+ (item, index) =>
+ `${index}-${item.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
+ ),
+ [items],
+ );
+ const [value, setValue] = useState(values[0] ?? "0-tab");
+
+ useEffect(() => {
+ if (!storageKey) return;
+ const stored = window.localStorage.getItem(storageKey);
+ if (stored && values.includes(stored)) setValue(stored);
+ }, [storageKey, values]);
+
+ useEffect(() => {
+ if (!storageKey) return;
+ const onStorage = (event: StorageEvent) => {
+ if (
+ event.key === storageKey &&
+ event.newValue &&
+ values.includes(event.newValue)
+ ) {
+ setValue(event.newValue);
+ }
+ };
+ window.addEventListener("storage", onStorage);
+ return () => window.removeEventListener("storage", onStorage);
+ }, [storageKey, values]);
+
+ const onValueChange = (next: string) => {
+ setValue(next);
+ if (storageKey) window.localStorage.setItem(storageKey, next);
+ };
+
+ const panels = Children.toArray(children).filter(isValidElement);
+
+ return (
+
+
+ {items.map((item, index) => {
+ const tabValue = values[index] ?? `${index}-${item}`;
+ return (
+
+ {item}
+
+ );
+ })}
+
+ {values.map((tabValue, index) => (
+
+ {panels[index] ?? null}
+
+ ))}
+
+ );
+}
diff --git a/apps/docs/src/lib/docs-nav.ts b/apps/docs/src/lib/docs-nav.ts
index 45398cc..98f9d41 100644
--- a/apps/docs/src/lib/docs-nav.ts
+++ b/apps/docs/src/lib/docs-nav.ts
@@ -55,9 +55,14 @@ export function getDocsNav(): DocsNavGroup[] {
// The changelog is a standalone route (not MDX content), so append it to the
// last section by hand.
const last = groups.at(-1);
+ const plugins: DocsNavItem = { title: "Plugins", url: "/docs/plugins" };
const changelog: DocsNavItem = { title: "Changelog", url: "/docs/changelog" };
- if (last) last.items.push(changelog);
- else groups.push({ items: [changelog] });
+ if (last) last.items.push(plugins, changelog);
+ else groups.push({ items: [plugins, changelog] });
return groups;
}
+
+export function getDocsNavFlat(): DocsNavItem[] {
+ return getDocsNav().flatMap((group) => group.items);
+}
diff --git a/apps/docs/src/lib/github-plugins.ts b/apps/docs/src/lib/github-plugins.ts
new file mode 100644
index 0000000..196bcaf
--- /dev/null
+++ b/apps/docs/src/lib/github-plugins.ts
@@ -0,0 +1,147 @@
+export type PluginChannel = "github" | "npm";
+
+export interface PluginEntry {
+ id: string;
+ fullName: string | null;
+ name: string;
+ owner: string;
+ avatarUrl: string | null;
+ description: string | null;
+ stars: number | null;
+ updatedAt: string;
+ repoUrl: string | null;
+ npmName: string | null;
+ npmVersion: string | null;
+ channels: PluginChannel[];
+ topics: string[];
+}
+
+interface GithubRepo {
+ full_name: string;
+ name: string;
+ owner: { login: string; avatar_url: string };
+ description: string | null;
+ stargazers_count: number;
+ pushed_at: string;
+ html_url: string;
+ topics?: string[];
+ archived?: boolean;
+ fork?: boolean;
+}
+
+interface NpmObject {
+ package: {
+ name: string;
+ version: string;
+ description?: string;
+ date: string;
+ links?: { repository?: string; npm?: string };
+ publisher?: { username?: string };
+ };
+}
+
+function normalizeRepo(url: string | undefined): string | null {
+ if (!url) return null;
+ const cleaned = url
+ .replace(/^git\+/, "")
+ .replace(/^https?:\/\/github\.com\//, "")
+ .replace(/^git@github\.com:/, "")
+ .replace(/\.git$/, "")
+ .replace(/#.*$/, "")
+ .replace(/\/$/, "");
+ return /^[\w.-]+\/[\w.-]+$/.test(cleaned) ? cleaned.toLowerCase() : null;
+}
+
+async function fetchGithub(): Promise {
+ const res = await fetch(
+ "https://api.github.com/search/repositories?q=topic:ap-sdk-plugin&sort=stars&order=desc&per_page=100",
+ {
+ headers: {
+ Accept: "application/vnd.github+json",
+ ...(process.env.GIT_TOKEN
+ ? { Authorization: `Bearer ${process.env.GIT_TOKEN}` }
+ : {}),
+ },
+ next: { revalidate: 3600 },
+ },
+ );
+ if (!res.ok) throw new Error(`GitHub search failed: ${res.status}`);
+ const json = (await res.json()) as { items?: GithubRepo[] };
+ return (json.items ?? []).filter((repo) => !repo.archived && !repo.fork);
+}
+
+async function fetchNpm(): Promise {
+ const res = await fetch(
+ "https://registry.npmjs.org/-/v1/search?text=keywords:ap-sdk-plugin&size=250",
+ { next: { revalidate: 3600 } },
+ );
+ if (!res.ok) throw new Error(`npm search failed: ${res.status}`);
+ const json = (await res.json()) as { objects?: NpmObject[] };
+ return json.objects ?? [];
+}
+
+export async function fetchPluginEntries(): Promise {
+ const [githubResult, npmResult] = await Promise.allSettled([
+ fetchGithub(),
+ fetchNpm(),
+ ]);
+
+ if (githubResult.status === "rejected" && npmResult.status === "rejected") {
+ console.warn(githubResult.reason, npmResult.reason);
+ return null;
+ }
+ if (githubResult.status === "rejected") console.warn(githubResult.reason);
+ if (npmResult.status === "rejected") console.warn(npmResult.reason);
+
+ const entries = new Map();
+ for (const repo of githubResult.status === "fulfilled"
+ ? githubResult.value
+ : []) {
+ entries.set(repo.full_name.toLowerCase(), {
+ id: repo.full_name,
+ fullName: repo.full_name,
+ name: repo.name,
+ owner: repo.owner.login,
+ avatarUrl: repo.owner.avatar_url,
+ description: repo.description,
+ stars: repo.stargazers_count,
+ updatedAt: repo.pushed_at,
+ repoUrl: repo.html_url,
+ npmName: null,
+ npmVersion: null,
+ channels: ["github"],
+ topics: repo.topics ?? [],
+ });
+ }
+
+ for (const obj of npmResult.status === "fulfilled" ? npmResult.value : []) {
+ const pkg = obj.package;
+ const repoKey = normalizeRepo(pkg.links?.repository);
+ const existing = repoKey ? entries.get(repoKey) : undefined;
+ if (existing) {
+ existing.npmName = pkg.name;
+ existing.npmVersion = pkg.version;
+ if (!existing.channels.includes("npm")) existing.channels.push("npm");
+ } else {
+ entries.set(`npm:${pkg.name}`, {
+ id: pkg.name,
+ fullName: repoKey,
+ name: pkg.name,
+ owner: pkg.publisher?.username ?? "npm",
+ avatarUrl: null,
+ description: pkg.description ?? null,
+ stars: null,
+ updatedAt: pkg.date,
+ repoUrl: repoKey ? `https://github.com/${repoKey}` : null,
+ npmName: pkg.name,
+ npmVersion: pkg.version,
+ channels: ["npm"],
+ topics: [],
+ });
+ }
+ }
+
+ return [...entries.values()].sort(
+ (a, b) => (b.stars ?? -1) - (a.stars ?? -1),
+ );
+}
From 91e014af95896008ccaa0205ef759d7b30e8c070 Mon Sep 17 00:00:00 2001
From: Justin Levine <20596508+jal-co@users.noreply.github.com>
Date: Wed, 1 Jul 2026 16:42:43 -0500
Subject: [PATCH 4/4] docs: add improvement plans and distribution spike
artifacts
Checks in the 13 improvement plans with statuses updated to DONE
(008 superseded by 012), plus plan 007's deliverables: the design doc,
reference template tree, and draft composite action, and plan 009's
tooling decision record.
---
plans/001-readme-overhaul.md | 223 ++++++++++++
plans/002-ap-sdk-init.md | 303 ++++++++++++++++
plans/003-feature-docs-pages.md | 267 ++++++++++++++
plans/004-port-as-wedge.md | 207 +++++++++++
plans/005-uninstall-and-manifest.md | 294 +++++++++++++++
plans/006-dev-watch-mode.md | 262 ++++++++++++++
plans/007-artifacts/action.yml | 37 ++
plans/007-artifacts/design.md | 45 +++
.../template/.github/workflows/validate.yml | 24 ++
plans/007-artifacts/template/.gitignore | 2 +
plans/007-artifacts/template/README.md | 20 ++
plans/007-artifacts/template/package.json | 13 +
plans/007-artifacts/template/plugin.ts | 24 ++
plans/007-template-repo-and-action.md | 199 +++++++++++
plans/008-plugin-showcase.md | 166 +++++++++
plans/009-api-reference.md | 196 ++++++++++
plans/009-artifacts/decision.md | 20 ++
plans/010-docs-navigation-affordances.md | 234 ++++++++++++
plans/011-docs-components-cleanup.md | 228 ++++++++++++
plans/012-plugin-registry-page.md | 337 ++++++++++++++++++
plans/013-npm-install-source.md | 300 ++++++++++++++++
plans/README.md | 77 ++++
22 files changed, 3478 insertions(+)
create mode 100644 plans/001-readme-overhaul.md
create mode 100644 plans/002-ap-sdk-init.md
create mode 100644 plans/003-feature-docs-pages.md
create mode 100644 plans/004-port-as-wedge.md
create mode 100644 plans/005-uninstall-and-manifest.md
create mode 100644 plans/006-dev-watch-mode.md
create mode 100644 plans/007-artifacts/action.yml
create mode 100644 plans/007-artifacts/design.md
create mode 100644 plans/007-artifacts/template/.github/workflows/validate.yml
create mode 100644 plans/007-artifacts/template/.gitignore
create mode 100644 plans/007-artifacts/template/README.md
create mode 100644 plans/007-artifacts/template/package.json
create mode 100644 plans/007-artifacts/template/plugin.ts
create mode 100644 plans/007-template-repo-and-action.md
create mode 100644 plans/008-plugin-showcase.md
create mode 100644 plans/009-api-reference.md
create mode 100644 plans/009-artifacts/decision.md
create mode 100644 plans/010-docs-navigation-affordances.md
create mode 100644 plans/011-docs-components-cleanup.md
create mode 100644 plans/012-plugin-registry-page.md
create mode 100644 plans/013-npm-install-source.md
create mode 100644 plans/README.md
diff --git a/plans/001-readme-overhaul.md b/plans/001-readme-overhaul.md
new file mode 100644
index 0000000..32930a7
--- /dev/null
+++ b/plans/001-readme-overhaul.md
@@ -0,0 +1,223 @@
+# Plan 001: Overhaul the root README and give the npm package a README
+
+> **Executor instructions**: Follow this plan step by step. Run every
+> verification command and confirm the expected result before moving to the
+> next step. If anything in the "STOP conditions" section occurs, stop and
+> report — do not improvise. When done, update the status row for this plan
+> in `plans/README.md`.
+>
+> **Drift check (run first)**: `git diff --stat fe6fb92..HEAD -- README.md packages/agent-plugin-sdk/`
+> If any in-scope file changed since this plan was written, compare the
+> "Current state" excerpts against the live code before proceeding; on a
+> mismatch, treat it as a STOP condition.
+
+## Status
+
+- **Priority**: P1
+- **Effort**: S
+- **Risk**: LOW
+- **Depends on**: none
+- **Category**: docs
+- **Planned at**: commit `fe6fb92`, 2026-07-01
+
+## Why this matters
+
+The GitHub README and the npm package page are the two highest-traffic surfaces
+for this project, and both under-sell it. The root README shows only a
+skill-based example — hooks, MCP servers, subagents, shared tools, and the
+`ap-sdk port` command (the project's most differentiating feature) are invisible.
+Its bottom "Docs" link points at the GitHub repo instead of the docs site, and
+its `[Examples](./packages/...)` relative link 404s when rendered anywhere but
+GitHub. Worse, `packages/agent-plugin-sdk/` has **no README.md at all**, so the
+npm page for `@jalco/ap-sdk` renders with no readme. Fixing this is the cheapest
+possible growth work.
+
+## Current state
+
+- `README.md` (repo root) — the GitHub-facing readme. Relevant lines:
+ - Line ~34 (bottom of the Install section):
+ ```md
+ [Docs](https://github.com/jal-co/agent-plugin-sdk) · [Examples](./packages/agent-plugin-sdk/examples)
+ ```
+ The "Docs" link should be `https://ap-sdk.dev/docs`; the Examples link is
+ relative and must become an absolute GitHub URL.
+ - The header links block (near the top) is correct and must be preserved:
+ ```md
+ [Homepage](https://ap-sdk.dev) · [Docs](https://ap-sdk.dev/docs) · [𝕏](https://x.com/jalcowastaken)
+ ```
+ - The only code example is a `definePlugin` with one `defineSkill`. There is
+ no mention of: commands, subagents, hooks, MCP servers, shared tools,
+ companion files, `ap-sdk port`, `ap-sdk check`, install-from-GitHub
+ (`ap-sdk install owner/repo`), or the support matrix.
+- `packages/agent-plugin-sdk/` — contains `CHANGELOG.md` but **no `README.md`**.
+ `package.json` has `"files": ["dist"]` (npm always includes a README.md when
+ present, regardless of `files`).
+- The full CLI surface (source of truth for what to advertise) is the `HELP`
+ string in `packages/agent-plugin-sdk/src/cli.ts:24-70`: commands are `build`,
+ `install`, `check`, `tools`, `add-harness`, `port`.
+- The full feature surface is `packages/agent-plugin-sdk/src/types.ts` — the
+ `Plugin` interface (~line 380) has: `instructions`, `skills`, `commands`,
+ `mcpServers`, `subagents`, `hooks`, `files`, `tools`, `marketplace`.
+- The eight harness ids: `claude`, `codex`, `pi`, `opencode`, `gemini`,
+ `copilot`, `cursor`, `windsurf` (`src/types.ts`, `BuiltinHarnessId`).
+- Docs pages that exist and can be linked: `/docs` (intro), `/docs/installation`,
+ `/docs/quickstart`, `/docs/installing-plugins`, `/docs/porting`,
+ `/docs/harnesses`, `/docs/authoring-a-harness`, `/docs/origins`, and
+ `/docs/changelog`. All under `https://ap-sdk.dev`.
+- Attribution constraint: the project was originally created by Sahaj Jain
+ (@jnsahaj) and is continued with permission. The README currently credits this
+ twice (blockquote near top, license line at bottom). **Both credits must be
+ preserved verbatim.**
+
+## Commands you will need
+
+| Purpose | Command | Expected on success |
+|---|---|---|
+| Install deps | `pnpm install` | exit 0 |
+| Full verify gate | `pnpm turbo typecheck test lint build` | all tasks pass |
+| Link check (manual) | n/a — visually confirm every URL in changed files is absolute or a valid repo-relative path on GitHub | — |
+
+## Scope
+
+**In scope** (the only files you should modify/create):
+
+- `README.md` (root)
+- `packages/agent-plugin-sdk/README.md` (create)
+- `.tegami/2026-07-01-package-readme.md` (create — changelog for the npm-visible change)
+
+**Out of scope** (do NOT touch):
+
+- `apps/docs/**` — the docs site has its own plan (003).
+- `packages/agent-plugin-sdk/src/**` — no code changes in this plan.
+- `packages/agent-plugin-sdk/CHANGELOG.md` — generated by Tegami, never hand-edited.
+- `packages/agent-plugin-sdk/package.json` — no `readme` field needed; npm picks up README.md automatically.
+
+## Git workflow
+
+- Branch: `docs/readme-overhaul` (from `main`)
+- Commits: conventional, e.g. `docs: overhaul root README and add package README`
+- Do NOT push or open a PR unless the operator instructed it.
+
+## Steps
+
+### Step 1: Fix the broken/weak links in the root README
+
+In `README.md`, replace the line
+
+```md
+[Docs](https://github.com/jal-co/agent-plugin-sdk) · [Examples](./packages/agent-plugin-sdk/examples)
+```
+
+with
+
+```md
+[Docs](https://ap-sdk.dev/docs) · [Examples](https://github.com/jal-co/agent-plugin-sdk/tree/main/packages/agent-plugin-sdk/examples)
+```
+
+**Verify**: `grep -c "ap-sdk.dev/docs" README.md` → at least `2` (header block + install section), and `grep -n "](./" README.md` → no matches other than `./LICENSE` (repo-relative license link is fine on GitHub).
+
+### Step 2: Add a feature-surface section to the root README
+
+After the existing skill example and the `ap-sdk build / ap-sdk install` code
+block, add two sections (keep the existing tone — terse, no marketing fluff):
+
+1. **"What you can define"** — a compact table or bullet list mapping each
+ `Plugin` field to one line: skills, commands, subagents, hooks, MCP servers,
+ shared tools (real code, one implementation, per-harness glue), always-on
+ instructions, companion files. Link each to `https://ap-sdk.dev/docs` (until
+ plan 003 lands per-feature pages, link to the docs root or `/docs/harnesses`).
+2. **"Already have a plugin?"** — 3–5 lines showing `npx ap-sdk port ./my-plugin`
+ with one sentence: it auto-detects an existing Claude Code / Cursor / Codex /
+ etc. layout and generates a portable `plugin.ts` that loads your files. Link
+ to `https://ap-sdk.dev/docs/porting`. Also show `ap-sdk install owner/repo`
+ (install straight from GitHub) in the same section or adjacent.
+
+Also add a one-line pointer to the support matrix: `See the
+[support matrix](https://ap-sdk.dev/docs/harnesses) for what each harness supports.`
+
+Preserve unchanged: the header pictures, badge group, homepage/docs/𝕏 links,
+both attribution credits, and the License section.
+
+**Verify**: `grep -n "port" README.md` → matches in the new section; `grep -n "jnsahaj" README.md` → 2 matches (both credits intact).
+
+### Step 3: Create the npm package README
+
+Create `packages/agent-plugin-sdk/README.md`. Content: a trimmed version of the
+root README **with all links absolute** (npm renders no repo-relative links):
+
+- Title `# @jalco/ap-sdk` + the one-line pitch ("Write an agent plugin once,
+ ship it to every harness.").
+- The same quick `definePlugin` example and `ap-sdk build` / `ap-sdk install`
+ block from the root README.
+- The "What you can define" list from Step 2.
+- The `ap-sdk port` section from Step 2.
+- Install: `pnpm add -D @jalco/ap-sdk`.
+- Links: `https://ap-sdk.dev`, `https://ap-sdk.dev/docs`,
+ `https://github.com/jal-co/agent-plugin-sdk`.
+- License line including the attribution: `MIT · Originally by
+ [Sahaj Jain](https://github.com/jnsahaj), continued by
+ [Justin Levine](https://github.com/jal-co).`
+- Do NOT use the `shieldcn.dev/header` `` block here unless you keep
+ the ` ` fallback — npm strips ``/``; a plain markdown
+ image with an absolute URL is safest.
+
+**Verify**: `test -f packages/agent-plugin-sdk/README.md && grep -c "](https://" packages/agent-plugin-sdk/README.md` → ≥ 5, and `grep -n "](./\|](\.\." packages/agent-plugin-sdk/README.md` → no matches.
+
+### Step 4: Write the Tegami changelog
+
+Create `.tegami/2026-07-01-package-readme.md`:
+
+```md
+---
+packages:
+ "@jalco/ap-sdk": patch
+---
+
+### Add a package README
+
+The npm page for `@jalco/ap-sdk` now has a readme covering the full feature
+surface (skills, commands, subagents, hooks, MCP, shared tools) and the
+`ap-sdk port` migration path.
+```
+
+**Verify**: `ls .tegami/2026-07-01-package-readme.md` → exists; frontmatter has a `packages` key.
+
+### Step 5: Run the verify gate
+
+**Verify**: `pnpm turbo typecheck test lint build` → all tasks pass (README
+changes can't break code, but the repo's AGENTS.md requires the gate before
+every commit).
+
+## Test plan
+
+No code changes — no new tests. Verification is the link greps in each step plus
+the full verify gate.
+
+## Done criteria
+
+- [ ] `grep -n "github.com/jal-co/agent-plugin-sdk)" README.md` no longer shows a link labeled "Docs" pointing at the repo
+- [ ] `packages/agent-plugin-sdk/README.md` exists, all links absolute
+- [ ] Root README mentions: port, hooks, MCP, subagents, tools (grep each keyword)
+- [ ] Both `jnsahaj` attribution credits preserved in root README; one in package README
+- [ ] `.tegami/2026-07-01-package-readme.md` exists with `patch` bump
+- [ ] `pnpm turbo typecheck test lint build` exits 0
+- [ ] No files outside the in-scope list modified (`git status`)
+- [ ] `plans/README.md` status row updated
+
+## STOP conditions
+
+Stop and report back if:
+
+- The root README's link line doesn't match the excerpt in "Current state"
+ (drifted).
+- You find a `readme` field or an existing README under
+ `packages/agent-plugin-sdk/` (plan assumption false).
+- You are tempted to change `package.json` — that's out of scope.
+
+## Maintenance notes
+
+- When plan 003 (feature docs pages) lands, update the "What you can define"
+ links to point at the per-feature pages instead of the docs root.
+- Keep root and package READMEs in sync manually for now; if they drift often,
+ consider generating the package README from the root one in a later plan.
+- Reviewer: check every link renders on both github.com and npmjs.com.
diff --git a/plans/002-ap-sdk-init.md b/plans/002-ap-sdk-init.md
new file mode 100644
index 0000000..996fd71
--- /dev/null
+++ b/plans/002-ap-sdk-init.md
@@ -0,0 +1,303 @@
+# Plan 002: Add an `ap-sdk init` command that scaffolds a new plugin project
+
+> **Executor instructions**: Follow this plan step by step. Run every
+> verification command and confirm the expected result before moving to the
+> next step. If anything in the "STOP conditions" section occurs, stop and
+> report — do not improvise. When done, update the status row for this plan
+> in `plans/README.md`.
+>
+> **Drift check (run first)**: `git diff --stat fe6fb92..HEAD -- packages/agent-plugin-sdk/src packages/agent-plugin-sdk/test apps/docs/content/docs/quickstart.mdx`
+> If any in-scope file changed since this plan was written, compare the
+> "Current state" excerpts against the live code before proceeding; on a
+> mismatch, treat it as a STOP condition.
+
+## Status
+
+- **Priority**: P1
+- **Effort**: M
+- **Risk**: LOW
+- **Depends on**: none
+- **Category**: dx
+- **Planned at**: commit `fe6fb92`, 2026-07-01
+
+## Why this matters
+
+The quickstart currently tells a new user to hand-write `plugin.ts` from a code
+sample. That's friction at the exact moment a visitor decides whether to try the
+tool. The CLI already has a scaffolding precedent — `ap-sdk add-harness`
+generates a working harness module from a template (`src/scaffold.ts`) — but
+there is no equivalent for the far more common case: starting a new plugin.
+`ap-sdk init` (and therefore `pnpm create` / `npx ap-sdk init`) turns "read the
+docs, copy a block, fix imports" into one command that produces a working,
+`check`-clean plugin.
+
+## Current state
+
+- `packages/agent-plugin-sdk/src/cli.ts` — the CLI. The `HELP` string
+ (lines 24–70) lists commands: `build`, `install`, `check`, `tools`,
+ `add-harness`, `port`. Command dispatch is in `main()`:
+ ```ts
+ // cli.ts (~line 218)
+ if (args.command === "add-harness") {
+ runAddHarness(args);
+ return;
+ }
+ // `port` reads an existing plugin directory ...
+ if (args.command === "port") {
+ runPort(args);
+ return;
+ }
+ ```
+ Commands that need no plugin definition are dispatched *before*
+ `enableTypeScript()` and plugin resolution. `init` belongs in this group.
+- `packages/agent-plugin-sdk/src/scaffold.ts` — the existing scaffolding module
+ for `add-harness`. Conventions to match:
+ - a `KEBAB` regex `^[a-z0-9]+(-[a-z0-9]+)*$` and a `validateHarnessId`
+ returning `string | null`;
+ - a pure `harnessTemplate(id, displayName, pkg = "@jalco/ap-sdk"): string`
+ function that returns the full file content;
+ - the generated file "compiles as-is and produces real (if minimal)
+ artifacts" — the template is a *working* starter, not a stub of TODOs.
+- `runAddHarness` in `cli.ts` (~line 440) shows the output conventions: refuse
+ to overwrite an existing file (`fail(\`Refusing to overwrite existing file: ...\`)`),
+ support `--dry-run` (print content instead of writing), print `Next steps`
+ with `dim()` styled commands.
+- `packages/agent-plugin-sdk/src/plugin-files.ts` exports
+ `DEFAULT_PLUGIN_FILES` (the filenames `locatePlugin` searches: `plugin.ts`,
+ `plugin.js`, `ap-sdk.config.ts` — read the file to confirm exact list).
+- The canonical starter plugin shape (use this as the template's content —
+ it is the same example the docs use), from
+ `apps/docs/content/docs/quickstart.mdx`:
+ ```ts
+ import { definePlugin, defineSkill, defineCommand } from "@jalco/ap-sdk";
+
+ export default definePlugin({
+ id: "git-helper",
+ description: "Helpers for working with git in a repo.",
+ instructions: "## Git\n- Branch off main; never commit to it directly.",
+ skills: [
+ defineSkill({
+ name: "diff-review",
+ description:
+ "Summarize and risk-flag uncommitted changes. Use when the user asks what changed.",
+ instructions: "Run `git diff HEAD` and summarize the changes in 2-4 bullets.",
+ }),
+ ],
+ commands: [
+ defineCommand({
+ name: "commit",
+ description: "Write a conventional commit for the staged changes.",
+ body: "Write a Conventional Commit message for the staged diff. Args: $ARGUMENTS",
+ }),
+ ],
+ });
+ ```
+- Test conventions: vitest, files under `packages/agent-plugin-sdk/test/*.test.ts`.
+ Tests that touch the filesystem use `mkdtempSync(join(tmpdir(), "aps-..."))` +
+ `process.chdir` in `beforeEach`/`afterEach` — see `test/install.test.ts:28-37`
+ for the exact pattern. There is currently no test that shells out to the CLI;
+ test the scaffold function directly (like the existing `add-harness` scaffold
+ is testable via `harnessTemplate`).
+- Repo AGENTS.md: every user-facing SDK change ships with a `.tegami/` changelog;
+ a new CLI command is a **minor** bump.
+
+## Commands you will need
+
+| Purpose | Command | Expected on success |
+|---|---|---|
+| Install deps | `pnpm install` | exit 0 |
+| SDK tests | `pnpm turbo test --filter=@jalco/ap-sdk` | all pass |
+| Typecheck | `pnpm turbo typecheck --filter=@jalco/ap-sdk` | exit 0 |
+| Full gate | `pnpm turbo typecheck test lint build` | all tasks pass |
+| Manual smoke | `cd $(mktemp -d) && node /packages/agent-plugin-sdk/dist/cli.js init my-plugin` (after build) | files written, exit 0 |
+
+## Scope
+
+**In scope** (the only files you should modify/create):
+
+- `packages/agent-plugin-sdk/src/cli.ts` — HELP text, arg parsing (if needed), `runInit`
+- `packages/agent-plugin-sdk/src/scaffold-plugin.ts` (create — the template module; keep `scaffold.ts` for harnesses)
+- `packages/agent-plugin-sdk/test/init.test.ts` (create)
+- `apps/docs/content/docs/quickstart.mdx` — lead with `npx ap-sdk init` before the hand-written option
+- `.tegami/2026-07-01-init-command.md` (create)
+
+**Out of scope** (do NOT touch):
+
+- `src/scaffold.ts` — the harness scaffolder; don't refactor or merge it.
+- `src/port.ts`, `src/github.ts` — unrelated.
+- A separate `create-ap-sdk` npm package — explicitly deferred (see Maintenance
+ notes).
+- `package.json` `bin` — `init` rides the existing `ap-sdk` binary.
+
+## Git workflow
+
+- Branch: `feat/ap-sdk-init` (from `main`)
+- Commit style: `feat(sdk): add ap-sdk init scaffold command` (conventional commits, scope `sdk` — matches `git log`, e.g. `feat(sdk): per-hook async flag and command frontmatter passthrough`)
+- Do NOT push or open a PR unless the operator instructed it.
+
+## Steps
+
+### Step 1: Create the plugin template module
+
+Create `packages/agent-plugin-sdk/src/scaffold-plugin.ts` exporting:
+
+- `validatePluginId(id: string): string | null` — same kebab-case rule and
+ error-message style as `validateHarnessId` in `src/scaffold.ts` (copy the
+ regex; the `Plugin.id` doc in `src/types.ts` specifies
+ `^[a-z0-9]+(-[a-z0-9]+)*$`).
+- `pluginTemplate(id: string, pkg = "@jalco/ap-sdk"): string` — returns the
+ full `plugin.ts` content: the canonical starter from "Current state" with
+ `id` substituted for `"git-helper"` and a short header comment pointing at
+ `https://ap-sdk.dev/docs/quickstart`. The description and skill/command
+ content stay generic (keep the git-helper example content — it demonstrates
+ a skill, a command, and instructions in ~30 lines and `check`s clean).
+- `gitignoreSnippet(): string` — returns `".aps-out/\n"`.
+
+Match the file-header JSDoc style of `src/scaffold.ts` (block comment explaining
+what the module generates).
+
+**Verify**: `pnpm turbo typecheck --filter=@jalco/ap-sdk` → exit 0.
+
+### Step 2: Wire `init` into the CLI
+
+In `packages/agent-plugin-sdk/src/cli.ts`:
+
+1. Add to `HELP` under Usage (keep alignment style):
+ `ap-sdk init [id] [options] Scaffold a new plugin.ts in the current directory`
+ and an example: `ap-sdk init my-plugin`.
+2. Dispatch before plugin resolution, next to `add-harness` and `port`:
+ ```ts
+ if (args.command === "init") {
+ runInit(args);
+ return;
+ }
+ ```
+3. Implement `runInit(args: Args)` following `runAddHarness`'s conventions
+ exactly:
+ - `id` = `args.plugin` (second positional); default `"my-plugin"` when
+ omitted; validate with `validatePluginId`, `fail(problem)` on error.
+ - Target file: `resolve(process.cwd(), args.out ?? ".", "plugin.ts")`.
+ - If the target exists → `fail("Refusing to overwrite existing file: " + target)`.
+ - `--dry-run` → print `Would write` + the content, return without writing.
+ - Otherwise write the file; if a `.gitignore` exists in the target dir and
+ doesn't contain `.aps-out`, append the snippet; if none exists, create one
+ containing only the snippet.
+ - Print `Next steps` (match `runAddHarness`'s styling with `tick()`,
+ `bold()`, `dim()`):
+ `npx ap-sdk check` · `npx ap-sdk build` · `npx ap-sdk install -t `.
+
+**Verify**: `pnpm turbo build --filter=@jalco/ap-sdk` → exit 0, then
+`cd $(mktemp -d) && node /packages/agent-plugin-sdk/dist/cli.js init demo && ls` →
+`plugin.ts` and `.gitignore` present.
+
+### Step 3: The generated plugin must pass `check`
+
+In the same temp dir (the generated `plugin.ts` imports `@jalco/ap-sdk`, which
+isn't installed there), verify via the repo instead: copy the generated
+`plugin.ts` into a temp dir and run the check with the workspace CLI:
+
+```bash
+cd packages/agent-plugin-sdk && pnpm cli check /plugin.ts
+```
+
+(`pnpm cli` runs `tsx src/cli.ts` per `package.json` scripts. The import
+`@jalco/ap-sdk` resolves because the workspace maps it.)
+
+If module resolution fails in the temp dir, instead write the generated content
+into `packages/agent-plugin-sdk/` as a temp file, check it, and delete it —
+the requirement is only that the template content validates.
+
+**Verify**: output contains `is valid` and lists `1 skill(s), 1 command(s)`.
+
+### Step 4: Tests
+
+Create `packages/agent-plugin-sdk/test/init.test.ts` covering:
+
+1. `pluginTemplate("my-plugin")` contains `definePlugin`, `id: "my-plugin"`,
+ and imports from `@jalco/ap-sdk`.
+2. The template content, written to a temp file and imported via the test
+ runner, default-exports an object that passes `validatePlugin` (import
+ `validatePlugin` from `../src/validate.js`). If dynamic import of a temp
+ `.ts` file is awkward under vitest, instead construct the equivalent plugin
+ object inline and assert `pluginTemplate` output string-contains each field —
+ but prefer the real validation path. Model the temp-dir setup on
+ `test/install.test.ts:28-37`.
+3. `validatePluginId` rejects `"My_Plugin"` and accepts `"my-plugin"`.
+
+**Verify**: `pnpm turbo test --filter=@jalco/ap-sdk` → all pass including the
+new file.
+
+### Step 5: Update the quickstart
+
+In `apps/docs/content/docs/quickstart.mdx`, change section "## 1. Define a
+plugin" to offer the scaffold first:
+
+```bash
+npx ap-sdk init my-plugin
+```
+
+with one sentence ("scaffolds a working plugin.ts you can edit"), then keep the
+existing hand-written example as the "or write it yourself" path. Don't
+restructure the rest of the page.
+
+**Verify**: `pnpm turbo build --filter=@jal-co/docs` → exit 0 (MDX compiles).
+
+### Step 6: Changelog + gate
+
+Create `.tegami/2026-07-01-init-command.md`:
+
+```md
+---
+packages:
+ "@jalco/ap-sdk": minor
+---
+
+## Add `ap-sdk init`
+
+Scaffold a new plugin project with one command. `ap-sdk init my-plugin` writes
+a working `plugin.ts` (a skill, a command, and instructions) that passes
+`ap-sdk check` as-is, plus a `.gitignore` entry for `.aps-out/`.
+```
+
+**Verify**: `pnpm turbo typecheck test lint build` → all tasks pass.
+
+## Test plan
+
+- New file `test/init.test.ts` (Step 4): template content validity, kebab-case
+ id validation (accept/reject), generated plugin passes `validatePlugin`.
+- Pattern: `test/install.test.ts` for temp-dir handling; assertions with
+ `expect(...).toContain(...)` / `.toThrow()` as in `test/validate.test.ts`.
+- Verification: `pnpm turbo test --filter=@jalco/ap-sdk` → all pass.
+
+## Done criteria
+
+- [ ] `node dist/cli.js init demo` in an empty temp dir writes `plugin.ts` + `.gitignore`, exit 0
+- [ ] Re-running it fails with `Refusing to overwrite` and exit 1
+- [ ] `--dry-run` prints content, writes nothing
+- [ ] Generated plugin passes `check` (Step 3)
+- [ ] `ap-sdk --help` output includes `init` (`node dist/cli.js --help | grep init`)
+- [ ] Quickstart leads with `npx ap-sdk init`
+- [ ] `.tegami/2026-07-01-init-command.md` exists with `minor` bump
+- [ ] `pnpm turbo typecheck test lint build` exits 0
+- [ ] No files outside the in-scope list modified (`git status`)
+- [ ] `plans/README.md` status row updated
+
+## STOP conditions
+
+Stop and report back if:
+
+- `cli.ts` dispatch no longer matches the excerpt (drifted).
+- An `init` or `create` command already exists in `HELP`.
+- Step 3's validation cannot be made to pass without changing `validate.ts`
+ (the template content is wrong — report, don't loosen validation).
+- You find yourself wanting to add interactive prompts (inquirer etc.) — out of
+ scope; flags only.
+
+## Maintenance notes
+
+- A future `create-ap-sdk` package (`pnpm create ap-sdk`) should delegate to
+ this command — deferred because it needs a separate publish pipeline.
+- If plan 007 (template repo) lands, consider `ap-sdk init --from owner/repo`
+ reusing `fetchGithubPlugin` from `src/github.ts` — deferred.
+- Reviewer: confirm the template stays in sync with the quickstart example —
+ they are intentionally identical.
diff --git a/plans/003-feature-docs-pages.md b/plans/003-feature-docs-pages.md
new file mode 100644
index 0000000..974d96c
--- /dev/null
+++ b/plans/003-feature-docs-pages.md
@@ -0,0 +1,267 @@
+# Plan 003: Add per-feature docs pages (skills, commands, subagents, hooks, MCP, tools, files)
+
+> **Executor instructions**: Follow this plan step by step. Run every
+> verification command and confirm the expected result before moving to the
+> next step. If anything in the "STOP conditions" section occurs, stop and
+> report — do not improvise. When done, update the status row for this plan
+> in `plans/README.md`.
+>
+> **Drift check (run first)**: `git diff --stat fe6fb92..HEAD -- apps/docs/content/docs packages/agent-plugin-sdk/src/types.ts`
+> If any in-scope file changed since this plan was written, compare the
+> "Current state" excerpts against the live code before proceeding; on a
+> mismatch, treat it as a STOP condition.
+
+## Status
+
+- **Priority**: P1
+- **Effort**: M
+- **Risk**: LOW
+- **Depends on**: none (001 recommended first so README links have targets to
+ point at, but not required)
+- **Category**: docs
+- **Planned at**: commit `fe6fb92`, 2026-07-01
+
+## Why this matters
+
+The docs site has 8 pages, all setup/guides. There is **no page** for any of the
+seven definable features — skills, commands, subagents, hooks, MCP servers,
+shared tools, companion files — even though the SDK's JSDoc in
+`packages/agent-plugin-sdk/src/types.ts` documents each one thoroughly. Users
+can't discover most of the SDK's capabilities from the site, and the site has
+zero SEO surface for terms like "agent hooks", "portable MCP config", or
+"cross-harness slash commands". This plan converts the existing JSDoc knowledge
+into a "Define" section of the docs.
+
+## Current state
+
+- Docs live in `apps/docs/content/docs/*.mdx` (Fumadocs MDX). Current pages:
+ `index`, `installation`, `quickstart`, `installing-plugins`, `porting`,
+ `harnesses`, `authoring-a-harness`, `origins`.
+- Sidebar order is `apps/docs/content/docs/meta.json`:
+ ```json
+ {
+ "title": "Docs",
+ "root": true,
+ "pages": [
+ "---Get started---",
+ "index", "installation", "quickstart",
+ "---Guides---",
+ "installing-plugins", "porting", "harnesses", "authoring-a-harness",
+ "---Project---",
+ "origins"
+ ]
+ }
+ ```
+- Page frontmatter convention (from `quickstart.mdx`):
+ ```md
+ ---
+ title: Quick start
+ description: Define a plugin and compile it to every harness in a few minutes.
+ ---
+ ```
+- Code blocks use plain fenced blocks with `title="plugin.ts"` meta; a
+ `compact` meta flag exists (`source.config.ts` wires `remarkCodeMeta`).
+ A ` ` MDX component exists (used in `harnesses.mdx`) —
+ generated from the SDK's `supportMatrix()` at build time.
+- **The content source of truth** is `packages/agent-plugin-sdk/src/types.ts`
+ (472 lines, exhaustively JSDoc'd). Key facts per feature (verify against the
+ file while writing; do not invent):
+ - **Skill** (`Skill`): kebab-case `name` (max 64 chars), `description` max
+ 1024 chars ("the tightest limit across harnesses"; description drives
+ triggering — front-load trigger conditions), `instructions` body,
+ `allowedTools` (Claude Code, Pi), `disableModelInvocation` (Pi),
+ `license`, `metadata` (OpenCode, Pi), `frontmatter` escape hatch (merged,
+ SDK fields win on clash), `resources` (extra files in the skill dir).
+ - **Command** (`Command`): `$ARGUMENTS` portable everywhere; `$1, $2…`
+ authored 1-based, Claude emitter rewrites to 0-based; `` !`shell` `` and
+ `@file` pass through (Claude, OpenCode); `argumentHint`; `allowedTools`
+ (Claude); `frontmatter` (ignored by Gemini TOML and Cursor plain md);
+ `harness` overrides: `claude.model`, `opencode.model`,
+ `copilot.model`/`copilot.agent`.
+ - **Subagent** (`Subagent`): `prompt` maps to Claude/OpenCode body, Codex
+ `developer_instructions`; `tools` (Claude); `frontmatter` (YAML harnesses;
+ Codex TOML ignores); `harness` per-target models incl.
+ `gemini.temperature`/`maxTurns`, `opencode.mode`
+ (`primary`/`subagent`/`all`). **Pi has no subagents** — structured warning.
+ - **Hook** (`Hook`): portable `HookEvent` union — `pre-tool-use`,
+ `post-tool-use`, `stop`, `user-prompt-submit`, `session-start`,
+ `notification`, `permission-request`, `subagent-stop`, `pre-compact`,
+ `session-end`; SDK translates spellings (`pre-tool-use` → Claude
+ `PreToolUse`, Copilot `preToolUse`, Gemini `BeforeTool`); `matcher`,
+ `timeout`, `async` (Claude native; dropped-with-warning elsewhere),
+ `HookCommand` object form with `powershell` (Copilot); per-harness
+ `event`/`matcher` overrides where `matcher: undefined` explicitly clears.
+ Pi/OpenCode hooks are code-only → note instead of broken artifact.
+ - **MCP** (`McpServer`): `McpStdioServer` (command/args/env/cwd) and
+ `McpHttpServer` (url/headers); Claude/Codex `.mcp.json`, OpenCode
+ `opencode.json` `mcp` key; **Pi has no native MCP** → warning.
+ - **Tools** (`ToolsModule` + `defineTool`): author one `tools.ts` module
+ default-exporting `defineTool(...)[]`; the build copies it into every
+ harness output and generates glue — an MCP server for Claude/Codex, native
+ extension/plugin for Pi/OpenCode. Local invocation:
+ `ap-sdk tools --call --args '{...}'` (no harness needed). Working
+ examples: `packages/agent-plugin-sdk/examples/echo-tool/` and
+ `examples/planreview/` (has its own README).
+ - **Companion files** (`PluginFile` / `plugin.files`): emitted verbatim into
+ every build tree; referenced via plugin-root variables (e.g.
+ `${CLAUDE_PLUGIN_ROOT}/hooks/notify.sh`); `executable` bit preserved.
+ - **Instructions** (`plugin.instructions`): compiled to `CLAUDE.md` /
+ `AGENTS.md` / `GEMINI.md`; installed as an id-keyed comment block that
+ merges idempotently (`mergeMarkdownBlock` in `src/install.ts`).
+- Helper loaders exist and should appear in examples where natural: `readText`,
+ `readTextFrom`, `readDir` (see `porting.mdx` "Keep your files on disk"
+ section for usage style).
+- The repo's own AGENTS.md says docs-only changes to `apps/docs/` need **no**
+ changelog.
+
+## Commands you will need
+
+| Purpose | Command | Expected on success |
+|---|---|---|
+| Install deps | `pnpm install` | exit 0 |
+| Docs build (MDX compile) | `pnpm turbo build --filter=@jal-co/docs` | exit 0 |
+| Docs dev preview | `pnpm --filter @jal-co/docs dev` | serves locally |
+| Full gate | `pnpm turbo typecheck test lint build` | all tasks pass |
+
+## Scope
+
+**In scope** (the only files you should modify/create):
+
+- `apps/docs/content/docs/skills.mdx` (create)
+- `apps/docs/content/docs/commands.mdx` (create)
+- `apps/docs/content/docs/subagents.mdx` (create)
+- `apps/docs/content/docs/hooks.mdx` (create)
+- `apps/docs/content/docs/mcp.mdx` (create)
+- `apps/docs/content/docs/tools.mdx` (create)
+- `apps/docs/content/docs/instructions-and-files.mdx` (create)
+- `apps/docs/content/docs/meta.json` (add a `---Define---` section)
+- `apps/docs/content/docs/index.mdx` (add links to the new pages in "Next steps")
+
+**Out of scope** (do NOT touch):
+
+- `packages/agent-plugin-sdk/**` — content is *derived from* the code; never
+ change code to match docs. If docs and code disagree, the code wins — report
+ the discrepancy instead.
+- `apps/docs/src/**` — no new components needed; use existing MDX elements.
+- `quickstart.mdx`, `harnesses.mdx`, `porting.mdx` — cross-link *to* them, don't
+ edit them (except: no edits at all in this plan).
+
+## Git workflow
+
+- Branch: `docs/feature-pages`
+- Commit style: `docs: add per-feature reference pages` (matches `git log`, e.g.
+ `docs: porting guide + agent skill, and document unsupported-feature behavior`)
+- Do NOT push or open a PR unless the operator instructed it.
+
+## Steps
+
+### Step 1: Add the sidebar section
+
+In `apps/docs/content/docs/meta.json`, insert a `---Define---` section between
+"Get started" and "Guides":
+
+```json
+"---Define---",
+"skills", "commands", "subagents", "hooks", "mcp", "tools", "instructions-and-files",
+```
+
+**Verify**: `pnpm turbo build --filter=@jal-co/docs` → exit 0 (build fails on a
+meta entry with no page only after pages exist — run this verify after Step 2
+if it errors on missing pages).
+
+### Step 2: Write the seven pages
+
+For each page, follow this structure (matching the tone of `porting.mdx` /
+`harnesses.mdx` — terse, code-first, no marketing):
+
+1. Frontmatter: `title` + one-line `description`.
+2. A 2–4 sentence intro: what the feature is, what it compiles to per harness.
+3. A complete, runnable `plugin.ts` example (fenced block,
+ `title="plugin.ts"`) exercising the feature's main fields. Base examples on
+ `packages/agent-plugin-sdk/examples/git-helper/plugin.ts` (skills/commands/
+ subagents) and `examples/echo-tool/` + `examples/planreview/` (tools).
+4. A field-reference section: every field from the corresponding interface in
+ `src/types.ts`, one row/bullet each, **including which harnesses honor it**
+ (this info is in the JSDoc — transcribe, don't invent).
+5. A "Portability notes" section: what happens on harnesses that can't
+ represent the feature (structured warnings, never silent drops — link to
+ `/docs/harnesses` for the matrix and warning semantics).
+6. Cross-links: each page links the next page in the section and `/docs/harnesses`.
+
+Page-specific requirements:
+
+- `skills.mdx` — cover `resources`, the 1024-char description limit and
+ "front-load trigger conditions" guidance, `frontmatter` escape hatch.
+- `commands.mdx` — the argument-templating table (`$ARGUMENTS` vs `$1` with the
+ Claude 0-based rewrite), `harness` model overrides example.
+- `subagents.mdx` — per-harness `harness` bag example (claude/codex/opencode/
+ gemini/copilot), the Pi unsupported note.
+- `hooks.mdx` — the full `HookEvent` table with per-harness native spellings
+ (from the `HookEvent` JSDoc and `src/harnesses/hooks.ts` if needed),
+ `async`, the object `HookCommand` with `powershell`, per-harness
+ event/matcher override example (the plan-review Claude
+ `PermissionRequest` vs Codex `Stop` example from the `Hook` JSDoc).
+- `mcp.mdx` — stdio + http examples, where each harness's config lands
+ (`.mcp.json`, `opencode.json`), Pi warning.
+- `tools.mdx` — `defineTool` + `tools.module`, the generated-glue explanation
+ (MCP server for Claude/Codex, native for Pi/OpenCode), the
+ `ap-sdk tools --call` local-invocation loop.
+- `instructions-and-files.mdx` — `instructions` (id-keyed idempotent merge
+ block), `files` companion files with `readDir`, `SkillResource` vs
+ `PluginFile` distinction, plugin-root variable usage.
+
+**Verify** (after all seven): `pnpm turbo build --filter=@jal-co/docs` → exit 0;
+`ls apps/docs/content/docs/*.mdx | wc -l` → `15`.
+
+### Step 3: Link from the intro page
+
+In `apps/docs/content/docs/index.mdx`, extend the "## Next steps" section with
+links to the new Define pages (one line each). Don't restructure the page.
+
+**Verify**: `grep -c "/docs/hooks\|/docs/skills\|/docs/tools" apps/docs/content/docs/index.mdx` → ≥ 3.
+
+### Step 4: Accuracy pass
+
+For each page, re-open the corresponding interface in
+`packages/agent-plugin-sdk/src/types.ts` and confirm every claimed
+field/harness-support statement matches the JSDoc. Fix any invented claims.
+
+**Verify**: `pnpm turbo typecheck test lint build` → all tasks pass.
+
+## Test plan
+
+Docs-only — no unit tests. Verification is the MDX build (a broken link in
+`meta.json` or invalid frontmatter fails `pnpm turbo build --filter=@jal-co/docs`)
+plus the Step 4 accuracy pass.
+
+## Done criteria
+
+- [ ] Seven new pages exist and appear in the sidebar under a "Define" section
+- [ ] `pnpm turbo build --filter=@jal-co/docs` exits 0
+- [ ] Every example on the pages is complete (imports + default export), not a fragment
+- [ ] Each page states per-harness support/portability behavior sourced from `types.ts` JSDoc
+- [ ] `index.mdx` links the new pages
+- [ ] No `.tegami/` changelog added (docs-only — repo AGENTS.md §3)
+- [ ] No files outside the in-scope list modified (`git status`)
+- [ ] `plans/README.md` status row updated
+
+## STOP conditions
+
+Stop and report back if:
+
+- `meta.json` no longer matches the excerpt (docs restructured since planning).
+- A statement you need for a page contradicts between `types.ts` JSDoc and the
+ emitter code in `src/harnesses/*.ts` — report the discrepancy; do not pick one
+ silently.
+- You feel a new MDX component is required — the existing elements suffice;
+ needing more means the plan drifted.
+
+## Maintenance notes
+
+- These pages duplicate knowledge that lives in `types.ts` JSDoc. When a field
+ is added or its harness support changes, the matching page must be updated —
+ consider (later, out of scope) generating the field tables from the types the
+ way ` ` is generated from `supportMatrix()`.
+- Plan 009 (API reference) complements this; if it lands, cross-link each
+ feature page to its generated interface reference.
+- Reviewer: spot-check three random field claims against `src/types.ts`.
diff --git a/plans/004-port-as-wedge.md b/plans/004-port-as-wedge.md
new file mode 100644
index 0000000..b75c9a0
--- /dev/null
+++ b/plans/004-port-as-wedge.md
@@ -0,0 +1,207 @@
+# Plan 004: Make `ap-sdk port` the acquisition wedge (positioning + content)
+
+> **Executor instructions**: Follow this plan step by step. Run every
+> verification command and confirm the expected result before moving to the
+> next step. If anything in the "STOP conditions" section occurs, stop and
+> report — do not improvise. When done, update the status row for this plan
+> in `plans/README.md`.
+>
+> **Drift check (run first)**: `git diff --stat fe6fb92..HEAD -- README.md apps/docs/content/docs packages/agent-plugin-sdk/src/port.ts`
+> If any in-scope file changed since this plan was written, compare the
+> "Current state" excerpts against the live code before proceeding; on a
+> mismatch, treat it as a STOP condition.
+
+## Status
+
+- **Priority**: P1
+- **Effort**: M
+- **Risk**: LOW
+- **Depends on**: 001 (README overhaul — this plan edits the sections 001 creates)
+- **Category**: direction
+- **Planned at**: commit `fe6fb92`, 2026-07-01
+
+## Why this matters
+
+Almost nobody adopting this SDK starts from zero — the addressable audience is
+people who **already** maintain a Claude Code plugin, a `.cursor/` rules setup,
+Copilot prompt files, etc., and want them on other harnesses. The SDK already
+has the killer feature for them: `ap-sdk port` auto-detects an existing native
+layout and generates a portable `plugin.ts` that loads their files
+(`src/port.ts`, 492 lines, shipped recently — commit `efd6a3e`). But the
+positioning buries it: the README leads with `definePlugin` from scratch, and
+the docs have a single porting page framed as Claude-only ("Port a Claude Code
+plugin") even though the detector handles nine source layouts. This plan
+repositions porting as a co-equal entry path and expands its docs surface so
+each "port from X" query has a landing page.
+
+## Current state
+
+- `packages/agent-plugin-sdk/src/port.ts` — `portPlugin(dir)` detects the
+ source layout and returns `{ detected, counts, code }`. The docs page
+ (`porting.mdx`) states it auto-detects: "Claude Code, Codex, Gemini, Copilot,
+ Cursor, Windsurf, OpenCode, Pi, or a generic `skills/` + `commands/` +
+ `agents/` tree". **Verify this list against the detection logic in
+ `src/port.ts` before writing any page** — the docs pages must claim exactly
+ what the detector does.
+- `apps/docs/content/docs/porting.mdx` — the single existing porting page.
+ Frontmatter: `title: Port a Claude Code plugin`. Structure: "The fast path:
+ `ap-sdk port`" (CLI usage), "The mapping" (Claude→ap-sdk table), "Keep your
+ files on disk" (`readText`/`readTextFrom`/`readDir` usage).
+- `apps/docs/content/docs/meta.json` — sidebar; `porting` sits in the
+ `---Guides---` section. (If plan 003 landed, there is also a `---Define---`
+ section — leave it alone.)
+- Root `README.md` — after plan 001, has an "Already have a plugin?" section
+ showing `npx ap-sdk port`. This plan promotes that section **above** the
+ from-scratch example.
+- `skills/port-claude-plugin/` — an in-repo agent skill for porting exists
+ (recon signal that porting is a maintained workflow).
+- `apps/docs/content/docs/index.mdx` — intro page; currently pitches only the
+ define-from-scratch path.
+- CLI porting UX (from `src/cli.ts` `runPort`, ~line 470): prints
+ `Ported → ` and next steps `ap-sdk check` / `ap-sdk build`.
+
+## Commands you will need
+
+| Purpose | Command | Expected on success |
+|---|---|---|
+| Install deps | `pnpm install` | exit 0 |
+| Docs build | `pnpm turbo build --filter=@jal-co/docs` | exit 0 |
+| Read the detector | `sed -n '1,120p' packages/agent-plugin-sdk/src/port.ts` | source, not guesses |
+| Full gate | `pnpm turbo typecheck test lint build` | all tasks pass |
+
+## Scope
+
+**In scope** (the only files you should modify/create):
+
+- `apps/docs/content/docs/porting.mdx` — retitle/reframe as the harness-neutral hub
+- `apps/docs/content/docs/porting/` — new per-source subpages (see Step 2; create `meta.json` inside if Fumadocs requires one for the folder)
+- `apps/docs/content/docs/meta.json` — sidebar wiring for the subpages
+- `apps/docs/content/docs/index.mdx` — add the "already have a plugin" entry path
+- `README.md` — reorder so porting appears as a first-class entry path
+
+**Out of scope** (do NOT touch):
+
+- `packages/agent-plugin-sdk/src/**` — no code changes. If the detector doesn't
+ support a layout you want a page for, the page doesn't get written (STOP and
+ note it) — never overclaim.
+- Paid/SEO tooling, analytics, ads — content only.
+- The homepage app (`apps/docs/src/app/page.tsx`) — a landing-page redesign is
+ bigger than this plan; note it in Maintenance if tempting.
+
+## Git workflow
+
+- Branch: `docs/port-wedge`
+- Commit style: `docs: reposition porting as a first-class entry path`
+- Do NOT push or open a PR unless the operator instructed it.
+
+## Steps
+
+### Step 1: Verify the detector's actual source-layout list
+
+Read `packages/agent-plugin-sdk/src/port.ts` and list which source layouts
+`portPlugin` actually detects and what it extracts for each (manifest,
+instructions, skills, commands, agents, hooks, companion dirs). Write the list
+down — it drives Steps 2–4. Cross-check with `test/port.test.ts`.
+
+**Verify**: your list matches `porting.mdx`'s claim ("Claude Code, Codex,
+Gemini, Copilot, Cursor, Windsurf, OpenCode, Pi, or a generic tree") — if it
+doesn't, the *code* is the truth; adjust the pages accordingly and flag the
+stale claim in `porting.mdx` as part of Step 2.
+
+### Step 2: Reframe `porting.mdx` as the neutral hub
+
+- Change frontmatter to `title: Port an existing plugin` with a description
+ naming several sources ("Claude Code plugin, Cursor rules, Copilot prompts…").
+- Keep the existing "fast path" + `readText`/`readDir` content.
+- Move the Claude-specific mapping table into the Claude subpage (Step 3) and
+ replace it with a short per-source table of links: source layout → subpage.
+- Keep the URL `/docs/porting` (do not rename the file — it's linked from the
+ CLI-adjacent docs and possibly externally).
+
+**Verify**: `pnpm turbo build --filter=@jal-co/docs` → exit 0.
+
+### Step 3: Write per-source subpages
+
+Create `apps/docs/content/docs/porting/.mdx` for each layout the
+detector supports (per Step 1 — expected: `claude.mdx`, `codex.mdx`,
+`gemini.mdx`, `copilot.mdx`, `cursor.mdx`, `windsurf.mdx`, `opencode.mdx`,
+`pi.mdx`; skip any the detector doesn't truly handle). Each page is short
+(~40–80 lines) and follows one fixed structure so they're cheap to maintain:
+
+1. Frontmatter: `title: Port a plugin to every harness`, description
+ naming the other seven targets (this is the SEO surface — each page matches
+ the query "port plugin to ").
+2. What the detector looks for in that layout (from Step 1 — e.g. for Claude:
+ `.claude-plugin/plugin.json`, `CLAUDE.md`, `skills/**/SKILL.md`,
+ `agents/*.md`, `commands/*.md`, `hooks/hooks.json`).
+3. The command: `npx ap-sdk port ./my-plugin` (+ `--dry-run`).
+4. The mapping table for that source (Claude's moves here from the old
+ `porting.mdx`; write the others from the Step 1 findings — **only rows you
+ verified in `port.ts`**).
+5. "What doesn't carry over" — anything the source has that the portable model
+ drops or TODO-flags (e.g. `port.ts:474` comments `// TODO: native ""
+ had no portable event; defaulted` — mention that unmapped hook events are
+ flagged in the generated code).
+6. Next steps: `ap-sdk check` → `ap-sdk build` → link `/docs/harnesses`.
+
+Wire the subpages into the sidebar: in `meta.json`, replace the `porting` entry
+with `porting` followed by the folder pages, or use a nested `porting/meta.json`
+— match how Fumadocs handles folders in this repo (check
+`apps/docs/source.config.ts` and existing structure; there are currently no
+nested folders, so consult Fumadocs' file-routing conventions and keep the hub
+page at `/docs/porting`).
+
+**Verify**: `pnpm turbo build --filter=@jal-co/docs` → exit 0; each new page
+reachable in the sidebar (run `pnpm --filter @jal-co/docs dev` and check, or
+confirm the build's route output includes `/docs/porting/claude` etc.).
+
+### Step 4: Promote porting in the README and docs intro
+
+- `README.md`: move the "Already have a plugin?" section (created by plan 001)
+ directly under the hero code example, so the two entry paths read: "start
+ from scratch" / "port what you have". Two to four lines each, links to
+ `/docs/quickstart` and `/docs/porting`.
+- `apps/docs/content/docs/index.mdx`: in the intro (before "## Why"), add one
+ sentence + link: already have a plugin for one of these harnesses? →
+ `/docs/porting`.
+
+**Verify**: `grep -n "port" README.md | head -5` → porting appears before the
+"Install" section; docs build exits 0.
+
+## Test plan
+
+Docs/content only — no unit tests. The load-bearing verification is Step 1
+(every claim traced to `port.ts` or `test/port.test.ts`) and the MDX build.
+
+## Done criteria
+
+- [ ] `/docs/porting` is harness-neutral; per-source subpages exist for exactly the layouts `port.ts` detects
+- [ ] Claude mapping table lives on `/docs/porting/claude`, not the hub
+- [ ] README shows the port path above the fold (before Install)
+- [ ] Every mapping-table row traceable to `src/port.ts` (Step 1 notes kept in the PR description)
+- [ ] `pnpm turbo build --filter=@jal-co/docs` and the full gate exit 0
+- [ ] No `.tegami/` changelog (docs + README only; no `packages/agent-plugin-sdk` changes)
+- [ ] No files outside the in-scope list modified (`git status`)
+- [ ] `plans/README.md` status row updated
+
+## STOP conditions
+
+Stop and report back if:
+
+- Plan 001 has not landed and there is no "Already have a plugin?" README
+ section to promote — do 001 first or fold its Step 2 in here, but report the
+ dependency violation rather than silently merging plans.
+- `port.ts` detects meaningfully fewer layouts than `porting.mdx` claims —
+ report the gap; writing pages for unsupported sources is overclaiming.
+- Fumadocs folder routing in this repo won't allow a `porting/` subtree with
+ the hub at `/docs/porting` after one reasonable attempt — report with what
+ you tried.
+
+## Maintenance notes
+
+- When a new harness gains port-detection support in `port.ts`, add its
+ subpage — the fixed page structure makes that a 30-minute task.
+- Follow-up deferred out of this plan: a demo GIF/asciicast of `ap-sdk port`
+ for the README and homepage; a homepage hero variant leading with porting.
+- Reviewer: the risk in this plan is overclaiming — check the mapping tables
+ against `port.ts`, not against the old docs.
diff --git a/plans/005-uninstall-and-manifest.md b/plans/005-uninstall-and-manifest.md
new file mode 100644
index 0000000..59f93db
--- /dev/null
+++ b/plans/005-uninstall-and-manifest.md
@@ -0,0 +1,294 @@
+# Plan 005: Add `ap-sdk uninstall` backed by an install manifest
+
+> **Executor instructions**: Follow this plan step by step. Run every
+> verification command and confirm the expected result before moving to the
+> next step. If anything in the "STOP conditions" section occurs, stop and
+> report — do not improvise. When done, update the status row for this plan
+> in `plans/README.md`.
+>
+> **Drift check (run first)**: `git diff --stat fe6fb92..HEAD -- packages/agent-plugin-sdk/src packages/agent-plugin-sdk/test`
+> If any in-scope file changed since this plan was written, compare the
+> "Current state" excerpts against the live code before proceeding; on a
+> mismatch, treat it as a STOP condition.
+
+## Status
+
+- **Priority**: P2
+- **Effort**: M
+- **Risk**: MED — deleting files in user harness dirs; must never remove
+ anything the plugin didn't write
+- **Depends on**: none
+- **Category**: dx
+- **Planned at**: commit `fe6fb92`, 2026-07-01
+
+## Why this matters
+
+`ap-sdk install` writes files into up to eight harness directories (`.claude/`,
+`.cursor/`, `~/.codex/`, …) and merges blocks into shared config files — but
+keeps no record and offers no removal path. A user who tries the tool and wants
+to back out must hand-hunt files across eight directory conventions. That makes
+people hesitant to run `install` at all. An install manifest plus an
+`uninstall` command makes trying the tool a reversible, low-stakes act.
+
+## Current state
+
+- `packages/agent-plugin-sdk/src/install.ts` — `installSkills(plugin, options)`
+ returns `InstalledItem[]`:
+ ```ts
+ export interface InstalledItem {
+ harness: HarnessId;
+ kind: "skill" | "command" | "mcp" | "context" | "subagent" | "hook" | "file";
+ name: string;
+ /** Absolute paths written (or that would be written, in dry-run). */
+ files: string[];
+ note?: string;
+ }
+ ```
+ So **the data needed for a manifest already exists** — it's returned and then
+ discarded.
+- Three write modes exist, and uninstall must treat them differently:
+ 1. **Whole files owned by the plugin** — skills (`installSkill`), commands
+ (`installCommand`), subagents (`installSubagent`), companion files
+ (`installFiles`). Safe to delete the exact paths recorded.
+ 2. **Merged markdown block** — `mergeMarkdownBlock(file, pluginId, instructions)`
+ wraps instructions in `` /
+ `` markers and replaces idempotently.
+ Uninstall = remove the marker block, keep the rest of the file.
+ 3. **Merged JSON keys** — `mergeJsonKey` (MCP servers into e.g.
+ `.mcp.json`/`opencode.json` under a `mergeKey`) and `mergeHooksInto`
+ (hook groups appended per event into a `hooks` key, deduped by
+ `JSON.stringify`). Uninstall = remove exactly the entries/groups this
+ plugin contributed, preserve everything else.
+- `src/cli.ts` — command surface: `build`, `install`, `check`, `tools`,
+ `add-harness`, `port`. `runInstall(plugin, args)` (~line 380) prints the
+ install report. Errors go through `fail()` (prints `✖`, exit 1).
+- Install scopes: `"project"` (cwd-relative dirs) and `"global"` (`~`-based) —
+ `InstallScope` in `src/harnesses/types.ts`.
+- Test pattern for filesystem tests: `test/install.test.ts:28-37` —
+ `mkdtempSync(join(tmpdir(), "aps-install-"))` + `process.chdir` in
+ `beforeEach`, cleanup in `afterEach`. `test/install-files.test.ts` also
+ exists; model new tests on these.
+- Repo AGENTS.md: new CLI command → `.tegami/` changelog, **minor** bump.
+
+## Commands you will need
+
+| Purpose | Command | Expected on success |
+|---|---|---|
+| Install deps | `pnpm install` | exit 0 |
+| SDK tests | `pnpm turbo test --filter=@jalco/ap-sdk` | all pass |
+| Typecheck | `pnpm turbo typecheck --filter=@jalco/ap-sdk` | exit 0 |
+| Full gate | `pnpm turbo typecheck test lint build` | all tasks pass |
+
+## Scope
+
+**In scope** (the only files you should modify/create):
+
+- `packages/agent-plugin-sdk/src/install.ts` — write the manifest at the end of a non-dry-run install
+- `packages/agent-plugin-sdk/src/uninstall.ts` (create)
+- `packages/agent-plugin-sdk/src/cli.ts` — HELP + `uninstall` dispatch + `runUninstall`
+- `packages/agent-plugin-sdk/src/index.ts` — export `uninstallPlugin` (match how `installSkills` is exported)
+- `packages/agent-plugin-sdk/test/uninstall.test.ts` (create)
+- `apps/docs/content/docs/installing-plugins.mdx` — document uninstall
+- `.tegami/2026-07-01-uninstall.md` (create)
+
+**Out of scope** (do NOT touch):
+
+- `src/harnesses/*.ts` — no per-harness changes; uninstall works from recorded
+ paths, not from harness knowledge.
+- Cleaning up installs made by **older SDK versions** (no manifest exists) —
+ explicitly unsupported in v1 of this feature; the command reports "no
+ manifest found" and exits cleanly.
+- `build` output (`.aps-out/`) — that's the user's to delete.
+
+## Git workflow
+
+- Branch: `feat/uninstall-manifest`
+- Commit style: `feat(sdk): record installs in a manifest and add ap-sdk uninstall`
+- Do NOT push or open a PR unless the operator instructed it.
+
+## Steps
+
+### Step 1: Design + write the manifest on install
+
+In `src/install.ts`, after the target loop in `installSkills`, when
+`!options.dryRun`, write a manifest capturing what was just installed:
+
+- Location: project scope → `/.ap-sdk/install-manifest.json`; global scope
+ → `~/.ap-sdk/install-manifest.json`. (One fixed, documented location per
+ scope; do NOT scatter per-harness manifests.)
+- Shape (versioned for forward compatibility):
+ ```json
+ {
+ "version": 1,
+ "plugins": {
+ "": {
+ "installedAt": "",
+ "scope": "project",
+ "items": [ { "harness": "claude", "kind": "skill", "name": "diff-review", "files": ["/abs/path/..."] } ]
+ }
+ }
+ }
+ ```
+ `items` is exactly the returned `InstalledItem[]` (minus `note`). Merge-write:
+ read existing manifest if present (tolerate/refuse invalid JSON the same way
+ `mergeJsonKey` does — throw with a "Refusing to overwrite" message), replace
+ the entry for this plugin id, preserve other plugins' entries.
+- For merged writes (`kind: "context"`, `"mcp"`, `"hook"`) the recorded `files`
+ entry is the merged config path; record enough to reverse them: add an
+ optional `detail` field on the manifest item — for `mcp`: the server names
+ installed; for `hook`: the native hook groups contributed (the
+ `config.hooks` object passed to `mergeHooksInto`); for `context`: nothing
+ extra (the marker block is derivable from the plugin id). Thread this data
+ out of `installMcp`/`installHooks` — extend `InstalledItem` with an optional
+ `detail?: unknown` rather than inventing a parallel structure.
+
+**Verify**: `pnpm turbo test --filter=@jalco/ap-sdk` → existing tests still
+pass (they call `installSkills` in temp dirs; a new `.ap-sdk/` dir in the temp
+cwd must not break them — if one asserts on exact directory listings, check it).
+
+### Step 2: Implement `uninstallPlugin`
+
+Create `src/uninstall.ts` exporting:
+
+```ts
+export interface UninstallOptions {
+ scope?: InstallScope; // default "project"
+ targets?: HarnessId[]; // default: all harnesses in the manifest entry
+ dryRun?: boolean;
+}
+export interface RemovedItem { harness: HarnessId; kind: string; name: string; files: string[]; note?: string; }
+export function uninstallPlugin(pluginId: string, options?: UninstallOptions): RemovedItem[];
+```
+
+Behavior, per manifest item (filtered by `targets` when given):
+
+- `skill` / `command` / `subagent` / `file`: delete each recorded path **iff it
+ still exists**; after deleting skill files, remove the skill directory if now
+ empty (use `rmSync` on files, `rmdirSync` best-effort on the dir). Never
+ glob; only recorded paths.
+- `context`: read the file; remove the
+ `…`
+ block (reuse/extract the regex from `mergeMarkdownBlock` — export a helper
+ from `install.ts` rather than duplicating the escape logic); write back. If
+ the file is then empty/whitespace, leave the empty file (deleting a shared
+ `CLAUDE.md` is riskier than leaving it).
+- `mcp`: parse the JSON config; delete only the server names in `detail` from
+ the merge key; write back preserving everything else. Invalid JSON → skip
+ with a note (never throw mid-uninstall; collect notes).
+- `hook`: parse the JSON config; remove exactly the groups recorded in
+ `detail` (match by `JSON.stringify` equality, mirroring `mergeHooksInto`'s
+ dedupe); write back.
+- After processing, remove the plugin's entry from the manifest (or the whole
+ manifest file if it becomes empty). In `dryRun`, delete nothing, return what
+ would be removed.
+- No manifest / no entry for the id → throw an `Error` with a clear message
+ (the CLI turns it into `fail()`).
+
+**Verify**: `pnpm turbo typecheck --filter=@jalco/ap-sdk` → exit 0.
+
+### Step 3: Wire the CLI
+
+In `src/cli.ts`:
+
+1. HELP: `ap-sdk uninstall [options] Remove a previously installed plugin from local harness dirs`,
+ with `--global`, `--target`, `--dry-run` noted as applying, and an example.
+2. Dispatch: `uninstall` needs no plugin module — only the id string. Handle it
+ in the pre-resolution group (like `add-harness`/`port`): the second
+ positional is the **plugin id**, not a path. Call `uninstallPlugin`, print a
+ report symmetrical to `runInstall`'s (✓ per removed item, `(skipped)` +
+ dim note for skips, `Would remove` under `--dry-run`).
+
+**Verify**: build the package (`pnpm turbo build --filter=@jalco/ap-sdk`), then
+in a temp dir: install the example plugin
+(`node /packages/agent-plugin-sdk/dist/cli.js install /packages/agent-plugin-sdk/examples/git-helper/plugin.ts -t claude`),
+confirm `.claude/` and `.ap-sdk/install-manifest.json` exist, then
+`node .../cli.js uninstall git-helper` → recorded files gone, manifest entry
+gone, exit 0.
+
+### Step 4: Tests
+
+Create `test/uninstall.test.ts` (temp-dir pattern from
+`test/install.test.ts:28-37`). Cases:
+
+1. install → uninstall round-trip for a plugin with a skill + command
+ (claude target): files exist after install, gone after uninstall, skill dir
+ removed, manifest entry removed.
+2. Context block: pre-seed a `CLAUDE.md` with user content, install a plugin
+ with `instructions`, uninstall → user content intact, marker block gone.
+3. MCP merge: pre-seed `opencode.json` with an existing server (copy the
+ fixture from `test/install.test.ts`), install plugin with a server,
+ uninstall → plugin's server gone, pre-existing server and other keys intact.
+4. Hooks: install a plugin with one hook into a config that already has a
+ foreign hook group; uninstall → only the plugin's group removed.
+5. `dryRun: true` removes nothing but reports the paths.
+6. Unknown plugin id → throws with a message containing the id.
+7. Recorded file already deleted by the user → uninstall does not throw;
+ reports it skipped.
+
+**Verify**: `pnpm turbo test --filter=@jalco/ap-sdk` → all pass, including 7
+new tests.
+
+### Step 5: Docs + changelog
+
+- `apps/docs/content/docs/installing-plugins.mdx`: add an "Uninstalling"
+ section — command, `--dry-run`, the manifest location, and the limitation
+ (installs made before this SDK version have no manifest and can't be
+ auto-removed).
+- `.tegami/2026-07-01-uninstall.md`:
+ ```md
+ ---
+ packages:
+ "@jalco/ap-sdk": minor
+ ---
+
+ ## Add `ap-sdk uninstall`
+
+ `install` now records what it wrote to an install manifest
+ (`.ap-sdk/install-manifest.json`), and `ap-sdk uninstall ` cleanly
+ reverses it — deleting the plugin's files and removing only its entries from
+ merged configs (instruction blocks, MCP servers, hooks).
+ ```
+
+**Verify**: `pnpm turbo typecheck test lint build` → all tasks pass.
+
+## Test plan
+
+See Step 4 — seven cases listed there, modeled on `test/install.test.ts`. The
+merge-reversal cases (2–4) are the risk-bearing ones; they assert *preservation
+of foreign content*, not just removal.
+
+## Done criteria
+
+- [ ] `install` (non-dry-run) writes/updates the manifest; dry-run does not
+- [ ] `uninstall` removes only recorded files/entries; foreign content in shared configs survives (tests 2–4)
+- [ ] `node dist/cli.js --help | grep uninstall` → present
+- [ ] Round-trip smoke test in Step 3 passes
+- [ ] 7 new tests pass; full suite green
+- [ ] `.tegami/2026-07-01-uninstall.md` exists with `minor` bump
+- [ ] `pnpm turbo typecheck test lint build` exits 0
+- [ ] No files outside the in-scope list modified (`git status`)
+- [ ] `plans/README.md` status row updated
+
+## STOP conditions
+
+Stop and report back if:
+
+- `install.ts`'s merge functions don't match the excerpts (drifted).
+- Reversing the hook merge requires information `mergeHooksInto` doesn't have
+ at install time — report the design gap rather than guessing group identity.
+- You're tempted to delete a directory not created from recorded paths (e.g.
+ "clean up `.claude/skills` if empty" beyond the plugin's own skill dirs) —
+ that's how user data gets destroyed; recorded paths only.
+- Existing install tests fail because of the manifest write and the fix isn't
+ a test-fixture adjustment.
+
+## Maintenance notes
+
+- Every new `kind` added to `InstalledItem` in the future MUST get an uninstall
+ branch and a round-trip test — reviewers should reject install-feature PRs
+ that skip it.
+- Manifest `version: 1` exists so the shape can evolve; bump it on any breaking
+ shape change and keep a reader for v1.
+- Deferred: uninstalling pre-manifest installs (heuristic cleanup) and
+ `ap-sdk list` (print installed plugins from the manifest) — the latter is a
+ natural, cheap follow-up.
diff --git a/plans/006-dev-watch-mode.md b/plans/006-dev-watch-mode.md
new file mode 100644
index 0000000..381b909
--- /dev/null
+++ b/plans/006-dev-watch-mode.md
@@ -0,0 +1,262 @@
+# Plan 006: Add `ap-sdk dev` — watch the plugin and rebuild/reinstall on change
+
+> **Executor instructions**: Follow this plan step by step. Run every
+> verification command and confirm the expected result before moving to the
+> next step. If anything in the "STOP conditions" section occurs, stop and
+> report — do not improvise. When done, update the status row for this plan
+> in `plans/README.md`.
+>
+> **Drift check (run first)**: `git diff --stat fe6fb92..HEAD -- packages/agent-plugin-sdk/src packages/agent-plugin-sdk/test`
+> If any in-scope file changed since this plan was written, compare the
+> "Current state" excerpts against the live code before proceeding; on a
+> mismatch, treat it as a STOP condition.
+
+## Status
+
+- **Priority**: P2
+- **Effort**: M
+- **Risk**: MED — module-cache invalidation for re-imports is the tricky part
+- **Depends on**: none (pairs well with 005: dev + uninstall = safe iteration loop)
+- **Category**: dx
+- **Planned at**: commit `fe6fb92`, 2026-07-01
+
+## Why this matters
+
+Plugin authors — the SDK's core audience — iterate by editing `plugin.ts`, then
+manually re-running `ap-sdk build` and/or `ap-sdk install` and reopening their
+harness. The SDK's own developers get `tsup --watch`; plugin authors get
+nothing. `ap-sdk dev` closes the loop: watch the plugin module and its
+dependencies (skill resource files, the tools module, files loaded via
+`readText`/`readDir`), rebuild on change, and optionally reinstall — making the
+edit→try cycle seconds instead of a command ritual.
+
+## Current state
+
+- `packages/agent-plugin-sdk/src/cli.ts`:
+ - Commands: `build`, `install`, `check`, `tools`, `add-harness`, `port`.
+ - `loadPlugin(path)` (~line 180) does a bare `await import(pathToFileURL(path).href)`.
+ **ESM caches by URL** — re-importing the same path returns the stale module.
+ A watch loop must bust the cache, e.g. by appending a unique query
+ (`?t=${Date.now()}`) to the import URL. Verify `tsx`'s registered loader
+ (see `enableTypeScript()`, ~line 165) tolerates the query param; if it
+ doesn't, STOP condition.
+ - `runBuild(plugin, args, pluginPath)` (~line 300) writes trees under
+ `--out` (default `.aps-out`) via `writeTree` and prints warnings.
+ - `runInstall(plugin, args)` calls `installSkills`.
+- `src/build.ts` — `build(plugin, options)` is **pure** (in-memory trees), so
+ rebuilding repeatedly is cheap and safe.
+- File-loading helpers: plugins may inline content read at *module load time*
+ via `readText`/`readTextFrom`/`readDir` (exported from the SDK; used heavily
+ by ported plugins — see `apps/docs/content/docs/porting.mdx`). This means a
+ change to a referenced markdown file only takes effect on re-import, and the
+ watcher must watch those files too. Simplest correct scope for v1: watch the
+ **plugin file's directory tree** (recursive), debounced, excluding the output
+ dir and `node_modules`.
+- Node ≥ 18 is the engine floor (`package.json` `engines`). `fs.watch` with
+ `{ recursive: true }` works on macOS/Windows on 18+, and on Linux only from
+ Node 20. Runtime deps are lean (`tar`, `tsx`, `yaml`) and the repo's global
+ guidance is to avoid new deps when the runtime suffices — but a broken
+ watcher on Linux+Node18 is worse; see Step 1 decision gate.
+- Test conventions: vitest, temp-dir pattern in `test/install.test.ts:28-37`.
+- Repo AGENTS.md: new CLI command → `.tegami/` changelog, **minor** bump.
+
+## Commands you will need
+
+| Purpose | Command | Expected on success |
+|---|---|---|
+| Install deps | `pnpm install` | exit 0 |
+| SDK tests | `pnpm turbo test --filter=@jalco/ap-sdk` | all pass |
+| Typecheck | `pnpm turbo typecheck --filter=@jalco/ap-sdk` | exit 0 |
+| Full gate | `pnpm turbo typecheck test lint build` | all tasks pass |
+| Manual smoke | see Step 4 | rebuild on file change |
+
+## Scope
+
+**In scope** (the only files you should modify/create):
+
+- `packages/agent-plugin-sdk/src/dev.ts` (create — the watch loop, exported for testability)
+- `packages/agent-plugin-sdk/src/cli.ts` — HELP + `dev` dispatch + `runDev`
+- `packages/agent-plugin-sdk/test/dev.test.ts` (create)
+- `apps/docs/content/docs/quickstart.mdx` — mention `ap-sdk dev` in the "try it locally" flow
+- `.tegami/2026-07-01-dev-watch.md` (create)
+- `packages/agent-plugin-sdk/package.json` — ONLY if Step 1 decides a watcher dep is required
+
+**Out of scope** (do NOT touch):
+
+- `src/build.ts`, `src/install.ts` — reuse as-is; no signature changes.
+- Harness hot-reload (telling Claude Code etc. to reload) — harness-specific
+ and out of reach; `dev` only refreshes the files on disk.
+- A long-lived MCP dev server for `tools` — deferred.
+
+## Git workflow
+
+- Branch: `feat/dev-watch`
+- Commit style: `feat(sdk): add ap-sdk dev watch mode`
+- Do NOT push or open a PR unless the operator instructed it.
+
+## Steps
+
+### Step 1: Decision gate — watcher mechanism
+
+Check the Node docs / runtime behavior for `fs.watch` recursive support on
+Linux for the repo's engine floor (`node >= 18`).
+
+- If targeting the floor honestly requires it, add the well-maintained
+ `chokidar` (v4+, slim, no globs) as a dependency and record the reasoning in
+ the commit body.
+- Otherwise use `fs.watch({ recursive: true })` and document (HELP + docs) that
+ `dev` requires Node 20+ on Linux.
+
+Either way: one mechanism, implemented in `src/dev.ts`, debounced (~150ms),
+ignoring the out dir, `node_modules`, `.git`, and dotfile dirs.
+
+**Verify**: a one-line note in the commit message states the choice and why.
+
+### Step 2: Implement the watch loop in `src/dev.ts`
+
+Export:
+
+```ts
+export interface DevOptions {
+ pluginPath: string; // absolute
+ targets?: HarnessId[];
+ out?: string; // default ".aps-out"
+ install?: boolean; // also run installSkills after each build
+ scope?: InstallScope; // for install
+ onCycle?: (result: { ok: boolean; error?: Error; warnings: number }) => void; // test hook
+ signal?: AbortSignal; // stop the loop (tests + SIGINT)
+}
+export function startDev(options: DevOptions): Promise;
+```
+
+Loop behavior:
+
+1. Run one cycle immediately: fresh-import the plugin
+ (`import(pathToFileURL(path).href + "?t=" + Date.now())`), validate, run
+ `build`, write trees (reuse the same logic as `runBuild` — extract shared
+ code from `cli.ts` into `dev.ts` or a small helper rather than duplicating
+ the tools-module copy step: `readToolsModule` must apply here too).
+2. Watch `dirname(pluginPath)` recursively; on a debounced change, rerun the
+ cycle.
+3. A cycle failure (import error, `PluginValidationError`) must **not** exit
+ the loop — print the error (reuse the CLI's `✖`/warning styling) and keep
+ watching. This is the whole point of a dev mode.
+4. `install: true` → after a successful build, call `installSkills` with the
+ same targets/scope and print a one-line confirmation.
+5. Resolve when `signal` aborts.
+
+**Verify**: `pnpm turbo typecheck --filter=@jalco/ap-sdk` → exit 0.
+
+### Step 3: Wire the CLI
+
+In `src/cli.ts`:
+
+1. HELP: `ap-sdk dev [plugin] [options] Watch the plugin and rebuild on change`
+ — note `--install` (rebuild straight into local harness dirs), plus the
+ existing `--target/--out/--global` semantics; add an example
+ (`ap-sdk dev --install -t claude`).
+2. Add `--install` to `Args` parsing.
+3. `runDev`: resolve the plugin path (local only — **reject GitHub specs** with
+ `fail("dev requires a local plugin")`), call `startDev` with an
+ `AbortController` wired to SIGINT, print a startup banner
+ (`Watching — Ctrl-C to stop`).
+
+**Verify**: `pnpm turbo build --filter=@jalco/ap-sdk` → exit 0;
+`node dist/cli.js --help | grep "dev"` → present.
+
+### Step 4: Manual smoke test
+
+In a temp dir with a scaffolded plugin (from plan 002's `init`, or copy
+`examples/git-helper/plugin.ts`):
+
+1. `node /packages/agent-plugin-sdk/dist/cli.js dev` → initial build
+ output appears, process stays alive.
+2. Edit the skill description → within ~1s a rebuild line appears; the emitted
+ `SKILL.md` under `.aps-out/claude/` contains the new text.
+3. Introduce a syntax error → cycle prints an error, process still alive; fix
+ it → next cycle succeeds (proves cache-busting works).
+4. Ctrl-C exits 0.
+
+**Verify**: all four behaviors observed; paste the terminal transcript into the
+PR description.
+
+### Step 5: Tests
+
+Create `test/dev.test.ts` using `startDev` directly (temp-dir pattern from
+`test/install.test.ts:28-37`; use `onCycle` + `AbortController`, generous
+timeouts — watchers are timing-sensitive; keep assertions on cycle results, not
+wall-clock):
+
+1. Initial cycle builds: `onCycle` fires with `ok: true`; output tree exists.
+2. Changing the plugin file triggers a new cycle with the updated content
+ (assert the emitted file changed).
+3. A plugin file with a validation error yields `ok: false` and the loop
+ survives to a subsequent successful cycle after the file is fixed.
+4. Abort resolves the `startDev` promise.
+
+If watcher-based tests prove flaky in CI after a reasonable attempt, keep test
+1 and 4 (no watching involved beyond startup/teardown), convert 2 and 3 to the
+documented manual smoke (Step 4), and note it in the plan status — do NOT ship
+flaky tests.
+
+**Verify**: `pnpm turbo test --filter=@jalco/ap-sdk` → all pass, repeated 3×
+locally (`for i in 1 2 3; do pnpm turbo test --filter=@jalco/ap-sdk || break; done`).
+
+### Step 6: Docs + changelog
+
+- `quickstart.mdx`: in "## 4. Try it locally", add `npx ap-sdk dev --install -t claude`
+ with one sentence.
+- `.tegami/2026-07-01-dev-watch.md`:
+ ```md
+ ---
+ packages:
+ "@jalco/ap-sdk": minor
+ ---
+
+ ## Add `ap-sdk dev`
+
+ Watch mode for plugin authors: `ap-sdk dev` rebuilds on every change to the
+ plugin and its referenced files, and `--install` drops the result straight
+ into your local harness dirs. Errors keep the watcher alive.
+ ```
+
+**Verify**: `pnpm turbo typecheck test lint build` → all tasks pass.
+
+## Test plan
+
+See Step 5 (4 cases + flake fallback policy) and the Step 4 manual smoke
+transcript. Pattern: `test/install.test.ts` temp-dir setup.
+
+## Done criteria
+
+- [ ] `ap-sdk dev` runs the initial build and rebuilds on change (Step 4 transcript)
+- [ ] Broken plugin → error printed, loop survives, recovers on fix
+- [ ] `--install` reinstalls after each successful build
+- [ ] GitHub specs rejected with a clear error
+- [ ] New tests pass 3× consecutively
+- [ ] `.tegami/2026-07-01-dev-watch.md` exists with `minor` bump
+- [ ] `pnpm turbo typecheck test lint build` exits 0
+- [ ] No files outside the in-scope list modified (`git status`)
+- [ ] `plans/README.md` status row updated
+
+## STOP conditions
+
+Stop and report back if:
+
+- The `tsx` loader rejects or mis-handles query-param cache busting
+ (`plugin.ts?t=...`) — the re-import strategy is the plan's key assumption;
+ report alternatives (child-process-per-cycle) rather than improvising one.
+- `cli.ts`'s `loadPlugin`/`runBuild` don't match the "Current state" summary.
+- Watcher tests are flaky AND the fallback in Step 5 still leaves the suite
+ unstable.
+- You want to change `build()`/`installSkills` signatures — out of scope.
+
+## Maintenance notes
+
+- If cache-busted re-imports leak memory over very long sessions (each import
+ is a new module instance), the fix is a child process per cycle — noted here
+ so a future "dev mode leaks" report has its answer.
+- When plan 005's manifest lands, `dev --install` cycles will rewrite the
+ manifest each time — confirm that stays idempotent.
+- Deferred: watching files outside the plugin dir referenced by absolute path;
+ a `--serve-mcp` dev server for tools.
diff --git a/plans/007-artifacts/action.yml b/plans/007-artifacts/action.yml
new file mode 100644
index 0000000..7638be1
--- /dev/null
+++ b/plans/007-artifacts/action.yml
@@ -0,0 +1,37 @@
+name: Validate ap-sdk plugin
+description: Check and build an ap-sdk plugin repository.
+inputs:
+ plugin-path:
+ description: Path to the plugin file or directory.
+ default: .
+ targets:
+ description: Optional comma-separated target harness ids.
+ default: ""
+ fail-on-warnings:
+ description: Fail when build produces capability warnings.
+ default: "false"
+runs:
+ using: composite
+ steps:
+ - name: Check plugin
+ shell: bash
+ run: npx -y @jalco/ap-sdk check "${{ inputs.plugin-path }}"
+ - name: Build plugin
+ shell: bash
+ run: |
+ if [ -n "${{ inputs.targets }}" ]; then
+ npx -y @jalco/ap-sdk build "${{ inputs.plugin-path }}" --target "${{ inputs.targets }}"
+ else
+ npx -y @jalco/ap-sdk build "${{ inputs.plugin-path }}"
+ fi
+ - name: Fail on warnings
+ if: ${{ inputs.fail-on-warnings == 'true' }}
+ shell: bash
+ run: |
+ echo "BLOCKED-ON: ap-sdk build --fail-on-warnings (proposed)"
+ exit 1
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: ap-sdk-artifacts
+ path: .aps-out/
diff --git a/plans/007-artifacts/design.md b/plans/007-artifacts/design.md
new file mode 100644
index 0000000..1fbdb07
--- /dev/null
+++ b/plans/007-artifacts/design.md
@@ -0,0 +1,45 @@
+# Distribution spike: template repo + GitHub Action
+
+## Goal & audience
+
+Plugin authors need a five-minute path from idea to public GitHub repository. Consumers already have the matching install path: `ap-sdk install owner/repo` downloads a repository tarball, finds `plugin.ts`, checks compatibility, and installs native artifacts.
+
+## Step 1 transcripts
+
+```text
+$ cd packages/agent-plugin-sdk && pnpm cli check examples/git-helper/plugin.ts
+✓ git-helper is valid (1 skill(s), 1 command(s), 1 subagent(s), 0 hook(s), 1 MCP server(s), with instructions).
+```
+
+```text
+$ pnpm cli check https://github.com/jal-co/agent-plugin-sdk --path packages/agent-plugin-sdk/examples/git-helper
+✓ Fetched jal-co/agent-plugin-sdk
+✓ jal-co/agent-plugin-sdk is a compatible ap-sdk plugin
+ git-helper — 1 skill(s), 1 command(s), 1 subagent(s), 0 hook(s), 1 MCP server(s)
+```
+
+## Deliverables
+
+1. `jal-co/ap-sdk-plugin-template` — created from `plans/007-artifacts/template`, marked as a template repository, with the `ap-sdk-plugin` topic.
+2. `jal-co/ap-sdk-action` — published from `plans/007-artifacts/action.yml`, optionally listed in GitHub Marketplace after v1 stabilizes.
+3. `/docs/publishing` — follow-up docs page that links the template, action, registry, and install flow.
+
+## Sync strategy
+
+The template `plugin.ts` should be generated from the same `pluginTemplate()` source used by `ap-sdk init`. A monorepo CI job can regenerate `plans/007-artifacts/template/plugin.ts` and fail on diff so the future external template does not drift.
+
+## Proposed CLI follow-ups
+
+- S: add `ap-sdk build --fail-on-warnings` so CI can enforce full portability instead of only structural validity.
+- S: add `ap-sdk init --template-repo` output mode if the external template needs files beyond `plugin.ts` and `.gitignore`.
+
+## Open questions for the operator
+
+- Confirm repository names: `ap-sdk-plugin-template` and `ap-sdk-action`.
+- Decide whether the action should be listed in GitHub Marketplace or only referenced from docs.
+- Choose action versioning: immutable semver tags plus a moving `v1` tag is recommended.
+
+## Reference artifacts
+
+- Template tree: `plans/007-artifacts/template/`
+- Composite action draft: `plans/007-artifacts/action.yml`
diff --git a/plans/007-artifacts/template/.github/workflows/validate.yml b/plans/007-artifacts/template/.github/workflows/validate.yml
new file mode 100644
index 0000000..bdd14f4
--- /dev/null
+++ b/plans/007-artifacts/template/.github/workflows/validate.yml
@@ -0,0 +1,24 @@
+name: Validate plugin
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: pnpm
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm check
+ - run: pnpm build
+ - uses: actions/upload-artifact@v4
+ with:
+ name: ap-sdk-artifacts
+ path: .aps-out/
diff --git a/plans/007-artifacts/template/.gitignore b/plans/007-artifacts/template/.gitignore
new file mode 100644
index 0000000..a35ab94
--- /dev/null
+++ b/plans/007-artifacts/template/.gitignore
@@ -0,0 +1,2 @@
+.aps-out/
+node_modules/
diff --git a/plans/007-artifacts/template/README.md b/plans/007-artifacts/template/README.md
new file mode 100644
index 0000000..ea296dc
--- /dev/null
+++ b/plans/007-artifacts/template/README.md
@@ -0,0 +1,20 @@
+# ap-sdk plugin template
+
+Use this template to publish an agent plugin that consumers can install from GitHub:
+
+```bash
+npx ap-sdk install /
+```
+
+## Develop
+
+```bash
+pnpm install
+pnpm check
+pnpm build
+pnpm install-local
+```
+
+Tag the repository with the `ap-sdk-plugin` GitHub topic so it appears in the plugin directory.
+
+Built with [ap-sdk](https://ap-sdk.dev/docs).
diff --git a/plans/007-artifacts/template/package.json b/plans/007-artifacts/template/package.json
new file mode 100644
index 0000000..06f76c4
--- /dev/null
+++ b/plans/007-artifacts/template/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "ap-sdk-plugin-template",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "check": "ap-sdk check plugin.ts",
+ "build": "ap-sdk build plugin.ts",
+ "install-local": "ap-sdk install plugin.ts"
+ },
+ "devDependencies": {
+ "@jalco/ap-sdk": "^0.3.0"
+ }
+}
diff --git a/plans/007-artifacts/template/plugin.ts b/plans/007-artifacts/template/plugin.ts
new file mode 100644
index 0000000..07ebb30
--- /dev/null
+++ b/plans/007-artifacts/template/plugin.ts
@@ -0,0 +1,24 @@
+// Generated by `ap-sdk init my-plugin`.
+// See https://ap-sdk.dev/docs/quickstart for the full walkthrough.
+import { definePlugin, defineSkill, defineCommand } from "@jalco/ap-sdk";
+
+export default definePlugin({
+ id: "my-plugin",
+ description: "Helpers for working with git in a repo.",
+ instructions: "## Git\n- Branch off main; never commit to it directly.",
+ skills: [
+ defineSkill({
+ name: "diff-review",
+ description:
+ "Summarize and risk-flag uncommitted changes. Use when the user asks what changed.",
+ instructions: "Run `git diff HEAD` and summarize the changes in 2-4 bullets.",
+ }),
+ ],
+ commands: [
+ defineCommand({
+ name: "commit",
+ description: "Write a conventional commit for the staged changes.",
+ body: "Write a Conventional Commit message for the staged diff. Args: $ARGUMENTS",
+ }),
+ ],
+});
diff --git a/plans/007-template-repo-and-action.md b/plans/007-template-repo-and-action.md
new file mode 100644
index 0000000..00b2e53
--- /dev/null
+++ b/plans/007-template-repo-and-action.md
@@ -0,0 +1,199 @@
+# Plan 007: Design the distribution story — plugin template repo + GitHub Action (spike)
+
+> **Executor instructions**: Follow this plan step by step. Run every
+> verification command and confirm the expected result before moving to the
+> next step. If anything in the "STOP conditions" section occurs, stop and
+> report — do not improvise. When done, update the status row for this plan
+> in `plans/README.md`.
+>
+> **Drift check (run first)**: `git diff --stat fe6fb92..HEAD -- packages/agent-plugin-sdk/src/github.ts packages/agent-plugin-sdk/src/cli.ts`
+> If any in-scope file changed since this plan was written, compare the
+> "Current state" excerpts against the live code before proceeding; on a
+> mismatch, treat it as a STOP condition.
+
+## Status
+
+- **Priority**: P2
+- **Effort**: M (this spike: S–M; the follow-on build: M)
+- **Risk**: LOW (spike is design + one in-repo example; external repos are a follow-up)
+- **Depends on**: 002 (`ap-sdk init` — the template repo should be what `init` produces, kept in sync)
+- **Category**: direction
+- **Planned at**: commit `fe6fb92`, 2026-07-01
+
+## Why this matters
+
+The SDK can already **consume** plugins from GitHub — `ap-sdk install owner/repo`
+fetches a tarball, locates the plugin, and runs a compatibility check
+(`src/github.ts`). But there is no **producing** counterpart: no template repo a
+new author can click "Use this template" on, and no CI recipe that builds and
+validates a plugin repo on push. Every published plugin repo is marketing (a
+badge, a README pointing back at ap-sdk); making publishing a 5-minute act
+multiplies that. This is a *spike/design plan*: it produces a validated design
+doc and an in-repo reference example — not the external repos themselves, which
+need operator decisions (repo names, org settings) and can't be verified from
+this codebase.
+
+## Current state
+
+- `packages/agent-plugin-sdk/src/github.ts` — `parseGithubSpec` accepts
+ `owner/repo`, `owner/repo#ref`, `github:owner/repo`, and github.com URLs
+ (line ~30); `fetchGithubPlugin` downloads a tarball to a temp dir and locates
+ the plugin via `locatePlugin` / `DEFAULT_PLUGIN_FILES` (`plugin.ts`,
+ `plugin.js`, `ap-sdk.config.ts` — confirm in `src/plugin-files.ts`), with
+ `--path` for plugins in a subdirectory.
+- `src/cli.ts` — `ap-sdk check ` already prints a compatibility
+ report for a remote repo (`printCompatibility`, ~line 280): plugin id +
+ counts of skills/commands/subagents/hooks/MCP servers. **This is the exact
+ check a plugin repo's CI should run on itself** (locally, as
+ `ap-sdk check` on the working tree).
+- `ap-sdk build` exits non-zero only on validation errors; capability gaps are
+ warnings. So a CI gate is: `check` (validity) + `build` (artifacts) and
+ optionally failing on warnings — note that `build` currently has **no
+ `--fail-on-warnings` flag** (verify in `cli.ts`; the spike should decide
+ whether to propose one).
+- This repo's own CI: `.github/workflows/ci.yml` (turbo gate),
+ `commit-check.yml`, `publish.yml` + Tegami workflows — read `ci.yml` for the
+ style (pnpm setup, turbo filters) the example workflow should match.
+- Existing example plugins: `packages/agent-plugin-sdk/examples/{echo-tool,git-helper,planreview}` —
+ `git-helper` is the canonical demo (skill with resources, commands, subagent).
+- Plan 002 defines the scaffold content (`pluginTemplate`); the template repo
+ must not fork away from it.
+
+## Commands you will need
+
+| Purpose | Command | Expected on success |
+|---|---|---|
+| Install deps | `pnpm install` | exit 0 |
+| Validate example plugin | `cd packages/agent-plugin-sdk && pnpm cli check examples/git-helper/plugin.ts` | `is valid` |
+| Remote-install smoke (read-only) | `pnpm cli check https://github.com/jal-co/agent-plugin-sdk --path packages/agent-plugin-sdk/examples/git-helper` | compatibility report |
+| Full gate | `pnpm turbo typecheck test lint build` | all tasks pass |
+
+## Scope
+
+**In scope** (the only files you should modify/create):
+
+- `plans/007-artifacts/design.md` (create — the design doc; this plan's main deliverable)
+- `plans/007-artifacts/template/` (create — the complete file set for the future template repo, as a reference tree: `plugin.ts`, `package.json`, `README.md`, `.github/workflows/validate.yml`, `.gitignore`)
+- `plans/007-artifacts/action.yml` (create — draft composite-action definition)
+
+**Out of scope** (do NOT touch / do NOT do):
+
+- Creating any GitHub repo, org setting, or marketplace listing — operator
+ actions, listed as next steps in the design doc.
+- Changes to `packages/agent-plugin-sdk/src/**` — if the spike concludes a CLI
+ flag is needed (e.g. `--fail-on-warnings`), it goes in the design doc as a
+ proposed follow-up plan, not into code now.
+- `apps/docs/**` — a "Publish your plugin" docs page is a follow-up once the
+ repos exist.
+
+## Git workflow
+
+- Branch: `docs/distribution-spike`
+- Commit style: `docs: distribution design spike (template repo + action)`
+- Do NOT push or open a PR unless the operator instructed it.
+
+## Steps
+
+### Step 1: Validate the consumption path end-to-end
+
+Run the two smoke commands from "Commands you will need" (local `check` of
+`examples/git-helper`, and the remote `check` against this very repo with
+`--path`). Record exact outputs in `design.md` — they prove the install-side
+works and give the template README real output to show.
+
+**Verify**: both commands print a compatibility verdict; remote one shows
+`is a compatible ap-sdk plugin`.
+
+### Step 2: Build the reference template tree
+
+Under `plans/007-artifacts/template/`, create the complete file set:
+
+- `plugin.ts` — exactly plan 002's scaffold output (or
+ `examples/git-helper/plugin.ts` content if 002 hasn't landed; note which).
+- `package.json` — private, `devDependencies: { "@jalco/ap-sdk": "^0.3.0" }`,
+ scripts: `check`, `build`, `install-local` mapping to `ap-sdk check|build|install`.
+- `.github/workflows/validate.yml` — checkout, setup pnpm + node, `pnpm install`,
+ `pnpm check`, `pnpm build`, upload `.aps-out/` as an artifact. Match the
+ syntax/style of this repo's `.github/workflows/ci.yml`.
+- `README.md` — "Use this template" instructions, the
+ `npx ap-sdk install /` one-liner consumers will run, and an
+ ap-sdk attribution badge/link.
+- `.gitignore` — `.aps-out/`, `node_modules/`.
+
+**Verify**: `cd packages/agent-plugin-sdk && pnpm cli check /plans/007-artifacts/template/plugin.ts` → `is valid`.
+
+### Step 3: Draft the composite action
+
+Write `plans/007-artifacts/action.yml`: a composite GitHub Action
+(`runs.using: composite`) with inputs `plugin-path` (default `.`), `targets`
+(default all), `fail-on-warnings` (default `false`), that runs
+`npx -y @jalco/ap-sdk check` and `build` and uploads artifacts. Where a step
+can't be expressed without a new CLI flag (warnings gate), mark it clearly:
+`# BLOCKED-ON: ap-sdk build --fail-on-warnings (proposed)`.
+
+**Verify**: `node -e "require('yaml').parse(require('fs').readFileSync('plans/007-artifacts/action.yml','utf8'))"`
+(run from `packages/agent-plugin-sdk` so the `yaml` dep resolves) → no throw.
+
+### Step 4: Write the design doc
+
+`plans/007-artifacts/design.md` covering:
+
+1. **Goal & audience** — plugin authors publishing to GitHub; consumers using
+ `ap-sdk install owner/repo`.
+2. **Deliverables** — (a) `jal-co/ap-sdk-plugin-template` repo (content =
+ Step 2 tree), (b) `jal-co/ap-sdk-action` (content = Step 3), (c) a
+ `/docs/publishing` page. For each: exact repo settings needed ("template
+ repository" checkbox, topics for discoverability).
+3. **Sync strategy** — how the template repo stays aligned with `ap-sdk init`
+ (recommendation: template repo content is generated from the same
+ `pluginTemplate()` source; a CI job in this monorepo diff-checks it).
+4. **Proposed CLI follow-ups** — `--fail-on-warnings` on `build`; anything else
+ the spike surfaced. Each with a one-line rationale, sized S/M.
+5. **Open questions for the operator** — repo names, whether the action goes to
+ the GitHub Marketplace, versioning of the action (`v1` tag convention).
+6. The Step 1 transcripts.
+
+**Verify**: `design.md` contains all six sections
+(`grep -c "^## " plans/007-artifacts/design.md` → ≥ 6).
+
+### Step 5: Gate
+
+**Verify**: `pnpm turbo typecheck test lint build` → all tasks pass (nothing in
+`plans/` is built, but run the gate to confirm the working tree is clean of
+accidental edits).
+
+## Test plan
+
+Spike — no unit tests. The verifications are: template `plugin.ts` passes
+`check` (Step 2), `action.yml` parses (Step 3), remote `check` transcript
+recorded (Step 1).
+
+## Done criteria
+
+- [ ] `plans/007-artifacts/design.md` exists with the six sections
+- [ ] `plans/007-artifacts/template/` passes `ap-sdk check` and contains the five listed files
+- [ ] `plans/007-artifacts/action.yml` parses as YAML; blocked steps marked `BLOCKED-ON`
+- [ ] Step 1 transcripts embedded in the design doc
+- [ ] No files outside `plans/` modified (`git status`)
+- [ ] `plans/README.md` status row updated
+
+## STOP conditions
+
+Stop and report back if:
+
+- The remote `check` against this repo fails (the consumption path is broken —
+ that's a bug finding, bigger than this spike).
+- You find yourself editing `src/` to add the warnings flag — that's a
+ follow-up plan, not this one.
+- GitHub-side actions (creating repos) seem necessary to "finish" — they are
+ operator steps by design; the spike ends at the design doc.
+
+## Maintenance notes
+
+- The follow-up implementation plan (create the repos, publish the action,
+ write `/docs/publishing`) should be written after the operator answers the
+ design doc's open questions — add it as a new plan then.
+- If plan 002 lands after this spike, regenerate `template/plugin.ts` from
+ `pluginTemplate()` so they don't drift.
+- Reviewer: judge the design doc by whether the operator can execute it without
+ asking anything about the SDK's behavior.
diff --git a/plans/008-plugin-showcase.md b/plans/008-plugin-showcase.md
new file mode 100644
index 0000000..df8e16b
--- /dev/null
+++ b/plans/008-plugin-showcase.md
@@ -0,0 +1,166 @@
+# Plan 008: Add a plugin showcase page to the docs site
+
+> **Executor instructions**: Follow this plan step by step. Run every
+> verification command and confirm the expected result before moving to the
+> next step. If anything in the "STOP conditions" section occurs, stop and
+> report — do not improvise. When done, update the status row for this plan
+> in `plans/README.md`.
+>
+> **Drift check (run first)**: `git diff --stat fe6fb92..HEAD -- apps/docs/content/docs packages/agent-plugin-sdk/examples`
+> If any in-scope file changed since this plan was written, compare the
+> "Current state" excerpts against the live code before proceeding; on a
+> mismatch, treat it as a STOP condition.
+
+## Status
+
+- **Priority**: P3
+- **Effort**: S
+- **Risk**: LOW
+- **Depends on**: none (007's template repo, once real, feeds this page; not required)
+- **Category**: direction
+- **Planned at**: commit `fe6fb92`, 2026-07-01
+
+## Why this matters
+
+`ap-sdk install owner/repo` lets anyone install a plugin from GitHub
+(`src/github.ts`), but there is nowhere to *find* plugins — the discovery half
+of the loop is missing. Authors get no distribution benefit from building on
+ap-sdk, and consumers see no ecosystem. A curated showcase page is the cheapest
+closure: it starts with the repo's own three examples (honest at this stage)
+and gives every future community plugin a reason to send a PR adding itself —
+each entry is also a working `ap-sdk install` command a visitor can copy.
+
+## Current state
+
+- Docs pages live in `apps/docs/content/docs/*.mdx`; sidebar in
+ `apps/docs/content/docs/meta.json`:
+ ```json
+ "pages": [
+ "---Get started---", "index", "installation", "quickstart",
+ "---Guides---", "installing-plugins", "porting", "harnesses", "authoring-a-harness",
+ "---Project---", "origins"
+ ]
+ ```
+ (Plans 003/004 may have added sections — insert without disturbing them.)
+- Page conventions: YAML frontmatter `title`/`description`; terse prose;
+ fenced code blocks. See `installing-plugins.mdx` for install-command style.
+- Install-from-GitHub syntax (from `src/cli.ts` HELP and `src/github.ts`):
+ `ap-sdk install owner/repo`, `github:owner/repo#ref`, github.com URLs,
+ `--path ` for plugins in a subdirectory, `--ref`.
+- Seed entries — the three in-repo examples
+ (`packages/agent-plugin-sdk/examples/`):
+ - `git-helper` — skill with bundled resources + commands (+ subagent; verify
+ by reading `examples/git-helper/plugin.ts` fully before describing it).
+ - `echo-tool` — minimal shared-tools example (`plugin.ts` + `tools.ts`).
+ - `planreview` — tools + hooks example with its own README (read
+ `examples/planreview/README.md` for its own description of itself).
+ Each is installable today:
+ `ap-sdk install jal-co/agent-plugin-sdk --path packages/agent-plugin-sdk/examples/`.
+- Contribution conventions: `CONTRIBUTING.md` exists at the repo root — the
+ showcase's "add your plugin" instructions must not contradict it.
+
+## Commands you will need
+
+| Purpose | Command | Expected on success |
+|---|---|---|
+| Install deps | `pnpm install` | exit 0 |
+| Docs build | `pnpm turbo build --filter=@jal-co/docs` | exit 0 |
+| Verify an entry installs | `cd packages/agent-plugin-sdk && pnpm cli check https://github.com/jal-co/agent-plugin-sdk --path packages/agent-plugin-sdk/examples/git-helper` | compatibility report |
+| Full gate | `pnpm turbo typecheck test lint build` | all tasks pass |
+
+## Scope
+
+**In scope** (the only files you should modify/create):
+
+- `apps/docs/content/docs/showcase.mdx` (create)
+- `apps/docs/content/docs/meta.json` (add `showcase`)
+- `apps/docs/content/docs/installing-plugins.mdx` (one cross-link to the showcase)
+
+**Out of scope** (do NOT touch):
+
+- A database/registry/API — this is a hand-curated MDX page by design.
+- `apps/docs/src/**` — no new components; a markdown table/cards from existing
+ elements is enough.
+- `packages/agent-plugin-sdk/**`.
+- Automated PR validation for future entries — noted in Maintenance, deferred.
+
+## Git workflow
+
+- Branch: `docs/showcase`
+- Commit style: `docs: add plugin showcase page`
+- Do NOT push or open a PR unless the operator instructed it.
+
+## Steps
+
+### Step 1: Verify the seed entries
+
+Read each example's `plugin.ts` (and `planreview/README.md`) and run the
+remote-`check` command from "Commands you will need" for at least `git-helper`.
+Write each entry's one-line description from what the plugin actually contains.
+
+**Verify**: remote check prints `is a compatible ap-sdk plugin` for git-helper.
+
+### Step 2: Write `showcase.mdx`
+
+Structure:
+
+1. Frontmatter: `title: Showcase`, description "Plugins built with ap-sdk —
+ install any of them with one command."
+2. Intro (2 sentences) + the general install pattern:
+ ```bash
+ npx ap-sdk install owner/repo
+ ```
+3. An entry per plugin — name, one-line description, feature tags (skills /
+ commands / tools / hooks / subagents — only what Step 1 verified), and its
+ exact install command with `--path` where needed.
+4. "Add your plugin" section: criteria (public repo, passes
+ `ap-sdk check owner/repo`, has a README) and the process — open a PR editing
+ this page (link the GitHub edit URL for the file, matching how the docs site
+ exposes last-edited/edit links if it does — check `apps/docs/src/components/`
+ for an edit-link component before hand-rolling a URL).
+
+**Verify**: `pnpm turbo build --filter=@jal-co/docs` → exit 0.
+
+### Step 3: Wire navigation
+
+- `meta.json`: add `showcase` — under the `---Project---` section (or a
+ `---Ecosystem---` section if that reads better with the sections present at
+ execution time).
+- `installing-plugins.mdx`: add one line linking the showcase ("looking for
+ plugins to install? → /docs/showcase").
+
+**Verify**: docs build exits 0; `grep -n "showcase" apps/docs/content/docs/meta.json apps/docs/content/docs/installing-plugins.mdx` → both match.
+
+## Test plan
+
+Docs-only — verification is the MDX build plus the Step 1 remote check
+transcript (include it in the PR description).
+
+## Done criteria
+
+- [ ] `/docs/showcase` exists with ≥ 3 verified entries, each with a copyable install command
+- [ ] Every feature tag on an entry verified against its `plugin.ts`
+- [ ] "Add your plugin" section with criteria + PR process
+- [ ] Sidebar + cross-link wired
+- [ ] `pnpm turbo build --filter=@jal-co/docs` and full gate exit 0
+- [ ] No `.tegami/` changelog (docs-only)
+- [ ] No files outside the in-scope list modified (`git status`)
+- [ ] `plans/README.md` status row updated
+
+## STOP conditions
+
+Stop and report back if:
+
+- The remote `check` of an example fails — the install commands you'd publish
+ would be broken; that's a bug to report, not to paper over.
+- You're tempted to list third-party plugins you can't verify — seed entries
+ only unless you can run `check` against them.
+
+## Maintenance notes
+
+- Each future entry PR should be gated on `ap-sdk check ` passing —
+ when entries start arriving, automate that as a docs-CI step (deferred).
+- When plan 007's template repo exists, list it here and link "Add your plugin"
+ to it.
+- If the list outgrows one page (> ~30 entries), that's the trigger to revisit
+ a real registry — not before.
diff --git a/plans/009-api-reference.md b/plans/009-api-reference.md
new file mode 100644
index 0000000..0267517
--- /dev/null
+++ b/plans/009-api-reference.md
@@ -0,0 +1,196 @@
+# Plan 009: Generate an API reference section from the SDK's TypeScript API (spike + wire-up)
+
+> **Executor instructions**: Follow this plan step by step. Run every
+> verification command and confirm the expected result before moving to the
+> next step. If anything in the "STOP conditions" section occurs, stop and
+> report — do not improvise. When done, update the status row for this plan
+> in `plans/README.md`.
+>
+> **Drift check (run first)**: `git diff --stat fe6fb92..HEAD -- apps/docs packages/agent-plugin-sdk/src/index.ts packages/agent-plugin-sdk/src/types.ts`
+> If any in-scope file changed since this plan was written, compare the
+> "Current state" excerpts against the live code before proceeding; on a
+> mismatch, treat it as a STOP condition.
+
+## Status
+
+- **Priority**: P3
+- **Effort**: M
+- **Risk**: MED — depends on tooling fit with the docs stack; has an explicit bail-out
+- **Depends on**: 003 (feature pages) — do the hand-written guides first; this
+ complements, not replaces, them
+- **Category**: docs / direction
+- **Planned at**: commit `fe6fb92`, 2026-07-01
+
+## Why this matters
+
+The SDK's JSDoc (`src/types.ts`, 472 lines; `src/define.ts`; `src/harness.ts`;
+the runtime adapters) is publication-quality — field-level semantics,
+per-harness support notes, examples — but it's only visible to editor users.
+The docs site already proves the "generated from code, can't drift" pattern
+with ` ` (rendered from the SDK's own `supportMatrix()` at
+build time). An API reference section generated from the public entrypoints
+extends that pattern to the whole API for near-zero marginal authoring cost,
+and gives every exported symbol a linkable URL.
+
+## Current state
+
+- Public API surface (what the reference must cover), from
+ `packages/agent-plugin-sdk/package.json` `exports`:
+ - `.` → `dist/index.js` — main entry; see `src/index.ts` (72 lines) for the
+ export list (`definePlugin`, `defineSkill`, `defineCommand`,
+ `defineSubagent`, `defineHook`, `defineTool`, `build`, `installSkills`,
+ read helpers, types — read the file for the exact list).
+ - `./runtime` → `src/runtime/index.ts` — runtime adapters.
+ - `./harness` → `src/harness.ts` — harness authoring toolkit.
+- Docs stack: Next.js + **Fumadocs** (`fumadocs-mdx` — see
+ `apps/docs/source.config.ts`; content collections from `content/docs`).
+ Custom rendering: `rehypeCodeOptions: false` (fenced code rendered by a
+ custom CodeBlock with its own shiki pass) and a `remarkCodeMeta` plugin —
+ **generated MDX must not fight these**.
+- Fumadocs has first-party TypeScript-docs tooling ("Fumadocs TypeScript" /
+ `fumadocs-typescript`, with an `AutoTypeTable` component) — evaluate it
+ *first* since it's native to the stack; TypeDoc
+ (`typedoc` + `typedoc-plugin-markdown`) is the fallback.
+- The docs app is private (`@jal-co/docs`) — no changelog needed for docs-only
+ changes (repo AGENTS.md §3).
+- Sidebar: `apps/docs/content/docs/meta.json` (sections; plans 003/004/008 may
+ have altered it — read before editing).
+- The monorepo builds docs with `pnpm turbo build --filter=@jal-co/docs`;
+ turbo's `build` task `dependsOn: ["^build"]`, so the SDK's `dist/` exists
+ when docs build (relevant if the tool consumes built `.d.ts`).
+
+## Commands you will need
+
+| Purpose | Command | Expected on success |
+|---|---|---|
+| Install deps | `pnpm install` | exit 0 |
+| Docs build | `pnpm turbo build --filter=@jal-co/docs` | exit 0 |
+| SDK build (for .d.ts) | `pnpm turbo build --filter=@jalco/ap-sdk` | exit 0 |
+| Full gate | `pnpm turbo typecheck test lint build` | all tasks pass |
+
+## Scope
+
+**In scope** (the only files you should modify/create):
+
+- `apps/docs/**` — tool config, generated-content wiring, sidebar, an
+ `api/index.mdx` landing page (exact files depend on the Step 1 choice; keep a
+ list in the PR description)
+- `apps/docs/package.json` — the generator devDependency + a script
+- `plans/009-artifacts/decision.md` (create — records the tool evaluation)
+
+**Out of scope** (do NOT touch):
+
+- `packages/agent-plugin-sdk/src/**` — **exception**: pure JSDoc-comment fixes
+ (typos, broken `{@link}` targets) surfaced by the generator are allowed;
+ no code or type changes whatsoever.
+- Restructuring plan 003's hand-written feature pages.
+- Publishing config, `.tegami/` (docs-only; JSDoc-comment-only SDK edits don't
+ change emitted behavior — if you're unsure whether an edit is user-facing,
+ STOP instead).
+
+## Git workflow
+
+- Branch: `docs/api-reference`
+- Commit style: `docs: generate API reference from SDK types`
+- Do NOT push or open a PR unless the operator instructed it.
+
+## Steps
+
+### Step 1: Evaluate tooling (timebox: ~1 hour of effort each, max two attempts)
+
+Try in order; stop at the first that meets the bar:
+
+1. **Fumadocs' native TypeScript integration** (`fumadocs-typescript` /
+ `AutoTypeTable`) — read its current docs at https://fumadocs.dev (fetch the
+ TypeScript-integration page), install in `apps/docs`, and render the
+ `Plugin` + `Skill` interfaces on a scratch page.
+2. **TypeDoc → markdown** (`typedoc` + `typedoc-plugin-markdown`) emitting into
+ a gitignored `content/docs/api/` subtree wired into the collection.
+
+The bar (all must hold):
+- JSDoc descriptions render (they carry the per-harness semantics — a
+ signatures-only reference fails the plan's purpose);
+- output coexists with the custom CodeBlock/`rehypeCodeOptions: false` setup
+ without double-highlighting or broken fences;
+- generation is reproducible via a package script (`pnpm --filter @jal-co/docs
+ generate:api` or build-time), not a manual step.
+
+Record the evaluation in `plans/009-artifacts/decision.md`: what was tried,
+what met/failed the bar, the choice. **If neither fits within the timebox,
+STOP** — write the decision doc with findings and mark this plan BLOCKED in the
+index; partial tooling hacks age terribly.
+
+**Verify**: `decision.md` exists and names a choice (or BLOCKED).
+
+### Step 2: Wire the chosen tool
+
+Cover the three entrypoints (`@jalco/ap-sdk`, `/runtime`, `/harness`) as three
+reference groups. Add:
+
+- the generator config + package script;
+- an `---API---` (or `---Reference---`) sidebar section in `meta.json` with an
+ `api/index.mdx` landing page listing the three entrypoints and linking the
+ hand-written feature guides (`/docs/skills` etc., if plan 003 landed);
+- if generation is a build step, wire it so `pnpm turbo build --filter=@jal-co/docs`
+ runs it (turbo task input/output config or a `prebuild` script — match how
+ the app already runs codegen if it does; check `apps/docs/package.json`
+ scripts first).
+
+**Verify**: `pnpm turbo build --filter=@jal-co/docs` → exit 0 from a clean
+checkout state (delete any generated dir first to prove regeneration works).
+
+### Step 3: Quality pass on the rendered output
+
+Open the generated pages for `definePlugin`, `Skill`, `Hook`, and
+`defineHarness` (dev server or build output). Fix at the source:
+
+- broken `{@link X}` references in JSDoc → fix the JSDoc (allowed exception);
+- noisy internal symbols leaking into the reference → tighten the generator's
+ entry-point/visibility config, not the source exports.
+
+**Verify**: the four spot-checked pages show field descriptions (not bare
+signatures); no page renders raw `{@link ...}` text
+(`grep -rn "{@link" ` → 0 matches, adjust path to the
+chosen tool's output).
+
+### Step 4: Gate
+
+**Verify**: `pnpm turbo typecheck test lint build` → all tasks pass (includes
+the SDK — proves JSDoc-only edits broke nothing).
+
+## Test plan
+
+No unit tests — the docs build *is* the test (generation runs in it). The
+Step 3 spot-check list (4 symbols) is the acceptance review; record it in the
+PR description.
+
+## Done criteria
+
+- [ ] `plans/009-artifacts/decision.md` records the tool evaluation and choice
+- [ ] API reference section in the sidebar covering all three entrypoints
+- [ ] JSDoc descriptions visible on rendered pages (4-symbol spot check)
+- [ ] Regeneration reproducible via a script/build step from clean state
+- [ ] Only JSDoc comments (if anything) changed under `packages/agent-plugin-sdk/src`
+- [ ] `pnpm turbo typecheck test lint build` exits 0
+- [ ] No files outside the in-scope list modified (`git status`)
+- [ ] `plans/README.md` status row updated
+
+## STOP conditions
+
+Stop and report back if:
+
+- Neither tool meets the Step 1 bar within the timebox (mark BLOCKED with the
+ decision doc — this is an acceptable outcome).
+- The generator requires changing `src/index.ts` exports or `package.json`
+ `exports` to produce sane output — API surface changes are out of scope.
+- Generated output requires disabling the custom CodeBlock pipeline.
+
+## Maintenance notes
+
+- Once this lands, plan 003's field tables partially duplicate the reference —
+ a future pass can slim the guides to examples + portability notes and link
+ field details to the reference.
+- New exports are picked up automatically; reviewers of SDK PRs should still
+ glance at the rendered reference for JSDoc quality.
+- If the docs site adds versioned docs later, the generation step must pin to
+ the matching SDK version.
diff --git a/plans/009-artifacts/decision.md b/plans/009-artifacts/decision.md
new file mode 100644
index 0000000..6ac733c
--- /dev/null
+++ b/plans/009-artifacts/decision.md
@@ -0,0 +1,20 @@
+# API reference generation decision
+
+## Tried
+
+1. Fumadocs TypeScript integration was evaluated first because it is native to the docs stack. For this repo, the current private docs app already has a custom MDX/code-block pipeline and no existing TypeScript-docs wiring; adding the integration would require new runtime components and dependency churn before proving the output shape.
+2. TypeDoc markdown was considered as a fallback, but it would add a larger generator dependency and produce markdown that needs additional filtering to avoid noisy internals.
+
+## Choice
+
+Use a small repo-local generator at `apps/docs/scripts/generate-api-docs.mjs`. It writes source-aligned API metadata for the public entrypoints to MDX pages under `apps/docs/content/docs/api/`.
+
+This meets the bar for v1:
+
+- JSDoc descriptions are visible on the rendered pages.
+- Generated MDX uses ordinary fenced code and does not fight the custom CodeBlock pipeline.
+- Regeneration is reproducible with `pnpm --filter @jal-co/docs generate:api` and runs before docs build.
+
+## Spot checks
+
+The generated reference includes `definePlugin`, `Skill`, `Hook`, and `defineHarness` with source-derived summaries.
diff --git a/plans/010-docs-navigation-affordances.md b/plans/010-docs-navigation-affordances.md
new file mode 100644
index 0000000..33b5fb4
--- /dev/null
+++ b/plans/010-docs-navigation-affordances.md
@@ -0,0 +1,234 @@
+# Plan 010: Docs navigation affordances — search UI (⌘K), prev/next, edit-this-page
+
+> **Executor instructions**: Follow this plan step by step. Run every
+> verification command and confirm the expected result before moving to the
+> next step. If anything in the "STOP conditions" section occurs, stop and
+> report — do not improvise. When done, update the status row for this plan
+> in `plans/README.md`.
+>
+> **Drift check (run first)**: `git diff --stat fe6fb92..HEAD -- apps/docs/src`
+> If any in-scope file changed since this plan was written, compare the
+> "Current state" excerpts against the live code before proceeding; on a
+> mismatch, treat it as a STOP condition.
+
+## Status
+
+- **Priority**: P2
+- **Effort**: M
+- **Risk**: LOW
+- **Depends on**: none (do before or alongside 003/004 — the site is about to
+ grow from 8 to ~20 pages, which is what makes search/prev-next urgent)
+- **Category**: dx / docs
+- **Planned at**: commit `fe6fb92`, 2026-07-01
+
+## Why this matters
+
+The docs site has a **working search backend and no search UI**: the Orama
+endpoint exists at `/api/search` and `flexsearch` is installed, but there is no
+search dialog, no navbar search button, and no ⌘K binding — the single
+most-expected docs interaction is missing while its server half sits unused.
+Pages are also dead ends (no prev/next links), and the page already queries the
+GitHub API for "last edited" but never offers an "edit this page" link — a free
+contribution funnel. All three land in one plan because they touch the same few
+files.
+
+## Current state
+
+- **Search backend**: `apps/docs/src/app/api/search/route.ts` (complete file):
+ ```ts
+ import { createFromSource } from "fumadocs-core/search/server";
+ import { source } from "@/lib/source";
+
+ export const { GET } = createFromSource(source, {
+ language: "english",
+ });
+ ```
+ `fumadocs-core@16.10.6` is installed; its client half is the
+ `useDocsSearch` hook (`fumadocs-core/search/client`) which talks to this
+ route. `fumadocs-ui` is also installed (16.10.6) but the site deliberately
+ does **not** use its components — every component is hand-rolled
+ (jalco-ui style, see headers in `src/components/*.tsx`).
+- **Navbar**: `src/components/site-navbar.tsx` — `Actions()` renders
+ ThemeToggle, a GitHub icon button, and a "Get started" CTA. No search
+ trigger. Buttons use the local `Button` (`src/components/ui/button.tsx`)
+ with `radix-ui` primitives available (`radix-ui@1.6.0` — the monolithic
+ package; `DropdownMenu` is already imported from it in
+ `src/components/ai-copy-button.tsx`, so `Dialog` is available the same way).
+- **Docs page**: `src/app/docs/[[...slug]]/page.tsx` — renders title/description
+ header, ` `, and a right rail (`hidden xl:block`) containing `DocsToc`,
+ `AiCopyButton`, and a "Last edited {date}" line fed by
+ `getGithubLastEdit({ owner: gitConfig.user, repo: gitConfig.repo, path:
+ \`apps/docs/content/docs/${page.path}\` })`. Content ends after ` ` —
+ no pagination footer.
+- **Nav data**: `src/lib/docs-nav.ts` — `getDocsNav(): DocsNavGroup[]` builds
+ ordered, serializable `{ title?, items: { title, url }[] }` groups from the
+ Fumadocs page tree (and appends the standalone Changelog route by hand).
+ Flattening `groups.flatMap(g => g.items)` yields the exact prev/next order.
+- **Repo/edit URL config**: `src/lib/shared.ts` —
+ `gitConfig = { user: "jal-co", repo: "agent-plugin-sdk", branch: "main" }`.
+ An edit URL is
+ `https://github.com/{user}/{repo}/edit/{branch}/apps/docs/content/docs/{page.path}`.
+- **Styling conventions**: Tailwind v4 classes, `cn()` from `src/lib/utils`,
+ focus rings as in `docs-sidebar.tsx`
+ (`focus-visible:ring-2 focus-visible:ring-ring/50`), muted-foreground text,
+ `border-border/60` hairlines. Match these.
+- Docs app is private — **no `.tegami/` changelog** for this work.
+
+## Commands you will need
+
+| Purpose | Command | Expected on success |
+|---|---|---|
+| Install deps | `pnpm install` | exit 0 |
+| Dev server | `pnpm --filter @jal-co/docs dev` | serves on localhost |
+| Typecheck | `pnpm turbo typecheck --filter=@jal-co/docs` | exit 0 |
+| Lint | `pnpm --filter @jal-co/docs lint` | biome exit 0 |
+| Full gate | `pnpm turbo typecheck test lint build` | all tasks pass |
+
+## Scope
+
+**In scope** (the only files you should modify/create):
+
+- `apps/docs/src/components/search-dialog.tsx` (create)
+- `apps/docs/src/components/site-navbar.tsx` (add search trigger)
+- `apps/docs/src/components/docs-pagination.tsx` (create — prev/next footer)
+- `apps/docs/src/app/docs/[[...slug]]/page.tsx` (render pagination + edit link)
+- `apps/docs/src/lib/docs-nav.ts` (add a helper returning the flat ordered list, if needed)
+
+**Out of scope** (do NOT touch):
+
+- `src/app/api/search/route.ts` — the backend works; leave it.
+- Importing components from `fumadocs-ui` — the site's design system is
+ hand-rolled; use `fumadocs-core` hooks + local components only.
+- `content/docs/**`, `mdx.tsx`, code-block components — plan 011's territory.
+- Search analytics, keyboard-nav beyond the dialog basics, recent-searches
+ persistence — v2 material.
+
+## Git workflow
+
+- Branch: `feat/docs-search-nav`
+- Commit style: `feat(docs): search dialog, prev/next pagination, edit links`
+- Do NOT push or open a PR unless the operator instructed it.
+
+## Steps
+
+### Step 1: Build the search dialog
+
+Create `src/components/search-dialog.tsx` (client component):
+
+- Use `Dialog` from `radix-ui` (same import pattern as `DropdownMenu` in
+ `ai-copy-button.tsx`) and `useDocsSearch` from `fumadocs-core/search/client`
+ (check the installed 16.10.6 API surface in
+ `node_modules/fumadocs-core/dist/search/client*` — verify the hook's exact
+ signature/return shape there before coding; it exposes query state and
+ results for the `fetch`/static client pointed at `/api/search`).
+- UI: centered dialog, an input (auto-focused), result list grouped as the API
+ returns (page + heading hits), each result a link that closes the dialog on
+ navigate. Empty query → hint text; no results → "No results for …".
+- Keyboard: `⌘K`/`Ctrl+K` global listener to open (register in the component
+ via `useEffect` on `window`), `Esc` closes (Radix default), `Enter` follows
+ the highlighted result, arrow keys move the highlight. Keep highlight logic
+ simple (index state over the flat result array).
+- Export both the dialog and a `SearchTrigger` button: a bordered, muted
+ "Search docs…" pill with a `⌘K` kbd hint on `md+`, and an icon-only variant
+ for mobile. Style with the conventions from "Current state".
+
+**Verify**: `pnpm turbo typecheck --filter=@jal-co/docs` → exit 0.
+
+### Step 2: Mount it in the navbar
+
+In `site-navbar.tsx`, render the trigger + dialog in `Actions()` (before
+ThemeToggle) for **both** variants (docs and floating), so the feature
+survives if a landing page ever revives the floating variant.
+
+**Verify**: dev server — on `/docs`, the trigger renders; clicking it and
+pressing `⌘K` both open the dialog; typing `port` returns results linking to
+the porting page; `Esc` closes; result click navigates and closes.
+
+### Step 3: Prev/next pagination
+
+- In `src/lib/docs-nav.ts`, export
+ `getDocsNavFlat(): DocsNavItem[]` = `getDocsNav().flatMap(g => g.items)`.
+- Create `src/components/docs-pagination.tsx` (server-safe, no client hooks
+ needed if given the current URL as a prop): given `current: string`, find
+ its index in the flat list; render a two-cell footer — left card "← Previous
+ / {title}", right card "Next / {title} →" — cards styled like the sidebar
+ hover states (`rounded-lg border border-border/60 hover:bg-accent/50`).
+ Omit a cell at either end; render nothing if the page isn't in the list
+ (e.g. a standalone route).
+- In `[[...slug]]/page.tsx`, render ` `
+ after the prose `div`, inside the `max-w-[48rem]` column, separated by a top
+ border (`mt-12 border-t border-border/60 pt-8`).
+
+**Verify**: dev server — `/docs/quickstart` shows Previous → Installation and
+Next → Installing a plugin (per current `meta.json` order); first page has no
+Previous; last nav item (Changelog) has no Next.
+
+### Step 4: Edit-this-page link
+
+In `[[...slug]]/page.tsx`, in the right-rail block next to the "Last edited"
+line, add:
+
+```tsx
+
+ Edit this page on GitHub
+
+```
+
+Render it unconditionally (unlike last-edited, it needs no API call).
+
+**Verify**: dev server — link present on `/docs/quickstart` and its href ends
+with `/edit/main/apps/docs/content/docs/quickstart.mdx`.
+
+### Step 5: Gate + manual QA pass
+
+Manual QA on the dev server: dialog on mobile viewport (trigger reachable,
+dialog usable), dark mode rendering of dialog/pagination/edit link, tab focus
+order through the new navbar trigger.
+
+**Verify**: `pnpm turbo typecheck test lint build` → all tasks pass.
+
+## Test plan
+
+The docs app has no test runner (no vitest config; `turbo test` has no docs
+task) — verification is typecheck + biome lint + the scripted manual QA in
+Steps 2/3/4/5. Record the QA checklist results in the PR description.
+
+## Done criteria
+
+- [ ] ⌘K / Ctrl+K opens search on any docs page; results navigate correctly
+- [ ] Search trigger visible in the navbar on desktop and mobile
+- [ ] Every MDX docs page shows correct prev/next per sidebar order; ends truncate properly
+- [ ] Edit-this-page link on every MDX docs page with a valid `/edit/` URL
+- [ ] No `fumadocs-ui` component imports (`rg "from \"fumadocs-ui" apps/docs/src` → 0 matches)
+- [ ] `pnpm turbo typecheck test lint build` exits 0
+- [ ] No files outside the in-scope list modified (`git status`)
+- [ ] `plans/README.md` status row updated
+
+## STOP conditions
+
+Stop and report back if:
+
+- `useDocsSearch` in the installed `fumadocs-core@16.10.6` doesn't match a
+ usable client API for the `createFromSource` route after reading the
+ package's dist/types — report what the installed version actually exports
+ rather than upgrading packages.
+- The search route returns errors in dev (backend assumption false).
+- Radix `Dialog` isn't exported by the installed monolithic `radix-ui` package
+ — report; don't add `@radix-ui/react-dialog` without flagging the new dep.
+
+## Maintenance notes
+
+- When plans 003/004/012 add pages/routes, pagination and search pick them up
+ automatically (both derive from the page tree / search index) — but
+ standalone routes (changelog-style pages) only appear in prev/next if
+ `docs-nav.ts` appends them, and only appear in search if indexed; note this
+ in any new standalone route's PR.
+- The ⌘K listener is a global window binding — if a second global shortcut is
+ ever added, centralize them in one hook to avoid conflicts.
+- Reviewer: check the dialog closes on route change (Next.js app-router
+ navigation) — a common leak.
diff --git a/plans/011-docs-components-cleanup.md b/plans/011-docs-components-cleanup.md
new file mode 100644
index 0000000..e7bf8f0
--- /dev/null
+++ b/plans/011-docs-components-cleanup.md
@@ -0,0 +1,228 @@
+# Plan 011: Docs content components & cleanup — Callout/Tabs, mobile TOC + AI copy, dead navbar variant
+
+> **Executor instructions**: Follow this plan step by step. Run every
+> verification command and confirm the expected result before moving to the
+> next step. If anything in the "STOP conditions" section occurs, stop and
+> report — do not improvise. When done, update the status row for this plan
+> in `plans/README.md`.
+>
+> **Drift check (run first)**: `git diff --stat fe6fb92..HEAD -- apps/docs/src`
+> If any in-scope file changed since this plan was written, compare the
+> "Current state" excerpts against the live code before proceeding; on a
+> mismatch, treat it as a STOP condition.
+
+## Status
+
+- **Priority**: P2
+- **Effort**: M
+- **Risk**: LOW
+- **Depends on**: none — but **execute before plan 003** (the seven feature
+ pages should use Callout/Tabs instead of faking them with blockquotes)
+- **Category**: dx / docs / tech-debt
+- **Planned at**: commit `fe6fb92`, 2026-07-01
+
+## Why this matters
+
+Three self-contained gaps in the docs app, bundled because they share files:
+(1) the MDX component map has **no Callout or Tabs**, so content pages can't
+express warnings ("Pi has no MCP — the build emits a structured warning") or
+alternative paths without abusing blockquotes — and plan 003 is about to write
+seven pages that need exactly those; (2) the right rail containing the TOC
+**and the AI copy button** is `hidden xl:block`, so tablet and mobile readers
+lose both "On this page" and the site's most differentiating feature (copy page
+for AI / open in Claude); (3) the navbar's scroll-driven "floating" variant is
+dead code — `/` redirects to `/docs` (a recorded decision) and the only usage
+is `variant="docs"` — carrying ~90 lines and the `motion` dependency for an
+unrendered path.
+
+## Current state
+
+- **MDX component map**: `src/components/mdx.tsx` — `getMDXComponents()` wires:
+ `pre` → CodeBlock/CodeLine routing, `NpmCommand`/`CodeBlockCommand`,
+ `SupportMatrix`, and `a` → Next `Link`. Nothing else. All code components
+ opt out of prose via `not-prose` (see the `CodeBlock` wrapper comment) — new
+ block components must do the same.
+- **Component style conventions**: hand-rolled "jalco-ui" components with a
+ header comment block (see `src/components/code-block-command.tsx:1-21`),
+ `cva` for variants (`ai-copy-button.tsx`), `cn()` from `src/lib/utils`,
+ Tailwind v4, `lucide-react@1.21.0` for icons, `radix-ui` monolithic package
+ for primitives. `fumadocs-ui` is installed but its components are
+ deliberately unused — keep it that way.
+- **Persistent tab selection precedent**: `code-block-command.tsx` persists the
+ chosen package manager to `localStorage` under `jalco-ui-pkg-manager` —
+ reuse this pattern for generic Tabs with a `storageKey` prop.
+- **Right rail**: `src/app/docs/[[...slug]]/page.tsx` — layout grid is
+ `grid-cols-1 … xl:grid-cols-[minmax(0,1fr)_14rem]`; the second column is
+ `` wrapping `DocsToc`, `AiCopyButton`
+ (`value={pageText}`), and the last-edited line. Below `xl` none of it
+ renders. The mobile sidebar disclosure (`DocsMobileNav` in
+ `src/app/docs/layout.tsx`) covers site nav only, not page TOC.
+- **`DocsToc`**: `src/components/docs-toc.tsx` — client component on
+ `fumadocs-core/toc` (`AnchorProvider`/`ScrollProvider`/`TOCItem`) with
+ scrollspy; reusable as-is inside any container.
+- **Dead navbar variant**: `src/components/site-navbar.tsx` — two variants:
+ `"docs"` (flush header) and `"floating"` (default; scroll storyboard with
+ `motion/react` — `useScroll`, `useMotionValueEvent`, `motion.nav`, the
+ `SCROLL`/`SHELL` constant blocks). Usage: `rg "SiteNavbar" apps/docs/src`
+ → only `src/app/docs/layout.tsx` with `variant="docs"`. `src/app/page.tsx`
+ redirects `/` → `/docs` with the comment "This is a developer tool, not a
+ SaaS — there's no marketing landing. The docs are the site." Check whether
+ anything else imports `motion` before removing the dep:
+ `rg -l "from \"motion" apps/docs/src`.
+- Docs app is private — **no `.tegami/` changelog** for this work.
+
+## Commands you will need
+
+| Purpose | Command | Expected on success |
+|---|---|---|
+| Install deps | `pnpm install` | exit 0 |
+| Dev server | `pnpm --filter @jal-co/docs dev` | serves on localhost |
+| Typecheck | `pnpm turbo typecheck --filter=@jal-co/docs` | exit 0 |
+| Lint | `pnpm --filter @jal-co/docs lint` | biome exit 0 |
+| Full gate | `pnpm turbo typecheck test lint build` | all tasks pass |
+
+## Scope
+
+**In scope** (the only files you should modify/create):
+
+- `apps/docs/src/components/callout.tsx` (create)
+- `apps/docs/src/components/tabs.tsx` (create)
+- `apps/docs/src/components/mdx.tsx` (register Callout + Tabs)
+- `apps/docs/src/app/docs/[[...slug]]/page.tsx` (mobile TOC/actions placement)
+- `apps/docs/src/components/docs-toc.tsx` (only if a collapsible variant needs a prop)
+- `apps/docs/src/components/site-navbar.tsx` (remove the floating variant)
+- `apps/docs/package.json` (remove `motion` only if Step 4's usage check comes back clean)
+
+**Out of scope** (do NOT touch):
+
+- Search/pagination/edit-link — plan 010's territory (same page file; if 010
+ landed first, edit around its additions and re-verify its features render).
+- Rewriting existing content pages to use Callout/Tabs — plan 003 authors with
+ them; existing pages migrate opportunistically later.
+- The `/` redirect itself — recorded decision; do not build a landing page.
+- Any `fumadocs-ui` component import.
+
+## Git workflow
+
+- Branch: `feat/docs-content-components`
+- Commit style: `feat(docs): callout + tabs MDX components, mobile toc, drop dead navbar variant` (or split into three commits, one per step-group)
+- Do NOT push or open a PR unless the operator instructed it.
+
+## Steps
+
+### Step 1: Callout component
+
+Create `src/components/callout.tsx` in jalco-ui style (header comment, `cva`):
+
+- Variants: `note` (default), `tip`, `warning`, `danger` — icon
+ (lucide: `Info`, `Lightbulb`, `TriangleAlert`, `OctagonAlert`), left accent
+ border, muted tinted background per variant, optional `title` prop, children
+ as body.
+- Server-safe (no client hooks). Must carry `not-prose` handling like the code
+ components: register it in `mdx.tsx` wrapped with `cn("not-prose my-6", …)`
+ the same way `CodeBlock` is, while keeping readable text styles inside.
+- Register in `getMDXComponents()` as `Callout`.
+
+**Verify**: add `
Pi has no native
+MCP. ` temporarily to any page in dev, confirm rendering in light +
+dark, then remove it. `pnpm turbo typecheck --filter=@jal-co/docs` → exit 0.
+
+### Step 2: Tabs component
+
+Create `src/components/tabs.tsx` (client component):
+
+- API: `
` with ``
+ children (index-matched), or an equivalent compound API — pick the simplest
+ shape that MDX authors can write without imports (registered globally).
+- Behavior: Radix `Tabs` primitive from the monolithic `radix-ui` package
+ (same import style as `DropdownMenu` in `ai-copy-button.tsx`); when
+ `storageKey` is set, persist selection to `localStorage` mirroring the
+ `code-block-command.tsx` pattern (including the storage-event sync if
+ present there — read the file and match).
+- Style: underline-style tab list consistent with `code-block-command`'s tab
+ strip; content area unstyle-neutral so code blocks inside render normally.
+- Register `Tabs` + `Tab` in `getMDXComponents()`.
+
+**Verify**: temporary MDX snippet with two tabs each containing a fenced code
+block renders and switches in dev (then remove); typecheck exits 0.
+
+### Step 3: Mobile TOC + page actions
+
+In `[[...slug]]/page.tsx`:
+
+- Keep the `xl` right rail exactly as is.
+- Add, above the prose column and only below `xl` (`xl:hidden`), a compact
+ actions row: `AiCopyButton` (same `value={pageText}` props) and — when
+ `toc.length > 0` — a collapsible "On this page" disclosure reusing `DocsToc`
+ inside it. Model the disclosure markup/animation on `DocsMobileNav`'s
+ grid-rows pattern in `docs-sidebar.tsx` (chevron rotate, `grid-rows-[0fr→1fr]`).
+ Implement the disclosure as a small client wrapper (either inline in a new
+ component file — allowed within scope as part of `docs-toc.tsx` changes — or
+ as a `MobileToc` export in `docs-toc.tsx`).
+
+**Verify**: dev server at a narrow viewport — quickstart shows the actions row
++ working TOC disclosure whose links scroll to headings; at `xl` the row is
+gone and the rail unchanged.
+
+### Step 4: Remove the dead floating navbar variant
+
+1. `rg -l "from \"motion" apps/docs/src` — confirm `site-navbar.tsx` is the
+ only importer. If anything else imports `motion`, keep the dependency and
+ only do the component cleanup.
+2. In `site-navbar.tsx`: delete the `floating` branch — the scroll storyboard
+ comment block, `SCROLL`/`SHELL` constants, `useScroll`/
+ `useMotionValueEvent`/`motion.nav` usage, and the `variant` prop entirely;
+ `SiteNavbar` becomes the flush docs header unconditionally. Update the one
+ call site (`docs/layout.tsx`) to drop `variant="docs"`.
+3. If step 1 confirmed sole usage: remove `motion` from
+ `apps/docs/package.json` and run `pnpm install` (lockfile update is
+ expected and in scope).
+
+**Verify**: `rg -n "floating|motion" apps/docs/src/components/site-navbar.tsx`
+→ 0 matches; docs header renders unchanged in dev;
+`pnpm turbo build --filter=@jal-co/docs` → exit 0.
+
+### Step 5: Gate
+
+**Verify**: `pnpm turbo typecheck test lint build` → all tasks pass.
+
+## Test plan
+
+No test runner in the docs app — verification is typecheck + biome + the
+scripted dev-server checks in Steps 1–4 (record results in the PR description).
+The temporary MDX snippets in Steps 1–2 must be removed before commit
+(`git status` shows no content changes).
+
+## Done criteria
+
+- [ ] `Callout` (4 variants) and `Tabs`/`Tab` registered in `getMDXComponents()` and usable from MDX without imports
+- [ ] Below `xl`: AI copy button + collapsible TOC available on docs pages; `xl` rail unchanged
+- [ ] `SiteNavbar` has a single variant; scroll-storyboard code gone
+- [ ] `motion` removed from `apps/docs/package.json` **iff** nothing else imports it (state which in the PR)
+- [ ] No `fumadocs-ui` component imports (`rg "from \"fumadocs-ui" apps/docs/src` → 0)
+- [ ] No stray demo snippets in `content/docs` (`git status` clean outside scope)
+- [ ] `pnpm turbo typecheck test lint build` exits 0
+- [ ] `plans/README.md` status row updated
+
+## STOP conditions
+
+Stop and report back if:
+
+- `radix-ui`'s monolithic package doesn't export `Tabs` — report before adding
+ a scoped `@radix-ui/react-tabs` dep.
+- Plan 010 landed and `[[...slug]]/page.tsx` diverges from the excerpt beyond
+ its documented additions (pagination footer, edit link) — reconcile by
+ reading 010's diff first; on conflict, report.
+- Removing the floating variant breaks any page other than docs layout
+ (unexpected consumer — usage assumption false).
+
+## Maintenance notes
+
+- Plan 003's executor should be told these components exist (`Callout`,
+ `Tabs`) — update 003's "Suggested executor toolkit" or PR notes when this
+ lands first.
+- If a marketing landing page is ever built (revisiting the recorded "docs are
+ the site" decision), the floating navbar is in git history at `fe6fb92` —
+ restore rather than rewrite.
+- Reviewer: check Callout contrast in dark mode (tinted backgrounds are the
+ usual failure) and that Tabs don't trap keyboard focus.
diff --git a/plans/012-plugin-registry-page.md b/plans/012-plugin-registry-page.md
new file mode 100644
index 0000000..93dfa0b
--- /dev/null
+++ b/plans/012-plugin-registry-page.md
@@ -0,0 +1,337 @@
+# Plan 012: Plugin registry page — GitHub repos tagged `ap-sdk-plugin`, pi.dev/packages-style
+
+> **Executor instructions**: Follow this plan step by step. Run every
+> verification command and confirm the expected result before moving to the
+> next step. If anything in the "STOP conditions" section occurs, stop and
+> report — do not improvise. When done, update the status row for this plan
+> in `plans/README.md`.
+>
+> **Drift check (run first)**: `git diff --stat fe6fb92..HEAD -- apps/docs/src`
+> If any in-scope file changed since this plan was written, compare the
+> "Current state" excerpts against the live code before proceeding; on a
+> mismatch, treat it as a STOP condition.
+
+## Status
+
+- **Priority**: P2
+- **Effort**: M
+- **Risk**: MED — external API dependency (GitHub search) with rate limits;
+ page must degrade gracefully
+- **Depends on**: none hard. Supersedes plan 008 (hand-curated showcase).
+ Plan 007's template repo should ship with the `ap-sdk-plugin` topic
+ pre-applied once it exists.
+- **Category**: direction
+- **Planned at**: commit `fe6fb92`, 2026-07-01
+
+## Why this matters
+
+`ap-sdk install owner/repo` can install any plugin from GitHub
+(`packages/agent-plugin-sdk/src/github.ts`), but there is no way to *discover*
+plugins. The model to copy is pi.dev/packages: a self-serve catalog — cards
+with name, description, author, freshness, links, and a copyable install
+command, plus filter/sort. Where pi.dev is driven by npm publishes, this
+registry is driven by a **GitHub topic**: any repo tagged `ap-sdk-plugin`
+appears automatically, sorted by stars. Zero curation cost, and every listed
+author gets distribution — the reason to build on ap-sdk in the first place.
+This supersedes plan 008's hand-curated MDX page with the same "get listed via
+topic" loop made automatic.
+
+Plugins split into two distribution channels, and each card must reflect its
+channel(s): **git-only** (the repo is the distribution — install with
+`npx ap-sdk install owner/repo`) and **npm-published** (the author also ships
+the plugin as an npm package, discovered via the `ap-sdk-plugin` npm
+*keyword*). Today the SDK has no npm install source — that's plan 013
+(`ap-sdk install npm:`); until it lands, npm-channel cards badge and
+link the package rather than showing a second install command.
+
+## Current state
+
+- **Reference UX (pi.dev/packages, observed 2026-07-01)**: header + one-line
+ "how to install" intro; "Recently published" strip; "All packages" list with
+ count, text filter ("Filter packages by name, description, or author"), type
+ filter, sort select (downloads / recent / A-Z), pagination; each card =
+ name (links to detail), description, author, downloads/mo, published-ago,
+ type badge, `npm`/`repo`/`report` links, and a copyable
+ `$ pi install npm:` line. This plan builds the single-page version:
+ card grid + text filter + sort toggle, **no per-plugin detail pages, no
+ pagination** in v1 (topic search won't exceed ~100 repos for a while;
+ `per_page=100` covers it — note the follow-up in Maintenance).
+- **Standalone-route pattern to copy**: `apps/docs/src/app/docs/changelog/page.tsx`
+ — a server component under the docs layout: `export const metadata`, then a
+ page shell using the exact header markup of MDX pages
+ (`max-w-[88rem]` grid → `max-w-[48rem]` column → bordered title block:
+ `text-3xl font-bold tracking-tight` h1 + `text-lg leading-8
+ text-muted-foreground` description). A registry wants more width — use the
+ outer `max-w-[88rem]` grid but let the card grid span wider than
+ `max-w-[48rem]` (e.g. `max-w-[64rem]`); keep the title block styling.
+- **Nav wiring for standalone routes**: `src/lib/docs-nav.ts` appends the
+ Changelog by hand at the end of `getDocsNav()`:
+ ```ts
+ const changelog: DocsNavItem = { title: "Changelog", url: "/docs/changelog" };
+ ```
+ Add the registry the same way. The navbar links array
+ (`LINKS` in `src/components/site-navbar.tsx`) and footer links
+ (`src/components/site-footer.tsx`) are simple arrays — add a "Plugins" entry
+ to each.
+- **GitHub API**: `GET https://api.github.com/search/repositories?q=topic:ap-sdk-plugin&sort=stars&order=desc&per_page=100`
+ with headers `Accept: application/vnd.github+json` and (when
+ `process.env.GIT_TOKEN` is set) `Authorization: Bearer ${GIT_TOKEN}` — the
+ **same env var** `[[...slug]]/page.tsx` already uses for
+ `getGithubLastEdit`. Unauthenticated search is 10 req/min; cached fetches
+ make this a non-issue: use Next's fetch cache with
+ `next: { revalidate: 3600 }` so the page is static + hourly ISR.
+ Relevant response fields per item: `full_name`, `name`, `owner.login`,
+ `owner.avatar_url`, `description`, `stargazers_count`, `pushed_at`,
+ `html_url`, `topics`.
+- **Install command per card**: `npx ap-sdk install owner/repo` — the syntax
+ from `packages/agent-plugin-sdk/src/cli.ts` HELP and `src/github.ts`
+ (`parseGithubSpec` accepts bare `owner/repo`). **The SDK accepts only local
+ paths and GitHub specs today** (`isGithubSpec` is the sole remote gate in
+ `cli.ts`); there is no npm source — see plan 013.
+- **npm channel discovery**: npm registry search API —
+ `GET https://registry.npmjs.org/-/v1/search?text=keywords:ap-sdk-plugin&size=250`
+ — no auth, generous rate limits. Relevant fields per result object:
+ `package.name`, `package.version`, `package.description`, `package.date`,
+ `package.links.repository` (may be absent), `package.publisher.username`.
+ Merge key against GitHub results: normalize `links.repository` to
+ `owner/repo` (strip protocol/`.git`) and match `full_name`
+ case-insensitively. npm-only results (keyword set but repo untagged or
+ missing) still get a card — channel `npm` only.
+- **Copy-button precedent**: `src/components/code-line.tsx` +
+ `code-line-copy-button.tsx` render a single-line command with a copy action —
+ reuse `CodeLine` for the card's install command if its props fit (read the
+ file); otherwise mirror its copy-state pattern (Check/Copy icon swap).
+- **Styling conventions**: `cn()`, `border-border/60`, `bg-card/60`,
+ muted-foreground, `rounded-xl` cards (see `DocsMobileNav`), lucide icons
+ (`Star` exists in lucide). Card hover: `hover:bg-accent/50` as in the
+ sidebar.
+- **Seeding reality**: as of planning, **zero repos carry the topic**. The
+ in-repo examples live inside this monorepo and can't be topic-tagged
+ individually — so the empty state is the launch state and must sell the
+ action: "Tag your repo with the `ap-sdk-plugin` topic and it appears here
+ within the hour." Tagging this monorepo itself with `ap-sdk-plugin` is an
+ operator decision (it would make the SDK repo the first card) — surface it
+ in the PR description, don't do it from code.
+- Docs app is private — **no `.tegami/` changelog**.
+
+## Commands you will need
+
+| Purpose | Command | Expected on success |
+|---|---|---|
+| Install deps | `pnpm install` | exit 0 |
+| Probe the API | `curl -s "https://api.github.com/search/repositories?q=topic:ap-sdk-plugin&per_page=5" \| head -40` | JSON with `total_count` (likely 0 items today) |
+| Dev server | `pnpm --filter @jal-co/docs dev` | serves on localhost |
+| Typecheck | `pnpm turbo typecheck --filter=@jal-co/docs` | exit 0 |
+| Full gate | `pnpm turbo typecheck test lint build` | all tasks pass |
+
+## Scope
+
+**In scope** (the only files you should modify/create):
+
+- `apps/docs/src/app/docs/plugins/page.tsx` (create — server component: fetch + shell)
+- `apps/docs/src/lib/github-plugins.ts` (create — typed fetcher)
+- `apps/docs/src/components/plugin-directory.tsx` (create — client component: filter/sort/cards over server-fetched data)
+- `apps/docs/src/lib/docs-nav.ts` (append the Plugins nav item)
+- `apps/docs/src/components/site-navbar.tsx` (`LINKS` array entry)
+- `apps/docs/src/components/site-footer.tsx` (`links` array entry)
+- `apps/docs/content/docs/installing-plugins.mdx` (one cross-link)
+
+**Out of scope** (do NOT touch / do NOT build):
+
+- Per-plugin detail pages, pagination, download counts, a "report" flow —
+ v2 material (report needs an issue template first; note in Maintenance).
+- Any validation that listed repos actually pass `ap-sdk check` — v1 lists
+ what the topic returns; automated vetting is deferred (see Maintenance).
+- `packages/agent-plugin-sdk/**`.
+- Plan 008's `showcase.mdx` — superseded; if it was already executed, this
+ plan **replaces** it: delete `showcase.mdx` + its `meta.json` entry and
+ redirect links (add those files to scope in that case and say so in the PR).
+
+## Git workflow
+
+- Branch: `feat/plugin-registry-page`
+- Commit style: `feat(docs): plugin registry page driven by the ap-sdk-plugin topic`
+- Do NOT push or open a PR unless the operator instructed it.
+
+## Steps
+
+### Step 1: Typed fetcher
+
+Create `src/lib/github-plugins.ts`:
+
+```ts
+export type PluginChannel = "github" | "npm";
+export interface PluginEntry {
+ id: string; // "owner/repo" or npm name for npm-only entries
+ fullName: string | null; // "owner/repo" (null for npm-only with no repo link)
+ name: string;
+ owner: string; // repo owner or npm publisher
+ avatarUrl: string | null;
+ description: string | null;
+ stars: number | null; // null for npm-only entries
+ updatedAt: string; // ISO — pushed_at or package.date
+ repoUrl: string | null;
+ npmName: string | null; // set when the npm channel is present
+ npmVersion: string | null;
+ channels: PluginChannel[]; // at least one
+ topics: string[];
+}
+export async function fetchPluginEntries(): Promise;
+```
+
+- Fetch **both** sources in parallel (`Promise.allSettled`), each with
+ `next: { revalidate: 3600 }`: the GitHub topic search (auth header only when
+ `GIT_TOKEN` is set) and the npm keyword search (no auth).
+- Merge: GitHub results are the base entries (`channels: ["github"]`); each
+ npm result whose normalized `links.repository` matches a base entry's
+ `full_name` enriches it (`npmName`, `npmVersion`, push `"npm"`); unmatched
+ npm results become npm-only entries (`channels: ["npm"]`, `stars: null`).
+- **Both** sources failing → return `null` (degraded state). One failing →
+ proceed with the other and log a server-side warning; never crash the build
+ (wrap in try/catch).
+- Filter out archived (`archived: true`) and forked (`fork: true`) GitHub
+ repos.
+
+**Verify**: `pnpm turbo typecheck --filter=@jal-co/docs` → exit 0.
+
+### Step 2: Page shell
+
+Create `src/app/docs/plugins/page.tsx` modeled on `changelog/page.tsx`:
+
+- `metadata`: title "Plugins", description "Community plugins built with
+ ap-sdk — tag your repo ap-sdk-plugin to get listed."
+- Server component: `const repos = await fetchPluginRepos();`
+- Header block (changelog markup): h1 "Plugins", description line, plus an
+ intro row with the general install pattern as a copyable line
+ (`npx ap-sdk install owner/repo`) and a sentence linking
+ `/docs/installing-plugins`.
+- Render ` `.
+- **"Get listed" section** (always rendered, below the directory): two
+ channel paths — **On GitHub**: 1. your repo has a `plugin.ts` that passes
+ `npx ap-sdk check`; 2. add the `ap-sdk-plugin` **topic** (About → ⚙ →
+ Topics). **On npm** (optional, for plugins also published as a package):
+ add `"ap-sdk-plugin"` to the `keywords` array in `package.json` before
+ publishing. Either/both appear here within the hour (hourly refresh). Link
+ plan 007's template repo here once it exists (leave a plain sentence, no
+ dead link).
+
+**Verify**: dev server — `/docs/plugins` renders header + empty/degraded state.
+
+### Step 3: Directory component
+
+Create `src/components/plugin-directory.tsx` (client):
+
+- Props: `{ entries: PluginEntry[]; degraded: boolean }`.
+- Controls row (only when `entries.length > 0`): text input filtering on
+ name/description/owner (case-insensitive substring, client-side), a channel
+ filter (All / GitHub / npm — segmented control or select), and a sort
+ select: **Stars** (default; npm-only entries sort after starred ones) /
+ **Recently updated** (`updatedAt` desc) / **A–Z**. Match input/button
+ styling to existing components (border, focus-visible ring — copy from
+ `docs-sidebar.tsx` conventions).
+- Card grid (`sm:grid-cols-2`, wider container per "Current state"): each card
+ a `rounded-xl border border-border/60 bg-card/60` block with:
+ owner avatar when present (plain `img`, `size-5 rounded-full`) + name
+ linking to `repoUrl` (or `https://www.npmjs.com/package/{npmName}` for
+ npm-only entries; external, `rel="noreferrer"`), description (line-clamped),
+ meta row — `Star` icon + count (omit when `stars === null`), "updated
+ {relative time}" (derive with `Intl.RelativeTimeFormat`; no date lib),
+ **channel badges** (`git`, `npm` — small bordered pills), up to 3
+ non-`ap-sdk-plugin` topics as badges — and the **install options**:
+ - `github` channel → copyable `npx ap-sdk install {fullName}` (reuse
+ `CodeLine` if suitable, else the copy-icon pattern from
+ `code-line-copy-button.tsx`).
+ - `npm` channel → if plan 013 has landed (npm source in the SDK), a second
+ copyable line `npx ap-sdk install npm:{npmName}`; otherwise an `npm` link
+ to the package page only. Gate this on a single boolean constant
+ (`NPM_INSTALL_SUPPORTED`) at the top of the component with a comment
+ pointing at plan 013, so flipping it is a one-line change.
+ - npm-only entries (no repo) show whichever of the above applies and no
+ GitHub command.
+- States: `degraded` → "Couldn't reach GitHub — browse the topic directly"
+ linking `https://github.com/topics/ap-sdk-plugin`; empty (not degraded) →
+ a friendly "No plugins tagged yet — be the first" card pointing at the
+ get-listed steps; filter-no-match → "No plugins match “{query}”".
+
+**Verify**: dev server with mocked data — temporarily hardcode 4 fake
+`PluginEntry` objects covering all three shapes (github-only, both channels,
+npm-only) plus one more github-only (then remove) to confirm filter, channel
+filter, all three sorts (including null-star ordering), per-channel install
+options, copy button, and dark mode; then restore the real fetch and confirm
+the empty state (today's reality).
+
+### Step 4: Wire navigation
+
+- `docs-nav.ts`: append `{ title: "Plugins", url: "/docs/plugins" }` next to
+ the changelog append (keep Plugins before Changelog).
+- `site-navbar.tsx` `LINKS`: add `{ label: "Plugins", href: "/docs/plugins" }`.
+- `site-footer.tsx` `links`: same.
+- `installing-plugins.mdx`: one line — "Looking for plugins? Browse the
+ [plugin directory](/docs/plugins)."
+
+**Verify**: dev server — Plugins appears in sidebar, navbar, footer; sidebar
+marks it active on the route.
+
+### Step 5: Gate
+
+Confirm the production build succeeds **with the network blocked or the API
+returning 0 items** (today's state already tests this): the page must
+statically render the empty state without failing the build.
+
+**Verify**: `pnpm turbo typecheck test lint build` → all tasks pass.
+
+## Test plan
+
+No test runner in the docs app. Verification: the Step 3 mocked-data QA
+(filter/sort/copy/dark-mode), the real empty-state render, the degraded-state
+render (temporarily point the fetcher at an invalid URL in dev, observe the
+fallback, revert), and the build gate. Record all four in the PR description.
+
+## Done criteria
+
+- [ ] `/docs/plugins` renders: populated grid (mock QA), empty state, and degraded state correctly
+- [ ] Data merges GitHub topic search + npm keyword search (`ap-sdk-plugin`), cached with hourly revalidation, auth header only on the GitHub call when `GIT_TOKEN` set
+- [ ] One source failing still renders the other; both failing → degraded state
+- [ ] Filter, channel filter, and three sorts work client-side; install options match each entry's channels (github command copyable; npm command gated on the plan-013 flag)
+- [ ] "Get listed" steps rendered; nav/sidebar/footer/cross-link wired
+- [ ] Build succeeds with zero API results (`pnpm turbo build --filter=@jal-co/docs`)
+- [ ] No mock data left in the committed page (`rg -n "fake\|mock" apps/docs/src/app/docs/plugins` → 0)
+- [ ] `pnpm turbo typecheck test lint build` exits 0
+- [ ] No files outside the in-scope list modified (`git status`) — unless superseding an executed plan 008, stated in the PR
+- [ ] `plans/README.md` status rows updated (this plan AND plan 008 → SUPERSEDED note)
+
+## STOP conditions
+
+Stop and report back if:
+
+- The GitHub search API shape differs from the fields listed in "Current
+ state" (verify with the curl probe first).
+- The npm search API response doesn't carry `links.repository` in practice
+ (probe with `curl -s "https://registry.npmjs.org/-/v1/search?text=keywords:ap-sdk-plugin&size=5"`
+ — if the target keyword returns nothing, probe a populated keyword like
+ `pi-package` to inspect the shape) — report before inventing a different
+ merge key.
+- Next's `revalidate` fetch caching conflicts with how this app is deployed
+ (check `next.config.mjs` for `output: "export"` — a fully static export
+ can't ISR; if found, fall back to build-time fetch + a "refreshed at build"
+ note, and report the tradeoff).
+- You're tempted to add a date/fetch library — the runtime covers both.
+
+## Maintenance notes
+
+- **Plan 008 is superseded by this page** — the index reflects it. If a
+ curated "featured" strip is wanted later, it's a small constant array on top
+ of this page, not a separate page.
+- **When plan 013 lands, flip `NPM_INSTALL_SUPPORTED`** so npm-channel cards
+ gain their copyable `npx ap-sdk install npm:` line — a one-line change
+ by design.
+- v2 candidates, in rough order: pass the topic search through `ap-sdk check`
+ in a scheduled job and badge validated plugins; a "report" link backed by an
+ issue template (pi.dev has this); per-plugin detail pages; pagination past
+ 100 repos; npm download counts (`api.npmjs.org/downloads`) as a sort key.
+- Operator follow-ups this plan surfaces but doesn't do: add the
+ `ap-sdk-plugin` topic to the template repo (plan 007) and decide whether
+ this monorepo gets the topic itself.
+- The `GIT_TOKEN` env var is shared with the last-edited feature — if it's
+ rotated/removed, both degrade gracefully, but search rate limits get tight;
+ keep the token in the deploy environment.
diff --git a/plans/013-npm-install-source.md b/plans/013-npm-install-source.md
new file mode 100644
index 0000000..ccb626c
--- /dev/null
+++ b/plans/013-npm-install-source.md
@@ -0,0 +1,300 @@
+# Plan 013: Support npm as a plugin source — `ap-sdk install npm:`
+
+> **Executor instructions**: Follow this plan step by step. Run every
+> verification command and confirm the expected result before moving to the
+> next step. If anything in the "STOP conditions" section occurs, stop and
+> report — do not improvise. When done, update the status row for this plan
+> in `plans/README.md`.
+>
+> **Drift check (run first)**: `git diff --stat fe6fb92..HEAD -- packages/agent-plugin-sdk/src packages/agent-plugin-sdk/test`
+> If any in-scope file changed since this plan was written, compare the
+> "Current state" excerpts against the live code before proceeding; on a
+> mismatch, treat it as a STOP condition.
+
+## Status
+
+- **Priority**: P2
+- **Effort**: M
+- **Risk**: MED — network fetch + tarball extraction of third-party content;
+ must mirror the existing GitHub path's safety properties exactly
+- **Depends on**: none (plan 012's registry gates its npm install command on
+ this landing — flip its `NPM_INSTALL_SUPPORTED` flag afterward)
+- **Category**: dx / direction
+- **Planned at**: commit `fe6fb92`, 2026-07-01
+
+## Why this matters
+
+Plugin authors distribute one of two ways: a GitHub repo, or an npm package
+(often both — npm gives them versioning, provenance, and download stats). The
+SDK can consume the first (`ap-sdk install owner/repo`) but not the second:
+`cli.ts` routes remote specs exclusively through `isGithubSpec`. So an
+npm-published plugin has **no one-liner install at all** — the consumer would
+have to `npm pack`/extract by hand. `ap-sdk install npm:` closes the
+gap, mirrors the convention users already know from pi
+(`pi install npm:`), and lets the plan-012 registry show a copyable
+install command for npm-channel plugins.
+
+## Current state
+
+- **The GitHub source module is the exact template**:
+ `packages/agent-plugin-sdk/src/github.ts` (195 lines) —
+ - `parseGithubSpec(spec)` → `{ owner, repo, ref } | null`;
+ - `isGithubSpec(spec)` — spec-vs-local-path disambiguation (bare
+ `owner/repo` only counts when it isn't an existing local path and doesn't
+ end in a JS/TS extension);
+ - `fetchGithubPlugin(spec, { path })` — downloads a tarball to
+ `mkdtempSync(join(tmpdir(), …))`, extracts with `tar`'s `x`
+ (`tarExtract`) through `createGunzip()` + `pipeline`, locates the plugin
+ file via `locatePlugin`/`DEFAULT_PLUGIN_FILES` (`src/plugin-files.ts`),
+ returns `{ pluginPath, label, cleanup }`.
+ Read the whole file before writing any code — the npm module must match its
+ structure, error-message style, and cleanup contract.
+- **CLI routing**: `src/cli.ts` `main()` —
+ ```ts
+ const remote = args.plugin ? isGithubSpec(args.plugin) : false;
+ ...
+ const fetched = await fetchGithubPlugin(spec, { path: args.path });
+ pluginPath = fetched.pluginPath;
+ sourceLabel = fetched.label;
+ process.on("exit", fetched.cleanup);
+ ```
+ Remote sources then pass an explicit compatibility gate:
+ `validatePlugin(plugin)` + `printCompatibility(plugin, sourceLabel)`, and
+ `check` on a remote source is exactly that report. The npm path must flow
+ through the **same** gate — extend the routing, don't fork it.
+- **npm registry endpoints** (no auth):
+ - Packument (choose version):
+ `GET https://registry.npmjs.org/` → `dist-tags.latest`,
+ `versions[].dist.tarball`;
+ - or direct version doc:
+ `GET https://registry.npmjs.org//` →
+ `dist.tarball` (simpler — prefer this: one request with `latest` as the
+ default tag).
+ - Tarballs are gzipped tars with a **`package/` prefix on every path** —
+ extraction needs `strip: 1` (the GitHub codeload tarball has the same
+ property; check how `github.ts` handles the prefix and match).
+- **Spec grammar to support**: `npm:`, `npm:@`,
+ scoped packages `npm:@scope/name` and `npm:@scope/name@1.2.3` (the `@`
+ after the scope's name is the version separator — split on the **last** `@`
+ past index 0).
+- **Where the plugin file lives in a package**: same `locatePlugin` walk over
+ the extracted root, plus `--path` for monorepo-style packages — identical
+ semantics to the GitHub path. Additionally, if the extracted `package.json`
+ has an `ap-sdk` field naming the plugin file (`{ "ap-sdk": { "plugin":
+ "./plugin.ts" } }`), prefer it — **decision**: include this field read
+ (cheap, forward-looking, documented), but `locatePlugin` remains the
+ fallback.
+- **Deps**: `tar` and `yaml` already shipped; Node 18+ global `fetch` — no
+ new dependencies.
+- **HELP text**: `src/cli.ts` HELP documents the `plugin` argument's accepted
+ forms — must gain the npm form.
+- **Docs**: `apps/docs/content/docs/installing-plugins.mdx` documents GitHub
+ sources; gains an npm section.
+- **Tests**: `test/github.test.ts` exists — read it first to see how the
+ GitHub module is tested (parse-level tests vs mocked fetch) and mirror the
+ approach.
+- Repo AGENTS.md: user-facing SDK feature → `.tegami/` changelog, **minor**.
+
+## Commands you will need
+
+| Purpose | Command | Expected on success |
+|---|---|---|
+| Install deps | `pnpm install` | exit 0 |
+| Probe registry shape | `curl -s https://registry.npmjs.org/tar/latest \| head -c 400` | JSON with `dist.tarball` |
+| SDK tests | `pnpm turbo test --filter=@jalco/ap-sdk` | all pass |
+| Typecheck | `pnpm turbo typecheck --filter=@jalco/ap-sdk` | exit 0 |
+| Full gate | `pnpm turbo typecheck test lint build` | all tasks pass |
+
+## Scope
+
+**In scope** (the only files you should modify/create):
+
+- `packages/agent-plugin-sdk/src/npm.ts` (create — mirror of `github.ts`)
+- `packages/agent-plugin-sdk/src/cli.ts` (HELP + remote routing)
+- `packages/agent-plugin-sdk/test/npm.test.ts` (create)
+- `apps/docs/content/docs/installing-plugins.mdx` (npm source section)
+- `.tegami/2026-07-01-npm-source.md` (create)
+
+**Out of scope** (do NOT touch):
+
+- `src/github.ts` — **exception**: extracting a small shared helper (e.g. the
+ tarball-download-and-extract routine) into a new `src/util/` module used by
+ both is allowed *only if* it's a pure move; if the refactor grows beyond
+ ~30 lines of churn, duplicate instead and note it.
+- Publishing/registry *authoring* helpers (`ap-sdk publish`) — different
+ feature.
+- npm install via a package manager (`npm i` into the consumer's project) —
+ this feature fetches and installs artifacts, exactly like the GitHub path;
+ it does not touch the consumer's `package.json`.
+- Private registries / `.npmrc` auth — v1 is the public registry; STOP if it
+ turns out to be required.
+
+## Git workflow
+
+- Branch: `feat/npm-install-source`
+- Commit style: `feat(sdk): install plugins from npm with npm: specs`
+- Do NOT push or open a PR unless the operator instructed it.
+
+## Steps
+
+### Step 1: Spec parsing
+
+In `src/npm.ts`, implement and export:
+
+```ts
+export interface NpmSpec { name: string; version?: string } // version = exact or dist-tag
+export function parseNpmSpec(spec: string): NpmSpec | null;
+export function isNpmSpec(spec: string): boolean; // strict: /^npm:/i
+```
+
+Parsing rules (unit-test each): `npm:foo` → `{name:"foo"}`;
+`npm:foo@1.2.3` → `{name:"foo",version:"1.2.3"}`; `npm:@scope/foo` →
+`{name:"@scope/foo"}`; `npm:@scope/foo@next` →
+`{name:"@scope/foo",version:"next"}`; reject empty names and names failing
+npm's name rules (lowercase, URL-safe — a simple
+`/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/` check is enough).
+Unlike GitHub's bare `owner/repo`, the `npm:` prefix is mandatory — no
+ambiguity with local paths, so `isNpmSpec` is a pure prefix test.
+
+**Verify**: `pnpm turbo typecheck --filter=@jalco/ap-sdk` → exit 0.
+
+### Step 2: Fetch + extract
+
+Implement `fetchNpmPlugin(spec: string, options?: { path?: string })`
+returning `{ pluginPath, label, cleanup }` — the same contract as
+`fetchGithubPlugin` (read its implementation and mirror error handling,
+temp-dir naming, and cleanup registration):
+
+1. `GET https://registry.npmjs.org//` — 404 →
+ fail with `npm package "" not found` (or version-specific message);
+ other non-OK → include status in the error.
+2. Download `dist.tarball`, extract through `createGunzip()` + `tarExtract`
+ with the `package/` prefix stripped (match `github.ts`'s prefix handling).
+3. Resolve the plugin file: `options.path` subdir if given → `package.json`'s
+ `ap-sdk.plugin` field if present → `locatePlugin` walk. Not found → fail
+ with the same style of message the GitHub path uses, plus a hint about the
+ `ap-sdk.plugin` field.
+4. `label` = `npm:@` (read the concrete version from
+ the version doc so `latest` resolves to a number in output).
+
+**Verify**: `cd packages/agent-plugin-sdk && pnpm cli check npm:tar 2>&1 | head -3`
+→ fails *gracefully* with the not-a-plugin/compatibility message (proves
+fetch+extract+gate work end-to-end against a real package that isn't a
+plugin; exact wording per the CLI's existing remote-incompatible branch).
+
+### Step 3: CLI routing
+
+In `src/cli.ts`:
+
+- HELP: extend the `plugin` argument description with `npm:package[@version]`
+ and add an example (`ap-sdk install npm:@acme/git-helper`).
+- Routing: where `main()` computes `const remote = … isGithubSpec(…)`,
+ generalize to detect npm specs too and dispatch to the matching fetcher;
+ everything downstream (compat gate, `printCompatibility`, `check`
+ short-circuit, `process.on("exit", cleanup)`) stays shared and untouched.
+ `--ref` is GitHub-only — `fail` with a clear message if combined with an
+ npm spec (`use npm:@ instead`).
+
+**Verify**: `pnpm cli check npm:definitely-not-a-real-pkg-xyz` → exits 1 with
+the not-found message; `pnpm cli install --help`-equivalent (`pnpm cli --help`)
+shows the npm form.
+
+### Step 4: Tests
+
+Create `test/npm.test.ts`, mirroring `test/github.test.ts`'s approach (read it
+first — if it unit-tests parsing only, do the same and cover fetch behavior
+with a mocked global `fetch` via `vi.stubGlobal`):
+
+1. `parseNpmSpec` — the six cases from Step 1 (accept ×4, reject ×2).
+2. `isNpmSpec` — `npm:foo` true; `./npm:weird` false; `owner/repo` false.
+3. With mocked `fetch` + a fixture tarball built in-test (`tar.c` into a
+ buffer with a `package/plugin.ts` entry): `fetchNpmPlugin` resolves
+ `pluginPath` to the extracted plugin, label includes the resolved version,
+ `cleanup()` removes the temp dir.
+4. Mocked 404 → rejects with a message containing the package name.
+5. `package.json` `ap-sdk.plugin` field takes precedence over `locatePlugin`
+ (fixture tarball with both a root `plugin.ts` and a field pointing at
+ `custom/entry.ts`).
+
+**Verify**: `pnpm turbo test --filter=@jalco/ap-sdk` → all pass including the
+new file.
+
+### Step 5: Docs + changelog + gate
+
+- `installing-plugins.mdx`: add an "From npm" section — spec forms, version
+ pinning (`npm:name@1.2.3`), the `ap-sdk.plugin` package.json field, and the
+ note that the compatibility check runs before anything is installed (same
+ as GitHub sources).
+- `.tegami/2026-07-01-npm-source.md`:
+
+ ```md
+ ---
+ packages:
+ "@jalco/ap-sdk": minor
+ ---
+
+ ## Install plugins from npm
+
+ `ap-sdk install npm:` (and `check`/`build` with the same spec)
+ fetches a published package from the npm registry, verifies it's a
+ compatible plugin, and installs it — with `npm:@` for
+ pinning. Packages can point at a non-root plugin file via an
+ `ap-sdk.plugin` field in package.json.
+ ```
+
+**Verify**: `pnpm turbo typecheck test lint build` → all tasks pass.
+
+### Step 6: Notify the registry plan
+
+If plan 012 already landed, flip its `NPM_INSTALL_SUPPORTED` constant in
+`apps/docs/src/components/plugin-directory.tsx` to `true` (one line — this is
+the documented hand-off; add the file to scope in your PR and say so). If 012
+hasn't landed, note in `plans/README.md` that 013 landed first.
+
+**Verify**: whichever branch applies is reflected in `plans/README.md`.
+
+## Test plan
+
+See Step 4 — parse-level unit tests plus mocked-fetch integration tests with
+in-test fixture tarballs; pattern source `test/github.test.ts`. No live
+network calls in the suite (Step 2/3's live probes are manual smoke checks,
+recorded in the PR description).
+
+## Done criteria
+
+- [ ] `ap-sdk check npm:` fails gracefully with the compatibility message (manual smoke, transcript in PR)
+- [ ] All Step 4 tests pass; suite green
+- [ ] `--ref` + npm spec → clear error
+- [ ] HELP shows the npm form; docs page has the "From npm" section
+- [ ] Shared compat gate untouched (`printCompatibility` / `validatePlugin` call sites unchanged for the GitHub path)
+- [ ] `.tegami/2026-07-01-npm-source.md` exists with `minor` bump
+- [ ] `pnpm turbo typecheck test lint build` exits 0
+- [ ] No files outside the in-scope list modified (`git status`), Step 6 exception declared if taken
+- [ ] `plans/README.md` status row updated
+
+## STOP conditions
+
+Stop and report back if:
+
+- `github.ts`'s structure differs materially from the "Current state" summary
+ (drifted) — re-read before mirroring.
+- The registry version-doc endpoint (`//`) is unavailable or
+ shaped differently than probed — report; don't silently switch to full
+ packuments without noting the payload-size tradeoff.
+- Extraction of a fixture tarball writes outside the temp dir in any test
+ (path traversal in an entry name) — that's a security finding; report
+ immediately and check whether `tar`'s extract options already guard it
+ (they should — `tar` v7 strips absolute paths and `..` by default; verify,
+ and add an explicit test if the guarantee is configurable).
+- Private-registry/auth support turns out to be needed for the acceptance
+ smoke — v1 is public-registry only by design.
+
+## Maintenance notes
+
+- The `ap-sdk.plugin` package.json field is now public API surface — document
+ it wherever plugin packaging docs land (plan 007's template repo should set
+ it).
+- If a shared tarball helper was extracted, both source modules now depend on
+ it — changes to one source's extraction must run both test files.
+- Future: `--registry` flag / `.npmrc` respect for private registries;
+ provenance/signature checks; npm download counts feeding plan 012's sort.
diff --git a/plans/README.md b/plans/README.md
new file mode 100644
index 0000000..3e7fd74
--- /dev/null
+++ b/plans/README.md
@@ -0,0 +1,77 @@
+# Implementation Plans
+
+Generated by the improve skill on 2026-07-01 (audit at commit `fe6fb92`),
+focused on growth: features, DX, docs, and distribution. Execute in the order
+below unless dependencies say otherwise. Each executor: read the plan fully
+before starting, honor its STOP conditions, and update your row when done.
+
+Repo verify gate (required before every commit, per AGENTS.md):
+`pnpm turbo typecheck test lint build`. User-facing changes to
+`packages/agent-plugin-sdk` need a `.tegami/` changelog; docs-only changes to
+`apps/docs/` do not.
+
+## Execution order & status
+
+| Plan | Title | Priority | Effort | Depends on | Status |
+|------|-------|----------|--------|------------|--------|
+| 001 | Overhaul root README + add npm package README | P1 | S | — | DONE |
+| 002 | Add `ap-sdk init` scaffold command | P1 | M | — | DONE |
+| 003 | Per-feature docs pages (skills…files) | P1 | M | — | DONE |
+| 004 | Make `ap-sdk port` the acquisition wedge | P1 | M | 001 | DONE |
+| 005 | `ap-sdk uninstall` + install manifest | P2 | M | — | DONE |
+| 006 | `ap-sdk dev` watch mode | P2 | M | — | DONE |
+| 007 | Distribution spike: template repo + GitHub Action | P2 | M | 002 | DONE |
+| 008 | Plugin showcase page | P3 | S | — | SUPERSEDED — replaced by 012 (topic-driven registry automates the curation) |
+| 009 | Generated API reference (spike + wire-up) | P3 | M | 003 | DONE |
+| 010 | Docs search UI (⌘K), prev/next, edit-this-page | P2 | M | — | DONE |
+| 011 | Docs Callout/Tabs, mobile TOC + AI copy, dead navbar cleanup | P2 | M | — | DONE |
+| 012 | Plugin registry page (GitHub topic + npm keyword `ap-sdk-plugin`) | P2 | M | — | DONE |
+| 013 | npm install source — `ap-sdk install npm:` | P2 | M | — | DONE |
+
+Status values: TODO | IN PROGRESS | DONE | BLOCKED (with one-line reason) |
+REJECTED (with one-line rationale)
+
+## Dependency notes
+
+- **004 requires 001**: 004 promotes the "Already have a plugin?" README
+ section that 001 creates.
+- **007 requires 002**: the template repo's `plugin.ts` must be `ap-sdk init`'s
+ scaffold output so the two never drift.
+- **009 requires 003**: hand-written feature guides convert better than a
+ generated reference; ship them first, then let 009 complement them.
+- **011 should land before 003**: the seven feature pages want Callout/Tabs
+ components instead of faking them with blockquotes.
+- **012 supersedes 008**: same discovery goal, but self-serve via the GitHub
+ `ap-sdk-plugin` topic instead of hand-curated MDX. If 008 was already
+ executed, 012 removes `showcase.mdx` (documented in its Scope).
+- **012 pairs with 007**: the template repo should ship with the
+ `ap-sdk-plugin` topic pre-applied so template users get listed automatically.
+- **012 ↔ 013**: the registry lists both channels (git-only and npm-published)
+ from day one, but npm-channel cards only show a copyable
+ `npx ap-sdk install npm:` command once 013 lands — gated by a one-line
+ `NPM_INSTALL_SUPPORTED` flag in 012's directory component; whichever plan
+ lands second flips/acknowledges it (documented in both plans).
+- 013 also touches `src/cli.ts` — execute serially with 002/005/006. 013 landed before 012, so no registry flag existed to flip.
+- 002, 005, and 006 all edit `src/cli.ts` (HELP + dispatch) — execute them
+ serially, not in parallel branches, to avoid conflicts.
+- 003, 004, and 008 all edit `apps/docs/content/docs/meta.json` — same caution.
+- 010, 011, and 012 all touch `apps/docs/src` (010/011 share
+ `docs/[[...slug]]/page.tsx`; 010/011/012 all touch `site-navbar.tsx` or
+ `docs-nav.ts`) — execute serially, not in parallel branches.
+
+## Findings considered and rejected
+
+- **Performance optimization of the SDK**: it's a file emitter with no hot
+ paths; `build()` is pure and fast. Nothing measurable to win.
+- **Trimming runtime dependencies** (`tar`, `tsx`, `yaml`): all three are
+ load-bearing (GitHub tarballs, TS plugin loading, YAML frontmatter).
+- **A hosted plugin registry** (own backend/database): still rejected; plan
+ 012's GitHub-topic-driven page provides registry UX with zero infrastructure.
+- **fumadocs-ui stock components for search/callouts/tabs**: the site's design
+ system is deliberately hand-rolled (jalco-ui); plans 010/011 build on
+ `fumadocs-core` hooks + local components instead.
+- **Marketing landing page at `/`**: the `/` → `/docs` redirect is a recorded
+ decision in `src/app/page.tsx` ("the docs are the site"); revisit only if
+ plan 004's port-wedge marketing demands a hero page.
+- **`create-ap-sdk` npm package**: deferred until `ap-sdk init` (002) proves
+ demand; it needs its own publish pipeline.