ruvnet-brain 4.0.4 → 4.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/lesson-command-scope.mjs +133 -0
- package/plugin/scripts/lesson-gate.mjs +401 -0
- package/plugin/scripts/lesson-presentation.mjs +99 -0
- package/plugin/scripts/lesson-store.mjs +452 -0
- package/plugin/scripts/route-dispatch.sh +1 -0
- package/plugin/scripts/verify-interface.sh +1 -0
- package/scripts/learning-replay-cli.mjs +236 -0
- package/scripts/learning-replay-contract.mjs +255 -0
- package/scripts/learning-replay-execution.mjs +193 -0
- package/scripts/learning-replay-fixture.mjs +380 -0
- package/scripts/learning-replay-proof.mjs +459 -0
- package/scripts/learning-replay.mjs +10 -1565
- package/scripts/lesson-gate.mjs +3 -679
- package/scripts/lesson-store.mjs +4 -447
- package/scripts/memory-doctor.mjs +19 -3
- package/scripts/onboarding-console.mjs +77 -43
- package/scripts/release-vector.mjs +49 -25
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { makeLesson, saveLessons, loadLessons } from './lesson-store.mjs';
|
|
6
|
+
import {
|
|
7
|
+
ROOT,
|
|
8
|
+
REPLAY_PROMPT,
|
|
9
|
+
TRAP,
|
|
10
|
+
classifyCommand,
|
|
11
|
+
classifyPostTaskCommand,
|
|
12
|
+
postTaskSubcommandCorrect,
|
|
13
|
+
subcommandCorrect,
|
|
14
|
+
trapSpec,
|
|
15
|
+
} from './learning-replay-contract.mjs';
|
|
16
|
+
import {
|
|
17
|
+
executeProducedCommand,
|
|
18
|
+
PROJECT_B_MEMORY_KEY,
|
|
19
|
+
PROJECT_B_MEMORY_VALUE,
|
|
20
|
+
RUFLO_BIN,
|
|
21
|
+
} from './learning-replay-execution.mjs';
|
|
22
|
+
|
|
23
|
+
const CLAUDE_BIN = process.env.RUVNET_CLAUDE_BIN
|
|
24
|
+
|| path.join(os.homedir(), '.npm-global', 'bin', 'claude');
|
|
25
|
+
const CODEX_BIN = process.env.RUVNET_CODEX_BIN || 'codex';
|
|
26
|
+
const sh = (cmd, args, options = {}) => spawnSync(cmd, args, {
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
timeout: 120_000,
|
|
29
|
+
...options,
|
|
30
|
+
});
|
|
31
|
+
const remove = (target) => {
|
|
32
|
+
try { fs.rmSync(target, { recursive: true, force: true }); } catch { /* already gone */ }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function allocateRunBase(root = path.join(ROOT, '.ruvnet-brain', 'learning-replay')) {
|
|
36
|
+
fs.mkdirSync(root, { recursive: true });
|
|
37
|
+
const base = fs.mkdtempSync(path.join(root, 'run-'));
|
|
38
|
+
const createdAt = new Date().toISOString();
|
|
39
|
+
fs.writeFileSync(path.join(base, 'run-manifest.json'), `${JSON.stringify({
|
|
40
|
+
schema: 1,
|
|
41
|
+
createdAt,
|
|
42
|
+
sequence: Date.now(),
|
|
43
|
+
}, null, 2)}\n`);
|
|
44
|
+
return base;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function initMemoryDb(ruflo, db, cwd) {
|
|
48
|
+
return sh(ruflo, ['memory', 'init', '--path', db, '--backend', 'hybrid'], { cwd });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildFixtures(baseDir) {
|
|
52
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
53
|
+
const dirs = {
|
|
54
|
+
base: baseDir,
|
|
55
|
+
projectA: path.join(baseDir, 'fixture-project-a'),
|
|
56
|
+
projectA2: path.join(baseDir, 'fixture-project-a-independent'),
|
|
57
|
+
projectB: path.join(baseDir, 'fixture-project-b'),
|
|
58
|
+
brainHome: path.join(baseDir, 'brain-home'),
|
|
59
|
+
stateOn: path.join(baseDir, 'state-on'),
|
|
60
|
+
stateOff: path.join(baseDir, 'state-off'),
|
|
61
|
+
transcripts: path.join(baseDir, 'transcripts'),
|
|
62
|
+
};
|
|
63
|
+
for (const directory of Object.values(dirs)) fs.mkdirSync(directory, { recursive: true });
|
|
64
|
+
dirs.lessons = path.join(baseDir, 'lessons.json');
|
|
65
|
+
dirs.gateState = path.join(baseDir, 'lesson-gate-state.json');
|
|
66
|
+
fs.writeFileSync(path.join(dirs.stateOff, 'brain-off'), JSON.stringify({
|
|
67
|
+
since: new Date().toISOString(),
|
|
68
|
+
}));
|
|
69
|
+
for (const project of [dirs.projectA, dirs.projectA2, dirs.projectB]) {
|
|
70
|
+
sh('git', ['init', '-q'], { cwd: project });
|
|
71
|
+
fs.mkdirSync(path.join(project, '.swarm'), { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
return dirs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function recordInProjectA(dirs, {
|
|
77
|
+
ruflo = RUFLO_BIN,
|
|
78
|
+
trap = TRAP.MEMORY_SEARCH,
|
|
79
|
+
} = {}) {
|
|
80
|
+
const spec = trapSpec(trap);
|
|
81
|
+
const sources = [dirs.projectA, dirs.projectA2].map((project, index) => {
|
|
82
|
+
const db = path.join(project, '.swarm', 'memory.db');
|
|
83
|
+
const key = `${spec.memoryKey}-${index + 1}`;
|
|
84
|
+
const init = initMemoryDb(ruflo, db, project);
|
|
85
|
+
const store = sh(ruflo, [
|
|
86
|
+
'memory', 'store', '-k', key, '--value', spec.lesson,
|
|
87
|
+
'-n', 'default', '--path', db,
|
|
88
|
+
], { cwd: project });
|
|
89
|
+
const readBack = sh(ruflo, [
|
|
90
|
+
'memory', 'search', '-q', spec.recordQuery,
|
|
91
|
+
'-n', 'default', '--path', db, '-t', 'keyword',
|
|
92
|
+
], { cwd: project });
|
|
93
|
+
return {
|
|
94
|
+
project: path.basename(project),
|
|
95
|
+
db,
|
|
96
|
+
key,
|
|
97
|
+
initExit: init.status,
|
|
98
|
+
storeExit: store.status,
|
|
99
|
+
readBackExit: readBack.status,
|
|
100
|
+
recorded: fs.existsSync(db),
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
const lesson = makeLesson({
|
|
104
|
+
id: spec.lessonId,
|
|
105
|
+
statement: spec.lesson,
|
|
106
|
+
trigger: 'assert-fact',
|
|
107
|
+
enforcement: 'checklist',
|
|
108
|
+
origin: 'user-stated',
|
|
109
|
+
status: 'ratified',
|
|
110
|
+
severity: 'high',
|
|
111
|
+
repeatCount: 4,
|
|
112
|
+
projects: sources.map((source) => source.project),
|
|
113
|
+
check: spec.check,
|
|
114
|
+
evidence: sources.map((source) => ({
|
|
115
|
+
observed: `independently recorded in ${source.project} as ${source.key}`,
|
|
116
|
+
})),
|
|
117
|
+
});
|
|
118
|
+
saveLessons([lesson], dirs.lessons);
|
|
119
|
+
const sourcesOk = sources.every((source) => source.initExit === 0
|
|
120
|
+
&& source.storeExit === 0 && source.readBackExit === 0 && source.recorded);
|
|
121
|
+
return {
|
|
122
|
+
ok: sourcesOk && loadLessons(dirs.lessons).length === 1 && lesson.projects.length >= 2,
|
|
123
|
+
trap: spec.id,
|
|
124
|
+
sources,
|
|
125
|
+
projectCount: lesson.projects.length,
|
|
126
|
+
promoted: lesson.projects.length >= 2,
|
|
127
|
+
key: sources[0].key,
|
|
128
|
+
storeExit: sources[0].storeExit,
|
|
129
|
+
readBackExit: sources[0].readBackExit,
|
|
130
|
+
lesson,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function seedProjectBMemory(dirs, { ruflo = RUFLO_BIN } = {}) {
|
|
135
|
+
const db = path.join(dirs.projectB, '.swarm', 'memory.db');
|
|
136
|
+
const init = initMemoryDb(ruflo, db, dirs.projectB);
|
|
137
|
+
const store = sh(ruflo, [
|
|
138
|
+
'memory', 'store', '-k', PROJECT_B_MEMORY_KEY,
|
|
139
|
+
'--value', PROJECT_B_MEMORY_VALUE, '-n', 'default', '--path', db,
|
|
140
|
+
], { cwd: dirs.projectB });
|
|
141
|
+
return {
|
|
142
|
+
db,
|
|
143
|
+
key: PROJECT_B_MEMORY_KEY,
|
|
144
|
+
initExit: init.status,
|
|
145
|
+
storeExit: store.status,
|
|
146
|
+
ok: init.status === 0 && store.status === 0 && fs.existsSync(db),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function nightlyRefresh(dirs, { ruflo = RUFLO_BIN } = {}) {
|
|
151
|
+
const generation = `d4-refresh-${Date.now()}`;
|
|
152
|
+
const versionDir = path.join(dirs.brainHome, 'versions', generation);
|
|
153
|
+
fs.mkdirSync(versionDir, { recursive: true });
|
|
154
|
+
fs.cpSync(path.join(ROOT, 'plugin'), versionDir, { recursive: true });
|
|
155
|
+
fs.copyFileSync(
|
|
156
|
+
path.join(ROOT, 'scripts', 'ci', 'learning-replay-codex-adapter.mjs'),
|
|
157
|
+
path.join(versionDir, 'scripts', 'codex-hook-adapter.mjs'),
|
|
158
|
+
);
|
|
159
|
+
fs.writeFileSync(path.join(dirs.brainHome, 'active.json'), JSON.stringify({
|
|
160
|
+
codeRoot: versionDir,
|
|
161
|
+
generation,
|
|
162
|
+
}, null, 2));
|
|
163
|
+
fs.writeFileSync(path.join(dirs.brainHome, '.spine-seeded'), generation);
|
|
164
|
+
const db = path.join(dirs.projectA, '.swarm', 'memory.db');
|
|
165
|
+
const distill = sh(ruflo, ['memory', 'distill', 'run', '--path', db], { cwd: dirs.projectA });
|
|
166
|
+
const backup = sh(ruflo, ['memory', 'backup', '--db', db, '--keep', '2'], { cwd: dirs.projectA });
|
|
167
|
+
return {
|
|
168
|
+
generation,
|
|
169
|
+
codeRoot: path.relative(ROOT, versionDir),
|
|
170
|
+
distillExit: distill.status,
|
|
171
|
+
backupExit: backup.status,
|
|
172
|
+
lessonSurvived: loadLessons(dirs.lessons).length === 1,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function writeSettings(file, { dirs, stateDir, attemptsFile }) {
|
|
177
|
+
const settings = {
|
|
178
|
+
env: {
|
|
179
|
+
RUVNET_BRAIN_HOME: dirs.brainHome,
|
|
180
|
+
RUVNET_BRAIN_STATE_DIR: stateDir,
|
|
181
|
+
RUVNET_CONFIG_ROOT: path.join(dirs.base, 'config'),
|
|
182
|
+
RUVNET_LESSON_STORE: dirs.lessons,
|
|
183
|
+
RUVNET_LESSON_GATE_STATE: dirs.gateState,
|
|
184
|
+
CLAUDE_PLUGIN_ROOT: path.join(ROOT, 'plugin'),
|
|
185
|
+
},
|
|
186
|
+
hooks: {
|
|
187
|
+
UserPromptSubmit: [{
|
|
188
|
+
matcher: '*',
|
|
189
|
+
hooks: [{
|
|
190
|
+
type: 'command',
|
|
191
|
+
command: `node ${JSON.stringify(path.join(ROOT, 'plugin', 'scripts', 'hook-shim.mjs'))} unprompted-speech UserPromptSubmit`,
|
|
192
|
+
timeout: 20,
|
|
193
|
+
}],
|
|
194
|
+
}],
|
|
195
|
+
PreToolUse: [{
|
|
196
|
+
matcher: 'Bash',
|
|
197
|
+
hooks: [{
|
|
198
|
+
type: 'command',
|
|
199
|
+
command: `node ${JSON.stringify(path.join(ROOT, 'scripts', 'ci', 'learning-replay-recorder.mjs'))} ${JSON.stringify(attemptsFile)}`,
|
|
200
|
+
timeout: 20,
|
|
201
|
+
}],
|
|
202
|
+
}],
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
fs.writeFileSync(file, JSON.stringify(settings, null, 2));
|
|
206
|
+
return file;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function buildCodexArgv({
|
|
210
|
+
model = 'gpt-5.6-sol',
|
|
211
|
+
prompt = REPLAY_PROMPT,
|
|
212
|
+
appendSystemPrompt = null,
|
|
213
|
+
} = {}) {
|
|
214
|
+
const fullPrompt = appendSystemPrompt ? `${appendSystemPrompt}\n\n${prompt}` : prompt;
|
|
215
|
+
return [
|
|
216
|
+
'exec', '--ephemeral', '--sandbox', 'read-only', '--color', 'never', '--json',
|
|
217
|
+
'--ignore-rules', '--dangerously-bypass-hook-trust', '-m', model,
|
|
218
|
+
'-c', 'model_reasoning_effort="low"',
|
|
219
|
+
'-c', 'shell_environment_policy.inherit="all"',
|
|
220
|
+
fullPrompt,
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function replayRunError(events, result) {
|
|
225
|
+
if (result?.error) return String(result.error.message || result.error);
|
|
226
|
+
const event = events.find((value) => value.type === 'result');
|
|
227
|
+
if (!event?.is_error) return null;
|
|
228
|
+
const status = event.api_error_status ? `HTTP ${event.api_error_status}: ` : '';
|
|
229
|
+
return `${status}${event.result || event.terminal_reason || 'model execution failed'}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function parseCodexRunError(events, result) {
|
|
233
|
+
if (result?.error) return String(result.error.message || result.error);
|
|
234
|
+
const failed = events.find((event) => event.type === 'turn.failed');
|
|
235
|
+
if (failed) return String(failed.error?.message || failed.error || 'Codex turn failed');
|
|
236
|
+
if (events.some((event) => event.type === 'turn.completed') && result?.status === 0) return null;
|
|
237
|
+
const error = events.find((event) => event.type === 'item.completed' && event.item?.type === 'error');
|
|
238
|
+
if (error) return String(error.item?.message || error.item?.text || 'Codex execution failed');
|
|
239
|
+
return result?.status && result.status !== 0 ? `Codex exited ${result.status}` : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function codexLessonBeforeTool(sequence) {
|
|
243
|
+
const lesson = sequence.find((event) => event.kind === 'lesson');
|
|
244
|
+
const tool = sequence.find((event) => event.kind === 'tool');
|
|
245
|
+
return Boolean(lesson && tool && BigInt(lesson.atNs) < BigInt(tool.atNs));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function readJsonLines(file) {
|
|
249
|
+
return fs.existsSync(file)
|
|
250
|
+
? fs.readFileSync(file, 'utf8').split('\n').filter(Boolean)
|
|
251
|
+
.map((line) => { try { return JSON.parse(line); } catch { return null; } })
|
|
252
|
+
.filter(Boolean)
|
|
253
|
+
: [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function runArm({
|
|
257
|
+
dirs,
|
|
258
|
+
arm,
|
|
259
|
+
stateDir,
|
|
260
|
+
model,
|
|
261
|
+
host = 'claude-code',
|
|
262
|
+
appendSystemPrompt = null,
|
|
263
|
+
tag,
|
|
264
|
+
forceCommand = null,
|
|
265
|
+
trap = TRAP.MEMORY_SEARCH,
|
|
266
|
+
}) {
|
|
267
|
+
const spec = trapSpec(trap);
|
|
268
|
+
const attempts = path.join(dirs.transcripts, `${tag}.attempts.jsonl`);
|
|
269
|
+
const sequenceFile = path.join(dirs.transcripts, `${tag}.sequence.jsonl`);
|
|
270
|
+
const streamFile = path.join(dirs.transcripts, `${tag}.stream.jsonl`);
|
|
271
|
+
let binary;
|
|
272
|
+
let argv;
|
|
273
|
+
if (host === 'codex') {
|
|
274
|
+
binary = CODEX_BIN;
|
|
275
|
+
argv = buildCodexArgv({ model, prompt: spec.prompt, appendSystemPrompt });
|
|
276
|
+
} else {
|
|
277
|
+
const settings = writeSettings(path.join(dirs.base, `settings-${tag}.json`), {
|
|
278
|
+
dirs,
|
|
279
|
+
stateDir,
|
|
280
|
+
attemptsFile: attempts,
|
|
281
|
+
});
|
|
282
|
+
binary = CLAUDE_BIN;
|
|
283
|
+
argv = [
|
|
284
|
+
'-p', spec.prompt, '--model', model, '--tools', 'Bash',
|
|
285
|
+
'--permission-mode', 'bypassPermissions', '--setting-sources', '',
|
|
286
|
+
'--settings', settings, '--output-format', 'stream-json', '--verbose',
|
|
287
|
+
'--include-hook-events', '--no-session-persistence', '--max-budget-usd', '0.30',
|
|
288
|
+
];
|
|
289
|
+
if (appendSystemPrompt) argv.push('--append-system-prompt', appendSystemPrompt);
|
|
290
|
+
}
|
|
291
|
+
const started = Date.now();
|
|
292
|
+
const result = spawnSync(binary, argv, {
|
|
293
|
+
cwd: dirs.projectB,
|
|
294
|
+
encoding: 'utf8',
|
|
295
|
+
timeout: 300_000,
|
|
296
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
297
|
+
env: {
|
|
298
|
+
...process.env,
|
|
299
|
+
RUVNET_BRAIN_HOME: dirs.brainHome,
|
|
300
|
+
RUVNET_BRAIN_STATE_DIR: stateDir,
|
|
301
|
+
RUVNET_CONFIG_ROOT: path.join(dirs.base, 'config'),
|
|
302
|
+
RUVNET_LESSON_STORE: dirs.lessons,
|
|
303
|
+
RUVNET_LESSON_GATE_STATE: dirs.gateState,
|
|
304
|
+
RUVNET_REPLAY_ATTEMPTS_FILE: attempts,
|
|
305
|
+
RUVNET_REPLAY_SEQUENCE_FILE: sequenceFile,
|
|
306
|
+
RUVNET_REPLAY_LESSON_PROBE: spec.lesson.slice(0, 60),
|
|
307
|
+
RUVNET_REPLAY_RECORDER: path.join(ROOT, 'scripts', 'ci', 'learning-replay-recorder.mjs'),
|
|
308
|
+
CLAUDE_PLUGIN_ROOT: path.join(ROOT, 'plugin'),
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
const wallMs = Date.now() - started;
|
|
312
|
+
fs.writeFileSync(streamFile, result.stdout || '');
|
|
313
|
+
if (result.stderr) fs.writeFileSync(path.join(dirs.transcripts, `${tag}.stderr.txt`), result.stderr);
|
|
314
|
+
const events = readJsonLines(streamFile);
|
|
315
|
+
const sequence = readJsonLines(sequenceFile);
|
|
316
|
+
const probe = spec.lesson.slice(0, 60);
|
|
317
|
+
let lessonIndex = -1;
|
|
318
|
+
let firstToolIndex = -1;
|
|
319
|
+
let lessonDelivered = false;
|
|
320
|
+
if (host === 'codex') {
|
|
321
|
+
lessonIndex = sequence.findIndex((event) => event.kind === 'lesson');
|
|
322
|
+
firstToolIndex = sequence.findIndex((event) => event.kind === 'tool');
|
|
323
|
+
lessonDelivered = lessonIndex !== -1;
|
|
324
|
+
} else {
|
|
325
|
+
events.forEach((event, index) => {
|
|
326
|
+
if (lessonIndex === -1 && event.type === 'system' && event.subtype === 'hook_response'
|
|
327
|
+
&& typeof event.output === 'string' && event.output.includes(probe)) {
|
|
328
|
+
lessonIndex = index;
|
|
329
|
+
lessonDelivered = true;
|
|
330
|
+
}
|
|
331
|
+
if (firstToolIndex === -1 && event.type === 'assistant'
|
|
332
|
+
&& Array.isArray(event.message?.content)
|
|
333
|
+
&& event.message.content.some((content) => content.type === 'tool_use')) firstToolIndex = index;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
const attemptLines = readJsonLines(attempts);
|
|
337
|
+
const command = forceCommand != null ? forceCommand : (attemptLines[0]?.command || '');
|
|
338
|
+
const classification = trap === TRAP.POST_TASK
|
|
339
|
+
? classifyPostTaskCommand(command)
|
|
340
|
+
: classifyCommand(command);
|
|
341
|
+
const subcommand = trap === TRAP.POST_TASK
|
|
342
|
+
? postTaskSubcommandCorrect(command)
|
|
343
|
+
: subcommandCorrect(command);
|
|
344
|
+
const execution = executeProducedCommand(command, {
|
|
345
|
+
cwd: dirs.projectB,
|
|
346
|
+
base: dirs.base,
|
|
347
|
+
trap,
|
|
348
|
+
});
|
|
349
|
+
const answer = events.find((event) => event.type === 'result');
|
|
350
|
+
return {
|
|
351
|
+
arm,
|
|
352
|
+
tag,
|
|
353
|
+
class: classification,
|
|
354
|
+
subcommandCorrect: subcommand,
|
|
355
|
+
exec: execution,
|
|
356
|
+
command,
|
|
357
|
+
forcedCommand: forceCommand != null,
|
|
358
|
+
attempts: attemptLines.map((attempt) => attempt.command),
|
|
359
|
+
lessonDelivered,
|
|
360
|
+
lessonIndex,
|
|
361
|
+
firstToolIndex,
|
|
362
|
+
lessonBeforeFirstToolCall: host === 'codex'
|
|
363
|
+
? codexLessonBeforeTool(sequence)
|
|
364
|
+
: lessonDelivered && firstToolIndex > -1 && lessonIndex < firstToolIndex,
|
|
365
|
+
costUsd: answer?.total_cost_usd ?? null,
|
|
366
|
+
wallMs,
|
|
367
|
+
modelUsed: events.find((event) => event.type === 'system' && event.subtype === 'init')?.model
|
|
368
|
+
|| events.find((event) => event.type === 'assistant')?.message?.model || model,
|
|
369
|
+
host,
|
|
370
|
+
transcript: path.relative(ROOT, streamFile),
|
|
371
|
+
exit: result.status,
|
|
372
|
+
spawnError: host === 'codex'
|
|
373
|
+
? parseCodexRunError(events, result)
|
|
374
|
+
: replayRunError(events, result),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function removeFixture(target) {
|
|
379
|
+
remove(target);
|
|
380
|
+
}
|