@wipcomputer/wip-ai-devops-toolbox 1.9.67 → 1.9.69-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -31,6 +31,24 @@
31
31
 
32
32
 
33
33
 
34
+
35
+ ## 1.9.69-alpha.1 (2026-04-01)
36
+
37
+ alpha prerelease
38
+
39
+ ## 1.9.68 (2026-04-01)
40
+
41
+ # Release Notes: wip-ai-devops-toolbox v1.9.68
42
+
43
+ Closes #239
44
+
45
+ ## Four-track release pipeline
46
+
47
+ The release tool now supports four tracks: alpha, beta, hotfix, and stable. This replaces the single-track model where every release was public.
48
+
49
+ Alpha is silent (no public release notes by default). Beta publishes prerelease notes to the public repo. Hotfix publishes to npm @latest without syncing code to public. Stable is the full deploy: npm + code sync + release notes. Developers can iterate on private, ship betas to testers, and only go public when ready.
50
+
51
+ Version numbering uses standard semver prereleases: `1.9.68-alpha.1`, `1.9.68-beta.1`. The installer (`ldm install --beta` / `--alpha`) pulls the right tag from npm.
34
52
 
35
53
  ## 1.9.67 (2026-03-31)
36
54
 
@@ -45,11 +45,45 @@ CLI is the universal fallback. MCP and plugin wrappers are optimizations.
45
45
  6. Merge PR: gh pr merge <number> --merge --delete-branch
46
46
  7. Rename merged branch: (see Post-Merge Branch Rename below)
47
47
  8. Pull merged main: git checkout main && git pull origin main
48
- 9. Release: wip-release patch
49
- # wip-release auto-detects the RELEASE-NOTES file
48
+ 9. Release: wip-release patch (stable: full pipeline)
49
+ wip-release alpha (prerelease: npm @alpha, silent)
50
+ wip-release beta (prerelease: npm @beta, public notes)
51
+ wip-release hotfix (urgent: npm @latest, no deploy-public)
50
52
  # flags: --dry-run (preview), --no-publish (bump + tag only)
51
53
  ```
52
54
 
55
+ ### Release Tracks
56
+
57
+ Four release tracks. Choose the right one for the situation:
58
+
59
+ | Track | Command | npm tag | Public code sync | When to use |
60
+ |-------|---------|---------|-----------------|-------------|
61
+ | Alpha | `wip-release alpha` | @alpha | No | Internal testing, not ready for users |
62
+ | Beta | `wip-release beta` | @beta | No | External testing, release candidate |
63
+ | Hotfix | `wip-release hotfix` | @latest | No | Urgent fix, skip deploy-public |
64
+ | Stable | `wip-release patch/minor/major` | @latest | Yes | Normal release, full pipeline |
65
+
66
+ **Alpha** is silent by default. No public release notes, no code sync. Add `--release-notes` to create a prerelease on the public GitHub repo.
67
+
68
+ **Beta** publishes prerelease notes to the public GitHub repo by default. Add `--no-release-notes` to skip.
69
+
70
+ **Hotfix** publishes to npm @latest and creates a release on the public GitHub repo, but does NOT run deploy-public (no code sync). Use this when you need a fix in npm immediately but the public repo code can wait for the next stable release.
71
+
72
+ **Stable** is the existing behavior. Full pipeline: npm @latest, deploy-public (code sync), full release notes.
73
+
74
+ Version numbering:
75
+ - Alpha: `1.9.68-alpha.1`, `1.9.68-alpha.2` (increments on repeat)
76
+ - Beta: `1.9.68-beta.1`, `1.9.68-beta.2` (increments on repeat)
77
+ - Hotfix: normal patch bump (`1.9.67` -> `1.9.68`)
78
+ - Stable: normal bump (patch/minor/major)
79
+
80
+ Install integration:
81
+ ```bash
82
+ ldm install # checks @latest (stable + hotfix)
83
+ ldm install --beta # checks @beta
84
+ ldm install --alpha # checks @alpha
85
+ ```
86
+
53
87
  **Important:**
54
88
  - **Every change goes through a PR.** No direct pushes to main. Not even "just a README fix." Branch, PR, merge. Every time.
55
89
  - **Never squash merge.** Every commit has co-authors and tells the story of how something was built. Squashing destroys attribution and history. Always use `--merge` or fast-forward. This applies to `gh pr merge`, manual merges, deploy-public.sh, and any other merge path. No exceptions. Always include `--delete-branch` so the PR branch is cleaned up automatically.
package/SKILL.md CHANGED
@@ -5,7 +5,7 @@ license: MIT
5
5
  interface: [cli, module, mcp, skill, hook, plugin]
6
6
  metadata:
7
7
  display-name: "WIP AI DevOps Toolbox"
8
- version: "1.9.67"
8
+ version: "1.9.68"
9
9
  homepage: "https://github.com/wipcomputer/wip-ai-devops-toolbox"
10
10
  author: "Parker Todd Brooks"
11
11
  category: dev-tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ai-devops-toolbox",
3
- "version": "1.9.67",
3
+ "version": "1.9.69-alpha.1",
4
4
  "type": "module",
5
5
  "description": "The complete AI DevOps toolkit for AI-assisted development teams.",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/deploy-public",
3
- "version": "1.9.67",
3
+ "version": "1.9.68",
4
4
  "description": "Private-to-public repo sync. Excludes ai/ folder, creates PR, merges, cleans up branches.",
5
5
  "bin": {
6
6
  "deploy-public": "./deploy-public.sh"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/post-merge-rename",
3
- "version": "1.9.67",
3
+ "version": "1.9.68",
4
4
  "description": "Post-merge branch renaming. Appends --merged-YYYY-MM-DD to preserve history.",
5
5
  "bin": {
6
6
  "post-merge-rename": "./post-merge-rename.sh"
@@ -136,6 +136,8 @@ const ALLOWED_BASH_PATTERNS = [
136
136
  /\brm\s+.*\.(openclaw|ldm)\/extensions\//, // cleaning deployed extensions (managed by ldm install, not source code)
137
137
  /\bclaude\s+mcp\b/, // MCP registration, not repo files
138
138
  /\bmkdir\s+.*\.worktrees\b/, // creating .worktrees/ directory is part of the process
139
+ /\brm\s+.*\.trash-approved-to-rm/, // Parker's approved-for-deletion folder (only Parker moves files here, agents only rm)
140
+ /\brm\s+.*\/_trash\//, // agent trash directories (agents can mv here and rm here)
139
141
  ];
140
142
 
141
143
  // Workflow steps for error messages (#213)
@@ -320,7 +322,9 @@ Use the proper workflow: edit files in a worktree, commit, push, PR.`);
320
322
  // Block npm install -g right after a release (#73)
321
323
  // wip-release writes ~/.ldm/state/.last-release on completion.
322
324
  // If a release happened < 5 minutes ago, block install unless user explicitly said "install".
323
- if (/\bnpm\s+install\s+-g\b/.test(cmd)) {
325
+ // Exception: prerelease installs (@alpha, @beta) skip the cooldown. The cooldown exists
326
+ // to enforce dogfooding stable releases. Prerelease installs ARE the dogfooding.
327
+ if (/\bnpm\s+install\s+-g\b/.test(cmd) && !/@(alpha|beta)\b/.test(cmd)) {
324
328
  try {
325
329
  const releasePath = join(process.env.HOME || '', '.ldm', 'state', '.last-release');
326
330
  if (existsSync(releasePath)) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-branch-guard",
3
- "version": "1.9.67",
3
+ "version": "1.9.68",
4
4
  "description": "PreToolUse hook that blocks all writes on main branch. Forces agents to work on branches or worktrees.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-file-guard",
3
- "version": "1.9.67",
3
+ "version": "1.9.68",
4
4
  "type": "module",
5
5
  "description": "Hook that blocks destructive edits to protected identity files. For Claude Code CLI and OpenClaw.",
6
6
  "main": "guard.mjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-license-guard",
3
- "version": "1.9.67",
3
+ "version": "1.9.68",
4
4
  "description": "License compliance for your own repos. Ensures correct copyright, dual-license blocks, and LICENSE files.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-license-hook",
3
- "version": "1.9.67",
3
+ "version": "1.9.68",
4
4
  "description": "License rug-pull detection and dependency license compliance for open source projects",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-readme-format",
3
- "version": "1.9.67",
3
+ "version": "1.9.68",
4
4
  "description": "Reformat any repo's README to follow the WIP Computer standard. Agent-first, human-readable.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,15 +3,47 @@
3
3
 
4
4
  Detailed usage, pipeline steps, flags, auth, and module API.
5
5
 
6
+ ## Release Tracks
7
+
8
+ Four release tracks, each with different behavior:
9
+
10
+ | Track | npm tag | Public code sync | Public release notes | Default notes |
11
+ |-------|---------|-----------------|---------------------|---------------|
12
+ | Alpha | @alpha | No | No (opt in with --release-notes) | Silent |
13
+ | Beta | @beta | No | Yes, prerelease (opt out with --no-release-notes) | Visible |
14
+ | Hotfix | @latest | No | Yes (opt out with --no-release-notes) | Visible |
15
+ | Stable | @latest | Yes, full deploy-public | Yes, full notes | Full deploy |
16
+
17
+ ### Version numbering
18
+
19
+ - Alpha: `1.9.68-alpha.1`, `1.9.68-alpha.2` (increments on repeat)
20
+ - Beta: `1.9.68-beta.1`, `1.9.68-beta.2` (increments on repeat)
21
+ - Hotfix: normal patch bump (`1.9.67` -> `1.9.68`)
22
+ - Stable: normal bump (patch/minor/major)
23
+
6
24
  ## Usage
7
25
 
8
26
  Run from inside any repo:
9
27
 
10
28
  ```bash
29
+ # Stable (existing behavior)
11
30
  wip-release patch # 1.0.0 -> 1.0.1
12
31
  wip-release minor # 1.0.0 -> 1.1.0
13
32
  wip-release major # 1.0.0 -> 2.0.0
14
33
 
34
+ # Alpha
35
+ wip-release alpha # 1.0.1-alpha.1 (npm @alpha, silent)
36
+ wip-release alpha --release-notes # 1.0.1-alpha.1 (npm @alpha + prerelease on public)
37
+
38
+ # Beta
39
+ wip-release beta # 1.0.1-beta.1 (npm @beta + prerelease on public)
40
+ wip-release beta --no-release-notes # 1.0.1-beta.1 (npm @beta, skip notes)
41
+
42
+ # Hotfix
43
+ wip-release hotfix # 1.0.0 -> 1.0.1 (npm @latest + release on public)
44
+ wip-release hotfix --no-release-notes # skip public release notes
45
+
46
+ # Common flags
15
47
  wip-release patch --notes="fix auth config" # with changelog note
16
48
  wip-release minor --dry-run # preview, no changes
17
49
  wip-release patch --no-publish # bump + tag only
@@ -19,6 +51,8 @@ wip-release patch --no-publish # bump + tag only
19
51
 
20
52
  ## What It Does
21
53
 
54
+ ### Stable pipeline
55
+
22
56
  ```
23
57
  wip-grok: 1.0.0 -> 1.0.1 (patch)
24
58
  ────────────────────────────────────────
@@ -28,32 +62,97 @@ wip-release patch --no-publish # bump + tag only
28
62
  ✓ Committed and tagged v1.0.1
29
63
  ✓ Pushed to remote
30
64
  ✓ Published to npm
31
- Published to GitHub Packages
65
+ - GitHub Packages: handled by deploy-public.sh
32
66
  ✓ GitHub release v1.0.1 created
33
67
  ✓ Published to ClawHub
34
68
 
35
69
  Done. wip-grok v1.0.1 released.
36
70
  ```
37
71
 
72
+ ### Alpha/beta pipeline
73
+
74
+ ```
75
+ wip-grok: 1.0.0 -> 1.0.1-alpha.1 (alpha)
76
+ ────────────────────────────────────────
77
+ ✓ package.json -> 1.0.1-alpha.1
78
+ ✓ CHANGELOG.md updated
79
+ ✓ Committed and tagged v1.0.1-alpha.1
80
+ ✓ Pushed to remote
81
+ ✓ Published to npm @alpha
82
+ - GitHub prerelease: skipped (silent alpha)
83
+
84
+ Done. wip-grok v1.0.1-alpha.1 (alpha) released.
85
+ ```
86
+
87
+ ### Hotfix pipeline
88
+
89
+ ```
90
+ wip-grok: 1.0.0 -> 1.0.1 (hotfix)
91
+ ────────────────────────────────────────
92
+ ✓ package.json -> 1.0.1
93
+ ✓ SKILL.md -> 1.0.1
94
+ ✓ CHANGELOG.md updated
95
+ ✓ Committed and tagged v1.0.1
96
+ ✓ Pushed to remote
97
+ ✓ Published to npm @latest
98
+ ✓ GitHub release v1.0.1 created on public repo
99
+ - deploy-public: skipped (hotfix)
100
+
101
+ Done. wip-grok v1.0.1 (hotfix) released.
102
+ ```
103
+
38
104
  ## Pipeline Steps
39
105
 
106
+ ### Stable (patch/minor/major)
107
+
40
108
  1. **Bump `package.json`** ... patch, minor, or major
41
109
  2. **Sync `SKILL.md`** ... updates version in YAML frontmatter (if file exists)
42
110
  3. **Update `CHANGELOG.md`** ... prepends new version entry with date and notes
43
111
  4. **Git commit + tag** ... commits changed files, creates `vX.Y.Z` tag
44
112
  5. **Push** ... pushes commit and tag to remote
45
- 6. **npm publish** ... publishes to npmjs.com (auth via 1Password)
46
- 7. **GitHub Packages** ... publishes to npm.pkg.github.com
47
- 8. **GitHub release** ... creates release with changelog notes
113
+ 6. **npm publish** ... publishes to npmjs.com with @latest (auth via 1Password)
114
+ 7. **GitHub Packages** ... handled by deploy-public.sh
115
+ 8. **GitHub release** ... creates release on private repo with changelog notes
48
116
  9. **ClawHub publish** ... publishes skill to ClawHub (if SKILL.md exists)
117
+ 10. **Branch cleanup** ... renames/prunes merged branches
118
+ 11. **Worktree cleanup** ... prunes merged worktrees
119
+
120
+ ### Alpha/Beta
121
+
122
+ 1. **Bump `package.json`** ... adds prerelease suffix (-alpha.N / -beta.N)
123
+ 2. **Update `CHANGELOG.md`** ... lightweight prerelease entry
124
+ 3. **Git commit + tag** ... commits changed files, creates tag
125
+ 4. **Push** ... pushes commit and tag to remote
126
+ 5. **npm publish** ... publishes with --tag alpha or --tag beta
127
+ 6. **GitHub prerelease** ... (beta: on by default, alpha: opt-in with --release-notes)
128
+
129
+ ### Hotfix
130
+
131
+ 1. **Bump `package.json`** ... patch bump (no suffix)
132
+ 2. **Sync `SKILL.md`** ... updates version in YAML frontmatter
133
+ 3. **Update `CHANGELOG.md`** ... prepends new version entry
134
+ 4. **Git commit + tag** ... commits changed files, creates tag
135
+ 5. **Push** ... pushes commit and tag to remote
136
+ 6. **npm publish** ... publishes with --tag latest
137
+ 7. **GitHub release** ... creates release on public repo (opt out with --no-release-notes)
138
+ 8. **ClawHub publish** ... publishes skill to ClawHub (if SKILL.md exists)
139
+ 9. **No deploy-public** ... code sync is skipped
49
140
 
50
141
  ## Flags
51
142
 
52
143
  | Flag | What |
53
144
  |------|------|
54
145
  | `--notes="text"` | Changelog entry text |
146
+ | `--notes-file=path` | Read release narrative from a markdown file |
147
+ | `--release-notes` | Opt in to public release notes (alpha only) |
148
+ | `--no-release-notes` | Opt out of public release notes (beta, hotfix) |
55
149
  | `--dry-run` | Show what would happen, change nothing |
56
150
  | `--no-publish` | Bump + tag only, skip npm and GitHub release |
151
+ | `--skip-product-check` | Skip product docs gate (stable only) |
152
+ | `--skip-stale-check` | Skip stale remote branch check (stable only) |
153
+ | `--skip-worktree-check` | Skip worktree guard |
154
+ | `--skip-tech-docs-check` | Skip technical docs check (stable only) |
155
+ | `--skip-coverage-check` | Skip interface coverage check (stable only) |
57
156
 
58
157
  ## Auth
59
158
 
@@ -66,15 +165,26 @@ Requires:
66
165
  - `gh` CLI authenticated (for GitHub Packages and releases)
67
166
  - `clawhub` CLI authenticated (for ClawHub skill publishing)
68
167
 
168
+ ## ldm install Integration
169
+
170
+ The installer checks different npm tags based on the track:
171
+
172
+ ```bash
173
+ ldm install # checks @latest (stable + hotfix)
174
+ ldm install --beta # checks @beta tag
175
+ ldm install --alpha # checks @alpha tag
176
+ ```
177
+
69
178
  ## As a Module
70
179
 
71
180
  ```javascript
72
- import { release, detectCurrentVersion, bumpSemver } from '@wipcomputer/wip-release';
181
+ import { release, releasePrerelease, releaseHotfix, detectCurrentVersion, bumpSemver, bumpPrerelease } from '@wipcomputer/wip-release';
73
182
 
74
183
  const current = detectCurrentVersion('/path/to/repo');
75
184
  const next = bumpSemver(current, 'minor');
76
185
  console.log(`${current} -> ${next}`);
77
186
 
187
+ // Stable release
78
188
  await release({
79
189
  repoPath: '/path/to/repo',
80
190
  level: 'patch',
@@ -82,19 +192,54 @@ await release({
82
192
  dryRun: false,
83
193
  noPublish: false,
84
194
  });
195
+
196
+ // Alpha prerelease
197
+ await releasePrerelease({
198
+ repoPath: '/path/to/repo',
199
+ track: 'alpha',
200
+ notes: 'testing new feature',
201
+ dryRun: false,
202
+ noPublish: false,
203
+ publishReleaseNotes: false,
204
+ });
205
+
206
+ // Beta prerelease
207
+ await releasePrerelease({
208
+ repoPath: '/path/to/repo',
209
+ track: 'beta',
210
+ notes: 'beta candidate',
211
+ dryRun: false,
212
+ noPublish: false,
213
+ publishReleaseNotes: true,
214
+ });
215
+
216
+ // Hotfix
217
+ await releaseHotfix({
218
+ repoPath: '/path/to/repo',
219
+ notes: 'critical fix',
220
+ dryRun: false,
221
+ noPublish: false,
222
+ publishReleaseNotes: true,
223
+ });
85
224
  ```
86
225
 
87
226
  ## Exports
88
227
 
89
228
  | Function | What |
90
229
  |----------|------|
91
- | `release({ repoPath, level, notes, dryRun, noPublish })` | Full pipeline |
230
+ | `release({ repoPath, level, notes, dryRun, noPublish })` | Full stable pipeline |
231
+ | `releasePrerelease({ repoPath, track, notes, dryRun, noPublish, publishReleaseNotes })` | Alpha/beta pipeline |
232
+ | `releaseHotfix({ repoPath, notes, dryRun, noPublish, publishReleaseNotes })` | Hotfix pipeline |
92
233
  | `detectCurrentVersion(repoPath)` | Read version from package.json |
93
- | `bumpSemver(version, level)` | Bump a semver string |
234
+ | `bumpSemver(version, level)` | Bump a semver string (patch/minor/major) |
235
+ | `bumpPrerelease(version, track)` | Bump a prerelease version (alpha/beta) |
94
236
  | `syncSkillVersion(repoPath, newVersion)` | Update SKILL.md frontmatter |
95
237
  | `updateChangelog(repoPath, newVersion, notes)` | Prepend to CHANGELOG.md |
96
- | `publishNpm(repoPath)` | Publish to npmjs.com |
238
+ | `publishNpm(repoPath)` | Publish to npmjs.com (@latest) |
239
+ | `publishNpmWithTag(repoPath, tag)` | Publish to npmjs.com with specific tag |
97
240
  | `publishGitHubPackages(repoPath)` | Publish to npm.pkg.github.com |
98
- | `createGitHubRelease(repoPath, newVersion, notes, currentVersion)` | Create GitHub release with rich notes |
241
+ | `createGitHubRelease(repoPath, newVersion, notes, currentVersion)` | Create GitHub release on private repo |
242
+ | `createGitHubPrerelease(repoPath, newVersion, notes)` | Create GitHub prerelease on public repo |
243
+ | `createGitHubReleaseOnPublic(repoPath, newVersion, notes, currentVersion)` | Create GitHub release on public repo |
99
244
  | `buildReleaseNotes(repoPath, currentVersion, newVersion, notes)` | Generate detailed release notes |
100
245
  | `publishClawHub(repoPath, newVersion, notes)` | Publish skill to ClawHub |
@@ -46,6 +46,8 @@ Local release pipeline. One command bumps version, updates all docs, publishes e
46
46
  - Releasing a new version of any @wipcomputer package
47
47
  - After merging a PR to main and you need to publish
48
48
  - Bumping version + changelog + SKILL.md in one step
49
+ - Publishing alpha or beta prereleases for testing
50
+ - Pushing hotfixes to npm without full deploy-public
49
51
 
50
52
  **Use --dry-run for:**
51
53
  - Previewing what a release would do before committing
@@ -55,18 +57,48 @@ Local release pipeline. One command bumps version, updates all docs, publishes e
55
57
 
56
58
  ### Do NOT Use For
57
59
 
58
- - Pre-release / alpha versions (not yet supported)
59
60
  - Repos without a package.json
60
61
 
62
+ ## Release Tracks
63
+
64
+ Four release tracks. Each has different behavior for npm tags, public repo sync, and release notes.
65
+
66
+ | Track | Command | npm tag | Public code sync | Public release notes |
67
+ |-------|---------|---------|-----------------|---------------------|
68
+ | Alpha | `wip-release alpha` | @alpha | No | No (opt in with --release-notes) |
69
+ | Beta | `wip-release beta` | @beta | No | Yes, prerelease (opt out with --no-release-notes) |
70
+ | Hotfix | `wip-release hotfix` | @latest | No | Yes (opt out with --no-release-notes) |
71
+ | Stable | `wip-release patch/minor/major` | @latest | Yes (deploy-public) | Yes, full notes |
72
+
73
+ ### Version numbering
74
+
75
+ - Alpha: `1.9.68-alpha.1`, `1.9.68-alpha.2`, etc.
76
+ - Beta: `1.9.68-beta.1`, `1.9.68-beta.2`, etc.
77
+ - Hotfix: normal version bump (patch)
78
+ - Stable: normal version bump (patch/minor/major)
79
+
61
80
  ## API Reference
62
81
 
63
82
  ### CLI
64
83
 
65
84
  ```bash
66
- wip-release patch --notes="fix X" # full pipeline
85
+ # Stable (existing behavior)
86
+ wip-release patch # full pipeline
67
87
  wip-release minor --dry-run # preview only
68
88
  wip-release major --no-publish # bump + tag only
69
89
  wip-release patch --skip-product-check # skip product docs gate
90
+
91
+ # Alpha
92
+ wip-release alpha # npm @alpha, no public notes
93
+ wip-release alpha --release-notes # npm @alpha + prerelease notes on public
94
+
95
+ # Beta
96
+ wip-release beta # npm @beta + prerelease notes on public
97
+ wip-release beta --no-release-notes # npm @beta, skip public notes
98
+
99
+ # Hotfix
100
+ wip-release hotfix # npm @latest + public release notes
101
+ wip-release hotfix --no-release-notes # npm @latest, skip public notes
70
102
  ```
71
103
 
72
104
  ### Product Docs Gate
@@ -108,9 +140,16 @@ After publishing, wip-release auto-copies SKILL.md to your website as plain text
108
140
  ### Module
109
141
 
110
142
  ```javascript
111
- import { release, detectCurrentVersion, bumpSemver, syncSkillVersion } from '@wipcomputer/wip-release';
143
+ import { release, releasePrerelease, releaseHotfix, detectCurrentVersion, bumpSemver, bumpPrerelease } from '@wipcomputer/wip-release';
112
144
 
145
+ // Stable release
113
146
  await release({ repoPath: '.', level: 'patch', notes: 'fix', dryRun: false, noPublish: false });
147
+
148
+ // Alpha/beta prerelease
149
+ await releasePrerelease({ repoPath: '.', track: 'alpha', notes: 'testing', dryRun: false, noPublish: false, publishReleaseNotes: false });
150
+
151
+ // Hotfix
152
+ await releaseHotfix({ repoPath: '.', notes: 'critical fix', dryRun: false, noPublish: false, publishReleaseNotes: true });
114
153
  ```
115
154
 
116
155
  ## Troubleshooting
@@ -5,10 +5,10 @@
5
5
  * Release tool CLI. Bumps version, updates docs, publishes.
6
6
  */
7
7
 
8
- import { release, detectCurrentVersion, collectMergedPRNotes } from './core.mjs';
8
+ import { release, releasePrerelease, releaseHotfix, detectCurrentVersion, collectMergedPRNotes } from './core.mjs';
9
9
 
10
10
  const args = process.argv.slice(2);
11
- const level = args.find(a => ['patch', 'minor', 'major'].includes(a));
11
+ const level = args.find(a => ['patch', 'minor', 'major', 'alpha', 'beta', 'hotfix'].includes(a));
12
12
 
13
13
  function flag(name) {
14
14
  const prefix = `--${name}=`;
@@ -23,6 +23,8 @@ const skipStaleCheck = args.includes('--skip-stale-check');
23
23
  const skipWorktreeCheck = args.includes('--skip-worktree-check');
24
24
  const skipTechDocsCheck = args.includes('--skip-tech-docs-check');
25
25
  const skipCoverageCheck = args.includes('--skip-coverage-check');
26
+ const wantReleaseNotes = args.includes('--release-notes');
27
+ const noReleaseNotes = args.includes('--no-release-notes');
26
28
  const notesFilePath = flag('notes-file');
27
29
  let notes = flag('notes');
28
30
  // Bug fix #121: use strict check, not truthiness. --notes="" is empty, not absent.
@@ -51,13 +53,15 @@ let notesSource = (notes !== null && notes !== undefined && notes !== '') ? 'fla
51
53
  }
52
54
  notes = readFileSync(resolved, 'utf8').trim();
53
55
  notesSource = 'file';
54
- } else if (level) {
56
+ } else if (level && ['patch', 'minor', 'major', 'hotfix'].includes(level)) {
55
57
  // 2. Auto-detect RELEASE-NOTES-v{version}.md (ALWAYS checks, even if --notes provided)
58
+ // Only for stable levels and hotfix. Alpha/beta skip this.
56
59
  try {
57
60
  const { detectCurrentVersion, bumpSemver } = await import('./core.mjs');
58
61
  const cwd = process.cwd();
59
62
  const currentVersion = detectCurrentVersion(cwd);
60
- const newVersion = bumpSemver(currentVersion, level);
63
+ const bumpLevel = level === 'hotfix' ? 'patch' : level;
64
+ const newVersion = bumpSemver(currentVersion, bumpLevel);
61
65
  const dashed = newVersion.replace(/\./g, '-');
62
66
  const autoFile = join(cwd, `RELEASE-NOTES-v${dashed}.md`);
63
67
  if (existsSync(autoFile)) {
@@ -75,12 +79,13 @@ let notesSource = (notes !== null && notes !== undefined && notes !== '') ? 'fla
75
79
  // 2.5. Auto-combine release notes from merged PRs since last tag (#237)
76
80
  // Only runs when no single RELEASE-NOTES file was found on disk.
77
81
  // Scans git merge history for RELEASE-NOTES files committed on PR branches.
78
- if (level && notesSource !== 'file') {
82
+ if (level && ['patch', 'minor', 'major', 'hotfix'].includes(level) && notesSource !== 'file') {
79
83
  try {
80
84
  const { collectMergedPRNotes, detectCurrentVersion: dcv, bumpSemver: bs } = await import('./core.mjs');
81
85
  const cwd = process.cwd();
82
86
  const cv = dcv(cwd);
83
- const nv = bs(cv, level);
87
+ const bumpLevel = level === 'hotfix' ? 'patch' : level;
88
+ const nv = bs(cv, bumpLevel);
84
89
  const combined = collectMergedPRNotes(cwd, cv, nv);
85
90
  if (combined) {
86
91
  if (flagNotes && flagNotes !== combined.notes) {
@@ -144,27 +149,37 @@ if (!level || args.includes('--help') || args.includes('-h')) {
144
149
  console.log(`wip-release ... local release tool${current}
145
150
 
146
151
  Usage:
147
- wip-release patch 1.0.0 -> 1.0.1
148
- wip-release minor 1.0.0 -> 1.1.0
149
- wip-release major 1.0.0 -> 2.0.0
152
+ wip-release patch 1.0.0 -> 1.0.1 (stable)
153
+ wip-release minor 1.0.0 -> 1.1.0 (stable)
154
+ wip-release major 1.0.0 -> 2.0.0 (stable)
155
+ wip-release alpha 1.0.1-alpha.1 (prerelease)
156
+ wip-release beta 1.0.1-beta.1 (prerelease)
157
+ wip-release hotfix 1.0.0 -> 1.0.1 (hotfix, no deploy-public)
158
+
159
+ Release tracks:
160
+ alpha npm @alpha tag, no public notes (opt in with --release-notes)
161
+ beta npm @beta tag, prerelease notes on public (opt out with --no-release-notes)
162
+ hotfix npm @latest tag, release notes on public (opt out with --no-release-notes)
163
+ stable npm @latest tag, deploy-public, full notes (patch/minor/major)
150
164
 
151
165
  Flags:
152
166
  --notes="description" Release narrative (what was built and why)
153
167
  --notes-file=path Read release narrative from a markdown file
168
+ --release-notes Opt in to public release notes (alpha only)
169
+ --no-release-notes Opt out of public release notes (beta, hotfix)
154
170
  --dry-run Show what would happen, change nothing
155
171
  --no-publish Bump + tag only, skip npm/GitHub
156
172
  --skip-product-check Skip product docs check (dev update, roadmap, readme-first)
157
173
  --skip-stale-check Skip stale remote branch check
158
174
  --skip-worktree-check Skip worktree guard (allow release from worktree)
159
175
 
160
- Release notes (REQUIRED, must be a file on disk):
176
+ Release notes (REQUIRED for stable, optional for other tracks):
161
177
  1. --notes-file=path Explicit file path
162
178
  2. RELEASE-NOTES-v{ver}.md In repo root (auto-detected)
163
179
  3. Merged PR notes Auto-combined from git history (#237)
164
180
  4. ai/dev-updates/YYYY-MM-DD* Today's dev update (auto-detected)
165
- The --notes flag is NOT accepted. Write a file. Commit it on your branch.
166
- The file shows up in the PR diff so it can be reviewed before merge.
167
- When batching multiple PRs, each PR's RELEASE-NOTES are auto-combined.
181
+ For stable releases: the --notes flag is NOT accepted. Write a file.
182
+ For alpha/beta/hotfix: --notes="text" is accepted as a convenience.
168
183
 
169
184
  Skill publish to website:
170
185
  Add .publish-skill.json to repo root: { "name": "my-tool" }
@@ -172,7 +187,7 @@ Skill publish to website:
172
187
  After release, SKILL.md is copied to {website}/wip.computer/install/{name}.txt
173
188
  and deploy.sh is run to push to VPS.
174
189
 
175
- Pipeline:
190
+ Pipeline (stable):
176
191
  1. Bump package.json version
177
192
  2. Sync SKILL.md version (if exists)
178
193
  3. Update CHANGELOG.md
@@ -181,23 +196,64 @@ Pipeline:
181
196
  6. npm publish (via 1Password)
182
197
  7. GitHub Packages publish
183
198
  8. GitHub release create
184
- 9. Publish SKILL.md to website (if configured)`);
199
+ 9. Publish SKILL.md to website (if configured)
200
+
201
+ Pipeline (alpha/beta):
202
+ 1. Bump version with prerelease suffix (-alpha.N / -beta.N)
203
+ 2. npm publish with --tag alpha or --tag beta
204
+ 3. GitHub prerelease (beta default, alpha opt-in)
205
+
206
+ Pipeline (hotfix):
207
+ 1. Bump patch version (no suffix)
208
+ 2. npm publish with --tag latest
209
+ 3. GitHub release (no deploy-public)`);
185
210
  process.exit(level ? 0 : 1);
186
211
  }
187
212
 
188
- release({
189
- repoPath: process.cwd(),
190
- level,
191
- notes,
192
- notesSource,
193
- dryRun,
194
- noPublish,
195
- skipProductCheck,
196
- skipStaleCheck,
197
- skipWorktreeCheck,
198
- skipTechDocsCheck,
199
- skipCoverageCheck,
200
- }).catch(err => {
201
- console.error(` ✗ ${err.message}`);
202
- process.exit(1);
203
- });
213
+ // Route to the correct release function based on track
214
+ if (level === 'alpha' || level === 'beta') {
215
+ // Prerelease track: alpha or beta
216
+ releasePrerelease({
217
+ repoPath: process.cwd(),
218
+ track: level,
219
+ notes,
220
+ dryRun,
221
+ noPublish,
222
+ publishReleaseNotes: level === 'alpha' ? wantReleaseNotes : !noReleaseNotes,
223
+ }).catch(err => {
224
+ console.error(` \u2717 ${err.message}`);
225
+ process.exit(1);
226
+ });
227
+ } else if (level === 'hotfix') {
228
+ // Hotfix track: patch bump, @latest tag, no deploy-public
229
+ releaseHotfix({
230
+ repoPath: process.cwd(),
231
+ notes,
232
+ notesSource,
233
+ dryRun,
234
+ noPublish,
235
+ publishReleaseNotes: !noReleaseNotes,
236
+ skipWorktreeCheck,
237
+ }).catch(err => {
238
+ console.error(` \u2717 ${err.message}`);
239
+ process.exit(1);
240
+ });
241
+ } else {
242
+ // Stable track: patch, minor, major
243
+ release({
244
+ repoPath: process.cwd(),
245
+ level,
246
+ notes,
247
+ notesSource,
248
+ dryRun,
249
+ noPublish,
250
+ skipProductCheck,
251
+ skipStaleCheck,
252
+ skipWorktreeCheck,
253
+ skipTechDocsCheck,
254
+ skipCoverageCheck,
255
+ }).catch(err => {
256
+ console.error(` \u2717 ${err.message}`);
257
+ process.exit(1);
258
+ });
259
+ }
@@ -34,6 +34,37 @@ export function bumpSemver(version, level) {
34
34
  }
35
35
  }
36
36
 
37
+ /**
38
+ * Bump a version string for prerelease tracks (alpha, beta).
39
+ *
40
+ * If the current version already has the same prerelease prefix,
41
+ * increment the counter: 1.2.3-alpha.1 -> 1.2.3-alpha.2
42
+ *
43
+ * If the current version is a clean release or a different prerelease,
44
+ * bump patch and start at .1: 1.2.3 -> 1.2.4-alpha.1
45
+ */
46
+ export function bumpPrerelease(version, track) {
47
+ // Check if current version already has this prerelease prefix
48
+ const preMatch = version.match(new RegExp(`^(\\d+\\.\\d+\\.\\d+)-${track}\\.(\\d+)$`));
49
+ if (preMatch) {
50
+ // Same track: increment the counter
51
+ const base = preMatch[1];
52
+ const counter = parseInt(preMatch[2], 10);
53
+ return `${base}-${track}.${counter + 1}`;
54
+ }
55
+
56
+ // Strip any existing prerelease suffix to get the base version
57
+ const baseMatch = version.match(/^(\d+)\.(\d+)\.(\d+)/);
58
+ if (!baseMatch) throw new Error(`Cannot parse version: ${version}`);
59
+
60
+ const major = parseInt(baseMatch[1], 10);
61
+ const minor = parseInt(baseMatch[2], 10);
62
+ const patch = parseInt(baseMatch[3], 10);
63
+
64
+ // Bump patch and start prerelease at .1
65
+ return `${major}.${minor}.${patch + 1}-${track}.1`;
66
+ }
67
+
37
68
  /**
38
69
  * Write new version to package.json.
39
70
  */
@@ -200,6 +231,18 @@ export function publishNpm(repoPath) {
200
231
  ], { cwd: repoPath, stdio: 'inherit' });
201
232
  }
202
233
 
234
+ /**
235
+ * Publish to npm with a specific dist-tag (alpha, beta, latest).
236
+ */
237
+ export function publishNpmWithTag(repoPath, tag) {
238
+ const token = getNpmToken();
239
+ execFileSync('npm', [
240
+ 'publish', '--access', 'public',
241
+ '--tag', tag,
242
+ `--//registry.npmjs.org/:_authToken=${token}`
243
+ ], { cwd: repoPath, stdio: 'inherit' });
244
+ }
245
+
203
246
  /**
204
247
  * Publish to GitHub Packages.
205
248
  */
@@ -1018,6 +1061,61 @@ export function createGitHubRelease(repoPath, newVersion, notes, currentVersion)
1018
1061
  }
1019
1062
  }
1020
1063
 
1064
+ /**
1065
+ * Create a GitHub prerelease on the PUBLIC repo (no code sync).
1066
+ * Used by alpha (opt-in) and beta (default) tracks.
1067
+ */
1068
+ export function createGitHubPrerelease(repoPath, newVersion, notes) {
1069
+ const repoSlug = detectRepoSlug(repoPath);
1070
+ if (!repoSlug) throw new Error('Cannot detect repo slug from git remote');
1071
+
1072
+ // Target the public repo (strip -private suffix)
1073
+ const publicSlug = repoSlug.replace(/-private$/, '');
1074
+ const body = notes || `Prerelease ${newVersion}`;
1075
+
1076
+ const tmpFile = join(repoPath, '.release-notes-tmp.md');
1077
+ writeFileSync(tmpFile, body);
1078
+
1079
+ try {
1080
+ execFileSync('gh', [
1081
+ 'release', 'create', `v${newVersion}`,
1082
+ '--title', `v${newVersion}`,
1083
+ '--notes-file', '.release-notes-tmp.md',
1084
+ '--prerelease',
1085
+ '--repo', publicSlug
1086
+ ], { cwd: repoPath, stdio: 'inherit' });
1087
+ } finally {
1088
+ try { execFileSync('rm', ['-f', tmpFile]); } catch {}
1089
+ }
1090
+ }
1091
+
1092
+ /**
1093
+ * Create a GitHub release on the PUBLIC repo (no code sync).
1094
+ * Used by the hotfix track.
1095
+ */
1096
+ export function createGitHubReleaseOnPublic(repoPath, newVersion, notes, currentVersion) {
1097
+ const repoSlug = detectRepoSlug(repoPath);
1098
+ if (!repoSlug) throw new Error('Cannot detect repo slug from git remote');
1099
+
1100
+ // Target the public repo (strip -private suffix)
1101
+ const publicSlug = repoSlug.replace(/-private$/, '');
1102
+ const body = buildReleaseNotes(repoPath, currentVersion, newVersion, notes);
1103
+
1104
+ const tmpFile = join(repoPath, '.release-notes-tmp.md');
1105
+ writeFileSync(tmpFile, body);
1106
+
1107
+ try {
1108
+ execFileSync('gh', [
1109
+ 'release', 'create', `v${newVersion}`,
1110
+ '--title', `v${newVersion}`,
1111
+ '--notes-file', '.release-notes-tmp.md',
1112
+ '--repo', publicSlug
1113
+ ], { cwd: repoPath, stdio: 'inherit' });
1114
+ } finally {
1115
+ try { execFileSync('rm', ['-f', tmpFile]); } catch {}
1116
+ }
1117
+ }
1118
+
1021
1119
  /**
1022
1120
  * Publish skill to ClawHub.
1023
1121
  */
@@ -1900,3 +1998,371 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
1900
1998
 
1901
1999
  return { currentVersion, newVersion, dryRun: false };
1902
2000
  }
2001
+
2002
+ // ── Prerelease Track (Alpha / Beta) ────────────────────────────────
2003
+
2004
+ /**
2005
+ * Release an alpha or beta prerelease.
2006
+ *
2007
+ * Alpha: npm @alpha, no public release notes by default (opt in with publishReleaseNotes).
2008
+ * Beta: npm @beta, prerelease notes on public repo by default (opt out with publishReleaseNotes=false).
2009
+ *
2010
+ * No deploy-public. No code sync. No CHANGELOG gate. No product docs gate.
2011
+ * Lightweight: bump version, npm publish with tag, optional GitHub prerelease.
2012
+ */
2013
+ export async function releasePrerelease({ repoPath, track, notes, dryRun, noPublish, publishReleaseNotes }) {
2014
+ repoPath = repoPath || process.cwd();
2015
+ const currentVersion = detectCurrentVersion(repoPath);
2016
+ const newVersion = bumpPrerelease(currentVersion, track);
2017
+ const repoName = basename(repoPath);
2018
+
2019
+ console.log('');
2020
+ console.log(` ${repoName}: ${currentVersion} -> ${newVersion} (${track})`);
2021
+ console.log(` ${'─'.repeat(40)}`);
2022
+
2023
+ if (dryRun) {
2024
+ console.log(` [dry run] Would bump package.json to ${newVersion}`);
2025
+ if (!noPublish) {
2026
+ console.log(` [dry run] Would npm publish with --tag ${track}`);
2027
+ if (publishReleaseNotes) {
2028
+ console.log(` [dry run] Would create GitHub prerelease v${newVersion} on public repo`);
2029
+ } else {
2030
+ console.log(` [dry run] No GitHub prerelease (silent)`);
2031
+ }
2032
+ }
2033
+ console.log('');
2034
+ console.log(` Dry run complete. No changes made.`);
2035
+ console.log('');
2036
+ return { currentVersion, newVersion, dryRun: true };
2037
+ }
2038
+
2039
+ // 1. Bump package.json
2040
+ writePackageVersion(repoPath, newVersion);
2041
+ console.log(` \u2713 package.json -> ${newVersion}`);
2042
+
2043
+ // 2. Update CHANGELOG.md (lightweight entry)
2044
+ updateChangelog(repoPath, newVersion, notes || `${track} prerelease`);
2045
+ console.log(` \u2713 CHANGELOG.md updated`);
2046
+
2047
+ // 3. Git commit + tag
2048
+ const msg = `v${newVersion}: ${track} prerelease`;
2049
+ for (const f of ['package.json', 'CHANGELOG.md']) {
2050
+ if (existsSync(join(repoPath, f))) {
2051
+ execFileSync('git', ['add', f], { cwd: repoPath, stdio: 'pipe' });
2052
+ }
2053
+ }
2054
+ execFileSync('git', ['commit', '--no-verify', '-m', msg], { cwd: repoPath, stdio: 'pipe' });
2055
+ execFileSync('git', ['tag', `v${newVersion}`], { cwd: repoPath, stdio: 'pipe' });
2056
+ console.log(` \u2713 Committed and tagged v${newVersion}`);
2057
+
2058
+ // 4. Push commit + tag
2059
+ try {
2060
+ execSync('git push && git push --tags', { cwd: repoPath, stdio: 'pipe' });
2061
+ console.log(` \u2713 Pushed to remote`);
2062
+ } catch {
2063
+ console.log(` ! Push failed. Push manually.`);
2064
+ }
2065
+
2066
+ const distResults = [];
2067
+
2068
+ if (!noPublish) {
2069
+ // 5. npm publish with dist-tag
2070
+ try {
2071
+ publishNpmWithTag(repoPath, track);
2072
+ const pkg = JSON.parse(readFileSync(join(repoPath, 'package.json'), 'utf8'));
2073
+ distResults.push({ target: 'npm', status: 'ok', detail: `${pkg.name}@${newVersion} (tag: ${track})` });
2074
+ console.log(` \u2713 Published to npm @${track}`);
2075
+ } catch (e) {
2076
+ distResults.push({ target: 'npm', status: 'failed', detail: e.message });
2077
+ console.log(` \u2717 npm publish failed: ${e.message}`);
2078
+ }
2079
+
2080
+ // 6. GitHub prerelease on public repo (if opted in)
2081
+ if (publishReleaseNotes) {
2082
+ try {
2083
+ createGitHubPrerelease(repoPath, newVersion, notes || `${track} prerelease`);
2084
+ distResults.push({ target: 'GitHub', status: 'ok', detail: `v${newVersion} (prerelease)` });
2085
+ console.log(` \u2713 GitHub prerelease v${newVersion} created on public repo`);
2086
+ } catch (e) {
2087
+ distResults.push({ target: 'GitHub', status: 'failed', detail: e.message });
2088
+ console.log(` \u2717 GitHub prerelease failed: ${e.message}`);
2089
+ }
2090
+ } else {
2091
+ console.log(` - GitHub prerelease: skipped (silent ${track})`);
2092
+ }
2093
+ }
2094
+
2095
+ // Distribution summary
2096
+ if (distResults.length > 0) {
2097
+ console.log('');
2098
+ console.log(' Distribution:');
2099
+ for (const r of distResults) {
2100
+ const icon = r.status === 'ok' ? '\u2713' : '\u2717';
2101
+ console.log(` ${icon} ${r.target}: ${r.detail}`);
2102
+ }
2103
+ }
2104
+
2105
+ console.log('');
2106
+ console.log(` Done. ${repoName} v${newVersion} (${track}) released.`);
2107
+ console.log('');
2108
+
2109
+ return { currentVersion, newVersion, dryRun: false };
2110
+ }
2111
+
2112
+ // ── Hotfix Track ────────────────────────────────────────────────────
2113
+
2114
+ /**
2115
+ * Release a hotfix.
2116
+ *
2117
+ * Same as stable patch but: no deploy-public, no code sync.
2118
+ * Publishes to npm @latest, creates GitHub release on public repo (opt out with publishReleaseNotes=false).
2119
+ *
2120
+ * Lighter gates than stable: no product docs check, no stale branch check.
2121
+ * Still runs: worktree guard, license compliance, tests.
2122
+ */
2123
+ export async function releaseHotfix({ repoPath, notes, notesSource, dryRun, noPublish, publishReleaseNotes, skipWorktreeCheck }) {
2124
+ repoPath = repoPath || process.cwd();
2125
+ const currentVersion = detectCurrentVersion(repoPath);
2126
+ const newVersion = bumpSemver(currentVersion, 'patch');
2127
+ const repoName = basename(repoPath);
2128
+
2129
+ console.log('');
2130
+ console.log(` ${repoName}: ${currentVersion} -> ${newVersion} (hotfix)`);
2131
+ console.log(` ${'─'.repeat(40)}`);
2132
+
2133
+ // Worktree guard
2134
+ if (!skipWorktreeCheck) {
2135
+ try {
2136
+ const gitDir = execFileSync('git', ['rev-parse', '--git-dir'], {
2137
+ cwd: repoPath, encoding: 'utf8'
2138
+ }).trim();
2139
+ if (gitDir.includes('/worktrees/')) {
2140
+ const worktreeList = execFileSync('git', ['worktree', 'list', '--porcelain'], {
2141
+ cwd: repoPath, encoding: 'utf8'
2142
+ });
2143
+ const mainWorktree = worktreeList.split('\n')
2144
+ .find(line => line.startsWith('worktree '));
2145
+ const mainPath = mainWorktree ? mainWorktree.replace('worktree ', '') : '(unknown)';
2146
+ console.log(` \u2717 wip-release must run from the main working tree, not a worktree.`);
2147
+ console.log(` Current: ${repoPath}`);
2148
+ console.log(` Main working tree: ${mainPath}`);
2149
+ console.log('');
2150
+ return { currentVersion, newVersion, dryRun: false, failed: true };
2151
+ }
2152
+ console.log(' \u2713 Running from main working tree');
2153
+ } catch {}
2154
+ }
2155
+
2156
+ // License compliance gate
2157
+ const configPath = join(repoPath, '.license-guard.json');
2158
+ if (existsSync(configPath)) {
2159
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
2160
+ const licenseIssues = [];
2161
+ const licensePath = join(repoPath, 'LICENSE');
2162
+ if (!existsSync(licensePath)) {
2163
+ licenseIssues.push('LICENSE file is missing');
2164
+ } else {
2165
+ const licenseText = readFileSync(licensePath, 'utf8');
2166
+ if (!licenseText.includes(config.copyright)) {
2167
+ licenseIssues.push(`LICENSE copyright does not match "${config.copyright}"`);
2168
+ }
2169
+ }
2170
+ if (licenseIssues.length > 0) {
2171
+ console.log(` \u2717 License compliance failed:`);
2172
+ for (const issue of licenseIssues) console.log(` - ${issue}`);
2173
+ console.log('');
2174
+ return { currentVersion, newVersion, dryRun: false, failed: true };
2175
+ }
2176
+ console.log(` \u2713 License compliance passed`);
2177
+ }
2178
+
2179
+ // Release notes: hotfix accepts --notes flag as convenience (no file-only gate)
2180
+ if (!notes) {
2181
+ console.log(` ! No release notes provided. Hotfix will have minimal notes.`);
2182
+ notes = 'Hotfix release.';
2183
+ }
2184
+
2185
+ // Run tests if they exist
2186
+ {
2187
+ const toolsDir = join(repoPath, 'tools');
2188
+ const testFiles = [];
2189
+ if (existsSync(toolsDir)) {
2190
+ for (const sub of readdirSync(toolsDir)) {
2191
+ const testPath = join(toolsDir, sub, 'test.sh');
2192
+ if (existsSync(testPath)) testFiles.push({ tool: sub, path: testPath });
2193
+ }
2194
+ }
2195
+ const rootTest = join(repoPath, 'test.sh');
2196
+ if (existsSync(rootTest)) testFiles.push({ tool: '(root)', path: rootTest });
2197
+
2198
+ if (testFiles.length > 0) {
2199
+ let allPassed = true;
2200
+ for (const { tool, path } of testFiles) {
2201
+ try {
2202
+ execFileSync('bash', [path], { cwd: dirname(path), stdio: 'pipe', timeout: 30000 });
2203
+ console.log(` \u2713 Tests passed: ${tool}`);
2204
+ } catch (e) {
2205
+ allPassed = false;
2206
+ console.log(` \u2717 Tests FAILED: ${tool}`);
2207
+ const output = (e.stdout || '').toString().trim();
2208
+ if (output) {
2209
+ for (const line of output.split('\n').slice(-5)) console.log(` ${line}`);
2210
+ }
2211
+ }
2212
+ }
2213
+ if (!allPassed) {
2214
+ console.log('');
2215
+ console.log(' Fix failing tests before releasing.');
2216
+ console.log('');
2217
+ return { currentVersion, newVersion, dryRun: false, failed: true };
2218
+ }
2219
+ }
2220
+ }
2221
+
2222
+ if (dryRun) {
2223
+ console.log(` [dry run] Would bump package.json to ${newVersion}`);
2224
+ console.log(` [dry run] Would update CHANGELOG.md`);
2225
+ if (!noPublish) {
2226
+ console.log(` [dry run] Would npm publish with --tag latest`);
2227
+ if (publishReleaseNotes) {
2228
+ console.log(` [dry run] Would create GitHub release v${newVersion} on public repo`);
2229
+ } else {
2230
+ console.log(` [dry run] No GitHub release (--no-release-notes)`);
2231
+ }
2232
+ console.log(` [dry run] No deploy-public (hotfix)`);
2233
+ }
2234
+ console.log('');
2235
+ console.log(` Dry run complete. No changes made.`);
2236
+ console.log('');
2237
+ return { currentVersion, newVersion, dryRun: true };
2238
+ }
2239
+
2240
+ // 1. Bump package.json
2241
+ writePackageVersion(repoPath, newVersion);
2242
+ console.log(` \u2713 package.json -> ${newVersion}`);
2243
+
2244
+ // 1.5. Bump sub-tool versions
2245
+ const toolsDir = join(repoPath, 'tools');
2246
+ if (existsSync(toolsDir)) {
2247
+ let subBumped = 0;
2248
+ try {
2249
+ const entries = readdirSync(toolsDir, { withFileTypes: true });
2250
+ for (const entry of entries) {
2251
+ if (!entry.isDirectory()) continue;
2252
+ const subPkgPath = join(toolsDir, entry.name, 'package.json');
2253
+ if (existsSync(subPkgPath)) {
2254
+ try {
2255
+ const subPkg = JSON.parse(readFileSync(subPkgPath, 'utf8'));
2256
+ subPkg.version = newVersion;
2257
+ writeFileSync(subPkgPath, JSON.stringify(subPkg, null, 2) + '\n');
2258
+ subBumped++;
2259
+ } catch {}
2260
+ }
2261
+ }
2262
+ } catch {}
2263
+ if (subBumped > 0) {
2264
+ console.log(` \u2713 ${subBumped} sub-tool(s) -> ${newVersion}`);
2265
+ }
2266
+ }
2267
+
2268
+ // 2. Sync SKILL.md
2269
+ if (syncSkillVersion(repoPath, newVersion)) {
2270
+ console.log(` \u2713 SKILL.md -> ${newVersion}`);
2271
+ }
2272
+
2273
+ // 3. Update CHANGELOG.md
2274
+ updateChangelog(repoPath, newVersion, notes);
2275
+ console.log(` \u2713 CHANGELOG.md updated`);
2276
+
2277
+ // 3.5. Move RELEASE-NOTES-v*.md to _trash/
2278
+ const trashed = trashReleaseNotes(repoPath);
2279
+ if (trashed > 0) {
2280
+ console.log(` \u2713 Moved ${trashed} RELEASE-NOTES file(s) to _trash/`);
2281
+ }
2282
+
2283
+ // 4. Git commit + tag
2284
+ gitCommitAndTag(repoPath, newVersion, notes);
2285
+ console.log(` \u2713 Committed and tagged v${newVersion}`);
2286
+
2287
+ // 5. Push commit + tag
2288
+ try {
2289
+ execSync('git push && git push --tags', { cwd: repoPath, stdio: 'pipe' });
2290
+ console.log(` \u2713 Pushed to remote`);
2291
+ } catch {
2292
+ console.log(` ! Push failed. Push manually.`);
2293
+ }
2294
+
2295
+ const distResults = [];
2296
+
2297
+ if (!noPublish) {
2298
+ // 6. npm publish with @latest tag
2299
+ try {
2300
+ publishNpmWithTag(repoPath, 'latest');
2301
+ const pkg = JSON.parse(readFileSync(join(repoPath, 'package.json'), 'utf8'));
2302
+ distResults.push({ target: 'npm', status: 'ok', detail: `${pkg.name}@${newVersion}` });
2303
+ console.log(` \u2713 Published to npm @latest`);
2304
+ } catch (e) {
2305
+ distResults.push({ target: 'npm', status: 'failed', detail: e.message });
2306
+ console.log(` \u2717 npm publish failed: ${e.message}`);
2307
+ }
2308
+
2309
+ // 7. GitHub release on public repo (not prerelease)
2310
+ if (publishReleaseNotes) {
2311
+ try {
2312
+ createGitHubReleaseOnPublic(repoPath, newVersion, notes, currentVersion);
2313
+ distResults.push({ target: 'GitHub', status: 'ok', detail: `v${newVersion}` });
2314
+ console.log(` \u2713 GitHub release v${newVersion} created on public repo`);
2315
+ } catch (e) {
2316
+ distResults.push({ target: 'GitHub', status: 'failed', detail: e.message });
2317
+ console.log(` \u2717 GitHub release failed: ${e.message}`);
2318
+ }
2319
+ } else {
2320
+ console.log(` - GitHub release: skipped (--no-release-notes)`);
2321
+ }
2322
+
2323
+ // No deploy-public for hotfix
2324
+ console.log(` - deploy-public: skipped (hotfix)`);
2325
+
2326
+ // 8. ClawHub skill publish
2327
+ const rootSkill = join(repoPath, 'SKILL.md');
2328
+ if (existsSync(rootSkill)) {
2329
+ try {
2330
+ publishClawHub(repoPath, newVersion, notes);
2331
+ const slug = detectSkillSlug(repoPath);
2332
+ distResults.push({ target: 'ClawHub', status: 'ok', detail: `${slug}@${newVersion}` });
2333
+ console.log(` \u2713 Published to ClawHub: ${slug}`);
2334
+ } catch (e) {
2335
+ distResults.push({ target: 'ClawHub', status: 'failed', detail: e.message });
2336
+ console.log(` \u2717 ClawHub publish failed: ${e.message}`);
2337
+ }
2338
+ }
2339
+ }
2340
+
2341
+ // Distribution summary
2342
+ if (distResults.length > 0) {
2343
+ console.log('');
2344
+ console.log(' Distribution:');
2345
+ for (const r of distResults) {
2346
+ const icon = r.status === 'ok' ? '\u2713' : '\u2717';
2347
+ console.log(` ${icon} ${r.target}: ${r.detail}`);
2348
+ }
2349
+ }
2350
+
2351
+ // Write release marker
2352
+ try {
2353
+ const markerDir = join(process.env.HOME || '', '.ldm', 'state');
2354
+ mkdirSync(markerDir, { recursive: true });
2355
+ writeFileSync(join(markerDir, '.last-release'), JSON.stringify({
2356
+ repo: repoName,
2357
+ version: newVersion,
2358
+ timestamp: new Date().toISOString(),
2359
+ track: 'hotfix',
2360
+ }) + '\n');
2361
+ } catch {}
2362
+
2363
+ console.log('');
2364
+ console.log(` Done. ${repoName} v${newVersion} (hotfix) released.`);
2365
+ console.log('');
2366
+
2367
+ return { currentVersion, newVersion, dryRun: false };
2368
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-release",
3
- "version": "1.9.67",
3
+ "version": "1.9.68",
4
4
  "type": "module",
5
5
  "description": "One-command release pipeline. Bumps version, updates changelog + SKILL.md, publishes to npm + GitHub.",
6
6
  "main": "core.mjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-repo-init",
3
- "version": "1.9.67",
3
+ "version": "1.9.68",
4
4
  "description": "Scaffold the standard ai/ directory structure in any repo",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-repo-permissions-hook",
3
- "version": "1.9.67",
3
+ "version": "1.9.68",
4
4
  "type": "module",
5
5
  "description": "Repo visibility guard. Blocks repos from going public without a -private counterpart.",
6
6
  "main": "core.mjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-repos",
3
- "version": "1.9.67",
3
+ "version": "1.9.68",
4
4
  "type": "module",
5
5
  "description": "Repo manifest reconciler. Single source of truth for repo organization. Like prettier for folder structure.",
6
6
  "main": "core.mjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/universal-installer",
3
- "version": "1.9.67",
3
+ "version": "1.9.68",
4
4
  "type": "module",
5
5
  "description": "The Universal Interface specification for agent-native software. Teaches your AI how to build repos with every interface: CLI, Module, MCP Server, OpenClaw Plugin, Skill, Claude Code Hook.",
6
6
  "main": "detect.mjs",