create-harness-vibe-coding 0.6.4 → 0.7.0
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/package.json +2 -1
- package/src/generator.js +95 -2
- package/templates/common/.claude/agents/architect-manager.md +45 -0
- package/templates/common/.claude/agents/explore-manager.md +41 -0
- package/templates/common/.claude/agents/implement-manager.md +49 -0
- package/templates/common/.claude/agents/review-manager.md +56 -0
- package/templates/common/.claude/commands/wf-max.md +28 -14
- package/templates/common/.claude/commands/wf-remove.md +23 -0
- package/templates/common/.claude/commands/wf-review.md +13 -20
- package/templates/common/.claude/commands/wf-update.md +6 -4
- package/templates/common/.claude/settings.json +33 -0
- package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +1 -1
- package/templates/common/.claude/skills/wf-max/SKILL.md +34 -8
- package/templates/common/.claude/skills/wf-remove/SKILL.md +51 -0
- package/templates/common/.claude/skills/wf-review/SKILL.md +72 -50
- package/templates/common/.claude/skills/wf-update/SKILL.md +74 -58
- package/templates/common/.harness-version +122 -3
- package/templates/common/CLAUDE.md +94 -77
- package/templates/common/MEMORY.md +75 -73
- package/templates/common/SETUP.md +1 -2
- package/templates/common/docs/README.md +2 -2
- package/templates/common/docs/harness/WF-MAX.md +99 -10
- package/templates/common/docs/harness/WF.md +5 -0
- package/templates/common/docs/harness/dispatch.md +4 -0
- package/templates/common/scripts/scan-clean.mjs +456 -0
- package/templates/common/scripts/validate-harness.mjs +9 -0
- package/templates/common/scripts/wf-mode-hook.mjs +318 -0
- package/templates/common/scripts/wf-remove.mjs +396 -0
- package/templates/common/scripts/wf-statusline.ps1 +38 -0
- package/templates/common/scripts/wf-statusline.sh +48 -0
- package/templates/common/scripts/wf-update-check.mjs +389 -0
- package/templates/optional/skills/browser-e2e/docs/workflows/browser-e2e.md +12 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scan-clean.mjs — Dead file scanner & cleaner for the Harness framework update pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Runs AFTER a successful wf-update to find and remove dead files — files that were
|
|
6
|
+
* tracked in the previous harness version but removed from the new remote template.
|
|
7
|
+
*
|
|
8
|
+
* Also detects orphan files: files physically on disk in framework-managed directories
|
|
9
|
+
* that are not tracked by either local or remote checksums.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node Harness/scripts/scan-clean.mjs # report mode: show what WOULD be cleaned
|
|
13
|
+
* node Harness/scripts/scan-clean.mjs --clean # delete DEAD files (prompts for confirmation)
|
|
14
|
+
* node Harness/scripts/scan-clean.mjs --clean --yes # delete without confirmation prompt
|
|
15
|
+
* node Harness/scripts/scan-clean.mjs --json # machine-readable JSON output
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync, lstatSync, readdirSync, unlinkSync, rmdirSync } from 'fs';
|
|
19
|
+
import { resolve, dirname, sep, join } from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
import { createInterface } from 'readline';
|
|
22
|
+
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const ROOT = process.env.WF_ROOT ? resolve(process.env.WF_ROOT) : resolve(__dirname, '..', '..');
|
|
25
|
+
const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
|
|
26
|
+
const SOURCE_BASE = 'https://raw.githubusercontent.com/zingspark/create-harness-vibe-coding/main/templates/common/';
|
|
27
|
+
|
|
28
|
+
// ── Classification constants ────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/** Files we NEVER delete, even if dead. */
|
|
31
|
+
const PRESERVE_PATTERNS = [
|
|
32
|
+
/^Harness\/PROGRESS\.md$/,
|
|
33
|
+
/^Harness\/tasks\//,
|
|
34
|
+
/^Harness\/memory\//,
|
|
35
|
+
/^Harness\/research\/PRD\.md$/,
|
|
36
|
+
/^Harness\/research\/research-results\.md$/,
|
|
37
|
+
/^Harness\/architecture\.md$/,
|
|
38
|
+
/^README\.md$/,
|
|
39
|
+
/^\.gitignore$/,
|
|
40
|
+
/^package\.json$/,
|
|
41
|
+
/^package-lock\.json$/,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** Directories scanned for orphan files (untracked by either checksum set). */
|
|
45
|
+
const FRAMEWORK_DIRS = [
|
|
46
|
+
'.claude/agents',
|
|
47
|
+
'.claude/skills',
|
|
48
|
+
'.claude/commands',
|
|
49
|
+
'.claude/rules',
|
|
50
|
+
'Harness/scripts',
|
|
51
|
+
'Harness/workflows',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/** Reject paths that escape ROOT (traversal, absolute, .., etc.). */
|
|
57
|
+
function safePath(file) {
|
|
58
|
+
let normalized = file.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
59
|
+
if (/\/\//.test(normalized)) return null;
|
|
60
|
+
if (normalized.split('/').some(p => p === '..')) return null;
|
|
61
|
+
if (file.startsWith('/') || file.startsWith('\\')) return null;
|
|
62
|
+
if (normalized === '.' || normalized === '') return null;
|
|
63
|
+
const resolved = resolve(ROOT, normalized);
|
|
64
|
+
if (!resolved.startsWith(ROOT + sep) && resolved !== ROOT) return null;
|
|
65
|
+
return resolved;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Canonical normalization for classification matching. */
|
|
69
|
+
function canonicalPath(file) {
|
|
70
|
+
return file.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Check whether a canonical path matches any PRESERVE pattern. */
|
|
74
|
+
function isPreserved(canonical) {
|
|
75
|
+
for (const p of PRESERVE_PATTERNS) {
|
|
76
|
+
if (p.test(canonical)) return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Detect template placeholders in remote content. */
|
|
82
|
+
function isTemplate(raw) {
|
|
83
|
+
return /\{\{[a-zA-Z]+\}\}/.test(raw);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function fetchRemote(url, timeoutMs = 30000) {
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
91
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
|
|
92
|
+
return res.text();
|
|
93
|
+
} finally {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Orphan detection ─────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Recursively list all files under a directory, returning canonical paths
|
|
102
|
+
* relative to ROOT. Skips symlinks and directories that don't exist.
|
|
103
|
+
*/
|
|
104
|
+
function listFilesRecursive(relDir) {
|
|
105
|
+
const absDir = resolve(ROOT, relDir);
|
|
106
|
+
if (!existsSync(absDir)) return [];
|
|
107
|
+
const results = [];
|
|
108
|
+
try {
|
|
109
|
+
const entries = readdirSync(absDir, { withFileTypes: true });
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
const absPath = join(absDir, entry.name);
|
|
112
|
+
const relPath = canonicalPath(join(relDir, entry.name));
|
|
113
|
+
try {
|
|
114
|
+
if (lstatSync(absPath).isSymbolicLink()) continue;
|
|
115
|
+
} catch (_) { continue; }
|
|
116
|
+
if (entry.isFile()) {
|
|
117
|
+
results.push(relPath);
|
|
118
|
+
} else if (entry.isDirectory()) {
|
|
119
|
+
results.push(...listFilesRecursive(relPath));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (_) { /* permission errors etc. — skip */ }
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Find orphan files: files physically in FRAMEWORK_DIRS that are not
|
|
128
|
+
* tracked by local or remote checksums AND do not match PRESERVE patterns.
|
|
129
|
+
*/
|
|
130
|
+
function findOrphanFiles(localChecksums, remoteChecksums) {
|
|
131
|
+
const allTracked = new Set([...Object.keys(localChecksums), ...Object.keys(remoteChecksums)]);
|
|
132
|
+
const orphans = [];
|
|
133
|
+
|
|
134
|
+
for (const dir of FRAMEWORK_DIRS) {
|
|
135
|
+
const diskFiles = listFilesRecursive(dir);
|
|
136
|
+
for (const file of diskFiles) {
|
|
137
|
+
if (allTracked.has(file)) continue;
|
|
138
|
+
if (isPreserved(file)) continue;
|
|
139
|
+
orphans.push({ file, reason: 'untracked in framework directory' });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return orphans;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Empty directory detection ────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Given a set of file paths that will be removed, find directories that
|
|
150
|
+
* would become empty. Returns canonical paths of empty directories.
|
|
151
|
+
*/
|
|
152
|
+
function findEmptyDirs(filesToRemove) {
|
|
153
|
+
const removeSet = new Set(filesToRemove.map(canonicalPath));
|
|
154
|
+
// Collect all unique directories touched by the files to remove
|
|
155
|
+
const dirSet = new Set();
|
|
156
|
+
for (const file of removeSet) {
|
|
157
|
+
let dir = dirname(file);
|
|
158
|
+
while (dir !== '.' && dir !== '/') {
|
|
159
|
+
dirSet.add(dir);
|
|
160
|
+
dir = dirname(dir);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const emptyDirs = [];
|
|
165
|
+
for (const dir of dirSet) {
|
|
166
|
+
const absDir = resolve(ROOT, dir);
|
|
167
|
+
if (!existsSync(absDir)) continue;
|
|
168
|
+
// Gather all files recursively under this directory
|
|
169
|
+
const allFiles = listFilesRecursive(dir);
|
|
170
|
+
// If every file in this tree is slated for removal, the dir becomes empty
|
|
171
|
+
if (allFiles.length > 0 && allFiles.every(f => removeSet.has(f))) {
|
|
172
|
+
emptyDirs.push(dir);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Return sorted, removing children whose parents are also empty
|
|
177
|
+
// (only report the highest-level empty dirs)
|
|
178
|
+
const result = [];
|
|
179
|
+
for (const dir of emptyDirs.sort()) {
|
|
180
|
+
// Skip if any parent is already in the result
|
|
181
|
+
if (result.some(p => dir.startsWith(p + '/'))) continue;
|
|
182
|
+
result.push(dir);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Clean helpers ────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Remove empty parent directories, walking up from the given file path.
|
|
192
|
+
* Stops when a directory is non-empty or we reach ROOT.
|
|
193
|
+
*/
|
|
194
|
+
function removeEmptyParents(fileAbsPath) {
|
|
195
|
+
let dir = dirname(fileAbsPath);
|
|
196
|
+
const removed = [];
|
|
197
|
+
|
|
198
|
+
while (dir !== ROOT && dir.startsWith(ROOT + sep)) {
|
|
199
|
+
if (!existsSync(dir)) break;
|
|
200
|
+
try {
|
|
201
|
+
const entries = readdirSync(dir);
|
|
202
|
+
if (entries.length === 0) {
|
|
203
|
+
rmdirSync(dir);
|
|
204
|
+
removed.push(dir);
|
|
205
|
+
dir = dirname(dir);
|
|
206
|
+
} else {
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
} catch (_) { break; }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return removed;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Main ─────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
async function main() {
|
|
218
|
+
const args = process.argv.slice(2);
|
|
219
|
+
const clean = args.includes('--clean');
|
|
220
|
+
const yes = args.includes('--yes');
|
|
221
|
+
const jsonOut = args.includes('--json');
|
|
222
|
+
|
|
223
|
+
// 1. Read local state
|
|
224
|
+
if (!existsSync(VERSION_FILE)) {
|
|
225
|
+
if (jsonOut) {
|
|
226
|
+
console.log(JSON.stringify({ status: 'error', message: 'Local .harness-version not found.' }));
|
|
227
|
+
} else {
|
|
228
|
+
console.error('ERROR: Harness/.harness-version not found. Nothing to scan.');
|
|
229
|
+
}
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let localVersion;
|
|
234
|
+
try {
|
|
235
|
+
localVersion = JSON.parse(readFileSync(VERSION_FILE, 'utf-8'));
|
|
236
|
+
} catch (e) {
|
|
237
|
+
if (jsonOut) {
|
|
238
|
+
console.log(JSON.stringify({ status: 'error', message: 'Failed to parse local .harness-version: ' + e.message }));
|
|
239
|
+
} else {
|
|
240
|
+
console.error('ERROR: Failed to parse Harness/.harness-version:', e.message);
|
|
241
|
+
}
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const localChecksums = localVersion.checksums || {};
|
|
246
|
+
|
|
247
|
+
// 2. Fetch remote version file
|
|
248
|
+
let remoteVersion;
|
|
249
|
+
try {
|
|
250
|
+
const raw = await fetchRemote(SOURCE_BASE + '.harness-version');
|
|
251
|
+
if (isTemplate(raw)) {
|
|
252
|
+
if (jsonOut) {
|
|
253
|
+
console.log(JSON.stringify({ status: 'error', message: 'Remote .harness-version is a template (contains {{placeholders}}). Cannot determine dead files.' }));
|
|
254
|
+
} else {
|
|
255
|
+
console.error('ERROR: Remote .harness-version is a template (contains placeholders).');
|
|
256
|
+
console.error(' Cannot determine what files have been removed from the template.');
|
|
257
|
+
console.error(' Re-run after the remote template is generated.');
|
|
258
|
+
}
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
remoteVersion = JSON.parse(raw);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
if (jsonOut) {
|
|
264
|
+
console.log(JSON.stringify({ status: 'error', message: 'Cannot reach GitHub or invalid remote JSON: ' + e.message }));
|
|
265
|
+
} else {
|
|
266
|
+
console.error('ERROR: Cannot reach GitHub or invalid remote JSON. Offline?');
|
|
267
|
+
console.error(e.message);
|
|
268
|
+
}
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const remoteChecksums = remoteVersion.checksums || {};
|
|
273
|
+
|
|
274
|
+
// 3. Classify dead files
|
|
275
|
+
// DEAD = in local checksums but NOT in remote checksums, AND NOT preserved
|
|
276
|
+
const deadFiles = [];
|
|
277
|
+
for (const file of Object.keys(localChecksums)) {
|
|
278
|
+
const canonical = canonicalPath(file);
|
|
279
|
+
if (remoteChecksums[file]) continue; // still in remote, not dead
|
|
280
|
+
if (isPreserved(canonical)) continue; // user data, never dead
|
|
281
|
+
deadFiles.push({ file, reason: 'not in remote template' });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 4. Find orphan files
|
|
285
|
+
const orphanFiles = findOrphanFiles(localChecksums, remoteChecksums);
|
|
286
|
+
|
|
287
|
+
// 5. Find empty dirs (predictive — what WOULD become empty)
|
|
288
|
+
const deadPaths = deadFiles.map(d => canonicalPath(d.file));
|
|
289
|
+
const emptyDirs = findEmptyDirs(deadPaths);
|
|
290
|
+
|
|
291
|
+
// 6. Output
|
|
292
|
+
if (jsonOut) {
|
|
293
|
+
const status = deadFiles.length > 0 || orphanFiles.length > 0 ? 'dead-found' : 'clean';
|
|
294
|
+
console.log(JSON.stringify({
|
|
295
|
+
status,
|
|
296
|
+
dead: deadFiles,
|
|
297
|
+
orphan: orphanFiles,
|
|
298
|
+
emptyDirs,
|
|
299
|
+
summary: {
|
|
300
|
+
dead: deadFiles.length,
|
|
301
|
+
orphan: orphanFiles.length,
|
|
302
|
+
emptyDirs: emptyDirs.length,
|
|
303
|
+
},
|
|
304
|
+
}, null, 2));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── Report mode ──────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
if (!clean) {
|
|
311
|
+
if (deadFiles.length === 0 && orphanFiles.length === 0) {
|
|
312
|
+
console.log('✅ Harness is clean — no dead files detected.');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (deadFiles.length > 0) {
|
|
317
|
+
console.log('DEAD FILES (safe to delete — tracked locally but not in remote template):');
|
|
318
|
+
for (const d of deadFiles) {
|
|
319
|
+
const onDisk = existsSync(safePath(d.file));
|
|
320
|
+
const marker = onDisk ? '' : ' [already deleted on disk]';
|
|
321
|
+
console.log(` ${d.file}${marker}`);
|
|
322
|
+
}
|
|
323
|
+
console.log('');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (orphanFiles.length > 0) {
|
|
327
|
+
console.log('ORPHAN FILES (review needed — untracked in framework directories):');
|
|
328
|
+
for (const o of orphanFiles) {
|
|
329
|
+
console.log(` ${o.file}`);
|
|
330
|
+
}
|
|
331
|
+
console.log('');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (emptyDirs.length > 0) {
|
|
335
|
+
console.log('EMPTY DIRS (would become empty after cleaning):');
|
|
336
|
+
for (const d of emptyDirs) {
|
|
337
|
+
console.log(` ${d}/`);
|
|
338
|
+
}
|
|
339
|
+
console.log('');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
console.log(`Summary: ${deadFiles.length} dead, ${orphanFiles.length} orphan, ${emptyDirs.length} empty dirs.`);
|
|
343
|
+
if (deadFiles.length > 0) {
|
|
344
|
+
console.log('Run with --clean to delete dead files.');
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── Clean mode ───────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
if (deadFiles.length === 0) {
|
|
352
|
+
console.log('✅ Harness is clean — no dead files to delete.');
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Show what will be deleted
|
|
357
|
+
console.log(`Will delete ${deadFiles.length} dead file(s):`);
|
|
358
|
+
for (const d of deadFiles) {
|
|
359
|
+
console.log(` ${d.file}`);
|
|
360
|
+
}
|
|
361
|
+
console.log('');
|
|
362
|
+
|
|
363
|
+
// Confirmation prompt (skip if --yes)
|
|
364
|
+
if (!yes) {
|
|
365
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
366
|
+
const answer = await new Promise(resolve => {
|
|
367
|
+
rl.question(`Delete ${deadFiles.length} dead files? [y/N]: `, ans => {
|
|
368
|
+
rl.close();
|
|
369
|
+
resolve(ans.trim().toLowerCase());
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
373
|
+
console.log('Aborted.');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
console.log('');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Delete dead files
|
|
380
|
+
let deletedCount = 0;
|
|
381
|
+
let failedCount = 0;
|
|
382
|
+
|
|
383
|
+
for (const d of deadFiles) {
|
|
384
|
+
const diskPath = safePath(d.file);
|
|
385
|
+
if (!diskPath) {
|
|
386
|
+
if (!jsonOut) console.error(` ✗ Traversal rejected: ${d.file}`);
|
|
387
|
+
failedCount++;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Re-verify PRESERVE before deletion (defense in depth)
|
|
392
|
+
if (isPreserved(canonicalPath(d.file))) {
|
|
393
|
+
if (!jsonOut) console.error(` ✗ PRESERVE override — skipped: ${d.file}`);
|
|
394
|
+
failedCount++;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// If file doesn't exist on disk, just clean the checksum entry
|
|
399
|
+
if (!existsSync(diskPath)) {
|
|
400
|
+
delete localChecksums[d.file];
|
|
401
|
+
deletedCount++;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Symlink rejection
|
|
406
|
+
try {
|
|
407
|
+
if (lstatSync(diskPath).isSymbolicLink()) {
|
|
408
|
+
if (!jsonOut) console.error(` ✗ Symlink rejected: ${d.file}`);
|
|
409
|
+
failedCount++;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
} catch (_) {
|
|
413
|
+
if (!jsonOut) console.error(` ✗ Cannot stat: ${d.file}`);
|
|
414
|
+
failedCount++;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Delete the file
|
|
419
|
+
try {
|
|
420
|
+
unlinkSync(diskPath);
|
|
421
|
+
delete localChecksums[d.file];
|
|
422
|
+
deletedCount++;
|
|
423
|
+
} catch (e) {
|
|
424
|
+
if (!jsonOut) console.error(` ✗ Failed to delete: ${d.file} — ${e.message}`);
|
|
425
|
+
failedCount++;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Remove empty parent directories
|
|
430
|
+
let emptyRemoved = 0;
|
|
431
|
+
for (const d of deadFiles) {
|
|
432
|
+
const diskPath = safePath(d.file);
|
|
433
|
+
if (!diskPath) continue;
|
|
434
|
+
const parentDir = dirname(diskPath);
|
|
435
|
+
if (existsSync(parentDir)) {
|
|
436
|
+
const removed = removeEmptyParents(parentDir);
|
|
437
|
+
emptyRemoved += removed.length;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Write updated checksums
|
|
442
|
+
try {
|
|
443
|
+
writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
|
|
444
|
+
} catch (e) {
|
|
445
|
+
console.error(` ✗ Failed to update .harness-version: ${e.message}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Summary
|
|
449
|
+
if (failedCount > 0) {
|
|
450
|
+
console.log(`\n⚠ Cleaned ${deletedCount} files, removed ${emptyRemoved} empty directories. ${failedCount} failed (see above).`);
|
|
451
|
+
} else {
|
|
452
|
+
console.log(`\n✅ Cleaned ${deletedCount} files, removed ${emptyRemoved} empty directories.`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
main().catch(e => { console.error(e); process.exit(1); });
|
|
@@ -27,6 +27,10 @@ const commonAgents = [
|
|
|
27
27
|
'verifier',
|
|
28
28
|
'memory-master',
|
|
29
29
|
'context-master',
|
|
30
|
+
'explore-manager',
|
|
31
|
+
'architect-manager',
|
|
32
|
+
'implement-manager',
|
|
33
|
+
'review-manager',
|
|
30
34
|
];
|
|
31
35
|
|
|
32
36
|
const commonSkills = [
|
|
@@ -36,6 +40,7 @@ const commonSkills = [
|
|
|
36
40
|
'wf-learn',
|
|
37
41
|
'subagent-orchestrator',
|
|
38
42
|
'wf-readme',
|
|
43
|
+
'wf-remove',
|
|
39
44
|
];
|
|
40
45
|
|
|
41
46
|
const memoryFiles = [
|
|
@@ -72,6 +77,10 @@ const required = [
|
|
|
72
77
|
'Harness/research/PRD.md',
|
|
73
78
|
'.claude/skills/wf-update/SKILL.md',
|
|
74
79
|
'.claude/commands/wf-update.md',
|
|
80
|
+
'Harness/scripts/wf-update-check.mjs',
|
|
81
|
+
'Harness/scripts/wf-remove.mjs',
|
|
82
|
+
'Harness/scripts/scan-clean.mjs',
|
|
83
|
+
'.claude/commands/wf-remove.md',
|
|
75
84
|
'Harness/.harness-version',
|
|
76
85
|
];
|
|
77
86
|
|