create-harness-vibe-coding 0.8.18 → 0.8.19

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 (128) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -0
  3. package/package.json +5 -2
  4. package/src/index.js +383 -267
  5. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
  6. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
  7. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
  8. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
  9. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
  10. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
  11. package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
  12. package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
  13. package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
  14. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
  15. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
  16. package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
  17. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
  18. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
  19. package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
  20. package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
  21. package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
  22. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
  23. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
  24. package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
  25. package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
  26. package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
  27. package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
  28. package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
  29. package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
  30. package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
  31. package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
  32. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
  33. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
  34. package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
  35. package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
  36. package/src/ui/dist/index.html +18 -0
  37. package/src/ui/index.html +17 -0
  38. package/src/ui/package.json +33 -0
  39. package/src/ui/pnpm-lock.yaml +1582 -0
  40. package/src/ui/pnpm-workspace.yaml +2 -0
  41. package/src/ui/src/App.tsx +86 -0
  42. package/src/ui/src/api.ts +93 -0
  43. package/src/ui/src/components/AgentsRoute.tsx +503 -0
  44. package/src/ui/src/components/Footer.tsx +69 -0
  45. package/src/ui/src/components/Header.tsx +175 -0
  46. package/src/ui/src/components/LoadingView.tsx +22 -0
  47. package/src/ui/src/components/RolesRoute.tsx +169 -0
  48. package/src/ui/src/components/SettingsRoute.tsx +166 -0
  49. package/src/ui/src/components/TaskList.tsx +388 -0
  50. package/src/ui/src/components/TerminalDrawer.tsx +611 -0
  51. package/src/ui/src/components/WorkflowRoute.tsx +670 -0
  52. package/src/ui/src/hooks/useReducedMotion.ts +17 -0
  53. package/src/ui/src/hooks/useServerConnection.ts +114 -0
  54. package/src/ui/src/index.css +281 -0
  55. package/src/ui/src/main.tsx +11 -0
  56. package/src/ui/src/types.ts +108 -0
  57. package/src/ui/tsconfig.json +21 -0
  58. package/src/ui/vite.config.ts +19 -0
  59. package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
  60. package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
  61. package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
  62. package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
  63. package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
  64. package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
  65. package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
  66. package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
  67. package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
  68. package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
  69. package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
  70. package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
  71. package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
  72. package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
  73. package/src/wf-ui-server/a2a-store.mjs +296 -0
  74. package/src/wf-ui-server/peer-capsule.mjs +213 -0
  75. package/src/wf-ui-server/pty-adapter.mjs +155 -0
  76. package/src/wf-ui-server/runtime-config.mjs +153 -0
  77. package/src/wf-ui-server/runtime-detector.mjs +374 -0
  78. package/src/wf-ui-server/security.mjs +67 -0
  79. package/src/wf-ui-server/server.mjs +849 -0
  80. package/src/wf-ui-server/session-registry.mjs +204 -0
  81. package/src/wf-ui-server/settings.mjs +100 -0
  82. package/src/wf-ui-server/task-parser.mjs +133 -0
  83. package/src/wf-ui-server/terminal-store.mjs +225 -0
  84. package/src/wf-ui-server/token.mjs +41 -0
  85. package/src/wf-ui-server/ws-events.mjs +354 -0
  86. package/src/wf-ui-server/ws-terminal.mjs +473 -0
  87. package/templates/common/.claude/commands/wf-help.md +1 -0
  88. package/templates/common/.claude/commands/wf-task-list.md +2 -0
  89. package/templates/common/.claude/commands/wf-ui.md +26 -0
  90. package/templates/common/.claude/commands/wf-update.md +54 -9
  91. package/templates/common/.claude/rules/ecc/common.md +1 -1
  92. package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -3
  93. package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
  94. package/templates/common/.claude/skills/wf-ui/SKILL.md +78 -0
  95. package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
  96. package/templates/common/.harness-version +63 -39
  97. package/templates/common/.opencode/commands/wf-help.md +1 -0
  98. package/templates/common/.opencode/commands/wf-task-list.md +2 -0
  99. package/templates/common/.opencode/commands/wf-ui.md +26 -0
  100. package/templates/common/.opencode/commands/wf-update.md +54 -9
  101. package/templates/common/CLAUDE.md +1 -1
  102. package/templates/common/Harness/MEMORY.md +2 -0
  103. package/templates/common/Harness/README.md +6 -4
  104. package/templates/common/Harness/a2a/role-graph.json +79 -0
  105. package/templates/common/Harness/a2a/runtime-registry.json +5 -0
  106. package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
  107. package/templates/common/Harness/ownership.manifest.json +57 -2
  108. package/templates/common/Harness/scripts/README.md +134 -0
  109. package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
  110. package/templates/common/Harness/scripts/context-budget.mjs +1 -1
  111. package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
  112. package/templates/common/Harness/scripts/task-state.mjs +556 -23
  113. package/templates/common/Harness/scripts/validate-harness.mjs +229 -19
  114. package/templates/common/Harness/scripts/wf-remove.mjs +8 -3
  115. package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
  116. package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
  117. package/templates/common/Harness/settings.json +35 -0
  118. package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +7 -2
  119. package/templates/common/Harness/specs/runtime/command-surface.json +14 -0
  120. package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
  121. package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
  122. package/templates/common/Harness/tasks/_template/NAMING.md +16 -16
  123. package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
  124. package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
  125. package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
  126. package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
  127. package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
  128. package/templates/common/Harness/tasks/_template/NOTES.md +0 -3
@@ -1,17 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { createHash } from 'node:crypto';
4
5
 
5
6
  const root = process.cwd();
6
7
  const args = new Set(process.argv.slice(2));
7
8
  const strict = args.has('--strict') || args.has('--post-bootstrap');
9
+ const manifestAudit = args.has('--manifest-audit');
8
10
 
9
11
  if (args.has('--help') || args.has('-h')) {
10
- console.log(`Usage: node Harness/scripts/validate-harness.mjs [--strict]
12
+ console.log(`Usage: node Harness/scripts/validate-harness.mjs [--strict] [--manifest-audit]
11
13
 
12
14
  Default mode checks scaffold structure, links, agents, and skills.
13
15
  --strict also fails when project fact docs still contain unresolved {{TOKEN}} placeholders.
14
- Literal explanatory {{...}} text is allowed.`);
16
+ --manifest-audit cross-references ownership.manifest.json against disk: flags missing framework files (manifest-to-disk) and extra files in harness directories (disk-to-manifest).`);
15
17
  process.exit(0);
16
18
  }
17
19
 
@@ -150,8 +152,10 @@ const required = [
150
152
  'Harness/scripts/context-budget.mjs',
151
153
  'Harness/scripts/l2-cache-telemetry.mjs',
152
154
  'Harness/scripts/wf-update-check.mjs',
155
+ 'Harness/scripts/wf-update-runner.mjs',
153
156
  'Harness/scripts/wf-remove.mjs',
154
157
  'Harness/scripts/scan-clean.mjs',
158
+ 'Harness/scripts/sync-host-global.mjs',
155
159
  'Harness/scripts/task-state.mjs',
156
160
  'Harness/scripts/archive-tasks.mjs',
157
161
  'Harness/specs/workflows/WF-STATE.md',
@@ -262,6 +266,48 @@ function resolveMetadataPath(value) {
262
266
  return path.isAbsolute(value) ? value : path.resolve(root, value);
263
267
  }
264
268
 
269
+ function containsPath(parent, child) {
270
+ const resolvedParent = path.resolve(parent);
271
+ const resolvedChild = path.resolve(child);
272
+ const rel = path.relative(resolvedParent, resolvedChild);
273
+ return rel === '' || (rel && !rel.startsWith('..') && !path.isAbsolute(rel));
274
+ }
275
+
276
+ function resolveUnder(base, rel) {
277
+ if (!rel || typeof rel !== 'string' || path.isAbsolute(rel)) return null;
278
+ const parts = rel.replace(/\\/g, '/').split('/').filter(Boolean);
279
+ if (parts.some(part => part === '..')) return null;
280
+ const resolved = path.resolve(base, ...parts);
281
+ return containsPath(base, resolved) ? resolved : null;
282
+ }
283
+
284
+ function sha256Content(content) {
285
+ return 'sha256-' + createHash('sha256').update(content.replace(/\r\n/g, '\n')).digest('hex');
286
+ }
287
+
288
+ function sha256File(filePath) {
289
+ if (!fs.existsSync(filePath)) return null;
290
+ return sha256Content(fs.readFileSync(filePath, 'utf8'));
291
+ }
292
+
293
+ function sourceDestForHostFile(host, file) {
294
+ const normalized = String(file || '').replace(/\\/g, '/');
295
+ if (host === 'claude') {
296
+ if (normalized === 'settings.json') return '.claude/settings.json';
297
+ if (/^(commands|skills|agents|rules)\//.test(normalized)) return `.claude/${normalized}`;
298
+ }
299
+ if (host === 'codex') {
300
+ if (normalized === 'config.toml') return '.codex/config.toml';
301
+ if (normalized === 'hooks.json') return '.codex/hooks.json';
302
+ if (normalized.startsWith('skills/')) return `.agents/${normalized}`;
303
+ }
304
+ if (host === 'opencode') {
305
+ if (normalized === 'opencode.json') return 'opencode.json';
306
+ if (/^(commands|agents|plugins)\//.test(normalized)) return `.opencode/${normalized}`;
307
+ }
308
+ return null;
309
+ }
310
+
265
311
  function requireText(rel, text, label = text) {
266
312
  const body = read(rel);
267
313
  if (body && !body.includes(text)) errors.push(`${rel} missing ${label}`);
@@ -304,10 +350,11 @@ function validateHostGlobalTargets() {
304
350
  }
305
351
 
306
352
  const minimumFiles = {
307
- claude: ['commands/wf.md', 'skills/wf/SKILL.md'],
308
- codex: ['skills/wf/SKILL.md'],
309
- opencode: ['commands/wf.md'],
353
+ claude: ['commands/wf.md', 'commands/wf-ui.md', 'skills/wf/SKILL.md', 'skills/wf-ui/SKILL.md'],
354
+ codex: ['skills/wf/SKILL.md', 'skills/wf-ui/SKILL.md'],
355
+ opencode: ['commands/wf.md', 'commands/wf-ui.md'],
310
356
  };
357
+ const runtimeRoot = isGlobalRuntimeRoot ? root : resolveMetadataPath(installMetadata.globalDir);
311
358
 
312
359
  for (const [host, requiredFiles] of Object.entries(minimumFiles)) {
313
360
  const target = hostGlobal.targets[host];
@@ -350,6 +397,22 @@ function validateHostGlobalTargets() {
350
397
  errors.push(`missing host-global ${host} copied file: ${file}`);
351
398
  continue;
352
399
  }
400
+
401
+ const sourceDest = sourceDestForHostFile(host, file);
402
+ const sourcePath = sourceDest && runtimeRoot ? resolveUnder(runtimeRoot, sourceDest) : null;
403
+ if (!sourceDest || !sourcePath) {
404
+ errors.push(`host-global ${host} copied file has no runtime source mapping: ${file}`);
405
+ continue;
406
+ }
407
+ if (!fs.existsSync(sourcePath)) {
408
+ errors.push(`host-global ${host} copied file source is missing: ${file} -> ${sourceDest}`);
409
+ continue;
410
+ }
411
+ const sourceHash = sha256File(sourcePath);
412
+ const targetHash = sha256File(filePath);
413
+ if (sourceHash && targetHash && sourceHash !== targetHash) {
414
+ errors.push(`host-global ${host} copied file is stale: ${file} (source ${sourceDest})`);
415
+ }
353
416
  }
354
417
  }
355
418
  }
@@ -466,7 +529,7 @@ function listMarkdownFiles(rel) {
466
529
  // Reserved: _template (system), auto (auto-mode capsule)
467
530
  const TASK_NAME_RE = /^task-[a-z]+(-[a-z0-9]+){1,4}$/;
468
531
  const TASK_NAME_MAX = 46; // "task-" (5) + ≤40 chars body + 1 safety = 46
469
- const TASK_RESERVED = new Set(['_template', 'auto', '_archive']);
532
+ const TASK_RESERVED = new Set(['_template', 'auto', '_archive', 'continuous']);
470
533
  const TASK_SAFE_ARCHIVE_STATUSES = new Set(['complete', 'verified', 'archived', 'abandoned', 'obsolete', 'done', 'closed', 'closeout']);
471
534
  const TASK_NEVER_ARCHIVE_STATUSES = new Set(['active', 'blocked', 'in_progress', 'running', 'pending', 'needs-user-decision']);
472
535
  const TASK_PHASE_ALIASES = new Map([
@@ -770,21 +833,147 @@ for (const rel of legacyRootSpecDocs) {
770
833
  }
771
834
  }
772
835
 
773
- const manifestText = read('Harness/ownership.manifest.json');
774
- if (manifestText) {
836
+ function auditManifestCoverage() {
837
+ const manifestText = read('Harness/ownership.manifest.json');
838
+ if (!manifestText) {
839
+ errors.push('Harness/ownership.manifest.json not found — cannot run manifest audit');
840
+ return;
841
+ }
842
+
843
+ let manifest;
775
844
  try {
776
- const manifest = JSON.parse(manifestText);
777
- for (const entry of manifest.frameworkOwned || []) {
778
- if (!entry || typeof entry.path !== 'string') continue;
779
- if (!fs.existsSync(path.join(root, ...entry.path.split('/')))) {
780
- errors.push(`ownership manifest frameworkOwned file missing: ${entry.path}`);
781
- }
782
- }
845
+ manifest = JSON.parse(manifestText);
783
846
  } catch (err) {
784
847
  errors.push(`Harness/ownership.manifest.json is not valid JSON: ${err.message}`);
848
+ return;
849
+ }
850
+
851
+ const frameworkOwned = manifest.frameworkOwned || [];
852
+ const manifestFileSet = new Set(frameworkOwned.map(e => e && e.path).filter(Boolean));
853
+ const preserveGlobs = manifest.preserve || [];
854
+ const mergeSet = new Set(manifest.merge || []);
855
+
856
+ // 1. Manifest → Disk: frameworkOwned files must exist (always checked)
857
+ for (const entry of frameworkOwned) {
858
+ if (!entry || typeof entry.path !== 'string') continue;
859
+ if (!fs.existsSync(path.join(root, ...entry.path.split('/')))) {
860
+ errors.push(`ownership manifest frameworkOwned file missing: ${entry.path}`);
861
+ }
862
+ }
863
+
864
+ // 2. Only run Disk → Manifest scan when --manifest-audit is passed
865
+ if (!manifestAudit) return;
866
+
867
+ // Directories that Harness framework owns (for disk→manifest scan)
868
+ const harnessOwnedDirs = [
869
+ '.claude/agents/',
870
+ '.claude/commands/',
871
+ '.claude/skills/',
872
+ '.claude/rules/ecc/',
873
+ '.opencode/agents/',
874
+ '.opencode/commands/',
875
+ '.opencode/plugins/',
876
+ '.agents/skills/',
877
+ '.codex/',
878
+ 'Harness/scripts/',
879
+ 'Harness/specs/',
880
+ 'Harness/templates/',
881
+ 'Harness/research/',
882
+ ];
883
+
884
+ // Root-level harness-owned files (not in subdirectories)
885
+ const harnessRootFiles = [
886
+ 'opencode.json',
887
+ 'AGENTS.md',
888
+ 'CLAUDE.md',
889
+ '.claude/settings.json',
890
+ ];
891
+
892
+ const extraFiles = [];
893
+
894
+ // Scan harness-owned directories
895
+ for (const dir of harnessOwnedDirs) {
896
+ const dirPath = path.join(root, dir);
897
+ if (!fs.existsSync(dirPath)) continue;
898
+ scanDir(dirPath, dir, extraFiles);
899
+ }
900
+
901
+ // Scan root-level harness files
902
+ for (const rel of harnessRootFiles) {
903
+ const filePath = path.join(root, rel);
904
+ if (!fs.existsSync(filePath)) continue;
905
+ if (!manifestFileSet.has(rel) && !mergeSet.has(rel)) {
906
+ extraFiles.push(rel);
907
+ }
908
+ }
909
+
910
+ // Also scan for top-level files like .harness-version, ownership.manifest.json
911
+ const versionFile = 'Harness/.harness-version';
912
+ if (fs.existsSync(path.join(root, versionFile)) && !manifestFileSet.has(versionFile)) {
913
+ // .harness-version is special — always framework-owned
914
+ }
915
+ const manifestFile = 'Harness/ownership.manifest.json';
916
+ if (fs.existsSync(path.join(root, manifestFile)) && !manifestFileSet.has(manifestFile)) {
917
+ // ownership.manifest.json is special — always framework-owned
918
+ }
919
+
920
+ // Check if extra files are user-owned (preserve/merge) or already in manifest
921
+ for (const file of extraFiles) {
922
+ // Skip files already registered in the manifest (correctly tracked)
923
+ if (manifestFileSet.has(file)) continue;
924
+
925
+ const isPreserve = preserveGlobs.some(g => {
926
+ const re = new RegExp('^' + g.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') + '$');
927
+ return re.test(file);
928
+ });
929
+ if (isPreserve || mergeSet.has(file)) continue; // User file, skip
930
+
931
+ // Check if it's in the .harness-version checksums (tracked but maybe not in manifest)
932
+ const checksums = (() => {
933
+ try { return JSON.parse(fs.readFileSync(path.join(root, 'Harness', '.harness-version'), 'utf8')).checksums || {}; }
934
+ catch { return {}; }
935
+ })();
936
+ const isTracked = Object.prototype.hasOwnProperty.call(checksums, file);
937
+
938
+ if (isTracked) {
939
+ // File is in .harness-version but NOT in ownership manifest → manifest coverage gap
940
+ errors.push(`manifest coverage gap: ${file} is in .harness-version checksums but NOT in ownership.manifest.json frameworkOwned`);
941
+ } else {
942
+ // File is NOT tracked at all → truly extra/stale
943
+ const content = (() => { try { return fs.readFileSync(path.join(root, file), 'utf8'); } catch { return ''; } })();
944
+ const hasHarnessMarker = /harness:|Harness\/|create-harness-vibe-coding/i.test(content);
945
+ if (hasHarnessMarker) {
946
+ errors.push(`extra harness-owned file not in manifest: ${file} (has Harness content marker — likely stale framework file)`);
947
+ } else {
948
+ // File in harness directory but no harness marker → could be user file, warn only
949
+ console.warn(`Warning: untracked file in harness directory: ${file} (no Harness content marker — may be user file)`);
950
+ }
951
+ }
785
952
  }
786
953
  }
787
954
 
955
+ function scanDir(dirPath, prefix, results) {
956
+ let entries;
957
+ try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); }
958
+ catch { return; }
959
+
960
+ for (const entry of entries) {
961
+ const relPath = `${prefix}${entry.name}`;
962
+ const fullPath = path.join(dirPath, entry.name);
963
+
964
+ if (entry.isDirectory()) {
965
+ scanDir(fullPath, `${relPath}/`, results);
966
+ } else if (entry.isFile()) {
967
+ // Normalize path separator
968
+ const normalized = relPath.replace(/\\/g, '/');
969
+ results.push(normalized);
970
+ }
971
+ }
972
+ }
973
+
974
+ // Run manifest audit (basic manifest→disk always; --manifest-audit adds disk→manifest)
975
+ auditManifestCoverage();
976
+
788
977
  const removedHookArtifacts = [
789
978
  'Harness/HOOK_PROTOCOL.md',
790
979
  'Harness/scripts/wf-mode-hook.mjs',
@@ -1331,8 +1520,8 @@ requireText('Harness/specs/workflows/WF-AUTO.md', 'Inherited WF/WF-MAX Constrain
1331
1520
  requireText('Harness/specs/workflows/WF-AUTO.md', 'Mini PRD -> AC IDs -> test/validation plan -> implementer -> verifier', 'wf-auto per-cycle WF chain');
1332
1521
  requireText('Harness/specs/workflows/WF-AUTO.md', 'reflector PASS', 'wf-auto reflector gate');
1333
1522
  requireText('Harness/specs/workflows/WF-AUTO.md', 'Manual or benchmark-driven single-cycle', 'wf-auto bounded single-cycle tick contract');
1334
- requireText('Harness/specs/workflows/WF-AUTO.md', 'Harness/tasks/auto/PLAN.md', 'wf-auto bounded tick PLAN record');
1335
- requireText('Harness/specs/workflows/WF-AUTO.md', 'Harness/tasks/auto/PROGRESS.md', 'wf-auto bounded tick PROGRESS record');
1523
+ requireText('Harness/specs/workflows/WF-AUTO.md', 'Harness/tasks/continuous/PLAN.md', 'wf-auto bounded tick PLAN record');
1524
+ requireText('Harness/specs/workflows/WF-AUTO.md', 'Harness/tasks/continuous/PROGRESS.md', 'wf-auto bounded tick PROGRESS record');
1336
1525
  requireText('Harness/specs/workflows/WF-AUTO-SPARK.md', 'Inherited Execution Chain', 'wf-auto-spark inherited execution chain');
1337
1526
  requireText('Harness/specs/workflows/WF-AUTO-SPARK.md', 'External spark search replaces discovery only', 'wf-auto-spark discovery-only inheritance');
1338
1527
  requireText('Harness/specs/workflows/WF-AUTO-SPARK.md', 'reflector PASS', 'wf-auto-spark reflector gate');
@@ -1464,8 +1653,8 @@ for (const skill of cacheDisciplinedSkills) {
1464
1653
  }
1465
1654
  requireText('.claude/skills/wf-auto/SKILL.md', 'bounded test tick', 'wf-auto skill bounded tick auto capsule rule');
1466
1655
  requireText('.claude/skills/wf-auto/SKILL.md', 'missing auto capsule evidence', 'wf-auto skill missing auto capsule failure rule');
1467
- requireText('.claude/skills/wf-auto/SKILL.md', 'Harness/tasks/auto/PLAN.md', 'wf-auto skill PLAN record');
1468
- requireText('.claude/skills/wf-auto/SKILL.md', 'Harness/tasks/auto/PROGRESS.md', 'wf-auto skill PROGRESS record');
1656
+ requireText('.claude/skills/wf-auto/SKILL.md', 'Harness/tasks/continuous/PLAN.md', 'wf-auto skill PLAN record');
1657
+ requireText('.claude/skills/wf-auto/SKILL.md', 'Harness/tasks/continuous/PROGRESS.md', 'wf-auto skill PROGRESS record');
1469
1658
  requireText('.claude/skills/wf-auto/SKILL.md', 'evidence ledger path/summary', 'wf-auto skill return evidence ledger path');
1470
1659
  if (fs.existsSync(path.join(root, '.claude/skills/wf-browser/SKILL.md'))) {
1471
1660
  requireText('.claude/skills/wf-browser/SKILL.md', 'Cache Discipline', 'wf-browser skill cache discipline');
@@ -1485,9 +1674,11 @@ if (fs.existsSync(path.join(root, '.claude/skills/wf-browser/SKILL.md'))) {
1485
1674
  // Direct command checks
1486
1675
  requireText('Harness/README.md', '/wf-update', 'wf-update direct command reference');
1487
1676
  requireText('.claude/commands/wf-update.md', 'Do not invoke a skill', 'wf-update direct command boundary');
1677
+ requireText('Harness/README.md', '| `/wf-ui`, `$wf-ui` |', 'wf-ui direct command row');
1488
1678
  for (const rel of ['.claude/commands/wf-update.md', '.opencode/commands/wf-update.md']) {
1489
1679
  requireText(rel, '## Cache Discipline', `${rel} cache discipline`);
1490
1680
  requireText(rel, 'agent.safeApplyCommand', `${rel} safe apply step`);
1681
+ requireText(rel, 'wf-update-runner.mjs', `${rel} multi-scope update runner`);
1491
1682
  requireText(rel, '--apply-safe', `${rel} apply-safe command`);
1492
1683
  requireText(rel, 'agent.aiMergeRequired', `${rel} AI merge step`);
1493
1684
  requireText(rel, '--accept-local', `${rel} accept-local decision`);
@@ -1495,16 +1686,35 @@ for (const rel of ['.claude/commands/wf-update.md', '.opencode/commands/wf-updat
1495
1686
  requireText(rel, '--accept-template', `${rel} accept-template decision`);
1496
1687
  requireText(rel, '--finalize', `${rel} finalize command`);
1497
1688
  requireText(rel, 'strict `--apply` only when', `${rel} strict apply boundary`);
1689
+ requireText(rel, 'sync-host-global.mjs', `${rel} host-global sync step`);
1498
1690
  requireText(rel, '## Return', `${rel} return contract`);
1499
1691
  requireText(rel, 'agent.releaseHighlights', `${rel} release highlights summary`);
1500
1692
  requireText(rel, 'releaseNotes.highlights', `${rel} release notes fallback`);
1501
1693
  }
1694
+ for (const rel of ['.claude/commands/wf-ui.md', '.opencode/commands/wf-ui.md']) {
1695
+ requireText(rel, 'direct command', `${rel} wf-ui direct command classification`);
1696
+ requireText(rel, 'Do not invoke a skill', `${rel} wf-ui direct no-skill boundary`);
1697
+ requireText(rel, 'create-harness-vibe-coding wf-ui', `${rel} wf-ui direct CLI launch`);
1698
+ requireText(rel, '--host 127.0.0.1', `${rel} wf-ui loopback host`);
1699
+ requireText(rel, '--open', `${rel} wf-ui browser open flag`);
1700
+ forbidText(rel, 'workflow command', `${rel} wf-ui workflow routing`);
1701
+ forbidText(rel, 'Load `CLAUDE.md`', `${rel} wf-ui old router preload`);
1702
+ forbidText(rel, 'Harness/tasks/task-wf-ui-control-0729/STATE.json', `${rel} wf-ui old task-state preload`);
1703
+ }
1704
+ for (const rel of ['.claude/skills/wf-ui/SKILL.md', '.agents/skills/wf-ui/SKILL.md']) {
1705
+ requireText(rel, 'Codex compatibility shim', `${rel} wf-ui Codex compatibility shim`);
1706
+ requireText(rel, 'direct command', `${rel} wf-ui direct command statement`);
1707
+ requireText(rel, 'create-harness-vibe-coding wf-ui', `${rel} wf-ui direct CLI launch`);
1708
+ forbidText(rel, 'Harness/tasks/task-wf-ui-control-0729/STATE.json', `${rel} wf-ui old task-state preload`);
1709
+ }
1502
1710
  requireText('Harness/scripts/wf-update-check.mjs', 'releaseHighlights', 'wf-update-check release highlights metadata');
1503
1711
  requireText('Harness/scripts/wf-update-check.mjs', 'updateReportRequired', 'wf-update-check user update report requirement');
1504
1712
  requireText('.claude/commands/wf-help.md', 'direct command', 'wf-help wf-update direct command classification');
1505
1713
  requireText('.claude/commands/wf-help.md', '$wf-help', 'wf-help Codex compatibility usage');
1506
1714
  requireText('.claude/commands/wf-help.md', '/wf-browser', 'wf-help built-in browser workflow row');
1715
+ requireText('.claude/commands/wf-help.md', '| `/wf-ui` | direct command |', 'wf-help wf-ui direct command row');
1507
1716
  requireText('.opencode/commands/wf-help.md', '/wf-browser', 'OpenCode wf-help built-in browser workflow row');
1717
+ requireText('.opencode/commands/wf-help.md', '| `/wf-ui` | direct command |', 'OpenCode wf-help wf-ui direct command row');
1508
1718
 
1509
1719
  // wf-update skill must NOT claim Claude Code /wf-update as a skill invocation
1510
1720
  // (allow mentions in the description/body that say "direct command" — those are correct)
@@ -83,6 +83,7 @@ const PURGEABLE_HARNESS_DATA_PATTERNS = [
83
83
  const FRAMEWORK_TASK_PATTERNS = [
84
84
  /^Harness\/tasks\/_template\//,
85
85
  /^Harness\/tasks\/auto\//,
86
+ /^Harness\/tasks\/continuous\//,
86
87
  ];
87
88
 
88
89
  function isUserData(file) {
@@ -151,6 +152,7 @@ const BUILT_IN_SKILL_NAMES = [
151
152
  'wf-task-archive',
152
153
  'wf-task-list',
153
154
  'wf-task-record',
155
+ 'wf-ui',
154
156
  'wf-update',
155
157
  ];
156
158
 
@@ -169,6 +171,7 @@ const BUILT_IN_COMMAND_NAMES = [
169
171
  'wf-task-archive',
170
172
  'wf-task-list',
171
173
  'wf-task-record',
174
+ 'wf-ui',
172
175
  'wf-update',
173
176
  ];
174
177
 
@@ -216,6 +219,7 @@ const CLEANUP_DIRS = [
216
219
  '.claude/skills/wf-task-list',
217
220
  '.claude/skills/wf-task-record',
218
221
  '.claude/skills/wf-update',
222
+ '.claude/skills/wf-ui',
219
223
  '.claude/skills/wf-remove',
220
224
  '.claude/skills/subagent-orchestrator',
221
225
  '.claude/skills',
@@ -239,6 +243,7 @@ const CLEANUP_DIRS = [
239
243
  '.agents/skills/wf-task-list',
240
244
  '.agents/skills/wf-task-record',
241
245
  '.agents/skills/wf-update',
246
+ '.agents/skills/wf-ui',
242
247
  '.agents/skills/wf-remove',
243
248
  '.agents/skills/subagent-orchestrator',
244
249
  '.agents/skills',
@@ -263,7 +268,7 @@ const CLEANUP_DIRS = [
263
268
  'Harness/features',
264
269
  'Harness/memory',
265
270
  'Harness/tasks/_template',
266
- 'Harness/tasks/auto',
271
+ 'Harness/tasks/continuous',
267
272
  'Harness/tasks',
268
273
  'Harness',
269
274
  ];
@@ -434,8 +439,8 @@ async function main() {
434
439
  '.claude/skills/wf-auto/SKILL.md',
435
440
  '.agents/skills/wf-auto/SKILL.md',
436
441
  'Harness/specs/workflows/WF-AUTO.md',
437
- 'Harness/tasks/auto/PROGRESS.md',
438
- 'Harness/tasks/auto/PLAN.md',
442
+ 'Harness/tasks/continuous/PROGRESS.md',
443
+ 'Harness/tasks/continuous/PLAN.md',
439
444
  'Harness/tasks/_template/ARTIFACTS.md',
440
445
  'Harness/tasks/_template/NOTES.md',
441
446
  'Harness/tasks/_template/PLAN.md',
@@ -541,6 +541,10 @@ async function main() {
541
541
  const acceptMerged = readRepeatedFlagValues(args, '--accept-merged');
542
542
  const acceptTemplate = readRepeatedFlagValues(args, '--accept-template');
543
543
  const ignoreVersion = args.includes('--ignore-version') || args.includes('--force-check');
544
+ const repairMode = args.includes('--repair');
545
+ const effectiveIgnoreVersion = ignoreVersion || repairMode;
546
+ const allowDowngrade = args.includes('--allow-downgrade');
547
+ const allowPrerelease = args.includes('--include-prerelease');
544
548
  const explicitSource = readFlagValue(args, '--source-base') || process.env.WF_SOURCE_BASE;
545
549
  let source;
546
550
  let sourceBase;
@@ -632,7 +636,7 @@ async function main() {
632
636
  const localGen = localVersion.generator || '0.0.0';
633
637
  const remoteGen = remoteVersion.generator || '0.0.0';
634
638
 
635
- if (!ignoreVersion && isPrerelease(remoteGen)) {
639
+ if (!allowPrerelease && isPrerelease(remoteGen)) {
636
640
  if (jsonOut) {
637
641
  console.log(JSON.stringify({ status: 'up-to-date', version: localGen, remote: remoteGen, sourceBase }));
638
642
  } else {
@@ -642,27 +646,41 @@ async function main() {
642
646
  }
643
647
 
644
648
  const versionCmp = cmpSemver(remoteGen, localGen);
649
+ const reportDowngrade = !sourceStable;
645
650
 
646
- if (!ignoreVersion && versionCmp <= 0) {
647
- const reportDowngrade = !sourceStable;
651
+ if (versionCmp < 0 && !allowDowngrade) {
648
652
  if (jsonOut) {
649
653
  console.log(JSON.stringify({
650
- status: (versionCmp < 0 && reportDowngrade) ? 'downgrade-refused' : 'up-to-date',
654
+ status: reportDowngrade ? 'downgrade-refused' : 'up-to-date',
651
655
  version: localGen,
652
656
  remote: remoteGen,
653
657
  sourceBase,
654
658
  }));
655
- } else if (versionCmp < 0 && reportDowngrade) {
659
+ } else if (reportDowngrade) {
656
660
  console.log(`WARN: Remote (v${remoteGen}) is OLDER than local (v${localGen}). Downgrade refused.`);
661
+ } else {
662
+ console.log(`Already up to date (v${localGen}). Remote ${remoteGen} is older and ignored.`);
663
+ }
664
+ if (reportDowngrade) process.exitCode = 1;
665
+ return;
666
+ }
667
+
668
+ if (!effectiveIgnoreVersion && versionCmp <= 0) {
669
+ if (jsonOut) {
670
+ console.log(JSON.stringify({
671
+ status: 'up-to-date',
672
+ version: localGen,
673
+ remote: remoteGen,
674
+ sourceBase,
675
+ }));
657
676
  } else {
658
677
  console.log(`Already up to date (v${localGen})`);
659
678
  }
660
- if (versionCmp < 0 && reportDowngrade) process.exitCode = 1;
661
679
  return;
662
680
  }
663
681
 
664
- if (ignoreVersion) {
665
- if (!jsonOut) console.log('Version check bypassed (--ignore-version). Comparing files anyway.');
682
+ if (effectiveIgnoreVersion) {
683
+ if (!jsonOut) console.log(`Version check bypassed (${repairMode ? '--repair' : '--ignore-version'}). Comparing files anyway.`);
666
684
  }
667
685
 
668
686
  const remoteChecksums = remoteVersion.checksums || {};
@@ -760,6 +778,9 @@ async function main() {
760
778
  : 0;
761
779
  return {
762
780
  mode: 'script-first-ai-conflicts',
781
+ multiScopeDryRunCommand: 'node Harness/scripts/wf-update-runner.mjs --json',
782
+ multiScopeSafeApplyCommand: 'node Harness/scripts/wf-update-runner.mjs --apply-safe --json',
783
+ multiScopeFinalizeCommand: 'node Harness/scripts/wf-update-runner.mjs --finalize --json',
763
784
  dryRunJsonCommand: 'node Harness/scripts/wf-update-check.mjs --json',
764
785
  safeApplyCommand: 'node Harness/scripts/wf-update-check.mjs --apply-safe',
765
786
  strictApplyCommand: 'node Harness/scripts/wf-update-check.mjs --apply',
@@ -776,7 +797,10 @@ async function main() {
776
797
  aiMergeRequiredTruncated: truncated > 0 ? truncated : undefined,
777
798
  conflictPolicy: 'Use the script for SAFE/NEW/adopted files. For each CONFLICT file, compare local content with templateHint/remoteUrl, then record the decision with --accept-local, --accept-merged, or --accept-template. Do not hand-edit Harness/.harness-version.',
778
799
  postUpdateCommands: [
800
+ 'node Harness/scripts/sync-host-global.mjs --json',
801
+ 'node Harness/scripts/sync-host-global.mjs --apply --json',
779
802
  'node Harness/scripts/validate-harness.mjs',
803
+ 'node Harness/scripts/validate-harness.mjs --manifest-audit',
780
804
  'node Harness/scripts/scan-clean.mjs --json',
781
805
  ],
782
806
  bootstrapDebtCommand: 'node Harness/scripts/validate-harness.mjs --strict',
@@ -1010,8 +1034,11 @@ async function main() {
1010
1034
  if (jsonOut) {
1011
1035
  const verbose = args.includes('--verbose') || args.includes('--full-plan');
1012
1036
  const jsonPlan = buildJsonPlan();
1037
+ const effectiveStatus = repairMode && versionCmp <= 0 && !localVersion.partialUpdate
1038
+ ? 'repair-check'
1039
+ : (localVersion.partialUpdate ? 'partial-update' : 'update-available');
1013
1040
  console.log(JSON.stringify({
1014
- status: localVersion.partialUpdate ? 'partial-update' : 'update-available',
1041
+ status: effectiveStatus,
1015
1042
  from: localGen,
1016
1043
  to: remoteGen,
1017
1044
  sourceBase,
@@ -1026,6 +1053,7 @@ async function main() {
1026
1053
  skipped: plan.skipped.length,
1027
1054
  // Token-safe by default: attach the full plan only when --verbose / --full-plan is passed.
1028
1055
  ...(verbose ? { plan: jsonPlan } : {}),
1056
+ repair: repairMode || undefined,
1029
1057
  agent: buildAgentHints(jsonPlan),
1030
1058
  }, null, 2));
1031
1059
  return;