thunderjar 0.1.3-preview-e09ffa1 → 0.1.3-preview-79a2982

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-79a2982",
2051
2051
  type: "module",
2052
2052
  description: "Thunderjar",
2053
2053
  license: "MIT",
@@ -3451,9 +3451,161 @@ 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 copilot = `import { declareHarness } from "thunderjar";
3512
+
3513
+ // GitHub Copilot CLI. COPILOT_GITHUB_TOKEN is a GitHub token with Copilot access: a fine-grained
3514
+ // personal access token with the "Copilot Requests" permission.
3515
+ export default declareHarness({
3516
+ applyParameter: (_config, ctx) => ${installAsRoot("npm install -g @github/copilot@${ctx.version}")},
3517
+ // The prompt is read from its file, so quotes and newlines reach the agent intact. Any turn after
3518
+ // the first continues the session.
3519
+ cli: (ctx) =>
3520
+ \`copilot -p "$(cat \${ctx.promptPath})" --model \${ctx.modelId} --allow-all-tools --no-color\` +
3521
+ \`\${ctx.turn === 0 ? "" : " --continue"} > \${ctx.resultPath}\`,
3522
+ ${stubReply}
3523
+ requiredEnv: ["COPILOT_GITHUB_TOKEN"],
3524
+ });
3525
+ `;
3526
+ var codex = `import { declareHarness } from "thunderjar";
3527
+
3528
+ // OpenAI's Codex CLI, without its sandbox or approvals: the container is the sandbox.
3529
+ export default declareHarness({
3530
+ applyParameter: (_config, ctx) => ${installAsRoot("npm install -g @openai/codex@${ctx.version}")},
3531
+ // The prompt is read from its file, so quotes and newlines reach the agent intact. Any turn after
3532
+ // the first resumes the session.
3533
+ cli: (ctx) =>
3534
+ \`codex exec\${ctx.turn === 0 ? "" : " resume --last"} --model \${ctx.modelId} --json --dangerously-bypass-approvals-and-sandbox\` +
3535
+ \` "$(cat \${ctx.promptPath})" > \${ctx.resultPath}\`,
3536
+ ${stubReply}
3537
+ requiredEnv: ["OPENAI_API_KEY"],
3538
+ });
3539
+ `;
3540
+ var opencode = `import { declareHarness } from "thunderjar";
3541
+
3542
+ // OpenCode, on OpenCode Zen. OPENCODE_API_KEY is an OpenCode Zen key.
3543
+ export default declareHarness({
3544
+ applyParameter: (_config, ctx) => ${installAsRoot("npm install -g opencode-ai@${ctx.version}")},
3545
+ // The prompt is read from its file, so quotes and newlines reach the agent intact. Any turn after
3546
+ // the first continues the session.
3547
+ cli: (ctx) =>
3548
+ \`opencode run --model \${ctx.modelId} --format json\${ctx.turn === 0 ? "" : " --continue"}\` +
3549
+ \` "$(cat \${ctx.promptPath})" > \${ctx.resultPath}\`,
3550
+ ${stubReply}
3551
+ requiredEnv: ["OPENCODE_API_KEY"],
3552
+ });
3553
+ `;
3554
+ var CHECK = (where) => `// Models come and go: check ${where} for the current IDs.`;
3555
+ var STARTER_HARNESSES = {
3556
+ "claude-code": {
3557
+ "index.ts": claudeCode,
3558
+ "models.ts": models(CHECK("https://docs.claude.com/en/docs/about-claude/models"), [
3559
+ ["haiku-4-5", "claude-haiku-4-5-20251001"],
3560
+ ["haiku-5-5", "claude-haiku-5-5"],
3561
+ ["sonnet-5-5", "claude-sonnet-5-5"],
3562
+ ["opus-5-5", "claude-opus-5-5"]
3563
+ ]),
3564
+ "versions.ts": versions("2.1.283"),
3565
+ "collectTokenCosts.ts": claudeCodeTokenCosts
3566
+ },
3567
+ copilot: {
3568
+ "index.ts": copilot,
3569
+ "models.ts": models(`${CHECK("`copilot --help`, or GitHub's supported models page,")}
3570
+ // Which you can use depends on your Copilot plan and your organization's policy.`, [
3571
+ ["gpt-6-1-sol", "gpt-6.1-sol"],
3572
+ ["gpt-5-6-luna", "gpt-5.6-luna"],
3573
+ ["gpt-5-5", "gpt-5.5"],
3574
+ ["haiku-5-5", "claude-haiku-5.5"],
3575
+ ["sonnet-5-5", "claude-sonnet-5.5"],
3576
+ ["opus-5-5", "claude-opus-5.5"]
3577
+ ]),
3578
+ "versions.ts": versions("1.0.105"),
3579
+ "collectTokenCosts.ts": stubTokenCosts("Copilot CLI")
3580
+ },
3581
+ codex: {
3582
+ "index.ts": codex,
3583
+ "models.ts": models(CHECK("https://developers.openai.com/codex/models"), [
3584
+ ["gpt-6-1-sol", "gpt-6.1-sol"],
3585
+ ["gpt-6-astra", "gpt-6-astra"],
3586
+ ["gpt-6-luna", "gpt-6-luna"],
3587
+ ["gpt-5-6-luna", "gpt-5.6-luna"],
3588
+ ["gpt-5-5", "gpt-5.5"]
3589
+ ]),
3590
+ "versions.ts": versions("0.166.0"),
3591
+ "collectTokenCosts.ts": stubTokenCosts("Codex")
3592
+ },
3593
+ opencode: {
3594
+ "index.ts": opencode,
3595
+ "models.ts": models(CHECK("https://opencode.ai/zen/v1/models"), [
3596
+ ["opus-5", "opencode/claude-opus-5"],
3597
+ ["gpt-5-6-luna", "opencode/gpt-5.6-luna"],
3598
+ ["gpt-5-5", "opencode/gpt-5.5"],
3599
+ ["deepseek-v4-pro", "opencode/deepseek-v4-pro"]
3600
+ ]),
3601
+ "versions.ts": versions("1.19.5"),
3602
+ "collectTokenCosts.ts": stubTokenCosts("OpenCode")
3603
+ }
3604
+ };
3605
+
3606
+ // src/cli/commands/init/scaffold.ts
3607
+ var declaration2 = (fn, body) => `import { ${fn} } from "thunderjar";
3608
+
3457
3609
  export default ${fn}(${body});
3458
3610
  `;
3459
3611
  var json = (value) => JSON.stringify(value, null, 2) + `
@@ -3478,7 +3630,7 @@ var tsconfig = json({
3478
3630
  include: ["**/*.ts"],
3479
3631
  exclude: ["node_modules", ".data", "**/*.template.ts"]
3480
3632
  });
3481
- var testCase = declaration("declareTestCase", `{
3633
+ var testCase = declaration2("declareTestCase", `{
3482
3634
  baseImage: ["node20"],
3483
3635
  codeState: ["baseline"],
3484
3636
  promptSet: ["snerk"],
@@ -3487,47 +3639,93 @@ var testCase = declaration("declareTestCase", `{
3487
3639
  initialPrompt: ["plain"],
3488
3640
  iterations: 1,
3489
3641
  }`);
3490
- var baseImage = declaration("declareBaseImage", `{
3491
- applyParameter: () => \`FROM node:20-bookworm\`,
3492
- }`);
3493
- var harness = `import { claudeCodeReply, declareHarness } from "thunderjar";
3642
+ var baseImage = `import { declareBaseImage } from "thunderjar";
3494
3643
 
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"],
3644
+ // With bun, which the templateTest measurement runs its tests with.
3645
+ export default declareBaseImage({
3646
+ applyParameter: () => \`FROM node:20-bookworm
3647
+ RUN npm install --global bun@1.3.14\`,
3508
3648
  });
3509
3649
  `;
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", `{
3650
+ var initialPrompt = declaration2("declareInitialPrompt", `{
3521
3651
  prompt: "Create a file isPrime.ts exporting a function that tests for primality.",
3522
3652
  timeoutSeconds: 600,
3523
3653
  }`);
3524
- var measurement = declaration("declareMeasurement", `{
3654
+ var measurement = declaration2("declareMeasurement", `{
3525
3655
  instrument: "fileCreated",
3526
3656
  config: { glob: "**/isPrime.ts" },
3527
3657
  aggregate: ["rate"],
3528
3658
  }`);
3659
+ var templateTestMeasurement = declaration2("declareMeasurement", `{
3660
+ instrument: "templateTest",
3661
+ config: { subject: "**/isPrime.ts", template: "./isPrime.test.template.ts" },
3662
+ aggregate: ["sum", "mean"],
3663
+ }`);
3664
+ var isPrimeTestTemplate = `import { {{defaultExport}} } from "{{subjectPath}}";
3665
+
3666
+ test("2 is prime", () => expect({{defaultExport}}(2)).toBe(true));
3667
+ test("17 is prime", () => expect({{defaultExport}}(17)).toBe(true));
3668
+ test("15 is not prime", () => expect({{defaultExport}}(15)).toBe(false));
3669
+ test("0 is not prime", () => expect({{defaultExport}}(0)).toBe(false));
3670
+ test("1 is not prime", () => expect({{defaultExport}}(1)).toBe(false));
3671
+ `;
3672
+ var tsIgnoreCountMeasurement = declaration2("declareMeasurement", `{
3673
+ instrument: "grep",
3674
+ config: { glob: "**/*.ts", pattern: "@ts-ignore" },
3675
+ aggregate: ["mean", "max"],
3676
+ }`);
3677
+ var codeState = (head) => head ? `import { checkoutCommit, declareCodeState } from "thunderjar";
3678
+
3679
+ // The project's repository at a commit, checked out into /workspace. The commit must be in the
3680
+ // repository: commit it, or Thunderjar can't build it.
3681
+ export default declareCodeState({
3682
+ config: { commit: "${head}" },
3683
+ applyParameter: (config, ctx) => checkoutCommit(ctx, config.commit),
3684
+ });
3685
+ ` : `import { declareCodeState } from "thunderjar";
3686
+
3687
+ // An empty /workspace. The project wasn't a git repository when \`thunderjar init\` ran. Once it is,
3688
+ // check a commit of it out into /workspace:
3689
+ //
3690
+ // import { checkoutCommit, declareCodeState } from "thunderjar";
3691
+ //
3692
+ // export default declareCodeState({
3693
+ // config: { commit: "<a commit hash>" },
3694
+ // applyParameter: (config, ctx) => checkoutCommit(ctx, config.commit),
3695
+ // });
3696
+ export default declareCodeState({});
3697
+ `;
3698
+ var snerk = (head) => `import { declarePromptSet } from "thunderjar";
3699
+
3700
+ // Files from a commit of the project's repository, such as its AGENTS.md or skills, copied into
3701
+ // /workspace at the same paths, like \`git checkout <commit> -- <paths>\`. They're left uncommitted.
3702
+ // Each path is from the repository's root, e.g. "AGENTS.md" or ".agents/skills/review/SKILL.md".
3703
+ export default declarePromptSet<{ commit: string; paths: string[] }>({
3704
+ config: {
3705
+ commit: "${head ?? "<a commit hash>"}",
3706
+ paths: [],
3707
+ },
3708
+ applyParameter: (config, ctx) =>
3709
+ config.paths.map((path) => \`COPY \${ctx.git.show(config.commit, path)} /workspace/\${path}\`).join("\\n"),
3710
+ });
3711
+ `;
3712
+ var houseStyle = `import { declarePromptSet } from "thunderjar";
3713
+
3714
+ // The files in this folder's files/, copied over /workspace and committed, so the agent's changes
3715
+ // can be told from them. \`git add -A\`, not \`git commit -a\`, which skips new files such as a new
3716
+ // skill. Use it with a code state that checks out a commit, so /workspace is a git repository.
3717
+ export default declarePromptSet({
3718
+ applyParameter: (_config, ctx) => \`COPY \${ctx.dir}/files/ /workspace/
3719
+ RUN cd /workspace && git add -A && git commit --quiet -m "Thunderjar: apply promptSet"\`,
3720
+ });
3721
+ `;
3722
+ var houseStyleAgentsMd = `# House style
3723
+
3724
+ - Write TypeScript, with strict types.
3725
+ - Add a test beside each new module.
3726
+ `;
3529
3727
  var P = "test-case-parameters";
3530
- function scaffold({ nestedPackage }) {
3728
+ function scaffold({ nestedPackage, head }) {
3531
3729
  return {
3532
3730
  ...nestedPackage && {
3533
3731
  "package.json": json({ name: "thunderjar-config", private: true, type: "module", dependencies: { thunderjar: "latest" } })
@@ -3539,14 +3737,16 @@ function scaffold({ nestedPackage }) {
3539
3737
  "thunderjar.config.ts": config,
3540
3738
  "test-cases/example.ts": testCase,
3541
3739
  [`${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,
3740
+ [`${P}/codeStates/baseline/index.ts`]: codeState(head),
3741
+ [`${P}/promptSets/snerk/index.ts`]: snerk(head),
3742
+ [`${P}/promptSets/house-style/index.ts`]: houseStyle,
3743
+ [`${P}/promptSets/house-style/files/AGENTS.md`]: houseStyleAgentsMd,
3744
+ ...Object.fromEntries(Object.entries(STARTER_HARNESSES).flatMap(([harness, files]) => Object.entries(files).map(([file, content]) => [`${P}/harnessModels/${harness}/${file}`, content]))),
3548
3745
  [`${P}/tasks/is-prime/initial-prompts/plain.ts`]: initialPrompt,
3549
- [`${P}/tasks/is-prime/measurements/isPrimeTsExists.ts`]: measurement
3746
+ [`${P}/tasks/is-prime/measurements/isPrimeTsExists.ts`]: measurement,
3747
+ [`${P}/tasks/is-prime/measurements/isPrimeTemplateTest.ts`]: templateTestMeasurement,
3748
+ [`${P}/tasks/is-prime/measurements/isPrime.test.template.ts`]: isPrimeTestTemplate,
3749
+ [`${P}/tasks/is-prime/measurements/tsIgnoreCount.ts`]: tsIgnoreCountMeasurement
3550
3750
  };
3551
3751
  }
3552
3752
 
@@ -3559,12 +3759,17 @@ async function isDirectory2(path2) {
3559
3759
  return false;
3560
3760
  }
3561
3761
  }
3762
+ async function headCommit(dir) {
3763
+ const result = await $5`git rev-parse --verify --quiet HEAD`.cwd(dir).quiet().nothrow();
3764
+ return result.exitCode === 0 ? result.stdout.toString().trim() : undefined;
3765
+ }
3562
3766
  async function init(projectDir) {
3563
3767
  const root = `${projectDir}/${FOLDER2}`;
3564
3768
  const createdFolder = !await isDirectory2(root);
3565
3769
  const nestedPackage = !await Bun.file(`${projectDir}/package.json`).exists();
3770
+ const head = await headCommit(projectDir);
3566
3771
  const result = { createdFolder, created: [], existing: [] };
3567
- for (const [path2, content] of Object.entries(scaffold({ nestedPackage }))) {
3772
+ for (const [path2, content] of Object.entries(scaffold({ nestedPackage, head }))) {
3568
3773
  const file = Bun.file(`${root}/${path2}`);
3569
3774
  if (await file.exists()) {
3570
3775
  result.existing.push(path2);
@@ -3576,7 +3781,7 @@ async function init(projectDir) {
3576
3781
  return result;
3577
3782
  }
3578
3783
  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 () => {
3784
+ 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
3785
  const { createdFolder, created, existing } = await init(process.cwd());
3581
3786
  if (createdFolder)
3582
3787
  console.log(`Created ${FOLDER2}/`);
@@ -3657,7 +3862,7 @@ var promptsOf = (initialPrompt2) => initialPrompt2.turns ?? [initialPrompt2.prom
3657
3862
  async function loadHarness(root, task, permutation, problems) {
3658
3863
  const { harness: name, version, model } = parsePair(permutation.harnessModel);
3659
3864
  const dir = `${PARAMETERS}/harnessModels/${name}`;
3660
- const [baseImage2, harness2, collectTokenCosts2, versions, models, initialPrompt2] = await Promise.all([
3865
+ const [baseImage2, harness, collectTokenCosts, versions2, models2, initialPrompt2] = await Promise.all([
3661
3866
  loadDeclaration(root, `${PARAMETERS}/baseImages/${permutation.baseImage}/index.ts`, "declareBaseImage", problems),
3662
3867
  loadDeclaration(root, `${dir}/index.ts`, "declareHarness", problems),
3663
3868
  loadDeclaration(root, `${dir}/collectTokenCosts.ts`, "declareTokenCostCollector", problems),
@@ -3665,16 +3870,16 @@ async function loadHarness(root, task, permutation, problems) {
3665
3870
  loadDeclaration(root, `${dir}/models.ts`, "declareHarnessModels", problems),
3666
3871
  loadDeclaration(root, `${PARAMETERS}/tasks/${task}/initial-prompts/${permutation.initialPrompt}.ts`, "declareInitialPrompt", problems)
3667
3872
  ]);
3668
- if (!baseImage2 || !harness2 || !collectTokenCosts2 || !versions || !models || !initialPrompt2)
3873
+ if (!baseImage2 || !harness || !collectTokenCosts || !versions2 || !models2 || !initialPrompt2)
3669
3874
  return;
3670
- const pinned = versions[version].version;
3671
- return { baseImage: baseImage2, harness: harness2, collectTokenCosts: collectTokenCosts2, version: pinned, modelId: models[model], initialPrompt: initialPrompt2 };
3875
+ const pinned = versions2[version].version;
3876
+ return { baseImage: baseImage2, harness, collectTokenCosts, version: pinned, modelId: models2[model], initialPrompt: initialPrompt2 };
3672
3877
  }
3673
- function planTurns({ harness: harness2, version, modelId, initialPrompt: initialPrompt2 }, sessionId) {
3878
+ function planTurns({ harness, version, modelId, initialPrompt: initialPrompt2 }, sessionId) {
3674
3879
  const prompts = promptsOf(initialPrompt2);
3675
3880
  return prompts.map((prompt, turn) => {
3676
3881
  const paths = turnPaths(turn, prompts.length);
3677
- const command = harness2.cli({ version, modelId, initialPrompt: prompt, ...paths, turn, sessionId });
3882
+ const command = harness.cli({ version, modelId, initialPrompt: prompt, ...paths, turn, sessionId });
3678
3883
  return { prompt, ...paths, command };
3679
3884
  });
3680
3885
  }
@@ -3799,8 +4004,8 @@ var message = (error) => error instanceof Error ? error.message : String(error);
3799
4004
  var recordable = (result) => result.outcome === "erroredWhileMeasuring" ? { outcome: "erroredWhileMeasuring", error: { message: message(result.error) } } : result;
3800
4005
  async function measureContainerRun(measurements, open, now = () => new Date) {
3801
4006
  const result = {};
3802
- for (const { name, declaration: declaration2, config: config2, contentHash } of measurements) {
3803
- const instrument = instruments[declaration2.instrument];
4007
+ for (const { name, declaration: declaration3, config: config2, contentHash } of measurements) {
4008
+ const instrument = instruments[declaration3.instrument];
3804
4009
  const ctx = await open(instrument.usesContainer !== false);
3805
4010
  let outcome;
3806
4011
  try {
@@ -3810,7 +4015,7 @@ async function measureContainerRun(measurements, open, now = () => new Date) {
3810
4015
  } finally {
3811
4016
  await ctx.close();
3812
4017
  }
3813
- result[name] = { instrument: declaration2.instrument, contentHash, measuredAt: now().toISOString(), outcome };
4018
+ result[name] = { instrument: declaration3.instrument, contentHash, measuredAt: now().toISOString(), outcome };
3814
4019
  }
3815
4020
  return result;
3816
4021
  }
@@ -3925,7 +4130,7 @@ function add(a, b) {
3925
4130
  ...costBasis !== undefined && { costBasis }
3926
4131
  };
3927
4132
  }
3928
- function collectTokenCosts2(collect, harnessResults, ctx) {
4133
+ function collectTokenCosts(collect, harnessResults, ctx) {
3929
4134
  const all = harnessResults.map((raw) => collectOne(collect, raw, ctx));
3930
4135
  const collected = all.filter((costs) => costs.outcome === "collected");
3931
4136
  if (collected.length === 0)
@@ -3943,7 +4148,7 @@ function collectTokenCosts2(collect, harnessResults, ctx) {
3943
4148
  // src/execution/runExecution.ts
3944
4149
  function containerRunRecord({ plan: plan2, name, testCase: testCase2 }, { number: permutationNumber, permutation }, iteration, execution, turns, tokenCosts) {
3945
4150
  const last = turns.at(-1);
3946
- const { harness: harness2, version, model } = parsePair(permutation.harnessModel);
4151
+ const { harness, version, model } = parsePair(permutation.harnessModel);
3947
4152
  return {
3948
4153
  executionId: plan2.executionId,
3949
4154
  testCase: name,
@@ -3954,7 +4159,7 @@ function containerRunRecord({ plan: plan2, name, testCase: testCase2 }, { number
3954
4159
  postrunImage: iteration.postrunImage,
3955
4160
  ranAt: new Date(execution.startedAt).toISOString(),
3956
4161
  parameters: Object.fromEntries(AXES.map((axis) => [axis, { name: permutation[axis], parameterHash: permutation.parameterHashes[axis] }])),
3957
- harnessModel: { harness: harness2, version, model, modelId: permutation.modelId },
4162
+ harnessModel: { harness, version, model, modelId: permutation.modelId },
3958
4163
  execution,
3959
4164
  harnessResult: last.harnessResult,
3960
4165
  reply: last.reply,
@@ -3996,7 +4201,7 @@ async function runExecution(run, store, hooks = {}) {
3996
4201
  container
3997
4202
  });
3998
4203
  const turns = harnessResults.map((harnessResult2) => ({ harnessResult: harnessResult2, reply: collectReply(permutation.harness.collectReply, harnessResult2) }));
3999
- const tokenCosts = collectTokenCosts2(permutation.collectTokenCosts, harnessResults, {
4204
+ const tokenCosts = collectTokenCosts(permutation.collectTokenCosts, harnessResults, {
4000
4205
  version: permutation.version,
4001
4206
  modelId: permutation.modelId
4002
4207
  });
@@ -4084,8 +4289,8 @@ var showMeasures = (result) => Object.entries(result).map(([name, { outcome }])
4084
4289
  function label(testCase2, { permutation }) {
4085
4290
  const varying = AXES.filter((axis) => axis !== "harnessModel" && axis !== "initialPrompt" && testCase2[axis].length > 1);
4086
4291
  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(" | ");
4292
+ const harness = harnesses.size === testCase2.harnessModel.length ? parsePair(permutation.harnessModel).harness : permutation.harnessModel;
4293
+ return [...varying.map((axis) => permutation[axis]), harness, permutation.initialPrompt].join(" | ");
4089
4294
  }
4090
4295
  async function run(cwd, name, imagePrefix, dataDir) {
4091
4296
  const root = await findThunderjarRoot(cwd);
@@ -4202,12 +4407,12 @@ function aggregateValues(values, methods) {
4202
4407
  };
4203
4408
  }
4204
4409
  function aggregatePermutation(runs, measurements) {
4205
- return Object.fromEntries(measurements.map(({ name, declaration: declaration2 }) => {
4410
+ return Object.fromEntries(measurements.map(({ name, declaration: declaration3 }) => {
4206
4411
  const values = runs.flatMap(({ result }) => {
4207
4412
  const outcome = result[name]?.outcome;
4208
4413
  return outcome?.outcome === "measured" ? [outcome.data] : [];
4209
4414
  });
4210
- const combined = aggregateValues(values, declaration2.aggregate);
4415
+ const combined = aggregateValues(values, declaration3.aggregate);
4211
4416
  const measure = "incompatible" in combined ? { measured: values.length, values: {}, incompatible: combined.incompatible } : { measured: values.length, values: combined.values };
4212
4417
  return [name, measure];
4213
4418
  }));
@@ -4285,7 +4490,7 @@ function varyingAxes(runs) {
4285
4490
  }
4286
4491
  function permutationTable(execution, runs, measurements) {
4287
4492
  const axes = varyingAxes(runs);
4288
- const methodColumns = measurements.flatMap(({ name, declaration: declaration2 }) => declaration2.aggregate.map((method, i) => ({ name, method, first: i === 0 })));
4493
+ const methodColumns = measurements.flatMap(({ name, declaration: declaration3 }) => declaration3.aggregate.map((method, i) => ({ name, method, first: i === 0 })));
4289
4494
  const numberWidth = String(execution.matrix.permutations).length;
4290
4495
  const withCosts = runs.some(({ tokenCosts }) => tokenCosts !== undefined);
4291
4496
  const rows = [];
@@ -4336,9 +4541,9 @@ function permutationTable(execution, runs, measurements) {
4336
4541
  }
4337
4542
  const header = ["", axes.join(" | "), "runs", ...methodColumns.map(({ method }) => method), ...withCosts ? ["tokens (mean)", "cost (mean)"] : []];
4338
4543
  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) {
4544
+ for (const { name, declaration: declaration3 } of measurements) {
4340
4545
  const start = 3 + methodColumns.findIndex((column2) => column2.name === name);
4341
- const end = start + declaration2.aggregate.length - 1;
4546
+ const end = start + declaration3.aggregate.length - 1;
4342
4547
  const span = widths.slice(start, end + 1).reduce((a, b) => a + b, 0) + GAP.length * (end - start);
4343
4548
  if (span < name.length)
4344
4549
  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-79a2982",
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
  };