diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 38ad89f..b882463 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -40,7 +40,7 @@ "name": "guardrails", "source": "./plugins/guardrails", "description": "Mod: /careful asks before destructive shell commands; /freeze blocks edits outside a folder. Enforced by hooks.", - "version": "0.1.0", + "version": "0.1.1", "category": "mod" }, { @@ -54,14 +54,14 @@ "name": "po-skills", "source": "./plugins/po-skills", "description": "Skills: brainstorming (with YC forcing questions) and writing-plans (with decision briefs and ponytail debt tracking), plus jira-stories and release-notes.", - "version": "0.3.0", + "version": "0.3.1", "category": "skills" }, { "name": "eng-skills", "source": "./plugins/eng-skills", "description": "Skills: investigate (root cause debugging, 3-strike rule), qa (exploratory verification with mandatory regression tests), ship (pre-flight checks, doc sync, PR).", - "version": "0.2.0", + "version": "0.2.1", "category": "skills" }, { diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ca79ca5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0fc00a9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,126 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + name: Repo consistency + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 22 + - run: node scripts/check.mjs + + shell: + name: ShellCheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - run: shellcheck setup.sh scripts/*.sh plugins/po-skills/skills/brainstorming/scripts/*.sh + + powershell: + name: PowerShell lint + runs-on: windows-latest + steps: + - uses: actions/checkout@v7 + - name: Parse setup.ps1 + shell: pwsh + run: | + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile("$PWD\setup.ps1", [ref]$null, [ref]$errors) | Out-Null + if ($errors) { $errors | ForEach-Object { Write-Error $_ }; exit 1 } + - name: PSScriptAnalyzer + shell: pwsh + run: | + Install-Module PSScriptAnalyzer -Force -Scope CurrentUser + $issues = Invoke-ScriptAnalyzer -Path setup.ps1 -Severity Error + $issues | Format-Table -AutoSize + if ($issues) { exit 1 } + + plugins: + name: Validate and test plugins + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 22 + - run: npm install -g @anthropic-ai/claude-code + - name: Validate marketplace and plugins + run: | + claude plugin validate . + for p in plugins/*/; do claude plugin validate "$p"; done + - name: Test mods + run: | + for p in plugins/*/; do + if [ -d "$p/tests" ]; then echo "::group::$p"; claude plugin test "$p"; echo "::endgroup::"; fi + done + + skills-portable: + name: Skills install for other agents + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 22 + - name: Install every skill for Codex and Gemini CLI + run: | + expected=$(find plugins/*/skills -mindepth 2 -maxdepth 2 -name SKILL.md | wc -l) + work=$(mktemp -d) + (cd "$work" && git init -q && npx -y skills add "$GITHUB_WORKSPACE" -a codex -a gemini-cli --skill '*' --copy -y) + got=$(find "$work" -name SKILL.md | sort -u | xargs -n1 dirname | xargs -n1 basename | sort -u | wc -l) + echo "expected $expected skills, installed $got" + test "$expected" -eq "$got" + + install-smoke: + name: Install (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 22 + - run: npm install -g @anthropic-ai/claude-code + - name: setup.sh --agents codex + if: runner.os != 'Windows' + run: ./setup.sh --agents codex + - name: setup.ps1 -Agents codex + if: runner.os == 'Windows' + shell: pwsh + run: .\setup.ps1 -Agents codex + - name: Every lippstack plugin is installed for Claude Code + shell: bash + run: | + claude plugin list > installed.txt + node -e ' + const fs = require("fs") + const list = fs.readFileSync("installed.txt", "utf8") + const names = JSON.parse(fs.readFileSync(".claude-plugin/marketplace.json", "utf8")).plugins.map(p => p.name) + const missing = names.filter(n => !list.includes(`${n}@lippstack`)) + if (missing.length) { console.error("not installed:", missing.join(", ")); process.exit(1) } + console.log(`all ${names.length} lippstack plugins installed`) + ' + - name: Skills were installed for Codex + shell: bash + run: | + n=$(find "$HOME/.agents/skills" -name SKILL.md | wc -l) + echo "$n skills in ~/.agents/skills" + test "$n" -ge 30 diff --git a/.github/workflows/vendor-drift.yml b/.github/workflows/vendor-drift.yml new file mode 100644 index 0000000..01bcca4 --- /dev/null +++ b/.github/workflows/vendor-drift.yml @@ -0,0 +1,38 @@ +name: Vendor drift + +on: + schedule: + - cron: '0 9 * * 1' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Sync vendored design skills + id: sync + run: | + { + echo 'log<> "$GITHUB_OUTPUT" + - uses: peter-evans/create-pull-request@v8 + with: + branch: chore/sync-vendor + title: Sync vendored design skills + commit-message: Sync vendored design skills from upstream + body: | + `scripts/sync-vendor.sh` found upstream changes. + + ``` + ${{ steps.sync.outputs.log }} + ``` + + Before merging: update the commits in `plugins/design-skills/README.md` and bump `design-skills` in `plugin.json` and `.claude-plugin/marketplace.json` (CI checks they match). + delete-branch: true diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8652a61 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# AGENTS.md + +Instructions for any coding agent (Claude Code, Codex, Gemini CLI, Kimi, OpenCode, Cursor…) working on this repo. `CLAUDE.md` and `GEMINI.md` point here. + +## What this repo is + +A Claude Code plugin marketplace (`.claude-plugin/marketplace.json`) whose skills also install into other agents through the `npx skills` CLI. + +``` +plugins//.claude-plugin/plugin.json manifest of each plugin +plugins//skills//SKILL.md Agent Skills: portable to any agent +plugins//hooks/register.ts(x) mods: Claude Code hooks, not portable +plugins//tests/*.test.ts mod tests (claude plugin test) +plugins/design-skills/ vendored by scripts/sync-vendor.sh; don't edit by hand +setup.sh / setup.ps1 installers (macOS+Linux / Windows) +scripts/check.mjs consistency checks CI runs +``` + +## Rules + +- **Versions move together.** Any change under `plugins//` bumps its `version` in both `plugins//.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json`. Patch for wording and fixes, minor for new skills or behaviour. +- **A new plugin** goes in `marketplace.json`, `setup.sh` (`LIPPSTACK_PLUGINS`), `setup.ps1` (`$lippPlugins`), and the README: its table and the "Or by hand" block. `scripts/check.mjs` fails if one is missing. +- **Skills stay agent-neutral.** `SKILL.md` needs `name` (equal to its folder; lowercase, digits, hyphens; ≤ 64) and `description` (≤ 1024 chars). Don't depend on one agent's tools without a fallback: subagents → "or do it yourself in a fresh pass"; ponytail → "if installed"; Jira → "an Atlassian MCP server". `disable-model-invocation: true` only works in Claude Code, so also say in the `description` that the skill runs only when explicitly asked. +- **Mods** are Claude Code only (TypeScript against the `claude-code` hooks API). Every mod has tests. +- **Vendored skills** (`plugins/design-skills/skills/*`) are refreshed by `scripts/sync-vendor.sh`, which overwrites local edits. A weekly workflow opens a PR when upstream changes. +- **Installers** must keep working on macOS (bash 3.2), Linux and Windows PowerShell 5.1+. In `setup.ps1`, run native commands through `Invoke-Native`/`Invoke-Claude` so a failure stops the script. +- Write README and skill prose in plain English, short sentences, no marketing. + +## Verify before committing + +```bash +node scripts/check.mjs # manifests, versions, skill frontmatter, installers, links +claude plugin validate . # marketplace; also each plugins/ +claude plugin test plugins/ # for each mod you touched +bash -n setup.sh # and shellcheck setup.sh if you have it +npx -y skills add . --list # every skill is still discoverable for other agents +``` + +CI (`.github/workflows/ci.yml`) runs all of this plus a real install of `setup.sh` / `setup.ps1` on Ubuntu, macOS and Windows. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md index 77e16d9..ed6db66 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # lippstack -Mods and skills for Claude Code, tuned to how we work. +[![CI](https://github.com/lippdev/lippstack/actions/workflows/ci.yml/badge.svg)](https://github.com/lippdev/lippstack/actions/workflows/ci.yml) + +Mods and skills for Claude Code, tuned to how we work. The skills also run in Codex, Gemini CLI, Kimi, OpenCode, Cursor and other agents: see [Other agents](#other-agents-codex-gemini-cli-kimi-opencode-cursor). By [Filipe Moreira](https://filipemoreira.top). @@ -92,6 +94,37 @@ claude plugin install ponytail@lippstack Then restart Claude Code, or run `/reload-plugins` in an open session. +### Other agents (Codex, Gemini CLI, Kimi, OpenCode, Cursor…) + +The skills follow the open [Agent Skills](https://agentskills.io) format, so any agent that reads `SKILL.md` can use them. The mods can't: they run on Claude Code's own hooks. + +| | Claude Code | Other agents | +| :- | :- | :- | +| Skills: `design-skills`, `po-skills`, `eng-skills`, ponytail, PM skills | ✅ | ✅ | +| Mods: `savvy-progress`, `stage-bar`, `done-ping`, `where-line`, `guardrails` | ✅ | ❌ | +| Official Anthropic plugins, `cc-plugin-you-should-know` | ✅ | ❌ | + +From a clone, add `--agents` with the agent ids. Claude Code is installed too when its CLI is present, and skipped when it isn't: + +```bash +./setup.sh --agents codex,gemini-cli,kimi-code-cli +``` + +```powershell +powershell -ExecutionPolicy Bypass -File .\setup.ps1 -Agents codex,gemini-cli,kimi-code-cli +``` + +Or without a clone, with the [skills CLI](https://github.com/vercel-labs/skills) (needs Node.js): + +```bash +npx skills add lippdev/lippstack -g # pick agents and skills interactively +npx skills add lippdev/lippstack -g -a codex -a opencode --skill '*' -y +npx skills add DietrichGebert/ponytail -g --skill '*' +npx skills add deanpeters/Product-Manager-Skills -g -s user-story -s prd-development # pick the PM skills you want +``` + +Common ids: `codex`, `gemini-cli`, `kimi-code-cli`, `opencode`, `cursor`, `github-copilot`, `windsurf`, `antigravity`; `'*'` targets every agent the CLI knows. A few things differ outside Claude Code: `/skill-name` may be spelled differently (Kimi uses `/skill:name`), skills that offer subagents fall back to doing the work in a fresh pass, and `jira-stories` / `release-notes` need an Atlassian MCP server in that agent. + ## How to use Skills load on their own when a request matches their description. To make sure one runs, name it in the request ("use user-story to…") or type `/` and pick it from the list; plugin skills show up as `plugin:skill`, for example `/po-skills:brainstorming` or `/eng-skills:investigate`. diff --git a/plugins/eng-skills/.claude-plugin/plugin.json b/plugins/eng-skills/.claude-plugin/plugin.json index 583ba4e..c1e0928 100644 --- a/plugins/eng-skills/.claude-plugin/plugin.json +++ b/plugins/eng-skills/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "eng-skills", - "version": "0.2.0", + "version": "0.2.1", "description": "Engineering, QA and delivery skills: investigate (systematic debugging with 3-strike rule), qa (exploratory verification with mandatory regression tests), and ship (pre-flight checks, doc sync, and PR creation).", "author": { "name": "lippstack" diff --git a/plugins/eng-skills/skills/investigate/SKILL.md b/plugins/eng-skills/skills/investigate/SKILL.md index f310424..3602843 100644 --- a/plugins/eng-skills/skills/investigate/SKILL.md +++ b/plugins/eng-skills/skills/investigate/SKILL.md @@ -24,7 +24,7 @@ Follow this methodology strictly whenever diagnosing or fixing a bug. Before touching anything: - Identify the exact component, module, or files involved in the failure. - Mentally **freeze** the rest of the codebase: do NOT edit or refactor unrelated files. -- If `/freeze` is available, lock edits to the relevant directory: +- If `/freeze` is available (the lippstack guardrails mod on Claude Code), lock edits to the relevant directory: ```bash /freeze ``` @@ -79,7 +79,7 @@ Follow the 3-strike rule: Once the root cause is proven: 1. **Write the regression test first:** Add an automated test that reproduces the bug. Run it and verify it **FAILS** for the identified root cause. -2. **Apply the minimal fix:** Use `ponytail` style — the simplest code that does the job. Avoid refactoring unrelated abstractions. +2. **Apply the minimal fix:** The simplest code that does the job (`ponytail` style, if installed). Avoid refactoring unrelated abstractions. 3. **Verify the test passes:** Run the regression test and confirm it now **PASSES**. 4. **Run the full test suite:** Ensure no other tests were broken by the fix. 5. **Clean up:** Remove any temporary debug logs or prints. diff --git a/plugins/eng-skills/skills/ship/SKILL.md b/plugins/eng-skills/skills/ship/SKILL.md index 2ef4e41..68218af 100644 --- a/plugins/eng-skills/skills/ship/SKILL.md +++ b/plugins/eng-skills/skills/ship/SKILL.md @@ -1,6 +1,6 @@ --- name: ship -description: Pre-flight release workflow. Verifies clean git status, runs test suites, checks documentation synchronization (Diataxis sanity check), creates atomic commit, pushes branch, and opens a Pull Request. +description: Pre-flight release workflow. Verifies clean git status, runs test suites, checks documentation synchronization (Diataxis sanity check), creates atomic commit, pushes branch, and opens a Pull Request. Only runs when the user explicitly asks to ship; never start it on your own, since it pushes. disable-model-invocation: true argument-hint: "[optional PR title or target branch]" --- @@ -57,7 +57,7 @@ Craft an atomic, conventional commit: - Provide a clear bulleted list in the commit body: - What changed. - Why the change was made. - - Any architectural trade-offs or `shortcut:` annotations left for `ponytail-debt`. + - Any architectural trade-offs or `shortcut:` annotations (listed by `/ponytail-debt` if ponytail is installed). --- @@ -84,4 +84,4 @@ Craft an atomic, conventional commit: ## 6. Next Steps - If working with Jira/Confluence: suggest running `release-notes` to update the release notes on Confluence. -- If running under review: suggest running `/ponytail-review` to double-check the final diff. +- If ponytail is installed: suggest `/ponytail-review` to double-check the final diff; otherwise offer a fresh review of the diff. diff --git a/plugins/guardrails/.claude-plugin/plugin.json b/plugins/guardrails/.claude-plugin/plugin.json index 74a7cff..2148387 100644 --- a/plugins/guardrails/.claude-plugin/plugin.json +++ b/plugins/guardrails/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "guardrails", - "version": "0.1.0", + "version": "0.1.1", "description": "Real guardrails, enforced by hooks: /careful asks you before a destructive shell command runs; /freeze blocks edits outside a folder.", "author": { "name": "lippstack" }, "types": "./types/index.d.ts" diff --git a/plugins/guardrails/hooks/register.ts b/plugins/guardrails/hooks/register.ts index 9ab15df..492d9b3 100644 --- a/plugins/guardrails/hooks/register.ts +++ b/plugins/guardrails/hooks/register.ts @@ -49,10 +49,12 @@ async function outsideFreeze($: Engine, cwd: string, path: string): Promise<{ de export const register: Register = on => { let cwd = '/' + let interactive = true on('session.start', async ($, e, next) => { const started = await next(e) cwd = e.cwd + interactive = e.isInteractive await $.command.register({ name: 'careful', description: 'Ask before destructive shell commands: on, off, or status (default on)' }) await $.command.register({ name: 'freeze', description: 'Confine file edits to a folder: /freeze , /freeze off, or /freeze for status' }) return started @@ -61,6 +63,8 @@ export const register: Register = on => { on('tool.call', { tool: 'Bash' }, async ($, e, next) => { const risk = riskOf(e.command) if (!risk || !(await read($, careful))) return next(e) + // Hosts without a prompt (SDK, T3 Code, CI) fail the whole turn on a question, so refuse instead. + if (!interactive) return { deny: `guardrails: this command ${risk}, and this session cannot ask the user. Use a safer alternative, or ask the user in chat to run it or turn /careful off.` } const shown = e.command.length > 160 ? `${e.command.slice(0, 159)}…` : e.command const answer = await $.ui.ask(`This command ${risk}: \`${shown}\`. Run it?`, { header: 'careful', options: [RUN, 'Block it'] }).catch(() => 'Block it') if (answer === RUN) return next(e) diff --git a/plugins/guardrails/tests/guardrails.test.ts b/plugins/guardrails/tests/guardrails.test.ts index a3104a5..c09397f 100644 --- a/plugins/guardrails/tests/guardrails.test.ts +++ b/plugins/guardrails/tests/guardrails.test.ts @@ -33,6 +33,14 @@ test('careful asks, and a block keeps the command from running', async ($, on) = expect(ran.deny).toContain('blocked') }) +test('careful refuses without asking when the session is not interactive', async ($, on) => { + const asked = engine(on, 'Run it') + await $.session.start({ ...start, isInteractive: false }) + expect((await $.tool.call({ tool: 'Bash', command: 'rm -rf build' })).deny).toContain('cannot ask') + expect((await $.tool.call({ tool: 'Bash', command: 'ls' })).deny).toBeUndefined() + expect(asked.length).toBe(0) +}) + test('careful lets the command run once approved, and /careful off stops asking', async ($, on) => { const asked = engine(on, 'Run it') await $.session.start(start) diff --git a/plugins/po-skills/.claude-plugin/plugin.json b/plugins/po-skills/.claude-plugin/plugin.json index cb23207..c2bbefd 100644 --- a/plugins/po-skills/.claude-plugin/plugin.json +++ b/plugins/po-skills/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "po-skills", - "version": "0.3.0", + "version": "0.3.1", "description": "Product owner skills: brainstorming (with YC forcing questions) and writing-plans (with decision briefs and ponytail debt tracking) take an idea to an approved design and plan; jira-stories and release-notes put the work in Jira and Confluence.", "author": { "name": "lippstack" diff --git a/plugins/po-skills/skills/jira-stories/SKILL.md b/plugins/po-skills/skills/jira-stories/SKILL.md index dca5172..cb6b76b 100644 --- a/plugins/po-skills/skills/jira-stories/SKILL.md +++ b/plugins/po-skills/skills/jira-stories/SKILL.md @@ -8,7 +8,7 @@ argument-hint: "[epic key, feature or PRD]" Write the stories, show them, and create them in Jira only after the user says yes. The draft is cheap; cards in someone's backlog are not. -Uses the Atlassian connector (tools such as `getAccessibleAtlassianResources`, `createJiraIssue`, `searchJiraIssuesUsingJql`). If it is not connected, stop and say so; still offer the stories as text. +Uses an Atlassian/Jira MCP server or connector (tools such as `getAccessibleAtlassianResources`, `createJiraIssue`, `searchJiraIssuesUsingJql`). If it is not connected, stop and say so; still offer the stories as text. ## 1. Gather diff --git a/plugins/po-skills/skills/release-notes/SKILL.md b/plugins/po-skills/skills/release-notes/SKILL.md index db6d743..fb785c6 100644 --- a/plugins/po-skills/skills/release-notes/SKILL.md +++ b/plugins/po-skills/skills/release-notes/SKILL.md @@ -8,7 +8,7 @@ argument-hint: "[project + sprint, version or dates]" Turn finished Jira work into notes people read. Everything in the notes must come from the issues; when an issue does not say why it matters, say less rather than invent it. -Uses the Atlassian connector (Jira search, Confluence pages). If it is not connected, stop and say so. +Uses an Atlassian/Jira MCP server or connector (Jira search, Confluence pages). If it is not connected, stop and say so. ## 1. Find the work diff --git a/plugins/po-skills/skills/writing-plans/SKILL.md b/plugins/po-skills/skills/writing-plans/SKILL.md index 0fdbd49..0dc8fd8 100644 --- a/plugins/po-skills/skills/writing-plans/SKILL.md +++ b/plugins/po-skills/skills/writing-plans/SKILL.md @@ -155,7 +155,7 @@ If a chosen task or step takes an intentional shortcut (Completeness ≤ 7) for ```ts // shortcut: in-memory state only, upgrade to Redis when multi-instance is needed ``` -- This directly links into ponytail's debt ledger (`/ponytail-debt`) so trade-offs are transparent and tracked. +- With ponytail installed, `/ponytail-debt` lists these as a debt ledger; without it, `grep -rn "shortcut:"` does the same. ## What a Step Contains @@ -220,3 +220,5 @@ them to review the plan and choose an execution method before implementation. **If Subagent-driven chosen:** dispatch one fresh subagent per task with the task text and the files it names; after each, have a fresh subagent review that task's diff against the plan before starting the next. End with one review of the whole branch. **If Native chosen:** work through the tasks yourself in order, running each task's tests before checking it off. End with one fresh subagent reviewing the whole branch against the plan. + +**Without a subagent tool:** offer Native only, and do the final review yourself in a separate pass that rereads the plan and the whole diff from scratch. diff --git a/plugins/savvy-progress/README.md b/plugins/savvy-progress/README.md index e03fe54..522d79b 100644 --- a/plugins/savvy-progress/README.md +++ b/plugins/savvy-progress/README.md @@ -1,6 +1,6 @@ # savvy-progress -A Claude Code mod: a progress bar above the prompt and a live panel of subagents. Made for the [savvy-flow](../savvy-flow) skill, and useful with any subagents. +A Claude Code mod: a progress bar above the prompt and a live panel of subagents. Made for the [savvy-flow](https://github.com/johnnyvizz/claude-kit/tree/main/plugins/savvy-flow) skill, and useful with any subagents. - **Progress bar**: the flow's title, phase, accepted tasks out of planned, and a button with the crew size that opens the panel. It appears once something reports progress (savvy-flow does) or a `savvy-*` worker starts. - **Agents panel** (`/agents-info` toggles it): running, finished and planned subagents with model, effort, task progress, context, estimated cost and time. Working crabs walk, and each savvy tier animates its prop: the astronaut floats, the detective sweeps the magnifier, the engineer turns the wrench, the chef tosses the omelette, the racer runs with a fluttering flag. `prefers-reduced-motion` stops them. diff --git a/scripts/check.mjs b/scripts/check.mjs new file mode 100644 index 0000000..5d938e5 --- /dev/null +++ b/scripts/check.mjs @@ -0,0 +1,109 @@ +#!/usr/bin/env node +// Repo consistency checks, no dependencies: node scripts/check.mjs +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs' +import { dirname, join, relative, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const errors = [] +const warnings = [] +const rel = p => relative(root, p) +const read = p => readFileSync(join(root, p), 'utf8') + +// Copied from upstream by scripts/sync-vendor.sh: report, don't fail. +const VENDORED = 'plugins/design-skills/' + +function walk(dir, out = []) { + for (const name of readdirSync(dir)) { + if (name === '.git' || name === 'node_modules') continue + const p = join(dir, name) + if (statSync(p).isDirectory()) walk(p, out) + else out.push(p) + } + return out +} +const files = walk(root) + +// 1. Every JSON file parses. +for (const f of files.filter(f => f.endsWith('.json'))) { + try { + JSON.parse(readFileSync(f, 'utf8')) + } catch (e) { + errors.push(`${rel(f)}: invalid JSON (${e.message})`) + } +} + +// 2. Local marketplace plugins exist and their versions match plugin.json. +const marketplace = JSON.parse(read('.claude-plugin/marketplace.json')) +const local = marketplace.plugins.filter(p => typeof p.source === 'string') +for (const p of local) { + const manifest = join(p.source, '.claude-plugin/plugin.json') + if (!existsSync(join(root, manifest))) { + errors.push(`marketplace.json: ${p.name} points at ${p.source}, which has no ${manifest}`) + continue + } + const own = JSON.parse(read(manifest)) + if (own.name !== p.name) errors.push(`${manifest}: name "${own.name}" but marketplace.json lists "${p.name}"`) + if (own.version !== p.version) errors.push(`${p.name}: version ${p.version} in marketplace.json but ${own.version} in ${manifest}; bump both`) +} + +// 3. SKILL.md frontmatter follows the Agent Skills spec, so every agent loads it. +function frontmatter(text) { + const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text) + if (!m) return undefined + const fields = {} + for (const line of m[1].split(/\r?\n/)) { + const kv = /^([\w-]+):\s*(.*)$/.exec(line) + if (kv) fields[kv[1]] = kv[2].replace(/^(["'])(.*)\1$/, '$2') + } + return fields +} +const skills = files.filter(f => f.endsWith('/SKILL.md') || f.endsWith('\\SKILL.md')) +for (const f of skills) { + const where = rel(f) + const folder = dirname(f).split(/[\\/]/).pop() + const fm = frontmatter(readFileSync(f, 'utf8')) + if (!fm) { + errors.push(`${where}: no frontmatter`) + continue + } + if (fm.name !== folder) errors.push(`${where}: name "${fm.name}" must match its folder "${folder}"`) + if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(fm.name ?? '') || fm.name.length > 64) + errors.push(`${where}: name must be lowercase letters, digits and hyphens, at most 64 characters`) + if (!fm.description) errors.push(`${where}: description is missing`) + else if (fm.description.length > 1024) errors.push(`${where}: description is ${fm.description.length} characters, the limit is 1024`) + // Only Claude Code reads disable-model-invocation; other agents need it in words. + if (fm['disable-model-invocation'] === 'true' && !/explicitly|only (runs?|when)/i.test(fm.description ?? '')) { + const msg = `${where}: disable-model-invocation is Claude-only; say in the description that it runs only when explicitly asked` + ;(where.startsWith(VENDORED) ? warnings : errors).push(msg) + } +} + +// 4. Every local plugin is installed by both setup scripts and the README's manual steps. +const setupSh = read('setup.sh') +const setupPs1 = read('setup.ps1') +const byHand = /### Or by hand[\s\S]*?```bash\n([\s\S]*?)```/.exec(read('README.md'))?.[1] ?? '' +if (!byHand) errors.push('README.md: no "### Or by hand" bash block found') +for (const { name } of local) { + if (!new RegExp(`(^|[\\s"])${name}([\\s;"\\\\]|$)`, 'm').test(setupSh)) errors.push(`setup.sh does not install ${name}`) + if (!setupPs1.includes(`"${name}@lippstack"`)) errors.push(`setup.ps1 does not install ${name}`) + if (!byHand.includes(`claude plugin install ${name}@lippstack`)) errors.push(`README.md "Or by hand" does not install ${name}`) +} + +// 5. Relative links in Markdown point at files that exist. +for (const f of files.filter(f => f.endsWith('.md'))) { + const text = readFileSync(f, 'utf8').replace(/```[\s\S]*?```/g, '').replace(/`[^`\n]*`/g, '') + for (const [, target] of text.matchAll(/\]\(([^)\s]+)\)/g)) { + if (/^([a-z]+:|#|\/)/i.test(target)) continue + const path = decodeURIComponent(target.split('#')[0]) + if (path && !existsSync(join(dirname(f), path))) { + const msg = `${rel(f)}: link to ${target} is broken` + ;(rel(f).startsWith(VENDORED) ? warnings : errors).push(msg) + } + } +} + +for (const w of warnings) console.log(`warning: ${w}`) +for (const e of errors) console.log(`error: ${e}`) +console.log(`${skills.length} skills, ${local.length} local plugins: ${errors.length} errors, ${warnings.length} warnings`) +process.exit(errors.length ? 1 : 0) diff --git a/scripts/sync-vendor.sh b/scripts/sync-vendor.sh index 17e7c29..8742743 100755 --- a/scripts/sync-vendor.sh +++ b/scripts/sync-vendor.sh @@ -10,7 +10,7 @@ trap 'rm -rf "$tmp"' EXIT # repo, path of the skill inside it, skill name here sync() { git clone -q --depth 1 "https://github.com/$1.git" "$tmp/$3" - rm -rf "$dest/$3" + rm -rf "${dest:?}/${3:?}" cp -R "$tmp/$3/$2" "$dest/$3" cp "$tmp/$3/LICENSE" "$dest/$3/LICENSE" echo "$3 <- $1 @ $(git -C "$tmp/$3" rev-parse --short HEAD)" diff --git a/setup.ps1 b/setup.ps1 index 11f2f3e..8c75ade 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -1,22 +1,15 @@ -# Installs the lippstack mods on Windows (PowerShell). Run it from the repo folder. -$ErrorActionPreference = "Stop" - -if (-not (Get-Command claude -ErrorAction SilentlyContinue)) { - Write-Error "Error: 'claude' CLI is not found in PATH. Please install Claude Code first." - exit 1 -} +# Installs lippstack on Windows (PowerShell). Run it from the repo folder. +# +# .\setup.ps1 Claude Code: mods, skills and plugins +# .\setup.ps1 -Agents codex,gemini-cli also the skills for other agents (npx skills) +# +# Agent ids: codex, gemini-cli, kimi-code-cli, opencode, cursor, github-copilot, ... +# ('*' for every agent the skills CLI knows). Mods are Claude Code only. +param([string[]]$Agents = @()) +$ErrorActionPreference = "Stop" $here = $PSScriptRoot -# "Stop" doesn't cover native commands, so check claude's exit code ourselves. -function Invoke-Claude { - & claude @args - if ($LASTEXITCODE -ne 0) { throw "claude $args failed (exit $LASTEXITCODE)" } -} - -Write-Host "==> Registering lippstack marketplace..." -ForegroundColor Cyan -Invoke-Claude plugin marketplace add "$here" - $lippPlugins = @( "savvy-progress@lippstack", "stage-bar@lippstack", @@ -28,39 +21,81 @@ $lippPlugins = @( "eng-skills@lippstack" ) -Write-Host "==> Installing lippstack plugins and skills..." -ForegroundColor Cyan -foreach ($plugin in $lippPlugins) { - Write-Host "Installing $plugin..." -ForegroundColor Gray - Invoke-Claude plugin install $plugin -} - -Write-Host "==> Installing ponytail..." -ForegroundColor Cyan -Invoke-Claude plugin install ponytail@lippstack - -Write-Host "==> Registering PM skills marketplace..." -ForegroundColor Cyan -Invoke-Claude plugin marketplace add deanpeters/Product-Manager-Skills - +# Dean Peters' PM skills (CC BY-NC-SA 4.0): only the ones we use, out of 77. $pmSkills = @( "user-story", "user-story-splitting", "epic-breakdown-advisor", "epic-hypothesis", "prd-development", "prioritization-advisor", "roadmap-planning", "discovery-process", "jobs-to-be-done", "problem-statement", "incoming-request-advisor", "stakeholder-mapping" ) -foreach ($skill in $pmSkills) { - Write-Host "Installing $skill@pm-skills..." -ForegroundColor Gray - Invoke-Claude plugin install "$skill@pm-skills" +$officialPlugins = @("hookify", "claude-md-management", "session-report", "playwright") + +# "Stop" doesn't cover native commands, so check their exit codes ourselves. +# A plain function (no param block), so flags like -y reach the program untouched. +function Invoke-Native { + $exe, $rest = $args + & $exe @rest + if ($LASTEXITCODE -ne 0) { throw "$exe $rest failed (exit $LASTEXITCODE)" } } +function Invoke-Claude { Invoke-Native claude @args } -Write-Host "==> Registering official plugins..." -ForegroundColor Cyan -Invoke-Claude plugin marketplace add anthropics/claude-plugins-official +function Install-Claude { + Write-Host "==> Registering lippstack marketplace..." -ForegroundColor Cyan + Invoke-Claude plugin marketplace add "$here" -$officialPlugins = @("hookify", "claude-md-management", "session-report", "playwright") -foreach ($plugin in $officialPlugins) { - Write-Host "Installing $plugin@claude-plugins-official..." -ForegroundColor Gray - Invoke-Claude plugin install "$plugin@claude-plugins-official" + Write-Host "==> Installing lippstack plugins and skills..." -ForegroundColor Cyan + foreach ($plugin in $lippPlugins) { + Write-Host "Installing $plugin..." -ForegroundColor Gray + Invoke-Claude plugin install $plugin + } + + Write-Host "==> Installing ponytail..." -ForegroundColor Cyan + Invoke-Claude plugin install ponytail@lippstack + + Write-Host "==> Registering PM skills marketplace..." -ForegroundColor Cyan + Invoke-Claude plugin marketplace add deanpeters/Product-Manager-Skills + foreach ($skill in $pmSkills) { + Write-Host "Installing $skill@pm-skills..." -ForegroundColor Gray + Invoke-Claude plugin install "$skill@pm-skills" + } + + Write-Host "==> Registering official plugins..." -ForegroundColor Cyan + Invoke-Claude plugin marketplace add anthropics/claude-plugins-official + foreach ($plugin in $officialPlugins) { + Write-Host "Installing $plugin@claude-plugins-official..." -ForegroundColor Gray + Invoke-Claude plugin install "$plugin@claude-plugins-official" + } + + Write-Host "==> Enabling built-in plugins..." -ForegroundColor Cyan + Invoke-Claude plugin enable cc-plugin-you-should-know@builtin +} + +function Install-Agents { + if (-not (Get-Command npx -ErrorAction SilentlyContinue)) { + throw "-Agents needs Node.js (npx) to run the skills CLI." + } + # Accepts -Agents codex,gemini-cli as well as -Agents "codex,gemini-cli". + $list = $Agents -join "," -split "," | Where-Object { $_ } + $flags = $list | ForEach-Object { "-a", $_ } + $pm = $pmSkills | ForEach-Object { "-s", $_ } + + Write-Host "==> Installing lippstack skills for: $($list -join ', ')" -ForegroundColor Cyan + Invoke-Native npx -y skills add "$here" -g @flags --skill "*" -y + Write-Host "==> Installing ponytail for: $($list -join ', ')" -ForegroundColor Cyan + Invoke-Native npx -y skills add DietrichGebert/ponytail -g @flags --skill "*" -y + Write-Host "==> Installing PM skills for: $($list -join ', ')" -ForegroundColor Cyan + Invoke-Native npx -y skills add deanpeters/Product-Manager-Skills -g @flags @pm -y +} + +if (Get-Command claude -ErrorAction SilentlyContinue) { + Install-Claude +} elseif ($Agents.Count -eq 0) { + Write-Error "'claude' CLI is not found in PATH. Install Claude Code, or use -Agents for other agents." + exit 1 +} else { + Write-Host "==> 'claude' not found: skipping Claude Code, installing skills only." -ForegroundColor Yellow } -Write-Host "==> Enabling built-in plugins..." -ForegroundColor Cyan -Invoke-Claude plugin enable cc-plugin-you-should-know@builtin +if ($Agents.Count -gt 0) { Install-Agents } -Write-Host "`nDone! Restart Claude Code, or run /reload-plugins in an open session." -ForegroundColor Green +Write-Host "`nDone! Restart your agent, or run /reload-plugins in an open Claude Code session." -ForegroundColor Green diff --git a/setup.sh b/setup.sh index 4307d8f..06d3f20 100755 --- a/setup.sh +++ b/setup.sh @@ -1,49 +1,92 @@ #!/usr/bin/env bash -# Installs the lippstack mods on macOS and Linux. Run it from the repo folder. +# Installs lippstack on macOS and Linux. Run it from the repo folder. +# +# ./setup.sh Claude Code: mods, skills and plugins +# ./setup.sh --agents codex,gemini-cli also the skills for other agents (npx skills) +# +# Agent ids: codex, gemini-cli, kimi-code-cli, opencode, cursor, github-copilot, ... +# ('*' for every agent the skills CLI knows). Mods are Claude Code only. set -euo pipefail -# Check for claude CLI -if ! command -v claude >/dev/null 2>&1; then - echo "Error: 'claude' CLI is not found in PATH. Please install Claude Code first." >&2 - exit 1 -fi - here="$(cd "$(dirname "$0")" && pwd)" -echo "==> Registering lippstack marketplace..." -claude plugin marketplace add "$here" +LIPPSTACK_PLUGINS="savvy-progress stage-bar done-ping where-line guardrails design-skills po-skills eng-skills" +# Dean Peters' PM skills (CC BY-NC-SA 4.0): only the ones we use, out of 77. +PM_SKILLS="user-story user-story-splitting epic-breakdown-advisor epic-hypothesis + prd-development prioritization-advisor roadmap-planning discovery-process + jobs-to-be-done problem-statement incoming-request-advisor stakeholder-mapping" +OFFICIAL_PLUGINS="hookify claude-md-management session-report playwright" -echo "==> Installing lippstack plugins and skills..." -for plugin in \ - savvy-progress stage-bar done-ping where-line guardrails \ - design-skills po-skills eng-skills; do - claude plugin install "${plugin}@lippstack" +agents="" +while [ $# -gt 0 ]; do + case "$1" in + --agents) agents="${2:-}"; shift 2 ;; + --agents=*) agents="${1#--agents=}"; shift ;; + -h|--help) sed -n '2,8p' "$0"; exit 0 ;; + *) echo "Unknown option: $1 (see ./setup.sh --help)" >&2; exit 1 ;; + esac done -# Third-party plugin, pulled from its own GitHub repo so it stays current. -echo "==> Installing ponytail..." -claude plugin install ponytail@lippstack - -# Dean Peters' PM skills (CC BY-NC-SA 4.0): from their own marketplace, only the ones we use. -echo "==> Registering PM skills marketplace..." -claude plugin marketplace add deanpeters/Product-Manager-Skills -for skill in \ - user-story user-story-splitting epic-breakdown-advisor epic-hypothesis \ - prd-development prioritization-advisor roadmap-planning discovery-process \ - jobs-to-be-done problem-statement incoming-request-advisor stakeholder-mapping; do - claude plugin install "$skill@pm-skills" -done +install_claude() { + echo "==> Registering lippstack marketplace..." + claude plugin marketplace add "$here" -# Anthropic's official plugins. -echo "==> Registering official plugins..." -claude plugin marketplace add anthropics/claude-plugins-official -for plugin in hookify claude-md-management session-report playwright; do - claude plugin install "$plugin@claude-plugins-official" -done + echo "==> Installing lippstack plugins and skills..." + for plugin in $LIPPSTACK_PLUGINS; do + claude plugin install "${plugin}@lippstack" + done + + # Third-party plugin, pulled from its own GitHub repo so it stays current. + echo "==> Installing ponytail..." + claude plugin install ponytail@lippstack + + echo "==> Registering PM skills marketplace..." + claude plugin marketplace add deanpeters/Product-Manager-Skills + for skill in $PM_SKILLS; do + claude plugin install "$skill@pm-skills" + done + + # Anthropic's official plugins. + echo "==> Registering official plugins..." + claude plugin marketplace add anthropics/claude-plugins-official + for plugin in $OFFICIAL_PLUGINS; do + claude plugin install "$plugin@claude-plugins-official" + done + + # Built into Claude Code: nothing to download, it only has to be switched on. + echo "==> Enabling built-in plugins..." + claude plugin enable cc-plugin-you-should-know@builtin +} + +install_agents() { + if ! command -v npx >/dev/null 2>&1; then + echo "Error: --agents needs Node.js (npx) to run the skills CLI." >&2 + exit 1 + fi + local flags=() a + IFS=',' read -r -a list <<< "$agents" + for a in "${list[@]}"; do flags+=(-a "$a"); done + + echo "==> Installing lippstack skills for: $agents" + npx -y skills add "$here" -g "${flags[@]}" --skill '*' -y + echo "==> Installing ponytail for: $agents" + npx -y skills add DietrichGebert/ponytail -g "${flags[@]}" --skill '*' -y + echo "==> Installing PM skills for: $agents" + local pm=() s + for s in $PM_SKILLS; do pm+=(-s "$s"); done + npx -y skills add deanpeters/Product-Manager-Skills -g "${flags[@]}" "${pm[@]}" -y +} + +if command -v claude >/dev/null 2>&1; then + install_claude +elif [ -z "$agents" ]; then + echo "Error: 'claude' CLI is not found in PATH. Install Claude Code, or use --agents for other agents." >&2 + exit 1 +else + echo "==> 'claude' not found: skipping Claude Code, installing skills only." +fi -# Built into Claude Code: nothing to download, it only has to be switched on. -echo "==> Enabling built-in plugins..." -claude plugin enable cc-plugin-you-should-know@builtin +if [ -n "$agents" ]; then install_agents; fi echo "" -echo "Done! Restart Claude Code, or run /reload-plugins in an open session." +echo "Done! Restart your agent, or run /reload-plugins in an open Claude Code session."