@kodevibe/harness 0.8.3
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/LICENSE +21 -0
- package/README.ko.md +351 -0
- package/README.md +314 -0
- package/bin/cli.js +4 -0
- package/harness/agent-memory/architect.md +42 -0
- package/harness/agent-memory/planner.md +47 -0
- package/harness/agent-memory/reviewer.md +46 -0
- package/harness/agent-memory/sprint-manager.md +49 -0
- package/harness/agents/architect.md +177 -0
- package/harness/agents/planner.md +320 -0
- package/harness/agents/reviewer.md +273 -0
- package/harness/agents/sprint-manager.md +250 -0
- package/harness/core-rules.md +136 -0
- package/harness/dependency-map.md +58 -0
- package/harness/failure-patterns.md +63 -0
- package/harness/features.md +53 -0
- package/harness/project-brief.md +145 -0
- package/harness/project-state.md +85 -0
- package/harness/skills/bootstrap.md +326 -0
- package/harness/skills/code-review-pr.md +141 -0
- package/harness/skills/deployment.md +144 -0
- package/harness/skills/feature-breakdown.md +136 -0
- package/harness/skills/impact-analysis.md +110 -0
- package/harness/skills/investigate.md +172 -0
- package/harness/skills/learn.md +308 -0
- package/harness/skills/pivot.md +171 -0
- package/harness/skills/security-checklist.md +101 -0
- package/harness/skills/test-integrity.md +94 -0
- package/package.json +53 -0
- package/src/init.js +772 -0
- package/templates/agent.template.md +56 -0
- package/templates/skill.template.md +54 -0
package/src/init.js
ADDED
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const readline = require('node:readline');
|
|
6
|
+
|
|
7
|
+
const HARNESS_DIR = path.join(__dirname, '..', 'harness');
|
|
8
|
+
|
|
9
|
+
// ─── Template reader ─────────────────────────────────────────
|
|
10
|
+
function readTemplate(name) {
|
|
11
|
+
return fs.readFileSync(path.join(HARNESS_DIR, name), 'utf8');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ─── File writer (mkdir -p + conflict check) ─────────────────
|
|
15
|
+
function writeFile(targetDir, relPath, content, overwrite) {
|
|
16
|
+
const fullPath = path.join(targetDir, relPath);
|
|
17
|
+
const exists = fs.existsSync(fullPath);
|
|
18
|
+
if (exists && !overwrite) {
|
|
19
|
+
console.log(` ⏭ Skipped (exists): ${relPath}`);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
23
|
+
fs.writeFileSync(fullPath, content, 'utf8');
|
|
24
|
+
console.log(` ${exists ? '↻' : '✓'} ${relPath}`);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Shared definitions ──────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const SKILLS = [
|
|
31
|
+
{ id: 'test-integrity', desc: 'Ensure test mocks stay synchronized when interfaces change. Use when modifying repository or service interfaces.' },
|
|
32
|
+
{ id: 'security-checklist', desc: 'Security risk inspection before commits. Use when reviewing code for security issues.' },
|
|
33
|
+
{ id: 'investigate', desc: 'Investigate and diagnose issues. Use when debugging or analyzing unexpected behavior.' },
|
|
34
|
+
{ id: 'impact-analysis', desc: 'Assess change blast radius. Use when modifying shared modules or interfaces.' },
|
|
35
|
+
{ id: 'feature-breakdown', desc: 'Break down features into implementable stories. Use when planning new features.' },
|
|
36
|
+
{ id: 'bootstrap', desc: 'Onboard project into kode:harness. Scans codebase and fills state files. Use after harness init or when state files are empty.' },
|
|
37
|
+
{ id: 'learn', desc: 'Capture session lessons and update state files. Use at the end of every session.' },
|
|
38
|
+
{ id: 'pivot', desc: 'Propagate direction changes across all state files. Use when project goals, technology, scope, or architecture changes.' },
|
|
39
|
+
{ id: 'code-review-pr', desc: 'Review external Pull Requests for quality, security, and direction alignment. Use when reviewing incoming PRs.' },
|
|
40
|
+
{ id: 'deployment', desc: 'Pre-deployment validation checklist. Use before deploying, publishing, or creating release tags.' },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const AGENTS = [
|
|
44
|
+
{ id: 'reviewer', file: 'agents/reviewer.md', desc: 'Code review + auto-fix. Validates quality, security, and test integrity before commits.' },
|
|
45
|
+
{ id: 'sprint-manager', file: 'agents/sprint-manager.md', desc: 'Sprint/Story state tracking, next task guidance, scope drift prevention.' },
|
|
46
|
+
{ id: 'planner', file: 'agents/planner.md', desc: 'Feature planning and dependency management. Analyze architecture, break down features.' },
|
|
47
|
+
{ id: 'architect', file: 'agents/architect.md', desc: 'Design review gate. Validates structural changes against project direction and module boundaries.' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const STATE_FILES = [
|
|
51
|
+
'project-state.md',
|
|
52
|
+
'failure-patterns.md',
|
|
53
|
+
'dependency-map.md',
|
|
54
|
+
'features.md',
|
|
55
|
+
'project-brief.md',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const AGENT_MEMORY_FILES = [
|
|
59
|
+
'agent-memory/reviewer.md',
|
|
60
|
+
'agent-memory/planner.md',
|
|
61
|
+
'agent-memory/sprint-manager.md',
|
|
62
|
+
'agent-memory/architect.md',
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const PERSONAL_STATE_FILES = ['project-state.md', 'failure-patterns.md'];
|
|
66
|
+
const PERSONAL_DIRS = ['agent-memory/'];
|
|
67
|
+
|
|
68
|
+
const STATE_DEST_DIR = 'docs';
|
|
69
|
+
const PERSONAL_DEST_DIR = '.harness';
|
|
70
|
+
|
|
71
|
+
function hasFrameworkMarker(content) {
|
|
72
|
+
return content.includes('kode:harness')
|
|
73
|
+
|| content.includes('harness engineering')
|
|
74
|
+
|| content.includes('@kodevibe/harness')
|
|
75
|
+
|| content.includes('harness-engineering')
|
|
76
|
+
|| content.includes('musher-engineering');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasIdeLayout(targetDir, ide) {
|
|
80
|
+
const requiredByIde = {
|
|
81
|
+
vscode: '.github/skills/bootstrap/SKILL.md',
|
|
82
|
+
claude: '.claude/skills/bootstrap/SKILL.md',
|
|
83
|
+
cursor: '.cursor/skills/bootstrap/SKILL.md',
|
|
84
|
+
codex: '.agents/skills/bootstrap/SKILL.md',
|
|
85
|
+
windsurf: '.windsurf/skills/bootstrap/SKILL.md',
|
|
86
|
+
antigravity: '.gemini/skills/bootstrap/SKILL.md',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const requiredPath = requiredByIde[ide];
|
|
90
|
+
return requiredPath ? fs.existsSync(path.join(targetDir, requiredPath)) : false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Team mode path resolver ─────────────────────────────────
|
|
94
|
+
const TEAM_MODE_SECTION = `
|
|
95
|
+
|
|
96
|
+
## Team Mode
|
|
97
|
+
|
|
98
|
+
This project uses Team mode. State files are split into shared and personal.
|
|
99
|
+
|
|
100
|
+
### File Locations
|
|
101
|
+
- **Shared** (docs/, git committed): project-brief.md, features.md, dependency-map.md
|
|
102
|
+
- **Personal** (.harness/, gitignored): project-state.md, failure-patterns.md, agent-memory/
|
|
103
|
+
|
|
104
|
+
### Rules
|
|
105
|
+
1. **Pre-Pull**: before modifying any shared file (docs/), run \`git pull\` to get latest changes
|
|
106
|
+
2. **Owner Column**: shared files use Owner columns — only modify your own rows
|
|
107
|
+
3. **Read-Only**: other developers' Owner rows are READ ONLY
|
|
108
|
+
4. **Append-Only**: new rows go at the bottom of the table
|
|
109
|
+
5. **Pivot Lock**: the \`pivot\` skill must be run on the default branch by the designated authority (per project-brief.md)
|
|
110
|
+
6. **FP Promotion**: if a personal failure pattern (FP-NNN) affects the team, promote it to a shared doc or team channel
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
function resolveContent(content, mode, crew = false) {
|
|
114
|
+
// Crew mode: strip Crew-only blocks unless --crew is specified
|
|
115
|
+
if (!crew) {
|
|
116
|
+
content = content.replace(/<!-- CREW_MODE_START -->[\s\S]*?<!-- CREW_MODE_END -->\n?/g, '');
|
|
117
|
+
} else {
|
|
118
|
+
// Remove markers, keep Crew content
|
|
119
|
+
content = content
|
|
120
|
+
.replaceAll('<!-- CREW_MODE_START -->\n', '')
|
|
121
|
+
.replaceAll('<!-- CREW_MODE_START -->', '')
|
|
122
|
+
.replaceAll('<!-- CREW_MODE_END -->\n', '')
|
|
123
|
+
.replaceAll('<!-- CREW_MODE_END -->', '');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (mode !== 'team') {
|
|
127
|
+
// Solo mode: strip Team-only blocks entirely
|
|
128
|
+
return content.replace(/<!-- TEAM_MODE_START -->[\s\S]*?<!-- TEAM_MODE_END -->\n?/g, '');
|
|
129
|
+
}
|
|
130
|
+
let result = content
|
|
131
|
+
.replaceAll('docs/project-state.md', '.harness/project-state.md')
|
|
132
|
+
.replaceAll('docs/failure-patterns.md', '.harness/failure-patterns.md')
|
|
133
|
+
.replaceAll('docs/agent-memory/', '.harness/agent-memory/');
|
|
134
|
+
|
|
135
|
+
// Remove markers, keep Team content
|
|
136
|
+
result = result
|
|
137
|
+
.replaceAll('<!-- TEAM_MODE_START -->\n', '')
|
|
138
|
+
.replaceAll('<!-- TEAM_MODE_START -->', '')
|
|
139
|
+
.replaceAll('<!-- TEAM_MODE_END -->\n', '')
|
|
140
|
+
.replaceAll('<!-- TEAM_MODE_END -->', '');
|
|
141
|
+
|
|
142
|
+
// Append Team Mode section to core-rules (detected by the heading)
|
|
143
|
+
if (result.includes('## State Files') && result.includes('## Session Start')) {
|
|
144
|
+
result += TEAM_MODE_SECTION;
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── Language detection ──────────────────────────────────────
|
|
150
|
+
function detectLanguage(targetDir) {
|
|
151
|
+
const markers = [
|
|
152
|
+
['python', ['requirements.txt', 'pyproject.toml', 'setup.py', 'Pipfile', 'setup.cfg']],
|
|
153
|
+
['go', ['go.mod']],
|
|
154
|
+
['java', ['pom.xml', 'build.gradle', 'build.gradle.kts']],
|
|
155
|
+
['rust', ['Cargo.toml']],
|
|
156
|
+
['ruby', ['Gemfile']],
|
|
157
|
+
['csharp', ['global.json', 'Directory.Build.props', 'nuget.config']],
|
|
158
|
+
['php', ['composer.json']],
|
|
159
|
+
['swift', ['Package.swift']],
|
|
160
|
+
['dart', ['pubspec.yaml']],
|
|
161
|
+
];
|
|
162
|
+
for (const [lang, files] of markers) {
|
|
163
|
+
for (const f of files) {
|
|
164
|
+
if (fs.existsSync(path.join(targetDir, f))) return lang;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return 'typescript';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
// ─── Shared writers ──────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
function writeStateFiles(targetDir, overwrite, mode = 'solo', crew = false) {
|
|
175
|
+
for (const file of STATE_FILES) {
|
|
176
|
+
const isPersonal = PERSONAL_STATE_FILES.includes(file);
|
|
177
|
+
const destDir = (mode === 'team' && isPersonal) ? PERSONAL_DEST_DIR : STATE_DEST_DIR;
|
|
178
|
+
const content = resolveContent(readTemplate(file), mode, crew);
|
|
179
|
+
writeFile(targetDir, `${destDir}/${file}`, content, overwrite);
|
|
180
|
+
}
|
|
181
|
+
for (const file of AGENT_MEMORY_FILES) {
|
|
182
|
+
const destDir = mode === 'team' ? PERSONAL_DEST_DIR : STATE_DEST_DIR;
|
|
183
|
+
writeFile(targetDir, `${destDir}/${file}`, readTemplate(file), overwrite);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function writeSkills(targetDir, skillsDir, overwrite, mode = 'solo', crew = false) {
|
|
188
|
+
for (const skill of SKILLS) {
|
|
189
|
+
const content = resolveContent(readTemplate(`skills/${skill.id}.md`), mode, crew);
|
|
190
|
+
const skillMd =
|
|
191
|
+
`---\nname: ${skill.id}\ndescription: '${skill.desc}'\n---\n\n` +
|
|
192
|
+
content;
|
|
193
|
+
writeFile(targetDir, `${skillsDir}/${skill.id}/SKILL.md`, skillMd, overwrite);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function writeAgentsAsSkills(targetDir, skillsDir, overwrite, mode = 'solo', crew = false) {
|
|
198
|
+
for (const agent of AGENTS) {
|
|
199
|
+
const content = resolveContent(readTemplate(agent.file), mode, crew);
|
|
200
|
+
const skillMd =
|
|
201
|
+
`---\nname: ${agent.id}\ndescription: '${agent.desc}'\n---\n\n` +
|
|
202
|
+
content;
|
|
203
|
+
writeFile(targetDir, `${skillsDir}/${agent.id}/SKILL.md`, skillMd, overwrite);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function writeAgentsAsMd(targetDir, agentsDir, overwrite, mode = 'solo', crew = false) {
|
|
208
|
+
for (const agent of AGENTS) {
|
|
209
|
+
const content = resolveContent(readTemplate(agent.file), mode, crew);
|
|
210
|
+
const agentMd =
|
|
211
|
+
`---\nname: ${agent.id}\ndescription: "${agent.desc}"\n---\n\n` +
|
|
212
|
+
content;
|
|
213
|
+
writeFile(targetDir, `${agentsDir}/${agent.id}.md`, agentMd, overwrite);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function writeAgentsAsToml(targetDir, agentsDir, overwrite, mode = 'solo', crew = false) {
|
|
218
|
+
for (const agent of AGENTS) {
|
|
219
|
+
const content = resolveContent(readTemplate(agent.file), mode, crew);
|
|
220
|
+
const toml =
|
|
221
|
+
`name = "${agent.id}"\n` +
|
|
222
|
+
`description = "${agent.desc}"\n` +
|
|
223
|
+
`developer_instructions = """\n${content}\n"""\n`;
|
|
224
|
+
writeFile(targetDir, `${agentsDir}/${agent.id}.toml`, toml, overwrite);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── IDE Generators ──────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
function generateVscode(targetDir, overwrite, mode = 'solo', crew = false) {
|
|
231
|
+
const coreRules = resolveContent(readTemplate('core-rules.md'), mode, crew);
|
|
232
|
+
|
|
233
|
+
// Global instructions (dispatcher only — rules are embedded in skills)
|
|
234
|
+
writeFile(targetDir, '.github/copilot-instructions.md', coreRules, true);
|
|
235
|
+
|
|
236
|
+
// Skills (.github/skills — VS Code default search path, SKILL.md with frontmatter)
|
|
237
|
+
writeSkills(targetDir, '.github/skills', true, mode, crew);
|
|
238
|
+
|
|
239
|
+
// Agents (.github/agents — VS Code uses .agent.md format with frontmatter)
|
|
240
|
+
for (const agent of AGENTS) {
|
|
241
|
+
const content = resolveContent(readTemplate(agent.file), mode, crew);
|
|
242
|
+
const agentMd =
|
|
243
|
+
`---\nname: ${agent.id}\ndescription: "${agent.desc}"\n---\n\n` +
|
|
244
|
+
content;
|
|
245
|
+
writeFile(targetDir, `.github/agents/${agent.id}.agent.md`, agentMd, true);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// State files (respect user's --overwrite for data files)
|
|
249
|
+
writeStateFiles(targetDir, overwrite, mode, crew);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function generateClaude(targetDir, overwrite, mode = 'solo', crew = false) {
|
|
253
|
+
// .claude/rules/core.md — dispatcher only (no paths = always loaded)
|
|
254
|
+
writeFile(targetDir, '.claude/rules/core.md', resolveContent(readTemplate('core-rules.md'), mode, crew), true);
|
|
255
|
+
|
|
256
|
+
// Skills (SKILL.md with frontmatter)
|
|
257
|
+
writeSkills(targetDir, '.claude/skills', true, mode, crew);
|
|
258
|
+
|
|
259
|
+
// Agents (.claude/agents/ — Claude Code agent definition files)
|
|
260
|
+
writeAgentsAsMd(targetDir, '.claude/agents', true, mode, crew);
|
|
261
|
+
|
|
262
|
+
// State files (respect user's --overwrite for data files)
|
|
263
|
+
writeStateFiles(targetDir, overwrite, mode, crew);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function generateCursor(targetDir, overwrite, mode = 'solo', crew = false) {
|
|
267
|
+
// .cursor/rules/core.mdc — dispatcher only (always active)
|
|
268
|
+
const coreRules = resolveContent(readTemplate('core-rules.md'), mode, crew);
|
|
269
|
+
const coreMdc =
|
|
270
|
+
'---\ndescription: kode:harness dispatcher — workflow guidance and state file references\nalwaysApply: true\n---\n\n' +
|
|
271
|
+
coreRules;
|
|
272
|
+
writeFile(targetDir, '.cursor/rules/core.mdc', coreMdc, true);
|
|
273
|
+
|
|
274
|
+
// Skills (.cursor/skills — invokable by mentioning skill name)
|
|
275
|
+
writeSkills(targetDir, '.cursor/skills', true, mode, crew);
|
|
276
|
+
|
|
277
|
+
// Agents (.cursor/agents/ — Cursor subagent definition files)
|
|
278
|
+
writeAgentsAsMd(targetDir, '.cursor/agents', true, mode, crew);
|
|
279
|
+
|
|
280
|
+
// State files (respect user's --overwrite for data files)
|
|
281
|
+
writeStateFiles(targetDir, overwrite, mode, crew);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function generateCodex(targetDir, overwrite, mode = 'solo', crew = false) {
|
|
285
|
+
// AGENTS.md — dispatcher only
|
|
286
|
+
writeFile(targetDir, 'AGENTS.md', resolveContent(readTemplate('core-rules.md'), mode, crew), true);
|
|
287
|
+
|
|
288
|
+
// Skills (SKILL.md with frontmatter — invokable via $skill-name)
|
|
289
|
+
writeSkills(targetDir, '.agents/skills', true, mode, crew);
|
|
290
|
+
|
|
291
|
+
// Agents (.codex/agents/ — Codex TOML agent definition files)
|
|
292
|
+
writeAgentsAsToml(targetDir, '.codex/agents', true, mode, crew);
|
|
293
|
+
|
|
294
|
+
// State files (respect user's --overwrite for data files)
|
|
295
|
+
writeStateFiles(targetDir, overwrite, mode, crew);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function generateWindsurf(targetDir, overwrite, mode = 'solo', crew = false) {
|
|
299
|
+
// .windsurf/rules/core.md — dispatcher (trigger: always_on)
|
|
300
|
+
const coreRules = resolveContent(readTemplate('core-rules.md'), mode, crew);
|
|
301
|
+
const coreRule =
|
|
302
|
+
'---\ntrigger: always_on\n---\n\n' +
|
|
303
|
+
coreRules;
|
|
304
|
+
writeFile(targetDir, '.windsurf/rules/core.md', coreRule, true);
|
|
305
|
+
|
|
306
|
+
// Skills (.windsurf/skills — Agent Skills standard)
|
|
307
|
+
writeSkills(targetDir, '.windsurf/skills', true, mode, crew);
|
|
308
|
+
|
|
309
|
+
// Agents as skills
|
|
310
|
+
writeAgentsAsSkills(targetDir, '.windsurf/skills', true, mode, crew);
|
|
311
|
+
|
|
312
|
+
// State files (respect user's --overwrite for data files)
|
|
313
|
+
writeStateFiles(targetDir, overwrite, mode, crew);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function generateAntigravity(targetDir, overwrite, mode = 'solo', crew = false) {
|
|
317
|
+
// GEMINI.md — project context (always loaded by Gemini CLI)
|
|
318
|
+
writeFile(targetDir, 'GEMINI.md', resolveContent(readTemplate('core-rules.md'), mode, crew), true);
|
|
319
|
+
|
|
320
|
+
// Skills (.gemini/skills/ — SKILL.md format)
|
|
321
|
+
writeSkills(targetDir, '.gemini/skills', true, mode, crew);
|
|
322
|
+
|
|
323
|
+
// Agents (.gemini/agents/ — Gemini CLI subagent definition files)
|
|
324
|
+
writeAgentsAsMd(targetDir, '.gemini/agents', true, mode, crew);
|
|
325
|
+
|
|
326
|
+
// State files (respect user's --overwrite for data files)
|
|
327
|
+
writeStateFiles(targetDir, overwrite, mode, crew);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── IDE registry ────────────────────────────────────────────
|
|
331
|
+
const GENERATORS = {
|
|
332
|
+
vscode: { name: 'VS Code Copilot', fn: generateVscode },
|
|
333
|
+
claude: { name: 'Claude Code', fn: generateClaude },
|
|
334
|
+
cursor: { name: 'Cursor', fn: generateCursor },
|
|
335
|
+
codex: { name: 'Codex (OpenAI)', fn: generateCodex },
|
|
336
|
+
windsurf: { name: 'Windsurf', fn: generateWindsurf },
|
|
337
|
+
antigravity: { name: 'Antigravity', fn: generateAntigravity },
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// ─── Interactive prompt ──────────────────────────────────────
|
|
341
|
+
function askQuestion(query) {
|
|
342
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
343
|
+
return new Promise((resolve) => {
|
|
344
|
+
rl.question(query, (answer) => {
|
|
345
|
+
rl.close();
|
|
346
|
+
resolve(answer.trim());
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function promptIde() {
|
|
352
|
+
const keys = Object.keys(GENERATORS);
|
|
353
|
+
console.log('\n Select your IDE:\n');
|
|
354
|
+
keys.forEach((key, i) => {
|
|
355
|
+
console.log(` ${i + 1}. ${GENERATORS[key].name}`);
|
|
356
|
+
});
|
|
357
|
+
console.log();
|
|
358
|
+
|
|
359
|
+
const answer = await askQuestion(` Choice (1-${keys.length}): `);
|
|
360
|
+
const idx = parseInt(answer, 10) - 1;
|
|
361
|
+
if (idx < 0 || idx >= keys.length || isNaN(idx)) {
|
|
362
|
+
console.error(' Invalid choice.');
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
return keys[idx];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function promptMode() {
|
|
369
|
+
console.log(' Project mode:\n');
|
|
370
|
+
console.log(' 1. Solo — Single developer (all state files in docs/)');
|
|
371
|
+
console.log(' 2. Team — Multiple developers (personal state in .harness/, shared in docs/)');
|
|
372
|
+
console.log();
|
|
373
|
+
|
|
374
|
+
const answer = await askQuestion(' Choice (1-2, default: 1): ');
|
|
375
|
+
if (answer === '2' || answer.toLowerCase() === 'team') return 'team';
|
|
376
|
+
return 'solo';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ─── Team mode helpers ───────────────────────────────────────
|
|
380
|
+
function appendGitignore(targetDir) {
|
|
381
|
+
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
382
|
+
const entry = '\n# kode:harness personal state (Team mode)\n.harness/\n';
|
|
383
|
+
if (fs.existsSync(gitignorePath)) {
|
|
384
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
385
|
+
if (content.includes('.harness/')) {
|
|
386
|
+
console.log(' ⏭ Skipped (exists): .gitignore entry');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
fs.appendFileSync(gitignorePath, entry);
|
|
390
|
+
} else {
|
|
391
|
+
fs.writeFileSync(gitignorePath, entry.trimStart());
|
|
392
|
+
}
|
|
393
|
+
console.log(' ✓ .gitignore — added .harness/');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function detectExistingInstall(targetDir) {
|
|
397
|
+
// kode:harness state files — these contain user data that overwrite would destroy
|
|
398
|
+
const stateMarkers = [
|
|
399
|
+
'docs/project-state.md',
|
|
400
|
+
'docs/features.md',
|
|
401
|
+
'docs/dependency-map.md',
|
|
402
|
+
'docs/failure-patterns.md',
|
|
403
|
+
'docs/project-brief.md',
|
|
404
|
+
'.harness/project-state.md',
|
|
405
|
+
'.harness/failure-patterns.md',
|
|
406
|
+
];
|
|
407
|
+
const existingState = stateMarkers.filter(f => fs.existsSync(path.join(targetDir, f)));
|
|
408
|
+
|
|
409
|
+
// IDE config files — always overwritten regardless of user choice
|
|
410
|
+
const ideMarkers = [
|
|
411
|
+
['.github/copilot-instructions.md', 'vscode'],
|
|
412
|
+
['.claude/rules/core.md', 'claude'],
|
|
413
|
+
['.cursor/rules/core.mdc', 'cursor'],
|
|
414
|
+
['.windsurf/rules/core.md', 'windsurf'],
|
|
415
|
+
['GEMINI.md', 'antigravity'],
|
|
416
|
+
];
|
|
417
|
+
// Only count as existing if the file contains a framework marker (not from other frameworks)
|
|
418
|
+
const existingIde = ideMarkers.filter(([f, ide]) => {
|
|
419
|
+
const fullPath = path.join(targetDir, f);
|
|
420
|
+
if (!fs.existsSync(fullPath)) return false;
|
|
421
|
+
if (hasIdeLayout(targetDir, ide)) return true;
|
|
422
|
+
try {
|
|
423
|
+
return hasFrameworkMarker(fs.readFileSync(fullPath, 'utf8'));
|
|
424
|
+
} catch { return false; }
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return { stateFiles: existingState, ideFiles: existingIde, hasAny: existingState.length > 0 || existingIde.length > 0 };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function writeGitattributes(targetDir) {
|
|
431
|
+
const content =
|
|
432
|
+
'# kode:harness Team mode — merge strategy for shared state files\n' +
|
|
433
|
+
'docs/features.md merge=union\n' +
|
|
434
|
+
'docs/dependency-map.md merge=union\n';
|
|
435
|
+
writeFile(targetDir, '.gitattributes', content, false);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ─── Post-install guide ──────────────────────────────────────
|
|
439
|
+
function showPostInstallGuide(ideName, mode) {
|
|
440
|
+
const modeLabel = mode === 'team' ? 'Team' : 'Solo';
|
|
441
|
+
const lines = [
|
|
442
|
+
'',
|
|
443
|
+
' ──────────────────────────────────────────',
|
|
444
|
+
' ✅ kode:harness initialized successfully!',
|
|
445
|
+
'',
|
|
446
|
+
` Mode: ${modeLabel}`,
|
|
447
|
+
` IDE: ${ideName}`,
|
|
448
|
+
'',
|
|
449
|
+
];
|
|
450
|
+
|
|
451
|
+
if (mode === 'team') {
|
|
452
|
+
lines.push(
|
|
453
|
+
' 📁 Files:',
|
|
454
|
+
' docs/ — shared state (git committed)',
|
|
455
|
+
' .harness/ — personal state (gitignored)',
|
|
456
|
+
' .gitignore — .harness/ added',
|
|
457
|
+
' .gitattributes — merge=union for shared files',
|
|
458
|
+
);
|
|
459
|
+
} else {
|
|
460
|
+
lines.push(
|
|
461
|
+
' 📁 Files:',
|
|
462
|
+
' docs/ — all state files',
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
lines.push(
|
|
467
|
+
'',
|
|
468
|
+
' 🚀 Next steps:',
|
|
469
|
+
' 1. Ask your AI: "Run bootstrap to onboard this project"',
|
|
470
|
+
' 2. AI scans your codebase and fills state files automatically',
|
|
471
|
+
' 3. Start coding: ask your AI to plan a new feature',
|
|
472
|
+
'',
|
|
473
|
+
' ⚙️ IDE Settings (large projects):',
|
|
474
|
+
'',
|
|
475
|
+
' VS Code → settings.json: "chat.agent.maxRequests": 100',
|
|
476
|
+
' Cursor → Default OK (auto-managed)',
|
|
477
|
+
' Windsurf → Default OK (auto-managed)',
|
|
478
|
+
' Claude Code → Default OK (terminal-based)',
|
|
479
|
+
'',
|
|
480
|
+
' 📖 Docs: https://www.npmjs.com/package/@kodevibe/harness',
|
|
481
|
+
' ──────────────────────────────────────────',
|
|
482
|
+
'',
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
console.log(lines.join('\n'));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ─── Doctor command ──────────────────────────────────────────
|
|
489
|
+
function runDoctor(targetDir) {
|
|
490
|
+
console.log('\n kode:harness Doctor — Installation Health Check\n');
|
|
491
|
+
const checks = [];
|
|
492
|
+
let passed = 0;
|
|
493
|
+
let failed = 0;
|
|
494
|
+
|
|
495
|
+
// Check state files
|
|
496
|
+
for (const file of STATE_FILES) {
|
|
497
|
+
const docsPath = path.join(targetDir, 'docs', file);
|
|
498
|
+
const harnessPath = path.join(targetDir, '.harness', file);
|
|
499
|
+
const exists = fs.existsSync(docsPath) || fs.existsSync(harnessPath);
|
|
500
|
+
if (exists) {
|
|
501
|
+
checks.push(` ✅ ${file}`);
|
|
502
|
+
passed++;
|
|
503
|
+
} else {
|
|
504
|
+
checks.push(` ❌ ${file} — not found`);
|
|
505
|
+
failed++;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Check agent-memory files
|
|
510
|
+
for (const file of AGENT_MEMORY_FILES) {
|
|
511
|
+
const docsPath = path.join(targetDir, 'docs', file);
|
|
512
|
+
const harnessPath = path.join(targetDir, '.harness', file);
|
|
513
|
+
const exists = fs.existsSync(docsPath) || fs.existsSync(harnessPath);
|
|
514
|
+
if (exists) {
|
|
515
|
+
checks.push(` ✅ ${file}`);
|
|
516
|
+
passed++;
|
|
517
|
+
} else {
|
|
518
|
+
checks.push(` ❌ ${file} — not found`);
|
|
519
|
+
failed++;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Check for IDE-specific files (detect which IDE was used)
|
|
524
|
+
const ideChecks = [
|
|
525
|
+
['.github/copilot-instructions.md', 'vscode'],
|
|
526
|
+
['.claude/rules/core.md', 'claude'],
|
|
527
|
+
['.cursor/rules/core.mdc', 'cursor'],
|
|
528
|
+
['AGENTS.md', 'codex'],
|
|
529
|
+
['.windsurf/rules/core.md', 'windsurf'],
|
|
530
|
+
['GEMINI.md', 'antigravity'],
|
|
531
|
+
];
|
|
532
|
+
|
|
533
|
+
let detectedIde = null;
|
|
534
|
+
for (const [file, ide] of ideChecks) {
|
|
535
|
+
const fullPath = path.join(targetDir, file);
|
|
536
|
+
if (fs.existsSync(fullPath)) {
|
|
537
|
+
if (hasIdeLayout(targetDir, ide)) {
|
|
538
|
+
detectedIde = ide;
|
|
539
|
+
checks.push(` ✅ IDE detected: ${GENERATORS[ide].name}`);
|
|
540
|
+
passed++;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
// Verify it's a kode:harness-managed file, not from another framework
|
|
544
|
+
try {
|
|
545
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
546
|
+
if (!hasFrameworkMarker(content)) continue;
|
|
547
|
+
} catch { continue; }
|
|
548
|
+
detectedIde = ide;
|
|
549
|
+
checks.push(` ✅ IDE detected: ${GENERATORS[ide].name}`);
|
|
550
|
+
passed++;
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (!detectedIde) {
|
|
555
|
+
checks.push(' ❌ No IDE configuration found — run `harness init` first');
|
|
556
|
+
failed++;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Detect mode
|
|
560
|
+
const isTeam = fs.existsSync(path.join(targetDir, '.harness'));
|
|
561
|
+
checks.push(` ℹ️ Mode: ${isTeam ? 'Team' : 'Solo'}`);
|
|
562
|
+
|
|
563
|
+
console.log(checks.join('\n'));
|
|
564
|
+
console.log(`\n Result: ${passed} passed, ${failed} failed\n`);
|
|
565
|
+
return failed === 0;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ─── Validate command ────────────────────────────────────────
|
|
569
|
+
function runValidate(targetDir) {
|
|
570
|
+
console.log('\n kode:harness Validate — State File Content Check\n');
|
|
571
|
+
const results = [];
|
|
572
|
+
let warnings = 0;
|
|
573
|
+
|
|
574
|
+
// Each state file has a known sentinel that only exists in unfilled templates.
|
|
575
|
+
// failure-patterns.md is excluded: it intentionally keeps FP-001~004 as templates
|
|
576
|
+
// after bootstrap (Frequency: 0 is the normal initial state, not a placeholder).
|
|
577
|
+
const templateSentinels = {
|
|
578
|
+
'project-state.md': 'S1-1 | Project scaffolding',
|
|
579
|
+
'dependency-map.md': 'Add new modules above this line',
|
|
580
|
+
'features.md': 'Add new features above this line',
|
|
581
|
+
'project-brief.md': 'This is the north star for all decisions',
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
for (const file of STATE_FILES) {
|
|
585
|
+
const docsPath = path.join(targetDir, 'docs', file);
|
|
586
|
+
const harnessPath = path.join(targetDir, '.harness', file);
|
|
587
|
+
const filePath = fs.existsSync(docsPath) ? docsPath : (fs.existsSync(harnessPath) ? harnessPath : null);
|
|
588
|
+
|
|
589
|
+
if (!filePath) {
|
|
590
|
+
results.push(` ❌ ${file} — not found`);
|
|
591
|
+
warnings++;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
596
|
+
const sentinel = templateSentinels[file];
|
|
597
|
+
|
|
598
|
+
if (!sentinel) {
|
|
599
|
+
// failure-patterns.md: no sentinel check — template state is normal
|
|
600
|
+
results.push(` ✅ ${file} — ok (no failures logged yet is normal)`);
|
|
601
|
+
} else if (content.includes(sentinel)) {
|
|
602
|
+
results.push(` ⚠️ ${file} — placeholder only. Run \`bootstrap\` to fill.`);
|
|
603
|
+
warnings++;
|
|
604
|
+
} else {
|
|
605
|
+
results.push(` ✅ ${file} — has content`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
console.log(results.join('\n'));
|
|
610
|
+
console.log(`\n Result: ${warnings === 0 ? 'All state files have content' : `${warnings} file(s) need attention`}\n`);
|
|
611
|
+
return warnings === 0;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ─── CLI entry ───────────────────────────────────────────────
|
|
615
|
+
function showHelp() {
|
|
616
|
+
console.log(`
|
|
617
|
+
kode:harness — Harness Engineering
|
|
618
|
+
|
|
619
|
+
Usage:
|
|
620
|
+
npx @kodevibe/harness init [options]
|
|
621
|
+
npx @kodevibe/harness doctor [--dir <path>]
|
|
622
|
+
npx @kodevibe/harness validate [--dir <path>]
|
|
623
|
+
|
|
624
|
+
Commands:
|
|
625
|
+
init Install kode:harness files for your IDE
|
|
626
|
+
doctor Check if kode:harness files are installed and healthy
|
|
627
|
+
validate Verify state files have content (not just placeholders)
|
|
628
|
+
|
|
629
|
+
Options:
|
|
630
|
+
--ide <name> IDE target: vscode, claude, cursor, codex, windsurf, antigravity
|
|
631
|
+
--mode <mode> Project mode: solo (default) or team
|
|
632
|
+
--dir <path> Target directory (default: current directory)
|
|
633
|
+
--overwrite Overwrite existing files (including state files)
|
|
634
|
+
--batch Non-interactive mode (requires --ide; defaults to solo mode)
|
|
635
|
+
--version Show version number
|
|
636
|
+
--help Show this help
|
|
637
|
+
|
|
638
|
+
Examples:
|
|
639
|
+
npx @kodevibe/harness init
|
|
640
|
+
npx @kodevibe/harness init --ide vscode
|
|
641
|
+
npx @kodevibe/harness init --ide vscode --mode team
|
|
642
|
+
npx @kodevibe/harness init --ide claude --dir ./my-project
|
|
643
|
+
npx @kodevibe/harness doctor
|
|
644
|
+
npx @kodevibe/harness validate
|
|
645
|
+
`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function parseArgs(argv) {
|
|
649
|
+
const args = { command: null, ide: null, mode: null, dir: process.cwd(), overwrite: false, help: false, batch: false, version: false, crew: false };
|
|
650
|
+
for (let i = 0; i < argv.length; i++) {
|
|
651
|
+
const arg = argv[i];
|
|
652
|
+
if (arg === 'init') args.command = 'init';
|
|
653
|
+
else if (arg === 'doctor') args.command = 'doctor';
|
|
654
|
+
else if (arg === 'validate') args.command = 'validate';
|
|
655
|
+
else if (arg === '--ide' && argv[i + 1]) { args.ide = argv[++i]; }
|
|
656
|
+
else if (arg === '--mode' && argv[i + 1]) { args.mode = argv[++i]; }
|
|
657
|
+
else if (arg === '--team') { args.mode = 'team'; }
|
|
658
|
+
else if (arg === '--crew') args.crew = true;
|
|
659
|
+
else if (arg === '--dir' && argv[i + 1]) { args.dir = path.resolve(argv[++i]); }
|
|
660
|
+
else if (arg === '--overwrite') args.overwrite = true;
|
|
661
|
+
else if (arg === '--batch') args.batch = true;
|
|
662
|
+
else if (arg === '--help' || arg === '-h') args.help = true;
|
|
663
|
+
else if (arg === '--version') args.version = true;
|
|
664
|
+
}
|
|
665
|
+
return args;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function run(argv) {
|
|
669
|
+
const args = parseArgs(argv);
|
|
670
|
+
|
|
671
|
+
if (args.version) {
|
|
672
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
673
|
+
console.log(pkg.version);
|
|
674
|
+
process.exit(0);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (args.help || !args.command) {
|
|
678
|
+
showHelp();
|
|
679
|
+
process.exit(args.help ? 0 : 1);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (args.command === 'doctor') {
|
|
683
|
+
const ok = runDoctor(args.dir);
|
|
684
|
+
process.exit(ok ? 0 : 1);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (args.command === 'validate') {
|
|
688
|
+
const ok = runValidate(args.dir);
|
|
689
|
+
process.exit(ok ? 0 : 1);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (args.command === 'init') {
|
|
693
|
+
console.log('\n kode:harness — Harness Engineering\n');
|
|
694
|
+
|
|
695
|
+
// Determine IDE
|
|
696
|
+
let ide = args.ide;
|
|
697
|
+
if (ide && !GENERATORS[ide]) {
|
|
698
|
+
console.error(` Unknown IDE: ${ide}`);
|
|
699
|
+
console.error(` Available: ${Object.keys(GENERATORS).join(', ')}`);
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
if (!ide) {
|
|
703
|
+
if (args.batch) {
|
|
704
|
+
console.error(' --batch requires --ide to be specified');
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
ide = await promptIde();
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Determine mode
|
|
711
|
+
let mode = args.mode;
|
|
712
|
+
if (mode && !['solo', 'team'].includes(mode)) {
|
|
713
|
+
console.error(` Unknown mode: ${mode}`);
|
|
714
|
+
console.error(' Available: solo, team');
|
|
715
|
+
process.exit(1);
|
|
716
|
+
}
|
|
717
|
+
if (!mode) {
|
|
718
|
+
if (args.batch) {
|
|
719
|
+
mode = 'solo';
|
|
720
|
+
} else {
|
|
721
|
+
mode = await promptMode();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Determine overwrite — prompt only in interactive terminal
|
|
726
|
+
let overwrite = args.overwrite;
|
|
727
|
+
if (!overwrite && !args.batch && process.stdin.isTTY) {
|
|
728
|
+
const existing = detectExistingInstall(args.dir);
|
|
729
|
+
if (existing.hasAny) {
|
|
730
|
+
console.log(' ⚠ Existing kode:harness files detected:\n');
|
|
731
|
+
if (existing.stateFiles.length > 0) {
|
|
732
|
+
console.log(' 📄 State files (contain your project data):');
|
|
733
|
+
for (const f of existing.stateFiles) {
|
|
734
|
+
console.log(` • ${f}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (existing.ideFiles.length > 0) {
|
|
738
|
+
console.log(' 🔧 IDE configs (always updated to latest version):');
|
|
739
|
+
for (const [f, ide] of existing.ideFiles) {
|
|
740
|
+
console.log(` • ${f} (${ide})`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
console.log();
|
|
744
|
+
if (existing.stateFiles.length > 0) {
|
|
745
|
+
console.log(' Overwrite resets state files to blank templates.');
|
|
746
|
+
console.log(' Choose N to keep your existing data (recommended).\n');
|
|
747
|
+
const answer = await askQuestion(' Overwrite state files? (y/N): ');
|
|
748
|
+
overwrite = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
749
|
+
} else {
|
|
750
|
+
console.log(' No state files to overwrite — proceeding with install.\n');
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const gen = GENERATORS[ide];
|
|
756
|
+
const crew = args.crew;
|
|
757
|
+
const lang = detectLanguage(args.dir);
|
|
758
|
+
const modeDesc = crew ? `${mode} + crew` : mode;
|
|
759
|
+
console.log(`\n Installing for ${gen.name} (${modeDesc} mode)... (detected language: ${lang})\n`);
|
|
760
|
+
gen.fn(args.dir, overwrite, mode, crew);
|
|
761
|
+
|
|
762
|
+
// Team mode extras
|
|
763
|
+
if (mode === 'team') {
|
|
764
|
+
appendGitignore(args.dir);
|
|
765
|
+
writeGitattributes(args.dir);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
showPostInstallGuide(gen.name, mode);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
module.exports = { run, detectLanguage, runDoctor, runValidate };
|