create-harness-vibe-coding 0.8.17 → 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 (150) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README-CN.md +2 -0
  3. package/README.md +3 -0
  4. package/package.json +5 -2
  5. package/src/generator.js +613 -97
  6. package/src/index.js +556 -309
  7. package/src/prompts.js +18 -0
  8. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
  9. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
  10. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
  11. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
  12. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
  13. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
  14. package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
  15. package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
  16. package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
  17. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
  18. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
  19. package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
  20. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
  21. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
  22. package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
  23. package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
  24. package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
  25. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
  26. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
  27. package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
  28. package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
  29. package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
  30. package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
  31. package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
  32. package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
  33. package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
  34. package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
  35. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
  36. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
  37. package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
  38. package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
  39. package/src/ui/dist/index.html +18 -0
  40. package/src/ui/index.html +17 -0
  41. package/src/ui/package.json +33 -0
  42. package/src/ui/pnpm-lock.yaml +1582 -0
  43. package/src/ui/pnpm-workspace.yaml +2 -0
  44. package/src/ui/src/App.tsx +86 -0
  45. package/src/ui/src/api.ts +93 -0
  46. package/src/ui/src/components/AgentsRoute.tsx +503 -0
  47. package/src/ui/src/components/Footer.tsx +69 -0
  48. package/src/ui/src/components/Header.tsx +175 -0
  49. package/src/ui/src/components/LoadingView.tsx +22 -0
  50. package/src/ui/src/components/RolesRoute.tsx +169 -0
  51. package/src/ui/src/components/SettingsRoute.tsx +166 -0
  52. package/src/ui/src/components/TaskList.tsx +388 -0
  53. package/src/ui/src/components/TerminalDrawer.tsx +611 -0
  54. package/src/ui/src/components/WorkflowRoute.tsx +670 -0
  55. package/src/ui/src/hooks/useReducedMotion.ts +17 -0
  56. package/src/ui/src/hooks/useServerConnection.ts +114 -0
  57. package/src/ui/src/index.css +281 -0
  58. package/src/ui/src/main.tsx +11 -0
  59. package/src/ui/src/types.ts +108 -0
  60. package/src/ui/tsconfig.json +21 -0
  61. package/src/ui/vite.config.ts +19 -0
  62. package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
  63. package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
  64. package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
  65. package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
  66. package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
  67. package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
  68. package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
  69. package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
  70. package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
  71. package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
  72. package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
  73. package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
  74. package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
  75. package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
  76. package/src/wf-ui-server/a2a-store.mjs +296 -0
  77. package/src/wf-ui-server/peer-capsule.mjs +213 -0
  78. package/src/wf-ui-server/pty-adapter.mjs +155 -0
  79. package/src/wf-ui-server/runtime-config.mjs +153 -0
  80. package/src/wf-ui-server/runtime-detector.mjs +374 -0
  81. package/src/wf-ui-server/security.mjs +67 -0
  82. package/src/wf-ui-server/server.mjs +849 -0
  83. package/src/wf-ui-server/session-registry.mjs +204 -0
  84. package/src/wf-ui-server/settings.mjs +100 -0
  85. package/src/wf-ui-server/task-parser.mjs +133 -0
  86. package/src/wf-ui-server/terminal-store.mjs +225 -0
  87. package/src/wf-ui-server/token.mjs +41 -0
  88. package/src/wf-ui-server/ws-events.mjs +354 -0
  89. package/src/wf-ui-server/ws-terminal.mjs +473 -0
  90. package/templates/common/.claude/commands/wf-command-create.md +58 -0
  91. package/templates/common/.claude/commands/wf-help.md +5 -0
  92. package/templates/common/.claude/commands/wf-task-archive.md +26 -0
  93. package/templates/common/.claude/commands/wf-task-list.md +26 -0
  94. package/templates/common/.claude/commands/wf-task-record.md +24 -0
  95. package/templates/common/.claude/commands/wf-ui.md +26 -0
  96. package/templates/common/.claude/commands/wf-update.md +54 -9
  97. package/templates/common/.claude/rules/ecc/common.md +1 -1
  98. package/templates/common/.claude/skills/wf-agents-docs/SKILL.md +15 -30
  99. package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -3
  100. package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
  101. package/templates/common/.claude/skills/wf-command-create/SKILL.md +37 -0
  102. package/templates/common/.claude/skills/wf-max/SKILL.md +1 -1
  103. package/templates/common/.claude/skills/wf-review/SKILL.md +29 -2
  104. package/templates/common/.claude/skills/wf-task-archive/SKILL.md +28 -0
  105. package/templates/common/.claude/skills/wf-task-list/SKILL.md +28 -0
  106. package/templates/common/.claude/skills/wf-task-record/SKILL.md +28 -0
  107. package/templates/common/.claude/skills/wf-ui/SKILL.md +78 -0
  108. package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
  109. package/templates/common/.harness-version +105 -47
  110. package/templates/common/.opencode/commands/wf-command-create.md +61 -0
  111. package/templates/common/.opencode/commands/wf-help.md +5 -0
  112. package/templates/common/.opencode/commands/wf-task-archive.md +29 -0
  113. package/templates/common/.opencode/commands/wf-task-list.md +29 -0
  114. package/templates/common/.opencode/commands/wf-task-record.md +27 -0
  115. package/templates/common/.opencode/commands/wf-ui.md +26 -0
  116. package/templates/common/.opencode/commands/wf-update.md +54 -9
  117. package/templates/common/CLAUDE.md +8 -6
  118. package/templates/common/Harness/MEMORY.md +11 -0
  119. package/templates/common/Harness/README.md +21 -38
  120. package/templates/common/Harness/a2a/role-graph.json +79 -0
  121. package/templates/common/Harness/a2a/runtime-registry.json +5 -0
  122. package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
  123. package/templates/common/Harness/ownership.manifest.json +142 -2
  124. package/templates/common/Harness/scripts/README.md +134 -0
  125. package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
  126. package/templates/common/Harness/scripts/context-budget.mjs +1 -1
  127. package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
  128. package/templates/common/Harness/scripts/task-state.mjs +949 -26
  129. package/templates/common/Harness/scripts/validate-harness.mjs +637 -62
  130. package/templates/common/Harness/scripts/wf-remove.mjs +42 -5
  131. package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
  132. package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
  133. package/templates/common/Harness/settings.json +35 -0
  134. package/templates/common/Harness/specs/guides/SETUP.md +8 -0
  135. package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +15 -0
  136. package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +16 -5
  137. package/templates/common/Harness/specs/runtime/command-surface.json +229 -0
  138. package/templates/common/Harness/specs/runtime/subagents.md +6 -0
  139. package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
  140. package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
  141. package/templates/common/Harness/specs/workflows/WF-MAX.md +5 -0
  142. package/templates/common/Harness/specs/workflows/WF-STATE.md +66 -0
  143. package/templates/common/Harness/tasks/_template/NAMING.md +16 -16
  144. package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
  145. package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
  146. package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
  147. package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
  148. package/templates/common/Harness/tasks/_template/STATE.json +6 -0
  149. package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
  150. package/templates/common/Harness/tasks/_template/NOTES.md +0 -3
@@ -1,20 +1,49 @@
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
 
20
+ let commandSurfaceLoadError = null;
21
+
22
+ function loadCommandSurface() {
23
+ const rel = path.join(root, 'Harness', 'specs', 'runtime', 'command-surface.json');
24
+ try {
25
+ const parsed = JSON.parse(fs.readFileSync(rel, 'utf8'));
26
+ if (!parsed || parsed.schemaVersion !== 1 || !Array.isArray(parsed.commands)) {
27
+ commandSurfaceLoadError = 'Harness/specs/runtime/command-surface.json must have schemaVersion 1 and commands[]';
28
+ return { commands: [] };
29
+ }
30
+ return parsed;
31
+ } catch (err) {
32
+ commandSurfaceLoadError = `Harness/specs/runtime/command-surface.json is not valid JSON: ${err.message}`;
33
+ return { commands: [] };
34
+ }
35
+ }
36
+
37
+ function uniqueSorted(values) {
38
+ return [...new Set(values)].sort();
39
+ }
40
+
41
+ const commandSurface = loadCommandSurface();
42
+ const commandDefinitions = commandSurface.commands;
43
+ const commandSkillNames = commandDefinitions
44
+ .filter(command => command.surfaces?.claudeSkill || command.surfaces?.codexSkill)
45
+ .map(command => command.id);
46
+
18
47
  const commonAgents = [
19
48
  'task-scribe',
20
49
  'codebase-explorer',
@@ -37,38 +66,26 @@ const commonAgents = [
37
66
  ];
38
67
 
39
68
  const commonSkills = [
40
- 'wf',
41
- 'wf-help',
42
69
  'tdd',
43
- 'wf-update',
44
- 'wf-max',
45
- 'wf-review',
46
- 'wf-learn',
47
- 'wf-browser',
48
70
  'subagent-orchestrator',
49
- 'wf-readme',
50
71
  'wf-agents-docs',
51
- 'wf-remove',
52
- 'wf-auto',
53
- 'wf-auto-spark',
72
+ ...commandSkillNames,
54
73
  ];
55
74
 
56
- const workflowCommands = [
57
- 'wf',
58
- 'wf-max',
59
- 'wf-auto',
60
- 'wf-auto-spark',
61
- 'wf-learn',
62
- 'wf-review',
63
- 'wf-browser',
64
- 'wf-readme',
65
- 'wf-remove',
66
- ];
75
+ const workflowCommands = commandDefinitions
76
+ .filter(command => command.classification === 'workflow' && command.surfaces?.claudeCommand)
77
+ .map(command => command.id);
67
78
 
68
- const opencodeWorkflowCommands = workflowCommands;
79
+ const directCommands = commandDefinitions
80
+ .filter(command => command.classification === 'direct')
81
+ .map(command => command.id);
82
+
83
+ const opencodeWorkflowCommands = commandDefinitions
84
+ .filter(command => command.classification === 'workflow' && command.surfaces?.opencodeCommand)
85
+ .map(command => command.id);
69
86
 
70
87
  const cacheDisciplinedSkills = commonSkills.filter(skill => (
71
- skill === 'subagent-orchestrator' || (skill.startsWith('wf') && skill !== 'wf-help')
88
+ skill === 'subagent-orchestrator' || skill === 'wf-agents-docs' || workflowCommands.includes(skill)
72
89
  ));
73
90
 
74
91
  const memoryFiles = [
@@ -103,12 +120,12 @@ const required = [
103
120
  '.codex/hooks.json',
104
121
  'opencode.json',
105
122
  '.claude/settings.json',
106
- '.claude/commands/wf-help.md',
107
- '.claude/commands/wf-update.md',
108
- ...workflowCommands.map(command => `.claude/commands/${command}.md`),
109
- '.opencode/commands/wf-help.md',
110
- '.opencode/commands/wf-update.md',
111
- ...opencodeWorkflowCommands.map(command => `.opencode/commands/${command}.md`),
123
+ ...commandDefinitions
124
+ .filter(command => command.surfaces?.claudeCommand)
125
+ .map(command => `.claude/commands/${command.id}.md`),
126
+ ...commandDefinitions
127
+ .filter(command => command.surfaces?.opencodeCommand)
128
+ .map(command => `.opencode/commands/${command.id}.md`),
112
129
  '.opencode/plugins/harness-wf-status.mjs',
113
130
  '.claude/rules/ecc/common.md',
114
131
  ...commonAgents.map(agent => `.claude/agents/${agent}.md`),
@@ -124,6 +141,7 @@ const required = [
124
141
  'Harness/specs/runtime/dispatch.md',
125
142
  'Harness/specs/guides/extension.md',
126
143
  'Harness/specs/runtime/context-loading.md',
144
+ 'Harness/specs/runtime/command-surface.json',
127
145
  'Harness/ownership.manifest.json',
128
146
  'Harness/specs/workflows/WF-KERNEL.md',
129
147
  'Harness/specs/runtime/agent-workflow.md',
@@ -134,8 +152,10 @@ const required = [
134
152
  'Harness/scripts/context-budget.mjs',
135
153
  'Harness/scripts/l2-cache-telemetry.mjs',
136
154
  'Harness/scripts/wf-update-check.mjs',
155
+ 'Harness/scripts/wf-update-runner.mjs',
137
156
  'Harness/scripts/wf-remove.mjs',
138
157
  'Harness/scripts/scan-clean.mjs',
158
+ 'Harness/scripts/sync-host-global.mjs',
139
159
  'Harness/scripts/task-state.mjs',
140
160
  'Harness/scripts/archive-tasks.mjs',
141
161
  'Harness/specs/workflows/WF-STATE.md',
@@ -204,6 +224,90 @@ function read(rel) {
204
224
  return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
205
225
  }
206
226
 
227
+ function readJson(rel) {
228
+ const body = read(rel);
229
+ if (!body) return null;
230
+ try {
231
+ return JSON.parse(body);
232
+ } catch {
233
+ return null;
234
+ }
235
+ }
236
+
237
+ function sameResolvedPath(a, b) {
238
+ if (!a || !b) return false;
239
+ const resolvedA = path.resolve(a);
240
+ const resolvedB = path.resolve(b);
241
+ return process.platform === 'win32'
242
+ ? resolvedA.toLowerCase() === resolvedB.toLowerCase()
243
+ : resolvedA === resolvedB;
244
+ }
245
+
246
+ function isGlobalRuntimeProjectStatePath(rel) {
247
+ return rel === 'Harness/PROGRESS.md'
248
+ || rel === 'Harness/tasks'
249
+ || rel.startsWith('Harness/tasks/')
250
+ || rel === 'Harness/research'
251
+ || rel.startsWith('Harness/research/')
252
+ || rel === 'Harness/project'
253
+ || rel.startsWith('Harness/project/');
254
+ }
255
+
256
+ function validateRequiredFiles(files, label = 'file') {
257
+ for (const rel of files) {
258
+ if (!fs.existsSync(path.join(root, rel))) {
259
+ errors.push(`missing required ${label}: ${rel}`);
260
+ }
261
+ }
262
+ }
263
+
264
+ function resolveMetadataPath(value) {
265
+ if (!value || typeof value !== 'string') return '';
266
+ return path.isAbsolute(value) ? value : path.resolve(root, value);
267
+ }
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
+
207
311
  function requireText(rel, text, label = text) {
208
312
  const body = read(rel);
209
313
  if (body && !body.includes(text)) errors.push(`${rel} missing ${label}`);
@@ -214,6 +318,177 @@ function forbidText(rel, text, label = text) {
214
318
  if (body && body.includes(text)) errors.push(`${rel} contains forbidden ${label}`);
215
319
  }
216
320
 
321
+ const installMetadata = readJson('Harness/.harness-version');
322
+ const isGlobalInstall = installMetadata?.installScope === 'global';
323
+ const isGlobalRuntimeRoot = isGlobalInstall && sameResolvedPath(root, installMetadata.globalDir);
324
+ const isGlobalProjectBridge = isGlobalInstall && !isGlobalRuntimeRoot;
325
+
326
+ function finishValidation() {
327
+ if (errors.length) {
328
+ console.error(`Harness validation failed${strict ? ' (strict)' : ''}:`);
329
+ for (const error of errors) console.error(`- ${error}`);
330
+ process.exit(1);
331
+ }
332
+
333
+ console.log(`Harness validation passed${strict ? ' (strict)' : ''}.`);
334
+ if (!strict) {
335
+ console.log('Tip: run `node Harness/scripts/validate-harness.mjs --strict` after bootstrap to check unresolved project placeholders.');
336
+ }
337
+ }
338
+
339
+ function validateHostGlobalTargets() {
340
+ if (!isGlobalInstall) return;
341
+
342
+ if (installMetadata?.copyMode !== 'copy') {
343
+ errors.push('Harness/.harness-version global install metadata must use copyMode "copy"');
344
+ }
345
+
346
+ const hostGlobal = installMetadata?.hostGlobal;
347
+ if (!hostGlobal || hostGlobal.copyMode !== 'copy' || !hostGlobal.targets || typeof hostGlobal.targets !== 'object') {
348
+ errors.push('Harness/.harness-version missing hostGlobal copy targets');
349
+ return;
350
+ }
351
+
352
+ const minimumFiles = {
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'],
356
+ };
357
+ const runtimeRoot = isGlobalRuntimeRoot ? root : resolveMetadataPath(installMetadata.globalDir);
358
+
359
+ for (const [host, requiredFiles] of Object.entries(minimumFiles)) {
360
+ const target = hostGlobal.targets[host];
361
+ if (!target || typeof target !== 'object') {
362
+ errors.push(`Harness/.harness-version missing hostGlobal target: ${host}`);
363
+ continue;
364
+ }
365
+
366
+ const hostRoot = resolveMetadataPath(target.root);
367
+ if (!hostRoot) {
368
+ errors.push(`Harness/.harness-version hostGlobal ${host} missing root`);
369
+ continue;
370
+ }
371
+
372
+ const files = Array.isArray(target.files) ? target.files : [];
373
+ for (const requiredFile of requiredFiles) {
374
+ if (!files.includes(requiredFile)) {
375
+ errors.push(`Harness/.harness-version hostGlobal ${host} missing required file target: ${requiredFile}`);
376
+ }
377
+ }
378
+
379
+ for (const file of files) {
380
+ if (typeof file !== 'string' || path.isAbsolute(file) || file.split('/').includes('..')) {
381
+ errors.push(`Harness/.harness-version hostGlobal ${host} has invalid relative file target: ${String(file)}`);
382
+ continue;
383
+ }
384
+ const filePath = path.join(hostRoot, ...file.split('/'));
385
+ let stat;
386
+ try {
387
+ stat = fs.lstatSync(filePath);
388
+ if (stat.isSymbolicLink()) {
389
+ errors.push(`host-global ${host} copied file is a symlink, not a real copy: ${file}`);
390
+ continue;
391
+ }
392
+ if (!stat.isFile()) {
393
+ errors.push(`host-global ${host} copied file is not a regular file: ${file}`);
394
+ continue;
395
+ }
396
+ } catch {
397
+ errors.push(`missing host-global ${host} copied file: ${file}`);
398
+ continue;
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
+ }
416
+ }
417
+ }
418
+ }
419
+
420
+ function validateGlobalRuntimeRoot() {
421
+ validateCommandSurface();
422
+ validateRequiredFiles(required.filter(rel => !isGlobalRuntimeProjectStatePath(rel)), 'global-runtime file');
423
+
424
+ for (const rel of ['Harness/PROGRESS.md', 'Harness/tasks', 'Harness/research', 'Harness/project']) {
425
+ if (fs.existsSync(path.join(root, rel))) {
426
+ errors.push(`global runtime must not contain project-local state: ${rel}`);
427
+ }
428
+ }
429
+
430
+ if (installMetadata?.projectState?.tasks !== 'Harness/tasks/') {
431
+ errors.push('Harness/.harness-version global runtime metadata must keep projectState.tasks project-local');
432
+ }
433
+ if (installMetadata?.projectState?.progress !== 'Harness/PROGRESS.md') {
434
+ errors.push('Harness/.harness-version global runtime metadata must keep projectState.progress project-local');
435
+ }
436
+ if (installMetadata?.settingsScopes?.precedence?.join('>') !== 'project>global') {
437
+ errors.push('Harness/.harness-version global runtime metadata must define project>global settings precedence');
438
+ }
439
+
440
+ validateHostGlobalTargets();
441
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project/Global Settings Boundary', 'project/global settings boundary section');
442
+ requireText('Harness/specs/guides/SETUP.md', 'copy Claude Code, Codex, and OpenCode command/skill surfaces', 'setup three-host global copy');
443
+
444
+ finishValidation();
445
+ process.exit(0);
446
+ }
447
+
448
+ function validateGlobalProjectBridge() {
449
+ const bridgeRequired = [
450
+ 'AGENTS.md',
451
+ 'CLAUDE.md',
452
+ 'Harness/README.md',
453
+ 'Harness/MEMORY.md',
454
+ 'Harness/settings.json',
455
+ 'Harness/specs/guides/SETUP.md',
456
+ 'Harness/.harness-version',
457
+ 'Harness/PROGRESS.md',
458
+ 'Harness/research/README.md',
459
+ 'Harness/research/PRD.md',
460
+ 'Harness/research/research-results.md',
461
+ 'Harness/project/architecture.md',
462
+ ...memoryFiles,
463
+ ];
464
+
465
+ for (const rel of bridgeRequired) {
466
+ if (!fs.existsSync(path.join(root, rel))) {
467
+ errors.push(`missing required global-project-bridge file: ${rel}`);
468
+ }
469
+ }
470
+
471
+ const taskTemplateDir = path.join(root, 'Harness', 'tasks', '_template');
472
+ if (!fs.existsSync(taskTemplateDir)) {
473
+ errors.push('missing directory: Harness/tasks/_template/');
474
+ } else {
475
+ for (const f of ['PROGRESS.md', 'PLAN.md']) {
476
+ if (!fs.existsSync(path.join(taskTemplateDir, f))) {
477
+ errors.push(`missing task template file: Harness/tasks/_template/${f}`);
478
+ }
479
+ }
480
+ }
481
+
482
+ requireText('CLAUDE.md', 'global Harness runtime', 'global runtime bridge pointer');
483
+ requireText('Harness/README.md', 'Never discover task context from the global runtime', 'project-local task authority');
484
+ requireText('Harness/specs/guides/SETUP.md', 'Read the full setup guide from', 'global setup bridge pointer');
485
+ requireText('Harness/MEMORY.md', 'Global memory lives under', 'global memory bridge pointer');
486
+ validateHostGlobalTargets();
487
+
488
+ finishValidation();
489
+ process.exit(0);
490
+ }
491
+
217
492
  function activeToml(rel) {
218
493
  return read(rel)
219
494
  .split(/\r?\n/)
@@ -254,7 +529,7 @@ function listMarkdownFiles(rel) {
254
529
  // Reserved: _template (system), auto (auto-mode capsule)
255
530
  const TASK_NAME_RE = /^task-[a-z]+(-[a-z0-9]+){1,4}$/;
256
531
  const TASK_NAME_MAX = 46; // "task-" (5) + ≤40 chars body + 1 safety = 46
257
- const TASK_RESERVED = new Set(['_template', 'auto', '_archive']);
532
+ const TASK_RESERVED = new Set(['_template', 'auto', '_archive', 'continuous']);
258
533
  const TASK_SAFE_ARCHIVE_STATUSES = new Set(['complete', 'verified', 'archived', 'abandoned', 'obsolete', 'done', 'closed', 'closeout']);
259
534
  const TASK_NEVER_ARCHIVE_STATUSES = new Set(['active', 'blocked', 'in_progress', 'running', 'pending', 'needs-user-decision']);
260
535
  const TASK_PHASE_ALIASES = new Map([
@@ -277,6 +552,13 @@ const TASK_VALID_PHASES = new Set([
277
552
  'archived', 'verified',
278
553
  ]);
279
554
  const TASK_VALID_STATUSES = new Set([...TASK_SAFE_ARCHIVE_STATUSES, ...TASK_NEVER_ARCHIVE_STATUSES, 'skipped', 'failed']);
555
+ const VALID_TASK_CAPSULE_POLICIES = new Set([
556
+ 'none',
557
+ 'required',
558
+ 'auto-capsule-required',
559
+ 'use-current-or-create-when-needed',
560
+ 'creates-or-updates',
561
+ ]);
280
562
 
281
563
  function validateTaskName(name, strict) {
282
564
  if (TASK_RESERVED.has(name)) return null;
@@ -385,7 +667,7 @@ function registeredWorkflowFiles(...texts) {
385
667
 
386
668
  function listedWorkflowCommands(...texts) {
387
669
  const commands = new Set();
388
- const pattern = /`\/(wf(?:-[a-z0-9]+)?)(?:\s+[^`]*)?`/g;
670
+ const pattern = /`\/(wf(?:-[a-z0-9]+)*)(?:\s+[^`]*)?`/g;
389
671
 
390
672
  for (const text of texts) {
391
673
  for (const match of text.matchAll(pattern)) {
@@ -398,33 +680,300 @@ function listedWorkflowCommands(...texts) {
398
680
  return [...commands].sort();
399
681
  }
400
682
 
401
- for (const rel of required) {
402
- if (!fs.existsSync(path.join(root, rel))) {
403
- errors.push(`missing required file: ${rel}`);
683
+ function extractStringArray(text, name) {
684
+ const match = text.match(new RegExp(`const ${name} = \\[([\\s\\S]*?)\\];`));
685
+ if (!match) return null;
686
+ return new Set([...match[1].matchAll(/'([^']+)'/g)].map(item => item[1]));
687
+ }
688
+
689
+ function expectedCommandAliases(id) {
690
+ return [`/${id}`, `$${id}`, `/skills ${id}`];
691
+ }
692
+
693
+ function validateCommandSurface() {
694
+ if (commandSurfaceLoadError) {
695
+ errors.push(commandSurfaceLoadError);
696
+ return;
697
+ }
698
+
699
+ const seen = new Set();
700
+ const claudeRouter = read('CLAUDE.md');
701
+ const readmeRouter = read('Harness/README.md');
702
+ const ecc = read('.claude/rules/ecc/common.md');
703
+ const eccExemptionLine = ecc.split(/\r?\n/).find(line => line.includes('excluding')) || '';
704
+ const claudeHelp = read('.claude/commands/wf-help.md');
705
+ const opencodeHelp = read('.opencode/commands/wf-help.md');
706
+ const removeScript = read('Harness/scripts/wf-remove.mjs');
707
+ const removeSkillRegistry = extractStringArray(removeScript, 'BUILT_IN_SKILL_NAMES');
708
+ const removeCommandRegistry = extractStringArray(removeScript, 'BUILT_IN_COMMAND_NAMES');
709
+ const cleanupDirs = extractStringArray(removeScript, 'CLEANUP_DIRS');
710
+
711
+ if (!removeSkillRegistry) errors.push('Harness/scripts/wf-remove.mjs missing BUILT_IN_SKILL_NAMES registry');
712
+ if (!removeCommandRegistry) errors.push('Harness/scripts/wf-remove.mjs missing BUILT_IN_COMMAND_NAMES registry');
713
+ if (!cleanupDirs) errors.push('Harness/scripts/wf-remove.mjs missing CLEANUP_DIRS registry');
714
+
715
+ for (const command of commandDefinitions) {
716
+ const id = command?.id;
717
+ const surfaces = command?.surfaces || {};
718
+ if (!id || !/^wf(?:-[a-z0-9]+)*$/.test(id)) {
719
+ errors.push(`command-surface has invalid command id: ${JSON.stringify(id)}`);
720
+ continue;
721
+ }
722
+ if (seen.has(id)) errors.push(`command-surface duplicate command id: ${id}`);
723
+ seen.add(id);
724
+
725
+ if (!['direct', 'workflow'].includes(command.classification)) {
726
+ errors.push(`command-surface ${id} has invalid classification: ${JSON.stringify(command.classification)}`);
727
+ }
728
+ if (command.entersWf !== (command.classification === 'workflow')) {
729
+ errors.push(`command-surface ${id} entersWf must match classification`);
730
+ }
731
+
732
+ for (const alias of expectedCommandAliases(id)) {
733
+ if (!Array.isArray(command.aliases) || !command.aliases.includes(alias)) {
734
+ errors.push(`command-surface ${id} missing alias ${alias}`);
735
+ }
736
+ }
737
+
738
+ const claudeCommand = `.claude/commands/${id}.md`;
739
+ const opencodeCommand = `.opencode/commands/${id}.md`;
740
+ const claudeSkill = `.claude/skills/${id}/SKILL.md`;
741
+ const codexSkill = `.agents/skills/${id}/SKILL.md`;
742
+
743
+ if (surfaces.claudeCommand && !fs.existsSync(path.join(root, claudeCommand))) {
744
+ errors.push(`command-surface ${id} missing Claude command: ${claudeCommand}`);
745
+ }
746
+ if (surfaces.opencodeCommand && !fs.existsSync(path.join(root, opencodeCommand))) {
747
+ errors.push(`command-surface ${id} missing OpenCode command: ${opencodeCommand}`);
748
+ }
749
+ if (surfaces.claudeSkill && !fs.existsSync(path.join(root, claudeSkill))) {
750
+ errors.push(`command-surface ${id} missing Claude skill: ${claudeSkill}`);
751
+ }
752
+ if (surfaces.codexSkill && !fs.existsSync(path.join(root, codexSkill))) {
753
+ errors.push(`command-surface ${id} missing Codex skill: ${codexSkill}`);
754
+ }
755
+ if (surfaces.helpRow) {
756
+ const rowMarker = `| \`/${id}`;
757
+ if (!claudeHelp.includes(rowMarker)) errors.push(`.claude/commands/wf-help.md missing command-surface help row for /${id}`);
758
+ if (!opencodeHelp.includes(rowMarker)) errors.push(`.opencode/commands/wf-help.md missing command-surface help row for /${id}`);
759
+ }
760
+
761
+ if (command.classification === 'direct') {
762
+ for (const alias of command.aliases || []) {
763
+ if (!claudeRouter.includes(`\`${alias}\``)) errors.push(`CLAUDE.md missing direct/compat alias ${alias}`);
764
+ if (!readmeRouter.includes(alias)) errors.push(`Harness/README.md missing direct/compat alias ${alias}`);
765
+ if (!eccExemptionLine.includes(`\`${alias}\``)) errors.push(`.claude/rules/ecc/common.md missing ECC direct command exemption ${alias}`);
766
+ }
767
+ for (const rel of [claudeCommand, opencodeCommand]) {
768
+ const text = read(rel);
769
+ if (text && id !== 'wf-help') {
770
+ if (!/direct command/i.test(text)) errors.push(`${rel} missing DIRECT command classification`);
771
+ if (!text.includes('Do not invoke a skill')) errors.push(`${rel} missing direct no-skill boundary`);
772
+ }
773
+ }
774
+ }
775
+
776
+ if (command.classification === 'workflow') {
777
+ for (const alias of command.aliases || []) {
778
+ if (eccExemptionLine.includes(`\`${alias}\``)) {
779
+ errors.push(`.claude/rules/ecc/common.md incorrectly exempts workflow command ${alias}`);
780
+ }
781
+ }
782
+ for (const rel of [claudeCommand, opencodeCommand]) {
783
+ const text = read(rel);
784
+ if (!text) continue;
785
+ if (!text.includes('workflow command')) errors.push(`${rel} missing workflow command classification`);
786
+ if (!text.includes('Harness/MEMORY.md')) errors.push(`${rel} missing workflow router load`);
787
+ }
788
+ }
789
+
790
+ if (['wf', 'wf-max', 'wf-command-create'].includes(id)) {
791
+ const text = `${read(claudeCommand)}\n${read(claudeSkill)}`;
792
+ if (!text.includes('task capsule')) errors.push(`${id} missing required task capsule instruction`);
793
+ if (!text.includes('task-<verb>-<noun>')) errors.push(`${id} missing task id convention`);
794
+ }
795
+
796
+ if (surfaces.claudeSkill || surfaces.codexSkill) {
797
+ if (removeSkillRegistry && !removeSkillRegistry.has(id)) {
798
+ errors.push(`Harness/scripts/wf-remove.mjs BUILT_IN_SKILL_NAMES missing ${id}`);
799
+ }
800
+ if (cleanupDirs) {
801
+ if (!cleanupDirs.has(`.claude/skills/${id}`)) errors.push(`Harness/scripts/wf-remove.mjs CLEANUP_DIRS missing .claude/skills/${id}`);
802
+ if (!cleanupDirs.has(`.agents/skills/${id}`)) errors.push(`Harness/scripts/wf-remove.mjs CLEANUP_DIRS missing .agents/skills/${id}`);
803
+ }
804
+ }
805
+ if ((surfaces.claudeCommand || surfaces.opencodeCommand) && removeCommandRegistry && !removeCommandRegistry.has(id)) {
806
+ errors.push(`Harness/scripts/wf-remove.mjs BUILT_IN_COMMAND_NAMES missing ${id}`);
807
+ }
404
808
  }
809
+
810
+ // Validate taskCapsulePolicy enum
811
+ for (const cmd of commandDefinitions) {
812
+ if (!cmd.taskCapsulePolicy || !VALID_TASK_CAPSULE_POLICIES.has(cmd.taskCapsulePolicy)) {
813
+ errors.push(`command-surface ${cmd.id}: invalid taskCapsulePolicy "${cmd.taskCapsulePolicy}". Valid: ${[...VALID_TASK_CAPSULE_POLICIES].join(', ')}`);
814
+ }
815
+ }
816
+ }
817
+
818
+ if (isGlobalRuntimeRoot) {
819
+ validateGlobalRuntimeRoot();
405
820
  }
406
821
 
822
+ if (isGlobalProjectBridge) {
823
+ validateGlobalProjectBridge();
824
+ }
825
+
826
+ validateCommandSurface();
827
+
828
+ validateRequiredFiles(required);
829
+
407
830
  for (const rel of legacyRootSpecDocs) {
408
831
  if (fs.existsSync(path.join(root, rel))) {
409
832
  errors.push(`legacy root Harness spec doc should be migrated to Harness/specs/**: ${rel}`);
410
833
  }
411
834
  }
412
835
 
413
- const manifestText = read('Harness/ownership.manifest.json');
414
- 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;
415
844
  try {
416
- const manifest = JSON.parse(manifestText);
417
- for (const entry of manifest.frameworkOwned || []) {
418
- if (!entry || typeof entry.path !== 'string') continue;
419
- if (!fs.existsSync(path.join(root, ...entry.path.split('/')))) {
420
- errors.push(`ownership manifest frameworkOwned file missing: ${entry.path}`);
421
- }
422
- }
845
+ manifest = JSON.parse(manifestText);
423
846
  } catch (err) {
424
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
+ }
425
952
  }
426
953
  }
427
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
+
428
977
  const removedHookArtifacts = [
429
978
  'Harness/HOOK_PROTOCOL.md',
430
979
  'Harness/scripts/wf-mode-hook.mjs',
@@ -526,6 +1075,14 @@ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Memory Candidate Dete
526
1075
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'explicit user preference', 'explicit user preference immediate write rule');
527
1076
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Memory Routing (L3)', 'memory routing section');
528
1077
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Scenario pack', 'route scoring scenario pack');
1078
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project/Global Memory Boundary', 'project/global memory boundary section');
1079
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Harness/tasks/` and `Harness/PROGRESS.md` are always project-local', 'global install keeps task state project-local');
1080
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project memory lives in `Harness/memory/`', 'project memory scope rule');
1081
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Global memory must never store project task state', 'global memory task-state exclusion');
1082
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project/Global Settings Boundary', 'project/global settings boundary section');
1083
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project settings live in `Harness/settings.json` and override global settings', 'project settings override global settings');
1084
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'copied, not symlinked, into Claude Code, Codex, and OpenCode', 'host-global copy mode');
1085
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Existing user-authored config, command, skill, or agent files are user-owned', 'user-owned host file rule');
529
1086
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'same tool or command pattern fails 3+ times', 'tool reflection trigger');
530
1087
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'user corrects the same assumption or preference 2+ times', 'user correction reflection trigger');
531
1088
  requireText('CLAUDE.md', 'startup-hints.md', 'CLAUDE startup-hints routing');
@@ -556,7 +1113,7 @@ requireText('CLAUDE.md', 'Keep intermediate user updates to 1-2 short sentences'
556
1113
  requireText('.claude/rules/ecc/common.md', '## Low-Noise Progress', 'ECC low-noise progress section');
557
1114
  requireText('.claude/rules/ecc/common.md', "Match the user's language for user-facing prose", 'ECC user-facing language match rule');
558
1115
  requireText('.claude/rules/ecc/common.md', 'Do not recap plans, paste logs, or narrate obvious file reads', 'ECC low-noise no-recap rule');
559
- requireText('.claude/rules/ecc/common.md', 'excluding `/wf-help`, `$wf-help`, `/skills wf-help`, `/wf-update`, `$wf-update`, and `/skills wf-update`', 'ECC direct command exemption');
1116
+ requireText('Harness/specs/runtime/command-surface.json', '"wf-command-create"', 'command surface registry wf-command-create entry');
560
1117
  requireText('Harness/README.md', 'Load By Task', 'Harness task router');
561
1118
  requireText('Harness/README.md', 'Need context/cache/token efficiency', 'cache/token router row');
562
1119
  requireText('Harness/specs/runtime/context-loading.md', 'Context Tiers', 'context tier load budget section');
@@ -578,6 +1135,12 @@ requireText('Harness/specs/runtime/subagents.md', 'Cache-first discipline', 'sub
578
1135
  forbidText('CLAUDE.md', 'Harness/specs/guides/SETUP.md', 'CLAUDE.md SETUP reference');
579
1136
  forbidText('CLAUDE.md', 'follow it before normal project work', 'installed-project SETUP hot-path routing');
580
1137
  requireText('Harness/specs/guides/SETUP.md', 'Harness/specs/protocols/MEMORY_PROTOCOL.md', 'setup memory protocol reference');
1138
+ requireText('Harness/specs/guides/SETUP.md', '--install-scope project', 'setup project install scope');
1139
+ requireText('Harness/specs/guides/SETUP.md', '--install-scope global', 'setup global install scope');
1140
+ requireText('Harness/specs/guides/SETUP.md', '--global-dir <dir>', 'setup global dir flag');
1141
+ requireText('Harness/specs/guides/SETUP.md', '--host-global-dir <dir>', 'setup host-global dir flag');
1142
+ requireText('Harness/specs/guides/SETUP.md', 'copy Claude Code, Codex, and OpenCode command/skill surfaces', 'setup three-host global copy');
1143
+ requireText('Harness/specs/guides/SETUP.md', 'Project settings in `Harness/settings.json` override global settings defaults', 'setup settings precedence');
581
1144
  requireText('Harness/specs/guides/SETUP.md', 'no startup dependency on this setup reference', 'SETUP startup boundary');
582
1145
  forbidText('Harness/specs/guides/SETUP.md', 'bootstrap contract line', 'stale SETUP-to-CLAUDE bootstrap contract');
583
1146
  forbidText('Harness/specs/runtime/context-loading.md', 'Always keep:', 'ambiguous always-load context rule');
@@ -957,8 +1520,8 @@ requireText('Harness/specs/workflows/WF-AUTO.md', 'Inherited WF/WF-MAX Constrain
957
1520
  requireText('Harness/specs/workflows/WF-AUTO.md', 'Mini PRD -> AC IDs -> test/validation plan -> implementer -> verifier', 'wf-auto per-cycle WF chain');
958
1521
  requireText('Harness/specs/workflows/WF-AUTO.md', 'reflector PASS', 'wf-auto reflector gate');
959
1522
  requireText('Harness/specs/workflows/WF-AUTO.md', 'Manual or benchmark-driven single-cycle', 'wf-auto bounded single-cycle tick contract');
960
- requireText('Harness/specs/workflows/WF-AUTO.md', 'Harness/tasks/auto/PLAN.md', 'wf-auto bounded tick PLAN record');
961
- 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');
962
1525
  requireText('Harness/specs/workflows/WF-AUTO-SPARK.md', 'Inherited Execution Chain', 'wf-auto-spark inherited execution chain');
963
1526
  requireText('Harness/specs/workflows/WF-AUTO-SPARK.md', 'External spark search replaces discovery only', 'wf-auto-spark discovery-only inheritance');
964
1527
  requireText('Harness/specs/workflows/WF-AUTO-SPARK.md', 'reflector PASS', 'wf-auto-spark reflector gate');
@@ -1090,8 +1653,8 @@ for (const skill of cacheDisciplinedSkills) {
1090
1653
  }
1091
1654
  requireText('.claude/skills/wf-auto/SKILL.md', 'bounded test tick', 'wf-auto skill bounded tick auto capsule rule');
1092
1655
  requireText('.claude/skills/wf-auto/SKILL.md', 'missing auto capsule evidence', 'wf-auto skill missing auto capsule failure rule');
1093
- requireText('.claude/skills/wf-auto/SKILL.md', 'Harness/tasks/auto/PLAN.md', 'wf-auto skill PLAN record');
1094
- 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');
1095
1658
  requireText('.claude/skills/wf-auto/SKILL.md', 'evidence ledger path/summary', 'wf-auto skill return evidence ledger path');
1096
1659
  if (fs.existsSync(path.join(root, '.claude/skills/wf-browser/SKILL.md'))) {
1097
1660
  requireText('.claude/skills/wf-browser/SKILL.md', 'Cache Discipline', 'wf-browser skill cache discipline');
@@ -1111,9 +1674,11 @@ if (fs.existsSync(path.join(root, '.claude/skills/wf-browser/SKILL.md'))) {
1111
1674
  // Direct command checks
1112
1675
  requireText('Harness/README.md', '/wf-update', 'wf-update direct command reference');
1113
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');
1114
1678
  for (const rel of ['.claude/commands/wf-update.md', '.opencode/commands/wf-update.md']) {
1115
1679
  requireText(rel, '## Cache Discipline', `${rel} cache discipline`);
1116
1680
  requireText(rel, 'agent.safeApplyCommand', `${rel} safe apply step`);
1681
+ requireText(rel, 'wf-update-runner.mjs', `${rel} multi-scope update runner`);
1117
1682
  requireText(rel, '--apply-safe', `${rel} apply-safe command`);
1118
1683
  requireText(rel, 'agent.aiMergeRequired', `${rel} AI merge step`);
1119
1684
  requireText(rel, '--accept-local', `${rel} accept-local decision`);
@@ -1121,16 +1686,35 @@ for (const rel of ['.claude/commands/wf-update.md', '.opencode/commands/wf-updat
1121
1686
  requireText(rel, '--accept-template', `${rel} accept-template decision`);
1122
1687
  requireText(rel, '--finalize', `${rel} finalize command`);
1123
1688
  requireText(rel, 'strict `--apply` only when', `${rel} strict apply boundary`);
1689
+ requireText(rel, 'sync-host-global.mjs', `${rel} host-global sync step`);
1124
1690
  requireText(rel, '## Return', `${rel} return contract`);
1125
1691
  requireText(rel, 'agent.releaseHighlights', `${rel} release highlights summary`);
1126
1692
  requireText(rel, 'releaseNotes.highlights', `${rel} release notes fallback`);
1127
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
+ }
1128
1710
  requireText('Harness/scripts/wf-update-check.mjs', 'releaseHighlights', 'wf-update-check release highlights metadata');
1129
1711
  requireText('Harness/scripts/wf-update-check.mjs', 'updateReportRequired', 'wf-update-check user update report requirement');
1130
1712
  requireText('.claude/commands/wf-help.md', 'direct command', 'wf-help wf-update direct command classification');
1131
1713
  requireText('.claude/commands/wf-help.md', '$wf-help', 'wf-help Codex compatibility usage');
1132
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');
1133
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');
1134
1718
 
1135
1719
  // wf-update skill must NOT claim Claude Code /wf-update as a skill invocation
1136
1720
  // (allow mentions in the description/body that say "direct command" — those are correct)
@@ -1290,18 +1874,9 @@ if (activeStateTasks.length > 1) {
1290
1874
  const OUTER_TASK_CAP = 5;
1291
1875
  const outerTasks = taskDirs.filter(name => !TASK_RESERVED.has(name) && !name.startsWith('_'));
1292
1876
  if (outerTasks.length > OUTER_TASK_CAP) {
1293
- const capMsg = `Harness/tasks/ has ${outerTasks.length} outer task capsules (cap ${OUTER_TASK_CAP}); archive completed tasks with node Harness/scripts/task-state.mjs archive --apply (compat: node Harness/scripts/archive-tasks.mjs --apply; see Harness/specs/protocols/TASK_ARCHIVE.md)`;
1877
+ const capMsg = `Harness/tasks/ has ${outerTasks.length} outer task capsules (cap ${OUTER_TASK_CAP}); remind the user to run $wf-task-archive when they want to archive completed tasks (apply mode maps to node Harness/scripts/task-state.mjs archive --apply)`;
1294
1878
  if (strict) errors.push(capMsg);
1295
1879
  else console.warn(`Warning: ${capMsg}`);
1296
1880
  }
1297
1881
 
1298
- if (errors.length) {
1299
- console.error(`Harness validation failed${strict ? ' (strict)' : ''}:`);
1300
- for (const error of errors) console.error(`- ${error}`);
1301
- process.exit(1);
1302
- }
1303
-
1304
- console.log(`Harness validation passed${strict ? ' (strict)' : ''}.`);
1305
- if (!strict) {
1306
- console.log('Tip: run `node Harness/scripts/validate-harness.mjs --strict` after bootstrap to check unresolved project placeholders.');
1307
- }
1882
+ finishValidation();