create-harness-vibe-coding 0.8.6 → 0.8.7

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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/generator.js +30 -11
  3. package/src/index.js +129 -5
  4. package/templates/common/.claude/agents/reflector.md +35 -0
  5. package/templates/common/.claude/agents/verifier.md +5 -3
  6. package/templates/common/.claude/commands/wf-help.md +1 -2
  7. package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +10 -4
  8. package/templates/common/.claude/skills/wf/SKILL.md +7 -3
  9. package/templates/common/.claude/skills/wf-auto/SKILL.md +59 -107
  10. package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +19 -17
  11. package/templates/common/.claude/skills/wf-max/SKILL.md +40 -21
  12. package/templates/common/.claude/skills/wf-update/SKILL.md +9 -4
  13. package/templates/common/.codex/config.toml +5 -0
  14. package/templates/common/.harness-version +36 -34
  15. package/templates/common/AGENTS.md +26 -25
  16. package/templates/common/CLAUDE.md +10 -9
  17. package/templates/common/Harness/ACCEPTANCE_PROTOCOL.md +12 -4
  18. package/templates/common/Harness/README.md +10 -11
  19. package/templates/common/Harness/WF-AUTO-SPARK.md +18 -1
  20. package/templates/common/Harness/WF-AUTO.md +518 -492
  21. package/templates/common/Harness/WF-MAX.md +284 -232
  22. package/templates/common/Harness/WF.md +47 -29
  23. package/templates/common/Harness/agent-workflow.md +108 -76
  24. package/templates/common/Harness/dispatch.md +96 -95
  25. package/templates/common/Harness/extension.md +1 -1
  26. package/templates/common/Harness/subagents.md +78 -56
  27. package/templates/common/Harness/tasks/_template/ARTIFACTS.md +1 -1
  28. package/templates/common/Harness/tasks/_template/NOTES.md +1 -1
  29. package/templates/common/Harness/tasks/_template/PLAN.md +53 -60
  30. package/templates/common/Harness/tasks/_template/PROGRESS.md +26 -29
  31. package/templates/common/MEMORY.md +26 -29
  32. package/templates/common/SETUP.md +1 -1
  33. package/templates/common/scripts/scan-clean.mjs +80 -41
  34. package/templates/common/scripts/validate-harness.mjs +101 -31
  35. package/templates/common/scripts/wf-remove.mjs +279 -278
  36. package/templates/common/scripts/wf-update-check.mjs +395 -195
  37. package/templates/optional/skills/browser-e2e/.claude/skills/wf-browser/SKILL.md +1 -1
  38. package/templates/optional/skills/browser-e2e/Harness/workflows/browser-e2e.md +57 -21
@@ -1,64 +1,64 @@
1
- #!/usr/bin/env node
2
- /**
3
- * wf-remove.mjs �?Safely remove Harness framework files.
4
- *
5
- * Classifies all Harness-owned files into:
6
- * SAFE �?framework files matching stored checksums �?auto-remove
7
- * MODIFIED �?framework files edited by user �?MUST confirm
8
- * USER �?user data files �?NEVER remove
9
- *
10
- * Usage:
11
- * node Harness/scripts/wf-remove.mjs # dry-run: show plan
12
- * node Harness/scripts/wf-remove.mjs --apply # apply SAFE, prompt for MODIFIED
1
+ #!/usr/bin/env node
2
+ /**
3
+ * wf-remove.mjs �?Safely remove Harness framework files.
4
+ *
5
+ * Classifies all Harness-owned files into:
6
+ * SAFE �?framework files matching stored checksums �?auto-remove
7
+ * MODIFIED �?framework files edited by user �?MUST confirm
8
+ * USER �?user data files �?NEVER remove
9
+ *
10
+ * Usage:
11
+ * node Harness/scripts/wf-remove.mjs # dry-run: show plan
12
+ * node Harness/scripts/wf-remove.mjs --apply # apply SAFE, prompt for MODIFIED
13
13
  * node Harness/scripts/wf-remove.mjs --yes # auto-yes for SAFE only (non-interactive)
14
14
  * node Harness/scripts/wf-remove.mjs --json # JSON plan for AI consumption
15
15
  * node Harness/scripts/wf-remove.mjs --apply --yes # auto-apply SAFE, skip MODIFIED, report
16
16
  * node Harness/scripts/wf-remove.mjs --apply --yes --purge-user-data --keep-tasks
17
17
  * # remove Harness project facts, keep task records
18
18
  */
19
-
20
- import { readFileSync, writeFileSync, existsSync, unlinkSync, rmdirSync, readdirSync, lstatSync } from 'fs';
21
- import { createHash } from 'crypto';
22
- import { resolve, dirname, join, sep } from 'path';
23
- import { fileURLToPath } from 'url';
24
- import { createInterface } from 'readline';
25
-
19
+
20
+ import { readFileSync, writeFileSync, existsSync, unlinkSync, rmdirSync, readdirSync, lstatSync } from 'fs';
21
+ import { createHash } from 'crypto';
22
+ import { resolve, dirname, join, sep } from 'path';
23
+ import { fileURLToPath } from 'url';
24
+ import { createInterface } from 'readline';
25
+
26
26
  const SCRIPT_FILE = fileURLToPath(import.meta.url);
27
27
  const __dirname = dirname(SCRIPT_FILE);
28
28
  const SCRIPT_ROOT = resolve(__dirname, '..', '..');
29
- const ROOT = process.env.WF_ROOT
30
- ? resolve(process.env.WF_ROOT)
31
- : SCRIPT_ROOT;
32
- const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
33
-
34
- // ── Classification ─────────────────────────────────────────────────
35
-
36
- /** Path prefixes that Harness framework is allowed to own. ANY file outside these is NEVER removed. */
29
+ const ROOT = process.env.WF_ROOT
30
+ ? resolve(process.env.WF_ROOT)
31
+ : SCRIPT_ROOT;
32
+ const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
33
+
34
+ // ── Classification ─────────────────────────────────────────────────
35
+
36
+ /** Path prefixes that Harness framework is allowed to own. ANY file outside these is NEVER removed. */
37
37
  const HARNESS_PREFIXES = [
38
38
  '.claude/',
39
39
  '.agents/',
40
40
  '.codex/',
41
41
  'Harness/',
42
- 'CLAUDE.md',
43
- 'AGENTS.md',
44
- 'MEMORY.md',
45
- 'tests/.gitkeep',
46
- ];
47
-
48
- /** Files the user owns �?NEVER remove. Consistent with wf-update-check PRESERVE_PATTERNS. */
42
+ 'CLAUDE.md',
43
+ 'AGENTS.md',
44
+ 'MEMORY.md',
45
+ 'tests/.gitkeep',
46
+ ];
47
+
48
+ /** Files the user owns �?NEVER remove. Consistent with wf-update-check PRESERVE_PATTERNS. */
49
49
  const USER_DATA_PATTERNS = [
50
50
  /^Harness\/PROGRESS\.md$/,
51
51
  /^Harness\/tasks\//,
52
52
  /^Harness\/memory\//,
53
- /^Harness\/research\/PRD\.md$/,
54
- /^Harness\/research\/research-results\.md$/,
55
- /^Harness\/architecture\.md$/,
56
- /^Harness\/workflows\//,
57
- /^Harness\/features\//,
58
- /^Harness\/domain\//,
59
- /^README\.md$/,
60
- /^\.gitignore$/,
61
- /^package\.json$/,
53
+ /^Harness\/research\/PRD\.md$/,
54
+ /^Harness\/research\/research-results\.md$/,
55
+ /^Harness\/architecture\.md$/,
56
+ /^Harness\/workflows\//,
57
+ /^Harness\/features\//,
58
+ /^Harness\/domain\//,
59
+ /^README\.md$/,
60
+ /^\.gitignore$/,
61
+ /^package\.json$/,
62
62
  /^package-lock\.json$/,
63
63
  ];
64
64
 
@@ -114,6 +114,7 @@ const BUILT_IN_AGENT_NAMES = [
114
114
  'implementer',
115
115
  'memory-master',
116
116
  'planner',
117
+ 'reflector',
117
118
  'researcher',
118
119
  'review-manager',
119
120
  'reviewer',
@@ -158,16 +159,16 @@ function isKnownFrameworkFile(file) {
158
159
 
159
160
  /** Directories to clean up if empty after file removal. */
160
161
  const CLEANUP_DIRS = [
161
- '.claude/agents',
162
+ '.claude/agents',
162
163
  '.claude/skills/wf-auto',
163
164
  '.claude/skills/wf-browser',
164
165
  '.claude/skills/tdd',
165
166
  '.claude/skills/wf',
166
167
  '.claude/skills/wf-learn',
167
- '.claude/skills/wf-max',
168
- '.claude/skills/wf-readme',
169
- '.claude/skills/wf-review',
170
- '.claude/skills/wf-update',
168
+ '.claude/skills/wf-max',
169
+ '.claude/skills/wf-readme',
170
+ '.claude/skills/wf-review',
171
+ '.claude/skills/wf-update',
171
172
  '.claude/skills/wf-remove',
172
173
  '.claude/skills/subagent-orchestrator',
173
174
  '.agents/skills/wf-auto',
@@ -185,33 +186,33 @@ const CLEANUP_DIRS = [
185
186
  '.agents',
186
187
  '.codex',
187
188
  '.claude/rules/ecc',
188
- '.claude/rules',
189
- 'Harness/scripts',
190
- 'Harness/research',
191
- 'Harness/workflows',
192
- 'Harness/domain',
193
- 'Harness/features',
194
- 'Harness/memory',
195
- 'Harness/tasks/_template',
196
- 'Harness/tasks/auto',
197
- 'Harness/tasks',
198
- 'Harness',
199
- ];
200
-
201
- // ── Helpers ────────────────────────────────────────────────────────
202
-
203
- /** Reject paths that escape ROOT (traversal, absolute, .., etc.). Sync with wf-update-check. */
204
- function safePath(file) {
205
- let normalized = file.replace(/\\/g, '/').replace(/^\/+/, '');
206
- if (normalized.includes('//')) return null; // double slash bypass
207
- if (normalized.split('/').some(p => p === '..')) return null;
208
- if (file.startsWith('/') || file.startsWith('\\')) return null;
209
- if (normalized === '.' || normalized === '') return null;
210
- const resolved = resolve(ROOT, normalized);
211
- if (!resolved.startsWith(ROOT + sep) && resolved !== ROOT) return null;
212
- return resolved;
213
- }
214
-
189
+ '.claude/rules',
190
+ 'Harness/scripts',
191
+ 'Harness/research',
192
+ 'Harness/workflows',
193
+ 'Harness/domain',
194
+ 'Harness/features',
195
+ 'Harness/memory',
196
+ 'Harness/tasks/_template',
197
+ 'Harness/tasks/auto',
198
+ 'Harness/tasks',
199
+ 'Harness',
200
+ ];
201
+
202
+ // ── Helpers ────────────────────────────────────────────────────────
203
+
204
+ /** Reject paths that escape ROOT (traversal, absolute, .., etc.). Sync with wf-update-check. */
205
+ function safePath(file) {
206
+ let normalized = file.replace(/\\/g, '/').replace(/^\/+/, '');
207
+ if (normalized.includes('//')) return null; // double slash bypass
208
+ if (normalized.split('/').some(p => p === '..')) return null;
209
+ if (file.startsWith('/') || file.startsWith('\\')) return null;
210
+ if (normalized === '.' || normalized === '') return null;
211
+ const resolved = resolve(ROOT, normalized);
212
+ if (!resolved.startsWith(ROOT + sep) && resolved !== ROOT) return null;
213
+ return resolved;
214
+ }
215
+
215
216
  function sha256File(path) {
216
217
  if (!existsSync(path)) return null;
217
218
  let content = readFileSync(path, 'utf-8');
@@ -288,34 +289,34 @@ function removePlannedFile(file) {
288
289
 
289
290
  function removeEmptyDirs(startDir) {
290
291
  if (!existsSync(startDir)) return;
291
- try {
292
- const entries = readdirSync(startDir);
293
- for (const e of entries) {
294
- const full = join(startDir, e);
295
- if (lstatSync(full).isDirectory() && !lstatSync(full).isSymbolicLink()) removeEmptyDirs(full);
296
- }
297
- // After recursing, check if dir is now empty
298
- const remaining = readdirSync(startDir);
299
- if (remaining.length === 0) {
300
- rmdirSync(startDir);
301
- }
302
- } catch (_) { /* permissions or already gone */ }
303
- }
304
-
305
- async function askUser(question) {
306
- if (!process.stdin.isTTY) {
307
- console.log(' (non-interactive �?defaulting to KEEP)');
308
- return 'k';
309
- }
310
- const rl = createInterface({ input: process.stdin, output: process.stdout });
311
- return new Promise(resolve => {
312
- rl.question(question, answer => { rl.close(); resolve(answer.trim().toLowerCase()); });
313
- });
314
- }
315
-
316
- // ── Main ───────────────────────────────────────────────────────────
317
-
318
- async function main() {
292
+ try {
293
+ const entries = readdirSync(startDir);
294
+ for (const e of entries) {
295
+ const full = join(startDir, e);
296
+ if (lstatSync(full).isDirectory() && !lstatSync(full).isSymbolicLink()) removeEmptyDirs(full);
297
+ }
298
+ // After recursing, check if dir is now empty
299
+ const remaining = readdirSync(startDir);
300
+ if (remaining.length === 0) {
301
+ rmdirSync(startDir);
302
+ }
303
+ } catch (_) { /* permissions or already gone */ }
304
+ }
305
+
306
+ async function askUser(question) {
307
+ if (!process.stdin.isTTY) {
308
+ console.log(' (non-interactive �?defaulting to KEEP)');
309
+ return 'k';
310
+ }
311
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
312
+ return new Promise(resolve => {
313
+ rl.question(question, answer => { rl.close(); resolve(answer.trim().toLowerCase()); });
314
+ });
315
+ }
316
+
317
+ // ── Main ───────────────────────────────────────────────────────────
318
+
319
+ async function main() {
319
320
  const args = process.argv.slice(2);
320
321
  const apply = args.includes('--apply');
321
322
  const yes = args.includes('--yes');
@@ -323,13 +324,13 @@ async function main() {
323
324
  const purgeUserData = args.includes('--purge-user-data') || args.includes('--purge');
324
325
  const keepTasks = args.includes('--keep-tasks');
325
326
  const deleteModified = new Set(parseRepeatedFlag(args, '--delete-modified'));
326
-
327
- // 1. Build file inventory
328
- const localVersion = existsSync(VERSION_FILE)
329
- ? JSON.parse(readFileSync(VERSION_FILE, 'utf-8'))
330
- : { checksums: {} };
331
- const storedChecksums = localVersion.checksums || {};
332
-
327
+
328
+ // 1. Build file inventory
329
+ const localVersion = existsSync(VERSION_FILE)
330
+ ? JSON.parse(readFileSync(VERSION_FILE, 'utf-8'))
331
+ : { checksums: {} };
332
+ const storedChecksums = localVersion.checksums || {};
333
+
333
334
  // All files from checksums + known framework patterns
334
335
  const allFiles = new Set(Object.keys(storedChecksums));
335
336
  const scanIssues = [];
@@ -352,7 +353,7 @@ async function main() {
352
353
  ]) {
353
354
  addFilesUnder(allFiles, dir, scanIssues);
354
355
  }
355
-
356
+
356
357
  // Add framework files that might not be in checksums (newer additions)
357
358
  const extraPatterns = [
358
359
  ...KNOWN_FRAMEWORK_FILES,
@@ -372,27 +373,27 @@ async function main() {
372
373
  'Harness/scripts/wf-update-check.mjs',
373
374
  'Harness/scripts/wf-remove.mjs',
374
375
  ];
375
- for (const f of extraPatterns) {
376
- if (existsSync(resolve(ROOT, f))) allFiles.add(f);
377
- }
378
-
379
- // 2. Classify every file
380
- const safe = []; // SAFE �?matches checksum, auto-remove
381
- const modified = []; // MODIFIED �?user edited, must confirm
382
- const user = []; // USER �?never remove
376
+ for (const f of extraPatterns) {
377
+ if (existsSync(resolve(ROOT, f))) allFiles.add(f);
378
+ }
379
+
380
+ // 2. Classify every file
381
+ const safe = []; // SAFE �?matches checksum, auto-remove
382
+ const modified = []; // MODIFIED �?user edited, must confirm
383
+ const user = []; // USER �?never remove
383
384
  const skipped = []; // File not on disk, traversal rejected, or framework-keep
384
385
  const purge = []; // Explicitly requested Harness user-data removal
385
386
  skipped.push(...scanIssues);
386
-
387
- for (const file of [...allFiles].sort()) {
388
- // Canonical normalization for classification
389
- const canonical = file.replace(/\\/g, '/').replace(/\/+/g, '/');
390
-
391
- if (KEEP_FRAMEWORK.has(canonical)) {
392
- skipped.push({ file, reason: 'framework keep' });
393
- continue;
394
- }
395
-
387
+
388
+ for (const file of [...allFiles].sort()) {
389
+ // Canonical normalization for classification
390
+ const canonical = file.replace(/\\/g, '/').replace(/\/+/g, '/');
391
+
392
+ if (KEEP_FRAMEWORK.has(canonical)) {
393
+ skipped.push({ file, reason: 'framework keep' });
394
+ continue;
395
+ }
396
+
396
397
  if (isUserData(canonical)) {
397
398
  if (purgeUserData && isPurgeableHarnessData(canonical, { keepTasks })) {
398
399
  purge.push({
@@ -401,25 +402,25 @@ async function main() {
401
402
  });
402
403
  continue;
403
404
  }
404
- user.push({ file, reason: 'user data �?NEVER removed' });
405
- continue;
406
- }
407
-
408
- // Only allow deletion of files under harness-owned prefixes
409
- if (!isHarnessOwned(canonical)) {
410
- user.push({ file, reason: 'outside harness prefix �?NEVER removed' });
411
- continue;
412
- }
413
-
414
- const diskPath = safePath(file);
415
- if (!diskPath) {
416
- skipped.push({ file, reason: 'path traversal rejected' });
417
- continue;
418
- }
419
- if (!existsSync(diskPath)) {
420
- skipped.push({ file, reason: 'not on disk' });
421
- continue;
422
- }
405
+ user.push({ file, reason: 'user data �?NEVER removed' });
406
+ continue;
407
+ }
408
+
409
+ // Only allow deletion of files under harness-owned prefixes
410
+ if (!isHarnessOwned(canonical)) {
411
+ user.push({ file, reason: 'outside harness prefix �?NEVER removed' });
412
+ continue;
413
+ }
414
+
415
+ const diskPath = safePath(file);
416
+ if (!diskPath) {
417
+ skipped.push({ file, reason: 'path traversal rejected' });
418
+ continue;
419
+ }
420
+ if (!existsSync(diskPath)) {
421
+ skipped.push({ file, reason: 'not on disk' });
422
+ continue;
423
+ }
423
424
 
424
425
  const currentHash = sha256File(diskPath);
425
426
  const storedHash = storedChecksums[file];
@@ -438,27 +439,27 @@ async function main() {
438
439
  } else if (currentHash === storedHash) {
439
440
  safe.push({ file, currentHash, storedHash });
440
441
  } else {
441
- modified.push({ file, currentHash, storedHash, reason: 'user modified' });
442
- }
443
- }
444
-
442
+ modified.push({ file, currentHash, storedHash, reason: 'user modified' });
443
+ }
444
+ }
445
+
445
446
  // Also check for .claude/settings.json (might have user permissions)
446
- const settingsFile = resolve(ROOT, '.claude', 'settings.json');
447
- if (existsSync(settingsFile)) {
448
- const settingsHash = sha256File(settingsFile);
449
- const storedSettingsHash = storedChecksums['.claude/settings.json'];
450
- if (storedSettingsHash && settingsHash !== storedSettingsHash) {
451
- // User modified settings �?move to modified
452
- // Remove from safe if it was there
453
- const idx = safe.findIndex(s => s.file === '.claude/settings.json');
454
- if (idx >= 0) {
455
- safe.splice(idx, 1);
447
+ const settingsFile = resolve(ROOT, '.claude', 'settings.json');
448
+ if (existsSync(settingsFile)) {
449
+ const settingsHash = sha256File(settingsFile);
450
+ const storedSettingsHash = storedChecksums['.claude/settings.json'];
451
+ if (storedSettingsHash && settingsHash !== storedSettingsHash) {
452
+ // User modified settings �?move to modified
453
+ // Remove from safe if it was there
454
+ const idx = safe.findIndex(s => s.file === '.claude/settings.json');
455
+ if (idx >= 0) {
456
+ safe.splice(idx, 1);
456
457
  modified.push({ file: '.claude/settings.json', currentHash: settingsHash, storedHash: storedSettingsHash, reason: 'user modified settings (may contain permissions)' });
457
- }
458
- }
459
- }
460
-
461
- // 3. Output plan
458
+ }
459
+ }
460
+ }
461
+
462
+ // 3. Output plan
462
463
  if (jsonOut) {
463
464
  console.log(JSON.stringify({
464
465
  safe: safe.map(s => s.file),
@@ -491,29 +492,29 @@ async function main() {
491
492
  }, null, 2));
492
493
  return;
493
494
  }
494
-
495
- console.log('\n🧹 WF-REMOVE �?Harness framework removal plan\n');
496
-
497
- if (safe.length > 0) {
498
- console.log(`�?SAFE (${safe.length} files �?auto-remove, unmodified framework):`);
499
- for (const s of safe) console.log(` �?${s.file}`);
500
- console.log('');
501
- }
502
-
503
- if (modified.length > 0) {
504
- console.log(`�?MODIFIED (${modified.length} files �?REQUIRE CONFIRMATION):`);
505
- for (const m of modified) {
506
- console.log(` ? ${m.file} [${m.reason}]`);
507
- }
508
- console.log('');
509
- }
510
-
511
- if (user.length > 0) {
512
- console.log(`🔒 USER DATA (${user.length} files �?NEVER removed):`);
513
- for (const u of user) console.log(` �?${u.file} [${u.reason}]`);
514
- console.log('');
515
- }
516
-
495
+
496
+ console.log('\n🧹 WF-REMOVE �?Harness framework removal plan\n');
497
+
498
+ if (safe.length > 0) {
499
+ console.log(`�?SAFE (${safe.length} files �?auto-remove, unmodified framework):`);
500
+ for (const s of safe) console.log(` �?${s.file}`);
501
+ console.log('');
502
+ }
503
+
504
+ if (modified.length > 0) {
505
+ console.log(`�?MODIFIED (${modified.length} files �?REQUIRE CONFIRMATION):`);
506
+ for (const m of modified) {
507
+ console.log(` ? ${m.file} [${m.reason}]`);
508
+ }
509
+ console.log('');
510
+ }
511
+
512
+ if (user.length > 0) {
513
+ console.log(`🔒 USER DATA (${user.length} files �?NEVER removed):`);
514
+ for (const u of user) console.log(` �?${u.file} [${u.reason}]`);
515
+ console.log('');
516
+ }
517
+
517
518
  if (purge.length > 0) {
518
519
  console.log(`PURGE (${purge.length} files - explicit user-data cleanup):`);
519
520
  for (const p of purge) console.log(` x ${p.file} [${p.reason}]`);
@@ -521,41 +522,41 @@ async function main() {
521
522
  }
522
523
 
523
524
  console.log(`Summary: ${safe.length} safe, ${modified.length} need confirm, ${purge.length} purge, ${user.length} preserved, ${skipped.length} skipped\n`);
524
-
525
- if (!apply) {
526
- console.log('DRY-RUN. Use --apply to execute the removal.\n');
527
- return;
528
- }
529
-
530
- // 4. Apply �?remove SAFE files automatically (rehash before unlink)
525
+
526
+ if (!apply) {
527
+ console.log('DRY-RUN. Use --apply to execute the removal.\n');
528
+ return;
529
+ }
530
+
531
+ // 4. Apply �?remove SAFE files automatically (rehash before unlink)
531
532
  let safeRemoved = 0;
532
533
  let safeFailed = 0;
533
- for (const s of safe) {
534
- const diskPath = safePath(s.file);
534
+ for (const s of safe) {
535
+ const diskPath = safePath(s.file);
535
536
  if (!diskPath) { console.error(` �?Traversal rejected: ${s.file}`); safeFailed++; continue; }
536
537
  if (!existsSync(diskPath)) continue;
537
- // Re-verify hash hasn't changed since classification
538
- const currentHash = sha256File(diskPath);
539
- if (currentHash !== s.storedHash) {
538
+ // Re-verify hash hasn't changed since classification
539
+ const currentHash = sha256File(diskPath);
540
+ if (currentHash !== s.storedHash) {
540
541
  console.log(` �?Skipped (modified since classification): ${s.file}`);
541
542
  safeFailed++;
542
543
  continue;
543
- }
544
- try {
545
- unlinkSync(diskPath);
546
- safeRemoved++;
547
- } catch (e) {
544
+ }
545
+ try {
546
+ unlinkSync(diskPath);
547
+ safeRemoved++;
548
+ } catch (e) {
548
549
  console.error(` �?Failed to remove: ${s.file} �?${e.message}`);
549
550
  safeFailed++;
550
- }
551
- }
552
- console.log(`�?Removed ${safeRemoved} safe files.`);
553
-
551
+ }
552
+ }
553
+ console.log(`�?Removed ${safeRemoved} safe files.`);
554
+
554
555
  // 5. Handle MODIFIED files �?prompt user
555
556
  let modifiedRemoved = 0;
556
557
  let modifiedKept = 0;
557
558
  let modifiedFailed = 0;
558
-
559
+
559
560
  for (const m of modified) {
560
561
  const canonicalModified = m.file.replace(/\\/g, '/').replace(/\/+/g, '/');
561
562
  if (deleteModified.has(canonicalModified)) {
@@ -569,19 +570,19 @@ async function main() {
569
570
  }
570
571
  continue;
571
572
  }
572
- if (yes) {
573
- // Non-interactive mode: skip all modified
574
- console.log(` �?Skipped (modified): ${m.file}`);
575
- modifiedKept++;
576
- continue;
577
- }
578
-
579
- console.log(`\n─── ${m.file} ───`);
580
- console.log(` Reason: ${m.reason}`);
581
- console.log(` [D]elete [K]eep (default)`);
582
-
583
- const answer = await askUser(' Choose [d/k]: ');
584
- if (answer === 'd' || answer === 'delete') {
573
+ if (yes) {
574
+ // Non-interactive mode: skip all modified
575
+ console.log(` �?Skipped (modified): ${m.file}`);
576
+ modifiedKept++;
577
+ continue;
578
+ }
579
+
580
+ console.log(`\n─── ${m.file} ───`);
581
+ console.log(` Reason: ${m.reason}`);
582
+ console.log(` [D]elete [K]eep (default)`);
583
+
584
+ const answer = await askUser(' Choose [d/k]: ');
585
+ if (answer === 'd' || answer === 'delete') {
585
586
  const diskPath = safePath(m.file);
586
587
  if (!diskPath) { console.error(` �?Traversal rejected: ${m.file}`); modifiedFailed++; continue; }
587
588
  try {
@@ -592,9 +593,9 @@ async function main() {
592
593
  console.error(` �?Failed: ${m.file} �?${e.message}`);
593
594
  modifiedFailed++;
594
595
  }
595
- } else {
596
- console.log(` �?Kept: ${m.file}`);
597
- modifiedKept++;
596
+ } else {
597
+ console.log(` �?Kept: ${m.file}`);
598
+ modifiedKept++;
598
599
  }
599
600
  }
600
601
 
@@ -629,14 +630,14 @@ async function main() {
629
630
  }
630
631
 
631
632
  // 6. Cleanup empty directories
632
- for (const dir of CLEANUP_DIRS) {
633
- removeEmptyDirs(resolve(ROOT, dir));
634
- }
635
- // Remove root Harness/ if empty
636
- removeEmptyDirs(resolve(ROOT, 'Harness'));
637
- // Remove root .claude/ if empty (but this is rare)
638
- removeEmptyDirs(resolve(ROOT, '.claude'));
639
-
633
+ for (const dir of CLEANUP_DIRS) {
634
+ removeEmptyDirs(resolve(ROOT, dir));
635
+ }
636
+ // Remove root Harness/ if empty
637
+ removeEmptyDirs(resolve(ROOT, 'Harness'));
638
+ // Remove root .claude/ if empty (but this is rare)
639
+ removeEmptyDirs(resolve(ROOT, '.claude'));
640
+
640
641
  // 7. Remove version file if everything is gone
641
642
  if (existsSync(VERSION_FILE)) {
642
643
  const harPath = resolve(ROOT, 'Harness');
@@ -655,45 +656,45 @@ async function main() {
655
656
  } else if (!existsSync(harPath) || (existsSync(harPath) && readdirSync(harPath).filter(f => f !== '.harness-version').length === 0)) {
656
657
  unlinkSync(VERSION_FILE);
657
658
  console.log('�?Removed .harness-version (Harness directory empty).');
658
- }
659
- }
660
-
661
- // 7.5 Prune stale checksums for files that no longer exist on disk
662
- if (existsSync(VERSION_FILE)) {
663
- const versionData = JSON.parse(readFileSync(VERSION_FILE, 'utf-8'));
664
- const checksums = versionData.checksums || {};
665
- let pruned = 0;
666
- for (const file of Object.keys(checksums)) {
667
- const diskPath = safePath(file);
668
- if (!diskPath || !existsSync(diskPath)) {
669
- delete checksums[file];
670
- pruned++;
671
- }
672
- }
673
- if (pruned > 0) {
674
- versionData.checksums = checksums;
675
- writeFileSync(VERSION_FILE, JSON.stringify(versionData, null, 2) + '\n', 'utf-8');
676
- console.log(`�?Pruned ${pruned} stale checksum(s) from .harness-version.`);
677
- }
678
- }
679
-
680
- // 8. Remove CLAUDE.md harness section if it only has harness binding
681
- const claudePath = resolve(ROOT, 'CLAUDE.md');
682
- if (existsSync(claudePath)) {
683
- const content = readFileSync(claudePath, 'utf-8');
684
- if (content.includes('Harness contract') || content.includes('Harness Binding')) {
685
- const answer = yes ? 'k' : await askUser('\nCLAUDE.md contains Harness binding section. Remove it? [D]elete harness section / [K]eep as-is (default): ');
686
- if (answer === 'd' || answer === 'delete') {
687
- // Strip the harness section: everything from "## 1. Harness Binding" to the next "## "
688
- const cleaned = content.replace(/## 1\. Harness Binding[\s\S]*?(?=## 2\.)/, '');
689
- writeFileSync(claudePath, cleaned, 'utf-8');
690
- console.log('�?Stripped Harness binding section from CLAUDE.md.');
691
- }
692
- }
693
- }
694
-
695
- console.log(`\n🏁 Done. Safe: ${safeRemoved} removed. Modified: ${modifiedRemoved} removed, ${modifiedKept} kept.`);
696
- console.log(' Run `git status` to review changes.\n');
697
- }
698
-
699
- main().catch(e => { console.error(e); process.exit(1); });
659
+ }
660
+ }
661
+
662
+ // 7.5 Prune stale checksums for files that no longer exist on disk
663
+ if (existsSync(VERSION_FILE)) {
664
+ const versionData = JSON.parse(readFileSync(VERSION_FILE, 'utf-8'));
665
+ const checksums = versionData.checksums || {};
666
+ let pruned = 0;
667
+ for (const file of Object.keys(checksums)) {
668
+ const diskPath = safePath(file);
669
+ if (!diskPath || !existsSync(diskPath)) {
670
+ delete checksums[file];
671
+ pruned++;
672
+ }
673
+ }
674
+ if (pruned > 0) {
675
+ versionData.checksums = checksums;
676
+ writeFileSync(VERSION_FILE, JSON.stringify(versionData, null, 2) + '\n', 'utf-8');
677
+ console.log(`�?Pruned ${pruned} stale checksum(s) from .harness-version.`);
678
+ }
679
+ }
680
+
681
+ // 8. Remove CLAUDE.md harness section if it only has harness binding
682
+ const claudePath = resolve(ROOT, 'CLAUDE.md');
683
+ if (existsSync(claudePath)) {
684
+ const content = readFileSync(claudePath, 'utf-8');
685
+ if (content.includes('Harness contract') || content.includes('Harness Binding')) {
686
+ const answer = yes ? 'k' : await askUser('\nCLAUDE.md contains Harness binding section. Remove it? [D]elete harness section / [K]eep as-is (default): ');
687
+ if (answer === 'd' || answer === 'delete') {
688
+ // Strip the harness section: everything from "## 1. Harness Binding" to the next "## "
689
+ const cleaned = content.replace(/## 1\. Harness Binding[\s\S]*?(?=## 2\.)/, '');
690
+ writeFileSync(claudePath, cleaned, 'utf-8');
691
+ console.log('�?Stripped Harness binding section from CLAUDE.md.');
692
+ }
693
+ }
694
+ }
695
+
696
+ console.log(`\n🏁 Done. Safe: ${safeRemoved} removed. Modified: ${modifiedRemoved} removed, ${modifiedKept} kept.`);
697
+ console.log(' Run `git status` to review changes.\n');
698
+ }
699
+
700
+ main().catch(e => { console.error(e); process.exit(1); });