create-walle 0.9.25 → 0.9.26

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 (179) hide show
  1. package/README.md +8 -0
  2. package/bin/create-walle.js +815 -45
  3. package/package.json +2 -2
  4. package/template/bin/ctm-dev-cleanup.js +90 -4
  5. package/template/bin/ctm-launch.sh +49 -1
  6. package/template/bin/dev.sh +45 -1
  7. package/template/bin/ensure-stable-node.js +132 -0
  8. package/template/bin/install-service.sh +9 -0
  9. package/template/claude-task-manager/api-prompts.js +899 -119
  10. package/template/claude-task-manager/approval-agent.js +360 -40
  11. package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
  12. package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
  13. package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
  14. package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
  15. package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
  16. package/template/claude-task-manager/db.js +399 -48
  17. package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
  18. package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
  19. package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
  20. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
  21. package/template/claude-task-manager/lib/approval-hook.js +200 -0
  22. package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
  23. package/template/claude-task-manager/lib/auth-rules.js +11 -0
  24. package/template/claude-task-manager/lib/background-llm.js +32 -4
  25. package/template/claude-task-manager/lib/codesign-identity.js +140 -0
  26. package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
  27. package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
  28. package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
  29. package/template/claude-task-manager/lib/codex-paths.js +73 -0
  30. package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
  31. package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
  32. package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
  33. package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
  34. package/template/claude-task-manager/lib/command-targets.js +163 -0
  35. package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
  36. package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
  37. package/template/claude-task-manager/lib/escalation-review.js +80 -3
  38. package/template/claude-task-manager/lib/flow-control.js +52 -0
  39. package/template/claude-task-manager/lib/fs-watcher.js +24 -15
  40. package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
  41. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
  42. package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
  43. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
  44. package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
  45. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
  46. package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
  47. package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
  48. package/template/claude-task-manager/lib/perf-tracker.js +29 -2
  49. package/template/claude-task-manager/lib/permission-match.js +146 -16
  50. package/template/claude-task-manager/lib/project-slug.js +33 -0
  51. package/template/claude-task-manager/lib/prompt-intent.js +51 -4
  52. package/template/claude-task-manager/lib/read-pool-client.js +48 -3
  53. package/template/claude-task-manager/lib/real-node.js +73 -0
  54. package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
  55. package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
  56. package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
  57. package/template/claude-task-manager/lib/session-history.js +5 -7
  58. package/template/claude-task-manager/lib/session-host-manager.js +19 -0
  59. package/template/claude-task-manager/lib/session-jobs.js +6 -0
  60. package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
  61. package/template/claude-task-manager/lib/session-messages-page.js +211 -0
  62. package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
  63. package/template/claude-task-manager/lib/session-standup.js +8 -0
  64. package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
  65. package/template/claude-task-manager/lib/session-token-usage.js +30 -8
  66. package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
  67. package/template/claude-task-manager/lib/storage-migration.js +2 -1
  68. package/template/claude-task-manager/lib/transcript-store.js +179 -12
  69. package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
  70. package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
  71. package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
  72. package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
  73. package/template/claude-task-manager/package.json +5 -2
  74. package/template/claude-task-manager/prompt-harvest.js +31 -11
  75. package/template/claude-task-manager/providers/claude-code.js +29 -1
  76. package/template/claude-task-manager/providers/codex.js +13 -1
  77. package/template/claude-task-manager/public/css/setup.css +11 -0
  78. package/template/claude-task-manager/public/css/walle-session.css +132 -4
  79. package/template/claude-task-manager/public/css/walle.css +89 -0
  80. package/template/claude-task-manager/public/icon-16.png +0 -0
  81. package/template/claude-task-manager/public/icon-32.png +0 -0
  82. package/template/claude-task-manager/public/icon-512.png +0 -0
  83. package/template/claude-task-manager/public/index.html +2483 -165
  84. package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
  85. package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
  86. package/template/claude-task-manager/public/js/message-renderer.js +60 -1
  87. package/template/claude-task-manager/public/js/prompts.js +13 -1
  88. package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
  89. package/template/claude-task-manager/public/js/setup.js +54 -10
  90. package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
  91. package/template/claude-task-manager/public/js/stream-view.js +78 -0
  92. package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
  93. package/template/claude-task-manager/public/js/tool-state.js +155 -0
  94. package/template/claude-task-manager/public/js/walle-session.js +887 -326
  95. package/template/claude-task-manager/public/js/walle.js +306 -195
  96. package/template/claude-task-manager/public/m/app.css +1 -0
  97. package/template/claude-task-manager/public/m/app.js +33 -3
  98. package/template/claude-task-manager/queue-engine.js +45 -1
  99. package/template/claude-task-manager/server.js +3367 -540
  100. package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
  101. package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
  102. package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
  103. package/template/claude-task-manager/workers/session-host-process.js +10 -0
  104. package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
  105. package/template/package.json +2 -3
  106. package/template/shared/icons/AppIcon-ctm.icns +0 -0
  107. package/template/shared/icons/AppIcon-walle.icns +0 -0
  108. package/template/wall-e/agent.js +139 -18
  109. package/template/wall-e/api-walle.js +201 -22
  110. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
  111. package/template/wall-e/brain.js +1049 -39
  112. package/template/wall-e/chat.js +427 -86
  113. package/template/wall-e/coding/acceptance-contract.js +26 -1
  114. package/template/wall-e/coding/action-memory-policy.js +353 -0
  115. package/template/wall-e/coding/action-memory-store.js +814 -0
  116. package/template/wall-e/coding/initial-messages.js +197 -0
  117. package/template/wall-e/coding/no-progress-guard.js +327 -0
  118. package/template/wall-e/coding/permission-service.js +88 -22
  119. package/template/wall-e/coding/session-workspaces.js +81 -0
  120. package/template/wall-e/coding/shell-sandbox.js +124 -0
  121. package/template/wall-e/coding/stream-processor.js +63 -2
  122. package/template/wall-e/coding/tool-execution-controller.js +14 -1
  123. package/template/wall-e/coding/tool-registry.js +1 -1
  124. package/template/wall-e/coding/transcript-writer.js +3 -0
  125. package/template/wall-e/coding-orchestrator.js +636 -35
  126. package/template/wall-e/coding-prompts.js +51 -2
  127. package/template/wall-e/docs/model-routing-policy.md +59 -0
  128. package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
  129. package/template/wall-e/extraction/knowledge-extractor.js +76 -23
  130. package/template/wall-e/http/chat-api.js +30 -12
  131. package/template/wall-e/http/model-admin.js +93 -1
  132. package/template/wall-e/lib/background-lanes.js +133 -0
  133. package/template/wall-e/lib/boot-profile.js +11 -0
  134. package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
  135. package/template/wall-e/lib/brain-read-pool-client.js +311 -0
  136. package/template/wall-e/lib/diagnostics-flags.js +87 -0
  137. package/template/wall-e/lib/event-loop-monitor.js +74 -3
  138. package/template/wall-e/lib/mcp-integration.js +7 -1
  139. package/template/wall-e/lib/real-node.js +98 -0
  140. package/template/wall-e/lib/runtime-health.js +206 -0
  141. package/template/wall-e/lib/runtime-worker-pool.js +101 -0
  142. package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
  143. package/template/wall-e/lib/scheduler.js +446 -17
  144. package/template/wall-e/lib/service-health.js +61 -2
  145. package/template/wall-e/lib/service-readiness.js +258 -0
  146. package/template/wall-e/lib/usage.js +152 -0
  147. package/template/wall-e/lib/worker-thread-pool.js +389 -0
  148. package/template/wall-e/llm/client.js +81 -4
  149. package/template/wall-e/llm/default-fallback.js +54 -8
  150. package/template/wall-e/llm/mlx.js +536 -73
  151. package/template/wall-e/llm/mlx.plugin.json +1 -1
  152. package/template/wall-e/llm/ollama.js +342 -43
  153. package/template/wall-e/llm/provider-error.js +18 -1
  154. package/template/wall-e/llm/provider-health-state.js +176 -0
  155. package/template/wall-e/llm/routing-policy.js +796 -0
  156. package/template/wall-e/llm/supported-models.js +5 -0
  157. package/template/wall-e/loops/tasks.js +60 -14
  158. package/template/wall-e/loops/think.js +89 -24
  159. package/template/wall-e/mcp-server.js +192 -28
  160. package/template/wall-e/server.js +32 -7
  161. package/template/wall-e/skills/script-skill-runner.js +8 -1
  162. package/template/wall-e/skills/skill-planner.js +64 -1
  163. package/template/wall-e/tools/builtin-middleware.js +67 -2
  164. package/template/wall-e/tools/local-tools.js +116 -26
  165. package/template/wall-e/tools/permission-checker.js +52 -4
  166. package/template/wall-e/tools/permission-rules.js +36 -0
  167. package/template/wall-e/tools/shell-analyzer.js +46 -1
  168. package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
  169. package/template/wall-e/training/real-trajectory-miner.js +2617 -0
  170. package/template/wall-e/training/replay-eval-analysis.js +151 -0
  171. package/template/wall-e/training/run-shell-command-selector.js +277 -0
  172. package/template/wall-e/training/tool-sft-dataset.js +312 -0
  173. package/template/wall-e/training/tool-sft-renderers.js +144 -0
  174. package/template/wall-e/training/tool-trace-harvester.js +1440 -0
  175. package/template/wall-e/training/trajectory-action-selector.js +364 -0
  176. package/template/wall-e/weather-runtime.js +232 -0
  177. package/template/wall-e/workers/brain-owner-worker.js +162 -0
  178. package/template/wall-e/workers/brain-read-worker.js +148 -0
  179. package/template/wall-e/workers/runtime-worker.js +145 -0
@@ -0,0 +1,1981 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+
8
+ const {
9
+ buildDatasetManifest,
10
+ checkHoldoutLeakage,
11
+ splitExamplesBySource,
12
+ stableStringify,
13
+ writeDatasetFiles,
14
+ } = require('../training/tool-sft-dataset');
15
+ const {
16
+ applyRunShellCommandSelection,
17
+ scoreRunShellCommandCandidates,
18
+ } = require('../training/run-shell-command-selector');
19
+ const {
20
+ applyTrajectoryActionSelection,
21
+ defaultActionMemoryFilesForEval,
22
+ loadTrajectoryActionMemory,
23
+ scoreTrajectoryActionCandidates,
24
+ } = require('../training/trajectory-action-selector');
25
+ const {
26
+ ActionMemoryStore,
27
+ defaultActionMemoryArtifactsDir,
28
+ defaultActionMemoryDbPath,
29
+ } = require('../coding/action-memory-store');
30
+ const {
31
+ summarizeActionMemoryReplayEval,
32
+ } = require('../training/replay-eval-analysis');
33
+ const { renderExample, writeRenderedSplit } = require('../training/tool-sft-renderers');
34
+ const { buildSyntheticToolUseExamples, harvestToolTracesFromSessions } = require('../training/tool-trace-harvester');
35
+ const {
36
+ auditDecisionPoints,
37
+ auditDecisiveActionExamples,
38
+ auditTrajectories,
39
+ buildDecisiveActionExamples,
40
+ buildPersonalReplayEvalCases,
41
+ buildPersonalReplayExamples,
42
+ decisionPointsFromTrajectories,
43
+ mineRealTrajectories,
44
+ targetToolCallFromExample,
45
+ trajectoryToCanonicalExamples,
46
+ writeWorkflowCatalog,
47
+ writeDecisionPointAudit,
48
+ writeTrajectoryAudit,
49
+ } = require('../training/real-trajectory-miner');
50
+ const { createMLXProvider } = require('../llm/mlx');
51
+ const {
52
+ buildMlxLoraCommand,
53
+ buildMlxLoraConfig,
54
+ buildTrainingManifest,
55
+ runMlxLoraTraining,
56
+ validateDatasetFiles,
57
+ writeCandidateMetadata,
58
+ writeMlxLoraConfig,
59
+ writeModelCard,
60
+ } = require('../training/gemma-e4b-qlora');
61
+
62
+ function parseArgs(argv = process.argv.slice(2)) {
63
+ const out = { command: argv[0] || 'help' };
64
+ for (let i = 1; i < argv.length; i += 1) {
65
+ const arg = argv[i];
66
+ if (!arg.startsWith('--')) continue;
67
+ const key = arg.slice(2);
68
+ if (['dry-run', 'help'].includes(key)) {
69
+ out[key] = true;
70
+ } else {
71
+ out[key] = argv[i + 1];
72
+ i += 1;
73
+ }
74
+ }
75
+ return out;
76
+ }
77
+
78
+ function defaultOutputDir() {
79
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
80
+ return path.join(os.homedir(), '.walle', 'training', 'gemma-e4b-tooluse', stamp);
81
+ }
82
+
83
+ function parseList(value) {
84
+ if (!value) return [];
85
+ return String(value).split(',').map((s) => s.trim()).filter(Boolean);
86
+ }
87
+
88
+ function parseBool(value, defaultValue = false) {
89
+ if (value === undefined || value === null) return defaultValue;
90
+ if (typeof value === 'boolean') return value;
91
+ return !/^(false|0|no|off)$/i.test(String(value).trim());
92
+ }
93
+
94
+ function normalizeToolCallFormat(value) {
95
+ return value === 'bare-json' ? 'bare-json' : 'fenced';
96
+ }
97
+
98
+ function parseRoots(args) {
99
+ return [
100
+ ...parseList(args.root),
101
+ ...parseList(args.roots),
102
+ ];
103
+ }
104
+
105
+ function parsePerSourceLimit(value) {
106
+ if (!value) return null;
107
+ const text = String(value).trim();
108
+ if (!text) return null;
109
+ if (/^\d+$/.test(text)) return { all: Number(text) };
110
+ const out = {};
111
+ for (const part of text.split(',')) {
112
+ const [rawKey, rawValue] = part.split('=');
113
+ const key = String(rawKey || '').trim();
114
+ const n = Number(String(rawValue || '').trim());
115
+ if (key && Number.isFinite(n) && n >= 0) out[key] = n;
116
+ }
117
+ return Object.keys(out).length ? out : null;
118
+ }
119
+
120
+ function parsePerToolLimit(value) {
121
+ return parsePerSourceLimit(value);
122
+ }
123
+
124
+ function applyPerToolLimit(examples = [], perToolLimit = null) {
125
+ const limits = parsePerToolLimit(perToolLimit);
126
+ if (!limits) return examples;
127
+ const counts = new Map();
128
+ const out = [];
129
+ for (const example of examples) {
130
+ const toolName = example.labels?.tool_name || 'unknown';
131
+ const cap = limits[toolName] ?? limits.all ?? limits.default ?? null;
132
+ const count = counts.get(toolName) || 0;
133
+ if (cap != null && count >= cap) continue;
134
+ counts.set(toolName, count + 1);
135
+ out.push(example);
136
+ }
137
+ return out;
138
+ }
139
+
140
+ function applyMaxRenderedChars(examples = [], { format = 'mlx-tools', maxChars = 0 } = {}) {
141
+ const limit = Number(maxChars || 0);
142
+ if (!Number.isFinite(limit) || limit <= 0) {
143
+ return { examples, dropped: 0, maxChars: limit, maxSeenChars: 0 };
144
+ }
145
+ const kept = [];
146
+ let dropped = 0;
147
+ let maxSeenChars = 0;
148
+ for (const example of examples) {
149
+ const rendered = renderExample(example, format, { compactTools: true });
150
+ const chars = JSON.stringify(rendered).length;
151
+ maxSeenChars = Math.max(maxSeenChars, chars);
152
+ if (chars <= limit) {
153
+ kept.push(example);
154
+ } else {
155
+ dropped += 1;
156
+ }
157
+ }
158
+ return { examples: kept, dropped, maxChars: limit, maxSeenChars };
159
+ }
160
+
161
+ function buildRenderedLengthGate({ filter = null, args = {} } = {}) {
162
+ if (!filter || !filter.maxChars) return null;
163
+ const minRows = parseNumber(args['min-rendered-length-kept-rows'], 0);
164
+ const checks = [{
165
+ name: 'renderedLengthKeptRows',
166
+ actual: filter.examples?.length || 0,
167
+ required: minRows,
168
+ }];
169
+ const failed = checks.filter((check) => check.actual < check.required);
170
+ return {
171
+ ok: failed.length === 0,
172
+ checks,
173
+ failed,
174
+ };
175
+ }
176
+
177
+ function parseNumber(value, defaultValue = 0) {
178
+ if (value === undefined || value === null || value === '') return defaultValue;
179
+ const n = Number(value);
180
+ return Number.isFinite(n) ? n : defaultValue;
181
+ }
182
+
183
+ function buildRealDatasetQualityGate({ audit = null, examples = [], args = {} } = {}) {
184
+ if (!audit) return null;
185
+ const checks = [
186
+ {
187
+ name: 'goldTrajectories',
188
+ actual: audit.tierCounts?.gold || 0,
189
+ required: parseNumber(args['min-gold-trajectories'], 0),
190
+ },
191
+ {
192
+ name: 'silverTrajectories',
193
+ actual: audit.tierCounts?.silver || 0,
194
+ required: parseNumber(args['min-silver-trajectories'], 0),
195
+ },
196
+ {
197
+ name: 'goldOrSilverTrajectories',
198
+ actual: audit.datasetCandidateCounts?.goldOrSilverTrajectories || 0,
199
+ required: parseNumber(args['min-gold-or-silver-trajectories'], 0),
200
+ },
201
+ {
202
+ name: 'passingVerificationTrajectories',
203
+ actual: audit.signalCounts?.hasPassingVerification || 0,
204
+ required: parseNumber(args['min-passing-verification'], 0),
205
+ },
206
+ {
207
+ name: 'realSftExamples',
208
+ actual: examples.filter((example) => example.labels?.real_trajectory).length,
209
+ required: parseNumber(args['min-real-examples'], 0),
210
+ },
211
+ ];
212
+ const failed = checks.filter((check) => check.actual < check.required);
213
+ return {
214
+ ok: failed.length === 0,
215
+ checks,
216
+ failed,
217
+ };
218
+ }
219
+
220
+ function buildDecisionPointQualityGate({ audit = null, args = {} } = {}) {
221
+ if (!audit) return null;
222
+ const checks = [
223
+ {
224
+ name: 'decisionPoints',
225
+ actual: audit.totalDecisionPoints || 0,
226
+ required: parseNumber(args['min-decision-points'], 0),
227
+ },
228
+ {
229
+ name: 'guardRecoveryDecisionPoints',
230
+ actual: audit.guardRecoveryCount || 0,
231
+ required: parseNumber(args['min-guard-recovery'], 0),
232
+ },
233
+ {
234
+ name: 'sameInspectionAfterGuard',
235
+ actual: audit.sameInspectionAfterGuardCount || 0,
236
+ maximum: parseNumber(args['max-same-inspection-after-guard'], 0),
237
+ },
238
+ ];
239
+ const failed = checks.filter((check) => {
240
+ if (check.required !== undefined) return check.actual < check.required;
241
+ if (check.maximum !== undefined) return check.actual > check.maximum;
242
+ return false;
243
+ });
244
+ return {
245
+ ok: failed.length === 0,
246
+ checks,
247
+ failed,
248
+ };
249
+ }
250
+
251
+ function buildPersonalReplayDataGate({ workflowAudit = null, examples = [], args = {} } = {}) {
252
+ if (!workflowAudit) return null;
253
+ const personalRows = examples.filter((example) => example.labels?.personal_replay);
254
+ const editVerificationRows = personalRows.filter((example) => (
255
+ /^(edit_file|write_file|apply_patch|run_shell)$/.test(example.labels?.tool_name || '') &&
256
+ (example.labels?.latest_decisive_verification_status === 'passed' || example.labels?.verification_commands?.length)
257
+ ));
258
+ const firstActionRows = personalRows.filter((example) => {
259
+ const messages = example.messages || [];
260
+ return messages.filter((message) => message.role === 'assistant' && message.tool_calls?.length).length === 1 &&
261
+ !messages.some((message) => message.role === 'tool');
262
+ });
263
+ const variantRows = personalRows.filter((example) => /variant/.test(example.labels?.replay_lane || ''));
264
+ const checks = [
265
+ {
266
+ name: 'seenWorkflowFamilies',
267
+ actual: workflowAudit.totalFamilies || 0,
268
+ required: parseNumber(args['min-seen-workflow-families'], 0),
269
+ },
270
+ {
271
+ name: 'repeatedWorkflowFamilies',
272
+ actual: workflowAudit.repeatedFamilies || 0,
273
+ required: parseNumber(args['min-repeated-workflow-families'], 0),
274
+ },
275
+ {
276
+ name: 'seenReplayRows',
277
+ actual: personalRows.length,
278
+ required: parseNumber(args['min-seen-replay-rows'], 0),
279
+ },
280
+ {
281
+ name: 'seenVariantRows',
282
+ actual: variantRows.length,
283
+ required: parseNumber(args['min-seen-variant-rows'], 0),
284
+ },
285
+ {
286
+ name: 'editVerificationRows',
287
+ actual: editVerificationRows.length,
288
+ required: parseNumber(args['min-edit-verification-rows'], 0),
289
+ },
290
+ {
291
+ name: 'firstActionRows',
292
+ actual: firstActionRows.length,
293
+ required: parseNumber(args['min-first-action-rows'], 0),
294
+ },
295
+ ];
296
+ const failed = checks.filter((check) => check.actual < check.required);
297
+ return {
298
+ ok: failed.length === 0,
299
+ checks,
300
+ failed,
301
+ };
302
+ }
303
+
304
+ function buildDecisiveActionDataGate({ audit = null, args = {} } = {}) {
305
+ if (!audit) return null;
306
+ const checks = [
307
+ {
308
+ name: 'decisiveActionRows',
309
+ actual: audit.totalRows || 0,
310
+ required: parseNumber(args['min-decisive-action-rows'], 0),
311
+ },
312
+ {
313
+ name: 'decisiveExactRows',
314
+ actual: audit.exactRows || 0,
315
+ required: parseNumber(args['min-decisive-exact-rows'], 0),
316
+ },
317
+ {
318
+ name: 'decisiveEditRows',
319
+ actual: audit.editRows || 0,
320
+ required: parseNumber(args['min-decisive-edit-rows'], 0),
321
+ },
322
+ {
323
+ name: 'decisiveVerificationRows',
324
+ actual: audit.verificationRows || 0,
325
+ required: parseNumber(args['min-decisive-verification-rows'], 0),
326
+ },
327
+ {
328
+ name: 'decisiveRealToolResultRows',
329
+ actual: audit.realToolResultHistoryRows || 0,
330
+ required: parseNumber(args['min-decisive-real-result-rows'], 0),
331
+ },
332
+ {
333
+ name: 'jsonOnlyContractRows',
334
+ actual: audit.jsonOnlyContractRows || 0,
335
+ required: parseNumber(args['min-json-only-contract-rows'], 0),
336
+ },
337
+ {
338
+ name: 'stateSummaryRows',
339
+ actual: audit.stateSummaryRows || 0,
340
+ required: parseNumber(args['min-state-summary-rows'], 0),
341
+ },
342
+ ];
343
+ const failed = checks.filter((check) => check.actual < check.required);
344
+ return {
345
+ ok: failed.length === 0,
346
+ checks,
347
+ failed,
348
+ };
349
+ }
350
+
351
+ async function exportDataset(args) {
352
+ const outputDir = path.resolve(args['output-dir'] || defaultOutputDir());
353
+ const datasetDir = path.join(outputDir, 'dataset');
354
+ const format = args.format || 'mlx-tools';
355
+ const stateSummaryMode = parseBool(args['state-summary'], format === 'mlx-provider-state-bare');
356
+ const maxExamples = args['max-examples'] ? Number(args['max-examples']) : 0;
357
+ const qualityThreshold = args['quality-threshold'] ? Number(args['quality-threshold']) : 0.8;
358
+ const source = args.source || 'synthetic';
359
+ const useDecisiveActions = source === 'decisive-actions' || parseBool(args['decisive-actions'], false);
360
+ const excludeHoldoutIds = parseList(args['exclude-holdout-ids']);
361
+ const traceFiles = parseList(args['trace-files']);
362
+ const datasetVersion = args['dataset-version'] || 'gemma-e4b-tooluse-v1';
363
+ const targetedRepeats = args['targeted-repeats'] ? Number(args['targeted-repeats']) : 1;
364
+ const useRealTrajectories = source === 'real-sessions' || useDecisiveActions || parseBool(args['real-trajectories'], false);
365
+ const usePersonalReplay = source === 'personal-replay';
366
+ let realAudit = null;
367
+ let decisionAudit = null;
368
+ let decisionQualityGate = null;
369
+ let workflowAudit = null;
370
+ let workflowFamilies = [];
371
+ let personalReplayDataGate = null;
372
+ let decisiveActionAudit = null;
373
+ let decisiveActionDataGate = null;
374
+ let renderedLengthFilter = null;
375
+ let renderedLengthGate = null;
376
+ let personalReplayEvalCases = null;
377
+ let minedRealTrajectories = [];
378
+
379
+ let examples = [];
380
+ if (traceFiles.length) {
381
+ examples.push(...await harvestToolTracesFromSessions({
382
+ traceFiles,
383
+ excludeHoldoutIds,
384
+ qualityThreshold,
385
+ datasetVersion,
386
+ }));
387
+ }
388
+ if (useRealTrajectories || usePersonalReplay) {
389
+ const roots = parseRoots(args);
390
+ const includeCtm = parseBool(args['include-ctm'], true);
391
+ const mined = mineRealTrajectories({
392
+ roots: roots.length ? roots.map((root) => path.resolve(root)) : null,
393
+ repoPath: args.repo ? path.resolve(args.repo) : null,
394
+ source: args['transcript-source'] || 'all',
395
+ sinceDays: args['since-days'] === undefined ? 14 : Number(args['since-days']),
396
+ limit: args['scan-limit'] ? Number(args['scan-limit']) : (maxExamples > 0 ? maxExamples : 500),
397
+ includeCtm,
398
+ includeFilesystem: parseBool(args['include-filesystem'], roots.length > 0 || !includeCtm),
399
+ ctmDbPath: args['ctm-db'] ? path.resolve(args['ctm-db']) : undefined,
400
+ requireCodingIntent: parseBool(args['require-coding-intent'], true),
401
+ maxResultChars: args['max-result-chars'] ? Number(args['max-result-chars']) : undefined,
402
+ maxFileBytes: args['max-file-mb'] ? Number(args['max-file-mb']) * 1024 * 1024 : undefined,
403
+ perSourceLimit: parsePerSourceLimit(args['per-source-limit']),
404
+ recoverGit: parseBool(args['recover-git'], false),
405
+ gitRecoveryWindowMs: args['git-recovery-window-minutes']
406
+ ? Number(args['git-recovery-window-minutes']) * 60 * 1000
407
+ : undefined,
408
+ gitRecoveryRequireOverlap: !parseBool(args['allow-weak-git-recovery'], false),
409
+ });
410
+ minedRealTrajectories = mined.trajectories;
411
+ if (usePersonalReplay || useDecisiveActions || args['workflow-audit-dir']) {
412
+ const workflowWritten = writeWorkflowCatalog({
413
+ trajectories: minedRealTrajectories,
414
+ outputDir: path.resolve(args['workflow-audit-dir'] || path.join(outputDir, 'workflow-audit')),
415
+ includeTiers: parseList(args['include-tiers']).length ? parseList(args['include-tiers']) : ['gold', 'silver'],
416
+ });
417
+ workflowAudit = workflowWritten.audit;
418
+ workflowFamilies = workflowWritten.families;
419
+ }
420
+ if (args['audit-dir']) {
421
+ const writtenAudit = writeTrajectoryAudit({
422
+ trajectories: minedRealTrajectories,
423
+ outputDir: path.resolve(args['audit-dir']),
424
+ includePaths: parseBool(args['include-paths'], false),
425
+ skipped: mined.skipped,
426
+ diagnostics: mined.diagnostics,
427
+ });
428
+ realAudit = writtenAudit.audit;
429
+ } else {
430
+ realAudit = auditTrajectories(minedRealTrajectories);
431
+ }
432
+ const decisionPoints = decisionPointsFromTrajectories(minedRealTrajectories, {
433
+ includeMessages: parseBool(args['include-messages'], false),
434
+ });
435
+ if (args['decision-audit-dir']) {
436
+ const writtenDecisionAudit = writeDecisionPointAudit({
437
+ trajectories: minedRealTrajectories,
438
+ decisionPoints,
439
+ outputDir: path.resolve(args['decision-audit-dir']),
440
+ includeMessages: parseBool(args['include-messages'], false),
441
+ gate: {
442
+ minDecisionPoints: parseNumber(args['min-decision-points'], 0),
443
+ minGuardRecovery: parseNumber(args['min-guard-recovery'], 0),
444
+ maxSameInspectionAfterGuard: parseNumber(args['max-same-inspection-after-guard'], 0),
445
+ },
446
+ });
447
+ decisionAudit = writtenDecisionAudit.audit;
448
+ } else {
449
+ decisionAudit = auditDecisionPoints(decisionPoints, {
450
+ minDecisionPoints: parseNumber(args['min-decision-points'], 0),
451
+ minGuardRecovery: parseNumber(args['min-guard-recovery'], 0),
452
+ maxSameInspectionAfterGuard: parseNumber(args['max-same-inspection-after-guard'], 0),
453
+ });
454
+ }
455
+ decisionQualityGate = buildDecisionPointQualityGate({ audit: decisionAudit, args });
456
+ if (usePersonalReplay || useDecisiveActions) {
457
+ const personalExamples = useDecisiveActions ? buildDecisiveActionExamples({
458
+ trajectories: minedRealTrajectories,
459
+ families: workflowFamilies,
460
+ datasetVersion,
461
+ maxExamplesPerTrajectory: args['max-tool-examples-per-session']
462
+ ? Number(args['max-tool-examples-per-session'])
463
+ : 12,
464
+ includeTiers: parseList(args['include-tiers']).length ? parseList(args['include-tiers']) : ['gold', 'silver'],
465
+ includeTrainVariants: parseBool(args['include-train-variants'], true),
466
+ variantRepeats: args['personal-variant-repeats'] ? Number(args['personal-variant-repeats']) : 1,
467
+ exactRepeats: args['decisive-exact-repeats'] ? Number(args['decisive-exact-repeats']) : 1,
468
+ requireToolResultHistory: parseBool(args['require-decisive-tool-result-history'], true),
469
+ stateSummary: stateSummaryMode,
470
+ }) : buildPersonalReplayExamples({
471
+ trajectories: minedRealTrajectories,
472
+ families: workflowFamilies,
473
+ datasetVersion,
474
+ maxExamplesPerTrajectory: args['max-tool-examples-per-session']
475
+ ? Number(args['max-tool-examples-per-session'])
476
+ : 10,
477
+ includeTiers: parseList(args['include-tiers']).length ? parseList(args['include-tiers']) : ['gold', 'silver'],
478
+ includeTrainVariants: parseBool(args['include-train-variants'], true),
479
+ variantRepeats: args['personal-variant-repeats'] ? Number(args['personal-variant-repeats']) : 1,
480
+ stateSummary: stateSummaryMode,
481
+ });
482
+ examples.push(...personalExamples);
483
+ if (useDecisiveActions) decisiveActionAudit = auditDecisiveActionExamples(personalExamples);
484
+ personalReplayEvalCases = buildPersonalReplayEvalCases({
485
+ examples: personalExamples,
486
+ families: workflowFamilies,
487
+ maxExact: args['max-seen-exact-eval'] ? Number(args['max-seen-exact-eval']) : 120,
488
+ maxVariant: args['max-seen-variant-eval'] ? Number(args['max-seen-variant-eval']) : 120,
489
+ });
490
+ } else {
491
+ examples.push(...minedRealTrajectories.flatMap((trajectory) => trajectoryToCanonicalExamples(trajectory, {
492
+ datasetVersion,
493
+ maxExamplesPerTrajectory: args['max-tool-examples-per-session']
494
+ ? Number(args['max-tool-examples-per-session'])
495
+ : 12,
496
+ includeTiers: parseList(args['include-tiers']).length ? parseList(args['include-tiers']) : ['gold', 'silver'],
497
+ stateSummary: stateSummaryMode,
498
+ })));
499
+ }
500
+ }
501
+ if (source === 'synthetic' || source === 'all') {
502
+ examples.push(...buildSyntheticToolUseExamples({ excludeHoldoutIds, datasetVersion, targetedRepeats }));
503
+ }
504
+ if (source === 'sessions' || source === 'all') {
505
+ const roots = parseRoots(args);
506
+ examples.push(...await harvestToolTracesFromSessions({
507
+ roots: roots.length ? roots.map((root) => path.resolve(root)) : null,
508
+ repoPath: args.repo ? path.resolve(args.repo) : null,
509
+ includeTranscripts: true,
510
+ transcriptSource: args['transcript-source'] || 'all',
511
+ sinceDays: args['since-days'] === undefined ? 14 : Number(args['since-days']),
512
+ limit: args['scan-limit'] ? Number(args['scan-limit']) : (maxExamples > 0 ? maxExamples : 500),
513
+ excludeHoldoutIds,
514
+ qualityThreshold,
515
+ datasetVersion,
516
+ requireEdits: parseBool(args['require-edits'], true),
517
+ requireCodingIntent: parseBool(args['require-coding-intent'], true),
518
+ maxToolExamplesPerSession: args['max-tool-examples-per-session']
519
+ ? Number(args['max-tool-examples-per-session'])
520
+ : 12,
521
+ }));
522
+ }
523
+ examples = examples.filter((example) => example.quality >= qualityThreshold);
524
+ examples = applyPerToolLimit(examples, args['per-tool-limit']);
525
+ renderedLengthFilter = applyMaxRenderedChars(examples, {
526
+ format,
527
+ maxChars: args['max-rendered-chars'] ? Number(args['max-rendered-chars']) : 0,
528
+ });
529
+ examples = renderedLengthFilter.examples;
530
+ if (maxExamples > 0) examples = examples.slice(0, maxExamples);
531
+ if (useDecisiveActions) decisiveActionAudit = auditDecisiveActionExamples(examples);
532
+ if ((usePersonalReplay || useDecisiveActions) && personalReplayEvalCases) {
533
+ personalReplayEvalCases = buildPersonalReplayEvalCases({
534
+ examples,
535
+ families: workflowFamilies,
536
+ maxExact: args['max-seen-exact-eval'] ? Number(args['max-seen-exact-eval']) : 120,
537
+ maxVariant: args['max-seen-variant-eval'] ? Number(args['max-seen-variant-eval']) : 120,
538
+ });
539
+ }
540
+ const realQualityGate = buildRealDatasetQualityGate({ audit: realAudit, examples, args });
541
+ personalReplayDataGate = buildPersonalReplayDataGate({ workflowAudit, examples, args });
542
+ decisiveActionDataGate = buildDecisiveActionDataGate({ audit: decisiveActionAudit, args });
543
+ renderedLengthGate = buildRenderedLengthGate({ filter: renderedLengthFilter, args });
544
+ if (parseBool(args['fail-on-quality-gate'], false)) {
545
+ const failedGroups = [];
546
+ if (realQualityGate && !realQualityGate.ok) {
547
+ failedGroups.push(`real: ${formatGateFailures(realQualityGate.failed)}`);
548
+ }
549
+ if (decisionQualityGate && !decisionQualityGate.ok) {
550
+ failedGroups.push(`decision: ${formatGateFailures(decisionQualityGate.failed)}`);
551
+ }
552
+ if (personalReplayDataGate && !personalReplayDataGate.ok) {
553
+ failedGroups.push(`personalReplay: ${formatGateFailures(personalReplayDataGate.failed)}`);
554
+ }
555
+ if (decisiveActionDataGate && !decisiveActionDataGate.ok) {
556
+ failedGroups.push(`decisiveAction: ${formatGateFailures(decisiveActionDataGate.failed)}`);
557
+ }
558
+ if (renderedLengthGate && !renderedLengthGate.ok) {
559
+ failedGroups.push(`renderedLength: ${formatGateFailures(renderedLengthGate.failed)}`);
560
+ }
561
+ if (failedGroups.length) {
562
+ throw new Error(`Dataset quality gate failed: ${failedGroups.join('; ')}`);
563
+ }
564
+ }
565
+
566
+ const splits = splitExamplesBySource(examples, {
567
+ train: Number(args['train-ratio'] || 0.8),
568
+ valid: Number(args['valid-ratio'] || 0.1),
569
+ calibration: Number(args['calibration-ratio'] || 0.1),
570
+ }, args.seed || 'gemma-e4b-tooluse-v1');
571
+
572
+ let holdout = [];
573
+ if (args['holdout-manifest']) {
574
+ const parsed = JSON.parse(fs.readFileSync(path.resolve(args['holdout-manifest']), 'utf8'));
575
+ holdout = Array.isArray(parsed) ? parsed : (parsed.examples || parsed.holdout || []);
576
+ }
577
+ const leakage = checkHoldoutLeakage({ ...splits, holdout });
578
+ if (!leakage.ok) throw new Error(`Holdout leakage detected: ${leakage.leaked.map((x) => x.id || x.hash).join(', ')}`);
579
+
580
+ const manifest = buildDatasetManifest({
581
+ splits,
582
+ config: {
583
+ datasetVersion,
584
+ format,
585
+ stateSummaryMode,
586
+ source,
587
+ transcriptSource: args['transcript-source'] || null,
588
+ qualityThreshold,
589
+ targetedRepeats,
590
+ perToolLimit: args['per-tool-limit'] || null,
591
+ leakageCheck: { ok: leakage.ok, leaked: leakage.leaked.length },
592
+ realQualityGate,
593
+ decisionQualityGate,
594
+ personalReplayDataGate,
595
+ decisiveActionDataGate,
596
+ decisiveActionAudit,
597
+ renderedLengthGate,
598
+ renderedLengthFilter: renderedLengthFilter ? {
599
+ maxChars: renderedLengthFilter.maxChars,
600
+ dropped: renderedLengthFilter.dropped,
601
+ maxSeenChars: renderedLengthFilter.maxSeenChars,
602
+ kept: examples.length,
603
+ } : null,
604
+ workflowAudit: workflowAudit ? {
605
+ totalFamilies: workflowAudit.totalFamilies,
606
+ repeatedFamilies: workflowAudit.repeatedFamilies,
607
+ editVerificationTrajectories: workflowAudit.editVerificationTrajectories,
608
+ coveredTrajectories: workflowAudit.coveredTrajectories,
609
+ } : null,
610
+ replayEvalCases: personalReplayEvalCases ? {
611
+ seenExact: personalReplayEvalCases.seenExact.length,
612
+ seenVariant: personalReplayEvalCases.seenVariant.length,
613
+ } : null,
614
+ decisionAudit: decisionAudit ? {
615
+ totalDecisionPoints: decisionAudit.totalDecisionPoints,
616
+ guardRecoveryCount: decisionAudit.guardRecoveryCount,
617
+ sameInspectionAfterGuardCount: decisionAudit.sameInspectionAfterGuardCount,
618
+ targetToolCounts: decisionAudit.targetToolCounts,
619
+ labelCounts: decisionAudit.labelCounts,
620
+ } : null,
621
+ },
622
+ });
623
+ manifest.leakageCheck = { ok: leakage.ok, leaked: leakage.leaked.length };
624
+ const written = writeDatasetFiles({ outputDir: datasetDir, splits, manifest });
625
+ for (const [split, rows] of Object.entries({ train: splits.train, valid: splits.valid, test: splits.calibration })) {
626
+ writeRenderedSplit({ examples: rows, outputDir: datasetDir, splitName: split, format });
627
+ }
628
+ let replayEval = null;
629
+ if (personalReplayEvalCases) {
630
+ replayEval = writeReplayEvalCases({
631
+ outputDir: path.join(outputDir, 'replay-eval'),
632
+ cases: personalReplayEvalCases,
633
+ format,
634
+ });
635
+ }
636
+ fs.writeFileSync(path.join(outputDir, 'training-summary.json'), JSON.stringify({
637
+ outputDir,
638
+ datasetDir,
639
+ manifest: written.manifest,
640
+ replayEval,
641
+ }, null, 2) + '\n');
642
+ return { outputDir, datasetDir, manifest: written.manifest, replayEval };
643
+ }
644
+
645
+ function formatGateFailures(failed = []) {
646
+ return failed.map((check) => {
647
+ if (check.required !== undefined) return `${check.name} ${check.actual}/${check.required}`;
648
+ if (check.maximum !== undefined) return `${check.name} ${check.actual}>${check.maximum}`;
649
+ return `${check.name} actual=${check.actual}`;
650
+ }).join(', ');
651
+ }
652
+
653
+ function writeJsonl(filePath, rows = []) {
654
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
655
+ fs.writeFileSync(filePath, rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : ''), 'utf8');
656
+ }
657
+
658
+ function writeReplayEvalCases({ outputDir, cases, format = 'mlx-tools' } = {}) {
659
+ if (!outputDir) throw new Error('outputDir is required');
660
+ fs.mkdirSync(outputDir, { recursive: true });
661
+ const seenExactPath = path.join(outputDir, 'seen-exact.jsonl');
662
+ const seenVariantPath = path.join(outputDir, 'seen-variant.jsonl');
663
+ writeJsonl(seenExactPath, cases.seenExact || []);
664
+ writeJsonl(seenVariantPath, cases.seenVariant || []);
665
+ const manifest = {
666
+ generatedAt: new Date().toISOString(),
667
+ evaluationPolicy: 'personal_seen_replay',
668
+ leakageAllowed: true,
669
+ purpose: 'verify repeatability of known local workflows and light parameter variation',
670
+ format,
671
+ counts: {
672
+ seenExact: cases.seenExact?.length || 0,
673
+ seenVariant: cases.seenVariant?.length || 0,
674
+ },
675
+ files: {
676
+ seenExact: path.basename(seenExactPath),
677
+ seenVariant: path.basename(seenVariantPath),
678
+ },
679
+ };
680
+ const manifestPath = path.join(outputDir, 'manifest.json');
681
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
682
+ return { outputDir, seenExactPath, seenVariantPath, manifestPath, manifest };
683
+ }
684
+
685
+ function exportReplaySliceDataset(args) {
686
+ const outputDir = path.resolve(args['output-dir'] || defaultOutputDir());
687
+ const datasetDir = path.join(outputDir, 'dataset');
688
+ const format = args.format || 'mlx-provider-state-bare';
689
+ const evalFile = args['eval-file'] ? path.resolve(args['eval-file']) : null;
690
+ if (!evalFile || !fs.existsSync(evalFile)) throw new Error(`Missing eval file: ${evalFile}`);
691
+ const resultsFile = args['results-file'] ? path.resolve(args['results-file']) : null;
692
+ if (resultsFile && !fs.existsSync(resultsFile)) throw new Error(`Missing results file: ${resultsFile}`);
693
+ const limit = args.limit ? Number(args.limit) : 12;
694
+ const repeats = args.repeats ? Number(args.repeats) : 8;
695
+ const targetMode = args['target-mode'] || 'full';
696
+ const includeTools = new Set(parseList(args['include-tools']));
697
+ const datasetVersion = args['dataset-version'] || 'gemma-e4b-tooluse-replay-slice';
698
+ const sliceMode = args['slice-mode'] || (resultsFile ? 'failures' : 'all');
699
+ const normalizedRows = readJsonl(evalFile)
700
+ .map((row) => normalizeReplayEvalExample(row))
701
+ .filter(Boolean);
702
+ const filteredRows = includeTools.size
703
+ ? normalizedRows.filter((row) => includeTools.has(targetToolCallFromExample(row)?.name))
704
+ : normalizedRows;
705
+ const resultFilteredRows = selectReplaySliceRows(filteredRows, { resultsFile, sliceMode });
706
+ const baseRows = resultFilteredRows.rows.slice(0, limit);
707
+ if (!baseRows.length) {
708
+ const toolSuffix = includeTools.size ? ` for tools: ${[...includeTools].join(',')}` : '';
709
+ const resultSuffix = resultsFile ? ` with slice mode: ${sliceMode}` : '';
710
+ throw new Error(`No replay rows selected${toolSuffix}${resultSuffix}`);
711
+ }
712
+ const examples = [];
713
+ for (let repeat = 0; repeat < Math.max(1, repeats); repeat += 1) {
714
+ for (const row of baseRows) {
715
+ const clone = JSON.parse(JSON.stringify(row));
716
+ clone.id = `${row.id}-replay-slice-${repeat}`;
717
+ clone.dataset_version = datasetVersion;
718
+ clone.source = `${row.source || 'replay-eval'}-replay-slice`;
719
+ clone.quality = Math.min(0.995, Math.max(0.8, Number(row.quality || 0.9)));
720
+ if (targetMode === 'tool-choice') eraseTargetArguments(clone);
721
+ clone.labels = {
722
+ ...(row.labels || {}),
723
+ replay_slice_training: true,
724
+ replay_slice_repeat: repeat,
725
+ replay_slice_target_mode: targetMode,
726
+ };
727
+ clone.leakage = {
728
+ ...(row.leakage || {}),
729
+ holdout: false,
730
+ source_session_id: `${row.leakage?.source_session_id || row.id}:replay-slice:${repeat}`,
731
+ source_task_id: `${row.leakage?.source_task_id || row.id}:replay-slice:${repeat}`,
732
+ };
733
+ examples.push(clone);
734
+ }
735
+ }
736
+ const renderedLengthFilter = applyMaxRenderedChars(examples, {
737
+ format,
738
+ maxChars: args['max-rendered-chars'] ? Number(args['max-rendered-chars']) : 0,
739
+ });
740
+ const keptExamples = renderedLengthFilter.examples;
741
+ const splits = ensureNonEmptyMlxTrainingSplits(splitExamplesBySource(keptExamples, {
742
+ train: Number(args['train-ratio'] || 0.96),
743
+ valid: Number(args['valid-ratio'] || 0.02),
744
+ calibration: Number(args['calibration-ratio'] || 0.02),
745
+ }, args.seed || 'gemma-e4b-tooluse-replay-slice'));
746
+ const manifest = buildDatasetManifest({
747
+ splits,
748
+ config: {
749
+ datasetVersion,
750
+ format,
751
+ source: 'replay-slice',
752
+ evalFile,
753
+ resultsFile,
754
+ limit,
755
+ repeats,
756
+ targetMode,
757
+ sliceMode,
758
+ includeTools: [...includeTools],
759
+ rowSelection: {
760
+ normalized: normalizedRows.length,
761
+ afterToolFilter: filteredRows.length,
762
+ ...(resultsFile ? { afterResultFilter: resultFilteredRows.rows.length } : {}),
763
+ selected: baseRows.length,
764
+ },
765
+ resultSelection: resultsFile ? resultFilteredRows.summary : null,
766
+ renderedLengthFilter: {
767
+ maxChars: renderedLengthFilter.maxChars,
768
+ dropped: renderedLengthFilter.dropped,
769
+ maxSeenChars: renderedLengthFilter.maxSeenChars,
770
+ kept: keptExamples.length,
771
+ },
772
+ splitRepair: splits.__repair || null,
773
+ },
774
+ });
775
+ manifest.leakageCheck = { ok: true, leaked: 0, replaySlice: true };
776
+ const written = writeDatasetFiles({ outputDir: datasetDir, splits, manifest });
777
+ for (const [split, rows] of Object.entries({ train: splits.train, valid: splits.valid, test: splits.calibration })) {
778
+ writeRenderedSplit({ examples: rows, outputDir: datasetDir, splitName: split, format });
779
+ }
780
+ const replayEval = writeReplayEvalCases({
781
+ outputDir: path.join(outputDir, 'replay-eval'),
782
+ cases: { seenExact: baseRows, seenVariant: [] },
783
+ format,
784
+ });
785
+ fs.writeFileSync(path.join(outputDir, 'training-summary.json'), JSON.stringify({
786
+ outputDir,
787
+ datasetDir,
788
+ manifest: written.manifest,
789
+ replayEval,
790
+ }, null, 2) + '\n');
791
+ return { outputDir, datasetDir, manifest: written.manifest, replayEval };
792
+ }
793
+
794
+ function ensureNonEmptyMlxTrainingSplits(inputSplits = {}) {
795
+ const splits = {
796
+ train: [...(inputSplits.train || [])],
797
+ valid: [...(inputSplits.valid || [])],
798
+ calibration: [...(inputSplits.calibration || [])],
799
+ ab_holdout: [...(inputSplits.ab_holdout || [])],
800
+ };
801
+ const allRows = [...splits.train, ...splits.valid, ...splits.calibration, ...splits.ab_holdout];
802
+ if (!allRows.length) return splits;
803
+ const repair = { train: 0, valid: 0, calibration: 0, reason: 'mlx_lora_requires_non_empty_local_subsets' };
804
+ if (!splits.train.length) {
805
+ splits.train.push(cloneReplaySplitBackfillRow(allRows[0], 'train', 0));
806
+ repair.train += 1;
807
+ }
808
+ for (const split of ['valid', 'calibration']) {
809
+ if (splits[split].length) continue;
810
+ const source = splits.train[(repair[split] || 0) % splits.train.length] || allRows[0];
811
+ splits[split].push(cloneReplaySplitBackfillRow(source, split, repair[split] || 0));
812
+ repair[split] += 1;
813
+ }
814
+ if (repair.train || repair.valid || repair.calibration) {
815
+ Object.defineProperty(splits, '__repair', {
816
+ value: repair,
817
+ enumerable: false,
818
+ });
819
+ }
820
+ return splits;
821
+ }
822
+
823
+ function cloneReplaySplitBackfillRow(row = {}, split = 'valid', idx = 0) {
824
+ const clone = JSON.parse(JSON.stringify(row));
825
+ clone.id = `${row.id || 'replay-row'}-mlx-${split}-backfill-${idx}`;
826
+ clone.split = split;
827
+ clone.labels = {
828
+ ...(clone.labels || {}),
829
+ replay_slice_split_backfill: true,
830
+ replay_slice_split_backfill_split: split,
831
+ replay_slice_split_backfill_source_id: row.id || null,
832
+ };
833
+ clone.leakage = {
834
+ ...(clone.leakage || {}),
835
+ holdout: split === 'ab_holdout',
836
+ };
837
+ return clone;
838
+ }
839
+
840
+ function selectReplaySliceRows(rows = [], { resultsFile = null, sliceMode = 'all' } = {}) {
841
+ const mode = sliceMode || 'all';
842
+ if (!resultsFile || mode === 'all') {
843
+ return { rows, summary: { mode, resultsFile: resultsFile || null, available: rows.length, selected: rows.length } };
844
+ }
845
+ const resultsById = new Map(readJsonl(resultsFile).map((row) => [row.id, row]));
846
+ const selected = [];
847
+ const summary = {
848
+ mode,
849
+ resultsFile,
850
+ available: rows.length,
851
+ resultRows: resultsById.size,
852
+ matched: 0,
853
+ parserFailures: 0,
854
+ wrongTool: 0,
855
+ failures: 0,
856
+ selected: 0,
857
+ };
858
+ for (const row of rows) {
859
+ const result = resultsById.get(row.id);
860
+ if (!result) continue;
861
+ summary.matched += 1;
862
+ const parserFailure = replayResultParserFailure(result);
863
+ const wrongTool = replayResultWrongTool(result, row);
864
+ const failed = !result.score?.pass;
865
+ if (parserFailure) summary.parserFailures += 1;
866
+ if (wrongTool) summary.wrongTool += 1;
867
+ if (failed) summary.failures += 1;
868
+ if (replaySliceResultMatchesMode({ mode, parserFailure, wrongTool, failed })) selected.push(row);
869
+ }
870
+ summary.selected = selected.length;
871
+ return { rows: selected, summary };
872
+ }
873
+
874
+ function replaySliceResultMatchesMode({ mode, parserFailure, wrongTool, failed }) {
875
+ if (mode === 'failures') return failed;
876
+ if (mode === 'parser-failures') return parserFailure;
877
+ if (mode === 'wrong-tool') return wrongTool;
878
+ if (mode === 'parser-or-wrong-tool') return parserFailure || wrongTool;
879
+ throw new Error(`Unsupported replay slice mode: ${mode}`);
880
+ }
881
+
882
+ function replayResultParserFailure(result = {}) {
883
+ if (!result.prediction) return true;
884
+ if (result.parsedToolCallCount !== undefined && Number(result.parsedToolCallCount) <= 0) return true;
885
+ return false;
886
+ }
887
+
888
+ function replayResultWrongTool(result = {}, example = {}) {
889
+ if (!result.prediction) return false;
890
+ if (result.score?.toolNameMatch === false) return true;
891
+ const predictedTool = result.prediction.tool || result.prediction.name;
892
+ const expectedTool = result.expected?.tool || targetToolCallFromExample(example)?.name;
893
+ return !!predictedTool && !!expectedTool && predictedTool !== expectedTool;
894
+ }
895
+
896
+ function eraseTargetArguments(example = {}) {
897
+ for (let i = (example.messages || []).length - 1; i >= 0; i -= 1) {
898
+ const message = example.messages[i];
899
+ if (message.role !== 'assistant' || !Array.isArray(message.tool_calls) || !message.tool_calls.length) continue;
900
+ for (const call of message.tool_calls) {
901
+ if (call.function) call.function.arguments = '{}';
902
+ else call.arguments = '{}';
903
+ }
904
+ return example;
905
+ }
906
+ return example;
907
+ }
908
+
909
+ function auditRealTrajectories(args) {
910
+ const outputDir = path.resolve(args['output-dir'] || path.join(defaultOutputDir(), 'real-trajectory-audit'));
911
+ const roots = parseRoots(args);
912
+ const includeCtm = parseBool(args['include-ctm'], true);
913
+ const mined = mineRealTrajectories({
914
+ roots: roots.length ? roots.map((root) => path.resolve(root)) : null,
915
+ repoPath: args.repo ? path.resolve(args.repo) : null,
916
+ source: args['transcript-source'] || args.source || 'all',
917
+ sinceDays: args['since-days'] === undefined ? 14 : Number(args['since-days']),
918
+ limit: args['scan-limit'] ? Number(args['scan-limit']) : 500,
919
+ includeCtm,
920
+ includeFilesystem: parseBool(args['include-filesystem'], roots.length > 0 || !includeCtm),
921
+ ctmDbPath: args['ctm-db'] ? path.resolve(args['ctm-db']) : undefined,
922
+ requireCodingIntent: parseBool(args['require-coding-intent'], true),
923
+ maxResultChars: args['max-result-chars'] ? Number(args['max-result-chars']) : undefined,
924
+ maxFileBytes: args['max-file-mb'] ? Number(args['max-file-mb']) * 1024 * 1024 : undefined,
925
+ perSourceLimit: parsePerSourceLimit(args['per-source-limit']),
926
+ recoverGit: parseBool(args['recover-git'], false),
927
+ gitRecoveryWindowMs: args['git-recovery-window-minutes']
928
+ ? Number(args['git-recovery-window-minutes']) * 60 * 1000
929
+ : undefined,
930
+ gitRecoveryRequireOverlap: !parseBool(args['allow-weak-git-recovery'], false),
931
+ });
932
+ const written = writeTrajectoryAudit({
933
+ trajectories: mined.trajectories,
934
+ outputDir,
935
+ includePaths: parseBool(args['include-paths'], false),
936
+ skipped: mined.skipped,
937
+ diagnostics: mined.diagnostics,
938
+ });
939
+ return {
940
+ outputDir,
941
+ scanned: mined.scanned,
942
+ trajectories: mined.trajectories.length,
943
+ skipped: mined.skipped,
944
+ auditPath: written.auditPath,
945
+ manifestPath: written.manifestPath,
946
+ tierCounts: written.audit.tierCounts,
947
+ sourceCounts: written.audit.sourceCounts,
948
+ signalCounts: written.audit.signalCounts,
949
+ datasetCandidateCounts: written.audit.datasetCandidateCounts,
950
+ resultCoverage: written.audit.resultCoverage,
951
+ };
952
+ }
953
+
954
+ function auditDecisionPointsCommand(args) {
955
+ const outputDir = path.resolve(args['output-dir'] || path.join(defaultOutputDir(), 'decision-point-audit'));
956
+ const roots = parseRoots(args);
957
+ const includeCtm = parseBool(args['include-ctm'], true);
958
+ const mined = mineRealTrajectories({
959
+ roots: roots.length ? roots.map((root) => path.resolve(root)) : null,
960
+ repoPath: args.repo ? path.resolve(args.repo) : null,
961
+ source: args['transcript-source'] || args.source || 'all',
962
+ sinceDays: args['since-days'] === undefined ? 14 : Number(args['since-days']),
963
+ limit: args['scan-limit'] ? Number(args['scan-limit']) : 500,
964
+ includeCtm,
965
+ includeFilesystem: parseBool(args['include-filesystem'], roots.length > 0 || !includeCtm),
966
+ ctmDbPath: args['ctm-db'] ? path.resolve(args['ctm-db']) : undefined,
967
+ requireCodingIntent: parseBool(args['require-coding-intent'], true),
968
+ maxResultChars: args['max-result-chars'] ? Number(args['max-result-chars']) : undefined,
969
+ maxFileBytes: args['max-file-mb'] ? Number(args['max-file-mb']) * 1024 * 1024 : undefined,
970
+ perSourceLimit: parsePerSourceLimit(args['per-source-limit']),
971
+ recoverGit: parseBool(args['recover-git'], false),
972
+ gitRecoveryWindowMs: args['git-recovery-window-minutes']
973
+ ? Number(args['git-recovery-window-minutes']) * 60 * 1000
974
+ : undefined,
975
+ gitRecoveryRequireOverlap: !parseBool(args['allow-weak-git-recovery'], false),
976
+ });
977
+ const decisionPoints = decisionPointsFromTrajectories(mined.trajectories, {
978
+ includeMessages: parseBool(args['include-messages'], false),
979
+ });
980
+ const written = writeDecisionPointAudit({
981
+ trajectories: mined.trajectories,
982
+ decisionPoints,
983
+ outputDir,
984
+ includeMessages: parseBool(args['include-messages'], false),
985
+ gate: {
986
+ minDecisionPoints: parseNumber(args['min-decision-points'], 0),
987
+ minGuardRecovery: parseNumber(args['min-guard-recovery'], 0),
988
+ maxSameInspectionAfterGuard: parseNumber(args['max-same-inspection-after-guard'], 0),
989
+ },
990
+ });
991
+ if (!written.audit.gate.ok && parseBool(args['fail-on-quality-gate'], false)) {
992
+ const detail = written.audit.gate.failed
993
+ .map((check) => `${check.name} actual=${check.actual} ${check.required !== undefined ? `required=${check.required}` : `maximum=${check.maximum}`}`)
994
+ .join(', ');
995
+ throw new Error(`Decision-point quality gate failed: ${detail}`);
996
+ }
997
+ return {
998
+ outputDir,
999
+ scanned: mined.scanned,
1000
+ trajectories: mined.trajectories.length,
1001
+ decisionPoints: decisionPoints.length,
1002
+ skipped: mined.skipped,
1003
+ decisionPath: written.decisionPath,
1004
+ auditPath: written.auditPath,
1005
+ manifestPath: written.manifestPath,
1006
+ targetToolCounts: written.audit.targetToolCounts,
1007
+ labelCounts: written.audit.labelCounts,
1008
+ gate: written.audit.gate,
1009
+ };
1010
+ }
1011
+
1012
+ function auditWorkflowsCommand(args) {
1013
+ const outputDir = path.resolve(args['output-dir'] || path.join(defaultOutputDir(), 'workflow-audit'));
1014
+ const roots = parseRoots(args);
1015
+ const includeCtm = parseBool(args['include-ctm'], true);
1016
+ const mined = mineRealTrajectories({
1017
+ roots: roots.length ? roots.map((root) => path.resolve(root)) : null,
1018
+ repoPath: args.repo ? path.resolve(args.repo) : null,
1019
+ source: args['transcript-source'] || args.source || 'all',
1020
+ sinceDays: args['since-days'] === undefined ? 90 : Number(args['since-days']),
1021
+ limit: args['scan-limit'] ? Number(args['scan-limit']) : 1000,
1022
+ includeCtm,
1023
+ includeFilesystem: parseBool(args['include-filesystem'], roots.length > 0 || !includeCtm),
1024
+ ctmDbPath: args['ctm-db'] ? path.resolve(args['ctm-db']) : undefined,
1025
+ requireCodingIntent: parseBool(args['require-coding-intent'], true),
1026
+ maxResultChars: args['max-result-chars'] ? Number(args['max-result-chars']) : undefined,
1027
+ maxFileBytes: args['max-file-mb'] ? Number(args['max-file-mb']) * 1024 * 1024 : undefined,
1028
+ perSourceLimit: parsePerSourceLimit(args['per-source-limit']),
1029
+ recoverGit: parseBool(args['recover-git'], false),
1030
+ gitRecoveryWindowMs: args['git-recovery-window-minutes']
1031
+ ? Number(args['git-recovery-window-minutes']) * 60 * 1000
1032
+ : undefined,
1033
+ gitRecoveryRequireOverlap: !parseBool(args['allow-weak-git-recovery'], false),
1034
+ });
1035
+ const written = writeWorkflowCatalog({
1036
+ trajectories: mined.trajectories,
1037
+ outputDir,
1038
+ includeTiers: parseList(args['include-tiers']).length ? parseList(args['include-tiers']) : ['gold', 'silver'],
1039
+ });
1040
+ return {
1041
+ outputDir,
1042
+ scanned: mined.scanned,
1043
+ trajectories: mined.trajectories.length,
1044
+ skipped: mined.skipped,
1045
+ catalogPath: written.catalogPath,
1046
+ auditPath: written.auditPath,
1047
+ totalFamilies: written.audit.totalFamilies,
1048
+ repeatedFamilies: written.audit.repeatedFamilies,
1049
+ editVerificationTrajectories: written.audit.editVerificationTrajectories,
1050
+ gates: written.audit.gates,
1051
+ topFamilies: written.audit.topFamilies.slice(0, 10),
1052
+ };
1053
+ }
1054
+
1055
+ async function evalReplayDataset(args) {
1056
+ const outputDir = path.resolve(args['output-dir'] || path.join(defaultOutputDir(), 'replay-eval-results'));
1057
+ const datasetOutputDir = args['dataset-output-dir'] ? path.resolve(args['dataset-output-dir']) : null;
1058
+ const evalFile = args['eval-file']
1059
+ ? path.resolve(args['eval-file'])
1060
+ : path.join(datasetOutputDir || path.resolve(args['dataset-dir'] || ''), 'replay-eval', `${args.lane || 'seen-exact'}.jsonl`);
1061
+ if (!evalFile || !fs.existsSync(evalFile)) throw new Error(`Missing eval file: ${evalFile}`);
1062
+ const model = args.model || 'gemma4:e4b-it-qat';
1063
+ const toolCallFormat = normalizeToolCallFormat(args['tool-call-format']);
1064
+ const provider = createMLXProvider({
1065
+ adapterPath: args['adapter-path'] ? path.resolve(args['adapter-path']) : undefined,
1066
+ inferenceTimeoutMs: args['inference-timeout-ms'] ? Number(args['inference-timeout-ms']) : undefined,
1067
+ toolCallFormat,
1068
+ forceToolJsonPrefix: parseBool(args['force-tool-json-prefix'], false),
1069
+ });
1070
+ const rows = readJsonl(evalFile)
1071
+ .map((row) => normalizeReplayEvalExample(row))
1072
+ .filter(Boolean);
1073
+ const limit = args.limit ? Number(args.limit) : rows.length;
1074
+ const maxTokens = args['max-tokens'] ? Number(args['max-tokens']) : 256;
1075
+ const actionMemorySelector = args['action-memory-selector'] || 'off';
1076
+ const runShellCommandSelector = args['run-shell-command-selector'] || (actionMemorySelector === 'store' ? 'store' : 'off');
1077
+ const runShellMaxCandidates = args['run-shell-max-candidates'] ? Number(args['run-shell-max-candidates']) : 12;
1078
+ const actionMemoryMaxCandidates = args['action-memory-max-candidates'] ? Number(args['action-memory-max-candidates']) : 24;
1079
+ const actionMemoryMinScore = args['action-memory-min-score'] ? Number(args['action-memory-min-score']) : 100;
1080
+ const actionMemorySelectionUsesStore = ['store', 'sidecar', 'action-memory'].includes(actionMemorySelector);
1081
+ const runShellSelectionUsesStore = ['store', 'sidecar', 'action-memory'].includes(runShellCommandSelector);
1082
+ const useActionMemoryStore = actionMemorySelectionUsesStore || runShellSelectionUsesStore;
1083
+ const actionMemoryFiles = (actionMemorySelector === 'heuristic' || useActionMemoryStore)
1084
+ ? actionMemoryFilePaths(args, evalFile)
1085
+ : [];
1086
+ const actionMemory = actionMemorySelector === 'heuristic'
1087
+ ? loadTrajectoryActionMemory(actionMemoryFiles)
1088
+ : [];
1089
+ let actionMemoryStore = null;
1090
+ let actionMemoryStoreInfo = null;
1091
+ if (useActionMemoryStore) {
1092
+ const storePaths = actionMemoryStorePaths(args, outputDir);
1093
+ actionMemoryStore = new ActionMemoryStore(storePaths).init();
1094
+ if (parseBool(args['action-memory-build-from-files'], true)) {
1095
+ actionMemoryStoreInfo = recordTrajectoryRowsToActionMemoryStore(actionMemoryFiles, {
1096
+ store: actionMemoryStore,
1097
+ excludeIds: parseBool(args['action-memory-allow-eval-rows'], false)
1098
+ ? new Set()
1099
+ : new Set(rows.map((row) => row.id).filter(Boolean)),
1100
+ excludeFamilyIds: parseBool(args['action-memory-exclude-eval-families'], false)
1101
+ ? new Set(rows.map((row) => row.labels?.workflow_family_id).filter(Boolean))
1102
+ : new Set(),
1103
+ sourceKind: 'trajectory-jsonl',
1104
+ model: {
1105
+ provider: args['source-provider'] || '',
1106
+ modelId: args['source-model'] || '',
1107
+ toolFormat: args['source-tool-format'] || '',
1108
+ },
1109
+ });
1110
+ } else {
1111
+ actionMemoryStoreInfo = { dbPath: storePaths.dbPath, artifactsDir: storePaths.artifactsDir, recorded: 0, skipped: 0 };
1112
+ }
1113
+ }
1114
+ const results = [];
1115
+ for (const example of rows.slice(0, limit)) {
1116
+ const target = targetToolCallFromExample(example);
1117
+ if (!target) continue;
1118
+ const promptMessages = example.messages.slice(0, target.messageIndex);
1119
+ const startedAt = Date.now();
1120
+ let prediction = null;
1121
+ let error = null;
1122
+ let raw = null;
1123
+ try {
1124
+ raw = await provider.chat({
1125
+ model,
1126
+ messages: promptMessages,
1127
+ tools: example.tools,
1128
+ maxTokens,
1129
+ temperature: 0,
1130
+ });
1131
+ prediction = raw.toolCalls?.[0] || null;
1132
+ if (runShellCommandSelector !== 'off') {
1133
+ const selected = applyRunShellCommandSelection(example, prediction, {
1134
+ maxCandidates: runShellMaxCandidates,
1135
+ actionMemoryStore: runShellSelectionUsesStore ? actionMemoryStore : null,
1136
+ });
1137
+ prediction = selected.prediction;
1138
+ raw.runShellCommandSelection = selected.selection;
1139
+ raw.runShellCommandCandidates = selected.candidates;
1140
+ raw.runShellCommandOracle = target.name === 'run_shell'
1141
+ ? scoreRunShellCommandCandidates(target.args?.command || '', selected.candidates, selected.selection)
1142
+ : null;
1143
+ }
1144
+ if (actionMemorySelector === 'heuristic' || actionMemorySelectionUsesStore) {
1145
+ const selected = applyTrajectoryActionSelection(example, prediction, {
1146
+ memory: actionMemory,
1147
+ actionMemoryStore: actionMemorySelectionUsesStore ? actionMemoryStore : null,
1148
+ rawText: raw?.rawText || raw?.content || '',
1149
+ maxCandidates: actionMemoryMaxCandidates,
1150
+ minScore: actionMemoryMinScore,
1151
+ });
1152
+ prediction = selected.prediction;
1153
+ raw.actionMemorySelection = selected.selection;
1154
+ raw.actionMemoryCandidates = selected.candidates;
1155
+ raw.actionMemoryOracle = scoreTrajectoryActionCandidates(target, selected.candidates, selected.selection);
1156
+ }
1157
+ } catch (err) {
1158
+ error = err.message;
1159
+ }
1160
+ const score = scoreReplayPrediction({ expected: target, prediction });
1161
+ const rawText = raw?.rawText || raw?.content || '';
1162
+ results.push({
1163
+ id: example.id,
1164
+ lane: example.labels?.replay_eval_lane || example.labels?.replay_lane || args.lane || null,
1165
+ workflowFamilyId: example.labels?.workflow_family_id || null,
1166
+ expected: {
1167
+ tool: target.name,
1168
+ argKeys: Object.keys(target.args || {}).sort(),
1169
+ argsHash: stableStringify(target.args || {}),
1170
+ },
1171
+ prediction: prediction ? {
1172
+ tool: prediction.name,
1173
+ input: prediction.input || {},
1174
+ } : null,
1175
+ runShellCommandSelection: raw?.runShellCommandSelection || null,
1176
+ runShellCommandOracle: raw?.runShellCommandOracle || null,
1177
+ runShellCommandCandidateCount: raw?.runShellCommandCandidates?.length || 0,
1178
+ actionMemorySelection: raw?.actionMemorySelection || null,
1179
+ actionMemoryOracle: raw?.actionMemoryOracle || null,
1180
+ actionMemoryCandidateCount: raw?.actionMemoryCandidates?.length || 0,
1181
+ rawText: rawText ? String(rawText).slice(0, 1200) : '',
1182
+ rawDiagnostics: summarizeRawGeneration(rawText),
1183
+ parsedToolCallCount: raw?.toolCalls?.length || 0,
1184
+ score,
1185
+ error,
1186
+ latencyMs: raw?.latencyMs || Date.now() - startedAt,
1187
+ usage: raw?.usage || null,
1188
+ });
1189
+ }
1190
+ const summary = summarizeReplayEval(results);
1191
+ const actionMemoryReport = useActionMemoryStore
1192
+ ? summarizeActionMemoryReplayEval(results, {
1193
+ allowSameRowCopies: parseBool(args['action-memory-allow-eval-rows'], false),
1194
+ })
1195
+ : null;
1196
+ fs.mkdirSync(outputDir, { recursive: true });
1197
+ const resultPath = path.join(outputDir, 'replay-eval-results.jsonl');
1198
+ const summaryPath = path.join(outputDir, 'replay-eval-summary.json');
1199
+ writeJsonl(resultPath, results);
1200
+ fs.writeFileSync(summaryPath, JSON.stringify({
1201
+ generatedAt: new Date().toISOString(),
1202
+ model,
1203
+ adapterPath: args['adapter-path'] || null,
1204
+ toolCallFormat,
1205
+ forceToolJsonPrefix: parseBool(args['force-tool-json-prefix'], false),
1206
+ runShellCommandSelector,
1207
+ runShellMaxCandidates,
1208
+ actionMemorySelector,
1209
+ actionMemoryFiles,
1210
+ actionMemoryCandidateCount: actionMemory.length,
1211
+ actionMemoryStore: actionMemoryStoreInfo,
1212
+ actionMemoryMaxCandidates,
1213
+ actionMemoryMinScore,
1214
+ evalFile,
1215
+ limit,
1216
+ summary,
1217
+ actionMemoryReport,
1218
+ }, null, 2) + '\n', 'utf8');
1219
+ if (actionMemoryStore) actionMemoryStore.close();
1220
+ return { outputDir, evalFile, resultPath, summaryPath, summary, actionMemoryStore: actionMemoryStoreInfo };
1221
+ }
1222
+
1223
+ function evalReplayResults(args) {
1224
+ const outputDir = path.resolve(args['output-dir'] || path.join(defaultOutputDir(), 'replay-result-eval'));
1225
+ const evalFile = args['eval-file'] ? path.resolve(args['eval-file']) : null;
1226
+ const resultsFile = args['results-file'] ? path.resolve(args['results-file']) : null;
1227
+ if (!evalFile || !fs.existsSync(evalFile)) throw new Error(`Missing eval file: ${evalFile}`);
1228
+ if (!resultsFile || !fs.existsSync(resultsFile)) throw new Error(`Missing results file: ${resultsFile}`);
1229
+
1230
+ const examples = new Map(readJsonl(evalFile)
1231
+ .map((row) => normalizeReplayEvalExample(row))
1232
+ .filter(Boolean)
1233
+ .map((row) => [row.id, row]));
1234
+ const baselineRows = readJsonl(resultsFile);
1235
+ const limit = args.limit ? Number(args.limit) : baselineRows.length;
1236
+ const runShellCommandSelector = args['run-shell-command-selector'] || 'off';
1237
+ const runShellMaxCandidates = args['run-shell-max-candidates'] ? Number(args['run-shell-max-candidates']) : 12;
1238
+ const actionMemorySelector = args['action-memory-selector'] || 'off';
1239
+ const actionMemoryMaxCandidates = args['action-memory-max-candidates'] ? Number(args['action-memory-max-candidates']) : 24;
1240
+ const actionMemoryMinScore = args['action-memory-min-score'] ? Number(args['action-memory-min-score']) : 100;
1241
+ const actionMemorySelectionUsesStore = ['store', 'sidecar', 'action-memory'].includes(actionMemorySelector);
1242
+ const runShellSelectionUsesStore = ['store', 'sidecar', 'action-memory'].includes(runShellCommandSelector);
1243
+ const useActionMemoryStore = actionMemorySelectionUsesStore || runShellSelectionUsesStore;
1244
+ const actionMemoryFiles = (actionMemorySelector === 'heuristic' || useActionMemoryStore)
1245
+ ? actionMemoryFilePaths(args, evalFile)
1246
+ : [];
1247
+ const actionMemory = actionMemorySelector === 'heuristic'
1248
+ ? loadTrajectoryActionMemory(actionMemoryFiles)
1249
+ : [];
1250
+ let actionMemoryStore = null;
1251
+ let actionMemoryStoreInfo = null;
1252
+ try {
1253
+ if (useActionMemoryStore) {
1254
+ const storePaths = actionMemoryStorePaths(args, outputDir);
1255
+ actionMemoryStore = new ActionMemoryStore(storePaths).init();
1256
+ if (parseBool(args['action-memory-build-from-files'], true)) {
1257
+ actionMemoryStoreInfo = recordTrajectoryRowsToActionMemoryStore(actionMemoryFiles, {
1258
+ store: actionMemoryStore,
1259
+ excludeIds: parseBool(args['action-memory-allow-eval-rows'], false)
1260
+ ? new Set()
1261
+ : new Set(examples.keys()),
1262
+ excludeFamilyIds: parseBool(args['action-memory-exclude-eval-families'], false)
1263
+ ? new Set([...examples.values()].map((row) => row.labels?.workflow_family_id).filter(Boolean))
1264
+ : new Set(),
1265
+ sourceKind: 'trajectory-jsonl',
1266
+ model: {
1267
+ provider: args['source-provider'] || '',
1268
+ modelId: args['source-model'] || '',
1269
+ toolFormat: args['source-tool-format'] || '',
1270
+ },
1271
+ });
1272
+ } else {
1273
+ actionMemoryStoreInfo = { dbPath: storePaths.dbPath, artifactsDir: storePaths.artifactsDir, recorded: 0, skipped: 0 };
1274
+ }
1275
+ }
1276
+
1277
+ const results = [];
1278
+ for (const baseline of baselineRows.slice(0, limit)) {
1279
+ const example = examples.get(baseline.id);
1280
+ if (!example) continue;
1281
+ const target = targetToolCallFromExample(example);
1282
+ if (!target) continue;
1283
+ let prediction = baseline.prediction
1284
+ ? { name: baseline.prediction.tool || baseline.prediction.name, input: baseline.prediction.input || {} }
1285
+ : null;
1286
+ const raw = {
1287
+ rawText: baseline.rawText || '',
1288
+ content: baseline.rawText || '',
1289
+ toolCalls: prediction ? [prediction] : [],
1290
+ };
1291
+ if (runShellCommandSelector !== 'off') {
1292
+ const selected = applyRunShellCommandSelection(example, prediction, {
1293
+ maxCandidates: runShellMaxCandidates,
1294
+ actionMemoryStore: runShellSelectionUsesStore ? actionMemoryStore : null,
1295
+ });
1296
+ prediction = selected.prediction;
1297
+ raw.runShellCommandSelection = selected.selection;
1298
+ raw.runShellCommandCandidates = selected.candidates;
1299
+ raw.runShellCommandOracle = target.name === 'run_shell'
1300
+ ? scoreRunShellCommandCandidates(target.args?.command || '', selected.candidates, selected.selection)
1301
+ : null;
1302
+ }
1303
+ if (actionMemorySelector === 'heuristic' || actionMemorySelectionUsesStore) {
1304
+ const selected = applyTrajectoryActionSelection(example, prediction, {
1305
+ memory: actionMemory,
1306
+ actionMemoryStore: actionMemorySelectionUsesStore ? actionMemoryStore : null,
1307
+ rawText: raw.rawText,
1308
+ maxCandidates: actionMemoryMaxCandidates,
1309
+ minScore: actionMemoryMinScore,
1310
+ });
1311
+ prediction = selected.prediction;
1312
+ raw.actionMemorySelection = selected.selection;
1313
+ raw.actionMemoryCandidates = selected.candidates;
1314
+ raw.actionMemoryOracle = scoreTrajectoryActionCandidates(target, selected.candidates, selected.selection);
1315
+ }
1316
+ const score = scoreReplayPrediction({ expected: target, prediction });
1317
+ results.push({
1318
+ id: example.id,
1319
+ lane: example.labels?.replay_eval_lane || example.labels?.replay_lane || baseline.lane || null,
1320
+ workflowFamilyId: example.labels?.workflow_family_id || null,
1321
+ expected: baseline.expected || {
1322
+ tool: target.name,
1323
+ argKeys: Object.keys(target.args || {}).sort(),
1324
+ argsHash: stableStringify(target.args || {}),
1325
+ },
1326
+ baselinePrediction: baseline.prediction || null,
1327
+ prediction: prediction ? { tool: prediction.name, input: prediction.input || {} } : null,
1328
+ runShellCommandSelection: raw.runShellCommandSelection || null,
1329
+ runShellCommandOracle: raw.runShellCommandOracle || null,
1330
+ runShellCommandCandidateCount: raw.runShellCommandCandidates?.length || 0,
1331
+ actionMemorySelection: raw.actionMemorySelection || null,
1332
+ actionMemoryOracle: raw.actionMemoryOracle || null,
1333
+ actionMemoryCandidateCount: raw.actionMemoryCandidates?.length || 0,
1334
+ rawText: raw.rawText ? String(raw.rawText).slice(0, 1200) : '',
1335
+ rawDiagnostics: summarizeRawGeneration(raw.rawText || ''),
1336
+ parsedToolCallCount: prediction ? 1 : 0,
1337
+ score,
1338
+ baselineScore: baseline.score || null,
1339
+ error: null,
1340
+ baselineError: baseline.error || null,
1341
+ latencyMs: 0,
1342
+ usage: null,
1343
+ });
1344
+ }
1345
+ const summary = summarizeReplayEval(results);
1346
+ const actionMemoryReport = useActionMemoryStore
1347
+ ? summarizeActionMemoryReplayEval(results, {
1348
+ allowSameRowCopies: parseBool(args['action-memory-allow-eval-rows'], false),
1349
+ })
1350
+ : null;
1351
+ fs.mkdirSync(outputDir, { recursive: true });
1352
+ const resultPath = path.join(outputDir, 'replay-eval-results.jsonl');
1353
+ const summaryPath = path.join(outputDir, 'replay-eval-summary.json');
1354
+ writeJsonl(resultPath, results);
1355
+ fs.writeFileSync(summaryPath, JSON.stringify({
1356
+ generatedAt: new Date().toISOString(),
1357
+ mode: 'offline-result-replay',
1358
+ evalFile,
1359
+ resultsFile,
1360
+ runShellCommandSelector,
1361
+ runShellMaxCandidates,
1362
+ actionMemorySelector,
1363
+ actionMemoryFiles,
1364
+ actionMemoryCandidateCount: actionMemory.length,
1365
+ actionMemoryStore: actionMemoryStoreInfo,
1366
+ actionMemoryMaxCandidates,
1367
+ actionMemoryMinScore,
1368
+ limit,
1369
+ baselineSummary: summarizeReplayEval(baselineRows.map((row) => ({ ...row, score: row.score || {} }))),
1370
+ summary,
1371
+ actionMemoryReport,
1372
+ }, null, 2) + '\n', 'utf8');
1373
+ return { outputDir, evalFile, resultsFile, resultPath, summaryPath, summary, actionMemoryStore: actionMemoryStoreInfo };
1374
+ } finally {
1375
+ if (actionMemoryStore) actionMemoryStore.close();
1376
+ }
1377
+ }
1378
+
1379
+ function actionMemoryFilePaths(args = {}, evalFile = '') {
1380
+ const explicit = [
1381
+ ...parseList(args['action-memory-file']),
1382
+ ...parseList(args['action-memory-files']),
1383
+ ].map((file) => path.resolve(file));
1384
+ if (explicit.length) return explicit;
1385
+ return defaultActionMemoryFilesForEval(evalFile);
1386
+ }
1387
+
1388
+ function actionMemoryStorePaths(args = {}, outputDir = '') {
1389
+ const dbPath = args['action-memory-db']
1390
+ ? path.resolve(args['action-memory-db'])
1391
+ : (outputDir
1392
+ ? path.join(path.resolve(outputDir), 'action-memory', 'action-memory.db')
1393
+ : defaultActionMemoryDbPath());
1394
+ return {
1395
+ dbPath,
1396
+ artifactsDir: args['action-memory-artifacts-dir']
1397
+ ? path.resolve(args['action-memory-artifacts-dir'])
1398
+ : path.join(path.dirname(dbPath), 'artifacts'),
1399
+ };
1400
+ }
1401
+
1402
+ function recordTrajectoryRowsToActionMemoryStore(filePaths = [], {
1403
+ store,
1404
+ excludeIds = new Set(),
1405
+ excludeFamilyIds = new Set(),
1406
+ sourceKind = 'trajectory-jsonl',
1407
+ model = {},
1408
+ maxRows = 20000,
1409
+ } = {}) {
1410
+ if (!store) throw new Error('recordTrajectoryRowsToActionMemoryStore requires store');
1411
+ const files = Array.isArray(filePaths) ? filePaths : [filePaths];
1412
+ let scanned = 0;
1413
+ let recorded = 0;
1414
+ let skipped = 0;
1415
+ const byTool = {};
1416
+ for (const filePath of files.map((file) => String(file || '')).filter(Boolean)) {
1417
+ if (!fs.existsSync(filePath)) {
1418
+ skipped += 1;
1419
+ continue;
1420
+ }
1421
+ for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
1422
+ if (scanned >= maxRows) break;
1423
+ if (!line.trim()) continue;
1424
+ scanned += 1;
1425
+ let row = null;
1426
+ try { row = normalizeReplayEvalExample(JSON.parse(line)); } catch {}
1427
+ if (!row || excludeIds.has(row.id) || excludeFamilyIds.has(row.labels?.workflow_family_id)) {
1428
+ skipped += 1;
1429
+ continue;
1430
+ }
1431
+ const target = targetToolCallFromExample(row);
1432
+ if (!target?.name) {
1433
+ skipped += 1;
1434
+ continue;
1435
+ }
1436
+ store.recordActionMemoryEntry({
1437
+ context: actionMemoryContextFromReplayExample(row, target),
1438
+ action: { toolName: target.name, input: target.args || {} },
1439
+ outcome: {
1440
+ status: actionMemoryOutcomeStatus(row),
1441
+ confidence: actionMemoryConfidence(row),
1442
+ verified: true,
1443
+ },
1444
+ source: {
1445
+ kind: sourceKind,
1446
+ rowId: row.id,
1447
+ sessionId: row.leakage?.source_session_id || row.labels?.source_session_id || '',
1448
+ toolCallId: targetToolCallIdFromExample(row, target.messageIndex),
1449
+ timestamp: row.timestamp || row.created_at || '',
1450
+ metadata: { sourceFile: path.resolve(filePath), source: row.source || '' },
1451
+ },
1452
+ model,
1453
+ metadata: {
1454
+ datasetVersion: row.dataset_version || '',
1455
+ split: row.split || '',
1456
+ replayLane: row.labels?.replay_eval_lane || row.labels?.replay_lane || '',
1457
+ quality: row.quality ?? null,
1458
+ },
1459
+ });
1460
+ recorded += 1;
1461
+ byTool[target.name] = (byTool[target.name] || 0) + 1;
1462
+ }
1463
+ }
1464
+ return {
1465
+ dbPath: store.dbPath,
1466
+ artifactsDir: store.artifactsDir,
1467
+ files: files.map((file) => path.resolve(file)),
1468
+ scanned,
1469
+ recorded,
1470
+ skipped,
1471
+ byTool,
1472
+ excludeIds: excludeIds.size,
1473
+ excludeFamilyIds: excludeFamilyIds.size,
1474
+ };
1475
+ }
1476
+
1477
+ function actionMemoryContextFromReplayExample(example = {}, target = {}) {
1478
+ const userContent = (example.messages || [])
1479
+ .filter((message) => message.role === 'user')
1480
+ .map((message) => message.content || '')
1481
+ .join('\n');
1482
+ return {
1483
+ familyId: example.labels?.workflow_family_id || null,
1484
+ repo: example.labels?.workflow_repo || null,
1485
+ intent: example.labels?.workflow_intent || null,
1486
+ intendedTool: target.name || example.labels?.tool_name || null,
1487
+ taskText: userContent.split(/\nCurrent Wall-E state:/)[0].replace(/^Task:\s*/i, '').trim(),
1488
+ sequence: firstLineValue(userContent, 'tool sequence') || null,
1489
+ lastTool: firstLineValue(userContent, 'last tool')?.split(/\s+/)[0] || null,
1490
+ toolCatalogId: example.tool_catalog || null,
1491
+ metadata: {
1492
+ replayLane: example.labels?.replay_eval_lane || example.labels?.replay_lane || '',
1493
+ },
1494
+ };
1495
+ }
1496
+
1497
+ function targetToolCallIdFromExample(example = {}, messageIndex = -1) {
1498
+ const message = example.messages?.[messageIndex];
1499
+ const call = message?.tool_calls?.[0] || null;
1500
+ return call?.id || call?.call_id || '';
1501
+ }
1502
+
1503
+ function actionMemoryOutcomeStatus(example = {}) {
1504
+ if (example.labels?.verification_passed || example.labels?.has_passing_verification) return 'verified_passed';
1505
+ return 'succeeded';
1506
+ }
1507
+
1508
+ function actionMemoryConfidence(example = {}) {
1509
+ const quality = Number(example.quality);
1510
+ if (Number.isFinite(quality)) return Math.max(0.1, Math.min(0.99, quality));
1511
+ return 0.85;
1512
+ }
1513
+
1514
+ function firstLineValue(text = '', label = '') {
1515
+ const re = new RegExp(`^\\s*-\\s*${String(label).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:\\s*(.+)$`, 'im');
1516
+ const match = String(text || '').match(re);
1517
+ return match ? match[1].trim() : null;
1518
+ }
1519
+
1520
+ function readJsonl(filePath) {
1521
+ return fs.readFileSync(filePath, 'utf8')
1522
+ .split('\n')
1523
+ .filter((line) => line.trim())
1524
+ .map((line) => JSON.parse(line));
1525
+ }
1526
+
1527
+ function normalizeReplayEvalExample(row) {
1528
+ try {
1529
+ const normalized = require('../training/tool-sft-dataset').normalizeCanonicalExample(row);
1530
+ return targetToolCallFromExample(normalized) ? normalized : null;
1531
+ } catch {
1532
+ return null;
1533
+ }
1534
+ }
1535
+
1536
+ function scoreReplayPrediction({ expected, prediction }) {
1537
+ if (!prediction) {
1538
+ return {
1539
+ pass: false,
1540
+ toolNameMatch: false,
1541
+ requiredArgsPresent: false,
1542
+ targetMatch: false,
1543
+ exactArgsMatch: false,
1544
+ argShapeScore: 0,
1545
+ };
1546
+ }
1547
+ const expectedArgs = expected.args || {};
1548
+ const predictedArgs = prediction.input || {};
1549
+ const toolNameMatch = prediction.name === expected.name;
1550
+ const requiredKeys = requiredArgKeysForTool(expected.name);
1551
+ const requiredArgsPresent = requiredKeys.every((key) => predictedArgs[key] !== undefined && predictedArgs[key] !== '');
1552
+ const targetMatch = targetArgMatches(expected.name, expectedArgs, predictedArgs);
1553
+ const exactArgsMatch = stableStringify(expectedArgs) === stableStringify(predictedArgs);
1554
+ const presentKeys = requiredKeys.filter((key) => predictedArgs[key] !== undefined && predictedArgs[key] !== '').length;
1555
+ const argShapeScore = requiredKeys.length ? presentKeys / requiredKeys.length : 1;
1556
+ return {
1557
+ pass: toolNameMatch && requiredArgsPresent && targetMatch,
1558
+ toolNameMatch,
1559
+ requiredArgsPresent,
1560
+ targetMatch,
1561
+ exactArgsMatch,
1562
+ argShapeScore: Number(argShapeScore.toFixed(4)),
1563
+ };
1564
+ }
1565
+
1566
+ function requiredArgKeysForTool(toolName) {
1567
+ if (toolName === 'read_file') return ['file_path'];
1568
+ if (toolName === 'list_directory') return ['directory'];
1569
+ if (toolName === 'glob') return ['pattern'];
1570
+ if (toolName === 'grep_files') return ['pattern'];
1571
+ if (toolName === 'edit_file') return ['file_path', 'old_string', 'new_string'];
1572
+ if (toolName === 'write_file') return ['file_path', 'content'];
1573
+ if (toolName === 'apply_patch') return ['patch_text'];
1574
+ if (toolName === 'run_shell') return ['command'];
1575
+ return [];
1576
+ }
1577
+
1578
+ function targetArgMatches(toolName, expectedArgs = {}, predictedArgs = {}) {
1579
+ if (toolName === 'read_file' || toolName === 'edit_file' || toolName === 'write_file') {
1580
+ if (!samePathish(expectedArgs.file_path, predictedArgs.file_path)) return false;
1581
+ if (toolName === 'edit_file') {
1582
+ return Boolean(predictedArgs.old_string && predictedArgs.new_string && predictedArgs.old_string !== predictedArgs.new_string);
1583
+ }
1584
+ return true;
1585
+ }
1586
+ if (toolName === 'list_directory') return samePathish(expectedArgs.directory, predictedArgs.directory);
1587
+ if (toolName === 'run_shell') return commandsEquivalentForMatch(expectedArgs.command, predictedArgs.command);
1588
+ if (toolName === 'glob') return String(expectedArgs.pattern || '') === String(predictedArgs.pattern || '');
1589
+ if (toolName === 'grep_files') return String(expectedArgs.pattern || '') === String(predictedArgs.pattern || '');
1590
+ if (toolName === 'apply_patch') return Boolean(predictedArgs.patch_text);
1591
+ return true;
1592
+ }
1593
+
1594
+ function samePathish(a, b) {
1595
+ const left = normalizePathish(a);
1596
+ const right = normalizePathish(b);
1597
+ return Boolean(left && right && (left === right || left.endsWith(`/${right}`) || right.endsWith(`/${left}`)));
1598
+ }
1599
+
1600
+ function normalizePathish(value) {
1601
+ return String(value || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '') || '.';
1602
+ }
1603
+
1604
+ function normalizeCommandForMatch(value) {
1605
+ return String(value || '').replace(/\s+/g, ' ').trim();
1606
+ }
1607
+
1608
+ function commandsEquivalentForMatch(expected, predicted) {
1609
+ const left = normalizeCommandForMatch(expected);
1610
+ const right = normalizeCommandForMatch(predicted);
1611
+ if (!left || !right) return false;
1612
+ if (left === right) return true;
1613
+ const leftClass = commandIntentClass(left);
1614
+ const rightClass = commandIntentClass(right);
1615
+ return Boolean(leftClass && rightClass && leftClass === rightClass);
1616
+ }
1617
+
1618
+ function commandIntentClass(command) {
1619
+ const value = normalizeCommandForMatch(command).toLowerCase();
1620
+ if (!value) return null;
1621
+ if (/^npm (run )?test\b/.test(value)) return 'npm-test';
1622
+ if (/^pnpm (run )?test\b/.test(value)) return 'pnpm-test';
1623
+ if (/^yarn (run )?test\b/.test(value)) return 'yarn-test';
1624
+ if (/^node --test\b/.test(value)) return 'node-test';
1625
+ if (/^node --check\b/.test(value)) return 'node-check';
1626
+ if (/^(python|python3) -m pytest\b|^pytest\b/.test(value)) return 'pytest';
1627
+ if (/playwright.*test|test.*playwright/.test(value)) return 'playwright-test';
1628
+ if (/^git diff --check\b/.test(value)) return 'git-diff-check';
1629
+ if (/^git status\b/.test(value)) return 'git-status';
1630
+ return null;
1631
+ }
1632
+
1633
+ function summarizeRawGeneration(text = '') {
1634
+ const raw = String(text || '');
1635
+ const trimmedStart = raw.trimStart();
1636
+ const firstNonWhitespace = trimmedStart[0] || '';
1637
+ const firstJsonIndex = raw.indexOf('{');
1638
+ return {
1639
+ firstNonWhitespace,
1640
+ jsonPrefixOk: firstNonWhitespace === '{',
1641
+ thoughtPrefix: /^<\|channel\>thought|^<\|start\|>thought/i.test(trimmedStart),
1642
+ containsJsonObjectStart: firstJsonIndex >= 0,
1643
+ nonWhitespaceBeforeJson: firstJsonIndex > 0 && raw.slice(0, firstJsonIndex).trim().length > 0,
1644
+ };
1645
+ }
1646
+
1647
+ function summarizeReplayEval(results = []) {
1648
+ const total = results.length;
1649
+ const count = (fn) => results.filter(fn).length;
1650
+ const decisive = results.filter((row) => isDecisiveExpectedTool(row.expected?.tool));
1651
+ const rate = (n) => total ? Number((n / total).toFixed(4)) : 0;
1652
+ const jsonPrefixCount = count((row) => row.rawDiagnostics?.jsonPrefixOk);
1653
+ const thoughtPrefixCount = count((row) => row.rawDiagnostics?.thoughtPrefix);
1654
+ const nonWhitespaceBeforeJsonCount = count((row) => row.rawDiagnostics?.nonWhitespaceBeforeJson);
1655
+ return {
1656
+ total,
1657
+ pass: count((row) => row.score.pass),
1658
+ passRate: rate(count((row) => row.score.pass)),
1659
+ toolNameMatchRate: rate(count((row) => row.score.toolNameMatch)),
1660
+ requiredArgsPresentRate: rate(count((row) => row.score.requiredArgsPresent)),
1661
+ targetMatchRate: rate(count((row) => row.score.targetMatch)),
1662
+ exactArgsMatchRate: rate(count((row) => row.score.exactArgsMatch)),
1663
+ errorCount: count((row) => row.error),
1664
+ invalidOrProseOnlyCount: count((row) => !row.prediction),
1665
+ invalidOrProseOnlyRate: rate(count((row) => !row.prediction)),
1666
+ thoughtChannelCount: count((row) => /<\|channel\>thought|<\|start\|>thought/i.test(row.rawText || '')),
1667
+ jsonPrefixCount,
1668
+ jsonPrefixRate: rate(jsonPrefixCount),
1669
+ thoughtPrefixCount,
1670
+ thoughtPrefixRate: rate(thoughtPrefixCount),
1671
+ nonWhitespaceBeforeJsonCount,
1672
+ nonWhitespaceBeforeJsonRate: rate(nonWhitespaceBeforeJsonCount),
1673
+ decisiveAction: summarizeReplaySubset(decisive),
1674
+ byTool: summarizeReplayBy(results, (row) => row.expected.tool),
1675
+ byActionClass: summarizeReplayBy(results, (row) => expectedActionClass(row.expected)),
1676
+ byLane: summarizeReplayBy(results, (row) => row.lane || 'unknown'),
1677
+ };
1678
+ }
1679
+
1680
+ function summarizeReplaySubset(rows = []) {
1681
+ const total = rows.length;
1682
+ const pass = rows.filter((row) => row.score.pass).length;
1683
+ return {
1684
+ total,
1685
+ pass,
1686
+ passRate: total ? Number((pass / total).toFixed(4)) : 0,
1687
+ toolNameMatchRate: total ? Number((rows.filter((row) => row.score.toolNameMatch).length / total).toFixed(4)) : 0,
1688
+ invalidOrProseOnlyRate: total ? Number((rows.filter((row) => !row.prediction).length / total).toFixed(4)) : 0,
1689
+ };
1690
+ }
1691
+
1692
+ function isDecisiveExpectedTool(toolName) {
1693
+ return /^(edit_file|write_file|apply_patch|run_shell)$/.test(toolName || '');
1694
+ }
1695
+
1696
+ function expectedActionClass(expected = {}) {
1697
+ if (expected.tool === 'apply_patch') return 'patch';
1698
+ if (expected.tool === 'edit_file' || expected.tool === 'write_file') return 'edit';
1699
+ if (expected.tool === 'run_shell') return 'verification';
1700
+ if (/^(read_file|list_directory|glob|grep_files|lsp_)/.test(expected.tool || '')) return 'inspect';
1701
+ return expected.tool || 'unknown';
1702
+ }
1703
+
1704
+ function summarizeReplayBy(results, keyFn) {
1705
+ const grouped = new Map();
1706
+ for (const row of results) {
1707
+ const key = keyFn(row) || 'unknown';
1708
+ if (!grouped.has(key)) grouped.set(key, []);
1709
+ grouped.get(key).push(row);
1710
+ }
1711
+ const out = {};
1712
+ for (const [key, rows] of grouped.entries()) {
1713
+ out[key] = {
1714
+ total: rows.length,
1715
+ pass: rows.filter((row) => row.score.pass).length,
1716
+ toolNameMatch: rows.filter((row) => row.score.toolNameMatch).length,
1717
+ };
1718
+ }
1719
+ return out;
1720
+ }
1721
+
1722
+ function validateDataset(args) {
1723
+ const outputDir = path.resolve(args['output-dir']);
1724
+ const datasetDir = path.join(outputDir, 'dataset');
1725
+ const manifestPath = path.join(datasetDir, 'manifest.json');
1726
+ if (!fs.existsSync(manifestPath)) throw new Error(`Missing manifest: ${manifestPath}`);
1727
+ validateDatasetFiles(datasetDir);
1728
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
1729
+ return { outputDir, datasetDir, manifest };
1730
+ }
1731
+
1732
+ function trainingPaths(args) {
1733
+ const outputDir = path.resolve(args['output-dir']);
1734
+ const datasetDir = path.join(outputDir, 'dataset');
1735
+ const adapterPath = path.resolve(args['adapter-path'] || path.join(outputDir, 'adapters', args.alias || 'gemma-e4b-tooluse-v1'));
1736
+ return { outputDir, datasetDir, adapterPath };
1737
+ }
1738
+
1739
+ function buildTrainingOpts(args) {
1740
+ const { outputDir, datasetDir, adapterPath } = trainingPaths(args);
1741
+ if (!args['base-model']) throw new Error('--base-model is required');
1742
+ const hyperparams = {};
1743
+ if (args.iters) hyperparams.iters = Number(args.iters);
1744
+ if (args.rank) hyperparams.rank = Number(args.rank);
1745
+ if (args.alpha) hyperparams.alpha = Number(args.alpha);
1746
+ if (args['batch-size']) hyperparams.batchSize = Number(args['batch-size']);
1747
+ if (args['grad-accumulation-steps']) hyperparams.gradAccumulationSteps = Number(args['grad-accumulation-steps']);
1748
+ if (args['num-layers']) hyperparams.numLayers = Number(args['num-layers']);
1749
+ if (args['max-seq-length']) hyperparams.maxSeqLength = Number(args['max-seq-length']);
1750
+ if (args['learning-rate']) hyperparams.learningRate = Number(args['learning-rate']);
1751
+ if (args['val-batches']) hyperparams.valBatches = Number(args['val-batches']);
1752
+ if (args['steps-per-report']) hyperparams.stepsPerReport = Number(args['steps-per-report']);
1753
+ if (args['steps-per-eval']) hyperparams.stepsPerEval = Number(args['steps-per-eval']);
1754
+ if (args['save-every']) hyperparams.saveEvery = Number(args['save-every']);
1755
+ if (args.optimizer) hyperparams.optimizer = args.optimizer;
1756
+ if (args['grad-checkpoint']) hyperparams.gradCheckpoint = parseBool(args['grad-checkpoint'], true);
1757
+ return {
1758
+ outputDir,
1759
+ dataDir: datasetDir,
1760
+ baseModel: path.resolve(args['base-model']),
1761
+ adapterPath,
1762
+ resumeAdapterFile: args['resume-adapter-file'] ? path.resolve(args['resume-adapter-file']) : null,
1763
+ hyperparams,
1764
+ };
1765
+ }
1766
+
1767
+ async function dryRunTrain(args) {
1768
+ const opts = buildTrainingOpts(args);
1769
+ validateDatasetFiles(opts.dataDir);
1770
+ const { config, outputPath } = writeMlxLoraConfig({ ...opts, outputPath: path.join(opts.outputDir, 'configs', 'mlx-lora-config.json') });
1771
+ const command = buildMlxLoraCommand({ config });
1772
+ return { outputDir: opts.outputDir, configPath: outputPath, command };
1773
+ }
1774
+
1775
+ async function train(args) {
1776
+ const opts = buildTrainingOpts(args);
1777
+ validateDatasetFiles(opts.dataDir);
1778
+ const { config, outputPath } = writeMlxLoraConfig({ ...opts, outputPath: path.join(opts.outputDir, 'configs', 'mlx-lora-config.json') });
1779
+ const result = await runMlxLoraTraining({ config });
1780
+ const manifest = buildTrainingManifest({ config });
1781
+ fs.writeFileSync(path.join(opts.outputDir, 'training-manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
1782
+ writeModelCard({ outputDir: opts.outputDir, manifest });
1783
+ return { outputDir: opts.outputDir, configPath: outputPath, result, manifest };
1784
+ }
1785
+
1786
+ function registerCandidate(args) {
1787
+ const outputDir = path.resolve(args['output-dir']);
1788
+ const manifestPath = path.join(outputDir, 'training-manifest.json');
1789
+ const manifest = fs.existsSync(manifestPath)
1790
+ ? JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
1791
+ : buildTrainingManifest(buildTrainingOpts(args));
1792
+ return writeCandidateMetadata({
1793
+ outputDir,
1794
+ alias: args.alias || 'gemma-e4b-tooluse-v1',
1795
+ provider: args.provider || 'mlx',
1796
+ manifest,
1797
+ });
1798
+ }
1799
+
1800
+ async function main(argv = process.argv.slice(2)) {
1801
+ const args = parseArgs(argv);
1802
+ if (args.command === 'help' || args.help) {
1803
+ printHelp();
1804
+ return null;
1805
+ }
1806
+ let result;
1807
+ if (args.command === 'export-dataset') result = await exportDataset(args);
1808
+ else if (args.command === 'export-replay-slice-dataset') result = exportReplaySliceDataset(args);
1809
+ else if (args.command === 'audit-real-trajectories') result = auditRealTrajectories(args);
1810
+ else if (args.command === 'audit-decision-points') result = auditDecisionPointsCommand(args);
1811
+ else if (args.command === 'audit-workflows') result = auditWorkflowsCommand(args);
1812
+ else if (args.command === 'eval-replay-dataset') result = await evalReplayDataset(args);
1813
+ else if (args.command === 'eval-replay-results') result = evalReplayResults(args);
1814
+ else if (args.command === 'validate-dataset') result = validateDataset(args);
1815
+ else if (args.command === 'dry-run-train') result = await dryRunTrain(args);
1816
+ else if (args.command === 'train') result = await train(args);
1817
+ else if (args.command === 'register-candidate') result = registerCandidate(args);
1818
+ else throw new Error(`Unknown command: ${args.command}`);
1819
+ console.log(JSON.stringify(result, null, 2));
1820
+ return result;
1821
+ }
1822
+
1823
+ function printHelp() {
1824
+ console.log(`Usage:
1825
+ node wall-e/bin/train-gemma-e4b-tooluse.js export-dataset --source synthetic --output-dir /tmp/run
1826
+ node wall-e/bin/train-gemma-e4b-tooluse.js export-dataset --source sessions --root /path/to/transcripts --repo /path/to/repo --since-days 14 --scan-limit 500 --output-dir /tmp/run
1827
+ node wall-e/bin/train-gemma-e4b-tooluse.js audit-real-trajectories --repo /path/to/repo --since-days 120 --scan-limit 500 --output-dir /tmp/audit
1828
+ node wall-e/bin/train-gemma-e4b-tooluse.js audit-decision-points --repo /path/to/repo --since-days 120 --scan-limit 500 --output-dir /tmp/decision-audit
1829
+ node wall-e/bin/train-gemma-e4b-tooluse.js audit-workflows --repo /path/to/repo --since-days 180 --scan-limit 1000 --output-dir /tmp/workflows
1830
+ node wall-e/bin/train-gemma-e4b-tooluse.js export-dataset --source real-sessions --repo /path/to/repo --audit-dir /tmp/audit --output-dir /tmp/run
1831
+ node wall-e/bin/train-gemma-e4b-tooluse.js export-dataset --source personal-replay --since-days 180 --scan-limit 1000 --output-dir /tmp/run
1832
+ node wall-e/bin/train-gemma-e4b-tooluse.js export-dataset --source decisive-actions --since-days 180 --scan-limit 1000 --format mlx-provider --output-dir /tmp/run
1833
+ node wall-e/bin/train-gemma-e4b-tooluse.js export-replay-slice-dataset --eval-file /tmp/run/replay-eval/seen-exact.jsonl --results-file /tmp/baseline/replay-eval-results.jsonl --slice-mode parser-or-wrong-tool --repeats 8 --output-dir /tmp/replay-slice
1834
+ node wall-e/bin/train-gemma-e4b-tooluse.js eval-replay-dataset --dataset-output-dir /tmp/run --lane seen-exact --model gemma4:e4b-it-qat
1835
+ node wall-e/bin/train-gemma-e4b-tooluse.js validate-dataset --output-dir /tmp/run
1836
+ node wall-e/bin/train-gemma-e4b-tooluse.js dry-run-train --output-dir /tmp/run --base-model /path/to/base
1837
+ node wall-e/bin/train-gemma-e4b-tooluse.js train --output-dir /tmp/run --base-model /path/to/base --iters 600 --max-seq-length 8192
1838
+ node wall-e/bin/train-gemma-e4b-tooluse.js register-candidate --output-dir /tmp/run --alias gemma-e4b-tooluse-v1
1839
+
1840
+ Dataset export options:
1841
+ --source synthetic|sessions|real-sessions|personal-replay|decisive-actions|all
1842
+ --trace-files file.jsonl[,file.json]
1843
+ --root path or --roots path1,path2
1844
+ --repo path
1845
+ --transcript-source all|claude|codex|walle
1846
+ --since-days n
1847
+ --scan-limit n
1848
+ --max-examples n
1849
+ --real-trajectories true|false
1850
+ --include-ctm true|false
1851
+ --include-filesystem true|false
1852
+ --per-source-limit claude=200,codex=200,walle=200
1853
+ --per-tool-limit read_file=400,grep_files=200,edit_file=500,apply_patch=160,run_shell=220
1854
+ --recover-git true|false
1855
+ --git-recovery-window-minutes n
1856
+ --allow-weak-git-recovery true|false
1857
+ --min-gold-trajectories n
1858
+ --min-silver-trajectories n
1859
+ --min-gold-or-silver-trajectories n
1860
+ --min-passing-verification n
1861
+ --min-real-examples n
1862
+ --fail-on-quality-gate true|false
1863
+ --ctm-db path
1864
+ --audit-dir path
1865
+ --decision-audit-dir path
1866
+ --workflow-audit-dir path
1867
+ --include-paths true|false
1868
+ --include-messages true|false
1869
+ --min-decision-points n
1870
+ --min-guard-recovery n
1871
+ --max-same-inspection-after-guard n
1872
+ --include-train-variants true|false
1873
+ --personal-variant-repeats n
1874
+ --decisive-actions true|false
1875
+ --decisive-exact-repeats n
1876
+ --require-decisive-tool-result-history true|false
1877
+ --max-seen-exact-eval n
1878
+ --max-seen-variant-eval n
1879
+ --min-seen-workflow-families n
1880
+ --min-repeated-workflow-families n
1881
+ --min-seen-replay-rows n
1882
+ --min-seen-variant-rows n
1883
+ --min-edit-verification-rows n
1884
+ --min-first-action-rows n
1885
+ --min-decisive-action-rows n
1886
+ --min-decisive-exact-rows n
1887
+ --min-decisive-edit-rows n
1888
+ --min-decisive-verification-rows n
1889
+ --min-decisive-real-result-rows n
1890
+ --min-json-only-contract-rows n
1891
+ --min-state-summary-rows n
1892
+ --include-tiers gold,silver
1893
+ --include-tools tool1,tool2
1894
+ --max-result-chars n
1895
+ --max-file-mb n
1896
+ --max-rendered-chars n
1897
+ --min-rendered-length-kept-rows n
1898
+ --quality-threshold n
1899
+ --targeted-repeats n
1900
+ --format mlx-tools|mlx-provider|mlx-provider-bare|mlx-provider-state-bare|trl-tools|text
1901
+ --state-summary true|false
1902
+ --target-mode full|tool-choice
1903
+ --results-file /tmp/baseline/replay-eval-results.jsonl
1904
+ --slice-mode all|failures|parser-failures|wrong-tool|parser-or-wrong-tool
1905
+ --require-edits true|false
1906
+ --require-coding-intent true|false
1907
+
1908
+ Training options:
1909
+ --iters n
1910
+ --rank n
1911
+ --alpha n
1912
+ --batch-size n
1913
+ --grad-accumulation-steps n
1914
+ --num-layers n
1915
+ --max-seq-length n
1916
+ --learning-rate n
1917
+ --val-batches n
1918
+ --steps-per-report n
1919
+ --steps-per-eval n
1920
+ --save-every n
1921
+ --optimizer adam|adamw|muon|sgd|adafactor
1922
+ --grad-checkpoint true|false
1923
+ --resume-adapter-file path
1924
+
1925
+ Replay eval options:
1926
+ --dataset-output-dir path
1927
+ --eval-file path
1928
+ --lane seen-exact|seen-variant
1929
+ --model name
1930
+ --adapter-path path
1931
+ --tool-call-format fenced|bare-json
1932
+ --force-tool-json-prefix true|false
1933
+ --run-shell-command-selector off|heuristic|store
1934
+ --run-shell-max-candidates n
1935
+ --action-memory-selector off|heuristic|store
1936
+ --action-memory-db path
1937
+ --action-memory-build-from-files true|false
1938
+ --action-memory-allow-eval-rows true|false
1939
+ --action-memory-exclude-eval-families true|false
1940
+ --limit n
1941
+ --max-tokens n
1942
+ --inference-timeout-ms n
1943
+ `);
1944
+ }
1945
+
1946
+ if (require.main === module) {
1947
+ main().catch((err) => {
1948
+ console.error(err.stack || err.message);
1949
+ process.exit(1);
1950
+ });
1951
+ }
1952
+
1953
+ module.exports = {
1954
+ parseArgs,
1955
+ exportDataset,
1956
+ exportReplaySliceDataset,
1957
+ validateDataset,
1958
+ dryRunTrain,
1959
+ train,
1960
+ registerCandidate,
1961
+ auditRealTrajectories,
1962
+ auditDecisionPointsCommand,
1963
+ auditWorkflowsCommand,
1964
+ evalReplayDataset,
1965
+ buildRealDatasetQualityGate,
1966
+ buildDecisionPointQualityGate,
1967
+ buildDecisiveActionDataGate,
1968
+ buildRenderedLengthGate,
1969
+ buildPersonalReplayDataGate,
1970
+ applyPerToolLimit,
1971
+ applyMaxRenderedChars,
1972
+ actionMemoryContextFromReplayExample,
1973
+ actionMemoryStorePaths,
1974
+ recordTrajectoryRowsToActionMemoryStore,
1975
+ scoreReplayPrediction,
1976
+ parseBool,
1977
+ parsePerSourceLimit,
1978
+ parsePerToolLimit,
1979
+ stableStringify,
1980
+ main,
1981
+ };