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
|
@@ -84,8 +84,11 @@ function batchItemFallbackReport(batch, item) {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
async function loadBatchChildReports(batch, reportsDir, lang) {
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// 读侧走 overlay(本次 reportsDir 优先 → 全局兜底):写默认翻项目后,batch.json 引用的存量 / 全局子报告
|
|
88
|
+
// 仍能 get 命中,不致降级到 batchItemFallbackReport(来自 batch metadata 的过时数据)。
|
|
89
|
+
const { createOverlayReportStore } = await import('../../../server/report-store.js');
|
|
90
|
+
const { globalReportsDir } = await import('../../../eval-core/measurement-dirs.js');
|
|
91
|
+
const store = createOverlayReportStore(reportsDir, globalReportsDir());
|
|
89
92
|
const reports = [];
|
|
90
93
|
for (const item of batch.items) {
|
|
91
94
|
const loaded = await store.get(item.reportId);
|
|
@@ -134,6 +137,8 @@ async function announceSavedReport({ report, filePath, reportsDir, values, lang,
|
|
|
134
137
|
process.stderr.write(tCli('cli.run.report_saved', lang, { path: filePath }));
|
|
135
138
|
if (!values['no-serve'] && process.stdout.isTTY) {
|
|
136
139
|
const { createReportServer } = await import('../../../server/report-server.js');
|
|
140
|
+
// eval 结束的即时 serve 刻意钉本次 outputDir(--global 时即全局),不走 overlay:它的语义是「看我刚跑的这份」,
|
|
141
|
+
// 单目录保证刚写的报告(含 --global 写到全局的)一定可见;浏览全机器历史是 omk studio 的事(默认 overlay 项目优先)。
|
|
137
142
|
const server = createReportServer({ reportsDir });
|
|
138
143
|
const serverUrl = await server.start();
|
|
139
144
|
const reportUrl = `${serverUrl}/reports/${report.id}`;
|
|
@@ -368,7 +373,13 @@ export default class Eval extends BaseCommand {
|
|
|
368
373
|
}),
|
|
369
374
|
}),
|
|
370
375
|
'output-dir': Flags.string({
|
|
371
|
-
description: bilingual({ zh: '
|
|
376
|
+
description: bilingual({ zh: '报告输出目录(默认项目级 .omk/reports)', en: 'Report output dir (default project .omk/reports)' }),
|
|
377
|
+
}),
|
|
378
|
+
global: Flags.boolean({
|
|
379
|
+
description: bilingual({
|
|
380
|
+
zh: '报告写全局 ~/.oh-my-knowledge/reports,而非项目 .omk/',
|
|
381
|
+
en: 'Write report to global ~/.oh-my-knowledge/reports instead of project .omk/',
|
|
382
|
+
}),
|
|
372
383
|
}),
|
|
373
384
|
// ── 评测 toggle ──
|
|
374
385
|
'no-judge': Flags.boolean({
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { BaseCommand } from '../oclif/base-command.js';
|
|
2
2
|
import { type CliLang } from '../lib/i18n.js';
|
|
3
3
|
import type { EvolveArgs, EvolveFlags } from '../lib/cmd-flags.js';
|
|
4
|
+
/** 路径处是否已存在「用例源」:按 statSync 判型 —— 文件(含无扩展名)直接算存在;
|
|
5
|
+
* 目录看是否含候选用例文件(排除 report/health/_ 前缀,对齐 sample.ts 的发现约定)。
|
|
6
|
+
* 用于区分「损坏文件(存在但解析失败 → 报错不覆盖)」与「确实没有用例(可生成)」。
|
|
7
|
+
* 不能用 extname 猜文件/目录:无扩展名的损坏样本文件会绕过守卫被覆盖,
|
|
8
|
+
* 带点的目录名(如 samples.v2/)会被误当文件。 */
|
|
9
|
+
export declare function sampleSourceExists(p: string): boolean;
|
|
10
|
+
/** 自动生成时的落盘目标:已存在的目录(含带点目录名,如 samples.v2/)→ 写进目录内的
|
|
11
|
+
* samples.json;已存在的文件 → 覆盖该文件;不存在 → 按扩展名(有扩展名当文件,无扩展名
|
|
12
|
+
* 当目录,落 samples.json)。与 sampleSourceExists 同用 statSync 判型,不被带点目录名
|
|
13
|
+
* 误当成文件(否则 writeFileSync 撞 EISDIR)。 */
|
|
14
|
+
export declare function resolveSampleOutFile(samplesAbs: string): string;
|
|
4
15
|
export declare function runEvolve(args: EvolveArgs, flags: EvolveFlags, lang: CliLang): Promise<void>;
|
|
5
16
|
export default class Evolve extends BaseCommand {
|
|
6
17
|
static description: string;
|
|
@@ -30,6 +41,7 @@ export default class Evolve extends BaseCommand {
|
|
|
30
41
|
'auto-fix-samples': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
31
42
|
'sample-fix-max-attempts': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
32
43
|
'reuse-latest-eval': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
44
|
+
'snapshot-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
33
45
|
'improve-mode': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
34
46
|
'holdout-ratio': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
35
47
|
'no-significance-gate': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { resolve, join } from 'node:path';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { resolve, join, dirname, extname } from 'node:path';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, statSync } from 'node:fs';
|
|
3
3
|
import { Args, Flags } from '@oclif/core';
|
|
4
4
|
import { LANG_FLAG, bilingual } from '../oclif/i18n.js';
|
|
5
5
|
import { BaseCommand } from '../oclif/base-command.js';
|
|
@@ -7,6 +7,17 @@ import { enumStringParser, integerStringParser, numberStringParser } from '../oc
|
|
|
7
7
|
import { CliExit } from '../lib/cli-exit.js';
|
|
8
8
|
import { tCli } from '../lib/i18n.js';
|
|
9
9
|
import { makeOnProgress } from '../lib/progress.js';
|
|
10
|
+
/** 受管联动旁路:evolve 写回 source 后记证据 + re-baseline。任何异常都不该让 evolve 失败,
|
|
11
|
+
* 故 try/catch 吞掉、返回 null(同 eval 的 recordEvidenceSafely 口径)。 */
|
|
12
|
+
async function recordEvolveOutcomeSafely(input) {
|
|
13
|
+
try {
|
|
14
|
+
const { recordEvolveOutcome } = await import('../lib/record-evolve-outcome.js');
|
|
15
|
+
return await recordEvolveOutcome(input);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
10
21
|
const VALID_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
11
22
|
function validateEvolveEffort(raw, lang) {
|
|
12
23
|
if (!VALID_EFFORTS.has(raw)) {
|
|
@@ -18,6 +29,40 @@ function validateEvolveEffort(raw, lang) {
|
|
|
18
29
|
}
|
|
19
30
|
return raw;
|
|
20
31
|
}
|
|
32
|
+
/** 路径处是否已存在「用例源」:按 statSync 判型 —— 文件(含无扩展名)直接算存在;
|
|
33
|
+
* 目录看是否含候选用例文件(排除 report/health/_ 前缀,对齐 sample.ts 的发现约定)。
|
|
34
|
+
* 用于区分「损坏文件(存在但解析失败 → 报错不覆盖)」与「确实没有用例(可生成)」。
|
|
35
|
+
* 不能用 extname 猜文件/目录:无扩展名的损坏样本文件会绕过守卫被覆盖,
|
|
36
|
+
* 带点的目录名(如 samples.v2/)会被误当文件。 */
|
|
37
|
+
export function sampleSourceExists(p) {
|
|
38
|
+
let st;
|
|
39
|
+
try {
|
|
40
|
+
st = statSync(p);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (!st.isDirectory())
|
|
46
|
+
return true;
|
|
47
|
+
try {
|
|
48
|
+
return readdirSync(p).some((f) => /\.(json|ya?ml)$/i.test(f) && !/^(report|health|_)/i.test(f));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** 自动生成时的落盘目标:已存在的目录(含带点目录名,如 samples.v2/)→ 写进目录内的
|
|
55
|
+
* samples.json;已存在的文件 → 覆盖该文件;不存在 → 按扩展名(有扩展名当文件,无扩展名
|
|
56
|
+
* 当目录,落 samples.json)。与 sampleSourceExists 同用 statSync 判型,不被带点目录名
|
|
57
|
+
* 误当成文件(否则 writeFileSync 撞 EISDIR)。 */
|
|
58
|
+
export function resolveSampleOutFile(samplesAbs) {
|
|
59
|
+
try {
|
|
60
|
+
return statSync(samplesAbs).isDirectory() ? join(samplesAbs, 'samples.json') : samplesAbs;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return extname(samplesAbs) ? samplesAbs : join(samplesAbs, 'samples.json');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
21
66
|
// runEvolve module-level helper:cli-exit.test 测「skillPath 空 throw CliExit(1)」走
|
|
22
67
|
// in-process import 验证业务,Command.run() body 直接调它。
|
|
23
68
|
export async function runEvolve(args, flags, lang) {
|
|
@@ -43,17 +88,86 @@ export async function runEvolve(args, flags, lang) {
|
|
|
43
88
|
throw new CliExit(1);
|
|
44
89
|
}
|
|
45
90
|
const skillPath = resolvedInput.skillPath;
|
|
91
|
+
// evolve 目标的形态(目录-skill / 文件-skill),供受管联动按形态精确匹配记录。**取解析后形态**而非「入参是不是
|
|
92
|
+
// 目录」:帮助文档鼓励传 `skills/foo/SKILL.md`,若按入参判会得 false、匹配不到 install 落的目录记录(漂移永不消)。
|
|
93
|
+
const skillIsDir = resolvedInput.isDirectorySkill;
|
|
46
94
|
let samplesFile = flags.samples;
|
|
47
95
|
if (samplesFile === 'eval-samples.json' && !existsSync(resolve(samplesFile))) {
|
|
48
96
|
samplesFile = resolvedInput.samplesPath;
|
|
49
97
|
}
|
|
50
|
-
|
|
98
|
+
// 参数校验必须早于任何昂贵副作用(自动生成用例 / LLM 调用)。
|
|
51
99
|
const { parseJudgeModelsArgOrExit } = await import('../lib/parse-run-config.js');
|
|
52
100
|
const evolveJudges = parseJudgeModelsArgOrExit(flags['judge-models']);
|
|
53
101
|
if (evolveJudges.length > 1) {
|
|
54
102
|
console.error(tCli('cli.common.judge_models_single_only', lang, { cmd: 'omk evolve' }));
|
|
55
103
|
throw new CliExit(2);
|
|
56
104
|
}
|
|
105
|
+
// 无用例时自动生成 —— 让 omk evolve 成为「检测(doctor) → 生成用例 → 自迭代」一键命令。
|
|
106
|
+
// 已有用例(且非空)则原样使用;生成失败按普通错误退出。
|
|
107
|
+
const samplesAbs = resolve(samplesFile);
|
|
108
|
+
let hasSamples = false;
|
|
109
|
+
let loadErr = null;
|
|
110
|
+
try {
|
|
111
|
+
const { loadSamples } = await import('../../inputs/load-samples.js');
|
|
112
|
+
hasSamples = loadSamples(samplesAbs).samples.length > 0;
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
loadErr = err;
|
|
116
|
+
}
|
|
117
|
+
const sourceExists = sampleSourceExists(samplesAbs);
|
|
118
|
+
// 用例源已存在却解析失败(JSON/YAML 语法错、duplicate id 等)= 损坏文件,
|
|
119
|
+
// 绝不用 LLM 生成内容覆盖它 —— 报错退出,让用户先修。只有真的没有用例源(文件
|
|
120
|
+
// 不存在 / 目录无候选用例文件)才进入自动生成。
|
|
121
|
+
if (loadErr && sourceExists) {
|
|
122
|
+
console.error(lang === 'zh'
|
|
123
|
+
? `评测用例文件解析失败,evolve 不会覆盖它,请先修复:${samplesAbs}\n 原因:${loadErr.message}`
|
|
124
|
+
: `Failed to parse the samples source; evolve will not overwrite it. Fix it first: ${samplesAbs}\n reason: ${loadErr.message}`);
|
|
125
|
+
throw new CliExit(1);
|
|
126
|
+
}
|
|
127
|
+
if (!hasSamples) {
|
|
128
|
+
try {
|
|
129
|
+
const { generateSamples } = await import('../../authoring/generator.js');
|
|
130
|
+
const skillContent = readFileSync(resolve(skillPath), 'utf-8');
|
|
131
|
+
// outFile:已存在目录写进内部 samples.json,已存在文件覆盖,不存在按扩展名 —— statSync
|
|
132
|
+
// 判型,不被带点目录名(samples.v2/)骗。loadSamples 目录模式会自动发现生成的文件。
|
|
133
|
+
const outFile = resolveSampleOutFile(samplesAbs);
|
|
134
|
+
// sourceExists 为真 = 用例源存在但解析出 0 条(合法空 [])→ 明示"重新生成覆盖空文件",
|
|
135
|
+
// 不静默盖用户文件;为假 = 真没有用例源 → "未发现,生成"。
|
|
136
|
+
process.stderr.write(lang === 'zh'
|
|
137
|
+
? (sourceExists
|
|
138
|
+
? `评测用例为空,正在重新生成并覆盖 ${outFile} …\n`
|
|
139
|
+
: `未发现评测用例,正在自动生成到 ${outFile} …\n`)
|
|
140
|
+
: (sourceExists
|
|
141
|
+
? `Samples are empty; regenerating (overwriting) ${outFile} …\n`
|
|
142
|
+
: `No samples found; auto-generating to ${outFile} …\n`));
|
|
143
|
+
const { samples, costUSD } = await generateSamples({
|
|
144
|
+
skillContent,
|
|
145
|
+
model: flags.model,
|
|
146
|
+
executorName: flags.executor,
|
|
147
|
+
});
|
|
148
|
+
// 模型可能保守返回 0 条 —— 不写空文件再空跑迭代,直接报错让用户改用
|
|
149
|
+
// `omk sample --focus` 引导生成或手写用例。
|
|
150
|
+
if (samples.length === 0) {
|
|
151
|
+
console.error(lang === 'zh'
|
|
152
|
+
? '自动生成返回 0 条用例,已中止。请用 `omk sample <skill> --focus "…"` 引导生成,或手写后重试。'
|
|
153
|
+
: 'Auto-generation produced 0 samples; aborting. Use `omk sample <skill> --focus "…"` to guide generation, or write samples manually.');
|
|
154
|
+
throw new CliExit(1);
|
|
155
|
+
}
|
|
156
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
157
|
+
writeFileSync(outFile, JSON.stringify(samples, null, 2));
|
|
158
|
+
const cost = costUSD > 0 ? ` $${costUSD.toFixed(4)}` : '';
|
|
159
|
+
process.stderr.write(lang === 'zh'
|
|
160
|
+
? `已生成 ${samples.length} 条用例${cost},开始自迭代。\n`
|
|
161
|
+
: `Generated ${samples.length} samples${cost}; starting evolution.\n`);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
if (err instanceof CliExit)
|
|
165
|
+
throw err;
|
|
166
|
+
console.error(tCli('cli.common.error_prefix', lang, { message: err.message }));
|
|
167
|
+
throw new CliExit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const { evolveSkill } = await import('../../authoring/evolver.js');
|
|
57
171
|
process.stderr.write(tCli('cli.evolve.section_header', lang, { path: skillPath }));
|
|
58
172
|
try {
|
|
59
173
|
const result = await evolveSkill({
|
|
@@ -83,6 +197,8 @@ export async function runEvolve(args, flags, lang) {
|
|
|
83
197
|
testRatio: Number(flags['test-ratio']),
|
|
84
198
|
editBudget: flags['no-edit-budget'] ? 0 : Number(flags['edit-budget']),
|
|
85
199
|
rejectMemory: !flags['no-reject-memory'],
|
|
200
|
+
// --snapshot-only:不写回 source,候选只留在 evolve/<skillName>.r{N}.md(供人工挑选 / promote)。
|
|
201
|
+
writeBackToSource: !flags['snapshot-only'],
|
|
86
202
|
improveMode: flags['improve-mode'] === 'rewrite' ? 'rewrite' : 'agent',
|
|
87
203
|
onProgress: makeOnProgress(lang),
|
|
88
204
|
onRoundProgress({ round, totalRounds: _totalRounds, phase, score, delta, accepted, costUSD, costReported, error, significant }) {
|
|
@@ -146,6 +262,28 @@ export async function runEvolve(args, flags, lang) {
|
|
|
146
262
|
if (result.reportId) {
|
|
147
263
|
process.stderr.write(tCli('cli.evolve.report_link', lang, { id: result.reportId }));
|
|
148
264
|
}
|
|
265
|
+
if (flags['snapshot-only']) {
|
|
266
|
+
// 不写回 source —— 候选留在 evolve/ 供人工挑选;受管记录不动。
|
|
267
|
+
process.stderr.write(tCli('cli.evolve.snapshot_only_hint', lang, {
|
|
268
|
+
dir: join(resolve(skillPath, '..'), 'evolve'),
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
// 受管 skill:把胜出版本记成带 verdict 的证据 + re-baseline → omk list 显 measurable。
|
|
273
|
+
// 升 promoted 仍由人 omk promote 决定(统计门 ≠ 人的接受)。未纳管 / 无改进 → 静默 no-op。
|
|
274
|
+
const recorded = await recordEvolveOutcomeSafely({
|
|
275
|
+
reportId: result.reportId,
|
|
276
|
+
bestRound: result.bestRound,
|
|
277
|
+
skillPath: resolvedInput.skillPath,
|
|
278
|
+
skillDir: resolvedInput.skillDir,
|
|
279
|
+
isDirectorySkill: skillIsDir,
|
|
280
|
+
});
|
|
281
|
+
if (recorded) {
|
|
282
|
+
process.stderr.write(tCli('cli.evolve.evidence_recorded_managed', lang, {
|
|
283
|
+
name: recorded.name, verdict: recorded.verdict,
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
149
287
|
console.log(JSON.stringify(result, null, 2));
|
|
150
288
|
}
|
|
151
289
|
catch (err) {
|
|
@@ -208,8 +346,8 @@ export default class Evolve extends BaseCommand {
|
|
|
208
346
|
}),
|
|
209
347
|
model: Flags.string({
|
|
210
348
|
description: bilingual({
|
|
211
|
-
zh: '被评测的 LLM,默认 sonnet',
|
|
212
|
-
en: 'Evaluated LLM, default sonnet',
|
|
349
|
+
zh: '被评测的 LLM,默认 sonnet。无用例时也用作自动生成用例的出题模型。',
|
|
350
|
+
en: 'Evaluated LLM, default sonnet. Also used as the sample-generation model when no samples exist.',
|
|
213
351
|
}),
|
|
214
352
|
default: 'sonnet',
|
|
215
353
|
}),
|
|
@@ -298,6 +436,13 @@ export default class Evolve extends BaseCommand {
|
|
|
298
436
|
}),
|
|
299
437
|
default: false,
|
|
300
438
|
}),
|
|
439
|
+
'snapshot-only': Flags.boolean({
|
|
440
|
+
description: bilingual({
|
|
441
|
+
zh: '只产候选、不写回 source:胜出版本留在 evolve/<skillName>.r{N}.md 供你挑选,再 omk promote 接受。受管 skill 默认会写回 source 并记证据(measurable)。',
|
|
442
|
+
en: 'Produce candidates only, do not write back to source: the winner stays in evolve/<skillName>.r{N}.md for you to pick and then omk promote. By default a managed skill is written back and evidence is recorded (measurable).',
|
|
443
|
+
}),
|
|
444
|
+
default: false,
|
|
445
|
+
}),
|
|
301
446
|
'improve-mode': Flags.string({
|
|
302
447
|
description: bilingual({
|
|
303
448
|
zh: '改写策略(默认:agent)',
|
|
@@ -3,6 +3,15 @@ import { Args } from '@oclif/core';
|
|
|
3
3
|
import { LANG_FLAG, bilingual, resolveLang } from '../oclif/i18n.js';
|
|
4
4
|
import { BaseCommand } from '../oclif/base-command.js';
|
|
5
5
|
import { tCli } from '../lib/i18n.js';
|
|
6
|
+
// 预置 .omk/.gitignore:测量 bulk + doctor --fix 备份(项目本地、不该入库)默认不入库;
|
|
7
|
+
// managed/ 治理档案 + 配置不在此列,默认 track。
|
|
8
|
+
const INIT_OMK_GITIGNORE = `# omk 测量 bulk + doctor --fix 备份(项目本地)——不入库;前导 / 锚定 .omk/ 顶层,不误伤嵌套同名目录。
|
|
9
|
+
/observe-health/
|
|
10
|
+
/doctors/
|
|
11
|
+
/observe-inbox/
|
|
12
|
+
/reports/
|
|
13
|
+
/backups/
|
|
14
|
+
`;
|
|
6
15
|
const INIT_SAMPLES = `[
|
|
7
16
|
{
|
|
8
17
|
"sample_id": "s001",
|
|
@@ -83,21 +92,21 @@ description: 多维度代码审查,覆盖安全 / 健壮 / 可维护 / 性能,
|
|
|
83
92
|
`;
|
|
84
93
|
export default class Init extends BaseCommand {
|
|
85
94
|
static description = bilingual({
|
|
86
|
-
zh: '
|
|
87
|
-
en: '
|
|
95
|
+
zh: '初始化一个 omk 项目:在目标目录铺好待测知识载体(skills/)与评测用例(eval-samples.json),供 omk eval / doctor / evolve / observe / list 操作。默认是两版 code-review skill 的 A/B 起步模板。',
|
|
96
|
+
en: 'Initialize an omk project: scaffold knowledge artifacts (skills/) and eval samples (eval-samples.json) in the target dir for omk eval / doctor / evolve / observe / list to work on. Ships a two-variant code-review A/B starter template by default.',
|
|
88
97
|
});
|
|
89
98
|
static examples = [
|
|
90
99
|
{
|
|
91
100
|
description: bilingual({
|
|
92
|
-
zh: '
|
|
93
|
-
en: '
|
|
101
|
+
zh: '在当前目录初始化一个 omk 项目',
|
|
102
|
+
en: 'Initialize an omk project in the current directory',
|
|
94
103
|
}),
|
|
95
104
|
command: '<%= config.bin %> init',
|
|
96
105
|
},
|
|
97
106
|
{
|
|
98
107
|
description: bilingual({
|
|
99
|
-
zh: '
|
|
100
|
-
en: '
|
|
108
|
+
zh: '在指定目录初始化一个 omk 项目',
|
|
109
|
+
en: 'Initialize an omk project in a specified directory',
|
|
101
110
|
}),
|
|
102
111
|
command: '<%= config.bin %> init my-project',
|
|
103
112
|
},
|
|
@@ -139,6 +148,9 @@ export default class Init extends BaseCommand {
|
|
|
139
148
|
writeFileSync(join(targetDir, 'eval-samples.json'), INIT_SAMPLES);
|
|
140
149
|
writeFileSync(join(targetDir, 'skills', 'code-review-v1', 'SKILL.md'), INIT_SKILL_V1);
|
|
141
150
|
writeFileSync(join(targetDir, 'skills', 'code-review-v2', 'SKILL.md'), INIT_SKILL_V2);
|
|
151
|
+
// 像 dvc init 那样预置忽略规则,开发者不会误把测量 bulk 提交进库。
|
|
152
|
+
mkdirSync(join(targetDir, '.omk'), { recursive: true });
|
|
153
|
+
writeFileSync(join(targetDir, '.omk', '.gitignore'), INIT_OMK_GITIGNORE);
|
|
142
154
|
console.log(tCli('cli.init.scaffolded', lang, { dir: targetDir }));
|
|
143
155
|
console.log('');
|
|
144
156
|
console.log(tCli('cli.init.next_steps_title', lang));
|
|
@@ -1,35 +1,12 @@
|
|
|
1
1
|
import { BaseCommand } from '../oclif/base-command.js';
|
|
2
|
-
import {
|
|
3
|
-
import type
|
|
2
|
+
import { sanitizeCell } from '../lib/cell-format.js';
|
|
3
|
+
import { type ManagedListRow } from '../../managed/index.js';
|
|
4
4
|
import type { CliLang } from '../lib/i18n.js';
|
|
5
|
-
/**
|
|
6
|
-
* 探测一条受管记录**当前源**的状态(三态,喂 buildManagedListRow 判 drift / 生命周期)。
|
|
7
|
-
* - 远端 git(带 url):源身份钉不可变 SHA(install 时 pin),内容恒定 → 直接取 record.contentHash,
|
|
8
|
-
* 不联网(list 是快读命令,不为 drift 检查发网络请求)。reachable。
|
|
9
|
-
* - 本地 git:locator `git:<ref>:<spec>` 复用 resolveInstallSource 在**仓库对象库**内重物化重哈
|
|
10
|
-
* (读取受 git 边界约束,无任意文件读 DoS)。解析不到(常因 `omk list` 的 cwd 与 install 时不同、
|
|
11
|
-
* spec 随 cwd 漂)→ 抛错 → **reachable:false(未核,不当 stale)**,避免对未改动的 skill 误报漂移。
|
|
12
|
-
* - 本地 file:locator 是绝对路径,但受管 JSON **用户可手改 / 随仓库分发**(无 install、无 opt-in 即被
|
|
13
|
-
* loadAllManagedRecords 读到)。只读命令绝不盲读任意路径:**拒软链、只读常规文件 / 真目录、单文件
|
|
14
|
-
* 设 size cap**(挡 `evil.md → /dev/zero` 这类 readFileSync 无界 DoS 与项目外任意读)。守卫不过 →
|
|
15
|
-
* reachable:false。目录-skill 整树哈本就跳软链(hashArtifactSource 用 isFile() 过滤)。
|
|
16
|
-
*/
|
|
17
|
-
export declare function probeSourceState(record: ManagedArtifactRecord): SourceProbe;
|
|
18
5
|
/** CJK 全角字符按 2 列计宽,使含中文表头的列也能对齐。 */
|
|
19
6
|
export declare function dispWidth(s: string): number;
|
|
20
7
|
/** 按**显示宽度**截断(不是 code unit):逐码点累加 dispWidth,绝不切断 surrogate 对、CJK 也不溢出列。 */
|
|
21
8
|
export declare function truncate(s: string, max: number): string;
|
|
22
|
-
|
|
23
|
-
* 否则会破坏表格甚至伪造终端输出。一律映射到可见 U+FFFD(--json 路径保留原值给脚本)。
|
|
24
|
-
* 用 Unicode **属性类**而非手列码点 —— 手列清单天然有缺口(BiDi、U+2028 / 2029、Tags 块都曾漏一轮补一轮),
|
|
25
|
-
* 属性类一次覆盖整类、新码点自动纳入:
|
|
26
|
-
* - `\p{Cc}` 控制符(C0 / C1 / DEL,含 ESC / 换行 / 回车 / TAB)→ 杀 ANSI / OSC 转义与终端控制;
|
|
27
|
-
* - `\p{Cf}` 格式符(BiDi 重排 / 隔离、零宽、joiners、BOM、Tags、interlinear)→ 防 Trojan-Source 视觉伪造与零宽隐藏 / 分割;
|
|
28
|
-
* - `\p{Zl}` / `\p{Zp}` 行 / 段分隔(U+2028 / 2029)→ LF 的 Unicode 孪生,防换行伪造表格行;
|
|
29
|
-
* - `\p{Mn}` / `\p{Me}` 非间距 / 封闭组合附加符 → 变可见,避免零前进宽度令 dispWidth 与终端列错位
|
|
30
|
-
* (间距组合符 `\p{Mc}` 合法占 1 列,保留);
|
|
31
|
-
* - Hangul filler(U+115F / U+1160 / U+3164,属 Lo 不在上述任何类)→ 零宽显示诡计,补列。 */
|
|
32
|
-
export declare function sanitizeCell(s: string): string;
|
|
9
|
+
export { sanitizeCell };
|
|
33
10
|
export declare function renderTable(rows: ManagedListRow[], lang: CliLang): string;
|
|
34
11
|
export default class List extends BaseCommand {
|
|
35
12
|
static description: string;
|
|
@@ -1,131 +1,9 @@
|
|
|
1
|
-
import { existsSync, lstatSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
1
|
import { Flags } from '@oclif/core';
|
|
4
2
|
import { LANG_FLAG, bilingual } from '../oclif/i18n.js';
|
|
5
3
|
import { BaseCommand } from '../oclif/base-command.js';
|
|
6
4
|
import { tCli } from '../lib/i18n.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
// 本地源读取成本上限。skill 是小体量 markdown(+少量 references 资产),远超这些边界的源必是手改 /
|
|
10
|
-
// 投毒,拒读避免 `omk list`(只读命令,读盘上可能随仓库分发的 locator)被 /dev/zero / 超大文件 / 巨型
|
|
11
|
-
// 目录递归拖垮(DoS)。
|
|
12
|
-
const MAX_FILE_SOURCE_BYTES = 8 * 1024 * 1024; // 单文件-skill 上限
|
|
13
|
-
const MAX_DIR_SOURCE_BYTES = 64 * 1024 * 1024; // 目录-skill 整树累计上限
|
|
14
|
-
const MAX_DIR_SOURCE_FILES = 4000; // 目录-skill 文件数上限
|
|
15
|
-
const MAX_DIR_SOURCE_DEPTH = 64; // 目录递归深度上限
|
|
16
|
-
/**
|
|
17
|
-
* 目录-skill 源的**有边界**整树哈:先恢复 resolveFileSource 的形态校验(SKILL.md 存在、是常规文件、非
|
|
18
|
-
* 软链 —— 否则 locator 可指向任意可读目录让 list 递归读整棵树),再 stat-walk(只 stat 不读)按与
|
|
19
|
-
* hashArtifactSource 同一 `isDistributablePath` 过滤累计 文件数 / 字节 / 深度,超界即返回 null(unreachable),
|
|
20
|
-
* 把单文件 DoS 不再换成目录递归 DoS。通过后才真正 hashArtifactSource。返回 null = 拒读 / 非法 skill 目录。
|
|
21
|
-
*/
|
|
22
|
-
function boundedDirSkillHash(abs) {
|
|
23
|
-
const skillMd = join(abs, 'SKILL.md');
|
|
24
|
-
let md;
|
|
25
|
-
try {
|
|
26
|
-
md = lstatSync(skillMd);
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return null; // 无 SKILL.md → 不是 skill 目录,拒
|
|
30
|
-
}
|
|
31
|
-
if (md.isSymbolicLink() || !md.isFile() || md.nlink !== 1)
|
|
32
|
-
return null; // SKILL.md 必须是常规、非软链、非硬链
|
|
33
|
-
let files = 0;
|
|
34
|
-
let bytes = 0;
|
|
35
|
-
const within = (dir, segs, depth) => {
|
|
36
|
-
if (depth > MAX_DIR_SOURCE_DEPTH)
|
|
37
|
-
return false;
|
|
38
|
-
let entries;
|
|
39
|
-
try {
|
|
40
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
for (const e of entries) {
|
|
46
|
-
const ns = [...segs, e.name];
|
|
47
|
-
if (!isDistributablePath(ns))
|
|
48
|
-
continue; // 与 hashArtifactSource 读取范围一致
|
|
49
|
-
if (e.isDirectory()) {
|
|
50
|
-
if (!within(join(dir, e.name), ns, depth + 1))
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
else if (e.isFile()) { // 软链 Dirent 既非 isFile 也非 isDirectory → 天然跳过
|
|
54
|
-
if (++files > MAX_DIR_SOURCE_FILES)
|
|
55
|
-
return false;
|
|
56
|
-
try {
|
|
57
|
-
const fst = lstatSync(join(dir, e.name));
|
|
58
|
-
if (fst.nlink !== 1)
|
|
59
|
-
return false; // 硬链(nlink>1)可在树内别名树外敏感 inode → 拒读整树
|
|
60
|
-
bytes += fst.size;
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
if (bytes > MAX_DIR_SOURCE_BYTES)
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return true;
|
|
70
|
-
};
|
|
71
|
-
if (!within(abs, [], 0))
|
|
72
|
-
return null;
|
|
73
|
-
return hashArtifactSource(abs, true);
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* 探测一条受管记录**当前源**的状态(三态,喂 buildManagedListRow 判 drift / 生命周期)。
|
|
77
|
-
* - 远端 git(带 url):源身份钉不可变 SHA(install 时 pin),内容恒定 → 直接取 record.contentHash,
|
|
78
|
-
* 不联网(list 是快读命令,不为 drift 检查发网络请求)。reachable。
|
|
79
|
-
* - 本地 git:locator `git:<ref>:<spec>` 复用 resolveInstallSource 在**仓库对象库**内重物化重哈
|
|
80
|
-
* (读取受 git 边界约束,无任意文件读 DoS)。解析不到(常因 `omk list` 的 cwd 与 install 时不同、
|
|
81
|
-
* spec 随 cwd 漂)→ 抛错 → **reachable:false(未核,不当 stale)**,避免对未改动的 skill 误报漂移。
|
|
82
|
-
* - 本地 file:locator 是绝对路径,但受管 JSON **用户可手改 / 随仓库分发**(无 install、无 opt-in 即被
|
|
83
|
-
* loadAllManagedRecords 读到)。只读命令绝不盲读任意路径:**拒软链、只读常规文件 / 真目录、单文件
|
|
84
|
-
* 设 size cap**(挡 `evil.md → /dev/zero` 这类 readFileSync 无界 DoS 与项目外任意读)。守卫不过 →
|
|
85
|
-
* reachable:false。目录-skill 整树哈本就跳软链(hashArtifactSource 用 isFile() 过滤)。
|
|
86
|
-
*/
|
|
87
|
-
export function probeSourceState(record) {
|
|
88
|
-
const s = record.source;
|
|
89
|
-
if (s.sourceKind === 'git' && s.url)
|
|
90
|
-
return { reachable: true, hash: record.contentHash };
|
|
91
|
-
try {
|
|
92
|
-
if (s.sourceKind === 'git') {
|
|
93
|
-
const resolved = resolveInstallSource(s.locator);
|
|
94
|
-
try {
|
|
95
|
-
return { reachable: true, hash: hashArtifactSource(resolved.localRoot, resolved.isDirectorySkill) };
|
|
96
|
-
}
|
|
97
|
-
finally {
|
|
98
|
-
resolved.cleanup();
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// 本地 file 源:守卫后再读。locator 必须是 install 实际写出的形态(绝对路径) —— 受管 JSON 随仓库
|
|
102
|
-
// 分发、无 opt-in 即被读到,相对 locator 不是 install 产物,拒,避免按 cwd 解析到项目外。
|
|
103
|
-
if (!isAbsolute(s.locator))
|
|
104
|
-
return { reachable: false };
|
|
105
|
-
const abs = resolve(s.locator);
|
|
106
|
-
if (!existsSync(abs))
|
|
107
|
-
return { reachable: false };
|
|
108
|
-
const st = lstatSync(abs); // lstat:不跟随软链 —— 软链直接拒(防 evil.md → /dev/zero)
|
|
109
|
-
if (st.isSymbolicLink())
|
|
110
|
-
return { reachable: false };
|
|
111
|
-
if (s.isDirectorySkill) {
|
|
112
|
-
if (!st.isDirectory())
|
|
113
|
-
return { reachable: false };
|
|
114
|
-
const hash = boundedDirSkillHash(abs); // 形态校验 + 成本边界(防任意目录递归读 / 目录 DoS)
|
|
115
|
-
return hash === null ? { reachable: false } : { reachable: true, hash };
|
|
116
|
-
}
|
|
117
|
-
// 单文件-skill:恢复 install(resolveFileSource)的形态约束 —— 必须是 `.md` 常规文件、非硬链、≤ size cap。
|
|
118
|
-
// 否则攻击者可写 locator:`/etc/passwd` / `~/.ssh/id_rsa`(非 .md 直接拒),或用 `.md` 命名的**硬链**别名
|
|
119
|
-
// 树外敏感文件绕过扩展名 / 软链守卫(lstat 分不出硬链)→ 诱 list 把任意本地文件读进进程参与 hash。install
|
|
120
|
-
// 写出的是 nlink=1 的全新副本,拒 nlink>1 不误伤合法源。非 install 形态一律 reachable:false。
|
|
121
|
-
if (!/\.md$/i.test(abs) || !st.isFile() || st.nlink !== 1 || st.size > MAX_FILE_SOURCE_BYTES)
|
|
122
|
-
return { reachable: false };
|
|
123
|
-
return { reachable: true, hash: hashArtifactSource(abs, false) };
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
return { reachable: false };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
5
|
+
import { sanitizeCell } from '../lib/cell-format.js';
|
|
6
|
+
import { globalManagedDir, listManagedRows, managedDir, resolveManagedDir, } from '../../managed/index.js';
|
|
129
7
|
/** CJK 全角字符按 2 列计宽,使含中文表头的列也能对齐。 */
|
|
130
8
|
export function dispWidth(s) {
|
|
131
9
|
let w = 0;
|
|
@@ -151,19 +29,9 @@ export function truncate(s, max) {
|
|
|
151
29
|
}
|
|
152
30
|
return `${out}…`;
|
|
153
31
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
* 属性类一次覆盖整类、新码点自动纳入:
|
|
158
|
-
* - `\p{Cc}` 控制符(C0 / C1 / DEL,含 ESC / 换行 / 回车 / TAB)→ 杀 ANSI / OSC 转义与终端控制;
|
|
159
|
-
* - `\p{Cf}` 格式符(BiDi 重排 / 隔离、零宽、joiners、BOM、Tags、interlinear)→ 防 Trojan-Source 视觉伪造与零宽隐藏 / 分割;
|
|
160
|
-
* - `\p{Zl}` / `\p{Zp}` 行 / 段分隔(U+2028 / 2029)→ LF 的 Unicode 孪生,防换行伪造表格行;
|
|
161
|
-
* - `\p{Mn}` / `\p{Me}` 非间距 / 封闭组合附加符 → 变可见,避免零前进宽度令 dispWidth 与终端列错位
|
|
162
|
-
* (间距组合符 `\p{Mc}` 合法占 1 列,保留);
|
|
163
|
-
* - Hangul filler(U+115F / U+1160 / U+3164,属 Lo 不在上述任何类)→ 零宽显示诡计,补列。 */
|
|
164
|
-
export function sanitizeCell(s) {
|
|
165
|
-
return s.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Mn}\p{Me}ᅟᅠㅤ]/gu, '�');
|
|
166
|
-
}
|
|
32
|
+
// sanitizeCell 已下沉到 ../lib/cell-format.js(promote 文本输出复用同一套不可信字符洗白)。
|
|
33
|
+
// 此处 re-export 保持既有 import 入口不破(test/cli/list-probe.test.ts 仍从本文件取 sanitizeCell)。
|
|
34
|
+
export { sanitizeCell };
|
|
167
35
|
export function renderTable(rows, lang) {
|
|
168
36
|
const headers = [
|
|
169
37
|
tCli('cli.list.col_name', lang),
|
|
@@ -176,8 +44,8 @@ export function renderTable(rows, lang) {
|
|
|
176
44
|
const cells = rows.map((r) => [
|
|
177
45
|
truncate(sanitizeCell(r.name), 40), // name 与 source 同为用户可控、可超长 → 同样按显示宽度截断,防撑爆表宽
|
|
178
46
|
r.kind, // ArtifactKind 枚举(validator 已收窄),无需洗
|
|
179
|
-
// 不可达 → 标「?」(drift 未核),绝不冒充 stale;reachable 且漂移才 stale
|
|
180
|
-
!r.reachable ? `${r.state} ?` : r.state === 'stale' ? 'stale ⚠️' : r.state,
|
|
47
|
+
// 不可达 → 标「?」(drift 未核),绝不冒充 stale;reachable 且漂移才 stale ⚠️;已人工接受标 promoted ✓。
|
|
48
|
+
!r.reachable ? `${r.state} ?` : r.state === 'stale' ? 'stale ⚠️' : r.state === 'promoted' ? 'promoted ✓' : r.state,
|
|
181
49
|
r.latestVerdict ? sanitizeCell(r.latestVerdict) : '—',
|
|
182
50
|
`${r.currentEvidenceCount}/${r.totalEvidenceCount}`,
|
|
183
51
|
truncate(sanitizeCell(r.sourceLabel), 48),
|
|
@@ -188,8 +56,8 @@ export function renderTable(rows, lang) {
|
|
|
188
56
|
}
|
|
189
57
|
export default class List extends BaseCommand {
|
|
190
58
|
static description = bilingual({
|
|
191
|
-
zh: '列出受管 skill 及其证据状态:生命周期(installed / measurable / stale)、最新 verdict、证据数、源。',
|
|
192
|
-
en: 'List managed skills with evidence status: lifecycle (installed / measurable / stale), latest verdict, evidence count, source.',
|
|
59
|
+
zh: '列出受管 skill 及其证据状态:生命周期(installed / measurable / promoted / stale)、最新 verdict、证据数、源。',
|
|
60
|
+
en: 'List managed skills with evidence status: lifecycle (installed / measurable / promoted / stale), latest verdict, evidence count, source.',
|
|
193
61
|
});
|
|
194
62
|
static examples = [
|
|
195
63
|
{ description: bilingual({ zh: '列出当前项目的受管 skill', en: 'List managed skills in the current project' }), command: '<%= config.bin %> list' },
|
|
@@ -210,8 +78,7 @@ export default class List extends BaseCommand {
|
|
|
210
78
|
const lang = this.lang;
|
|
211
79
|
await this.runWithCliExit(async () => {
|
|
212
80
|
const dir = flags.global ? globalManagedDir() : resolveManagedDir(managedDir());
|
|
213
|
-
const
|
|
214
|
-
const rows = buildManagedListRows(records, probeSourceState);
|
|
81
|
+
const rows = listManagedRows(dir);
|
|
215
82
|
if (flags.json) {
|
|
216
83
|
// 版本化信封 —— 与 eval / doctor / diagnosis 等机读出口一致(都带 schemaVersion),让未来字段增删 /
|
|
217
84
|
// 改名是可检测的版本 bump,而非脚本静默 break。rows 的逐行形态见 ManagedListRow。
|
|
@@ -231,8 +98,11 @@ export default class List extends BaseCommand {
|
|
|
231
98
|
// 注脚块前留一行空白(无论先触发哪条 note);空行挂在块上而非 drift 分支,避免只 unreachable 时丢分隔。
|
|
232
99
|
const hasDrift = rows.some((r) => r.drifted);
|
|
233
100
|
const hasUnreachable = rows.some((r) => !r.reachable);
|
|
234
|
-
|
|
101
|
+
const hasPromoted = rows.some((r) => r.state === 'promoted');
|
|
102
|
+
if (hasDrift || hasUnreachable || hasPromoted)
|
|
235
103
|
process.stderr.write('\n');
|
|
104
|
+
if (hasPromoted)
|
|
105
|
+
process.stderr.write(tCli('cli.list.promoted_note', lang));
|
|
236
106
|
if (hasDrift)
|
|
237
107
|
process.stderr.write(tCli('cli.list.drift_note', lang));
|
|
238
108
|
if (hasUnreachable)
|
|
@@ -7,6 +7,7 @@ export default class ObserveInbox 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
|
skill: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
limit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
explore: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -20,8 +20,11 @@ function pickSkillToolCounts(value, skillName) {
|
|
|
20
20
|
}
|
|
21
21
|
// runObserveInbox export:test/cli/observe.test.ts in-process import 验证 by-skill 聚合行为。
|
|
22
22
|
export async function runObserveInbox(_args, flags, lang) {
|
|
23
|
-
const { queryObservationInbox, selectExploreInboxItems, loadLatestObservationInboxReports, summarizeObservationInboxBySkill, DEFAULT_OBSERVATIONS_DIR } = await import('../../../observability/inbox.js');
|
|
24
|
-
|
|
23
|
+
const { queryObservationInbox, selectExploreInboxItems, loadLatestObservationInboxReports, summarizeObservationInboxBySkill, DEFAULT_OBSERVATIONS_DIR, DEFAULT_GLOBAL_OBSERVATIONS_DIR } = await import('../../../observability/inbox.js');
|
|
24
|
+
// 显式 --input-dir 最高;否则 --global 直读全局、默认读项目(空则 loadObservationInboxReports 兜底全局)。
|
|
25
|
+
const dir = flags['input-dir']
|
|
26
|
+
? resolve(flags['input-dir'])
|
|
27
|
+
: (flags.global ? DEFAULT_GLOBAL_OBSERVATIONS_DIR : DEFAULT_OBSERVATIONS_DIR);
|
|
25
28
|
if (flags['llm-enhanced-review']) {
|
|
26
29
|
const { buildObservationInboxViewModel } = await import('../../../observability/inbox-view-model.js');
|
|
27
30
|
const { extractSkillSoftStandards, DEFAULT_LLM_ENHANCED_REVIEW_MODEL } = await import('../../../observability/soft-standards/index.js');
|
|
@@ -186,10 +189,17 @@ export default class ObserveInbox extends BaseCommand {
|
|
|
186
189
|
lang: LANG_FLAG,
|
|
187
190
|
'input-dir': Flags.string({
|
|
188
191
|
description: bilingual({
|
|
189
|
-
zh: 'inbox 数据目录,默认 .omk/
|
|
190
|
-
en: 'Inbox data dir, default .omk/
|
|
192
|
+
zh: 'inbox 数据目录,默认 .omk/observe-inbox(项目级,相对于 cwd);目录不存在时兜底读 ~/.oh-my-knowledge/observe-inbox。',
|
|
193
|
+
en: 'Inbox data dir, default .omk/observe-inbox (project-local); falls back to ~/.oh-my-knowledge/observe-inbox when missing.',
|
|
191
194
|
}),
|
|
192
195
|
}),
|
|
196
|
+
global: Flags.boolean({
|
|
197
|
+
description: bilingual({
|
|
198
|
+
zh: '直接读取全局 ~/.oh-my-knowledge/observe-inbox(跳过项目级与兜底)。',
|
|
199
|
+
en: 'Read directly from global ~/.oh-my-knowledge/observe-inbox (skip project-local and fallback).',
|
|
200
|
+
}),
|
|
201
|
+
default: false,
|
|
202
|
+
}),
|
|
193
203
|
skill: Flags.string({
|
|
194
204
|
description: bilingual({ zh: '只看指定 skill', en: 'Filter to specific skill' }),
|
|
195
205
|
}),
|