You are executing the Ship phase (6/6) of CocoBrew.
Before proceeding, verify that .cocoplus/ exists.
If not: output "CocoPlus not initialized in this directory. Run $pod init to begin." Then stop.
Read .cocoplus/lifecycle/meta.json. Verify phases_completed contains "review".
If not: output "The Review phase must be approved before shipping. Run $review first." Then stop.
Verify that required lifecycle artifact files exist on disk — meta.json alone is not sufficient:
- Check
.cocoplus/lifecycle/spec.mdexists. If not: output "spec.md is missing. Run$specto regenerate it before shipping." Then stop. - Check
.cocoplus/lifecycle/plan.mdexists. If not: output "plan.md is missing. Run$planto regenerate it before shipping." Then stop. - Check
.cocoplus/lifecycle/review.mdexists. If not: output "review.md is missing. Run$reviewto regenerate it before shipping." Then stop.
Read .cocoplus/lifecycle/review.md. Verify ## Approval Status contains APPROVED.
If not: output "Review has not been approved. Run $review to complete the approval process." Then stop.
Read .cocoplus/lifecycle/review-state.json if it exists. If any finding has severity: BLOCKED and resolved is not true:
Output: "Ship blocked: [N] unresolved BLOCKED finding(s) require a human decision before shipping. Run $review clear-blocked --id <finding-id> --rationale <text> to resolve each one." List each unresolved finding's ID and one-line summary. Then stop. BLOCKED findings are treated identically to BLOCKING findings — there is no override.
Determine Version
First inspect the current git branch and look for a semantic version token:
node -e "
const { spawnSync } = require('child_process');
const branch = spawnSync('git', ['branch', '--show-current'], { encoding: 'utf8' }).stdout.trim();
const match = branch.match(/(?:^|[^0-9])v?(\d+\.\d+\.\d+)(?:$|[^0-9])/);
console.log(JSON.stringify({ branch, version: match ? 'v' + match[1] : null }));
"
If the branch contains a version, normalize it to v<major>.<minor>.<patch> and use that as the release version unless the developer explicitly overrides it. Examples: feature/cocoplus-v1.2.0 and release/1.2.0 both resolve to v1.2.0.
If the branch does not contain a version, do not infer or auto-increment one. Prompt:
"The current git branch does not include a semantic version. What semantic version should this release be? (e.g., v1.2.0)"
Validate the developer's answer matches v<major>.<minor>.<patch> exactly. If it does not, re-prompt once. Use the developer's answer as the version.
Use existing tags only as context for the prompt:
node -e "
const { spawnSync } = require('child_process');
const tags = spawnSync('git', ['tag', '--list'], { encoding: 'utf8' }).stdout.trim().split(/\r?\n/).filter(Boolean);
const semver = tags.map(t => {
const m = t.match(/^v?(\d+)\.(\d+)\.(\d+)$/);
return m ? { tag: 'v' + m.slice(1).join('.'), nums: m.slice(1).map(Number) } : null;
}).filter(Boolean);
semver.sort((a,b)=>{ for(let i=0;i<3;i++){ if(a.nums[i]!==b.nums[i]) return b.nums[i]-a.nums[i]; } return 0; });
console.log(semver[0]?.tag || 'none');
"
When updating CHANGELOG.md, use the branch-derived release version. If that version already exists, append the new release notes under that version. If the version exists but the current month does not, create a new month subsection under that same version. If the branch has no version, ask for the version before editing the changelog.
Full Diff Review
Show the developer a summary of all changes since the last version tag (or initial commit):
node -e "
const { spawnSync } = require('child_process');
function git(args) {
const r = spawnSync('git', args, { encoding: 'utf8' });
return r.status === 0 ? r.stdout.trim() : '';
}
const tags = git(['tag', '--list']).split(/\r?\n/).filter(Boolean)
.map(t => {
const m = t.match(/^v?(\d+)\.(\d+)\.(\d+)$/);
return m ? { tag: t, nums: m.slice(1).map(Number) } : null;
}).filter(Boolean);
tags.sort((a,b)=>{ for(let i=0;i<3;i++){ if(a.nums[i]!==b.nums[i]) return b.nums[i]-a.nums[i]; } return 0; });
const base = tags[0]?.tag || git(['rev-list', '--max-parents=0', 'HEAD']).split(/\r?\n/)[0];
const range = base ? [base + '..HEAD', '--stat'] : ['--stat'];
process.stdout.write(git(['diff', ...range]));
"
Ask: "These are all changes included in this release. Proceed with shipping? (yes/no)" If no: stop.
Create Deployment Record
Write .cocoplus/lifecycle/deployment.md:
# Deployment Record
**Date:** [ISO 8601 timestamp]
**Phase:** Ship (6/6)
**Version:** [version]
**Phase ID:** ship-YYYYMMDD-001
## Changes Shipped
[Summary of git diff stat]
## Spec ID
[spec ID from meta.json]
## Plan ID
[plan ID from meta.json]
## Review Approval
Approved: [timestamp from review.md]
## Commit SHA
[will be filled after commit]
## PR URL
[if PR was created, URL here — otherwise "Not created"]
Create Final Commit and Tag
git add .cocoplus/lifecycle/deployment.md .cocoplus/lifecycle/meta.json .cocoplus/AGENTS.md
git commit -m "release: [version] — [brief description from project.md goal]"
git tag -a [version] -m "CocoPlus release [version]"
Optional PR Creation
Prompt: "Do you want to create a pull request? (yes/no)" If yes:
- Check if
ghCLI is available:which gh - If available:
gh pr create --title "[version]: [project name]" --body "$(cat .cocoplus/lifecycle/review.md)" - If not available: output "gh CLI not found. Install it from https://cli.github.com/ to create PRs automatically."
Update deployment.md with commit SHA and PR URL.
Prune Stale Worktrees
Remove any CocoHarvest worktrees that remain from the Build phase:
node -e "
const { spawnSync } = require('child_process');
try {
const out = spawnSync('git', ['worktree', 'list', '--porcelain'], { encoding: 'utf8' }).stdout;
const trees = out.split('\n\n').filter(Boolean);
let removed = 0;
for (const tree of trees) {
const lines = tree.trim().split('\n');
const wtPath = lines[0].replace(/^worktree /, '');
if (/[/\\\\]agent[/\\\\]stage-/.test(wtPath)) {
const r = spawnSync('git', ['worktree', 'remove', '--force', wtPath], { encoding: 'utf8' });
if (r.status === 0) removed++;
}
}
spawnSync('git', ['worktree', 'prune'], { encoding: 'utf8' });
if (!removed) console.log('No stale CocoHarvest worktrees found.');
} catch(e) { /* worktree cleanup is best-effort */ }
"
Update State
Update lifecycle/meta.json — add ship to phases_completed, set current_phase to "shipped".
Append to AGENTS.md (≤200 lines): Phase: SHIPPED — Version [version] — [timestamp]
Output:
Shipped successfully!
Version: [version]
Tag: [version]
PR: [URL or "not created"]
Full CocoBrew lifecycle complete: Spec → Plan → Build → Test → Review → Ship ✓
Anti-Rationalization
| Shortcut / Temptation | Why It Fails |
|---|---|
| Skip the full diff review and proceed to tag directly | Developer must consciously review what goes out — blind shipping can include unintended changes |
| Auto-increment the version when the branch has no version | Semantic versioning is a project decision; without a branch version, ask explicitly before changing release files |
| Create the git tag before the deployment record commit | Tag must point to the final commit that includes deployment.md — out-of-order tagging creates a misleading release point |
Exit Criteria
-
.cocoplus/lifecycle/deployment.mdexists with Version, Spec ID, Plan ID, and Review Approval fields - Git tag matching
v*.*.*exists pointing to the release commit -
.cocoplus/lifecycle/meta.jsoncurrent_phaseis"shipped"andphases_completedcontains"ship" -
.cocoplus/AGENTS.mdcontainsPhase: SHIPPEDwith version and timestamp -
$shipis blocked when anyBLOCKEDfinding inreview-state.jsonis unresolved, with no override path