labgate 0.5.37 → 0.5.38

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.
@@ -2979,7 +2979,7 @@ var require_compile = __commonJS({
2979
2979
  const schOrFunc = root.refs[ref];
2980
2980
  if (schOrFunc)
2981
2981
  return schOrFunc;
2982
- let _sch = resolve.call(this, root, ref);
2982
+ let _sch = resolve2.call(this, root, ref);
2983
2983
  if (_sch === void 0) {
2984
2984
  const schema = (_a2 = root.localRefs) === null || _a2 === void 0 ? void 0 : _a2[ref];
2985
2985
  const { schemaId } = this.opts;
@@ -3006,7 +3006,7 @@ var require_compile = __commonJS({
3006
3006
  function sameSchemaEnv(s1, s2) {
3007
3007
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3008
3008
  }
3009
- function resolve(root, ref) {
3009
+ function resolve2(root, ref) {
3010
3010
  let sch;
3011
3011
  while (typeof (sch = this.refs[ref]) == "string")
3012
3012
  ref = sch;
@@ -3581,55 +3581,55 @@ var require_fast_uri = __commonJS({
3581
3581
  }
3582
3582
  return uri;
3583
3583
  }
3584
- function resolve(baseURI, relativeURI, options) {
3584
+ function resolve2(baseURI, relativeURI, options) {
3585
3585
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3586
3586
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
3587
3587
  schemelessOptions.skipEscape = true;
3588
3588
  return serialize(resolved, schemelessOptions);
3589
3589
  }
3590
- function resolveComponent(base, relative, options, skipNormalization) {
3590
+ function resolveComponent(base, relative2, options, skipNormalization) {
3591
3591
  const target = {};
3592
3592
  if (!skipNormalization) {
3593
3593
  base = parse3(serialize(base, options), options);
3594
- relative = parse3(serialize(relative, options), options);
3594
+ relative2 = parse3(serialize(relative2, options), options);
3595
3595
  }
3596
3596
  options = options || {};
3597
- if (!options.tolerant && relative.scheme) {
3598
- target.scheme = relative.scheme;
3599
- target.userinfo = relative.userinfo;
3600
- target.host = relative.host;
3601
- target.port = relative.port;
3602
- target.path = removeDotSegments(relative.path || "");
3603
- target.query = relative.query;
3597
+ if (!options.tolerant && relative2.scheme) {
3598
+ target.scheme = relative2.scheme;
3599
+ target.userinfo = relative2.userinfo;
3600
+ target.host = relative2.host;
3601
+ target.port = relative2.port;
3602
+ target.path = removeDotSegments(relative2.path || "");
3603
+ target.query = relative2.query;
3604
3604
  } else {
3605
- if (relative.userinfo !== void 0 || relative.host !== void 0 || relative.port !== void 0) {
3606
- target.userinfo = relative.userinfo;
3607
- target.host = relative.host;
3608
- target.port = relative.port;
3609
- target.path = removeDotSegments(relative.path || "");
3610
- target.query = relative.query;
3605
+ if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
3606
+ target.userinfo = relative2.userinfo;
3607
+ target.host = relative2.host;
3608
+ target.port = relative2.port;
3609
+ target.path = removeDotSegments(relative2.path || "");
3610
+ target.query = relative2.query;
3611
3611
  } else {
3612
- if (!relative.path) {
3612
+ if (!relative2.path) {
3613
3613
  target.path = base.path;
3614
- if (relative.query !== void 0) {
3615
- target.query = relative.query;
3614
+ if (relative2.query !== void 0) {
3615
+ target.query = relative2.query;
3616
3616
  } else {
3617
3617
  target.query = base.query;
3618
3618
  }
3619
3619
  } else {
3620
- if (relative.path[0] === "/") {
3621
- target.path = removeDotSegments(relative.path);
3620
+ if (relative2.path[0] === "/") {
3621
+ target.path = removeDotSegments(relative2.path);
3622
3622
  } else {
3623
3623
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
3624
- target.path = "/" + relative.path;
3624
+ target.path = "/" + relative2.path;
3625
3625
  } else if (!base.path) {
3626
- target.path = relative.path;
3626
+ target.path = relative2.path;
3627
3627
  } else {
3628
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
3628
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
3629
3629
  }
3630
3630
  target.path = removeDotSegments(target.path);
3631
3631
  }
3632
- target.query = relative.query;
3632
+ target.query = relative2.query;
3633
3633
  }
3634
3634
  target.userinfo = base.userinfo;
3635
3635
  target.host = base.host;
@@ -3637,7 +3637,7 @@ var require_fast_uri = __commonJS({
3637
3637
  }
3638
3638
  target.scheme = base.scheme;
3639
3639
  }
3640
- target.fragment = relative.fragment;
3640
+ target.fragment = relative2.fragment;
3641
3641
  return target;
3642
3642
  }
3643
3643
  function equal(uriA, uriB, options) {
@@ -3808,7 +3808,7 @@ var require_fast_uri = __commonJS({
3808
3808
  var fastUri = {
3809
3809
  SCHEMES,
3810
3810
  normalize,
3811
- resolve,
3811
+ resolve: resolve2,
3812
3812
  resolveComponent,
3813
3813
  equal,
3814
3814
  serialize,
@@ -27964,7 +27964,7 @@ var Protocol = class {
27964
27964
  return;
27965
27965
  }
27966
27966
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
27967
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
27967
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
27968
27968
  options?.signal?.throwIfAborted();
27969
27969
  }
27970
27970
  } catch (error48) {
@@ -27981,7 +27981,7 @@ var Protocol = class {
27981
27981
  */
27982
27982
  request(request, resultSchema, options) {
27983
27983
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
27984
- return new Promise((resolve, reject) => {
27984
+ return new Promise((resolve2, reject) => {
27985
27985
  const earlyReject = (error48) => {
27986
27986
  reject(error48);
27987
27987
  };
@@ -28059,7 +28059,7 @@ var Protocol = class {
28059
28059
  if (!parseResult.success) {
28060
28060
  reject(parseResult.error);
28061
28061
  } else {
28062
- resolve(parseResult.data);
28062
+ resolve2(parseResult.data);
28063
28063
  }
28064
28064
  } catch (error48) {
28065
28065
  reject(error48);
@@ -28320,12 +28320,12 @@ var Protocol = class {
28320
28320
  }
28321
28321
  } catch {
28322
28322
  }
28323
- return new Promise((resolve, reject) => {
28323
+ return new Promise((resolve2, reject) => {
28324
28324
  if (signal.aborted) {
28325
28325
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
28326
28326
  return;
28327
28327
  }
28328
- const timeoutId = setTimeout(resolve, interval);
28328
+ const timeoutId = setTimeout(resolve2, interval);
28329
28329
  signal.addEventListener("abort", () => {
28330
28330
  clearTimeout(timeoutId);
28331
28331
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -29284,7 +29284,7 @@ var McpServer = class {
29284
29284
  let task = createTaskResult.task;
29285
29285
  const pollInterval = task.pollInterval ?? 5e3;
29286
29286
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
29287
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
29287
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
29288
29288
  const updatedTask = await extra.taskStore.getTask(taskId);
29289
29289
  if (!updatedTask) {
29290
29290
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -29927,17 +29927,24 @@ var StdioServerTransport = class {
29927
29927
  this.onclose?.();
29928
29928
  }
29929
29929
  send(message) {
29930
- return new Promise((resolve) => {
29930
+ return new Promise((resolve2) => {
29931
29931
  const json2 = serializeMessage(message);
29932
29932
  if (this._stdout.write(json2)) {
29933
- resolve();
29933
+ resolve2();
29934
29934
  } else {
29935
- this._stdout.once("drain", resolve);
29935
+ this._stdout.once("drain", resolve2);
29936
29936
  }
29937
29937
  });
29938
29938
  }
29939
29939
  };
29940
29940
 
29941
+ // src/lib/results-mcp.ts
29942
+ import { chmodSync as chmodSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
29943
+ import { dirname as dirname2, isAbsolute, relative, resolve } from "path";
29944
+ import { createHash } from "crypto";
29945
+ import { execFile } from "child_process";
29946
+ import { promisify } from "util";
29947
+
29941
29948
  // src/lib/results-store.ts
29942
29949
  import { randomUUID } from "crypto";
29943
29950
  import { existsSync as existsSync2, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
@@ -29947,6 +29954,100 @@ import { dirname, join as join2 } from "path";
29947
29954
  import { readFileSync, existsSync, chmodSync, mkdirSync } from "fs";
29948
29955
  import { join } from "path";
29949
29956
  import { homedir } from "os";
29957
+ var DEFAULT_CONFIG = {
29958
+ runtime: "auto",
29959
+ // Default sandbox image: includes python3 + basic build tools (git/make/g++).
29960
+ // Note: pip/venv are not included by default in Debian; users may still need a
29961
+ // custom image for richer Python workflows.
29962
+ image: "docker.io/library/node:20-bookworm",
29963
+ session_timeout_hours: 8,
29964
+ filesystem: {
29965
+ extra_paths: [],
29966
+ blocked_patterns: [
29967
+ "**/.ssh",
29968
+ "**/.gnupg",
29969
+ "**/.aws",
29970
+ "**/.config/gcloud",
29971
+ "**/.azure",
29972
+ "**/.env",
29973
+ "**/.netrc",
29974
+ "**/.git-credentials",
29975
+ "**/*.pem",
29976
+ "**/*.key",
29977
+ "**/id_rsa*",
29978
+ "**/id_ed25519*",
29979
+ "**/credentials*",
29980
+ "**/secrets*"
29981
+ ]
29982
+ },
29983
+ datasets: [],
29984
+ commands: {
29985
+ blacklist: [
29986
+ "mount",
29987
+ "umount",
29988
+ "mkfs",
29989
+ "reboot",
29990
+ "shutdown"
29991
+ ]
29992
+ },
29993
+ network: {
29994
+ mode: "host",
29995
+ allowed_domains: [
29996
+ "api.anthropic.com",
29997
+ "api.openai.com",
29998
+ "pypi.org",
29999
+ "files.pythonhosted.org",
30000
+ "conda.anaconda.org",
30001
+ "registry.npmjs.org",
30002
+ "github.com"
30003
+ ]
30004
+ },
30005
+ slurm: {
30006
+ enabled: true,
30007
+ poll_interval_seconds: 5,
30008
+ sacct_lookback_hours: 24,
30009
+ mcp_server: true
30010
+ },
30011
+ audit: {
30012
+ enabled: true,
30013
+ log_dir: "~/.labgate/logs"
30014
+ },
30015
+ plugins: {
30016
+ files: true,
30017
+ datasets: false,
30018
+ results: false,
30019
+ charting: true,
30020
+ molecular: true,
30021
+ genomics: true,
30022
+ phylogenetics: true,
30023
+ network: true,
30024
+ slurm: true,
30025
+ explorer: true,
30026
+ automation: false
30027
+ },
30028
+ automation: {
30029
+ model: "claude-sonnet-4-5-20250929",
30030
+ system_prompt: "You are an automation assistant helping guide a coding agent. When the agent asks a question or waits for input, provide a concise, helpful response based on the context.",
30031
+ trigger_patterns: [
30032
+ "\\?\\s*$",
30033
+ "^>\\s*$",
30034
+ "Do you want to proceed",
30035
+ "Press Enter to continue",
30036
+ "Y/n\\]?\\s*$"
30037
+ ],
30038
+ context_lines: 100,
30039
+ delay_ms: 3e3,
30040
+ max_turns: 20,
30041
+ max_tokens: 1024
30042
+ },
30043
+ headless: {
30044
+ claude_run_with_allowed_permissions: true,
30045
+ continuation_in_other_terminals: true,
30046
+ git_integration: false
30047
+ }
30048
+ };
30049
+ var KNOWN_PLUGIN_IDS = Object.freeze(Object.keys(DEFAULT_CONFIG.plugins));
30050
+ var KNOWN_PLUGIN_ID_SET = new Set(KNOWN_PLUGIN_IDS);
29950
30051
  var LABGATE_DIR = process.env.LABGATE_DIR ?? join(homedir(), ".labgate");
29951
30052
  var PRIVATE_DIR_MODE = 448;
29952
30053
  var PRIVATE_FILE_MODE = 384;
@@ -29995,6 +30096,10 @@ function sanitizeSource(value) {
29995
30096
  if (!/^[a-z0-9._-]+$/.test(source)) return "unknown";
29996
30097
  return source;
29997
30098
  }
30099
+ function sanitizeBoolean(value, fallback = false) {
30100
+ if (typeof value === "boolean") return value;
30101
+ return fallback;
30102
+ }
29998
30103
  function sanitizeStringArray(value, maxItemLen, maxItems) {
29999
30104
  if (!Array.isArray(value)) return [];
30000
30105
  const out = [];
@@ -30023,6 +30128,139 @@ function sanitizeMetadata(value) {
30023
30128
  }
30024
30129
  return Object.keys(out).length > 0 ? out : null;
30025
30130
  }
30131
+ function sanitizePositiveInt(value, fallback, min, max) {
30132
+ if (!Number.isFinite(value)) return fallback;
30133
+ const intVal = Math.floor(value);
30134
+ return Math.min(max, Math.max(min, intVal));
30135
+ }
30136
+ function sanitizeScriptKind(value) {
30137
+ const normalized = sanitizeString(value, 16).toLowerCase();
30138
+ if (normalized === "bash") return "bash";
30139
+ if (normalized === "python") return "python";
30140
+ if (normalized === "slurm") return "slurm";
30141
+ return "other";
30142
+ }
30143
+ function sanitizeReproStrategy(value) {
30144
+ const normalized = sanitizeString(value, 16).toLowerCase();
30145
+ if (normalized === "precomputed") return "precomputed";
30146
+ return "run";
30147
+ }
30148
+ function sanitizeExecutionStatus(value) {
30149
+ const normalized = sanitizeString(value, 24).toLowerCase();
30150
+ if (normalized === "succeeded") return "succeeded";
30151
+ if (normalized === "failed") return "failed";
30152
+ if (normalized === "submitted") return "submitted";
30153
+ if (normalized === "precomputed") return "precomputed";
30154
+ return "not_run";
30155
+ }
30156
+ function sanitizeReproExecution(value) {
30157
+ if (value === null || value === void 0) return null;
30158
+ if (typeof value !== "object" || Array.isArray(value)) return null;
30159
+ const src = value;
30160
+ return {
30161
+ status: sanitizeExecutionStatus(src.status),
30162
+ command: sanitizeNullableString(src.command, 4096),
30163
+ ran_at: sanitizeNullableString(src.ran_at, 64),
30164
+ duration_ms: src.duration_ms === null || src.duration_ms === void 0 ? null : sanitizePositiveInt(src.duration_ms, 0, 0, 7 * 24 * 60 * 60 * 1e3),
30165
+ exit_code: src.exit_code === null || src.exit_code === void 0 ? null : sanitizePositiveInt(src.exit_code, 0, 0, 65535),
30166
+ signal: sanitizeNullableString(src.signal, 64),
30167
+ stdout_tail: sanitizeNullableString(src.stdout_tail, 2e4),
30168
+ stderr_tail: sanitizeNullableString(src.stderr_tail, 2e4),
30169
+ slurm_job_id: sanitizeNullableString(src.slurm_job_id, 128)
30170
+ };
30171
+ }
30172
+ function sanitizeReproducibility(value) {
30173
+ if (value === null || value === void 0) return null;
30174
+ if (typeof value !== "object" || Array.isArray(value)) return null;
30175
+ const src = value;
30176
+ return {
30177
+ script_path: sanitizeNullableString(src.script_path, 4096),
30178
+ script_kind: sanitizeScriptKind(src.script_kind),
30179
+ script_sha256: sanitizeNullableString(src.script_sha256, 128),
30180
+ input_files: sanitizeStringArray(src.input_files, 4096, 500),
30181
+ runtime: sanitizeNullableString(src.runtime, 2048),
30182
+ requirements: sanitizeStringArray(src.requirements, 512, 200),
30183
+ strategy: sanitizeReproStrategy(src.strategy),
30184
+ precomputed_reason: sanitizeNullableString(src.precomputed_reason, 4e3),
30185
+ execution: sanitizeReproExecution(src.execution)
30186
+ };
30187
+ }
30188
+ function mergeReproExecutionPatch(current, patch) {
30189
+ if (patch === void 0) return current;
30190
+ if (patch === null) return null;
30191
+ if (typeof patch !== "object" || Array.isArray(patch)) return current;
30192
+ const src = patch;
30193
+ const merged = { ...current || {} };
30194
+ const fields = [
30195
+ "status",
30196
+ "command",
30197
+ "ran_at",
30198
+ "duration_ms",
30199
+ "exit_code",
30200
+ "signal",
30201
+ "stdout_tail",
30202
+ "stderr_tail",
30203
+ "slurm_job_id"
30204
+ ];
30205
+ for (const field of fields) {
30206
+ if (Object.prototype.hasOwnProperty.call(src, field)) {
30207
+ merged[field] = src[field];
30208
+ }
30209
+ }
30210
+ return sanitizeReproExecution(merged);
30211
+ }
30212
+ function mergeReproducibilityPatch(current, patch) {
30213
+ if (patch === void 0) return current;
30214
+ if (patch === null) return null;
30215
+ if (typeof patch !== "object" || Array.isArray(patch)) return current;
30216
+ const src = patch;
30217
+ const merged = { ...current || {} };
30218
+ const fields = [
30219
+ "script_path",
30220
+ "script_kind",
30221
+ "script_sha256",
30222
+ "input_files",
30223
+ "runtime",
30224
+ "requirements",
30225
+ "strategy",
30226
+ "precomputed_reason"
30227
+ ];
30228
+ for (const field of fields) {
30229
+ if (Object.prototype.hasOwnProperty.call(src, field)) {
30230
+ merged[field] = src[field];
30231
+ }
30232
+ }
30233
+ if (Object.prototype.hasOwnProperty.call(src, "execution")) {
30234
+ merged.execution = mergeReproExecutionPatch(current?.execution || null, src.execution);
30235
+ } else if (current?.execution !== void 0) {
30236
+ merged.execution = current.execution;
30237
+ }
30238
+ return sanitizeReproducibility(merged);
30239
+ }
30240
+ function normalizeLineageId(value, fallback) {
30241
+ const lineage = sanitizeString(value, 128);
30242
+ return lineage || fallback;
30243
+ }
30244
+ function sanitizeDerivedIdComponent(value) {
30245
+ const cleaned = value.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^-/, "").replace(/-$/, "");
30246
+ return cleaned || "result";
30247
+ }
30248
+ function formatVersionLabel(version2) {
30249
+ return `v${String(version2).padStart(2, "0")}`;
30250
+ }
30251
+ function deriveVersionedResultId(lineageId, version2, existing) {
30252
+ const base = sanitizeDerivedIdComponent(sanitizeString(lineageId, 112) || "result");
30253
+ const stemRaw = `${base}-${formatVersionLabel(version2)}`;
30254
+ const stem = stemRaw.length > 128 ? stemRaw.slice(0, 128) : stemRaw;
30255
+ let candidate = stem;
30256
+ let suffix = 2;
30257
+ while (existing.has(candidate)) {
30258
+ const extra = `-${suffix}`;
30259
+ candidate = `${stem.slice(0, Math.max(1, 128 - extra.length))}${extra}`;
30260
+ suffix += 1;
30261
+ }
30262
+ return candidate;
30263
+ }
30026
30264
  function clampInt(value, fallback, min, max) {
30027
30265
  if (!Number.isFinite(value)) return fallback;
30028
30266
  return Math.min(max, Math.max(min, Math.floor(value)));
@@ -30030,18 +30268,36 @@ function clampInt(value, fallback, min, max) {
30030
30268
  function buildSearchText(result) {
30031
30269
  return [
30032
30270
  result.id,
30271
+ result.lineage_id,
30272
+ `v${result.version}`,
30033
30273
  result.title,
30034
30274
  result.summary,
30035
30275
  result.details || "",
30036
30276
  result.source,
30277
+ result.starred ? "starred" : "",
30278
+ result.previous_result_id || "",
30037
30279
  result.session_id || "",
30038
30280
  result.workdir || "",
30039
30281
  result.tags.join(" "),
30040
- result.artifacts.join(" ")
30282
+ result.artifacts.join(" "),
30283
+ result.reproducibility?.script_path || "",
30284
+ result.reproducibility?.script_kind || "",
30285
+ result.reproducibility?.runtime || "",
30286
+ result.reproducibility?.input_files.join(" ") || "",
30287
+ result.reproducibility?.requirements.join(" ") || "",
30288
+ result.reproducibility?.strategy || "",
30289
+ result.reproducibility?.precomputed_reason || "",
30290
+ result.reproducibility?.execution?.command || "",
30291
+ result.reproducibility?.execution?.stdout_tail || "",
30292
+ result.reproducibility?.execution?.stderr_tail || "",
30293
+ result.reproducibility?.execution?.slurm_job_id || ""
30041
30294
  ].join("\n").toLowerCase();
30042
30295
  }
30043
- function sortByUpdatedDesc(items) {
30044
- return [...items].sort((a, b) => b.updated_at.localeCompare(a.updated_at));
30296
+ function sortByPriority(items) {
30297
+ return [...items].sort((a, b) => {
30298
+ if (a.starred !== b.starred) return (b.starred ? 1 : 0) - (a.starred ? 1 : 0);
30299
+ return b.updated_at.localeCompare(a.updated_at);
30300
+ });
30045
30301
  }
30046
30302
  function countParsedResults(raw) {
30047
30303
  try {
@@ -30069,7 +30325,9 @@ var ResultsStore = class {
30069
30325
  const search = sanitizeString(opts?.search, 256).toLowerCase();
30070
30326
  const source = sanitizeString(opts?.source, 64).toLowerCase();
30071
30327
  const tag = sanitizeString(opts?.tag, 64).toLowerCase();
30072
- let rows = sortByUpdatedDesc(this.readFile().results);
30328
+ const lineage = sanitizeString(opts?.lineage, 128);
30329
+ const starred = typeof opts?.starred === "boolean" ? opts.starred : void 0;
30330
+ let rows = sortByPriority(this.readFile().results);
30073
30331
  if (search) {
30074
30332
  rows = rows.filter((r) => buildSearchText(r).includes(search));
30075
30333
  }
@@ -30079,6 +30337,12 @@ var ResultsStore = class {
30079
30337
  if (tag) {
30080
30338
  rows = rows.filter((r) => r.tags.some((t) => t.toLowerCase() === tag));
30081
30339
  }
30340
+ if (lineage) {
30341
+ rows = rows.filter((r) => r.lineage_id === lineage);
30342
+ }
30343
+ if (starred !== void 0) {
30344
+ rows = rows.filter((r) => r.starred === starred);
30345
+ }
30082
30346
  const total = rows.length;
30083
30347
  rows = rows.slice(safeOffset, safeOffset + safeLimit);
30084
30348
  return { results: rows, total };
@@ -30088,14 +30352,74 @@ var ResultsStore = class {
30088
30352
  if (!normalized) return null;
30089
30353
  return this.readFile().results.find((r) => r.id === normalized) || null;
30090
30354
  }
30355
+ listResultVersions(idOrLineage) {
30356
+ const normalized = sanitizeString(idOrLineage, 128);
30357
+ if (!normalized) return [];
30358
+ const rows = this.readFile().results;
30359
+ const byId = rows.find((r) => r.id === normalized) || null;
30360
+ const lineage = byId ? byId.lineage_id : normalized;
30361
+ return rows.filter((r) => r.lineage_id === lineage).sort((a, b) => {
30362
+ if (a.version !== b.version) return b.version - a.version;
30363
+ return b.updated_at.localeCompare(a.updated_at);
30364
+ });
30365
+ }
30091
30366
  createResult(input) {
30092
- const title = sanitizeString(input.title, 240);
30093
- if (!title) throw new Error("title is required");
30094
30367
  const file2 = this.readFile();
30095
- const id = sanitizeString(input.id, 128) || randomUUID();
30368
+ const existingIds = new Set(file2.results.map((r) => r.id));
30369
+ let title = sanitizeString(input.title, 240);
30370
+ let lineageId = sanitizeString(input.lineage_id, 128);
30371
+ let version2 = sanitizePositiveInt(input.version, 1, 1, 1e4);
30372
+ let previousResultId = sanitizeNullableString(input.previous_result_id, 128);
30373
+ const versionOf = sanitizeString(input.version_of, 128);
30374
+ if (versionOf) {
30375
+ const base = file2.results.find((r) => r.id === versionOf);
30376
+ if (!base) throw new Error(`version_of result not found: ${versionOf}`);
30377
+ lineageId = base.lineage_id;
30378
+ const lineageRows = file2.results.filter((r) => r.lineage_id === lineageId);
30379
+ const latest = lineageRows.sort((a, b) => {
30380
+ if (a.version !== b.version) return b.version - a.version;
30381
+ return b.updated_at.localeCompare(a.updated_at);
30382
+ })[0] || base;
30383
+ version2 = latest.version + 1;
30384
+ previousResultId = latest.id;
30385
+ if (!title) title = base.title;
30386
+ }
30387
+ if (!title) throw new Error("title is required");
30388
+ if (lineageId) {
30389
+ const lineageRows = file2.results.filter((r) => r.lineage_id === lineageId);
30390
+ if (lineageRows.length > 0) {
30391
+ const latest = lineageRows.sort((a, b) => {
30392
+ if (a.version !== b.version) return b.version - a.version;
30393
+ return b.updated_at.localeCompare(a.updated_at);
30394
+ })[0];
30395
+ const hasVersionConflict = lineageRows.some((r) => r.version === version2);
30396
+ if ((input.version === void 0 || input.version === null) && (!versionOf || hasVersionConflict)) {
30397
+ version2 = (latest?.version || 0) + 1;
30398
+ if (!previousResultId && latest) previousResultId = latest.id;
30399
+ } else if (hasVersionConflict) {
30400
+ throw new Error(`version ${version2} already exists for lineage ${lineageId}`);
30401
+ }
30402
+ }
30403
+ }
30404
+ let id = sanitizeString(input.id, 128);
30405
+ if (!id) {
30406
+ if (versionOf) {
30407
+ const nextLineage = lineageId || randomUUID();
30408
+ id = deriveVersionedResultId(nextLineage, version2, existingIds);
30409
+ } else {
30410
+ id = randomUUID();
30411
+ }
30412
+ }
30096
30413
  if (file2.results.some((r) => r.id === id)) {
30097
30414
  throw new Error(`Result id already exists: ${id}`);
30098
30415
  }
30416
+ const effectiveLineageId = normalizeLineageId(lineageId, id);
30417
+ if (file2.results.some((r) => r.lineage_id === effectiveLineageId && r.version === version2)) {
30418
+ throw new Error(`version ${version2} already exists for lineage ${effectiveLineageId}`);
30419
+ }
30420
+ if (previousResultId && previousResultId === id) {
30421
+ throw new Error("previous_result_id cannot equal result id");
30422
+ }
30099
30423
  const ts = nowIso();
30100
30424
  const created = {
30101
30425
  id,
@@ -30103,11 +30427,16 @@ var ResultsStore = class {
30103
30427
  summary: sanitizeString(input.summary, 1e3),
30104
30428
  details: sanitizeNullableString(input.details, 2e4),
30105
30429
  source: sanitizeSource(input.source),
30430
+ starred: sanitizeBoolean(input.starred, false),
30106
30431
  session_id: sanitizeNullableString(input.session_id, 128),
30107
30432
  workdir: sanitizeNullableString(input.workdir, 4096),
30433
+ lineage_id: effectiveLineageId,
30434
+ version: version2,
30435
+ previous_result_id: sanitizeNullableString(previousResultId, 128),
30108
30436
  tags: sanitizeStringArray(input.tags, 64, 50),
30109
30437
  artifacts: sanitizeStringArray(input.artifacts, 4096, 100),
30110
30438
  metadata: sanitizeMetadata(input.metadata),
30439
+ reproducibility: sanitizeReproducibility(input.reproducibility),
30111
30440
  created_at: ts,
30112
30441
  updated_at: ts
30113
30442
  };
@@ -30153,6 +30482,13 @@ var ResultsStore = class {
30153
30482
  changed = true;
30154
30483
  }
30155
30484
  }
30485
+ if (patch.starred !== void 0) {
30486
+ const starred = sanitizeBoolean(patch.starred, false);
30487
+ if (starred !== next.starred) {
30488
+ next.starred = starred;
30489
+ changed = true;
30490
+ }
30491
+ }
30156
30492
  if (patch.session_id !== void 0) {
30157
30493
  const sessionId = sanitizeNullableString(patch.session_id, 128);
30158
30494
  if (sessionId !== next.session_id) {
@@ -30167,6 +30503,28 @@ var ResultsStore = class {
30167
30503
  changed = true;
30168
30504
  }
30169
30505
  }
30506
+ if (patch.lineage_id !== void 0) {
30507
+ const lineageId = sanitizeString(patch.lineage_id, 128);
30508
+ if (!lineageId) throw new Error("lineage_id cannot be empty");
30509
+ if (lineageId !== next.lineage_id) {
30510
+ next.lineage_id = lineageId;
30511
+ changed = true;
30512
+ }
30513
+ }
30514
+ if (patch.version !== void 0) {
30515
+ const version2 = sanitizePositiveInt(patch.version, next.version, 1, 1e4);
30516
+ if (version2 !== next.version) {
30517
+ next.version = version2;
30518
+ changed = true;
30519
+ }
30520
+ }
30521
+ if (patch.previous_result_id !== void 0) {
30522
+ const previousResultId = sanitizeNullableString(patch.previous_result_id, 128);
30523
+ if (previousResultId !== next.previous_result_id) {
30524
+ next.previous_result_id = previousResultId;
30525
+ changed = true;
30526
+ }
30527
+ }
30170
30528
  if (patch.tags !== void 0) {
30171
30529
  const tags = sanitizeStringArray(patch.tags, 64, 50);
30172
30530
  if (JSON.stringify(tags) !== JSON.stringify(next.tags)) {
@@ -30188,7 +30546,20 @@ var ResultsStore = class {
30188
30546
  changed = true;
30189
30547
  }
30190
30548
  }
30549
+ if (patch.reproducibility !== void 0) {
30550
+ const reproducibility = mergeReproducibilityPatch(next.reproducibility, patch.reproducibility);
30551
+ if (JSON.stringify(reproducibility) !== JSON.stringify(next.reproducibility)) {
30552
+ next.reproducibility = reproducibility;
30553
+ changed = true;
30554
+ }
30555
+ }
30191
30556
  if (!changed) return current;
30557
+ if (next.previous_result_id && next.previous_result_id === next.id) {
30558
+ throw new Error("previous_result_id cannot equal result id");
30559
+ }
30560
+ if (file2.results.some((r, i) => i !== idx && r.lineage_id === next.lineage_id && r.version === next.version)) {
30561
+ throw new Error(`version ${next.version} already exists for lineage ${next.lineage_id}`);
30562
+ }
30192
30563
  next.updated_at = nowIso();
30193
30564
  file2.results[idx] = next;
30194
30565
  this.writeFile(file2);
@@ -30271,11 +30642,16 @@ var ResultsStore = class {
30271
30642
  summary: sanitizeString(r.summary, 1e3),
30272
30643
  details: sanitizeNullableString(r.details, 2e4),
30273
30644
  source: sanitizeSource(r.source),
30645
+ starred: sanitizeBoolean(r.starred, false),
30274
30646
  session_id: sanitizeNullableString(r.session_id, 128),
30275
30647
  workdir: sanitizeNullableString(r.workdir, 4096),
30648
+ lineage_id: normalizeLineageId(r.lineage_id, id),
30649
+ version: sanitizePositiveInt(r.version, 1, 1, 1e4),
30650
+ previous_result_id: sanitizeNullableString(r.previous_result_id, 128),
30276
30651
  tags: sanitizeStringArray(r.tags, 64, 50),
30277
30652
  artifacts: sanitizeStringArray(r.artifacts, 4096, 100),
30278
30653
  metadata: sanitizeMetadata(r.metadata),
30654
+ reproducibility: sanitizeReproducibility(r.reproducibility),
30279
30655
  created_at: created,
30280
30656
  updated_at: updated
30281
30657
  };
@@ -30322,6 +30698,9 @@ var ResultsStore = class {
30322
30698
  };
30323
30699
 
30324
30700
  // src/lib/results-mcp.ts
30701
+ var execFileAsync = promisify(execFile);
30702
+ var DEFAULT_REPRO_TIMEOUT_SECONDS = 120;
30703
+ var MAX_REPRO_OUTPUT_CHARS = 2e4;
30325
30704
  function clampInt2(value, fallback, min, max) {
30326
30705
  if (!Number.isFinite(value)) return fallback;
30327
30706
  return Math.min(max, Math.max(min, Math.floor(value)));
@@ -30361,6 +30740,193 @@ function parseDbPathFromArgs(args) {
30361
30740
  }
30362
30741
  return dbPath;
30363
30742
  }
30743
+ function trimTail(value, maxLen = MAX_REPRO_OUTPUT_CHARS) {
30744
+ let text = null;
30745
+ if (typeof value === "string") {
30746
+ text = value;
30747
+ } else if (Buffer.isBuffer(value)) {
30748
+ text = value.toString("utf-8");
30749
+ } else if (value !== void 0 && value !== null) {
30750
+ text = String(value);
30751
+ }
30752
+ if (text === null) return null;
30753
+ if (text.length <= maxLen) return text;
30754
+ return text.slice(text.length - maxLen);
30755
+ }
30756
+ function isPathInside(rootPath, targetPath) {
30757
+ const rel = relative(rootPath, targetPath);
30758
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
30759
+ }
30760
+ function buildAllowedRoots(input) {
30761
+ const roots = [
30762
+ input.executionWorkdir ? resolve(input.executionWorkdir) : "",
30763
+ input.workdir ? resolve(input.workdir) : "",
30764
+ resolve(process.cwd())
30765
+ ].filter(Boolean);
30766
+ return [...new Set(roots)];
30767
+ }
30768
+ function ensurePathAllowed(path, allowedRoots) {
30769
+ const allowed = allowedRoots.some((root) => isPathInside(root, path));
30770
+ if (allowed) return;
30771
+ throw new Error(
30772
+ `Script path "${path}" is outside allowed roots: ${allowedRoots.join(", ")}. Set workdir/working_directory or use a path inside your workspace.`
30773
+ );
30774
+ }
30775
+ function ensureScriptOnDisk(scriptPath, allowedRoots, scriptContent) {
30776
+ const resolvedPath = resolve(String(scriptPath || "").trim());
30777
+ if (!resolvedPath) {
30778
+ throw new Error("reproducibility.script_path is required.");
30779
+ }
30780
+ ensurePathAllowed(resolvedPath, allowedRoots);
30781
+ if (typeof scriptContent === "string") {
30782
+ mkdirSync2(dirname2(resolvedPath), { recursive: true });
30783
+ const normalized = scriptContent.endsWith("\n") ? scriptContent : `${scriptContent}
30784
+ `;
30785
+ writeFileSync2(resolvedPath, normalized, "utf-8");
30786
+ try {
30787
+ chmodSync2(resolvedPath, 493);
30788
+ } catch {
30789
+ }
30790
+ }
30791
+ if (!existsSync3(resolvedPath)) {
30792
+ throw new Error(`Reproducibility script not found: ${resolvedPath}`);
30793
+ }
30794
+ const raw = readFileSync3(resolvedPath);
30795
+ const scriptSha256 = createHash("sha256").update(raw).digest("hex");
30796
+ return { scriptPath: resolvedPath, scriptSha256 };
30797
+ }
30798
+ function parseSlurmJobId(text) {
30799
+ const match = text.match(/Submitted batch job\s+([0-9]+)/i);
30800
+ return match?.[1] || null;
30801
+ }
30802
+ function makeDefaultExecution(strategy, runNow) {
30803
+ if (String(strategy || "").toLowerCase() === "precomputed") {
30804
+ return { status: "precomputed" };
30805
+ }
30806
+ if (runNow === false) return { status: "not_run" };
30807
+ return { status: "not_run" };
30808
+ }
30809
+ async function executeLocalReproScript(input) {
30810
+ const cwd = input.workdir ? resolve(input.workdir) : dirname2(input.scriptPath);
30811
+ const timeoutMs = clampInt2(input.timeoutSeconds, DEFAULT_REPRO_TIMEOUT_SECONDS, 10, 86400) * 1e3;
30812
+ const start = Date.now();
30813
+ const scriptKind = String(input.scriptKind || "").toLowerCase();
30814
+ const useCustomCommand = typeof input.commandOverride === "string" && input.commandOverride.trim().length > 0;
30815
+ const displayCommand = useCustomCommand ? input.commandOverride.trim() : scriptKind === "python" ? `python3 ${input.scriptPath}` : `bash ${input.scriptPath}`;
30816
+ try {
30817
+ if (useCustomCommand) {
30818
+ const ok2 = await execFileAsync("bash", ["-lc", input.commandOverride.trim()], {
30819
+ cwd,
30820
+ timeout: timeoutMs,
30821
+ maxBuffer: 5 * 1024 * 1024
30822
+ });
30823
+ return {
30824
+ status: "succeeded",
30825
+ command: displayCommand,
30826
+ ran_at: new Date(start).toISOString(),
30827
+ duration_ms: Date.now() - start,
30828
+ exit_code: 0,
30829
+ signal: null,
30830
+ stdout_tail: trimTail(ok2.stdout),
30831
+ stderr_tail: trimTail(ok2.stderr),
30832
+ slurm_job_id: null
30833
+ };
30834
+ }
30835
+ if (scriptKind === "python") {
30836
+ const ok2 = await execFileAsync("python3", [input.scriptPath], {
30837
+ cwd,
30838
+ timeout: timeoutMs,
30839
+ maxBuffer: 5 * 1024 * 1024
30840
+ });
30841
+ return {
30842
+ status: "succeeded",
30843
+ command: displayCommand,
30844
+ ran_at: new Date(start).toISOString(),
30845
+ duration_ms: Date.now() - start,
30846
+ exit_code: 0,
30847
+ signal: null,
30848
+ stdout_tail: trimTail(ok2.stdout),
30849
+ stderr_tail: trimTail(ok2.stderr),
30850
+ slurm_job_id: null
30851
+ };
30852
+ }
30853
+ const ok = await execFileAsync("bash", [input.scriptPath], {
30854
+ cwd,
30855
+ timeout: timeoutMs,
30856
+ maxBuffer: 5 * 1024 * 1024
30857
+ });
30858
+ return {
30859
+ status: "succeeded",
30860
+ command: displayCommand,
30861
+ ran_at: new Date(start).toISOString(),
30862
+ duration_ms: Date.now() - start,
30863
+ exit_code: 0,
30864
+ signal: null,
30865
+ stdout_tail: trimTail(ok.stdout),
30866
+ stderr_tail: trimTail(ok.stderr),
30867
+ slurm_job_id: null
30868
+ };
30869
+ } catch (err) {
30870
+ const code = typeof err?.code === "number" ? err.code : null;
30871
+ const signal = typeof err?.signal === "string" ? err.signal : null;
30872
+ return {
30873
+ status: "failed",
30874
+ command: displayCommand,
30875
+ ran_at: new Date(start).toISOString(),
30876
+ duration_ms: Date.now() - start,
30877
+ exit_code: code,
30878
+ signal,
30879
+ stdout_tail: trimTail(err?.stdout),
30880
+ stderr_tail: trimTail(err?.stderr || err?.message),
30881
+ slurm_job_id: null
30882
+ };
30883
+ }
30884
+ }
30885
+ async function executeSlurmReproScript(input) {
30886
+ const cwd = input.workdir ? resolve(input.workdir) : dirname2(input.scriptPath);
30887
+ const timeoutMs = clampInt2(input.timeoutSeconds, DEFAULT_REPRO_TIMEOUT_SECONDS, 10, 86400) * 1e3;
30888
+ const start = Date.now();
30889
+ const useCustomCommand = typeof input.commandOverride === "string" && input.commandOverride.trim().length > 0;
30890
+ const displayCommand = useCustomCommand ? input.commandOverride.trim() : `sbatch ${input.scriptPath}`;
30891
+ try {
30892
+ const ok = useCustomCommand ? await execFileAsync("bash", ["-lc", input.commandOverride.trim()], {
30893
+ cwd,
30894
+ timeout: timeoutMs,
30895
+ maxBuffer: 5 * 1024 * 1024
30896
+ }) : await execFileAsync("sbatch", [input.scriptPath], {
30897
+ cwd,
30898
+ timeout: timeoutMs,
30899
+ maxBuffer: 5 * 1024 * 1024
30900
+ });
30901
+ const combined = `${ok.stdout || ""}
30902
+ ${ok.stderr || ""}`;
30903
+ return {
30904
+ status: "submitted",
30905
+ command: displayCommand,
30906
+ ran_at: new Date(start).toISOString(),
30907
+ duration_ms: Date.now() - start,
30908
+ exit_code: 0,
30909
+ signal: null,
30910
+ stdout_tail: trimTail(ok.stdout),
30911
+ stderr_tail: trimTail(ok.stderr),
30912
+ slurm_job_id: parseSlurmJobId(combined)
30913
+ };
30914
+ } catch (err) {
30915
+ const code = typeof err?.code === "number" ? err.code : null;
30916
+ const signal = typeof err?.signal === "string" ? err.signal : null;
30917
+ return {
30918
+ status: "failed",
30919
+ command: displayCommand,
30920
+ ran_at: new Date(start).toISOString(),
30921
+ duration_ms: Date.now() - start,
30922
+ exit_code: code,
30923
+ signal,
30924
+ stdout_tail: trimTail(err?.stdout),
30925
+ stderr_tail: trimTail(err?.stderr || err?.message),
30926
+ slurm_job_id: null
30927
+ };
30928
+ }
30929
+ }
30364
30930
  async function main(args = process.argv.slice(2)) {
30365
30931
  const dbPath = parseDbPathFromArgs(args);
30366
30932
  const store = dbPath ? new ResultsStore(dbPath) : new ResultsStore();
@@ -30376,6 +30942,36 @@ async function main(args = process.argv.slice(2)) {
30376
30942
  instructions: "LabGate results registry. Use these tools to record important findings, retrieve previously recorded outcomes, and keep results structured for later review."
30377
30943
  }
30378
30944
  );
30945
+ const reproducibilityExecutionSchema = external_exports3.object({
30946
+ status: external_exports3.enum(["not_run", "succeeded", "failed", "submitted", "precomputed"]).optional(),
30947
+ command: external_exports3.string().nullable().optional(),
30948
+ ran_at: external_exports3.string().nullable().optional(),
30949
+ duration_ms: external_exports3.number().int().min(0).nullable().optional(),
30950
+ exit_code: external_exports3.number().int().nullable().optional(),
30951
+ signal: external_exports3.string().nullable().optional(),
30952
+ stdout_tail: external_exports3.string().nullable().optional(),
30953
+ stderr_tail: external_exports3.string().nullable().optional(),
30954
+ slurm_job_id: external_exports3.string().nullable().optional()
30955
+ });
30956
+ const reproducibilitySchema = external_exports3.object({
30957
+ script_path: external_exports3.string().nullable().optional(),
30958
+ script_kind: external_exports3.enum(["bash", "python", "slurm", "other"]).optional(),
30959
+ script_sha256: external_exports3.string().nullable().optional(),
30960
+ input_files: external_exports3.array(external_exports3.string()).optional(),
30961
+ runtime: external_exports3.string().nullable().optional(),
30962
+ requirements: external_exports3.array(external_exports3.string()).optional(),
30963
+ strategy: external_exports3.enum(["run", "precomputed"]).optional(),
30964
+ precomputed_reason: external_exports3.string().nullable().optional(),
30965
+ execution: reproducibilityExecutionSchema.nullable().optional()
30966
+ });
30967
+ const reproducibilityRegistrationSchema = reproducibilitySchema.extend({
30968
+ script_path: external_exports3.string().describe("Path to the script that reproduces this result"),
30969
+ script_content: external_exports3.string().optional().describe("Optional script body to write at script_path"),
30970
+ run_now: external_exports3.boolean().optional().default(true).describe("Execute or submit the script immediately"),
30971
+ timeout_seconds: external_exports3.number().int().min(10).max(86400).optional().default(DEFAULT_REPRO_TIMEOUT_SECONDS),
30972
+ command_override: external_exports3.string().optional().describe("Optional shell command used instead of the default runner"),
30973
+ working_directory: external_exports3.string().nullable().optional().describe("Working directory used for execution/submission")
30974
+ });
30379
30975
  server.registerTool(
30380
30976
  "list_results",
30381
30977
  {
@@ -30385,17 +30981,21 @@ async function main(args = process.argv.slice(2)) {
30385
30981
  search: external_exports3.string().optional().describe("Search text across title, summary, details, tags, and metadata"),
30386
30982
  tag: external_exports3.string().optional().describe("Filter by exact tag"),
30387
30983
  source: external_exports3.string().optional().describe("Filter by source (e.g. claude, codex)"),
30984
+ lineage: external_exports3.string().optional().describe("Filter by lineage id (all versions of a result thread)"),
30985
+ starred: external_exports3.boolean().optional().describe("Filter by starred status"),
30388
30986
  limit: external_exports3.number().int().min(1).max(500).optional().default(50).describe("Maximum results to return"),
30389
30987
  offset: external_exports3.number().int().min(0).optional().default(0).describe("Offset for pagination")
30390
30988
  }
30391
30989
  },
30392
- async ({ search, tag, source, limit, offset }) => {
30990
+ async ({ search, tag, source, lineage, starred, limit, offset }) => {
30393
30991
  const safeLimit = clampInt2(limit, 50, 1, 500);
30394
30992
  const safeOffset = clampInt2(offset, 0, 0, 1e5);
30395
30993
  const listed = store.listResults({
30396
30994
  search: search || void 0,
30397
30995
  tag: tag || void 0,
30398
30996
  source: source || void 0,
30997
+ lineage: lineage || void 0,
30998
+ starred: typeof starred === "boolean" ? starred : void 0,
30399
30999
  limit: safeLimit,
30400
31000
  offset: safeOffset
30401
31001
  });
@@ -30414,29 +31014,60 @@ async function main(args = process.argv.slice(2)) {
30414
31014
  title: "Register Result",
30415
31015
  description: "Register a new structured result (finding/outcome) so it can be reviewed later in the LabGate UI.",
30416
31016
  inputSchema: {
31017
+ id: external_exports3.string().optional().describe("Optional explicit id (normally auto-generated)"),
30417
31018
  title: external_exports3.string().describe("Short title of the result"),
30418
31019
  summary: external_exports3.string().optional().describe("One-line summary"),
30419
31020
  details: external_exports3.string().nullable().optional().describe("Long-form details"),
30420
31021
  source: external_exports3.string().optional().default("claude").describe("Result source label, e.g. claude or codex"),
31022
+ starred: external_exports3.boolean().optional().default(false).describe("Whether the result is starred for quick access"),
30421
31023
  session_id: external_exports3.string().nullable().optional().describe("Optional LabGate session id"),
30422
31024
  workdir: external_exports3.string().nullable().optional().describe("Optional work directory path"),
31025
+ lineage_id: external_exports3.string().optional().describe("Optional lineage id used to group versioned results"),
31026
+ version: external_exports3.number().int().min(1).optional().describe("Explicit version number within the lineage"),
31027
+ previous_result_id: external_exports3.string().nullable().optional().describe("Optional previous version id in this lineage"),
31028
+ version_of: external_exports3.string().optional().describe("Create a next version based on an existing result id"),
30423
31029
  tags: external_exports3.array(external_exports3.string()).optional().describe("Tags for filtering"),
30424
31030
  artifacts: external_exports3.array(external_exports3.string()).optional().describe("Related file paths or artifact references"),
30425
- metadata: external_exports3.record(external_exports3.string(), external_exports3.unknown()).nullable().optional().describe("Optional flat metadata map")
31031
+ metadata: external_exports3.record(external_exports3.string(), external_exports3.unknown()).nullable().optional().describe("Optional flat metadata map"),
31032
+ reproducibility: reproducibilitySchema.nullable().optional().describe("Reproducibility specification for this result")
30426
31033
  }
30427
31034
  },
30428
- async ({ title, summary, details, source, session_id, workdir, tags, artifacts, metadata }) => {
31035
+ async ({
31036
+ id,
31037
+ title,
31038
+ summary,
31039
+ details,
31040
+ source,
31041
+ starred,
31042
+ session_id,
31043
+ workdir,
31044
+ lineage_id,
31045
+ version: version2,
31046
+ previous_result_id,
31047
+ version_of,
31048
+ tags,
31049
+ artifacts,
31050
+ metadata,
31051
+ reproducibility
31052
+ }) => {
30429
31053
  try {
30430
31054
  const created = store.createResult({
31055
+ id,
30431
31056
  title,
30432
31057
  summary,
30433
31058
  details: details === void 0 ? null : details,
30434
31059
  source,
31060
+ starred,
30435
31061
  session_id,
30436
31062
  workdir,
31063
+ lineage_id,
31064
+ version: version2,
31065
+ previous_result_id,
31066
+ version_of,
30437
31067
  tags,
30438
31068
  artifacts,
30439
- metadata
31069
+ metadata,
31070
+ reproducibility
30440
31071
  });
30441
31072
  return asText(`Result "${created.title}" recorded with id ${created.id}.`);
30442
31073
  } catch (err) {
@@ -30444,6 +31075,125 @@ async function main(args = process.argv.slice(2)) {
30444
31075
  }
30445
31076
  }
30446
31077
  );
31078
+ server.registerTool(
31079
+ "register_reproducible_result",
31080
+ {
31081
+ title: "Register Reproducible Result",
31082
+ description: "Register a result with reproducibility guarantees: script path/hash, explicit inputs/requirements, and optional immediate execution (or SLURM submission).",
31083
+ inputSchema: {
31084
+ id: external_exports3.string().optional().describe("Optional explicit id (normally auto-generated)"),
31085
+ title: external_exports3.string().describe("Short title of the result"),
31086
+ summary: external_exports3.string().optional().describe("One-line summary"),
31087
+ details: external_exports3.string().nullable().optional().describe("Long-form details"),
31088
+ source: external_exports3.string().optional().default("claude").describe("Result source label, e.g. claude or codex"),
31089
+ starred: external_exports3.boolean().optional().default(false).describe("Whether the result is starred for quick access"),
31090
+ session_id: external_exports3.string().nullable().optional().describe("Optional LabGate session id"),
31091
+ workdir: external_exports3.string().nullable().optional().describe("Optional result workdir (also default run cwd)"),
31092
+ lineage_id: external_exports3.string().optional().describe("Optional lineage id used to group versioned results"),
31093
+ version: external_exports3.number().int().min(1).optional().describe("Explicit version number within the lineage"),
31094
+ previous_result_id: external_exports3.string().nullable().optional().describe("Optional previous version id in this lineage"),
31095
+ version_of: external_exports3.string().optional().describe("Create a next version based on an existing result id"),
31096
+ tags: external_exports3.array(external_exports3.string()).optional().describe("Tags for filtering"),
31097
+ artifacts: external_exports3.array(external_exports3.string()).optional().describe("Related file paths or artifact references"),
31098
+ metadata: external_exports3.record(external_exports3.string(), external_exports3.unknown()).nullable().optional().describe("Optional flat metadata map"),
31099
+ reproducibility: reproducibilityRegistrationSchema
31100
+ }
31101
+ },
31102
+ async ({
31103
+ id,
31104
+ title,
31105
+ summary,
31106
+ details,
31107
+ source,
31108
+ starred,
31109
+ session_id,
31110
+ workdir,
31111
+ lineage_id,
31112
+ version: version2,
31113
+ previous_result_id,
31114
+ version_of,
31115
+ tags,
31116
+ artifacts,
31117
+ metadata,
31118
+ reproducibility
31119
+ }) => {
31120
+ try {
31121
+ const repro = reproducibility || {};
31122
+ const executionWorkdir = typeof repro.working_directory === "string" && repro.working_directory.trim() ? String(repro.working_directory) : null;
31123
+ const allowedRoots = buildAllowedRoots({
31124
+ workdir: typeof workdir === "string" ? workdir : null,
31125
+ executionWorkdir
31126
+ });
31127
+ const scriptSpec = ensureScriptOnDisk(
31128
+ String(repro.script_path || ""),
31129
+ allowedRoots,
31130
+ typeof repro.script_content === "string" ? repro.script_content : void 0
31131
+ );
31132
+ const strategy = String(repro.strategy || "run").toLowerCase() === "precomputed" ? "precomputed" : "run";
31133
+ const runNow = repro.run_now === void 0 ? true : !!repro.run_now;
31134
+ const scriptKind = String(repro.script_kind || "bash").toLowerCase();
31135
+ const timeoutSeconds = clampInt2(
31136
+ typeof repro.timeout_seconds === "number" ? repro.timeout_seconds : void 0,
31137
+ DEFAULT_REPRO_TIMEOUT_SECONDS,
31138
+ 10,
31139
+ 86400
31140
+ );
31141
+ const commandOverride = typeof repro.command_override === "string" ? repro.command_override : void 0;
31142
+ const runCwd = executionWorkdir && executionWorkdir.trim() || typeof workdir === "string" && workdir.trim() || null;
31143
+ let execution = makeDefaultExecution(strategy, runNow);
31144
+ if (strategy !== "precomputed" && runNow) {
31145
+ execution = scriptKind === "slurm" ? await executeSlurmReproScript({
31146
+ scriptPath: scriptSpec.scriptPath,
31147
+ timeoutSeconds,
31148
+ workdir: runCwd,
31149
+ commandOverride
31150
+ }) : await executeLocalReproScript({
31151
+ scriptPath: scriptSpec.scriptPath,
31152
+ scriptKind,
31153
+ timeoutSeconds,
31154
+ workdir: runCwd,
31155
+ commandOverride
31156
+ });
31157
+ }
31158
+ const reproducibilityPayload = {
31159
+ script_path: scriptSpec.scriptPath,
31160
+ script_kind: scriptKind,
31161
+ script_sha256: scriptSpec.scriptSha256,
31162
+ input_files: Array.isArray(repro.input_files) ? repro.input_files : void 0,
31163
+ runtime: typeof repro.runtime === "string" ? repro.runtime : null,
31164
+ requirements: Array.isArray(repro.requirements) ? repro.requirements : void 0,
31165
+ strategy,
31166
+ precomputed_reason: typeof repro.precomputed_reason === "string" ? repro.precomputed_reason : null,
31167
+ execution
31168
+ };
31169
+ const created = store.createResult({
31170
+ id,
31171
+ title,
31172
+ summary,
31173
+ details: details === void 0 ? null : details,
31174
+ source,
31175
+ starred,
31176
+ session_id,
31177
+ workdir,
31178
+ lineage_id,
31179
+ version: version2,
31180
+ previous_result_id,
31181
+ version_of,
31182
+ tags,
31183
+ artifacts,
31184
+ metadata,
31185
+ reproducibility: reproducibilityPayload
31186
+ });
31187
+ return asJson({
31188
+ ok: true,
31189
+ result: created,
31190
+ reproducibility_execution: created.reproducibility?.execution || null
31191
+ });
31192
+ } catch (err) {
31193
+ return asError(`Failed to register reproducible result: ${err.message}`);
31194
+ }
31195
+ }
31196
+ );
30447
31197
  server.registerTool(
30448
31198
  "get_result",
30449
31199
  {
@@ -30461,6 +31211,27 @@ async function main(args = process.argv.slice(2)) {
30461
31211
  return asJson(result);
30462
31212
  }
30463
31213
  );
31214
+ server.registerTool(
31215
+ "list_result_versions",
31216
+ {
31217
+ title: "List Result Versions",
31218
+ description: "List all versions in a result lineage. Accepts either a result id or a lineage id.",
31219
+ inputSchema: {
31220
+ id_or_lineage: external_exports3.string().describe("Result id or lineage id")
31221
+ }
31222
+ },
31223
+ async ({ id_or_lineage }) => {
31224
+ const versions = store.listResultVersions(id_or_lineage);
31225
+ if (versions.length === 0) {
31226
+ return asError(`No versions found for "${id_or_lineage}".`);
31227
+ }
31228
+ return asJson({
31229
+ lineage_id: versions[0].lineage_id,
31230
+ count: versions.length,
31231
+ versions
31232
+ });
31233
+ }
31234
+ );
30464
31235
  server.registerTool(
30465
31236
  "update_result",
30466
31237
  {
@@ -30472,15 +31243,36 @@ async function main(args = process.argv.slice(2)) {
30472
31243
  summary: external_exports3.string().optional().describe("New summary"),
30473
31244
  details: external_exports3.string().nullable().optional().describe("New details, null clears"),
30474
31245
  source: external_exports3.string().optional().describe("New source"),
31246
+ starred: external_exports3.boolean().optional().describe("Set whether this result is starred"),
30475
31247
  session_id: external_exports3.string().nullable().optional().describe("New session id, null clears"),
30476
31248
  workdir: external_exports3.string().nullable().optional().describe("New workdir, null clears"),
31249
+ lineage_id: external_exports3.string().optional().describe("Set lineage id"),
31250
+ version: external_exports3.number().int().min(1).optional().describe("Set version number"),
31251
+ previous_result_id: external_exports3.string().nullable().optional().describe("Set previous version id"),
30477
31252
  tags: external_exports3.array(external_exports3.string()).optional().describe("Replace tags"),
30478
31253
  artifacts: external_exports3.array(external_exports3.string()).optional().describe("Replace artifacts"),
30479
- metadata: external_exports3.record(external_exports3.string(), external_exports3.unknown()).nullable().optional().describe("Replace metadata, null clears")
31254
+ metadata: external_exports3.record(external_exports3.string(), external_exports3.unknown()).nullable().optional().describe("Replace metadata, null clears"),
31255
+ reproducibility: reproducibilitySchema.nullable().optional().describe("Replace reproducibility metadata, null clears")
30480
31256
  }
30481
31257
  },
30482
- async ({ id, title, summary, details, source, session_id, workdir, tags, artifacts, metadata }) => {
30483
- const hasPatch = title !== void 0 || summary !== void 0 || details !== void 0 || source !== void 0 || session_id !== void 0 || workdir !== void 0 || tags !== void 0 || artifacts !== void 0 || metadata !== void 0;
31258
+ async ({
31259
+ id,
31260
+ title,
31261
+ summary,
31262
+ details,
31263
+ source,
31264
+ starred,
31265
+ session_id,
31266
+ workdir,
31267
+ lineage_id,
31268
+ version: version2,
31269
+ previous_result_id,
31270
+ tags,
31271
+ artifacts,
31272
+ metadata,
31273
+ reproducibility
31274
+ }) => {
31275
+ const hasPatch = title !== void 0 || summary !== void 0 || details !== void 0 || source !== void 0 || starred !== void 0 || session_id !== void 0 || workdir !== void 0 || lineage_id !== void 0 || version2 !== void 0 || previous_result_id !== void 0 || tags !== void 0 || artifacts !== void 0 || metadata !== void 0 || reproducibility !== void 0;
30484
31276
  if (!hasPatch) {
30485
31277
  return asError("No update fields provided.");
30486
31278
  }
@@ -30490,11 +31282,16 @@ async function main(args = process.argv.slice(2)) {
30490
31282
  summary,
30491
31283
  details,
30492
31284
  source,
31285
+ starred,
30493
31286
  session_id,
30494
31287
  workdir,
31288
+ lineage_id,
31289
+ version: version2,
31290
+ previous_result_id,
30495
31291
  tags,
30496
31292
  artifacts,
30497
- metadata
31293
+ metadata,
31294
+ reproducibility
30498
31295
  });
30499
31296
  if (!updated) {
30500
31297
  return asError(`Result "${id}" not found.`);