oh-my-knowledge 0.46.0 → 0.48.0

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.
package/README.md CHANGED
@@ -8,8 +8,8 @@
8
8
 
9
9
  **English** | [简体中文](./README.zh.md)
10
10
 
11
- **Did your prompt actually get better?**
12
- A/B test your prompts and skills with statistical rigor bootstrap CI and length-debias on by default, Krippendorff α the moment you add a gold set.
11
+ **Is this knowledge input (prompt / skill / RAG / agent) any good, and can you ship it with evidence?**
12
+ `doctor` checks whether this knowledge input is coherent enough to measure; `eval` fixes the model and samples, changes only the knowledge input, and tells you whether the new version is genuinely better. Bootstrap CI and length-debias are on by default; Krippendorff α appears the moment you add a gold set.
13
13
 
14
14
  📖 **Full documentation: [oh-my-knowledge.pages.dev](https://oh-my-knowledge.pages.dev)** (searchable, English / 简体中文)
15
15
 
@@ -35,6 +35,20 @@ Walkthrough: [5-minute quickstart guide](docs/quickstart-skill-eval.md) (recomme
35
35
 
36
36
  Deeper: [who omk is for](docs/explanation/who-omk-is-for.md) · [CLI reference](docs/reference/cli.md) · [how it works](docs/explanation/architecture.md) · [eval sample format](docs/reference/eval-sample-format.md) · [executors](docs/reference/executors.md) · [artifact layout](docs/reference/artifact-layout.md)
37
37
 
38
+ ## The first workflow
39
+
40
+ omk is primarily for authors and maintainers of LLM knowledge artifacts who need a release decision, not for passive end-users of a skill. The first workflow is deliberately small:
41
+
42
+ ```text
43
+ change a skill / prompt / agent artifact
44
+ → run omk doctor to catch structure, dependency, and measurability problems
45
+ → run omk eval to compare against a baseline on the same samples
46
+ → read the report / Studio view for the next concrete fix
47
+ → decide ship / don't ship
48
+ ```
49
+
50
+ `observe` is the later production-feedback loop: useful once real usage traces exist, but not required for omk's first value. The trunk is the pre-ship doctor → eval decision.
51
+
38
52
  ## Use inside AI Coding Agents
39
53
 
40
54
  Install the official omk Agent Skill to let your coding agent run omk workflows from natural language:
@@ -73,7 +87,7 @@ You can also describe the goal in natural language, such as "compare v1 vs v2" o
73
87
 
74
88
  ## Why this tool
75
89
 
76
- Teams doing knowledge engineering produce lots of knowledge artifacts (skills today, but also prompts, agents, workflows…). When someone asks "why is v2 better than v1", you need objective data instead of gut feeling. `oh-my-knowledge` solves this with controlled experiments: **same model, same test samples, only the knowledge artifact changes.**
90
+ Teams doing knowledge engineering produce lots of knowledge artifacts (skills today, but also prompts, agents, workflows…). When someone asks "can we ship v2, and why?", you need objective data instead of gut feeling. `oh-my-knowledge` solves this with controlled experiments: **same model, same test samples, only the knowledge artifact changes.**
77
91
 
78
92
  ## Why omk over alternatives
79
93
 
package/README.zh.md CHANGED
@@ -8,8 +8,8 @@
8
8
 
9
9
  [English](./README.md) | **简体中文**
10
10
 
11
- **你改完 prompt,真的变好了吗?**
12
- 用统计严谨性 A/B 测试你的 prompt 和 skill —— Bootstrap 置信区间、长度去偏默认开,配一份人工 gold 就自动算 Krippendorff α。
11
+ **一段知识输入(prompt / skill / RAG / agent)好不好,能不能有证据地发布?**
12
+ `doctor` 先检查这类知识输入是否清楚到值得测;`eval` 固定模型和用例,只改变知识输入,判断新版本是否真的更好。Bootstrap 置信区间、长度去偏默认开,配一份人工 gold 就自动算 Krippendorff α。
13
13
 
14
14
  📖 **完整文档:[oh-my-knowledge.pages.dev/zh](https://oh-my-knowledge.pages.dev/zh/)**(可搜索,可切换英文)
15
15
 
@@ -35,6 +35,20 @@ omk eval --control code-review-v1 --treatment code-review-v2
35
35
 
36
36
  深入:[为谁、解决什么](docs/zh/explanation/who-omk-is-for.md) · [CLI 参考](docs/zh/reference/cli.md) · [工作原理](docs/zh/explanation/architecture.md) · [评测用例格式](docs/zh/reference/eval-sample-format.md) · [执行器](docs/zh/reference/executors.md) · [artifact 布局](docs/zh/reference/artifact-layout.md)
37
37
 
38
+ ## 第一条工作流
39
+
40
+ omk 主要给 LLM 知识载体的作者 / 维护者用,帮他们做发布判断;它不是给被动安装 skill 的普通使用者用的。第一条工作流应该很短:
41
+
42
+ ```text
43
+ 改了一个 skill / prompt / agent artifact
44
+ → 跑 omk doctor,先抓结构、依赖和可测性问题
45
+ → 跑 omk eval,在同一批用例上和 baseline 对比
46
+ → 看报告 / Studio,知道下一步具体该改哪里
47
+ → 决定发布 / 不发布
48
+ ```
49
+
50
+ `observe` 是后续的生产反馈闭环:等真实使用 trace 存在后很有价值,但不是 omk 第一价值的前提。主干先是发布前的 doctor → eval 判断。
51
+
38
52
  ## 在 AI Coding Agent 中使用
39
53
 
40
54
  安装 omk 官方 Agent Skill 后,可以直接用自然语言让 coding agent 跑 omk 工作流:
@@ -73,7 +87,7 @@ omk sample skills/my-skill.md
73
87
 
74
88
  ## 为什么需要这个工具
75
89
 
76
- 做知识工程的团队会产出大量知识载体(当前常见是 skill,也包括 prompt、agent、workflow 等)。当被问到「v2 比 v1 好在哪」时,需要客观数据而非主观判断。`oh-my-knowledge` 通过控制变量实验解决这个问题:**相同模型、相同评测用例,只改变知识载体。**
90
+ 做知识工程的团队会产出大量知识载体(当前常见是 skill,也包括 prompt、agent、workflow 等)。当被问到「v2 能不能发、为什么」时,需要客观数据而非主观判断。`oh-my-knowledge` 通过控制变量实验解决这个问题:**相同模型、相同评测用例,只改变知识载体。**
77
91
 
78
92
  ## 为什么选 omk
79
93
 
@@ -604,6 +604,7 @@ omk sample [skillPath] [flags]
604
604
  - `--no-mock` `boolean`:不生成 mocks,eval 时所有工具调用真实执行。
605
605
  - `--observations-dir` `option`:observe inbox 目录(from-traces 模式用),默认项目 .omk/observe-inbox。
606
606
  - `--reports-dir` `option`:报告目录(fix 模式用),默认 ~/.oh-my-knowledge/reports。
607
+ - `--skill` `option`:仅从指定 skill 的 observe inbox 信号生成草稿(仅 from-traces 模式用)。
607
608
  - `--skill-dir` `option` (默认 `skills`):skill 根目录,默认 skills。batch 模式扫此目录。
608
609
  - `--treatment` `option`:指定 treatment 名(fix 模式用),默认推断自 skill 路径。
609
610
 
@@ -178,7 +178,7 @@ export default class Doctor extends BaseCommand {
178
178
  }
179
179
  }
180
180
  const { runDoctor } = await import('../../doctor/index.js');
181
- const { renderDoctorReportText, renderDoctorReportJson } = await import('../../doctor/renderer.js');
181
+ const { renderDoctorReportText, renderDoctorReportJson, renderDoctorActionPlanText } = await import('../../doctor/renderer.js');
182
182
  const { getRegisteredRules } = await import('../../doctor/rules.js');
183
183
  const { isComposerRule } = await import('../../types/doctor.js');
184
184
  // 默认:静态规则 + 在线检查(LLM health composer + endpoint 自定义维度 external=true)。
@@ -236,6 +236,7 @@ export default class Doctor extends BaseCommand {
236
236
  ? `doctor failed: ${report.totals.fail} 个 skill 未通过 (${report.totals.warn} warn / ${report.totals.pass} pass)`
237
237
  : `doctor failed: ${report.totals.fail} skills did not pass (${report.totals.warn} warn / ${report.totals.pass} pass)`;
238
238
  console.error(summary);
239
+ process.stderr.write(renderDoctorActionPlanText(report, lang));
239
240
  }
240
241
  }
241
242
  else {
@@ -152,6 +152,14 @@ async function emitBatchVerdict(report, reportsDir, values, lang) {
152
152
  for (const result of results) {
153
153
  emitVerdictText(` ${result.verdict.level}: ${result.treatment} — ${result.verdict.headline}`);
154
154
  }
155
+ const next = lang === 'zh'
156
+ ? (failed === 0
157
+ ? '全部通过,可以进入发布流程:请留存这些报告作为发布证据;如果是受管 skill,再运行 `omk promote`。'
158
+ : '先处理未通过的 skill;逐个打开对应报告,看最差层和失败用例后再重跑。')
159
+ : (failed === 0
160
+ ? 'all skills passed and are ready for release: keep these reports as release evidence; for managed skills, run `omk promote`.'
161
+ : 'fix the failing skills first; open each report, inspect the weakest layer and failing samples, then re-run.');
162
+ emitVerdictText(tCli('cli.run.batch_verdict_next_step', lang, { next }));
155
163
  return failed === 0 ? 0 : 1;
156
164
  }
157
165
  async function announceSavedReport({ report, filePath, reportsDir, values, lang, }) {
@@ -3,6 +3,7 @@ import { Flags } from '@oclif/core';
3
3
  import { BaseCommand } from '../../oclif/base-command.js';
4
4
  import { LANG_FLAG, bilingual } from '../../oclif/i18n.js';
5
5
  import { integerStringParser } from '../../oclif/parsers.js';
6
+ import { shellQuoteArg } from '../../../shared/shell-quote.js';
6
7
  function pickSkillCount(value, skillName) {
7
8
  if (!value || value[skillName] == null)
8
9
  return undefined;
@@ -61,6 +62,11 @@ export async function runObserveInbox(_args, flags, lang) {
61
62
  if (flags.skill) {
62
63
  items = items.filter((item) => item.skillName === flags.skill);
63
64
  }
65
+ const recyclableCount = items.filter((item) => item.severity !== 'noise').length;
66
+ const sampleCommandBase = `omk sample --from-traces --observations-dir ${shellQuoteArg(dir)}`;
67
+ const sampleCommand = flags.skill
68
+ ? `${sampleCommandBase} --skill ${shellQuoteArg(flags.skill)}`
69
+ : sampleCommandBase;
64
70
  if (flags['by-skill']) {
65
71
  const reports = flags.skill
66
72
  ? loadLatestObservationInboxReports(dir).map((report) => ({
@@ -87,6 +93,11 @@ export async function runObserveInbox(_args, flags, lang) {
87
93
  for (const row of rows) {
88
94
  console.log(`- ${row.skillName} invocations=${row.invocationCount} sessions=${row.sessionCount} processFindings=${row.observationCount} high=${row.highCount} medium=${row.mediumCount} low=${row.lowCount} noise=${row.noiseCount}${row.latestSeen ? ` latest=${row.latestSeen}` : ''}`);
89
95
  }
96
+ if (recyclableCount > 0) {
97
+ console.log(lang === 'zh'
98
+ ? `提示:确认信号后生成回归用例草稿:${sampleCommand}`
99
+ : `Tip: after confirming signals, draft regression samples: ${sampleCommand}`);
100
+ }
90
101
  return;
91
102
  }
92
103
  if (flags.explore) {
@@ -122,6 +133,11 @@ export async function runObserveInbox(_args, flags, lang) {
122
133
  console.log(lang === 'zh'
123
134
  ? 'Tip: omk observe inbox --explore 10 --include-noise # 显式包含 noise 桶'
124
135
  : 'Tip: omk observe inbox --explore 10 --include-noise # explicitly include the noise bucket');
136
+ if (recyclableCount > 0) {
137
+ console.log(lang === 'zh'
138
+ ? `提示:确认高风险或抽样信号后生成回归用例草稿:${sampleCommand}`
139
+ : `Tip: after confirming high-risk / sampled signals, draft regression samples: ${sampleCommand}`);
140
+ }
125
141
  }
126
142
  function hasLlmEnhancedRuntimeEvidence(evidence) {
127
143
  return evidence.userMessages.length > 0
@@ -3,6 +3,7 @@ import { Args, Flags } from '@oclif/core';
3
3
  import { BaseCommand } from '../../oclif/base-command.js';
4
4
  import { LANG_FLAG, bilingual } from '../../oclif/i18n.js';
5
5
  import { CliExit } from '../../lib/cli-exit.js';
6
+ import { shellQuoteArg } from '../../../shared/shell-quote.js';
6
7
  export default class ObserveIngest extends BaseCommand {
7
8
  static description = bilingual({
8
9
  zh: '把 trace 目录 ingest 成 observation inbox 报告。',
@@ -71,9 +72,11 @@ export default class ObserveIngest extends BaseCommand {
71
72
  report.diagnostics = buildObserveDiagnosticsFromReport(report);
72
73
  const path = saveObservationInboxReport(report, outDir);
73
74
  console.log(JSON.stringify(report, null, 2));
75
+ const inboxCommand = `omk observe inbox --input-dir ${shellQuoteArg(outDir)}`;
76
+ const sampleCommand = `omk sample --from-traces --observations-dir ${shellQuoteArg(outDir)}`;
74
77
  process.stderr.write(lang === 'zh'
75
- ? `observe inbox 已写入: ${path}\n`
76
- : `observe inbox written to: ${path}\n`);
78
+ ? `observe inbox 已写入:${path}\n下一步:${inboxCommand}\n确认高风险或抽样信号后,可生成回归用例草稿:${sampleCommand}\n`
79
+ : `observe inbox written to: ${path}\nNext: ${inboxCommand}\nAfter confirming high-risk / sampled signals, draft regression samples: ${sampleCommand}\n`);
77
80
  });
78
81
  }
79
82
  }
@@ -1,6 +1,7 @@
1
1
  import { BaseCommand } from '../oclif/base-command.js';
2
2
  import { type CliLang } from '../lib/i18n.js';
3
3
  import { type LoadSamplesResult } from '../../inputs/load-samples.js';
4
+ import type { SampleFlags } from '../lib/cmd-flags.js';
4
5
  import type { Report, Sample as SampleType } from '../../types/index.js';
5
6
  /** --append 合并:已有用例原样保留,新用例逐条接在后面;sample_id 撞已有(或本批已用)时
6
7
  * 自动加 `-2`/`-3` 后缀去重。模型每次从 s001 重编号,撞 id 不代表内容重复,所以是改名保留
@@ -24,6 +25,7 @@ export declare function assertFixReportMatchesCurrentInputs(params: {
24
25
  lang?: CliLang;
25
26
  }): void;
26
27
  export declare function writeFixedSamplesToSources(loaded: Pick<LoadSamplesResult, 'sourceFiles' | 'sampleSourceById'>, samples: SampleType[], changedIds: Set<string>): string[];
28
+ export declare function runSampleFromTraces(flags: SampleFlags, lang: CliLang): Promise<void>;
27
29
  export default class Sample extends BaseCommand {
28
30
  static description: string;
29
31
  static examples: {
@@ -48,6 +50,7 @@ export default class Sample extends BaseCommand {
48
50
  treatment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
49
51
  'from-traces': import("@oclif/core/interfaces").BooleanFlag<boolean>;
50
52
  'observations-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
53
+ skill: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
51
54
  };
52
55
  run(): Promise<void>;
53
56
  }
@@ -346,7 +346,7 @@ async function runSampleFix(args, flags, lang) {
346
346
  ? `\n🔧 修复完成: ${result.fixedCount}/${sampleDesignCount} 条已修复 → ${outputTarget}${cost}\n`
347
347
  : `\n🔧 Fix complete: ${result.fixedCount}/${sampleDesignCount} fixed → ${outputTarget}${cost}\n`);
348
348
  }
349
- async function runSampleFromTraces(flags, lang) {
349
+ export async function runSampleFromTraces(flags, lang) {
350
350
  const { queryObservationInbox, DEFAULT_OBSERVATIONS_DIR } = await import('../../observability/inbox.js');
351
351
  const { generateSamplesFromTraces } = await import('../../authoring/generator.js');
352
352
  const obsDir = resolve(flags['observations-dir'] ?? DEFAULT_OBSERVATIONS_DIR);
@@ -358,11 +358,14 @@ async function runSampleFromTraces(flags, lang) {
358
358
  }
359
359
  // Drop noise-tier signals up front: they're exactly what the generator is told to
360
360
  // skip, so filtering here avoids feeding junk to the LLM and keeps the no-op path clean.
361
- const items = queryObservationInbox(obsDir).filter((it) => it.severity !== 'noise');
361
+ let items = queryObservationInbox(obsDir).filter((it) => it.severity !== 'noise');
362
+ if (flags.skill) {
363
+ items = items.filter((it) => it.skillName === flags.skill);
364
+ }
362
365
  if (items.length === 0) {
363
366
  process.stderr.write(lang === 'zh'
364
- ? `✅ ${obsDir} 没有可回流的失败信号(噪声级已跳过)\n`
365
- : `✅ No recyclable failure signals in ${obsDir} (noise-level skipped)\n`);
367
+ ? `✅ ${obsDir}${flags.skill ? ` 中 ${flags.skill}` : ''} 没有可回流的失败信号(噪声级已跳过)\n`
368
+ : `✅ No recyclable failure signals${flags.skill ? ` for ${flags.skill}` : ''} in ${obsDir} (noise-level skipped)\n`);
366
369
  return;
367
370
  }
368
371
  const outPath = join(obsDir, 'sample-drafts.json');
@@ -374,8 +377,8 @@ async function runSampleFromTraces(flags, lang) {
374
377
  }
375
378
  const count = flags.count !== undefined ? Math.max(1, Number(flags.count) || 5) : undefined;
376
379
  process.stderr.write(lang === 'zh'
377
- ? `🔭 发现 ${items.length} 个失败信号,正在生成回归用例草稿...\n`
378
- : `🔭 Found ${items.length} failure signal(s); generating regression-sample drafts...\n`);
380
+ ? `🔭 发现 ${items.length} 个${flags.skill ? ` ${flags.skill} 的` : ''}失败信号,正在生成回归用例草稿...\n`
381
+ : `🔭 Found ${items.length}${flags.skill ? ` ${flags.skill}` : ''} failure signal(s); generating regression-sample drafts...\n`);
379
382
  try {
380
383
  const { samples, costUSD } = await generateSamplesFromTraces({ items, count, model: flags.model, executorName: flags.executor });
381
384
  const cost = costUSD > 0 ? ` $${costUSD.toFixed(4)}` : '';
@@ -401,6 +404,10 @@ async function runSampleFromTraces(flags, lang) {
401
404
  }
402
405
  }
403
406
  async function runSample(args, flags, lang) {
407
+ if (flags.skill && !flags['from-traces']) {
408
+ console.error(lang === 'zh' ? '--skill 仅支持 --from-traces 模式。' : '--skill is only supported with --from-traces.');
409
+ throw new CliExit(2);
410
+ }
404
411
  // --append 目前只在单 skill 生成路径实现;batch / from-traces / fix 不处理它,
405
412
  // 静默忽略会误导(用户以为在追加,实际没有)。提前互斥校验,明确报错。
406
413
  if (flags.append && (flags.batch || flags['from-traces'] || flags.fix)) {
@@ -695,6 +702,12 @@ export default class Sample extends BaseCommand {
695
702
  en: 'Observe inbox dir (from-traces mode), default project .omk/observe-inbox.',
696
703
  }),
697
704
  }),
705
+ skill: Flags.string({
706
+ description: bilingual({
707
+ zh: '仅从指定 skill 的 observe inbox 信号生成草稿(仅 from-traces 模式用)。',
708
+ en: 'Only draft from observe-inbox signals for the specified skill (from-traces mode only).',
709
+ }),
710
+ }),
698
711
  };
699
712
  async run() {
700
713
  const { args, flags } = await this.parse(Sample);
@@ -162,6 +162,7 @@ export interface SampleFlags {
162
162
  treatment?: string;
163
163
  'from-traces': boolean;
164
164
  'observations-dir'?: string;
165
+ skill?: string;
165
166
  }
166
167
  export type GoldInitArgs = Record<string, never>;
167
168
  export interface GoldInitFlags {
@@ -1,3 +1,3 @@
1
1
  import type { CliMessage } from './types.js';
2
- export type RunMessageKey = 'cli.progress.preflight_starting' | 'cli.progress.sample_retry' | 'cli.progress.sample_error' | 'cli.progress.sample_executing' | 'cli.progress.sample_exec_done' | 'cli.progress.output_preview' | 'cli.progress.judging' | 'cli.progress.judged' | 'cli.progress.skipped' | 'cli.progress.sample_done' | 'cli.progress.sample_failed_done' | 'cli.run.invalid_repeat' | 'cli.run.invalid_holdout_ratio' | 'cli.run.invalid_judge_repeat' | 'cli.run.no_debias_length_active' | 'cli.run.invalid_bootstrap_samples' | 'cli.run.bootstrap_samples_too_large' | 'cli.run.dry_run_no_scores' | 'cli.run.skill_section' | 'cli.run.run_section' | 'cli.run.batch_complete' | 'cli.run.batch_verdict_header' | 'cli.run.batch_child_report_missing' | 'cli.run.eval_complete' | 'cli.run.tally' | 'cli.run.report_saved' | 'cli.run.evidence_recorded' | 'cli.run.evidence_recorded_unbound' | 'cli.run.report_only_gate_skipped' | 'cli.run.report_server_running' | 'cli.run.report_server_view' | 'cli.run.report_server_stop' | 'cli.run.no_serve_in_non_tty' | 'cli.run.no_serve_view_hint' | 'cli.run.gold_load_failed' | 'cli.run.gold_load_issue' | 'cli.run.contamination_warning' | 'cli.run.skip_connectivity_warning';
2
+ export type RunMessageKey = 'cli.progress.preflight_starting' | 'cli.progress.sample_retry' | 'cli.progress.sample_error' | 'cli.progress.sample_executing' | 'cli.progress.sample_exec_done' | 'cli.progress.output_preview' | 'cli.progress.judging' | 'cli.progress.judged' | 'cli.progress.skipped' | 'cli.progress.sample_done' | 'cli.progress.sample_failed_done' | 'cli.run.invalid_repeat' | 'cli.run.invalid_holdout_ratio' | 'cli.run.invalid_judge_repeat' | 'cli.run.no_debias_length_active' | 'cli.run.invalid_bootstrap_samples' | 'cli.run.bootstrap_samples_too_large' | 'cli.run.dry_run_no_scores' | 'cli.run.skill_section' | 'cli.run.run_section' | 'cli.run.batch_complete' | 'cli.run.batch_verdict_header' | 'cli.run.batch_verdict_next_step' | 'cli.run.batch_child_report_missing' | 'cli.run.eval_complete' | 'cli.run.tally' | 'cli.run.report_saved' | 'cli.run.evidence_recorded' | 'cli.run.evidence_recorded_unbound' | 'cli.run.report_only_gate_skipped' | 'cli.run.report_server_running' | 'cli.run.report_server_view' | 'cli.run.report_server_stop' | 'cli.run.no_serve_in_non_tty' | 'cli.run.no_serve_view_hint' | 'cli.run.gold_load_failed' | 'cli.run.gold_load_issue' | 'cli.run.contamination_warning' | 'cli.run.skip_connectivity_warning';
3
3
  export declare const runDict: Record<RunMessageKey, CliMessage>;
@@ -87,6 +87,10 @@ export const runDict = {
87
87
  zh: '批量评测结论:{status}({passed}/{total} 通过)',
88
88
  en: 'Batch verdict: {status} ({passed}/{total} passed)',
89
89
  },
90
+ 'cli.run.batch_verdict_next_step': {
91
+ zh: ' 下一步:{next}',
92
+ en: ' Next: {next}',
93
+ },
90
94
  'cli.run.batch_child_report_missing': {
91
95
  zh: '⚠ 子报告缺失:{id},将按不可 ship 处理。\n',
92
96
  en: '⚠ Child report missing: {id}; treating it as not shippable.\n',
@@ -12,5 +12,6 @@
12
12
  */
13
13
  import type { Lang } from '../types/shared.js';
14
14
  import type { DoctorReport } from '../types/index.js';
15
+ export declare function renderDoctorActionPlanText(report: DoctorReport, lang: Lang): string;
15
16
  export declare function renderDoctorReportText(report: DoctorReport, lang: Lang, write?: (s: string) => void): void;
16
17
  export declare function renderDoctorReportJson(report: DoctorReport): string;
@@ -43,6 +43,68 @@ function renderRuleLine(result, ruleLabel, lang, indent = ' ') {
43
43
  }
44
44
  return line;
45
45
  }
46
+ function compactActionText(text) {
47
+ return (text ?? '').replace(/\s+/g, ' ').trim();
48
+ }
49
+ function collectDoctorRepairItems(report, lang) {
50
+ const allItems = [];
51
+ for (const skill of report.skills) {
52
+ for (const result of skill.results) {
53
+ if (result.status !== 'fail' && result.status !== 'warn')
54
+ continue;
55
+ const action = compactActionText(result.hint || result.message);
56
+ allItems.push({
57
+ skillName: skill.skillName,
58
+ status: result.status,
59
+ label: renderRuleLabel(result, lang),
60
+ action,
61
+ isSummary: result.ruleId.endsWith(':_summary'),
62
+ });
63
+ }
64
+ }
65
+ const focusedItems = allItems.filter((item) => !item.isSummary);
66
+ const source = focusedItems.length > 0 ? focusedItems : allItems;
67
+ const sorted = [...source].sort((a, b) => STATUS_RANK[a.status] - STATUS_RANK[b.status]);
68
+ return { items: sorted.slice(0, 6), total: source.length };
69
+ }
70
+ export function renderDoctorActionPlanText(report, lang) {
71
+ const { items, total } = collectDoctorRepairItems(report, lang);
72
+ if (items.length === 0) {
73
+ return lang === 'zh'
74
+ ? '\n下一步:doctor 已通过,可以继续运行 `omk eval`。\n'
75
+ : '\nNext: doctor passed; continue with `omk eval`.\n';
76
+ }
77
+ const hasFail = items.some((item) => item.status === 'fail');
78
+ const lines = [
79
+ '',
80
+ lang === 'zh'
81
+ ? (hasFail ? '修复清单(先处理阻塞项):' : '修复清单(建议处理,不阻断 eval):')
82
+ : (hasFail ? 'Repair checklist (fix blocking items first):' : 'Repair checklist (recommended; does not block eval):'),
83
+ ];
84
+ items.forEach((item, index) => {
85
+ const status = statusLabel(item.status, lang);
86
+ if (lang === 'zh') {
87
+ lines.push(` ${index + 1}. [${item.skillName}] ${status}:${item.label}。${item.action}`);
88
+ }
89
+ else {
90
+ lines.push(` ${index + 1}. [${item.skillName}] ${status}: ${item.label}. ${item.action}`);
91
+ }
92
+ });
93
+ const remaining = total - items.length;
94
+ if (remaining > 0) {
95
+ lines.push(lang === 'zh'
96
+ ? ` ……还有 ${remaining} 项,运行 \`omk doctor\` 查看完整明细。`
97
+ : ` ...and ${remaining} more; run \`omk doctor\` for full detail.`);
98
+ }
99
+ lines.push(hasFail
100
+ ? (lang === 'zh'
101
+ ? '下一步:先修阻塞项,重跑 `omk doctor --gate`;通过后再跑 `omk eval`。'
102
+ : 'Next: fix the blocking items, re-run `omk doctor --gate`, then run `omk eval`.')
103
+ : (lang === 'zh'
104
+ ? '下一步:可以先跑 `omk eval`,但发布前建议把这些 warning 处理掉。'
105
+ : 'Next: you can run `omk eval`, but clear these warnings before shipping.'));
106
+ return `${lines.join('\n')}\n`;
107
+ }
46
108
  function segmentResults(results) {
47
109
  const segs = [];
48
110
  const groupSegMap = new Map();
@@ -115,6 +177,7 @@ export function renderDoctorReportText(report, lang, write = (s) => process.stde
115
177
  ? `\n总览: ${report.totals.pass} 通过 / ${report.totals.warn} 警告 / ${report.totals.fail} 失败\n`
116
178
  : `\nSummary: ${report.totals.pass} pass / ${report.totals.warn} warn / ${report.totals.fail} fail\n`;
117
179
  write(summary);
180
+ write(renderDoctorActionPlanText(report, lang));
118
181
  }
119
182
  export function renderDoctorReportJson(report) {
120
183
  return JSON.stringify(report, null, 2);
@@ -166,8 +166,8 @@ export declare function medianStabilityCV(report: Report): {
166
166
  cv: number;
167
167
  } | null;
168
168
  /**
169
- * Plain-text formatter for the `omk eval` verdict. Stays under 6 lines per the
170
- * spec — one verdict + four rationale bullets + one ship recommendation.
169
+ * Plain-text formatter for the `omk eval` verdict. Stays terse for the
170
+ * spec — one verdict, rationale bullets, one ship recommendation, and one next step.
171
171
  */
172
172
  export declare function formatVerdictText(result: VerdictResult, options?: {
173
173
  verbose?: boolean;
@@ -108,6 +108,7 @@ export function computeVerdict(report, options = {}) {
108
108
  ...(judgeInd.rationale ? { judgeAgreement: judgeInd.rationale } : {}),
109
109
  ...(overfit.rationale ? { overfitting: overfit.rationale } : {}),
110
110
  ...(gap.rationale ? { gapSignal: gap.rationale } : {}),
111
+ shipRecommendation: recommendation('SOLO', []),
111
112
  },
112
113
  ...((overfit.data || gap.data) ? {
113
114
  caveats: {
@@ -583,9 +584,41 @@ function recommendation(level, _perPair, lang = 'en') {
583
584
  return 'ADD A CONTROL — single-variant report. Re-run with --control baseline --treatment <name>.';
584
585
  }
585
586
  }
587
+ function releaseNextStep(level, lang) {
588
+ if (lang === 'zh') {
589
+ switch (level) {
590
+ case 'PROGRESS':
591
+ return '可以进入发布流程:请留存本次报告作为发布证据;如果这是受管 skill,再运行 `omk promote`。';
592
+ case 'CAUTIOUS':
593
+ return '先看触发的告警(分层门控、评委分歧、稳定性或 holdout),修完再重跑。';
594
+ case 'REGRESS':
595
+ return '不要发布;定位最差层和失败用例,修复后重跑。';
596
+ case 'NOISE':
597
+ return '先别发布;增加样本数或提高用例区分度,再重跑。';
598
+ case 'UNDERPOWERED':
599
+ return '把样本数加到至少 20,或先按当前规模 2× 扩充后重跑。';
600
+ case 'SOLO':
601
+ return '补一个 baseline 对照,再跑 `omk eval --control baseline --treatment <名字>`。';
602
+ }
603
+ }
604
+ switch (level) {
605
+ case 'PROGRESS':
606
+ return 'ready for release: keep this report as release evidence; for a managed skill, run `omk promote`.';
607
+ case 'CAUTIOUS':
608
+ return 'inspect the warnings (layer gates, judge dissent, stability, or holdout), fix them, then re-run.';
609
+ case 'REGRESS':
610
+ return 'do not ship; inspect the weakest layer and failing samples, fix them, then re-run.';
611
+ case 'NOISE':
612
+ return 'do not ship yet; add samples or sharpen the test set, then re-run.';
613
+ case 'UNDERPOWERED':
614
+ return 'increase the sample set to at least 20, or roughly 2x the current size, then re-run.';
615
+ case 'SOLO':
616
+ return 'add a baseline control and re-run `omk eval --control baseline --treatment <name>`.';
617
+ }
618
+ }
586
619
  /**
587
- * Plain-text formatter for the `omk eval` verdict. Stays under 6 lines per the
588
- * spec — one verdict + four rationale bullets + one ship recommendation.
620
+ * Plain-text formatter for the `omk eval` verdict. Stays terse for the
621
+ * spec — one verdict, rationale bullets, one ship recommendation, and one next step.
589
622
  */
590
623
  export function formatVerdictText(result, options = {}) {
591
624
  // lang 默认 'en':保留既有英文输出逐字节不变(verdict.test 与历史 CLI 行为)。zh 只本地化
@@ -609,6 +642,7 @@ export function formatVerdictText(result, options = {}) {
609
642
  lines.push(zh ? ` 知识缺口:${result.rationale.gapSignal}` : ` Gap signal: ${result.rationale.gapSignal}`);
610
643
  if (result.rationale.shipRecommendation) {
611
644
  lines.push(` ${zh ? recommendation(result.level, [], 'zh') : result.rationale.shipRecommendation}`);
645
+ lines.push(zh ? ` 下一步:${releaseNextStep(result.level, 'zh')}` : ` Next: ${releaseNextStep(result.level, 'en')}`);
612
646
  }
613
647
  if (options.verbose && result.perPair && result.perPair.length > 1) {
614
648
  lines.push('');
@@ -12,8 +12,8 @@ const MESSAGES = {
12
12
  en: '⚠ --repeat=1: single-run cannot measure stability (CV will be marked "not measured"). Use --repeat 3+ to detect within-variant variance.',
13
13
  },
14
14
  doctor_gate_blocked: {
15
- zh: 'skill 健康检查未通过,评测已中止。doctor 是评测必经环节,无 skip 选项——请修复上述问题后重跑。',
16
- en: 'skill health check failed; evaluation aborted. doctor is mandatory and not skippable fix the issues above and re-run.',
15
+ zh: '发布前 doctor 门禁未通过,评测已中止。\n下一步:先修复上面的阻塞项,再重跑 `omk eval`。\n原因:这段输入还不值得测,继续比较分数会是 garbage-in。\n如果依赖确实由 mock / stub 提供、doctor 误报,可用 `--skip-doctor` 绕过,但这次结果由你承担不可比风险。',
16
+ en: 'pre-ship doctor gate failed; evaluation aborted.\nNext: fix the blocking findings above, then re-run `omk eval`.\nWhy: this input is not measurable enough yet, and comparing scores now would be garbage-in.\nIf deps are truly supplied by mocks/stubs and doctor is a false positive, use `--skip-doctor`, but you own the comparability risk.',
17
17
  },
18
18
  };
19
19
  const DEFAULT_LANG = 'zh';
@@ -62,7 +62,7 @@ export async function runEvaluation({ samplesPath, skillDir, variantSpecs = [],
62
62
  });
63
63
  if (doctorReport.outcome === 'failed') {
64
64
  renderDoctorReportText(doctorReport, lang);
65
- throw new Error(`doctor failed: ${tEvalWorkflowMessage('doctor_gate_blocked', lang)}`);
65
+ throw new Error(`doctor failed:\n${tEvalWorkflowMessage('doctor_gate_blocked', lang)}`);
66
66
  }
67
67
  }
68
68
  }
@@ -5,6 +5,7 @@ import { type ResolvedObservationReviewSession } from './resolved-review.js';
5
5
  import { type ObservationSkillChain } from './skill-chain.js';
6
6
  import { type ResolvedSkillStandards, type SkillDerivedStandards } from './soft-standards/index.js';
7
7
  export interface ObservationInboxViewModel {
8
+ observationsDir?: string;
8
9
  activeSkill?: string;
9
10
  allItems: ObservationInboxItem[];
10
11
  items: ObservationInboxItem[];
@@ -76,6 +76,7 @@ export function buildObservationInboxViewModel(observationsDir, options = {}) {
76
76
  }
77
77
  }
78
78
  return {
79
+ observationsDir,
79
80
  activeSkill,
80
81
  allItems,
81
82
  items,
@@ -4,9 +4,10 @@ import { findNegativeFeedbackMatches, findPositiveFeedbackMatches, findUserCorre
4
4
  import { durationMsBetween } from '../shared/time.js';
5
5
  import { OBSERVATION_INBOX_STYLES } from './observation-inbox/styles.js';
6
6
  import { experienceSkillAnchor, formatTimeRange as formatTimeRangeImpl, formatTimestamp, renderArtifactVersion, renderField, renderJson, renderFeedbackAttributionLabel, skillAnchor, truncateText, } from './observation-inbox/helpers.js';
7
+ import { shellQuoteArg } from '../shared/shell-quote.js';
7
8
  export { renderFeedbackAttributionLabel } from './observation-inbox/helpers.js';
8
9
  export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
9
- const { activeSkill, allItems, items, reports, experienceReports, skillInvocationCounts, skillSessionCounts, skillInvocationLastSeen, skillToolCallCounts, skillChains, skillDerivedStandards, totalSkillInvocations, severitySkillCounts, skillCount, reportCount, latestSeenLabel, reviewState, resolvedReviewSessions, } = model;
10
+ const { observationsDir, activeSkill, allItems, items, reports, experienceReports, skillInvocationCounts, skillSessionCounts, skillInvocationLastSeen, skillToolCallCounts, skillChains, skillDerivedStandards, totalSkillInvocations, severitySkillCounts, skillCount, reportCount, latestSeenLabel, reviewState, resolvedReviewSessions, } = model;
10
11
  const experience = experienceReports.find((report) => report.skills.length > 0 || report.sessions.length > 0 || report.invocations.length > 0);
11
12
  const pageTitle = activeSkill ? `Observe Inbox · ${activeSkill}` : 'Observe Inbox';
12
13
  const experienceToolCountsBySkill = new Map();
@@ -5167,6 +5168,22 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
5167
5168
  const empty = items.length === 0 && !experience
5168
5169
  ? `<p style="color:var(--text-muted);margin-top:24px">${activeSkill ? `当前 skill 没有可展示的调用或过程发现:${e(activeSkill)}` : (lang === 'zh' ? '暂无 inbox item。运行 omk observe ingest <sessions-dir> 生成。' : 'No inbox items. Run omk observe ingest <sessions-dir> first.')}</p>`
5169
5170
  : '';
5171
+ const recyclableObservationCount = allItems.filter((item) => item.severity !== 'noise').reduce((sum, item) => sum + item.occurrences, 0);
5172
+ const sampleFromTracesBaseCommand = `omk sample --from-traces --observations-dir ${shellQuoteArg(observationsDir || '.omk/observe-inbox')}`;
5173
+ const sampleFromTracesCommand = activeSkill
5174
+ ? `${sampleFromTracesBaseCommand} --skill ${shellQuoteArg(activeSkill)}`
5175
+ : sampleFromTracesBaseCommand;
5176
+ const observeLoopCta = recyclableObservationCount > 0
5177
+ ? `<section data-observe-feedback-loop style="margin-top:14px;border:1px solid var(--border);border-radius:8px;background:var(--bg-muted);padding:12px 14px;font-size:13px;line-height:1.55">
5178
+ <div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap">
5179
+ <div>
5180
+ <div style="font-weight:700;color:var(--text-primary)">${lang === 'zh' ? '把已确认的 observe gap 回流成 eval sample' : 'Recycle confirmed observe gaps into eval samples'}</div>
5181
+ <div style="color:var(--text-muted);margin-top:3px">${lang === 'zh' ? `当前有 ${recyclableObservationCount} 个非噪声信号。先 review 高风险或抽样信号,确认可复现后生成草稿。` : `${recyclableObservationCount} non-noise signal(s) are available. Review high-risk / sampled signals first, then draft reproducible cases.`}</div>
5182
+ </div>
5183
+ <code style="display:block;max-width:100%;overflow:auto;white-space:nowrap;padding:6px 8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-surface);color:var(--text-primary)">${e(sampleFromTracesCommand)}</code>
5184
+ </div>
5185
+ </section>`
5186
+ : '';
5170
5187
  const v0SummarySection = `
5171
5188
  <section class="observe-summary-grid" style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:14px">
5172
5189
  <div style="padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface)">
@@ -5222,6 +5239,7 @@ export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
5222
5239
  <div class="metric-guide-body">${metricGuideHtml}</div>
5223
5240
  </aside>
5224
5241
  ${empty}
5242
+ ${observeLoopCta}
5225
5243
  ${experienceSection}
5226
5244
  <div data-v0-observation-view style="display:none">
5227
5245
  <div class="report-version-divider" aria-label="1.0 和 2.0 报告分隔">
@@ -0,0 +1,2 @@
1
+ /** Format one POSIX shell argument for user-facing copy/paste commands. */
2
+ export declare function shellQuoteArg(value: string): string;
@@ -0,0 +1,7 @@
1
+ const SAFE_SHELL_ARG_RE = /^[A-Za-z0-9_./:@%+=,-]+$/;
2
+ /** Format one POSIX shell argument for user-facing copy/paste commands. */
3
+ export function shellQuoteArg(value) {
4
+ if (value && SAFE_SHELL_ARG_RE.test(value))
5
+ return value;
6
+ return `'${value.replace(/'/g, "'\\''")}'`;
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-knowledge",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
4
4
  "packageManager": "yarn@4.16.0",
5
5
  "description": "Evaluation framework for LLM knowledge inputs — prompts, RAG corpora, skills, agent workflows. Fix the model, vary the artifact. Built-in statistical rigor: bootstrap CI, Krippendorff α, length-debias, saturation curves.",
6
6
  "type": "module",
@@ -93,11 +93,11 @@
93
93
  "license": "MIT",
94
94
  "dependencies": {
95
95
  "@anthropic-ai/claude-agent-sdk": "^0.3.143",
96
- "@anthropic-ai/sdk": "^0.105.0",
96
+ "@anthropic-ai/sdk": "^0.107.0",
97
97
  "@inquirer/prompts": "^8.4.3",
98
98
  "@modelcontextprotocol/sdk": "^1.29.0",
99
99
  "@oclif/core": "^4",
100
- "@openai/codex-sdk": "0.141.0",
100
+ "@openai/codex-sdk": "0.142.4",
101
101
  "ajv": "^8.18.0",
102
102
  "chart.js": "^4.5.1",
103
103
  "js-yaml": "^4.1.1",
@@ -112,11 +112,11 @@
112
112
  "@types/node": "^25.5.0",
113
113
  "eslint": "^10.1.0",
114
114
  "husky": "^9.1.7",
115
- "lint-staged": "17.0.7",
115
+ "lint-staged": "17.0.8",
116
116
  "npm-run-all2": "^9.0.1",
117
117
  "typescript": "^6.0.2",
118
118
  "typescript-eslint": "^8.58.0",
119
119
  "vitepress": "^1.6.4",
120
- "vitest": "4.1.8"
120
+ "vitest": "4.1.9"
121
121
  }
122
122
  }