create-harness-vibe-coding 0.8.17 → 0.8.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README-CN.md +2 -0
- package/README.md +3 -0
- package/package.json +5 -2
- package/src/generator.js +613 -97
- package/src/index.js +556 -309
- package/src/prompts.js +18 -0
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
- package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
- package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
- package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
- package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
- package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
- package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
- package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
- package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
- package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
- package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
- package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
- package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
- package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
- package/src/ui/dist/index.html +18 -0
- package/src/ui/index.html +17 -0
- package/src/ui/package.json +33 -0
- package/src/ui/pnpm-lock.yaml +1582 -0
- package/src/ui/pnpm-workspace.yaml +2 -0
- package/src/ui/src/App.tsx +86 -0
- package/src/ui/src/api.ts +93 -0
- package/src/ui/src/components/AgentsRoute.tsx +503 -0
- package/src/ui/src/components/Footer.tsx +69 -0
- package/src/ui/src/components/Header.tsx +175 -0
- package/src/ui/src/components/LoadingView.tsx +22 -0
- package/src/ui/src/components/RolesRoute.tsx +169 -0
- package/src/ui/src/components/SettingsRoute.tsx +166 -0
- package/src/ui/src/components/TaskList.tsx +388 -0
- package/src/ui/src/components/TerminalDrawer.tsx +611 -0
- package/src/ui/src/components/WorkflowRoute.tsx +670 -0
- package/src/ui/src/hooks/useReducedMotion.ts +17 -0
- package/src/ui/src/hooks/useServerConnection.ts +114 -0
- package/src/ui/src/index.css +281 -0
- package/src/ui/src/main.tsx +11 -0
- package/src/ui/src/types.ts +108 -0
- package/src/ui/tsconfig.json +21 -0
- package/src/ui/vite.config.ts +19 -0
- package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
- package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
- package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
- package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
- package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
- package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
- package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
- package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
- package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
- package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
- package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
- package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
- package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
- package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
- package/src/wf-ui-server/a2a-store.mjs +296 -0
- package/src/wf-ui-server/peer-capsule.mjs +213 -0
- package/src/wf-ui-server/pty-adapter.mjs +155 -0
- package/src/wf-ui-server/runtime-config.mjs +153 -0
- package/src/wf-ui-server/runtime-detector.mjs +374 -0
- package/src/wf-ui-server/security.mjs +67 -0
- package/src/wf-ui-server/server.mjs +849 -0
- package/src/wf-ui-server/session-registry.mjs +204 -0
- package/src/wf-ui-server/settings.mjs +100 -0
- package/src/wf-ui-server/task-parser.mjs +133 -0
- package/src/wf-ui-server/terminal-store.mjs +225 -0
- package/src/wf-ui-server/token.mjs +41 -0
- package/src/wf-ui-server/ws-events.mjs +354 -0
- package/src/wf-ui-server/ws-terminal.mjs +473 -0
- package/templates/common/.claude/commands/wf-command-create.md +58 -0
- package/templates/common/.claude/commands/wf-help.md +5 -0
- package/templates/common/.claude/commands/wf-task-archive.md +26 -0
- package/templates/common/.claude/commands/wf-task-list.md +26 -0
- package/templates/common/.claude/commands/wf-task-record.md +24 -0
- package/templates/common/.claude/commands/wf-ui.md +26 -0
- package/templates/common/.claude/commands/wf-update.md +54 -9
- 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-auto/SKILL.md +3 -3
- package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
- 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/.claude/skills/wf-ui/SKILL.md +78 -0
- package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
- package/templates/common/.harness-version +105 -47
- package/templates/common/.opencode/commands/wf-command-create.md +61 -0
- package/templates/common/.opencode/commands/wf-help.md +5 -0
- package/templates/common/.opencode/commands/wf-task-archive.md +29 -0
- package/templates/common/.opencode/commands/wf-task-list.md +29 -0
- package/templates/common/.opencode/commands/wf-task-record.md +27 -0
- package/templates/common/.opencode/commands/wf-ui.md +26 -0
- package/templates/common/.opencode/commands/wf-update.md +54 -9
- package/templates/common/CLAUDE.md +8 -6
- package/templates/common/Harness/MEMORY.md +11 -0
- package/templates/common/Harness/README.md +21 -38
- package/templates/common/Harness/a2a/role-graph.json +79 -0
- package/templates/common/Harness/a2a/runtime-registry.json +5 -0
- package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
- package/templates/common/Harness/ownership.manifest.json +142 -2
- package/templates/common/Harness/scripts/README.md +134 -0
- package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
- package/templates/common/Harness/scripts/context-budget.mjs +1 -1
- package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
- package/templates/common/Harness/scripts/task-state.mjs +949 -26
- package/templates/common/Harness/scripts/validate-harness.mjs +637 -62
- package/templates/common/Harness/scripts/wf-remove.mjs +42 -5
- package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
- package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
- package/templates/common/Harness/settings.json +35 -0
- 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 +16 -5
- package/templates/common/Harness/specs/runtime/command-surface.json +229 -0
- package/templates/common/Harness/specs/runtime/subagents.md +6 -0
- package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
- package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
- 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/NAMING.md +16 -16
- package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
- package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
- package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
- package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
- package/templates/common/Harness/tasks/_template/STATE.json +6 -0
- package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
- package/templates/common/Harness/tasks/_template/NOTES.md +0 -3
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
4
5
|
|
|
5
6
|
const root = process.cwd();
|
|
6
7
|
const args = new Set(process.argv.slice(2));
|
|
7
8
|
const strict = args.has('--strict') || args.has('--post-bootstrap');
|
|
9
|
+
const manifestAudit = args.has('--manifest-audit');
|
|
8
10
|
|
|
9
11
|
if (args.has('--help') || args.has('-h')) {
|
|
10
|
-
console.log(`Usage: node Harness/scripts/validate-harness.mjs [--strict]
|
|
12
|
+
console.log(`Usage: node Harness/scripts/validate-harness.mjs [--strict] [--manifest-audit]
|
|
11
13
|
|
|
12
14
|
Default mode checks scaffold structure, links, agents, and skills.
|
|
13
15
|
--strict also fails when project fact docs still contain unresolved {{TOKEN}} placeholders.
|
|
14
|
-
|
|
16
|
+
--manifest-audit cross-references ownership.manifest.json against disk: flags missing framework files (manifest-to-disk) and extra files in harness directories (disk-to-manifest).`);
|
|
15
17
|
process.exit(0);
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
let commandSurfaceLoadError = null;
|
|
21
|
+
|
|
22
|
+
function loadCommandSurface() {
|
|
23
|
+
const rel = path.join(root, 'Harness', 'specs', 'runtime', 'command-surface.json');
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(fs.readFileSync(rel, 'utf8'));
|
|
26
|
+
if (!parsed || parsed.schemaVersion !== 1 || !Array.isArray(parsed.commands)) {
|
|
27
|
+
commandSurfaceLoadError = 'Harness/specs/runtime/command-surface.json must have schemaVersion 1 and commands[]';
|
|
28
|
+
return { commands: [] };
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
commandSurfaceLoadError = `Harness/specs/runtime/command-surface.json is not valid JSON: ${err.message}`;
|
|
33
|
+
return { commands: [] };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function uniqueSorted(values) {
|
|
38
|
+
return [...new Set(values)].sort();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const commandSurface = loadCommandSurface();
|
|
42
|
+
const commandDefinitions = commandSurface.commands;
|
|
43
|
+
const commandSkillNames = commandDefinitions
|
|
44
|
+
.filter(command => command.surfaces?.claudeSkill || command.surfaces?.codexSkill)
|
|
45
|
+
.map(command => command.id);
|
|
46
|
+
|
|
18
47
|
const commonAgents = [
|
|
19
48
|
'task-scribe',
|
|
20
49
|
'codebase-explorer',
|
|
@@ -37,38 +66,26 @@ const commonAgents = [
|
|
|
37
66
|
];
|
|
38
67
|
|
|
39
68
|
const commonSkills = [
|
|
40
|
-
'wf',
|
|
41
|
-
'wf-help',
|
|
42
69
|
'tdd',
|
|
43
|
-
'wf-update',
|
|
44
|
-
'wf-max',
|
|
45
|
-
'wf-review',
|
|
46
|
-
'wf-learn',
|
|
47
|
-
'wf-browser',
|
|
48
70
|
'subagent-orchestrator',
|
|
49
|
-
'wf-readme',
|
|
50
71
|
'wf-agents-docs',
|
|
51
|
-
|
|
52
|
-
'wf-auto',
|
|
53
|
-
'wf-auto-spark',
|
|
72
|
+
...commandSkillNames,
|
|
54
73
|
];
|
|
55
74
|
|
|
56
|
-
const workflowCommands =
|
|
57
|
-
'
|
|
58
|
-
|
|
59
|
-
'wf-auto',
|
|
60
|
-
'wf-auto-spark',
|
|
61
|
-
'wf-learn',
|
|
62
|
-
'wf-review',
|
|
63
|
-
'wf-browser',
|
|
64
|
-
'wf-readme',
|
|
65
|
-
'wf-remove',
|
|
66
|
-
];
|
|
75
|
+
const workflowCommands = commandDefinitions
|
|
76
|
+
.filter(command => command.classification === 'workflow' && command.surfaces?.claudeCommand)
|
|
77
|
+
.map(command => command.id);
|
|
67
78
|
|
|
68
|
-
const
|
|
79
|
+
const directCommands = commandDefinitions
|
|
80
|
+
.filter(command => command.classification === 'direct')
|
|
81
|
+
.map(command => command.id);
|
|
82
|
+
|
|
83
|
+
const opencodeWorkflowCommands = commandDefinitions
|
|
84
|
+
.filter(command => command.classification === 'workflow' && command.surfaces?.opencodeCommand)
|
|
85
|
+
.map(command => command.id);
|
|
69
86
|
|
|
70
87
|
const cacheDisciplinedSkills = commonSkills.filter(skill => (
|
|
71
|
-
skill === 'subagent-orchestrator' ||
|
|
88
|
+
skill === 'subagent-orchestrator' || skill === 'wf-agents-docs' || workflowCommands.includes(skill)
|
|
72
89
|
));
|
|
73
90
|
|
|
74
91
|
const memoryFiles = [
|
|
@@ -103,12 +120,12 @@ const required = [
|
|
|
103
120
|
'.codex/hooks.json',
|
|
104
121
|
'opencode.json',
|
|
105
122
|
'.claude/settings.json',
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
123
|
+
...commandDefinitions
|
|
124
|
+
.filter(command => command.surfaces?.claudeCommand)
|
|
125
|
+
.map(command => `.claude/commands/${command.id}.md`),
|
|
126
|
+
...commandDefinitions
|
|
127
|
+
.filter(command => command.surfaces?.opencodeCommand)
|
|
128
|
+
.map(command => `.opencode/commands/${command.id}.md`),
|
|
112
129
|
'.opencode/plugins/harness-wf-status.mjs',
|
|
113
130
|
'.claude/rules/ecc/common.md',
|
|
114
131
|
...commonAgents.map(agent => `.claude/agents/${agent}.md`),
|
|
@@ -124,6 +141,7 @@ const required = [
|
|
|
124
141
|
'Harness/specs/runtime/dispatch.md',
|
|
125
142
|
'Harness/specs/guides/extension.md',
|
|
126
143
|
'Harness/specs/runtime/context-loading.md',
|
|
144
|
+
'Harness/specs/runtime/command-surface.json',
|
|
127
145
|
'Harness/ownership.manifest.json',
|
|
128
146
|
'Harness/specs/workflows/WF-KERNEL.md',
|
|
129
147
|
'Harness/specs/runtime/agent-workflow.md',
|
|
@@ -134,8 +152,10 @@ const required = [
|
|
|
134
152
|
'Harness/scripts/context-budget.mjs',
|
|
135
153
|
'Harness/scripts/l2-cache-telemetry.mjs',
|
|
136
154
|
'Harness/scripts/wf-update-check.mjs',
|
|
155
|
+
'Harness/scripts/wf-update-runner.mjs',
|
|
137
156
|
'Harness/scripts/wf-remove.mjs',
|
|
138
157
|
'Harness/scripts/scan-clean.mjs',
|
|
158
|
+
'Harness/scripts/sync-host-global.mjs',
|
|
139
159
|
'Harness/scripts/task-state.mjs',
|
|
140
160
|
'Harness/scripts/archive-tasks.mjs',
|
|
141
161
|
'Harness/specs/workflows/WF-STATE.md',
|
|
@@ -204,6 +224,90 @@ function read(rel) {
|
|
|
204
224
|
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
205
225
|
}
|
|
206
226
|
|
|
227
|
+
function readJson(rel) {
|
|
228
|
+
const body = read(rel);
|
|
229
|
+
if (!body) return null;
|
|
230
|
+
try {
|
|
231
|
+
return JSON.parse(body);
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function sameResolvedPath(a, b) {
|
|
238
|
+
if (!a || !b) return false;
|
|
239
|
+
const resolvedA = path.resolve(a);
|
|
240
|
+
const resolvedB = path.resolve(b);
|
|
241
|
+
return process.platform === 'win32'
|
|
242
|
+
? resolvedA.toLowerCase() === resolvedB.toLowerCase()
|
|
243
|
+
: resolvedA === resolvedB;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isGlobalRuntimeProjectStatePath(rel) {
|
|
247
|
+
return rel === 'Harness/PROGRESS.md'
|
|
248
|
+
|| rel === 'Harness/tasks'
|
|
249
|
+
|| rel.startsWith('Harness/tasks/')
|
|
250
|
+
|| rel === 'Harness/research'
|
|
251
|
+
|| rel.startsWith('Harness/research/')
|
|
252
|
+
|| rel === 'Harness/project'
|
|
253
|
+
|| rel.startsWith('Harness/project/');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function validateRequiredFiles(files, label = 'file') {
|
|
257
|
+
for (const rel of files) {
|
|
258
|
+
if (!fs.existsSync(path.join(root, rel))) {
|
|
259
|
+
errors.push(`missing required ${label}: ${rel}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveMetadataPath(value) {
|
|
265
|
+
if (!value || typeof value !== 'string') return '';
|
|
266
|
+
return path.isAbsolute(value) ? value : path.resolve(root, value);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function containsPath(parent, child) {
|
|
270
|
+
const resolvedParent = path.resolve(parent);
|
|
271
|
+
const resolvedChild = path.resolve(child);
|
|
272
|
+
const rel = path.relative(resolvedParent, resolvedChild);
|
|
273
|
+
return rel === '' || (rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function resolveUnder(base, rel) {
|
|
277
|
+
if (!rel || typeof rel !== 'string' || path.isAbsolute(rel)) return null;
|
|
278
|
+
const parts = rel.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
279
|
+
if (parts.some(part => part === '..')) return null;
|
|
280
|
+
const resolved = path.resolve(base, ...parts);
|
|
281
|
+
return containsPath(base, resolved) ? resolved : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function sha256Content(content) {
|
|
285
|
+
return 'sha256-' + createHash('sha256').update(content.replace(/\r\n/g, '\n')).digest('hex');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function sha256File(filePath) {
|
|
289
|
+
if (!fs.existsSync(filePath)) return null;
|
|
290
|
+
return sha256Content(fs.readFileSync(filePath, 'utf8'));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function sourceDestForHostFile(host, file) {
|
|
294
|
+
const normalized = String(file || '').replace(/\\/g, '/');
|
|
295
|
+
if (host === 'claude') {
|
|
296
|
+
if (normalized === 'settings.json') return '.claude/settings.json';
|
|
297
|
+
if (/^(commands|skills|agents|rules)\//.test(normalized)) return `.claude/${normalized}`;
|
|
298
|
+
}
|
|
299
|
+
if (host === 'codex') {
|
|
300
|
+
if (normalized === 'config.toml') return '.codex/config.toml';
|
|
301
|
+
if (normalized === 'hooks.json') return '.codex/hooks.json';
|
|
302
|
+
if (normalized.startsWith('skills/')) return `.agents/${normalized}`;
|
|
303
|
+
}
|
|
304
|
+
if (host === 'opencode') {
|
|
305
|
+
if (normalized === 'opencode.json') return 'opencode.json';
|
|
306
|
+
if (/^(commands|agents|plugins)\//.test(normalized)) return `.opencode/${normalized}`;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
207
311
|
function requireText(rel, text, label = text) {
|
|
208
312
|
const body = read(rel);
|
|
209
313
|
if (body && !body.includes(text)) errors.push(`${rel} missing ${label}`);
|
|
@@ -214,6 +318,177 @@ function forbidText(rel, text, label = text) {
|
|
|
214
318
|
if (body && body.includes(text)) errors.push(`${rel} contains forbidden ${label}`);
|
|
215
319
|
}
|
|
216
320
|
|
|
321
|
+
const installMetadata = readJson('Harness/.harness-version');
|
|
322
|
+
const isGlobalInstall = installMetadata?.installScope === 'global';
|
|
323
|
+
const isGlobalRuntimeRoot = isGlobalInstall && sameResolvedPath(root, installMetadata.globalDir);
|
|
324
|
+
const isGlobalProjectBridge = isGlobalInstall && !isGlobalRuntimeRoot;
|
|
325
|
+
|
|
326
|
+
function finishValidation() {
|
|
327
|
+
if (errors.length) {
|
|
328
|
+
console.error(`Harness validation failed${strict ? ' (strict)' : ''}:`);
|
|
329
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
console.log(`Harness validation passed${strict ? ' (strict)' : ''}.`);
|
|
334
|
+
if (!strict) {
|
|
335
|
+
console.log('Tip: run `node Harness/scripts/validate-harness.mjs --strict` after bootstrap to check unresolved project placeholders.');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function validateHostGlobalTargets() {
|
|
340
|
+
if (!isGlobalInstall) return;
|
|
341
|
+
|
|
342
|
+
if (installMetadata?.copyMode !== 'copy') {
|
|
343
|
+
errors.push('Harness/.harness-version global install metadata must use copyMode "copy"');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const hostGlobal = installMetadata?.hostGlobal;
|
|
347
|
+
if (!hostGlobal || hostGlobal.copyMode !== 'copy' || !hostGlobal.targets || typeof hostGlobal.targets !== 'object') {
|
|
348
|
+
errors.push('Harness/.harness-version missing hostGlobal copy targets');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const minimumFiles = {
|
|
353
|
+
claude: ['commands/wf.md', 'commands/wf-ui.md', 'skills/wf/SKILL.md', 'skills/wf-ui/SKILL.md'],
|
|
354
|
+
codex: ['skills/wf/SKILL.md', 'skills/wf-ui/SKILL.md'],
|
|
355
|
+
opencode: ['commands/wf.md', 'commands/wf-ui.md'],
|
|
356
|
+
};
|
|
357
|
+
const runtimeRoot = isGlobalRuntimeRoot ? root : resolveMetadataPath(installMetadata.globalDir);
|
|
358
|
+
|
|
359
|
+
for (const [host, requiredFiles] of Object.entries(minimumFiles)) {
|
|
360
|
+
const target = hostGlobal.targets[host];
|
|
361
|
+
if (!target || typeof target !== 'object') {
|
|
362
|
+
errors.push(`Harness/.harness-version missing hostGlobal target: ${host}`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const hostRoot = resolveMetadataPath(target.root);
|
|
367
|
+
if (!hostRoot) {
|
|
368
|
+
errors.push(`Harness/.harness-version hostGlobal ${host} missing root`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const files = Array.isArray(target.files) ? target.files : [];
|
|
373
|
+
for (const requiredFile of requiredFiles) {
|
|
374
|
+
if (!files.includes(requiredFile)) {
|
|
375
|
+
errors.push(`Harness/.harness-version hostGlobal ${host} missing required file target: ${requiredFile}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
for (const file of files) {
|
|
380
|
+
if (typeof file !== 'string' || path.isAbsolute(file) || file.split('/').includes('..')) {
|
|
381
|
+
errors.push(`Harness/.harness-version hostGlobal ${host} has invalid relative file target: ${String(file)}`);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const filePath = path.join(hostRoot, ...file.split('/'));
|
|
385
|
+
let stat;
|
|
386
|
+
try {
|
|
387
|
+
stat = fs.lstatSync(filePath);
|
|
388
|
+
if (stat.isSymbolicLink()) {
|
|
389
|
+
errors.push(`host-global ${host} copied file is a symlink, not a real copy: ${file}`);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (!stat.isFile()) {
|
|
393
|
+
errors.push(`host-global ${host} copied file is not a regular file: ${file}`);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
errors.push(`missing host-global ${host} copied file: ${file}`);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const sourceDest = sourceDestForHostFile(host, file);
|
|
402
|
+
const sourcePath = sourceDest && runtimeRoot ? resolveUnder(runtimeRoot, sourceDest) : null;
|
|
403
|
+
if (!sourceDest || !sourcePath) {
|
|
404
|
+
errors.push(`host-global ${host} copied file has no runtime source mapping: ${file}`);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (!fs.existsSync(sourcePath)) {
|
|
408
|
+
errors.push(`host-global ${host} copied file source is missing: ${file} -> ${sourceDest}`);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const sourceHash = sha256File(sourcePath);
|
|
412
|
+
const targetHash = sha256File(filePath);
|
|
413
|
+
if (sourceHash && targetHash && sourceHash !== targetHash) {
|
|
414
|
+
errors.push(`host-global ${host} copied file is stale: ${file} (source ${sourceDest})`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function validateGlobalRuntimeRoot() {
|
|
421
|
+
validateCommandSurface();
|
|
422
|
+
validateRequiredFiles(required.filter(rel => !isGlobalRuntimeProjectStatePath(rel)), 'global-runtime file');
|
|
423
|
+
|
|
424
|
+
for (const rel of ['Harness/PROGRESS.md', 'Harness/tasks', 'Harness/research', 'Harness/project']) {
|
|
425
|
+
if (fs.existsSync(path.join(root, rel))) {
|
|
426
|
+
errors.push(`global runtime must not contain project-local state: ${rel}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (installMetadata?.projectState?.tasks !== 'Harness/tasks/') {
|
|
431
|
+
errors.push('Harness/.harness-version global runtime metadata must keep projectState.tasks project-local');
|
|
432
|
+
}
|
|
433
|
+
if (installMetadata?.projectState?.progress !== 'Harness/PROGRESS.md') {
|
|
434
|
+
errors.push('Harness/.harness-version global runtime metadata must keep projectState.progress project-local');
|
|
435
|
+
}
|
|
436
|
+
if (installMetadata?.settingsScopes?.precedence?.join('>') !== 'project>global') {
|
|
437
|
+
errors.push('Harness/.harness-version global runtime metadata must define project>global settings precedence');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
validateHostGlobalTargets();
|
|
441
|
+
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project/Global Settings Boundary', 'project/global settings boundary section');
|
|
442
|
+
requireText('Harness/specs/guides/SETUP.md', 'copy Claude Code, Codex, and OpenCode command/skill surfaces', 'setup three-host global copy');
|
|
443
|
+
|
|
444
|
+
finishValidation();
|
|
445
|
+
process.exit(0);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function validateGlobalProjectBridge() {
|
|
449
|
+
const bridgeRequired = [
|
|
450
|
+
'AGENTS.md',
|
|
451
|
+
'CLAUDE.md',
|
|
452
|
+
'Harness/README.md',
|
|
453
|
+
'Harness/MEMORY.md',
|
|
454
|
+
'Harness/settings.json',
|
|
455
|
+
'Harness/specs/guides/SETUP.md',
|
|
456
|
+
'Harness/.harness-version',
|
|
457
|
+
'Harness/PROGRESS.md',
|
|
458
|
+
'Harness/research/README.md',
|
|
459
|
+
'Harness/research/PRD.md',
|
|
460
|
+
'Harness/research/research-results.md',
|
|
461
|
+
'Harness/project/architecture.md',
|
|
462
|
+
...memoryFiles,
|
|
463
|
+
];
|
|
464
|
+
|
|
465
|
+
for (const rel of bridgeRequired) {
|
|
466
|
+
if (!fs.existsSync(path.join(root, rel))) {
|
|
467
|
+
errors.push(`missing required global-project-bridge file: ${rel}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const taskTemplateDir = path.join(root, 'Harness', 'tasks', '_template');
|
|
472
|
+
if (!fs.existsSync(taskTemplateDir)) {
|
|
473
|
+
errors.push('missing directory: Harness/tasks/_template/');
|
|
474
|
+
} else {
|
|
475
|
+
for (const f of ['PROGRESS.md', 'PLAN.md']) {
|
|
476
|
+
if (!fs.existsSync(path.join(taskTemplateDir, f))) {
|
|
477
|
+
errors.push(`missing task template file: Harness/tasks/_template/${f}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
requireText('CLAUDE.md', 'global Harness runtime', 'global runtime bridge pointer');
|
|
483
|
+
requireText('Harness/README.md', 'Never discover task context from the global runtime', 'project-local task authority');
|
|
484
|
+
requireText('Harness/specs/guides/SETUP.md', 'Read the full setup guide from', 'global setup bridge pointer');
|
|
485
|
+
requireText('Harness/MEMORY.md', 'Global memory lives under', 'global memory bridge pointer');
|
|
486
|
+
validateHostGlobalTargets();
|
|
487
|
+
|
|
488
|
+
finishValidation();
|
|
489
|
+
process.exit(0);
|
|
490
|
+
}
|
|
491
|
+
|
|
217
492
|
function activeToml(rel) {
|
|
218
493
|
return read(rel)
|
|
219
494
|
.split(/\r?\n/)
|
|
@@ -254,7 +529,7 @@ function listMarkdownFiles(rel) {
|
|
|
254
529
|
// Reserved: _template (system), auto (auto-mode capsule)
|
|
255
530
|
const TASK_NAME_RE = /^task-[a-z]+(-[a-z0-9]+){1,4}$/;
|
|
256
531
|
const TASK_NAME_MAX = 46; // "task-" (5) + ≤40 chars body + 1 safety = 46
|
|
257
|
-
const TASK_RESERVED = new Set(['_template', 'auto', '_archive']);
|
|
532
|
+
const TASK_RESERVED = new Set(['_template', 'auto', '_archive', 'continuous']);
|
|
258
533
|
const TASK_SAFE_ARCHIVE_STATUSES = new Set(['complete', 'verified', 'archived', 'abandoned', 'obsolete', 'done', 'closed', 'closeout']);
|
|
259
534
|
const TASK_NEVER_ARCHIVE_STATUSES = new Set(['active', 'blocked', 'in_progress', 'running', 'pending', 'needs-user-decision']);
|
|
260
535
|
const TASK_PHASE_ALIASES = new Map([
|
|
@@ -277,6 +552,13 @@ const TASK_VALID_PHASES = new Set([
|
|
|
277
552
|
'archived', 'verified',
|
|
278
553
|
]);
|
|
279
554
|
const TASK_VALID_STATUSES = new Set([...TASK_SAFE_ARCHIVE_STATUSES, ...TASK_NEVER_ARCHIVE_STATUSES, 'skipped', 'failed']);
|
|
555
|
+
const VALID_TASK_CAPSULE_POLICIES = new Set([
|
|
556
|
+
'none',
|
|
557
|
+
'required',
|
|
558
|
+
'auto-capsule-required',
|
|
559
|
+
'use-current-or-create-when-needed',
|
|
560
|
+
'creates-or-updates',
|
|
561
|
+
]);
|
|
280
562
|
|
|
281
563
|
function validateTaskName(name, strict) {
|
|
282
564
|
if (TASK_RESERVED.has(name)) return null;
|
|
@@ -385,7 +667,7 @@ function registeredWorkflowFiles(...texts) {
|
|
|
385
667
|
|
|
386
668
|
function listedWorkflowCommands(...texts) {
|
|
387
669
|
const commands = new Set();
|
|
388
|
-
const pattern = /`\/(wf(?:-[a-z0-9]+)
|
|
670
|
+
const pattern = /`\/(wf(?:-[a-z0-9]+)*)(?:\s+[^`]*)?`/g;
|
|
389
671
|
|
|
390
672
|
for (const text of texts) {
|
|
391
673
|
for (const match of text.matchAll(pattern)) {
|
|
@@ -398,33 +680,300 @@ function listedWorkflowCommands(...texts) {
|
|
|
398
680
|
return [...commands].sort();
|
|
399
681
|
}
|
|
400
682
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
683
|
+
function extractStringArray(text, name) {
|
|
684
|
+
const match = text.match(new RegExp(`const ${name} = \\[([\\s\\S]*?)\\];`));
|
|
685
|
+
if (!match) return null;
|
|
686
|
+
return new Set([...match[1].matchAll(/'([^']+)'/g)].map(item => item[1]));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function expectedCommandAliases(id) {
|
|
690
|
+
return [`/${id}`, `$${id}`, `/skills ${id}`];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function validateCommandSurface() {
|
|
694
|
+
if (commandSurfaceLoadError) {
|
|
695
|
+
errors.push(commandSurfaceLoadError);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const seen = new Set();
|
|
700
|
+
const claudeRouter = read('CLAUDE.md');
|
|
701
|
+
const readmeRouter = read('Harness/README.md');
|
|
702
|
+
const ecc = read('.claude/rules/ecc/common.md');
|
|
703
|
+
const eccExemptionLine = ecc.split(/\r?\n/).find(line => line.includes('excluding')) || '';
|
|
704
|
+
const claudeHelp = read('.claude/commands/wf-help.md');
|
|
705
|
+
const opencodeHelp = read('.opencode/commands/wf-help.md');
|
|
706
|
+
const removeScript = read('Harness/scripts/wf-remove.mjs');
|
|
707
|
+
const removeSkillRegistry = extractStringArray(removeScript, 'BUILT_IN_SKILL_NAMES');
|
|
708
|
+
const removeCommandRegistry = extractStringArray(removeScript, 'BUILT_IN_COMMAND_NAMES');
|
|
709
|
+
const cleanupDirs = extractStringArray(removeScript, 'CLEANUP_DIRS');
|
|
710
|
+
|
|
711
|
+
if (!removeSkillRegistry) errors.push('Harness/scripts/wf-remove.mjs missing BUILT_IN_SKILL_NAMES registry');
|
|
712
|
+
if (!removeCommandRegistry) errors.push('Harness/scripts/wf-remove.mjs missing BUILT_IN_COMMAND_NAMES registry');
|
|
713
|
+
if (!cleanupDirs) errors.push('Harness/scripts/wf-remove.mjs missing CLEANUP_DIRS registry');
|
|
714
|
+
|
|
715
|
+
for (const command of commandDefinitions) {
|
|
716
|
+
const id = command?.id;
|
|
717
|
+
const surfaces = command?.surfaces || {};
|
|
718
|
+
if (!id || !/^wf(?:-[a-z0-9]+)*$/.test(id)) {
|
|
719
|
+
errors.push(`command-surface has invalid command id: ${JSON.stringify(id)}`);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
if (seen.has(id)) errors.push(`command-surface duplicate command id: ${id}`);
|
|
723
|
+
seen.add(id);
|
|
724
|
+
|
|
725
|
+
if (!['direct', 'workflow'].includes(command.classification)) {
|
|
726
|
+
errors.push(`command-surface ${id} has invalid classification: ${JSON.stringify(command.classification)}`);
|
|
727
|
+
}
|
|
728
|
+
if (command.entersWf !== (command.classification === 'workflow')) {
|
|
729
|
+
errors.push(`command-surface ${id} entersWf must match classification`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
for (const alias of expectedCommandAliases(id)) {
|
|
733
|
+
if (!Array.isArray(command.aliases) || !command.aliases.includes(alias)) {
|
|
734
|
+
errors.push(`command-surface ${id} missing alias ${alias}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const claudeCommand = `.claude/commands/${id}.md`;
|
|
739
|
+
const opencodeCommand = `.opencode/commands/${id}.md`;
|
|
740
|
+
const claudeSkill = `.claude/skills/${id}/SKILL.md`;
|
|
741
|
+
const codexSkill = `.agents/skills/${id}/SKILL.md`;
|
|
742
|
+
|
|
743
|
+
if (surfaces.claudeCommand && !fs.existsSync(path.join(root, claudeCommand))) {
|
|
744
|
+
errors.push(`command-surface ${id} missing Claude command: ${claudeCommand}`);
|
|
745
|
+
}
|
|
746
|
+
if (surfaces.opencodeCommand && !fs.existsSync(path.join(root, opencodeCommand))) {
|
|
747
|
+
errors.push(`command-surface ${id} missing OpenCode command: ${opencodeCommand}`);
|
|
748
|
+
}
|
|
749
|
+
if (surfaces.claudeSkill && !fs.existsSync(path.join(root, claudeSkill))) {
|
|
750
|
+
errors.push(`command-surface ${id} missing Claude skill: ${claudeSkill}`);
|
|
751
|
+
}
|
|
752
|
+
if (surfaces.codexSkill && !fs.existsSync(path.join(root, codexSkill))) {
|
|
753
|
+
errors.push(`command-surface ${id} missing Codex skill: ${codexSkill}`);
|
|
754
|
+
}
|
|
755
|
+
if (surfaces.helpRow) {
|
|
756
|
+
const rowMarker = `| \`/${id}`;
|
|
757
|
+
if (!claudeHelp.includes(rowMarker)) errors.push(`.claude/commands/wf-help.md missing command-surface help row for /${id}`);
|
|
758
|
+
if (!opencodeHelp.includes(rowMarker)) errors.push(`.opencode/commands/wf-help.md missing command-surface help row for /${id}`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (command.classification === 'direct') {
|
|
762
|
+
for (const alias of command.aliases || []) {
|
|
763
|
+
if (!claudeRouter.includes(`\`${alias}\``)) errors.push(`CLAUDE.md missing direct/compat alias ${alias}`);
|
|
764
|
+
if (!readmeRouter.includes(alias)) errors.push(`Harness/README.md missing direct/compat alias ${alias}`);
|
|
765
|
+
if (!eccExemptionLine.includes(`\`${alias}\``)) errors.push(`.claude/rules/ecc/common.md missing ECC direct command exemption ${alias}`);
|
|
766
|
+
}
|
|
767
|
+
for (const rel of [claudeCommand, opencodeCommand]) {
|
|
768
|
+
const text = read(rel);
|
|
769
|
+
if (text && id !== 'wf-help') {
|
|
770
|
+
if (!/direct command/i.test(text)) errors.push(`${rel} missing DIRECT command classification`);
|
|
771
|
+
if (!text.includes('Do not invoke a skill')) errors.push(`${rel} missing direct no-skill boundary`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (command.classification === 'workflow') {
|
|
777
|
+
for (const alias of command.aliases || []) {
|
|
778
|
+
if (eccExemptionLine.includes(`\`${alias}\``)) {
|
|
779
|
+
errors.push(`.claude/rules/ecc/common.md incorrectly exempts workflow command ${alias}`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
for (const rel of [claudeCommand, opencodeCommand]) {
|
|
783
|
+
const text = read(rel);
|
|
784
|
+
if (!text) continue;
|
|
785
|
+
if (!text.includes('workflow command')) errors.push(`${rel} missing workflow command classification`);
|
|
786
|
+
if (!text.includes('Harness/MEMORY.md')) errors.push(`${rel} missing workflow router load`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (['wf', 'wf-max', 'wf-command-create'].includes(id)) {
|
|
791
|
+
const text = `${read(claudeCommand)}\n${read(claudeSkill)}`;
|
|
792
|
+
if (!text.includes('task capsule')) errors.push(`${id} missing required task capsule instruction`);
|
|
793
|
+
if (!text.includes('task-<verb>-<noun>')) errors.push(`${id} missing task id convention`);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (surfaces.claudeSkill || surfaces.codexSkill) {
|
|
797
|
+
if (removeSkillRegistry && !removeSkillRegistry.has(id)) {
|
|
798
|
+
errors.push(`Harness/scripts/wf-remove.mjs BUILT_IN_SKILL_NAMES missing ${id}`);
|
|
799
|
+
}
|
|
800
|
+
if (cleanupDirs) {
|
|
801
|
+
if (!cleanupDirs.has(`.claude/skills/${id}`)) errors.push(`Harness/scripts/wf-remove.mjs CLEANUP_DIRS missing .claude/skills/${id}`);
|
|
802
|
+
if (!cleanupDirs.has(`.agents/skills/${id}`)) errors.push(`Harness/scripts/wf-remove.mjs CLEANUP_DIRS missing .agents/skills/${id}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if ((surfaces.claudeCommand || surfaces.opencodeCommand) && removeCommandRegistry && !removeCommandRegistry.has(id)) {
|
|
806
|
+
errors.push(`Harness/scripts/wf-remove.mjs BUILT_IN_COMMAND_NAMES missing ${id}`);
|
|
807
|
+
}
|
|
404
808
|
}
|
|
809
|
+
|
|
810
|
+
// Validate taskCapsulePolicy enum
|
|
811
|
+
for (const cmd of commandDefinitions) {
|
|
812
|
+
if (!cmd.taskCapsulePolicy || !VALID_TASK_CAPSULE_POLICIES.has(cmd.taskCapsulePolicy)) {
|
|
813
|
+
errors.push(`command-surface ${cmd.id}: invalid taskCapsulePolicy "${cmd.taskCapsulePolicy}". Valid: ${[...VALID_TASK_CAPSULE_POLICIES].join(', ')}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (isGlobalRuntimeRoot) {
|
|
819
|
+
validateGlobalRuntimeRoot();
|
|
405
820
|
}
|
|
406
821
|
|
|
822
|
+
if (isGlobalProjectBridge) {
|
|
823
|
+
validateGlobalProjectBridge();
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
validateCommandSurface();
|
|
827
|
+
|
|
828
|
+
validateRequiredFiles(required);
|
|
829
|
+
|
|
407
830
|
for (const rel of legacyRootSpecDocs) {
|
|
408
831
|
if (fs.existsSync(path.join(root, rel))) {
|
|
409
832
|
errors.push(`legacy root Harness spec doc should be migrated to Harness/specs/**: ${rel}`);
|
|
410
833
|
}
|
|
411
834
|
}
|
|
412
835
|
|
|
413
|
-
|
|
414
|
-
|
|
836
|
+
function auditManifestCoverage() {
|
|
837
|
+
const manifestText = read('Harness/ownership.manifest.json');
|
|
838
|
+
if (!manifestText) {
|
|
839
|
+
errors.push('Harness/ownership.manifest.json not found — cannot run manifest audit');
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
let manifest;
|
|
415
844
|
try {
|
|
416
|
-
|
|
417
|
-
for (const entry of manifest.frameworkOwned || []) {
|
|
418
|
-
if (!entry || typeof entry.path !== 'string') continue;
|
|
419
|
-
if (!fs.existsSync(path.join(root, ...entry.path.split('/')))) {
|
|
420
|
-
errors.push(`ownership manifest frameworkOwned file missing: ${entry.path}`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
845
|
+
manifest = JSON.parse(manifestText);
|
|
423
846
|
} catch (err) {
|
|
424
847
|
errors.push(`Harness/ownership.manifest.json is not valid JSON: ${err.message}`);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const frameworkOwned = manifest.frameworkOwned || [];
|
|
852
|
+
const manifestFileSet = new Set(frameworkOwned.map(e => e && e.path).filter(Boolean));
|
|
853
|
+
const preserveGlobs = manifest.preserve || [];
|
|
854
|
+
const mergeSet = new Set(manifest.merge || []);
|
|
855
|
+
|
|
856
|
+
// 1. Manifest → Disk: frameworkOwned files must exist (always checked)
|
|
857
|
+
for (const entry of frameworkOwned) {
|
|
858
|
+
if (!entry || typeof entry.path !== 'string') continue;
|
|
859
|
+
if (!fs.existsSync(path.join(root, ...entry.path.split('/')))) {
|
|
860
|
+
errors.push(`ownership manifest frameworkOwned file missing: ${entry.path}`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// 2. Only run Disk → Manifest scan when --manifest-audit is passed
|
|
865
|
+
if (!manifestAudit) return;
|
|
866
|
+
|
|
867
|
+
// Directories that Harness framework owns (for disk→manifest scan)
|
|
868
|
+
const harnessOwnedDirs = [
|
|
869
|
+
'.claude/agents/',
|
|
870
|
+
'.claude/commands/',
|
|
871
|
+
'.claude/skills/',
|
|
872
|
+
'.claude/rules/ecc/',
|
|
873
|
+
'.opencode/agents/',
|
|
874
|
+
'.opencode/commands/',
|
|
875
|
+
'.opencode/plugins/',
|
|
876
|
+
'.agents/skills/',
|
|
877
|
+
'.codex/',
|
|
878
|
+
'Harness/scripts/',
|
|
879
|
+
'Harness/specs/',
|
|
880
|
+
'Harness/templates/',
|
|
881
|
+
'Harness/research/',
|
|
882
|
+
];
|
|
883
|
+
|
|
884
|
+
// Root-level harness-owned files (not in subdirectories)
|
|
885
|
+
const harnessRootFiles = [
|
|
886
|
+
'opencode.json',
|
|
887
|
+
'AGENTS.md',
|
|
888
|
+
'CLAUDE.md',
|
|
889
|
+
'.claude/settings.json',
|
|
890
|
+
];
|
|
891
|
+
|
|
892
|
+
const extraFiles = [];
|
|
893
|
+
|
|
894
|
+
// Scan harness-owned directories
|
|
895
|
+
for (const dir of harnessOwnedDirs) {
|
|
896
|
+
const dirPath = path.join(root, dir);
|
|
897
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
898
|
+
scanDir(dirPath, dir, extraFiles);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Scan root-level harness files
|
|
902
|
+
for (const rel of harnessRootFiles) {
|
|
903
|
+
const filePath = path.join(root, rel);
|
|
904
|
+
if (!fs.existsSync(filePath)) continue;
|
|
905
|
+
if (!manifestFileSet.has(rel) && !mergeSet.has(rel)) {
|
|
906
|
+
extraFiles.push(rel);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Also scan for top-level files like .harness-version, ownership.manifest.json
|
|
911
|
+
const versionFile = 'Harness/.harness-version';
|
|
912
|
+
if (fs.existsSync(path.join(root, versionFile)) && !manifestFileSet.has(versionFile)) {
|
|
913
|
+
// .harness-version is special — always framework-owned
|
|
914
|
+
}
|
|
915
|
+
const manifestFile = 'Harness/ownership.manifest.json';
|
|
916
|
+
if (fs.existsSync(path.join(root, manifestFile)) && !manifestFileSet.has(manifestFile)) {
|
|
917
|
+
// ownership.manifest.json is special — always framework-owned
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Check if extra files are user-owned (preserve/merge) or already in manifest
|
|
921
|
+
for (const file of extraFiles) {
|
|
922
|
+
// Skip files already registered in the manifest (correctly tracked)
|
|
923
|
+
if (manifestFileSet.has(file)) continue;
|
|
924
|
+
|
|
925
|
+
const isPreserve = preserveGlobs.some(g => {
|
|
926
|
+
const re = new RegExp('^' + g.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') + '$');
|
|
927
|
+
return re.test(file);
|
|
928
|
+
});
|
|
929
|
+
if (isPreserve || mergeSet.has(file)) continue; // User file, skip
|
|
930
|
+
|
|
931
|
+
// Check if it's in the .harness-version checksums (tracked but maybe not in manifest)
|
|
932
|
+
const checksums = (() => {
|
|
933
|
+
try { return JSON.parse(fs.readFileSync(path.join(root, 'Harness', '.harness-version'), 'utf8')).checksums || {}; }
|
|
934
|
+
catch { return {}; }
|
|
935
|
+
})();
|
|
936
|
+
const isTracked = Object.prototype.hasOwnProperty.call(checksums, file);
|
|
937
|
+
|
|
938
|
+
if (isTracked) {
|
|
939
|
+
// File is in .harness-version but NOT in ownership manifest → manifest coverage gap
|
|
940
|
+
errors.push(`manifest coverage gap: ${file} is in .harness-version checksums but NOT in ownership.manifest.json frameworkOwned`);
|
|
941
|
+
} else {
|
|
942
|
+
// File is NOT tracked at all → truly extra/stale
|
|
943
|
+
const content = (() => { try { return fs.readFileSync(path.join(root, file), 'utf8'); } catch { return ''; } })();
|
|
944
|
+
const hasHarnessMarker = /harness:|Harness\/|create-harness-vibe-coding/i.test(content);
|
|
945
|
+
if (hasHarnessMarker) {
|
|
946
|
+
errors.push(`extra harness-owned file not in manifest: ${file} (has Harness content marker — likely stale framework file)`);
|
|
947
|
+
} else {
|
|
948
|
+
// File in harness directory but no harness marker → could be user file, warn only
|
|
949
|
+
console.warn(`Warning: untracked file in harness directory: ${file} (no Harness content marker — may be user file)`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
425
952
|
}
|
|
426
953
|
}
|
|
427
954
|
|
|
955
|
+
function scanDir(dirPath, prefix, results) {
|
|
956
|
+
let entries;
|
|
957
|
+
try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); }
|
|
958
|
+
catch { return; }
|
|
959
|
+
|
|
960
|
+
for (const entry of entries) {
|
|
961
|
+
const relPath = `${prefix}${entry.name}`;
|
|
962
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
963
|
+
|
|
964
|
+
if (entry.isDirectory()) {
|
|
965
|
+
scanDir(fullPath, `${relPath}/`, results);
|
|
966
|
+
} else if (entry.isFile()) {
|
|
967
|
+
// Normalize path separator
|
|
968
|
+
const normalized = relPath.replace(/\\/g, '/');
|
|
969
|
+
results.push(normalized);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Run manifest audit (basic manifest→disk always; --manifest-audit adds disk→manifest)
|
|
975
|
+
auditManifestCoverage();
|
|
976
|
+
|
|
428
977
|
const removedHookArtifacts = [
|
|
429
978
|
'Harness/HOOK_PROTOCOL.md',
|
|
430
979
|
'Harness/scripts/wf-mode-hook.mjs',
|
|
@@ -526,6 +1075,14 @@ requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Memory Candidate Dete
|
|
|
526
1075
|
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'explicit user preference', 'explicit user preference immediate write rule');
|
|
527
1076
|
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Memory Routing (L3)', 'memory routing section');
|
|
528
1077
|
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Scenario pack', 'route scoring scenario pack');
|
|
1078
|
+
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project/Global Memory Boundary', 'project/global memory boundary section');
|
|
1079
|
+
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Harness/tasks/` and `Harness/PROGRESS.md` are always project-local', 'global install keeps task state project-local');
|
|
1080
|
+
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project memory lives in `Harness/memory/`', 'project memory scope rule');
|
|
1081
|
+
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Global memory must never store project task state', 'global memory task-state exclusion');
|
|
1082
|
+
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project/Global Settings Boundary', 'project/global settings boundary section');
|
|
1083
|
+
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Project settings live in `Harness/settings.json` and override global settings', 'project settings override global settings');
|
|
1084
|
+
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'copied, not symlinked, into Claude Code, Codex, and OpenCode', 'host-global copy mode');
|
|
1085
|
+
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'Existing user-authored config, command, skill, or agent files are user-owned', 'user-owned host file rule');
|
|
529
1086
|
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'same tool or command pattern fails 3+ times', 'tool reflection trigger');
|
|
530
1087
|
requireText('Harness/specs/protocols/MEMORY_PROTOCOL.md', 'user corrects the same assumption or preference 2+ times', 'user correction reflection trigger');
|
|
531
1088
|
requireText('CLAUDE.md', 'startup-hints.md', 'CLAUDE startup-hints routing');
|
|
@@ -556,7 +1113,7 @@ requireText('CLAUDE.md', 'Keep intermediate user updates to 1-2 short sentences'
|
|
|
556
1113
|
requireText('.claude/rules/ecc/common.md', '## Low-Noise Progress', 'ECC low-noise progress section');
|
|
557
1114
|
requireText('.claude/rules/ecc/common.md', "Match the user's language for user-facing prose", 'ECC user-facing language match rule');
|
|
558
1115
|
requireText('.claude/rules/ecc/common.md', 'Do not recap plans, paste logs, or narrate obvious file reads', 'ECC low-noise no-recap rule');
|
|
559
|
-
requireText('
|
|
1116
|
+
requireText('Harness/specs/runtime/command-surface.json', '"wf-command-create"', 'command surface registry wf-command-create entry');
|
|
560
1117
|
requireText('Harness/README.md', 'Load By Task', 'Harness task router');
|
|
561
1118
|
requireText('Harness/README.md', 'Need context/cache/token efficiency', 'cache/token router row');
|
|
562
1119
|
requireText('Harness/specs/runtime/context-loading.md', 'Context Tiers', 'context tier load budget section');
|
|
@@ -578,6 +1135,12 @@ requireText('Harness/specs/runtime/subagents.md', 'Cache-first discipline', 'sub
|
|
|
578
1135
|
forbidText('CLAUDE.md', 'Harness/specs/guides/SETUP.md', 'CLAUDE.md SETUP reference');
|
|
579
1136
|
forbidText('CLAUDE.md', 'follow it before normal project work', 'installed-project SETUP hot-path routing');
|
|
580
1137
|
requireText('Harness/specs/guides/SETUP.md', 'Harness/specs/protocols/MEMORY_PROTOCOL.md', 'setup memory protocol reference');
|
|
1138
|
+
requireText('Harness/specs/guides/SETUP.md', '--install-scope project', 'setup project install scope');
|
|
1139
|
+
requireText('Harness/specs/guides/SETUP.md', '--install-scope global', 'setup global install scope');
|
|
1140
|
+
requireText('Harness/specs/guides/SETUP.md', '--global-dir <dir>', 'setup global dir flag');
|
|
1141
|
+
requireText('Harness/specs/guides/SETUP.md', '--host-global-dir <dir>', 'setup host-global dir flag');
|
|
1142
|
+
requireText('Harness/specs/guides/SETUP.md', 'copy Claude Code, Codex, and OpenCode command/skill surfaces', 'setup three-host global copy');
|
|
1143
|
+
requireText('Harness/specs/guides/SETUP.md', 'Project settings in `Harness/settings.json` override global settings defaults', 'setup settings precedence');
|
|
581
1144
|
requireText('Harness/specs/guides/SETUP.md', 'no startup dependency on this setup reference', 'SETUP startup boundary');
|
|
582
1145
|
forbidText('Harness/specs/guides/SETUP.md', 'bootstrap contract line', 'stale SETUP-to-CLAUDE bootstrap contract');
|
|
583
1146
|
forbidText('Harness/specs/runtime/context-loading.md', 'Always keep:', 'ambiguous always-load context rule');
|
|
@@ -957,8 +1520,8 @@ requireText('Harness/specs/workflows/WF-AUTO.md', 'Inherited WF/WF-MAX Constrain
|
|
|
957
1520
|
requireText('Harness/specs/workflows/WF-AUTO.md', 'Mini PRD -> AC IDs -> test/validation plan -> implementer -> verifier', 'wf-auto per-cycle WF chain');
|
|
958
1521
|
requireText('Harness/specs/workflows/WF-AUTO.md', 'reflector PASS', 'wf-auto reflector gate');
|
|
959
1522
|
requireText('Harness/specs/workflows/WF-AUTO.md', 'Manual or benchmark-driven single-cycle', 'wf-auto bounded single-cycle tick contract');
|
|
960
|
-
requireText('Harness/specs/workflows/WF-AUTO.md', 'Harness/tasks/
|
|
961
|
-
requireText('Harness/specs/workflows/WF-AUTO.md', 'Harness/tasks/
|
|
1523
|
+
requireText('Harness/specs/workflows/WF-AUTO.md', 'Harness/tasks/continuous/PLAN.md', 'wf-auto bounded tick PLAN record');
|
|
1524
|
+
requireText('Harness/specs/workflows/WF-AUTO.md', 'Harness/tasks/continuous/PROGRESS.md', 'wf-auto bounded tick PROGRESS record');
|
|
962
1525
|
requireText('Harness/specs/workflows/WF-AUTO-SPARK.md', 'Inherited Execution Chain', 'wf-auto-spark inherited execution chain');
|
|
963
1526
|
requireText('Harness/specs/workflows/WF-AUTO-SPARK.md', 'External spark search replaces discovery only', 'wf-auto-spark discovery-only inheritance');
|
|
964
1527
|
requireText('Harness/specs/workflows/WF-AUTO-SPARK.md', 'reflector PASS', 'wf-auto-spark reflector gate');
|
|
@@ -1090,8 +1653,8 @@ for (const skill of cacheDisciplinedSkills) {
|
|
|
1090
1653
|
}
|
|
1091
1654
|
requireText('.claude/skills/wf-auto/SKILL.md', 'bounded test tick', 'wf-auto skill bounded tick auto capsule rule');
|
|
1092
1655
|
requireText('.claude/skills/wf-auto/SKILL.md', 'missing auto capsule evidence', 'wf-auto skill missing auto capsule failure rule');
|
|
1093
|
-
requireText('.claude/skills/wf-auto/SKILL.md', 'Harness/tasks/
|
|
1094
|
-
requireText('.claude/skills/wf-auto/SKILL.md', 'Harness/tasks/
|
|
1656
|
+
requireText('.claude/skills/wf-auto/SKILL.md', 'Harness/tasks/continuous/PLAN.md', 'wf-auto skill PLAN record');
|
|
1657
|
+
requireText('.claude/skills/wf-auto/SKILL.md', 'Harness/tasks/continuous/PROGRESS.md', 'wf-auto skill PROGRESS record');
|
|
1095
1658
|
requireText('.claude/skills/wf-auto/SKILL.md', 'evidence ledger path/summary', 'wf-auto skill return evidence ledger path');
|
|
1096
1659
|
if (fs.existsSync(path.join(root, '.claude/skills/wf-browser/SKILL.md'))) {
|
|
1097
1660
|
requireText('.claude/skills/wf-browser/SKILL.md', 'Cache Discipline', 'wf-browser skill cache discipline');
|
|
@@ -1111,9 +1674,11 @@ if (fs.existsSync(path.join(root, '.claude/skills/wf-browser/SKILL.md'))) {
|
|
|
1111
1674
|
// Direct command checks
|
|
1112
1675
|
requireText('Harness/README.md', '/wf-update', 'wf-update direct command reference');
|
|
1113
1676
|
requireText('.claude/commands/wf-update.md', 'Do not invoke a skill', 'wf-update direct command boundary');
|
|
1677
|
+
requireText('Harness/README.md', '| `/wf-ui`, `$wf-ui` |', 'wf-ui direct command row');
|
|
1114
1678
|
for (const rel of ['.claude/commands/wf-update.md', '.opencode/commands/wf-update.md']) {
|
|
1115
1679
|
requireText(rel, '## Cache Discipline', `${rel} cache discipline`);
|
|
1116
1680
|
requireText(rel, 'agent.safeApplyCommand', `${rel} safe apply step`);
|
|
1681
|
+
requireText(rel, 'wf-update-runner.mjs', `${rel} multi-scope update runner`);
|
|
1117
1682
|
requireText(rel, '--apply-safe', `${rel} apply-safe command`);
|
|
1118
1683
|
requireText(rel, 'agent.aiMergeRequired', `${rel} AI merge step`);
|
|
1119
1684
|
requireText(rel, '--accept-local', `${rel} accept-local decision`);
|
|
@@ -1121,16 +1686,35 @@ for (const rel of ['.claude/commands/wf-update.md', '.opencode/commands/wf-updat
|
|
|
1121
1686
|
requireText(rel, '--accept-template', `${rel} accept-template decision`);
|
|
1122
1687
|
requireText(rel, '--finalize', `${rel} finalize command`);
|
|
1123
1688
|
requireText(rel, 'strict `--apply` only when', `${rel} strict apply boundary`);
|
|
1689
|
+
requireText(rel, 'sync-host-global.mjs', `${rel} host-global sync step`);
|
|
1124
1690
|
requireText(rel, '## Return', `${rel} return contract`);
|
|
1125
1691
|
requireText(rel, 'agent.releaseHighlights', `${rel} release highlights summary`);
|
|
1126
1692
|
requireText(rel, 'releaseNotes.highlights', `${rel} release notes fallback`);
|
|
1127
1693
|
}
|
|
1694
|
+
for (const rel of ['.claude/commands/wf-ui.md', '.opencode/commands/wf-ui.md']) {
|
|
1695
|
+
requireText(rel, 'direct command', `${rel} wf-ui direct command classification`);
|
|
1696
|
+
requireText(rel, 'Do not invoke a skill', `${rel} wf-ui direct no-skill boundary`);
|
|
1697
|
+
requireText(rel, 'create-harness-vibe-coding wf-ui', `${rel} wf-ui direct CLI launch`);
|
|
1698
|
+
requireText(rel, '--host 127.0.0.1', `${rel} wf-ui loopback host`);
|
|
1699
|
+
requireText(rel, '--open', `${rel} wf-ui browser open flag`);
|
|
1700
|
+
forbidText(rel, 'workflow command', `${rel} wf-ui workflow routing`);
|
|
1701
|
+
forbidText(rel, 'Load `CLAUDE.md`', `${rel} wf-ui old router preload`);
|
|
1702
|
+
forbidText(rel, 'Harness/tasks/task-wf-ui-control-0729/STATE.json', `${rel} wf-ui old task-state preload`);
|
|
1703
|
+
}
|
|
1704
|
+
for (const rel of ['.claude/skills/wf-ui/SKILL.md', '.agents/skills/wf-ui/SKILL.md']) {
|
|
1705
|
+
requireText(rel, 'Codex compatibility shim', `${rel} wf-ui Codex compatibility shim`);
|
|
1706
|
+
requireText(rel, 'direct command', `${rel} wf-ui direct command statement`);
|
|
1707
|
+
requireText(rel, 'create-harness-vibe-coding wf-ui', `${rel} wf-ui direct CLI launch`);
|
|
1708
|
+
forbidText(rel, 'Harness/tasks/task-wf-ui-control-0729/STATE.json', `${rel} wf-ui old task-state preload`);
|
|
1709
|
+
}
|
|
1128
1710
|
requireText('Harness/scripts/wf-update-check.mjs', 'releaseHighlights', 'wf-update-check release highlights metadata');
|
|
1129
1711
|
requireText('Harness/scripts/wf-update-check.mjs', 'updateReportRequired', 'wf-update-check user update report requirement');
|
|
1130
1712
|
requireText('.claude/commands/wf-help.md', 'direct command', 'wf-help wf-update direct command classification');
|
|
1131
1713
|
requireText('.claude/commands/wf-help.md', '$wf-help', 'wf-help Codex compatibility usage');
|
|
1132
1714
|
requireText('.claude/commands/wf-help.md', '/wf-browser', 'wf-help built-in browser workflow row');
|
|
1715
|
+
requireText('.claude/commands/wf-help.md', '| `/wf-ui` | direct command |', 'wf-help wf-ui direct command row');
|
|
1133
1716
|
requireText('.opencode/commands/wf-help.md', '/wf-browser', 'OpenCode wf-help built-in browser workflow row');
|
|
1717
|
+
requireText('.opencode/commands/wf-help.md', '| `/wf-ui` | direct command |', 'OpenCode wf-help wf-ui direct command row');
|
|
1134
1718
|
|
|
1135
1719
|
// wf-update skill must NOT claim Claude Code /wf-update as a skill invocation
|
|
1136
1720
|
// (allow mentions in the description/body that say "direct command" — those are correct)
|
|
@@ -1290,18 +1874,9 @@ if (activeStateTasks.length > 1) {
|
|
|
1290
1874
|
const OUTER_TASK_CAP = 5;
|
|
1291
1875
|
const outerTasks = taskDirs.filter(name => !TASK_RESERVED.has(name) && !name.startsWith('_'));
|
|
1292
1876
|
if (outerTasks.length > OUTER_TASK_CAP) {
|
|
1293
|
-
const capMsg = `Harness/tasks/ has ${outerTasks.length} outer task capsules (cap ${OUTER_TASK_CAP});
|
|
1877
|
+
const capMsg = `Harness/tasks/ has ${outerTasks.length} outer task capsules (cap ${OUTER_TASK_CAP}); remind the user to run $wf-task-archive when they want to archive completed tasks (apply mode maps to node Harness/scripts/task-state.mjs archive --apply)`;
|
|
1294
1878
|
if (strict) errors.push(capMsg);
|
|
1295
1879
|
else console.warn(`Warning: ${capMsg}`);
|
|
1296
1880
|
}
|
|
1297
1881
|
|
|
1298
|
-
|
|
1299
|
-
console.error(`Harness validation failed${strict ? ' (strict)' : ''}:`);
|
|
1300
|
-
for (const error of errors) console.error(`- ${error}`);
|
|
1301
|
-
process.exit(1);
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
console.log(`Harness validation passed${strict ? ' (strict)' : ''}.`);
|
|
1305
|
-
if (!strict) {
|
|
1306
|
-
console.log('Tip: run `node Harness/scripts/validate-harness.mjs --strict` after bootstrap to check unresolved project placeholders.');
|
|
1307
|
-
}
|
|
1882
|
+
finishValidation();
|