create-harness-vibe-coding 0.8.7 → 0.8.9
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/README-CN.md +163 -105
- package/README.md +179 -244
- package/bin/create-harness-vibe-coding.js +2 -2
- package/docs/images/harness-architecture-light.png +0 -0
- package/docs/images/harness-architecture.drawio +164 -0
- package/docs/images/harness-icon.png +0 -0
- package/package.json +47 -44
- package/src/generator.js +41 -5
- package/src/index.js +86 -13
- package/src/prompts.js +37 -37
- package/templates/common/.claude/agents/architect-manager.md +45 -45
- package/templates/common/.claude/agents/architect.md +31 -31
- package/templates/common/.claude/agents/codebase-explorer.md +45 -0
- package/templates/common/.claude/agents/context-master.md +75 -75
- package/templates/common/.claude/agents/debugger.md +41 -41
- package/templates/common/.claude/agents/docs-researcher.md +41 -41
- package/templates/common/.claude/agents/explore-manager.md +41 -41
- package/templates/common/.claude/agents/implement-manager.md +49 -49
- package/templates/common/.claude/agents/implementer.md +40 -40
- package/templates/common/.claude/agents/memory-master.md +82 -64
- package/templates/common/.claude/agents/planner.md +34 -34
- package/templates/common/.claude/agents/researcher.md +41 -41
- package/templates/common/.claude/agents/review-manager.md +56 -56
- package/templates/common/.claude/agents/reviewer.md +34 -34
- package/templates/common/.claude/agents/task-scribe.md +70 -0
- package/templates/common/.claude/agents/verifier.md +29 -29
- package/templates/common/.claude/commands/wf-help.md +9 -5
- package/templates/common/.claude/commands/wf-update.md +24 -0
- package/templates/common/.claude/rules/ecc/common.md +57 -44
- package/templates/common/.claude/settings.json +13 -0
- package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +8 -4
- package/templates/common/.claude/skills/wf/SKILL.md +15 -8
- package/templates/common/.claude/skills/wf-auto/SKILL.md +10 -7
- package/templates/common/.claude/skills/wf-learn/SKILL.md +9 -2
- package/templates/common/.claude/skills/wf-max/SKILL.md +23 -5
- package/templates/common/.claude/skills/wf-readme/SKILL.md +49 -49
- package/templates/common/.claude/skills/wf-remove/SKILL.md +7 -7
- package/templates/common/.claude/skills/wf-update/SKILL.md +15 -2
- package/templates/common/.codex/hooks.json +17 -0
- package/templates/common/.harness-version +130 -45
- package/templates/common/.opencode/agents/architect-manager.md +52 -0
- package/templates/common/.opencode/agents/architect.md +35 -0
- package/templates/common/.opencode/agents/codebase-explorer.md +45 -0
- package/templates/common/.opencode/agents/context-master.md +81 -0
- package/templates/common/.opencode/agents/debugger.md +43 -0
- package/templates/common/.opencode/agents/docs-researcher.md +42 -0
- package/templates/common/.opencode/agents/explore-manager.md +49 -0
- package/templates/common/.opencode/agents/implement-manager.md +56 -0
- package/templates/common/.opencode/agents/implementer.md +42 -0
- package/templates/common/.opencode/agents/memory-master.md +88 -0
- package/templates/common/.opencode/agents/planner.md +38 -0
- package/templates/common/.opencode/agents/reflector.md +39 -0
- package/templates/common/.opencode/agents/researcher.md +42 -0
- package/templates/common/.opencode/agents/review-manager.md +63 -0
- package/templates/common/.opencode/agents/reviewer.md +37 -0
- package/templates/common/.opencode/agents/task-scribe.md +70 -0
- package/templates/common/.opencode/agents/tdd-guide.md +83 -0
- package/templates/common/.opencode/agents/test-writer.md +54 -0
- package/templates/common/.opencode/agents/verifier.md +37 -0
- package/templates/common/.opencode/commands/wf-auto-spark.md +15 -0
- package/templates/common/.opencode/commands/wf-auto.md +15 -0
- package/templates/common/.opencode/commands/wf-help.md +27 -0
- package/templates/common/.opencode/commands/wf-learn.md +15 -0
- package/templates/common/.opencode/commands/wf-max.md +15 -0
- package/templates/common/.opencode/commands/wf-readme.md +15 -0
- package/templates/common/.opencode/commands/wf-remove.md +15 -0
- package/templates/common/.opencode/commands/wf-review.md +15 -0
- package/templates/common/.opencode/commands/wf-update.md +24 -0
- package/templates/common/.opencode/commands/wf.md +15 -0
- package/templates/common/.opencode/plugins/harness-wf-status.mjs +135 -0
- package/templates/common/AGENTS.md +2 -29
- package/templates/common/CLAUDE.md +114 -88
- package/templates/common/Harness/ACCEPTANCE_PROTOCOL.md +2 -2
- package/templates/common/{MEMORY.md → Harness/MEMORY.md} +17 -4
- package/templates/common/Harness/MEMORY_PROTOCOL.md +80 -30
- package/templates/common/Harness/PROGRESS.md +17 -17
- package/templates/common/Harness/README.md +58 -19
- package/templates/common/{SETUP.md → Harness/SETUP.md} +278 -276
- package/templates/common/Harness/TASK_ARCHIVE.md +56 -0
- package/templates/common/Harness/WF-AUTO-ANGLES.md +170 -0
- package/templates/common/Harness/WF-AUTO-SPARK.md +10 -19
- package/templates/common/Harness/WF-AUTO.md +93 -167
- package/templates/common/Harness/WF-KERNEL.md +189 -0
- package/templates/common/Harness/WF-MAX.md +60 -328
- package/templates/common/Harness/WF-STATE.md +83 -0
- package/templates/common/Harness/WF.md +117 -237
- package/templates/common/Harness/agent-workflow.md +2 -2
- package/templates/common/Harness/architecture.md +124 -124
- package/templates/common/Harness/context-loading.md +111 -111
- package/templates/common/Harness/dispatch.md +43 -35
- package/templates/common/Harness/extension.md +66 -66
- package/templates/common/Harness/lifecycle.md +20 -20
- package/templates/common/Harness/research/PRD.md +56 -56
- package/templates/common/Harness/research/README.md +169 -169
- package/templates/common/Harness/research/research-results.md +66 -66
- package/templates/common/Harness/scripts/archive-tasks.mjs +239 -0
- package/templates/common/{scripts → Harness/scripts}/scan-clean.mjs +443 -416
- package/templates/common/{scripts → Harness/scripts}/validate-harness.mjs +691 -452
- package/templates/common/Harness/scripts/wf-auto-update-prompt.mjs +258 -0
- package/templates/common/{scripts → Harness/scripts}/wf-remove.mjs +56 -39
- package/templates/common/{scripts → Harness/scripts}/wf-update-check.mjs +632 -599
- package/templates/common/Harness/subagents.md +215 -214
- package/templates/common/Harness/tasks/_template/ARTIFACTS.md +2 -2
- package/templates/common/Harness/tasks/_template/NOTES.md +2 -2
- package/templates/common/Harness/tasks/_template/PLAN.md +5 -0
- package/templates/common/Harness/tasks/_template/STATE.json +23 -0
- package/templates/common/README.md +37 -37
- package/templates/common/memory/agent-lessons-patterns.md +22 -21
- package/templates/common/memory/routes.md +43 -0
- package/templates/common/memory/startup-hints.md +32 -0
- package/templates/common/memory/tool-usage-reflections.md +22 -21
- package/templates/common/memory/user-corrections-preferences.md +23 -21
- package/templates/common/opencode.json +19 -0
- package/templates/optional/catalog.json +49 -33
- package/templates/optional/skills/browser-e2e/.claude/skills/browser-e2e/SKILL.md +42 -42
- package/templates/optional/skills/browser-e2e/.claude/skills/wf-browser/SKILL.md +193 -193
- package/templates/optional/skills/browser-e2e/.opencode/commands/wf-browser.md +15 -0
- package/templates/optional/skills/browser-e2e/Harness/workflows/browser-e2e.md +48 -48
- package/templates/optional/skills/github-pr-review/.claude/skills/github-pr-review/SKILL.md +40 -40
- package/templates/optional/skills/github-pr-review/Harness/workflows/github-pr-review.md +28 -28
- package/templates/optional/skills/python-backend/.claude/skills/python-backend/SKILL.md +40 -40
- package/templates/optional/skills/python-backend/Harness/workflows/python-backend.md +34 -34
- package/templates/optional/skills/ts-react-frontend/.claude/skills/ts-react-frontend/SKILL.md +43 -43
- package/templates/optional/skills/ts-react-frontend/Harness/workflows/ts-react-frontend.md +34 -34
- package/templates/optional/skills/ui-ux-review/.claude/skills/ui-ux-review/SKILL.md +40 -40
- package/templates/optional/skills/ui-ux-review/Harness/workflows/ui-ux-review.md +26 -26
|
@@ -1,63 +1,78 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
const root = process.cwd();
|
|
6
|
-
const args = new Set(process.argv.slice(2));
|
|
7
|
-
const strict = args.has('--strict') || args.has('--post-bootstrap');
|
|
8
|
-
|
|
9
|
-
if (args.has('--help') || args.has('-h')) {
|
|
10
|
-
console.log(`Usage: node Harness/scripts/validate-harness.mjs [--strict]
|
|
11
|
-
|
|
12
|
-
Default mode checks scaffold structure, links, agents, and skills.
|
|
13
|
-
--strict also fails when project fact docs still contain unresolved {{TOKEN}} placeholders.
|
|
14
|
-
Literal explanatory {{...}} text is allowed.`);
|
|
15
|
-
process.exit(0);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const commonAgents = [
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const root = process.cwd();
|
|
6
|
+
const args = new Set(process.argv.slice(2));
|
|
7
|
+
const strict = args.has('--strict') || args.has('--post-bootstrap');
|
|
8
|
+
|
|
9
|
+
if (args.has('--help') || args.has('-h')) {
|
|
10
|
+
console.log(`Usage: node Harness/scripts/validate-harness.mjs [--strict]
|
|
11
|
+
|
|
12
|
+
Default mode checks scaffold structure, links, agents, and skills.
|
|
13
|
+
--strict also fails when project fact docs still contain unresolved {{TOKEN}} placeholders.
|
|
14
|
+
Literal explanatory {{...}} text is allowed.`);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const commonAgents = [
|
|
19
|
+
'task-scribe',
|
|
20
|
+
'codebase-explorer',
|
|
21
|
+
'researcher',
|
|
22
|
+
'docs-researcher',
|
|
23
|
+
'planner',
|
|
24
|
+
'architect',
|
|
25
|
+
'test-writer',
|
|
26
|
+
'implementer',
|
|
25
27
|
'debugger',
|
|
26
28
|
'reviewer',
|
|
27
29
|
'verifier',
|
|
28
30
|
'reflector',
|
|
29
31
|
'memory-master',
|
|
30
|
-
'context-master',
|
|
31
|
-
'explore-manager',
|
|
32
|
-
'architect-manager',
|
|
33
|
-
'implement-manager',
|
|
34
|
-
'review-manager',
|
|
35
|
-
];
|
|
36
|
-
|
|
32
|
+
'context-master',
|
|
33
|
+
'explore-manager',
|
|
34
|
+
'architect-manager',
|
|
35
|
+
'implement-manager',
|
|
36
|
+
'review-manager',
|
|
37
|
+
];
|
|
38
|
+
|
|
37
39
|
const commonSkills = [
|
|
38
40
|
'wf',
|
|
39
41
|
'tdd',
|
|
40
42
|
'wf-update',
|
|
41
43
|
'wf-max',
|
|
42
44
|
'wf-review',
|
|
43
|
-
'wf-learn',
|
|
44
|
-
'subagent-orchestrator',
|
|
45
|
-
'wf-readme',
|
|
46
|
-
'wf-remove',
|
|
47
|
-
'wf-auto',
|
|
48
|
-
'wf-auto-spark',
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
|
|
45
|
+
'wf-learn',
|
|
46
|
+
'subagent-orchestrator',
|
|
47
|
+
'wf-readme',
|
|
48
|
+
'wf-remove',
|
|
49
|
+
'wf-auto',
|
|
50
|
+
'wf-auto-spark',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const opencodeWorkflowCommands = [
|
|
54
|
+
'wf',
|
|
55
|
+
'wf-max',
|
|
56
|
+
'wf-auto',
|
|
57
|
+
'wf-auto-spark',
|
|
58
|
+
'wf-learn',
|
|
59
|
+
'wf-review',
|
|
60
|
+
'wf-readme',
|
|
61
|
+
'wf-remove',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const memoryFiles = [
|
|
65
|
+
'Harness/memory/startup-hints.md',
|
|
66
|
+
'Harness/memory/routes.md',
|
|
67
|
+
'Harness/memory/tool-usage-reflections.md',
|
|
68
|
+
'Harness/memory/user-corrections-preferences.md',
|
|
69
|
+
'Harness/memory/agent-lessons-patterns.md',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const required = [
|
|
73
|
+
'AGENTS.md',
|
|
74
|
+
'CLAUDE.md',
|
|
75
|
+
'README.md',
|
|
61
76
|
'Harness/MEMORY.md',
|
|
62
77
|
'Harness/WF.md',
|
|
63
78
|
'Harness/WF-MAX.md',
|
|
@@ -75,66 +90,76 @@ const required = [
|
|
|
75
90
|
'Harness/templates/VALIDATION_REPORT.template.md',
|
|
76
91
|
...memoryFiles,
|
|
77
92
|
'.codex/config.toml',
|
|
93
|
+
'opencode.json',
|
|
78
94
|
'.claude/settings.json',
|
|
79
95
|
'.claude/commands/wf-help.md',
|
|
96
|
+
'.claude/commands/wf-update.md',
|
|
97
|
+
'.opencode/commands/wf-help.md',
|
|
98
|
+
'.opencode/commands/wf-update.md',
|
|
99
|
+
...opencodeWorkflowCommands.map(command => `.opencode/commands/${command}.md`),
|
|
80
100
|
'.claude/rules/ecc/common.md',
|
|
81
|
-
...commonAgents.map(agent => `.claude/agents/${agent}.md`),
|
|
82
|
-
...
|
|
83
|
-
...commonSkills.map(skill => `.
|
|
84
|
-
|
|
85
|
-
'Harness/
|
|
86
|
-
'Harness/
|
|
87
|
-
'Harness/
|
|
88
|
-
'Harness/
|
|
89
|
-
'Harness/
|
|
90
|
-
'Harness/
|
|
91
|
-
'Harness/
|
|
92
|
-
'Harness/
|
|
93
|
-
'Harness/
|
|
94
|
-
'Harness/
|
|
101
|
+
...commonAgents.map(agent => `.claude/agents/${agent}.md`),
|
|
102
|
+
...commonAgents.map(agent => `.opencode/agents/${agent}.md`),
|
|
103
|
+
...commonSkills.map(skill => `.claude/skills/${skill}/SKILL.md`),
|
|
104
|
+
...commonSkills.map(skill => `.agents/skills/${skill}/SKILL.md`),
|
|
105
|
+
'Harness/README.md',
|
|
106
|
+
'Harness/PROGRESS.md',
|
|
107
|
+
'Harness/lifecycle.md',
|
|
108
|
+
'Harness/subagents.md',
|
|
109
|
+
'Harness/dispatch.md',
|
|
110
|
+
'Harness/extension.md',
|
|
111
|
+
'Harness/context-loading.md',
|
|
112
|
+
'Harness/WF-KERNEL.md',
|
|
113
|
+
'Harness/agent-workflow.md',
|
|
114
|
+
'Harness/architecture.md',
|
|
115
|
+
'Harness/research/README.md',
|
|
116
|
+
'Harness/research/research-results.md',
|
|
95
117
|
'Harness/research/PRD.md',
|
|
96
118
|
'Harness/scripts/wf-update-check.mjs',
|
|
97
|
-
'Harness/scripts/wf-remove.mjs',
|
|
98
|
-
'Harness/scripts/scan-clean.mjs',
|
|
99
|
-
'Harness
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
'Harness/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
'
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
'Harness/
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
119
|
+
'Harness/scripts/wf-remove.mjs',
|
|
120
|
+
'Harness/scripts/scan-clean.mjs',
|
|
121
|
+
'Harness/scripts/archive-tasks.mjs',
|
|
122
|
+
'Harness/WF-STATE.md',
|
|
123
|
+
'Harness/TASK_ARCHIVE.md',
|
|
124
|
+
'Harness/.harness-version',
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const projectFacts = [
|
|
128
|
+
'Harness/PROGRESS.md',
|
|
129
|
+
'Harness/research/PRD.md',
|
|
130
|
+
'Harness/research/research-results.md',
|
|
131
|
+
'Harness/architecture.md',
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const contextPacks = [
|
|
135
|
+
'Explorer Pass:',
|
|
136
|
+
'Planner:',
|
|
137
|
+
'Researcher:',
|
|
138
|
+
'Docs Researcher:',
|
|
139
|
+
'Architect:',
|
|
140
|
+
'Test Writer:',
|
|
141
|
+
'Implementer', // matches "Implementer:", "Implementer (Frontend):", "Implementer (Backend):"
|
|
142
|
+
'Reviewer:',
|
|
143
|
+
'Debugger:',
|
|
144
|
+
'Verifier:',
|
|
145
|
+
'Memory Master:',
|
|
146
|
+
'Context Master:',
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const durableCommunicationDocs = [
|
|
150
|
+
'Harness/README.md',
|
|
151
|
+
'Harness/subagents.md',
|
|
152
|
+
'Harness/dispatch.md',
|
|
153
|
+
'Harness/context-loading.md',
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const errors = [];
|
|
157
|
+
|
|
158
|
+
function read(rel) {
|
|
159
|
+
const file = path.join(root, rel);
|
|
160
|
+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
161
|
+
}
|
|
162
|
+
|
|
138
163
|
function requireText(rel, text, label = text) {
|
|
139
164
|
const body = read(rel);
|
|
140
165
|
if (body && !body.includes(text)) errors.push(`${rel} missing ${label}`);
|
|
@@ -145,85 +170,85 @@ function forbidText(rel, text, label = text) {
|
|
|
145
170
|
if (body && body.includes(text)) errors.push(`${rel} contains forbidden ${label}`);
|
|
146
171
|
}
|
|
147
172
|
|
|
148
|
-
function frontmatterField(text, field) {
|
|
149
|
-
const match = text.match(new RegExp(`^${field}:\\s*(.+)$`, 'm'));
|
|
150
|
-
return match ? match[1].trim() : '';
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function listDirectories(rel) {
|
|
154
|
-
const dir = path.join(root, rel);
|
|
155
|
-
if (!fs.existsSync(dir)) return [];
|
|
156
|
-
return fs.readdirSync(dir, { withFileTypes: true })
|
|
157
|
-
.filter(entry => entry.isDirectory())
|
|
158
|
-
.map(entry => entry.name);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function listMarkdownFiles(rel) {
|
|
162
|
-
const dir = path.join(root, rel);
|
|
163
|
-
if (!fs.existsSync(dir)) return [];
|
|
164
|
-
const normalizedRel = rel.replaceAll(path.sep, '/');
|
|
165
|
-
return fs.readdirSync(dir, { withFileTypes: true })
|
|
166
|
-
.filter(entry => entry.isFile() && entry.name.endsWith('.md'))
|
|
167
|
-
.map(entry => `${normalizedRel}/${entry.name}`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Task naming convention: task-<verb>-<noun>[-detail], kebab-case, ≤46 chars
|
|
171
|
-
// Prefix "task-" required for new tasks. Existing tasks without prefix grandfathered (warning only).
|
|
172
|
-
// Reserved: _template (system), auto (auto-mode capsule)
|
|
173
|
-
const TASK_NAME_RE = /^task-[a-z]+(-[a-z0-9]+){1,4}$/;
|
|
174
|
-
const TASK_NAME_MAX = 46; // "task-" (5) + ≤40 chars body + 1 safety = 46
|
|
175
|
-
const TASK_RESERVED = new Set(['_template', 'auto']);
|
|
176
|
-
|
|
177
|
-
function validateTaskName(name, strict) {
|
|
178
|
-
if (TASK_RESERVED.has(name)) return null;
|
|
179
|
-
if (name.startsWith('_')) return `Task name "${name}" — leading underscore reserved for system dirs`;
|
|
180
|
-
if (name.length > TASK_NAME_MAX) return `Task name "${name}" — ${name.length} chars, max ${TASK_NAME_MAX}`;
|
|
181
|
-
if (!TASK_NAME_RE.test(name)) {
|
|
182
|
-
// Grandfather: existing tasks without "task-" prefix get a warning, not an error
|
|
183
|
-
if (/^[a-z]+(-[a-z0-9]+){1,4}$/.test(name)) {
|
|
184
|
-
if (strict) return `Task name "${name}" — missing "task-" prefix (required for new tasks). Rename to "task-${name}".`;
|
|
185
|
-
return null; // non-strict: allow grandfathered names
|
|
186
|
-
}
|
|
187
|
-
return `Task name "${name}" — must be task-<verb>-<noun>[-detail], kebab-case, 2-5 words after prefix`;
|
|
188
|
-
}
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function unresolvedTemplatePlaceholders(text) {
|
|
193
|
-
const placeholders = [];
|
|
194
|
-
const pattern = /\{\{([^{}\r\n]+)\}\}/g;
|
|
195
|
-
|
|
196
|
-
for (const match of text.matchAll(pattern)) {
|
|
197
|
-
const token = match[1].trim();
|
|
198
|
-
if (token === '...') continue;
|
|
199
|
-
placeholders.push(`{{${token}}}`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return [...new Set(placeholders)];
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function registeredSkillFiles(...texts) {
|
|
206
|
-
const files = new Set();
|
|
207
|
-
const pattern = /(?:\.\.\/)?((?:\.claude|\.agents)\/skills\/[a-z0-9-]+\/SKILL\.md)/g;
|
|
208
|
-
|
|
209
|
-
for (const text of texts) {
|
|
210
|
-
for (const match of text.matchAll(pattern)) {
|
|
211
|
-
files.add(match[1]);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return [...files].sort();
|
|
216
|
-
}
|
|
217
|
-
|
|
173
|
+
function frontmatterField(text, field) {
|
|
174
|
+
const match = text.match(new RegExp(`^${field}:\\s*(.+)$`, 'm'));
|
|
175
|
+
return match ? match[1].trim() : '';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function listDirectories(rel) {
|
|
179
|
+
const dir = path.join(root, rel);
|
|
180
|
+
if (!fs.existsSync(dir)) return [];
|
|
181
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
182
|
+
.filter(entry => entry.isDirectory())
|
|
183
|
+
.map(entry => entry.name);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function listMarkdownFiles(rel) {
|
|
187
|
+
const dir = path.join(root, rel);
|
|
188
|
+
if (!fs.existsSync(dir)) return [];
|
|
189
|
+
const normalizedRel = rel.replaceAll(path.sep, '/');
|
|
190
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
191
|
+
.filter(entry => entry.isFile() && entry.name.endsWith('.md'))
|
|
192
|
+
.map(entry => `${normalizedRel}/${entry.name}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Task naming convention: task-<verb>-<noun>[-detail], kebab-case, ≤46 chars
|
|
196
|
+
// Prefix "task-" required for new tasks. Existing tasks without prefix grandfathered (warning only).
|
|
197
|
+
// Reserved: _template (system), auto (auto-mode capsule)
|
|
198
|
+
const TASK_NAME_RE = /^task-[a-z]+(-[a-z0-9]+){1,4}$/;
|
|
199
|
+
const TASK_NAME_MAX = 46; // "task-" (5) + ≤40 chars body + 1 safety = 46
|
|
200
|
+
const TASK_RESERVED = new Set(['_template', 'auto', '_archive']);
|
|
201
|
+
|
|
202
|
+
function validateTaskName(name, strict) {
|
|
203
|
+
if (TASK_RESERVED.has(name)) return null;
|
|
204
|
+
if (name.startsWith('_')) return `Task name "${name}" — leading underscore reserved for system dirs`;
|
|
205
|
+
if (name.length > TASK_NAME_MAX) return `Task name "${name}" — ${name.length} chars, max ${TASK_NAME_MAX}`;
|
|
206
|
+
if (!TASK_NAME_RE.test(name)) {
|
|
207
|
+
// Grandfather: existing tasks without "task-" prefix get a warning, not an error
|
|
208
|
+
if (/^[a-z]+(-[a-z0-9]+){1,4}$/.test(name)) {
|
|
209
|
+
if (strict) return `Task name "${name}" — missing "task-" prefix (required for new tasks). Rename to "task-${name}".`;
|
|
210
|
+
return null; // non-strict: allow grandfathered names
|
|
211
|
+
}
|
|
212
|
+
return `Task name "${name}" — must be task-<verb>-<noun>[-detail], kebab-case, 2-5 words after prefix`;
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function unresolvedTemplatePlaceholders(text) {
|
|
218
|
+
const placeholders = [];
|
|
219
|
+
const pattern = /\{\{([^{}\r\n]+)\}\}/g;
|
|
220
|
+
|
|
221
|
+
for (const match of text.matchAll(pattern)) {
|
|
222
|
+
const token = match[1].trim();
|
|
223
|
+
if (token === '...') continue;
|
|
224
|
+
placeholders.push(`{{${token}}}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return [...new Set(placeholders)];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function registeredSkillFiles(...texts) {
|
|
231
|
+
const files = new Set();
|
|
232
|
+
const pattern = /(?:\.\.\/)?((?:\.claude|\.agents)\/skills\/[a-z0-9-]+\/SKILL\.md)/g;
|
|
233
|
+
|
|
234
|
+
for (const text of texts) {
|
|
235
|
+
for (const match of text.matchAll(pattern)) {
|
|
236
|
+
files.add(match[1]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return [...files].sort();
|
|
241
|
+
}
|
|
242
|
+
|
|
218
243
|
function registeredWorkflowFiles(...texts) {
|
|
219
244
|
const files = new Set();
|
|
220
245
|
const pattern = /(?:Harness\/)?workflows\/([A-Za-z0-9._-]+\.md)/g;
|
|
221
|
-
|
|
222
|
-
for (const text of texts) {
|
|
223
|
-
for (const match of text.matchAll(pattern)) {
|
|
224
|
-
files.add(`Harness/workflows/${match[1]}`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
246
|
+
|
|
247
|
+
for (const text of texts) {
|
|
248
|
+
for (const match of text.matchAll(pattern)) {
|
|
249
|
+
files.add(`Harness/workflows/${match[1]}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
227
252
|
|
|
228
253
|
return [...files].sort();
|
|
229
254
|
}
|
|
@@ -236,13 +261,14 @@ function listedWorkflowCommands(...texts) {
|
|
|
236
261
|
for (const match of text.matchAll(pattern)) {
|
|
237
262
|
const command = `/${match[1]}`;
|
|
238
263
|
if (command === '/wf-help') continue;
|
|
264
|
+
if (command === '/wf-browser') continue; // optional workflow, not a required skill
|
|
239
265
|
commands.add(command);
|
|
240
266
|
}
|
|
241
267
|
}
|
|
242
268
|
|
|
243
269
|
return [...commands].sort();
|
|
244
270
|
}
|
|
245
|
-
|
|
271
|
+
|
|
246
272
|
for (const rel of required) {
|
|
247
273
|
if (!fs.existsSync(path.join(root, rel))) {
|
|
248
274
|
errors.push(`missing required file: ${rel}`);
|
|
@@ -262,140 +288,169 @@ for (const rel of removedHookArtifacts) {
|
|
|
262
288
|
errors.push(`removed hook artifact should not exist: ${rel}`);
|
|
263
289
|
}
|
|
264
290
|
}
|
|
265
|
-
|
|
266
|
-
// Task capsule template files
|
|
267
|
-
const taskTemplateDir = path.join(root, 'Harness', 'tasks', '_template');
|
|
268
|
-
if (!fs.existsSync(taskTemplateDir)) {
|
|
269
|
-
errors.push('missing directory: Harness/tasks/_template/');
|
|
270
|
-
} else {
|
|
271
|
-
for (const f of ['PROGRESS.md', 'PLAN.md']) {
|
|
272
|
-
if (!fs.existsSync(path.join(taskTemplateDir, f))) {
|
|
273
|
-
errors.push(`missing task template file: Harness/tasks/_template/${f}`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Cross-reference: DONE files in task PLAN.md must exist on disk
|
|
279
|
-
const taskDirs = fs.existsSync(path.join(root, 'Harness', 'tasks'))
|
|
280
|
-
? fs.readdirSync(path.join(root, 'Harness', 'tasks'), { withFileTypes: true })
|
|
281
|
-
.filter(e => e.isDirectory() && e.name !== '_template')
|
|
282
|
-
.map(e => e.name)
|
|
283
|
-
: [];
|
|
284
|
-
for (const taskDir of taskDirs) {
|
|
285
|
-
// Validate task naming convention
|
|
286
|
-
const nameErr = validateTaskName(taskDir, strict);
|
|
287
|
-
if (nameErr) errors.push(nameErr);
|
|
288
|
-
|
|
289
|
-
const planPath = `Harness/tasks/${taskDir}/PLAN.md`;
|
|
290
|
-
const planText = read(planPath);
|
|
291
|
-
if (!planText) continue;
|
|
292
|
-
const donePattern = /`([^`]+\.(?:md|mjs|js|ts|json|html|css))`[^\n]*DONE/gi;
|
|
293
|
-
for (const match of planText.matchAll(donePattern)) {
|
|
294
|
-
const claimedFile = match[1];
|
|
295
|
-
if (!fs.existsSync(path.join(root, claimedFile))) {
|
|
296
|
-
errors.push(`${planPath} claims '${claimedFile}' is DONE but file does not exist`);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (fs.existsSync(path.join(root, 'Harness/research/scaffolds.md'))) {
|
|
302
|
-
errors.push('legacy research file should be renamed: Harness/research/scaffolds.md -> Harness/research/research-results.md');
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (strict) {
|
|
306
|
-
console.log('Strict placeholder scope:');
|
|
307
|
-
for (const rel of projectFacts) {
|
|
308
|
-
console.log(`- ${rel}`);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
for (const rel of projectFacts) {
|
|
312
|
-
const text = read(rel);
|
|
313
|
-
for (const placeholder of unresolvedTemplatePlaceholders(text)) {
|
|
314
|
-
errors.push(`template placeholder remains in project fact file: ${rel}: ${placeholder}`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const docsReadme = read('Harness/README.md');
|
|
320
|
-
if (docsReadme) {
|
|
321
|
-
for (const marker of ['## Keyword Routing', '## Load By Task', 'When to Read', 'Keywords']) {
|
|
322
|
-
if (!docsReadme.includes(marker)) errors.push(`Harness/README.md missing router marker: ${marker}`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
for (const rel of durableCommunicationDocs) {
|
|
327
|
-
requireText(rel, 'project files are the only durable communication channel', 'durable filesystem communication invariant');
|
|
328
|
-
requireText(rel, 'chat/subagent transcript state is non-authoritative', 'non-authoritative transcript invariant');
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
requireText('
|
|
332
|
-
requireText('
|
|
333
|
-
requireText('
|
|
334
|
-
requireText('
|
|
335
|
-
requireText('
|
|
336
|
-
requireText('
|
|
291
|
+
|
|
292
|
+
// Task capsule template files
|
|
293
|
+
const taskTemplateDir = path.join(root, 'Harness', 'tasks', '_template');
|
|
294
|
+
if (!fs.existsSync(taskTemplateDir)) {
|
|
295
|
+
errors.push('missing directory: Harness/tasks/_template/');
|
|
296
|
+
} else {
|
|
297
|
+
for (const f of ['PROGRESS.md', 'PLAN.md']) {
|
|
298
|
+
if (!fs.existsSync(path.join(taskTemplateDir, f))) {
|
|
299
|
+
errors.push(`missing task template file: Harness/tasks/_template/${f}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Cross-reference: DONE files in task PLAN.md must exist on disk
|
|
305
|
+
const taskDirs = fs.existsSync(path.join(root, 'Harness', 'tasks'))
|
|
306
|
+
? fs.readdirSync(path.join(root, 'Harness', 'tasks'), { withFileTypes: true })
|
|
307
|
+
.filter(e => e.isDirectory() && e.name !== '_template')
|
|
308
|
+
.map(e => e.name)
|
|
309
|
+
: [];
|
|
310
|
+
for (const taskDir of taskDirs) {
|
|
311
|
+
// Validate task naming convention
|
|
312
|
+
const nameErr = validateTaskName(taskDir, strict);
|
|
313
|
+
if (nameErr) errors.push(nameErr);
|
|
314
|
+
|
|
315
|
+
const planPath = `Harness/tasks/${taskDir}/PLAN.md`;
|
|
316
|
+
const planText = read(planPath);
|
|
317
|
+
if (!planText) continue;
|
|
318
|
+
const donePattern = /`([^`]+\.(?:md|mjs|js|ts|json|html|css))`[^\n]*DONE/gi;
|
|
319
|
+
for (const match of planText.matchAll(donePattern)) {
|
|
320
|
+
const claimedFile = match[1];
|
|
321
|
+
if (!fs.existsSync(path.join(root, claimedFile))) {
|
|
322
|
+
errors.push(`${planPath} claims '${claimedFile}' is DONE but file does not exist`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (fs.existsSync(path.join(root, 'Harness/research/scaffolds.md'))) {
|
|
328
|
+
errors.push('legacy research file should be renamed: Harness/research/scaffolds.md -> Harness/research/research-results.md');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (strict) {
|
|
332
|
+
console.log('Strict placeholder scope:');
|
|
333
|
+
for (const rel of projectFacts) {
|
|
334
|
+
console.log(`- ${rel}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
for (const rel of projectFacts) {
|
|
338
|
+
const text = read(rel);
|
|
339
|
+
for (const placeholder of unresolvedTemplatePlaceholders(text)) {
|
|
340
|
+
errors.push(`template placeholder remains in project fact file: ${rel}: ${placeholder}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const docsReadme = read('Harness/README.md');
|
|
346
|
+
if (docsReadme) {
|
|
347
|
+
for (const marker of ['## Keyword Routing', '## Load By Task', 'When to Read', 'Keywords']) {
|
|
348
|
+
if (!docsReadme.includes(marker)) errors.push(`Harness/README.md missing router marker: ${marker}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
for (const rel of durableCommunicationDocs) {
|
|
353
|
+
requireText(rel, 'project files are the only durable communication channel', 'durable filesystem communication invariant');
|
|
354
|
+
requireText(rel, 'chat/subagent transcript state is non-authoritative', 'non-authoritative transcript invariant');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
requireText('Harness/MEMORY_PROTOCOL.md', 'L1/L2/L3 Memory Architecture', 'L2/L3 memory architecture section');
|
|
358
|
+
requireText('Harness/MEMORY_PROTOCOL.md', 'Memory Candidate Detection', 'memory candidate detection section');
|
|
359
|
+
requireText('Harness/MEMORY_PROTOCOL.md', 'explicit user preference', 'explicit user preference immediate write rule');
|
|
360
|
+
requireText('Harness/MEMORY_PROTOCOL.md', 'Memory Routing (L3)', 'memory routing section');
|
|
361
|
+
requireText('Harness/MEMORY_PROTOCOL.md', 'Scenario pack', 'route scoring scenario pack');
|
|
362
|
+
requireText('Harness/MEMORY_PROTOCOL.md', 'same tool or command pattern fails 3+ times', 'tool reflection trigger');
|
|
363
|
+
requireText('Harness/MEMORY_PROTOCOL.md', 'user corrects the same assumption or preference 2+ times', 'user correction reflection trigger');
|
|
364
|
+
requireText('CLAUDE.md', 'startup-hints.md', 'CLAUDE startup-hints routing');
|
|
365
|
+
requireText('.claude/rules/ecc/common.md', 'startup-hints.md', 'ECC startup-hints routing');
|
|
366
|
+
requireText('.claude/rules/ecc/common.md', 'memory candidates', 'ECC memory candidate detection');
|
|
367
|
+
requireText('.claude/rules/ecc/common.md', 'remember', 'ECC explicit memory trigger');
|
|
368
|
+
requireText('.claude/agents/memory-master.md', 'remember', 'memory-master explicit trigger');
|
|
369
|
+
requireText('.claude/agents/memory-master.md', 'compact format', 'memory-master compact format');
|
|
370
|
+
requireText('.claude/agents/memory-master.md', 'superseded', 'memory-master date only for superseded');
|
|
371
|
+
requireText('.opencode/agents/memory-master.md', 'remember', 'OpenCode memory-master explicit trigger');
|
|
372
|
+
requireText('.claude/skills/wf-learn/SKILL.md', 'without waiting for', 'wf-learn explicit preference immediate write');
|
|
373
|
+
requireText('.agents/skills/wf-learn/SKILL.md', 'without waiting for', 'Codex wf-learn explicit preference immediate write');
|
|
374
|
+
requireText('Harness/memory/startup-hints.md', 'Memory Candidate Detection', 'startup-hints memory candidate section');
|
|
375
|
+
requireText('Harness/memory/startup-hints.md', '记住', 'startup-hints chinese triggers');
|
|
376
|
+
requireText('Harness/memory/routes.md', 'signals', 'routes has signals column');
|
|
377
|
+
requireText('Harness/memory/routes.md', 'avoid', 'routes has avoid column');
|
|
378
|
+
requireText('Harness/memory/user-corrections-preferences.md', 'supersedes', 'memory template date only for superseded');
|
|
379
|
+
requireText('Harness/memory/tool-usage-reflections.md', 'supersedes', 'tool reflections template date rule');
|
|
380
|
+
requireText('Harness/memory/agent-lessons-patterns.md', 'supersedes', 'agent lessons template date rule');
|
|
381
|
+
requireText('Harness/MEMORY.md', 'startup-hints.md', 'MEMORY.md startup-hints registration');
|
|
382
|
+
requireText('Harness/MEMORY.md', 'Memory routes', 'MEMORY.md routes.md registration');
|
|
383
|
+
requireText('CLAUDE.md', 'If `Harness/` exists, this repository is governed by the Harness contract', 'Harness binding contract');
|
|
384
|
+
requireText('CLAUDE.md', 'memory and resource router', 'memory/resource router');
|
|
385
|
+
requireText('CLAUDE.md', '## 5a. Low-Noise Progress', 'low-noise progress section');
|
|
386
|
+
requireText('CLAUDE.md', 'Keep intermediate user updates to 1-2 short sentences', 'low-noise intermediate update rule');
|
|
387
|
+
requireText('.claude/rules/ecc/common.md', '## Low-Noise Progress', 'ECC low-noise progress section');
|
|
388
|
+
requireText('.claude/rules/ecc/common.md', 'Do not recap plans, paste logs, or narrate obvious file reads', 'ECC low-noise no-recap rule');
|
|
389
|
+
requireText('Harness/README.md', 'Load By Task', 'Harness task router');
|
|
390
|
+
requireText('CLAUDE.md', 'Harness/SETUP.md` is a bootstrap-only document', 'setup bootstrap-only contract');
|
|
391
|
+
forbidText('CLAUDE.md', 'follow it before normal project work', 'installed-project SETUP hot-path routing');
|
|
337
392
|
requireText('Harness/SETUP.md', 'Harness/MEMORY_PROTOCOL.md', 'setup memory protocol reference');
|
|
338
|
-
requireText('
|
|
339
|
-
requireText('
|
|
340
|
-
requireText('CLAUDE.md', 'Harness/tasks/', 'task capsule directory reference');
|
|
341
|
-
requireText('
|
|
342
|
-
for (const heading of ['## 2. Think Before Coding', '## 3. Simplicity First', '## 4. Surgical Changes', '## 5. Goal-Driven Execution']) {
|
|
343
|
-
requireText('CLAUDE.md', heading, `Karpathy-style rule heading: ${heading}`);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Root PROGRESS.md structure check
|
|
347
|
-
const progress = read('Harness/PROGRESS.md');
|
|
348
|
-
if (progress) {
|
|
349
|
-
for (const heading of ['## Active Task', '## Task Index', '## Cross-Task Decisions']) {
|
|
350
|
-
if (!progress.includes(heading)) errors.push(`Harness/PROGRESS.md missing heading: ${heading}`);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Legacy PLAN.md deprecation check (removed — stub only)
|
|
355
|
-
|
|
356
|
-
const dispatch = read('Harness/dispatch.md');
|
|
357
|
-
if (dispatch) {
|
|
358
|
-
for (const agent of commonAgents) {
|
|
359
|
-
if (!dispatch.includes(`\`${agent}\``)) errors.push(`Harness/dispatch.md missing common agent: ${agent}`);
|
|
360
|
-
}
|
|
361
|
-
if (!dispatch.includes('## Handoff Format')) errors.push('Harness/dispatch.md missing heading: ## Handoff Format');
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const contextLoading = read('Harness/context-loading.md');
|
|
365
|
-
if (contextLoading) {
|
|
366
|
-
if (!contextLoading.includes('Harness/README.md` is the primary router')) {
|
|
367
|
-
errors.push('Harness/context-loading.md must declare Harness/README.md as the primary router');
|
|
368
|
-
}
|
|
369
|
-
for (const pack of contextPacks) {
|
|
370
|
-
if (!contextLoading.includes(pack)) errors.push(`Harness/context-loading.md missing subagent pack: ${pack}`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const memory = read('Harness/MEMORY.md');
|
|
375
|
-
if (memory) {
|
|
376
|
-
for (const agent of commonAgents) {
|
|
377
|
-
const rel = `.claude/agents/${agent}.md`;
|
|
378
|
-
if (!memory.includes(rel)) errors.push(`Harness/MEMORY.md missing agent registration: ${rel}`);
|
|
379
|
-
}
|
|
380
|
-
for (const skill of commonSkills) {
|
|
381
|
-
const rel = `.claude/skills/${skill}/SKILL.md`;
|
|
382
|
-
if (!memory.includes(rel)) errors.push(`Harness/MEMORY.md missing skill registration: ${rel}`);
|
|
383
|
-
}
|
|
384
|
-
for (const rel of memoryFiles) {
|
|
385
|
-
const relativeRel = rel.replace(/^Harness\//, '');
|
|
386
|
-
if (!memory.includes(rel) && !memory.includes(relativeRel)) {
|
|
387
|
-
errors.push(`Harness/MEMORY.md missing memory file registration: ${rel}`);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
for (const workflow of listMarkdownFiles('Harness/workflows')) {
|
|
393
|
-
const relativeWorkflow = workflow.replace(/^Harness\//, '');
|
|
394
|
-
if (!docsReadme.includes(workflow) && !docsReadme.includes(relativeWorkflow) && !memory.includes(workflow) && !memory.includes(relativeWorkflow)) {
|
|
395
|
-
errors.push(`workflow is not registered in Harness/README.md or Harness/MEMORY.md: ${workflow}`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
393
|
+
requireText('Harness/README.md', 'subagent', 'subagent orchestrator entry trigger');
|
|
394
|
+
requireText('Harness/README.md', 'PROGRESS.md', 'PROGRESS global task index');
|
|
395
|
+
requireText('CLAUDE.md', 'Harness/tasks/', 'task capsule directory reference');
|
|
396
|
+
requireText('Harness/subagents.md', 'Subagents are readers and reporters', 'subagent state committer rule');
|
|
397
|
+
for (const heading of ['## 2. Think Before Coding', '## 3. Simplicity First', '## 4. Surgical Changes', '## 5. Goal-Driven Execution']) {
|
|
398
|
+
requireText('CLAUDE.md', heading, `Karpathy-style rule heading: ${heading}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Root PROGRESS.md structure check
|
|
402
|
+
const progress = read('Harness/PROGRESS.md');
|
|
403
|
+
if (progress) {
|
|
404
|
+
for (const heading of ['## Active Task', '## Task Index', '## Cross-Task Decisions']) {
|
|
405
|
+
if (!progress.includes(heading)) errors.push(`Harness/PROGRESS.md missing heading: ${heading}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Legacy PLAN.md deprecation check (removed — stub only)
|
|
410
|
+
|
|
411
|
+
const dispatch = read('Harness/dispatch.md');
|
|
412
|
+
if (dispatch) {
|
|
413
|
+
for (const agent of commonAgents) {
|
|
414
|
+
if (!dispatch.includes(`\`${agent}\``)) errors.push(`Harness/dispatch.md missing common agent: ${agent}`);
|
|
415
|
+
}
|
|
416
|
+
if (!dispatch.includes('## Handoff Format')) errors.push('Harness/dispatch.md missing heading: ## Handoff Format');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const contextLoading = read('Harness/context-loading.md');
|
|
420
|
+
if (contextLoading) {
|
|
421
|
+
if (!contextLoading.includes('Harness/README.md` is the primary Harness documentation router')) {
|
|
422
|
+
errors.push('Harness/context-loading.md must declare Harness/README.md as the primary Harness documentation router');
|
|
423
|
+
}
|
|
424
|
+
for (const pack of contextPacks) {
|
|
425
|
+
if (!contextLoading.includes(pack)) errors.push(`Harness/context-loading.md missing subagent pack: ${pack}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const memory = read('Harness/MEMORY.md');
|
|
430
|
+
if (memory) {
|
|
431
|
+
for (const agent of commonAgents) {
|
|
432
|
+
const rel = `.claude/agents/${agent}.md`;
|
|
433
|
+
if (!memory.includes(rel)) errors.push(`Harness/MEMORY.md missing agent registration: ${rel}`);
|
|
434
|
+
}
|
|
435
|
+
for (const skill of commonSkills) {
|
|
436
|
+
const rel = `.claude/skills/${skill}/SKILL.md`;
|
|
437
|
+
if (!memory.includes(rel)) errors.push(`Harness/MEMORY.md missing skill registration: ${rel}`);
|
|
438
|
+
}
|
|
439
|
+
for (const rel of memoryFiles) {
|
|
440
|
+
const relativeRel = rel.replace(/^Harness\//, '');
|
|
441
|
+
if (!memory.includes(rel) && !memory.includes(relativeRel)) {
|
|
442
|
+
errors.push(`Harness/MEMORY.md missing memory file registration: ${rel}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
for (const workflow of listMarkdownFiles('Harness/workflows')) {
|
|
448
|
+
const relativeWorkflow = workflow.replace(/^Harness\//, '');
|
|
449
|
+
if (!docsReadme.includes(workflow) && !docsReadme.includes(relativeWorkflow) && !memory.includes(workflow) && !memory.includes(relativeWorkflow)) {
|
|
450
|
+
errors.push(`workflow is not registered in Harness/README.md or Harness/MEMORY.md: ${workflow}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
399
454
|
for (const skillFile of registeredSkillFiles(docsReadme, memory)) {
|
|
400
455
|
if (!fs.existsSync(path.join(root, skillFile))) {
|
|
401
456
|
errors.push(`registered skill file is missing: ${skillFile}`);
|
|
@@ -415,131 +470,181 @@ for (const command of listedWorkflowCommands(docsReadme, read('.claude/commands/
|
|
|
415
470
|
}
|
|
416
471
|
|
|
417
472
|
for (const workflowFile of registeredWorkflowFiles(docsReadme, memory)) {
|
|
418
|
-
if (!fs.existsSync(path.join(root, workflowFile))) {
|
|
419
|
-
// Optional workflows (browser-e2e, ts-react-frontend, etc.) may not be installed
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function requireUiSelectorContract(rel) {
|
|
425
|
-
const text = read(rel);
|
|
426
|
-
if (!text) return;
|
|
427
|
-
|
|
428
|
-
const markers = [
|
|
429
|
-
'data-testid',
|
|
430
|
-
'accessible labels/roles',
|
|
431
|
-
'inputs, buttons, filters, rows, empty/error/loading states',
|
|
432
|
-
];
|
|
433
|
-
|
|
434
|
-
if (markers.some(marker => !text.includes(marker))) {
|
|
435
|
-
errors.push(`${rel} missing stable UI selector contract`);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
requireUiSelectorContract('Harness/workflows/browser-e2e.md');
|
|
440
|
-
requireUiSelectorContract('Harness/workflows/ts-react-frontend.md');
|
|
441
|
-
|
|
442
|
-
for (const skill of commonSkills) {
|
|
443
|
-
const rel = `.claude/skills/${skill}/SKILL.md`;
|
|
444
|
-
const text = read(rel);
|
|
445
|
-
if (!text) continue;
|
|
446
|
-
if (frontmatterField(text, 'name') !== skill) errors.push(`${rel} frontmatter name does not match directory`);
|
|
447
|
-
if (!frontmatterField(text, 'description')) errors.push(`${rel} missing frontmatter field: description`);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
for (const skill of commonSkills) {
|
|
451
|
-
const claudeRel = `.claude/skills/${skill}/SKILL.md`;
|
|
452
|
-
const codexRel = `.agents/skills/${skill}/SKILL.md`;
|
|
453
|
-
const claudeText = read(claudeRel);
|
|
454
|
-
const codexText = read(codexRel);
|
|
455
|
-
if (!claudeText || !codexText) continue;
|
|
456
|
-
if (claudeText !== codexText) errors.push(`${codexRel} must mirror ${claudeRel}`);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
for (const skill of listDirectories('.claude/skills')) {
|
|
460
|
-
if (commonSkills.includes(skill)) continue;
|
|
461
|
-
|
|
462
|
-
const rel = `.claude/skills/${skill}/SKILL.md`;
|
|
463
|
-
const text = read(rel);
|
|
464
|
-
if (!text) continue;
|
|
465
|
-
|
|
466
|
-
if (frontmatterField(text, 'name') !== skill) errors.push(`${rel} frontmatter name does not match directory`);
|
|
467
|
-
if (!frontmatterField(text, 'description')) errors.push(`${rel} missing frontmatter field: description`);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
for (const skill of listDirectories('.agents/skills')) {
|
|
471
|
-
const rel = `.agents/skills/${skill}/SKILL.md`;
|
|
472
|
-
const text = read(rel);
|
|
473
|
-
if (!text) continue;
|
|
474
|
-
|
|
475
|
-
if (frontmatterField(text, 'name') !== skill) errors.push(`${rel} frontmatter name does not match directory`);
|
|
476
|
-
if (!frontmatterField(text, 'description')) errors.push(`${rel} missing frontmatter field: description`);
|
|
477
|
-
|
|
478
|
-
const claudeMirror = `.claude/skills/${skill}/SKILL.md`;
|
|
479
|
-
if (fs.existsSync(path.join(root, claudeMirror)) && text !== read(claudeMirror)) {
|
|
480
|
-
errors.push(`${rel} must mirror ${claudeMirror}`);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
for (const agent of commonAgents) {
|
|
485
|
-
const rel = `.claude/agents/${agent}.md`;
|
|
486
|
-
const text = read(rel);
|
|
487
|
-
if (!text) continue;
|
|
488
|
-
|
|
489
|
-
for (const field of ['name', 'description', 'tools', 'model']) {
|
|
490
|
-
if (!frontmatterField(text, field)) errors.push(`${rel} missing frontmatter field: ${field}:`);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (frontmatterField(text, 'name') !== agent) {
|
|
494
|
-
errors.push(`${rel} frontmatter name does not match filename`);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const skill = frontmatterField(text, 'skills');
|
|
498
|
-
if (skill && !commonSkills.includes(skill)) {
|
|
499
|
-
errors.push(`${rel} references unknown skill: ${skill}`);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (skill && !fs.existsSync(path.join(root, `.claude/skills/${skill}/SKILL.md`))) {
|
|
503
|
-
errors.push(`${rel} references missing skill file: .claude/skills/${skill}/SKILL.md`);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
473
|
+
if (!fs.existsSync(path.join(root, workflowFile))) {
|
|
474
|
+
// Optional workflows (browser-e2e, ts-react-frontend, etc.) may not be installed
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function requireUiSelectorContract(rel) {
|
|
480
|
+
const text = read(rel);
|
|
481
|
+
if (!text) return;
|
|
482
|
+
|
|
483
|
+
const markers = [
|
|
484
|
+
'data-testid',
|
|
485
|
+
'accessible labels/roles',
|
|
486
|
+
'inputs, buttons, filters, rows, empty/error/loading states',
|
|
487
|
+
];
|
|
488
|
+
|
|
489
|
+
if (markers.some(marker => !text.includes(marker))) {
|
|
490
|
+
errors.push(`${rel} missing stable UI selector contract`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
requireUiSelectorContract('Harness/workflows/browser-e2e.md');
|
|
495
|
+
requireUiSelectorContract('Harness/workflows/ts-react-frontend.md');
|
|
496
|
+
|
|
497
|
+
for (const skill of commonSkills) {
|
|
498
|
+
const rel = `.claude/skills/${skill}/SKILL.md`;
|
|
499
|
+
const text = read(rel);
|
|
500
|
+
if (!text) continue;
|
|
501
|
+
if (frontmatterField(text, 'name') !== skill) errors.push(`${rel} frontmatter name does not match directory`);
|
|
502
|
+
if (!frontmatterField(text, 'description')) errors.push(`${rel} missing frontmatter field: description`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
for (const skill of commonSkills) {
|
|
506
|
+
const claudeRel = `.claude/skills/${skill}/SKILL.md`;
|
|
507
|
+
const codexRel = `.agents/skills/${skill}/SKILL.md`;
|
|
508
|
+
const claudeText = read(claudeRel);
|
|
509
|
+
const codexText = read(codexRel);
|
|
510
|
+
if (!claudeText || !codexText) continue;
|
|
511
|
+
if (claudeText !== codexText) errors.push(`${codexRel} must mirror ${claudeRel}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
for (const skill of listDirectories('.claude/skills')) {
|
|
515
|
+
if (commonSkills.includes(skill)) continue;
|
|
516
|
+
|
|
517
|
+
const rel = `.claude/skills/${skill}/SKILL.md`;
|
|
518
|
+
const text = read(rel);
|
|
519
|
+
if (!text) continue;
|
|
520
|
+
|
|
521
|
+
if (frontmatterField(text, 'name') !== skill) errors.push(`${rel} frontmatter name does not match directory`);
|
|
522
|
+
if (!frontmatterField(text, 'description')) errors.push(`${rel} missing frontmatter field: description`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
for (const skill of listDirectories('.agents/skills')) {
|
|
526
|
+
const rel = `.agents/skills/${skill}/SKILL.md`;
|
|
527
|
+
const text = read(rel);
|
|
528
|
+
if (!text) continue;
|
|
529
|
+
|
|
530
|
+
if (frontmatterField(text, 'name') !== skill) errors.push(`${rel} frontmatter name does not match directory`);
|
|
531
|
+
if (!frontmatterField(text, 'description')) errors.push(`${rel} missing frontmatter field: description`);
|
|
532
|
+
|
|
533
|
+
const claudeMirror = `.claude/skills/${skill}/SKILL.md`;
|
|
534
|
+
if (fs.existsSync(path.join(root, claudeMirror)) && text !== read(claudeMirror)) {
|
|
535
|
+
errors.push(`${rel} must mirror ${claudeMirror}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
for (const agent of commonAgents) {
|
|
540
|
+
const rel = `.claude/agents/${agent}.md`;
|
|
541
|
+
const text = read(rel);
|
|
542
|
+
if (!text) continue;
|
|
543
|
+
|
|
544
|
+
for (const field of ['name', 'description', 'tools', 'model']) {
|
|
545
|
+
if (!frontmatterField(text, field)) errors.push(`${rel} missing frontmatter field: ${field}:`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (frontmatterField(text, 'name') !== agent) {
|
|
549
|
+
errors.push(`${rel} frontmatter name does not match filename`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const skill = frontmatterField(text, 'skills');
|
|
553
|
+
if (skill && !commonSkills.includes(skill)) {
|
|
554
|
+
errors.push(`${rel} references unknown skill: ${skill}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (skill && !fs.existsSync(path.join(root, `.claude/skills/${skill}/SKILL.md`))) {
|
|
558
|
+
errors.push(`${rel} references missing skill file: .claude/skills/${skill}/SKILL.md`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
for (const agent of commonAgents) {
|
|
563
|
+
const rel = `.opencode/agents/${agent}.md`;
|
|
564
|
+
const text = read(rel);
|
|
565
|
+
if (!text) continue;
|
|
566
|
+
|
|
567
|
+
for (const field of ['description', 'mode']) {
|
|
568
|
+
if (!frontmatterField(text, field)) errors.push(`${rel} missing frontmatter field: ${field}:`);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const mode = frontmatterField(text, 'mode');
|
|
572
|
+
if (mode && mode !== 'subagent') {
|
|
573
|
+
errors.push(`${rel} frontmatter mode should be subagent (got: ${mode})`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// OpenCode mirror sync: .claude is the only editable source; .opencode files are
|
|
578
|
+
// format-converted mirrors. Frontmatter is platform-specific, but description and
|
|
579
|
+
// markdown body must match the .claude source exactly (edit .claude, then copy).
|
|
580
|
+
function markdownBody(text) {
|
|
581
|
+
return text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '').replace(/\r\n/g, '\n').trim();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
for (const opencodeRel of listMarkdownFiles('.opencode/agents')) {
|
|
585
|
+
const fileName = opencodeRel.split('/').pop();
|
|
586
|
+
const claudeRel = `.claude/agents/${fileName}`;
|
|
587
|
+
const opencodeText = read(opencodeRel);
|
|
588
|
+
if (!opencodeText) continue;
|
|
589
|
+
const claudeText = read(claudeRel);
|
|
590
|
+
if (!claudeText) {
|
|
591
|
+
errors.push(`${opencodeRel} has no .claude source: ${claudeRel} (add the .claude agent first)`);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (frontmatterField(opencodeText, 'description') !== frontmatterField(claudeText, 'description')) {
|
|
595
|
+
errors.push(`${opencodeRel} description must mirror ${claudeRel} (edit the .claude source, then copy)`);
|
|
596
|
+
}
|
|
597
|
+
if (markdownBody(opencodeText) !== markdownBody(claudeText)) {
|
|
598
|
+
errors.push(`${opencodeRel} body must mirror ${claudeRel} (edit the .claude source, then copy)`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
for (const claudeRel of listMarkdownFiles('.claude/commands')) {
|
|
603
|
+
const fileName = claudeRel.split('/').pop();
|
|
604
|
+
const opencodeRel = `.opencode/commands/${fileName}`;
|
|
605
|
+
const claudeText = read(claudeRel);
|
|
606
|
+
if (!claudeText) continue;
|
|
607
|
+
const opencodeText = read(opencodeRel);
|
|
608
|
+
if (!opencodeText) {
|
|
609
|
+
errors.push(`missing OpenCode command mirror: ${opencodeRel} (copy from ${claudeRel})`);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (markdownBody(opencodeText) !== markdownBody(claudeText)) {
|
|
613
|
+
errors.push(`${opencodeRel} body must mirror ${claudeRel} (edit the .claude source, then copy)`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
507
617
|
requireText('Harness/extension.md', 'Skills should extend the harness');
|
|
508
618
|
requireText('Harness/agent-workflow.md', 'Harness/tasks/<task-id>/PROGRESS.md');
|
|
509
|
-
requireText('CLAUDE.md', 'Keep task records compact', 'compact task record startup rule');
|
|
510
619
|
requireText('Harness/README.md', 'Task records are compact by default', 'compact task record router rule');
|
|
511
620
|
requireText('Harness/agent-workflow.md', 'AC record size', 'compact AC record size rule');
|
|
512
621
|
requireText('Harness/ACCEPTANCE_PROTOCOL.md', 'Default to 1-3 concise ACs', 'compact AC default');
|
|
513
|
-
requireText('Harness/WF.md', '
|
|
622
|
+
requireText('Harness/WF.md', 'task-scribe', 'compact heartbeat rule');
|
|
514
623
|
requireText('Harness/tasks/_template/PLAN.md', 'Compact task record', 'compact task PLAN template');
|
|
515
624
|
requireText('Harness/tasks/_template/PLAN.md', 'Default: keep 1-3 concise ACs', 'compact task PLAN AC guidance');
|
|
516
625
|
requireText('Harness/tasks/_template/PROGRESS.md', 'Compact heartbeat', 'compact task PROGRESS template');
|
|
517
626
|
requireText('Harness/agent-workflow.md', 'TDD-GUIDE.md', 'agent workflow loads TDD guide');
|
|
518
627
|
requireText('Harness/agent-workflow.md', 'real user-path test', 'agent workflow real user path requirement');
|
|
519
628
|
requireText('Harness/research/README.md', 'research-results.md');
|
|
520
|
-
requireText('Harness/WF.md', '
|
|
521
|
-
requireText('Harness/WF.md', '
|
|
522
|
-
requireText('Harness/WF.md', '
|
|
523
|
-
requireText('Harness/WF.md', '
|
|
524
|
-
requireText('Harness/WF.md', 'Independent Validation', 'WF independent validation role');
|
|
525
|
-
requireText('Harness/WF.md', 'reflector', 'WF reflector gate');
|
|
526
|
-
requireText('Harness/WF.md', '.claude/agents/', 'WF built-in agent roster path');
|
|
527
|
-
requireText('Harness/WF.md', 'Collaboration decision tree', 'WF decision tree');
|
|
629
|
+
requireText('Harness/WF.md', 'Orchestration Loop', 'WF orchestration loop');
|
|
630
|
+
requireText('Harness/WF.md', 'WF-KERNEL.md', 'WF references WF-KERNEL');
|
|
631
|
+
requireText('Harness/WF.md', 'Cross-review and reflector NOT mandatory', 'WF-Light no mandatory cross-review');
|
|
632
|
+
requireText('Harness/WF.md', '`.claude/agents/`', 'WF agent roster path');
|
|
528
633
|
requireText('Harness/WF.md', 'Harness/tasks/', 'WF task directory reference');
|
|
529
|
-
requireText('Harness/
|
|
530
|
-
requireText('Harness/README.md', '
|
|
531
|
-
requireText('Harness/
|
|
532
|
-
requireText('.claude/skills/wf/SKILL.md', '
|
|
533
|
-
requireText('.agents/skills/wf/SKILL.md', 'Harness/WF.md', 'Codex wf skill loads core WF doc');
|
|
534
|
-
requireText('.claude/skills/subagent-orchestrator/SKILL.md', 'Harness/subagents.md', 'subagent-orchestrator loads subagents doc');
|
|
535
|
-
requireText('.claude/skills/subagent-orchestrator/SKILL.md', '.claude/agents/', 'subagent-orchestrator built-in agent roster path');
|
|
536
|
-
requireText('.claude/skills/subagent-orchestrator/SKILL.md', '
|
|
537
|
-
requireText('.claude/skills/wf-readme/SKILL.md', 'README.md', 'wf-readme loads README');
|
|
538
|
-
requireText('.claude/skills/wf-readme/SKILL.md', 'Harness/architecture.md', 'wf-readme links architecture docs');
|
|
539
|
-
requireText('Harness/subagents.md', '## Source Attribution', 'subagent source attribution');
|
|
540
|
-
requireText('Harness/subagents.md', 'npx skills find', 'find-skills discovery attribution');
|
|
541
|
-
requireText('Harness/subagents.md', 'superpowers:dispatching-parallel-agents', 'parallel-agent source attribution');
|
|
542
|
-
requireText('Harness/subagents.md', 'superpowers:subagent-driven-development', 'subagent-driven source attribution');
|
|
634
|
+
requireText('Harness/README.md', 'explicit WF token', 'WF explicit contract');
|
|
635
|
+
requireText('Harness/README.md', 'tier-gated acceptance', 'WF tiered acceptance router output');
|
|
636
|
+
requireText('.claude/skills/wf/SKILL.md', 'Harness/WF.md', 'wf skill loads core WF doc');
|
|
637
|
+
requireText('.claude/skills/wf/SKILL.md', 'Tier-aware acceptance', 'wf skill tier-aware acceptance');
|
|
638
|
+
requireText('.agents/skills/wf/SKILL.md', 'Harness/WF.md', 'Codex wf skill loads core WF doc');
|
|
639
|
+
requireText('.claude/skills/subagent-orchestrator/SKILL.md', 'Harness/subagents.md', 'subagent-orchestrator loads subagents doc');
|
|
640
|
+
requireText('.claude/skills/subagent-orchestrator/SKILL.md', '.claude/agents/', 'subagent-orchestrator built-in agent roster path');
|
|
641
|
+
requireText('.claude/skills/subagent-orchestrator/SKILL.md', 'WF/WF-MAX requires tier-specific', 'subagent-orchestrator WF tier-specific contract');
|
|
642
|
+
requireText('.claude/skills/wf-readme/SKILL.md', 'README.md', 'wf-readme loads README');
|
|
643
|
+
requireText('.claude/skills/wf-readme/SKILL.md', 'Harness/architecture.md', 'wf-readme links architecture docs');
|
|
644
|
+
requireText('Harness/subagents.md', '## Source Attribution', 'subagent source attribution');
|
|
645
|
+
requireText('Harness/subagents.md', 'npx skills find', 'find-skills discovery attribution');
|
|
646
|
+
requireText('Harness/subagents.md', 'superpowers:dispatching-parallel-agents', 'parallel-agent source attribution');
|
|
647
|
+
requireText('Harness/subagents.md', 'superpowers:subagent-driven-development', 'subagent-driven source attribution');
|
|
543
648
|
requireText('Harness/subagents.md', '## Built-in Agent Roster', 'built-in agent roster');
|
|
544
649
|
requireText('Harness/subagents.md', '## WF Default Fan-Out', 'WF default fan-out');
|
|
545
650
|
requireText('Harness/subagents.md', 'concrete conditions', 'subagent decision tree');
|
|
@@ -548,25 +653,25 @@ requireText('Harness/subagents.md', '`reflector`', 'reflector agent roster');
|
|
|
548
653
|
requireText('Harness/subagents.md', 'reflector PASS', 'reflector review gate');
|
|
549
654
|
requireText('Harness/subagents.md', '## Efficiency Ladder', 'subagent efficiency ladder');
|
|
550
655
|
requireText('Harness/subagents.md', '## Review Gates', 'subagent review gates');
|
|
551
|
-
requireText('Harness/architecture.md', '## 2. Interface Decoupling', 'architecture interface decoupling');
|
|
552
|
-
requireText('Harness/architecture.md', '## 3. State Design', 'architecture state design');
|
|
553
|
-
requireText('Harness/architecture.md', 'Avoid speculative abstraction', 'anti-overengineering architecture rule');
|
|
554
|
-
requireText('CLAUDE.md', 'Use explicit interfaces or state models only when they protect a real boundary', 'CLAUDE interface/state simplicity rule');
|
|
555
|
-
requireText('
|
|
556
|
-
requireText('Harness/README.md', 'Need harness update', 'update routing row');
|
|
656
|
+
requireText('Harness/architecture.md', '## 2. Interface Decoupling', 'architecture interface decoupling');
|
|
657
|
+
requireText('Harness/architecture.md', '## 3. State Design', 'architecture state design');
|
|
658
|
+
requireText('Harness/architecture.md', 'Avoid speculative abstraction', 'anti-overengineering architecture rule');
|
|
659
|
+
requireText('CLAUDE.md', 'Use explicit interfaces or state models only when they protect a real boundary', 'CLAUDE interface/state simplicity rule');
|
|
660
|
+
requireText('Harness/README.md', '/wf-update', 'wf update startup instruction');
|
|
661
|
+
requireText('Harness/README.md', 'Need harness update', 'update routing row');
|
|
557
662
|
requireText('Harness/WF-MAX.md', 'three-layer architecture', 'WF-MAX three-layer architecture');
|
|
558
|
-
requireText('Harness/WF-MAX.md', '
|
|
559
|
-
requireText('Harness/WF-MAX.md', '
|
|
560
|
-
requireText('Harness/WF-MAX.md', '
|
|
561
|
-
requireText('Harness/WF-MAX.md', '
|
|
562
|
-
requireText('Harness/WF-MAX.md', '
|
|
663
|
+
requireText('Harness/WF-MAX.md', 'dispatch permissions', 'WF-MAX role separation');
|
|
664
|
+
requireText('Harness/WF-MAX.md', 'file_claim', 'WF-MAX coloring algorithm');
|
|
665
|
+
requireText('Harness/WF-MAX.md', 'CEO → Manager → Worker', 'WF-MAX wave dispatch');
|
|
666
|
+
requireText('Harness/WF-MAX.md', 'inherits the full', 'WF-MAX strict superset');
|
|
667
|
+
requireText('Harness/WF-MAX.md', 'Cross-CLI', 'WF-MAX cross-CLI overflow');
|
|
563
668
|
requireText('Harness/WF-MAX.md', 'claude -p', 'WF-MAX Claude CLI overflow');
|
|
564
669
|
requireText('Harness/WF-MAX.md', 'codex exec', 'WF-MAX Codex CLI overflow');
|
|
565
670
|
requireText('Harness/WF-MAX.md', 'agents.max_threads', 'WF-MAX Codex max_threads config');
|
|
566
671
|
requireText('Harness/WF-MAX.md', 'agents.max_depth', 'WF-MAX Codex max_depth config');
|
|
567
672
|
requireText('Harness/WF-MAX.md', 'max_threads = 12', 'WF-MAX Codex scaffold max_threads default');
|
|
568
673
|
requireText('Harness/WF-MAX.md', 'max_depth = 1', 'WF-MAX Codex scaffold max_depth default');
|
|
569
|
-
requireText('Harness/WF-MAX.md', '
|
|
674
|
+
requireText('Harness/WF-MAX.md', 'Ask the user before raising', 'WF-MAX asks before raising Codex thread cap');
|
|
570
675
|
requireText('Harness/WF-MAX.md', 'Close completed agents', 'WF-MAX close completed agents before overflow');
|
|
571
676
|
requireText('Harness/WF-MAX.md', 'Codex++', 'WF-MAX forbids relying on Codex++ as stable capacity');
|
|
572
677
|
requireText('.claude/skills/wf-max/SKILL.md', 'agents.max_threads', 'wf-max skill Codex thread config');
|
|
@@ -576,6 +681,8 @@ requireText('.claude/skills/wf-max/SKILL.md', 'Codex++', 'wf-max skill forbids C
|
|
|
576
681
|
requireText('.codex/config.toml', '[agents]', 'Codex agents config table');
|
|
577
682
|
requireText('.codex/config.toml', 'max_threads = 12', 'Codex scaffold max_threads default');
|
|
578
683
|
requireText('.codex/config.toml', 'max_depth = 1', 'Codex scaffold max_depth default');
|
|
684
|
+
requireText('opencode.json', '"$schema": "https://opencode.ai/config.json"', 'OpenCode config schema');
|
|
685
|
+
requireText('opencode.json', '.claude/rules/ecc/common.md', 'OpenCode instructions reference to ECC rules');
|
|
579
686
|
requireText('Harness/README.md', '/wf-max', 'wf max router alias');
|
|
580
687
|
requireText('Harness/README.md', 'WF-MAX.md', 'WF-MAX router reference');
|
|
581
688
|
requireText('Harness/README.md', 'ACCEPTANCE_PROTOCOL.md', 'acceptance protocol router reference');
|
|
@@ -587,6 +694,16 @@ requireText('Harness/ACCEPTANCE_PROTOCOL.md', 'Syntax-only checks', 'syntax-only
|
|
|
587
694
|
requireText('Harness/AGENT_ISOLATION.md', 'implementer', 'agent isolation implementer rule');
|
|
588
695
|
requireText('Harness/HARNESS_BRIDGE.md', 'Network Trace Collector', 'harness bridge network trace collector');
|
|
589
696
|
requireText('Harness/WF-AUTO.md', 'Intent Checkpoint', 'wf-auto intent checkpoint');
|
|
697
|
+
requireText('Harness/WF-AUTO.md', 'Adaptive Coverage Exhaustion Gate', 'wf-auto adaptive coverage exhaustion gate');
|
|
698
|
+
requireText('Harness/WF-AUTO.md', 'dynamic high-risk obligations', 'wf-auto dynamic high-risk obligations');
|
|
699
|
+
requireText('Harness/WF-AUTO.md', 'two different confirmation strategies', 'wf-auto two different confirmation strategies');
|
|
700
|
+
requireText('Harness/WF-AUTO-ANGLES.md', 'Selection algorithm', 'wf-auto adaptive angle selection');
|
|
701
|
+
requireText('Harness/WF-AUTO-ANGLES.md', 'Dynamic obligations', 'wf-auto dynamic obligations');
|
|
702
|
+
requireText('Harness/WF-AUTO-ANGLES.md', 'Common probe recipes', 'wf-auto common probe recipes');
|
|
703
|
+
forbidText('Harness/WF-AUTO.md', 'All 8 exhausted', 'stale eight-angle exhaustion rule');
|
|
704
|
+
forbidText('Harness/WF-AUTO.md', '3 consecutive all-exhausted rounds', 'stale three-consecutive-all-exhausted stop rule');
|
|
705
|
+
forbidText('Harness/WF-AUTO.md', '8-Angle Exhaustion Gate', 'stale eight-angle exhaustion gate name');
|
|
706
|
+
forbidText('Harness/WF-AUTO-SPARK.md', '8 parallel external searches', 'stale eight-spark fixed-count search');
|
|
590
707
|
requireText('Harness/WF-AUTO.md', 'Inherited WF/WF-MAX Constraints', 'wf-auto inherited WF/WF-MAX constraints');
|
|
591
708
|
requireText('Harness/WF-AUTO.md', 'Mini PRD -> AC IDs -> test/validation plan -> implementer -> verifier', 'wf-auto per-cycle WF chain');
|
|
592
709
|
requireText('Harness/WF-AUTO.md', 'reflector PASS', 'wf-auto reflector gate');
|
|
@@ -617,8 +734,8 @@ requireText('.claude/agents/reflector.md', 'PASS, RETURN_TO_DEBUG, or BLOCKED',
|
|
|
617
734
|
requireText('Harness/DEBUG_PROTOCOL.md', 'Layer Classification', 'debug layer classification');
|
|
618
735
|
requireText('Harness/MEMORY_PROTOCOL.md', 'AC ID', 'memory AC traceability');
|
|
619
736
|
requireText('Harness/subagents.md', 'Max parallelism', 'subagents max parallelism row');
|
|
620
|
-
requireText('Harness/dispatch.md', 'Concurrency group', 'dispatch concurrency group field');
|
|
621
|
-
requireText('Harness/dispatch.md', 'File claim', 'dispatch file claim field');
|
|
737
|
+
requireText('Harness/dispatch.md', 'Concurrency group', 'dispatch concurrency group field');
|
|
738
|
+
requireText('Harness/dispatch.md', 'File claim', 'dispatch file claim field');
|
|
622
739
|
requireText('CLAUDE.md', '/wf-max', 'wf max startup instruction');
|
|
623
740
|
requireText('.claude/commands/wf-help.md', 'Do not invoke a skill', 'wf-help direct command boundary');
|
|
624
741
|
requireText('.claude/commands/wf-help.md', '| `/wf-help` |', 'wf-help command row');
|
|
@@ -627,8 +744,8 @@ requireText('.claude/commands/wf-help.md', '| `/wf-auto` |', 'wf-help wf-auto ro
|
|
|
627
744
|
requireText('.claude/commands/wf-help.md', '| `/wf-readme <task>` |', 'wf-help wf-readme row');
|
|
628
745
|
requireText('Harness/README.md', '## Direct Commands', 'direct commands section');
|
|
629
746
|
requireText('Harness/README.md', '.claude/commands/wf-help.md', 'wf-help router reference');
|
|
630
|
-
requireText('
|
|
631
|
-
requireText('
|
|
747
|
+
requireText('Harness/WF-MAX.md', 'three-layer architecture', 'CLAUDE.md three-layer role architecture');
|
|
748
|
+
requireText('Harness/README.md', 'no runtime hook state', 'CLAUDE no-hook role enforcement statement');
|
|
632
749
|
requireText('Harness/README.md', 'no runtime hook state', 'README no-hook runtime statement');
|
|
633
750
|
requireText('Harness/WF-AUTO.md', 'WF-AUTO Hook Exception', 'wf-auto-only hook exception');
|
|
634
751
|
requireText('Harness/WF-AUTO.md', 'only `/wf-auto` may use a runtime hook', 'wf-auto-only hook boundary');
|
|
@@ -658,13 +775,135 @@ if (claudeSettings.includes('"hooks"') && !claudeSettings.includes('wf-auto')) {
|
|
|
658
775
|
if (read('.codex/config.toml').includes('hooks = true') && !codexHookConfig.includes('wf-auto')) {
|
|
659
776
|
errors.push('.codex/config.toml may enable hooks only with a wf-auto hook configuration');
|
|
660
777
|
}
|
|
778
|
+
|
|
779
|
+
// AGENTS.md must be a thin shim — no Harness/WF-MAX/command table content
|
|
780
|
+
forbidText('AGENTS.md', 'WF-MAX Role Contract', 'AGENTS.md WF-MAX section');
|
|
781
|
+
forbidText('AGENTS.md', 'Key Commands', 'AGENTS.md command table');
|
|
782
|
+
forbidText('AGENTS.md', 'Harness/MEMORY.md', 'AGENTS.md Harness routing (must defer to CLAUDE.md)');
|
|
783
|
+
forbidText('AGENTS.md', 'Harness/README.md', 'AGENTS.md Harness routing (must defer to CLAUDE.md)');
|
|
784
|
+
requireText('AGENTS.md', 'CLAUDE.md', 'AGENTS.md CLAUDE.md reference');
|
|
785
|
+
requireText('AGENTS.md', 'compatibility entry', 'AGENTS.md Codex shim purpose');
|
|
786
|
+
requireText('AGENTS.md', 'single source', 'AGENTS.md CLAUDE.md as single source');
|
|
787
|
+
|
|
788
|
+
// Implicit WF trigger phrases forbidden in active runtime docs and SETUP
|
|
789
|
+
for (const rel of ['Harness/README.md', 'Harness/WF.md', 'Harness/context-loading.md', 'Harness/subagents.md', 'Harness/SETUP.md']) {
|
|
790
|
+
forbidText(rel, 'workflow mode', 'implicit WF trigger: workflow mode');
|
|
791
|
+
forbidText(rel, 'wk mode', 'implicit WF trigger: wk mode');
|
|
792
|
+
}
|
|
793
|
+
// SETUP must not require old WF complete-role-chain contract
|
|
794
|
+
requireText('Harness/SETUP.md', 'explicit WF entry only', 'SETUP explicit WF entry contract');
|
|
795
|
+
requireText('Harness/SETUP.md', 'WF-Light', 'SETUP WF-Light tier reference');
|
|
796
|
+
requireText('Harness/SETUP.md', 'WF-Max-Useful', 'SETUP WF-Max-Useful tier reference');
|
|
797
|
+
|
|
798
|
+
// Memory Preflight markers required in workflow skill adapters
|
|
799
|
+
for (const skill of ['wf', 'wf-max', 'subagent-orchestrator']) {
|
|
800
|
+
requireText(`.claude/skills/${skill}/SKILL.md`, 'Memory Preflight', `${skill} skill Memory Preflight section`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Direct command checks
|
|
804
|
+
requireText('Harness/README.md', '/wf-update', 'wf-update direct command reference');
|
|
805
|
+
requireText('.claude/commands/wf-update.md', 'Do not invoke a skill', 'wf-update direct command boundary');
|
|
806
|
+
requireText('.claude/commands/wf-help.md', 'direct command', 'wf-help wf-update direct command classification');
|
|
807
|
+
|
|
808
|
+
// wf-update skill must NOT claim Claude Code /wf-update as a skill invocation
|
|
809
|
+
// (allow mentions in the description/body that say "direct command" — those are correct)
|
|
810
|
+
forbidText('.claude/skills/wf-update/SKILL.md', 'Claude Code: use', 'wf-update skill Claude Code skill-claim');
|
|
811
|
+
forbidText('.claude/skills/wf-update/SKILL.md', 'Claude Code and Codex', 'wf-update skill old dual-platform claim');
|
|
812
|
+
requireText('.claude/skills/wf-update/SKILL.md', 'Codex compatibility', 'wf-update skill Codex compatibility statement');
|
|
813
|
+
|
|
814
|
+
// OpenCode command parity
|
|
815
|
+
requireText('.opencode/commands/wf-help.md', 'Do not invoke a skill', 'OpenCode wf-help direct command boundary');
|
|
816
|
+
requireText('.opencode/commands/wf-update.md', 'Do not invoke a skill', 'OpenCode wf-update direct command boundary');
|
|
817
|
+
for (const command of opencodeWorkflowCommands) {
|
|
818
|
+
requireText(`.opencode/commands/${command}.md`, 'workflow command', `OpenCode ${command} workflow command classification`);
|
|
819
|
+
requireText(`.opencode/commands/${command}.md`, `.claude/skills/${command}/SKILL.md`, `OpenCode ${command} wrapper skill routing`);
|
|
820
|
+
requireText(`.opencode/commands/${command}.md`, 'Harness/MEMORY.md', `OpenCode ${command} wrapper router load`);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// wf-max adapter must stay tier-aware, not the old unconditional contract
|
|
824
|
+
forbidText('.claude/skills/wf-max/SKILL.md', 'every WF role, gate, and acceptance rule still', 'old wf-max unconditional role/gate inheritance');
|
|
825
|
+
forbidText('.claude/skills/wf-max/SKILL.md', 'Final acceptance requires verifier evidence, cross-review, and reflector PASS', 'old wf-max unconditional final acceptance');
|
|
826
|
+
requireText('.claude/skills/wf-max/SKILL.md', 'Final acceptance is tier-aware', 'wf-max tier-aware final acceptance');
|
|
827
|
+
|
|
828
|
+
// New small-fast agents
|
|
829
|
+
requireText('.claude/agents/task-scribe.md', 'model: haiku', 'task-scribe must use haiku model');
|
|
830
|
+
requireText('.claude/agents/codebase-explorer.md', 'model: haiku', 'codebase-explorer must use haiku model');
|
|
831
|
+
requireText('.claude/agents/codebase-explorer.md', 'read-only', 'codebase-explorer read-only contract');
|
|
832
|
+
|
|
833
|
+
// WF-KERNEL.md required invariants
|
|
834
|
+
requireText('Harness/WF-KERNEL.md', 'Ready-Queue', 'WF-KERNEL ready-queue section');
|
|
835
|
+
requireText('Harness/WF-KERNEL.md', 'Role / Model Matrix', 'WF-KERNEL role-model matrix');
|
|
836
|
+
requireText('Harness/WF-KERNEL.md', 'Dispatch Packet', 'WF-KERNEL dispatch packet format');
|
|
837
|
+
requireText('Harness/WF-KERNEL.md', 'Task Type', 'WF-KERNEL task type routing');
|
|
838
|
+
requireText('Harness/WF-KERNEL.md', 'small-fast', 'WF-KERNEL small-fast tier mapping');
|
|
839
|
+
|
|
840
|
+
// WF variants reference WF-KERNEL
|
|
841
|
+
requireText('Harness/WF.md', 'WF-KERNEL.md', 'WF.md references WF-KERNEL');
|
|
842
|
+
requireText('Harness/WF-MAX.md', 'WF-KERNEL.md', 'WF-MAX.md references WF-KERNEL');
|
|
843
|
+
|
|
844
|
+
// Tier-aware acceptance: WF-Light must NOT require global cross-review/reflector
|
|
845
|
+
requireText('Harness/WF.md', 'Cross-review and reflector NOT mandatory', 'WF.md WF-Light no mandatory cross-review');
|
|
846
|
+
requireText('Harness/WF-KERNEL.md', 'Cross-review and reflector are NOT mandatory', 'WF-KERNEL WF-Light no mandatory cross-review');
|
|
847
|
+
|
|
848
|
+
// WF-MAX: maximum safe fan-out, not unconditional
|
|
849
|
+
requireText('Harness/WF-MAX.md', 'maximum safe fan-out', 'WF-MAX maximum safe fan-out');
|
|
850
|
+
requireText('Harness/WF-MAX.md', 'WF-Max-Useful', 'WF-MAX useful fan-out mode');
|
|
851
|
+
requireText('Harness/WF-MAX.md', 'WF-Max-Strict', 'WF-MAX strict override mode');
|
|
852
|
+
|
|
853
|
+
// task-scribe is task-state write exception
|
|
854
|
+
requireText('Harness/subagents.md', 'task-scribe', 'subagents.md task-scribe registration');
|
|
855
|
+
requireText('Harness/dispatch.md', 'task-scribe', 'dispatch.md task-scribe roster');
|
|
856
|
+
requireText('Harness/WF-KERNEL.md', 'task-scribe', 'WF-KERNEL task-scribe ownership');
|
|
857
|
+
forbidText('Harness/WF-KERNEL.md', 'Only the main agent writes task', 'old single-writer rule');
|
|
858
|
+
|
|
859
|
+
// Old WF contract must not return to hot-path docs:
|
|
860
|
+
// /wf is WF-KERNEL tiered orchestration, not a default complete role chain;
|
|
861
|
+
// /wf-max defaults to WF-Max-Useful, unconditional fan-out is strict-only.
|
|
862
|
+
const hotPathDocs = [
|
|
863
|
+
'CLAUDE.md',
|
|
864
|
+
'Harness/MEMORY.md',
|
|
865
|
+
'Harness/README.md',
|
|
866
|
+
'Harness/WF.md',
|
|
867
|
+
'Harness/WF-MAX.md',
|
|
868
|
+
'Harness/WF-AUTO.md',
|
|
869
|
+
'Harness/WF-AUTO-SPARK.md',
|
|
870
|
+
'Harness/WF-KERNEL.md',
|
|
871
|
+
'Harness/agent-workflow.md',
|
|
872
|
+
'Harness/dispatch.md',
|
|
873
|
+
'Harness/subagents.md',
|
|
874
|
+
'Harness/context-loading.md',
|
|
875
|
+
'Harness/ACCEPTANCE_PROTOCOL.md',
|
|
876
|
+
];
|
|
877
|
+
for (const rel of hotPathDocs) {
|
|
878
|
+
forbidText(rel, 'complete role chain mandatory', 'old /wf default complete-role-chain contract');
|
|
879
|
+
forbidText(rel, 'default complete role chain', 'old /wf default complete-role-chain contract');
|
|
880
|
+
forbidText(rel, 'requires the complete role chain by default', 'old /wf default complete-role-chain contract');
|
|
881
|
+
forbidText(rel, 'mandatory maximum fan-out', 'old /wf-max unconditional fan-out contract');
|
|
882
|
+
forbidText(rel, 'strict superset: complete role chain plus maximum parallelism', 'old /wf-max strict-superset contract');
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// WF-STATE.md and TASK_ARCHIVE.md must be reachable from the entry router
|
|
886
|
+
requireText('CLAUDE.md', 'Harness/WF-STATE.md', 'CLAUDE.md WF-STATE routing');
|
|
887
|
+
requireText('CLAUDE.md', 'Harness/TASK_ARCHIVE.md', 'CLAUDE.md TASK_ARCHIVE routing');
|
|
888
|
+
requireText('Harness/README.md', 'WF-STATE.md', 'README WF-STATE routing');
|
|
889
|
+
requireText('Harness/TASK_ARCHIVE.md', 'archive-tasks.mjs', 'TASK_ARCHIVE script reference');
|
|
890
|
+
|
|
891
|
+
// Outer task capsule cap: keep Harness/tasks/ lean (see Harness/TASK_ARCHIVE.md)
|
|
892
|
+
const OUTER_TASK_CAP = 5;
|
|
893
|
+
const outerTasks = taskDirs.filter(name => !TASK_RESERVED.has(name) && !name.startsWith('_'));
|
|
894
|
+
if (outerTasks.length > OUTER_TASK_CAP) {
|
|
895
|
+
const capMsg = `Harness/tasks/ has ${outerTasks.length} outer task capsules (cap ${OUTER_TASK_CAP}); archive completed tasks with node Harness/scripts/archive-tasks.mjs --apply (see Harness/TASK_ARCHIVE.md)`;
|
|
896
|
+
if (strict) errors.push(capMsg);
|
|
897
|
+
else console.warn(`Warning: ${capMsg}`);
|
|
898
|
+
}
|
|
899
|
+
|
|
661
900
|
if (errors.length) {
|
|
662
|
-
console.error(`Harness validation failed${strict ? ' (strict)' : ''}:`);
|
|
663
|
-
for (const error of errors) console.error(`- ${error}`);
|
|
664
|
-
process.exit(1);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
console.log(`Harness validation passed${strict ? ' (strict)' : ''}.`);
|
|
668
|
-
if (!strict) {
|
|
669
|
-
console.log('Tip: run `node Harness/scripts/validate-harness.mjs --strict` after bootstrap to check unresolved project placeholders.');
|
|
670
|
-
}
|
|
901
|
+
console.error(`Harness validation failed${strict ? ' (strict)' : ''}:`);
|
|
902
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
903
|
+
process.exit(1);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
console.log(`Harness validation passed${strict ? ' (strict)' : ''}.`);
|
|
907
|
+
if (!strict) {
|
|
908
|
+
console.log('Tip: run `node Harness/scripts/validate-harness.mjs --strict` after bootstrap to check unresolved project placeholders.');
|
|
909
|
+
}
|