create-harness-vibe-coding 0.1.9 → 0.2.0

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.
Files changed (57) hide show
  1. package/README.md +177 -32
  2. package/package.json +5 -2
  3. package/src/generator.js +407 -53
  4. package/src/index.js +236 -22
  5. package/templates/common/.claude/agents/architect.md +4 -4
  6. package/templates/common/.claude/agents/debugger.md +1 -1
  7. package/templates/common/.claude/agents/docs-researcher.md +4 -4
  8. package/templates/common/.claude/agents/implementer.md +1 -1
  9. package/templates/common/.claude/agents/planner.md +3 -3
  10. package/templates/common/.claude/agents/researcher.md +4 -4
  11. package/templates/common/.claude/agents/reviewer.md +1 -1
  12. package/templates/common/.claude/agents/test-writer.md +2 -2
  13. package/templates/common/.claude/agents/verifier.md +1 -1
  14. package/templates/common/.claude/commands/wf.md +18 -0
  15. package/templates/common/.claude/rules/ecc/common.md +16 -6
  16. package/templates/common/.claude/skills/harness-build-loop/SKILL.md +4 -3
  17. package/templates/common/.claude/skills/harness-context/SKILL.md +4 -3
  18. package/templates/common/.claude/skills/harness-lifecycle/SKILL.md +3 -3
  19. package/templates/common/.claude/skills/harness-research/SKILL.md +4 -4
  20. package/templates/common/.claude/skills/harness-router/SKILL.md +7 -5
  21. package/templates/common/.claude/skills/readme-optimizer/SKILL.md +48 -0
  22. package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +36 -0
  23. package/templates/common/.claude/skills/wf-mode/SKILL.md +48 -0
  24. package/templates/common/AGENTS.md +2 -2
  25. package/templates/common/CLAUDE.md +39 -71
  26. package/templates/common/MEMORY.md +42 -33
  27. package/templates/common/README.md +41 -0
  28. package/templates/common/SETUP.md +149 -41
  29. package/templates/common/docs/README.md +72 -47
  30. package/templates/common/docs/domain/ports.md +1 -1
  31. package/templates/common/docs/features/_template.md +20 -10
  32. package/templates/common/docs/harness/PLAN.md +25 -2
  33. package/templates/common/docs/harness/WF.md +136 -0
  34. package/templates/common/docs/harness/agent-workflow.md +8 -8
  35. package/templates/common/docs/harness/context-loading.md +17 -14
  36. package/templates/common/docs/harness/data-flow.md +1 -1
  37. package/templates/common/docs/harness/dispatch.md +6 -0
  38. package/templates/common/docs/harness/extension.md +20 -8
  39. package/templates/common/docs/harness/lifecycle.md +2 -2
  40. package/templates/common/docs/harness/subagents.md +140 -0
  41. package/templates/common/docs/research/PRD.md +1 -1
  42. package/templates/common/docs/research/README.md +5 -5
  43. package/templates/common/memory/agent-lessons-patterns.md +21 -0
  44. package/templates/common/memory/tool-usage-reflections.md +21 -0
  45. package/templates/common/memory/user-corrections-preferences.md +21 -0
  46. package/templates/common/scripts/validate-harness.mjs +217 -46
  47. package/templates/optional/catalog.json +43 -0
  48. package/templates/optional/skills/browser-e2e/.claude/skills/browser-e2e/SKILL.md +42 -0
  49. package/templates/optional/skills/browser-e2e/docs/workflows/browser-e2e.md +42 -0
  50. package/templates/optional/skills/github-pr-review/.claude/skills/github-pr-review/SKILL.md +40 -0
  51. package/templates/optional/skills/github-pr-review/docs/workflows/github-pr-review.md +28 -0
  52. package/templates/optional/skills/python-backend/.claude/skills/python-backend/SKILL.md +40 -0
  53. package/templates/optional/skills/python-backend/docs/workflows/python-backend.md +34 -0
  54. package/templates/optional/skills/ts-react-frontend/.claude/skills/ts-react-frontend/SKILL.md +43 -0
  55. package/templates/optional/skills/ts-react-frontend/docs/workflows/ts-react-frontend.md +35 -0
  56. package/templates/optional/skills/ui-ux-review/.claude/skills/ui-ux-review/SKILL.md +40 -0
  57. package/templates/optional/skills/ui-ux-review/docs/workflows/ui-ux-review.md +26 -0
package/src/generator.js CHANGED
@@ -1,36 +1,42 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import pc from 'picocolors';
5
4
 
6
5
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
6
  const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates', 'common');
7
+ const OPTIONAL_DIR = path.resolve(__dirname, '..', 'templates', 'optional');
8
+ const OPTIONAL_CATALOG = path.join(OPTIONAL_DIR, 'catalog.json');
9
+ const VALID_CONFLICT_POLICIES = new Set(['fail', 'skip', 'backup', 'overwrite']);
10
+ const EMPTY_DIRS = [
11
+ '.claude/hooks',
12
+ 'tests',
13
+ ];
14
+
15
+ function harnessDest(file) {
16
+ if (file === 'SETUP.md') return 'Harness/SETUP.md';
17
+ if (file === 'MEMORY.md') return 'Harness/MEMORY.md';
18
+ if (file === 'scripts/validate-harness.mjs') return 'Harness/scripts/validate-harness.mjs';
19
+ if (file.startsWith('memory/')) return `Harness/${file}`;
20
+ if (file === 'docs/README.md') return 'Harness/README.md';
21
+ if (file.startsWith('docs/harness/')) return file.replace(/^docs\/harness\//, 'Harness/');
22
+ if (file.startsWith('docs/research/')) return file.replace(/^docs\/research\//, 'Harness/research/');
23
+ if (file.startsWith('docs/domain/')) return file.replace(/^docs\/domain\//, 'Harness/domain/');
24
+ if (file.startsWith('docs/features/')) return file.replace(/^docs\/features\//, 'Harness/features/');
25
+ if (file.startsWith('docs/workflows/')) return file.replace(/^docs\/workflows\//, 'Harness/workflows/');
26
+ return file;
27
+ }
8
28
 
9
29
  /**
10
- * Copy a directory recursively, replacing {{vars}} in file contents.
30
+ * Replace {{vars}} in file contents.
11
31
  */
12
- function copyDir(src, dest, vars) {
13
- fs.mkdirSync(dest, { recursive: true });
14
-
15
- const entries = fs.readdirSync(src, { withFileTypes: true });
16
-
17
- for (const entry of entries) {
18
- const srcPath = path.join(src, entry.name);
19
- const destPath = path.join(dest, entry.name);
20
-
21
- if (entry.isDirectory()) {
22
- copyDir(srcPath, destPath, vars);
23
- } else {
24
- let content = fs.readFileSync(srcPath, 'utf-8');
32
+ function renderTemplate(src, vars) {
33
+ let content = fs.readFileSync(src, 'utf-8');
25
34
 
26
- // Replace template variables
27
- for (const [key, value] of Object.entries(vars)) {
28
- content = content.replaceAll(`{{${key}}}`, value);
29
- }
30
-
31
- fs.writeFileSync(destPath, content, 'utf-8');
32
- }
35
+ for (const [key, value] of Object.entries(vars)) {
36
+ content = content.replaceAll(`{{${key}}}`, value);
33
37
  }
38
+
39
+ return content;
34
40
  }
35
41
 
36
42
  /**
@@ -52,53 +58,401 @@ function walkFiles(dir, base = dir) {
52
58
  return results;
53
59
  }
54
60
 
55
- export function generate({ projectName, targetDir }) {
61
+ function normalizePath(filePath) {
62
+ return filePath.replace(/\\/g, '/');
63
+ }
64
+
65
+ export function getOptionalCatalog() {
66
+ if (!fs.existsSync(OPTIONAL_CATALOG)) {
67
+ return { skills: [], presets: {} };
68
+ }
69
+
70
+ return JSON.parse(fs.readFileSync(OPTIONAL_CATALOG, 'utf-8'));
71
+ }
72
+
73
+ function normalizeOptionList(value) {
74
+ const values = Array.isArray(value) ? value : [value];
75
+ return values
76
+ .filter(Boolean)
77
+ .flatMap(item => String(item).split(','))
78
+ .map(item => item.trim())
79
+ .filter(Boolean);
80
+ }
81
+
82
+ function resolveOptionalSelection({ withOptions = [], withoutOptions = [], preset = undefined }) {
83
+ const catalog = getOptionalCatalog();
84
+ const skillsById = new Map(catalog.skills.map(skill => [skill.id, skill]));
85
+ const selected = [];
86
+ const errors = [];
87
+
88
+ if (preset) {
89
+ if (!catalog.presets[preset]) {
90
+ errors.push(`Unknown preset "${preset}". Run --list-options to see available presets.`);
91
+ } else {
92
+ selected.push(...catalog.presets[preset]);
93
+ }
94
+ }
95
+
96
+ selected.push(...normalizeOptionList(withOptions));
97
+
98
+ const ids = [...new Set(selected)];
99
+ const withoutIds = [...new Set(normalizeOptionList(withoutOptions))];
100
+ for (const id of ids) {
101
+ if (!skillsById.has(id)) {
102
+ errors.push(`Unknown optional skill "${id}". Run --list-options to see available options.`);
103
+ }
104
+ }
105
+ for (const id of withoutIds) {
106
+ if (!skillsById.has(id)) {
107
+ errors.push(`Unknown optional skill in --without "${id}". Run --list-options to see available options.`);
108
+ }
109
+ }
110
+
111
+ const withoutSet = new Set(withoutIds);
112
+ const selectedIds = ids.filter(id => !withoutSet.has(id));
113
+
114
+ return {
115
+ catalog,
116
+ selectedSkills: errors.length ? [] : selectedIds.map(id => skillsById.get(id)),
117
+ errors,
118
+ };
119
+ }
120
+
121
+ function createCoreFileSpecs() {
122
+ const specs = walkFiles(TEMPLATES_DIR)
123
+ .map(file => normalizePath(file))
124
+ .sort()
125
+ .map(file => ({
126
+ dest: harnessDest(file),
127
+ src: path.join(TEMPLATES_DIR, ...file.split('/')),
128
+ type: 'common',
129
+ }));
130
+
131
+ specs.push({ dest: 'tests/.gitkeep', src: undefined, type: 'empty' });
132
+ return specs;
133
+ }
134
+
135
+ function createOptionalFileSpecs(selectedSkills) {
136
+ const specs = [];
137
+
138
+ for (const skill of selectedSkills) {
139
+ for (const fileRoot of skill.files) {
140
+ const absRoot = path.join(OPTIONAL_DIR, ...fileRoot.split('/'));
141
+ for (const file of walkFiles(absRoot).map(normalizePath).sort()) {
142
+ specs.push({
143
+ dest: harnessDest(file),
144
+ src: path.join(absRoot, ...file.split('/')),
145
+ type: 'optional',
146
+ skillId: skill.id,
147
+ });
148
+ }
149
+ }
150
+ }
151
+
152
+ return specs;
153
+ }
154
+
155
+ function duplicateDests(fileSpecs) {
156
+ const seen = new Set();
157
+ const duplicates = new Set();
158
+
159
+ for (const spec of fileSpecs) {
160
+ if (seen.has(spec.dest)) {
161
+ duplicates.add(spec.dest);
162
+ } else {
163
+ seen.add(spec.dest);
164
+ }
165
+ }
166
+
167
+ return [...duplicates].sort();
168
+ }
169
+
170
+ function createPlan(resolvedDir, fileSpecs) {
171
+ const plan = {
172
+ create: [],
173
+ skip: [],
174
+ overwrite: [],
175
+ backup: [],
176
+ conflict: [],
177
+ mkdir: [],
178
+ };
179
+
180
+ const fileSet = new Set(fileSpecs.map(spec => spec.dest));
181
+
182
+ const dirSet = new Set(['.']);
183
+ for (const file of fileSet) {
184
+ let dir = path.posix.dirname(file);
185
+ while (dir && dir !== '.') {
186
+ dirSet.add(dir);
187
+ dir = path.posix.dirname(dir);
188
+ }
189
+ }
190
+ for (const dir of EMPTY_DIRS) {
191
+ dirSet.add(dir);
192
+ }
193
+
194
+ for (const dir of [...dirSet].sort()) {
195
+ const absDir = dir === '.'
196
+ ? resolvedDir
197
+ : path.join(resolvedDir, ...dir.split('/'));
198
+ if (fs.existsSync(absDir)) {
199
+ if (!fs.statSync(absDir).isDirectory()) {
200
+ plan.conflict.push(dir === '.' ? './' : `${dir}/`);
201
+ }
202
+ } else {
203
+ plan.mkdir.push(dir === '.' ? './' : `${dir}/`);
204
+ }
205
+ }
206
+
207
+ return plan;
208
+ }
209
+
210
+ function addFileActions(plan, fileSpecs, resolvedDir, onConflict) {
211
+ for (const { dest: file } of fileSpecs) {
212
+ const destPath = path.join(resolvedDir, ...file.split('/'));
213
+
214
+ if (!fs.existsSync(destPath)) {
215
+ plan.create.push(file);
216
+ continue;
217
+ }
218
+
219
+ if (fs.statSync(destPath).isDirectory()) {
220
+ if (onConflict === 'skip') {
221
+ plan.skip.push(file);
222
+ } else {
223
+ plan.conflict.push(file);
224
+ }
225
+ continue;
226
+ }
227
+
228
+ if (onConflict === 'skip') {
229
+ plan.skip.push(file);
230
+ } else if (onConflict === 'backup') {
231
+ plan.backup.push(file);
232
+ } else if (onConflict === 'overwrite') {
233
+ plan.overwrite.push(file);
234
+ } else {
235
+ plan.conflict.push(file);
236
+ }
237
+ }
238
+ }
239
+
240
+ function createSummary(plan, { noWrites = false } = {}) {
241
+ return {
242
+ created: noWrites ? 0 : plan.create.length,
243
+ skipped: noWrites ? 0 : plan.skip.length,
244
+ overwritten: noWrites ? 0 : plan.overwrite.length,
245
+ backedUp: noWrites ? 0 : plan.backup.length,
246
+ conflicts: plan.conflict.length,
247
+ mkdir: noWrites ? 0 : plan.mkdir.length,
248
+ };
249
+ }
250
+
251
+ function nextBackupPath(destPath) {
252
+ const first = `${destPath}.harness-backup`;
253
+ if (!fs.existsSync(first)) {
254
+ return first;
255
+ }
256
+
257
+ for (let index = 1; ; index += 1) {
258
+ const candidate = `${first}.${index}`;
259
+ if (!fs.existsSync(candidate)) {
260
+ return candidate;
261
+ }
262
+ }
263
+ }
264
+
265
+ function registerOptionalContent(file, content, selectedSkills) {
266
+ if (!selectedSkills.length) return content;
267
+
268
+ if (file === 'Harness/MEMORY.md') {
269
+ const lines = selectedSkills.map(skill => (
270
+ `- [${skill.id}](../.claude/skills/${skill.id}/SKILL.md) - ${skill.description} Workflow: [workflows/${skill.id}.md](workflows/${skill.id}.md)`
271
+ ));
272
+ return content.replace(
273
+ 'Stack-specific skills can be added after the product shape is known.',
274
+ `Installed optional skills:\n\n${lines.join('\n')}\n\nStack-specific skills can be added after the product shape is known.`,
275
+ );
276
+ }
277
+
278
+ if (file === 'Harness/README.md') {
279
+ const lines = selectedSkills.map(skill => (
280
+ `- [${skill.title}](workflows/${skill.id}.md) - ${skill.description}`
281
+ ));
282
+ return `${content.trimEnd()}\n\n## Installed Optional Workflows\n\n${lines.join('\n')}\n`;
283
+ }
284
+
285
+ return content;
286
+ }
287
+
288
+ function registrationWarnings(plan, selectedSkills) {
289
+ const warnings = [];
290
+
291
+ if (selectedSkills.length) {
292
+ if (plan.skip.includes('Harness/MEMORY.md')) {
293
+ warnings.push('Harness/MEMORY.md was skipped; manually register selected optional skills under #Skills.');
294
+ }
295
+ if (plan.skip.includes('Harness/README.md')) {
296
+ warnings.push('Harness/README.md was skipped; manually register selected optional workflow paths.');
297
+ }
298
+ }
299
+
300
+ if (plan.skip.includes('README.md')) {
301
+ warnings.push('README.md was skipped; keep project run, build, test, and git conventions there, not in CLAUDE.md.');
302
+ }
303
+ if (plan.skip.includes('CLAUDE.md') || plan.conflict.includes('CLAUDE.md')) {
304
+ warnings.push('CLAUDE.md already exists; ask the user to confirm refactoring or merging it with the Harness root-entry contract before editing.');
305
+ }
306
+ if (plan.skip.includes('AGENTS.md')) {
307
+ warnings.push('AGENTS.md was skipped; ask for user consent before merging or replacing the project agent entry contract.');
308
+ }
309
+
310
+ return warnings;
311
+ }
312
+
313
+ export function generate({
314
+ projectName,
315
+ targetDir,
316
+ dryRun = false,
317
+ onConflict = 'fail',
318
+ withOptions = [],
319
+ withoutOptions = [],
320
+ preset = undefined,
321
+ }) {
56
322
  const created = [];
57
323
  const errors = [];
324
+ const warnings = [];
58
325
 
59
326
  // Resolve targetDir relative to cwd
60
327
  const resolvedDir = path.resolve(process.cwd(), targetDir);
328
+ const vars = { projectName };
61
329
 
62
- // Check if target exists and is non-empty
63
- if (fs.existsSync(resolvedDir)) {
64
- const existing = fs.readdirSync(resolvedDir).filter(f => f !== '.git');
65
- if (existing.length > 0) {
66
- console.log(pc.yellow(`Directory "${targetDir}" already exists and is not empty. Files may be overwritten.`));
67
- }
330
+ const optional = resolveOptionalSelection({ withOptions, withoutOptions, preset });
331
+ const fileSpecs = [
332
+ ...createCoreFileSpecs(),
333
+ ...createOptionalFileSpecs(optional.selectedSkills),
334
+ ];
335
+ const specsByDest = new Map(fileSpecs.map(spec => [spec.dest, spec]));
336
+ const plan = createPlan(resolvedDir, fileSpecs);
337
+ const duplicateDestinations = duplicateDests(fileSpecs);
338
+
339
+ if (!VALID_CONFLICT_POLICIES.has(onConflict)) {
340
+ errors.push(`Unknown conflict policy "${onConflict}". Use fail, skip, backup, or overwrite.`);
341
+ return {
342
+ success: false,
343
+ created,
344
+ errors,
345
+ plan,
346
+ summary: createSummary(plan, { noWrites: true }),
347
+ warnings,
348
+ };
68
349
  }
69
350
 
70
- const vars = { projectName };
351
+ if (optional.errors.length > 0) {
352
+ errors.push(...optional.errors);
353
+ return {
354
+ success: false,
355
+ created,
356
+ errors,
357
+ plan,
358
+ summary: createSummary(plan, { noWrites: true }),
359
+ warnings,
360
+ };
361
+ }
71
362
 
72
- try {
73
- // 1. Copy all template files
74
- console.log(pc.cyan('Copying templates...'));
75
- copyDir(TEMPLATES_DIR, resolvedDir, vars);
76
-
77
- // List all created files from templates
78
- created.push(...walkFiles(TEMPLATES_DIR).map(f => f.replace(/\\/g, '/')));
79
-
80
- // 2. Create empty directories
81
- console.log(pc.cyan('Creating placeholder directories...'));
82
- const emptyDirs = [
83
- '.claude/hooks',
84
- 'tests',
85
- ];
363
+ if (duplicateDestinations.length > 0) {
364
+ errors.push(`Generation stopped because duplicate template destination(s) were found: ${duplicateDestinations.join(', ')}`);
365
+ return {
366
+ success: false,
367
+ created,
368
+ errors,
369
+ plan,
370
+ summary: createSummary(plan, { noWrites: true }),
371
+ warnings,
372
+ };
373
+ }
374
+
375
+ addFileActions(plan, fileSpecs, resolvedDir, onConflict);
376
+ warnings.push(...registrationWarnings(plan, optional.selectedSkills));
377
+
378
+ if (dryRun) {
379
+ return {
380
+ success: true,
381
+ created,
382
+ errors,
383
+ plan,
384
+ summary: createSummary(plan),
385
+ warnings,
386
+ dryRun: true,
387
+ };
388
+ }
86
389
 
87
- for (const dir of emptyDirs) {
88
- const dirPath = path.join(resolvedDir, dir);
390
+ if (plan.conflict.length > 0) {
391
+ errors.push(`Generation stopped because ${plan.conflict.length} file conflict(s) were found: ${plan.conflict.join(', ')}`);
392
+ if (plan.conflict.includes('CLAUDE.md')) {
393
+ errors.push('CLAUDE.md already exists. It is the root agent entry contract; ask the user to confirm refactoring or merging it with the Harness entry contract before editing, backing up, or overwriting.');
394
+ }
395
+ return {
396
+ success: false,
397
+ created,
398
+ errors,
399
+ plan,
400
+ summary: createSummary(plan, { noWrites: true }),
401
+ warnings,
402
+ };
403
+ }
404
+
405
+ try {
406
+ for (const dir of plan.mkdir) {
407
+ const dirPath = dir === './'
408
+ ? resolvedDir
409
+ : path.join(resolvedDir, ...dir.slice(0, -1).split('/'));
89
410
  fs.mkdirSync(dirPath, { recursive: true });
90
- created.push(`${dir}/ (empty)`);
411
+ created.push(dir === './' ? './ (directory)' : `${dir} (directory)`);
91
412
  }
92
413
 
93
- // 3. Create .gitkeep in tests/
94
- const gitkeepPath = path.join(resolvedDir, 'tests', '.gitkeep');
95
- fs.writeFileSync(gitkeepPath, '', 'utf-8');
96
- created.push('tests/.gitkeep');
414
+ for (const file of plan.backup) {
415
+ const destPath = path.join(resolvedDir, ...file.split('/'));
416
+ fs.renameSync(destPath, nextBackupPath(destPath));
417
+ }
418
+
419
+ const writableFiles = [
420
+ ...plan.create,
421
+ ...plan.overwrite,
422
+ ...plan.backup,
423
+ ];
424
+
425
+ for (const file of writableFiles) {
426
+ const spec = specsByDest.get(file);
427
+ const destPath = path.join(resolvedDir, ...file.split('/'));
428
+ let content = spec.type === 'empty'
429
+ ? ''
430
+ : renderTemplate(spec.src, vars);
431
+ content = registerOptionalContent(file, content, optional.selectedSkills);
432
+
433
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
434
+ fs.writeFileSync(destPath, content, 'utf-8');
435
+ created.push(file);
436
+ }
97
437
 
98
- return { success: true, created, errors };
438
+ return {
439
+ success: true,
440
+ created,
441
+ errors,
442
+ plan,
443
+ summary: createSummary(plan),
444
+ warnings,
445
+ };
99
446
 
100
447
  } catch (err) {
101
448
  errors.push(err.message);
102
- return { success: false, created, errors };
449
+ return {
450
+ success: false,
451
+ created,
452
+ errors,
453
+ plan,
454
+ summary: createSummary(plan),
455
+ warnings,
456
+ };
103
457
  }
104
458
  }