oh-my-knowledge 0.48.0 → 0.49.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 (243) hide show
  1. package/README.md +50 -18
  2. package/README.zh.md +55 -23
  3. package/dist/analysis/coverage-analyzer.d.ts +1 -0
  4. package/dist/analysis/coverage-analyzer.js +125 -62
  5. package/dist/analysis/failure-clusterer.js +2 -1
  6. package/dist/analysis/gap-analyzer.d.ts +2 -2
  7. package/dist/analysis/gap-analyzer.js +13 -3
  8. package/dist/analysis/hedging-classifier.d.ts +2 -2
  9. package/dist/analysis/hedging-classifier.js +3 -4
  10. package/dist/analysis/report-diagnostics.js +9 -7
  11. package/dist/analysis/sample-diagnostics.js +6 -6
  12. package/dist/artifact-graph/doctor.js +15 -7
  13. package/dist/assets/agent-skills/omk/SKILL.md +27 -7
  14. package/dist/assets/agent-skills/omk/references/commands.md +18 -17
  15. package/dist/authoring/evolver.d.ts +10 -6
  16. package/dist/authoring/evolver.js +496 -83
  17. package/dist/authoring/generator.d.ts +3 -3
  18. package/dist/authoring/generator.js +5 -10
  19. package/dist/authoring/sample-fixer.d.ts +8 -6
  20. package/dist/authoring/sample-fixer.js +76 -5
  21. package/dist/cli/commands/doctor.js +31 -14
  22. package/dist/cli/commands/eval/index.d.ts +3 -0
  23. package/dist/cli/commands/eval/index.js +163 -17
  24. package/dist/cli/commands/evolve.d.ts +4 -4
  25. package/dist/cli/commands/evolve.js +27 -13
  26. package/dist/cli/commands/init.js +16 -3
  27. package/dist/cli/commands/observe/inbox.js +28 -21
  28. package/dist/cli/commands/observe/index.js +20 -11
  29. package/dist/cli/commands/observe/ingest.d.ts +3 -0
  30. package/dist/cli/commands/observe/ingest.js +30 -2
  31. package/dist/cli/commands/sample.d.ts +6 -3
  32. package/dist/cli/commands/sample.js +72 -68
  33. package/dist/cli/lib/codex-model-hint.d.ts +9 -0
  34. package/dist/cli/lib/codex-model-hint.js +45 -0
  35. package/dist/cli/lib/generation-failure-hint.d.ts +2 -0
  36. package/dist/cli/lib/generation-failure-hint.js +61 -0
  37. package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
  38. package/dist/cli/lib/i18n-dict/common.js +4 -0
  39. package/dist/cli/lib/i18n-dict/gen.d.ts +1 -1
  40. package/dist/cli/lib/i18n-dict/gen.js +38 -6
  41. package/dist/cli/lib/i18n-dict/help.js +6 -6
  42. package/dist/cli/lib/i18n-dict/init.d.ts +1 -1
  43. package/dist/cli/lib/i18n-dict/init.js +13 -9
  44. package/dist/cli/lib/i18n-dict/run.d.ts +1 -1
  45. package/dist/cli/lib/i18n-dict/run.js +34 -2
  46. package/dist/cli/lib/llm-failure-classifier.d.ts +2 -0
  47. package/dist/cli/lib/llm-failure-classifier.js +8 -0
  48. package/dist/cli/lib/parse-run-config.d.ts +6 -5
  49. package/dist/cli/lib/parse-run-config.js +16 -9
  50. package/dist/cli/lib/runtime-defaults.d.ts +21 -0
  51. package/dist/cli/lib/runtime-defaults.js +79 -0
  52. package/dist/diagnosis/observe-mapper.js +14 -15
  53. package/dist/diagnosis/observe-producer.js +3 -1
  54. package/dist/diagnosis/studio-projection.js +14 -7
  55. package/dist/diagnosis/types.d.ts +2 -0
  56. package/dist/diagnosis/types.js +12 -0
  57. package/dist/doctor/endpoint-rule.js +2 -1
  58. package/dist/eval-core/artifact-file-names.js +18 -1
  59. package/dist/eval-core/artifact-index.d.ts +7 -11
  60. package/dist/eval-core/artifact-index.js +139 -80
  61. package/dist/eval-core/cache.d.ts +12 -3
  62. package/dist/eval-core/cache.js +89 -29
  63. package/dist/eval-core/comparability.js +10 -6
  64. package/dist/eval-core/evaluation-execution.d.ts +2 -1
  65. package/dist/eval-core/evaluation-execution.js +122 -37
  66. package/dist/eval-core/evaluation-job.d.ts +4 -1
  67. package/dist/eval-core/evaluation-job.js +4 -1
  68. package/dist/eval-core/evaluation-reporting.d.ts +15 -13
  69. package/dist/eval-core/evaluation-reporting.js +54 -52
  70. package/dist/eval-core/execution-strategy.d.ts +2 -0
  71. package/dist/eval-core/execution-strategy.js +11 -9
  72. package/dist/eval-core/fact-checker.js +15 -7
  73. package/dist/eval-core/holdout.js +3 -2
  74. package/dist/eval-core/judge-independence.d.ts +2 -2
  75. package/dist/eval-core/mock-hook.cjs +23 -6
  76. package/dist/eval-core/mocks-runtime.js +30 -8
  77. package/dist/eval-core/report-document.d.ts +12 -0
  78. package/dist/eval-core/report-document.js +1151 -0
  79. package/dist/eval-core/report-extensions.d.ts +4 -0
  80. package/dist/eval-core/report-extensions.js +500 -0
  81. package/dist/eval-core/report-file-migration.js +7 -2
  82. package/dist/eval-core/resume-compatibility.d.ts +31 -0
  83. package/dist/eval-core/resume-compatibility.js +141 -0
  84. package/dist/eval-core/sample-fingerprint.d.ts +12 -0
  85. package/dist/eval-core/sample-fingerprint.js +193 -0
  86. package/dist/eval-core/schema.js +86 -31
  87. package/dist/eval-core/verdict.d.ts +8 -4
  88. package/dist/eval-core/verdict.js +24 -10
  89. package/dist/eval-workflows/batch-evaluation-workflow.d.ts +2 -1
  90. package/dist/eval-workflows/batch-evaluation-workflow.js +25 -12
  91. package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.d.ts +10 -5
  92. package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.js +58 -21
  93. package/dist/eval-workflows/evaluation-pipeline/report-finalize.js +3 -1
  94. package/dist/eval-workflows/evaluation-pipeline/run-state.d.ts +4 -1
  95. package/dist/eval-workflows/evaluation-pipeline/run-state.js +4 -1
  96. package/dist/eval-workflows/evaluation-pipeline/test-set-hash.d.ts +6 -5
  97. package/dist/eval-workflows/evaluation-pipeline/test-set-hash.js +17 -10
  98. package/dist/eval-workflows/evaluation-pipeline.js +12 -7
  99. package/dist/eval-workflows/run-evaluation.d.ts +9 -7
  100. package/dist/eval-workflows/run-evaluation.js +79 -51
  101. package/dist/executors/anthropic-api.js +65 -9
  102. package/dist/executors/claude-cli.js +16 -79
  103. package/dist/executors/claude-protocol.d.ts +28 -0
  104. package/dist/executors/claude-protocol.js +180 -0
  105. package/dist/executors/claude-sdk-trace.js +56 -28
  106. package/dist/executors/claude-sdk.d.ts +1 -0
  107. package/dist/executors/claude-sdk.js +39 -93
  108. package/dist/executors/codex-cli-trace.js +166 -31
  109. package/dist/executors/codex-cli.d.ts +6 -8
  110. package/dist/executors/codex-cli.js +49 -151
  111. package/dist/executors/codex-protocol.d.ts +24 -0
  112. package/dist/executors/codex-protocol.js +234 -0
  113. package/dist/executors/codex-sdk.js +68 -120
  114. package/dist/executors/gemini.js +88 -13
  115. package/dist/executors/index.d.ts +2 -3
  116. package/dist/executors/index.js +5 -3
  117. package/dist/executors/openai-api.js +70 -9
  118. package/dist/executors/runtime-fingerprint.js +88 -11
  119. package/dist/executors/script-command.d.ts +8 -0
  120. package/dist/executors/script-command.js +87 -0
  121. package/dist/executors/script.js +202 -29
  122. package/dist/executors/shared.d.ts +35 -3
  123. package/dist/executors/shared.js +113 -15
  124. package/dist/grading/assertions.d.ts +1 -1
  125. package/dist/grading/assertions.js +19 -9
  126. package/dist/grading/diagnostic.d.ts +9 -2
  127. package/dist/grading/diagnostic.js +25 -2
  128. package/dist/grading/index.js +10 -4
  129. package/dist/grading/judge.js +19 -6
  130. package/dist/grading/layered-scores.d.ts +2 -3
  131. package/dist/grading/layered-scores.js +2 -3
  132. package/dist/inputs/load-samples.d.ts +1 -2
  133. package/dist/inputs/load-samples.js +23 -1
  134. package/dist/inputs/mcp-resolver.js +6 -3
  135. package/dist/inputs/sample-document.d.ts +11 -0
  136. package/dist/inputs/sample-document.js +96 -0
  137. package/dist/managed/evidence.d.ts +1 -0
  138. package/dist/managed/evidence.js +1 -1
  139. package/dist/managed/store.js +200 -91
  140. package/dist/observability/codex-trace-adapter.d.ts +5 -0
  141. package/dist/observability/codex-trace-adapter.js +850 -0
  142. package/dist/observability/experience.d.ts +32 -6
  143. package/dist/observability/experience.js +2695 -459
  144. package/dist/observability/feedback-matchers.js +16 -1
  145. package/dist/observability/inbox-view-model.d.ts +1 -1
  146. package/dist/observability/inbox-view-model.js +19 -14
  147. package/dist/observability/inbox.d.ts +7 -1
  148. package/dist/observability/inbox.js +632 -124
  149. package/dist/observability/problem-patterns.js +2 -0
  150. package/dist/observability/review-state.d.ts +6 -0
  151. package/dist/observability/review-state.js +235 -63
  152. package/dist/observability/skill-chain-advisories.js +1 -1
  153. package/dist/observability/skill-chain.js +17 -4
  154. package/dist/observability/skill-health-analyzer.d.ts +32 -7
  155. package/dist/observability/skill-health-analyzer.js +194 -121
  156. package/dist/observability/skill-health-report.d.ts +10 -0
  157. package/dist/observability/skill-health-report.js +620 -0
  158. package/dist/observability/soft-standards/constants.d.ts +0 -1
  159. package/dist/observability/soft-standards/constants.js +0 -1
  160. package/dist/observability/soft-standards/index.d.ts +1 -1
  161. package/dist/observability/soft-standards/index.js +1 -1
  162. package/dist/observability/soft-standards/llm-extractor.js +8 -10
  163. package/dist/observability/soft-standards/skill-standards-store.d.ts +2 -1
  164. package/dist/observability/soft-standards/skill-standards-store.js +59 -18
  165. package/dist/observability/soft-standards/types.d.ts +2 -2
  166. package/dist/observability/trace-adapter.d.ts +12 -7
  167. package/dist/observability/trace-adapter.js +11 -9
  168. package/dist/observability/trace-attribution.d.ts +13 -5
  169. package/dist/observability/trace-attribution.js +315 -21
  170. package/dist/observability/trace-ingestion.d.ts +9 -0
  171. package/dist/observability/trace-ingestion.js +80 -0
  172. package/dist/observability/trace-ir.d.ts +113 -0
  173. package/dist/observability/trace-ir.js +87 -0
  174. package/dist/observability/trace-segmenter.d.ts +19 -6
  175. package/dist/observability/trace-segmenter.js +377 -196
  176. package/dist/observability/trace-session-index.d.ts +19 -0
  177. package/dist/observability/trace-session-index.js +68 -0
  178. package/dist/observability/trace-source.d.ts +12 -4
  179. package/dist/observability/trace-source.js +939 -215
  180. package/dist/renderer/html-renderer.js +37 -6
  181. package/dist/renderer/icons.js +3 -0
  182. package/dist/renderer/observation-inbox-renderer.js +208 -90
  183. package/dist/renderer/skill-detail-renderer.js +452 -109
  184. package/dist/renderer/skill-health-renderer.js +69 -12
  185. package/dist/renderer/summary.js +28 -7
  186. package/dist/renderer/table.js +21 -4
  187. package/dist/renderer/test-view.d.ts +1 -0
  188. package/dist/renderer/test-view.js +44 -9
  189. package/dist/server/indexed-report-store.js +14 -18
  190. package/dist/server/job-store.js +64 -26
  191. package/dist/server/report-server.js +190 -78
  192. package/dist/server/report-store.js +57 -80
  193. package/dist/server/skill-index.js +143 -49
  194. package/dist/server/skill-insights.js +44 -5
  195. package/dist/shared/artifact-graph.d.ts +3 -0
  196. package/dist/shared/artifact-graph.js +224 -0
  197. package/dist/shared/assertion-types.d.ts +8 -0
  198. package/dist/shared/assertion-types.js +46 -0
  199. package/dist/shared/atomic-json.d.ts +8 -0
  200. package/dist/shared/atomic-json.js +33 -0
  201. package/dist/shared/diagnosis-schema.d.ts +9 -0
  202. package/dist/shared/diagnosis-schema.js +181 -0
  203. package/dist/shared/doctor-report.d.ts +3 -0
  204. package/dist/shared/doctor-report.js +103 -0
  205. package/dist/shared/evaluation-job.d.ts +6 -0
  206. package/dist/shared/evaluation-job.js +217 -0
  207. package/dist/shared/executor-result.d.ts +17 -0
  208. package/dist/shared/executor-result.js +221 -0
  209. package/dist/shared/file-lock.d.ts +12 -0
  210. package/dist/shared/file-lock.js +129 -0
  211. package/dist/shared/json-value.d.ts +5 -0
  212. package/dist/shared/json-value.js +36 -0
  213. package/dist/shared/keyed-mutex.d.ts +7 -0
  214. package/dist/shared/keyed-mutex.js +24 -0
  215. package/dist/shared/record-count.d.ts +8 -0
  216. package/dist/shared/record-count.js +43 -0
  217. package/dist/shared/sample-contract.d.ts +3 -0
  218. package/dist/shared/sample-contract.js +332 -0
  219. package/dist/shared/timestamp.d.ts +6 -0
  220. package/dist/shared/timestamp.js +64 -0
  221. package/dist/shared/token-usage.d.ts +19 -0
  222. package/dist/shared/token-usage.js +50 -0
  223. package/dist/shared/tool-call-status.d.ts +8 -0
  224. package/dist/shared/tool-call-status.js +28 -0
  225. package/dist/shared/tool-identity.d.ts +21 -0
  226. package/dist/shared/tool-identity.js +84 -0
  227. package/dist/shared/tool-search.js +73 -16
  228. package/dist/shared/trace-projection.d.ts +5 -0
  229. package/dist/shared/trace-projection.js +20 -0
  230. package/dist/shared/trace-source-kind.d.ts +3 -0
  231. package/dist/shared/trace-source-kind.js +12 -0
  232. package/dist/types/diagnosis.d.ts +2 -0
  233. package/dist/types/eval.d.ts +4 -0
  234. package/dist/types/executor.d.ts +32 -5
  235. package/dist/types/index.d.ts +1 -0
  236. package/dist/types/index.js +1 -0
  237. package/dist/types/judge.d.ts +2 -0
  238. package/dist/types/observability.d.ts +116 -9
  239. package/dist/types/report.d.ts +58 -6
  240. package/dist/types/skill-index.d.ts +7 -0
  241. package/dist/types/trace.d.ts +2 -0
  242. package/dist/types/trace.js +1 -0
  243. package/package.json +9 -5
@@ -1,4 +1,5 @@
1
- import { asErrorLike, DEFAULT_TIMEOUT_MS, errorMessage } from './shared.js';
1
+ import { asErrorLike, DEFAULT_TIMEOUT_MS, errorMessage, readJsonResponse, responseBodyPreview, } from './shared.js';
2
+ import { optionalTokenCount, splitInclusiveInputTokens } from '../shared/token-usage.js';
2
3
  export async function openAiApiExecutor({ model, system, prompt, timeoutMs = DEFAULT_TIMEOUT_MS }) {
3
4
  const apiKey = process.env.OPENAI_API_KEY;
4
5
  if (!apiKey)
@@ -16,17 +17,77 @@ export async function openAiApiExecutor({ model, system, prompt, timeoutMs = DEF
16
17
  body: JSON.stringify({ model: model || 'gpt-4o', messages }),
17
18
  signal: AbortSignal.timeout(timeoutMs),
18
19
  });
19
- const data = await res.json();
20
+ const { data, rawBody } = await readJsonResponse(res);
20
21
  const durationMs = Date.now() - start;
21
22
  if (!res.ok) {
22
- return { ok: false, error: data.error?.message || `API error ${res.status}`, durationMs, durationApiMs: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUSD: 0, output: null, stopReason: 'error', numTurns: 0 };
23
+ const bodyPreview = responseBodyPreview(rawBody);
24
+ return { ok: false, error: data?.error?.message || `API error ${res.status}${bodyPreview ? `: ${bodyPreview}` : ''}`, durationMs, durationApiMs: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, tokenUsageReportedByExecutor: false, costUSD: 0, costReportedByExecutor: false, output: null, stopReason: 'error', numTurns: 0 };
25
+ }
26
+ if (!data) {
27
+ return { ok: false, error: 'OpenAI API returned an empty or non-JSON response', durationMs, durationApiMs: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, tokenUsageReportedByExecutor: false, costUSD: 0, costReportedByExecutor: false, output: null, stopReason: 'error', numTurns: 0 };
28
+ }
29
+ const usage = data.usage;
30
+ const rawInputTokens = optionalTokenCount(usage?.prompt_tokens);
31
+ const rawOutputTokens = optionalTokenCount(usage?.completion_tokens);
32
+ const rawCacheReadTokens = usage?.prompt_tokens_details?.cached_tokens === undefined
33
+ ? 0
34
+ : optionalTokenCount(usage.prompt_tokens_details.cached_tokens);
35
+ if (rawInputTokens === undefined
36
+ || rawOutputTokens === undefined
37
+ || rawCacheReadTokens === undefined
38
+ || rawCacheReadTokens > rawInputTokens) {
39
+ return {
40
+ ok: false,
41
+ error: 'OpenAI response contained missing or invalid token usage',
42
+ durationMs,
43
+ durationApiMs: 0,
44
+ inputTokens: 0,
45
+ outputTokens: 0,
46
+ cacheReadTokens: 0,
47
+ cacheCreationTokens: 0,
48
+ tokenUsageReportedByExecutor: false,
49
+ costUSD: 0,
50
+ costReportedByExecutor: false,
51
+ output: null,
52
+ stopReason: 'error',
53
+ numTurns: 1,
54
+ };
55
+ }
56
+ const normalizedInput = splitInclusiveInputTokens(rawInputTokens, rawCacheReadTokens);
57
+ const inputTokens = normalizedInput.inputTokens;
58
+ const outputTokens = rawOutputTokens;
59
+ const cacheReadTokens = normalizedInput.cacheReadTokens;
60
+ const choice = data.choices?.[0];
61
+ const content = choice?.message?.content;
62
+ if (typeof content !== 'string' || content.trim().length === 0) {
63
+ const refusal = choice?.message?.refusal?.trim();
64
+ return {
65
+ ok: false,
66
+ error: refusal
67
+ ? `OpenAI response refused the request: ${refusal}`
68
+ : 'OpenAI response did not contain assistant text',
69
+ durationMs,
70
+ durationApiMs: 0,
71
+ inputTokens,
72
+ outputTokens,
73
+ cacheReadTokens,
74
+ cacheCreationTokens: 0,
75
+ costUSD: 0,
76
+ costReportedByExecutor: false,
77
+ output: null,
78
+ stopReason: 'error',
79
+ numTurns: 1,
80
+ };
23
81
  }
24
- const usage = data.usage || {};
25
82
  return {
26
- ok: true, output: data.choices?.[0]?.message?.content || '', durationMs, durationApiMs: 0,
27
- inputTokens: usage.prompt_tokens || 0, outputTokens: usage.completion_tokens || 0,
28
- cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || 0, cacheCreationTokens: 0,
29
- costUSD: 0, stopReason: data.choices?.[0]?.finish_reason || 'unknown', numTurns: 1,
83
+ ok: true, output: content, durationMs, durationApiMs: 0,
84
+ // OpenAI reports cached tokens as a subset of prompt_tokens. Keep all
85
+ // executor buckets mutually exclusive so totals and cross-runtime trends
86
+ // do not count cache reads twice.
87
+ inputTokens, outputTokens,
88
+ cacheReadTokens, cacheCreationTokens: 0,
89
+ costUSD: 0, costReportedByExecutor: false,
90
+ stopReason: choice?.finish_reason || 'unknown', numTurns: 1,
30
91
  };
31
92
  }
32
93
  catch (err) {
@@ -34,6 +95,6 @@ export async function openAiApiExecutor({ model, system, prompt, timeoutMs = DEF
34
95
  const details = asErrorLike(err);
35
96
  const stopReason = details.name === 'TimeoutError' ? 'timeout' : 'error';
36
97
  const error = details.name === 'TimeoutError' ? `API request timed out after ${timeoutMs / 1000}s` : errorMessage(err);
37
- return { ok: false, error, durationMs, durationApiMs: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUSD: 0, output: null, stopReason, numTurns: 0 };
98
+ return { ok: false, error, durationMs, durationApiMs: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, tokenUsageReportedByExecutor: false, costUSD: 0, costReportedByExecutor: false, output: null, stopReason, numTurns: 0 };
38
99
  }
39
100
  }
@@ -1,10 +1,11 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
- import { existsSync, readFileSync } from 'node:fs';
3
+ import { existsSync, readFileSync, statSync } from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
5
  import { delimiter, dirname, isAbsolute, join } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { buildExecEnv } from './shared.js';
8
+ import { isScriptFileInterpreter, resolveScriptCommand, } from './script-command.js';
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
10
  const requireFromHere = createRequire(import.meta.url);
10
11
  const RUNTIME_CACHE = new Map();
@@ -95,7 +96,7 @@ function readPackageField(packageName, field, from) {
95
96
  return undefined;
96
97
  }
97
98
  }
98
- function readCommand(command, args, env, timeout = 1000) {
99
+ function readCommand(command, args, env, timeout = 3000) {
99
100
  try {
100
101
  const output = execFileSync(command, args, {
101
102
  encoding: 'utf-8',
@@ -140,12 +141,13 @@ function withFingerprint(input) {
140
141
  const stablePayload = {
141
142
  executor: input.executor,
142
143
  model: input.model,
143
- kind: input.runtimeKind,
144
+ runtimeKind: input.runtimeKind,
144
145
  binary: input.binary
145
146
  ? {
146
147
  name: input.binary.name,
147
148
  source: input.binary.source,
148
149
  version: input.binary.version,
150
+ contentHash: input.binary.contentHash,
149
151
  status: input.binary.version ? 'ok' : input.binary.error ? 'error' : 'missing',
150
152
  package: input.binary.package
151
153
  ? {
@@ -179,13 +181,91 @@ function runtime(executor, model, kind, capabilities, extra = {}) {
179
181
  function runtimeEnv(options) {
180
182
  return options?.env ?? buildExecEnv(options?.skillDir);
181
183
  }
184
+ function hashRuntimeFiles(paths) {
185
+ const files = [...new Set(paths)]
186
+ .filter((path) => {
187
+ try {
188
+ return statSync(path).isFile();
189
+ }
190
+ catch {
191
+ return false;
192
+ }
193
+ })
194
+ .sort();
195
+ if (files.length === 0)
196
+ return {};
197
+ const hash = createHash('sha256');
198
+ const errors = [];
199
+ for (const path of files) {
200
+ hash.update(path);
201
+ hash.update('\0');
202
+ try {
203
+ hash.update(readFileSync(path));
204
+ }
205
+ catch (err) {
206
+ const message = err instanceof Error ? err.message : String(err);
207
+ errors.push(`${path}: ${message}`);
208
+ hash.update(`<unreadable:${message}>`);
209
+ }
210
+ hash.update('\0');
211
+ }
212
+ return {
213
+ contentHash: hash.digest('hex'),
214
+ ...(errors.length > 0 && { error: errors.join('; ') }),
215
+ };
216
+ }
217
+ function scriptRuntime(executorName, model, env) {
218
+ const resolved = resolveScriptCommand(executorName);
219
+ const executablePath = resolvePathBinary(resolved.command, env);
220
+ const interpreter = isScriptFileInterpreter(resolved.command);
221
+ const executable = interpreter
222
+ ? readPathBinary(resolved.command, env)
223
+ : {
224
+ name: resolved.command,
225
+ source: executablePath ? 'path' : 'unknown',
226
+ ...(executablePath && { path: executablePath }),
227
+ ...(!executablePath && { error: 'executable not found on PATH' }),
228
+ };
229
+ const fileIdentity = hashRuntimeFiles([
230
+ ...resolved.referencedFiles,
231
+ ...(!interpreter && executablePath ? [executablePath] : []),
232
+ ]);
233
+ const errors = [executable.error, fileIdentity.error].filter(Boolean).join('; ');
234
+ return runtime(executorName, model, 'script', {
235
+ ...UNKNOWN_CAPABILITIES,
236
+ trace: 'best-effort',
237
+ skillIsolation: 'none',
238
+ }, {
239
+ binary: {
240
+ ...executable,
241
+ ...(fileIdentity.contentHash && {
242
+ contentHash: fileIdentity.contentHash,
243
+ }),
244
+ ...(errors && { error: errors }),
245
+ },
246
+ });
247
+ }
182
248
  export function getExecutorRuntimeFingerprint(executorName, model, options = {}) {
183
249
  const env = runtimeEnv(options);
250
+ if (![
251
+ 'claude',
252
+ 'claude-sdk',
253
+ 'codex',
254
+ 'codex-sdk',
255
+ 'gemini',
256
+ 'anthropic-api',
257
+ 'openai-api',
258
+ ].includes(executorName)) {
259
+ // Custom executors are local code. Re-read their referenced files so a
260
+ // long-running Studio process cannot reuse stale outputs after the script
261
+ // changes while the command line remains identical.
262
+ return scriptRuntime(executorName, model, env);
263
+ }
184
264
  const pathHash = hashString(env.PATH || '');
185
265
  const cacheKey = `${executorName}\0${model}\0${pathHash}`;
186
266
  const cached = RUNTIME_CACHE.get(cacheKey);
187
267
  if (cached)
188
- return cached;
268
+ return structuredClone(cached);
189
269
  let fp;
190
270
  switch (executorName) {
191
271
  case 'claude': {
@@ -265,12 +345,9 @@ export function getExecutorRuntimeFingerprint(executorName, model, options = {})
265
345
  }, { binary: { name: executorName, source: 'none' } });
266
346
  break;
267
347
  }
268
- default: {
269
- fp = runtime(executorName, model, 'script', UNKNOWN_CAPABILITIES, {
270
- binary: { name: executorName, source: 'unknown' },
271
- });
272
- }
348
+ default:
349
+ throw new Error(`unreachable executor runtime: ${executorName}`);
273
350
  }
274
- RUNTIME_CACHE.set(cacheKey, fp);
275
- return fp;
351
+ RUNTIME_CACHE.set(cacheKey, structuredClone(fp));
352
+ return structuredClone(fp);
276
353
  }
@@ -0,0 +1,8 @@
1
+ export interface ResolvedScriptCommand {
2
+ command: string;
3
+ args: string[];
4
+ /** Existing local files that can change the command's behavior. */
5
+ referencedFiles: string[];
6
+ }
7
+ export declare function isScriptFileInterpreter(commandPath: string): boolean;
8
+ export declare function resolveScriptCommand(commandLine: string, baseCwd?: string): ResolvedScriptCommand;
@@ -0,0 +1,87 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { basename, isAbsolute, resolve } from 'node:path';
3
+ const SCRIPT_FILE_INTERPRETERS = new Set([
4
+ 'node',
5
+ 'nodejs',
6
+ 'bash',
7
+ 'sh',
8
+ 'zsh',
9
+ 'ruby',
10
+ 'perl',
11
+ 'php',
12
+ 'bun',
13
+ 'deno',
14
+ ]);
15
+ const SCRIPTLESS_INTERPRETER_FLAGS_WITH_VALUE = new Set([
16
+ '-m',
17
+ '-c',
18
+ '-e',
19
+ '-p',
20
+ '--eval',
21
+ '--print',
22
+ ]);
23
+ export function isScriptFileInterpreter(commandPath) {
24
+ const name = basename(commandPath).toLowerCase().replace(/\.(exe|cmd|bat)$/, '');
25
+ return SCRIPT_FILE_INTERPRETERS.has(name) || /^python(?:\d+(?:\.\d+)*)?$/.test(name);
26
+ }
27
+ function isBareScriptFileArg(part) {
28
+ return !part.includes('/')
29
+ && !part.startsWith('.')
30
+ && /\.(?:cjs|cts|js|jsx|mjs|mts|php|pl|py|pyw|rb|sh|ts|tsx)$/i.test(part);
31
+ }
32
+ function isInlineScriptlessInterpreterFlag(arg) {
33
+ return /^(?:--eval=|--print=|-[mcep].+)/.test(arg);
34
+ }
35
+ function resolveExecutorPath(part, baseCwd, options = {}) {
36
+ if (isAbsolute(part))
37
+ return part;
38
+ if (part.startsWith('-'))
39
+ return part;
40
+ const pathLike = part.includes('/') || part.startsWith('.');
41
+ if (!pathLike && !options.resolveBare)
42
+ return part;
43
+ const candidate = resolve(baseCwd, part);
44
+ return existsSync(candidate) ? candidate : part;
45
+ }
46
+ function resolveExecutorArgs(args, commandPath, baseCwd) {
47
+ if (!isScriptFileInterpreter(commandPath)) {
48
+ return args.map((arg) => resolveExecutorPath(arg, baseCwd));
49
+ }
50
+ let scriptResolved = false;
51
+ let scriptPositionClosed = false;
52
+ let skipNext = false;
53
+ return args.map((arg) => {
54
+ if (skipNext) {
55
+ skipNext = false;
56
+ return arg;
57
+ }
58
+ if (!scriptResolved && !scriptPositionClosed) {
59
+ if (SCRIPTLESS_INTERPRETER_FLAGS_WITH_VALUE.has(arg)) {
60
+ skipNext = true;
61
+ scriptPositionClosed = true;
62
+ return arg;
63
+ }
64
+ if (isInlineScriptlessInterpreterFlag(arg)) {
65
+ scriptPositionClosed = true;
66
+ return arg;
67
+ }
68
+ if (arg.startsWith('-') && arg !== '--')
69
+ return arg;
70
+ if (arg === '--')
71
+ return arg;
72
+ scriptResolved = true;
73
+ return resolveExecutorPath(arg, baseCwd, {
74
+ resolveBare: isBareScriptFileArg(arg),
75
+ });
76
+ }
77
+ return resolveExecutorPath(arg, baseCwd);
78
+ });
79
+ }
80
+ export function resolveScriptCommand(commandLine, baseCwd = process.cwd()) {
81
+ const parts = commandLine.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [commandLine];
82
+ const command = resolveExecutorPath(parts[0].replace(/^["']|["']$/g, ''), baseCwd);
83
+ const rawArgs = parts.slice(1).map((arg) => arg.replace(/^["']|["']$/g, ''));
84
+ const args = resolveExecutorArgs(rawArgs, command, baseCwd);
85
+ const referencedFiles = [command, ...args].filter((part) => isAbsolute(part) && existsSync(part));
86
+ return { command, args, referencedFiles };
87
+ }
@@ -1,31 +1,102 @@
1
- import { existsSync } from 'node:fs';
2
- import { isAbsolute, resolve } from 'node:path';
3
1
  import { materializeForCliConfigDir } from '../eval-core/mocks-runtime.js';
2
+ import { executorResultValidationError, normalizeExecResultToolIdentities, } from '../shared/executor-result.js';
3
+ import { resolveScriptCommand } from './script-command.js';
4
4
  import { DEFAULT_TIMEOUT_MS, interruptedExecResult, spawnWithSigintPropagation, timeoutExecResult, } from './shared.js';
5
5
  // script executor 由用户自定义,omk 无法保证它实现 skill 隔离。
6
6
  // 任何 allowedSkills(包括 [])下都 stderr 一次性 warn,不阻塞执行,
7
7
  // 让用户知道 strict-baseline / 显式 allowedSkills 在 script executor 下静默无效。
8
8
  let scriptIsolationWarned = false;
9
- function resolveExecutorPath(part, baseCwd) {
10
- if (isAbsolute(part))
11
- return part;
12
- if (!part.includes('/') && !part.startsWith('.'))
13
- return part;
14
- const candidate = resolve(baseCwd, part);
15
- return existsSync(candidate) ? candidate : part;
9
+ function nonNegativeNumber(value) {
10
+ return typeof value === 'number'
11
+ && Number.isFinite(value)
12
+ && value >= 0
13
+ && value <= Number.MAX_SAFE_INTEGER
14
+ ? value
15
+ : 0;
16
+ }
17
+ function nonNegativeInteger(value) {
18
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
19
+ }
20
+ function scriptProtocolMetricsError(protocol) {
21
+ const integerFields = [
22
+ 'inputTokens',
23
+ 'outputTokens',
24
+ 'cacheReadTokens',
25
+ 'cacheCreationTokens',
26
+ 'numTurns',
27
+ ];
28
+ for (const field of integerFields) {
29
+ if (protocol[field] !== undefined && nonNegativeInteger(protocol[field]) !== protocol[field]) {
30
+ return `"${field}" must be a non-negative safe integer`;
31
+ }
32
+ }
33
+ for (const field of ['durationApiMs', 'costUSD']) {
34
+ if (protocol[field] !== undefined && nonNegativeNumber(protocol[field]) !== protocol[field]) {
35
+ return `"${field}" must be a non-negative finite number`;
36
+ }
37
+ }
38
+ return undefined;
39
+ }
40
+ function scriptProtocolReportsTokenUsage(protocol) {
41
+ return [
42
+ 'inputTokens',
43
+ 'outputTokens',
44
+ 'cacheReadTokens',
45
+ 'cacheCreationTokens',
46
+ ].every((field) => protocol[field] !== undefined);
47
+ }
48
+ function scriptProtocolTraceFields(protocol) {
49
+ return {
50
+ ...(protocol.turns !== undefined && {
51
+ turns: protocol.turns,
52
+ }),
53
+ ...(protocol.toolCalls !== undefined && {
54
+ toolCalls: protocol.toolCalls,
55
+ }),
56
+ ...(protocol.fullNumTurns !== undefined && {
57
+ fullNumTurns: protocol.fullNumTurns,
58
+ }),
59
+ ...(protocol.numSubAgents !== undefined && {
60
+ numSubAgents: protocol.numSubAgents,
61
+ }),
62
+ };
63
+ }
64
+ function invalidScriptProtocolResult(message, durationMs, mockStats) {
65
+ return {
66
+ ok: false,
67
+ error: `script executor returned malformed protocol JSON: ${message}`,
68
+ output: null,
69
+ durationMs,
70
+ durationApiMs: 0,
71
+ inputTokens: 0,
72
+ outputTokens: 0,
73
+ cacheReadTokens: 0,
74
+ cacheCreationTokens: 0,
75
+ tokenUsageReportedByExecutor: false,
76
+ costUSD: 0,
77
+ costReportedByExecutor: false,
78
+ stopReason: 'error',
79
+ numTurns: 0,
80
+ ...(mockStats && { mockStats }),
81
+ };
82
+ }
83
+ function validateScriptProtocolResult(result, mockStats) {
84
+ const validationError = executorResultValidationError(result);
85
+ if (validationError) {
86
+ return invalidScriptProtocolResult(validationError, result.durationMs, mockStats);
87
+ }
88
+ return normalizeExecResultToolIdentities(result);
16
89
  }
17
90
  export function createScriptExecutor(command) {
18
91
  const baseCwd = process.cwd();
19
- const parts = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [command];
20
- const cmd = resolveExecutorPath(parts[0].replace(/^["']|["']$/g, ''), baseCwd);
21
- const args = parts.slice(1)
22
- .map((a) => a.replace(/^["']|["']$/g, ''))
23
- .map((a) => resolveExecutorPath(a, baseCwd));
92
+ const resolved = resolveScriptCommand(command, baseCwd);
93
+ const cmd = resolved.command;
94
+ const args = resolved.args;
24
95
  return async function scriptExecutor({ model, system, prompt, cwd, timeoutMs = DEFAULT_TIMEOUT_MS, allowedSkills, mocks, mocksBaseDir, mocksStrict }) {
25
96
  if (allowedSkills !== undefined && !scriptIsolationWarned) {
26
97
  scriptIsolationWarned = true;
27
98
  process.stderr.write(`[omk] script executor 不参与 skill isolation:allowedSkills=${JSON.stringify(allowedSkills)} 在自定义 executor "${command}" 下无效。\n`
28
- + ` 如需 baseline 真正隔离,请用 --executor claude-sdk(或 claude-cli degraded mode)。\n`);
99
+ + ' 如需 baseline 真正隔离,请用 --executor codex。\n');
29
100
  }
30
101
  const input = JSON.stringify({ model, system: system || '', prompt });
31
102
  const start = Date.now();
@@ -64,31 +135,132 @@ export function createScriptExecutor(command) {
64
135
  timeoutMs,
65
136
  ...(cwd && { cwd }),
66
137
  });
67
- child.stdin?.write(input);
68
- child.stdin?.end();
138
+ child.stdin?.on('error', () => undefined);
139
+ child.stdin?.end(input);
69
140
  try {
70
141
  const r = await done;
71
142
  const durationMs = Date.now() - start;
72
143
  const ms = captureMockStats();
144
+ let protocol;
73
145
  try {
74
146
  const data = JSON.parse(r.stdout);
75
- return {
76
- ok: true, output: data.output || '', durationMs,
77
- durationApiMs: data.durationApiMs || 0,
78
- inputTokens: data.inputTokens || 0, outputTokens: data.outputTokens || 0,
79
- cacheReadTokens: data.cacheReadTokens || 0, cacheCreationTokens: data.cacheCreationTokens || 0,
80
- costUSD: data.costUSD || 0, stopReason: data.stopReason || 'end', numTurns: data.numTurns || 1,
81
- ...(ms && { mockStats: ms }),
82
- };
147
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
148
+ const candidate = data;
149
+ if (Object.hasOwn(candidate, 'ok') || Object.hasOwn(candidate, 'output')) {
150
+ protocol = candidate;
151
+ }
152
+ }
83
153
  }
84
154
  catch {
155
+ // Non-JSON stdout is the executor's plain model output.
156
+ }
157
+ if (protocol) {
158
+ const metricsError = scriptProtocolMetricsError(protocol);
159
+ if (metricsError) {
160
+ return invalidScriptProtocolResult(metricsError, durationMs, ms);
161
+ }
162
+ const costReported = protocol.costUSD !== undefined;
163
+ const tokenUsageReported = scriptProtocolReportsTokenUsage(protocol);
164
+ if (protocol.ok !== undefined && typeof protocol.ok !== 'boolean') {
165
+ return invalidScriptProtocolResult('"ok" must be boolean', durationMs, ms);
166
+ }
167
+ if (protocol.output !== undefined
168
+ && protocol.output !== null
169
+ && typeof protocol.output !== 'string') {
170
+ return invalidScriptProtocolResult('"output" must be string or null', durationMs, ms);
171
+ }
172
+ if (protocol.error !== undefined && typeof protocol.error !== 'string') {
173
+ return invalidScriptProtocolResult('"error" must be string when present', durationMs, ms);
174
+ }
175
+ const traceFields = scriptProtocolTraceFields(protocol);
176
+ if (protocol.ok === false) {
177
+ return validateScriptProtocolResult({
178
+ ok: false,
179
+ error: typeof protocol.error === 'string' && protocol.error
180
+ ? protocol.error
181
+ : 'script executor reported failure',
182
+ output: typeof protocol.output === 'string' ? protocol.output : null,
183
+ durationMs,
184
+ durationApiMs: nonNegativeNumber(protocol.durationApiMs),
185
+ inputTokens: nonNegativeInteger(protocol.inputTokens),
186
+ outputTokens: nonNegativeInteger(protocol.outputTokens),
187
+ cacheReadTokens: nonNegativeInteger(protocol.cacheReadTokens),
188
+ cacheCreationTokens: nonNegativeInteger(protocol.cacheCreationTokens),
189
+ ...(!tokenUsageReported && { tokenUsageReportedByExecutor: false }),
190
+ costUSD: nonNegativeNumber(protocol.costUSD),
191
+ ...(!costReported && { costReportedByExecutor: false }),
192
+ stopReason: typeof protocol.stopReason === 'string' ? protocol.stopReason : 'error',
193
+ numTurns: nonNegativeInteger(protocol.numTurns),
194
+ ...traceFields,
195
+ ...(ms && { mockStats: ms }),
196
+ }, ms);
197
+ }
198
+ if (typeof protocol.output !== 'string') {
199
+ return invalidScriptProtocolResult('"output" must be string', durationMs, ms);
200
+ }
201
+ if (!protocol.output.trim()) {
202
+ return {
203
+ ok: false,
204
+ error: 'script executor completed without model output',
205
+ output: null,
206
+ durationMs,
207
+ durationApiMs: nonNegativeNumber(protocol.durationApiMs),
208
+ inputTokens: nonNegativeInteger(protocol.inputTokens),
209
+ outputTokens: nonNegativeInteger(protocol.outputTokens),
210
+ cacheReadTokens: nonNegativeInteger(protocol.cacheReadTokens),
211
+ cacheCreationTokens: nonNegativeInteger(protocol.cacheCreationTokens),
212
+ ...(!tokenUsageReported && { tokenUsageReportedByExecutor: false }),
213
+ costUSD: nonNegativeNumber(protocol.costUSD),
214
+ ...(!costReported && { costReportedByExecutor: false }),
215
+ stopReason: 'error',
216
+ numTurns: protocol.numTurns === undefined ? 1 : nonNegativeInteger(protocol.numTurns),
217
+ ...(ms && { mockStats: ms }),
218
+ };
219
+ }
220
+ return validateScriptProtocolResult({
221
+ ok: true, output: protocol.output, durationMs,
222
+ durationApiMs: nonNegativeNumber(protocol.durationApiMs),
223
+ inputTokens: nonNegativeInteger(protocol.inputTokens),
224
+ outputTokens: nonNegativeInteger(protocol.outputTokens),
225
+ cacheReadTokens: nonNegativeInteger(protocol.cacheReadTokens),
226
+ cacheCreationTokens: nonNegativeInteger(protocol.cacheCreationTokens),
227
+ ...(!tokenUsageReported && { tokenUsageReportedByExecutor: false }),
228
+ costUSD: nonNegativeNumber(protocol.costUSD),
229
+ ...(!costReported && { costReportedByExecutor: false }),
230
+ stopReason: typeof protocol.stopReason === 'string' ? protocol.stopReason : 'end',
231
+ numTurns: protocol.numTurns === undefined ? 1 : nonNegativeInteger(protocol.numTurns),
232
+ ...traceFields,
233
+ ...(ms && { mockStats: ms }),
234
+ }, ms);
235
+ }
236
+ const output = r.stdout.trim();
237
+ if (!output) {
85
238
  return {
86
- ok: true, output: r.stdout.trim(), durationMs, durationApiMs: 0,
87
- inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0,
88
- costUSD: 0, stopReason: 'end', numTurns: 1,
239
+ ok: false,
240
+ error: 'script executor completed without model output',
241
+ output: null,
242
+ durationMs,
243
+ durationApiMs: 0,
244
+ inputTokens: 0,
245
+ outputTokens: 0,
246
+ cacheReadTokens: 0,
247
+ cacheCreationTokens: 0,
248
+ tokenUsageReportedByExecutor: false,
249
+ costUSD: 0,
250
+ costReportedByExecutor: false,
251
+ stopReason: 'error',
252
+ numTurns: 1,
89
253
  ...(ms && { mockStats: ms }),
90
254
  };
91
255
  }
256
+ return {
257
+ ok: true, output, durationMs, durationApiMs: 0,
258
+ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0,
259
+ tokenUsageReportedByExecutor: false,
260
+ costUSD: 0, costReportedByExecutor: false,
261
+ stopReason: 'end', numTurns: 1,
262
+ ...(ms && { mockStats: ms }),
263
+ };
92
264
  }
93
265
  catch (err) {
94
266
  const durationMs = Date.now() - start;
@@ -104,7 +276,8 @@ export function createScriptExecutor(command) {
104
276
  error: stderr || details.message || `executor exited with code ${details.code ?? '?'}`,
105
277
  durationMs, durationApiMs: 0,
106
278
  inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0,
107
- costUSD: 0, output: null, stopReason: 'error', numTurns: 0,
279
+ tokenUsageReportedByExecutor: false,
280
+ costUSD: 0, costReportedByExecutor: false, output: null, stopReason: 'error', numTurns: 0,
108
281
  ...(ms && { mockStats: ms }),
109
282
  };
110
283
  }