labgate 0.5.40 → 0.5.42

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 (44) hide show
  1. package/README.md +132 -265
  2. package/dist/cli.js +9 -33
  3. package/dist/cli.js.map +1 -1
  4. package/dist/lib/config.d.ts +18 -3
  5. package/dist/lib/config.js +151 -80
  6. package/dist/lib/config.js.map +1 -1
  7. package/dist/lib/container.d.ts +11 -9
  8. package/dist/lib/container.js +753 -302
  9. package/dist/lib/container.js.map +1 -1
  10. package/dist/lib/dataset-mcp.js +2 -9
  11. package/dist/lib/dataset-mcp.js.map +1 -1
  12. package/dist/lib/display-mcp.d.ts +2 -2
  13. package/dist/lib/display-mcp.js +17 -38
  14. package/dist/lib/display-mcp.js.map +1 -1
  15. package/dist/lib/doctor.js +8 -0
  16. package/dist/lib/doctor.js.map +1 -1
  17. package/dist/lib/explorer-claude.js +36 -1
  18. package/dist/lib/explorer-claude.js.map +1 -1
  19. package/dist/lib/explorer-eval.js +3 -2
  20. package/dist/lib/explorer-eval.js.map +1 -1
  21. package/dist/lib/init.js +14 -18
  22. package/dist/lib/init.js.map +1 -1
  23. package/dist/lib/slurm-cli-passthrough.d.ts +12 -2
  24. package/dist/lib/slurm-cli-passthrough.js +401 -143
  25. package/dist/lib/slurm-cli-passthrough.js.map +1 -1
  26. package/dist/lib/startup-stage-lock.d.ts +21 -0
  27. package/dist/lib/startup-stage-lock.js +196 -0
  28. package/dist/lib/startup-stage-lock.js.map +1 -0
  29. package/dist/lib/ui.d.ts +40 -0
  30. package/dist/lib/ui.html +4953 -3366
  31. package/dist/lib/ui.js +1749 -295
  32. package/dist/lib/ui.js.map +1 -1
  33. package/dist/lib/web-terminal-startup-readiness.d.ts +8 -0
  34. package/dist/lib/web-terminal-startup-readiness.js +29 -0
  35. package/dist/lib/web-terminal-startup-readiness.js.map +1 -0
  36. package/dist/lib/web-terminal.d.ts +51 -0
  37. package/dist/lib/web-terminal.js +171 -1
  38. package/dist/lib/web-terminal.js.map +1 -1
  39. package/dist/mcp-bundles/dataset-mcp.bundle.mjs +125 -74
  40. package/dist/mcp-bundles/display-mcp.bundle.mjs +22 -30
  41. package/dist/mcp-bundles/explorer-mcp.bundle.mjs +211 -106
  42. package/dist/mcp-bundles/results-mcp.bundle.mjs +22 -24
  43. package/dist/mcp-bundles/slurm-mcp.bundle.mjs +6 -8
  44. package/package.json +1 -1
@@ -7209,6 +7209,7 @@ __export(config_exports, {
7209
7209
  LABGATE_DIR: () => LABGATE_DIR,
7210
7210
  PRIVATE_DIR_MODE: () => PRIVATE_DIR_MODE,
7211
7211
  PRIVATE_FILE_MODE: () => PRIVATE_FILE_MODE,
7212
+ buildConfigFromRaw: () => buildConfigFromRaw,
7212
7213
  ensurePrivateDir: () => ensurePrivateDir,
7213
7214
  ensurePrivateFile: () => ensurePrivateFile,
7214
7215
  findLockedFieldConflicts: () => findLockedFieldConflicts,
@@ -7236,11 +7237,15 @@ __export(config_exports, {
7236
7237
  isKnownPluginId: () => isKnownPluginId,
7237
7238
  loadConfig: () => loadConfig,
7238
7239
  loadEffectiveConfig: () => loadEffectiveConfig,
7240
+ parseRawConfigText: () => parseRawConfigText,
7241
+ readRawConfigFile: () => readRawConfigFile,
7239
7242
  shouldClaudeHeadlessRunWithAllowedPermissions: () => shouldClaudeHeadlessRunWithAllowedPermissions,
7240
- validateConfig: () => validateConfig
7243
+ stripJsonComments: () => stripJsonComments,
7244
+ validateConfig: () => validateConfig,
7245
+ writeRawConfigFile: () => writeRawConfigFile
7241
7246
  });
7242
- import { readFileSync as readFileSync3, existsSync as existsSync3, chmodSync, mkdirSync } from "fs";
7243
- import { join as join3, resolve } from "path";
7247
+ import { readFileSync as readFileSync3, existsSync as existsSync3, chmodSync, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
7248
+ import { dirname as dirname2, join as join3, resolve } from "path";
7244
7249
  import { homedir as homedir3 } from "os";
7245
7250
  function isKnownPluginId(pluginId) {
7246
7251
  return KNOWN_PLUGIN_ID_SET.has(pluginId);
@@ -7255,6 +7260,12 @@ function sanitizePluginMap(rawPlugins) {
7255
7260
  }
7256
7261
  return merged;
7257
7262
  }
7263
+ function resolveImagesDirOverride(raw) {
7264
+ if (typeof raw !== "string") return null;
7265
+ const trimmed = raw.trim();
7266
+ if (!trimmed) return null;
7267
+ return resolve(trimmed.replace(/^~/, homedir3()));
7268
+ }
7258
7269
  function ensurePrivateDir(path) {
7259
7270
  mkdirSync(path, { recursive: true, mode: PRIVATE_DIR_MODE });
7260
7271
  try {
@@ -7278,14 +7289,15 @@ function getSandboxHome() {
7278
7289
  return join3(LABGATE_DIR, "ai-home");
7279
7290
  }
7280
7291
  function getImagesDir() {
7281
- const override = String(process.env.LABGATE_IMAGES_DIR || "").trim();
7282
- if (override) {
7283
- return resolve(override.replace(/^~/, homedir3()));
7284
- }
7292
+ const envOverride = resolveImagesDirOverride(process.env.LABGATE_IMAGES_DIR);
7293
+ if (envOverride) return envOverride;
7294
+ const configOverride = resolveImagesDirOverride(loadConfig().images_dir);
7295
+ if (configOverride) return configOverride;
7285
7296
  return join3(LABGATE_DIR, "images");
7286
7297
  }
7287
7298
  function isImagesDirOverridden() {
7288
- return String(process.env.LABGATE_IMAGES_DIR || "").trim().length > 0;
7299
+ if (resolveImagesDirOverride(process.env.LABGATE_IMAGES_DIR)) return true;
7300
+ return resolveImagesDirOverride(loadConfig().images_dir) !== null;
7289
7301
  }
7290
7302
  function getEmptyDir() {
7291
7303
  return join3(LABGATE_DIR, "empty");
@@ -7335,12 +7347,104 @@ function getExplorerLockDir(experimentId) {
7335
7347
  function getExplorerWorktreesDir(experimentId) {
7336
7348
  return join3(getExplorerWorktreesRootDir(), experimentId);
7337
7349
  }
7338
- function shouldClaudeHeadlessRunWithAllowedPermissions(config2) {
7339
- return config2.headless?.claude_run_with_allowed_permissions !== false;
7350
+ function shouldClaudeHeadlessRunWithAllowedPermissions(_config) {
7351
+ return false;
7340
7352
  }
7341
7353
  function cloneConfig(config2) {
7342
7354
  return JSON.parse(JSON.stringify(config2));
7343
7355
  }
7356
+ function normalizeLegacyRuntime(rawRuntime, warnOnLegacyRuntime) {
7357
+ if (rawRuntime === "docker") {
7358
+ if (warnOnLegacyRuntime) {
7359
+ console.error('Warning: runtime "docker" is deprecated. Using "podman".');
7360
+ }
7361
+ return "podman";
7362
+ }
7363
+ if (rawRuntime === "singularity") {
7364
+ if (warnOnLegacyRuntime) {
7365
+ console.error('Warning: runtime "singularity" is deprecated. Using "apptainer".');
7366
+ }
7367
+ return "apptainer";
7368
+ }
7369
+ return rawRuntime;
7370
+ }
7371
+ function stripJsonComments(rawText) {
7372
+ return rawText.split("\n").filter((line) => !line.trimStart().startsWith("//")).join("\n");
7373
+ }
7374
+ function parseRawConfigText(rawText) {
7375
+ const stripped = stripJsonComments(rawText).trim();
7376
+ if (!stripped) return {};
7377
+ const parsed = JSON.parse(stripped);
7378
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
7379
+ throw new Error("Config root must be a JSON object");
7380
+ }
7381
+ return parsed;
7382
+ }
7383
+ function buildConfigFromRaw(rawInput, options = {}) {
7384
+ const raw = rawInput;
7385
+ const runtime = normalizeLegacyRuntime(
7386
+ raw.runtime,
7387
+ options.warnOnLegacyRuntime ?? false
7388
+ );
7389
+ return {
7390
+ runtime: runtime ?? DEFAULT_CONFIG.runtime,
7391
+ images_dir: raw.images_dir ?? DEFAULT_CONFIG.images_dir,
7392
+ image: raw.image ?? DEFAULT_CONFIG.image,
7393
+ session_timeout_hours: raw.session_timeout_hours ?? DEFAULT_CONFIG.session_timeout_hours,
7394
+ filesystem: {
7395
+ extra_paths: raw.filesystem?.extra_paths ?? [...DEFAULT_CONFIG.filesystem.extra_paths],
7396
+ blocked_patterns: raw.filesystem?.blocked_patterns ?? [...DEFAULT_CONFIG.filesystem.blocked_patterns]
7397
+ },
7398
+ datasets: Array.isArray(raw.datasets) ? raw.datasets : [],
7399
+ commands: {
7400
+ blacklist: raw.commands?.blacklist ?? [...DEFAULT_CONFIG.commands.blacklist],
7401
+ ensure_commands: raw.commands?.ensure_commands ?? [...DEFAULT_CONFIG.commands.ensure_commands ?? []]
7402
+ },
7403
+ network: {
7404
+ mode: raw.network?.mode ?? DEFAULT_CONFIG.network.mode,
7405
+ allowed_domains: raw.network?.allowed_domains ?? [...DEFAULT_CONFIG.network.allowed_domains]
7406
+ },
7407
+ slurm: {
7408
+ enabled: raw.slurm?.enabled ?? DEFAULT_CONFIG.slurm.enabled,
7409
+ poll_interval_seconds: raw.slurm?.poll_interval_seconds ?? DEFAULT_CONFIG.slurm.poll_interval_seconds,
7410
+ sacct_lookback_hours: raw.slurm?.sacct_lookback_hours ?? DEFAULT_CONFIG.slurm.sacct_lookback_hours,
7411
+ mcp_server: raw.slurm?.mcp_server ?? DEFAULT_CONFIG.slurm.mcp_server
7412
+ },
7413
+ audit: {
7414
+ enabled: raw.audit?.enabled ?? DEFAULT_CONFIG.audit.enabled,
7415
+ log_dir: raw.audit?.log_dir ?? DEFAULT_CONFIG.audit.log_dir
7416
+ },
7417
+ automation: {
7418
+ api_key: raw.automation?.api_key ?? DEFAULT_CONFIG.automation?.api_key,
7419
+ model: raw.automation?.model ?? DEFAULT_CONFIG.automation.model,
7420
+ system_prompt: raw.automation?.system_prompt ?? DEFAULT_CONFIG.automation.system_prompt,
7421
+ trigger_patterns: raw.automation?.trigger_patterns ?? [...DEFAULT_CONFIG.automation.trigger_patterns],
7422
+ context_lines: raw.automation?.context_lines ?? DEFAULT_CONFIG.automation.context_lines,
7423
+ delay_ms: raw.automation?.delay_ms ?? DEFAULT_CONFIG.automation.delay_ms,
7424
+ max_turns: raw.automation?.max_turns ?? DEFAULT_CONFIG.automation.max_turns,
7425
+ max_tokens: raw.automation?.max_tokens ?? DEFAULT_CONFIG.automation.max_tokens
7426
+ },
7427
+ headless: {
7428
+ claude_run_with_allowed_permissions: raw.headless?.claude_run_with_allowed_permissions ?? DEFAULT_CONFIG.headless?.claude_run_with_allowed_permissions ?? false,
7429
+ continuation_in_other_terminals: raw.headless?.continuation_in_other_terminals ?? DEFAULT_CONFIG.headless?.continuation_in_other_terminals ?? true,
7430
+ git_integration: raw.headless?.git_integration ?? DEFAULT_CONFIG.headless?.git_integration ?? false
7431
+ },
7432
+ plugins: sanitizePluginMap(raw.plugins)
7433
+ };
7434
+ }
7435
+ function readRawConfigFile(configPath = getConfigPath()) {
7436
+ if (!existsSync3(configPath)) return {};
7437
+ ensurePrivateFile(configPath);
7438
+ return parseRawConfigText(readFileSync3(configPath, "utf-8"));
7439
+ }
7440
+ function writeRawConfigFile(rawConfig, configPath = getConfigPath()) {
7441
+ ensurePrivateDir(dirname2(configPath));
7442
+ writeFileSync2(configPath, JSON.stringify(rawConfig, null, 2) + "\n", {
7443
+ encoding: "utf-8",
7444
+ mode: PRIVATE_FILE_MODE
7445
+ });
7446
+ ensurePrivateFile(configPath);
7447
+ }
7344
7448
  function validateConfig(config2) {
7345
7449
  const errors = [];
7346
7450
  const networkMode = config2.network?.mode;
@@ -7348,11 +7452,15 @@ function validateConfig(config2) {
7348
7452
  const blockedPatterns = config2.filesystem?.blocked_patterns;
7349
7453
  const extraPaths = config2.filesystem?.extra_paths;
7350
7454
  const blacklist = config2.commands?.blacklist;
7455
+ const ensureCommands = config2.commands?.ensure_commands;
7351
7456
  const plugins = config2.plugins;
7352
7457
  const headless = config2.headless;
7353
7458
  if (!VALID_RUNTIMES2.includes(config2.runtime)) {
7354
7459
  errors.push(`Invalid runtime: "${config2.runtime}". Must be one of: ${VALID_RUNTIMES2.join(", ")}`);
7355
7460
  }
7461
+ if (config2.images_dir !== void 0 && typeof config2.images_dir !== "string") {
7462
+ errors.push("images_dir must be a string");
7463
+ }
7356
7464
  if (!config2.image || typeof config2.image !== "string") {
7357
7465
  errors.push("image must be a non-empty string");
7358
7466
  }
@@ -7374,6 +7482,22 @@ function validateConfig(config2) {
7374
7482
  if (!Array.isArray(blacklist)) {
7375
7483
  errors.push("commands.blacklist must be an array");
7376
7484
  }
7485
+ if (ensureCommands !== void 0) {
7486
+ if (!Array.isArray(ensureCommands)) {
7487
+ errors.push("commands.ensure_commands must be an array");
7488
+ } else {
7489
+ for (let i = 0; i < ensureCommands.length; i++) {
7490
+ const command = ensureCommands[i];
7491
+ if (typeof command !== "string" || !command.trim()) {
7492
+ errors.push(`commands.ensure_commands[${i}] must be a non-empty string`);
7493
+ } else if (!/^[a-zA-Z0-9._+-]+$/.test(command)) {
7494
+ errors.push(
7495
+ `commands.ensure_commands[${i}] contains invalid characters; use letters, digits, ., _, +, - only`
7496
+ );
7497
+ }
7498
+ }
7499
+ }
7500
+ }
7377
7501
  if (plugins !== void 0) {
7378
7502
  if (!plugins || typeof plugins !== "object" || Array.isArray(plugins)) {
7379
7503
  errors.push("plugins must be an object");
@@ -7500,61 +7624,8 @@ function loadConfig() {
7500
7624
  }
7501
7625
  ensurePrivateFile(configPath);
7502
7626
  try {
7503
- const rawText = readFileSync3(configPath, "utf-8");
7504
- const stripped = rawText.split("\n").filter((line) => !line.trimStart().startsWith("//")).join("\n");
7505
- const raw = JSON.parse(stripped);
7506
- const rawRuntime = raw.runtime;
7507
- let runtime = rawRuntime;
7508
- if (rawRuntime === "docker") {
7509
- runtime = "podman";
7510
- console.error('Warning: runtime "docker" is deprecated. Using "podman".');
7511
- } else if (rawRuntime === "singularity") {
7512
- runtime = "apptainer";
7513
- console.error('Warning: runtime "singularity" is deprecated. Using "apptainer".');
7514
- }
7515
- const config2 = {
7516
- runtime: runtime ?? DEFAULT_CONFIG.runtime,
7517
- image: raw.image ?? DEFAULT_CONFIG.image,
7518
- session_timeout_hours: raw.session_timeout_hours ?? DEFAULT_CONFIG.session_timeout_hours,
7519
- filesystem: {
7520
- extra_paths: raw.filesystem?.extra_paths ?? [...DEFAULT_CONFIG.filesystem.extra_paths],
7521
- blocked_patterns: raw.filesystem?.blocked_patterns ?? [...DEFAULT_CONFIG.filesystem.blocked_patterns]
7522
- },
7523
- datasets: Array.isArray(raw.datasets) ? raw.datasets : [],
7524
- commands: {
7525
- blacklist: raw.commands?.blacklist ?? [...DEFAULT_CONFIG.commands.blacklist]
7526
- },
7527
- network: {
7528
- mode: raw.network?.mode ?? DEFAULT_CONFIG.network.mode,
7529
- allowed_domains: raw.network?.allowed_domains ?? [...DEFAULT_CONFIG.network.allowed_domains]
7530
- },
7531
- slurm: {
7532
- enabled: raw.slurm?.enabled ?? DEFAULT_CONFIG.slurm.enabled,
7533
- poll_interval_seconds: raw.slurm?.poll_interval_seconds ?? DEFAULT_CONFIG.slurm.poll_interval_seconds,
7534
- sacct_lookback_hours: raw.slurm?.sacct_lookback_hours ?? DEFAULT_CONFIG.slurm.sacct_lookback_hours,
7535
- mcp_server: raw.slurm?.mcp_server ?? DEFAULT_CONFIG.slurm.mcp_server
7536
- },
7537
- audit: {
7538
- enabled: raw.audit?.enabled ?? DEFAULT_CONFIG.audit.enabled,
7539
- log_dir: raw.audit?.log_dir ?? DEFAULT_CONFIG.audit.log_dir
7540
- },
7541
- automation: {
7542
- api_key: raw.automation?.api_key ?? DEFAULT_CONFIG.automation?.api_key,
7543
- model: raw.automation?.model ?? DEFAULT_CONFIG.automation.model,
7544
- system_prompt: raw.automation?.system_prompt ?? DEFAULT_CONFIG.automation.system_prompt,
7545
- trigger_patterns: raw.automation?.trigger_patterns ?? [...DEFAULT_CONFIG.automation.trigger_patterns],
7546
- context_lines: raw.automation?.context_lines ?? DEFAULT_CONFIG.automation.context_lines,
7547
- delay_ms: raw.automation?.delay_ms ?? DEFAULT_CONFIG.automation.delay_ms,
7548
- max_turns: raw.automation?.max_turns ?? DEFAULT_CONFIG.automation.max_turns,
7549
- max_tokens: raw.automation?.max_tokens ?? DEFAULT_CONFIG.automation.max_tokens
7550
- },
7551
- headless: {
7552
- claude_run_with_allowed_permissions: raw.headless?.claude_run_with_allowed_permissions ?? DEFAULT_CONFIG.headless?.claude_run_with_allowed_permissions ?? true,
7553
- continuation_in_other_terminals: raw.headless?.continuation_in_other_terminals ?? DEFAULT_CONFIG.headless?.continuation_in_other_terminals ?? true,
7554
- git_integration: raw.headless?.git_integration ?? DEFAULT_CONFIG.headless?.git_integration ?? false
7555
- },
7556
- plugins: sanitizePluginMap(raw.plugins)
7557
- };
7627
+ const raw = readRawConfigFile(configPath);
7628
+ const config2 = buildConfigFromRaw(raw, { warnOnLegacyRuntime: true });
7558
7629
  const errors = validateConfig(config2);
7559
7630
  if (errors.length > 0) {
7560
7631
  console.error(`Warning: invalid config in ${configPath}:`);
@@ -7631,6 +7702,7 @@ var init_config = __esm({
7631
7702
  "use strict";
7632
7703
  DEFAULT_CONFIG = {
7633
7704
  runtime: "auto",
7705
+ images_dir: "",
7634
7706
  // Default sandbox image: includes python3 + basic build tools (git/make/g++).
7635
7707
  // Note: pip/venv are not included by default in Debian; users may still need a
7636
7708
  // custom image for richer Python workflows.
@@ -7663,7 +7735,8 @@ var init_config = __esm({
7663
7735
  "mkfs",
7664
7736
  "reboot",
7665
7737
  "shutdown"
7666
- ]
7738
+ ],
7739
+ ensure_commands: ["git"]
7667
7740
  },
7668
7741
  network: {
7669
7742
  mode: "host",
@@ -7689,13 +7762,9 @@ var init_config = __esm({
7689
7762
  },
7690
7763
  plugins: {
7691
7764
  files: true,
7765
+ activity: true,
7692
7766
  datasets: false,
7693
7767
  results: false,
7694
- charting: true,
7695
- molecular: true,
7696
- genomics: true,
7697
- phylogenetics: true,
7698
- network: true,
7699
7768
  slurm: true,
7700
7769
  explorer: true,
7701
7770
  automation: false
@@ -7716,7 +7785,7 @@ var init_config = __esm({
7716
7785
  max_tokens: 1024
7717
7786
  },
7718
7787
  headless: {
7719
- claude_run_with_allowed_permissions: true,
7788
+ claude_run_with_allowed_permissions: false,
7720
7789
  continuation_in_other_terminals: true,
7721
7790
  git_integration: false
7722
7791
  }
@@ -36397,14 +36466,14 @@ var StdioServerTransport = class {
36397
36466
 
36398
36467
  // src/lib/explorer-mcp.ts
36399
36468
  init_config();
36400
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync8 } from "fs";
36469
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
36401
36470
  import { join as join11 } from "path";
36402
36471
 
36403
36472
  // src/lib/explorer-store.ts
36404
36473
  init_config();
36405
36474
  import { randomUUID } from "crypto";
36406
- import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
36407
- import { dirname as dirname2 } from "path";
36475
+ import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
36476
+ import { dirname as dirname3 } from "path";
36408
36477
 
36409
36478
  // src/lib/explorer-retention.ts
36410
36479
  var DEFAULT_RETENTION_POLICY = {
@@ -36642,7 +36711,7 @@ function mapEventRow(row) {
36642
36711
  var ExplorerStore = class {
36643
36712
  constructor(dbPath = getExplorerDbPath()) {
36644
36713
  this.dbPath = dbPath;
36645
- const dir = dirname2(dbPath);
36714
+ const dir = dirname3(dbPath);
36646
36715
  if (!existsSync4(dir)) {
36647
36716
  mkdirSync2(dir, { recursive: true, mode: 448 });
36648
36717
  }
@@ -37293,7 +37362,7 @@ var ExplorerStore = class {
37293
37362
  flushJson() {
37294
37363
  if (!this.jsonPath) return;
37295
37364
  try {
37296
- writeFileSync2(this.jsonPath, JSON.stringify(this.jsonStore, null, 2) + "\n", {
37365
+ writeFileSync3(this.jsonPath, JSON.stringify(this.jsonStore, null, 2) + "\n", {
37297
37366
  encoding: "utf-8",
37298
37367
  mode: 384
37299
37368
  });
@@ -37308,7 +37377,7 @@ var ExplorerStore = class {
37308
37377
 
37309
37378
  // src/lib/explorer-git.ts
37310
37379
  import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
37311
- import { dirname as dirname3, resolve as resolve2 } from "path";
37380
+ import { dirname as dirname4, resolve as resolve2 } from "path";
37312
37381
  import { execFileSync, spawnSync } from "child_process";
37313
37382
  function runGit(args, cwd) {
37314
37383
  return execFileSync("git", args, {
@@ -37353,7 +37422,7 @@ function resolveGitSha(repoPath, ref = "HEAD") {
37353
37422
  function cloneRepository(sourceRepoPath, targetRepoPath) {
37354
37423
  const source = resolve2(sourceRepoPath);
37355
37424
  const target = resolve2(targetRepoPath);
37356
- const targetParent = dirname3(target);
37425
+ const targetParent = dirname4(target);
37357
37426
  if (!existsSync5(targetParent)) {
37358
37427
  mkdirSync3(targetParent, { recursive: true, mode: 448 });
37359
37428
  }
@@ -37369,7 +37438,7 @@ function createRunWorkspace(repoPath, worktreePath, parentSha, experimentId, run
37369
37438
  const expPart = sanitizeBranchPart(experimentId) || "exp";
37370
37439
  const runPart = sanitizeBranchPart(runId) || "run";
37371
37440
  const branchName = `explorer/${expPart}/${runPart}`;
37372
- const parentDir = dirname3(fullWorktree);
37441
+ const parentDir = dirname4(fullWorktree);
37373
37442
  if (!existsSync5(parentDir)) {
37374
37443
  mkdirSync3(parentDir, { recursive: true, mode: 448 });
37375
37444
  }
@@ -37438,7 +37507,7 @@ function cleanupWorkspace(repoPath, worktreePath, branchName) {
37438
37507
  }
37439
37508
 
37440
37509
  // src/lib/explorer-eval.ts
37441
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "fs";
37510
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
37442
37511
  import { join as join4, resolve as resolve3 } from "path";
37443
37512
  import { spawnSync as spawnSync2 } from "child_process";
37444
37513
  function parseLastNonEmptyLine(text) {
@@ -37453,13 +37522,13 @@ function shellForCurrentPlatform() {
37453
37522
  if (process.platform === "win32") {
37454
37523
  return process.env.COMSPEC || "cmd.exe";
37455
37524
  }
37456
- return process.env.SHELL || "/bin/sh";
37525
+ return "/bin/sh";
37457
37526
  }
37458
37527
  function shellArgsForCommand(command) {
37459
37528
  if (process.platform === "win32") {
37460
37529
  return ["/d", "/s", "/c", command];
37461
37530
  }
37462
- return ["-lc", command];
37531
+ return ["-c", command];
37463
37532
  }
37464
37533
  function runEvaluation(opts) {
37465
37534
  const worktreePath = resolve3(opts.worktree_path);
@@ -37482,8 +37551,8 @@ function runEvaluation(opts) {
37482
37551
  const finishedAt = new Date(started + runtimeMs).toISOString();
37483
37552
  const stdout = String(result.stdout || "");
37484
37553
  const stderr = String(result.stderr || "");
37485
- writeFileSync3(stdoutPath, stdout, "utf-8");
37486
- writeFileSync3(stderrPath, stderr, "utf-8");
37554
+ writeFileSync4(stdoutPath, stdout, "utf-8");
37555
+ writeFileSync4(stderrPath, stderr, "utf-8");
37487
37556
  if (result.error && (result.error.code === "ETIMEDOUT" || result.signal === "SIGTERM")) {
37488
37557
  return {
37489
37558
  status: "timeout",
@@ -37597,13 +37666,13 @@ import { join as join10, resolve as resolve7 } from "path";
37597
37666
 
37598
37667
  // src/lib/explorer-autopilot.ts
37599
37668
  init_config();
37600
- import { existsSync as existsSync7, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
37669
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
37601
37670
  import { join as join7, resolve as resolve5 } from "path";
37602
37671
  import { spawnSync as spawnSync4 } from "child_process";
37603
37672
 
37604
37673
  // src/lib/explorer-lock.ts
37605
37674
  init_config();
37606
- import { closeSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
37675
+ import { closeSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
37607
37676
  import { join as join5 } from "path";
37608
37677
  function parseLockPid(raw) {
37609
37678
  const firstLine = raw.split(/\r?\n/, 1)[0] || "";
@@ -37654,7 +37723,7 @@ function acquireExperimentLock(experimentId, lockName = "autopilot.lock") {
37654
37723
  return null;
37655
37724
  }
37656
37725
  try {
37657
- writeFileSync4(fd, `${process.pid}
37726
+ writeFileSync5(fd, `${process.pid}
37658
37727
  `, { encoding: "utf-8" });
37659
37728
  } catch (err) {
37660
37729
  try {
@@ -37684,12 +37753,13 @@ function acquireExperimentLock(experimentId, lockName = "autopilot.lock") {
37684
37753
 
37685
37754
  // src/lib/explorer-claude.ts
37686
37755
  init_config();
37687
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
37688
- import { basename, dirname as dirname4, join as join6, resolve as resolve4 } from "path";
37756
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
37757
+ import { basename, dirname as dirname5, join as join6, resolve as resolve4 } from "path";
37689
37758
  import { homedir as homedir4 } from "os";
37690
37759
  import { spawnSync as spawnSync3 } from "child_process";
37691
37760
 
37692
37761
  // src/lib/container.ts
37762
+ import { randomBytes as randomBytes2, createHash } from "crypto";
37693
37763
  var import_fast_glob = __toESM(require_out4());
37694
37764
  init_config();
37695
37765
 
@@ -37918,9 +37988,16 @@ init_config();
37918
37988
  // src/lib/slurm-cli-passthrough.ts
37919
37989
  init_config();
37920
37990
 
37991
+ // src/lib/startup-stage-lock.ts
37992
+ var DEFAULT_STAGE_LOCK_WAIT_TIMEOUT_MS = 5 * 60 * 1e3;
37993
+ var DEFAULT_STAGE_LOCK_STALE_AFTER_MS = 15 * 60 * 1e3;
37994
+
37921
37995
  // src/lib/container.ts
37922
37996
  function imageToSifName(image) {
37923
- return image.replace(/[/:]/g, "_") + ".sif";
37997
+ const normalized = String(image || "").trim();
37998
+ const readable = normalized.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 160) || "image";
37999
+ const hash2 = createHash("sha256").update(normalized).digest("hex").slice(0, 12);
38000
+ return `${readable}-${hash2}.sif`;
37924
38001
  }
37925
38002
 
37926
38003
  // src/lib/explorer-claude.ts
@@ -38080,12 +38157,37 @@ function runApptainer(args, cwd, timeoutSec) {
38080
38157
  function ensureDisplayDbFile() {
38081
38158
  const displayDbPath = getDisplayDbPath();
38082
38159
  if (existsSync6(displayDbPath)) return;
38083
- ensurePrivateDir(dirname4(displayDbPath));
38084
- writeFileSync5(displayDbPath, JSON.stringify({ version: 1, events: [] }, null, 2) + "\n", {
38160
+ ensurePrivateDir(dirname5(displayDbPath));
38161
+ writeFileSync6(displayDbPath, JSON.stringify({ version: 1, events: [] }, null, 2) + "\n", {
38085
38162
  encoding: "utf-8",
38086
38163
  mode: PRIVATE_FILE_MODE
38087
38164
  });
38088
38165
  }
38166
+ function buildClaudeHeadlessAddDirArgs(config2) {
38167
+ const dirs = /* @__PURE__ */ new Set();
38168
+ let hasExtraMounts = false;
38169
+ let hasDatasets = false;
38170
+ for (const mount of config2.filesystem.extra_paths || []) {
38171
+ if (!mount || typeof mount.path !== "string") continue;
38172
+ const resolved = mount.path.replace(/^~/, homedir4());
38173
+ const target = `/mnt/${basename(resolved)}`;
38174
+ hasExtraMounts = true;
38175
+ dirs.add(target);
38176
+ }
38177
+ for (const ds of config2.datasets || []) {
38178
+ if (!ds || typeof ds.name !== "string") continue;
38179
+ const name = ds.name.trim();
38180
+ if (!name) continue;
38181
+ hasDatasets = true;
38182
+ dirs.add(`/datasets/${name}`);
38183
+ }
38184
+ if (hasExtraMounts) dirs.add("/mnt");
38185
+ if (hasDatasets) dirs.add("/datasets");
38186
+ return Array.from(dirs).flatMap((dir) => ["--add-dir", dir]);
38187
+ }
38188
+ function buildClaudeHeadlessAllowedToolsArgs() {
38189
+ return ["--allowed-tools", "Glob,Read,Grep,WebFetch"];
38190
+ }
38089
38191
  function buildContainerPrefixArgs(config2, worktreePath, sifPath) {
38090
38192
  const sandboxHome = getSandboxHome();
38091
38193
  const args = [
@@ -38181,7 +38283,8 @@ function normalizeTimeout(raw) {
38181
38283
  return Math.max(30, Math.min(4 * 60 * 60, Math.floor(parsed)));
38182
38284
  }
38183
38285
  function runClaudeHeadlessInWorktree(input) {
38184
- const worktreePath = resolve4(String(input.worktree_path || "").trim());
38286
+ const worktreePathRaw = String(input.worktree_path || "").trim();
38287
+ const worktreePath = worktreePathRaw ? resolve4(worktreePathRaw) : "";
38185
38288
  const prompt = String(input.prompt || "").trim();
38186
38289
  const resumeSessionId = String(input.resume_session_id || "").trim();
38187
38290
  const timeoutSec = normalizeTimeout(input.timeout_sec);
@@ -38210,6 +38313,8 @@ function runClaudeHeadlessInWorktree(input) {
38210
38313
  "--output-format",
38211
38314
  "stream-json",
38212
38315
  "--include-partial-messages",
38316
+ ...buildClaudeHeadlessAddDirArgs(config2),
38317
+ ...buildClaudeHeadlessAllowedToolsArgs(),
38213
38318
  ...shouldClaudeHeadlessRunWithAllowedPermissions(config2) ? ["--dangerously-skip-permissions"] : [],
38214
38319
  ...resumeSessionId ? ["--resume", resumeSessionId] : [],
38215
38320
  prompt
@@ -38517,13 +38622,13 @@ function writeAgentDebugArtifacts(artifactDir, debug) {
38517
38622
  if (!debug) return;
38518
38623
  try {
38519
38624
  if (debug.activity_log) {
38520
- writeFileSync6(join7(artifactDir, "agent.log"), debug.activity_log, "utf-8");
38625
+ writeFileSync7(join7(artifactDir, "agent.log"), debug.activity_log, "utf-8");
38521
38626
  }
38522
38627
  if (debug.stdout) {
38523
- writeFileSync6(join7(artifactDir, "claude.stdout.log"), debug.stdout, "utf-8");
38628
+ writeFileSync7(join7(artifactDir, "claude.stdout.log"), debug.stdout, "utf-8");
38524
38629
  }
38525
38630
  if (debug.stderr) {
38526
- writeFileSync6(join7(artifactDir, "claude.stderr.log"), debug.stderr, "utf-8");
38631
+ writeFileSync7(join7(artifactDir, "claude.stderr.log"), debug.stderr, "utf-8");
38527
38632
  }
38528
38633
  } catch {
38529
38634
  }
@@ -38570,7 +38675,7 @@ function recoverStaleActiveRuns(experimentId, store, staleAfterSec) {
38570
38675
  ].join("\n");
38571
38676
  try {
38572
38677
  mkdirSync7(run.artifact_dir, { recursive: true, mode: 448 });
38573
- writeFileSync6(join7(run.artifact_dir, "summary.md"), summary, "utf-8");
38678
+ writeFileSync7(join7(run.artifact_dir, "summary.md"), summary, "utf-8");
38574
38679
  } catch {
38575
38680
  }
38576
38681
  const updated = store.recordRunResult(run.id, {
@@ -38694,7 +38799,7 @@ function autopilotTick(input) {
38694
38799
  writeAgentDebugArtifacts(artifactDir, agentResult.debug);
38695
38800
  if (!agentResult.ok) {
38696
38801
  const summary2 = buildSummary(run.id, parentScore, "failed_build", null, "", agentResult.error, agentResult.error);
38697
- writeFileSync6(join7(artifactDir, "summary.md"), summary2, "utf-8");
38802
+ writeFileSync7(join7(artifactDir, "summary.md"), summary2, "utf-8");
38698
38803
  store.recordRunResult(run.id, {
38699
38804
  status: "failed_build",
38700
38805
  metrics_json: JSON.stringify(agentResult.metrics || {}),
@@ -38710,7 +38815,7 @@ function autopilotTick(input) {
38710
38815
  }
38711
38816
  store.recordRunCommit(run.id, agentResult.commit_sha);
38712
38817
  const diffText = getDiff(experiment.repo_path, parent.parent_commit_sha, agentResult.commit_sha);
38713
- writeFileSync6(join7(artifactDir, "diff.patch"), diffText, "utf-8");
38818
+ writeFileSync7(join7(artifactDir, "diff.patch"), diffText, "utf-8");
38714
38819
  const evalResult = runEvaluation({
38715
38820
  worktree_path: workspace.worktree_path,
38716
38821
  eval_command: experiment.eval_command,
@@ -38731,7 +38836,7 @@ function autopilotTick(input) {
38731
38836
  stderr_path: evalResult.stderr_path,
38732
38837
  error: evalResult.error || null
38733
38838
  };
38734
- writeFileSync6(join7(artifactDir, "eval.json"), JSON.stringify(evalPayload, null, 2) + "\n", "utf-8");
38839
+ writeFileSync7(join7(artifactDir, "eval.json"), JSON.stringify(evalPayload, null, 2) + "\n", "utf-8");
38735
38840
  const runStatus = evalResult.status;
38736
38841
  const runScore = Number.isFinite(evalResult.score) ? Number(evalResult.score) : null;
38737
38842
  const mergedMetrics = {
@@ -38747,7 +38852,7 @@ function autopilotTick(input) {
38747
38852
  evalResult.error,
38748
38853
  agentResult.note
38749
38854
  );
38750
- writeFileSync6(join7(artifactDir, "summary.md"), summary, "utf-8");
38855
+ writeFileSync7(join7(artifactDir, "summary.md"), summary, "utf-8");
38751
38856
  store.recordRunResult(run.id, {
38752
38857
  status: runStatus,
38753
38858
  score: runScore,
@@ -38789,7 +38894,7 @@ function autopilotTick(input) {
38789
38894
  if (inFlightArtifactDir) {
38790
38895
  try {
38791
38896
  mkdirSync7(inFlightArtifactDir, { recursive: true, mode: 448 });
38792
- writeFileSync6(join7(inFlightArtifactDir, "summary.md"), summary, "utf-8");
38897
+ writeFileSync7(join7(inFlightArtifactDir, "summary.md"), summary, "utf-8");
38793
38898
  } catch {
38794
38899
  }
38795
38900
  }
@@ -39090,7 +39195,7 @@ function experimentGc(experimentId, store, options) {
39090
39195
  // src/lib/explorer-compare.ts
39091
39196
  init_config();
39092
39197
  import { execFileSync as execFileSync3 } from "child_process";
39093
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
39198
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
39094
39199
  import { join as join9 } from "path";
39095
39200
  function runGit2(repoPath, args) {
39096
39201
  return String(execFileSync3("git", ["-C", repoPath, ...args], {
@@ -39215,7 +39320,7 @@ function runCompare(input, store) {
39215
39320
  const safeB = (bRunId || "base").replace(/[^a-zA-Z0-9._-]+/g, "_");
39216
39321
  const target = join9(compareDir, `${aRun.id}_vs_${safeB}.patch`);
39217
39322
  const patchText = runGit2(experiment.repo_path, ["diff", "--binary", `${bCommitSha}..${aRun.commit_sha}`]);
39218
- writeFileSync7(target, patchText, "utf-8");
39323
+ writeFileSync8(target, patchText, "utf-8");
39219
39324
  patchPath = target;
39220
39325
  }
39221
39326
  const aMetrics = parseMetrics(aRun.metrics_json);
@@ -39551,7 +39656,7 @@ function runExperimentBaselineInit(experimentId, store) {
39551
39656
  error: baseline.error || null
39552
39657
  };
39553
39658
  mkdirSync10(artifactDir, { recursive: true, mode: 448 });
39554
- writeFileSync8(join11(artifactDir, "eval.json"), JSON.stringify(evalPayload, null, 2) + "\n", "utf-8");
39659
+ writeFileSync9(join11(artifactDir, "eval.json"), JSON.stringify(evalPayload, null, 2) + "\n", "utf-8");
39555
39660
  store.createEvent(experiment.id, "note", {
39556
39661
  message: "experiment initialized with baseline evaluation",
39557
39662
  status: baseline.status,