oh-my-knowledge 0.36.0 → 0.37.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 +5 -3
- package/README.zh.md +5 -3
- package/dist/assets/agent-skills/omk/SKILL.md +4 -2
- package/dist/assets/agent-skills/omk/references/commands.md +86 -3
- package/dist/cli/commands/doctor.js +19 -4
- package/dist/cli/commands/evolve.d.ts +11 -0
- package/dist/cli/commands/evolve.js +105 -5
- package/dist/cli/commands/list.d.ts +5 -26
- package/dist/cli/commands/list.js +17 -142
- package/dist/cli/commands/promote.d.ts +24 -0
- package/dist/cli/commands/promote.js +158 -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 +10 -4
- 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 +1 -0
- package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/common.js +8 -0
- 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/progress.d.ts +3 -0
- package/dist/cli/lib/progress.js +22 -0
- package/dist/cli/lib/source-probe.d.ts +15 -0
- package/dist/cli/lib/source-probe.js +129 -0
- 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 +8 -1
- package/dist/executors/codex-cli.js +5 -1
- package/dist/managed/index.d.ts +1 -0
- package/dist/managed/index.js +1 -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/store.d.ts +25 -2
- package/dist/managed/store.js +92 -3
- 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 +99 -99
- 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/observation-inbox/styles.d.ts +1 -1
- package/dist/renderer/observation-inbox/styles.js +53 -53
- package/dist/renderer/report-shell.d.ts +77 -0
- package/dist/renderer/report-shell.js +223 -0
- package/dist/renderer/skill-detail-renderer.js +147 -352
- package/dist/renderer/skill-health-renderer.js +50 -73
- package/dist/renderer/skill-list-renderer.js +358 -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/report-server.js +142 -28
- package/dist/types/doctor.d.ts +22 -0
- package/dist/types/managed.d.ts +17 -3
- package/package.json +1 -1
|
@@ -1,131 +1,13 @@
|
|
|
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
|
-
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
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 { probeSourceState } from '../lib/source-probe.js';
|
|
6
|
+
import { sanitizeCell } from '../lib/cell-format.js';
|
|
7
|
+
import { buildManagedListRows, globalManagedDir, loadAllManagedRecords, managedDir, resolveManagedDir, } from '../../managed/index.js';
|
|
8
|
+
// 源探测(含 DoS / 软硬链 / 投毒守卫)已抽到 ../lib/source-probe.js,promote 复用同一套守卫与 drift 判定。
|
|
9
|
+
// 此处 re-export 保持既有 import 入口不破(test/cli/list-probe.test.ts 仍从本文件取 probeSourceState)。
|
|
10
|
+
export { probeSourceState };
|
|
129
11
|
/** CJK 全角字符按 2 列计宽,使含中文表头的列也能对齐。 */
|
|
130
12
|
export function dispWidth(s) {
|
|
131
13
|
let w = 0;
|
|
@@ -151,19 +33,9 @@ export function truncate(s, max) {
|
|
|
151
33
|
}
|
|
152
34
|
return `${out}…`;
|
|
153
35
|
}
|
|
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
|
-
}
|
|
36
|
+
// sanitizeCell 已下沉到 ../lib/cell-format.js(promote 文本输出复用同一套不可信字符洗白)。
|
|
37
|
+
// 此处 re-export 保持既有 import 入口不破(test/cli/list-probe.test.ts 仍从本文件取 sanitizeCell)。
|
|
38
|
+
export { sanitizeCell };
|
|
167
39
|
export function renderTable(rows, lang) {
|
|
168
40
|
const headers = [
|
|
169
41
|
tCli('cli.list.col_name', lang),
|
|
@@ -176,8 +48,8 @@ export function renderTable(rows, lang) {
|
|
|
176
48
|
const cells = rows.map((r) => [
|
|
177
49
|
truncate(sanitizeCell(r.name), 40), // name 与 source 同为用户可控、可超长 → 同样按显示宽度截断,防撑爆表宽
|
|
178
50
|
r.kind, // ArtifactKind 枚举(validator 已收窄),无需洗
|
|
179
|
-
// 不可达 → 标「?」(drift 未核),绝不冒充 stale;reachable 且漂移才 stale
|
|
180
|
-
!r.reachable ? `${r.state} ?` : r.state === 'stale' ? 'stale ⚠️' : r.state,
|
|
51
|
+
// 不可达 → 标「?」(drift 未核),绝不冒充 stale;reachable 且漂移才 stale ⚠️;已人工接受标 promoted ✓。
|
|
52
|
+
!r.reachable ? `${r.state} ?` : r.state === 'stale' ? 'stale ⚠️' : r.state === 'promoted' ? 'promoted ✓' : r.state,
|
|
181
53
|
r.latestVerdict ? sanitizeCell(r.latestVerdict) : '—',
|
|
182
54
|
`${r.currentEvidenceCount}/${r.totalEvidenceCount}`,
|
|
183
55
|
truncate(sanitizeCell(r.sourceLabel), 48),
|
|
@@ -188,8 +60,8 @@ export function renderTable(rows, lang) {
|
|
|
188
60
|
}
|
|
189
61
|
export default class List extends BaseCommand {
|
|
190
62
|
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.',
|
|
63
|
+
zh: '列出受管 skill 及其证据状态:生命周期(installed / measurable / promoted / stale)、最新 verdict、证据数、源。',
|
|
64
|
+
en: 'List managed skills with evidence status: lifecycle (installed / measurable / promoted / stale), latest verdict, evidence count, source.',
|
|
193
65
|
});
|
|
194
66
|
static examples = [
|
|
195
67
|
{ description: bilingual({ zh: '列出当前项目的受管 skill', en: 'List managed skills in the current project' }), command: '<%= config.bin %> list' },
|
|
@@ -231,8 +103,11 @@ export default class List extends BaseCommand {
|
|
|
231
103
|
// 注脚块前留一行空白(无论先触发哪条 note);空行挂在块上而非 drift 分支,避免只 unreachable 时丢分隔。
|
|
232
104
|
const hasDrift = rows.some((r) => r.drifted);
|
|
233
105
|
const hasUnreachable = rows.some((r) => !r.reachable);
|
|
234
|
-
|
|
106
|
+
const hasPromoted = rows.some((r) => r.state === 'promoted');
|
|
107
|
+
if (hasDrift || hasUnreachable || hasPromoted)
|
|
235
108
|
process.stderr.write('\n');
|
|
109
|
+
if (hasPromoted)
|
|
110
|
+
process.stderr.write(tCli('cli.list.promoted_note', lang));
|
|
236
111
|
if (hasDrift)
|
|
237
112
|
process.stderr.write(tCli('cli.list.drift_note', lang));
|
|
238
113
|
if (hasUnreachable)
|
|
@@ -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,158 @@
|
|
|
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 { probeSourceState } from '../lib/source-probe.js';
|
|
8
|
+
import { sanitizeCell } from '../lib/cell-format.js';
|
|
9
|
+
import { getJudgePromptHash } from '../../grading/judge.js';
|
|
10
|
+
import { appendManagedDecision, evaluatePromoteGate, globalManagedDir, isCurrentlyPromoted, loadManagedRecord, managedDir, managedRecordId, resolveManagedDir, } from '../../managed/index.js';
|
|
11
|
+
const SUPPORTED_KINDS = ['skill'];
|
|
12
|
+
/** 决定的 actor:--actor > git config user.name > $USER / $LOGNAME > unknown。git 参数无用户输入,安全。 */
|
|
13
|
+
function resolveActor(flagActor) {
|
|
14
|
+
if (flagActor && flagActor.trim())
|
|
15
|
+
return flagActor.trim();
|
|
16
|
+
try {
|
|
17
|
+
const name = execFileSync('git', ['config', 'user.name'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
18
|
+
if (name)
|
|
19
|
+
return name;
|
|
20
|
+
}
|
|
21
|
+
catch { /* git 缺失 / 无配置 → 回退环境 */ }
|
|
22
|
+
return process.env.USER || process.env.LOGNAME || 'unknown';
|
|
23
|
+
}
|
|
24
|
+
/** 洗 detail 里来自受管 JSON 的不可信值(verdict / judgePromptHash),文本输出防 ANSI/OSC 注入。 */
|
|
25
|
+
function safeDetail(detail) {
|
|
26
|
+
if (!detail)
|
|
27
|
+
return undefined;
|
|
28
|
+
return Object.fromEntries(Object.entries(detail).map(([k, v]) => [k, sanitizeCell(v)]));
|
|
29
|
+
}
|
|
30
|
+
/** blockKind → i18n key(细节 detail 作为 tCli params,文本路径经 safeDetail 洗白)。 */
|
|
31
|
+
const BLOCK_KEY = {
|
|
32
|
+
drifted: 'cli.promote.drifted',
|
|
33
|
+
no_current_evidence: 'cli.promote.no_current_evidence',
|
|
34
|
+
incomparable: 'cli.promote.incomparable',
|
|
35
|
+
verdict_blocked: 'cli.promote.verdict_blocked',
|
|
36
|
+
};
|
|
37
|
+
export default class Promote extends BaseCommand {
|
|
38
|
+
static description = bilingual({
|
|
39
|
+
zh: '把受管 skill 的当前版本按证据门禁「接受」为 promoted:默认仅放行 verdict=PROGRESS,在记录里追加一条带证据指针的人工决定。',
|
|
40
|
+
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.',
|
|
41
|
+
});
|
|
42
|
+
static examples = [
|
|
43
|
+
{ description: bilingual({ zh: 'promote 一个证据达标的 skill', en: 'Promote a skill whose evidence passes the gate' }), command: '<%= config.bin %> promote review' },
|
|
44
|
+
{ description: bilingual({ zh: '接受 CAUTIOUS 结果(显式放宽门禁)', en: 'Accept a CAUTIOUS result (explicitly loosen the gate)' }), command: '<%= config.bin %> promote review --accept-cautious' },
|
|
45
|
+
{ description: bilingual({ zh: '越门 promote 并记录理由(人工 override)', en: 'Override the gate and record a reason (human override)' }), command: '<%= config.bin %> promote review --force --reason "已人工复核"' },
|
|
46
|
+
];
|
|
47
|
+
static args = {
|
|
48
|
+
name: Args.string({ description: bilingual({ zh: '受管 skill 名(omk list 里的 NAME)', en: 'Managed skill name (the NAME shown by omk list)' }), required: true }),
|
|
49
|
+
};
|
|
50
|
+
static flags = {
|
|
51
|
+
lang: LANG_FLAG,
|
|
52
|
+
kind: Flags.string({ description: bilingual({ zh: 'artifact 类型(当前仅 skill)', en: 'artifact kind (only skill today)' }), default: 'skill' }),
|
|
53
|
+
'accept-cautious': Flags.boolean({ description: bilingual({ zh: '把 CAUTIOUS 也算可接受(默认仅 PROGRESS)', en: 'also accept CAUTIOUS (default PROGRESS only)' }) }),
|
|
54
|
+
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)' }) }),
|
|
55
|
+
reason: Flags.string({ description: bilingual({ zh: 'promote / 越门的理由(写入决定)', en: 'reason for the promotion / override (stored on the decision)' }) }),
|
|
56
|
+
actor: Flags.string({ description: bilingual({ zh: '决定的 actor(默认取 git config user.name)', en: 'decision actor (defaults to git config user.name)' }) }),
|
|
57
|
+
global: Flags.boolean({ description: bilingual({ zh: '操作全局受管目录而非项目 .omk/managed', en: 'operate on the global managed dir instead of project .omk/managed' }) }),
|
|
58
|
+
json: Flags.boolean({ description: bilingual({ zh: '输出 JSON(版本化信封)供脚本消费', en: 'output JSON (versioned envelope) for scripts' }) }),
|
|
59
|
+
};
|
|
60
|
+
async run() {
|
|
61
|
+
const { args, flags } = await this.parse(Promote);
|
|
62
|
+
const lang = this.lang;
|
|
63
|
+
await this.runWithCliExit(async () => {
|
|
64
|
+
const name = args.name;
|
|
65
|
+
const kind = flags.kind;
|
|
66
|
+
if (!SUPPORTED_KINDS.includes(kind)) {
|
|
67
|
+
process.stderr.write(tCli('cli.promote.kind_unsupported', lang, { kind: sanitizeCell(String(kind)) }) + '\n');
|
|
68
|
+
throw new CliExit(1);
|
|
69
|
+
}
|
|
70
|
+
const dir = flags.global ? globalManagedDir() : resolveManagedDir(managedDir());
|
|
71
|
+
const record = loadManagedRecord(dir, managedRecordId(kind, name));
|
|
72
|
+
if (!record) {
|
|
73
|
+
process.stderr.write(tCli('cli.promote.not_managed', lang, { name: sanitizeCell(name), kind }) + '\n');
|
|
74
|
+
throw new CliExit(1);
|
|
75
|
+
}
|
|
76
|
+
const probe = probeSourceState(record);
|
|
77
|
+
const currentContentHash = probe.reachable ? probe.hash : undefined;
|
|
78
|
+
const sourceHashMismatch = probe.reachable && currentContentHash !== record.contentHash;
|
|
79
|
+
// 幂等:当前内容已 promote 过 → 无操作成功退出,不堆冗余事件。与 list 的不可达分支同口径:
|
|
80
|
+
// 源不可达不代表内容已变;只有源可达且 hash 已变时,才不把旧 promote 当作当前 no-op。
|
|
81
|
+
const alreadyPromoted = !sourceHashMismatch && isCurrentlyPromoted(record);
|
|
82
|
+
if (alreadyPromoted) {
|
|
83
|
+
if (flags.json)
|
|
84
|
+
this.log(JSON.stringify({ schemaVersion: 1, alreadyPromoted: { name, contentHash: record.contentHash } }, null, 2));
|
|
85
|
+
else
|
|
86
|
+
process.stderr.write(tCli('cli.promote.already_promoted', lang, { name: sanitizeCell(name) }) + '\n');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const currentJudgeHashes = new Set([getJudgePromptHash(true), getJudgePromptHash(false)]);
|
|
90
|
+
const gate = evaluatePromoteGate({ record, currentContentHash, acceptCautious: flags['accept-cautious'], currentJudgeHashes });
|
|
91
|
+
// 拦截:无当前证据 force 也越不过;源可达但 hash 已变也不可越门,因为 decision 仍锚在
|
|
92
|
+
// record.contentHash,越过去后 list 仍会按当前源 hash 判 stale。源不可达则可作为人工未核越门。
|
|
93
|
+
const forceable = gate.evidence !== undefined && !sourceHashMismatch;
|
|
94
|
+
const reason = flags.reason?.trim();
|
|
95
|
+
if (!gate.ok && flags.force && forceable && !reason) {
|
|
96
|
+
if (flags.json)
|
|
97
|
+
this.log(JSON.stringify({ schemaVersion: 1, blocked: { name, reasons: [{ blockKind: 'force_reason_required' }] } }, null, 2));
|
|
98
|
+
else
|
|
99
|
+
process.stderr.write(tCli('cli.promote.force_reason_required', lang) + '\n');
|
|
100
|
+
throw new CliExit(1);
|
|
101
|
+
}
|
|
102
|
+
if (!gate.ok && !(flags.force && forceable)) {
|
|
103
|
+
this.emitBlocked(name, gate, lang, flags.json, forceable);
|
|
104
|
+
throw new CliExit(1);
|
|
105
|
+
}
|
|
106
|
+
// 通过 或 force 越门:写决定。
|
|
107
|
+
const overridden = !gate.ok; // 走到这里且未提前 return,说明是 force 越门
|
|
108
|
+
const evidence = gate.evidence; // forceable 已保证非空(无证据分支在上面已拦)
|
|
109
|
+
const decision = {
|
|
110
|
+
decisionKind: 'promote',
|
|
111
|
+
actor: resolveActor(flags.actor),
|
|
112
|
+
decidedAt: new Date().toISOString(),
|
|
113
|
+
contentHash: record.contentHash,
|
|
114
|
+
...(evidence.reportId ? { reportId: evidence.reportId } : {}),
|
|
115
|
+
...(reason ? { reason } : {}),
|
|
116
|
+
...(overridden ? { override: { verdict: evidence.verdict ?? 'UNKNOWN', overriddenBlocks: gate.blocked.map((b) => b.blockKind) } } : {}),
|
|
117
|
+
};
|
|
118
|
+
appendManagedDecision(dir, record.id, decision);
|
|
119
|
+
// 非拦截级提醒(如 judgePromptHash 缺失)始终提示。当前 warnings 仅 incomparable_unverified 一类。
|
|
120
|
+
if (gate.warnings.length > 0)
|
|
121
|
+
process.stderr.write(tCli('cli.promote.incomparable_unverified', lang) + '\n');
|
|
122
|
+
this.emitPromoted(name, record, evidence.verdict, evidence.reportId, overridden, lang, flags.json);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
emitBlocked(name, gate, lang, json, forceable) {
|
|
126
|
+
if (json) {
|
|
127
|
+
this.log(JSON.stringify({ schemaVersion: 1, blocked: { name, reasons: gate.blocked.map((b) => ({ blockKind: b.blockKind, ...(b.detail ? { detail: b.detail } : {}) })) } }, null, 2));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
process.stderr.write(tCli('cli.promote.blocked_header', lang, { name: sanitizeCell(name) }) + '\n');
|
|
131
|
+
// detail 里的 verdict / judgePromptHash 来自不可信受管 JSON,文本路径先洗(与 list 同口径,防 ANSI/OSC 注入)。
|
|
132
|
+
for (const b of gate.blocked)
|
|
133
|
+
process.stderr.write(tCli(BLOCK_KEY[b.blockKind], lang, safeDetail(b.detail)) + '\n');
|
|
134
|
+
if (gate.warnings.length > 0)
|
|
135
|
+
process.stderr.write(tCli('cli.promote.incomparable_unverified', lang) + '\n');
|
|
136
|
+
// 有可锚定证据(非「无证据」终态)才提示 --force 可越门。
|
|
137
|
+
if (forceable)
|
|
138
|
+
process.stderr.write(tCli('cli.promote.force_hint', lang) + '\n');
|
|
139
|
+
}
|
|
140
|
+
emitPromoted(name, record, verdict, reportId, overridden, lang, json) {
|
|
141
|
+
const hash = record.contentHash;
|
|
142
|
+
if (json) {
|
|
143
|
+
this.log(JSON.stringify({
|
|
144
|
+
schemaVersion: 1,
|
|
145
|
+
promoted: { name, kind: record.kind, contentHash: hash, ...(reportId ? { reportId } : {}), verdict: verdict ?? null, override: overridden },
|
|
146
|
+
}, null, 2));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// hash / verdict / reportId 来自不可信受管 JSON,文本路径先洗(--json 路径上面已 return、保留原值)。
|
|
150
|
+
const short = sanitizeCell(hash.slice(0, 12));
|
|
151
|
+
const safeVerdict = sanitizeCell(verdict ?? 'UNKNOWN');
|
|
152
|
+
const safeName = sanitizeCell(name);
|
|
153
|
+
if (overridden)
|
|
154
|
+
this.log(tCli('cli.promote.forced_override', lang, { name: safeName, hash: short, verdict: safeVerdict }));
|
|
155
|
+
else
|
|
156
|
+
this.log(tCli('cli.promote.promoted', lang, { name: safeName, hash: short, verdict: safeVerdict, reportId: sanitizeCell(reportId ?? '—') }));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -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>;
|
|
@@ -225,7 +225,7 @@ async function runSampleFix(args, flags, lang) {
|
|
|
225
225
|
}
|
|
226
226
|
process.stderr.write(lang === 'zh' ? `🔧 发现 ${sampleDesignCount} 条 sample_design 失败,开始修复...\n` : `🔧 Found ${sampleDesignCount} sample_design failure(s), fixing...\n`);
|
|
227
227
|
const { createExecutor } = await import('../../executors/index.js');
|
|
228
|
-
const exec = createExecutor('claude');
|
|
228
|
+
const exec = createExecutor(flags.executor ?? 'claude');
|
|
229
229
|
const executorFn = async (opts) => {
|
|
230
230
|
const result = await exec({
|
|
231
231
|
model: opts.model,
|
|
@@ -298,7 +298,7 @@ async function runSampleFromTraces(flags, lang) {
|
|
|
298
298
|
? `🔭 发现 ${items.length} 个失败信号,正在生成回归用例草稿...\n`
|
|
299
299
|
: `🔭 Found ${items.length} failure signal(s); generating regression-sample drafts...\n`);
|
|
300
300
|
try {
|
|
301
|
-
const { samples, costUSD } = await generateSamplesFromTraces({ items, count, model: flags.model });
|
|
301
|
+
const { samples, costUSD } = await generateSamplesFromTraces({ items, count, model: flags.model, executorName: flags.executor });
|
|
302
302
|
const cost = costUSD > 0 ? ` $${costUSD.toFixed(4)}` : '';
|
|
303
303
|
if (samples.length === 0) {
|
|
304
304
|
// The model conservatively skipped every signal (noise / unreproducible). That's a
|
|
@@ -380,7 +380,7 @@ async function runSample(args, flags, lang) {
|
|
|
380
380
|
}
|
|
381
381
|
try {
|
|
382
382
|
const skillContent = readFileSync(skillPath, 'utf-8');
|
|
383
|
-
const { samples, costUSD } = await generateSamples({ skillContent, count, model, focus, noMock: flags['no-mock'] });
|
|
383
|
+
const { samples, costUSD } = await generateSamples({ skillContent, count, model, focus, noMock: flags['no-mock'], executorName: flags.executor });
|
|
384
384
|
mkdirSync(dirname(samplesPath), { recursive: true });
|
|
385
385
|
writeFileSync(samplesPath, JSON.stringify(samples, null, 2));
|
|
386
386
|
const cost = costUSD > 0 ? ` $${costUSD.toFixed(4)}` : '';
|
|
@@ -444,7 +444,7 @@ async function runSample(args, flags, lang) {
|
|
|
444
444
|
process.stderr.write(tCli('cli.gen.single_generating_auto', lang));
|
|
445
445
|
}
|
|
446
446
|
try {
|
|
447
|
-
const { samples, costUSD } = await generateSamples({ skillContent, count, model, focus, noMock: flags['no-mock'] });
|
|
447
|
+
const { samples, costUSD } = await generateSamples({ skillContent, count, model, focus, noMock: flags['no-mock'], executorName: flags.executor });
|
|
448
448
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
449
449
|
writeFileSync(outputPath, JSON.stringify(samples, null, 2));
|
|
450
450
|
const cost = costUSD > 0 ? ` $${costUSD.toFixed(4)}` : '';
|
|
@@ -528,6 +528,12 @@ export default class Sample extends BaseCommand {
|
|
|
528
528
|
}),
|
|
529
529
|
default: 'sonnet',
|
|
530
530
|
}),
|
|
531
|
+
executor: Flags.string({
|
|
532
|
+
description: bilingual({
|
|
533
|
+
zh: '执行器名,默认 claude(同 omk eval / doctor / evolve)。指定 codex 等其它执行器时,记得连带传一个该执行器能识别的 --model。',
|
|
534
|
+
en: 'Executor name, default claude (same as omk eval / doctor / evolve). When using another executor like codex, also pass a --model it recognizes.',
|
|
535
|
+
}),
|
|
536
|
+
}),
|
|
531
537
|
'skill-dir': Flags.string({
|
|
532
538
|
description: bilingual({
|
|
533
539
|
zh: 'skill 根目录,默认 skills。batch 模式扫此目录。',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 终端展示前的不可信字符洗白——`omk list` 与 `omk promote` 共用。受管 JSON 可随仓库分发、用户可手改
|
|
3
|
+
* (store.ts validator 只收窄到「是字符串」,不卡内容),把它的字段(name / verdict / judgePromptHash /
|
|
4
|
+
* reportId / sourceLabel 等)塞进终端输出前必须先洗,否则 ANSI / OSC 转义、BiDi 重排、零宽分割会破坏排版
|
|
5
|
+
* 甚至伪造输出(清屏、改窗口标题、覆盖历史行误导审计)。一律映射到可见 U+FFFD;`--json` 路径保留原值
|
|
6
|
+
* (JSON.stringify 自带控制符转义,脚本消费要的是原始值)。
|
|
7
|
+
*
|
|
8
|
+
* 用 Unicode **属性类**而非手列码点 —— 手列清单天然有缺口(BiDi、U+2028 / 2029、Tags 块都曾漏一轮补一轮),
|
|
9
|
+
* 属性类一次覆盖整类、新码点自动纳入:
|
|
10
|
+
* - `\p{Cc}` 控制符(C0 / C1 / DEL,含 ESC / 换行 / 回车 / TAB)→ 杀 ANSI / OSC 转义与终端控制;
|
|
11
|
+
* - `\p{Cf}` 格式符(BiDi 重排 / 隔离、零宽、joiners、BOM、Tags、interlinear)→ 防 Trojan-Source 视觉伪造与零宽隐藏 / 分割;
|
|
12
|
+
* - `\p{Zl}` / `\p{Zp}` 行 / 段分隔(U+2028 / 2029)→ LF 的 Unicode 孪生,防换行伪造;
|
|
13
|
+
* - `\p{Mn}` / `\p{Me}` 非间距 / 封闭组合附加符 → 变可见,避免零前进宽度令列宽与终端错位
|
|
14
|
+
* (间距组合符 `\p{Mc}` 合法占 1 列,保留);
|
|
15
|
+
* - Hangul filler(U+115F / U+1160 / U+3164,属 Lo 不在上述任何类)→ 零宽显示诡计,补列。
|
|
16
|
+
*/
|
|
17
|
+
export declare function sanitizeCell(s: string): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 终端展示前的不可信字符洗白——`omk list` 与 `omk promote` 共用。受管 JSON 可随仓库分发、用户可手改
|
|
3
|
+
* (store.ts validator 只收窄到「是字符串」,不卡内容),把它的字段(name / verdict / judgePromptHash /
|
|
4
|
+
* reportId / sourceLabel 等)塞进终端输出前必须先洗,否则 ANSI / OSC 转义、BiDi 重排、零宽分割会破坏排版
|
|
5
|
+
* 甚至伪造输出(清屏、改窗口标题、覆盖历史行误导审计)。一律映射到可见 U+FFFD;`--json` 路径保留原值
|
|
6
|
+
* (JSON.stringify 自带控制符转义,脚本消费要的是原始值)。
|
|
7
|
+
*
|
|
8
|
+
* 用 Unicode **属性类**而非手列码点 —— 手列清单天然有缺口(BiDi、U+2028 / 2029、Tags 块都曾漏一轮补一轮),
|
|
9
|
+
* 属性类一次覆盖整类、新码点自动纳入:
|
|
10
|
+
* - `\p{Cc}` 控制符(C0 / C1 / DEL,含 ESC / 换行 / 回车 / TAB)→ 杀 ANSI / OSC 转义与终端控制;
|
|
11
|
+
* - `\p{Cf}` 格式符(BiDi 重排 / 隔离、零宽、joiners、BOM、Tags、interlinear)→ 防 Trojan-Source 视觉伪造与零宽隐藏 / 分割;
|
|
12
|
+
* - `\p{Zl}` / `\p{Zp}` 行 / 段分隔(U+2028 / 2029)→ LF 的 Unicode 孪生,防换行伪造;
|
|
13
|
+
* - `\p{Mn}` / `\p{Me}` 非间距 / 封闭组合附加符 → 变可见,避免零前进宽度令列宽与终端错位
|
|
14
|
+
* (间距组合符 `\p{Mc}` 合法占 1 列,保留);
|
|
15
|
+
* - Hangul filler(U+115F / U+1160 / U+3164,属 Lo 不在上述任何类)→ 零宽显示诡计,补列。
|
|
16
|
+
*/
|
|
17
|
+
export function sanitizeCell(s) {
|
|
18
|
+
return s.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Mn}\p{Me}ᅟᅠㅤ]/gu, '�');
|
|
19
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { CliMessage } from './types.js';
|
|
2
|
-
export type CommonMessageKey = 'cli.common.unknown_domain' | 'cli.common.error_prefix' | 'cli.common.skill_dir_not_found' | 'cli.common.skill_file_not_found' | 'cli.common.skill_dir_no_skill_md' | 'cli.common.report_not_found' | 'cli.common.no_judge_model' | 'cli.common.judge_models_single_only' | 'cli.common.warn_load_samples_failed' | 'cli.update.new_version_available' | 'cli.update.box_title' | 'cli.update.box_version_line' | 'cli.update.box_upgrade_line' | 'cli.update.box_silence_line' | 'cli.observe.view_hint' | 'cli.studio.started' | 'cli.studio.stop_hint' | 'cli.studio.open_failed' | 'cli.doctor.no_skill_found' | 'cli.doctor.samples_detected';
|
|
2
|
+
export type CommonMessageKey = 'cli.common.unknown_domain' | 'cli.common.error_prefix' | 'cli.common.skill_dir_not_found' | 'cli.common.skill_file_not_found' | 'cli.common.skill_dir_no_skill_md' | 'cli.common.report_not_found' | 'cli.common.no_judge_model' | 'cli.common.judge_models_single_only' | 'cli.common.warn_load_samples_failed' | 'cli.update.new_version_available' | 'cli.update.box_title' | 'cli.update.box_version_line' | 'cli.update.box_upgrade_line' | 'cli.update.box_silence_line' | 'cli.observe.view_hint' | 'cli.studio.started' | 'cli.studio.stop_hint' | 'cli.studio.open_failed' | 'cli.doctor.no_skill_found' | 'cli.doctor.samples_detected' | 'cli.doctor.progress_skill_start' | 'cli.doctor.progress_skill_done';
|
|
3
3
|
export declare const commonDict: Record<CommonMessageKey, CliMessage>;
|
|
@@ -79,4 +79,12 @@ export const commonDict = {
|
|
|
79
79
|
zh: '✓ 使用评测用例文件:{path}',
|
|
80
80
|
en: '✓ Using eval samples file: {path}',
|
|
81
81
|
},
|
|
82
|
+
'cli.doctor.progress_skill_start': {
|
|
83
|
+
zh: '{prefix}{skill} ⏳ 体检中...\n',
|
|
84
|
+
en: '{prefix}{skill} ⏳ checking...\n',
|
|
85
|
+
},
|
|
86
|
+
'cli.doctor.progress_skill_done': {
|
|
87
|
+
zh: '{prefix}{skill} {result}({ms}ms)\n',
|
|
88
|
+
en: '{prefix}{skill} {result} ({ms}ms)\n',
|
|
89
|
+
},
|
|
82
90
|
};
|