create-harness-vibe-coding 0.8.16 → 0.8.18

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 (70) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README-CN.md +100 -127
  3. package/README.md +63 -42
  4. package/package.json +1 -1
  5. package/src/generator.js +628 -121
  6. package/src/index.js +174 -43
  7. package/src/prompts.js +18 -0
  8. package/templates/common/.claude/commands/wf-auto-spark.md +16 -0
  9. package/templates/common/.claude/commands/wf-auto.md +16 -0
  10. package/templates/{optional/skills/browser-e2e/.opencode → common/.claude}/commands/wf-browser.md +1 -1
  11. package/templates/common/.claude/commands/wf-command-create.md +58 -0
  12. package/templates/common/.claude/commands/wf-help.md +13 -6
  13. package/templates/common/.claude/commands/wf-learn.md +16 -0
  14. package/templates/common/.claude/commands/wf-max.md +20 -0
  15. package/templates/common/.claude/commands/wf-readme.md +16 -0
  16. package/templates/common/.claude/commands/wf-remove.md +16 -0
  17. package/templates/common/.claude/commands/wf-review.md +16 -0
  18. package/templates/common/.claude/commands/wf-task-archive.md +26 -0
  19. package/templates/common/.claude/commands/wf-task-list.md +24 -0
  20. package/templates/common/.claude/commands/wf-task-record.md +24 -0
  21. package/templates/common/.claude/commands/wf.md +16 -0
  22. package/templates/common/.claude/rules/ecc/common.md +1 -1
  23. package/templates/common/.claude/skills/wf-agents-docs/SKILL.md +15 -30
  24. package/templates/common/.claude/skills/wf-browser/SKILL.md +176 -0
  25. package/templates/common/.claude/skills/wf-command-create/SKILL.md +37 -0
  26. package/templates/common/.claude/skills/wf-help/SKILL.md +30 -0
  27. package/templates/common/.claude/skills/wf-max/SKILL.md +22 -8
  28. package/templates/common/.claude/skills/wf-review/SKILL.md +29 -2
  29. package/templates/common/.claude/skills/wf-task-archive/SKILL.md +28 -0
  30. package/templates/common/.claude/skills/wf-task-list/SKILL.md +28 -0
  31. package/templates/common/.claude/skills/wf-task-record/SKILL.md +28 -0
  32. package/templates/common/.codex/config.toml +3 -5
  33. package/templates/common/.harness-version +108 -41
  34. package/templates/common/.opencode/agents/architect-manager.md +7 -1
  35. package/templates/common/.opencode/agents/explore-manager.md +10 -1
  36. package/templates/common/.opencode/agents/implement-manager.md +5 -3
  37. package/templates/common/.opencode/agents/review-manager.md +5 -2
  38. package/templates/common/.opencode/commands/wf-browser.md +16 -0
  39. package/templates/common/.opencode/commands/wf-command-create.md +61 -0
  40. package/templates/common/.opencode/commands/wf-help.md +13 -6
  41. package/templates/common/.opencode/commands/wf-max.md +12 -7
  42. package/templates/common/.opencode/commands/wf-task-archive.md +29 -0
  43. package/templates/common/.opencode/commands/wf-task-list.md +27 -0
  44. package/templates/common/.opencode/commands/wf-task-record.md +27 -0
  45. package/templates/common/CLAUDE.md +9 -7
  46. package/templates/common/Harness/MEMORY.md +12 -3
  47. package/templates/common/Harness/README.md +23 -41
  48. package/templates/common/Harness/ownership.manifest.json +162 -14
  49. package/templates/common/Harness/scripts/archive-tasks.mjs +12 -220
  50. package/templates/common/Harness/scripts/scan-clean.mjs +3 -9
  51. package/templates/common/Harness/scripts/task-state.mjs +1279 -0
  52. package/templates/common/Harness/scripts/validate-harness.mjs +635 -65
  53. package/templates/common/Harness/scripts/wf-remove.mjs +37 -5
  54. package/templates/common/Harness/scripts/wf-update-check.mjs +3 -0
  55. package/templates/common/Harness/specs/guides/SETUP.md +10 -2
  56. package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +15 -0
  57. package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +49 -23
  58. package/templates/common/Harness/specs/runtime/command-surface.json +215 -0
  59. package/templates/common/Harness/specs/runtime/dispatch.md +2 -2
  60. package/templates/common/Harness/specs/runtime/subagents.md +15 -5
  61. package/templates/common/Harness/specs/workflows/WF-AUTO.md +6 -6
  62. package/templates/common/Harness/specs/workflows/WF-KERNEL.md +1 -1
  63. package/templates/common/Harness/specs/workflows/WF-MAX.md +35 -2
  64. package/templates/common/Harness/specs/workflows/WF-STATE.md +155 -36
  65. package/templates/common/Harness/tasks/_template/STATE.json +6 -0
  66. package/templates/common/opencode.json +1 -0
  67. package/templates/optional/catalog.json +2 -9
  68. package/templates/optional/skills/browser-e2e/.claude/skills/browser-e2e/SKILL.md +0 -42
  69. package/templates/optional/skills/browser-e2e/.claude/skills/wf-browser/SKILL.md +0 -201
  70. package/templates/optional/skills/browser-e2e/Harness/workflows/browser-e2e.md +0 -119
@@ -15,6 +15,33 @@ Literal explanatory {{...}} text is allowed.`);
15
15
  process.exit(0);
16
16
  }
17
17
 
18
+ let commandSurfaceLoadError = null;
19
+
20
+ function loadCommandSurface() {
21
+ const rel = path.join(root, 'Harness', 'specs', 'runtime', 'command-surface.json');
22
+ try {
23
+ const parsed = JSON.parse(fs.readFileSync(rel, 'utf8'));
24
+ if (!parsed || parsed.schemaVersion !== 1 || !Array.isArray(parsed.commands)) {
25
+ commandSurfaceLoadError = 'Harness/specs/runtime/command-surface.json must have schemaVersion 1 and commands[]';
26
+ return { commands: [] };
27
+ }
28
+ return parsed;
29
+ } catch (err) {
30
+ commandSurfaceLoadError = `Harness/specs/runtime/command-surface.json is not valid JSON: ${err.message}`;
31
+ return { commands: [] };
32
+ }
33
+ }
34
+
35
+ function uniqueSorted(values) {
36
+ return [...new Set(values)].sort();
37
+ }
38
+
39
+ const commandSurface = loadCommandSurface();
40
+ const commandDefinitions = commandSurface.commands;
41
+ const commandSkillNames = commandDefinitions
42
+ .filter(command => command.surfaces?.claudeSkill || command.surfaces?.codexSkill)
43
+ .map(command => command.id);
44
+
18
45
  const commonAgents = [
19
46
  'task-scribe',
20
47
  'codebase-explorer',
@@ -37,33 +64,26 @@ const commonAgents = [
37
64
  ];
38
65
 
39
66
  const commonSkills = [
40
- 'wf',
41
67
  'tdd',
42
- 'wf-update',
43
- 'wf-max',
44
- 'wf-review',
45
- 'wf-learn',
46
68
  'subagent-orchestrator',
47
- 'wf-readme',
48
69
  'wf-agents-docs',
49
- 'wf-remove',
50
- 'wf-auto',
51
- 'wf-auto-spark',
70
+ ...commandSkillNames,
52
71
  ];
53
72
 
54
- const opencodeWorkflowCommands = [
55
- 'wf',
56
- 'wf-max',
57
- 'wf-auto',
58
- 'wf-auto-spark',
59
- 'wf-learn',
60
- 'wf-review',
61
- 'wf-readme',
62
- 'wf-remove',
63
- ];
73
+ const workflowCommands = commandDefinitions
74
+ .filter(command => command.classification === 'workflow' && command.surfaces?.claudeCommand)
75
+ .map(command => command.id);
76
+
77
+ const directCommands = commandDefinitions
78
+ .filter(command => command.classification === 'direct')
79
+ .map(command => command.id);
80
+
81
+ const opencodeWorkflowCommands = commandDefinitions
82
+ .filter(command => command.classification === 'workflow' && command.surfaces?.opencodeCommand)
83
+ .map(command => command.id);
64
84
 
65
85
  const cacheDisciplinedSkills = commonSkills.filter(skill => (
66
- skill === 'subagent-orchestrator' || skill.startsWith('wf')
86
+ skill === 'subagent-orchestrator' || skill === 'wf-agents-docs' || workflowCommands.includes(skill)
67
87
  ));
68
88
 
69
89
  const memoryFiles = [
@@ -95,13 +115,15 @@ const required = [
95
115
  'Harness/templates/VALIDATION_REPORT.template.md',
96
116
  ...memoryFiles,
97
117
  '.codex/config.toml',
118
+ '.codex/hooks.json',
98
119
  'opencode.json',
99
120
  '.claude/settings.json',
100
- '.claude/commands/wf-help.md',
101
- '.claude/commands/wf-update.md',
102
- '.opencode/commands/wf-help.md',
103
- '.opencode/commands/wf-update.md',
104
- ...opencodeWorkflowCommands.map(command => `.opencode/commands/${command}.md`),
121
+ ...commandDefinitions
122
+ .filter(command => command.surfaces?.claudeCommand)
123
+ .map(command => `.claude/commands/${command.id}.md`),
124
+ ...commandDefinitions
125
+ .filter(command => command.surfaces?.opencodeCommand)
126
+ .map(command => `.opencode/commands/${command.id}.md`),
105
127
  '.opencode/plugins/harness-wf-status.mjs',
106
128
  '.claude/rules/ecc/common.md',
107
129
  ...commonAgents.map(agent => `.claude/agents/${agent}.md`),
@@ -117,6 +139,7 @@ const required = [
117
139
  'Harness/specs/runtime/dispatch.md',
118
140
  'Harness/specs/guides/extension.md',
119
141
  'Harness/specs/runtime/context-loading.md',
142
+ 'Harness/specs/runtime/command-surface.json',
120
143
  'Harness/ownership.manifest.json',
121
144
  'Harness/specs/workflows/WF-KERNEL.md',
122
145
  'Harness/specs/runtime/agent-workflow.md',
@@ -129,6 +152,7 @@ const required = [
129
152
  'Harness/scripts/wf-update-check.mjs',
130
153
  'Harness/scripts/wf-remove.mjs',
131
154
  'Harness/scripts/scan-clean.mjs',
155
+ 'Harness/scripts/task-state.mjs',
132
156
  'Harness/scripts/archive-tasks.mjs',
133
157
  'Harness/specs/workflows/WF-STATE.md',
134
158
  'Harness/specs/protocols/TASK_ARCHIVE.md',
@@ -196,6 +220,48 @@ function read(rel) {
196
220
  return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
197
221
  }
198
222
 
223
+ function readJson(rel) {
224
+ const body = read(rel);
225
+ if (!body) return null;
226
+ try {
227
+ return JSON.parse(body);
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+
233
+ function sameResolvedPath(a, b) {
234
+ if (!a || !b) return false;
235
+ const resolvedA = path.resolve(a);
236
+ const resolvedB = path.resolve(b);
237
+ return process.platform === 'win32'
238
+ ? resolvedA.toLowerCase() === resolvedB.toLowerCase()
239
+ : resolvedA === resolvedB;
240
+ }
241
+
242
+ function isGlobalRuntimeProjectStatePath(rel) {
243
+ return rel === 'Harness/PROGRESS.md'
244
+ || rel === 'Harness/tasks'
245
+ || rel.startsWith('Harness/tasks/')
246
+ || rel === 'Harness/research'
247
+ || rel.startsWith('Harness/research/')
248
+ || rel === 'Harness/project'
249
+ || rel.startsWith('Harness/project/');
250
+ }
251
+
252
+ function validateRequiredFiles(files, label = 'file') {
253
+ for (const rel of files) {
254
+ if (!fs.existsSync(path.join(root, rel))) {
255
+ errors.push(`missing required ${label}: ${rel}`);
256
+ }
257
+ }
258
+ }
259
+
260
+ function resolveMetadataPath(value) {
261
+ if (!value || typeof value !== 'string') return '';
262
+ return path.isAbsolute(value) ? value : path.resolve(root, value);
263
+ }
264
+
199
265
  function requireText(rel, text, label = text) {
200
266
  const body = read(rel);
201
267
  if (body && !body.includes(text)) errors.push(`${rel} missing ${label}`);
@@ -206,6 +272,173 @@ function forbidText(rel, text, label = text) {
206
272
  if (body && body.includes(text)) errors.push(`${rel} contains forbidden ${label}`);
207
273
  }
208
274
 
275
+ const installMetadata = readJson('Harness/.harness-version');
276
+ const isGlobalInstall = installMetadata?.installScope === 'global';
277
+ const isGlobalRuntimeRoot = isGlobalInstall && sameResolvedPath(root, installMetadata.globalDir);
278
+ const isGlobalProjectBridge = isGlobalInstall && !isGlobalRuntimeRoot;
279
+
280
+ function finishValidation() {
281
+ if (errors.length) {
282
+ console.error(`Harness validation failed${strict ? ' (strict)' : ''}:`);
283
+ for (const error of errors) console.error(`- ${error}`);
284
+ process.exit(1);
285
+ }
286
+
287
+ console.log(`Harness validation passed${strict ? ' (strict)' : ''}.`);
288
+ if (!strict) {
289
+ console.log('Tip: run `node Harness/scripts/validate-harness.mjs --strict` after bootstrap to check unresolved project placeholders.');
290
+ }
291
+ }
292
+
293
+ function validateHostGlobalTargets() {
294
+ if (!isGlobalInstall) return;
295
+
296
+ if (installMetadata?.copyMode !== 'copy') {
297
+ errors.push('Harness/.harness-version global install metadata must use copyMode "copy"');
298
+ }
299
+
300
+ const hostGlobal = installMetadata?.hostGlobal;
301
+ if (!hostGlobal || hostGlobal.copyMode !== 'copy' || !hostGlobal.targets || typeof hostGlobal.targets !== 'object') {
302
+ errors.push('Harness/.harness-version missing hostGlobal copy targets');
303
+ return;
304
+ }
305
+
306
+ const minimumFiles = {
307
+ claude: ['commands/wf.md', 'skills/wf/SKILL.md'],
308
+ codex: ['skills/wf/SKILL.md'],
309
+ opencode: ['commands/wf.md'],
310
+ };
311
+
312
+ for (const [host, requiredFiles] of Object.entries(minimumFiles)) {
313
+ const target = hostGlobal.targets[host];
314
+ if (!target || typeof target !== 'object') {
315
+ errors.push(`Harness/.harness-version missing hostGlobal target: ${host}`);
316
+ continue;
317
+ }
318
+
319
+ const hostRoot = resolveMetadataPath(target.root);
320
+ if (!hostRoot) {
321
+ errors.push(`Harness/.harness-version hostGlobal ${host} missing root`);
322
+ continue;
323
+ }
324
+
325
+ const files = Array.isArray(target.files) ? target.files : [];
326
+ for (const requiredFile of requiredFiles) {
327
+ if (!files.includes(requiredFile)) {
328
+ errors.push(`Harness/.harness-version hostGlobal ${host} missing required file target: ${requiredFile}`);
329
+ }
330
+ }
331
+
332
+ for (const file of files) {
333
+ if (typeof file !== 'string' || path.isAbsolute(file) || file.split('/').includes('..')) {
334
+ errors.push(`Harness/.harness-version hostGlobal ${host} has invalid relative file target: ${String(file)}`);
335
+ continue;
336
+ }
337
+ const filePath = path.join(hostRoot, ...file.split('/'));
338
+ let stat;
339
+ try {
340
+ stat = fs.lstatSync(filePath);
341
+ if (stat.isSymbolicLink()) {
342
+ errors.push(`host-global ${host} copied file is a symlink, not a real copy: ${file}`);
343
+ continue;
344
+ }
345
+ if (!stat.isFile()) {
346
+ errors.push(`host-global ${host} copied file is not a regular file: ${file}`);
347
+ continue;
348
+ }
349
+ } catch {
350
+ errors.push(`missing host-global ${host} copied file: ${file}`);
351
+ continue;
352
+ }
353
+ }
354
+ }
355
+ }
356
+
357
+ function validateGlobalRuntimeRoot() {
358
+ validateCommandSurface();
359
+ validateRequiredFiles(required.filter(rel => !isGlobalRuntimeProjectStatePath(rel)), 'global-runtime file');
360
+
361
+ for (const rel of ['Harness/PROGRESS.md', 'Harness/tasks', 'Harness/research', 'Harness/project']) {
362
+ if (fs.existsSync(path.join(root, rel))) {
363
+ errors.push(`global runtime must not contain project-local state: ${rel}`);
364
+ }
365
+ }
366
+
367
+ if (installMetadata?.projectState?.tasks !== 'Harness/tasks/') {
368
+ errors.push('Harness/.harness-version global runtime metadata must keep projectState.tasks project-local');
369
+ }
370
+ if (installMetadata?.projectState?.progress !== 'Harness/PROGRESS.md') {
371
+ errors.push('Harness/.harness-version global runtime metadata must keep projectState.progress project-local');
372
+ }
373
+ if (installMetadata?.settingsScopes?.precedence?.join('>') !== 'project>global') {
374
+ errors.push('Harness/.harness-version global runtime metadata must define project>global settings precedence');
375
+ }
376
+
377
+ validateHostGlobalTargets();
378
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project/Global Settings Boundary', 'project/global settings boundary section');
379
+ requireText('Harness/specs/guides/SETUP.md', 'copy Claude Code, Codex, and OpenCode command/skill surfaces', 'setup three-host global copy');
380
+
381
+ finishValidation();
382
+ process.exit(0);
383
+ }
384
+
385
+ function validateGlobalProjectBridge() {
386
+ const bridgeRequired = [
387
+ 'AGENTS.md',
388
+ 'CLAUDE.md',
389
+ 'Harness/README.md',
390
+ 'Harness/MEMORY.md',
391
+ 'Harness/settings.json',
392
+ 'Harness/specs/guides/SETUP.md',
393
+ 'Harness/.harness-version',
394
+ 'Harness/PROGRESS.md',
395
+ 'Harness/research/README.md',
396
+ 'Harness/research/PRD.md',
397
+ 'Harness/research/research-results.md',
398
+ 'Harness/project/architecture.md',
399
+ ...memoryFiles,
400
+ ];
401
+
402
+ for (const rel of bridgeRequired) {
403
+ if (!fs.existsSync(path.join(root, rel))) {
404
+ errors.push(`missing required global-project-bridge file: ${rel}`);
405
+ }
406
+ }
407
+
408
+ const taskTemplateDir = path.join(root, 'Harness', 'tasks', '_template');
409
+ if (!fs.existsSync(taskTemplateDir)) {
410
+ errors.push('missing directory: Harness/tasks/_template/');
411
+ } else {
412
+ for (const f of ['PROGRESS.md', 'PLAN.md']) {
413
+ if (!fs.existsSync(path.join(taskTemplateDir, f))) {
414
+ errors.push(`missing task template file: Harness/tasks/_template/${f}`);
415
+ }
416
+ }
417
+ }
418
+
419
+ requireText('CLAUDE.md', 'global Harness runtime', 'global runtime bridge pointer');
420
+ requireText('Harness/README.md', 'Never discover task context from the global runtime', 'project-local task authority');
421
+ requireText('Harness/specs/guides/SETUP.md', 'Read the full setup guide from', 'global setup bridge pointer');
422
+ requireText('Harness/MEMORY.md', 'Global memory lives under', 'global memory bridge pointer');
423
+ validateHostGlobalTargets();
424
+
425
+ finishValidation();
426
+ process.exit(0);
427
+ }
428
+
429
+ function activeToml(rel) {
430
+ return read(rel)
431
+ .split(/\r?\n/)
432
+ .map(line => line.trim())
433
+ .filter(line => line && !line.startsWith('#'))
434
+ .join('\n');
435
+ }
436
+
437
+ function forbidActiveToml(rel, pattern, label) {
438
+ const body = activeToml(rel);
439
+ if (body && pattern.test(body)) errors.push(`${rel} contains active forbidden ${label}`);
440
+ }
441
+
209
442
  function frontmatterField(text, field) {
210
443
  const match = text.match(new RegExp(`^${field}:\\s*(.+)$`, 'm'));
211
444
  return match ? match[1].trim() : '';
@@ -234,6 +467,35 @@ function listMarkdownFiles(rel) {
234
467
  const TASK_NAME_RE = /^task-[a-z]+(-[a-z0-9]+){1,4}$/;
235
468
  const TASK_NAME_MAX = 46; // "task-" (5) + ≤40 chars body + 1 safety = 46
236
469
  const TASK_RESERVED = new Set(['_template', 'auto', '_archive']);
470
+ const TASK_SAFE_ARCHIVE_STATUSES = new Set(['complete', 'verified', 'archived', 'abandoned', 'obsolete', 'done', 'closed', 'closeout']);
471
+ const TASK_NEVER_ARCHIVE_STATUSES = new Set(['active', 'blocked', 'in_progress', 'running', 'pending', 'needs-user-decision']);
472
+ const TASK_PHASE_ALIASES = new Map([
473
+ ['implementation', 'implement'],
474
+ ['build', 'implement'],
475
+ ['validation', 'verify'],
476
+ ['complete', 'verified'],
477
+ ['done', 'verified'],
478
+ ['closed', 'closeout'],
479
+ ]);
480
+ const TASK_STATUS_ALIASES = new Map([
481
+ ['in-progress', 'in_progress'],
482
+ ['inprogress', 'in_progress'],
483
+ ['needs_user_decision', 'needs-user-decision'],
484
+ ['needs-user', 'needs-user-decision'],
485
+ ]);
486
+ const TASK_VALID_PHASES = new Set([
487
+ 'intake', 'clarify', 'requirements', 'prd', 'acceptance', 'plan', 'explore',
488
+ 'implement', 'verify', 'review', 'fix', 'reflect', 'closeout', 'blocked',
489
+ 'archived', 'verified',
490
+ ]);
491
+ const TASK_VALID_STATUSES = new Set([...TASK_SAFE_ARCHIVE_STATUSES, ...TASK_NEVER_ARCHIVE_STATUSES, 'skipped', 'failed']);
492
+ const VALID_TASK_CAPSULE_POLICIES = new Set([
493
+ 'none',
494
+ 'required',
495
+ 'auto-capsule-required',
496
+ 'use-current-or-create-when-needed',
497
+ 'creates-or-updates',
498
+ ]);
237
499
 
238
500
  function validateTaskName(name, strict) {
239
501
  if (TASK_RESERVED.has(name)) return null;
@@ -250,6 +512,57 @@ function validateTaskName(name, strict) {
250
512
  return null;
251
513
  }
252
514
 
515
+ function normalizeTaskToken(value, validSet, aliasMap) {
516
+ if (value === null || value === undefined) return '';
517
+ const raw = String(value).trim().toLowerCase();
518
+ if (!raw) return '';
519
+ const compact = raw.replace(/\s+/g, '-');
520
+ const direct = aliasMap.get(compact) || compact;
521
+ if (validSet.has(direct)) return direct;
522
+ const tokens = raw
523
+ .replace(/[`*_()[\]{}:]/g, ' ')
524
+ .replace(/[^a-z0-9_-]+/g, ' ')
525
+ .split(/\s+/)
526
+ .map(token => token.trim())
527
+ .filter(Boolean);
528
+ for (const token of tokens) {
529
+ const canonical = aliasMap.get(token) || token;
530
+ if (validSet.has(canonical)) return canonical;
531
+ }
532
+ return '';
533
+ }
534
+
535
+ function normalizeTaskStatus(value) {
536
+ return normalizeTaskToken(value, TASK_VALID_STATUSES, TASK_STATUS_ALIASES);
537
+ }
538
+
539
+ function normalizeTaskPhase(value) {
540
+ return normalizeTaskToken(value, TASK_VALID_PHASES, TASK_PHASE_ALIASES);
541
+ }
542
+
543
+ function taskStateIssue(message) {
544
+ if (strict) errors.push(message);
545
+ else console.warn(`Warning: ${message}`);
546
+ }
547
+
548
+ function parseRootTaskProgress(text) {
549
+ const activeMatch = text.match(/## Active Task\s*\r?\n+(?:\s*\r?\n)?\s*-\s*([^\r\n]+)/);
550
+ const rawActive = activeMatch ? activeMatch[1].trim() : '';
551
+ const activeTask = rawActive && !/^none$/i.test(rawActive) ? rawActive : null;
552
+ const rows = [];
553
+ const taskIndexMatch = text.match(/## Task Index\s*\r?\n([\s\S]*?)(?=\r?\n## |\s*$)/);
554
+ if (taskIndexMatch) {
555
+ for (const line of taskIndexMatch[1].split(/\r?\n/)) {
556
+ const trimmed = line.trim();
557
+ if (!trimmed.startsWith('|')) continue;
558
+ if (/^\|\s*-+/.test(trimmed) || /^\|\s*ID\s*\|/i.test(trimmed)) continue;
559
+ const cells = trimmed.split('|').slice(1, -1).map(cell => cell.trim());
560
+ if (cells.length >= 4 && cells[0]) rows.push({ id: cells[0], phase: cells[2] });
561
+ }
562
+ }
563
+ return { activeTask, rows };
564
+ }
565
+
253
566
  function unresolvedTemplatePlaceholders(text) {
254
567
  const placeholders = [];
255
568
  const pattern = /\{\{([^{}\r\n]+)\}\}/g;
@@ -291,13 +604,12 @@ function registeredWorkflowFiles(...texts) {
291
604
 
292
605
  function listedWorkflowCommands(...texts) {
293
606
  const commands = new Set();
294
- const pattern = /`\/(wf(?:-[a-z0-9]+)?)(?:\s+[^`]*)?`/g;
607
+ const pattern = /`\/(wf(?:-[a-z0-9]+)*)(?:\s+[^`]*)?`/g;
295
608
 
296
609
  for (const text of texts) {
297
610
  for (const match of text.matchAll(pattern)) {
298
611
  const command = `/${match[1]}`;
299
612
  if (command === '/wf-help') continue;
300
- if (command === '/wf-browser') continue; // optional workflow, not a required skill
301
613
  commands.add(command);
302
614
  }
303
615
  }
@@ -305,12 +617,153 @@ function listedWorkflowCommands(...texts) {
305
617
  return [...commands].sort();
306
618
  }
307
619
 
308
- for (const rel of required) {
309
- if (!fs.existsSync(path.join(root, rel))) {
310
- errors.push(`missing required file: ${rel}`);
620
+ function extractStringArray(text, name) {
621
+ const match = text.match(new RegExp(`const ${name} = \\[([\\s\\S]*?)\\];`));
622
+ if (!match) return null;
623
+ return new Set([...match[1].matchAll(/'([^']+)'/g)].map(item => item[1]));
624
+ }
625
+
626
+ function expectedCommandAliases(id) {
627
+ return [`/${id}`, `$${id}`, `/skills ${id}`];
628
+ }
629
+
630
+ function validateCommandSurface() {
631
+ if (commandSurfaceLoadError) {
632
+ errors.push(commandSurfaceLoadError);
633
+ return;
634
+ }
635
+
636
+ const seen = new Set();
637
+ const claudeRouter = read('CLAUDE.md');
638
+ const readmeRouter = read('Harness/README.md');
639
+ const ecc = read('.claude/rules/ecc/common.md');
640
+ const eccExemptionLine = ecc.split(/\r?\n/).find(line => line.includes('excluding')) || '';
641
+ const claudeHelp = read('.claude/commands/wf-help.md');
642
+ const opencodeHelp = read('.opencode/commands/wf-help.md');
643
+ const removeScript = read('Harness/scripts/wf-remove.mjs');
644
+ const removeSkillRegistry = extractStringArray(removeScript, 'BUILT_IN_SKILL_NAMES');
645
+ const removeCommandRegistry = extractStringArray(removeScript, 'BUILT_IN_COMMAND_NAMES');
646
+ const cleanupDirs = extractStringArray(removeScript, 'CLEANUP_DIRS');
647
+
648
+ if (!removeSkillRegistry) errors.push('Harness/scripts/wf-remove.mjs missing BUILT_IN_SKILL_NAMES registry');
649
+ if (!removeCommandRegistry) errors.push('Harness/scripts/wf-remove.mjs missing BUILT_IN_COMMAND_NAMES registry');
650
+ if (!cleanupDirs) errors.push('Harness/scripts/wf-remove.mjs missing CLEANUP_DIRS registry');
651
+
652
+ for (const command of commandDefinitions) {
653
+ const id = command?.id;
654
+ const surfaces = command?.surfaces || {};
655
+ if (!id || !/^wf(?:-[a-z0-9]+)*$/.test(id)) {
656
+ errors.push(`command-surface has invalid command id: ${JSON.stringify(id)}`);
657
+ continue;
658
+ }
659
+ if (seen.has(id)) errors.push(`command-surface duplicate command id: ${id}`);
660
+ seen.add(id);
661
+
662
+ if (!['direct', 'workflow'].includes(command.classification)) {
663
+ errors.push(`command-surface ${id} has invalid classification: ${JSON.stringify(command.classification)}`);
664
+ }
665
+ if (command.entersWf !== (command.classification === 'workflow')) {
666
+ errors.push(`command-surface ${id} entersWf must match classification`);
667
+ }
668
+
669
+ for (const alias of expectedCommandAliases(id)) {
670
+ if (!Array.isArray(command.aliases) || !command.aliases.includes(alias)) {
671
+ errors.push(`command-surface ${id} missing alias ${alias}`);
672
+ }
673
+ }
674
+
675
+ const claudeCommand = `.claude/commands/${id}.md`;
676
+ const opencodeCommand = `.opencode/commands/${id}.md`;
677
+ const claudeSkill = `.claude/skills/${id}/SKILL.md`;
678
+ const codexSkill = `.agents/skills/${id}/SKILL.md`;
679
+
680
+ if (surfaces.claudeCommand && !fs.existsSync(path.join(root, claudeCommand))) {
681
+ errors.push(`command-surface ${id} missing Claude command: ${claudeCommand}`);
682
+ }
683
+ if (surfaces.opencodeCommand && !fs.existsSync(path.join(root, opencodeCommand))) {
684
+ errors.push(`command-surface ${id} missing OpenCode command: ${opencodeCommand}`);
685
+ }
686
+ if (surfaces.claudeSkill && !fs.existsSync(path.join(root, claudeSkill))) {
687
+ errors.push(`command-surface ${id} missing Claude skill: ${claudeSkill}`);
688
+ }
689
+ if (surfaces.codexSkill && !fs.existsSync(path.join(root, codexSkill))) {
690
+ errors.push(`command-surface ${id} missing Codex skill: ${codexSkill}`);
691
+ }
692
+ if (surfaces.helpRow) {
693
+ const rowMarker = `| \`/${id}`;
694
+ if (!claudeHelp.includes(rowMarker)) errors.push(`.claude/commands/wf-help.md missing command-surface help row for /${id}`);
695
+ if (!opencodeHelp.includes(rowMarker)) errors.push(`.opencode/commands/wf-help.md missing command-surface help row for /${id}`);
696
+ }
697
+
698
+ if (command.classification === 'direct') {
699
+ for (const alias of command.aliases || []) {
700
+ if (!claudeRouter.includes(`\`${alias}\``)) errors.push(`CLAUDE.md missing direct/compat alias ${alias}`);
701
+ if (!readmeRouter.includes(alias)) errors.push(`Harness/README.md missing direct/compat alias ${alias}`);
702
+ if (!eccExemptionLine.includes(`\`${alias}\``)) errors.push(`.claude/rules/ecc/common.md missing ECC direct command exemption ${alias}`);
703
+ }
704
+ for (const rel of [claudeCommand, opencodeCommand]) {
705
+ const text = read(rel);
706
+ if (text && id !== 'wf-help') {
707
+ if (!/direct command/i.test(text)) errors.push(`${rel} missing DIRECT command classification`);
708
+ if (!text.includes('Do not invoke a skill')) errors.push(`${rel} missing direct no-skill boundary`);
709
+ }
710
+ }
711
+ }
712
+
713
+ if (command.classification === 'workflow') {
714
+ for (const alias of command.aliases || []) {
715
+ if (eccExemptionLine.includes(`\`${alias}\``)) {
716
+ errors.push(`.claude/rules/ecc/common.md incorrectly exempts workflow command ${alias}`);
717
+ }
718
+ }
719
+ for (const rel of [claudeCommand, opencodeCommand]) {
720
+ const text = read(rel);
721
+ if (!text) continue;
722
+ if (!text.includes('workflow command')) errors.push(`${rel} missing workflow command classification`);
723
+ if (!text.includes('Harness/MEMORY.md')) errors.push(`${rel} missing workflow router load`);
724
+ }
725
+ }
726
+
727
+ if (['wf', 'wf-max', 'wf-command-create'].includes(id)) {
728
+ const text = `${read(claudeCommand)}\n${read(claudeSkill)}`;
729
+ if (!text.includes('task capsule')) errors.push(`${id} missing required task capsule instruction`);
730
+ if (!text.includes('task-<verb>-<noun>')) errors.push(`${id} missing task id convention`);
731
+ }
732
+
733
+ if (surfaces.claudeSkill || surfaces.codexSkill) {
734
+ if (removeSkillRegistry && !removeSkillRegistry.has(id)) {
735
+ errors.push(`Harness/scripts/wf-remove.mjs BUILT_IN_SKILL_NAMES missing ${id}`);
736
+ }
737
+ if (cleanupDirs) {
738
+ if (!cleanupDirs.has(`.claude/skills/${id}`)) errors.push(`Harness/scripts/wf-remove.mjs CLEANUP_DIRS missing .claude/skills/${id}`);
739
+ if (!cleanupDirs.has(`.agents/skills/${id}`)) errors.push(`Harness/scripts/wf-remove.mjs CLEANUP_DIRS missing .agents/skills/${id}`);
740
+ }
741
+ }
742
+ if ((surfaces.claudeCommand || surfaces.opencodeCommand) && removeCommandRegistry && !removeCommandRegistry.has(id)) {
743
+ errors.push(`Harness/scripts/wf-remove.mjs BUILT_IN_COMMAND_NAMES missing ${id}`);
744
+ }
745
+ }
746
+
747
+ // Validate taskCapsulePolicy enum
748
+ for (const cmd of commandDefinitions) {
749
+ if (!cmd.taskCapsulePolicy || !VALID_TASK_CAPSULE_POLICIES.has(cmd.taskCapsulePolicy)) {
750
+ errors.push(`command-surface ${cmd.id}: invalid taskCapsulePolicy "${cmd.taskCapsulePolicy}". Valid: ${[...VALID_TASK_CAPSULE_POLICIES].join(', ')}`);
751
+ }
311
752
  }
312
753
  }
313
754
 
755
+ if (isGlobalRuntimeRoot) {
756
+ validateGlobalRuntimeRoot();
757
+ }
758
+
759
+ if (isGlobalProjectBridge) {
760
+ validateGlobalProjectBridge();
761
+ }
762
+
763
+ validateCommandSurface();
764
+
765
+ validateRequiredFiles(required);
766
+
314
767
  for (const rel of legacyRootSpecDocs) {
315
768
  if (fs.existsSync(path.join(root, rel))) {
316
769
  errors.push(`legacy root Harness spec doc should be migrated to Harness/specs/**: ${rel}`);
@@ -433,6 +886,14 @@ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Memory Candidate Dete
433
886
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'explicit user preference', 'explicit user preference immediate write rule');
434
887
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Memory Routing (L3)', 'memory routing section');
435
888
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Scenario pack', 'route scoring scenario pack');
889
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project/Global Memory Boundary', 'project/global memory boundary section');
890
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Harness/tasks/` and `Harness/PROGRESS.md` are always project-local', 'global install keeps task state project-local');
891
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project memory lives in `Harness/memory/`', 'project memory scope rule');
892
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Global memory must never store project task state', 'global memory task-state exclusion');
893
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project/Global Settings Boundary', 'project/global settings boundary section');
894
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project settings live in `Harness/settings.json` and override global settings', 'project settings override global settings');
895
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'copied, not symlinked, into Claude Code, Codex, and OpenCode', 'host-global copy mode');
896
+ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Existing user-authored config, command, skill, or agent files are user-owned', 'user-owned host file rule');
436
897
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'same tool or command pattern fails 3+ times', 'tool reflection trigger');
437
898
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'user corrects the same assumption or preference 2+ times', 'user correction reflection trigger');
438
899
  requireText('CLAUDE.md', 'startup-hints.md', 'CLAUDE startup-hints routing');
@@ -463,7 +924,7 @@ requireText('CLAUDE.md', 'Keep intermediate user updates to 1-2 short sentences'
463
924
  requireText('.claude/rules/ecc/common.md', '## Low-Noise Progress', 'ECC low-noise progress section');
464
925
  requireText('.claude/rules/ecc/common.md', "Match the user's language for user-facing prose", 'ECC user-facing language match rule');
465
926
  requireText('.claude/rules/ecc/common.md', 'Do not recap plans, paste logs, or narrate obvious file reads', 'ECC low-noise no-recap rule');
466
- requireText('.claude/rules/ecc/common.md', 'excluding `/wf-help` and `/wf-update`', 'ECC direct command exemption');
927
+ requireText('Harness/specs/runtime/command-surface.json', '"wf-command-create"', 'command surface registry wf-command-create entry');
467
928
  requireText('Harness/README.md', 'Load By Task', 'Harness task router');
468
929
  requireText('Harness/README.md', 'Need context/cache/token efficiency', 'cache/token router row');
469
930
  requireText('Harness/specs/runtime/context-loading.md', 'Context Tiers', 'context tier load budget section');
@@ -485,6 +946,12 @@ requireText('Harness/specs/runtime/subagents.md', 'Cache-first discipline', 'sub
485
946
  forbidText('CLAUDE.md', 'Harness/specs/guides/SETUP.md', 'CLAUDE.md SETUP reference');
486
947
  forbidText('CLAUDE.md', 'follow it before normal project work', 'installed-project SETUP hot-path routing');
487
948
  requireText('Harness/specs/guides/SETUP.md', 'Harness/specs/protocols/MEMORY_PROTOCOL.md', 'setup memory protocol reference');
949
+ requireText('Harness/specs/guides/SETUP.md', '--install-scope project', 'setup project install scope');
950
+ requireText('Harness/specs/guides/SETUP.md', '--install-scope global', 'setup global install scope');
951
+ requireText('Harness/specs/guides/SETUP.md', '--global-dir <dir>', 'setup global dir flag');
952
+ requireText('Harness/specs/guides/SETUP.md', '--host-global-dir <dir>', 'setup host-global dir flag');
953
+ requireText('Harness/specs/guides/SETUP.md', 'copy Claude Code, Codex, and OpenCode command/skill surfaces', 'setup three-host global copy');
954
+ requireText('Harness/specs/guides/SETUP.md', 'Project settings in `Harness/settings.json` override global settings defaults', 'setup settings precedence');
488
955
  requireText('Harness/specs/guides/SETUP.md', 'no startup dependency on this setup reference', 'SETUP startup boundary');
489
956
  forbidText('Harness/specs/guides/SETUP.md', 'bootstrap contract line', 'stale SETUP-to-CLAUDE bootstrap contract');
490
957
  forbidText('Harness/specs/runtime/context-loading.md', 'Always keep:', 'ambiguous always-load context rule');
@@ -582,7 +1049,7 @@ for (const command of listedWorkflowCommands(docsReadme, read('.claude/commands/
582
1049
 
583
1050
  for (const workflowFile of registeredWorkflowFiles(docsReadme, memory)) {
584
1051
  if (!fs.existsSync(path.join(root, workflowFile))) {
585
- // Optional workflows (browser-e2e, ts-react-frontend, etc.) may not be installed
1052
+ // Optional workflows (ts-react-frontend, ui-ux-review, etc.) may not be installed
586
1053
  continue;
587
1054
  }
588
1055
  }
@@ -602,9 +1069,14 @@ function requireUiSelectorContract(rel) {
602
1069
  }
603
1070
  }
604
1071
 
605
- requireUiSelectorContract('Harness/workflows/browser-e2e.md');
1072
+ requireUiSelectorContract('.claude/skills/wf-browser/SKILL.md');
1073
+ requireUiSelectorContract('.agents/skills/wf-browser/SKILL.md');
606
1074
  requireUiSelectorContract('Harness/workflows/ts-react-frontend.md');
607
1075
 
1076
+ if (fs.existsSync(path.join(root, 'Harness/workflows/browser-e2e.md'))) {
1077
+ errors.push('retired optional workflow must not exist: Harness/workflows/browser-e2e.md');
1078
+ }
1079
+
608
1080
  for (const skill of commonSkills) {
609
1081
  const rel = `.claude/skills/${skill}/SKILL.md`;
610
1082
  const text = read(rel);
@@ -784,28 +1256,56 @@ requireText('Harness/specs/workflows/WF-MAX.md', 'inherits the full', 'WF-MAX st
784
1256
  requireText('Harness/specs/workflows/WF-MAX.md', 'Cross-CLI', 'WF-MAX cross-CLI overflow');
785
1257
  requireText('Harness/specs/workflows/WF-MAX.md', 'claude -p', 'WF-MAX Claude CLI overflow');
786
1258
  requireText('Harness/specs/workflows/WF-MAX.md', 'codex exec', 'WF-MAX Codex CLI overflow');
787
- requireText('Harness/specs/workflows/WF-MAX.md', 'agents.max_threads', 'WF-MAX Codex max_threads config');
788
- requireText('Harness/specs/workflows/WF-MAX.md', 'agents.max_depth', 'WF-MAX Codex max_depth config');
789
- requireText('Harness/specs/workflows/WF-MAX.md', 'max_threads = 12', 'WF-MAX Codex scaffold max_threads default');
790
- requireText('Harness/specs/workflows/WF-MAX.md', 'max_depth = 1', 'WF-MAX Codex scaffold max_depth default');
791
- requireText('Harness/specs/workflows/WF-MAX.md', 'Ask the user before raising', 'WF-MAX asks before raising Codex thread cap');
1259
+ requireText('Harness/specs/workflows/WF-MAX.md', 'agents.max_threads', 'WF-MAX Codex legacy max_threads alias');
1260
+ requireText('Harness/specs/workflows/WF-MAX.md', 'max_concurrent_threads_per_session', 'WF-MAX Codex concurrent thread config');
1261
+ requireText('Harness/specs/workflows/WF-MAX.md', 'Mandatory Fan-Out Contract', 'WF-MAX mandatory fan-out attempt contract');
1262
+ requireText('Harness/specs/workflows/WF-MAX.md', 'fanoutAttempted: true', 'WF-MAX records fan-out attempt');
1263
+ requireText('Harness/specs/workflows/WF-MAX.md', 'Runtime Capacity Map', 'WF-MAX runtime capacity map');
1264
+ requireText('Harness/specs/workflows/WF-MAX.md', 'CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION', 'WF-MAX Claude Code subagent cap docs');
1265
+ requireText('Harness/specs/workflows/WF-MAX.md', 'CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS', 'WF-MAX Claude Code concurrent subagent cap docs');
1266
+ requireText('Harness/specs/workflows/WF-MAX.md', 'CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH', 'WF-MAX Claude Code spawn-depth cap docs');
1267
+ requireText('Harness/specs/workflows/WF-MAX.md', 'Do not scaffold Codex scalar agent caps', 'WF-MAX Codex config compatibility guard');
1268
+ requireText('Harness/specs/workflows/WF-MAX.md', 'subagent_depth = 2', 'WF-MAX OpenCode subagent depth default');
1269
+ requireText('Harness/specs/workflows/WF-MAX.md', 'asks the user before any project/global Codex config change', 'WF-MAX asks before changing Codex config');
792
1270
  requireText('Harness/specs/workflows/WF-MAX.md', 'Close completed agents', 'WF-MAX close completed agents before overflow');
793
1271
  requireText('Harness/specs/workflows/WF-MAX.md', 'Codex++', 'WF-MAX forbids relying on Codex++ as stable capacity');
794
1272
  requireText('Harness/specs/workflows/WF-MAX.md', 'does not authorize CEO source edits', 'WF-MAX useful-degrade CEO source-edit boundary');
795
1273
  requireText('Harness/specs/workflows/WF-MAX.md', '## Cross-Task Decisions', 'WF-MAX PROGRESS heading preservation');
796
- requireText('.claude/skills/wf-max/SKILL.md', 'agents.max_threads', 'wf-max skill Codex thread config');
797
- requireText('.claude/skills/wf-max/SKILL.md', 'max_threads = 12', 'wf-max skill Codex scaffold max_threads default');
1274
+ requireText('.claude/skills/wf-max/SKILL.md', 'agents.max_concurrent_threads_per_session', 'wf-max skill Codex thread config');
1275
+ requireText('.claude/skills/wf-max/SKILL.md', 'agents.max_threads', 'wf-max skill Codex legacy alias');
1276
+ requireText('.claude/skills/wf-max/SKILL.md', 'Do not scaffold scalar `[agents]` caps', 'wf-max skill Codex config compatibility guard');
1277
+ requireText('.claude/skills/wf-max/SKILL.md', 'CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION', 'wf-max skill Claude Code subagent cap');
1278
+ requireText('.claude/skills/wf-max/SKILL.md', 'CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS', 'wf-max skill Claude Code concurrent subagent cap');
1279
+ requireText('.claude/skills/wf-max/SKILL.md', 'CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH', 'wf-max skill Claude Code spawn-depth cap');
1280
+ requireText('.claude/skills/wf-max/SKILL.md', 'subagent_depth = 2', 'wf-max skill OpenCode subagent depth default');
798
1281
  requireText('.claude/skills/wf-max/SKILL.md', 'ask the user before raising', 'wf-max skill asks before raising Codex thread cap');
799
1282
  requireText('.claude/skills/wf-max/SKILL.md', 'Codex++', 'wf-max skill forbids Codex++ capacity assumption');
800
1283
  requireText('.claude/skills/wf-max/SKILL.md', taskIdConvention, 'wf-max skill task-id naming convention');
801
1284
  requireText('.agents/skills/wf-max/SKILL.md', taskIdConvention, 'Codex wf-max skill task-id naming convention');
802
1285
  requireText('.claude/skills/wf-max/SKILL.md', 'does not authorize CEO source edits', 'wf-max skill useful-degrade CEO source-edit boundary');
803
1286
  requireText('.claude/skills/wf-max/SKILL.md', '## Cross-Task Decisions', 'wf-max skill PROGRESS heading preservation');
804
- requireText('.codex/config.toml', '[agents]', 'Codex agents config table');
805
- requireText('.codex/config.toml', 'max_threads = 12', 'Codex scaffold max_threads default');
806
- requireText('.codex/config.toml', 'max_depth = 1', 'Codex scaffold max_depth default');
1287
+ requireText('.codex/config.toml', 'intentionally avoids WF-MAX capacity defaults', 'Codex config compatibility note');
1288
+ forbidActiveToml('.codex/config.toml', /^\[agents\]$/m, 'Codex project config agents table');
1289
+ forbidActiveToml('.codex/config.toml', /^\s*max_concurrent_threads_per_session\s*=/m, 'Codex project config concurrent thread scalar');
1290
+ forbidActiveToml('.codex/config.toml', /^\s*max_depth\s*=/m, 'Codex project config max_depth scalar');
1291
+ forbidActiveToml('.codex/config.toml', /^\s*max_threads\s*=/m, 'Codex project config legacy max_threads scalar');
807
1292
  requireText('opencode.json', '"$schema": "https://opencode.ai/config.json"', 'OpenCode config schema');
808
1293
  requireText('opencode.json', '.claude/rules/ecc/common.md', 'OpenCode instructions reference to ECC rules');
1294
+ requireText('opencode.json', '"subagent_depth": 2', 'OpenCode WF-MAX subagent nesting depth');
1295
+ requireText('.opencode/commands/wf-max.md', 'MUST attempt native runtime subagent fan-out', 'OpenCode wf-max command fan-out attempt');
1296
+ requireText('.opencode/commands/wf-max.md', 'subagent_depth >= 2', 'OpenCode wf-max command subagent depth requirement');
1297
+ requireText('.opencode/commands/wf-max.md', 'fanoutAttempted: true', 'OpenCode wf-max command records fan-out attempt');
1298
+ requireText('.opencode/agents/explore-manager.md', 'task:', 'OpenCode explore-manager task permission');
1299
+ requireText('.opencode/agents/explore-manager.md', '"codebase-explorer": allow', 'OpenCode explore-manager child allowlist');
1300
+ requireText('.opencode/agents/explore-manager.md', '"git status*": allow', 'OpenCode explore-manager read-only git status');
1301
+ requireText('.opencode/agents/explore-manager.md', '"git diff*": allow', 'OpenCode explore-manager read-only git diff');
1302
+ forbidText('.opencode/agents/explore-manager.md', '"git *": allow', 'OpenCode explore-manager broad git permission');
1303
+ requireText('.opencode/agents/architect-manager.md', '"architect": allow', 'OpenCode architect-manager child allowlist');
1304
+ requireText('.opencode/agents/implement-manager.md', '"implementer": allow', 'OpenCode implement-manager child allowlist');
1305
+ forbidText('.opencode/agents/implement-manager.md', '"node *": allow', 'OpenCode implement-manager broad node permission');
1306
+ forbidText('.opencode/agents/implement-manager.md', '"npm *": allow', 'OpenCode implement-manager broad npm permission');
1307
+ requireText('.opencode/agents/review-manager.md', '"reviewer": allow', 'OpenCode review-manager child allowlist');
1308
+ forbidText('.opencode/agents/review-manager.md', '"node *": allow', 'OpenCode review-manager broad node permission');
809
1309
  requireText('Harness/README.md', '/wf-max', 'wf max router alias');
810
1310
  requireText('Harness/README.md', 'WF-MAX.md', 'WF-MAX router reference');
811
1311
  requireText('Harness/README.md', 'ACCEPTANCE_PROTOCOL.md', 'acceptance protocol router reference');
@@ -893,13 +1393,14 @@ requireText('.claude/commands/wf-help.md', '| `/wf-help` |', 'wf-help command ro
893
1393
  requireText('.claude/commands/wf-help.md', '| `/wf-max <task>` |', 'wf-help wf-max row');
894
1394
  requireText('.claude/commands/wf-help.md', '| `/wf-auto` |', 'wf-help wf-auto row');
895
1395
  requireText('.claude/commands/wf-help.md', '| `/wf-readme <task>` |', 'wf-help wf-readme row');
1396
+ forbidText('.claude/commands/wf-help.md', '.opencode/skills/', 'wf-help nonexistent OpenCode skills directory claim');
1397
+ forbidText('.opencode/commands/wf-help.md', '.opencode/skills/', 'OpenCode wf-help nonexistent skills directory claim');
896
1398
  requireText('Harness/README.md', '## Direct Commands', 'direct commands section');
897
1399
  requireText('Harness/README.md', '.claude/commands/wf-help.md', 'wf-help router reference');
898
1400
  requireText('Harness/specs/workflows/WF-MAX.md', 'three-layer architecture', 'CLAUDE.md three-layer role architecture');
899
- requireText('Harness/README.md', 'no runtime hook state', 'CLAUDE no-hook role enforcement statement');
900
- requireText('Harness/README.md', 'no runtime hook state', 'README no-hook runtime statement');
901
- requireText('Harness/specs/workflows/WF-AUTO.md', 'WF-AUTO Hook Exception', 'wf-auto-only hook exception');
902
- requireText('Harness/specs/workflows/WF-AUTO.md', 'only `/wf-auto` may use a runtime hook', 'wf-auto-only hook boundary');
1401
+ requireText('Harness/README.md', 'role enforcement has no runtime hook state', 'CLAUDE no-hook role enforcement statement');
1402
+ requireText('Harness/specs/workflows/WF-AUTO.md', 'Runtime Hook Boundaries', 'runtime hook boundaries');
1403
+ requireText('Harness/specs/workflows/WF-AUTO.md', 'only `/wf-auto` may use a runtime hook to drive auto-optimization', 'wf-auto-only hook boundary');
903
1404
  forbidText('CLAUDE.md', 'Enforced by hooks', 'WF-MAX hook enforcement claim');
904
1405
  forbidText('Harness/specs/workflows/WF.md', 'MUST use at least 3 distinct role passes', 'old WF role-pass minimum');
905
1406
  forbidText('Harness/specs/workflows/WF.md', 'at least three distinct role passes', 'old WF role-pass wording');
@@ -919,18 +1420,20 @@ const codexHookConfig = read('.codex/hooks.json');
919
1420
  const claudeSettings = read('.claude/settings.json');
920
1421
  requireText('.codex/hooks.json', '"SessionStart"', 'Codex startup-only update hook');
921
1422
  requireText('.claude/settings.json', '"SessionStart"', 'Claude startup-only update hook');
922
- forbidText('.codex/hooks.json', 'UserPromptSubmit', 'Codex turn-by-turn update hook');
1423
+ forbidText('.codex/hooks.json', 'UserPromptSubmit', 'Codex turn-by-turn status hook');
1424
+ forbidText('.codex/hooks.json', '"Stop"', 'Codex stop status hook');
1425
+ forbidText('.codex/hooks.json', 'wf-status.mjs', 'removed Codex WF status script');
923
1426
  forbidText('.claude/settings.json', 'UserPromptSubmit', 'Claude turn-by-turn update hook');
924
1427
  requireText('.opencode/plugins/harness-wf-status.mjs', 'opencode.startup', 'OpenCode startup-only update check');
925
1428
  forbidText('.opencode/plugins/harness-wf-status.mjs', "'chat.message'", 'OpenCode turn-by-turn update hook');
926
- if (codexHookConfig && !codexHookConfig.includes('wf-auto')) {
927
- errors.push('.codex/hooks.json may only exist for a wf-auto hook configuration');
1429
+ if (codexHookConfig && !codexHookConfig.includes('wf-auto-update-prompt.mjs')) {
1430
+ errors.push('.codex/hooks.json missing startup wf-auto update check');
928
1431
  }
929
1432
  if (claudeSettings.includes('"hooks"') && !claudeSettings.includes('wf-auto')) {
930
1433
  errors.push('.claude/settings.json hooks may only be used for wf-auto');
931
1434
  }
932
- if (read('.codex/config.toml').includes('hooks = true') && !codexHookConfig.includes('wf-auto')) {
933
- errors.push('.codex/config.toml may enable hooks only with a wf-auto hook configuration');
1435
+ if (read('.codex/config.toml').includes('hooks = true') && !codexHookConfig.includes('wf-auto-update-prompt.mjs')) {
1436
+ errors.push('.codex/config.toml may enable hooks only with a Harness hook configuration');
934
1437
  }
935
1438
 
936
1439
  // AGENTS.md must be a thin shim — no Harness/WF-MAX/command table content
@@ -966,6 +1469,17 @@ requireText('.claude/skills/wf-auto/SKILL.md', 'Harness/tasks/auto/PROGRESS.md',
966
1469
  requireText('.claude/skills/wf-auto/SKILL.md', 'evidence ledger path/summary', 'wf-auto skill return evidence ledger path');
967
1470
  if (fs.existsSync(path.join(root, '.claude/skills/wf-browser/SKILL.md'))) {
968
1471
  requireText('.claude/skills/wf-browser/SKILL.md', 'Cache Discipline', 'wf-browser skill cache discipline');
1472
+ requireText('.claude/skills/wf-browser/SKILL.md', 'Browser Evidence Contract', 'wf-browser browser evidence contract');
1473
+ requireText('.claude/skills/wf-browser/SKILL.md', 'Controllable UI Contract', 'wf-browser controllable UI contract');
1474
+ requireText('.claude/skills/wf-browser/SKILL.md', 'Browser Use CLI', 'wf-browser current Browser Use CLI guidance');
1475
+ requireText('.claude/skills/wf-browser/SKILL.md', 'browser-use --doctor', 'wf-browser Browser Use doctor command');
1476
+ requireText('.claude/skills/wf-browser/SKILL.md', 'browser-use skill', 'wf-browser Browser Use skill command');
1477
+ requireText('.claude/skills/wf-browser/SKILL.md', 'new_tab("https://example.com")', 'wf-browser Browser Use script-style example');
1478
+ requireText('.claude/skills/wf-browser/SKILL.md', 'capture_screenshot', 'wf-browser Browser Use screenshot helper');
1479
+ requireText('.claude/skills/wf-browser/SKILL.md', 'old `browser-use open/state/click/screenshot/input/wait` subcommands are removed', 'wf-browser removed old Browser Use subcommands');
1480
+ requireText('.claude/skills/wf-browser/SKILL.md', 'data-testid', 'wf-browser data-testid guidance');
1481
+ requireText('.claude/skills/wf-browser/SKILL.md', 'accessible labels/roles', 'wf-browser accessible selector guidance');
1482
+ requireText('.claude/skills/wf-browser/SKILL.md', 'inputs, buttons, filters, rows, empty/error/loading states', 'wf-browser controllable UI coverage targets');
969
1483
  }
970
1484
 
971
1485
  // Direct command checks
@@ -988,8 +1502,9 @@ for (const rel of ['.claude/commands/wf-update.md', '.opencode/commands/wf-updat
988
1502
  requireText('Harness/scripts/wf-update-check.mjs', 'releaseHighlights', 'wf-update-check release highlights metadata');
989
1503
  requireText('Harness/scripts/wf-update-check.mjs', 'updateReportRequired', 'wf-update-check user update report requirement');
990
1504
  requireText('.claude/commands/wf-help.md', 'direct command', 'wf-help wf-update direct command classification');
991
- requireText('.claude/commands/wf-help.md', '/wf-browser', 'wf-help optional browser workflow row');
992
- requireText('.opencode/commands/wf-help.md', '/wf-browser', 'OpenCode wf-help optional browser workflow row');
1505
+ requireText('.claude/commands/wf-help.md', '$wf-help', 'wf-help Codex compatibility usage');
1506
+ requireText('.claude/commands/wf-help.md', '/wf-browser', 'wf-help built-in browser workflow row');
1507
+ requireText('.opencode/commands/wf-help.md', '/wf-browser', 'OpenCode wf-help built-in browser workflow row');
993
1508
 
994
1509
  // wf-update skill must NOT claim Claude Code /wf-update as a skill invocation
995
1510
  // (allow mentions in the description/body that say "direct command" — those are correct)
@@ -1080,23 +1595,78 @@ requireText('CLAUDE.md', 'Harness/specs/workflows/WF-STATE.md', 'CLAUDE.md WF-ST
1080
1595
  requireText('CLAUDE.md', 'Harness/specs/protocols/TASK_ARCHIVE.md', 'CLAUDE.md TASK_ARCHIVE routing');
1081
1596
  requireText('Harness/README.md', 'WF-STATE.md', 'README WF-STATE routing');
1082
1597
  requireText('Harness/specs/protocols/TASK_ARCHIVE.md', 'archive-tasks.mjs', 'TASK_ARCHIVE script reference');
1598
+ requireText('Harness/specs/workflows/WF-STATE.md', 'task-state.mjs', 'WF-STATE task-state script contract');
1599
+ requireText('Harness/specs/protocols/TASK_ARCHIVE.md', 'task-state.mjs', 'TASK_ARCHIVE task-state script reference');
1600
+
1601
+ // Task-state consistency: prompt text is not authoritative for machine state.
1602
+ const rootTaskProgress = parseRootTaskProgress(read('Harness/PROGRESS.md'));
1603
+ const outerTaskSet = new Set(taskDirs.filter(name => !TASK_RESERVED.has(name) && !name.startsWith('_')));
1604
+ const rootTaskRows = new Set(rootTaskProgress.rows.map(row => row.id));
1605
+
1606
+ if (rootTaskProgress.activeTask && !outerTaskSet.has(rootTaskProgress.activeTask)) {
1607
+ taskStateIssue(`Harness/PROGRESS.md Active Task "${rootTaskProgress.activeTask}" does not exist under Harness/tasks/`);
1608
+ }
1609
+
1610
+ for (const row of rootTaskProgress.rows) {
1611
+ if (!outerTaskSet.has(row.id)) {
1612
+ taskStateIssue(`Harness/PROGRESS.md Task Index row has no outer task directory: ${row.id}`);
1613
+ }
1614
+ if (row.phase && !normalizeTaskPhase(row.phase)) {
1615
+ taskStateIssue(`Harness/PROGRESS.md Task Index row has unknown phase for ${row.id}: ${row.phase}`);
1616
+ }
1617
+ }
1618
+
1619
+ const activeStateTasks = [];
1620
+ for (const taskDir of outerTaskSet) {
1621
+ if (!rootTaskRows.has(taskDir)) {
1622
+ taskStateIssue(`Harness/tasks/${taskDir}/ is missing from Harness/PROGRESS.md Task Index`);
1623
+ }
1624
+
1625
+ const statePath = path.join(root, 'Harness', 'tasks', taskDir, 'STATE.json');
1626
+ if (!fs.existsSync(statePath)) {
1627
+ taskStateIssue(`Harness/tasks/${taskDir}/STATE.json is missing; run node Harness/scripts/task-state.mjs reconcile --apply`);
1628
+ continue;
1629
+ }
1630
+
1631
+ let state;
1632
+ try {
1633
+ state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
1634
+ } catch (err) {
1635
+ taskStateIssue(`Harness/tasks/${taskDir}/STATE.json is invalid JSON: ${err.message}`);
1636
+ continue;
1637
+ }
1638
+
1639
+ if (state.taskId && state.taskId !== taskDir) {
1640
+ taskStateIssue(`Harness/tasks/${taskDir}/STATE.json taskId "${state.taskId}" does not match directory`);
1641
+ }
1642
+ const status = normalizeTaskStatus(state.status);
1643
+ const phase = normalizeTaskPhase(state.phase);
1644
+ if (state.status && !status) {
1645
+ taskStateIssue(`Harness/tasks/${taskDir}/STATE.json has unknown status "${state.status}"`);
1646
+ }
1647
+ if (state.phase && !phase) {
1648
+ taskStateIssue(`Harness/tasks/${taskDir}/STATE.json has unknown phase "${state.phase}"`);
1649
+ }
1650
+ if (status === 'active') activeStateTasks.push(taskDir);
1651
+ if (status === 'active' && taskDir !== rootTaskProgress.activeTask) {
1652
+ taskStateIssue(`Harness/tasks/${taskDir}/STATE.json is active but Harness/PROGRESS.md Active Task is ${rootTaskProgress.activeTask || 'None'}; run node Harness/scripts/task-state.mjs reconcile --apply`);
1653
+ }
1654
+ if (taskDir === rootTaskProgress.activeTask && status && status !== 'active') {
1655
+ taskStateIssue(`Harness/PROGRESS.md Active Task points to ${taskDir}, but STATE.json status is "${status}"; run node Harness/scripts/task-state.mjs reconcile --apply`);
1656
+ }
1657
+ }
1658
+
1659
+ if (activeStateTasks.length > 1) {
1660
+ taskStateIssue(`Multiple STATE.json files are active: ${activeStateTasks.join(', ')}; run node Harness/scripts/task-state.mjs reconcile --apply`);
1661
+ }
1083
1662
 
1084
1663
  // Outer task capsule cap: keep Harness/tasks/ lean (see Harness/specs/protocols/TASK_ARCHIVE.md)
1085
1664
  const OUTER_TASK_CAP = 5;
1086
1665
  const outerTasks = taskDirs.filter(name => !TASK_RESERVED.has(name) && !name.startsWith('_'));
1087
1666
  if (outerTasks.length > OUTER_TASK_CAP) {
1088
- const capMsg = `Harness/tasks/ has ${outerTasks.length} outer task capsules (cap ${OUTER_TASK_CAP}); archive completed tasks with node Harness/scripts/archive-tasks.mjs --apply (see Harness/specs/protocols/TASK_ARCHIVE.md)`;
1667
+ 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)`;
1089
1668
  if (strict) errors.push(capMsg);
1090
1669
  else console.warn(`Warning: ${capMsg}`);
1091
1670
  }
1092
1671
 
1093
- if (errors.length) {
1094
- console.error(`Harness validation failed${strict ? ' (strict)' : ''}:`);
1095
- for (const error of errors) console.error(`- ${error}`);
1096
- process.exit(1);
1097
- }
1098
-
1099
- console.log(`Harness validation passed${strict ? ' (strict)' : ''}.`);
1100
- if (!strict) {
1101
- console.log('Tip: run `node Harness/scripts/validate-harness.mjs --strict` after bootstrap to check unresolved project placeholders.');
1102
- }
1672
+ finishValidation();