oh-my-knowledge 0.36.0 → 0.38.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 +6 -4
- package/README.zh.md +6 -4
- package/dist/assets/agent-skills/omk/SKILL.md +5 -3
- package/dist/assets/agent-skills/omk/references/commands.md +107 -15
- package/dist/authoring/evolver.d.ts +4 -1
- package/dist/authoring/evolver.js +13 -8
- package/dist/cli/commands/doctor.d.ts +1 -0
- package/dist/cli/commands/doctor.js +45 -10
- package/dist/cli/commands/eval/gold/compare.js +7 -4
- package/dist/cli/commands/eval/index.d.ts +1 -0
- package/dist/cli/commands/eval/index.js +14 -3
- package/dist/cli/commands/evolve.d.ts +12 -0
- package/dist/cli/commands/evolve.js +150 -5
- package/dist/cli/commands/init.js +18 -6
- package/dist/cli/commands/list.d.ts +3 -26
- package/dist/cli/commands/list.js +14 -144
- package/dist/cli/commands/observe/inbox.d.ts +1 -0
- package/dist/cli/commands/observe/inbox.js +14 -4
- package/dist/cli/commands/observe/index.d.ts +11 -0
- package/dist/cli/commands/observe/index.js +35 -12
- package/dist/cli/commands/observe/ingest.d.ts +1 -0
- package/dist/cli/commands/observe/ingest.js +13 -4
- package/dist/cli/commands/observe/show.d.ts +1 -0
- package/dist/cli/commands/observe/show.js +12 -2
- package/dist/cli/commands/promote.d.ts +24 -0
- package/dist/cli/commands/promote.js +157 -0
- package/dist/cli/commands/rollback.d.ts +20 -0
- package/dist/cli/commands/rollback.js +95 -0
- package/dist/cli/commands/sample.d.ts +1 -0
- package/dist/cli/commands/sample.js +25 -13
- package/dist/cli/commands/studio.d.ts +2 -0
- package/dist/cli/commands/studio.js +62 -12
- package/dist/cli/lib/cell-format.d.ts +17 -0
- package/dist/cli/lib/cell-format.js +19 -0
- package/dist/cli/lib/cmd-flags.d.ts +9 -0
- package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/common.js +9 -1
- package/dist/cli/lib/i18n-dict/evolve.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/evolve.js +8 -0
- package/dist/cli/lib/i18n-dict/help.js +27 -45
- package/dist/cli/lib/i18n-dict/init.js +2 -2
- package/dist/cli/lib/i18n-dict/list.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/list.js +4 -0
- package/dist/cli/lib/i18n-dict/promote.d.ts +3 -0
- package/dist/cli/lib/i18n-dict/promote.js +54 -0
- package/dist/cli/lib/i18n-dict/rollback.d.ts +3 -0
- package/dist/cli/lib/i18n-dict/rollback.js +22 -0
- package/dist/cli/lib/i18n-dict.d.ts +3 -1
- package/dist/cli/lib/i18n-dict.js +4 -0
- package/dist/cli/lib/parse-run-config.d.ts +0 -1
- package/dist/cli/lib/parse-run-config.js +6 -4
- package/dist/cli/lib/progress.d.ts +3 -0
- package/dist/cli/lib/progress.js +22 -0
- package/dist/cli/lib/record-evolve-outcome.d.ts +31 -0
- package/dist/cli/lib/record-evolve-outcome.js +110 -0
- package/dist/cli/lib/resolve-skill-input.d.ts +6 -0
- package/dist/cli/lib/resolve-skill-input.js +4 -2
- package/dist/cli/lib/source-probe.d.ts +15 -0
- package/dist/cli/lib/source-probe.js +129 -0
- package/dist/diagnosis/types.js +1 -1
- package/dist/doctor/endpoint-rule.d.ts +60 -0
- package/dist/doctor/endpoint-rule.js +381 -0
- package/dist/doctor/health/load-custom-dimensions.d.ts +6 -0
- package/dist/doctor/health/load-custom-dimensions.js +43 -3
- package/dist/doctor/index.js +12 -2
- package/dist/eval-core/artifact-index.d.ts +106 -0
- package/dist/eval-core/artifact-index.js +277 -0
- package/dist/eval-core/default-dirs.d.ts +34 -0
- package/dist/eval-core/default-dirs.js +36 -0
- package/dist/eval-core/evaluation-execution.js +3 -3
- package/dist/eval-core/evaluation-reporting.d.ts +7 -0
- package/dist/eval-core/evaluation-reporting.js +20 -5
- package/dist/eval-core/execution-strategy.js +3 -3
- package/dist/eval-core/measurement-dirs.d.ts +18 -0
- package/dist/eval-core/measurement-dirs.js +76 -0
- package/dist/eval-workflows/batch-evaluation-workflow.js +2 -1
- package/dist/eval-workflows/evaluation-pipeline/run-state.js +2 -1
- package/dist/eval-workflows/run-evaluation.js +5 -2
- package/dist/executors/codex-cli.js +7 -3
- package/dist/executors/script.js +43 -4
- package/dist/inputs/materialize-copy.d.ts +1 -1
- package/dist/inputs/materialize-copy.js +3 -3
- package/dist/inputs/skill-loader.d.ts +1 -1
- package/dist/inputs/skill-loader.js +1 -1
- package/dist/managed/index.d.ts +3 -0
- package/dist/managed/index.js +3 -0
- package/dist/managed/list-query.d.ts +7 -0
- package/dist/managed/list-query.js +11 -0
- package/dist/managed/list-view.d.ts +3 -0
- package/dist/managed/list-view.js +8 -4
- package/dist/managed/promote-gate.d.ts +40 -0
- package/dist/managed/promote-gate.js +37 -0
- package/dist/managed/source-probe.d.ts +15 -0
- package/dist/managed/source-probe.js +129 -0
- package/dist/managed/store.d.ts +35 -2
- package/dist/managed/store.js +117 -5
- package/dist/observability/inbox.js +6 -3
- package/dist/observability/skill-health-analyzer.d.ts +3 -0
- package/dist/observability/skill-health-analyzer.js +2 -0
- package/dist/renderer/doctor-detail-renderer.d.ts +9 -0
- package/dist/renderer/doctor-detail-renderer.js +114 -0
- package/dist/renderer/html-renderer.d.ts +3 -2
- package/dist/renderer/html-renderer.js +103 -101
- package/dist/renderer/icons.d.ts +29 -0
- package/dist/renderer/icons.js +66 -0
- package/dist/renderer/layout.js +81 -68
- package/dist/renderer/managed-history-renderer.d.ts +5 -0
- package/dist/renderer/managed-history-renderer.js +255 -0
- package/dist/renderer/observation-inbox/styles.d.ts +1 -1
- package/dist/renderer/observation-inbox/styles.js +53 -53
- package/dist/renderer/observation-inbox-renderer.js +19 -19
- package/dist/renderer/report-shell.d.ts +77 -0
- package/dist/renderer/report-shell.js +223 -0
- package/dist/renderer/skill-detail-renderer.js +164 -360
- package/dist/renderer/skill-health-renderer.js +50 -73
- package/dist/renderer/skill-list-renderer.js +359 -306
- package/dist/renderer/summary.js +272 -162
- package/dist/renderer/test-view.d.ts +4 -3
- package/dist/renderer/test-view.js +386 -135
- package/dist/server/indexed-report-store.d.ts +8 -0
- package/dist/server/indexed-report-store.js +130 -0
- package/dist/server/job-store.d.ts +0 -1
- package/dist/server/job-store.js +0 -2
- package/dist/server/report-server.d.ts +17 -3
- package/dist/server/report-server.js +357 -97
- package/dist/server/report-store.d.ts +14 -8
- package/dist/server/report-store.js +46 -0
- package/dist/server/skill-index.d.ts +7 -1
- package/dist/server/skill-index.js +48 -12
- package/dist/types/doctor.d.ts +22 -0
- package/dist/types/executor.d.ts +4 -2
- package/dist/types/managed.d.ts +17 -3
- package/dist/types/report.d.ts +15 -0
- package/dist/types/skill-index.d.ts +3 -0
- package/package.json +1 -1
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { BaseCommand } from '../../oclif/base-command.js';
|
|
2
|
+
import type { SkillHealthReport } from '../../../observability/skill-health-analyzer.js';
|
|
3
|
+
/**
|
|
4
|
+
* observe-health 报告落盘:id / 文件名加 4 位随机段,根治「同秒两次 omk observe 直接覆盖、数据丢失」的 bug。
|
|
5
|
+
* 保留 `-observe-health.json` 后缀 —— resolveObserveHealthDir 靠它判项目优先、listAnalyses 靠它取 id stem、
|
|
6
|
+
* loadAnalysis 靠 `${id}.json` 读真身。落盘后 best-effort 追加全局轻卡片,让 studio 跨项目聚合。
|
|
7
|
+
*/
|
|
8
|
+
export declare function persistObserveHealthReport(report: SkillHealthReport, outDir: string): {
|
|
9
|
+
id: string;
|
|
10
|
+
jsonPath: string;
|
|
11
|
+
};
|
|
2
12
|
export default class Observe extends BaseCommand {
|
|
3
13
|
static description: string;
|
|
4
14
|
static examples: {
|
|
@@ -16,6 +26,7 @@ export default class Observe extends BaseCommand {
|
|
|
16
26
|
to: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
27
|
skills: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
28
|
'output-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
29
|
+
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
30
|
};
|
|
20
31
|
run(): Promise<void>;
|
|
21
32
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
1
2
|
import { resolve, join } from 'node:path';
|
|
2
3
|
import { Args, Flags } from '@oclif/core';
|
|
3
4
|
import { LANG_FLAG, bilingual } from '../../oclif/i18n.js';
|
|
@@ -5,12 +6,29 @@ import { BaseCommand } from '../../oclif/base-command.js';
|
|
|
5
6
|
import { CliExit } from '../../lib/cli-exit.js';
|
|
6
7
|
import { tCli } from '../../lib/i18n.js';
|
|
7
8
|
import { parseLastWindow } from '../../lib/shared.js';
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
import { projectObserveHealthDir, globalObserveHealthDir } from '../../../eval-core/measurement-dirs.js';
|
|
10
|
+
import { indexObserveWrite } from '../../../eval-core/artifact-index.js';
|
|
11
|
+
/**
|
|
12
|
+
* observe-health 报告落盘:id / 文件名加 4 位随机段,根治「同秒两次 omk observe 直接覆盖、数据丢失」的 bug。
|
|
13
|
+
* 保留 `-observe-health.json` 后缀 —— resolveObserveHealthDir 靠它判项目优先、listAnalyses 靠它取 id stem、
|
|
14
|
+
* loadAnalysis 靠 `${id}.json` 读真身。落盘后 best-effort 追加全局轻卡片,让 studio 跨项目聚合。
|
|
15
|
+
*/
|
|
16
|
+
export function persistObserveHealthReport(report, outDir) {
|
|
17
|
+
mkdirSync(outDir, { recursive: true });
|
|
18
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
19
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
20
|
+
const id = `${timestamp}-${rand}-observe-health`;
|
|
21
|
+
const jsonPath = join(outDir, `${id}.json`);
|
|
22
|
+
writeFileSync(jsonPath, JSON.stringify(report, null, 2));
|
|
23
|
+
indexObserveWrite(report, jsonPath, outDir, id);
|
|
24
|
+
return { id, jsonPath };
|
|
25
|
+
}
|
|
26
|
+
// `omk observe <sessions-dir>` 是默认命令 —— 分析 sessions 目录的 skill 调用健康度,产出 observe-health 报告(JSON),
|
|
27
|
+
// 由 Studio 健康报告页按需渲染。observe 这条线的另一条产物是观测收件箱(observe-inbox),走子命令 ingest / inbox / show。
|
|
10
28
|
export default class Observe extends BaseCommand {
|
|
11
29
|
static description = bilingual({
|
|
12
30
|
zh: '分析 sessions 目录的 skill 调用健康度(默认行为)。子命令:ingest / inbox / show。',
|
|
13
|
-
en: 'Analyze skill invocation health from sessions dir (default). Subcommands: ingest / inbox / show.',
|
|
31
|
+
en: 'Analyze skill invocation health from a sessions dir (default). Subcommands: ingest / inbox / show.',
|
|
14
32
|
});
|
|
15
33
|
static examples = [
|
|
16
34
|
{
|
|
@@ -52,8 +70,14 @@ export default class Observe extends BaseCommand {
|
|
|
52
70
|
}),
|
|
53
71
|
'output-dir': Flags.string({
|
|
54
72
|
description: bilingual({
|
|
55
|
-
zh: '
|
|
56
|
-
en: '
|
|
73
|
+
zh: '健康报告输出目录,默认项目级 .omk/observe-health(--global 写全局)',
|
|
74
|
+
en: 'Health report output dir, default project-level .omk/observe-health (--global for global)',
|
|
75
|
+
}),
|
|
76
|
+
}),
|
|
77
|
+
global: Flags.boolean({
|
|
78
|
+
description: bilingual({
|
|
79
|
+
zh: '写全局 ~/.oh-my-knowledge/observe-health,而非项目 .omk/observe-health',
|
|
80
|
+
en: 'Write to global ~/.oh-my-knowledge/observe-health instead of project .omk/observe-health',
|
|
57
81
|
}),
|
|
58
82
|
}),
|
|
59
83
|
};
|
|
@@ -67,7 +91,7 @@ export default class Observe extends BaseCommand {
|
|
|
67
91
|
throw new CliExit(1);
|
|
68
92
|
}
|
|
69
93
|
const tracePath = resolve(dir);
|
|
70
|
-
const { existsSync
|
|
94
|
+
const { existsSync } = await import('node:fs');
|
|
71
95
|
if (!existsSync(tracePath)) {
|
|
72
96
|
console.error(`Trace path does not exist: ${tracePath}`);
|
|
73
97
|
throw new CliExit(1);
|
|
@@ -92,12 +116,11 @@ export default class Observe extends BaseCommand {
|
|
|
92
116
|
to,
|
|
93
117
|
skills,
|
|
94
118
|
});
|
|
95
|
-
// JSON 是主产物;HTML 由 report server
|
|
96
|
-
const outDir =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const jsonPath =
|
|
100
|
-
writeFileSync(jsonPath, JSON.stringify(report, null, 2));
|
|
119
|
+
// JSON 是主产物;HTML 由 report server 的健康报告详情页按需渲染。
|
|
120
|
+
const outDir = flags['output-dir']
|
|
121
|
+
? resolve(flags['output-dir'])
|
|
122
|
+
: (flags.global ? globalObserveHealthDir() : projectObserveHealthDir());
|
|
123
|
+
const { jsonPath } = persistObserveHealthReport(report, outDir);
|
|
101
124
|
const { sessionCount, segmentCount, toolCallCount, toolFailureRate } = report.meta;
|
|
102
125
|
console.log('');
|
|
103
126
|
console.log(`sessions: ${sessionCount} · segments: ${segmentCount} · tool calls: ${toolCallCount} · fail rate: ${(toolFailureRate * 100).toFixed(1)}%`);
|
|
@@ -7,6 +7,7 @@ export default class ObserveIngest extends BaseCommand {
|
|
|
7
7
|
static flags: {
|
|
8
8
|
lang: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
9
|
'output-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
};
|
|
11
12
|
run(): Promise<void>;
|
|
12
13
|
}
|
|
@@ -21,10 +21,17 @@ export default class ObserveIngest extends BaseCommand {
|
|
|
21
21
|
lang: LANG_FLAG,
|
|
22
22
|
'output-dir': Flags.string({
|
|
23
23
|
description: bilingual({
|
|
24
|
-
zh: '输出目录,默认 .omk/
|
|
25
|
-
en: 'Output dir, default .omk/
|
|
24
|
+
zh: '输出目录,默认 .omk/observe-inbox(项目级,相对于 cwd;--global 写全局)。',
|
|
25
|
+
en: 'Output dir, default .omk/observe-inbox (project-local, relative to cwd; --global writes global).',
|
|
26
26
|
}),
|
|
27
27
|
}),
|
|
28
|
+
global: Flags.boolean({
|
|
29
|
+
description: bilingual({
|
|
30
|
+
zh: '写入全局 ~/.oh-my-knowledge/observe-inbox,而非项目 .omk/observe-inbox。',
|
|
31
|
+
en: 'Write to global ~/.oh-my-knowledge/observe-inbox instead of project .omk/observe-inbox.',
|
|
32
|
+
}),
|
|
33
|
+
default: false,
|
|
34
|
+
}),
|
|
28
35
|
};
|
|
29
36
|
async run() {
|
|
30
37
|
const { args, flags } = await this.parse(ObserveIngest);
|
|
@@ -54,8 +61,10 @@ export default class ObserveIngest extends BaseCommand {
|
|
|
54
61
|
: 'Error: --output-dir must not be an empty string.');
|
|
55
62
|
throw new CliExit(2);
|
|
56
63
|
}
|
|
57
|
-
const { buildObservationInboxReport, saveObservationInboxReport,
|
|
58
|
-
|
|
64
|
+
const { buildObservationInboxReport, saveObservationInboxReport, DEFAULT_PROJECT_OBSERVATIONS_DIR, DEFAULT_GLOBAL_OBSERVATIONS_DIR } = await import('../../../observability/inbox.js');
|
|
65
|
+
// 显式 --output-dir 最高;否则 --global 写全局、默认写项目(读侧 loadObservationInboxReports 会从项目兜底到全局)。
|
|
66
|
+
const defaultDir = flags.global ? DEFAULT_GLOBAL_OBSERVATIONS_DIR : DEFAULT_PROJECT_OBSERVATIONS_DIR;
|
|
67
|
+
const outDir = resolve(outDirRaw ?? defaultDir);
|
|
59
68
|
const { loadObservationReviewState } = await import('../../../observability/review-state.js');
|
|
60
69
|
const { buildObserveDiagnosticsFromReport } = await import('../../../diagnosis/observe-producer.js');
|
|
61
70
|
const report = buildObservationInboxReport(tracePath, { reviewState: loadObservationReviewState(outDir) });
|
|
@@ -7,6 +7,7 @@ export default class ObserveShow extends BaseCommand {
|
|
|
7
7
|
static flags: {
|
|
8
8
|
lang: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
9
|
'input-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
};
|
|
11
12
|
run(): Promise<void>;
|
|
12
13
|
}
|
|
@@ -25,6 +25,13 @@ export default class ObserveShow extends BaseCommand {
|
|
|
25
25
|
en: 'Inbox data dir',
|
|
26
26
|
}),
|
|
27
27
|
}),
|
|
28
|
+
global: Flags.boolean({
|
|
29
|
+
description: bilingual({
|
|
30
|
+
zh: '直接读取全局 ~/.oh-my-knowledge/observe-inbox(跳过项目级与兜底)。',
|
|
31
|
+
en: 'Read directly from global ~/.oh-my-knowledge/observe-inbox (skip project-local and fallback).',
|
|
32
|
+
}),
|
|
33
|
+
default: false,
|
|
34
|
+
}),
|
|
28
35
|
};
|
|
29
36
|
async run() {
|
|
30
37
|
const { args, flags } = await this.parse(ObserveShow);
|
|
@@ -35,8 +42,11 @@ export default class ObserveShow extends BaseCommand {
|
|
|
35
42
|
console.error(lang === 'zh' ? '用法:omk observe show <inbox_id> [--input-dir <path>]' : 'Usage: omk observe show <inbox_id> [--input-dir <path>]');
|
|
36
43
|
throw new CliExit(1);
|
|
37
44
|
}
|
|
38
|
-
const { findObservationInboxItem, formatObservationShow, DEFAULT_OBSERVATIONS_DIR } = await import('../../../observability/inbox.js');
|
|
39
|
-
|
|
45
|
+
const { findObservationInboxItem, formatObservationShow, DEFAULT_OBSERVATIONS_DIR, DEFAULT_GLOBAL_OBSERVATIONS_DIR } = await import('../../../observability/inbox.js');
|
|
46
|
+
// 显式 --input-dir 最高;否则 --global 直读全局、默认读项目(空则 findObservationInboxItem 兜底全局)。与 observe inbox 同口径。
|
|
47
|
+
const dir = flags['input-dir']
|
|
48
|
+
? resolve(flags['input-dir'])
|
|
49
|
+
: (flags.global ? DEFAULT_GLOBAL_OBSERVATIONS_DIR : DEFAULT_OBSERVATIONS_DIR);
|
|
40
50
|
const item = findObservationInboxItem(id, dir);
|
|
41
51
|
if (!item) {
|
|
42
52
|
console.error(lang === 'zh' ? `未找到 observation:${id}` : `Observation not found: ${id}`);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { BaseCommand } from '../oclif/base-command.js';
|
|
2
|
+
export default class Promote extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
description: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}[];
|
|
8
|
+
static args: {
|
|
9
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
10
|
+
};
|
|
11
|
+
static flags: {
|
|
12
|
+
lang: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
kind: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'accept-cautious': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
reason: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
actor: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
|
+
};
|
|
21
|
+
run(): Promise<void>;
|
|
22
|
+
private emitBlocked;
|
|
23
|
+
private emitPromoted;
|
|
24
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { LANG_FLAG, bilingual } from '../oclif/i18n.js';
|
|
4
|
+
import { BaseCommand } from '../oclif/base-command.js';
|
|
5
|
+
import { tCli } from '../lib/i18n.js';
|
|
6
|
+
import { CliExit } from '../lib/cli-exit.js';
|
|
7
|
+
import { sanitizeCell } from '../lib/cell-format.js';
|
|
8
|
+
import { getJudgePromptHash } from '../../grading/judge.js';
|
|
9
|
+
import { appendManagedDecision, evaluatePromoteGate, globalManagedDir, isCurrentlyPromoted, loadManagedRecord, probeSourceState, managedDir, managedRecordId, resolveManagedDir, } from '../../managed/index.js';
|
|
10
|
+
const SUPPORTED_KINDS = ['skill'];
|
|
11
|
+
/** 决定的 actor:--actor > git config user.name > $USER / $LOGNAME > unknown。git 参数无用户输入,安全。 */
|
|
12
|
+
function resolveActor(flagActor) {
|
|
13
|
+
if (flagActor && flagActor.trim())
|
|
14
|
+
return flagActor.trim();
|
|
15
|
+
try {
|
|
16
|
+
const name = execFileSync('git', ['config', 'user.name'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
17
|
+
if (name)
|
|
18
|
+
return name;
|
|
19
|
+
}
|
|
20
|
+
catch { /* git 缺失 / 无配置 → 回退环境 */ }
|
|
21
|
+
return process.env.USER || process.env.LOGNAME || 'unknown';
|
|
22
|
+
}
|
|
23
|
+
/** 洗 detail 里来自受管 JSON 的不可信值(verdict / judgePromptHash),文本输出防 ANSI/OSC 注入。 */
|
|
24
|
+
function safeDetail(detail) {
|
|
25
|
+
if (!detail)
|
|
26
|
+
return undefined;
|
|
27
|
+
return Object.fromEntries(Object.entries(detail).map(([k, v]) => [k, sanitizeCell(v)]));
|
|
28
|
+
}
|
|
29
|
+
/** blockKind → i18n key(细节 detail 作为 tCli params,文本路径经 safeDetail 洗白)。 */
|
|
30
|
+
const BLOCK_KEY = {
|
|
31
|
+
drifted: 'cli.promote.drifted',
|
|
32
|
+
no_current_evidence: 'cli.promote.no_current_evidence',
|
|
33
|
+
incomparable: 'cli.promote.incomparable',
|
|
34
|
+
verdict_blocked: 'cli.promote.verdict_blocked',
|
|
35
|
+
};
|
|
36
|
+
export default class Promote extends BaseCommand {
|
|
37
|
+
static description = bilingual({
|
|
38
|
+
zh: '把受管 skill 的当前版本按证据门禁「接受」为 promoted:默认仅放行 verdict=PROGRESS,在记录里追加一条带证据指针的人工决定。',
|
|
39
|
+
en: 'Accept a managed skill\'s current version as promoted, gated on evidence: by default only verdict=PROGRESS passes; appends a human decision with an evidence pointer to the record.',
|
|
40
|
+
});
|
|
41
|
+
static examples = [
|
|
42
|
+
{ description: bilingual({ zh: 'promote 一个证据达标的 skill', en: 'Promote a skill whose evidence passes the gate' }), command: '<%= config.bin %> promote review' },
|
|
43
|
+
{ description: bilingual({ zh: '接受 CAUTIOUS 结果(显式放宽门禁)', en: 'Accept a CAUTIOUS result (explicitly loosen the gate)' }), command: '<%= config.bin %> promote review --accept-cautious' },
|
|
44
|
+
{ description: bilingual({ zh: '越门 promote 并记录理由(人工 override)', en: 'Override the gate and record a reason (human override)' }), command: '<%= config.bin %> promote review --force --reason "已人工复核"' },
|
|
45
|
+
];
|
|
46
|
+
static args = {
|
|
47
|
+
name: Args.string({ description: bilingual({ zh: '受管 skill 名(omk list 里的 NAME)', en: 'Managed skill name (the NAME shown by omk list)' }), required: true }),
|
|
48
|
+
};
|
|
49
|
+
static flags = {
|
|
50
|
+
lang: LANG_FLAG,
|
|
51
|
+
kind: Flags.string({ description: bilingual({ zh: 'artifact 类型(当前仅 skill)', en: 'artifact kind (only skill today)' }), default: 'skill' }),
|
|
52
|
+
'accept-cautious': Flags.boolean({ description: bilingual({ zh: '把 CAUTIOUS 也算可接受(默认仅 PROGRESS)', en: 'also accept CAUTIOUS (default PROGRESS only)' }) }),
|
|
53
|
+
force: Flags.boolean({ description: bilingual({ zh: '越过可越门拦截强制 promote,记为人工 override 决定(无当前证据或源 hash 已变时仍拒)', en: 'override forceable gate blocks and force-promote, recorded as a human override (still refused with no current evidence or changed source hash)' }) }),
|
|
54
|
+
reason: Flags.string({ description: bilingual({ zh: 'promote / 越门的理由(写入决定)', en: 'reason for the promotion / override (stored on the decision)' }) }),
|
|
55
|
+
actor: Flags.string({ description: bilingual({ zh: '决定的 actor(默认取 git config user.name)', en: 'decision actor (defaults to git config user.name)' }) }),
|
|
56
|
+
global: Flags.boolean({ description: bilingual({ zh: '操作全局受管目录而非项目 .omk/managed', en: 'operate on the global managed dir instead of project .omk/managed' }) }),
|
|
57
|
+
json: Flags.boolean({ description: bilingual({ zh: '输出 JSON(版本化信封)供脚本消费', en: 'output JSON (versioned envelope) for scripts' }) }),
|
|
58
|
+
};
|
|
59
|
+
async run() {
|
|
60
|
+
const { args, flags } = await this.parse(Promote);
|
|
61
|
+
const lang = this.lang;
|
|
62
|
+
await this.runWithCliExit(async () => {
|
|
63
|
+
const name = args.name;
|
|
64
|
+
const kind = flags.kind;
|
|
65
|
+
if (!SUPPORTED_KINDS.includes(kind)) {
|
|
66
|
+
process.stderr.write(tCli('cli.promote.kind_unsupported', lang, { kind: sanitizeCell(String(kind)) }) + '\n');
|
|
67
|
+
throw new CliExit(1);
|
|
68
|
+
}
|
|
69
|
+
const dir = flags.global ? globalManagedDir() : resolveManagedDir(managedDir());
|
|
70
|
+
const record = loadManagedRecord(dir, managedRecordId(kind, name));
|
|
71
|
+
if (!record) {
|
|
72
|
+
process.stderr.write(tCli('cli.promote.not_managed', lang, { name: sanitizeCell(name), kind }) + '\n');
|
|
73
|
+
throw new CliExit(1);
|
|
74
|
+
}
|
|
75
|
+
const probe = probeSourceState(record);
|
|
76
|
+
const currentContentHash = probe.reachable ? probe.hash : undefined;
|
|
77
|
+
const sourceHashMismatch = probe.reachable && currentContentHash !== record.contentHash;
|
|
78
|
+
// 幂等:当前内容已 promote 过 → 无操作成功退出,不堆冗余事件。与 list 的不可达分支同口径:
|
|
79
|
+
// 源不可达不代表内容已变;只有源可达且 hash 已变时,才不把旧 promote 当作当前 no-op。
|
|
80
|
+
const alreadyPromoted = !sourceHashMismatch && isCurrentlyPromoted(record);
|
|
81
|
+
if (alreadyPromoted) {
|
|
82
|
+
if (flags.json)
|
|
83
|
+
this.log(JSON.stringify({ schemaVersion: 1, alreadyPromoted: { name, contentHash: record.contentHash } }, null, 2));
|
|
84
|
+
else
|
|
85
|
+
process.stderr.write(tCli('cli.promote.already_promoted', lang, { name: sanitizeCell(name) }) + '\n');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const currentJudgeHashes = new Set([getJudgePromptHash(true), getJudgePromptHash(false)]);
|
|
89
|
+
const gate = evaluatePromoteGate({ record, currentContentHash, acceptCautious: flags['accept-cautious'], currentJudgeHashes });
|
|
90
|
+
// 拦截:无当前证据 force 也越不过;源可达但 hash 已变也不可越门,因为 decision 仍锚在
|
|
91
|
+
// record.contentHash,越过去后 list 仍会按当前源 hash 判 stale。源不可达则可作为人工未核越门。
|
|
92
|
+
const forceable = gate.evidence !== undefined && !sourceHashMismatch;
|
|
93
|
+
const reason = flags.reason?.trim();
|
|
94
|
+
if (!gate.ok && flags.force && forceable && !reason) {
|
|
95
|
+
if (flags.json)
|
|
96
|
+
this.log(JSON.stringify({ schemaVersion: 1, blocked: { name, reasons: [{ blockKind: 'force_reason_required' }] } }, null, 2));
|
|
97
|
+
else
|
|
98
|
+
process.stderr.write(tCli('cli.promote.force_reason_required', lang) + '\n');
|
|
99
|
+
throw new CliExit(1);
|
|
100
|
+
}
|
|
101
|
+
if (!gate.ok && !(flags.force && forceable)) {
|
|
102
|
+
this.emitBlocked(name, gate, lang, flags.json, forceable);
|
|
103
|
+
throw new CliExit(1);
|
|
104
|
+
}
|
|
105
|
+
// 通过 或 force 越门:写决定。
|
|
106
|
+
const overridden = !gate.ok; // 走到这里且未提前 return,说明是 force 越门
|
|
107
|
+
const evidence = gate.evidence; // forceable 已保证非空(无证据分支在上面已拦)
|
|
108
|
+
const decision = {
|
|
109
|
+
decisionKind: 'promote',
|
|
110
|
+
actor: resolveActor(flags.actor),
|
|
111
|
+
decidedAt: new Date().toISOString(),
|
|
112
|
+
contentHash: record.contentHash,
|
|
113
|
+
...(evidence.reportId ? { reportId: evidence.reportId } : {}),
|
|
114
|
+
...(reason ? { reason } : {}),
|
|
115
|
+
...(overridden ? { override: { verdict: evidence.verdict ?? 'UNKNOWN', overriddenBlocks: gate.blocked.map((b) => b.blockKind) } } : {}),
|
|
116
|
+
};
|
|
117
|
+
appendManagedDecision(dir, record.id, decision);
|
|
118
|
+
// 非拦截级提醒(如 judgePromptHash 缺失)始终提示。当前 warnings 仅 incomparable_unverified 一类。
|
|
119
|
+
if (gate.warnings.length > 0)
|
|
120
|
+
process.stderr.write(tCli('cli.promote.incomparable_unverified', lang) + '\n');
|
|
121
|
+
this.emitPromoted(name, record, evidence.verdict, evidence.reportId, overridden, lang, flags.json);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
emitBlocked(name, gate, lang, json, forceable) {
|
|
125
|
+
if (json) {
|
|
126
|
+
this.log(JSON.stringify({ schemaVersion: 1, blocked: { name, reasons: gate.blocked.map((b) => ({ blockKind: b.blockKind, ...(b.detail ? { detail: b.detail } : {}) })) } }, null, 2));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
process.stderr.write(tCli('cli.promote.blocked_header', lang, { name: sanitizeCell(name) }) + '\n');
|
|
130
|
+
// detail 里的 verdict / judgePromptHash 来自不可信受管 JSON,文本路径先洗(与 list 同口径,防 ANSI/OSC 注入)。
|
|
131
|
+
for (const b of gate.blocked)
|
|
132
|
+
process.stderr.write(tCli(BLOCK_KEY[b.blockKind], lang, safeDetail(b.detail)) + '\n');
|
|
133
|
+
if (gate.warnings.length > 0)
|
|
134
|
+
process.stderr.write(tCli('cli.promote.incomparable_unverified', lang) + '\n');
|
|
135
|
+
// 有可锚定证据(非「无证据」终态)才提示 --force 可越门。
|
|
136
|
+
if (forceable)
|
|
137
|
+
process.stderr.write(tCli('cli.promote.force_hint', lang) + '\n');
|
|
138
|
+
}
|
|
139
|
+
emitPromoted(name, record, verdict, reportId, overridden, lang, json) {
|
|
140
|
+
const hash = record.contentHash;
|
|
141
|
+
if (json) {
|
|
142
|
+
this.log(JSON.stringify({
|
|
143
|
+
schemaVersion: 1,
|
|
144
|
+
promoted: { name, kind: record.kind, contentHash: hash, ...(reportId ? { reportId } : {}), verdict: verdict ?? null, override: overridden },
|
|
145
|
+
}, null, 2));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// hash / verdict / reportId 来自不可信受管 JSON,文本路径先洗(--json 路径上面已 return、保留原值)。
|
|
149
|
+
const short = sanitizeCell(hash.slice(0, 12));
|
|
150
|
+
const safeVerdict = sanitizeCell(verdict ?? 'UNKNOWN');
|
|
151
|
+
const safeName = sanitizeCell(name);
|
|
152
|
+
if (overridden)
|
|
153
|
+
this.log(tCli('cli.promote.forced_override', lang, { name: safeName, hash: short, verdict: safeVerdict }));
|
|
154
|
+
else
|
|
155
|
+
this.log(tCli('cli.promote.promoted', lang, { name: safeName, hash: short, verdict: safeVerdict, reportId: sanitizeCell(reportId ?? '—') }));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BaseCommand } from '../oclif/base-command.js';
|
|
2
|
+
export default class Rollback extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
description: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}[];
|
|
8
|
+
static args: {
|
|
9
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
10
|
+
};
|
|
11
|
+
static flags: {
|
|
12
|
+
lang: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
kind: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
reason: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
actor: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
};
|
|
19
|
+
run(): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { LANG_FLAG, bilingual } from '../oclif/i18n.js';
|
|
4
|
+
import { BaseCommand } from '../oclif/base-command.js';
|
|
5
|
+
import { tCli } from '../lib/i18n.js';
|
|
6
|
+
import { CliExit } from '../lib/cli-exit.js';
|
|
7
|
+
import { sanitizeCell } from '../lib/cell-format.js';
|
|
8
|
+
import { appendManagedDecision, globalManagedDir, isCurrentlyPromoted, loadManagedRecord, managedDir, managedRecordId, resolveManagedDir, } from '../../managed/index.js';
|
|
9
|
+
const SUPPORTED_KINDS = ['skill'];
|
|
10
|
+
/** 决定的 actor:--actor > git config user.name > $USER / $LOGNAME > unknown。git 参数无用户输入,安全。 */
|
|
11
|
+
function resolveActor(flagActor) {
|
|
12
|
+
if (flagActor && flagActor.trim())
|
|
13
|
+
return flagActor.trim();
|
|
14
|
+
try {
|
|
15
|
+
const name = execFileSync('git', ['config', 'user.name'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
16
|
+
if (name)
|
|
17
|
+
return name;
|
|
18
|
+
}
|
|
19
|
+
catch { /* git 缺失 / 无配置 → 回退环境 */ }
|
|
20
|
+
return process.env.USER || process.env.LOGNAME || 'unknown';
|
|
21
|
+
}
|
|
22
|
+
export default class Rollback extends BaseCommand {
|
|
23
|
+
static description = bilingual({
|
|
24
|
+
zh: '回退受管 skill 当前版本的 promoted 接受:撤销最近一次 promote,在记录里追加一条 rollback 决定(源未漂移则状态回到 measurable,源已漂移则仍 stale)。',
|
|
25
|
+
en: 'Roll back a managed skill\'s current promoted acceptance: revoke the latest promote by appending a rollback decision (state returns to measurable if the source is unchanged, or stays stale if it has drifted).',
|
|
26
|
+
});
|
|
27
|
+
static examples = [
|
|
28
|
+
{ description: bilingual({ zh: '回退一个已 promoted 的 skill', en: 'Roll back a promoted skill' }), command: '<%= config.bin %> rollback review' },
|
|
29
|
+
{ description: bilingual({ zh: '回退并记录理由', en: 'Roll back and record a reason' }), command: '<%= config.bin %> rollback review --reason "线上发现回归"' },
|
|
30
|
+
];
|
|
31
|
+
static args = {
|
|
32
|
+
name: Args.string({ description: bilingual({ zh: '受管 skill 名(omk list 里的 NAME)', en: 'Managed skill name (the NAME shown by omk list)' }), required: true }),
|
|
33
|
+
};
|
|
34
|
+
static flags = {
|
|
35
|
+
lang: LANG_FLAG,
|
|
36
|
+
kind: Flags.string({ description: bilingual({ zh: 'artifact 类型(当前仅 skill)', en: 'artifact kind (only skill today)' }), default: 'skill' }),
|
|
37
|
+
reason: Flags.string({ description: bilingual({ zh: '回退的理由(写入决定)', en: 'reason for the rollback (stored on the decision)' }) }),
|
|
38
|
+
actor: Flags.string({ description: bilingual({ zh: '决定的 actor(默认取 git config user.name)', en: 'decision actor (defaults to git config user.name)' }) }),
|
|
39
|
+
global: Flags.boolean({ description: bilingual({ zh: '操作全局受管目录而非项目 .omk/managed', en: 'operate on the global managed dir instead of project .omk/managed' }) }),
|
|
40
|
+
json: Flags.boolean({ description: bilingual({ zh: '输出 JSON(版本化信封)供脚本消费', en: 'output JSON (versioned envelope) for scripts' }) }),
|
|
41
|
+
};
|
|
42
|
+
async run() {
|
|
43
|
+
const { args, flags } = await this.parse(Rollback);
|
|
44
|
+
const lang = this.lang;
|
|
45
|
+
await this.runWithCliExit(async () => {
|
|
46
|
+
const name = args.name;
|
|
47
|
+
const kind = flags.kind;
|
|
48
|
+
if (!SUPPORTED_KINDS.includes(kind)) {
|
|
49
|
+
process.stderr.write(tCli('cli.rollback.kind_unsupported', lang, { kind: sanitizeCell(String(kind)) }) + '\n');
|
|
50
|
+
throw new CliExit(1);
|
|
51
|
+
}
|
|
52
|
+
const dir = flags.global ? globalManagedDir() : resolveManagedDir(managedDir());
|
|
53
|
+
const record = loadManagedRecord(dir, managedRecordId(kind, name));
|
|
54
|
+
if (!record) {
|
|
55
|
+
process.stderr.write(tCli('cli.rollback.not_managed', lang, { name: sanitizeCell(name), kind }) + '\n');
|
|
56
|
+
throw new CliExit(1);
|
|
57
|
+
}
|
|
58
|
+
// rollback 是内容锚定的纯决定撤销:只看 record.contentHash 上的 promote/rollback 事件流,不探源、不设门禁
|
|
59
|
+
// (降级永远安全)。当前未 promoted → 无可回退;若曾对当前内容 promote 过(即已回退)则幂等成功,否则报错。
|
|
60
|
+
if (!isCurrentlyPromoted(record)) {
|
|
61
|
+
const hadPromote = record.decisions.some((d) => d.decisionKind === 'promote' && d.contentHash === record.contentHash);
|
|
62
|
+
if (hadPromote) {
|
|
63
|
+
if (flags.json)
|
|
64
|
+
this.log(JSON.stringify({ schemaVersion: 1, alreadyRolledBack: { name, contentHash: record.contentHash } }, null, 2));
|
|
65
|
+
else
|
|
66
|
+
process.stderr.write(tCli('cli.rollback.already_rolled_back', lang, { name: sanitizeCell(name) }) + '\n');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (flags.json)
|
|
70
|
+
this.log(JSON.stringify({ schemaVersion: 1, notPromoted: { name } }, null, 2));
|
|
71
|
+
else
|
|
72
|
+
process.stderr.write(tCli('cli.rollback.not_promoted', lang, { name: sanitizeCell(name) }) + '\n');
|
|
73
|
+
throw new CliExit(1);
|
|
74
|
+
}
|
|
75
|
+
const reason = flags.reason?.trim();
|
|
76
|
+
const decision = {
|
|
77
|
+
decisionKind: 'rollback',
|
|
78
|
+
actor: resolveActor(flags.actor),
|
|
79
|
+
decidedAt: new Date().toISOString(),
|
|
80
|
+
contentHash: record.contentHash,
|
|
81
|
+
...(reason ? { reason } : {}),
|
|
82
|
+
};
|
|
83
|
+
appendManagedDecision(dir, record.id, decision);
|
|
84
|
+
if (flags.json) {
|
|
85
|
+
this.log(JSON.stringify({
|
|
86
|
+
schemaVersion: 1,
|
|
87
|
+
rolledBack: { name, kind: record.kind, contentHash: record.contentHash, ...(reason ? { reason } : {}) },
|
|
88
|
+
}, null, 2));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// hash 来自不可信受管 JSON,文本路径先洗(--json 路径上面已 return、保留原值)。
|
|
92
|
+
this.log(tCli('cli.rollback.rolled_back', lang, { name: sanitizeCell(name), hash: sanitizeCell(record.contentHash.slice(0, 12)) }));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -26,6 +26,7 @@ export default class Sample extends BaseCommand {
|
|
|
26
26
|
batch: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
27
27
|
count: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
28
28
|
model: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
29
|
+
executor: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
29
30
|
'skill-dir': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
30
31
|
focus: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
31
32
|
'no-mock': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -7,7 +7,7 @@ import { BaseCommand } from '../oclif/base-command.js';
|
|
|
7
7
|
import { integerStringParser } from '../oclif/parsers.js';
|
|
8
8
|
import { CliExit } from '../lib/cli-exit.js';
|
|
9
9
|
import { tCli } from '../lib/i18n.js';
|
|
10
|
-
import {
|
|
10
|
+
import { projectReportsDir, globalReportsDir } from '../../eval-core/measurement-dirs.js';
|
|
11
11
|
import { loadSamples, parseYaml } from '../../inputs/load-samples.js';
|
|
12
12
|
import { hashSample } from '../../eval-core/evaluation-reporting.js';
|
|
13
13
|
import { hashArtifactSource } from '../../inputs/content-hash.js';
|
|
@@ -154,9 +154,8 @@ export function writeFixedSamplesToSources(loaded, samples, changedIds) {
|
|
|
154
154
|
}
|
|
155
155
|
async function runSampleFix(args, flags, lang) {
|
|
156
156
|
const { fixSamples } = await import('../../authoring/sample-fixer.js');
|
|
157
|
-
const { createFileStore } = await import('../../server/report-store.js');
|
|
157
|
+
const { createFileStore, createOverlayReportStore } = await import('../../server/report-store.js');
|
|
158
158
|
const model = flags.model;
|
|
159
|
-
const reportsDir = resolve(flags['reports-dir'] ?? DEFAULT_REPORTS_DIR);
|
|
160
159
|
const skillPath = args.skillPath;
|
|
161
160
|
if (!skillPath) {
|
|
162
161
|
console.error(lang === 'zh' ? '请指定 skill 路径,如: omk sample skills/my-skill/SKILL.md --fix' : 'Specify skill path: omk sample skills/my-skill/SKILL.md --fix');
|
|
@@ -181,10 +180,17 @@ async function runSampleFix(args, flags, lang) {
|
|
|
181
180
|
: basename(resolvedSkillPath, extname(resolvedSkillPath));
|
|
182
181
|
const treatmentName = flags.treatment ?? defaultTreatmentName;
|
|
183
182
|
process.stderr.write(lang === 'zh' ? `🔍 正在查找 ${treatmentName} 的最新评测报告...\n` : `🔍 Scanning latest report for ${treatmentName}...\n`);
|
|
184
|
-
|
|
183
|
+
// 显式 --reports-dir 固定该目录;默认 overlay(项目 .omk/reports 盖全局),findByVariant 记录优先看项目、
|
|
184
|
+
// 空则全局兜底,不因 eval 写默认翻项目而查不到报告。
|
|
185
|
+
const store = flags['reports-dir']
|
|
186
|
+
? createFileStore(resolve(flags['reports-dir']))
|
|
187
|
+
: createOverlayReportStore(projectReportsDir(), globalReportsDir());
|
|
185
188
|
const reports = await store.findByVariant(treatmentName);
|
|
186
189
|
if (reports.length === 0) {
|
|
187
|
-
|
|
190
|
+
const where = flags['reports-dir']
|
|
191
|
+
? resolve(flags['reports-dir'])
|
|
192
|
+
: (lang === 'zh' ? '项目 .omk/reports 或全局 ~/.oh-my-knowledge/reports' : 'project .omk/reports or global ~/.oh-my-knowledge/reports');
|
|
193
|
+
console.error(lang === 'zh' ? `未找到 ${treatmentName} 的评测报告(报告目录: ${where})` : `No eval report found for ${treatmentName} in ${where}`);
|
|
188
194
|
throw new CliExit(1);
|
|
189
195
|
}
|
|
190
196
|
const report = reports[0];
|
|
@@ -225,7 +231,7 @@ async function runSampleFix(args, flags, lang) {
|
|
|
225
231
|
}
|
|
226
232
|
process.stderr.write(lang === 'zh' ? `🔧 发现 ${sampleDesignCount} 条 sample_design 失败,开始修复...\n` : `🔧 Found ${sampleDesignCount} sample_design failure(s), fixing...\n`);
|
|
227
233
|
const { createExecutor } = await import('../../executors/index.js');
|
|
228
|
-
const exec = createExecutor('claude');
|
|
234
|
+
const exec = createExecutor(flags.executor ?? 'claude');
|
|
229
235
|
const executorFn = async (opts) => {
|
|
230
236
|
const result = await exec({
|
|
231
237
|
model: opts.model,
|
|
@@ -273,8 +279,8 @@ async function runSampleFromTraces(flags, lang) {
|
|
|
273
279
|
const obsDir = resolve(flags['observations-dir'] ?? DEFAULT_OBSERVATIONS_DIR);
|
|
274
280
|
if (!existsSync(obsDir)) {
|
|
275
281
|
console.error(lang === 'zh'
|
|
276
|
-
? `
|
|
277
|
-
: `
|
|
282
|
+
? `observe-inbox 目录不存在: ${obsDir}(先运行 omk observe ingest 生成)`
|
|
283
|
+
: `Observe-inbox dir not found: ${obsDir} (run omk observe ingest first)`);
|
|
278
284
|
throw new CliExit(1);
|
|
279
285
|
}
|
|
280
286
|
// Drop noise-tier signals up front: they're exactly what the generator is told to
|
|
@@ -298,7 +304,7 @@ async function runSampleFromTraces(flags, lang) {
|
|
|
298
304
|
? `🔭 发现 ${items.length} 个失败信号,正在生成回归用例草稿...\n`
|
|
299
305
|
: `🔭 Found ${items.length} failure signal(s); generating regression-sample drafts...\n`);
|
|
300
306
|
try {
|
|
301
|
-
const { samples, costUSD } = await generateSamplesFromTraces({ items, count, model: flags.model });
|
|
307
|
+
const { samples, costUSD } = await generateSamplesFromTraces({ items, count, model: flags.model, executorName: flags.executor });
|
|
302
308
|
const cost = costUSD > 0 ? ` $${costUSD.toFixed(4)}` : '';
|
|
303
309
|
if (samples.length === 0) {
|
|
304
310
|
// The model conservatively skipped every signal (noise / unreproducible). That's a
|
|
@@ -380,7 +386,7 @@ async function runSample(args, flags, lang) {
|
|
|
380
386
|
}
|
|
381
387
|
try {
|
|
382
388
|
const skillContent = readFileSync(skillPath, 'utf-8');
|
|
383
|
-
const { samples, costUSD } = await generateSamples({ skillContent, count, model, focus, noMock: flags['no-mock'] });
|
|
389
|
+
const { samples, costUSD } = await generateSamples({ skillContent, count, model, focus, noMock: flags['no-mock'], executorName: flags.executor });
|
|
384
390
|
mkdirSync(dirname(samplesPath), { recursive: true });
|
|
385
391
|
writeFileSync(samplesPath, JSON.stringify(samples, null, 2));
|
|
386
392
|
const cost = costUSD > 0 ? ` $${costUSD.toFixed(4)}` : '';
|
|
@@ -444,7 +450,7 @@ async function runSample(args, flags, lang) {
|
|
|
444
450
|
process.stderr.write(tCli('cli.gen.single_generating_auto', lang));
|
|
445
451
|
}
|
|
446
452
|
try {
|
|
447
|
-
const { samples, costUSD } = await generateSamples({ skillContent, count, model, focus, noMock: flags['no-mock'] });
|
|
453
|
+
const { samples, costUSD } = await generateSamples({ skillContent, count, model, focus, noMock: flags['no-mock'], executorName: flags.executor });
|
|
448
454
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
449
455
|
writeFileSync(outputPath, JSON.stringify(samples, null, 2));
|
|
450
456
|
const cost = costUSD > 0 ? ` $${costUSD.toFixed(4)}` : '';
|
|
@@ -528,6 +534,12 @@ export default class Sample extends BaseCommand {
|
|
|
528
534
|
}),
|
|
529
535
|
default: 'sonnet',
|
|
530
536
|
}),
|
|
537
|
+
executor: Flags.string({
|
|
538
|
+
description: bilingual({
|
|
539
|
+
zh: '执行器名,默认 claude(同 omk eval / doctor / evolve)。指定 codex 等其它执行器时,记得连带传一个该执行器能识别的 --model。',
|
|
540
|
+
en: 'Executor name, default claude (same as omk eval / doctor / evolve). When using another executor like codex, also pass a --model it recognizes.',
|
|
541
|
+
}),
|
|
542
|
+
}),
|
|
531
543
|
'skill-dir': Flags.string({
|
|
532
544
|
description: bilingual({
|
|
533
545
|
zh: 'skill 根目录,默认 skills。batch 模式扫此目录。',
|
|
@@ -576,8 +588,8 @@ export default class Sample extends BaseCommand {
|
|
|
576
588
|
}),
|
|
577
589
|
'observations-dir': Flags.string({
|
|
578
590
|
description: bilingual({
|
|
579
|
-
zh: 'observe inbox 目录(from-traces 模式用),默认项目 .omk/
|
|
580
|
-
en: 'Observe inbox dir (from-traces mode), default project .omk/
|
|
591
|
+
zh: 'observe inbox 目录(from-traces 模式用),默认项目 .omk/observe-inbox。',
|
|
592
|
+
en: 'Observe inbox dir (from-traces mode), default project .omk/observe-inbox.',
|
|
581
593
|
}),
|
|
582
594
|
}),
|
|
583
595
|
};
|
|
@@ -14,7 +14,9 @@ export default class Studio extends BaseCommand {
|
|
|
14
14
|
host: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
15
|
'reports-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
16
|
'analyses-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
'doctors-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
18
|
'observations-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
20
|
'no-open': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
21
|
dev: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
22
|
};
|