@sjawhar/claude-envoy 1.12.0 → 1.13.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/.claude-plugin/plugin.json +1 -1
- package/.omp-plugin/plugin.json +1 -1
- package/dist/dispatch.js +21 -11
- package/dist/envoy-channel.js +78 -43
- package/dist/session-hook.js +20 -11
- package/legion/skills/legion-worker/references/merge-gate.md +7 -3
- package/legion/skills/legion-worker/references/pr-body.md +4 -4
- package/package.json +1 -1
- package/src/session-title.ts +3 -42
package/.omp-plugin/plugin.json
CHANGED
package/dist/dispatch.js
CHANGED
|
@@ -17565,9 +17565,12 @@ async function tryExec(exec, file, args, cwd, limitMs) {
|
|
|
17565
17565
|
exec(file, args, { cwd, signal: controller.signal }),
|
|
17566
17566
|
expired.promise
|
|
17567
17567
|
]);
|
|
17568
|
-
|
|
17568
|
+
if (answer === null) {
|
|
17569
|
+
return { failure: "timedOut", ok: false };
|
|
17570
|
+
}
|
|
17571
|
+
return { ok: true, stdout: answer.stdout };
|
|
17569
17572
|
} catch {
|
|
17570
|
-
return
|
|
17573
|
+
return { failure: "refused", ok: false };
|
|
17571
17574
|
} finally {
|
|
17572
17575
|
clearTimeout(timer);
|
|
17573
17576
|
}
|
|
@@ -17608,13 +17611,13 @@ function parseGitHubRemoteUrl(url) {
|
|
|
17608
17611
|
return null;
|
|
17609
17612
|
}
|
|
17610
17613
|
async function resolveCwdRepo(cwd, exec) {
|
|
17611
|
-
const
|
|
17612
|
-
if (
|
|
17613
|
-
const url = selectRemoteUrl(parseRemoteList(
|
|
17614
|
+
const remoteList = await tryExec(exec, "jj", ["git", "remote", "list"], cwd, REMOTE_LOOKUP_MS);
|
|
17615
|
+
if (remoteList.ok) {
|
|
17616
|
+
const url = selectRemoteUrl(parseRemoteList(remoteList.stdout));
|
|
17614
17617
|
return url ? parseGitHubRemoteUrl(url) : null;
|
|
17615
17618
|
}
|
|
17616
|
-
const
|
|
17617
|
-
return
|
|
17619
|
+
const originRemote = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd, REMOTE_LOOKUP_MS);
|
|
17620
|
+
return originRemote.ok ? parseGitHubRemoteUrl(originRemote.stdout) : null;
|
|
17618
17621
|
}
|
|
17619
17622
|
async function resolveOrigin(env, exec, cwd) {
|
|
17620
17623
|
const origin = {
|
|
@@ -17623,8 +17626,8 @@ async function resolveOrigin(env, exec, cwd) {
|
|
|
17623
17626
|
};
|
|
17624
17627
|
const pane = env.TMUX_PANE;
|
|
17625
17628
|
if (pane) {
|
|
17626
|
-
const
|
|
17627
|
-
const [target, paneId] =
|
|
17629
|
+
const answer = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd, TMUX_PANE_LOOKUP_MS);
|
|
17630
|
+
const [target, paneId] = answer.ok ? answer.stdout.trim().split(" ") : [];
|
|
17628
17631
|
if (target)
|
|
17629
17632
|
origin.tmux = target;
|
|
17630
17633
|
if (paneId)
|
|
@@ -27532,6 +27535,7 @@ function dispatchTarget(env, config) {
|
|
|
27532
27535
|
var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
|
|
27533
27536
|
var DEFAULT_HEARTBEAT_MS = SESSION_HEARTBEAT_MS;
|
|
27534
27537
|
var MIN_PASS_DEADLINE_MS = 30000;
|
|
27538
|
+
var DEFAULT_TMUX_PLACE_CACHE_MS = 30000;
|
|
27535
27539
|
var MIN_INTERVAL_MS = 25;
|
|
27536
27540
|
var MAX_INTERVAL_MS = 2147483647;
|
|
27537
27541
|
var REGISTRATION_SETTLE_MS = 5000;
|
|
@@ -27545,6 +27549,10 @@ function namedInterval(raw) {
|
|
|
27545
27549
|
const parsed = Number(raw);
|
|
27546
27550
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
27547
27551
|
}
|
|
27552
|
+
function resolvedInterval(raw, fallback) {
|
|
27553
|
+
const named = namedInterval(raw);
|
|
27554
|
+
return named === undefined ? fallback : Math.min(Math.max(named, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
|
|
27555
|
+
}
|
|
27548
27556
|
function natsSeedFromEnvironment(env) {
|
|
27549
27557
|
const { NATS_NKEY_SEED_FILE: file, NATS_NKEY_SEED: plain } = env;
|
|
27550
27558
|
if (file !== undefined) {
|
|
@@ -27635,8 +27643,8 @@ function envoySettings(env, config, options = {}) {
|
|
|
27635
27643
|
inbound = { off: messageFor(error) };
|
|
27636
27644
|
}
|
|
27637
27645
|
}
|
|
27638
|
-
const
|
|
27639
|
-
const
|
|
27646
|
+
const heartbeatMs = resolvedInterval(env.ENVOY_HEARTBEAT_MS, DEFAULT_HEARTBEAT_MS);
|
|
27647
|
+
const tmuxPlaceCacheMs = resolvedInterval(env.ENVOY_TMUX_PLACE_CACHE_MS, DEFAULT_TMUX_PLACE_CACHE_MS);
|
|
27640
27648
|
const namedPassDeadlineMs = namedInterval(env.ENVOY_PASS_DEADLINE_MS);
|
|
27641
27649
|
return {
|
|
27642
27650
|
enabled: true,
|
|
@@ -27644,6 +27652,7 @@ function envoySettings(env, config, options = {}) {
|
|
|
27644
27652
|
credential,
|
|
27645
27653
|
inbound,
|
|
27646
27654
|
heartbeatMs,
|
|
27655
|
+
tmuxPlaceCacheMs,
|
|
27647
27656
|
passDeadlineMs: Math.min(Math.max(namedPassDeadlineMs ?? MIN_PASS_DEADLINE_MS, heartbeatMs), MAX_INTERVAL_MS),
|
|
27648
27657
|
passDeadlineRaisedFrom: namedPassDeadlineMs !== undefined && namedPassDeadlineMs < heartbeatMs ? namedPassDeadlineMs : undefined
|
|
27649
27658
|
};
|
|
@@ -28063,6 +28072,7 @@ function createEnvoyClient(config) {
|
|
|
28063
28072
|
...input.delivery === undefined ? {} : { delivery: input.delivery },
|
|
28064
28073
|
...input.clientInstanceID === undefined ? {} : { client_instance_id: input.clientInstanceID },
|
|
28065
28074
|
host: machineID(),
|
|
28075
|
+
...input.place === undefined ? {} : { tmux_place: input.place },
|
|
28066
28076
|
plugin: input.plugin.name,
|
|
28067
28077
|
plugin_version: input.plugin.version
|
|
28068
28078
|
}, { actingSession: input.sessionID }))),
|
package/dist/envoy-channel.js
CHANGED
|
@@ -44626,6 +44626,7 @@ function fileBearer(bearer, url, section, env, commands = "run") {
|
|
|
44626
44626
|
var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
|
|
44627
44627
|
var DEFAULT_HEARTBEAT_MS = SESSION_HEARTBEAT_MS;
|
|
44628
44628
|
var MIN_PASS_DEADLINE_MS = 30000;
|
|
44629
|
+
var DEFAULT_TMUX_PLACE_CACHE_MS = 30000;
|
|
44629
44630
|
var MIN_INTERVAL_MS = 25;
|
|
44630
44631
|
var MAX_INTERVAL_MS = 2147483647;
|
|
44631
44632
|
var REGISTRATION_SETTLE_MS = 5000;
|
|
@@ -44639,6 +44640,10 @@ function namedInterval(raw) {
|
|
|
44639
44640
|
const parsed = Number(raw);
|
|
44640
44641
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
44641
44642
|
}
|
|
44643
|
+
function resolvedInterval(raw, fallback) {
|
|
44644
|
+
const named = namedInterval(raw);
|
|
44645
|
+
return named === undefined ? fallback : Math.min(Math.max(named, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
|
|
44646
|
+
}
|
|
44642
44647
|
function natsSeedFromEnvironment(env) {
|
|
44643
44648
|
const { NATS_NKEY_SEED_FILE: file, NATS_NKEY_SEED: plain } = env;
|
|
44644
44649
|
if (file !== undefined) {
|
|
@@ -44729,8 +44734,8 @@ function envoySettings(env, config, options = {}) {
|
|
|
44729
44734
|
inbound = { off: messageFor(error) };
|
|
44730
44735
|
}
|
|
44731
44736
|
}
|
|
44732
|
-
const
|
|
44733
|
-
const
|
|
44737
|
+
const heartbeatMs = resolvedInterval(env.ENVOY_HEARTBEAT_MS, DEFAULT_HEARTBEAT_MS);
|
|
44738
|
+
const tmuxPlaceCacheMs = resolvedInterval(env.ENVOY_TMUX_PLACE_CACHE_MS, DEFAULT_TMUX_PLACE_CACHE_MS);
|
|
44734
44739
|
const namedPassDeadlineMs = namedInterval(env.ENVOY_PASS_DEADLINE_MS);
|
|
44735
44740
|
return {
|
|
44736
44741
|
enabled: true,
|
|
@@ -44738,6 +44743,7 @@ function envoySettings(env, config, options = {}) {
|
|
|
44738
44743
|
credential,
|
|
44739
44744
|
inbound,
|
|
44740
44745
|
heartbeatMs,
|
|
44746
|
+
tmuxPlaceCacheMs,
|
|
44741
44747
|
passDeadlineMs: Math.min(Math.max(namedPassDeadlineMs ?? MIN_PASS_DEADLINE_MS, heartbeatMs), MAX_INTERVAL_MS),
|
|
44742
44748
|
passDeadlineRaisedFrom: namedPassDeadlineMs !== undefined && namedPassDeadlineMs < heartbeatMs ? namedPassDeadlineMs : undefined
|
|
44743
44749
|
};
|
|
@@ -45064,6 +45070,7 @@ function createEnvoyClient(config) {
|
|
|
45064
45070
|
...input.delivery === undefined ? {} : { delivery: input.delivery },
|
|
45065
45071
|
...input.clientInstanceID === undefined ? {} : { client_instance_id: input.clientInstanceID },
|
|
45066
45072
|
host: machineID(),
|
|
45073
|
+
...input.place === undefined ? {} : { tmux_place: input.place },
|
|
45067
45074
|
plugin: input.plugin.name,
|
|
45068
45075
|
plugin_version: input.plugin.version
|
|
45069
45076
|
}, { actingSession: input.sessionID }))),
|
|
@@ -46869,9 +46876,12 @@ async function tryExec(exec, file, args, cwd, limitMs) {
|
|
|
46869
46876
|
exec(file, args, { cwd, signal: controller.signal }),
|
|
46870
46877
|
expired.promise
|
|
46871
46878
|
]);
|
|
46872
|
-
|
|
46879
|
+
if (answer === null) {
|
|
46880
|
+
return { failure: "timedOut", ok: false };
|
|
46881
|
+
}
|
|
46882
|
+
return { ok: true, stdout: answer.stdout };
|
|
46873
46883
|
} catch {
|
|
46874
|
-
return
|
|
46884
|
+
return { failure: "refused", ok: false };
|
|
46875
46885
|
} finally {
|
|
46876
46886
|
clearTimeout(timer);
|
|
46877
46887
|
}
|
|
@@ -46912,13 +46922,13 @@ function parseGitHubRemoteUrl(url) {
|
|
|
46912
46922
|
return null;
|
|
46913
46923
|
}
|
|
46914
46924
|
async function resolveCwdRepo(cwd, exec) {
|
|
46915
|
-
const
|
|
46916
|
-
if (
|
|
46917
|
-
const url = selectRemoteUrl(parseRemoteList(
|
|
46925
|
+
const remoteList = await tryExec(exec, "jj", ["git", "remote", "list"], cwd, REMOTE_LOOKUP_MS);
|
|
46926
|
+
if (remoteList.ok) {
|
|
46927
|
+
const url = selectRemoteUrl(parseRemoteList(remoteList.stdout));
|
|
46918
46928
|
return url ? parseGitHubRemoteUrl(url) : null;
|
|
46919
46929
|
}
|
|
46920
|
-
const
|
|
46921
|
-
return
|
|
46930
|
+
const originRemote = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd, REMOTE_LOOKUP_MS);
|
|
46931
|
+
return originRemote.ok ? parseGitHubRemoteUrl(originRemote.stdout) : null;
|
|
46922
46932
|
}
|
|
46923
46933
|
async function resolveOrigin(env, exec, cwd) {
|
|
46924
46934
|
const origin = {
|
|
@@ -46927,8 +46937,8 @@ async function resolveOrigin(env, exec, cwd) {
|
|
|
46927
46937
|
};
|
|
46928
46938
|
const pane = env.TMUX_PANE;
|
|
46929
46939
|
if (pane) {
|
|
46930
|
-
const
|
|
46931
|
-
const [target, paneId] =
|
|
46940
|
+
const answer = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd, TMUX_PANE_LOOKUP_MS);
|
|
46941
|
+
const [target, paneId] = answer.ok ? answer.stdout.trim().split(" ") : [];
|
|
46932
46942
|
if (target)
|
|
46933
46943
|
origin.tmux = target;
|
|
46934
46944
|
if (paneId)
|
|
@@ -55145,7 +55155,7 @@ var import_nats2 = __toESM(require_mod4(), 1);
|
|
|
55145
55155
|
|
|
55146
55156
|
// package.json
|
|
55147
55157
|
var name = "@sjawhar/claude-envoy";
|
|
55148
|
-
var version2 = "1.
|
|
55158
|
+
var version2 = "1.13.0";
|
|
55149
55159
|
var legion = {
|
|
55150
55160
|
daemonApiVersion: 55
|
|
55151
55161
|
};
|
|
@@ -57218,9 +57228,63 @@ function replayStateWriter(file, restored) {
|
|
|
57218
57228
|
}
|
|
57219
57229
|
|
|
57220
57230
|
// src/session-title.ts
|
|
57231
|
+
import { basename as basename4 } from "path";
|
|
57232
|
+
|
|
57233
|
+
// ../envoy-client/src/process-ancestry.ts
|
|
57221
57234
|
import { execFileSync } from "child_process";
|
|
57222
57235
|
import { readFileSync as readFileSync9 } from "fs";
|
|
57223
|
-
|
|
57236
|
+
var MAX_ANCESTORS = 64;
|
|
57237
|
+
var PARENT_LOOKUP_MS = 1000;
|
|
57238
|
+
function procParentPid(pid) {
|
|
57239
|
+
try {
|
|
57240
|
+
const stat = readFileSync9(`/proc/${pid}/stat`, "utf8");
|
|
57241
|
+
const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
57242
|
+
return Number.isInteger(parent) ? parent : undefined;
|
|
57243
|
+
} catch {
|
|
57244
|
+
return;
|
|
57245
|
+
}
|
|
57246
|
+
}
|
|
57247
|
+
function processTable() {
|
|
57248
|
+
const table = new Map;
|
|
57249
|
+
try {
|
|
57250
|
+
const listing = execFileSync("ps", ["-Ao", "pid=,ppid="], {
|
|
57251
|
+
encoding: "utf8",
|
|
57252
|
+
killSignal: "SIGKILL",
|
|
57253
|
+
timeout: PARENT_LOOKUP_MS
|
|
57254
|
+
});
|
|
57255
|
+
for (const line of listing.split(`
|
|
57256
|
+
`)) {
|
|
57257
|
+
const [, pid = "", parent = ""] = line.match(/^\s*(\d+)\s+(\d+)\s*$/) ?? [];
|
|
57258
|
+
if (pid !== "")
|
|
57259
|
+
table.set(Number(pid), Number(parent));
|
|
57260
|
+
}
|
|
57261
|
+
} catch {}
|
|
57262
|
+
return table;
|
|
57263
|
+
}
|
|
57264
|
+
function isAncestor(candidate, pid = process.pid) {
|
|
57265
|
+
let table;
|
|
57266
|
+
const parentPid = (child) => {
|
|
57267
|
+
if (process.platform === "linux")
|
|
57268
|
+
return procParentPid(child);
|
|
57269
|
+
table ??= processTable();
|
|
57270
|
+
return table.get(child);
|
|
57271
|
+
};
|
|
57272
|
+
let current = pid;
|
|
57273
|
+
for (let step = 0;current !== undefined && current > 1 && step < MAX_ANCESTORS; step++) {
|
|
57274
|
+
if (String(current) === candidate)
|
|
57275
|
+
return true;
|
|
57276
|
+
current = parentPid(current);
|
|
57277
|
+
}
|
|
57278
|
+
return false;
|
|
57279
|
+
}
|
|
57280
|
+
|
|
57281
|
+
// ../envoy-client/src/tmux-server.ts
|
|
57282
|
+
function tmuxServer(value) {
|
|
57283
|
+
const parts = (value ?? "").split(",");
|
|
57284
|
+
return { serverPid: parts.at(-2) ?? "", socket: parts.slice(0, -2).join(",") };
|
|
57285
|
+
}
|
|
57286
|
+
|
|
57287
|
+
// src/session-title.ts
|
|
57224
57288
|
var TMUX_PANE_FORMAT = [
|
|
57225
57289
|
"#{session_name}",
|
|
57226
57290
|
"#{window_name}",
|
|
@@ -57231,7 +57295,6 @@ var TMUX_PANE_FORMAT = [
|
|
|
57231
57295
|
].join("\t");
|
|
57232
57296
|
var TMUX_TIMEOUT_MS = 1000;
|
|
57233
57297
|
var SHORT_ID_LENGTH = 8;
|
|
57234
|
-
var MAX_ANCESTORS = 64;
|
|
57235
57298
|
var CSI_SEQUENCE = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "gu");
|
|
57236
57299
|
function oneLine(value) {
|
|
57237
57300
|
return value.replace(CSI_SEQUENCE, "").replace(/[\u202a-\u202e\u2066-\u2069]/gu, "").replace(/[\s\p{Cc}]+/gu, " ").trim();
|
|
@@ -57239,32 +57302,6 @@ function oneLine(value) {
|
|
|
57239
57302
|
function visible(value) {
|
|
57240
57303
|
return value.replace(/[\s\p{Cc}\p{Cf}\p{Cs}\p{Cn}\p{M}\p{Default_Ignorable_Code_Point}\u2800]/gu, "") !== "";
|
|
57241
57304
|
}
|
|
57242
|
-
function parentPid(pid) {
|
|
57243
|
-
try {
|
|
57244
|
-
if (process.platform === "linux") {
|
|
57245
|
-
const stat = readFileSync9(`/proc/${pid}/stat`, "utf8");
|
|
57246
|
-
const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
57247
|
-
return Number.isInteger(parent) ? parent : undefined;
|
|
57248
|
-
}
|
|
57249
|
-
const parent = Number(execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
|
|
57250
|
-
encoding: "utf8",
|
|
57251
|
-
timeout: TMUX_TIMEOUT_MS,
|
|
57252
|
-
killSignal: "SIGKILL"
|
|
57253
|
-
}).trim());
|
|
57254
|
-
return Number.isInteger(parent) ? parent : undefined;
|
|
57255
|
-
} catch {
|
|
57256
|
-
return;
|
|
57257
|
-
}
|
|
57258
|
-
}
|
|
57259
|
-
function isAncestor(candidate, pid = process.pid) {
|
|
57260
|
-
let current = pid;
|
|
57261
|
-
for (let step = 0;current !== undefined && current > 1 && step < MAX_ANCESTORS; step++) {
|
|
57262
|
-
if (String(current) === candidate)
|
|
57263
|
-
return true;
|
|
57264
|
-
current = parentPid(current);
|
|
57265
|
-
}
|
|
57266
|
-
return false;
|
|
57267
|
-
}
|
|
57268
57305
|
async function queryTmux(socket, pane, format) {
|
|
57269
57306
|
let child;
|
|
57270
57307
|
try {
|
|
@@ -57328,9 +57365,7 @@ async function sessionTitle(environment, directory, sources = {}) {
|
|
|
57328
57365
|
const explicit = oneLine(configuredValue(environment.ENVOY_SESSION_TITLE) ?? "");
|
|
57329
57366
|
if (visible(explicit))
|
|
57330
57367
|
return () => explicit;
|
|
57331
|
-
const
|
|
57332
|
-
const socket = parts.slice(0, -2).join(",");
|
|
57333
|
-
const serverPid = parts.at(-2) ?? "";
|
|
57368
|
+
const { serverPid, socket } = tmuxServer(configuredValue(environment.TMUX));
|
|
57334
57369
|
const pane = configuredValue(environment.TMUX_PANE)?.trim();
|
|
57335
57370
|
const directoryName = oneLine(basename4(directory) || directory);
|
|
57336
57371
|
const name = (socket === "" || serverPid === "" || pane === undefined ? undefined : await tmuxPaneTitle(socket, serverPid, pane, ownPane, query)) ?? (visible(directoryName) ? directoryName : "session");
|
package/dist/session-hook.js
CHANGED
|
@@ -16569,9 +16569,12 @@ async function tryExec(exec, file, args, cwd, limitMs) {
|
|
|
16569
16569
|
exec(file, args, { cwd, signal: controller.signal }),
|
|
16570
16570
|
expired.promise
|
|
16571
16571
|
]);
|
|
16572
|
-
|
|
16572
|
+
if (answer === null) {
|
|
16573
|
+
return { failure: "timedOut", ok: false };
|
|
16574
|
+
}
|
|
16575
|
+
return { ok: true, stdout: answer.stdout };
|
|
16573
16576
|
} catch {
|
|
16574
|
-
return
|
|
16577
|
+
return { failure: "refused", ok: false };
|
|
16575
16578
|
} finally {
|
|
16576
16579
|
clearTimeout(timer);
|
|
16577
16580
|
}
|
|
@@ -16612,13 +16615,13 @@ function parseGitHubRemoteUrl(url) {
|
|
|
16612
16615
|
return null;
|
|
16613
16616
|
}
|
|
16614
16617
|
async function resolveCwdRepo(cwd, exec) {
|
|
16615
|
-
const
|
|
16616
|
-
if (
|
|
16617
|
-
const url = selectRemoteUrl(parseRemoteList(
|
|
16618
|
+
const remoteList = await tryExec(exec, "jj", ["git", "remote", "list"], cwd, REMOTE_LOOKUP_MS);
|
|
16619
|
+
if (remoteList.ok) {
|
|
16620
|
+
const url = selectRemoteUrl(parseRemoteList(remoteList.stdout));
|
|
16618
16621
|
return url ? parseGitHubRemoteUrl(url) : null;
|
|
16619
16622
|
}
|
|
16620
|
-
const
|
|
16621
|
-
return
|
|
16623
|
+
const originRemote = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd, REMOTE_LOOKUP_MS);
|
|
16624
|
+
return originRemote.ok ? parseGitHubRemoteUrl(originRemote.stdout) : null;
|
|
16622
16625
|
}
|
|
16623
16626
|
async function resolveOrigin(env, exec, cwd) {
|
|
16624
16627
|
const origin = {
|
|
@@ -16627,8 +16630,8 @@ async function resolveOrigin(env, exec, cwd) {
|
|
|
16627
16630
|
};
|
|
16628
16631
|
const pane = env.TMUX_PANE;
|
|
16629
16632
|
if (pane) {
|
|
16630
|
-
const
|
|
16631
|
-
const [target, paneId] =
|
|
16633
|
+
const answer = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd, TMUX_PANE_LOOKUP_MS);
|
|
16634
|
+
const [target, paneId] = answer.ok ? answer.stdout.trim().split(" ") : [];
|
|
16632
16635
|
if (target)
|
|
16633
16636
|
origin.tmux = target;
|
|
16634
16637
|
if (paneId)
|
|
@@ -26411,6 +26414,7 @@ function dispatchTarget(env, config) {
|
|
|
26411
26414
|
var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
|
|
26412
26415
|
var DEFAULT_HEARTBEAT_MS = SESSION_HEARTBEAT_MS;
|
|
26413
26416
|
var MIN_PASS_DEADLINE_MS = 30000;
|
|
26417
|
+
var DEFAULT_TMUX_PLACE_CACHE_MS = 30000;
|
|
26414
26418
|
var MIN_INTERVAL_MS = 25;
|
|
26415
26419
|
var MAX_INTERVAL_MS = 2147483647;
|
|
26416
26420
|
var REGISTRATION_SETTLE_MS = 5000;
|
|
@@ -26424,6 +26428,10 @@ function namedInterval(raw) {
|
|
|
26424
26428
|
const parsed = Number(raw);
|
|
26425
26429
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
26426
26430
|
}
|
|
26431
|
+
function resolvedInterval(raw, fallback) {
|
|
26432
|
+
const named = namedInterval(raw);
|
|
26433
|
+
return named === undefined ? fallback : Math.min(Math.max(named, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
|
|
26434
|
+
}
|
|
26427
26435
|
function natsSeedFromEnvironment(env) {
|
|
26428
26436
|
const { NATS_NKEY_SEED_FILE: file, NATS_NKEY_SEED: plain } = env;
|
|
26429
26437
|
if (file !== undefined) {
|
|
@@ -26514,8 +26522,8 @@ function envoySettings(env, config, options = {}) {
|
|
|
26514
26522
|
inbound = { off: messageFor(error) };
|
|
26515
26523
|
}
|
|
26516
26524
|
}
|
|
26517
|
-
const
|
|
26518
|
-
const
|
|
26525
|
+
const heartbeatMs = resolvedInterval(env.ENVOY_HEARTBEAT_MS, DEFAULT_HEARTBEAT_MS);
|
|
26526
|
+
const tmuxPlaceCacheMs = resolvedInterval(env.ENVOY_TMUX_PLACE_CACHE_MS, DEFAULT_TMUX_PLACE_CACHE_MS);
|
|
26519
26527
|
const namedPassDeadlineMs = namedInterval(env.ENVOY_PASS_DEADLINE_MS);
|
|
26520
26528
|
return {
|
|
26521
26529
|
enabled: true,
|
|
@@ -26523,6 +26531,7 @@ function envoySettings(env, config, options = {}) {
|
|
|
26523
26531
|
credential,
|
|
26524
26532
|
inbound,
|
|
26525
26533
|
heartbeatMs,
|
|
26534
|
+
tmuxPlaceCacheMs,
|
|
26526
26535
|
passDeadlineMs: Math.min(Math.max(namedPassDeadlineMs ?? MIN_PASS_DEADLINE_MS, heartbeatMs), MAX_INTERVAL_MS),
|
|
26527
26536
|
passDeadlineRaisedFrom: namedPassDeadlineMs !== undefined && namedPassDeadlineMs < heartbeatMs ? namedPassDeadlineMs : undefined
|
|
26528
26537
|
};
|
|
@@ -55,13 +55,17 @@ other change to the head voids it.
|
|
|
55
55
|
(`gh api repos/<o>/<r>/commits/<headRefOid>/check-runs --paginate --jq '.check_runs[] | [.name, .status, .conclusion] | @tsv'`):
|
|
56
56
|
every required name present, `completed` and `success` (GitHub counts `neutral` and `skipped`
|
|
57
57
|
as passing for a required check). A required check
|
|
58
|
-
`queued` or `in_progress`: wait. One `completed` as `cancelled`, or absent:
|
|
58
|
+
`queued` or `in_progress`: wait. One `completed` as `cancelled`, or absent: on a `pull_request`
|
|
59
|
+
run, re-run that run's
|
|
59
60
|
failed and cancelled jobs, once, then wait — never the whole run, which runs again every job that
|
|
60
|
-
succeeded too.
|
|
61
|
+
succeeded too. A merge-group run is never re-run: the implementer finishes its round instead,
|
|
62
|
+
and the pull request is tested again when it is queued again.
|
|
63
|
+
Find the run and job at the head: the head's runs
|
|
61
64
|
(`gh api 'repos/<o>/<r>/actions/runs?head_sha=<headRefOid>&per_page=100' --jq '.workflow_runs[] | [.id, .name, .event, .status, .conclusion] | @tsv'`),
|
|
62
65
|
then each candidate run's jobs
|
|
63
66
|
(`gh api repos/<o>/<r>/actions/runs/<run-id>/jobs --jq '.jobs[] | [.id, .name, .status, .conclusion] | @tsv'`).
|
|
64
|
-
A cancelled job: `gh run rerun <run-id> --failed`, which GitHub applies to
|
|
67
|
+
A cancelled job of a pull-request run: `gh run rerun <run-id> --failed`, which GitHub applies to
|
|
68
|
+
the run's failed and
|
|
65
69
|
cancelled jobs and the jobs that depend on them, and to nothing that succeeded. Never
|
|
66
70
|
`--job <job-id>` alone: under a routing workflow, re-running only the final job reads the
|
|
67
71
|
cancelled results again and stays red. GitHub refuses a re-run while an attempt of that run is
|
|
@@ -54,11 +54,11 @@ gives them).
|
|
|
54
54
|
Under the Go daemon a check or workflow the base branch requires that settles red on the head,
|
|
55
55
|
before the completion or after it, sends the work back to `implementing` with the failing checks
|
|
56
56
|
named, until `max_fix_attempts` is spent, when the architect decides. The one CI result an implementer waits for is a re-run: when its task names a red that
|
|
57
|
-
failed on its own, it re-runs the failed jobs once (`gh run rerun <run-id> --failed`) and completes once
|
|
57
|
+
failed on its own in a `pull_request` run, it re-runs the failed jobs once (`gh run rerun <run-id> --failed`) and completes once
|
|
58
58
|
the re-run settles green, woken by the pull request's checks event (`skill://envoy`, *Wait for
|
|
59
|
-
CI or a merge*: subscribe, then end the turn), never by a sleep loop.
|
|
60
|
-
|
|
61
|
-
(within two minutes) for a required workflow.
|
|
59
|
+
CI or a merge*: subscribe, then end the turn), never by a sleep loop.
|
|
60
|
+
A merge-group run is never re-run: the implementer finishes its round instead, and the pull request is tested again when it is queued again.
|
|
61
|
+
A completion while that red still stands is sent back, at once for a required check and at the daemon's next read of the runs (within two minutes) for a required workflow.
|
|
62
62
|
|
|
63
63
|
A pull-request body edit re-runs a check that judges the body at once, on the head as it is. While
|
|
64
64
|
the pull request conflicts with its base (GitHub shows it `CONFLICTING`), no workflow triggered
|
package/package.json
CHANGED
package/src/session-title.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process"
|
|
2
|
-
import { readFileSync } from "node:fs"
|
|
3
1
|
import { basename } from "node:path"
|
|
2
|
+
import { isAncestor } from "@legion/envoy-client/process-ancestry"
|
|
3
|
+
import { tmuxServer } from "@legion/envoy-client/tmux-server"
|
|
4
4
|
import { configuredValue } from "./claude-session"
|
|
5
5
|
|
|
6
6
|
export interface SessionTitleEnvironment {
|
|
@@ -48,9 +48,6 @@ const TMUX_PANE_FORMAT = [
|
|
|
48
48
|
const TMUX_TIMEOUT_MS = 1_000
|
|
49
49
|
const SHORT_ID_LENGTH = 8
|
|
50
50
|
|
|
51
|
-
/** How many parents `isAncestor` follows before it stops. */
|
|
52
|
-
const MAX_ANCESTORS = 64
|
|
53
|
-
|
|
54
51
|
/** A terminal control sequence (a colour code: ESC [ parameters final byte). Built from the ESC
|
|
55
52
|
* code, since a control character in a regular expression literal is a lint error. */
|
|
56
53
|
const CSI_SEQUENCE = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-?]*[ -/]*[@-~]`, "gu")
|
|
@@ -80,39 +77,6 @@ function visible(value: string): boolean {
|
|
|
80
77
|
)
|
|
81
78
|
}
|
|
82
79
|
|
|
83
|
-
/** The parent of process `pid`, or undefined when it cannot be read. */
|
|
84
|
-
function parentPid(pid: number): number | undefined {
|
|
85
|
-
try {
|
|
86
|
-
if (process.platform === "linux") {
|
|
87
|
-
// The command name, in parentheses, may hold spaces; the parent is the second field after it.
|
|
88
|
-
const stat = readFileSync(`/proc/${pid}/stat`, "utf8")
|
|
89
|
-
const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1])
|
|
90
|
-
return Number.isInteger(parent) ? parent : undefined
|
|
91
|
-
}
|
|
92
|
-
const parent = Number(
|
|
93
|
-
execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
|
|
94
|
-
encoding: "utf8",
|
|
95
|
-
timeout: TMUX_TIMEOUT_MS,
|
|
96
|
-
killSignal: "SIGKILL",
|
|
97
|
-
}).trim(),
|
|
98
|
-
)
|
|
99
|
-
return Number.isInteger(parent) ? parent : undefined
|
|
100
|
-
} catch {
|
|
101
|
-
return undefined
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Whether process `candidate` is `pid` or one of its ancestors, walking up from `pid` and
|
|
106
|
-
* stopping at the first match, at init, or after `MAX_ANCESTORS` steps. */
|
|
107
|
-
export function isAncestor(candidate: string, pid: number = process.pid): boolean {
|
|
108
|
-
let current: number | undefined = pid
|
|
109
|
-
for (let step = 0; current !== undefined && current > 1 && step < MAX_ANCESTORS; step++) {
|
|
110
|
-
if (String(current) === candidate) return true
|
|
111
|
-
current = parentPid(current)
|
|
112
|
-
}
|
|
113
|
-
return false
|
|
114
|
-
}
|
|
115
|
-
|
|
116
80
|
/**
|
|
117
81
|
* `tmux -u -S <socket> display-message -p -t <pane> <format>`; undefined when tmux is missing, slow
|
|
118
82
|
* or refuses, or prints nothing: a running tmux server answers a pane it does not have with an
|
|
@@ -232,10 +196,7 @@ export async function sessionTitle(
|
|
|
232
196
|
// A title with nothing that shows once cleaned (a zero-width space, a colour code alone) would
|
|
233
197
|
// be a blank row.
|
|
234
198
|
if (visible(explicit)) return () => explicit
|
|
235
|
-
|
|
236
|
-
const parts = configuredValue(environment.TMUX)?.split(",") ?? []
|
|
237
|
-
const socket = parts.slice(0, -2).join(",")
|
|
238
|
-
const serverPid = parts.at(-2) ?? ""
|
|
199
|
+
const { serverPid, socket } = tmuxServer(configuredValue(environment.TMUX))
|
|
239
200
|
const pane = configuredValue(environment.TMUX_PANE)?.trim()
|
|
240
201
|
// A directory name holds whatever the filesystem allows, newlines and overrides among them.
|
|
241
202
|
const directoryName = oneLine(basename(directory) || directory)
|