( ctx: CliFailureContext, options: WriteBundleOptions, )
| 386 | * → 10 by default; the original ApiError is preserved when applicable). |
| 387 | */ |
| 388 | export async function writeBundle( |
| 389 | ctx: CliFailureContext, |
| 390 | options: WriteBundleOptions, |
| 391 | ): Promise<WriteBundleResult> { |
| 392 | const requestId = options.requestId ?? 'local'; |
| 393 | assertContextIntegrity(ctx, requestId); |
| 394 | |
| 395 | const filtered = options.failedOnly ? applyFailedOnly(ctx) : ctx; |
| 396 | // Re-check after filtering — in theory a buggy filter could leave a |
| 397 | // mismatched evidence list. Cheap defense-in-depth. |
| 398 | assertContextIntegrity(filtered, requestId); |
| 399 | |
| 400 | const dir = resolveBundleDir(options.dir); |
| 401 | const fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis); |
| 402 | const meta = buildMeta(filtered); |
| 403 | const codeExt = pickCodeExtension(filtered.code.language, filtered.code.framework); |
| 404 | |
| 405 | await mkdir(dir, { recursive: true }); |
| 406 | // Fresh `.tmp/` each run. A previous SIGKILL'd run can leave bytes |
| 407 | // behind; we don't want to mix them with the new fetch. |
| 408 | const tmpDir = await freshTmpDir(dir); |
| 409 | const stepsTmpDir = join(tmpDir, 'steps'); |
| 410 | await mkdir(stepsTmpDir, { recursive: true }); |
| 411 | |
| 412 | const filesWritten: string[] = []; |
| 413 | |
| 414 | try { |
| 415 | // result.json + failure.json + code.<ext> are local writes; no |
| 416 | // network, no streaming. |
| 417 | await writeFile( |
| 418 | join(tmpDir, 'result.json'), |
| 419 | JSON.stringify(filtered.result, null, 2) + '\n', |
| 420 | 'utf8', |
| 421 | ); |
| 422 | filesWritten.push('result.json'); |
| 423 | |
| 424 | await writeFile( |
| 425 | join(tmpDir, 'failure.json'), |
| 426 | JSON.stringify(filtered.failure, null, 2) + '\n', |
| 427 | 'utf8', |
| 428 | ); |
| 429 | filesWritten.push('failure.json'); |
| 430 | |
| 431 | const codeFile = `code.${codeExt}`; |
| 432 | if (isPresignedUrl(filtered.code.code)) { |
| 433 | // §6.3 alt: when the `code` field is a presigned URL (>= 100 KB |
| 434 | // bodies), stream the URL into the file rather than embedding |
| 435 | // the URL in code.<ext>. M2's backend hasn't shipped the |
| 436 | // >=100KB branch yet, but the contract supports it. |
| 437 | await streamUrlToFile(filtered.code.code, join(tmpDir, codeFile), fetchImpl); |
| 438 | } else { |
| 439 | await writeFile(join(tmpDir, codeFile), filtered.code.code, 'utf8'); |
| 440 | } |
| 441 | filesWritten.push(codeFile); |
| 442 | |
| 443 | // Optional video. Wire field is `result.videoUrl` (not in `failure`), |
| 444 | // and the CLI surfaces it as a top-level on-disk artifact for agent |
| 445 | // ergonomics — the agent doesn't have to know which sub-object held |
no test coverage detected