@sjawhar/opencode-legion-envoy 9.4.0 → 9.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/dispatch.js CHANGED
@@ -947,7 +947,301 @@ __export(exports_coerce, {
947
947
  });
948
948
 
949
949
  // ../envoy-client/bin/dispatch.ts
950
- import { readFileSync as readFileSync4 } from "fs";
950
+ import { readFileSync as readFileSync6 } from "fs";
951
+
952
+ // ../envoy-client/src/dispatch-cli.ts
953
+ import { closeSync as closeSync4, constants as constants2, fstatSync as fstatSync2, openSync as openSync4, readFileSync as readFileSync5 } from "fs";
954
+
955
+ // ../envoy-client/src/askgate-marker.ts
956
+ import { createHash, randomBytes } from "crypto";
957
+ import {
958
+ closeSync,
959
+ constants,
960
+ mkdirSync,
961
+ openSync,
962
+ readdirSync,
963
+ readFileSync,
964
+ renameSync,
965
+ rmSync,
966
+ statSync,
967
+ unlinkSync,
968
+ writeFileSync
969
+ } from "fs";
970
+ import { isAbsolute, join, resolve } from "path";
971
+
972
+ // ../envoy-client/src/errors.ts
973
+ function messageFor(error) {
974
+ return error instanceof Error ? error.message : String(error);
975
+ }
976
+ function hasErrnoCode(error, code) {
977
+ return error instanceof Error && "code" in error && error.code === code;
978
+ }
979
+
980
+ // ../envoy-client/src/askgate-marker.ts
981
+ var ASKGATE_RECORD = "askgate";
982
+ var MARKER_MAX_AGE_MS = 2 * 60 * 60000;
983
+ var MARKERS = "markers";
984
+ var ALWAYS_GATED = {
985
+ dispatch_ask: true,
986
+ dispatch_message: true,
987
+ dispatch_edit_ask: true,
988
+ dispatch_comment: true,
989
+ dispatch_doc_edit: true,
990
+ dispatch_request_approval: true
991
+ };
992
+ var MARKDOWN_NAME = /\.md$/i;
993
+ var MARKDOWN_PATH = /\.(md|markdown)$/i;
994
+ function uploadIsDocument(args) {
995
+ const { content, name, path } = args;
996
+ return typeof content === "string" || typeof name === "string" && MARKDOWN_NAME.test(name.trim()) || typeof path === "string" && MARKDOWN_PATH.test(path);
997
+ }
998
+ function gatedWrite(tool, args) {
999
+ if (ALWAYS_GATED[tool] === true)
1000
+ return true;
1001
+ const { spec } = args;
1002
+ if (tool === "dispatch_issue")
1003
+ return spec !== undefined;
1004
+ if (tool === "dispatch_artifact")
1005
+ return uploadIsDocument(args);
1006
+ return false;
1007
+ }
1008
+ var digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
1009
+ function argvDigest(argv) {
1010
+ return digest(argv);
1011
+ }
1012
+ function argsDigest(args) {
1013
+ return digest(args);
1014
+ }
1015
+ function fileOperands(argv) {
1016
+ const [command, ...flags] = argv;
1017
+ const files = [];
1018
+ for (let index = 0;index < flags.length; index++) {
1019
+ const word = flags[index];
1020
+ const eq = word.indexOf("=");
1021
+ const flag = eq === -1 ? word : word.slice(0, eq);
1022
+ const named = flag.startsWith("--") && flag.endsWith("-file");
1023
+ if (!named && !(command === "artifact" && flag === "--path"))
1024
+ continue;
1025
+ const operand = eq === -1 ? flags[index + 1] : word.slice(eq + 1);
1026
+ if (operand !== undefined && operand !== "-")
1027
+ files.push({ flag, operand });
1028
+ }
1029
+ return files;
1030
+ }
1031
+ function operandPath(cwd, file) {
1032
+ if (file.flag === "--path")
1033
+ return resolve(cwd, file.operand);
1034
+ return isAbsolute(file.operand) ? file.operand : `${cwd}/${file.operand}`;
1035
+ }
1036
+ function bytesDigest(bytes) {
1037
+ return createHash("sha256").update(bytes).digest("hex");
1038
+ }
1039
+ function processStart(pid) {
1040
+ try {
1041
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
1042
+ return stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19] ?? null;
1043
+ } catch {
1044
+ return null;
1045
+ }
1046
+ }
1047
+ function ownerLive(owner) {
1048
+ if (owner.start !== null)
1049
+ return processStart(owner.pid) === owner.start;
1050
+ try {
1051
+ process.kill(owner.pid, 0);
1052
+ return true;
1053
+ } catch (error) {
1054
+ return !hasErrnoCode(error, "ESRCH");
1055
+ }
1056
+ }
1057
+ function isOwner(value) {
1058
+ if (typeof value !== "object" || value === null)
1059
+ return false;
1060
+ const { pid, start, store } = value;
1061
+ return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 && (start === null || typeof start === "string") && typeof store === "string" && store.startsWith("/");
1062
+ }
1063
+ function parseOwner(text) {
1064
+ let data;
1065
+ try {
1066
+ data = JSON.parse(text);
1067
+ } catch {
1068
+ return;
1069
+ }
1070
+ if (!isOwner(data) || !("v" in data) || data.v !== 2)
1071
+ return;
1072
+ const { pid, start, store, mode } = data;
1073
+ return {
1074
+ owner: { pid, start, store },
1075
+ mode: mode === "block" ? "block" : "warn"
1076
+ };
1077
+ }
1078
+ var OWNER_FILE = ".json";
1079
+ var STORE_NAME = /^([1-9][0-9]*)(?:-([0-9]+))?$/;
1080
+ function storeTouched(store) {
1081
+ const own = statSync(store).mtimeMs;
1082
+ try {
1083
+ return Math.max(own, statSync(join(store, MARKERS)).mtimeMs);
1084
+ } catch (error) {
1085
+ if (hasErrnoCode(error, "ENOENT"))
1086
+ return own;
1087
+ throw error;
1088
+ }
1089
+ }
1090
+ function sweepStores(root, now) {
1091
+ const parent = join(root, "askgate");
1092
+ let names;
1093
+ try {
1094
+ names = readdirSync(parent);
1095
+ } catch {
1096
+ return;
1097
+ }
1098
+ for (const name of names) {
1099
+ const match = STORE_NAME.exec(name);
1100
+ if (match === null)
1101
+ continue;
1102
+ const store = join(parent, name);
1103
+ try {
1104
+ if (ownerLive({ pid: Number(match[1]), start: match[2] ?? null, store }))
1105
+ continue;
1106
+ if (now - storeTouched(store) <= MARKER_MAX_AGE_MS)
1107
+ continue;
1108
+ rmSync(store, { recursive: true, force: true });
1109
+ } catch {}
1110
+ }
1111
+ }
1112
+ function askgateState(dir) {
1113
+ const record = join(dir, ASKGATE_RECORD);
1114
+ let names;
1115
+ try {
1116
+ names = readdirSync(record);
1117
+ } catch (error) {
1118
+ if (hasErrnoCode(error, "ENOENT"))
1119
+ return { kind: "off" };
1120
+ return { kind: "broken", problem: `${record}, could not be read: ${messageFor(error)}` };
1121
+ }
1122
+ const live = [];
1123
+ for (const name of names) {
1124
+ if (!name.endsWith(OWNER_FILE))
1125
+ continue;
1126
+ const file = join(record, name);
1127
+ let text;
1128
+ try {
1129
+ text = readFileSync(file, "utf-8");
1130
+ } catch (error) {
1131
+ if (hasErrnoCode(error, "ENOENT"))
1132
+ continue;
1133
+ return { kind: "broken", problem: `${file}, could not be read: ${messageFor(error)}` };
1134
+ }
1135
+ const parsed = parseOwner(text);
1136
+ if (parsed === undefined)
1137
+ return { kind: "broken", problem: `${file}, is not an AskGate record` };
1138
+ if (ownerLive(parsed.owner))
1139
+ live.push({ file, store: parsed.owner.store, mode: parsed.mode });
1140
+ }
1141
+ if (live.length === 0)
1142
+ return { kind: "off" };
1143
+ for (const { file, store } of live) {
1144
+ try {
1145
+ if (!statSync(join(store, MARKERS)).isDirectory())
1146
+ throw new Error("not a directory");
1147
+ } catch (error) {
1148
+ return {
1149
+ kind: "broken",
1150
+ problem: `${file}, names the marker store ${store}, which cannot be read: ${messageFor(error)}`
1151
+ };
1152
+ }
1153
+ }
1154
+ return {
1155
+ kind: "on",
1156
+ stores: live.map(({ store }) => store),
1157
+ mode: live.some(({ mode }) => mode === "block") ? "block" : "warn"
1158
+ };
1159
+ }
1160
+ var isFileDigest = (value) => typeof value === "object" && value !== null && typeof value.flag === "string" && typeof value.operand === "string" && typeof value.sha256 === "string";
1161
+ function readMarker(file) {
1162
+ let text;
1163
+ try {
1164
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
1165
+ try {
1166
+ text = readFileSync(fd, "utf-8");
1167
+ } finally {
1168
+ closeSync(fd);
1169
+ }
1170
+ } catch {
1171
+ return;
1172
+ }
1173
+ try {
1174
+ const data = JSON.parse(text);
1175
+ if (typeof data !== "object" || data === null)
1176
+ return;
1177
+ const { v, argv, args, files, createdAt } = data;
1178
+ if (v !== 2 || typeof argv !== "string" || typeof createdAt !== "number")
1179
+ return;
1180
+ if (args !== null && typeof args !== "string")
1181
+ return;
1182
+ if (!Array.isArray(files) || !files.every(isFileDigest))
1183
+ return;
1184
+ return { argv, args, files, createdAt };
1185
+ } catch {
1186
+ return;
1187
+ }
1188
+ }
1189
+ function consumeMarker(stores, argv, args, files, now) {
1190
+ const key = argvDigest(argv);
1191
+ const reviewed = argsDigest(args);
1192
+ let changed;
1193
+ let mismatched = false;
1194
+ let expired = false;
1195
+ const usable = [];
1196
+ for (const store of stores) {
1197
+ const dir = join(store, MARKERS);
1198
+ for (const name of readdirSync(dir)) {
1199
+ if (!name.startsWith(`${key}.`))
1200
+ continue;
1201
+ const file = join(dir, name);
1202
+ const marker = readMarker(file);
1203
+ if (marker === undefined || marker.argv !== key)
1204
+ continue;
1205
+ if (now - marker.createdAt > MARKER_MAX_AGE_MS) {
1206
+ expired = true;
1207
+ rmSync(file, { force: true });
1208
+ continue;
1209
+ }
1210
+ if (marker.args !== null) {
1211
+ const differs = marker.files.find((bound, index) => bound.flag !== files[index]?.flag || bound.operand !== files[index]?.operand || bound.sha256 !== files[index]?.sha256);
1212
+ if (differs !== undefined) {
1213
+ changed ??= { flag: differs.flag, operand: differs.operand };
1214
+ continue;
1215
+ }
1216
+ if (marker.files.length !== files.length || marker.args !== reviewed) {
1217
+ mismatched = true;
1218
+ continue;
1219
+ }
1220
+ }
1221
+ usable.push({ file, bound: marker.args !== null });
1222
+ }
1223
+ }
1224
+ usable.sort((a, b) => Number(b.bound) - Number(a.bound));
1225
+ for (const { file } of usable) {
1226
+ try {
1227
+ unlinkSync(file);
1228
+ return { ok: true };
1229
+ } catch (error) {
1230
+ if (!hasErrnoCode(error, "ENOENT"))
1231
+ throw error;
1232
+ }
1233
+ }
1234
+ if (changed !== undefined)
1235
+ return {
1236
+ ok: false,
1237
+ why: `a file this write posts changed after AskGate reviewed it (\`${changed.flag} ${changed.operand}\`)`,
1238
+ changed
1239
+ };
1240
+ return {
1241
+ ok: false,
1242
+ why: mismatched ? "its arguments differ from the ones AskGate reviewed" : expired ? "its AskGate marker expired before it ran" : "this call carries no AskGate marker"
1243
+ };
1244
+ }
951
1245
 
952
1246
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/core/core.js
953
1247
  var NEVER = Object.freeze({
@@ -14619,14 +14913,6 @@ function zodSchemaApi(zod) {
14619
14913
  }
14620
14914
  };
14621
14915
  }
14622
- // ../envoy-client/src/errors.ts
14623
- function messageFor(error) {
14624
- return error instanceof Error ? error.message : String(error);
14625
- }
14626
- function hasErrnoCode(error, code) {
14627
- return error instanceof Error && "code" in error && error.code === code;
14628
- }
14629
-
14630
14916
  // ../envoy-client/src/dispatch-command.ts
14631
14917
  var STDIN = "-";
14632
14918
  var BARE_WORD = /^[A-Za-z0-9_./:@%+=,-]+$/;
@@ -18744,7 +19030,9 @@ ${trailer.join(`
18744
19030
  const content = optionalString(args, "content");
18745
19031
  const artifactInput = content === undefined ? {
18746
19032
  name,
18747
- file: Bun.file(resolvePath2(input.cwd, stringArg(args, "path"))),
19033
+ file: input.uploadBytes === undefined ? Bun.file(resolvePath2(input.cwd, stringArg(args, "path"))) : new Blob([new Uint8Array(input.uploadBytes)], {
19034
+ type: Bun.file(resolvePath2(input.cwd, stringArg(args, "path"))).type
19035
+ }),
18748
19036
  ...summary === undefined ? {} : { summary },
18749
19037
  actor
18750
19038
  } : {
@@ -18979,23 +19267,23 @@ function asObject(value) {
18979
19267
  }
18980
19268
 
18981
19269
  // ../envoy-client/src/dispatch-session-state.ts
18982
- import { createHash } from "crypto";
19270
+ import { createHash as createHash2 } from "crypto";
18983
19271
  import {
18984
19272
  appendFileSync,
18985
- closeSync,
19273
+ closeSync as closeSync2,
18986
19274
  fstatSync,
18987
- mkdirSync,
18988
- openSync,
18989
- readdirSync,
18990
- readFileSync,
19275
+ mkdirSync as mkdirSync2,
19276
+ openSync as openSync2,
19277
+ readdirSync as readdirSync2,
19278
+ readFileSync as readFileSync2,
18991
19279
  readSync,
18992
- renameSync,
18993
- rmSync,
18994
- statSync,
18995
- writeFileSync
19280
+ renameSync as renameSync2,
19281
+ rmSync as rmSync2,
19282
+ statSync as statSync2,
19283
+ writeFileSync as writeFileSync2
18996
19284
  } from "fs";
18997
19285
  import { homedir } from "os";
18998
- import { join } from "path";
19286
+ import { join as join2 } from "path";
18999
19287
  var SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
19000
19288
  var PRIVATE_DIR = 448;
19001
19289
  var PRIVATE_FILE = 384;
@@ -19012,20 +19300,20 @@ function sessionStateRoot(env) {
19012
19300
  const stateDir = env["DISPATCH_STATE_DIR"];
19013
19301
  if (stateDir)
19014
19302
  return stateDir;
19015
- return join(env["XDG_STATE_HOME"] || join(homedir(), ".local", "state"), "dispatch");
19303
+ return join2(env["XDG_STATE_HOME"] || join2(homedir(), ".local", "state"), "dispatch");
19016
19304
  }
19017
19305
  function sessionDirectory(env, sessionId) {
19018
19306
  if (!SESSION_ID.test(sessionId) || sessionId === "." || sessionId === "..") {
19019
19307
  throw new Error(`dispatch: the session id ${JSON.stringify(sessionId)} is not 1-128 letters, digits, ".", "_" or "-"`);
19020
19308
  }
19021
- return join(sessionStateRoot(env), "sessions", sessionId);
19309
+ return join2(sessionStateRoot(env), "sessions", sessionId);
19022
19310
  }
19023
19311
  function ensureDirectory(dir) {
19024
- mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR });
19312
+ mkdirSync2(dir, { recursive: true, mode: PRIVATE_DIR });
19025
19313
  }
19026
19314
  function readIfPresent(path) {
19027
19315
  try {
19028
- return readFileSync(path, "utf-8");
19316
+ return readFileSync2(path, "utf-8");
19029
19317
  } catch (error) {
19030
19318
  if (hasErrnoCode(error, "ENOENT"))
19031
19319
  return;
@@ -19034,14 +19322,14 @@ function readIfPresent(path) {
19034
19322
  }
19035
19323
  function writeAtomically(path, text) {
19036
19324
  const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
19037
- writeFileSync(temporary, text, { mode: PRIVATE_FILE });
19038
- renameSync(temporary, path);
19325
+ writeFileSync2(temporary, text, { mode: PRIVATE_FILE });
19326
+ renameSync2(temporary, path);
19039
19327
  }
19040
19328
  function stringList(value) {
19041
19329
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
19042
19330
  }
19043
19331
  function loadSessionMemory(dir, sessionId) {
19044
- const path = join(dir, "state.json");
19332
+ const path = join2(dir, "state.json");
19045
19333
  const text = readIfPresent(path);
19046
19334
  if (text === undefined)
19047
19335
  return new Set;
@@ -19065,7 +19353,7 @@ function writeMemory(dir, sessionId, follows) {
19065
19353
  pictures: [...shownPictures(sessionId)],
19066
19354
  follows: [...follows]
19067
19355
  };
19068
- writeAtomically(join(dir, "state.json"), `${JSON.stringify(state)}
19356
+ writeAtomically(join2(dir, "state.json"), `${JSON.stringify(state)}
19069
19357
  `);
19070
19358
  }
19071
19359
  function appendResult(dir, entry) {
@@ -19074,7 +19362,7 @@ function appendResult(dir, entry) {
19074
19362
  }
19075
19363
  function writeResult(dir, entry) {
19076
19364
  const line = { at: new Date().toISOString(), ...entry };
19077
- appendFileSync(join(dir, "results.jsonl"), `${JSON.stringify(line)}
19365
+ appendFileSync(join2(dir, "results.jsonl"), `${JSON.stringify(line)}
19078
19366
  `, { mode: PRIVATE_FILE });
19079
19367
  }
19080
19368
  function recordOutcome(dir, sessionId, follows, entry) {
@@ -19084,12 +19372,12 @@ function recordOutcome(dir, sessionId, follows, entry) {
19084
19372
  }
19085
19373
  function writePicture(dir, image) {
19086
19374
  const bytes = Buffer.from(image.data, "base64");
19087
- const name = createHash("sha256").update(bytes).digest("hex").slice(0, 16);
19088
- const pictures = join(dir, "pictures");
19089
- const path = join(pictures, `${name}.${PICTURE_EXTENSIONS[image.mimeType]}`);
19375
+ const name = createHash2("sha256").update(bytes).digest("hex").slice(0, 16);
19376
+ const pictures = join2(dir, "pictures");
19377
+ const path = join2(pictures, `${name}.${PICTURE_EXTENSIONS[image.mimeType]}`);
19090
19378
  ensureDirectory(pictures);
19091
19379
  try {
19092
- writeFileSync(path, bytes, { mode: PRIVATE_FILE, flag: "wx" });
19380
+ writeFileSync2(path, bytes, { mode: PRIVATE_FILE, flag: "wx" });
19093
19381
  } catch (error) {
19094
19382
  if (!hasErrnoCode(error, "EEXIST"))
19095
19383
  throw error;
@@ -19097,28 +19385,28 @@ function writePicture(dir, image) {
19097
19385
  return { path, bytes: bytes.length };
19098
19386
  }
19099
19387
  function writeLongOutput(dir, text) {
19100
- const out = join(dir, "out");
19388
+ const out = join2(dir, "out");
19101
19389
  ensureDirectory(out);
19102
- const path = join(out, `${new Date().toISOString().replaceAll(":", "-")}-${process.pid}.md`);
19103
- writeFileSync(path, text, { mode: PRIVATE_FILE });
19390
+ const path = join2(out, `${new Date().toISOString().replaceAll(":", "-")}-${process.pid}.md`);
19391
+ writeFileSync2(path, text, { mode: PRIVATE_FILE });
19104
19392
  return path;
19105
19393
  }
19106
19394
  function readSessionTitle(dir) {
19107
- return readIfPresent(join(dir, "title")) || undefined;
19395
+ return readIfPresent(join2(dir, "title")) || undefined;
19108
19396
  }
19109
19397
  function pruneSessions(root, now) {
19110
- const stamp = join(root, PRUNED_STAMP);
19398
+ const stamp = join2(root, PRUNED_STAMP);
19111
19399
  try {
19112
- if (now - statSync(stamp).mtimeMs < PRUNE_INTERVAL_MS)
19400
+ if (now - statSync2(stamp).mtimeMs < PRUNE_INTERVAL_MS)
19113
19401
  return;
19114
19402
  } catch (error) {
19115
19403
  if (!hasErrnoCode(error, "ENOENT"))
19116
19404
  throw error;
19117
19405
  }
19118
- const sessions = join(root, "sessions");
19406
+ const sessions = join2(root, "sessions");
19119
19407
  let entries = [];
19120
19408
  try {
19121
- entries = readdirSync(sessions, { withFileTypes: true });
19409
+ entries = readdirSync2(sessions, { withFileTypes: true });
19122
19410
  } catch (error) {
19123
19411
  if (!hasErrnoCode(error, "ENOENT"))
19124
19412
  throw error;
@@ -19126,32 +19414,33 @@ function pruneSessions(root, now) {
19126
19414
  for (const entry of entries) {
19127
19415
  if (!entry.isDirectory())
19128
19416
  continue;
19129
- const dir = join(sessions, entry.name);
19417
+ const dir = join2(sessions, entry.name);
19130
19418
  try {
19131
- if (now - statSync(dir).mtimeMs > IDLE_SESSION_MS) {
19132
- rmSync(dir, { recursive: true, force: true });
19419
+ if (now - statSync2(dir).mtimeMs > IDLE_SESSION_MS && askgateState(dir).kind === "off") {
19420
+ rmSync2(dir, { recursive: true, force: true });
19133
19421
  }
19134
19422
  } catch (error) {
19135
19423
  if (!hasErrnoCode(error, "ENOENT"))
19136
19424
  throw error;
19137
19425
  }
19138
19426
  }
19427
+ sweepStores(root, now);
19139
19428
  ensureDirectory(root);
19140
- writeFileSync(stamp, "", { mode: PRIVATE_FILE });
19429
+ writeFileSync2(stamp, "", { mode: PRIVATE_FILE });
19141
19430
  }
19142
19431
 
19143
19432
  // ../envoy-client/src/envoy-config.ts
19144
19433
  import {
19145
19434
  chmodSync,
19146
- closeSync as closeSync2,
19435
+ closeSync as closeSync3,
19147
19436
  existsSync,
19148
19437
  lstatSync,
19149
19438
  mkdtempSync,
19150
- openSync as openSync2,
19151
- readFileSync as readFileSync3,
19439
+ openSync as openSync3,
19440
+ readFileSync as readFileSync4,
19152
19441
  readlinkSync,
19153
- rmSync as rmSync2,
19154
- statSync as statSync2
19442
+ rmSync as rmSync3,
19443
+ statSync as statSync3
19155
19444
  } from "fs";
19156
19445
  import { homedir as homedir2, tmpdir } from "os";
19157
19446
  import * as path from "path";
@@ -19243,14 +19532,14 @@ function jsonErrorPosition(text) {
19243
19532
  }
19244
19533
 
19245
19534
  // ../envoy-client/src/secret-file.ts
19246
- import { readFileSync as readFileSync2 } from "fs";
19535
+ import { readFileSync as readFileSync3 } from "fs";
19247
19536
  function sendable(value) {
19248
19537
  return !/[\s\p{Cc}]/u.test(value);
19249
19538
  }
19250
19539
  function readSecretFile(variable, filePath) {
19251
19540
  let contents;
19252
19541
  try {
19253
- contents = readFileSync2(filePath, "utf8");
19542
+ contents = readFileSync3(filePath, "utf8");
19254
19543
  } catch (error) {
19255
19544
  throw new Error(`${variable} names ${filePath}, which could not be read: ${messageFor(error)}`);
19256
19545
  }
@@ -19348,6 +19637,7 @@ var CheckoutFileSchema = looseObject({
19348
19637
  $schema: string2().optional(),
19349
19638
  dispatch: DispatchSectionSchema.optional()
19350
19639
  });
19640
+ var AskgateSectionSchema = strictObject({ enabled: boolean2().optional() });
19351
19641
  function offWithNotice(notice) {
19352
19642
  return { enabled: false, notice, error: null };
19353
19643
  }
@@ -19384,7 +19674,7 @@ function danglingLink(filePath) {
19384
19674
  function parseFile(filePath, schema, checkout) {
19385
19675
  let raw;
19386
19676
  try {
19387
- raw = readFileSync3(filePath, "utf-8");
19677
+ raw = readFileSync4(filePath, "utf-8");
19388
19678
  } catch (error) {
19389
19679
  if (hasErrnoCode(error, "ENOENT") || hasErrnoCode(error, "ENOTDIR")) {
19390
19680
  return { kind: "absent", link: danglingLink(filePath) };
@@ -19428,7 +19718,7 @@ function runCommand(command, env, cwd) {
19428
19718
  try {
19429
19719
  chmodSync(directory, 448);
19430
19720
  const output = path.join(directory, "stdout");
19431
- const fd = openSync2(output, "wx", 384);
19721
+ const fd = openSync3(output, "wx", 384);
19432
19722
  let result;
19433
19723
  try {
19434
19724
  result = Bun.spawnSync(["/bin/sh", "-c", command], {
@@ -19443,7 +19733,7 @@ function runCommand(command, env, cwd) {
19443
19733
  } catch (error) {
19444
19734
  return { failure: `could not start: ${messageFor(error)}`, timedOut: false };
19445
19735
  } finally {
19446
- closeSync2(fd);
19736
+ closeSync3(fd);
19447
19737
  }
19448
19738
  try {
19449
19739
  process.kill(-result.pid, "SIGKILL");
@@ -19460,7 +19750,7 @@ function runCommand(command, env, cwd) {
19460
19750
  timedOut: false
19461
19751
  };
19462
19752
  }
19463
- const value = readFileSync3(output, "utf-8").trim();
19753
+ const value = readFileSync4(output, "utf-8").trim();
19464
19754
  if (value.length === 0)
19465
19755
  return { failure: "printed nothing", timedOut: false };
19466
19756
  if (!sendable(value)) {
@@ -19471,7 +19761,7 @@ function runCommand(command, env, cwd) {
19471
19761
  }
19472
19762
  return { value };
19473
19763
  } finally {
19474
- rmSync2(directory, { recursive: true, force: true });
19764
+ rmSync3(directory, { recursive: true, force: true });
19475
19765
  }
19476
19766
  }
19477
19767
  function fileValue(value, field, filePath, env) {
@@ -19495,7 +19785,7 @@ function fileValue(value, field, filePath, env) {
19495
19785
  }
19496
19786
  function statDevice(directory) {
19497
19787
  try {
19498
- return statSync2(directory).dev;
19788
+ return statSync3(directory).dev;
19499
19789
  } catch {
19500
19790
  return;
19501
19791
  }
@@ -19556,7 +19846,8 @@ function readEnvoyConfig(filePath, env, options) {
19556
19846
  kind: "valid",
19557
19847
  path: checkoutPath,
19558
19848
  dispatch: checkoutFile.settings.dispatch ?? {},
19559
- envoyError: Object.hasOwn(checkoutFile.settings, "envoy") ? `${checkoutPath}: envoy is not read from a checkout's envoy.json; put it in ${filePath}` : null
19849
+ envoyError: Object.hasOwn(checkoutFile.settings, "envoy") ? `${checkoutPath}: envoy is not read from a checkout's envoy.json; put it in ${filePath}` : null,
19850
+ askgateError: Object.hasOwn(checkoutFile.settings, "askgate") ? `askgate in ${checkoutPath} is not read; set it in ${filePath}` : null
19560
19851
  };
19561
19852
  }
19562
19853
  const oldCheckout = path.resolve(options.cwd, ".opencode", "envoy.json");
@@ -20098,10 +20389,12 @@ var REFUSED = 1;
20098
20389
  var USAGE = 2;
20099
20390
  var HOSTS = ["omp", "claude", "opencode"];
20100
20391
  var CLAUDE_OUTPUT_MAX = 25000;
20392
+ var SAME_MESSAGE = "AskGate reviews the calls of a message before any of them runs, so it did not review that change.";
20101
20393
  var STATE_WRITE_FAILED = "dispatch: Dispatch took the call, but this session's state";
20102
20394
  function stateWriteFailed(problem) {
20103
20395
  return `${STATE_WRITE_FAILED} could not be written: ${problem}`;
20104
20396
  }
20397
+ var LITERAL_FORM = "AskGate reviews a Dispatch write, and lets it post, only when it is sent as one bash call of literal words: `dispatch <command> <words\u2026>`. Each word is plain text, text in single quotes (also over more than one line), or text in double quotes with no `$`, backtick or `\\`. A `\\` at the end of a line continues the command. Before it, one `cd <path> &&` is permitted; after it, one `2>&1`, one here-document opened with a quoted delimiter (`<<'EOF'`) and one `| head -n N` or `| tail -n N`. For text with `$` or a backtick in it, use `--<field>-file - <<'EOF'`. A variable, `~`, a glob, an assignment, another redirect (`2>/dev/null`, `> file`), another pipe, another command (`;`, `&&`, `||`), a loop, xargs, `bash -c`, a script, an alias, or a process that a program or an eval cell starts carries no marker.";
20105
20398
  var SUBSCRIPTIONS_TIMEOUT_MS = 1500;
20106
20399
  function fitOutput(text, pictures, kept, limit, writeFull) {
20107
20400
  const whole = [text, ...pictures, ...kept].join(`
@@ -20144,14 +20437,15 @@ async function runDispatchCli(argv, rawEnv, io) {
20144
20437
  if (value !== undefined && value !== "")
20145
20438
  env[name] = value;
20146
20439
  }
20147
- const refuse = (text, code, tool) => {
20440
+ const refuse = (text, code, tool, askgate) => {
20148
20441
  print(text);
20149
20442
  const sessionId = env.CLAUDE_CODE_SESSION_ID ?? env.DISPATCH_SESSION_ID;
20150
20443
  if (sessionId !== undefined) {
20151
20444
  try {
20152
20445
  appendResult(sessionDirectory(env, sessionId), {
20153
20446
  tool: tool ?? "dispatch",
20154
- error: text.trimEnd()
20447
+ error: text.trimEnd(),
20448
+ ...askgate === undefined ? {} : { askgate }
20155
20449
  });
20156
20450
  } catch {}
20157
20451
  }
@@ -20175,7 +20469,7 @@ async function runDispatchCli(argv, rawEnv, io) {
20175
20469
  const hostName = env.DISPATCH_HOST ?? (env.CLAUDE_CODE_CHILD_SESSION === "1" ? "claude" : undefined);
20176
20470
  const host = HOSTS.find((name) => name === hostName);
20177
20471
  if (host === undefined) {
20178
- return refuse(hostName === undefined ? "dispatch: DISPATCH_HOST is not set. The host plugin sets it in an agent's shell; to run dispatch by hand, set DISPATCH_HOST and DISPATCH_SESSION_ID yourself." : `dispatch: DISPATCH_HOST is ${JSON.stringify(hostName)}, not omp, claude or opencode.`, USAGE, parsed.tool);
20472
+ return refuse(hostName === undefined ? "dispatch: DISPATCH_HOST is not set. The host plugin sets it in an agent's shell. From an agent's session, send the command as a bash call. To run dispatch by hand, set DISPATCH_HOST, and set DISPATCH_SESSION_ID to the id of your own session." : `dispatch: DISPATCH_HOST is ${JSON.stringify(hostName)}, not omp, claude or opencode.`, USAGE, parsed.tool);
20179
20473
  }
20180
20474
  const sessionVariable = host === "claude" ? "CLAUDE_CODE_SESSION_ID" : "DISPATCH_SESSION_ID";
20181
20475
  const sessionId = env[sessionVariable];
@@ -20214,6 +20508,48 @@ async function runDispatchCli(argv, rawEnv, io) {
20214
20508
  return refuse(resolution.error === null ? `dispatch: ${resolution.notice}` : `dispatch: dispatch config: ${resolution.error}`, USAGE, parsed.tool);
20215
20509
  }
20216
20510
  const config = resolution;
20511
+ let uploadBytes;
20512
+ if (gatedWrite(parsed.tool, parsed.args)) {
20513
+ const notCalled = `dispatch ${commandName(parsed.tool)} was not called:`;
20514
+ try {
20515
+ const gate = askgateState(dir);
20516
+ if (gate.kind === "broken") {
20517
+ return refuse(`${notCalled} AskGate's record for this session, ${gate.problem}, so whether AskGate reviewed this write cannot be told.`, REFUSED, parsed.tool, { mode: "block", why: gate.problem });
20518
+ }
20519
+ if (gate.kind === "on") {
20520
+ const files = [];
20521
+ for (const file of fileOperands(argv)) {
20522
+ let bytes;
20523
+ try {
20524
+ const fd = openSync4(operandPath(io.cwd, file), constants2.O_RDONLY | constants2.O_NONBLOCK);
20525
+ try {
20526
+ if (fstatSync2(fd).isFile())
20527
+ bytes = readFileSync5(fd);
20528
+ } finally {
20529
+ closeSync4(fd);
20530
+ }
20531
+ } catch {}
20532
+ files.push({ ...file, sha256: bytes === undefined ? null : bytesDigest(bytes) });
20533
+ if (file.flag === "--path" && file.operand === parsed.args.path)
20534
+ uploadBytes = bytes;
20535
+ }
20536
+ const spent = consumeMarker(gate.stores, argv, parsed.args, files, Date.now());
20537
+ if (!spent.ok && gate.mode === "block") {
20538
+ return refuse(spent.changed === undefined ? `${notCalled} AskGate is on for this session, and ${spent.why}. ${LITERAL_FORM} Send the write again in that form, as a bash call of its own.` : `${notCalled} AskGate is on for this session, and ${spent.why}. ${SAME_MESSAGE} Send the call again in a later message.`, REFUSED, parsed.tool, { mode: "block", why: spent.why });
20539
+ }
20540
+ if (!spent.ok) {
20541
+ try {
20542
+ appendResult(dir, { tool: parsed.tool, askgate: { mode: "warn", why: spent.why } });
20543
+ } catch (error) {
20544
+ return refuse(`${notCalled} AskGate is on for this session in warn mode, and ${spent.why}. A write that posts unreviewed is first recorded in this session's ledger, and the ledger could not be written: ${messageFor(error)}.`, REFUSED, parsed.tool);
20545
+ }
20546
+ print(spent.changed === undefined ? `dispatch: warning: AskGate is on for this session in warn mode, and ${spent.why}, so this write posts unreviewed. ${LITERAL_FORM} Send the next one in that form.` : `dispatch: warning: AskGate is on for this session in warn mode, and ${spent.why}, so this write posts unreviewed. ${SAME_MESSAGE} Send a write in a later message than the call that changes its file.`);
20547
+ }
20548
+ }
20549
+ } catch (error) {
20550
+ return refuse(`${notCalled} AskGate's marker for this call could not be checked: ${messageFor(error)}.`, REFUSED, parsed.tool, { mode: "block", why: messageFor(error) });
20551
+ }
20552
+ }
20217
20553
  let topics;
20218
20554
  const readTopics = async () => {
20219
20555
  try {
@@ -20255,6 +20591,7 @@ async function runDispatchCli(argv, rawEnv, io) {
20255
20591
  tool,
20256
20592
  args,
20257
20593
  cwd: io.cwd,
20594
+ ...uploadBytes === undefined ? {} : { uploadBytes },
20258
20595
  host,
20259
20596
  sessionId,
20260
20597
  sessionTitle: env.DISPATCH_SESSION_TITLE ?? readSessionTitle(dir),
@@ -20306,7 +20643,7 @@ async function runDispatchCli(argv, rawEnv, io) {
20306
20643
  }
20307
20644
 
20308
20645
  // ../envoy-client/bin/dispatch.ts
20309
- var readText = (path) => readFileSync4(path === "-" ? 0 : path, "utf-8");
20646
+ var readText = (path) => readFileSync6(path === "-" ? 0 : path, "utf-8");
20310
20647
  var pending = [];
20311
20648
  var writeTo = (stream) => (text) => {
20312
20649
  const { promise, resolve } = Promise.withResolvers();
@@ -14668,6 +14668,7 @@ var CheckoutFileSchema = looseObject({
14668
14668
  $schema: string2().optional(),
14669
14669
  dispatch: DispatchSectionSchema.optional()
14670
14670
  });
14671
+ var AskgateSectionSchema = strictObject({ enabled: boolean2().optional() });
14671
14672
  function offWithNotice(notice) {
14672
14673
  return { enabled: false, notice, error: null };
14673
14674
  }
@@ -14876,7 +14877,8 @@ function readEnvoyConfig(filePath, env, options) {
14876
14877
  kind: "valid",
14877
14878
  path: checkoutPath,
14878
14879
  dispatch: checkoutFile.settings.dispatch ?? {},
14879
- envoyError: Object.hasOwn(checkoutFile.settings, "envoy") ? `${checkoutPath}: envoy is not read from a checkout's envoy.json; put it in ${filePath}` : null
14880
+ envoyError: Object.hasOwn(checkoutFile.settings, "envoy") ? `${checkoutPath}: envoy is not read from a checkout's envoy.json; put it in ${filePath}` : null,
14881
+ askgateError: Object.hasOwn(checkoutFile.settings, "askgate") ? `askgate in ${checkoutPath} is not read; set it in ${filePath}` : null
14880
14882
  };
14881
14883
  }
14882
14884
  const oldCheckout = path.resolve(options.cwd, ".opencode", "envoy.json");
@@ -15151,6 +15153,9 @@ function startNotice(config, dispatch, envoy, nats) {
15151
15153
  }
15152
15154
  if (checkout?.kind === "valid" && checkout.envoyError !== null)
15153
15155
  parts.push(checkout.envoyError);
15156
+ if (checkout?.kind === "valid" && checkout.askgateError !== null) {
15157
+ parts.push(checkout.askgateError);
15158
+ }
15154
15159
  if (checkout?.kind === "invalid" && (dispatch.enabled || dispatch.error !== checkout.error)) {
15155
15160
  parts.push(`${checkout.error}; not read while DISPATCH_URL is set`);
15156
15161
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "9.4.0",
3
+ "version": "9.5.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/src/server.js",
@@ -82,7 +82,7 @@ missing proof goes back as step 1 says. The Dispatch message (step 7) is for a f
82
82
  Dispatch's message limit, and ends with your attribution footer:
83
83
 
84
84
  ```bash
85
- dispatch message --issue "$LEGION_ISSUE" --body-file - <<'EOF'
85
+ dispatch message --issue '<KEY>' --body-file - <<'EOF'
86
86
  ## Retro Complete
87
87
 
88
88
  **Learnings documented in:** docs/solutions/<path>.md, or none: <why no point qualified>.
@@ -157,13 +157,13 @@ takes. That issue,
157
157
  it](#when-no-controller-takes-it) says:
158
158
 
159
159
  ```bash
160
- dispatch comment --issue <ASK KEY> --body-file - <<'EOF'
160
+ dispatch comment --issue '<ASK KEY>' --body-file - <<'EOF'
161
161
  <the evidence: commands, output with its time, file:line, links>
162
162
  EOF
163
163
  # Keep `File it for Legion` only when your Legion addressing line lists the repository where the
164
164
  # fix belongs. For a clear defect no controller took, replace the `File an issue` option below with
165
165
  # 'Hand it to the owning tree: <the tree, when you know it>', the first option and recommended.
166
- dispatch ask --issue <ASK KEY> --ref 'dispatch://<ASK KEY>/comment/<id>' --question-file - \
166
+ dispatch ask --issue '<ASK KEY>' --ref 'dispatch://<ASK KEY>/comment/<id>' --question-file - \
167
167
  --option 'File an issue: <what it would track, and who would own it>' \
168
168
  --option 'File it for Legion: Legion works it once it is filed' \
169
169
  --option 'Do not file: <what stays as it is>' <<'EOF'