create-harness-vibe-coding 0.8.17 → 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 (41) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README-CN.md +2 -0
  3. package/README.md +2 -0
  4. package/package.json +1 -1
  5. package/src/generator.js +613 -97
  6. package/src/index.js +173 -42
  7. package/src/prompts.js +18 -0
  8. package/templates/common/.claude/commands/wf-command-create.md +58 -0
  9. package/templates/common/.claude/commands/wf-help.md +4 -0
  10. package/templates/common/.claude/commands/wf-task-archive.md +26 -0
  11. package/templates/common/.claude/commands/wf-task-list.md +24 -0
  12. package/templates/common/.claude/commands/wf-task-record.md +24 -0
  13. package/templates/common/.claude/rules/ecc/common.md +1 -1
  14. package/templates/common/.claude/skills/wf-agents-docs/SKILL.md +15 -30
  15. package/templates/common/.claude/skills/wf-command-create/SKILL.md +37 -0
  16. package/templates/common/.claude/skills/wf-max/SKILL.md +1 -1
  17. package/templates/common/.claude/skills/wf-review/SKILL.md +29 -2
  18. package/templates/common/.claude/skills/wf-task-archive/SKILL.md +28 -0
  19. package/templates/common/.claude/skills/wf-task-list/SKILL.md +28 -0
  20. package/templates/common/.claude/skills/wf-task-record/SKILL.md +28 -0
  21. package/templates/common/.harness-version +66 -32
  22. package/templates/common/.opencode/commands/wf-command-create.md +61 -0
  23. package/templates/common/.opencode/commands/wf-help.md +4 -0
  24. package/templates/common/.opencode/commands/wf-task-archive.md +29 -0
  25. package/templates/common/.opencode/commands/wf-task-list.md +27 -0
  26. package/templates/common/.opencode/commands/wf-task-record.md +27 -0
  27. package/templates/common/CLAUDE.md +8 -6
  28. package/templates/common/Harness/MEMORY.md +9 -0
  29. package/templates/common/Harness/README.md +17 -36
  30. package/templates/common/Harness/ownership.manifest.json +87 -2
  31. package/templates/common/Harness/scripts/task-state.mjs +395 -5
  32. package/templates/common/Harness/scripts/validate-harness.mjs +411 -46
  33. package/templates/common/Harness/scripts/wf-remove.mjs +34 -2
  34. package/templates/common/Harness/specs/guides/SETUP.md +8 -0
  35. package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +15 -0
  36. package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +9 -3
  37. package/templates/common/Harness/specs/runtime/command-surface.json +215 -0
  38. package/templates/common/Harness/specs/runtime/subagents.md +6 -0
  39. package/templates/common/Harness/specs/workflows/WF-MAX.md +5 -0
  40. package/templates/common/Harness/specs/workflows/WF-STATE.md +66 -0
  41. package/templates/common/Harness/tasks/_template/STATE.json +6 -0
@@ -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,38 +64,26 @@ const commonAgents = [
37
64
  ];
38
65
 
39
66
  const commonSkills = [
40
- 'wf',
41
- 'wf-help',
42
67
  'tdd',
43
- 'wf-update',
44
- 'wf-max',
45
- 'wf-review',
46
- 'wf-learn',
47
- 'wf-browser',
48
68
  'subagent-orchestrator',
49
- 'wf-readme',
50
69
  'wf-agents-docs',
51
- 'wf-remove',
52
- 'wf-auto',
53
- 'wf-auto-spark',
70
+ ...commandSkillNames,
54
71
  ];
55
72
 
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
- ];
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);
67
80
 
68
- const opencodeWorkflowCommands = workflowCommands;
81
+ const opencodeWorkflowCommands = commandDefinitions
82
+ .filter(command => command.classification === 'workflow' && command.surfaces?.opencodeCommand)
83
+ .map(command => command.id);
69
84
 
70
85
  const cacheDisciplinedSkills = commonSkills.filter(skill => (
71
- skill === 'subagent-orchestrator' || (skill.startsWith('wf') && skill !== 'wf-help')
86
+ skill === 'subagent-orchestrator' || skill === 'wf-agents-docs' || workflowCommands.includes(skill)
72
87
  ));
73
88
 
74
89
  const memoryFiles = [
@@ -103,12 +118,12 @@ const required = [
103
118
  '.codex/hooks.json',
104
119
  'opencode.json',
105
120
  '.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`),
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`),
112
127
  '.opencode/plugins/harness-wf-status.mjs',
113
128
  '.claude/rules/ecc/common.md',
114
129
  ...commonAgents.map(agent => `.claude/agents/${agent}.md`),
@@ -124,6 +139,7 @@ const required = [
124
139
  'Harness/specs/runtime/dispatch.md',
125
140
  'Harness/specs/guides/extension.md',
126
141
  'Harness/specs/runtime/context-loading.md',
142
+ 'Harness/specs/runtime/command-surface.json',
127
143
  'Harness/ownership.manifest.json',
128
144
  'Harness/specs/workflows/WF-KERNEL.md',
129
145
  'Harness/specs/runtime/agent-workflow.md',
@@ -204,6 +220,48 @@ function read(rel) {
204
220
  return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
205
221
  }
206
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
+
207
265
  function requireText(rel, text, label = text) {
208
266
  const body = read(rel);
209
267
  if (body && !body.includes(text)) errors.push(`${rel} missing ${label}`);
@@ -214,6 +272,160 @@ function forbidText(rel, text, label = text) {
214
272
  if (body && body.includes(text)) errors.push(`${rel} contains forbidden ${label}`);
215
273
  }
216
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
+
217
429
  function activeToml(rel) {
218
430
  return read(rel)
219
431
  .split(/\r?\n/)
@@ -277,6 +489,13 @@ const TASK_VALID_PHASES = new Set([
277
489
  'archived', 'verified',
278
490
  ]);
279
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
+ ]);
280
499
 
281
500
  function validateTaskName(name, strict) {
282
501
  if (TASK_RESERVED.has(name)) return null;
@@ -385,7 +604,7 @@ function registeredWorkflowFiles(...texts) {
385
604
 
386
605
  function listedWorkflowCommands(...texts) {
387
606
  const commands = new Set();
388
- const pattern = /`\/(wf(?:-[a-z0-9]+)?)(?:\s+[^`]*)?`/g;
607
+ const pattern = /`\/(wf(?:-[a-z0-9]+)*)(?:\s+[^`]*)?`/g;
389
608
 
390
609
  for (const text of texts) {
391
610
  for (const match of text.matchAll(pattern)) {
@@ -398,12 +617,153 @@ function listedWorkflowCommands(...texts) {
398
617
  return [...commands].sort();
399
618
  }
400
619
 
401
- for (const rel of required) {
402
- if (!fs.existsSync(path.join(root, rel))) {
403
- 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
+ }
404
752
  }
405
753
  }
406
754
 
755
+ if (isGlobalRuntimeRoot) {
756
+ validateGlobalRuntimeRoot();
757
+ }
758
+
759
+ if (isGlobalProjectBridge) {
760
+ validateGlobalProjectBridge();
761
+ }
762
+
763
+ validateCommandSurface();
764
+
765
+ validateRequiredFiles(required);
766
+
407
767
  for (const rel of legacyRootSpecDocs) {
408
768
  if (fs.existsSync(path.join(root, rel))) {
409
769
  errors.push(`legacy root Harness spec doc should be migrated to Harness/specs/**: ${rel}`);
@@ -526,6 +886,14 @@ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Memory Candidate Dete
526
886
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'explicit user preference', 'explicit user preference immediate write rule');
527
887
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Memory Routing (L3)', 'memory routing section');
528
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');
529
897
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'same tool or command pattern fails 3+ times', 'tool reflection trigger');
530
898
  requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'user corrects the same assumption or preference 2+ times', 'user correction reflection trigger');
531
899
  requireText('CLAUDE.md', 'startup-hints.md', 'CLAUDE startup-hints routing');
@@ -556,7 +924,7 @@ requireText('CLAUDE.md', 'Keep intermediate user updates to 1-2 short sentences'
556
924
  requireText('.claude/rules/ecc/common.md', '## Low-Noise Progress', 'ECC low-noise progress section');
557
925
  requireText('.claude/rules/ecc/common.md', "Match the user's language for user-facing prose", 'ECC user-facing language match rule');
558
926
  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');
927
+ requireText('Harness/specs/runtime/command-surface.json', '"wf-command-create"', 'command surface registry wf-command-create entry');
560
928
  requireText('Harness/README.md', 'Load By Task', 'Harness task router');
561
929
  requireText('Harness/README.md', 'Need context/cache/token efficiency', 'cache/token router row');
562
930
  requireText('Harness/specs/runtime/context-loading.md', 'Context Tiers', 'context tier load budget section');
@@ -578,6 +946,12 @@ requireText('Harness/specs/runtime/subagents.md', 'Cache-first discipline', 'sub
578
946
  forbidText('CLAUDE.md', 'Harness/specs/guides/SETUP.md', 'CLAUDE.md SETUP reference');
579
947
  forbidText('CLAUDE.md', 'follow it before normal project work', 'installed-project SETUP hot-path routing');
580
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');
581
955
  requireText('Harness/specs/guides/SETUP.md', 'no startup dependency on this setup reference', 'SETUP startup boundary');
582
956
  forbidText('Harness/specs/guides/SETUP.md', 'bootstrap contract line', 'stale SETUP-to-CLAUDE bootstrap contract');
583
957
  forbidText('Harness/specs/runtime/context-loading.md', 'Always keep:', 'ambiguous always-load context rule');
@@ -1290,18 +1664,9 @@ if (activeStateTasks.length > 1) {
1290
1664
  const OUTER_TASK_CAP = 5;
1291
1665
  const outerTasks = taskDirs.filter(name => !TASK_RESERVED.has(name) && !name.startsWith('_'));
1292
1666
  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)`;
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)`;
1294
1668
  if (strict) errors.push(capMsg);
1295
1669
  else console.warn(`Warning: ${capMsg}`);
1296
1670
  }
1297
1671
 
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
- }
1672
+ finishValidation();
@@ -141,12 +141,34 @@ const BUILT_IN_SKILL_NAMES = [
141
141
  'wf-auto',
142
142
  'wf-auto-spark',
143
143
  'wf-browser',
144
+ 'wf-command-create',
144
145
  'wf-help',
145
146
  'wf-learn',
146
147
  'wf-max',
147
148
  'wf-readme',
148
149
  'wf-remove',
149
150
  'wf-review',
151
+ 'wf-task-archive',
152
+ 'wf-task-list',
153
+ 'wf-task-record',
154
+ 'wf-update',
155
+ ];
156
+
157
+ const BUILT_IN_COMMAND_NAMES = [
158
+ 'wf',
159
+ 'wf-auto',
160
+ 'wf-auto-spark',
161
+ 'wf-browser',
162
+ 'wf-command-create',
163
+ 'wf-help',
164
+ 'wf-learn',
165
+ 'wf-max',
166
+ 'wf-readme',
167
+ 'wf-remove',
168
+ 'wf-review',
169
+ 'wf-task-archive',
170
+ 'wf-task-list',
171
+ 'wf-task-record',
150
172
  'wf-update',
151
173
  ];
152
174
 
@@ -159,8 +181,10 @@ const KNOWN_FRAMEWORK_FILES = new Set([
159
181
  `.claude/skills/${name}/SKILL.md`,
160
182
  `.agents/skills/${name}/SKILL.md`,
161
183
  ]),
162
- '.claude/commands/wf-help.md',
163
- '.opencode/commands/wf-help.md',
184
+ ...BUILT_IN_COMMAND_NAMES.flatMap(name => [
185
+ `.claude/commands/${name}.md`,
186
+ `.opencode/commands/${name}.md`,
187
+ ]),
164
188
  '.claude/rules/ecc/common.md',
165
189
  'opencode.json',
166
190
  ]);
@@ -177,6 +201,7 @@ const CLEANUP_DIRS = [
177
201
  '.claude/skills/wf-auto',
178
202
  '.claude/skills/wf-auto-spark',
179
203
  '.claude/skills/wf-browser',
204
+ '.claude/skills/wf-command-create',
180
205
  '.claude/skills/wf-help',
181
206
  '.claude/skills/tdd',
182
207
  '.claude/skills/ts-react-frontend',
@@ -187,6 +212,9 @@ const CLEANUP_DIRS = [
187
212
  '.claude/skills/wf-max',
188
213
  '.claude/skills/wf-readme',
189
214
  '.claude/skills/wf-review',
215
+ '.claude/skills/wf-task-archive',
216
+ '.claude/skills/wf-task-list',
217
+ '.claude/skills/wf-task-record',
190
218
  '.claude/skills/wf-update',
191
219
  '.claude/skills/wf-remove',
192
220
  '.claude/skills/subagent-orchestrator',
@@ -196,6 +224,7 @@ const CLEANUP_DIRS = [
196
224
  '.agents/skills/wf-auto',
197
225
  '.agents/skills/wf-auto-spark',
198
226
  '.agents/skills/wf-browser',
227
+ '.agents/skills/wf-command-create',
199
228
  '.agents/skills/wf-help',
200
229
  '.agents/skills/tdd',
201
230
  '.agents/skills/ts-react-frontend',
@@ -206,6 +235,9 @@ const CLEANUP_DIRS = [
206
235
  '.agents/skills/wf-max',
207
236
  '.agents/skills/wf-readme',
208
237
  '.agents/skills/wf-review',
238
+ '.agents/skills/wf-task-archive',
239
+ '.agents/skills/wf-task-list',
240
+ '.agents/skills/wf-task-record',
209
241
  '.agents/skills/wf-update',
210
242
  '.agents/skills/wf-remove',
211
243
  '.agents/skills/subagent-orchestrator',
@@ -55,6 +55,14 @@ Claude or Codex must follow this order during bootstrap. This sequence is broade
55
55
 
56
56
  Before writing, identify the project state:
57
57
 
58
+ Choose the install scope before scaffold writes:
59
+
60
+ - `--install-scope project` (default): full Harness scaffold is project-local.
61
+ - `--install-scope global`: write shared runtime assets to the selected global Harness directory and copy Claude Code, Codex, and OpenCode command/skill surfaces to host-global directories while keeping `Harness/tasks/`, `Harness/PROGRESS.md`, project memory, research, architecture, and project settings under the target project.
62
+ - Use `--global-dir <dir>` when the global runtime location must be explicit.
63
+ - Use `--host-global-dir <dir>` only when the host-global copy base must be explicit; the installer creates `claude/`, `codex/`, and `opencode/` subdirectories under it.
64
+ - Project settings in `Harness/settings.json` override global settings defaults. Existing user-authored files at config, command, skill, or agent paths are preserved or surfaced for review unless they carry a Harness ownership marker.
65
+
58
66
  | Project state | Required action |
59
67
  | --- | --- |
60
68
  | Empty or new project | Run the scaffold, then follow this file for 0-1 bootstrap |