@wipcomputer/wip-ai-devops-toolbox 1.9.39 → 1.9.41

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,48 @@
31
31
 
32
32
 
33
33
 
34
+
35
+ ## 1.9.41 (2026-03-17)
36
+
37
+ # Doc enforcement gates for wip-release
38
+
39
+ **Date:** 2026-03-16
40
+ **Closes:** #117, #128
41
+
42
+ ## What changed
43
+
44
+ Two new pre-release gates in wip-release:
45
+
46
+ **Technical Docs Gate (#117):** When source code (*.mjs, *.js, *.ts) changed since the last release tag, checks that SKILL.md or TECHNICAL.md was also modified. Catches code shipping without doc updates. Warns on patch, blocks on minor/major. Skip with `--skip-tech-docs-check`.
47
+
48
+ **Interface Coverage Gate (#128):** For toolbox repos, scans each tool in tools/*/ for actual interfaces (CLI, Module, MCP, OC Plugin, Skill, CC Hook) and compares to the coverage table in README.md and SKILL.md. Reports: tools missing from table, interfaces detected but not marked Y, interfaces marked Y but not detected, tool count mismatches. Warns on patch, blocks on minor/major. Skip with `--skip-coverage-check`.
49
+
50
+ Both follow the same pattern as existing gates (checkProductDocs, checkStaleBranches). Both run in real and dry-run modes.
51
+
52
+ ## Why
53
+
54
+ Source code was shipping without doc updates constantly. SKILL.md and TECHNICAL.md fell behind the code. Interface coverage tables drifted from reality. These gates catch it before release instead of after.
55
+
56
+ ## 1.9.40 (2026-03-16)
57
+
58
+ # Auto-sync product docs version/date on release
59
+
60
+ **Date:** 2026-03-16
61
+ **Closes:** #202
62
+
63
+ ## What changed
64
+
65
+ wip-release now auto-updates version and date lines in product docs before the release commit. No more stale "Current version: v1.9.1" when you're shipping v1.9.39.
66
+
67
+ Files updated automatically:
68
+ - `ai/product/plans-prds/roadmap.md`: "Current version" and "Last updated"
69
+ - `ai/product/readme-first-product.md`: "Last updated" and "What's Built (as of vX.Y.Z)"
70
+
71
+ Runs between changelog update and git commit (step 3.75). Only touches files that exist. Only updates lines that match the expected patterns.
72
+
73
+ ## Why
74
+
75
+ These files were stale from v1.9.1 through v1.9.39 (8 days, 38 releases). Nobody remembered to update them. The existing product docs gate warned about it but couldn't fix it. Now it fixes itself.
34
76
 
35
77
  ## 1.9.39 (2026-03-16)
36
78
 
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.39"
8
+ version: "1.9.41"
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.39",
3
+ "version": "1.9.41",
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.39",
3
+ "version": "1.9.41",
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.39",
3
+ "version": "1.9.41",
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"
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { execSync } from 'node:child_process';
9
9
  import { dirname, join } from 'node:path';
10
- import { statSync, readFileSync } from 'node:fs';
10
+ import { statSync, readFileSync, existsSync } from 'node:fs';
11
11
  import { fileURLToPath } from 'node:url';
12
12
 
13
13
  if (process.argv.includes('--version') || process.argv.includes('-v')) {
@@ -186,6 +186,23 @@ async function main() {
186
186
  deny('BLOCKED: git push --force can destroy remote history. Use --force-with-lease or ask Parker.');
187
187
  process.exit(0);
188
188
  }
189
+
190
+ // Block npm install -g right after a release (#73)
191
+ // wip-release writes ~/.ldm/state/.last-release on completion.
192
+ // If a release happened < 5 minutes ago, block install unless user explicitly said "install".
193
+ if (/\bnpm\s+install\s+-g\b/.test(cmd)) {
194
+ try {
195
+ const releasePath = join(process.env.HOME || '', '.ldm', 'state', '.last-release');
196
+ if (existsSync(releasePath)) {
197
+ const data = JSON.parse(readFileSync(releasePath, 'utf8'));
198
+ const age = Date.now() - new Date(data.timestamp).getTime();
199
+ if (age < 5 * 60 * 1000) { // 5 minutes
200
+ deny(`BLOCKED: Release completed ${Math.round(age / 1000)}s ago. Dogfood first. Remove ~/.ldm/state/.last-release when ready to install.`);
201
+ process.exit(0);
202
+ }
203
+ }
204
+ } catch {}
205
+ }
189
206
  }
190
207
 
191
208
  // Determine which repo to check.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-branch-guard",
3
- "version": "1.9.39",
3
+ "version": "1.9.41",
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
  "main": "guard.mjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-file-guard",
3
- "version": "1.9.39",
3
+ "version": "1.9.41",
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.39",
3
+ "version": "1.9.41",
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.39",
3
+ "version": "1.9.41",
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.39",
3
+ "version": "1.9.41",
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": {
@@ -21,6 +21,8 @@ const noPublish = args.includes('--no-publish');
21
21
  const skipProductCheck = args.includes('--skip-product-check');
22
22
  const skipStaleCheck = args.includes('--skip-stale-check');
23
23
  const skipWorktreeCheck = args.includes('--skip-worktree-check');
24
+ const skipTechDocsCheck = args.includes('--skip-tech-docs-check');
25
+ const skipCoverageCheck = args.includes('--skip-coverage-check');
24
26
  const notesFilePath = flag('notes-file');
25
27
  let notes = flag('notes');
26
28
  // Bug fix #121: use strict check, not truthiness. --notes="" is empty, not absent.
@@ -164,6 +166,8 @@ release({
164
166
  skipProductCheck,
165
167
  skipStaleCheck,
166
168
  skipWorktreeCheck,
169
+ skipTechDocsCheck,
170
+ skipCoverageCheck,
167
171
  }).catch(err => {
168
172
  console.error(` ✗ ${err.message}`);
169
173
  process.exit(1);
@@ -383,6 +383,259 @@ function checkProductDocs(repoPath) {
383
383
  return { missing, ok: missing.length === 0, skipped: false };
384
384
  }
385
385
 
386
+ /**
387
+ * Check that technical docs (SKILL.md, TECHNICAL.md) were updated
388
+ * when source code changed since last release tag.
389
+ * Returns { missing: string[], ok: boolean, skipped: boolean }.
390
+ */
391
+ function checkTechnicalDocs(repoPath) {
392
+ try {
393
+ let lastTag;
394
+ try {
395
+ lastTag = execFileSync('git', ['describe', '--tags', '--abbrev=0'],
396
+ { cwd: repoPath, encoding: 'utf8' }).trim();
397
+ } catch {
398
+ return { missing: [], ok: true, skipped: true }; // No tags yet
399
+ }
400
+
401
+ const diff = execFileSync('git', ['diff', '--name-only', lastTag, 'HEAD'],
402
+ { cwd: repoPath, encoding: 'utf8' });
403
+ const changedFiles = diff.split('\n').map(f => f.trim()).filter(Boolean);
404
+
405
+ // Find source code changes (*.mjs, *.js, *.ts) excluding non-source dirs
406
+ const excludePattern = /\/(node_modules|dist|_trash|examples)\//;
407
+ const sourcePattern = /\.(mjs|js|ts)$/;
408
+ const sourceChanges = changedFiles.filter(f =>
409
+ sourcePattern.test(f) && !excludePattern.test(f) && !f.startsWith('ai/')
410
+ );
411
+
412
+ if (sourceChanges.length === 0) {
413
+ return { missing: [], ok: true, skipped: false }; // No source changes
414
+ }
415
+
416
+ // Check if any doc files were also modified
417
+ const docChanges = changedFiles.filter(f =>
418
+ f === 'SKILL.md' || f === 'TECHNICAL.md' ||
419
+ /^tools\/[^/]+\/SKILL\.md$/.test(f) ||
420
+ /^tools\/[^/]+\/TECHNICAL\.md$/.test(f)
421
+ );
422
+
423
+ if (docChanges.length > 0) {
424
+ return { missing: [], ok: true, skipped: false }; // Docs updated
425
+ }
426
+
427
+ // Source changed but no doc updates
428
+ const missing = [];
429
+ const preview = sourceChanges.slice(0, 5).join(', ');
430
+ const more = sourceChanges.length > 5 ? ` (and ${sourceChanges.length - 5} more)` : '';
431
+ missing.push('Source files changed since last tag but no SKILL.md or TECHNICAL.md was updated');
432
+ missing.push(`Changed: ${preview}${more}`);
433
+ missing.push('Update SKILL.md or TECHNICAL.md to document these changes');
434
+
435
+ return { missing, ok: false, skipped: false };
436
+ } catch {
437
+ return { missing: [], ok: true, skipped: true }; // Graceful fallback
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Parse the interface coverage table from a markdown file.
443
+ * Returns array of { name, cli, module, mcp, openclaw, skill, ccHook } or null.
444
+ */
445
+ function parseInterfaceCoverageTable(filePath) {
446
+ if (!existsSync(filePath)) return null;
447
+ const content = readFileSync(filePath, 'utf8');
448
+ const lines = content.split('\n');
449
+
450
+ const headerIdx = lines.findIndex(l => /^\|\s*#\s*\|\s*Tool\s*\|/i.test(l));
451
+ if (headerIdx === -1) return null;
452
+
453
+ const rows = [];
454
+ for (let i = headerIdx + 2; i < lines.length; i++) {
455
+ const line = lines[i].trim();
456
+ if (!line.startsWith('|')) break;
457
+ const cells = line.split('|').map(c => c.trim()).filter(c => c !== '');
458
+ if (cells.length < 8) continue;
459
+ // Skip category header rows (# cell is empty, non-numeric, or bold)
460
+ const num = cells[0].trim();
461
+ if (!num || /^\*\*/.test(num) || isNaN(parseInt(num))) continue;
462
+ rows.push({
463
+ name: cells[1].trim(),
464
+ cli: /^Y$/i.test(cells[2]),
465
+ module: /^Y$/i.test(cells[3]),
466
+ mcp: /^Y$/i.test(cells[4]),
467
+ openclaw: /^Y$/i.test(cells[5]),
468
+ skill: /^Y$/i.test(cells[6]),
469
+ ccHook: /^Y$/i.test(cells[7]),
470
+ });
471
+ }
472
+ return rows.length > 0 ? rows : null;
473
+ }
474
+
475
+ /**
476
+ * Read display name from a tool's SKILL.md frontmatter.
477
+ * Tries display-name, then name field. Falls back to null.
478
+ */
479
+ function getToolDisplayName(toolPath) {
480
+ const skillPath = join(toolPath, 'SKILL.md');
481
+ if (!existsSync(skillPath)) return null;
482
+ try {
483
+ const content = readFileSync(skillPath, 'utf8');
484
+ const displayMatch = content.match(/^\s*display-name:\s*"?([^"\n]+)"?/m);
485
+ if (displayMatch) return displayMatch[1].trim();
486
+ const nameMatch = content.match(/^name:\s*"?([^"\n]+)"?/m);
487
+ if (nameMatch) return nameMatch[1].trim();
488
+ } catch {}
489
+ return null;
490
+ }
491
+
492
+ /**
493
+ * Check that the interface coverage table in README.md and SKILL.md
494
+ * matches the actual interfaces detected in tools/*/.
495
+ * Returns { missing: string[], ok: boolean, skipped: boolean }.
496
+ */
497
+ function checkInterfaceCoverage(repoPath) {
498
+ try {
499
+ // Only applies to toolbox repos
500
+ const toolsDir = join(repoPath, 'tools');
501
+ if (!existsSync(toolsDir)) return { missing: [], ok: true, skipped: true };
502
+
503
+ const entries = readdirSync(toolsDir, { withFileTypes: true });
504
+ const tools = entries
505
+ .filter(e => e.isDirectory() && existsSync(join(toolsDir, e.name, 'package.json')))
506
+ .map(e => ({ name: e.name, path: join(toolsDir, e.name) }));
507
+
508
+ if (tools.length === 0) return { missing: [], ok: true, skipped: true };
509
+
510
+ // Detect actual interfaces for each tool
511
+ const actualMap = {};
512
+ for (const tool of tools) {
513
+ const pkg = JSON.parse(readFileSync(join(tool.path, 'package.json'), 'utf8'));
514
+ actualMap[tool.name] = {
515
+ displayName: getToolDisplayName(tool.path) || tool.name,
516
+ cli: !!(pkg.bin),
517
+ module: !!(pkg.main || pkg.exports),
518
+ mcp: ['mcp-server.mjs', 'mcp-server.js', 'dist/mcp-server.js'].some(f => existsSync(join(tool.path, f))),
519
+ openclaw: existsSync(join(tool.path, 'openclaw.plugin.json')),
520
+ skill: existsSync(join(tool.path, 'SKILL.md')),
521
+ ccHook: !!(pkg.claudeCode?.hook) || existsSync(join(tool.path, 'guard.mjs')),
522
+ };
523
+ }
524
+
525
+ const missing = [];
526
+
527
+ // Check both README.md and SKILL.md tables
528
+ for (const [label, filePath] of [['README.md', join(repoPath, 'README.md')], ['SKILL.md', join(repoPath, 'SKILL.md')]]) {
529
+ const tableRows = parseInterfaceCoverageTable(filePath);
530
+ if (!tableRows) continue;
531
+
532
+ // Tool count
533
+ if (tools.length !== tableRows.length) {
534
+ missing.push(`${label}: tool count mismatch (${tools.length} in tools/, ${tableRows.length} in table)`);
535
+ }
536
+
537
+ // Check each actual tool against the table
538
+ for (const tool of tools) {
539
+ const actual = actualMap[tool.name];
540
+ const displayName = actual.displayName;
541
+ const tableRow = tableRows.find(r =>
542
+ r.name === displayName ||
543
+ r.name.toLowerCase() === displayName.toLowerCase() ||
544
+ r.name.toLowerCase().includes(tool.name.replace(/^wip-/, '').replace(/-/g, ' '))
545
+ );
546
+
547
+ if (!tableRow) {
548
+ missing.push(`${label}: ${tool.name} (${displayName}) missing from coverage table`);
549
+ continue;
550
+ }
551
+
552
+ const ifaceMap = [
553
+ ['cli', 'CLI'], ['module', 'Module'], ['mcp', 'MCP'],
554
+ ['openclaw', 'OC Plugin'], ['skill', 'Skill'], ['ccHook', 'CC Hook']
555
+ ];
556
+
557
+ for (const [key, name] of ifaceMap) {
558
+ if (actual[key] && !tableRow[key]) {
559
+ missing.push(`${label}: ${displayName} has ${name} but table says no`);
560
+ }
561
+ if (tableRow[key] && !actual[key]) {
562
+ missing.push(`${label}: ${displayName} marked ${name} in table but not detected`);
563
+ }
564
+ }
565
+ }
566
+ }
567
+
568
+ return { missing, ok: missing.length === 0, skipped: false };
569
+ } catch {
570
+ return { missing: [], ok: true, skipped: true }; // Graceful fallback
571
+ }
572
+ }
573
+
574
+ /**
575
+ * Auto-update version/date lines in product docs before the release commit.
576
+ * Updates roadmap.md "Current version" and "Last updated",
577
+ * and readme-first-product.md "Last updated" and "What's Built (as of vX.Y.Z)".
578
+ * Returns number of files updated.
579
+ */
580
+ function syncProductDocs(repoPath, newVersion) {
581
+ let updated = 0;
582
+ const today = new Date().toISOString().split('T')[0];
583
+
584
+ // 1. roadmap.md
585
+ const roadmapPath = join(repoPath, 'ai', 'product', 'plans-prds', 'roadmap.md');
586
+ if (existsSync(roadmapPath)) {
587
+ let content = readFileSync(roadmapPath, 'utf8');
588
+ let changed = false;
589
+
590
+ // Update "Current version: vX.Y.Z"
591
+ const versionRe = /(\*\*Current version:\*\*\s*)v[\d.]+/;
592
+ if (versionRe.test(content)) {
593
+ content = content.replace(versionRe, `$1v${newVersion}`);
594
+ changed = true;
595
+ }
596
+
597
+ // Update "Last updated: YYYY-MM-DD"
598
+ const dateRe = /(\*\*Last updated:\*\*\s*)[\d-]+/;
599
+ if (dateRe.test(content)) {
600
+ content = content.replace(dateRe, `$1${today}`);
601
+ changed = true;
602
+ }
603
+
604
+ if (changed) {
605
+ writeFileSync(roadmapPath, content);
606
+ updated++;
607
+ }
608
+ }
609
+
610
+ // 2. readme-first-product.md
611
+ const rfpPath = join(repoPath, 'ai', 'product', 'readme-first-product.md');
612
+ if (existsSync(rfpPath)) {
613
+ let content = readFileSync(rfpPath, 'utf8');
614
+ let changed = false;
615
+
616
+ // Update "Last updated: YYYY-MM-DD"
617
+ const dateRe = /(\*\*Last updated:\*\*\s*)[\d-]+/;
618
+ if (dateRe.test(content)) {
619
+ content = content.replace(dateRe, `$1${today}`);
620
+ changed = true;
621
+ }
622
+
623
+ // Update "What's Built (as of vX.Y.Z)"
624
+ const builtRe = /(What's Built \(as of\s*)v[\d.]+(\))/;
625
+ if (builtRe.test(content)) {
626
+ content = content.replace(builtRe, `$1v${newVersion}$2`);
627
+ changed = true;
628
+ }
629
+
630
+ if (changed) {
631
+ writeFileSync(rfpPath, content);
632
+ updated++;
633
+ }
634
+ }
635
+
636
+ return updated;
637
+ }
638
+
386
639
  /**
387
640
  * Build release notes with narrative first, commit details second.
388
641
  *
@@ -753,7 +1006,7 @@ export function checkStaleBranches(repoPath, level) {
753
1006
  /**
754
1007
  * Run the full release pipeline.
755
1008
  */
756
- export async function release({ repoPath, level, notes, notesSource, dryRun, noPublish, skipProductCheck, skipStaleCheck, skipWorktreeCheck }) {
1009
+ export async function release({ repoPath, level, notes, notesSource, dryRun, noPublish, skipProductCheck, skipStaleCheck, skipWorktreeCheck, skipTechDocsCheck, skipCoverageCheck }) {
757
1010
  repoPath = repoPath || process.cwd();
758
1011
  const currentVersion = detectCurrentVersion(repoPath);
759
1012
  const newVersion = bumpSemver(currentVersion, level);
@@ -897,6 +1150,50 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
897
1150
  }
898
1151
  }
899
1152
 
1153
+ // 0.85. Technical docs check
1154
+ if (!skipTechDocsCheck) {
1155
+ const techDocsCheck = checkTechnicalDocs(repoPath);
1156
+ if (!techDocsCheck.skipped) {
1157
+ if (techDocsCheck.ok) {
1158
+ console.log(' ✓ Technical docs up to date');
1159
+ } else {
1160
+ const isMinorOrMajor = level === 'minor' || level === 'major';
1161
+ const prefix = isMinorOrMajor ? '✗' : '!';
1162
+ console.log(` ${prefix} Technical docs need attention:`);
1163
+ for (const m of techDocsCheck.missing) console.log(` - ${m}`);
1164
+ if (isMinorOrMajor) {
1165
+ console.log('');
1166
+ console.log(' Update SKILL.md or TECHNICAL.md before a minor/major release.');
1167
+ console.log(' Use --skip-tech-docs-check to override.');
1168
+ console.log('');
1169
+ return { currentVersion, newVersion, dryRun: false, failed: true };
1170
+ }
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ // 0.9. Interface coverage check
1176
+ if (!skipCoverageCheck) {
1177
+ const coverageCheck = checkInterfaceCoverage(repoPath);
1178
+ if (!coverageCheck.skipped) {
1179
+ if (coverageCheck.ok) {
1180
+ console.log(' ✓ Interface coverage table matches');
1181
+ } else {
1182
+ const isMinorOrMajor = level === 'minor' || level === 'major';
1183
+ const prefix = isMinorOrMajor ? '✗' : '!';
1184
+ console.log(` ${prefix} Interface coverage table has mismatches:`);
1185
+ for (const m of coverageCheck.missing) console.log(` - ${m}`);
1186
+ if (isMinorOrMajor) {
1187
+ console.log('');
1188
+ console.log(' Update the coverage table in README.md and SKILL.md.');
1189
+ console.log(' Use --skip-coverage-check to override.');
1190
+ console.log('');
1191
+ return { currentVersion, newVersion, dryRun: false, failed: true };
1192
+ }
1193
+ }
1194
+ }
1195
+ }
1196
+
900
1197
  if (dryRun) {
901
1198
  // Product docs check (dry-run)
902
1199
  if (!skipProductCheck) {
@@ -934,6 +1231,32 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
934
1231
  console.log(' [dry run] ✓ No stale remote branches');
935
1232
  }
936
1233
  }
1234
+ // Technical docs check (dry-run)
1235
+ if (!skipTechDocsCheck) {
1236
+ const techDocsCheck = checkTechnicalDocs(repoPath);
1237
+ if (!techDocsCheck.skipped) {
1238
+ if (techDocsCheck.ok) {
1239
+ console.log(' [dry run] ✓ Technical docs up to date');
1240
+ } else {
1241
+ const isMinorOrMajor = level === 'minor' || level === 'major';
1242
+ console.log(` [dry run] ${isMinorOrMajor ? '✗ Would BLOCK' : '! Would WARN'}: technical docs need updates`);
1243
+ for (const m of techDocsCheck.missing) console.log(` - ${m}`);
1244
+ }
1245
+ }
1246
+ }
1247
+ // Interface coverage check (dry-run)
1248
+ if (!skipCoverageCheck) {
1249
+ const coverageCheck = checkInterfaceCoverage(repoPath);
1250
+ if (!coverageCheck.skipped) {
1251
+ if (coverageCheck.ok) {
1252
+ console.log(' [dry run] ✓ Interface coverage table matches');
1253
+ } else {
1254
+ const isMinorOrMajor = level === 'minor' || level === 'major';
1255
+ console.log(` [dry run] ${isMinorOrMajor ? '✗ Would BLOCK' : '! Would WARN'}: interface coverage mismatches`);
1256
+ for (const m of coverageCheck.missing) console.log(` - ${m}`);
1257
+ }
1258
+ }
1259
+ }
937
1260
  const hasSkill = existsSync(join(repoPath, 'SKILL.md'));
938
1261
  console.log(` [dry run] Would bump package.json to ${newVersion}`);
939
1262
  if (hasSkill) console.log(` [dry run] Would update SKILL.md version`);
@@ -1016,6 +1339,12 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
1016
1339
  console.log(` ✓ Moved ${trashed} RELEASE-NOTES file(s) to _trash/`);
1017
1340
  }
1018
1341
 
1342
+ // 3.75. Auto-update product docs version/date
1343
+ const docsUpdated = syncProductDocs(repoPath, newVersion);
1344
+ if (docsUpdated > 0) {
1345
+ console.log(` ✓ Product docs synced to v${newVersion} (${docsUpdated} file(s))`);
1346
+ }
1347
+
1019
1348
  // 4. Git commit + tag
1020
1349
  gitCommitAndTag(repoPath, newVersion, notes);
1021
1350
  console.log(` ✓ Committed and tagged v${newVersion}`);
@@ -1241,6 +1570,18 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
1241
1570
  console.log(` ! Branch prune skipped: ${e.message}`);
1242
1571
  }
1243
1572
 
1573
+ // Write release marker so branch guard blocks immediate install (#73)
1574
+ try {
1575
+ const markerDir = join(process.env.HOME || '', '.ldm', 'state');
1576
+ const { mkdirSync, writeFileSync } = await import('node:fs');
1577
+ mkdirSync(markerDir, { recursive: true });
1578
+ writeFileSync(join(markerDir, '.last-release'), JSON.stringify({
1579
+ repo: repoName,
1580
+ version: newVersion,
1581
+ timestamp: new Date().toISOString(),
1582
+ }) + '\n');
1583
+ } catch {}
1584
+
1244
1585
  console.log('');
1245
1586
  console.log(` Done. ${repoName} v${newVersion} released.`);
1246
1587
  console.log('');
@@ -31,6 +31,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
31
31
  dryRun: { type: 'boolean', description: 'Preview only, no changes', default: false },
32
32
  noPublish: { type: 'boolean', description: 'Bump + tag only, skip npm/GitHub publish', default: false },
33
33
  skipProductCheck: { type: 'boolean', description: 'Skip product doc freshness check', default: false },
34
+ skipTechDocsCheck: { type: 'boolean', description: 'Skip technical docs freshness check', default: false },
35
+ skipCoverageCheck: { type: 'boolean', description: 'Skip interface coverage table check', default: false },
34
36
  },
35
37
  required: ['level', 'notes'],
36
38
  },
@@ -65,6 +67,8 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
65
67
  notesSource: 'flag', // MCP always passes notes directly
66
68
  noPublish: args.noPublish || false,
67
69
  skipProductCheck: args.skipProductCheck || false,
70
+ skipTechDocsCheck: args.skipTechDocsCheck || false,
71
+ skipCoverageCheck: args.skipCoverageCheck || false,
68
72
  });
69
73
  return {
70
74
  content: [{
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-release",
3
- "version": "1.9.39",
3
+ "version": "1.9.41",
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.39",
3
+ "version": "1.9.41",
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.39",
3
+ "version": "1.9.41",
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.39",
3
+ "version": "1.9.41",
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.39",
3
+ "version": "1.9.41",
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",
@@ -1,63 +0,0 @@
1
- # todos/
2
-
3
- One todo file per person or agent. Three sections per file: To Do, Done, Deprecated.
4
-
5
- ## What This Folder Is
6
-
7
- Todos that are specific to this repo. Each person or agent who works on this repo gets one file. The file tracks what they need to do, what they've done, and what got dropped.
8
-
9
- For cross-repo work or bigger items, use GitHub Issues. Todos here are for repo-scoped tasks that don't need the overhead of an issue.
10
-
11
- ## Files
12
-
13
- One file per person/agent. Name it `[Name]-todo.md`.
14
-
15
- Example:
16
-
17
- | File | Who |
18
- |------|-----|
19
- | `Parker-todo.md` | Parker (human tasks: reviews, credentials, approvals) |
20
- | `CC-Mini-todo.md` | Claude Code on Mac Mini (code, builds, deploys) |
21
-
22
- Create the file when that person/agent first has work to do. No empty placeholder files.
23
-
24
- ## File Format
25
-
26
- ```markdown
27
- # [Name] Todos
28
-
29
- **Last updated:** YYYY-MM-DD
30
-
31
- ## To Do
32
-
33
- - [ ] Task description
34
- - [ ] Task description (blocked by: reason)
35
-
36
- ## Done
37
-
38
- - [x] Task description (YYYY-MM-DD)
39
- - [x] Task description (YYYY-MM-DD)
40
-
41
- ## Deprecated
42
-
43
- - ~~Task description~~ ... reason. (YYYY-MM-DD)
44
- ```
45
-
46
- ## Rules
47
-
48
- - **Never delete anything.** Items move between sections, never off the page.
49
- - **To Do** ... work that needs to happen.
50
- - **Done** ... completed work. Check the box, add the date.
51
- - **Deprecated** ... planned but no longer needed. Strikethrough, add the reason and date. Not the same as Done. Done means it shipped. Deprecated means the requirement changed.
52
- - **Update the date** at the top of the file every time you edit it.
53
- - **One file per person/agent.** No dated files, no subfolders, no inboxes.
54
- - **Blocked items** stay in To Do with a `(blocked by: reason)` note. Don't move them to a separate section.
55
-
56
- ## When to Use Todos vs GitHub Issues
57
-
58
- | Use | When |
59
- |-----|------|
60
- | **Todo file** | Quick tasks, repo-scoped work, things you'll do this session or this week |
61
- | **GitHub Issue** | Bugs, feature requests, cross-repo work, things that need tracking or discussion |
62
-
63
- Both is fine. File an issue AND add a todo that references it. The todo is your personal checklist. The issue is the team's record.