@wipcomputer/wip-ai-devops-toolbox 1.9.62 → 1.9.64

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,44 @@
31
31
 
32
32
 
33
33
 
34
+
35
+ ## 1.9.64 (2026-03-29)
36
+
37
+ # Release Notes: wip-ai-devops-toolbox v1.9.64
38
+
39
+ Closes #295
40
+
41
+ ## Branch guard: allow extension cleanup
42
+
43
+ The branch guard blocked `rm` on deployed extension directories (`~/.openclaw/extensions/` and `~/.ldm/extensions/`) because those paths live inside git repos. But deployed extensions are managed by `ldm install`, not by hand. When a stale `-private` extension needed to be removed (e.g. `wip-xai-grok-private` replaced by the public `wip-xai-grok`), the agent couldn't clean it up without asking the user to run the command manually.
44
+
45
+ Added an allowlist pattern for `rm` targeting `.openclaw/extensions/` and `.ldm/extensions/` paths. Same approach as the existing `.ldm/state/` allowlist. The guard still blocks `rm` on actual repo source files.
46
+
47
+ ## 1.9.63 (2026-03-29)
48
+
49
+ # Release Notes: wip-ai-devops-toolbox v1.9.63
50
+
51
+ **Fix all wip-release errors: branch cleanup crashes, shell injection, stale remote refs.**
52
+
53
+ ## The story
54
+
55
+ Every wip-release run produced errors: "fatal: Not a valid object name +", "remote ref does not exist", and shell injection risks from branch names passed through execSync template strings. These were dismissed as "non-blocking" but they cluttered every release output and masked real problems.
56
+
57
+ Root cause: branch cleanup code (sections 10 and 11) used `execSync` with template strings, which breaks on branch names with special characters and allows shell injection. Also tried to delete remote branches that GitHub already deleted during PR merge.
58
+
59
+ Fix: replaced all `execSync` template strings with `execFileSync` array args (safe from injection). Added character validation to skip branches with special chars. Wrapped remote delete in try/catch since GitHub PR merge already handles deletion.
60
+
61
+ ## Issues closed
62
+
63
+ - #231 (continued: release pipeline reliability)
64
+
65
+ ## How to verify
66
+
67
+ ```bash
68
+ wip-release patch --dry-run
69
+ # Should show no "fatal" or "Not a valid object name" errors
70
+ # Guard tests: cd tools/wip-branch-guard && bash test.sh
71
+ ```
34
72
 
35
73
  ## 1.9.62 (2026-03-29)
36
74
 
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.62"
8
+ version: "1.9.64"
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.62",
3
+ "version": "1.9.64",
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.62",
3
+ "version": "1.9.64",
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.62",
3
+ "version": "1.9.64",
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"
@@ -0,0 +1,23 @@
1
+ # Release Notes: wip-branch-guard v1.9.64
2
+
3
+ **One-line summary of what this release does**
4
+
5
+ Tell the story. What was broken or missing? What did we build? Why does the user care?
6
+ Write at least one real paragraph of prose. Not just bullets. The release notes gate
7
+ will block if there is no narrative. Bullets are fine for details, but the story comes first.
8
+
9
+ ## The story
10
+
11
+ (Write a paragraph here. What was the problem? What does this release fix? Why does it matter?
12
+ This is what users read. Make it worth reading.)
13
+
14
+ ## Issues closed
15
+
16
+ - #296
17
+ - #295
18
+
19
+ ## How to verify
20
+
21
+ ```bash
22
+ # Commands to test the changes
23
+ ```
@@ -133,6 +133,7 @@ const ALLOWED_BASH_PATTERNS = [
133
133
  /\bnpm\s+link\b/, // global operation, not repo-local
134
134
  /\bldm\s+(install|init|doctor|stack|updates)\b/, // LDM OS commands modify ~/.ldm/, not the repo
135
135
  /\brm\s+.*\.ldm\/state\//, // cleaning LDM state files only, not repo files
136
+ /\brm\s+.*\.(openclaw|ldm)\/extensions\//, // cleaning deployed extensions (managed by ldm install, not source code)
136
137
  /\bclaude\s+mcp\b/, // MCP registration, not repo files
137
138
  /\bmkdir\s+.*\.worktrees\b/, // creating .worktrees/ directory is part of the process
138
139
  ];
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-branch-guard",
3
- "version": "1.9.62",
3
+ "version": "1.9.64",
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
+ "scripts": {
7
+ "test": "bash test.sh"
8
+ },
6
9
  "main": "guard.mjs",
7
10
  "bin": {
8
11
  "wip-branch-guard": "guard.mjs"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-file-guard",
3
- "version": "1.9.62",
3
+ "version": "1.9.64",
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",
@@ -83,9 +83,10 @@ check "Block Write to TOOLS.md" \
83
83
  "block"
84
84
 
85
85
  # Large replacement (same line count, different content)
86
- check "Block Edit replacing 8 lines with 8 different lines in SHARED-CONTEXT.md" \
86
+ # SHARED-CONTEXT.md is shared state (maxReplace=30), so 8 lines is allowed
87
+ check "Allow Edit replacing 8 lines with 8 different lines in SHARED-CONTEXT.md (shared state)" \
87
88
  '{"tool_name":"Edit","tool_input":{"file_path":"/foo/SHARED-CONTEXT.md","old_string":"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8","new_string":"new1\nnew2\nnew3\nnew4\nnew5\nnew6\nnew7\nnew8"}}' \
88
- "block"
89
+ "allow"
89
90
 
90
91
  check "Allow Edit replacing 3 lines in CLAUDE.md" \
91
92
  '{"tool_name":"Edit","tool_input":{"file_path":"/foo/CLAUDE.md","old_string":"a\nb\nc","new_string":"x\ny\nz"}}' \
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-license-guard",
3
- "version": "1.9.62",
3
+ "version": "1.9.64",
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.62",
3
+ "version": "1.9.64",
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.62",
3
+ "version": "1.9.64",
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": {
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { execSync, execFileSync } from 'node:child_process';
9
9
  import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, renameSync } from 'node:fs';
10
- import { join, basename } from 'node:path';
10
+ import { join, basename, dirname } from 'node:path';
11
11
 
12
12
  // ── Version ─────────────────────────────────────────────────────────
13
13
 
@@ -1569,31 +1569,33 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
1569
1569
  .filter(b => b && b !== 'main' && b !== 'master' && !b.startsWith('*') && !b.includes('--merged-'));
1570
1570
 
1571
1571
  if (merged.length > 0) {
1572
+ const current = execSync('git branch --show-current', { cwd: repoPath, encoding: 'utf8' }).trim();
1572
1573
  console.log(` Scanning ${merged.length} merged branch(es) for rename...`);
1573
1574
  for (const branch of merged) {
1574
- const current = execSync('git branch --show-current', { cwd: repoPath, encoding: 'utf8' }).trim();
1575
1575
  if (branch === current) continue;
1576
+ // Skip branches with characters that break git commands
1577
+ if (/[+\s~^:?*\[\]]/.test(branch)) continue;
1576
1578
 
1577
1579
  let mergeDate;
1578
1580
  try {
1579
- const mergeBase = execSync(`git merge-base main ${branch}`, { cwd: repoPath, encoding: 'utf8' }).trim();
1580
- mergeDate = execSync(
1581
- `git log main --format="%ai" --ancestry-path ${mergeBase}..main`,
1582
- { cwd: repoPath, encoding: 'utf8' }
1583
- ).trim().split('\n').pop().split(' ')[0];
1581
+ // Use execFileSync (array args) instead of execSync (shell string) to avoid injection
1582
+ const mergeBase = execFileSync('git', ['merge-base', 'main', branch], { cwd: repoPath, encoding: 'utf8' }).trim();
1583
+ const logOutput = execFileSync('git', ['log', 'main', '--format=%ai', '--ancestry-path', `${mergeBase}..main`], { cwd: repoPath, encoding: 'utf8' }).trim();
1584
+ if (logOutput) mergeDate = logOutput.split('\n').pop().split(' ')[0];
1584
1585
  } catch {}
1585
1586
  if (!mergeDate) {
1586
1587
  try {
1587
- mergeDate = execSync(`git log ${branch} -1 --format="%ai"`, { cwd: repoPath, encoding: 'utf8' }).trim().split(' ')[0];
1588
+ mergeDate = execFileSync('git', ['log', branch, '-1', '--format=%ai'], { cwd: repoPath, encoding: 'utf8' }).trim().split(' ')[0];
1588
1589
  } catch {}
1589
1590
  }
1590
1591
  if (!mergeDate) continue;
1591
1592
 
1592
1593
  const newName = `${branch}--merged-${mergeDate}`;
1593
1594
  try {
1594
- execSync(`git branch -m "${branch}" "${newName}"`, { cwd: repoPath, stdio: 'pipe' });
1595
- execSync(`git push origin "${newName}"`, { cwd: repoPath, stdio: 'pipe' });
1596
- execSync(`git push origin --delete "${branch}"`, { cwd: repoPath, stdio: 'pipe' });
1595
+ execFileSync('git', ['branch', '-m', branch, newName], { cwd: repoPath, stdio: 'pipe' });
1596
+ execFileSync('git', ['push', 'origin', newName], { cwd: repoPath, stdio: 'pipe' });
1597
+ // Remote branch may already be deleted by GitHub PR merge. That's fine.
1598
+ try { execFileSync('git', ['push', 'origin', '--delete', branch], { cwd: repoPath, stdio: 'pipe' }); } catch {}
1597
1599
  console.log(` ✓ Renamed: ${branch} -> ${newName}`);
1598
1600
  } catch (e) {
1599
1601
  console.log(` ! Could not rename ${branch}: ${e.message}`);
@@ -1635,8 +1637,8 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
1635
1637
 
1636
1638
  for (let i = KEEP_COUNT; i < branches.length; i++) {
1637
1639
  try {
1638
- execSync(`git push origin --delete "${branches[i]}"`, { cwd: repoPath, stdio: 'pipe' });
1639
- execSync(`git branch -d "${branches[i]}" 2>/dev/null || true`, { cwd: repoPath, stdio: 'pipe', shell: true });
1640
+ execFileSync('git', ['push', 'origin', '--delete', branches[i]], { cwd: repoPath, stdio: 'pipe' });
1641
+ try { execFileSync('git', ['branch', '-d', branches[i]], { cwd: repoPath, stdio: 'pipe' }); } catch {}
1640
1642
  pruned++;
1641
1643
  } catch {}
1642
1644
  }
@@ -1659,11 +1661,12 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
1659
1661
  let staleCleaned = 0;
1660
1662
  for (const branch of allRemote) {
1661
1663
  if (branch === current) continue;
1664
+ if (/[+\s~^:?*\[\]]/.test(branch)) continue;
1662
1665
  try {
1663
- execSync(`git merge-base --is-ancestor origin/${branch} origin/main`, { cwd: repoPath, stdio: 'pipe' });
1666
+ execFileSync('git', ['merge-base', '--is-ancestor', `origin/${branch}`, 'origin/main'], { cwd: repoPath, stdio: 'pipe' });
1664
1667
  // If we get here, branch is fully merged
1665
- execSync(`git push origin --delete "${branch}"`, { cwd: repoPath, stdio: 'pipe' });
1666
- execSync(`git branch -d "${branch}" 2>/dev/null || true`, { cwd: repoPath, stdio: 'pipe', shell: true });
1668
+ try { execFileSync('git', ['push', 'origin', '--delete', branch], { cwd: repoPath, stdio: 'pipe' }); } catch {}
1669
+ try { execFileSync('git', ['branch', '-d', branch], { cwd: repoPath, stdio: 'pipe' }); } catch {}
1667
1670
  staleCleaned++;
1668
1671
  } catch {}
1669
1672
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-release",
3
- "version": "1.9.62",
3
+ "version": "1.9.64",
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.62",
3
+ "version": "1.9.64",
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.62",
3
+ "version": "1.9.64",
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.62",
3
+ "version": "1.9.64",
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.62",
3
+ "version": "1.9.64",
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",