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.
- package/CHANGELOG.md +18 -0
- package/README-CN.md +100 -127
- package/README.md +63 -42
- package/package.json +1 -1
- package/src/generator.js +628 -121
- package/src/index.js +174 -43
- package/src/prompts.js +18 -0
- package/templates/common/.claude/commands/wf-auto-spark.md +16 -0
- package/templates/common/.claude/commands/wf-auto.md +16 -0
- package/templates/{optional/skills/browser-e2e/.opencode → common/.claude}/commands/wf-browser.md +1 -1
- package/templates/common/.claude/commands/wf-command-create.md +58 -0
- package/templates/common/.claude/commands/wf-help.md +13 -6
- package/templates/common/.claude/commands/wf-learn.md +16 -0
- package/templates/common/.claude/commands/wf-max.md +20 -0
- package/templates/common/.claude/commands/wf-readme.md +16 -0
- package/templates/common/.claude/commands/wf-remove.md +16 -0
- package/templates/common/.claude/commands/wf-review.md +16 -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/commands/wf.md +16 -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-browser/SKILL.md +176 -0
- package/templates/common/.claude/skills/wf-command-create/SKILL.md +37 -0
- package/templates/common/.claude/skills/wf-help/SKILL.md +30 -0
- package/templates/common/.claude/skills/wf-max/SKILL.md +22 -8
- 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/.codex/config.toml +3 -5
- package/templates/common/.harness-version +108 -41
- package/templates/common/.opencode/agents/architect-manager.md +7 -1
- package/templates/common/.opencode/agents/explore-manager.md +10 -1
- package/templates/common/.opencode/agents/implement-manager.md +5 -3
- package/templates/common/.opencode/agents/review-manager.md +5 -2
- package/templates/common/.opencode/commands/wf-browser.md +16 -0
- package/templates/common/.opencode/commands/wf-command-create.md +61 -0
- package/templates/common/.opencode/commands/wf-help.md +13 -6
- package/templates/common/.opencode/commands/wf-max.md +12 -7
- 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 +9 -7
- package/templates/common/Harness/MEMORY.md +12 -3
- package/templates/common/Harness/README.md +23 -41
- package/templates/common/Harness/ownership.manifest.json +162 -14
- package/templates/common/Harness/scripts/archive-tasks.mjs +12 -220
- package/templates/common/Harness/scripts/scan-clean.mjs +3 -9
- package/templates/common/Harness/scripts/task-state.mjs +1279 -0
- package/templates/common/Harness/scripts/validate-harness.mjs +635 -65
- package/templates/common/Harness/scripts/wf-remove.mjs +37 -5
- package/templates/common/Harness/scripts/wf-update-check.mjs +3 -0
- package/templates/common/Harness/specs/guides/SETUP.md +10 -2
- package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +15 -0
- package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +49 -23
- package/templates/common/Harness/specs/runtime/command-surface.json +215 -0
- package/templates/common/Harness/specs/runtime/dispatch.md +2 -2
- package/templates/common/Harness/specs/runtime/subagents.md +15 -5
- package/templates/common/Harness/specs/workflows/WF-AUTO.md +6 -6
- package/templates/common/Harness/specs/workflows/WF-KERNEL.md +1 -1
- package/templates/common/Harness/specs/workflows/WF-MAX.md +35 -2
- package/templates/common/Harness/specs/workflows/WF-STATE.md +155 -36
- package/templates/common/Harness/tasks/_template/STATE.json +6 -0
- package/templates/common/opencode.json +1 -0
- package/templates/optional/catalog.json +2 -9
- package/templates/optional/skills/browser-e2e/.claude/skills/browser-e2e/SKILL.md +0 -42
- package/templates/optional/skills/browser-e2e/.claude/skills/wf-browser/SKILL.md +0 -201
- 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
|
-
|
|
50
|
-
'wf-auto',
|
|
51
|
-
'wf-auto-spark',
|
|
70
|
+
...commandSkillNames,
|
|
52
71
|
];
|
|
53
72
|
|
|
54
|
-
const
|
|
55
|
-
'
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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]+)
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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('
|
|
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 (
|
|
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('
|
|
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
|
|
788
|
-
requireText('Harness/specs/workflows/WF-MAX.md', '
|
|
789
|
-
requireText('Harness/specs/workflows/WF-MAX.md', '
|
|
790
|
-
requireText('Harness/specs/workflows/WF-MAX.md', '
|
|
791
|
-
requireText('Harness/specs/workflows/WF-MAX.md', '
|
|
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.
|
|
797
|
-
requireText('.claude/skills/wf-max/SKILL.md', 'max_threads
|
|
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', '
|
|
805
|
-
|
|
806
|
-
|
|
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/
|
|
901
|
-
requireText('Harness/specs/workflows/WF-AUTO.md', '
|
|
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
|
|
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
|
|
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
|
|
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', '
|
|
992
|
-
requireText('.
|
|
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
|
|
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
|
-
|
|
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();
|