claude-yes 1.156.0 → 1.158.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.
@@ -0,0 +1,8 @@
1
+ import "./ts-IAAUoBpk.js";
2
+ import "./logger-CDIsZ-Pp.js";
3
+ import "./versionChecker-Dwe07Fe6.js";
4
+ import "./pidStore-fqXqTKkh.js";
5
+ import "./globalPidIndex-DlmmJlO8.js";
6
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CeyKUwT5.js";
7
+
8
+ export { SUPPORTED_CLIS };
@@ -1,8 +1,8 @@
1
- import { t as CLIS_CONFIG } from "./ts-CLHiUE0V.js";
1
+ import { t as CLIS_CONFIG } from "./ts-IAAUoBpk.js";
2
2
 
3
3
  //#region ts/SUPPORTED_CLIS.ts
4
4
  const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
5
5
 
6
6
  //#endregion
7
7
  export { SUPPORTED_CLIS as t };
8
- //# sourceMappingURL=SUPPORTED_CLIS-BrOIMMzT.js.map
8
+ //# sourceMappingURL=SUPPORTED_CLIS-CeyKUwT5.js.map
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
3
3
  import { n as logger } from "./logger-CDIsZ-Pp.js";
4
- import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-C4EICUab.js";
4
+ import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-Dwe07Fe6.js";
5
5
  import { argv } from "process";
6
6
  import { execFileSync, spawn } from "child_process";
7
7
  import ms from "ms";
@@ -481,7 +481,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
481
481
  const rawArg = process.argv[2];
482
482
  const managerCommands = !invokedCliName(process.argv);
483
483
  const isHelpFlag = rawArg === "-h" || rawArg === "--help";
484
- const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-bpbzbGmp.js");
484
+ const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-CyKom1IU.js");
485
485
  if (isHelpFlag && process.argv.length === 3) {
486
486
  cmdHelp(managerCommands);
487
487
  process.exit(0);
@@ -514,7 +514,7 @@ if (config.useRust) {
514
514
  }
515
515
  }
516
516
  if (rustBinary) {
517
- const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-DmRDiLen.js");
517
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-BSuNAuIv.js");
518
518
  const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
519
519
  if (config.verbose) {
520
520
  console.log(`[rust] Using binary: ${rustBinary}`);
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-CLHiUE0V.js";
1
+ import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-IAAUoBpk.js";
2
2
  import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-C4EICUab.js";
3
+ import "./versionChecker-Dwe07Fe6.js";
4
4
  import "./pidStore-fqXqTKkh.js";
5
5
  import "./globalPidIndex-DlmmJlO8.js";
6
6
 
@@ -1,10 +1,10 @@
1
- import "./ts-CLHiUE0V.js";
1
+ import "./ts-IAAUoBpk.js";
2
2
  import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-C4EICUab.js";
3
+ import "./versionChecker-Dwe07Fe6.js";
4
4
  import "./pidStore-fqXqTKkh.js";
5
5
  import "./globalPidIndex-DlmmJlO8.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BrOIMMzT.js";
7
- import { i as resolveSpawnCwd } from "./workspaceConfig-DT3cCBZK.js";
6
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CeyKUwT5.js";
7
+ import { o as resolveSpawnCwd } from "./workspaceConfig-B0Q9-q2B.js";
8
8
  import { createHash } from "node:crypto";
9
9
 
10
10
  //#region ts/oxmgrService.ts
@@ -141,4 +141,4 @@ async function cmdSchedule(rest) {
141
141
 
142
142
  //#endregion
143
143
  export { cmdSchedule };
144
- //# sourceMappingURL=schedule-DLMYKfDJ.js.map
144
+ //# sourceMappingURL=schedule-BVKN6cO5.js.map
@@ -1,18 +1,18 @@
1
- import "./ts-CLHiUE0V.js";
1
+ import "./ts-IAAUoBpk.js";
2
2
  import "./logger-CDIsZ-Pp.js";
3
- import { r as getInstalledPackage } from "./versionChecker-C4EICUab.js";
3
+ import { r as getInstalledPackage } from "./versionChecker-Dwe07Fe6.js";
4
4
  import "./pidStore-fqXqTKkh.js";
5
5
  import { a as updateGlobalPidStatus } from "./globalPidIndex-DlmmJlO8.js";
6
6
  import { t as pgidForWrapper } from "./reaper-C-eWAxIj.js";
7
7
  import "./configShared-CEyhl0WH.js";
8
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BrOIMMzT.js";
8
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CeyKUwT5.js";
9
9
  import "./e2e-Bfw7qL9O.js";
10
10
  import "./webrtcLink-CBZkZ-LT.js";
11
11
  import "./remotes-DBCvpp3B.js";
12
- import { i as resolveSpawnCwd, r as isProvisionAllowed, t as getProvisionRoot } from "./workspaceConfig-DT3cCBZK.js";
13
- import { E as writeToIpc, _ as readPtysize, b as resolveOne, c as extractTaskCounts, g as readNotes, i as controlCodeFromName, m as listRecords, o as deriveLiveStatus, v as renderRawLog, w as snapshotStatus } from "./subcommands-DTWNUJAa.js";
12
+ import { a as isProvisionAllowed, i as hasSpawnHook, n as getSpawnHook, o as resolveSpawnCwd, t as getProvisionRoot } from "./workspaceConfig-B0Q9-q2B.js";
13
+ import { E as writeToIpc, _ as readPtysize, b as resolveOne, c as extractTaskCounts, g as readNotes, i as controlCodeFromName, m as listRecords, o as deriveLiveStatus, v as renderRawLog, w as snapshotStatus } from "./subcommands-COLw05N-.js";
14
14
  import yargs from "yargs";
15
- import { mkdir, open, readFile, stat, writeFile } from "fs/promises";
15
+ import { mkdir, open, readFile, stat, unlink, writeFile } from "fs/promises";
16
16
  import { homedir, hostname, userInfo } from "os";
17
17
  import path from "path";
18
18
  import { execFileSync } from "node:child_process";
@@ -789,6 +789,7 @@ Options:
789
789
  return Response.json({ host: user ? `${user}@${host}` : host });
790
790
  }
791
791
  if (req.method === "GET" && p === "/api/version") return Response.json({ version: getInstalledPackage().version });
792
+ if (req.method === "GET" && p === "/api/spawn-config") return Response.json({ hasSpawnHook: hasSpawnHook() });
792
793
  if (req.method === "GET" && p === "/api/notes") {
793
794
  const notes = await readNotes();
794
795
  return Response.json(Object.fromEntries(notes));
@@ -1154,15 +1155,59 @@ Options:
1154
1155
  }
1155
1156
  process.stderr.write(`→ console spawned: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""} (cwd: ${cwd}${provisioned ? `, ${provisioned.action}` : ""})\n`);
1156
1157
  const ayBin = Bun.which("ay") ?? process.argv[1];
1157
- const ayCmd = process.platform === "win32" && ayBin.toLowerCase().endsWith(".exe") ? [ayBin] : [process.execPath, ayBin];
1158
+ const agentArgv = [
1159
+ ...process.platform === "win32" && ayBin.toLowerCase().endsWith(".exe") ? [ayBin] : [process.execPath, ayBin],
1160
+ cli,
1161
+ ...prompt ? ["--", prompt] : []
1162
+ ];
1163
+ const agentEnv = freshAgentEnv();
1158
1164
  try {
1159
- const child = Bun.spawn([
1160
- ...ayCmd,
1161
- cli,
1162
- ...prompt ? ["--", prompt] : []
1163
- ], {
1165
+ const hook = getSpawnHook();
1166
+ if (hook) {
1167
+ const shell = process.env.AGENT_YES_SPAWN_SHELL?.trim() || "/bin/sh";
1168
+ const script = `set -e\n${hook}\nexec "$@"`;
1169
+ const errPath = path.join(agentYesHome(), `spawn-hook-${process.pid}-${performance.now().toString(36).replace(".", "")}.err`);
1170
+ const child = Bun.spawn([
1171
+ shell,
1172
+ "-c",
1173
+ script,
1174
+ "ay-spawn",
1175
+ ...agentArgv
1176
+ ], {
1177
+ cwd,
1178
+ env: {
1179
+ ...agentEnv,
1180
+ AGENT_YES_CWD: cwd,
1181
+ AGENT_YES_CLI: cli
1182
+ },
1183
+ stdin: "ignore",
1184
+ stdout: "ignore",
1185
+ stderr: Bun.file(errPath)
1186
+ });
1187
+ const windowMs = Number(process.env.AGENT_YES_SPAWN_HOOK_TIMEOUT_MS) || 5e3;
1188
+ const exitCode = await Promise.race([child.exited, new Promise((r) => setTimeout(() => r(null), windowMs))]);
1189
+ if (exitCode !== null && exitCode !== 0) {
1190
+ let detail = "";
1191
+ try {
1192
+ detail = (await Bun.file(errPath).text()).slice(0, 4096);
1193
+ } catch {}
1194
+ await unlink(errPath).catch(() => {});
1195
+ return new Response(`spawn hook failed (exit ${exitCode})${detail ? `:\n${detail.trimEnd()}` : ""}`, { status: 502 });
1196
+ }
1197
+ child.unref();
1198
+ setTimeout(() => void unlink(errPath).catch(() => {}), 6e4).unref?.();
1199
+ return Response.json({
1200
+ ok: true,
1201
+ pid: child.pid,
1202
+ cli,
1203
+ cwd,
1204
+ hook: true,
1205
+ ...provisioned ? { provisioned } : {}
1206
+ });
1207
+ }
1208
+ const child = Bun.spawn(agentArgv, {
1164
1209
  cwd,
1165
- env: freshAgentEnv(),
1210
+ env: agentEnv,
1166
1211
  stdin: "ignore",
1167
1212
  stdout: "ignore",
1168
1213
  stderr: "ignore"
@@ -1303,4 +1348,4 @@ Options:
1303
1348
 
1304
1349
  //#endregion
1305
1350
  export { cmdServe };
1306
- //# sourceMappingURL=serve-BXUK9_Ex.js.map
1351
+ //# sourceMappingURL=serve-Dij1y--H.js.map
@@ -1,4 +1,4 @@
1
- import { a as setWorkspaceRoot, n as getWorkspaceRoot } from "./workspaceConfig-DT3cCBZK.js";
1
+ import { r as getWorkspaceRoot, s as setWorkspaceRoot } from "./workspaceConfig-B0Q9-q2B.js";
2
2
  import { existsSync } from "node:fs";
3
3
  import { stdin, stdout } from "node:process";
4
4
  import { createInterface } from "node:readline/promises";
@@ -32,7 +32,7 @@ async function cmdSetup(rest) {
32
32
  if (!existsSync(abs)) process.stderr.write(` note: that directory doesn't exist yet — create it, or agents spawned there will fail\n`);
33
33
  if (noShare) return 0;
34
34
  process.stdout.write(`\nsharing this machine to agent-yes.com…\n`);
35
- const { cmdServe } = await import("./serve-BXUK9_Ex.js");
35
+ const { cmdServe } = await import("./serve-Dij1y--H.js");
36
36
  return cmdServe([
37
37
  "install",
38
38
  "--share",
@@ -42,4 +42,4 @@ async function cmdSetup(rest) {
42
42
 
43
43
  //#endregion
44
44
  export { cmdSetup };
45
- //# sourceMappingURL=setup-BXIWWZAP.js.map
45
+ //# sourceMappingURL=setup-DymiMHke.js.map
@@ -554,15 +554,15 @@ async function runSubcommand(argv) {
554
554
  case "restart": return await cmdRestart(rest);
555
555
  case "note": return await cmdNote(rest);
556
556
  case "serve": {
557
- const { cmdServe } = await import("./serve-BXUK9_Ex.js");
557
+ const { cmdServe } = await import("./serve-Dij1y--H.js");
558
558
  return cmdServe(rest);
559
559
  }
560
560
  case "setup": {
561
- const { cmdSetup } = await import("./setup-BXIWWZAP.js");
561
+ const { cmdSetup } = await import("./setup-DymiMHke.js");
562
562
  return cmdSetup(rest);
563
563
  }
564
564
  case "schedule": {
565
- const { cmdSchedule } = await import("./schedule-DLMYKfDJ.js");
565
+ const { cmdSchedule } = await import("./schedule-BVKN6cO5.js");
566
566
  return cmdSchedule(rest);
567
567
  }
568
568
  case "remote": {
@@ -2710,4 +2710,4 @@ async function cmdResultSet(rest) {
2710
2710
 
2711
2711
  //#endregion
2712
2712
  export { runSubcommand as C, writeToIpc as E, resolveResumeArgs as S, stopTipForCli as T, readPtysize as _, cursorAbs as a, resolveOne as b, extractTaskCounts as c, isExitRequest as d, isPidAlive as f, readNotes as g, matchKeyword as h, controlCodeFromName as i, finalizedLines as l, listRecords as m, READ_PAGE_DEFAULT as n, deriveLiveStatus as o, isSubcommand as p, cmdHelp as r, extractNeedsInput as s, GRACEFUL_EXIT_COMMANDS as t, isAgentStuck as u, renderRawLog as v, snapshotStatus as w, resolveReadWindow as x, renderRawLogLines as y };
2713
- //# sourceMappingURL=subcommands-DTWNUJAa.js.map
2713
+ //# sourceMappingURL=subcommands-COLw05N-.js.map
@@ -4,6 +4,6 @@ import "./configShared-CEyhl0WH.js";
4
4
  import "./e2e-Bfw7qL9O.js";
5
5
  import "./webrtcLink-CBZkZ-LT.js";
6
6
  import "./remotes-DBCvpp3B.js";
7
- import { C as runSubcommand, E as writeToIpc, S as resolveResumeArgs, T as stopTipForCli, _ as readPtysize, a as cursorAbs, b as resolveOne, c as extractTaskCounts, d as isExitRequest, f as isPidAlive, g as readNotes, h as matchKeyword, i as controlCodeFromName, l as finalizedLines, m as listRecords, n as READ_PAGE_DEFAULT, o as deriveLiveStatus, p as isSubcommand, r as cmdHelp, s as extractNeedsInput, t as GRACEFUL_EXIT_COMMANDS, u as isAgentStuck, v as renderRawLog, w as snapshotStatus, x as resolveReadWindow, y as renderRawLogLines } from "./subcommands-DTWNUJAa.js";
7
+ import { C as runSubcommand, E as writeToIpc, S as resolveResumeArgs, T as stopTipForCli, _ as readPtysize, a as cursorAbs, b as resolveOne, c as extractTaskCounts, d as isExitRequest, f as isPidAlive, g as readNotes, h as matchKeyword, i as controlCodeFromName, l as finalizedLines, m as listRecords, n as READ_PAGE_DEFAULT, o as deriveLiveStatus, p as isSubcommand, r as cmdHelp, s as extractNeedsInput, t as GRACEFUL_EXIT_COMMANDS, u as isAgentStuck, v as renderRawLog, w as snapshotStatus, x as resolveReadWindow, y as renderRawLogLines } from "./subcommands-COLw05N-.js";
8
8
 
9
9
  export { cmdHelp, isSubcommand, runSubcommand };
@@ -1,5 +1,5 @@
1
1
  import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
2
- import { r as getInstalledPackage } from "./versionChecker-C4EICUab.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-Dwe07Fe6.js";
3
3
  import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
4
4
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-V4qvXgAw.js";
5
5
  import { t as PidStore } from "./pidStore-fqXqTKkh.js";
@@ -1802,4 +1802,4 @@ function sleep(ms) {
1802
1802
 
1803
1803
  //#endregion
1804
1804
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1805
- //# sourceMappingURL=ts-CLHiUE0V.js.map
1805
+ //# sourceMappingURL=ts-IAAUoBpk.js.map
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
7
7
 
8
8
  //#region package.json
9
9
  var name = "claude-yes";
10
- var version = "1.156.0";
10
+ var version = "1.158.0";
11
11
 
12
12
  //#endregion
13
13
  //#region ts/versionChecker.ts
@@ -215,4 +215,4 @@ async function displayVersion() {
215
215
 
216
216
  //#endregion
217
217
  export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
218
- //# sourceMappingURL=versionChecker-C4EICUab.js.map
218
+ //# sourceMappingURL=versionChecker-Dwe07Fe6.js.map
@@ -1,5 +1,5 @@
1
1
  import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
2
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { lstatSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
3
3
  import { homedir } from "os";
4
4
  import path from "path";
5
5
 
@@ -61,6 +61,41 @@ function isProvisionAllowed(owner, repo) {
61
61
  const full = `${owner}/${repo}`.toLowerCase();
62
62
  return list.some((e) => e.replace(/\/\*$/, "") === o || e === full);
63
63
  }
64
+ /**
65
+ * A trusted, HOST-LOCAL shell hook run before each agent spawn. It is a POSIX
66
+ * `sh -c` script that prepares the environment (provisioning, env, cd) and that
67
+ * agent-yes runs as `sh -c "set -e\n<hook>\nexec \"$@\"" ay-spawn <agent argv…>`
68
+ * — so the real agent argv is passed as positional params and the prompt is
69
+ * never shell-parsed. `set -e` aborts the spawn if any hook step fails.
70
+ *
71
+ * This is arbitrary local code that runs on EVERY spawn, so it is deliberately
72
+ * NOT writable over the network. Set it on the machine by editing
73
+ * `~/.agent-yes/config.json` (`"spawnHook"`), or via env `AGENT_YES_SPAWN_HOOK`.
74
+ *
75
+ * Tampering guard (POSIX): a file-backed hook is ignored when the config file is
76
+ * a symlink, is not owned by us, or is group/world-writable — we refuse to run a
77
+ * hook from a file other users can swap or rewrite. The env override is trusted
78
+ * (it comes from the daemon's own environment). Returns null when unset/guarded.
79
+ */
80
+ function getSpawnHook() {
81
+ const env = process.env.AGENT_YES_SPAWN_HOOK;
82
+ if (env && env.trim()) return env;
83
+ const h = readConfig().spawnHook;
84
+ if (!h || !h.trim()) return null;
85
+ if (process.platform !== "win32") try {
86
+ if (lstatSync(configPath()).isSymbolicLink()) return null;
87
+ const st = statSync(configPath());
88
+ if ((st.mode & 18) !== 0) return null;
89
+ if (typeof process.getuid === "function" && st.uid !== process.getuid()) return null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ return h;
94
+ }
95
+ /** Whether a usable {@link getSpawnHook} is configured (for read-only disclosure). */
96
+ function hasSpawnHook() {
97
+ return getSpawnHook() !== null;
98
+ }
64
99
  /** Persist the workspace root, tilde-expanded and resolved to an absolute path. */
65
100
  function setWorkspaceRoot(dir) {
66
101
  const abs = path.resolve(expandTilde(dir));
@@ -87,5 +122,5 @@ function resolveSpawnCwd(input) {
87
122
  }
88
123
 
89
124
  //#endregion
90
- export { setWorkspaceRoot as a, resolveSpawnCwd as i, getWorkspaceRoot as n, isProvisionAllowed as r, getProvisionRoot as t };
91
- //# sourceMappingURL=workspaceConfig-DT3cCBZK.js.map
125
+ export { isProvisionAllowed as a, hasSpawnHook as i, getSpawnHook as n, resolveSpawnCwd as o, getWorkspaceRoot as r, setWorkspaceRoot as s, getProvisionRoot as t };
126
+ //# sourceMappingURL=workspaceConfig-B0Q9-q2B.js.map
@@ -305,6 +305,54 @@ function agentForestNodes(list) {
305
305
  return roots;
306
306
  }
307
307
 
308
+ // ---- display sort order -----------------------------------------------------
309
+ // The console cycles a button through these; the chosen comparator runs over the
310
+ // flat entry list BEFORE layeredRows builds the room/peer/agent tree (which keeps
311
+ // the given order for siblings and first-seen order for room/peer groups). So
312
+ // sorting reorders roots and siblings without breaking the nesting.
313
+ export const SORT_MODES = ["state", "created", "identity"];
314
+
315
+ // Attention-first state ranking: someone scanning the fleet wants the agents that
316
+ // need them (needs_input) up top, then the wedged ones (stuck), then live work,
317
+ // then quiet/idle, then finished. Unknown states sort last.
318
+ const STATE_RANK = {
319
+ needs_input: 0,
320
+ stuck: 1,
321
+ active: 2,
322
+ running: 2,
323
+ idle: 3,
324
+ stopped: 4,
325
+ exited: 4,
326
+ };
327
+ function stateRank(e) {
328
+ const r = STATE_RANK[e.status];
329
+ return r === undefined ? 5 : r;
330
+ }
331
+
332
+ // Git "busyness" for the state mode's secondary key: a dirty / ahead / behind
333
+ // repo outranks a clean one, and more outstanding changes rank higher. The +0.5
334
+ // for `dirty` breaks a 0-count tie toward the dirty tree.
335
+ function gitWeight(e) {
336
+ const g = e.git || {};
337
+ return (g.changed || 0) + (g.ahead || 0) + (g.behind || 0) + (g.dirty ? 0.5 : 0);
338
+ }
339
+
340
+ // Return a NEW array sorted for display per `mode` (default "state"):
341
+ // - "state": attention-first state, then git busyness, then newest.
342
+ // - "created": newest first (started_at desc).
343
+ // - "identity": user@host:owner/repo/branch (alphabetical).
344
+ // Every comparator falls back to newest-first so order is total & deterministic.
345
+ export function sortEntries(entries, mode = "state") {
346
+ const byNewest = (a, b) => (b.started_at || 0) - (a.started_at || 0);
347
+ const cmp =
348
+ mode === "created"
349
+ ? byNewest
350
+ : mode === "identity"
351
+ ? (a, b) => fullIdent(a).localeCompare(fullIdent(b)) || byNewest(a, b)
352
+ : (a, b) => stateRank(a) - stateRank(b) || gitWeight(b) - gitWeight(a) || byNewest(a, b);
353
+ return entries.slice().sort(cmp);
354
+ }
355
+
308
356
  // Order entries as agent>subagent forests so a nested `ay` (one agent spawning
309
357
  // another) renders indented under its parent. SCOPED PER HOST. Returns a NEW
310
358
  // array in depth-first order; each entry is shallow-copied with `_branch` (a
package/lab/ui/index.html CHANGED
@@ -838,6 +838,11 @@
838
838
  .dot.stuck {
839
839
  background: var(--red);
840
840
  }
841
+ /* Waiting on you: alive and quiet, but parked on an unanswered prompt/menu.
842
+ The UI accent (blue) reads as "your turn" — distinct from idle-amber. */
843
+ .dot.needs_input {
844
+ background: var(--accent);
845
+ }
841
846
  .dot.stopped,
842
847
  .dot.exited {
843
848
  background: var(--muted);
@@ -1346,6 +1351,7 @@
1346
1351
  <div class="meta">
1347
1352
  <span id="count"></span>
1348
1353
  <span class="metaright">
1354
+ <button id="sortbtn" class="viewbtn" title="cycle sort order">⇅ state</button>
1349
1355
  <button id="viewbtn" class="viewbtn" title="toggle compact list">☰</button>
1350
1356
  <button id="newbtn" class="newbtn" title="spawn a new agent on this fleet">
1351
1357
  + New agent
@@ -1529,6 +1535,8 @@
1529
1535
  hasIdent,
1530
1536
  deviceCount,
1531
1537
  layeredRows,
1538
+ sortEntries,
1539
+ SORT_MODES,
1532
1540
  taskLabel,
1533
1541
  hashHue,
1534
1542
  selFromBottom,
@@ -3155,6 +3163,22 @@
3155
3163
  // Compact list: one line per agent (dot + cli + title), persisted per device.
3156
3164
  let compactList = localStorage.getItem("ay.compactList") === "1";
3157
3165
 
3166
+ // Display sort order, cycled by the sort button and persisted per device.
3167
+ // Default "state" (attention-first state, then git busyness). Guard against
3168
+ // a stale/garbage stored value so we never start in an unknown mode.
3169
+ let sortMode = localStorage.getItem("ay.sortMode") || "state";
3170
+ if (!SORT_MODES.includes(sortMode)) sortMode = "state";
3171
+
3172
+ // The filtered + sorted entry list, in the SAME order the left panel renders
3173
+ // and the keyboard nav steps. Sorting runs before layeredRows builds the
3174
+ // room/peer/agent tree, so it reorders roots/siblings without breaking nesting.
3175
+ function orderedEntries(toks) {
3176
+ return sortEntries(
3177
+ entries.filter((e) => matches(e, toks)),
3178
+ sortMode,
3179
+ );
3180
+ }
3181
+
3158
3182
  // A small git chip (±changed ↑ahead ↓behind) for a row, amber when the tree
3159
3183
  // is dirty. Empty for clean/in-sync repos and non-git cwds — no noise.
3160
3184
  function gitChipHtml(e) {
@@ -3195,10 +3219,14 @@
3195
3219
  const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
3196
3220
  // Build the full signalling-server > rooms > peers > agents > subagents
3197
3221
  // tree. Single-node room/peer layers fold away; ≥2 become ├ └ │ branches.
3198
- const rows = layeredRows(entries.filter((e) => matches(e, toks)));
3222
+ const rows = layeredRows(orderedEntries(toks));
3199
3223
  const agentRows = rows.filter((r) => r.kind === "agent");
3200
3224
  $("count").textContent = `${agentRows.length} / ${entries.length} agents`;
3201
3225
  $("viewbtn").classList.toggle("on", compactList);
3226
+ // Show the active sort mode on the button so it's self-documenting; the
3227
+ // default ("state") is also highlighted to hint it's the implicit order.
3228
+ $("sortbtn").textContent = `⇅ ${sortMode}`;
3229
+ $("sortbtn").classList.toggle("on", sortMode !== "state");
3202
3230
  // identContext blanks any field uniform across the shown list (so a
3203
3231
  // single-device fleet shows no device); multiDevice gates the detailed
3204
3232
  // host tag so it only appears when machines are actually mixed.
@@ -3658,6 +3686,12 @@
3658
3686
  localStorage.setItem("ay.compactList", compactList ? "1" : "0");
3659
3687
  renderList();
3660
3688
  });
3689
+ // Cycle the sort order: state → created → identity → state.
3690
+ $("sortbtn").addEventListener("click", () => {
3691
+ sortMode = SORT_MODES[(SORT_MODES.indexOf(sortMode) + 1) % SORT_MODES.length];
3692
+ localStorage.setItem("ay.sortMode", sortMode);
3693
+ renderList();
3694
+ });
3661
3695
  window.addEventListener("resize", () => {
3662
3696
  // Resizing our window is strong intent → re-fit to our pane (we become the
3663
3697
  // size driver; the push rides term.onResize). applyCanvasScale then clears
@@ -3695,7 +3729,7 @@
3695
3729
  function stepSelection(dir) {
3696
3730
  const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
3697
3731
  // Must match renderList's order; step only over agent rows (skip headers).
3698
- const shown = layeredRows(entries.filter((e) => matches(e, toks)))
3732
+ const shown = layeredRows(orderedEntries(toks))
3699
3733
  .filter((r) => r.kind === "agent")
3700
3734
  .map((r) => r.entry);
3701
3735
  if (!shown.length) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-yes",
3
- "version": "1.156.0",
3
+ "version": "1.158.0",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
package/ts/serve.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, open, readFile, stat, writeFile } from "fs/promises";
1
+ import { mkdir, open, readFile, stat, unlink, writeFile } from "fs/promises";
2
2
  import { renameSync, watch, writeFileSync } from "node:fs";
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
@@ -23,7 +23,13 @@ import { updateGlobalPidStatus } from "./globalPidIndex.ts";
23
23
  import { pgidForWrapper } from "./reaper.ts";
24
24
  import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
25
25
  import { getInstalledPackage } from "./versionChecker.ts";
26
- import { getProvisionRoot, isProvisionAllowed, resolveSpawnCwd } from "./workspaceConfig.ts";
26
+ import {
27
+ getProvisionRoot,
28
+ getSpawnHook,
29
+ hasSpawnHook,
30
+ isProvisionAllowed,
31
+ resolveSpawnCwd,
32
+ } from "./workspaceConfig.ts";
27
33
 
28
34
  const DEFAULT_PORT = 7432;
29
35
 
@@ -1157,6 +1163,15 @@ export async function cmdServe(rest: string[]): Promise<number> {
1157
1163
  return Response.json({ version: getInstalledPackage().version });
1158
1164
  }
1159
1165
 
1166
+ // GET /api/spawn-config — read-only disclosure of WHETHER a host-local spawn
1167
+ // hook is configured. Deliberately returns only a boolean, never the hook
1168
+ // body: the hook is arbitrary local code and console/room clients holding the
1169
+ // share token are not necessarily host admins. Setting it stays host-local
1170
+ // (edit ~/.agent-yes/config.json) — there is no PUT.
1171
+ if (req.method === "GET" && p === "/api/spawn-config") {
1172
+ return Response.json({ hasSpawnHook: hasSpawnHook() });
1173
+ }
1174
+
1160
1175
  // GET /api/notes
1161
1176
  if (req.method === "GET" && p === "/api/notes") {
1162
1177
  const notes = await readNotes();
@@ -1685,10 +1700,75 @@ export async function cmdServe(rest: string[]): Promise<number> {
1685
1700
  process.platform === "win32" && ayBin.toLowerCase().endsWith(".exe")
1686
1701
  ? [ayBin]
1687
1702
  : [process.execPath, ayBin];
1703
+ const agentArgv = [...ayCmd, cli, ...(prompt ? ["--", prompt] : [])];
1704
+ // don't leak our Claude Code session into the agent
1705
+ const agentEnv = freshAgentEnv();
1688
1706
  try {
1689
- const child = Bun.spawn([...ayCmd, cli, ...(prompt ? ["--", prompt] : [])], {
1707
+ const hook = getSpawnHook();
1708
+ if (hook) {
1709
+ // Host-local spawn hook (trusted local code, never network-writable). We
1710
+ // run it via POSIX `sh -c` then `exec "$@"` the real agent — the agent
1711
+ // argv is passed as positional params ($1…), so the prompt is NEVER
1712
+ // shell-parsed (no quoting/injection surface). `set -e` aborts the spawn
1713
+ // if any hook step fails. No `-l`: we don't re-source rc files here (the
1714
+ // env is already the recovered login-shell env via freshAgentEnv). cwd
1715
+ // and cli are exposed as env for the hook to consume.
1716
+ const shell = process.env.AGENT_YES_SPAWN_SHELL?.trim() || "/bin/sh";
1717
+ const script = `set -e\n${hook}\nexec "$@"`;
1718
+ const errPath = path.join(
1719
+ agentYesHome(),
1720
+ `spawn-hook-${process.pid}-${performance.now().toString(36).replace(".", "")}.err`,
1721
+ );
1722
+ // File-backed stderr never blocks the child (a pipe we stop draining
1723
+ // after exec would), and bounds our read to the first few KB.
1724
+ const child = Bun.spawn([shell, "-c", script, "ay-spawn", ...agentArgv], {
1725
+ cwd,
1726
+ env: { ...agentEnv, AGENT_YES_CWD: cwd, AGENT_YES_CLI: cli },
1727
+ stdin: "ignore",
1728
+ stdout: "ignore",
1729
+ stderr: Bun.file(errPath),
1730
+ });
1731
+ // Handshake: surface an EARLY hook/provision failure synchronously. If
1732
+ // the child exits non-zero within the window, the hook failed before the
1733
+ // agent settled — return the captured stderr. If it's still alive after
1734
+ // the window, `exec` succeeded (or the agent is running) → detach and
1735
+ // report success. We can't always attribute a failure to the hook vs the
1736
+ // agent, so we just surface the captured output.
1737
+ const windowMs = Number(process.env.AGENT_YES_SPAWN_HOOK_TIMEOUT_MS) || 5000;
1738
+ const exitCode = await Promise.race([
1739
+ child.exited,
1740
+ new Promise<null>((r) => setTimeout(() => r(null), windowMs)),
1741
+ ]);
1742
+ if (exitCode !== null && exitCode !== 0) {
1743
+ let detail = "";
1744
+ try {
1745
+ detail = (await Bun.file(errPath).text()).slice(0, 4096);
1746
+ } catch {
1747
+ /* no stderr captured */
1748
+ }
1749
+ await unlink(errPath).catch(() => {});
1750
+ return new Response(
1751
+ `spawn hook failed (exit ${exitCode})${detail ? `:\n${detail.trimEnd()}` : ""}`,
1752
+ { status: 502 },
1753
+ );
1754
+ }
1755
+ child.unref();
1756
+ // Clean up the stderr sidecar. On POSIX the running agent may still hold
1757
+ // the fd; unlinking an open file is harmless (the inode lives until
1758
+ // close), so a brief delay is enough to keep early-failure reads valid.
1759
+ setTimeout(() => void unlink(errPath).catch(() => {}), 60_000).unref?.();
1760
+ return Response.json({
1761
+ ok: true,
1762
+ pid: child.pid,
1763
+ cli,
1764
+ cwd,
1765
+ hook: true,
1766
+ ...(provisioned ? { provisioned } : {}),
1767
+ });
1768
+ }
1769
+ const child = Bun.spawn(agentArgv, {
1690
1770
  cwd,
1691
- env: freshAgentEnv(), // don't leak our Claude Code session into the agent
1771
+ env: agentEnv,
1692
1772
  stdin: "ignore",
1693
1773
  stdout: "ignore",
1694
1774
  stderr: "ignore",
@@ -1,12 +1,14 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
- import { mkdtempSync, rmSync, writeFileSync } from "fs";
2
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "fs";
3
3
  import { homedir, tmpdir } from "os";
4
4
  import path from "path";
5
5
  import {
6
6
  expandTilde,
7
7
  getProvisionAllowlist,
8
8
  getProvisionRoot,
9
+ getSpawnHook,
9
10
  getWorkspaceRoot,
11
+ hasSpawnHook,
10
12
  isProvisionAllowed,
11
13
  resolveSpawnCwd,
12
14
  setWorkspaceRoot,
@@ -25,6 +27,7 @@ describe("workspaceConfig", () => {
25
27
  else process.env.AGENT_YES_HOME = original;
26
28
  delete process.env.CODEHOST_WS_ROOT;
27
29
  delete process.env.CODEHOST_PROVISION_ALLOWLIST;
30
+ delete process.env.AGENT_YES_SPAWN_HOOK;
28
31
  rmSync(tmp, { recursive: true, force: true });
29
32
  });
30
33
 
@@ -142,4 +145,42 @@ describe("workspaceConfig", () => {
142
145
  expect(isProvisionAllowed("org", "anything")).toBe(true);
143
146
  });
144
147
  });
148
+
149
+ describe("getSpawnHook / hasSpawnHook", () => {
150
+ const isPosix = process.platform !== "win32";
151
+
152
+ it("is null/false when unset", () => {
153
+ expect(getSpawnHook()).toBeNull();
154
+ expect(hasSpawnHook()).toBe(false);
155
+ });
156
+
157
+ it("returns the configured hook from a private (0600) config", () => {
158
+ writeConfig({ spawnHook: 'echo hi >&2\nexec "$@"' });
159
+ if (isPosix) chmodSync(path.join(tmp, "config.json"), 0o600);
160
+ expect(getSpawnHook()).toBe('echo hi >&2\nexec "$@"');
161
+ expect(hasSpawnHook()).toBe(true);
162
+ });
163
+
164
+ it("ignores a blank hook", () => {
165
+ writeConfig({ spawnHook: " " });
166
+ expect(getSpawnHook()).toBeNull();
167
+ });
168
+
169
+ it("env AGENT_YES_SPAWN_HOOK overrides the config", () => {
170
+ writeConfig({ spawnHook: "from-file" });
171
+ process.env.AGENT_YES_SPAWN_HOOK = "from-env";
172
+ expect(getSpawnHook()).toBe("from-env");
173
+ });
174
+
175
+ it.skipIf(!isPosix)(
176
+ "refuses a file-backed hook when the config is group/world-writable (tampering guard)",
177
+ () => {
178
+ writeConfig({ spawnHook: "echo pwned" });
179
+ chmodSync(path.join(tmp, "config.json"), 0o666);
180
+ expect(getSpawnHook()).toBeNull();
181
+ chmodSync(path.join(tmp, "config.json"), 0o600);
182
+ expect(getSpawnHook()).toBe("echo pwned");
183
+ },
184
+ );
185
+ });
145
186
  });
@@ -1,4 +1,4 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
1
+ import { lstatSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
2
2
  import { homedir } from "os";
3
3
  import path from "path";
4
4
  import { agentYesHome } from "./agentYesHome.ts";
@@ -19,6 +19,12 @@ interface Config {
19
19
  provisionRoot?: string;
20
20
  /** Owners/repos permitted for `from`-provisioning; empty = deny all. */
21
21
  provisionAllowlist?: string[];
22
+ /**
23
+ * A trusted, HOST-LOCAL shell hook run before each console/CLI spawn. See
24
+ * {@link getSpawnHook}. Intentionally NOT settable over the network — it is
25
+ * arbitrary local code that runs on every spawn.
26
+ */
27
+ spawnHook?: string;
22
28
  }
23
29
 
24
30
  function configPath(): string {
@@ -86,6 +92,45 @@ export function isProvisionAllowed(owner: string, repo: string): boolean {
86
92
  return list.some((e) => e.replace(/\/\*$/, "") === o || e === full);
87
93
  }
88
94
 
95
+ /**
96
+ * A trusted, HOST-LOCAL shell hook run before each agent spawn. It is a POSIX
97
+ * `sh -c` script that prepares the environment (provisioning, env, cd) and that
98
+ * agent-yes runs as `sh -c "set -e\n<hook>\nexec \"$@\"" ay-spawn <agent argv…>`
99
+ * — so the real agent argv is passed as positional params and the prompt is
100
+ * never shell-parsed. `set -e` aborts the spawn if any hook step fails.
101
+ *
102
+ * This is arbitrary local code that runs on EVERY spawn, so it is deliberately
103
+ * NOT writable over the network. Set it on the machine by editing
104
+ * `~/.agent-yes/config.json` (`"spawnHook"`), or via env `AGENT_YES_SPAWN_HOOK`.
105
+ *
106
+ * Tampering guard (POSIX): a file-backed hook is ignored when the config file is
107
+ * a symlink, is not owned by us, or is group/world-writable — we refuse to run a
108
+ * hook from a file other users can swap or rewrite. The env override is trusted
109
+ * (it comes from the daemon's own environment). Returns null when unset/guarded.
110
+ */
111
+ export function getSpawnHook(): string | null {
112
+ const env = process.env.AGENT_YES_SPAWN_HOOK;
113
+ if (env && env.trim()) return env;
114
+ const h = readConfig().spawnHook;
115
+ if (!h || !h.trim()) return null;
116
+ if (process.platform !== "win32") {
117
+ try {
118
+ if (lstatSync(configPath()).isSymbolicLink()) return null;
119
+ const st = statSync(configPath());
120
+ if ((st.mode & 0o022) !== 0) return null; // group/world-writable
121
+ if (typeof process.getuid === "function" && st.uid !== process.getuid()) return null;
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+ return h;
127
+ }
128
+
129
+ /** Whether a usable {@link getSpawnHook} is configured (for read-only disclosure). */
130
+ export function hasSpawnHook(): boolean {
131
+ return getSpawnHook() !== null;
132
+ }
133
+
89
134
  /** Persist the workspace root, tilde-expanded and resolved to an absolute path. */
90
135
  export function setWorkspaceRoot(dir: string): string {
91
136
  const abs = path.resolve(expandTilde(dir));
@@ -1,8 +0,0 @@
1
- import "./ts-CLHiUE0V.js";
2
- import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-C4EICUab.js";
4
- import "./pidStore-fqXqTKkh.js";
5
- import "./globalPidIndex-DlmmJlO8.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BrOIMMzT.js";
7
-
8
- export { SUPPORTED_CLIS };