@kodax-ai/kodax 0.7.95 → 0.7.96-alpha.2

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 (163) hide show
  1. package/CHANGELOG.md +178 -1
  2. package/LICENSE +158 -158
  3. package/README.md +149 -24
  4. package/README_CN.md +117 -24
  5. package/config-templates/config.example.jsonc +2 -1
  6. package/config-templates/integrations/a2a.example.jsonc +91 -91
  7. package/config-templates/integrations/extensions.example.jsonc +7 -7
  8. package/config-templates/integrations/mcp.example.jsonc +16 -16
  9. package/dist/builtin/code-review/SKILL.md +82 -82
  10. package/dist/builtin/git-workflow/SKILL.md +84 -84
  11. package/dist/builtin/skill-creator/SKILL.md +127 -127
  12. package/dist/builtin/skill-creator/agents/analyzer.md +12 -12
  13. package/dist/builtin/skill-creator/agents/comparator.md +13 -13
  14. package/dist/builtin/skill-creator/agents/grader.md +13 -13
  15. package/dist/builtin/skill-creator/references/schemas.md +227 -227
  16. package/dist/builtin/skill-creator/scripts/aggregate-benchmark.d.ts +46 -46
  17. package/dist/builtin/skill-creator/scripts/aggregate-benchmark.js +208 -208
  18. package/dist/builtin/skill-creator/scripts/analyze-benchmark.d.ts +46 -46
  19. package/dist/builtin/skill-creator/scripts/analyze-benchmark.js +286 -286
  20. package/dist/builtin/skill-creator/scripts/compare-runs.d.ts +62 -62
  21. package/dist/builtin/skill-creator/scripts/compare-runs.js +330 -330
  22. package/dist/builtin/skill-creator/scripts/generate-review.d.ts +33 -33
  23. package/dist/builtin/skill-creator/scripts/generate-review.js +414 -414
  24. package/dist/builtin/skill-creator/scripts/grade-evals.d.ts +73 -73
  25. package/dist/builtin/skill-creator/scripts/grade-evals.js +402 -402
  26. package/dist/builtin/skill-creator/scripts/improve-description.d.ts +23 -23
  27. package/dist/builtin/skill-creator/scripts/improve-description.js +160 -160
  28. package/dist/builtin/skill-creator/scripts/init-skill.d.ts +14 -14
  29. package/dist/builtin/skill-creator/scripts/init-skill.js +155 -155
  30. package/dist/builtin/skill-creator/scripts/install-skill.d.ts +29 -29
  31. package/dist/builtin/skill-creator/scripts/install-skill.js +173 -173
  32. package/dist/builtin/skill-creator/scripts/package-skill.d.ts +38 -38
  33. package/dist/builtin/skill-creator/scripts/package-skill.js +121 -121
  34. package/dist/builtin/skill-creator/scripts/quick-validate.d.ts +8 -8
  35. package/dist/builtin/skill-creator/scripts/quick-validate.js +163 -163
  36. package/dist/builtin/skill-creator/scripts/run-eval.d.ts +66 -66
  37. package/dist/builtin/skill-creator/scripts/run-eval.js +353 -353
  38. package/dist/builtin/skill-creator/scripts/run-loop.d.ts +49 -49
  39. package/dist/builtin/skill-creator/scripts/run-loop.js +242 -242
  40. package/dist/builtin/skill-creator/scripts/run-trigger-eval.d.ts +58 -58
  41. package/dist/builtin/skill-creator/scripts/run-trigger-eval.js +224 -224
  42. package/dist/builtin/tdd/SKILL.md +56 -56
  43. package/dist/chunks/agent-4FABF6WK.js +2 -0
  44. package/dist/chunks/argument-completer-72VHB2VL.js +2 -0
  45. package/dist/chunks/{chunk-7JXA3533.js → chunk-4OIQZWA5.js} +223 -226
  46. package/dist/chunks/{chunk-BNVSKIAB.js → chunk-6BVF3HZ7.js} +136 -136
  47. package/dist/chunks/chunk-7JZZIFZH.js +501 -0
  48. package/dist/chunks/chunk-AC7Z2OIY.js +2 -0
  49. package/dist/chunks/{chunk-JS452J2F.js → chunk-C4KYZAFU.js} +2 -2
  50. package/dist/chunks/chunk-CHUPIWIF.js +251 -0
  51. package/dist/chunks/{chunk-C4DTUKTR.js → chunk-GYUU7XLH.js} +5 -4
  52. package/dist/chunks/{chunk-OK4AXDC5.js → chunk-HAX55GOQ.js} +1 -1
  53. package/dist/chunks/chunk-JKTJCGO2.js +418 -0
  54. package/dist/chunks/{chunk-O743AJ5V.js → chunk-LQYETRFK.js} +171 -175
  55. package/dist/chunks/{chunk-2PEDBGKN.js → chunk-LTUZTQ6W.js} +1 -1
  56. package/dist/chunks/chunk-LYWDUSKO.js +2 -0
  57. package/dist/chunks/chunk-NGH6T4FD.js +92 -0
  58. package/dist/chunks/{chunk-SRIJX5ZP.js → chunk-NVRWCSA5.js} +1 -1
  59. package/dist/chunks/chunk-O4DS7RIQ.js +384 -0
  60. package/dist/chunks/chunk-ORXU6OWZ.js +2 -0
  61. package/dist/chunks/chunk-POTW3O65.js +124 -0
  62. package/dist/chunks/{chunk-2TM3W3GK.js → chunk-RIQSS56Y.js} +1 -1
  63. package/dist/chunks/chunk-U27XKEN5.js +407 -0
  64. package/dist/chunks/{chunk-6ABQJUK6.js → chunk-XUX6OUCA.js} +2 -2
  65. package/dist/chunks/{client-JDOS3YJR.js → client-4C456K4O.js} +1 -1
  66. package/dist/chunks/compaction-config-3VQRDFF4.js +2 -0
  67. package/dist/chunks/{construction-bootstrap-YCWHHUYO.js → construction-bootstrap-AHN7EDGG.js} +1 -1
  68. package/dist/chunks/dist-BCBQXAGI.js +2 -0
  69. package/dist/chunks/dist-QK2YDWU6.js +2 -0
  70. package/dist/chunks/host-VIHV7ADG.js +2 -0
  71. package/dist/chunks/run-manager-NBVMU3KV.js +2 -0
  72. package/dist/chunks/utils-MEJZ2IGI.js +2 -0
  73. package/dist/index.d.ts +22 -21
  74. package/dist/index.js +7 -7
  75. package/dist/kodax_bootstrap.js +26 -26
  76. package/dist/kodax_cli.js +1905 -1882
  77. package/dist/native/darwin-arm64/LICENSE-APACHE.txt +13 -0
  78. package/dist/native/darwin-arm64/kodax-text-transaction.node +0 -0
  79. package/dist/native/darwin-arm64/manifest.json +16 -0
  80. package/dist/native/darwin-x64/LICENSE-APACHE.txt +13 -0
  81. package/dist/native/darwin-x64/kodax-text-transaction.node +0 -0
  82. package/dist/native/darwin-x64/manifest.json +16 -0
  83. package/dist/native/linux-arm64/LICENSE-APACHE.txt +13 -0
  84. package/dist/native/linux-arm64/kodax-text-transaction.node +0 -0
  85. package/dist/native/linux-arm64/manifest.json +16 -0
  86. package/dist/native/linux-x64/LICENSE-APACHE.txt +13 -0
  87. package/dist/native/linux-x64/kodax-text-transaction.node +0 -0
  88. package/dist/native/linux-x64/manifest.json +16 -0
  89. package/dist/native/win32-x64/LICENSE-APACHE.txt +13 -0
  90. package/dist/native/win32-x64/NOTICE-windows-sandbox.txt +8 -0
  91. package/dist/native/win32-x64/kodax-windows-sandbox.exe +0 -0
  92. package/dist/native/win32-x64/kodax-windows-text-transaction.node +0 -0
  93. package/dist/native/win32-x64/manifest.json +30 -0
  94. package/dist/provider-capabilities.json +108 -2
  95. package/dist/runtime-worker.js +1745 -1727
  96. package/dist/sandbox-network-broker.js +1715 -0
  97. package/dist/sdk-a2a.d.ts +13 -12
  98. package/dist/sdk-a2a.js +1 -1
  99. package/dist/sdk-agent.d.ts +27 -18
  100. package/dist/sdk-agent.js +1 -1
  101. package/dist/sdk-coding.d.ts +107 -58
  102. package/dist/sdk-coding.js +1 -1
  103. package/dist/sdk-experimental-memory.d.ts +4 -4
  104. package/dist/sdk-experimental-memory.js +1 -1
  105. package/dist/sdk-llm.d.ts +9 -10
  106. package/dist/sdk-llm.js +1 -1
  107. package/dist/sdk-mcp.js +1 -1
  108. package/dist/sdk-media.d.ts +1 -1
  109. package/dist/sdk-media.js +1 -1
  110. package/dist/sdk-repl.d.ts +25 -17
  111. package/dist/sdk-repl.js +1 -1
  112. package/dist/sdk-runtime.d.ts +41 -17
  113. package/dist/sdk-runtime.js +1 -1
  114. package/dist/sdk-sandbox.d.ts +17 -3
  115. package/dist/sdk-sandbox.js +1 -1
  116. package/dist/sdk-session.d.ts +8 -7
  117. package/dist/sdk-session.js +1 -1
  118. package/dist/sdk-skills.js +1 -1
  119. package/dist/semantic-worker.js +61 -61
  120. package/dist/types-chunks/{base.d-vopO1rml.d.ts → base.d-COo8E2dk.d.ts} +18 -27
  121. package/dist/types-chunks/{bash-prefix-extractor.d-B2rxNI4L.d.ts → bash-prefix-extractor.d-DQOlA8cy.d.ts} +72 -52
  122. package/dist/types-chunks/{capability-learning.d-CE1tm29C.d.ts → capability-learning.d-lRPgA3Or.d.ts} +1 -1
  123. package/dist/types-chunks/{capsule.d-Y7QRS4J2.d.ts → capsule.d-0zxsgXg_.d.ts} +3 -3
  124. package/dist/types-chunks/{controller.d-BR-UVeUR.d.ts → controller.d-BTtyIIC1.d.ts} +1 -1
  125. package/dist/types-chunks/{controller.d-C5noq5-w.d.ts → controller.d-C7tXUcXo.d.ts} +2 -2
  126. package/dist/types-chunks/{guardrail.d-DqHD-61O.d.ts → guardrail.d-DKTyEHJa.d.ts} +5 -5
  127. package/dist/types-chunks/{history-retrieval.d-CKU8zmt_.d.ts → history-retrieval.d-CHnL99S7.d.ts} +2 -2
  128. package/dist/types-chunks/{public-api.d-D7fXaWW8.d.ts → public-api.d-Dx3L3W4Z.d.ts} +7 -4
  129. package/dist/types-chunks/{repl.d-BYDb14s6.d.ts → repl.d-mO01olIa.d.ts} +5 -5
  130. package/dist/types-chunks/{resolver.d-CwHUwbdE.d.ts → resolver.d-DlXPjeyx.d.ts} +38 -6
  131. package/dist/types-chunks/{review-inbox.d-BSgFewyj.d.ts → review-inbox.d-WDSe1nze.d.ts} +1 -1
  132. package/dist/types-chunks/{run-manager.d-BVJSntpS.d.ts → run-manager.d-DENbbzgR.d.ts} +1 -1
  133. package/dist/types-chunks/{sdk-session-CqEG84oP.d.ts → sdk-session-DYHGXXH4.d.ts} +3 -3
  134. package/dist/types-chunks/{shell-command-sets.d-DqaaBwhU.d.ts → shell-command-sets.d-CjFqS4dp.d.ts} +1 -1
  135. package/dist/types-chunks/{side-query.d-iH0P9ASy.d.ts → side-query.d-j8A3sFv_.d.ts} +2 -2
  136. package/dist/types-chunks/{types.d-rUOjXych.d.ts → types.d-BbZjR0Wr.d.ts} +1 -1
  137. package/dist/types-chunks/{types.d-CaTXAW_Q.d.ts → types.d-BkTIKdH6.d.ts} +2 -2
  138. package/dist/types-chunks/{types.d-C5xQEFcj.d.ts → types.d-DncLrpu_.d.ts} +4 -4
  139. package/dist/types-chunks/{types.d-CK9A9rpP.d.ts → types.d-Xy0f3x4i.d.ts} +9 -0
  140. package/dist/types-chunks/{utils.d-CWUyLiXo.d.ts → utils.d-CnxefMos.d.ts} +6 -6
  141. package/package.json +10 -3
  142. package/public_docs/README.md +130 -0
  143. package/public_docs/configuration/sandbox.md +289 -0
  144. package/public_docs/sdk/embedder-guide.md +6674 -0
  145. package/scripts/kodax-bin.cjs +28 -28
  146. package/scripts/production-env.cjs +25 -25
  147. package/dist/chunks/agent-XXZMXKKA.js +0 -2
  148. package/dist/chunks/argument-completer-KCYDGTQW.js +0 -2
  149. package/dist/chunks/chunk-77PNP27P.js +0 -92
  150. package/dist/chunks/chunk-7QBWHCIZ.js +0 -2
  151. package/dist/chunks/chunk-BHE66SP3.js +0 -123
  152. package/dist/chunks/chunk-BR2OSB2I.js +0 -722
  153. package/dist/chunks/chunk-DSMONSVB.js +0 -407
  154. package/dist/chunks/chunk-Q5VRWSKG.js +0 -418
  155. package/dist/chunks/chunk-XWRPOGJN.js +0 -4
  156. package/dist/chunks/chunk-Y5VIQBT6.js +0 -383
  157. package/dist/chunks/compaction-config-EGS4577X.js +0 -2
  158. package/dist/chunks/dist-ITTXQHRJ.js +0 -2
  159. package/dist/chunks/dist-KFYN3OKD.js +0 -2
  160. package/dist/chunks/host-VAYADIPW.js +0 -2
  161. package/dist/chunks/run-manager-52IJZMZJ.js +0 -2
  162. package/dist/chunks/utils-P7PBSL3R.js +0 -2
  163. package/dist/sandbox-workspace-session.js +0 -1705
@@ -1,334 +1,334 @@
1
- #!/usr/bin/env node
2
-
3
- import { readdir, readFile, writeFile } from 'node:fs/promises';
4
- import path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import {
7
- extractJsonObject,
8
- loadKodaXSDK,
9
- loadRelativeText,
10
- readJsonFile,
11
- truncateText,
12
- } from './utils.js';
13
-
14
- async function listDirectories(dirPath) {
15
- const entries = await readdir(dirPath, { withFileTypes: true }).catch(() => []);
16
- return entries
17
- .filter((entry) => entry.isDirectory())
18
- .map((entry) => path.join(dirPath, entry.name))
19
- .sort((left, right) => left.localeCompare(right));
20
- }
21
-
22
- function normalizeStringArray(value) {
23
- if (!Array.isArray(value)) {
24
- return [];
25
- }
26
- return value
27
- .map((item) => String(item ?? '').trim())
28
- .filter(Boolean);
29
- }
30
-
31
- function normalizeComparisonResult(rawText) {
32
- const parsed = extractJsonObject(rawText) ?? {};
33
- const winner = ['A', 'B', 'tie', 'inconclusive'].includes(parsed.winner)
34
- ? parsed.winner
35
- : 'inconclusive';
36
- const confidenceValue = Number(parsed.confidence);
37
-
38
- return {
39
- winner,
40
- confidence: Number.isFinite(confidenceValue)
41
- ? Math.max(0, Math.min(1, Number(confidenceValue.toFixed(4))))
42
- : 0,
43
- rationale: String(parsed.rationale ?? '').trim(),
44
- strengths_a: normalizeStringArray(parsed.strengths_a),
45
- strengths_b: normalizeStringArray(parsed.strengths_b),
46
- risks: normalizeStringArray(parsed.risks),
47
- };
48
- }
49
-
50
- async function loadComparisonPair(evalDir, configA, configB, pairIndex) {
51
- const runsA = (await listDirectories(path.join(evalDir, configA)))
52
- .filter((dirPath) => path.basename(dirPath).startsWith('run-'));
53
- const runsB = (await listDirectories(path.join(evalDir, configB)))
54
- .filter((dirPath) => path.basename(dirPath).startsWith('run-'));
55
- const left = runsA[pairIndex];
56
- const right = runsB[pairIndex];
57
-
58
- if (!left || !right) {
59
- return null;
60
- }
61
-
62
- const evalMetadata = await readJsonFile(path.join(evalDir, 'eval_metadata.json'), {});
63
- const outputA = await readFile(path.join(left, 'outputs', 'result.md'), 'utf8').catch(() => '');
64
- const outputB = await readFile(path.join(right, 'outputs', 'result.md'), 'utf8').catch(() => '');
65
-
66
- return {
67
- evalDir,
68
- evalMetadata,
69
- runA: {
70
- runDir: left,
71
- runId: path.basename(left),
72
- configName: configA,
73
- output: outputA,
74
- },
75
- runB: {
76
- runDir: right,
77
- runId: path.basename(right),
78
- configName: configB,
79
- output: outputB,
80
- },
81
- };
82
- }
83
-
84
- export function buildComparisonPrompt(input) {
85
- const pair = input.presentPrimaryFirst
86
- ? { A: input.runA, B: input.runB }
87
- : { A: input.runB, B: input.runA };
88
-
89
- return `${input.agentInstructions.trim()}
90
-
91
- Return JSON with this shape:
92
- {
93
- "winner": "A | B | tie | inconclusive",
94
- "confidence": 0.0,
95
- "rationale": "short explanation",
96
- "strengths_a": [],
97
- "strengths_b": [],
98
- "risks": []
99
- }
100
-
101
- Judge only the visible outputs. Do not mention hidden config names in the rationale.
102
-
103
- ## Eval Prompt
104
- ${truncateText(input.evalMetadata.prompt ?? '', 4000)}
105
-
106
- ## Expected Outcome
107
- ${truncateText(input.evalMetadata.expected_output ?? '', 2000)}
108
-
109
- ## Assertions
110
- ${truncateText(JSON.stringify(input.evalMetadata.assertions ?? [], null, 2), 4000)}
111
-
112
- ## Candidate A
113
- ${truncateText(pair.A.output, 12000)}
114
-
115
- ## Candidate B
116
- ${truncateText(pair.B.output, 12000)}
117
- `;
118
- }
119
-
120
- async function defaultRunComparator(prompt, options) {
121
- const { runKodaX } = await loadKodaXSDK();
122
- const result = await runKodaX(
123
- {
124
- provider: options.provider ?? 'anthropic',
125
- model: options.model,
126
- maxIter: options.maxIter ?? 20,
127
- reasoningMode: options.reasoningMode ?? 'balanced',
128
- thinking: options.reasoningMode ? options.reasoningMode !== 'off' : true,
129
- context: {
130
- gitRoot: path.resolve(options.cwd ?? options.workspaceDir ?? process.cwd()),
131
- },
132
- },
133
- prompt
134
- );
135
- return result.lastText;
136
- }
137
-
138
- function mapWinnerToConfig(winner, presentPrimaryFirst, configA, configB) {
139
- if (winner === 'A') {
140
- return presentPrimaryFirst ? configA : configB;
141
- }
142
- if (winner === 'B') {
143
- return presentPrimaryFirst ? configB : configA;
144
- }
145
- return winner;
146
- }
147
-
148
- export function renderComparisonMarkdown(document) {
149
- const lines = [
150
- `# Blind Comparison: ${document.config_a} vs ${document.config_b}`,
151
- '',
152
- `Generated: ${document.generated_at}`,
153
- '',
154
- `- ${document.config_a} wins: ${document.summary.config_a_wins}`,
155
- `- ${document.config_b} wins: ${document.summary.config_b_wins}`,
156
- `- ties: ${document.summary.ties}`,
157
- `- inconclusive: ${document.summary.inconclusive}`,
158
- '',
159
- ];
160
-
161
- for (const comparison of document.comparisons) {
162
- lines.push(`## Eval ${comparison.eval_id ?? comparison.eval_name ?? comparison.index}`);
163
- lines.push('');
164
- lines.push(`- Winner: ${comparison.winner_config}`);
165
- lines.push(`- Confidence: ${comparison.confidence}`);
166
- lines.push(`- Rationale: ${comparison.rationale || 'n/a'}`);
167
- lines.push('');
168
- }
169
-
170
- return `${lines.join('\n')}\n`;
171
- }
172
-
173
- export async function compareWorkspace(
174
- options,
175
- runner = defaultRunComparator
176
- ) {
177
- const workspaceDir = path.resolve(options.workspaceDir);
178
- const configA = options.configA ?? 'with_skill';
179
- const configB = options.configB ?? 'without_skill';
180
- const maxPairs = Number.isFinite(options.maxPairs) && options.maxPairs > 0
181
- ? Math.floor(options.maxPairs)
182
- : Number.POSITIVE_INFINITY;
183
- const agentInstructions = await loadRelativeText(import.meta.url, '../agents/comparator.md');
184
- const comparisons = [];
185
-
186
- for (const evalDir of await listDirectories(workspaceDir)) {
187
- if (!path.basename(evalDir).startsWith('eval-')) {
188
- continue;
189
- }
190
-
191
- for (let pairIndex = 0; pairIndex < maxPairs; pairIndex += 1) {
192
- const pair = await loadComparisonPair(evalDir, configA, configB, pairIndex);
193
- if (!pair) {
194
- break;
195
- }
196
-
197
- const presentPrimaryFirst = comparisons.length % 2 === 0;
198
- const prompt = buildComparisonPrompt({
199
- agentInstructions,
200
- evalMetadata: pair.evalMetadata,
201
- runA: pair.runA,
202
- runB: pair.runB,
203
- presentPrimaryFirst,
204
- });
205
- const rawResponse = await runner(prompt, {
206
- ...options,
207
- workspaceDir,
208
- evalDir,
209
- pairIndex,
210
- configA,
211
- configB,
212
- });
213
- const parsed = normalizeComparisonResult(rawResponse);
214
- const winnerConfig = mapWinnerToConfig(parsed.winner, presentPrimaryFirst, configA, configB);
215
-
216
- comparisons.push({
217
- index: comparisons.length + 1,
218
- eval_id: pair.evalMetadata.eval_id ?? null,
219
- eval_name: pair.evalMetadata.eval_name ?? null,
220
- run_a: path.relative(workspaceDir, pair.runA.runDir).replace(/\\/g, '/'),
221
- run_b: path.relative(workspaceDir, pair.runB.runDir).replace(/\\/g, '/'),
222
- presented_as: presentPrimaryFirst
223
- ? { A: configA, B: configB }
224
- : { A: configB, B: configA },
225
- winner_label: parsed.winner,
226
- winner_config: winnerConfig,
227
- confidence: parsed.confidence,
228
- rationale: parsed.rationale,
229
- strengths_a: parsed.strengths_a,
230
- strengths_b: parsed.strengths_b,
231
- risks: parsed.risks,
232
- });
233
- }
234
- }
235
-
236
- if (comparisons.length === 0) {
237
- throw new Error(`No comparable run pairs found for ${configA} vs ${configB} in ${workspaceDir}`);
238
- }
239
-
240
- const document = {
241
- workspace: workspaceDir,
242
- generated_at: new Date().toISOString(),
243
- config_a: configA,
244
- config_b: configB,
245
- summary: {
246
- total_pairs: comparisons.length,
247
- config_a_wins: comparisons.filter((item) => item.winner_config === configA).length,
248
- config_b_wins: comparisons.filter((item) => item.winner_config === configB).length,
249
- ties: comparisons.filter((item) => item.winner_config === 'tie').length,
250
- inconclusive: comparisons.filter((item) => item.winner_config === 'inconclusive').length,
251
- },
252
- comparisons,
253
- };
254
-
255
- const outputPath = path.resolve(options.outputPath ?? path.join(workspaceDir, 'comparison.json'));
256
- const markdownPath = path.resolve(options.markdownPath ?? path.join(workspaceDir, 'comparison.md'));
257
- await writeFile(outputPath, `${JSON.stringify(document, null, 2)}\n`, 'utf8');
258
- await writeFile(markdownPath, renderComparisonMarkdown(document), 'utf8');
259
-
260
- return {
261
- document,
262
- outputPath,
263
- markdownPath,
264
- };
265
- }
266
-
267
- function parseArgs(argv) {
268
- const args = {
269
- workspaceDir: argv[2] ?? '',
270
- configA: 'with_skill',
271
- configB: 'without_skill',
272
- outputPath: undefined,
273
- markdownPath: undefined,
274
- provider: 'anthropic',
275
- model: undefined,
276
- reasoningMode: 'balanced',
277
- maxIter: 20,
278
- maxPairs: undefined,
279
- cwd: process.cwd(),
280
- };
281
-
282
- for (let index = 3; index < argv.length; index += 1) {
283
- const token = argv[index];
284
- if (token === '--config-a' && argv[index + 1]) {
285
- args.configA = argv[++index];
286
- } else if (token === '--config-b' && argv[index + 1]) {
287
- args.configB = argv[++index];
288
- } else if (token === '--output' && argv[index + 1]) {
289
- args.outputPath = argv[++index];
290
- } else if (token === '--markdown' && argv[index + 1]) {
291
- args.markdownPath = argv[++index];
292
- } else if (token === '--provider' && argv[index + 1]) {
293
- args.provider = argv[++index];
294
- } else if (token === '--model' && argv[index + 1]) {
295
- args.model = argv[++index];
296
- } else if (token === '--reasoning' && argv[index + 1]) {
297
- args.reasoningMode = argv[++index];
298
- } else if (token === '--max-iter' && argv[index + 1]) {
299
- args.maxIter = Number(argv[++index]);
300
- } else if (token === '--max-pairs' && argv[index + 1]) {
301
- args.maxPairs = Number(argv[++index]);
302
- } else if (token === '--cwd' && argv[index + 1]) {
303
- args.cwd = argv[++index];
304
- }
305
- }
306
-
307
- return args;
308
- }
309
-
1
+ #!/usr/bin/env node
2
+
3
+ import { readdir, readFile, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import {
7
+ extractJsonObject,
8
+ loadKodaXSDK,
9
+ loadRelativeText,
10
+ readJsonFile,
11
+ truncateText,
12
+ } from './utils.js';
13
+
14
+ async function listDirectories(dirPath) {
15
+ const entries = await readdir(dirPath, { withFileTypes: true }).catch(() => []);
16
+ return entries
17
+ .filter((entry) => entry.isDirectory())
18
+ .map((entry) => path.join(dirPath, entry.name))
19
+ .sort((left, right) => left.localeCompare(right));
20
+ }
21
+
22
+ function normalizeStringArray(value) {
23
+ if (!Array.isArray(value)) {
24
+ return [];
25
+ }
26
+ return value
27
+ .map((item) => String(item ?? '').trim())
28
+ .filter(Boolean);
29
+ }
30
+
31
+ function normalizeComparisonResult(rawText) {
32
+ const parsed = extractJsonObject(rawText) ?? {};
33
+ const winner = ['A', 'B', 'tie', 'inconclusive'].includes(parsed.winner)
34
+ ? parsed.winner
35
+ : 'inconclusive';
36
+ const confidenceValue = Number(parsed.confidence);
37
+
38
+ return {
39
+ winner,
40
+ confidence: Number.isFinite(confidenceValue)
41
+ ? Math.max(0, Math.min(1, Number(confidenceValue.toFixed(4))))
42
+ : 0,
43
+ rationale: String(parsed.rationale ?? '').trim(),
44
+ strengths_a: normalizeStringArray(parsed.strengths_a),
45
+ strengths_b: normalizeStringArray(parsed.strengths_b),
46
+ risks: normalizeStringArray(parsed.risks),
47
+ };
48
+ }
49
+
50
+ async function loadComparisonPair(evalDir, configA, configB, pairIndex) {
51
+ const runsA = (await listDirectories(path.join(evalDir, configA)))
52
+ .filter((dirPath) => path.basename(dirPath).startsWith('run-'));
53
+ const runsB = (await listDirectories(path.join(evalDir, configB)))
54
+ .filter((dirPath) => path.basename(dirPath).startsWith('run-'));
55
+ const left = runsA[pairIndex];
56
+ const right = runsB[pairIndex];
57
+
58
+ if (!left || !right) {
59
+ return null;
60
+ }
61
+
62
+ const evalMetadata = await readJsonFile(path.join(evalDir, 'eval_metadata.json'), {});
63
+ const outputA = await readFile(path.join(left, 'outputs', 'result.md'), 'utf8').catch(() => '');
64
+ const outputB = await readFile(path.join(right, 'outputs', 'result.md'), 'utf8').catch(() => '');
65
+
66
+ return {
67
+ evalDir,
68
+ evalMetadata,
69
+ runA: {
70
+ runDir: left,
71
+ runId: path.basename(left),
72
+ configName: configA,
73
+ output: outputA,
74
+ },
75
+ runB: {
76
+ runDir: right,
77
+ runId: path.basename(right),
78
+ configName: configB,
79
+ output: outputB,
80
+ },
81
+ };
82
+ }
83
+
84
+ export function buildComparisonPrompt(input) {
85
+ const pair = input.presentPrimaryFirst
86
+ ? { A: input.runA, B: input.runB }
87
+ : { A: input.runB, B: input.runA };
88
+
89
+ return `${input.agentInstructions.trim()}
90
+
91
+ Return JSON with this shape:
92
+ {
93
+ "winner": "A | B | tie | inconclusive",
94
+ "confidence": 0.0,
95
+ "rationale": "short explanation",
96
+ "strengths_a": [],
97
+ "strengths_b": [],
98
+ "risks": []
99
+ }
100
+
101
+ Judge only the visible outputs. Do not mention hidden config names in the rationale.
102
+
103
+ ## Eval Prompt
104
+ ${truncateText(input.evalMetadata.prompt ?? '', 4000)}
105
+
106
+ ## Expected Outcome
107
+ ${truncateText(input.evalMetadata.expected_output ?? '', 2000)}
108
+
109
+ ## Assertions
110
+ ${truncateText(JSON.stringify(input.evalMetadata.assertions ?? [], null, 2), 4000)}
111
+
112
+ ## Candidate A
113
+ ${truncateText(pair.A.output, 12000)}
114
+
115
+ ## Candidate B
116
+ ${truncateText(pair.B.output, 12000)}
117
+ `;
118
+ }
119
+
120
+ async function defaultRunComparator(prompt, options) {
121
+ const { runKodaX } = await loadKodaXSDK();
122
+ const result = await runKodaX(
123
+ {
124
+ provider: options.provider ?? 'anthropic',
125
+ model: options.model,
126
+ maxIter: options.maxIter ?? 20,
127
+ reasoningMode: options.reasoningMode ?? 'balanced',
128
+ thinking: options.reasoningMode ? options.reasoningMode !== 'off' : true,
129
+ context: {
130
+ gitRoot: path.resolve(options.cwd ?? options.workspaceDir ?? process.cwd()),
131
+ },
132
+ },
133
+ prompt
134
+ );
135
+ return result.lastText;
136
+ }
137
+
138
+ function mapWinnerToConfig(winner, presentPrimaryFirst, configA, configB) {
139
+ if (winner === 'A') {
140
+ return presentPrimaryFirst ? configA : configB;
141
+ }
142
+ if (winner === 'B') {
143
+ return presentPrimaryFirst ? configB : configA;
144
+ }
145
+ return winner;
146
+ }
147
+
148
+ export function renderComparisonMarkdown(document) {
149
+ const lines = [
150
+ `# Blind Comparison: ${document.config_a} vs ${document.config_b}`,
151
+ '',
152
+ `Generated: ${document.generated_at}`,
153
+ '',
154
+ `- ${document.config_a} wins: ${document.summary.config_a_wins}`,
155
+ `- ${document.config_b} wins: ${document.summary.config_b_wins}`,
156
+ `- ties: ${document.summary.ties}`,
157
+ `- inconclusive: ${document.summary.inconclusive}`,
158
+ '',
159
+ ];
160
+
161
+ for (const comparison of document.comparisons) {
162
+ lines.push(`## Eval ${comparison.eval_id ?? comparison.eval_name ?? comparison.index}`);
163
+ lines.push('');
164
+ lines.push(`- Winner: ${comparison.winner_config}`);
165
+ lines.push(`- Confidence: ${comparison.confidence}`);
166
+ lines.push(`- Rationale: ${comparison.rationale || 'n/a'}`);
167
+ lines.push('');
168
+ }
169
+
170
+ return `${lines.join('\n')}\n`;
171
+ }
172
+
173
+ export async function compareWorkspace(
174
+ options,
175
+ runner = defaultRunComparator
176
+ ) {
177
+ const workspaceDir = path.resolve(options.workspaceDir);
178
+ const configA = options.configA ?? 'with_skill';
179
+ const configB = options.configB ?? 'without_skill';
180
+ const maxPairs = Number.isFinite(options.maxPairs) && options.maxPairs > 0
181
+ ? Math.floor(options.maxPairs)
182
+ : Number.POSITIVE_INFINITY;
183
+ const agentInstructions = await loadRelativeText(import.meta.url, '../agents/comparator.md');
184
+ const comparisons = [];
185
+
186
+ for (const evalDir of await listDirectories(workspaceDir)) {
187
+ if (!path.basename(evalDir).startsWith('eval-')) {
188
+ continue;
189
+ }
190
+
191
+ for (let pairIndex = 0; pairIndex < maxPairs; pairIndex += 1) {
192
+ const pair = await loadComparisonPair(evalDir, configA, configB, pairIndex);
193
+ if (!pair) {
194
+ break;
195
+ }
196
+
197
+ const presentPrimaryFirst = comparisons.length % 2 === 0;
198
+ const prompt = buildComparisonPrompt({
199
+ agentInstructions,
200
+ evalMetadata: pair.evalMetadata,
201
+ runA: pair.runA,
202
+ runB: pair.runB,
203
+ presentPrimaryFirst,
204
+ });
205
+ const rawResponse = await runner(prompt, {
206
+ ...options,
207
+ workspaceDir,
208
+ evalDir,
209
+ pairIndex,
210
+ configA,
211
+ configB,
212
+ });
213
+ const parsed = normalizeComparisonResult(rawResponse);
214
+ const winnerConfig = mapWinnerToConfig(parsed.winner, presentPrimaryFirst, configA, configB);
215
+
216
+ comparisons.push({
217
+ index: comparisons.length + 1,
218
+ eval_id: pair.evalMetadata.eval_id ?? null,
219
+ eval_name: pair.evalMetadata.eval_name ?? null,
220
+ run_a: path.relative(workspaceDir, pair.runA.runDir).replace(/\\/g, '/'),
221
+ run_b: path.relative(workspaceDir, pair.runB.runDir).replace(/\\/g, '/'),
222
+ presented_as: presentPrimaryFirst
223
+ ? { A: configA, B: configB }
224
+ : { A: configB, B: configA },
225
+ winner_label: parsed.winner,
226
+ winner_config: winnerConfig,
227
+ confidence: parsed.confidence,
228
+ rationale: parsed.rationale,
229
+ strengths_a: parsed.strengths_a,
230
+ strengths_b: parsed.strengths_b,
231
+ risks: parsed.risks,
232
+ });
233
+ }
234
+ }
235
+
236
+ if (comparisons.length === 0) {
237
+ throw new Error(`No comparable run pairs found for ${configA} vs ${configB} in ${workspaceDir}`);
238
+ }
239
+
240
+ const document = {
241
+ workspace: workspaceDir,
242
+ generated_at: new Date().toISOString(),
243
+ config_a: configA,
244
+ config_b: configB,
245
+ summary: {
246
+ total_pairs: comparisons.length,
247
+ config_a_wins: comparisons.filter((item) => item.winner_config === configA).length,
248
+ config_b_wins: comparisons.filter((item) => item.winner_config === configB).length,
249
+ ties: comparisons.filter((item) => item.winner_config === 'tie').length,
250
+ inconclusive: comparisons.filter((item) => item.winner_config === 'inconclusive').length,
251
+ },
252
+ comparisons,
253
+ };
254
+
255
+ const outputPath = path.resolve(options.outputPath ?? path.join(workspaceDir, 'comparison.json'));
256
+ const markdownPath = path.resolve(options.markdownPath ?? path.join(workspaceDir, 'comparison.md'));
257
+ await writeFile(outputPath, `${JSON.stringify(document, null, 2)}\n`, 'utf8');
258
+ await writeFile(markdownPath, renderComparisonMarkdown(document), 'utf8');
259
+
260
+ return {
261
+ document,
262
+ outputPath,
263
+ markdownPath,
264
+ };
265
+ }
266
+
267
+ function parseArgs(argv) {
268
+ const args = {
269
+ workspaceDir: argv[2] ?? '',
270
+ configA: 'with_skill',
271
+ configB: 'without_skill',
272
+ outputPath: undefined,
273
+ markdownPath: undefined,
274
+ provider: 'anthropic',
275
+ model: undefined,
276
+ reasoningMode: 'balanced',
277
+ maxIter: 20,
278
+ maxPairs: undefined,
279
+ cwd: process.cwd(),
280
+ };
281
+
282
+ for (let index = 3; index < argv.length; index += 1) {
283
+ const token = argv[index];
284
+ if (token === '--config-a' && argv[index + 1]) {
285
+ args.configA = argv[++index];
286
+ } else if (token === '--config-b' && argv[index + 1]) {
287
+ args.configB = argv[++index];
288
+ } else if (token === '--output' && argv[index + 1]) {
289
+ args.outputPath = argv[++index];
290
+ } else if (token === '--markdown' && argv[index + 1]) {
291
+ args.markdownPath = argv[++index];
292
+ } else if (token === '--provider' && argv[index + 1]) {
293
+ args.provider = argv[++index];
294
+ } else if (token === '--model' && argv[index + 1]) {
295
+ args.model = argv[++index];
296
+ } else if (token === '--reasoning' && argv[index + 1]) {
297
+ args.reasoningMode = argv[++index];
298
+ } else if (token === '--max-iter' && argv[index + 1]) {
299
+ args.maxIter = Number(argv[++index]);
300
+ } else if (token === '--max-pairs' && argv[index + 1]) {
301
+ args.maxPairs = Number(argv[++index]);
302
+ } else if (token === '--cwd' && argv[index + 1]) {
303
+ args.cwd = argv[++index];
304
+ }
305
+ }
306
+
307
+ return args;
308
+ }
309
+
310
310
  export async function main(argv = process.argv) {
311
311
  const args = parseArgs(argv);
312
- if (!args.workspaceDir) {
313
- console.error('Usage: node scripts/compare-runs.js <workspace> [--config-a with_skill] [--config-b without_skill] [--output comparison.json]');
314
- process.exit(1);
315
- }
316
-
317
- const result = await compareWorkspace(args);
318
- process.stdout.write(`${JSON.stringify({
319
- comparison: result.document.summary,
320
- output: result.outputPath,
321
- markdown: result.markdownPath,
322
- }, null, 2)}\n`);
323
- }
324
-
312
+ if (!args.workspaceDir) {
313
+ console.error('Usage: node scripts/compare-runs.js <workspace> [--config-a with_skill] [--config-b without_skill] [--output comparison.json]');
314
+ process.exit(1);
315
+ }
316
+
317
+ const result = await compareWorkspace(args);
318
+ process.stdout.write(`${JSON.stringify({
319
+ comparison: result.document.summary,
320
+ output: result.outputPath,
321
+ markdown: result.markdownPath,
322
+ }, null, 2)}\n`);
323
+ }
324
+
325
325
  const isDirectRun = process.env.KODAX_MODULE_BUNDLE !== 'true'
326
326
  && process.argv[1]
327
- && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
328
-
329
- if (isDirectRun) {
330
- main().catch((error) => {
331
- console.error(error instanceof Error ? error.message : String(error));
332
- process.exit(1);
333
- });
334
- }
327
+ && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
328
+
329
+ if (isDirectRun) {
330
+ main().catch((error) => {
331
+ console.error(error instanceof Error ? error.message : String(error));
332
+ process.exit(1);
333
+ });
334
+ }