@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,271 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const fsp = require('fs/promises');
4
+ const path = require('path');
5
+ const cp = require('child_process');
6
+
7
+ /**
8
+ * Auto-detection helpers for `devflow init` — all functions return safe defaults
9
+ * when detection fails (never throw).
10
+ *
11
+ * Mirrors arb-workflow-kit/skills/dev-workflow/scripts/init-project.py but
12
+ * ships as a library rather than a generator script.
13
+ */
14
+
15
+ /** Detect git remote URL; returns null when not a git repo or no remote. */
16
+ function gitRemote(root, name = 'origin') {
17
+ try {
18
+ const r = cp.spawnSync('git', ['config', '--get', `remote.${name}.url`], {
19
+ cwd: root, encoding: 'utf8',
20
+ });
21
+ if (r.status !== 0) return null;
22
+ const url = (r.stdout || '').trim();
23
+ return url || null;
24
+ } catch { return null; }
25
+ }
26
+
27
+ /**
28
+ * Parse a git remote URL into { host, group, project }.
29
+ *
30
+ * Handles:
31
+ * - git@gitlab.corp.com:group/subgroup/project.git
32
+ * - ssh://git@gitlab.corp.com:2222/group/project.git
33
+ * - https://gitlab.corp.com/group/project
34
+ * - https://gitlab.corp.com/group/subgroup/project.git
35
+ */
36
+ function parseRemote(url) {
37
+ if (!url) return null;
38
+ let host, pathPart;
39
+
40
+ // git@host:group/project.git
41
+ const sshShort = /^([\w.-]+@)?([\w.-]+):(.+?)(?:\.git)?\/?$/.exec(url);
42
+ // ssh://git@host[:port]/group/project(.git)?
43
+ const sshLong = /^ssh:\/\/(?:[\w.-]+@)?([\w.-]+)(?::\d+)?\/(.+?)(?:\.git)?\/?$/.exec(url);
44
+ // https?://host/group/project(.git)?
45
+ const https = /^https?:\/\/(?:[^@/]+@)?([\w.-]+)(?::\d+)?\/(.+?)(?:\.git)?\/?$/.exec(url);
46
+
47
+ if (sshLong) { host = sshLong[1]; pathPart = sshLong[2]; }
48
+ else if (https) { host = https[1]; pathPart = https[2]; }
49
+ else if (sshShort) { host = sshShort[2]; pathPart = sshShort[3]; }
50
+ else return null;
51
+
52
+ pathPart = pathPart.replace(/\.git$/, '').replace(/^\/+|\/+$/g, '');
53
+ const segs = pathPart.split('/').filter(Boolean);
54
+ if (segs.length < 2) return { host, group: '', project: segs[0] || '', fullPath: pathPart };
55
+ const project = segs.pop();
56
+ const group = segs.join('/');
57
+ return { host, group, project, fullPath: `${group}/${project}` };
58
+ }
59
+
60
+ /**
61
+ * Detect the repo's default branch. Returns a best guess branch name
62
+ * (never throws). Priority:
63
+ *
64
+ * 1. `git symbolic-ref refs/remotes/origin/HEAD` — what the remote marks
65
+ * as HEAD (authoritative for cloned repos)
66
+ * 2. Existence of remote tracking `origin/master` → master
67
+ * 3. Existence of remote tracking `origin/main` → main
68
+ * 4. Existence of local branch `master` → master
69
+ * 5. Existence of local branch `main` → main
70
+ * 6. `git config init.defaultBranch` (when explicitly set)
71
+ * 7. Literal `'main'` (git 2.28+ default)
72
+ *
73
+ * We prefer `master` ahead of `main` when both remote/local flags must be
74
+ * inspected, because legacy repos (common in internal GitLab) still default
75
+ * to master; newer repos declare their HEAD via step 1.
76
+ */
77
+ function detectDefaultBranch(root) {
78
+ const run = (args) => {
79
+ try { return cp.spawnSync('git', args, { cwd: root, encoding: 'utf8' }); }
80
+ catch { return { status: 1, stdout: '', stderr: '' }; }
81
+ };
82
+
83
+ // 1. origin/HEAD symbolic ref — most reliable when present
84
+ let r = run(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
85
+ if (r.status === 0) {
86
+ const full = (r.stdout || '').trim(); // e.g. "origin/master"
87
+ if (full) {
88
+ const branch = full.replace(/^origin\//, '');
89
+ if (branch) return branch;
90
+ }
91
+ }
92
+
93
+ // 2-3. Remote tracking branches
94
+ for (const name of ['master', 'main']) {
95
+ r = run(['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${name}`]);
96
+ if (r.status === 0) return name;
97
+ }
98
+
99
+ // 4-5. Local branches (e.g. fresh repo, no remote yet)
100
+ for (const name of ['master', 'main']) {
101
+ r = run(['rev-parse', '--verify', '--quiet', `refs/heads/${name}`]);
102
+ if (r.status === 0) return name;
103
+ }
104
+
105
+ // 6. Respect explicit git config
106
+ r = run(['config', '--get', 'init.defaultBranch']);
107
+ if (r.status === 0) {
108
+ const v = (r.stdout || '').trim();
109
+ if (v) return v;
110
+ }
111
+
112
+ // 7. Sensible fallback (matches modern git default)
113
+ return 'main';
114
+ }
115
+
116
+ /**
117
+ * Detect primary language / stack from lock + manifest files.
118
+ * Returns first match (priority ordered) or 'unknown'.
119
+ */
120
+ function detectLanguage(root) {
121
+ const checks = [
122
+ { f: 'go.mod', lang: 'go' },
123
+ { f: 'package.json', lang: 'node' },
124
+ { f: 'pyproject.toml', lang: 'python' },
125
+ { f: 'requirements.txt', lang: 'python' },
126
+ { f: 'setup.cfg', lang: 'python' },
127
+ { f: 'Cargo.toml', lang: 'rust' },
128
+ { f: 'pom.xml', lang: 'java' },
129
+ { f: 'build.gradle', lang: 'java' },
130
+ { f: 'build.gradle.kts', lang: 'kotlin' },
131
+ { f: 'composer.json', lang: 'php' },
132
+ { f: 'Gemfile', lang: 'ruby' },
133
+ { f: 'mix.exs', lang: 'elixir' },
134
+ { f: '*.csproj', lang: 'dotnet', glob: true },
135
+ ];
136
+
137
+ for (const c of checks) {
138
+ if (c.glob) {
139
+ try {
140
+ const files = fs.readdirSync(root);
141
+ if (files.some((n) => n.endsWith('.csproj'))) return c.lang;
142
+ } catch { /* ignore */ }
143
+ } else if (fs.existsSync(path.join(root, c.f))) {
144
+ return c.lang;
145
+ }
146
+ }
147
+ return 'unknown';
148
+ }
149
+
150
+ /**
151
+ * Detect package manager / build tool for the current project.
152
+ */
153
+ function detectBuildTool(root, lang) {
154
+ const exists = (f) => fs.existsSync(path.join(root, f));
155
+ if (lang === 'node') {
156
+ if (exists('pnpm-lock.yaml')) return 'pnpm';
157
+ if (exists('yarn.lock')) return 'yarn';
158
+ if (exists('bun.lockb')) return 'bun';
159
+ return 'npm';
160
+ }
161
+ if (lang === 'python') {
162
+ if (exists('poetry.lock')) return 'poetry';
163
+ if (exists('uv.lock')) return 'uv';
164
+ if (exists('Pipfile.lock')) return 'pipenv';
165
+ return 'pip';
166
+ }
167
+ if (lang === 'java') return exists('pom.xml') ? 'maven' : 'gradle';
168
+ if (lang === 'go') return 'go';
169
+ return '';
170
+ }
171
+
172
+ /**
173
+ * Suggest a Jenkins job name based on project name + common conventions.
174
+ * Returns a list of candidates so UI can offer choices.
175
+ *
176
+ * - <project>-test
177
+ * - <project>-test2
178
+ * - <group>-<project>-test
179
+ * - deploy/<project>
180
+ */
181
+ function suggestJenkinsJobs({ group, project, env = 'test' }) {
182
+ if (!project) return [];
183
+ const proj = project.replace(/[^\w-]/g, '-');
184
+ const grp = (group || '').replace(/[^\w-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
185
+ const envName = String(env || 'test').replace(/[^\w-]/g, '-');
186
+ const out = [
187
+ `${proj}-${envName}`,
188
+ `${proj}-test`,
189
+ `${proj}-test2`,
190
+ `${proj}-deploy-${envName}`,
191
+ `deploy/${proj}`,
192
+ `deploy/${proj}-${envName}`,
193
+ `${envName}/${proj}`,
194
+ ];
195
+ if (grp) out.unshift(`${grp}-${proj}`, `${grp}-${proj}-${envName}`, `${grp}-${envName}-${proj}`);
196
+ return [...new Set(out)];
197
+ }
198
+
199
+ function rankJenkinsJobs(candidates, { group, project, env = 'test' } = {}) {
200
+ const expected = suggestJenkinsJobs({ group, project, env });
201
+ const scored = (candidates || []).map((job) => {
202
+ const name = String(job && (job.name || job.fullName || job.url || job) || '');
203
+ const normalized = name.toLowerCase();
204
+ let score = 0;
205
+ const exactIdx = expected.findIndex((x) => x.toLowerCase() === normalized);
206
+ if (exactIdx >= 0) score += 100 - exactIdx;
207
+ if (project && normalized.includes(String(project).toLowerCase())) score += 30;
208
+ if (group && normalized.includes(String(group).split('/').pop().toLowerCase())) score += 10;
209
+ if (env && normalized.includes(String(env).toLowerCase())) score += 20;
210
+ if (/deploy|发布|部署/.test(normalized)) score += 5;
211
+ if (/rollback|回滚/.test(normalized)) score -= 20;
212
+ return { job, name, score };
213
+ }).filter((x) => x.score > 0);
214
+ return scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).map((x) => x.job);
215
+ }
216
+
217
+ /**
218
+ * Full one-shot detect used by `devflow init`.
219
+ * Returns object with detection flags + never-throws.
220
+ */
221
+ async function detect(root) {
222
+ const remoteUrl = gitRemote(root);
223
+ const remote = parseRemote(remoteUrl);
224
+ const language = detectLanguage(root);
225
+ const buildTool = detectBuildTool(root, language);
226
+ const jenkinsJobs = remote ? suggestJenkinsJobs(remote) : [];
227
+ const defaultBranch = detectDefaultBranch(root);
228
+
229
+ return {
230
+ remoteUrl,
231
+ remote, // { host, group, project, fullPath } or null
232
+ language, // 'go' | 'node' | 'python' | ...
233
+ buildTool,
234
+ jenkinsJobs,
235
+ defaultBranch, // 'main' | 'master' | <custom>
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Render detection result to human-readable summary for CLI output.
241
+ */
242
+ function formatSummary(d) {
243
+ const lines = [];
244
+ if (d.remote) {
245
+ lines.push(` remote: ${d.remote.host}:${d.remote.fullPath}`);
246
+ } else if (d.remoteUrl) {
247
+ lines.push(` remote: ${d.remoteUrl} (parse failed)`);
248
+ } else {
249
+ lines.push(` remote: (not a git repo or no origin)`);
250
+ }
251
+ lines.push(` language: ${d.language}${d.buildTool ? ` (${d.buildTool})` : ''}`);
252
+ if (d.defaultBranch) {
253
+ lines.push(` branch: ${d.defaultBranch} (default)`);
254
+ }
255
+ if (d.jenkinsJobs.length) {
256
+ lines.push(` jenkins: ${d.jenkinsJobs.slice(0, 3).join(' / ')}`);
257
+ }
258
+ return lines.join('\n');
259
+ }
260
+
261
+ module.exports = {
262
+ gitRemote,
263
+ parseRemote,
264
+ detectLanguage,
265
+ detectBuildTool,
266
+ detectDefaultBranch,
267
+ suggestJenkinsJobs,
268
+ rankJenkinsJobs,
269
+ detect,
270
+ formatSummary,
271
+ };
@@ -0,0 +1,208 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const fsSync = require('fs');
4
+ const path = require('path');
5
+ const cp = require('child_process');
6
+ const paths = require('./paths.js');
7
+ const state = require('./state.js');
8
+
9
+ function slugify(input) {
10
+ if (!input) return 'untitled-' + Date.now().toString(36);
11
+ return String(input)
12
+ .toLowerCase()
13
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
14
+ .replace(/^-+|-+$/g, '')
15
+ .slice(0, 80);
16
+ }
17
+
18
+ function currentBranch(root) {
19
+ for (const args of [
20
+ ['branch', '--show-current'],
21
+ ['rev-parse', '--abbrev-ref', 'HEAD'],
22
+ ]) {
23
+ const r = cp.spawnSync('git', args, { cwd: root, encoding: 'utf8' });
24
+ if (r.status !== 0) continue;
25
+ const branch = (r.stdout || '').trim();
26
+ if (branch && branch !== 'HEAD') return branch;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ function withCurrentBranchSlug(root, slug) {
32
+ const safeSlug = slugify(slug);
33
+ const branch = currentBranch(root);
34
+ if (!branch) return safeSlug;
35
+ const branchSlug = slugify(branch);
36
+ if (!branchSlug) return safeSlug;
37
+ if (safeSlug === branchSlug || safeSlug.startsWith(`${branchSlug}-`)) return safeSlug;
38
+ return slugify(`${branchSlug}-${safeSlug}`);
39
+ }
40
+
41
+ async function exists(p) {
42
+ try { await fs.access(p); return true; } catch { return false; }
43
+ }
44
+
45
+ async function listChanges(root) {
46
+ const dirs = [paths.changesDir(root), legacyChangesDir(root)];
47
+ const names = new Set();
48
+ for (const dir of dirs) {
49
+ if (!fsSync.existsSync(dir)) continue;
50
+ const entries = await fs.readdir(dir, { withFileTypes: true });
51
+ for (const e of entries) {
52
+ if (e.isDirectory()) names.add(e.name);
53
+ }
54
+ }
55
+ return Array.from(names);
56
+ }
57
+
58
+ async function getCurrent(root) {
59
+ const all = await listChanges(root);
60
+ if (all.length === 0) {
61
+ // Fallback: look for the most recently archived change
62
+ return getLastArchived(root);
63
+ }
64
+ if (all.length === 1) return all[0];
65
+ // multi-change: pick most recently updated state.json
66
+ let best = null;
67
+ let bestTime = 0;
68
+ for (const slug of all) {
69
+ const file = resolveStateFile(root, slug);
70
+ if (!fsSync.existsSync(file)) continue;
71
+ const stat = await fs.stat(file);
72
+ if (stat.mtimeMs > bestTime) {
73
+ bestTime = stat.mtimeMs;
74
+ best = slug;
75
+ }
76
+ }
77
+ return best;
78
+ }
79
+
80
+ /**
81
+ * Return the slug of the most recently archived change (by dir mtime).
82
+ * Archive dirs are named `<YYYY-MM-DD>-<slug>`.
83
+ */
84
+ async function getLastArchived(root) {
85
+ const all = [];
86
+ for (const dir of [paths.archiveDir(root), legacyArchiveDir(root)]) {
87
+ if (!fsSync.existsSync(dir)) continue;
88
+ const entries = await fs.readdir(dir, { withFileTypes: true });
89
+ for (const e of entries) {
90
+ if (e.isDirectory()) all.push({ base: dir, name: e.name });
91
+ }
92
+ }
93
+ const dirs = all;
94
+ if (!dirs.length) return null;
95
+ let best = null;
96
+ let bestTime = 0;
97
+ for (const e of dirs) {
98
+ const abs = path.join(e.base, e.name);
99
+ try {
100
+ const stat = await fs.stat(abs);
101
+ if (stat.mtimeMs > bestTime) {
102
+ bestTime = stat.mtimeMs;
103
+ best = e.name;
104
+ }
105
+ } catch { /* ignore */ }
106
+ }
107
+ if (!best) return null;
108
+ // Strip leading date prefix: "2026-05-07-<slug>" → "<slug>"
109
+ const m = best.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
110
+ return m ? m[1] : best;
111
+ }
112
+
113
+ async function create(root, opts) {
114
+ const slug = opts.slug;
115
+ if (!slug) throw new Error('change.create requires slug');
116
+ const dir = paths.changeDir(root, slug);
117
+ if (await exists(dir)) {
118
+ throw new Error(`change already exists: ${slug}`);
119
+ }
120
+ await fs.mkdir(dir, { recursive: true });
121
+ const st = state.newState(opts);
122
+ await state.write(root, slug, st);
123
+ return { slug, dir, state: st };
124
+ }
125
+
126
+ async function ensureSubdir(root, slug, sub) {
127
+ const dir = await ensureWorkspaceChangeDir(root, slug);
128
+ const p = path.join(dir, sub);
129
+ await fs.mkdir(p, { recursive: true });
130
+ return p;
131
+ }
132
+
133
+ async function writeArtifact(root, slug, name, content) {
134
+ const dir = await ensureWorkspaceChangeDir(root, slug);
135
+ const file = path.join(dir, name);
136
+ await fs.mkdir(path.dirname(file), { recursive: true });
137
+ await fs.writeFile(file, content, 'utf8');
138
+ return file;
139
+ }
140
+
141
+ async function readArtifact(root, slug, name) {
142
+ const workspaceFile = path.join(paths.changeDir(root, slug), name);
143
+ if (fsSync.existsSync(workspaceFile)) return fs.readFile(workspaceFile, 'utf8');
144
+ const legacyFile = path.join(legacyChangeDir(root, slug), name);
145
+ return fs.readFile(legacyFile, 'utf8');
146
+ }
147
+
148
+ async function hasArtifact(root, slug, name) {
149
+ if (await exists(path.join(paths.changeDir(root, slug), name))) return true;
150
+ return exists(path.join(legacyChangeDir(root, slug), name));
151
+ }
152
+
153
+ function legacyChangesDir(root) {
154
+ return path.join(paths.devflowDir(root), 'changes');
155
+ }
156
+
157
+ function legacyArchiveDir(root) {
158
+ return path.join(paths.devflowDir(root), 'archive');
159
+ }
160
+
161
+ function legacyChangeDir(root, slug) {
162
+ return path.join(legacyChangesDir(root), slug);
163
+ }
164
+
165
+ async function ensureWorkspaceChangeDir(root, slug) {
166
+ const workspace = paths.changeDir(root, slug);
167
+ const legacy = legacyChangeDir(root, slug);
168
+ if (fsSync.existsSync(workspace)) return workspace;
169
+ if (fsSync.existsSync(legacy)) {
170
+ try {
171
+ await fs.mkdir(path.dirname(workspace), { recursive: true });
172
+ await fs.cp(legacy, workspace, { recursive: true });
173
+ return workspace;
174
+ } catch (e) {
175
+ if (e && (e.code === 'EACCES' || e.code === 'EPERM')) return legacy;
176
+ throw e;
177
+ }
178
+ }
179
+ await fs.mkdir(workspace, { recursive: true });
180
+ return workspace;
181
+ }
182
+
183
+ function resolveChangeDir(root, slug) {
184
+ const workspace = paths.changeDir(root, slug);
185
+ const legacy = legacyChangeDir(root, slug);
186
+ if (fsSync.existsSync(workspace)) return workspace;
187
+ if (fsSync.existsSync(legacy)) return legacy;
188
+ return workspace;
189
+ }
190
+
191
+ function resolveStateFile(root, slug) {
192
+ return path.join(resolveChangeDir(root, slug), 'state.json');
193
+ }
194
+
195
+ module.exports = {
196
+ slugify,
197
+ currentBranch,
198
+ withCurrentBranchSlug,
199
+ listChanges,
200
+ getCurrent,
201
+ create,
202
+ ensureSubdir,
203
+ writeArtifact,
204
+ readArtifact,
205
+ hasArtifact,
206
+ ensureWorkspaceChangeDir,
207
+ resolveChangeDir,
208
+ };
@@ -0,0 +1,217 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const VALID_STATUS = new Set(['pending', 'resolved', 'cancelled']);
6
+
7
+ function createCheckpoint(input = {}) {
8
+ const now = input.createdAt || new Date().toISOString();
9
+ const type = required(input.type, 'type');
10
+ const phase = input.phase || null;
11
+ const options = normalizeOptions(input.options || []);
12
+ return {
13
+ id: input.id || makeId(type, now),
14
+ type,
15
+ phase,
16
+ status: input.status || 'pending',
17
+ summary: input.summary || '',
18
+ question: input.question || '',
19
+ options,
20
+ defaultOption: input.defaultOption || (options[0] && options[0].id) || null,
21
+ nextAction: input.nextAction || (options[0] && options[0].command) || null,
22
+ risks: Array.isArray(input.risks) ? input.risks : [],
23
+ evidence: Array.isArray(input.evidence) ? input.evidence : [],
24
+ createdAt: now,
25
+ };
26
+ }
27
+
28
+ function addCheckpoint(state, checkpointInput) {
29
+ const checkpoint = createCheckpoint(checkpointInput);
30
+ state.checkpoints = Array.isArray(state.checkpoints) ? state.checkpoints : [];
31
+ state.checkpoints.push(checkpoint);
32
+ return checkpoint;
33
+ }
34
+
35
+ function resolveCheckpoint(state, id, decision, note) {
36
+ const checkpoint = findCheckpoint(state, id);
37
+ if (!checkpoint) throw new Error(`checkpoint not found: ${id}`);
38
+ checkpoint.status = 'resolved';
39
+ checkpoint.decision = decision || null;
40
+ checkpoint.note = note || null;
41
+ checkpoint.resolvedAt = new Date().toISOString();
42
+ return checkpoint;
43
+ }
44
+
45
+ function cancelCheckpoint(state, id, reason) {
46
+ const checkpoint = findCheckpoint(state, id);
47
+ if (!checkpoint) throw new Error(`checkpoint not found: ${id}`);
48
+ checkpoint.status = 'cancelled';
49
+ checkpoint.reason = reason || null;
50
+ checkpoint.resolvedAt = new Date().toISOString();
51
+ return checkpoint;
52
+ }
53
+
54
+ function latestPending(state) {
55
+ const checkpoints = Array.isArray(state && state.checkpoints) ? state.checkpoints : [];
56
+ for (let i = checkpoints.length - 1; i >= 0; i--) {
57
+ if (normalizeStatus(checkpoints[i].status) === 'pending') return checkpoints[i];
58
+ }
59
+ return null;
60
+ }
61
+
62
+ function latestPendingByType(state, type) {
63
+ const checkpoints = Array.isArray(state && state.checkpoints) ? state.checkpoints : [];
64
+ for (let i = checkpoints.length - 1; i >= 0; i--) {
65
+ const cp = checkpoints[i];
66
+ if (cp.type === type && normalizeStatus(cp.status) === 'pending') return cp;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ function latestPendingByTypes(state, types) {
72
+ const wanted = new Set(types || []);
73
+ const checkpoints = Array.isArray(state && state.checkpoints) ? state.checkpoints : [];
74
+ for (let i = checkpoints.length - 1; i >= 0; i--) {
75
+ const cp = checkpoints[i];
76
+ if (wanted.has(cp.type) && normalizeStatus(cp.status) === 'pending') return cp;
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function hasResolvedDecision(state, type, decision) {
82
+ const checkpoints = Array.isArray(state && state.checkpoints) ? state.checkpoints : [];
83
+ return checkpoints.some((cp) => {
84
+ if (cp.type !== type) return false;
85
+ if (normalizeStatus(cp.status) !== 'resolved') return false;
86
+ return decision ? cp.decision === decision : true;
87
+ });
88
+ }
89
+
90
+ function renderNextStepCard(checkpoint, result = 'NEEDS_INPUT') {
91
+ if (!checkpoint) return '';
92
+ const options = normalizeOptions(checkpoint.options || []);
93
+ const lines = [
94
+ '## 下一步',
95
+ `状态:${result}`,
96
+ `检查点:${checkpoint.type}`,
97
+ ];
98
+ if (checkpoint.phase) lines.push(`阶段:${checkpoint.phase}`);
99
+ if (checkpoint.summary) lines.push(`摘要:${checkpoint.summary}`);
100
+ if (checkpoint.question) lines.push(`需要你确认:${checkpoint.question}`);
101
+ if (options.length) {
102
+ lines.push('选项:');
103
+ for (const opt of options) {
104
+ const pieces = [`- ${opt.id}`];
105
+ if (opt.label && opt.label !== opt.id) pieces.push(opt.label);
106
+ if (opt.command) pieces.push(`命令:\`${opt.command}\``);
107
+ if (opt.description) pieces.push(opt.description);
108
+ lines.push(pieces.join(' — '));
109
+ }
110
+ }
111
+ const command = checkpoint.nextAction || (options[0] && options[0].command) || '<等待用户选择>';
112
+ lines.push(`命令:\`${command}\``);
113
+ if (checkpoint.risks && checkpoint.risks.length) lines.push(`风险:${checkpoint.risks.join(';')}`);
114
+ return lines.join('\n');
115
+ }
116
+
117
+ function buildConfirmationCard(checkpoint, options = {}) {
118
+ if (!checkpoint) return null;
119
+ const normalized = normalizeOptions(checkpoint.options || []);
120
+ const primaryId = options.primaryActionId || checkpoint.defaultOption || (normalized[0] && normalized[0].id);
121
+ const primary = normalized.find((opt) => opt.id === primaryId) || normalized[0] || null;
122
+ return {
123
+ type: options.type || `${checkpoint.type}_confirm`,
124
+ title: options.title || '确认下一步',
125
+ question: checkpoint.question || '',
126
+ checkpoint: summarizeCheckpoint(checkpoint),
127
+ risks: Array.isArray(checkpoint.risks) ? checkpoint.risks.slice() : [],
128
+ evidence: Array.isArray(checkpoint.evidence) ? checkpoint.evidence.slice() : [],
129
+ primaryAction: primary ? actionFromOption(primary) : null,
130
+ secondaryActions: normalized
131
+ .filter((opt) => !primary || opt.id !== primary.id)
132
+ .map(actionFromOption),
133
+ };
134
+ }
135
+
136
+ function summarizeCheckpoint(checkpoint) {
137
+ return {
138
+ id: checkpoint.id,
139
+ type: checkpoint.type,
140
+ phase: checkpoint.phase || null,
141
+ status: checkpoint.status || 'pending',
142
+ summary: checkpoint.summary || '',
143
+ };
144
+ }
145
+
146
+ function actionFromOption(option) {
147
+ return {
148
+ id: option.id,
149
+ label: option.label || option.id,
150
+ command: option.command || null,
151
+ description: option.description || '',
152
+ };
153
+ }
154
+
155
+ function parseOptions(value) {
156
+ if (!value) return [];
157
+ if (Array.isArray(value)) return normalizeOptions(value);
158
+ return String(value)
159
+ .split(';')
160
+ .map((entry) => entry.trim())
161
+ .filter(Boolean)
162
+ .map((entry) => {
163
+ const [left, ...rest] = entry.split('=');
164
+ const id = left.trim();
165
+ const command = rest.join('=').trim();
166
+ return { id, label: id, command };
167
+ });
168
+ }
169
+
170
+ function normalizeOptions(options) {
171
+ return options.map((opt) => {
172
+ if (typeof opt === 'string') return { id: opt, label: opt, command: null, description: '' };
173
+ const id = required(opt.id || opt.label, 'option.id');
174
+ return {
175
+ id,
176
+ label: opt.label || id,
177
+ command: opt.command || null,
178
+ description: opt.description || '',
179
+ };
180
+ });
181
+ }
182
+
183
+ function findCheckpoint(state, id) {
184
+ const checkpoints = Array.isArray(state && state.checkpoints) ? state.checkpoints : [];
185
+ return checkpoints.find((c) => c.id === id || c.name === id) || null;
186
+ }
187
+
188
+ function normalizeStatus(status) {
189
+ const s = status || 'pending';
190
+ return VALID_STATUS.has(s) ? s : s === 'passed' ? 'resolved' : s;
191
+ }
192
+
193
+ function makeId(type, ts) {
194
+ const nonce = crypto.randomBytes(4).toString('hex');
195
+ const hash = crypto.createHash('sha1').update(`${type}:${ts}:${nonce}`).digest('hex').slice(0, 8);
196
+ return `${type}-${hash}`;
197
+ }
198
+
199
+ function required(value, name) {
200
+ if (!value) throw new Error(`checkpoint ${name} is required`);
201
+ return String(value);
202
+ }
203
+
204
+ module.exports = {
205
+ createCheckpoint,
206
+ addCheckpoint,
207
+ resolveCheckpoint,
208
+ cancelCheckpoint,
209
+ latestPending,
210
+ latestPendingByType,
211
+ latestPendingByTypes,
212
+ hasResolvedDecision,
213
+ renderNextStepCard,
214
+ buildConfirmationCard,
215
+ summarizeCheckpoint,
216
+ parseOptions,
217
+ };