create-harness-vibe-coding 0.8.9 → 0.8.12

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 (29) hide show
  1. package/README-CN.md +5 -5
  2. package/README.md +6 -6
  3. package/docs/images/harness-architecture.drawio +2 -2
  4. package/package.json +3 -2
  5. package/templates/common/.claude/commands/wf-help.md +1 -1
  6. package/templates/common/.claude/settings.json +2 -2
  7. package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +2 -2
  8. package/templates/common/.claude/skills/wf-max/SKILL.md +3 -3
  9. package/templates/common/.claude/skills/wf-review/SKILL.md +63 -12
  10. package/templates/common/.codex/hooks.json +5 -5
  11. package/templates/common/.harness-version +28 -28
  12. package/templates/common/.opencode/commands/wf-help.md +1 -1
  13. package/templates/common/.opencode/commands/wf-review.md +2 -2
  14. package/templates/common/.opencode/plugins/harness-wf-status.mjs +12 -21
  15. package/templates/common/Harness/MEMORY.md +2 -2
  16. package/templates/common/Harness/README.md +3 -3
  17. package/templates/common/Harness/SETUP.md +1 -1
  18. package/templates/common/Harness/WF-AUTO-ANGLES.md +1 -1
  19. package/templates/common/Harness/WF-AUTO-SPARK.md +21 -4
  20. package/templates/common/Harness/WF-AUTO.md +9 -8
  21. package/templates/common/Harness/WF-MAX.md +37 -3
  22. package/templates/common/Harness/dispatch.md +1 -1
  23. package/templates/common/Harness/scripts/scan-clean.mjs +2 -1
  24. package/templates/common/Harness/scripts/validate-harness.mjs +36 -0
  25. package/templates/common/Harness/scripts/wf-auto-update-prompt.mjs +2 -2
  26. package/templates/common/Harness/scripts/wf-remove.mjs +1 -0
  27. package/templates/common/Harness/scripts/wf-update-check.mjs +123 -45
  28. package/templates/common/Harness/subagents.md +8 -1
  29. package/templates/common/README.md +1 -1
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * wf-update-check.mjs Fast harness update comparison.
3
+ * wf-update-check.mjs - Fast harness update comparison.
4
4
  * Fetches remote checksums, compares locally, classifies all files instantly.
5
5
  * Only CONFLICT files need AI/user decision.
6
6
  *
@@ -23,9 +23,13 @@ import { fileURLToPath } from 'url';
23
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
24
24
  const ROOT = process.env.WF_ROOT ? resolve(process.env.WF_ROOT) : resolve(__dirname, '..', '..');
25
25
  const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
26
- const DEFAULT_SOURCE_BASE = 'https://raw.githubusercontent.com/zingspark/create-harness-vibe-coding/main/templates/common/';
26
+ const GITHUB_REPO = 'LiWeny16/create-harness-vibe-coding';
27
+ const GITHUB_LATEST_STABLE = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
28
+ const RAW_GITHUB = `https://raw.githubusercontent.com/${GITHUB_REPO}`;
29
+ const TEMPLATE_SUBPATH = 'templates/common/';
30
+ const DEFAULT_SOURCE_BASE = `${RAW_GITHUB}/main/${TEMPLATE_SUBPATH}`;
27
31
 
28
- // ── Tier classification ──────────────────────────────────────────
32
+ // Tier classification
29
33
 
30
34
  /** Files we NEVER overwrite or delete. */
31
35
  const PRESERVE_PATTERNS = [
@@ -56,10 +60,15 @@ const OPTIONAL_REGISTRATION_FILES = new Set([
56
60
  'Harness/README.md',
57
61
  ]);
58
62
 
59
- // ── Helpers ────────────────────────────────────────────────────────
63
+ const BOOTSTRAP_ONLY_FILES = new Set([
64
+ 'Harness/SETUP.md',
65
+ ]);
66
+
67
+ // Helpers
60
68
 
61
69
  /** Reject paths that escape ROOT (traversal, absolute, .., etc.). */
62
70
  function safePath(file) {
71
+ if (/^[A-Za-z]:/.test(file)) return null;
63
72
  let normalized = file.replace(/\\/g, '/').replace(/^\/+/, '');
64
73
  if (/\/\//.test(normalized)) return null;
65
74
  if (normalized.split('/').some(p => p === '..')) return null;
@@ -136,7 +145,7 @@ function sha256(content) {
136
145
  function sha256File(path) {
137
146
  if (!existsSync(path)) return null;
138
147
  let content = readFileSync(path, 'utf-8');
139
- // Normalize CRLF LF
148
+ // Normalize CRLF to LF.
140
149
  content = content.replace(/\r\n/g, '\n');
141
150
  return sha256(content);
142
151
  }
@@ -146,16 +155,18 @@ function classify(file, localHash, storedHash) {
146
155
  for (const p of PRESERVE_PATTERNS) {
147
156
  if (p.test(file)) return 'PRESERVE';
148
157
  }
149
- // MERGE dual-purpose, check if user modified
158
+ // MERGE: dual-purpose, check if user modified.
150
159
  for (const p of MERGE_PATTERNS) {
151
160
  if (p.test(file)) {
152
161
  if (localHash === storedHash) return 'SAFE'; // unmodified, safe
153
162
  return 'CONFLICT'; // user modified, needs decision
154
163
  }
155
164
  }
156
- // Everything else is SAFE runtime file
157
- if (localHash === storedHash || localHash === null) return 'SAFE';
158
- return 'CONFLICT'; // modified runtime file unexpected
165
+ // Everything else is SAFE runtime file: always overwrite.
166
+ // Harness system files (scripts, skills, agents, commands, WF docs) are
167
+ // not user data; the template is authoritative. Only PRESERVE and MERGE
168
+ // files should ever require conflict resolution.
169
+ return 'SAFE';
159
170
  }
160
171
 
161
172
  async function fetchRemote(url, timeoutMs = 30000) {
@@ -174,7 +185,49 @@ async function fetchRemote(url, timeoutMs = 30000) {
174
185
  }
175
186
  }
176
187
 
177
- // ── Main ───────────────────────────────────────────────────────────
188
+ // Main
189
+
190
+ function isPrerelease(v) {
191
+ if (!v || typeof v !== 'string') return false;
192
+ return /-[0-9A-Za-z.-]+/.test(v.replace(/^[^0-9]*/, ''));
193
+ }
194
+
195
+ async function resolveStableSourceBase() {
196
+ try {
197
+ const controller = new AbortController();
198
+ const timer = setTimeout(() => controller.abort(), 15000);
199
+ let res;
200
+ try {
201
+ res = await fetch(GITHUB_LATEST_STABLE, {
202
+ signal: controller.signal,
203
+ headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'harness-wf-update-check' },
204
+ });
205
+ } finally {
206
+ clearTimeout(timer);
207
+ }
208
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
209
+ const data = JSON.parse(await res.text());
210
+ const tag = data && data.tag_name;
211
+ if (!tag || data.prerelease) return null;
212
+ const source = `${RAW_GITHUB}/${tag}/${TEMPLATE_SUBPATH}`;
213
+ const versionController = new AbortController();
214
+ const versionTimer = setTimeout(() => versionController.abort(), 15000);
215
+ try {
216
+ const versionRes = await fetch(source + '.harness-version', {
217
+ signal: versionController.signal,
218
+ headers: { 'User-Agent': 'harness-wf-update-check' },
219
+ });
220
+ if (!versionRes.ok) return null;
221
+ const raw = await versionRes.text();
222
+ if (isTemplate(raw)) return null;
223
+ } finally {
224
+ clearTimeout(versionTimer);
225
+ }
226
+ return source;
227
+ } catch {
228
+ return null;
229
+ }
230
+ }
178
231
 
179
232
  async function main() {
180
233
  const args = process.argv.slice(2);
@@ -186,7 +239,16 @@ async function main() {
186
239
  const acceptMerged = readRepeatedFlagValues(args, '--accept-merged');
187
240
  const acceptTemplate = readRepeatedFlagValues(args, '--accept-template');
188
241
  const ignoreVersion = args.includes('--ignore-version') || args.includes('--force-check');
189
- const sourceBase = normalizeSourceBase(readFlagValue(args, '--source-base') || process.env.WF_SOURCE_BASE || DEFAULT_SOURCE_BASE);
242
+ const explicitSource = readFlagValue(args, '--source-base') || process.env.WF_SOURCE_BASE;
243
+ let sourceBase = normalizeSourceBase(explicitSource || DEFAULT_SOURCE_BASE);
244
+ let stableTagResolved = false;
245
+ if (!explicitSource) {
246
+ const resolved = await resolveStableSourceBase();
247
+ if (resolved) {
248
+ sourceBase = normalizeSourceBase(resolved);
249
+ stableTagResolved = true;
250
+ }
251
+ }
190
252
 
191
253
  // 1. Read local state
192
254
  if (!existsSync(VERSION_FILE)) {
@@ -248,9 +310,9 @@ async function main() {
248
310
  if (jsonOut) {
249
311
  console.log(JSON.stringify({ status: 'template-remote', message: 'Remote .harness-version has not been generated yet.' }));
250
312
  } else {
251
- console.log(' Remote .harness-version is a template (contains {{placeholders}}).');
313
+ console.log('WARN: Remote .harness-version is a template (contains {{placeholders}}).');
252
314
  console.log(' The generate step has not been run on the remote repo. No update possible.');
253
- console.log(' This is expected during development the update mechanism works once the remote is live.');
315
+ console.log(' This is expected during development; the update mechanism works once the remote is live.');
254
316
  }
255
317
  process.exitCode = 1;
256
318
  return;
@@ -267,10 +329,10 @@ async function main() {
267
329
  return;
268
330
  }
269
331
 
270
- // Compare versions warn if remote is older (downgrade prevention)
332
+ // Compare versions and prevent downgrades from explicit custom sources.
271
333
  function parseSemver(v) {
272
334
  if (!v || typeof v !== 'string') return [0, 0, 0];
273
- return v.replace(/^[^0-9]*/, '').split('-')[0].split('.').map(Number);
335
+ return v.replace(/^[^0-9]*/, '').split('.').slice(0, 3).map(n => Number(n) || 0);
274
336
  }
275
337
  function cmpSemver(a, b) {
276
338
  const va = parseSemver(a), vb = parseSemver(b);
@@ -280,27 +342,38 @@ async function main() {
280
342
 
281
343
  const localGen = localVersion.generator || '0.0.0';
282
344
  const remoteGen = remoteVersion.generator || '0.0.0';
345
+
346
+ if (!ignoreVersion && isPrerelease(remoteGen)) {
347
+ if (jsonOut) {
348
+ console.log(JSON.stringify({ status: 'up-to-date', version: localGen, remote: remoteGen, sourceBase }));
349
+ } else {
350
+ console.log(`Already up to date (v${localGen}). Remote ${remoteGen} is a prerelease and is ignored.`);
351
+ }
352
+ return;
353
+ }
354
+
283
355
  const versionCmp = cmpSemver(remoteGen, localGen);
284
356
 
285
357
  if (!ignoreVersion && versionCmp <= 0) {
358
+ const reportDowngrade = !stableTagResolved;
286
359
  if (jsonOut) {
287
360
  console.log(JSON.stringify({
288
- status: versionCmp < 0 ? 'downgrade-refused' : 'up-to-date',
361
+ status: (versionCmp < 0 && reportDowngrade) ? 'downgrade-refused' : 'up-to-date',
289
362
  version: localGen,
290
363
  remote: remoteGen,
291
364
  sourceBase,
292
365
  }));
293
- } else if (versionCmp < 0) {
294
- console.log(`⚠ Remote (v${remoteGen}) is OLDER than local (v${localGen}). Downgrade refused.`);
366
+ } else if (versionCmp < 0 && reportDowngrade) {
367
+ console.log(`WARN: Remote (v${remoteGen}) is OLDER than local (v${localGen}). Downgrade refused.`);
295
368
  } else {
296
- console.log(`✅ Already up to date (v${localGen})`);
369
+ console.log(`Already up to date (v${localGen})`);
297
370
  }
298
- if (versionCmp < 0) process.exitCode = 1;
371
+ if (versionCmp < 0 && reportDowngrade) process.exitCode = 1;
299
372
  return;
300
373
  }
301
374
 
302
375
  if (ignoreVersion) {
303
- if (!jsonOut) console.log('🔧 Version check bypassed (--ignore-version). Comparing files anyway.');
376
+ if (!jsonOut) console.log('Version check bypassed (--ignore-version). Comparing files anyway.');
304
377
  }
305
378
 
306
379
  const remoteChecksums = remoteVersion.checksums || {};
@@ -469,8 +542,13 @@ async function main() {
469
542
  continue;
470
543
  }
471
544
 
545
+ if (BOOTSTRAP_ONLY_FILES.has(canonical) && localHash === null) {
546
+ plan.skipped.push({ file, reason: 'bootstrap-only file already removed locally' });
547
+ continue;
548
+ }
549
+
472
550
  if (!storedHash) {
473
- // New file from remote if local file exists, it's a CONFLICT
551
+ // New file from remote: if local file exists, it is a CONFLICT.
474
552
  if (localHash) {
475
553
  if (localHash === remoteHash) {
476
554
  plan.adopted.push({ file, localHash, remoteHash, reason: 'new remote file already matches local file' });
@@ -483,10 +561,10 @@ async function main() {
483
561
  plan.conflict.push({ file, localHash, storedHash: 'none', remoteHash, reason: 'new remote file conflicts with existing local file' });
484
562
  continue;
485
563
  }
486
- // New file still respect PRESERVE classification
564
+ // New file: still respect PRESERVE classification.
487
565
  const tier = classify(canonical, null, null);
488
566
  if (tier === 'PRESERVE') {
489
- plan.skipped.push({ file, reason: 'PRESERVE new file would overwrite user data' });
567
+ plan.skipped.push({ file, reason: 'PRESERVE: new file would overwrite user data' });
490
568
  } else {
491
569
  plan.created.push({ file, remoteHash });
492
570
  }
@@ -517,7 +595,7 @@ async function main() {
517
595
  const tier = classify(canonical, localHash, storedHash);
518
596
 
519
597
  if (tier === 'PRESERVE') {
520
- plan.skipped.push({ file, reason: 'PRESERVE user data' });
598
+ plan.skipped.push({ file, reason: 'PRESERVE: user data' });
521
599
  } else if (tier === 'SAFE') {
522
600
  if (localHash === remoteHash) {
523
601
  plan.skipped.push({ file, reason: 'already current' });
@@ -564,14 +642,14 @@ async function main() {
564
642
  return;
565
643
  }
566
644
 
567
- console.log(`\n🔄 Update: v${localGen} v${remoteGen}`);
645
+ console.log(`\nUpdate: v${localGen} -> v${remoteGen}`);
568
646
  console.log(` ${plan.updated.length} safe update, ${plan.created.length} new, ${plan.conflict.length} conflict, ${plan.skipped.length} skipped\n`);
569
647
 
570
648
  // Show conflicts (these need AI/user decision)
571
649
  if (plan.conflict.length > 0) {
572
- console.log('CONFLICTS (need your decision):');
650
+ console.log('CONFLICTS (need your decision):');
573
651
  for (const c of plan.conflict) {
574
- console.log(` 📄 ${c.file} [${c.reason}]`);
652
+ console.log(` ! ${c.file} [${c.reason}]`);
575
653
  }
576
654
  if (plan.updated.length + plan.created.length > 0) {
577
655
  console.log(' Tip: run --apply-safe to apply SAFE/NEW files first, then merge conflicts.');
@@ -581,17 +659,17 @@ async function main() {
581
659
 
582
660
  // Show what will be auto-updated
583
661
  if (plan.updated.length + plan.created.length > 0) {
584
- console.log('AUTO (safe to apply):');
585
- for (const u of plan.updated) console.log(` ${u.file}`);
662
+ console.log('AUTO (safe to apply):');
663
+ for (const u of plan.updated) console.log(` ^ ${u.file}`);
586
664
  for (const c of plan.created) console.log(` + ${c.file}`);
587
665
  console.log('');
588
666
  }
589
667
 
590
668
  // 4. Apply if requested
591
669
  if (apply || applySafe) {
592
- // Refuse to apply when conflicts exist must resolve first
670
+ // Refuse to apply when conflicts exist; must resolve first.
593
671
  if (apply && !applySafe && plan.conflict.length > 0) {
594
- console.log(`❌ Cannot apply: ${plan.conflict.length} conflicts must be resolved first.`);
672
+ console.log(`Cannot apply: ${plan.conflict.length} conflicts must be resolved first.`);
595
673
  console.log(' Run --apply-safe to apply SAFE/NEW files first, or resolve conflicts manually then re-run --apply.');
596
674
  process.exitCode = 1;
597
675
  return plan;
@@ -607,23 +685,23 @@ async function main() {
607
685
  for (const u of plan.updated) {
608
686
  try {
609
687
  const dest = safePath(u.file);
610
- if (!dest) { console.error(` Traversal rejected: ${u.file}`); failed++; continue; }
611
- // Symlink rejection don't follow symlinks
688
+ if (!dest) { console.error(` x Traversal rejected: ${u.file}`); failed++; continue; }
689
+ // Symlink rejection: do not follow symlinks.
612
690
  if (lexists(dest)) {
613
- try { if (lstatSync(dest).isSymbolicLink()) { console.error(` Symlink rejected: ${u.file}`); failed++; continue; } } catch (_) {}
691
+ try { if (lstatSync(dest).isSymbolicLink()) { console.error(` x Symlink rejected: ${u.file}`); failed++; continue; } } catch (_) {}
614
692
  }
615
693
  const content = await fetchRemote(sourceBase + remotePath(u.file));
616
694
  const normalized = content.replace(/\r\n/g, '\n');
617
695
  const fetchedHash = sha256(normalized);
618
696
  if (fetchedHash !== u.remoteHash) {
619
- console.error(` Hash mismatch: ${u.file}`);
697
+ console.error(` x Hash mismatch: ${u.file}`);
620
698
  failed++; continue;
621
699
  }
622
700
  mkdirSync(dirname(dest), { recursive: true });
623
701
  writeFileSync(dest, normalized, 'utf-8');
624
702
  applied++;
625
703
  } catch (e) {
626
- console.error(` Failed: ${u.file} ${e.message}`);
704
+ console.error(` x Failed: ${u.file} - ${e.message}`);
627
705
  failed++;
628
706
  }
629
707
  }
@@ -631,25 +709,25 @@ async function main() {
631
709
  for (const c of plan.created) {
632
710
  try {
633
711
  const dest = safePath(c.file);
634
- if (!dest) { console.error(` Traversal rejected: ${c.file}`); failed++; continue; }
712
+ if (!dest) { console.error(` x Traversal rejected: ${c.file}`); failed++; continue; }
635
713
  // TOCTOU: recheck file didn't appear since planning
636
714
  if (lexists(dest)) {
637
- try { if (lstatSync(dest).isSymbolicLink()) { console.error(` Symlink rejected: ${c.file}`); failed++; continue; } } catch (_) {}
638
- console.error(` File created since plan: ${c.file} treating as CONFLICT`);
715
+ try { if (lstatSync(dest).isSymbolicLink()) { console.error(` x Symlink rejected: ${c.file}`); failed++; continue; } } catch (_) {}
716
+ console.error(` x File created since plan: ${c.file} - treating as CONFLICT`);
639
717
  failed++; continue;
640
718
  }
641
719
  const content = await fetchRemote(sourceBase + remotePath(c.file));
642
720
  const normalized = content.replace(/\r\n/g, '\n');
643
721
  const fetchedHash = sha256(normalized);
644
722
  if (fetchedHash !== c.remoteHash) {
645
- console.error(` Hash mismatch: ${c.file}`);
723
+ console.error(` x Hash mismatch: ${c.file}`);
646
724
  failed++; continue;
647
725
  }
648
726
  mkdirSync(dirname(dest), { recursive: true });
649
727
  writeFileSync(dest, normalized, 'utf-8');
650
728
  applied++;
651
729
  } catch (e) {
652
- console.error(` Failed: ${c.file} ${e.message}`);
730
+ console.error(` x Failed: ${c.file} - ${e.message}`);
653
731
  failed++;
654
732
  }
655
733
  }
@@ -676,12 +754,12 @@ async function main() {
676
754
  }
677
755
  writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
678
756
  if (plan.conflict.length === 0) {
679
- console.log(`✅ Applied ${applied} files. Version updated to ${remoteVersion.generator}.`);
757
+ console.log(`Applied ${applied} files. Version updated to ${remoteVersion.generator}.`);
680
758
  } else {
681
- console.log(`✅ Applied ${applied} SAFE/NEW files. Version remains ${localGen}; ${plan.conflict.length} conflicts still need merge.`);
759
+ console.log(`Applied ${applied} SAFE/NEW files. Version remains ${localGen}; ${plan.conflict.length} conflicts still need merge.`);
682
760
  }
683
761
  } else {
684
- console.log(`❌ ${failed} failures. NO files were version-tracked. Fix and re-run.`);
762
+ console.log(`${failed} failures. NO files were version-tracked. Fix and re-run.`);
685
763
  process.exitCode = 1;
686
764
  }
687
765
  }
@@ -124,7 +124,7 @@ Choose the cheapest coordination level that is safe.
124
124
  Max parallelism removes the Harness default cap, not the runtime's physical or
125
125
  account cap. For WF-MAX, record the current runtime budget, use native
126
126
  subagents first, close completed agents before declaring the pool exhausted,
127
- then overflow to the other CLI (`claude -p` or `codex exec`) with explicit
127
+ then overflow to a peer CLI (`claude -p`, `codex exec`, or `opencode run --agent <role> --dir .`) with explicit
128
128
  dispatch packets. Generated Harness Codex config defaults to
129
129
  `agents.max_threads = 12` and `agents.max_depth = 1`; if that becomes the
130
130
  bottleneck, ask the user before raising `agents.max_threads` and keep
@@ -192,6 +192,13 @@ matrix from running behavior and evidence, not from the implementer's summary.
192
192
 
193
193
  If either reviewer finds issues, the implementer or debugger fixes them and the same gate runs again. Do not move to final acceptance with open critical/high findings or without reflector PASS.
194
194
 
195
+ For `/wf-review`, use the installed `reviewer` role before inventing any
196
+ ad hoc review prompt. If no peer CLI is available, dispatch `reviewer` as an
197
+ independent same-runtime subagent context; for broad WF-MAX review, dispatch
198
+ `review-manager` when the runtime supports nested reviewer fan-out. Reviewer
199
+ agents report suggestions only. The controller accepts, rejects, or escalates
200
+ each finding and owns the final decision.
201
+
195
202
  ## Subagent Status Handling
196
203
 
197
204
  | Status | Controller Action |
@@ -45,4 +45,4 @@ The agentic engineering harness lives in `Harness/`.
45
45
  Tool discovery files stay at the repository root:
46
46
 
47
47
  - Claude Code: `.claude/settings.json`, `.claude/agents/`, and `.claude/skills/`.
48
- - Codex: `.agents/skills/` for repo skills and `.codex/` for config placeholders. Runtime hooks are absent by default; only `/wf-auto` may opt into a bounded tick hook.
48
+ - Codex: `.agents/skills/` for repo skills and `.codex/` for config placeholders. The bundled update reminder uses a startup-only hook; avoid turn-by-turn runtime hooks unless `/wf-auto` explicitly opts into a bounded tick hook.