create-harness-vibe-coding 0.7.2 → 0.8.1

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 (82) hide show
  1. package/README-CN.md +103 -155
  2. package/README.md +213 -175
  3. package/bin/create-harness-vibe-coding.js +2 -2
  4. package/package.json +45 -43
  5. package/src/generator.js +115 -77
  6. package/src/index.js +159 -49
  7. package/src/prompts.js +95 -27
  8. package/templates/common/.claude/agents/architect-manager.md +45 -45
  9. package/templates/common/.claude/agents/context-master.md +77 -77
  10. package/templates/common/.claude/agents/debugger.md +41 -41
  11. package/templates/common/.claude/agents/explore-manager.md +41 -41
  12. package/templates/common/.claude/agents/implement-manager.md +49 -49
  13. package/templates/common/.claude/agents/implementer.md +40 -40
  14. package/templates/common/.claude/agents/memory-master.md +66 -66
  15. package/templates/common/.claude/agents/planner.md +34 -34
  16. package/templates/common/.claude/agents/researcher.md +41 -41
  17. package/templates/common/.claude/agents/review-manager.md +56 -56
  18. package/templates/common/.claude/agents/test-writer.md +38 -38
  19. package/templates/common/.claude/agents/verifier.md +32 -32
  20. package/templates/common/.claude/rules/ecc/common.md +45 -45
  21. package/templates/common/.claude/settings.json +68 -68
  22. package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +36 -31
  23. package/templates/common/.claude/skills/wf/SKILL.md +36 -0
  24. package/templates/common/.claude/skills/wf-learn/SKILL.md +13 -28
  25. package/templates/common/.claude/skills/wf-max/SKILL.md +24 -88
  26. package/templates/common/.claude/skills/wf-readme/SKILL.md +49 -49
  27. package/templates/common/.claude/skills/wf-remove/SKILL.md +17 -38
  28. package/templates/common/.claude/skills/wf-review/SKILL.md +19 -69
  29. package/templates/common/.claude/skills/wf-update/SKILL.md +19 -61
  30. package/templates/common/.codex/config.toml +2 -2
  31. package/templates/common/.codex/hooks.json +37 -37
  32. package/templates/common/.harness-version +53 -51
  33. package/templates/common/AGENTS.md +30 -30
  34. package/templates/common/CLAUDE.md +94 -94
  35. package/templates/common/{docs/harness → Harness}/PROGRESS.md +17 -17
  36. package/templates/common/{docs → Harness}/README.md +18 -17
  37. package/templates/common/{docs/harness → Harness}/WF-MAX.md +223 -223
  38. package/templates/common/Harness/WF.md +217 -0
  39. package/templates/common/{docs/harness → Harness}/architecture.md +126 -126
  40. package/templates/common/{docs/harness → Harness}/dispatch.md +99 -99
  41. package/templates/common/{docs → Harness}/research/research-results.md +66 -66
  42. package/templates/common/{docs/harness → Harness}/subagents.md +194 -194
  43. package/templates/common/{docs → Harness}/tasks/_template/ARTIFACTS.md +3 -3
  44. package/templates/common/{docs → Harness}/tasks/_template/NOTES.md +3 -3
  45. package/templates/common/{docs → Harness}/tasks/_template/PLAN.md +40 -40
  46. package/templates/common/{docs → Harness}/tasks/_template/PROGRESS.md +29 -29
  47. package/templates/common/MEMORY.md +68 -66
  48. package/templates/common/README.md +44 -37
  49. package/templates/common/SETUP.md +67 -47
  50. package/templates/common/memory/agent-lessons-patterns.md +21 -21
  51. package/templates/common/memory/tool-usage-reflections.md +21 -21
  52. package/templates/common/memory/user-corrections-preferences.md +21 -21
  53. package/templates/common/scripts/scan-clean.mjs +450 -449
  54. package/templates/common/scripts/validate-harness.mjs +56 -34
  55. package/templates/common/scripts/wf-mode-hook.mjs +318 -318
  56. package/templates/common/scripts/wf-remove.mjs +400 -384
  57. package/templates/common/scripts/wf-statusline.ps1 +38 -38
  58. package/templates/common/scripts/wf-statusline.sh +48 -48
  59. package/templates/common/scripts/wf-update-check.mjs +389 -389
  60. package/templates/optional/catalog.json +63 -29
  61. package/templates/optional/skills/browser-e2e/.claude/skills/wf-browser/SKILL.md +194 -194
  62. package/templates/optional/skills/browser-e2e/{docs → Harness}/workflows/browser-e2e.md +75 -75
  63. package/templates/optional/skills/github-pr-review/{docs → Harness}/workflows/github-pr-review.md +28 -28
  64. package/templates/optional/skills/python-backend/{docs → Harness}/workflows/python-backend.md +34 -34
  65. package/templates/optional/skills/ts-react-frontend/{docs → Harness}/workflows/ts-react-frontend.md +35 -35
  66. package/templates/optional/skills/ui-ux-review/{docs → Harness}/workflows/ui-ux-review.md +26 -26
  67. package/templates/common/.claude/commands/wf-learn.md +0 -17
  68. package/templates/common/.claude/commands/wf-max.md +0 -49
  69. package/templates/common/.claude/commands/wf-remove.md +0 -23
  70. package/templates/common/.claude/commands/wf-review.md +0 -25
  71. package/templates/common/.claude/commands/wf-update.md +0 -17
  72. package/templates/common/.claude/commands/wf.md +0 -31
  73. package/templates/common/commands/wf-max.toml +0 -18
  74. package/templates/common/commands/wf-review.toml +0 -15
  75. package/templates/common/docs/harness/WF.md +0 -174
  76. package/templates/optional/skills/browser-e2e/.claude/commands/wf-browser.md +0 -25
  77. /package/templates/common/{docs/harness → Harness}/agent-workflow.md +0 -0
  78. /package/templates/common/{docs/harness → Harness}/context-loading.md +0 -0
  79. /package/templates/common/{docs/harness → Harness}/extension.md +0 -0
  80. /package/templates/common/{docs/harness → Harness}/lifecycle.md +0 -0
  81. /package/templates/common/{docs → Harness}/research/PRD.md +0 -0
  82. /package/templates/common/{docs → Harness}/research/README.md +0 -0
package/src/generator.js CHANGED
@@ -76,18 +76,12 @@ const EMPTY_DIRS = [
76
76
  export function harnessDest(file) {
77
77
  if (file === '.harness-version') return 'Harness/.harness-version';
78
78
  if (file === 'SETUP.md') return 'Harness/SETUP.md';
79
- if (file === 'MEMORY.md') return 'Harness/MEMORY.md';
80
- if (file.startsWith('scripts/')) return `Harness/${file}`;
81
- if (file.startsWith('memory/')) return `Harness/${file}`;
82
- if (file === 'docs/README.md') return 'Harness/README.md';
83
- if (file.startsWith('docs/harness/')) return file.replace(/^docs\/harness\//, 'Harness/');
84
- if (file.startsWith('docs/research/')) return file.replace(/^docs\/research\//, 'Harness/research/');
85
- if (file.startsWith('docs/domain/')) return file.replace(/^docs\/domain\//, 'Harness/domain/');
86
- if (file.startsWith('docs/features/')) return file.replace(/^docs\/features\//, 'Harness/features/');
87
- if (file.startsWith('docs/workflows/')) return file.replace(/^docs\/workflows\//, 'Harness/workflows/');
88
- if (file.startsWith('docs/tasks/')) return file.replace(/^docs\/tasks\//, 'Harness/tasks/');
89
- return file;
90
- }
79
+ if (file === 'MEMORY.md') return 'Harness/MEMORY.md';
80
+ if (file.startsWith('scripts/')) return `Harness/${file}`;
81
+ if (file.startsWith('memory/')) return `Harness/${file}`;
82
+ if (file.startsWith('Harness/')) return file;
83
+ return file;
84
+ }
91
85
 
92
86
  /**
93
87
  * Replace {{vars}} in file contents.
@@ -126,27 +120,39 @@ function normalizePath(filePath) {
126
120
  }
127
121
 
128
122
  export function getOptionalCatalog() {
129
- if (!fs.existsSync(OPTIONAL_CATALOG)) {
130
- return { skills: [], presets: {} };
131
- }
123
+ if (!fs.existsSync(OPTIONAL_CATALOG)) {
124
+ return { skills: [], presets: {}, externalRecommendations: [] };
125
+ }
132
126
 
133
127
  return JSON.parse(fs.readFileSync(OPTIONAL_CATALOG, 'utf-8'));
134
128
  }
135
129
 
136
- function normalizeOptionList(value) {
130
+ function normalizeOptionList(value) {
137
131
  const values = Array.isArray(value) ? value : [value];
138
132
  return values
139
133
  .filter(Boolean)
140
134
  .flatMap(item => String(item).split(','))
141
135
  .map(item => item.trim())
142
136
  .filter(Boolean);
143
- }
144
-
145
- function resolveOptionalSelection({ withOptions = [], withoutOptions = [], preset = undefined }) {
146
- const catalog = getOptionalCatalog();
147
- const skillsById = new Map(catalog.skills.map(skill => [skill.id, skill]));
148
- const selected = [];
149
- const errors = [];
137
+ }
138
+
139
+ function externalRecommendations(catalog) {
140
+ return Array.isArray(catalog.externalRecommendations)
141
+ ? catalog.externalRecommendations
142
+ : [];
143
+ }
144
+
145
+ function resolveOptionalSelection({
146
+ withOptions = [],
147
+ withoutOptions = [],
148
+ preset = undefined,
149
+ externalOptions = [],
150
+ }) {
151
+ const catalog = getOptionalCatalog();
152
+ const skillsById = new Map(catalog.skills.map(skill => [skill.id, skill]));
153
+ const recommendationsById = new Map(externalRecommendations(catalog).map(item => [item.id, item]));
154
+ const selected = [];
155
+ const errors = [];
150
156
 
151
157
  if (preset) {
152
158
  if (!catalog.presets[preset]) {
@@ -165,37 +171,46 @@ function resolveOptionalSelection({ withOptions = [], withoutOptions = [], prese
165
171
  errors.push(`Unknown optional skill "${id}". Run --list-options to see available options.`);
166
172
  }
167
173
  }
168
- for (const id of withoutIds) {
169
- if (!skillsById.has(id)) {
170
- errors.push(`Unknown optional skill in --without "${id}". Run --list-options to see available options.`);
171
- }
172
- }
173
-
174
- const withoutSet = new Set(withoutIds);
175
- const selectedIds = ids.filter(id => !withoutSet.has(id));
176
-
177
- return {
178
- catalog,
179
- selectedSkills: errors.length ? [] : selectedIds.map(id => skillsById.get(id)),
180
- errors,
181
- };
182
- }
183
-
184
- function createCoreFileSpecs() {
185
- const specs = walkFiles(TEMPLATES_DIR)
186
- .map(file => normalizePath(file))
187
- .sort()
188
- .map(file => ({
189
- dest: harnessDest(file),
190
- src: path.join(TEMPLATES_DIR, ...file.split('/')),
191
- type: 'common',
192
- }));
193
-
194
- specs.push({ dest: 'tests/.gitkeep', src: undefined, type: 'empty' });
195
- return specs;
196
- }
197
-
198
- function createOptionalFileSpecs(selectedSkills) {
174
+ for (const id of withoutIds) {
175
+ if (!skillsById.has(id)) {
176
+ errors.push(`Unknown optional skill in --without "${id}". Run --list-options to see available options.`);
177
+ }
178
+ }
179
+
180
+ const recommendationIds = [...new Set(normalizeOptionList(externalOptions))];
181
+ for (const id of recommendationIds) {
182
+ if (!recommendationsById.has(id)) {
183
+ errors.push(`Unknown external recommendation "${id}". Run --list-options to see available options.`);
184
+ }
185
+ }
186
+
187
+ const withoutSet = new Set(withoutIds);
188
+ const selectedIds = ids.filter(id => !withoutSet.has(id));
189
+
190
+ return {
191
+ catalog,
192
+ selectedSkills: errors.length ? [] : selectedIds.map(id => skillsById.get(id)),
193
+ selectedRecommendations: errors.length ? [] : recommendationIds.map(id => recommendationsById.get(id)),
194
+ errors,
195
+ };
196
+ }
197
+
198
+ function createCoreFileSpecs() {
199
+ const specs = walkFiles(TEMPLATES_DIR)
200
+ .map(file => normalizePath(file))
201
+ .sort()
202
+ .map(file => ({
203
+ dest: harnessDest(file),
204
+ src: path.join(TEMPLATES_DIR, ...file.split('/')),
205
+ type: 'common',
206
+ }));
207
+
208
+ specs.push(...createCodexSkillMirrors(specs));
209
+ specs.push({ dest: 'tests/.gitkeep', src: undefined, type: 'empty' });
210
+ return specs;
211
+ }
212
+
213
+ function createOptionalFileSpecs(selectedSkills) {
199
214
  const specs = [];
200
215
 
201
216
  for (const skill of selectedSkills) {
@@ -208,12 +223,22 @@ function createOptionalFileSpecs(selectedSkills) {
208
223
  type: 'optional',
209
224
  skillId: skill.id,
210
225
  });
211
- }
212
- }
213
- }
214
-
215
- return specs;
216
- }
226
+ }
227
+ }
228
+ }
229
+
230
+ specs.push(...createCodexSkillMirrors(specs));
231
+ return specs;
232
+ }
233
+
234
+ function createCodexSkillMirrors(fileSpecs) {
235
+ return fileSpecs
236
+ .filter(spec => spec.dest.startsWith('.claude/skills/'))
237
+ .map(spec => ({
238
+ ...spec,
239
+ dest: spec.dest.replace(/^\.claude\/skills\//, '.agents/skills/'),
240
+ }));
241
+ }
217
242
 
218
243
  function duplicateDests(fileSpecs) {
219
244
  const seen = new Set();
@@ -325,13 +350,13 @@ function nextBackupPath(destPath) {
325
350
  }
326
351
  }
327
352
 
328
- function registerOptionalContent(file, content, selectedSkills) {
353
+ function registerOptionalContent(file, content, selectedSkills) {
329
354
  if (!selectedSkills.length) return content;
330
355
 
331
- if (file === 'Harness/MEMORY.md') {
332
- const lines = selectedSkills.map(skill => (
333
- `- [${skill.id}](../.claude/skills/${skill.id}/SKILL.md) - ${skill.description} Workflow: [workflows/${skill.id}.md](workflows/${skill.id}.md)`
334
- ));
356
+ if (file === 'Harness/MEMORY.md') {
357
+ const lines = selectedSkills.map(skill => (
358
+ `- [${skill.id}](../.claude/skills/${skill.id}/SKILL.md) - ${skill.description} Codex mirror: [${skill.id}](../.agents/skills/${skill.id}/SKILL.md). Workflow: [workflows/${skill.id}.md](workflows/${skill.id}.md)`
359
+ ));
335
360
  return content.replace(
336
361
  'Stack-specific skills can be added after the product shape is known.',
337
362
  `Installed optional skills:\n\n${lines.join('\n')}\n\nStack-specific skills can be added after the product shape is known.`,
@@ -345,8 +370,18 @@ function registerOptionalContent(file, content, selectedSkills) {
345
370
  return `${content.trimEnd()}\n\n## Installed Optional Workflows\n\n${lines.join('\n')}\n`;
346
371
  }
347
372
 
348
- return content;
349
- }
373
+ return content;
374
+ }
375
+
376
+ function registerExternalRecommendations(file, content, selectedRecommendations) {
377
+ if (!selectedRecommendations.length || file !== 'Harness/SETUP.md') return content;
378
+
379
+ const lines = selectedRecommendations.map(item => (
380
+ `- \`${item.id}\` - ${item.description} Recommendation only; not installed by this scaffold. Source: ${item.url}`
381
+ ));
382
+
383
+ return `${content.trimEnd()}\n\n## Selected External Recommendations\n\n${lines.join('\n')}\n`;
384
+ }
350
385
 
351
386
  function registrationWarnings(plan, selectedSkills) {
352
387
  const warnings = [];
@@ -378,10 +413,11 @@ export function generate({
378
413
  targetDir,
379
414
  dryRun = false,
380
415
  onConflict = 'fail',
381
- withOptions = [],
382
- withoutOptions = [],
383
- preset = undefined,
384
- }) {
416
+ withOptions = [],
417
+ withoutOptions = [],
418
+ preset = undefined,
419
+ externalOptions = [],
420
+ }) {
385
421
  const created = [];
386
422
  const errors = [];
387
423
  const warnings = [];
@@ -394,7 +430,7 @@ export function generate({
394
430
  generatedTimestamp: new Date().toISOString(),
395
431
  };
396
432
 
397
- const optional = resolveOptionalSelection({ withOptions, withoutOptions, preset });
433
+ const optional = resolveOptionalSelection({ withOptions, withoutOptions, preset, externalOptions });
398
434
  const fileSpecs = [
399
435
  ...createCoreFileSpecs(),
400
436
  ...createOptionalFileSpecs(optional.selectedSkills),
@@ -500,10 +536,11 @@ export function generate({
500
536
  }
501
537
  const spec = specsByDest.get(file);
502
538
  const destPath = path.join(resolvedDir, ...file.split('/'));
503
- let content = spec.type === 'empty'
504
- ? ''
505
- : renderTemplate(spec.src, vars);
506
- content = registerOptionalContent(file, content, optional.selectedSkills);
539
+ let content = spec.type === 'empty'
540
+ ? ''
541
+ : renderTemplate(spec.src, vars);
542
+ content = registerOptionalContent(file, content, optional.selectedSkills);
543
+ content = registerExternalRecommendations(file, content, optional.selectedRecommendations);
507
544
 
508
545
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
509
546
  fs.writeFileSync(destPath, content, 'utf-8');
@@ -521,7 +558,8 @@ export function generate({
521
558
  }
522
559
  parsed.generator = vars.generatorVersion;
523
560
  parsed.generated = vars.generatedTimestamp;
524
- parsed.options = optional.selectedSkills.map(s => s.id);
561
+ parsed.options = optional.selectedSkills.map(s => s.id);
562
+ parsed.externalRecommendations = optional.selectedRecommendations.map(item => item.id);
525
563
  parsed.autoCheck = true;
526
564
  parsed.checksums = computeChecksums(generatedFiles);
527
565
  // Build sources map from fileSpecs (all writable files, not just generatedFiles)
package/src/index.js CHANGED
@@ -1,14 +1,17 @@
1
- #!/usr/bin/env node
2
- import * as p from '@clack/prompts';
3
- import pc from 'picocolors';
4
- import { askProjectName, askTargetDir } from './prompts.js';
5
- import { generate, getOptionalCatalog } from './generator.js';
1
+ #!/usr/bin/env node
2
+ import * as p from '@clack/prompts';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import pc from 'picocolors';
6
+ import { askConflictPolicy, askOptionalSelections, askProjectName, askTargetDir } from './prompts.js';
7
+ import { generate, getOptionalCatalog } from './generator.js';
6
8
 
7
9
  // ── CLI flags ──────────────────────────────────────────────
8
10
  const raw = process.argv.slice(2);
9
- const parsed = parseArgs(raw);
10
- const showHelp = parsed.flags.help || parsed.flags.h;
11
- const skipPrompts = parsed.flags.yes || parsed.flags.y;
11
+ const parsed = parseArgs(raw);
12
+ const showHelp = parsed.flags.help || parsed.flags.h;
13
+ const skipPrompts = parsed.flags.yes || parsed.flags.y;
14
+ const conflictPolicyProvided = parsed.flags.onConflict !== undefined;
12
15
 
13
16
  if (parsed.errors.length > 0) {
14
17
  for (const err of parsed.errors) {
@@ -33,11 +36,14 @@ if (showHelp) {
33
36
  console.log(' -h, --help Show this help');
34
37
  console.log(' --dry-run Print the planned writes without creating files');
35
38
  console.log(' --on-conflict <policy> fail, skip, backup, or overwrite (default: fail)');
36
- console.log(' --with <id,id> Add optional local workflow skills');
37
- console.log(' --without <id,id> Remove optional workflow skills selected by --preset or --with');
38
- console.log(' --preset <name> Add a built-in optional workflow preset');
39
- console.log(' --list-options Print optional workflow skills and presets');
40
- console.log(' --json Output machine-readable JSON (use with --dry-run for planning)');
39
+ console.log(' --with <id,id> Add optional local workflow skills');
40
+ console.log(' --without <id,id> Remove optional workflow skills selected by --preset or --with');
41
+ console.log(' --recommend <id,id> Record recommendation-only external capabilities');
42
+ console.log(' --preset <name> Add a built-in optional workflow preset');
43
+ console.log(' --list-options Print optional workflow skills and presets');
44
+ console.log(' --json Output machine-readable JSON (use with --dry-run for planning)');
45
+ console.log('');
46
+ console.log(' Interactive mode offers checkbox selection for optional workflows and external recommendations.');
41
47
  console.log('');
42
48
  console.log(' Examples:');
43
49
  console.log(' npx create-harness-vibe-coding@latest');
@@ -66,10 +72,11 @@ const generationOptions = {
66
72
  dryRun: Boolean(parsed.flags.dryRun),
67
73
  onConflict: parsed.flags.onConflict || 'fail',
68
74
  withOptions: parsed.flags.with || [],
69
- withoutOptions: parsed.flags.without || [],
70
- preset: parsed.flags.preset,
71
- json: Boolean(parsed.flags.json),
72
- };
75
+ withoutOptions: parsed.flags.without || [],
76
+ externalOptions: parsed.flags.recommend || [],
77
+ preset: parsed.flags.preset,
78
+ json: Boolean(parsed.flags.json),
79
+ };
73
80
 
74
81
  const DEFAULT_NAME = 'my-vibe-project';
75
82
 
@@ -99,8 +106,8 @@ if (argName || skipPrompts) {
99
106
 
100
107
  console.log(pc.dim('────────────────────────────────────────────'));
101
108
  console.log(` Project ${pc.green(projectName)}`);
102
- console.log(` Directory ${pc.green(targetDir)}`);
103
- console.log(` Creates ${pc.cyan('CLAUDE.md, README.md, Harness/PROGRESS.md, Harness/, .claude/, tests/')}`);
109
+ console.log(` Directory ${pc.green(targetDir)}`);
110
+ console.log(` Creates ${pc.cyan('CLAUDE.md, README.md, Harness/PROGRESS.md, Harness/, .claude/, .agents/, tests/')}`);
104
111
  if (generationOptions.dryRun) {
105
112
  console.log(` Mode ${pc.yellow('dry-run')}`);
106
113
  }
@@ -108,9 +115,12 @@ if (argName || skipPrompts) {
108
115
  if (generationOptions.withOptions.length > 0) {
109
116
  console.log(` Optional ${pc.cyan(generationOptions.withOptions.join(','))}`);
110
117
  }
111
- if (generationOptions.withoutOptions.length > 0) {
112
- console.log(` Without ${pc.cyan(generationOptions.withoutOptions.join(','))}`);
113
- }
118
+ if (generationOptions.withoutOptions.length > 0) {
119
+ console.log(` Without ${pc.cyan(generationOptions.withoutOptions.join(','))}`);
120
+ }
121
+ if (generationOptions.externalOptions.length > 0) {
122
+ console.log(` Recommend ${pc.cyan(generationOptions.externalOptions.join(','))}`);
123
+ }
114
124
  if (generationOptions.preset) {
115
125
  console.log(` Preset ${pc.cyan(generationOptions.preset)}`);
116
126
  }
@@ -131,19 +141,54 @@ if (argName || skipPrompts) {
131
141
  console.log(pc.dim(` Project: ${projectName} (default)`));
132
142
  }
133
143
 
134
- try {
135
- targetDir = await askTargetDir(projectName);
136
- } catch {
144
+ try {
145
+ targetDir = await askTargetDir(projectName);
146
+ } catch {
137
147
  targetDir = `./${projectName}`;
138
148
  console.log(pc.dim(` Directory: ${targetDir} (default)`));
139
- }
149
+ }
150
+
151
+ const scan = scanTarget(targetDir);
152
+ printScan(scan);
153
+
154
+ if (!generationOptions.dryRun && !conflictPolicyProvided && scan.needsConflictPolicy) {
155
+ try {
156
+ generationOptions.onConflict = await askConflictPolicy(scan);
157
+ } catch {
158
+ generationOptions.onConflict = 'skip';
159
+ console.log(pc.dim(' Conflicts: skip (preserve existing files default)'));
160
+ }
161
+ }
162
+
163
+ const optionFlagsProvided = generationOptions.withOptions.length > 0
164
+ || generationOptions.withoutOptions.length > 0
165
+ || generationOptions.externalOptions.length > 0
166
+ || generationOptions.preset;
167
+
168
+ if (!optionFlagsProvided) {
169
+ try {
170
+ const selected = await askOptionalSelections(getOptionalCatalog());
171
+ generationOptions.withOptions = selected.withOptions;
172
+ generationOptions.externalOptions = selected.externalOptions;
173
+ } catch {
174
+ generationOptions.withOptions = [];
175
+ generationOptions.externalOptions = [];
176
+ console.log(pc.dim(' Optional: none (default)'));
177
+ }
178
+ }
140
179
 
141
180
  console.log('');
142
181
  console.log(pc.dim('────────────────────────────────────────────'));
143
182
  console.log(` Project ${pc.green(projectName)}`);
144
- console.log(` Directory ${pc.green(targetDir)}`);
145
- console.log(` Creates ${pc.cyan('CLAUDE.md, README.md, Harness/PROGRESS.md, Harness/, .claude/, tests/')}`);
146
- console.log(` Conflicts ${pc.cyan(generationOptions.onConflict)}`);
183
+ console.log(` Directory ${pc.green(targetDir)}`);
184
+ console.log(` Creates ${pc.cyan('CLAUDE.md, README.md, Harness/PROGRESS.md, Harness/, .claude/, .agents/, tests/')}`);
185
+ console.log(` Conflicts ${pc.cyan(generationOptions.onConflict)}`);
186
+ if (generationOptions.withOptions.length > 0) {
187
+ console.log(` Optional ${pc.cyan(generationOptions.withOptions.join(','))}`);
188
+ }
189
+ if (generationOptions.externalOptions.length > 0) {
190
+ console.log(` Recommend ${pc.cyan(generationOptions.externalOptions.join(','))}`);
191
+ }
147
192
  console.log(pc.dim('────────────────────────────────────────────'));
148
193
  console.log('');
149
194
 
@@ -204,10 +249,11 @@ function printResult(result, targetDir) {
204
249
 
205
250
  printWarnings(result);
206
251
 
207
- console.log(pc.bold('Next steps:'));
208
- console.log(` ${pc.cyan(`cd ${targetDir}`)}`);
209
- console.log(` ${pc.cyan('claude')} # Start Claude Code`);
210
- console.log(` Tell Claude: "${pc.yellow('Read Harness/SETUP.md. Bootstrap this project from idea to first vertical slice.')}"`);
252
+ console.log(pc.bold('Next steps:'));
253
+ console.log(` ${pc.cyan(`cd ${targetDir}`)}`);
254
+ console.log(` ${pc.cyan('claude')} # Start Claude Code`);
255
+ console.log(` ${pc.cyan('codex')} # Or start Codex`);
256
+ console.log(` Tell your agent: "${pc.yellow('Read Harness/SETUP.md. Bootstrap this project from idea to first vertical slice.')}"`);
211
257
  console.log('');
212
258
  console.log(pc.dim(' Harness/SETUP.md is temporary. Delete it after initialization.'));
213
259
  console.log('');
@@ -259,10 +305,10 @@ function parseArgs(args) {
259
305
  flags.yes = true;
260
306
  } else if (arg === '--dry-run') {
261
307
  flags.dryRun = true;
262
- } else if (arg === '--list-options') {
263
- flags.listOptions = true;
264
- } else if (arg === '--json') {
265
- flags.json = true;
308
+ } else if (arg === '--list-options') {
309
+ flags.listOptions = true;
310
+ } else if (arg === '--json') {
311
+ flags.json = true;
266
312
  } else if (arg === '--on-conflict') {
267
313
  const parsedValue = readValue('--on-conflict', i);
268
314
  flags.onConflict = parsedValue.value;
@@ -281,9 +327,22 @@ function parseArgs(args) {
281
327
  if (parsedValue.value !== undefined) flags.without.push(parsedValue.value);
282
328
  i = parsedValue.nextIndex;
283
329
  } else if (arg.startsWith('--without=')) {
284
- const value = readEqualsValue('--without', arg.slice('--without='.length));
285
- if (value !== undefined) flags.without.push(value);
286
- } else if (arg === '--preset') {
330
+ const value = readEqualsValue('--without', arg.slice('--without='.length));
331
+ if (value !== undefined) flags.without.push(value);
332
+ } else if (arg === '--recommend') {
333
+ const parsedValue = readValue('--recommend', i);
334
+ if (parsedValue.value !== undefined) {
335
+ if (!flags.recommend) flags.recommend = [];
336
+ flags.recommend.push(parsedValue.value);
337
+ }
338
+ i = parsedValue.nextIndex;
339
+ } else if (arg.startsWith('--recommend=')) {
340
+ const value = readEqualsValue('--recommend', arg.slice('--recommend='.length));
341
+ if (value !== undefined) {
342
+ if (!flags.recommend) flags.recommend = [];
343
+ flags.recommend.push(value);
344
+ }
345
+ } else if (arg === '--preset') {
287
346
  const parsedValue = readValue('--preset', i);
288
347
  flags.preset = parsedValue.value;
289
348
  i = parsedValue.nextIndex;
@@ -308,13 +367,21 @@ function printOptions() {
308
367
  console.log(` ${pc.cyan(skill.id)} - ${skill.description}`);
309
368
  }
310
369
 
311
- console.log('');
312
- console.log(pc.bold('Presets:'));
313
- for (const [name, skills] of Object.entries(catalog.presets)) {
314
- console.log(` ${pc.cyan(name)} - ${skills.join(', ')}`);
315
- }
316
- console.log('');
317
- }
370
+ console.log('');
371
+ console.log(pc.bold('Presets:'));
372
+ for (const [name, skills] of Object.entries(catalog.presets)) {
373
+ console.log(` ${pc.cyan(name)} - ${skills.join(', ')}`);
374
+ }
375
+
376
+ if (catalog.externalRecommendations?.length) {
377
+ console.log('');
378
+ console.log(pc.bold('External recommendations:'));
379
+ for (const item of catalog.externalRecommendations) {
380
+ console.log(` ${pc.cyan(item.id)} - ${item.description} (${item.installMode}) ${item.url || ''}`.trimEnd());
381
+ }
382
+ }
383
+ console.log('');
384
+ }
318
385
 
319
386
  function printSummary(summary) {
320
387
  console.log(` created ${pc.green(summary.created)}`);
@@ -345,11 +412,54 @@ function printWarnings(result) {
345
412
  }
346
413
  }
347
414
 
348
- function printJsonResult(result) {
415
+ function printJsonResult(result) {
349
416
  // Remove `created` array from output — it is already in the plan, avoid duplication
350
417
  const { created, ...rest } = result;
351
418
  console.log(JSON.stringify(rest, null, 2));
352
419
  if (!result.success) {
353
420
  process.exit(1);
354
421
  }
355
- }
422
+ }
423
+
424
+ function scanTarget(targetDir) {
425
+ const resolvedDir = path.resolve(process.cwd(), targetDir);
426
+ const exists = fs.existsSync(resolvedDir);
427
+ const isDirectory = exists && fs.statSync(resolvedDir).isDirectory();
428
+ const entries = isDirectory
429
+ ? fs.readdirSync(resolvedDir)
430
+ : [];
431
+ const hasHarness = isDirectory && fs.existsSync(path.join(resolvedDir, 'Harness'));
432
+
433
+ return {
434
+ resolvedDir,
435
+ exists,
436
+ isDirectory,
437
+ entries,
438
+ hasHarness,
439
+ hasClaude: isDirectory && fs.existsSync(path.join(resolvedDir, 'CLAUDE.md')),
440
+ hasAgents: isDirectory && fs.existsSync(path.join(resolvedDir, 'AGENTS.md')),
441
+ hasAgentSkills: isDirectory && fs.existsSync(path.join(resolvedDir, '.agents')),
442
+ hasCodex: isDirectory && fs.existsSync(path.join(resolvedDir, '.codex')),
443
+ hasDocs: isDirectory && fs.existsSync(path.join(resolvedDir, 'docs')),
444
+ needsConflictPolicy: exists && (!isDirectory || entries.length > 0 || hasHarness),
445
+ };
446
+ }
447
+
448
+ function printScan(scan) {
449
+ if (!scan.exists) return;
450
+
451
+ console.log('');
452
+ console.log(pc.bold('Root scan:'));
453
+ console.log(` Directory ${pc.dim(scan.resolvedDir)}`);
454
+ if (!scan.isDirectory) {
455
+ console.log(` Conflict ${pc.yellow('target path exists and is not a directory')}`);
456
+ return;
457
+ }
458
+ console.log(` Entries ${pc.cyan(scan.entries.length)}`);
459
+ if (scan.hasHarness) console.log(` Harness ${pc.yellow('exists')}`);
460
+ if (scan.hasClaude) console.log(` CLAUDE.md ${pc.yellow('exists')}`);
461
+ if (scan.hasAgents) console.log(` AGENTS.md ${pc.yellow('exists')}`);
462
+ if (scan.hasAgentSkills) console.log(` .agents/ ${pc.yellow('exists')}`);
463
+ if (scan.hasCodex) console.log(` .codex/ ${pc.yellow('exists')}`);
464
+ if (scan.hasDocs) console.log(` docs/ ${pc.dim('project-owned; Harness stays in Harness/')}`);
465
+ }