@wipcomputer/wip-ai-devops-toolbox 1.9.40 → 1.9.42
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 +46 -0
- package/SKILL.md +1 -1
- package/package.json +1 -1
- package/tools/deploy-public/package.json +1 -1
- package/tools/post-merge-rename/package.json +1 -1
- package/tools/wip-branch-guard/guard.mjs +23 -1
- package/tools/wip-branch-guard/package.json +1 -1
- package/tools/wip-file-guard/package.json +1 -1
- package/tools/wip-license-guard/package.json +1 -1
- package/tools/wip-license-hook/package.json +1 -1
- package/tools/wip-readme-format/package.json +1 -1
- package/tools/wip-release/cli.js +6 -1
- package/tools/wip-release/core.mjs +275 -3
- package/tools/wip-release/mcp-server.mjs +4 -0
- package/tools/wip-release/package.json +1 -1
- package/tools/wip-repo-init/package.json +1 -1
- package/tools/wip-repo-permissions-hook/package.json +1 -1
- package/tools/wip-repos/package.json +1 -1
- package/tools/wip-universal-installer/package.json +1 -1
- package/tools/wip-repo-init/templates/product/plans-prds/todos/README 2.md +0 -63
package/CHANGELOG.md
CHANGED
|
@@ -31,6 +31,52 @@
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
|
|
35
|
+
## 1.9.42 (2026-03-17)
|
|
36
|
+
|
|
37
|
+
# Guard non-repo files fix + UTC date fix
|
|
38
|
+
|
|
39
|
+
Two bugs fixed in one PR.
|
|
40
|
+
|
|
41
|
+
## Bug 1: Guard blocks files outside git repos (#77)
|
|
42
|
+
|
|
43
|
+
**Problem:** When Write/Edit targets a file outside any git repo (e.g. `~/.claude/plans/`), `findRepoRoot()` returns null. The guard fell back to CWD (`~/.openclaw` on main) and blocked the operation. Files outside repos aren't the guard's concern.
|
|
44
|
+
|
|
45
|
+
**Fix:** If `findRepoRoot(filePath)` returns null for Write/Edit operations, allow immediately. The guard only protects git repos from direct-on-main edits.
|
|
46
|
+
|
|
47
|
+
**File:** `tools/wip-branch-guard/guard.mjs`
|
|
48
|
+
|
|
49
|
+
## Bug 2: UTC date mismatch in wip-release
|
|
50
|
+
|
|
51
|
+
**Problem:** Dev-update files are named with local date (e.g. `2026-03-16--cc-mini--...md`). But `new Date().toISOString().split('T')[0]` returns UTC date. After midnight UTC (4 PM PST), the dates diverge. Release notes gate fails to find today's dev-update.
|
|
52
|
+
|
|
53
|
+
**Fix:** Replaced all three instances of `toISOString()` date extraction with explicit local date construction using `getFullYear()/getMonth()/getDate()`.
|
|
54
|
+
|
|
55
|
+
**Files:**
|
|
56
|
+
- `tools/wip-release/cli.js` (line 80, dev-update detection)
|
|
57
|
+
- `tools/wip-release/core.mjs` (line 92, CHANGELOG date)
|
|
58
|
+
- `tools/wip-release/core.mjs` (line 582, product docs sync date)
|
|
59
|
+
|
|
60
|
+
## 1.9.41 (2026-03-17)
|
|
61
|
+
|
|
62
|
+
# Doc enforcement gates for wip-release
|
|
63
|
+
|
|
64
|
+
**Date:** 2026-03-16
|
|
65
|
+
**Closes:** #117, #128
|
|
66
|
+
|
|
67
|
+
## What changed
|
|
68
|
+
|
|
69
|
+
Two new pre-release gates in wip-release:
|
|
70
|
+
|
|
71
|
+
**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`.
|
|
72
|
+
|
|
73
|
+
**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`.
|
|
74
|
+
|
|
75
|
+
Both follow the same pattern as existing gates (checkProductDocs, checkStaleBranches). Both run in real and dry-run modes.
|
|
76
|
+
|
|
77
|
+
## Why
|
|
78
|
+
|
|
79
|
+
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.
|
|
34
80
|
|
|
35
81
|
## 1.9.40 (2026-03-16)
|
|
36
82
|
|
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.
|
|
8
|
+
version: "1.9.42"
|
|
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
|
@@ -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.
|
|
@@ -201,6 +218,11 @@ async function main() {
|
|
|
201
218
|
if (filePath) {
|
|
202
219
|
// Walk up from file path to find .git directory
|
|
203
220
|
repoDir = findRepoRoot(filePath);
|
|
221
|
+
if (!repoDir) {
|
|
222
|
+
// File is outside any git repo (e.g. ~/.claude/plans/, /tmp/).
|
|
223
|
+
// The guard only protects git repos. Allow it.
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
204
226
|
}
|
|
205
227
|
|
|
206
228
|
if (!repoDir && command) {
|
package/tools/wip-release/cli.js
CHANGED
|
@@ -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.
|
|
@@ -75,7 +77,8 @@ let notesSource = (notes !== null && notes !== undefined && notes !== '') ? 'fla
|
|
|
75
77
|
const { readdirSync } = await import('node:fs');
|
|
76
78
|
const devUpdatesDir = join(process.cwd(), 'ai', 'dev-updates');
|
|
77
79
|
if (existsSync(devUpdatesDir)) {
|
|
78
|
-
const
|
|
80
|
+
const d = new Date();
|
|
81
|
+
const today = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
|
|
79
82
|
const todayFiles = readdirSync(devUpdatesDir)
|
|
80
83
|
.filter(f => f.startsWith(today) && f.endsWith('.md'))
|
|
81
84
|
.sort()
|
|
@@ -164,6 +167,8 @@ release({
|
|
|
164
167
|
skipProductCheck,
|
|
165
168
|
skipStaleCheck,
|
|
166
169
|
skipWorktreeCheck,
|
|
170
|
+
skipTechDocsCheck,
|
|
171
|
+
skipCoverageCheck,
|
|
167
172
|
}).catch(err => {
|
|
168
173
|
console.error(` ✗ ${err.message}`);
|
|
169
174
|
process.exit(1);
|
|
@@ -89,7 +89,8 @@ export function syncSkillVersion(repoPath, newVersion) {
|
|
|
89
89
|
*/
|
|
90
90
|
export function updateChangelog(repoPath, newVersion, notes) {
|
|
91
91
|
const changelogPath = join(repoPath, 'CHANGELOG.md');
|
|
92
|
-
const
|
|
92
|
+
const d = new Date();
|
|
93
|
+
const date = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
|
|
93
94
|
|
|
94
95
|
// Bug fix #121: never silently default to "Release." when notes are empty.
|
|
95
96
|
// If notes are empty at this point, warn loudly.
|
|
@@ -383,6 +384,194 @@ function checkProductDocs(repoPath) {
|
|
|
383
384
|
return { missing, ok: missing.length === 0, skipped: false };
|
|
384
385
|
}
|
|
385
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Check that technical docs (SKILL.md, TECHNICAL.md) were updated
|
|
389
|
+
* when source code changed since last release tag.
|
|
390
|
+
* Returns { missing: string[], ok: boolean, skipped: boolean }.
|
|
391
|
+
*/
|
|
392
|
+
function checkTechnicalDocs(repoPath) {
|
|
393
|
+
try {
|
|
394
|
+
let lastTag;
|
|
395
|
+
try {
|
|
396
|
+
lastTag = execFileSync('git', ['describe', '--tags', '--abbrev=0'],
|
|
397
|
+
{ cwd: repoPath, encoding: 'utf8' }).trim();
|
|
398
|
+
} catch {
|
|
399
|
+
return { missing: [], ok: true, skipped: true }; // No tags yet
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const diff = execFileSync('git', ['diff', '--name-only', lastTag, 'HEAD'],
|
|
403
|
+
{ cwd: repoPath, encoding: 'utf8' });
|
|
404
|
+
const changedFiles = diff.split('\n').map(f => f.trim()).filter(Boolean);
|
|
405
|
+
|
|
406
|
+
// Find source code changes (*.mjs, *.js, *.ts) excluding non-source dirs
|
|
407
|
+
const excludePattern = /\/(node_modules|dist|_trash|examples)\//;
|
|
408
|
+
const sourcePattern = /\.(mjs|js|ts)$/;
|
|
409
|
+
const sourceChanges = changedFiles.filter(f =>
|
|
410
|
+
sourcePattern.test(f) && !excludePattern.test(f) && !f.startsWith('ai/')
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
if (sourceChanges.length === 0) {
|
|
414
|
+
return { missing: [], ok: true, skipped: false }; // No source changes
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check if any doc files were also modified
|
|
418
|
+
const docChanges = changedFiles.filter(f =>
|
|
419
|
+
f === 'SKILL.md' || f === 'TECHNICAL.md' ||
|
|
420
|
+
/^tools\/[^/]+\/SKILL\.md$/.test(f) ||
|
|
421
|
+
/^tools\/[^/]+\/TECHNICAL\.md$/.test(f)
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
if (docChanges.length > 0) {
|
|
425
|
+
return { missing: [], ok: true, skipped: false }; // Docs updated
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Source changed but no doc updates
|
|
429
|
+
const missing = [];
|
|
430
|
+
const preview = sourceChanges.slice(0, 5).join(', ');
|
|
431
|
+
const more = sourceChanges.length > 5 ? ` (and ${sourceChanges.length - 5} more)` : '';
|
|
432
|
+
missing.push('Source files changed since last tag but no SKILL.md or TECHNICAL.md was updated');
|
|
433
|
+
missing.push(`Changed: ${preview}${more}`);
|
|
434
|
+
missing.push('Update SKILL.md or TECHNICAL.md to document these changes');
|
|
435
|
+
|
|
436
|
+
return { missing, ok: false, skipped: false };
|
|
437
|
+
} catch {
|
|
438
|
+
return { missing: [], ok: true, skipped: true }; // Graceful fallback
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Parse the interface coverage table from a markdown file.
|
|
444
|
+
* Returns array of { name, cli, module, mcp, openclaw, skill, ccHook } or null.
|
|
445
|
+
*/
|
|
446
|
+
function parseInterfaceCoverageTable(filePath) {
|
|
447
|
+
if (!existsSync(filePath)) return null;
|
|
448
|
+
const content = readFileSync(filePath, 'utf8');
|
|
449
|
+
const lines = content.split('\n');
|
|
450
|
+
|
|
451
|
+
const headerIdx = lines.findIndex(l => /^\|\s*#\s*\|\s*Tool\s*\|/i.test(l));
|
|
452
|
+
if (headerIdx === -1) return null;
|
|
453
|
+
|
|
454
|
+
const rows = [];
|
|
455
|
+
for (let i = headerIdx + 2; i < lines.length; i++) {
|
|
456
|
+
const line = lines[i].trim();
|
|
457
|
+
if (!line.startsWith('|')) break;
|
|
458
|
+
const cells = line.split('|').map(c => c.trim()).filter(c => c !== '');
|
|
459
|
+
if (cells.length < 8) continue;
|
|
460
|
+
// Skip category header rows (# cell is empty, non-numeric, or bold)
|
|
461
|
+
const num = cells[0].trim();
|
|
462
|
+
if (!num || /^\*\*/.test(num) || isNaN(parseInt(num))) continue;
|
|
463
|
+
rows.push({
|
|
464
|
+
name: cells[1].trim(),
|
|
465
|
+
cli: /^Y$/i.test(cells[2]),
|
|
466
|
+
module: /^Y$/i.test(cells[3]),
|
|
467
|
+
mcp: /^Y$/i.test(cells[4]),
|
|
468
|
+
openclaw: /^Y$/i.test(cells[5]),
|
|
469
|
+
skill: /^Y$/i.test(cells[6]),
|
|
470
|
+
ccHook: /^Y$/i.test(cells[7]),
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return rows.length > 0 ? rows : null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Read display name from a tool's SKILL.md frontmatter.
|
|
478
|
+
* Tries display-name, then name field. Falls back to null.
|
|
479
|
+
*/
|
|
480
|
+
function getToolDisplayName(toolPath) {
|
|
481
|
+
const skillPath = join(toolPath, 'SKILL.md');
|
|
482
|
+
if (!existsSync(skillPath)) return null;
|
|
483
|
+
try {
|
|
484
|
+
const content = readFileSync(skillPath, 'utf8');
|
|
485
|
+
const displayMatch = content.match(/^\s*display-name:\s*"?([^"\n]+)"?/m);
|
|
486
|
+
if (displayMatch) return displayMatch[1].trim();
|
|
487
|
+
const nameMatch = content.match(/^name:\s*"?([^"\n]+)"?/m);
|
|
488
|
+
if (nameMatch) return nameMatch[1].trim();
|
|
489
|
+
} catch {}
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Check that the interface coverage table in README.md and SKILL.md
|
|
495
|
+
* matches the actual interfaces detected in tools/*/.
|
|
496
|
+
* Returns { missing: string[], ok: boolean, skipped: boolean }.
|
|
497
|
+
*/
|
|
498
|
+
function checkInterfaceCoverage(repoPath) {
|
|
499
|
+
try {
|
|
500
|
+
// Only applies to toolbox repos
|
|
501
|
+
const toolsDir = join(repoPath, 'tools');
|
|
502
|
+
if (!existsSync(toolsDir)) return { missing: [], ok: true, skipped: true };
|
|
503
|
+
|
|
504
|
+
const entries = readdirSync(toolsDir, { withFileTypes: true });
|
|
505
|
+
const tools = entries
|
|
506
|
+
.filter(e => e.isDirectory() && existsSync(join(toolsDir, e.name, 'package.json')))
|
|
507
|
+
.map(e => ({ name: e.name, path: join(toolsDir, e.name) }));
|
|
508
|
+
|
|
509
|
+
if (tools.length === 0) return { missing: [], ok: true, skipped: true };
|
|
510
|
+
|
|
511
|
+
// Detect actual interfaces for each tool
|
|
512
|
+
const actualMap = {};
|
|
513
|
+
for (const tool of tools) {
|
|
514
|
+
const pkg = JSON.parse(readFileSync(join(tool.path, 'package.json'), 'utf8'));
|
|
515
|
+
actualMap[tool.name] = {
|
|
516
|
+
displayName: getToolDisplayName(tool.path) || tool.name,
|
|
517
|
+
cli: !!(pkg.bin),
|
|
518
|
+
module: !!(pkg.main || pkg.exports),
|
|
519
|
+
mcp: ['mcp-server.mjs', 'mcp-server.js', 'dist/mcp-server.js'].some(f => existsSync(join(tool.path, f))),
|
|
520
|
+
openclaw: existsSync(join(tool.path, 'openclaw.plugin.json')),
|
|
521
|
+
skill: existsSync(join(tool.path, 'SKILL.md')),
|
|
522
|
+
ccHook: !!(pkg.claudeCode?.hook) || existsSync(join(tool.path, 'guard.mjs')),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const missing = [];
|
|
527
|
+
|
|
528
|
+
// Check both README.md and SKILL.md tables
|
|
529
|
+
for (const [label, filePath] of [['README.md', join(repoPath, 'README.md')], ['SKILL.md', join(repoPath, 'SKILL.md')]]) {
|
|
530
|
+
const tableRows = parseInterfaceCoverageTable(filePath);
|
|
531
|
+
if (!tableRows) continue;
|
|
532
|
+
|
|
533
|
+
// Tool count
|
|
534
|
+
if (tools.length !== tableRows.length) {
|
|
535
|
+
missing.push(`${label}: tool count mismatch (${tools.length} in tools/, ${tableRows.length} in table)`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Check each actual tool against the table
|
|
539
|
+
for (const tool of tools) {
|
|
540
|
+
const actual = actualMap[tool.name];
|
|
541
|
+
const displayName = actual.displayName;
|
|
542
|
+
const tableRow = tableRows.find(r =>
|
|
543
|
+
r.name === displayName ||
|
|
544
|
+
r.name.toLowerCase() === displayName.toLowerCase() ||
|
|
545
|
+
r.name.toLowerCase().includes(tool.name.replace(/^wip-/, '').replace(/-/g, ' '))
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
if (!tableRow) {
|
|
549
|
+
missing.push(`${label}: ${tool.name} (${displayName}) missing from coverage table`);
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const ifaceMap = [
|
|
554
|
+
['cli', 'CLI'], ['module', 'Module'], ['mcp', 'MCP'],
|
|
555
|
+
['openclaw', 'OC Plugin'], ['skill', 'Skill'], ['ccHook', 'CC Hook']
|
|
556
|
+
];
|
|
557
|
+
|
|
558
|
+
for (const [key, name] of ifaceMap) {
|
|
559
|
+
if (actual[key] && !tableRow[key]) {
|
|
560
|
+
missing.push(`${label}: ${displayName} has ${name} but table says no`);
|
|
561
|
+
}
|
|
562
|
+
if (tableRow[key] && !actual[key]) {
|
|
563
|
+
missing.push(`${label}: ${displayName} marked ${name} in table but not detected`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return { missing, ok: missing.length === 0, skipped: false };
|
|
570
|
+
} catch {
|
|
571
|
+
return { missing: [], ok: true, skipped: true }; // Graceful fallback
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
386
575
|
/**
|
|
387
576
|
* Auto-update version/date lines in product docs before the release commit.
|
|
388
577
|
* Updates roadmap.md "Current version" and "Last updated",
|
|
@@ -391,7 +580,8 @@ function checkProductDocs(repoPath) {
|
|
|
391
580
|
*/
|
|
392
581
|
function syncProductDocs(repoPath, newVersion) {
|
|
393
582
|
let updated = 0;
|
|
394
|
-
const
|
|
583
|
+
const td = new Date();
|
|
584
|
+
const today = `${td.getFullYear()}-${String(td.getMonth()+1).padStart(2,'0')}-${String(td.getDate()).padStart(2,'0')}`;
|
|
395
585
|
|
|
396
586
|
// 1. roadmap.md
|
|
397
587
|
const roadmapPath = join(repoPath, 'ai', 'product', 'plans-prds', 'roadmap.md');
|
|
@@ -818,7 +1008,7 @@ export function checkStaleBranches(repoPath, level) {
|
|
|
818
1008
|
/**
|
|
819
1009
|
* Run the full release pipeline.
|
|
820
1010
|
*/
|
|
821
|
-
export async function release({ repoPath, level, notes, notesSource, dryRun, noPublish, skipProductCheck, skipStaleCheck, skipWorktreeCheck }) {
|
|
1011
|
+
export async function release({ repoPath, level, notes, notesSource, dryRun, noPublish, skipProductCheck, skipStaleCheck, skipWorktreeCheck, skipTechDocsCheck, skipCoverageCheck }) {
|
|
822
1012
|
repoPath = repoPath || process.cwd();
|
|
823
1013
|
const currentVersion = detectCurrentVersion(repoPath);
|
|
824
1014
|
const newVersion = bumpSemver(currentVersion, level);
|
|
@@ -962,6 +1152,50 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
962
1152
|
}
|
|
963
1153
|
}
|
|
964
1154
|
|
|
1155
|
+
// 0.85. Technical docs check
|
|
1156
|
+
if (!skipTechDocsCheck) {
|
|
1157
|
+
const techDocsCheck = checkTechnicalDocs(repoPath);
|
|
1158
|
+
if (!techDocsCheck.skipped) {
|
|
1159
|
+
if (techDocsCheck.ok) {
|
|
1160
|
+
console.log(' ✓ Technical docs up to date');
|
|
1161
|
+
} else {
|
|
1162
|
+
const isMinorOrMajor = level === 'minor' || level === 'major';
|
|
1163
|
+
const prefix = isMinorOrMajor ? '✗' : '!';
|
|
1164
|
+
console.log(` ${prefix} Technical docs need attention:`);
|
|
1165
|
+
for (const m of techDocsCheck.missing) console.log(` - ${m}`);
|
|
1166
|
+
if (isMinorOrMajor) {
|
|
1167
|
+
console.log('');
|
|
1168
|
+
console.log(' Update SKILL.md or TECHNICAL.md before a minor/major release.');
|
|
1169
|
+
console.log(' Use --skip-tech-docs-check to override.');
|
|
1170
|
+
console.log('');
|
|
1171
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// 0.9. Interface coverage check
|
|
1178
|
+
if (!skipCoverageCheck) {
|
|
1179
|
+
const coverageCheck = checkInterfaceCoverage(repoPath);
|
|
1180
|
+
if (!coverageCheck.skipped) {
|
|
1181
|
+
if (coverageCheck.ok) {
|
|
1182
|
+
console.log(' ✓ Interface coverage table matches');
|
|
1183
|
+
} else {
|
|
1184
|
+
const isMinorOrMajor = level === 'minor' || level === 'major';
|
|
1185
|
+
const prefix = isMinorOrMajor ? '✗' : '!';
|
|
1186
|
+
console.log(` ${prefix} Interface coverage table has mismatches:`);
|
|
1187
|
+
for (const m of coverageCheck.missing) console.log(` - ${m}`);
|
|
1188
|
+
if (isMinorOrMajor) {
|
|
1189
|
+
console.log('');
|
|
1190
|
+
console.log(' Update the coverage table in README.md and SKILL.md.');
|
|
1191
|
+
console.log(' Use --skip-coverage-check to override.');
|
|
1192
|
+
console.log('');
|
|
1193
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
965
1199
|
if (dryRun) {
|
|
966
1200
|
// Product docs check (dry-run)
|
|
967
1201
|
if (!skipProductCheck) {
|
|
@@ -999,6 +1233,32 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
999
1233
|
console.log(' [dry run] ✓ No stale remote branches');
|
|
1000
1234
|
}
|
|
1001
1235
|
}
|
|
1236
|
+
// Technical docs check (dry-run)
|
|
1237
|
+
if (!skipTechDocsCheck) {
|
|
1238
|
+
const techDocsCheck = checkTechnicalDocs(repoPath);
|
|
1239
|
+
if (!techDocsCheck.skipped) {
|
|
1240
|
+
if (techDocsCheck.ok) {
|
|
1241
|
+
console.log(' [dry run] ✓ Technical docs up to date');
|
|
1242
|
+
} else {
|
|
1243
|
+
const isMinorOrMajor = level === 'minor' || level === 'major';
|
|
1244
|
+
console.log(` [dry run] ${isMinorOrMajor ? '✗ Would BLOCK' : '! Would WARN'}: technical docs need updates`);
|
|
1245
|
+
for (const m of techDocsCheck.missing) console.log(` - ${m}`);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
// Interface coverage check (dry-run)
|
|
1250
|
+
if (!skipCoverageCheck) {
|
|
1251
|
+
const coverageCheck = checkInterfaceCoverage(repoPath);
|
|
1252
|
+
if (!coverageCheck.skipped) {
|
|
1253
|
+
if (coverageCheck.ok) {
|
|
1254
|
+
console.log(' [dry run] ✓ Interface coverage table matches');
|
|
1255
|
+
} else {
|
|
1256
|
+
const isMinorOrMajor = level === 'minor' || level === 'major';
|
|
1257
|
+
console.log(` [dry run] ${isMinorOrMajor ? '✗ Would BLOCK' : '! Would WARN'}: interface coverage mismatches`);
|
|
1258
|
+
for (const m of coverageCheck.missing) console.log(` - ${m}`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1002
1262
|
const hasSkill = existsSync(join(repoPath, 'SKILL.md'));
|
|
1003
1263
|
console.log(` [dry run] Would bump package.json to ${newVersion}`);
|
|
1004
1264
|
if (hasSkill) console.log(` [dry run] Would update SKILL.md version`);
|
|
@@ -1312,6 +1572,18 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
1312
1572
|
console.log(` ! Branch prune skipped: ${e.message}`);
|
|
1313
1573
|
}
|
|
1314
1574
|
|
|
1575
|
+
// Write release marker so branch guard blocks immediate install (#73)
|
|
1576
|
+
try {
|
|
1577
|
+
const markerDir = join(process.env.HOME || '', '.ldm', 'state');
|
|
1578
|
+
const { mkdirSync, writeFileSync } = await import('node:fs');
|
|
1579
|
+
mkdirSync(markerDir, { recursive: true });
|
|
1580
|
+
writeFileSync(join(markerDir, '.last-release'), JSON.stringify({
|
|
1581
|
+
repo: repoName,
|
|
1582
|
+
version: newVersion,
|
|
1583
|
+
timestamp: new Date().toISOString(),
|
|
1584
|
+
}) + '\n');
|
|
1585
|
+
} catch {}
|
|
1586
|
+
|
|
1315
1587
|
console.log('');
|
|
1316
1588
|
console.log(` Done. ${repoName} v${newVersion} released.`);
|
|
1317
1589
|
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/universal-installer",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.42",
|
|
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.
|