@openprd/cli 0.1.9 → 0.1.11

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/benchmarks/index.md +16 -0
  2. package/.openprd/benchmarks/sources.yaml +53 -1
  3. package/.openprd/config.yaml +10 -0
  4. package/.openprd/design/README.md +47 -0
  5. package/.openprd/design/README_EN.md +25 -0
  6. package/.openprd/design/active/asset-spec.md +15 -0
  7. package/.openprd/design/active/direction-plan.md +7 -0
  8. package/.openprd/design/active/facts-sheet.md +13 -0
  9. package/.openprd/design/active/image-preflight.md +9 -0
  10. package/.openprd/design/active/selected-direction.md +11 -0
  11. package/.openprd/design/anti-slop.md +16 -0
  12. package/.openprd/design/assets/dark-mesh.svg +14 -0
  13. package/.openprd/design/assets/paper-grid.svg +14 -0
  14. package/.openprd/design/assets/surface-presets.md +21 -0
  15. package/.openprd/design/assets/workbench-grid.svg +16 -0
  16. package/.openprd/design/checklists/ui-quality-gate.md +22 -0
  17. package/.openprd/design/components/component-catalog.md +42 -0
  18. package/.openprd/design/layouts/layout-catalog.json +45 -0
  19. package/.openprd/design/lenses/frontend-lenses.md +31 -0
  20. package/.openprd/design/recipes/content-experience.md +26 -0
  21. package/.openprd/design/recipes/operational-tool.md +25 -0
  22. package/.openprd/design/recipes/product-launch.md +26 -0
  23. package/.openprd/design/templates/README.md +36 -0
  24. package/.openprd/design/templates/README_EN.md +31 -0
  25. package/.openprd/design/templates/content-home.html +415 -0
  26. package/.openprd/design/templates/ops-dashboard.html +340 -0
  27. package/.openprd/design/templates/product-launch.html +360 -0
  28. package/.openprd/design/themes/theme-catalog.json +77 -0
  29. package/.openprd/engagements/active/prd.md +111 -100
  30. package/.openprd/schema/prd.schema.yaml +25 -0
  31. package/.openprd/templates/base/intake.md +17 -0
  32. package/.openprd/templates/base/prd.md +31 -6
  33. package/AGENTS.md +10 -7
  34. package/README.md +26 -28
  35. package/README_EN.md +3 -3
  36. package/package.json +41 -2
  37. package/scripts/openprd-codex-isolated-worker.mjs +799 -0
  38. package/skills/openprd-benchmark-router/SKILL.md +5 -0
  39. package/skills/openprd-frontend-design/SKILL.md +161 -0
  40. package/skills/openprd-frontend-design/references/design-asset-contract.md +34 -0
  41. package/skills/openprd-frontend-design/references/direction-engine.md +46 -0
  42. package/skills/openprd-harness/SKILL.md +37 -20
  43. package/skills/openprd-learning-review/SKILL.md +2 -1
  44. package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +1 -1
  45. package/skills/openprd-quality/SKILL.md +5 -3
  46. package/skills/openprd-requirement-intake/SKILL.md +27 -5
  47. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -3
  48. package/skills/openprd-requirement-intake/references/routing-rubric.md +9 -5
  49. package/skills/openprd-requirement-intake/references/startup-validation-lens.md +108 -0
  50. package/skills/openprd-router/SKILL.md +13 -5
  51. package/skills/openprd-shared/SKILL.md +38 -25
  52. package/src/agent-integration.js +120 -69
  53. package/src/brainstorm-artifacts.js +1805 -0
  54. package/src/brainstorm-presentation.js +233 -0
  55. package/src/brainstorm.js +766 -0
  56. package/src/cli/args.js +51 -3
  57. package/src/cli/doctor-print.js +34 -8
  58. package/src/cli/print.js +6 -0
  59. package/src/cli/quality-print.js +79 -3
  60. package/src/cli/run-print.js +43 -1
  61. package/src/cli/shared-print.js +33 -2
  62. package/src/cli/workflow-print.js +21 -0
  63. package/src/codex-hook-runner-template.mjs +838 -58
  64. package/src/design-starter-support.js +462 -0
  65. package/src/design-starter.js +1207 -0
  66. package/src/diagram-core.js +384 -151
  67. package/src/html-artifacts.js +403 -50
  68. package/src/knowledge.js +291 -32
  69. package/src/learning-review.js +3 -3
  70. package/src/loop.js +839 -101
  71. package/src/openprd.js +86 -1
  72. package/src/openspec/change-validate.js +1 -0
  73. package/src/prd-core.js +119 -2
  74. package/src/quality-html-artifact.js +30 -5
  75. package/src/quality-learning.js +50 -6
  76. package/src/quality.js +22 -12
  77. package/src/run-harness.js +191 -29
  78. package/src/self-update.js +301 -4
  79. package/src/standards.js +46 -5
  80. package/src/visual-prepare.js +940 -0
  81. package/src/workspace-core.js +175 -0
  82. package/src/workspace-workflow.js +307 -7
  83. package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +0 -14
  84. package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +0 -14
  85. package/.openprd/discovery/config.json +0 -35
  86. package/.openprd/engagements/active/flows.md +0 -35
  87. package/.openprd/engagements/active/handoff.md +0 -16
  88. package/.openprd/engagements/active/review.html +0 -61
  89. package/.openprd/engagements/active/roles.md +0 -22
  90. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +0 -23
  91. package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +0 -18
  92. package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +0 -18
  93. package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +0 -18
  94. package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +0 -18
  95. package/.openprd/exports/.gitkeep +0 -0
  96. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +0 -78
  97. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +0 -129
  98. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +0 -41
  99. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +0 -14
  100. package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +0 -49
  101. package/.openprd/knowledge/index.json +0 -47
  102. package/.openprd/reviews/v0001.html +0 -1322
  103. package/.openprd/reviews/v0002.html +0 -1150
  104. package/.openprd/reviews/v0003.html +0 -1150
  105. package/.openprd/reviews/v0004.html +0 -1150
  106. package/.openprd/reviews/v0005.html +0 -1150
  107. package/.openprd/sessions/.gitkeep +0 -0
  108. package/.openprd/state/changes.json +0 -27
  109. package/.openprd/state/current.json +0 -505
  110. package/.openprd/state/release-ledger.json +0 -387
  111. package/.openprd/state/version-index.json +0 -67
  112. package/.openprd/state/versions/.gitkeep +0 -0
  113. package/.openprd/state/versions/v0001.json +0 -121
  114. package/.openprd/state/versions/v0001.md +0 -161
  115. package/.openprd/state/versions/v0002.json +0 -264
  116. package/.openprd/state/versions/v0002.md +0 -183
  117. package/.openprd/state/versions/v0003.json +0 -269
  118. package/.openprd/state/versions/v0003.md +0 -188
  119. package/.openprd/state/versions/v0004.json +0 -274
  120. package/.openprd/state/versions/v0004.md +0 -193
  121. package/.openprd/state/versions/v0005.json +0 -299
  122. package/.openprd/state/versions/v0005.md +0 -189
  123. package/docs/assets/openprd-capability-overview-en.png +0 -0
  124. package/docs/assets/openprd-capability-overview-zh.png +0 -0
  125. package/docs/assets/openprd-learning-html.png +0 -0
  126. package/docs/assets/openprd-quality-html.png +0 -0
  127. package/docs/assets/openprd-requirement-routing-en.png +0 -0
  128. package/docs/assets/openprd-requirement-routing-en.svg +0 -102
  129. package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
  130. package/docs/assets/openprd-requirement-routing-zh.png +0 -0
  131. package/docs/assets/openprd-requirement-routing-zh.svg +0 -102
  132. package/docs/assets/openprd-review-html.png +0 -0
  133. package/docs/assets/openprd-scenario-overview.png +0 -0
  134. package/docs/assets/openprd-scenario-overview.svg +0 -114
  135. package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
  136. package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
  137. package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
  138. package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
@@ -3,15 +3,25 @@ import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import crypto from 'node:crypto';
5
5
  import { spawnSync } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
6
7
 
7
8
  const eventName = process.argv[2] || 'Unknown';
9
+ const hookScriptFilePath = fileURLToPath(import.meta.url);
10
+ const hookScriptWorkspaceRoot = (() => {
11
+ const candidate = path.resolve(path.dirname(hookScriptFilePath), '..', '..');
12
+ return fs.existsSync(path.join(candidate, '.openprd')) ? candidate : null;
13
+ })();
8
14
  let input = '';
9
15
  process.stdin.setEncoding('utf8');
10
16
  process.stdin.on('data', (chunk) => { input += chunk; });
11
17
  process.stdin.on('end', () => {
12
18
  let payload = {};
13
19
  try { payload = input.trim() ? JSON.parse(input) : {}; } catch {}
14
- const cwd = payload.cwd || process.cwd();
20
+ const cwd = payload.cwd
21
+ || payload.workspace_root
22
+ || payload.workspaceRoot
23
+ || hookScriptWorkspaceRoot
24
+ || process.cwd();
15
25
  const result = handle(eventName, cwd, payload);
16
26
  if (result) {
17
27
  process.stdout.write(JSON.stringify(result));
@@ -98,6 +108,7 @@ function defaultTurnState() {
98
108
  return {
99
109
  version: 1,
100
110
  id: null,
111
+ knowledgeCandidateId: null,
101
112
  sessionId: null,
102
113
  prompt: null,
103
114
  promptPreview: null,
@@ -296,6 +307,9 @@ function commandText(payload) {
296
307
  if (typeof toolInput === 'string') {
297
308
  return toolInput;
298
309
  }
310
+ if (toolInput && typeof toolInput.command === 'string') {
311
+ return toolInput.command;
312
+ }
299
313
  if (toolInput && typeof toolInput.cmd === 'string') {
300
314
  return toolInput.cmd;
301
315
  }
@@ -593,6 +607,270 @@ function readTextSync(filePath, maxLength = 20000) {
593
607
  }
594
608
  }
595
609
 
610
+ function readJsonlTailSync(filePath, maxLines = 160, maxBytes = 262144) {
611
+ try {
612
+ const stat = fs.statSync(filePath);
613
+ const size = Number(stat.size || 0);
614
+ const start = Math.max(0, size - maxBytes);
615
+ const length = Math.max(0, size - start);
616
+ const fd = fs.openSync(filePath, 'r');
617
+ const buffer = Buffer.alloc(length);
618
+ fs.readSync(fd, buffer, 0, length, start);
619
+ fs.closeSync(fd);
620
+ let lines = buffer.toString('utf8').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
621
+ if (start > 0 && lines.length > 0) {
622
+ lines = lines.slice(1);
623
+ }
624
+ return lines.slice(-maxLines).map((line) => {
625
+ try {
626
+ return JSON.parse(line);
627
+ } catch {
628
+ return null;
629
+ }
630
+ }).filter(Boolean);
631
+ } catch {
632
+ return [];
633
+ }
634
+ }
635
+
636
+ function stateEventsPath(root) {
637
+ return path.join(root, '.openprd', 'state', 'events.jsonl');
638
+ }
639
+
640
+ function latestDesignStarterRecord(root) {
641
+ const entries = readJsonlTailSync(stateEventsPath(root), 80, 131072);
642
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
643
+ const entry = entries[index];
644
+ if (entry?.type !== 'design_starter_created' || typeof entry.output !== 'string') {
645
+ continue;
646
+ }
647
+ const output = normalizeProjectFile(root, entry.output) || entry.output.trim();
648
+ if (!output) {
649
+ continue;
650
+ }
651
+ return {
652
+ ...entry,
653
+ output,
654
+ };
655
+ }
656
+ return null;
657
+ }
658
+
659
+ const PATCH_MODE_ACTIVE_CONTRACT_FILES = [
660
+ '.openprd/design/active/facts-sheet.md',
661
+ '.openprd/design/active/asset-spec.md',
662
+ '.openprd/design/active/image-preflight.md',
663
+ '.openprd/design/active/direction-plan.md',
664
+ '.openprd/design/active/selected-direction.md',
665
+ ];
666
+
667
+ const PATCH_MODE_HANDOFF_FOCUS_ALLOWANCE = 2;
668
+
669
+ function patchModeDraftCandidates(relativeTarget) {
670
+ const normalized = String(relativeTarget || '').replace(/\\/g, '/').trim();
671
+ if (!normalized) {
672
+ return [];
673
+ }
674
+ const parsed = path.posix.parse(normalized);
675
+ const dirPrefix = parsed.dir ? `${parsed.dir}/` : '';
676
+ const ext = parsed.ext || '';
677
+ const name = parsed.name || normalized;
678
+ return [
679
+ `${dirPrefix}${name}.next${ext}`,
680
+ `${dirPrefix}${name}.rewrite${ext}`,
681
+ ];
682
+ }
683
+
684
+ function safeProjectMtimeMs(root, relativePath) {
685
+ const normalized = normalizeProjectFile(root, relativePath);
686
+ if (!normalized) {
687
+ return null;
688
+ }
689
+ try {
690
+ return fs.statSync(path.join(root, normalized)).mtimeMs;
691
+ } catch {
692
+ return null;
693
+ }
694
+ }
695
+
696
+ function patchModeTargetFiles(gate) {
697
+ return [...new Set([
698
+ gate?.targetFile,
699
+ ...(Array.isArray(gate?.draftFiles) ? gate.draftFiles : []),
700
+ ].filter(Boolean).map((file) => String(file).replace(/\\/g, '/')))];
701
+ }
702
+
703
+ function patchModeFocusFiles(root, gate) {
704
+ return [...new Set([
705
+ gate?.targetFile,
706
+ ...PATCH_MODE_ACTIVE_CONTRACT_FILES.map((file) => normalizeProjectFile(root, file) || file),
707
+ ].filter(Boolean).map((file) => String(file).replace(/\\/g, '/')))];
708
+ }
709
+
710
+ function isPatchModeWriteObserved(root, gate) {
711
+ const threshold = Number(gate?.armedAtMs ?? 0);
712
+ if (!threshold) {
713
+ return false;
714
+ }
715
+ for (const file of patchModeTargetFiles(gate)) {
716
+ const mtimeMs = safeProjectMtimeMs(root, file);
717
+ if (mtimeMs && mtimeMs > threshold) {
718
+ return true;
719
+ }
720
+ }
721
+ return false;
722
+ }
723
+
724
+ function transcriptPathFor(root, payload) {
725
+ const raw = String(payload?.transcript_path || payload?.transcriptPath || '').trim();
726
+ if (!raw) {
727
+ return null;
728
+ }
729
+ const resolved = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(root, raw);
730
+ return fs.existsSync(resolved) ? resolved : null;
731
+ }
732
+
733
+ function assistantMessageTextFromTranscriptEntry(entry) {
734
+ if (!entry || typeof entry !== 'object') {
735
+ return '';
736
+ }
737
+ if (entry.type === 'event_msg' && entry.payload?.type === 'agent_message') {
738
+ return String(entry.payload.message || '');
739
+ }
740
+ if (entry.type !== 'response_item') {
741
+ return '';
742
+ }
743
+ const payload = entry.payload || {};
744
+ if (payload.type !== 'message' || payload.role !== 'assistant') {
745
+ return '';
746
+ }
747
+ const content = Array.isArray(payload.content) ? payload.content : [];
748
+ return content
749
+ .filter((item) => item?.type === 'output_text' && typeof item.text === 'string')
750
+ .map((item) => item.text)
751
+ .join('\n');
752
+ }
753
+
754
+ function hashText(value) {
755
+ return crypto.createHash('sha256').update(String(value || '')).digest('hex').slice(0, 16);
756
+ }
757
+
758
+ function hashProjectFile(root, relativePath) {
759
+ const normalized = normalizeProjectFile(root, relativePath);
760
+ if (!normalized) {
761
+ return null;
762
+ }
763
+ try {
764
+ const buffer = fs.readFileSync(path.join(root, normalized));
765
+ return crypto.createHash('sha256').update(buffer).digest('hex').slice(0, 16);
766
+ } catch {
767
+ return null;
768
+ }
769
+ }
770
+
771
+ function patchModeFileHashSnapshot(root, files) {
772
+ const snapshot = {};
773
+ for (const file of files) {
774
+ const normalized = normalizeProjectFile(root, file);
775
+ if (!normalized) {
776
+ continue;
777
+ }
778
+ snapshot[normalized] = hashProjectFile(root, normalized);
779
+ }
780
+ return snapshot;
781
+ }
782
+
783
+ function latestAssistantTranscriptMessage(root, payload) {
784
+ const transcriptPath = transcriptPathFor(root, payload);
785
+ if (!transcriptPath) {
786
+ return null;
787
+ }
788
+ const entries = readJsonlTailSync(transcriptPath, 240, 393216);
789
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
790
+ const text = assistantMessageTextFromTranscriptEntry(entries[index]);
791
+ if (!text) {
792
+ continue;
793
+ }
794
+ const normalized = stripMarkdown(text).replace(/\s+/g, ' ').trim();
795
+ if (!normalized) {
796
+ continue;
797
+ }
798
+ return {
799
+ text,
800
+ normalized,
801
+ preview: preview(normalized, 500),
802
+ hash: hashText(normalized),
803
+ transcriptPath,
804
+ };
805
+ }
806
+ return null;
807
+ }
808
+
809
+ function looksLikePatchModeDeclaration(text) {
810
+ const normalized = stripMarkdown(text).replace(/\s+/g, ' ').trim();
811
+ if (!normalized) {
812
+ return false;
813
+ }
814
+ if (/(不要|先别|暂时别).{0,10}(覆盖|重写|改写)/.test(normalized)) {
815
+ return false;
816
+ }
817
+ return [
818
+ /(?:开始|现在开始|准备开始|进入).{0,12}(?:整页)?(?:覆盖|重写|改写).{0,20}(?:入口文件|index\.html|首页|页面)/i,
819
+ /(?:开始|现在开始|准备开始).{0,12}(?:对|把)?\s*index\.html.{0,12}(?:覆盖|重写|改写)/i,
820
+ /(?:start|starting|begin|beginning|now).{0,24}(?:overwrite|rewrite|replace).{0,24}(?:index\.html|entry file|home page|landing page|page)/i,
821
+ /(?:overwrite|rewrite|replace).{0,24}(?:index\.html|entry file|home page|landing page).{0,24}(?:now|next)/i,
822
+ ].some((pattern) => pattern.test(normalized));
823
+ }
824
+
825
+ function commandLooksLikeFileWrite(command) {
826
+ const text = String(command || '');
827
+ if (!text) {
828
+ return false;
829
+ }
830
+ return /(?:^|[\s(;])(cat\s*<<|tee\b|cp\b|mv\b|install\b|perl\b|python\b|node\b|ruby\b|sed\s+-i\b|awk\b)|>>?/.test(text);
831
+ }
832
+
833
+ function isPatchModeWriteAttempt(root, payload, gate) {
834
+ const targetFiles = new Set(patchModeTargetFiles(gate));
835
+ if (targetFiles.size === 0) {
836
+ return false;
837
+ }
838
+ const name = toolName(payload) || (commandText(payload) ? 'Bash' : '');
839
+ const touchedFiles = extractTouchedFiles(root, payload);
840
+ if (['Write', 'Edit', 'MultiEdit', 'apply_patch'].includes(name) && touchedFiles.some((file) => targetFiles.has(file))) {
841
+ return true;
842
+ }
843
+ const command = commandText(payload).replace(/\\/g, '/');
844
+ if (!['Bash', 'LS', 'Glob', 'Grep'].includes(name) || !command || !commandLooksLikeFileWrite(command)) {
845
+ return false;
846
+ }
847
+ return [...targetFiles].some((file) => command.includes(file));
848
+ }
849
+
850
+ function isPatchModeFocusAttempt(root, payload, gate) {
851
+ const remaining = Number(gate?.focusAllowanceRemaining ?? 0);
852
+ if (remaining <= 0) {
853
+ return false;
854
+ }
855
+ const focusFiles = new Set(patchModeFocusFiles(root, gate));
856
+ if (focusFiles.size === 0) {
857
+ return false;
858
+ }
859
+ const name = toolName(payload) || (commandText(payload) ? 'Bash' : '');
860
+ const touchedFiles = extractTouchedFiles(root, payload);
861
+ if (name === 'Read') {
862
+ return touchedFiles.length > 0 && touchedFiles.every((file) => focusFiles.has(file));
863
+ }
864
+ if (!['Bash', 'Glob', 'Grep', 'LS'].includes(name)) {
865
+ return false;
866
+ }
867
+ const command = commandText(payload).replace(/\\/g, '/');
868
+ if (!command || commandLooksLikeFileWrite(command)) {
869
+ return false;
870
+ }
871
+ return [...focusFiles].some((file) => command.includes(file));
872
+ }
873
+
596
874
  function findChangeDir(root, changeId) {
597
875
  const candidates = [
598
876
  path.join(root, 'openprd', 'changes', changeId),
@@ -786,14 +1064,35 @@ function knowledgeSkillContextLines(knowledgeSkills) {
786
1064
  if (matched.length === 0) {
787
1065
  return [];
788
1066
  }
1067
+ const mandatoryCheck = knowledgeSkills?.mandatoryCheck ?? null;
789
1068
  const lines = [
790
- `项目级 Skill: 自动命中 ${matched.length} 个,并已加入当前上下文`,
1069
+ mandatoryCheck?.required
1070
+ ? `项目级经验候选: 找到 ${matched.length} 条,并已加入当前上下文供判断`
1071
+ : `项目级 Skill: 自动命中 ${matched.length} 个,并已加入当前上下文`,
791
1072
  ];
1073
+ if (mandatoryCheck?.required) {
1074
+ lines.push(`${mandatoryCheck.title}: ${mandatoryCheck.summary}`);
1075
+ for (const instruction of (mandatoryCheck.instructions ?? []).slice(0, 4)) {
1076
+ lines.push(`- ${instruction}`);
1077
+ }
1078
+ if (Array.isArray(mandatoryCheck.focusSignals) && mandatoryCheck.focusSignals.length > 0) {
1079
+ lines.push(`当前判断线索: ${mandatoryCheck.focusSignals.join(';')}`);
1080
+ }
1081
+ }
792
1082
  for (const skill of matched.slice(0, 3)) {
793
1083
  lines.push(`- ${skill.skillName}: ${skill.matchSummary || '命中当前上下文'}`);
794
- if (skill.description) {
1084
+ const useWhen = String(skill.useWhen || skill.description || '').trim().replace(/^use when\b[::]?\s*/i, '').trim();
1085
+ if (useWhen) {
1086
+ lines.push(` 适用时机: ${useWhen}`);
1087
+ } else if (skill.description) {
795
1088
  lines.push(` 说明: ${skill.description}`);
796
1089
  }
1090
+ if (Array.isArray(skill.reviewFirst) && skill.reviewFirst.length > 0) {
1091
+ lines.push(` 先看: ${skill.reviewFirst.slice(0, 3).join(';')}`);
1092
+ }
1093
+ if (Array.isArray(skill.antiPatterns) && skill.antiPatterns.length > 0) {
1094
+ lines.push(` 不要直接套用: ${skill.antiPatterns.slice(0, 2).join(';')}`);
1095
+ }
797
1096
  if (Array.isArray(skill.touchedFiles) && skill.touchedFiles.length > 0) {
798
1097
  lines.push(` 相关文件: ${skill.touchedFiles.slice(0, 4).join(';')}`);
799
1098
  }
@@ -834,8 +1133,14 @@ function renderRunContextText(result) {
834
1133
  lines.push(...knowledgeSkillContextLines(result.knowledgeSkills));
835
1134
  lines.push('对外表达: 面向用户时,请优先说“本次调整”“后续任务”“继续落地”“完成后检查”这类人话,不要直接复述内部编号、命令、路径、版本号或流程术语。');
836
1135
  const recommendation = result.recommendation || {};
1136
+ const clarifyReplyRequired = (recommendation.nextAction ?? '') === 'clarify-user'
1137
+ || recommendation.title === 'clarify-user'
1138
+ || recommendation.title === '继续本轮需求入口澄清';
837
1139
  lines.push('建议下一步: ' + recommendation.title);
838
1140
  lines.push('这样安排的原因: ' + recommendation.reason);
1141
+ if (clarifyReplyRequired) {
1142
+ lines.push('当前回复目标: 先在对话里输出 requirement 摘要或只追问 1 个最高价值澄清点;不要承诺“按默认方案直接实现”,也不要把“请帮我实现/继续实现”当成跳过 requirement 摘要确认的依据。');
1143
+ }
839
1144
  if (recommendation.preparationCommand || recommendation.executionCommand || recommendation.commitCommand) {
840
1145
  lines.push('开始动手前提: 只有在用户明确要求继续落地、实现、修复、深挖或提交时,才继续往下做;如果还缺这一步,就先用人话说明范围和影响。');
841
1146
  }
@@ -873,7 +1178,7 @@ function renderRunContextText(result) {
873
1178
  if (recommendation.commitCommand) {
874
1179
  lines.push('内部提交参考: ' + recommendation.commitCommand);
875
1180
  }
876
- if (recommendation.loop?.worktreeRecommended) {
1181
+ if (recommendation.loop?.worktreeRecommended || recommendation.isolation?.worktreeRecommended) {
877
1182
  lines.push('环境建议: 最好放到单独环境里继续,避免和别的事项串线。');
878
1183
  }
879
1184
  lines.push('内部检查参考: ' + recommendation.verifyCommand);
@@ -1119,9 +1424,7 @@ function analyzePromptIntent(prompt) {
1119
1424
  const visualMockupRequest = imageGenerationTerms.test(text)
1120
1425
  && imageGenerationAction.test(text)
1121
1426
  && !codeVisualArtifactRequested;
1122
- const largeUiChangeRequest = /(界面|页面|视觉|样式|UI|前端体验|布局|信息架构|主视觉|效果图|视觉稿|mockup|设计方向|设计预览)/i.test(text)
1123
- && /(大|较大|比较大|明显|重做|重构|改版|优化|重新设计|设计方向|三种|3种|方案|效果图|先看样子|确认方向|体验优化|产品内)/i.test(text);
1124
- const visualReview = /效果图|实现截图|视觉对比|视觉评审|对标效果图|复刻/i.test(text);
1427
+ const visualReview = /效果图|实现截图|视觉对比|视觉评审|对标效果图|复刻|不一致|好丑|没对齐|对不上|不像/i.test(text);
1125
1428
  const directBugfixExecution = explicitExecution && bugfixOrDiagnostic;
1126
1429
  const newFeatureVerbMatched = /(新增|增加|新建)/.test(text);
1127
1430
  const capabilityCreationMatched = capabilityCreationPatterns.some((pattern) => pattern.test(text));
@@ -1186,7 +1489,7 @@ function analyzePromptIntent(prompt) {
1186
1489
  ? 'l0'
1187
1490
  : (!visualMockupRequest
1188
1491
  && !readOnly
1189
- && (largeUiChangeRequest || l1OptimizationMatched || requirementSignalMatched)
1492
+ && (l1OptimizationMatched || requirementSignalMatched)
1190
1493
  ? 'l1'
1191
1494
  : null)))
1192
1495
  : null;
@@ -1206,7 +1509,6 @@ function analyzePromptIntent(prompt) {
1206
1509
  readOnly,
1207
1510
  simpleConcrete,
1208
1511
  visualMockupRequest,
1209
- largeUiChangeRequest,
1210
1512
  continuationRequest,
1211
1513
  continuationSessionId,
1212
1514
  continuationTaskHandle,
@@ -1224,7 +1526,6 @@ function analyzePromptIntent(prompt) {
1224
1526
  || confirmation
1225
1527
  || readOnly
1226
1528
  || visualMockupRequest
1227
- || largeUiChangeRequest
1228
1529
  || continuationRequest
1229
1530
  || visualReview
1230
1531
  || publicRepoResearchRequest
@@ -1244,6 +1545,20 @@ function isShortAffirmativeConfirmation(prompt) {
1244
1545
  return /^(可以|好|行|确认|没问题|OK|ok|yes|Yes|yep|Yep)[。!!,.,s]*$/.test(text);
1245
1546
  }
1246
1547
 
1548
+ function looksLikeClarificationFollowupReply(prompt, intent = null) {
1549
+ const text = stripMarkdown(prompt).trim();
1550
+ if (!text || text.length > 48 || /[\n\r]/.test(text) || /[??]/.test(text)) {
1551
+ return false;
1552
+ }
1553
+ if (intent?.requiresIntake || intent?.readOnly || intent?.reviewDecision || intent?.publicRepoResearchRequest || intent?.externalTechResearchRequest || intent?.skillWorkflowEditRequest || intent?.continuationRequest) {
1554
+ return false;
1555
+ }
1556
+ if (/[::]/.test(text) && text.length > 24) {
1557
+ return false;
1558
+ }
1559
+ return true;
1560
+ }
1561
+
1247
1562
  function detectRequirementIntakeMode(prompt) {
1248
1563
  const text = String(prompt || '');
1249
1564
  const deep = text.length >= 80
@@ -1459,6 +1774,93 @@ function closeWeappGate(root, sessionId = null, patch = {}) {
1459
1774
  }, sessionId);
1460
1775
  }
1461
1776
 
1777
+ function openPatchModeGate(root, patch, sessionId = null) {
1778
+ const current = readNamedGate(root, 'patch-mode', sessionId);
1779
+ return writeNamedGate(root, 'patch-mode', {
1780
+ version: 1,
1781
+ active: true,
1782
+ status: 'awaiting-entry-write',
1783
+ openedAt: current?.openedAt || now(),
1784
+ updatedAt: now(),
1785
+ ...current,
1786
+ ...patch,
1787
+ }, sessionId);
1788
+ }
1789
+
1790
+ function closePatchModeGate(root, sessionId = null, patch = {}) {
1791
+ const current = readNamedGate(root, 'patch-mode', sessionId);
1792
+ if (!current) {
1793
+ return null;
1794
+ }
1795
+ return writeNamedGate(root, 'patch-mode', {
1796
+ ...current,
1797
+ active: false,
1798
+ status: patch.status || 'closed',
1799
+ closedAt: patch.closedAt || now(),
1800
+ updatedAt: now(),
1801
+ ...patch,
1802
+ }, sessionId);
1803
+ }
1804
+
1805
+ function syncPatchModeGate(root, payload, sessionId = null, intent = null) {
1806
+ let gate = readNamedGate(root, 'patch-mode', sessionId);
1807
+ if (gate?.active && isPatchModeWriteObserved(root, gate)) {
1808
+ gate = closePatchModeGate(root, sessionId, {
1809
+ status: 'write-observed',
1810
+ writeObservedAt: now(),
1811
+ });
1812
+ }
1813
+ const designStarter = latestDesignStarterRecord(root);
1814
+ if (!designStarter && !isFrontendTaskIntent(intent)) {
1815
+ return gate;
1816
+ }
1817
+ const targetFile = normalizeProjectFile(root, designStarter?.output || 'index.html') || 'index.html';
1818
+ const draftFiles = patchModeDraftCandidates(targetFile);
1819
+ const fileHashesAtArm = patchModeFileHashSnapshot(root, [targetFile, ...draftFiles]);
1820
+ const assistantMessage = latestAssistantTranscriptMessage(root, payload);
1821
+ if (assistantMessage && looksLikePatchModeDeclaration(assistantMessage.normalized)) {
1822
+ if (gate?.messageHash === assistantMessage.hash && gate?.targetFile === targetFile && gate?.phase === 'strict') {
1823
+ return gate;
1824
+ }
1825
+ return openPatchModeGate(root, {
1826
+ phase: 'strict',
1827
+ status: 'awaiting-entry-write',
1828
+ targetFile,
1829
+ targetHashAtArm: hashProjectFile(root, targetFile),
1830
+ fileHashesAtArm,
1831
+ draftFiles,
1832
+ focusAllowanceRemaining: 0,
1833
+ messageHash: assistantMessage.hash,
1834
+ messagePreview: assistantMessage.preview,
1835
+ transcriptPath: assistantMessage.transcriptPath,
1836
+ designStarterOutput: targetFile,
1837
+ armedAtMs: Date.now(),
1838
+ armedAt: now(),
1839
+ }, sessionId);
1840
+ }
1841
+ if (!designStarter) {
1842
+ return gate;
1843
+ }
1844
+ if (gate?.active && gate?.targetFile === targetFile) {
1845
+ return gate;
1846
+ }
1847
+ return openPatchModeGate(root, {
1848
+ phase: 'handoff',
1849
+ status: 'awaiting-patch-handoff',
1850
+ targetFile,
1851
+ targetHashAtArm: hashProjectFile(root, targetFile),
1852
+ fileHashesAtArm,
1853
+ draftFiles,
1854
+ focusAllowanceRemaining: PATCH_MODE_HANDOFF_FOCUS_ALLOWANCE,
1855
+ messageHash: assistantMessage?.hash || null,
1856
+ messagePreview: assistantMessage?.preview || null,
1857
+ transcriptPath: assistantMessage?.transcriptPath || null,
1858
+ designStarterOutput: targetFile,
1859
+ armedAtMs: Date.now(),
1860
+ armedAt: now(),
1861
+ }, sessionId);
1862
+ }
1863
+
1462
1864
  function evaluateRequirementGateProgress(root, sessionId = null) {
1463
1865
  const gate = readRequirementGate(root, sessionId);
1464
1866
  const binding = readSessionBinding(root, sessionId);
@@ -1477,17 +1879,45 @@ function evaluateRequirementGateProgress(root, sessionId = null) {
1477
1879
  const hasTaskBreakdown = Boolean(activeChange && Number(taskSummary?.total ?? 0) > 0);
1478
1880
  const approvalPolicy = requirementApprovalPolicy(gate);
1479
1881
  const clarificationConfirmed = requirementWritePathExplicitlyAuthorized(gate);
1882
+ const recommendationNextAction = run.parsed?.recommendation?.nextAction
1883
+ ?? run.parsed?.recommendation?.title
1884
+ ?? '';
1885
+ const missingRequiredFields = Number(run.parsed?.analysis?.missingRequiredFields ?? 0);
1886
+ const lacksProductType = !run.parsed?.analysis?.productType;
1887
+ const clarificationStillNeedsWriteback = clarificationConfirmed
1888
+ && recommendationNextAction === 'clarify-user'
1889
+ && (lacksProductType || missingRequiredFields > 0);
1890
+ const classificationStillNeeded = clarificationConfirmed && recommendationNextAction === 'classify';
1891
+ const interviewStillNeeded = clarificationConfirmed && recommendationNextAction === 'interview';
1480
1892
  let nextStep = 'implementation-ready';
1481
1893
  let reason = '这版需求和后续任务都已经准备好了;如果用户原本就明确要继续做,就直接往下推进,否则再补一句清楚的人话授权。';
1482
1894
  if (!review.versionId) {
1483
- nextStep = clarificationConfirmed ? 'prd-synthesis-required' : 'clarification-confirmation-required';
1895
+ nextStep = clarificationConfirmed
1896
+ ? (
1897
+ clarificationStillNeedsWriteback
1898
+ ? 'clarification-writeback-required'
1899
+ : classificationStillNeeded
1900
+ ? 'product-type-classification-required'
1901
+ : interviewStillNeeded
1902
+ ? 'requirement-interview-required'
1903
+ : 'prd-synthesis-required'
1904
+ )
1905
+ : 'clarification-confirmation-required';
1484
1906
  reason = clarificationConfirmed
1485
1907
  ? (
1486
- reviewPolicyAllowsSilentRecord(approvalPolicy)
1487
- ? '用户已明确表示不需要再停下来确认,本轮可以直接整理需求事实、生成这版确认稿,并继续后面的整理步骤。'
1488
- : '当前需求摘要已经确认,下一步把已确认内容整理成可确认的需求稿。'
1908
+ clarificationStillNeedsWriteback
1909
+ ? '用户已经确认当前需求摘要或默认方案,但 current.json 里还没有把这轮确认内容按 canonical 字段稳定写回。不要重新回到泛化 clarify,也不要继续写 agent-inferred / project-derived 事实;请直接用 openprd capture 按 canonical 字段路径并以 user-confirmed 写回这轮已确认内容。'
1910
+ : classificationStillNeeded
1911
+ ? '用户已经确认当前需求摘要,但当前 lane 还没有锁定产品场景。先完成 openprd classify .,让产品场景和后续写回事实一致,再继续 synthesize。'
1912
+ : interviewStillNeeded
1913
+ ? '用户已经确认当前需求摘要,但当前 lane 仍缺少必填 requirement 事实。先继续用 openprd capture 按 canonical 字段补齐当前范围、目标、约束和交接信息;如还缺问题,再用 openprd interview . 收敛剩余缺口,然后继续 synthesize。'
1914
+ : (
1915
+ reviewPolicyAllowsSilentRecord(approvalPolicy)
1916
+ ? '用户已明确表示不需要再停下来确认,本轮可以直接整理需求事实、生成这版确认稿,并继续后面的整理步骤。'
1917
+ : '当前需求摘要已经确认,下一步把已确认内容整理成可确认的需求稿。'
1918
+ )
1489
1919
  )
1490
- : '当前还缺需求摘要确认。先在对话里按“需求判断 / 需求理解 / 功能范围 / 技术方案”整理结构化摘要,其中“功能范围”和“技术方案”优先用 Markdown 表格;用户没确认前,不要直接把核心需求写成既定事实。';
1920
+ : '当前还缺需求摘要确认。先在对话里按“需求判断 / 需求理解 / 功能范围 / 技术方案”整理结构化摘要,其中“功能范围”和“技术方案”优先用 Markdown 表格;`需求判断` 和 `需求理解` 先用 1 到 2 句轻量主句说清这次是什么、核心问题和第一版目标,再把边界、风险和技术细项下沉到后续分项或表格。若当前还在判断值不值得做,还要主动补上验证与创业闭环:第一批最容易触达的社区或种子用户、你为什么算这个社区里的自己人、当前替代方案和痛点证据、先怎么手工交付、手工作战卡怎么写、能不能先用 spreadsheet / 表单 / no-code 跑起来、如果必须开始做产品也只自动化最重复的一步并先压成 forms / lists / CRUD 骨架、第一版只做哪一件事、能不能压成周末级 MVP、第一批客户路径、从第一个客户开始怎么收费、客户 1 如何打平成本、有没有 10 个样本和更强付费信号、达到什么条件才允许产品化、增长阶段守什么纪律、这条路是否可逆、是否真在解决客户问题,以及是否符合团队价值观、是不是你愿意长期住进去的业务形态。用户没确认前,不要直接把核心需求写成既定事实。';
1491
1921
  } else if (review.status === 'needs-revision') {
1492
1922
  nextStep = 'prd-review-required';
1493
1923
  reason = '当前这版需求确认稿已经被标记为需要调整,先改完再继续后面的整理或实现。';
@@ -1603,7 +2033,10 @@ function gateHasConfirmedCurrentReview(gate, progress) {
1603
2033
  function canAutoAuthorizeRequirementExecution(gate, progress) {
1604
2034
  return Boolean(
1605
2035
  isBlockingRequirementGate(gate)
1606
- && gate?.intent?.explicitExecution
2036
+ && (
2037
+ gate?.intent?.explicitExecution
2038
+ || gate?.reviewActionAuthorization?.continueAfterReview
2039
+ )
1607
2040
  && progress?.nextStep === 'implementation-ready'
1608
2041
  && gateHasConfirmedCurrentReview(gate, progress)
1609
2042
  );
@@ -1655,15 +2088,129 @@ function isMutationPayload(payload, risk) {
1655
2088
  || /\*\*\* Begin Patch/.test(text);
1656
2089
  }
1657
2090
 
2091
+ function isFrontendTaskIntent(intent = null) {
2092
+ const text = String(intent?.promptText || '').trim();
2093
+ if (!text || intent?.visualMockupRequest) {
2094
+ return false;
2095
+ }
2096
+ return /(界面|页面|前端|首页|落地页|原型|导览|展览|馆藏|网站|web\s*page|landing|dashboard|hero|layout|静态单页|静态页|样式|视觉|app\s*首页|官网)/i.test(text);
2097
+ }
2098
+
2099
+ function promptHasExplicitVisualReference(intent = null) {
2100
+ const text = String(intent?.promptText || '');
2101
+ return /(参考图|效果图|设计稿|按这个做|照着这个做|复刻|跟这个|reference|mockup|figma)/i.test(text);
2102
+ }
2103
+
2104
+ function shouldRequireFactsSheet(intent = null) {
2105
+ const text = String(intent?.promptText || '');
2106
+ return /(开放时间|营业时间|票价|价格|英镑|美元|会员|版本|发布|规格|排名|数据|hours?|price|ticket|version|release|spec)/i.test(text);
2107
+ }
2108
+
2109
+ function shouldRequireImagePreflight(intent = null) {
2110
+ const text = String(intent?.promptText || '');
2111
+ return /(馆藏|展览|导览|展品|museum|gallery|travel|旅行|内容|专题|story|editorial|摄影|图片|photo|地图|route|路线规划)/i.test(text);
2112
+ }
2113
+
2114
+ function shouldRequireAssetSpec(intent = null) {
2115
+ return isFrontendTaskIntent(intent);
2116
+ }
2117
+
2118
+ function designActiveFilePath(root, filename) {
2119
+ return path.join(root, '.openprd', 'design', 'active', filename);
2120
+ }
2121
+
2122
+ function designArtifactHasPlaceholder(root, filename) {
2123
+ const text = readTextSync(designActiveFilePath(root, filename), 12000);
2124
+ return !text || /待填写/.test(text);
2125
+ }
2126
+
2127
+ function looksLikeFrontendImplementationFile(relativePath) {
2128
+ const file = String(relativePath || '');
2129
+ if (!file || file.startsWith('.openprd/')) {
2130
+ return false;
2131
+ }
2132
+ if (/^(miniprogram|wechat|weapp)\//i.test(file)) {
2133
+ return false;
2134
+ }
2135
+ if (/(^|\/)(index|home|landing)\.(html|jsx|tsx|vue|svelte|astro)$/i.test(file)) {
2136
+ return true;
2137
+ }
2138
+ if (/\.(html|css|scss|sass|less|jsx|tsx|vue|svelte|astro)$/i.test(file)) {
2139
+ return true;
2140
+ }
2141
+ if (!/\.(js|ts)$/i.test(file)) {
2142
+ return false;
2143
+ }
2144
+ return /(^|\/)(app|pages?|components?|ui|layouts?|views?|screens?|routes?|frontend|web)\//i.test(file);
2145
+ }
2146
+
2147
+ function frontendImplementationTargets(root, payload) {
2148
+ return extractTouchedFiles(root, payload).filter((file) => looksLikeFrontendImplementationFile(file));
2149
+ }
2150
+
2151
+ function frontendDesignPreflightIssues(root, intent, implementationFiles) {
2152
+ if (!isFrontendTaskIntent(intent) || implementationFiles.length === 0) {
2153
+ return [];
2154
+ }
2155
+ const issues = [];
2156
+ if (shouldRequireFactsSheet(intent) && designArtifactHasPlaceholder(root, 'facts-sheet.md')) {
2157
+ issues.push({
2158
+ file: 'facts-sheet.md',
2159
+ reason: '这次页面要写明确事实,先把已知事实和来源或用户给定值写进去。',
2160
+ });
2161
+ }
2162
+ if (shouldRequireAssetSpec(intent) && designArtifactHasPlaceholder(root, 'asset-spec.md')) {
2163
+ issues.push({
2164
+ file: 'asset-spec.md',
2165
+ reason: '进入实现前先冻结资产口径;没有官方资产也要写“缺失 / 暂无 / 不适用”,不要留模板占位。',
2166
+ });
2167
+ }
2168
+ if (shouldRequireImagePreflight(intent) && designArtifactHasPlaceholder(root, 'image-preflight.md')) {
2169
+ issues.push({
2170
+ file: 'image-preflight.md',
2171
+ reason: '这是内容型页面,先判断真实图片是不是成立前提,以及缺失时怎么降级。',
2172
+ });
2173
+ }
2174
+ if (!promptHasExplicitVisualReference(intent) && designArtifactHasPlaceholder(root, 'direction-plan.md')) {
2175
+ issues.push({
2176
+ file: 'direction-plan.md',
2177
+ reason: '当前没有明确参考方向,先把 3 个异源方向写实,而不是直接落回同一种安全解。',
2178
+ });
2179
+ }
2180
+ if (!promptHasExplicitVisualReference(intent) && designArtifactHasPlaceholder(root, 'selected-direction.md')) {
2181
+ issues.push({
2182
+ file: 'selected-direction.md',
2183
+ reason: '进入实现前先锁定选中的 lens、theme、layout 和组件。',
2184
+ });
2185
+ }
2186
+ return issues;
2187
+ }
2188
+
1658
2189
  function captureSourceFromCommand(command) {
1659
2190
  return readCliFlagValue(command, '--source');
1660
2191
  }
1661
2192
 
2193
+ function captureFieldFromCommand(command) {
2194
+ return readCliFlagValue(command, '--field');
2195
+ }
2196
+
1662
2197
  function isNonSemanticCaptureCommand(command) {
1663
2198
  return /openprd\s+capture\b/i.test(command)
1664
2199
  && captureSourceFromCommand(command) === 'agent-normalized';
1665
2200
  }
1666
2201
 
2202
+ function isDerivedClarificationCaptureCommand(command) {
2203
+ if (!/openprd\s+capture\b/i.test(command)) {
2204
+ return false;
2205
+ }
2206
+ const source = captureSourceFromCommand(command);
2207
+ return source === 'agent-inferred' || source === 'project-derived';
2208
+ }
2209
+
2210
+ function isMetaStatusCaptureCommand(command) {
2211
+ return /openprd\s+capture\b/i.test(command) && captureFieldFromCommand(command) === 'meta.status';
2212
+ }
2213
+
1667
2214
  function gateHasClarificationConfirmation(gate) {
1668
2215
  return Boolean(gate?.clarificationConfirmedAt || gate?.status === 'clarification-confirmed');
1669
2216
  }
@@ -1696,9 +2243,27 @@ function isAllowedDuringRequirementGate(root, payload, gate, sessionId = null) {
1696
2243
  return true;
1697
2244
  }
1698
2245
  if (/openprd\s+capture\b/i.test(command)) {
1699
- return isNonSemanticCaptureCommand(command) || requirementWritePathExplicitlyAuthorized(gate);
2246
+ if (isNonSemanticCaptureCommand(command)) {
2247
+ return true;
2248
+ }
2249
+ if (!requirementWritePathExplicitlyAuthorized(gate)) {
2250
+ return false;
2251
+ }
2252
+ if (progress.nextStep === 'clarification-writeback-required') {
2253
+ if (isMetaStatusCaptureCommand(command)) {
2254
+ return false;
2255
+ }
2256
+ if (isDerivedClarificationCaptureCommand(command)) {
2257
+ return false;
2258
+ }
2259
+ }
2260
+ return true;
1700
2261
  }
1701
- if (/openprd\s+(classify|synthesize|diagram)\b/i.test(command)) {
2262
+ if (/openprd\s+synthesize\b/i.test(command)) {
2263
+ return requirementWritePathExplicitlyAuthorized(gate)
2264
+ && !['clarification-writeback-required', 'product-type-classification-required', 'requirement-interview-required'].includes(progress.nextStep);
2265
+ }
2266
+ if (/openprd\s+(classify|diagram)\b/i.test(command)) {
1702
2267
  return requirementWritePathExplicitlyAuthorized(gate);
1703
2268
  }
1704
2269
  if (/openprd\s+review-presentation\b/i.test(command)) {
@@ -1947,6 +2512,7 @@ function runOpenPrd(args, cwd) {
1947
2512
  cwd,
1948
2513
  encoding: 'utf8',
1949
2514
  timeout: 30000,
2515
+ maxBuffer: 10 * 1024 * 1024,
1950
2516
  env: process.env,
1951
2517
  });
1952
2518
  return {
@@ -1969,6 +2535,17 @@ function parseJsonOutput(text) {
1969
2535
  }
1970
2536
  }
1971
2537
 
2538
+ function compactSingleLine(text, limit = 240) {
2539
+ const singleLine = String(text || '').replace(/\s+/g, ' ').trim();
2540
+ if (!singleLine) {
2541
+ return '';
2542
+ }
2543
+ if (singleLine.length <= limit) {
2544
+ return singleLine;
2545
+ }
2546
+ return `${singleLine.slice(0, Math.max(0, limit - 1))}…`;
2547
+ }
2548
+
1972
2549
  function devCheckWrapUpMessage(root, turnState) {
1973
2550
  const files = Array.isArray(turnState?.touchedFiles)
1974
2551
  ? [...new Set(turnState.touchedFiles)].filter(Boolean)
@@ -2023,7 +2600,8 @@ function summarizeRunVerifyCheck(parsed, fallbackText = '') {
2023
2600
 
2024
2601
  function summarizeDoctorCheck(parsed, fallbackText = '') {
2025
2602
  if (!parsed?.agentIntegration && !parsed?.standards && !parsed?.validation) {
2026
- return fallbackText || 'doctor result unavailable';
2603
+ const compact = compactSingleLine(fallbackText);
2604
+ return compact || 'doctor result unavailable';
2027
2605
  }
2028
2606
  const parts = [];
2029
2607
  if (parsed.agentIntegration) {
@@ -2144,32 +2722,33 @@ function requirementGateMessage(intent, gate) {
2144
2722
  'The user is asking for an image asset such as a cover image, poster, illustration, icon, sticker, visual mockup, or effect image, not code implementation.',
2145
2723
  'For logo, icon, avatar, badge, and similar development assets, default to a standalone asset: full-frame single subject with no extra UI frame, card shell, device mockup, or presentation container unless the user explicitly asked for one.',
2146
2724
  'Do not create temporary HTML/SVG/CSS files for this image unless the user explicitly requested that format.',
2147
- 'Use `imagegen`, which is Codex native Image 2, to generate the image; keep implementation, PRD review, and visual-compare for later explicit confirmation.',
2725
+ 'Use `imagegen`, which is Codex native Image 2, to generate the image; keep implementation, PRD review, and visual-compare for later explicit confirmation of whether this generated image should become a reference.',
2148
2726
  ].join('\n');
2149
2727
  }
2150
2728
  const status = gateBlocksImplementation ? 'active' : 'opened';
2151
2729
  return [
2152
2730
  'OpenPrd requirement intake gate: ' + status + '.',
2153
2731
  'This prompt looks like a likely 新功能/新流程方案 (L2), so the heavy requirement-intake lane is active. Do not decide from fixed keywords; first use $openprd-requirement-intake to classify the user-visible requirement type by impact, unknowns, decision cost, and validation cost.',
2154
- 'Keep this mapping visible for internal review: 快速修正=L0, 现有功能优化=L1, 新功能/新流程方案=L2.',
2732
+ 'Keep this mapping visible for internal review: 直接处理=L0, 现有功能优化=L1, 新功能/新流程方案=L2.',
2155
2733
  'L0 and L1 stay on lightweight paths and should not be forced through formal PRD/review/change/tasks unless the scope expands.',
2156
2734
  'If the requirement type is 新功能/新流程方案 (L2), do not edit implementation files yet and proceed through PRD/review/change/tasks with the appropriate PRD scene lens: 通用场景、面向个人消费者场景、面向企业服务场景,或以 Agent 为主要使用场景。 Keep raw enum values such as base / consumer / b2b / agent for internal commands or records only; do not surface them to the user unless truly necessary.',
2157
2735
  reviewPolicyAllowsSilentRecord(approvalPolicy)
2158
2736
  ? 'Decision-point policy: because the user explicitly said there is no need for any confirmation stop, you may skip the requirement-summary confirmation stop, write back the requirement facts, synthesize the PRD, record the exact current stable review artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.'
2159
- : 'Decision-point policy: first output a short structured requirement summary in chat with 需求判断 / 需求理解 / 功能范围 / 技术方案, where 功能范围 and 技术方案 should prefer Markdown tables; wait for the user to confirm that summary, then write back confirmed facts, synthesize the PRD, wait for a human decision on the stable review artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.',
2737
+ : 'Decision-point policy: first output a short structured requirement summary in chat with 需求判断 / 需求理解 / 功能范围 / 技术方案, where 功能范围 and 技术方案 should prefer Markdown tables; if this is still a 0-to-1 or worth-doing discussion, also surface the validation/startup loop with 第一批最容易触达的社区或种子用户、你为什么算这个社区里的自己人、当前替代方案和痛点证据、先怎么手工交付、手工作战卡怎么写、能不能先用 spreadsheet / 表单 / no-code 跑起来、什么承诺才算真需求、有没有 10 个样本和更强付费信号、最低成本先验证什么、达到什么条件才允许产品化、增长阶段守什么纪律、以及验证阶段怎样先活下来; wait for the user to confirm that summary, then write back confirmed facts, synthesize the PRD, wait for a human decision on the stable review artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.',
2160
2738
  reviewPolicyAllowsSilentRecord(approvalPolicy)
2161
2739
  ? 'This lane is in silent-record mode only because the user explicitly said there is no need for any further review or confirmation stop. Plain "请帮我实现" is not enough; you may record only the exact current version, digest, and work unit.'
2162
2740
  : 'Requirement-summary confirmation, review-artifact confirmation, and implementation authorization are different gates: do not treat "可以开做", "继续实现", plain "请帮我实现", or "不需要评审" as permission to skip them.',
2163
2741
  'If the original request already asked to implement, execution can continue once the active approval policy and tasks are ready; otherwise wait for a clear execution request.',
2164
- 'Recommended next action: write a short 需求类型判断 in chat, and by default merge the route into the label as 需求类型:新功能/新流程方案(L2); only add a separate 内部路由码 when internal debugging truly benefits. Then run openprd clarify ., summarize the requirement in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, ask for confirmation, and only after that write back confirmed facts. Do not open a clarification HTML page; the formal HTML review happens after synthesize/review.',
2742
+ 'Recommended next action: write a short 需求类型判断 in chat, and by default merge the route into the label as 需求类型:新功能/新流程方案(L2); only add a separate 内部路由码 when internal debugging truly benefits. Then run openprd clarify ., summarize the requirement in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, and if the ask still looks like 0-to-1 validation also add 第一批最容易触达的社区或种子用户、你为什么算这个社区里的自己人、当前替代方案和痛点证据、先怎么手工交付、手工作战卡怎么写、能不能先用 spreadsheet / 表单 / no-code 跑起来、什么承诺才算真需求、有没有 10 个样本和更强付费信号、最低成本先验证什么、达到什么条件才允许产品化、增长阶段守什么纪律、以及验证阶段怎样先活下来; ask for confirmation, and only after that write back confirmed facts. Do not open a clarification HTML page; the formal HTML review happens after synthesize/review.',
2165
2743
  ].join('\n');
2166
2744
  }
2167
2745
 
2168
2746
  function lightweightRequirementMessage(intent) {
2169
2747
  if (intent?.requirementTier === 'l0') {
2170
2748
  return [
2171
- 'OpenPrd 轻量需求路径: 当前更接近快速修正 (L0)。',
2172
- '先在 chat 用短格式写出“需求类型 / 理由 / 推荐下一步”,并默认写成“需求类型:快速修正(L0)”;只有内部排障确实需要时,才额外单列“内部路由码”。',
2749
+ 'OpenPrd 轻量需求路径: 当前更接近直接处理 (L0)。',
2750
+ '先在 chat 用短格式写出“需求类型 / 理由 / 推荐下一步”,并默认写成“需求类型:直接处理(L0)”;只有内部排障确实需要时,才额外单列“内部路由码”。',
2751
+ '对用户先用一句轻量主句说清这次是什么和为什么先这样处理;边界和技术细项放到后一句,不要揉成一大段解释。',
2173
2752
  '直接处理并事后说明即可,不打开正式 PRD/review/change/tasks。',
2174
2753
  '优先做最小足够验证,并用 1-2 句说明本轮特别需要强化的测试点;默认不要求正式测试报告。',
2175
2754
  '如果过程中暴露出跨系统依赖、支付/账号/权限/回调等高风险因素,再升级到 L2 重流程。',
@@ -2179,12 +2758,10 @@ function lightweightRequirementMessage(intent) {
2179
2758
  return [
2180
2759
  'OpenPrd 轻量需求路径: 当前更接近现有功能优化 (L1)。',
2181
2760
  '先在 chat 用短格式写出“需求类型 / 理由 / 推荐下一步”,并默认写成“需求类型:现有功能优化(L1)”,再给 3-5 行 mini-plan;只有内部排障确实需要时,才额外单列“内部路由码”。',
2761
+ '主句先轻一点:先说这次调整是什么、要解决什么,再把范围边界和技术细项留给 mini-plan,不要一上来写成长段。',
2182
2762
  '先在对话里给 3-5 行 mini-plan,至少写清目标、范围内、范围外和验证方式。',
2183
2763
  '默认不要打开正式 PRD/review/change/tasks;只有在 mini-plan 暴露新决策缺口、跨系统风险或范围升级时,才提升到 L2 重流程。',
2184
2764
  '验证采用最小足够组合即可,重点说明需要强化测试的地方;默认不要求正式测试报告。',
2185
- intent?.largeUiChangeRequest
2186
- ? '如果这是大界面改动,mini-plan 之后先做 3 方向视觉方案评审,再进入实现。'
2187
- : '',
2188
2765
  ].filter(Boolean).join('\n');
2189
2766
  }
2190
2767
  return null;
@@ -2200,20 +2777,38 @@ function visualMockupMessage(intent) {
2200
2777
  '对 logo、icon、avatar、badge 等开发素材,如果用户没有明确要求 mockup、场景图、设备框、卡片承载、名片/包装展示或参考界面复刻,默认按独立素材输出(standalone asset)处理:使用全画布单主体,不额外添加 UI frame、卡片、设备壳、名片、桌面陈列、手持实拍或其他展示容器。',
2201
2778
  '只有当用户明确要求 mockup、场景化效果图、容器化呈现,或参考图本身就包含这些承载结构时,才生成对应的容器或场景。',
2202
2779
  '只有在实际发生 `imagegen` 调用后,才能汇报生图结果、失败或限流;未调用 `imagegen` 前,不要声称“生图限流”或“生图失败”。',
2203
- 'OpenPrd review.html 只用于需求评审,visual-compare 只用于实现阶段视觉证据:已有参考图时做效果图/实现截图对比,无参考图但改动界面时做修改前/修改后自检;局部细节优先补局部焦点证据板,并行优化方向优先补并行实验证据板。',
2780
+ ' `imagegen` 结果先当成候选效果图,不要默认登记到 `.openprd/harness/visual-reviews/`,也不要自动当成后续验收参考。',
2781
+ '如果用户看完图后还要继续做实现,主动连问三件事:是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续后续实现。只有用户确认纳入对比或继续实现后,才把这张图、这组图或其中选定子图整理成后续 reference-set;如果用户只是认可图片但暂不实现,就继续把它当候选效果图。',
2782
+ '如果一张图里有多个子图、网格、多对象或多个方向,不要把整板直接当成单一参考图;先运行 `openprd visual-prepare . --reference <效果图> --grid <列>x<行>` 或 `--boxes <plan.json>` 生成 reference-set、contact sheet 和 compare-plan,确认 contact sheet 后再逐项对比或统一验收。',
2783
+ 'OpenPrd review.html 只用于需求评审,visual-compare 只用于实现阶段视觉证据:已有参考图时做效果图/实现截图对比;没有参考图时先判断新建界面还是修改既有界面,新建界面回到实现前 3 方向方案评审,修改既有界面做修改前/修改后自检;局部细节优先补局部焦点证据板,并行优化方向优先补并行实验证据板。',
2204
2784
  ].join('\n');
2205
2785
  }
2206
2786
 
2207
- function largeUiVisualDirectionMessage(intent) {
2208
- if (!intent?.largeUiChangeRequest) {
2209
- return null;
2210
- }
2787
+ function learningReviewMessage() {
2788
+ return [
2789
+ '如果当前交付目标是把一次工作转成可学习、可复用、可回看、可教学或可沉淀的材料,不要先按关键词判断,也不要只停留在普通说明文档。',
2790
+ '先判断期望产物形态:是否需要章节结构、证据锚点、图文讲解、检索练习、工作示例或阅读体验。需要这些形态时,优先使用 `openprd learn .` 生成学习包骨架、阅读器和证据清单,再补充正文内容;普通 Markdown 只能作为辅助讲义。',
2791
+ ].join('\n');
2792
+ }
2793
+
2794
+ function largeUiVisualDirectionMessage() {
2211
2795
  return [
2212
2796
  'OpenPrd 大界面改动视觉方案评审:',
2213
2797
  '位置: 需求分流之后、PRD 定稿或实现开工之前;它不同于 review.html,也不同于实现后的 visual-compare。',
2214
- '判断: 会明显改变信息架构、核心布局、主视觉、关键路径、组件层级/密度,或用户需要先选设计方向时触发。',
2215
- '步骤: Codex Computer Use 进入产品内对应功能并截当前真实界面;基于截图调用 `imagegen`(Codex 原生 Image 2)做图生图,至少生成 3 个不同设计思想方向;把效果图横向拼成一张大图,每张左上角标注 1/2/3,并保存到 .openprd/harness/visual-reviews/。',
2216
- '交互: 把横向大图展示给用户评审确认;用户确认方向前,不进入大 UI 实现,也不要声称界面方案已定。',
2798
+ '判断: 先从用户目标、信息架构变化、视觉决策成本和验收风险判断是否需要方向评审,不用关键词触发。会决定首屏、核心布局、主视觉、关键路径、组件层级/密度,或用户需要先选设计方向时触发。',
2799
+ '步骤: 已有界面时用 Codex Computer Use 进入产品内对应功能并截当前真实界面;冷启动没有现有界面时,基于已确认 PRD、用户画像、第一版切片和视觉目标写设计 brief。然后调用 `imagegen`(Codex 原生 Image 2)生成至少 3 个不同设计思想方向;把效果图横向拼成一张带 1/2/3 序号的大图,先作为候选效果图给用户评审,不要默认写入 `.openprd/harness/visual-reviews/` 当验收参考。',
2800
+ '交互: 主动追问三件事:是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续后续实现。只有用户确认纳入后续对比或继续实现后,才把选定方向、整张图或其中子图整理成 reference-set 并写入 `.openprd/harness/visual-reviews/`;用户确认前,不进入大 UI 实现,也不要声称界面方案已定。',
2801
+ '多对象参考处理: 如果候选效果图是一张图里包含多个子图、网格、多对象或多方向,不要拿整板直接硬比;先运行 `openprd visual-prepare . --reference <效果图> --grid <列>x<行>` 或 `--boxes <plan.json>`,检查 contact sheet,确认 reference-set 后再逐项进入实现和验收。',
2802
+ ].join('\n');
2803
+ }
2804
+
2805
+ function visualLocalAdjustmentMessage() {
2806
+ return [
2807
+ '如果这轮验收重点在局部变化,不要只改代码后凭主观判断收尾。',
2808
+ '先判断这是新建界面还是修改既有界面:新建界面走实现前 3 方向方案评审;修改既有界面先保留修改前截图。完成后从同一入口、视口、账号和数据状态截修改后截图,再运行 `openprd visual-compare . --before <修改前截图> --after <修改后截图>`。',
2809
+ '如果局部细节需要放到同一张证据板里审阅,再补一份 `openprd visual-compare . --board <focus-board.json>` 的局部焦点证据板,把局部变化组合到同一张证据板里统一验收。',
2810
+ '如果参考图是一张整板、网格图或多对象组合图,先运行 `openprd visual-prepare . --reference <效果图> --grid <列>x<行>` 或 `--boxes <plan.json>`,确认 contact sheet 后,再决定是逐项 `--reference/--actual` 还是统一做 `focus-board` / `parallel-board`。',
2811
+ '当用户后续说“跟效果图”“不一致”“好丑”“复刻”“没对齐”这类反馈时,不能只口头说已经对比过了;至少先产出一份 `openprd visual-compare`、`focus-board` 或 `parallel-board` 证据图再下结论。'
2217
2812
  ].join('\n');
2218
2813
  }
2219
2814
 
@@ -2231,7 +2826,8 @@ function confirmationGateMessage(gate) {
2231
2826
  return [
2232
2827
  intro,
2233
2828
  'Implementation may proceed only within the confirmed scope, with docs/basic, file manuals, folder README docs, standards verification, and OpenPrd run verification kept up to date. For backend, script, agent, tooling, service, or data-processing changes, keep CLI and API surface review current in docs/basic/backend-structure.md.',
2234
- 'For UI or visual work with an existing reference image, capture the implemented UI and run openprd visual-compare . --reference <effect-image> --actual <implementation-screenshot>; if local detail matters more than the whole screen, add openprd visual-compare . --board <focus-board.json> so the agent can review numbered zoom regions. When there is no reference image, capture the before screenshot first, implement, capture the after screenshot from the same entry, viewport, account, and data state, then run openprd visual-compare . --before <before-screenshot> --after <after-screenshot>; if the agent explored multiple optimization directions, add openprd visual-compare . --board <parallel-board.json> and inspect expected changes plus unintended drift before claiming completion.',
2829
+ 'For UI or visual work with an existing reference image, first confirm the user has accepted that image as a later comparison reference; if one image contains multiple sub-images, grid cells, or objects, run openprd visual-prepare . --reference <effect-image> --grid <columns>x<rows> or --boxes <plan.json>, inspect the contact sheet, and use the generated reference-set / compare-plan before comparing. Then capture the implemented UI and run openprd visual-compare . --reference <effect-image> --actual <implementation-screenshot>; if local detail matters more than the whole screen, add openprd visual-compare . --board <focus-board.json> so the agent can review numbered zoom regions. When there is no reference image, capture the before screenshot first, implement, capture the after screenshot from the same entry, viewport, account, and data state, then run openprd visual-compare . --before <before-screenshot> --after <after-screenshot>; if the agent explored multiple optimization directions, add openprd visual-compare . --board <parallel-board.json> and inspect expected changes plus unintended drift before claiming completion.',
2830
+ 'If the user later says the implementation does not match the effect image, looks wrong, or asks for replication, do not rely on subjective narration alone; produce at least one visual evidence artifact before claiming completion.',
2235
2831
  ].join('\n');
2236
2832
  }
2237
2833
 
@@ -2247,6 +2843,12 @@ function currentRequirementStatusLine(gate, progress) {
2247
2843
  switch (progress?.nextStep) {
2248
2844
  case 'clarification-confirmation-required':
2249
2845
  return '当前卡点: 先让用户确认当前需求摘要;在此之前不要把核心需求写成既定事实,也不要直接往后推进。';
2846
+ case 'clarification-writeback-required':
2847
+ return '当前卡点: 用户已经确认这轮需求摘要或默认方案,但 current.json 里还没有把确认后的事实稳定写回。先按 canonical 字段路径用 user-confirmed 回写,再继续 synthesize;不要再回退成泛化 clarify,也不要继续写 agent-inferred。';
2848
+ case 'product-type-classification-required':
2849
+ return '当前卡点: 用户已经确认这轮需求摘要,但产品场景还没锁定。先完成 classify 并让当前 lane 的产品场景与写回事实一致,再继续 synthesize。';
2850
+ case 'requirement-interview-required':
2851
+ return '当前卡点: 用户已经确认这轮需求摘要,但 current.json 里还缺这轮 requirement 的关键事实。先补齐 canonical 字段,必要时再用 openprd interview 收敛剩余问题,然后继续 synthesize。';
2250
2852
  case 'prd-synthesis-required':
2251
2853
  return reviewPolicyAllowsSilentRecord(requirementApprovalPolicy(gate))
2252
2854
  ? '当前卡点: 用户已明确表示不需要再停下来确认,可以直接整理已确认内容,并生成这版需求确认稿。'
@@ -2279,7 +2881,7 @@ function currentRequirementMessage(intent, gate, progress) {
2279
2881
  gateStatus,
2280
2882
  '对外表达要求: 面向用户不要直接复述 PRD、review artifact、change、tasks、lane、approval policy、work unit、digest、worker shard、write-scope、worktree、内部版本号、命令或文件路径;改说“需求确认稿”“本次调整”“后续任务”“继续落地”“完成后检查”。',
2281
2883
  '当前输入已被判定为可能的新功能/新流程方案(L2),因此进入重流程需求入口。不要按固定关键词判断;先用 $openprd-requirement-intake 按影响面、未知数、决策成本和验证成本判断用户可见需求类型。',
2282
- '内部审查保留固定对照:快速修正=L0,现有功能优化=L1,新功能/新流程方案=L2。',
2884
+ '内部审查保留固定对照:直接处理=L0,现有功能优化=L1,新功能/新流程方案=L2。',
2283
2885
  '如果用户刚刚已经确认了现有功能优化(L1)的 mini-plan、范围边界或正式产品边界,下一句要明确写成“已确认,我按这个继续/收口/落地”;不要只写一个“确认”,更不要写成“确认,我们就按这个……”这种容易让用户误以为还要再表态的句子。',
2284
2886
  '如果需求类型是新功能/新流程方案(L2),本轮只围绕这个新需求推进 PRD/review/change/tasks,并选择通用场景 / 面向个人消费者场景 / 面向企业服务场景 / 以 Agent 为主要使用场景的 PRD 视角,不自动继续历史 active change。对用户复述时不要直接把 consumer / b2b / agent 当展示词;这些枚举值只用于内部记录和命令。',
2285
2887
  prompt ? '本轮需求: ' + prompt : '',
@@ -2295,11 +2897,8 @@ function currentRequirementMessage(intent, gate, progress) {
2295
2897
  currentRequirementStatusLine(gate, progress),
2296
2898
  reviewPolicyAllowsSilentRecord(approvalPolicy)
2297
2899
  ? 'Decision-point order: because the user explicitly waived any confirmation stop, you may skip requirement-summary confirmation, write back requirement facts, synthesize the PRD, record the exact stable review artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.'
2298
- : 'Decision-point order: clarify the requirement, summarize it in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, wait for the user to confirm that requirement summary, write back only confirmed facts, synthesize the PRD, wait for a human review decision on the stable artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.',
2299
- intent?.largeUiChangeRequest
2300
- ? 'Large UI direction gate: before PRD freeze or implementation, capture the current in-product screen with Codex Computer Use, generate at least three Image 2 directions, combine them into one horizontal numbered contact sheet, and wait for the user to choose a direction.'
2301
- : '',
2302
- 'Recommended next action: 先在 chat 输出“需求类型判断”,默认把路由码并进“需求类型:新功能/新流程方案(L2)”这类标签里;只有内部排障确实需要时,才额外写“内部路由码”。若为新功能/新流程方案(L2),再运行 openprd clarify .,并按“需求判断 / 需求理解 / 功能范围 / 技术方案”给出十句话左右的结构化摘要,其中“功能范围”和“技术方案”优先用 Markdown 表格;请求确认后再写回 requirement 事实并继续 classify/synthesize,不要把这一步表述成“确认后直接开始实现”。Do not open clarification HTML; use review.html only after synthesize/review.',
2900
+ : 'Decision-point order: clarify the requirement, summarize it in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, and if the ask still looks like 0-to-1 validation also surface 第一批最容易触达的社区或种子用户、你为什么算这个社区里的自己人、当前替代方案和痛点证据、先怎么手工交付、手工作战卡怎么写、能不能先用 spreadsheet / 表单 / no-code 跑起来、什么承诺才算真需求、有没有 10 个样本和更强付费信号、最低成本先验证什么、达到什么条件才允许产品化、增长阶段守什么纪律、以及验证阶段怎样先活下来; wait for the user to confirm that requirement summary, write back only confirmed facts, synthesize the PRD, wait for a human review decision on the stable artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.',
2901
+ 'Recommended next action: 先在 chat 输出“需求类型判断”,默认把路由码并进“需求类型:新功能/新流程方案(L2)”这类标签里;只有内部排障确实需要时,才额外写“内部路由码”。若为新功能/新流程方案(L2),再运行 openprd clarify .,并按“需求判断 / 需求理解 / 功能范围 / 技术方案”给出结构化摘要,其中“功能范围”和“技术方案”优先用 Markdown 表格;`需求判断` 和 `需求理解` 先用轻量主句说清这次是什么、核心问题和第一版目标,再把边界、风险、异常例子和技术细项下沉到后续分项或表格,不要把某条示例文案写成固定模板。如果这轮本质上还在判断值不值得做,还要主动补上:第一批最容易触达的社区或种子用户、当前替代方案、先怎么手工交付、什么承诺才算真需求、最低成本先验证什么,以及验证阶段怎样先活下来。请求确认后再写回 requirement 事实并继续 classify/synthesize,不要把这一步表述成“确认后直接开始实现”。Do not open clarification HTML; use review.html only after synthesize/review.',
2303
2902
  ];
2304
2903
  if (isImplementationAdvanceIntent(intent)) {
2305
2904
  lines.splice(2, 1, gate?.active
@@ -2311,7 +2910,7 @@ function currentRequirementMessage(intent, gate, progress) {
2311
2910
  }
2312
2911
 
2313
2912
  function requirementRoutingSummary() {
2314
- return '需求类型由 $openprd-requirement-intake 按影响面、未知数、决策成本和验证成本判断:快速修正(L0)直接处理并事后说明,不打开正式 PRD/review/change/tasks;现有功能优化(L1)先给对话内 mini-plan,默认不生成正式 PRD/change/tasks;新功能/新流程方案(L2)才进入 requirement intake 与 PRD/review/change/tasks,并选择通用场景 / 面向个人消费者场景 / 面向企业服务场景 / 以 Agent 为主要使用场景的 PRD 视角。对用户复述时不要直接把 consumer / b2b / agent 当展示词;这些枚举值只用于内部记录和命令。单纯的“请帮我实现/继续实现”只表示有执行意图,不表示跳过 requirement 摘要确认或 review;只有用户明确表示不需要进行任何确认时,才允许静默走完整 requirement write path。';
2913
+ return '需求类型由 $openprd-requirement-intake 按影响面、未知数、决策成本和验证成本判断:直接处理(L0)可直接处理并事后说明,不打开正式 PRD/review/change/tasks;现有功能优化(L1)先给对话内 mini-plan,默认不生成正式 PRD/change/tasks;新功能/新流程方案(L2)才进入 requirement intake 与 PRD/review/change/tasks,并选择通用场景 / 面向个人消费者场景 / 面向企业服务场景 / 以 Agent 为主要使用场景的 PRD 视角。对用户复述时不要直接把 consumer / b2b / agent 当展示词;这些枚举值只用于内部记录和命令。单纯的“请帮我实现/继续实现”只表示有执行意图,不表示跳过 requirement 摘要确认或 review;只有用户明确表示不需要进行任何确认时,才允许静默走完整 requirement write path。';
2315
2914
  }
2316
2915
 
2317
2916
  function historicalRequirementReminder(root, runContext, intent, gate) {
@@ -2359,17 +2958,19 @@ function contextMessage(cwd, intent = null, gate = null, progress = null) {
2359
2958
  'OpenPrd 上下文只是建议,不是自动执行指令。请先判断用户当前意图。',
2360
2959
  lightweightRequirementMessage(intent),
2361
2960
  visualMockupMessage(intent),
2362
- largeUiVisualDirectionMessage(intent),
2961
+ learningReviewMessage(),
2962
+ largeUiVisualDirectionMessage(),
2963
+ visualLocalAdjustmentMessage(),
2363
2964
  requirementRoutingSummary(),
2364
2965
  '如果用户只是要求看看、规划、分析、审查、解释影响或列出文件,请保持只读并基于证据回答;不要运行 OpenPrd loop、任务推进、discovery 推进、commit 或其他写入命令。',
2365
2966
  '只有当用户当前明确要求开发、实现、修复、继续任务、深度调研、对标复刻或提交时,才运行 openprd loop --run、openprd tasks --advance、openprd discovery --advance、commit/push 等执行命令。',
2366
2967
  '代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 openprd dev-check . <file...>;若出现需要关注的文件,最终回复必须以 **后续建议** 为标题,直接复用 dev-check 生成的 Markdown 表格,列出影响对象、关注程度、规模信号、预警原因、本次处理结果和后续建议,并按 🔴 → 🟠 → 🟡 排序;不要把“关注程度”列改写成纯 emoji,必须保留例如“🟠 中风险|建议优先关注”这类完整标签;如果你改写了“预警原因 / 本次处理结果 / 后续建议”,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试。',
2367
- '大界面改动进入实现前,先用 Codex Computer Use 截取产品内当前功能截图,再用 `imagegen`(Codex 原生 Image 2)基于截图生成至少 3 个设计方向,横向拼接为一张带 1/2/3 序号的大图给用户确认;未确认方向前不要进入大 UI 实现。',
2368
- '涉及界面、页面、视觉、样式或前端体验,且已经有效果图/设计稿/用户给图并进入实现阶段时,阶段性完成后必须截图并运行 openprd visual-compare . --reference <效果图> --actual <实现截图>;如果这次重点在局部细节,再补一份 openprd visual-compare . --board <focus-board.json>。没有明确参考图但改动界面时,动手前先截修改前截图,完成后用同一入口、视口、账号和数据状态截修改后截图,并运行 openprd visual-compare . --before <修改前截图> --after <修改后截图>;如果并行试了多个优化方向,再补一份 openprd visual-compare . --board <parallel-board.json>;默认输出 JPG .openprd/harness/visual-reviews/。查看合成图后继续对标或自检,直到没有明显视觉差异或意外漂移。',
2968
+ '大界面改动的效果图先当候选效果图;出图后主动确认是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续实现。只有确认后,才把选定方向、整张图或其中子图整理到 `.openprd/harness/visual-reviews/` 并进入大 UI 实现。',
2969
+ '涉及界面、页面、视觉、样式或前端体验时,已有确认参考图才运行 openprd visual-compare . --reference <效果图> --actual <实现截图>;如果一张参考图里有多个子图/对象/网格,先运行 openprd visual-prepare . --reference <效果图> --grid <列>x<行> 或 --boxes <plan.json>,确认 contact sheet 后再逐项对比。没有明确参考图时先判断新建还是修改:新建界面走实现前 3 方向方案评审;修改既有界面则动手前先截修改前截图,完成后运行 openprd visual-compare . --before <修改前截图> --after <修改后截图>;如果并行试了多个优化方向,再补一份 openprd visual-compare . --board <parallel-board.json>。用户后续如果说“跟效果图”“不一致”“好丑”“复刻”,不能只口头说对比过了,至少产出一份 visual-compare / focus-board / parallel-board 证据图。',
2369
2970
  '发现可沉淀项时不要中途打断任务:代码扩展识别这类白名单工具补全会自动应用并记录;用户偏好、项目协作规矩和 OpenPrd 默认行为先记录为候选,收工时运行 openprd grow . --review 集中确认。',
2370
2971
  '维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;高置信可成长默认纳入,不确定则主动询问用户。',
2371
2972
  '涉及后端、脚本、Agent、工具链、服务或数据处理变更时,把 CLI 与 API 视为同级接入面:同步检查命令入口、参数、输出契约、help/doctor/dry-run/status 与接口协议、返回结构、身份边界是否受影响,并更新 docs/basic/backend-structure.md 或明确写不适用原因。',
2372
- '声明实现就绪前,先运行 openprd standards . --verify 和 openprd run . --verify',
2973
+ '声明实现就绪前,先运行 openprd standards . --verify、openprd quality . --verify 和 openprd run . --verify;L2 或跨页面实现的最终回复要列出最新 HTML 质量报告和 task-scoped Markdown/HTML 测试报告路径。',
2373
2974
  ].filter(Boolean).join('\n');
2374
2975
  }
2375
2976
  return [
@@ -2379,17 +2980,19 @@ function contextMessage(cwd, intent = null, gate = null, progress = null) {
2379
2980
  'OpenPrd 上下文只是建议,不是自动执行指令。请先判断用户当前意图。',
2380
2981
  lightweightRequirementMessage(intent),
2381
2982
  visualMockupMessage(intent),
2382
- largeUiVisualDirectionMessage(intent),
2983
+ learningReviewMessage(),
2984
+ largeUiVisualDirectionMessage(),
2985
+ visualLocalAdjustmentMessage(),
2383
2986
  requirementRoutingSummary(),
2384
2987
  '如果用户只是要求看看、规划、分析、审查、解释影响或列出文件,请保持只读并基于证据回答;不要运行 OpenPrd loop、任务推进、discovery 推进、commit 或其他写入命令。',
2385
2988
  '只有当用户当前明确要求开发、实现、修复、继续任务、深度调研、对标复刻或提交时,才运行 openprd loop --run、openprd tasks --advance、openprd discovery --advance、commit/push 等执行命令。',
2386
2989
  '代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 openprd dev-check . <file...>;若出现需要关注的文件,最终回复必须以 **后续建议** 为标题,直接复用 dev-check 生成的 Markdown 表格,列出影响对象、关注程度、规模信号、预警原因、本次处理结果和后续建议,并按 🔴 → 🟠 → 🟡 排序;不要把“关注程度”列改写成纯 emoji,必须保留例如“🟠 中风险|建议优先关注”这类完整标签;如果你改写了“预警原因 / 本次处理结果 / 后续建议”,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试。',
2387
- '大界面改动进入实现前,先用 Codex Computer Use 截取产品内当前功能截图,再用 `imagegen`(Codex 原生 Image 2)基于截图生成至少 3 个设计方向,横向拼接为一张带 1/2/3 序号的大图给用户确认;未确认方向前不要进入大 UI 实现。',
2388
- '涉及界面、页面、视觉、样式或前端体验,且已经有效果图/设计稿/用户给图并进入实现阶段时,阶段性完成后必须截图并运行 openprd visual-compare . --reference <效果图> --actual <实现截图>;如果这次重点在局部细节,再补一份 openprd visual-compare . --board <focus-board.json>。没有明确参考图但改动界面时,动手前先截修改前截图,完成后用同一入口、视口、账号和数据状态截修改后截图,并运行 openprd visual-compare . --before <修改前截图> --after <修改后截图>;如果并行试了多个优化方向,再补一份 openprd visual-compare . --board <parallel-board.json>;默认输出 JPG .openprd/harness/visual-reviews/。查看合成图后继续对标或自检,直到没有明显视觉差异或意外漂移。',
2990
+ '大界面改动的效果图先当候选效果图;出图后主动确认是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续实现。只有确认后,才把选定方向、整张图或其中子图整理到 `.openprd/harness/visual-reviews/` 并进入大 UI 实现。',
2991
+ '涉及界面、页面、视觉、样式或前端体验时,已有确认参考图才运行 openprd visual-compare . --reference <效果图> --actual <实现截图>;如果一张参考图里有多个子图/对象/网格,先运行 openprd visual-prepare . --reference <效果图> --grid <列>x<行> 或 --boxes <plan.json>,确认 contact sheet 后再逐项对比。没有明确参考图时先判断新建还是修改:新建界面走实现前 3 方向方案评审;修改既有界面则动手前先截修改前截图,完成后运行 openprd visual-compare . --before <修改前截图> --after <修改后截图>;如果并行试了多个优化方向,再补一份 openprd visual-compare . --board <parallel-board.json>。用户后续如果说“跟效果图”“不一致”“好丑”“复刻”,不能只口头说对比过了,至少产出一份 visual-compare / focus-board / parallel-board 证据图。',
2389
2992
  '发现可沉淀项时不要中途打断任务:代码扩展识别这类白名单工具补全会自动应用并记录;用户偏好、项目协作规矩和 OpenPrd 默认行为先记录为候选,收工时运行 openprd grow . --review 集中确认。',
2390
2993
  '维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;高置信可成长默认纳入,不确定则主动询问用户。',
2391
2994
  '涉及后端、脚本、Agent、工具链、服务或数据处理变更时,把 CLI 与 API 视为同级接入面:同步检查命令入口、参数、输出契约、help/doctor/dry-run/status 与接口协议、返回结构、身份边界是否受影响,并更新 docs/basic/backend-structure.md 或明确写不适用原因。',
2392
- '声明实现就绪前,先运行 openprd standards . --verify 和 openprd run . --verify',
2995
+ '声明实现就绪前,先运行 openprd standards . --verify、openprd quality . --verify 和 openprd run . --verify;L2 或跨页面实现的最终回复要列出最新 HTML 质量报告和 task-scoped Markdown/HTML 测试报告路径。',
2393
2996
  ].filter(Boolean).join('\n');
2394
2997
  }
2395
2998
  const status = runOpenPrd(['status', '.'], cwd);
@@ -2425,7 +3028,6 @@ function shouldInjectOpenPrdContext(payload) {
2425
3028
  if (
2426
3029
  intent.simpleConcrete
2427
3030
  && !intent.visualMockupRequest
2428
- && !intent.largeUiChangeRequest
2429
3031
  && !intent.continuationRequest
2430
3032
  && !hasProjectPathReference
2431
3033
  && !intent.productCopyRequest
@@ -2580,6 +3182,83 @@ function runGateChecks(cwd, payload, risk) {
2580
3182
  };
2581
3183
  }
2582
3184
 
3185
+ function hasTurnReviewSignal(turnState, kind) {
3186
+ return Array.isArray(turnState?.reviewSignals)
3187
+ && turnState.reviewSignals.some((signal) => signal?.kind === kind);
3188
+ }
3189
+
3190
+ function parseShanghaiTimestamp(value) {
3191
+ const text = String(value || '').trim();
3192
+ if (!text) return null;
3193
+ const normalized = text.includes('T') ? text : text.replace(' ', 'T');
3194
+ const withZone = /(?:Z|[+-]\d{2}:\d{2})$/.test(normalized) ? normalized : `${normalized}+08:00`;
3195
+ const parsed = Date.parse(withZone);
3196
+ return Number.isFinite(parsed) ? parsed : null;
3197
+ }
3198
+
3199
+ function collectRecentArtifactPaths(dir, startedAtMs, matcher, output = []) {
3200
+ if (!fs.existsSync(dir)) {
3201
+ return output;
3202
+ }
3203
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
3204
+ const absolutePath = path.join(dir, entry.name);
3205
+ if (entry.isDirectory()) {
3206
+ collectRecentArtifactPaths(absolutePath, startedAtMs, matcher, output);
3207
+ continue;
3208
+ }
3209
+ if (typeof matcher === 'function' && !matcher(absolutePath)) {
3210
+ continue;
3211
+ }
3212
+ const stat = fs.statSync(absolutePath);
3213
+ if (startedAtMs && Number.isFinite(startedAtMs) && stat.mtimeMs < startedAtMs) {
3214
+ continue;
3215
+ }
3216
+ output.push(absolutePath);
3217
+ }
3218
+ return output;
3219
+ }
3220
+
3221
+ function hasRecentArtifacts(root, relativeDir, turnState, matcher) {
3222
+ const dir = path.join(root, relativeDir);
3223
+ const startedAtMs = parseShanghaiTimestamp(turnState?.startedAt);
3224
+ return collectRecentArtifactPaths(dir, startedAtMs, matcher, []).length > 0;
3225
+ }
3226
+
3227
+ function closeoutVerificationReminder(root, turnState, stopIntent) {
3228
+ const touchedFiles = Array.isArray(turnState?.touchedFiles) ? turnState.touchedFiles : [];
3229
+ if (touchedFiles.length === 0) {
3230
+ return null;
3231
+ }
3232
+ if (stopIntent?.requirementTier !== 'l2' && !hasTurnReviewSignal(turnState, 'loop-finish')) {
3233
+ return null;
3234
+ }
3235
+ const codeTouched = touchedFiles.some((file) => /^(app|src|lib|components|pages|server|backend|frontend|scripts|openprd\/src)\//.test(String(file))
3236
+ || /\.(js|jsx|ts|tsx|mjs|cjs|py|rb|go|java|kt|swift|css|scss|less|vue|svelte|html)$/i.test(String(file)));
3237
+ if (!codeTouched) {
3238
+ return null;
3239
+ }
3240
+ const missing = [];
3241
+ if (!hasTurnReviewSignal(turnState, 'quality-verify')) {
3242
+ missing.push('最终 `openprd quality . --verify` 与 HTML 质量报告');
3243
+ }
3244
+ if (!hasTurnReviewSignal(turnState, 'run-verify')) {
3245
+ missing.push('最终 `openprd run . --verify`');
3246
+ }
3247
+ const hasMarkdownReport = hasRecentArtifacts(root, '.openprd/harness/test-reports', turnState, (artifactPath) => artifactPath.endsWith('.md'));
3248
+ const hasHtmlReport = hasRecentArtifacts(root, '.openprd/harness/test-reports', turnState, (artifactPath) => artifactPath.endsWith('.html'));
3249
+ if (!hasMarkdownReport || !hasHtmlReport) {
3250
+ missing.push('本轮 task-scoped Markdown / HTML 测试报告');
3251
+ }
3252
+ if (missing.length === 0) {
3253
+ return null;
3254
+ }
3255
+ return [
3256
+ 'OpenPrd 在本轮收工回顾里发现项目级收口证据还没补齐。',
3257
+ `如果你准备宣称“本次调整已经整体完成”或“项目级完成”,请先补齐:${missing.join(';')}。`,
3258
+ '未补齐前,只能如实说“代码或局部实现已完成,但项目级收口还没做完”,不要把缺少最终验证的状态表述成已完全闭环。',
3259
+ ].join('\n');
3260
+ }
3261
+
2583
3262
  function hookSuppressed(root) {
2584
3263
  const state = readJsonSync(path.join(harnessDir(root), 'hook-state.json'), {});
2585
3264
  const lockPath = path.join(harnessDir(root), 'input-lock.json');
@@ -2639,6 +3318,9 @@ function handle(eventName, cwd, payload) {
2639
3318
  let weappGate = readNamedGate(root, 'weapp', sessionId);
2640
3319
  const shortAffirmative = isShortAffirmativeConfirmation(prompt);
2641
3320
  let progress = isBlockingRequirementGate(gate) ? evaluateRequirementGateProgress(root, sessionId) : null;
3321
+ const clarificationFollowup = isBlockingRequirementGate(gate)
3322
+ ? looksLikeClarificationFollowupReply(prompt, intent)
3323
+ : false;
2642
3324
  if (isBlockingRequirementGate(gate) && shortAffirmative) {
2643
3325
  intent = {
2644
3326
  ...intent,
@@ -2677,7 +3359,7 @@ function handle(eventName, cwd, payload) {
2677
3359
  progress = null;
2678
3360
  }
2679
3361
  if (isBlockingRequirementGate(gate)) {
2680
- if ((intent.confirmation || shortAffirmative) && progress?.nextStep === 'clarification-confirmation-required') {
3362
+ if ((intent.confirmation || shortAffirmative || clarificationFollowup) && progress?.nextStep === 'clarification-confirmation-required') {
2681
3363
  gate = holdRequirementGate(root, prompt, progress, sessionId, {
2682
3364
  status: 'clarification-confirmed',
2683
3365
  clarificationConfirmedAt: now(),
@@ -2755,6 +3437,12 @@ function handle(eventName, cwd, payload) {
2755
3437
  if (sessionId && isAllowedDuringRequirementGate(root, payload, gate, sessionId)) {
2756
3438
  mirrorRequirementGate(root, sessionId);
2757
3439
  }
3440
+ const authorizedReviewMutation = /openprd\s+review\b/i.test(commandText(payload))
3441
+ && /--mark\b/i.test(commandText(payload))
3442
+ && (
3443
+ isAuthorizedReviewMarkCommand(payload, gate)
3444
+ || isSilentRecordReviewMarkCommand(payload, gate, progress)
3445
+ );
2758
3446
  if (skillGate?.active && looksLikeSkillContractMutation(root, payload)) {
2759
3447
  appendEvent(root, { ...baseEvent, outcome: 'blocked-skill-visualization-gate' });
2760
3448
  recordRunHook(root, baseEvent, 'blocked-skill-visualization-gate');
@@ -2764,7 +3452,7 @@ function handle(eventName, cwd, payload) {
2764
3452
  'Before editing SKILL.md, AGENTS.md, or related skill workflow files, first output a color-coded Mermaid plan, summarize added/changed/unchanged/removed items, then wait for explicit user confirmation.',
2765
3453
  ].join('\n'));
2766
3454
  }
2767
- if (researchGate?.active && isMutationPayload(payload, risk)) {
3455
+ if (researchGate?.active && isMutationPayload(payload, risk) && !authorizedReviewMutation) {
2768
3456
  appendEvent(root, { ...baseEvent, outcome: 'blocked-research-gate' });
2769
3457
  recordRunHook(root, baseEvent, 'blocked-research-gate');
2770
3458
  updateHookState(root, baseEvent);
@@ -2808,7 +3496,13 @@ function handle(eventName, cwd, payload) {
2808
3496
  progress?.nextStep === 'implementation-ready'
2809
3497
  ? 'Do not edit implementation files until the user clearly asks to execute this reviewed requirement.'
2810
3498
  : writePathMutation && progress?.nextStep === 'clarification-confirmation-required'
2811
- ? 'Do not write requirement facts, classify, or synthesize yet. First summarize the requirement in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, wait for the user to confirm that summary, then continue the requirement write path.'
3499
+ ? 'Do not write requirement facts, classify, or synthesize yet. First summarize the requirement in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, use light lead sentences for 需求判断 and 需求理解 before sinking details into later bullets or tables, and if this is still a 0-to-1 or worth-doing discussion also surface 第一批最容易触达的社区或种子用户、你为什么算这个社区里的自己人、当前替代方案和痛点证据、先怎么手工交付、手工作战卡怎么写、能不能先用 spreadsheet / 表单 / no-code 跑起来、什么承诺才算真需求、有没有 10 个样本和更强付费信号、最低成本先验证什么、达到什么条件才允许产品化、增长阶段守什么纪律、以及验证阶段怎样先活下来; wait for the user to confirm that summary, then continue the requirement write path.'
3500
+ : writePathMutation && progress?.nextStep === 'clarification-writeback-required'
3501
+ ? 'Do not synthesize yet. The user has already confirmed the current summary or default choice, but current.json still lacks canonical write-back for this lane. Write the confirmed facts back with openprd capture using canonical field paths and source user-confirmed; do not fall back to generic clarify, and do not keep writing agent-inferred / project-derived clarification fields.'
3502
+ : writePathMutation && progress?.nextStep === 'product-type-classification-required'
3503
+ ? 'Do not synthesize yet. The current lane still lacks a confirmed product-type classification. Run openprd classify . so the lane scenario is locked before you synthesize.'
3504
+ : writePathMutation && progress?.nextStep === 'requirement-interview-required'
3505
+ ? 'Do not synthesize yet. The current lane still lacks required requirement facts. Fill the missing canonical fields with openprd capture, and only use openprd interview . to surface the remaining gaps before you synthesize.'
2812
3506
  : writePathMutation && progress?.nextStep === 'prd-synthesis-required'
2813
3507
  ? 'You may continue the requirement write path only within the confirmed summary: write back confirmed facts, classify if needed, synthesize the PRD, then proceed to review/change/tasks.'
2814
3508
  : progress?.nextStep === 'review-recording-required'
@@ -2816,13 +3510,72 @@ function handle(eventName, cwd, payload) {
2816
3510
  : 'Do not edit implementation files until the active approval policy is satisfied and the OpenPrd change has generated tasks.',
2817
3511
  silentRecord
2818
3512
  ? 'Decision-point order: because the user explicitly waived any confirmation stop, you may skip requirement-summary confirmation, write back requirement facts, synthesize the PRD, record the exact stable review artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.'
2819
- : 'Decision-point order: clarify the requirement, summarize it in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, wait for the user to confirm that requirement summary, write back only confirmed facts, synthesize the PRD, wait for a human review decision on the stable artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.',
3513
+ : 'Decision-point order: clarify the requirement, summarize it in chat using 需求判断 / 需求理解 / 功能范围 / 技术方案, prefer Markdown tables for 功能范围 and 技术方案, and if the ask still looks like 0-to-1 validation also surface 第一批最容易触达的社区或种子用户、你为什么算这个社区里的自己人、当前替代方案和痛点证据、先怎么手工交付、手工作战卡怎么写、能不能先用 spreadsheet / 表单 / no-code 跑起来、什么承诺才算真需求、有没有 10 个样本和更强付费信号、最低成本先验证什么、达到什么条件才允许产品化、增长阶段守什么纪律、以及验证阶段怎样先活下来; wait for the user to confirm that requirement summary, write back only confirmed facts, synthesize the PRD, wait for a human review decision on the stable artifact, generate the OpenPrd change, prepare the task breakdown, then implement within the confirmed scope.',
2820
3514
  ].join('\n');
2821
3515
  appendEvent(root, { ...baseEvent, outcome: 'blocked-requirement-intake' });
2822
3516
  recordRunHook(root, baseEvent, 'blocked-requirement-intake');
2823
3517
  updateHookState(root, baseEvent);
2824
3518
  return blockHook(reason);
2825
3519
  }
3520
+ let patchModeGate = syncPatchModeGate(root, payload, sessionId, turnIntent);
3521
+ if (patchModeGate?.active) {
3522
+ if (isPatchModeWriteAttempt(root, payload, patchModeGate)) {
3523
+ patchModeGate = updateNamedGate(root, 'patch-mode', {
3524
+ status: 'write-attempted',
3525
+ lastWriteAttemptAt: now(),
3526
+ lastWriteAttemptAtMs: Date.now(),
3527
+ lastWriteAttemptTool: toolName(payload) || null,
3528
+ lastWriteAttemptFiles: extractTouchedFiles(root, payload),
3529
+ }, sessionId) || patchModeGate;
3530
+ } else if (patchModeGate.phase === 'handoff' && isPatchModeFocusAttempt(root, payload, patchModeGate)) {
3531
+ const nextRemaining = Math.max(0, Number(patchModeGate.focusAllowanceRemaining ?? PATCH_MODE_HANDOFF_FOCUS_ALLOWANCE) - 1);
3532
+ patchModeGate = updateNamedGate(root, 'patch-mode', {
3533
+ status: nextRemaining > 0 ? 'handoff-focus-read' : 'handoff-awaiting-entry-write',
3534
+ focusAllowanceRemaining: nextRemaining,
3535
+ lastFocusAt: now(),
3536
+ lastFocusAtMs: Date.now(),
3537
+ lastFocusTool: toolName(payload) || null,
3538
+ lastFocusFiles: extractTouchedFiles(root, payload),
3539
+ }, sessionId) || patchModeGate;
3540
+ } else {
3541
+ appendEvent(root, {
3542
+ ...baseEvent,
3543
+ outcome: 'blocked-patch-mode-hover',
3544
+ targetFile: patchModeGate.targetFile,
3545
+ });
3546
+ recordRunHook(root, baseEvent, 'blocked-patch-mode-hover');
3547
+ updateHookState(root, baseEvent);
3548
+ if (patchModeGate.phase === 'handoff') {
3549
+ return blockHook([
3550
+ 'OpenPrd blocked a post-starter hover because Patch Mode handoff is already active.',
3551
+ `design-starter has already landed ${patchModeGate.targetFile} and the active design contracts. You may do at most one immediate orientation pass over the generated entry and the active contracts, then the next action must be a real write to that entry file or one of its sibling drafts.`,
3552
+ `Remaining orientation reads: ${Math.max(0, Number(patchModeGate.focusAllowanceRemaining ?? 0))}.`,
3553
+ `Allowed orientation files: ${patchModeFocusFiles(root, patchModeGate).join(', ')}.`,
3554
+ `Allowed write files: ${patchModeTargetFiles(patchModeGate).join(', ')}.`,
3555
+ 'Do not go back to web search, docs/basic, template scans, or another shell-only loop after starter output has landed.',
3556
+ ].join('\n'));
3557
+ }
3558
+ return blockHook([
3559
+ 'OpenPrd blocked a non-writing tool call because Patch Mode is already in the entry-overwrite stage.',
3560
+ `You already announced an entry rewrite/overwrite step for ${patchModeGate.targetFile}. The very next action must be a real write to that entry file or one of its sibling drafts, not more reading, scanning, image handling, or shell exploration.`,
3561
+ `Allowed next files: ${patchModeTargetFiles(patchModeGate).join(', ')}.`,
3562
+ 'If the user gave a mockup or reference image, keep it as the style judge, but apply that judgment inside the actual page write instead of pausing in another read-only loop.',
3563
+ ].join('\n'));
3564
+ }
3565
+ }
3566
+ const frontendTargets = frontendImplementationTargets(root, payload);
3567
+ const designIssues = frontendDesignPreflightIssues(root, turnIntent, frontendTargets);
3568
+ if (designIssues.length > 0) {
3569
+ appendEvent(root, { ...baseEvent, outcome: 'blocked-frontend-design-preflight' });
3570
+ recordRunHook(root, baseEvent, 'blocked-frontend-design-preflight');
3571
+ updateHookState(root, baseEvent);
3572
+ return blockHook([
3573
+ 'OpenPrd blocked a frontend implementation write because the design-preflight contract is still incomplete.',
3574
+ frontendTargets.length > 0 ? `Trying to edit: ${frontendTargets.join(', ')}.` : null,
3575
+ 'Before touching implementation files, update the required files under `.openprd/design/active/` and remove every `待填写` placeholder. If something truly does not apply, write `不适用` or a clear gap note instead of leaving the template unchanged.',
3576
+ ...designIssues.map((issue) => `- ${issue.file}: ${issue.reason}`),
3577
+ ].filter(Boolean).join('\n'));
3578
+ }
2826
3579
  if (turnIntent.browserSafetyRequest && isHighRiskBrowserAction(payload)) {
2827
3580
  appendEvent(root, { ...baseEvent, outcome: 'browser-safety-reminder' });
2828
3581
  recordRunHook(root, baseEvent, 'browser-safety-reminder');
@@ -2845,7 +3598,7 @@ function handle(eventName, cwd, payload) {
2845
3598
  recordRunHook(root, baseEvent, 'allowed-medium-risk');
2846
3599
  updateHookState(root, baseEvent);
2847
3600
  recordTouchedFiles(root, payload);
2848
- return allowHook('OpenPrd 检测到写入动作。本轮写入完成后、最终回复前,请针对实际 touched code files 运行 openprd dev-check . <file...>;如出现需要关注的文件,最终回复必须以 **后续建议** 为标题,直接复用 dev-check 生成的 Markdown 表格,说明影响对象、关注程度、规模信号、预警原因、本次处理结果和后续建议,并按 🔴 → 🟠 → 🟡 排序;不要把“关注程度”列改写成纯 emoji,必须保留例如“🟠 中风险|建议优先关注”这类完整标签;如果你改写了“预警原因 / 本次处理结果 / 后续建议”,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试;如涉及界面视觉且已有参考效果图并进入实现阶段,阶段性完成后运行 openprd visual-compare . --reference <效果图> --actual <实现截图> 并查看 JPG 对比图;若局部细节更重要,再补 openprd visual-compare . --board <focus-board.json>;如无参考图但改动界面,确认已先截修改前截图,并在完成后运行 openprd visual-compare . --before <修改前截图> --after <修改后截图> 查看 JPG 自检图;若并行试了多个优化方向,再补 openprd visual-compare . --board <parallel-board.json>;发现可沉淀项时不要中途打断任务,代码扩展识别这类白名单工具补全会自动应用并记录,用户偏好、项目协作规矩和 OpenPrd 默认行为留到收工时用 openprd grow . --review 集中确认;维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;声明就绪前,请同步维护 docs/basic、文件说明书、文件夹 README,以及相关 OpenPrd change/task 状态;如果涉及后端、脚本、Agent、工具链、服务或数据处理变更,还要把 CLI 与 API 视为同级接入面并更新 docs/basic/backend-structure.md。');
3601
+ return allowHook('OpenPrd 检测到写入动作。本轮写入完成后、最终回复前,请针对实际 touched code files 运行 openprd dev-check . <file...>;如出现需要关注的文件,最终回复必须以 **后续建议** 为标题,直接复用 dev-check 生成的 Markdown 表格,说明影响对象、关注程度、规模信号、预警原因、本次处理结果和后续建议,并按 🔴 → 🟠 → 🟡 排序;不要把“关注程度”列改写成纯 emoji,必须保留例如“🟠 中风险|建议优先关注”这类完整标签;如果你改写了“预警原因 / 本次处理结果 / 后续建议”,先用 `node scripts/dev-check-wrapup-copy.mjs --validate` 校验每格不超过 20 字;若报错,按提示缩短后重试;如涉及界面视觉且已有参考效果图并进入实现阶段,阶段性完成后运行 openprd visual-compare . --reference <效果图> --actual <实现截图> 并查看 JPG 对比图;如果参考图来自本轮或前序 `imagegen`,先确认用户已接受它作为后续对比参考;若一张图里有多个子图、对象或网格,先运行 openprd visual-prepare . --reference <效果图> --grid <列>x<行> 或 --boxes <plan.json>,确认 contact sheet 后再逐项对比。若局部细节更重要,再补 openprd visual-compare . --board <focus-board.json>;如无参考图,先判断新建界面还是修改既有界面:新建界面应先完成 3 方向方案评审,修改既有界面确认已先截修改前截图,并在完成后运行 openprd visual-compare . --before <修改前截图> --after <修改后截图> 查看 JPG 自检图;若并行试了多个优化方向,再补 openprd visual-compare . --board <parallel-board.json>;当用户说“跟效果图”“不一致”“好丑”“复刻”时,至少给出一份视觉证据图,不要只口头判断。发现可沉淀项时不要中途打断任务,代码扩展识别这类白名单工具补全会自动应用并记录,用户偏好、项目协作规矩和 OpenPrd 默认行为留到收工时用 openprd grow . --review 集中确认;维护 OpenPrd 本身且涉及配置类能力时,先判断是否应纳入 openprd grow;声明就绪前,请同步维护 docs/basic、文件说明书、文件夹 README,以及相关 OpenPrd change/task 状态;如果涉及后端、脚本、Agent、工具链、服务或数据处理变更,还要把 CLI 与 API 视为同级接入面并更新 docs/basic/backend-structure.md。');
2849
3602
  }
2850
3603
  return allowHook();
2851
3604
  }
@@ -2886,12 +3639,39 @@ function handle(eventName, cwd, payload) {
2886
3639
  return allowHook([
2887
3640
  'OpenPrd 生图事实对齐提醒:如果这轮要汇报图片结果、失败或限流,请确保它来自一次实际的 `imagegen` 调用。',
2888
3641
  '未实际调用 `imagegen` 前,不要声称“生图限流”“生图失败”或“已经生成图片结果”;若本轮还没调用,请如实说明仍未开始或尚未完成生图。',
3642
+ '如果这轮确实已经生图,不要默认把结果登记到 `.openprd/harness/visual-reviews/` 或直接当成验收参考;先把它当候选效果图。',
3643
+ '若用户还要继续做实现,主动追问三件事:是否符合预期、是否纳入后续效果图/实现截图对比、是否按此继续后续实现;未确认前不要自动进入实现,也不要声称参考图已定。',
2889
3644
  ].join('\n'));
2890
3645
  }
3646
+ const patchModeGate = readNamedGate(root, 'patch-mode', sessionId);
3647
+ if (patchModeGate?.active) {
3648
+ if (isPatchModeWriteObserved(root, patchModeGate)) {
3649
+ closePatchModeGate(root, sessionId, {
3650
+ status: 'write-observed',
3651
+ writeObservedAt: now(),
3652
+ });
3653
+ } else if (patchModeGate.phase === 'handoff') {
3654
+ return allowHook([
3655
+ 'OpenPrd 在本轮收工回顾里发现 design-starter 已经落地,但 Patch Mode handoff 还没真正收口。',
3656
+ `当前应先把后续真实页面继续写到 ${patchModeGate.targetFile} 或它的 sibling draft(${(patchModeGate.draftFiles || []).join(', ')})之一,再继续汇报完成。`,
3657
+ `如果只是读了入口文件或 active design contracts,那还只算就地对焦,不算真正落盘;starter 之后不要再回头搜网页、翻 docs/basic 或继续模板漫游。`,
3658
+ ].join('\n'));
3659
+ } else {
3660
+ return allowHook([
3661
+ 'OpenPrd 在本轮收工回顾里发现 Patch Mode 还停在“已宣布开始覆盖,但入口文件还没真正落盘”的状态。',
3662
+ `当前应先把真实写入动作落到 ${patchModeGate.targetFile} 或它的 sibling draft(${(patchModeGate.draftFiles || []).join(', ')})之一,再继续汇报完成。`,
3663
+ '如果用户给了效果图或参考图,继续以它为准,但这个判断必须落实在实际页面写入里;只补说明、只补合同、只下载素材或只说“准备开始改”都不算完成。',
3664
+ ].join('\n'));
3665
+ }
3666
+ }
2891
3667
  const devCheckMessage = devCheckWrapUpMessage(root, turnState);
2892
3668
  if (devCheckMessage) {
2893
3669
  return allowHook(devCheckMessage);
2894
3670
  }
3671
+ const closeoutReminder = closeoutVerificationReminder(root, turnState, stopIntent);
3672
+ if (closeoutReminder) {
3673
+ return allowHook(closeoutReminder);
3674
+ }
2895
3675
  if (Array.isArray(turnState.touchedFiles) && turnState.touchedFiles.length > 0) {
2896
3676
  const review = runOpenPrd(['quality', '.', '--learn', '--review', '--from', '.openprd/harness/turn-state.json', '--json'], root);
2897
3677
  if (review.ok) {