dflow-sdd-ddd 0.1.0 → 0.1.1
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/README.md +136 -160
- package/bin/dflow.js +37 -8
- package/docs/why-ddd-for-ai.md +35 -0
- package/lib/init.js +347 -65
- package/package.json +9 -7
- package/templates/brownfield/scaffolding/AI-AGENT-GUIDE.md +64 -0
- package/templates/{webforms → brownfield}/scaffolding/CLAUDE-md-snippet.md +7 -9
- package/templates/{webforms → brownfield}/scaffolding/Git-principles-gitflow.md +1 -1
- package/templates/{webforms → brownfield}/scaffolding/Git-principles-trunk.md +1 -1
- package/templates/{webforms → brownfield}/scaffolding/_conventions.md +1 -1
- package/templates/{webforms → brownfield}/scaffolding/_overview.md +2 -2
- package/templates/{webforms → brownfield}/templates/context-map.md +1 -1
- package/templates/{webforms → brownfield}/templates/glossary.md +1 -1
- package/templates/{webforms → brownfield}/templates/models.md +1 -1
- package/templates/{webforms → brownfield}/templates/phase-spec.md +1 -1
- package/templates/{webforms → brownfield}/templates/rules.md +1 -1
- package/templates/{webforms → brownfield}/templates/tech-debt.md +1 -1
- package/templates/greenfield/scaffolding/AI-AGENT-GUIDE.md +64 -0
- package/templates/{core → greenfield}/scaffolding/CLAUDE-md-snippet.md +14 -14
- package/templates/{core → greenfield}/scaffolding/Git-principles-gitflow.md +1 -1
- package/templates/{core → greenfield}/scaffolding/Git-principles-trunk.md +1 -1
- package/templates/{core → greenfield}/scaffolding/_conventions.md +1 -1
- package/templates/{core → greenfield}/scaffolding/_overview.md +2 -2
- package/templates/{core → greenfield}/scaffolding/architecture-decisions-README.md +1 -1
- package/templates/{core → greenfield}/templates/context-map.md +1 -1
- package/templates/{core → greenfield}/templates/events.md +1 -1
- package/templates/{core → greenfield}/templates/glossary.md +1 -1
- package/templates/{core → greenfield}/templates/models.md +1 -1
- package/templates/{core → greenfield}/templates/phase-spec.md +1 -1
- package/templates/{core → greenfield}/templates/rules.md +1 -1
- package/templates/{core → greenfield}/templates/tech-debt.md +1 -1
- /package/templates/{webforms → brownfield}/templates/CLAUDE.md +0 -0
- /package/templates/{webforms → brownfield}/templates/_index.md +0 -0
- /package/templates/{webforms → brownfield}/templates/behavior.md +0 -0
- /package/templates/{webforms → brownfield}/templates/context-definition.md +0 -0
- /package/templates/{webforms → brownfield}/templates/lightweight-spec.md +0 -0
- /package/templates/{core → greenfield}/templates/CLAUDE.md +0 -0
- /package/templates/{core → greenfield}/templates/_index.md +0 -0
- /package/templates/{core → greenfield}/templates/aggregate-design.md +0 -0
- /package/templates/{core → greenfield}/templates/behavior.md +0 -0
- /package/templates/{core → greenfield}/templates/context-definition.md +0 -0
- /package/templates/{core → greenfield}/templates/lightweight-spec.md +0 -0
package/lib/init.js
CHANGED
|
@@ -37,19 +37,6 @@ const PROJECT_TYPE_OPTIONS = [
|
|
|
37
37
|
}
|
|
38
38
|
];
|
|
39
39
|
|
|
40
|
-
const EDITION_OPTIONS = [
|
|
41
|
-
{
|
|
42
|
-
key: 'core',
|
|
43
|
-
label: 'ASP.NET Core - Clean Architecture + DDD',
|
|
44
|
-
aliases: ['core', 'asp.net core', 'aspnet core']
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
key: 'webforms',
|
|
48
|
-
label: 'ASP.NET WebForms - progressive domain extraction',
|
|
49
|
-
aliases: ['webforms', 'asp.net webforms', 'aspnet webforms']
|
|
50
|
-
}
|
|
51
|
-
];
|
|
52
|
-
|
|
53
40
|
const PROSE_LANGUAGE_OPTIONS = [
|
|
54
41
|
{
|
|
55
42
|
key: 'zh-TW',
|
|
@@ -88,11 +75,29 @@ const OPTIONAL_FILE_OPTIONS = [
|
|
|
88
75
|
key: 'git-flow',
|
|
89
76
|
label: 'Git principles - Git Flow',
|
|
90
77
|
aliases: ['git principles - git flow', 'git flow', 'gitflow']
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const AI_AGENT_OPTIONS = [
|
|
82
|
+
{
|
|
83
|
+
key: 'agents',
|
|
84
|
+
label: 'AGENTS.md - Codex / Copilot coding agent',
|
|
85
|
+
aliases: ['agents', 'agents.md', 'codex', 'copilot agent', 'copilot coding agent']
|
|
91
86
|
},
|
|
92
87
|
{
|
|
93
88
|
key: 'claude',
|
|
94
|
-
label: 'CLAUDE.md
|
|
89
|
+
label: 'CLAUDE.md - Claude Code',
|
|
95
90
|
aliases: ['claude', 'claude.md']
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: 'gemini',
|
|
94
|
+
label: 'GEMINI.md - Gemini CLI',
|
|
95
|
+
aliases: ['gemini', 'gemini.md']
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: 'copilot',
|
|
99
|
+
label: '.github/copilot-instructions.md - GitHub Copilot',
|
|
100
|
+
aliases: ['copilot', 'github copilot', 'copilot-instructions', 'copilot-instructions.md']
|
|
96
101
|
}
|
|
97
102
|
];
|
|
98
103
|
|
|
@@ -211,6 +216,95 @@ async function runInit(options = {}) {
|
|
|
211
216
|
}
|
|
212
217
|
}
|
|
213
218
|
|
|
219
|
+
async function runConfigureAgents(options = {}) {
|
|
220
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
221
|
+
const stdin = options.stdin || process.stdin;
|
|
222
|
+
const stdout = options.stdout || process.stdout;
|
|
223
|
+
const stderr = options.stderr || process.stderr;
|
|
224
|
+
|
|
225
|
+
let rl;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
rl = readline.createInterface({
|
|
229
|
+
input: stdin,
|
|
230
|
+
output: stdout,
|
|
231
|
+
terminal: Boolean(stdin.isTTY && stdout.isTTY)
|
|
232
|
+
});
|
|
233
|
+
rl._dflowOutput = stdout;
|
|
234
|
+
getLinePrompter(rl);
|
|
235
|
+
|
|
236
|
+
if (compareVersions(process.versions.node, MIN_NODE_VERSION) < 0) {
|
|
237
|
+
throw new InitError(`Dflow configure-agents requires Node.js ${MIN_NODE_VERSION}+.`, 1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await assertWritableProjectRoot(cwd);
|
|
241
|
+
await assertDflowInitialized(cwd);
|
|
242
|
+
|
|
243
|
+
const projectContext = await inferProjectContext(cwd, rl, stdout, stderr);
|
|
244
|
+
const aiAgents = await askAiAgents(rl, stdout, stderr);
|
|
245
|
+
|
|
246
|
+
if (aiAgents.length === 0) {
|
|
247
|
+
throw new UserAbort('No AI agents selected. Nothing changed.');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const plan = await buildConfigureAgentsPlan(cwd, {
|
|
251
|
+
...projectContext,
|
|
252
|
+
aiAgents
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
renderPreview(stdout, plan, []);
|
|
256
|
+
const confirmed = await askConfirmation(rl, 'Create these files? (y/N) ');
|
|
257
|
+
|
|
258
|
+
if (!confirmed) {
|
|
259
|
+
throw new UserAbort();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
rl.close();
|
|
263
|
+
rl = undefined;
|
|
264
|
+
|
|
265
|
+
const result = await writeFilePlan(cwd, plan);
|
|
266
|
+
result.warnings.push(...collectUnresolvedPlaceholderWarnings(plan, result.created));
|
|
267
|
+
|
|
268
|
+
printResultReport(stdout, result, plan.deferred);
|
|
269
|
+
printConfigureAgentsNextSteps(stdout);
|
|
270
|
+
return 0;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (rl) {
|
|
273
|
+
rl.close();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (error instanceof UserAbort) {
|
|
277
|
+
stdout.write(`${error.message}\n`);
|
|
278
|
+
return error.exitCode;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (error instanceof WritePhaseError) {
|
|
282
|
+
stderr.write(`${error.message}\n`);
|
|
283
|
+
stderr.write('Files already created were kept; clean up partial output manually if needed.\n');
|
|
284
|
+
if (error.result) {
|
|
285
|
+
printResultReport(stdout, error.result, []);
|
|
286
|
+
}
|
|
287
|
+
return error.exitCode;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (error instanceof InitError) {
|
|
291
|
+
stderr.write(`${error.message}\n`);
|
|
292
|
+
return error.exitCode;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
stderr.write(`${error && error.message ? error.message : error}\n`);
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function assertDflowInitialized(cwd) {
|
|
301
|
+
const dflowSpecsPath = path.join(cwd, 'dflow', 'specs');
|
|
302
|
+
|
|
303
|
+
if (!(await pathExists(dflowSpecsPath)) || !(await containsInitializedContent(dflowSpecsPath))) {
|
|
304
|
+
throw new InitError('Dflow is not initialized in this project. Run `dflow init` first.');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
214
308
|
async function runPreflight(cwd) {
|
|
215
309
|
const warnings = [];
|
|
216
310
|
const dflowSpecsPath = path.join(cwd, 'dflow', 'specs');
|
|
@@ -309,16 +403,16 @@ async function detectProjectSignals(cwd) {
|
|
|
309
403
|
}
|
|
310
404
|
}
|
|
311
405
|
|
|
312
|
-
let
|
|
406
|
+
let trackHint = null;
|
|
313
407
|
if (coreSignal && !webFormsSignal) {
|
|
314
|
-
|
|
408
|
+
trackHint = 'greenfield';
|
|
315
409
|
} else if (webFormsSignal && !coreSignal) {
|
|
316
|
-
|
|
410
|
+
trackHint = 'brownfield';
|
|
317
411
|
}
|
|
318
412
|
|
|
319
413
|
return {
|
|
320
414
|
hasSourceTree: hasSourceTree || relNames.has('src'),
|
|
321
|
-
|
|
415
|
+
trackHint
|
|
322
416
|
};
|
|
323
417
|
}
|
|
324
418
|
|
|
@@ -369,9 +463,9 @@ function buildDetectionWarnings(answers, detection) {
|
|
|
369
463
|
warnings.push('Warning: source-tree signals already exist, but project type is Greenfield. Continuing with your selected project type.');
|
|
370
464
|
}
|
|
371
465
|
|
|
372
|
-
if (detection.
|
|
466
|
+
if (detection.trackHint && answers.projectType !== detection.trackHint) {
|
|
373
467
|
warnings.push(
|
|
374
|
-
`Warning: project signals look like ${
|
|
468
|
+
`Warning: project signals look like ${formatTrack(detection.trackHint)}, but selected track is ${formatTrack(answers.projectType)}. Continuing with your selected track.`
|
|
375
469
|
);
|
|
376
470
|
}
|
|
377
471
|
|
|
@@ -387,15 +481,8 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
|
|
|
387
481
|
defaultKey: projectTypeDefault
|
|
388
482
|
});
|
|
389
483
|
|
|
390
|
-
const edition = await askSelect(rl, stdout, stderr, {
|
|
391
|
-
id: 'Q2',
|
|
392
|
-
question: 'Which Dflow edition should initialize this project?',
|
|
393
|
-
options: EDITION_OPTIONS,
|
|
394
|
-
defaultKey: detection.editionHint
|
|
395
|
-
});
|
|
396
|
-
|
|
397
484
|
const techStackSummary = await askText(rl, stderr, {
|
|
398
|
-
id: '
|
|
485
|
+
id: 'Q2',
|
|
399
486
|
question: 'Confirm the main tech stack details for placeholders.',
|
|
400
487
|
required: true,
|
|
401
488
|
maxLength: 1000,
|
|
@@ -403,7 +490,7 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
|
|
|
403
490
|
});
|
|
404
491
|
|
|
405
492
|
const migrationContext = await askText(rl, stderr, {
|
|
406
|
-
id: '
|
|
493
|
+
id: 'Q3',
|
|
407
494
|
question: 'Is there migration or legacy context Dflow should note?',
|
|
408
495
|
required: false,
|
|
409
496
|
maxLength: 1000,
|
|
@@ -411,7 +498,7 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
|
|
|
411
498
|
});
|
|
412
499
|
|
|
413
500
|
const proseLanguageSelection = await askSelect(rl, stdout, stderr, {
|
|
414
|
-
id: '
|
|
501
|
+
id: 'Q4',
|
|
415
502
|
question: 'Project prose language for generated spec content?',
|
|
416
503
|
options: PROSE_LANGUAGE_OPTIONS,
|
|
417
504
|
defaultKey: null
|
|
@@ -423,17 +510,76 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
|
|
|
423
510
|
}
|
|
424
511
|
|
|
425
512
|
const optionalFiles = await askOptionalFiles(rl, stdout, stderr);
|
|
513
|
+
const aiAgents = await askAiAgents(rl, stdout, stderr);
|
|
426
514
|
|
|
427
515
|
return {
|
|
428
516
|
projectType,
|
|
429
|
-
edition,
|
|
517
|
+
edition: projectType,
|
|
430
518
|
techStackSummary,
|
|
431
519
|
migrationContext,
|
|
432
520
|
proseLanguage,
|
|
433
|
-
optionalFiles
|
|
521
|
+
optionalFiles,
|
|
522
|
+
aiAgents
|
|
434
523
|
};
|
|
435
524
|
}
|
|
436
525
|
|
|
526
|
+
async function inferProjectContext(cwd, rl, stdout, stderr) {
|
|
527
|
+
let edition = await inferExistingEdition(cwd);
|
|
528
|
+
|
|
529
|
+
if (!edition) {
|
|
530
|
+
stderr.write('Could not infer the Dflow track from dflow/specs/. Please choose it explicitly.\n');
|
|
531
|
+
edition = await askSelect(rl, stdout, stderr, {
|
|
532
|
+
id: 'track',
|
|
533
|
+
question: 'Which Dflow track is this project using?',
|
|
534
|
+
options: PROJECT_TYPE_OPTIONS,
|
|
535
|
+
defaultKey: null
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
projectType: edition,
|
|
541
|
+
edition,
|
|
542
|
+
techStackSummary: await inferTechStackSummary(cwd),
|
|
543
|
+
migrationContext: await inferMigrationContext(cwd),
|
|
544
|
+
proseLanguage: await inferProseLanguage(cwd),
|
|
545
|
+
optionalFiles: []
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function inferExistingEdition(cwd) {
|
|
550
|
+
if (await pathExists(path.join(cwd, 'dflow/specs/architecture/tech-debt.md'))) {
|
|
551
|
+
return 'greenfield';
|
|
552
|
+
}
|
|
553
|
+
if (await pathExists(path.join(cwd, 'dflow/specs/migration/tech-debt.md'))) {
|
|
554
|
+
return 'brownfield';
|
|
555
|
+
}
|
|
556
|
+
if (await pathExists(path.join(cwd, 'dflow/specs/domain/context-map.md'))) {
|
|
557
|
+
return 'greenfield';
|
|
558
|
+
}
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function inferProseLanguage(cwd) {
|
|
563
|
+
const conventionsPath = path.join(cwd, 'dflow/specs/shared/_conventions.md');
|
|
564
|
+
const content = await fs.readFile(conventionsPath, 'utf8').catch(() => '');
|
|
565
|
+
const match = content.match(/Project prose language:\s*`([^`]+)`/);
|
|
566
|
+
return match ? match[1] : 'unknown';
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function inferTechStackSummary(cwd) {
|
|
570
|
+
const overviewPath = path.join(cwd, 'dflow/specs/shared/_overview.md');
|
|
571
|
+
const content = await fs.readFile(overviewPath, 'utf8').catch(() => '');
|
|
572
|
+
const match = content.match(/\|\s*Tech stack\s*\|\s*([^|\n]+?)\s*\|/i);
|
|
573
|
+
return match ? match[1].trim() : 'unknown';
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function inferMigrationContext(cwd) {
|
|
577
|
+
const overviewPath = path.join(cwd, 'dflow/specs/shared/_overview.md');
|
|
578
|
+
const content = await fs.readFile(overviewPath, 'utf8').catch(() => '');
|
|
579
|
+
const match = content.match(/\|\s*Migration \/ legacy context\s*\|\s*([^|\n]+?)\s*\|/i);
|
|
580
|
+
return match ? match[1].trim() : 'none';
|
|
581
|
+
}
|
|
582
|
+
|
|
437
583
|
async function askSelect(rl, stdout, stderr, config) {
|
|
438
584
|
let failedAttempts = 0;
|
|
439
585
|
|
|
@@ -506,7 +652,7 @@ async function askCustomProseLanguage(rl, stderr) {
|
|
|
506
652
|
|
|
507
653
|
failedAttempts += 1;
|
|
508
654
|
if (failedAttempts >= 3) {
|
|
509
|
-
throw new InitError('Too many invalid attempts for
|
|
655
|
+
throw new InitError('Too many invalid attempts for Q4a. Dflow init aborted.');
|
|
510
656
|
}
|
|
511
657
|
stderr.write(`${validation.message} (${3 - failedAttempts} attempts left)\n`);
|
|
512
658
|
}
|
|
@@ -528,7 +674,7 @@ async function askOptionalFiles(rl, stdout, stderr) {
|
|
|
528
674
|
if (!parsed.valid) {
|
|
529
675
|
failedAttempts += 1;
|
|
530
676
|
if (failedAttempts >= 3) {
|
|
531
|
-
throw new InitError('Too many invalid attempts for
|
|
677
|
+
throw new InitError('Too many invalid attempts for Q5. Dflow init aborted.');
|
|
532
678
|
}
|
|
533
679
|
stderr.write(`${parsed.message} (${3 - failedAttempts} attempts left)\n`);
|
|
534
680
|
continue;
|
|
@@ -549,6 +695,31 @@ async function askOptionalFiles(rl, stdout, stderr) {
|
|
|
549
695
|
}
|
|
550
696
|
}
|
|
551
697
|
|
|
698
|
+
async function askAiAgents(rl, stdout, stderr) {
|
|
699
|
+
let failedAttempts = 0;
|
|
700
|
+
|
|
701
|
+
while (true) {
|
|
702
|
+
stdout.write('\nWhich AI coding agents should Dflow configure?\n');
|
|
703
|
+
AI_AGENT_OPTIONS.forEach((option, index) => {
|
|
704
|
+
stdout.write(` ${index + 1}. ${option.label}\n`);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
const answer = await askLine(rl, 'Enter comma-separated choices or "none" (default: none): ');
|
|
708
|
+
const parsed = parseMultiselectAnswer(answer || 'none', AI_AGENT_OPTIONS, []);
|
|
709
|
+
|
|
710
|
+
if (!parsed.valid) {
|
|
711
|
+
failedAttempts += 1;
|
|
712
|
+
if (failedAttempts >= 3) {
|
|
713
|
+
throw new InitError('Too many invalid attempts for Q6. Dflow init aborted.');
|
|
714
|
+
}
|
|
715
|
+
stderr.write(`${parsed.message} (${3 - failedAttempts} attempts left)\n`);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return parsed.values;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
552
723
|
function askLine(rl, prompt) {
|
|
553
724
|
return getLinePrompter(rl).ask(prompt);
|
|
554
725
|
}
|
|
@@ -669,7 +840,7 @@ function parseMultiselectAnswer(answer, options, defaultKeys) {
|
|
|
669
840
|
for (const token of tokens) {
|
|
670
841
|
const selectedKey = parseSelectAnswer(token, options, null);
|
|
671
842
|
if (!selectedKey) {
|
|
672
|
-
return { valid: false, message: `Invalid
|
|
843
|
+
return { valid: false, message: `Invalid selection: ${token}` };
|
|
673
844
|
}
|
|
674
845
|
if (!selected.includes(selectedKey)) {
|
|
675
846
|
selected.push(selectedKey);
|
|
@@ -713,9 +884,6 @@ async function buildFilePlan(cwd, answers) {
|
|
|
713
884
|
|
|
714
885
|
const addTemplate = async (relativePath, sourceRel, notes, options = {}) => {
|
|
715
886
|
let content = await readPackagedTemplate(answers.edition, sourceRel);
|
|
716
|
-
if (options.extractClaudeSnippetBody) {
|
|
717
|
-
content = extractClaudeSnippetBody(content, sourceRel);
|
|
718
|
-
}
|
|
719
887
|
content = substitutePlaceholders(content, substitution);
|
|
720
888
|
if (options.injectProseLanguage) {
|
|
721
889
|
content = ensureProseLanguageSection(content, answers.proseLanguage);
|
|
@@ -737,7 +905,7 @@ async function buildFilePlan(cwd, answers) {
|
|
|
737
905
|
});
|
|
738
906
|
await addTemplate('dflow/specs/domain/glossary.md', 'templates/glossary.md', 'mandatory');
|
|
739
907
|
|
|
740
|
-
if (answers.edition === '
|
|
908
|
+
if (answers.edition === 'greenfield') {
|
|
741
909
|
await addTemplate('dflow/specs/domain/context-map.md', 'templates/context-map.md', 'mandatory');
|
|
742
910
|
await addTemplate('dflow/specs/architecture/tech-debt.md', 'templates/tech-debt.md', 'mandatory');
|
|
743
911
|
await addTemplate(
|
|
@@ -758,17 +926,57 @@ async function buildFilePlan(cwd, answers) {
|
|
|
758
926
|
if (answers.optionalFiles.includes('git-flow')) {
|
|
759
927
|
await addTemplate('dflow/specs/shared/Git-principles-gitflow.md', 'scaffolding/Git-principles-gitflow.md', 'selected');
|
|
760
928
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
await addTemplate('CLAUDE.md', 'scaffolding/CLAUDE-md-snippet.md', 'selected, snippet body only', {
|
|
767
|
-
extractClaudeSnippetBody: true
|
|
768
|
-
});
|
|
929
|
+
|
|
930
|
+
if (answers.aiAgents.length > 0) {
|
|
931
|
+
await addTemplate('dflow/specs/shared/AI-AGENT-GUIDE.md', 'scaffolding/AI-AGENT-GUIDE.md', 'selected, canonical AI agent guide');
|
|
932
|
+
for (const agent of answers.aiAgents) {
|
|
933
|
+
await addAiAgentShim(cwd, items, agent, substitution);
|
|
769
934
|
}
|
|
770
935
|
}
|
|
771
936
|
|
|
937
|
+
await finalizePlanItems(cwd, items);
|
|
938
|
+
|
|
939
|
+
return {
|
|
940
|
+
items,
|
|
941
|
+
deferred: buildDeferredItems(answers.edition),
|
|
942
|
+
unresolvedInitPlaceholders: Array.from(substitution.entries())
|
|
943
|
+
.filter(([placeholder, value]) => placeholder === value)
|
|
944
|
+
.map(([placeholder]) => placeholder)
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
async function buildConfigureAgentsPlan(cwd, answers) {
|
|
949
|
+
const substitution = buildSubstitutionMap(cwd, {
|
|
950
|
+
...answers,
|
|
951
|
+
optionalFiles: answers.optionalFiles || []
|
|
952
|
+
});
|
|
953
|
+
const items = [];
|
|
954
|
+
|
|
955
|
+
let content = await readPackagedTemplate(answers.edition, 'scaffolding/AI-AGENT-GUIDE.md');
|
|
956
|
+
content = substitutePlaceholders(content, substitution);
|
|
957
|
+
items.push({
|
|
958
|
+
relativePath: 'dflow/specs/shared/AI-AGENT-GUIDE.md',
|
|
959
|
+
source: `packaged:${answers.edition}/scaffolding/AI-AGENT-GUIDE.md`,
|
|
960
|
+
notes: 'canonical AI agent guide',
|
|
961
|
+
content
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
for (const agent of answers.aiAgents) {
|
|
965
|
+
await addAiAgentShim(cwd, items, agent, substitution);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
await finalizePlanItems(cwd, items);
|
|
969
|
+
|
|
970
|
+
return {
|
|
971
|
+
items,
|
|
972
|
+
deferred: [],
|
|
973
|
+
unresolvedInitPlaceholders: Array.from(substitution.entries())
|
|
974
|
+
.filter(([placeholder, value]) => placeholder === value)
|
|
975
|
+
.map(([placeholder]) => placeholder)
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async function finalizePlanItems(cwd, items) {
|
|
772
980
|
for (const item of items) {
|
|
773
981
|
const absolute = path.join(cwd, item.relativePath);
|
|
774
982
|
item.action = (await pathExists(absolute)) ? 'skip' : 'create';
|
|
@@ -777,22 +985,92 @@ async function buildFilePlan(cwd, answers) {
|
|
|
777
985
|
}
|
|
778
986
|
item.size = Buffer.byteLength(item.content, 'utf8');
|
|
779
987
|
}
|
|
988
|
+
}
|
|
780
989
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
990
|
+
async function addAiAgentShim(cwd, items, agent, substitution) {
|
|
991
|
+
const target = getAiAgentTarget(agent);
|
|
992
|
+
const targetPath = path.join(cwd, target.relativePath);
|
|
993
|
+
const targetExists = await pathExists(targetPath);
|
|
994
|
+
const targetConfigured = targetExists && await fileReferencesAiAgentGuide(targetPath);
|
|
995
|
+
const relativePath = targetExists && !targetConfigured ? target.snippetPath : target.relativePath;
|
|
996
|
+
const content = substitutePlaceholders(buildAiAgentShim(target.relativePath), substitution);
|
|
997
|
+
let notes = 'selected, tool-specific shim';
|
|
998
|
+
if (targetConfigured) {
|
|
999
|
+
notes = `selected, ${target.relativePath} already points to AI-AGENT-GUIDE.md`;
|
|
1000
|
+
} else if (targetExists) {
|
|
1001
|
+
notes = `selected, ${target.relativePath} already exists; merge this snippet manually`;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
items.push({
|
|
1005
|
+
relativePath,
|
|
1006
|
+
source: `generated:${agent}-shim`,
|
|
1007
|
+
notes,
|
|
1008
|
+
content
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async function fileReferencesAiAgentGuide(targetPath) {
|
|
1013
|
+
try {
|
|
1014
|
+
const content = await fs.readFile(targetPath, 'utf8');
|
|
1015
|
+
return content.includes('dflow/specs/shared/AI-AGENT-GUIDE.md') ||
|
|
1016
|
+
content.includes('dflow\\specs\\shared\\AI-AGENT-GUIDE.md');
|
|
1017
|
+
} catch {
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function getAiAgentTarget(agent) {
|
|
1023
|
+
const targets = {
|
|
1024
|
+
agents: {
|
|
1025
|
+
relativePath: 'AGENTS.md',
|
|
1026
|
+
snippetPath: 'dflow/specs/shared/AGENTS-md-snippet.md'
|
|
1027
|
+
},
|
|
1028
|
+
claude: {
|
|
1029
|
+
relativePath: 'CLAUDE.md',
|
|
1030
|
+
snippetPath: 'dflow/specs/shared/CLAUDE-md-snippet.md'
|
|
1031
|
+
},
|
|
1032
|
+
gemini: {
|
|
1033
|
+
relativePath: 'GEMINI.md',
|
|
1034
|
+
snippetPath: 'dflow/specs/shared/GEMINI-md-snippet.md'
|
|
1035
|
+
},
|
|
1036
|
+
copilot: {
|
|
1037
|
+
relativePath: '.github/copilot-instructions.md',
|
|
1038
|
+
snippetPath: 'dflow/specs/shared/copilot-instructions-snippet.md'
|
|
1039
|
+
}
|
|
787
1040
|
};
|
|
1041
|
+
|
|
1042
|
+
return targets[agent];
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function buildAiAgentShim(targetPath) {
|
|
1046
|
+
const title = targetPath === '.github/copilot-instructions.md'
|
|
1047
|
+
? 'GitHub Copilot Repository Instructions'
|
|
1048
|
+
: `${targetPath} - Dflow Project Instructions`;
|
|
1049
|
+
|
|
1050
|
+
const importHint = targetPath === 'CLAUDE.md' || targetPath === 'GEMINI.md'
|
|
1051
|
+
? '\nIf your tool supports Markdown imports, the canonical guide is imported below:\n\n@dflow/specs/shared/AI-AGENT-GUIDE.md\n'
|
|
1052
|
+
: '';
|
|
1053
|
+
|
|
1054
|
+
return `# ${title}
|
|
1055
|
+
|
|
1056
|
+
This project uses Dflow for spec-first AI-assisted development.
|
|
1057
|
+
|
|
1058
|
+
Before planning or editing code, read and follow:
|
|
1059
|
+
|
|
1060
|
+
- \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
|
|
1061
|
+
|
|
1062
|
+
Keep tool-specific instruction files small. The Dflow guide above is the
|
|
1063
|
+
single source of truth for project workflow rules, slash-command behavior,
|
|
1064
|
+
spec locations, and SDD/DDD constraints.${importHint}
|
|
1065
|
+
`;
|
|
788
1066
|
}
|
|
789
1067
|
|
|
790
1068
|
function buildDeferredItems(edition) {
|
|
791
1069
|
const deferred = [...DEFERRED_COMMON];
|
|
792
|
-
if (edition === '
|
|
1070
|
+
if (edition === 'greenfield') {
|
|
793
1071
|
deferred.splice(3, 0, {
|
|
794
1072
|
relativePath: 'dflow/specs/domain/{context}/events.md',
|
|
795
|
-
reason: '
|
|
1073
|
+
reason: 'Greenfield only, but still needs a real bounded context.'
|
|
796
1074
|
});
|
|
797
1075
|
}
|
|
798
1076
|
return deferred;
|
|
@@ -892,14 +1170,6 @@ function extractTestFramework(text) {
|
|
|
892
1170
|
return null;
|
|
893
1171
|
}
|
|
894
1172
|
|
|
895
|
-
function extractClaudeSnippetBody(content, sourceRel) {
|
|
896
|
-
const match = content.match(/## Snippet to merge into `CLAUDE\.md`[\s\S]*?```markdown\r?\n([\s\S]*?)\r?\n```/);
|
|
897
|
-
if (!match) {
|
|
898
|
-
throw new InitError(`Internal error: CLAUDE.md snippet body not found in packaged template: ${sourceRel}`);
|
|
899
|
-
}
|
|
900
|
-
return `${match[1].trimEnd()}\n`;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
1173
|
function ensureProseLanguageSection(content, proseLanguage) {
|
|
904
1174
|
const section = buildProseLanguageSection(proseLanguage);
|
|
905
1175
|
let stripped = stripProseLanguageSections(content);
|
|
@@ -1116,6 +1386,17 @@ Recommended next steps:
|
|
|
1116
1386
|
`);
|
|
1117
1387
|
}
|
|
1118
1388
|
|
|
1389
|
+
function printConfigureAgentsNextSteps(stdout) {
|
|
1390
|
+
stdout.write(`
|
|
1391
|
+
Dflow AI agent configuration complete.
|
|
1392
|
+
|
|
1393
|
+
Recommended next steps:
|
|
1394
|
+
- Keep AI-agent-specific root files small.
|
|
1395
|
+
- Put durable workflow changes in dflow/specs/shared/AI-AGENT-GUIDE.md.
|
|
1396
|
+
- If a merge snippet was created, review it and merge the pointer into the existing tool instruction file.
|
|
1397
|
+
`);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1119
1400
|
function printList(stdout, values) {
|
|
1120
1401
|
if (!values || values.length === 0) {
|
|
1121
1402
|
stdout.write('- (none)\n');
|
|
@@ -1186,8 +1467,8 @@ function formatBytes(bytes) {
|
|
|
1186
1467
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1187
1468
|
}
|
|
1188
1469
|
|
|
1189
|
-
function
|
|
1190
|
-
return
|
|
1470
|
+
function formatTrack(track) {
|
|
1471
|
+
return track === 'greenfield' ? 'Greenfield' : 'Brownfield';
|
|
1191
1472
|
}
|
|
1192
1473
|
|
|
1193
1474
|
function escapeTableCell(value) {
|
|
@@ -1199,6 +1480,7 @@ function dedupe(values) {
|
|
|
1199
1480
|
}
|
|
1200
1481
|
|
|
1201
1482
|
module.exports = {
|
|
1483
|
+
runConfigureAgents,
|
|
1202
1484
|
runInit,
|
|
1203
1485
|
validateProseLanguage,
|
|
1204
1486
|
ensureProseLanguageSection,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dflow-sdd-ddd",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Spec-first SDD/DDD workflow kit for AI-assisted development",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"dflow": "bin/dflow.js",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"bin/",
|
|
15
|
+
"docs/",
|
|
15
16
|
"lib/",
|
|
16
17
|
"templates/",
|
|
17
18
|
"README.md"
|
|
@@ -21,17 +22,18 @@
|
|
|
21
22
|
"ddd",
|
|
22
23
|
"specification-driven-development",
|
|
23
24
|
"domain-driven-design",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
25
|
+
"spec-first",
|
|
26
|
+
"ai-assisted-development",
|
|
27
|
+
"ai-coding-agent"
|
|
26
28
|
],
|
|
27
29
|
"repository": {
|
|
28
30
|
"type": "git",
|
|
29
|
-
"url": "git+ssh://git@github.com/weilung/
|
|
31
|
+
"url": "git+ssh://git@github.com/weilung/dflow-sdd-ddd.git"
|
|
30
32
|
},
|
|
31
33
|
"bugs": {
|
|
32
|
-
"url": "https://github.com/weilung/
|
|
34
|
+
"url": "https://github.com/weilung/dflow-sdd-ddd/issues"
|
|
33
35
|
},
|
|
34
|
-
"homepage": "https://github.com/weilung/
|
|
36
|
+
"homepage": "https://github.com/weilung/dflow-sdd-ddd#readme",
|
|
35
37
|
"scripts": {
|
|
36
38
|
"test": "node test/smoke.mjs"
|
|
37
39
|
},
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Dflow AI Agent Guide
|
|
2
|
+
|
|
3
|
+
This project uses Dflow for spec-first AI-assisted development.
|
|
4
|
+
|
|
5
|
+
## Project Context
|
|
6
|
+
|
|
7
|
+
| Field | Value |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Project | {System Name} |
|
|
10
|
+
| Dflow track | {edition} |
|
|
11
|
+
| Project type | {project-type} |
|
|
12
|
+
| Tech stack | {tech-stack-summary} |
|
|
13
|
+
| Migration / legacy context | {migration-context} |
|
|
14
|
+
| Prose language | {prose-language} |
|
|
15
|
+
|
|
16
|
+
## Before Editing Code
|
|
17
|
+
|
|
18
|
+
Do not jump from a request directly to code. First identify the matching
|
|
19
|
+
Dflow workflow and confirm the intended path with the developer.
|
|
20
|
+
|
|
21
|
+
Use these workflow entry points as plain chat instructions if slash commands
|
|
22
|
+
are not available in the current AI tool:
|
|
23
|
+
|
|
24
|
+
| Workflow | Use when |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `/dflow:new-feature` | A new user-visible capability or business behavior is requested. |
|
|
27
|
+
| `/dflow:modify-existing` | Existing behavior needs to change. |
|
|
28
|
+
| `/dflow:bug-fix` | A defect can be described with expected vs actual behavior. |
|
|
29
|
+
| `/dflow:new-phase` | An active feature needs another implementation slice. |
|
|
30
|
+
| `/dflow:finish-feature` | Implementation is complete and needs drift closure. |
|
|
31
|
+
| `/dflow:verify` | Specs, domain docs, implementation, and tests need consistency checks. |
|
|
32
|
+
| `/dflow:pr-review` | A change is ready for SDD/DDD review. |
|
|
33
|
+
|
|
34
|
+
## Source of Truth
|
|
35
|
+
|
|
36
|
+
Dflow-owned project documents live under `dflow/specs/`.
|
|
37
|
+
|
|
38
|
+
| Area | Path |
|
|
39
|
+
|---|---|
|
|
40
|
+
| Shared conventions | `dflow/specs/shared/_conventions.md` |
|
|
41
|
+
| System overview | `dflow/specs/shared/_overview.md` |
|
|
42
|
+
| Domain glossary | `dflow/specs/domain/glossary.md` |
|
|
43
|
+
| Context map | `dflow/specs/domain/context-map.md` |
|
|
44
|
+
| Active feature specs | `dflow/specs/features/active/` |
|
|
45
|
+
| Completed feature snapshots | `dflow/specs/features/completed/` |
|
|
46
|
+
| Technical debt | `dflow/specs/architecture/tech-debt.md` or `dflow/specs/migration/tech-debt.md` |
|
|
47
|
+
|
|
48
|
+
## Core Rules
|
|
49
|
+
|
|
50
|
+
1. Spec before code: meaningful behavior changes need a spec or lightweight bug spec before implementation.
|
|
51
|
+
2. Keep domain language explicit: update glossary, rules, models, and behavior snapshots when domain meaning changes.
|
|
52
|
+
3. Keep phase delta, feature snapshot, and system state separate.
|
|
53
|
+
4. Check drift before calling work complete.
|
|
54
|
+
5. Follow `dflow/specs/shared/_conventions.md`, especially `## Prose Language`.
|
|
55
|
+
|
|
56
|
+
## Tool-Specific Notes
|
|
57
|
+
|
|
58
|
+
This file is the canonical Dflow guide. Root-level files such as
|
|
59
|
+
`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, and `.github/copilot-instructions.md`
|
|
60
|
+
should stay thin and point back here.
|
|
61
|
+
|
|
62
|
+
If a tool does not support Dflow slash commands, treat the command names as
|
|
63
|
+
plain workflow names and execute the matching process from the Dflow skill
|
|
64
|
+
source in this repository.
|