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.
- package/CHANGELOG.md +9 -0
- package/README-CN.md +2 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/generator.js +613 -97
- package/src/index.js +173 -42
- package/src/prompts.js +18 -0
- package/templates/common/.claude/commands/wf-command-create.md +58 -0
- package/templates/common/.claude/commands/wf-help.md +4 -0
- package/templates/common/.claude/commands/wf-task-archive.md +26 -0
- package/templates/common/.claude/commands/wf-task-list.md +24 -0
- package/templates/common/.claude/commands/wf-task-record.md +24 -0
- package/templates/common/.claude/rules/ecc/common.md +1 -1
- package/templates/common/.claude/skills/wf-agents-docs/SKILL.md +15 -30
- package/templates/common/.claude/skills/wf-command-create/SKILL.md +37 -0
- package/templates/common/.claude/skills/wf-max/SKILL.md +1 -1
- package/templates/common/.claude/skills/wf-review/SKILL.md +29 -2
- package/templates/common/.claude/skills/wf-task-archive/SKILL.md +28 -0
- package/templates/common/.claude/skills/wf-task-list/SKILL.md +28 -0
- package/templates/common/.claude/skills/wf-task-record/SKILL.md +28 -0
- package/templates/common/.harness-version +66 -32
- package/templates/common/.opencode/commands/wf-command-create.md +61 -0
- package/templates/common/.opencode/commands/wf-help.md +4 -0
- package/templates/common/.opencode/commands/wf-task-archive.md +29 -0
- package/templates/common/.opencode/commands/wf-task-list.md +27 -0
- package/templates/common/.opencode/commands/wf-task-record.md +27 -0
- package/templates/common/CLAUDE.md +8 -6
- package/templates/common/Harness/MEMORY.md +9 -0
- package/templates/common/Harness/README.md +17 -36
- package/templates/common/Harness/ownership.manifest.json +87 -2
- package/templates/common/Harness/scripts/task-state.mjs +395 -5
- package/templates/common/Harness/scripts/validate-harness.mjs +411 -46
- package/templates/common/Harness/scripts/wf-remove.mjs +34 -2
- package/templates/common/Harness/specs/guides/SETUP.md +8 -0
- package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +15 -0
- package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +9 -3
- package/templates/common/Harness/specs/runtime/command-surface.json +215 -0
- package/templates/common/Harness/specs/runtime/subagents.md +6 -0
- package/templates/common/Harness/specs/workflows/WF-MAX.md +5 -0
- package/templates/common/Harness/specs/workflows/WF-STATE.md +66 -0
- 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
|
-
|
|
52
|
-
'wf-auto',
|
|
53
|
-
'wf-auto-spark',
|
|
70
|
+
...commandSkillNames,
|
|
54
71
|
];
|
|
55
72
|
|
|
56
|
-
const workflowCommands =
|
|
57
|
-
'
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
'
|
|
62
|
-
|
|
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 =
|
|
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' ||
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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]+)
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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('
|
|
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});
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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 |
|