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.
- package/CHANGELOG.md +65 -0
- package/README.en.md +30 -0
- package/README.md +14 -0
- package/bin/dflow.js +7 -3
- package/docs/evaluating-dflow.en.md +14 -5
- package/docs/evaluating-dflow.md +14 -5
- package/docs/npm-publish-checklist.md +8 -0
- package/docs/using-with-claude-code.en.md +118 -11
- package/docs/using-with-claude-code.md +94 -9
- package/docs/using-with-codex.en.md +14 -0
- package/docs/using-with-codex.md +10 -0
- package/docs/using-with-github-copilot.en.md +36 -0
- package/docs/using-with-github-copilot.md +26 -0
- package/lib/init.js +461 -24
- package/package.json +1 -1
- package/templates/brownfield/references/dflow-feedback-flow.md +179 -0
- package/templates/brownfield/references/drift-verification.md +183 -0
- package/templates/brownfield/references/finish-feature-flow.md +259 -0
- package/templates/brownfield/references/git-integration.md +312 -0
- package/templates/brownfield/references/init-project-flow.md +413 -0
- package/templates/brownfield/references/modify-existing-flow.md +444 -0
- package/templates/brownfield/references/new-feature-flow.md +367 -0
- package/templates/brownfield/references/new-phase-flow.md +259 -0
- package/templates/brownfield/references/pr-review-checklist.md +179 -0
- package/templates/brownfield/scaffolding/AI-AGENT-GUIDE.md +31 -4
- package/templates/brownfield/scaffolding/CLAUDE-md-snippet.md +12 -8
- package/templates/brownfield/scaffolding/Git-principles-gitflow.md +1 -1
- package/templates/brownfield/scaffolding/Git-principles-trunk.md +1 -1
- package/templates/brownfield/scaffolding/_conventions.md +1 -1
- package/templates/brownfield/scaffolding/_overview.md +3 -3
- package/templates/brownfield/templates/context-map.md +1 -1
- package/templates/brownfield/templates/glossary.md +1 -1
- package/templates/brownfield/templates/models.md +1 -1
- package/templates/brownfield/templates/rules.md +1 -1
- package/templates/brownfield/templates/tech-debt.md +1 -1
- package/templates/common/skill/SKILL.md +35 -0
- package/templates/greenfield/references/ddd-modeling-guide.md +351 -0
- package/templates/greenfield/references/dflow-feedback-flow.md +179 -0
- package/templates/greenfield/references/drift-verification.md +195 -0
- package/templates/greenfield/references/finish-feature-flow.md +280 -0
- package/templates/greenfield/references/git-integration.md +285 -0
- package/templates/greenfield/references/init-project-flow.md +447 -0
- package/templates/greenfield/references/modify-existing-flow.md +362 -0
- package/templates/greenfield/references/new-feature-flow.md +397 -0
- package/templates/greenfield/references/new-phase-flow.md +273 -0
- package/templates/greenfield/references/pr-review-checklist.md +130 -0
- package/templates/greenfield/scaffolding/AI-AGENT-GUIDE.md +31 -4
- package/templates/greenfield/scaffolding/CLAUDE-md-snippet.md +15 -13
- package/templates/greenfield/scaffolding/Git-principles-gitflow.md +1 -1
- package/templates/greenfield/scaffolding/Git-principles-trunk.md +1 -1
- package/templates/greenfield/scaffolding/_conventions.md +1 -1
- package/templates/greenfield/scaffolding/_overview.md +5 -3
- package/templates/greenfield/scaffolding/architecture-decisions-README.md +1 -1
- package/templates/greenfield/templates/context-map.md +1 -1
- package/templates/greenfield/templates/events.md +1 -1
- package/templates/greenfield/templates/glossary.md +1 -1
- package/templates/greenfield/templates/models.md +1 -1
- package/templates/greenfield/templates/rules.md +1 -1
- 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
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
810
|
+
const marker = validDefaults.includes(option.key) ? ' (currently configured)' : '';
|
|
811
|
+
stdout.write(` ${index + 1}. ${option.label}${marker}\n`);
|
|
754
812
|
});
|
|
755
813
|
|
|
756
|
-
const
|
|
757
|
-
|
|
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
|
|
1128
|
-
|
|
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
|
-
|
|
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 =
|
|
1714
|
-
? `
|
|
1715
|
-
:
|
|
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
|
-
|
|
1739
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
}
|