dflow-sdd-ddd 0.8.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/LICENSE +679 -21
  3. package/README.en.md +5 -4
  4. package/README.md +3 -3
  5. package/bin/dflow.js +3 -2
  6. package/docs/using-with-codex.en.md +12 -8
  7. package/docs/using-with-codex.md +8 -6
  8. package/lib/init.js +217 -35
  9. package/package.json +2 -2
  10. package/templates/brownfield/references/dflow-feedback-flow.md +135 -63
  11. package/templates/brownfield/references/finish-feature-flow.md +56 -21
  12. package/templates/brownfield/references/git-integration.md +65 -6
  13. package/templates/brownfield/references/init-project-flow.md +36 -19
  14. package/templates/brownfield/references/modify-existing-flow.md +4 -0
  15. package/templates/brownfield/references/new-feature-flow.md +15 -0
  16. package/templates/brownfield/references/new-phase-flow.md +15 -0
  17. package/templates/brownfield/scaffolding/Git-principles-gitflow.md +13 -12
  18. package/templates/brownfield/scaffolding/Git-principles-trunk.md +13 -16
  19. package/templates/brownfield/templates/_index.md +20 -2
  20. package/templates/greenfield/references/dflow-feedback-flow.md +135 -63
  21. package/templates/greenfield/references/finish-feature-flow.md +55 -21
  22. package/templates/greenfield/references/git-integration.md +65 -6
  23. package/templates/greenfield/references/init-project-flow.md +36 -19
  24. package/templates/greenfield/references/modify-existing-flow.md +4 -0
  25. package/templates/greenfield/references/new-feature-flow.md +15 -0
  26. package/templates/greenfield/references/new-phase-flow.md +15 -0
  27. package/templates/greenfield/scaffolding/Git-principles-gitflow.md +13 -12
  28. package/templates/greenfield/scaffolding/Git-principles-trunk.md +13 -17
  29. package/templates/greenfield/templates/_index.md +20 -2
package/README.en.md CHANGED
@@ -35,8 +35,9 @@ npm install -g dflow-sdd-ddd
35
35
  dflow init
36
36
  ```
37
37
 
38
- The init flow asks whether the project is greenfield or brownfield, then
39
- previews the files it will create. Existing files are not overwritten. Init
38
+ The init flow asks whether the project is greenfield or brownfield, which Git
39
+ policy the team follows (GitFlow / Trunk), and how AI-made commits should be
40
+ marked, then previews the files it will create. Existing files are not overwritten. Init
40
41
  creates workflow documentation and AI instruction files; it does not inspect,
41
42
  refactor, or migrate your application code.
42
43
 
@@ -317,7 +318,7 @@ Usable only inside an already-started active feature. Targets pointing at `compl
317
318
  |---|---|---|
318
319
  | `/dflow:verify` | Need to confirm docs, code, tests, and tech-debt records are still in sync | Drift report across spec, domain docs, implementation, tests, and debt records |
319
320
  | `/dflow:pr-review` | A change is ready for review | SDD/DDD compliance review checklist with risks, gaps, and follow-up items |
320
- | `/dflow:report-dflow-feedback` | You or the AI found a Dflow issue or improvement while using it | Sanitized local feedback draft; nothing is submitted automatically |
321
+ | `/dflow:report-dflow-feedback` | You or the AI found a Dflow issue or improvement while using it | Sanitized local draft rendered field-by-field for the upstream issue form, ready to paste; nothing is submitted automatically |
321
322
 
322
323
  ### What should I run? (rule of thumb)
323
324
 
@@ -389,4 +390,4 @@ release history.
389
390
 
390
391
  ## License
391
392
 
392
- MIT License. See [LICENSE](LICENSE).
393
+ GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). See [LICENSE](LICENSE).
package/README.md CHANGED
@@ -33,7 +33,7 @@ npm install -g dflow-sdd-ddd
33
33
  dflow init
34
34
  ```
35
35
 
36
- init 流程會詢問是 greenfield 或 brownfield,接著預覽即將建立的檔案。既有檔案不會被覆寫。Init 只建立 workflow 文件與 AI 指示檔,**不會**檢查、重構、或遷移你的應用程式碼。
36
+ init 流程會詢問是 greenfield 或 brownfield、團隊採用的 Git policy(GitFlow / Trunk)、以及 AI commit 的標記方式,接著預覽即將建立的檔案。既有檔案不會被覆寫。Init 只建立 workflow 文件與 AI 指示檔,**不會**檢查、重構、或遷移你的應用程式碼。
37
37
 
38
38
  若專案已初始化、之後又要加入另一個 AI 程式設計工具,執行:
39
39
 
@@ -263,7 +263,7 @@ Dflow 指令依角色分四類。「我要做的事」對應到指令的速查
263
263
  |---|---|---|
264
264
  | `/dflow:verify` | 需要確認文件、程式、測試、債務紀錄是否一致 | 跨規格、領域文件、實作、測試、債務的 drift report |
265
265
  | `/dflow:pr-review` | 變更已準備接受審查 | SDD/DDD 合規 review 清單,含風險、缺口、後續項目 |
266
- | `/dflow:report-dflow-feedback` | 你或 AI 在使用中發現 Dflow 本身的問題 | sanitized 的本地 feedback 草稿;不自動送出 |
266
+ | `/dflow:report-dflow-feedback` | 你或 AI 在使用中發現 Dflow 本身的問題 | sanitized 的本地草稿,逐欄對齊上游 issue 表單可直接貼上;不自動送出 |
267
267
 
268
268
  ### 該選哪個指令(rule of thumb)
269
269
 
@@ -328,4 +328,4 @@ GitHub 上的 source 可能包含 `0.2.0` 之後尚未發佈的 repo 變更。
328
328
 
329
329
  ## 授權
330
330
 
331
- MIT License,見 [LICENSE](LICENSE)。
331
+ GNU Affero General Public License v3.0 或更新版本(AGPL-3.0-or-later),見 [LICENSE](LICENSE)。
package/bin/dflow.js CHANGED
@@ -22,8 +22,9 @@ function printInitHelp() {
22
22
  dflow init
23
23
 
24
24
  Initializes Dflow project specs under dflow/specs/.
25
- The command prompts for project type, tech stack, prose language,
26
- optional starter files, and AI coding agents before showing a full file preview.
25
+ The command prompts for project type, tech stack, prose language, Git policy,
26
+ AI commit marker, optional starter files, and AI coding agents before showing a
27
+ full file preview.
27
28
  `);
28
29
  }
29
30
 
@@ -162,20 +162,24 @@ triggers only. It does not create Codex command files and it does not add
162
162
  equivalent to Claude `.claude/commands` or Copilot `.github/prompts`.
163
163
 
164
164
  When you select `AGENTS.md - Codex / Copilot coding agent` in
165
- `--command-adapters` mode and Dflow can create a new `AGENTS.md` shim, the
166
- shim includes a trigger list generated from the canonical command registry.
167
- Those triggers are still plain text prompts, for example:
165
+ `--command-adapters` mode, Dflow writes a trigger list generated from the
166
+ canonical command registry into `AGENTS.md`. Those triggers are still plain
167
+ text prompts, for example:
168
168
 
169
169
  ```text
170
170
  dflow:new-feature
171
171
  ```
172
172
 
173
- If the project already has a custom `AGENTS.md`, Dflow still preserves that
174
- file; merge the Dflow pointer manually from the generated snippet or the
175
- documentation guidance.
173
+ As long as `AGENTS.md` is an unmodified Dflow-generated shim — which is exactly
174
+ the case after the standard `init` → `configure-agents --command-adapters`
175
+ flow — Dflow **injects** the trigger section directly into `AGENTS.md`, wrapped
176
+ in markers, with zero manual merge. Re-running re-projects that same section in
177
+ place instead of appending a duplicate.
176
178
 
177
- In this mode, the Codex-target merge snippet filename is
178
- `dflow/specs/shared/AGENTS-md-command-adapters-snippet.md`.
179
+ If `AGENTS.md` was edited after Dflow generated it, or is your own custom file,
180
+ Dflow leaves it untouched and instead writes the trigger section to a merge
181
+ snippet for you to merge manually. In that case the Codex-target merge snippet
182
+ filename is `dflow/specs/shared/AGENTS-md-command-adapters-snippet.md`.
179
183
 
180
184
  ### Version-Control Policy for Generated Artifacts (Codex)
181
185
 
@@ -149,18 +149,20 @@ Claude `.claude/commands` 或 Copilot `.github/prompts` 對等的 Dflow command-
149
149
  adapter。
150
150
 
151
151
  當你在 `--command-adapters` 模式下選擇 `AGENTS.md - Codex / Copilot coding agent`
152
- 且 Dflow 可以建立新的 `AGENTS.md` shim 時,shim 會加入從 canonical command
153
- registry 產生的 trigger 清單。這些 trigger 仍是文字提示,例如:
152
+ 時,Dflow 會把從 canonical command registry 產生的 trigger 清單寫進 `AGENTS.md`。
153
+ 這些 trigger 仍是文字提示,例如:
154
154
 
155
155
  ```text
156
156
  dflow:new-feature
157
157
  ```
158
158
 
159
- 如果專案已有自訂 `AGENTS.md`,Dflow 仍會保留既有檔案;請依產生的 merge snippet
160
- 或文件指引手動合併 Dflow 指標。
159
+ 只要 `AGENTS.md` 是 Dflow 產生且未經改動的 shim(標準 `init` → `configure-agents
160
+ --command-adapters` 流程就是如此),Dflow 會把帶 marker 的 trigger 段**直接注入**
161
+ `AGENTS.md`,零手動合併;重複執行會就地重投影同一段、不會重複附加。
161
162
 
162
- 在這個模式下,Codex 目標的 merge snippet 檔名是
163
- `dflow/specs/shared/AGENTS-md-command-adapters-snippet.md`。
163
+ 如果 `AGENTS.md` 在 Dflow 產生後被改過、或本來就是你自訂的檔案,Dflow 會保留既有
164
+ 檔案不動,改把 trigger 段寫到 merge snippet 供你手動合併。此情況下 Codex 目標的
165
+ merge snippet 檔名是 `dflow/specs/shared/AGENTS-md-command-adapters-snippet.md`。
164
166
 
165
167
  ### 產生物的版控政策(Codex)
166
168
 
package/lib/init.js CHANGED
@@ -13,6 +13,8 @@ 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
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 -->';
16
18
  const WORKFLOW_BUNDLE_DEST = 'dflow/specs/shared/dflow-workflows';
17
19
  const WORKFLOW_BUNDLE_MANIFEST_PATH = `${WORKFLOW_BUNDLE_DEST}/.dflow-bundle-manifest.json`;
18
20
  const COMMON_SKILL_SOURCE_REL = 'common/skill/SKILL.md';
@@ -110,16 +112,42 @@ const OPTIONAL_FILE_OPTIONS = [
110
112
  key: 'overview',
111
113
  label: '_overview.md - system overview',
112
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']
113
141
  },
114
142
  {
115
- key: 'git-trunk',
116
- label: 'Git principles - trunk-based',
117
- aliases: ['git principles - trunk-based', 'trunk', 'trunk-based']
143
+ key: 'co-authored-by',
144
+ label: 'Co-Authored-By trailer (dflow-ai) - filterable / auditable',
145
+ aliases: ['co-authored-by', 'co-author', 'trailer', 'coauthored']
118
146
  },
119
147
  {
120
- key: 'git-flow',
121
- label: 'Git principles - Git Flow',
122
- aliases: ['git principles - git flow', 'git flow', 'gitflow']
148
+ key: 'prefix',
149
+ label: '[ai-assisted] commit-message prefix - visible at a glance',
150
+ aliases: ['prefix', 'ai-assisted', '[ai-assisted]']
123
151
  }
124
152
  ];
125
153
 
@@ -612,6 +640,20 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
612
640
  proseLanguage = await askCustomProseLanguage(rl, stderr);
613
641
  }
614
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
+
615
657
  const optionalFiles = await askOptionalFiles(rl, stdout, stderr);
616
658
  const aiAgents = await askAiAgents(rl, stdout, stderr, detection.configuredAgents || []);
617
659
 
@@ -621,6 +663,8 @@ async function promptForAnswers(rl, stdout, stderr, detection) {
621
663
  techStackSummary,
622
664
  migrationContext,
623
665
  proseLanguage,
666
+ gitPolicy,
667
+ aiCommitMarker,
624
668
  optionalFiles,
625
669
  aiAgents
626
670
  };
@@ -645,10 +689,26 @@ async function inferProjectContext(cwd, rl, stdout, stderr) {
645
689
  techStackSummary: await inferTechStackSummary(cwd),
646
690
  migrationContext: await inferMigrationContext(cwd),
647
691
  proseLanguage: await inferProseLanguage(cwd),
692
+ gitPolicy: await inferGitPolicy(cwd),
693
+ aiCommitMarker: await inferAiCommitMarker(cwd),
648
694
  optionalFiles: []
649
695
  };
650
696
  }
651
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
+
652
712
  async function inferExistingEdition(cwd) {
653
713
  if (await pathExists(path.join(cwd, 'dflow/specs/architecture/tech-debt.md'))) {
654
714
  return 'greenfield';
@@ -767,33 +827,22 @@ async function askOptionalFiles(rl, stdout, stderr) {
767
827
  while (true) {
768
828
  stdout.write('\nWhich optional starter files should Dflow seed?\n');
769
829
  OPTIONAL_FILE_OPTIONS.forEach((option, index) => {
770
- const defaultMarker = option.key === 'overview' || option.key === 'git-trunk' ? ' (recommended)' : '';
830
+ const defaultMarker = option.key === 'overview' ? ' (recommended)' : '';
771
831
  stdout.write(` ${index + 1}. ${option.label}${defaultMarker}\n`);
772
832
  });
773
833
 
774
- const answer = await askLine(rl, 'Enter comma-separated choices, "none", or press Enter for recommended [1,2]: ');
775
- const parsed = parseMultiselectAnswer(answer, OPTIONAL_FILE_OPTIONS, ['overview', 'git-trunk']);
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']);
776
836
 
777
837
  if (!parsed.valid) {
778
838
  failedAttempts += 1;
779
839
  if (failedAttempts >= 3) {
780
- throw new InitError('Too many invalid attempts for Q5. Dflow init aborted.');
840
+ throw new InitError('Too many invalid attempts for Q7. Dflow init aborted.');
781
841
  }
782
842
  stderr.write(`${parsed.message} (${3 - failedAttempts} attempts left)\n`);
783
843
  continue;
784
844
  }
785
845
 
786
- if (parsed.values.includes('git-trunk') && parsed.values.includes('git-flow')) {
787
- const keepBoth = await askConfirmation(
788
- rl,
789
- 'You selected both Git principles templates. Most projects choose one. Keep both? (y/N) '
790
- );
791
- if (!keepBoth) {
792
- failedAttempts = 0;
793
- continue;
794
- }
795
- }
796
-
797
846
  return parsed.values;
798
847
  }
799
848
  }
@@ -997,6 +1046,7 @@ async function buildFilePlan(cwd, answers) {
997
1046
  content = substitutePlaceholders(content, substitution);
998
1047
  if (options.injectProseLanguage) {
999
1048
  content = ensureProseLanguageSection(content, answers.proseLanguage);
1049
+ content = ensureConventionPolicySections(content, answers);
1000
1050
  }
1001
1051
  items.push({
1002
1052
  relativePath,
@@ -1030,11 +1080,13 @@ async function buildFilePlan(cwd, answers) {
1030
1080
  if (answers.optionalFiles.includes('overview')) {
1031
1081
  await addTemplate('dflow/specs/shared/_overview.md', 'scaffolding/_overview.md', 'selected');
1032
1082
  }
1033
- if (answers.optionalFiles.includes('git-trunk')) {
1034
- await addTemplate('dflow/specs/shared/Git-principles-trunk.md', 'scaffolding/Git-principles-trunk.md', 'selected');
1035
- }
1036
- if (answers.optionalFiles.includes('git-flow')) {
1037
- 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');
1038
1090
  }
1039
1091
 
1040
1092
  if (answers.aiAgents.length > 0) {
@@ -1080,7 +1132,7 @@ async function buildConfigureAgentsPlan(cwd, answers) {
1080
1132
  const commandRegistry = answers.commandAdapters ? parseDflowCommandRegistry(content) : [];
1081
1133
 
1082
1134
  for (const agent of answers.aiAgents) {
1083
- await addAiAgentShim(cwd, items, agent, substitution, { commandRegistry });
1135
+ await addAiAgentShim(cwd, items, agent, substitution, { commandRegistry, warnings });
1084
1136
  }
1085
1137
 
1086
1138
  if (answers.commandAdapters) {
@@ -1308,15 +1360,48 @@ async function addAiAgentShim(cwd, items, agent, substitution, options = {}) {
1308
1360
  const targetExists = await pathExists(targetPath);
1309
1361
  const targetConfigured = targetExists && await fileReferencesAiAgentGuide(targetPath);
1310
1362
  const commandRegistry = options.commandRegistry || [];
1363
+ const warnings = options.warnings;
1311
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
+
1312
1400
  const relativePath = codexCommandAdapterSnippet
1313
- ? (targetConfigured ? 'dflow/specs/shared/AGENTS-md-command-adapters-snippet.md' : target.snippetPath)
1401
+ ? target.snippetPath
1314
1402
  : (targetExists && !targetConfigured ? target.snippetPath : target.relativePath);
1315
- const content = substitutePlaceholders(buildAiAgentShim(target.relativePath, options.commandRegistry), substitution);
1316
1403
  let notes = 'selected, tool-specific shim';
1317
- if (codexCommandAdapterSnippet && targetConfigured) {
1318
- notes = `selected, ${target.relativePath} already points to AI-AGENT-GUIDE.md; merge this command trigger snippet manually`;
1319
- } else if (codexCommandAdapterSnippet) {
1404
+ if (codexCommandAdapterSnippet) {
1320
1405
  notes = `selected, ${target.relativePath} already exists; merge this command trigger snippet manually`;
1321
1406
  } else if (targetConfigured) {
1322
1407
  notes = `selected, ${target.relativePath} already points to AI-AGENT-GUIDE.md`;
@@ -1326,7 +1411,7 @@ async function addAiAgentShim(cwd, items, agent, substitution, options = {}) {
1326
1411
 
1327
1412
  items.push({
1328
1413
  relativePath,
1329
- source: `generated:${agent}-shim`,
1414
+ source,
1330
1415
  notes,
1331
1416
  content,
1332
1417
  overwrite: codexCommandAdapterSnippet
@@ -1544,6 +1629,36 @@ function normalizeCommandAdapterFingerprint(content) {
1544
1629
  return String(content).replace(/\r\n/g, '\n');
1545
1630
  }
1546
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
+
1547
1662
  function buildCodexCommandTriggerSection(commandRegistry) {
1548
1663
  const triggers = commandRegistry
1549
1664
  .map((command) => {
@@ -1554,6 +1669,8 @@ function buildCodexCommandTriggerSection(commandRegistry) {
1554
1669
 
1555
1670
  return `
1556
1671
 
1672
+ ${CODEX_TRIGGER_SECTION_START}
1673
+
1557
1674
  ## Dflow Text Triggers
1558
1675
 
1559
1676
  Codex does not install Dflow command files. When the developer asks for a
@@ -1568,6 +1685,8 @@ and execute it.
1568
1685
  Recognized canonical triggers:
1569
1686
 
1570
1687
  ${triggers}
1688
+
1689
+ ${CODEX_TRIGGER_SECTION_END}
1571
1690
  `;
1572
1691
  }
1573
1692
 
@@ -1757,8 +1876,7 @@ const PLACEHOLDER_ALIASES = {
1757
1876
 
1758
1877
  function buildSubstitutionMap(cwd, answers) {
1759
1878
  const extracted = extractTechStackPlaceholders(answers.techStackSummary);
1760
- const gitSelection = answers.optionalFiles.filter((key) => key === 'git-trunk' || key === 'git-flow');
1761
- const gitStyle = gitSelection.length === 1 ? (gitSelection[0] === 'git-trunk' ? 'trunk' : 'gitflow') : null;
1879
+ const gitStyle = answers.gitPolicy === 'gitflow' ? 'gitflow' : (answers.gitPolicy === 'trunk' ? 'trunk' : null);
1762
1880
  const systemName = path.basename(cwd);
1763
1881
 
1764
1882
  const map = new Map([
@@ -1990,6 +2108,70 @@ function stripProseLanguageSections(content) {
1990
2108
  return kept.join('\n');
1991
2109
  }
1992
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
+
1993
2175
  function buildProseLanguageSection(proseLanguage) {
1994
2176
  return `## Prose Language
1995
2177
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dflow-sdd-ddd",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Spec-first SDD/DDD workflow kit for AI-assisted development",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -41,7 +41,7 @@
41
41
  "scripts": {
42
42
  "test": "node test/smoke.mjs"
43
43
  },
44
- "license": "MIT",
44
+ "license": "AGPL-3.0-or-later",
45
45
  "publishConfig": {
46
46
  "access": "public"
47
47
  }