@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,143 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const fsSync = require('fs');
4
+ const path = require('path');
5
+ const log = require('../../utils/log.js');
6
+ const change = require('../../core/change.js');
7
+ const templates = require('../../templates/index.js');
8
+ const helpers = require('./_helpers.js');
9
+ const aggregate = require('../../reports/aggregate.js');
10
+
11
+ async function run({ sub, flags = {}, positional = [], cwd }) {
12
+ const root = cwd || process.cwd();
13
+ const kind = sub || positional[0];
14
+ if (kind !== 'self-test' && kind !== 'compact' && kind !== 'test-report') {
15
+ log.error('usage: devflow report self-test | compact');
16
+ log.dim('(other report kinds are produced via devflow test <kind>)');
17
+ process.exitCode = 2;
18
+ return;
19
+ }
20
+ const slug = await helpers.resolveSlug(root, flags, []);
21
+ if (!slug) { process.exitCode = 1; return; }
22
+ const st = await helpers.loadStateOrFail(root, slug);
23
+ if (!st) { process.exitCode = 1; return; }
24
+
25
+ if (kind === 'compact' || kind === 'test-report') {
26
+ return compactReports({ root, slug, st, flags });
27
+ }
28
+
29
+ const reportsDir = await change.ensureSubdir(root, slug, 'reports');
30
+ const tpl = await templates.load('reports/self-test.md');
31
+ const body = templates.render(tpl, {
32
+ slug,
33
+ title: st.title || slug,
34
+ status: 'draft',
35
+ acceptancePass: '-',
36
+ acceptanceTotal: '-',
37
+ unitPassRate: extractRate(root, slug, 'unit-test.md'),
38
+ integrationPassRate: extractRate(root, slug, 'integration-test.md'),
39
+ e2ePassRate: extractRate(root, slug, 'e2e-test.md'),
40
+ jointPassRate: extractRate(root, slug, 'joint-test.md'),
41
+ remotePassRate: extractRate(root, slug, 'remote-test.md'),
42
+ smokePassRate: extractRate(root, slug, 'smoke-test.md'),
43
+ date: new Date().toISOString(),
44
+ });
45
+ const file = path.join(reportsDir, 'self-test.md');
46
+ await aggregate.upsertReport({
47
+ reportsDir,
48
+ slug,
49
+ title: st.title || slug,
50
+ kind: 'self-test',
51
+ body,
52
+ meta: { status: 'draft' },
53
+ });
54
+ if (flags.splitReport === true || flags['split-report'] === true) {
55
+ await fs.writeFile(file, body, 'utf8');
56
+ }
57
+ log.ok(`reports/test-report.md#self-test`);
58
+ log.dim('edit the self-test section if needed, then devflow verify finalize');
59
+ }
60
+
61
+ async function compactReports({ root, slug, st, flags = {} }) {
62
+ const reportsDir = await change.ensureSubdir(root, slug, 'reports');
63
+ let entries = [];
64
+ try { entries = await fs.readdir(reportsDir); } catch (_) { entries = []; }
65
+ const files = entries
66
+ .filter((name) => name.endsWith('.md'))
67
+ .filter((name) => !['submit.md', 'test-report.md'].includes(name))
68
+ .sort();
69
+
70
+ let count = 0;
71
+ for (const name of files) {
72
+ const file = path.join(reportsDir, name);
73
+ const body = await fs.readFile(file, 'utf8');
74
+ const fm = readFrontmatter(body);
75
+ const kind = (fm.kind || kindFromReportFile(name)).trim();
76
+ await aggregate.upsertReport({
77
+ reportsDir,
78
+ slug,
79
+ title: st.title || slug,
80
+ kind,
81
+ body,
82
+ meta: {
83
+ status: fm.status || 'pending',
84
+ total: numericOrNull(fm.total),
85
+ passed: numericOrNull(fm.passed),
86
+ failed: numericOrNull(fm.failed),
87
+ coverage: fm.coverage || null,
88
+ failureType: fm.failureType || fm.failure_type || null,
89
+ },
90
+ });
91
+ count += 1;
92
+ if (flags.removeSplit === true || flags['remove-split'] === true) {
93
+ await fs.rm(file);
94
+ }
95
+ }
96
+
97
+ log.ok(`reports/test-report.md (${count} section${count === 1 ? '' : 's'} compacted)`);
98
+ if (flags.removeSplit === true || flags['remove-split'] === true) {
99
+ log.dim('legacy split report files removed after compaction.');
100
+ } else {
101
+ log.dim('legacy split files kept; pass --remove-split to remove them after compaction.');
102
+ }
103
+ }
104
+
105
+ function extractRate(root, slug, name) {
106
+ const f = path.join(change.resolveChangeDir(root, slug), 'reports', name);
107
+ if (!fsSync.existsSync(f)) {
108
+ const agg = aggregate.readAggregate(path.join(change.resolveChangeDir(root, slug), 'reports'));
109
+ const kind = name.replace(/-test\.md$/, '');
110
+ const found = agg.reports.find((r) => r.kind === kind);
111
+ return found ? found.status : '-';
112
+ }
113
+ const c = require('fs').readFileSync(f, 'utf8');
114
+ const m = c.match(/^status:\s*(\w+)/m);
115
+ return m ? m[1] : '-';
116
+ }
117
+
118
+ function readFrontmatter(text) {
119
+ const s = String(text || '');
120
+ if (!s.startsWith('---')) return {};
121
+ const end = s.indexOf('\n---', 3);
122
+ if (end < 0) return {};
123
+ const yaml = s.slice(3, end).trim();
124
+ const out = {};
125
+ for (const line of yaml.split('\n')) {
126
+ const m = /^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(line.trim());
127
+ if (m) out[m[1]] = m[2].trim();
128
+ }
129
+ return out;
130
+ }
131
+
132
+ function kindFromReportFile(name) {
133
+ if (name === 'self-test.md') return 'self-test';
134
+ return name.replace(/-test\.md$/, '').replace(/\.md$/, '');
135
+ }
136
+
137
+ function numericOrNull(value) {
138
+ if (value == null || value === '' || value === '-') return null;
139
+ const n = Number(value);
140
+ return Number.isFinite(n) ? n : null;
141
+ }
142
+
143
+ module.exports = { run, _internals: { compactReports, kindFromReportFile } };
@@ -0,0 +1,227 @@
1
+ 'use strict';
2
+ const path = require('path');
3
+ const log = require('../../utils/log.js');
4
+ const state = require('../../core/state.js');
5
+ const change = require('../../core/change.js');
6
+ const routing = require('../../core/project-routing.js');
7
+ const checkpoint = require('../../core/checkpoint.js');
8
+ const helpers = require('./_helpers.js');
9
+
10
+ async function run({ flags = {}, positional = [], cwd }) {
11
+ const root = cwd || process.cwd();
12
+ const slug = await helpers.resolveSlug(root, flags, positional);
13
+ if (!slug) { process.exitCode = 1; return; }
14
+ const st = await helpers.loadStateOrFail(root, slug);
15
+ if (!st) { process.exitCode = 1; return; }
16
+
17
+ if (st.mode === 'micro' || st.level === 'L0') {
18
+ log.error('requirement skipped: this change is L0 micro mode. Upgrade to L1 before running requirement-analysis.');
19
+ log.dim(`run: devflow status set-level L1 --slug=${slug} --reason="micro scope expanded"`);
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+ if (helpers.blockWorkflowStep(root, st, slug, 'requirement', 'requirement')) return;
24
+
25
+ if (await blockPendingCheckpoint(root, slug, st, flags, ['problem_card'], 'requirement')) return;
26
+
27
+ await helpers.writePhaseArtifact(root, slug, st, 'requirement.md', 'requirement.md', {});
28
+ const projectRouting = await runProjectRouting(root, slug, flags, st);
29
+ if (projectRouting) {
30
+ st.projectRouting = projectRouting;
31
+ st.projects = projectRouting.recommended;
32
+ await writeRoutingSummary(root, slug, projectRouting);
33
+ await routing.writeProjectRootsArtifact(root, slug, projectRouting, { source: 'requirement' });
34
+ ensureProjectConfirmationCheckpoint(st, projectRouting);
35
+ }
36
+ state.setPhase(st, 'requirement', 'in_progress');
37
+ helpers.completeWorkflowStep(st, 'requirement');
38
+ state.logEvent(st, 'requirement scaffold', projectRouting ? { projectRouting: projectRouting.recommended.length } : undefined);
39
+ await state.write(root, slug, st);
40
+
41
+ if (projectRouting && projectRouting.candidates.length) {
42
+ log.raw('');
43
+ log.info('project routing recommendations:');
44
+ for (const [i, p] of projectRouting.candidates.slice(0, 5).entries()) {
45
+ const mark = projectRouting.recommended.some((r) => r.root === p.root) ? '*' : ' ';
46
+ log.raw(` ${mark} ${i + 1}. ${p.name} confidence=${p.confidence} ${p.root}`);
47
+ for (const e of (p.evidence || []).slice(0, 3)) log.dim(` - ${e}`);
48
+ }
49
+ log.dim('confirm selected projects in requirement.md / state.json before Round 2 code reconnaissance.');
50
+ }
51
+
52
+ const pendingProject = checkpoint.latestPendingByType(st, 'project_confirmation');
53
+ if (pendingProject) {
54
+ log.raw('');
55
+ log.raw(checkpoint.renderNextStepCard(pendingProject));
56
+ } else {
57
+ log.dim('next: drive double-round clarification in your IDE (use the devflow-requirement-analysis SKILL)');
58
+ log.dim('then run: devflow design');
59
+ }
60
+ }
61
+
62
+ async function blockPendingCheckpoint(root, slug, st, flags, types, commandName) {
63
+ const pending = checkpoint.latestPendingByTypes(st, types);
64
+ if (!pending) return false;
65
+ if (!flags.force) {
66
+ log.error(`${commandName} blocked: pending ${pending.type} checkpoint.`);
67
+ log.raw('');
68
+ log.raw(checkpoint.renderNextStepCard(pending));
69
+ log.dim(`override: devflow ${commandName} --force --reason="..."`);
70
+ process.exitCode = 1;
71
+ return true;
72
+ }
73
+ if (!flags.reason) {
74
+ log.error(`${commandName} --force requires --reason`);
75
+ process.exitCode = 2;
76
+ return true;
77
+ }
78
+ state.logEvent(st, `${commandName}.force_bypass_checkpoint`, {
79
+ checkpoint: pending.id,
80
+ type: pending.type,
81
+ reason: flags.reason,
82
+ });
83
+ await state.write(root, slug, st);
84
+ log.warn(`${commandName}: force-bypassed pending ${pending.type} checkpoint`);
85
+ return false;
86
+ }
87
+
88
+ function ensureProjectConfirmationCheckpoint(st, projectRouting) {
89
+ if (!projectRouting || !projectRouting.candidates || !projectRouting.candidates.length) return null;
90
+ if (projectRouting.status === 'explicit' || projectRouting.status === 'selected') return null;
91
+ if (checkpoint.latestPendingByType(st, 'project_confirmation')) return null;
92
+ if (checkpoint.hasResolvedDecision(st, 'project_confirmation', 'accept')) return null;
93
+
94
+ const recommended = projectRouting.recommended || [];
95
+ const candidateSummary = recommended.length
96
+ ? recommended.map((p) => `${p.name}:${p.role || 'candidate'}`).join(', ')
97
+ : '无推荐项目';
98
+ const cp = checkpoint.addCheckpoint(st, {
99
+ type: 'project_confirmation',
100
+ phase: 'requirement',
101
+ summary: `推荐项目:${candidateSummary}`,
102
+ question: '是否接受当前涉及项目范围,并允许进入 design?',
103
+ options: [
104
+ { id: 'accept', label: '接受项目范围', command: 'devflow checkpoint resolve --id=<checkpoint-id> --decision=accept' },
105
+ { id: 'revise', label: '改选项目', command: 'devflow requirement --project-root=<primary>[,<dependency>]' },
106
+ ],
107
+ nextAction: 'devflow checkpoint resolve --id=<checkpoint-id> --decision=accept',
108
+ evidence: ['requirement.md', 'state.json#projectRouting'],
109
+ risks: ['项目范围未确认时进入 design,容易侦察或修改错误仓库。'],
110
+ });
111
+ cp.options = cp.options.map((opt) => opt.id === 'accept'
112
+ ? { ...opt, command: `devflow checkpoint resolve --id=${cp.id} --decision=accept` }
113
+ : opt);
114
+ cp.nextAction = `devflow checkpoint resolve --id=${cp.id} --decision=accept`;
115
+ state.logEvent(st, 'checkpoint.add', { id: cp.id, type: cp.type, phase: cp.phase });
116
+ return cp;
117
+ }
118
+
119
+ async function runProjectRouting(root, slug, flags, st) {
120
+ if (flags['no-project-routing'] || flags.noProjectRouting) return null;
121
+ if (!hasRoutingFlags(flags) && st && Array.isArray(st.projects) && st.projects.length) {
122
+ return st.projectRouting || {
123
+ status: 'preselected',
124
+ searchRoots: st.projects.map((p) => p.root),
125
+ queryTerms: [],
126
+ candidates: st.projects,
127
+ recommended: st.projects,
128
+ };
129
+ }
130
+ const explicitRoots = parseSearchRoots(flags.projectRoot || flags['project-root']);
131
+ if (explicitRoots.length) {
132
+ return routing.explicitProjectRouting(explicitRoots, { source: 'requirement' });
133
+ }
134
+ const searchRoots = parseSearchRoots(flags.searchRoot || flags['search-root'] || flags.projectsRoot || root);
135
+ const query = await collectRoutingQuery(root, slug);
136
+ const result = await routing.routeProjects({
137
+ searchRoots,
138
+ query,
139
+ maxDepth: parseInt(flags.projectDepth || flags['project-depth'] || '3', 10),
140
+ topK: parseInt(flags.projectTopK || flags['project-top-k'] || '5', 10),
141
+ });
142
+ return {
143
+ status: result.recommended.length ? 'recommended' : 'none',
144
+ searchRoots: result.searchRoots,
145
+ queryTerms: routing.extractTerms(query).slice(0, 20),
146
+ candidates: result.candidates,
147
+ recommended: result.recommended,
148
+ };
149
+ }
150
+
151
+ function hasRoutingFlags(flags) {
152
+ return Boolean(
153
+ flags.projectRoot || flags['project-root'] ||
154
+ flags.searchRoot || flags['search-root'] ||
155
+ flags.projectsRoot || flags.projectDepth || flags['project-depth'] ||
156
+ flags.projectTopK || flags['project-top-k']
157
+ );
158
+ }
159
+
160
+ async function collectRoutingQuery(root, slug) {
161
+ const parts = [];
162
+ for (const name of ['proposal.md', 'requirement.md', 'knowledge.md']) {
163
+ try { parts.push(await change.readArtifact(root, slug, name)); } catch { /* optional */ }
164
+ }
165
+ const cdir = change.resolveChangeDir(root, slug);
166
+ await collectRefs(path.join(cdir, 'refs'), parts);
167
+ return parts.join('\n');
168
+ }
169
+
170
+ async function collectRefs(dir, parts) {
171
+ const fs = require('fs/promises');
172
+ const fsSync = require('fs');
173
+ if (!fsSync.existsSync(dir)) return;
174
+ const entries = await fs.readdir(dir, { withFileTypes: true });
175
+ for (const e of entries) {
176
+ const full = path.join(dir, e.name);
177
+ if (e.isDirectory()) {
178
+ await collectRefs(full, parts);
179
+ } else if (e.isFile() && e.name.endsWith('.md')) {
180
+ try { parts.push(await fs.readFile(full, 'utf8')); } catch { /* ignore unreadable refs */ }
181
+ }
182
+ }
183
+ }
184
+
185
+ function parseSearchRoots(value) {
186
+ return String(value || '')
187
+ .split(',')
188
+ .map((s) => s.trim())
189
+ .filter(Boolean);
190
+ }
191
+
192
+ async function writeRoutingSummary(root, slug, projectRouting) {
193
+ if (!projectRouting || !projectRouting.candidates.length) return;
194
+ let md;
195
+ try { md = await change.readArtifact(root, slug, 'requirement.md'); } catch { return; }
196
+ if (md.includes('## 7. 项目路由确认')) return;
197
+
198
+ const lines = [
199
+ '',
200
+ '## 7. 项目路由确认',
201
+ '',
202
+ '> Round 2 代码侦察前必须确认本次涉及哪些项目;后续 design/apply/review/verify 只围绕已确认项目展开。',
203
+ '',
204
+ '| 选择 | 项目 | 角色 | 置信度 | 路径 |',
205
+ '|------|------|------|--------|------|',
206
+ ];
207
+ if (projectRouting.status === 'explicit') {
208
+ lines.push('状态:用户已指定项目,本节不再执行自动发现评分。', '');
209
+ }
210
+ const selected = new Set(projectRouting.recommended.map((p) => p.root));
211
+ for (const p of projectRouting.candidates.slice(0, 8)) {
212
+ const picked = selected.has(p.root) ? '✓' : '';
213
+ const role = projectRouting.recommended.find((r) => r.root === p.root)?.role || '';
214
+ lines.push(`| ${picked} | ${p.name} | ${role} | ${p.confidence} | \`${p.root}\` |`);
215
+ }
216
+ lines.push('', '证据:');
217
+ for (const p of projectRouting.candidates.slice(0, 5)) {
218
+ lines.push(`- ${p.name}: ${(p.evidence || []).slice(0, 3).join('; ') || '无明显命中'}`);
219
+ }
220
+ lines.push('', '> 如推荐不准确,请手动修改本节和 `state.json#projects` 后再进入 Round 2。', '');
221
+
222
+ const updated = md.replace(/## 7\. 第二轮澄清\(实现\)/, `${lines.join('\n')}## 8. 第二轮澄清(实现)`)
223
+ .replace(/## 8\. 风险与待决项/, '## 9. 风险与待决项');
224
+ await change.writeArtifact(root, slug, 'requirement.md', updated);
225
+ }
226
+
227
+ module.exports = { run };
@@ -0,0 +1,301 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const fsSync = require('fs');
4
+ const log = require('../../utils/log.js');
5
+ const state = require('../../core/state.js');
6
+ const change = require('../../core/change.js');
7
+ const checkpoint = require('../../core/checkpoint.js');
8
+ const helpers = require('./_helpers.js');
9
+
10
+ const MAX_ROUNDS = 3;
11
+
12
+ /**
13
+ * devflow review [--round] [--must=N --should=M --nit=K] [--reviewer=...]
14
+ * [--status]
15
+ * [--force-pass --reason="..."]
16
+ * [--continue-rounds] # only after MAX_ROUNDS, with audit
17
+ *
18
+ * Default invocation == --status (just show current).
19
+ *
20
+ * --round record a new review round; will read review.md to count
21
+ * MUST/SHOULD/NIT mentions if not given via flags
22
+ * --must / --should / --nit
23
+ * explicit counts (override review.md scan)
24
+ * --reviewer annotation only
25
+ *
26
+ * Outcomes per round:
27
+ * must > 0 → outcome=back_to_apply, phase stays in 'review' but apply is reopened
28
+ * must == 0 → outcome=pass, phase 'review' marked completed
29
+ *
30
+ * Hard rules:
31
+ * - At round = MAX_ROUNDS with must>0: outcome=blocked, exit 1
32
+ * (--force-pass --reason="..." can override; logged in audit)
33
+ * - At round > MAX_ROUNDS: refuse unless --continue-rounds (logged)
34
+ */
35
+
36
+ async function run({ flags = {}, positional = [], cwd }) {
37
+ const root = cwd || process.cwd();
38
+ const slug = await helpers.resolveSlug(root, flags, positional);
39
+ if (!slug) { process.exitCode = 1; return; }
40
+ const st = await helpers.loadStateOrFail(root, slug);
41
+ if (!st) { process.exitCode = 1; return; }
42
+
43
+ ensureReviewBucket(st);
44
+
45
+ if (flags.status === true || (!flags.round && !flags['force-pass'])) {
46
+ return showStatus(st, slug);
47
+ }
48
+ if (helpers.blockWorkflowStep(root, st, slug, 'review', 'review')) return;
49
+
50
+ // Force pass path
51
+ if (flags['force-pass'] === true || flags.forcePass === true) {
52
+ return forcePass(root, slug, st, flags);
53
+ }
54
+
55
+ // New round path
56
+ await helpers.writePhaseArtifact(root, slug, st, 'review.md', 'review.md', {});
57
+ return appendRound(root, slug, st, flags);
58
+ }
59
+
60
+ function ensureReviewBucket(st) {
61
+ st.phases.review = st.phases.review || { status: 'pending' };
62
+ st.phases.review.rounds = st.phases.review.rounds || [];
63
+ st.phases.review.max_rounds = st.phases.review.max_rounds || MAX_ROUNDS;
64
+ st.iterations = st.iterations || {};
65
+ st.iterations.review = st.iterations.review || st.phases.review.rounds.length;
66
+ }
67
+
68
+ function showStatus(st, slug) {
69
+ const r = st.phases.review;
70
+ log.raw(`review phase: ${r.status} (iterations=${st.iterations.review || 0} / max=${r.max_rounds})`);
71
+ if (!r.rounds || !r.rounds.length) { log.dim(`no rounds recorded yet. run "devflow review --slug=${slug} --round"`); return; }
72
+ for (const round of r.rounds) {
73
+ const outcome = (round.outcome || '?').padEnd(14);
74
+ log.raw(` round ${round.round} ${outcome} must=${round.must||0} should=${round.should||0} nit=${round.nit||0} ${round.reviewer ? '@'+round.reviewer : ''} ${round.ts.slice(0,19)}`);
75
+ }
76
+ if (r.status === 'completed') {
77
+ const verifyStart = checkpoint.latestPendingByType(st, 'verify_start');
78
+ if (verifyStart) log.ok(`review passed → confirm before verify: ${verifyStart.nextAction || `devflow checkpoint resolve --id=${verifyStart.id} --decision=start-verify`}`);
79
+ else log.ok(`review passed → safe to devflow verify --slug=${slug}`);
80
+ } else if (r.status === 'blocked') {
81
+ log.error(`review BLOCKED at round ${r.rounds.length} — must>0 after MAX_ROUNDS. either fix and "devflow review --slug=${slug} --round --continue-rounds" or "devflow review --slug=${slug} --force-pass --reason=..."`);
82
+ }
83
+ }
84
+
85
+ async function appendRound(root, slug, st, flags) {
86
+ const r = st.phases.review;
87
+ const nextRound = (r.rounds.length || 0) + 1;
88
+
89
+ // Check round caps
90
+ if (nextRound > r.max_rounds) {
91
+ if (!(flags['continue-rounds'] === true || flags.continueRounds === true)) {
92
+ log.error(`review at round ${r.rounds.length}, max=${r.max_rounds} already. add --continue-rounds (audited) to continue.`);
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+ state.logEvent(st, 'review.continue_beyond_max', { round: nextRound });
97
+ }
98
+
99
+ // Compute counts
100
+ let counts;
101
+ if (flags.must !== undefined || flags.should !== undefined || flags.nit !== undefined) {
102
+ counts = {
103
+ must: toInt(flags.must, 0),
104
+ should: toInt(flags.should, 0),
105
+ nit: toInt(flags.nit, 0),
106
+ source: 'flags',
107
+ };
108
+ } else {
109
+ counts = await scanReviewMd(root, slug);
110
+ }
111
+
112
+ const outcome = counts.must > 0
113
+ ? (nextRound >= r.max_rounds ? 'blocked' : 'back_to_apply')
114
+ : 'pass';
115
+
116
+ const round = {
117
+ round: nextRound,
118
+ ts: new Date().toISOString(),
119
+ must: counts.must, should: counts.should, nit: counts.nit,
120
+ source: counts.source,
121
+ reviewer: flags.reviewer || null,
122
+ outcome,
123
+ };
124
+ r.rounds.push(round);
125
+ st.iterations.review = nextRound;
126
+
127
+ switch (outcome) {
128
+ case 'pass':
129
+ state.setPhase(st, 'apply', 'completed');
130
+ state.setPhase(st, 'review', 'completed');
131
+ r.status = 'completed';
132
+ helpers.completeWorkflowStep(st, 'review');
133
+ cancelPendingReviewCheckpoints(st, 'review passed');
134
+ ensureVerifyStartCheckpoint(st, slug, round);
135
+ log.ok(`round ${nextRound}: PASS (must=0) → verify requires confirmation`);
136
+ break;
137
+ case 'back_to_apply':
138
+ state.setPhase(st, 'review', 'in_progress');
139
+ state.setPhase(st, 'apply', 'in_progress');
140
+ r.status = 'in_progress';
141
+ ensureReviewFixCheckpoint(st, slug, round);
142
+ log.warn(`round ${nextRound}: must=${counts.must} → back to apply (round ${nextRound+1}/${r.max_rounds} remaining)`);
143
+ log.dim(`fix MUSTs, then run "devflow apply --slug=${slug} --task=<id> --note=..." and "devflow review --slug=${slug} --round"`);
144
+ break;
145
+ case 'blocked':
146
+ state.setPhase(st, 'review', 'in_progress');
147
+ r.status = 'blocked';
148
+ ensureReviewBlockedCheckpoint(st, slug, round);
149
+ log.error(`round ${nextRound}: BLOCKED — must=${counts.must} after MAX_ROUNDS=${r.max_rounds}`);
150
+ log.dim(`options: fix and "devflow review --slug=${slug} --round --continue-rounds" OR "devflow review --slug=${slug} --force-pass --reason=..."`);
151
+ process.exitCode = 1;
152
+ break;
153
+ }
154
+
155
+ state.logEvent(st, 'review.round', round);
156
+ await state.write(root, slug, st);
157
+ const pending = outcome === 'back_to_apply'
158
+ ? checkpoint.latestPendingByType(st, 'review_fix')
159
+ : outcome === 'blocked'
160
+ ? checkpoint.latestPendingByType(st, 'review_blocked')
161
+ : null;
162
+ if (pending) {
163
+ log.raw('');
164
+ log.raw(checkpoint.renderNextStepCard(pending));
165
+ } else if (outcome === 'pass') {
166
+ const verifyStart = checkpoint.latestPendingByType(st, 'verify_start');
167
+ if (verifyStart) {
168
+ log.raw('');
169
+ log.raw(checkpoint.renderNextStepCard(verifyStart));
170
+ }
171
+ }
172
+ }
173
+
174
+ async function forcePass(root, slug, st, flags) {
175
+ const reason = flags.reason || flags['reason'];
176
+ if (!reason || reason === true) {
177
+ log.error('--force-pass requires --reason="..." (audited)');
178
+ process.exitCode = 2;
179
+ return;
180
+ }
181
+ const r = st.phases.review;
182
+ const last = r.rounds[r.rounds.length - 1] || {};
183
+ const round = {
184
+ round: (last.round || 0) + 1,
185
+ ts: new Date().toISOString(),
186
+ must: 0, should: last.should || 0, nit: last.nit || 0,
187
+ source: 'force-pass',
188
+ reviewer: flags.reviewer || null,
189
+ outcome: 'pass',
190
+ reason,
191
+ };
192
+ r.rounds.push(round);
193
+ state.setPhase(st, 'apply', 'completed');
194
+ state.setPhase(st, 'review', 'completed');
195
+ r.status = 'completed';
196
+ helpers.completeWorkflowStep(st, 'review');
197
+ st.iterations.review = round.round;
198
+ state.logEvent(st, 'review.force_pass', round);
199
+ cancelPendingReviewCheckpoints(st, reason);
200
+ ensureVerifyStartCheckpoint(st, slug, round);
201
+ await state.write(root, slug, st);
202
+ log.warn(`force-pass recorded at round ${round.round} — reason: "${reason}"`);
203
+ log.ok(`review marked completed. confirm before devflow verify --slug=${slug}.`);
204
+ }
205
+
206
+ function ensureReviewFixCheckpoint(st, slug, round) {
207
+ if (checkpoint.latestPendingByType(st, 'review_fix')) return null;
208
+ const cp = checkpoint.addCheckpoint(st, {
209
+ type: 'review_fix',
210
+ phase: 'review',
211
+ summary: `Round ${round.round} 发现 MUST=${round.must},需要回到 apply 修复。`,
212
+ question: '是否进入 apply 修复本轮 MUST 问题?',
213
+ options: [
214
+ { id: 'fix', label: '进入修复', command: `devflow apply --slug=${slug} --note="fix review MUSTs"` },
215
+ { id: 're-review', label: '重新审查', command: `devflow review --slug=${slug} --round` },
216
+ ],
217
+ nextAction: `devflow apply --slug=${slug} --note="fix review MUSTs"`,
218
+ evidence: ['review.md', `state.json#phases.review.rounds[${round.round - 1}]`],
219
+ risks: ['未修复 MUST 就进入 verify,会把明确缺陷带到交付前。'],
220
+ });
221
+ state.logEvent(st, 'checkpoint.add', { id: cp.id, type: cp.type, phase: cp.phase });
222
+ return cp;
223
+ }
224
+
225
+ function ensureReviewBlockedCheckpoint(st, slug, round) {
226
+ if (checkpoint.latestPendingByType(st, 'review_blocked')) return null;
227
+ const cp = checkpoint.addCheckpoint(st, {
228
+ type: 'review_blocked',
229
+ phase: 'review',
230
+ summary: `Round ${round.round} 后仍有 MUST=${round.must},review 已达到最大轮次。`,
231
+ question: '继续返工审查,还是带原因强制通过?',
232
+ options: [
233
+ { id: 'continue', label: '继续返工审查', command: `devflow review --slug=${slug} --round --continue-rounds` },
234
+ { id: 'force-pass', label: '强制通过', command: `devflow review --slug=${slug} --force-pass --reason="..."` },
235
+ ],
236
+ nextAction: `devflow review --slug=${slug} --round --continue-rounds`,
237
+ evidence: ['review.md', `state.json#phases.review.rounds[${round.round - 1}]`],
238
+ risks: ['review 达到最大轮次仍有 MUST,必须明确选择继续修复或审计接受风险。'],
239
+ });
240
+ state.logEvent(st, 'checkpoint.add', { id: cp.id, type: cp.type, phase: cp.phase });
241
+ return cp;
242
+ }
243
+
244
+ function ensureVerifyStartCheckpoint(st, slug, round) {
245
+ if (checkpoint.latestPendingByType(st, 'verify_start')) return null;
246
+ const cp = checkpoint.addCheckpoint(st, {
247
+ type: 'verify_start',
248
+ phase: 'review',
249
+ summary: `Review round ${round.round} 已通过,verify 需要显式确认后开始。`,
250
+ question: '是否允许进入 verify 阶段?确认后再单独运行 devflow verify。',
251
+ options: [
252
+ { id: 'start-verify', label: '允许进入 verify', command: 'devflow checkpoint resolve --id=<checkpoint-id> --decision=start-verify' },
253
+ { id: 'hold', label: '暂不验证', command: `devflow status --slug=${slug}` },
254
+ ],
255
+ nextAction: 'devflow checkpoint resolve --id=<checkpoint-id> --decision=start-verify',
256
+ evidence: ['review.md', `state.json#phases.review.rounds[${round.round - 1}]`],
257
+ risks: ['跳过显式确认直接 verify,容易在多 change 工作区里跑错对象或缺少验证范围确认。'],
258
+ });
259
+ const startCommand = `devflow checkpoint resolve --id=${cp.id} --decision=start-verify`;
260
+ cp.options[0].command = startCommand;
261
+ cp.nextAction = startCommand;
262
+ state.logEvent(st, 'checkpoint.add', { id: cp.id, type: cp.type, phase: cp.phase });
263
+ return cp;
264
+ }
265
+
266
+ function cancelPendingReviewCheckpoints(st, reason) {
267
+ const checkpoints = Array.isArray(st.checkpoints) ? st.checkpoints : [];
268
+ for (const cp of checkpoints) {
269
+ if ((cp.type === 'review_fix' || cp.type === 'review_blocked') && cp.status === 'pending') {
270
+ cp.status = 'resolved';
271
+ cp.decision = 'force-pass';
272
+ cp.note = reason;
273
+ cp.resolvedAt = new Date().toISOString();
274
+ state.logEvent(st, 'checkpoint.resolve', { id: cp.id, decision: cp.decision });
275
+ }
276
+ }
277
+ }
278
+
279
+ async function scanReviewMd(root, slug) {
280
+ const file = require('path').join(change.resolveChangeDir(root, slug), 'review.md');
281
+ if (!fsSync.existsSync(file)) {
282
+ return { must: 0, should: 0, nit: 0, source: 'no-file' };
283
+ }
284
+ const text = await fs.readFile(file, 'utf8');
285
+ // Count occurrences of MUST / SHOULD / NIT, case-sensitive in plain words and table cells
286
+ const c = (re) => (text.match(re) || []).length;
287
+ return {
288
+ must: c(/\bMUST\b/g),
289
+ should: c(/\bSHOULD\b/g),
290
+ nit: c(/\bNIT\b/g),
291
+ source: 'review.md',
292
+ };
293
+ }
294
+
295
+ function toInt(v, d) {
296
+ if (v === undefined || v === null) return d;
297
+ const n = parseInt(String(v), 10);
298
+ return Number.isFinite(n) ? n : d;
299
+ }
300
+
301
+ module.exports = { run, MAX_ROUNDS };