@openprd/cli 0.1.10 → 0.1.18

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 (146) hide show
  1. package/.openprd/benchmarks/index.md +17 -15
  2. package/.openprd/benchmarks/sources.yaml +53 -1
  3. package/.openprd/changes/adaptive-project-framing/.openprd.yaml +2 -0
  4. package/.openprd/changes/adaptive-project-framing/design.md +47 -0
  5. package/.openprd/changes/adaptive-project-framing/proposal.md +35 -0
  6. package/.openprd/changes/adaptive-project-framing/specs/agent-requirements/spec.md +16 -0
  7. package/.openprd/changes/adaptive-project-framing/task-events.jsonl +20 -0
  8. package/.openprd/changes/adaptive-project-framing/tasks.md +340 -0
  9. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/.openprd.yaml +2 -0
  10. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/design.md +34 -0
  11. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/proposal.md +23 -0
  12. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/specs/agent-requirements/spec.md +16 -0
  13. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/task-events.jsonl +14 -0
  14. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/tasks.md +238 -0
  15. package/.openprd/changes/loop-isolated-worktree-commit/.openprd.yaml +2 -0
  16. package/.openprd/changes/loop-isolated-worktree-commit/design.md +53 -0
  17. package/.openprd/changes/loop-isolated-worktree-commit/proposal.md +38 -0
  18. package/.openprd/changes/loop-isolated-worktree-commit/specs/agent-requirements/spec.md +16 -0
  19. package/.openprd/changes/loop-isolated-worktree-commit/task-events.jsonl +22 -0
  20. package/.openprd/changes/loop-isolated-worktree-commit/tasks.md +357 -0
  21. package/.openprd/changes/openprd/.openprd.yaml +2 -0
  22. package/.openprd/changes/openprd/design.md +46 -0
  23. package/.openprd/changes/openprd/proposal.md +36 -0
  24. package/.openprd/changes/openprd/specs/agent-requirements/spec.md +16 -0
  25. package/.openprd/changes/openprd/task-events.jsonl +23 -0
  26. package/.openprd/changes/openprd/tasks.md +242 -0
  27. package/.openprd/changes/pm-human-in-the-loop/.openprd.yaml +2 -0
  28. package/.openprd/changes/pm-human-in-the-loop/design.md +38 -0
  29. package/.openprd/changes/pm-human-in-the-loop/proposal.md +29 -0
  30. package/.openprd/changes/pm-human-in-the-loop/specs/agent-requirements/spec.md +16 -0
  31. package/.openprd/changes/pm-human-in-the-loop/task-events.jsonl +18 -0
  32. package/.openprd/changes/pm-human-in-the-loop/tasks.md +306 -0
  33. package/.openprd/changes/test-strategy-router/.openprd.yaml +2 -0
  34. package/.openprd/changes/test-strategy-router/design.md +65 -0
  35. package/.openprd/changes/test-strategy-router/proposal.md +45 -0
  36. package/.openprd/changes/test-strategy-router/specs/agent-requirements/spec.md +16 -0
  37. package/.openprd/changes/test-strategy-router/task-events.jsonl +1 -0
  38. package/.openprd/changes/test-strategy-router/tasks.md +132 -0
  39. package/.openprd/changes/verify-boundaries-and-historical-refresh/.openprd.yaml +2 -0
  40. package/.openprd/changes/verify-boundaries-and-historical-refresh/design.md +28 -0
  41. package/.openprd/changes/verify-boundaries-and-historical-refresh/proposal.md +23 -0
  42. package/.openprd/changes/verify-boundaries-and-historical-refresh/specs/agent-requirements/spec.md +16 -0
  43. package/.openprd/changes/verify-boundaries-and-historical-refresh/task-events.jsonl +11 -0
  44. package/.openprd/changes/verify-boundaries-and-historical-refresh/tasks.md +66 -0
  45. package/.openprd/changes/vibe-coding-dev-check/.openprd.yaml +2 -0
  46. package/.openprd/changes/vibe-coding-dev-check/design.md +37 -0
  47. package/.openprd/changes/vibe-coding-dev-check/proposal.md +28 -0
  48. package/.openprd/changes/vibe-coding-dev-check/specs/agent-requirements/spec.md +16 -0
  49. package/.openprd/changes/vibe-coding-dev-check/task-events.jsonl +18 -0
  50. package/.openprd/changes/vibe-coding-dev-check/tasks.md +306 -0
  51. package/.openprd/design/README.md +11 -6
  52. package/.openprd/design/README_EN.md +4 -1
  53. package/.openprd/design/active/asset-spec.md +6 -0
  54. package/.openprd/design/active/direction-plan.md +5 -5
  55. package/.openprd/design/active/selected-direction.md +2 -0
  56. package/.openprd/design/anti-slop.md +26 -1
  57. package/.openprd/design/assets/surface-presets.md +6 -0
  58. package/.openprd/design/checklists/ui-quality-gate.md +7 -0
  59. package/.openprd/design/lenses/frontend-lenses.md +28 -0
  60. package/.openprd/design/recipes/content-experience.md +8 -1
  61. package/.openprd/design/recipes/operational-tool.md +7 -0
  62. package/.openprd/design/recipes/product-launch.md +8 -1
  63. package/.openprd/design/templates/README.md +6 -3
  64. package/.openprd/design/templates/README_EN.md +6 -3
  65. package/.openprd/design/templates/ops-dashboard.html +8 -1
  66. package/.openprd/design/templates/product-launch.html +2 -1
  67. package/.openprd/design/themes/theme-catalog.json +23 -5
  68. package/.openprd/engagements/active/prd.md +103 -100
  69. package/.openprd/schema/prd.schema.yaml +15 -0
  70. package/.openprd/standards/config.json +7 -1
  71. package/.openprd/templates/base/intake.md +13 -0
  72. package/.openprd/templates/base/prd.md +15 -0
  73. package/AGENTS.md +14 -4
  74. package/README.md +107 -14
  75. package/README_EN.md +131 -13
  76. package/package.json +1 -1
  77. package/scripts/dev-check-wrapup-copy.mjs +46 -21
  78. package/skills/openprd-benchmark-router/SKILL.md +2 -1
  79. package/skills/openprd-diagram-review/SKILL.md +27 -2
  80. package/skills/openprd-diagram-review/references/explanation-svg-patterns.md +131 -0
  81. package/skills/openprd-discovery-loop/SKILL.md +2 -2
  82. package/skills/openprd-frontend-design/SKILL.md +44 -20
  83. package/skills/openprd-frontend-design/references/design-asset-contract.md +6 -0
  84. package/skills/openprd-frontend-design/references/direction-engine.md +29 -4
  85. package/skills/openprd-harness/SKILL.md +22 -16
  86. package/skills/openprd-quality/SKILL.md +10 -8
  87. package/skills/openprd-requirement-intake/SKILL.md +5 -3
  88. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +2 -2
  89. package/skills/openprd-requirement-intake/references/startup-validation-lens.md +98 -13
  90. package/skills/openprd-router/SKILL.md +2 -1
  91. package/skills/openprd-shared/SKILL.md +27 -17
  92. package/skills/openprd-standards/SKILL.md +4 -2
  93. package/src/agent-canonical-content.js +808 -0
  94. package/src/agent-integration.js +259 -778
  95. package/src/brainstorm-artifacts.js +156 -8
  96. package/src/brainstorm-presentation.js +14 -4
  97. package/src/brainstorm.js +165 -2
  98. package/src/canvas-app.html.js +2910 -0
  99. package/src/canvas-i18n.js +274 -0
  100. package/src/canvas-workspace.js +2271 -0
  101. package/src/cli/args.js +72 -4
  102. package/src/cli/basic-print.js +3 -0
  103. package/src/cli/canvas-print.js +106 -0
  104. package/src/cli/doctor-print.js +20 -0
  105. package/src/cli/print.js +2 -0
  106. package/src/cli/quality-commands.js +222 -0
  107. package/src/cli/quality-print.js +37 -5
  108. package/src/cli/run-print.js +5 -0
  109. package/src/cli/workflow-print.js +3 -0
  110. package/src/codex-app-server-relay.js +251 -0
  111. package/src/codex-hook-runner-template.mjs +963 -65
  112. package/src/design-starter-support.js +2 -2
  113. package/src/design-starter.js +39 -3
  114. package/src/dev-standards.js +125 -10
  115. package/src/diagram-core.js +390 -171
  116. package/src/discovery.js +2 -8
  117. package/src/docs-compact.js +125 -0
  118. package/src/execution-strategy.js +1 -1
  119. package/src/fleet.js +9 -3
  120. package/src/guide.js +227 -0
  121. package/src/html-artifacts.js +825 -51
  122. package/src/knowledge.js +101 -20
  123. package/src/language-policy.js +63 -3
  124. package/src/learning-review.js +1 -1
  125. package/src/loop.js +1 -1
  126. package/src/openprd.js +68 -131
  127. package/src/openspec/change-validate.js +2 -2
  128. package/src/openspec/constants.js +6 -7
  129. package/src/openspec/execute.js +2 -2
  130. package/src/openspec/generate.js +318 -102
  131. package/src/openspec/migration.js +168 -0
  132. package/src/openspec/paths.js +1 -22
  133. package/src/prd-core.js +102 -3
  134. package/src/quality-html-artifact.js +30 -5
  135. package/src/quality-visual-review.js +257 -14
  136. package/src/quality.js +66 -18
  137. package/src/review-presentation.js +60 -0
  138. package/src/run-harness.js +41 -3
  139. package/src/session-evidence.js +363 -0
  140. package/src/standards.js +64 -0
  141. package/src/test-strategy.js +7 -4
  142. package/src/visual-compare-centering.js +622 -0
  143. package/src/visual-compare-core.js +1123 -0
  144. package/src/visual-compare.js +620 -698
  145. package/src/workspace-core.js +132 -4
  146. package/src/workspace-workflow.js +172 -10
package/src/discovery.js CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { analyzePrdSnapshot, buildPrdSnapshot, formatVersionId } from './prd-core.js';
4
4
  import { formatProductTypeDisplay } from './product-type-copy.js';
5
5
  import { analyzeOpenSpecTaskVolumes } from './openspec/tasks.js';
6
- import { legacyOpenSpecDiscoveryDir, openPrdDiscoveryDir, readDiscoveryConfig } from './openspec/paths.js';
6
+ import { openPrdDiscoveryDir, readDiscoveryConfig } from './openspec/paths.js';
7
7
  import { appendJsonl, cjoin, exists, readJson, readJsonl, writeJson, writeText } from './fs-utils.js';
8
8
  import { collectSourceInventory, shouldIgnoreSourceDirectory } from './source-inventory.js';
9
9
  import { compactTimestamp, timestamp } from './time.js';
@@ -391,13 +391,7 @@ async function openspecDiscoveryWorkspaceImpl(projectRoot, options = {}, depende
391
391
  }
392
392
 
393
393
  async function loadOpenSpecDiscoveryRun(projectRoot) {
394
- let discoveryRoot = openPrdDiscoveryDir(projectRoot);
395
- if (!(await exists(cjoin(discoveryRoot, 'current.json')))) {
396
- const legacyRoot = legacyOpenSpecDiscoveryDir(projectRoot);
397
- if (await exists(cjoin(legacyRoot, 'current.json'))) {
398
- discoveryRoot = legacyRoot;
399
- }
400
- }
394
+ const discoveryRoot = openPrdDiscoveryDir(projectRoot);
401
395
  const currentPath = cjoin(discoveryRoot, 'current.json');
402
396
  if (!(await exists(currentPath))) {
403
397
  throw new Error(`Missing OpenPrd discovery state: ${currentPath}`);
@@ -0,0 +1,125 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ DOC_MEGA_LINE_CHARS,
5
+ DOC_STALE_VERSION_THRESHOLD,
6
+ STANDARD_DOCS,
7
+ STANDARD_DOCS_ROOT,
8
+ analyzeDocQuality,
9
+ analyzeDocStaleness,
10
+ } from './standards.js';
11
+
12
+ const PREVIEW_CHARS = 80;
13
+ const MAX_LINE_SUGGESTIONS_PER_DOC = 8;
14
+
15
+ function lineSuggestion(lineNumber, line) {
16
+ const preview = Array.from(line.trim()).slice(0, PREVIEW_CHARS).join('');
17
+ const separators = (line.match(/[;;。]/gu) ?? []).length;
18
+ const hint = separators >= 2
19
+ ? `含 ${separators + 1} 个句段,建议拆成分点列表(每点一行)`
20
+ : '建议拆成小节或分点,删除重复叙述';
21
+ return {
22
+ line: lineNumber,
23
+ chars: Array.from(line).length,
24
+ preview: `${preview}…`,
25
+ hint,
26
+ };
27
+ }
28
+
29
+ export async function docsCompactWorkspace(projectRoot, options = {}) {
30
+ const resolvedRoot = path.resolve(projectRoot);
31
+ const docs = [];
32
+ let totalMegaLines = 0;
33
+
34
+ for (const doc of STANDARD_DOCS) {
35
+ const docPath = path.join(resolvedRoot, STANDARD_DOCS_ROOT, doc.fileName);
36
+ const text = await fs.readFile(docPath, 'utf8').catch(() => null);
37
+ if (text === null) {
38
+ continue;
39
+ }
40
+ const relativePath = path.relative(resolvedRoot, docPath);
41
+ const quality = analyzeDocQuality(relativePath, text);
42
+ const lines = text.split('\n');
43
+ const suggestions = [];
44
+ lines.forEach((line, index) => {
45
+ if (Array.from(line).length > DOC_MEGA_LINE_CHARS && suggestions.length < MAX_LINE_SUGGESTIONS_PER_DOC) {
46
+ suggestions.push(lineSuggestion(index + 1, line));
47
+ }
48
+ });
49
+ totalMegaLines += quality.megaLines;
50
+
51
+ const entry = {
52
+ ...quality,
53
+ suggestions,
54
+ status: quality.megaLines > 0 ? 'needs-compaction' : 'ok',
55
+ };
56
+
57
+ if (doc.fileName === 'prd.md') {
58
+ const stat = await fs.stat(docPath).catch(() => null);
59
+ if (stat) {
60
+ const staleness = await analyzeDocStaleness(resolvedRoot, stat.mtimeMs);
61
+ entry.versionsBehind = staleness.versionsBehind;
62
+ entry.totalVersions = staleness.totalVersions;
63
+ if (staleness.versionsBehind > DOC_STALE_VERSION_THRESHOLD) {
64
+ entry.status = entry.status === 'needs-compaction' ? 'needs-compaction-and-backfill' : 'needs-backfill';
65
+ }
66
+ }
67
+ }
68
+
69
+ docs.push(entry);
70
+ }
71
+
72
+ const needsAttention = docs.filter((doc) => doc.status !== 'ok');
73
+ const recommendations = [];
74
+ if (needsAttention.length > 0) {
75
+ recommendations.push('压缩原则:同一事实只保留一处最新版本;把巨行拆成分点;过时段落直接删除而不是追加“更新:”。');
76
+ recommendations.push('压缩后运行 openprd standards . --verify 确认警告消除,再运行 openprd dev-check . 收口。');
77
+ }
78
+ for (const doc of needsAttention) {
79
+ if (doc.status.includes('backfill')) {
80
+ recommendations.push(`${doc.path}: 已落后 ${doc.versionsBehind} 个 requirement 版本,需要按最新已确认需求回填基线,再删除已失效内容。`);
81
+ }
82
+ if (doc.megaLines > 0) {
83
+ recommendations.push(`${doc.path}: ${doc.megaLines} 行巨行(阈值 ${DOC_MEGA_LINE_CHARS} 字符),按 suggestions 列出的行号拆分。`);
84
+ }
85
+ }
86
+
87
+ return {
88
+ ok: true,
89
+ action: 'docs-compact',
90
+ projectRoot: resolvedRoot,
91
+ docsRoot: STANDARD_DOCS_ROOT,
92
+ analyzedDocs: docs.length,
93
+ docsNeedingAttention: needsAttention.length,
94
+ totalMegaLines,
95
+ docs,
96
+ recommendations,
97
+ };
98
+ }
99
+
100
+ export function printDocsCompactResult(result, json) {
101
+ if (json) {
102
+ console.log(JSON.stringify(result, null, 2));
103
+ return;
104
+ }
105
+ console.log(`docs/basic 压缩体检: ${result.docsNeedingAttention === 0 ? '健康' : `${result.docsNeedingAttention} 个文档需要处理`}`);
106
+ console.log(`- 已分析文档: ${result.analyzedDocs}`);
107
+ console.log(`- 巨行总数(> ${DOC_MEGA_LINE_CHARS} 字符): ${result.totalMegaLines}`);
108
+ for (const doc of result.docs) {
109
+ if (doc.status === 'ok') {
110
+ console.log(`- ${doc.path}: 健康(${doc.lines} 行,平均 ${doc.avgLineChars} 字符/行)`);
111
+ continue;
112
+ }
113
+ console.log(`- ${doc.path}: ${doc.status}(${doc.lines} 行,巨行 ${doc.megaLines},最长 ${doc.maxLineChars} 字符${doc.versionsBehind != null ? `,落后 ${doc.versionsBehind} 版本` : ''})`);
114
+ for (const suggestion of doc.suggestions) {
115
+ console.log(` · L${suggestion.line}(${suggestion.chars} 字符): ${suggestion.preview}`);
116
+ console.log(` ${suggestion.hint}`);
117
+ }
118
+ }
119
+ if (result.recommendations.length > 0) {
120
+ console.log('建议:');
121
+ for (const item of result.recommendations) {
122
+ console.log(`- ${item}`);
123
+ }
124
+ }
125
+ }
@@ -85,7 +85,7 @@ function inferParallelGroup(text, type, phase) {
85
85
 
86
86
  function inferWriteScope(parallelGroup) {
87
87
  const scopeByGroup = {
88
- governance: ['openprd/changes/**', '.openprd/**'],
88
+ governance: ['.openprd/changes/**', '.openprd/**'],
89
89
  contracts: ['src/**', 'test/**', 'docs/basic/backend-structure.md'],
90
90
  domain: ['src/**', 'test/**'],
91
91
  surface: ['src/cli/**', 'src/**', 'test/**'],
package/src/fleet.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { cjoin, exists, writeJson } from './fs-utils.js';
4
+ import { hasLegacyOpenPrdArtifacts } from './openspec/migration.js';
4
5
  import { SOURCE_INVENTORY_IGNORE_DIRS } from './source-inventory.js';
5
6
  import { timestamp } from './time.js';
6
7
  import { readWorkspaceRegistry, upsertWorkspaceRegistryEntry } from './workspace-registry.js';
@@ -33,6 +34,7 @@ const FLEET_PROJECT_MARKERS = [
33
34
  'Makefile',
34
35
  ];
35
36
  const FLEET_AGENT_MARKERS = ['.codex', '.claude', '.cursor', 'AGENTS.md', 'CLAUDE.md'];
37
+ const FLEET_LEGACY_OPENPRD_MARKER = 'openprd-legacy-artifacts';
36
38
 
37
39
  function normalizeCsvList(value) {
38
40
  if (!value) {
@@ -73,7 +75,7 @@ function pathWithinRoot(rootPath, candidatePath) {
73
75
  }
74
76
 
75
77
  function classifyFleetMarkers(markers) {
76
- if (markers.includes('.openprd')) {
78
+ if (markers.includes('.openprd') || markers.includes(FLEET_LEGACY_OPENPRD_MARKER)) {
77
79
  return 'openprd-workspace';
78
80
  }
79
81
  if (markers.some((marker) => FLEET_AGENT_MARKERS.includes(marker))) {
@@ -107,7 +109,11 @@ function plannedFleetAction(category, options) {
107
109
  async function detectFleetMarkers(projectPath) {
108
110
  const entries = await fs.readdir(projectPath, { withFileTypes: true }).catch(() => []);
109
111
  const names = new Set(entries.map((entry) => entry.name));
110
- return FLEET_PROJECT_MARKERS.filter((marker) => names.has(marker));
112
+ const markers = FLEET_PROJECT_MARKERS.filter((marker) => names.has(marker));
113
+ if (!markers.includes('.openprd') && await hasLegacyOpenPrdArtifacts(projectPath)) {
114
+ markers.push(FLEET_LEGACY_OPENPRD_MARKER);
115
+ }
116
+ return markers;
111
117
  }
112
118
 
113
119
  async function scanFleetProjects(rootPath, options = {}) {
@@ -199,7 +205,7 @@ async function collectRegistryFleetScope(rootPath, options, registry) {
199
205
  path: entry.workspaceRoot,
200
206
  relativePath: path.relative(root, entry.workspaceRoot) || '.',
201
207
  category: 'openprd-workspace',
202
- markers: ['.openprd'],
208
+ markers: ['.openprd', ...(await hasLegacyOpenPrdArtifacts(entry.workspaceRoot) ? [FLEET_LEGACY_OPENPRD_MARKER] : [])],
203
209
  discoverySource: 'registry',
204
210
  registryKnown: true,
205
211
  registryEntry: entry,
package/src/guide.js ADDED
@@ -0,0 +1,227 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const PLACEHOLDER_TEXT = /^待(补充|澄清|确认)/u;
5
+
6
+ export const GUIDE_STAGES = [
7
+ {
8
+ id: 'clarify',
9
+ title: '需求澄清',
10
+ description: '把要解决的问题、目标用户和这次的目标讲清楚。',
11
+ nextAction: '把想法直接告诉 Agent:要解决什么问题、给谁用、这次想达成什么。Agent 会用 openprd capture 记录下来。',
12
+ },
13
+ {
14
+ id: 'draft',
15
+ title: '需求摘要',
16
+ description: '把已确认的信息整理成一份可评审的 PRD 草稿。',
17
+ nextAction: '让 Agent 运行 openprd synthesize,生成 PRD 草稿和评审页面。',
18
+ },
19
+ {
20
+ id: 'review',
21
+ title: '评审确认',
22
+ description: '打开 review.html,逐块确认主流程、功能、护栏和风险。',
23
+ nextAction: '打开评审页面逐块看完,用底部按钮告诉 Agent「认可并继续」或「需要调整」。',
24
+ },
25
+ {
26
+ id: 'direction',
27
+ title: '交互方向选择',
28
+ description: '界面类需求会给出 2 到 4 个交互稿方向,选定一个再继续。',
29
+ nextAction: '在评审页面或画布里比较各方向,点「选这个方向」把选择发回 Agent。',
30
+ },
31
+ {
32
+ id: 'tasks',
33
+ title: '任务拆解',
34
+ description: '把已确认的需求拆成一份带验证要求的任务清单。',
35
+ nextAction: '让 Agent 运行 openprd change . --generate 生成变更提案和任务清单。',
36
+ },
37
+ {
38
+ id: 'build',
39
+ title: '实现执行',
40
+ description: 'Agent 按任务清单实现,每个任务完成后留下验证证据。',
41
+ nextAction: '让 Agent 按任务清单继续实现;你可以随时问「现在做到哪一步了」。',
42
+ },
43
+ {
44
+ id: 'wrapup',
45
+ title: '验证收口',
46
+ description: '运行质量门禁,确认全部任务完成且验证通过。',
47
+ nextAction: '让 Agent 运行 openprd quality . --verify 和 openprd run . --verify 收口。',
48
+ },
49
+ ];
50
+
51
+ async function readJson(filePath) {
52
+ const text = await fs.readFile(filePath, 'utf8').catch(() => null);
53
+ if (text === null) return null;
54
+ try {
55
+ return JSON.parse(text);
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ function taskProgressFromMarkdown(text) {
62
+ const done = (text.match(/^\s*-\s*\[x\]/gim) ?? []).length;
63
+ const open = (text.match(/^\s*-\s*\[ \]/gim) ?? []).length;
64
+ return { done, total: done + open };
65
+ }
66
+
67
+ async function latestQualityReadiness(projectRoot) {
68
+ const reportsDir = path.join(projectRoot, '.openprd', 'quality', 'reports');
69
+ const entries = await fs.readdir(reportsDir).catch(() => []);
70
+ const evalReports = entries.filter((name) => name.startsWith('eval-') && name.endsWith('.json')).sort();
71
+ if (evalReports.length === 0) return null;
72
+ const latest = await readJson(path.join(reportsDir, evalReports[evalReports.length - 1]));
73
+ return latest?.readiness ?? null;
74
+ }
75
+
76
+ export async function guideWorkspace(projectRoot) {
77
+ const resolvedRoot = path.resolve(projectRoot);
78
+ const stateDir = path.join(resolvedRoot, '.openprd', 'state');
79
+ const current = await readJson(path.join(stateDir, 'current.json'));
80
+ const changesState = await readJson(path.join(stateDir, 'changes.json'));
81
+
82
+ const problemStatement = String(current?.problemStatement ?? '').trim();
83
+ const clarified = problemStatement.length > 0 && !PLACEHOLDER_TEXT.test(problemStatement);
84
+
85
+ const prdVersion = Number(current?.prdVersion ?? 0);
86
+ const drafted = clarified && prdVersion >= 1;
87
+
88
+ const reviewStatus = current?.reviewStatus ?? null;
89
+ const reviewConfirmed = drafted && reviewStatus?.status === 'confirmed';
90
+
91
+ let designDirections = null;
92
+ const versionId = reviewStatus?.versionId ?? null;
93
+ if (versionId) {
94
+ const snapshot = await readJson(path.join(stateDir, 'versions', `${versionId}.json`));
95
+ const block = snapshot?.reviewPresentation?.designDirections;
96
+ if (block && Array.isArray(block.directions) && block.directions.length > 0) {
97
+ designDirections = block.directions;
98
+ }
99
+ }
100
+ const directionApplicable = designDirections !== null;
101
+ const directionChosen = Boolean(reviewStatus?.designDirection?.id);
102
+
103
+ const activeChange = changesState?.activeChange ?? null;
104
+ let taskProgress = null;
105
+ if (activeChange) {
106
+ const tasksText = await fs.readFile(
107
+ path.join(resolvedRoot, '.openprd', 'changes', activeChange, 'tasks.md'),
108
+ 'utf8',
109
+ ).catch(() => null);
110
+ if (tasksText !== null) {
111
+ taskProgress = taskProgressFromMarkdown(tasksText);
112
+ }
113
+ }
114
+ const tasksReady = reviewConfirmed && taskProgress !== null && taskProgress.total > 0;
115
+ const buildDone = tasksReady && taskProgress.done === taskProgress.total;
116
+
117
+ const readiness = await latestQualityReadiness(resolvedRoot);
118
+ const wrappedUp = buildDone && readiness?.productionReady === true;
119
+
120
+ const completion = {
121
+ clarify: clarified,
122
+ draft: drafted,
123
+ review: reviewConfirmed,
124
+ direction: directionApplicable ? directionChosen : null,
125
+ tasks: tasksReady,
126
+ build: buildDone,
127
+ wrapup: wrappedUp,
128
+ };
129
+
130
+ const stages = [];
131
+ let currentStage = null;
132
+ for (let index = 0; index < GUIDE_STAGES.length; index += 1) {
133
+ const spec = GUIDE_STAGES[index];
134
+ const completed = completion[spec.id];
135
+ let status;
136
+ if (spec.id === 'direction' && !directionApplicable) {
137
+ status = 'skipped';
138
+ } else if (completed) {
139
+ status = 'done';
140
+ } else if (currentStage === null) {
141
+ status = 'current';
142
+ currentStage = spec.id;
143
+ } else {
144
+ status = 'pending';
145
+ }
146
+
147
+ let detail = null;
148
+ if (spec.id === 'clarify' && clarified) {
149
+ detail = '问题定义已记录。';
150
+ } else if (spec.id === 'draft' && drafted) {
151
+ detail = `已生成第 ${prdVersion} 版需求摘要。`;
152
+ } else if (spec.id === 'review' && reviewStatus) {
153
+ detail = reviewConfirmed ? `评审版本 ${versionId} 已确认。` : `评审版本 ${versionId} 等待确认。`;
154
+ } else if (spec.id === 'direction') {
155
+ if (!directionApplicable) {
156
+ detail = '本次需求不涉及界面方向选择,自动跳过。';
157
+ } else if (directionChosen) {
158
+ detail = `已选定方向「${reviewStatus.designDirection.title ?? reviewStatus.designDirection.id}」。`;
159
+ } else {
160
+ detail = `${designDirections.length} 个交互方向等待选择。`;
161
+ }
162
+ } else if (spec.id === 'tasks' && activeChange && taskProgress) {
163
+ detail = `变更 ${activeChange} 已有 ${taskProgress.total} 个任务。`;
164
+ } else if (spec.id === 'build' && taskProgress) {
165
+ detail = `任务进度 ${taskProgress.done}/${taskProgress.total}。`;
166
+ } else if (spec.id === 'wrapup' && readiness && buildDone) {
167
+ detail = readiness.productionReady ? '质量门禁已通过。' : '质量门禁尚未全部通过。';
168
+ }
169
+
170
+ stages.push({
171
+ id: spec.id,
172
+ index: index + 1,
173
+ title: spec.title,
174
+ description: spec.description,
175
+ status,
176
+ detail,
177
+ });
178
+ }
179
+
180
+ if (currentStage === null) {
181
+ currentStage = 'wrapup';
182
+ }
183
+ const currentSpec = GUIDE_STAGES.find((spec) => spec.id === currentStage);
184
+ const currentEntry = stages.find((stage) => stage.id === currentStage);
185
+
186
+ return {
187
+ ok: true,
188
+ action: 'guide',
189
+ projectRoot: resolvedRoot,
190
+ totalStages: GUIDE_STAGES.length,
191
+ currentStage: {
192
+ id: currentStage,
193
+ index: currentEntry?.index ?? GUIDE_STAGES.length,
194
+ title: currentSpec?.title ?? '验证收口',
195
+ },
196
+ nextAction: currentEntry?.status === 'done' ? '全部阶段已完成,可以开始下一个需求。' : currentSpec?.nextAction ?? null,
197
+ reviewPath: reviewStatus?.entryPath ?? reviewStatus?.reviewPath ?? null,
198
+ stages,
199
+ };
200
+ }
201
+
202
+ export function printGuideResult(result, json) {
203
+ if (json) {
204
+ console.log(JSON.stringify(result, null, 2));
205
+ return;
206
+ }
207
+ console.log(`OpenPrd 流程指引: 第 ${result.currentStage.index}/${result.totalStages} 步 · ${result.currentStage.title}`);
208
+ for (const stage of result.stages) {
209
+ const marker = stage.status === 'done'
210
+ ? '[完成]'
211
+ : stage.status === 'current'
212
+ ? '[当前]'
213
+ : stage.status === 'skipped'
214
+ ? '[跳过]'
215
+ : '[待办]';
216
+ console.log(`${marker} ${stage.index}. ${stage.title} — ${stage.description}`);
217
+ if (stage.detail) {
218
+ console.log(` ${stage.detail}`);
219
+ }
220
+ }
221
+ if (result.nextAction) {
222
+ console.log(`下一步: ${result.nextAction}`);
223
+ }
224
+ if (result.reviewPath) {
225
+ console.log(`评审页面: ${result.reviewPath}`);
226
+ }
227
+ }