@openprd/cli 0.1.0 → 0.1.8

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 (138) hide show
  1. package/.openprd/README.md +43 -69
  2. package/.openprd/README_EN.md +84 -0
  3. package/.openprd/benchmarks/index.md +7 -0
  4. package/.openprd/benchmarks/sources.yaml +25 -3
  5. package/.openprd/discovery/config.json +16 -2
  6. package/.openprd/engagements/active/flows.md +19 -14
  7. package/.openprd/engagements/active/handoff.md +11 -4
  8. package/.openprd/engagements/active/prd.md +99 -71
  9. package/.openprd/engagements/active/review.html +4 -4
  10. package/.openprd/engagements/active/roles.md +9 -8
  11. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
  12. package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
  13. package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
  14. package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
  15. package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
  16. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
  17. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
  18. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
  19. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
  20. package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
  21. package/.openprd/knowledge/index.json +44 -4
  22. package/.openprd/reviews/v0001.html +195 -129
  23. package/.openprd/reviews/v0002.html +1150 -0
  24. package/.openprd/reviews/v0003.html +1150 -0
  25. package/.openprd/reviews/v0004.html +1150 -0
  26. package/.openprd/reviews/v0005.html +1150 -0
  27. package/.openprd/standards/config.json +12 -9
  28. package/.openprd/state/changes.json +17 -2
  29. package/.openprd/state/current.json +399 -63
  30. package/.openprd/state/release-ledger.json +344 -0
  31. package/.openprd/state/version-index.json +52 -0
  32. package/.openprd/state/versions/v0002.json +264 -0
  33. package/.openprd/state/versions/v0002.md +183 -0
  34. package/.openprd/state/versions/v0003.json +269 -0
  35. package/.openprd/state/versions/v0003.md +188 -0
  36. package/.openprd/state/versions/v0004.json +274 -0
  37. package/.openprd/state/versions/v0004.md +193 -0
  38. package/.openprd/state/versions/v0005.json +299 -0
  39. package/.openprd/state/versions/v0005.md +189 -0
  40. package/.openprd/templates/agent/intake.md +5 -4
  41. package/.openprd/templates/b2b/intake.md +5 -4
  42. package/.openprd/templates/base/intake.md +10 -4
  43. package/.openprd/templates/company/README.md +9 -7
  44. package/.openprd/templates/company/README_EN.md +12 -0
  45. package/.openprd/templates/consumer/intake.md +5 -4
  46. package/.openprd/templates/industry/README.md +12 -10
  47. package/.openprd/templates/industry/README_EN.md +18 -0
  48. package/.openprd/templates/project/README.md +11 -9
  49. package/.openprd/templates/project/README_EN.md +16 -0
  50. package/.openprd/templates/session/README.md +11 -9
  51. package/.openprd/templates/session/README_EN.md +16 -0
  52. package/AGENTS.md +12 -8
  53. package/README.md +402 -441
  54. package/README_CN.md +4 -578
  55. package/README_EN.md +850 -0
  56. package/docs/assets/openprd-requirement-routing-en.png +0 -0
  57. package/docs/assets/openprd-requirement-routing-en.svg +102 -0
  58. package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
  59. package/docs/assets/openprd-requirement-routing-zh.png +0 -0
  60. package/docs/assets/openprd-requirement-routing-zh.svg +102 -0
  61. package/package.json +6 -2
  62. package/scripts/dev-check-wrapup-copy.mjs +110 -0
  63. package/scripts/openprd-github-release-notes.mjs +99 -0
  64. package/scripts/quality-perf-check.mjs +203 -0
  65. package/skills/openprd-benchmark-router/SKILL.md +1 -0
  66. package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
  67. package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
  68. package/skills/openprd-discovery-loop/SKILL.md +2 -2
  69. package/skills/openprd-harness/SKILL.md +46 -24
  70. package/skills/openprd-harness/references/workflow-gates.md +15 -0
  71. package/skills/openprd-quality/SKILL.md +10 -4
  72. package/skills/openprd-requirement-intake/SKILL.md +39 -23
  73. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
  74. package/skills/openprd-requirement-intake/references/routing-rubric.md +22 -8
  75. package/skills/openprd-router/SKILL.md +2 -2
  76. package/skills/openprd-shared/SKILL.md +51 -23
  77. package/skills/openprd-standards/SKILL.md +2 -1
  78. package/src/agent-integration.js +265 -65
  79. package/src/benchmark/constants.js +107 -0
  80. package/src/benchmark/operations.js +235 -0
  81. package/src/benchmark/registry.js +64 -0
  82. package/src/benchmark/render.js +115 -0
  83. package/src/benchmark/source.js +617 -0
  84. package/src/benchmark/storage.js +121 -0
  85. package/src/benchmark/verify.js +235 -0
  86. package/src/benchmark.js +50 -851
  87. package/src/change-summary.js +339 -0
  88. package/src/cli/args.js +67 -6
  89. package/src/cli/basic-print.js +365 -0
  90. package/src/cli/benchmark-print.js +91 -0
  91. package/src/cli/change-print.js +221 -0
  92. package/src/cli/doctor-print.js +268 -0
  93. package/src/cli/growth-print.js +176 -0
  94. package/src/cli/print.js +73 -1384
  95. package/src/cli/quality-print.js +284 -0
  96. package/src/cli/run-print.js +297 -0
  97. package/src/cli/shared-print.js +127 -0
  98. package/src/cli/workflow-print.js +195 -0
  99. package/src/codex-hook-runner-template.mjs +639 -117
  100. package/src/codex-runtime.js +324 -0
  101. package/src/dev-standards.js +178 -5
  102. package/src/diagram-core.js +5 -5
  103. package/src/discovery.js +2 -1
  104. package/src/execution-strategy.js +369 -0
  105. package/src/fleet.js +4 -0
  106. package/src/github-release.js +156 -0
  107. package/src/growth.js +311 -13
  108. package/src/html-artifact-utils.js +25 -0
  109. package/src/html-artifacts.js +157 -1596
  110. package/src/knowledge.js +1176 -75
  111. package/src/language-policy.js +2 -112
  112. package/src/learning-html-artifact.js +1031 -0
  113. package/src/learning-review.js +3 -2
  114. package/src/loop.js +280 -9
  115. package/src/openprd.js +341 -38
  116. package/src/openspec/change-validate.js +0 -9
  117. package/src/openspec/execute.js +79 -3
  118. package/src/openspec/generate.js +33 -20
  119. package/src/openspec/tasks.js +33 -2
  120. package/src/prd-core.js +10 -9
  121. package/src/product-type-copy.js +69 -0
  122. package/src/quality-html-artifact.js +108 -9
  123. package/src/quality-learning.js +30 -0
  124. package/src/quality-visual-review.js +237 -0
  125. package/src/quality.js +329 -43
  126. package/src/registry-hygiene.js +54 -0
  127. package/src/release-ledger.js +413 -0
  128. package/src/review-presentation.js +12 -6
  129. package/src/run-harness.js +722 -48
  130. package/src/self-update.js +1 -1
  131. package/src/session-binding.js +40 -3
  132. package/src/session-registry.js +159 -0
  133. package/src/standards.js +5 -3
  134. package/src/test-strategy.js +386 -0
  135. package/src/visual-compare.js +915 -34
  136. package/src/work-unit-migration.js +5 -1
  137. package/src/workspace-core.js +343 -19
  138. package/src/workspace-workflow.js +538 -134
@@ -2,16 +2,16 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
4
  import { analyzePrdSnapshot, buildPrdSnapshot, diffSnapshots, formatVersionId, renderPrdMarkdown, summarizeSnapshot } from './prd-core.js';
5
+ import { formatProductTypeDisplay, formatProductTypeOptions, formatProductTypeQuestion, formatProductTypeSentence, formatTemplatePackDisplay } from './product-type-copy.js';
5
6
  import { getDiagramReviewState } from './diagram-workspace.js';
6
7
  import { exists, parseYamlText, readJson, readText, writeJson, writeText } from './fs-utils.js';
7
8
  import { artifactBundlePaths, canonicalReviewPath, defaultReviewArtifactPath, openArtifactInBrowser, renderPlaygroundArtifact, renderPlaygroundMarkdown, renderPlaygroundPatch, renderReviewArtifact, renderReviewEntryHtml, writeHtmlArtifact } from './html-artifacts.js';
8
- import { findOpenPrdSpecLanguageViolations } from './language-policy.js';
9
- import { buildSpec as buildOpenSpecSpec } from './openspec/generate.js';
9
+ import { buildReleaseLedgerSummary } from './release-ledger.js';
10
10
  import { assertReviewPresentationReady, getReviewPresentationGate } from './review-presentation.js';
11
11
  import { syncSessionBindingFromReview, syncSessionBindingFromSnapshot } from './session-binding.js';
12
12
  import { timestamp } from './time.js';
13
13
  import { generateWorkUnitId, normalizeWorkUnitId, readWorkUnitBinding, resolveTargetRoot, writeWorkUnitBinding } from './work-unit.js';
14
- import { appendDecision, appendOpenQuestions, appendProgress, appendWorkflowEvent, buildClarificationPlan, buildClarificationState, buildWorkflowTaskGraph, CAPTURE_SOURCES, coerceCapturedValue, deriveGateLabels, detectWorkspaceScenario, extractMarkdownSection, FIELD_PATH_TO_STATE_KEY, isSupportedProductType, loadLatestVersionSnapshot, loadWorkspace, normalizeVersionId, readVersionIndex, readVersionSnapshot, renderFlowDoc, renderHandoffDoc, renderRolesDoc, resolveActiveTemplatePack, resolveCurrentProductType, validateWorkspace, writeVersionIndex, writeVersionSnapshot } from './workspace-core.js';
14
+ import { appendDecision, appendOpenQuestions, appendProgress, appendWorkflowEvent, buildClarificationPlan, buildClarificationState, buildCurrentStateSnapshot, buildWorkflowTaskGraph, CAPTURE_SOURCES, coerceCapturedValue, deriveGateLabels, detectWorkspaceScenario, extractMarkdownSection, FIELD_PATH_TO_STATE_KEY, isSupportedProductType, loadCurrentLaneSnapshot, loadLatestVersionSnapshot, loadWorkspace, normalizeVersionId, persistWorkspaceCurrentState, readActiveRequirementLane, readVersionIndex, readVersionSnapshot, renderFlowDoc, renderHandoffDoc, renderRolesDoc, resolveActiveTemplatePack, resolveCurrentProductType, validateWorkspace, writeVersionIndex, writeVersionSnapshot } from './workspace-core.js';
15
15
 
16
16
  function requirementGatePath(projectRoot) {
17
17
  return path.join(projectRoot, '.openprd', 'harness', 'requirement-gate.json');
@@ -162,8 +162,8 @@ function normalizePrdReviewStatus(status) {
162
162
  }
163
163
 
164
164
  async function readActiveRequirementGate(projectRoot) {
165
- const gate = await readJson(requirementGatePath(projectRoot)).catch(() => null);
166
- return gate?.active ? gate : null;
165
+ const lane = await readActiveRequirementLane(projectRoot).catch(() => null);
166
+ return lane?.gate?.active ? lane.gate : null;
167
167
  }
168
168
 
169
169
  function meaningfulOverrideValue(value) {
@@ -231,7 +231,10 @@ function resolveReviewPaths(ws, snapshot) {
231
231
 
232
232
  async function writeReviewFiles(ws, snapshot, { writeEntry = true } = {}) {
233
233
  assertReviewPresentationReady(snapshot);
234
- const reviewHtml = renderReviewArtifact({ snapshot });
234
+ const reviewHtml = renderReviewArtifact({
235
+ snapshot,
236
+ projectRelease: buildReleaseLedgerSummary(ws.data.releaseLedger),
237
+ });
235
238
  const { canonicalReview, activeReviewEntry } = resolveReviewPaths(ws, snapshot);
236
239
  await writeHtmlArtifact(canonicalReview, reviewHtml);
237
240
  if (writeEntry) {
@@ -357,24 +360,6 @@ function requirementLooksLikeInterfaceWork(gate) {
357
360
  return /界面|页面|菜单|入口|按钮|表单|弹窗|导航|布局|看板|列表|配置页|模块|组件|UI|tab/i.test(text);
358
361
  }
359
362
 
360
- function assertOpenSpecPreflightReady(snapshot) {
361
- const specText = buildOpenSpecSpec({ snapshot });
362
- const violations = findOpenPrdSpecLanguageViolations(specText);
363
- if (violations.length === 0) {
364
- return;
365
- }
366
- const examples = violations
367
- .slice(0, 3)
368
- .map((violation) => `第 ${violation.line} 行:${violation.reason}(${violation.text})`)
369
- .join(';');
370
- throw new Error([
371
- 'OpenPrd 已阻止 synthesize:按当前 PRD 生成的 spec.md 仍会触发简体中文预检,review.html 还不能进入确认。',
372
- '请先把标题、问题陈述和场景文案整理成可直接产出 spec 的简体中文表达。',
373
- '如果只是内部措辞规范化,请先用 openprd capture . --source agent-normalized 写回,再重新 synthesize。',
374
- examples ? `示例:${examples}。` : null,
375
- ].filter(Boolean).join(' '));
376
- }
377
-
378
363
  function requirementPrompt(gate) {
379
364
  return String(gate?.promptPreview ?? '').trim();
380
365
  }
@@ -450,6 +435,315 @@ function shortList(items, fallback = '待补充') {
450
435
  return list.length > 0 ? list.slice(0, 3).join(';') : fallback;
451
436
  }
452
437
 
438
+ function normalizeTextList(items) {
439
+ if (Array.isArray(items)) {
440
+ return items.map((item) => String(item || '').trim()).filter(Boolean);
441
+ }
442
+ const text = String(items || '').trim();
443
+ return text ? [text] : [];
444
+ }
445
+
446
+ function summarizeProductShape(productType, gate) {
447
+ if (productType === 'consumer' || productType === 'b2b' || productType === 'agent' || productType === 'base') {
448
+ return formatProductTypeSentence(productType);
449
+ }
450
+ const text = requirementPrompt(gate);
451
+ if (/agent|自动化|workflow|MCP|tool|skill/i.test(text)) {
452
+ return formatProductTypeSentence('agent', { inferred: true });
453
+ }
454
+ if (/企业|团队|后台|审批|权限|客户|运营/i.test(text)) {
455
+ return formatProductTypeSentence('b2b', { inferred: true });
456
+ }
457
+ if (/个人|用户|社区|内容|创作者|消费|c端|to c/i.test(text)) {
458
+ return formatProductTypeSentence('consumer', { inferred: true });
459
+ }
460
+ return `产品场景仍待确认,可在 ${formatProductTypeOptions()} 之间进一步锁定。`;
461
+ }
462
+
463
+ function summarizeArchitectureSignals(gate, snapshot) {
464
+ const sections = snapshot.sections ?? {};
465
+ const text = `${requirementPrompt(gate)} ${JSON.stringify({
466
+ technical: sections.constraints?.technical ?? [],
467
+ dependencies: sections.constraints?.dependencies ?? [],
468
+ })}`;
469
+ const hits = [];
470
+ if (/前端|页面|客户端|web|h5|移动端|ios|android|桌面|ui|交互|网站|站点|博客|内容|文章|落地页/i.test(text)) {
471
+ hits.push('用户入口与页面体验');
472
+ }
473
+ if (/后端|服务端|server|api|数据库|存储|队列|定时任务|cron|webhook/i.test(text)) {
474
+ hits.push('服务流程与数据处理');
475
+ }
476
+ if (/agent|workflow|tool|skill|MCP|自动化|编排/i.test(text)) {
477
+ hits.push('Agent 协作与自动执行');
478
+ }
479
+ return hits.length > 0
480
+ ? hits.join(';')
481
+ : '影响环节仍待确认,可先按用户入口、服务流程或 Agent 协作补齐。';
482
+ }
483
+
484
+ function collectProjectRiskProbes(gate, snapshot) {
485
+ const sections = snapshot.sections ?? {};
486
+ const text = `${requirementPrompt(gate)} ${JSON.stringify({
487
+ users: sections.users ?? {},
488
+ constraints: sections.constraints ?? {},
489
+ requirements: sections.requirements ?? {},
490
+ })}`;
491
+ const probes = [];
492
+ const probeMap = [
493
+ { label: '账号身份与可见范围', pattern: /登录|账号|auth|oauth|权限|角色|rbac/i },
494
+ { label: '用户数据与信息处理', pattern: /数据|数据库|存储|隐私|文件|上传|下载|同步/i },
495
+ { label: '团队协作与审批流转', pattern: /团队|协作|审批|组织|成员|管理员|buyer|admin/i },
496
+ { label: '外部系统与合作方对接', pattern: /第三方|外部|api|sdk|集成|webhook|支付|短信/i },
497
+ { label: 'AI 结果可靠性与人工兜底', pattern: /ai|agent|模型|llm|生成|推理|prompt/i },
498
+ { label: '收费模式、额度与成本', pattern: /收费|订阅|付费|价格|额度|点数|积分|成本|quota|billing/i },
499
+ ];
500
+ for (const probe of probeMap) {
501
+ if (probe.pattern.test(text)) {
502
+ probes.push(probe.label);
503
+ }
504
+ }
505
+ return probes;
506
+ }
507
+
508
+ function buildProjectFraming({ gate, snapshot, scenario, productType }) {
509
+ const sections = snapshot.sections ?? {};
510
+ const guardrailHints = [
511
+ ...normalizeTextList(sections.requirements?.businessRules),
512
+ ...normalizeTextList(sections.constraints?.dependencies),
513
+ ];
514
+ const riskProbes = collectProjectRiskProbes(gate, snapshot);
515
+ return {
516
+ audience: shortList(
517
+ sections.users?.primaryUsers,
518
+ scenario.id === 'cold-start-existing-project'
519
+ ? '需要先确认这次改动主要服务谁。'
520
+ : '需要先确认目标用户或关键角色。'
521
+ ),
522
+ productShape: summarizeProductShape(productType, gate),
523
+ firstSlice: shortList(sections.scope?.inScope, '需要先确认第一版最小可用切片。'),
524
+ nonGoals: shortList(sections.scope?.outOfScope, '需要先确认本轮先不做什么。'),
525
+ guardrails: shortList(guardrailHints, '需要先确认哪些现有能力、数据、流程或体验不能被破坏。'),
526
+ architectureSignals: summarizeArchitectureSignals(gate, snapshot),
527
+ riskProbes,
528
+ riskProbeSummary: shortList(riskProbes, '当前没有明显命中额外风险探针。'),
529
+ };
530
+ }
531
+
532
+ function escapeMarkdownTableCell(value) {
533
+ return String(value ?? '').replace(/\|/g, '/');
534
+ }
535
+
536
+ function containsPendingClarifyMarker(value) {
537
+ return /(待确认|需要先确认|仍待确认|未分类)/.test(String(value ?? ''));
538
+ }
539
+
540
+ function describeProductLensFocus(productType) {
541
+ if (productType === 'consumer') {
542
+ return '这轮我会重点看首次使用场景、关键感受,以及用户愿不愿意继续回来。';
543
+ }
544
+ if (productType === 'b2b') {
545
+ return '这轮我会重点看谁拍板、谁使用、谁运营,以及上线协作会卡在哪里。';
546
+ }
547
+ if (productType === 'agent') {
548
+ return '这轮我会重点看哪些步骤让 Agent 自主完成、哪些节点必须人工拍板,以及失败时谁兜底。';
549
+ }
550
+ return '这轮我会优先把用户、场景、第一版切片、边界和风险讲清楚。';
551
+ }
552
+
553
+ function requirementTypeDisplay(gate) {
554
+ const tier = String(gate?.intent?.requirementTier ?? '').toLowerCase();
555
+ if (tier === 'l0') {
556
+ return '快速修正(L0)';
557
+ }
558
+ if (tier === 'l1') {
559
+ return '现有功能优化(L1)';
560
+ }
561
+ return '新功能/新流程方案(L2)';
562
+ }
563
+
564
+ function inferredProductTypeDisplay(productType, gate) {
565
+ if (productType === 'consumer' || productType === 'b2b' || productType === 'agent' || productType === 'base') {
566
+ return formatProductTypeDisplay(productType);
567
+ }
568
+ const text = requirementPrompt(gate);
569
+ if (/agent|自动化|workflow|MCP|tool|skill/i.test(text)) {
570
+ return formatProductTypeDisplay('agent');
571
+ }
572
+ if (/企业|团队|后台|审批|权限|客户|运营/i.test(text)) {
573
+ return formatProductTypeDisplay('b2b');
574
+ }
575
+ if (/个人|用户|社区|内容|创作者|消费|c端|to c/i.test(text)) {
576
+ return formatProductTypeDisplay('consumer');
577
+ }
578
+ return '待确认';
579
+ }
580
+
581
+ function fallbackScopeText(text, fallback) {
582
+ return containsPendingClarifyMarker(text) ? fallback : text;
583
+ }
584
+
585
+ function buildMarkdownTable(headers, rows) {
586
+ const normalizedRows = rows
587
+ .filter((row) => Array.isArray(row) && row.length > 0)
588
+ .map((row) => headers.map((_, index) => escapeMarkdownTableCell(row[index] ?? '待确认')));
589
+ return [
590
+ `| ${headers.map(escapeMarkdownTableCell).join(' | ')} |`,
591
+ `| ${headers.map(() => '---').join(' | ')} |`,
592
+ ...normalizedRows.map((row) => `| ${row.join(' | ')} |`),
593
+ ];
594
+ }
595
+
596
+ function inferCoreScopeModule(prompt, productType) {
597
+ const text = String(prompt ?? '');
598
+ if (/页面|界面|布局|导航|按钮|表单|网站|站点|博客|内容|文章|落地页|UI/i.test(text)) {
599
+ return '用户入口与界面';
600
+ }
601
+ if (/后端|服务端|接口|api|数据库|存储|同步|队列|webhook|数据/i.test(text)) {
602
+ return '服务流程与数据处理';
603
+ }
604
+ if (/agent|自动化|workflow|tool|skill|MCP|编排/i.test(text) || productType === 'agent') {
605
+ return 'Agent 主流程';
606
+ }
607
+ return '核心主流程';
608
+ }
609
+
610
+ function pushScopeRow(rows, seen, module, inScope, outOfScope) {
611
+ const key = String(module ?? '').trim();
612
+ if (!key || seen.has(key)) {
613
+ return;
614
+ }
615
+ seen.add(key);
616
+ rows.push([module, inScope, outOfScope]);
617
+ }
618
+
619
+ function buildScopeTableRows({ projectFraming, prompt, productType }) {
620
+ const rows = [];
621
+ const seen = new Set();
622
+ const text = `${prompt ?? ''} ${projectFraming.architectureSignals ?? ''}`;
623
+ const firstSlice = fallbackScopeText(
624
+ projectFraming.firstSlice,
625
+ '先把第一版核心主流程收敛到一个最小可交付闭环。'
626
+ );
627
+ const nonGoals = fallbackScopeText(
628
+ projectFraming.nonGoals,
629
+ '暂不把与核心目标无关的大范围扩展一起塞进第一版。'
630
+ );
631
+ if (/用户入口与页面体验|页面|界面|布局|导航|按钮|表单|网站|站点|博客|内容|文章|落地页|UI/i.test(text)) {
632
+ pushScopeRow(
633
+ rows,
634
+ seen,
635
+ '用户入口与界面',
636
+ containsPendingClarifyMarker(projectFraming.firstSlice)
637
+ ? '先把用户看到的入口、页面结构和关键交互讲清楚。'
638
+ : firstSlice,
639
+ nonGoals,
640
+ );
641
+ }
642
+ if (/服务流程与数据处理|后端|服务端|接口|api|数据库|存储|同步|队列|webhook|数据/i.test(text)) {
643
+ pushScopeRow(
644
+ rows,
645
+ seen,
646
+ '服务流程与数据处理',
647
+ '先只覆盖支撑主流程所需的接口、状态处理或数据留痕。',
648
+ '暂不扩到与这次主流程无关的重构、清库或系统级迁移。',
649
+ );
650
+ }
651
+ if (/Agent 协作与自动执行|agent|自动化|workflow|tool|skill|MCP|编排/i.test(text) || productType === 'agent') {
652
+ pushScopeRow(
653
+ rows,
654
+ seen,
655
+ 'Agent 主流程',
656
+ '先讲清 Agent 负责的步骤、人工确认点和失败恢复。',
657
+ '暂不把所有自动化场景一次性铺开,避免边界失控。',
658
+ );
659
+ }
660
+ if (rows.length === 0) {
661
+ pushScopeRow(rows, seen, inferCoreScopeModule(prompt, productType), firstSlice, nonGoals);
662
+ }
663
+ return rows;
664
+ }
665
+
666
+ function pushTechRow(rows, seen, area, approach, responsibility) {
667
+ const key = String(area ?? '').trim();
668
+ if (!key || seen.has(key)) {
669
+ return;
670
+ }
671
+ seen.add(key);
672
+ rows.push([area, approach, responsibility]);
673
+ }
674
+
675
+ function buildTechnicalSolutionRows({ projectFraming, prompt, productType }) {
676
+ const rows = [];
677
+ const seen = new Set();
678
+ const text = `${prompt ?? ''} ${projectFraming.architectureSignals ?? ''}`;
679
+ if (/用户入口与页面体验|页面|界面|布局|导航|按钮|表单|网站|站点|博客|内容|文章|落地页|UI/i.test(text)) {
680
+ pushTechRow(
681
+ rows,
682
+ seen,
683
+ '前端 / 用户入口',
684
+ '先围绕当前入口、页面结构和关键交互做第一版,不提前铺开大范围视觉重做。',
685
+ '负责把用户真正看到的流程、反馈和操作路径讲清楚。',
686
+ );
687
+ }
688
+ if (/服务流程与数据处理|后端|服务端|接口|api|数据库|存储|同步|队列|webhook|数据/i.test(text)) {
689
+ pushTechRow(
690
+ rows,
691
+ seen,
692
+ '后端 / 服务逻辑',
693
+ '只补当前需求真正需要的接口、状态流转或数据写入,先保证主链路跑通。',
694
+ '负责把规则、状态和数据处理接住,并留下可验证证据。',
695
+ );
696
+ }
697
+ if (/Agent 协作与自动执行|agent|自动化|workflow|tool|skill|MCP|编排/i.test(text) || productType === 'agent') {
698
+ pushTechRow(
699
+ rows,
700
+ seen,
701
+ 'Agent / 自动化编排',
702
+ '先定义 Agent 自主边界、人工拍板点和失败恢复,再决定要不要继续扩编排深度。',
703
+ '负责把多步流程串起来,并在失败时把用户安全带回人工兜底。',
704
+ );
705
+ }
706
+ if (rows.length === 0) {
707
+ pushTechRow(
708
+ rows,
709
+ seen,
710
+ '实现主链路',
711
+ '先按当前确认范围做最小可用闭环,再决定是否扩展到更多技术面。',
712
+ '负责把第一版能力真正落到可验证的主流程里。',
713
+ );
714
+ }
715
+ return rows;
716
+ }
717
+
718
+ function buildClarifyDirectionChoices(productType) {
719
+ if (productType === 'consumer') {
720
+ return [
721
+ '先把第一次上手跑通:更快定第一版,但后续可能还要补留存动作。',
722
+ '先把关键转化做好:更贴近商业结果,但对场景边界要求更高。',
723
+ '先把愿意回来这件事做顺:更利于长期价值,但首版会慢一点。',
724
+ ];
725
+ }
726
+ if (productType === 'b2b') {
727
+ return [
728
+ '先解决单角色提效:更容易落地,但跨团队价值暂时有限。',
729
+ '先打通跨角色流转:业务收益更明显,但协调成本更高。',
730
+ '先补管理与配置能力:便于后续扩张,但首版体感未必最强。',
731
+ ];
732
+ }
733
+ if (productType === 'agent') {
734
+ return [
735
+ '先跑通单步自动化:最快看到结果,但协作链路还比较浅。',
736
+ '先做多步编排:更像完整方案,但边界和失败恢复要更早想清楚。',
737
+ '先把人工兜底做稳:风险更低,但自动化收益会慢一点体现。',
738
+ ];
739
+ }
740
+ return [
741
+ '先把用户最痛的一步打通:更容易收敛首版,但覆盖面会窄一些。',
742
+ '先把主流程串起来:更完整,但第一版实现成本更高。',
743
+ '先把边界和风险控住:更稳,但用户感知收益会慢一点出现。',
744
+ ];
745
+ }
746
+
453
747
  function normalizeClarifyMode(mode) {
454
748
  if (mode === 'artifact') {
455
749
  return 'inline-with-checklist';
@@ -458,8 +752,15 @@ function normalizeClarifyMode(mode) {
458
752
  }
459
753
 
460
754
  function estimateInlineClarificationLines(clarification, reflection) {
755
+ const projectFraming = reflection?.projectContext?.projectFraming ?? null;
461
756
  const activeChangeLines = reflection?.projectContext?.activeChange ? 1 : 0;
462
- return 4
757
+ const framingLines = projectFraming
758
+ ? 5
759
+ + (projectFraming.architectureSignals ? 1 : 0)
760
+ + (projectFraming.riskProbes?.length > 0 ? 1 : 0)
761
+ : 0;
762
+ return 2
763
+ + framingLines
463
764
  + clarification.mustAskUser.length
464
765
  + Math.min(clarification.canInferLater.length, 2)
465
766
  + activeChangeLines;
@@ -498,23 +799,79 @@ function buildInlineClarification({ clarification, reflection, presentation }) {
498
799
  }
499
800
  const prompt = reflection?.promptPreview || '本轮需求';
500
801
  const projectContext = reflection?.projectContext ?? {};
802
+ const projectFraming = projectContext.projectFraming ?? {};
803
+ const productType = projectContext.productType;
804
+ const primaryQuestion = clarification.mustAskUser[0] ?? null;
805
+ const followUpQuestions = clarification.mustAskUser.slice(1, presentation.mode === 'inline' ? 2 : 3);
806
+ const scopeTable = buildMarkdownTable(
807
+ ['功能模块', '这次先做什么', '这次先不做什么'],
808
+ buildScopeTableRows({ projectFraming, prompt, productType }),
809
+ );
810
+ const technicalTable = buildMarkdownTable(
811
+ ['技术部分', '初步方案', '主要负责什么'],
812
+ buildTechnicalSolutionRows({ projectFraming, prompt, productType }),
813
+ );
814
+ const directionChoices = (
815
+ containsPendingClarifyMarker(projectFraming.firstSlice)
816
+ || containsPendingClarifyMarker(projectFraming.audience)
817
+ )
818
+ ? buildClarifyDirectionChoices(productType)
819
+ : [];
501
820
  const lines = [
502
- `我理解的目标:${prompt}`,
503
- `落点:${projectContext.productName ?? '当前项目'};按${projectContext.scenario ?? '当前工作区'}处理。`,
504
- '范围边界:只处理本轮需求,不自动合并历史 active change 或未提到的扩展。',
505
- '验收方式:确认用户能看到或完成的结果,以及哪些既有行为不能被改变。',
821
+ `我先用产品和业务语言复述一下这次需求,并先按总分结构收一下:${prompt}`,
822
+ '',
823
+ '需求判断:',
824
+ `- 需求类型:${requirementTypeDisplay(reflection?.gate)}。`,
825
+ `- 产品类型:${inferredProductTypeDisplay(productType, reflection?.gate)}。`,
826
+ '',
827
+ '需求理解:',
828
+ `- 主要服务对象:${projectFraming.audience ?? '待确认'}。`,
829
+ `- 使用场景更像:${projectFraming.productShape ?? '待确认'}。`,
830
+ `- 第一版先让用户做到:${projectFraming.firstSlice ?? '待确认'}。`,
831
+ `- 这轮先不碰:${projectFraming.nonGoals ?? '待确认'}。`,
832
+ `- 必须守住:${projectFraming.guardrails ?? '待确认'}。`,
506
833
  ];
834
+ if (projectFraming.architectureSignals) {
835
+ lines.push(`- 这次更可能会影响:${projectFraming.architectureSignals}。`);
836
+ }
837
+ if (projectFraming.riskProbes?.length > 0) {
838
+ lines.push(`- 我先提醒的业务风险:${projectFraming.riskProbeSummary}。`);
839
+ }
840
+ lines.push(`- ${describeProductLensFocus(productType)}`);
841
+ lines.push('- 判断这轮是否值得做成:看用户是否真的更顺、更快、更稳地完成关键动作。');
842
+ lines.push('');
843
+ lines.push('功能范围:');
844
+ lines.push(...scopeTable);
845
+ lines.push('');
846
+ lines.push('技术方案:');
847
+ lines.push(...technicalTable);
507
848
  if (projectContext.activeChange) {
849
+ lines.push('');
508
850
  lines.push(`历史提醒:当前还有 ${projectContext.activeChange.activeChange},本轮先分开处理。`);
509
851
  }
510
- const questions = clarification.mustAskUser.slice(0, presentation.mode === 'inline' ? 3 : 5);
511
- if (questions.length > 0) {
512
- lines.push('建议确认:');
513
- for (const item of questions) {
514
- lines.push(`- ${item.prompt}`);
852
+ lines.push('');
853
+ if (primaryQuestion) {
854
+ lines.push('我建议这轮先确认这一点:');
855
+ lines.push(`- ${primaryQuestion.prompt}`);
856
+ if (followUpQuestions.length > 0) {
857
+ lines.push('这点定下来后,我再继续补下面这些:');
858
+ for (const item of followUpQuestions) {
859
+ lines.push(`- ${item.prompt}`);
860
+ }
861
+ }
862
+ } else {
863
+ lines.push('我这边暂时没有新的关键追问了。');
864
+ }
865
+ if (directionChoices.length > 0) {
866
+ lines.push('如果你现在还不想展开细节,也可以先在这 3 个方向里选一个:');
867
+ for (const choice of directionChoices) {
868
+ lines.push(`- ${choice}`);
515
869
  }
870
+ lines.push('你可以先回一个方向编号,或直接补一句你的倾向。');
871
+ lines.push('我收到后会先整理需求摘要给你确认;确认后再进入 PRD 和评审流程,不会直接跳到实现。');
516
872
  } else {
517
- lines.push('建议确认:如果以上理解正确,用户回复“可以”或“确认执行”后再继续。');
873
+ lines.push('如果以上理解基本对,请先回复“可以”,或直接指出要调整的地方。');
874
+ lines.push('我收到后会先整理需求摘要给你确认;确认后再进入 PRD 和评审流程,不会直接跳到实现。');
518
875
  }
519
876
  return {
520
877
  mode: presentation.mode,
@@ -547,33 +904,72 @@ function reflectionQuestion(id, label, prompt) {
547
904
  };
548
905
  }
549
906
 
907
+ function buildLensReflectionQuestion(productType) {
908
+ if (productType === 'consumer') {
909
+ return reflectionQuestion('product-lens', '用户场景与回访价值', '请确认这次主要服务哪类个人用户、他们第一次会在什么场景下想用它,以及什么结果会让他们愿意继续回来。');
910
+ }
911
+ if (productType === 'b2b') {
912
+ return reflectionQuestion('product-lens', '角色关系与上线阻力', '请确认谁拍板、谁使用、谁负责推进或运营,以及最可能拖慢上线的是哪段协作、审批或对接。');
913
+ }
914
+ if (productType === 'agent') {
915
+ return reflectionQuestion('product-lens', '自主边界与人工兜底', '请确认哪些步骤希望 Agent 自主完成、哪些节点必须人工拍板,以及失败时由谁接住。');
916
+ }
917
+ return reflectionQuestion('product-lens', '用户价值与取舍', '请确认这次最重要的用户价值是什么;如果只能先保一个方向,你更想先保效率、体验,还是风险可控。');
918
+ }
919
+
920
+ function shouldBuildRequirementIntakeReflection({ gate, scenario, analysis }) {
921
+ if (gate?.active) {
922
+ return true;
923
+ }
924
+ if (['cold-start-greenfield', 'cold-start-existing-project'].includes(scenario?.id)) {
925
+ return true;
926
+ }
927
+ return Number(analysis?.missingRequiredFields ?? 0) > 0;
928
+ }
929
+
550
930
  async function buildRequirementIntakeReflection({ projectRoot, ws, snapshot, analysis, scenario, gate }) {
551
- if (!gate?.active) {
931
+ if (!shouldBuildRequirementIntakeReflection({ gate, scenario, analysis })) {
552
932
  return null;
553
933
  }
554
934
 
935
+ const sections = snapshot.sections ?? {};
555
936
  const text = requirementPrompt(gate);
937
+ const promptPreview = text || sections.problem?.problemStatement || snapshot.title || '当前需求待确认';
556
938
  const complexity = detectRequirementIntakeComplexity(gate);
557
939
  const activeChange = await readActiveChangeHint(projectRoot);
558
- const sections = snapshot.sections ?? {};
559
940
  const productName = snapshot.title || sections.meta?.title || '当前项目';
560
941
  const productType = snapshot.productType ?? resolveCurrentProductType(ws) ?? '未分类';
561
942
  const currentProblem = sections.problem?.problemStatement || '待补充';
562
943
  const currentScope = shortList(sections.scope?.inScope, '当前范围还没有稳定记录');
563
944
  const missing = analysis.missingFields.slice(0, 4).map((field) => field.label);
564
945
  const needsInterfaceSketch = requirementLooksLikeInterfaceWork(gate);
946
+ const projectFraming = buildProjectFraming({
947
+ gate,
948
+ snapshot,
949
+ scenario,
950
+ productType: snapshot.productType ?? resolveCurrentProductType(ws),
951
+ });
952
+ const lensQuestion = buildLensReflectionQuestion(snapshot.productType ?? resolveCurrentProductType(ws));
953
+ const needsDeliveryShapeQuestion = !needsInterfaceSketch
954
+ && (
955
+ projectFraming.riskProbes.length > 0
956
+ || !containsPendingClarifyMarker(projectFraming.architectureSignals)
957
+ );
565
958
  const mustConfirm = complexity.mode === 'deep'
566
959
  ? [
567
- reflectionQuestion('intent', '意图与目标', '请确认我对需求目标的理解:目标用户是谁、在哪个场景下,需要完成什么结果?'),
568
- reflectionQuestion('project-context', '项目影响范围', '结合当前项目,哪些已有模块、入口、流程或历史需求必须复用,哪些可以调整?'),
569
- reflectionQuestion('scope-quality', '范围与验收', '这个需求的范围内、范围外、成功标准和失败路径分别是什么?'),
960
+ reflectionQuestion('intent', '意图与目标', '请确认我理解得对不对:这次主要是谁在什么场景下遇到什么问题,第一版最想先改善什么结果?'),
961
+ lensQuestion,
962
+ reflectionQuestion('project-context', '项目影响范围', '结合当前项目,请确认第一版最小可用切片是什么;哪些已有模块、入口、流程或历史需求必须复用,哪些可以调整?'),
963
+ reflectionQuestion('scope-quality', '范围与验收', '请确认这次先做到哪一步就算有价值;哪些这轮先不动;哪些老用户习惯、现有业务结果或交付节奏不能被影响?'),
570
964
  needsInterfaceSketch
571
965
  ? reflectionQuestion('interface-sketch', '界面或流程草图', '需求涉及界面或流程,请先确认主要区域、操作入口、预览/确认点和风险提示。')
572
- : reflectionQuestion('details-boundary', '细节与边界', '请确认关键字段、状态变化、数据来源、权限边界和可验收细节。'),
966
+ : needsDeliveryShapeQuestion
967
+ ? reflectionQuestion('delivery-shape', '影响环节与业务风险', '请确认这次大概会牵动哪些环节,例如用户入口、内部流程、账号与权限、外部对接、AI 自动化或成本控制;其中最大的业务风险是什么。')
968
+ : reflectionQuestion('details-boundary', '关键状态与验收细节', '请确认用户会看到的关键状态、重要字段、例外场景和最小验收标准。'),
573
969
  ]
574
970
  : [
575
- reflectionQuestion('project-context', '项目映射', '请确认这个调整具体落在哪个页面、模块、入口或流程,以及哪些已有行为不能被改变。'),
576
- reflectionQuestion('acceptance', '验收方式', '请确认完成后用户能看到或做到什么,以及最小验收标准是什么。'),
971
+ reflectionQuestion('project-context', '项目映射', '请确认这个调整具体落在哪个页面、模块、入口或流程,以及第一版先做哪一小块最有价值。'),
972
+ reflectionQuestion('acceptance', '验收方式', '请确认完成后用户能明显感受到什么变化、哪些既有行为不能改变,以及最小验收标准是什么。'),
577
973
  ];
578
974
 
579
975
  return {
@@ -583,7 +979,7 @@ async function buildRequirementIntakeReflection({ projectRoot, ws, snapshot, ana
583
979
  label: complexity.label,
584
980
  minimumDepth: complexity.minimumDepth,
585
981
  questionLimit: gateQuestionLimit(gate, complexity.questionLimit),
586
- promptPreview: text,
982
+ promptPreview,
587
983
  reasons: complexity.reasons,
588
984
  needsInterfaceSketch,
589
985
  projectContext: {
@@ -595,15 +991,16 @@ async function buildRequirementIntakeReflection({ projectRoot, ws, snapshot, ana
595
991
  currentScope,
596
992
  activeChange,
597
993
  missingFields: missing,
994
+ projectFraming,
598
995
  },
599
996
  rounds: [
600
997
  {
601
998
  id: 'intent-normalization',
602
999
  title: '第 1 轮:意图归一化',
603
1000
  findings: [
604
- `用户原始输入:${text || '待补充'}`,
1001
+ `用户原始输入:${promptPreview}`,
605
1002
  `初步判断:${complexity.label}`,
606
- `需要先把表达收敛成用户、场景、目标、动作和期望结果。`,
1003
+ `需要先把表达收敛成用户、产品形态、场景、目标、动作和期望结果。`,
607
1004
  ],
608
1005
  },
609
1006
  {
@@ -611,8 +1008,9 @@ async function buildRequirementIntakeReflection({ projectRoot, ws, snapshot, ana
611
1008
  title: '第 2 轮:项目上下文映射',
612
1009
  findings: [
613
1010
  `工作区场景:${scenario.label},${scenario.reason}`,
614
- `当前产品:${productName}(${productType}),已记录问题:${currentProblem}`,
1011
+ `当前产品:${productName};当前产品场景:${formatProductTypeDisplay(productType, { fallback: '待确认' })};已记录问题:${currentProblem}`,
615
1012
  `当前范围线索:${currentScope}`,
1013
+ `首轮画像:用户群体=${projectFraming.audience};产品形态=${projectFraming.productShape};第一版先做=${projectFraming.firstSlice}`,
616
1014
  activeChange ? `仍有 active change:${activeChange.activeChange}(${activeChange.status}),需要和本轮需求分开评估。` : '当前没有检测到 active change 冲突。',
617
1015
  ],
618
1016
  },
@@ -621,7 +1019,9 @@ async function buildRequirementIntakeReflection({ projectRoot, ws, snapshot, ana
621
1019
  title: '第 3 轮:产品质量自检',
622
1020
  findings: [
623
1021
  `仍需确认的信息:${shortList(missing, '暂无明显缺口')}`,
624
- needsInterfaceSketch ? '需求看起来涉及界面或流程,需要先给用户确认草图或关键操作路径。' : '需求暂未明显命中界面,但仍要确认状态、边界和验收方式。',
1022
+ `边界与约束:先不做=${projectFraming.nonGoals};不能破坏=${projectFraming.guardrails}`,
1023
+ needsInterfaceSketch ? '需求看起来涉及界面或流程,需要先给用户确认草图或关键操作路径。' : `影响环节:${projectFraming.architectureSignals}`,
1024
+ `业务提醒:${projectFraming.riskProbeSummary}`,
625
1025
  '进入实现前必须保留范围、非目标、异常路径和验收证据。',
626
1026
  ],
627
1027
  },
@@ -644,11 +1044,23 @@ function renderRequirementIntakeReflection(reflection) {
644
1044
  '## 项目上下文',
645
1045
  '',
646
1046
  `- 工作区场景: ${reflection.projectContext.scenario}`,
647
- `- 当前产品: ${reflection.projectContext.productName} (${reflection.projectContext.productType})`,
1047
+ `- 当前产品: ${reflection.projectContext.productName};产品场景: ${formatProductTypeDisplay(reflection.projectContext.productType, { fallback: '待确认' })}`,
648
1048
  `- 当前问题: ${reflection.projectContext.currentProblem}`,
649
1049
  `- 当前范围: ${reflection.projectContext.currentScope}`,
650
1050
  reflection.projectContext.activeChange ? `- 历史 active change: ${reflection.projectContext.activeChange.activeChange}` : '- 历史 active change: 无',
651
1051
  '',
1052
+ '## 首轮项目画像',
1053
+ '',
1054
+ '| 模块 | 当前理解 |',
1055
+ '|---|---|',
1056
+ `| 用户群体 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.audience ?? '待补充')} |`,
1057
+ `| 产品形态 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.productShape ?? '待补充')} |`,
1058
+ `| 第一版先做 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.firstSlice ?? '待补充')} |`,
1059
+ `| 暂不处理 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.nonGoals ?? '待补充')} |`,
1060
+ `| 不能破坏 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.guardrails ?? '待补充')} |`,
1061
+ `| 影响环节 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.architectureSignals ?? '待补充')} |`,
1062
+ `| 业务提醒 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.riskProbeSummary ?? '待补充')} |`,
1063
+ '',
652
1064
  ];
653
1065
  for (const round of reflection.rounds) {
654
1066
  lines.push(`## ${round.title}`, '');
@@ -677,15 +1089,15 @@ async function writeRequirementIntakeReflection(ws, reflection) {
677
1089
  function buildRequirementIntakeDepth(gate, reflection = null) {
678
1090
  const needsInterfaceSketch = requirementLooksLikeInterfaceWork(gate);
679
1091
  const fallbackLayers = [
680
- reflectionQuestion('product-context', '用户 / 场景 / 问题', '先确认:什么用户,在什么场景下,遇到什么问题?为什么现在值得解决?'),
681
- reflectionQuestion('product-outcome', '目标 / 影响 / 成功标准', '解决后用户能完成什么?减少什么成本或风险?用什么成功指标或验收标准判断有效?'),
682
- reflectionQuestion('product-flow', '现状流程 / 目标流程 / 异常路径', '请拆出当前流程、目标流程、关键决策点、失败路径,以及哪些动作必须由用户确认。'),
1092
+ reflectionQuestion('product-context', '用户 / 产品形态 / 问题', '先确认:这是给谁用的、它更像个人产品 / 团队流程 / Agent 协作中的哪一种、为什么现在值得解决?'),
1093
+ reflectionQuestion('product-outcome', '第一版切片 / 目标 / 成功标准', '请确认第一版最小可用切片是什么;解决后用户先能完成什么,用什么业务结果或验收标准判断有效?'),
1094
+ reflectionQuestion('product-flow', '范围 / 非目标 / 异常路径', '请拆出本轮先做什么、不做什么、哪些既有行为不能被破坏,以及关键失败路径和恢复方式。'),
683
1095
  reflectionQuestion(
684
1096
  'product-detail',
685
1097
  needsInterfaceSketch ? '界面草图 / 字段 / 状态' : '细节 / 状态 / 边界',
686
1098
  needsInterfaceSketch
687
1099
  ? '这个需求涉及界面,请先给用户一版 ASCII 线框草图,标出主要区域、操作入口、预览/确认点和风险提示,让用户确认后再 synthesize。'
688
- : '请补齐关键字段、状态变化、数据来源、权限边界和可验收细节;如果后续发现涉及界面,也要先补 ASCII 线框草图。'
1100
+ : '请补齐用户会看到的关键状态、重要字段、业务边界和可验收细节;如果后续发现涉及界面,也要先补 ASCII 线框草图。'
689
1101
  ),
690
1102
  ];
691
1103
  const layers = reflection?.mustConfirm?.length > 0 ? reflection.mustConfirm : fallbackLayers;
@@ -850,7 +1262,6 @@ async function synthesizeWorkspace(projectRoot, overrides = {}) {
850
1262
 
851
1263
  snapshot.content = renderPrdMarkdown(snapshot);
852
1264
  snapshot.digest = crypto.createHash('sha256').update(snapshot.content).digest('hex');
853
- assertOpenSpecPreflightReady(snapshot);
854
1265
 
855
1266
  await writeVersionSnapshot(ws, snapshot);
856
1267
 
@@ -890,14 +1301,13 @@ async function synthesizeWorkspace(projectRoot, overrides = {}) {
890
1301
  reviewPresentationRequired: !presentationGate.ok,
891
1302
  });
892
1303
  await appendDecision(ws, [
893
- `已生成版本 ${snapshot.versionId}。`,
894
- `产品类型: ${snapshot.productType ?? '未分类'}。`,
895
- `模板包: ${snapshot.templatePack}。`,
896
- `Digest: ${snapshot.digest}.`,
1304
+ `已整理出一版可确认的需求稿。`,
1305
+ `产品场景: ${formatProductTypeDisplay(snapshot.productType, { fallback: '待确认' })}。`,
1306
+ `场景模板: ${formatTemplatePackDisplay(snapshot.templatePack, { fallback: '待确认' })}。`,
897
1307
  ]);
898
1308
  await appendProgress(ws, [
899
- `已生成 PRD 快照 ${snapshot.versionId}。`,
900
- `已更新当前 PRD、流程、角色和交接文档。`,
1309
+ '已生成新的需求确认稿。',
1310
+ '已同步更新当前需求、流程、角色和交接说明。',
901
1311
  presentationGate.ok
902
1312
  ? `已生成可确认评审面板: ${reviewFiles.canonicalReview}。`
903
1313
  : '评审面板暂未生成:需要先通过 openprd review-presentation 写入展示文案。',
@@ -936,9 +1346,10 @@ async function synthesizeWorkspace(projectRoot, overrides = {}) {
936
1346
  templatePack: snapshot.templatePack,
937
1347
  synthesizedAt: snapshot.createdAt,
938
1348
  }, snapshot);
939
- await writeJson(ws.paths.currentState, currentState);
940
- const nextWs = { ...ws, data: { ...ws.data, currentState } };
1349
+ const storedCurrentState = await persistWorkspaceCurrentState(ws, currentState);
1350
+ const nextWs = { ...ws, data: { ...ws.data, currentState: storedCurrentState } };
941
1351
  await syncSessionBindingFromSnapshot(projectRoot, snapshot, {
1352
+ sessionId: ws.data.currentSessionId ?? null,
942
1353
  reviewStatus: 'pending-confirmation',
943
1354
  reviewPath: reviewFiles.canonicalReview,
944
1355
  activeReviewPath: reviewFiles.activeReviewEntry,
@@ -948,7 +1359,7 @@ async function synthesizeWorkspace(projectRoot, overrides = {}) {
948
1359
  return {
949
1360
  ws: nextWs,
950
1361
  snapshot,
951
- currentState,
1362
+ currentState: storedCurrentState,
952
1363
  indexEntry,
953
1364
  versionIndex: [...versionIndex, indexEntry],
954
1365
  reviewArtifact: reviewFiles.activeReviewEntry,
@@ -1003,8 +1414,16 @@ async function reviewWorkspace(projectRoot, options = {}) {
1003
1414
  if (!(await exists(ws.workspaceRoot))) {
1004
1415
  throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
1005
1416
  }
1006
- const latest = await loadLatestVersionSnapshot(ws);
1007
- if (!latest?.snapshot) {
1417
+ const requestedVersion = normalizeVersionId(options.version);
1418
+ const latest = await loadCurrentLaneSnapshot(ws, {
1419
+ fallbackToLatest: !ws.data.currentSessionId || Boolean(requestedVersion),
1420
+ });
1421
+ const fallbackLatest = latest?.snapshot ? latest : (requestedVersion ? await loadLatestVersionSnapshot(ws) : null);
1422
+ const latestSnapshot = fallbackLatest?.snapshot ?? null;
1423
+ const snapshot = requestedVersion
1424
+ ? await readVersionSnapshot(ws, requestedVersion)
1425
+ : latestSnapshot;
1426
+ if (!snapshot) {
1008
1427
  return {
1009
1428
  ok: false,
1010
1429
  action: 'review',
@@ -1012,12 +1431,7 @@ async function reviewWorkspace(projectRoot, options = {}) {
1012
1431
  errors: ['No synthesized PRD version exists yet. Run openprd synthesize first.'],
1013
1432
  };
1014
1433
  }
1015
-
1016
- const requestedVersion = normalizeVersionId(options.version);
1017
- const snapshot = requestedVersion
1018
- ? await readVersionSnapshot(ws, requestedVersion)
1019
- : latest.snapshot;
1020
- if (!snapshot) {
1434
+ if (requestedVersion && normalizeVersionId(snapshot.versionId) !== requestedVersion) {
1021
1435
  return {
1022
1436
  ok: false,
1023
1437
  action: 'review',
@@ -1041,10 +1455,10 @@ async function reviewWorkspace(projectRoot, options = {}) {
1041
1455
 
1042
1456
  const validationErrors = [];
1043
1457
  if (options.digest && options.digest !== snapshot.digest) {
1044
- validationErrors.push(`Digest mismatch for ${snapshot.versionId}: expected ${snapshot.digest}, got ${options.digest}.`);
1458
+ validationErrors.push(`确认指纹不匹配:当前稿件与传入参数不是同一版,请重新从确认页面复制这次确认命令。`);
1045
1459
  }
1046
1460
  if (requestedWorkUnitId && snapshot.workUnitId !== requestedWorkUnitId) {
1047
- validationErrors.push(`Work unit mismatch for ${snapshot.versionId}: expected ${snapshot.workUnitId ?? 'none'}, got ${requestedWorkUnitId}.`);
1461
+ validationErrors.push('这次确认命令对应的稿件不一致,请重新从当前确认页面复制命令后再执行。');
1048
1462
  }
1049
1463
  if (validationErrors.length > 0) {
1050
1464
  return {
@@ -1058,7 +1472,7 @@ async function reviewWorkspace(projectRoot, options = {}) {
1058
1472
  };
1059
1473
  }
1060
1474
 
1061
- const isLatest = normalizeVersionId(snapshot.versionId) === normalizeVersionId(latest.snapshot.versionId);
1475
+ const isLatest = normalizeVersionId(snapshot.versionId) === normalizeVersionId((latestSnapshot ?? snapshot).versionId);
1062
1476
  const presentationGate = getReviewPresentationGate(snapshot);
1063
1477
  if (!presentationGate.ok) {
1064
1478
  return {
@@ -1106,7 +1520,7 @@ async function reviewWorkspace(projectRoot, options = {}) {
1106
1520
  notes: options.notes ?? null,
1107
1521
  },
1108
1522
  };
1109
- await writeJson(ws.paths.currentState, currentState);
1523
+ await persistWorkspaceCurrentState(ws, currentState);
1110
1524
  }
1111
1525
  workUnit = await writeWorkUnitBinding(ws, {
1112
1526
  snapshot,
@@ -1126,6 +1540,7 @@ async function reviewWorkspace(projectRoot, options = {}) {
1126
1540
  snapshot.workUnitId ? `工作单元: ${snapshot.workUnitId}。` : null,
1127
1541
  ]);
1128
1542
  await syncSessionBindingFromReview(projectRoot, snapshot, {
1543
+ sessionId: ws.data.currentSessionId ?? null,
1129
1544
  reviewStatus: status,
1130
1545
  reviewPath: reviewFiles.canonicalReview,
1131
1546
  activeReviewPath: reviewFiles.activeReviewEntry,
@@ -1174,16 +1589,8 @@ async function clarifyWorkspace(projectRoot, options = {}) {
1174
1589
 
1175
1590
  const versionIndex = await readVersionIndex(ws);
1176
1591
  const currentState = ws.data.currentState ?? {};
1177
- const snapshot = (await loadLatestVersionSnapshot(ws))?.snapshot ?? buildPrdSnapshot(ws, {
1178
- ...currentState,
1179
- versionNumber: currentState.prdVersion ?? (versionIndex.at(-1)?.versionNumber ?? 0),
1180
- versionId: currentState.prdVersion > 0
1181
- ? formatVersionId(currentState.prdVersion)
1182
- : (versionIndex.at(-1)?.versionId ?? 'v0000'),
1183
- productType: resolveCurrentProductType(ws),
1184
- templatePack: resolveActiveTemplatePack(ws),
1185
- status: currentState.status ?? 'draft',
1186
- });
1592
+ const snapshot = (await loadCurrentLaneSnapshot(ws, { fallbackToLatest: !ws.data.currentSessionId }))?.snapshot
1593
+ ?? buildCurrentStateSnapshot(ws, currentState, versionIndex);
1187
1594
 
1188
1595
  const analysis = analyzePrdSnapshot(snapshot);
1189
1596
  const basePlan = buildClarificationPlan(snapshot, analysis);
@@ -1257,16 +1664,8 @@ async function playgroundWorkspace(projectRoot, options = {}) {
1257
1664
 
1258
1665
  const versionIndex = await readVersionIndex(ws);
1259
1666
  const currentState = ws.data.currentState ?? {};
1260
- const snapshot = (await loadLatestVersionSnapshot(ws))?.snapshot ?? buildPrdSnapshot(ws, {
1261
- ...currentState,
1262
- versionNumber: currentState.prdVersion ?? (versionIndex.at(-1)?.versionNumber ?? 0),
1263
- versionId: currentState.prdVersion > 0
1264
- ? formatVersionId(currentState.prdVersion)
1265
- : (versionIndex.at(-1)?.versionId ?? 'v0000'),
1266
- productType: resolveCurrentProductType(ws),
1267
- templatePack: resolveActiveTemplatePack(ws),
1268
- status: currentState.status ?? 'draft',
1269
- });
1667
+ const snapshot = (await loadCurrentLaneSnapshot(ws, { fallbackToLatest: !ws.data.currentSessionId }))?.snapshot
1668
+ ?? buildCurrentStateSnapshot(ws, currentState, versionIndex);
1270
1669
 
1271
1670
  const state = buildPlaygroundState(snapshot);
1272
1671
  const bundle = artifactBundlePaths(ws, `${snapshot.versionId}-playground`);
@@ -1433,18 +1832,18 @@ async function captureWorkspace(projectRoot, options = {}) {
1433
1832
  };
1434
1833
  }
1435
1834
  const staleReview = markReviewStateStaleAfterCapture(currentState, applied, currentState.lastCapturedAt);
1436
- await writeJson(ws.paths.currentState, currentState);
1437
-
1438
- const snapshot = buildPrdSnapshot({ ...ws, data: { ...ws.data, currentState } }, {
1439
- ...currentState,
1440
- versionNumber: currentState.prdVersion ?? 0,
1441
- versionId: currentState.prdVersion > 0 ? formatVersionId(currentState.prdVersion) : 'v0000',
1442
- productType: currentState.productType ?? resolveCurrentProductType(ws),
1443
- templatePack: currentState.templatePack ?? resolveActiveTemplatePack(ws),
1835
+ const storedCurrentState = await persistWorkspaceCurrentState(ws, currentState);
1836
+
1837
+ const snapshot = buildPrdSnapshot({ ...ws, data: { ...ws.data, currentState: storedCurrentState } }, {
1838
+ ...storedCurrentState,
1839
+ versionNumber: storedCurrentState.prdVersion ?? 0,
1840
+ versionId: storedCurrentState.prdVersion > 0 ? formatVersionId(storedCurrentState.prdVersion) : 'v0000',
1841
+ productType: storedCurrentState.productType ?? resolveCurrentProductType(ws),
1842
+ templatePack: storedCurrentState.templatePack ?? resolveActiveTemplatePack(ws),
1444
1843
  });
1445
1844
  const analysis = analyzePrdSnapshot(snapshot);
1446
- const diagramState = await getDiagramReviewState({ ...ws, data: { ...ws.data, currentState } }, snapshot);
1447
- const updatedWs = { ...ws, data: { ...ws.data, currentState } };
1845
+ const diagramState = await getDiagramReviewState({ ...ws, data: { ...ws.data, currentState: storedCurrentState } }, snapshot);
1846
+ const updatedWs = { ...ws, data: { ...ws.data, currentState: storedCurrentState } };
1448
1847
  const scenario = await detectWorkspaceScenario(projectRoot, updatedWs, await readVersionIndex(ws));
1449
1848
  const requirementGate = await readActiveRequirementGate(projectRoot);
1450
1849
  const intakeReflection = await buildRequirementIntakeReflection({
@@ -1462,7 +1861,7 @@ async function captureWorkspace(projectRoot, options = {}) {
1462
1861
  analysis,
1463
1862
  basePlan: buildClarificationPlan(snapshot, analysis),
1464
1863
  scenario,
1465
- captureMeta: currentState.captureMeta,
1864
+ captureMeta: storedCurrentState.captureMeta,
1466
1865
  prdReviewState,
1467
1866
  limit: 8,
1468
1867
  }), requirementGate, intakeReflection);
@@ -1480,7 +1879,7 @@ async function captureWorkspace(projectRoot, options = {}) {
1480
1879
  ]);
1481
1880
 
1482
1881
  return {
1483
- ws: { ...ws, data: { ...ws.data, currentState } },
1882
+ ws: { ...ws, data: { ...ws.data, currentState: storedCurrentState } },
1484
1883
  applied,
1485
1884
  artifactMarkdown: options.artifactMarkdown ?? null,
1486
1885
  field: applied[0]?.field ?? null,
@@ -1496,16 +1895,12 @@ async function computeWorkspaceGuidance(ws, options = {}) {
1496
1895
  const currentState = ws.data.currentState ?? {};
1497
1896
  const currentProductType = resolveCurrentProductType(ws);
1498
1897
  const currentStatus = currentState.status ?? 'unknown';
1499
- const latestVersion = versionIndex.length > 0 ? await loadLatestVersionSnapshot(ws) : null;
1500
- const currentDraftSnapshot = buildPrdSnapshot(ws, {
1898
+ const latestVersion = await loadCurrentLaneSnapshot(ws, { fallbackToLatest: !ws.data.currentSessionId });
1899
+ const currentDraftSnapshot = buildCurrentStateSnapshot(ws, {
1501
1900
  ...currentState,
1502
- versionNumber: currentState.prdVersion ?? (versionIndex.at(-1)?.versionNumber ?? 0),
1503
- versionId: currentState.prdVersion > 0
1504
- ? formatVersionId(currentState.prdVersion)
1505
- : (versionIndex.at(-1)?.versionId ?? 'v0000'),
1506
1901
  productType: currentProductType,
1507
1902
  templatePack: resolveActiveTemplatePack(ws),
1508
- });
1903
+ }, versionIndex);
1509
1904
  const analysisSnapshot = shouldUseCurrentDraftForGuidance(currentState)
1510
1905
  ? currentDraftSnapshot
1511
1906
  : (latestVersion?.snapshot ?? currentDraftSnapshot);
@@ -1546,9 +1941,9 @@ async function computeWorkspaceGuidance(ws, options = {}) {
1546
1941
  suggestedQuestions = clarification.mustAskUser.map((item) => item.prompt);
1547
1942
  } else if (!hasProductType) {
1548
1943
  nextAction = 'classify';
1549
- reason = '产品类型尚未锁定。';
1944
+ reason = '产品场景尚未锁定。';
1550
1945
  suggestedCommand = 'openprd classify . <consumer|b2b|agent>';
1551
- suggestedQuestions = ['这是 consumer、b2b 还是 agent 产品?'];
1946
+ suggestedQuestions = [formatProductTypeQuestion()];
1552
1947
  } else if (analysis.missingRequiredFields > 0) {
1553
1948
  nextAction = 'interview';
1554
1949
  reason = `仍缺少 ${analysis.missingRequiredFields} 个必填字段。`;
@@ -1708,18 +2103,18 @@ async function classifyWorkspace(projectRoot, productType) {
1708
2103
  templatePack: productType,
1709
2104
  classifiedAt: timestamp(),
1710
2105
  };
1711
- await writeJson(ws.paths.currentState, currentState);
2106
+ const storedCurrentState = await persistWorkspaceCurrentState(ws, currentState);
1712
2107
  await appendWorkflowEvent(ws, 'classified', { productType });
1713
2108
  await appendDecision(ws, [
1714
- `已锁定产品类型为 ${productType}。`,
1715
- `模板包已设置为 ${productType}。`,
2109
+ `已锁定产品场景为 ${formatProductTypeDisplay(productType, { fallback: productType })}。`,
2110
+ `场景模板已设置为 ${formatTemplatePackDisplay(productType, { fallback: productType })}。`,
1716
2111
  ]);
1717
2112
  await appendProgress(ws, [
1718
- `已将工作区分类为 ${productType}。`,
2113
+ `已将工作区产品场景锁定为 ${formatProductTypeDisplay(productType, { fallback: productType })}。`,
1719
2114
  ]);
1720
- await writeJson(ws.paths.taskGraph, buildWorkflowTaskGraph(currentState));
2115
+ await writeJson(ws.paths.taskGraph, buildWorkflowTaskGraph(storedCurrentState));
1721
2116
 
1722
- return { ws, currentState };
2117
+ return { ws: { ...ws, data: { ...ws.data, currentState: storedCurrentState } }, currentState: storedCurrentState };
1723
2118
  }
1724
2119
 
1725
2120
  async function interviewWorkspace(projectRoot, requestedType = null) {
@@ -1754,30 +2149,39 @@ ${content}`);
1754
2149
  templatePack: productType ?? resolveActiveTemplatePack(ws),
1755
2150
  interviewStartedAt: timestamp(),
1756
2151
  };
1757
- await writeJson(ws.paths.currentState, currentState);
2152
+ const storedCurrentState = await persistWorkspaceCurrentState(ws, currentState);
1758
2153
  await appendWorkflowEvent(ws, 'interview_started', {
1759
2154
  productType: currentState.productType,
1760
2155
  sourceFiles: sourceFiles.map((filePath) => path.relative(ws.workspaceRoot, filePath)),
1761
2156
  });
1762
2157
  await appendProgress(ws, [
1763
- `已加载 ${productType ?? '未分类'} 的访谈问题。`,
2158
+ `已加载 ${formatProductTypeDisplay(productType, { fallback: '待确认' })} 的访谈问题。`,
1764
2159
  `来源文件: ${sourceFiles.map((filePath) => path.relative(ws.workspaceRoot, filePath)).join(', ')}`,
1765
2160
  ]);
1766
- await appendOpenQuestions(ws, [
1767
- '我们要解决什么问题?',
1768
- '主要用户是谁?',
1769
- '成功是什么样?',
1770
- '哪些内容明确不在范围内?',
1771
- '我们希望 freeze 的第一个里程碑是什么?',
1772
- ]);
1773
- await writeJson(ws.paths.taskGraph, buildWorkflowTaskGraph(currentState));
2161
+ const openQuestions = [
2162
+ '这次主要是给谁用的,他们在什么场景下最卡?',
2163
+ '第一版最值得先让用户完成什么关键动作?',
2164
+ '这轮先不做什么,哪些既有体验、流程或业务结果不能被影响?',
2165
+ ];
2166
+ if (productType === 'consumer') {
2167
+ openQuestions.push('什么结果会让用户愿意继续回来,甚至愿意推荐或付费?');
2168
+ } else if (productType === 'b2b') {
2169
+ openQuestions.push('谁拍板、谁使用、谁推进或运营,这几方最容易卡在哪里?');
2170
+ } else if (productType === 'agent') {
2171
+ openQuestions.push('哪些步骤让 Agent 自主做,哪些节点必须保留人工确认或兜底?');
2172
+ } else {
2173
+ openQuestions.push('如果现在还不想讲太细,先确认最重要的用户价值、边界和风险也可以。');
2174
+ }
2175
+ openQuestions.push('如果涉及账号、数据、外部对接、AI 或成本,最大的业务风险是什么?');
2176
+ await appendOpenQuestions(ws, openQuestions);
2177
+ await writeJson(ws.paths.taskGraph, buildWorkflowTaskGraph(storedCurrentState));
1774
2178
 
1775
2179
  return {
1776
- ws,
2180
+ ws: { ...ws, data: { ...ws.data, currentState: storedCurrentState } },
1777
2181
  productType,
1778
2182
  sourceFiles: sourceFiles.map((filePath) => path.relative(ws.workspaceRoot, filePath)),
1779
2183
  transcript: sourceContent.join('\n\n---\n\n'),
1780
- currentState,
2184
+ currentState: storedCurrentState,
1781
2185
  };
1782
2186
  }
1783
2187