@openprd/cli 0.1.9 → 0.1.10

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 (134) hide show
  1. package/.openprd/benchmarks/index.md +15 -1
  2. package/.openprd/config.yaml +10 -0
  3. package/.openprd/design/README.md +47 -0
  4. package/.openprd/design/README_EN.md +25 -0
  5. package/.openprd/design/active/asset-spec.md +15 -0
  6. package/.openprd/design/active/direction-plan.md +7 -0
  7. package/.openprd/design/active/facts-sheet.md +13 -0
  8. package/.openprd/design/active/image-preflight.md +9 -0
  9. package/.openprd/design/active/selected-direction.md +11 -0
  10. package/.openprd/design/anti-slop.md +16 -0
  11. package/.openprd/design/assets/dark-mesh.svg +14 -0
  12. package/.openprd/design/assets/paper-grid.svg +14 -0
  13. package/.openprd/design/assets/surface-presets.md +21 -0
  14. package/.openprd/design/assets/workbench-grid.svg +16 -0
  15. package/.openprd/design/checklists/ui-quality-gate.md +22 -0
  16. package/.openprd/design/components/component-catalog.md +42 -0
  17. package/.openprd/design/layouts/layout-catalog.json +45 -0
  18. package/.openprd/design/lenses/frontend-lenses.md +31 -0
  19. package/.openprd/design/recipes/content-experience.md +26 -0
  20. package/.openprd/design/recipes/operational-tool.md +25 -0
  21. package/.openprd/design/recipes/product-launch.md +26 -0
  22. package/.openprd/design/templates/README.md +36 -0
  23. package/.openprd/design/templates/README_EN.md +31 -0
  24. package/.openprd/design/templates/content-home.html +415 -0
  25. package/.openprd/design/templates/ops-dashboard.html +340 -0
  26. package/.openprd/design/templates/product-launch.html +360 -0
  27. package/.openprd/design/themes/theme-catalog.json +77 -0
  28. package/.openprd/engagements/active/prd.md +111 -100
  29. package/.openprd/schema/prd.schema.yaml +10 -0
  30. package/.openprd/templates/base/intake.md +4 -0
  31. package/.openprd/templates/base/prd.md +16 -6
  32. package/AGENTS.md +10 -7
  33. package/README.md +26 -28
  34. package/README_EN.md +3 -3
  35. package/package.json +41 -2
  36. package/scripts/openprd-codex-isolated-worker.mjs +799 -0
  37. package/skills/openprd-benchmark-router/SKILL.md +4 -0
  38. package/skills/openprd-frontend-design/SKILL.md +161 -0
  39. package/skills/openprd-frontend-design/references/design-asset-contract.md +34 -0
  40. package/skills/openprd-frontend-design/references/direction-engine.md +46 -0
  41. package/skills/openprd-harness/SKILL.md +37 -20
  42. package/skills/openprd-learning-review/SKILL.md +2 -1
  43. package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +1 -1
  44. package/skills/openprd-quality/SKILL.md +5 -3
  45. package/skills/openprd-requirement-intake/SKILL.md +27 -5
  46. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -3
  47. package/skills/openprd-requirement-intake/references/routing-rubric.md +9 -5
  48. package/skills/openprd-requirement-intake/references/startup-validation-lens.md +23 -0
  49. package/skills/openprd-router/SKILL.md +13 -5
  50. package/skills/openprd-shared/SKILL.md +38 -25
  51. package/src/agent-integration.js +120 -69
  52. package/src/brainstorm-artifacts.js +1657 -0
  53. package/src/brainstorm-presentation.js +223 -0
  54. package/src/brainstorm.js +603 -0
  55. package/src/cli/args.js +51 -3
  56. package/src/cli/doctor-print.js +34 -8
  57. package/src/cli/print.js +6 -0
  58. package/src/cli/quality-print.js +70 -0
  59. package/src/cli/run-print.js +43 -1
  60. package/src/cli/shared-print.js +33 -2
  61. package/src/cli/workflow-print.js +21 -0
  62. package/src/codex-hook-runner-template.mjs +838 -58
  63. package/src/design-starter-support.js +462 -0
  64. package/src/design-starter.js +1207 -0
  65. package/src/knowledge.js +232 -20
  66. package/src/learning-review.js +3 -3
  67. package/src/loop.js +839 -101
  68. package/src/openprd.js +86 -1
  69. package/src/openspec/change-validate.js +1 -0
  70. package/src/prd-core.js +18 -0
  71. package/src/quality-learning.js +50 -6
  72. package/src/quality.js +2 -2
  73. package/src/run-harness.js +191 -29
  74. package/src/self-update.js +301 -4
  75. package/src/standards.js +46 -5
  76. package/src/visual-prepare.js +940 -0
  77. package/src/workspace-core.js +137 -0
  78. package/src/workspace-workflow.js +190 -7
  79. package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +0 -14
  80. package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +0 -14
  81. package/.openprd/discovery/config.json +0 -35
  82. package/.openprd/engagements/active/flows.md +0 -35
  83. package/.openprd/engagements/active/handoff.md +0 -16
  84. package/.openprd/engagements/active/review.html +0 -61
  85. package/.openprd/engagements/active/roles.md +0 -22
  86. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +0 -23
  87. package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +0 -18
  88. package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +0 -18
  89. package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +0 -18
  90. package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +0 -18
  91. package/.openprd/exports/.gitkeep +0 -0
  92. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +0 -78
  93. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +0 -129
  94. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +0 -41
  95. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +0 -14
  96. package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +0 -49
  97. package/.openprd/knowledge/index.json +0 -47
  98. package/.openprd/reviews/v0001.html +0 -1322
  99. package/.openprd/reviews/v0002.html +0 -1150
  100. package/.openprd/reviews/v0003.html +0 -1150
  101. package/.openprd/reviews/v0004.html +0 -1150
  102. package/.openprd/reviews/v0005.html +0 -1150
  103. package/.openprd/sessions/.gitkeep +0 -0
  104. package/.openprd/state/changes.json +0 -27
  105. package/.openprd/state/current.json +0 -505
  106. package/.openprd/state/release-ledger.json +0 -387
  107. package/.openprd/state/version-index.json +0 -67
  108. package/.openprd/state/versions/.gitkeep +0 -0
  109. package/.openprd/state/versions/v0001.json +0 -121
  110. package/.openprd/state/versions/v0001.md +0 -161
  111. package/.openprd/state/versions/v0002.json +0 -264
  112. package/.openprd/state/versions/v0002.md +0 -183
  113. package/.openprd/state/versions/v0003.json +0 -269
  114. package/.openprd/state/versions/v0003.md +0 -188
  115. package/.openprd/state/versions/v0004.json +0 -274
  116. package/.openprd/state/versions/v0004.md +0 -193
  117. package/.openprd/state/versions/v0005.json +0 -299
  118. package/.openprd/state/versions/v0005.md +0 -189
  119. package/docs/assets/openprd-capability-overview-en.png +0 -0
  120. package/docs/assets/openprd-capability-overview-zh.png +0 -0
  121. package/docs/assets/openprd-learning-html.png +0 -0
  122. package/docs/assets/openprd-quality-html.png +0 -0
  123. package/docs/assets/openprd-requirement-routing-en.png +0 -0
  124. package/docs/assets/openprd-requirement-routing-en.svg +0 -102
  125. package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
  126. package/docs/assets/openprd-requirement-routing-zh.png +0 -0
  127. package/docs/assets/openprd-requirement-routing-zh.svg +0 -102
  128. package/docs/assets/openprd-review-html.png +0 -0
  129. package/docs/assets/openprd-scenario-overview.png +0 -0
  130. package/docs/assets/openprd-scenario-overview.svg +0 -114
  131. package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
  132. package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
  133. package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
  134. package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
package/src/knowledge.js CHANGED
@@ -170,6 +170,10 @@ function normalizeSkillIndexEntry(entry = {}) {
170
170
  ]),
171
171
  categories: normalizeStringList(skill.categories),
172
172
  triggerHints: normalizeStringList(skill.triggerHints),
173
+ useWhen: firstString(skill.useWhen, skill.use_when),
174
+ applicability: normalizeStringList(skill.applicability),
175
+ antiPatterns: normalizeStringList(skill.antiPatterns),
176
+ reviewFirst: normalizeStringList(skill.reviewFirst),
173
177
  touchedFiles: normalizeStringList(skill.touchedFiles),
174
178
  evidencePaths: normalizeStringList(skill.evidencePaths),
175
179
  rootCauseLabels: normalizeStringList(skill.rootCauseLabels),
@@ -213,6 +217,37 @@ function sortByLength(items = []) {
213
217
  return [...items].sort((left, right) => String(right).length - String(left).length);
214
218
  }
215
219
 
220
+ function isGenericKnowledgeDescription(value) {
221
+ const normalized = normalizeSearchText(value);
222
+ if (!normalized) {
223
+ return true;
224
+ }
225
+ return [
226
+ '自动沉淀的项目级排查经验',
227
+ '待确认项目经验草案',
228
+ '自动生成的待确认项目经验草案',
229
+ '项目级排查经验',
230
+ '项目经验草案',
231
+ ].some((needle) => normalized.includes(normalizeSearchText(needle)));
232
+ }
233
+
234
+ function stripUseWhenPrefix(value) {
235
+ return String(value ?? '').trim().replace(/^use when\b[::]?\s*/i, '').trim();
236
+ }
237
+
238
+ function deriveSkillUseWhen({ useWhen, description, applicability = [], triggerHints = [] }) {
239
+ const explicit = firstString(useWhen);
240
+ if (explicit) {
241
+ return explicit;
242
+ }
243
+ const desc = firstString(description);
244
+ if (desc && !isGenericKnowledgeDescription(desc)) {
245
+ return desc;
246
+ }
247
+ const fallback = firstString(applicability[0], triggerHints[0], desc);
248
+ return fallback ? `Use when ${stripUseWhenPrefix(fallback)}` : null;
249
+ }
250
+
216
251
  function scoreQueryAgainstFields(queryText, queryTokens, fields = []) {
217
252
  let score = 0;
218
253
  const matchedOn = [];
@@ -264,22 +299,47 @@ function parseMarkdownSectionList(markdown, headings = []) {
264
299
  return uniq(collected);
265
300
  }
266
301
 
267
- function parseSkillMetadataFromText(markdown) {
302
+ function parseFrontmatterValue(markdown, key) {
268
303
  const text = String(markdown ?? '');
269
304
  const frontmatter = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
270
- const descriptionLine = frontmatter?.[1]
271
- ?.split(/\r?\n/)
272
- .map((line) => line.trim())
273
- .find((line) => line.startsWith('description:'));
274
- const description = descriptionLine ? descriptionLine.replace(/^description:\s*/, '').trim() : null;
275
- const triggerHints = parseMarkdownSectionList(text, ['触发场景', '常见误判', '先看什么', '收尾顺序', '反模式', '下次触发时先看什么']);
276
- const rootCauseLabels = parseMarkdownSectionList(text, ['可复用模式']);
305
+ if (!frontmatter) {
306
+ return null;
307
+ }
308
+ const pattern = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:\\s*(.+?)\\s*$`);
309
+ const line = frontmatter[1]
310
+ .split(/\r?\n/)
311
+ .map((item) => item.trim())
312
+ .find((item) => pattern.test(item));
313
+ if (!line) {
314
+ return null;
315
+ }
316
+ const match = line.match(pattern);
317
+ return match?.[1]?.trim() ?? null;
318
+ }
319
+
320
+ function parseSkillMetadataFromText(markdown) {
321
+ const text = String(markdown ?? '');
322
+ const description = parseFrontmatterValue(text, 'description');
323
+ const useWhen = firstString(
324
+ parseFrontmatterValue(text, 'use_when'),
325
+ parseFrontmatterValue(text, 'use-when'),
326
+ /^use when\b/i.test(String(description ?? '')) ? description : null,
327
+ );
328
+ const triggerHints = parseMarkdownSectionList(text, ['触发场景', '触发条件', '同步触发条件', '收尾顺序']);
329
+ const applicability = parseMarkdownSectionList(text, ['适用范围', '适用时机', 'When to Use', 'When to use']);
330
+ const antiPatterns = parseMarkdownSectionList(text, ['不要直接套用', '不要在这些情况使用', '不要在这些情况直接套用', 'When Not to Use', 'Do Not Use When', '反模式', '常见误判']);
331
+ const reviewFirst = parseMarkdownSectionList(text, ['下次触发时先看什么', '先看什么', '先看哪些证据', '判断前先看', '排查顺序']);
332
+ const rootCauseLabels = parseMarkdownSectionList(text, ['可复用模式', '常见根因']);
277
333
  const evidencePaths = uniq((text.match(/`([^`]+)`/g) ?? [])
278
334
  .map((entry) => entry.replace(/`/g, '').trim())
279
335
  .filter((entry) => entry.includes('/') || entry.endsWith('.md') || entry.endsWith('.js') || entry.endsWith('.ts')));
280
336
  return {
281
337
  description,
338
+ useWhen,
282
339
  triggerHints,
340
+ applicability,
341
+ antiPatterns,
342
+ reviewFirst,
283
343
  rootCauseLabels,
284
344
  evidencePaths,
285
345
  };
@@ -377,6 +437,18 @@ async function hydrateKnowledgeSkillEntry(projectRoot, entry, cache = new Map())
377
437
  ...parsedSkill.triggerHints,
378
438
  ...candidateBundles.flatMap((bundle) => bundle.triggerHints),
379
439
  ]).slice(0, 24),
440
+ applicability: uniq([
441
+ ...current.applicability,
442
+ ...parsedSkill.applicability,
443
+ ]).slice(0, 24),
444
+ antiPatterns: uniq([
445
+ ...current.antiPatterns,
446
+ ...parsedSkill.antiPatterns,
447
+ ]).slice(0, 24),
448
+ reviewFirst: uniq([
449
+ ...current.reviewFirst,
450
+ ...parsedSkill.reviewFirst,
451
+ ]).slice(0, 24),
380
452
  touchedFiles: uniq([
381
453
  ...current.touchedFiles,
382
454
  ...candidateBundles.flatMap((bundle) => bundle.touchedFiles),
@@ -391,6 +463,19 @@ async function hydrateKnowledgeSkillEntry(projectRoot, entry, cache = new Map())
391
463
  ...parsedSkill.rootCauseLabels,
392
464
  ...candidateBundles.flatMap((bundle) => bundle.rootCauseLabels),
393
465
  ]).slice(0, 24),
466
+ useWhen: deriveSkillUseWhen({
467
+ useWhen: current.useWhen ?? parsedSkill.useWhen,
468
+ description: current.description ?? parsedSkill.description,
469
+ applicability: uniq([
470
+ ...current.applicability,
471
+ ...parsedSkill.applicability,
472
+ ]),
473
+ triggerHints: uniq([
474
+ ...current.triggerHints,
475
+ ...parsedSkill.triggerHints,
476
+ ...candidateBundles.flatMap((bundle) => bundle.triggerHints),
477
+ ]),
478
+ }),
394
479
  description: current.description ?? parsedSkill.description,
395
480
  summary: current.summary ?? candidateBundles.map((bundle) => bundle.summary).find(Boolean) ?? null,
396
481
  adoption: normalizeSkillAdoption(current.adoption),
@@ -644,6 +729,29 @@ function summarizeReviewSignalKinds(reviewSignals = []) {
644
729
  return uniq(reviewSignals.map((signal) => signal.kind).filter(Boolean)).slice(0, 6);
645
730
  }
646
731
 
732
+ function summarizeKnowledgeScenario(source = {}, candidate = null) {
733
+ if (source.kind === 'quality-report') {
734
+ return source.attentionGates.length > 0
735
+ ? `任务场景: 质量门禁收口(${source.attentionGates.slice(0, 4).join('、')})`
736
+ : '任务场景: 质量门禁收口';
737
+ }
738
+ const topRootCause = source.rootCauseCandidates?.[0]?.title ?? null;
739
+ if (topRootCause) {
740
+ return `任务场景: ${topRootCause}`;
741
+ }
742
+ const topTrigger = normalizeStringList(source.triggers)[0] ?? null;
743
+ if (topTrigger) {
744
+ return `任务场景: ${topTrigger}`;
745
+ }
746
+ const fallbackTitle = firstString(candidate?.title, source.title);
747
+ return fallbackTitle ? `任务场景: ${fallbackTitle}` : null;
748
+ }
749
+
750
+ function summarizeEvidenceKinds(evidenceSources = []) {
751
+ const kinds = uniq(evidenceSources.map((item) => firstString(item.kind)).filter(Boolean));
752
+ return kinds.length > 0 ? `已有证据类型: ${kinds.join('、')}` : null;
753
+ }
754
+
647
755
  function buildKnowledgeAbstraction({
648
756
  candidate,
649
757
  source,
@@ -670,27 +778,35 @@ function buildKnowledgeAbstraction({
670
778
  ...source.verificationSteps,
671
779
  ]).slice(0, 8);
672
780
  const typicalInputs = uniq([
673
- firstString(source.title, candidate.title) ? `任务摘要: ${firstString(source.title, candidate.title)}` : null,
781
+ summarizeKnowledgeScenario(source, candidate),
674
782
  touchedFiles.length > 0 ? `相关文件: ${touchedFiles.slice(0, 6).join('、')}` : null,
675
- source.evidenceSources.length > 0
676
- ? `已有证据: ${source.evidenceSources.slice(0, 4).map((item) => `${item.kind}:${item.path}`).join(';')}`
677
- : null,
783
+ summarizeEvidenceKinds(source.evidenceSources),
678
784
  reviewSignals.length > 0
679
785
  ? `验证信号: ${summarizeReviewSignalKinds(reviewSignals).join('、')}`
680
786
  : null,
681
787
  ]).slice(0, 6);
682
788
  const typicalOutputs = uniq([
683
- relativeCandidateDir ? `knowledge candidate: ${relativeCandidateDir}/candidate.json` : null,
684
- relativeCandidateDir ? `诊断报告: ${relativeCandidateDir}/diagnostic-report.json` : null,
685
- relativeDraftSkillPath ? `draft skill: ${relativeDraftSkillPath}` : null,
789
+ relativeCandidateDir ? '项目经验候选与诊断包' : null,
790
+ relativeDraftSkillPath ? '待确认的项目经验草案' : null,
686
791
  verificationSteps[0] ? `验证结论: ${verificationSteps[0]}` : null,
792
+ '可复用的验证链路与收尾动作',
687
793
  ]).slice(0, 6);
794
+ const antiPatterns = uniq([
795
+ '如果只是文件名、路径或个别词相似,但当前目标和验证方式不同,不要直接套用。',
796
+ source.kind === 'quality-report'
797
+ ? '如果当前任务还没进入验证或收尾阶段,不要把这条经验当成通用实现模板。'
798
+ : '如果当前问题没有出现相似症状、事件或证据入口,不要因为改到相似模块就直接照搬旧结论。',
799
+ source.evidenceSources.length > 0
800
+ ? '如果本轮已经有更新的现场证据,先核对新证据,再决定是否复用旧经验。'
801
+ : null,
802
+ ]).slice(0, 4);
688
803
  return {
689
804
  triggerConditions,
690
805
  applicability,
691
806
  verificationSteps,
692
807
  typicalInputs,
693
808
  typicalOutputs,
809
+ antiPatterns,
694
810
  };
695
811
  }
696
812
 
@@ -707,13 +823,37 @@ function categoryReason(category) {
707
823
  return '这次实现已经具备沉淀项目经验的价值。';
708
824
  }
709
825
 
710
- function deriveKnowledgeNames(source) {
826
+ function deriveStablePatternKey(source = {}) {
827
+ if (source.kind === 'quality-report') {
828
+ const gates = normalizeStringList(source.attentionGates);
829
+ if (gates.length > 0) {
830
+ return gates.slice(0, 4).join('-');
831
+ }
832
+ return 'quality-closeout';
833
+ }
834
+ const rootCauseLabels = normalizeStringList(source.rootCauseCandidates?.map((item) => item.title));
835
+ if (rootCauseLabels.length > 0) {
836
+ return rootCauseLabels.slice(0, 3).join('-');
837
+ }
838
+ const triggers = normalizeStringList(source.triggers);
839
+ if (triggers.length > 0) {
840
+ return triggers.slice(0, 4).join('-');
841
+ }
842
+ if (source.abstractPattern) {
843
+ return source.abstractPattern;
844
+ }
845
+ return firstString(source.title, source.sourceId, source.status, 'diagnostic') ?? 'diagnostic';
846
+ }
847
+
848
+ function deriveKnowledgeNames(source, options = {}) {
711
849
  const sourceRef = source?.sourceId ?? source?.title ?? source?.status ?? 'diagnostic';
712
850
  const sourceKind = source?.kind === 'quality-report' ? 'quality' : 'diagnostic';
851
+ const stablePattern = options.stablePattern !== false;
713
852
  const incidentId = source?.kind === 'quality-report'
714
853
  ? `incident-${sourceRef}`
715
854
  : `incident-${slugify(sourceRef, 'diagnostic')}`;
716
- const patternId = `${sourceKind}-${slugify(sourceRef, sourceKind)}`;
855
+ const patternSeed = stablePattern ? deriveStablePatternKey(source) : sourceRef;
856
+ const patternId = `${sourceKind}-${slugify(patternSeed, sourceKind)}`;
717
857
  const skillName = `openprd-experience-${slugify(patternId)}`;
718
858
  return { incidentId, patternId, skillName };
719
859
  }
@@ -898,9 +1038,11 @@ function renderKnowledgeDraftSkill({ skillName, candidate, source, relativeCandi
898
1038
  ...candidate.touchedFiles.map((file) => `\`${file}\``),
899
1039
  ...source.evidenceSources.map((item) => `\`${item.path}\``),
900
1040
  ]);
1041
+ const useWhen = 'Use when the current task overlaps this draft project experience and you should verify fit before reusing it.';
901
1042
  return `---
902
1043
  name: ${skillName}
903
- description: OpenPrd 在本轮回顾时自动生成的待确认项目经验草案。
1044
+ description: ${useWhen}
1045
+ use_when: ${useWhen}
904
1046
  ---
905
1047
 
906
1048
  # ${skillName}
@@ -929,6 +1071,10 @@ ${renderList(abstraction.typicalOutputs ?? [], '至少产出 knowledge candidate
929
1071
 
930
1072
  ${renderList(inspectItems, '先看本轮 touched files 和已有诊断证据。')}
931
1073
 
1074
+ ## 不要直接套用
1075
+
1076
+ ${renderList(abstraction.antiPatterns ?? [], '如果只是文件名相似,但当前目标、阶段或验证方式不同,不要直接套用。')}
1077
+
932
1078
  ## 可复用模式
933
1079
 
934
1080
  ${renderList(source.rootCauseCandidates.map((candidateItem) => candidateItem.title), '先按本轮诊断线索复走一次,再补最小必要证据。')}
@@ -1292,7 +1438,7 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
1292
1438
  ? (rawCandidateRef.startsWith('candidate-') ? rawCandidateRef : `candidate-${slugify(rawCandidateRef, 'knowledge')}`)
1293
1439
  : `candidate-${slugify(source.sourceId ?? title, 'knowledge')}`;
1294
1440
  const promotedSource = { ...source, sourceId: candidateId };
1295
- const names = deriveKnowledgeNames(promotedSource);
1441
+ const names = deriveKnowledgeNames(promotedSource, { stablePattern: false });
1296
1442
  const candidateDir = knowledgePath(projectRoot, cjoin(KNOWLEDGE_CANDIDATES_DIR, candidateId));
1297
1443
  const candidatePath = path.join(candidateDir, 'candidate.json');
1298
1444
  const diagnosticReportPath = path.join(candidateDir, 'diagnostic-report.json');
@@ -1387,6 +1533,17 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
1387
1533
  status: candidate.status,
1388
1534
  }),
1389
1535
  });
1536
+ if (turnStateSource && await exists(knowledgePath(projectRoot, turnStateSource))) {
1537
+ const turnState = await readJson(knowledgePath(projectRoot, turnStateSource)).catch(() => null);
1538
+ const currentTurnState = readJsonObject(turnState);
1539
+ if (currentTurnState) {
1540
+ await writeJson(knowledgePath(projectRoot, turnStateSource), {
1541
+ ...currentTurnState,
1542
+ knowledgeCandidateId: candidateId,
1543
+ updatedAt: timestamp(),
1544
+ }).catch(() => null);
1545
+ }
1546
+ }
1390
1547
 
1391
1548
  return {
1392
1549
  ok: true,
@@ -1545,6 +1702,54 @@ function buildKnowledgeMatchQuery(options = {}) {
1545
1702
  };
1546
1703
  }
1547
1704
 
1705
+ function trimPromptList(items = [], limit = 3, max = 96) {
1706
+ return uniq(
1707
+ normalizeStringList(items)
1708
+ .map((item) => trimPreview(item, max))
1709
+ .filter(Boolean),
1710
+ ).slice(0, limit);
1711
+ }
1712
+
1713
+ function buildKnowledgeMatchCandidateCard(skill) {
1714
+ return {
1715
+ skillName: skill.skillName,
1716
+ useWhen: firstString(skill.useWhen, skill.description),
1717
+ applicability: trimPromptList(skill.applicability, 2, 96),
1718
+ reviewFirst: trimPromptList(skill.reviewFirst, 3, 96),
1719
+ antiPatterns: trimPromptList(skill.antiPatterns, 2, 96),
1720
+ touchedFiles: trimPromptList(skill.touchedFiles, 4, 80),
1721
+ matchedOn: trimPromptList(skill.matchedOn, 3, 96),
1722
+ matchSummary: trimPreview(skill.matchSummary, 160),
1723
+ };
1724
+ }
1725
+
1726
+ function buildKnowledgeMandatoryCheck(matches, options = {}) {
1727
+ if (!Array.isArray(matches) || matches.length === 0) {
1728
+ return null;
1729
+ }
1730
+ const focusSignals = trimPromptList([
1731
+ options.recommendationTitle ? `当前目标: ${options.recommendationTitle}` : null,
1732
+ options.recommendationReason ? `目标原因: ${options.recommendationReason}` : null,
1733
+ options.activeChange ? `当前变更: ${options.activeChange}` : null,
1734
+ options.nextTaskTitle ? `下一任务: ${options.nextTaskTitle}` : null,
1735
+ ...normalizeStringList(options.relatedFiles).map((file) => `相关文件: ${file}`),
1736
+ ], 6, 120);
1737
+ return {
1738
+ required: true,
1739
+ mode: 'prompt-rerank',
1740
+ title: '先做项目经验检查,再决定是否复用',
1741
+ summary: '这些项目经验只是候选,不代表都要复用;先按当前目标、阶段和验证方式判断。',
1742
+ instructions: [
1743
+ '先判断当前任务真正要解决什么,再看候选经验。',
1744
+ '只有当适用时机、范围或证据入口与当前任务一致时,才复用对应经验。',
1745
+ '如果只是文件名、路径或个别词重合,但任务目标、阶段或验证方式不一致,就不要套用。',
1746
+ '最多选择 0 到 3 条最相关经验;如果都不贴切,可以明确本轮不复用项目经验。',
1747
+ ],
1748
+ focusSignals,
1749
+ candidates: matches.map((skill) => buildKnowledgeMatchCandidateCard(skill)).slice(0, Math.max(1, Number(options.limit ?? 3))),
1750
+ };
1751
+ }
1752
+
1548
1753
  function scoreKnowledgeSkillMatch(skill, query) {
1549
1754
  const fileHints = uniq([
1550
1755
  ...skill.touchedFiles,
@@ -1554,10 +1759,13 @@ function scoreKnowledgeSkillMatch(skill, query) {
1554
1759
  ]);
1555
1760
  const fields = [
1556
1761
  skill.skillName,
1557
- skill.description,
1762
+ skill.useWhen,
1763
+ ...(!isGenericKnowledgeDescription(skill.description) ? [skill.description] : []),
1558
1764
  skill.summary,
1559
1765
  ...skill.categories,
1560
1766
  ...skill.triggerHints,
1767
+ ...skill.applicability,
1768
+ ...skill.reviewFirst,
1561
1769
  ...skill.rootCauseLabels,
1562
1770
  ...fileHints,
1563
1771
  ];
@@ -1602,14 +1810,18 @@ export async function resolveKnowledgeSkillMatches(projectRoot, options = {}) {
1602
1810
  .filter(Boolean)
1603
1811
  .sort((left, right) => right.score - left.score || left.skillName.localeCompare(right.skillName))
1604
1812
  .slice(0, Math.max(1, Number(options.limit ?? 3)));
1813
+ const mandatoryCheck = buildKnowledgeMandatoryCheck(matches, options);
1605
1814
  return {
1606
1815
  ok: true,
1607
1816
  action: 'knowledge-match',
1608
1817
  projectRoot,
1609
1818
  query: trimPreview(query.text, 320),
1610
1819
  matched: matches,
1820
+ mandatoryCheck,
1611
1821
  summary: {
1612
1822
  matched: matches.length,
1823
+ reviewRequired: Boolean(mandatoryCheck?.required),
1824
+ reviewMode: mandatoryCheck?.mode ?? null,
1613
1825
  },
1614
1826
  };
1615
1827
  }
@@ -161,10 +161,10 @@ const STYLE_PROMPT_PACKS = {
161
161
  titlePatterns: [
162
162
  '《{topic}》修行札记',
163
163
  '《{topic}》证道小卷',
164
- '《{topic}》归藏篇',
164
+ '《{topic}》藏录篇',
165
165
  ],
166
166
  outlineArc: ['筑基立卷', '观脉识图', '破雾辨源', '传功成谱', '归元再启'],
167
- imageryBank: ['灵根', '经脉', '法门', '玉简', '破境', '归藏', '心法', '炉火'],
167
+ imageryBank: ['灵根', '经脉', '法门', '玉简', '破境', '藏录', '心法', '炉火'],
168
168
  sentenceRhythm: '长短句交错;每段先给意象,再落回事实、路径或证据。',
169
169
  taboo: [
170
170
  '不要把证据不存在的内容写成神迹或事实。',
@@ -179,7 +179,7 @@ const STYLE_PROMPT_PACKS = {
179
179
  titlePrompt: [
180
180
  '输入: topic、genre、substyle、agent-context、evidence summary。',
181
181
  '输出: 一个像书名的标题和一个短副题。',
182
- '要求: 标题可带“札记/小卷/归藏/心法”等书籍意象,但必须保留 topic 的核心名词。',
182
+ '要求: 标题可带“札记/小卷/藏录/心法”等书籍意象,但必须保留 topic 的核心名词。',
183
183
  ].join('\n'),
184
184
  outlinePrompt: [
185
185
  '输入: 章节目标、证据类别、读者学习路径。',