oh-my-knowledge 0.47.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.
@@ -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
 
@@ -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 {
@@ -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.47.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",