@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
package/src/openprd.js CHANGED
@@ -33,7 +33,7 @@ import { doctorOpenPrdAgentIntegration, setupOpenPrdAgentIntegration, updateOpen
33
33
  import { finishLoopWorkspace, initLoopWorkspace, nextLoopWorkspace, planLoopWorkspace, promptLoopWorkspace, runLoopWorkspace, statusLoopWorkspace, verifyLoopWorkspace } from './loop.js';
34
34
  import { timestamp } from './time.js';
35
35
  import { parseCommandArgs, usage } from './cli/args.js';
36
- import { printAcceptedSpecsResult, printAgentIntegrationResult, printBenchmarkResult, printCaptureResult, printClarifyResult, printClassifyResult, printDevelopmentStandardsResult, printDiagramResult, printDiffResult, printDoctorResult, printFleetResult, printFreezeResult, printGrowthResult, printHandoffResult, printHistoryResult, printInitResult, printInterviewResult, printKnowledgeResult, printLearningResult, printLoopResult, printNextResult, printOpenPrdChangeActionResult, printOpenPrdChangesResult, printOpenSpecChangeValidationResult, printOpenSpecDiscoveryResult, printOpenSpecGenerateResult, printOpenSpecTaskResult, printPlaygroundResult, printQualityResult, printReleaseResult, printReviewResult, printRunResult, printSelfUpdateResult, printStandardsResult, printStatus, printSynthesizeResult, printUpgradeResult, printValidation, printVisualCompareResult } from './cli/print.js';
36
+ import { printAcceptedSpecsResult, printAgentIntegrationResult, printBenchmarkResult, printBrainstormResult, printCaptureResult, printClarifyResult, printClassifyResult, printDesignStarterResult, printDevelopmentStandardsResult, printDiagramResult, printDiffResult, printDoctorResult, printFleetResult, printFreezeResult, printGrowthResult, printHandoffResult, printHistoryResult, printInitResult, printInterviewResult, printKnowledgeResult, printLearningResult, printLoopResult, printNextResult, printOpenPrdChangeActionResult, printOpenPrdChangesResult, printOpenSpecChangeValidationResult, printOpenSpecDiscoveryResult, printOpenSpecGenerateResult, printOpenSpecTaskResult, printPlaygroundResult, printQualityResult, printReleaseResult, printReviewResult, printRunResult, printSelfUpdateResult, printStandardsResult, printStatus, printSynthesizeResult, printUpgradeResult, printValidation, printVisualCompareResult, printVisualPrepareResult } from './cli/print.js';
37
37
  import { cjoin, exists, readJson, writeJson, writeText, writeYaml } from './fs-utils.js';
38
38
  import { diagramWorkspace } from './diagram-workspace.js';
39
39
  import { createOpenSpecDiscoveryWorkspace } from './discovery.js';
@@ -61,11 +61,15 @@ import {
61
61
  rejectGrowthCandidateWorkspace,
62
62
  reviewGrowthWorkspace,
63
63
  } from './growth.js';
64
+ import { brainstormWorkspace } from './brainstorm.js';
65
+ import { buildBrainstormPresentationTemplatePayload, brainstormPresentationWorkspace } from './brainstorm-presentation.js';
66
+ import { designStarterWorkspace } from './design-starter.js';
64
67
  import { buildReviewPresentationTemplatePayload, reviewPresentationWorkspace } from './review-presentation.js';
65
68
  import { analyzeWorkspaceRegistryHygiene } from './registry-hygiene.js';
66
69
  import { syncSessionBindingFromChange } from './session-binding.js';
67
70
  import { readSessionRegistry } from './session-registry.js';
68
71
  import { visualCompareWorkspace } from './visual-compare.js';
72
+ import { visualPrepareWorkspace } from './visual-prepare.js';
69
73
  import { captureWorkspace, clarifyWorkspace, classifyWorkspace, computeWorkspaceGuidance, diffWorkspace, historyWorkspace, interviewWorkspace, nextWorkspace, playgroundWorkspace, reviewWorkspace, synthesizeWorkspace } from './workspace-workflow.js';
70
74
  import { appendDecision, appendProgress, appendVerification, appendWorkflowEvent, buildCurrentStateSnapshot, buildWorkflowTaskGraph, computeWorkspaceDigest, CORE_TEMPLATE_FILES, ensureWorkspaceSkeleton, isSupportedProductType, loadCurrentLaneSnapshot, loadLatestVersionSnapshot, loadWorkspace, migrateWorkspaceSkeleton, normalizeVersionId, persistWorkspaceCurrentState, readVersionIndex, resolveActiveTemplatePack, resolveCurrentProductType, validateWorkspace } from './workspace-core.js';
71
75
  import { readWorkspaceRegistry } from './workspace-registry.js';
@@ -784,8 +788,10 @@ export async function main(argv = process.argv.slice(2)) {
784
788
 
785
789
  if (command === 'self-update') {
786
790
  const result = await selfUpdateWorkspace({
791
+ check: flags.check,
787
792
  dryRun: flags.dryRun,
788
793
  json: flags.json,
794
+ openprdHome: flags.openprdHome,
789
795
  });
790
796
  printSelfUpdateResult(result, flags.json);
791
797
  return result.ok ? 0 : 1;
@@ -866,6 +872,9 @@ export async function main(argv = process.argv.slice(2)) {
866
872
  evidence: flags.evidence,
867
873
  notes: flags.notes,
868
874
  repairAgent: flags.repairAgent,
875
+ worktree: flags.worktree,
876
+ branch: flags.branch,
877
+ allowDirtyMain: flags.allowDirtyMain,
869
878
  };
870
879
  if (flags.init) {
871
880
  result = await initLoopWorkspace(projectPath, options);
@@ -944,6 +953,20 @@ export async function main(argv = process.argv.slice(2)) {
944
953
  return result.ok ? 0 : 1;
945
954
  }
946
955
 
956
+ if (command === 'visual-prepare') {
957
+ const result = await visualPrepareWorkspace(projectPath, {
958
+ reference: flags.reference,
959
+ grid: flags.grid,
960
+ boxes: flags.boxes,
961
+ include: flags.include,
962
+ id: flags.id,
963
+ title: flags.title,
964
+ out: flags.out,
965
+ });
966
+ printVisualPrepareResult(result, flags.json);
967
+ return result.ok ? 0 : 1;
968
+ }
969
+
947
970
  if (command === 'visual-compare') {
948
971
  const result = await visualCompareWorkspace(projectPath, {
949
972
  reference: flags.reference,
@@ -962,6 +985,22 @@ export async function main(argv = process.argv.slice(2)) {
962
985
  return result.ok ? 0 : 1;
963
986
  }
964
987
 
988
+ if (command === 'design-starter') {
989
+ const result = await designStarterWorkspace(projectPath, {
990
+ starter: flags.starter,
991
+ out: flags.out,
992
+ title: flags.title,
993
+ brief: flags.brief,
994
+ sections: flags.sections,
995
+ noExternalFacts: flags.noExternalFacts,
996
+ noBrandAssets: flags.noBrandAssets,
997
+ noRealImages: flags.noRealImages,
998
+ force: flags.force,
999
+ });
1000
+ printDesignStarterResult(result, flags.json);
1001
+ return result.ok ? 0 : 1;
1002
+ }
1003
+
965
1004
  if (command === 'grow') {
966
1005
  const firstArgIsProjectPath = !flags.path && positionals.length > 0 && await isDirectoryPath(positionals[0]);
967
1006
  const growthProjectPath = path.resolve(flags.path ?? (firstArgIsProjectPath ? positionals[0] : process.cwd()));
@@ -1073,6 +1112,16 @@ export async function main(argv = process.argv.slice(2)) {
1073
1112
  return 0;
1074
1113
  }
1075
1114
 
1115
+ if (command === 'brainstorm') {
1116
+ const result = await brainstormWorkspace(projectPath, {
1117
+ topic: flags.topic,
1118
+ open: flags.open || !flags.json,
1119
+ json: flags.json,
1120
+ });
1121
+ printBrainstormResult(result, flags.json);
1122
+ return 0;
1123
+ }
1124
+
1076
1125
  if (command === 'learn') {
1077
1126
  if (flags.enable || flags.disable) {
1078
1127
  const enabled = flags.enable ? true : false;
@@ -1163,6 +1212,38 @@ export async function main(argv = process.argv.slice(2)) {
1163
1212
  return flags.failOnViolation && !result.ok ? 1 : 0;
1164
1213
  }
1165
1214
 
1215
+ if (command === 'brainstorm-presentation') {
1216
+ if (flags.template) {
1217
+ console.log(JSON.stringify(buildBrainstormPresentationTemplatePayload(), null, 2));
1218
+ return 0;
1219
+ }
1220
+ const result = await brainstormPresentationWorkspace(projectPath, {
1221
+ presentationPath: flags.presentation,
1222
+ write: flags.write,
1223
+ });
1224
+ if (flags.json) {
1225
+ console.log(JSON.stringify(result, null, 2));
1226
+ } else {
1227
+ console.log(`展示文案校验: ${result.ok ? '通过' : '需要重写'}`);
1228
+ console.log(`主题: ${result.topic}`);
1229
+ console.log(`超限或格式问题: ${result.presentationFeedback.length}`);
1230
+ if (result.presentationFeedback.length > 0) {
1231
+ for (const item of result.presentationFeedback.slice(0, 6)) {
1232
+ const pathHint = item.jsonPath ? `${item.jsonPath}: ` : '';
1233
+ const sizeHint = item.maxChars ? ` 当前 ${item.currentChars} 字,限制 ${item.maxChars} 字。` : '';
1234
+ console.log(`- ${item.area} / ${item.target}: ${pathHint}${item.action}${sizeHint}`);
1235
+ }
1236
+ }
1237
+ if (result.written) {
1238
+ console.log(`已写入: ${result.written}`);
1239
+ }
1240
+ if (result.htmlPath) {
1241
+ console.log(`已更新脑暴页面: ${result.htmlPath}`);
1242
+ }
1243
+ }
1244
+ return flags.failOnViolation && !result.ok ? 1 : 0;
1245
+ }
1246
+
1166
1247
  if (command === 'diagram') {
1167
1248
  const result = await diagramWorkspace(projectPath, { open: flags.open, type: flags.type, input: flags.input, mark: flags.mark });
1168
1249
  printDiagramResult(result, flags.json);
@@ -1384,6 +1465,7 @@ export {
1384
1465
  clarifyWorkspace,
1385
1466
  captureWorkspace,
1386
1467
  playgroundWorkspace,
1468
+ brainstormWorkspace,
1387
1469
  generateLearningReviewWorkspace,
1388
1470
  setLearningReviewModeWorkspace,
1389
1471
  synthesizeWorkspace,
@@ -1392,6 +1474,7 @@ export {
1392
1474
  historyWorkspace,
1393
1475
  releaseWorkspace,
1394
1476
  reviewWorkspace,
1477
+ brainstormPresentationWorkspace,
1395
1478
  reviewPresentationWorkspace,
1396
1479
  freezeWorkspace,
1397
1480
  handoffWorkspace,
@@ -1409,6 +1492,7 @@ export {
1409
1492
  archiveOpenPrdChangeWorkspace,
1410
1493
  listAcceptedSpecsWorkspace,
1411
1494
  diagramWorkspace,
1495
+ designStarterWorkspace,
1412
1496
  classifyWorkspace,
1413
1497
  interviewWorkspace,
1414
1498
  loadWorkspace,
@@ -1423,6 +1507,7 @@ export {
1423
1507
  rejectKnowledgeCandidate,
1424
1508
  archiveKnowledgeCandidate,
1425
1509
  restoreKnowledgeCandidate,
1510
+ visualPrepareWorkspace,
1426
1511
  visualCompareWorkspace,
1427
1512
  checkDevelopmentStandardsWorkspace,
1428
1513
  initGrowthWorkspace,
@@ -210,6 +210,7 @@ export async function validateOpenSpecChangeWorkspace(projectRoot, options = {})
210
210
  optional: !(await exists(cjoin(projectRoot, '.openprd'))),
211
211
  sourceManuals: options.sourceManuals,
212
212
  docsContent: options.docsContent,
213
+ folderManualModuleName: options.folderManualModuleName,
213
214
  });
214
215
  if (!standards.skipped) {
215
216
  errors.push(...standards.errors);
package/src/prd-core.js CHANGED
@@ -239,6 +239,30 @@ export function buildPrdSnapshot(ws, options = {}) {
239
239
  successMetrics: normalizeArray(pickValue(options.successMetrics, state.successMetrics)),
240
240
  acceptanceGoals: normalizeArray(pickValue(options.acceptanceGoals, state.acceptanceGoals)),
241
241
  },
242
+ validation: {
243
+ community: normalizeArray(pickValue(options.community, state.community)),
244
+ seedUsers: normalizeArray(pickValue(options.seedUsers, state.seedUsers)),
245
+ communityFit: normalizeArray(pickValue(options.communityFit, state.communityFit)),
246
+ currentAlternative: pickValue(options.currentAlternative, state.currentAlternative, state.asIs),
247
+ painEvidence: normalizeArray(pickValue(options.painEvidence, state.painEvidence)),
248
+ manualPath: normalizeArray(pickValue(options.manualPath, state.manualPath)),
249
+ manualPlaybook: normalizeArray(pickValue(options.manualPlaybook, state.manualPlaybook)),
250
+ commitmentSignals: normalizeArray(pickValue(options.commitmentSignals, state.commitmentSignals)),
251
+ firstValidationStep: pickValue(options.firstValidationStep, state.firstValidationStep, state.nextStep),
252
+ defaultAlivePlan: normalizeArray(pickValue(options.defaultAlivePlan, state.defaultAlivePlan)),
253
+ paymentProof: normalizeArray(pickValue(options.paymentProof, state.paymentProof)),
254
+ mvpSlice: pickValue(options.mvpSlice, state.mvpSlice),
255
+ weekendTest: pickValue(options.weekendTest, state.weekendTest),
256
+ smallestExecution: normalizeArray(pickValue(options.smallestExecution, state.smallestExecution)),
257
+ productizeGate: normalizeArray(pickValue(options.productizeGate, state.productizeGate)),
258
+ firstCustomerPath: normalizeArray(pickValue(options.firstCustomerPath, state.firstCustomerPath)),
259
+ pricingHypothesis: pickValue(options.pricingHypothesis, state.pricingHypothesis),
260
+ customerOneProfitability: pickValue(options.customerOneProfitability, state.customerOneProfitability),
261
+ growthDiscipline: normalizeArray(pickValue(options.growthDiscipline, state.growthDiscipline)),
262
+ reversibility: pickValue(options.reversibility, state.reversibility),
263
+ customerTruth: pickValue(options.customerTruth, state.customerTruth),
264
+ valuesFit: pickValue(options.valuesFit, state.valuesFit),
265
+ },
242
266
  scope: {
243
267
  inScope: normalizeArray(pickValue(options.inScope, state.inScope)),
244
268
  outOfScope: normalizeArray(pickValue(options.outOfScope, state.outOfScope)),
@@ -367,6 +391,30 @@ export function renderPrdMarkdown(snapshot) {
367
391
  ['成功指标', sections.goals.successMetrics],
368
392
  ['验收目标', sections.goals.acceptanceGoals],
369
393
  ]),
394
+ renderSection('验证与创业闭环', [
395
+ ['可触达社区', sections.validation.community],
396
+ ['第一批种子用户', sections.validation.seedUsers],
397
+ ['社区契合与触达依据', sections.validation.communityFit],
398
+ ['当前替代方案', sections.validation.currentAlternative],
399
+ ['痛点与替代证据', sections.validation.painEvidence],
400
+ ['手工交付路径', sections.validation.manualPath],
401
+ ['手工作战卡', sections.validation.manualPlaybook],
402
+ ['承诺信号', sections.validation.commitmentSignals],
403
+ ['首个低成本验证', sections.validation.firstValidationStep],
404
+ ['先活下来方案', sections.validation.defaultAlivePlan],
405
+ ['付费验证信号', sections.validation.paymentProof],
406
+ ['第一版只做一件事', sections.validation.mvpSlice],
407
+ ['周末级验证', sections.validation.weekendTest],
408
+ ['最小工具桥接', sections.validation.smallestExecution],
409
+ ['产品化门槛', sections.validation.productizeGate],
410
+ ['第一批客户路径', sections.validation.firstCustomerPath],
411
+ ['初始收费假设', sections.validation.pricingHypothesis],
412
+ ['客户 1 盈利路径', sections.validation.customerOneProfitability],
413
+ ['销售与增长纪律', sections.validation.growthDiscipline],
414
+ ['可逆性判断', sections.validation.reversibility],
415
+ ['客户真问题校验', sections.validation.customerTruth],
416
+ ['价值观一致性', sections.validation.valuesFit],
417
+ ]),
370
418
  renderSection('范围与非目标', [
371
419
  ['范围内', sections.scope.inScope],
372
420
  ['范围外', sections.scope.outOfScope],
@@ -459,6 +507,32 @@ const BUSINESS_GUARDRAIL_FIELD_DESCRIPTORS = [
459
507
  { section: 'businessGuardrails', path: 'businessGuardrails.stopLossActions', label: '止损动作', prompt: '触发异常后,应该降级、暂停、关闭哪些能力,谁来处理?' },
460
508
  ];
461
509
 
510
+
511
+ const VALIDATION_REQUIRED_FIELD_DESCRIPTORS = [
512
+ { section: 'validation', path: 'validation.community', label: '第一批可触达人群', prompt: '第一批最容易触达的社区、渠道或人群是谁?' },
513
+ { section: 'validation', path: 'validation.seedUsers', label: '种子用户', prompt: '至少 3 到 10 个最该先找的人是谁?' },
514
+ { section: 'validation', path: 'validation.communityFit', label: '社区契合', prompt: '你为什么算这个社区里的自己人?你现在是否已经在真实参与或贡献,他们为什么愿意先相信你?' },
515
+ { section: 'validation', path: 'validation.currentAlternative', label: '当前替代方案', prompt: '用户现在主要靠什么替代方案、流程或人工方式在解决?' },
516
+ { section: 'validation', path: 'validation.painEvidence', label: '痛点证据', prompt: '这个问题到底有多痛,用户已经在为更差的办法花什么时间或金钱?' },
517
+ { section: 'validation', path: 'validation.manualPath', label: '手工交付路径', prompt: '如果先不做完整产品,怎么靠手工服务或半自动流程把价值跑出来?' },
518
+ { section: 'validation', path: 'validation.manualPlaybook', label: '手工作战卡', prompt: '触发条件、步骤、工具、耗时和交接点分别是什么?' },
519
+ { section: 'validation', path: 'validation.commitmentSignals', label: '承诺信号', prompt: '什么真实承诺最能证明这不是口头兴趣?' },
520
+ { section: 'validation', path: 'validation.firstValidationStep', label: '最低成本验证', prompt: '最低成本先验证哪一步最关键?' },
521
+ { section: 'validation', path: 'validation.defaultAlivePlan', label: '先活下来方案', prompt: '验证阶段怎样不把成本、复杂度和维护负担一下子做爆?' },
522
+ { section: 'validation', path: 'validation.paymentProof', label: '付费验证信号', prompt: '有没有 10 个样本、3/10 付费意愿或更强交易信号?' },
523
+ { section: 'validation', path: 'validation.mvpSlice', label: '一件事 MVP', prompt: '第一版到底只做哪一件事?' },
524
+ { section: 'validation', path: 'validation.weekendTest', label: '周末级验证', prompt: '能不能压成周末级 MVP、顾问式试跑或更轻验证?' },
525
+ { section: 'validation', path: 'validation.smallestExecution', label: '最小工具桥接', prompt: '能不能先用 spreadsheet、表单或 no-code 工具把第一版跑起来?如果必须开始做产品,也只自动化最重复的一步并先压成 forms / lists / CRUD 骨架;不要先为假想中的未来客户造复杂能力。' },
526
+ { section: 'validation', path: 'validation.productizeGate', label: '产品化门槛', prompt: '达到什么条件才允许继续产品化或加功能?先服务今天已经存在的重复需求,不要提前为未来规模开壳。' },
527
+ { section: 'validation', path: 'validation.firstCustomerPath', label: '第一批客户路径', prompt: '第一批客户最现实的触达顺序是什么?' },
528
+ { section: 'validation', path: 'validation.pricingHypothesis', label: '初始收费假设', prompt: '从第一个客户开始准备怎么收费?' },
529
+ { section: 'validation', path: 'validation.customerOneProfitability', label: '客户 1 盈利路径', prompt: '第一个客户如何覆盖时间和交付成本?' },
530
+ { section: 'validation', path: 'validation.growthDiscipline', label: '增长纪律', prompt: '销售、launch 和增长阶段准备守哪些纪律?' },
531
+ { section: 'validation', path: 'validation.reversibility', label: '可逆性', prompt: '如果结果一般,这条路是否容易回退?是否在逼你做重招聘、长期绑定或重平台化这类不可逆决策?' },
532
+ { section: 'validation', path: 'validation.customerTruth', label: '客户真问题', prompt: '这更像在解决客户真问题,还是满足内部冲动?' },
533
+ { section: 'validation', path: 'validation.valuesFit', label: '价值观一致性', prompt: '这条路是否符合团队想坚持的价值观和长期经营方式?如果连续这样做 3 到 5 年,你还愿意住在这套业务里吗?' },
534
+ ];
535
+
462
536
  const TYPE_REQUIRED_FIELD_DESCRIPTORS = {
463
537
  consumer: [
464
538
  { section: 'consumer', path: 'typeSpecific.fields.persona', label: '用户画像', prompt: '目标用户画像是什么?' },
@@ -517,8 +591,49 @@ function isMissingPrdValue(value) {
517
591
  return false;
518
592
  }
519
593
 
520
- export function getRequiredFieldDescriptors(productType) {
594
+ function needsValidationLoop(snapshot) {
595
+ const sections = snapshot?.sections ?? {};
596
+ const validation = sections.validation ?? {};
597
+ const explicitValidationSignals = [
598
+ validation.community,
599
+ validation.seedUsers,
600
+ validation.communityFit,
601
+ validation.painEvidence,
602
+ validation.manualPath,
603
+ validation.manualPlaybook,
604
+ validation.commitmentSignals,
605
+ validation.defaultAlivePlan,
606
+ validation.paymentProof,
607
+ validation.mvpSlice,
608
+ validation.weekendTest,
609
+ validation.smallestExecution,
610
+ validation.productizeGate,
611
+ validation.firstCustomerPath,
612
+ validation.pricingHypothesis,
613
+ validation.customerOneProfitability,
614
+ validation.growthDiscipline,
615
+ validation.reversibility,
616
+ validation.customerTruth,
617
+ validation.valuesFit,
618
+ ];
619
+ if (explicitValidationSignals.some((value) => !isMissingPrdValue(value))) {
620
+ return true;
621
+ }
622
+ const text = flattenForSearch({
623
+ problem: sections.problem,
624
+ goals: sections.goals,
625
+ scope: sections.scope,
626
+ requirements: sections.requirements,
627
+ risks: sections.risks,
628
+ });
629
+ return /(创业|脑暴|brainstorm|0\s*(到|to)\s*1|值不值得|worth doing|种子用户|第一批(客户|用户)|社区|workaround|手工交付|顾问式|一件事\s*mvp|周末级|首批成交|default alive|productize|no-code|spreadsheet|first customer)/iu.test(text);
630
+ }
631
+
632
+ export function getRequiredFieldDescriptors(productType, options = {}) {
521
633
  const descriptors = [...BASE_REQUIRED_FIELD_DESCRIPTORS];
634
+ if (options.includeValidation) {
635
+ descriptors.push(...VALIDATION_REQUIRED_FIELD_DESCRIPTORS);
636
+ }
522
637
  if (TYPE_REQUIRED_FIELD_DESCRIPTORS[productType]) {
523
638
  descriptors.push(...TYPE_REQUIRED_FIELD_DESCRIPTORS[productType]);
524
639
  }
@@ -550,7 +665,8 @@ export function needsBusinessGuardrails(snapshot) {
550
665
 
551
666
  export function analyzePrdSnapshot(snapshot) {
552
667
  const productType = snapshot.productType ?? null;
553
- const descriptors = getRequiredFieldDescriptors(productType);
668
+ const includeValidation = needsValidationLoop(snapshot);
669
+ const descriptors = getRequiredFieldDescriptors(productType, { includeValidation });
554
670
  if (needsBusinessGuardrails(snapshot)) {
555
671
  descriptors.push(...BUSINESS_GUARDRAIL_FIELD_DESCRIPTORS);
556
672
  }
@@ -578,6 +694,7 @@ export function analyzePrdSnapshot(snapshot) {
578
694
 
579
695
  return {
580
696
  productType,
697
+ includeValidation,
581
698
  totalRequiredFields,
582
699
  completedRequiredFields,
583
700
  missingRequiredFields: missingFields.length,
@@ -517,13 +517,38 @@ function exceptionItems(report) {
517
517
  }));
518
518
  }
519
519
 
520
+ const EVIDENCE_SOURCE_LABELS = {
521
+ 'openprd-tasks': '任务清单',
522
+ 'project-scan': '项目扫描',
523
+ 'openprd-knowledge': '项目经验库',
524
+ 'openprd-knowledge-candidate': '待确认经验草案',
525
+ 'openprd-growth-ledger': '成长记录',
526
+ 'openprd-test-report': '测试报告',
527
+ 'openprd-visual-review': '视觉对比记录',
528
+ evidence: '证据文件',
529
+ };
530
+
531
+ const EVIDENCE_PATH_LABELS = {
532
+ 'no-active-change': '当前没有进行中的需求改动',
533
+ 'no-cost-risk-detected': '本次没有发现成本相关风险',
534
+ 'business-guardrails-evidence': '成本与滥用护栏证据',
535
+ };
536
+
537
+ function evidenceSourceLabel(source) {
538
+ return EVIDENCE_SOURCE_LABELS[source] ?? source;
539
+ }
540
+
541
+ function evidencePathLabel(path) {
542
+ return EVIDENCE_PATH_LABELS[path] ?? path;
543
+ }
544
+
520
545
  function evidenceItems(report) {
521
546
  return evidenceRows(report)
522
547
  .filter((row) => !row.empty)
523
548
  .slice(0, 5)
524
549
  .map((row) => ({
525
550
  summary: gateDisplay(row.gate),
526
- detail: `${row.source},${row.path}`,
551
+ detail: `${evidenceSourceLabel(row.source)},${evidencePathLabel(row.path)}`,
527
552
  }));
528
553
  }
529
554
 
@@ -573,8 +598,8 @@ function environmentItems(report) {
573
598
  {
574
599
  summary: '成长账本',
575
600
  detail: Number(growth.summary?.eventCount ?? 0) > 0
576
- ? `已记录 ${growth.summary.eventCount} 条事件,completion checkpoint ${growth.summary.completionCheckpoints ?? 0} 条`
577
- : '当前还没有成长账本事件;完成态至少应留下 checkpoint 或候选',
601
+ ? `已记录 ${growth.summary.eventCount} 条成长事件,其中收尾检查记录 ${growth.summary.completionCheckpoints ?? 0} 条`
602
+ : '当前还没有成长记录;收尾前至少应留下一条收尾检查记录或经验草案',
578
603
  },
579
604
  ];
580
605
  }
@@ -692,8 +717,8 @@ function tableRowsForEvidence(report) {
692
717
  return evidenceRows(report).map((row) => `
693
718
  <tr>
694
719
  <td>${escapeHtml(gateDisplay(row.gate))}</td>
695
- <td>${escapeHtml(row.source)}</td>
696
- <td><code>${escapeHtml(row.path)}</code></td>
720
+ <td>${escapeHtml(evidenceSourceLabel(row.source))}</td>
721
+ <td><code>${escapeHtml(evidencePathLabel(row.path))}</code></td>
697
722
  <td>${row.gate.required ? '本期必测' : '按风险确认'}</td>
698
723
  </tr>
699
724
  `).join('\n');
@@ -575,9 +575,24 @@ function renderList(items, fallback) {
575
575
  return list.map((item) => `- ${item}`).join('\n')
576
576
  }
577
577
 
578
- function renderEvidenceSources(evidenceSources) {
578
+ function describeEvidenceSource(source, options = {}) {
579
+ if (!options.genericPaths) {
580
+ return `${source.kind}: \`${source.path}\``;
581
+ }
582
+ const genericLabel = {
583
+ 'quality-report': '当前项目里最新一轮相关的质量报告与 HTML 评估页',
584
+ 'diagnostic-bundle': '当前任务对应的诊断目录',
585
+ 'diagnostic-report': '当前任务对应的 diagnostic-report',
586
+ 'runtime-events': '当前任务对应的 runtime-events',
587
+ timeline: '当前任务对应的 timeline',
588
+ 'root-cause-candidates': '当前任务对应的 root-cause-candidates',
589
+ }[source.kind] ?? '当前任务对应的相关证据';
590
+ return `${source.kind}: ${genericLabel}`;
591
+ }
592
+
593
+ function renderEvidenceSources(evidenceSources, options = {}) {
579
594
  return renderList(
580
- evidenceSources.map((source) => `${source.kind}: \`${source.path}\``),
595
+ evidenceSources.map((source) => describeEvidenceSource(source, options)),
581
596
  '当前来源没有显式证据清单,后续应补齐标准诊断目录。',
582
597
  )
583
598
  }
@@ -608,6 +623,18 @@ function renderCorrelationSection({ correlationFields, extraContextFields, missi
608
623
  }
609
624
 
610
625
  export function renderExperienceSkill({ skillName, source }) {
626
+ const stableProblemSummary = source.kind === 'quality-report'
627
+ ? (
628
+ source.attentionGates.length > 0
629
+ ? `当前任务的质量收口与门禁闭环(${source.attentionGates.slice(0, 4).join(', ')})`
630
+ : '当前任务的质量收口与门禁闭环'
631
+ )
632
+ : (
633
+ source.rootCauseCandidates[0]?.title
634
+ ?? source.eventNames[0]
635
+ ?? source.symptoms[0]
636
+ ?? '当前任务对应的问题症状'
637
+ );
611
638
  const triggers = source.kind === 'quality-report'
612
639
  ? [
613
640
  '某项任务改动了前端、后端、agent 工作流或错误处理行为。',
@@ -626,9 +653,9 @@ export function renderExperienceSkill({ skillName, source }) {
626
653
  : '适用于已经拿到 runtime-events、timeline、root-cause-candidates 或 diagnostic-report 的排查任务。',
627
654
  ]
628
655
  const typicalInputs = [
629
- source.title ? `问题摘要: ${source.title}` : null,
656
+ stableProblemSummary ? `问题摘要: ${stableProblemSummary}` : null,
630
657
  source.evidenceSources.length > 0
631
- ? `可直接使用的证据: ${source.evidenceSources.slice(0, 4).map((item) => `${item.kind}:${item.path}`).join(';')}`
658
+ ? `可直接使用的证据类型: ${source.evidenceSources.slice(0, 4).map((item) => item.kind).join(';')}`
632
659
  : null,
633
660
  source.eventNames.length > 0 ? `关键事件: ${source.eventNames.join(' -> ')}` : null,
634
661
  ]
@@ -637,10 +664,23 @@ export function renderExperienceSkill({ skillName, source }) {
637
664
  source.verificationSteps.length > 0 ? `最小验证链路: ${source.verificationSteps[0]}` : null,
638
665
  '可复用的项目级 knowledge skill 或后续 candidate / incident / pattern 记录。',
639
666
  ]
667
+ const useWhen = source.kind === 'quality-report'
668
+ ? 'Use when the current task is entering quality closeout and you should reuse a proven evidence and gate-check path instead of rebuilding the checklist from scratch.'
669
+ : 'Use when the current task overlaps this verified diagnosis pattern, symptoms, or touched files and you should reuse proven evidence before starting from scratch.'
670
+ const antiPatterns = [
671
+ source.kind === 'quality-report'
672
+ ? '如果当前任务还没进入验证或收尾阶段,不要把这条经验当成通用实现模板直接套用。'
673
+ : '如果当前问题没有出现相似症状、关键事件或证据入口,不要只因为改到相似文件就直接套用这条经验。',
674
+ '如果只是文件名、路径或个别词相似,但当前目标、阶段或验证方式不同,不要直接照搬。',
675
+ source.evidenceSources.length > 0
676
+ ? '如果本轮已经拿到更新的现场证据,先核对新证据,再决定是否复用这条经验。'
677
+ : null,
678
+ ]
640
679
 
641
680
  return `---
642
681
  name: ${skillName}
643
- description: 由 OpenPrd 从 ${source.kind} 自动沉淀的项目级排查经验。目标是在相似问题再次出现时优先复用现有诊断证据,而不是临时补日志。
682
+ description: ${useWhen}
683
+ use_when: ${useWhen}
644
684
  ---
645
685
 
646
686
  # ${skillName}
@@ -663,7 +703,11 @@ ${renderList(typicalOutputs, '至少产出可复用结论、验证链路和后
663
703
 
664
704
  ## 先看哪些证据
665
705
 
666
- ${renderEvidenceSources(source.evidenceSources)}
706
+ ${renderEvidenceSources(source.evidenceSources, { genericPaths: true })}
707
+
708
+ ## 不要直接套用
709
+
710
+ ${renderList(antiPatterns, '如果只是表面相似,但目标和验证方式不一致,不要直接套用。')}
667
711
 
668
712
  ## 关联字段
669
713
 
package/src/quality.js CHANGED
@@ -115,6 +115,13 @@ const EVIDENCE_TOKENS = {
115
115
  growth: ['growth ledger', 'completion checkpoint', 'openprd grow', 'workflow-gotcha', 'code-extension', '自我成长', '账本', '候选'],
116
116
  };
117
117
 
118
+ const DIAGNOSTIC_SURFACE_LABELS = {
119
+ 'runtime-events': '运行事件记录',
120
+ timeline: '问题时间线',
121
+ 'root-cause-candidates': '根因排查线索',
122
+ 'diagnostic-report': '诊断报告',
123
+ };
124
+
118
125
  function qualityPath(projectRoot, relativePath) {
119
126
  return cjoin(projectRoot, relativePath);
120
127
  }
@@ -606,18 +613,21 @@ function buildEvidenceLedger({ evidenceFiles, activeTasks, observability, busine
606
613
  };
607
614
  }
608
615
  if (observability.status === 'pass') {
609
- const observabilitySignals = [
610
- ...observability.centralizedTools,
611
- ...(observability.diagnosticSurfaces ?? []),
612
- ];
616
+ const toolCount = observability.centralizedTools.length;
617
+ const surfaceLabels = (observability.diagnosticSurfaces ?? [])
618
+ .map((surface) => DIAGNOSTIC_SURFACE_LABELS[surface] ?? surface);
619
+ const signalSummary = [
620
+ toolCount > 0 ? `${toolCount} 类日志或追踪工具` : '',
621
+ surfaceLabels.length > 0 ? `${surfaceLabels.length} 类诊断记录` : '',
622
+ ].filter(Boolean).join('和');
613
623
  ledger.traceability = {
614
624
  ...ledger.traceability,
615
625
  present: true,
616
626
  sources: [
617
627
  ...ledger.traceability.sources,
618
- { path: 'project-observability-signals', source: observabilitySignals.join(', ') || 'observability' },
628
+ { path: '项目内的日志与追踪配置', source: signalSummary ? `检测到${signalSummary}` : '日志追踪检查' },
619
629
  ].slice(0, 12),
620
- summary: `检测到 ${observability.correlationFields.length} 个链路关联字段${observability.diagnosticSurfaces?.length ? `;诊断面: ${observability.diagnosticSurfaces.join(', ')}` : ''}`,
630
+ summary: `出问题时可以追查:检测到 ${observability.correlationFields.length} 个追踪线索${surfaceLabels.length > 0 ? `,并留有${surfaceLabels.join('')}` : ''}`,
621
631
  };
622
632
  }
623
633
  if (!businessGuardrails.riskDetected || businessGuardrails.status === 'pass') {
@@ -641,8 +651,8 @@ function buildEvidenceLedger({ evidenceFiles, activeTasks, observability, busine
641
651
  ...knowledge.candidates.slice(0, 3).map((candidate) => ({ path: candidate, source: 'openprd-knowledge-candidate' })),
642
652
  ].slice(0, 12),
643
653
  summary: knowledge.candidates.length > 0
644
- ? `已有 ${knowledge.skills.length} 个项目经验 Skill,命中 ${knowledge.adoption?.totals?.hit ?? 0} / 引用 ${knowledge.adoption?.totals?.referenced ?? 0} / 注入 ${knowledge.adoption?.totals?.injected ?? 0},另有 ${knowledge.candidates.length} 个待确认 candidate`
645
- : `已有 ${knowledge.skills.length} 个项目经验 Skill,命中 ${knowledge.adoption?.totals?.hit ?? 0} / 引用 ${knowledge.adoption?.totals?.referenced ?? 0} / 注入 ${knowledge.adoption?.totals?.injected ?? 0}`,
654
+ ? `已沉淀 ${knowledge.skills.length} 条项目经验,近期被实际复用 ${knowledge.adoption?.totals?.referenced ?? 0} 次,另有 ${knowledge.candidates.length} 条经验草案等确认`
655
+ : `已沉淀 ${knowledge.skills.length} 条项目经验,近期被实际复用 ${knowledge.adoption?.totals?.referenced ?? 0} 次`,
646
656
  };
647
657
  } else if (knowledge.candidates.length > 0) {
648
658
  ledger.knowledge = {
@@ -652,7 +662,7 @@ function buildEvidenceLedger({ evidenceFiles, activeTasks, observability, busine
652
662
  ...ledger.knowledge.sources,
653
663
  ...knowledge.candidates.slice(0, 6).map((candidate) => ({ path: candidate, source: 'openprd-knowledge-candidate' })),
654
664
  ].slice(0, 12),
655
- summary: `已有 ${knowledge.candidates.length} 个待确认 knowledge candidate`,
665
+ summary: `已有 ${knowledge.candidates.length} 条经验草案等确认,确认后会成为可复用的项目经验`,
656
666
  };
657
667
  }
658
668
  if (growth?.summary) {
@@ -666,7 +676,7 @@ function buildEvidenceLedger({ evidenceFiles, activeTasks, observability, busine
666
676
  ...ledger.growth.sources,
667
677
  { path: growth.ledgerPath ?? OPENPRD_GROWTH_LEDGER, source: 'openprd-growth-ledger' },
668
678
  ].slice(0, 12),
669
- summary: `growth 账本已有 ${growth.summary.eventCount ?? 0} 条事件,其中 completion checkpoint ${completionCheckpoints} 条`,
679
+ summary: `成长记录已有 ${growth.summary.eventCount ?? 0} 条,其中收尾检查记录 ${completionCheckpoints} 条`,
670
680
  };
671
681
  }
672
682
  }
@@ -1019,7 +1029,7 @@ function detectKnowledge({ config, knowledgeFiles, candidateState, knowledgeInde
1019
1029
  warnings.push('本次已经达到可交付状态,但还没有自动生成 knowledge candidate;收工前至少保留一条可审查的项目经验草案。');
1020
1030
  }
1021
1031
  if (config.knowledge.enabled && skills.length > 0 && adoption.totals.referenced === 0) {
1022
- warnings.push('项目级经验 skill 已产出,但还没有任何 run-context 引用记录;优先接入自动命中与注入链路。');
1032
+ warnings.push('项目级经验 skill 已产出,但还没有任何 run-context 引用记录;优先接入候选召回与判断注入链路。');
1023
1033
  }
1024
1034
  return {
1025
1035
  status: !config.knowledge.enabled || hasReusableArtifact ? 'pass' : 'needs-attention',
@@ -1444,7 +1454,7 @@ export async function learnQualityWorkspace(projectRoot, options = {}) {
1444
1454
  };
1445
1455
  }
1446
1456
  const source = resolved.source;
1447
- const { incidentId, patternId, skillName } = deriveKnowledgeNames(source);
1457
+ const { incidentId, patternId, skillName } = deriveKnowledgeNames(source, { stablePattern: true });
1448
1458
  const incidentPath = qualityPath(projectRoot, cjoin(KNOWLEDGE_DIR, 'incidents', `${incidentId}.json`));
1449
1459
  const patternPath = qualityPath(projectRoot, cjoin(KNOWLEDGE_DIR, 'patterns', `${patternId}.json`));
1450
1460
  const skillDir = qualityPath(projectRoot, cjoin(KNOWLEDGE_DIR, 'skills', skillName));