create-harness-vibe-coding 0.6.5 → 0.7.1

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 (37) hide show
  1. package/package.json +2 -1
  2. package/src/generator.js +95 -2
  3. package/templates/common/.claude/agents/architect-manager.md +45 -0
  4. package/templates/common/.claude/agents/explore-manager.md +41 -0
  5. package/templates/common/.claude/agents/implement-manager.md +49 -0
  6. package/templates/common/.claude/agents/review-manager.md +56 -0
  7. package/templates/common/.claude/commands/wf-max.md +28 -14
  8. package/templates/common/.claude/commands/wf-remove.md +23 -0
  9. package/templates/common/.claude/commands/wf-review.md +13 -20
  10. package/templates/common/.claude/commands/wf-update.md +6 -4
  11. package/templates/common/.claude/settings.json +33 -0
  12. package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +1 -1
  13. package/templates/common/.claude/skills/wf-max/SKILL.md +34 -8
  14. package/templates/common/.claude/skills/wf-remove/SKILL.md +51 -0
  15. package/templates/common/.claude/skills/wf-review/SKILL.md +72 -50
  16. package/templates/common/.claude/skills/wf-update/SKILL.md +74 -58
  17. package/templates/common/.codex/config.toml +2 -0
  18. package/templates/common/.codex/hooks.json +37 -0
  19. package/templates/common/.harness-version +130 -3
  20. package/templates/common/AGENTS.md +26 -3
  21. package/templates/common/CLAUDE.md +94 -77
  22. package/templates/common/MEMORY.md +75 -73
  23. package/templates/common/SETUP.md +1 -2
  24. package/templates/common/commands/wf-max.toml +18 -0
  25. package/templates/common/commands/wf-review.toml +15 -0
  26. package/templates/common/docs/README.md +2 -2
  27. package/templates/common/docs/harness/WF-MAX.md +99 -10
  28. package/templates/common/docs/harness/WF.md +5 -0
  29. package/templates/common/docs/harness/dispatch.md +4 -0
  30. package/templates/common/scripts/scan-clean.mjs +456 -0
  31. package/templates/common/scripts/validate-harness.mjs +9 -0
  32. package/templates/common/scripts/wf-mode-hook.mjs +318 -0
  33. package/templates/common/scripts/wf-remove.mjs +396 -0
  34. package/templates/common/scripts/wf-statusline.ps1 +38 -0
  35. package/templates/common/scripts/wf-statusline.sh +48 -0
  36. package/templates/common/scripts/wf-update-check.mjs +389 -0
  37. package/templates/optional/skills/browser-e2e/docs/workflows/browser-e2e.md +12 -0
@@ -0,0 +1,396 @@
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
+ const HARNESS_PREFIXES = [
36
+ '.claude/',
37
+ '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',
77
+ '.claude/skills/wf-auto',
78
+ '.claude/skills/wf-browser',
79
+ '.claude/skills/wf-learn',
80
+ '.claude/skills/wf-max',
81
+ '.claude/skills/wf-readme',
82
+ '.claude/skills/wf-review',
83
+ '.claude/skills/wf-update',
84
+ '.claude/skills/wf-remove',
85
+ '.claude/skills/subagent-orchestrator',
86
+ '.claude/commands',
87
+ '.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)
167
+ const extraPatterns = [
168
+ '.claude/skills/wf-auto/SKILL.md',
169
+ '.claude/commands/wf-auto.md',
170
+ '.claude/commands/wf-auto-spark.md',
171
+ '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); });
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env pwsh
2
+ # wf-statusline.ps1 — Windows statusline badge for WF-MAX / WF-REVIEW
3
+ # Reads Harness/.runtime/current-mode.json and outputs a colored badge.
4
+ #
5
+ # Usage in .claude/settings.json:
6
+ # "statusLine": { "type": "command", "command": "pwsh -File Harness/scripts/wf-statusline.ps1" }
7
+ #
8
+ # Security: refuses symlinks, validates JSON, caps read size.
9
+
10
+ param()
11
+
12
+ $MODE_FILE = Join-Path $PSScriptRoot '..' '.runtime' 'current-mode.json'
13
+
14
+ # Refuse symlinks / missing file
15
+ if (-not (Test-Path $MODE_FILE -PathType Leaf)) { exit 0 }
16
+ $item = Get-Item $MODE_FILE -Force
17
+ if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { exit 0 }
18
+
19
+ # Size cap
20
+ if ($item.Length -gt 4096) { exit 0 }
21
+
22
+ try {
23
+ $json = Get-Content $MODE_FILE -Raw -ErrorAction Stop | ConvertFrom-Json
24
+ } catch {
25
+ exit 0 # Silent-fail — never crash statusline
26
+ }
27
+
28
+ if (-not $json.active -or $json.role -ne 'ceo') { exit 0 }
29
+
30
+ switch ($json.mode) {
31
+ 'wf-max' {
32
+ $phase = if ($json.phase) { ":$($json.phase -replace 'W\d_','W')" } else { '' }
33
+ Write-Host -NoNewline "`e[38;5;75m[WF-MAX$phase]`e[0m"
34
+ }
35
+ 'wf-review' {
36
+ Write-Host -NoNewline "`e[38;5;178m[WF-REVIEW]`e[0m"
37
+ }
38
+ }
@@ -0,0 +1,48 @@
1
+ #!/bin/bash
2
+ # wf-statusline.sh — Unix statusline badge for WF-MAX / WF-REVIEW
3
+ # Reads Harness/.runtime/current-mode.json and outputs a colored badge.
4
+ #
5
+ # Usage in .claude/settings.json:
6
+ # "statusLine": { "type": "command", "command": "bash Harness/scripts/wf-statusline.sh" }
7
+ #
8
+ # Security: refuses symlinks, validates JSON, caps read size.
9
+
10
+ MODE_FILE="$(dirname "$0")/../.runtime/current-mode.json"
11
+
12
+ # Resolve relative path to absolute (from project root or script location)
13
+ if [ ! -f "$MODE_FILE" ]; then
14
+ # Try relative to CWD (project root)
15
+ MODE_FILE="Harness/.runtime/current-mode.json"
16
+ fi
17
+
18
+ [ -L "$MODE_FILE" ] && exit 0 # Refuse symlinks
19
+ [ ! -f "$MODE_FILE" ] && exit 0 # Missing file
20
+
21
+ # Read with python for safe JSON parse (jq may not be installed).
22
+ # Pass file path as argv to avoid shell injection via MODE_FILE content.
23
+ MODE=$(python3 -c "
24
+ import json, os, sys
25
+ try:
26
+ fpath = sys.argv[1]
27
+ if os.path.getsize(fpath) > 4096: sys.exit(1)
28
+ with open(fpath, 'r') as f:
29
+ d = json.load(f)
30
+ if d.get('active') and d.get('role') == 'ceo':
31
+ print(d.get('mode','') + '|' + d.get('phase',''))
32
+ except: pass
33
+ " "$MODE_FILE" 2>/dev/null)
34
+
35
+ [ -z "$MODE" ] && exit 0
36
+
37
+ MODE_NAME="${MODE%%|*}"
38
+ MODE_PHASE="${MODE##*|}"
39
+
40
+ case "$MODE_NAME" in
41
+ wf-max)
42
+ PHASE_SHORT=$(echo "$MODE_PHASE" | sed 's/W._/W/')
43
+ printf '\033[38;5;75m[WF-MAX:%s]\033[0m' "$PHASE_SHORT"
44
+ ;;
45
+ wf-review)
46
+ printf '\033[38;5;178m[WF-REVIEW]\033[0m'
47
+ ;;
48
+ esac