agent-yes 1.153.3 → 1.154.1

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 (36) hide show
  1. package/README.md +1 -0
  2. package/agent-yes.config.schema.json +4 -0
  3. package/default.config.yaml +37 -0
  4. package/dist/{SUPPORTED_CLIS-Bc0HAQER.js → SUPPORTED_CLIS-C91byt6u.js} +2 -2
  5. package/dist/SUPPORTED_CLIS-DbeXJMsk.js +8 -0
  6. package/dist/{agent-yes.config-BKtCmmy9.js → agent-yes.config-Bl08kMpl.js} +2 -2
  7. package/dist/cli.js +5 -5
  8. package/dist/{configShared-C1C04bbq.js → configShared-CEyhl0WH.js} +3 -2
  9. package/dist/{e2e-ClOI_aqV.js → e2e-Bfw7qL9O.js} +1 -1
  10. package/dist/index.js +2 -2
  11. package/dist/openBrowser-CuOlFtow.js +71 -0
  12. package/dist/{remotes-qK6uozO4.js → remotes-DBCvpp3B.js} +3 -3
  13. package/dist/{remotes-5lHLAAqn.js → remotes-DkNuq417.js} +3 -3
  14. package/dist/{schedule-CJ8RFfUT.js → schedule-Ck_R87TW.js} +5 -5
  15. package/dist/{serve-CvMdNHGV.js → serve-BULXUYwQ.js} +25 -18
  16. package/dist/{setup-mzsKEOzd.js → setup-ciRARTh8.js} +3 -3
  17. package/dist/{share-ShLKJTUE.js → share-Bq_tDXQU.js} +2 -2
  18. package/dist/{subcommands-w_qFLMDs.js → subcommands-BBNrlR8y.js} +102 -18
  19. package/dist/subcommands-DLaTMHwq.js +9 -0
  20. package/dist/{tray-5Hezw9uj.js → tray-BMzpUSfa.js} +1 -1
  21. package/dist/{ts-CxC_nYZ4.js → ts-CV9vvB1z.js} +3 -3
  22. package/dist/{versionChecker-D6lNBbCr.js → versionChecker-CedgMmjm.js} +2 -2
  23. package/dist/{webrtcLink-BWhuA74k.js → webrtcLink-CBZkZ-LT.js} +2 -2
  24. package/dist/{webrtcRemote-jGM3ZHK3.js → webrtcRemote-Ccdzmuc-.js} +3 -3
  25. package/dist/{workspaceConfig-B9VX8tVO.js → workspaceConfig-DT3cCBZK.js} +1 -1
  26. package/package.json +1 -1
  27. package/ts/configShared.ts +4 -0
  28. package/ts/globalPidIndex.ts +5 -0
  29. package/ts/index.ts +1 -0
  30. package/ts/openBrowser.spec.ts +130 -0
  31. package/ts/openBrowser.ts +67 -0
  32. package/ts/serve.ts +19 -6
  33. package/ts/subcommands.spec.ts +119 -2
  34. package/ts/subcommands.ts +104 -7
  35. package/dist/SUPPORTED_CLIS-C4swzsq1.js +0 -8
  36. package/dist/subcommands-CVSvhyYN.js +0 -9
@@ -1019,6 +1019,33 @@ describe("subcommands.cmdLs --all / --active / keyword filter / aliases", () =>
1019
1019
  expect(parsed.some((r: any) => r.prompt === "exited agent")).toBe(true);
1020
1020
  });
1021
1021
 
1022
+ it("--json surfaces the Rust unresponsive flag as state 'stuck'", async () => {
1023
+ const mod = await loadModule();
1024
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
1025
+ await appendGlobalPid({
1026
+ pid: process.pid, // alive → not "stopped"
1027
+ cli: "claude",
1028
+ prompt: "wedged-agent-probe",
1029
+ cwd: process.cwd(),
1030
+ log_file: null,
1031
+ status: "active",
1032
+ unresponsive: true,
1033
+ exit_code: null,
1034
+ exit_reason: null,
1035
+ started_at: Date.now(),
1036
+ });
1037
+ const cap = captureOutput();
1038
+ let code: number | null;
1039
+ try {
1040
+ code = await mod.runSubcommand(["bun", "cli.js", "ls", "--json"]);
1041
+ } finally {
1042
+ cap.restore();
1043
+ }
1044
+ expect(code).toBe(0);
1045
+ const row = JSON.parse(cap.stdout).find((r: any) => r.prompt === "wedged-agent-probe");
1046
+ expect(row?.state).toBe("stuck");
1047
+ });
1048
+
1022
1049
  it("keyword filter restricts results to matching agents", async () => {
1023
1050
  const mod = await loadModule();
1024
1051
  const { appendGlobalPid } = await import("./globalPidIndex.ts");
@@ -1458,7 +1485,10 @@ describe("subcommands.cmdStatus", () => {
1458
1485
  // ---------------------------------------------------------------------------
1459
1486
 
1460
1487
  describe("subcommands.cmdRestart", () => {
1461
- it("returns 1 and warns when the agent is still alive", async () => {
1488
+ // A live agent is now stopped-then-resumed. With no fifo_file the graceful
1489
+ // stop can't be sent, so it errors out *before* any kill — which also proves
1490
+ // the live test process (used as the fake pid) is never signalled.
1491
+ it("returns 1 when a live agent can't be gracefully stopped (no fifo_file)", async () => {
1462
1492
  const mod = await loadModule();
1463
1493
  const { appendGlobalPid } = await import("./globalPidIndex.ts");
1464
1494
  await appendGlobalPid({
@@ -1482,10 +1512,13 @@ describe("subcommands.cmdRestart", () => {
1482
1512
  try {
1483
1513
  const code = await mod.runSubcommand(["bun", "cli.js", "restart", String(process.pid)]);
1484
1514
  expect(code).toBe(1);
1485
- expect(stderr.join("")).toMatch(/still running/);
1515
+ expect(stderr.join("")).toMatch(/no fifo_file/);
1486
1516
  } finally {
1487
1517
  process.stderr.write = origErr;
1488
1518
  }
1519
+ // The live process must still be alive — restart must not have killed it.
1520
+ // (signal 0 throws only if the pid is gone / not signalable.)
1521
+ expect(() => process.kill(process.pid, 0)).not.toThrow();
1489
1522
  });
1490
1523
  });
1491
1524
 
@@ -1745,6 +1778,29 @@ describe("subcommands.isAgentStuck / stuck state", () => {
1745
1778
  await rm(dir, { recursive: true, force: true }).catch(() => null);
1746
1779
  }
1747
1780
  });
1781
+
1782
+ it("snapshotStatus: reports 'stuck' when the Rust unresponsive flag is set (no log needed)", async () => {
1783
+ const mod = await loadModule();
1784
+ const snap = await mod.snapshotStatus(rec({ unresponsive: true, log_file: null }));
1785
+ expect(snap.state).toBe("stuck");
1786
+ });
1787
+
1788
+ it("snapshotStatus: the unresponsive flag overrides a quiet (would-be idle) log", async () => {
1789
+ const dir = await mkdtemp(path.join(tmpdir(), "ay-stuck-"));
1790
+ try {
1791
+ const log = path.join(dir, "a.log");
1792
+ // Long-silent, no busy marker → would read as `idle`; the flag forces `stuck`.
1793
+ await writeFile(log, "⏺ Done — all green.\r\n❯\r\n");
1794
+ await utimes(log, tenMinAgo(), tenMinAgo());
1795
+ const mod = await loadModule();
1796
+ expect((await mod.snapshotStatus(rec({ log_file: log }))).state).toBe("idle");
1797
+ expect((await mod.snapshotStatus(rec({ log_file: log, unresponsive: true }))).state).toBe(
1798
+ "stuck",
1799
+ );
1800
+ } finally {
1801
+ await rm(dir, { recursive: true, force: true }).catch(() => null);
1802
+ }
1803
+ });
1748
1804
  });
1749
1805
 
1750
1806
  // A CLI like Claude Code repaints by moving the cursor UP over the previous
@@ -1823,3 +1879,64 @@ describe("subcommands.cmdRead replays at the ptysize sidecar geometry", () => {
1823
1879
  }
1824
1880
  });
1825
1881
  });
1882
+
1883
+ describe("subcommands.resolveResumeArgs", () => {
1884
+ it("replays the original prompt when --fresh", async () => {
1885
+ const { resolveResumeArgs } = await loadModule();
1886
+ expect(
1887
+ resolveResumeArgs({ restoreArgs: ["--continue"] }, "irrelevant log", {
1888
+ fresh: true,
1889
+ prompt: "do the thing",
1890
+ }),
1891
+ ).toEqual({ args: ["do the thing"], strategy: "fresh (replay original prompt)" });
1892
+ });
1893
+
1894
+ it("--fresh with no prompt yields no args", async () => {
1895
+ const { resolveResumeArgs } = await loadModule();
1896
+ expect(resolveResumeArgs(undefined, "", { fresh: true })).toEqual({
1897
+ args: [],
1898
+ strategy: "fresh (no prompt)",
1899
+ });
1900
+ });
1901
+
1902
+ it("scrapes a printed resume command (capture group 1, whitespace-split)", async () => {
1903
+ const { resolveResumeArgs } = await loadModule();
1904
+ const conf = { resumeCommand: /To resume run: agent (.+)/ };
1905
+ const log = "...\nTo resume run: agent --resume abc-123\n> ";
1906
+ expect(resolveResumeArgs(conf, log, { fresh: false })).toEqual({
1907
+ args: ["--resume", "abc-123"],
1908
+ strategy: "printed resume command: --resume abc-123",
1909
+ });
1910
+ });
1911
+
1912
+ it("ignores a stray global flag on resumeCommand and still captures", async () => {
1913
+ const { resolveResumeArgs } = await loadModule();
1914
+ const conf = { resumeCommand: /resume=(\S+)/g };
1915
+ expect(resolveResumeArgs(conf, "session resume=zzz here", { fresh: false }).args).toEqual([
1916
+ "zzz",
1917
+ ]);
1918
+ });
1919
+
1920
+ it("falls back to restoreArgs when resumeCommand is absent or unmatched", async () => {
1921
+ const { resolveResumeArgs } = await loadModule();
1922
+ expect(
1923
+ resolveResumeArgs({ restoreArgs: ["--continue"] }, "no resume hint here", { fresh: false }),
1924
+ ).toEqual({ args: ["--continue"], strategy: "restoreArgs (--continue)" });
1925
+ // resumeCommand present but no match in the log → still falls back.
1926
+ expect(
1927
+ resolveResumeArgs(
1928
+ { resumeCommand: /resume (\S+)/, restoreArgs: ["resume", "--last"] },
1929
+ "nothing matches",
1930
+ { fresh: false },
1931
+ ).args,
1932
+ ).toEqual(["resume", "--last"]);
1933
+ });
1934
+
1935
+ it("falls back to --continue when neither resumeCommand nor restoreArgs exist", async () => {
1936
+ const { resolveResumeArgs } = await loadModule();
1937
+ expect(resolveResumeArgs(undefined, "", { fresh: false })).toEqual({
1938
+ args: ["--continue"],
1939
+ strategy: "--continue (fallback)",
1940
+ });
1941
+ });
1942
+ });
package/ts/subcommands.ts CHANGED
@@ -392,6 +392,7 @@ export function cmdHelp(managerCommands = true): number {
392
392
  ` ay attach <keyword> interactive attach (detach: Ctrl-\\)\n` +
393
393
  ` ay stop <keyword> graceful shutdown (/exit for claude/codex)\n` +
394
394
  ` ay exit <keyword> [reason] graceful shutdown, recording who/why (= 'ay send <kw> exit')\n` +
395
+ ` ay restart <keyword> [--fresh] stop (if live) + relaunch resuming the session; --fresh replays the prompt\n` +
395
396
  ` ay status <keyword> agent status snapshot\n` +
396
397
  ` ay result <keyword> [--wait] pull an agent's structured result envelope\n` +
397
398
  ` ay result set '<json>' (inside an agent) deposit your result envelope\n` +
@@ -999,6 +1000,10 @@ async function deriveLiveState(
999
1000
  ): Promise<{ state: LiveState; question: string | null }> {
1000
1001
  const base = await deriveLiveStatus(r);
1001
1002
  if (base === "exited") return { state: "stopped", question: null };
1003
+ // The Rust supervisor flagged this agent unresponsive (no PTY output after a
1004
+ // poke / a frozen "working" spinner) — an authoritative wedge signal, so it
1005
+ // wins over the log-tail heuristics (needs_input / stuck) below.
1006
+ if (r.unresponsive) return { state: "stuck", question: null };
1002
1007
  // A blocked menu overrides active/idle (alive + quiet, but waiting for an answer).
1003
1008
  if (r.log_file) {
1004
1009
  const ni = await extractNeedsInput(r.log_file, r.cli);
@@ -2754,11 +2759,72 @@ async function cmdAttach(rest: string[]): Promise<number> {
2754
2759
  // ay restart
2755
2760
  // ---------------------------------------------------------------------------
2756
2761
 
2762
+ /**
2763
+ * Decide how to relaunch an agent on `ay restart`. Pure (no I/O) so it's unit
2764
+ * testable. Precedence:
2765
+ * - `fresh`: replay the original prompt (the old behaviour), no resume.
2766
+ * - else if the CLI printed a resume command its `resumeCommand` regex matches
2767
+ * in the captured log (capture group 1 = the arg string), relaunch with those
2768
+ * whitespace-split args.
2769
+ * - else fall back to `restoreArgs` (e.g. `--continue`) so the wrapper's own
2770
+ * resume plumbing (claude --continue, codex stored-session) kicks in.
2771
+ */
2772
+ export function resolveResumeArgs(
2773
+ conf: AgentCliConfig | undefined,
2774
+ logText: string,
2775
+ opts: { fresh: boolean; prompt?: string },
2776
+ ): { args: string[]; strategy: string } {
2777
+ if (opts.fresh) {
2778
+ return opts.prompt
2779
+ ? { args: [opts.prompt], strategy: "fresh (replay original prompt)" }
2780
+ : { args: [], strategy: "fresh (no prompt)" };
2781
+ }
2782
+ const re = conf?.resumeCommand;
2783
+ if (re) {
2784
+ // Strip a stray `g` flag so .exec returns capture groups deterministically.
2785
+ const probe = re.global ? new RegExp(re.source, re.flags.replace(/g/g, "")) : re;
2786
+ const m = probe.exec(logText);
2787
+ const captured = m?.[1]?.trim();
2788
+ if (captured) {
2789
+ const parts = captured.split(/\s+/).filter(Boolean);
2790
+ if (parts.length) return { args: parts, strategy: `printed resume command: ${captured}` };
2791
+ }
2792
+ }
2793
+ const restore = conf?.restoreArgs;
2794
+ if (restore && restore.length) {
2795
+ return { args: [...restore], strategy: `restoreArgs (${restore.join(" ")})` };
2796
+ }
2797
+ return { args: ["--continue"], strategy: "--continue (fallback)" };
2798
+ }
2799
+
2800
+ /**
2801
+ * Wait for a pid to exit. No cross-process exit event exists (the agent is owned
2802
+ * by its own wrapper), so poll `isPidAlive` — checked once immediately, then with
2803
+ * golden-ratio backoff (1.0, 1.6, 2.6…s, capped) up to `timeoutMs`. Returns true
2804
+ * once the pid is gone.
2805
+ */
2806
+ async function waitForExit(pid: number, timeoutMs: number): Promise<boolean> {
2807
+ if (!isPidAlive(pid)) return true;
2808
+ const start = Date.now();
2809
+ let delay = 1000;
2810
+ while (Date.now() - start < timeoutMs) {
2811
+ await new Promise((r) => setTimeout(r, delay));
2812
+ if (!isPidAlive(pid)) return true;
2813
+ delay = Math.min(Math.round(delay * 1.618), 8000);
2814
+ }
2815
+ return !isPidAlive(pid);
2816
+ }
2817
+
2757
2818
  async function cmdRestart(rest: string[]): Promise<number> {
2758
2819
  const y = yargs(rest)
2759
- .usage("Usage: ay restart <keyword>")
2820
+ .usage("Usage: ay restart <keyword> [--fresh]")
2760
2821
  .option("latest", { type: "boolean", default: false, description: "Use most recent match" })
2761
2822
  .option("cwd", { type: "string", description: "Restrict to agents under this dir" })
2823
+ .option("fresh", {
2824
+ type: "boolean",
2825
+ default: false,
2826
+ description: "Replay the original prompt instead of resuming the session",
2827
+ })
2762
2828
  .help(false)
2763
2829
  .version(false)
2764
2830
  .exitProcess(false);
@@ -2773,23 +2839,50 @@ async function cmdRestart(rest: string[]): Promise<number> {
2773
2839
  };
2774
2840
  const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
2775
2841
  const record = await resolveOne(keyword, opts);
2842
+ const fresh = Boolean(argv.fresh);
2776
2843
 
2844
+ // Live agent: gracefully stop it (claude /exit / double-Ctrl+C via FIFO), then
2845
+ // wait for it to actually exit before relaunching.
2777
2846
  if (isPidAlive(record.pid)) {
2778
- process.stderr.write(`pid ${record.pid} is still running — stop it first or use ay send\n`);
2779
- return 1;
2847
+ await gracefulExitAgent(record, "restart");
2848
+ process.stdout.write(`stopping pid ${record.pid} (${record.cli}) before restart…\n`);
2849
+ let exited = await waitForExit(record.pid, 30_000);
2850
+ if (!exited) {
2851
+ // Wouldn't go gracefully — SIGKILL the pid (the reaper sweeps its pgid).
2852
+ try {
2853
+ process.kill(record.pid, "SIGKILL");
2854
+ } catch {
2855
+ /* already gone / not permitted */
2856
+ }
2857
+ exited = await waitForExit(record.pid, 5_000);
2858
+ }
2859
+ if (!exited) {
2860
+ process.stderr.write(
2861
+ `pid ${record.pid} did not exit — aborting restart ` +
2862
+ `(try: ay stop ${record.pid} --method=double-ctrl-c)\n`,
2863
+ );
2864
+ return 1;
2865
+ }
2780
2866
  }
2781
2867
 
2782
- const args = ["--cli=" + record.cli];
2783
- if (record.prompt) args.push(record.prompt);
2868
+ // Resolve how to relaunch: a printed resume command (config `resumeCommand`),
2869
+ // else restoreArgs/--continue, else replay the prompt when --fresh.
2870
+ const conf = (await cliDefaults())[record.cli];
2871
+ const logText =
2872
+ !fresh && record.log_file ? await readFile(record.log_file, "utf8").catch(() => "") : "";
2873
+ const { args: resumeArgs, strategy } = resolveResumeArgs(conf, logText, {
2874
+ fresh,
2875
+ prompt: record.prompt,
2876
+ });
2784
2877
 
2785
- const proc = Bun.spawn(["agent-yes", ...args], {
2878
+ const proc = Bun.spawn(["agent-yes", "--cli=" + record.cli, ...resumeArgs], {
2786
2879
  cwd: record.cwd,
2787
2880
  detached: true,
2788
2881
  stdio: ["ignore", "ignore", "ignore"],
2789
2882
  });
2790
2883
 
2791
2884
  process.stdout.write(
2792
- `restarted ${record.cli} in ${shortenPath(record.cwd)} (new pid: ${proc.pid})\n`,
2885
+ `restarted ${record.cli} in ${shortenPath(record.cwd)} via ${strategy} (new pid: ${proc.pid})\n`,
2793
2886
  );
2794
2887
  process.stderr.write(
2795
2888
  `\n` +
@@ -2891,6 +2984,10 @@ export async function snapshotStatus(record: GlobalPidRecord): Promise<StatusSna
2891
2984
  state = "stuck";
2892
2985
  }
2893
2986
  }
2987
+ // The Rust supervisor's unresponsive flag is an authoritative wedge signal —
2988
+ // it overrides the log-tail heuristics above (but never a dead agent, which
2989
+ // Rust clears the flag on anyway).
2990
+ if (alive && record.unresponsive) state = "stuck";
2894
2991
  const notes = await readNotes();
2895
2992
  const note = notes.get(record.pid) ?? null;
2896
2993
  return {
@@ -1,8 +0,0 @@
1
- import "./ts-CxC_nYZ4.js";
2
- import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-D6lNBbCr.js";
4
- import "./pidStore-fqXqTKkh.js";
5
- import "./globalPidIndex-DlmmJlO8.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-Bc0HAQER.js";
7
-
8
- export { SUPPORTED_CLIS };
@@ -1,9 +0,0 @@
1
- import "./logger-CDIsZ-Pp.js";
2
- import "./globalPidIndex-DlmmJlO8.js";
3
- import "./configShared-C1C04bbq.js";
4
- import "./e2e-ClOI_aqV.js";
5
- import "./webrtcLink-BWhuA74k.js";
6
- import "./remotes-qK6uozO4.js";
7
- import { C as snapshotStatus, S as runSubcommand, T as writeToIpc, _ 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 stopTipForCli, x as resolveReadWindow, y as renderRawLogLines } from "./subcommands-w_qFLMDs.js";
8
-
9
- export { cmdHelp, isSubcommand, runSubcommand };