@sjawhar/pi-legion 9.8.1 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/legion.js +569 -107
  2. package/package.json +1 -1
package/dist/legion.js CHANGED
@@ -19,9 +19,20 @@ var __require = import.meta.require;
19
19
  import { randomUUID } from "crypto";
20
20
  import path5 from "path";
21
21
 
22
- // ../envoy-client/src/dispatch-config.ts
23
- import { readFileSync as readFileSync2 } from "fs";
24
- import { homedir } from "os";
22
+ // ../envoy-client/src/envoy-config.ts
23
+ import {
24
+ chmodSync,
25
+ closeSync,
26
+ existsSync,
27
+ lstatSync,
28
+ mkdtempSync,
29
+ openSync,
30
+ readFileSync as readFileSync2,
31
+ readlinkSync,
32
+ rmSync,
33
+ statSync
34
+ } from "fs";
35
+ import { homedir, tmpdir } from "os";
25
36
  import * as path from "path";
26
37
 
27
38
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
@@ -13564,8 +13575,97 @@ function hasErrnoCode(error48, code) {
13564
13575
  return error48 instanceof Error && "code" in error48 && error48.code === code;
13565
13576
  }
13566
13577
 
13578
+ // ../envoy-client/src/json-position.ts
13579
+ function jsonErrorPosition(text) {
13580
+ let at = 0;
13581
+ const fail = () => {
13582
+ throw new RangeError("not JSON");
13583
+ };
13584
+ const space = () => {
13585
+ while (at < text.length && `
13586
+ \r`.includes(text.charAt(at)))
13587
+ at += 1;
13588
+ };
13589
+ const expect = (char) => {
13590
+ if (text.charAt(at) !== char)
13591
+ fail();
13592
+ at += 1;
13593
+ };
13594
+ const string4 = () => {
13595
+ expect('"');
13596
+ while (at < text.length) {
13597
+ const char = text.charAt(at);
13598
+ if (char === '"') {
13599
+ at += 1;
13600
+ return;
13601
+ }
13602
+ if (char < " ")
13603
+ fail();
13604
+ at += char === "\\" ? 2 : 1;
13605
+ }
13606
+ fail();
13607
+ };
13608
+ const value = () => {
13609
+ space();
13610
+ const char = text.charAt(at);
13611
+ if (char === "{" || char === "[") {
13612
+ const close = char === "{" ? "}" : "]";
13613
+ at += 1;
13614
+ space();
13615
+ if (text.charAt(at) === close) {
13616
+ at += 1;
13617
+ return;
13618
+ }
13619
+ for (;; ) {
13620
+ if (close === "}") {
13621
+ space();
13622
+ string4();
13623
+ space();
13624
+ expect(":");
13625
+ }
13626
+ value();
13627
+ space();
13628
+ if (text.charAt(at) === ",") {
13629
+ at += 1;
13630
+ continue;
13631
+ }
13632
+ expect(close);
13633
+ return;
13634
+ }
13635
+ }
13636
+ if (char === '"') {
13637
+ string4();
13638
+ return;
13639
+ }
13640
+ for (const word of ["true", "false", "null"]) {
13641
+ if (text.startsWith(word, at)) {
13642
+ at += word.length;
13643
+ return;
13644
+ }
13645
+ }
13646
+ const number4 = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/.exec(text.slice(at));
13647
+ if (number4 === null)
13648
+ fail();
13649
+ at += number4?.[0].length ?? 0;
13650
+ };
13651
+ try {
13652
+ value();
13653
+ space();
13654
+ if (at < text.length)
13655
+ fail();
13656
+ return;
13657
+ } catch {
13658
+ const before = text.slice(0, at).split(`
13659
+ `);
13660
+ return `line ${before.length}, column ${(before.at(-1)?.length ?? 0) + 1}`;
13661
+ }
13662
+ }
13663
+
13567
13664
  // ../envoy-client/src/secret-file.ts
13568
13665
  import { readFileSync } from "fs";
13666
+ function sendable(value) {
13667
+ return !/[\s\p{Cc}]/u.test(value);
13668
+ }
13569
13669
  function readSecretFile(variable, filePath) {
13570
13670
  let contents;
13571
13671
  try {
@@ -13576,131 +13676,431 @@ function readSecretFile(variable, filePath) {
13576
13676
  const value = contents.trim();
13577
13677
  if (value.length === 0)
13578
13678
  throw new Error(`${variable} names ${filePath}, which is empty`);
13679
+ if (!sendable(value)) {
13680
+ throw new Error(`${variable} names ${filePath}, which holds whitespace or a control character`);
13681
+ }
13579
13682
  return value;
13580
13683
  }
13581
13684
 
13582
- // ../envoy-client/src/dispatch-config.ts
13583
- var DEFAULT_SERVER_URL = "http://localhost:8766";
13584
- function normalizeDispatchUrl(url2) {
13585
- return url2.replace(/\/+$/, "");
13685
+ // ../envoy-client/src/envoy-config.ts
13686
+ var COMMAND_TIMEOUT_MS = 1e4;
13687
+ function nonEmpty(value) {
13688
+ return value === undefined || value === "" ? undefined : value;
13586
13689
  }
13587
- function parsedDispatchUrl(value, source) {
13588
- const url2 = normalizeDispatchUrl(value);
13589
- try {
13590
- new URL(url2);
13591
- return { url: url2, error: null };
13592
- } catch {
13593
- return { url: null, error: `${source} must be a valid URL` };
13690
+ function absolute(name, value) {
13691
+ if (value !== undefined && !path.isAbsolute(value)) {
13692
+ throw new Error(`${name} is ${value}, which is not an absolute path`);
13693
+ }
13694
+ return value;
13695
+ }
13696
+ function neutralConfig(env) {
13697
+ const xdgConfigHome = nonEmpty(env.XDG_CONFIG_HOME);
13698
+ if (xdgConfigHome !== undefined && !path.isAbsolute(xdgConfigHome)) {
13699
+ return { error: `XDG_CONFIG_HOME is ${xdgConfigHome}, which is not an absolute path` };
13594
13700
  }
13701
+ const configHome = xdgConfigHome ?? path.join(nonEmpty(env.HOME) ?? homedir(), ".config");
13702
+ return { path: path.resolve(configHome, "legion", "envoy.json") };
13595
13703
  }
13704
+ var HARNESS_DIRECTORY_VARIABLE = {
13705
+ omp: "PI_CODING_AGENT_DIR",
13706
+ claude: "CLAUDE_CONFIG_DIR",
13707
+ opencode: "OPENCODE_CONFIG_DIR"
13708
+ };
13709
+ var PLUGIN_HARNESSES = Object.keys(HARNESS_DIRECTORY_VARIABLE);
13710
+ function namedDirectory(harness, env) {
13711
+ if (harness === "omp") {
13712
+ const profile = nonEmpty(env.OMP_PROFILE);
13713
+ if (profile === undefined || profile === "default")
13714
+ return;
13715
+ }
13716
+ return nonEmpty(env[HARNESS_DIRECTORY_VARIABLE[harness]]);
13717
+ }
13718
+ function harnessDirectory(harness, env) {
13719
+ for (const plugin of harness === "command" ? PLUGIN_HARNESSES : [harness]) {
13720
+ if (harness === "command" && plugin === "claude" && env.CLAUDE_CODE_CHILD_SESSION !== "1") {
13721
+ continue;
13722
+ }
13723
+ const directory = namedDirectory(plugin, env);
13724
+ if (directory !== undefined)
13725
+ return absolute(HARNESS_DIRECTORY_VARIABLE[plugin], directory);
13726
+ }
13727
+ return;
13728
+ }
13729
+ function handedConfigPath(env) {
13730
+ const handed = nonEmpty(env.ENVOY_CONFIG_FILE);
13731
+ return handed === undefined ? undefined : path.resolve(absolute("ENVOY_CONFIG_FILE", handed));
13732
+ }
13733
+ function envoyConfigPath(harness, env) {
13734
+ const handedFirst = harness === "command" ? handedConfigPath(env) : undefined;
13735
+ if (handedFirst !== undefined)
13736
+ return handedFirst;
13737
+ const directory = harnessDirectory(harness, env);
13738
+ if (directory !== undefined)
13739
+ return path.resolve(directory, "envoy.json");
13740
+ const handed = handedConfigPath(env);
13741
+ if (handed !== undefined)
13742
+ return handed;
13743
+ const neutral = neutralConfig(env);
13744
+ if ("error" in neutral)
13745
+ throw new Error(neutral.error);
13746
+ return neutral.path;
13747
+ }
13748
+ var httpUrlSchema = exports_external.url({ protocol: /^https?$/, error: "must be an http or https URL" });
13749
+ var DispatchSectionSchema = exports_external.strictObject({
13750
+ enabled: exports_external.boolean().optional(),
13751
+ serverUrl: httpUrlSchema.optional(),
13752
+ token: exports_external.string().optional()
13753
+ });
13754
+ var EnvoySectionSchema = exports_external.strictObject({
13755
+ enabled: exports_external.boolean().optional(),
13756
+ url: httpUrlSchema.optional(),
13757
+ natsUrls: exports_external.array(exports_external.string().trim().min(1, "must not be empty")).optional(),
13758
+ token: exports_external.string().optional(),
13759
+ natsSeed: exports_external.string().optional()
13760
+ });
13596
13761
  var EnvoyFileSchema = exports_external.looseObject({
13597
13762
  $schema: exports_external.string().optional(),
13598
- natsUrls: exports_external.array(exports_external.string()).optional(),
13599
- dispatch: exports_external.strictObject({
13600
- enabled: exports_external.boolean().optional(),
13601
- serverUrl: exports_external.url().optional(),
13602
- token: exports_external.string().optional()
13603
- }).optional()
13763
+ dispatch: DispatchSectionSchema.optional(),
13764
+ envoy: EnvoySectionSchema.optional()
13765
+ });
13766
+ var CheckoutFileSchema = exports_external.looseObject({
13767
+ $schema: exports_external.string().optional(),
13768
+ dispatch: DispatchSectionSchema.optional()
13604
13769
  });
13770
+ function offWithNotice(notice) {
13771
+ return { enabled: false, notice, error: null };
13772
+ }
13773
+ function offWithError(error48) {
13774
+ return { enabled: false, notice: null, error: error48 };
13775
+ }
13776
+ function httpUrl2(value) {
13777
+ let url2;
13778
+ try {
13779
+ url2 = new URL(value);
13780
+ } catch {
13781
+ return;
13782
+ }
13783
+ return url2.protocol === "http:" || url2.protocol === "https:" ? url2 : undefined;
13784
+ }
13605
13785
  function describeSchemaIssue(filePath, error48) {
13606
13786
  const issue2 = error48.issues[0];
13607
13787
  if (!issue2)
13608
- return `${filePath}: invalid dispatch config`;
13788
+ return `${filePath}: invalid envoy.json`;
13609
13789
  if (issue2.code === "unrecognized_keys") {
13610
- const keys = issue2.keys.map((key) => `dispatch.${key}`).join(", ");
13611
- return `${filePath}: unrecognized dispatch key(s): ${keys}`;
13790
+ const section = issue2.path.join(".");
13791
+ const keys = issue2.keys.map((key) => `${section}.${key}`).join(", ");
13792
+ return `${filePath}: unrecognized ${section} key(s): ${keys}`;
13612
13793
  }
13613
13794
  return `${filePath}: ${issue2.path.join(".")}: ${issue2.message}`;
13614
13795
  }
13615
- function readEnvoyFile(filePath) {
13796
+ function danglingLink(filePath) {
13797
+ try {
13798
+ return lstatSync(filePath).isSymbolicLink() ? readlinkSync(filePath) : null;
13799
+ } catch {
13800
+ return null;
13801
+ }
13802
+ }
13803
+ function parseFile(filePath, schema, checkout) {
13616
13804
  let raw;
13617
13805
  try {
13618
13806
  raw = readFileSync2(filePath, "utf-8");
13619
- } catch {
13620
- return { kind: "absent" };
13807
+ } catch (error48) {
13808
+ if (hasErrnoCode(error48, "ENOENT") || hasErrnoCode(error48, "ENOTDIR")) {
13809
+ return { kind: "absent", link: danglingLink(filePath) };
13810
+ }
13811
+ return { kind: "invalid", reason: `${filePath}: could not be read (${messageFor(error48)})` };
13621
13812
  }
13622
13813
  let parsedJson;
13623
13814
  try {
13624
13815
  parsedJson = JSON.parse(raw);
13625
- } catch (err) {
13626
- return { kind: "invalid", reason: `${filePath}: invalid JSON (${messageFor(err)})` };
13816
+ } catch {
13817
+ const position = jsonErrorPosition(raw);
13818
+ return {
13819
+ kind: "invalid",
13820
+ reason: `${filePath}: invalid JSON${position === undefined ? "" : ` at ${position}`}`
13821
+ };
13822
+ }
13823
+ if (typeof parsedJson === "object" && parsedJson !== null && "natsUrls" in parsedJson) {
13824
+ return {
13825
+ kind: "invalid",
13826
+ reason: checkout ? `${filePath}: the top-level natsUrls is not read, and a checkout's envoy.json holds only a dispatch section` : `${filePath}: the top-level natsUrls is not read; move it to envoy.natsUrls`
13827
+ };
13627
13828
  }
13628
- const parsed = EnvoyFileSchema.safeParse(parsedJson);
13829
+ const parsed = schema.safeParse(parsedJson);
13629
13830
  if (!parsed.success) {
13630
13831
  return { kind: "invalid", reason: describeSchemaIssue(filePath, parsed.error) };
13631
13832
  }
13632
- return { kind: "valid", settings: parsed.data.dispatch ?? null };
13633
- }
13634
- function resolveDispatchConfig(env, options = {}) {
13635
- const explicitUrl = env.DISPATCH_URL;
13636
- const home = options.home ?? env.HOME ?? homedir();
13637
- const cwd = options.cwd ?? process.cwd();
13638
- const userFile = readEnvoyFile(path.join(home, ".config", "opencode", "envoy.json"));
13639
- const repoFile = readEnvoyFile(path.join(cwd, ".opencode", "envoy.json"));
13640
- for (const file2 of [userFile, repoFile]) {
13641
- if (file2.kind === "invalid") {
13642
- return { enabled: false, url: null, token: null, error: file2.reason };
13643
- }
13644
- }
13645
- const userSettings = userFile.kind === "valid" ? userFile.settings : null;
13646
- const repoSettings = repoFile.kind === "valid" ? repoFile.settings : null;
13647
- const merged = { ...userSettings, ...repoSettings };
13648
- const repoURL = repoSettings?.serverUrl;
13649
- if (explicitUrl === undefined && merged.enabled === true && repoURL !== undefined) {
13650
- const token2 = repoSettings?.token ?? null;
13651
- if (!token2) {
13833
+ return { kind: "valid", settings: parsed.data };
13834
+ }
13835
+ var FAILURE_KEPT_MS = 60000;
13836
+ var COMMAND_OUTCOMES = Symbol.for("legion.envoy-config.commands.v1");
13837
+ var processGlobal = globalThis;
13838
+ var commandOutcomes = processGlobal[COMMAND_OUTCOMES] ?? new Map;
13839
+ processGlobal[COMMAND_OUTCOMES] = commandOutcomes;
13840
+ function runCommand(command, env, cwd) {
13841
+ const childEnv = {};
13842
+ for (const [name, variable] of Object.entries(env)) {
13843
+ if (variable !== undefined)
13844
+ childEnv[name] = variable;
13845
+ }
13846
+ const directory = mkdtempSync(path.join(tmpdir(), "envoy-command-"));
13847
+ try {
13848
+ chmodSync(directory, 448);
13849
+ const output = path.join(directory, "stdout");
13850
+ const fd = openSync(output, "wx", 384);
13851
+ let result;
13852
+ try {
13853
+ result = Bun.spawnSync(["/bin/sh", "-c", command], {
13854
+ cwd,
13855
+ env: childEnv,
13856
+ stdin: "ignore",
13857
+ stdout: fd,
13858
+ stderr: "ignore",
13859
+ timeout: COMMAND_TIMEOUT_MS,
13860
+ detached: true
13861
+ });
13862
+ } catch (error48) {
13863
+ return { failure: `could not start: ${messageFor(error48)}`, timedOut: false };
13864
+ } finally {
13865
+ closeSync(fd);
13866
+ }
13867
+ try {
13868
+ process.kill(-result.pid, "SIGKILL");
13869
+ } catch {}
13870
+ if (result.exitedDueToTimeout) {
13652
13871
  return {
13653
- enabled: false,
13654
- url: null,
13655
- token: null,
13656
- error: "repository dispatch.serverUrl requires dispatch.token from the same .opencode/envoy.json or DISPATCH_URL with DISPATCH_TOKEN"
13872
+ failure: `did not finish within ${COMMAND_TIMEOUT_MS / 1000} s, and does not run again in this process`,
13873
+ timedOut: true
13657
13874
  };
13658
13875
  }
13659
- const url3 = parsedDispatchUrl(repoURL, "repository dispatch.serverUrl");
13660
- if (url3.error !== null)
13661
- return { enabled: false, url: null, token: null, error: url3.error };
13662
- return { enabled: true, url: url3.url, token: token2, error: null };
13663
- }
13664
- const rawUrl = explicitUrl !== undefined ? { value: explicitUrl, source: "DISPATCH_URL" } : merged.enabled === true ? { value: merged.serverUrl ?? DEFAULT_SERVER_URL, source: "dispatch.serverUrl" } : null;
13665
- const url2 = rawUrl ? parsedDispatchUrl(rawUrl.value, rawUrl.source) : { url: null, error: null };
13666
- let token;
13667
- let tokenSource;
13668
- if (env.DISPATCH_TOKEN_FILE !== undefined) {
13669
- tokenSource = "DISPATCH_TOKEN_FILE";
13670
- try {
13671
- token = readSecretFile(tokenSource, env.DISPATCH_TOKEN_FILE);
13672
- } catch (error48) {
13673
- return { enabled: false, url: url2.url, token: null, error: messageFor(error48) };
13876
+ if (!result.success) {
13877
+ return {
13878
+ failure: `exited with ${result.signalCode ?? `status ${result.exitCode}`}`,
13879
+ timedOut: false
13880
+ };
13674
13881
  }
13675
- } else if (env.DISPATCH_TOKEN !== undefined) {
13676
- tokenSource = "DISPATCH_TOKEN";
13677
- token = env.DISPATCH_TOKEN;
13678
- } else {
13679
- tokenSource = "dispatch.token";
13680
- token = merged.token ?? null;
13681
- }
13682
- if (url2.error !== null)
13683
- return { enabled: false, url: null, token, error: url2.error };
13684
- if (url2.url === null)
13685
- return { enabled: false, url: null, token, error: null };
13686
- if (!token) {
13687
- return {
13688
- enabled: false,
13689
- url: url2.url,
13690
- token,
13691
- error: `${tokenSource} must be a non-empty bearer token`
13882
+ const value = readFileSync2(output, "utf-8").trim();
13883
+ if (value.length === 0)
13884
+ return { failure: "printed nothing", timedOut: false };
13885
+ if (!sendable(value)) {
13886
+ return {
13887
+ failure: "printed more than one line, or whitespace inside its value",
13888
+ timedOut: false
13889
+ };
13890
+ }
13891
+ return { value };
13892
+ } finally {
13893
+ rmSync(directory, { recursive: true, force: true });
13894
+ }
13895
+ }
13896
+ function fileValue(value, field, filePath, env) {
13897
+ if (!value.startsWith("!")) {
13898
+ if (!sendable(value)) {
13899
+ throw new Error(`${filePath}: ${field} holds whitespace or a control character`);
13900
+ }
13901
+ return value;
13902
+ }
13903
+ const command = value.slice(1);
13904
+ const cwd = path.dirname(path.resolve(filePath));
13905
+ const key = JSON.stringify([command, filePath, cwd]);
13906
+ let outcome = commandOutcomes.get(key);
13907
+ if (outcome === undefined || "failure" in outcome && !outcome.timedOut && Date.now() - outcome.at >= FAILURE_KEPT_MS) {
13908
+ outcome = { ...runCommand(command, env, cwd), at: Date.now() };
13909
+ commandOutcomes.set(key, outcome);
13910
+ }
13911
+ if ("failure" in outcome)
13912
+ throw new Error(`${filePath}: ${field}'s command ${outcome.failure}`);
13913
+ return outcome.value;
13914
+ }
13915
+ function statDevice(directory) {
13916
+ try {
13917
+ return statSync(directory).dev;
13918
+ } catch {
13919
+ return;
13920
+ }
13921
+ }
13922
+ function moveNotice(oldFile, newFile, move, deviceOf) {
13923
+ const oldDevice = deviceOf(path.dirname(oldFile));
13924
+ let ancestor = path.dirname(newFile);
13925
+ let newDevice = deviceOf(ancestor);
13926
+ while (newDevice === undefined && path.dirname(ancestor) !== ancestor) {
13927
+ ancestor = path.dirname(ancestor);
13928
+ newDevice = deviceOf(ancestor);
13929
+ }
13930
+ if (oldDevice !== undefined && newDevice !== undefined && oldDevice !== newDevice) {
13931
+ return `${oldFile} is not read, and is on another filesystem than ${newFile} (a mount): create ${newFile}, and move ${oldFile} on the machine that owns it`;
13932
+ }
13933
+ return `${oldFile} is not read: ${move}`;
13934
+ }
13935
+ function oldUserFileNotice(filePath, env, deviceOf) {
13936
+ const home = nonEmpty(env.HOME) ?? homedir();
13937
+ const old = path.join(home, ".config", "opencode", "envoy.json");
13938
+ if (!existsSync(old))
13939
+ return;
13940
+ const selected = path.resolve(filePath);
13941
+ const userFile = path.resolve(home, ".config", "legion", "envoy.json");
13942
+ if (selected === userFile) {
13943
+ return moveNotice(old, userFile, "mkdir -p ~/.config/legion && mv ~/.config/opencode/envoy.json ~/.config/legion/", deviceOf);
13944
+ }
13945
+ const profileFile = PLUGIN_HARNESSES.map((harness) => namedDirectory(harness, env)).some((directory) => directory !== undefined && path.isAbsolute(directory) && selected === path.resolve(directory, "envoy.json"));
13946
+ if (profileFile) {
13947
+ return `${old} is not read for this profile; copy or link it to ${filePath}, or write that file`;
13948
+ }
13949
+ const configHome = nonEmpty(env.XDG_CONFIG_HOME);
13950
+ const neutral = neutralConfig(env);
13951
+ if (configHome !== undefined && path.isAbsolute(configHome) && "path" in neutral && selected === neutral.path) {
13952
+ return `${old} is not read under XDG_CONFIG_HOME=${configHome}`;
13953
+ }
13954
+ return;
13955
+ }
13956
+ function missingConfigNotice(filePath, missing, off) {
13957
+ const link = missing.link === null ? "" : ` (a link to ${missing.link}, which does not exist)`;
13958
+ const old = missing.oldUserFileNotice === undefined ? "" : `; ${missing.oldUserFileNotice}`;
13959
+ return `no envoy.json at ${filePath}${link}: ${off}${old}`;
13960
+ }
13961
+ function readEnvoyConfig(filePath, env, options) {
13962
+ const deviceOf = options.deviceOf ?? statDevice;
13963
+ const chosen = parseFile(filePath, EnvoyFileSchema, false);
13964
+ if (chosen.kind === "invalid")
13965
+ return { kind: "failed", path: filePath, error: chosen.reason };
13966
+ const resolvedChosen = path.resolve(filePath);
13967
+ const checkoutPath = path.resolve(options.cwd, ".envoy", "envoy.json");
13968
+ const checkoutFile = checkoutPath === resolvedChosen ? { kind: "absent", link: null } : parseFile(checkoutPath, CheckoutFileSchema, true);
13969
+ const own = chosen.kind === "valid" ? chosen.settings : {};
13970
+ let checkout = null;
13971
+ if (checkoutFile.kind === "invalid") {
13972
+ checkout = { kind: "invalid", path: checkoutPath, error: checkoutFile.reason };
13973
+ } else if (checkoutFile.kind === "valid") {
13974
+ checkout = {
13975
+ kind: "valid",
13976
+ path: checkoutPath,
13977
+ dispatch: checkoutFile.settings.dispatch ?? {},
13978
+ envoyError: Object.hasOwn(checkoutFile.settings, "envoy") ? `${checkoutPath}: envoy is not read from a checkout's envoy.json; put it in ${filePath}` : null
13692
13979
  };
13693
13980
  }
13694
- return { enabled: true, url: url2.url, token, error: null };
13981
+ const oldCheckout = path.resolve(options.cwd, ".opencode", "envoy.json");
13982
+ const warnings = oldCheckout !== resolvedChosen && !existsSync(checkoutPath) && existsSync(oldCheckout) ? [
13983
+ moveNotice(oldCheckout, checkoutPath, `mkdir -p ${path.dirname(checkoutPath)} && mv ${oldCheckout} ${checkoutPath}`, deviceOf)
13984
+ ] : [];
13985
+ const unreadOldUserFile = chosen.kind === "absent" ? oldUserFileNotice(filePath, env, deviceOf) : undefined;
13986
+ return {
13987
+ kind: "read",
13988
+ path: filePath,
13989
+ dispatch: own.dispatch ?? {},
13990
+ envoy: own.envoy ?? {},
13991
+ checkout,
13992
+ missing: chosen.kind === "absent" ? {
13993
+ link: chosen.link,
13994
+ ...unreadOldUserFile === undefined ? {} : { oldUserFileNotice: unreadOldUserFile }
13995
+ } : null,
13996
+ warnings
13997
+ };
13695
13998
  }
13696
- function activeDispatchConfig(env, options = {}) {
13697
- const config2 = resolveDispatchConfig(env, options);
13698
- if (config2.error !== null)
13699
- throw new Error(`dispatch config: ${config2.error}`);
13700
- const { url: url2, token } = config2;
13701
- if (!config2.enabled || url2 === null || token === null)
13702
- return null;
13703
- return { ...config2, url: url2, token };
13999
+
14000
+ // ../envoy-client/src/bearer.ts
14001
+ function bearerSource(env, section, written) {
14002
+ const pointer = nonEmpty(env[`${section}_TOKEN_FILE`]);
14003
+ if (pointer !== undefined) {
14004
+ return { kind: "variable", token: readSecretFile(`${section}_TOKEN_FILE`, pointer) };
14005
+ }
14006
+ const plain = nonEmpty(env[`${section}_TOKEN`]);
14007
+ if (plain !== undefined) {
14008
+ if (!sendable(plain)) {
14009
+ throw new Error(`${section}_TOKEN holds whitespace or a control character`);
14010
+ }
14011
+ return { kind: "variable", token: plain };
14012
+ }
14013
+ const bearer = written();
14014
+ return bearer === undefined ? { kind: "none" } : { kind: "file", ...bearer };
14015
+ }
14016
+ function fileBearer(bearer, url2, section, env) {
14017
+ const named = httpUrl2(url2)?.origin;
14018
+ const paired = httpUrl2(bearer.server)?.origin;
14019
+ if (named !== paired) {
14020
+ throw new Error(`${section}_URL names ${named}, but ${bearer.path}'s ${bearer.field} is for ${paired}; set ${section}_TOKEN or ${section}_TOKEN_FILE for it`);
14021
+ }
14022
+ if (bearer.checkout && bearer.token.startsWith("!")) {
14023
+ throw new Error(`${bearer.path}: ${bearer.field} is a command, which a checkout's envoy.json cannot run`);
14024
+ }
14025
+ if (bearer.token === "") {
14026
+ throw new Error(`${bearer.field} in ${bearer.path} must be a non-empty bearer token`);
14027
+ }
14028
+ return fileValue(bearer.token, bearer.field, bearer.path, env);
14029
+ }
14030
+
14031
+ // ../envoy-client/src/dispatch-config.ts
14032
+ var DEFAULT_SERVER_URL = "http://localhost:8766";
14033
+ function checkoutPair(checkout, serverUrl) {
14034
+ const { token } = checkout.dispatch;
14035
+ if (token === undefined) {
14036
+ throw new Error(`${checkout.path}: dispatch.serverUrl requires dispatch.token in the same file`);
14037
+ }
14038
+ return {
14039
+ kind: "file",
14040
+ token,
14041
+ field: "dispatch.token",
14042
+ path: checkout.path,
14043
+ server: serverUrl,
14044
+ checkout: true
14045
+ };
14046
+ }
14047
+ function writtenToken(config2, checkout) {
14048
+ if (checkout?.dispatch.token !== undefined) {
14049
+ throw new Error(`${checkout.path}: dispatch.token requires dispatch.serverUrl in the same file`);
14050
+ }
14051
+ const own = config2.dispatch;
14052
+ if (own.token === undefined)
14053
+ return;
14054
+ const server = own.serverUrl ?? DEFAULT_SERVER_URL;
14055
+ return {
14056
+ token: own.token,
14057
+ field: "dispatch.token",
14058
+ path: config2.path,
14059
+ server,
14060
+ checkout: false
14061
+ };
14062
+ }
14063
+ function dispatchSettings(env, config2) {
14064
+ if (config2.kind === "failed")
14065
+ return offWithError(config2.error);
14066
+ const explicitUrl = nonEmpty(env.DISPATCH_URL);
14067
+ const checkoutFile = explicitUrl === undefined ? config2.checkout : null;
14068
+ if (checkoutFile?.kind === "invalid")
14069
+ return offWithError(checkoutFile.error);
14070
+ const checkout = checkoutFile?.kind === "valid" ? checkoutFile : null;
14071
+ const own = config2.dispatch;
14072
+ if (own.enabled === false) {
14073
+ return offWithNotice(`${config2.path} sets dispatch.enabled to false: Dispatch is off`);
14074
+ }
14075
+ if (checkout?.dispatch.enabled === false) {
14076
+ return offWithNotice(`${checkout.path} sets dispatch.enabled to false: Dispatch is off`);
14077
+ }
14078
+ const checkoutServer = checkout?.dispatch.serverUrl;
14079
+ const addressed = explicitUrl !== undefined || checkoutServer !== undefined || own.serverUrl !== undefined || own.enabled === true;
14080
+ if (!addressed) {
14081
+ return offWithNotice(config2.missing === null ? `${config2.path} names no Dispatch server, and DISPATCH_URL is not set: Dispatch is off` : missingConfigNotice(config2.path, config2.missing, "Dispatch is off"));
14082
+ }
14083
+ if (explicitUrl !== undefined && httpUrl2(explicitUrl) === undefined) {
14084
+ return offWithError("DISPATCH_URL must be an http or https URL");
14085
+ }
14086
+ try {
14087
+ const source = checkout !== null && checkoutServer !== undefined ? checkoutPair(checkout, checkoutServer) : bearerSource(env, "DISPATCH", () => writtenToken(config2, checkout));
14088
+ if (source.kind === "none") {
14089
+ return offWithError(explicitUrl === undefined ? `${config2.path}: dispatch is enabled and no token is: set dispatch.token, or DISPATCH_TOKEN or DISPATCH_TOKEN_FILE` : `DISPATCH_URL is set and no token is: set DISPATCH_TOKEN or DISPATCH_TOKEN_FILE, or dispatch.token in ${config2.path}`);
14090
+ }
14091
+ const url2 = (explicitUrl ?? (source.kind === "file" ? source.server : own.serverUrl ?? DEFAULT_SERVER_URL)).replace(/\/+$/, "");
14092
+ const token = source.kind === "variable" ? source.token : fileBearer(source, url2, "DISPATCH", env);
14093
+ return { enabled: true, url: url2, token };
14094
+ } catch (error48) {
14095
+ return offWithError(messageFor(error48));
14096
+ }
14097
+ }
14098
+ function activeDispatchSettings(resolution) {
14099
+ if (resolution.enabled)
14100
+ return resolution;
14101
+ if (resolution.error !== null)
14102
+ throw new Error(`dispatch config: ${resolution.error}`);
14103
+ return null;
13704
14104
  }
13705
14105
 
13706
14106
  // ../contracts/src/dispatch-api.ts
@@ -15077,16 +15477,16 @@ function dispatchAnswered(error48, status) {
15077
15477
  // ../envoy-client/src/dispatch-session-state.ts
15078
15478
  import {
15079
15479
  appendFileSync,
15080
- closeSync,
15480
+ closeSync as closeSync2,
15081
15481
  fstatSync,
15082
15482
  mkdirSync,
15083
- openSync,
15483
+ openSync as openSync2,
15084
15484
  readdirSync,
15085
15485
  readFileSync as readFileSync3,
15086
15486
  readSync,
15087
15487
  renameSync,
15088
- rmSync,
15089
- statSync,
15488
+ rmSync as rmSync2,
15489
+ statSync as statSync2,
15090
15490
  writeFileSync
15091
15491
  } from "fs";
15092
15492
  import { homedir as homedir2 } from "os";
@@ -15119,6 +15519,25 @@ function writeSessionTitle(dir, title) {
15119
15519
  writeAtomically(join2(dir, "title"), title);
15120
15520
  }
15121
15521
 
15522
+ // ../envoy-client/src/session-settings.ts
15523
+ function sessionConfigFile(harness, env) {
15524
+ try {
15525
+ return { path: envoyConfigPath(harness, env), error: null };
15526
+ } catch (error48) {
15527
+ return { path: null, error: messageFor(error48) };
15528
+ }
15529
+ }
15530
+ function sessionDispatchSettings(file2, env, options) {
15531
+ if (file2.path === null)
15532
+ return { dispatch: offWithError(file2.error), warnings: [], missing: null };
15533
+ const config2 = readEnvoyConfig(file2.path, env, options);
15534
+ return {
15535
+ dispatch: dispatchSettings(env, config2),
15536
+ warnings: config2.kind === "read" ? config2.warnings : [],
15537
+ missing: config2.kind === "read" ? config2.missing : null
15538
+ };
15539
+ }
15540
+
15122
15541
  // ../pi-shared/src/interface.ts
15123
15542
  import { logger } from "@oh-my-pi/pi-utils";
15124
15543
  var ENVOY_PLUGIN_INTERFACE_VERSION = 2;
@@ -15650,7 +16069,7 @@ async function measureCapabilities(host2, input, budgets = DEFAULT_BUDGETS) {
15650
16069
  });
15651
16070
  return { measuredAt, elapsedMs: Date.now() - started, rows };
15652
16071
  }
15653
- async function runCommand(command, args, options) {
16072
+ async function runCommand2(command, args, options) {
15654
16073
  const child = Bun.spawn([command, ...args], {
15655
16074
  cwd: options.cwd,
15656
16075
  env: process.env,
@@ -15700,7 +16119,7 @@ function ompCapabilityHost() {
15700
16119
  disabledExtensions: cfgDisabledExtensions.get(settings)
15701
16120
  });
15702
16121
  },
15703
- run: runCommand
16122
+ run: runCommand2
15704
16123
  };
15705
16124
  }
15706
16125
 
@@ -15729,7 +16148,7 @@ function onEnvoyRoleRegained(listener) {
15729
16148
  // package.json
15730
16149
  var package_default = {
15731
16150
  name: "@sjawhar/pi-legion",
15732
- version: "9.8.1",
16151
+ version: "10.0.0",
15733
16152
  license: "Apache-2.0",
15734
16153
  type: "module",
15735
16154
  omp: {
@@ -16256,17 +16675,17 @@ async function userConfigChain(timeoutMs = JJ_CONFIG_PATH_TIMEOUT_MS, env = proc
16256
16675
  new Response(child.stdout).text(),
16257
16676
  new Response(child.stderr).text()
16258
16677
  ]);
16259
- const stdout = await new Promise((resolve) => {
16678
+ const stdout = await new Promise((resolve2) => {
16260
16679
  const timer = setTimeout(() => {
16261
16680
  killGroup();
16262
- resolve(undefined);
16681
+ resolve2(undefined);
16263
16682
  }, timeoutMs);
16264
16683
  completed.then(([code, output]) => {
16265
16684
  clearTimeout(timer);
16266
- resolve(code === 0 ? output : undefined);
16685
+ resolve2(code === 0 ? output : undefined);
16267
16686
  }, () => {
16268
16687
  clearTimeout(timer);
16269
- resolve(undefined);
16688
+ resolve2(undefined);
16270
16689
  });
16271
16690
  });
16272
16691
  if (stdout === undefined)
@@ -16515,6 +16934,45 @@ function createControllerSession(deps) {
16515
16934
  // src/handoff-commit.ts
16516
16935
  import { execFile as execFile2 } from "child_process";
16517
16936
  import { promisify as promisify2 } from "util";
16937
+
16938
+ // src/git-lock.ts
16939
+ import { lstat } from "fs/promises";
16940
+ var GIT_LOCK_REPORTS = [
16941
+ /The lockfile at '([^']+)' might need manual deletion/g,
16942
+ /Unable to create '([^']+)': File exists/g
16943
+ ];
16944
+ function age(ms) {
16945
+ const total = Math.round(Math.abs(ms) / 1000);
16946
+ const sign = ms < 0 && total > 0 ? "-" : "";
16947
+ const hours = Math.floor(total / 3600);
16948
+ const minutes = Math.floor(total % 3600 / 60);
16949
+ const seconds = total % 60;
16950
+ if (hours > 0)
16951
+ return `${sign}${hours}h${minutes}m${seconds}s`;
16952
+ if (minutes > 0)
16953
+ return `${sign}${minutes}m${seconds}s`;
16954
+ return `${sign}${seconds}s`;
16955
+ }
16956
+ async function staleGitLockNote(output, now = Date.now()) {
16957
+ const locks = new Set;
16958
+ for (const report of GIT_LOCK_REPORTS) {
16959
+ for (const match of output.matchAll(report))
16960
+ locks.add(match[1]);
16961
+ }
16962
+ const notes = [];
16963
+ for (const lock of locks) {
16964
+ const written = await lstat(lock).then((info) => info.mtimeMs, () => {
16965
+ return;
16966
+ });
16967
+ if (written === undefined)
16968
+ continue;
16969
+ notes.push(`git lock ${lock} was last written ${age(now - written)} ago and blocks this command. Do not remove it while the issue's pod runs: another role's git, which this container cannot see, may hold it. Replacing the issue's pod removes it. Under the tmux runtime nothing removes it: remove it by hand only once no git or jj process on the host is using the repository.`);
16970
+ }
16971
+ return notes.join(`
16972
+ `);
16973
+ }
16974
+
16975
+ // src/handoff-commit.ts
16518
16976
  var FILE_BACKED = {
16519
16977
  planning: { word: "plan", role: "planner" },
16520
16978
  implementing: { word: "implement", role: "implementer" },
@@ -16531,7 +16989,9 @@ var pathJj = async (args, cwd) => {
16531
16989
  return stdout.trim();
16532
16990
  } catch (error48) {
16533
16991
  const detail = error48 !== null && typeof error48 === "object" && "stderr" in error48 ? String(error48.stderr).trim() : error48 instanceof Error ? error48.message : String(error48);
16534
- throw new Error(`jj ${args.join(" ")} failed: ${detail}`);
16992
+ const note = await staleGitLockNote(detail);
16993
+ throw new Error(`jj ${args.join(" ")} failed: ${detail}${note === "" ? "" : `
16994
+ ${note}`}`);
16535
16995
  }
16536
16996
  };
16537
16997
  function requiredVariable(env, name) {
@@ -17158,7 +17618,9 @@ function legionExtension(pi) {
17158
17618
  onPhaseCompleted,
17159
17619
  handoffCommit: (phase, role) => findHandoffCommit({ phase, role, env: process.env }),
17160
17620
  resolveDocument: async (issue2, reference) => {
17161
- const config2 = activeDispatchConfig(process.env, { cwd: process.cwd() });
17621
+ const config2 = activeDispatchSettings(sessionDispatchSettings(sessionConfigFile("omp", process.env), process.env, {
17622
+ cwd: process.cwd()
17623
+ }).dispatch);
17162
17624
  if (config2 === null) {
17163
17625
  throw new Error(`register_gate cannot look up document "${reference}" on ${issue2}: this pane has no Dispatch configured; pass the document's id`);
17164
17626
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion",
3
- "version": "9.8.1",
3
+ "version": "10.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "omp": {