oh-my-knowledge 0.34.0 → 0.35.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 (45) hide show
  1. package/README.md +3 -0
  2. package/README.zh.md +3 -0
  3. package/dist/assets/agent-skills/omk/references/commands.md +9 -0
  4. package/dist/authoring/evolver.js +12 -3
  5. package/dist/cli/commands/eval/index.d.ts +1 -0
  6. package/dist/cli/commands/eval/index.js +36 -2
  7. package/dist/cli/commands/install.d.ts +2 -0
  8. package/dist/cli/commands/install.js +45 -20
  9. package/dist/cli/commands/sample.d.ts +1 -1
  10. package/dist/cli/commands/sample.js +25 -12
  11. package/dist/cli/lib/cmd-flags.d.ts +1 -0
  12. package/dist/cli/lib/i18n-dict/install.d.ts +1 -1
  13. package/dist/cli/lib/i18n-dict/install.js +20 -0
  14. package/dist/cli/lib/i18n-dict/run.d.ts +1 -1
  15. package/dist/cli/lib/i18n-dict/run.js +8 -0
  16. package/dist/cli/lib/parse-run-config/variant-resolution.js +7 -0
  17. package/dist/doctor/index.js +5 -5
  18. package/dist/eval-core/cache.d.ts +11 -3
  19. package/dist/eval-core/cache.js +13 -5
  20. package/dist/eval-core/dependency-checker.js +5 -3
  21. package/dist/eval-core/evaluation-execution.js +4 -1
  22. package/dist/eval-core/evaluation-reporting.js +17 -8
  23. package/dist/eval-core/execution-strategy.js +6 -4
  24. package/dist/eval-core/task-planner.js +2 -1
  25. package/dist/eval-workflows/evaluation-preparation.js +3 -1
  26. package/dist/inputs/content-hash.d.ts +28 -0
  27. package/dist/inputs/content-hash.js +106 -0
  28. package/dist/inputs/eval-config.js +44 -11
  29. package/dist/inputs/materialize-copy.d.ts +37 -0
  30. package/dist/inputs/materialize-copy.js +193 -0
  31. package/dist/inputs/skill-loader.d.ts +65 -2
  32. package/dist/inputs/skill-loader.js +308 -13
  33. package/dist/inputs/source-resolver.d.ts +16 -9
  34. package/dist/inputs/source-resolver.js +57 -77
  35. package/dist/managed/evidence.d.ts +22 -0
  36. package/dist/managed/evidence.js +143 -0
  37. package/dist/managed/index.d.ts +1 -0
  38. package/dist/managed/index.js +1 -0
  39. package/dist/managed/store.d.ts +16 -20
  40. package/dist/managed/store.js +40 -76
  41. package/dist/renderer/layout.js +2 -2
  42. package/dist/types/eval.d.ts +12 -1
  43. package/dist/types/managed.d.ts +29 -4
  44. package/dist/types/report.d.ts +17 -3
  45. package/package.json +3 -3
package/README.md CHANGED
@@ -100,6 +100,8 @@ RAG-specific evals: see RAGAS (separate niche, complementary to omk). Full compa
100
100
  | **Production observability** | parse Claude Code session JSONL traces; measure per-skill failure rate / latency / cost / knowledge-gap signals |
101
101
  | **Knowledge-gap detection** | severity-weighted signals quantify risk exposure instead of claiming completeness |
102
102
  | **Construct-validity isolation** | `--strict-baseline` (default ON) cuts three contamination channels so baseline doesn't silently see the skill it's being compared against |
103
+ | **Git & remote sources** | install / eval from a local git ref or a remote git URL (`--git-url`); directory-skills run in a content-addressed **isolated copy** so `references/` assets are real measured input, not just `SKILL.md` |
104
+ | **Evidence-gated management** | `omk install` registers a managed record; `omk eval` auto-writes evidence bound by content fingerprint, moving a skill `installed → measurable`. [spec →](docs/specs/evidence-gated-management.md) |
103
105
  | **Sample design science** | sample schema with `capability` / `difficulty` / `construct` / `provenance` metadata (HF Dataset Cards style); studio surfaces coverage breakdown plus `rubric_clarity_low` / `capability_thin` flags. [docs/specs/sample-design-spec.md](docs/specs/sample-design-spec.md) |
104
106
  | **Multi-judge ensemble** | `--judge-models claude:opus,openai:gpt-4o` cross-vendor scoring + agreement metrics |
105
107
  | **Blind A/B** | `--blind` hides variant names; HTML report has a reveal button |
@@ -122,6 +124,7 @@ The full docs are published at **[oh-my-knowledge.pages.dev](https://oh-my-knowl
122
124
  - **[Sample design spec](docs/specs/sample-design-spec.md)** — capability / construct / provenance metadata; industry-gap mapping
123
125
  - **[Statistical rigor](docs/explanation/statistical-rigor.md)** — why bootstrap CI / α / length-debias / saturation matter
124
126
  - **[Comparison with 7 tools](docs/reference/comparison.md)** — 25+ dimensions across promptfoo / DeepEval / RAGAS / OpenAI Evals / LangSmith / lm-eval-harness / inspect-ai
127
+ - **[Evidence-gated management](docs/specs/evidence-gated-management.md)** — managed records, lifecycle states (installed / measurable / stale), install → eval → measurable
125
128
 
126
129
  ## Environment variables
127
130
 
package/README.zh.md CHANGED
@@ -100,6 +100,8 @@ RAG 专项评测请看 RAGAS(独立 niche,跟 omk 互补)。完整对比
100
100
  | **线上 session 观测** | 解析 Claude Code session JSONL,测量各 skill 的失败率、耗时、token 成本、知识缺口信号 |
101
101
  | **知识缺口识别** | 严重度加权的信号量化风险敞口,不宣称完备性 |
102
102
  | **用例隔离 (construct validity)** | `--strict-baseline`(默认开)三堵 baseline 拿到被测 skill 的污染路径 |
103
+ | **Git / 远端源** | install / eval 支持本地 git ref 或远端 git URL(`--git-url`);目录-skill 在内容寻址**隔离副本**里执行,`references/` 资产是真实测量输入,不只是 `SKILL.md` |
104
+ | **证据门控管理** | `omk install` 登记受管记录;`omk eval` 按内容指纹自动写入证据,把 skill 从 `installed` 推到 `measurable`。[规范 →](docs/zh/specs/evidence-gated-management.md) |
103
105
  | **用例设计科学性** | Sample schema 加 `capability` / `difficulty` / `construct` / `provenance` 元数据字段(HF Dataset Cards 风),studio 输出 coverage 分桶 + `rubric_clarity_low` / `capability_thin` issue。[docs/zh/specs/sample-design-spec.md](docs/zh/specs/sample-design-spec.md) |
104
106
  | **多评委 ensemble** | `--judge-models claude:opus,openai:gpt-4o` 跨厂商评分 + agreement 度量 |
105
107
  | **盲测 A/B** | `--blind` 隐藏变体名称,HTML 报告有揭晓按钮 |
@@ -122,6 +124,7 @@ RAG 专项评测请看 RAGAS(独立 niche,跟 omk 互补)。完整对比
122
124
  - **[用例设计规范](docs/zh/specs/sample-design-spec.md)** —— capability / construct / provenance 元数据;行业 gap 映射
123
125
  - **[统计严谨性](docs/zh/explanation/statistical-rigor.md)** —— 为什么 Bootstrap CI / α / 长度去偏 / 饱和曲线重要
124
126
  - **[7 工具对比](docs/zh/reference/comparison.md)** —— promptfoo / DeepEval / RAGAS / OpenAI Evals / LangSmith / lm-eval-harness / inspect-ai 等 25+ 维度横评
127
+ - **[证据门控管理](docs/zh/specs/evidence-gated-management.md)** —— 受管记录、生命周期状态(installed / measurable / stale)、install → eval → measurable
125
128
 
126
129
  ## 环境变量
127
130
 
@@ -89,6 +89,7 @@ omk eval [flags]
89
89
  - `--no-cache` `boolean`:跳过 executor cache
90
90
  - `--no-debias-length` `boolean`:关 length-debias(默认开)
91
91
  - `--no-diagnostic` `boolean`:关闭 diagnostic 诊断 LLM 调用(默认开,给 failed sample 出「哪错了 + 怎么改」建议)。
92
+ - `--no-evidence` `boolean`:不把本次评测写成证据追加进受管记录(默认会为已 install 的 skill 自动写)。
92
93
  - `--no-gate` `boolean`:关 verdict gate
93
94
  - `--no-judge` `boolean`:跳过 LLM judge
94
95
  - `--no-serve` `boolean`:不启 report server
@@ -303,6 +304,8 @@ omk install <input> [flags]
303
304
  - `--dest` `option`:自定义 skill 根目录;skill 安装到 <dir>/<name>(内置 omk-agent-skill 为 <dir>/omk)。
304
305
  - `--dry-run` `boolean`:只打印安装目标,不写文件。
305
306
  - `--force` `boolean`:覆盖目标位置已存在的 skill。
307
+ - `--git-ref` `option`:远端 git 的 ref(分支 / tag / SHA),默认 HEAD。仅配合 --git-url 使用。
308
+ - `--git-url` `option`:远端 git 仓库 URL(https / ssh / git@host:path)。给了它时,位置参数当作仓库内 skill 路径(spec)。
306
309
  - `--kind` `skill|prompt|agent|workflow`:用户 artifact 的 kind(对齐 Artifact.kind)。可省:命中 SKILL.md 自动推导,当前仅支持 skill。
307
310
  - `--lang` `option` (默认 `zh`):输出语言 zh|en,优先级 CLI > OMK_LANG env > zh。
308
311
  - `--to` `option` (默认 `auto`):安装目标:auto(默认,本机已检测目标) / codex / claude / all。
@@ -339,6 +342,12 @@ omk install ./skills/review
339
342
  omk install git:main:skills/review
340
343
  ```
341
344
 
345
+ > 从远端 git 仓库安装 skill(位置参数是仓库内路径;认证用本机 git 凭证;记录钉实际 SHA)
346
+
347
+ ```bash
348
+ omk install --git-url https://github.com/org/repo.git --git-ref v1.0.0 skills/review
349
+ ```
350
+
342
351
  ## omk observe
343
352
 
344
353
  分析 sessions 目录的 skill 调用健康度(默认行为)。子命令:ingest / inbox / show。
@@ -6,6 +6,7 @@ import { persistReport, DEFAULT_OUTPUT_DIR, generateRunId, hashString } from '..
6
6
  import { createFileStore } from '../server/report-store.js';
7
7
  import { analyzeResults } from '../analysis/report-diagnostics.js';
8
8
  import { loadSamples } from '../inputs/load-samples.js';
9
+ import { hashArtifactSource } from '../inputs/content-hash.js';
9
10
  import { buildVariantSummary } from '../eval-core/schema.js';
10
11
  import { bootstrapDiffCI, DEFAULT_BOOTSTRAP_ALPHA, DEFAULT_BOOTSTRAP_SAMPLES } from '../eval-core/bootstrap.js';
11
12
  import { fixSamples } from './sample-fixer.js';
@@ -88,9 +89,13 @@ function singleVariantReport(report, variantKey) {
88
89
  async function findReusableBaselineReport(opts) {
89
90
  const store = createFileStore(DEFAULT_OUTPUT_DIR);
90
91
  const { samples } = loadSamples(opts.samplesPath);
91
- const artifactHash = hashString(opts.skillContent);
92
+ const artifactHash = opts.artifactHash;
92
93
  const reports = await store.findByArtifactHash(artifactHash);
93
94
  for (const report of reports) {
95
+ // schemaVersion < 2 的报告 artifactHashes 是旧文本哈,与当前树哈不同空间:即便值偶合也不该复用
96
+ // (口径不同会让 lineage 串错身份)。直接跳过,让旧 baseline 重跑出树哈报告。
97
+ if ((report.meta.schemaVersion ?? 0) < 2)
98
+ continue;
94
99
  if (report.meta.model !== opts.model || report.meta.executor !== opts.executorName)
95
100
  continue;
96
101
  if ((report.meta.effort ?? undefined) !== (opts.effort ?? undefined))
@@ -669,10 +674,14 @@ export async function evolveSkill({ skillPath, samplesPath, rounds = 5, target =
669
674
  let stopReason = 'rounds';
670
675
  // 给定一个 report 看任一 variant 的 exec/judge cost 是否未报告
671
676
  const reportHasUnreportedCost = (rep) => Object.values(rep.summary).some((v) => v.execCostReported === false || v.judgeCostReported === false);
672
- // Round 0: baseline evaluation
677
+ // Round 0: baseline evaluation。复用查询键走整树哈,与 eval 报告口径一致:round 0 的 currentBest 即
678
+ // 磁盘上原始 skill,树哈取自磁盘(dir-skill 哈整目录、单文件 .md 哈单文件)。evolve 只改 SKILL.md 正文、
679
+ // 不动 references/ 资产,故磁盘树哈即该 baseline 的权威指纹。
680
+ const baselineIsDirSkill = basename(absSkillPath) === 'SKILL.md';
681
+ const baselineArtifactHash = hashArtifactSource(baselineIsDirSkill ? skillDir : absSkillPath, baselineIsDirSkill);
673
682
  let baselineReport = reuseLatestEval
674
683
  ? await findReusableBaselineReport({
675
- skillContent: currentBest,
684
+ artifactHash: baselineArtifactHash,
676
685
  samplesPath: absSamplesPath,
677
686
  model,
678
687
  executorName,
@@ -50,6 +50,7 @@ export default class Eval extends BaseCommand {
50
50
  'trivial-diff': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
51
51
  'report-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
52
52
  'no-gate': import("@oclif/core/interfaces").BooleanFlag<boolean>;
53
+ 'no-evidence': import("@oclif/core/interfaces").BooleanFlag<boolean>;
53
54
  };
54
55
  run(): Promise<void>;
55
56
  }
@@ -37,12 +37,32 @@ function applyGateExitCode(code, values, lang) {
37
37
  process.stderr.write(tCli('cli.run.report_only_gate_skipped', lang));
38
38
  return 0;
39
39
  }
40
- async function emitEvaluationVerdict(report, values) {
40
+ async function emitEvaluationVerdict(report, values, lang) {
41
41
  const { computeVerdict, formatVerdictText } = await import('../../../eval-core/verdict.js');
42
42
  const result = computeVerdict(report, verdictOptions(values));
43
43
  console.log(formatVerdictText(result, { verbose: true }));
44
+ await recordEvidenceSafely(report, result.level, values, lang);
44
45
  return verdictPasses(result.level, result.headline) ? 0 : 1;
45
46
  }
47
+ /**
48
+ * 把本次评测写成证据追加进**已纳管**记录(让 install 过的 skill 走到 measurable)。
49
+ * 永不致命:管理是 eval 的旁路,写入失败 / 无匹配记录都不影响 verdict 与 exit code。
50
+ * `--no-evidence` 关闭。仅对实际写入的记录打一行提示(无匹配则全静默)。
51
+ */
52
+ async function recordEvidenceSafely(report, verdict, values, lang) {
53
+ if (values['no-evidence'] === true)
54
+ return;
55
+ try {
56
+ const { recordEvalEvidence } = await import('../../../managed/index.js');
57
+ const written = recordEvalEvidence(report, verdict, new Date().toISOString());
58
+ for (const w of written) {
59
+ process.stderr.write(tCli(w.bound ? 'cli.run.evidence_recorded' : 'cli.run.evidence_recorded_unbound', lang, { name: w.name }));
60
+ }
61
+ }
62
+ catch {
63
+ // 证据写入是旁路,任何异常都不该让评测失败
64
+ }
65
+ }
46
66
  function batchItemFallbackReport(batch, item) {
47
67
  return {
48
68
  reportKind: 'evaluation',
@@ -52,6 +72,9 @@ function batchItemFallbackReport(batch, item) {
52
72
  variants: ['baseline', item.name],
53
73
  sampleCount: item.sampleCount,
54
74
  totalCostUSD: item.totalCostUSD,
75
+ // item.artifactHash 来自子报告(走 aggregateReport 的整树哈),故 fallback 与之一致标 schemaVersion 3,
76
+ // 避免「树哈 artifactHashes + 错位 schemaVersion」的错配。
77
+ schemaVersion: 3,
55
78
  artifactHashes: item.artifactHash ? { [item.name]: item.artifactHash } : {},
56
79
  },
57
80
  summary: item.summary,
@@ -83,6 +106,11 @@ async function emitBatchVerdict(report, reportsDir, values, lang) {
83
106
  treatment: child.meta.variants[1] ?? child.id,
84
107
  verdict: computeVerdict(child, verdictOptions(values)),
85
108
  }));
109
+ // batch 每个子报告各自是一份独立 skill 的评测 → 各自写证据。
110
+ for (const child of childReports) {
111
+ const v = results.find((r) => r.id === child.id)?.verdict.level ?? 'SOLO';
112
+ await recordEvidenceSafely(child, v, values, lang);
113
+ }
86
114
  const passed = results.filter((r) => verdictPasses(r.verdict.level, r.verdict.headline)).length;
87
115
  const failed = results.length - passed;
88
116
  const status = lang === 'zh'
@@ -255,7 +283,7 @@ async function runEval(_args, flags, lang) {
255
283
  if (filePath) {
256
284
  await announceSavedReport({ report, filePath, reportsDir: config.outputDir, values, lang });
257
285
  }
258
- const exitCode = await emitEvaluationVerdict(report, values);
286
+ const exitCode = await emitEvaluationVerdict(report, values, lang);
259
287
  throw new CliExit(applyGateExitCode(exitCode, values, lang));
260
288
  }
261
289
  catch (err) {
@@ -466,6 +494,12 @@ export default class Eval extends BaseCommand {
466
494
  'no-gate': Flags.boolean({
467
495
  description: bilingual({ zh: '关 verdict gate', en: 'Disable verdict gate' }),
468
496
  }),
497
+ 'no-evidence': Flags.boolean({
498
+ description: bilingual({
499
+ zh: '不把本次评测写成证据追加进受管记录(默认会为已 install 的 skill 自动写)。',
500
+ en: 'Do not append this run as evidence to managed records (auto-written for installed skills by default).',
501
+ }),
502
+ }),
469
503
  };
470
504
  async run() {
471
505
  const { args, flags } = await this.parse(Eval);
@@ -15,6 +15,8 @@ export default class Install extends BaseCommand {
15
15
  dest: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
16
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
17
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ 'git-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ 'git-ref': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
20
  };
19
21
  run(): Promise<void>;
20
22
  private installBuiltinAgentSkill;
@@ -1,13 +1,13 @@
1
- import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, realpathSync, rmSync, statSync } from 'node:fs';
1
+ import { copyFileSync, cpSync, existsSync, mkdirSync, realpathSync, rmSync, statSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
- import { basename, dirname, relative, resolve, join, sep } from 'node:path';
3
+ import { basename, dirname, resolve, join, sep } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { Args, Flags } from '@oclif/core';
6
6
  import { LANG_FLAG, bilingual } from '../oclif/i18n.js';
7
7
  import { BaseCommand } from '../oclif/base-command.js';
8
8
  import { tCli } from '../lib/i18n.js';
9
- import { resolveInstallSource, SourceResolveError } from '../../inputs/source-resolver.js';
10
- import { buildManagedArtifactRecord, hashArtifactSource, isDistributablePath, managedDir, recordManagedArtifact } from '../../managed/index.js';
9
+ import { resolveInstallSource, resolveRemoteGitSource, SourceResolveError } from '../../inputs/source-resolver.js';
10
+ import { buildManagedArtifactRecord, hashArtifactSource, distributableCopyFilter, managedDir, recordManagedArtifact } from '../../managed/index.js';
11
11
  const BUILTIN_OMK_AGENT_SKILL_ID = 'omk-agent-skill';
12
12
  const INSTALLABLE_KINDS = ['skill', 'prompt', 'agent', 'workflow'];
13
13
  const TARGET_ORDER = ['codex', 'claude'];
@@ -163,17 +163,10 @@ function copyArtifactToTarget(params) {
163
163
  if (params.isDirectorySkill) {
164
164
  cpSync(params.source, params.targetPath, {
165
165
  recursive: true,
166
- // 与 hashArtifactSource 共用过滤,保证"分发出去的 == 算进 hash 的":
167
- // 源根永远拷;软链跳过(与 hash walk 一致,避免软链目标改了却不触发 drift);
168
- // evolve 只在源根第一层排除(嵌套 references/evolve 正常分发),.omk/.git 任意层级排除。
169
- filter: (src) => {
170
- const rel = relative(params.source, src);
171
- if (rel === '')
172
- return true;
173
- if (lstatSync(src).isSymbolicLink())
174
- return false;
175
- return isDistributablePath(rel.split(sep));
176
- },
166
+ // 与 hashArtifactSource / eval 隔离副本共用同一处过滤(distributableCopyFilter),保证
167
+ // "分发出去的 == 算进 hash == 测量的副本":源根永远拷;软链跳过;evolve 仅源根第一层排除、
168
+ // .omk/.git/node_modules 任意层级排除。
169
+ filter: distributableCopyFilter(params.source),
177
170
  });
178
171
  }
179
172
  else {
@@ -253,6 +246,13 @@ export default class Install extends BaseCommand {
253
246
  }),
254
247
  command: '<%= config.bin %> install git:main:skills/review',
255
248
  },
249
+ {
250
+ description: bilingual({
251
+ zh: '从远端 git 仓库安装 skill(位置参数是仓库内路径;认证用本机 git 凭证;记录钉实际 SHA)',
252
+ en: 'Install a skill from a remote git repo (positional arg is the in-repo path; auth via local git credentials; record pins the actual SHA)',
253
+ }),
254
+ command: '<%= config.bin %> install --git-url https://github.com/org/repo.git --git-ref v1.0.0 skills/review',
255
+ },
256
256
  ];
257
257
  static args = {
258
258
  input: Args.string({
@@ -299,11 +299,35 @@ export default class Install extends BaseCommand {
299
299
  }),
300
300
  default: false,
301
301
  }),
302
+ 'git-url': Flags.string({
303
+ description: bilingual({
304
+ zh: '远端 git 仓库 URL(https / ssh / git@host:path)。给了它时,位置参数当作仓库内 skill 路径(spec)。',
305
+ en: 'Remote git repository URL (https / ssh / git@host:path). When set, the positional arg is the in-repo skill path (spec).',
306
+ }),
307
+ }),
308
+ 'git-ref': Flags.string({
309
+ description: bilingual({
310
+ zh: '远端 git 的 ref(分支 / tag / SHA),默认 HEAD。仅配合 --git-url 使用。',
311
+ en: 'Remote git ref (branch / tag / SHA), default HEAD. Only with --git-url.',
312
+ }),
313
+ }),
302
314
  };
303
315
  async run() {
304
316
  const { args, flags } = await this.parse(Install);
305
317
  const lang = this.lang;
306
318
  await this.runWithCliExit(async () => {
319
+ // --git-ref 必须配 --git-url(否则静默丢弃、误把 spec 当本地路径解析,报错令人困惑)。
320
+ if (flags['git-ref'] && !flags['git-url']) {
321
+ throw new Error(tCli('cli.install.git_ref_needs_url', lang));
322
+ }
323
+ // 远端 git:--git-url 在场时,位置参数是仓库内 spec(repo 相对路径),先于其它分支判定。
324
+ if (flags['git-url']) {
325
+ this.installManagedSkill(args.input, flags.kind, flags, lang, {
326
+ url: flags['git-url'],
327
+ ref: flags['git-ref'] || 'HEAD',
328
+ });
329
+ return;
330
+ }
307
331
  if (args.input === BUILTIN_OMK_AGENT_SKILL_ID) {
308
332
  this.installBuiltinAgentSkill(flags, lang);
309
333
  return;
@@ -330,17 +354,18 @@ export default class Install extends BaseCommand {
330
354
  if (!flags['dry-run'])
331
355
  console.log(tCli('cli.install.next_hint', lang));
332
356
  }
333
- installManagedSkill(input, kindFlag, flags, lang) {
357
+ installManagedSkill(input, kindFlag, flags, lang, remote) {
334
358
  // kind 推导:--kind 显式优先;否则缺省 skill(Phase 1 仅 skill)。
335
359
  const kind = kindFlag ?? 'skill';
336
360
  if (kind !== 'skill') {
337
361
  throw new Error(tCli('cli.install.kind_unsupported', lang, { kind }));
338
362
  }
339
- // 源解析委托给 source-resolver(file / git ...),install 主干源无关。
363
+ // 源解析委托给 source-resolver(file / 本地 git / 远端 git),install 主干源无关。
364
+ // 远端走结构化 resolveRemoteGitSource(url/ref/spec 分字段,URL 不经任何字符串切分);
340
365
  // resolver 不依赖 CLI,错误以 SourceResolveError(messageKey) 抛出,这里映射成本地化文案。
341
366
  let src;
342
367
  try {
343
- src = resolveInstallSource(input);
368
+ src = remote ? resolveRemoteGitSource(remote.url, remote.ref, input) : resolveInstallSource(input);
344
369
  }
345
370
  catch (err) {
346
371
  if (err instanceof SourceResolveError) {
@@ -349,7 +374,7 @@ export default class Install extends BaseCommand {
349
374
  throw err;
350
375
  }
351
376
  try {
352
- const { localRoot, name, isDirectorySkill, sourceKind, locator, ref } = src;
377
+ const { localRoot, name, isDirectorySkill, sourceKind, locator, ref, url } = src;
353
378
  // 目录-skill 哈整棵可分发树(排除 .omk/.git/evolve);git 源哈的是物化后的临时树。
354
379
  const contentHash = hashArtifactSource(localRoot, isDirectorySkill);
355
380
  const targets = resolveInstallTargets({ to: flags.to, dest: flags.dest, lang });
@@ -371,7 +396,7 @@ export default class Install extends BaseCommand {
371
396
  const record = buildManagedArtifactRecord({
372
397
  name,
373
398
  kind,
374
- source: { sourceKind, locator, ...(ref ? { ref } : {}), isDirectorySkill },
399
+ source: { sourceKind, locator, ...(ref ? { ref } : {}), ...(url ? { url } : {}), isDirectorySkill },
375
400
  contentHash,
376
401
  installedAt: now,
377
402
  distribution,
@@ -6,7 +6,7 @@ export declare function collectSampleDesignFailureIds(report: Pick<Report, 'resu
6
6
  export declare function assertFixReportMatchesCurrentInputs(params: {
7
7
  report: Pick<Report, 'meta'>;
8
8
  treatmentName: string;
9
- skillContent: string;
9
+ currentContentHash: string;
10
10
  samples: SampleType[];
11
11
  sampleIds: Set<string>;
12
12
  lang?: CliLang;
@@ -9,7 +9,8 @@ import { CliExit } from '../lib/cli-exit.js';
9
9
  import { tCli } from '../lib/i18n.js';
10
10
  import { DEFAULT_REPORTS_DIR } from '../lib/parse-run-config.js';
11
11
  import { loadSamples, parseYaml } from '../../inputs/load-samples.js';
12
- import { hashSample, hashString } from '../../eval-core/evaluation-reporting.js';
12
+ import { hashSample } from '../../eval-core/evaluation-reporting.js';
13
+ import { hashArtifactSource } from '../../inputs/content-hash.js';
13
14
  function isRecord(value) {
14
15
  return typeof value === 'object' && value !== null && !Array.isArray(value);
15
16
  }
@@ -48,20 +49,29 @@ export function collectSampleDesignFailureIds(report, treatmentName) {
48
49
  return ids;
49
50
  }
50
51
  export function assertFixReportMatchesCurrentInputs(params) {
51
- const { report, treatmentName, skillContent, samples, sampleIds } = params;
52
+ const { report, treatmentName, currentContentHash, samples, sampleIds } = params;
52
53
  const lang = params.lang ?? 'zh';
53
54
  const issues = [];
54
- const expectedSkillHash = report.meta.artifactHashes?.[treatmentName];
55
- const currentSkillHash = hashString(skillContent);
56
- if (!expectedSkillHash) {
55
+ // schemaVersion < 2 的报告:artifactHashes 是旧「仅 SKILL.md 正文文本」哈,与当前「整棵可分发树」哈
56
+ // 不同空间,直接比对会必然误报不一致。识别后给可见提示(归入 issues → 触发「请先重跑 eval」),
57
+ // 不拿旧文本哈与当前树哈错配比对。sample 指纹口径未变,下面照常校。
58
+ if ((report.meta.schemaVersion ?? 0) < 2) {
57
59
  issues.push(lang === 'zh'
58
- ? `报告缺少 ${treatmentName} 的 skill 指纹,无法确认诊断对应当前 SKILL.md。`
59
- : `Report is missing the skill hash for ${treatmentName}; cannot verify it matches the current SKILL.md.`);
60
+ ? `报告早于树哈纪元(skill 指纹口径已从「仅 SKILL.md 文本」改为「整棵可分发树」),无法与当前指纹比对。`
61
+ : `Report predates the tree-hash era (skill fingerprint changed from SKILL.md-body-text to whole-tree); cannot compare against the current fingerprint.`);
60
62
  }
61
- else if (expectedSkillHash !== currentSkillHash) {
62
- issues.push(lang === 'zh'
63
- ? `skill 指纹不一致:报告 ${expectedSkillHash},当前 ${currentSkillHash}。`
64
- : `Skill hash mismatch: report ${expectedSkillHash}, current ${currentSkillHash}.`);
63
+ else {
64
+ const expectedSkillHash = report.meta.artifactHashes?.[treatmentName];
65
+ if (!expectedSkillHash) {
66
+ issues.push(lang === 'zh'
67
+ ? `报告缺少 ${treatmentName} 的 skill 指纹,无法确认诊断对应当前 SKILL.md。`
68
+ : `Report is missing the skill hash for ${treatmentName}; cannot verify it matches the current SKILL.md.`);
69
+ }
70
+ else if (expectedSkillHash !== currentContentHash) {
71
+ issues.push(lang === 'zh'
72
+ ? `skill 指纹不一致:报告 ${expectedSkillHash},当前 ${currentContentHash}。`
73
+ : `Skill hash mismatch: report ${expectedSkillHash}, current ${currentContentHash}.`);
74
+ }
65
75
  }
66
76
  const reportSampleHashes = report.meta.sampleHashes;
67
77
  if (!reportSampleHashes) {
@@ -196,11 +206,14 @@ async function runSampleFix(args, flags, lang) {
196
206
  process.stderr.write(lang === 'zh' ? '✅ 没有 sample_design 类型的失败,无需修复\n' : '✅ No sample_design failures found, nothing to fix\n');
197
207
  return;
198
208
  }
209
+ // 当前内容指纹走整树哈,与 eval 报告口径一致:dir-skill(用户传 .../SKILL.md)哈整棵 skill 目录、
210
+ // 单文件 .md 哈单文件字节。
211
+ const currentContentHash = hashArtifactSource(isDir ? skillDir : resolvedSkillPath, isDir);
199
212
  try {
200
213
  assertFixReportMatchesCurrentInputs({
201
214
  report,
202
215
  treatmentName,
203
- skillContent,
216
+ currentContentHash,
204
217
  samples,
205
218
  sampleIds: sampleDesignIds,
206
219
  lang,
@@ -108,6 +108,7 @@ export interface EvalFlags {
108
108
  'trivial-diff'?: string;
109
109
  'report-only'?: boolean;
110
110
  'no-gate'?: boolean;
111
+ 'no-evidence'?: boolean;
111
112
  }
112
113
  export interface EvolveArgs {
113
114
  skillPath: string;
@@ -1,3 +1,3 @@
1
1
  import type { CliMessage } from './types.js';
2
- export type InstallMessageKey = 'cli.install.asset_missing' | 'cli.install.unknown_input' | 'cli.install.unknown_target' | 'cli.install.invalid_target_combo' | 'cli.install.no_detected_targets' | 'cli.install.target_exists' | 'cli.install.plan' | 'cli.install.plan_skill' | 'cli.install.installed' | 'cli.install.kind_unsupported' | 'cli.install.copied' | 'cli.install.adopted' | 'cli.install.registered' | 'cli.install.target_overlaps_source' | 'cli.install.path_not_found' | 'cli.install.skillmd_missing' | 'cli.install.skillmd_is_symlink' | 'cli.install.not_a_skill' | 'cli.install.not_a_git_repo' | 'cli.install.git_skill_not_found' | 'cli.install.git_unsafe_path' | 'cli.install.next_hint';
2
+ export type InstallMessageKey = 'cli.install.asset_missing' | 'cli.install.unknown_input' | 'cli.install.unknown_target' | 'cli.install.invalid_target_combo' | 'cli.install.no_detected_targets' | 'cli.install.target_exists' | 'cli.install.plan' | 'cli.install.plan_skill' | 'cli.install.installed' | 'cli.install.kind_unsupported' | 'cli.install.copied' | 'cli.install.adopted' | 'cli.install.registered' | 'cli.install.target_overlaps_source' | 'cli.install.path_not_found' | 'cli.install.skillmd_missing' | 'cli.install.skillmd_is_symlink' | 'cli.install.not_a_skill' | 'cli.install.not_a_git_repo' | 'cli.install.git_skill_not_found' | 'cli.install.git_unsafe_path' | 'cli.install.invalid_remote_url' | 'cli.install.remote_fetch_failed' | 'cli.install.remote_ref_not_found' | 'cli.install.remote_skill_not_found' | 'cli.install.git_ref_needs_url' | 'cli.install.next_hint';
3
3
  export declare const installDict: Record<InstallMessageKey, CliMessage>;
@@ -83,6 +83,26 @@ export const installDict = {
83
83
  zh: 'git tree 含越界路径 {path}(.. / 绝对路径 / 空段),拒绝物化以防写出临时目录。该 skill 的 git tree 可能被手工构造,请核查来源。',
84
84
  en: 'git tree contains an out-of-bounds path {path} (.. / absolute / empty segment); refusing to materialize to avoid escaping the temp dir. The skill git tree may be hand-crafted; verify the source.',
85
85
  },
86
+ 'cli.install.invalid_remote_url': {
87
+ zh: '不是合法的 git 远端 URL:{url}。支持 https:// / ssh:// / git:// / file:// 或 scp 形式 git@host:path。',
88
+ en: 'Not a valid git remote URL: {url}. Supported: https:// / ssh:// / git:// / file:// or scp-form git@host:path.',
89
+ },
90
+ 'cli.install.remote_fetch_failed': {
91
+ zh: '从远端 {url} 拉取 ref {ref} 失败。请检查 URL、ref 是否存在、以及本机 git 凭证(SSH key / credential helper)。',
92
+ en: 'Failed to fetch ref {ref} from remote {url}. Check the URL, whether the ref exists, and your local git credentials (SSH key / credential helper).',
93
+ },
94
+ 'cli.install.remote_ref_not_found': {
95
+ zh: '远端 {url} 上解析不到 ref {ref}。',
96
+ en: 'Could not resolve ref {ref} on remote {url}.',
97
+ },
98
+ 'cli.install.remote_skill_not_found': {
99
+ zh: '在远端 {url} 的 ref {ref} 下找不到 skill {name}(既无 {name}/SKILL.md 也无 {name}.md)。',
100
+ en: 'Skill {name} not found at ref {ref} of remote {url} (neither {name}/SKILL.md nor {name}.md).',
101
+ },
102
+ 'cli.install.git_ref_needs_url': {
103
+ zh: '--git-ref 需要配 --git-url 一起用(远端 git 源 = URL + ref + 仓库内路径)。',
104
+ en: '--git-ref requires --git-url (a remote git source = URL + ref + in-repo path).',
105
+ },
86
106
  'cli.install.next_hint': {
87
107
  zh: '现在可以在 coding agent 中说「用 omk 评测这个 skill」。',
88
108
  en: 'You can now ask your coding agent: "use omk to evaluate this skill".',
@@ -1,3 +1,3 @@
1
1
  import type { CliMessage } from './types.js';
2
- export type RunMessageKey = 'cli.progress.preflight_starting' | 'cli.progress.sample_retry' | 'cli.progress.sample_error' | 'cli.progress.sample_executing' | 'cli.progress.sample_exec_done' | 'cli.progress.output_preview' | 'cli.progress.judging' | 'cli.progress.judged' | 'cli.progress.skipped' | 'cli.progress.sample_done' | 'cli.progress.sample_failed_done' | 'cli.run.invalid_repeat' | 'cli.run.invalid_judge_repeat' | 'cli.run.no_debias_length_active' | 'cli.run.invalid_bootstrap_samples' | 'cli.run.bootstrap_samples_too_large' | 'cli.run.dry_run_no_scores' | 'cli.run.skill_section' | 'cli.run.run_section' | 'cli.run.batch_complete' | 'cli.run.batch_verdict_header' | 'cli.run.batch_child_report_missing' | 'cli.run.eval_complete' | 'cli.run.tally' | 'cli.run.report_saved' | 'cli.run.report_only_gate_skipped' | 'cli.run.report_server_running' | 'cli.run.report_server_view' | 'cli.run.report_server_stop' | 'cli.run.no_serve_in_non_tty' | 'cli.run.no_serve_view_hint' | 'cli.run.gold_load_failed' | 'cli.run.gold_load_issue' | 'cli.run.contamination_warning' | 'cli.run.skip_connectivity_warning';
2
+ export type RunMessageKey = 'cli.progress.preflight_starting' | 'cli.progress.sample_retry' | 'cli.progress.sample_error' | 'cli.progress.sample_executing' | 'cli.progress.sample_exec_done' | 'cli.progress.output_preview' | 'cli.progress.judging' | 'cli.progress.judged' | 'cli.progress.skipped' | 'cli.progress.sample_done' | 'cli.progress.sample_failed_done' | 'cli.run.invalid_repeat' | 'cli.run.invalid_judge_repeat' | 'cli.run.no_debias_length_active' | 'cli.run.invalid_bootstrap_samples' | 'cli.run.bootstrap_samples_too_large' | 'cli.run.dry_run_no_scores' | 'cli.run.skill_section' | 'cli.run.run_section' | 'cli.run.batch_complete' | 'cli.run.batch_verdict_header' | 'cli.run.batch_child_report_missing' | 'cli.run.eval_complete' | 'cli.run.tally' | 'cli.run.report_saved' | 'cli.run.evidence_recorded' | 'cli.run.evidence_recorded_unbound' | 'cli.run.report_only_gate_skipped' | 'cli.run.report_server_running' | 'cli.run.report_server_view' | 'cli.run.report_server_stop' | 'cli.run.no_serve_in_non_tty' | 'cli.run.no_serve_view_hint' | 'cli.run.gold_load_failed' | 'cli.run.gold_load_issue' | 'cli.run.contamination_warning' | 'cli.run.skip_connectivity_warning';
3
3
  export declare const runDict: Record<RunMessageKey, CliMessage>;
@@ -99,6 +99,14 @@ export const runDict = {
99
99
  zh: '📄 报告已保存到: {path}\n',
100
100
  en: '📄 Report saved to: {path}\n',
101
101
  },
102
+ 'cli.run.evidence_recorded': {
103
+ zh: '🔖 已为受管 skill「{name}」记录评测证据 → measurable\n',
104
+ en: '🔖 Recorded eval evidence for managed skill "{name}" → measurable\n',
105
+ },
106
+ 'cli.run.evidence_recorded_unbound': {
107
+ zh: '🔖 受管 skill「{name}」:评测内容与当前安装版本指纹不一致,证据已留存但不绑当前版本\n',
108
+ en: '🔖 Managed skill "{name}": eval content differs from the installed version; evidence kept but not bound to current\n',
109
+ },
102
110
  'cli.run.report_only_gate_skipped': {
103
111
  zh: 'ℹ 已启用 report-only 模式:保留 verdict 输出,但本次不使用 verdict 改写 exit code。\n',
104
112
  en: 'ℹ Report-only mode enabled: verdict is still printed, but it will not affect the exit code.\n',
@@ -24,6 +24,13 @@ import { configVariantsToSpecs } from '../../../inputs/eval-config.js';
24
24
  * 引导用户改用 --control-cwd / --treatment-cwd 或 eval.yaml 的 variant.cwd。git 修订语法 `@{...}`
25
25
  * 由 parseVariantCwd 保护、不误判。cwd 由调用方在边界注入(见 eval-runner)。 */
26
26
  function cliVariantSpec(rawExpr, role, cwd) {
27
+ // 远端 git 不走 CLI 字符串(URL 的 `:`/`@` 会被 parseGitInput / parseVariantCwd 误切)——
28
+ // 先于 @cwd guard 拦协议 URL 与 scp 形式 `git@host:path`,给明确的「走 eval.yaml」指引,
29
+ // 而非让 scp 形式落到下面 @cwd 的迁移报错(误导)。绝对本地路径 `/abs/skill.md` 不在此列。
30
+ if (/^(https?|ssh|git):\/\//i.test(rawExpr) || /^[\w.-]+@[\w.-]+:/.test(rawExpr)) {
31
+ throw new Error(`远端 git 源不支持经 --${role} 字符串传入: "${rawExpr}"。请在 eval.yaml 的 variant 上用结构化 `
32
+ + `git: { url, ref, spec } 字段(避开 URL 里的 : / @ 被误切),CLI 仅支持本地路径与 git:<ref>:<spec>。`);
33
+ }
27
34
  // 探测旧 name@cwd 形态报错。注:含合法 `@`(非 `@{`)的路径(如 /x/@dir/skill.md)会被一并
28
35
  // 误判 —— 属 pre-existing 限制(这类路径本就不被 @cwd 支持),报错文案仍指向迁移指引。
29
36
  if (parseVariantCwd(rawExpr).cwd !== undefined) {
@@ -26,10 +26,10 @@ export function resolveDoctorTargets(target, cwd) {
26
26
  const skillsDir = join(cwd, 'skills');
27
27
  if (existsSync(skillsDir) && statSync(skillsDir).isDirectory()) {
28
28
  const variants = discoverVariants(skillsDir).filter((v) => v !== 'baseline');
29
- return variants.length > 0 ? resolveArtifacts(skillsDir, variants, { strictBaseline: false }) : [];
29
+ return variants.length > 0 ? resolveArtifacts(skillsDir, variants, { strictBaseline: false, materialize: false }) : [];
30
30
  }
31
31
  const variants = discoverVariants(cwd).filter((v) => v !== 'baseline');
32
- return variants.length > 0 ? resolveArtifacts(cwd, variants, { strictBaseline: false }) : [];
32
+ return variants.length > 0 ? resolveArtifacts(cwd, variants, { strictBaseline: false, materialize: false }) : [];
33
33
  }
34
34
  const absTarget = resolve(target);
35
35
  if (!existsSync(absTarget)) {
@@ -38,7 +38,7 @@ export function resolveDoctorTargets(target, cwd) {
38
38
  const stat = statSync(absTarget);
39
39
  if (stat.isFile() && absTarget.endsWith('.md')) {
40
40
  // 单文件:用 file-path 模式(走 resolveArtifacts 的 "包含 /" 分支)
41
- return resolveArtifacts(dirname(absTarget), [absTarget], { strictBaseline: false });
41
+ return resolveArtifacts(dirname(absTarget), [absTarget], { strictBaseline: false, materialize: false });
42
42
  }
43
43
  if (stat.isDirectory()) {
44
44
  // 目录自身就是 directory-skill (含 SKILL.md): 按单个 skill 解析,
@@ -48,10 +48,10 @@ export function resolveDoctorTargets(target, cwd) {
48
48
  // skillRoot 丢失,assets/foo.md 这类相对依赖会锚到 doctor 的 cwd 而不是
49
49
  // skill 目录, 误报缺文件。
50
50
  if (existsSync(join(absTarget, 'SKILL.md'))) {
51
- return resolveArtifacts(dirname(absTarget), [basename(absTarget)], { strictBaseline: false });
51
+ return resolveArtifacts(dirname(absTarget), [basename(absTarget)], { strictBaseline: false, materialize: false });
52
52
  }
53
53
  const variants = discoverVariants(absTarget).filter((v) => v !== 'baseline');
54
- return variants.length > 0 ? resolveArtifacts(absTarget, variants, { strictBaseline: false }) : [];
54
+ return variants.length > 0 ? resolveArtifacts(absTarget, variants, { strictBaseline: false, materialize: false }) : [];
55
55
  }
56
56
  throw new Error(`doctor target must be a .md file or directory: ${target}`);
57
57
  }
@@ -2,8 +2,8 @@
2
2
  * Executor result cache.
3
3
  *
4
4
  * Caches successful executor results to disk to avoid redundant API calls.
5
- * Cache key v5 = sha256(model + system + prompt + cwd + allowedSkills + executor +
6
- * runtime + mocks + mocksStrict + effort).
5
+ * Cache key v6 = sha256(model + system + prompt + cwd + allowedSkills + executor +
6
+ * runtime + mocks + mocksStrict + effort + artifactContentHash).
7
7
  * Loaded into memory on init, flushed to disk on save().
8
8
  *
9
9
  * Prefix bumps intentionally invalidate old entries when construct-validity
@@ -15,6 +15,11 @@
15
15
  * 报告 meta 标的 effort 跟实际跑的 effort 不一致 — 测量可比性污染);
16
16
  * 同时 cache.set 不再砍 turns / toolCalls — 工具类断言 + diagnostic 要看 trace,
17
17
  * 砍掉的话 cached rerun 会让工具断言为空、diagnostic 没真实证据,跟 cold run 不一致
18
+ * - v6: artifact 内容指纹(contentHash)。`system` 只含 SKILL.md 正文,但本地 dir-skill 的
19
+ * references/ 资产是真实运行时输入(cwd=skillRoot,agent 可读),改资产只动 contentHash、
20
+ * 不动 system → 旧 key 会命中旧输出、贴到新 artifactHashes 上,形成静默测量污染。把
21
+ * contentHash 纳入 key,资产变即重跑。git skill 的 contentHash 只随 SKILL.md 变(其资产不
22
+ * 暴露给 executor、本就不该触发重跑),口径自洽
18
23
  */
19
24
  import type { ExecutorCache } from '../types/index.js';
20
25
  export declare function createCache(cacheDir: string): ExecutorCache;
@@ -27,4 +32,7 @@ mocksStrict?: boolean,
27
32
  /** Executor effort 也进 key:effort 'low'/'medium'/'high' 改变 LLM 思考预算,
28
33
  * 输出/工具调用/分数都可能不同,跨 effort 共享 cache 会让报告 meta 标的 effort
29
34
  * 跟实际生成的 effort 不一致,违反"两份报告比分数前先比 cliVersion / effort"语义。 */
30
- effort?: string): string;
35
+ effort?: string,
36
+ /** artifact 内容指纹(整树 / 单文件哈)。本地 dir-skill 改 references/ 资产只动此值、不动 system,
37
+ * 不进 key 会让改资产后命中旧输出 → 静默污染。空(baseline / 无 skill)等价无指纹。 */
38
+ artifactContentHash?: string): string;
@@ -2,8 +2,8 @@
2
2
  * Executor result cache.
3
3
  *
4
4
  * Caches successful executor results to disk to avoid redundant API calls.
5
- * Cache key v5 = sha256(model + system + prompt + cwd + allowedSkills + executor +
6
- * runtime + mocks + mocksStrict + effort).
5
+ * Cache key v6 = sha256(model + system + prompt + cwd + allowedSkills + executor +
6
+ * runtime + mocks + mocksStrict + effort + artifactContentHash).
7
7
  * Loaded into memory on init, flushed to disk on save().
8
8
  *
9
9
  * Prefix bumps intentionally invalidate old entries when construct-validity
@@ -15,6 +15,11 @@
15
15
  * 报告 meta 标的 effort 跟实际跑的 effort 不一致 — 测量可比性污染);
16
16
  * 同时 cache.set 不再砍 turns / toolCalls — 工具类断言 + diagnostic 要看 trace,
17
17
  * 砍掉的话 cached rerun 会让工具断言为空、diagnostic 没真实证据,跟 cold run 不一致
18
+ * - v6: artifact 内容指纹(contentHash)。`system` 只含 SKILL.md 正文,但本地 dir-skill 的
19
+ * references/ 资产是真实运行时输入(cwd=skillRoot,agent 可读),改资产只动 contentHash、
20
+ * 不动 system → 旧 key 会命中旧输出、贴到新 artifactHashes 上,形成静默测量污染。把
21
+ * contentHash 纳入 key,资产变即重跑。git skill 的 contentHash 只随 SKILL.md 变(其资产不
22
+ * 暴露给 executor、本就不该触发重跑),口径自洽
18
23
  */
19
24
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
20
25
  import { join } from 'node:path';
@@ -114,7 +119,10 @@ mocksStrict,
114
119
  /** Executor effort 也进 key:effort 'low'/'medium'/'high' 改变 LLM 思考预算,
115
120
  * 输出/工具调用/分数都可能不同,跨 effort 共享 cache 会让报告 meta 标的 effort
116
121
  * 跟实际生成的 effort 不一致,违反"两份报告比分数前先比 cliVersion / effort"语义。 */
117
- effort) {
122
+ effort,
123
+ /** artifact 内容指纹(整树 / 单文件哈)。本地 dir-skill 改 references/ 资产只动此值、不动 system,
124
+ * 不进 key 会让改资产后命中旧输出 → 静默污染。空(baseline / 无 skill)等价无指纹。 */
125
+ artifactContentHash) {
118
126
  // allowedSkills 序列化:undefined → "" / [] → "[]" / [...] → 排序后 JSON。
119
127
  // 排序保证 ["a","b"] 和 ["b","a"] 命中同一缓存(语义等价)。
120
128
  const isoStr = allowedSkills === undefined
@@ -130,8 +138,8 @@ effort) {
130
138
  // executor + runtime + effort 进 cache key:同 model 名走不同 executor 或同 executor
131
139
  // 换 binary/SDK 版本时输出可能不同,旧 cache 不可复用。
132
140
  const hash = createHash('sha256')
133
- .update(`${model || ''}\n${system || ''}\n${prompt || ''}\n${cwd || ''}\n${isoStr}\n${executor || ''}\n${runtimeFingerprint || ''}\n${mockStr}\n${strictStr}\n${effortStr}`)
141
+ .update(`${model || ''}\n${system || ''}\n${prompt || ''}\n${cwd || ''}\n${isoStr}\n${executor || ''}\n${runtimeFingerprint || ''}\n${mockStr}\n${strictStr}\n${effortStr}\n${artifactContentHash || ''}`)
134
142
  .digest('hex')
135
143
  .slice(0, 16);
136
- return `v5:${hash}`;
144
+ return `v6:${hash}`;
137
145
  }