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.
Files changed (67) hide show
  1. package/README.md +5 -3
  2. package/README.zh.md +5 -3
  3. package/dist/assets/agent-skills/omk/SKILL.md +4 -2
  4. package/dist/assets/agent-skills/omk/references/commands.md +86 -3
  5. package/dist/cli/commands/doctor.js +19 -4
  6. package/dist/cli/commands/evolve.d.ts +11 -0
  7. package/dist/cli/commands/evolve.js +105 -5
  8. package/dist/cli/commands/list.d.ts +5 -26
  9. package/dist/cli/commands/list.js +17 -142
  10. package/dist/cli/commands/promote.d.ts +24 -0
  11. package/dist/cli/commands/promote.js +158 -0
  12. package/dist/cli/commands/rollback.d.ts +20 -0
  13. package/dist/cli/commands/rollback.js +95 -0
  14. package/dist/cli/commands/sample.d.ts +1 -0
  15. package/dist/cli/commands/sample.js +10 -4
  16. package/dist/cli/lib/cell-format.d.ts +17 -0
  17. package/dist/cli/lib/cell-format.js +19 -0
  18. package/dist/cli/lib/cmd-flags.d.ts +1 -0
  19. package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
  20. package/dist/cli/lib/i18n-dict/common.js +8 -0
  21. package/dist/cli/lib/i18n-dict/list.d.ts +1 -1
  22. package/dist/cli/lib/i18n-dict/list.js +4 -0
  23. package/dist/cli/lib/i18n-dict/promote.d.ts +3 -0
  24. package/dist/cli/lib/i18n-dict/promote.js +54 -0
  25. package/dist/cli/lib/i18n-dict/rollback.d.ts +3 -0
  26. package/dist/cli/lib/i18n-dict/rollback.js +22 -0
  27. package/dist/cli/lib/i18n-dict.d.ts +3 -1
  28. package/dist/cli/lib/i18n-dict.js +4 -0
  29. package/dist/cli/lib/progress.d.ts +3 -0
  30. package/dist/cli/lib/progress.js +22 -0
  31. package/dist/cli/lib/source-probe.d.ts +15 -0
  32. package/dist/cli/lib/source-probe.js +129 -0
  33. package/dist/doctor/endpoint-rule.d.ts +60 -0
  34. package/dist/doctor/endpoint-rule.js +381 -0
  35. package/dist/doctor/health/load-custom-dimensions.d.ts +6 -0
  36. package/dist/doctor/health/load-custom-dimensions.js +43 -3
  37. package/dist/doctor/index.js +8 -1
  38. package/dist/executors/codex-cli.js +5 -1
  39. package/dist/managed/index.d.ts +1 -0
  40. package/dist/managed/index.js +1 -0
  41. package/dist/managed/list-view.d.ts +3 -0
  42. package/dist/managed/list-view.js +8 -4
  43. package/dist/managed/promote-gate.d.ts +40 -0
  44. package/dist/managed/promote-gate.js +37 -0
  45. package/dist/managed/store.d.ts +25 -2
  46. package/dist/managed/store.js +92 -3
  47. package/dist/renderer/doctor-detail-renderer.d.ts +9 -0
  48. package/dist/renderer/doctor-detail-renderer.js +114 -0
  49. package/dist/renderer/html-renderer.d.ts +3 -2
  50. package/dist/renderer/html-renderer.js +99 -99
  51. package/dist/renderer/icons.d.ts +29 -0
  52. package/dist/renderer/icons.js +66 -0
  53. package/dist/renderer/layout.js +81 -68
  54. package/dist/renderer/observation-inbox/styles.d.ts +1 -1
  55. package/dist/renderer/observation-inbox/styles.js +53 -53
  56. package/dist/renderer/report-shell.d.ts +77 -0
  57. package/dist/renderer/report-shell.js +223 -0
  58. package/dist/renderer/skill-detail-renderer.js +147 -352
  59. package/dist/renderer/skill-health-renderer.js +50 -73
  60. package/dist/renderer/skill-list-renderer.js +358 -306
  61. package/dist/renderer/summary.js +272 -162
  62. package/dist/renderer/test-view.d.ts +4 -3
  63. package/dist/renderer/test-view.js +386 -135
  64. package/dist/server/report-server.js +142 -28
  65. package/dist/types/doctor.d.ts +22 -0
  66. package/dist/types/managed.d.ts +17 -3
  67. 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 { resolveInstallSource } from '../../inputs/source-resolver.js';
8
- import { buildManagedListRows, globalManagedDir, hashArtifactSource, isDistributablePath, loadAllManagedRecords, managedDir, resolveManagedDir, } from '../../managed/index.js';
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 { 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
- /** 洗不可信显示字符:managed JSON 可随仓库分发,name / sourceLabel / verdict 等字段塞进表格前必须先洗,
155
- * 否则会破坏表格甚至伪造终端输出。一律映射到可见 U+FFFD(--json 路径保留原值给脚本)。
156
- * 用 Unicode **属性类**而非手列码点 —— 手列清单天然有缺口(BiDi、U+2028 / 2029、Tags 块都曾漏一轮补一轮),
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
- if (hasDrift || hasUnreachable)
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
+ }
@@ -149,6 +149,7 @@ export interface SampleFlags {
149
149
  batch: boolean;
150
150
  count?: string;
151
151
  model: string;
152
+ executor?: string;
152
153
  'skill-dir': string;
153
154
  focus?: string;
154
155
  'no-mock': boolean;
@@ -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
  };