severally-mcp 2.4.0 → 2.5.0

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 (50) hide show
  1. package/CONFIG.md +1 -0
  2. package/README.ja.md +44 -19
  3. package/README.md +44 -18
  4. package/config.example.json +1 -1
  5. package/docs/how-it-works.ja.md +2 -0
  6. package/docs/how-it-works.md +2 -0
  7. package/package.json +3 -1
  8. package/plugins/severally/.claude-plugin/plugin.json +1 -1
  9. package/plugins/severally/.codex-plugin/plugin.json +1 -1
  10. package/plugins/severally/dist/severally-mcp.mjs +630 -271
  11. package/plugins/severally/skills/_template/SKILL.md.tmpl +2 -1
  12. package/plugins/severally/skills/_template/reference.md.tmpl +2 -2
  13. package/plugins/severally/skills/antigravity/severally/SKILL.md +2 -1
  14. package/plugins/severally/skills/antigravity/severally/reference.md +2 -2
  15. package/plugins/severally/skills/claude/severally/SKILL.md +2 -1
  16. package/plugins/severally/skills/claude/severally/reference.md +2 -2
  17. package/plugins/severally/skills/codex/severally/SKILL.md +2 -1
  18. package/plugins/severally/skills/codex/severally/reference.md +2 -2
  19. package/plugins/severally/skills/opencode/severally/SKILL.md +2 -1
  20. package/plugins/severally/skills/opencode/severally/reference.md +2 -2
  21. package/scripts/build.mjs +57 -54
  22. package/scripts/doctor.mjs +15 -4
  23. package/scripts/init-config.mjs +2 -1
  24. package/scripts/install.mjs +73 -9
  25. package/scripts/live-check.mjs +11 -5
  26. package/scripts/live-mcp-check.mjs +10 -4
  27. package/src/adapters/antigravity-sandbox.mjs +24 -6
  28. package/src/adapters/antigravity.mjs +80 -17
  29. package/src/adapters/claude-code.mjs +49 -8
  30. package/src/adapters/codex.mjs +56 -6
  31. package/src/adapters/opencode-sandbox.mjs +20 -2
  32. package/src/adapters/opencode.mjs +115 -19
  33. package/src/brief.mjs +84 -7
  34. package/src/consultants.mjs +193 -27
  35. package/src/cross-spawn-escape.d.ts +7 -0
  36. package/src/export.mjs +50 -6
  37. package/src/expose-paths.mjs +149 -7
  38. package/src/failures.mjs +8 -1
  39. package/src/jobs.mjs +632 -152
  40. package/src/jsonc.mjs +11 -3
  41. package/src/parse-result.mjs +91 -14
  42. package/src/platform.mjs +52 -2
  43. package/src/policy.mjs +151 -28
  44. package/src/redact.mjs +22 -3
  45. package/src/result-schema.mjs +13 -0
  46. package/src/run.mjs +158 -12
  47. package/src/schema.mjs +84 -11
  48. package/src/server.mjs +49 -14
  49. package/src/status.mjs +16 -4
  50. package/src/store.mjs +287 -6
@@ -62,7 +62,7 @@ function stripJsonc(text2) {
62
62
  }
63
63
  };
64
64
  while (i < text2.length) {
65
- const c = text2[i];
65
+ const c = text2.charAt(i);
66
66
  const next = text2[i + 1];
67
67
  if (inString) {
68
68
  out.push(c);
@@ -110,6 +110,7 @@ function parseJsonc(text2) {
110
110
  }
111
111
  var init_jsonc = __esm({
112
112
  "src/jsonc.mjs"() {
113
+ "use strict";
113
114
  }
114
115
  });
115
116
 
@@ -634,7 +635,10 @@ function findCommand(command, { cwd = process.cwd(), env = process.env } = {}) {
634
635
  if (typeof command !== "string" || !command) return null;
635
636
  cwd = path.resolve(cwd);
636
637
  const windows = process.platform === "win32";
637
- const value = (name) => env[windows ? Object.keys(env).find((key) => key.toUpperCase() === name) : name];
638
+ const value = (name) => {
639
+ const key = windows ? Object.keys(env).find((k) => k.toUpperCase() === name) : name;
640
+ return key === void 0 ? void 0 : env[key];
641
+ };
638
642
  const explicit = command.includes("/") || windows && command.includes("\\");
639
643
  const dirs = explicit ? [cwd] : [...windows ? [cwd] : [], ...(value("PATH") ?? "").split(path.delimiter).filter(Boolean)];
640
644
  for (const dir of dirs) {
@@ -654,7 +658,8 @@ function findCommand(command, { cwd = process.cwd(), env = process.env } = {}) {
654
658
  return null;
655
659
  }
656
660
  function execCommandSync(command, args, options = {}) {
657
- const opts = { windowsHide: true, encoding: "utf8", ...options };
661
+ const encoding = "utf8";
662
+ const opts = { windowsHide: true, encoding, ...options };
658
663
  const batch = batchInvocation(command, args, opts);
659
664
  const result = batch ? nativeSpawnSync(batch.command, batch.args, batch.options) : import_cross_spawn.default.sync(command, args, opts);
660
665
  if (result.error) throw result.error;
@@ -666,6 +671,7 @@ function execCommandSync(command, args, options = {}) {
666
671
  var import_which, import_cross_spawn, import_escape;
667
672
  var init_platform = __esm({
668
673
  "src/platform.mjs"() {
674
+ "use strict";
669
675
  import_which = __toESM(require_which(), 1);
670
676
  import_cross_spawn = __toESM(require_cross_spawn(), 1);
671
677
  import_escape = __toESM(require_escape(), 1);
@@ -691,7 +697,10 @@ function modelVersion(model) {
691
697
  const at = words.findIndex(isDigitWord);
692
698
  if (at === -1) return null;
693
699
  const digits = [];
694
- for (let i = at; i < words.length && isDigitWord(words[i]); i++) digits.push(Number(words[i].replace(/^v/, "")));
700
+ for (const w of words.slice(at)) {
701
+ if (!isDigitWord(w)) break;
702
+ digits.push(Number(w.replace(/^v/, "")));
703
+ }
695
704
  return digits;
696
705
  }
697
706
  function vendorOf(model, explicit = null) {
@@ -715,7 +724,9 @@ function byTier(cands, tier) {
715
724
  if (!tier.family(cands)) return cands;
716
725
  const rank = (c) => {
717
726
  const words = modelWords(c.model);
718
- return tier.last ? tier.words.indexOf(words.at(-1)) : tier.words.findIndex((w) => words.includes(w));
727
+ const last = words.at(-1);
728
+ if (tier.last) return last === void 0 ? -1 : tier.words.indexOf(last);
729
+ return tier.words.findIndex((w) => words.includes(w));
719
730
  };
720
731
  const ranks = cands.map(rank);
721
732
  if (ranks.includes(-1) || new Set(ranks).size === 1) return cands;
@@ -723,11 +734,18 @@ function byTier(cands, tier) {
723
734
  return cands.filter((_, i) => ranks[i] === best);
724
735
  }
725
736
  function byVersion(cands) {
726
- const stem = modelStem(cands[0].model).join("-");
727
- const versions = cands.map((c) => modelVersion(c.model));
728
- if (versions.includes(null) || cands.some((c) => modelStem(c.model).join("-") !== stem)) return cands;
729
- const best = versions.reduce((a, b) => compareVersions(a, b) >= 0 ? a : b);
730
- return cands.filter((_, i) => compareVersions(versions[i], best) === 0);
737
+ const [first] = cands;
738
+ if (!first) return cands;
739
+ const stem = modelStem(first.model).join("-");
740
+ if (cands.some((c) => modelStem(c.model).join("-") !== stem)) return cands;
741
+ const versioned = [];
742
+ for (const c of cands) {
743
+ const version2 = modelVersion(c.model);
744
+ if (version2 === null) return cands;
745
+ versioned.push({ c, version: version2 });
746
+ }
747
+ const best = versioned.map((x) => x.version).reduce((a, b) => compareVersions(a, b) >= 0 ? a : b);
748
+ return versioned.filter((x) => compareVersions(x.version, best) === 0).map((x) => x.c);
731
749
  }
732
750
  function rankCandidates(cands, ranking = []) {
733
751
  let left = byRanking(cands, ranking);
@@ -736,7 +754,7 @@ function rankCandidates(cands, ranking = []) {
736
754
  left = byTier(left, tier);
737
755
  }
738
756
  if (left.length > 1) left = byVersion(left);
739
- return left.length === 1 ? left[0] : null;
757
+ return left.length === 1 ? left[0] ?? null : null;
740
758
  }
741
759
  function resolveConsultant(input2, consultants, ranking = []) {
742
760
  const key = normalizeName(input2);
@@ -747,8 +765,9 @@ function resolveConsultant(input2, consultants, ranking = []) {
747
765
  if (CLI_ALIASES[key]) cands = consultants.filter((c) => c.cli === CLI_ALIASES[key]);
748
766
  else if (VENDOR_WORDS[key]) cands = consultants.filter((c) => c.vendor === VENDOR_WORDS[key]);
749
767
  else cands = consultants.filter((c) => matchesWords(c.model, input2));
750
- if (cands.length === 0) return { none: true };
751
- if (cands.length === 1) return { consultant: cands[0] };
768
+ const [only] = cands;
769
+ if (!only) return { none: true };
770
+ if (cands.length === 1) return { consultant: only };
752
771
  const winner = rankCandidates(cands, ranking);
753
772
  return winner ? { consultant: winner } : { ambiguous: cands };
754
773
  }
@@ -773,7 +792,8 @@ function buildConsultants(config2) {
773
792
  for (const key of Object.keys(cfg)) {
774
793
  if (!TOP_KEYS.includes(key)) fault(`unknown top-level key "${key}" (allowed: ${TOP_KEYS.join(", ")})`);
775
794
  }
776
- if (isObject(cfg.server) && cfg.server.network !== void 0 && !["on", "off"].includes(cfg.server.network)) {
795
+ const networkModes = ["on", "off"];
796
+ if (isObject(cfg.server) && cfg.server.network !== void 0 && !networkModes.includes(cfg.server.network)) {
777
797
  fault('server.network must be "on" or "off"');
778
798
  }
779
799
  let ranking = [];
@@ -782,11 +802,12 @@ function buildConsultants(config2) {
782
802
  ranking = cfg.ranking.map((r) => r.trim());
783
803
  else fault("ranking must be an array of non-empty strings");
784
804
  }
805
+ const cliIds = CLI_IDS;
785
806
  if (cfg.clis !== void 0) {
786
807
  if (!isObject(cfg.clis)) fault("clis must be an object keyed by CLI");
787
808
  else {
788
809
  for (const [id2, def] of Object.entries(cfg.clis)) {
789
- if (!CLI_IDS.includes(id2)) {
810
+ if (!cliIds.includes(id2)) {
790
811
  problems.push(`clis.${id2}: unknown CLI (known: ${CLI_IDS.join(", ")})`);
791
812
  continue;
792
813
  }
@@ -840,9 +861,9 @@ function buildConsultants(config2) {
840
861
  }
841
862
  for (const k of Object.keys(def))
842
863
  if (!CONSULTANT_KEYS.includes(k)) problems.push(`${at}.${k}: unknown key (allowed: ${CONSULTANT_KEYS.join(", ")})`);
843
- if (!CLI_IDS.includes(def.cli)) problems.push(`${at}.cli: must be one of ${CLI_IDS.join(", ")}`);
864
+ if (!cliIds.includes(def.cli)) problems.push(`${at}.cli: must be one of ${CLI_IDS.join(", ")}`);
844
865
  if (typeof def.model !== "string" || def.model.trim() === "") problems.push(`${at}.model: required`);
845
- for (const k of ["effort", "vendor", "note"]) {
866
+ for (const k of ["effort", "fallback_model", "vendor", "note"]) {
846
867
  if (def[k] !== void 0 && typeof def[k] !== "string") problems.push(`${at}.${k}: must be a string`);
847
868
  }
848
869
  if (def.enabled !== void 0 && typeof def.enabled !== "boolean") problems.push(`${at}.enabled: must be true or false`);
@@ -850,6 +871,10 @@ function buildConsultants(config2) {
850
871
  if (def[k] !== void 0 && !(typeof def[k] === "number" && Number.isFinite(def[k])))
851
872
  problems.push(`${at}.${k}: must be a number`);
852
873
  }
874
+ if (typeof def.fallback_model === "string" && def.fallback_model.trim() === "")
875
+ problems.push(`${at}.fallback_model: must not be empty`);
876
+ else if (typeof def.fallback_model === "string" && def.fallback_model.trim() === String(def.model).trim())
877
+ problems.push(`${at}.fallback_model: must differ from model; the fallback is for when the model's quota is used up`);
853
878
  if (def.max_budget_usd !== void 0 && def.cli !== "claude-code")
854
879
  problems.push(`${at}.max_budget_usd: only a claude-code consultant has a budget`);
855
880
  if (def.effort !== void 0 && def.cli === "antigravity")
@@ -858,15 +883,19 @@ function buildConsultants(config2) {
858
883
  broken[name] = problems.slice(before);
859
884
  continue;
860
885
  }
861
- const pair = `${def.cli}
862
- ${def.model.trim()}`;
886
+ const valid = (
887
+ /** @type {ConsultantDef} */
888
+ def
889
+ );
890
+ const pair = `${valid.cli}
891
+ ${valid.model.trim()}`;
863
892
  if (seen.has(pair)) {
864
893
  problems.push(`${at}: same cli and model as consultants.${seen.get(pair)}; asking one model twice adds no independence`);
865
894
  broken[name] = problems.slice(before);
866
895
  continue;
867
896
  }
868
897
  seen.set(pair, name);
869
- consultants.push(make(name, def));
898
+ consultants.push(make(name, valid));
870
899
  }
871
900
  }
872
901
  return { consultants: Object.freeze(consultants), ranking: Object.freeze(ranking), problems, global: global2, broken, brokenClis };
@@ -880,7 +909,8 @@ function make(name, def) {
880
909
  effort: typeof def.effort === "string" && def.effort.trim() !== "" ? def.effort.trim() : null,
881
910
  timeoutMs: typeof def.timeout_ms === "number" ? clamp(def.timeout_ms, 1e3, 72e5) : null,
882
911
  maxBudgetUsd: typeof def.max_budget_usd === "number" ? clamp(def.max_budget_usd, 0.05, 20) : null,
883
- vendor: vendorOf(model, def.vendor ?? null),
912
+ fallbackModel: typeof def.fallback_model === "string" && def.fallback_model.trim() !== "" ? def.fallback_model.trim() : null,
913
+ vendor: vendorOf(model, def.vendor),
884
914
  enabled: def.enabled !== false,
885
915
  note: typeof def.note === "string" && def.note.trim() !== "" ? def.note.trim() : null
886
916
  });
@@ -888,6 +918,7 @@ function make(name, def) {
888
918
  var CLI_IDS, DEFAULT_MODELS, CLI_VENDORS, table, CLI_ALIASES, VENDOR_WORDS, STEM_VENDORS, sameStem, TIERS, RESERVED_NAMES, isDigitWord, containsRun, compareVersions, TOP_KEYS, CLI_KEYS, CONSULTANT_KEYS, isObject, clamp;
889
919
  var init_consultants = __esm({
890
920
  "src/consultants.mjs"() {
921
+ "use strict";
891
922
  CLI_IDS = Object.freeze(["codex", "claude-code", "antigravity", "opencode"]);
892
923
  DEFAULT_MODELS = Object.freeze({
893
924
  codex: "gpt-6-astra",
@@ -932,7 +963,7 @@ var init_consultants = __esm({
932
963
  };
933
964
  TOP_KEYS = ["server", "clis", "consultants", "ranking"];
934
965
  CLI_KEYS = ["enabled", "note", "bin", "args", "env", "provider"];
935
- CONSULTANT_KEYS = ["cli", "model", "effort", "timeout_ms", "max_budget_usd", "vendor", "enabled", "note"];
966
+ CONSULTANT_KEYS = ["cli", "model", "effort", "timeout_ms", "max_budget_usd", "fallback_model", "vendor", "enabled", "note"];
936
967
  isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
937
968
  clamp = (v, min, max) => Math.min(max, Math.max(min, v));
938
969
  }
@@ -960,7 +991,7 @@ function isInstalled(command) {
960
991
  }
961
992
  function resolveCaller(raw) {
962
993
  const key = normalizeName(raw);
963
- return typeof key === "string" && Object.hasOwn(CALLER_ALIASES, key) ? CALLER_ALIASES[key] : null;
994
+ return typeof key === "string" ? CALLER_ALIASES[key] ?? null : null;
964
995
  }
965
996
  function detectCaller(env = process.env) {
966
997
  if (env.CLAUDECODE === "1" || env.CLAUDE_CODE_ENTRYPOINT) return "claude-code";
@@ -1020,6 +1051,11 @@ function isProtectedPath(abs) {
1020
1051
  function consultantByName(name) {
1021
1052
  return POLICY.consultants.find((c) => c.name === name);
1022
1053
  }
1054
+ function consultantNamed(name) {
1055
+ const c = consultantByName(name);
1056
+ if (!c) throw new Error(`no consultant named "${name}"`);
1057
+ return c;
1058
+ }
1023
1059
  function consultantAvailable(c) {
1024
1060
  return c.enabled && POLICY.clis[c.cli].available;
1025
1061
  }
@@ -1051,17 +1087,20 @@ function limitsSummary() {
1051
1087
  max_concurrent_jobs: POLICY.maxConcurrent,
1052
1088
  max_wait_ms: POLICY.maxWaitMs,
1053
1089
  input_char_budget: POLICY.input.totalCharsMax,
1054
- // Stated so it is true of all three consultants. Reading is allowed for
1055
- // every one of them -- what keeps a consultation brief-only is that the
1056
- // child starts in an empty working directory and is never told where the
1057
- // repository is, not that it cannot read. Codex is additionally sandboxed
1058
- // read-only rather than execution-free, so do not advertise "no execution".
1059
- consultant_permissions: "web search/browse and reading local files allowed, but the consultant starts in an empty working directory and is not told where your repository is -- unless context.expose_paths names files or directories, which are copied read-only into that working directory and are then the only part of your repository it has; file edits, network access, MCP tools and further consultations denied; the Codex consultant additionally has a read-only shell"
1090
+ // Stated so it is true of all four CLIs. Reading is allowed for every one
1091
+ // of them -- what keeps a consultation brief-only is that the child starts
1092
+ // in an empty working directory and is never told where the repository
1093
+ // is, not that it cannot read. Only Claude Code drops its whole-disk read
1094
+ // when files are exposed (claude-code.mjs). Codex is additionally
1095
+ // sandboxed read-only rather than execution-free, so do not advertise
1096
+ // "no execution".
1097
+ consultant_permissions: "web search/browse and reading local files allowed; the consultant starts in an empty working directory and is not told where your repository is. context.expose_paths copies the files or directories it names read-only into that working directory, but that is not a read boundary: apart from the protected credential paths, Codex, Antigravity and OpenCode can read any file your account can, and so can Claude Code when nothing is exposed; file edits, network access, MCP tools and further consultations denied; the Codex consultant additionally has a read-only shell"
1060
1098
  };
1061
1099
  }
1062
1100
  var CONFIG_HOME, CONFIG_PATH, configError, CONFIG, BUILT, FROM, LOCAL, usable, cfgCli, cfgServer, num, limit, str, CALLER_ALIASES, CALLER_INPUTS, MODES, VERDICTS, STANCES, binFor, disabledNote, isEnabled, CODEX_BIN, CLAUDE_BIN, AGY_BIN, OPENCODE_BIN, extras, POLICY, PROTECTED_HOME_PATHS, PROTECTED_SEVERALLY_PATHS, PROTECTED_FILE_NAMES, escapeRe, PATHS_CASE_INSENSITIVE, artifactKinds;
1063
1101
  var init_policy = __esm({
1064
1102
  "src/policy.mjs"() {
1103
+ "use strict";
1065
1104
  init_jsonc();
1066
1105
  init_platform();
1067
1106
  init_consultants();
@@ -1072,7 +1111,8 @@ var init_policy = __esm({
1072
1111
  try {
1073
1112
  return parseJsonc(fs2.readFileSync(CONFIG_PATH, "utf8"));
1074
1113
  } catch (err) {
1075
- if (err.code !== "ENOENT") configError = `${CONFIG_PATH}: ${err.message}`;
1114
+ const code = err instanceof Error && "code" in err ? err.code : void 0;
1115
+ if (code !== "ENOENT") configError = `${CONFIG_PATH}: ${err instanceof Error ? err.message : String(err)}`;
1076
1116
  return {};
1077
1117
  }
1078
1118
  })();
@@ -1083,17 +1123,22 @@ var init_policy = __esm({
1083
1123
  }
1084
1124
  FROM = configError ? buildConsultants({}) : BUILT;
1085
1125
  LOCAL = configError ? [] : BUILT.problems.filter((p) => !BUILT.global.includes(p));
1086
- usable = configError ? {} : CONFIG;
1126
+ usable = configError || !isObject(CONFIG) ? {} : CONFIG;
1087
1127
  cfgCli = (id2) => {
1088
- if (Object.hasOwn(BUILT.brokenClis, id2) && !configError) {
1128
+ const broken = Object.hasOwn(BUILT.brokenClis, id2) ? BUILT.brokenClis[id2] : void 0;
1129
+ if (broken && !configError) {
1089
1130
  return {
1090
1131
  enabled: false,
1091
- note: `turned off: the config file has a mistake in clis.${id2}: ${BUILT.brokenClis[id2].join("; ")}`
1132
+ note: `turned off: the config file has a mistake in clis.${id2}: ${broken.join("; ")}`
1092
1133
  };
1093
1134
  }
1094
- return usable.clis && typeof usable.clis === "object" ? usable.clis[id2] ?? {} : {};
1135
+ const def = isObject(usable.clis) ? usable.clis[id2] : void 0;
1136
+ return isObject(def) ? (
1137
+ /** @type {CliSection} */
1138
+ def
1139
+ ) : {};
1095
1140
  };
1096
- cfgServer = usable.server && typeof usable.server === "object" ? usable.server : {};
1141
+ cfgServer = isObject(usable.server) ? usable.server : {};
1097
1142
  num = (name, dflt, min, max) => {
1098
1143
  const raw = process.env[name];
1099
1144
  if (raw === void 0 || raw === "") return dflt;
@@ -1131,7 +1176,8 @@ var init_policy = __esm({
1131
1176
  CALLER_INPUTS = Object.freeze(Object.keys(CALLER_ALIASES));
1132
1177
  MODES = ["explore", "review", "debate"];
1133
1178
  VERDICTS = ["unverified", "confirmed", "not_applicable", "unverifiable"];
1134
- STANCES = ["proceed", "do_not_proceed", "alternative", "undetermined"];
1179
+ STANCES = /** @type {const} */
1180
+ ["proceed", "do_not_proceed", "alternative", "undetermined"];
1135
1181
  binFor = (id2, envName, dflt) => {
1136
1182
  const fromEnv = process.env[envName];
1137
1183
  if (fromEnv !== void 0 && fromEnv !== "") return expandHome(fromEnv);
@@ -1151,7 +1197,10 @@ var init_policy = __esm({
1151
1197
  CLAUDE_BIN = binFor("claude-code", "SEVERALLY_CLAUDE_BIN", "claude");
1152
1198
  AGY_BIN = binFor("antigravity", "SEVERALLY_AGY_BIN", "agy");
1153
1199
  OPENCODE_BIN = binFor("opencode", "SEVERALLY_OPENCODE_BIN", "opencode");
1154
- extras = (id2) => ({ extraArgs: cfgCli(id2).args ?? [], extraEnv: cfgCli(id2).env ?? {} });
1200
+ extras = (id2) => ({
1201
+ extraArgs: cfgCli(id2).args ?? [],
1202
+ extraEnv: cfgCli(id2).env ?? {}
1203
+ });
1155
1204
  POLICY = Object.freeze({
1156
1205
  home: str("SEVERALLY_HOME", path2.join(os.homedir(), ".severally")),
1157
1206
  // Per CLI: how to launch it and whether it can run here. What runs on it is
@@ -32610,12 +32659,12 @@ var init_ajvProvider_CEoC_sr = __esm({
32610
32659
  },
32611
32660
  code(cxt) {
32612
32661
  const { gen, data, schema, parentSchema, it } = cxt;
32613
- const { oneOf } = parentSchema;
32662
+ const { oneOf: oneOf2 } = parentSchema;
32614
32663
  if (!it.opts.discriminator) throw new Error("discriminator: requires discriminator option");
32615
32664
  const tagName = schema.propertyName;
32616
32665
  if (typeof tagName != "string") throw new Error("discriminator: requires propertyName");
32617
32666
  if (schema.mapping) throw new Error("discriminator: mapping is not supported");
32618
- if (!oneOf) throw new Error("discriminator: requires oneOf keyword");
32667
+ if (!oneOf2) throw new Error("discriminator: requires oneOf keyword");
32619
32668
  const valid = gen.let("valid", false);
32620
32669
  const tag = gen.const("tag", (0, codegen_1._)`${data}${(0, codegen_1.getProperty)(tagName)}`);
32621
32670
  gen.if((0, codegen_1._)`typeof ${tag} == "string"`, () => validateMapping(), () => cxt.error(false, {
@@ -32653,8 +32702,8 @@ var init_ajvProvider_CEoC_sr = __esm({
32653
32702
  const oneOfMapping = {};
32654
32703
  const topRequired = hasRequired(parentSchema);
32655
32704
  let tagRequired = true;
32656
- for (let i = 0; i < oneOf.length; i++) {
32657
- let sch = oneOf[i];
32705
+ for (let i = 0; i < oneOf2.length; i++) {
32706
+ let sch = oneOf2[i];
32658
32707
  if ((sch === null || sch === void 0 ? void 0 : sch.$ref) && !(0, util_1.schemaHasRulesButRef)(sch, it.self.RULES)) {
32659
32708
  const ref = sch.$ref;
32660
32709
  sch = compile_1.resolveRef.call(it.self, it.schemaEnv.root, it.baseId, ref);
@@ -35561,11 +35610,11 @@ var init_mcp_DIH4cS6P = __esm({
35561
35610
  * @param errorMessage - The error message.
35562
35611
  * @returns The tool error result.
35563
35612
  */
35564
- createToolError(errorMessage2) {
35613
+ createToolError(errorMessage3) {
35565
35614
  return {
35566
35615
  content: [{
35567
35616
  type: "text",
35568
- text: errorMessage2
35617
+ text: errorMessage3
35569
35618
  }],
35570
35619
  isError: true
35571
35620
  };
@@ -36508,8 +36557,8 @@ var init_stdio = __esm({
36508
36557
  import path3 from "node:path";
36509
36558
  function charCount(req) {
36510
36559
  let n = req.question.length + (req.objective?.length ?? 0);
36511
- for (const c of req.constraints) n += c.length;
36512
- for (const c of req.success_criteria) n += c.length;
36560
+ for (const c of req.constraints ?? []) n += c.length;
36561
+ for (const c of req.success_criteria ?? []) n += c.length;
36513
36562
  for (const f of req.context.facts) n += f.length;
36514
36563
  for (const c of req.context.counterpoints) n += c.length;
36515
36564
  if (req.context.proposal) n += req.context.proposal.length;
@@ -36522,19 +36571,27 @@ function parseRequest(raw, { isFollowup = false, parentMode = null, groupFollowu
36522
36571
  const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
36523
36572
  throw new RequestError(`request failed validation: ${issues.join("; ")}`, "invalid_request", issues);
36524
36573
  }
36525
- const req = parsed.data;
36526
- for (const k of ["constraints", "success_criteria"]) {
36527
- if (req.context[k] === void 0) continue;
36574
+ const req = (
36575
+ /** @type {DraftRequest} */
36576
+ parsed.data
36577
+ );
36578
+ const hoist = (k) => {
36579
+ const inner = req.context[k];
36580
+ if (inner === void 0) return;
36528
36581
  if (req[k] !== void 0) {
36529
36582
  throw new RequestError(`pass ${k} once: at the top of the request or inside context, not both`, "invalid_request");
36530
36583
  }
36531
- req[k] = req.context[k];
36584
+ req[k] = inner;
36532
36585
  delete req.context[k];
36533
- }
36586
+ };
36587
+ hoist("constraints");
36588
+ hoist("success_criteria");
36534
36589
  req.network ??= "on";
36535
36590
  req.success_criteria ??= [];
36536
36591
  req.constraints ??= [];
36537
- for (const k of ["facts", "counterpoints", "artifacts"]) req.context[k] ??= [];
36592
+ req.context.facts ??= [];
36593
+ req.context.counterpoints ??= [];
36594
+ req.context.artifacts ??= [];
36538
36595
  const hasSingle = req.target !== void 0 && req.target !== null;
36539
36596
  const hasMany = Array.isArray(req.targets) && req.targets.length > 0;
36540
36597
  if (hasSingle && hasMany) {
@@ -36605,7 +36662,8 @@ function parseRequest(raw, { isFollowup = false, parentMode = null, groupFollowu
36605
36662
  );
36606
36663
  }
36607
36664
  req.targets = unique;
36608
- req.target = unique[0];
36665
+ req.target = /** @type {string} */
36666
+ unique[0];
36609
36667
  req.fanout = unique.length > 1;
36610
36668
  req.caller = resolveCaller(req.caller ?? "") ?? null;
36611
36669
  req.caller_model = req.caller_model ?? null;
@@ -36656,6 +36714,7 @@ function parseRequest(raw, { isFollowup = false, parentMode = null, groupFollowu
36656
36714
  var L, trimmed, artifactSchema, exposedPathSchema, gitRevision, contextSchema, targetSpec, INITIATORS, NETWORK_MODES, requestSchema, RequestError;
36657
36715
  var init_schema = __esm({
36658
36716
  "src/schema.mjs"() {
36717
+ "use strict";
36659
36718
  init_zod();
36660
36719
  init_policy();
36661
36720
  init_consultants();
@@ -36740,6 +36799,11 @@ var init_schema = __esm({
36740
36799
  followup_to: external_exports.string().trim().max(80).nullish()
36741
36800
  }).strict();
36742
36801
  RequestError = class extends Error {
36802
+ /**
36803
+ * @param {string} message
36804
+ * @param {string} [code]
36805
+ * @param {unknown[]} [details]
36806
+ */
36743
36807
  constructor(message, code = "invalid_request", details = void 0) {
36744
36808
  super(message);
36745
36809
  this.name = "RequestError";
@@ -36754,6 +36818,7 @@ var init_schema = __esm({
36754
36818
  var s, enumOf, obj, arr, CONSULT_RESULT_SCHEMA, RESULT_KEYS;
36755
36819
  var init_result_schema = __esm({
36756
36820
  "src/result-schema.mjs"() {
36821
+ "use strict";
36757
36822
  s = (desc) => ({ type: "string", description: desc });
36758
36823
  enumOf = (values, desc) => ({ type: "string", enum: values, description: desc });
36759
36824
  obj = (properties, description) => ({
@@ -36844,14 +36909,14 @@ var init_result_schema = __esm({
36844
36909
  function looksLikeCode(name, value) {
36845
36910
  if (CODE_WORDS.has(value.toLowerCase()) || value.startsWith("${") || /^\$[A-Z_][A-Z0-9_]*$/.test(value) || /^(?:process\.env\.|os\.environ\b)/.test(value))
36846
36911
  return true;
36847
- if (value.length >= 8 && /^PASSW(?:OR)?D$/i.test(name.split("_").at(-1))) return false;
36912
+ if (value.length >= 8 && /^PASSW(?:OR)?D$/i.test(name.split("_").at(-1) ?? "")) return false;
36848
36913
  return /^[A-Za-z_][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value) || /^[A-Za-z_][\w$.]*\(/.test(value) || value.startsWith("&");
36849
36914
  }
36850
36915
  function redact(value) {
36851
36916
  if (value === null || value === void 0) return value;
36852
36917
  if (typeof value === "string") {
36853
36918
  let out = value;
36854
- for (const [re, rep] of PATTERNS) out = out.replace(re, rep);
36919
+ for (const [re, rep] of PATTERNS) out = typeof rep === "string" ? out.replace(re, rep) : out.replace(re, rep);
36855
36920
  return out;
36856
36921
  }
36857
36922
  if (Array.isArray(value)) return value.map(redact);
@@ -36865,6 +36930,7 @@ function redact(value) {
36865
36930
  var CODE_WORDS, PATTERNS;
36866
36931
  var init_redact = __esm({
36867
36932
  "src/redact.mjs"() {
36933
+ "use strict";
36868
36934
  CODE_WORDS = /* @__PURE__ */ new Set([
36869
36935
  "true",
36870
36936
  "false",
@@ -36960,7 +37026,7 @@ function statEntry(source) {
36960
37026
  try {
36961
37027
  stat = fs3.lstatSync(source);
36962
37028
  } catch (err) {
36963
- throw new ExposeError(`expose_paths entry "${source}" cannot be read: ${err.message}`, "not_found");
37029
+ throw new ExposeError(`expose_paths entry "${source}" cannot be read: ${messageOf(err)}`, "not_found");
36964
37030
  }
36965
37031
  if (stat.isSymbolicLink()) {
36966
37032
  throw new ExposeError(`expose_paths entry "${source}" is a symlink; name the path it points at instead`, "invalid_type");
@@ -36975,7 +37041,7 @@ function walkDir(root, dir, onFile, totals, skipped) {
36975
37041
  try {
36976
37042
  items = fs3.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
36977
37043
  } catch (err) {
36978
- throw new ExposeError(`expose_paths entry "${dir}" cannot be read: ${err.message}`, "not_found");
37044
+ throw new ExposeError(`expose_paths entry "${dir}" cannot be read: ${messageOf(err)}`, "not_found");
36979
37045
  }
36980
37046
  for (const item of items) {
36981
37047
  const child = path4.join(dir, item.name);
@@ -37002,7 +37068,11 @@ function walkDir(root, dir, onFile, totals, skipped) {
37002
37068
  function byPathParts(a, b) {
37003
37069
  const x = a.split("/");
37004
37070
  const y = b.split("/");
37005
- for (let i = 0; i < Math.min(x.length, y.length); i++) if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1;
37071
+ for (const [i, part] of x.entries()) {
37072
+ const other = y[i];
37073
+ if (other === void 0) break;
37074
+ if (part !== other) return part < other ? -1 : 1;
37075
+ }
37006
37076
  return x.length - y.length;
37007
37077
  }
37008
37078
  function walkGit(dir, exposedAs, onFile, totals, skipped) {
@@ -37213,7 +37283,7 @@ function reexposeChange(workdir, entry, manifest) {
37213
37283
  const changes = [];
37214
37284
  for (const c of entry.changes ?? []) {
37215
37285
  const m = /^(.*) \(([^()]+)\)$/.exec(c);
37216
- const p = m ? m[1] : c;
37286
+ const p = m?.[1] ?? c;
37217
37287
  if (p.includes("[REDACTED")) {
37218
37288
  manifest.skipped.push({ source: path4.join(entry.source, p), reason: "masked", exposed_as: `workspace/repo/${p}` });
37219
37289
  continue;
@@ -37268,10 +37338,7 @@ function git(cwd, args) {
37268
37338
  env: { ...env, GIT_OPTIONAL_LOCKS: "0" }
37269
37339
  });
37270
37340
  if (r.error || r.status !== 0) {
37271
- const why = (
37272
- /** @type {NodeJS.ErrnoException} */
37273
- r.error?.code === "ENOENT" ? "git is not on PATH" : String(r.stderr ?? r.error?.message ?? "").trim()
37274
- );
37341
+ const why = r.error && "code" in r.error && r.error.code === "ENOENT" ? "git is not on PATH" : String(r.stderr ?? r.error?.message ?? "").trim();
37275
37342
  throw new ExposeError(`context.expose_diff: git ${args[0]} failed in "${cwd}": ${why}`, "not_found");
37276
37343
  }
37277
37344
  return r.stdout;
@@ -37283,7 +37350,11 @@ function exposeDiff(workdir, { repo, base = "HEAD" }, manifest, maxChars) {
37283
37350
  git(top, ["diff", "--name-status", "-z", "--no-renames", "--no-relative", "--no-ext-diff", "--no-textconv", base, "--"])
37284
37351
  );
37285
37352
  const changes = [];
37286
- for (let i = 0; i + 1 < tokens.length; i += 2) changes.push({ status: tokens[i], path: tokens[i + 1] });
37353
+ for (let i = 0; i + 1 < tokens.length; i += 2) {
37354
+ const status2 = tokens[i];
37355
+ const file2 = tokens[i + 1];
37356
+ if (status2 !== void 0 && file2 !== void 0) changes.push({ status: status2, path: file2 });
37357
+ }
37287
37358
  for (const p of nulSplit(git(top, ["ls-files", "--others", "--exclude-standard", "-z"])))
37288
37359
  changes.push({ status: "untracked", path: p });
37289
37360
  const before = { ...manifest.totals };
@@ -37336,18 +37407,24 @@ function exposeDiff(workdir, { repo, base = "HEAD" }, manifest, maxChars) {
37336
37407
  }
37337
37408
  return text2;
37338
37409
  }
37339
- var ExposeError, nulSplit;
37410
+ var ExposeError, messageOf, nulSplit;
37340
37411
  var init_expose_paths = __esm({
37341
37412
  "src/expose-paths.mjs"() {
37413
+ "use strict";
37342
37414
  init_policy();
37343
37415
  init_redact();
37344
37416
  ExposeError = class extends Error {
37417
+ /**
37418
+ * @param {string} message
37419
+ * @param {string} code
37420
+ */
37345
37421
  constructor(message, code) {
37346
37422
  super(message);
37347
37423
  this.name = "ExposeError";
37348
37424
  this.code = code;
37349
37425
  }
37350
37426
  };
37427
+ messageOf = (err) => err instanceof Error ? err.message : String(err);
37351
37428
  nulSplit = (buf) => buf.toString("utf8").split("\0").filter(Boolean);
37352
37429
  }
37353
37430
  });
@@ -37458,7 +37535,7 @@ function renderExposeManifest(manifest) {
37458
37535
  if (skipped.length) skipped.unshift("");
37459
37536
  return [
37460
37537
  "The lead exposed these paths; they are copied under ./workspace in your working directory and you may read",
37461
- "them directly. They are the only part of the lead's repository you have -- if you need something that is",
37538
+ "them directly. They are the only part of the lead's repository the lead gave you -- if you need something that is",
37462
37539
  "not here, record it under `unknowns` rather than guessing. Text files were passed through the same",
37463
37540
  "credential masking as this brief; binary files were copied unchanged.",
37464
37541
  "",
@@ -37518,8 +37595,9 @@ function renderBrief(req, chain = { round: 1, priorRounds: [] }, exposeManifest
37518
37595
  const blocks = chain.priorRounds.map(renderRound);
37519
37596
  let kept = 0;
37520
37597
  let used = 0;
37521
- for (let i = blocks.length - 1; i >= 0 && (kept === 0 || used + blocks[i].length <= HISTORY_CHARS); i--) {
37522
- used += blocks[i].length;
37598
+ for (const block of [...blocks].reverse()) {
37599
+ if (kept > 0 && used + block.length > HISTORY_CHARS) break;
37600
+ used += block.length;
37523
37601
  kept += 1;
37524
37602
  }
37525
37603
  const left = chain.priorRounds.slice(0, blocks.length - kept).map((p) => p.round);
@@ -37543,6 +37621,7 @@ function renderBrief(req, chain = { round: 1, priorRounds: [] }, exposeManifest
37543
37621
  var MODE_BRIEFS, GUARDRAILS_HEAD, GUARDRAILS_RULES, WEB_ON, WEB_OFF, WORKDIR_EMPTY, WORKDIR_EXPOSED, GUARDRAILS_TAIL, EXPOSE_LISTING_MAX, clip, HISTORY_CHARS;
37544
37622
  var init_brief = __esm({
37545
37623
  "src/brief.mjs"() {
37624
+ "use strict";
37546
37625
  init_policy();
37547
37626
  init_result_schema();
37548
37627
  init_redact();
@@ -37581,7 +37660,7 @@ var init_brief = __esm({
37581
37660
  WEB_ON = "- You may search and browse the web freely; cite what you actually opened in `references`.";
37582
37661
  WEB_OFF = "- Web search and browsing are switched off for this consultation. Answer from the brief, and record what you would have looked up under `unknowns`.";
37583
37662
  WORKDIR_EMPTY = "- You are running in an empty working directory and do not have the lead's repository; everything you are meant to have is in the brief below. If something is missing, record it under `unknowns` instead of guessing or substituting an assumption.";
37584
- WORKDIR_EXPOSED = '- Your working directory is not empty: the lead exposed specific paths, listed under "Files available to you" below and copied read-only under ./workspace. Those are the only part of the lead\'s repository you have. If something you need is missing, record it under `unknowns` instead of guessing or substituting an assumption.';
37663
+ WORKDIR_EXPOSED = '- Your working directory is not empty: the lead exposed specific paths, listed under "Files available to you" below and copied read-only under ./workspace. Those are the only part of the lead\'s repository the lead gave you. If something you need is missing, record it under `unknowns` instead of guessing or substituting an assumption.';
37585
37664
  GUARDRAILS_TAIL = [
37586
37665
  "- Never include credentials, API keys, tokens, or environment variable values in your answer.",
37587
37666
  "- Do not output your internal reasoning trace. Report conclusions with their grounds and evidence.",
@@ -37611,15 +37690,15 @@ function clampText(v, max) {
37611
37690
  }
37612
37691
  function level(v, fallback = null) {
37613
37692
  const t = typeof v === "string" ? v.trim().toLowerCase() : "";
37614
- return LEVELS.includes(t) ? t : fallback;
37693
+ return oneOf(LEVELS, t) ? t : fallback;
37615
37694
  }
37616
37695
  function basis(v) {
37617
37696
  const t = typeof v === "string" ? v.trim().toLowerCase() : "";
37618
- return BASIS.includes(t) ? t : null;
37697
+ return oneOf(BASIS, t) ? t : null;
37619
37698
  }
37620
37699
  function stance(v) {
37621
37700
  const t = typeof v === "string" ? v.trim().toLowerCase() : "";
37622
- return STANCES.includes(t) ? t : null;
37701
+ return oneOf(STANCES, t) ? t : null;
37623
37702
  }
37624
37703
  function extractJson(text2) {
37625
37704
  if (typeof text2 !== "string") return null;
@@ -37627,9 +37706,7 @@ function extractJson(text2) {
37627
37706
  if (!trimmed2) return null;
37628
37707
  const candidates = [];
37629
37708
  if (trimmed2.startsWith("{")) candidates.push(trimmed2);
37630
- const fence2 = /```(?:json)?\s*([\s\S]*?)```/gi;
37631
- let m;
37632
- while ((m = fence2.exec(trimmed2)) !== null) candidates.push(m[1].trim());
37709
+ for (const [, body = ""] of trimmed2.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) candidates.push(body.trim());
37633
37710
  const start = trimmed2.indexOf("{");
37634
37711
  if (start !== -1) {
37635
37712
  let depth = 0;
@@ -37679,7 +37756,10 @@ function mapList(raw, fields, { requiredField, idPrefix = null }) {
37679
37756
  item[key] = kind === "level" ? level(entry[key]) : clampText(entry[key], O.itemTextMax);
37680
37757
  }
37681
37758
  if (!item[requiredField]) continue;
37682
- out.push(item);
37759
+ out.push(
37760
+ /** @type {T} */
37761
+ item
37762
+ );
37683
37763
  }
37684
37764
  return out;
37685
37765
  }
@@ -37758,10 +37838,14 @@ function normalizeResult(raw) {
37758
37838
  ).filter((r) => r.title || r.url)
37759
37839
  };
37760
37840
  const listTruncated = Object.fromEntries(
37761
- ["findings", "alternatives", "unknowns", "decision_changers", "next_checks", "remaining_disagreements"].filter((k) => Array.isArray(src[k]) && src[k].length > O.listMax).map((k) => [k, src[k].length])
37841
+ ["findings", "alternatives", "unknowns", "decision_changers", "next_checks", "remaining_disagreements"].flatMap((k) => {
37842
+ const v = src[k];
37843
+ return Array.isArray(v) && v.length > O.listMax ? [[k, v.length]] : [];
37844
+ })
37762
37845
  );
37846
+ const fields = result;
37763
37847
  const missing = RESULT_KEYS.filter((k) => {
37764
- const v = result[k];
37848
+ const v = fields[k];
37765
37849
  return v === null || v === void 0 || Array.isArray(v) && v.length === 0;
37766
37850
  });
37767
37851
  const groundless = result.findings.filter((f) => !f.grounds).length;
@@ -37775,13 +37859,18 @@ function normalizeResult(raw) {
37775
37859
  }
37776
37860
  };
37777
37861
  }
37778
- var OutputError, O, LEVELS, BASIS;
37862
+ var OutputError, O, oneOf, LEVELS, BASIS;
37779
37863
  var init_parse_result = __esm({
37780
37864
  "src/parse-result.mjs"() {
37865
+ "use strict";
37781
37866
  init_policy();
37782
37867
  init_result_schema();
37783
37868
  init_redact();
37784
37869
  OutputError = class extends Error {
37870
+ /**
37871
+ * @param {string} message
37872
+ * @param {string} [detail]
37873
+ */
37785
37874
  constructor(message, detail) {
37786
37875
  super(message);
37787
37876
  this.name = "OutputError";
@@ -37789,6 +37878,7 @@ var init_parse_result = __esm({
37789
37878
  }
37790
37879
  };
37791
37880
  O = POLICY.output;
37881
+ oneOf = (list, t) => list.some((x) => x === t);
37792
37882
  LEVELS = ["high", "medium", "low"];
37793
37883
  BASIS = ["sufficient", "thin", "insufficient"];
37794
37884
  }
@@ -37806,6 +37896,7 @@ function isRetriable(kind) {
37806
37896
  var RULES, AUTO_RETRY_KINDS;
37807
37897
  var init_failures = __esm({
37808
37898
  "src/failures.mjs"() {
37899
+ "use strict";
37809
37900
  RULES = [
37810
37901
  [
37811
37902
  /\b(usage limit|quota (exceeded|reached|exhausted)|out of credits|reached your .* limit|insufficient_quota|credit balance)\b/i,
@@ -37846,8 +37937,8 @@ function childEnv(target, extra = {}, operator = {}) {
37846
37937
  return { ...env, ...extra };
37847
37938
  }
37848
37939
  function providerEnvNames(target) {
37849
- const provider = JSON.stringify(POLICY.clis?.[target]?.provider ?? {});
37850
- return new Set([...provider.matchAll(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((m) => m[1]));
37940
+ const provider = JSON.stringify(target === "opencode" ? POLICY.clis.opencode.provider : {});
37941
+ return new Set([...provider.matchAll(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g)].flatMap(([, name]) => name ?? []));
37851
37942
  }
37852
37943
  function foreignSecret(target, key, keep) {
37853
37944
  if (keep.has(key) || (OWN_PREFIX[target] ?? []).some((p) => key.startsWith(p))) return false;
@@ -37905,10 +37996,10 @@ function runChild({
37905
37996
  let timedOut = false;
37906
37997
  let cancelled = false;
37907
37998
  let stalled = false;
37908
- let settleTimer = null;
37999
+ let settleTimer;
37909
38000
  let settleAt = Infinity;
37910
38001
  const watch = (line) => {
37911
- const ms = settleAfter2(line);
38002
+ const ms = settleAfter2?.(line);
37912
38003
  if (ms == null || Date.now() + ms >= settleAt) return;
37913
38004
  settleAt = Date.now() + ms;
37914
38005
  clearTimeout(settleTimer);
@@ -37940,7 +38031,7 @@ function runChild({
37940
38031
  const compact = compactLine2 ?? ((l) => l);
37941
38032
  const flushLines = () => {
37942
38033
  const lines = pending.split("\n");
37943
- pending = lines.pop();
38034
+ pending = lines.pop() ?? "";
37944
38035
  for (const l of lines) {
37945
38036
  if (settleAfter2) watch(l);
37946
38037
  append("out", `${compact(l)}
@@ -37972,7 +38063,7 @@ function runChild({
37972
38063
  const done = new Promise((resolve) => {
37973
38064
  let settled2 = false;
37974
38065
  let pipesForced = false;
37975
- let orphanTimer = null;
38066
+ let orphanTimer;
37976
38067
  const finish = (payload) => {
37977
38068
  if (settled2) return;
37978
38069
  settled2 = true;
@@ -38019,6 +38110,7 @@ function runChild({
38019
38110
  var DROP_EXACT, DROP_PREFIX, TARGET_DROP_PREFIX, TARGET_DROP_EXACT, SECRET_PREFIX, SECRET_SUFFIX, SECRET_EXACT, OWN_PREFIX, ProcHandle;
38020
38111
  var init_run = __esm({
38021
38112
  "src/run.mjs"() {
38113
+ "use strict";
38022
38114
  init_platform();
38023
38115
  init_policy();
38024
38116
  DROP_EXACT = /* @__PURE__ */ new Set(["CLAUDECODE", "CLAUDE_PID", "CLAUDE_EFFORT", "AI_AGENT", "ANTHROPIC_LOG", "NODE_OPTIONS"]);
@@ -38048,6 +38140,10 @@ var init_run = __esm({
38048
38140
  opencode: ["ZAI_"]
38049
38141
  };
38050
38142
  ProcHandle = class {
38143
+ /**
38144
+ * @param {import('node:child_process').ChildProcess} child
38145
+ * @param {() => string} [readStdout]
38146
+ */
38051
38147
  constructor(child, readStdout = () => "") {
38052
38148
  this.child = child;
38053
38149
  this.killed = false;
@@ -38057,6 +38153,7 @@ var init_run = __esm({
38057
38153
  * Kill the child and every descendant it spawned -- also after the child
38058
38154
  * itself has exited: a descendant still holding its pipes keeps the
38059
38155
  * consultation from closing, and on POSIX it is still in the group.
38156
+ * @param {NodeJS.Signals} [signal]
38060
38157
  */
38061
38158
  stop(signal = "SIGTERM") {
38062
38159
  if (this.killed || !this.child) return;
@@ -38279,7 +38376,7 @@ function withSeatLock(fn) {
38279
38376
  fs4.mkdirSync(lock);
38280
38377
  break;
38281
38378
  } catch (err) {
38282
- if (err.code !== "EEXIST") throw err;
38379
+ if (!(err instanceof Error && "code" in err && err.code === "EEXIST")) throw err;
38283
38380
  try {
38284
38381
  if (Date.now() - fs4.statSync(lock).mtimeMs > LOCK_STALE_MS) fs4.rmdirSync(lock);
38285
38382
  } catch {
@@ -38322,7 +38419,7 @@ function takeSeats(seats, cap, held) {
38322
38419
  } catch {
38323
38420
  }
38324
38421
  }
38325
- for (const mine of seats.filter((x) => x.chain_id)) {
38422
+ for (const mine of seats.filter(continuesChain)) {
38326
38423
  let other = live.find((x) => x.chain_id === mine.chain_id);
38327
38424
  if (!other && fs4.existsSync(roundPath(mine.chain_id, mine.round))) {
38328
38425
  try {
@@ -38389,9 +38486,10 @@ function cleanupJobDir(jobId) {
38389
38486
  }
38390
38487
  }
38391
38488
  }
38392
- var roundPath, JOB_ID, GROUP_ID, runningDir, runningFile, seatsDir, seatFile, LOCK_WAIT_MS, LOCK_STALE_MS, cancelFile, cancelRequested;
38489
+ var roundPath, JOB_ID, GROUP_ID, runningDir, runningFile, seatsDir, seatFile, LOCK_WAIT_MS, LOCK_STALE_MS, continuesChain, cancelFile, cancelRequested;
38393
38490
  var init_store = __esm({
38394
38491
  "src/store.mjs"() {
38492
+ "use strict";
38395
38493
  init_policy();
38396
38494
  roundPath = (chainId, round) => path6.join(POLICY.home, "history", chainId, `round-${String(round).padStart(2, "0")}.json`);
38397
38495
  JOB_ID = /^job_[0-9a-f]{12}$/;
@@ -38402,6 +38500,7 @@ var init_store = __esm({
38402
38500
  seatFile = (jobId) => path6.join(seatsDir(), `${jobId}.json`);
38403
38501
  LOCK_WAIT_MS = 2e3;
38404
38502
  LOCK_STALE_MS = 1e4;
38503
+ continuesChain = (seat) => Boolean(seat.chain_id);
38405
38504
  cancelFile = (jobId) => path6.join(seatsDir(), `${jobId}.cancel`);
38406
38505
  cancelRequested = (jobId) => JOB_ID.test(jobId) && fs4.existsSync(cancelFile(jobId));
38407
38506
  }
@@ -38503,8 +38602,8 @@ function interpret({ stdout, stderr, code, lastMessageText }) {
38503
38602
  for (const e of events3) walkUsage(e, usage);
38504
38603
  let text2 = (lastMessageText || "").trim();
38505
38604
  if (!text2) {
38506
- for (let i = events3.length - 1; i >= 0 && !text2; i--) {
38507
- const e = events3[i];
38605
+ for (const e of events3.toReversed()) {
38606
+ if (text2) break;
38508
38607
  const item = e.item ?? e;
38509
38608
  const candidate = item && item.type && String(item.type).includes("agent_message") && (item.text ?? item.message) || e.type === "item.completed" && item && (item.text ?? item.message) || null;
38510
38609
  if (typeof candidate === "string" && candidate.trim()) text2 = candidate.trim();
@@ -38570,9 +38669,15 @@ function toolCalls({ stdout }) {
38570
38669
  return [...seen.values()].reduce((o, n) => ({ ...o, [n]: (o[n] ?? 0) + 1 }), {});
38571
38670
  }
38572
38671
  function usageRecord(raw) {
38573
- const u = raw ?? {};
38672
+ const u = (
38673
+ /** @type {Record<string, number>} */
38674
+ raw ?? {}
38675
+ );
38574
38676
  const pick2 = (...names) => {
38575
- for (const n of names) if (typeof u[n] === "number") return u[n];
38677
+ for (const n of names) {
38678
+ const v = u[n];
38679
+ if (typeof v === "number") return v;
38680
+ }
38576
38681
  return null;
38577
38682
  };
38578
38683
  return {
@@ -38599,6 +38704,7 @@ function boundary({ args }) {
38599
38704
  var FORBIDDEN_FLAGS, RESERVED_FLAGS, PROFILE, listDir;
38600
38705
  var init_codex = __esm({
38601
38706
  "src/adapters/codex.mjs"() {
38707
+ "use strict";
38602
38708
  init_policy();
38603
38709
  init_failures();
38604
38710
  FORBIDDEN_FLAGS = [
@@ -38697,9 +38803,9 @@ function buildInvocation2({ workdir, guardrails, consultant, hasExposedPaths = f
38697
38803
  return { command: t.cli, args, model: chosen, workdir };
38698
38804
  }
38699
38805
  function lastJsonObject(stdout) {
38700
- const lines = (stdout || "").split("\n");
38701
- for (let i = lines.length - 1; i >= 0; i--) {
38702
- const t = lines[i].trim();
38806
+ const lines = (stdout || "").split("\n").reverse();
38807
+ for (const line of lines) {
38808
+ const t = line.trim();
38703
38809
  if (!t.startsWith("{")) continue;
38704
38810
  try {
38705
38811
  const parsed = JSON.parse(t);
@@ -38738,7 +38844,11 @@ function interpret2({ stdout, stderr, code }) {
38738
38844
  }
38739
38845
  return { ok: true, text: text2, payload, usageRaw: payload, denials: payload.permission_denials ?? [] };
38740
38846
  }
38741
- function usageRecord2(payload) {
38847
+ function usageRecord2(raw) {
38848
+ const payload = (
38849
+ /** @type {ClaudeResult | null} */
38850
+ raw
38851
+ );
38742
38852
  if (!payload) return null;
38743
38853
  const u = payload.usage ?? {};
38744
38854
  const num2 = (v) => typeof v === "number" ? v : null;
@@ -38753,7 +38863,10 @@ function usageRecord2(payload) {
38753
38863
  };
38754
38864
  }
38755
38865
  function boundary2({ args, hasExposedPaths }) {
38756
- const tools = args[args.indexOf("--tools") + 1];
38866
+ const tools = (
38867
+ /** @type {string} */
38868
+ args[args.indexOf("--tools") + 1]
38869
+ );
38757
38870
  return {
38758
38871
  reads: hasExposedPaths ? "the exposed copy only" : "whole disk",
38759
38872
  shell: "none",
@@ -38767,6 +38880,7 @@ function boundary2({ args, hasExposedPaths }) {
38767
38880
  var FORBIDDEN_FLAGS2, RESERVED_FLAGS2, posixAbsolute;
38768
38881
  var init_claude_code = __esm({
38769
38882
  "src/adapters/claude-code.mjs"() {
38883
+ "use strict";
38770
38884
  init_policy();
38771
38885
  init_failures();
38772
38886
  init_result_schema();
@@ -38987,13 +39101,14 @@ function prepareSandbox({ workdir, network = "on" }) {
38987
39101
  var SANDBOX_ALLOW, SANDBOX_DENY, ALLOWED_TOOLS, WEB_TOOLS, PATH_TOOLS, TOKEN_REL;
38988
39102
  var init_antigravity_sandbox = __esm({
38989
39103
  "src/adapters/antigravity-sandbox.mjs"() {
39104
+ "use strict";
38990
39105
  init_policy();
38991
39106
  init_store();
38992
39107
  SANDBOX_ALLOW = Object.freeze(["read_url(*)", "read_file(*)"]);
38993
39108
  SANDBOX_DENY = Object.freeze(["write_file(*)", "command(*)", "mcp(*)", "execute_url(*)", "unsandboxed(*)"]);
38994
- ALLOWED_TOOLS = Object.freeze(["view_file", "search_web", "read_url_content", "finish"]);
39109
+ ALLOWED_TOOLS = Object.freeze(["view_file", "read_file", "search_web", "read_url_content", "finish"]);
38995
39110
  WEB_TOOLS = /* @__PURE__ */ new Set(["search_web", "read_url_content"]);
38996
- PATH_TOOLS = ["view_file", "read_url_content"];
39111
+ PATH_TOOLS = ["view_file", "read_file", "read_url_content"];
38997
39112
  TOKEN_REL = path8.join(".gemini", "antigravity-cli", "antigravity-oauth-token");
38998
39113
  }
38999
39114
  });
@@ -39001,6 +39116,8 @@ var init_antigravity_sandbox = __esm({
39001
39116
  // src/adapters/antigravity.mjs
39002
39117
  var antigravity_exports = {};
39003
39118
  __export(antigravity_exports, {
39119
+ ALLOWED_TOOLS: () => ALLOWED_TOOLS,
39120
+ FILE_READ_TOOLS: () => FILE_READ_TOOLS,
39004
39121
  FORBIDDEN_FLAGS: () => FORBIDDEN_FLAGS3,
39005
39122
  RESERVED_FLAGS: () => RESERVED_FLAGS3,
39006
39123
  boundary: () => boundary3,
@@ -39047,19 +39164,17 @@ function events(stdout) {
39047
39164
  return out;
39048
39165
  }
39049
39166
  function finalEnvelope(stdout) {
39050
- const all = events(stdout);
39051
- for (let i = all.length - 1; i >= 0; i--) {
39052
- const e = all[i];
39167
+ for (const e of events(stdout).reverse()) {
39053
39168
  if (e.event === "result" && e.result && typeof e.result === "object") return e.result;
39054
39169
  if (e.event === void 0 && (e.status || e.response || e.structured_output)) return e;
39055
39170
  }
39056
39171
  return null;
39057
39172
  }
39058
39173
  function progress2({ stdout }) {
39059
- const steps = events(stdout).filter((e) => e.event === "step_update" && e.step_update);
39060
- if (!steps.length) return null;
39061
- const last = steps[steps.length - 1].step_update;
39062
- const seen = new Set(steps.map((s2) => s2.step_update.step_index)).size;
39174
+ const steps = events(stdout).flatMap((e) => e.event === "step_update" && e.step_update ? [e.step_update] : []);
39175
+ const last = steps.at(-1);
39176
+ if (!last) return null;
39177
+ const seen = new Set(steps.map((s2) => s2.step_index)).size;
39063
39178
  const what = last.tool_name ? `tool ${last.tool_name}` : last.step_type ?? "a step";
39064
39179
  return `${seen} step(s), last was ${what} (${last.state ?? "state unknown"})`;
39065
39180
  }
@@ -39123,7 +39238,11 @@ function interpret3({ stdout, stderr, code }) {
39123
39238
  }
39124
39239
  return { ok: true, text: text2, usageRaw: payload, denials: payload.denied_actions ?? [] };
39125
39240
  }
39126
- function usageRecord3(payload) {
39241
+ function usageRecord3(raw) {
39242
+ const payload = (
39243
+ /** @type {AgyEnvelope | null} */
39244
+ raw
39245
+ );
39127
39246
  if (!payload) return null;
39128
39247
  const u = payload.usage ?? {};
39129
39248
  const num2 = (v) => typeof v === "number" ? v : null;
@@ -39139,23 +39258,28 @@ function usageRecord3(payload) {
39139
39258
  turns: num2(payload.num_turns),
39140
39259
  // Which permissions the sandbox refused. A denied read_url usually explains
39141
39260
  // a thin evidence basis, so it is worth keeping in the record.
39142
- denied_actions: Array.isArray(payload.denied_actions) ? payload.denied_actions.map((d) => d?.action).filter(Boolean) : null
39261
+ denied_actions: Array.isArray(payload.denied_actions) ? payload.denied_actions.flatMap((d) => d?.action ? [d.action] : []) : null
39143
39262
  };
39144
39263
  }
39145
39264
  function boundary3({ sandbox }) {
39265
+ const { tools } = (
39266
+ /** @type {Sandbox} */
39267
+ sandbox
39268
+ );
39146
39269
  return {
39147
39270
  reads: "whole disk",
39148
39271
  shell: "none",
39149
- web: sandbox.tools.includes("search_web"),
39272
+ web: tools.includes("search_web"),
39150
39273
  writes: false,
39151
39274
  mcp: false,
39152
39275
  protected_paths: "denied (tool hook)",
39153
- tools: [...sandbox.tools]
39276
+ tools: [...tools]
39154
39277
  };
39155
39278
  }
39156
- var FORBIDDEN_FLAGS3, RESERVED_FLAGS3, stopSignal;
39279
+ var FORBIDDEN_FLAGS3, RESERVED_FLAGS3, FILE_READ_TOOLS, stopSignal;
39157
39280
  var init_antigravity = __esm({
39158
39281
  "src/adapters/antigravity.mjs"() {
39282
+ "use strict";
39159
39283
  init_policy();
39160
39284
  init_failures();
39161
39285
  init_antigravity_sandbox();
@@ -39180,6 +39304,7 @@ var init_antigravity = __esm({
39180
39304
  "-i"
39181
39305
  ];
39182
39306
  RESERVED_FLAGS3 = ["--print", "-p", "--prompt", "--project", "--remote-control", "--effort"];
39307
+ FILE_READ_TOOLS = ["view_file", "read_file"];
39183
39308
  stopSignal = "SIGQUIT";
39184
39309
  }
39185
39310
  });
@@ -39273,9 +39398,9 @@ function prepareSandbox2({ workdir, network = "on" }) {
39273
39398
  fs8.rmSync(root, { recursive: true, force: true });
39274
39399
  } catch {
39275
39400
  }
39276
- }
39401
+ },
39402
+ seed: (provider) => seedCredentialFromAuth(sandbox, provider)
39277
39403
  };
39278
- sandbox.seed = (provider) => seedCredentialFromAuth(sandbox, provider);
39279
39404
  return sandbox;
39280
39405
  }
39281
39406
  function seedCredentialFromAuth(sandbox, provider) {
@@ -39330,6 +39455,7 @@ function seedCredentialFromAuth(sandbox, provider) {
39330
39455
  var DatabaseSync, SANDBOX_DENY2, SANDBOX_ALLOW2, WEB_TOOLS2, SANDBOX_TOOLS_OFF, AUTH_REL;
39331
39456
  var init_opencode_sandbox = __esm({
39332
39457
  "src/adapters/opencode-sandbox.mjs"() {
39458
+ "use strict";
39333
39459
  init_policy();
39334
39460
  init_store();
39335
39461
  DatabaseSync = null;
@@ -39366,6 +39492,7 @@ var init_opencode_sandbox = __esm({
39366
39492
  // src/adapters/opencode.mjs
39367
39493
  var opencode_exports = {};
39368
39494
  __export(opencode_exports, {
39495
+ FILE_READ_TOOLS: () => FILE_READ_TOOLS2,
39369
39496
  FORBIDDEN_FLAGS: () => FORBIDDEN_FLAGS4,
39370
39497
  RESERVED_FLAGS: () => RESERVED_FLAGS4,
39371
39498
  boundary: () => boundary4,
@@ -39512,14 +39639,14 @@ function interpret4({ stdout, stderr, code, signal = null, truncated = false })
39512
39639
  let text2 = null;
39513
39640
  let textIndex = -1;
39514
39641
  let lastErrorIndex = -1;
39642
+ let lastError = "";
39515
39643
  let finishedAfterText = false;
39516
39644
  let tokens = null;
39517
39645
  let cost = null;
39518
39646
  let steps = 0;
39519
39647
  const errors = [];
39520
39648
  const earlierTextParts = [];
39521
- for (let i = 0; i < all.length; i++) {
39522
- const e = all[i];
39649
+ for (const [i, e] of all.entries()) {
39523
39650
  const part = e.part;
39524
39651
  if (e.type === "text" && part?.type === "text" && part.time?.end && typeof part.text === "string" && part.text.trim()) {
39525
39652
  if (text2 !== null) earlierTextParts.push(text2);
@@ -39532,7 +39659,8 @@ function interpret4({ stdout, stderr, code, signal = null, truncated = false })
39532
39659
  if (part.tokens && typeof part.tokens === "object") tokens = part.tokens;
39533
39660
  if (typeof part.cost === "number") cost = part.cost;
39534
39661
  } else if (e.type === "error" && e.error) {
39535
- errors.push(errorMessage(e.error));
39662
+ lastError = errorMessage(e.error);
39663
+ errors.push(lastError);
39536
39664
  lastErrorIndex = i;
39537
39665
  }
39538
39666
  }
@@ -39547,14 +39675,14 @@ function interpret4({ stdout, stderr, code, signal = null, truncated = false })
39547
39675
  };
39548
39676
  }
39549
39677
  if (lastErrorIndex > textIndex) {
39550
- const msg = errors[errors.length - 1];
39678
+ const msg = lastError;
39551
39679
  return { ok: false, failureKind: classifyMessage(msg), message: msg, usageRaw };
39552
39680
  }
39553
39681
  if (code !== 0) {
39554
39682
  const { msg, kind } = failureMessage({ errors, stderr, stdout, code, signal, what: "after producing an answer" });
39555
39683
  return { ok: false, failureKind: kind, message: msg, usageRaw };
39556
39684
  }
39557
- if (truncated && !(finishedAfterText && all.length > 0 && all[all.length - 1].type === "step_finish")) {
39685
+ if (truncated && !(finishedAfterText && all.at(-1)?.type === "step_finish")) {
39558
39686
  return {
39559
39687
  ok: false,
39560
39688
  failureKind: "invalid_output",
@@ -39577,14 +39705,19 @@ function toolCalls3({ stdout }) {
39577
39705
  return [...seen.values()].reduce((o, n) => ({ ...o, [n]: (o[n] ?? 0) + 1 }), {});
39578
39706
  }
39579
39707
  function progress3({ stdout }) {
39580
- const seen = events2(stdout).filter((e) => ["step_start", "step_finish", "text", "tool_use"].includes(e.type));
39581
- if (!seen.length) return null;
39582
- const last = seen[seen.length - 1];
39708
+ const seen = events2(stdout).filter(
39709
+ (e) => e.type !== void 0 && ["step_start", "step_finish", "text", "tool_use"].includes(e.type)
39710
+ );
39711
+ const last = seen.at(-1);
39712
+ if (!last) return null;
39583
39713
  const what = last.type === "tool_use" && last.part?.tool ? `tool ${last.part.tool}` : last.type;
39584
39714
  return `${seen.length} event(s), last was ${what}`;
39585
39715
  }
39586
39716
  function usageRecord4(raw) {
39587
- const r = raw ?? {};
39717
+ const r = (
39718
+ /** @type {OpencodeUsage | null} */
39719
+ raw ?? {}
39720
+ );
39588
39721
  const t = r.tokens && typeof r.tokens === "object" ? r.tokens : {};
39589
39722
  const num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
39590
39723
  const cache = t.cache && typeof t.cache === "object" ? t.cache : {};
@@ -39601,19 +39734,24 @@ function usageRecord4(raw) {
39601
39734
  };
39602
39735
  }
39603
39736
  function boundary4({ sandbox }) {
39737
+ const { tools } = (
39738
+ /** @type {Sandbox} */
39739
+ sandbox
39740
+ );
39604
39741
  return {
39605
39742
  reads: "whole disk",
39606
39743
  shell: "none",
39607
- web: sandbox.tools.includes("webfetch"),
39744
+ web: tools.includes("webfetch"),
39608
39745
  writes: false,
39609
39746
  mcp: false,
39610
39747
  protected_paths: "denied (permission rules)",
39611
- tools: [...sandbox.tools]
39748
+ tools: [...tools]
39612
39749
  };
39613
39750
  }
39614
- var FORBIDDEN_FLAGS4, standaloneSupport, probePromise, RESERVED_FLAGS4;
39751
+ var FORBIDDEN_FLAGS4, standaloneSupport, probePromise, RESERVED_FLAGS4, FILE_READ_TOOLS2;
39615
39752
  var init_opencode = __esm({
39616
39753
  "src/adapters/opencode.mjs"() {
39754
+ "use strict";
39617
39755
  init_policy();
39618
39756
  init_failures();
39619
39757
  init_platform();
@@ -39642,6 +39780,7 @@ var init_opencode = __esm({
39642
39780
  standaloneSupport = null;
39643
39781
  probePromise = null;
39644
39782
  RESERVED_FLAGS4 = ["--model", "--standalone", "--wizard"];
39783
+ FILE_READ_TOOLS2 = ["read", "grep"];
39645
39784
  }
39646
39785
  });
39647
39786
 
@@ -39662,7 +39801,7 @@ function ownerState(jobId, owner) {
39662
39801
  process.kill(owner.pid, 0);
39663
39802
  pidAlive = true;
39664
39803
  } catch (err) {
39665
- pidAlive = err.code === "EPERM";
39804
+ pidAlive = err instanceof Error && "code" in err && err.code === "EPERM";
39666
39805
  }
39667
39806
  if (!pidAlive) return "dead";
39668
39807
  return Date.now() > Date.parse(owner.deadline_at) ? "overdue" : "alive";
@@ -39710,7 +39849,7 @@ async function settled(promise2, ms, signal) {
39710
39849
  await (promise2 ? Promise.race([promise2, until]) : until);
39711
39850
  } finally {
39712
39851
  clearTimeout(timer);
39713
- signal?.removeEventListener("abort", stop);
39852
+ if (stop) signal?.removeEventListener("abort", stop);
39714
39853
  }
39715
39854
  }
39716
39855
  function parses(text2) {
@@ -39735,11 +39874,18 @@ function tally(values) {
39735
39874
  return out;
39736
39875
  }
39737
39876
  function recordSummary(job) {
39738
- const ids = recordableIds(job.result);
39739
- const written = new Set(job.record.entries.map((e) => e.id));
39877
+ const record2 = (
39878
+ /** @type {JobRecord} */
39879
+ job.record
39880
+ );
39881
+ const ids = recordableIds(
39882
+ /** @type {ConsultResult} */
39883
+ job.result
39884
+ );
39885
+ const written = new Set(record2.entries.map((e) => e.id));
39740
39886
  return {
39741
- ...job.record,
39742
- verdicts: tally(job.record.entries.map((e) => e.verdict)),
39887
+ ...record2,
39888
+ verdicts: tally(record2.entries.map((e) => e.verdict)),
39743
39889
  coverage: { recordable: ids.length, recorded: written.size },
39744
39890
  unrecorded: ids.filter((id2) => !written.has(id2))
39745
39891
  };
@@ -39755,13 +39901,16 @@ function inherit(raw, round) {
39755
39901
  const { brief } = round;
39756
39902
  const out = raw.network === void 0 && round.network ? { ...raw, network: round.network } : { ...raw };
39757
39903
  if (!brief) return out;
39758
- for (const k of ["objective", "success_criteria", "constraints"]) {
39759
- if (out[k] === void 0 && raw.context?.[k] === void 0 && brief[k] != null && !(Array.isArray(brief[k]) && !brief[k].length))
39760
- out[k] = brief[k];
39904
+ const topKeys = ["objective", "success_criteria", "constraints"];
39905
+ for (const k of topKeys) {
39906
+ const v = brief[k];
39907
+ if (out[k] === void 0 && raw.context?.[k] === void 0 && v != null && !(Array.isArray(v) && !v.length)) out[k] = v;
39761
39908
  }
39762
39909
  const ctx = { ...raw.context ?? {} };
39763
- for (const k of ["facts", "proposal", "counterpoints"]) {
39764
- if (ctx[k] === void 0 && brief[k] != null && !(Array.isArray(brief[k]) && !brief[k].length)) ctx[k] = brief[k];
39910
+ const contextKeys = ["facts", "proposal", "counterpoints"];
39911
+ for (const k of contextKeys) {
39912
+ const v = brief[k];
39913
+ if (ctx[k] === void 0 && v != null && !(Array.isArray(v) && !v.length)) ctx[k] = v;
39765
39914
  }
39766
39915
  const artifacts = [...brief.artifacts ?? []];
39767
39916
  const last = artifacts.at(-1);
@@ -39793,8 +39942,22 @@ function adviceCaveat(normalized) {
39793
39942
  }
39794
39943
  return notes.length ? notes.join("; ") : null;
39795
39944
  }
39945
+ function unreadCaveat(calls, readTools) {
39946
+ if (!calls || !readTools) return null;
39947
+ if (readTools.some((t) => calls[t])) return null;
39948
+ const failed = readTools.reduce((n, t) => n + (calls[`${t}:error`] ?? 0), 0);
39949
+ const how = failed ? `${failed} failed or were refused` : "none was made";
39950
+ return `files were exposed, but no call to read one succeeded (${how})`;
39951
+ }
39952
+ function refusedToolCaveat(calls, allowed) {
39953
+ if (!calls || !allowed) return null;
39954
+ const names = Object.keys(calls).filter((k) => k.endsWith(":error")).map((k) => k.slice(0, -":error".length)).filter((t) => !allowed.includes(t) && /read|view|open/i.test(t));
39955
+ if (!names.length) return null;
39956
+ return `the consultant's CLI called ${names.join(", ")}, which severally does not allow, and was refused; a new CLI release may have renamed its file-reading tool`;
39957
+ }
39796
39958
  function sameVendorCaveat(caller, callerModel, consultant) {
39797
- const callerVendor = (callerModel ? vendorOf(callerModel) : null) ?? (caller ? CLI_VENDORS[caller] ?? null : null);
39959
+ const callerCli = CLI_IDS.find((c) => c === caller);
39960
+ const callerVendor = (callerModel ? vendorOf(callerModel) : null) ?? (callerCli ? CLI_VENDORS[callerCli] ?? null : null);
39798
39961
  const vendor = consultant.vendor;
39799
39962
  if (!callerVendor || !vendor || callerVendor !== vendor) return null;
39800
39963
  const consultantModel = consultant.model;
@@ -39804,11 +39967,12 @@ function sameVendorCaveat(caller, callerModel, consultant) {
39804
39967
  }
39805
39968
  function comparison(members2, budget) {
39806
39969
  const each = (budget - COMPARISON_NOTE_CHARS) / Math.max(1, members2.length) - COLUMN_FIXED_CHARS;
39807
- let columns = members2.map((m) => byTarget(m, each));
39970
+ const firstPass = members2.map((m) => ({ m, column: byTarget(m, each) }));
39971
+ let columns = firstPass.map((x) => x.column);
39808
39972
  const cutShort = columns.filter((c) => c.truncated).length;
39809
39973
  if (each !== Infinity && cutShort) {
39810
39974
  const spare = columns.reduce((n, c) => n + (c.truncated ? 0 : Math.max(0, each + COLUMN_FIXED_CHARS - printed(c))), 0);
39811
- columns = members2.map((m, i) => columns[i].truncated ? byTarget(m, each + spare / cutShort) : columns[i]);
39975
+ columns = firstPass.map(({ m, column }) => column.truncated ? byTarget(m, each + spare / cutShort) : column);
39812
39976
  }
39813
39977
  return {
39814
39978
  by_target: columns,
@@ -39938,8 +40102,12 @@ function pointsOf(view, ids) {
39938
40102
  const verdicts = new Map((view.record?.entries ?? []).map((e) => [e.id, e]));
39939
40103
  const lists = { finding: r.findings, unknown: r.unknowns, next_check: r.next_checks };
39940
40104
  const points = [...new Set(ids)].map((id2) => {
39941
- const [kind, item] = Object.entries(lists).map(([k, l]) => [k, l.find((x) => x.id === id2)]).find(([, x]) => x);
39942
- return { kind, ...item, recorded: verdicts.get(id2) ?? null };
40105
+ const [point] = Object.entries(lists).flatMap(([kind, l]) => {
40106
+ const item = l.find((x) => x.id === id2);
40107
+ return item ? [{ kind, ...item }] : [];
40108
+ });
40109
+ if (!point) throw new RequestError(`"${id2}" is not a point in this answer`, "unknown_entry_id");
40110
+ return { ...point, recorded: verdicts.get(id2) ?? null };
39943
40111
  });
39944
40112
  return {
39945
40113
  job_id: view.job_id,
@@ -40043,6 +40211,7 @@ function assertNoForbiddenFlags(target, args) {
40043
40211
  var ADAPTERS, MIN_RETRY_MS, CANCEL_POLL_MS, API_KEY_VARS, CREDENTIAL_FILE_VARS, ALL_API_CREDENTIAL_VARS, USAGE_FIGURES, id, LIVE_HERE, JobManager, PARTIAL_TAIL_CHARS, GROUP_CHARS, COLUMN_FIXED_CHARS, COMPARISON_NOTE_CHARS, QUESTION_CHARS, FAILURE_MESSAGE_CHARS, SUMMARY_CHARS, CAVEAT_CHARS, ITEM_CHARS, MIN_TEXT_CHARS, flagNames;
40044
40212
  var init_jobs = __esm({
40045
40213
  "src/jobs.mjs"() {
40214
+ "use strict";
40046
40215
  init_policy();
40047
40216
  init_consultants();
40048
40217
  init_schema();
@@ -40078,6 +40247,10 @@ var init_jobs = __esm({
40078
40247
  // A consultation this process does not hold: finished (its round file), or
40079
40248
  // still in progress (its marker). A marker whose owner is gone becomes a
40080
40249
  // failed round, `interrupted`, written once by whichever process claims it.
40250
+ /**
40251
+ * @param {string} jobId
40252
+ * @returns {StoredRound | null}
40253
+ */
40081
40254
  #fromDisk(jobId) {
40082
40255
  const done = loadRound(jobId);
40083
40256
  if (done) return done;
@@ -40090,6 +40263,12 @@ var init_jobs = __esm({
40090
40263
  // something the next reader finishes: the round is written (the index
40091
40264
  // takes it once), then the marker goes. A round already on disk wins -- its
40092
40265
  // owner finished and died before removing the marker.
40266
+ /**
40267
+ * @param {string} jobId
40268
+ * @param {StoredRound} rec
40269
+ * @param {'alive' | 'dead' | 'overdue'} state
40270
+ * @returns {Round}
40271
+ */
40093
40272
  #interrupt(jobId, rec, state) {
40094
40273
  const done = loadRound(jobId);
40095
40274
  if (done) {
@@ -40120,25 +40299,33 @@ var init_jobs = __esm({
40120
40299
  for (const jobId of listRunning()) {
40121
40300
  const rec = loadRunning(jobId);
40122
40301
  const state = rec && ownerState(jobId, rec.owner);
40123
- if (rec && state !== "alive") this.#interrupt(jobId, rec, state);
40302
+ if (rec && state && state !== "alive") this.#interrupt(jobId, rec, state);
40124
40303
  }
40125
40304
  }
40305
+ /**
40306
+ * @param {StoredRound} rec
40307
+ * @returns {FullView}
40308
+ */
40126
40309
  #diskView(rec) {
40127
- const elsewhere = Boolean(rec.owner);
40128
40310
  const { owner, ...view } = rec;
40129
40311
  return {
40130
40312
  ...view,
40131
- cancellable: false,
40313
+ // consult_cancel asks the process that owns it to stop it.
40314
+ cancellable: Boolean(owner) && (view.status === "running" || view.status === "queued"),
40132
40315
  progress: null,
40133
40316
  record: view.record ? recordSummary(view) : null,
40134
- brief: elsewhere ? null : view.brief,
40135
- limits: elsewhere ? null : limitsSummary(),
40136
- next_step: elsewhere ? `running in another severally process (pid ${owner.pid}); poll again later -- it can only be cancelled from that process` : nextStep(view)
40317
+ brief: owner ? null : view.brief,
40318
+ limits: owner ? null : limitsSummary(),
40319
+ next_step: owner ? `running in another severally process (pid ${owner.pid}); poll again later, or stop it with consult_cancel` : nextStep(view)
40137
40320
  };
40138
40321
  }
40139
40322
  get running() {
40140
40323
  return [...this.jobs.values()].filter((j) => j.status === "running");
40141
40324
  }
40325
+ /**
40326
+ * @param {Job} job
40327
+ * @returns {Round}
40328
+ */
40142
40329
  #record(job) {
40143
40330
  return {
40144
40331
  job_id: job.job_id,
@@ -40181,13 +40368,25 @@ var init_jobs = __esm({
40181
40368
  }
40182
40369
  // `detail: "full"` returns the brief as stored; by default a finished
40183
40370
  // consultation names it (size, sha256, round file) instead of repeating it.
40184
- /** @param {string} jobId @param {{detail?: string}} [opts] */
40371
+ /**
40372
+ * @param {string} jobId
40373
+ * @param {{detail?: string | undefined}} [opts]
40374
+ * @returns {JobView | null}
40375
+ */
40185
40376
  view(jobId, { detail } = {}) {
40186
40377
  const job = this.jobs.get(jobId);
40187
40378
  if (!job) {
40188
- const rec2 = this.#fromDisk(jobId);
40189
- return rec2 ? compactBrief(this.#diskView(rec2), detail) : null;
40379
+ const rec = this.#fromDisk(jobId);
40380
+ return rec ? compactBrief(this.#diskView(rec), detail) : null;
40190
40381
  }
40382
+ return this.#jobView(job, detail);
40383
+ }
40384
+ /**
40385
+ * @param {Job} job
40386
+ * @param {string | undefined} detail
40387
+ * @returns {JobView}
40388
+ */
40389
+ #jobView(job, detail) {
40191
40390
  const rec = this.#record(job);
40192
40391
  rec.cancellable = job.status === "running" || job.status === "queued";
40193
40392
  rec.progress = job.status === "running" && job._handle ? ADAPTERS[job.cli]?.progress?.({ stdout: job._handle.stdoutSoFar() }) ?? null : null;
@@ -40207,30 +40406,35 @@ var init_jobs = __esm({
40207
40406
  // job_id and group_id can still be followed up and recorded against. The
40208
40407
  // index only picks and orders; each row shown is read from its round, which
40209
40408
  // holds the verdicts written since. Memory wins over disk for the same job.
40409
+ /** @param {{limit?: number}} [opts] */
40210
40410
  list({ limit: limit2 = 20 } = {}) {
40211
- const here = this.order.map((jid) => this.jobs.get(jid));
40212
- const seen = new Set(here.map((j) => j.job_id));
40213
- const elsewhere = [];
40411
+ const candidates = [];
40412
+ const seen = /* @__PURE__ */ new Set();
40413
+ for (const jid of this.order) {
40414
+ const job = this.jobs.get(jid);
40415
+ if (!job) continue;
40416
+ seen.add(job.job_id);
40417
+ candidates.push({ at: job.created_at ?? "", load: () => job });
40418
+ }
40214
40419
  for (const jobId of listRunning()) {
40215
40420
  const live = seen.has(jobId) ? null : loadRunning(jobId);
40216
40421
  if (!live) continue;
40217
40422
  seen.add(jobId);
40218
40423
  const state = ownerState(jobId, live.owner);
40219
40424
  const saved = state === "alive" ? null : loadRoundAt(live);
40220
- elsewhere.push({
40221
- rec: state === "alive" ? live : saved ?? { ...live, status: "failed", failure: { kind: "interrupted" } }
40222
- });
40425
+ const rec = state === "alive" ? live : saved ?? /** @type {Round} */
40426
+ { ...live, status: "failed", failure: { kind: "interrupted" } };
40427
+ candidates.push({ at: rec.created_at ?? "", load: () => rec });
40223
40428
  }
40224
40429
  for (const row of indexRows())
40225
40430
  if (!seen.has(row.job_id)) {
40226
40431
  seen.add(row.job_id);
40227
- elsewhere.push({ row });
40432
+ candidates.push({ at: row.created_at ?? "", load: () => loadRoundAt(row) });
40228
40433
  }
40229
- const at = (x) => x.created_at ?? x.rec?.created_at ?? x.row?.created_at ?? "";
40230
40434
  const picked = [];
40231
- for (const x of [...here, ...elsewhere].sort((p, q) => at(q) < at(p) ? -1 : at(q) > at(p) ? 1 : 0)) {
40435
+ for (const x of candidates.sort((p, q) => q.at < p.at ? -1 : q.at > p.at ? 1 : 0)) {
40232
40436
  if (picked.length >= limit2) break;
40233
- const j = x.row ? loadRoundAt(x.row) : x.rec ?? x;
40437
+ const j = x.load();
40234
40438
  if (j) picked.push(j);
40235
40439
  }
40236
40440
  return picked.map((j) => {
@@ -40267,7 +40471,7 @@ var init_jobs = __esm({
40267
40471
  const by = {};
40268
40472
  const reported = {};
40269
40473
  for (const j of this.jobs.values()) {
40270
- const attempts = [...(j.retries ?? []).map((r) => r.usage), j.usage].filter(Boolean);
40474
+ const attempts = [...(j.retries ?? []).map((r) => r.usage), j.usage].filter((u) => u != null);
40271
40475
  if (!attempts.length) continue;
40272
40476
  const t = by[j.target] ??= { consultations: 0, attempts: 0 };
40273
40477
  const seen = reported[j.target] ??= /* @__PURE__ */ new Set();
@@ -40281,11 +40485,13 @@ var init_jobs = __esm({
40281
40485
  }
40282
40486
  }
40283
40487
  }
40488
+ const out = {};
40284
40489
  for (const [target, t] of Object.entries(by)) {
40285
- t.not_reported = USAGE_FIGURES.filter((k) => !reported[target].has(k));
40490
+ out[target] = { ...t, not_reported: USAGE_FIGURES.filter((k) => !reported[target]?.has(k)) };
40286
40491
  }
40287
- return by;
40492
+ return out;
40288
40493
  }
40494
+ /** @param {RawRequest} rawRequest the tool arguments, not yet validated */
40289
40495
  start(rawRequest) {
40290
40496
  if (process.env.SEVERALLY_ACTIVE === "1") {
40291
40497
  throw new RequestError(
@@ -40308,29 +40514,30 @@ var init_jobs = __esm({
40308
40514
  fromGroup = this.#groupParents(followupTo);
40309
40515
  rawRequest = { ...rawRequest, targets: fromGroup.links.map((l) => l.parent.target) };
40310
40516
  } else if (followupTo) {
40311
- parent = this.jobs.get(followupTo) ?? loadRound(followupTo);
40312
- if (!parent) throw new RequestError(`followup_to references unknown job "${followupTo}"`, "unknown_job");
40313
- if (parent.status !== "completed") {
40517
+ const found = this.jobs.get(followupTo) ?? loadRound(followupTo);
40518
+ if (!found) throw new RequestError(`followup_to references unknown job "${followupTo}"`, "unknown_job");
40519
+ if (found.status !== "completed") {
40314
40520
  throw new RequestError(
40315
- `followup_to job "${followupTo}" is ${parent.status}; only a completed consultation can be followed up`,
40521
+ `followup_to job "${followupTo}" is ${found.status}; only a completed consultation can be followed up`,
40316
40522
  "followup_not_completed"
40317
40523
  );
40318
40524
  }
40319
- const rounds = this.#chainRounds(parent.chain_id);
40320
- const later = rounds.find((j) => j.round > parent.round && !["failed", "cancelled"].includes(j.status));
40525
+ const rounds = this.#chainRounds(found.chain_id);
40526
+ const later = rounds.find((j) => j.round > found.round && !["failed", "cancelled"].includes(j.status));
40321
40527
  if (later) {
40322
40528
  throw new RequestError(
40323
- `job "${followupTo}" is round ${parent.round}, and this consultation already has round ${later.round} (job "${later.job_id}", ${later.status}); follow up the latest round`,
40529
+ `job "${followupTo}" is round ${found.round}, and this consultation already has round ${later.round} (job "${later.job_id}", ${later.status}); follow up the latest round`,
40324
40530
  "followup_not_latest"
40325
40531
  );
40326
40532
  }
40327
- parent = { ...parent, nextRound: Math.max(...rounds.map((j) => j.round)) + 1 };
40533
+ parent = { ...found, nextRound: Math.max(...rounds.map((j) => j.round)) + 1 };
40328
40534
  }
40329
40535
  if (parent && (rawRequest.target ?? null) === null && !rawRequest.targets?.length) {
40330
40536
  rawRequest = { ...rawRequest, target: parent.target };
40331
40537
  }
40332
- if (parent || fromGroup)
40333
- rawRequest = inherit(rawRequest, fromGroup ? fromGroup.links[0].groupMember ?? fromGroup.links[0].parent : parent);
40538
+ const first = fromGroup?.links[0];
40539
+ const inheritFrom = first ? first.groupMember ?? first.parent : parent;
40540
+ if (inheritFrom) rawRequest = inherit(rawRequest, inheritFrom);
40334
40541
  const req = parseRequest(rawRequest, {
40335
40542
  isFollowup: Boolean(parent || fromGroup),
40336
40543
  parentMode: parent?.mode ?? fromGroup?.group.mode ?? null,
@@ -40357,18 +40564,21 @@ var init_jobs = __esm({
40357
40564
  );
40358
40565
  }
40359
40566
  const chainOf = (i) => fromGroup ? fromGroup.links[i] : parent ? { parent, round } : null;
40360
- const seats = req.targets.map((target, i) => ({
40361
- job_id: id("job"),
40362
- ...chainOf(i) ? { chain_id: chainOf(i).parent.chain_id, round: chainOf(i).round } : {},
40363
- // Twice the budget plus an hour: the deadline is wall-clock, the budget is
40364
- // not, so a suspended laptop must not make a live owner look overdue.
40365
- owner: {
40366
- pid: process.pid,
40367
- deadline_at: new Date(
40368
- Date.now() + 2 * timeoutMs(consultantByName(target)) + POLICY.killGraceMs + 36e5
40369
- ).toISOString()
40370
- }
40371
- }));
40567
+ const seats = req.targets.map((target, i) => {
40568
+ const link = chainOf(i);
40569
+ return {
40570
+ job_id: id("job"),
40571
+ ...link ? { chain_id: link.parent.chain_id, round: link.round } : {},
40572
+ // Twice the budget plus an hour: the deadline is wall-clock, the budget is
40573
+ // not, so a suspended laptop must not make a live owner look overdue.
40574
+ owner: {
40575
+ pid: process.pid,
40576
+ deadline_at: new Date(
40577
+ Date.now() + 2 * timeoutMs(consultantByName(target)) + POLICY.killGraceMs + 36e5
40578
+ ).toISOString()
40579
+ }
40580
+ };
40581
+ });
40372
40582
  const taken = takeSeats(seats, POLICY.maxConcurrent, (seat) => ownerState(seat.job_id, seat.owner) === "alive");
40373
40583
  if (taken.chainBusy) {
40374
40584
  const { live, mine } = taken.chainBusy;
@@ -40395,6 +40605,10 @@ var init_jobs = __esm({
40395
40605
  // round of its own chain. A member that is still running, never answered or
40396
40606
  // has used its rounds is skipped and named; if that is every member, the
40397
40607
  // request is refused.
40608
+ /**
40609
+ * @param {string} groupId
40610
+ * @returns {GroupParents}
40611
+ */
40398
40612
  #groupParents(groupId) {
40399
40613
  const group = this.groups.get(groupId) ?? loadGroup(groupId);
40400
40614
  if (!group) throw new RequestError(`followup_to references unknown group "${groupId}"`, "unknown_group");
@@ -40412,10 +40626,18 @@ var init_jobs = __esm({
40412
40626
  }
40413
40627
  const rounds = this.#chainRounds(member.chain_id);
40414
40628
  const latest = rounds.filter((j) => !["failed", "cancelled"].includes(j.status)).sort((a, b) => b.round - a.round)[0];
40629
+ if (!latest) {
40630
+ skipped.push({
40631
+ job_id: member.job_id,
40632
+ target: member.target,
40633
+ reason: `${member.status}, so there is no answer to follow up`
40634
+ });
40635
+ continue;
40636
+ }
40415
40637
  const nextRound = Math.max(...rounds.map((j) => j.round)) + 1;
40416
40638
  const consultant = consultantByName(member.target);
40417
- const reason = !latest ? `${member.status}, so there is no answer to follow up` : latest.status !== "completed" ? `round ${latest.round} is still ${latest.status}` : nextRound > POLICY.maxRounds ? `has used its ${POLICY.maxRounds} rounds` : !consultant || !consultantAvailable(consultant) ? "this consultant is no longer configured or available here" : null;
40418
- if (reason) skipped.push({ job_id: (latest ?? member).job_id, target: member.target, reason });
40639
+ const reason = latest.status !== "completed" ? `round ${latest.round} is still ${latest.status}` : nextRound > POLICY.maxRounds ? `has used its ${POLICY.maxRounds} rounds` : !consultant || !consultantAvailable(consultant) ? "this consultant is no longer configured or available here" : null;
40640
+ if (reason) skipped.push({ job_id: latest.job_id, target: member.target, reason });
40419
40641
  else links.push({ parent: { ...latest, nextRound }, round: nextRound, groupMember: member });
40420
40642
  }
40421
40643
  if (!links.length) {
@@ -40426,14 +40648,21 @@ var init_jobs = __esm({
40426
40648
  }
40427
40649
  return { group, links, skipped };
40428
40650
  }
40651
+ /**
40652
+ * @param {ParsedRequest} req
40653
+ * @param {Array<{parent: Round | null, round: number, groupMember?: Round}>} links
40654
+ * @param {Seat[]} seats
40655
+ * @param {GroupParents | null} [fromGroup]
40656
+ */
40429
40657
  #admitted(req, links, seats, fromGroup = null) {
40430
- const parent = links[0].groupMember ?? links[0].parent;
40658
+ const first = links[0];
40659
+ const parent = first?.groupMember ?? first?.parent;
40431
40660
  const groupId = id("group");
40432
40661
  let exposeManifest = null;
40433
- const parentEntries = req.context.expose_from_parent ? parent.brief?.expose_paths?.entries ?? [] : [];
40662
+ const parentEntries = req.context.expose_from_parent ? parent?.brief?.expose_paths?.entries ?? [] : [];
40434
40663
  if (req.context.expose_from_parent && parentEntries.length === 0) {
40435
40664
  throw new RequestError(
40436
- `context.expose_from_parent: job "${parent.job_id}" exposed no files, so there is nothing to copy again; name them in context.expose_paths`,
40665
+ `context.expose_from_parent: job "${parent?.job_id}" exposed no files, so there is nothing to copy again; name them in context.expose_paths`,
40437
40666
  "nothing_to_inherit"
40438
40667
  );
40439
40668
  }
@@ -40454,7 +40683,10 @@ var init_jobs = __esm({
40454
40683
  if (req.context.expose_diff) {
40455
40684
  exposeManifest.skipped.push({ source: d.source, reason: "replaced", exposed_as: d.exposed_as });
40456
40685
  } else {
40457
- if (reexposeChange(snapshot, d, exposeManifest)) exposeManifest.entries.at(-1).inherited = true;
40686
+ if (reexposeChange(snapshot, d, exposeManifest)) {
40687
+ const added = exposeManifest.entries.at(-1);
40688
+ if (added) added.inherited = true;
40689
+ }
40458
40690
  }
40459
40691
  }
40460
40692
  if (req.context.expose_diff) {
@@ -40463,28 +40695,32 @@ var init_jobs = __esm({
40463
40695
  if (diff) {
40464
40696
  req.context.artifacts = [
40465
40697
  ...req.context.artifacts,
40466
- { name: `git diff ${exposeManifest.entries.at(-1).base}`, kind: "diff", language: "diff", excerpt: diff }
40698
+ { name: `git diff ${exposeManifest.entries.at(-1)?.base}`, kind: "diff", language: "diff", excerpt: diff }
40467
40699
  ];
40468
40700
  }
40469
40701
  }
40470
40702
  markSinceParent(exposeManifest, parentEntries);
40471
40703
  if (parentEntries.length && !exposeManifest.entries.length && !req.context.facts.length && !req.context.artifacts.length) {
40472
40704
  throw new RequestError(
40473
- `context.expose_from_parent: nothing job "${parent.job_id}" exposed is there any more, and this request has no facts or artifacts`,
40705
+ `context.expose_from_parent: nothing job "${parent?.job_id}" exposed is there any more, and this request has no facts or artifacts`,
40474
40706
  "nothing_to_inherit"
40475
40707
  );
40476
40708
  }
40477
40709
  } catch (err) {
40478
40710
  cleanupJobDir(groupId);
40479
- if (err instanceof RequestError || !err.code) throw err;
40711
+ if (err instanceof RequestError || !(err instanceof Error) || !("code" in err) || !err.code) throw err;
40480
40712
  throw new RequestError(err.message, err.code === "too_large" ? "expose_paths_too_large" : "expose_path_invalid");
40481
40713
  }
40482
40714
  req._snapshot = path11.join(snapshot, "workspace");
40483
40715
  }
40484
40716
  const members2 = req.targets.map((target, i) => {
40485
- const { parent: from, round: round2 } = links[i];
40717
+ const link = links[i];
40718
+ const seat = seats[i];
40719
+ if (!link || !seat) throw new Error(`no follow-up link or seat for target "${target}"`);
40720
+ const { parent: from, round: round2 } = link;
40486
40721
  const chainId = from ? from.chain_id : id("chain");
40487
- const jobId = seats[i].job_id;
40722
+ const jobId = seat.job_id;
40723
+ const consultant = consultantNamed(target);
40488
40724
  const job = {
40489
40725
  job_id: jobId,
40490
40726
  chain_id: chainId,
@@ -40519,9 +40755,9 @@ var init_jobs = __esm({
40519
40755
  status: "queued",
40520
40756
  // What this consultant is: the CLI it runs on, the model and its
40521
40757
  // lineage, as the operator defined it.
40522
- cli: consultantByName(target).cli,
40523
- model: consultantByName(target).model,
40524
- vendor: consultantByName(target).vendor,
40758
+ cli: consultant.cli,
40759
+ model: consultant.model,
40760
+ vendor: consultant.vendor,
40525
40761
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
40526
40762
  started_at: null,
40527
40763
  finished_at: null,
@@ -40538,16 +40774,18 @@ var init_jobs = __esm({
40538
40774
  this.order.push(jobId);
40539
40775
  LIVE_HERE.add(jobId);
40540
40776
  this.#watchCancels();
40541
- job._owner = seats[i].owner;
40777
+ job._owner = seat.owner;
40542
40778
  this.#mark(job);
40543
40779
  return job;
40544
40780
  });
40545
40781
  this.#evict();
40782
+ const [firstMember] = members2;
40783
+ if (!firstMember) throw new Error("a consultation needs a target");
40546
40784
  const group = {
40547
40785
  group_id: groupId,
40548
40786
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
40549
40787
  mode: req.mode,
40550
- question: members2[0].question,
40788
+ question: firstMember.question,
40551
40789
  // already redacted, and byte-identical across the group
40552
40790
  job_ids: members2.map((m) => m.job_id)
40553
40791
  };
@@ -40564,13 +40802,13 @@ var init_jobs = __esm({
40564
40802
  });
40565
40803
  }
40566
40804
  if (req._snapshot) cleanupJobDir(groupId);
40567
- const { round } = members2[0];
40805
+ const { round } = firstMember;
40568
40806
  const sameRound = members2.every((m) => m.round === round);
40569
40807
  const common = {
40570
40808
  group_id: groupId,
40571
40809
  ...sameRound ? { round, rounds_remaining: POLICY.maxRounds - round } : {},
40572
40810
  ...fromGroup ? { followup_to_group: fromGroup.group.group_id, skipped: fromGroup.skipped } : {},
40573
- ...parentEntries.length ? { exposed_from_parent: inheritedSummary(exposeManifest) } : {},
40811
+ ...parentEntries.length && exposeManifest ? { exposed_from_parent: inheritedSummary(exposeManifest) } : {},
40574
40812
  mode: req.mode,
40575
40813
  // Counted, not refused: a brief of facts alone is a legitimate
40576
40814
  // consultation, but one about code that hands over no file is one the
@@ -40584,7 +40822,7 @@ var init_jobs = __esm({
40584
40822
  limits: limitsSummary()
40585
40823
  };
40586
40824
  if (!req.fanout) {
40587
- const job = members2[0];
40825
+ const job = firstMember;
40588
40826
  return {
40589
40827
  ...common,
40590
40828
  job_id: job.job_id,
@@ -40615,13 +40853,24 @@ var init_jobs = __esm({
40615
40853
  // (memory wins: it can hold a verdict written since the round was saved).
40616
40854
  // Each point goes with the lead's verdict on it, so the consultant answers
40617
40855
  // knowing which of its points were checked and how they came out.
40856
+ /**
40857
+ * @param {string} chainId
40858
+ * @returns {Round[]}
40859
+ */
40618
40860
  #chainRounds(chainId) {
40619
40861
  const byRound = new Map(loadChain(chainId).map((j) => [j.round, j]));
40620
40862
  for (const j of this.jobs.values()) if (j.chain_id === chainId) byRound.set(j.round, j);
40621
40863
  return [...byRound.values()];
40622
40864
  }
40865
+ /**
40866
+ * @param {string} chainId
40867
+ * @returns {PriorRound[]}
40868
+ */
40623
40869
  #chainHistory(chainId) {
40624
- return this.#chainRounds(chainId).filter((j) => j.status === "completed" && j.result).sort((a, b) => a.round - b.round).map((j) => {
40870
+ return this.#chainRounds(chainId).filter(
40871
+ /** @returns {j is Round & {result: ConsultResult}} */
40872
+ (j) => j.status === "completed" && Boolean(j.result)
40873
+ ).sort((a, b) => a.round - b.round).map((j) => {
40625
40874
  const verdicts = new Map((j.record?.entries ?? []).map((e) => [e.id, e]));
40626
40875
  const ordered = (items, shape) => [...items.filter((x) => verdicts.has(x.id)), ...items.filter((x) => !verdicts.has(x.id))].map((x) => ({
40627
40876
  id: x.id,
@@ -40646,6 +40895,7 @@ var init_jobs = __esm({
40646
40895
  #evict() {
40647
40896
  while (this.order.length > POLICY.maxJobsRetained) {
40648
40897
  const oldest = this.order.shift();
40898
+ if (oldest === void 0) break;
40649
40899
  const j = this.jobs.get(oldest);
40650
40900
  if (j && (j.status === "running" || j.status === "queued")) {
40651
40901
  this.order.push(oldest);
@@ -40659,14 +40909,22 @@ var init_jobs = __esm({
40659
40909
  // last member there is nothing left to compare, so drop the group rather than
40660
40910
  // keep answering as a complete fan-out with no answers in it (and keep the
40661
40911
  // Map, which holds the question text, from growing without bound).
40912
+ /** @param {string} groupId */
40662
40913
  #dropGroupIfEmpty(groupId) {
40663
40914
  const group = this.groups.get(groupId);
40664
40915
  if (!group) return;
40665
40916
  if (group.job_ids.some((jid) => this.jobs.has(jid))) return;
40666
40917
  this.groups.delete(groupId);
40667
40918
  }
40919
+ /**
40920
+ * @param {Job} job
40921
+ * @param {ParsedRequest} req
40922
+ * @param {import('./brief.mjs').BriefChain} chain
40923
+ * @param {string} [brief]
40924
+ * @returns {Promise<Job>}
40925
+ */
40668
40926
  async #execute(job, req, chain, brief) {
40669
- const consultant = consultantByName(job.target);
40927
+ const consultant = consultantNamed(job.target);
40670
40928
  const cli = consultant.cli;
40671
40929
  const adapter = ADAPTERS[cli];
40672
40930
  const workdir = makeWorkdir(job.job_id);
@@ -40674,7 +40932,7 @@ var init_jobs = __esm({
40674
40932
  try {
40675
40933
  fs9.cpSync(req._snapshot, path11.join(workdir, "workspace"), { recursive: true });
40676
40934
  } catch (err) {
40677
- return this.#fail(job, "spawn_error", `could not copy the exposed files: ${err.message}`);
40935
+ return this.#fail(job, "spawn_error", `could not copy the exposed files: ${err instanceof Error ? err.message : err}`);
40678
40936
  }
40679
40937
  }
40680
40938
  const text2 = brief ?? renderBrief(req, chain);
@@ -40698,20 +40956,26 @@ var init_jobs = __esm({
40698
40956
  this.#mark(job);
40699
40957
  const t0 = Date.now();
40700
40958
  if (adapter.prepare) await adapter.prepare();
40701
- const invocation = adapter.buildInvocation({
40702
- workdir,
40703
- schemaPath,
40704
- guardrails: renderGuardrails(Boolean(req._snapshot), req.network),
40705
- sandbox,
40706
- consultant,
40707
- hasExposedPaths: Boolean(req._snapshot),
40708
- network: req.network
40709
- });
40710
- const argsProblem = extraArgsProblem(cli, invocation.args, t.extraArgs);
40711
- if (argsProblem) return this.#fail(job, "spawn_error", `config clis.${cli}.args: ${argsProblem}`);
40712
- invocation.args = withExtraArgs(invocation.args, t.extraArgs);
40713
- assertNoForbiddenFlags(cli, invocation.args);
40714
- job.boundary = adapter.boundary?.({ args: invocation.args, sandbox, network: req.network, hasExposedPaths: Boolean(req._snapshot) }) ?? null;
40959
+ const launch = (c) => {
40960
+ const invocation2 = adapter.buildInvocation({
40961
+ workdir,
40962
+ schemaPath,
40963
+ guardrails: renderGuardrails(Boolean(req._snapshot), req.network),
40964
+ sandbox,
40965
+ consultant: c,
40966
+ hasExposedPaths: Boolean(req._snapshot),
40967
+ network: req.network
40968
+ });
40969
+ const problem = extraArgsProblem(cli, invocation2.args, t.extraArgs);
40970
+ if (problem) return problem;
40971
+ invocation2.args = withExtraArgs(invocation2.args, t.extraArgs);
40972
+ assertNoForbiddenFlags(cli, invocation2.args);
40973
+ job.boundary = adapter.boundary?.({ args: invocation2.args, sandbox, network: req.network, hasExposedPaths: Boolean(req._snapshot) }) ?? null;
40974
+ return invocation2;
40975
+ };
40976
+ const first = launch(consultant);
40977
+ if (typeof first === "string") return this.#fail(job, "spawn_error", `config clis.${cli}.args: ${first}`);
40978
+ let invocation = first;
40715
40979
  const attempt = (timeout) => {
40716
40980
  const { handle, done } = runChild({
40717
40981
  command: invocation.command,
@@ -40720,7 +40984,9 @@ var init_jobs = __esm({
40720
40984
  env,
40721
40985
  input: text2,
40722
40986
  timeoutMs: timeout,
40723
- onCancelSignal: (fn) => job._cancelFns.push(fn),
40987
+ onCancelSignal: (fn) => {
40988
+ job._cancelFns.push(fn);
40989
+ },
40724
40990
  compactLine: adapter.compactLine,
40725
40991
  settleAfter: adapter.settleAfter,
40726
40992
  stopSignal: adapter.stopSignal
@@ -40733,6 +40999,7 @@ var init_jobs = __esm({
40733
40999
  let interpreted;
40734
41000
  let authTried = false;
40735
41001
  let autoRetries = 0;
41002
+ let fallenBack = false;
40736
41003
  for (; ; ) {
40737
41004
  job.tool_calls = adapter.toolCalls?.(run) ?? null;
40738
41005
  job.duration_ms = Date.now() - t0;
@@ -40761,7 +41028,7 @@ var init_jobs = __esm({
40761
41028
  }
40762
41029
  const lastMessageText = invocation.lastMessagePath ? readIfExists(invocation.lastMessagePath) : "";
40763
41030
  interpreted = adapter.interpret({ ...run, lastMessageText });
40764
- const { recoveredError } = interpreted;
41031
+ const recoveredError = interpreted.ok ? interpreted.recoveredError : null;
40765
41032
  if (interpreted.ok && recoveredError && !parses(interpreted.text)) {
40766
41033
  interpreted = { ok: false, ...recoveredError, usageRaw: interpreted.usageRaw };
40767
41034
  }
@@ -40789,6 +41056,22 @@ var init_jobs = __esm({
40789
41056
  continue;
40790
41057
  }
40791
41058
  }
41059
+ if (interpreted.failureKind === "usage_limit" && consultant.fallbackModel && !fallenBack && remaining >= MIN_RETRY_MS) {
41060
+ fallenBack = true;
41061
+ const fallback = consultant.fallbackModel;
41062
+ const next = launch({ ...consultant, model: fallback });
41063
+ if (typeof next === "string") return this.#fail(job, "spawn_error", `config clis.${cli}.args: ${next}`);
41064
+ invocation = next;
41065
+ job.retries.push({
41066
+ failure_kind: interpreted.failureKind,
41067
+ reason: `${job.model} quota used up; re-run on ${fallback}`,
41068
+ at: (/* @__PURE__ */ new Date()).toISOString(),
41069
+ usage: adapter.usageRecord(interpreted.usageRaw)
41070
+ });
41071
+ job.model = fallback;
41072
+ run = await attempt(remaining);
41073
+ continue;
41074
+ }
40792
41075
  const wait = POLICY.retryBackoffMs * 3 ** autoRetries;
40793
41076
  if (AUTO_RETRY_KINDS.includes(interpreted.failureKind) && autoRetries < POLICY.maxAutoRetries && remaining - wait >= MIN_RETRY_MS) {
40794
41077
  autoRetries += 1;
@@ -40845,7 +41128,10 @@ var init_jobs = __esm({
40845
41128
  adviceCaveat(normalized),
40846
41129
  sameVendorCaveat(job.caller, job.caller_model, consultant),
40847
41130
  interpreted.recoveredError && `the consultant's CLI reported an error mid-run and then finished its answer: ${redact(interpreted.recoveredError.message).slice(0, 300)}`,
40848
- leftover && "the answer parsed, but a process the consultant started kept its output open after it exited and was stopped (one that left its process group may still be running)"
41131
+ leftover && "the answer parsed, but a process the consultant started kept its output open after it exited and was stopped (one that left its process group may still be running)",
41132
+ fallenBack && `answered by ${job.model}: ${consultant.model}'s quota was used up`,
41133
+ req._snapshot && unreadCaveat(job.tool_calls, adapter.FILE_READ_TOOLS),
41134
+ refusedToolCaveat(job.tool_calls, adapter.ALLOWED_TOOLS)
40849
41135
  ].filter(Boolean);
40850
41136
  job.quality = {
40851
41137
  ...normalized.quality,
@@ -40860,6 +41146,13 @@ var init_jobs = __esm({
40860
41146
  sandbox?.cleanup();
40861
41147
  }
40862
41148
  }
41149
+ /**
41150
+ * @param {Job} job
41151
+ * @param {string} kind
41152
+ * @param {unknown} message
41153
+ * @param {string | null} [detail]
41154
+ * @returns {Job}
41155
+ */
40863
41156
  #fail(job, kind, message, detail) {
40864
41157
  if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") return job;
40865
41158
  job.status = kind === "cancelled" ? "cancelled" : "failed";
@@ -40875,12 +41168,14 @@ var init_jobs = __esm({
40875
41168
  return job;
40876
41169
  }
40877
41170
  // The in-progress marker other processes read (see store.writeRunning).
41171
+ /** @param {Job} job */
40878
41172
  #mark(job) {
40879
41173
  try {
40880
41174
  writeRunning({ ...this.#record(job), owner: job._owner });
40881
41175
  } catch {
40882
41176
  }
40883
41177
  }
41178
+ /** @param {Job} job */
40884
41179
  #finish(job) {
40885
41180
  try {
40886
41181
  persistRound(this.#record(job));
@@ -40898,7 +41193,7 @@ var init_jobs = __esm({
40898
41193
  // consultant did not produce and a word outside VERDICTS, then stores what
40899
41194
  // was written. It never decides that a finding was adopted, and it never
40900
41195
  // fills in a verdict the lead did not write.
40901
- /** @param {Record<string, any>} [input] */
41196
+ /** @param {RawRecord} [input] */
40902
41197
  record(input2 = {}) {
40903
41198
  if (input2.records !== void 0) return this.#recordMany(input2);
40904
41199
  const allowed = ["job_id", "entries", "reflection"];
@@ -40918,6 +41213,7 @@ var init_jobs = __esm({
40918
41213
  // record is checked before anything is written, so a bad id leaves the
40919
41214
  // history as it was; then each job is written on its own, and a write that
40920
41215
  // fails is named in the answer rather than reported as saved.
41216
+ /** @param {RawRecord} input */
40921
41217
  #recordMany(input2) {
40922
41218
  const extra = Object.keys(input2).filter((k) => k !== "records");
40923
41219
  if (extra.length) {
@@ -40960,7 +41256,10 @@ var init_jobs = __esm({
40960
41256
  }
40961
41257
  // Everything that can be wrong with a record, found before anything is
40962
41258
  // written: what the job's record and reflection become, or a RequestError.
40963
- /** @param {{job_id?: string, entries?: any[], reflection?: any}} rec */
41259
+ /**
41260
+ * @param {RawRecord} rec
41261
+ * @returns {RecordChange}
41262
+ */
40964
41263
  #checkRecord({ job_id: jobId, entries, reflection }) {
40965
41264
  if (entries === void 0 && reflection === void 0) {
40966
41265
  throw new RequestError(
@@ -40984,7 +41283,7 @@ var init_jobs = __esm({
40984
41283
  }
40985
41284
  const ids = recordableIds(job.result);
40986
41285
  const now = (/* @__PURE__ */ new Date()).toISOString();
40987
- const merged = new Map((job.record?.entries ?? []).map((e) => [e.id, e]));
41286
+ const written = /* @__PURE__ */ new Map();
40988
41287
  for (const entry of entries ?? []) {
40989
41288
  const id2 = typeof entry?.id === "string" ? entry.id.trim() : "";
40990
41289
  if (!ids.includes(id2)) {
@@ -41000,7 +41299,7 @@ var init_jobs = __esm({
41000
41299
  "invalid_verdict"
41001
41300
  );
41002
41301
  }
41003
- merged.set(id2, {
41302
+ written.set(id2, {
41004
41303
  id: id2,
41005
41304
  verdict,
41006
41305
  effect: text(entry?.effect),
@@ -41025,21 +41324,28 @@ var init_jobs = __esm({
41025
41324
  }
41026
41325
  newReflection = { delta, related_item_ids: related, recorded_at: now };
41027
41326
  }
41028
- const newRecord = entries === void 0 ? void 0 : { updated_at: now, entries: ids.filter((id2) => merged.has(id2)).map((id2) => merged.get(id2)) };
41029
- return { job, live, record: newRecord, reflection: newReflection };
41327
+ return { job, live, entries: entries === void 0 ? void 0 : [...written.values()], reflection: newReflection };
41030
41328
  }
41031
- #saveRecord({ job, live, record: record2, reflection }) {
41329
+ /** @param {RecordChange} change */
41330
+ #saveRecord({ job, live, entries, reflection }) {
41032
41331
  const was = { record: job.record, reflection: job.reflection };
41033
- if (reflection !== void 0) job.reflection = reflection;
41034
- if (record2 !== void 0) job.record = record2;
41332
+ const disk = loadRoundAt(job);
41333
+ const base = disk?.result ? disk : job;
41334
+ job.reflection = reflection ?? base.reflection;
41335
+ if (entries !== void 0) {
41336
+ const merged = new Map((base.record?.entries ?? []).map((e) => [e.id, e]));
41337
+ for (const e of entries) merged.set(e.id, e);
41338
+ const ids = job.result ? recordableIds(job.result) : [];
41339
+ job.record = { updated_at: (/* @__PURE__ */ new Date()).toISOString(), entries: ids.flatMap((id2) => merged.get(id2) ?? []) };
41340
+ } else if (base.record) job.record = base.record;
41035
41341
  try {
41036
- persistRound(live ? this.#record(job) : job, { appendIndex: false });
41342
+ persistRound(live ? this.#record(live) : job, { appendIndex: false });
41037
41343
  } catch (e) {
41038
41344
  Object.assign(job, was);
41039
41345
  return {
41040
41346
  job_id: job.job_id,
41041
41347
  error: "record_not_saved",
41042
- message: `the verdict for "${job.job_id}" could not be written to the history (${e.message}); record it again`
41348
+ message: `the verdict for "${job.job_id}" could not be written to the history (${e instanceof Error ? e.message : e}); record it again`
41043
41349
  };
41044
41350
  }
41045
41351
  const summary = job.record && recordSummary(job);
@@ -41051,6 +41357,12 @@ var init_jobs = __esm({
41051
41357
  reflection_saved: job.reflection != null
41052
41358
  };
41053
41359
  }
41360
+ /**
41361
+ * @param {string} jobId
41362
+ * @param {number | undefined} waitMs
41363
+ * @param {{detail?: string | undefined}} [opts]
41364
+ * @param {AbortSignal} [signal]
41365
+ */
41054
41366
  async wait(jobId, waitMs, opts, signal) {
41055
41367
  const job = this.jobs.get(jobId);
41056
41368
  if (!job) return this.#waitOnDisk(() => this.view(jobId, opts), waitMs, signal);
@@ -41063,6 +41375,13 @@ var init_jobs = __esm({
41063
41375
  }
41064
41376
  // A consultation another process runs has no promise here, so its history
41065
41377
  // is re-read once a second until it is no longer running or the wait is up.
41378
+ /**
41379
+ * @template {{status: string}} T
41380
+ * @param {() => T | null} read
41381
+ * @param {number | undefined} waitMs
41382
+ * @param {AbortSignal} [signal]
41383
+ * @returns {Promise<T | null>}
41384
+ */
41066
41385
  async #waitOnDisk(read, waitMs, signal) {
41067
41386
  const until = Date.now() + Math.min(Math.max(0, waitMs ?? 0), POLICY.maxWaitMs);
41068
41387
  let view = read();
@@ -41085,6 +41404,7 @@ var init_jobs = __esm({
41085
41404
  }
41086
41405
  }, CANCEL_POLL_MS).unref();
41087
41406
  }
41407
+ /** @param {string} jobId */
41088
41408
  cancel(jobId) {
41089
41409
  const job = this.jobs.get(jobId);
41090
41410
  if (!job) {
@@ -41098,17 +41418,21 @@ var init_jobs = __esm({
41098
41418
  };
41099
41419
  }
41100
41420
  if (job.status !== "running" && job.status !== "queued") {
41101
- return { ...this.view(jobId), cancel_effect: `job was already ${job.status}` };
41421
+ return { ...this.#jobView(job, void 0), cancel_effect: `job was already ${job.status}` };
41102
41422
  }
41103
41423
  job.cancelRequested = true;
41104
41424
  for (const fn of job._cancelFns) fn();
41105
41425
  if (job._handle) job._handle.stop();
41106
- return { ...this.view(jobId), status: "cancelling", cancel_effect: "consultant process group signalled" };
41426
+ return { ...this.#jobView(job, void 0), status: "cancelling", cancel_effect: "consultant process group signalled" };
41107
41427
  }
41108
41428
  // Every field here is derived from the one `members` array it is handed, so a
41109
41429
  // group view can never report one status in `members` and another in
41110
41430
  // `comparison` or `status` for the same job.
41111
- /** @param {any} group @param {any[]} members @param {{detail?: string}} [opts] */
41431
+ /**
41432
+ * @param {Group} group
41433
+ * @param {JobView[]} members
41434
+ * @param {{detail?: string | undefined}} [opts]
41435
+ */
41112
41436
  #shapeGroup(group, members2, { detail } = {}) {
41113
41437
  const full = detail === "full";
41114
41438
  const cancelling = members2.some((m) => m.status === "cancelling");
@@ -41158,14 +41482,27 @@ var init_jobs = __esm({
41158
41482
  limits: live || !full ? null : limitsSummary()
41159
41483
  };
41160
41484
  const anyFinished = members2.some((m) => ["completed", "failed", "cancelled"].includes(m.status));
41161
- if (anyFinished) view.comparison = comparison(members2, full ? Infinity : GROUP_CHARS - printed(view));
41162
- return view;
41485
+ return anyFinished ? { ...view, comparison: comparison(members2, full ? Infinity : GROUP_CHARS - printed(view)) } : view;
41163
41486
  }
41487
+ /**
41488
+ * @param {string} groupId
41489
+ * @param {{detail?: string | undefined}} [opts]
41490
+ */
41164
41491
  groupView(groupId, opts) {
41165
41492
  const group = this.groups.get(groupId) ?? loadGroup(groupId);
41166
41493
  if (!group) return null;
41167
- return this.#shapeGroup(group, group.job_ids.map((jid) => this.view(jid, opts)).filter(Boolean), opts);
41494
+ return this.#shapeGroup(
41495
+ group,
41496
+ group.job_ids.map((jid) => this.view(jid, opts)).filter((v) => v != null),
41497
+ opts
41498
+ );
41168
41499
  }
41500
+ /**
41501
+ * @param {string} groupId
41502
+ * @param {number | undefined} waitMs
41503
+ * @param {{detail?: string | undefined}} [opts]
41504
+ * @param {AbortSignal} [signal]
41505
+ */
41169
41506
  async waitGroup(groupId, waitMs, opts, signal) {
41170
41507
  const group = this.groups.get(groupId);
41171
41508
  if (!group) return this.#waitOnDisk(() => this.groupView(groupId, opts), waitMs, signal);
@@ -41176,6 +41513,7 @@ var init_jobs = __esm({
41176
41513
  }
41177
41514
  return this.groupView(groupId, opts);
41178
41515
  }
41516
+ /** @param {string} groupId */
41179
41517
  cancelGroup(groupId) {
41180
41518
  const group = this.groups.get(groupId);
41181
41519
  if (!group) {
@@ -41191,7 +41529,7 @@ var init_jobs = __esm({
41191
41529
  const j = this.jobs.get(jid);
41192
41530
  return j && (j.status === "running" || j.status === "queued");
41193
41531
  }).length;
41194
- const members2 = group.job_ids.map((jid) => this.cancel(jid)).filter(Boolean);
41532
+ const members2 = group.job_ids.map((jid) => this.cancel(jid)).filter((v) => v != null);
41195
41533
  const total = group.job_ids.length;
41196
41534
  return {
41197
41535
  ...this.#shapeGroup(group, members2),
@@ -41242,7 +41580,7 @@ var init_jobs = __esm({
41242
41580
  MIN_TEXT_CHARS = 40;
41243
41581
  flagNames = (a) => {
41244
41582
  if (!a.startsWith("-") || a === "-") return [];
41245
- const name = a.split("=")[0];
41583
+ const [name = ""] = a.split("=");
41246
41584
  return a.startsWith("--") ? [name] : [name, a.slice(0, 2), `-${name}`];
41247
41585
  };
41248
41586
  }
@@ -41465,13 +41803,13 @@ function exportChain({ chain_id: chainId, group_id: groupId } = {}) {
41465
41803
  if (Boolean(chainId) === Boolean(groupId)) {
41466
41804
  throw new ExportError("pass exactly one of chain_id or group_id", "export_target_required");
41467
41805
  }
41468
- const chainIds = groupId ? chainsOfGroup(groupId) : [chainId];
41806
+ const chainIds = groupId ? chainsOfGroup(groupId) : chainId ? [chainId] : [];
41469
41807
  const perChain = chainIds.map((id2) => ({ chain_id: id2, rounds: roundsOf(id2) }));
41470
- const total = perChain.reduce((n, c) => n + c.rounds.length, 0);
41471
- if (total === 0) {
41808
+ const first = perChain.find((c) => c.rounds.length)?.rounds[0];
41809
+ if (!first) {
41472
41810
  throw new ExportError(`no consultation recorded under "${chainId ?? groupId}" in ~/.severally/history`, "unknown_chain");
41473
41811
  }
41474
- const first = perChain.find((c) => c.rounds.length)?.rounds[0];
41812
+ const total = perChain.reduce((n, c) => n + c.rounds.length, 0);
41475
41813
  const lines = [
41476
41814
  `# Consultation record: ${first.question}`,
41477
41815
  "",
@@ -41501,9 +41839,14 @@ function exportChain({ chain_id: chainId, group_id: groupId } = {}) {
41501
41839
  var ExportError, historyDir, bullets2;
41502
41840
  var init_export = __esm({
41503
41841
  "src/export.mjs"() {
41842
+ "use strict";
41504
41843
  init_policy();
41505
41844
  init_expose_paths();
41506
41845
  ExportError = class extends Error {
41846
+ /**
41847
+ * @param {string} message
41848
+ * @param {string} code
41849
+ */
41507
41850
  constructor(message, code) {
41508
41851
  super(message);
41509
41852
  this.name = "ExportError";
@@ -41540,6 +41883,7 @@ function staleInstall(home = os5.homedir(), severallyHome = POLICY.home) {
41540
41883
  // This release names it SEVERALLY_VERSION; older ones only had it in the server info.
41541
41884
  runtime: bundle?.match(/(?:SEVERALLY_VERSION = |name: ["']severally["'], version: )["'](\d+\.\d+\.\d+)["']/)?.[1],
41542
41885
  claude_skills_dir: (() => {
41886
+ if (plugin === null) return void 0;
41543
41887
  try {
41544
41888
  return JSON.parse(plugin).version;
41545
41889
  } catch {
@@ -41548,28 +41892,22 @@ function staleInstall(home = os5.homedir(), severallyHome = POLICY.home) {
41548
41892
  })()
41549
41893
  };
41550
41894
  const copies = { this_server: SEVERALLY_VERSION };
41551
- for (const [name, v] of Object.entries(found)) if (/^\d+\.\d+\.\d+$/.test(v ?? "")) copies[name] = v;
41895
+ for (const [name, v] of Object.entries(found)) if (v !== void 0 && /^\d+\.\d+\.\d+$/.test(v)) copies[name] = v;
41552
41896
  const versions = Object.values(copies);
41553
41897
  if (versions.every((v) => v === SEVERALLY_VERSION)) return null;
41554
41898
  const newest = versions.reduce((a, b) => older(a, b) ? b : a);
41555
41899
  return {
41556
41900
  copies,
41557
- behind: Object.keys(copies).filter((k) => older(copies[k], newest)),
41901
+ behind: Object.entries(copies).filter(([, v]) => older(v, newest)).map(([k]) => k),
41558
41902
  fix: `bring every copy to ${newest}: run \`npx -y severally-mcp@${newest} install\` (or \`node scripts/install.mjs\` from a checkout of that version) (it updates the runtime and the ~/.claude/skills copy), run \`/plugin marketplace update severally\` for a marketplace install, then restart the clients`
41559
41903
  };
41560
41904
  }
41561
41905
  function createServer(manager = new JobManager()) {
41562
41906
  const stale = staleInstall();
41563
41907
  const server = new McpServer({ name: "severally", version: SEVERALLY_VERSION }, { instructions: SERVER_INSTRUCTIONS });
41564
- const ok = (payload) => ({ content: [{ type: (
41565
- /** @type {const} */
41566
- "text"
41567
- ), text: JSON.stringify(payload, null, 2) }] });
41908
+ const ok = (payload) => ({ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] });
41568
41909
  const fail = (payload) => ({
41569
- content: [{ type: (
41570
- /** @type {const} */
41571
- "text"
41572
- ), text: JSON.stringify(payload, null, 2) }],
41910
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
41573
41911
  isError: true
41574
41912
  });
41575
41913
  server.registerTool(
@@ -41588,7 +41926,7 @@ function createServer(manager = new JobManager()) {
41588
41926
  if (err instanceof RequestError) {
41589
41927
  return fail({ error: err.code, message: err.message, details: err.details ?? null, limits: limitsSummary() });
41590
41928
  }
41591
- return fail({ error: "internal_error", message: String(err?.message ?? err) });
41929
+ return fail({ error: "internal_error", message: errorMessage2(err) });
41592
41930
  }
41593
41931
  }
41594
41932
  );
@@ -41631,9 +41969,15 @@ function createServer(manager = new JobManager()) {
41631
41969
  }, PROGRESS_EVERY_MS);
41632
41970
  let view;
41633
41971
  try {
41634
- view = group_id ? await manager.waitGroup(group_id, wait_ms ?? DEFAULT_WAIT_MS, { detail }, req?.signal) : await manager.wait(job_id, wait_ms ?? DEFAULT_WAIT_MS, { detail }, req?.signal);
41972
+ view = group_id ? await manager.waitGroup(group_id, wait_ms ?? DEFAULT_WAIT_MS, { detail }, req?.signal) : await manager.wait(
41973
+ /** @type {string} */
41974
+ job_id,
41975
+ wait_ms ?? DEFAULT_WAIT_MS,
41976
+ { detail },
41977
+ req?.signal
41978
+ );
41635
41979
  } finally {
41636
- clearInterval(timer);
41980
+ clearInterval(timer ?? void 0);
41637
41981
  }
41638
41982
  if (!view) {
41639
41983
  return fail({
@@ -41643,7 +41987,11 @@ function createServer(manager = new JobManager()) {
41643
41987
  }
41644
41988
  if (!ids || ["running", "queued", "cancelling"].includes(view.status)) return ok(view);
41645
41989
  try {
41646
- return ok(pointsOf(view, ids));
41990
+ return ok(pointsOf(
41991
+ /** @type {import('./jobs.mjs').JobView} */
41992
+ view,
41993
+ ids
41994
+ ));
41647
41995
  } catch (err) {
41648
41996
  if (err instanceof RequestError) return fail({ error: err.code, message: err.message });
41649
41997
  throw err;
@@ -41665,7 +42013,10 @@ function createServer(manager = new JobManager()) {
41665
42013
  if (Boolean(job_id) === Boolean(group_id)) {
41666
42014
  return fail({ error: "invalid_request", message: "pass exactly one of job_id or group_id" });
41667
42015
  }
41668
- const view = group_id ? manager.cancelGroup(group_id) : manager.cancel(job_id);
42016
+ const view = group_id ? manager.cancelGroup(group_id) : manager.cancel(
42017
+ /** @type {string} */
42018
+ job_id
42019
+ );
41669
42020
  if (!view) {
41670
42021
  return fail({
41671
42022
  error: "unknown_job",
@@ -41708,7 +42059,7 @@ function createServer(manager = new JobManager()) {
41708
42059
  if (err instanceof RequestError) {
41709
42060
  return fail({ error: err.code, message: err.message, details: err.details ?? null });
41710
42061
  }
41711
- return fail({ error: "internal_error", message: String(err?.message ?? err) });
42062
+ return fail({ error: "internal_error", message: errorMessage2(err) });
41712
42063
  }
41713
42064
  }
41714
42065
  );
@@ -41728,7 +42079,7 @@ function createServer(manager = new JobManager()) {
41728
42079
  return ok(exportChain({ chain_id, group_id }));
41729
42080
  } catch (err) {
41730
42081
  if (err instanceof ExportError) return fail({ error: err.code, message: err.message });
41731
- return fail({ error: "internal_error", message: String(err?.message ?? err) });
42082
+ return fail({ error: "internal_error", message: errorMessage2(err) });
41732
42083
  }
41733
42084
  }
41734
42085
  );
@@ -41779,9 +42130,10 @@ async function main() {
41779
42130
  process.on("exit", () => manager.killNow());
41780
42131
  serveStdio(() => createServer(manager).server);
41781
42132
  }
41782
- var SEVERALLY_VERSION, DEFAULT_WAIT_MS, PROGRESS_EVERY_MS, semver, older, SERVER_INSTRUCTIONS, startDescription, recordShape;
42133
+ var SEVERALLY_VERSION, DEFAULT_WAIT_MS, PROGRESS_EVERY_MS, semver, older, SERVER_INSTRUCTIONS, startDescription, recordShape, errorMessage2;
41783
42134
  var init_server = __esm({
41784
42135
  "src/server.mjs"() {
42136
+ "use strict";
41785
42137
  init_zod();
41786
42138
  init_dist();
41787
42139
  init_stdio();
@@ -41789,13 +42141,17 @@ var init_server = __esm({
41789
42141
  init_schema();
41790
42142
  init_jobs();
41791
42143
  init_export();
41792
- SEVERALLY_VERSION = "2.4.0";
42144
+ SEVERALLY_VERSION = "2.5.0";
41793
42145
  DEFAULT_WAIT_MS = process.env.SEVERALLY_MAX_WAIT_MS ? POLICY.maxWaitMs : Math.min(POLICY.maxWaitMs, 45e3);
41794
42146
  PROGRESS_EVERY_MS = 15e3;
41795
42147
  semver = (v) => String(v).split(".").map(Number);
41796
42148
  older = (a, b) => {
41797
42149
  const [x, y] = [semver(a), semver(b)];
41798
- for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i];
42150
+ for (let i = 0; i < 3; i++) {
42151
+ const [p, q] = [x[i], y[i]];
42152
+ if (p === void 0 || q === void 0) return false;
42153
+ if (p !== q) return p < q;
42154
+ }
41799
42155
  return false;
41800
42156
  };
41801
42157
  SERVER_INSTRUCTIONS = `severally lets you get a genuinely independent opinion from another coding agent:
@@ -41858,6 +42214,7 @@ Model, permissions, rounds, timeout and size caps are fixed by this server.`;
41858
42214
  related_item_ids: external_exports.array(external_exports.string().min(1)).max(50).optional().describe("f1, u1, c1 ...")
41859
42215
  }).optional().describe("written after reading the answer")
41860
42216
  };
42217
+ errorMessage2 = (err) => String((typeof err === "object" && err !== null && "message" in err ? err.message : void 0) ?? err);
41861
42218
  }
41862
42219
  });
41863
42220
 
@@ -41875,7 +42232,7 @@ function registration({ cmd, checks }) {
41875
42232
  ran = true;
41876
42233
  if (/(^|[\s@:])severally([\s@:]|$)/m.test(out)) return "registered (listed by the client)";
41877
42234
  } catch (err) {
41878
- if (typeof err.status === "number") ran = true;
42235
+ if (typeof thrown(err, "status") === "number") ran = true;
41879
42236
  }
41880
42237
  }
41881
42238
  return ran ? "not registered" : `could not tell (${cmd} ${checks.map((a) => a.join(" ")).join(" / ")} failed)`;
@@ -41888,9 +42245,9 @@ function login(cli) {
41888
42245
  let ok = true;
41889
42246
  try {
41890
42247
  out = execCommandSync(check2.cmd, check2.args, { encoding: "utf8", timeout: 2e4, stdio: ["ignore", "pipe", "pipe"] });
41891
- } catch (err) {
41892
- if (typeof err.status !== "number") return `could not tell (${check2.cmd} ${check2.args.join(" ")} failed)`;
41893
- out = `${err.stdout ?? ""}`;
42248
+ } catch (e) {
42249
+ if (typeof thrown(e, "status") !== "number") return `could not tell (${check2.cmd} ${check2.args.join(" ")} failed)`;
42250
+ out = `${thrown(e, "stdout") ?? ""}`;
41894
42251
  ok = false;
41895
42252
  }
41896
42253
  try {
@@ -41928,9 +42285,10 @@ function status() {
41928
42285
  `);
41929
42286
  return problem ? 1 : 0;
41930
42287
  }
41931
- var CLIENTS, LOGIN;
42288
+ var CLIENTS, thrown, LOGIN;
41932
42289
  var init_status = __esm({
41933
42290
  "src/status.mjs"() {
42291
+ "use strict";
41934
42292
  init_policy();
41935
42293
  init_server();
41936
42294
  init_platform();
@@ -41954,6 +42312,7 @@ var init_status = __esm({
41954
42312
  { label: "Antigravity", cmd: "agy", checks: [["mcp", "list"]] },
41955
42313
  { label: "OpenCode", cmd: "opencode", checks: [["mcp", "list"]] }
41956
42314
  ];
42315
+ thrown = (e, key) => typeof e === "object" && e !== null ? Reflect.get(e, key) : void 0;
41957
42316
  LOGIN = {
41958
42317
  "claude-code": { cmd: "claude", args: ["auth", "status", "--json"], read: (out) => JSON.parse(out).loggedIn === true },
41959
42318
  // Codex prints its answer on stderr; the exit status carries the verdict.