dflow-sdd-ddd 0.6.0 → 0.8.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 +65 -0
  2. package/README.en.md +30 -0
  3. package/README.md +14 -0
  4. package/bin/dflow.js +7 -3
  5. package/docs/evaluating-dflow.en.md +14 -5
  6. package/docs/evaluating-dflow.md +14 -5
  7. package/docs/npm-publish-checklist.md +8 -0
  8. package/docs/using-with-claude-code.en.md +118 -11
  9. package/docs/using-with-claude-code.md +94 -9
  10. package/docs/using-with-codex.en.md +14 -0
  11. package/docs/using-with-codex.md +10 -0
  12. package/docs/using-with-github-copilot.en.md +36 -0
  13. package/docs/using-with-github-copilot.md +26 -0
  14. package/lib/init.js +461 -24
  15. package/package.json +1 -1
  16. package/templates/brownfield/references/dflow-feedback-flow.md +179 -0
  17. package/templates/brownfield/references/drift-verification.md +183 -0
  18. package/templates/brownfield/references/finish-feature-flow.md +259 -0
  19. package/templates/brownfield/references/git-integration.md +312 -0
  20. package/templates/brownfield/references/init-project-flow.md +413 -0
  21. package/templates/brownfield/references/modify-existing-flow.md +444 -0
  22. package/templates/brownfield/references/new-feature-flow.md +367 -0
  23. package/templates/brownfield/references/new-phase-flow.md +259 -0
  24. package/templates/brownfield/references/pr-review-checklist.md +179 -0
  25. package/templates/brownfield/scaffolding/AI-AGENT-GUIDE.md +31 -4
  26. package/templates/brownfield/scaffolding/CLAUDE-md-snippet.md +12 -8
  27. package/templates/brownfield/scaffolding/Git-principles-gitflow.md +1 -1
  28. package/templates/brownfield/scaffolding/Git-principles-trunk.md +1 -1
  29. package/templates/brownfield/scaffolding/_conventions.md +1 -1
  30. package/templates/brownfield/scaffolding/_overview.md +3 -3
  31. package/templates/brownfield/templates/context-map.md +1 -1
  32. package/templates/brownfield/templates/glossary.md +1 -1
  33. package/templates/brownfield/templates/models.md +1 -1
  34. package/templates/brownfield/templates/rules.md +1 -1
  35. package/templates/brownfield/templates/tech-debt.md +1 -1
  36. package/templates/common/skill/SKILL.md +35 -0
  37. package/templates/greenfield/references/ddd-modeling-guide.md +351 -0
  38. package/templates/greenfield/references/dflow-feedback-flow.md +179 -0
  39. package/templates/greenfield/references/drift-verification.md +195 -0
  40. package/templates/greenfield/references/finish-feature-flow.md +280 -0
  41. package/templates/greenfield/references/git-integration.md +285 -0
  42. package/templates/greenfield/references/init-project-flow.md +447 -0
  43. package/templates/greenfield/references/modify-existing-flow.md +362 -0
  44. package/templates/greenfield/references/new-feature-flow.md +397 -0
  45. package/templates/greenfield/references/new-phase-flow.md +273 -0
  46. package/templates/greenfield/references/pr-review-checklist.md +130 -0
  47. package/templates/greenfield/scaffolding/AI-AGENT-GUIDE.md +31 -4
  48. package/templates/greenfield/scaffolding/CLAUDE-md-snippet.md +15 -13
  49. package/templates/greenfield/scaffolding/Git-principles-gitflow.md +1 -1
  50. package/templates/greenfield/scaffolding/Git-principles-trunk.md +1 -1
  51. package/templates/greenfield/scaffolding/_conventions.md +1 -1
  52. package/templates/greenfield/scaffolding/_overview.md +5 -3
  53. package/templates/greenfield/scaffolding/architecture-decisions-README.md +1 -1
  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
@@ -10,6 +10,12 @@ const PACKAGE_ROOT = path.resolve(__dirname, '..');
10
10
  const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, 'templates');
11
11
  const COMMAND_REGISTRY_START = '<!-- dflow-command-registry:start -->';
12
12
  const COMMAND_REGISTRY_END = '<!-- dflow-command-registry:end -->';
13
+ const COMMAND_ADAPTER_GENERATED_MARKER = '<!-- dflow-generated: command-adapter -->';
14
+ const SKILL_ADAPTER_GENERATED_MARKER = '<!-- dflow-generated: skill-adapter -->';
15
+ const WORKFLOW_BUNDLE_GENERATED_MARKER = '<!-- dflow-generated: workflow-bundle -->';
16
+ const WORKFLOW_BUNDLE_DEST = 'dflow/specs/shared/dflow-workflows';
17
+ const WORKFLOW_BUNDLE_MANIFEST_PATH = `${WORKFLOW_BUNDLE_DEST}/.dflow-bundle-manifest.json`;
18
+ const COMMON_SKILL_SOURCE_REL = 'common/skill/SKILL.md';
13
19
  const EXPECTED_COMMAND_IDS = [
14
20
  'new-feature',
15
21
  'modify-existing',
@@ -23,6 +29,28 @@ const EXPECTED_COMMAND_IDS = [
23
29
  'next',
24
30
  'cancel'
25
31
  ];
32
+ const LEGACY_COMMAND_ADAPTERS = [
33
+ {
34
+ version: '0.5.0',
35
+ agent: 'claude',
36
+ source: 'generated:legacy-claude-command-adapter-v0.5.0',
37
+ pathPattern: '.claude/commands/dflow/dflow-<id>.md',
38
+ fingerprint: 'v0.5.0 buildThinCommandWrapper',
39
+ commands: [
40
+ { id: 'new-feature', label: '/dflow:new-feature', argHint: 'feature request' },
41
+ { id: 'modify-existing', label: '/dflow:modify-existing', argHint: 'change request' },
42
+ { id: 'bug-fix', label: '/dflow:bug-fix', argHint: 'expected vs actual' },
43
+ { id: 'new-phase', label: '/dflow:new-phase', argHint: 'feature id or phase goal' },
44
+ { id: 'finish-feature', label: '/dflow:finish-feature', argHint: 'feature id' },
45
+ { id: 'verify', label: '/dflow:verify', argHint: 'area or feature id' },
46
+ { id: 'pr-review', label: '/dflow:pr-review', argHint: 'change or branch' },
47
+ { id: 'report-dflow-feedback', label: '/dflow:report-dflow-feedback', argHint: 'issue or improvement' },
48
+ { id: 'status', label: '/dflow:status', argHint: '-' },
49
+ { id: 'next', label: '/dflow:next', argHint: '-' },
50
+ { id: 'cancel', label: '/dflow:cancel', argHint: '-' }
51
+ ]
52
+ }
53
+ ];
26
54
 
27
55
  const PROSE_LANGUAGE_PATTERN =
28
56
  /^[A-Za-z]{2,3}(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|[0-9]{3}))?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*$/;
@@ -179,8 +207,8 @@ async function runInit(options = {}) {
179
207
 
180
208
  const detection = await detectProjectSignals(cwd);
181
209
  const answers = await promptForAnswers(rl, stdout, stderr, detection);
182
- const warnings = [...preflight.warnings, ...buildDetectionWarnings(answers, detection)];
183
210
  const plan = await buildFilePlan(cwd, answers);
211
+ const warnings = [...preflight.warnings, ...buildDetectionWarnings(answers, detection), ...(plan.bundleWarnings || [])];
184
212
 
185
213
  renderPreview(stdout, plan, warnings);
186
214
  const confirmed = await askConfirmation(rl, 'Create these files? (y/N) ');
@@ -253,7 +281,8 @@ async function runConfigureAgents(options = {}) {
253
281
  await assertDflowInitialized(cwd);
254
282
 
255
283
  const projectContext = await inferProjectContext(cwd, rl, stdout, stderr);
256
- const aiAgents = await askAiAgents(rl, stdout, stderr);
284
+ const detectedAgents = await detectConfiguredAgents(cwd);
285
+ const aiAgents = await askAiAgents(rl, stdout, stderr, detectedAgents);
257
286
 
258
287
  if (aiAgents.length === 0) {
259
288
  throw new UserAbort('No AI agents selected. Nothing changed.');
@@ -262,10 +291,12 @@ async function runConfigureAgents(options = {}) {
262
291
  const plan = await buildConfigureAgentsPlan(cwd, {
263
292
  ...projectContext,
264
293
  aiAgents,
265
- commandAdapters: Boolean(options.commandAdapters)
294
+ commandAdapters: Boolean(options.commandAdapters),
295
+ skills: Boolean(options.skills)
266
296
  });
267
297
 
268
- renderPreview(stdout, plan, []);
298
+ const warnings = plan.warnings || [];
299
+ renderPreview(stdout, plan, warnings);
269
300
  const confirmed = await askConfirmation(rl, 'Create these files? (y/N) ');
270
301
 
271
302
  if (!confirmed) {
@@ -276,6 +307,7 @@ async function runConfigureAgents(options = {}) {
276
307
  rl = undefined;
277
308
 
278
309
  const result = await writeFilePlan(cwd, plan);
310
+ result.warnings.unshift(...warnings);
279
311
  result.warnings.push(...collectUnresolvedPlaceholderWarnings(plan, result.created));
280
312
 
281
313
  printResultReport(stdout, result, plan.deferred);
@@ -461,10 +493,32 @@ async function detectProjectSignals(cwd) {
461
493
  return {
462
494
  hasSourceTree: hasSourceTree || relNames.has('src'),
463
495
  trackHint,
464
- stackHints
496
+ stackHints,
497
+ configuredAgents: await detectConfiguredAgents(cwd)
465
498
  };
466
499
  }
467
500
 
501
+ async function detectConfiguredAgents(cwd) {
502
+ // Surface agents this project already has configured so init / configure-agents
503
+ // can default to them instead of re-asking from scratch on every invocation.
504
+ // Order matches AI_AGENT_OPTIONS so the prompt numbering lines up.
505
+ const detected = [];
506
+ if (await pathExists(path.join(cwd, 'AGENTS.md'))) {
507
+ detected.push('agents');
508
+ }
509
+ if (
510
+ (await pathExists(path.join(cwd, 'CLAUDE.md'))) ||
511
+ (await pathExists(path.join(cwd, '.claude/commands/dflow'))) ||
512
+ (await pathExists(path.join(cwd, '.claude/skills/dflow')))
513
+ ) {
514
+ detected.push('claude');
515
+ }
516
+ if (await pathExists(path.join(cwd, '.github/copilot-instructions.md'))) {
517
+ detected.push('copilot');
518
+ }
519
+ return detected;
520
+ }
521
+
468
522
  async function collectProjectFiles(cwd, maxDepth, maxFiles) {
469
523
  const results = [];
470
524
  const ignored = new Set(['.git', 'node_modules', 'bin', 'obj', 'dflow']);
@@ -508,8 +562,8 @@ async function collectProjectFiles(cwd, maxDepth, maxFiles) {
508
562
  function buildDetectionWarnings(answers, detection) {
509
563
  const warnings = [];
510
564
 
511
- if (answers.projectType === 'greenfield' && detection.hasSourceTree) {
512
- warnings.push('Warning: source-tree signals already exist, but project type is Greenfield. Continuing with your selected project type.');
565
+ if (answers.projectType === 'greenfield' && detection.hasSourceTree && !detection.trackHint) {
566
+ 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.');
513
567
  }
514
568
 
515
569
  if (detection.trackHint && answers.projectType !== detection.trackHint) {
@@ -559,7 +613,7 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
559
613
  }
560
614
 
561
615
  const optionalFiles = await askOptionalFiles(rl, stdout, stderr);
562
- const aiAgents = await askAiAgents(rl, stdout, stderr);
616
+ const aiAgents = await askAiAgents(rl, stdout, stderr, detection.configuredAgents || []);
563
617
 
564
618
  return {
565
619
  projectType,
@@ -744,17 +798,24 @@ async function askOptionalFiles(rl, stdout, stderr) {
744
798
  }
745
799
  }
746
800
 
747
- async function askAiAgents(rl, stdout, stderr) {
801
+ async function askAiAgents(rl, stdout, stderr, defaultKeys = []) {
748
802
  let failedAttempts = 0;
803
+ const validDefaults = AI_AGENT_OPTIONS
804
+ .filter((option) => defaultKeys.includes(option.key))
805
+ .map((option) => option.key);
749
806
 
750
807
  while (true) {
751
808
  stdout.write('\nWhich AI coding agents should Dflow configure?\n');
752
809
  AI_AGENT_OPTIONS.forEach((option, index) => {
753
- stdout.write(` ${index + 1}. ${option.label}\n`);
810
+ const marker = validDefaults.includes(option.key) ? ' (currently configured)' : '';
811
+ stdout.write(` ${index + 1}. ${option.label}${marker}\n`);
754
812
  });
755
813
 
756
- const answer = await askLine(rl, 'Enter comma-separated choices or "none" (default: none): ');
757
- const parsed = parseMultiselectAnswer(answer || 'none', AI_AGENT_OPTIONS, []);
814
+ const defaultHint = validDefaults.length > 0
815
+ ? validDefaults.map((key) => AI_AGENT_OPTIONS.findIndex((option) => option.key === key) + 1).join(',')
816
+ : 'none';
817
+ const answer = await askLine(rl, `Enter comma-separated choices or "none" (default: ${defaultHint}): `);
818
+ const parsed = parseMultiselectAnswer(answer, AI_AGENT_OPTIONS, validDefaults);
758
819
 
759
820
  if (!parsed.valid) {
760
821
  failedAttempts += 1;
@@ -985,9 +1046,14 @@ async function buildFilePlan(cwd, answers) {
985
1046
 
986
1047
  await finalizePlanItems(cwd, items);
987
1048
 
1049
+ // Always project the workflow bundle (required for /dflow:* workflows to be reachable).
1050
+ const bundleWarnings = [];
1051
+ await addWorkflowBundleItems(cwd, items, bundleWarnings, answers.edition);
1052
+
988
1053
  return {
989
1054
  items,
990
1055
  deferred: buildDeferredItems(answers.edition),
1056
+ bundleWarnings,
991
1057
  unresolvedInitPlaceholders: Array.from(substitution.entries())
992
1058
  .filter(([placeholder, value]) => placeholder === value)
993
1059
  .map(([placeholder]) => placeholder)
@@ -1000,6 +1066,7 @@ async function buildConfigureAgentsPlan(cwd, answers) {
1000
1066
  optionalFiles: answers.optionalFiles || []
1001
1067
  });
1002
1068
  const items = [];
1069
+ const warnings = [];
1003
1070
 
1004
1071
  let content = await readPackagedTemplate(answers.edition, 'scaffolding/AI-AGENT-GUIDE.md');
1005
1072
  content = substitutePlaceholders(content, substitution);
@@ -1022,9 +1089,24 @@ async function buildConfigureAgentsPlan(cwd, answers) {
1022
1089
 
1023
1090
  await finalizePlanItems(cwd, items);
1024
1091
 
1092
+ if (answers.commandAdapters) {
1093
+ await addLegacyCommandAdapterCleanupItems(cwd, items, answers.aiAgents, warnings);
1094
+ }
1095
+
1096
+ await addSkillAdapterItems(cwd, items, answers.aiAgents, answers.skills, warnings);
1097
+
1098
+ // Project the workflow bundle on configure-agents too, so pre-039 projects (no bundle)
1099
+ // and edition-switch repairs get the runtime references/templates reachable. The function
1100
+ // is idempotent: it skips fresh bundle files, updates Dflow-generated ones, and warns
1101
+ // (without overwriting) on user-modified bundle files.
1102
+ const bundleWarnings = [];
1103
+ await addWorkflowBundleItems(cwd, items, bundleWarnings, answers.edition);
1104
+ warnings.push(...bundleWarnings);
1105
+
1025
1106
  return {
1026
1107
  items,
1027
1108
  deferred: [],
1109
+ warnings,
1028
1110
  unresolvedInitPlaceholders: Array.from(substitution.entries())
1029
1111
  .filter(([placeholder, value]) => placeholder === value)
1030
1112
  .map(([placeholder]) => placeholder)
@@ -1043,6 +1125,183 @@ async function finalizePlanItems(cwd, items) {
1043
1125
  }
1044
1126
  }
1045
1127
 
1128
+ async function listBundleSourceFiles(edition) {
1129
+ const bundleDirs = ['references', 'templates'];
1130
+ const files = [];
1131
+
1132
+ for (const dir of bundleDirs) {
1133
+ const sourceDir = path.join(TEMPLATE_ROOT, edition, dir);
1134
+ let entries;
1135
+ try {
1136
+ entries = await fs.readdir(sourceDir);
1137
+ } catch (error) {
1138
+ if (error.code === 'ENOENT') {
1139
+ continue;
1140
+ }
1141
+ throw error;
1142
+ }
1143
+ for (const entry of entries) {
1144
+ const sourceRel = `${dir}/${entry}`;
1145
+ const sourcePath = path.join(sourceDir, entry);
1146
+ const stat = await fs.stat(sourcePath);
1147
+ if (stat.isFile()) {
1148
+ files.push({ sourceRel, dir, name: entry });
1149
+ }
1150
+ }
1151
+ }
1152
+
1153
+ return files;
1154
+ }
1155
+
1156
+ async function readCurrentBundleManifest(cwd) {
1157
+ const manifestPath = path.join(cwd, WORKFLOW_BUNDLE_MANIFEST_PATH);
1158
+ try {
1159
+ const raw = await fs.readFile(manifestPath, 'utf8');
1160
+ return JSON.parse(raw);
1161
+ } catch {
1162
+ return null;
1163
+ }
1164
+ }
1165
+
1166
+ function buildBundleManifest(edition, version, files) {
1167
+ return {
1168
+ edition,
1169
+ version,
1170
+ generatedBy: 'dflow-sdd-ddd',
1171
+ files: files.map((f) => `${WORKFLOW_BUNDLE_DEST}/${f.sourceRel}`)
1172
+ };
1173
+ }
1174
+
1175
+ function injectBundleMarker(content) {
1176
+ return `${WORKFLOW_BUNDLE_GENERATED_MARKER}\n\n${content}`;
1177
+ }
1178
+
1179
+ async function addWorkflowBundleItems(cwd, items, warnings, edition) {
1180
+ const bundleFiles = await listBundleSourceFiles(edition);
1181
+
1182
+ // Detect any previous manifest to handle edition-switch stale cleanup.
1183
+ const existingManifest = await readCurrentBundleManifest(cwd);
1184
+ const previousEdition = existingManifest ? existingManifest.edition : null;
1185
+
1186
+ // If the edition changed, schedule removal of stale generated files from prior edition.
1187
+ if (previousEdition && previousEdition !== edition) {
1188
+ const staleFiles = existingManifest.files || [];
1189
+ for (const staleRelPath of staleFiles) {
1190
+ const staleAbsPath = path.join(cwd, staleRelPath);
1191
+ let staleExists = false;
1192
+ try {
1193
+ await fs.stat(staleAbsPath);
1194
+ staleExists = true;
1195
+ } catch {
1196
+ staleExists = false;
1197
+ }
1198
+ if (!staleExists) {
1199
+ continue;
1200
+ }
1201
+ const staleContent = await fs.readFile(staleAbsPath, 'utf8');
1202
+ if (!staleContent.includes(WORKFLOW_BUNDLE_GENERATED_MARKER)) {
1203
+ warnings.push(
1204
+ `Edition changed from ${previousEdition} to ${edition}; skipped removal of user-modified bundle file: ${staleRelPath}`
1205
+ );
1206
+ continue;
1207
+ }
1208
+ // Check if this path is also in the new edition bundle — if so, it will be overwritten, not removed.
1209
+ const newRelPaths = new Set(bundleFiles.map((f) => `${WORKFLOW_BUNDLE_DEST}/${f.sourceRel}`));
1210
+ if (!newRelPaths.has(staleRelPath)) {
1211
+ items.push({
1212
+ relativePath: staleRelPath,
1213
+ source: `stale-bundle:${previousEdition}`,
1214
+ notes: `stale workflow bundle file from ${previousEdition} edition`,
1215
+ action: 'remove',
1216
+ size: Buffer.byteLength(staleContent, 'utf8'),
1217
+ expectedContent: staleContent
1218
+ });
1219
+ }
1220
+ }
1221
+ }
1222
+
1223
+ // Build items for current edition bundle files.
1224
+ for (const { sourceRel } of bundleFiles) {
1225
+ const relativePath = `${WORKFLOW_BUNDLE_DEST}/${sourceRel}`;
1226
+ const absolutePath = path.join(cwd, relativePath);
1227
+ const sourceContent = await readPackagedBundleFile(edition, sourceRel);
1228
+ const content = injectBundleMarker(sourceContent);
1229
+
1230
+ let action;
1231
+ let notes = 'workflow bundle';
1232
+ const targetExists = await pathExists(absolutePath);
1233
+
1234
+ if (targetExists) {
1235
+ const existingContent = await fs.readFile(absolutePath, 'utf8');
1236
+ if (existingContent.includes(WORKFLOW_BUNDLE_GENERATED_MARKER)) {
1237
+ action = 'update';
1238
+ } else {
1239
+ action = 'skip';
1240
+ notes = 'workflow bundle, user-modified — skipped; remove or rename to let Dflow manage';
1241
+ warnings.push(
1242
+ `Existing ${relativePath} is not a Dflow-generated bundle file; left unchanged. Remove or rename it to let Dflow manage this file.`
1243
+ );
1244
+ }
1245
+ } else {
1246
+ action = 'create';
1247
+ }
1248
+
1249
+ items.push({
1250
+ relativePath,
1251
+ source: `packaged-bundle:${edition}/${sourceRel}`,
1252
+ notes,
1253
+ content,
1254
+ action,
1255
+ overwrite: action === 'update',
1256
+ size: Buffer.byteLength(content, 'utf8')
1257
+ });
1258
+ }
1259
+
1260
+ // Add the manifest file.
1261
+ const manifestContent = JSON.stringify(
1262
+ buildBundleManifest(edition, pkg.version, bundleFiles),
1263
+ null,
1264
+ 2
1265
+ ) + '\n';
1266
+ const manifestExists = await pathExists(path.join(cwd, WORKFLOW_BUNDLE_MANIFEST_PATH));
1267
+
1268
+ items.push({
1269
+ relativePath: WORKFLOW_BUNDLE_MANIFEST_PATH,
1270
+ source: `generated:workflow-bundle-manifest`,
1271
+ notes: 'workflow bundle manifest',
1272
+ content: manifestContent,
1273
+ action: manifestExists ? 'update' : 'create',
1274
+ overwrite: true,
1275
+ size: Buffer.byteLength(manifestContent, 'utf8')
1276
+ });
1277
+ }
1278
+
1279
+ async function readPackagedBundleFile(edition, sourceRel) {
1280
+ const filePath = path.join(TEMPLATE_ROOT, edition, sourceRel);
1281
+ const normalizedRoot = path.resolve(TEMPLATE_ROOT, edition);
1282
+ const normalizedFilePath = path.resolve(filePath);
1283
+
1284
+ if (!normalizedFilePath.startsWith(`${normalizedRoot}${path.sep}`)) {
1285
+ throw new InitError(`Internal error: packaged bundle file not found: templates/${edition}/${sourceRel}`);
1286
+ }
1287
+
1288
+ let buffer;
1289
+ try {
1290
+ buffer = await fs.readFile(normalizedFilePath);
1291
+ } catch (error) {
1292
+ if (error.code === 'ENOENT') {
1293
+ throw new InitError(`Internal error: packaged bundle file not found: templates/${edition}/${sourceRel}`);
1294
+ }
1295
+ throw new InitError(`Internal error: cannot read packaged bundle file: templates/${edition}/${sourceRel}`);
1296
+ }
1297
+
1298
+ try {
1299
+ return new TextDecoder('utf-8', { fatal: true }).decode(buffer);
1300
+ } catch {
1301
+ throw new InitError(`Internal error: invalid UTF-8 packaged bundle file: templates/${edition}/${sourceRel}`);
1302
+ }
1303
+ }
1304
+
1046
1305
  async function addAiAgentShim(cwd, items, agent, substitution, options = {}) {
1047
1306
  const target = getAiAgentTarget(agent);
1048
1307
  const targetPath = path.join(cwd, target.relativePath);
@@ -1122,10 +1381,11 @@ This project uses Dflow for spec-first AI-assisted development.
1122
1381
 
1123
1382
  Before planning or editing code, read and follow:
1124
1383
 
1125
- - \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
1384
+ - \`dflow/specs/shared/AI-AGENT-GUIDE.md\` — command registry, routing rules, and project context.
1385
+ - \`dflow/specs/shared/dflow-workflows/\` — vendored workflow bundle with executable step definitions.
1126
1386
 
1127
- Keep tool-specific instruction files small. The Dflow guide above is the
1128
- single source of truth for project workflow rules, slash-command behavior,
1387
+ Keep tool-specific instruction files small. The guide and workflow bundle are
1388
+ the authoritative sources for Dflow workflow rules, slash-command behavior,
1129
1389
  spec locations, and SDD/DDD constraints.${commandTriggerHint}${importHint}
1130
1390
  `;
1131
1391
  }
@@ -1156,6 +1416,134 @@ function addCommandAdapterItems(items, aiAgents, commandRegistry) {
1156
1416
  }
1157
1417
  }
1158
1418
 
1419
+ async function addLegacyCommandAdapterCleanupItems(cwd, items, aiAgents, warnings) {
1420
+ for (const legacy of LEGACY_COMMAND_ADAPTERS) {
1421
+ if (!aiAgents.includes(legacy.agent)) {
1422
+ continue;
1423
+ }
1424
+
1425
+ for (const command of legacy.commands) {
1426
+ const relativePath = legacy.pathPattern.replace('<id>', command.id);
1427
+ const targetPath = path.join(cwd, relativePath);
1428
+ let stats;
1429
+
1430
+ try {
1431
+ stats = await fs.stat(targetPath);
1432
+ } catch (error) {
1433
+ if (error.code === 'ENOENT') {
1434
+ continue;
1435
+ }
1436
+ throw error;
1437
+ }
1438
+
1439
+ if (!stats.isFile()) {
1440
+ warnings.push(`Found legacy Dflow command adapter path but it is not a file; not removed: ${relativePath}`);
1441
+ continue;
1442
+ }
1443
+
1444
+ const content = await fs.readFile(targetPath, 'utf8');
1445
+ const expectedContent = buildLegacyCommandAdapterFingerprint(legacy, command);
1446
+ if (normalizeCommandAdapterFingerprint(content) !== normalizeCommandAdapterFingerprint(expectedContent)) {
1447
+ warnings.push(`Found legacy Dflow command adapter with non-generated content; not removed: ${relativePath}. Inspect it manually before deleting.`);
1448
+ continue;
1449
+ }
1450
+
1451
+ items.push({
1452
+ relativePath,
1453
+ source: legacy.source,
1454
+ notes: `stale dflow adapter generated by Dflow ${legacy.version}`,
1455
+ action: 'remove',
1456
+ size: Buffer.byteLength(content, 'utf8'),
1457
+ expectedContent
1458
+ });
1459
+ }
1460
+ }
1461
+ }
1462
+
1463
+ // Edition-agnostic thin shell: a single canonical source at
1464
+ // templates/common/skill/SKILL.md (PROPOSAL-041 C1). Returns the file
1465
+ // verbatim — frontmatter, marker, and body all live in the source file so
1466
+ // the skill content can be edited without touching JS.
1467
+ async function buildDflowSkillAdapter() {
1468
+ const sourcePath = path.join(TEMPLATE_ROOT, COMMON_SKILL_SOURCE_REL);
1469
+ return fs.readFile(sourcePath, 'utf8');
1470
+ }
1471
+
1472
+ async function addSkillAdapterItems(cwd, items, aiAgents, skills, warnings) {
1473
+ if (!skills) {
1474
+ return;
1475
+ }
1476
+
1477
+ if (!aiAgents.includes('claude')) {
1478
+ warnings.push(
1479
+ 'The --skills flag currently supports Claude Code only; no skill adapter was generated because Claude Code was not a selected target.'
1480
+ );
1481
+ return;
1482
+ }
1483
+
1484
+ const relativePath = '.claude/skills/dflow/SKILL.md';
1485
+ const targetPath = path.join(cwd, relativePath);
1486
+
1487
+ // The thin skill is edition-neutral (it only points to the per-edition guide),
1488
+ // so there is nothing edition-specific to go stale — re-running just rewrites
1489
+ // the same marker-guarded file (idempotent). No LEGACY skill set exists yet
1490
+ // (skills are new in PROPOSAL-038); future skill cleanup would extend the same
1491
+ // LEGACY_* / addLegacyCommandAdapterCleanupItems marker-fingerprint pattern.
1492
+ let existingContent;
1493
+ try {
1494
+ existingContent = await fs.readFile(targetPath, 'utf8');
1495
+ } catch (error) {
1496
+ if (error.code !== 'ENOENT') {
1497
+ throw error;
1498
+ }
1499
+ existingContent = undefined;
1500
+ }
1501
+
1502
+ if (existingContent !== undefined && !existingContent.includes(SKILL_ADAPTER_GENERATED_MARKER)) {
1503
+ warnings.push(
1504
+ 'Existing .claude/skills/dflow/SKILL.md is not a Dflow-generated skill; left unchanged. Remove or rename it to let Dflow manage this skill.'
1505
+ );
1506
+ return;
1507
+ }
1508
+
1509
+ const skillContent = await buildDflowSkillAdapter();
1510
+ items.push({
1511
+ relativePath,
1512
+ source: 'generated:claude-skill-adapter',
1513
+ notes: 'skill adapter, thin skill pointing to AI-AGENT-GUIDE.md',
1514
+ content: skillContent,
1515
+ size: Buffer.byteLength(skillContent, 'utf8'),
1516
+ overwrite: true
1517
+ });
1518
+ }
1519
+
1520
+ function buildLegacyCommandAdapterFingerprint(legacy, command) {
1521
+ if (legacy.version === '0.5.0' && legacy.fingerprint === 'v0.5.0 buildThinCommandWrapper') {
1522
+ return buildLegacyV050ThinCommandWrapper(command);
1523
+ }
1524
+
1525
+ throw new InitError(`Internal error: unsupported legacy command adapter fingerprint: ${legacy.version}`);
1526
+ }
1527
+
1528
+ function buildLegacyV050ThinCommandWrapper(command) {
1529
+ const argHint = command.argHint === '-'
1530
+ ? 'Argument hint: none.'
1531
+ : `Argument hint: ${command.argHint}.`;
1532
+
1533
+ return `# /dflow-${command.id}
1534
+
1535
+ Execute the canonical \`${command.label}\` Dflow workflow or control command.
1536
+
1537
+ Definition: \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
1538
+
1539
+ ${argHint}
1540
+ `;
1541
+ }
1542
+
1543
+ function normalizeCommandAdapterFingerprint(content) {
1544
+ return String(content).replace(/\r\n/g, '\n');
1545
+ }
1546
+
1159
1547
  function buildCodexCommandTriggerSection(commandRegistry) {
1160
1548
  const triggers = commandRegistry
1161
1549
  .map((command) => {
@@ -1190,9 +1578,12 @@ function buildThinCommandWrapper(command, displayName = command.label) {
1190
1578
 
1191
1579
  return `# ${displayName}
1192
1580
 
1581
+ ${COMMAND_ADAPTER_GENERATED_MARKER}
1582
+
1193
1583
  Execute the canonical \`${command.label}\` Dflow workflow or control command.
1194
1584
 
1195
- Definition: \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
1585
+ Registry and rules: \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
1586
+ Workflow steps: \`dflow/specs/shared/dflow-workflows/\`
1196
1587
 
1197
1588
  ${argHint}
1198
1589
  `;
@@ -1651,6 +2042,7 @@ async function writeFilePlan(cwd, plan) {
1651
2042
  const result = {
1652
2043
  created: [],
1653
2044
  updated: [],
2045
+ removed: [],
1654
2046
  skipped: [],
1655
2047
  warnings: []
1656
2048
  };
@@ -1659,6 +2051,37 @@ async function writeFilePlan(cwd, plan) {
1659
2051
  const targetPath = path.join(cwd, item.relativePath);
1660
2052
 
1661
2053
  try {
2054
+ if (item.action === 'remove') {
2055
+ let stats;
2056
+ try {
2057
+ stats = await fs.stat(targetPath);
2058
+ } catch (error) {
2059
+ if (error.code === 'ENOENT') {
2060
+ result.skipped.push(item.relativePath);
2061
+ result.warnings.push(`Skipped missing stale adapter: ${item.relativePath}`);
2062
+ continue;
2063
+ }
2064
+ throw error;
2065
+ }
2066
+
2067
+ if (!stats.isFile()) {
2068
+ result.skipped.push(item.relativePath);
2069
+ result.warnings.push(`Skipped stale adapter removal because target is not a file: ${item.relativePath}`);
2070
+ continue;
2071
+ }
2072
+
2073
+ const currentContent = await fs.readFile(targetPath, 'utf8');
2074
+ if (normalizeCommandAdapterFingerprint(currentContent) !== normalizeCommandAdapterFingerprint(item.expectedContent || '')) {
2075
+ result.skipped.push(item.relativePath);
2076
+ result.warnings.push(`Skipped stale adapter removal because content changed after preview: ${item.relativePath}`);
2077
+ continue;
2078
+ }
2079
+
2080
+ await fs.unlink(targetPath);
2081
+ result.removed.push(item.relativePath);
2082
+ continue;
2083
+ }
2084
+
1662
2085
  if (await pathExists(targetPath)) {
1663
2086
  if (item.overwrite) {
1664
2087
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
@@ -1710,9 +2133,11 @@ async function writeFilePlan(cwd, plan) {
1710
2133
  );
1711
2134
  }
1712
2135
 
1713
- const message = error.code === 'ENOTDIR' || error.code === 'EEXIST'
1714
- ? `Cannot create parent directory for ${item.relativePath}: a parent path is a file.`
1715
- : `Write failed while creating ${item.relativePath}: ${error.message}`;
2136
+ const message = item.action === 'remove'
2137
+ ? `Write failed while removing ${item.relativePath}: ${error.message}`
2138
+ : error.code === 'ENOTDIR' || error.code === 'EEXIST'
2139
+ ? `Cannot create parent directory for ${item.relativePath}: a parent path is a file.`
2140
+ : `Write failed while creating ${item.relativePath}: ${error.message}`;
1716
2141
  throw new WritePhaseError(message, result);
1717
2142
  }
1718
2143
  }
@@ -1729,14 +2154,17 @@ function collectUnresolvedPlaceholderWarnings(plan, createdPaths) {
1729
2154
  return [];
1730
2155
  }
1731
2156
 
2157
+ const placeholderFiles = new Set();
1732
2158
  for (const item of plan.items) {
1733
2159
  if (!createdSet.has(item.relativePath)) {
1734
2160
  continue;
1735
2161
  }
1736
2162
  const matches = item.content.match(/{[^{}\n]+}/g) || [];
1737
- matches
1738
- .filter((match) => unresolvedInitPlaceholders.has(match))
1739
- .forEach((match) => placeholders.add(match));
2163
+ const hits = matches.filter((match) => unresolvedInitPlaceholders.has(match));
2164
+ hits.forEach((match) => placeholders.add(match));
2165
+ if (hits.length > 0) {
2166
+ placeholderFiles.add(item.relativePath);
2167
+ }
1740
2168
  }
1741
2169
 
1742
2170
  if (placeholders.size === 0) {
@@ -1746,7 +2174,10 @@ function collectUnresolvedPlaceholderWarnings(plan, createdPaths) {
1746
2174
  const sorted = Array.from(placeholders).sort();
1747
2175
  const shown = sorted.slice(0, 25).join(', ');
1748
2176
  const suffix = sorted.length > 25 ? `, and ${sorted.length - 25} more` : '';
1749
- return [`Unresolved placeholders remain for later SDD workflows: ${shown}${suffix}.`];
2177
+ const files = Array.from(placeholderFiles).sort();
2178
+ const filesShown = files.slice(0, 10).join(', ');
2179
+ const filesSuffix = files.length > 10 ? `, and ${files.length - 10} more` : '';
2180
+ return [`Unresolved placeholders remain for later SDD workflows: ${shown}${suffix}. Fill them in (or leave for the workflow to resolve) in: ${filesShown}${filesSuffix}.`];
1750
2181
  }
1751
2182
 
1752
2183
  function printResultReport(stdout, result, deferred) {
@@ -1756,6 +2187,9 @@ function printResultReport(stdout, result, deferred) {
1756
2187
  stdout.write('\nUpdated:\n');
1757
2188
  printList(stdout, result.updated);
1758
2189
 
2190
+ stdout.write('\nRemoved:\n');
2191
+ printList(stdout, result.removed);
2192
+
1759
2193
  stdout.write('\nSkipped:\n');
1760
2194
  printList(stdout, result.skipped);
1761
2195
 
@@ -1782,7 +2216,7 @@ Recommended next steps:
1782
2216
 
1783
2217
  function printConfigureAgentsNextSteps(stdout, commandAdapters = false) {
1784
2218
  const commandAdapterStep = commandAdapters
1785
- ? '- Command adapters use tool-specific invocation names: Claude Code `/dflow:<id>`; GitHub Copilot prompt menu `/dflow-<id>` or canonical `/dflow:<id>` as text; Codex CLI plain text without a slash, such as `dflow:status`. Canonical `/dflow:*` names remain defined in dflow/specs/shared/AI-AGENT-GUIDE.md. If upgrading from Dflow 0.5.0, manually remove stale `.claude/commands/dflow/dflow-*.md` files so Claude Code does not show both old and new command names.\n'
2219
+ ? '- Command adapters use tool-specific invocation names: Claude Code `/dflow:<id>`; GitHub Copilot prompt menu `/dflow-<id>` or canonical `/dflow:<id>` as text; Codex CLI plain text without a slash, such as `dflow:status`. Canonical `/dflow:*` names remain defined in dflow/specs/shared/AI-AGENT-GUIDE.md. If upgrading from Dflow 0.5.0, stale `.claude/commands/dflow/dflow-*.md` files generated by 0.5.0 are detected and listed for removal in the confirmation preview, so Claude Code does not show both old and new command names; edited or non-Dflow files are kept with a warning.\n'
1786
2220
  : '';
1787
2221
 
1788
2222
  stdout.write(`
@@ -1860,6 +2294,9 @@ function normalizePath(value) {
1860
2294
  }
1861
2295
 
1862
2296
  function formatBytes(bytes) {
2297
+ if (!Number.isFinite(bytes)) {
2298
+ return '—';
2299
+ }
1863
2300
  if (bytes < 1024) {
1864
2301
  return `${bytes} B`;
1865
2302
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dflow-sdd-ddd",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Spec-first SDD/DDD workflow kit for AI-assisted development",
5
5
  "type": "commonjs",
6
6
  "bin": {