Skip to main content

ship

Package and release a verified forge-built plugin: claude plugin validate --strict, frontmatter/scoped-name/freshness lints, README with the permission story, versioning decision (SHA vs pinned semver), distribution menu (skills-dir, private marketplace, community), and installing the trace-capture flywheel. Use when the user says ship, release, publish, or distribute the plugin.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
Aznatkoiny/claude-dev-toolkit
آخر نشاط في المصدر
٢ أغسطس ٢٠٢٦ في ٢٠:٢٦
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٠
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
ship
description
Package and release a verified forge-built plugin: claude plugin validate --strict, frontmatter/scoped-name/freshness lints, README with the permission story, versioning decision (SHA vs pinned semver), distribution menu (skills-dir, private marketplace, community), and installing the trace-capture flywheel. Use when the user says ship, release, publish, or distribute the plugin.
when_to_use
Only when .forge/state.json phase is ship (verification green). User-gated release: the user invokes /plugin-forge:ship; the model never auto-invokes it.
argument-hint
[skills-dir|marketplace|community]
disable-model-invocation
true
allowed-tools
Bash(claude plugin *), Bash(grep *), Bash(python3 *), Bash(mkdir *), Bash(cp *), Bash(chmod +x *)
# Ship — validate, lint, document, version, distribute Current forge state: !`cat .forge/state.json 2>/dev/null || echo NO_STATE` If the line above shows a policy notice instead of JSON (org setting `disableSkillShellExecution`), read `.forge/state.json` with the Read tool before continuing. ## Phase gate (hard) Proceed only when `phase` is `ship` — verification must already be green. For any other state, run NOTHING and reply with exactly the remediation below, then stop: | Injected state | Reply and stop | |---|---| | NO_STATE | "No forge run in this project. Run `/plugin-forge:forge` to start one." | | `interview` | "Interview incomplete. Run `/plugin-forge:forge`." | | `contract` | "No approved contract yet. Run `/plugin-forge:write-contract`." | | `evals` | "Suite not armed. Run `/plugin-forge:build-evals`, then `/plugin-forge:arm-evals`." | | `armed` | "Nothing built yet. Run `/plugin-forge:build-loop`." | | `building` | "Build loop has not gone green. Run `/plugin-forge:build-loop`." | | `smoke` or `verify` | "Verification is not complete. Run `/plugin-forge:verify`." | Inputs to read before Step 1: `plugin_name` and `plugin_dir` from the injected state; `design/CONTRACT.md` (C6 distribution decisions, runtime answers — cloud/routine or local, version floors, permission story, cost sheet); the latest `runs/<ts>/verify-report.md` (baseline numbers for the README); and `.forge/freeze.json` (suite version). ## Step 1 — Static gates (all four pass before anything is written) **1a. Strict validation** — the same check the community pipeline runs: ``` claude plugin validate --strict <plugin_dir> ``` **1b. Frontmatter YAML lint** on every generated SKILL.md. Malformed YAML fails silently: the body loads with no metadata, so the skill never auto-triggers. Run: ``` python3 - <plugin_dir> <<'EOF' import sys, re, pathlib root, bad = pathlib.Path(sys.argv[1]), [] files = sorted(root.glob('skills/**/SKILL.md')) + sorted(root.glob('SKILL.md')) for f in files: t = f.read_text(encoding='utf-8') m = re.match(r'\A---\r?\n(.*?)\r?\n---\r?\n', t, re.S) if not m: bad.append(f'{f}: frontmatter fence missing or unclosed'); continue fm = m.group(1) tops = [(k.group(1), k.start()) for k in re.finditer(r'^([A-Za-z][\w-]*):', fm, re.M)] names = [k for k, _ in tops] for req in ('name', 'description'): if req not in names: bad.append(f'{f}: missing required key {req}:') if '\t' in fm: bad.append(f'{f}: literal tab in frontmatter (YAML hazard)') for b in re.finditer(r':\s*(yes|no|on|off)\s*$', fm, re.M | re.I): bad.append(f'{f}: boolean "{b.group(1)}" needs >=2.1.218 - use true/false') def block(key): d = dict(tops) if key not in d: return '' starts = sorted(s for _, s in tops); i = starts.index(d[key]) return fm[d[key]:starts[i+1]] if i + 1 < len(starts) else fm[d[key]:] if len(block('description')) + len(block('when_to_use')) > 1536: bad.append(f'{f}: description+when_to_use exceed the 1536-char listing cut') for b in bad: print('FAIL', b) print('frontmatter lint:', 'FAIL' if bad else 'PASS') sys.exit(1 if bad else 0) EOF ``` **1c. Scoped-name lint** — bundled-MCP references using a bare server name never fire; they must be `mcp__plugin_<plugin>_<server>__<tool>` (and `plugin:<plugin>:<server>` in `mcp_tool` hook `server` fields): ``` grep -rnE 'mcp__' <plugin_dir> --include='*.md' --include='*.json' | grep -v 'mcp__plugin_' ``` Every hit in hook matchers, permission rules, agent tools lists, or skill allowed-tools is a FAIL — unless it names an EXTERNAL server the plugin does not bundle (check against the plugin's `.mcp.json` before failing). **1d. Freshness lint** — deprecated shapes must not ship: ``` grep -rnE '"decision"[[:space:]]*:[[:space:]]*"(approve|block)"' <plugin_dir> grep -rn '/mnt/skills' <plugin_dir> ``` Both greps must return nothing. The decision-JSON hook shape is deprecated (current discipline: stderr + exit 2 to block, or `hookSpecificOutput`); `/mnt/skills` paths are foreign-environment rot that never resolves in Claude Code. Fix at the source, never by weakening the lint. ## Step 2 — Install the flywheel (trace capture) ``` mkdir -p <plugin_dir>/hooks/scripts cp "${CLAUDE_PLUGIN_ROOT}/templates/hooks/trace-capture.sh" <plugin_dir>/hooks/scripts/trace-capture.sh chmod +x <plugin_dir>/hooks/scripts/trace-capture.sh ``` Merge `${CLAUDE_PLUGIN_ROOT}/templates/hooks/hooks-snippet.json` into `<plugin_dir>/hooks/hooks.json`: create the file from the snippet when the plugin ships no hooks yet; otherwise append the snippet's PostToolUse entry to the existing array — never clobber existing entries. Confirm the merged entry is exec-form and references `"${CLAUDE_PLUGIN_ROOT}/hooks/scripts/trace-capture.sh"` — at the generated plugin's runtime that variable resolves to ITS root, not to plugin-forge's. Why this ships in every generated plugin: the hook appends tool args/results/errors to `${CLAUDE_PLUGIN_DATA}/traces/*.jsonl`, so production failures become eval tasks — `/plugin-forge:build-evals mine` converts traces to tasks, `/plugin-forge:arm-evals` re-freezes vN+1, and `/plugin-forge:build-loop` rebuilds until green. Document the loop in the README (Step 4). ## Step 3 — Optional observability (fleet/long-job plugins) Offer when the generated plugin dispatches subagent fleets or long jobs; otherwise skip. - **subagentStatusLine** — a plugin `settings.json` supports ONLY the `agent` and `subagentStatusLine` keys: ```json {"subagentStatusLine": {"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/status.sh"}} ``` with the script reading a `${CLAUDE_PLUGIN_DATA}/status.json` the plugin's long-running jobs update. - **Monitor** — pair each long-job skill with `monitors/monitors.json`: ```json [{"name": "<job>-watch", "command": "tail -F <log path>", "description": "streams <job> progress into the session", "when": "on-skill-invoke:<skill-name>"}] ``` State the caveats wherever this is offered: monitors are experimental, interactive-CLI only, do not load from project-scope skills-dir installs, reject `${user_config.*}` in commands, and need a session restart after plugin updates. ## Step 4 — README for the generated plugin Instantiate `${CLAUDE_PLUGIN_ROOT}/skills/generation-standards/templates/readme.tmpl.md` into `<plugin_dir>/README.md`. Required sections: 1. **Overview + invocations** — what the plugin does; every user-facing skill as `/<plugin_name>:<skill>` with its argument hint. 2. **Permission story** — a `permissions.allow` list covering EVERY command the plugin's skills and agents run. allowed-tools grants are turn-scoped (cleared on the next user message), so long flows need standing allows in the consumer's `.claude/settings.json`: `{"permissions": {"allow": ["Bash(<cmd> *)", "..."]}}`. 3. **Minimum Claude Code version** — the highest floor among features used (from CONTRACT C6). Known floors: `${CLAUDE_SKILL_DIR}` in allowed-tools 2.1.129; `defaultEnabled` 2.1.154; marketplace `renames` 2.1.193; `--json-schema` hard errors / LSP `restartOnCrash` 2.1.205; worktree resume 2.1.212; frontmatter `name` segment override 2.1.216; yes/no boolean forms and background-fork default 2.1.218. 4. **Headless recipe** — the `claude -p` invocation for CI/routines, using the suite's sandbox settings profile (never `--dangerously-skip-permissions`), plus `CLAUDE_CODE_PLUGIN_SEED_DIR` for network-free container loading. 5. **How to run your evals** — the `evals/` layout; `evals/bin/run.py` is vendored and stdlib-only, so the suite runs WITHOUT plugin-forge installed; red baseline via the no-plugin target; the final `FORGE_EVAL:` line and exit code as the CI signal; run the regression suite in CI; cost honesty (`--max-cost-usd`); current baseline for future battle A/B: suite version from `.forge/freeze.json`, pass^3 and cost from `verify-report.md`. 6. **Flywheel** — traces land in `${CLAUDE_PLUGIN_DATA}/traces/`; feed failures back with `/plugin-forge:build-evals mine`. ## Step 5 — Versioning decision (ask the user) Present exactly two options via AskUserQuestion: - **Still iterating** — OMIT `version` from plugin.json. The git SHA becomes the version: every pushed commit reaches users. - **Releasing** — pin semver in plugin.json, add a CHANGELOG entry, and run `claude plugin tag --push` (creates the `<name>--v<version>` tag that dependency constraints resolve against). Explain the trap before they choose: a pinned semver means users update ONLY when the field is bumped — pushing commits under a stale pin does nothing ("already at latest version"). A plugin pinned at 1.0.0 and then iterated strands every installed user on the first build. Never pin without a release process that bumps it. Also state: the plugin `name` is a permanent public identifier (the marketplace `renames` map is the only migration path, 2.1.193+); `displayName` is the safe-to-change label. ## Step 6 — Final re-validation Steps 2–4 added files. Re-run on the FINAL tree: ``` claude plugin validate --strict <plugin_dir> ``` and repeat the Step 1c/1d greps over the added files. Ship nothing that is not green here. ## Step 7 — Distribution menu Ask via AskUserQuestion unless `$ARGUMENTS` pre-selects a path: **A. skills-dir plugin (lightest)** — copy `<plugin_dir>` (with `.claude-plugin/plugin.json`) to `~/.claude/skills/<plugin_name>/` (personal) or `<repo>/.claude/skills/<plugin_name>/` (project). It loads as `<plugin_name>@skills-dir` next session; SKILL.md edits are live, hooks/MCP changes need `/reload-plugins`. Warn: project scope is trust-gated, does NOT walk up to the repo root (launch Claude from the root), and monitors do not load. **B. Private marketplace** — add or extend `.claude-plugin/marketplace.json` in the hosting repo: ```json {"name": "<marketplace-name>", "owner": {"name": "<owner>"}, "plugins": [{"name": "<plugin_name>", "source": "./<relative-path>", "description": "<one line>", "category": "<category>"}]} ``` Validate the marketplace repo with `claude plugin validate .`. Consumers run: ``` claude plugin marketplace add <org>/<repo> claude plugin install <plugin_name>@<marketplace-name> ``` Maintenance: `claude plugin marketplace update <marketplace-name>`; inspection: `claude plugin marketplace list --json`. Warn: marketplace names collide globally per user (a second add replaces the first) and several names are reserved — pick something distinctive. **C. Community submission** — pre-submission requirements: kebab-case plugin name (the claude.ai marketplace sync REJECTS non-kebab names); `claude plugin validate --strict` clean (the community pipeline runs the same check as Step 1a); README and LICENSE present; semver pinned and tagged via `claude plugin tag --push`; public repo. Then submit the repo through the community marketplace submission form, with the marketplace-entry fields (name, source, description, category, tags) prepared to paste. ## Step 8 — Repo-declared settings (cloud/routine contracts) If CONTRACT.md says the plugin runs in cloud sessions or scheduled routines, personal installs are invisible there (cloud sessions never read `~/.claude/skills`) — the consuming repo must declare the plugin. Instantiate `${CLAUDE_PLUGIN_ROOT}/skills/generation-standards/templates/settings-snippet.tmpl.json` into the consuming repo's `.claude/settings.json`: ```json {"extraKnownMarketplaces": {"<marketplace-name>": {"source": {"source": "github", "repo": "<org>/<repo>"}}}, "enabledPlugins": {"<plugin_name>@<marketplace-name>": true}} ``` Teammates get a one-time trust prompt, then auto-install. When the contract includes long headless runs, fold the Step 4 `permissions.allow` block into the same snippet. ## Step 9 — Wrap up Report to the user: - Gates re-passed on the final tree (Step 6). - Files added: `hooks/scripts/trace-capture.sh`, the hooks.json entry, `README.md`, plus `settings.json`/`monitors/monitors.json` if chosen. - Version strategy chosen and why; distribution path with the exact consumer commands; settings snippet emitted (or why not). - Baseline carried into the README: suite vN, pass^3, cost, run timestamp. - The honest claim: the plugin is "green + holdout + triaged" against suite vN — not proven "correct". Leave `.forge/` in place: it is the flywheel's memory (freeze hash, change requests, run history). Phase stays `ship`. To start a fresh forge run in this project later, `forge-eval doctor` clears stuck state safely.
عرض على GitHub