thunderjar 0.1.3-preview-e09ffa1 → 0.1.3-preview-d23f868

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/dist/cli.js CHANGED
@@ -2047,7 +2047,7 @@ var program = new Command;
2047
2047
  // package.json
2048
2048
  var package_default = {
2049
2049
  name: "thunderjar",
2050
- version: "0.1.3-preview-e09ffa1",
2050
+ version: "0.1.3-preview-d23f868",
2051
2051
  type: "module",
2052
2052
  description: "Thunderjar",
2053
2053
  license: "MIT",
@@ -3451,9 +3451,194 @@ function createGenerateCommand() {
3451
3451
  }));
3452
3452
  }
3453
3453
 
3454
- // src/cli/commands/init/scaffold.ts
3454
+ // src/cli/commands/init/init.ts
3455
+ var {$: $5 } = globalThis.Bun;
3456
+
3457
+ // src/cli/commands/init/starterHarnesses.ts
3455
3458
  var declaration = (fn, body) => `import { ${fn} } from "thunderjar";
3456
3459
 
3460
+ export default ${fn}(${body});
3461
+ `;
3462
+ var models = (comment, entries) => `import { declareHarnessModels } from "thunderjar";
3463
+
3464
+ ${comment}
3465
+ export default declareHarnessModels({
3466
+ ${entries.map(([name, id]) => ` ${JSON.stringify(name)}: ${JSON.stringify(id)},`).join(`
3467
+ `)}
3468
+ });
3469
+ `;
3470
+ var versions = (version) => declaration("declareHarnessVersions", `{
3471
+ ${JSON.stringify(version)}: { version: ${JSON.stringify(version)} },
3472
+ }`);
3473
+ var claudeCodeTokenCosts = `import { claudeCodeTokenCosts, declareTokenCostCollector } from "thunderjar";
3474
+
3475
+ export default declareTokenCostCollector((raw) => claudeCodeTokenCosts(raw));
3476
+ `;
3477
+ var stubTokenCosts = (agent) => `import { declareTokenCostCollector } from "thunderjar";
3478
+
3479
+ // Thunderjar has no reader for ${agent}'s output yet. \`raw\` is what the harness's \`cli\` wrote to
3480
+ // its result path. To collect token costs, read them from it:
3481
+ //
3482
+ // return {
3483
+ // outcome: "collected",
3484
+ // models: [{ model: ctx.modelId, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens }],
3485
+ // };
3486
+ export default declareTokenCostCollector((_raw, _ctx) => ({ outcome: "unavailable", reason: "notReported" }));
3487
+ `;
3488
+ var installAsRoot = (install) => `\`RUN ${install}
3489
+ RUN mkdir -p /thunderjar && chown -R node:node /workspace /thunderjar
3490
+ USER node\``;
3491
+ var stubReply = ` // Thunderjar has no reader for this agent's reply yet. To measure it, return the agent's final
3492
+ // reply from what \`cli\` wrote to its result path:
3493
+ // collectReply: (raw) => \u2026,`;
3494
+ var claudeCode = `import { claudeCodeReply, declareHarness } from "thunderjar";
3495
+
3496
+ // Claude Code, installed as root, then run as the image's non-root \`node\` user: it won't skip its
3497
+ // permission prompts as root, and nobody is there to answer them.
3498
+ export default declareHarness({
3499
+ applyParameter: (_config, ctx) => \`RUN npm install -g @anthropic-ai/claude-code@\${ctx.version}
3500
+ RUN mkdir -p /thunderjar && chown -R node:node /workspace /thunderjar
3501
+ USER node\`,
3502
+ // The prompt is read from its file, so quotes and newlines reach the agent intact. Turn 0 starts
3503
+ // the session, and any later turn resumes it.
3504
+ cli: (ctx) =>
3505
+ \`claude -p "$(cat \${ctx.promptPath})" --model \${ctx.modelId} --output-format json --dangerously-skip-permissions\` +
3506
+ \` \${ctx.turn === 0 ? "--session-id" : "--resume"} \${ctx.sessionId} > \${ctx.resultPath}\`,
3507
+ collectReply: claudeCodeReply,
3508
+ requiredEnv: ["ANTHROPIC_API_KEY"],
3509
+ });
3510
+ `;
3511
+ var claudeCodeDeepseek = `import { claudeCodeReply, declareHarness } from "thunderjar";
3512
+
3513
+ // The claude-code harness, on DeepSeek's Anthropic-compatible API. DEEPSEEK_API_KEY is passed to
3514
+ // Claude Code as its auth token, and Claude Code's background requests go to DeepSeek too.
3515
+ //
3516
+ // Claude Code, installed as root, then run as the image's non-root \`node\` user: it won't skip its
3517
+ // permission prompts as root, and nobody is there to answer them.
3518
+ export default declareHarness({
3519
+ applyParameter: (_config, ctx) => \`RUN npm install -g @anthropic-ai/claude-code@\${ctx.version}
3520
+ ENV ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic \\\\
3521
+ ANTHROPIC_SMALL_FAST_MODEL=deepseek-chat \\\\
3522
+ ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-chat \\\\
3523
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
3524
+ RUN mkdir -p /thunderjar && chown -R node:node /workspace /thunderjar
3525
+ USER node\`,
3526
+ // The prompt is read from its file, so quotes and newlines reach the agent intact. Turn 0 starts
3527
+ // the session, and any later turn resumes it.
3528
+ cli: (ctx) =>
3529
+ \`ANTHROPIC_AUTH_TOKEN="$DEEPSEEK_API_KEY" claude -p "$(cat \${ctx.promptPath})" --model \${ctx.modelId} --output-format json --dangerously-skip-permissions\` +
3530
+ \` \${ctx.turn === 0 ? "--session-id" : "--resume"} \${ctx.sessionId} > \${ctx.resultPath}\`,
3531
+ collectReply: claudeCodeReply,
3532
+ requiredEnv: ["DEEPSEEK_API_KEY"],
3533
+ });
3534
+ `;
3535
+ var copilot = `import { declareHarness } from "thunderjar";
3536
+
3537
+ // GitHub Copilot CLI. COPILOT_GITHUB_TOKEN is a GitHub token with Copilot access: a fine-grained
3538
+ // personal access token with the "Copilot Requests" permission.
3539
+ export default declareHarness({
3540
+ applyParameter: (_config, ctx) => ${installAsRoot("npm install -g @github/copilot@${ctx.version}")},
3541
+ // The prompt is read from its file, so quotes and newlines reach the agent intact. Any turn after
3542
+ // the first continues the session.
3543
+ cli: (ctx) =>
3544
+ \`copilot -p "$(cat \${ctx.promptPath})" --model \${ctx.modelId} --allow-all-tools --no-color\` +
3545
+ \`\${ctx.turn === 0 ? "" : " --continue"} > \${ctx.resultPath}\`,
3546
+ ${stubReply}
3547
+ requiredEnv: ["COPILOT_GITHUB_TOKEN"],
3548
+ });
3549
+ `;
3550
+ var codex = `import { declareHarness } from "thunderjar";
3551
+
3552
+ // OpenAI's Codex CLI, without its sandbox or approvals: the container is the sandbox.
3553
+ export default declareHarness({
3554
+ applyParameter: (_config, ctx) => ${installAsRoot("npm install -g @openai/codex@${ctx.version}")},
3555
+ // The prompt is read from its file, so quotes and newlines reach the agent intact. Any turn after
3556
+ // the first resumes the session.
3557
+ cli: (ctx) =>
3558
+ \`codex exec\${ctx.turn === 0 ? "" : " resume --last"} --model \${ctx.modelId} --json --dangerously-bypass-approvals-and-sandbox\` +
3559
+ \` "$(cat \${ctx.promptPath})" > \${ctx.resultPath}\`,
3560
+ ${stubReply}
3561
+ requiredEnv: ["OPENAI_API_KEY"],
3562
+ });
3563
+ `;
3564
+ var opencode = `import { declareHarness } from "thunderjar";
3565
+
3566
+ // OpenCode, on OpenCode Zen. OPENCODE_API_KEY is an OpenCode Zen key.
3567
+ export default declareHarness({
3568
+ applyParameter: (_config, ctx) => ${installAsRoot("npm install -g opencode-ai@${ctx.version}")},
3569
+ // The prompt is read from its file, so quotes and newlines reach the agent intact. Any turn after
3570
+ // the first continues the session.
3571
+ cli: (ctx) =>
3572
+ \`opencode run --model \${ctx.modelId} --format json\${ctx.turn === 0 ? "" : " --continue"}\` +
3573
+ \` "$(cat \${ctx.promptPath})" > \${ctx.resultPath}\`,
3574
+ ${stubReply}
3575
+ requiredEnv: ["OPENCODE_API_KEY"],
3576
+ });
3577
+ `;
3578
+ var CHECK = (where) => `// Models come and go: check ${where} for the current IDs.`;
3579
+ var STARTER_HARNESSES = {
3580
+ "claude-code": {
3581
+ "index.ts": claudeCode,
3582
+ "models.ts": models(CHECK("https://docs.claude.com/en/docs/about-claude/models"), [
3583
+ ["haiku-4-5", "claude-haiku-4-5-20251001"],
3584
+ ["haiku-5-5", "claude-haiku-5-5"],
3585
+ ["sonnet-5-5", "claude-sonnet-5-5"],
3586
+ ["opus-5-5", "claude-opus-5-5"]
3587
+ ]),
3588
+ "versions.ts": versions("2.1.283"),
3589
+ "collectTokenCosts.ts": claudeCodeTokenCosts
3590
+ },
3591
+ copilot: {
3592
+ "index.ts": copilot,
3593
+ "models.ts": models(`${CHECK("`copilot --help`, or GitHub's supported models page,")}
3594
+ // Which you can use depends on your Copilot plan and your organization's policy.`, [
3595
+ ["gpt-6-1-sol", "gpt-6.1-sol"],
3596
+ ["gpt-5-6-luna", "gpt-5.6-luna"],
3597
+ ["gpt-5-5", "gpt-5.5"],
3598
+ ["haiku-5-5", "claude-haiku-5.5"],
3599
+ ["sonnet-5-5", "claude-sonnet-5.5"],
3600
+ ["opus-5-5", "claude-opus-5.5"]
3601
+ ]),
3602
+ "versions.ts": versions("1.0.105"),
3603
+ "collectTokenCosts.ts": stubTokenCosts("Copilot CLI")
3604
+ },
3605
+ codex: {
3606
+ "index.ts": codex,
3607
+ "models.ts": models(CHECK("https://developers.openai.com/codex/models"), [
3608
+ ["gpt-6-1-sol", "gpt-6.1-sol"],
3609
+ ["gpt-6-astra", "gpt-6-astra"],
3610
+ ["gpt-6-luna", "gpt-6-luna"],
3611
+ ["gpt-5-6-luna", "gpt-5.6-luna"],
3612
+ ["gpt-5-5", "gpt-5.5"]
3613
+ ]),
3614
+ "versions.ts": versions("0.166.0"),
3615
+ "collectTokenCosts.ts": stubTokenCosts("Codex")
3616
+ },
3617
+ "claude-code-deepseek": {
3618
+ "index.ts": claudeCodeDeepseek,
3619
+ "models.ts": models(CHECK("https://api-docs.deepseek.com/quick_start/pricing"), [
3620
+ ["deepseek-chat", "deepseek-chat"],
3621
+ ["deepseek-reasoner", "deepseek-reasoner"]
3622
+ ]),
3623
+ "versions.ts": versions("2.1.283"),
3624
+ "collectTokenCosts.ts": claudeCodeTokenCosts
3625
+ },
3626
+ opencode: {
3627
+ "index.ts": opencode,
3628
+ "models.ts": models(CHECK("https://opencode.ai/zen/v1/models"), [
3629
+ ["opus-5", "opencode/claude-opus-5"],
3630
+ ["gpt-5-6-luna", "opencode/gpt-5.6-luna"],
3631
+ ["gpt-5-5", "opencode/gpt-5.5"],
3632
+ ["deepseek-v4-pro", "opencode/deepseek-v4-pro"]
3633
+ ]),
3634
+ "versions.ts": versions("1.19.5"),
3635
+ "collectTokenCosts.ts": stubTokenCosts("OpenCode")
3636
+ }
3637
+ };
3638
+
3639
+ // src/cli/commands/init/scaffold.ts
3640
+ var declaration2 = (fn, body) => `import { ${fn} } from "thunderjar";
3641
+
3457
3642
  export default ${fn}(${body});
3458
3643
  `;
3459
3644
  var json = (value) => JSON.stringify(value, null, 2) + `
@@ -3478,7 +3663,7 @@ var tsconfig = json({
3478
3663
  include: ["**/*.ts"],
3479
3664
  exclude: ["node_modules", ".data", "**/*.template.ts"]
3480
3665
  });
3481
- var testCase = declaration("declareTestCase", `{
3666
+ var testCase = declaration2("declareTestCase", `{
3482
3667
  baseImage: ["node20"],
3483
3668
  codeState: ["baseline"],
3484
3669
  promptSet: ["snerk"],
@@ -3487,47 +3672,70 @@ var testCase = declaration("declareTestCase", `{
3487
3672
  initialPrompt: ["plain"],
3488
3673
  iterations: 1,
3489
3674
  }`);
3490
- var baseImage = declaration("declareBaseImage", `{
3675
+ var baseImage = declaration2("declareBaseImage", `{
3491
3676
  applyParameter: () => \`FROM node:20-bookworm\`,
3492
3677
  }`);
3493
- var harness = `import { claudeCodeReply, declareHarness } from "thunderjar";
3494
-
3495
- // Claude Code, installed as root, then run as the image's non-root \`node\` user: it won't skip its
3496
- // permission prompts as root, and nobody is there to answer them.
3497
- export default declareHarness({
3498
- applyParameter: (_config, ctx) => \`RUN npm install -g @anthropic-ai/claude-code@\${ctx.version}
3499
- RUN mkdir -p /thunderjar && chown -R node:node /workspace /thunderjar
3500
- USER node\`,
3501
- // The prompt is read from its file, so quotes and newlines reach the agent intact. Turn 0 starts
3502
- // the session, and any later turn resumes it.
3503
- cli: (ctx) =>
3504
- \`claude -p "$(cat \${ctx.promptPath})" --model \${ctx.modelId} --output-format json --dangerously-skip-permissions\` +
3505
- \` \${ctx.turn === 0 ? "--session-id" : "--resume"} \${ctx.sessionId} > \${ctx.resultPath}\`,
3506
- collectReply: claudeCodeReply,
3507
- requiredEnv: ["ANTHROPIC_API_KEY"],
3508
- });
3509
- `;
3510
- var harnessModels = declaration("declareHarnessModels", `{
3511
- "haiku-4-5": "claude-haiku-4-5-20251001",
3512
- }`);
3513
- var harnessVersions = declaration("declareHarnessVersions", `{
3514
- "2.1.283": { version: "2.1.283" },
3515
- }`);
3516
- var collectTokenCosts = `import { claudeCodeTokenCosts, declareTokenCostCollector } from "thunderjar";
3517
-
3518
- export default declareTokenCostCollector((raw) => claudeCodeTokenCosts(raw));
3519
- `;
3520
- var initialPrompt = declaration("declareInitialPrompt", `{
3678
+ var initialPrompt = declaration2("declareInitialPrompt", `{
3521
3679
  prompt: "Create a file isPrime.ts exporting a function that tests for primality.",
3522
3680
  timeoutSeconds: 600,
3523
3681
  }`);
3524
- var measurement = declaration("declareMeasurement", `{
3682
+ var measurement = declaration2("declareMeasurement", `{
3525
3683
  instrument: "fileCreated",
3526
3684
  config: { glob: "**/isPrime.ts" },
3527
3685
  aggregate: ["rate"],
3528
3686
  }`);
3687
+ var codeState = (head) => head ? `import { checkoutCommit, declareCodeState } from "thunderjar";
3688
+
3689
+ // The project's repository at a commit, checked out into /workspace. The commit must be in the
3690
+ // repository: commit it, or Thunderjar can't build it.
3691
+ export default declareCodeState({
3692
+ config: { commit: "${head}" },
3693
+ applyParameter: (config, ctx) => checkoutCommit(ctx, config.commit),
3694
+ });
3695
+ ` : `import { declareCodeState } from "thunderjar";
3696
+
3697
+ // An empty /workspace. The project wasn't a git repository when \`thunderjar init\` ran. Once it is,
3698
+ // check a commit of it out into /workspace:
3699
+ //
3700
+ // import { checkoutCommit, declareCodeState } from "thunderjar";
3701
+ //
3702
+ // export default declareCodeState({
3703
+ // config: { commit: "<a commit hash>" },
3704
+ // applyParameter: (config, ctx) => checkoutCommit(ctx, config.commit),
3705
+ // });
3706
+ export default declareCodeState({});
3707
+ `;
3708
+ var snerk = (head) => `import { declarePromptSet } from "thunderjar";
3709
+
3710
+ // Files from a commit of the project's repository, such as its AGENTS.md or skills, copied into
3711
+ // /workspace at the same paths, like \`git checkout <commit> -- <paths>\`. They're left uncommitted.
3712
+ // Each path is from the repository's root, e.g. "AGENTS.md" or ".agents/skills/review/SKILL.md".
3713
+ export default declarePromptSet<{ commit: string; paths: string[] }>({
3714
+ config: {
3715
+ commit: "${head ?? "<a commit hash>"}",
3716
+ paths: [],
3717
+ },
3718
+ applyParameter: (config, ctx) =>
3719
+ config.paths.map((path) => \`COPY \${ctx.git.show(config.commit, path)} /workspace/\${path}\`).join("\\n"),
3720
+ });
3721
+ `;
3722
+ var houseStyle = `import { declarePromptSet } from "thunderjar";
3723
+
3724
+ // The files in this folder's files/, copied over /workspace and committed, so the agent's changes
3725
+ // can be told from them. \`git add -A\`, not \`git commit -a\`, which skips new files such as a new
3726
+ // skill. Use it with a code state that checks out a commit, so /workspace is a git repository.
3727
+ export default declarePromptSet({
3728
+ applyParameter: (_config, ctx) => \`COPY \${ctx.dir}/files/ /workspace/
3729
+ RUN cd /workspace && git add -A && git commit --quiet -m "Thunderjar: apply promptSet"\`,
3730
+ });
3731
+ `;
3732
+ var houseStyleAgentsMd = `# House style
3733
+
3734
+ - Write TypeScript, with strict types.
3735
+ - Add a test beside each new module.
3736
+ `;
3529
3737
  var P = "test-case-parameters";
3530
- function scaffold({ nestedPackage }) {
3738
+ function scaffold({ nestedPackage, head }) {
3531
3739
  return {
3532
3740
  ...nestedPackage && {
3533
3741
  "package.json": json({ name: "thunderjar-config", private: true, type: "module", dependencies: { thunderjar: "latest" } })
@@ -3539,12 +3747,11 @@ function scaffold({ nestedPackage }) {
3539
3747
  "thunderjar.config.ts": config,
3540
3748
  "test-cases/example.ts": testCase,
3541
3749
  [`${P}/baseImages/node20/index.ts`]: baseImage,
3542
- [`${P}/codeStates/baseline/index.ts`]: declaration("declareCodeState", "{}"),
3543
- [`${P}/promptSets/snerk/index.ts`]: declaration("declarePromptSet", "{}"),
3544
- [`${P}/harnessModels/claude-code/index.ts`]: harness,
3545
- [`${P}/harnessModels/claude-code/models.ts`]: harnessModels,
3546
- [`${P}/harnessModels/claude-code/versions.ts`]: harnessVersions,
3547
- [`${P}/harnessModels/claude-code/collectTokenCosts.ts`]: collectTokenCosts,
3750
+ [`${P}/codeStates/baseline/index.ts`]: codeState(head),
3751
+ [`${P}/promptSets/snerk/index.ts`]: snerk(head),
3752
+ [`${P}/promptSets/house-style/index.ts`]: houseStyle,
3753
+ [`${P}/promptSets/house-style/files/AGENTS.md`]: houseStyleAgentsMd,
3754
+ ...Object.fromEntries(Object.entries(STARTER_HARNESSES).flatMap(([harness, files]) => Object.entries(files).map(([file, content]) => [`${P}/harnessModels/${harness}/${file}`, content]))),
3548
3755
  [`${P}/tasks/is-prime/initial-prompts/plain.ts`]: initialPrompt,
3549
3756
  [`${P}/tasks/is-prime/measurements/isPrimeTsExists.ts`]: measurement
3550
3757
  };
@@ -3559,12 +3766,17 @@ async function isDirectory2(path2) {
3559
3766
  return false;
3560
3767
  }
3561
3768
  }
3769
+ async function headCommit(dir) {
3770
+ const result = await $5`git rev-parse --verify --quiet HEAD`.cwd(dir).quiet().nothrow();
3771
+ return result.exitCode === 0 ? result.stdout.toString().trim() : undefined;
3772
+ }
3562
3773
  async function init(projectDir) {
3563
3774
  const root = `${projectDir}/${FOLDER2}`;
3564
3775
  const createdFolder = !await isDirectory2(root);
3565
3776
  const nestedPackage = !await Bun.file(`${projectDir}/package.json`).exists();
3777
+ const head = await headCommit(projectDir);
3566
3778
  const result = { createdFolder, created: [], existing: [] };
3567
- for (const [path2, content] of Object.entries(scaffold({ nestedPackage }))) {
3779
+ for (const [path2, content] of Object.entries(scaffold({ nestedPackage, head }))) {
3568
3780
  const file = Bun.file(`${root}/${path2}`);
3569
3781
  if (await file.exists()) {
3570
3782
  result.existing.push(path2);
@@ -3576,7 +3788,7 @@ async function init(projectDir) {
3576
3788
  return result;
3577
3789
  }
3578
3790
  function createInitCommand() {
3579
- return new Command("init").description("Scaffold the .thunderjar/ configuration folder in the current directory, with an example test case. Existing files are left as they are.").action(async () => {
3791
+ return new Command("init").description("Scaffold the .thunderjar/ configuration folder in the current directory, with an example test case, and starter prompt sets and harnesses to copy. Existing files are left as they are.").action(async () => {
3580
3792
  const { createdFolder, created, existing } = await init(process.cwd());
3581
3793
  if (createdFolder)
3582
3794
  console.log(`Created ${FOLDER2}/`);
@@ -3657,7 +3869,7 @@ var promptsOf = (initialPrompt2) => initialPrompt2.turns ?? [initialPrompt2.prom
3657
3869
  async function loadHarness(root, task, permutation, problems) {
3658
3870
  const { harness: name, version, model } = parsePair(permutation.harnessModel);
3659
3871
  const dir = `${PARAMETERS}/harnessModels/${name}`;
3660
- const [baseImage2, harness2, collectTokenCosts2, versions, models, initialPrompt2] = await Promise.all([
3872
+ const [baseImage2, harness, collectTokenCosts, versions2, models2, initialPrompt2] = await Promise.all([
3661
3873
  loadDeclaration(root, `${PARAMETERS}/baseImages/${permutation.baseImage}/index.ts`, "declareBaseImage", problems),
3662
3874
  loadDeclaration(root, `${dir}/index.ts`, "declareHarness", problems),
3663
3875
  loadDeclaration(root, `${dir}/collectTokenCosts.ts`, "declareTokenCostCollector", problems),
@@ -3665,16 +3877,16 @@ async function loadHarness(root, task, permutation, problems) {
3665
3877
  loadDeclaration(root, `${dir}/models.ts`, "declareHarnessModels", problems),
3666
3878
  loadDeclaration(root, `${PARAMETERS}/tasks/${task}/initial-prompts/${permutation.initialPrompt}.ts`, "declareInitialPrompt", problems)
3667
3879
  ]);
3668
- if (!baseImage2 || !harness2 || !collectTokenCosts2 || !versions || !models || !initialPrompt2)
3880
+ if (!baseImage2 || !harness || !collectTokenCosts || !versions2 || !models2 || !initialPrompt2)
3669
3881
  return;
3670
- const pinned = versions[version].version;
3671
- return { baseImage: baseImage2, harness: harness2, collectTokenCosts: collectTokenCosts2, version: pinned, modelId: models[model], initialPrompt: initialPrompt2 };
3882
+ const pinned = versions2[version].version;
3883
+ return { baseImage: baseImage2, harness, collectTokenCosts, version: pinned, modelId: models2[model], initialPrompt: initialPrompt2 };
3672
3884
  }
3673
- function planTurns({ harness: harness2, version, modelId, initialPrompt: initialPrompt2 }, sessionId) {
3885
+ function planTurns({ harness, version, modelId, initialPrompt: initialPrompt2 }, sessionId) {
3674
3886
  const prompts = promptsOf(initialPrompt2);
3675
3887
  return prompts.map((prompt, turn) => {
3676
3888
  const paths = turnPaths(turn, prompts.length);
3677
- const command = harness2.cli({ version, modelId, initialPrompt: prompt, ...paths, turn, sessionId });
3889
+ const command = harness.cli({ version, modelId, initialPrompt: prompt, ...paths, turn, sessionId });
3678
3890
  return { prompt, ...paths, command };
3679
3891
  });
3680
3892
  }
@@ -3799,8 +4011,8 @@ var message = (error) => error instanceof Error ? error.message : String(error);
3799
4011
  var recordable = (result) => result.outcome === "erroredWhileMeasuring" ? { outcome: "erroredWhileMeasuring", error: { message: message(result.error) } } : result;
3800
4012
  async function measureContainerRun(measurements, open, now = () => new Date) {
3801
4013
  const result = {};
3802
- for (const { name, declaration: declaration2, config: config2, contentHash } of measurements) {
3803
- const instrument = instruments[declaration2.instrument];
4014
+ for (const { name, declaration: declaration3, config: config2, contentHash } of measurements) {
4015
+ const instrument = instruments[declaration3.instrument];
3804
4016
  const ctx = await open(instrument.usesContainer !== false);
3805
4017
  let outcome;
3806
4018
  try {
@@ -3810,7 +4022,7 @@ async function measureContainerRun(measurements, open, now = () => new Date) {
3810
4022
  } finally {
3811
4023
  await ctx.close();
3812
4024
  }
3813
- result[name] = { instrument: declaration2.instrument, contentHash, measuredAt: now().toISOString(), outcome };
4025
+ result[name] = { instrument: declaration3.instrument, contentHash, measuredAt: now().toISOString(), outcome };
3814
4026
  }
3815
4027
  return result;
3816
4028
  }
@@ -3925,7 +4137,7 @@ function add(a, b) {
3925
4137
  ...costBasis !== undefined && { costBasis }
3926
4138
  };
3927
4139
  }
3928
- function collectTokenCosts2(collect, harnessResults, ctx) {
4140
+ function collectTokenCosts(collect, harnessResults, ctx) {
3929
4141
  const all = harnessResults.map((raw) => collectOne(collect, raw, ctx));
3930
4142
  const collected = all.filter((costs) => costs.outcome === "collected");
3931
4143
  if (collected.length === 0)
@@ -3943,7 +4155,7 @@ function collectTokenCosts2(collect, harnessResults, ctx) {
3943
4155
  // src/execution/runExecution.ts
3944
4156
  function containerRunRecord({ plan: plan2, name, testCase: testCase2 }, { number: permutationNumber, permutation }, iteration, execution, turns, tokenCosts) {
3945
4157
  const last = turns.at(-1);
3946
- const { harness: harness2, version, model } = parsePair(permutation.harnessModel);
4158
+ const { harness, version, model } = parsePair(permutation.harnessModel);
3947
4159
  return {
3948
4160
  executionId: plan2.executionId,
3949
4161
  testCase: name,
@@ -3954,7 +4166,7 @@ function containerRunRecord({ plan: plan2, name, testCase: testCase2 }, { number
3954
4166
  postrunImage: iteration.postrunImage,
3955
4167
  ranAt: new Date(execution.startedAt).toISOString(),
3956
4168
  parameters: Object.fromEntries(AXES.map((axis) => [axis, { name: permutation[axis], parameterHash: permutation.parameterHashes[axis] }])),
3957
- harnessModel: { harness: harness2, version, model, modelId: permutation.modelId },
4169
+ harnessModel: { harness, version, model, modelId: permutation.modelId },
3958
4170
  execution,
3959
4171
  harnessResult: last.harnessResult,
3960
4172
  reply: last.reply,
@@ -3996,7 +4208,7 @@ async function runExecution(run, store, hooks = {}) {
3996
4208
  container
3997
4209
  });
3998
4210
  const turns = harnessResults.map((harnessResult2) => ({ harnessResult: harnessResult2, reply: collectReply(permutation.harness.collectReply, harnessResult2) }));
3999
- const tokenCosts = collectTokenCosts2(permutation.collectTokenCosts, harnessResults, {
4211
+ const tokenCosts = collectTokenCosts(permutation.collectTokenCosts, harnessResults, {
4000
4212
  version: permutation.version,
4001
4213
  modelId: permutation.modelId
4002
4214
  });
@@ -4084,8 +4296,8 @@ var showMeasures = (result) => Object.entries(result).map(([name, { outcome }])
4084
4296
  function label(testCase2, { permutation }) {
4085
4297
  const varying = AXES.filter((axis) => axis !== "harnessModel" && axis !== "initialPrompt" && testCase2[axis].length > 1);
4086
4298
  const harnesses = new Set(testCase2.harnessModel.map((pair) => parsePair(pair).harness));
4087
- const harness2 = harnesses.size === testCase2.harnessModel.length ? parsePair(permutation.harnessModel).harness : permutation.harnessModel;
4088
- return [...varying.map((axis) => permutation[axis]), harness2, permutation.initialPrompt].join(" | ");
4299
+ const harness = harnesses.size === testCase2.harnessModel.length ? parsePair(permutation.harnessModel).harness : permutation.harnessModel;
4300
+ return [...varying.map((axis) => permutation[axis]), harness, permutation.initialPrompt].join(" | ");
4089
4301
  }
4090
4302
  async function run(cwd, name, imagePrefix, dataDir) {
4091
4303
  const root = await findThunderjarRoot(cwd);
@@ -4202,12 +4414,12 @@ function aggregateValues(values, methods) {
4202
4414
  };
4203
4415
  }
4204
4416
  function aggregatePermutation(runs, measurements) {
4205
- return Object.fromEntries(measurements.map(({ name, declaration: declaration2 }) => {
4417
+ return Object.fromEntries(measurements.map(({ name, declaration: declaration3 }) => {
4206
4418
  const values = runs.flatMap(({ result }) => {
4207
4419
  const outcome = result[name]?.outcome;
4208
4420
  return outcome?.outcome === "measured" ? [outcome.data] : [];
4209
4421
  });
4210
- const combined = aggregateValues(values, declaration2.aggregate);
4422
+ const combined = aggregateValues(values, declaration3.aggregate);
4211
4423
  const measure = "incompatible" in combined ? { measured: values.length, values: {}, incompatible: combined.incompatible } : { measured: values.length, values: combined.values };
4212
4424
  return [name, measure];
4213
4425
  }));
@@ -4285,7 +4497,7 @@ function varyingAxes(runs) {
4285
4497
  }
4286
4498
  function permutationTable(execution, runs, measurements) {
4287
4499
  const axes = varyingAxes(runs);
4288
- const methodColumns = measurements.flatMap(({ name, declaration: declaration2 }) => declaration2.aggregate.map((method, i) => ({ name, method, first: i === 0 })));
4500
+ const methodColumns = measurements.flatMap(({ name, declaration: declaration3 }) => declaration3.aggregate.map((method, i) => ({ name, method, first: i === 0 })));
4289
4501
  const numberWidth = String(execution.matrix.permutations).length;
4290
4502
  const withCosts = runs.some(({ tokenCosts }) => tokenCosts !== undefined);
4291
4503
  const rows = [];
@@ -4336,9 +4548,9 @@ function permutationTable(execution, runs, measurements) {
4336
4548
  }
4337
4549
  const header = ["", axes.join(" | "), "runs", ...methodColumns.map(({ method }) => method), ...withCosts ? ["tokens (mean)", "cost (mean)"] : []];
4338
4550
  const widths = header.map((cell, i) => Math.max(cell.length, ...rows.map((row) => row[i]?.length ?? 0)));
4339
- for (const { name, declaration: declaration2 } of measurements) {
4551
+ for (const { name, declaration: declaration3 } of measurements) {
4340
4552
  const start = 3 + methodColumns.findIndex((column2) => column2.name === name);
4341
- const end = start + declaration2.aggregate.length - 1;
4553
+ const end = start + declaration3.aggregate.length - 1;
4342
4554
  const span = widths.slice(start, end + 1).reduce((a, b) => a + b, 0) + GAP.length * (end - start);
4343
4555
  if (span < name.length)
4344
4556
  widths[end] += name.length - span;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunderjar",
3
- "version": "0.1.3-preview-e09ffa1",
3
+ "version": "0.1.3-preview-d23f868",
4
4
  "type": "module",
5
5
  "description": "Thunderjar",
6
6
  "license": "MIT",
@@ -79,6 +79,9 @@ export type BaseImageDeclaration<C = unknown> = Applies<C, ApplyParameterCtx> &
79
79
  * namespaces: `["--security-opt", "seccomp=unconfined", "--security-opt", "apparmor=unconfined"]`.
80
80
  * Each option and value is its own string. Ask for the least you need: `--privileged` works, but
81
81
  * gives the agent the whole machine.
82
+ *
83
+ * Some hosts forbid what no option can allow. Ubuntu 23.10 and later stop unconfined processes
84
+ * making user namespaces until `sysctl kernel.apparmor_restrict_unprivileged_userns=0`.
82
85
  */
83
86
  runOptions?: string[];
84
87
  };