create-harness-vibe-coding 0.8.6 → 0.8.8

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 (93) hide show
  1. package/README-CN.md +157 -105
  2. package/README.md +160 -244
  3. package/bin/create-harness-vibe-coding.js +2 -2
  4. package/docs/images/harness-architecture-light.png +0 -0
  5. package/docs/images/harness-architecture.drawio +164 -0
  6. package/package.json +46 -44
  7. package/src/generator.js +30 -11
  8. package/src/index.js +143 -14
  9. package/src/prompts.js +37 -37
  10. package/templates/common/.claude/agents/architect-manager.md +45 -45
  11. package/templates/common/.claude/agents/context-master.md +75 -75
  12. package/templates/common/.claude/agents/debugger.md +41 -41
  13. package/templates/common/.claude/agents/explore-manager.md +41 -41
  14. package/templates/common/.claude/agents/implement-manager.md +49 -49
  15. package/templates/common/.claude/agents/implementer.md +40 -40
  16. package/templates/common/.claude/agents/memory-master.md +64 -64
  17. package/templates/common/.claude/agents/planner.md +34 -34
  18. package/templates/common/.claude/agents/reflector.md +35 -0
  19. package/templates/common/.claude/agents/researcher.md +41 -41
  20. package/templates/common/.claude/agents/review-manager.md +56 -56
  21. package/templates/common/.claude/agents/verifier.md +34 -32
  22. package/templates/common/.claude/commands/wf-help.md +2 -3
  23. package/templates/common/.claude/rules/ecc/common.md +44 -44
  24. package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +10 -4
  25. package/templates/common/.claude/skills/wf/SKILL.md +7 -3
  26. package/templates/common/.claude/skills/wf-auto/SKILL.md +61 -107
  27. package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +19 -17
  28. package/templates/common/.claude/skills/wf-max/SKILL.md +40 -21
  29. package/templates/common/.claude/skills/wf-readme/SKILL.md +49 -49
  30. package/templates/common/.claude/skills/wf-remove/SKILL.md +7 -7
  31. package/templates/common/.claude/skills/wf-update/SKILL.md +9 -4
  32. package/templates/common/.codex/config.toml +5 -0
  33. package/templates/common/.harness-version +78 -36
  34. package/templates/common/.opencode/agents/architect-manager.md +52 -0
  35. package/templates/common/.opencode/agents/architect.md +35 -0
  36. package/templates/common/.opencode/agents/context-master.md +81 -0
  37. package/templates/common/.opencode/agents/debugger.md +43 -0
  38. package/templates/common/.opencode/agents/docs-researcher.md +42 -0
  39. package/templates/common/.opencode/agents/explore-manager.md +49 -0
  40. package/templates/common/.opencode/agents/implement-manager.md +56 -0
  41. package/templates/common/.opencode/agents/implementer.md +42 -0
  42. package/templates/common/.opencode/agents/memory-master.md +70 -0
  43. package/templates/common/.opencode/agents/planner.md +38 -0
  44. package/templates/common/.opencode/agents/reflector.md +39 -0
  45. package/templates/common/.opencode/agents/researcher.md +42 -0
  46. package/templates/common/.opencode/agents/review-manager.md +63 -0
  47. package/templates/common/.opencode/agents/reviewer.md +37 -0
  48. package/templates/common/.opencode/agents/tdd-guide.md +83 -0
  49. package/templates/common/.opencode/agents/test-writer.md +54 -0
  50. package/templates/common/.opencode/agents/verifier.md +37 -0
  51. package/templates/common/.opencode/commands/wf-help.md +23 -0
  52. package/templates/common/AGENTS.md +26 -25
  53. package/templates/common/CLAUDE.md +86 -88
  54. package/templates/common/Harness/ACCEPTANCE_PROTOCOL.md +12 -4
  55. package/templates/common/Harness/PROGRESS.md +17 -17
  56. package/templates/common/Harness/README.md +26 -16
  57. package/templates/common/Harness/WF-AUTO-ANGLES.md +170 -0
  58. package/templates/common/Harness/WF-AUTO-SPARK.md +23 -6
  59. package/templates/common/Harness/WF-AUTO.md +508 -493
  60. package/templates/common/Harness/WF-MAX.md +284 -232
  61. package/templates/common/Harness/WF.md +47 -29
  62. package/templates/common/Harness/agent-workflow.md +108 -76
  63. package/templates/common/Harness/architecture.md +124 -124
  64. package/templates/common/Harness/context-loading.md +111 -111
  65. package/templates/common/Harness/dispatch.md +96 -95
  66. package/templates/common/Harness/extension.md +67 -67
  67. package/templates/common/Harness/lifecycle.md +20 -20
  68. package/templates/common/Harness/research/PRD.md +56 -56
  69. package/templates/common/Harness/research/README.md +169 -169
  70. package/templates/common/Harness/research/research-results.md +66 -66
  71. package/templates/common/Harness/subagents.md +192 -170
  72. package/templates/common/Harness/tasks/_template/ARTIFACTS.md +3 -3
  73. package/templates/common/Harness/tasks/_template/NOTES.md +3 -3
  74. package/templates/common/Harness/tasks/_template/PLAN.md +53 -60
  75. package/templates/common/Harness/tasks/_template/PROGRESS.md +26 -29
  76. package/templates/common/MEMORY.md +27 -30
  77. package/templates/common/README.md +36 -36
  78. package/templates/common/SETUP.md +1 -1
  79. package/templates/common/memory/agent-lessons-patterns.md +21 -21
  80. package/templates/common/memory/tool-usage-reflections.md +21 -21
  81. package/templates/common/memory/user-corrections-preferences.md +21 -21
  82. package/templates/common/opencode.json +19 -0
  83. package/templates/common/scripts/scan-clean.mjs +487 -448
  84. package/templates/common/scripts/validate-harness.mjs +245 -146
  85. package/templates/common/scripts/wf-remove.mjs +311 -293
  86. package/templates/common/scripts/wf-update-check.mjs +511 -311
  87. package/templates/optional/catalog.json +41 -33
  88. package/templates/optional/skills/browser-e2e/.claude/skills/wf-browser/SKILL.md +194 -194
  89. package/templates/optional/skills/browser-e2e/Harness/workflows/browser-e2e.md +105 -69
  90. package/templates/optional/skills/github-pr-review/Harness/workflows/github-pr-review.md +28 -28
  91. package/templates/optional/skills/python-backend/Harness/workflows/python-backend.md +34 -34
  92. package/templates/optional/skills/ts-react-frontend/Harness/workflows/ts-react-frontend.md +34 -34
  93. package/templates/optional/skills/ui-ux-review/Harness/workflows/ui-ux-review.md +26 -26
@@ -1,64 +1,66 @@
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
+ '.opencode/',
42
+ 'opencode.json',
41
43
  '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. */
44
+ 'CLAUDE.md',
45
+ 'AGENTS.md',
46
+ 'MEMORY.md',
47
+ 'tests/.gitkeep',
48
+ ];
49
+
50
+ /** Files the user owns �?NEVER remove. Consistent with wf-update-check PRESERVE_PATTERNS. */
49
51
  const USER_DATA_PATTERNS = [
50
52
  /^Harness\/PROGRESS\.md$/,
51
53
  /^Harness\/tasks\//,
52
54
  /^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$/,
55
+ /^Harness\/research\/PRD\.md$/,
56
+ /^Harness\/research\/research-results\.md$/,
57
+ /^Harness\/architecture\.md$/,
58
+ /^Harness\/workflows\//,
59
+ /^Harness\/features\//,
60
+ /^Harness\/domain\//,
61
+ /^README\.md$/,
62
+ /^\.gitignore$/,
63
+ /^package\.json$/,
62
64
  /^package-lock\.json$/,
63
65
  ];
64
66
 
@@ -114,6 +116,7 @@ const BUILT_IN_AGENT_NAMES = [
114
116
  'implementer',
115
117
  'memory-master',
116
118
  'planner',
119
+ 'reflector',
117
120
  'researcher',
118
121
  'review-manager',
119
122
  'reviewer',
@@ -143,13 +146,18 @@ const BUILT_IN_SKILL_NAMES = [
143
146
  ];
144
147
 
145
148
  const KNOWN_FRAMEWORK_FILES = new Set([
146
- ...BUILT_IN_AGENT_NAMES.map(name => `.claude/agents/${name}.md`),
149
+ ...BUILT_IN_AGENT_NAMES.flatMap(name => [
150
+ `.claude/agents/${name}.md`,
151
+ `.opencode/agents/${name}.md`,
152
+ ]),
147
153
  ...BUILT_IN_SKILL_NAMES.flatMap(name => [
148
154
  `.claude/skills/${name}/SKILL.md`,
149
155
  `.agents/skills/${name}/SKILL.md`,
150
156
  ]),
151
157
  '.claude/commands/wf-help.md',
158
+ '.opencode/commands/wf-help.md',
152
159
  '.claude/rules/ecc/common.md',
160
+ 'opencode.json',
153
161
  ]);
154
162
 
155
163
  function isKnownFrameworkFile(file) {
@@ -158,16 +166,16 @@ function isKnownFrameworkFile(file) {
158
166
 
159
167
  /** Directories to clean up if empty after file removal. */
160
168
  const CLEANUP_DIRS = [
161
- '.claude/agents',
169
+ '.claude/agents',
162
170
  '.claude/skills/wf-auto',
163
171
  '.claude/skills/wf-browser',
164
172
  '.claude/skills/tdd',
165
173
  '.claude/skills/wf',
166
174
  '.claude/skills/wf-learn',
167
- '.claude/skills/wf-max',
168
- '.claude/skills/wf-readme',
169
- '.claude/skills/wf-review',
170
- '.claude/skills/wf-update',
175
+ '.claude/skills/wf-max',
176
+ '.claude/skills/wf-readme',
177
+ '.claude/skills/wf-review',
178
+ '.claude/skills/wf-update',
171
179
  '.claude/skills/wf-remove',
172
180
  '.claude/skills/subagent-orchestrator',
173
181
  '.agents/skills/wf-auto',
@@ -184,34 +192,38 @@ const CLEANUP_DIRS = [
184
192
  '.agents/skills',
185
193
  '.agents',
186
194
  '.codex',
195
+ '.opencode/commands',
196
+ '.opencode/agents',
197
+ '.opencode/skills',
198
+ '.opencode',
187
199
  '.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
-
200
+ '.claude/rules',
201
+ 'Harness/scripts',
202
+ 'Harness/research',
203
+ 'Harness/workflows',
204
+ 'Harness/domain',
205
+ 'Harness/features',
206
+ 'Harness/memory',
207
+ 'Harness/tasks/_template',
208
+ 'Harness/tasks/auto',
209
+ 'Harness/tasks',
210
+ 'Harness',
211
+ ];
212
+
213
+ // ── Helpers ────────────────────────────────────────────────────────
214
+
215
+ /** Reject paths that escape ROOT (traversal, absolute, .., etc.). Sync with wf-update-check. */
216
+ function safePath(file) {
217
+ let normalized = file.replace(/\\/g, '/').replace(/^\/+/, '');
218
+ if (normalized.includes('//')) return null; // double slash bypass
219
+ if (normalized.split('/').some(p => p === '..')) return null;
220
+ if (file.startsWith('/') || file.startsWith('\\')) return null;
221
+ if (normalized === '.' || normalized === '') return null;
222
+ const resolved = resolve(ROOT, normalized);
223
+ if (!resolved.startsWith(ROOT + sep) && resolved !== ROOT) return null;
224
+ return resolved;
225
+ }
226
+
215
227
  function sha256File(path) {
216
228
  if (!existsSync(path)) return null;
217
229
  let content = readFileSync(path, 'utf-8');
@@ -288,34 +300,34 @@ function removePlannedFile(file) {
288
300
 
289
301
  function removeEmptyDirs(startDir) {
290
302
  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() {
303
+ try {
304
+ const entries = readdirSync(startDir);
305
+ for (const e of entries) {
306
+ const full = join(startDir, e);
307
+ if (lstatSync(full).isDirectory() && !lstatSync(full).isSymbolicLink()) removeEmptyDirs(full);
308
+ }
309
+ // After recursing, check if dir is now empty
310
+ const remaining = readdirSync(startDir);
311
+ if (remaining.length === 0) {
312
+ rmdirSync(startDir);
313
+ }
314
+ } catch (_) { /* permissions or already gone */ }
315
+ }
316
+
317
+ async function askUser(question) {
318
+ if (!process.stdin.isTTY) {
319
+ console.log(' (non-interactive �?defaulting to KEEP)');
320
+ return 'k';
321
+ }
322
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
323
+ return new Promise(resolve => {
324
+ rl.question(question, answer => { rl.close(); resolve(answer.trim().toLowerCase()); });
325
+ });
326
+ }
327
+
328
+ // ── Main ───────────────────────────────────────────────────────────
329
+
330
+ async function main() {
319
331
  const args = process.argv.slice(2);
320
332
  const apply = args.includes('--apply');
321
333
  const yes = args.includes('--yes');
@@ -323,13 +335,13 @@ async function main() {
323
335
  const purgeUserData = args.includes('--purge-user-data') || args.includes('--purge');
324
336
  const keepTasks = args.includes('--keep-tasks');
325
337
  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
-
338
+
339
+ // 1. Build file inventory
340
+ const localVersion = existsSync(VERSION_FILE)
341
+ ? JSON.parse(readFileSync(VERSION_FILE, 'utf-8'))
342
+ : { checksums: {} };
343
+ const storedChecksums = localVersion.checksums || {};
344
+
333
345
  // All files from checksums + known framework patterns
334
346
  const allFiles = new Set(Object.keys(storedChecksums));
335
347
  const scanIssues = [];
@@ -352,7 +364,7 @@ async function main() {
352
364
  ]) {
353
365
  addFilesUnder(allFiles, dir, scanIssues);
354
366
  }
355
-
367
+
356
368
  // Add framework files that might not be in checksums (newer additions)
357
369
  const extraPatterns = [
358
370
  ...KNOWN_FRAMEWORK_FILES,
@@ -372,27 +384,27 @@ async function main() {
372
384
  'Harness/scripts/wf-update-check.mjs',
373
385
  'Harness/scripts/wf-remove.mjs',
374
386
  ];
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
387
+ for (const f of extraPatterns) {
388
+ if (existsSync(resolve(ROOT, f))) allFiles.add(f);
389
+ }
390
+
391
+ // 2. Classify every file
392
+ const safe = []; // SAFE �?matches checksum, auto-remove
393
+ const modified = []; // MODIFIED �?user edited, must confirm
394
+ const user = []; // USER �?never remove
383
395
  const skipped = []; // File not on disk, traversal rejected, or framework-keep
384
396
  const purge = []; // Explicitly requested Harness user-data removal
385
397
  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
-
398
+
399
+ for (const file of [...allFiles].sort()) {
400
+ // Canonical normalization for classification
401
+ const canonical = file.replace(/\\/g, '/').replace(/\/+/g, '/');
402
+
403
+ if (KEEP_FRAMEWORK.has(canonical)) {
404
+ skipped.push({ file, reason: 'framework keep' });
405
+ continue;
406
+ }
407
+
396
408
  if (isUserData(canonical)) {
397
409
  if (purgeUserData && isPurgeableHarnessData(canonical, { keepTasks })) {
398
410
  purge.push({
@@ -401,64 +413,70 @@ async function main() {
401
413
  });
402
414
  continue;
403
415
  }
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
- }
416
+ user.push({ file, reason: 'user data �?NEVER removed' });
417
+ continue;
418
+ }
419
+
420
+ // Only allow deletion of files under harness-owned prefixes
421
+ if (!isHarnessOwned(canonical)) {
422
+ user.push({ file, reason: 'outside harness prefix �?NEVER removed' });
423
+ continue;
424
+ }
425
+
426
+ const diskPath = safePath(file);
427
+ if (!diskPath) {
428
+ skipped.push({ file, reason: 'path traversal rejected' });
429
+ continue;
430
+ }
431
+ if (!existsSync(diskPath)) {
432
+ skipped.push({ file, reason: 'not on disk' });
433
+ continue;
434
+ }
423
435
 
424
436
  const currentHash = sha256File(diskPath);
425
437
  const storedHash = storedChecksums[file];
426
438
 
427
439
  if (!storedHash) {
428
440
  if (isKnownFrameworkFile(canonical)) {
429
- safe.push({
430
- file,
431
- currentHash,
432
- storedHash: currentHash,
433
- reason: 'known framework file missing from legacy checksums',
434
- });
441
+ if (canonical === 'opencode.json') {
442
+ // opencode.json without provenance — may pre-date Harness install.
443
+ // Only auto-remove when checksum proves Harness created it.
444
+ modified.push({ file, currentHash, storedHash: 'none', reason: 'not in checksums (may be pre-existing user config)' });
445
+ } else {
446
+ safe.push({
447
+ file,
448
+ currentHash,
449
+ storedHash: currentHash,
450
+ reason: 'known framework file missing from legacy checksums',
451
+ });
452
+ }
435
453
  } else {
436
454
  modified.push({ file, currentHash, storedHash: 'none', reason: 'not in checksums' });
437
455
  }
438
456
  } else if (currentHash === storedHash) {
439
457
  safe.push({ file, currentHash, storedHash });
440
458
  } else {
441
- modified.push({ file, currentHash, storedHash, reason: 'user modified' });
442
- }
443
- }
444
-
459
+ modified.push({ file, currentHash, storedHash, reason: 'user modified' });
460
+ }
461
+ }
462
+
445
463
  // 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);
464
+ const settingsFile = resolve(ROOT, '.claude', 'settings.json');
465
+ if (existsSync(settingsFile)) {
466
+ const settingsHash = sha256File(settingsFile);
467
+ const storedSettingsHash = storedChecksums['.claude/settings.json'];
468
+ if (storedSettingsHash && settingsHash !== storedSettingsHash) {
469
+ // User modified settings �?move to modified
470
+ // Remove from safe if it was there
471
+ const idx = safe.findIndex(s => s.file === '.claude/settings.json');
472
+ if (idx >= 0) {
473
+ safe.splice(idx, 1);
456
474
  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
475
+ }
476
+ }
477
+ }
478
+
479
+ // 3. Output plan
462
480
  if (jsonOut) {
463
481
  console.log(JSON.stringify({
464
482
  safe: safe.map(s => s.file),
@@ -491,29 +509,29 @@ async function main() {
491
509
  }, null, 2));
492
510
  return;
493
511
  }
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
-
512
+
513
+ console.log('\n🧹 WF-REMOVE �?Harness framework removal plan\n');
514
+
515
+ if (safe.length > 0) {
516
+ console.log(`�?SAFE (${safe.length} files �?auto-remove, unmodified framework):`);
517
+ for (const s of safe) console.log(` �?${s.file}`);
518
+ console.log('');
519
+ }
520
+
521
+ if (modified.length > 0) {
522
+ console.log(`�?MODIFIED (${modified.length} files �?REQUIRE CONFIRMATION):`);
523
+ for (const m of modified) {
524
+ console.log(` ? ${m.file} [${m.reason}]`);
525
+ }
526
+ console.log('');
527
+ }
528
+
529
+ if (user.length > 0) {
530
+ console.log(`🔒 USER DATA (${user.length} files �?NEVER removed):`);
531
+ for (const u of user) console.log(` �?${u.file} [${u.reason}]`);
532
+ console.log('');
533
+ }
534
+
517
535
  if (purge.length > 0) {
518
536
  console.log(`PURGE (${purge.length} files - explicit user-data cleanup):`);
519
537
  for (const p of purge) console.log(` x ${p.file} [${p.reason}]`);
@@ -521,41 +539,41 @@ async function main() {
521
539
  }
522
540
 
523
541
  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)
542
+
543
+ if (!apply) {
544
+ console.log('DRY-RUN. Use --apply to execute the removal.\n');
545
+ return;
546
+ }
547
+
548
+ // 4. Apply �?remove SAFE files automatically (rehash before unlink)
531
549
  let safeRemoved = 0;
532
550
  let safeFailed = 0;
533
- for (const s of safe) {
534
- const diskPath = safePath(s.file);
535
- if (!diskPath) { console.error(` �?Traversal rejected: ${s.file}`); safeFailed++; continue; }
551
+ for (const s of safe) {
552
+ const diskPath = safePath(s.file);
553
+ if (!diskPath) { console.error(` �?Traversal rejected: ${s.file}`); safeFailed++; continue; }
536
554
  if (!existsSync(diskPath)) continue;
537
- // Re-verify hash hasn't changed since classification
538
- const currentHash = sha256File(diskPath);
539
- if (currentHash !== s.storedHash) {
540
- console.log(` �?Skipped (modified since classification): ${s.file}`);
555
+ // Re-verify hash hasn't changed since classification
556
+ const currentHash = sha256File(diskPath);
557
+ if (currentHash !== s.storedHash) {
558
+ console.log(` �?Skipped (modified since classification): ${s.file}`);
541
559
  safeFailed++;
542
560
  continue;
543
- }
544
- try {
545
- unlinkSync(diskPath);
546
- safeRemoved++;
547
- } catch (e) {
548
- console.error(` �?Failed to remove: ${s.file} �?${e.message}`);
561
+ }
562
+ try {
563
+ unlinkSync(diskPath);
564
+ safeRemoved++;
565
+ } catch (e) {
566
+ console.error(` �?Failed to remove: ${s.file} �?${e.message}`);
549
567
  safeFailed++;
550
- }
551
- }
552
- console.log(`�?Removed ${safeRemoved} safe files.`);
553
-
554
- // 5. Handle MODIFIED files �?prompt user
568
+ }
569
+ }
570
+ console.log(`�?Removed ${safeRemoved} safe files.`);
571
+
572
+ // 5. Handle MODIFIED files �?prompt user
555
573
  let modifiedRemoved = 0;
556
574
  let modifiedKept = 0;
557
575
  let modifiedFailed = 0;
558
-
576
+
559
577
  for (const m of modified) {
560
578
  const canonicalModified = m.file.replace(/\\/g, '/').replace(/\/+/g, '/');
561
579
  if (deleteModified.has(canonicalModified)) {
@@ -569,32 +587,32 @@ async function main() {
569
587
  }
570
588
  continue;
571
589
  }
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') {
590
+ if (yes) {
591
+ // Non-interactive mode: skip all modified
592
+ console.log(` �?Skipped (modified): ${m.file}`);
593
+ modifiedKept++;
594
+ continue;
595
+ }
596
+
597
+ console.log(`\n─── ${m.file} ───`);
598
+ console.log(` Reason: ${m.reason}`);
599
+ console.log(` [D]elete [K]eep (default)`);
600
+
601
+ const answer = await askUser(' Choose [d/k]: ');
602
+ if (answer === 'd' || answer === 'delete') {
585
603
  const diskPath = safePath(m.file);
586
- if (!diskPath) { console.error(` �?Traversal rejected: ${m.file}`); modifiedFailed++; continue; }
604
+ if (!diskPath) { console.error(` �?Traversal rejected: ${m.file}`); modifiedFailed++; continue; }
587
605
  try {
588
606
  unlinkSync(diskPath);
589
- console.log(` �?Deleted: ${m.file}`);
607
+ console.log(` �?Deleted: ${m.file}`);
590
608
  modifiedRemoved++;
591
609
  } catch (e) {
592
- console.error(` �?Failed: ${m.file} �?${e.message}`);
610
+ console.error(` �?Failed: ${m.file} �?${e.message}`);
593
611
  modifiedFailed++;
594
612
  }
595
- } else {
596
- console.log(` �?Kept: ${m.file}`);
597
- modifiedKept++;
613
+ } else {
614
+ console.log(` �?Kept: ${m.file}`);
615
+ modifiedKept++;
598
616
  }
599
617
  }
600
618
 
@@ -629,14 +647,14 @@ async function main() {
629
647
  }
630
648
 
631
649
  // 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
-
650
+ for (const dir of CLEANUP_DIRS) {
651
+ removeEmptyDirs(resolve(ROOT, dir));
652
+ }
653
+ // Remove root Harness/ if empty
654
+ removeEmptyDirs(resolve(ROOT, 'Harness'));
655
+ // Remove root .claude/ if empty (but this is rare)
656
+ removeEmptyDirs(resolve(ROOT, '.claude'));
657
+
640
658
  // 7. Remove version file if everything is gone
641
659
  if (existsSync(VERSION_FILE)) {
642
660
  const harPath = resolve(ROOT, 'Harness');
@@ -654,46 +672,46 @@ async function main() {
654
672
  }
655
673
  } else if (!existsSync(harPath) || (existsSync(harPath) && readdirSync(harPath).filter(f => f !== '.harness-version').length === 0)) {
656
674
  unlinkSync(VERSION_FILE);
657
- 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); });
675
+ console.log('�?Removed .harness-version (Harness directory empty).');
676
+ }
677
+ }
678
+
679
+ // 7.5 Prune stale checksums for files that no longer exist on disk
680
+ if (existsSync(VERSION_FILE)) {
681
+ const versionData = JSON.parse(readFileSync(VERSION_FILE, 'utf-8'));
682
+ const checksums = versionData.checksums || {};
683
+ let pruned = 0;
684
+ for (const file of Object.keys(checksums)) {
685
+ const diskPath = safePath(file);
686
+ if (!diskPath || !existsSync(diskPath)) {
687
+ delete checksums[file];
688
+ pruned++;
689
+ }
690
+ }
691
+ if (pruned > 0) {
692
+ versionData.checksums = checksums;
693
+ writeFileSync(VERSION_FILE, JSON.stringify(versionData, null, 2) + '\n', 'utf-8');
694
+ console.log(`�?Pruned ${pruned} stale checksum(s) from .harness-version.`);
695
+ }
696
+ }
697
+
698
+ // 8. Remove CLAUDE.md harness section if it only has harness binding
699
+ const claudePath = resolve(ROOT, 'CLAUDE.md');
700
+ if (existsSync(claudePath)) {
701
+ const content = readFileSync(claudePath, 'utf-8');
702
+ if (content.includes('Harness contract') || content.includes('Harness Binding')) {
703
+ const answer = yes ? 'k' : await askUser('\nCLAUDE.md contains Harness binding section. Remove it? [D]elete harness section / [K]eep as-is (default): ');
704
+ if (answer === 'd' || answer === 'delete') {
705
+ // Strip the harness section: everything from "## 1. Harness Binding" to the next "## "
706
+ const cleaned = content.replace(/## 1\. Harness Binding[\s\S]*?(?=## 2\.)/, '');
707
+ writeFileSync(claudePath, cleaned, 'utf-8');
708
+ console.log('�?Stripped Harness binding section from CLAUDE.md.');
709
+ }
710
+ }
711
+ }
712
+
713
+ console.log(`\n🏁 Done. Safe: ${safeRemoved} removed. Modified: ${modifiedRemoved} removed, ${modifiedKept} kept.`);
714
+ console.log(' Run `git status` to review changes.\n');
715
+ }
716
+
717
+ main().catch(e => { console.error(e); process.exit(1); });