release-skill
简体中文 · Installation: English / 简体中文
Release preparation for Claude Code and Codex, with human-edited files kept intact.
release-skill helps a maintainer answer three questions: what will be released, which checks still fail, and which exact bytes will reach users. It freezes the reviewed artifacts first and publishes those same artifacts later; it does not regenerate a README or re-pack the live workspace at the last step.
Current boundary: v0.1.5 is the current release. v0.1.1 completed a real production release to GitHub and npm — the first production-verified milestone — followed by exact npm installation and Claude/Codex consumer installation verification from the frozen Git ref; "current release" and "first production-verified milestone" are two distinct facts and must not be conflated. The same workflow also has a local production-equivalent protocol suite using the real release-skill CLI and frozen artifacts, local bare Git remotes, and protocol fakes for
gh,npm, Claude, and Codex. The suite does not provide OS-level network isolation, and it does not prove that another project's credentials, permissions, rate limits, or eventual-consistency behavior will match this release. Treat each project's first production run as a monitored canary.prepare --onlineobserves bound previous-public baselines and fails closed on drift; remote uniqueness checks run during publish global preflight.
Production path verified since the v0.1.1 milestone; v0.1.5 is the current release. The npm-installed CLI is the supported user entry. Source checkout is the development/contributor fallback.
Start here:
- npm install:
npm install -g release-skill→release-skill help- source checkout:
node "$RELEASE_SKILL_HOME/packages/release-skill/bin/release-skill.mjs" help
Safe defaults: the recommended path is
help → assess → prepare --offline → human review. Production publishing addsprepare --production → approve → publish --confirm-production <planDigest>; aboundprevious-public baseline specifically requiresprepare --online --production. Without digest confirmation, no remote preflight or write starts.
Why this is safe for a hand-edited README
release-skill does not regenerate or rewrite project source files. prepare copies
each configured public file from the current workspace into an isolated local
snapshot and verifies the copied bytes. That includes the complete README:
slogans, examples, prose, formatting, and later human edits.
- A later prepare reads the current file again; it does not rebuild it from a template.
- The snapshot must match the source bytes exactly.
- A changed plan gets a new digest, so an old approval cannot authorize it.
- A source edit after prepare makes publish stop before remote writes. Preserve the edit by preparing, reviewing, and approving a new plan.
- Tampering with a frozen snapshot, Git object, or tarball fails its digest gate.
- Existing remote branches, tags, releases, or npm versions require human intervention; the tool does not force or overwrite them.
- Only files listed in
publicFilesare copied. Add translated READMEs, images, demos, and linked documents explicitly when they belong in the release. - A release freezes only the current truth:
preparenever refreshes or rewrites human docs. Maintainers update README, INSTALL, and CHANGELOG first — including the machine-readablerelease-skill:release-versionmarkers, which must equal thepackage.jsonversion, and the formal CHANGELOG heading for the current version — then prepare, review, and approve. A pre-release gate fails closed when any doc version marker or the CHANGELOG current-version entry drifts.
This is the preservation contract: copy current truth, freeze reviewed truth, and never rewrite human truth.
Quick start
Install / requirements
- Node.js 22+
- Git 2.30+
- A target Git repository with at least one commit
Install from npm (recommended):
npm install -g release-skill
Or run directly without installing:
npx release-skill help
Verify the install:
release-skill help
Development install (contributor fallback, from source checkout):
Set the checkout location and install dependencies:
export RELEASE_SKILL_HOME=/absolute/path/to/release-skill
cd "$RELEASE_SKILL_HOME"
npm exec --yes pnpm@10.17.1 -- install --frozen-lockfile
Then use the CLI via node "$RELEASE_SKILL_HOME/packages/release-skill/bin/release-skill.mjs".
First keep local plans, approvals, and frozen artifacts out of Git:
.release-skill/*
!.release-skill/project.yaml
First use: deterministic setup without loading the full report
Setup is read-only by default. Keep its potentially large report in temporary
files; show the user or Agent only the deterministic compactSummary review
view. The summary does not replace authorization: setupDigest still binds the
complete facts, candidates, and answers.
PROJECT=/absolute/path/to/my-project
SETUP_SESSION="$(mktemp -d "${TMPDIR:-/tmp}/release-setup.XXXXXX")"
REPORT="$SETUP_SESSION/discovery.json"
ANSWERS="$SETUP_SESSION/answers.json"
BOUND_REPORT="$SETUP_SESSION/bound.json"
printf 'SETUP_SESSION=%s\nPROJECT=%s\n' "$SETUP_SESSION" "$PROJECT"
release-skill setup --root "$PROJECT" --json > "$REPORT" || test "$?" -eq 2
node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));if(!r.compactSummary){console.error("compactSummary missing");process.exit(2)}process.stdout.write(JSON.stringify(r.compactSummary,null,2)+"\n")' "$REPORT"
NEEDS_INPUT and LOCAL_ONLY_DETECTED intentionally exit with code 2. If
proposalConflicts is non-empty—including PUBLIC_REPO_AUTHORITY_CONFLICT or
a public-file mapping conflict—stop and let a human correct the conflicting
repository or mapping authority, then rerun setup. Do not guess a winner.
With no conflicts, copy the machine proposal mechanically. The Agent must not rewrite or transcribe it:
SETUP_SESSION='/session-directory-absolute-path-printed-above'
PROJECT='/project-absolute-path-printed-above'
REPORT="$SETUP_SESSION/discovery.json"
ANSWERS="$SETUP_SESSION/answers.json"
BOUND_REPORT="$SETUP_SESSION/bound.json"
node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));if((r.proposalConflicts??[]).length){console.error("proposal conflicts require human resolution");process.exit(2)}if(!r.recommendedAnswers){console.error("recommendedAnswers missing");process.exit(2)}fs.writeFileSync(process.argv[2],JSON.stringify(r.recommendedAnswers,null,2)+"\n",{flag:"wx",mode:0o600})' "$REPORT" "$ANSWERS"
release-skill setup --root "$PROJECT" --answers "$ANSWERS" --json > "$BOUND_REPORT"
node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));if(!r.compactSummary||!r.setupDigest){console.error("bound setup report incomplete");process.exit(2)}process.stdout.write(JSON.stringify({compactSummary:r.compactSummary,setupDigest:r.setupDigest},null,2)+"\n")' "$BOUND_REPORT"
printf 'SETUP_SESSION=%s\nPROJECT=%s\n' "$SETUP_SESSION" "$PROJECT"
Review that bound summary and exact digest once. After explicit human confirmation, use the confirmed digest literal to create the configuration:
SETUP_SESSION=<session-directory-absolute-path-printed-above>
PROJECT=<project-absolute-path-printed-above>
ANSWERS="$SETUP_SESSION/answers.json"
CREATED_REPORT="$SETUP_SESSION/created.json"
POST_REPORT="$SETUP_SESSION/post-setup.json"
ASSESS_REPORT="$SETUP_SESSION/assess.json"
release-skill setup --root "$PROJECT" --answers "$ANSWERS" \
--write --confirm-setup <confirmed-setupDigest> --json > "$CREATED_REPORT"
release-skill setup --root "$PROJECT" --json > "$POST_REPORT"
set +e
release-skill assess --root "$PROJECT" --offline --json > "$ASSESS_REPORT"
ASSESS_EXIT=$?
set -e
[ "$ASSESS_EXIT" -eq 0 ] || [ "$ASSESS_EXIT" -eq 1 ] || exit "$ASSESS_EXIT"
node -e 'const fs=require("node:fs");const [c,p,a]=process.argv.slice(1).map(x=>JSON.parse(fs.readFileSync(x,"utf8")));if(c.status!=="CONFIG_CREATED"||p.status!=="ALREADY_CONFIGURED"||!["ASSESSED","NEEDS_INPUT","BLOCKED"].includes(a.status)){process.exit(2)}process.stdout.write(JSON.stringify({created:c.status,postSetup:p.status,assessment:{status:a.status,summary:a.summary,gapCount:(a.gaps??[]).length,blockingCodes:(a.gaps??[]).filter(g=>g.severity==="error").map(g=>g.code)}},null,2)+"\n")' "$CREATED_REPORT" "$POST_REPORT" "$ASSESS_REPORT"
node -e 'require("node:fs").rmSync(process.argv[1],{recursive:true,force:false})' "$SETUP_SESSION"
The write must return CONFIG_CREATED; the next setup must return
ALREADY_CONFIGURED. Existing configuration is never regenerated—make only
reviewed incremental edits. Discovered interpreter/package-manager scripts are
SIDE_EFFECTS_UNPROVEN and are not selected automatically. Add a project-specific
hook or gate only after human review: edit projectConfig.hooks, or edit
verificationGates and add the same id to selectedGateIds, then rerun the
bound dry-run. Keep human files at mode: preserve, and
use sourceScope: workspace only for explicit cross-unit shared sources.
Advanced schema reference—not the first-use path
The wrapper below illustrates the schema only. Do not hand-write it during the
normal setup path; mechanically extract recommendedAnswers as shown above.
{
"projectConfig": {
"apiVersion": "release-skill/v1",
"kind": "ReleaseProject",
"project": { "name": "my-project", "defaultBranch": "main" },
"releaseUnits": [{
"id": "my-project",
"source": ".",
"publicRepo": "owner/my-project",
"version": { "source": "package.json", "tagTemplate": "v{version}" },
"distributions": [{
"type": "npm",
"package": "my-project",
"access": "public",
"provenance": false,
"tag": "latest",
"registry": "https://registry.npmjs.org",
"publisher": "my-npm-username"
}],
"publicFiles": [
{ "from": "README.md", "to": "README.md", "mode": "preserve" },
{ "from": "package.json", "to": "package.json", "mode": "preserve" }
],
"requiredPublicFiles": ["README.md", "package.json"],
"previousPublicBaseline": { "mode": "none" },
"production": {
"branchTemplate": "release/{tag}",
"branchStrategy": "create-release-branch"
}
}]
},
"selectedGateIds": []
}
This is a schema reference, not an onboarding template. Normal setup must use
the machine proposal. mode: none is valid only when no public version exists.
The following reference shows the exact relationship between a manually
reviewed gate and selectedGateIds. Apply that relationship only as an
incremental edit to the extracted machine proposal:
{
"projectConfig": {
"apiVersion": "release-skill/v1",
"kind": "ReleaseProject",
"project": { "name": "my-project", "defaultBranch": "main" },
"releaseUnits": [{
"id": "my-project",
"source": ".",
"publicRepo": "owner/my-project",
"version": { "source": "package.json", "tagTemplate": "v{version}" },
"distributions": [{
"type": "npm",
"package": "my-project",
"access": "public",
"provenance": false,
"tag": "latest",
"registry": "https://registry.npmjs.org",
"publisher": "my-npm-username"
}],
"publicFiles": [
{ "from": "package.json", "to": "package.json", "mode": "preserve" }
],
"requiredPublicFiles": ["package.json"],
"previousPublicBaseline": { "mode": "none" },
"production": {
"branchTemplate": "release/{tag}",
"branchStrategy": "create-release-branch"
}
}],
"verificationGates": [{
"id": "my-project-script-test",
"phase": "snapshot-verify",
"scope": { "unit": "my-project" },
"command": ["node", "-e", "const p=require('./package.json');if(!p.name)process.exit(1)"],
"cwd": ".",
"timeoutMs": 30000,
"envAllowlist": []
}]
},
"selectedGateIds": ["my-project-script-test"]
}
The id must be copied from the current gateCandidates; do not invent one.
The example command is self-contained in the public snapshot. A project script
is valid only when the script and every dependency it needs are included in
publicFiles; a snapshot gate cannot see the parent workspace's tests,
development dependencies, or node_modules unless they are explicitly public.
release-skill setup --root /absolute/path/to/my-project \
--answers /absolute/path/to/setup-answers.json --json
release-skill setup --root /absolute/path/to/my-project \
--answers /absolute/path/to/setup-answers.json \
--write --confirm-setup <setupDigest> --json
Setup atomically creates only an absent .release-skill/project.yaml.
That create-once step uses the digest-registered darwin-arm64 native
prebuild shipped in v0.1.3; unsupported platforms fail closed with
SAFE_WRITE_UNAVAILABLE instead of falling back to path-based writes.
ALREADY_CONFIGURED/CONFIG_EXISTS means the existing file remains
human-owned and must be edited incrementally. README, slogans, CHANGELOG, and
business scripts are never generated or overwritten. A project with no remote
channel reports LOCAL_ONLY_DETECTED instead of inventing production support.
The following is a minimal human-authored configuration. npm visibility, public-file boundaries, and remote targets must be explicit:
apiVersion: release-skill/v1
kind: ReleaseProject
project:
name: my-project
defaultBranch: main
releaseUnits:
- id: my-project
source: .
publicRepo: owner/my-project
version:
source: package.json
tagTemplate: v{version}
publicFiles:
- from: README.md
to: README.md
mode: preserve
- from: package.json
to: package.json
mode: preserve
- from: LICENSE
to: LICENSE
mode: preserve
requiredPublicFiles: [README.md, LICENSE, package.json]
previousPublicBaseline:
mode: none # first release: no prior public version exists
distributions:
- type: npm
package: my-project
access: public # or restricted; choose the real package policy
provenance: false # use true only after CI/OIDC is configured
tag: latest
registry: https://registry.npmjs.org
publisher: my-npm-username
# Optional: CLI smoke verification. When smokeBin is set, verify
# installs the package in an isolated directory and runs the named
# binary. Without smokeBin, verify only confirms install + name/version.
# smokeBin: my-project
# smokeArgs: [help, --json]
# smokeExpectedJson:
# command: help
# status: READY
production:
branchTemplate: release/{tag}
branchStrategy: create-release-branch
releaseTitleTemplate: "{unit} {version}"
releaseNotes: "Human-maintained release notes"
Every release unit must declare its previous public baseline. Use mode: none
only when you have verified that no earlier public version exists. For an
existing public repository, bind the exact immutable ref and commit instead:
previousPublicBaseline:
mode: bound
repo: owner/my-project
ref: release/v0.9.0
commit: 0123456789abcdef0123456789abcdef01234567
none is not a conflict-check bypass: publish still checks target branch,
tag, GitHub Release, and npm version uniqueness before any write. A bound
production prepare must run online so the ref-to-commit mapping can be observed.
The default observer does not download remote file contents, so it reports a
mapping diff and marks content diff unavailable. On drift, stop and choose
merge, adopt, or reject manually. First obtain and review the actual remote
commit; the tool does not download or merge its files. merge keeps both local
and remote edits in the human-owned source. adopt copies the reviewed remote
bytes into that source. reject stops the release while the remote/ref is
investigated or corrected; never switch to mode: none to bypass the drift.
After merge or adopt, rebind previousPublicBaseline to the accepted
immutable repo/ref/commit, then run a new prepare --online --production,
review, and approval.
Choose a branch strategy that matches the real repository:
create-release-branchcreates an absent immutable release branch and stops if the name already exists.advance-existing-branchcreates a single-parent commit on the exactpreviousPublicBaselinecommit and permits only an ordinary fast-forward push; concurrent drift requires human intervention.initialize-default-branchcreates an absent standard branch under control. Only explicitsetAsDefaultBranchandexpectedCurrentDefaultBranchvalues add a separately approved, observed, and reconcilable default-branch action.
Minimal configurations for the three strategies are:
# New immutable release branch; the target must not exist.
previousPublicBaseline: { mode: none } # only for a true first public release
production:
branchTemplate: release/{tag}
branchStrategy: create-release-branch
# Advance main; the bound ref must be exactly the target branch.
previousPublicBaseline:
mode: bound
repo: owner/my-project
ref: refs/heads/main
commit: 0123456789abcdef0123456789abcdef01234567
production:
branchTemplate: main
branchStrategy: advance-existing-branch
# One-time creation of an absent main and an explicit default-branch switch.
previousPublicBaseline:
mode: bound
repo: owner/my-project
ref: refs/heads/old-public-branch
commit: 0123456789abcdef0123456789abcdef01234567
production:
branchTemplate: main
branchStrategy: initialize-default-branch
setAsDefaultBranch: true
expectedCurrentDefaultBranch: old-public-branch
The latter two require prepare --online --production. If the observed branch,
commit, target absence, or current default branch differs, stop and update the
human-owned source/config only after reviewing the real remote state; never
force-push or weaken the baseline.
This is a mechanics-only local example, not a complete npm publication map.
Before a real release, enumerate every public runtime file, executable, type
declaration, image, and linked document. In a monorepo, set source to a path
such as packages/my-plugin, and keep each from path relative to the workspace
root, for example packages/my-plugin/README.md.
Before the first prepare, preferably commit .gitignore, .release-skill/project.yaml,
the README, version files, and all intended release content so the Git baseline
is easy to reproduce. Uncommitted edits that already exist at prepare time and
remain unchanged are included in the snapshot/baseline; only a later change
causes baseline validation to stop.
Main workflow
Run these steps in order. Steps 1–4 are safe default (read-only or local-only); steps 5–9 are production publishing with explicit human gates.
# npm-installed CLI (recommended):
CLI=(release-skill)
PROJECT=/absolute/path/to/my-project
ACTOR=your-name
# Development fallback (source checkout):
# CLI=(node "$RELEASE_SKILL_HOME/packages/release-skill/bin/release-skill.mjs")
The npm-installed CLI is the supported user entry after v0.1.1 production publication. The source checkout remains the development/contributor fallback.
- Environment check:
"${CLI[@]}" help - First-use setup (only when config is absent; read-only):
Follow the mechanical"${CLI[@]}" setup --root "$PROJECT" --jsoncompactSummaryandrecommendedAnswerspath above; confirm the boundsetupDigestonce, and skip this step when configuration exists. - Readiness assessment (read-only):
"${CLI[@]}" assess --root "$PROJECT" --offline --json - Local snapshot and plan freeze:
Omit an acknowledgement only when that project config has no corresponding hook or snapshot gate. Never grant either acknowledgement before reviewing the configured executable, arguments, working directory, and side effects."${CLI[@]}" prepare --root "$PROJECT" --offline \ --acknowledge-hook-side-effects \ --acknowledge-gate-side-effects --json - Human review: inspect the returned
planPath,externalActions,units[].targetVersion, andplanDigest. Each unit's snapshot is under<evidenceDir>/snapshots/<unit-id>/. The release-skill pipeline writes its own data under.release-skill/; acknowledged project hooks and gates are unsandboxed processes and may write elsewhere or access the network. - Production plan freeze:
As above, omit only acknowledgements that are not required by the project config, and review every configured process before granting them. Review the new plan's externalActions, npm policy, branch/tag, and frozen digests.PRODUCTION_JSON=$("${CLI[@]}" prepare --root "$PROJECT" --online --production \ --acknowledge-hook-side-effects \ --acknowledge-gate-side-effects --json) printf '%s\n' "$PRODUCTION_JSON" | jq . PLAN_PATH=$(printf '%s\n' "$PRODUCTION_JSON" | jq -r '.planPath') PLAN_DIGEST=$(printf '%s\n' "$PRODUCTION_JSON" | jq -r '.planDigest')prepare --jsonreturns the immutable production authority as<project>/.release-skill/plans/<planDigest>.json; always carry that returnedplanPathforward..release-skill/release-plan.jsonis only a mutable convenience alias and must not be passed to production approve/publish/reconcile. - Approval:
Returns the immutable production authority asAPPROVAL_JSON=$("${CLI[@]}" approve --plan "$PLAN_PATH" \ --digest "$PLAN_DIGEST" --actor "$ACTOR" --json) printf '%s\n' "$APPROVAL_JSON" | jq . APPROVAL_PATH=$(printf '%s\n' "$APPROVAL_JSON" | jq -r '.approvalPath')approvalPathat<project>/.release-skill/approvals/<planDigest>/<approvalDigest>.json.latestApprovalPathpoints to.release-skill/approval-record.json, which is only a mutable convenience alias and must not be passed to production publish/reconcile. Approval expires after 24 hours; a PARTIAL recovery may create a new approval for the same plan while preserving every earlier approval byte-for-byte. Use the returnedapprovalPathandexpiresAtas authority. - Publish (remote writes start here):
Save the returnedPUBLISH_JSON=$("${CLI[@]}" publish --root "$PROJECT" \ --plan "$PLAN_PATH" --approval "$APPROVAL_PATH" \ --confirm-production "$PLAN_DIGEST" --json) printf '%s\n' "$PUBLISH_JSON" | jq . PUBLISH_RUN_PATH=$(printf '%s\n' "$PUBLISH_JSON" | jq -r '.runPath')runPath.PUBLISHEDis not the terminal state. - Verify (consumer install check):
Omit the acknowledgement only when the plan has neither consumer gates nor a configured npm"${CLI[@]}" verify --root "$PROJECT" \ --plan "$PLAN_PATH" --run "$PUBLISH_RUN_PATH" \ --acknowledge-gate-side-effects --jsonsmokeBin. Both execute installed project code without an OS or network sandbox.
The handoff example requires jq. Without it, copy the same four returned JSON
fields exactly; do not pass the angle-bracket labels shown elsewhere as shell
syntax.
Production prepare seals a standalone Git commit/tree for every public snapshot
and creates a fixed tarball for every npm unit. Publish globally preflights all
actions, then executes and observes public branch, tag, npm, GitHub Release,
and configured Claude/Codex marketplace installation checkpoints. verify
installs every exact npm package@version in an isolated directory; when
smokeBin is configured it also runs the CLI and validates output. Only when
all evidence matches does the run reach VERIFIED.
Before a real release run gh auth login, gh auth setup-git, and
npm login, and confirm Git HTTPS credentials can access the target repository.
Version branches default to release/<tag> and can be configured per unit with
production.branchTemplate; any existing remote object stops for human review.
Parent workspace with npm + plugin sub-units
When a monorepo produces both an npm package and a Claude/Codex plugin from different directories, define separate release units. Only add a plugin distribution when the unit actually ships a plugin with manifest, marketplace, and entry Skill:
Here project is the parent workspace's orchestration container, not a public
release unit. If the workspace root also publishes its own repository or
package, add another release unit with source: ..
apiVersion: release-skill/v1
kind: ReleaseProject
project:
name: my-workspace
defaultBranch: main
releaseUnits:
- id: my-app
source: packages/app
publicRepo: owner/my-app
version:
source: packages/app/package.json
tagTemplate: my-app-v{version}
distributions:
- type: npm
package: my-app
access: public
provenance: false
tag: latest
registry: https://registry.npmjs.org
publisher: my-npm-username
smokeBin: my-app
smokeArgs: [help, --json]
smokeExpectedJson:
command: help
status: READY
publicFiles:
- from: packages/app/README.md
to: README.md
mode: preserve
- from: packages/app/package.json
to: package.json
mode: preserve
- from: packages/app/LICENSE
to: LICENSE
mode: preserve
requiredPublicFiles: [README.md, package.json, LICENSE]
previousPublicBaseline:
mode: none
production:
branchTemplate: release/{tag}
releaseTitleTemplate: "{unit} {version}"
- id: my-plugin
source: packages/plugin
publicRepo: owner/my-plugin
version:
source: packages/plugin/package.json
tagTemplate: my-plugin-v{version}
distributions:
# Declare plugin consumers only when the unit ships a plugin.
# The CLI smoke is independent; only declare smokeBin when the plugin
# package also exposes a CLI binary.
- type: claude-plugin
plugin: my-plugin
marketplace: my-plugin
entrySkill: my-plugin-help
timeoutMs: 300000 # optional; range 30000-900000; default 300000
- type: codex-plugin
plugin: my-plugin
marketplace: my-plugin
entrySkill: my-plugin-help
timeoutMs: 300000 # optional; range 30000-900000; default 300000
publicFiles:
- from: packages/plugin/.claude-plugin/plugin.json
to: .claude-plugin/plugin.json
mode: preserve
- from: packages/plugin/.claude-plugin/marketplace.json
to: .claude-plugin/marketplace.json
mode: preserve
- from: packages/plugin/.codex-plugin/plugin.json
to: .codex-plugin/plugin.json
mode: preserve
- from: packages/plugin/.agents/plugins/marketplace.json
to: .agents/plugins/marketplace.json
mode: preserve
- from: packages/plugin/skills/my-plugin-help/SKILL.md
to: skills/my-plugin-help/SKILL.md
mode: preserve
- from: packages/plugin/README.md
to: README.md
mode: preserve
- from: packages/plugin/package.json
to: package.json
mode: preserve
- from: packages/plugin/LICENSE
to: LICENSE
mode: preserve
requiredPublicFiles:
- .claude-plugin/plugin.json
- .claude-plugin/marketplace.json
- .codex-plugin/plugin.json
- .agents/plugins/marketplace.json
- skills/my-plugin-help/SKILL.md
- README.md
- package.json
- LICENSE
previousPublicBaseline:
mode: none
production:
branchTemplate: release/{tag}
releaseTitleTemplate: "{unit} {version}"
Each plugin unit must list its Claude/Codex plugin.json, marketplace.json,
the entry Skill, and all required public files. A CLI smoke (smokeBin) is
optional for plugin units and only applies when the published npm package
exposes a CLI binary.
Plugin distributions may declare timeoutMs (range 30,000--900,000 ms; default
300,000 ms). This sets the subprocess timeout for the marketplace add, plugin
install, and plugin list commands. On real networks these commands can take
40--105 seconds; the default 300-second timeout avoids false PARTIAL failures.
The resolved value is frozen into the plan and approved along with all other
action parameters. Old plans without timeoutMs default to 300,000 ms at
execution time for backward compatibility.
PARTIAL recovery and reconcile
When publish succeeds at some checkpoints but fails at others, the run enters
PARTIAL status. Do not restart from scratch and do not delete remote state
(e.g., do not delete a tag that was already pushed, or unpublish a package).
Instead, use reconcile to inspect actual remote state, skip already-consistent
steps, and safely retry incomplete actions:
RECONCILE_JSON=$("${CLI[@]}" reconcile --root "$PROJECT" \
--run "$PUBLISH_RUN_PATH" \
--plan "$PLAN_PATH" \
--approval "$APPROVAL_PATH" \
--confirm-production "$PLAN_DIGEST" --json)
printf '%s\n' "$RECONCILE_JSON" | jq .
RECONCILE_RUN_PATH=$(printf '%s\n' "$RECONCILE_JSON" | jq -r '.runPath')
# Save reconcile's new runPath, then perform the fresh install verification.
"${CLI[@]}" verify --root "$PROJECT" \
--plan "$PLAN_PATH" --run "$RECONCILE_RUN_PATH" \
--acknowledge-gate-side-effects --json
Omit the verify acknowledgement only when the frozen plan has neither consumer
gates nor an npm smokeBin. The variables above are the exact values captured
by the main flow; if approval expired during recovery, create a fresh approval
for the same immutable plan and replace APPROVAL_PATH before reconcile.
reconcile queries the actual remote state (Git refs, npm version, GitHub
Release, marketplace install), skips any step whose evidence already matches
the frozen plan, and retries only safe and incomplete steps. Remote conflicts
(e.g., an unexpected tag or npm version) require human decision and cannot be
auto-resolved.
Successful reconcile returns PUBLISHED, not VERIFIED; only the fresh
verify run may produce the terminal VERIFIED state.
Accepted capabilities
- validates project configuration and release units;
- reports readiness without changing the project during
assess; - copies configured public files into an isolated snapshot;
- discovers first-use candidates read-only and creates a config only once after
exact
setupDigestconfirmation; - runs human-selected project gates in frozen-snapshot copies and exact consumer installation roots;
- checks required files, path safety, exact bytes/modes, and obvious leaks;
- records Git/workspace identity and freezes a digest-bound release plan;
- binds approval to the plan digest, expiry, and explicit action allowlist;
- publishes only frozen Git objects and npm tarballs, then checks remote commit/tree/tag/integrity;
- installs configured Claude/Codex plugins from the frozen Git ref and proves the entry Skill and payload digest in fresh isolated consumer homes;
- distinguishes
PUBLISHED(writes completed) fromVERIFIED(remote and consumer installation evidence completed); - stops subsequent checkpoints on failure and writes a separate run record without mutating the frozen plan or undoing successful remote actions.
Project-specific verification: hooks and gates
hooks.docs/build/test/typecheck/lint run before the snapshot is frozen. Use
them only for work that genuinely needs the parent workspace or generates
source files. They can modify files or access the network, so prepare requires
--acknowledge-hook-side-effects.
verificationGates are the controlled extension point for release calibration:
verificationGates:
- id: package-contract
phase: snapshot-verify
scope: { unit: my-project }
command:
- node
- -e
- "const p=require('./package.json'); if (!p.name) process.exit(1)"
cwd: .
timeoutMs: 120000
envAllowlist: [CI]
- id: installed-help
phase: consumer-verify
scope: { unit: my-project, distribution: npm }
command: [node, scripts/check-installed-help.mjs]
cwd: .
timeoutMs: 30000
envAllowlist: []
expectedJson: { status: READY }
The snapshot example is deliberately self-contained and reads only a mapped
public file. Any replacement script and every dependency it needs must exist
in the frozen public snapshot. The consumer script must likewise be present in
the exact installed distribution; gates cannot borrow tests, development
dependencies, or node_modules from the parent workspace.
snapshot-verify runs in a disposable writable copy of the frozen public
snapshot. consumer-verify runs from an exact isolated npm/Claude/Codex install
root. Both use executable arrays instead of shell strings; definitions and
results enter digest-bound evidence, and prepare/verify require
--acknowledge-gate-side-effects. Gates are still project processes without a
network sandbox, so release-skill cannot promise that they will not write files
or access the network. Push, tag, default-branch changes, GitHub Releases, and
npm publish may never be hooks/gates; they remain controlled plan actions.
What it does not do yet
- no automatic README generation or source-file overwrite;
- no automatic conflict merge or rollback workflow;
- no claim that a real production canary has run for marketplace verification;
prepare --onlineobserves previous public baselines (bound mode) and defers remote uniqueness checks to publish global preflight;- no overwrite of branches/tags/releases or npm unpublish; create-only refs use
--force-with-lease=<ref>:solely as an atomic compare-and-set assertion that the ref is absent, while existing branches use an ordinary non-force push; - no promise of Windows or broad multi-platform native write support;
- no hidden commit, push, tag, release, or package publication.
Write Safety
setup is read-only by default and may create a config only once after exact
digest confirmation. assess is read-only unless an explicit report output is requested. prepare
writes local files under .release-skill/; it does not write project source
files or remote services. If hooks are configured, they are arbitrary local
processes and require --acknowledge-hook-side-effects; hooks may have their
own filesystem or network side effects. Gates are also project processes and
require --acknowledge-gate-side-effects; they may have the same side effects.
publish is the production write entry
and requires both approval and the current plan digest. Omit hooks and use local
sandbox targets for the smallest safe rehearsal.
If something fails
| Result | What to do |
|---|---|
NEEDS_INPUT |
Complete setup's repository, tag, channel, baseline, and gate decisions. |
LOCAL_ONLY_DETECTED |
Establish a remote channel or keep only a local configuration design; do not claim production readiness. |
SETUP_DIGEST_MISMATCH |
Facts or answers changed; rerun dry-run, review, and confirm the new digest. |
CONFIG_EXISTS |
Setup never overwrites the existing config; assess it and edit incrementally. |
SAFE_WRITE_UNAVAILABLE |
Automatic create-once setup is unsupported on this platform; keep the dry-run report and create the reviewed config manually without overwriting an existing file. |
CONFIG_INVALID |
Correct .release-skill/project.yaml, then rerun assess. |
PUBLIC_FILE_MISSING |
Add or correct the configured public file. |
FORBIDDEN_CONTENT_DETECTED |
Remove the leaked/private content, then prepare again. |
SNAPSHOT_FIDELITY_FAILED |
Inspect the source/snapshot path and rerun prepare. |
BASELINE_CHANGED |
Keep the human edit, then prepare, review, and approve again. |
GATE_FAILED during prepare |
Fix the snapshot gate or frozen public artifact, then run a new prepare; the failed plan cannot be approved. |
GATE_FAILED during verify |
If the consumer environment failed, repair it and rerun verify from the same PUBLISHED run. If the published artifact is defective, release a new patch version; never overwrite it. |
PARTIAL |
Do not restart or delete remote state; review the returned runPath and run reconcile (see above). |
PUBLISHED |
Run verify --plan <planPath> --run <publishRunPath>; this is not terminal success. |
VERIFIED |
Remote state, exact npm install, and configured plugin consumer installs all matched the frozen plan. |
Skills
release-help: environment check and next-step guidance.release-setup: read-only discovery, human calibration, and create-once first-use configuration.release-assess: read-only release readiness report.release-prepare: local snapshot and reviewable release plan.release-publish: approved, digest-confirmed frozen GitHub+npm publishing.release-reconcile: evidence-based PARTIAL recovery with human intervention on conflicts.release-verify: post-publish verification; onlyVERIFIEDis the happy end.
Conflicts still default to human intervention. The npm-installed release-skill
CLI is the supported user entry after v0.1.1 production publication; source
checkout remains the development/contributor fallback.
License
MIT. See LICENSE.