agent-yes 1.168.0 → 1.169.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/SUPPORTED_CLIS-BFV10E3f.js +8 -0
- package/dist/{SUPPORTED_CLIS-CnvLgnlN.js → SUPPORTED_CLIS-D1WUoZA9.js} +2 -2
- package/dist/cli.js +47 -6
- package/dist/{e2e-Bfw7qL9O.js → e2e-BeKjLhmO.js} +1 -1
- package/dist/forkNested-UCKPEgSI.js +59 -0
- package/dist/index.js +2 -2
- package/dist/{openBrowser-CuOlFtow.js → openBrowser-CCF1iuQK.js} +1 -1
- package/dist/{remotes-T6nf0t3K.js → remotes-BmAPylU_.js} +3 -3
- package/dist/{remotes-ClT1bq16.js → remotes-cx_GDFPj.js} +3 -3
- package/dist/{runningLock-V4qvXgAw.js → runningLock-BobVW1_A.js} +2 -1
- package/dist/{schedule-CgdRQVI9.js → schedule-DTHFMzDj.js} +5 -5
- package/dist/{serve-Bv2Fvt4n.js → serve-D-joIHrL.js} +34 -15
- package/dist/{setup-2mTN8Pz1.js → setup-K6VTmirE.js} +3 -3
- package/dist/{share-BjqQBWM-.js → share-D_e2Fwiy.js} +2 -2
- package/dist/{spawnGate-UH73I2le.js → spawnGate-CQ1Il3Xk.js} +1 -1
- package/dist/{spawnGate-B_VDMXYL.js → spawnGate-DzPfa1PZ.js} +2 -2
- package/dist/{subcommands-gqQYqyVj.js → subcommands-BZPW0UVn.js} +45 -11
- package/dist/subcommands-DuGwtmz5.js +9 -0
- package/dist/{tray-CZarCA2Q.js → tray-CWUpaZF4.js} +2 -2
- package/dist/{ts-j34CxbQs.js → ts-DeahYDN3.js} +3 -3
- package/dist/{versionChecker-CCPt7CpW.js → versionChecker-C7KP-aoh.js} +2 -2
- package/dist/{webrtcLink-CBZkZ-LT.js → webrtcLink-BG0Xc4-W.js} +2 -2
- package/dist/{webrtcRemote-GAgF5K45.js → webrtcRemote-SybKurg9.js} +3 -3
- package/dist/{workspaceConfig-D3OH7and.js → workspaceConfig-BC03X4Y1.js} +1 -1
- package/lab/ui/console-logic.js +16 -0
- package/lab/ui/index.html +28 -0
- package/package.json +1 -1
- package/ts/badges.spec.ts +54 -0
- package/ts/badges.ts +42 -0
- package/ts/cli.ts +46 -0
- package/ts/forkNested.spec.ts +34 -0
- package/ts/forkNested.ts +64 -0
- package/ts/needsInput.ts +1 -1
- package/ts/parseCliArgs.spec.ts +18 -0
- package/ts/parseCliArgs.ts +8 -0
- package/ts/runningLock.ts +1 -0
- package/ts/serve.ts +24 -0
- package/ts/subcommands.spec.ts +22 -0
- package/ts/subcommands.ts +40 -13
- package/dist/SUPPORTED_CLIS-JQkvO2eq.js +0 -8
- package/dist/subcommands-Bc08oDD_.js +0 -9
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as computeTranscriptHash, c as open, d as randomHex, f as seal, l as packEnvelope, n as FLAG_CONFIRM, o as deriveAuthToken, p as unpackEnvelope, s as deriveDirKeys } from "./e2e-
|
|
2
|
-
import { n as isWebrtcSpec, r as parseWebrtcLink, t as SIGNAL_SUBPROTOCOL } from "./webrtcLink-
|
|
1
|
+
import { a as computeTranscriptHash, c as open, d as randomHex, f as seal, l as packEnvelope, n as FLAG_CONFIRM, o as deriveAuthToken, p as unpackEnvelope, s as deriveDirKeys } from "./e2e-BeKjLhmO.js";
|
|
2
|
+
import { n as isWebrtcSpec, r as parseWebrtcLink, t as SIGNAL_SUBPROTOCOL } from "./webrtcLink-BG0Xc4-W.js";
|
|
3
3
|
import { RTCPeerConnection } from "node-datachannel/polyfill";
|
|
4
4
|
|
|
5
5
|
//#region ts/webrtcRemote.ts
|
|
@@ -256,4 +256,4 @@ async function startWebrtcBridge(link) {
|
|
|
256
256
|
|
|
257
257
|
//#endregion
|
|
258
258
|
export { startWebrtcBridge };
|
|
259
|
-
//# sourceMappingURL=webrtcRemote-
|
|
259
|
+
//# sourceMappingURL=webrtcRemote-SybKurg9.js.map
|
|
@@ -162,4 +162,4 @@ function resolveSpawnCwd(input) {
|
|
|
162
162
|
|
|
163
163
|
//#endregion
|
|
164
164
|
export { getSpawnWaitMs as a, isProvisionAllowed as c, getSpawnHook as i, resolveSpawnCwd as l, getMinFreeMb as n, getWorkspaceRoot as o, getProvisionRoot as r, hasSpawnHook as s, getMaxAgents as t, setWorkspaceRoot as u };
|
|
165
|
-
//# sourceMappingURL=workspaceConfig-
|
|
165
|
+
//# sourceMappingURL=workspaceConfig-BC03X4Y1.js.map
|
package/lab/ui/console-logic.js
CHANGED
|
@@ -180,6 +180,22 @@ export function taskLabel(e) {
|
|
|
180
180
|
return `${t.done}/${t.total}`;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// Display metadata for status flags matched server-side against an agent's
|
|
184
|
+
// screen (ts/badges.ts BADGE_DEFS — matching runs server-side; the API sends
|
|
185
|
+
// just the matched ids in e.badges). Keep the label/title text here in sync
|
|
186
|
+
// with ts/badges.ts. An id with no entry still renders (falls back to the raw
|
|
187
|
+
// id), so a newly-added server-side badge is never silently dropped.
|
|
188
|
+
export const BADGE_META = {
|
|
189
|
+
"goal-active": { label: "goal", title: "A /goal Stop-hook loop is active on this agent" },
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Status-flag chips ("badges") matched against the agent's screen — e.g. an
|
|
193
|
+
// active /goal loop. [] when e.badges is missing/empty. Returns
|
|
194
|
+
// { id, label, title } objects ready for the caller to render as chips.
|
|
195
|
+
export function badgesFor(e) {
|
|
196
|
+
return (e.badges || []).map((id) => ({ id, ...(BADGE_META[id] || { label: id, title: id }) }));
|
|
197
|
+
}
|
|
198
|
+
|
|
183
199
|
// Human age of an agent ("12s" / "5m" / "3h"). `now` is injectable so tests
|
|
184
200
|
// don't depend on the wall clock; the browser calls age(e) and gets Date.now().
|
|
185
201
|
export function age(e, now = Date.now()) {
|
package/lab/ui/index.html
CHANGED
|
@@ -891,6 +891,20 @@
|
|
|
891
891
|
color: var(--green);
|
|
892
892
|
border-color: #2a3a2a;
|
|
893
893
|
}
|
|
894
|
+
/* Status-flag chip ("badge") matched server-side against the agent's
|
|
895
|
+
screen — e.g. an active /goal loop (ts/badges.ts). Same pill shape as
|
|
896
|
+
.tasks but accent-colored so a matched flag stands out as a distinct
|
|
897
|
+
signal, not just a counter. */
|
|
898
|
+
.flag {
|
|
899
|
+
font-family: var(--mono);
|
|
900
|
+
font-size: 10.5px;
|
|
901
|
+
color: var(--accent);
|
|
902
|
+
white-space: nowrap;
|
|
903
|
+
flex: none;
|
|
904
|
+
padding: 0 5px;
|
|
905
|
+
border: 1px solid var(--accent);
|
|
906
|
+
border-radius: 8px;
|
|
907
|
+
}
|
|
894
908
|
/* The pending question when an agent needs_input — blue accent to match the
|
|
895
909
|
needs_input dot, reading as "your turn / here's what it's asking". */
|
|
896
910
|
.detail.ask {
|
|
@@ -1543,6 +1557,7 @@
|
|
|
1543
1557
|
sortEntries,
|
|
1544
1558
|
SORT_MODES,
|
|
1545
1559
|
taskLabel,
|
|
1560
|
+
badgesFor,
|
|
1546
1561
|
hashHue,
|
|
1547
1562
|
selFromBottom,
|
|
1548
1563
|
parseSel,
|
|
@@ -3219,6 +3234,17 @@
|
|
|
3219
3234
|
return `<span class="tasks${allDone ? " done" : ""}" title="${esc(tip)}">${esc(label)}</span>`;
|
|
3220
3235
|
}
|
|
3221
3236
|
|
|
3237
|
+
// Status-flag chips ("badges") matched server-side against the agent's
|
|
3238
|
+
// screen (ts/badges.ts) — e.g. an active /goal loop. One small pill per
|
|
3239
|
+
// matched flag; "" when none matched. CSS class is "flag" (not "badge")
|
|
3240
|
+
// to avoid colliding with the existing pid pill's .badge class. See
|
|
3241
|
+
// BADGE_META in console-logic.js for the id → label/title mapping.
|
|
3242
|
+
function badgeChipsHtml(e) {
|
|
3243
|
+
return badgesFor(e)
|
|
3244
|
+
.map((b) => `<span class="flag" title="${esc(b.title)}">${esc(b.label)}</span>`)
|
|
3245
|
+
.join("");
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3222
3248
|
// A room/peer group header row. Non-selectable; carries the same tree rails
|
|
3223
3249
|
// as agent rows so the hierarchy reads as one tree. The room/peer layers
|
|
3224
3250
|
// only appear here when there are ≥2 of them (single ones fold away in
|
|
@@ -3268,6 +3294,7 @@
|
|
|
3268
3294
|
${cli ? `<span class="cname">${esc(cli)}</span>` : ""}
|
|
3269
3295
|
<span class="ctitle ${e.title ? "" : "dim"}" title="${esc(t)}">${esc(t)}</span>
|
|
3270
3296
|
${taskChipHtml(e)}
|
|
3297
|
+
${badgeChipsHtml(e)}
|
|
3271
3298
|
${gitChipHtml(e)}
|
|
3272
3299
|
<span class="age">${age(e)}</span></div>`;
|
|
3273
3300
|
})
|
|
@@ -3293,6 +3320,7 @@
|
|
|
3293
3320
|
<span class="name">${esc(cliLabel(e) || ident(e) || "agent")}</span>
|
|
3294
3321
|
<span class="badge">pid ${e.pid}</span>
|
|
3295
3322
|
${taskChipHtml(e)}
|
|
3323
|
+
${badgeChipsHtml(e)}
|
|
3296
3324
|
${gitChipHtml(e)}
|
|
3297
3325
|
<span class="age">${age(e)}</span></div>
|
|
3298
3326
|
${e.title ? `<div class="rowtitle" title="${esc(e.title)}">${esc(e.title)}</div>` : ""}
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { badgeDef, BADGE_DEFS, matchBadges, type BadgeDef } from "./badges.ts";
|
|
3
|
+
|
|
4
|
+
describe("matchBadges", () => {
|
|
5
|
+
it("matches goal-active when the /goal status line is on screen", () => {
|
|
6
|
+
const lines = ["some output", "/goal active (42s)", "more output"];
|
|
7
|
+
expect(matchBadges(lines)).toEqual(["goal-active"]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("is case-insensitive", () => {
|
|
11
|
+
expect(matchBadges(["/GOAL ACTIVE (1s)"])).toEqual(["goal-active"]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns empty when nothing matches", () => {
|
|
15
|
+
expect(matchBadges(["just some regular CLI output", "nothing special here"])).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns empty for an empty screen", () => {
|
|
19
|
+
expect(matchBadges([])).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("matches across a join boundary between lines (pattern spans the joined text)", () => {
|
|
23
|
+
// Sanity: matchBadges joins lines with \n before testing, so a pattern that
|
|
24
|
+
// doesn't care about line boundaries still finds a hit split across two lines.
|
|
25
|
+
const defs: BadgeDef[] = [{ id: "spans", label: "x", title: "t", pattern: /foo\nbar/ }];
|
|
26
|
+
expect(matchBadges(["foo", "bar"], defs)).toEqual(["spans"]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("supports custom def sets independent of the built-in BADGE_DEFS", () => {
|
|
30
|
+
const defs: BadgeDef[] = [
|
|
31
|
+
{ id: "custom-error", label: "err", title: "custom error banner", pattern: /FATAL: boom/ },
|
|
32
|
+
];
|
|
33
|
+
expect(matchBadges(["FATAL: boom"], defs)).toEqual(["custom-error"]);
|
|
34
|
+
expect(matchBadges(["FATAL: boom"])).toEqual([]); // not in the default set
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("can match multiple badges at once, in def order", () => {
|
|
38
|
+
const defs: BadgeDef[] = [
|
|
39
|
+
{ id: "a", label: "a", title: "a", pattern: /alpha/ },
|
|
40
|
+
{ id: "b", label: "b", title: "b", pattern: /beta/ },
|
|
41
|
+
];
|
|
42
|
+
expect(matchBadges(["alpha and beta both here"], defs)).toEqual(["a", "b"]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("badgeDef", () => {
|
|
47
|
+
it("looks up a built-in definition by id", () => {
|
|
48
|
+
expect(badgeDef("goal-active")).toBe(BADGE_DEFS[0]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns undefined for an unknown id", () => {
|
|
52
|
+
expect(badgeDef("does-not-exist")).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
package/ts/badges.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Badge/flag matchers: small regex patterns run against an agent's last
|
|
3
|
+
* rendered screen (the same tail window `ay tail` shows — see
|
|
4
|
+
* extractBadges in subcommands.ts) to surface a short status chip in the
|
|
5
|
+
* web console's agent list. Extensible: add more entries to BADGE_DEFS as
|
|
6
|
+
* useful patterns turn up (a known error banner, a loop-state flag, etc.).
|
|
7
|
+
* Pure and CLI-agnostic — a definition just won't match on CLIs that never
|
|
8
|
+
* print its pattern.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface BadgeDef {
|
|
12
|
+
/** Stable id, used as the wire value and as the key when re-deriving the label. */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Short chip text, e.g. "goal". Keep it a couple of characters — the chip is tiny. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** Tooltip shown on hover, explaining what the badge means. */
|
|
17
|
+
title: string;
|
|
18
|
+
/** Matched against the tail-rendered screen text (lines joined with \n). */
|
|
19
|
+
pattern: RegExp;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const BADGE_DEFS: BadgeDef[] = [
|
|
23
|
+
{
|
|
24
|
+
id: "goal-active",
|
|
25
|
+
label: "goal",
|
|
26
|
+
title: "A /goal Stop-hook loop is active on this agent",
|
|
27
|
+
pattern: /\/goal active/i,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Which badge ids match the given rendered screen lines. Pure so it's
|
|
33
|
+
* unit-tested without a live PTY/log file.
|
|
34
|
+
*/
|
|
35
|
+
export function matchBadges(lines: string[], defs: BadgeDef[] = BADGE_DEFS): string[] {
|
|
36
|
+
const text = lines.join("\n");
|
|
37
|
+
return defs.filter((d) => d.pattern.test(text)).map((d) => d.id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function badgeDef(id: string, defs: BadgeDef[] = BADGE_DEFS): BadgeDef | undefined {
|
|
41
|
+
return defs.find((d) => d.id === id);
|
|
42
|
+
}
|
package/ts/cli.ts
CHANGED
|
@@ -106,6 +106,52 @@ if (config.useRust) {
|
|
|
106
106
|
console.log(`[rust] Args: ${rustArgs.join(" ")}`);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// Nested + non-tty (e.g. an agent ran this via its Bash tool): detach the
|
|
110
|
+
// agent so we don't block the parent's tool call for the whole session, then
|
|
111
|
+
// print how to drive it and exit. `--attach`/AGENT_YES_ATTACH=1 opts out.
|
|
112
|
+
const attach = config.attach || process.env.AGENT_YES_ATTACH === "1";
|
|
113
|
+
const { shouldForkNested, buildSpawnTutorial, waitForFifo } = await import("./forkNested.ts");
|
|
114
|
+
if (
|
|
115
|
+
shouldForkNested({
|
|
116
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
117
|
+
ayPid: process.env.AGENT_YES_PID,
|
|
118
|
+
attach,
|
|
119
|
+
})
|
|
120
|
+
) {
|
|
121
|
+
const forked = spawn(rustBinary, rustArgs, {
|
|
122
|
+
detached: true,
|
|
123
|
+
stdio: "ignore",
|
|
124
|
+
env: process.env,
|
|
125
|
+
cwd: process.cwd(),
|
|
126
|
+
});
|
|
127
|
+
const forkedPid = forked.pid;
|
|
128
|
+
if (!forkedPid) {
|
|
129
|
+
console.error("Failed to spawn agent: no pid.");
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
// Race a fast startup failure against FIFO registration so we never print a
|
|
133
|
+
// success tutorial for a dead pid. The wrapper keeps its own per-pid log, so
|
|
134
|
+
// real output stays reachable via `ay tail` even with stdio ignored. Store a
|
|
135
|
+
// pre-formatted message (not a union) — the callbacks run async, so control-flow
|
|
136
|
+
// analysis can't narrow a union here anyway.
|
|
137
|
+
let deathMsg: string | null = null;
|
|
138
|
+
forked.on("error", (err) => {
|
|
139
|
+
deathMsg ??= `Failed to spawn agent: ${err.message}`;
|
|
140
|
+
});
|
|
141
|
+
forked.on("exit", (code, signal) => {
|
|
142
|
+
deathMsg ??= `Agent exited immediately (${signal ?? `code ${code}`}). See: ay tail ${forkedPid}`;
|
|
143
|
+
});
|
|
144
|
+
const registered = await waitForFifo(forkedPid, 2000, () => deathMsg !== null);
|
|
145
|
+
if (deathMsg) {
|
|
146
|
+
console.error(deathMsg);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
forked.unref();
|
|
150
|
+
console.log(buildSpawnTutorial(config.cli || "agent", forkedPid));
|
|
151
|
+
if (!registered) console.log(`(note: still registering — give ay send/tail a moment)`);
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
|
|
109
155
|
const child = spawn(rustBinary, rustArgs, {
|
|
110
156
|
stdio: "inherit",
|
|
111
157
|
env: process.env,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildSpawnTutorial, shouldForkNested } from "./forkNested";
|
|
3
|
+
|
|
4
|
+
describe("shouldForkNested", () => {
|
|
5
|
+
it("forks when nested (AGENT_YES_PID set) and stdout is not a TTY", () => {
|
|
6
|
+
expect(shouldForkNested({ isTTY: false, ayPid: "1234", attach: false })).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("does NOT fork on an interactive TTY (a human running it directly)", () => {
|
|
10
|
+
expect(shouldForkNested({ isTTY: true, ayPid: "1234", attach: false })).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("does NOT fork when not nested — a human piping output has no AGENT_YES_PID", () => {
|
|
14
|
+
expect(shouldForkNested({ isTTY: false, ayPid: undefined, attach: false })).toBe(false);
|
|
15
|
+
expect(shouldForkNested({ isTTY: false, ayPid: "", attach: false })).toBe(false);
|
|
16
|
+
expect(shouldForkNested({ isTTY: false, ayPid: " ", attach: false })).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("does NOT fork when attach opts out, regardless of context", () => {
|
|
20
|
+
expect(shouldForkNested({ isTTY: false, ayPid: "1234", attach: true })).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("buildSpawnTutorial", () => {
|
|
25
|
+
it("names the cli + pid and lists the drive commands with that pid", () => {
|
|
26
|
+
const out = buildSpawnTutorial("claude", 4242);
|
|
27
|
+
expect(out).toContain("Spawned claude agent as pid 4242");
|
|
28
|
+
expect(out).toContain("ay tail 4242");
|
|
29
|
+
expect(out).toContain("ay send 4242");
|
|
30
|
+
expect(out).toContain("ay ls");
|
|
31
|
+
expect(out).toContain("ay result get 4242");
|
|
32
|
+
expect(out).toContain("ay exit 4242");
|
|
33
|
+
});
|
|
34
|
+
});
|
package/ts/forkNested.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { agentYesHome } from "./agentYesHome.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Should a nested agent-run detach (fork) instead of blocking the caller?
|
|
7
|
+
*
|
|
8
|
+
* A claude/codex agent that runs `ay <cli> -- <task>` inside its Bash tool would
|
|
9
|
+
* otherwise block that Bash call for the whole (possibly very long) session and
|
|
10
|
+
* time out. When we detect that context we spawn the agent detached, print a
|
|
11
|
+
* tutorial, and return immediately so the parent stays responsive.
|
|
12
|
+
*
|
|
13
|
+
* The context is: we are NESTED inside another agent (`AGENT_YES_PID` is injected
|
|
14
|
+
* into an agent's environment by its wrapper — a human shell never has it) AND
|
|
15
|
+
* stdout is not a TTY (captured/piped, e.g. a tool's Bash). A human piping output
|
|
16
|
+
* (`ay claude | cat`) has no `AGENT_YES_PID`, so they still block as before.
|
|
17
|
+
* `attach` (the `--attach` flag or `AGENT_YES_ATTACH=1`) forces foreground.
|
|
18
|
+
*/
|
|
19
|
+
export function shouldForkNested(opts: {
|
|
20
|
+
isTTY: boolean;
|
|
21
|
+
ayPid: string | undefined;
|
|
22
|
+
attach: boolean;
|
|
23
|
+
}): boolean {
|
|
24
|
+
if (opts.attach) return false;
|
|
25
|
+
if (opts.isTTY) return false;
|
|
26
|
+
return Boolean(opts.ayPid && opts.ayPid.trim());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The tutorial printed to the parent agent after a detached spawn, telling it
|
|
30
|
+
* exactly how to drive the agent it just started. */
|
|
31
|
+
export function buildSpawnTutorial(cli: string, pid: number): string {
|
|
32
|
+
return [
|
|
33
|
+
`Spawned ${cli} agent as pid ${pid} (detached — this shell returned immediately).`,
|
|
34
|
+
`It runs in the background; drive it with:`,
|
|
35
|
+
` ay tail ${pid} # watch its output (live)`,
|
|
36
|
+
` ay send ${pid} "..." # send it a message / instruction`,
|
|
37
|
+
` ay send ${pid} /compact # send a slash command`,
|
|
38
|
+
` ay ls # list running agents`,
|
|
39
|
+
` ay result get ${pid} # read its final result when done`,
|
|
40
|
+
` ay exit ${pid} # stop it`,
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Poll for the spawned wrapper's stdin FIFO to appear, so the tutorial's
|
|
46
|
+
* `ay send`/`ay tail` work the instant we print them (the wrapper registers its
|
|
47
|
+
* FIFO a moment after spawn). Resolves true once registered, false on timeout or
|
|
48
|
+
* if `aborted()` reports the child already died (so a startup failure fails fast
|
|
49
|
+
* instead of waiting out the whole window).
|
|
50
|
+
*/
|
|
51
|
+
export async function waitForFifo(
|
|
52
|
+
pid: number,
|
|
53
|
+
timeoutMs = 2000,
|
|
54
|
+
aborted?: () => boolean,
|
|
55
|
+
): Promise<boolean> {
|
|
56
|
+
const fifo = path.join(agentYesHome(), "fifo", `${pid}.stdin`);
|
|
57
|
+
const deadline = Date.now() + timeoutMs;
|
|
58
|
+
while (Date.now() < deadline) {
|
|
59
|
+
if (aborted?.()) return false;
|
|
60
|
+
if (existsSync(fifo)) return true;
|
|
61
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
62
|
+
}
|
|
63
|
+
return existsSync(fifo);
|
|
64
|
+
}
|
package/ts/needsInput.ts
CHANGED
|
@@ -78,7 +78,7 @@ export interface MenuState {
|
|
|
78
78
|
|
|
79
79
|
// An option row: an optional cursor glyph / bullet, then "N. " (the trailing
|
|
80
80
|
// space rejects version-like "3.5GB" that isn't a menu option).
|
|
81
|
-
const OPTION_LINE = /^[\s
|
|
81
|
+
const OPTION_LINE = /^[\s❯›>▶◉○●·*-]*?(\d+)\.\s/;
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
84
|
* Parse the selection menu a `needs_input` agent is parked on into a cursor
|
package/ts/parseCliArgs.spec.ts
CHANGED
|
@@ -439,4 +439,22 @@ describe("CLI argument parsing", () => {
|
|
|
439
439
|
expect(afterCli.useStdinAppend).toBe(true);
|
|
440
440
|
expect(afterCli.cliArgs).toContain("--no-stdpush");
|
|
441
441
|
});
|
|
442
|
+
|
|
443
|
+
it("consumes --attach as an agent-yes flag before the CLI positional", () => {
|
|
444
|
+
const result = parseCliArgs(["node", "/path/to/ay", "--attach", "claude", "--", "task"]);
|
|
445
|
+
expect(result.attach).toBe(true);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("defaults attach to false so fork-by-default stays enabled", () => {
|
|
449
|
+
const result = parseCliArgs(["node", "/path/to/ay", "claude"]);
|
|
450
|
+
expect(result.attach).toBe(false);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("does NOT treat --attach after -- as an agent-yes flag (passthrough safety)", () => {
|
|
454
|
+
// Everything after `--` is prompt / CLI passthrough, never an agent-yes flag —
|
|
455
|
+
// so `ay claude -- --attach` sends --attach to the CLI and still forks.
|
|
456
|
+
const result = parseCliArgs(["node", "/path/to/ay", "claude", "--", "--attach"]);
|
|
457
|
+
expect(result.attach).toBe(false);
|
|
458
|
+
expect(result.prompt).toContain("--attach");
|
|
459
|
+
});
|
|
442
460
|
});
|
package/ts/parseCliArgs.ts
CHANGED
|
@@ -30,6 +30,13 @@ export function parseCliArgs(argv: string[], supportedClis?: readonly string[])
|
|
|
30
30
|
description: "re-spawn Claude with --continue if it crashes, only works for claude yet",
|
|
31
31
|
alias: "r",
|
|
32
32
|
})
|
|
33
|
+
.option("attach", {
|
|
34
|
+
type: "boolean",
|
|
35
|
+
default: false,
|
|
36
|
+
description:
|
|
37
|
+
"Run the agent in the foreground even when nested inside another agent (disables fork-by-default; also AGENT_YES_ATTACH=1).",
|
|
38
|
+
alias: "foreground",
|
|
39
|
+
})
|
|
33
40
|
.option("logFile", {
|
|
34
41
|
type: "string",
|
|
35
42
|
description: "Rendered log file to write to.",
|
|
@@ -307,6 +314,7 @@ export function parseCliArgs(argv: string[], supportedClis?: readonly string[])
|
|
|
307
314
|
),
|
|
308
315
|
queue: parsedArgv.queue,
|
|
309
316
|
robust: parsedArgv.robust,
|
|
317
|
+
attach: parsedArgv.attach,
|
|
310
318
|
logFile: parsedArgv.logFile,
|
|
311
319
|
verbose: parsedArgv.verbose,
|
|
312
320
|
resume: parsedArgv.continue, // Note: intentional use resume here to avoid preserved keyword (continue)
|
package/ts/runningLock.ts
CHANGED
|
@@ -171,6 +171,7 @@ async function checkLock(cwd: string, _prompt: string): Promise<LockCheckResult>
|
|
|
171
171
|
|
|
172
172
|
// Find running tasks for this location
|
|
173
173
|
const blockingTasks = lockFile.tasks.filter((task) => {
|
|
174
|
+
if (task.pid === process.pid) return false; // Never self-block on our own prior entry
|
|
174
175
|
if (!isProcessRunning(task.pid)) return false; // Skip stale locks
|
|
175
176
|
if (task.status !== "running") return false; // Only check running tasks
|
|
176
177
|
|
package/ts/serve.ts
CHANGED
|
@@ -9,6 +9,7 @@ import yargs from "yargs";
|
|
|
9
9
|
import {
|
|
10
10
|
controlCodeFromName,
|
|
11
11
|
deriveLiveStatus,
|
|
12
|
+
extractBadges,
|
|
12
13
|
extractNeedsInput,
|
|
13
14
|
extractTaskCounts,
|
|
14
15
|
listRecords,
|
|
@@ -935,6 +936,26 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
935
936
|
}
|
|
936
937
|
};
|
|
937
938
|
|
|
939
|
+
// Per-agent status badges/flags (see badges.ts) matched against the agent's
|
|
940
|
+
// rendered screen — e.g. an active /goal Stop-hook loop. Cached per
|
|
941
|
+
// (size, mtime) exactly like logTasks. Extend BADGE_DEFS in badges.ts to
|
|
942
|
+
// surface more patterns (error banners, other flags); no server changes
|
|
943
|
+
// needed beyond that.
|
|
944
|
+
const badgeCache = new Map<string, { size: number; mtimeMs: number; badges: string[] }>();
|
|
945
|
+
const logBadges = async (logFile: string | null | undefined): Promise<string[]> => {
|
|
946
|
+
if (!logFile) return [];
|
|
947
|
+
try {
|
|
948
|
+
const { size, mtimeMs } = await stat(logFile);
|
|
949
|
+
const hit = badgeCache.get(logFile);
|
|
950
|
+
if (hit && hit.size === size && hit.mtimeMs === mtimeMs) return hit.badges;
|
|
951
|
+
const badges = await extractBadges(logFile);
|
|
952
|
+
badgeCache.set(logFile, { size, mtimeMs, badges });
|
|
953
|
+
return badges;
|
|
954
|
+
} catch {
|
|
955
|
+
return [];
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
|
|
938
959
|
// Per-agent "waiting on you" detection: the agent is parked on an interactive
|
|
939
960
|
// menu it did NOT auto-resolve (config `needsInput` patterns). Same source and
|
|
940
961
|
// classifier as `ay ls` / `ay status`, so the console's dot matches the CLI's
|
|
@@ -1148,6 +1169,9 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1148
1169
|
// Task progress from the rendered todo block (null when none detected → no
|
|
1149
1170
|
// badge). Skipped for exited agents — their screen is no longer live.
|
|
1150
1171
|
tasks: status === "exited" ? null : await logTasks(r.log_file),
|
|
1172
|
+
// Status flags matched against the rendered screen (see badges.ts) — e.g.
|
|
1173
|
+
// an active /goal loop. [] when none matched or for exited agents.
|
|
1174
|
+
badges: status === "exited" ? [] : await logBadges(r.log_file),
|
|
1151
1175
|
};
|
|
1152
1176
|
};
|
|
1153
1177
|
|
package/ts/subcommands.spec.ts
CHANGED
|
@@ -964,6 +964,28 @@ describe("subcommands.isExitRequest", () => {
|
|
|
964
964
|
});
|
|
965
965
|
});
|
|
966
966
|
|
|
967
|
+
describe("subcommands.isSlashCommand", () => {
|
|
968
|
+
it("matches a body that starts with a slash command (so it is sent unprefixed)", async () => {
|
|
969
|
+
const { isSlashCommand } = await loadModule();
|
|
970
|
+
for (const s of ["/compact", "/clear", "/model sonnet", "/resume\nmore text"]) {
|
|
971
|
+
expect(isSlashCommand(s)).toBe(true);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
it("does NOT match plain prose, or a slash not at column 0", async () => {
|
|
975
|
+
const { isSlashCommand } = await loadModule();
|
|
976
|
+
for (const s of [
|
|
977
|
+
"please run /compact",
|
|
978
|
+
" /compact", // leading space — the CLI won't parse it as a command either
|
|
979
|
+
"//two slashes... wait, no letter after first slash",
|
|
980
|
+
"/ ",
|
|
981
|
+
"hello",
|
|
982
|
+
"",
|
|
983
|
+
]) {
|
|
984
|
+
expect(isSlashCommand(s)).toBe(false);
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
|
|
967
989
|
describe("subcommands.writeToIpc reliable delivery", () => {
|
|
968
990
|
const itUnix = process.platform === "linux" || process.platform === "darwin";
|
|
969
991
|
|
package/ts/subcommands.ts
CHANGED
|
@@ -18,6 +18,7 @@ import path from "path";
|
|
|
18
18
|
import { type GlobalPidRecord, readGlobalPids, updateGlobalPidStatus } from "./globalPidIndex.ts";
|
|
19
19
|
import { buildAgentForest, flattenForest } from "./agentTree.ts";
|
|
20
20
|
import { parseTaskCounts, type TaskCounts } from "./todoParse.ts";
|
|
21
|
+
import { matchBadges } from "./badges.ts";
|
|
21
22
|
import {
|
|
22
23
|
classifyNeedsInput,
|
|
23
24
|
isWorkingScreen,
|
|
@@ -2128,6 +2129,17 @@ export async function extractNeedsInput(logPath: string, cli: string): Promise<N
|
|
|
2128
2129
|
return classifyNeedsInput(lines, { needsInput: cfg.needsInput, working: cfg.working });
|
|
2129
2130
|
}
|
|
2130
2131
|
|
|
2132
|
+
/**
|
|
2133
|
+
* Which badges (see badges.ts) match an agent's current screen — the same 32 KB
|
|
2134
|
+
* tail window `ay tail` renders, no CLI-specific config needed. Returns [] on
|
|
2135
|
+
* any read/render error or an empty log, same failure shape as extractNeedsInput.
|
|
2136
|
+
*/
|
|
2137
|
+
export async function extractBadges(logPath: string): Promise<string[]> {
|
|
2138
|
+
const lines = await renderLogTailLines(logPath, 40);
|
|
2139
|
+
if (!lines) return [];
|
|
2140
|
+
return matchBadges(lines);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2131
2143
|
/**
|
|
2132
2144
|
* Whether an alive agent is wedged: its log has been silent for at least
|
|
2133
2145
|
* STUCK_THRESHOLD_MS yet its screen still shows a `working` busy marker (a live
|
|
@@ -2388,7 +2400,10 @@ export async function extractMenu(logPath: string, cli: string): Promise<MenuSta
|
|
|
2388
2400
|
|
|
2389
2401
|
/** Poll until the agent is no longer parked on a menu (selection accepted → it
|
|
2390
2402
|
* resumed / moved on) or the deadline passes. Returns true if it cleared. */
|
|
2391
|
-
async function waitForNeedsInputClear(
|
|
2403
|
+
async function waitForNeedsInputClear(
|
|
2404
|
+
record: GlobalPidRecord,
|
|
2405
|
+
timeoutMs: number,
|
|
2406
|
+
): Promise<boolean> {
|
|
2392
2407
|
const deadline = Date.now() + timeoutMs;
|
|
2393
2408
|
while (Date.now() < deadline) {
|
|
2394
2409
|
await new Promise((r) => setTimeout(r, 250));
|
|
@@ -2476,10 +2491,15 @@ async function cmdSend(rest: string[]): Promise<number> {
|
|
|
2476
2491
|
}
|
|
2477
2492
|
|
|
2478
2493
|
// When an agent sends, prefix one line so the recipient knows who pinged it
|
|
2479
|
-
// and exactly how to reply (to a resolvable pid — the sender's own).
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2494
|
+
// and exactly how to reply (to a resolvable pid — the sender's own). BUT a
|
|
2495
|
+
// slash command is only recognized when `/` is the very first character of the
|
|
2496
|
+
// submitted message; the prefix would bump it to line 2 and the CLI would type
|
|
2497
|
+
// the command as plain text. So skip the prefix for a command body and send it
|
|
2498
|
+
// verbatim — attribution is dropped for the command, but it actually runs.
|
|
2499
|
+
const prefix =
|
|
2500
|
+
sender.agent && !isSlashCommand(body)
|
|
2501
|
+
? `[from ${sender.agent.cli} #${sender.agent.pid} @ ${shortenPath(sender.agent.cwd)} — reply: ay send ${sender.agent.pid} "..."]\n`
|
|
2502
|
+
: "";
|
|
2483
2503
|
|
|
2484
2504
|
const fullBody = prefix + body;
|
|
2485
2505
|
if (fullBody && trailing) {
|
|
@@ -2610,9 +2630,7 @@ async function cmdSelect(rest: string[]): Promise<number> {
|
|
|
2610
2630
|
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
2611
2631
|
const n = Number(argv._[1]);
|
|
2612
2632
|
if (!keyword || !Number.isInteger(n) || n < 1) {
|
|
2613
|
-
throw new Error(
|
|
2614
|
-
"usage: ay select <keyword> <N> (N = the 1-based option number to choose)",
|
|
2615
|
-
);
|
|
2633
|
+
throw new Error("usage: ay select <keyword> <N> (N = the 1-based option number to choose)");
|
|
2616
2634
|
}
|
|
2617
2635
|
|
|
2618
2636
|
const opts: CommonOpts = {
|
|
@@ -2624,7 +2642,9 @@ async function cmdSelect(rest: string[]): Promise<number> {
|
|
|
2624
2642
|
};
|
|
2625
2643
|
const record = await resolveWritableAgent(keyword, opts);
|
|
2626
2644
|
if (!record.log_file) {
|
|
2627
|
-
throw new Error(
|
|
2645
|
+
throw new Error(
|
|
2646
|
+
`pid ${record.pid}: no log_file recorded — can't read the menu to select from.`,
|
|
2647
|
+
);
|
|
2628
2648
|
}
|
|
2629
2649
|
const force = Boolean(argv.force) || process.env.AGENT_YES_FORCE_SEND === "1";
|
|
2630
2650
|
await enforceSendGuards(record, force);
|
|
@@ -2636,9 +2656,7 @@ async function cmdSelect(rest: string[]): Promise<number> {
|
|
|
2636
2656
|
);
|
|
2637
2657
|
}
|
|
2638
2658
|
if (menu.options.length > 0 && !menu.options.includes(n)) {
|
|
2639
|
-
throw new Error(
|
|
2640
|
-
`option ${n} is out of range — this menu offers ${menu.options.join(", ")}.`,
|
|
2641
|
-
);
|
|
2659
|
+
throw new Error(`option ${n} is out of range — this menu offers ${menu.options.join(", ")}.`);
|
|
2642
2660
|
}
|
|
2643
2661
|
|
|
2644
2662
|
// Move the cursor from where it sits to option N, then confirm. Delta from the
|
|
@@ -2648,7 +2666,8 @@ async function cmdSelect(rest: string[]): Promise<number> {
|
|
|
2648
2666
|
await writeKeysPaced(record.fifo_file!, byteSeqs, Math.max(0, argv.pace));
|
|
2649
2667
|
|
|
2650
2668
|
const delta = n - menu.cursor;
|
|
2651
|
-
const moved =
|
|
2669
|
+
const moved =
|
|
2670
|
+
delta === 0 ? "cursor already there" : `${Math.abs(delta)}× ${delta > 0 ? "down" : "up"}`;
|
|
2652
2671
|
process.stdout.write(
|
|
2653
2672
|
`pid ${record.pid} (${record.cli}): selected option ${n} (${moved} + enter)\n`,
|
|
2654
2673
|
);
|
|
@@ -2917,6 +2936,14 @@ export function isExitRequest(body: string): boolean {
|
|
|
2917
2936
|
return t === "exit" || t === "/exit";
|
|
2918
2937
|
}
|
|
2919
2938
|
|
|
2939
|
+
/** A body that the CLI will parse as a slash command — `/` as the very first
|
|
2940
|
+
* character (claude requires column 0, no leading whitespace, then a letter).
|
|
2941
|
+
* Such a body must be sent verbatim: any prefix line bumps the `/` off column 0
|
|
2942
|
+
* and the CLI types the command as plain text instead of running it. */
|
|
2943
|
+
export function isSlashCommand(body: string): boolean {
|
|
2944
|
+
return /^\/[A-Za-z]/.test(body);
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2920
2947
|
/**
|
|
2921
2948
|
* Gracefully terminate a live agent and record WHY in its note (the audit trail
|
|
2922
2949
|
* shown by `ay ls`). Sends the CLI's graceful-exit command (e.g. claude's
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import "./ts-j34CxbQs.js";
|
|
2
|
-
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-CCPt7CpW.js";
|
|
4
|
-
import "./pidStore-BfoBWUjc.js";
|
|
5
|
-
import "./globalPidIndex-DlmmJlO8.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CnvLgnlN.js";
|
|
7
|
-
|
|
8
|
-
export { SUPPORTED_CLIS };
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import "./logger-CDIsZ-Pp.js";
|
|
2
|
-
import "./globalPidIndex-DlmmJlO8.js";
|
|
3
|
-
import "./configShared-CEyhl0WH.js";
|
|
4
|
-
import "./e2e-Bfw7qL9O.js";
|
|
5
|
-
import "./webrtcLink-CBZkZ-LT.js";
|
|
6
|
-
import "./remotes-T6nf0t3K.js";
|
|
7
|
-
import { A as writeToIpc, C as resolveReadWindow, D as snapshotStatus, E as runSubcommand, O as stopTipForCli, S as resolveOne, T as restartHintLines, _ as menuSelectKeys, a as cursorAbs, b as renderRawLog, c as extractNeedsInput, d as isAgentStuck, f as isExitRequest, g as matchKeyword, h as listRecords, i as controlCodeFromName, k as writeKeysPaced, l as extractTaskCounts, m as isSubcommand, n as READ_PAGE_DEFAULT, o as deriveLiveStatus, p as isPidAlive, r as cmdHelp, s as extractMenu, t as GRACEFUL_EXIT_COMMANDS, u as finalizedLines, v as readNotes, w as resolveResumeArgs, x as renderRawLogLines, y as readPtysize } from "./subcommands-gqQYqyVj.js";
|
|
8
|
-
|
|
9
|
-
export { cmdHelp, isSubcommand, runSubcommand };
|