create-harness-vibe-coding 0.6.4 → 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.
- package/package.json +2 -1
- package/src/generator.js +95 -2
- package/templates/common/.claude/agents/architect-manager.md +45 -0
- package/templates/common/.claude/agents/explore-manager.md +41 -0
- package/templates/common/.claude/agents/implement-manager.md +49 -0
- package/templates/common/.claude/agents/review-manager.md +56 -0
- package/templates/common/.claude/commands/wf-max.md +28 -14
- package/templates/common/.claude/commands/wf-remove.md +23 -0
- package/templates/common/.claude/commands/wf-review.md +13 -20
- package/templates/common/.claude/commands/wf-update.md +6 -4
- package/templates/common/.claude/settings.json +33 -0
- package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +1 -1
- package/templates/common/.claude/skills/wf-max/SKILL.md +34 -8
- package/templates/common/.claude/skills/wf-remove/SKILL.md +51 -0
- package/templates/common/.claude/skills/wf-review/SKILL.md +72 -50
- package/templates/common/.claude/skills/wf-update/SKILL.md +74 -58
- package/templates/common/.harness-version +122 -3
- package/templates/common/CLAUDE.md +94 -77
- package/templates/common/MEMORY.md +75 -73
- package/templates/common/SETUP.md +1 -2
- package/templates/common/docs/README.md +2 -2
- package/templates/common/docs/harness/WF-MAX.md +99 -10
- package/templates/common/docs/harness/WF.md +5 -0
- package/templates/common/docs/harness/dispatch.md +4 -0
- package/templates/common/scripts/scan-clean.mjs +456 -0
- package/templates/common/scripts/validate-harness.mjs +9 -0
- package/templates/common/scripts/wf-mode-hook.mjs +318 -0
- package/templates/common/scripts/wf-remove.mjs +396 -0
- package/templates/common/scripts/wf-statusline.ps1 +38 -0
- package/templates/common/scripts/wf-statusline.sh +48 -0
- package/templates/common/scripts/wf-update-check.mjs +389 -0
- 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
|
|