@kaiba-cloud/orbital 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10421,7 +10421,7 @@ var require_compile = __commonJS({
10421
10421
  const schOrFunc = root.refs[ref];
10422
10422
  if (schOrFunc)
10423
10423
  return schOrFunc;
10424
- let _sch = resolve25.call(this, root, ref);
10424
+ let _sch = resolve26.call(this, root, ref);
10425
10425
  if (_sch === void 0) {
10426
10426
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
10427
10427
  const { schemaId } = this.opts;
@@ -10448,7 +10448,7 @@ var require_compile = __commonJS({
10448
10448
  function sameSchemaEnv(s1, s2) {
10449
10449
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
10450
10450
  }
10451
- function resolve25(root, ref) {
10451
+ function resolve26(root, ref) {
10452
10452
  let sch;
10453
10453
  while (typeof (sch = this.refs[ref]) == "string")
10454
10454
  ref = sch;
@@ -11312,7 +11312,7 @@ var require_fast_uri = __commonJS({
11312
11312
  }
11313
11313
  return uri;
11314
11314
  }
11315
- function resolve25(baseURI, relativeURI, options) {
11315
+ function resolve26(baseURI, relativeURI, options) {
11316
11316
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
11317
11317
  const {
11318
11318
  parsed: baseParsed,
@@ -11703,7 +11703,7 @@ var require_fast_uri = __commonJS({
11703
11703
  var fastUri = {
11704
11704
  SCHEMES,
11705
11705
  normalize,
11706
- resolve: resolve25,
11706
+ resolve: resolve26,
11707
11707
  resolveComponent,
11708
11708
  equal,
11709
11709
  serialize,
@@ -14786,12 +14786,12 @@ var require_isexe = __commonJS({
14786
14786
  if (typeof Promise !== "function") {
14787
14787
  throw new TypeError("callback not provided");
14788
14788
  }
14789
- return new Promise(function(resolve25, reject) {
14789
+ return new Promise(function(resolve26, reject) {
14790
14790
  isexe(path3, options || {}, function(er, is) {
14791
14791
  if (er) {
14792
14792
  reject(er);
14793
14793
  } else {
14794
- resolve25(is);
14794
+ resolve26(is);
14795
14795
  }
14796
14796
  });
14797
14797
  });
@@ -14857,27 +14857,27 @@ var require_which = __commonJS({
14857
14857
  opt = {};
14858
14858
  const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt);
14859
14859
  const found = [];
14860
- const step = (i) => new Promise((resolve25, reject) => {
14860
+ const step = (i) => new Promise((resolve26, reject) => {
14861
14861
  if (i === pathEnv.length)
14862
- return opt.all && found.length ? resolve25(found) : reject(getNotFoundError(cmd));
14862
+ return opt.all && found.length ? resolve26(found) : reject(getNotFoundError(cmd));
14863
14863
  const ppRaw = pathEnv[i];
14864
14864
  const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
14865
14865
  const pCmd = path3.join(pathPart, cmd);
14866
14866
  const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
14867
- resolve25(subStep(p, i, 0));
14867
+ resolve26(subStep(p, i, 0));
14868
14868
  });
14869
- const subStep = (p, i, ii) => new Promise((resolve25, reject) => {
14869
+ const subStep = (p, i, ii) => new Promise((resolve26, reject) => {
14870
14870
  if (ii === pathExt.length)
14871
- return resolve25(step(i + 1));
14871
+ return resolve26(step(i + 1));
14872
14872
  const ext = pathExt[ii];
14873
14873
  isexe(p + ext, { pathExt: pathExtExe }, (er, is) => {
14874
14874
  if (!er && is) {
14875
14875
  if (opt.all)
14876
14876
  found.push(p + ext);
14877
14877
  else
14878
- return resolve25(p + ext);
14878
+ return resolve26(p + ext);
14879
14879
  }
14880
- return resolve25(subStep(p, i, ii + 1));
14880
+ return resolve26(subStep(p, i, ii + 1));
14881
14881
  });
14882
14882
  });
14883
14883
  return cb ? step(0).then((res) => cb(null, res), cb) : step(0);
@@ -15190,7 +15190,7 @@ var require_cross_spawn = __commonJS({
15190
15190
  });
15191
15191
 
15192
15192
  // src/kit/command/brand.ts
15193
- var VERSION = true ? "0.0.4" : "0.0.0-dev";
15193
+ var VERSION = true ? "0.0.5" : "0.0.0-dev";
15194
15194
  function defineBrand(overrides) {
15195
15195
  const { program } = overrides;
15196
15196
  return {
@@ -20652,7 +20652,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
20652
20652
  var NONE = 0;
20653
20653
  var ASSUMED = 1;
20654
20654
  var PROVEN = 2;
20655
- function isRecursive(inst, stack, resolve25) {
20655
+ function isRecursive(inst, stack, resolve26) {
20656
20656
  const cached2 = recursive.get(inst);
20657
20657
  if (cached2 !== void 0)
20658
20658
  return cached2 ? PROVEN : NONE;
@@ -20662,7 +20662,7 @@ function isRecursive(inst, stack, resolve25) {
20662
20662
  let result2 = NONE;
20663
20663
  const check3 = (child) => {
20664
20664
  if (result2 !== PROVEN && child?._zod) {
20665
- const answer2 = isRecursive(child, stack, resolve25);
20665
+ const answer2 = isRecursive(child, stack, resolve26);
20666
20666
  if (answer2 > result2)
20667
20667
  result2 = answer2;
20668
20668
  }
@@ -20673,7 +20673,7 @@ function isRecursive(inst, stack, resolve25) {
20673
20673
  const desc = Object.getOwnPropertyDescriptor(sh, key2);
20674
20674
  if (spread && !desc.enumerable)
20675
20675
  continue;
20676
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve25) : NONE;
20676
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve26) : NONE;
20677
20677
  if (child > answer2)
20678
20678
  answer2 = child;
20679
20679
  }
@@ -20737,7 +20737,7 @@ function isRecursive(inst, stack, resolve25) {
20737
20737
  break;
20738
20738
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
20739
20739
  case "lazy": {
20740
- const inner = def._cachedInner ?? (resolve25 ? inst._zod.innerType : void 0);
20740
+ const inner = def._cachedInner ?? (resolve26 ? inst._zod.innerType : void 0);
20741
20741
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
20742
20742
  break;
20743
20743
  }
@@ -35891,7 +35891,7 @@ var MissionAskedSchema = external_exports.object({
35891
35891
  question: external_exports.string().min(1),
35892
35892
  at: external_exports.string()
35893
35893
  });
35894
- function waitsOnGiver(mission) {
35894
+ function waitsOnDecision(mission) {
35895
35895
  return mission.state === "waiting" || mission.state === "active" && mission.questions.length > 0;
35896
35896
  }
35897
35897
  var MISSION_QUESTION_META = "orbital/question";
@@ -35929,7 +35929,7 @@ var MissionStateFileSchema = external_exports.object({
35929
35929
  source: external_exports.string().nullable(),
35930
35930
  result: external_exports.string().nullable(),
35931
35931
  question: external_exports.string().nullable(),
35932
- giver: external_exports.string().nullable(),
35932
+ receivers: external_exports.array(external_exports.string().min(1)).default([]),
35933
35933
  lead: external_exports.string().nullable(),
35934
35934
  claim: external_exports.string().nullable(),
35935
35935
  claimedAt: external_exports.string().nullable(),
@@ -35956,7 +35956,7 @@ var MissionRecordSchema = external_exports.object({
35956
35956
  result: external_exports.string().nullable(),
35957
35957
  question: external_exports.string().nullable(),
35958
35958
  questions: external_exports.array(MissionAskedSchema).default([]),
35959
- giver: external_exports.string().nullable(),
35959
+ receivers: external_exports.array(external_exports.string().min(1)).default([]),
35960
35960
  lead: external_exports.string().nullable(),
35961
35961
  lastActivity: external_exports.string().nullable().default(null),
35962
35962
  claim: external_exports.string().nullable().default(null),
@@ -35998,9 +35998,9 @@ var MissionReplySchema = external_exports.object({
35998
35998
  text: external_exports.string().min(1),
35999
35999
  questionId: external_exports.string().min(1).optional()
36000
36000
  });
36001
- var RemoteGiverSchema = external_exports.object({ node: external_exports.string().min(1), sessionId: external_exports.string().min(1) });
36001
+ var RemoteReceiverSchema = external_exports.object({ node: external_exports.string().min(1), sessionId: external_exports.string().min(1) });
36002
36002
  var SENDER_FIELDS = { team: external_exports.string().min(1).nullable().default(null), agent: external_exports.string().min(1).nullable().default(null) };
36003
- var RelayJoinSchema = external_exports.object({ missionId: MissionIdSchema, giver: RemoteGiverSchema, ...SENDER_FIELDS });
36003
+ var RelayJoinSchema = external_exports.object({ missionId: MissionIdSchema, receiver: RemoteReceiverSchema, ...SENDER_FIELDS });
36004
36004
  var RelayReplySchema = external_exports.object({ missionId: MissionIdSchema, text: external_exports.string().min(1), questionId: external_exports.string().min(1).optional(), by: external_exports.string().min(1), ...SENDER_FIELDS });
36005
36005
  var MissionControlSchema = external_exports.object({ missionId: MissionIdSchema });
36006
36006
  var STEER_MARK = "[orbital] message during your turn";
@@ -36034,18 +36034,27 @@ var MissionQuestionSchema = external_exports.object({
36034
36034
  leadSessionId: external_exports.string().min(1),
36035
36035
  question: external_exports.string(),
36036
36036
  questionId: external_exports.string().min(1).optional(),
36037
- giver: RemoteGiverSchema,
36037
+ receiver: RemoteReceiverSchema,
36038
36038
  team: external_exports.string().min(1).nullable().default(null)
36039
36039
  });
36040
- function giverRef(giver) {
36041
- return `${giver.sessionId}@${giver.node}`;
36040
+ var EXTERNAL_RECEIVER = "external:";
36041
+ function externalReceiver(externalId) {
36042
+ return `${EXTERNAL_RECEIVER}${externalId}`;
36042
36043
  }
36043
- function remoteGiverOf(giver) {
36044
- const at = giver.lastIndexOf("@");
36045
- return at > 0 && at < giver.length - 1 ? { sessionId: giver.slice(0, at), node: giver.slice(at + 1) } : null;
36044
+ function externalIdOf(receiver) {
36045
+ return receiver.startsWith(EXTERNAL_RECEIVER) ? receiver.slice(EXTERNAL_RECEIVER.length) : null;
36046
+ }
36047
+ function receiverRef(receiver) {
36048
+ return `${receiver.sessionId}@${receiver.node}`;
36049
+ }
36050
+ function remoteReceiverOf(receiver) {
36051
+ if (externalIdOf(receiver) !== null) return null;
36052
+ const at = receiver.lastIndexOf("@");
36053
+ return at > 0 && at < receiver.length - 1 ? { sessionId: receiver.slice(0, at), node: receiver.slice(at + 1) } : null;
36046
36054
  }
36047
36055
  var MissionJoinSchema = external_exports.object({
36048
- missionId: MissionIdSchema
36056
+ missionId: MissionIdSchema,
36057
+ externalId: external_exports.string().min(1).optional()
36049
36058
  });
36050
36059
 
36051
36060
  // src/protocol/charter.ts
@@ -36183,7 +36192,7 @@ var MissionStateEventSchema = external_exports.object({
36183
36192
  transition: MissionTransitionSchema,
36184
36193
  reason: external_exports.string().nullable(),
36185
36194
  by: external_exports.string(),
36186
- giver: external_exports.string().nullable(),
36195
+ receivers: external_exports.array(external_exports.string()).default([]),
36187
36196
  sessionId: external_exports.string().nullable(),
36188
36197
  lead: external_exports.string().nullable(),
36189
36198
  question: external_exports.string().nullable(),
@@ -36237,7 +36246,7 @@ var TelemetrySnapshotSchema = external_exports.object({
36237
36246
  state: MissionStateSchema,
36238
36247
  sessionId: external_exports.string().nullable(),
36239
36248
  lead: external_exports.string().nullable(),
36240
- giver: external_exports.string().nullable(),
36249
+ receivers: external_exports.array(external_exports.string()).default([]),
36241
36250
  givenBy: external_exports.string().nullable(),
36242
36251
  question: external_exports.string().nullable(),
36243
36252
  questions: external_exports.array(MissionAskedSchema).default([]),
@@ -36831,6 +36840,9 @@ var NodeClient = class {
36831
36840
  async joinMission(missionId) {
36832
36841
  return MissionRecordSchema.parse(await this.request("/orbital/missions/join", { method: "POST", body: { missionId } }));
36833
36842
  }
36843
+ async leaveMission(missionId) {
36844
+ return MissionRecordSchema.parse(await this.request("/orbital/missions/leave", { method: "POST", body: { missionId } }));
36845
+ }
36834
36846
  async handoff(handoff2) {
36835
36847
  return MissionRecordSchema.parse(await this.request("/orbital/missions/handoff", { method: "POST", body: handoff2 }));
36836
36848
  }
@@ -37472,6 +37484,13 @@ var DeliverySchema = external_exports.object({
37472
37484
  question: external_exports.string().nullable().default(null),
37473
37485
  text: external_exports.string()
37474
37486
  });
37487
+ var AnsweredSchema = external_exports.object({
37488
+ kind: external_exports.literal("answered"),
37489
+ mission: external_exports.string().min(1),
37490
+ question: external_exports.string().nullable(),
37491
+ by: external_exports.string().min(1),
37492
+ text: external_exports.string()
37493
+ });
37475
37494
  var SettleSchema = external_exports.object({
37476
37495
  taskId: external_exports.string().min(1).optional(),
37477
37496
  text: external_exports.string()
@@ -37717,6 +37736,60 @@ var PORT_METHODS = {
37717
37736
  reasoner: ["extractModule", "classify", "align", "checkConsistency"],
37718
37737
  missions: ["join", "reply", "pause", "resume", "ask", "say"]
37719
37738
  };
37739
+ var PLUGIN_COMMAND_RESERVED_OPTIONS = ["output", "quiet", "non-interactive", "confirm", "timeout", "help", "version", "workspace", "node"];
37740
+ var PluginCommandOptionSchema = external_exports.object({
37741
+ name: external_exports.string().regex(NAME_SHAPE).refine((name) => !PLUGIN_COMMAND_RESERVED_OPTIONS.includes(name), { error: `an option cannot take a name orbital gives every command: ${PLUGIN_COMMAND_RESERVED_OPTIONS.join(", ")}` }),
37742
+ type: external_exports.enum(["string", "boolean", "integer", "enum", "path"]),
37743
+ summary: external_exports.string().min(1),
37744
+ values: external_exports.array(external_exports.string().min(1)).min(1).optional(),
37745
+ required: external_exports.boolean().default(false),
37746
+ default: external_exports.union([external_exports.string(), external_exports.number().int(), external_exports.boolean()]).optional()
37747
+ }).strict().superRefine((option, issues2) => {
37748
+ if (option.type === "enum" !== (option.values !== void 0)) issues2.addIssue({ code: "custom", message: `the option ${option.name} names values only when its type is enum` });
37749
+ if (option.type === "boolean" && option.required) issues2.addIssue({ code: "custom", message: `the boolean option ${option.name} cannot be required` });
37750
+ if (option.default === void 0) return;
37751
+ const fits = option.type === "boolean" ? typeof option.default === "boolean" : option.type === "integer" ? typeof option.default === "number" : typeof option.default === "string" && (option.values?.includes(option.default) ?? true);
37752
+ if (!fits) issues2.addIssue({ code: "custom", message: `the default of the option ${option.name} is not a ${option.type === "enum" ? `value of ${option.values.join(", ")}` : option.type}` });
37753
+ });
37754
+ var PluginCommandArgSchema = external_exports.object({
37755
+ name: external_exports.string().regex(NAME_SHAPE),
37756
+ summary: external_exports.string().min(1),
37757
+ required: external_exports.boolean().default(false),
37758
+ variadic: external_exports.boolean().default(false)
37759
+ }).strict();
37760
+ var PluginCommandSchema = external_exports.object({
37761
+ name: external_exports.string().regex(NAME_SHAPE),
37762
+ summary: external_exports.string().min(1),
37763
+ args: external_exports.array(PluginCommandArgSchema).default([]),
37764
+ options: external_exports.array(PluginCommandOptionSchema).default([]),
37765
+ interactive: external_exports.boolean().default(false),
37766
+ node: external_exports.boolean().default(false)
37767
+ }).strict().superRefine((command, issues2) => {
37768
+ const twice = (names) => names.find((name, index) => names.indexOf(name) !== index);
37769
+ const arg = twice(command.args.map((one) => one.name));
37770
+ if (arg) issues2.addIssue({ code: "custom", message: `the command ${command.name} names the argument ${arg} twice` });
37771
+ const option = twice(command.options.map((one) => one.name));
37772
+ if (option) issues2.addIssue({ code: "custom", message: `the command ${command.name} names the option ${option} twice` });
37773
+ if (command.args.slice(0, -1).some((one) => one.variadic)) issues2.addIssue({ code: "custom", message: `only the last argument of the command ${command.name} can be variadic` });
37774
+ if (command.args.some((one, index) => one.required && command.args.slice(0, index).some((before) => !before.required))) issues2.addIssue({ code: "custom", message: `a required argument of the command ${command.name} cannot come after an optional one` });
37775
+ if (command.interactive && command.node) issues2.addIssue({ code: "custom", message: `the command ${command.name} cannot be interactive and need the node: the node has no terminal to give it` });
37776
+ });
37777
+ var PluginCommandsSchema = external_exports.array(PluginCommandSchema).default([]).superRefine((commands, issues2) => {
37778
+ const names = commands.map((one) => one.name);
37779
+ const twice = names.find((name, index) => names.indexOf(name) !== index);
37780
+ if (twice) issues2.addIssue({ code: "custom", message: `the command ${twice} is declared twice` });
37781
+ });
37782
+ var PluginCommandRequestSchema = external_exports.object({
37783
+ name: external_exports.string().regex(NAME_SHAPE),
37784
+ args: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.array(external_exports.string())])).default({}),
37785
+ options: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()])).default({}),
37786
+ terminal: external_exports.object({ tty: external_exports.boolean() }).nullable().default(null)
37787
+ }).strict();
37788
+ var PluginCommandResultSchema = external_exports.object({
37789
+ exitCode: external_exports.union([external_exports.literal(0), external_exports.literal(1), external_exports.literal(2), external_exports.literal(3), external_exports.literal(5)]).default(0),
37790
+ text: external_exports.string().nullable().default(null),
37791
+ data: external_exports.record(external_exports.string(), external_exports.unknown()).default({})
37792
+ }).strict();
37720
37793
  var PluginManifestSchema = external_exports.object({
37721
37794
  name: external_exports.string().regex(NAME_SHAPE),
37722
37795
  version: external_exports.string().min(1),
@@ -37746,7 +37819,7 @@ var PluginManifestSchema = external_exports.object({
37746
37819
  secrets: external_exports.array(external_exports.string().min(1)).default([]),
37747
37820
  models: external_exports.array(external_exports.string().regex(/^[a-z0-9-]+:.+$/, "a model pattern is <provider>:<model>, such as anthropic:*")).default([])
37748
37821
  }).prefault({}),
37749
- commands: external_exports.array(external_exports.object({ name: external_exports.string().regex(NAME_SHAPE), summary: external_exports.string() })).default([]),
37822
+ commands: PluginCommandsSchema,
37750
37823
  panels: external_exports.array(external_exports.object({ id: external_exports.string().min(1), title: external_exports.string(), kind: external_exports.enum(["list", "tree", "transcript", "form"]) })).default([])
37751
37824
  }).strict();
37752
37825
  var PluginInitSchema = external_exports.object({
@@ -37769,7 +37842,8 @@ var PluginPackageSchema = external_exports.object({
37769
37842
  dir: external_exports.string().min(1).optional(),
37770
37843
  env: external_exports.array(external_exports.string().regex(PLUGIN_ENV_NAME, "is not the name of an environment variable")).default([]),
37771
37844
  config: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
37772
- harnesses: external_exports.array(HarnessAdapterDeclarationSchema).default([])
37845
+ harnesses: external_exports.array(HarnessAdapterDeclarationSchema).default([]),
37846
+ commands: PluginCommandsSchema
37773
37847
  }).strict();
37774
37848
  var PLUGIN_PACKAGE_FILE = "orbital-plugin.json";
37775
37849
  var MarketplaceSchema = external_exports.object({
@@ -37815,12 +37889,12 @@ var RpcPeer = class {
37815
37889
  request(method, params, timeoutMs = this.timeoutMs) {
37816
37890
  if (this.closedWith) return Promise.reject(this.closedWith);
37817
37891
  const id = this.nextId++;
37818
- return new Promise((resolve25, reject) => {
37892
+ return new Promise((resolve26, reject) => {
37819
37893
  const timer = timeoutMs > 0 ? setTimeout(() => {
37820
37894
  this.pending.delete(id);
37821
37895
  reject(new RpcError(RPC_ERRORS.internal, `${method} got no answer in ${timeoutMs} ms`));
37822
37896
  }, timeoutMs) : null;
37823
- this.pending.set(id, { resolve: resolve25, reject, timer });
37897
+ this.pending.set(id, { resolve: resolve26, reject, timer });
37824
37898
  this.write({ jsonrpc: "2.0", id, method, ...params !== void 0 && { params } });
37825
37899
  });
37826
37900
  }
@@ -38915,7 +38989,7 @@ function installPlugin(home, ref, exec2 = run) {
38915
38989
  const installed = Object.keys(JSON.parse(readFileSync7(join10(scratch, "package.json"), "utf8")).dependencies ?? {});
38916
38990
  const pkgDir = join10("node_modules", ...(installed[0] ?? ref.pkg.replace(/(.)@[^/]*$/, "$1")).split("/"));
38917
38991
  const inner = PluginPackageSchema.parse(JSON.parse(readFileSync7(join10(scratch, pkgDir, PLUGIN_PACKAGE_FILE), "utf8")));
38918
- writeFileSync5(join10(scratch, PLUGIN_PACKAGE_FILE), `${JSON.stringify({ name: ref.name, command: inner.command, dir: inner.dir ? join10(pkgDir, inner.dir) : pkgDir, env: inner.env, ...Object.keys(inner.config).length > 0 && { config: inner.config }, ...inner.harnesses.length > 0 && { harnesses: inner.harnesses } }, null, 2)}
38992
+ writeFileSync5(join10(scratch, PLUGIN_PACKAGE_FILE), `${JSON.stringify({ name: ref.name, command: inner.command, dir: inner.dir ? join10(pkgDir, inner.dir) : pkgDir, env: inner.env, ...Object.keys(inner.config).length > 0 && { config: inner.config }, ...inner.harnesses.length > 0 && { harnesses: inner.harnesses }, ...inner.commands.length > 0 && { commands: inner.commands } }, null, 2)}
38919
38993
  `);
38920
38994
  break;
38921
38995
  }
@@ -38981,15 +39055,22 @@ function ensurePluginInstalled(home, name, source, exec2 = run) {
38981
39055
  function pluginsDir(home) {
38982
39056
  return join11(home, "plugins");
38983
39057
  }
39058
+ function isLocalSource(source) {
39059
+ return source.startsWith(".") || isAbsolute5(source);
39060
+ }
39061
+ function pluginDir(name, source, roots) {
39062
+ if (!isLocalSource(source)) return join11(pluginsDir(roots.home), name);
39063
+ return isAbsolute5(source) ? source : resolve7(roots.harnessRoot ?? process.cwd(), source);
39064
+ }
38984
39065
  function commandOf(name, source, roots, entryEnv = []) {
38985
39066
  if (source.startsWith("exec:")) {
38986
39067
  const [command2, ...args2] = source.slice("exec:".length).trim().split(/\s+/);
38987
39068
  if (!command2) throw new Error(`The plugin ${name}'s source "${source}" names no command.`);
38988
- return { command: command2, args: args2, cwd: roots.harnessRoot ?? process.cwd(), env: [...entryEnv], defaults: {}, harnesses: null };
39069
+ return { command: command2, args: args2, cwd: roots.harnessRoot ?? process.cwd(), env: [...entryEnv], defaults: {}, harnesses: null, commands: null };
38989
39070
  }
38990
39071
  if (entryEnv.length > 0) throw new Error(`The plugin ${name}'s ${ORBITAL_FILE} entry names env:, and only an exec: source does: its ${PLUGIN_PACKAGE_FILE} declares the variables it reads.`);
38991
- const local = source.startsWith(".") || isAbsolute5(source);
38992
- const dir = local ? isAbsolute5(source) ? source : resolve7(roots.harnessRoot ?? process.cwd(), source) : join11(pluginsDir(roots.home), name);
39072
+ const local = isLocalSource(source);
39073
+ const dir = pluginDir(name, source, roots);
38993
39074
  const descriptor = join11(dir, PLUGIN_PACKAGE_FILE);
38994
39075
  if (!local && !existsSync7(descriptor) && /^(npm|git):/.test(source)) ensurePluginInstalled(roots.home, name, source);
38995
39076
  if (!existsSync7(descriptor)) {
@@ -38997,7 +39078,7 @@ function commandOf(name, source, roots, entryEnv = []) {
38997
39078
  }
38998
39079
  const plugin = PluginPackageSchema.parse(JSON.parse(readFileSync8(descriptor, "utf8")));
38999
39080
  const [command, ...args] = plugin.command;
39000
- return { command: command === "node" ? process.execPath : command, args, cwd: plugin.dir ? resolve7(dir, plugin.dir) : dir, env: plugin.env, defaults: plugin.config, harnesses: plugin.harnesses };
39081
+ return { command: command === "node" ? process.execPath : command, args, cwd: plugin.dir ? resolve7(dir, plugin.dir) : dir, env: plugin.env, defaults: plugin.config, harnesses: plugin.harnesses, commands: plugin.commands };
39001
39082
  }
39002
39083
  var PLUGIN_ENV = [
39003
39084
  "PATH",
@@ -39042,6 +39123,15 @@ function folderMismatch(manifest, folders) {
39042
39123
  const extra = folders.find((one) => !manifest.provides.harnesses.some((harness) => harness.id === one.id && harness.folder !== null));
39043
39124
  return extra ? `declares the harness ${extra.id} in its ${PLUGIN_PACKAGE_FILE}, and its harnesses port serves no folder for it` : null;
39044
39125
  }
39126
+ function commandMismatch(manifest, declared) {
39127
+ for (const command of manifest.commands) {
39128
+ const same2 = declared.find((one) => one.name === command.name);
39129
+ if (!same2) return `serves the command ${command.name}, and its ${PLUGIN_PACKAGE_FILE} declares no command ${command.name}`;
39130
+ if (JSON.stringify(same2) !== JSON.stringify(command)) return `declares the command ${command.name} one way in its ${PLUGIN_PACKAGE_FILE} and another way in its code`;
39131
+ }
39132
+ const extra = declared.find((one) => !manifest.commands.some((command) => command.name === one.name));
39133
+ return extra ? `declares the command ${extra.name} in its ${PLUGIN_PACKAGE_FILE}, and serves no command ${extra.name}` : null;
39134
+ }
39045
39135
  function manifestEnv(manifest) {
39046
39136
  return manifest.permissions.secrets.filter((name) => PLUGIN_ENV_NAME.test(name));
39047
39137
  }
@@ -39070,15 +39160,16 @@ async function startRemotePlugin(options) {
39070
39160
  let reconnecting = false;
39071
39161
  const connect = async () => {
39072
39162
  const start = commandOf(options.name, options.source, options.roots, options.entryEnv);
39073
- const child = spawn2(start.command, start.args, { cwd: start.cwd, env: pluginProcessEnv(options.env, start.env), stdio: ["pipe", "pipe", "pipe"] });
39163
+ const terminal = options.terminal ? [options.terminal.input, options.terminal.output, options.terminal.error] : [];
39164
+ const child = spawn2(start.command, start.args, { cwd: start.cwd, env: pluginProcessEnv(options.env, start.env), stdio: ["pipe", "pipe", "pipe", ...terminal] });
39074
39165
  try {
39075
- return await initialize(child, start.env, start.defaults, start.harnesses);
39166
+ return await initialize(child, start.env, start.defaults, start.harnesses, start.commands);
39076
39167
  } catch (error62) {
39077
39168
  child.kill("SIGKILL");
39078
39169
  throw error62;
39079
39170
  }
39080
39171
  };
39081
- const initialize = async (child, env, defaults, folders = null) => {
39172
+ const initialize = async (child, env, defaults, folders, commands) => {
39082
39173
  const peer = new RpcPeer({ send: (line) => child.stdin?.write(`${line}
39083
39174
  `) });
39084
39175
  createInterface2({ input: child.stdout }).on("line", (line) => peer.receive(line));
@@ -39113,6 +39204,10 @@ async function startRemotePlugin(options) {
39113
39204
  const where2 = options.source.startsWith("exec:") ? `its ${ORBITAL_FILE} entry` : `its ${PLUGIN_PACKAGE_FILE}`;
39114
39205
  throw new Error(`The plugin ${options.name} declares the variables [${declared.join(", ")}] as secrets, and ${where2} declares env: [${env.join(", ")}]. Make them the same.`);
39115
39206
  }
39207
+ if (commands) {
39208
+ const mismatch = commandMismatch(manifest2, commands);
39209
+ if (mismatch) throw new Error(`The plugin ${options.name} ${mismatch}. The commands of its ${PLUGIN_PACKAGE_FILE} and the commands of its code must be the same.`);
39210
+ }
39116
39211
  if (options.offline && manifest2.permissions.network.length > 0) {
39117
39212
  throw new Error(`The plugin ${options.name} reaches ${manifest2.permissions.network.join(", ")}, and ${ORBITAL_FILE} says offline: true.`);
39118
39213
  }
@@ -39298,7 +39393,7 @@ async function startRemotePlugin(options) {
39298
39393
  endpoint = given;
39299
39394
  void current2.then((connection) => connection.peer.notify(PLUGIN_METHODS.hostEndpoint, given));
39300
39395
  },
39301
- command: async (name, args) => (await call(PLUGIN_METHODS.commandRun, { name, args })).output,
39396
+ command: async (request) => PluginCommandResultSchema.parse(await call(PLUGIN_METHODS.commandRun, request)),
39302
39397
  close: async () => {
39303
39398
  stopped = true;
39304
39399
  const connection = await current2.catch(() => null);
@@ -40653,7 +40748,7 @@ var readBodyDirect = (request) => {
40653
40748
  request[bodyBufferKey] = buffered;
40654
40749
  return Promise.resolve(buffered);
40655
40750
  }
40656
- const promise2 = new Promise((resolve25, reject) => {
40751
+ const promise2 = new Promise((resolve26, reject) => {
40657
40752
  const chunks = [];
40658
40753
  let settled = false;
40659
40754
  const finish = (callback) => {
@@ -40671,7 +40766,7 @@ var readBodyDirect = (request) => {
40671
40766
  else if (recovered === void 0) reject(error62 ?? normalizeAbortError(request, incoming));
40672
40767
  else {
40673
40768
  request[bodyBufferKey] = recovered;
40674
- resolve25(recovered);
40769
+ resolve26(recovered);
40675
40770
  }
40676
40771
  });
40677
40772
  return true;
@@ -40683,7 +40778,7 @@ var readBodyDirect = (request) => {
40683
40778
  finish(() => {
40684
40779
  const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks);
40685
40780
  request[bodyBufferKey] = buffer;
40686
- resolve25(buffer);
40781
+ resolve26(buffer);
40687
40782
  });
40688
40783
  };
40689
40784
  const onError = (error62) => {
@@ -41230,7 +41325,7 @@ var responseViaResponseObject = async (res, outgoing, options = {}) => {
41230
41325
  });
41231
41326
  if (!chunk) {
41232
41327
  if (i === 1) {
41233
- await new Promise((resolve25) => setTimeout(resolve25));
41328
+ await new Promise((resolve26) => setTimeout(resolve26));
41234
41329
  maxReadCount = 3;
41235
41330
  continue;
41236
41331
  }
@@ -43499,9 +43594,9 @@ data:
43499
43594
  const initRequest = messages.find((m) => isInitializeRequest(m));
43500
43595
  const clientProtocolVersion = initRequest ? initRequest.params.protocolVersion : req.headers.get("mcp-protocol-version") ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
43501
43596
  if (this._enableJsonResponse) {
43502
- return new Promise((resolve25) => {
43597
+ return new Promise((resolve26) => {
43503
43598
  this._streamMapping.set(streamId, {
43504
- resolveJson: resolve25,
43599
+ resolveJson: resolve26,
43505
43600
  cleanup: () => {
43506
43601
  this._streamMapping.delete(streamId);
43507
43602
  }
@@ -49864,7 +49959,7 @@ var Protocol = class {
49864
49959
  return;
49865
49960
  }
49866
49961
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
49867
- await new Promise((resolve25) => setTimeout(resolve25, pollInterval));
49962
+ await new Promise((resolve26) => setTimeout(resolve26, pollInterval));
49868
49963
  options?.signal?.throwIfAborted();
49869
49964
  }
49870
49965
  } catch (error62) {
@@ -49881,7 +49976,7 @@ var Protocol = class {
49881
49976
  */
49882
49977
  request(request, resultSchema, options) {
49883
49978
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
49884
- return new Promise((resolve25, reject) => {
49979
+ return new Promise((resolve26, reject) => {
49885
49980
  const earlyReject = (error62) => {
49886
49981
  reject(error62);
49887
49982
  };
@@ -49959,7 +50054,7 @@ var Protocol = class {
49959
50054
  if (!parseResult.success) {
49960
50055
  reject(parseResult.error);
49961
50056
  } else {
49962
- resolve25(parseResult.data);
50057
+ resolve26(parseResult.data);
49963
50058
  }
49964
50059
  } catch (error62) {
49965
50060
  reject(error62);
@@ -50221,12 +50316,12 @@ var Protocol = class {
50221
50316
  }
50222
50317
  } catch {
50223
50318
  }
50224
- return new Promise((resolve25, reject) => {
50319
+ return new Promise((resolve26, reject) => {
50225
50320
  if (signal.aborted) {
50226
50321
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
50227
50322
  return;
50228
50323
  }
50229
- const timeoutId = setTimeout(resolve25, interval);
50324
+ const timeoutId = setTimeout(resolve26, interval);
50230
50325
  signal.addEventListener("abort", () => {
50231
50326
  clearTimeout(timeoutId);
50232
50327
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -51366,7 +51461,7 @@ var McpServer = class {
51366
51461
  let task = createTaskResult.task;
51367
51462
  const pollInterval = task.pollInterval ?? 5e3;
51368
51463
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
51369
- await new Promise((resolve25) => setTimeout(resolve25, pollInterval));
51464
+ await new Promise((resolve26) => setTimeout(resolve26, pollInterval));
51370
51465
  const updatedTask = await extra.taskStore.getTask(taskId);
51371
51466
  if (!updatedTask) {
51372
51467
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -52091,14 +52186,14 @@ function claudeService(env) {
52091
52186
  if (!dir) return SERVICE;
52092
52187
  return `${SERVICE}-${createHash4("sha256").update(dir.normalize("NFC")).digest("hex").slice(0, 8)}`;
52093
52188
  }
52094
- var findWithSecurity = (args) => new Promise((resolve25) => {
52189
+ var findWithSecurity = (args) => new Promise((resolve26) => {
52095
52190
  const child = spawn3(SECURITY, [...args], { stdio: ["ignore", "pipe", "ignore"], timeout: SECURITY_TIMEOUT_MS });
52096
52191
  let stdout = "";
52097
52192
  child.stdout.setEncoding("utf8").on("data", (chunk) => {
52098
52193
  stdout += chunk;
52099
52194
  });
52100
- child.once("error", () => resolve25({ status: 1, stdout: "" }));
52101
- child.once("close", (code) => resolve25({ status: code ?? 1, stdout }));
52195
+ child.once("error", () => resolve26({ status: 1, stdout: "" }));
52196
+ child.once("close", (code) => resolve26({ status: code ?? 1, stdout }));
52102
52197
  });
52103
52198
  function keychainRead(account, service, find = findWithSecurity) {
52104
52199
  return async () => {
@@ -52119,7 +52214,7 @@ function renewEnv(env) {
52119
52214
  return Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== void 0 && !KEPT_OUT.includes(entry[0])));
52120
52215
  }
52121
52216
  function claudeRenewal(binary, env) {
52122
- return () => new Promise((resolve25, reject) => {
52217
+ return () => new Promise((resolve26, reject) => {
52123
52218
  const cwd = mkdtempSync(join16(tmpdir(), "orbital-claude-renew-"));
52124
52219
  const child = spawn3(binary, RENEW_ARGS, { cwd, env: renewEnv(env), stdio: ["ignore", "ignore", "pipe"], timeout: RENEW_TIMEOUT_MS });
52125
52220
  let stderr = "";
@@ -52129,7 +52224,7 @@ function claudeRenewal(binary, env) {
52129
52224
  const done = (error62) => {
52130
52225
  rmSync5(cwd, { recursive: true, force: true });
52131
52226
  if (error62) reject(error62);
52132
- else resolve25();
52227
+ else resolve26();
52133
52228
  };
52134
52229
  child.once("error", done);
52135
52230
  child.once("close", (code) => done(code === 0 ? null : new Error(`claude exited ${code ?? "on a signal"}${stderr.trim() ? `: ${stderr.trim().split("\n")[0]}` : ""}`)));
@@ -53396,7 +53491,7 @@ function missionOfRow(row2) {
53396
53491
  result: textOf(row2, "result"),
53397
53492
  question: textOf(row2, "question"),
53398
53493
  questions: questionsOf(row2),
53399
- giver: null,
53494
+ receivers: [],
53400
53495
  lead: textOf(row2, "lead"),
53401
53496
  lastActivity: textOf(row2, "activity"),
53402
53497
  claim: textOf(row2, "claim"),
@@ -53549,7 +53644,7 @@ var MissionStore = class {
53549
53644
  source: request.source !== void 0 ? request.source : known?.source ?? null,
53550
53645
  result: request.result !== void 0 ? request.result : known?.result ?? null,
53551
53646
  question: request.question !== void 0 ? request.question : request.state === "waiting" ? known?.question ?? null : null,
53552
- giver: request.giver !== void 0 ? request.giver : known?.giver ?? null,
53647
+ receivers: request.receivers !== void 0 ? request.receivers : known?.receivers ?? [],
53553
53648
  lead: request.lead !== void 0 ? request.lead : known?.lead ?? null,
53554
53649
  claim: request.claim !== void 0 ? request.claim : known?.claim ?? null,
53555
53650
  claimedAt: request.claimedAt !== void 0 ? request.claimedAt : known?.claimedAt ?? null,
@@ -53604,7 +53699,7 @@ function missionRecord(node2, mission, declared, stored) {
53604
53699
  result: stored?.result ?? null,
53605
53700
  question: stored?.question ?? null,
53606
53701
  questions: stored?.questions ?? [],
53607
- giver: stored?.giver ?? null,
53702
+ receivers: stored?.receivers ?? [],
53608
53703
  lead: stored?.lead ?? null,
53609
53704
  lastActivity: null,
53610
53705
  claim: stored?.claim ?? null,
@@ -53711,9 +53806,9 @@ var Telemetry = class {
53711
53806
  async drain(timeoutMs) {
53712
53807
  if (this.queues.every((queue) => !queue.running && queue.items.length === 0)) return true;
53713
53808
  let done = null;
53714
- const empty = new Promise((resolve25) => {
53809
+ const empty = new Promise((resolve26) => {
53715
53810
  done = () => {
53716
- if (this.queues.every((queue) => !queue.running && queue.items.length === 0)) resolve25(true);
53811
+ if (this.queues.every((queue) => !queue.running && queue.items.length === 0)) resolve26(true);
53717
53812
  };
53718
53813
  this.drained.add(done);
53719
53814
  });
@@ -53966,7 +54061,7 @@ var SessionHost = class {
53966
54061
  transition: missionTransition(before?.state ?? "open", after.state, after.question),
53967
54062
  reason: change?.note ?? null,
53968
54063
  by: change?.by ?? "node",
53969
- giver: after.giver,
54064
+ receivers: after.receivers,
53970
54065
  sessionId: after.sessionId,
53971
54066
  lead: after.lead,
53972
54067
  question: after.question,
@@ -53974,7 +54069,7 @@ var SessionHost = class {
53974
54069
  });
53975
54070
  }
53976
54071
  missionWritten(mission, team, by) {
53977
- this.telemetry.emit({ kind: "mission.state", mission, team, from: null, to: "open", transition: "new", reason: "written", by, giver: null, sessionId: null, lead: null, question: null, result: null });
54072
+ this.telemetry.emit({ kind: "mission.state", mission, team, from: null, to: "open", transition: "new", reason: "written", by, receivers: [], sessionId: null, lead: null, question: null, result: null });
53978
54073
  }
53979
54074
  async snapshot() {
53980
54075
  const definition = await this.definitionOf(this.options.defaultWorkspace);
@@ -54003,7 +54098,7 @@ var SessionHost = class {
54003
54098
  retiring: note?.retiring ?? null
54004
54099
  };
54005
54100
  }), ...[...notes].filter(([name]) => !definition.teams.some((team) => team.name === name)).map(([name, note]) => ({ name, node: this.node, purpose: null, lead: null, members: [], problems: [...note.problems], retiring: note.retiring }))],
54006
- missions: missions.map((record2) => ({ missionId: record2.missionId, team: record2.team, state: record2.state, sessionId: record2.sessionId, lead: record2.lead, giver: record2.giver, givenBy: record2.givenBy, question: record2.question, questions: record2.questions, result: record2.result, updatedAt: record2.updatedAt })),
54101
+ missions: missions.map((record2) => ({ missionId: record2.missionId, team: record2.team, state: record2.state, sessionId: record2.sessionId, lead: record2.lead, receivers: record2.receivers, givenBy: record2.givenBy, question: record2.question, questions: record2.questions, result: record2.result, updatedAt: record2.updatedAt })),
54007
54102
  sessions: this.liveHosted().map((session) => ({ ...this.contextOf(session), role: this.roleOf(session), placement: session.placement, running: session.running !== null, since: this.turnSince.get(session.id) ?? null })),
54008
54103
  jobs: [...this.openJobs].map(([jobId, open2]) => ({ ...open2.context, jobId, since: new Date(open2.since).toISOString(), via: open2.via })),
54009
54104
  tokens: this.telemetry.tokens(),
@@ -54057,19 +54152,48 @@ var SessionHost = class {
54057
54152
  this.redeliverQuestions(id);
54058
54153
  return info;
54059
54154
  }
54060
- redeliverQuestions(giverId) {
54155
+ redeliverQuestions(sessionId) {
54156
+ const session = this.sessions.get(sessionId);
54157
+ if (!session) return;
54061
54158
  for (const stored of this.missions.all()) {
54062
- if (stored.giver !== giverId || !stored.sessionId || stored.state === "paused" || !hasLead(stored.state)) continue;
54063
- if (stored.state === "waiting" && !this.questionOpen(giverId, stored.mission, null)) this.deliverQuestion(giverId, { team: stored.team, mission: stored.mission, lead: stored.lead, question: stored.question ?? NO_HANDOFF_TELL, questionId: null }, stored.sessionId);
54064
- for (const asked2 of stored.questions) {
54065
- if (!this.questionOpen(giverId, stored.mission, asked2.id)) this.deliverQuestion(giverId, { team: stored.team, mission: stored.mission, lead: stored.lead, question: asked2.question, questionId: asked2.id }, stored.sessionId);
54066
- }
54159
+ if (!stored.receivers.some((key2) => this.receives(session, key2))) continue;
54160
+ for (const open2 of this.openQuestions(stored)) if (!this.questionOpen(sessionId, open2.mission, open2.questionId)) this.deliverQuestion(sessionId, open2, stored.sessionId);
54067
54161
  }
54068
54162
  }
54069
- questionOpen(giverId, mission, questionId) {
54070
- if (this.questionsInFlight.has(`${giverId}:${mission}:${questionId ?? ""}`)) return true;
54163
+ openQuestions(stored) {
54164
+ if (!stored.sessionId || stored.state === "paused" || !hasLead(stored.state)) return [];
54165
+ const base2 = { team: stored.team, mission: stored.mission, lead: stored.lead };
54166
+ return [
54167
+ ...stored.state === "waiting" ? [{ ...base2, question: stored.question ?? NO_HANDOFF_TELL, questionId: null }] : [],
54168
+ ...stored.questions.map((asked2) => ({ ...base2, question: asked2.question, questionId: asked2.id }))
54169
+ ];
54170
+ }
54171
+ receiverKey(session) {
54172
+ return session.external?.externalId ? externalReceiver(session.external.externalId) : session.id;
54173
+ }
54174
+ receives(session, key2) {
54175
+ return key2 === session.id || key2 === this.receiverKey(session);
54176
+ }
54177
+ liveReceivers(receivers, except = null) {
54178
+ return [...this.sessions.values()].filter((session) => session.id !== except && !this.ended.has(session.id) && (session.external ? session.external.attached : true) && receivers.some((key2) => this.receives(session, key2)));
54179
+ }
54180
+ keyOfRemote(receiver) {
54181
+ if (receiver.node !== this.node) return receiverRef(receiver);
54182
+ const session = this.sessions.get(receiver.sessionId);
54183
+ return session ? this.receiverKey(session) : receiver.sessionId;
54184
+ }
54185
+ starterKey(by) {
54186
+ if (!by.startsWith("session:")) return null;
54187
+ const named2 = by.slice("session:".length);
54188
+ const remote = remoteReceiverOf(named2);
54189
+ if (remote) return this.keyOfRemote(remote);
54190
+ const session = this.sessions.get(named2);
54191
+ return session ? this.receiverKey(session) : named2;
54192
+ }
54193
+ questionOpen(sessionId, mission, questionId) {
54194
+ if (this.questionsInFlight.has(`${sessionId}:${mission}:${questionId ?? ""}`)) return true;
54071
54195
  for (const [taskId, asked2] of [...this.questionDeliveries]) {
54072
- if (asked2.mission !== mission || asked2.questionId !== questionId || this.taskSession.get(taskId) !== giverId) continue;
54196
+ if (asked2.mission !== mission || asked2.questionId !== questionId || this.taskSession.get(taskId) !== sessionId) continue;
54073
54197
  const task = this.tasks.get(taskId);
54074
54198
  if (task && !(task.status && isTerminal(task.status.state))) return true;
54075
54199
  this.questionDeliveries.delete(taskId);
@@ -54101,7 +54225,7 @@ var SessionHost = class {
54101
54225
  async settleExternal(sessionId, settle2) {
54102
54226
  const session = this.sessions.get(sessionId);
54103
54227
  if (!session?.external) throw new SessionHostError("not-found", `There is no attached session ${sessionId} on this node.`);
54104
- const taskId = settle2.taskId ?? session.running;
54228
+ const taskId = settle2.taskId ?? session.running ?? session.external.open.keys().next().value ?? null;
54105
54229
  const task = taskId ? this.tasks.get(taskId) : void 0;
54106
54230
  if (!task || this.taskSession.get(task.id) !== sessionId || task.status && isTerminal(task.status.state)) throw new SessionHostError("invalid", `Session ${sessionId} has no open delivery${settle2.taskId ? ` ${settle2.taskId}` : ""} to settle.`);
54107
54231
  await this.finish(sessionId, task, "TASK_STATE_COMPLETED", settle2.text);
@@ -54135,23 +54259,21 @@ var SessionHost = class {
54135
54259
  await this.finish(session.id, task, "TASK_STATE_FAILED", `The attached session ${session.id} is detached.`);
54136
54260
  return;
54137
54261
  }
54138
- session.running = task.id;
54262
+ const asked2 = this.questionOf(message);
54263
+ if (asked2 === null) session.running = task.id;
54139
54264
  await this.update(session.id, task, "TASK_STATE_WORKING");
54140
54265
  const fromAgent = message.metadata?.[ORBITAL_META.fromAgent];
54141
54266
  const fromSession = message.metadata?.[ORBITAL_META.fromSession];
54142
- const asked2 = this.questionOf(message);
54143
54267
  const delivery = { kind: "delivery", taskId: task.id, fromAgent: typeof fromAgent === "string" ? fromAgent : null, fromSession: typeof fromSession === "string" ? fromSession : null, mission: asked2, question: asked2 === null ? null : this.questionIdOf(message), text: messageText(message) };
54144
- const settled = new Promise((resolve25) => this.externalWaits.set(task.id, resolve25));
54268
+ const settled = new Promise((resolve26) => this.externalWaits.set(task.id, resolve26)).finally(() => {
54269
+ this.externalWaits.delete(task.id);
54270
+ external.open.delete(task.id);
54271
+ if (session.running === task.id) session.running = null;
54272
+ });
54145
54273
  if (asked2 !== null) external.open.set(task.id, delivery);
54146
54274
  if (external.listeners.size === 0) external.waiting.push(delivery);
54147
54275
  for (const listener of external.listeners) listener(delivery);
54148
- try {
54149
- await settled;
54150
- } finally {
54151
- this.externalWaits.delete(task.id);
54152
- external.open.delete(task.id);
54153
- session.running = null;
54154
- }
54276
+ if (asked2 === null) await settled;
54155
54277
  }
54156
54278
  stopFollowing() {
54157
54279
  this.following.abort();
@@ -54166,9 +54288,9 @@ var SessionHost = class {
54166
54288
  if (this.followingHub) return;
54167
54289
  let listener = null;
54168
54290
  try {
54169
- await Promise.race([this.settled(), new Promise((resolve25) => {
54170
- listener = resolve25;
54171
- this.followListeners.add(resolve25);
54291
+ await Promise.race([this.settled(), new Promise((resolve26) => {
54292
+ listener = resolve26;
54293
+ this.followListeners.add(resolve26);
54172
54294
  })]);
54173
54295
  } finally {
54174
54296
  if (listener) this.followListeners.delete(listener);
@@ -54259,7 +54381,7 @@ var SessionHost = class {
54259
54381
  const leadLives = known?.sessionId ? this.sessions.has(known.sessionId) : false;
54260
54382
  if (known?.state === "paused" && this.leadKept(known.sessionId)) throw new SessionHostError("invalid", `The mission ${missionId} is paused, in session ${known.sessionId}. Resume it with mission resume ${missionId}.`);
54261
54383
  if (known?.state === "active" && leadLives) throw new SessionHostError("invalid", `The mission ${missionId} is active already, in session ${known.sessionId}. Its lead hands it off first.`);
54262
- if (known?.state === "waiting" && leadLives) throw new SessionHostError("invalid", `The mission ${missionId} waits for its giver, in session ${known.sessionId}. Answer it with mission reply.`);
54384
+ if (known?.state === "waiting" && leadLives) throw new SessionHostError("invalid", `The mission ${missionId} waits for a decision, in session ${known.sessionId}. Answer it with mission reply.`);
54263
54385
  const ended = known && hasLead(known.state) && !leadLives ? `the lead session ${known.sessionId ?? "(none)"} ended with its node` : null;
54264
54386
  if (known?.state === "done") throw new SessionHostError("invalid", `The mission ${missionId} is done. Give the team a new mission for new work.`);
54265
54387
  const claim2 = declared?.source ?? missionId;
@@ -54270,12 +54392,13 @@ var SessionHost = class {
54270
54392
  const resumable = ended && known?.sessionId && readSessionRecord(this.paths.sessionsDir, known.sessionId) !== null ? known.sessionId : null;
54271
54393
  if (resumable) {
54272
54394
  const task2 = await this.submit({ sessionId: resumable, message });
54273
- this.missions.change(mission, { state: "active", by, team, note: `${source.event}; ${ended}, so it goes on in the same session`, taskId: task2.id, source: key2, question: null, giver: by.startsWith("session:") ? by.slice("session:".length) : known?.giver ?? null });
54395
+ const starter = this.starterKey(by);
54396
+ this.missions.change(mission, { state: "active", by, team, note: `${source.event}; ${ended}, so it goes on in the same session`, taskId: task2.id, source: key2, question: null, receivers: [.../* @__PURE__ */ new Set([...known?.receivers ?? [], ...starter ? [starter] : []])] });
54274
54397
  return { task: task2, duplicate: false };
54275
54398
  }
54276
54399
  const task = await this.submit({ message, start: { agent: team, workspaceDir, team, mission } });
54277
54400
  const takenOver = held2.map(claimHolder);
54278
- this.missions.change(mission, { state: "active", by, team, note: [source.event, ended, takenOver.length > 0 ? `took over the ${live2.length > 0 ? "claim" : "stale claim"} of ${takenOver.join(", ")}` : null].filter((part) => part !== null).join("; "), claim: claim2, claimedAt: new Date(this.options.context.now()).toISOString(), sessionId: task.contextId ?? null, taskId: task.id, source: key2, result: null, question: null, questions: [], giver: by.startsWith("session:") ? by.slice("session:".length) : null, lead: here.members[0]?.agent ?? null });
54401
+ this.missions.change(mission, { state: "active", by, team, note: [source.event, ended, takenOver.length > 0 ? `took over the ${live2.length > 0 ? "claim" : "stale claim"} of ${takenOver.join(", ")}` : null].filter((part) => part !== null).join("; "), claim: claim2, claimedAt: new Date(this.options.context.now()).toISOString(), sessionId: task.contextId ?? null, taskId: task.id, source: key2, result: null, question: null, questions: [], receivers: [this.starterKey(by)].filter((one) => one !== null), lead: here.members[0]?.agent ?? null });
54279
54402
  return { task, duplicate: false };
54280
54403
  }
54281
54404
  pauseInterrupted() {
@@ -54378,7 +54501,7 @@ var SessionHost = class {
54378
54501
  for (const { relay: port } of this.options.ports.missionRelays()) {
54379
54502
  const answered = await port.reply({ missionId, text: text3, ...questionId && { questionId }, by, ...this.claimOfBy(by) });
54380
54503
  if (!answered) continue;
54381
- await this.closeQuestions(missionId, questionId ?? void 0, `answered: ${text3}`);
54504
+ await this.closeQuestions(missionId, questionId ?? void 0, by, text3);
54382
54505
  return answered;
54383
54506
  }
54384
54507
  }
@@ -54389,14 +54512,14 @@ var SessionHost = class {
54389
54512
  if (known?.state !== "waiting" || !known.sessionId) throw new SessionHostError("invalid", `The mission ${missionId} is ${known?.state ?? "open"}, not waiting, and has no open question, so it takes no reply.`);
54390
54513
  const lead = known.sessionId;
54391
54514
  this.missions.change(missionId, { state: "active", by, note: "reply" });
54392
- await this.closeQuestions(missionId, null, `answered: ${text3}`);
54515
+ await this.closeQuestions(missionId, null, by, text3);
54393
54516
  await this.submit({ sessionId: lead, message: { messageId: randomUUID4(), role: "ROLE_USER", parts: [textPart(`[reply to your question on the mission ${missionId}, from ${by}]
54394
54517
  ${text3}`)] } });
54395
54518
  return this.missionOf(missionId);
54396
54519
  }
54397
54520
  async answerAsked(by, missionId, lead, asked2, text3) {
54398
54521
  this.missions.answer(missionId, asked2.id);
54399
- await this.closeQuestions(missionId, asked2.id, `answered: ${text3}`);
54522
+ await this.closeQuestions(missionId, asked2.id, by, text3);
54400
54523
  const session = this.sessions.get(lead);
54401
54524
  if (session?.running && !session.external) {
54402
54525
  this.steering.set(lead, [...this.steering.get(lead) ?? [], { from: by, missionId, text: `reply to your question ${asked2.id}: ${asked2.question}
@@ -54413,7 +54536,7 @@ ${text3}`)] } });
54413
54536
  const missionId = given.mission;
54414
54537
  const known = this.missions.get(missionId);
54415
54538
  if (!known || !hasLead(known.state) || known.state === "paused" || !known.sessionId) throw new SessionHostError("invalid", `The mission ${missionId} is ${known?.state ?? "open"}, so its lead asks nothing.`);
54416
- if (given.sessionId !== null && known.sessionId !== given.sessionId) throw new SessionHostError("invalid", `Only the lead session of the mission ${missionId} asks its giver: session ${known.sessionId}.`);
54539
+ if (given.sessionId !== null && known.sessionId !== given.sessionId) throw new SessionHostError("invalid", `Only the lead session of the mission ${missionId} asks its decision receivers: session ${known.sessionId}.`);
54417
54540
  this.askedInTurn.add(known.sessionId);
54418
54541
  this.recordQuestion(missionId, known.sessionId, given.question);
54419
54542
  return this.missionOf(missionId);
@@ -54421,8 +54544,8 @@ ${text3}`)] } });
54421
54544
  recordQuestion(missionId, sessionId, question) {
54422
54545
  const asked2 = { id: `q-${randomUUID4().slice(0, 8)}`, question, at: new Date(this.options.context.now()).toISOString() };
54423
54546
  const after = this.missions.ask(missionId, asked2);
54424
- this.telemetry.emit({ kind: "mission.state", mission: missionId, team: after.team, from: after.state, to: after.state, transition: "question", reason: `asked ${asked2.id}`, by: `session:${sessionId}`, giver: after.giver, sessionId: after.sessionId, lead: after.lead, question: asked2.question, result: after.result });
54425
- this.tellGiver({ team: after.team, mission: missionId, giver: after.giver, lead: after.lead, question: asked2.question, questionId: asked2.id }, sessionId);
54547
+ this.telemetry.emit({ kind: "mission.state", mission: missionId, team: after.team, from: after.state, to: after.state, transition: "question", reason: `asked ${asked2.id}`, by: `session:${sessionId}`, receivers: after.receivers, sessionId: after.sessionId, lead: after.lead, question: asked2.question, result: after.result });
54548
+ this.tellReceivers(after.receivers, { team: after.team, mission: missionId, lead: after.lead, question: asked2.question, questionId: asked2.id }, sessionId);
54426
54549
  }
54427
54550
  async say(by, target, text3, reader, relay = true) {
54428
54551
  const named2 = this.missions.get(target);
@@ -54478,16 +54601,28 @@ ${text3}`)] } });
54478
54601
  const mission = message.metadata?.[ORBITAL_META.mission];
54479
54602
  return typeof mission === "string" ? mission : null;
54480
54603
  }
54481
- async closeQuestions(mission, questionId, text3) {
54604
+ async closeQuestions(mission, questionId, by, text3) {
54605
+ const answerer = by.startsWith("session:") ? by.slice("session:".length) : null;
54606
+ const told = /* @__PURE__ */ new Set();
54482
54607
  for (const [taskId, asked2] of [...this.questionDeliveries]) {
54483
54608
  if (asked2.mission !== mission || questionId !== void 0 && asked2.questionId !== questionId) continue;
54484
54609
  this.questionDeliveries.delete(taskId);
54485
54610
  const task = this.tasks.get(taskId);
54486
- const giver = this.taskSession.get(taskId);
54487
- if (!task || !giver || task.status && isTerminal(task.status.state)) continue;
54488
- await this.finish(giver, task, "TASK_STATE_COMPLETED", text3);
54611
+ const receiver = this.taskSession.get(taskId);
54612
+ if (!task || !receiver || task.status && isTerminal(task.status.state)) continue;
54613
+ await this.finish(receiver, task, "TASK_STATE_COMPLETED", `answered by ${by}: ${text3}`);
54489
54614
  this.externalWaits.get(taskId)?.();
54615
+ if (receiver !== answerer) this.tellAnswered(receiver, { kind: "answered", mission, question: asked2.questionId, by, text: text3 }, told);
54490
54616
  }
54617
+ for (const session of this.liveReceivers(this.missions.get(mission)?.receivers ?? [], answerer)) this.tellAnswered(session.id, { kind: "answered", mission, question: questionId ?? null, by, text: text3 }, told);
54618
+ }
54619
+ tellAnswered(sessionId, answered, told) {
54620
+ const external = this.sessions.get(sessionId)?.external;
54621
+ if (!external?.attached || told.has(sessionId)) return;
54622
+ told.add(sessionId);
54623
+ const answers = (delivery) => delivery.mission === answered.mission && (answered.question === null || delivery.question === answered.question);
54624
+ for (let index = external.waiting.length - 1; index >= 0; index -= 1) if (answers(external.waiting[index])) external.waiting.splice(index, 1);
54625
+ for (const listener of external.listeners) listener(answered);
54491
54626
  }
54492
54627
  busyBelow(sessionId) {
54493
54628
  return (this.pendingWakes.get(sessionId) ?? []).some((taskId) => !this.delivered.has(taskId)) || [...this.sessions.values(), ...this.remoteSessions.values()].some((one) => one.parentSessionId === sessionId && one.running !== null);
@@ -54514,32 +54649,46 @@ ${text3}`)] } });
54514
54649
  }
54515
54650
  this.nudged.delete(sessionId);
54516
54651
  this.missions.change(known.mission, { state: "waiting", by: `session:${sessionId}`, note: question === "" ? NO_HANDOFF_NOTE : stateWord(state), question: question === "" ? null : question });
54517
- this.tellGiver({ ...known, question: question === "" ? NO_HANDOFF_TELL : question, questionId: null }, sessionId);
54652
+ this.tellReceivers(known.receivers, { team: known.team, mission: known.mission, lead: known.lead, question: question === "" ? NO_HANDOFF_TELL : question, questionId: null }, sessionId);
54518
54653
  }
54519
- async joinMission(sessionId, missionId) {
54520
- const session = this.sessions.get(sessionId);
54521
- if (!session?.external) throw new SessionHostError("invalid", `Only an attached session joins a mission as its giver, and ${sessionId} is not one.`);
54654
+ async joinMission(receiver, missionId) {
54655
+ const session = "sessionId" in receiver ? this.sessions.get(receiver.sessionId) : void 0;
54656
+ if ("sessionId" in receiver && !session) throw new SessionHostError("not-found", `There is no session ${receiver.sessionId} on this node.`);
54522
54657
  const known = this.missions.get(missionId);
54523
- if (!known) {
54658
+ if (!known && session) {
54524
54659
  for (const { relay } of this.options.ports.missionRelays()) {
54525
- const joined = await relay.join({ missionId, giver: { node: this.node, sessionId }, ...this.claimOfSession(sessionId) });
54660
+ const joined = await relay.join({ missionId, receiver: { node: this.node, sessionId: session.id }, ...this.claimOfSession(session.id) });
54526
54661
  if (joined) return joined;
54527
54662
  }
54528
54663
  }
54529
- return this.joinHere(giverRef({ node: this.node, sessionId }), `session:${sessionId}`, missionId);
54664
+ if (session && known?.sessionId === session.id) throw new SessionHostError("invalid", `The session ${session.id} leads the mission ${missionId}, so it asks its questions and receives none.`);
54665
+ if (!session) return this.joinHere(externalReceiver("externalId" in receiver ? receiver.externalId : ""), "cli", missionId, null);
54666
+ return this.joinHere(this.receiverKey(session), `session:${session.id}`, missionId, [session.id]);
54530
54667
  }
54531
54668
  async joinRemote(request) {
54532
- return this.joinHere(giverRef(request.giver), `session:${giverRef(request.giver)}`, request.missionId);
54669
+ const key2 = this.keyOfRemote(request.receiver);
54670
+ const local = remoteReceiverOf(key2) === null ? [request.receiver.sessionId] : null;
54671
+ return this.joinHere(key2, `session:${receiverRef(request.receiver)}`, request.missionId, local);
54533
54672
  }
54534
- async joinHere(giverId, by, missionId) {
54535
- const self = this.node;
54536
- const sessionId = remoteGiverOf(giverId)?.node === self ? remoteGiverOf(giverId).sessionId : giverId;
54673
+ async leaveMission(receiver, missionId) {
54674
+ const known = this.missions.get(missionId);
54675
+ if (!known || !hasLead(known.state)) throw new SessionHostError("invalid", `The mission ${missionId} is ${known?.state ?? "open"} on this node, so it has no decision receivers to leave.`);
54676
+ const session = "sessionId" in receiver ? this.sessions.get(receiver.sessionId) : void 0;
54677
+ const leaves = (key2) => session ? this.receives(session, key2) : "sessionId" in receiver ? key2 === receiver.sessionId : externalIdOf(key2) === receiver.externalId;
54678
+ const kept = known.receivers.filter((key2) => !leaves(key2));
54679
+ if (kept.length === known.receivers.length) return this.missionOf(missionId);
54680
+ this.missions.change(missionId, { state: known.state, by: "sessionId" in receiver ? `session:${receiver.sessionId}` : "cli", note: `${known.receivers.filter(leaves).join(", ")} left the decision receivers`, question: known.question, receivers: kept });
54681
+ return this.missionOf(missionId);
54682
+ }
54683
+ async joinHere(key2, by, missionId, deliverTo) {
54537
54684
  const known = this.missions.get(missionId);
54538
54685
  if (!known || !hasLead(known.state) || !known.sessionId) throw new SessionHostError("invalid", `The mission ${missionId} is ${known?.state ?? "open"}, so it has no lead to join.`);
54539
- if (known.giver !== sessionId) {
54540
- const changed = this.missions.change(missionId, { state: known.state, by, note: `joined as giver${known.giver ? `, in place of session ${known.giver}` : ""}`, giver: sessionId });
54541
- if (changed.state === "waiting") this.tellGiver({ ...changed, question: changed.question ?? NO_HANDOFF_TELL, questionId: null }, changed.sessionId);
54542
- if (changed.state !== "paused") for (const asked2 of changed.questions) this.tellGiver({ ...changed, question: asked2.question, questionId: asked2.id }, changed.sessionId);
54686
+ const stored = known.receivers.includes(key2) ? known : this.missions.change(missionId, { state: known.state, by, note: `${key2} joined the decision receivers`, question: known.question, receivers: [...known.receivers, key2] });
54687
+ const remote = remoteReceiverOf(key2);
54688
+ const live2 = this.liveReceivers([key2]).filter((session) => deliverTo === null || deliverTo.includes(session.id));
54689
+ for (const open2 of this.openQuestions(stored)) {
54690
+ if (remote && remote.node !== this.node) this.askRemote(remote, open2, stored.sessionId);
54691
+ for (const session of live2) if (!this.questionOpen(session.id, open2.mission, open2.questionId)) this.deliverQuestion(session.id, open2, stored.sessionId);
54543
54692
  }
54544
54693
  return this.missionOf(missionId);
54545
54694
  }
@@ -54549,36 +54698,34 @@ ${text3}`)] } });
54549
54698
  missionTeam(missionId) {
54550
54699
  return this.missions.get(missionId)?.team ?? null;
54551
54700
  }
54552
- async askGiver(question) {
54553
- const giver = this.sessions.get(question.giver.sessionId);
54554
- if (!giver?.external || question.giver.node !== this.node) return false;
54555
- this.deliverQuestion(giver.id, { team: null, mission: question.missionId, lead: question.lead ? `${question.lead} on ${question.node}` : null, question: question.question, questionId: question.questionId ?? null }, question.leadSessionId);
54701
+ async askReceiver(question) {
54702
+ const session = this.sessions.get(question.receiver.sessionId);
54703
+ if (!session || question.receiver.node !== this.node || this.liveReceivers([session.id]).length === 0) return false;
54704
+ this.deliverQuestion(session.id, { team: null, mission: question.missionId, lead: question.lead ? `${question.lead} on ${question.node}` : null, question: question.question, questionId: question.questionId ?? null }, question.leadSessionId);
54556
54705
  return true;
54557
54706
  }
54558
- tellGiver(known, sessionId) {
54559
- const missionId = known.mission;
54560
- const remote = known.giver ? remoteGiverOf(known.giver) : null;
54561
- const self = this.node;
54562
- if (remote && remote.node !== self) {
54563
- const question = { missionId, node: self, lead: known.lead, leadSessionId: sessionId, question: known.question, ...known.questionId && { questionId: known.questionId }, giver: remote, team: known.team };
54564
- void (async () => {
54565
- for (const { relay } of this.options.ports.missionRelays()) if (await relay.ask(question)) return;
54566
- this.options.context.output.warn(`no plugin carried the question of ${missionId} to ${known.giver}`);
54567
- })().catch((error62) => this.options.context.output.warn(`could not tell the giver of ${missionId}: ${error62 instanceof Error ? error62.message : String(error62)}`));
54568
- return;
54707
+ tellReceivers(receivers, open2, leadSessionId) {
54708
+ for (const key2 of receivers) {
54709
+ const remote = remoteReceiverOf(key2);
54710
+ if (remote && remote.node !== this.node) this.askRemote(remote, open2, leadSessionId);
54569
54711
  }
54570
- const giver = known.giver ? this.sessions.get(remote ? remote.sessionId : known.giver) : void 0;
54571
- if (!giver) return;
54572
- this.deliverQuestion(giver.id, known, sessionId);
54712
+ for (const session of this.liveReceivers(receivers)) this.deliverQuestion(session.id, open2, leadSessionId);
54573
54713
  }
54574
- deliverQuestion(giverId, known, sessionId) {
54714
+ askRemote(receiver, open2, leadSessionId) {
54715
+ const question = { missionId: open2.mission, node: this.node, lead: open2.lead, leadSessionId, question: open2.question, ...open2.questionId && { questionId: open2.questionId }, receiver, team: open2.team };
54716
+ void (async () => {
54717
+ for (const { relay } of this.options.ports.missionRelays()) if (await relay.ask(question)) return;
54718
+ this.options.context.output.warn(`no plugin carried the question of ${open2.mission} to ${receiverRef(receiver)}`);
54719
+ })().catch((error62) => this.options.context.output.warn(`could not tell ${receiverRef(receiver)} the question of ${open2.mission}: ${error62 instanceof Error ? error62.message : String(error62)}`));
54720
+ }
54721
+ deliverQuestion(receiverId, known, sessionId) {
54575
54722
  const missionId = known.mission;
54576
54723
  const question = known.question;
54577
54724
  const questionId = known.questionId;
54578
- const key2 = `${giverId}:${missionId}:${questionId ?? ""}`;
54725
+ const key2 = `${receiverId}:${missionId}:${questionId ?? ""}`;
54579
54726
  this.questionsInFlight.add(key2);
54580
54727
  void this.submit({
54581
- sessionId: giverId,
54728
+ sessionId: receiverId,
54582
54729
  message: {
54583
54730
  messageId: randomUUID4(),
54584
54731
  role: "ROLE_USER",
@@ -54593,7 +54740,7 @@ Answer with missionSay on ${missionId}.`)],
54593
54740
  }
54594
54741
  }).then((task) => {
54595
54742
  this.questionDeliveries.set(task.id, { mission: known.mission, questionId });
54596
- }).catch((error62) => this.options.context.output.warn(`could not tell the giver of ${missionId}: ${error62 instanceof Error ? error62.message : String(error62)}`)).finally(() => this.questionsInFlight.delete(key2));
54743
+ }).catch((error62) => this.options.context.output.warn(`could not tell ${receiverId} the question of ${missionId}: ${error62 instanceof Error ? error62.message : String(error62)}`)).finally(() => this.questionsInFlight.delete(key2));
54597
54744
  }
54598
54745
  async missionActivity(missionId, limit) {
54599
54746
  const sessionId = this.missions.get(missionId)?.sessionId;
@@ -54811,8 +54958,8 @@ Answer with missionSay on ${missionId}.`)],
54811
54958
  }
54812
54959
  }
54813
54960
  if (signal?.aborted) return;
54814
- await new Promise((resolve25) => {
54815
- wake = resolve25;
54961
+ await new Promise((resolve26) => {
54962
+ wake = resolve26;
54816
54963
  });
54817
54964
  wake = null;
54818
54965
  }
@@ -54888,9 +55035,13 @@ Answer with missionSay on ${missionId}.`)],
54888
55035
  }
54889
55036
  missionRoots(readerSessionId) {
54890
55037
  const outside = this.sessions.get(readerSessionId)?.external !== void 0;
54891
- const missionRoots = this.missions.all().filter((stored) => stored.sessionId !== null && (outside || stored.giver === readerSessionId) && (this.sessions.has(stored.sessionId) || this.remoteSessions.has(stored.sessionId))).map((stored) => this.rootOf(stored.sessionId));
55038
+ const missionRoots = this.missions.all().filter((stored) => stored.sessionId !== null && (outside || this.receivesFor(readerSessionId, stored.receivers)) && (this.sessions.has(stored.sessionId) || this.remoteSessions.has(stored.sessionId))).map((stored) => this.rootOf(stored.sessionId));
54892
55039
  return new Set(missionRoots);
54893
55040
  }
55041
+ receivesFor(sessionId, receivers) {
55042
+ const session = this.sessions.get(sessionId);
55043
+ return session !== void 0 && receivers.some((key2) => this.receives(session, key2));
55044
+ }
54894
55045
  reachRoots(readerSessionId) {
54895
55046
  const outside = this.sessions.get(readerSessionId)?.external !== void 0;
54896
55047
  const attachedRoots = outside ? [...this.sessions.values()].filter((one) => one.external?.attached === true).map((one) => this.rootOf(one.id)) : [];
@@ -56457,7 +56608,7 @@ var StdioClientTransport = class {
56457
56608
  if (this._process) {
56458
56609
  throw new Error("StdioClientTransport already started! If using Client class, note that connect() calls start() automatically.");
56459
56610
  }
56460
- return new Promise((resolve25, reject) => {
56611
+ return new Promise((resolve26, reject) => {
56461
56612
  this._process = (0, import_cross_spawn.default)(this._serverParams.command, this._serverParams.args ?? [], {
56462
56613
  // merge default env with server env because mcp server needs some env vars
56463
56614
  env: {
@@ -56474,7 +56625,7 @@ var StdioClientTransport = class {
56474
56625
  this.onerror?.(error62);
56475
56626
  });
56476
56627
  this._process.on("spawn", () => {
56477
- resolve25();
56628
+ resolve26();
56478
56629
  });
56479
56630
  this._process.on("close", (_code) => {
56480
56631
  this._process = void 0;
@@ -56539,22 +56690,22 @@ var StdioClientTransport = class {
56539
56690
  if (this._process) {
56540
56691
  const processToClose = this._process;
56541
56692
  this._process = void 0;
56542
- const closePromise = new Promise((resolve25) => {
56693
+ const closePromise = new Promise((resolve26) => {
56543
56694
  processToClose.once("close", () => {
56544
- resolve25();
56695
+ resolve26();
56545
56696
  });
56546
56697
  });
56547
56698
  try {
56548
56699
  processToClose.stdin?.end();
56549
56700
  } catch {
56550
56701
  }
56551
- await Promise.race([closePromise, new Promise((resolve25) => setTimeout(resolve25, 2e3).unref())]);
56702
+ await Promise.race([closePromise, new Promise((resolve26) => setTimeout(resolve26, 2e3).unref())]);
56552
56703
  if (processToClose.exitCode === null) {
56553
56704
  try {
56554
56705
  processToClose.kill("SIGTERM");
56555
56706
  } catch {
56556
56707
  }
56557
- await Promise.race([closePromise, new Promise((resolve25) => setTimeout(resolve25, 2e3).unref())]);
56708
+ await Promise.race([closePromise, new Promise((resolve26) => setTimeout(resolve26, 2e3).unref())]);
56558
56709
  }
56559
56710
  if (processToClose.exitCode === null) {
56560
56711
  try {
@@ -56566,15 +56717,15 @@ var StdioClientTransport = class {
56566
56717
  this._readBuffer.clear();
56567
56718
  }
56568
56719
  send(message) {
56569
- return new Promise((resolve25) => {
56720
+ return new Promise((resolve26) => {
56570
56721
  if (!this._process?.stdin) {
56571
56722
  throw new Error("Not connected");
56572
56723
  }
56573
56724
  const json4 = serializeMessage(message);
56574
56725
  if (this._process.stdin.write(json4)) {
56575
- resolve25();
56726
+ resolve26();
56576
56727
  } else {
56577
- this._process.stdin.once("drain", resolve25);
56728
+ this._process.stdin.once("drain", resolve26);
56578
56729
  }
56579
56730
  });
56580
56731
  }
@@ -56667,12 +56818,12 @@ var StdioServerTransport = class {
56667
56818
  this.onclose?.();
56668
56819
  }
56669
56820
  send(message) {
56670
- return new Promise((resolve25) => {
56821
+ return new Promise((resolve26) => {
56671
56822
  const json4 = serializeMessage(message);
56672
56823
  if (this._stdout.write(json4)) {
56673
- resolve25();
56824
+ resolve26();
56674
56825
  } else {
56675
- this._stdout.once("drain", resolve25);
56826
+ this._stdout.once("drain", resolve26);
56676
56827
  }
56677
56828
  });
56678
56829
  }
@@ -56781,6 +56932,7 @@ function agentTools(options) {
56781
56932
  };
56782
56933
  const send = (message, wait) => node2.send(message, wait);
56783
56934
  const a2a = () => node2.a2a();
56935
+ const own2 = async () => options.attached ? await options.attached() : options.sessionId;
56784
56936
  return [
56785
56937
  defineMcpTool({
56786
56938
  name: "agentList",
@@ -56824,7 +56976,7 @@ function agentTools(options) {
56824
56976
  description: "List the sessions of your team's work: the lead and every agent it or another member started. Each has its agent, its session id (a jobId for agentMessage), its role (lead or member), its status and its harness.",
56825
56977
  inputSchema: {},
56826
56978
  run: async () => {
56827
- if (!options.sessionId) throw new Error("agentPeers needs your own session: only an agent the node runs has peers.");
56979
+ if (!await own2()) throw new Error("agentPeers needs your own session: only an agent the node runs has peers.");
56828
56980
  return await node2.peers();
56829
56981
  }
56830
56982
  }),
@@ -56904,13 +57056,13 @@ function agentTools(options) {
56904
57056
  }),
56905
57057
  defineMcpTool({
56906
57058
  name: "missionList",
56907
- description: "List every mission and its state (open, active, waiting, paused, done or failed): the missions of this node and of the other nodes it knows, such as the nodes a lan plugin finds. Each has its id, its team, its node, its state, its open questions (questions: each lead question that waits on its giver, with its id and text; answer one with missionReply) and, when it ended, its result.",
57059
+ description: "List every mission and its state (open, active, waiting, paused, done or failed): the missions of this node and of the other nodes it knows, such as the nodes a lan plugin finds. Each has its id, its team, its node, its state, its open questions (questions: each lead question that waits on a decision, with its id and text; answer one with missionReply), its decision receivers and, when it ended, its result.",
56908
57060
  inputSchema: {},
56909
57061
  run: async () => node2.missions()
56910
57062
  }),
56911
57063
  defineMcpTool({
56912
57064
  name: "missionShow",
56913
- description: "Show one mission by its id: its brief, its state, its open questions (questions: each lead question that waits on its giver, with its id and text; answer one with missionReply), its session, its result and the history of its states.",
57065
+ description: "Show one mission by its id: its brief, its state, its open questions (questions: each lead question that waits on a decision, with its id and text; answer one with missionReply), its decision receivers, its session, its result and the history of its states.",
56914
57066
  inputSchema: { mission: MissionIdSchema.describe("The mission id.") },
56915
57067
  run: async (params) => {
56916
57068
  const found = (await node2.missions()).filter((record3) => record3.missionId === params.mission);
@@ -56962,7 +57114,7 @@ function agentTools(options) {
56962
57114
  }),
56963
57115
  defineMcpTool({
56964
57116
  name: "missionReply",
56965
- description: "Answer a question of a mission, by its id: the question its waiting lead ended its turn with, or a question its lead asked with missionAsk, by the question id. The answer reaches the lead and closes that question. With no question id, the reply answers the question of a waiting mission, or else the oldest open question.",
57117
+ description: "Answer a question of a mission, by its id: the question its waiting lead ended its turn with, or a question its lead asked with missionAsk, by the question id. The answer reaches the lead and closes that question for every decision receiver, and the node tells the other receivers that it is answered. With no question id, the reply answers the question of a waiting mission, or else the oldest open question.",
56966
57118
  inputSchema: {
56967
57119
  mission: MissionIdSchema.describe("The mission id."),
56968
57120
  text: external_exports.string().min(1).describe("The answer, in full: the lead has none of your context."),
@@ -56996,13 +57148,22 @@ function agentTools(options) {
56996
57148
  }),
56997
57149
  defineMcpTool({
56998
57150
  name: "missionJoin",
56999
- description: "Become the giver of an active, waiting or paused mission of this node, by its id: the questions of its lead then come to this session, and you answer them with missionSay. A mission that waits tells you its question at once.",
57151
+ description: "Join an active, waiting or paused mission, by its id, as one of its decision receivers: any session can join, also a Claude Code session that the orbital mod attached. Each question of its lead goes to every live receiver, and the questions that are open now come to you at once. Answer a question with missionReply and its question id, or the question of a waiting mission with missionSay: the first answer from any receiver closes it for all. An attached session stays a receiver after it or the node restarts, so join once.",
57000
57152
  inputSchema: { mission: MissionIdSchema.describe("The mission id.") },
57001
57153
  run: async (params) => {
57002
- if (!options.sessionId) throw new Error("missionJoin needs your own session: only a session of the node becomes the giver of a mission.");
57154
+ if (!await own2()) throw new Error("missionJoin needs your own session: run it in a session of the node, or in a session that the orbital mod attached.");
57003
57155
  return node2.joinMission(params.mission);
57004
57156
  }
57005
57157
  }),
57158
+ defineMcpTool({
57159
+ name: "missionLeave",
57160
+ description: "Leave the decision receivers of a mission, by its id: the questions of its lead stop coming to this session. The other receivers still get them.",
57161
+ inputSchema: { mission: MissionIdSchema.describe("The mission id.") },
57162
+ run: async (params) => {
57163
+ if (!await own2()) throw new Error("missionLeave needs your own session: only a decision receiver leaves a mission.");
57164
+ return node2.leaveMission(params.mission);
57165
+ }
57166
+ }),
57006
57167
  defineMcpTool({
57007
57168
  name: "missionNew",
57008
57169
  description: "Write a new mission for a team of this project: missions/<id>/MISSION.md in the folder that holds the files of orbital, with the team in its frontmatter. One id names one mission. It is open until missionStart starts it.",
@@ -57022,7 +57183,7 @@ function agentTools(options) {
57022
57183
  description: "Read the mission of your session: its id, its team, its brief with the intent, the end state and the limits, its state (open, active, waiting, paused, done or failed), and its history.",
57023
57184
  inputSchema: {},
57024
57185
  run: async () => {
57025
- if (!options.sessionId) throw new Error("missionGet needs your own session: only an agent the node runs has a mission.");
57186
+ if (!await own2()) throw new Error("missionGet needs your own session: only an agent the node runs has a mission.");
57026
57187
  return await node2.mission();
57027
57188
  }
57028
57189
  }),
@@ -57034,7 +57195,7 @@ function agentTools(options) {
57034
57195
  state: external_exports.enum(["done", "failed"]).optional().describe("done when the end state is reached, failed when it cannot be. Default: done.")
57035
57196
  },
57036
57197
  run: async (params) => {
57037
- if (!options.sessionId) throw new Error("missionHandoff needs your own session: only the lead session of a mission hands it off.");
57198
+ if (!await own2()) throw new Error("missionHandoff needs your own session: only the lead session of a mission hands it off.");
57038
57199
  return await node2.handoff({ result: params.result, state: params.state ?? "done" });
57039
57200
  }
57040
57201
  }),
@@ -57047,18 +57208,18 @@ function agentTools(options) {
57047
57208
  state: external_exports.string().min(1).optional().describe("The state of the linked thing, such as open, merged or closed.")
57048
57209
  },
57049
57210
  run: async (params) => {
57050
- if (!options.sessionId) throw new Error("missionLink needs your own session: only the lead session of a mission adds a link to it.");
57211
+ if (!await own2()) throw new Error("missionLink needs your own session: only the lead session of a mission adds a link to it.");
57051
57212
  return await node2.link({ label: params.label, url: params.url, state: params.state ?? null });
57052
57213
  }
57053
57214
  }),
57054
57215
  defineMcpTool({
57055
57216
  name: "missionAsk",
57056
- description: "Ask the giver of the mission of your session a question, as its lead, at any moment, also while your workers run. The node keeps each question open with the mission until the giver answers it, shows it to the person as waiting on them, and pushes it to the giver at once. The answer comes back to you as a message that names the question. Use it for every decision you need from the giver: a question only in your reply text reaches no one. Keep working on what does not depend on the answer.",
57217
+ description: "Ask the decision receivers of the mission of your session a question, as its lead, at any moment, also while your workers run. The node keeps each question open with the mission until a receiver answers it, shows it to the person as waiting on them, and pushes it at once to every live receiver. The answer comes back to you as a message that names the question. Use it for every decision you need: a question only in your reply text reaches no one. Keep working on what does not depend on the answer.",
57057
57218
  inputSchema: {
57058
- question: external_exports.string().min(1).describe("The question, in full, with the options you see and what each one means: the giver has none of your context.")
57219
+ question: external_exports.string().min(1).describe("The question, in full, with the options you see and what each one means: the receivers have none of your context.")
57059
57220
  },
57060
57221
  run: async (params) => {
57061
- if (!options.sessionId) throw new Error("missionAsk needs your own session: only the lead session of a mission asks its giver.");
57222
+ if (!await own2()) throw new Error("missionAsk needs your own session: only the lead session of a mission asks its decision receivers.");
57062
57223
  return await node2.ask({ question: params.question });
57063
57224
  }
57064
57225
  })
@@ -57620,8 +57781,8 @@ function relaySender(plugin, by) {
57620
57781
  function pluginSender(plugin) {
57621
57782
  return { channel: plugin, peer: plugin, from: { kind: "node", name: plugin } };
57622
57783
  }
57623
- function remoteSessionSender(plugin, giver) {
57624
- return { channel: plugin, peer: giver.node, from: { kind: "session", name: giver.sessionId, ade: giver.node } };
57784
+ function remoteSessionSender(plugin, session) {
57785
+ return { channel: plugin, peer: session.node, from: { kind: "session", name: session.sessionId, ade: session.node } };
57625
57786
  }
57626
57787
  function missionTarget(node2, team) {
57627
57788
  return team ? { kind: "team", name: team, ade: node2 } : { kind: "node", name: node2 };
@@ -57760,10 +57921,10 @@ function a2aHandlerFor(host, reader, confined = false, network = null) {
57760
57921
  };
57761
57922
  }
57762
57923
  function readBody(request) {
57763
- return new Promise((resolve25, reject) => {
57924
+ return new Promise((resolve26, reject) => {
57764
57925
  const chunks = [];
57765
57926
  request.on("data", (chunk) => chunks.push(chunk));
57766
- request.on("end", () => resolve25(Buffer.concat(chunks).toString("utf8")));
57927
+ request.on("end", () => resolve26(Buffer.concat(chunks).toString("utf8")));
57767
57928
  request.on("error", reject);
57768
57929
  });
57769
57930
  }
@@ -57827,7 +57988,7 @@ async function serveHostApi(request, response, url2, api) {
57827
57988
  if (request.method === "POST" && url2.pathname === "/orbital/host/missions/ask") {
57828
57989
  const asked2 = MissionQuestionSchema.safeParse(JSON.parse(await readBody(request) || "{}"));
57829
57990
  if (!asked2.success) return json3(response, 400, { error: asked2.error.issues.map((issue2) => `${issue2.path.join(".")}: ${issue2.message}`).join("; ") });
57830
- return json3(response, 200, { delivered: await api.askGiver(asked2.data) });
57991
+ return json3(response, 200, { delivered: await api.askReceiver(asked2.data) });
57831
57992
  }
57832
57993
  if (request.method === "POST" && url2.pathname === "/orbital/host/settled") {
57833
57994
  const { orFollowing } = JSON.parse(await readBody(request) || "{}");
@@ -57946,11 +58107,16 @@ async function startNodeServer(options) {
57946
58107
  const by = caller.bound ? `session:${caller.bound}` : "cli";
57947
58108
  return json3(response, 200, control[1] === "pause" ? await options.host.pauseMission(by, asked2.data.missionId) : await options.host.resumeMission(by, asked2.data.missionId));
57948
58109
  }
57949
- if (request.method === "POST" && url2.pathname === "/orbital/missions/join") {
57950
- if (!caller.bound) return json3(response, 403, { error: "Only an attached session joins a mission as its giver: present its session token." });
58110
+ const membership = /^\/orbital\/missions\/(join|leave)$/.exec(url2.pathname);
58111
+ if (request.method === "POST" && membership) {
57951
58112
  const asked2 = MissionJoinSchema.safeParse(JSON.parse(await readBody(request) || "{}"));
57952
58113
  if (!asked2.success) return json3(response, 400, { error: asked2.error.issues.map((issue2) => `${issue2.path.join(".")}: ${issue2.message}`).join("; ") });
57953
- return json3(response, 200, await options.host.joinMission(caller.bound, asked2.data.missionId));
58114
+ const session = caller.bound ?? caller.reader;
58115
+ const holder = caller.presented === caller.token;
58116
+ if (asked2.data.externalId && !holder) return json3(response, 403, { error: "Only a holder of the node secret names an external id: a session joins as itself." });
58117
+ if (!session && !asked2.data.externalId) return json3(response, 403, { error: `Only a session ${membership[1]}s the decision receivers of a mission: present its session token, or name the external id of an attached session.` });
58118
+ const receiver = asked2.data.externalId ? { externalId: asked2.data.externalId } : { sessionId: session };
58119
+ return json3(response, 200, membership[1] === "join" ? await options.host.joinMission(receiver, asked2.data.missionId) : await options.host.leaveMission(receiver, asked2.data.missionId));
57954
58120
  }
57955
58121
  if (request.method === "POST" && url2.pathname === "/orbital/missions/new") {
57956
58122
  if (caller.presented !== caller.token && !caller.bound) return json3(response, 403, { error: "Only a holder of the node secret or a session of the node writes a mission here." });
@@ -57988,7 +58154,7 @@ async function startNodeServer(options) {
57988
58154
  const named2 = asked2.data.missionId ?? own2?.missionId ?? null;
57989
58155
  if (!named2) return json3(response, 400, { error: "Name the mission by its id." });
57990
58156
  if (own2 && own2.missionId !== named2) return json3(response, 403, { error: `The session ${session} works on the mission ${own2.missionId}, not on ${named2}.` });
57991
- if (!session && caller.presented !== caller.token) return json3(response, 403, { error: "Only the lead session or a holder of the node secret asks the giver of a mission." });
58157
+ if (!session && caller.presented !== caller.token) return json3(response, 403, { error: "Only the lead session or a holder of the node secret asks the decision receivers of a mission." });
57992
58158
  return json3(response, 200, await options.host.askMission({ mission: named2, question: asked2.data.question, sessionId: session }));
57993
58159
  }
57994
58160
  const activity = /^\/orbital\/missions\/([^/]+)\/activity$/.exec(url2.pathname);
@@ -58029,6 +58195,20 @@ async function startNodeServer(options) {
58029
58195
  return json3(response, 502, { error: `The panel ${plugin2}/${id} did not answer: ${error62 instanceof Error ? error62.message : String(error62)}` });
58030
58196
  }
58031
58197
  }
58198
+ const commandPath = /^\/orbital\/plugins\/([^/]+)\/commands\/([^/]+)$/.exec(url2.pathname);
58199
+ if (request.method === "POST" && commandPath) {
58200
+ if (presented !== token) return json3(response, 403, { error: "Only a holder of the node secret runs a command of a plugin." });
58201
+ const [name, command] = [decodeURIComponent(commandPath[1]), decodeURIComponent(commandPath[2])];
58202
+ const plugin2 = options.ports.plugins.find((one) => one.name === name);
58203
+ if (!plugin2?.command) return json3(response, 404, { error: `No plugin ${name} with commands runs on this node.` });
58204
+ const asked2 = PluginCommandRequestSchema.safeParse(await readBody(request).then((raw) => JSON.parse(raw)).catch(() => null));
58205
+ if (!asked2.success || asked2.data.name !== command || asked2.data.terminal !== null) return json3(response, 400, { error: `The request does not run the command ${command}: it names another command or asks for a terminal.` });
58206
+ try {
58207
+ return json3(response, 200, await plugin2.command(asked2.data));
58208
+ } catch (error62) {
58209
+ return json3(response, 502, { error: `The command ${name} ${command} failed: ${error62 instanceof Error ? error62.message : String(error62)}` });
58210
+ }
58211
+ }
58032
58212
  if (url2.pathname === "/orbital/telemetry/snapshot" || url2.pathname === "/orbital/telemetry/events") {
58033
58213
  if (request.method === "GET" && url2.pathname === "/orbital/telemetry/snapshot" && (presented === token || bound)) return json3(response, 200, await options.host.snapshot());
58034
58214
  if (presented !== token) return json3(response, 403, { error: "Only a holder of the node secret reads the telemetry stream of the node." });
@@ -58188,7 +58368,7 @@ async function startNodeServer(options) {
58188
58368
  const result2 = await dispatchA2a(await readBody(request), a2aHandlerFor(options.host, reader, bound !== null, httpSender(request.socket.remoteAddress)));
58189
58369
  if (result2.kind === "json") return json3(response, 200, result2.body);
58190
58370
  response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
58191
- const closed = new Promise((resolve25) => response.on("close", resolve25));
58371
+ const closed = new Promise((resolve26) => response.on("close", resolve26));
58192
58372
  const iterator = result2.events[Symbol.asyncIterator]();
58193
58373
  for (; ; ) {
58194
58374
  const next = await Promise.race([iterator.next(), closed.then(() => null)]);
@@ -58201,9 +58381,9 @@ async function startNodeServer(options) {
58201
58381
  }
58202
58382
  response.end();
58203
58383
  }
58204
- await new Promise((resolve25, reject) => {
58384
+ await new Promise((resolve26, reject) => {
58205
58385
  server.once("error", reject);
58206
- server.listen(options.port, address, () => resolve25());
58386
+ server.listen(options.port, address, () => resolve26());
58207
58387
  });
58208
58388
  const info = { name: options.name, workspaceDir: options.workspaceDir, url: nodeUrl(), token, pid: process.pid, startedAt: new Date(options.now()).toISOString(), restart: restartOf(process.env[NODE_RESTART_ENV]), code: pinnedCodeKey() };
58209
58389
  mkdirSync17(nodeDir(options.home, options.name), { recursive: true, mode: 448 });
@@ -58214,7 +58394,7 @@ async function startNodeServer(options) {
58214
58394
  close: async () => {
58215
58395
  if (readNodeInfo(options.home, options.name)?.pid === process.pid) rmSync9(nodeInfoPath(options.home, options.name), { force: true });
58216
58396
  server.closeAllConnections();
58217
- await new Promise((resolve25) => server.close(() => resolve25()));
58397
+ await new Promise((resolve26) => server.close(() => resolve26()));
58218
58398
  }
58219
58399
  };
58220
58400
  }
@@ -59220,7 +59400,7 @@ function hostApiOf(host, plugin, catalog, channels, models) {
59220
59400
  startMission: (start) => host.startMission(plugin, start),
59221
59401
  missions: () => host.missionList(),
59222
59402
  joinMission: async (request) => {
59223
- await host.receive(networkReceive(remoteSessionSender(plugin, request.giver), missionTarget(host.node, host.missionTeam(request.missionId)), `join the mission ${request.missionId} as its giver`, relayClaim(request)));
59403
+ await host.receive(networkReceive(remoteSessionSender(plugin, request.receiver), missionTarget(host.node, host.missionTeam(request.missionId)), `join the mission ${request.missionId} as a decision receiver`, relayClaim(request)));
59224
59404
  return host.joinRemote(request);
59225
59405
  },
59226
59406
  replyMission: async (request) => {
@@ -59235,9 +59415,9 @@ function hostApiOf(host, plugin, catalog, channels, models) {
59235
59415
  await relayed(request, `resume the mission ${request.missionId}`);
59236
59416
  return host.resumeMission(request.by, request.missionId, false);
59237
59417
  },
59238
- askGiver: async (question) => {
59239
- await host.receive(networkReceive(remoteSessionSender(plugin, { node: question.node, sessionId: question.leadSessionId }), { kind: "session", name: question.giver.sessionId, ade: host.node }, question.question, relayClaim({ team: question.team, agent: question.lead })));
59240
- return host.askGiver(question);
59418
+ askReceiver: async (question) => {
59419
+ await host.receive(networkReceive(remoteSessionSender(plugin, { node: question.node, sessionId: question.leadSessionId }), { kind: "session", name: question.receiver.sessionId, ade: host.node }, question.question, relayClaim({ team: question.team, agent: question.lead })));
59420
+ return host.askReceiver(question);
59241
59421
  },
59242
59422
  sayMission: async (request) => {
59243
59423
  await relayed(request, request.text);
@@ -59451,8 +59631,8 @@ var ChannelHub = class {
59451
59631
  if (state && isTerminal(state)) return;
59452
59632
  }
59453
59633
  if (signal?.aborted) return;
59454
- await new Promise((resolve25) => {
59455
- wake = resolve25;
59634
+ await new Promise((resolve26) => {
59635
+ wake = resolve26;
59456
59636
  });
59457
59637
  wake = null;
59458
59638
  }
@@ -59626,8 +59806,8 @@ Answer yes or no in this thread.`)], metadata: { [ORBITAL_META.channel]: binding
59626
59806
  thread: null
59627
59807
  });
59628
59808
  const threadKey = `${binding.channel}|${binding.id}|${sent.thread ?? sent.ref}`;
59629
- const answer2 = await new Promise((resolve25, reject) => {
59630
- this.asks.set(threadKey, { resolve: resolve25, question, channel: binding.channel, since: new Date(this.options.context.now()).toISOString() });
59809
+ const answer2 = await new Promise((resolve26, reject) => {
59810
+ this.asks.set(threadKey, { resolve: resolve26, question, channel: binding.channel, since: new Date(this.options.context.now()).toISOString() });
59631
59811
  signal?.addEventListener("abort", () => {
59632
59812
  this.asks.delete(threadKey);
59633
59813
  reject(new Error("The question was withdrawn."));
@@ -60301,8 +60481,9 @@ var panel = {
60301
60481
  var panelCommands = [panel];
60302
60482
 
60303
60483
  // src/core/cli/plugin.ts
60304
- import { readFileSync as readFileSync28 } from "node:fs";
60305
- import { basename as basename9, resolve as resolve14 } from "node:path";
60484
+ var import_yaml8 = __toESM(require_dist(), 1);
60485
+ import { readFileSync as readFileSync29 } from "node:fs";
60486
+ import { basename as basename9, resolve as resolve15 } from "node:path";
60306
60487
 
60307
60488
  // src/sdk/conformance.ts
60308
60489
  import { existsSync as existsSync29, mkdirSync as mkdirSync21, mkdtempSync as mkdtempSync2, readFileSync as readFileSync27, rmSync as rmSync11, writeFileSync as writeFileSync19 } from "node:fs";
@@ -60673,13 +60854,140 @@ async function runConformance(ports, fixtures = {}) {
60673
60854
  return results;
60674
60855
  }
60675
60856
 
60857
+ // src/core/cli/plugin-commands.ts
60858
+ import { existsSync as existsSync30, readFileSync as readFileSync28 } from "node:fs";
60859
+ import { join as join37, resolve as resolve14 } from "node:path";
60860
+ var INTROSPECTION_WORDS = ["describe", "schema", "help"];
60861
+ function commandsOf(dir) {
60862
+ const descriptor = join37(dir, PLUGIN_PACKAGE_FILE);
60863
+ if (!existsSync30(descriptor)) return [];
60864
+ try {
60865
+ return PluginPackageSchema.parse(JSON.parse(readFileSync28(descriptor, "utf8"))).commands;
60866
+ } catch {
60867
+ return [];
60868
+ }
60869
+ }
60870
+ function declaredPluginCommands(workspaceDir, env) {
60871
+ try {
60872
+ const source = definitionSource(workspaceDir, env);
60873
+ const config2 = source ? workspaceConfig(workspaceDir, env) : null;
60874
+ if (!source || !config2) return [];
60875
+ return Object.entries(config2.plugins).filter(([, entry]) => entry.enabled && entry.source).flatMap(([plugin, entry]) => {
60876
+ const from = pluginSource(plugin, entry);
60877
+ const commands = from.startsWith("exec:") ? [] : commandsOf(pluginDir(plugin, from, { harnessRoot: source.root, home: orbitalHome(env) }));
60878
+ return commands.length > 0 ? [{ plugin, commands }] : [];
60879
+ });
60880
+ } catch {
60881
+ return [];
60882
+ }
60883
+ }
60884
+ function workspaceOf(input2, fallback = ".") {
60885
+ return resolve14(typeof input2.flags.workspace === "string" ? input2.flags.workspace : fallback);
60886
+ }
60887
+ async function startWorkspacePlugin(context, workspaceDir, name, terminal) {
60888
+ const root = definitionSource(workspaceDir, context.env)?.root ?? null;
60889
+ const config2 = workspaceConfig(workspaceDir, context.env);
60890
+ const entry = config2?.plugins[name];
60891
+ if (!config2 || !entry) throw new CliError({ code: "not-found", message: `No plugin ${name} is named in ${ORBITAL_FILE} or found in ${PROJECT_PLUGINS_DIR}.`, exitCode: ExitCode.Validation, nextCommand: `${commandPrefix(context.env)} plugin add ${name}@<marketplace>` });
60892
+ const secrets = new PluginSecrets(context.env);
60893
+ return startRemotePlugin({
60894
+ name,
60895
+ source: pluginSource(name, entry),
60896
+ config: entry.config,
60897
+ entryEnv: entry.env,
60898
+ node: { name: `plugin-${name}`, workspaceDir, harness: defaultHarness(context.env), harnesses: [...RUNNER_HARNESSES], once: true, idleExitMs: null, project: null },
60899
+ roots: { harnessRoot: root, home: orbitalHome(context.env) },
60900
+ offline: config2.offline,
60901
+ policy: new PolicyChain([]),
60902
+ env: context.env,
60903
+ log: (level, message) => level === "info" ? context.output.step(message) : context.output.warn(message),
60904
+ secret: async (secret) => secrets.get(name, secret),
60905
+ restarts: 0,
60906
+ ...terminal && { terminal }
60907
+ });
60908
+ }
60909
+ function flagOf(option) {
60910
+ return {
60911
+ name: option.name,
60912
+ type: option.type,
60913
+ summary: option.summary,
60914
+ ...option.required && { required: true },
60915
+ ...option.default !== void 0 && { default: option.default },
60916
+ ...option.values && { enumValues: option.values }
60917
+ };
60918
+ }
60919
+ function requestOf(command, input2, terminal) {
60920
+ const args = Object.fromEntries(command.args.flatMap((arg) => {
60921
+ const value2 = input2.positionals[arg.name];
60922
+ return value2 === void 0 ? [] : [[arg.name, value2]];
60923
+ }));
60924
+ const options = Object.fromEntries(command.options.flatMap((option) => {
60925
+ const value2 = input2.flags[option.name];
60926
+ if (option.type === "boolean") return [[option.name, value2 === true]];
60927
+ return typeof value2 === "string" || typeof value2 === "number" ? [[option.name, value2]] : [];
60928
+ }));
60929
+ return { name: command.name, args, options, terminal };
60930
+ }
60931
+ async function runOnNode(context, workspaceDir, input2, plugin, request) {
60932
+ const configured = workspaceConfig(workspaceDir, context.env)?.node ?? null;
60933
+ const name = chooseNode({ ...typeof input2.flags.node === "string" && { flag: input2.flags.node }, env: context.env, configured, workspaceDir }).name;
60934
+ const node2 = await liveNode(orbitalHome(context.env), name, context.fetch);
60935
+ if (!node2) throw new CliError({ code: "not-found", message: `The command ${plugin} ${request.name} needs the node ${name}, and it does not run.`, exitCode: ExitCode.Validation, nextCommand: `${commandPrefix(context.env)} up` });
60936
+ const response = await context.fetch(`${node2.url}/orbital/plugins/${encodeURIComponent(plugin)}/commands/${encodeURIComponent(request.name)}`, {
60937
+ method: "POST",
60938
+ headers: { authorization: `Bearer ${node2.token}`, "content-type": "application/json" },
60939
+ body: JSON.stringify(request)
60940
+ });
60941
+ const body2 = await response.json();
60942
+ if (!response.ok) throw new CliError({ code: response.status === 404 ? "not-found" : "transport", message: body2.error ?? `The node answered ${response.status}.`, exitCode: response.status === 404 ? ExitCode.Validation : ExitCode.Error });
60943
+ return PluginCommandResultSchema.parse(body2);
60944
+ }
60945
+ async function runInPlugin(context, workspaceDir, input2, plugin, command, terminal) {
60946
+ if (command.interactive && context.nonInteractive) {
60947
+ throw new CliError({ code: "invalid-input", message: `The command ${plugin} ${command.name} is interactive: it needs a terminal.`, exitCode: ExitCode.Validation, hint: "Run it in a terminal, without --non-interactive." });
60948
+ }
60949
+ const given = command.interactive ? { input: terminal.input, output: context.outputMode === "text" ? terminal.output : terminal.error, error: terminal.error } : void 0;
60950
+ const started = await startWorkspacePlugin(context, workspaceDir, plugin, given);
60951
+ try {
60952
+ return await started.command(requestOf(command, input2, given ? { tty: context.isTty } : null));
60953
+ } finally {
60954
+ await started.close?.();
60955
+ }
60956
+ }
60957
+ function specOf(workspaceDir, plugin, command, terminal) {
60958
+ return {
60959
+ path: [plugin, command.name],
60960
+ summary: command.summary,
60961
+ group: CommandGroup.Packs,
60962
+ credential: "none",
60963
+ flags: [
60964
+ { name: "workspace", type: "path", summary: `The project whose ${ORBITAL_FILE} names the plugin. Default: the current directory.` },
60965
+ ...command.node ? [{ name: "node", type: "string", identifier: true, summary: "The node that runs the plugin. Default: the node of this workspace." }] : [],
60966
+ ...command.options.map(flagOf)
60967
+ ],
60968
+ positionals: command.args.map((arg) => ({ name: arg.name, type: "string", summary: arg.summary, ...arg.required && { required: true }, ...arg.variadic && { variadic: true } })),
60969
+ outputShape: { kind: "object", ref: "PluginCommandResult", fields: [{ name: "data", type: "object", summary: `What the command of the plugin ${plugin} answered.` }] },
60970
+ exitCodes: [0, 1, 2, 3, 5],
60971
+ routes: [],
60972
+ examples: [{ command: `orbital ${plugin} ${command.name}`, why: command.summary }],
60973
+ run: async (input2, context) => {
60974
+ const workspace = workspaceOf(input2, workspaceDir);
60975
+ const result2 = command.node ? await runOnNode(context, workspace, input2, plugin, requestOf(command, input2, null)) : await runInPlugin(context, workspace, input2, plugin, command, terminal);
60976
+ if (result2.text !== null) context.output.line(result2.text);
60977
+ if (result2.text === null && Object.keys(result2.data).length > 0) context.output.line(JSON.stringify(result2.data, null, 2));
60978
+ return { data: result2.data, ...result2.exitCode !== 0 && { exitCode: result2.exitCode } };
60979
+ }
60980
+ };
60981
+ }
60982
+ function pluginCommandSpecs(options) {
60983
+ const terminal = options.terminal ?? { input: 0, output: 1, error: 2 };
60984
+ return declaredPluginCommands(options.workspaceDir, options.env).filter(({ plugin }) => !options.reserved.includes(plugin)).flatMap(({ plugin, commands }) => commands.map((command) => specOf(options.workspaceDir, plugin, command, terminal)));
60985
+ }
60986
+
60676
60987
  // src/core/cli/plugin.ts
60677
60988
  function workspaceFlag() {
60678
60989
  return { name: "workspace", type: "path", summary: `The project whose ${ORBITAL_FILE} names the plugin. Default: the current directory.` };
60679
60990
  }
60680
- function workspaceOf(input2) {
60681
- return resolve14(typeof input2.flags.workspace === "string" ? input2.flags.workspace : ".");
60682
- }
60683
60991
  function sourceOrRefuse(context, input2) {
60684
60992
  const workspaceDir = workspaceOf(input2);
60685
60993
  const source = definitionSource(workspaceDir, context.env);
@@ -60703,6 +61011,25 @@ async function manifestOf(context, workspaceDir, name, source, root, config2) {
60703
61011
  await plugin.close?.();
60704
61012
  return plugin.manifest;
60705
61013
  }
61014
+ function configOf(input2, known) {
61015
+ const file2 = typeof input2.flags["config-file"] === "string" ? resolve15(input2.flags["config-file"]) : null;
61016
+ const fromFile = file2 ? (0, import_yaml8.parse)(readFileSync29(file2, "utf8")) : {};
61017
+ if (fromFile === null || typeof fromFile !== "object" || Array.isArray(fromFile)) throw new CliError({ code: "invalid-input", message: `${file2} holds no object of config keys.`, exitCode: ExitCode.Validation, hint: "Write the config as YAML or JSON keys, such as channel: general." });
61018
+ const pairs = Array.isArray(input2.flags.config) ? input2.flags.config.map(String) : typeof input2.flags.config === "string" ? [input2.flags.config] : [];
61019
+ const fromPairs = Object.fromEntries(pairs.map((pair) => {
61020
+ const at = pair.indexOf("=");
61021
+ if (at <= 0) throw new CliError({ code: "invalid-input", message: `--config ${pair} is not key=value.`, exitCode: ExitCode.Validation, hint: "Write one key and its value, such as --config channel=general." });
61022
+ return [pair.slice(0, at), (0, import_yaml8.parse)(pair.slice(at + 1)) ?? ""];
61023
+ }));
61024
+ return { ...known, ...fromFile, ...fromPairs };
61025
+ }
61026
+ async function checkedManifest(context, workspaceDir, name, source, root, config2) {
61027
+ try {
61028
+ return await manifestOf(context, workspaceDir, name, source, root, config2);
61029
+ } catch (error62) {
61030
+ throw new CliError({ code: "invalid-input", message: `${name} did not start with the config ${JSON.stringify(config2)}, so no config names it: ${error62 instanceof Error ? error62.message : String(error62)}`, exitCode: ExitCode.Validation, hint: "Give the plugin its config with --config key=value or --config-file <file>." });
61031
+ }
61032
+ }
60706
61033
  function permissionLines(manifest) {
60707
61034
  const { network, secrets, models } = manifest.permissions;
60708
61035
  return [
@@ -60718,7 +61045,9 @@ var add = () => ({
60718
61045
  flags: [
60719
61046
  workspaceFlag(),
60720
61047
  { name: "install-only", type: "boolean", summary: `Install the plugin on this machine only: do not name it in ${ORBITAL_FILE}.` },
60721
- { name: "user", type: "boolean", summary: `Add the plugin for you, in your user config, not in ${ORBITAL_FILE}: every project of yours loads it, after the plugins its ${ORBITAL_FILE} names.` }
61048
+ { name: "user", type: "boolean", summary: `Add the plugin for you, in your user config, not in ${ORBITAL_FILE}: every project of yours loads it, after the plugins its ${ORBITAL_FILE} names.` },
61049
+ { name: "config", type: "string", repeatable: true, summary: `One config key of the plugin, as key=value; the value reads as YAML, so 3 is a number and true a boolean. The plugin starts with it before ${ORBITAL_FILE} names it.` },
61050
+ { name: "config-file", type: "path", summary: "A YAML or JSON file of config keys for the plugin. A --config key wins over the same key of the file." }
60722
61051
  ],
60723
61052
  positionals: [
60724
61053
  { name: "plugin", type: "string", required: true, summary: "<name>@<marketplace>, npm:<package>, git:<url>, or a folder such as ./plugins/mine." }
@@ -60739,7 +61068,8 @@ var add = () => ({
60739
61068
  examples: [
60740
61069
  { command: "orbital plugin add slack@community", why: "Install the slack plugin from the community marketplace, and use it in this workspace." },
60741
61070
  { command: "orbital plugin add ./plugins/mine", why: `Use a plugin you write in this project: ${ORBITAL_FILE} names it by its path.` },
60742
- { command: "orbital plugin add npm:@acme/orbital-discord --install-only", why: "Install a plugin from npm on this machine, for a workspace to name later." }
61071
+ { command: "orbital plugin add npm:@acme/orbital-discord --install-only", why: "Install a plugin from npm on this machine, for a workspace to name later." },
61072
+ { command: "orbital plugin add ./plugins/notify --config channel=general --config retries=3", why: "Add a plugin that checks its config when it starts." }
60743
61073
  ],
60744
61074
  run: async (input2, context) => {
60745
61075
  const home = orbitalHome(context.env);
@@ -60751,20 +61081,22 @@ var add = () => ({
60751
61081
  if (input2.flags["install-only"] === true) return { data: { name: ref.name, source: given, installed, enabled: false, manifest: null } };
60752
61082
  if (input2.flags.user === true) {
60753
61083
  const source2 = ref.kind === "folder" && !given.includes("@") ? ref.dir : ref.kind === "npm" ? `npm:${ref.pkg}` : ref.kind === "git" ? `git:${ref.url}` : given;
60754
- const manifest2 = await manifestOf(context, workspaceOf(input2), ref.name, source2, null, {});
61084
+ const config3 = configOf(input2, userPlugins(context.env)[ref.name]?.config ?? {});
61085
+ const manifest2 = await checkedManifest(context, workspaceOf(input2), ref.name, source2, null, config3);
60755
61086
  context.output.step(`${ref.name} ${manifest2.version} reaches:`);
60756
61087
  for (const line of permissionLines(manifest2)) context.output.step(line);
60757
- setUserPlugin(context.env, ref.name, { source: source2, enabled: true, config: {}, env: [] });
61088
+ setUserPlugin(context.env, ref.name, { source: source2, enabled: true, config: config3, env: [] });
60758
61089
  context.output.step(`\u2713 your user config names ${ref.name}: source ${source2}`);
60759
61090
  return { data: { name: ref.name, source: source2, installed, enabled: true, manifest: manifest2 } };
60760
61091
  }
60761
61092
  const definition = sourceOrRefuse(context, input2);
60762
61093
  const root = definition.root;
60763
61094
  const source = sourceOf(ref, given, root);
60764
- const manifest = await manifestOf(context, workspaceOf(input2), ref.name, source, root, {});
61095
+ const config2 = configOf(input2, sourceConfig(definition).plugins[ref.name]?.config ?? {});
61096
+ const manifest = await checkedManifest(context, workspaceOf(input2), ref.name, source, root, config2);
60765
61097
  context.output.step(`${ref.name} ${manifest.version} reaches:`);
60766
61098
  for (const line of permissionLines(manifest)) context.output.step(line);
60767
- setPluginEntry(definition.configFile, ref.name, { source, enabled: true, config: sourceConfig(definition).plugins[ref.name]?.config ?? {}, env: [] });
61099
+ setPluginEntry(definition.configFile, ref.name, { source, enabled: true, config: config2, env: [] });
60768
61100
  context.output.step(`\u2713 ${basename9(definition.configFile)} names ${ref.name}: source ${source}`);
60769
61101
  return { data: { name: ref.name, source, installed, enabled: true, manifest } };
60770
61102
  }
@@ -60842,57 +61174,6 @@ var remove = () => ({
60842
61174
  return { data: { name, unnamed, uninstalled } };
60843
61175
  }
60844
61176
  });
60845
- var runCommand = () => ({
60846
- path: ["plugin", "run"],
60847
- summary: "Run one of a plugin's own commands, such as a login, and print what it answers.",
60848
- credential: "none",
60849
- flags: [workspaceFlag()],
60850
- positionals: [
60851
- { name: "plugin", type: "string", required: true, identifier: true, summary: `A plugin ${ORBITAL_FILE} names.` },
60852
- { name: "command", type: "string", required: true, summary: "The plugin's command." },
60853
- { name: "args", type: "string", summary: "Arguments for the command, after `--`.", passthrough: true }
60854
- ],
60855
- outputShape: { kind: "object", ref: "PluginCommand", fields: [{ name: "output", type: "string", summary: "What the command answered." }] },
60856
- exitCodes: [0, 1, 3],
60857
- routes: [],
60858
- examples: [{ command: "orbital plugin run slack login", why: "Run the slack plugin's login command." }],
60859
- run: async (input2, context) => {
60860
- const name = String(input2.positionals.plugin);
60861
- const plugin = await startNamed(context, input2, name);
60862
- try {
60863
- const command = String(input2.positionals.command);
60864
- if (!plugin.manifest.commands.some((one) => one.name === command)) {
60865
- throw new CliError({ code: "not-found", message: `The plugin ${name} has no command ${command}.`, exitCode: ExitCode.Validation, hint: plugin.manifest.commands.length > 0 ? `Its commands: ${plugin.manifest.commands.map((one) => one.name).join(", ")}.` : "It has no commands." });
60866
- }
60867
- const output2 = await plugin.command(command, [...input2.passthrough]);
60868
- context.output.line(output2);
60869
- return { data: { output: output2 } };
60870
- } finally {
60871
- await plugin.close?.();
60872
- }
60873
- }
60874
- });
60875
- async function startNamed(context, input2, name) {
60876
- const root = definitionSource(workspaceOf(input2), context.env)?.root ?? null;
60877
- const config2 = workspaceConfig(workspaceOf(input2), context.env);
60878
- const entry = config2?.plugins[name];
60879
- if (!config2 || !entry) throw new CliError({ code: "not-found", message: `No plugin ${name} is named in ${ORBITAL_FILE} or found in ${PROJECT_PLUGINS_DIR}.`, exitCode: ExitCode.Validation, nextCommand: `${commandPrefix(context.env)} plugin add ${name}@<marketplace>` });
60880
- const secrets = new PluginSecrets(context.env);
60881
- return startRemotePlugin({
60882
- name,
60883
- source: pluginSource(name, entry),
60884
- config: entry.config,
60885
- entryEnv: entry.env,
60886
- node: { name: `plugin-${name}`, workspaceDir: workspaceOf(input2), harness: defaultHarness(context.env), harnesses: [...RUNNER_HARNESSES], once: true, idleExitMs: null, project: null },
60887
- roots: { harnessRoot: root, home: orbitalHome(context.env) },
60888
- offline: config2.offline,
60889
- policy: new PolicyChain([]),
60890
- env: context.env,
60891
- log: (level, message) => level === "info" ? context.output.step(message) : context.output.warn(message),
60892
- secret: async (secret) => secrets.get(name, secret),
60893
- restarts: 0
60894
- });
60895
- }
60896
61177
  var check2 = () => ({
60897
61178
  path: ["plugin", "check"],
60898
61179
  summary: "Run the conformance checks of each port a plugin provides, so any orbital host can use it.",
@@ -60918,8 +61199,8 @@ var check2 = () => ({
60918
61199
  ],
60919
61200
  run: async (input2, context) => {
60920
61201
  const name = String(input2.positionals.plugin);
60921
- const fixtures = typeof input2.flags.fixtures === "string" ? JSON.parse(readFileSync28(input2.flags.fixtures, "utf8")) : {};
60922
- const plugin = await startNamed(context, input2, name);
61202
+ const fixtures = typeof input2.flags.fixtures === "string" ? JSON.parse(readFileSync29(input2.flags.fixtures, "utf8")) : {};
61203
+ const plugin = await startWorkspacePlugin(context, workspaceOf(input2), name);
60923
61204
  try {
60924
61205
  const results = await runConformance(plugin.ports, fixtures);
60925
61206
  const mark = { pass: "\u2713", fail: "\u2717", skip: "\u25CB" };
@@ -61047,13 +61328,13 @@ var secretRemove = {
61047
61328
  return { data: { plugin, name } };
61048
61329
  }
61049
61330
  };
61050
- var pluginCommands = () => [add(), list3(), remove(), runCommand(), check2(), marketplaceAdd, marketplaceList, marketplaceUpdate, marketplaceRemove, secretSet, secretRemove];
61331
+ var pluginCommands = () => [add(), list3(), remove(), check2(), marketplaceAdd, marketplaceList, marketplaceUpdate, marketplaceRemove, secretSet, secretRemove];
61051
61332
 
61052
61333
  // src/core/cli/plugin-new.ts
61053
- import { existsSync as existsSync30, mkdirSync as mkdirSync22, readdirSync as readdirSync20, writeFileSync as writeFileSync20 } from "node:fs";
61054
- import { dirname as dirname16, join as join37, relative as relative7, resolve as resolve15 } from "node:path";
61334
+ import { existsSync as existsSync31, mkdirSync as mkdirSync22, readdirSync as readdirSync20, writeFileSync as writeFileSync20 } from "node:fs";
61335
+ import { dirname as dirname16, join as join38, relative as relative7, resolve as resolve16 } from "node:path";
61055
61336
  var PLUGIN_PORTS = ["channel", "store", "workspace", "harness", "panel", "policy", "telemetry", "actions", "workflows", "command"];
61056
- var SDK_VERSION = true ? `^${"0.0.3"}` : "*";
61337
+ var SDK_VERSION = true ? `^${"0.0.4"}` : "*";
61057
61338
  var ESBUILD_VERSION = "^0.25.0";
61058
61339
  var TYPES_NODE_VERSION = "^20.19.43";
61059
61340
  var HARNESS_ID = /^[a-z][a-z0-9-]{0,31}$/;
@@ -61432,7 +61713,7 @@ export function plugin(context: PluginContext): PluginImplementation {
61432
61713
  switch (event.kind) {
61433
61714
  case 'mission.state': {
61434
61715
  const known = graph.missions.find((mission) => mission.missionId === event.mission);
61435
- const state = { missionId: event.mission, team: event.team, state: event.to, sessionId: event.sessionId, lead: event.lead, giver: event.giver, givenBy: known?.givenBy ?? null, question: event.question, result: event.result, updatedAt: event.at };
61716
+ const state = { missionId: event.mission, team: event.team, state: event.to, sessionId: event.sessionId, lead: event.lead, receivers: event.receivers, givenBy: known?.givenBy ?? null, question: event.question, result: event.result, updatedAt: event.at };
61436
61717
  graph.missions = [...graph.missions.filter((mission) => mission.missionId !== event.mission), state];
61437
61718
  return;
61438
61719
  }
@@ -61573,7 +61854,7 @@ export function plugin(_context: PluginContext): PluginImplementation {
61573
61854
  case "command":
61574
61855
  return {
61575
61856
  summary: `An orbital plugin with a command: ${name}.`,
61576
- plugin: `${header("one command. A person runs it with `orbital plugin run " + name + " hello [name]`.")}import type { PluginContext, PluginImplementation } from '@kaiba-cloud/orbital-sdk';
61857
+ plugin: `${header("one command. A person runs it with `orbital " + name + " hello [name]`.\n * orbital-plugin.json declares the same command, so orbital lists it without starting the plugin.")}import { defineCommand, type PluginContext, type PluginImplementation } from '@kaiba-cloud/orbital-sdk';
61577
61858
 
61578
61859
  export const NAME = '${name}';
61579
61860
 
@@ -61583,17 +61864,19 @@ export function plugin(context: PluginContext): PluginImplementation {
61583
61864
  version: '0.1.0',
61584
61865
  permissions: { network: [], secrets: [], models: [] },
61585
61866
  ports: {},
61586
- commands: {
61587
- hello: {
61588
- summary: 'Greet the name given, or the node.',
61589
- run: async (args) => \`Hello, \${args.join(' ') || context.init.node.name}.\`,
61590
- },
61591
- },
61867
+ commands: [
61868
+ defineCommand({
61869
+ name: 'hello',
61870
+ summary: 'Greet the name given, or the workspace.',
61871
+ args: [{ name: 'name', summary: 'The name to greet.' }],
61872
+ run: async ({ args }) => \`Hello, \${args.name ?? context.init.node.workspaceDir}.\`,
61873
+ }),
61874
+ ],
61592
61875
  };
61593
61876
  }
61594
61877
  `,
61595
61878
  fixtures: {},
61596
- test: ` assert.equal(await implementation.commands?.hello?.run(['Ada']), 'Hello, Ada.');`
61879
+ test: ` assert.equal(await implementation.commands?.[0]?.run({ args: { name: 'Ada' }, options: {}, terminal: null }), 'Hello, Ada.');`
61597
61880
  };
61598
61881
  }
61599
61882
  }
@@ -61627,10 +61910,11 @@ ${template.test ? `${template.test}
61627
61910
  ` : ""}});
61628
61911
  `;
61629
61912
  }
61913
+ var COMMAND_DECLARATION = { name: "hello", summary: "Greet the name given, or the workspace.", args: [{ name: "name", summary: "The name to greet." }] };
61630
61914
  function pluginFiles(name, port) {
61631
61915
  const template = templateOf(name, port);
61632
61916
  const files = {
61633
- "orbital-plugin.json": `${JSON.stringify({ name, command: ["node", "dist/plugin.js"], ...port === "harness" && { harnesses: [{ id: name, folder: `.${name}`, instructions: `${name.toUpperCase()}.md`, agents: "agents", skills: "skills" }] } }, null, 2)}
61917
+ "orbital-plugin.json": `${JSON.stringify({ name, command: ["node", "dist/plugin.js"], ...port === "harness" && { harnesses: [{ id: name, folder: `.${name}`, instructions: `${name.toUpperCase()}.md`, agents: "agents", skills: "skills" }] }, ...port === "command" && { commands: [COMMAND_DECLARATION] } }, null, 2)}
61634
61918
  `,
61635
61919
  "package.json": `${JSON.stringify({
61636
61920
  name,
@@ -61688,14 +61972,14 @@ var pluginNewCommand = {
61688
61972
  if (port === "harness" && !HARNESS_ID.test(name)) {
61689
61973
  throw new CliError({ code: "invalid-input", message: `"${name}" is not a harness id.`, exitCode: ExitCode.Validation, hint: "A harness plugin's name starts with a letter and has at most 32 characters." });
61690
61974
  }
61691
- const dir = resolve15(typeof input2.flags.dir === "string" ? input2.flags.dir : name);
61692
- if (existsSync30(dir) && readdirSync20(dir).length > 0) {
61975
+ const dir = resolve16(typeof input2.flags.dir === "string" ? input2.flags.dir : name);
61976
+ if (existsSync31(dir) && readdirSync20(dir).length > 0) {
61693
61977
  throw new CliError({ code: "conflict", message: `${dir} is not empty.`, exitCode: ExitCode.Validation, hint: "Name a new folder with --dir." });
61694
61978
  }
61695
61979
  const files = pluginFiles(name, port);
61696
61980
  for (const [path3, content] of Object.entries(files)) {
61697
- mkdirSync22(dirname16(join37(dir, path3)), { recursive: true });
61698
- writeFileSync20(join37(dir, path3), content);
61981
+ mkdirSync22(dirname16(join38(dir, path3)), { recursive: true });
61982
+ writeFileSync20(join38(dir, path3), content);
61699
61983
  }
61700
61984
  const program = commandPrefix(context.env);
61701
61985
  const shown = relative7(process.cwd(), dir) || ".";
@@ -61838,8 +62122,8 @@ If the browser does not return here in ${CALLBACK_WAIT_MS / 6e4} minutes, paste
61838
62122
  ${url2}
61839
62123
  Then paste the code, or the full URL the browser ends on.`);
61840
62124
  },
61841
- onManualCodeInput: () => new Promise((resolve25) => {
61842
- timer = setTimeout(() => resolve25(""), opened2 ? CALLBACK_WAIT_MS : 0);
62125
+ onManualCodeInput: () => new Promise((resolve26) => {
62126
+ timer = setTimeout(() => resolve26(""), opened2 ? CALLBACK_WAIT_MS : 0);
61843
62127
  timer.unref();
61844
62128
  }),
61845
62129
  onPrompt: async (prompt) => (await context.ask(`${prompt.message} `))?.trim() ?? "",
@@ -61932,12 +62216,12 @@ function createProviderCommands(signIn = loginAnthropic) {
61932
62216
  }
61933
62217
 
61934
62218
  // src/core/cli/run.ts
61935
- import { resolve as resolve17 } from "node:path";
62219
+ import { resolve as resolve18 } from "node:path";
61936
62220
 
61937
62221
  // src/core/local/local-run.ts
61938
62222
  import { randomBytes as randomBytes4, randomUUID as randomUUID8 } from "node:crypto";
61939
62223
  import { mkdirSync as mkdirSync23, rmSync as rmSync12 } from "node:fs";
61940
- import { join as join38, resolve as resolve16 } from "node:path";
62224
+ import { join as join39, resolve as resolve17 } from "node:path";
61941
62225
 
61942
62226
  // src/core/local/interrupt.ts
61943
62227
  var EXIT_CODE = { SIGINT: 130, SIGTERM: 143 };
@@ -61960,7 +62244,7 @@ function cleanupOnInterrupt(cleanup, options = {}) {
61960
62244
  async function runLocal(context, request, plugins) {
61961
62245
  const { workspaceDir } = request;
61962
62246
  const home = orbitalHome(context.env);
61963
- const sessionsDir = join38(home, "sessions");
62247
+ const sessionsDir = join39(home, "sessions");
61964
62248
  mkdirSync23(sessionsDir, { recursive: true, mode: 448 });
61965
62249
  const resumed = request.resumeSessionId ? readSessionRecord(sessionsDir, request.resumeSessionId) : null;
61966
62250
  if (request.resumeSessionId && !resumed) {
@@ -61990,9 +62274,9 @@ async function runLocal(context, request, plugins) {
61990
62274
  let channels = null;
61991
62275
  const asks = new PersonDesk(async (question, name) => await channels?.askPerson(`${name} asks: ${question}`, null) ?? null);
61992
62276
  const policy = new PolicyChain(ports.policies(), asks.ask);
61993
- channels = new ChannelHub({ context, ports, policy, self: nodeName, stateDir: join38(home, "channels", nodeName), plugins: () => definition.config.plugins });
62277
+ channels = new ChannelHub({ context, ports, policy, self: nodeName, stateDir: join39(home, "channels", nodeName), plugins: () => definition.config.plugins });
61994
62278
  const models = modelGatewayOf(context, policy, remote);
61995
- const actions = nodeActions({ ports: () => ports, policy: () => policy, definition: { workspaceDir: resolve16(workspaceDir), definition }, span: (span2) => host.telemetry.span(span2), now: context.now });
62279
+ const actions = nodeActions({ ports: () => ports, policy: () => policy, definition: { workspaceDir: resolve17(workspaceDir), definition }, span: (span2) => host.telemetry.span(span2), now: context.now });
61996
62280
  const agentName = request.agent ?? resumed?.agent ?? null;
61997
62281
  const target = await resolveTarget(definition, agentName, ports);
61998
62282
  const harness = request.harness ?? resumed?.harness ?? defaultHarness(context.env);
@@ -62053,7 +62337,7 @@ async function runLocal(context, request, plugins) {
62053
62337
  const result2 = chat ? await chatTurns(chat, host, { host, asks, panels: () => panelsOf(ownPanels, ports.plugins), sessionId, agent: target.local?.name ?? target.ref.name, harness, model: target.local?.model ?? null, workspaceDir, node: nodeName }) : request.interactive ? await interactiveTurn(context, request, { host, ports, policy, definition, target, sessionId, harness, harnessArgs, credentials, home, sessionsDir, node: { url: `${node2.info.url}/a2a`, token: node2.sessionToken(sessionId) } }) : await headlessTurn(context, request, host, { sessionId, agent: target.local?.name ?? target.ref.name, harness });
62054
62338
  if (result2 !== null) context.output.line(result2);
62055
62339
  const cli = commandPrefix(context.env);
62056
- context.output.step(`\u2713 transcript: ${join38(sessionsDir, sessionId, "conversation.jsonl")}`);
62340
+ context.output.step(`\u2713 transcript: ${join39(sessionsDir, sessionId, "conversation.jsonl")}`);
62057
62341
  context.output.step(`\u2713 resume with: ${cli} run --resume ${sessionId}`);
62058
62342
  return {
62059
62343
  sessionId,
@@ -62061,7 +62345,7 @@ async function runLocal(context, request, plugins) {
62061
62345
  done: true,
62062
62346
  harness,
62063
62347
  result: result2,
62064
- transcript: join38(sessionsDir, sessionId, "conversation.jsonl"),
62348
+ transcript: join39(sessionsDir, sessionId, "conversation.jsonl"),
62065
62349
  interactive: request.interactive
62066
62350
  };
62067
62351
  } finally {
@@ -62121,7 +62405,7 @@ async function interactiveTurn(context, request, run2) {
62121
62405
  const info = sessionInfoOf(run2.sessionId, run2.target, { node: run2.host.node, agent: run2.target.local?.name ?? run2.target.ref.name, parentSessionId: null, workspaceDir: request.workspaceDir, harness: run2.harness, team: null, mission: null, definition: request.useDefinition ? run2.definition : null });
62122
62406
  const opened2 = await openSession(run2.ports, run2.policy, info);
62123
62407
  const record2 = run2.host.attach(info, opened2.sinks, { agent: info.agent, harness: run2.harness, workspaceDir: request.workspaceDir });
62124
- const runDir = join38(run2.home, "runs", randomUUID8());
62408
+ const runDir = join39(run2.home, "runs", randomUUID8());
62125
62409
  mkdirSync23(runDir, { recursive: true, mode: 448 });
62126
62410
  let usage = [];
62127
62411
  try {
@@ -62228,7 +62512,7 @@ function createRunCommand(options) {
62228
62512
  const terminal = log ? await options.terminal() : null;
62229
62513
  const data = await runLocal(log ? { ...context, output: log.output(context.output) } : context, {
62230
62514
  agent,
62231
- workspaceDir: resolve17(typeof input2.flags.workspace === "string" ? input2.flags.workspace : "."),
62515
+ workspaceDir: resolve18(typeof input2.flags.workspace === "string" ? input2.flags.workspace : "."),
62232
62516
  prompt,
62233
62517
  useDefinition: true,
62234
62518
  follow: false,
@@ -62244,24 +62528,24 @@ function createRunCommand(options) {
62244
62528
  }
62245
62529
 
62246
62530
  // src/core/cli/session.ts
62247
- import { resolve as resolve18 } from "node:path";
62531
+ import { resolve as resolve19 } from "node:path";
62248
62532
 
62249
62533
  // src/core/local/attachments.ts
62250
- import { existsSync as existsSync32, mkdirSync as mkdirSync24, readFileSync as readFileSync29, writeFileSync as writeFileSync21 } from "node:fs";
62251
- import { join as join39 } from "node:path";
62534
+ import { existsSync as existsSync33, mkdirSync as mkdirSync24, readFileSync as readFileSync30, writeFileSync as writeFileSync21 } from "node:fs";
62535
+ import { join as join40 } from "node:path";
62252
62536
  function fileOf(home, externalId) {
62253
- return join39(home, "attachments", `${encodeURIComponent(externalId)}.json`);
62537
+ return join40(home, "attachments", `${encodeURIComponent(externalId)}.json`);
62254
62538
  }
62255
62539
  function writeAttachment(home, externalId, node2) {
62256
- mkdirSync24(join39(home, "attachments"), { recursive: true, mode: 448 });
62540
+ mkdirSync24(join40(home, "attachments"), { recursive: true, mode: 448 });
62257
62541
  writeFileSync21(fileOf(home, externalId), `${JSON.stringify({ node: node2 })}
62258
62542
  `, { mode: 384 });
62259
62543
  }
62260
62544
  function readAttachment(home, externalId) {
62261
62545
  const file2 = fileOf(home, externalId);
62262
- if (!existsSync32(file2)) return null;
62546
+ if (!existsSync33(file2)) return null;
62263
62547
  try {
62264
- const parsed = JSON.parse(readFileSync29(file2, "utf8"));
62548
+ const parsed = JSON.parse(readFileSync30(file2, "utf8"));
62265
62549
  return typeof parsed === "object" && parsed !== null && "node" in parsed && typeof parsed.node === "string" ? parsed.node : null;
62266
62550
  } catch {
62267
62551
  return null;
@@ -62329,7 +62613,7 @@ async function joinMission(context, attached, missionId) {
62329
62613
  const body2 = await response.json();
62330
62614
  if (!response.ok) {
62331
62615
  const error62 = typeof body2 === "object" && body2 !== null && "error" in body2 ? String(body2.error) : `The node answered ${response.status}.`;
62332
- throw new CliError({ code: "invalid-input", message: `The node refused to make this session the giver of ${missionId}: ${error62}`, exitCode: ExitCode.Validation });
62616
+ throw new CliError({ code: "invalid-input", message: `The node refused to make this session a decision receiver of ${missionId}: ${error62}`, exitCode: ExitCode.Validation });
62333
62617
  }
62334
62618
  return MissionRecordSchema.parse(body2);
62335
62619
  }
@@ -62409,7 +62693,7 @@ var sessionAttachCommand = {
62409
62693
  { name: "node", type: "string", summary: "The node to join. Default: ORBITAL_NODE, then the node key of the definition, then the node of the folder." },
62410
62694
  { name: "team", type: "string", summary: "The team the session joins. Sessions of one team are peers." },
62411
62695
  { name: "external-id", type: "string", summary: "The id the outside program gives the session. With it, the command attaches again after a node restarts." },
62412
- { name: "mission", type: "string", summary: "A mission id: join the node that runs it, from any folder, as the giver of the mission, so its questions come to this session." }
62696
+ { name: "mission", type: "string", summary: "A mission id: join the node that runs it, from any folder, as a decision receiver of the mission, so its questions come to this session." }
62413
62697
  ],
62414
62698
  positionals: [],
62415
62699
  outputShape: {
@@ -62426,7 +62710,7 @@ var sessionAttachCommand = {
62426
62710
  examples: [
62427
62711
  { command: "orbital session attach --harness claude-code --output ndjson", why: "Join the node of this folder, and print each delivery as a record." },
62428
62712
  { command: "orbital session attach --node shared --team review --output ndjson", why: "Join the node shared, as a member of the team review." },
62429
- { command: "orbital session attach --mission build/pr-7 --output ndjson", why: "Join the node that runs the mission build/pr-7 as its giver, and print each question of its lead as a record." }
62713
+ { command: "orbital session attach --mission build/pr-7 --output ndjson", why: "Join the node that runs the mission build/pr-7 as a decision receiver, and print each question of its lead as a record." }
62430
62714
  ],
62431
62715
  run: async (input2, context) => {
62432
62716
  const harness = typeof input2.flags.harness === "string" ? input2.flags.harness : "external";
@@ -62438,7 +62722,7 @@ var sessionAttachCommand = {
62438
62722
  const found = await keepAttached(context, {
62439
62723
  harness,
62440
62724
  agent: typeof input2.flags.agent === "string" ? input2.flags.agent : harness,
62441
- workspaceDir: resolve18(typeof input2.flags.workspace === "string" ? input2.flags.workspace : "."),
62725
+ workspaceDir: resolve19(typeof input2.flags.workspace === "string" ? input2.flags.workspace : "."),
62442
62726
  ...typeof input2.flags.node === "string" && { node: input2.flags.node },
62443
62727
  team: typeof input2.flags.team === "string" ? input2.flags.team : null,
62444
62728
  externalId: typeof input2.flags["external-id"] === "string" ? input2.flags["external-id"] : null,
@@ -62451,6 +62735,8 @@ var sessionAttachCommand = {
62451
62735
  delivery: (event) => {
62452
62736
  const delivery = DeliverySchema.safeParse(event);
62453
62737
  if (delivery.success) context.output.record({ type: StreamRecordType.Event, ...delivery.data });
62738
+ const answered = AnsweredSchema.safeParse(event);
62739
+ if (answered.success) context.output.record({ type: StreamRecordType.Event, ...answered.data });
62454
62740
  },
62455
62741
  detached: (reason) => {
62456
62742
  context.output.record({ type: StreamRecordType.Event, kind: "detached", reason });
@@ -62538,8 +62824,8 @@ var remove3 = {
62538
62824
  var workspaceCommands = [list5, show, remove3];
62539
62825
 
62540
62826
  // src/core/cli/up.ts
62541
- import { existsSync as existsSync33 } from "node:fs";
62542
- import { resolve as resolve19 } from "node:path";
62827
+ import { existsSync as existsSync34 } from "node:fs";
62828
+ import { resolve as resolve20 } from "node:path";
62543
62829
 
62544
62830
  // src/core/cli/node-control.ts
62545
62831
  var STOP_WAIT_MS = 3e4;
@@ -62613,7 +62899,7 @@ function groupOf(state) {
62613
62899
  }
62614
62900
  }
62615
62901
  function missionGroup(mission) {
62616
- return waitsOnGiver(mission) ? "waiting" : groupOf(mission.state);
62902
+ return waitsOnDecision(mission) ? "waiting" : groupOf(mission.state);
62617
62903
  }
62618
62904
  var SESSIONS_PER_MEMBER = 3;
62619
62905
  var RUNNING_SHOWN = 5;
@@ -63092,7 +63378,7 @@ async function liveView(context, nodes, signal, terminal, home) {
63092
63378
  for (const line of events(viewed, states.get(one.node), now)) context.output.line(line);
63093
63379
  states.set(one.node, now);
63094
63380
  }
63095
- await Promise.race([context.sleep(REFRESH_MS), new Promise((resolve25) => signal.addEventListener("abort", () => resolve25(), { once: true }))]);
63381
+ await Promise.race([context.sleep(REFRESH_MS), new Promise((resolve26) => signal.addEventListener("abort", () => resolve26(), { once: true }))]);
63096
63382
  }
63097
63383
  context.output.line(closing);
63098
63384
  }
@@ -63101,7 +63387,7 @@ async function liveView(context, nodes, signal, terminal, home) {
63101
63387
  var WORKSPACE_FLAG = { name: "workspace", type: "path", summary: "The project. Default: the current directory." };
63102
63388
  var TEAMS = { name: "teams", type: "string", variadic: true, summary: "Only the nodes of these teams. Default: every node of the project." };
63103
63389
  async function planOf(context, input2) {
63104
- const workspaceDir = resolve19(typeof input2.flags.workspace === "string" ? input2.flags.workspace : ".");
63390
+ const workspaceDir = resolve20(typeof input2.flags.workspace === "string" ? input2.flags.workspace : ".");
63105
63391
  const { definition, refusals } = await loadDefinitionPartial(workspaceDir, context.env);
63106
63392
  for (const refusal of refusals) context.output.warn(refusal.message);
63107
63393
  const project = projectNodeOf(definition, workspaceDir);
@@ -63248,7 +63534,7 @@ var statusCommand = {
63248
63534
  for (const { charters: _charters, named: _named, ...planned } of nodes) {
63249
63535
  const log = nodeLogPath(home, planned.node);
63250
63536
  const supervisor = readSupervisor(home, planned.node);
63251
- if (existsSync33(log)) logs.push({ node: planned.node, log, lastCrash: supervisor?.crashes.at(-1) ?? null, lastExit: supervisor?.lastExit ?? null, gaveUp: supervisor?.gaveUp ?? null });
63537
+ if (existsSync34(log)) logs.push({ node: planned.node, log, lastCrash: supervisor?.crashes.at(-1) ?? null, lastExit: supervisor?.lastExit ?? null, gaveUp: supervisor?.gaveUp ?? null });
63252
63538
  const running = await liveNode(home, planned.node, context.fetch);
63253
63539
  const served = running ? await servedTeams(context, running) : null;
63254
63540
  for (const note of served ?? []) if (note.problems.length > 0 || note.retiring) notes.push({ node: planned.node, ...note });
@@ -63281,7 +63567,7 @@ var downCommand = {
63281
63567
  const { workspaceDir, nodes } = await planOf(context, input2);
63282
63568
  const home = orbitalHome(context.env);
63283
63569
  const rows = [];
63284
- const separate2 = (await liveNodes(home, context.fetch)).filter((info) => resolve19(info.workspaceDir) === resolve19(workspaceDir) && nodes.some((planned) => new RegExp(`^${planned.node}-[0-9]+$`).test(info.name)));
63570
+ const separate2 = (await liveNodes(home, context.fetch)).filter((info) => resolve20(info.workspaceDir) === resolve20(workspaceDir) && nodes.some((planned) => new RegExp(`^${planned.node}-[0-9]+$`).test(info.name)));
63285
63571
  const targets = [...nodes.map(({ charters: _charters, named: _named, ...planned }) => planned), ...separate2.map((info) => ({ node: info.name, teams: nodes.find((planned) => info.name.startsWith(`${planned.node}-`))?.teams ?? [] }))];
63286
63572
  for (const planned of targets) {
63287
63573
  const running = await liveNode(home, planned.node, context.fetch);
@@ -63340,7 +63626,7 @@ var createJoinCommand = (options) => ({
63340
63626
  }
63341
63627
  let charters = [];
63342
63628
  try {
63343
- const workspaceDir = resolve19(typeof input2.flags.workspace === "string" ? input2.flags.workspace : ".");
63629
+ const workspaceDir = resolve20(typeof input2.flags.workspace === "string" ? input2.flags.workspace : ".");
63344
63630
  charters = (await loadDefinition(workspaceDir, context.env)).teams;
63345
63631
  } catch {
63346
63632
  charters = [];
@@ -63365,9 +63651,9 @@ function createUpCommands(options) {
63365
63651
  // src/core/cli/service.ts
63366
63652
  import { spawnSync as spawnSync5 } from "node:child_process";
63367
63653
  import { createHash as createHash10 } from "node:crypto";
63368
- import { existsSync as existsSync34, mkdirSync as mkdirSync25, rmSync as rmSync13, writeFileSync as writeFileSync22 } from "node:fs";
63654
+ import { existsSync as existsSync35, mkdirSync as mkdirSync25, rmSync as rmSync13, writeFileSync as writeFileSync22 } from "node:fs";
63369
63655
  import { homedir as homedir8 } from "node:os";
63370
- import { join as join40, resolve as resolve20 } from "node:path";
63656
+ import { join as join41, resolve as resolve21 } from "node:path";
63371
63657
  var exec = (command, args) => {
63372
63658
  const result2 = spawnSync5(command, args, { encoding: "utf8" });
63373
63659
  return { status: result2.status, output: `${result2.stdout ?? ""}${result2.stderr ?? ""}${result2.error?.message ?? ""}` };
@@ -63385,7 +63671,7 @@ function serviceSpec(env, workspaceDir) {
63385
63671
  workspaceDir,
63386
63672
  program: [process.execPath, installedProgram(), ...mountPath(), "up", "--detach", "--workspace", workspaceDir],
63387
63673
  env: Object.fromEntries(KEPT_ENV.flatMap((name) => env[name] ? [[name, env[name]]] : [])),
63388
- log: join40(orbitalHome(env), "logs", `service-${hash2}.log`)
63674
+ log: join41(orbitalHome(env), "logs", `service-${hash2}.log`)
63389
63675
  };
63390
63676
  }
63391
63677
  function xml(text3) {
@@ -63448,7 +63734,7 @@ function userHome(env) {
63448
63734
  return env.HOME ?? homedir8();
63449
63735
  }
63450
63736
  function fileOf2(platform, env, label) {
63451
- return platform === "launchd" ? join40(userHome(env), "Library", "LaunchAgents", `${label}.plist`) : join40(env.XDG_CONFIG_HOME ?? join40(userHome(env), ".config"), "systemd", "user", `${label}.service`);
63737
+ return platform === "launchd" ? join41(userHome(env), "Library", "LaunchAgents", `${label}.plist`) : join41(env.XDG_CONFIG_HOME ?? join41(userHome(env), ".config"), "systemd", "user", `${label}.service`);
63452
63738
  }
63453
63739
  function domain2() {
63454
63740
  return `gui/${process.getuid?.() ?? 0}`;
@@ -63461,13 +63747,13 @@ function serviceState(platform, env, workspaceDir, run2 = exec) {
63461
63747
  const { label } = serviceSpec(env, workspaceDir);
63462
63748
  const file2 = fileOf2(platform, env, label);
63463
63749
  const loaded = platform === "launchd" ? run2("launchctl", ["print", `${domain2()}/${label}`]).status === 0 : run2("systemctl", ["--user", "is-enabled", `${label}.service`]).status === 0;
63464
- return { platform, label, file: file2, installed: existsSync34(file2), loaded };
63750
+ return { platform, label, file: file2, installed: existsSync35(file2), loaded };
63465
63751
  }
63466
63752
  function installService(platform, env, workspaceDir, run2 = exec) {
63467
63753
  const spec = serviceSpec(env, workspaceDir);
63468
63754
  const file2 = fileOf2(platform, env, spec.label);
63469
- mkdirSync25(join40(file2, ".."), { recursive: true });
63470
- mkdirSync25(join40(spec.log, ".."), { recursive: true, mode: 448 });
63755
+ mkdirSync25(join41(file2, ".."), { recursive: true });
63756
+ mkdirSync25(join41(spec.log, ".."), { recursive: true, mode: 448 });
63471
63757
  if (platform === "launchd") {
63472
63758
  writeFileSync22(file2, launchdPlist(spec));
63473
63759
  run2("launchctl", ["bootout", `${domain2()}/${spec.label}`]);
@@ -63489,7 +63775,7 @@ function uninstallService(platform, env, workspaceDir, run2 = exec) {
63489
63775
  return serviceState(platform, env, workspaceDir, run2);
63490
63776
  }
63491
63777
  function workspaceOf2(input2) {
63492
- return resolve20(typeof input2.flags.workspace === "string" ? input2.flags.workspace : ".");
63778
+ return resolve21(typeof input2.flags.workspace === "string" ? input2.flags.workspace : ".");
63493
63779
  }
63494
63780
  function platformOrRefuse() {
63495
63781
  const platform = platformOf();
@@ -63564,9 +63850,9 @@ var serviceCommands = [
63564
63850
  ];
63565
63851
 
63566
63852
  // src/core/cli/browser.ts
63567
- import { existsSync as existsSync35, mkdirSync as mkdirSync26, readFileSync as readFileSync30, writeFileSync as writeFileSync23 } from "node:fs";
63853
+ import { existsSync as existsSync36, mkdirSync as mkdirSync26, readFileSync as readFileSync31, writeFileSync as writeFileSync23 } from "node:fs";
63568
63854
  import { homedir as homedir9 } from "node:os";
63569
- import { dirname as dirname17, join as join41 } from "node:path";
63855
+ import { dirname as dirname17, join as join42 } from "node:path";
63570
63856
 
63571
63857
  // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.2_supports-color@7.2.0_zod@4.6.7/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/transport.js
63572
63858
  function normalizeHeaders(headers) {
@@ -65179,7 +65465,7 @@ function isChromeDevtools(entry) {
65179
65465
  return isChromeDevtoolsMcp(command, args);
65180
65466
  }
65181
65467
  function claudeJsonPath(env) {
65182
- return join41(env.CLAUDE_CONFIG_DIR ?? env.HOME ?? homedir9(), ".claude.json");
65468
+ return join42(env.CLAUDE_CONFIG_DIR ?? env.HOME ?? homedir9(), ".claude.json");
65183
65469
  }
65184
65470
  function chromeServersOf(config2) {
65185
65471
  return Object.entries(config2.mcpServers ?? {}).filter(([, entry]) => isChromeDevtools(entry)).map(([name]) => name);
@@ -65191,21 +65477,21 @@ function sharedBrowserEntry() {
65191
65477
  return { type: "stdio", ...sharedBrowserCommand(), env: {} };
65192
65478
  }
65193
65479
  function backupPath(env) {
65194
- return join41(browserDir(orbitalHome(env)), "claude-mcp-backup.json");
65480
+ return join42(browserDir(orbitalHome(env)), "claude-mcp-backup.json");
65195
65481
  }
65196
65482
  function codexBackupPath(env) {
65197
- return join41(browserDir(orbitalHome(env)), "codex-config-backup.json");
65483
+ return join42(browserDir(orbitalHome(env)), "codex-config-backup.json");
65198
65484
  }
65199
65485
  function codexConfigPath(env) {
65200
- return join41(env.CODEX_HOME ?? join41(env.HOME ?? homedir9(), ".codex"), "config.toml");
65486
+ return join42(env.CODEX_HOME ?? join42(env.HOME ?? homedir9(), ".codex"), "config.toml");
65201
65487
  }
65202
65488
  function pointClaudeAtSharedBrowser(env, entry = sharedBrowserEntry()) {
65203
65489
  const file2 = claudeJsonPath(env);
65204
- const config2 = existsSync35(file2) ? JSON.parse(readFileSync30(file2, "utf8")) : {};
65490
+ const config2 = existsSync36(file2) ? JSON.parse(readFileSync31(file2, "utf8")) : {};
65205
65491
  const found = chromeServersOf(config2);
65206
65492
  const names = found.length > 0 ? found : [SHARED_BROWSER_SERVER];
65207
65493
  const backup = backupPath(env);
65208
- const previous = existsSync35(backup) ? JSON.parse(readFileSync30(backup, "utf8")) : {};
65494
+ const previous = existsSync36(backup) ? JSON.parse(readFileSync31(backup, "utf8")) : {};
65209
65495
  const saved = { ...Object.fromEntries(names.filter((name) => config2.mcpServers?.[name] !== void 0).map((name) => [name, config2.mcpServers[name]])), ...previous };
65210
65496
  mkdirSync26(browserDir(orbitalHome(env)), { recursive: true, mode: 448 });
65211
65497
  writeFileSync23(backup, `${JSON.stringify(saved, null, 2)}
@@ -65218,9 +65504,9 @@ function pointClaudeAtSharedBrowser(env, entry = sharedBrowserEntry()) {
65218
65504
  function revertClaudeBrowser(env) {
65219
65505
  const file2 = claudeJsonPath(env);
65220
65506
  const backup = backupPath(env);
65221
- if (!existsSync35(backup)) return null;
65222
- const saved = JSON.parse(readFileSync30(backup, "utf8"));
65223
- const config2 = existsSync35(file2) ? JSON.parse(readFileSync30(file2, "utf8")) : {};
65507
+ if (!existsSync36(backup)) return null;
65508
+ const saved = JSON.parse(readFileSync31(backup, "utf8"));
65509
+ const config2 = existsSync36(file2) ? JSON.parse(readFileSync31(file2, "utf8")) : {};
65224
65510
  const shared = Object.entries(config2.mcpServers ?? {}).filter(([, entry]) => Array.isArray(entry.args) && entry.args.join(" ").endsWith("browser connect")).map(([name]) => name);
65225
65511
  const servers = { ...config2.mcpServers };
65226
65512
  for (const name of shared) delete servers[name];
@@ -65231,18 +65517,18 @@ function revertClaudeBrowser(env) {
65231
65517
  }
65232
65518
  function pointCodexAtSharedBrowser(env, shared = sharedBrowserCommand()) {
65233
65519
  const file2 = codexConfigPath(env);
65234
- if (!existsSync35(file2) && !existsSync35(dirname17(file2))) return null;
65235
- const text3 = existsSync35(file2) ? readFileSync30(file2, "utf8") : "";
65520
+ if (!existsSync36(file2) && !existsSync36(dirname17(file2))) return null;
65521
+ const text3 = existsSync36(file2) ? readFileSync31(file2, "utf8") : "";
65236
65522
  const tables = mcpServerTables(text3);
65237
65523
  const found = tables.filter((table2) => isChromeDevtoolsMcp(table2.command, table2.args)).map((table2) => table2.name);
65238
65524
  const connected = tables.filter((table2) => isSharedBrowserConnect(table2.args)).map((table2) => table2.name);
65239
65525
  const names = found.length > 0 ? found : connected.length > 0 ? [] : [SHARED_BROWSER_SERVER];
65240
65526
  const backup = codexBackupPath(env);
65241
- if (names.length === 0) return { file: file2, servers: connected, backup: existsSync35(backup) ? backup : null };
65527
+ if (names.length === 0) return { file: file2, servers: connected, backup: existsSync36(backup) ? backup : null };
65242
65528
  const block = (name) => stdioServerBlock(name, shared.command, shared.args);
65243
65529
  const replaced = replaceMcpServers(text3, (table2) => names.includes(table2.name) ? block(table2.name) : null);
65244
65530
  const next = names.filter((name) => !replaced.replaced.some((one) => one.name === name)).reduce((current2, name) => appendMcpServer(current2, block(name)), replaced.text);
65245
- const previous = existsSync35(backup) ? JSON.parse(readFileSync30(backup, "utf8")) : {};
65531
+ const previous = existsSync36(backup) ? JSON.parse(readFileSync31(backup, "utf8")) : {};
65246
65532
  const saved = { ...Object.fromEntries(replaced.replaced.map((one) => [one.name, one.original])), ...previous };
65247
65533
  mkdirSync26(browserDir(orbitalHome(env)), { recursive: true, mode: 448 });
65248
65534
  writeFileSync23(backup, `${JSON.stringify(saved, null, 2)}
@@ -65254,9 +65540,9 @@ function pointCodexAtSharedBrowser(env, shared = sharedBrowserCommand()) {
65254
65540
  function revertCodexBrowser(env) {
65255
65541
  const file2 = codexConfigPath(env);
65256
65542
  const backup = codexBackupPath(env);
65257
- if (!existsSync35(backup)) return null;
65258
- const saved = JSON.parse(readFileSync30(backup, "utf8"));
65259
- const text3 = existsSync35(file2) ? readFileSync30(file2, "utf8") : "";
65543
+ if (!existsSync36(backup)) return null;
65544
+ const saved = JSON.parse(readFileSync31(backup, "utf8"));
65545
+ const text3 = existsSync36(file2) ? readFileSync31(file2, "utf8") : "";
65260
65546
  const replaced = replaceMcpServers(text3, (table2) => isSharedBrowserConnect(table2.args) ? saved[table2.name] ?? "" : null);
65261
65547
  const next = Object.entries(saved).filter(([name]) => !replaced.replaced.some((one) => one.name === name)).reduce((current2, [, original]) => appendMcpServer(current2, original), replaced.text);
65262
65548
  mkdirSync26(dirname17(file2), { recursive: true });
@@ -65441,12 +65727,12 @@ var browserCommands = [
65441
65727
  ];
65442
65728
 
65443
65729
  // src/core/cli/mission.ts
65444
- import { existsSync as existsSync37 } from "node:fs";
65445
- import { join as join43, relative as relative8, resolve as resolve21 } from "node:path";
65730
+ import { existsSync as existsSync38 } from "node:fs";
65731
+ import { join as join44, relative as relative8, resolve as resolve22 } from "node:path";
65446
65732
 
65447
65733
  // src/core/local/mission-migrate.ts
65448
- import { existsSync as existsSync36, mkdirSync as mkdirSync27, readFileSync as readFileSync31, readdirSync as readdirSync21, renameSync as renameSync8, rmSync as rmSync14, rmdirSync as rmdirSync2, writeFileSync as writeFileSync24 } from "node:fs";
65449
- import { join as join42 } from "node:path";
65734
+ import { existsSync as existsSync37, mkdirSync as mkdirSync27, readFileSync as readFileSync32, readdirSync as readdirSync21, renameSync as renameSync8, rmSync as rmSync14, rmdirSync as rmdirSync2, writeFileSync as writeFileSync24 } from "node:fs";
65735
+ import { join as join43 } from "node:path";
65450
65736
  function withTeam(content, team) {
65451
65737
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n[\s\S]*)?$/);
65452
65738
  if (!match) return content;
@@ -65458,26 +65744,26 @@ ${head.join("\n")}
65458
65744
  ---${match[2] ?? "\n"}`;
65459
65745
  }
65460
65746
  function emptyDir(dir) {
65461
- if (existsSync36(dir) && readdirSync21(dir).length === 0) rmdirSync2(dir);
65747
+ if (existsSync37(dir) && readdirSync21(dir).length === 0) rmdirSync2(dir);
65462
65748
  }
65463
65749
  function migrateMissions(root, nodesDir, nodes, dryRun) {
65464
65750
  const moved = [];
65465
65751
  const refused3 = [];
65466
- const teamsDir = join42(root, TEAMS_DIR);
65467
- const target = join42(root, MISSIONS_DIR);
65752
+ const teamsDir = join43(root, TEAMS_DIR);
65753
+ const target = join43(root, MISSIONS_DIR);
65468
65754
  const seen = /* @__PURE__ */ new Map();
65469
- for (const team of existsSync36(teamsDir) ? readdirSync21(teamsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()) : []) {
65470
- const missionsDir = join42(teamsDir, team.name, MISSIONS_DIR);
65471
- if (!existsSync36(missionsDir)) continue;
65755
+ for (const team of existsSync37(teamsDir) ? readdirSync21(teamsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()) : []) {
65756
+ const missionsDir = join43(teamsDir, team.name, MISSIONS_DIR);
65757
+ if (!existsSync37(missionsDir)) continue;
65472
65758
  for (const mission of readdirSync21(missionsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory())) {
65473
- const from = join42(missionsDir, mission.name);
65474
- const to = join42(target, mission.name);
65759
+ const from = join43(missionsDir, mission.name);
65760
+ const to = join43(target, mission.name);
65475
65761
  const other = seen.get(mission.name);
65476
65762
  if (other) {
65477
65763
  refused3.push(`${from}: the team ${other} has a mission ${mission.name} too. Rename one folder and its mission: key, then run the migration again.`);
65478
65764
  continue;
65479
65765
  }
65480
- if (existsSync36(to)) {
65766
+ if (existsSync37(to)) {
65481
65767
  refused3.push(`${from}: ${to} exists already.`);
65482
65768
  continue;
65483
65769
  }
@@ -65486,22 +65772,22 @@ function migrateMissions(root, nodesDir, nodes, dryRun) {
65486
65772
  if (dryRun) continue;
65487
65773
  mkdirSync27(target, { recursive: true });
65488
65774
  renameSync8(from, to);
65489
- const file2 = join42(to, MISSION_FILE);
65490
- if (existsSync36(file2)) writeFileSync24(file2, withTeam(readFileSync31(file2, "utf8"), team.name));
65775
+ const file2 = join43(to, MISSION_FILE);
65776
+ if (existsSync37(file2)) writeFileSync24(file2, withTeam(readFileSync32(file2, "utf8"), team.name));
65491
65777
  }
65492
65778
  if (!dryRun) emptyDir(missionsDir);
65493
65779
  }
65494
65780
  for (const node2 of nodes) {
65495
- const nodeDir2 = join42(nodesDir, node2);
65496
- if (!existsSync36(nodeDir2)) continue;
65781
+ const nodeDir2 = join43(nodesDir, node2);
65782
+ if (!existsSync37(nodeDir2)) continue;
65497
65783
  const files = [];
65498
65784
  for (const entry of readdirSync21(nodeDir2, { withFileTypes: true })) {
65499
- if (entry.isFile() && entry.name.endsWith(".json")) files.push({ from: join42(nodeDir2, entry.name), mission: entry.name.slice(0, -".json".length), team: null, backup: false });
65500
- if (entry.isFile() && entry.name.endsWith(".json.migrated")) files.push({ from: join42(nodeDir2, entry.name), mission: entry.name.slice(0, -".json.migrated".length), team: null, backup: true });
65785
+ if (entry.isFile() && entry.name.endsWith(".json")) files.push({ from: join43(nodeDir2, entry.name), mission: entry.name.slice(0, -".json".length), team: null, backup: false });
65786
+ if (entry.isFile() && entry.name.endsWith(".json.migrated")) files.push({ from: join43(nodeDir2, entry.name), mission: entry.name.slice(0, -".json.migrated".length), team: null, backup: true });
65501
65787
  if (!entry.isDirectory()) continue;
65502
- for (const name of readdirSync21(join42(nodeDir2, entry.name))) {
65503
- if (name.endsWith(".json")) files.push({ from: join42(nodeDir2, entry.name, name), mission: name.slice(0, -".json".length), team: entry.name, backup: false });
65504
- if (name.endsWith(".json.migrated")) files.push({ from: join42(nodeDir2, entry.name, name), mission: name.slice(0, -".json.migrated".length), team: entry.name, backup: true });
65788
+ for (const name of readdirSync21(join43(nodeDir2, entry.name))) {
65789
+ if (name.endsWith(".json")) files.push({ from: join43(nodeDir2, entry.name, name), mission: name.slice(0, -".json".length), team: entry.name, backup: false });
65790
+ if (name.endsWith(".json.migrated")) files.push({ from: join43(nodeDir2, entry.name, name), mission: name.slice(0, -".json.migrated".length), team: entry.name, backup: true });
65505
65791
  }
65506
65792
  }
65507
65793
  files.sort((a, b) => Number(a.backup) - Number(b.backup) || Number(a.team !== null) - Number(b.team !== null));
@@ -65513,13 +65799,13 @@ function migrateMissions(root, nodesDir, nodes, dryRun) {
65513
65799
  if (!dryRun) rmSync14(file2.from);
65514
65800
  continue;
65515
65801
  }
65516
- if (taken.has(file2.mission) || existsSync36(to)) {
65802
+ if (taken.has(file2.mission) || existsSync37(to)) {
65517
65803
  refused3.push(`${file2.from}: ${to} exists already.`);
65518
65804
  continue;
65519
65805
  }
65520
- const raw = JSON.parse(readFileSync31(file2.from, "utf8"));
65806
+ const raw = JSON.parse(readFileSync32(file2.from, "utf8"));
65521
65807
  const { file: _file2, ...rest } = raw;
65522
- const state = MissionStateFileSchema.safeParse({ question: null, giver: null, lead: null, claim: null, claimedAt: null, ...rest, mission: file2.mission, node: node2, team: rest.team ?? file2.team });
65808
+ const state = MissionStateFileSchema.safeParse({ question: null, lead: null, claim: null, claimedAt: null, ...rest, mission: file2.mission, node: node2, team: rest.team ?? file2.team });
65523
65809
  if (!state.success) {
65524
65810
  refused3.push(`${file2.from}: it is not the state of a mission: ${state.error.issues.map((issue2) => `${issue2.path.join(".")}: ${issue2.message}`).join("; ")}`);
65525
65811
  continue;
@@ -65531,7 +65817,7 @@ function migrateMissions(root, nodesDir, nodes, dryRun) {
65531
65817
  rmSync14(file2.from);
65532
65818
  }
65533
65819
  if (dryRun) continue;
65534
- for (const entry of readdirSync21(nodeDir2, { withFileTypes: true })) if (entry.isDirectory()) emptyDir(join42(nodeDir2, entry.name));
65820
+ for (const entry of readdirSync21(nodeDir2, { withFileTypes: true })) if (entry.isDirectory()) emptyDir(join43(nodeDir2, entry.name));
65535
65821
  emptyDir(nodeDir2);
65536
65822
  }
65537
65823
  emptyDir(nodesDir);
@@ -65553,13 +65839,13 @@ var MISSION_FIELDS = {
65553
65839
  { name: "result", type: "string", nullable: true, summary: "The hand-off of the mission, when it ended." },
65554
65840
  { name: "summary", type: "string", nullable: true, summary: "The first sentence of the Intent of the brief." },
65555
65841
  { name: "question", type: "string", nullable: true, summary: "The question a waiting lead ended its turn with." },
65556
- { name: "questions", type: "array", summary: "The open questions the lead asked its giver with missionAsk, each with an id, the question and the time: the mission waits on its giver until each is answered." },
65842
+ { name: "questions", type: "array", summary: "The open questions the lead asked with missionAsk, each with an id, the question and the time: the mission waits on a decision until each is answered." },
65557
65843
  { name: "links", type: "array", summary: "The links of the mission, each with a label, a URL and a state: the issue of its source, and the links its lead adds, such as its pull request." },
65558
65844
  { name: "history", type: "array", summary: "Each change of state, with its time and who made it." }
65559
65845
  ]
65560
65846
  };
65561
65847
  function workspaceOf3(input2) {
65562
- return resolve21(typeof input2.flags.workspace === "string" ? input2.flags.workspace : ".");
65848
+ return resolve22(typeof input2.flags.workspace === "string" ? input2.flags.workspace : ".");
65563
65849
  }
65564
65850
  function refused2(message, hint) {
65565
65851
  return new CliError({ code: "invalid-input", message, exitCode: ExitCode.Validation, ...hint && { hint } });
@@ -65641,7 +65927,7 @@ function missionNote(context, record2) {
65641
65927
  }
65642
65928
  }
65643
65929
  function shownState(record2) {
65644
- return waitsOnGiver(record2) && record2.state !== "waiting" ? `${record2.state}, waits on you` : record2.state;
65930
+ return waitsOnDecision(record2) && record2.state !== "waiting" ? `${record2.state}, waits on you` : record2.state;
65645
65931
  }
65646
65932
  function missionLines2(context, records) {
65647
65933
  const widths = ["mission", "node", "state"].map((key2) => Math.max(key2.length, ...records.map((record2) => (key2 === "mission" ? record2.missionId : key2 === "node" ? record2.node : shownState(record2)).length)));
@@ -65693,7 +65979,7 @@ var missionNew = {
65693
65979
  if (!id.success) throw refused2(`"${String(input2.positionals.id)}" is not a mission id: use lowercase letters, digits and hyphens.`);
65694
65980
  const file2 = missionFile(teamRoot(team), id.data);
65695
65981
  if (definition.missions.some((one) => one.id === id.data && one.team !== team.name)) throw refused2(`The mission ${id.data} belongs to another team. A mission id names one mission: choose another id.`);
65696
- if (existsSync37(file2) && input2.flags.force !== true) throw refused2(`${relative8(workspaceDir, file2)} exists.`, "Pass --force to write over it.");
65982
+ if (existsSync38(file2) && input2.flags.force !== true) throw refused2(`${relative8(workspaceDir, file2)} exists.`, "Pass --force to write over it.");
65697
65983
  const intent = await asked(context, input2, "intent", "Intent: why does the team do this mission? ");
65698
65984
  const endState = await asked(context, input2, "end-state", "End state: what is true when the mission is done? ");
65699
65985
  const limits = await asked(context, input2, "limits", "Limits: what must the team not do or change? ");
@@ -65772,7 +66058,7 @@ var missionShow = {
65772
66058
  if (record2.sessionId) context.output.line(`session: ${record2.sessionId}`);
65773
66059
  if (record2.brief) context.output.line(["", record2.brief].join("\n"));
65774
66060
  if (record2.lead) context.output.line(`lead: ${record2.lead}`);
65775
- if (record2.giver) context.output.line(`giver: session ${record2.giver} (its questions go to that session)`);
66061
+ if (record2.receivers.length > 0) context.output.line(`decision receivers: ${record2.receivers.join(", ")} (each question goes to every live one)`);
65776
66062
  if (record2.claim) context.output.line(`claim: ${record2.claim}${record2.claimedAt ? ` since ${record2.claimedAt}` : ""}${record2.stale ? " (stale)" : ""}`);
65777
66063
  if (!record2.question && record2.state === "waiting") context.output.line(["", "The lead stopped without a hand-off.", `Tell it how to go on with: ${commandPrefix(context.env)} mission reply ${record2.missionId} "<message>"`].join("\n"));
65778
66064
  if (record2.question && record2.state === "waiting") context.output.line(["", "question:", record2.question, "", `Answer with: ${commandPrefix(context.env)} mission reply ${record2.missionId} "<answer>"`].join("\n"));
@@ -65847,6 +66133,31 @@ var missionSay = {
65847
66133
  return { data: { ...said2, node: node2.name } };
65848
66134
  }
65849
66135
  };
66136
+ var CLAUDE_SESSION_ENV = "CLAUDE_CODE_SESSION_ID";
66137
+ function missionMembership(verb, summary, why) {
66138
+ return {
66139
+ path: ["mission", verb],
66140
+ summary,
66141
+ credential: "none",
66142
+ flags: [WORKSPACE2, { name: "external-id", type: "string", summary: `The external id of the attached session, such as the Claude Code session id. Default: ${CLAUDE_SESSION_ENV}, so a Claude Code session names itself.` }],
66143
+ positionals: [MISSION_ID2],
66144
+ outputShape: MISSION_FIELDS,
66145
+ exitCodes: [0, 3],
66146
+ routes: [],
66147
+ examples: [{ command: `orbital mission ${verb} pr-42`, why }],
66148
+ run: async (input2, context) => {
66149
+ const mission = idOf(input2);
66150
+ const externalId = typeof input2.flags["external-id"] === "string" ? input2.flags["external-id"] : context.env[CLAUDE_SESSION_ENV] || null;
66151
+ if (!externalId) throw refused2(`Name the session that ${verb}s the decision receivers of ${mission}.`, `Pass --external-id <id>, or run the command in a Claude Code session, which sets ${CLAUDE_SESSION_ENV}.`);
66152
+ const node2 = await nodeOfMission(context, mission, () => nodeOfProject(context, workspaceOf3(input2)));
66153
+ const record2 = MissionRecordSchema.parse(await nodeCall(context, node2, `/orbital/missions/${verb}`, { missionId: mission, externalId }));
66154
+ context.output.line(`${record2.missionId} on ${record2.node}: ${record2.state}; decision receivers: ${record2.receivers.join(", ") || "none"}`);
66155
+ return { data: record2 };
66156
+ }
66157
+ };
66158
+ }
66159
+ var missionJoin = missionMembership("join", "Make an attached session, such as this Claude Code session, a decision receiver of a running mission: each question of its lead goes to every live receiver, and the open questions come at once. The node keys the receiver by its external id, so the session stays a receiver after it or the node restarts.", "Get the questions of the lead of pr-42 in this Claude Code session.");
66160
+ var missionLeave = missionMembership("leave", "Remove an attached session from the decision receivers of a mission: its lead's questions stop coming to it.", "Stop getting the questions of the lead of pr-42 in this Claude Code session.");
65850
66161
  var missionMigrate = {
65851
66162
  path: ["mission", "migrate"],
65852
66163
  summary: `Move the missions of the old layout, teams/<team>/missions/<id>/, to missions/<id>/${MISSION_FILE} with team: <team> in the frontmatter, and the state of each mission of the nodes of this project from the node home, missions/<node>/<id>.json, to missions/<id>/${MISSION_STATE_FILE} beside its ${MISSION_FILE}. Stop the nodes of the project first.`,
@@ -65861,11 +66172,11 @@ var missionMigrate = {
65861
66172
  const workspaceDir = workspaceOf3(input2);
65862
66173
  const source = definitionSource(workspaceDir, context.env);
65863
66174
  if (!source) throw refused2(`${workspaceDir} has no definition of orbital.`);
65864
- const running = (await liveNodes(orbitalHome(context.env), context.fetch)).filter((node2) => resolve21(node2.workspaceDir) === resolve21(source.projectDir));
66175
+ const running = (await liveNodes(orbitalHome(context.env), context.fetch)).filter((node2) => resolve22(node2.workspaceDir) === resolve22(source.projectDir));
65865
66176
  if (running.length > 0 && input2.flags["dry-run"] !== true) throw refused2(`The node ${running.map((node2) => node2.name).join(", ")} of this project runs.`, `Stop it first, with ${commandPrefix(context.env)} down, then run the migration.`);
65866
66177
  const project = sourceConfig(source).node ?? defaultNodeName(workspaceDir);
65867
66178
  const nodes = teamNodes(readTeams(source.root).teams, project);
65868
- const migration = migrateMissions(source.root, join43(orbitalHome(context.env), "missions"), nodes, input2.flags["dry-run"] === true);
66179
+ const migration = migrateMissions(source.root, join44(orbitalHome(context.env), "missions"), nodes, input2.flags["dry-run"] === true);
65869
66180
  for (const move of migration.moved) context.output.line(`${input2.flags["dry-run"] === true ? "would move" : "moved"} ${move.from} -> ${move.to}`);
65870
66181
  for (const reason of migration.refused) context.output.warn(reason);
65871
66182
  if (migration.moved.length === 0 && migration.refused.length === 0) context.output.line("Nothing to move: the missions are in the new layout.");
@@ -65890,10 +66201,10 @@ var missionReply = {
65890
66201
  return { data: record2 };
65891
66202
  }
65892
66203
  };
65893
- var missionCommands = [missionNew, missionStart, missionList, missionShow, missionPause, missionResume, missionSay, missionReply, missionDone, missionMigrate].map((spec) => ({ ...spec, group: CommandGroup.OperatingModel }));
66204
+ var missionCommands = [missionNew, missionStart, missionList, missionShow, missionPause, missionResume, missionSay, missionReply, missionJoin, missionLeave, missionDone, missionMigrate].map((spec) => ({ ...spec, group: CommandGroup.OperatingModel }));
65894
66205
 
65895
66206
  // src/core/cli/check.ts
65896
- import { resolve as resolve22 } from "node:path";
66207
+ import { resolve as resolve23 } from "node:path";
65897
66208
  var LOAD_RULE = "orbital.load";
65898
66209
  async function loadRefusals(workspaceDir, env) {
65899
66210
  try {
@@ -65931,7 +66242,7 @@ var checkCommand = {
65931
66242
  { command: "orbital check --annotations", why: "Show the findings on a pull request." }
65932
66243
  ],
65933
66244
  run: async (input2, context) => {
65934
- const workspaceDir = resolve22(typeof input2.flags.workspace === "string" ? input2.flags.workspace : ".");
66245
+ const workspaceDir = resolve23(typeof input2.flags.workspace === "string" ? input2.flags.workspace : ".");
65935
66246
  const source = definitionSource(workspaceDir, context.env);
65936
66247
  const findings = await loadRefusals(workspaceDir, context.env);
65937
66248
  for (const finding of findings) context.output.line(`${finding.finding} ${finding.rule} ${finding.path} ${finding.message}`);
@@ -66241,18 +66552,18 @@ function createOrbitalCommands(options) {
66241
66552
  }
66242
66553
 
66243
66554
  // src/core/cli/shipped-skills.ts
66244
- var import_yaml8 = __toESM(require_dist(), 1);
66245
- import { cpSync as cpSync4, existsSync as existsSync38, mkdirSync as mkdirSync28, readFileSync as readFileSync32, readdirSync as readdirSync22, rmSync as rmSync15, writeFileSync as writeFileSync25 } from "node:fs";
66555
+ var import_yaml9 = __toESM(require_dist(), 1);
66556
+ import { cpSync as cpSync4, existsSync as existsSync39, mkdirSync as mkdirSync28, readFileSync as readFileSync33, readdirSync as readdirSync22, rmSync as rmSync15, writeFileSync as writeFileSync25 } from "node:fs";
66246
66557
  import { createHash as createHash11 } from "node:crypto";
66247
66558
  import { homedir as homedir11 } from "node:os";
66248
- import { dirname as dirname18, join as join44, relative as relative9, resolve as resolve23, sep as sep7 } from "node:path";
66559
+ import { dirname as dirname18, join as join45, relative as relative9, resolve as resolve24, sep as sep7 } from "node:path";
66249
66560
  import { fileURLToPath as fileURLToPath3 } from "node:url";
66250
66561
  var HASH_FILE = ".installed-hash";
66251
66562
  function skillsRoot(shipped) {
66252
66563
  let directory = dirname18(fileURLToPath3(shipped.anchor));
66253
66564
  for (let depth = 0; depth < 5; depth += 1) {
66254
- const candidate = join44(directory, "skills");
66255
- if (existsSync38(join44(candidate, shipped.names[0], "SKILL.md"))) return candidate;
66565
+ const candidate = join45(directory, "skills");
66566
+ if (existsSync39(join45(candidate, shipped.names[0], "SKILL.md"))) return candidate;
66256
66567
  directory = dirname18(directory);
66257
66568
  }
66258
66569
  throw new CliError({
@@ -66264,33 +66575,33 @@ function skillsRoot(shipped) {
66264
66575
  }
66265
66576
  function filesOf2(dir) {
66266
66577
  return readdirSync22(dir, { withFileTypes: true }).flatMap((entry) => {
66267
- const path3 = join44(dir, entry.name);
66578
+ const path3 = join45(dir, entry.name);
66268
66579
  if (entry.isDirectory()) return filesOf2(path3);
66269
66580
  return entry.name === HASH_FILE ? [] : [path3];
66270
66581
  });
66271
66582
  }
66272
66583
  function descriptionOf(skillFile) {
66273
- const match = /^---\n([\s\S]*?)\n---/.exec(readFileSync32(skillFile, "utf8"));
66584
+ const match = /^---\n([\s\S]*?)\n---/.exec(readFileSync33(skillFile, "utf8"));
66274
66585
  if (!match?.[1]) return "";
66275
- const frontmatter = (0, import_yaml8.parse)(match[1]);
66586
+ const frontmatter = (0, import_yaml9.parse)(match[1]);
66276
66587
  return typeof frontmatter?.description === "string" ? frontmatter.description : "";
66277
66588
  }
66278
66589
  function shippedSkills(shipped) {
66279
66590
  const root = skillsRoot(shipped);
66280
66591
  return shipped.names.map((name) => {
66281
- const dir = join44(root, name);
66282
- if (!existsSync38(join44(dir, "SKILL.md"))) {
66592
+ const dir = join45(root, name);
66593
+ if (!existsSync39(join45(dir, "SKILL.md"))) {
66283
66594
  throw new CliError({ code: "internal", message: `This build is missing its skill ${name}.`, exitCode: ExitCode.Error, hint: "Reinstall the CLI. The skills ship inside the package." });
66284
66595
  }
66285
66596
  const files = filesOf2(dir).map((file2) => relative9(dir, file2).split(sep7).join("/")).sort();
66286
- return { name, description: descriptionOf(join44(dir, "SKILL.md")), dir, files };
66597
+ return { name, description: descriptionOf(join45(dir, "SKILL.md")), dir, files };
66287
66598
  });
66288
66599
  }
66289
66600
  function folderHash(dir) {
66290
66601
  const hash2 = createHash11("sha256");
66291
66602
  for (const file2 of filesOf2(dir).map((path3) => relative9(dir, path3).split(sep7).join("/")).sort()) {
66292
66603
  hash2.update(`${file2}\0`);
66293
- hash2.update(readFileSync32(join44(dir, file2)));
66604
+ hash2.update(readFileSync33(join45(dir, file2)));
66294
66605
  hash2.update("\0");
66295
66606
  }
66296
66607
  return hash2.digest("hex");
@@ -66310,18 +66621,18 @@ function pick2(all, names) {
66310
66621
  });
66311
66622
  }
66312
66623
  function installBase(input2) {
66313
- if (typeof input2.flags.dir === "string") return resolve23(input2.flags.dir);
66314
- if (input2.flags.global === true) return join44(homedir11(), ".claude", "skills");
66315
- return resolve23(".claude", "skills");
66624
+ if (typeof input2.flags.dir === "string") return resolve24(input2.flags.dir);
66625
+ if (input2.flags.global === true) return join45(homedir11(), ".claude", "skills");
66626
+ return resolve24(".claude", "skills");
66316
66627
  }
66317
66628
  function planOne(skill, base2) {
66318
- const target = join44(base2, skill.name);
66629
+ const target = join45(base2, skill.name);
66319
66630
  const hash2 = folderHash(skill.dir);
66320
- if (!existsSync38(join44(target, "SKILL.md"))) return { skill, target, hash: hash2, action: "installed", edited: false };
66631
+ if (!existsSync39(join45(target, "SKILL.md"))) return { skill, target, hash: hash2, action: "installed", edited: false };
66321
66632
  const onDisk = folderHash(target);
66322
66633
  if (onDisk === hash2) return { skill, target, hash: hash2, action: "unchanged", edited: false };
66323
- const hashFile = join44(target, HASH_FILE);
66324
- const recorded = existsSync38(hashFile) ? readFileSync32(hashFile, "utf8").trim() : "";
66634
+ const hashFile = join45(target, HASH_FILE);
66635
+ const recorded = existsSync39(hashFile) ? readFileSync33(hashFile, "utf8").trim() : "";
66325
66636
  return { skill, target, hash: hash2, action: "updated", edited: onDisk !== recorded };
66326
66637
  }
66327
66638
  function writeOne(plan) {
@@ -66329,7 +66640,7 @@ function writeOne(plan) {
66329
66640
  rmSync15(plan.target, { recursive: true, force: true });
66330
66641
  mkdirSync28(plan.target, { recursive: true });
66331
66642
  cpSync4(plan.skill.dir, plan.target, { recursive: true });
66332
- writeFileSync25(join44(plan.target, HASH_FILE), `${plan.hash}
66643
+ writeFileSync25(join45(plan.target, HASH_FILE), `${plan.hash}
66333
66644
  `);
66334
66645
  } catch (error62) {
66335
66646
  throw new CliError({
@@ -66374,7 +66685,7 @@ function createSkillCommands(shipped) {
66374
66685
  context.output.line(skill.dir);
66375
66686
  return { data: { name: skill.name, path: skill.dir, content: null } };
66376
66687
  }
66377
- const content = readFileSync32(join44(skill.dir, "SKILL.md"), "utf8");
66688
+ const content = readFileSync33(join45(skill.dir, "SKILL.md"), "utf8");
66378
66689
  context.output.line(content);
66379
66690
  return { data: { name: skill.name, path: skill.dir, content } };
66380
66691
  }
@@ -66454,21 +66765,21 @@ function createSkillCommands(shipped) {
66454
66765
  }
66455
66766
 
66456
66767
  // src/init.ts
66457
- import { existsSync as existsSync39, mkdirSync as mkdirSync29, writeFileSync as writeFileSync26 } from "node:fs";
66458
- import { dirname as dirname19, isAbsolute as isAbsolute8, relative as relative10, resolve as resolve24 } from "node:path";
66459
- var import_yaml9 = __toESM(require_dist(), 1);
66768
+ import { existsSync as existsSync40, mkdirSync as mkdirSync29, writeFileSync as writeFileSync26 } from "node:fs";
66769
+ import { dirname as dirname19, isAbsolute as isAbsolute8, relative as relative10, resolve as resolve25 } from "node:path";
66770
+ var import_yaml10 = __toESM(require_dist(), 1);
66460
66771
  function initHarnessFolder(workspaceDir, adapter = CLAUDE_ADAPTER) {
66461
66772
  const file2 = harnessRootAt(workspaceDir, adapter).file;
66462
- if (existsSync39(file2)) {
66773
+ if (existsSync40(file2)) {
66463
66774
  throw new CliError({ code: "conflict", message: `This project has ${file2} already.`, exitCode: ExitCode.Validation, hint: "A harness folder holds one orbital.yaml." });
66464
66775
  }
66465
66776
  mkdirSync29(dirname19(file2), { recursive: true });
66466
- writeFileSync26(file2, (0, import_yaml9.stringify)({ apiVersion: ORBITAL_API_VERSION }));
66777
+ writeFileSync26(file2, (0, import_yaml10.stringify)({ apiVersion: ORBITAL_API_VERSION }));
66467
66778
  return file2;
66468
66779
  }
66469
66780
  function workspaceOf4(flag) {
66470
- const workspaceDir = resolve24(typeof flag === "string" ? flag : ".");
66471
- if (!existsSync39(workspaceDir)) throw new CliError({ code: "not-found", message: `${workspaceDir} does not exist.`, exitCode: ExitCode.Validation });
66781
+ const workspaceDir = resolve25(typeof flag === "string" ? flag : ".");
66782
+ if (!existsSync40(workspaceDir)) throw new CliError({ code: "not-found", message: `${workspaceDir} does not exist.`, exitCode: ExitCode.Validation });
66472
66783
  return workspaceDir;
66473
66784
  }
66474
66785
  var WORKSPACE3 = { name: "workspace", type: "path", summary: "The project. Default: the current directory." };
@@ -66544,7 +66855,7 @@ function targetOf2(context, workspaceDir, named2, given, adapters) {
66544
66855
  });
66545
66856
  }
66546
66857
  async function chooseTargets(context, workspaceDir, adapters) {
66547
- const present = adapters.filter((adapter) => existsSync39(resolve24(workspaceDir, adapter.folder.folder)));
66858
+ const present = adapters.filter((adapter) => existsSync40(resolve25(workspaceDir, adapter.folder.folder)));
66548
66859
  if (present.length === 1) {
66549
66860
  context.output.step(`installing ${present[0].id}: ${present[0].folder.folder} is the harness folder of this project`);
66550
66861
  return [{ adapter: present[0], given: null }];
@@ -66578,9 +66889,9 @@ async function adapterInstall(context, adapter, workspaceDir, force) {
66578
66889
  async function installTarget(context, workspaceDir, target, user, force) {
66579
66890
  const { adapter, given } = target;
66580
66891
  const root = harnessRootAt(workspaceDir, adapter);
66581
- const file2 = existsSync39(root.file) ? null : initHarnessFolder(workspaceDir, adapter);
66892
+ const file2 = existsSync40(root.file) ? null : initHarnessFolder(workspaceDir, adapter);
66582
66893
  if (file2) context.output.line(`wrote ${file2}`);
66583
- const plugin = given ? { name: given.ref.name, source: user ? isPackageSource(given.ref.source) ? given.ref.source : resolve24(given.ref.source) : given.recorded(root.root), user } : null;
66894
+ const plugin = given ? { name: given.ref.name, source: user ? isPackageSource(given.ref.source) ? given.ref.source : resolve25(given.ref.source) : given.recorded(root.root), user } : null;
66584
66895
  const named2 = plugin ? user ? setUserPlugin(context.env, plugin.name, { source: plugin.source, enabled: true, config: {}, env: [] }) : setPluginEntry(root.file, plugin.name, { source: plugin.source, enabled: true, config: {}, env: [] }) : false;
66585
66896
  if (plugin && named2) context.output.line(user ? `your user config names the plugin ${plugin.name}: source ${plugin.source}` : `${ORBITAL_FILE} names the plugin ${plugin.name}: source ${plugin.source}`);
66586
66897
  const install2 = await adapterInstall(context, adapter, workspaceDir, force);
@@ -66665,6 +66976,12 @@ function followAttach(context, options, signal, inbox, timing = {}) {
66665
66976
  delivery: (event) => {
66666
66977
  const delivery = DeliverySchema.safeParse(event);
66667
66978
  if (delivery.success) inbox.push(delivery.data);
66979
+ const answered = AnsweredSchema.safeParse(event);
66980
+ if (!answered.success) return;
66981
+ for (let index = inbox.length - 1; index >= 0; index -= 1) {
66982
+ const held2 = inbox[index];
66983
+ if (held2.mission === answered.data.mission && (answered.data.question === null || held2.question === answered.data.question)) inbox.splice(index, 1);
66984
+ }
66668
66985
  },
66669
66986
  detached: (reason) => {
66670
66987
  if (kept) kept.ended = true;
@@ -66794,9 +67111,8 @@ function mcpCommand(toolsOf) {
66794
67111
  const joined = await attachment.current();
66795
67112
  return { url: `${joined.nodeUrl}/a2a`, token: joined.token };
66796
67113
  },
66797
- get sessionId() {
66798
- return attachment.sessionId();
66799
- },
67114
+ sessionId: null,
67115
+ attached: async () => (await attachment.current()).sessionId,
66800
67116
  harness,
66801
67117
  follow: true,
66802
67118
  tools: inboxTools(context, attachment.current, inbox)
@@ -66805,8 +67121,8 @@ function mcpCommand(toolsOf) {
66805
67121
  const server = await mcpServer(options, input2.flags.actions === true, toolsOf);
66806
67122
  const transport = new StdioServerTransport();
66807
67123
  await server.connect(transport);
66808
- await new Promise((resolve25) => {
66809
- server.server.onclose = resolve25;
67124
+ await new Promise((resolve26) => {
67125
+ server.server.onclose = resolve26;
66810
67126
  process.stdin.once("end", () => void server.close());
66811
67127
  });
66812
67128
  stop.abort();
@@ -66832,6 +67148,10 @@ function orbitalCommands(options) {
66832
67148
  ...options.commands ?? []
66833
67149
  ];
66834
67150
  }
67151
+ function workspacePluginCommands(options) {
67152
+ const reserved = [.../* @__PURE__ */ new Set([...options.builtIn.map((spec) => spec.path[0]), ...INTROSPECTION_WORDS])];
67153
+ return pluginCommandSpecs({ workspaceDir: options.workspaceDir, env: options.env, reserved, ...options.terminal && { terminal: options.terminal } });
67154
+ }
66835
67155
 
66836
67156
  // src/launch.ts
66837
67157
  import { spawn as spawn9 } from "node:child_process";
@@ -66869,7 +67189,8 @@ export {
66869
67189
  nodeLaunch,
66870
67190
  nodeStartArgv,
66871
67191
  orbitalCommands,
66872
- skillCommands
67192
+ skillCommands,
67193
+ workspacePluginCommands
66873
67194
  };
66874
67195
  /*! Bundled license information:
66875
67196