create-harness-vibe-coding 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) 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/.harness-version +122 -3
  18. package/templates/common/CLAUDE.md +94 -77
  19. package/templates/common/MEMORY.md +75 -73
  20. package/templates/common/SETUP.md +1 -2
  21. package/templates/common/docs/README.md +2 -2
  22. package/templates/common/docs/harness/WF-MAX.md +99 -10
  23. package/templates/common/docs/harness/WF.md +5 -0
  24. package/templates/common/docs/harness/dispatch.md +4 -0
  25. package/templates/common/scripts/scan-clean.mjs +456 -0
  26. package/templates/common/scripts/validate-harness.mjs +9 -0
  27. package/templates/common/scripts/wf-mode-hook.mjs +318 -0
  28. package/templates/common/scripts/wf-remove.mjs +396 -0
  29. package/templates/common/scripts/wf-statusline.ps1 +38 -0
  30. package/templates/common/scripts/wf-statusline.sh +48 -0
  31. package/templates/common/scripts/wf-update-check.mjs +389 -0
  32. package/templates/optional/skills/browser-e2e/docs/workflows/browser-e2e.md +12 -0
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * wf-update-check.mjs — Fast harness update comparison.
4
+ * Fetches remote checksums, compares locally, classifies all files instantly.
5
+ * Only CONFLICT files need AI/user decision.
6
+ *
7
+ * Usage:
8
+ * node Harness/scripts/wf-update-check.mjs # full plan
9
+ * node Harness/scripts/wf-update-check.mjs --apply # apply SAFE+MERGE+NEW, report CONFLICT
10
+ * node Harness/scripts/wf-update-check.mjs --json # JSON output for AI consumption
11
+ */
12
+
13
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, lstatSync } from 'fs';
14
+ import { createHash } from 'crypto';
15
+ import { resolve, dirname, sep } from 'path';
16
+ import { fileURLToPath } from 'url';
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const ROOT = process.env.WF_ROOT ? resolve(process.env.WF_ROOT) : resolve(__dirname, '..', '..');
20
+ const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
21
+ const SOURCE_BASE = 'https://raw.githubusercontent.com/zingspark/create-harness-vibe-coding/main/templates/common/';
22
+
23
+ // ── Tier classification ──────────────────────────────────────────
24
+
25
+ /** Files we NEVER overwrite or delete. */
26
+ const PRESERVE_PATTERNS = [
27
+ /^Harness\/PROGRESS\.md$/,
28
+ /^Harness\/tasks\//,
29
+ /^Harness\/memory\//,
30
+ /^Harness\/research\/PRD\.md$/,
31
+ /^Harness\/research\/research-results\.md$/,
32
+ /^Harness\/architecture\.md$/,
33
+ /^README\.md$/,
34
+ /^\.gitignore$/,
35
+ /^package\.json$/,
36
+ /^package-lock\.json$/,
37
+ ];
38
+
39
+ /** Files that are safe to overwrite if checksums match, otherwise need merge. */
40
+ const MERGE_PATTERNS = [
41
+ /^CLAUDE\.md$/,
42
+ /^AGENTS\.md$/,
43
+ /^MEMORY\.md$/,
44
+ /^Harness\/MEMORY\.md$/,
45
+ /^Harness\/README\.md$/,
46
+ ];
47
+
48
+ // ── Helpers ────────────────────────────────────────────────────────
49
+
50
+ /** Reject paths that escape ROOT (traversal, absolute, .., etc.). */
51
+ function safePath(file) {
52
+ let normalized = file.replace(/\\/g, '/').replace(/^\/+/, '');
53
+ if (/\/\//.test(normalized)) return null;
54
+ if (normalized.split('/').some(p => p === '..')) return null;
55
+ if (file.startsWith('/') || file.startsWith('\\')) return null;
56
+ if (normalized === '.' || normalized === '') return null;
57
+ const resolved = resolve(ROOT, normalized);
58
+ if (!resolved.startsWith(ROOT + sep) && resolved !== ROOT) return null;
59
+ return resolved;
60
+ }
61
+
62
+ /** Canonical normalization for classification matching. */
63
+ function canonicalPath(file) {
64
+ return file.replace(/\\/g, '/').replace(/\/+/g, '/');
65
+ }
66
+
67
+ /** Detect template placeholders in remote content. */
68
+ function isTemplate(raw) {
69
+ return /\{\{[a-zA-Z]+\}\}/.test(raw);
70
+ }
71
+
72
+ function sha256(content) {
73
+ return 'sha256-' + createHash('sha256').update(content).digest('hex');
74
+ }
75
+
76
+ function sha256File(path) {
77
+ if (!existsSync(path)) return null;
78
+ let content = readFileSync(path, 'utf-8');
79
+ // Normalize CRLF → LF
80
+ content = content.replace(/\r\n/g, '\n');
81
+ return sha256(content);
82
+ }
83
+
84
+ function classify(file, localHash, storedHash) {
85
+ // PRESERVE
86
+ for (const p of PRESERVE_PATTERNS) {
87
+ if (p.test(file)) return 'PRESERVE';
88
+ }
89
+ // MERGE — dual-purpose, check if user modified
90
+ for (const p of MERGE_PATTERNS) {
91
+ if (p.test(file)) {
92
+ if (localHash === storedHash) return 'SAFE'; // unmodified, safe
93
+ return 'CONFLICT'; // user modified, needs decision
94
+ }
95
+ }
96
+ // Everything else is SAFE runtime file
97
+ if (localHash === storedHash || localHash === null) return 'SAFE';
98
+ return 'CONFLICT'; // modified runtime file — unexpected
99
+ }
100
+
101
+ async function fetchRemote(url, timeoutMs = 30000) {
102
+ const controller = new AbortController();
103
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
104
+ try {
105
+ const res = await fetch(url, { signal: controller.signal });
106
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
107
+ return res.text();
108
+ } finally {
109
+ clearTimeout(timer);
110
+ }
111
+ }
112
+
113
+ // ── Main ───────────────────────────────────────────────────────────
114
+
115
+ async function main() {
116
+ const args = process.argv.slice(2);
117
+ const apply = args.includes('--apply');
118
+ const jsonOut = args.includes('--json');
119
+ const ignoreVersion = args.includes('--ignore-version') || args.includes('--force-check');
120
+
121
+ // 1. Read local state
122
+ if (!existsSync(VERSION_FILE)) {
123
+ if (jsonOut) {
124
+ console.log(JSON.stringify({ status: 'error', message: 'Local Harness/.harness-version not found.' }));
125
+ } else {
126
+ console.error('ERROR: Harness/.harness-version not found. Is Harness installed?');
127
+ }
128
+ return;
129
+ }
130
+
131
+ let localVersion;
132
+ try {
133
+ localVersion = JSON.parse(readFileSync(VERSION_FILE, 'utf-8'));
134
+ } catch (e) {
135
+ if (jsonOut) {
136
+ console.log(JSON.stringify({ status: 'error', message: 'Failed to parse Harness/.harness-version: ' + e.message }));
137
+ } else {
138
+ console.error('ERROR: Failed to parse Harness/.harness-version:', e.message);
139
+ console.error(' The file may be corrupted. If this is an old project, try reinstalling the harness.');
140
+ }
141
+ return;
142
+ }
143
+
144
+ if (!localVersion || typeof localVersion !== 'object') {
145
+ if (jsonOut) {
146
+ console.log(JSON.stringify({ status: 'error', message: 'Invalid Harness/.harness-version structure.' }));
147
+ } else {
148
+ console.error('ERROR: Harness/.harness-version has unexpected structure.');
149
+ }
150
+ return;
151
+ }
152
+
153
+ const localChecksums = localVersion.checksums || {};
154
+
155
+ // 2. Fetch remote version file
156
+ let remoteVersion;
157
+ try {
158
+ const raw = await fetchRemote(SOURCE_BASE + '.harness-version');
159
+ if (isTemplate(raw)) {
160
+ if (jsonOut) {
161
+ console.log(JSON.stringify({ status: 'template-remote', message: 'Remote .harness-version has not been generated yet.' }));
162
+ } else {
163
+ console.log('⚠ Remote .harness-version is a template (contains {{placeholders}}).');
164
+ console.log(' The generate step has not been run on the remote repo. No update possible.');
165
+ console.log(' This is expected during development — the update mechanism works once the remote is live.');
166
+ }
167
+ return;
168
+ }
169
+ remoteVersion = JSON.parse(raw);
170
+ } catch (e) {
171
+ if (jsonOut) {
172
+ console.log(JSON.stringify({ status: 'offline', message: 'Cannot reach GitHub.' }));
173
+ } else {
174
+ console.error('ERROR: Cannot reach GitHub or invalid JSON. Offline?');
175
+ console.error(e.message);
176
+ }
177
+ return;
178
+ }
179
+
180
+ // Compare versions — warn if remote is older (downgrade prevention)
181
+ function parseSemver(v) {
182
+ if (!v || typeof v !== 'string') return [0, 0, 0];
183
+ return v.replace(/^[^0-9]*/, '').split('-')[0].split('.').map(Number);
184
+ }
185
+ function cmpSemver(a, b) {
186
+ const va = parseSemver(a), vb = parseSemver(b);
187
+ for (let i = 0; i < 3; i++) { if ((va[i]||0) > (vb[i]||0)) return 1; if ((va[i]||0) < (vb[i]||0)) return -1; }
188
+ return 0;
189
+ }
190
+
191
+ const localGen = localVersion.generator || '0.0.0';
192
+ const remoteGen = remoteVersion.generator || '0.0.0';
193
+
194
+ if (!ignoreVersion && cmpSemver(remoteGen, localGen) <= 0) {
195
+ if (jsonOut) {
196
+ console.log(JSON.stringify({ status: 'up-to-date', version: localGen, remote: remoteGen }));
197
+ } else if (cmpSemver(remoteGen, localGen) < 0) {
198
+ console.log(`⚠ Remote (v${remoteGen}) is OLDER than local (v${localGen}). Downgrade refused.`);
199
+ } else {
200
+ console.log(`✅ Already up to date (v${localGen})`);
201
+ }
202
+ return;
203
+ }
204
+
205
+ if (ignoreVersion) {
206
+ if (!jsonOut) console.log('🔧 Version check bypassed (--ignore-version). Comparing files anyway.');
207
+ }
208
+
209
+ const remoteChecksums = remoteVersion.checksums || {};
210
+ const remoteSources = remoteVersion.sources || {};
211
+
212
+ /** Resolve the remote template-relative path for a dest-keyed file. Falls back to the key itself for back-compat. */
213
+ function remotePath(file) {
214
+ return remoteSources[file] || file;
215
+ }
216
+
217
+ const allFiles = new Set([...Object.keys(localChecksums), ...Object.keys(remoteChecksums)]);
218
+
219
+ const plan = { updated: [], created: [], conflict: [], skipped: [] };
220
+
221
+ for (const file of [...allFiles].sort()) {
222
+ const canonical = canonicalPath(file);
223
+
224
+ // Reject paths that escape ROOT before any file access
225
+ const diskPath = safePath(file);
226
+ if (!diskPath) {
227
+ plan.skipped.push({ file, reason: 'path traversal rejected' });
228
+ continue;
229
+ }
230
+
231
+ const localHash = sha256File(diskPath);
232
+ const storedHash = localChecksums[file];
233
+ const remoteHash = remoteChecksums[file];
234
+
235
+ if (!remoteHash) {
236
+ plan.skipped.push({ file, reason: 'not in remote' });
237
+ continue;
238
+ }
239
+
240
+ if (!storedHash) {
241
+ // New file from remote — if local file exists, it's a CONFLICT
242
+ if (localHash) {
243
+ plan.conflict.push({ file, localHash, storedHash: 'none', remoteHash, reason: 'new remote file conflicts with existing local file' });
244
+ continue;
245
+ }
246
+ // New file — still respect PRESERVE classification
247
+ const tier = classify(canonical, null, null);
248
+ if (tier === 'PRESERVE') {
249
+ plan.skipped.push({ file, reason: 'PRESERVE — new file would overwrite user data' });
250
+ } else {
251
+ plan.created.push({ file, remoteHash });
252
+ }
253
+ continue;
254
+ }
255
+
256
+ const tier = classify(canonical, localHash, storedHash);
257
+
258
+ if (tier === 'PRESERVE') {
259
+ plan.skipped.push({ file, reason: 'PRESERVE — user data' });
260
+ } else if (tier === 'SAFE') {
261
+ plan.updated.push({ file, remoteHash });
262
+ } else if (tier === 'CONFLICT') {
263
+ plan.conflict.push({
264
+ file,
265
+ localHash,
266
+ storedHash,
267
+ remoteHash,
268
+ reason: MERGE_PATTERNS.some(p => p.test(canonical))
269
+ ? 'user modified MERGE file'
270
+ : 'user modified runtime file',
271
+ });
272
+ }
273
+ }
274
+
275
+ // 3. Output
276
+ if (jsonOut) {
277
+ console.log(JSON.stringify({
278
+ status: 'update-available',
279
+ from: localGen,
280
+ to: remoteGen,
281
+ updated: plan.updated.length,
282
+ created: plan.created.length,
283
+ conflict: plan.conflict.length,
284
+ skipped: plan.skipped.length,
285
+ plan,
286
+ }, null, 2));
287
+ return;
288
+ }
289
+
290
+ console.log(`\n🔄 Update: v${localGen} → v${remoteGen}`);
291
+ console.log(` ${plan.updated.length} safe update, ${plan.created.length} new, ${plan.conflict.length} conflict, ${plan.skipped.length} skipped\n`);
292
+
293
+ // Show conflicts (these need AI/user decision)
294
+ if (plan.conflict.length > 0) {
295
+ console.log('⚠ CONFLICTS (need your decision):');
296
+ for (const c of plan.conflict) {
297
+ console.log(` 📄 ${c.file} [${c.reason}]`);
298
+ }
299
+ console.log('');
300
+ }
301
+
302
+ // Show what will be auto-updated
303
+ if (plan.updated.length + plan.created.length > 0) {
304
+ console.log('✅ AUTO (safe to apply):');
305
+ for (const u of plan.updated) console.log(` ↑ ${u.file}`);
306
+ for (const c of plan.created) console.log(` + ${c.file}`);
307
+ console.log('');
308
+ }
309
+
310
+ // 4. Apply if requested
311
+ if (apply) {
312
+ // Refuse to apply when conflicts exist — must resolve first
313
+ if (plan.conflict.length > 0) {
314
+ console.log(`❌ Cannot apply: ${plan.conflict.length} conflicts must be resolved first.`);
315
+ console.log(' Resolve conflicts manually, then re-run --apply.');
316
+ return plan;
317
+ }
318
+
319
+ const lexists = existsSync;
320
+ let applied = 0;
321
+ let failed = 0;
322
+
323
+ for (const u of plan.updated) {
324
+ try {
325
+ const dest = safePath(u.file);
326
+ if (!dest) { console.error(` ✗ Traversal rejected: ${u.file}`); failed++; continue; }
327
+ // Symlink rejection — don't follow symlinks
328
+ if (lexists(dest)) {
329
+ try { if (lstatSync(dest).isSymbolicLink()) { console.error(` ✗ Symlink rejected: ${u.file}`); failed++; continue; } } catch (_) {}
330
+ }
331
+ const content = await fetchRemote(SOURCE_BASE + remotePath(u.file));
332
+ const normalized = content.replace(/\r\n/g, '\n');
333
+ const fetchedHash = sha256(normalized);
334
+ if (fetchedHash !== u.remoteHash) {
335
+ console.error(` ✗ Hash mismatch: ${u.file}`);
336
+ failed++; continue;
337
+ }
338
+ mkdirSync(dirname(dest), { recursive: true });
339
+ writeFileSync(dest, normalized, 'utf-8');
340
+ applied++;
341
+ } catch (e) {
342
+ console.error(` ✗ Failed: ${u.file} — ${e.message}`);
343
+ failed++;
344
+ }
345
+ }
346
+
347
+ for (const c of plan.created) {
348
+ try {
349
+ const dest = safePath(c.file);
350
+ if (!dest) { console.error(` ✗ Traversal rejected: ${c.file}`); failed++; continue; }
351
+ // TOCTOU: recheck file didn't appear since planning
352
+ if (lexists(dest)) {
353
+ try { if (lstatSync(dest).isSymbolicLink()) { console.error(` ✗ Symlink rejected: ${c.file}`); failed++; continue; } } catch (_) {}
354
+ console.error(` ✗ File created since plan: ${c.file} — treating as CONFLICT`);
355
+ failed++; continue;
356
+ }
357
+ const content = await fetchRemote(SOURCE_BASE + remotePath(c.file));
358
+ const normalized = content.replace(/\r\n/g, '\n');
359
+ const fetchedHash = sha256(normalized);
360
+ if (fetchedHash !== c.remoteHash) {
361
+ console.error(` ✗ Hash mismatch: ${c.file}`);
362
+ failed++; continue;
363
+ }
364
+ mkdirSync(dirname(dest), { recursive: true });
365
+ writeFileSync(dest, normalized, 'utf-8');
366
+ applied++;
367
+ } catch (e) {
368
+ console.error(` ✗ Failed: ${c.file} — ${e.message}`);
369
+ failed++;
370
+ }
371
+ }
372
+
373
+ // Only update version on complete success
374
+ if (failed === 0) {
375
+ localVersion.generator = remoteGen;
376
+ localVersion.generated = new Date().toISOString();
377
+ for (const u of plan.updated) localVersion.checksums[u.file] = u.remoteHash;
378
+ for (const c of plan.created) localVersion.checksums[c.file] = c.remoteHash;
379
+ writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
380
+ console.log(`✅ Applied ${applied} files. Version updated to ${remoteVersion.generator}.`);
381
+ } else {
382
+ console.log(`❌ ${failed} failures. NO files were version-tracked. Fix and re-run.`);
383
+ }
384
+ }
385
+
386
+ return plan;
387
+ }
388
+
389
+ main().catch(e => { console.error(e); process.exit(1); });
@@ -17,6 +17,18 @@ Browser evidence in this project follows the contract:
17
17
  2. **CLI mode is preferred for deterministic steps** — use `browser-use open/state/click/screenshot` for predictable flows
18
18
  3. **Agent mode is for dynamic exploration** — use Browser Use Agent API when the page structure is unknown or changing
19
19
  4. **Evidence goes to the task directory** — `Harness/tasks/<task-id>/evidence/*.png`
20
+ 5. **Stable UI selector contract** — Stable accessible labels/roles and stable test hooks such as `data-testid` are required for critical UI controls and states: inputs, buttons, filters, rows, empty/error/loading states.
21
+
22
+ ## Chrome DevTools / CDP / MCP Checklist
23
+
24
+ - [ ] record the URL and port
25
+ - [ ] Verify available CDP, MCP, browser automation, or manual tooling
26
+ - [ ] Check not just HTTP 200
27
+ - [ ] Verify no runtime exceptions, console errors, and failed network requests
28
+ - [ ] Confirm stable accessible labels/roles or `data-testid` on interactive elements
29
+ - [ ] Test critical flow end-to-end
30
+ - [ ] Capture screenshot, trace, video, or result artifact paths
31
+ - [ ] Clean up any dev server or browser processes
20
32
 
21
33
  ## Quick Install
22
34