diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..27eb1c5 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,15 @@ +{ + "name": "pstack", + "description": "pstack plugin catalog: skills, agents, and sticky /poteto-mode for Claude Code.", + "owner": { + "name": "Ed Heltzel", + "url": "https://github.com/edheltzel" + }, + "plugins": [ + { + "name": "pstack", + "source": "./", + "description": "pstack skills and agents. Claude slash skills are /pstack:do-*." + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..4ac81dd --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "pstack", + "displayName": "pstack", + "version": "0.14.5", + "description": "pstack skills, agents, and sticky /poteto-mode. Claude slash skills are /pstack:do-*.", + "author": { + "name": "Ed Heltzel", + "url": "https://github.com/edheltzel" + }, + "homepage": "https://github.com/edheltzel/pstack-for-omp", + "repository": "https://github.com/edheltzel/pstack-for-omp", + "license": "MIT", + "keywords": ["pstack", "poteto-mode", "skills", "omp"] +} diff --git a/AGENTS.md b/AGENTS.md index ddf695d..5ef5100 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # pstack -omp plugin: one factory (`extensions/pstack.ts`), skills, agents, and commands. +omp + Claude Code plugin: one factory (`extensions/pstack.ts`), Claude manifest (`.claude-plugin/plugin.json`), skills, agents. ## Codegraph @@ -51,7 +51,7 @@ Closeout: re-check paths, update owning docs, refresh indexes, run verification, ## Project contracts - Static gate (CI and local): `python3 e2e/run.py --skip-rpc` and `npm test` (Vitest). -- No `omp`, no `omp --mode rpc`, no TUI on GitHub runners. Live RPC is local only: `python3 e2e/run.py` without `--skip-rpc`. +- No `omp`, no `omp --mode rpc`, no TUI, no `claude` on GitHub runners. Live RPC is local only: `python3 e2e/run.py` without `--skip-rpc`. Host-only prove (`omp plugin doctor`, `claude plugin validate`) is documented in e2e/AGENTS.md. - One factory. Poteto-mode is per-session, never a process-wide boolean. ## Native omp @@ -64,6 +64,17 @@ Tell operators plugin-link only: - Do not marketplace. Do not copy `pstack.ts` into `~/.omp/agent/extensions/`. - Do not keep a repo `.omp/skills` symlink. Sibling `skills/` is what plugin-link discovers. +## Native Claude Code + +Tell operators plugin-first only: + +- Manifest: `.claude-plugin/plugin.json`. Catalog: `.claude-plugin/marketplace.json`. Skills stay at plugin-root `skills/`, not inside `.claude-plugin/`. +- Install: `claude plugin marketplace add ` then `claude plugin install pstack@pstack`. +- This session only: `claude --plugin-dir ./` +- Slash skills: `/pstack:do-*`. Do not rename the `do-` skill set. +- Validate on a host with Claude Code: `claude plugin validate .` +- Sticky `/poteto-mode` is omp factory only. Claude uses `/pstack:do-poteto-mode`. + ## Verification ```bash @@ -78,7 +89,7 @@ npm test | [extensions/AGENTS.md](extensions/AGENTS.md) | Factory `pstack.ts`; per-session poteto-mode | | [e2e/AGENTS.md](e2e/AGENTS.md) | Static doctor/lint + Vitest; live RPC local-only via plugin-link | | [skills/AGENTS.md](skills/AGENTS.md) | Skill tree; SKILL.md frontmatter `name` + `description` | -| [docs/AGENTS.md](docs/AGENTS.md) | original numbered `guide/`; first-run lives on README (`omp plugin link ./`) | +| [docs/AGENTS.md](docs/AGENTS.md) | original numbered `guide/`; first-run lives on README (omp plugin link + Claude plugin install) | | [automations/AGENTS.md](automations/AGENTS.md) | Dormant Benny pack; not slash skills | `agents/` and `commands/` have no child AGENTS.md; they follow this rail. Commands need frontmatter `description`. diff --git a/README.md b/README.md index 6aca7ed..6157b13 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pstack -This package is an installable omp **plugin**. From the package root, one-time `omp plugin link ./`. Later sessions (any cwd): start `omp`, type `/` (`/skill:do-…`, sticky `/poteto-mode`). Sibling `skills/` at the package root is what omp auto-discovers after link. +This package is an installable **plugin** for omp/Pi and Claude Code. Skills stay `do-*` on disk. omp exposes them as `/skill:do-…`. Claude Code exposes them as `/pstack:do-*`. Sticky `/poteto-mode` is the omp factory command. It is not the Cursor plugin and not an official Cursor port. @@ -16,7 +16,7 @@ Those pages are the Cursor product. This README does not retell them. ## Docs -- [The pstack guide](./docs/guide/README.md) — original numbered product tutorial (setup through recipes, plus images). On-disk skill links use `skills/do-*`; slash skills are `/skill:do-…`. `/poteto-mode` stays unprefixed. +- [The pstack guide](./docs/guide/README.md) — original numbered product tutorial (setup through recipes, plus images). On-disk skill links use `skills/do-*`. omp slash skills are `/skill:do-…`. Claude slash skills are `/pstack:do-*`. `/poteto-mode` stays unprefixed on omp. ## Automations @@ -24,40 +24,58 @@ pstack also ships a dormant [benny automation pack](./automations/benny/). Benny Setup starts at [`automations/benny/FOR_AGENTS.md`](./automations/benny/FOR_AGENTS.md). That README records the Cursor Automations host vs what omp can run. -## What omp is - -[omp](https://omp.sh) is “A coding agent with the IDE wired in.” +## Load this plugin -- Home: https://omp.sh -- Install omp: `curl -fsSL https://omp.sh/install | sh` -- Source: https://github.com/can1357/oh-my-pi -- Docs: https://omp.sh/docs -- How omp can install plugins in general (not this repo’s command): https://omp.sh/docs/plugins -- Authoring: https://omp.sh/docs/extension-authoring +Clone https://github.com/edheltzel/pstack-for-omp, then pick the host. -## Load this plugin +### omp / Pi -Clone https://github.com/edheltzel/pstack, then from the package root: +Install omp from https://omp.sh if needed (`curl -fsSL https://omp.sh/install | sh`). From the package root, one-time: ``` -git clone https://github.com/edheltzel/pstack.git -cd pstack +git clone https://github.com/edheltzel/pstack-for-omp.git +cd pstack-for-omp omp plugin link ./ ``` -That one-time link is how later sessions (any cwd) type `/` (`/skill:do-…`, sticky `/poteto-mode`). Confirm with `omp plugin list`. +Later sessions (any cwd): start `omp`, type `/` (`/skill:do-…`, sticky `/poteto-mode`). Sibling `skills/` at the package root is what omp auto-discovers after link. Confirm with `omp plugin list`. Prove the link with `omp plugin doctor`. + +Do not copy only `pstack.ts` into `~/.omp/agent/extensions/`. Do not marketplace-install this package on omp; local-link is the install path. + +omp docs: https://omp.sh/docs/plugins and https://omp.sh/docs/extension-authoring. + +### Claude Code + +This checkout is the plugin root: `.claude-plugin/plugin.json` plus `skills/` and `agents/` beside it (not inside `.claude-plugin/`). + +From the package root, add this repo as a marketplace and install: + +``` +claude plugin marketplace add /absolute/path/to/pstack-for-omp +claude plugin install pstack@pstack +``` + +From GitHub: `claude plugin marketplace add edheltzel/pstack-for-omp` then `claude plugin install pstack@pstack`. -Do not copy only `pstack.ts` into `~/.omp/agent/extensions/`. +This session only (no install record): + +``` +claude --plugin-dir ./ +``` + +Claude slash skills are namespaced: `/pstack:do-how`, `/pstack:do-poteto-mode`. Validate the layout with `claude plugin validate .`. Sticky `/poteto-mode` is omp-only; on Claude run `/pstack:do-poteto-mode`. + +Claude plugin docs: https://code.claude.com/docs/en/plugins. ## First steps -1. Install omp from https://omp.sh (`curl -fsSL https://omp.sh/install | sh`). -2. Clone this repository and, from the package root, `omp plugin link ./` (see Load this plugin). Later sessions (any cwd): `omp`, then type `/`. -3. `/poteto-mode` — enable sticky Poteto Mode for this conversation. Optional task arguments are passed through; this also sends `/skill:do-poteto-mode`. -4. Work as usual. Resume an on conversation and it stays on. `/new` starts off. -5. `/poteto-mode off` (aliases: `disable`, `stop`) — disable this conversation. +1. Install the host (omp from https://omp.sh, or Claude Code). +2. Load this plugin (see Load this plugin). +3. On omp: `/poteto-mode` — enable sticky Poteto Mode for this conversation. Optional task arguments are passed through; this also sends `/skill:do-poteto-mode`. On Claude: `/pstack:do-poteto-mode`. +4. Work as usual. On omp, resume an on conversation and it stays on. `/new` starts off. +5. `/poteto-mode off` (aliases: `disable`, `stop`) — disable this omp conversation. -If Poteto Mode was never turned on in this conversation, it is off. Mode is per conversation, not process-wide. +If Poteto Mode was never turned on in this omp conversation, it is off. Mode is per conversation, not process-wide. The TUI status reads `pstack: poteto mode` when on. Other omp surfaces may not show it. @@ -65,14 +83,14 @@ The TUI status reads `pstack: poteto mode` when on. Other omp surfaces may not s | Command | What it does | |---|---| -| `/poteto-mode` | Enable sticky Poteto Mode for this conversation. Also sends `/skill:do-poteto-mode`. | -| `/poteto-mode off` | Disable this conversation. Aliases: `disable`, `stop`. | +| `/poteto-mode` | omp factory: enable sticky Poteto Mode for this conversation. Also sends `/skill:do-poteto-mode`. | +| `/poteto-mode off` | Disable this omp conversation. Aliases: `disable`, `stop`. | -That is the live extension command. It stays unprefixed. There is no worktree command. No `hooks/` tree. No `src/` tree. No Cursor marketplace APIs. +That is the live omp extension command. It stays unprefixed. There is no worktree command. No `hooks/` tree. No `src/` tree. No Cursor marketplace APIs. Do not ship `commands/poteto-mode.md`. ## Poteto Mode (this extension) -- Enable sends `/skill:do-poteto-mode`. The `/skill:do-poteto-mode` input hook also persists enabled. +- On omp, enable sends `/skill:do-poteto-mode`. The `/skill:do-poteto-mode` input hook also persists enabled. - Stored as a custom `pstack-mode` entry on that conversation’s session jsonl. Last `{enabled}` wins. Missing means off. - `session_start` re-reads the jsonl. `new_session` / `/new` starts off. Resume of an on conversation stays on. - When on, a prompt needle prepends “Pstack Poteto Mode is on…” @@ -82,9 +100,12 @@ That is the live extension command. It stays unprefixed. There is no worktree co ## Skills -Files under `skills/` are markdown prompts. Invoke them with `/skill:do-`. They are live after `omp plugin link ./` (sibling tree auto-discovered). They are not live omp functions or CLIs. +Files under `skills/` are markdown prompts. They are not live omp functions or CLIs. + +- omp, after `omp plugin link ./`: `/skill:do-` +- Claude Code, after plugin install: `/pstack:do-` -Examples: `/skill:do-create-verification-skill`, `/skill:do-swarm`, `/skill:do-principle-build-the-lever`. Feature Map is a section in `do-create-verification-skill`, not a runner. Do not treat Feature Map, swarm, or Build the Lever as functions or CLIs. +Examples: `/skill:do-create-verification-skill` (Claude: `/pstack:do-create-verification-skill`), `/skill:do-swarm`, `/skill:do-principle-build-the-lever`. Feature Map is a section in `do-create-verification-skill`, not a runner. Do not treat Feature Map, swarm, or Build the Lever as functions or CLIs. ## License diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 037e0c0..abf6b78 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -2,7 +2,7 @@ ## Purpose -Human docs for this omp plugin. First-run is the README (`omp plugin link ./`). The numbered guide is the original pstack product tutorial. This file must not teach a second mount story. +Human docs for this plugin. First-run is the README (`omp plugin link ./` and Claude `claude plugin install pstack@pstack`). The numbered guide is the original pstack product tutorial. This file must not teach a second omp mount story (`omp -e`) or a fake installer. ## Ownership @@ -10,14 +10,14 @@ Human docs for this omp plugin. First-run is the README (`omp plugin link ./`). ## Local Contracts -- README owns first-run: `omp plugin link ./`. Do not add `docs/getting-started.md` or teach `omp -e` as an install path. +- README owns first-run for both hosts: `omp plugin link ./` and Claude plugin install (`marketplace add` + `pstack@pstack`, or `claude --plugin-dir ./`). Do not add `docs/getting-started.md` or teach `omp -e` as an install path. - README may link the numbered guide. -- Guide on-disk skill links use `skills/do-*` so they resolve in this tree. Slash names in guide prose may still say `/how`; the live surface is `/skill:do-how`. `/poteto-mode` stays unprefixed. -- Operator install matches the root rail Native omp contract. +- Guide on-disk skill links use `skills/do-*` so they resolve in this tree. Slash names in guide prose may still say `/how`; the live omp surface is `/skill:do-how`, Claude is `/pstack:do-how`. `/poteto-mode` stays unprefixed on omp. +- Operator install matches the root rail Native omp and Native Claude Code contracts. ## Work Guidance -Port guide pages and images from upstream. Remap `../../skills//` to `../../skills/do-/`. Agents tell operators plugin-link. +Port guide pages and images from upstream. Remap `../../skills//` to `../../skills/do-/`. Agents tell operators plugin-link (omp) or plugin install (Claude). ## Verification diff --git a/docs/guide/01-setup.md b/docs/guide/01-setup.md index 78fb6bb..ad0a167 100644 --- a/docs/guide/01-setup.md +++ b/docs/guide/01-setup.md @@ -4,15 +4,32 @@ In this page you install the plugin, pick which models pstack uses, and run your ## Install the plugin +This package is the plugin root for both hosts. Pick one. + +### omp / Pi + From the package root: ```text omp plugin link ./ ``` -Confirm with `omp plugin list`. +Confirm with `omp plugin list`. Prove with `omp plugin doctor`. + +The Cursor product used `/add-plugin pstack`. That is history, not how you install this package. omp marketplaces are catalogs you add (a git repo with `marketplace.json`); this package is local-link only on omp, not listed in an omp catalog. + +### Claude Code + +From the package root: + +```text +claude plugin marketplace add /absolute/path/to/this-checkout +claude plugin install pstack@pstack +``` + +Or this session only: `claude --plugin-dir ./`. Validate with `claude plugin validate .`. Skills show as `/pstack:do-*`. -The Cursor product used `/add-plugin pstack`. That is history, not how you install this omp package. omp marketplaces are catalogs you add (a git repo with `marketplace.json`); this package is local-link only, not listed in a catalog. +The Claude catalog is `.claude-plugin/marketplace.json`. Skills stay at the plugin root (`skills/do-*/SKILL.md`), not inside `.claude-plugin/`. ## Pick your models @@ -22,6 +39,8 @@ Run: /skill:do-setup-pstack ``` +On Claude Code the same skill is [`/pstack:do-setup-pstack`](../../skills/do-setup-pstack/SKILL.md). + [`/skill:do-setup-pstack`](../../skills/do-setup-pstack/SKILL.md) detects the models you have access to, shows you each role (code delegates, judgment, the review panels), and asks what you want. Answer the questions. It writes `~/.cursor/rules/pstack-models.mdc`, a small rule every pstack skill reads. You only override what you care about. A role with no line in the rule keeps the skill's default. To restore a default later, delete that role's line, or just run `/skill:do-setup-pstack` again. @@ -44,8 +63,10 @@ Pick something real but small, and describe it the way you'd describe it to a co /poteto-mode add a --json flag to this command. text output stays byte-identical. verify both. ``` +On Claude Code, start with `/pstack:do-poteto-mode` instead of the omp factory `/poteto-mode`. + Watch the todo list. The first item is always "read the Principles section". The rest are the matched playbook's steps copied in, the Feature playbook for this prompt. If `/poteto-mode` skips a step, the step stays in the list with `skip: `, so you can see what it chose not to do. -From here you can type normal follow-ups. `/poteto-mode` is sticky. It stays on for the conversation until you opt out by saying so. +From here you can type normal follow-ups. On omp, `/poteto-mode` is sticky. It stays on for the conversation until you opt out by saying so. Next: [Route work through `/poteto-mode`](./02-poteto-mode.md). diff --git a/docs/guide/README.md b/docs/guide/README.md index 3b3c0e2..0285a9a 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -1,6 +1,6 @@ # The pstack guide -> This numbered guide is the original pstack product tutorial. On this omp fork, first-run is the [README](../../README.md) (`omp plugin link ./`). Slash skills are `/skill:do-…` (on-disk `skills/do-*`). `/poteto-mode` stays unprefixed. +> This numbered guide is the original pstack product tutorial. On this fork, first-run is the [README](../../README.md) (`omp plugin link ./` or Claude `claude plugin install pstack@pstack`). Slash skills are `/skill:do-…` on omp and `/pstack:do-*` on Claude (on-disk `skills/do-*`). `/poteto-mode` stays unprefixed on omp. pstack works best when you stop micromanaging the agent. You describe what you want and how you'll know it's done. `/poteto-mode` picks the playbook, runs the other skills as the steps need them, and shows you the evidence. This guide teaches that habit with realistic prompts. diff --git a/e2e/AGENTS.md b/e2e/AGENTS.md index f5e42ec..5fb9588 100644 --- a/e2e/AGENTS.md +++ b/e2e/AGENTS.md @@ -2,28 +2,38 @@ ## Purpose -Prove the plugin without chat-only guesses. Static doctor/lint + Vitest on every PR. Live omp RPC is local-only and loads this tree with `omp plugin link ./`. +Prove the plugin without chat-only guesses. Static doctor/lint + Vitest on every PR. Live omp RPC is local-only and loads this tree with `omp plugin link ./`. Live `claude plugin validate` / `omp plugin doctor` are host-only. ## Ownership - `run.py` — live RPC evals; `--skip-rpc` is static-only -- `ci_static.py` — doctor, frontmatter, size, product pack +- `ci_static.py` — doctor, frontmatter, size, product pack, Claude plugin layout - `unit/` — Vitest helpers and tests - `ci-shims/` — `node:fs` stub for `tsc` without `@types/node` ## Local Contracts - CI and local static gate: `python3 e2e/run.py --skip-rpc` and `npm test`. -- No `omp --mode rpc` on GitHub runners. +- No `omp --mode rpc` on GitHub runners. No `claude` / `omp` install on CI. - Do not install or run live omp / the plugin extension as a test harness on CI. - Live RPC, when omp is present, must `omp plugin link ./` before starting. Do not pass `-e`. - Do not keep a TypeScript copy of `ci_static.py` (no `unit/static-checks.ts`, no homemade brace parser). - Do not keep a repo `.omp/skills` symlink. `static_install_docs` and `unit/product-capabilities.test.ts` assert filesystem absence, not only README text. +- Claude components stay at plugin root. `.claude-plugin/` may hold `plugin.json` and `marketplace.json` only — not `skills/`. ## Work Guidance Add static cases to `ci_static.py` or `unit/`. Product-pack presence (docs/guide, automations/benny, do-swarm) lives in `ci_static.py --product` and `unit/product-capabilities.test.ts`. Add live RPC cases to `run.py` only when they stay local. +Host-only prove (needs BigMac omp/claude, not CI): + +```bash +omp plugin link ./ +omp plugin list +omp plugin doctor +claude plugin validate . +``` + ## Verification ```bash diff --git a/e2e/README.md b/e2e/README.md index d5044a0..61c10a8 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -2,7 +2,7 @@ Static doctor/lint + Vitest on every PR. Live omp RPC evals are local-only. Not chat-only. -Static (CI and local; no omp): +Static (CI and local; no omp, no claude): ```bash python3 e2e/run.py --skip-rpc @@ -20,9 +20,18 @@ Each RPC run uses `--session-dir` under a temp folder and `--no-tools`. A factory change still needs an omp restart (a fresh `omp --mode rpc` process loads the tree). Do not run `omp --mode rpc` on GitHub runners. +Host-only prove (needs omp and/or Claude Code on the machine; not CI): + +```bash +omp plugin link ./ +omp plugin list +omp plugin doctor +claude plugin validate . +``` + | Case | What it proves | | --- | --- | -| `install_plugin_link` | README/AGENTS teach `omp plugin link ./`; competing stories gone | +| `install_plugin_link` | README/AGENTS teach `omp plugin link ./` and Claude plugin install; competing stories gone | | `poteto_on` | `/poteto-mode` injects the needle and writes `pstack-mode` on this session | | `second_turn_sticky` | Next turn still has the needle | | `poteto_off` | `/poteto-mode off` then next turn has no needle | diff --git a/e2e/ci_static.py b/e2e/ci_static.py index 42054cb..71c6c94 100755 --- a/e2e/ci_static.py +++ b/e2e/ci_static.py @@ -4,11 +4,14 @@ import argparse import json +import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] PSTACK_MAX_BYTES = 32 * 1024 +KEBAB = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") +CLAUDE_FORBIDDEN = ("skills", "agents", "commands", "hooks") def pstack_ts(root: Path) -> Path: @@ -87,11 +90,12 @@ def quality(root: Path | None = None) -> list[dict]: results.append(check("pstack_size", False, "extensions/pstack.ts missing")) else: raw = target.read_bytes() - size_ok = len(raw) <= PSTACK_MAX_BYTES and b"\0" not in raw + factory_text = "" try: - raw.decode("utf-8") + factory_text = raw.decode("utf-8") except UnicodeDecodeError: - size_ok = False + pass + size_ok = len(raw) <= PSTACK_MAX_BYTES and b"\0" not in raw and factory_text != "" results.append( check( "pstack_size", @@ -99,6 +103,127 @@ def quality(root: Path | None = None) -> list[dict]: f"{len(raw)} bytes (budget {PSTACK_MAX_BYTES})", ) ) + if factory_text: + results.append( + check( + "factory_default_export", + "export default function" in factory_text, + "default factory function", + ) + ) + results.append( + check( + "independent_of_zenspc", + "@zenspc/pi-pstack" not in factory_text, + "no @zenspc/pi-pstack import", + ) + ) + pi = pkg.get("pi") + if isinstance(pi, dict) and isinstance(pi.get("extensions"), list): + results.append( + check( + "pi_extensions", + pi.get("extensions") == declared, + "pi.extensions matches omp.extensions", + ) + ) + results.extend(claude_plugin(root)) + return results + + +def claude_plugin(root: Path) -> list[dict]: + """Official Claude Code plugin layout: manifest in .claude-plugin/, skills at plugin root.""" + results = [] + hidden = root / ".claude-plugin" + plugin_path = hidden / "plugin.json" + market_path = hidden / "marketplace.json" + if not hidden.is_dir(): + return [ + check("claude_plugin_dir", False, ".claude-plugin missing"), + check("claude_plugin_json", False, "plugin.json missing"), + check("claude_marketplace_json", False, "marketplace.json missing"), + check("claude_skills_at_root", False, "skills/ at plugin root"), + ] + nested = [name for name in CLAUDE_FORBIDDEN if (hidden / name).exists()] + results.append( + check( + "claude_plugin_dir", + not nested, + "no component dirs inside .claude-plugin" if not nested else f"nested {nested}", + ) + ) + if not plugin_path.is_file(): + results.append(check("claude_plugin_json", False, "plugin.json missing")) + else: + try: + manifest = json.loads(plugin_path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + results.append(check("claude_plugin_json", False, f"invalid JSON: {exc}")) + manifest = None + if isinstance(manifest, dict): + name = manifest.get("name") + name_ok = isinstance(name, str) and bool(KEBAB.fullmatch(name)) and name == "pstack" + results.append( + check( + "claude_plugin_json", + name_ok, + f"name={name!r}" if name_ok else "name must be kebab-case pstack", + ) + ) + pkg_path = root / "package.json" + pkg_ver = None + try: + pkg_ver = json.loads(pkg_path.read_text(encoding="utf-8")).get("version") + except (OSError, json.JSONDecodeError): + pkg_ver = None + plugin_ver = manifest.get("version") + if isinstance(pkg_ver, str) and isinstance(plugin_ver, str): + results.append( + check( + "claude_plugin_version", + plugin_ver == pkg_ver, + f"{plugin_ver} == package.json {pkg_ver}", + ) + ) + elif manifest is not None: + results.append(check("claude_plugin_json", False, "plugin.json must be an object")) + if not market_path.is_file(): + results.append(check("claude_marketplace_json", False, "marketplace.json missing")) + else: + try: + market = json.loads(market_path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + results.append(check("claude_marketplace_json", False, f"invalid JSON: {exc}")) + market = None + if isinstance(market, dict): + plugins = market.get("plugins") + sources = [entry.get("source") for entry in plugins if isinstance(entry, dict)] if isinstance(plugins, list) else [] + ok = ( + isinstance(market.get("name"), str) + and bool(KEBAB.fullmatch(str(market.get("name") or ""))) + and isinstance(market.get("description"), str) + and bool(str(market.get("description") or "").strip()) + and isinstance(market.get("owner"), dict) + and isinstance((market.get("owner") or {}).get("name"), str) + and isinstance(plugins, list) + and "./" in sources + ) + results.append( + check( + "claude_marketplace_json", + ok, + "name + description + owner + plugins source ./" if ok else "marketplace catalog incomplete", + ) + ) + elif market is not None: + results.append(check("claude_marketplace_json", False, "marketplace.json must be an object")) + results.append( + check( + "claude_skills_at_root", + (root / "skills").is_dir() and not (hidden / "skills").exists(), + "skills/ at plugin root", + ) + ) return results diff --git a/e2e/run.py b/e2e/run.py index 525fa2b..643aecd 100755 --- a/e2e/run.py +++ b/e2e/run.py @@ -7,7 +7,7 @@ poteto_off /poteto-mode off removes the needle new_session_starts_off same-process new_session: new sid, no entry, next turn off resume_stays_on switch_session back to the on-session: needle on - install_plugin_link README/AGENTS teach omp plugin link ./; competing stories gone + install_plugin_link README/AGENTS teach omp plugin link ./ and Claude plugin install; competing stories gone worktree_cleanup_omp playbook uses ~/.omp/wt and worktree.base Usage (from repo root): @@ -232,9 +232,17 @@ def static_setup_docs(): def static_install_docs(): readme = (ROOT / "README.md").read_text() agents = (ROOT / "AGENTS.md").read_text() + setup = (ROOT / "docs/guide/01-setup.md").read_text() ok = ( "omp plugin link ./" in readme and "omp plugin link ./" in agents + and "claude plugin marketplace add" in readme + and "claude plugin install pstack@pstack" in readme + and "claude plugin install pstack@pstack" in agents + and "claude --plugin-dir ./" in readme + and "claude plugin validate" in readme + and "omp plugin link ./" in setup + and "claude plugin marketplace add" in setup and "omp -e" not in readme and "/add-plugin" not in readme and ".omp/skills" not in readme @@ -244,13 +252,16 @@ def static_install_docs(): and not (ROOT / "commands/setup-pstack.md").exists() and not (ROOT / "commands/poteto-mode.md").exists() and not (ROOT / ".omp-plugin/marketplace.json").exists() + and (ROOT / ".claude-plugin/plugin.json").is_file() + and (ROOT / ".claude-plugin/marketplace.json").is_file() + and not (ROOT / ".claude-plugin/skills").exists() and not (ROOT / "e2e/unit/static-checks.ts").exists() and not (ROOT / "skills/do-poteto-mode/playbooks/shipping.md").exists() ) return check( "install_plugin_link", ok, - "canonical install is omp plugin link ./; competing stories gone", + "canonical installs are omp plugin link ./ and Claude plugin install; competing stories gone", ) diff --git a/e2e/unit/ci-static-cli.test.ts b/e2e/unit/ci-static-cli.test.ts index 3fb8bca..00a0f93 100644 --- a/e2e/unit/ci-static-cli.test.ts +++ b/e2e/unit/ci-static-cli.test.ts @@ -44,6 +44,18 @@ describe("ci_static.py CLI fixtures", () => { expect(summary.failed).toContain("pstack_size"); }); + it("fails quality without Claude plugin.json", () => { + const root = fixture({ + "package.json": JSON.stringify({ omp: { extensions: ["./extensions/pstack.ts"] } }), + "extensions/pstack.ts": "export default function pstack() {}\n", + "skills/.keep": "", + "agents/.keep": "", + }); + const { status, summary } = runCiStatic(["--quality"], root); + expect(status).toBe(1); + expect(summary.failed).toContain("claude_plugin_json"); + }); + it("fails product when docs/guide is missing", () => { const root = fixture({ "docs/README.md": "# docs\n", diff --git a/e2e/unit/doctor.test.ts b/e2e/unit/doctor.test.ts index 7f2b5ca..eb0d4fc 100644 --- a/e2e/unit/doctor.test.ts +++ b/e2e/unit/doctor.test.ts @@ -19,20 +19,41 @@ function walkText(root: string, rel = ""): string[] { } describe("package.json doctor", () => { - it("plugin layout is real: omp.extensions path and skills/do-*/SKILL.md", () => { + it("plugin layout is real: omp.extensions path, Claude plugin.json, and skills/do-*/SKILL.md", () => { const root = repoDir(); const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { omp?: { extensions?: unknown }; + pi?: { extensions?: unknown }; }; const exts = pkg.omp?.extensions; expect(Array.isArray(exts) && exts.length > 0).toBe(true); for (const rel of exts as string[]) { expect(existsSync(join(root, rel)), rel).toBe(true); } + expect(pkg.pi?.extensions).toEqual(exts); const skillMd = readdirSync(join(root, "skills")).filter( (name) => name.startsWith("do-") && existsSync(join(root, "skills", name, "SKILL.md")), ); expect(skillMd.length).toBeGreaterThan(0); + + const plugin = JSON.parse(readFileSync(join(root, ".claude-plugin/plugin.json"), "utf8")) as { + name?: unknown; + }; + const market = JSON.parse(readFileSync(join(root, ".claude-plugin/marketplace.json"), "utf8")) as { + name?: unknown; + owner?: { name?: unknown }; + plugins?: { name?: unknown; source?: unknown }[]; + }; + expect(plugin.name).toBe("pstack"); + expect(market.name).toBe("pstack"); + expect(market.owner?.name).toBeTruthy(); + expect(market.plugins?.some((entry) => entry.source === "./" && entry.name === "pstack")).toBe(true); + expect(existsSync(join(root, ".claude-plugin/skills"))).toBe(false); + expect(existsSync(join(root, "skills"))).toBe(true); + + const factory = readFileSync(join(root, "extensions/pstack.ts"), "utf8"); + expect(factory).toContain("export default function pstackExtension"); + expect(factory).not.toContain("@zenspc/pi-pstack"); }); it("live slash is /skill:do-* and leftover ps- skill folders/slashes are gone", () => { diff --git a/e2e/unit/product-capabilities.test.ts b/e2e/unit/product-capabilities.test.ts index bf140ce..e7be572 100644 --- a/e2e/unit/product-capabilities.test.ts +++ b/e2e/unit/product-capabilities.test.ts @@ -97,11 +97,16 @@ describe("original product pack", () => { expect(hits.length, "README.md must link to ./docs/guide/README.md").toBeGreaterThan(0); }); - it("README teaches omp plugin link ./ and drops competing install stories", () => { + it("README teaches omp plugin link ./ and Claude plugin install; competing stories gone", () => { const readme = readFileSync(join(root, "README.md"), "utf8"); const agents = readFileSync(join(root, "AGENTS.md"), "utf8"); expect(readme).toContain("omp plugin link ./"); expect(agents).toContain("omp plugin link ./"); + expect(readme).toContain("claude plugin marketplace add"); + expect(readme).toContain("claude plugin install pstack@pstack"); + expect(agents).toContain("claude plugin install pstack@pstack"); + expect(readme).toContain("claude --plugin-dir ./"); + expect(readme).toContain("/pstack:do-"); expect(readme).not.toMatch(/omp -e /); expect(readme).not.toContain("/add-plugin"); expect(readme).not.toContain(".omp/skills"); @@ -111,6 +116,9 @@ describe("original product pack", () => { expect(existsSync(join(root, "commands/setup-pstack.md"))).toBe(false); expect(existsSync(join(root, "commands/poteto-mode.md"))).toBe(false); expect(existsSync(join(root, ".omp-plugin/marketplace.json"))).toBe(false); + expect(existsSync(join(root, ".claude-plugin/plugin.json"))).toBe(true); + expect(existsSync(join(root, ".claude-plugin/marketplace.json"))).toBe(true); + expect(existsSync(join(root, ".claude-plugin/skills"))).toBe(false); expect(existsSync(join(root, "e2e/unit/static-checks.ts"))).toBe(false); expect(existsSync(join(root, "skills/do-poteto-mode/playbooks/shipping.md"))).toBe(false); }); diff --git a/extensions/AGENTS.md b/extensions/AGENTS.md index b73dff4..a3d2995 100644 --- a/extensions/AGENTS.md +++ b/extensions/AGENTS.md @@ -13,7 +13,7 @@ The one omp factory. Sticky `/poteto-mode` for a single conversation. - Mode is per-session (`pstack-mode` on that session's jsonl / session id). Missing entry means off. - Never a process-wide boolean. - Sticky `/poteto-mode` is factory `registerCommand("poteto-mode")` only. Do not ship `commands/poteto-mode.md`. -- `package.json` `omp.extensions` points here. +- `package.json` `omp.extensions` points here. Keep legacy `pi.extensions` as the same path list. ## Work Guidance @@ -24,6 +24,7 @@ Restart omp after a factory change. Do not add a second extension unless the roo - `npx tsc --noEmit -p tsconfig.ci.json` - `python3 e2e/ci_static.py --quality` (includes factory size) - Live sticky behavior: `python3 e2e/run.py` locally (not CI); launcher uses `omp plugin link ./` +- Host-only: `omp plugin list` / `omp plugin doctor` after link. Claude does not load this factory. ## Child DOX Index diff --git a/extensions/pstack.ts b/extensions/pstack.ts index bc64db2..67227a6 100644 --- a/extensions/pstack.ts +++ b/extensions/pstack.ts @@ -70,6 +70,12 @@ function isPotetoOn(ctx: any, cache: Map): boolean { } export default function pstackExtension(pi: any): void { + try { + pi.setLabel?.("pstack"); + } catch { + // older hosts + } + const modeBySession = new Map(); function setStatus(ctx: any, on: boolean): void { diff --git a/package.json b/package.json index 7372ef9..a69d77b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pstack", "version": "0.14.5", - "description": "pstack for omp: one extension (sticky /poteto-mode), skills, agents.", + "description": "pstack plugin for omp/Pi and Claude Code: sticky /poteto-mode, skills, agents.", "private": true, "type": "module", "scripts": { diff --git a/skills/AGENTS.md b/skills/AGENTS.md index 8c061f2..7ad5e83 100644 --- a/skills/AGENTS.md +++ b/skills/AGENTS.md @@ -2,7 +2,7 @@ ## Purpose -pstack skill tree. Sibling `skills/` at the package root is what omp auto-discovers after `omp plugin link ./`. Do not add a second skills tree. +pstack skill tree. Sibling `skills/` at the package root is what omp auto-discovers after `omp plugin link ./` and what Claude Code loads from the plugin root after install. Do not add a second skills tree. Do not nest skills inside `.claude-plugin/`. ## Ownership @@ -10,7 +10,7 @@ pstack skill tree. Sibling `skills/` at the package root is what omp auto-discov ## Local Contracts -- Skill directories and YAML `name` use the `do-` prefix. After `omp plugin link ./`, the live slash is `/skill:do-`. +- Skill directories and YAML `name` use the `do-` prefix. After `omp plugin link ./`, the live slash is `/skill:do-`. After Claude plugin install, the live slash is `/pstack:do-`. Do not rename the `do-` set. - Every `SKILL.md` has YAML frontmatter with non-empty `name` and `description`. - `python3 e2e/ci_static.py --frontmatter` is the gate.