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