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.
- package/README.md +3 -0
- package/README.zh.md +3 -0
- package/dist/assets/agent-skills/omk/references/commands.md +9 -0
- package/dist/authoring/evolver.js +12 -3
- package/dist/cli/commands/eval/index.d.ts +1 -0
- package/dist/cli/commands/eval/index.js +36 -2
- package/dist/cli/commands/install.d.ts +2 -0
- package/dist/cli/commands/install.js +45 -20
- package/dist/cli/commands/sample.d.ts +1 -1
- package/dist/cli/commands/sample.js +25 -12
- package/dist/cli/lib/cmd-flags.d.ts +1 -0
- package/dist/cli/lib/i18n-dict/install.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/install.js +20 -0
- package/dist/cli/lib/i18n-dict/run.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/run.js +8 -0
- package/dist/cli/lib/parse-run-config/variant-resolution.js +7 -0
- package/dist/doctor/index.js +5 -5
- package/dist/eval-core/cache.d.ts +11 -3
- package/dist/eval-core/cache.js +13 -5
- package/dist/eval-core/dependency-checker.js +5 -3
- package/dist/eval-core/evaluation-execution.js +4 -1
- package/dist/eval-core/evaluation-reporting.js +17 -8
- package/dist/eval-core/execution-strategy.js +6 -4
- package/dist/eval-core/task-planner.js +2 -1
- package/dist/eval-workflows/evaluation-preparation.js +3 -1
- package/dist/inputs/content-hash.d.ts +28 -0
- package/dist/inputs/content-hash.js +106 -0
- package/dist/inputs/eval-config.js +44 -11
- package/dist/inputs/materialize-copy.d.ts +37 -0
- package/dist/inputs/materialize-copy.js +193 -0
- package/dist/inputs/skill-loader.d.ts +65 -2
- package/dist/inputs/skill-loader.js +308 -13
- package/dist/inputs/source-resolver.d.ts +16 -9
- package/dist/inputs/source-resolver.js +57 -77
- package/dist/managed/evidence.d.ts +22 -0
- package/dist/managed/evidence.js +143 -0
- package/dist/managed/index.d.ts +1 -0
- package/dist/managed/index.js +1 -0
- package/dist/managed/store.d.ts +16 -20
- package/dist/managed/store.js +40 -76
- package/dist/renderer/layout.js +2 -2
- package/dist/types/eval.d.ts +12 -1
- package/dist/types/managed.d.ts +29 -4
- package/dist/types/report.d.ts +17 -3
- 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 =
|
|
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
|
-
|
|
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,
|
|
1
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, realpathSync, rmSync, statSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { basename, dirname,
|
|
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,
|
|
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
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
filter: (
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
52
|
+
const { report, treatmentName, currentContentHash, samples, sampleIds } = params;
|
|
52
53
|
const lang = params.lang ?? 'zh';
|
|
53
54
|
const issues = [];
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
?
|
|
59
|
-
: `Report
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
216
|
+
currentContentHash,
|
|
204
217
|
samples,
|
|
205
218
|
sampleIds: sampleDesignIds,
|
|
206
219
|
lang,
|
|
@@ -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) {
|
package/dist/doctor/index.js
CHANGED
|
@@ -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
|
|
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
|
|
35
|
+
effort?: string,
|
|
36
|
+
/** artifact 内容指纹(整树 / 单文件哈)。本地 dir-skill 改 references/ 资产只动此值、不动 system,
|
|
37
|
+
* 不进 key 会让改资产后命中旧输出 → 静默污染。空(baseline / 无 skill)等价无指纹。 */
|
|
38
|
+
artifactContentHash?: string): string;
|
package/dist/eval-core/cache.js
CHANGED
|
@@ -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
|
|
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 `
|
|
144
|
+
return `v6:${hash}`;
|
|
137
145
|
}
|