@openprd/cli 0.1.11 → 0.1.19

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 (147) hide show
  1. package/.openprd/changes/adaptive-project-framing/.openprd.yaml +2 -0
  2. package/.openprd/changes/adaptive-project-framing/design.md +47 -0
  3. package/.openprd/changes/adaptive-project-framing/proposal.md +35 -0
  4. package/.openprd/changes/adaptive-project-framing/specs/agent-requirements/spec.md +16 -0
  5. package/.openprd/changes/adaptive-project-framing/task-events.jsonl +20 -0
  6. package/.openprd/changes/adaptive-project-framing/tasks.md +340 -0
  7. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/.openprd.yaml +2 -0
  8. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/design.md +34 -0
  9. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/proposal.md +23 -0
  10. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/specs/agent-requirements/spec.md +16 -0
  11. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/task-events.jsonl +14 -0
  12. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/tasks.md +238 -0
  13. package/.openprd/changes/loop-isolated-worktree-commit/.openprd.yaml +2 -0
  14. package/.openprd/changes/loop-isolated-worktree-commit/design.md +53 -0
  15. package/.openprd/changes/loop-isolated-worktree-commit/proposal.md +38 -0
  16. package/.openprd/changes/loop-isolated-worktree-commit/specs/agent-requirements/spec.md +16 -0
  17. package/.openprd/changes/loop-isolated-worktree-commit/task-events.jsonl +22 -0
  18. package/.openprd/changes/loop-isolated-worktree-commit/tasks.md +357 -0
  19. package/.openprd/changes/openprd/.openprd.yaml +2 -0
  20. package/.openprd/changes/openprd/design.md +46 -0
  21. package/.openprd/changes/openprd/proposal.md +36 -0
  22. package/.openprd/changes/openprd/specs/agent-requirements/spec.md +16 -0
  23. package/.openprd/changes/openprd/task-events.jsonl +23 -0
  24. package/.openprd/changes/openprd/tasks.md +242 -0
  25. package/.openprd/changes/openprd-generated-change/.openprd.yaml +2 -0
  26. package/.openprd/changes/openprd-generated-change/design.md +50 -0
  27. package/.openprd/changes/openprd-generated-change/proposal.md +37 -0
  28. package/.openprd/changes/openprd-generated-change/specs/agent-requirements/spec.md +16 -0
  29. package/.openprd/changes/openprd-generated-change/task-events.jsonl +21 -0
  30. package/.openprd/changes/openprd-generated-change/tasks.md +357 -0
  31. package/.openprd/changes/pm-human-in-the-loop/.openprd.yaml +2 -0
  32. package/.openprd/changes/pm-human-in-the-loop/design.md +38 -0
  33. package/.openprd/changes/pm-human-in-the-loop/proposal.md +29 -0
  34. package/.openprd/changes/pm-human-in-the-loop/specs/agent-requirements/spec.md +16 -0
  35. package/.openprd/changes/pm-human-in-the-loop/task-events.jsonl +18 -0
  36. package/.openprd/changes/pm-human-in-the-loop/tasks.md +306 -0
  37. package/.openprd/changes/test-strategy-router/.openprd.yaml +2 -0
  38. package/.openprd/changes/test-strategy-router/design.md +65 -0
  39. package/.openprd/changes/test-strategy-router/proposal.md +45 -0
  40. package/.openprd/changes/test-strategy-router/specs/agent-requirements/spec.md +16 -0
  41. package/.openprd/changes/test-strategy-router/task-events.jsonl +1 -0
  42. package/.openprd/changes/test-strategy-router/tasks.md +132 -0
  43. package/.openprd/changes/verify-boundaries-and-historical-refresh/.openprd.yaml +2 -0
  44. package/.openprd/changes/verify-boundaries-and-historical-refresh/design.md +28 -0
  45. package/.openprd/changes/verify-boundaries-and-historical-refresh/proposal.md +23 -0
  46. package/.openprd/changes/verify-boundaries-and-historical-refresh/specs/agent-requirements/spec.md +16 -0
  47. package/.openprd/changes/verify-boundaries-and-historical-refresh/task-events.jsonl +11 -0
  48. package/.openprd/changes/verify-boundaries-and-historical-refresh/tasks.md +66 -0
  49. package/.openprd/changes/vibe-coding-dev-check/.openprd.yaml +2 -0
  50. package/.openprd/changes/vibe-coding-dev-check/design.md +37 -0
  51. package/.openprd/changes/vibe-coding-dev-check/proposal.md +28 -0
  52. package/.openprd/changes/vibe-coding-dev-check/specs/agent-requirements/spec.md +16 -0
  53. package/.openprd/changes/vibe-coding-dev-check/task-events.jsonl +18 -0
  54. package/.openprd/changes/vibe-coding-dev-check/tasks.md +306 -0
  55. package/.openprd/design/README.md +11 -6
  56. package/.openprd/design/README_EN.md +4 -1
  57. package/.openprd/design/active/asset-spec.md +6 -0
  58. package/.openprd/design/active/direction-plan.md +5 -5
  59. package/.openprd/design/active/selected-direction.md +2 -0
  60. package/.openprd/design/anti-slop.md +26 -1
  61. package/.openprd/design/assets/surface-presets.md +6 -0
  62. package/.openprd/design/checklists/ui-quality-gate.md +7 -0
  63. package/.openprd/design/lenses/frontend-lenses.md +28 -0
  64. package/.openprd/design/recipes/content-experience.md +8 -1
  65. package/.openprd/design/recipes/operational-tool.md +7 -0
  66. package/.openprd/design/recipes/product-launch.md +8 -1
  67. package/.openprd/design/templates/README.md +6 -3
  68. package/.openprd/design/templates/README_EN.md +6 -3
  69. package/.openprd/design/templates/ops-dashboard.html +8 -1
  70. package/.openprd/design/templates/product-launch.html +2 -1
  71. package/.openprd/design/themes/theme-catalog.json +23 -5
  72. package/.openprd/engagements/active/prd.md +137 -112
  73. package/.openprd/standards/config.json +7 -1
  74. package/AGENTS.md +13 -3
  75. package/README.md +127 -14
  76. package/README_EN.md +156 -13
  77. package/package.json +1 -1
  78. package/scripts/dev-check-wrapup-copy.mjs +46 -21
  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 +2 -0
  88. package/skills/openprd-router/SKILL.md +2 -1
  89. package/skills/openprd-shared/SKILL.md +26 -16
  90. package/skills/openprd-standards/SKILL.md +4 -2
  91. package/src/agent-canonical-content.js +810 -0
  92. package/src/agent-integration.js +258 -777
  93. package/src/canvas-app-client-app.js +838 -0
  94. package/src/canvas-app-client-helpers.js +1138 -0
  95. package/src/canvas-app-styles.js +898 -0
  96. package/src/canvas-app.html.js +135 -0
  97. package/src/canvas-binding.js +414 -0
  98. package/src/canvas-codex-relay.js +430 -0
  99. package/src/canvas-i18n.js +282 -0
  100. package/src/canvas-scene.js +377 -0
  101. package/src/canvas-session-store.js +148 -0
  102. package/src/canvas-workspace.js +1264 -0
  103. package/src/cli/args.js +72 -4
  104. package/src/cli/basic-print.js +3 -0
  105. package/src/cli/canvas-print.js +106 -0
  106. package/src/cli/doctor-print.js +20 -0
  107. package/src/cli/print.js +2 -0
  108. package/src/cli/quality-commands.js +222 -0
  109. package/src/cli/quality-print.js +28 -2
  110. package/src/cli/run-print.js +5 -0
  111. package/src/cli/workflow-print.js +3 -0
  112. package/src/codex-app-server-relay.js +251 -0
  113. package/src/codex-hook-runner-template.mjs +978 -58
  114. package/src/design-starter-support.js +2 -2
  115. package/src/design-starter.js +39 -3
  116. package/src/dev-standards.js +125 -10
  117. package/src/diagram-core.js +6 -20
  118. package/src/discovery.js +2 -8
  119. package/src/docs-compact.js +125 -0
  120. package/src/execution-strategy.js +1 -1
  121. package/src/fleet.js +9 -3
  122. package/src/guide.js +227 -0
  123. package/src/html-artifacts.js +428 -7
  124. package/src/knowledge.js +45 -11
  125. package/src/language-policy.js +63 -3
  126. package/src/learning-review.js +1 -1
  127. package/src/loop.js +1 -1
  128. package/src/openprd.js +68 -131
  129. package/src/openspec/change-validate.js +2 -2
  130. package/src/openspec/constants.js +6 -7
  131. package/src/openspec/execute.js +2 -2
  132. package/src/openspec/generate.js +318 -102
  133. package/src/openspec/migration.js +168 -0
  134. package/src/openspec/paths.js +1 -22
  135. package/src/prd-core.js +1 -1
  136. package/src/quality-visual-review.js +257 -14
  137. package/src/quality.js +46 -8
  138. package/src/review-presentation.js +60 -0
  139. package/src/run-harness.js +41 -3
  140. package/src/session-evidence.js +363 -0
  141. package/src/standards.js +64 -0
  142. package/src/test-strategy.js +7 -4
  143. package/src/visual-compare-centering.js +622 -0
  144. package/src/visual-compare-core.js +1123 -0
  145. package/src/visual-compare.js +620 -698
  146. package/src/workspace-core.js +94 -4
  147. package/src/workspace-workflow.js +46 -1
package/src/quality.js CHANGED
@@ -108,7 +108,7 @@ const EVIDENCE_TOKENS = {
108
108
  'test-strategy': ['test-layer', 'test-size', 'test-scope', 'evidence-plan', 'testing pyramid', '测试策略', '测试分流', '单元测试', '集成测试', '端到端'],
109
109
  smoke: ['smoke', 'e2e', 'playwright', 'cypress', 'main flow', 'happy path', '冒烟', '主流程'],
110
110
  'feature-coverage': ['feature coverage', 'acceptance', 'tasks done', 'openprd tasks', '验收', '功能覆盖', '任务完成'],
111
- 'visual-review': ['visual-compare', 'visual-before-after', 'visual-focus-board', 'visual-parallel-board', 'reference-actual', 'before-after', 'focus-board', 'parallel-board', '效果图', '实现截图', '修改前', '修改后', '视觉对比', '局部焦点证据板', '并行实验证据板'],
111
+ 'visual-review': ['visual-compare', 'visual-before-after', 'visual-focus-board', 'visual-parallel-board', 'visual-verification-board', 'visual-alignment-board', 'visual-centering-board', 'reference-actual', 'before-after', 'focus-board', 'parallel-board', 'verification-board', 'alignment-board', 'centering-board', 'center-board', '效果图', '实现截图', '修改前', '修改后', '视觉对比', '局部焦点证据板', '并行实验证据板', '截图实测证据板', '对齐辅助线证据板', '内部居中证据板', '内容槽位', '内部槽位', '视觉重心', '主体外接框', '画布中心', '偏心'],
112
112
  'normal-performance': ['performance', 'perf', 'benchmark', 'latency', 'p95', 'lighthouse', 'k6', '性能', '耗时'],
113
113
  'extreme-performance': ['extreme', 'stress', 'load test', 'large-data', 'pressure', 'k6', '压力', '极端', '大数据'],
114
114
  knowledge: ['quality learn', 'incident', 'pattern', 'skill', '复盘', '经验', '沉淀'],
@@ -253,6 +253,10 @@ function defaultQualityConfig() {
253
253
  ledgerPath: OPENPRD_GROWTH_LEDGER,
254
254
  completionCheckpointRequired: true,
255
255
  },
256
+ reports: {
257
+ retentionMaxReports: 60,
258
+ retentionEnabled: true,
259
+ },
256
260
  };
257
261
  }
258
262
 
@@ -301,6 +305,10 @@ function normalizeQualityConfig(config = {}) {
301
305
  ...defaults.growth,
302
306
  ...(config.growth ?? {}),
303
307
  },
308
+ reports: {
309
+ ...defaults.reports,
310
+ ...(config.reports ?? {}),
311
+ },
304
312
  };
305
313
  }
306
314
 
@@ -442,8 +450,7 @@ async function readActiveChangeContext(projectRoot, activeChange) {
442
450
  return { activeChange: null, files: [], text: '' };
443
451
  }
444
452
  const roots = [
445
- cjoin('openprd', 'changes', activeChange),
446
- cjoin('openspec', 'changes', activeChange),
453
+ cjoin('.openprd', 'changes', activeChange),
447
454
  ];
448
455
  const files = [];
449
456
  async function walk(root, dir) {
@@ -904,7 +911,9 @@ async function readActiveTasks(projectRoot) {
904
911
  return { activeChange: null, total: 0, done: 0, pending: 0, blocked: 0, tasks: [] };
905
912
  }
906
913
  const taskFiles = [];
907
- for (const root of [cjoin('openprd', 'changes', activeChange), cjoin('openspec', 'changes', activeChange)]) {
914
+ for (const root of [
915
+ cjoin('.openprd', 'changes', activeChange),
916
+ ]) {
908
917
  const dir = cjoin(projectRoot, root);
909
918
  const entries = await fs.readdir(dir).catch(() => []);
910
919
  for (const entry of entries) {
@@ -1283,7 +1292,35 @@ async function buildQualityReport(projectRoot, config, options = {}) {
1283
1292
  };
1284
1293
  }
1285
1294
 
1286
- async function writeReport(projectRoot, report) {
1295
+ export async function pruneQualityReports(projectRoot, retainedReports, config) {
1296
+ const retention = config?.reports ?? {};
1297
+ if (retention.retentionEnabled === false) {
1298
+ return { pruned: 0 };
1299
+ }
1300
+ const maxReports = Number.isInteger(retention.retentionMaxReports) && retention.retentionMaxReports > 0
1301
+ ? retention.retentionMaxReports
1302
+ : 60;
1303
+ const reportsDir = qualityPath(projectRoot, QUALITY_REPORTS_DIR);
1304
+ const entries = await fs.readdir(reportsDir).catch(() => []);
1305
+ const keepBases = new Set(
1306
+ retainedReports.slice(0, maxReports).map((item) => String(item.reportId ?? '').replace(/[^a-zA-Z0-9._-]/g, '_')),
1307
+ );
1308
+ let pruned = 0;
1309
+ for (const name of entries) {
1310
+ const match = name.match(/^(eval-[a-zA-Z0-9._-]+)\.(json|html)$/);
1311
+ if (!match) {
1312
+ continue;
1313
+ }
1314
+ if (keepBases.has(match[1])) {
1315
+ continue;
1316
+ }
1317
+ await fs.rm(cjoin(reportsDir, name), { force: true }).catch(() => {});
1318
+ pruned += 1;
1319
+ }
1320
+ return { pruned };
1321
+ }
1322
+
1323
+ async function writeReport(projectRoot, report, config = null) {
1287
1324
  await ensureQualityDirs(projectRoot);
1288
1325
  const reportBase = report.id.replace(/[^a-zA-Z0-9._-]/g, '_');
1289
1326
  const jsonPath = qualityPath(projectRoot, cjoin(QUALITY_REPORTS_DIR, `${reportBase}.json`));
@@ -1304,7 +1341,8 @@ async function writeReport(projectRoot, report) {
1304
1341
  ...(Array.isArray(index.reports) ? index.reports.filter((item) => item.reportId !== report.id) : []),
1305
1342
  ].slice(0, 100);
1306
1343
  await writeJson(indexPath, { version: 1, updatedAt: timestamp(), reports });
1307
- return { jsonPath, htmlPath, indexPath };
1344
+ const retention = await pruneQualityReports(projectRoot, reports, config ?? normalizeQualityConfig());
1345
+ return { jsonPath, htmlPath, indexPath, prunedReports: retention.pruned };
1308
1346
  }
1309
1347
 
1310
1348
  export async function initQualityWorkspace(projectRoot, options = {}) {
@@ -1349,7 +1387,7 @@ export async function verifyQualityWorkspace(projectRoot, options = {}) {
1349
1387
  await ensureQualityDirs(projectRoot);
1350
1388
  const initialReport = await buildQualityReport(projectRoot, config);
1351
1389
  let report = initialReport;
1352
- let paths = await writeReport(projectRoot, report);
1390
+ let paths = await writeReport(projectRoot, report, config);
1353
1391
  const knowledgeSignal = {
1354
1392
  kind: 'quality-verify',
1355
1393
  ok: report.readiness.productionReady,
@@ -1408,7 +1446,7 @@ export async function verifyQualityWorkspace(projectRoot, options = {}) {
1408
1446
  reason: 'reusable-knowledge-artifact-already-exists',
1409
1447
  };
1410
1448
  report = await buildQualityReport(projectRoot, config, { reportId: initialReport.id });
1411
- paths = await writeReport(projectRoot, report);
1449
+ paths = await writeReport(projectRoot, report, config);
1412
1450
  const strict = options.strict === true;
1413
1451
  const blocking = (strict || config.enforcement === 'blocking') && !report.readiness.productionReady;
1414
1452
  const featureCoverageOnly = report.readiness.failingGates.length === 0
@@ -48,6 +48,32 @@ export const REVIEW_PRESENTATION_TEMPLATE = {
48
48
  USER_CHANGE_SUMMARY_GUIDE.panelExamples.risk,
49
49
  ],
50
50
  },
51
+ designDirections: {
52
+ note: '可选;只有需求涉及界面/页面/交互时提供。先用 imagegen 生成 3 个差异足够大的交互稿,把图片放到 .openprd/design/active/ 后写入路径;不涉及界面时删除本字段。',
53
+ directions: [
54
+ {
55
+ id: 'direction-1',
56
+ title: '15 字以内方向名',
57
+ image: '.openprd/design/active/direction-1.png',
58
+ summary: '30 字以内一句话说明这个方向的核心体验',
59
+ tradeoff: '30 字以内说明取舍或适用场景',
60
+ },
61
+ {
62
+ id: 'direction-2',
63
+ title: '15 字以内方向名',
64
+ image: '.openprd/design/active/direction-2.png',
65
+ summary: '30 字以内一句话说明这个方向的核心体验',
66
+ tradeoff: '30 字以内说明取舍或适用场景',
67
+ },
68
+ {
69
+ id: 'direction-3',
70
+ title: '15 字以内方向名',
71
+ image: '.openprd/design/active/direction-3.png',
72
+ summary: '30 字以内一句话说明这个方向的核心体验',
73
+ tradeoff: '30 字以内说明取舍或适用场景',
74
+ },
75
+ ],
76
+ },
51
77
  };
52
78
 
53
79
  export function buildReviewPresentationTemplatePayload() {
@@ -182,6 +208,39 @@ function stableStringify(value) {
182
208
  return JSON.stringify(value);
183
209
  }
184
210
 
211
+ const ACTIVE_ARTIFACT_RETAIN_VERSIONS = 12;
212
+
213
+ export async function archiveStaleActiveArtifacts(projectRoot, options = {}) {
214
+ const retain = Number.isInteger(options.retain) && options.retain > 0
215
+ ? options.retain
216
+ : ACTIVE_ARTIFACT_RETAIN_VERSIONS;
217
+ const activeDir = path.join(projectRoot, '.openprd', 'artifacts', 'active');
218
+ const archiveDir = path.join(projectRoot, '.openprd', 'artifacts', 'archive');
219
+ const entries = await fs.readdir(activeDir, { withFileTypes: true }).catch(() => []);
220
+ const versioned = entries
221
+ .filter((entry) => entry.isDirectory() && /^v\d{4}-/.test(entry.name))
222
+ .map((entry) => entry.name)
223
+ .sort();
224
+ const stale = versioned.slice(0, Math.max(0, versioned.length - retain));
225
+ if (stale.length === 0) {
226
+ return { archived: [] };
227
+ }
228
+ await fs.mkdir(archiveDir, { recursive: true });
229
+ const archived = [];
230
+ for (const name of stale) {
231
+ const from = path.join(activeDir, name);
232
+ const to = path.join(archiveDir, name);
233
+ try {
234
+ await fs.rm(to, { recursive: true, force: true });
235
+ await fs.rename(from, to);
236
+ archived.push(name);
237
+ } catch {
238
+ // 归档失败不阻断评审生成
239
+ }
240
+ }
241
+ return { archived };
242
+ }
243
+
185
244
  async function renderValidatedReviewPresentation(projectRoot, snapshot) {
186
245
  const workspaceRoot = path.join(projectRoot, '.openprd');
187
246
  const canonicalReview = canonicalReviewPath({ workspaceRoot }, snapshot.versionId);
@@ -191,6 +250,7 @@ async function renderValidatedReviewPresentation(projectRoot, snapshot) {
191
250
  snapshot,
192
251
  projectRelease: buildReleaseLedgerSummary(releaseLedger.ledger),
193
252
  }));
253
+ await archiveStaleActiveArtifacts(projectRoot).catch(() => null);
194
254
 
195
255
  const versionIndexPath = path.join(workspaceRoot, 'state', 'version-index.json');
196
256
  const versionIndex = await readJson(versionIndexPath);
@@ -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
+ };