create-harness-vibe-coding 0.7.2 → 0.8.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.
Files changed (82) hide show
  1. package/README-CN.md +91 -155
  2. package/README.md +200 -177
  3. package/bin/create-harness-vibe-coding.js +2 -2
  4. package/package.json +45 -43
  5. package/src/generator.js +115 -77
  6. package/src/index.js +159 -49
  7. package/src/prompts.js +95 -27
  8. package/templates/common/.claude/agents/architect-manager.md +45 -45
  9. package/templates/common/.claude/agents/context-master.md +77 -77
  10. package/templates/common/.claude/agents/debugger.md +41 -41
  11. package/templates/common/.claude/agents/explore-manager.md +41 -41
  12. package/templates/common/.claude/agents/implement-manager.md +49 -49
  13. package/templates/common/.claude/agents/implementer.md +40 -40
  14. package/templates/common/.claude/agents/memory-master.md +66 -66
  15. package/templates/common/.claude/agents/planner.md +34 -34
  16. package/templates/common/.claude/agents/researcher.md +41 -41
  17. package/templates/common/.claude/agents/review-manager.md +56 -56
  18. package/templates/common/.claude/agents/test-writer.md +38 -38
  19. package/templates/common/.claude/agents/verifier.md +32 -32
  20. package/templates/common/.claude/rules/ecc/common.md +45 -45
  21. package/templates/common/.claude/settings.json +68 -68
  22. package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +36 -31
  23. package/templates/common/.claude/skills/wf/SKILL.md +36 -0
  24. package/templates/common/.claude/skills/wf-learn/SKILL.md +13 -28
  25. package/templates/common/.claude/skills/wf-max/SKILL.md +24 -88
  26. package/templates/common/.claude/skills/wf-readme/SKILL.md +49 -49
  27. package/templates/common/.claude/skills/wf-remove/SKILL.md +17 -38
  28. package/templates/common/.claude/skills/wf-review/SKILL.md +19 -69
  29. package/templates/common/.claude/skills/wf-update/SKILL.md +19 -61
  30. package/templates/common/.codex/config.toml +2 -2
  31. package/templates/common/.codex/hooks.json +37 -37
  32. package/templates/common/.harness-version +53 -51
  33. package/templates/common/AGENTS.md +30 -30
  34. package/templates/common/CLAUDE.md +94 -94
  35. package/templates/common/{docs/harness → Harness}/PROGRESS.md +17 -17
  36. package/templates/common/{docs → Harness}/README.md +18 -17
  37. package/templates/common/{docs/harness → Harness}/WF-MAX.md +223 -223
  38. package/templates/common/Harness/WF.md +217 -0
  39. package/templates/common/{docs/harness → Harness}/architecture.md +126 -126
  40. package/templates/common/{docs/harness → Harness}/dispatch.md +99 -99
  41. package/templates/common/{docs → Harness}/research/research-results.md +66 -66
  42. package/templates/common/{docs/harness → Harness}/subagents.md +194 -194
  43. package/templates/common/{docs → Harness}/tasks/_template/ARTIFACTS.md +3 -3
  44. package/templates/common/{docs → Harness}/tasks/_template/NOTES.md +3 -3
  45. package/templates/common/{docs → Harness}/tasks/_template/PLAN.md +40 -40
  46. package/templates/common/{docs → Harness}/tasks/_template/PROGRESS.md +29 -29
  47. package/templates/common/MEMORY.md +68 -66
  48. package/templates/common/README.md +44 -37
  49. package/templates/common/SETUP.md +41 -34
  50. package/templates/common/memory/agent-lessons-patterns.md +21 -21
  51. package/templates/common/memory/tool-usage-reflections.md +21 -21
  52. package/templates/common/memory/user-corrections-preferences.md +21 -21
  53. package/templates/common/scripts/scan-clean.mjs +450 -449
  54. package/templates/common/scripts/validate-harness.mjs +56 -34
  55. package/templates/common/scripts/wf-mode-hook.mjs +318 -318
  56. package/templates/common/scripts/wf-remove.mjs +400 -384
  57. package/templates/common/scripts/wf-statusline.ps1 +38 -38
  58. package/templates/common/scripts/wf-statusline.sh +48 -48
  59. package/templates/common/scripts/wf-update-check.mjs +389 -389
  60. package/templates/optional/catalog.json +63 -29
  61. package/templates/optional/skills/browser-e2e/.claude/skills/wf-browser/SKILL.md +194 -194
  62. package/templates/optional/skills/browser-e2e/{docs → Harness}/workflows/browser-e2e.md +75 -75
  63. package/templates/optional/skills/github-pr-review/{docs → Harness}/workflows/github-pr-review.md +28 -28
  64. package/templates/optional/skills/python-backend/{docs → Harness}/workflows/python-backend.md +34 -34
  65. package/templates/optional/skills/ts-react-frontend/{docs → Harness}/workflows/ts-react-frontend.md +35 -35
  66. package/templates/optional/skills/ui-ux-review/{docs → Harness}/workflows/ui-ux-review.md +26 -26
  67. package/templates/common/.claude/commands/wf-learn.md +0 -17
  68. package/templates/common/.claude/commands/wf-max.md +0 -49
  69. package/templates/common/.claude/commands/wf-remove.md +0 -23
  70. package/templates/common/.claude/commands/wf-review.md +0 -25
  71. package/templates/common/.claude/commands/wf-update.md +0 -17
  72. package/templates/common/.claude/commands/wf.md +0 -31
  73. package/templates/common/commands/wf-max.toml +0 -18
  74. package/templates/common/commands/wf-review.toml +0 -15
  75. package/templates/common/docs/harness/WF.md +0 -174
  76. package/templates/optional/skills/browser-e2e/.claude/commands/wf-browser.md +0 -25
  77. /package/templates/common/{docs/harness → Harness}/agent-workflow.md +0 -0
  78. /package/templates/common/{docs/harness → Harness}/context-loading.md +0 -0
  79. /package/templates/common/{docs/harness → Harness}/extension.md +0 -0
  80. /package/templates/common/{docs/harness → Harness}/lifecycle.md +0 -0
  81. /package/templates/common/{docs → Harness}/research/PRD.md +0 -0
  82. /package/templates/common/{docs → Harness}/research/README.md +0 -0
@@ -1,396 +1,412 @@
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
- * node Harness/scripts/wf-remove.mjs --yes # auto-yes for SAFE only (non-interactive)
14
- * node Harness/scripts/wf-remove.mjs --json # JSON plan for AI consumption
15
- * node Harness/scripts/wf-remove.mjs --apply --yes # auto-apply SAFE, skip MODIFIED, report
16
- */
17
-
18
- import { readFileSync, writeFileSync, existsSync, unlinkSync, rmdirSync, readdirSync, lstatSync } from 'fs';
19
- import { createHash } from 'crypto';
20
- import { resolve, dirname, join, sep } from 'path';
21
- import { fileURLToPath } from 'url';
22
- import { createInterface } from 'readline';
23
-
24
- const __dirname = dirname(fileURLToPath(import.meta.url));
25
- const ROOT = process.env.WF_ROOT
26
- ? resolve(process.env.WF_ROOT)
27
- : (existsSync(resolve(process.cwd(), 'Harness', '.harness-version'))
28
- ? process.cwd()
29
- : resolve(__dirname, '..', '..'));
30
- const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
31
-
32
- // ── Classification ─────────────────────────────────────────────────
33
-
34
- /** Path prefixes that Harness framework is allowed to own. ANY file outside these is NEVER removed. */
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
+ * node Harness/scripts/wf-remove.mjs --yes # auto-yes for SAFE only (non-interactive)
14
+ * node Harness/scripts/wf-remove.mjs --json # JSON plan for AI consumption
15
+ * node Harness/scripts/wf-remove.mjs --apply --yes # auto-apply SAFE, skip MODIFIED, report
16
+ */
17
+
18
+ import { readFileSync, writeFileSync, existsSync, unlinkSync, rmdirSync, readdirSync, lstatSync } from 'fs';
19
+ import { createHash } from 'crypto';
20
+ import { resolve, dirname, join, sep } from 'path';
21
+ import { fileURLToPath } from 'url';
22
+ import { createInterface } from 'readline';
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const ROOT = process.env.WF_ROOT
26
+ ? resolve(process.env.WF_ROOT)
27
+ : (existsSync(resolve(process.cwd(), 'Harness', '.harness-version'))
28
+ ? process.cwd()
29
+ : resolve(__dirname, '..', '..'));
30
+ const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
31
+
32
+ // ── Classification ─────────────────────────────────────────────────
33
+
34
+ /** Path prefixes that Harness framework is allowed to own. ANY file outside these is NEVER removed. */
35
35
  const HARNESS_PREFIXES = [
36
36
  '.claude/',
37
+ '.agents/',
38
+ '.codex/',
37
39
  'Harness/',
38
- 'CLAUDE.md',
39
- 'AGENTS.md',
40
- 'MEMORY.md',
41
- 'tests/.gitkeep',
42
- ];
43
-
44
- /** Files the user owns — NEVER remove. Consistent with wf-update-check PRESERVE_PATTERNS. */
45
- const USER_DATA_PATTERNS = [
46
- /^Harness\/PROGRESS\.md$/,
47
- /^Harness\/tasks\//,
48
- /^Harness\/memory\//,
49
- /^Harness\/research\/PRD\.md$/,
50
- /^Harness\/research\/research-results\.md$/,
51
- /^Harness\/architecture\.md$/,
52
- /^Harness\/workflows\//,
53
- /^Harness\/features\//,
54
- /^Harness\/domain\//,
55
- /^README\.md$/,
56
- /^\.gitignore$/,
57
- /^package\.json$/,
58
- /^package-lock\.json$/,
59
- ];
60
-
61
- function isUserData(file) {
62
- return USER_DATA_PATTERNS.some(p => p.test(file));
63
- }
64
-
65
- function isHarnessOwned(file) {
66
- return HARNESS_PREFIXES.some(p => file.startsWith(p));
67
- }
68
-
69
- /** Framework files that should always exist (not part of removal). */
70
- const KEEP_FRAMEWORK = new Set([
71
- 'Harness/.harness-version',
72
- ]);
73
-
74
- /** Directories to clean up if empty after file removal. */
75
- const CLEANUP_DIRS = [
76
- '.claude/agents',
40
+ 'CLAUDE.md',
41
+ 'AGENTS.md',
42
+ 'MEMORY.md',
43
+ 'tests/.gitkeep',
44
+ ];
45
+
46
+ /** Files the user owns — NEVER remove. Consistent with wf-update-check PRESERVE_PATTERNS. */
47
+ const USER_DATA_PATTERNS = [
48
+ /^Harness\/PROGRESS\.md$/,
49
+ /^Harness\/tasks\//,
50
+ /^Harness\/memory\//,
51
+ /^Harness\/research\/PRD\.md$/,
52
+ /^Harness\/research\/research-results\.md$/,
53
+ /^Harness\/architecture\.md$/,
54
+ /^Harness\/workflows\//,
55
+ /^Harness\/features\//,
56
+ /^Harness\/domain\//,
57
+ /^README\.md$/,
58
+ /^\.gitignore$/,
59
+ /^package\.json$/,
60
+ /^package-lock\.json$/,
61
+ ];
62
+
63
+ function isUserData(file) {
64
+ return USER_DATA_PATTERNS.some(p => p.test(file));
65
+ }
66
+
67
+ function isHarnessOwned(file) {
68
+ return HARNESS_PREFIXES.some(p => file.startsWith(p));
69
+ }
70
+
71
+ /** Framework files that should always exist (not part of removal). */
72
+ const KEEP_FRAMEWORK = new Set([
73
+ 'Harness/.harness-version',
74
+ ]);
75
+
76
+ /** Directories to clean up if empty after file removal. */
77
+ const CLEANUP_DIRS = [
78
+ '.claude/agents',
77
79
  '.claude/skills/wf-auto',
78
80
  '.claude/skills/wf-browser',
81
+ '.claude/skills/wf',
79
82
  '.claude/skills/wf-learn',
80
- '.claude/skills/wf-max',
81
- '.claude/skills/wf-readme',
82
- '.claude/skills/wf-review',
83
- '.claude/skills/wf-update',
83
+ '.claude/skills/wf-max',
84
+ '.claude/skills/wf-readme',
85
+ '.claude/skills/wf-review',
86
+ '.claude/skills/wf-update',
84
87
  '.claude/skills/wf-remove',
85
88
  '.claude/skills/subagent-orchestrator',
86
- '.claude/commands',
89
+ '.agents/skills/wf-auto',
90
+ '.agents/skills/wf-browser',
91
+ '.agents/skills/wf',
92
+ '.agents/skills/wf-learn',
93
+ '.agents/skills/wf-max',
94
+ '.agents/skills/wf-readme',
95
+ '.agents/skills/wf-review',
96
+ '.agents/skills/wf-update',
97
+ '.agents/skills/wf-remove',
98
+ '.agents/skills/subagent-orchestrator',
99
+ '.agents/skills',
100
+ '.agents',
101
+ '.codex',
87
102
  '.claude/rules/ecc',
88
- '.claude/rules',
89
- 'Harness/scripts',
90
- 'Harness/research',
91
- 'Harness/workflows',
92
- 'Harness/domain',
93
- 'Harness/features',
94
- 'Harness/memory',
95
- 'Harness/tasks/_template',
96
- 'Harness/tasks/auto',
97
- 'Harness/tasks',
98
- 'Harness',
99
- ];
100
-
101
- // ── Helpers ────────────────────────────────────────────────────────
102
-
103
- /** Reject paths that escape ROOT (traversal, absolute, .., etc.). Sync with wf-update-check. */
104
- function safePath(file) {
105
- let normalized = file.replace(/\\/g, '/').replace(/^\/+/, '');
106
- if (normalized.includes('//')) return null; // double slash bypass
107
- if (normalized.split('/').some(p => p === '..')) return null;
108
- if (file.startsWith('/') || file.startsWith('\\')) return null;
109
- if (normalized === '.' || normalized === '') return null;
110
- const resolved = resolve(ROOT, normalized);
111
- if (!resolved.startsWith(ROOT + sep) && resolved !== ROOT) return null;
112
- return resolved;
113
- }
114
-
115
- function sha256File(path) {
116
- if (!existsSync(path)) return null;
117
- let content = readFileSync(path, 'utf-8');
118
- content = content.replace(/\r\n/g, '\n');
119
- return 'sha256-' + createHash('sha256').update(content).digest('hex');
120
- }
121
-
122
- function removeEmptyDirs(startDir) {
123
- if (!existsSync(startDir)) return;
124
- try {
125
- const entries = readdirSync(startDir);
126
- for (const e of entries) {
127
- const full = join(startDir, e);
128
- if (lstatSync(full).isDirectory() && !lstatSync(full).isSymbolicLink()) removeEmptyDirs(full);
129
- }
130
- // After recursing, check if dir is now empty
131
- const remaining = readdirSync(startDir);
132
- if (remaining.length === 0) {
133
- rmdirSync(startDir);
134
- }
135
- } catch (_) { /* permissions or already gone */ }
136
- }
137
-
138
- async function askUser(question) {
139
- if (!process.stdin.isTTY) {
140
- console.log(' (non-interactive — defaulting to KEEP)');
141
- return 'k';
142
- }
143
- const rl = createInterface({ input: process.stdin, output: process.stdout });
144
- return new Promise(resolve => {
145
- rl.question(question, answer => { rl.close(); resolve(answer.trim().toLowerCase()); });
146
- });
147
- }
148
-
149
- // ── Main ───────────────────────────────────────────────────────────
150
-
151
- async function main() {
152
- const args = process.argv.slice(2);
153
- const apply = args.includes('--apply');
154
- const yes = args.includes('--yes');
155
- const jsonOut = args.includes('--json');
156
-
157
- // 1. Build file inventory
158
- const localVersion = existsSync(VERSION_FILE)
159
- ? JSON.parse(readFileSync(VERSION_FILE, 'utf-8'))
160
- : { checksums: {} };
161
- const storedChecksums = localVersion.checksums || {};
162
-
163
- // All files from checksums + known framework patterns
164
- const allFiles = new Set(Object.keys(storedChecksums));
165
-
166
- // Add framework files that might not be in checksums (newer additions)
103
+ '.claude/rules',
104
+ 'Harness/scripts',
105
+ 'Harness/research',
106
+ 'Harness/workflows',
107
+ 'Harness/domain',
108
+ 'Harness/features',
109
+ 'Harness/memory',
110
+ 'Harness/tasks/_template',
111
+ 'Harness/tasks/auto',
112
+ 'Harness/tasks',
113
+ 'Harness',
114
+ ];
115
+
116
+ // ── Helpers ────────────────────────────────────────────────────────
117
+
118
+ /** Reject paths that escape ROOT (traversal, absolute, .., etc.). Sync with wf-update-check. */
119
+ function safePath(file) {
120
+ let normalized = file.replace(/\\/g, '/').replace(/^\/+/, '');
121
+ if (normalized.includes('//')) return null; // double slash bypass
122
+ if (normalized.split('/').some(p => p === '..')) return null;
123
+ if (file.startsWith('/') || file.startsWith('\\')) return null;
124
+ if (normalized === '.' || normalized === '') return null;
125
+ const resolved = resolve(ROOT, normalized);
126
+ if (!resolved.startsWith(ROOT + sep) && resolved !== ROOT) return null;
127
+ return resolved;
128
+ }
129
+
130
+ function sha256File(path) {
131
+ if (!existsSync(path)) return null;
132
+ let content = readFileSync(path, 'utf-8');
133
+ content = content.replace(/\r\n/g, '\n');
134
+ return 'sha256-' + createHash('sha256').update(content).digest('hex');
135
+ }
136
+
137
+ function removeEmptyDirs(startDir) {
138
+ if (!existsSync(startDir)) return;
139
+ try {
140
+ const entries = readdirSync(startDir);
141
+ for (const e of entries) {
142
+ const full = join(startDir, e);
143
+ if (lstatSync(full).isDirectory() && !lstatSync(full).isSymbolicLink()) removeEmptyDirs(full);
144
+ }
145
+ // After recursing, check if dir is now empty
146
+ const remaining = readdirSync(startDir);
147
+ if (remaining.length === 0) {
148
+ rmdirSync(startDir);
149
+ }
150
+ } catch (_) { /* permissions or already gone */ }
151
+ }
152
+
153
+ async function askUser(question) {
154
+ if (!process.stdin.isTTY) {
155
+ console.log(' (non-interactive — defaulting to KEEP)');
156
+ return 'k';
157
+ }
158
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
159
+ return new Promise(resolve => {
160
+ rl.question(question, answer => { rl.close(); resolve(answer.trim().toLowerCase()); });
161
+ });
162
+ }
163
+
164
+ // ── Main ───────────────────────────────────────────────────────────
165
+
166
+ async function main() {
167
+ const args = process.argv.slice(2);
168
+ const apply = args.includes('--apply');
169
+ const yes = args.includes('--yes');
170
+ const jsonOut = args.includes('--json');
171
+
172
+ // 1. Build file inventory
173
+ const localVersion = existsSync(VERSION_FILE)
174
+ ? JSON.parse(readFileSync(VERSION_FILE, 'utf-8'))
175
+ : { checksums: {} };
176
+ const storedChecksums = localVersion.checksums || {};
177
+
178
+ // All files from checksums + known framework patterns
179
+ const allFiles = new Set(Object.keys(storedChecksums));
180
+
181
+ // Add framework files that might not be in checksums (newer additions)
167
182
  const extraPatterns = [
183
+ '.claude/skills/wf/SKILL.md',
184
+ '.agents/skills/wf/SKILL.md',
168
185
  '.claude/skills/wf-auto/SKILL.md',
169
- '.claude/commands/wf-auto.md',
170
- '.claude/commands/wf-auto-spark.md',
186
+ '.agents/skills/wf-auto/SKILL.md',
171
187
  'Harness/WF-AUTO.md',
172
- 'Harness/tasks/auto/PROGRESS.md',
173
- 'Harness/tasks/auto/PLAN.md',
174
- 'Harness/scripts/wf-update-check.mjs',
175
- 'Harness/scripts/wf-remove.mjs',
176
- ];
177
- for (const f of extraPatterns) {
178
- if (existsSync(resolve(ROOT, f))) allFiles.add(f);
179
- }
180
-
181
- // 2. Classify every file
182
- const safe = []; // SAFE — matches checksum, auto-remove
183
- const modified = []; // MODIFIED — user edited, must confirm
184
- const user = []; // USER — never remove
185
- const skipped = []; // File not on disk, traversal rejected, or framework-keep
186
-
187
- for (const file of [...allFiles].sort()) {
188
- // Canonical normalization for classification
189
- const canonical = file.replace(/\\/g, '/').replace(/\/+/g, '/');
190
-
191
- if (KEEP_FRAMEWORK.has(canonical)) {
192
- skipped.push({ file, reason: 'framework keep' });
193
- continue;
194
- }
195
-
196
- if (isUserData(canonical)) {
197
- user.push({ file, reason: 'user data — NEVER removed' });
198
- continue;
199
- }
200
-
201
- // Only allow deletion of files under harness-owned prefixes
202
- if (!isHarnessOwned(canonical)) {
203
- user.push({ file, reason: 'outside harness prefix — NEVER removed' });
204
- continue;
205
- }
206
-
207
- const diskPath = safePath(file);
208
- if (!diskPath) {
209
- skipped.push({ file, reason: 'path traversal rejected' });
210
- continue;
211
- }
212
- if (!existsSync(diskPath)) {
213
- skipped.push({ file, reason: 'not on disk' });
214
- continue;
215
- }
216
-
217
- const currentHash = sha256File(diskPath);
218
- const storedHash = storedChecksums[file];
219
-
220
- if (!storedHash) {
221
- modified.push({ file, currentHash, storedHash: 'none', reason: 'not in checksums' });
222
- } else if (currentHash === storedHash) {
223
- safe.push({ file, currentHash, storedHash });
224
- } else {
225
- modified.push({ file, currentHash, storedHash, reason: 'user modified' });
226
- }
227
- }
228
-
229
- // Also check for .claude/settings.json (might have user hooks)
230
- const settingsFile = resolve(ROOT, '.claude', 'settings.json');
231
- if (existsSync(settingsFile)) {
232
- const settingsHash = sha256File(settingsFile);
233
- const storedSettingsHash = storedChecksums['.claude/settings.json'];
234
- if (storedSettingsHash && settingsHash !== storedSettingsHash) {
235
- // User modified settings — move to modified
236
- // Remove from safe if it was there
237
- const idx = safe.findIndex(s => s.file === '.claude/settings.json');
238
- if (idx >= 0) {
239
- safe.splice(idx, 1);
240
- modified.push({ file: '.claude/settings.json', currentHash: settingsHash, storedHash: storedSettingsHash, reason: 'user modified settings (may contain hooks/permissions)' });
241
- }
242
- }
243
- }
244
-
245
- // 3. Output plan
246
- if (jsonOut) {
247
- console.log(JSON.stringify({
248
- safe: safe.map(s => s.file),
249
- modified: modified.map(m => m.file),
250
- user: user.map(u => u.file),
251
- totalRemove: safe.length + modified.length,
252
- totalSafe: safe.length,
253
- totalModified: modified.length,
254
- }, null, 2));
255
- return;
256
- }
257
-
258
- console.log('\n🧹 WF-REMOVE — Harness framework removal plan\n');
259
-
260
- if (safe.length > 0) {
261
- console.log(`✅ SAFE (${safe.length} files — auto-remove, unmodified framework):`);
262
- for (const s of safe) console.log(` ✕ ${s.file}`);
263
- console.log('');
264
- }
265
-
266
- if (modified.length > 0) {
267
- console.log(`⚠ MODIFIED (${modified.length} files — REQUIRE CONFIRMATION):`);
268
- for (const m of modified) {
269
- console.log(` ? ${m.file} [${m.reason}]`);
270
- }
271
- console.log('');
272
- }
273
-
274
- if (user.length > 0) {
275
- console.log(`🔒 USER DATA (${user.length} files — NEVER removed):`);
276
- for (const u of user) console.log(` ○ ${u.file} [${u.reason}]`);
277
- console.log('');
278
- }
279
-
280
- console.log(`Summary: ${safe.length} safe, ${modified.length} need confirm, ${user.length} preserved, ${skipped.length} skipped\n`);
281
-
282
- if (!apply) {
283
- console.log('DRY-RUN. Use --apply to execute the removal.\n');
284
- return;
285
- }
286
-
287
- // 4. Apply — remove SAFE files automatically (rehash before unlink)
288
- let safeRemoved = 0;
289
- for (const s of safe) {
290
- const diskPath = safePath(s.file);
291
- if (!diskPath) { console.error(` ✗ Traversal rejected: ${s.file}`); continue; }
292
- // Re-verify hash hasn't changed since classification
293
- const currentHash = sha256File(diskPath);
294
- if (currentHash !== s.storedHash) {
295
- console.log(` ⊘ Skipped (modified since classification): ${s.file}`);
296
- continue;
297
- }
298
- try {
299
- unlinkSync(diskPath);
300
- safeRemoved++;
301
- } catch (e) {
302
- console.error(` ✗ Failed to remove: ${s.file} — ${e.message}`);
303
- }
304
- }
305
- console.log(`✓ Removed ${safeRemoved} safe files.`);
306
-
307
- // 5. Handle MODIFIED files — prompt user
308
- let modifiedRemoved = 0;
309
- let modifiedKept = 0;
310
-
311
- for (const m of modified) {
312
- if (yes) {
313
- // Non-interactive mode: skip all modified
314
- console.log(` ⊘ Skipped (modified): ${m.file}`);
315
- modifiedKept++;
316
- continue;
317
- }
318
-
319
- console.log(`\n─── ${m.file} ───`);
320
- console.log(` Reason: ${m.reason}`);
321
- console.log(` [D]elete [K]eep (default)`);
322
-
323
- const answer = await askUser(' Choose [d/k]: ');
324
- if (answer === 'd' || answer === 'delete') {
325
- const diskPath = safePath(m.file);
326
- if (!diskPath) { console.error(` ✗ Traversal rejected: ${m.file}`); continue; }
327
- try {
328
- unlinkSync(diskPath);
329
- console.log(` ✓ Deleted: ${m.file}`);
330
- modifiedRemoved++;
331
- } catch (e) {
332
- console.error(` ✗ Failed: ${m.file} — ${e.message}`);
333
- }
334
- } else {
335
- console.log(` ⊘ Kept: ${m.file}`);
336
- modifiedKept++;
337
- }
338
- }
339
-
340
- // 6. Cleanup empty directories
341
- for (const dir of CLEANUP_DIRS) {
342
- removeEmptyDirs(resolve(ROOT, dir));
343
- }
344
- // Remove root Harness/ if empty
345
- removeEmptyDirs(resolve(ROOT, 'Harness'));
346
- // Remove root .claude/ if empty (but this is rare)
347
- removeEmptyDirs(resolve(ROOT, '.claude'));
348
-
349
- // 7. Remove version file if everything is gone
350
- if (existsSync(VERSION_FILE)) {
351
- const harPath = resolve(ROOT, 'Harness');
352
- if (!existsSync(harPath) || (existsSync(harPath) && readdirSync(harPath).filter(f => f !== '.harness-version').length === 0)) {
353
- unlinkSync(VERSION_FILE);
354
- console.log('✓ Removed .harness-version (Harness directory empty).');
355
- }
356
- }
357
-
358
- // 7.5 Prune stale checksums for files that no longer exist on disk
359
- if (existsSync(VERSION_FILE)) {
360
- const versionData = JSON.parse(readFileSync(VERSION_FILE, 'utf-8'));
361
- const checksums = versionData.checksums || {};
362
- let pruned = 0;
363
- for (const file of Object.keys(checksums)) {
364
- const diskPath = safePath(file);
365
- if (!diskPath || !existsSync(diskPath)) {
366
- delete checksums[file];
367
- pruned++;
368
- }
369
- }
370
- if (pruned > 0) {
371
- versionData.checksums = checksums;
372
- writeFileSync(VERSION_FILE, JSON.stringify(versionData, null, 2) + '\n', 'utf-8');
373
- console.log(`✓ Pruned ${pruned} stale checksum(s) from .harness-version.`);
374
- }
375
- }
376
-
377
- // 8. Remove CLAUDE.md harness section if it only has harness binding
378
- const claudePath = resolve(ROOT, 'CLAUDE.md');
379
- if (existsSync(claudePath)) {
380
- const content = readFileSync(claudePath, 'utf-8');
381
- if (content.includes('Harness contract') || content.includes('Harness Binding')) {
382
- const answer = yes ? 'k' : await askUser('\nCLAUDE.md contains Harness binding section. Remove it? [D]elete harness section / [K]eep as-is (default): ');
383
- if (answer === 'd' || answer === 'delete') {
384
- // Strip the harness section: everything from "## 1. Harness Binding" to the next "## "
385
- const cleaned = content.replace(/## 1\. Harness Binding[\s\S]*?(?=## 2\.)/, '');
386
- writeFileSync(claudePath, cleaned, 'utf-8');
387
- console.log('✓ Stripped Harness binding section from CLAUDE.md.');
388
- }
389
- }
390
- }
391
-
392
- console.log(`\n🏁 Done. Safe: ${safeRemoved} removed. Modified: ${modifiedRemoved} removed, ${modifiedKept} kept.`);
393
- console.log(' Run `git status` to review changes.\n');
394
- }
395
-
396
- main().catch(e => { console.error(e); process.exit(1); });
188
+ 'Harness/tasks/auto/PROGRESS.md',
189
+ 'Harness/tasks/auto/PLAN.md',
190
+ 'Harness/scripts/wf-update-check.mjs',
191
+ 'Harness/scripts/wf-remove.mjs',
192
+ ];
193
+ for (const f of extraPatterns) {
194
+ if (existsSync(resolve(ROOT, f))) allFiles.add(f);
195
+ }
196
+
197
+ // 2. Classify every file
198
+ const safe = []; // SAFE — matches checksum, auto-remove
199
+ const modified = []; // MODIFIED — user edited, must confirm
200
+ const user = []; // USER — never remove
201
+ const skipped = []; // File not on disk, traversal rejected, or framework-keep
202
+
203
+ for (const file of [...allFiles].sort()) {
204
+ // Canonical normalization for classification
205
+ const canonical = file.replace(/\\/g, '/').replace(/\/+/g, '/');
206
+
207
+ if (KEEP_FRAMEWORK.has(canonical)) {
208
+ skipped.push({ file, reason: 'framework keep' });
209
+ continue;
210
+ }
211
+
212
+ if (isUserData(canonical)) {
213
+ user.push({ file, reason: 'user data — NEVER removed' });
214
+ continue;
215
+ }
216
+
217
+ // Only allow deletion of files under harness-owned prefixes
218
+ if (!isHarnessOwned(canonical)) {
219
+ user.push({ file, reason: 'outside harness prefix — NEVER removed' });
220
+ continue;
221
+ }
222
+
223
+ const diskPath = safePath(file);
224
+ if (!diskPath) {
225
+ skipped.push({ file, reason: 'path traversal rejected' });
226
+ continue;
227
+ }
228
+ if (!existsSync(diskPath)) {
229
+ skipped.push({ file, reason: 'not on disk' });
230
+ continue;
231
+ }
232
+
233
+ const currentHash = sha256File(diskPath);
234
+ const storedHash = storedChecksums[file];
235
+
236
+ if (!storedHash) {
237
+ modified.push({ file, currentHash, storedHash: 'none', reason: 'not in checksums' });
238
+ } else if (currentHash === storedHash) {
239
+ safe.push({ file, currentHash, storedHash });
240
+ } else {
241
+ modified.push({ file, currentHash, storedHash, reason: 'user modified' });
242
+ }
243
+ }
244
+
245
+ // Also check for .claude/settings.json (might have user hooks)
246
+ const settingsFile = resolve(ROOT, '.claude', 'settings.json');
247
+ if (existsSync(settingsFile)) {
248
+ const settingsHash = sha256File(settingsFile);
249
+ const storedSettingsHash = storedChecksums['.claude/settings.json'];
250
+ if (storedSettingsHash && settingsHash !== storedSettingsHash) {
251
+ // User modified settings — move to modified
252
+ // Remove from safe if it was there
253
+ const idx = safe.findIndex(s => s.file === '.claude/settings.json');
254
+ if (idx >= 0) {
255
+ safe.splice(idx, 1);
256
+ modified.push({ file: '.claude/settings.json', currentHash: settingsHash, storedHash: storedSettingsHash, reason: 'user modified settings (may contain hooks/permissions)' });
257
+ }
258
+ }
259
+ }
260
+
261
+ // 3. Output plan
262
+ if (jsonOut) {
263
+ console.log(JSON.stringify({
264
+ safe: safe.map(s => s.file),
265
+ modified: modified.map(m => m.file),
266
+ user: user.map(u => u.file),
267
+ totalRemove: safe.length + modified.length,
268
+ totalSafe: safe.length,
269
+ totalModified: modified.length,
270
+ }, null, 2));
271
+ return;
272
+ }
273
+
274
+ console.log('\n🧹 WF-REMOVE — Harness framework removal plan\n');
275
+
276
+ if (safe.length > 0) {
277
+ console.log(`✅ SAFE (${safe.length} files — auto-remove, unmodified framework):`);
278
+ for (const s of safe) console.log(` ✕ ${s.file}`);
279
+ console.log('');
280
+ }
281
+
282
+ if (modified.length > 0) {
283
+ console.log(`⚠ MODIFIED (${modified.length} files — REQUIRE CONFIRMATION):`);
284
+ for (const m of modified) {
285
+ console.log(` ? ${m.file} [${m.reason}]`);
286
+ }
287
+ console.log('');
288
+ }
289
+
290
+ if (user.length > 0) {
291
+ console.log(`🔒 USER DATA (${user.length} files — NEVER removed):`);
292
+ for (const u of user) console.log(` ○ ${u.file} [${u.reason}]`);
293
+ console.log('');
294
+ }
295
+
296
+ console.log(`Summary: ${safe.length} safe, ${modified.length} need confirm, ${user.length} preserved, ${skipped.length} skipped\n`);
297
+
298
+ if (!apply) {
299
+ console.log('DRY-RUN. Use --apply to execute the removal.\n');
300
+ return;
301
+ }
302
+
303
+ // 4. Apply — remove SAFE files automatically (rehash before unlink)
304
+ let safeRemoved = 0;
305
+ for (const s of safe) {
306
+ const diskPath = safePath(s.file);
307
+ if (!diskPath) { console.error(` ✗ Traversal rejected: ${s.file}`); continue; }
308
+ // Re-verify hash hasn't changed since classification
309
+ const currentHash = sha256File(diskPath);
310
+ if (currentHash !== s.storedHash) {
311
+ console.log(` ⊘ Skipped (modified since classification): ${s.file}`);
312
+ continue;
313
+ }
314
+ try {
315
+ unlinkSync(diskPath);
316
+ safeRemoved++;
317
+ } catch (e) {
318
+ console.error(` ✗ Failed to remove: ${s.file} — ${e.message}`);
319
+ }
320
+ }
321
+ console.log(`✓ Removed ${safeRemoved} safe files.`);
322
+
323
+ // 5. Handle MODIFIED files — prompt user
324
+ let modifiedRemoved = 0;
325
+ let modifiedKept = 0;
326
+
327
+ for (const m of modified) {
328
+ if (yes) {
329
+ // Non-interactive mode: skip all modified
330
+ console.log(` ⊘ Skipped (modified): ${m.file}`);
331
+ modifiedKept++;
332
+ continue;
333
+ }
334
+
335
+ console.log(`\n─── ${m.file} ───`);
336
+ console.log(` Reason: ${m.reason}`);
337
+ console.log(` [D]elete [K]eep (default)`);
338
+
339
+ const answer = await askUser(' Choose [d/k]: ');
340
+ if (answer === 'd' || answer === 'delete') {
341
+ const diskPath = safePath(m.file);
342
+ if (!diskPath) { console.error(` ✗ Traversal rejected: ${m.file}`); continue; }
343
+ try {
344
+ unlinkSync(diskPath);
345
+ console.log(` ✓ Deleted: ${m.file}`);
346
+ modifiedRemoved++;
347
+ } catch (e) {
348
+ console.error(` ✗ Failed: ${m.file} — ${e.message}`);
349
+ }
350
+ } else {
351
+ console.log(` ⊘ Kept: ${m.file}`);
352
+ modifiedKept++;
353
+ }
354
+ }
355
+
356
+ // 6. Cleanup empty directories
357
+ for (const dir of CLEANUP_DIRS) {
358
+ removeEmptyDirs(resolve(ROOT, dir));
359
+ }
360
+ // Remove root Harness/ if empty
361
+ removeEmptyDirs(resolve(ROOT, 'Harness'));
362
+ // Remove root .claude/ if empty (but this is rare)
363
+ removeEmptyDirs(resolve(ROOT, '.claude'));
364
+
365
+ // 7. Remove version file if everything is gone
366
+ if (existsSync(VERSION_FILE)) {
367
+ const harPath = resolve(ROOT, 'Harness');
368
+ if (!existsSync(harPath) || (existsSync(harPath) && readdirSync(harPath).filter(f => f !== '.harness-version').length === 0)) {
369
+ unlinkSync(VERSION_FILE);
370
+ console.log('✓ Removed .harness-version (Harness directory empty).');
371
+ }
372
+ }
373
+
374
+ // 7.5 Prune stale checksums for files that no longer exist on disk
375
+ if (existsSync(VERSION_FILE)) {
376
+ const versionData = JSON.parse(readFileSync(VERSION_FILE, 'utf-8'));
377
+ const checksums = versionData.checksums || {};
378
+ let pruned = 0;
379
+ for (const file of Object.keys(checksums)) {
380
+ const diskPath = safePath(file);
381
+ if (!diskPath || !existsSync(diskPath)) {
382
+ delete checksums[file];
383
+ pruned++;
384
+ }
385
+ }
386
+ if (pruned > 0) {
387
+ versionData.checksums = checksums;
388
+ writeFileSync(VERSION_FILE, JSON.stringify(versionData, null, 2) + '\n', 'utf-8');
389
+ console.log(`✓ Pruned ${pruned} stale checksum(s) from .harness-version.`);
390
+ }
391
+ }
392
+
393
+ // 8. Remove CLAUDE.md harness section if it only has harness binding
394
+ const claudePath = resolve(ROOT, 'CLAUDE.md');
395
+ if (existsSync(claudePath)) {
396
+ const content = readFileSync(claudePath, 'utf-8');
397
+ if (content.includes('Harness contract') || content.includes('Harness Binding')) {
398
+ const answer = yes ? 'k' : await askUser('\nCLAUDE.md contains Harness binding section. Remove it? [D]elete harness section / [K]eep as-is (default): ');
399
+ if (answer === 'd' || answer === 'delete') {
400
+ // Strip the harness section: everything from "## 1. Harness Binding" to the next "## "
401
+ const cleaned = content.replace(/## 1\. Harness Binding[\s\S]*?(?=## 2\.)/, '');
402
+ writeFileSync(claudePath, cleaned, 'utf-8');
403
+ console.log('✓ Stripped Harness binding section from CLAUDE.md.');
404
+ }
405
+ }
406
+ }
407
+
408
+ console.log(`\n🏁 Done. Safe: ${safeRemoved} removed. Modified: ${modifiedRemoved} removed, ${modifiedKept} kept.`);
409
+ console.log(' Run `git status` to review changes.\n');
410
+ }
411
+
412
+ main().catch(e => { console.error(e); process.exit(1); });