@openprd/cli 0.1.10 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.openprd/benchmarks/index.md +17 -15
  2. package/.openprd/benchmarks/sources.yaml +53 -1
  3. package/.openprd/changes/adaptive-project-framing/.openprd.yaml +2 -0
  4. package/.openprd/changes/adaptive-project-framing/design.md +47 -0
  5. package/.openprd/changes/adaptive-project-framing/proposal.md +35 -0
  6. package/.openprd/changes/adaptive-project-framing/specs/agent-requirements/spec.md +16 -0
  7. package/.openprd/changes/adaptive-project-framing/task-events.jsonl +20 -0
  8. package/.openprd/changes/adaptive-project-framing/tasks.md +340 -0
  9. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/.openprd.yaml +2 -0
  10. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/design.md +34 -0
  11. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/proposal.md +23 -0
  12. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/specs/agent-requirements/spec.md +16 -0
  13. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/task-events.jsonl +14 -0
  14. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/tasks.md +238 -0
  15. package/.openprd/changes/loop-isolated-worktree-commit/.openprd.yaml +2 -0
  16. package/.openprd/changes/loop-isolated-worktree-commit/design.md +53 -0
  17. package/.openprd/changes/loop-isolated-worktree-commit/proposal.md +38 -0
  18. package/.openprd/changes/loop-isolated-worktree-commit/specs/agent-requirements/spec.md +16 -0
  19. package/.openprd/changes/loop-isolated-worktree-commit/task-events.jsonl +22 -0
  20. package/.openprd/changes/loop-isolated-worktree-commit/tasks.md +357 -0
  21. package/.openprd/changes/openprd/.openprd.yaml +2 -0
  22. package/.openprd/changes/openprd/design.md +46 -0
  23. package/.openprd/changes/openprd/proposal.md +36 -0
  24. package/.openprd/changes/openprd/specs/agent-requirements/spec.md +16 -0
  25. package/.openprd/changes/openprd/task-events.jsonl +23 -0
  26. package/.openprd/changes/openprd/tasks.md +242 -0
  27. package/.openprd/changes/pm-human-in-the-loop/.openprd.yaml +2 -0
  28. package/.openprd/changes/pm-human-in-the-loop/design.md +38 -0
  29. package/.openprd/changes/pm-human-in-the-loop/proposal.md +29 -0
  30. package/.openprd/changes/pm-human-in-the-loop/specs/agent-requirements/spec.md +16 -0
  31. package/.openprd/changes/pm-human-in-the-loop/task-events.jsonl +18 -0
  32. package/.openprd/changes/pm-human-in-the-loop/tasks.md +306 -0
  33. package/.openprd/changes/test-strategy-router/.openprd.yaml +2 -0
  34. package/.openprd/changes/test-strategy-router/design.md +65 -0
  35. package/.openprd/changes/test-strategy-router/proposal.md +45 -0
  36. package/.openprd/changes/test-strategy-router/specs/agent-requirements/spec.md +16 -0
  37. package/.openprd/changes/test-strategy-router/task-events.jsonl +1 -0
  38. package/.openprd/changes/test-strategy-router/tasks.md +132 -0
  39. package/.openprd/changes/verify-boundaries-and-historical-refresh/.openprd.yaml +2 -0
  40. package/.openprd/changes/verify-boundaries-and-historical-refresh/design.md +28 -0
  41. package/.openprd/changes/verify-boundaries-and-historical-refresh/proposal.md +23 -0
  42. package/.openprd/changes/verify-boundaries-and-historical-refresh/specs/agent-requirements/spec.md +16 -0
  43. package/.openprd/changes/verify-boundaries-and-historical-refresh/task-events.jsonl +11 -0
  44. package/.openprd/changes/verify-boundaries-and-historical-refresh/tasks.md +66 -0
  45. package/.openprd/changes/vibe-coding-dev-check/.openprd.yaml +2 -0
  46. package/.openprd/changes/vibe-coding-dev-check/design.md +37 -0
  47. package/.openprd/changes/vibe-coding-dev-check/proposal.md +28 -0
  48. package/.openprd/changes/vibe-coding-dev-check/specs/agent-requirements/spec.md +16 -0
  49. package/.openprd/changes/vibe-coding-dev-check/task-events.jsonl +18 -0
  50. package/.openprd/changes/vibe-coding-dev-check/tasks.md +306 -0
  51. package/.openprd/design/README.md +11 -6
  52. package/.openprd/design/README_EN.md +4 -1
  53. package/.openprd/design/active/asset-spec.md +6 -0
  54. package/.openprd/design/active/direction-plan.md +5 -5
  55. package/.openprd/design/active/selected-direction.md +2 -0
  56. package/.openprd/design/anti-slop.md +26 -1
  57. package/.openprd/design/assets/surface-presets.md +6 -0
  58. package/.openprd/design/checklists/ui-quality-gate.md +7 -0
  59. package/.openprd/design/lenses/frontend-lenses.md +28 -0
  60. package/.openprd/design/recipes/content-experience.md +8 -1
  61. package/.openprd/design/recipes/operational-tool.md +7 -0
  62. package/.openprd/design/recipes/product-launch.md +8 -1
  63. package/.openprd/design/templates/README.md +6 -3
  64. package/.openprd/design/templates/README_EN.md +6 -3
  65. package/.openprd/design/templates/ops-dashboard.html +8 -1
  66. package/.openprd/design/templates/product-launch.html +2 -1
  67. package/.openprd/design/themes/theme-catalog.json +23 -5
  68. package/.openprd/engagements/active/prd.md +103 -100
  69. package/.openprd/schema/prd.schema.yaml +15 -0
  70. package/.openprd/standards/config.json +7 -1
  71. package/.openprd/templates/base/intake.md +13 -0
  72. package/.openprd/templates/base/prd.md +15 -0
  73. package/AGENTS.md +14 -4
  74. package/README.md +107 -14
  75. package/README_EN.md +131 -13
  76. package/package.json +1 -1
  77. package/scripts/dev-check-wrapup-copy.mjs +46 -21
  78. package/skills/openprd-benchmark-router/SKILL.md +2 -1
  79. package/skills/openprd-diagram-review/SKILL.md +27 -2
  80. package/skills/openprd-diagram-review/references/explanation-svg-patterns.md +131 -0
  81. package/skills/openprd-discovery-loop/SKILL.md +2 -2
  82. package/skills/openprd-frontend-design/SKILL.md +44 -20
  83. package/skills/openprd-frontend-design/references/design-asset-contract.md +6 -0
  84. package/skills/openprd-frontend-design/references/direction-engine.md +29 -4
  85. package/skills/openprd-harness/SKILL.md +22 -16
  86. package/skills/openprd-quality/SKILL.md +10 -8
  87. package/skills/openprd-requirement-intake/SKILL.md +5 -3
  88. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +2 -2
  89. package/skills/openprd-requirement-intake/references/startup-validation-lens.md +98 -13
  90. package/skills/openprd-router/SKILL.md +2 -1
  91. package/skills/openprd-shared/SKILL.md +27 -17
  92. package/skills/openprd-standards/SKILL.md +4 -2
  93. package/src/agent-canonical-content.js +808 -0
  94. package/src/agent-integration.js +259 -778
  95. package/src/brainstorm-artifacts.js +156 -8
  96. package/src/brainstorm-presentation.js +14 -4
  97. package/src/brainstorm.js +165 -2
  98. package/src/canvas-app.html.js +2910 -0
  99. package/src/canvas-i18n.js +274 -0
  100. package/src/canvas-workspace.js +2271 -0
  101. package/src/cli/args.js +72 -4
  102. package/src/cli/basic-print.js +3 -0
  103. package/src/cli/canvas-print.js +106 -0
  104. package/src/cli/doctor-print.js +20 -0
  105. package/src/cli/print.js +2 -0
  106. package/src/cli/quality-commands.js +222 -0
  107. package/src/cli/quality-print.js +37 -5
  108. package/src/cli/run-print.js +5 -0
  109. package/src/cli/workflow-print.js +3 -0
  110. package/src/codex-app-server-relay.js +251 -0
  111. package/src/codex-hook-runner-template.mjs +963 -65
  112. package/src/design-starter-support.js +2 -2
  113. package/src/design-starter.js +39 -3
  114. package/src/dev-standards.js +125 -10
  115. package/src/diagram-core.js +390 -171
  116. package/src/discovery.js +2 -8
  117. package/src/docs-compact.js +125 -0
  118. package/src/execution-strategy.js +1 -1
  119. package/src/fleet.js +9 -3
  120. package/src/guide.js +227 -0
  121. package/src/html-artifacts.js +825 -51
  122. package/src/knowledge.js +101 -20
  123. package/src/language-policy.js +63 -3
  124. package/src/learning-review.js +1 -1
  125. package/src/loop.js +1 -1
  126. package/src/openprd.js +68 -131
  127. package/src/openspec/change-validate.js +2 -2
  128. package/src/openspec/constants.js +6 -7
  129. package/src/openspec/execute.js +2 -2
  130. package/src/openspec/generate.js +318 -102
  131. package/src/openspec/migration.js +168 -0
  132. package/src/openspec/paths.js +1 -22
  133. package/src/prd-core.js +102 -3
  134. package/src/quality-html-artifact.js +30 -5
  135. package/src/quality-visual-review.js +257 -14
  136. package/src/quality.js +66 -18
  137. package/src/review-presentation.js +60 -0
  138. package/src/run-harness.js +41 -3
  139. package/src/session-evidence.js +363 -0
  140. package/src/standards.js +64 -0
  141. package/src/test-strategy.js +7 -4
  142. package/src/visual-compare-centering.js +622 -0
  143. package/src/visual-compare-core.js +1123 -0
  144. package/src/visual-compare.js +620 -698
  145. package/src/workspace-core.js +132 -4
  146. package/src/workspace-workflow.js +172 -10
@@ -19,6 +19,9 @@ const OPENPRD_HARNESS_RUN_STATE = cjoin(OPENPRD_HARNESS_DIR, 'run-state.json');
19
19
  const OPENPRD_HARNESS_ITERATIONS = cjoin(OPENPRD_HARNESS_DIR, 'iterations.jsonl');
20
20
  const OPENPRD_HARNESS_LEARNINGS = cjoin(OPENPRD_HARNESS_DIR, 'learnings.md');
21
21
  const OPENPRD_HARNESS_LOOP_FEATURE_LIST = cjoin(OPENPRD_HARNESS_DIR, 'feature-list.json');
22
+ const OPENPRD_HARNESS_INSTALL_MANIFEST = cjoin(OPENPRD_HARNESS_DIR, 'install-manifest.json');
23
+ const OPENPRD_HARNESS_HOOK_STATE = cjoin(OPENPRD_HARNESS_DIR, 'hook-state.json');
24
+ const OPENPRD_HARNESS_RUNTIME_ENVIRONMENT = cjoin(OPENPRD_HARNESS_DIR, 'runtime-environment.json');
22
25
  const OPENPRD_HARNESS_REQUIREMENT_GATE = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gate.json');
23
26
  const OPENPRD_HARNESS_REQUIREMENT_GATES_DIR = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gates');
24
27
  const OPENPRD_HARNESS_SESSION_BINDINGS_DIR = cjoin(OPENPRD_HARNESS_DIR, 'session-bindings');
@@ -82,6 +85,34 @@ async function ensureRunHarness(projectRoot) {
82
85
  }
83
86
  }
84
87
 
88
+ async function readRuntimeEnvironmentContext(projectRoot) {
89
+ const [runtimeEnvironment, installManifest, hookState] = await Promise.all([
90
+ readJson(harnessFile(projectRoot, OPENPRD_HARNESS_RUNTIME_ENVIRONMENT)).catch(() => null),
91
+ readJson(harnessFile(projectRoot, OPENPRD_HARNESS_INSTALL_MANIFEST)).catch(() => null),
92
+ readJson(harnessFile(projectRoot, OPENPRD_HARNESS_HOOK_STATE)).catch(() => null),
93
+ ]);
94
+ const active = runtimeEnvironment?.active
95
+ ?? hookState?.runtimeEnvironment
96
+ ?? {
97
+ activeClient: 'unknown',
98
+ surface: 'unknown',
99
+ confidence: 0,
100
+ sessionId: null,
101
+ evidence: [],
102
+ observedCapabilities: [],
103
+ };
104
+ return {
105
+ active,
106
+ runtimeDetection: installManifest?.runtimeDetection ?? runtimeEnvironment?.runtimeDetection ?? null,
107
+ platformCapabilityPacks: installManifest?.platformCapabilityPacks ?? runtimeEnvironment?.platformCapabilityPacks ?? [],
108
+ files: {
109
+ runtimeEnvironment: OPENPRD_HARNESS_RUNTIME_ENVIRONMENT,
110
+ installManifest: OPENPRD_HARNESS_INSTALL_MANIFEST,
111
+ hookState: OPENPRD_HARNESS_HOOK_STATE,
112
+ },
113
+ };
114
+ }
115
+
85
116
  async function readRunState(projectRoot) {
86
117
  await ensureRunHarness(projectRoot);
87
118
  return readJson(harnessFile(projectRoot, OPENPRD_HARNESS_RUN_STATE)).catch(() => ({
@@ -225,7 +256,7 @@ function executionGate() {
225
256
  confirmationChecklistRequired: true,
226
257
  allowedIntents: ['开发', '实现', '修复', '继续任务', '落地执行', '深度调研', '深度对标', '复刻落地', '提交'],
227
258
  readOnlyIntents: ['看看', '规划', '梳理', '分析', '评估', '预计动哪些文件', '怎么改', '代码审查'],
228
- rule: '只有当用户当前明确要求实现、继续、深度调研、对标或提交时,才运行 executionCommand。单纯的“请帮我实现/继续实现”只表示有执行意图,不表示可以跳过 requirement 摘要确认、`capture/classify/synthesize` 写入路径或 review;只有用户明确表示“不需要进行任何确认”时,才允许静默走完整 requirement write path,并对当前精确匹配的稳定 review artifact 记录确认。若还需要向用户索取执行授权,先展示 executionConfirmationChecklist,再请求明确确认;规划、分析、文件影响范围和审查类请求保持只读,并基于证据回答。',
259
+ rule: '只有当用户当前明确要求实现、继续、深度调研、对标或提交时,才运行 executionCommand。单纯的“请帮我实现/继续实现”只表示有执行意图,不表示可以跳过 requirement 摘要确认、`capture/classify/synthesize` 写入路径或 review;只有用户明确表示“不需要进行任何确认”时,才允许静默走完整 requirement write path,并对当前精确匹配的稳定 review artifact 记录确认。即使用户授权实现,也不等于授权云端热修复、生产远端写入、业务兜底、写死逻辑或缓存补行;这些动作必须由当前用户明确批准,并说明如何同步回本地源码、配置或迁移。若还需要向用户索取执行授权,先展示 executionConfirmationChecklist,再请求明确确认;规划、分析、文件影响范围和审查类请求保持只读,并基于证据回答。',
229
260
  };
230
261
  }
231
262
 
@@ -251,6 +282,7 @@ function buildExecutionConfirmationChecklist(recommendation) {
251
282
  outOfScope: [
252
283
  '不会默认处理这次范围以外的历史问题。',
253
284
  '不会默认顺带做提交、发布或额外的全局调整。',
285
+ '不会默认做云端热修复、生产远端写入、业务兜底、写死逻辑或缓存补行。',
254
286
  '如果真的需要扩大范围,我会先单独说明。',
255
287
  ].filter(Boolean),
256
288
  verification: [
@@ -261,6 +293,7 @@ function buildExecutionConfirmationChecklist(recommendation) {
261
293
  risks: [
262
294
  '这一步会正式写入当前工作区。',
263
295
  '如果牵出和本次无关的历史遗留问题,我会单列说明,不把它混成本次失败。',
296
+ '如果确实需要临时兜底或线上热修复,我会先单独说明影响、回滚和本地同步计划,等你明确批准。',
264
297
  parallelPlan?.eligible ? '如果多人同时推进,我会避免范围互相踩踏。' : null,
265
298
  ],
266
299
  confirmationPrompt: '如果你希望我现在就按这次范围继续,我就直接往下做。',
@@ -1816,6 +1849,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
1816
1849
  await ensureRunHarness(projectRoot);
1817
1850
  const runState = await readRunState(projectRoot);
1818
1851
  const currentTurn = await readJson(harnessFile(projectRoot, OPENPRD_HARNESS_TURN_STATE)).catch(() => null);
1852
+ const runtimeEnvironment = await readRuntimeEnvironmentContext(projectRoot);
1819
1853
  const currentTurnPrompt = String(currentTurn?.prompt ?? '').trim();
1820
1854
  const fallbackPrompt = !options.hookInject
1821
1855
  ? (inferPromptDrivenLightweightRecommendation(currentTurnPrompt) ? currentTurnPrompt : null)
@@ -1942,8 +1976,8 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
1942
1976
  activeChange,
1943
1977
  nextTaskTitle: nextTask?.title,
1944
1978
  relatedFiles: [
1945
- focusedChangeId ? `openprd/changes/${focusedChangeId}/tasks.md` : null,
1946
- activeChange ? `openprd/changes/${activeChange}` : null,
1979
+ focusedChangeId ? `.openprd/changes/${focusedChangeId}/tasks.md` : null,
1980
+ activeChange ? `.openprd/changes/${activeChange}` : null,
1947
1981
  ].filter(Boolean),
1948
1982
  limit: options.hookInject ? 4 : 3,
1949
1983
  }).catch(() => ({ matched: [], summary: { matched: 0 } }));
@@ -2035,6 +2069,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
2035
2069
  : null,
2036
2070
  lane,
2037
2071
  recommendation: effectiveRecommendation,
2072
+ runtimeEnvironment,
2038
2073
  knowledgeSkills: {
2039
2074
  matched: renderedKnowledgeSkills,
2040
2075
  mandatoryCheck: knowledgeSkillMatches.mandatoryCheck ?? null,
@@ -2050,6 +2085,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
2050
2085
  runState: OPENPRD_HARNESS_RUN_STATE,
2051
2086
  iterations: OPENPRD_HARNESS_ITERATIONS,
2052
2087
  learnings: OPENPRD_HARNESS_LEARNINGS,
2088
+ runtimeEnvironment: OPENPRD_HARNESS_RUNTIME_ENVIRONMENT,
2053
2089
  },
2054
2090
  };
2055
2091
 
@@ -2065,6 +2101,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
2065
2101
  async function recordRunHook(projectRoot, options = {}) {
2066
2102
  await ensureRunHarness(projectRoot);
2067
2103
  const state = await readRunState(projectRoot);
2104
+ const runtimeEnvironment = await readRuntimeEnvironmentContext(projectRoot);
2068
2105
  const currentIteration = Number(state.currentIteration ?? 0) + 1;
2069
2106
  const event = {
2070
2107
  version: 1,
@@ -2075,6 +2112,7 @@ async function recordRunHook(projectRoot, options = {}) {
2075
2112
  risk: options.risk ?? 'unknown',
2076
2113
  outcome: options.outcome ?? 'unknown',
2077
2114
  preview: options.preview ?? null,
2115
+ runtimeEnvironment: runtimeEnvironment.active,
2078
2116
  };
2079
2117
  await appendJsonl(harnessFile(projectRoot, OPENPRD_HARNESS_ITERATIONS), event);
2080
2118
  await writeRunState(projectRoot, {
@@ -0,0 +1,363 @@
1
+ /*
2
+ * 核心功能
3
+ * 从 Codex 原始会话事件中抽取可审查证据,并隔离系统提示、AGENTS 和工具 schema 噪声。
4
+ *
5
+ * 输入
6
+ * 接收 JSONL 事件对象、消息对象或文本片段。
7
+ *
8
+ * 输出
9
+ * 输出带 sourceKind 的 evidence records,以及供 knowledge matching 使用的清理后文本。
10
+ *
11
+ * 定位
12
+ * 位于会话证据层,服务 daily review、knowledge review 和项目经验召回,不直接读写工作区。
13
+ *
14
+ * 依赖
15
+ * 仅依赖标准 JavaScript,不依赖 CLI、文件系统或 OpenPrd workspace 状态。
16
+ *
17
+ * 维护规则
18
+ * 这里负责证据卫生,不负责判断需求类型;新增来源时先补 sourceKind 和污染样例测试。
19
+ */
20
+
21
+ const CONTEXT_ONLY_PATTERNS = [
22
+ /\bAGENTS\.md instructions\b/i,
23
+ /<INSTRUCTIONS>/i,
24
+ /<\/INSTRUCTIONS>/i,
25
+ /<environment_context>/i,
26
+ /# Tools\b/i,
27
+ /## Skills\b/i,
28
+ /### Available skills\b/i,
29
+ /\bnamespace\s+\w+\s*\{/i,
30
+ /\btool schema\b/i,
31
+ /\bYou are Codex\b/i,
32
+ /\bOpenPrd Harness\b/i,
33
+ /\bproject-doc\b/i,
34
+ ];
35
+
36
+ const CONTEXT_ROLE_SET = new Set(['system', 'developer']);
37
+
38
+ const GENERIC_FRAMEWORK_TERMS = new Set([
39
+ 'agent',
40
+ 'agents',
41
+ 'agentruntime',
42
+ 'codex',
43
+ 'context',
44
+ 'developer',
45
+ 'hook',
46
+ 'hooks',
47
+ 'harness',
48
+ 'instruction',
49
+ 'instructions',
50
+ 'openprd',
51
+ 'schema',
52
+ 'skill',
53
+ 'skills',
54
+ 'system',
55
+ 'tool',
56
+ 'tools',
57
+ 'workflow',
58
+ 'workflows',
59
+ '项目级',
60
+ '项目经验',
61
+ '沉淀',
62
+ ]);
63
+
64
+ function normalizeWhitespace(value) {
65
+ return String(value ?? '').replace(/\s+/g, ' ').trim();
66
+ }
67
+
68
+ function trimEvidenceText(value, max = 1200) {
69
+ const text = normalizeWhitespace(value);
70
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
71
+ }
72
+
73
+ function normalizeEventType(event = {}) {
74
+ return String(
75
+ event.type
76
+ ?? event.event
77
+ ?? event.kind
78
+ ?? event.eventType
79
+ ?? event.name
80
+ ?? event.item?.type
81
+ ?? event.event_msg?.type
82
+ ?? '',
83
+ ).trim();
84
+ }
85
+
86
+ function normalizeRole(event = {}) {
87
+ return String(
88
+ event.role
89
+ ?? event.message?.role
90
+ ?? event.item?.role
91
+ ?? event.event_msg?.role
92
+ ?? '',
93
+ ).trim().toLowerCase();
94
+ }
95
+
96
+ function firstString(...values) {
97
+ for (const value of values) {
98
+ if (typeof value === 'string' && value.trim()) {
99
+ return value.trim();
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
105
+ function extractTextFromContent(content) {
106
+ if (typeof content === 'string') return content;
107
+ if (Array.isArray(content)) {
108
+ return content
109
+ .map((part) => firstString(part?.text, part?.content, part?.input, part?.output))
110
+ .filter(Boolean)
111
+ .join('\n');
112
+ }
113
+ if (content && typeof content === 'object') {
114
+ return firstString(content.text, content.content, content.input, content.output) ?? '';
115
+ }
116
+ return '';
117
+ }
118
+
119
+ function extractEventText(event = {}) {
120
+ if (typeof event === 'string') return event;
121
+ const eventMsg = event.event_msg ?? {};
122
+ const item = event.item ?? {};
123
+ const message = event.message ?? {};
124
+ return firstString(
125
+ event.text,
126
+ event.content,
127
+ event.output,
128
+ event.final_answer,
129
+ event.summary,
130
+ event.prompt,
131
+ event.user_message,
132
+ event.agent_message,
133
+ eventMsg.user_message,
134
+ eventMsg.agent_message,
135
+ eventMsg.final_answer,
136
+ eventMsg.summary,
137
+ eventMsg.message,
138
+ item.final_answer,
139
+ item.summary,
140
+ item.text,
141
+ item.output,
142
+ message.text,
143
+ extractTextFromContent(message.content),
144
+ extractTextFromContent(item.content),
145
+ ) ?? '';
146
+ }
147
+
148
+ function looksLikeContextOnly(text, event = {}) {
149
+ const role = normalizeRole(event);
150
+ if (CONTEXT_ROLE_SET.has(role)) return true;
151
+ return CONTEXT_ONLY_PATTERNS.some((pattern) => pattern.test(text));
152
+ }
153
+
154
+ function classifyEvent(event = {}, text = '') {
155
+ const role = normalizeRole(event);
156
+ const type = normalizeEventType(event).toLowerCase();
157
+ const eventMsg = event.event_msg ?? {};
158
+ const item = event.item ?? {};
159
+ if (looksLikeContextOnly(text, event)) {
160
+ return { sourceKind: 'context-only', reason: 'system-context' };
161
+ }
162
+ if (role === 'user' || eventMsg.user_message) {
163
+ return { sourceKind: 'user-direct', reason: 'user-message' };
164
+ }
165
+ if (
166
+ role === 'assistant'
167
+ || eventMsg.agent_message
168
+ || eventMsg.final_answer
169
+ || item.final_answer
170
+ || type.includes('final')
171
+ ) {
172
+ return { sourceKind: 'assistant-final', reason: 'assistant-message' };
173
+ }
174
+ if (
175
+ type.includes('task_complete')
176
+ || event.task_complete
177
+ || eventMsg.task_complete
178
+ || item.type === 'task_complete'
179
+ ) {
180
+ return { sourceKind: 'assistant-final', reason: 'task-complete' };
181
+ }
182
+ if (
183
+ type.includes('function_call')
184
+ || type.includes('tool')
185
+ || event.tool
186
+ || event.name
187
+ || item.name
188
+ ) {
189
+ return { sourceKind: 'tool-action', reason: 'tool-event' };
190
+ }
191
+ if (/\b(git status|npm test|node --test|openprd .*--verify|doctor)\b/i.test(text)) {
192
+ return { sourceKind: 'tool-action', reason: 'command-evidence' };
193
+ }
194
+ return { sourceKind: 'context-only', reason: 'unclassified' };
195
+ }
196
+
197
+ function normalizeEvidenceRecord(record = {}, index = 0) {
198
+ const text = trimEvidenceText(record.text);
199
+ if (!text) return null;
200
+ const sourceKind = String(record.sourceKind ?? 'context-only').trim() || 'context-only';
201
+ return {
202
+ sourceKind,
203
+ text,
204
+ reason: String(record.reason ?? '').trim() || null,
205
+ role: String(record.role ?? '').trim() || null,
206
+ eventType: String(record.eventType ?? '').trim() || null,
207
+ eventIndex: Number.isFinite(Number(record.eventIndex)) ? Number(record.eventIndex) : index,
208
+ confidence: Number.isFinite(Number(record.confidence)) ? Number(record.confidence) : (sourceKind === 'context-only' ? 0.2 : 0.8),
209
+ };
210
+ }
211
+
212
+ function extractSessionEvidenceRecords(events = [], options = {}) {
213
+ const maxRecords = Math.max(1, Number(options.maxRecords ?? 80));
214
+ return (Array.isArray(events) ? events : [events])
215
+ .map((event, index) => {
216
+ const text = extractEventText(event);
217
+ if (!normalizeWhitespace(text)) return null;
218
+ const classification = classifyEvent(event, text);
219
+ return normalizeEvidenceRecord({
220
+ ...classification,
221
+ text,
222
+ role: normalizeRole(event),
223
+ eventType: normalizeEventType(event),
224
+ eventIndex: index,
225
+ }, index);
226
+ })
227
+ .filter(Boolean)
228
+ .slice(0, maxRecords);
229
+ }
230
+
231
+ function normalizeSessionEvidenceRecords(records = []) {
232
+ return (Array.isArray(records) ? records : [records])
233
+ .map((record, index) => normalizeEvidenceRecord(record, index))
234
+ .filter(Boolean);
235
+ }
236
+
237
+ function cleanEvidenceRecords(records = [], options = {}) {
238
+ const includeContextOnly = options.includeContextOnly === true;
239
+ return normalizeSessionEvidenceRecords(records)
240
+ .filter((record) => includeContextOnly || record.sourceKind !== 'context-only');
241
+ }
242
+
243
+ function buildCleanEvidenceText(records = [], options = {}) {
244
+ const maxRecords = Math.max(1, Number(options.maxRecords ?? 24));
245
+ return cleanEvidenceRecords(records, options)
246
+ .slice(0, maxRecords)
247
+ .map((record) => `[${record.sourceKind}] ${record.text}`)
248
+ .join('\n');
249
+ }
250
+
251
+ function normalizeSearchText(value) {
252
+ return String(value ?? '')
253
+ .toLowerCase()
254
+ .replace(/[`"'()[\]{}:;,!?]/g, ' ')
255
+ .replace(/[_/\\.-]+/g, ' ')
256
+ .replace(/\s+/g, ' ')
257
+ .trim();
258
+ }
259
+
260
+ function sanitizeEvidenceText(value) {
261
+ const text = String(value ?? '');
262
+ if (!text.trim()) return '';
263
+ const lines = text.split(/\r?\n/);
264
+ const kept = [];
265
+ let skipBlock = false;
266
+ for (const line of lines) {
267
+ const startsContextBlock = /<INSTRUCTIONS>|<environment_context>|# Tools\b|### Available skills\b/i.test(line);
268
+ if (startsContextBlock) {
269
+ skipBlock = true;
270
+ continue;
271
+ }
272
+ if (skipBlock && /<\/INSTRUCTIONS>|<\/environment_context>/i.test(line)) {
273
+ skipBlock = false;
274
+ continue;
275
+ }
276
+ if (skipBlock) continue;
277
+ if (CONTEXT_ONLY_PATTERNS.some((pattern) => pattern.test(line))) continue;
278
+ kept.push(line);
279
+ }
280
+ return normalizeWhitespace(kept.join('\n'));
281
+ }
282
+
283
+ function isGenericFrameworkTerm(token) {
284
+ const normalized = normalizeSearchText(token);
285
+ return !normalized || GENERIC_FRAMEWORK_TERMS.has(normalized);
286
+ }
287
+
288
+ function filterGenericFrameworkTokens(tokens = []) {
289
+ return [...new Set(tokens.filter((token) => !isGenericFrameworkTerm(token)))];
290
+ }
291
+
292
+ function buildSessionEvidenceReviewSource(options = {}) {
293
+ const records = cleanEvidenceRecords(options.records ?? []);
294
+ const title = firstString(options.title, records.find((record) => record.sourceKind === 'user-direct')?.text, '只读复盘证据候选');
295
+ const sourceId = firstString(options.sourceId, title, 'session-evidence-review');
296
+ const sourcePath = firstString(options.sourcePath, 'session-evidence');
297
+ const userRecords = records.filter((record) => record.sourceKind === 'user-direct');
298
+ const assistantRecords = records.filter((record) => record.sourceKind === 'assistant-final');
299
+ const toolRecords = records.filter((record) => record.sourceKind === 'tool-action');
300
+ const symptoms = [...userRecords, ...assistantRecords]
301
+ .map((record) => record.text)
302
+ .slice(0, 8);
303
+ const rootCauseCandidates = records
304
+ .filter((record) => ['assistant-final', 'tool-action', 'repo-state'].includes(record.sourceKind))
305
+ .slice(0, 6)
306
+ .map((record) => ({
307
+ title: record.text,
308
+ nextSteps: ['把这条证据和用户目标重新核对,只在确认可复用时推荐项目级沉淀。'],
309
+ sourceKind: record.sourceKind,
310
+ }));
311
+ return {
312
+ kind: 'session-evidence-review',
313
+ evidenceOnly: true,
314
+ sourceId,
315
+ sourcePath,
316
+ primaryPath: sourcePath,
317
+ sourcePaths: [sourcePath],
318
+ title,
319
+ status: 'needs-review',
320
+ symptoms,
321
+ attentionGates: [],
322
+ correlationFields: [],
323
+ extraContextFields: [],
324
+ missingCorrelationFields: [],
325
+ eventNames: [...new Set(records.map((record) => record.sourceKind))],
326
+ rootCauseCandidates,
327
+ evidenceSources: records.map((record) => ({
328
+ kind: record.sourceKind,
329
+ path: `${sourcePath}#${record.eventIndex}`,
330
+ reason: record.reason,
331
+ })),
332
+ queryExamples: [
333
+ '先只看 user-direct、assistant-final、tool-action 证据,不把 AGENTS 或系统提示当成用户诉求。',
334
+ '如果复盘证据跨多轮重复出现,再推荐用户把它保留为当前项目经验。',
335
+ ],
336
+ abstractPattern: '从只读复盘证据中先隔离系统上下文噪声,再把重复摩擦或可复用判断沉淀为项目级候选。',
337
+ triggers: records
338
+ .filter((record) => record.sourceKind !== 'context-only')
339
+ .map((record) => `${record.sourceKind}: ${record.text}`)
340
+ .slice(0, 8),
341
+ prevention: [
342
+ 'raw session 先抽取 clean evidence,再进入候选判断。',
343
+ 'AGENTS、system、developer、tool schema 和 skill list 只能作为 context-only 背景,不触发用户诉求或项目经验候选。',
344
+ '项目级沉淀先作为候选推荐给用户确认,不自动 promote。',
345
+ ],
346
+ verificationSteps: [
347
+ '确认每条证据都有 sourceKind。',
348
+ '确认候选推荐没有依赖 context-only 命中。',
349
+ '确认用户确认前只生成候选,不修改正式项目 skill。',
350
+ ],
351
+ };
352
+ }
353
+
354
+ export {
355
+ buildCleanEvidenceText,
356
+ buildSessionEvidenceReviewSource,
357
+ cleanEvidenceRecords,
358
+ extractSessionEvidenceRecords,
359
+ filterGenericFrameworkTokens,
360
+ isGenericFrameworkTerm,
361
+ normalizeSessionEvidenceRecords,
362
+ sanitizeEvidenceText,
363
+ };
package/src/standards.js CHANGED
@@ -773,6 +773,42 @@ export async function initStandardsWorkspace(projectRoot, options = {}) {
773
773
  };
774
774
  }
775
775
 
776
+ export const DOC_MEGA_LINE_CHARS = 300;
777
+ export const DOC_AVG_LINE_CHARS = 200;
778
+ export const DOC_STALE_VERSION_THRESHOLD = 20;
779
+
780
+ export function analyzeDocQuality(relativePath, text) {
781
+ const lines = text.split('\n');
782
+ const contentLines = lines.filter((line) => line.trim().length > 0);
783
+ const megaLines = contentLines.filter((line) => line.length > DOC_MEGA_LINE_CHARS);
784
+ const totalChars = contentLines.reduce((sum, line) => sum + line.length, 0);
785
+ const avgLineChars = contentLines.length > 0 ? Math.round(totalChars / contentLines.length) : 0;
786
+ const maxLineChars = contentLines.reduce((max, line) => Math.max(max, line.length), 0);
787
+ return {
788
+ path: relativePath,
789
+ lines: lines.length,
790
+ contentLines: contentLines.length,
791
+ megaLines: megaLines.length,
792
+ avgLineChars,
793
+ maxLineChars,
794
+ bytes: Buffer.byteLength(text, 'utf8'),
795
+ };
796
+ }
797
+
798
+ export async function analyzeDocStaleness(projectRoot, docMtimeMs) {
799
+ const versionsDir = cjoin(projectRoot, '.openprd', 'state', 'versions');
800
+ const entries = await fs.readdir(versionsDir).catch(() => []);
801
+ const versionFiles = entries.filter((name) => /^v\d+\.json$/.test(name)).sort();
802
+ let versionsBehind = 0;
803
+ for (const name of versionFiles) {
804
+ const stat = await fs.stat(cjoin(versionsDir, name)).catch(() => null);
805
+ if (stat && stat.mtimeMs > docMtimeMs) {
806
+ versionsBehind += 1;
807
+ }
808
+ }
809
+ return { totalVersions: versionFiles.length, versionsBehind };
810
+ }
811
+
776
812
  export async function checkStandardsWorkspace(projectRoot, options = {}) {
777
813
  const errors = [];
778
814
  const warnings = [];
@@ -838,6 +874,7 @@ export async function checkStandardsWorkspace(projectRoot, options = {}) {
838
874
  ...externalReferenceIgnorePatterns(projectRoot, config ?? {}),
839
875
  ];
840
876
  const requiredDocs = [];
877
+ const docsQuality = [];
841
878
  const sourceFiles = await collectSourceFiles(projectRoot, projectRoot, [], ignorePatterns);
842
879
  const hasProjectSource = sourceFiles.length > 0;
843
880
  const openPrdToolProject = await isOpenPrdToolProject(projectRoot);
@@ -860,6 +897,28 @@ export async function checkStandardsWorkspace(projectRoot, options = {}) {
860
897
  if (hasProjectSource && !openPrdToolProject && options.docsContent !== false && DOC_PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(text))) {
861
898
  errors.push(`${relativePath} 仍包含模板占位内容,必须更新为当前项目事实。`);
862
899
  }
900
+
901
+ if (options.docsQuality !== false) {
902
+ const quality = analyzeDocQuality(relativePath, text);
903
+ if (quality.megaLines > 0) {
904
+ warnings.push(`${relativePath} 存在 ${quality.megaLines} 行超过 ${DOC_MEGA_LINE_CHARS} 字符的巨行(最长 ${quality.maxLineChars} 字符),疑似 append-only 污染,建议运行 openprd docs-compact . 查看压缩建议。`);
905
+ }
906
+ if (quality.contentLines >= 10 && quality.avgLineChars > DOC_AVG_LINE_CHARS) {
907
+ warnings.push(`${relativePath} 平均行长 ${quality.avgLineChars} 字符(阈值 ${DOC_AVG_LINE_CHARS}),文档结构疑似退化为长段落堆叠,建议拆分为结构化小节。`);
908
+ }
909
+ if (doc.fileName === 'prd.md') {
910
+ const docStat = await fs.stat(docPath).catch(() => null);
911
+ if (docStat) {
912
+ const staleness = await analyzeDocStaleness(projectRoot, docStat.mtimeMs);
913
+ quality.versionsBehind = staleness.versionsBehind;
914
+ quality.totalVersions = staleness.totalVersions;
915
+ if (staleness.versionsBehind > DOC_STALE_VERSION_THRESHOLD) {
916
+ warnings.push(`${relativePath} 自上次更新后已产生 ${staleness.versionsBehind} 个 requirement 版本(阈值 ${DOC_STALE_VERSION_THRESHOLD}),基线 PRD 已陈旧,需要回填最新已确认需求。`);
917
+ }
918
+ }
919
+ }
920
+ docsQuality.push(quality);
921
+ }
863
922
  }
864
923
 
865
924
  const templateFiles = [];
@@ -901,6 +960,10 @@ export async function checkStandardsWorkspace(projectRoot, options = {}) {
901
960
 
902
961
  checks.push(`Standards docs root: ${STANDARD_DOCS_ROOT.replaceAll(path.sep, '/')}`);
903
962
  checks.push(`Required docs: ${requiredDocs.filter((doc) => doc.exists).length}/${STANDARD_DOCS.length}.`);
963
+ if (docsQuality.length > 0) {
964
+ const pollutedDocs = docsQuality.filter((doc) => doc.megaLines > 0).length;
965
+ checks.push(`Docs quality: ${docsQuality.length} docs analyzed, ${pollutedDocs} with mega-lines > ${DOC_MEGA_LINE_CHARS} chars.`);
966
+ }
904
967
  checks.push(`Manual templates: ${templateFiles.filter((file) => file.exists).length}/${STANDARD_TEMPLATE_FILES.length}.`);
905
968
  const lineConfig = config?.developmentStandards?.codeFileLines ?? DEFAULT_DEVELOPMENT_STANDARDS.codeFileLines;
906
969
  checks.push(`Development standards: code files ok <= ${lineConfig.okMax} lines, attention <= ${lineConfig.attentionMax} lines.`);
@@ -916,6 +979,7 @@ export async function checkStandardsWorkspace(projectRoot, options = {}) {
916
979
  warnings,
917
980
  checks,
918
981
  requiredDocs,
982
+ docsQuality,
919
983
  templateFiles,
920
984
  manualReport,
921
985
  };
@@ -145,13 +145,16 @@ export function inferTestStrategyForTask(task = {}) {
145
145
  };
146
146
  }
147
147
 
148
- if (includesAny(text, [/visual|视觉|截图|界面|页面|组件|样式|browser|playwright|cypress|e2e|端到端|用户主路径|主流程/])) {
148
+ const singleElementCentering = includesAny(text, [/centering-board|center-board|visual centroid|bbox center|canvas center|inside centered|internal centering|内部居中|视觉重心|主体外接框|外接框中心|画布中心|单元素|单个元素|偏心|不居中|没居中|图标内部|图片内部|素材内部|按钮图形|logo center|icon center/]);
149
+ if (includesAny(text, [/visual|视觉|截图|界面|页面|组件|样式|间距|留白|边距|宽度|高度|卡片|对齐|内容槽位|内部槽位|标题|副标题|标签|状态|价格|按钮|密度|圆角|颜色|字号|图标|按钮|padding|margin|spacing|gap|browser|playwright|cypress|e2e|端到端|用户主路径|主流程|内部居中|视觉重心|主体外接框|画布中心|偏心|不居中|没居中/])) {
149
150
  return {
150
151
  layers: ['integration', 'e2e'],
151
152
  size: 'large',
152
- scope: includesAny(text, [/visual|视觉|截图|样式/]) ? 'visual-flow' : 'user-flow',
153
- evidencePlan: '主流程自动化、截图或 visual-compare 证据 + 本任务 verify 命令',
154
- upgradeReason: '触达用户可见路径,需要端到端或视觉级证据',
153
+ scope: includesAny(text, [/visual|视觉|截图|样式|间距|留白|边距|宽度|高度|卡片|对齐|内容槽位|内部槽位|标题|副标题|标签|状态|价格|按钮|密度|圆角|颜色|字号|图标|按钮|padding|margin|spacing|gap|内部居中|视觉重心|主体外接框|画布中心|偏心|不居中|没居中/]) ? 'visual-flow' : 'user-flow',
154
+ evidencePlan: singleElementCentering
155
+ ? '主流程自动化、截图或 visual-compare 证据 + 本任务 verify 命令;单元素内部居中要补 centering-board,量测画布中心、主体外接框中心和视觉重心偏移'
156
+ : '主流程自动化、截图或 visual-compare 证据 + 本任务 verify 命令;轻量 UI 可视优化也需要 before/after、verification-board 或 alignment-board 证据,同构卡片/列表要覆盖内部内容槽位',
157
+ upgradeReason: '触达用户可见路径,需要端到端或视觉级证据;轻量 UI 可视优化不能只用构建或 dev-check 收口',
155
158
  inferred: true,
156
159
  };
157
160
  }