dflow-sdd-ddd 0.7.0 → 0.9.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 (59) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/LICENSE +679 -21
  3. package/README.en.md +5 -4
  4. package/README.md +3 -3
  5. package/bin/dflow.js +3 -2
  6. package/docs/evaluating-dflow.en.md +14 -5
  7. package/docs/evaluating-dflow.md +14 -5
  8. package/docs/using-with-claude-code.en.md +17 -9
  9. package/docs/using-with-claude-code.md +15 -8
  10. package/docs/using-with-codex.en.md +12 -8
  11. package/docs/using-with-codex.md +8 -6
  12. package/lib/init.js +480 -87
  13. package/package.json +2 -2
  14. package/templates/brownfield/references/dflow-feedback-flow.md +251 -0
  15. package/templates/brownfield/references/drift-verification.md +183 -0
  16. package/templates/brownfield/references/finish-feature-flow.md +294 -0
  17. package/templates/brownfield/references/git-integration.md +371 -0
  18. package/templates/brownfield/references/init-project-flow.md +430 -0
  19. package/templates/brownfield/references/modify-existing-flow.md +448 -0
  20. package/templates/brownfield/references/new-feature-flow.md +382 -0
  21. package/templates/brownfield/references/new-phase-flow.md +274 -0
  22. package/templates/brownfield/references/pr-review-checklist.md +179 -0
  23. package/templates/brownfield/scaffolding/AI-AGENT-GUIDE.md +31 -4
  24. package/templates/brownfield/scaffolding/CLAUDE-md-snippet.md +12 -8
  25. package/templates/brownfield/scaffolding/Git-principles-gitflow.md +14 -13
  26. package/templates/brownfield/scaffolding/Git-principles-trunk.md +14 -17
  27. package/templates/brownfield/scaffolding/_conventions.md +1 -1
  28. package/templates/brownfield/scaffolding/_overview.md +3 -3
  29. package/templates/brownfield/templates/_index.md +20 -2
  30. package/templates/brownfield/templates/context-map.md +1 -1
  31. package/templates/brownfield/templates/glossary.md +1 -1
  32. package/templates/brownfield/templates/models.md +1 -1
  33. package/templates/brownfield/templates/rules.md +1 -1
  34. package/templates/brownfield/templates/tech-debt.md +1 -1
  35. package/templates/common/skill/SKILL.md +35 -0
  36. package/templates/greenfield/references/ddd-modeling-guide.md +351 -0
  37. package/templates/greenfield/references/dflow-feedback-flow.md +251 -0
  38. package/templates/greenfield/references/drift-verification.md +195 -0
  39. package/templates/greenfield/references/finish-feature-flow.md +314 -0
  40. package/templates/greenfield/references/git-integration.md +344 -0
  41. package/templates/greenfield/references/init-project-flow.md +464 -0
  42. package/templates/greenfield/references/modify-existing-flow.md +366 -0
  43. package/templates/greenfield/references/new-feature-flow.md +412 -0
  44. package/templates/greenfield/references/new-phase-flow.md +288 -0
  45. package/templates/greenfield/references/pr-review-checklist.md +130 -0
  46. package/templates/greenfield/scaffolding/AI-AGENT-GUIDE.md +31 -4
  47. package/templates/greenfield/scaffolding/CLAUDE-md-snippet.md +15 -13
  48. package/templates/greenfield/scaffolding/Git-principles-gitflow.md +14 -13
  49. package/templates/greenfield/scaffolding/Git-principles-trunk.md +14 -18
  50. package/templates/greenfield/scaffolding/_conventions.md +1 -1
  51. package/templates/greenfield/scaffolding/_overview.md +5 -3
  52. package/templates/greenfield/scaffolding/architecture-decisions-README.md +1 -1
  53. package/templates/greenfield/templates/_index.md +20 -2
  54. package/templates/greenfield/templates/context-map.md +1 -1
  55. package/templates/greenfield/templates/events.md +1 -1
  56. package/templates/greenfield/templates/glossary.md +1 -1
  57. package/templates/greenfield/templates/models.md +1 -1
  58. package/templates/greenfield/templates/rules.md +1 -1
  59. package/templates/greenfield/templates/tech-debt.md +1 -1
package/lib/init.js CHANGED
@@ -12,6 +12,12 @@ const COMMAND_REGISTRY_START = '<!-- dflow-command-registry:start -->';
12
12
  const COMMAND_REGISTRY_END = '<!-- dflow-command-registry:end -->';
13
13
  const COMMAND_ADAPTER_GENERATED_MARKER = '<!-- dflow-generated: command-adapter -->';
14
14
  const SKILL_ADAPTER_GENERATED_MARKER = '<!-- dflow-generated: skill-adapter -->';
15
+ const WORKFLOW_BUNDLE_GENERATED_MARKER = '<!-- dflow-generated: workflow-bundle -->';
16
+ const CODEX_TRIGGER_SECTION_START = '<!-- dflow-generated: codex-command-triggers START -->';
17
+ const CODEX_TRIGGER_SECTION_END = '<!-- dflow-generated: codex-command-triggers END -->';
18
+ const WORKFLOW_BUNDLE_DEST = 'dflow/specs/shared/dflow-workflows';
19
+ const WORKFLOW_BUNDLE_MANIFEST_PATH = `${WORKFLOW_BUNDLE_DEST}/.dflow-bundle-manifest.json`;
20
+ const COMMON_SKILL_SOURCE_REL = 'common/skill/SKILL.md';
15
21
  const EXPECTED_COMMAND_IDS = [
16
22
  'new-feature',
17
23
  'modify-existing',
@@ -106,16 +112,42 @@ const OPTIONAL_FILE_OPTIONS = [
106
112
  key: 'overview',
107
113
  label: '_overview.md - system overview',
108
114
  aliases: ['overview', '_overview.md']
115
+ }
116
+ ];
117
+
118
+ // Git policy is a mandatory team choice (PROPOSAL-047): both options use feature
119
+ // branches; the choice selects the finish-stage merge guidance and drives the
120
+ // runtime branch gates / commit checkpoints.
121
+ const GIT_POLICY_OPTIONS = [
122
+ {
123
+ key: 'gitflow',
124
+ label: 'Git Flow - long-lived develop/release branches',
125
+ aliases: ['gitflow', 'git flow', 'flow']
126
+ },
127
+ {
128
+ key: 'trunk',
129
+ label: 'Trunk / GitHub Flow - short-lived feature branches (lightest)',
130
+ aliases: ['trunk', 'trunk-based', 'github flow', 'githubflow']
131
+ }
132
+ ];
133
+
134
+ // How AI-made commits are marked (PROPOSAL-047). Chosen once at init; the
135
+ // runtime does not re-ask. None is the default.
136
+ const AI_COMMIT_MARKER_OPTIONS = [
137
+ {
138
+ key: 'none',
139
+ label: 'None - AI commits look like any other commit',
140
+ aliases: ['none', 'off', 'no']
109
141
  },
110
142
  {
111
- key: 'git-trunk',
112
- label: 'Git principles - trunk-based',
113
- aliases: ['git principles - trunk-based', 'trunk', 'trunk-based']
143
+ key: 'co-authored-by',
144
+ label: 'Co-Authored-By trailer (dflow-ai) - filterable / auditable',
145
+ aliases: ['co-authored-by', 'co-author', 'trailer', 'coauthored']
114
146
  },
115
147
  {
116
- key: 'git-flow',
117
- label: 'Git principles - Git Flow',
118
- aliases: ['git principles - git flow', 'git flow', 'gitflow']
148
+ key: 'prefix',
149
+ label: '[ai-assisted] commit-message prefix - visible at a glance',
150
+ aliases: ['prefix', 'ai-assisted', '[ai-assisted]']
119
151
  }
120
152
  ];
121
153
 
@@ -203,8 +235,8 @@ async function runInit(options = {}) {
203
235
 
204
236
  const detection = await detectProjectSignals(cwd);
205
237
  const answers = await promptForAnswers(rl, stdout, stderr, detection);
206
- const warnings = [...preflight.warnings, ...buildDetectionWarnings(answers, detection)];
207
238
  const plan = await buildFilePlan(cwd, answers);
239
+ const warnings = [...preflight.warnings, ...buildDetectionWarnings(answers, detection), ...(plan.bundleWarnings || [])];
208
240
 
209
241
  renderPreview(stdout, plan, warnings);
210
242
  const confirmed = await askConfirmation(rl, 'Create these files? (y/N) ');
@@ -277,7 +309,8 @@ async function runConfigureAgents(options = {}) {
277
309
  await assertDflowInitialized(cwd);
278
310
 
279
311
  const projectContext = await inferProjectContext(cwd, rl, stdout, stderr);
280
- const aiAgents = await askAiAgents(rl, stdout, stderr);
312
+ const detectedAgents = await detectConfiguredAgents(cwd);
313
+ const aiAgents = await askAiAgents(rl, stdout, stderr, detectedAgents);
281
314
 
282
315
  if (aiAgents.length === 0) {
283
316
  throw new UserAbort('No AI agents selected. Nothing changed.');
@@ -488,10 +521,32 @@ async function detectProjectSignals(cwd) {
488
521
  return {
489
522
  hasSourceTree: hasSourceTree || relNames.has('src'),
490
523
  trackHint,
491
- stackHints
524
+ stackHints,
525
+ configuredAgents: await detectConfiguredAgents(cwd)
492
526
  };
493
527
  }
494
528
 
529
+ async function detectConfiguredAgents(cwd) {
530
+ // Surface agents this project already has configured so init / configure-agents
531
+ // can default to them instead of re-asking from scratch on every invocation.
532
+ // Order matches AI_AGENT_OPTIONS so the prompt numbering lines up.
533
+ const detected = [];
534
+ if (await pathExists(path.join(cwd, 'AGENTS.md'))) {
535
+ detected.push('agents');
536
+ }
537
+ if (
538
+ (await pathExists(path.join(cwd, 'CLAUDE.md'))) ||
539
+ (await pathExists(path.join(cwd, '.claude/commands/dflow'))) ||
540
+ (await pathExists(path.join(cwd, '.claude/skills/dflow')))
541
+ ) {
542
+ detected.push('claude');
543
+ }
544
+ if (await pathExists(path.join(cwd, '.github/copilot-instructions.md'))) {
545
+ detected.push('copilot');
546
+ }
547
+ return detected;
548
+ }
549
+
495
550
  async function collectProjectFiles(cwd, maxDepth, maxFiles) {
496
551
  const results = [];
497
552
  const ignored = new Set(['.git', 'node_modules', 'bin', 'obj', 'dflow']);
@@ -535,8 +590,8 @@ async function collectProjectFiles(cwd, maxDepth, maxFiles) {
535
590
  function buildDetectionWarnings(answers, detection) {
536
591
  const warnings = [];
537
592
 
538
- if (answers.projectType === 'greenfield' && detection.hasSourceTree) {
539
- warnings.push('Warning: source-tree signals already exist, but project type is Greenfield. Continuing with your selected project type.');
593
+ if (answers.projectType === 'greenfield' && detection.hasSourceTree && !detection.trackHint) {
594
+ warnings.push('Note: existing source files were detected (e.g. a src/ directory or a build manifest). This is expected if you just scaffolded a fresh project — Dflow will continue as Greenfield. If this is actually an existing codebase, consider re-running and selecting Brownfield.');
540
595
  }
541
596
 
542
597
  if (detection.trackHint && answers.projectType !== detection.trackHint) {
@@ -585,8 +640,22 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
585
640
  proseLanguage = await askCustomProseLanguage(rl, stderr);
586
641
  }
587
642
 
643
+ const gitPolicy = await askSelect(rl, stdout, stderr, {
644
+ id: 'Q5',
645
+ question: 'Which Git policy does the team follow? (drives branch gates and finish-stage merge guidance)',
646
+ options: GIT_POLICY_OPTIONS,
647
+ defaultKey: null
648
+ });
649
+
650
+ const aiCommitMarker = await askSelect(rl, stdout, stderr, {
651
+ id: 'Q6',
652
+ question: 'How should AI-made commits be marked? (the AI offers to commit at checkpoints; you can always decline)',
653
+ options: AI_COMMIT_MARKER_OPTIONS,
654
+ defaultKey: 'none'
655
+ });
656
+
588
657
  const optionalFiles = await askOptionalFiles(rl, stdout, stderr);
589
- const aiAgents = await askAiAgents(rl, stdout, stderr);
658
+ const aiAgents = await askAiAgents(rl, stdout, stderr, detection.configuredAgents || []);
590
659
 
591
660
  return {
592
661
  projectType,
@@ -594,6 +663,8 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
594
663
  techStackSummary,
595
664
  migrationContext,
596
665
  proseLanguage,
666
+ gitPolicy,
667
+ aiCommitMarker,
597
668
  optionalFiles,
598
669
  aiAgents
599
670
  };
@@ -618,10 +689,26 @@ async function inferProjectContext(cwd, rl, stdout, stderr) {
618
689
  techStackSummary: await inferTechStackSummary(cwd),
619
690
  migrationContext: await inferMigrationContext(cwd),
620
691
  proseLanguage: await inferProseLanguage(cwd),
692
+ gitPolicy: await inferGitPolicy(cwd),
693
+ aiCommitMarker: await inferAiCommitMarker(cwd),
621
694
  optionalFiles: []
622
695
  };
623
696
  }
624
697
 
698
+ async function inferGitPolicy(cwd) {
699
+ const conventionsPath = path.join(cwd, 'dflow/specs/shared/_conventions.md');
700
+ const content = await fs.readFile(conventionsPath, 'utf8').catch(() => '');
701
+ const match = content.match(/Selected Git policy:\s*`([^`]+)`/);
702
+ return match ? match[1] : null;
703
+ }
704
+
705
+ async function inferAiCommitMarker(cwd) {
706
+ const conventionsPath = path.join(cwd, 'dflow/specs/shared/_conventions.md');
707
+ const content = await fs.readFile(conventionsPath, 'utf8').catch(() => '');
708
+ const match = content.match(/AI commit marker:\s*`([^`]+)`/);
709
+ return match ? match[1] : null;
710
+ }
711
+
625
712
  async function inferExistingEdition(cwd) {
626
713
  if (await pathExists(path.join(cwd, 'dflow/specs/architecture/tech-debt.md'))) {
627
714
  return 'greenfield';
@@ -740,48 +827,44 @@ async function askOptionalFiles(rl, stdout, stderr) {
740
827
  while (true) {
741
828
  stdout.write('\nWhich optional starter files should Dflow seed?\n');
742
829
  OPTIONAL_FILE_OPTIONS.forEach((option, index) => {
743
- const defaultMarker = option.key === 'overview' || option.key === 'git-trunk' ? ' (recommended)' : '';
830
+ const defaultMarker = option.key === 'overview' ? ' (recommended)' : '';
744
831
  stdout.write(` ${index + 1}. ${option.label}${defaultMarker}\n`);
745
832
  });
746
833
 
747
- const answer = await askLine(rl, 'Enter comma-separated choices, "none", or press Enter for recommended [1,2]: ');
748
- const parsed = parseMultiselectAnswer(answer, OPTIONAL_FILE_OPTIONS, ['overview', 'git-trunk']);
834
+ const answer = await askLine(rl, 'Enter comma-separated choices, "none", or press Enter for recommended [1]: ');
835
+ const parsed = parseMultiselectAnswer(answer, OPTIONAL_FILE_OPTIONS, ['overview']);
749
836
 
750
837
  if (!parsed.valid) {
751
838
  failedAttempts += 1;
752
839
  if (failedAttempts >= 3) {
753
- throw new InitError('Too many invalid attempts for Q5. Dflow init aborted.');
840
+ throw new InitError('Too many invalid attempts for Q7. Dflow init aborted.');
754
841
  }
755
842
  stderr.write(`${parsed.message} (${3 - failedAttempts} attempts left)\n`);
756
843
  continue;
757
844
  }
758
845
 
759
- if (parsed.values.includes('git-trunk') && parsed.values.includes('git-flow')) {
760
- const keepBoth = await askConfirmation(
761
- rl,
762
- 'You selected both Git principles templates. Most projects choose one. Keep both? (y/N) '
763
- );
764
- if (!keepBoth) {
765
- failedAttempts = 0;
766
- continue;
767
- }
768
- }
769
-
770
846
  return parsed.values;
771
847
  }
772
848
  }
773
849
 
774
- async function askAiAgents(rl, stdout, stderr) {
850
+ async function askAiAgents(rl, stdout, stderr, defaultKeys = []) {
775
851
  let failedAttempts = 0;
852
+ const validDefaults = AI_AGENT_OPTIONS
853
+ .filter((option) => defaultKeys.includes(option.key))
854
+ .map((option) => option.key);
776
855
 
777
856
  while (true) {
778
857
  stdout.write('\nWhich AI coding agents should Dflow configure?\n');
779
858
  AI_AGENT_OPTIONS.forEach((option, index) => {
780
- stdout.write(` ${index + 1}. ${option.label}\n`);
859
+ const marker = validDefaults.includes(option.key) ? ' (currently configured)' : '';
860
+ stdout.write(` ${index + 1}. ${option.label}${marker}\n`);
781
861
  });
782
862
 
783
- const answer = await askLine(rl, 'Enter comma-separated choices or "none" (default: none): ');
784
- const parsed = parseMultiselectAnswer(answer || 'none', AI_AGENT_OPTIONS, []);
863
+ const defaultHint = validDefaults.length > 0
864
+ ? validDefaults.map((key) => AI_AGENT_OPTIONS.findIndex((option) => option.key === key) + 1).join(',')
865
+ : 'none';
866
+ const answer = await askLine(rl, `Enter comma-separated choices or "none" (default: ${defaultHint}): `);
867
+ const parsed = parseMultiselectAnswer(answer, AI_AGENT_OPTIONS, validDefaults);
785
868
 
786
869
  if (!parsed.valid) {
787
870
  failedAttempts += 1;
@@ -963,6 +1046,7 @@ async function buildFilePlan(cwd, answers) {
963
1046
  content = substitutePlaceholders(content, substitution);
964
1047
  if (options.injectProseLanguage) {
965
1048
  content = ensureProseLanguageSection(content, answers.proseLanguage);
1049
+ content = ensureConventionPolicySections(content, answers);
966
1050
  }
967
1051
  items.push({
968
1052
  relativePath,
@@ -996,11 +1080,13 @@ async function buildFilePlan(cwd, answers) {
996
1080
  if (answers.optionalFiles.includes('overview')) {
997
1081
  await addTemplate('dflow/specs/shared/_overview.md', 'scaffolding/_overview.md', 'selected');
998
1082
  }
999
- if (answers.optionalFiles.includes('git-trunk')) {
1000
- await addTemplate('dflow/specs/shared/Git-principles-trunk.md', 'scaffolding/Git-principles-trunk.md', 'selected');
1001
- }
1002
- if (answers.optionalFiles.includes('git-flow')) {
1003
- await addTemplate('dflow/specs/shared/Git-principles-gitflow.md', 'scaffolding/Git-principles-gitflow.md', 'selected');
1083
+
1084
+ // PROPOSAL-047: the selected Git policy is mandatory — always project exactly
1085
+ // the matching Git-principles file so the runtime branch gates have a policy.
1086
+ if (answers.gitPolicy === 'gitflow') {
1087
+ await addTemplate('dflow/specs/shared/Git-principles-gitflow.md', 'scaffolding/Git-principles-gitflow.md', 'mandatory, selected Git policy');
1088
+ } else {
1089
+ await addTemplate('dflow/specs/shared/Git-principles-trunk.md', 'scaffolding/Git-principles-trunk.md', 'mandatory, selected Git policy');
1004
1090
  }
1005
1091
 
1006
1092
  if (answers.aiAgents.length > 0) {
@@ -1012,9 +1098,14 @@ async function buildFilePlan(cwd, answers) {
1012
1098
 
1013
1099
  await finalizePlanItems(cwd, items);
1014
1100
 
1101
+ // Always project the workflow bundle (required for /dflow:* workflows to be reachable).
1102
+ const bundleWarnings = [];
1103
+ await addWorkflowBundleItems(cwd, items, bundleWarnings, answers.edition);
1104
+
1015
1105
  return {
1016
1106
  items,
1017
1107
  deferred: buildDeferredItems(answers.edition),
1108
+ bundleWarnings,
1018
1109
  unresolvedInitPlaceholders: Array.from(substitution.entries())
1019
1110
  .filter(([placeholder, value]) => placeholder === value)
1020
1111
  .map(([placeholder]) => placeholder)
@@ -1041,7 +1132,7 @@ async function buildConfigureAgentsPlan(cwd, answers) {
1041
1132
  const commandRegistry = answers.commandAdapters ? parseDflowCommandRegistry(content) : [];
1042
1133
 
1043
1134
  for (const agent of answers.aiAgents) {
1044
- await addAiAgentShim(cwd, items, agent, substitution, { commandRegistry });
1135
+ await addAiAgentShim(cwd, items, agent, substitution, { commandRegistry, warnings });
1045
1136
  }
1046
1137
 
1047
1138
  if (answers.commandAdapters) {
@@ -1056,6 +1147,14 @@ async function buildConfigureAgentsPlan(cwd, answers) {
1056
1147
 
1057
1148
  await addSkillAdapterItems(cwd, items, answers.aiAgents, answers.skills, warnings);
1058
1149
 
1150
+ // Project the workflow bundle on configure-agents too, so pre-039 projects (no bundle)
1151
+ // and edition-switch repairs get the runtime references/templates reachable. The function
1152
+ // is idempotent: it skips fresh bundle files, updates Dflow-generated ones, and warns
1153
+ // (without overwriting) on user-modified bundle files.
1154
+ const bundleWarnings = [];
1155
+ await addWorkflowBundleItems(cwd, items, bundleWarnings, answers.edition);
1156
+ warnings.push(...bundleWarnings);
1157
+
1059
1158
  return {
1060
1159
  items,
1061
1160
  deferred: [],
@@ -1078,21 +1177,231 @@ async function finalizePlanItems(cwd, items) {
1078
1177
  }
1079
1178
  }
1080
1179
 
1180
+ async function listBundleSourceFiles(edition) {
1181
+ const bundleDirs = ['references', 'templates'];
1182
+ const files = [];
1183
+
1184
+ for (const dir of bundleDirs) {
1185
+ const sourceDir = path.join(TEMPLATE_ROOT, edition, dir);
1186
+ let entries;
1187
+ try {
1188
+ entries = await fs.readdir(sourceDir);
1189
+ } catch (error) {
1190
+ if (error.code === 'ENOENT') {
1191
+ continue;
1192
+ }
1193
+ throw error;
1194
+ }
1195
+ for (const entry of entries) {
1196
+ const sourceRel = `${dir}/${entry}`;
1197
+ const sourcePath = path.join(sourceDir, entry);
1198
+ const stat = await fs.stat(sourcePath);
1199
+ if (stat.isFile()) {
1200
+ files.push({ sourceRel, dir, name: entry });
1201
+ }
1202
+ }
1203
+ }
1204
+
1205
+ return files;
1206
+ }
1207
+
1208
+ async function readCurrentBundleManifest(cwd) {
1209
+ const manifestPath = path.join(cwd, WORKFLOW_BUNDLE_MANIFEST_PATH);
1210
+ try {
1211
+ const raw = await fs.readFile(manifestPath, 'utf8');
1212
+ return JSON.parse(raw);
1213
+ } catch {
1214
+ return null;
1215
+ }
1216
+ }
1217
+
1218
+ function buildBundleManifest(edition, version, files) {
1219
+ return {
1220
+ edition,
1221
+ version,
1222
+ generatedBy: 'dflow-sdd-ddd',
1223
+ files: files.map((f) => `${WORKFLOW_BUNDLE_DEST}/${f.sourceRel}`)
1224
+ };
1225
+ }
1226
+
1227
+ function injectBundleMarker(content) {
1228
+ return `${WORKFLOW_BUNDLE_GENERATED_MARKER}\n\n${content}`;
1229
+ }
1230
+
1231
+ async function addWorkflowBundleItems(cwd, items, warnings, edition) {
1232
+ const bundleFiles = await listBundleSourceFiles(edition);
1233
+
1234
+ // Detect any previous manifest to handle edition-switch stale cleanup.
1235
+ const existingManifest = await readCurrentBundleManifest(cwd);
1236
+ const previousEdition = existingManifest ? existingManifest.edition : null;
1237
+
1238
+ // If the edition changed, schedule removal of stale generated files from prior edition.
1239
+ if (previousEdition && previousEdition !== edition) {
1240
+ const staleFiles = existingManifest.files || [];
1241
+ for (const staleRelPath of staleFiles) {
1242
+ const staleAbsPath = path.join(cwd, staleRelPath);
1243
+ let staleExists = false;
1244
+ try {
1245
+ await fs.stat(staleAbsPath);
1246
+ staleExists = true;
1247
+ } catch {
1248
+ staleExists = false;
1249
+ }
1250
+ if (!staleExists) {
1251
+ continue;
1252
+ }
1253
+ const staleContent = await fs.readFile(staleAbsPath, 'utf8');
1254
+ if (!staleContent.includes(WORKFLOW_BUNDLE_GENERATED_MARKER)) {
1255
+ warnings.push(
1256
+ `Edition changed from ${previousEdition} to ${edition}; skipped removal of user-modified bundle file: ${staleRelPath}`
1257
+ );
1258
+ continue;
1259
+ }
1260
+ // Check if this path is also in the new edition bundle — if so, it will be overwritten, not removed.
1261
+ const newRelPaths = new Set(bundleFiles.map((f) => `${WORKFLOW_BUNDLE_DEST}/${f.sourceRel}`));
1262
+ if (!newRelPaths.has(staleRelPath)) {
1263
+ items.push({
1264
+ relativePath: staleRelPath,
1265
+ source: `stale-bundle:${previousEdition}`,
1266
+ notes: `stale workflow bundle file from ${previousEdition} edition`,
1267
+ action: 'remove',
1268
+ size: Buffer.byteLength(staleContent, 'utf8'),
1269
+ expectedContent: staleContent
1270
+ });
1271
+ }
1272
+ }
1273
+ }
1274
+
1275
+ // Build items for current edition bundle files.
1276
+ for (const { sourceRel } of bundleFiles) {
1277
+ const relativePath = `${WORKFLOW_BUNDLE_DEST}/${sourceRel}`;
1278
+ const absolutePath = path.join(cwd, relativePath);
1279
+ const sourceContent = await readPackagedBundleFile(edition, sourceRel);
1280
+ const content = injectBundleMarker(sourceContent);
1281
+
1282
+ let action;
1283
+ let notes = 'workflow bundle';
1284
+ const targetExists = await pathExists(absolutePath);
1285
+
1286
+ if (targetExists) {
1287
+ const existingContent = await fs.readFile(absolutePath, 'utf8');
1288
+ if (existingContent.includes(WORKFLOW_BUNDLE_GENERATED_MARKER)) {
1289
+ action = 'update';
1290
+ } else {
1291
+ action = 'skip';
1292
+ notes = 'workflow bundle, user-modified — skipped; remove or rename to let Dflow manage';
1293
+ warnings.push(
1294
+ `Existing ${relativePath} is not a Dflow-generated bundle file; left unchanged. Remove or rename it to let Dflow manage this file.`
1295
+ );
1296
+ }
1297
+ } else {
1298
+ action = 'create';
1299
+ }
1300
+
1301
+ items.push({
1302
+ relativePath,
1303
+ source: `packaged-bundle:${edition}/${sourceRel}`,
1304
+ notes,
1305
+ content,
1306
+ action,
1307
+ overwrite: action === 'update',
1308
+ size: Buffer.byteLength(content, 'utf8')
1309
+ });
1310
+ }
1311
+
1312
+ // Add the manifest file.
1313
+ const manifestContent = JSON.stringify(
1314
+ buildBundleManifest(edition, pkg.version, bundleFiles),
1315
+ null,
1316
+ 2
1317
+ ) + '\n';
1318
+ const manifestExists = await pathExists(path.join(cwd, WORKFLOW_BUNDLE_MANIFEST_PATH));
1319
+
1320
+ items.push({
1321
+ relativePath: WORKFLOW_BUNDLE_MANIFEST_PATH,
1322
+ source: `generated:workflow-bundle-manifest`,
1323
+ notes: 'workflow bundle manifest',
1324
+ content: manifestContent,
1325
+ action: manifestExists ? 'update' : 'create',
1326
+ overwrite: true,
1327
+ size: Buffer.byteLength(manifestContent, 'utf8')
1328
+ });
1329
+ }
1330
+
1331
+ async function readPackagedBundleFile(edition, sourceRel) {
1332
+ const filePath = path.join(TEMPLATE_ROOT, edition, sourceRel);
1333
+ const normalizedRoot = path.resolve(TEMPLATE_ROOT, edition);
1334
+ const normalizedFilePath = path.resolve(filePath);
1335
+
1336
+ if (!normalizedFilePath.startsWith(`${normalizedRoot}${path.sep}`)) {
1337
+ throw new InitError(`Internal error: packaged bundle file not found: templates/${edition}/${sourceRel}`);
1338
+ }
1339
+
1340
+ let buffer;
1341
+ try {
1342
+ buffer = await fs.readFile(normalizedFilePath);
1343
+ } catch (error) {
1344
+ if (error.code === 'ENOENT') {
1345
+ throw new InitError(`Internal error: packaged bundle file not found: templates/${edition}/${sourceRel}`);
1346
+ }
1347
+ throw new InitError(`Internal error: cannot read packaged bundle file: templates/${edition}/${sourceRel}`);
1348
+ }
1349
+
1350
+ try {
1351
+ return new TextDecoder('utf-8', { fatal: true }).decode(buffer);
1352
+ } catch {
1353
+ throw new InitError(`Internal error: invalid UTF-8 packaged bundle file: templates/${edition}/${sourceRel}`);
1354
+ }
1355
+ }
1356
+
1081
1357
  async function addAiAgentShim(cwd, items, agent, substitution, options = {}) {
1082
1358
  const target = getAiAgentTarget(agent);
1083
1359
  const targetPath = path.join(cwd, target.relativePath);
1084
1360
  const targetExists = await pathExists(targetPath);
1085
1361
  const targetConfigured = targetExists && await fileReferencesAiAgentGuide(targetPath);
1086
1362
  const commandRegistry = options.commandRegistry || [];
1363
+ const warnings = options.warnings;
1087
1364
  const codexCommandAdapterSnippet = target.relativePath === 'AGENTS.md' && commandRegistry.length > 0 && targetExists;
1365
+ const content = substitutePlaceholders(buildAiAgentShim(target.relativePath, options.commandRegistry), substitution);
1366
+ const source = `generated:${agent}-shim`;
1367
+
1368
+ // PROPOSAL-046: when adding Codex command triggers and the existing AGENTS.md
1369
+ // is an unmodified Dflow shim, inject the marked trigger section directly
1370
+ // (zero manual merge) instead of parking a side snippet. A user-modified shim
1371
+ // still degrades safely to the snippet + warning.
1372
+ if (codexCommandAdapterSnippet && targetConfigured) {
1373
+ const existingContent = await fs.readFile(targetPath, 'utf8');
1374
+ const baseShim = substitutePlaceholders(buildAiAgentShim(target.relativePath), substitution);
1375
+ if (isPristineDflowAgentsShim(existingContent, baseShim)) {
1376
+ items.push({
1377
+ relativePath: target.relativePath,
1378
+ source,
1379
+ notes: `selected, injected command trigger section into Dflow-generated ${target.relativePath}`,
1380
+ content,
1381
+ overwrite: true
1382
+ });
1383
+ return;
1384
+ }
1385
+ if (warnings) {
1386
+ warnings.push(
1387
+ `Existing ${target.relativePath} was modified after Dflow generated it; wrote the command trigger section to dflow/specs/shared/AGENTS-md-command-adapters-snippet.md for manual merge.`
1388
+ );
1389
+ }
1390
+ items.push({
1391
+ relativePath: 'dflow/specs/shared/AGENTS-md-command-adapters-snippet.md',
1392
+ source,
1393
+ notes: `selected, ${target.relativePath} was modified after Dflow generated it; merge this command trigger snippet manually`,
1394
+ content,
1395
+ overwrite: true
1396
+ });
1397
+ return;
1398
+ }
1399
+
1088
1400
  const relativePath = codexCommandAdapterSnippet
1089
- ? (targetConfigured ? 'dflow/specs/shared/AGENTS-md-command-adapters-snippet.md' : target.snippetPath)
1401
+ ? target.snippetPath
1090
1402
  : (targetExists && !targetConfigured ? target.snippetPath : target.relativePath);
1091
- const content = substitutePlaceholders(buildAiAgentShim(target.relativePath, options.commandRegistry), substitution);
1092
1403
  let notes = 'selected, tool-specific shim';
1093
- if (codexCommandAdapterSnippet && targetConfigured) {
1094
- notes = `selected, ${target.relativePath} already points to AI-AGENT-GUIDE.md; merge this command trigger snippet manually`;
1095
- } else if (codexCommandAdapterSnippet) {
1404
+ if (codexCommandAdapterSnippet) {
1096
1405
  notes = `selected, ${target.relativePath} already exists; merge this command trigger snippet manually`;
1097
1406
  } else if (targetConfigured) {
1098
1407
  notes = `selected, ${target.relativePath} already points to AI-AGENT-GUIDE.md`;
@@ -1102,7 +1411,7 @@ async function addAiAgentShim(cwd, items, agent, substitution, options = {}) {
1102
1411
 
1103
1412
  items.push({
1104
1413
  relativePath,
1105
- source: `generated:${agent}-shim`,
1414
+ source,
1106
1415
  notes,
1107
1416
  content,
1108
1417
  overwrite: codexCommandAdapterSnippet
@@ -1157,10 +1466,11 @@ This project uses Dflow for spec-first AI-assisted development.
1157
1466
 
1158
1467
  Before planning or editing code, read and follow:
1159
1468
 
1160
- - \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
1469
+ - \`dflow/specs/shared/AI-AGENT-GUIDE.md\` — command registry, routing rules, and project context.
1470
+ - \`dflow/specs/shared/dflow-workflows/\` — vendored workflow bundle with executable step definitions.
1161
1471
 
1162
- Keep tool-specific instruction files small. The Dflow guide above is the
1163
- single source of truth for project workflow rules, slash-command behavior,
1472
+ Keep tool-specific instruction files small. The guide and workflow bundle are
1473
+ the authoritative sources for Dflow workflow rules, slash-command behavior,
1164
1474
  spec locations, and SDD/DDD constraints.${commandTriggerHint}${importHint}
1165
1475
  `;
1166
1476
  }
@@ -1235,39 +1545,13 @@ async function addLegacyCommandAdapterCleanupItems(cwd, items, aiAgents, warning
1235
1545
  }
1236
1546
  }
1237
1547
 
1238
- function buildDflowSkillAdapter() {
1239
- return `---
1240
- name: dflow
1241
- description: >
1242
- Dflow SDD/DDD workflow guardian for this project. PRIMARY: the canonical
1243
- /dflow:* commands (/dflow:new-feature, /dflow:modify-existing, /dflow:bug-fix,
1244
- /dflow:new-phase, /dflow:finish-feature, /dflow:pr-review, /dflow:verify,
1245
- /dflow:report-dflow-feedback, /dflow:status, /dflow:next, /dflow:cancel).
1246
- SECONDARY (auto-trigger safety net) — engage ONLY for: adding or changing
1247
- product/domain behavior, new requirements, a feature or bug-fix workflow, or
1248
- spec-impacting architecture/domain-model decisions. Do NOT engage for pure
1249
- refactors, infrastructure chores, formatting, or general code questions.
1250
- When engaged by natural language, DO NOT auto-enter a workflow: judge the
1251
- intent, suggest the matching /dflow: command, and wait for confirmation.
1252
- ---
1253
-
1254
- ${SKILL_ADAPTER_GENERATED_MARKER}
1255
-
1256
- # Dflow SDD/DDD Workflow Guardian
1257
-
1258
- This project uses Dflow for spec-first AI-assisted development. The canonical
1259
- workflow contract, command registry, spec locations, and SDD/DDD rules live in:
1260
-
1261
- - \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
1262
-
1263
- When this skill engages, read that guide and follow the matching \`/dflow:\`
1264
- workflow or control command defined there. Do not duplicate or invent workflow
1265
- steps here — the guide is the single source of truth.
1266
-
1267
- If engaged by natural language (not an explicit \`/dflow:\` command): identify
1268
- which \`/dflow:\` command fits, suggest it, and wait for the developer to confirm
1269
- before entering any workflow.
1270
- `;
1548
+ // Edition-agnostic thin shell: a single canonical source at
1549
+ // templates/common/skill/SKILL.md (PROPOSAL-041 C1). Returns the file
1550
+ // verbatim — frontmatter, marker, and body all live in the source file so
1551
+ // the skill content can be edited without touching JS.
1552
+ async function buildDflowSkillAdapter() {
1553
+ const sourcePath = path.join(TEMPLATE_ROOT, COMMON_SKILL_SOURCE_REL);
1554
+ return fs.readFile(sourcePath, 'utf8');
1271
1555
  }
1272
1556
 
1273
1557
  async function addSkillAdapterItems(cwd, items, aiAgents, skills, warnings) {
@@ -1307,11 +1591,13 @@ async function addSkillAdapterItems(cwd, items, aiAgents, skills, warnings) {
1307
1591
  return;
1308
1592
  }
1309
1593
 
1594
+ const skillContent = await buildDflowSkillAdapter();
1310
1595
  items.push({
1311
1596
  relativePath,
1312
1597
  source: 'generated:claude-skill-adapter',
1313
1598
  notes: 'skill adapter, thin skill pointing to AI-AGENT-GUIDE.md',
1314
- content: buildDflowSkillAdapter(),
1599
+ content: skillContent,
1600
+ size: Buffer.byteLength(skillContent, 'utf8'),
1315
1601
  overwrite: true
1316
1602
  });
1317
1603
  }
@@ -1343,6 +1629,36 @@ function normalizeCommandAdapterFingerprint(content) {
1343
1629
  return String(content).replace(/\r\n/g, '\n');
1344
1630
  }
1345
1631
 
1632
+ function normalizeShimForMatch(content) {
1633
+ return String(content)
1634
+ .replace(/\r\n/g, '\n')
1635
+ .split('\n')
1636
+ .map((line) => line.replace(/[ \t]+$/, ''))
1637
+ .join('\n')
1638
+ .replace(/\n{3,}/g, '\n\n')
1639
+ .trim();
1640
+ }
1641
+
1642
+ function stripCodexTriggerBlock(content) {
1643
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1644
+ const re = new RegExp(
1645
+ `\\n*${escape(CODEX_TRIGGER_SECTION_START)}[\\s\\S]*?${escape(CODEX_TRIGGER_SECTION_END)}\\n*`
1646
+ );
1647
+ return content.replace(re, '\n');
1648
+ }
1649
+
1650
+ // An AGENTS.md is a safely-injectable Dflow shim when, after removing any
1651
+ // previously-injected trigger block, it matches the shim Dflow itself
1652
+ // generates. This covers both a pristine 0.8.0/0.9.0 shim (no marker, normalized
1653
+ // exact-template match) and a shim Dflow already injected into (idempotent
1654
+ // re-projection). A user-edited shim fails the match and degrades to a snippet.
1655
+ // (A future shim could carry its own generated-marker for a cheaper check, but
1656
+ // that would require freezing this pre-marker template for back-compat matching;
1657
+ // the normalized template match works for both eras without that.)
1658
+ function isPristineDflowAgentsShim(existingContent, baseShim) {
1659
+ return normalizeShimForMatch(stripCodexTriggerBlock(existingContent)) === normalizeShimForMatch(baseShim);
1660
+ }
1661
+
1346
1662
  function buildCodexCommandTriggerSection(commandRegistry) {
1347
1663
  const triggers = commandRegistry
1348
1664
  .map((command) => {
@@ -1353,6 +1669,8 @@ function buildCodexCommandTriggerSection(commandRegistry) {
1353
1669
 
1354
1670
  return `
1355
1671
 
1672
+ ${CODEX_TRIGGER_SECTION_START}
1673
+
1356
1674
  ## Dflow Text Triggers
1357
1675
 
1358
1676
  Codex does not install Dflow command files. When the developer asks for a
@@ -1367,6 +1685,8 @@ and execute it.
1367
1685
  Recognized canonical triggers:
1368
1686
 
1369
1687
  ${triggers}
1688
+
1689
+ ${CODEX_TRIGGER_SECTION_END}
1370
1690
  `;
1371
1691
  }
1372
1692
 
@@ -1381,7 +1701,8 @@ ${COMMAND_ADAPTER_GENERATED_MARKER}
1381
1701
 
1382
1702
  Execute the canonical \`${command.label}\` Dflow workflow or control command.
1383
1703
 
1384
- Definition: \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
1704
+ Registry and rules: \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
1705
+ Workflow steps: \`dflow/specs/shared/dflow-workflows/\`
1385
1706
 
1386
1707
  ${argHint}
1387
1708
  `;
@@ -1555,8 +1876,7 @@ const PLACEHOLDER_ALIASES = {
1555
1876
 
1556
1877
  function buildSubstitutionMap(cwd, answers) {
1557
1878
  const extracted = extractTechStackPlaceholders(answers.techStackSummary);
1558
- const gitSelection = answers.optionalFiles.filter((key) => key === 'git-trunk' || key === 'git-flow');
1559
- const gitStyle = gitSelection.length === 1 ? (gitSelection[0] === 'git-trunk' ? 'trunk' : 'gitflow') : null;
1879
+ const gitStyle = answers.gitPolicy === 'gitflow' ? 'gitflow' : (answers.gitPolicy === 'trunk' ? 'trunk' : null);
1560
1880
  const systemName = path.basename(cwd);
1561
1881
 
1562
1882
  const map = new Map([
@@ -1788,6 +2108,70 @@ function stripProseLanguageSections(content) {
1788
2108
  return kept.join('\n');
1789
2109
  }
1790
2110
 
2111
+ function stripNamedSections(content, headings) {
2112
+ const set = new Set(headings.map((heading) => `## ${heading}`));
2113
+ const kept = [];
2114
+ let skipping = false;
2115
+
2116
+ for (const line of content.split(/\r?\n/)) {
2117
+ if (set.has(line.trim())) {
2118
+ skipping = true;
2119
+ continue;
2120
+ }
2121
+ if (skipping && /^## /.test(line)) {
2122
+ skipping = false;
2123
+ }
2124
+ if (!skipping) {
2125
+ kept.push(line);
2126
+ }
2127
+ }
2128
+
2129
+ return kept.join('\n');
2130
+ }
2131
+
2132
+ function buildGitPolicySection(gitPolicy) {
2133
+ const policy = gitPolicy === 'gitflow' ? 'gitflow' : 'trunk';
2134
+ return `## Git Policy
2135
+
2136
+ Selected Git policy: \`${policy}\`
2137
+
2138
+ Dflow runtime branch gates and finish-feature guidance follow this policy. Both
2139
+ policies use feature branches; the policy selects the finish-stage merge
2140
+ guidance — \`gitflow\` introduces merge-commit / release+develop flow, while
2141
+ \`trunk\` (GitHub Flow) favors squash or fast-forward back to the main branch
2142
+ with small, frequent merges.`;
2143
+ }
2144
+
2145
+ function buildAiCommitPolicySection(aiCommitMarker) {
2146
+ const marker = ['none', 'co-authored-by', 'prefix'].includes(aiCommitMarker) ? aiCommitMarker : 'none';
2147
+ return `## AI Commit Policy
2148
+
2149
+ AI commit marker: \`${marker}\`
2150
+
2151
+ At lifecycle checkpoints the AI may offer to commit using your Git identity; you
2152
+ can always decline (Y / N). Completed and skipped checkpoints are recorded in
2153
+ each feature's Checkpoint Log. Marker modes:
2154
+
2155
+ - \`none\`: AI-made commits carry no extra marker.
2156
+ - \`co-authored-by\`: append a \`Co-Authored-By: dflow-ai <noreply@dflow.local>\`
2157
+ trailer (teams may customize the name/email).
2158
+ - \`prefix\`: prefix the commit subject with \`[ai-assisted]\`.`;
2159
+ }
2160
+
2161
+ function ensureConventionPolicySections(content, answers) {
2162
+ const stripped = stripNamedSections(content, ['Git Policy', 'AI Commit Policy']).replace(/\n{3,}/g, '\n\n');
2163
+ const sections = `${buildGitPolicySection(answers.gitPolicy)}\n\n${buildAiCommitPolicySection(answers.aiCommitMarker)}`;
2164
+ const markerMatch = stripped.match(/^## Filling the Templates/m);
2165
+
2166
+ if (markerMatch && typeof markerMatch.index === 'number') {
2167
+ const before = stripped.slice(0, markerMatch.index).replace(/\s*$/, '\n\n');
2168
+ const after = stripped.slice(markerMatch.index).replace(/^\s*/, '');
2169
+ return `${before}${sections}\n\n${after}`;
2170
+ }
2171
+
2172
+ return `${stripped.replace(/\s*$/, '\n\n')}${sections}\n`;
2173
+ }
2174
+
1791
2175
  function buildProseLanguageSection(proseLanguage) {
1792
2176
  return `## Prose Language
1793
2177
 
@@ -1952,14 +2336,17 @@ function collectUnresolvedPlaceholderWarnings(plan, createdPaths) {
1952
2336
  return [];
1953
2337
  }
1954
2338
 
2339
+ const placeholderFiles = new Set();
1955
2340
  for (const item of plan.items) {
1956
2341
  if (!createdSet.has(item.relativePath)) {
1957
2342
  continue;
1958
2343
  }
1959
2344
  const matches = item.content.match(/{[^{}\n]+}/g) || [];
1960
- matches
1961
- .filter((match) => unresolvedInitPlaceholders.has(match))
1962
- .forEach((match) => placeholders.add(match));
2345
+ const hits = matches.filter((match) => unresolvedInitPlaceholders.has(match));
2346
+ hits.forEach((match) => placeholders.add(match));
2347
+ if (hits.length > 0) {
2348
+ placeholderFiles.add(item.relativePath);
2349
+ }
1963
2350
  }
1964
2351
 
1965
2352
  if (placeholders.size === 0) {
@@ -1969,7 +2356,10 @@ function collectUnresolvedPlaceholderWarnings(plan, createdPaths) {
1969
2356
  const sorted = Array.from(placeholders).sort();
1970
2357
  const shown = sorted.slice(0, 25).join(', ');
1971
2358
  const suffix = sorted.length > 25 ? `, and ${sorted.length - 25} more` : '';
1972
- return [`Unresolved placeholders remain for later SDD workflows: ${shown}${suffix}.`];
2359
+ const files = Array.from(placeholderFiles).sort();
2360
+ const filesShown = files.slice(0, 10).join(', ');
2361
+ const filesSuffix = files.length > 10 ? `, and ${files.length - 10} more` : '';
2362
+ return [`Unresolved placeholders remain for later SDD workflows: ${shown}${suffix}. Fill them in (or leave for the workflow to resolve) in: ${filesShown}${filesSuffix}.`];
1973
2363
  }
1974
2364
 
1975
2365
  function printResultReport(stdout, result, deferred) {
@@ -2086,6 +2476,9 @@ function normalizePath(value) {
2086
2476
  }
2087
2477
 
2088
2478
  function formatBytes(bytes) {
2479
+ if (!Number.isFinite(bytes)) {
2480
+ return '—';
2481
+ }
2089
2482
  if (bytes < 1024) {
2090
2483
  return `${bytes} B`;
2091
2484
  }