dflow-sdd-ddd 0.1.0 → 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 (52) hide show
  1. package/CHANGELOG.md +1176 -0
  2. package/CONTRIBUTING.md +123 -0
  3. package/README.md +199 -157
  4. package/TEMPLATE-COVERAGE.md +46 -0
  5. package/TEMPLATE-LANGUAGE-GLOSSARY.md +52 -0
  6. package/bin/dflow.js +73 -8
  7. package/docs/evaluating-dflow.md +226 -0
  8. package/docs/migrating-to-dflow-v1.md +212 -0
  9. package/docs/npm-publish-checklist.md +93 -0
  10. package/docs/release-versioning-policy.md +99 -0
  11. package/docs/using-with-claude-code.md +207 -0
  12. package/docs/using-with-codex.md +244 -0
  13. package/docs/why-ddd-for-ai.md +35 -0
  14. package/lib/init.js +444 -66
  15. package/package.json +13 -7
  16. package/templates/brownfield/scaffolding/AI-AGENT-GUIDE.md +92 -0
  17. package/templates/{webforms → brownfield}/scaffolding/CLAUDE-md-snippet.md +8 -9
  18. package/templates/{webforms → brownfield}/scaffolding/Git-principles-gitflow.md +1 -1
  19. package/templates/{webforms → brownfield}/scaffolding/Git-principles-trunk.md +1 -1
  20. package/templates/{webforms → brownfield}/scaffolding/_conventions.md +2 -1
  21. package/templates/{webforms → brownfield}/scaffolding/_overview.md +2 -2
  22. package/templates/{webforms → brownfield}/templates/context-map.md +1 -1
  23. package/templates/{webforms → brownfield}/templates/glossary.md +1 -1
  24. package/templates/{webforms → brownfield}/templates/models.md +1 -1
  25. package/templates/{webforms → brownfield}/templates/phase-spec.md +1 -1
  26. package/templates/{webforms → brownfield}/templates/rules.md +1 -1
  27. package/templates/{webforms → brownfield}/templates/tech-debt.md +1 -1
  28. package/templates/greenfield/scaffolding/AI-AGENT-GUIDE.md +92 -0
  29. package/templates/{core → greenfield}/scaffolding/CLAUDE-md-snippet.md +15 -14
  30. package/templates/{core → greenfield}/scaffolding/Git-principles-gitflow.md +1 -1
  31. package/templates/{core → greenfield}/scaffolding/Git-principles-trunk.md +1 -1
  32. package/templates/{core → greenfield}/scaffolding/_conventions.md +2 -1
  33. package/templates/{core → greenfield}/scaffolding/_overview.md +2 -2
  34. package/templates/{core → greenfield}/scaffolding/architecture-decisions-README.md +1 -1
  35. package/templates/{core → greenfield}/templates/context-map.md +1 -1
  36. package/templates/{core → greenfield}/templates/events.md +1 -1
  37. package/templates/{core → greenfield}/templates/glossary.md +1 -1
  38. package/templates/{core → greenfield}/templates/models.md +1 -1
  39. package/templates/{core → greenfield}/templates/phase-spec.md +1 -1
  40. package/templates/{core → greenfield}/templates/rules.md +1 -1
  41. package/templates/{core → greenfield}/templates/tech-debt.md +1 -1
  42. /package/templates/{webforms → brownfield}/templates/CLAUDE.md +0 -0
  43. /package/templates/{webforms → brownfield}/templates/_index.md +0 -0
  44. /package/templates/{webforms → brownfield}/templates/behavior.md +0 -0
  45. /package/templates/{webforms → brownfield}/templates/context-definition.md +0 -0
  46. /package/templates/{webforms → brownfield}/templates/lightweight-spec.md +0 -0
  47. /package/templates/{core → greenfield}/templates/CLAUDE.md +0 -0
  48. /package/templates/{core → greenfield}/templates/_index.md +0 -0
  49. /package/templates/{core → greenfield}/templates/aggregate-design.md +0 -0
  50. /package/templates/{core → greenfield}/templates/behavior.md +0 -0
  51. /package/templates/{core → greenfield}/templates/context-definition.md +0 -0
  52. /package/templates/{core → greenfield}/templates/lightweight-spec.md +0 -0
package/lib/init.js CHANGED
@@ -3,6 +3,8 @@ const path = require('node:path');
3
3
  const readline = require('node:readline');
4
4
  const { TextDecoder } = require('node:util');
5
5
 
6
+ const pkg = require('../package.json');
7
+
6
8
  const MIN_NODE_VERSION = '22.0.0';
7
9
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
8
10
  const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, 'templates');
@@ -37,19 +39,6 @@ const PROJECT_TYPE_OPTIONS = [
37
39
  }
38
40
  ];
39
41
 
40
- const EDITION_OPTIONS = [
41
- {
42
- key: 'core',
43
- label: 'ASP.NET Core - Clean Architecture + DDD',
44
- aliases: ['core', 'asp.net core', 'aspnet core']
45
- },
46
- {
47
- key: 'webforms',
48
- label: 'ASP.NET WebForms - progressive domain extraction',
49
- aliases: ['webforms', 'asp.net webforms', 'aspnet webforms']
50
- }
51
- ];
52
-
53
42
  const PROSE_LANGUAGE_OPTIONS = [
54
43
  {
55
44
  key: 'zh-TW',
@@ -88,11 +77,29 @@ const OPTIONAL_FILE_OPTIONS = [
88
77
  key: 'git-flow',
89
78
  label: 'Git principles - Git Flow',
90
79
  aliases: ['git principles - git flow', 'git flow', 'gitflow']
80
+ }
81
+ ];
82
+
83
+ const AI_AGENT_OPTIONS = [
84
+ {
85
+ key: 'agents',
86
+ label: 'AGENTS.md - Codex / Copilot coding agent',
87
+ aliases: ['agents', 'agents.md', 'codex', 'copilot agent', 'copilot coding agent']
91
88
  },
92
89
  {
93
90
  key: 'claude',
94
- label: 'CLAUDE.md snippet / project AI guide',
91
+ label: 'CLAUDE.md - Claude Code',
95
92
  aliases: ['claude', 'claude.md']
93
+ },
94
+ {
95
+ key: 'gemini',
96
+ label: 'GEMINI.md - Gemini CLI',
97
+ aliases: ['gemini', 'gemini.md']
98
+ },
99
+ {
100
+ key: 'copilot',
101
+ label: '.github/copilot-instructions.md - GitHub Copilot',
102
+ aliases: ['copilot', 'github copilot', 'copilot-instructions', 'copilot-instructions.md']
96
103
  }
97
104
  ];
98
105
 
@@ -211,6 +218,95 @@ async function runInit(options = {}) {
211
218
  }
212
219
  }
213
220
 
221
+ async function runConfigureAgents(options = {}) {
222
+ const cwd = path.resolve(options.cwd || process.cwd());
223
+ const stdin = options.stdin || process.stdin;
224
+ const stdout = options.stdout || process.stdout;
225
+ const stderr = options.stderr || process.stderr;
226
+
227
+ let rl;
228
+
229
+ try {
230
+ rl = readline.createInterface({
231
+ input: stdin,
232
+ output: stdout,
233
+ terminal: Boolean(stdin.isTTY && stdout.isTTY)
234
+ });
235
+ rl._dflowOutput = stdout;
236
+ getLinePrompter(rl);
237
+
238
+ if (compareVersions(process.versions.node, MIN_NODE_VERSION) < 0) {
239
+ throw new InitError(`Dflow configure-agents requires Node.js ${MIN_NODE_VERSION}+.`, 1);
240
+ }
241
+
242
+ await assertWritableProjectRoot(cwd);
243
+ await assertDflowInitialized(cwd);
244
+
245
+ const projectContext = await inferProjectContext(cwd, rl, stdout, stderr);
246
+ const aiAgents = await askAiAgents(rl, stdout, stderr);
247
+
248
+ if (aiAgents.length === 0) {
249
+ throw new UserAbort('No AI agents selected. Nothing changed.');
250
+ }
251
+
252
+ const plan = await buildConfigureAgentsPlan(cwd, {
253
+ ...projectContext,
254
+ aiAgents
255
+ });
256
+
257
+ renderPreview(stdout, plan, []);
258
+ const confirmed = await askConfirmation(rl, 'Create these files? (y/N) ');
259
+
260
+ if (!confirmed) {
261
+ throw new UserAbort();
262
+ }
263
+
264
+ rl.close();
265
+ rl = undefined;
266
+
267
+ const result = await writeFilePlan(cwd, plan);
268
+ result.warnings.push(...collectUnresolvedPlaceholderWarnings(plan, result.created));
269
+
270
+ printResultReport(stdout, result, plan.deferred);
271
+ printConfigureAgentsNextSteps(stdout);
272
+ return 0;
273
+ } catch (error) {
274
+ if (rl) {
275
+ rl.close();
276
+ }
277
+
278
+ if (error instanceof UserAbort) {
279
+ stdout.write(`${error.message}\n`);
280
+ return error.exitCode;
281
+ }
282
+
283
+ if (error instanceof WritePhaseError) {
284
+ stderr.write(`${error.message}\n`);
285
+ stderr.write('Files already created were kept; clean up partial output manually if needed.\n');
286
+ if (error.result) {
287
+ printResultReport(stdout, error.result, []);
288
+ }
289
+ return error.exitCode;
290
+ }
291
+
292
+ if (error instanceof InitError) {
293
+ stderr.write(`${error.message}\n`);
294
+ return error.exitCode;
295
+ }
296
+
297
+ stderr.write(`${error && error.message ? error.message : error}\n`);
298
+ return 1;
299
+ }
300
+ }
301
+
302
+ async function assertDflowInitialized(cwd) {
303
+ const dflowSpecsPath = path.join(cwd, 'dflow', 'specs');
304
+
305
+ if (!(await pathExists(dflowSpecsPath)) || !(await containsInitializedContent(dflowSpecsPath))) {
306
+ throw new InitError('Dflow is not initialized in this project. Run `dflow init` first.');
307
+ }
308
+ }
309
+
214
310
  async function runPreflight(cwd) {
215
311
  const warnings = [];
216
312
  const dflowSpecsPath = path.join(cwd, 'dflow', 'specs');
@@ -226,7 +322,7 @@ async function runPreflight(cwd) {
226
322
  const legacySpecsPath = path.join(cwd, 'specs');
227
323
  if ((await pathExists(legacySpecsPath)) && (await containsInitializedContent(legacySpecsPath))) {
228
324
  warnings.push(
229
- 'Detected legacy specs/. Dflow V1 will not migrate or modify it; new files will be created under dflow/specs/.'
325
+ 'Detected legacy specs/. Dflow V1 will not migrate or modify it; new files will be created under dflow/specs/. See docs/migrating-to-dflow-v1.md for the manual migration checklist.'
230
326
  );
231
327
  }
232
328
 
@@ -309,16 +405,16 @@ async function detectProjectSignals(cwd) {
309
405
  }
310
406
  }
311
407
 
312
- let editionHint = null;
408
+ let trackHint = null;
313
409
  if (coreSignal && !webFormsSignal) {
314
- editionHint = 'core';
410
+ trackHint = 'greenfield';
315
411
  } else if (webFormsSignal && !coreSignal) {
316
- editionHint = 'webforms';
412
+ trackHint = 'brownfield';
317
413
  }
318
414
 
319
415
  return {
320
416
  hasSourceTree: hasSourceTree || relNames.has('src'),
321
- editionHint
417
+ trackHint
322
418
  };
323
419
  }
324
420
 
@@ -369,9 +465,9 @@ function buildDetectionWarnings(answers, detection) {
369
465
  warnings.push('Warning: source-tree signals already exist, but project type is Greenfield. Continuing with your selected project type.');
370
466
  }
371
467
 
372
- if (detection.editionHint && answers.edition !== detection.editionHint) {
468
+ if (detection.trackHint && answers.projectType !== detection.trackHint) {
373
469
  warnings.push(
374
- `Warning: project signals look like ${formatEdition(detection.editionHint)}, but selected edition is ${formatEdition(answers.edition)}. Continuing with your selected edition.`
470
+ `Warning: project signals look like ${formatTrack(detection.trackHint)}, but selected track is ${formatTrack(answers.projectType)}. Continuing with your selected track.`
375
471
  );
376
472
  }
377
473
 
@@ -387,15 +483,8 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
387
483
  defaultKey: projectTypeDefault
388
484
  });
389
485
 
390
- const edition = await askSelect(rl, stdout, stderr, {
391
- id: 'Q2',
392
- question: 'Which Dflow edition should initialize this project?',
393
- options: EDITION_OPTIONS,
394
- defaultKey: detection.editionHint
395
- });
396
-
397
486
  const techStackSummary = await askText(rl, stderr, {
398
- id: 'Q3',
487
+ id: 'Q2',
399
488
  question: 'Confirm the main tech stack details for placeholders.',
400
489
  required: true,
401
490
  maxLength: 1000,
@@ -403,7 +492,7 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
403
492
  });
404
493
 
405
494
  const migrationContext = await askText(rl, stderr, {
406
- id: 'Q4',
495
+ id: 'Q3',
407
496
  question: 'Is there migration or legacy context Dflow should note?',
408
497
  required: false,
409
498
  maxLength: 1000,
@@ -411,7 +500,7 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
411
500
  });
412
501
 
413
502
  const proseLanguageSelection = await askSelect(rl, stdout, stderr, {
414
- id: 'Q5',
503
+ id: 'Q4',
415
504
  question: 'Project prose language for generated spec content?',
416
505
  options: PROSE_LANGUAGE_OPTIONS,
417
506
  defaultKey: null
@@ -423,17 +512,76 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
423
512
  }
424
513
 
425
514
  const optionalFiles = await askOptionalFiles(rl, stdout, stderr);
515
+ const aiAgents = await askAiAgents(rl, stdout, stderr);
426
516
 
427
517
  return {
428
518
  projectType,
429
- edition,
519
+ edition: projectType,
430
520
  techStackSummary,
431
521
  migrationContext,
432
522
  proseLanguage,
433
- optionalFiles
523
+ optionalFiles,
524
+ aiAgents
525
+ };
526
+ }
527
+
528
+ async function inferProjectContext(cwd, rl, stdout, stderr) {
529
+ let edition = await inferExistingEdition(cwd);
530
+
531
+ if (!edition) {
532
+ stderr.write('Could not infer the Dflow track from dflow/specs/. Please choose it explicitly.\n');
533
+ edition = await askSelect(rl, stdout, stderr, {
534
+ id: 'track',
535
+ question: 'Which Dflow track is this project using?',
536
+ options: PROJECT_TYPE_OPTIONS,
537
+ defaultKey: null
538
+ });
539
+ }
540
+
541
+ return {
542
+ projectType: edition,
543
+ edition,
544
+ techStackSummary: await inferTechStackSummary(cwd),
545
+ migrationContext: await inferMigrationContext(cwd),
546
+ proseLanguage: await inferProseLanguage(cwd),
547
+ optionalFiles: []
434
548
  };
435
549
  }
436
550
 
551
+ async function inferExistingEdition(cwd) {
552
+ if (await pathExists(path.join(cwd, 'dflow/specs/architecture/tech-debt.md'))) {
553
+ return 'greenfield';
554
+ }
555
+ if (await pathExists(path.join(cwd, 'dflow/specs/migration/tech-debt.md'))) {
556
+ return 'brownfield';
557
+ }
558
+ if (await pathExists(path.join(cwd, 'dflow/specs/domain/context-map.md'))) {
559
+ return 'greenfield';
560
+ }
561
+ return null;
562
+ }
563
+
564
+ async function inferProseLanguage(cwd) {
565
+ const conventionsPath = path.join(cwd, 'dflow/specs/shared/_conventions.md');
566
+ const content = await fs.readFile(conventionsPath, 'utf8').catch(() => '');
567
+ const match = content.match(/Project prose language:\s*`([^`]+)`/);
568
+ return match ? match[1] : 'unknown';
569
+ }
570
+
571
+ async function inferTechStackSummary(cwd) {
572
+ const overviewPath = path.join(cwd, 'dflow/specs/shared/_overview.md');
573
+ const content = await fs.readFile(overviewPath, 'utf8').catch(() => '');
574
+ const match = content.match(/\|\s*Tech stack\s*\|\s*([^|\n]+?)\s*\|/i);
575
+ return match ? match[1].trim() : 'unknown';
576
+ }
577
+
578
+ async function inferMigrationContext(cwd) {
579
+ const overviewPath = path.join(cwd, 'dflow/specs/shared/_overview.md');
580
+ const content = await fs.readFile(overviewPath, 'utf8').catch(() => '');
581
+ const match = content.match(/\|\s*Migration \/ legacy context\s*\|\s*([^|\n]+?)\s*\|/i);
582
+ return match ? match[1].trim() : 'none';
583
+ }
584
+
437
585
  async function askSelect(rl, stdout, stderr, config) {
438
586
  let failedAttempts = 0;
439
587
 
@@ -506,7 +654,7 @@ async function askCustomProseLanguage(rl, stderr) {
506
654
 
507
655
  failedAttempts += 1;
508
656
  if (failedAttempts >= 3) {
509
- throw new InitError('Too many invalid attempts for Q5a. Dflow init aborted.');
657
+ throw new InitError('Too many invalid attempts for Q4a. Dflow init aborted.');
510
658
  }
511
659
  stderr.write(`${validation.message} (${3 - failedAttempts} attempts left)\n`);
512
660
  }
@@ -528,7 +676,7 @@ async function askOptionalFiles(rl, stdout, stderr) {
528
676
  if (!parsed.valid) {
529
677
  failedAttempts += 1;
530
678
  if (failedAttempts >= 3) {
531
- throw new InitError('Too many invalid attempts for Q6. Dflow init aborted.');
679
+ throw new InitError('Too many invalid attempts for Q5. Dflow init aborted.');
532
680
  }
533
681
  stderr.write(`${parsed.message} (${3 - failedAttempts} attempts left)\n`);
534
682
  continue;
@@ -549,6 +697,31 @@ async function askOptionalFiles(rl, stdout, stderr) {
549
697
  }
550
698
  }
551
699
 
700
+ async function askAiAgents(rl, stdout, stderr) {
701
+ let failedAttempts = 0;
702
+
703
+ while (true) {
704
+ stdout.write('\nWhich AI coding agents should Dflow configure?\n');
705
+ AI_AGENT_OPTIONS.forEach((option, index) => {
706
+ stdout.write(` ${index + 1}. ${option.label}\n`);
707
+ });
708
+
709
+ const answer = await askLine(rl, 'Enter comma-separated choices or "none" (default: none): ');
710
+ const parsed = parseMultiselectAnswer(answer || 'none', AI_AGENT_OPTIONS, []);
711
+
712
+ if (!parsed.valid) {
713
+ failedAttempts += 1;
714
+ if (failedAttempts >= 3) {
715
+ throw new InitError('Too many invalid attempts for Q6. Dflow init aborted.');
716
+ }
717
+ stderr.write(`${parsed.message} (${3 - failedAttempts} attempts left)\n`);
718
+ continue;
719
+ }
720
+
721
+ return parsed.values;
722
+ }
723
+ }
724
+
552
725
  function askLine(rl, prompt) {
553
726
  return getLinePrompter(rl).ask(prompt);
554
727
  }
@@ -669,7 +842,7 @@ function parseMultiselectAnswer(answer, options, defaultKeys) {
669
842
  for (const token of tokens) {
670
843
  const selectedKey = parseSelectAnswer(token, options, null);
671
844
  if (!selectedKey) {
672
- return { valid: false, message: `Invalid optional file selection: ${token}` };
845
+ return { valid: false, message: `Invalid selection: ${token}` };
673
846
  }
674
847
  if (!selected.includes(selectedKey)) {
675
848
  selected.push(selectedKey);
@@ -713,9 +886,6 @@ async function buildFilePlan(cwd, answers) {
713
886
 
714
887
  const addTemplate = async (relativePath, sourceRel, notes, options = {}) => {
715
888
  let content = await readPackagedTemplate(answers.edition, sourceRel);
716
- if (options.extractClaudeSnippetBody) {
717
- content = extractClaudeSnippetBody(content, sourceRel);
718
- }
719
889
  content = substitutePlaceholders(content, substitution);
720
890
  if (options.injectProseLanguage) {
721
891
  content = ensureProseLanguageSection(content, answers.proseLanguage);
@@ -737,7 +907,7 @@ async function buildFilePlan(cwd, answers) {
737
907
  });
738
908
  await addTemplate('dflow/specs/domain/glossary.md', 'templates/glossary.md', 'mandatory');
739
909
 
740
- if (answers.edition === 'core') {
910
+ if (answers.edition === 'greenfield') {
741
911
  await addTemplate('dflow/specs/domain/context-map.md', 'templates/context-map.md', 'mandatory');
742
912
  await addTemplate('dflow/specs/architecture/tech-debt.md', 'templates/tech-debt.md', 'mandatory');
743
913
  await addTemplate(
@@ -758,17 +928,57 @@ async function buildFilePlan(cwd, answers) {
758
928
  if (answers.optionalFiles.includes('git-flow')) {
759
929
  await addTemplate('dflow/specs/shared/Git-principles-gitflow.md', 'scaffolding/Git-principles-gitflow.md', 'selected');
760
930
  }
761
- if (answers.optionalFiles.includes('claude')) {
762
- const rootClaudePath = path.join(cwd, 'CLAUDE.md');
763
- if (await pathExists(rootClaudePath)) {
764
- await addTemplate('dflow/specs/shared/CLAUDE-md-snippet.md', 'scaffolding/CLAUDE-md-snippet.md', 'selected, root CLAUDE.md already exists');
765
- } else {
766
- await addTemplate('CLAUDE.md', 'scaffolding/CLAUDE-md-snippet.md', 'selected, snippet body only', {
767
- extractClaudeSnippetBody: true
768
- });
931
+
932
+ if (answers.aiAgents.length > 0) {
933
+ await addTemplate('dflow/specs/shared/AI-AGENT-GUIDE.md', 'scaffolding/AI-AGENT-GUIDE.md', 'selected, canonical AI agent guide');
934
+ for (const agent of answers.aiAgents) {
935
+ await addAiAgentShim(cwd, items, agent, substitution);
769
936
  }
770
937
  }
771
938
 
939
+ await finalizePlanItems(cwd, items);
940
+
941
+ return {
942
+ items,
943
+ deferred: buildDeferredItems(answers.edition),
944
+ unresolvedInitPlaceholders: Array.from(substitution.entries())
945
+ .filter(([placeholder, value]) => placeholder === value)
946
+ .map(([placeholder]) => placeholder)
947
+ };
948
+ }
949
+
950
+ async function buildConfigureAgentsPlan(cwd, answers) {
951
+ const substitution = buildSubstitutionMap(cwd, {
952
+ ...answers,
953
+ optionalFiles: answers.optionalFiles || []
954
+ });
955
+ const items = [];
956
+
957
+ let content = await readPackagedTemplate(answers.edition, 'scaffolding/AI-AGENT-GUIDE.md');
958
+ content = substitutePlaceholders(content, substitution);
959
+ items.push({
960
+ relativePath: 'dflow/specs/shared/AI-AGENT-GUIDE.md',
961
+ source: `packaged:${answers.edition}/scaffolding/AI-AGENT-GUIDE.md`,
962
+ notes: 'canonical AI agent guide',
963
+ content
964
+ });
965
+
966
+ for (const agent of answers.aiAgents) {
967
+ await addAiAgentShim(cwd, items, agent, substitution);
968
+ }
969
+
970
+ await finalizePlanItems(cwd, items);
971
+
972
+ return {
973
+ items,
974
+ deferred: [],
975
+ unresolvedInitPlaceholders: Array.from(substitution.entries())
976
+ .filter(([placeholder, value]) => placeholder === value)
977
+ .map(([placeholder]) => placeholder)
978
+ };
979
+ }
980
+
981
+ async function finalizePlanItems(cwd, items) {
772
982
  for (const item of items) {
773
983
  const absolute = path.join(cwd, item.relativePath);
774
984
  item.action = (await pathExists(absolute)) ? 'skip' : 'create';
@@ -777,22 +987,92 @@ async function buildFilePlan(cwd, answers) {
777
987
  }
778
988
  item.size = Buffer.byteLength(item.content, 'utf8');
779
989
  }
990
+ }
780
991
 
781
- return {
782
- items,
783
- deferred: buildDeferredItems(answers.edition),
784
- unresolvedInitPlaceholders: Array.from(substitution.entries())
785
- .filter(([placeholder, value]) => placeholder === value)
786
- .map(([placeholder]) => placeholder)
992
+ async function addAiAgentShim(cwd, items, agent, substitution) {
993
+ const target = getAiAgentTarget(agent);
994
+ const targetPath = path.join(cwd, target.relativePath);
995
+ const targetExists = await pathExists(targetPath);
996
+ const targetConfigured = targetExists && await fileReferencesAiAgentGuide(targetPath);
997
+ const relativePath = targetExists && !targetConfigured ? target.snippetPath : target.relativePath;
998
+ const content = substitutePlaceholders(buildAiAgentShim(target.relativePath), substitution);
999
+ let notes = 'selected, tool-specific shim';
1000
+ if (targetConfigured) {
1001
+ notes = `selected, ${target.relativePath} already points to AI-AGENT-GUIDE.md`;
1002
+ } else if (targetExists) {
1003
+ notes = `selected, ${target.relativePath} already exists; merge this snippet manually`;
1004
+ }
1005
+
1006
+ items.push({
1007
+ relativePath,
1008
+ source: `generated:${agent}-shim`,
1009
+ notes,
1010
+ content
1011
+ });
1012
+ }
1013
+
1014
+ async function fileReferencesAiAgentGuide(targetPath) {
1015
+ try {
1016
+ const content = await fs.readFile(targetPath, 'utf8');
1017
+ return content.includes('dflow/specs/shared/AI-AGENT-GUIDE.md') ||
1018
+ content.includes('dflow\\specs\\shared\\AI-AGENT-GUIDE.md');
1019
+ } catch {
1020
+ return false;
1021
+ }
1022
+ }
1023
+
1024
+ function getAiAgentTarget(agent) {
1025
+ const targets = {
1026
+ agents: {
1027
+ relativePath: 'AGENTS.md',
1028
+ snippetPath: 'dflow/specs/shared/AGENTS-md-snippet.md'
1029
+ },
1030
+ claude: {
1031
+ relativePath: 'CLAUDE.md',
1032
+ snippetPath: 'dflow/specs/shared/CLAUDE-md-snippet.md'
1033
+ },
1034
+ gemini: {
1035
+ relativePath: 'GEMINI.md',
1036
+ snippetPath: 'dflow/specs/shared/GEMINI-md-snippet.md'
1037
+ },
1038
+ copilot: {
1039
+ relativePath: '.github/copilot-instructions.md',
1040
+ snippetPath: 'dflow/specs/shared/copilot-instructions-snippet.md'
1041
+ }
787
1042
  };
1043
+
1044
+ return targets[agent];
1045
+ }
1046
+
1047
+ function buildAiAgentShim(targetPath) {
1048
+ const title = targetPath === '.github/copilot-instructions.md'
1049
+ ? 'GitHub Copilot Repository Instructions'
1050
+ : `${targetPath} - Dflow Project Instructions`;
1051
+
1052
+ const importHint = targetPath === 'CLAUDE.md' || targetPath === 'GEMINI.md'
1053
+ ? '\nIf your tool supports Markdown imports, the canonical guide is imported below:\n\n@dflow/specs/shared/AI-AGENT-GUIDE.md\n'
1054
+ : '';
1055
+
1056
+ return `# ${title}
1057
+
1058
+ This project uses Dflow for spec-first AI-assisted development.
1059
+
1060
+ Before planning or editing code, read and follow:
1061
+
1062
+ - \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
1063
+
1064
+ Keep tool-specific instruction files small. The Dflow guide above is the
1065
+ single source of truth for project workflow rules, slash-command behavior,
1066
+ spec locations, and SDD/DDD constraints.${importHint}
1067
+ `;
788
1068
  }
789
1069
 
790
1070
  function buildDeferredItems(edition) {
791
1071
  const deferred = [...DEFERRED_COMMON];
792
- if (edition === 'core') {
1072
+ if (edition === 'greenfield') {
793
1073
  deferred.splice(3, 0, {
794
1074
  relativePath: 'dflow/specs/domain/{context}/events.md',
795
- reason: 'Core only, but still needs a real bounded context.'
1075
+ reason: 'Greenfield only, but still needs a real bounded context.'
796
1076
  });
797
1077
  }
798
1078
  return deferred;
@@ -839,6 +1119,7 @@ function buildSubstitutionMap(cwd, answers) {
839
1119
  ['{tech-stack-summary}', answers.techStackSummary],
840
1120
  ['{migration-context}', answers.migrationContext],
841
1121
  ['{prose-language}', answers.proseLanguage],
1122
+ ['{dflow-version}', pkg.version],
842
1123
  ['{ASP.NET Core version}', extracted.aspNetCoreVersion || '{ASP.NET Core version}'],
843
1124
  ['{EF Core version}', extracted.efCoreVersion || '{EF Core version}'],
844
1125
  ['{MediatR version}', extracted.mediatRVersion || '{MediatR version}'],
@@ -892,14 +1173,6 @@ function extractTestFramework(text) {
892
1173
  return null;
893
1174
  }
894
1175
 
895
- function extractClaudeSnippetBody(content, sourceRel) {
896
- const match = content.match(/## Snippet to merge into `CLAUDE\.md`[\s\S]*?```markdown\r?\n([\s\S]*?)\r?\n```/);
897
- if (!match) {
898
- throw new InitError(`Internal error: CLAUDE.md snippet body not found in packaged template: ${sourceRel}`);
899
- }
900
- return `${match[1].trimEnd()}\n`;
901
- }
902
-
903
1176
  function ensureProseLanguageSection(content, proseLanguage) {
904
1177
  const section = buildProseLanguageSection(proseLanguage);
905
1178
  let stripped = stripProseLanguageSections(content);
@@ -1116,6 +1389,17 @@ Recommended next steps:
1116
1389
  `);
1117
1390
  }
1118
1391
 
1392
+ function printConfigureAgentsNextSteps(stdout) {
1393
+ stdout.write(`
1394
+ Dflow AI agent configuration complete.
1395
+
1396
+ Recommended next steps:
1397
+ - Keep AI-agent-specific root files small.
1398
+ - Put durable workflow changes in dflow/specs/shared/AI-AGENT-GUIDE.md.
1399
+ - If a merge snippet was created, review it and merge the pointer into the existing tool instruction file.
1400
+ `);
1401
+ }
1402
+
1119
1403
  function printList(stdout, values) {
1120
1404
  if (!values || values.length === 0) {
1121
1405
  stdout.write('- (none)\n');
@@ -1186,8 +1470,8 @@ function formatBytes(bytes) {
1186
1470
  return `${(bytes / 1024).toFixed(1)} KB`;
1187
1471
  }
1188
1472
 
1189
- function formatEdition(edition) {
1190
- return edition === 'core' ? 'ASP.NET Core' : 'ASP.NET WebForms';
1473
+ function formatTrack(track) {
1474
+ return track === 'greenfield' ? 'Greenfield' : 'Brownfield';
1191
1475
  }
1192
1476
 
1193
1477
  function escapeTableCell(value) {
@@ -1198,7 +1482,101 @@ function dedupe(values) {
1198
1482
  return Array.from(new Set(values));
1199
1483
  }
1200
1484
 
1485
+ async function runDoctor(options = {}) {
1486
+ const cwd = path.resolve(options.cwd || process.cwd());
1487
+ const stdout = options.stdout || process.stdout;
1488
+ const stderr = options.stderr || process.stderr;
1489
+
1490
+ try {
1491
+ if (compareVersions(process.versions.node, MIN_NODE_VERSION) < 0) {
1492
+ throw new InitError(`Dflow doctor requires Node.js ${MIN_NODE_VERSION}+.`, 1);
1493
+ }
1494
+
1495
+ const findings = [];
1496
+ await checkLegacyRootSpecsDir(cwd, findings);
1497
+ await checkLegacySharedDir(cwd, findings);
1498
+ await checkConventionsDflowVersion(cwd, findings);
1499
+
1500
+ printDoctorReport(stdout, cwd, findings);
1501
+ return 0;
1502
+ } catch (error) {
1503
+ if (error instanceof InitError) {
1504
+ stderr.write(`${error.message}\n`);
1505
+ return error.exitCode;
1506
+ }
1507
+ stderr.write(`${error && error.message ? error.message : error}\n`);
1508
+ return 1;
1509
+ }
1510
+ }
1511
+
1512
+ async function checkLegacyRootSpecsDir(cwd, findings) {
1513
+ const legacyPath = path.join(cwd, 'specs');
1514
+ if ((await pathExists(legacyPath)) && (await containsInitializedContent(legacyPath))) {
1515
+ findings.push({
1516
+ level: 'warn',
1517
+ title: 'Legacy specs/ directory at project root',
1518
+ detail: 'V1 layout uses dflow/specs/ instead. The CLI does not modify root specs/.',
1519
+ action: 'See docs/migrating-to-dflow-v1.md (Step 1) for the manual migration steps.'
1520
+ });
1521
+ }
1522
+ }
1523
+
1524
+ async function checkLegacySharedDir(cwd, findings) {
1525
+ const candidates = [
1526
+ path.join(cwd, 'dflow', 'specs', '_共用'),
1527
+ path.join(cwd, 'specs', '_共用')
1528
+ ];
1529
+ for (const candidate of candidates) {
1530
+ if (await pathExists(candidate)) {
1531
+ const rel = normalizePath(path.relative(cwd, candidate));
1532
+ findings.push({
1533
+ level: 'warn',
1534
+ title: `Legacy ${rel}/ directory`,
1535
+ detail: 'V1 layout uses shared/ (canonical English directory name).',
1536
+ action: 'See docs/migrating-to-dflow-v1.md (Step 2) for the rename steps.'
1537
+ });
1538
+ }
1539
+ }
1540
+ }
1541
+
1542
+ async function checkConventionsDflowVersion(cwd, findings) {
1543
+ const conventionsPath = path.join(cwd, 'dflow', 'specs', 'shared', '_conventions.md');
1544
+ if (!(await pathExists(conventionsPath))) return;
1545
+ const content = await fs.readFile(conventionsPath, 'utf8').catch(() => '');
1546
+ if (!/^> Dflow Version:/m.test(content)) {
1547
+ findings.push({
1548
+ level: 'info',
1549
+ title: 'dflow/specs/shared/_conventions.md missing Dflow Version line',
1550
+ detail: 'V1 init writes a `> Dflow Version: <x.y.z>` line in the front matter automatically. This project predates that convention.',
1551
+ action: 'Optionally add the line manually so future migration / review can identify the spec convention version.'
1552
+ });
1553
+ }
1554
+ }
1555
+
1556
+ function printDoctorReport(stdout, cwd, findings) {
1557
+ stdout.write(`Dflow Doctor ${pkg.version}\n`);
1558
+ stdout.write(`Project: ${cwd}\n\n`);
1559
+
1560
+ if (findings.length === 0) {
1561
+ stdout.write('All checks passed. No legacy artifacts detected.\n');
1562
+ return;
1563
+ }
1564
+
1565
+ for (const finding of findings) {
1566
+ stdout.write(`[${finding.level}] ${finding.title}\n`);
1567
+ stdout.write(` ${finding.detail}\n`);
1568
+ stdout.write(` ${finding.action}\n\n`);
1569
+ }
1570
+
1571
+ const counts = { warn: 0, info: 0 };
1572
+ for (const f of findings) counts[f.level] = (counts[f.level] || 0) + 1;
1573
+ stdout.write(`${findings.length} finding(s): ${counts.warn} warn, ${counts.info} info.\n`);
1574
+ stdout.write('Doctor is read-only and does not modify any files.\n');
1575
+ }
1576
+
1201
1577
  module.exports = {
1578
+ runConfigureAgents,
1579
+ runDoctor,
1202
1580
  runInit,
1203
1581
  validateProseLanguage,
1204
1582
  ensureProseLanguageSection,