dflow-sdd-ddd 0.5.0 → 0.7.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.
package/lib/init.js CHANGED
@@ -10,6 +10,8 @@ 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 -->';
13
15
  const EXPECTED_COMMAND_IDS = [
14
16
  'new-feature',
15
17
  'modify-existing',
@@ -23,6 +25,28 @@ const EXPECTED_COMMAND_IDS = [
23
25
  'next',
24
26
  'cancel'
25
27
  ];
28
+ const LEGACY_COMMAND_ADAPTERS = [
29
+ {
30
+ version: '0.5.0',
31
+ agent: 'claude',
32
+ source: 'generated:legacy-claude-command-adapter-v0.5.0',
33
+ pathPattern: '.claude/commands/dflow/dflow-<id>.md',
34
+ fingerprint: 'v0.5.0 buildThinCommandWrapper',
35
+ commands: [
36
+ { id: 'new-feature', label: '/dflow:new-feature', argHint: 'feature request' },
37
+ { id: 'modify-existing', label: '/dflow:modify-existing', argHint: 'change request' },
38
+ { id: 'bug-fix', label: '/dflow:bug-fix', argHint: 'expected vs actual' },
39
+ { id: 'new-phase', label: '/dflow:new-phase', argHint: 'feature id or phase goal' },
40
+ { id: 'finish-feature', label: '/dflow:finish-feature', argHint: 'feature id' },
41
+ { id: 'verify', label: '/dflow:verify', argHint: 'area or feature id' },
42
+ { id: 'pr-review', label: '/dflow:pr-review', argHint: 'change or branch' },
43
+ { id: 'report-dflow-feedback', label: '/dflow:report-dflow-feedback', argHint: 'issue or improvement' },
44
+ { id: 'status', label: '/dflow:status', argHint: '-' },
45
+ { id: 'next', label: '/dflow:next', argHint: '-' },
46
+ { id: 'cancel', label: '/dflow:cancel', argHint: '-' }
47
+ ]
48
+ }
49
+ ];
26
50
 
27
51
  const PROSE_LANGUAGE_PATTERN =
28
52
  /^[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}))*$/;
@@ -262,10 +286,12 @@ async function runConfigureAgents(options = {}) {
262
286
  const plan = await buildConfigureAgentsPlan(cwd, {
263
287
  ...projectContext,
264
288
  aiAgents,
265
- commandAdapters: Boolean(options.commandAdapters)
289
+ commandAdapters: Boolean(options.commandAdapters),
290
+ skills: Boolean(options.skills)
266
291
  });
267
292
 
268
- renderPreview(stdout, plan, []);
293
+ const warnings = plan.warnings || [];
294
+ renderPreview(stdout, plan, warnings);
269
295
  const confirmed = await askConfirmation(rl, 'Create these files? (y/N) ');
270
296
 
271
297
  if (!confirmed) {
@@ -276,6 +302,7 @@ async function runConfigureAgents(options = {}) {
276
302
  rl = undefined;
277
303
 
278
304
  const result = await writeFilePlan(cwd, plan);
305
+ result.warnings.unshift(...warnings);
279
306
  result.warnings.push(...collectUnresolvedPlaceholderWarnings(plan, result.created));
280
307
 
281
308
  printResultReport(stdout, result, plan.deferred);
@@ -1000,6 +1027,7 @@ async function buildConfigureAgentsPlan(cwd, answers) {
1000
1027
  optionalFiles: answers.optionalFiles || []
1001
1028
  });
1002
1029
  const items = [];
1030
+ const warnings = [];
1003
1031
 
1004
1032
  let content = await readPackagedTemplate(answers.edition, 'scaffolding/AI-AGENT-GUIDE.md');
1005
1033
  content = substitutePlaceholders(content, substitution);
@@ -1022,9 +1050,16 @@ async function buildConfigureAgentsPlan(cwd, answers) {
1022
1050
 
1023
1051
  await finalizePlanItems(cwd, items);
1024
1052
 
1053
+ if (answers.commandAdapters) {
1054
+ await addLegacyCommandAdapterCleanupItems(cwd, items, answers.aiAgents, warnings);
1055
+ }
1056
+
1057
+ await addSkillAdapterItems(cwd, items, answers.aiAgents, answers.skills, warnings);
1058
+
1025
1059
  return {
1026
1060
  items,
1027
1061
  deferred: [],
1062
+ warnings,
1028
1063
  unresolvedInitPlaceholders: Array.from(substitution.entries())
1029
1064
  .filter(([placeholder, value]) => placeholder === value)
1030
1065
  .map(([placeholder]) => placeholder)
@@ -1134,10 +1169,10 @@ function addCommandAdapterItems(items, aiAgents, commandRegistry) {
1134
1169
  if (aiAgents.includes('claude')) {
1135
1170
  for (const command of commandRegistry) {
1136
1171
  items.push({
1137
- relativePath: `.claude/commands/dflow/dflow-${command.id}.md`,
1172
+ relativePath: `.claude/commands/dflow/${command.id}.md`,
1138
1173
  source: 'generated:claude-command-adapter',
1139
1174
  notes: 'command adapter, derived from dflow command registry',
1140
- content: buildThinCommandWrapper(command),
1175
+ content: buildThinCommandWrapper(command, `/dflow:${command.id}`),
1141
1176
  overwrite: true
1142
1177
  });
1143
1178
  }
@@ -1149,13 +1184,165 @@ function addCommandAdapterItems(items, aiAgents, commandRegistry) {
1149
1184
  relativePath: `.github/prompts/dflow-${command.id}.prompt.md`,
1150
1185
  source: 'generated:copilot-command-adapter',
1151
1186
  notes: 'command adapter, derived from dflow command registry',
1152
- content: buildThinCommandWrapper(command),
1187
+ content: buildThinCommandWrapper(command, `/dflow-${command.id}`),
1153
1188
  overwrite: true
1154
1189
  });
1155
1190
  }
1156
1191
  }
1157
1192
  }
1158
1193
 
1194
+ async function addLegacyCommandAdapterCleanupItems(cwd, items, aiAgents, warnings) {
1195
+ for (const legacy of LEGACY_COMMAND_ADAPTERS) {
1196
+ if (!aiAgents.includes(legacy.agent)) {
1197
+ continue;
1198
+ }
1199
+
1200
+ for (const command of legacy.commands) {
1201
+ const relativePath = legacy.pathPattern.replace('<id>', command.id);
1202
+ const targetPath = path.join(cwd, relativePath);
1203
+ let stats;
1204
+
1205
+ try {
1206
+ stats = await fs.stat(targetPath);
1207
+ } catch (error) {
1208
+ if (error.code === 'ENOENT') {
1209
+ continue;
1210
+ }
1211
+ throw error;
1212
+ }
1213
+
1214
+ if (!stats.isFile()) {
1215
+ warnings.push(`Found legacy Dflow command adapter path but it is not a file; not removed: ${relativePath}`);
1216
+ continue;
1217
+ }
1218
+
1219
+ const content = await fs.readFile(targetPath, 'utf8');
1220
+ const expectedContent = buildLegacyCommandAdapterFingerprint(legacy, command);
1221
+ if (normalizeCommandAdapterFingerprint(content) !== normalizeCommandAdapterFingerprint(expectedContent)) {
1222
+ warnings.push(`Found legacy Dflow command adapter with non-generated content; not removed: ${relativePath}. Inspect it manually before deleting.`);
1223
+ continue;
1224
+ }
1225
+
1226
+ items.push({
1227
+ relativePath,
1228
+ source: legacy.source,
1229
+ notes: `stale dflow adapter generated by Dflow ${legacy.version}`,
1230
+ action: 'remove',
1231
+ size: Buffer.byteLength(content, 'utf8'),
1232
+ expectedContent
1233
+ });
1234
+ }
1235
+ }
1236
+ }
1237
+
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
+ `;
1271
+ }
1272
+
1273
+ async function addSkillAdapterItems(cwd, items, aiAgents, skills, warnings) {
1274
+ if (!skills) {
1275
+ return;
1276
+ }
1277
+
1278
+ if (!aiAgents.includes('claude')) {
1279
+ warnings.push(
1280
+ 'The --skills flag currently supports Claude Code only; no skill adapter was generated because Claude Code was not a selected target.'
1281
+ );
1282
+ return;
1283
+ }
1284
+
1285
+ const relativePath = '.claude/skills/dflow/SKILL.md';
1286
+ const targetPath = path.join(cwd, relativePath);
1287
+
1288
+ // The thin skill is edition-neutral (it only points to the per-edition guide),
1289
+ // so there is nothing edition-specific to go stale — re-running just rewrites
1290
+ // the same marker-guarded file (idempotent). No LEGACY skill set exists yet
1291
+ // (skills are new in PROPOSAL-038); future skill cleanup would extend the same
1292
+ // LEGACY_* / addLegacyCommandAdapterCleanupItems marker-fingerprint pattern.
1293
+ let existingContent;
1294
+ try {
1295
+ existingContent = await fs.readFile(targetPath, 'utf8');
1296
+ } catch (error) {
1297
+ if (error.code !== 'ENOENT') {
1298
+ throw error;
1299
+ }
1300
+ existingContent = undefined;
1301
+ }
1302
+
1303
+ if (existingContent !== undefined && !existingContent.includes(SKILL_ADAPTER_GENERATED_MARKER)) {
1304
+ warnings.push(
1305
+ 'Existing .claude/skills/dflow/SKILL.md is not a Dflow-generated skill; left unchanged. Remove or rename it to let Dflow manage this skill.'
1306
+ );
1307
+ return;
1308
+ }
1309
+
1310
+ items.push({
1311
+ relativePath,
1312
+ source: 'generated:claude-skill-adapter',
1313
+ notes: 'skill adapter, thin skill pointing to AI-AGENT-GUIDE.md',
1314
+ content: buildDflowSkillAdapter(),
1315
+ overwrite: true
1316
+ });
1317
+ }
1318
+
1319
+ function buildLegacyCommandAdapterFingerprint(legacy, command) {
1320
+ if (legacy.version === '0.5.0' && legacy.fingerprint === 'v0.5.0 buildThinCommandWrapper') {
1321
+ return buildLegacyV050ThinCommandWrapper(command);
1322
+ }
1323
+
1324
+ throw new InitError(`Internal error: unsupported legacy command adapter fingerprint: ${legacy.version}`);
1325
+ }
1326
+
1327
+ function buildLegacyV050ThinCommandWrapper(command) {
1328
+ const argHint = command.argHint === '-'
1329
+ ? 'Argument hint: none.'
1330
+ : `Argument hint: ${command.argHint}.`;
1331
+
1332
+ return `# /dflow-${command.id}
1333
+
1334
+ Execute the canonical \`${command.label}\` Dflow workflow or control command.
1335
+
1336
+ Definition: \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
1337
+
1338
+ ${argHint}
1339
+ `;
1340
+ }
1341
+
1342
+ function normalizeCommandAdapterFingerprint(content) {
1343
+ return String(content).replace(/\r\n/g, '\n');
1344
+ }
1345
+
1159
1346
  function buildCodexCommandTriggerSection(commandRegistry) {
1160
1347
  const triggers = commandRegistry
1161
1348
  .map((command) => {
@@ -1172,18 +1359,25 @@ Codex does not install Dflow command files. When the developer asks for a
1172
1359
  canonical Dflow command, treat it as a text trigger, read the canonical guide,
1173
1360
  and execute the matching workflow or control command.
1174
1361
 
1362
+ If the CLI intercepts a slash-prefixed Dflow name such as \`/dflow:status\` as
1363
+ an unknown command, the developer may resend it without the slash, for example
1364
+ \`dflow:status\`. Treat that as the same Dflow text trigger, read the guide,
1365
+ and execute it.
1366
+
1175
1367
  Recognized canonical triggers:
1176
1368
 
1177
1369
  ${triggers}
1178
1370
  `;
1179
1371
  }
1180
1372
 
1181
- function buildThinCommandWrapper(command) {
1373
+ function buildThinCommandWrapper(command, displayName = command.label) {
1182
1374
  const argHint = command.argHint === '-'
1183
1375
  ? 'Argument hint: none.'
1184
1376
  : `Argument hint: ${command.argHint}.`;
1185
1377
 
1186
- return `# /dflow-${command.id}
1378
+ return `# ${displayName}
1379
+
1380
+ ${COMMAND_ADAPTER_GENERATED_MARKER}
1187
1381
 
1188
1382
  Execute the canonical \`${command.label}\` Dflow workflow or control command.
1189
1383
 
@@ -1646,6 +1840,7 @@ async function writeFilePlan(cwd, plan) {
1646
1840
  const result = {
1647
1841
  created: [],
1648
1842
  updated: [],
1843
+ removed: [],
1649
1844
  skipped: [],
1650
1845
  warnings: []
1651
1846
  };
@@ -1654,6 +1849,37 @@ async function writeFilePlan(cwd, plan) {
1654
1849
  const targetPath = path.join(cwd, item.relativePath);
1655
1850
 
1656
1851
  try {
1852
+ if (item.action === 'remove') {
1853
+ let stats;
1854
+ try {
1855
+ stats = await fs.stat(targetPath);
1856
+ } catch (error) {
1857
+ if (error.code === 'ENOENT') {
1858
+ result.skipped.push(item.relativePath);
1859
+ result.warnings.push(`Skipped missing stale adapter: ${item.relativePath}`);
1860
+ continue;
1861
+ }
1862
+ throw error;
1863
+ }
1864
+
1865
+ if (!stats.isFile()) {
1866
+ result.skipped.push(item.relativePath);
1867
+ result.warnings.push(`Skipped stale adapter removal because target is not a file: ${item.relativePath}`);
1868
+ continue;
1869
+ }
1870
+
1871
+ const currentContent = await fs.readFile(targetPath, 'utf8');
1872
+ if (normalizeCommandAdapterFingerprint(currentContent) !== normalizeCommandAdapterFingerprint(item.expectedContent || '')) {
1873
+ result.skipped.push(item.relativePath);
1874
+ result.warnings.push(`Skipped stale adapter removal because content changed after preview: ${item.relativePath}`);
1875
+ continue;
1876
+ }
1877
+
1878
+ await fs.unlink(targetPath);
1879
+ result.removed.push(item.relativePath);
1880
+ continue;
1881
+ }
1882
+
1657
1883
  if (await pathExists(targetPath)) {
1658
1884
  if (item.overwrite) {
1659
1885
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
@@ -1705,9 +1931,11 @@ async function writeFilePlan(cwd, plan) {
1705
1931
  );
1706
1932
  }
1707
1933
 
1708
- const message = error.code === 'ENOTDIR' || error.code === 'EEXIST'
1709
- ? `Cannot create parent directory for ${item.relativePath}: a parent path is a file.`
1710
- : `Write failed while creating ${item.relativePath}: ${error.message}`;
1934
+ const message = item.action === 'remove'
1935
+ ? `Write failed while removing ${item.relativePath}: ${error.message}`
1936
+ : error.code === 'ENOTDIR' || error.code === 'EEXIST'
1937
+ ? `Cannot create parent directory for ${item.relativePath}: a parent path is a file.`
1938
+ : `Write failed while creating ${item.relativePath}: ${error.message}`;
1711
1939
  throw new WritePhaseError(message, result);
1712
1940
  }
1713
1941
  }
@@ -1751,6 +1979,9 @@ function printResultReport(stdout, result, deferred) {
1751
1979
  stdout.write('\nUpdated:\n');
1752
1980
  printList(stdout, result.updated);
1753
1981
 
1982
+ stdout.write('\nRemoved:\n');
1983
+ printList(stdout, result.removed);
1984
+
1754
1985
  stdout.write('\nSkipped:\n');
1755
1986
  printList(stdout, result.skipped);
1756
1987
 
@@ -1777,7 +2008,7 @@ Recommended next steps:
1777
2008
 
1778
2009
  function printConfigureAgentsNextSteps(stdout, commandAdapters = false) {
1779
2010
  const commandAdapterStep = commandAdapters
1780
- ? '- Command adapters use adapter-native names such as `/dflow-new-feature`; canonical `/dflow:*` names remain defined in dflow/specs/shared/AI-AGENT-GUIDE.md.\n'
2011
+ ? '- 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'
1781
2012
  : '';
1782
2013
 
1783
2014
  stdout.write(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dflow-sdd-ddd",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Spec-first SDD/DDD workflow kit for AI-assisted development",
5
5
  "type": "commonjs",
6
6
  "bin": {