@chenguangyao/devflow-kit 0.1.43

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 (198) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/LICENSE +21 -0
  3. package/README.md +539 -0
  4. package/bin/devflow.js +9 -0
  5. package/docs/RFC-001-devflow-kit.md +617 -0
  6. package/docs/RFC-002-workflow-kernel.md +134 -0
  7. package/docs/enterprise-integration-supplement.md +274 -0
  8. package/docs/internal-gitlab-setup.md +426 -0
  9. package/docs/marketplace-skills.md +231 -0
  10. package/docs/migration-from-arb.md +232 -0
  11. package/docs/tooling-overview.md +774 -0
  12. package/docs/workflow-orchestration.md +695 -0
  13. package/docs/workflow-ui-prototype.html +271 -0
  14. package/package.json +52 -0
  15. package/schemas/config.schema.json +51 -0
  16. package/schemas/delta.schema.json +22 -0
  17. package/schemas/state.schema.json +130 -0
  18. package/schemas/status-surface.schema.json +197 -0
  19. package/schemas/workflow-confirmation-surface.schema.json +70 -0
  20. package/schemas/workflow-picker.schema.json +94 -0
  21. package/scripts/postinstall.js +101 -0
  22. package/scripts/render-workflow-ui-prototype.js +271 -0
  23. package/skills/apply/SKILL.md +313 -0
  24. package/skills/apply/references/discipline-checklist.md +145 -0
  25. package/skills/apply/references/subagent-implementer-prompt.md +113 -0
  26. package/skills/apply/references/subagent-orchestration.md +150 -0
  27. package/skills/apply/references/subagent-reviewer-prompt.md +180 -0
  28. package/skills/apply/references/tdd-loop.md +287 -0
  29. package/skills/apply/references/when-plan-is-wrong.md +279 -0
  30. package/skills/apply/references/worktree-swarm.md +292 -0
  31. package/skills/archive/SKILL.md +229 -0
  32. package/skills/archive/references/conflict-resolution.md +336 -0
  33. package/skills/archive/references/knowledge-deposit.md +381 -0
  34. package/skills/archive/references/spec-merge.md +365 -0
  35. package/skills/brainstorm/SKILL.md +123 -0
  36. package/skills/brainstorm/references/proposal-template.md +244 -0
  37. package/skills/brainstorm/references/question-catalog.md +168 -0
  38. package/skills/brainstorm/references/session-template.md +184 -0
  39. package/skills/ci-fix/SKILL.md +63 -0
  40. package/skills/ci-fix/references/loop.md +25 -0
  41. package/skills/code-review/SKILL.md +279 -0
  42. package/skills/code-review/references/escalation-playbook.md +192 -0
  43. package/skills/code-review/references/language-cheatsheets/go.md +175 -0
  44. package/skills/code-review/references/language-cheatsheets/java-spring-mybatis.md +246 -0
  45. package/skills/code-review/references/language-cheatsheets/python.md +170 -0
  46. package/skills/code-review/references/language-cheatsheets/vue.md +199 -0
  47. package/skills/code-review/references/output-template.md +275 -0
  48. package/skills/code-review/references/review-checklist.md +251 -0
  49. package/skills/complexity-grading/SKILL.md +259 -0
  50. package/skills/deliver/SKILL.md +271 -0
  51. package/skills/deliver/references/delivery-modes.md +299 -0
  52. package/skills/deliver/references/notify.md +359 -0
  53. package/skills/deliver/references/pr-description.md +319 -0
  54. package/skills/dependency-upgrade/SKILL.md +57 -0
  55. package/skills/dependency-upgrade/references/risk-matrix.md +38 -0
  56. package/skills/df-orchestrator/SKILL.md +407 -0
  57. package/skills/df-orchestrator/references/complexity-grading.md +177 -0
  58. package/skills/df-orchestrator/references/escalation-matrix.md +191 -0
  59. package/skills/df-orchestrator/references/routing-rules.md +290 -0
  60. package/skills/df-orchestrator/references/workflow-state-machine.md +208 -0
  61. package/skills/frontend-quality/SKILL.md +61 -0
  62. package/skills/frontend-quality/references/checklist.md +35 -0
  63. package/skills/handoff-resume/SKILL.md +59 -0
  64. package/skills/handoff-resume/references/handoff-template.md +54 -0
  65. package/skills/plan/SKILL.md +166 -0
  66. package/skills/plan/references/task-breakdown.md +207 -0
  67. package/skills/plan/references/task-sequencing.md +143 -0
  68. package/skills/plan/references/task-template.md +248 -0
  69. package/skills/requirement-analysis/SKILL.md +499 -0
  70. package/skills/requirement-analysis/references/acceptance-criteria.md +183 -0
  71. package/skills/requirement-analysis/references/code-recon.md +151 -0
  72. package/skills/requirement-analysis/references/edge-case-catalog.md +164 -0
  73. package/skills/requirement-analysis/references/requirement-template.md +339 -0
  74. package/skills/requirement-analysis/references/scope-negotiation.md +162 -0
  75. package/skills/security-hardening/SKILL.md +60 -0
  76. package/skills/security-hardening/references/checklist.md +42 -0
  77. package/skills/tech-spec/SKILL.md +388 -0
  78. package/skills/tech-spec/references/api-contract-design.md +172 -0
  79. package/skills/tech-spec/references/decision-records.md +110 -0
  80. package/skills/tech-spec/references/design-template.md +301 -0
  81. package/skills/tech-spec/references/rollout-and-rollback.md +203 -0
  82. package/skills/tech-spec/references/spec-delta-conventions.md +250 -0
  83. package/skills/tech-spec/references/transaction-patterns.md +212 -0
  84. package/skills/test-spec/SKILL.md +219 -0
  85. package/skills/test-spec/references/coverage-strategy.md +218 -0
  86. package/skills/test-spec/references/edge-case-to-test.md +143 -0
  87. package/skills/test-spec/references/test-case-template.md +276 -0
  88. package/skills/verify/SKILL.md +232 -0
  89. package/skills/verify/references/nfr-verification.md +292 -0
  90. package/skills/verify/references/report-templates.md +510 -0
  91. package/skills/verify/references/self-test-guide.md +240 -0
  92. package/skills/verify/references/verify-rollback-map.md +247 -0
  93. package/src/cli/commands/_helpers.js +108 -0
  94. package/src/cli/commands/_submit.js +718 -0
  95. package/src/cli/commands/apply.js +198 -0
  96. package/src/cli/commands/archive.js +180 -0
  97. package/src/cli/commands/checkpoint.js +113 -0
  98. package/src/cli/commands/deliver.js +377 -0
  99. package/src/cli/commands/deploy.js +504 -0
  100. package/src/cli/commands/design.js +158 -0
  101. package/src/cli/commands/disable.js +21 -0
  102. package/src/cli/commands/doctor.js +178 -0
  103. package/src/cli/commands/enable.js +21 -0
  104. package/src/cli/commands/flow.js +645 -0
  105. package/src/cli/commands/help.js +93 -0
  106. package/src/cli/commands/ingest.js +602 -0
  107. package/src/cli/commands/init.js +341 -0
  108. package/src/cli/commands/knowledge.js +523 -0
  109. package/src/cli/commands/logs.js +43 -0
  110. package/src/cli/commands/new.js +202 -0
  111. package/src/cli/commands/plan.js +49 -0
  112. package/src/cli/commands/propose.js +27 -0
  113. package/src/cli/commands/provider.js +698 -0
  114. package/src/cli/commands/report.js +143 -0
  115. package/src/cli/commands/requirement.js +227 -0
  116. package/src/cli/commands/review.js +301 -0
  117. package/src/cli/commands/skills.js +457 -0
  118. package/src/cli/commands/status.js +925 -0
  119. package/src/cli/commands/switch.js +27 -0
  120. package/src/cli/commands/sync.js +47 -0
  121. package/src/cli/commands/test.js +366 -0
  122. package/src/cli/commands/uninstall.js +32 -0
  123. package/src/cli/commands/update.js +74 -0
  124. package/src/cli/commands/verify.js +354 -0
  125. package/src/cli/commands/worktree.js +78 -0
  126. package/src/cli/index.js +72 -0
  127. package/src/cli/parse-args.js +102 -0
  128. package/src/core/autodetect.js +271 -0
  129. package/src/core/change.js +208 -0
  130. package/src/core/checkpoint.js +217 -0
  131. package/src/core/config.js +60 -0
  132. package/src/core/delta.js +290 -0
  133. package/src/core/markers.js +59 -0
  134. package/src/core/paths.js +173 -0
  135. package/src/core/plan-tasks.js +36 -0
  136. package/src/core/project-routing.js +285 -0
  137. package/src/core/projects.js +200 -0
  138. package/src/core/state.js +200 -0
  139. package/src/core/workflow-check.js +177 -0
  140. package/src/core/workflow-init.js +34 -0
  141. package/src/core/workflow-picker.js +154 -0
  142. package/src/core/workflow-policy.js +119 -0
  143. package/src/core/workflow-suggest.js +181 -0
  144. package/src/core/workflow-verify.js +88 -0
  145. package/src/core/workflow.js +433 -0
  146. package/src/core/worktree.js +241 -0
  147. package/src/knowledge/categories.js +107 -0
  148. package/src/knowledge/classify.js +125 -0
  149. package/src/knowledge/deposit.js +414 -0
  150. package/src/knowledge/migrate.js +149 -0
  151. package/src/knowledge/mr.js +219 -0
  152. package/src/knowledge/query.js +131 -0
  153. package/src/knowledge/registry.js +151 -0
  154. package/src/knowledge/sync.js +179 -0
  155. package/src/providers/base.js +74 -0
  156. package/src/providers/drivers/api-yapi.js +78 -0
  157. package/src/providers/drivers/ci-jenkins.js +109 -0
  158. package/src/providers/drivers/intake-confluence.js +544 -0
  159. package/src/providers/drivers/kb-git.js +549 -0
  160. package/src/providers/drivers/kb-weknora.js +472 -0
  161. package/src/providers/drivers/notify-smtp.js +515 -0
  162. package/src/providers/drivers/observability-oss.js +43 -0
  163. package/src/providers/drivers/observability-sls.js +50 -0
  164. package/src/providers/lifecycle.js +135 -0
  165. package/src/providers/loader.js +132 -0
  166. package/src/providers/local.js +190 -0
  167. package/src/providers/userconfig.js +283 -0
  168. package/src/reports/aggregate.js +185 -0
  169. package/src/reports/coverage.js +163 -0
  170. package/src/reports/detect.js +143 -0
  171. package/src/reports/parse.js +236 -0
  172. package/src/templates/files/ci/github.yml +38 -0
  173. package/src/templates/files/ci/gitlab.yml +27 -0
  174. package/src/templates/files/design.md +63 -0
  175. package/src/templates/files/ide/devflow-workflow.md +58 -0
  176. package/src/templates/files/ide/project-overview-reference.md +1 -0
  177. package/src/templates/files/ide/project-overview.md +27 -0
  178. package/src/templates/files/knowledge-index.json +17 -0
  179. package/src/templates/files/knowledge.md +28 -0
  180. package/src/templates/files/meta.json +8 -0
  181. package/src/templates/files/plan.md +38 -0
  182. package/src/templates/files/proposal.md +33 -0
  183. package/src/templates/files/reports/contract-test.md +40 -0
  184. package/src/templates/files/reports/e2e-test.md +30 -0
  185. package/src/templates/files/reports/integration-test.md +36 -0
  186. package/src/templates/files/reports/joint-test.md +58 -0
  187. package/src/templates/files/reports/perf.md +24 -0
  188. package/src/templates/files/reports/regression.md +20 -0
  189. package/src/templates/files/reports/remote-test.md +55 -0
  190. package/src/templates/files/reports/self-test.md +43 -0
  191. package/src/templates/files/reports/smoke-test.md +22 -0
  192. package/src/templates/files/reports/unit-test.md +36 -0
  193. package/src/templates/files/requirement.md +51 -0
  194. package/src/templates/files/review.md +38 -0
  195. package/src/templates/files/tests.md +36 -0
  196. package/src/templates/files/verify.md +32 -0
  197. package/src/templates/index.js +21 -0
  198. package/src/utils/log.js +37 -0
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const fsSync = require('fs');
4
+ const path = require('path');
5
+ const paths = require('./paths.js');
6
+
7
+ const DEFAULT_CONFIG = {
8
+ $schema: 'https://devflow.dev/schemas/config.schema.json',
9
+ version: 1,
10
+ ide: {
11
+ targets: ['claude', 'cursor', 'agents'],
12
+ },
13
+ projectOverview: {
14
+ mode: 'reference',
15
+ candidates: [
16
+ '.claude/CLAUDE.md',
17
+ 'CLAUDE.md',
18
+ 'PROJECT.md',
19
+ 'AGENTS.md',
20
+ '.cursor/rules/project.mdc',
21
+ 'docs/project-overview.md',
22
+ 'docs/architecture.md',
23
+ 'README.md',
24
+ ],
25
+ referencedFile: null,
26
+ },
27
+ knowledge: {
28
+ layout: 'improved-9',
29
+ domains: ['1-domain', '2-system', '3-ops', '4-archive'],
30
+ },
31
+ defaults: {
32
+ level: 'L2',
33
+ workflow: 'standard',
34
+ branchPrefix: 'feature',
35
+ },
36
+ };
37
+
38
+ async function read(root) {
39
+ const file = paths.configFile(root);
40
+ if (!fsSync.existsSync(file)) return null;
41
+ const buf = await fs.readFile(file, 'utf8');
42
+ return JSON.parse(buf);
43
+ }
44
+
45
+ async function write(root, cfg) {
46
+ const file = paths.configFile(root);
47
+ await fs.mkdir(path.dirname(file), { recursive: true });
48
+ await fs.writeFile(file, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
49
+ }
50
+
51
+ async function ensure(root) {
52
+ let cfg = await read(root);
53
+ if (!cfg) {
54
+ cfg = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
55
+ await write(root, cfg);
56
+ }
57
+ return cfg;
58
+ }
59
+
60
+ module.exports = { DEFAULT_CONFIG, read, write, ensure };
@@ -0,0 +1,290 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const fsSync = require('fs');
4
+ const path = require('path');
5
+ const paths = require('./paths.js');
6
+
7
+ /**
8
+ * Supported section labels (case-insensitive).
9
+ * Both "Requirements" (legacy) and "Capabilities" (OpenSpec) are accepted.
10
+ */
11
+ const SECTION_RE = /^##\s+(ADDED|MODIFIED|REMOVED)\s+(Capabilities?|Requirements?)\s*$/i;
12
+
13
+ /**
14
+ * Parse YAML-ish frontmatter from a markdown file.
15
+ * Only reads simple "key: value" pairs (no nesting).
16
+ * Returns { capability?, slug?, [key]: value }
17
+ */
18
+ function parseFrontmatter(md) {
19
+ const m = /^---\s*\n([\s\S]*?)\n---/.exec(md);
20
+ if (!m) return {};
21
+ const meta = {};
22
+ for (const line of m[1].split('\n')) {
23
+ const kv = /^(\w[\w-]*):\s*(.+)$/.exec(line.trim());
24
+ if (kv) meta[kv[1]] = kv[2].trim();
25
+ }
26
+ return meta;
27
+ }
28
+
29
+ /**
30
+ * Parse a single OpenSpec delta markdown file.
31
+ * Handles both:
32
+ * ## ADDED Requirements (OpenSpec Requirements format)
33
+ * ## ADDED Capabilities (legacy compatibility)
34
+ *
35
+ * Returns:
36
+ * {
37
+ * meta: { capability?, slug? },
38
+ * added: [{ heading, body }],
39
+ * modified: [{ heading, body }],
40
+ * removed: [{ heading, body }],
41
+ * rejected: [{ heading, body }], // REJECTED Alternatives — appended verbatim
42
+ * }
43
+ */
44
+ function parseDelta(md) {
45
+ const result = { meta: parseFrontmatter(md), added: [], modified: [], removed: [], rejected: [] };
46
+ const lines = md.split(/\r?\n/);
47
+ let section = null;
48
+ let item = null;
49
+
50
+ for (const line of lines) {
51
+ // Section header: ## ADDED Requirements / ## MODIFIED Capabilities / etc.
52
+ const m2 = SECTION_RE.exec(line);
53
+ if (m2) {
54
+ section = m2[1].toLowerCase(); // 'added' | 'modified' | 'removed'
55
+ item = null;
56
+ continue;
57
+ }
58
+ // REJECTED Alternatives section
59
+ if (/^##\s+REJECTED\s+Alternatives?\s*$/i.test(line)) {
60
+ section = 'rejected';
61
+ item = null;
62
+ continue;
63
+ }
64
+
65
+ if (!section) continue;
66
+
67
+ // Sub-heading starts a new item block
68
+ const m3 = /^###\s+(.*?)\s*$/.exec(line);
69
+ if (m3) {
70
+ item = { heading: m3[1].trim(), body: '' };
71
+ result[section].push(item);
72
+ continue;
73
+ }
74
+
75
+ if (item) item.body += (item.body ? '\n' : '') + line;
76
+ }
77
+ return result;
78
+ }
79
+
80
+ /**
81
+ * Apply parsed delta to a target spec markdown (OpenSpec format).
82
+ *
83
+ * - ADDED Requirements → append under "## Requirements" section
84
+ * - MODIFIED Requirements → find `### heading`; replace body until next heading
85
+ * - REMOVED Requirements → drop `### heading` + body
86
+ * - REJECTED Alternatives → append under "## REJECTED Alternatives" section
87
+ *
88
+ * Returns { content, conflicts: [{ heading, kind, reason }] }
89
+ */
90
+ function applyToSpec(specMd, parsed) {
91
+ let content = specMd || '';
92
+ const conflicts = [];
93
+ let lines = content.split(/\r?\n/);
94
+
95
+ const findHeading = (heading) => {
96
+ for (let i = 0; i < lines.length; i++) {
97
+ if (/^###\s+/.test(lines[i]) && lines[i].slice(4).trim() === heading) return i;
98
+ }
99
+ return -1;
100
+ };
101
+ const blockEnd = (start) => {
102
+ for (let j = start + 1; j < lines.length; j++) {
103
+ if (/^##?#\s+/.test(lines[j])) return j;
104
+ }
105
+ return lines.length;
106
+ };
107
+
108
+ // REMOVED first (so MODIFIED doesn't operate on deleted blocks)
109
+ for (const it of parsed.removed) {
110
+ const idx = findHeading(it.heading);
111
+ if (idx === -1) {
112
+ conflicts.push({ heading: it.heading, kind: 'removed', reason: 'heading not found' });
113
+ continue;
114
+ }
115
+ lines.splice(idx, blockEnd(idx) - idx);
116
+ }
117
+
118
+ // MODIFIED
119
+ for (const it of parsed.modified) {
120
+ const idx = findHeading(it.heading);
121
+ if (idx === -1) {
122
+ conflicts.push({ heading: it.heading, kind: 'modified', reason: 'heading not found' });
123
+ continue;
124
+ }
125
+ const newBlock = (`### ${it.heading}\n` + it.body.replace(/^\n+|\n+$/g, '')).split('\n');
126
+ lines.splice(idx, blockEnd(idx) - idx, ...newBlock, '');
127
+ }
128
+
129
+ // ADDED Requirements → append into the OpenSpec Requirements section.
130
+ if (parsed.added.length) {
131
+ let reqIdx = lines.findIndex((l) => /^##\s+Requirements?\s*$/.test(l));
132
+ if (reqIdx === -1) {
133
+ if (lines.length === 0 || lines[lines.length - 1] !== '') lines.push('');
134
+ lines.push('## Requirements', '');
135
+ reqIdx = lines.length - 2;
136
+ }
137
+
138
+ let insertAt = lines.length;
139
+ for (let i = reqIdx + 1; i < lines.length; i++) {
140
+ if (/^##\s+/.test(lines[i])) {
141
+ insertAt = i;
142
+ break;
143
+ }
144
+ }
145
+
146
+ const blocks = [];
147
+ if (insertAt > 0 && lines[insertAt - 1] !== '') blocks.push('');
148
+ for (const it of parsed.added) {
149
+ blocks.push(`### ${it.heading}`);
150
+ if (it.body) blocks.push(it.body.replace(/^\n+|\n+$/g, ''));
151
+ blocks.push('');
152
+ }
153
+ lines.splice(insertAt, 0, ...blocks);
154
+ }
155
+
156
+ // REJECTED Alternatives → append / merge into dedicated section
157
+ if (parsed.rejected.length) {
158
+ if (lines.length === 0 || lines[lines.length - 1] !== '') lines.push('');
159
+ const rejIdx = lines.findIndex((l) => /^##\s+REJECTED\s+Alternatives?/.test(l));
160
+ if (rejIdx === -1) lines.push('## REJECTED Alternatives', '');
161
+ for (const it of parsed.rejected) {
162
+ lines.push(`### ${it.heading}`);
163
+ if (it.body) lines.push(it.body.replace(/^\n+|\n+$/g, ''));
164
+ lines.push('');
165
+ }
166
+ }
167
+
168
+ return { content: lines.join('\n'), conflicts };
169
+ }
170
+
171
+ /**
172
+ * Resolve the target spec path for a delta file.
173
+ *
174
+ * Priority:
175
+ * 1. frontmatter `capability:` field e.g. "coupon/grant" → specs/coupon/spec.md
176
+ * 2. filename (basename without .md) e.g. "coupon-grant.md" → specs/coupon-grant.md
177
+ *
178
+ * The `capability` value supports:
179
+ * - Simple: "coupon" → specs/coupon/spec.md
180
+ * - Nested: "coupon/grant" → specs/coupon/spec.md
181
+ * - Deep: "payment/alipay/v2" → specs/payment/spec.md
182
+ */
183
+ function resolveSpecTarget(specsRoot, deltaFile, meta) {
184
+ const cap = (meta.capability || '').trim().replace(/\.md$/, '');
185
+ if (cap) {
186
+ const domain = cap.split('/').filter(Boolean)[0];
187
+ if (domain) return path.join(specsRoot, domain, 'spec.md');
188
+ }
189
+ // fallback: use basename (legacy behaviour)
190
+ return path.join(specsRoot, path.basename(deltaFile));
191
+ }
192
+
193
+ /**
194
+ * Scaffold content for a brand-new spec file (no existing content).
195
+ * Uses the spec domain from path for the title.
196
+ */
197
+ function newSpecContent(specPath, slug) {
198
+ const isDomainSpec = path.basename(specPath) === 'spec.md';
199
+ const name = (isDomainSpec ? path.basename(path.dirname(specPath)) : path.basename(specPath, '.md')).replace(/-/g, ' ');
200
+ return [
201
+ `<!-- auto-created by devflow archive -->`,
202
+ `<!-- edit via devflow/changes/<slug>/delta/ — do not edit directly -->`,
203
+ '',
204
+ `# ${name} Specification`,
205
+ '',
206
+ `## Purpose`,
207
+ '',
208
+ `TBD - created by archiving change ${slug}. Update Purpose after archive.`,
209
+ '',
210
+ `## Requirements`,
211
+ '',
212
+ ].join('\n');
213
+ }
214
+
215
+ /**
216
+ * Recursively collect all *.md files under a directory.
217
+ */
218
+ async function collectDeltaFiles(dir) {
219
+ const out = [];
220
+ if (!fsSync.existsSync(dir)) return out;
221
+ const entries = await fs.readdir(dir, { withFileTypes: true });
222
+ for (const e of entries) {
223
+ const full = path.join(dir, e.name);
224
+ if (e.isDirectory()) {
225
+ out.push(...await collectDeltaFiles(full));
226
+ } else if (e.name.endsWith('.md') && e.name !== 'sample-capability.md') {
227
+ out.push(full);
228
+ }
229
+ }
230
+ return out;
231
+ }
232
+
233
+ /**
234
+ * Merge all delta/*.md (recursive) from a change folder into devflow/specs/.
235
+ *
236
+ * Target path is determined per-file by `capability` frontmatter:
237
+ * capability: coupon/grant → specs/coupon/spec.md
238
+ * capability: order → specs/order/spec.md
239
+ * (no capability) → specs/<filename>.md (legacy)
240
+ *
241
+ * Returns { merged: [{ file, target }], conflicts: [{ file, conflicts }] }
242
+ */
243
+ async function mergeChangeDeltas(root, slug, opts = {}) {
244
+ const deltaDir = path.join(paths.changeDir(root, slug), 'delta');
245
+ const deltaFiles = await collectDeltaFiles(deltaDir);
246
+
247
+ if (!deltaFiles.length) return { merged: [], conflicts: [] };
248
+
249
+ const specsDir = paths.specsDir(root);
250
+ const merged = [];
251
+ const allConflicts = [];
252
+
253
+ for (const absFile of deltaFiles) {
254
+ const md = await fs.readFile(absFile, 'utf8');
255
+ const parsed = parseDelta(md);
256
+
257
+ // Skip files that have no capability changes at all
258
+ const isEmpty = !parsed.added.length && !parsed.modified.length
259
+ && !parsed.removed.length && !parsed.rejected.length;
260
+ if (isEmpty) continue;
261
+
262
+ const target = resolveSpecTarget(specsDir, path.basename(absFile), parsed.meta);
263
+
264
+ let existing = '';
265
+ if (fsSync.existsSync(target)) {
266
+ existing = await fs.readFile(target, 'utf8');
267
+ } else {
268
+ existing = newSpecContent(target, slug);
269
+ }
270
+
271
+ const { content, conflicts } = applyToSpec(existing, parsed);
272
+
273
+ if (conflicts.length && !opts.force) {
274
+ allConflicts.push({ file: path.relative(root, absFile), conflicts });
275
+ continue;
276
+ }
277
+
278
+ if (!opts.dryRun) {
279
+ await fs.mkdir(path.dirname(target), { recursive: true });
280
+ const header = `<!-- merged-from: change=${slug} ts=${new Date().toISOString()} -->\n`;
281
+ const needsHeader = !content.includes('merged-from:');
282
+ await fs.writeFile(target, needsHeader ? header + content : content, 'utf8');
283
+ }
284
+ merged.push({ file: path.relative(root, absFile), target });
285
+ }
286
+
287
+ return { merged, conflicts: allConflicts };
288
+ }
289
+
290
+ module.exports = { parseDelta, applyToSpec, mergeChangeDeltas, parseFrontmatter, resolveSpecTarget };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const path = require('path');
4
+
5
+ function begin(name) { return `<!-- BEGIN devflow:${name} -->`; }
6
+ function end(name) { return `<!-- END devflow:${name} -->`; }
7
+
8
+ /**
9
+ * Idempotently upsert a marker section in a markdown file.
10
+ * If file does not exist, creates it with just the section.
11
+ */
12
+ async function upsertSection(file, name, body, opts = {}) {
13
+ const header = opts.header || '';
14
+ const beginTag = begin(name);
15
+ const endTag = end(name);
16
+ const block = `${beginTag}\n${body}\n${endTag}`;
17
+
18
+ let content = '';
19
+ try {
20
+ content = await fs.readFile(file, 'utf8');
21
+ } catch (e) {
22
+ if (e.code !== 'ENOENT') throw e;
23
+ }
24
+
25
+ if (content.includes(beginTag) && content.includes(endTag)) {
26
+ const re = new RegExp(escapeRe(beginTag) + '[\\s\\S]*?' + escapeRe(endTag), 'm');
27
+ content = content.replace(re, block);
28
+ } else {
29
+ if (content && !content.endsWith('\n')) content += '\n';
30
+ content += (content ? '\n' : header ? header + '\n\n' : '') + block + '\n';
31
+ }
32
+
33
+ await fs.mkdir(path.dirname(file), { recursive: true });
34
+ await fs.writeFile(file, content, 'utf8');
35
+ return file;
36
+ }
37
+
38
+ async function removeSection(file, name) {
39
+ const beginTag = begin(name);
40
+ const endTag = end(name);
41
+ let content;
42
+ try {
43
+ content = await fs.readFile(file, 'utf8');
44
+ } catch (e) {
45
+ if (e.code === 'ENOENT') return false;
46
+ throw e;
47
+ }
48
+ const re = new RegExp('\\n*' + escapeRe(beginTag) + '[\\s\\S]*?' + escapeRe(endTag) + '\\n*', 'm');
49
+ if (!re.test(content)) return false;
50
+ content = content.replace(re, '\n');
51
+ await fs.writeFile(file, content, 'utf8');
52
+ return true;
53
+ }
54
+
55
+ function escapeRe(s) {
56
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
57
+ }
58
+
59
+ module.exports = { begin, end, upsertSection, removeSection };
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ const DEVFLOW_DIR = 'devflow';
6
+
7
+ function repoRoot(start) {
8
+ let cur = start || process.cwd();
9
+ // Search for .git or devflow/ marker upward
10
+ for (let i = 0; i < 32; i++) {
11
+ if (
12
+ fs.existsSync(path.join(cur, '.git')) ||
13
+ fs.existsSync(path.join(cur, DEVFLOW_DIR, 'config.json'))
14
+ ) {
15
+ return cur;
16
+ }
17
+ const parent = path.dirname(cur);
18
+ if (parent === cur) break;
19
+ cur = parent;
20
+ }
21
+ // Fallback: cwd
22
+ return start || process.cwd();
23
+ }
24
+
25
+ function devflowDir(root) {
26
+ return path.join(root, DEVFLOW_DIR);
27
+ }
28
+
29
+ function specsDir(root) {
30
+ return path.join(devflowDir(root), 'specs');
31
+ }
32
+
33
+ function knowledgeDir(root) {
34
+ return path.join(devflowDir(root), 'knowledge');
35
+ }
36
+
37
+ function changesDir(root) {
38
+ return path.join(workspaceBaseDir(), 'changes');
39
+ }
40
+
41
+ function archiveDir(root) {
42
+ return path.join(workspaceBaseDir(), 'archive');
43
+ }
44
+
45
+ function changeDir(root, slug) {
46
+ return path.join(changesDir(root), slug);
47
+ }
48
+
49
+ function stateFile(root, slug) {
50
+ return path.join(changeDir(root, slug), 'state.json');
51
+ }
52
+
53
+ function configFile(root) {
54
+ return path.join(devflowDir(root), 'config.json');
55
+ }
56
+
57
+ function projectProvidersFile(root) {
58
+ return path.join(devflowDir(root), 'providers.json');
59
+ }
60
+
61
+ function userConfigDir() {
62
+ const home = process.env.HOME || process.env.USERPROFILE || '';
63
+ return path.join(home, '.devflow');
64
+ }
65
+
66
+ function legacyUserConfigDir() {
67
+ const home = process.env.HOME || process.env.USERPROFILE || '';
68
+ return path.join(home, '.config', 'devflow');
69
+ }
70
+
71
+ function userProvidersFile() {
72
+ return path.join(userConfigDir(), 'providers.json');
73
+ }
74
+
75
+ function userProvidersExampleFile() {
76
+ return path.join(userConfigDir(), 'providers.example.json');
77
+ }
78
+
79
+ function userProjectsFile() {
80
+ return path.join(userConfigDir(), 'projects.json');
81
+ }
82
+
83
+ function legacyUserProvidersFile() {
84
+ return path.join(legacyUserConfigDir(), 'providers.json');
85
+ }
86
+
87
+ function legacyUserProjectsFile() {
88
+ return path.join(legacyUserConfigDir(), 'projects.json');
89
+ }
90
+
91
+ function workspaceBaseDir() {
92
+ const home = process.env.HOME || process.env.USERPROFILE || '';
93
+ return path.join(home, '.devflow', 'workspace');
94
+ }
95
+
96
+ function workspaceKnowledgeRoot() {
97
+ return path.join(workspaceBaseDir(), 'knowledge');
98
+ }
99
+
100
+ function safeWorkspaceSlug(slug) {
101
+ return String(slug || 'change')
102
+ .normalize('NFKC')
103
+ .replace(/[^a-zA-Z0-9\u4e00-\u9fa5-]+/g, '-')
104
+ .replace(/-{2,}/g, '-')
105
+ .replace(/^-|-$/g, '')
106
+ .slice(0, 80) || 'change';
107
+ }
108
+
109
+ function workspaceDir(slug) {
110
+ const base = workspaceBaseDir();
111
+ const safe = safeWorkspaceSlug(slug);
112
+ const changesBase = path.join(base, 'changes');
113
+ if (fs.existsSync(changesBase)) {
114
+ const entries = fs.readdirSync(changesBase, { withFileTypes: true });
115
+ const existing = entries
116
+ .filter((e) => e.isDirectory())
117
+ .map((e) => e.name)
118
+ .find((name) => name === safe);
119
+ if (existing) return path.join(changesBase, existing);
120
+ }
121
+ return path.join(changesBase, safe);
122
+ }
123
+
124
+ function workspaceChangesDir(slug) {
125
+ return workspaceDir(slug);
126
+ }
127
+
128
+ function workspaceKnowledgeDir(slug) {
129
+ return path.join(workspaceDir(slug), 'knowledge');
130
+ }
131
+
132
+ function workspaceArchiveDir(slug) {
133
+ const date = new Date().toISOString().slice(0, 10);
134
+ return path.join(workspaceBaseDir(), 'archive', `${date}-${safeWorkspaceSlug(slug)}`);
135
+ }
136
+
137
+ function worktreesDir(root) {
138
+ return path.join(root, '.devflow-worktrees');
139
+ }
140
+
141
+ function worktreePath(root, slug, taskId) {
142
+ if (!taskId) return path.join(worktreesDir(root), slug);
143
+ return path.join(worktreesDir(root), slug, taskId);
144
+ }
145
+
146
+ module.exports = {
147
+ DEVFLOW_DIR,
148
+ repoRoot,
149
+ devflowDir,
150
+ specsDir,
151
+ knowledgeDir,
152
+ changesDir,
153
+ archiveDir,
154
+ changeDir,
155
+ stateFile,
156
+ configFile,
157
+ projectProvidersFile,
158
+ userConfigDir,
159
+ legacyUserConfigDir,
160
+ userProvidersFile,
161
+ userProvidersExampleFile,
162
+ userProjectsFile,
163
+ legacyUserProvidersFile,
164
+ legacyUserProjectsFile,
165
+ workspaceBaseDir,
166
+ workspaceKnowledgeRoot,
167
+ workspaceDir,
168
+ workspaceChangesDir,
169
+ workspaceKnowledgeDir,
170
+ workspaceArchiveDir,
171
+ worktreesDir,
172
+ worktreePath,
173
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const change = require('./change.js');
5
+
6
+ function readPlanTaskIds(root, slug) {
7
+ const file = path.join(change.resolveChangeDir(root, slug), 'plan.md');
8
+ if (!fs.existsSync(file)) return [];
9
+ const content = fs.readFileSync(file, 'utf8');
10
+ return parsePlanTaskIds(content);
11
+ }
12
+
13
+ function parsePlanTaskIds(content) {
14
+ const headingIds = collectTaskIds(content, /^#{2,6}\s+(?:Task\s*)?(?:T|task)-0*(\d+)\b/gim);
15
+ if (headingIds.length) return headingIds;
16
+
17
+ const tableIds = collectTaskIds(content, /^\|\s*(?:T|task)-0*(\d+)\s*\|/gim);
18
+ if (tableIds.length) return tableIds;
19
+
20
+ return collectTaskIds(content, /\b(?:T|task)-0*(\d+)\b/gi);
21
+ }
22
+
23
+ function collectTaskIds(content, re) {
24
+ const ids = [];
25
+ const seen = new Set();
26
+ let m;
27
+ while ((m = re.exec(content)) !== null) {
28
+ const id = `task-${parseInt(m[1], 10)}`;
29
+ if (seen.has(id)) continue;
30
+ seen.add(id);
31
+ ids.push(id);
32
+ }
33
+ return ids;
34
+ }
35
+
36
+ module.exports = { readPlanTaskIds, parsePlanTaskIds };