agent-yes 1.122.2 → 1.123.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/default.config.yaml +19 -0
- package/dist/{SUPPORTED_CLIS-BTu2brih.js → SUPPORTED_CLIS-B4O2cFlt.js} +2 -2
- package/dist/SUPPORTED_CLIS-DHkqGoNv.js +8 -0
- package/dist/{agent-yes.config-z-IPzH5U.js → agent-yes.config-D6ycMApr.js} +2 -65
- package/dist/cli.js +6 -6
- package/dist/configShared-C5QaNPnz.js +71 -0
- package/dist/{globalPidIndex-gZuTvTBs.js → globalPidIndex-C7r2m6s7.js} +19 -20
- package/dist/index.js +4 -4
- package/dist/pidStore-C4c2O15q.js +5 -0
- package/dist/{pidStore-B5vBu8Px.js → pidStore-CGKIhaJO.js} +5 -4
- package/dist/reaper-BLVA780B.js +3 -0
- package/dist/{reaper-Dj8R7ltI.js → reaper-BkjPN7mw.js} +24 -2
- package/dist/{remotes-CpGcTr7A.js → remotes-BRCDVnR7.js} +1 -1
- package/dist/{remotes-D2fqaRU8.js → remotes-D8GvSbhf.js} +1 -1
- package/dist/{schedule-DgRrdA_n.js → schedule-DULdIkU9.js} +7 -7
- package/dist/{serve-tn7ZetZs.js → serve-r_2v9EKc.js} +202 -58
- package/dist/{setup-dZhgpNse.js → setup-DHa6fX8M.js} +3 -3
- package/dist/{share-CksllWW-.js → share-YuM6-Q6A.js} +78 -4
- package/dist/{subcommands-D9BWZilr.js → subcommands-B13Kto-u.js} +647 -32
- package/dist/subcommands-Tv6AwUkD.js +7 -0
- package/dist/{tray-DjCIyakK.js → tray-BVnJLThD.js} +1 -1
- package/dist/{ts-CIf0uaR7.js → ts-DgukRoEI.js} +10 -7
- package/dist/{versionChecker-DjxKi4qe.js → versionChecker-BqOr1YqC.js} +2 -2
- package/dist/{workspaceConfig-XP2NEWmV.js → workspaceConfig-BJO4fzEn.js} +1 -1
- package/lab/ui/console-logic.js +222 -10
- package/lab/ui/icon.svg +5 -0
- package/lab/ui/index.html +689 -14
- package/lab/ui/landing.html +276 -0
- package/lab/ui/manifest.webmanifest +14 -0
- package/lab/ui/sw.js +56 -0
- package/package.json +5 -1
- package/ts/agentTree.spec.ts +92 -0
- package/ts/agentTree.ts +149 -0
- package/ts/configShared.ts +4 -0
- package/ts/globalPidIndex.ts +28 -20
- package/ts/idleWaiter.spec.ts +7 -1
- package/ts/index.ts +9 -0
- package/ts/lsWatch.spec.ts +61 -0
- package/ts/lsWatch.ts +94 -0
- package/ts/needsInput.spec.ts +55 -0
- package/ts/needsInput.ts +68 -0
- package/ts/pidStore.ts +3 -0
- package/ts/reaper.spec.ts +26 -2
- package/ts/reaper.ts +25 -0
- package/ts/resultEnvelope.spec.ts +43 -0
- package/ts/resultEnvelope.ts +88 -0
- package/ts/serve.ts +276 -41
- package/ts/share.ts +156 -3
- package/ts/subcommands.ts +0 -0
- package/ts/todoParse.spec.ts +68 -0
- package/ts/todoParse.ts +88 -0
- package/ts/utils.spec.ts +4 -1
- package/dist/SUPPORTED_CLIS-DcOKE9Nz.js +0 -8
- package/dist/pidStore-7y1cTcAE.js +0 -5
- package/dist/reaper-HqcUms2d.js +0 -3
- package/dist/subcommands-D8sHibKu.js +0 -6
|
@@ -1,11 +1,300 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
1
|
+
import { t as agentYesHome } from "./agentYesHome-BvaUOzCV.js";
|
|
2
|
+
import { a as updateGlobalPidStatus, i as readGlobalPids } from "./globalPidIndex-C7r2m6s7.js";
|
|
3
|
+
import { t as loadSharedCliDefaults } from "./configShared-C5QaNPnz.js";
|
|
4
|
+
import { a as resolveRemoteSpec, i as readRemotes } from "./remotes-D8GvSbhf.js";
|
|
3
5
|
import ms from "ms";
|
|
4
6
|
import yargs from "yargs";
|
|
5
7
|
import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
|
|
6
8
|
import { homedir } from "os";
|
|
7
9
|
import path from "path";
|
|
8
10
|
|
|
11
|
+
//#region ts/agentTree.ts
|
|
12
|
+
/**
|
|
13
|
+
* Link records into a forest via parent_pid === wrapper_pid. Records whose
|
|
14
|
+
* parent isn't present in the set (top-level agents, or links into agents
|
|
15
|
+
* filtered out by a keyword/scope) become roots. Root and sibling order follows
|
|
16
|
+
* the input order, so a caller that pre-sorts (e.g. newest-first) is preserved.
|
|
17
|
+
*/
|
|
18
|
+
function buildAgentForest(records) {
|
|
19
|
+
const nodes = records.map((record) => ({
|
|
20
|
+
record,
|
|
21
|
+
children: []
|
|
22
|
+
}));
|
|
23
|
+
const byWrapper = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const n of nodes) {
|
|
25
|
+
const w = n.record.wrapper_pid;
|
|
26
|
+
if (typeof w === "number" && w > 0) byWrapper.set(w, n);
|
|
27
|
+
}
|
|
28
|
+
const roots = [];
|
|
29
|
+
for (const n of nodes) {
|
|
30
|
+
const p = n.record.parent_pid;
|
|
31
|
+
const parent = typeof p === "number" && p > 0 ? byWrapper.get(p) : void 0;
|
|
32
|
+
if (parent && parent !== n) parent.children.push(n);
|
|
33
|
+
else roots.push(n);
|
|
34
|
+
}
|
|
35
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36
|
+
const mark = (n) => {
|
|
37
|
+
if (seen.has(n)) return;
|
|
38
|
+
seen.add(n);
|
|
39
|
+
n.children.forEach(mark);
|
|
40
|
+
};
|
|
41
|
+
roots.forEach(mark);
|
|
42
|
+
for (const n of nodes) if (!seen.has(n)) roots.push(n);
|
|
43
|
+
return roots;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Depth-first flatten a forest into rows carrying a box-drawing branch prefix.
|
|
47
|
+
* A `visited` guard makes a pathological parent_pid cycle terminate instead of
|
|
48
|
+
* recursing forever.
|
|
49
|
+
*/
|
|
50
|
+
function flattenForest(roots) {
|
|
51
|
+
const rows = [];
|
|
52
|
+
const visited = /* @__PURE__ */ new Set();
|
|
53
|
+
const walk = (node, ancestorsLast) => {
|
|
54
|
+
if (visited.has(node)) return;
|
|
55
|
+
visited.add(node);
|
|
56
|
+
const depth = ancestorsLast.length;
|
|
57
|
+
let prefix = "";
|
|
58
|
+
for (let i = 0; i < depth - 1; i++) prefix += ancestorsLast[i] ? " " : "│ ";
|
|
59
|
+
if (depth > 0) prefix += ancestorsLast[depth - 1] ? "└─ " : "├─ ";
|
|
60
|
+
rows.push({
|
|
61
|
+
record: node.record,
|
|
62
|
+
prefix,
|
|
63
|
+
depth
|
|
64
|
+
});
|
|
65
|
+
node.children.forEach((c, i) => walk(c, [...ancestorsLast, i === node.children.length - 1]));
|
|
66
|
+
};
|
|
67
|
+
for (const r of roots) walk(r, []);
|
|
68
|
+
return rows;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region ts/todoParse.ts
|
|
73
|
+
/**
|
|
74
|
+
* Parse an agent's todo/task list out of its RENDERED TUI screen.
|
|
75
|
+
*
|
|
76
|
+
* Source of truth for EVERY CLI (claude, codex, gemini, …) is the screen the
|
|
77
|
+
* agent draws — never a CLI-specific session file. The durable copy is the
|
|
78
|
+
* per-pid raw log (`<cwd>/.agent-yes/<pid>.raw.log`); rendering it through a
|
|
79
|
+
* headless xterm (see renderRawLog) collapses the reflow/redraw frames into the
|
|
80
|
+
* final coherent text, which is what we scan here.
|
|
81
|
+
*
|
|
82
|
+
* The todo list in these TUIs renders as a tree block anchored by the `⎿`
|
|
83
|
+
* branch glyph, one marker per line:
|
|
84
|
+
*
|
|
85
|
+
* ⎿ ☒ Wire up the parser
|
|
86
|
+
* ☒ Add the badge
|
|
87
|
+
* ◼ Compute in /api/ls ← in progress
|
|
88
|
+
* ◻ Render in the console ← pending
|
|
89
|
+
* ◻ Tests
|
|
90
|
+
*
|
|
91
|
+
* Badge = `${done}/${total}` (done is the numerator → "2/5").
|
|
92
|
+
*
|
|
93
|
+
* This parse is deliberately conservative: we only report a count when a block
|
|
94
|
+
* is confidently detected (the `⎿` anchor + ≥2 consecutive marker lines), so an
|
|
95
|
+
* agent that merely prints a check glyph in prose never produces a phantom badge.
|
|
96
|
+
*/
|
|
97
|
+
const DONE = new Set([
|
|
98
|
+
"✔",
|
|
99
|
+
"☑",
|
|
100
|
+
"✓",
|
|
101
|
+
"☒"
|
|
102
|
+
]);
|
|
103
|
+
const IN_PROGRESS = new Set(["◼"]);
|
|
104
|
+
const PENDING = new Set(["◻", "☐"]);
|
|
105
|
+
const ANCHOR = "⎿";
|
|
106
|
+
function markerOf(line) {
|
|
107
|
+
let s = line.replace(/^\s+/, "");
|
|
108
|
+
if (s.startsWith(ANCHOR)) s = s.slice(1).replace(/^\s+/, "");
|
|
109
|
+
const ch = [...s][0];
|
|
110
|
+
if (ch === void 0) return null;
|
|
111
|
+
if (DONE.has(ch)) return "done";
|
|
112
|
+
if (IN_PROGRESS.has(ch)) return "inprogress";
|
|
113
|
+
if (PENDING.has(ch)) return "pending";
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Find the MOST RECENT confidently-detected todo block in the rendered lines and
|
|
118
|
+
* return its {done, total}. Returns null when none qualifies (caller omits the
|
|
119
|
+
* badge entirely — never shows "0/0").
|
|
120
|
+
*
|
|
121
|
+
* A block is a maximal run of consecutive marker lines. It only counts when it
|
|
122
|
+
* is anchored — the `⎿` glyph appears on the run's first line or the line
|
|
123
|
+
* directly above it — and has ≥2 marker lines. The last qualifying block wins,
|
|
124
|
+
* since the agent's current todo state is the one drawn most recently.
|
|
125
|
+
*/
|
|
126
|
+
function parseTaskCounts(lines) {
|
|
127
|
+
let best = null;
|
|
128
|
+
const n = lines.length;
|
|
129
|
+
let i = 0;
|
|
130
|
+
while (i < n) {
|
|
131
|
+
if (markerOf(lines[i]) === null) {
|
|
132
|
+
i++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
let hasAnchor = i > 0 && lines[i - 1].includes(ANCHOR);
|
|
136
|
+
const counts = {
|
|
137
|
+
done: 0,
|
|
138
|
+
inprogress: 0,
|
|
139
|
+
pending: 0
|
|
140
|
+
};
|
|
141
|
+
let j = i;
|
|
142
|
+
for (; j < n; j++) {
|
|
143
|
+
const mk = markerOf(lines[j]);
|
|
144
|
+
if (mk === null) break;
|
|
145
|
+
if (lines[j].includes(ANCHOR)) hasAnchor = true;
|
|
146
|
+
counts[mk]++;
|
|
147
|
+
}
|
|
148
|
+
const total = counts.done + counts.inprogress + counts.pending;
|
|
149
|
+
if (hasAnchor && total >= 2) best = {
|
|
150
|
+
done: counts.done,
|
|
151
|
+
total
|
|
152
|
+
};
|
|
153
|
+
i = j === i ? i + 1 : j;
|
|
154
|
+
}
|
|
155
|
+
return best;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region ts/needsInput.ts
|
|
160
|
+
function reTest(re, s) {
|
|
161
|
+
return (re.global || re.sticky ? new RegExp(re.source, re.flags.replace(/[gy]/g, "")) : re).test(s);
|
|
162
|
+
}
|
|
163
|
+
function isChromeLine(s) {
|
|
164
|
+
const t = s.trim();
|
|
165
|
+
return !t || /^─+$/.test(t) || /^esc to (interrupt|cancel)/i.test(t) || /\? for shortcuts/.test(t) || /\d+%\s*until auto-compact/i.test(t);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Returns a NeedsInput when the screen shows an unresolved selection menu, else
|
|
169
|
+
* null. `cfg.working` short-circuits to null (an actively-working agent isn't
|
|
170
|
+
* blocked). Pure + synchronous so it's trivially unit-testable.
|
|
171
|
+
*/
|
|
172
|
+
function classifyNeedsInput(lines, cfg) {
|
|
173
|
+
const patterns = cfg.needsInput ?? [];
|
|
174
|
+
if (patterns.length === 0) return null;
|
|
175
|
+
const text = lines.join("\n");
|
|
176
|
+
if ((cfg.working ?? []).some((re) => reTest(re, text))) return null;
|
|
177
|
+
if (!patterns.some((re) => reTest(re, text))) return null;
|
|
178
|
+
let last = -1;
|
|
179
|
+
for (let i = 0; i < lines.length; i++) if (patterns.some((re) => reTest(re, lines[i]))) last = i;
|
|
180
|
+
const start = Math.max(0, last - 6);
|
|
181
|
+
const end = Math.min(lines.length, last + 6);
|
|
182
|
+
return { question: lines.slice(start, end).map((l) => l.trim()).filter((l) => l && !isChromeLine(l)).join(" • ").slice(0, 400) };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region ts/lsWatch.ts
|
|
187
|
+
/**
|
|
188
|
+
* Diff the previous per-pid states against the current snapshot and return the
|
|
189
|
+
* transition events to emit plus the next prev-map. Pure: no I/O, no clock —
|
|
190
|
+
* the caller passes `ts`.
|
|
191
|
+
*
|
|
192
|
+
* Emits an event when:
|
|
193
|
+
* - an agent is seen for the first time (baseline, `prev_state: null`)
|
|
194
|
+
* - its `state` or `question` changed since last tick
|
|
195
|
+
* - it vanished from the live set without us ever seeing it `stopped` (reaped
|
|
196
|
+
* between ticks) — a synthetic `stopped` event so a "done" transition is
|
|
197
|
+
* never silently dropped.
|
|
198
|
+
*/
|
|
199
|
+
function diffLsStates(prev, cur, ts) {
|
|
200
|
+
const events = [];
|
|
201
|
+
const next = /* @__PURE__ */ new Map();
|
|
202
|
+
const curPids = /* @__PURE__ */ new Set();
|
|
203
|
+
for (const a of cur) {
|
|
204
|
+
curPids.add(a.pid);
|
|
205
|
+
next.set(a.pid, a);
|
|
206
|
+
const p = prev.get(a.pid);
|
|
207
|
+
if (!p) events.push({
|
|
208
|
+
...toEvent(a, ts),
|
|
209
|
+
prev_state: null
|
|
210
|
+
});
|
|
211
|
+
else if (p.state !== a.state || p.question !== a.question) events.push({
|
|
212
|
+
...toEvent(a, ts),
|
|
213
|
+
prev_state: p.state
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
for (const [pid, p] of prev) if (!curPids.has(pid) && p.state !== "stopped") events.push({
|
|
217
|
+
ts,
|
|
218
|
+
pid,
|
|
219
|
+
cli: p.cli,
|
|
220
|
+
cwd: p.cwd,
|
|
221
|
+
state: "stopped",
|
|
222
|
+
question: null,
|
|
223
|
+
prev_state: p.state
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
226
|
+
events,
|
|
227
|
+
next
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function toEvent(a, ts) {
|
|
231
|
+
return {
|
|
232
|
+
ts,
|
|
233
|
+
pid: a.pid,
|
|
234
|
+
cli: a.cli,
|
|
235
|
+
cwd: a.cwd,
|
|
236
|
+
state: a.state,
|
|
237
|
+
question: a.question
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region ts/resultEnvelope.ts
|
|
243
|
+
/**
|
|
244
|
+
* Structured result envelope — P4 of the orchestrator-observability work.
|
|
245
|
+
*
|
|
246
|
+
* A fan-out parent that spawned a sub-agent wants its *outcome* (branch, commit
|
|
247
|
+
* SHAs, changed files, status, blockers, a summary) as machine-readable data,
|
|
248
|
+
* not by grepping `ay tail`. This is the agent-yes analog of an in-harness
|
|
249
|
+
* Agent tool's `<result>` block. The sub-agent deposits one JSON envelope when
|
|
250
|
+
* it finishes; the parent pulls it with `ay result <keyword>`.
|
|
251
|
+
*
|
|
252
|
+
* Why a PERSISTED file, not a query-time screen scrape (the model that
|
|
253
|
+
* `needs_input`/activity use): a completion record is read AFTER the agent is
|
|
254
|
+
* done — exactly when its rendered screen is gone and its log may be reaped. It
|
|
255
|
+
* must outlive the process, so it is written once to
|
|
256
|
+
* `$AGENT_YES_HOME/results/<pid>.json` and read back verbatim. It is keyed by
|
|
257
|
+
* the wrapper pid the agent already knows via the injected `AGENT_YES_PID` env
|
|
258
|
+
* var, so depositing needs no new spawn-time wiring in either runtime.
|
|
259
|
+
*
|
|
260
|
+
* This module is the pure, fs-free core (path math + input normalization) so it
|
|
261
|
+
* is trivially unit-testable, mirroring `lsWatch.ts` / `needsInput.ts`. The fs
|
|
262
|
+
* read/write + CLI live in `subcommands.ts` (`cmdResult`).
|
|
263
|
+
*/
|
|
264
|
+
/** Directory holding the per-pid result files. */
|
|
265
|
+
function resultsDir() {
|
|
266
|
+
return path.join(agentYesHome(), "results");
|
|
267
|
+
}
|
|
268
|
+
/** Absolute path of one agent's result envelope. */
|
|
269
|
+
function resultPath(pid) {
|
|
270
|
+
return path.join(resultsDir(), `${pid}.json`);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Coerce raw write-side input into an envelope payload. If it parses as JSON we
|
|
274
|
+
* keep it as-is (object, array, or scalar — the agent owns the shape). If it
|
|
275
|
+
* does NOT parse, we don't reject: a bare string is a perfectly good summary, so
|
|
276
|
+
* we wrap it as `{ summary }`. Empty / whitespace-only input is an error the
|
|
277
|
+
* caller should surface (returns null).
|
|
278
|
+
*/
|
|
279
|
+
function normalizeEnvelope(raw) {
|
|
280
|
+
const trimmed = raw.trim();
|
|
281
|
+
if (trimmed.length === 0) return null;
|
|
282
|
+
try {
|
|
283
|
+
return JSON.parse(trimmed);
|
|
284
|
+
} catch {
|
|
285
|
+
return { summary: trimmed };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** Wrap a normalized payload with correlation metadata for persistence. */
|
|
289
|
+
function buildStoredResult(pid, result, writtenAt) {
|
|
290
|
+
return {
|
|
291
|
+
pid,
|
|
292
|
+
written_at: writtenAt,
|
|
293
|
+
result
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
//#endregion
|
|
9
298
|
//#region ts/subcommands.ts
|
|
10
299
|
/**
|
|
11
300
|
* `ay ls / read / cat / tail / head / send` subcommand implementations.
|
|
@@ -189,6 +478,7 @@ const SUBCOMMANDS = new Set([
|
|
|
189
478
|
"list",
|
|
190
479
|
"ps",
|
|
191
480
|
"status",
|
|
481
|
+
"result",
|
|
192
482
|
"read",
|
|
193
483
|
"cat",
|
|
194
484
|
"tail",
|
|
@@ -223,6 +513,7 @@ async function runSubcommand(argv) {
|
|
|
223
513
|
case "list":
|
|
224
514
|
case "ps": return await cmdLs(rest);
|
|
225
515
|
case "status": return await cmdStatus(rest);
|
|
516
|
+
case "result": return await cmdResult(rest);
|
|
226
517
|
case "read":
|
|
227
518
|
case "cat": return await cmdRead(rest, { mode: "cat" });
|
|
228
519
|
case "tail": return await cmdRead(rest, { mode: "tail" });
|
|
@@ -233,23 +524,23 @@ async function runSubcommand(argv) {
|
|
|
233
524
|
case "restart": return await cmdRestart(rest);
|
|
234
525
|
case "note": return await cmdNote(rest);
|
|
235
526
|
case "serve": {
|
|
236
|
-
const { cmdServe } = await import("./serve-
|
|
527
|
+
const { cmdServe } = await import("./serve-r_2v9EKc.js");
|
|
237
528
|
return cmdServe(rest);
|
|
238
529
|
}
|
|
239
530
|
case "setup": {
|
|
240
|
-
const { cmdSetup } = await import("./setup-
|
|
531
|
+
const { cmdSetup } = await import("./setup-DHa6fX8M.js");
|
|
241
532
|
return cmdSetup(rest);
|
|
242
533
|
}
|
|
243
534
|
case "schedule": {
|
|
244
|
-
const { cmdSchedule } = await import("./schedule-
|
|
535
|
+
const { cmdSchedule } = await import("./schedule-DULdIkU9.js");
|
|
245
536
|
return cmdSchedule(rest);
|
|
246
537
|
}
|
|
247
538
|
case "remote": {
|
|
248
|
-
const { cmdRemote } = await import("./remotes-
|
|
539
|
+
const { cmdRemote } = await import("./remotes-BRCDVnR7.js");
|
|
249
540
|
return cmdRemote(rest);
|
|
250
541
|
}
|
|
251
542
|
case "reap":
|
|
252
|
-
await (await import("./reaper-
|
|
543
|
+
await (await import("./reaper-BLVA780B.js")).sweep();
|
|
253
544
|
return 0;
|
|
254
545
|
case "help": return cmdHelp();
|
|
255
546
|
default: return null;
|
|
@@ -261,7 +552,7 @@ async function runSubcommand(argv) {
|
|
|
261
552
|
}
|
|
262
553
|
}
|
|
263
554
|
function cmdHelp() {
|
|
264
|
-
process.stdout.write("ay - agent-yes CLI\n\nManagement:\n ay ls [keyword] list running agents\n ay tail [-f] <keyword> stream output (Ctrl-C to stop)\n ay cat <keyword> full log\n ay head <keyword> first N lines\n ay send <keyword> <msg> send a message\n ay attach <keyword> interactive attach (detach: Ctrl-\\)\n ay stop <keyword> graceful shutdown (/exit for claude/codex)\n ay status <keyword> agent status snapshot\n ay reap kill process groups leaked by dead agents\n\nRemote:\n ay setup guided setup: pick a workspace, share to agent-yes.com\n ay schedule <when> <cli> -- <msg> run an agent on a schedule (HH:MM or cron)\n ay serve [--port N] start HTTP API server (prints token)\n ay serve status show serve daemon/server status\n ay remote add <alias> http://<token>@<host>:<port>\n ay remote ls / rm <alias> manage saved remotes\n ay ls <token>@<host>:<port> connect inline (no alias needed)\n ay send <token>@<host>:<port>:<kw> <msg>\n\nRun an agent:\n ay [claude|codex|gemini|...] [options] -- [prompt]\n ay claude -- \"fix the bug in auth.ts\"\n ay claude --help full agent-runner options\n\nLabs (examples at https://github.com/snomiao/agent-yes/tree/main/lab):\n local-role-play/ designer + builder on one machine\n http-remote/ ay serve remote access demo\n p2p-pairing/ libp2p P2P (needs: cargo build --features swarm)\n");
|
|
555
|
+
process.stdout.write("ay - agent-yes CLI\n\nManagement:\n ay ls [keyword] list running agents\n ay tail [-f] <keyword> stream output (Ctrl-C to stop)\n ay cat <keyword> full log\n ay head <keyword> first N lines\n ay send <keyword> <msg> send a message\n ay attach <keyword> interactive attach (detach: Ctrl-\\)\n ay stop <keyword> graceful shutdown (/exit for claude/codex)\n ay status <keyword> agent status snapshot\n ay result <keyword> [--wait] pull an agent's structured result envelope\n ay result set '<json>' (inside an agent) deposit your result envelope\n ay reap kill process groups leaked by dead agents\n\nRemote:\n ay setup guided setup: pick a workspace, share to agent-yes.com\n ay schedule <when> <cli> -- <msg> run an agent on a schedule (HH:MM or cron)\n ay serve [--port N] start HTTP API server (prints token)\n ay serve status show serve daemon/server status\n ay remote add <alias> http://<token>@<host>:<port>\n ay remote ls / rm <alias> manage saved remotes\n ay ls <token>@<host>:<port> connect inline (no alias needed)\n ay send <token>@<host>:<port>:<kw> <msg>\n\nRun an agent:\n ay [claude|codex|gemini|...] [options] -- [prompt]\n ay claude -- \"fix the bug in auth.ts\"\n ay claude --help full agent-runner options\n\nLabs (examples at https://github.com/snomiao/agent-yes/tree/main/lab):\n local-role-play/ designer + builder on one machine\n http-remote/ ay serve remote access demo\n p2p-pairing/ libp2p P2P (needs: cargo build --features swarm)\n");
|
|
265
556
|
return 0;
|
|
266
557
|
}
|
|
267
558
|
function matchKeyword(record, keyword) {
|
|
@@ -629,6 +920,35 @@ async function runAllRemotesLs(opts) {
|
|
|
629
920
|
}
|
|
630
921
|
return 0;
|
|
631
922
|
}
|
|
923
|
+
/**
|
|
924
|
+
* The live display state of one agent: stopped (exited) / idle (alive+quiet) /
|
|
925
|
+
* active (alive+recent output) / needs_input (alive but parked on an unanswered
|
|
926
|
+
* menu). Shared by the `ay ls` human table AND its `--json` output so both report
|
|
927
|
+
* needs_input identically — an orchestrator parsing `ay ls --json` is the primary
|
|
928
|
+
* consumer.
|
|
929
|
+
*/
|
|
930
|
+
async function deriveLiveState(r) {
|
|
931
|
+
if (!isPidAlive(r.pid)) return {
|
|
932
|
+
state: "stopped",
|
|
933
|
+
question: null
|
|
934
|
+
};
|
|
935
|
+
let state = "active";
|
|
936
|
+
if (r.log_file) {
|
|
937
|
+
const mtime = await stat(r.log_file).then((s) => s.mtimeMs).catch(() => null);
|
|
938
|
+
state = mtime !== null && Date.now() - mtime > IDLE_THRESHOLD_MS ? "idle" : "active";
|
|
939
|
+
}
|
|
940
|
+
if (r.log_file) {
|
|
941
|
+
const ni = await extractNeedsInput(r.log_file, r.cli);
|
|
942
|
+
if (ni) return {
|
|
943
|
+
state: "needs_input",
|
|
944
|
+
question: ni.question
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
state,
|
|
949
|
+
question: null
|
|
950
|
+
};
|
|
951
|
+
}
|
|
632
952
|
async function cmdLs(rest) {
|
|
633
953
|
const y = yargs(rest).usage("Usage: ay ls [keyword] [options]\n ay list [keyword] [options]\n ay ps [keyword] [options]\n\nList running agents. Optionally filter by keyword (pid, cwd substring, or prompt substring).").option("all", {
|
|
634
954
|
type: "boolean",
|
|
@@ -642,6 +962,15 @@ async function cmdLs(rest) {
|
|
|
642
962
|
type: "boolean",
|
|
643
963
|
default: false,
|
|
644
964
|
description: "Output as JSON array"
|
|
965
|
+
}).option("watch", {
|
|
966
|
+
alias: "w",
|
|
967
|
+
type: "boolean",
|
|
968
|
+
default: false,
|
|
969
|
+
description: "Stream agent state transitions (needs_input | idle | active | stopped) as NDJSON across all matched agents — one event stream for a whole fan-out, instead of N per-pid `ay status --watch`es. Runs until Ctrl-C."
|
|
970
|
+
}).option("interval", {
|
|
971
|
+
type: "number",
|
|
972
|
+
default: 2,
|
|
973
|
+
description: "Poll interval in seconds (--watch)"
|
|
645
974
|
}).option("latest", {
|
|
646
975
|
type: "boolean",
|
|
647
976
|
default: false,
|
|
@@ -658,7 +987,7 @@ async function cmdLs(rest) {
|
|
|
658
987
|
type: "boolean",
|
|
659
988
|
default: false,
|
|
660
989
|
description: "Show this help"
|
|
661
|
-
}).example("ay ls", "list running agents").example("ay ls --all-remotes", "include all configured remote machines").example("ay ls --all", "include exited agents").example("ay ls --json", "machine-readable output").example("ay ls symval", "filter by cwd/prompt keyword").help(false).version(false).exitProcess(false);
|
|
990
|
+
}).example("ay ls", "list running agents").example("ay ls --all-remotes", "include all configured remote machines").example("ay ls --all", "include exited agents").example("ay ls --json", "machine-readable output").example("ay ls --watch", "stream state transitions for a whole fan-out as NDJSON").example("ay ls symval", "filter by cwd/prompt keyword").help(false).version(false).exitProcess(false);
|
|
662
991
|
const argv = await y.parseAsync();
|
|
663
992
|
if (argv.help || argv.h) {
|
|
664
993
|
process.stdout.write(await y.getHelp() + "\n");
|
|
@@ -684,9 +1013,45 @@ async function cmdLs(rest) {
|
|
|
684
1013
|
latest: argv.latest,
|
|
685
1014
|
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null
|
|
686
1015
|
};
|
|
1016
|
+
if (argv.watch) {
|
|
1017
|
+
const intervalMs = Math.max(500, (Number.isFinite(argv.interval) ? argv.interval : 2) * 1e3);
|
|
1018
|
+
process.stderr.write(`watching agents every ${intervalMs / 1e3}s… (Ctrl-C to stop)\n`);
|
|
1019
|
+
let prev = /* @__PURE__ */ new Map();
|
|
1020
|
+
const tick = async () => {
|
|
1021
|
+
const recs = await listRecords(keyword, opts);
|
|
1022
|
+
const cur = await Promise.all(recs.map(async (r) => {
|
|
1023
|
+
const { state, question } = await deriveLiveState(r);
|
|
1024
|
+
return {
|
|
1025
|
+
pid: r.pid,
|
|
1026
|
+
cli: r.cli,
|
|
1027
|
+
cwd: r.cwd,
|
|
1028
|
+
state,
|
|
1029
|
+
question
|
|
1030
|
+
};
|
|
1031
|
+
}));
|
|
1032
|
+
const { events, next } = diffLsStates(prev, cur, Date.now());
|
|
1033
|
+
for (const e of events) process.stdout.write(JSON.stringify(e) + "\n");
|
|
1034
|
+
prev = next;
|
|
1035
|
+
};
|
|
1036
|
+
await tick();
|
|
1037
|
+
await new Promise((resolve) => {
|
|
1038
|
+
const timer = setInterval(() => {
|
|
1039
|
+
tick();
|
|
1040
|
+
}, intervalMs);
|
|
1041
|
+
process.on("SIGINT", () => {
|
|
1042
|
+
clearInterval(timer);
|
|
1043
|
+
resolve();
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
return 0;
|
|
1047
|
+
}
|
|
687
1048
|
const records = await listRecords(keyword, opts);
|
|
688
1049
|
if (opts.json) {
|
|
689
|
-
|
|
1050
|
+
const enriched = await Promise.all(records.map(async (r) => ({
|
|
1051
|
+
...r,
|
|
1052
|
+
...await deriveLiveState(r)
|
|
1053
|
+
})));
|
|
1054
|
+
process.stdout.write(JSON.stringify(enriched, null, 2) + "\n");
|
|
690
1055
|
return 0;
|
|
691
1056
|
}
|
|
692
1057
|
if (records.length === 0) {
|
|
@@ -704,22 +1069,22 @@ async function cmdLs(rest) {
|
|
|
704
1069
|
};
|
|
705
1070
|
const fixedWidth = widths.pid + widths.cli + widths.status + widths.age + widths.cwd + 10;
|
|
706
1071
|
const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
|
|
1072
|
+
const forestRows = flattenForest(buildAgentForest(records));
|
|
707
1073
|
const notes = await readNotes();
|
|
708
|
-
const rows = await Promise.all(
|
|
709
|
-
|
|
710
|
-
if (!isPidAlive(r.pid)) displayStatus = "stopped";
|
|
711
|
-
else if (r.log_file) {
|
|
712
|
-
const mtime = await stat(r.log_file).then((s) => s.mtimeMs).catch(() => null);
|
|
713
|
-
displayStatus = mtime !== null && Date.now() - mtime > IDLE_THRESHOLD_MS ? "idle" : "active";
|
|
714
|
-
} else displayStatus = "active";
|
|
1074
|
+
const rows = await Promise.all(forestRows.map(async ({ record: r, prefix }) => {
|
|
1075
|
+
const displayStatus = (await deriveLiveState(r)).state;
|
|
715
1076
|
const note = notes.get(r.pid);
|
|
1077
|
+
const tasks = displayStatus !== "stopped" && r.log_file ? await extractTaskCounts(r.log_file) : null;
|
|
1078
|
+
const badge = tasks ? `${tasks.done}/${tasks.total} ` : "";
|
|
1079
|
+
const budget = Math.max(8, promptBudget - prefix.length - badge.length);
|
|
716
1080
|
let label;
|
|
717
1081
|
let hasNote = false;
|
|
718
1082
|
if (note) {
|
|
719
|
-
label = truncate(note,
|
|
1083
|
+
label = truncate(note, budget);
|
|
720
1084
|
hasNote = true;
|
|
721
|
-
} else if (r.log_file && displayStatus !== "stopped") label = truncate(await extractActivity(r.log_file) ?? (r.prompt ? `→ ${r.prompt}` : ""),
|
|
722
|
-
else label = truncate(r.prompt ? `→ ${r.prompt}` : "",
|
|
1085
|
+
} else if (r.log_file && displayStatus !== "stopped") label = truncate(await extractActivity(r.log_file) ?? (r.prompt ? `→ ${r.prompt}` : ""), budget);
|
|
1086
|
+
else label = truncate(r.prompt ? `→ ${r.prompt}` : "", budget);
|
|
1087
|
+
label = prefix + (hasNote ? "* " : "") + badge + label;
|
|
723
1088
|
return {
|
|
724
1089
|
pid: String(r.pid),
|
|
725
1090
|
cli: r.cli,
|
|
@@ -746,16 +1111,16 @@ async function cmdLs(rest) {
|
|
|
746
1111
|
r.status.padEnd(widths.status),
|
|
747
1112
|
r.age.padEnd(widths.age),
|
|
748
1113
|
r.cwd.padEnd(widths.cwd),
|
|
749
|
-
r.
|
|
1114
|
+
r.label
|
|
750
1115
|
].join(" ") + "\n");
|
|
751
1116
|
if (!opts.json && rows.length > 0) {
|
|
752
1117
|
const alive = rows.find((r) => r._alive);
|
|
753
1118
|
const stopped = rows.find((r) => !r._alive);
|
|
754
1119
|
const hints = ["\n"];
|
|
755
1120
|
if (alive) {
|
|
756
|
-
hints.push(` ay status ${alive.pid} # JSON status snapshot\n`);
|
|
757
|
-
hints.push(` ay status ${alive.pid} --watch # stream changes as JSON\n`);
|
|
758
|
-
hints.push(` ay status ${alive.pid} --wait
|
|
1121
|
+
hints.push(` ay status ${alive.pid} # JSON status snapshot (+ question)\n`);
|
|
1122
|
+
hints.push(` ay status ${alive.pid} --watch # stream state changes as JSON\n`);
|
|
1123
|
+
hints.push(` ay status ${alive.pid} --wait # block until it needs you (needs_input|idle|stopped)\n`);
|
|
759
1124
|
hints.push(` ay tail ${alive.pid} # view latest output\n`);
|
|
760
1125
|
hints.push(` ay tail -f ${alive.pid} # follow live output\n`);
|
|
761
1126
|
hints.push(` ay send ${alive.pid} "next: ..." # send a prompt (keyword: pid, cwd, or prompt substring)\n`);
|
|
@@ -1095,6 +1460,91 @@ async function extractActivity(logPath) {
|
|
|
1095
1460
|
return null;
|
|
1096
1461
|
}
|
|
1097
1462
|
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Extract the agent's current task progress ({done,total}) from its rendered TUI
|
|
1465
|
+
* screen — works for every CLI since the source is the drawn todo block, not a
|
|
1466
|
+
* CLI-specific session file. Reads a generous tail (the latest todo block can be
|
|
1467
|
+
* scrolled well back from the very last lines), renders the whole window through
|
|
1468
|
+
* xterm so reflow/redraw frames collapse to coherent text, then scans for the
|
|
1469
|
+
* most recent ⎿-anchored block. Returns null when none is confidently detected.
|
|
1470
|
+
*/
|
|
1471
|
+
async function extractTaskCounts(logPath) {
|
|
1472
|
+
const TAIL_BYTES = 256 * 1024;
|
|
1473
|
+
let buf;
|
|
1474
|
+
try {
|
|
1475
|
+
const fh = await open(logPath, "r");
|
|
1476
|
+
try {
|
|
1477
|
+
const { size } = await fh.stat();
|
|
1478
|
+
if (size === 0) return null;
|
|
1479
|
+
if (size <= TAIL_BYTES) {
|
|
1480
|
+
const data = await fh.readFile();
|
|
1481
|
+
buf = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
1482
|
+
} else {
|
|
1483
|
+
const tmp = Buffer.alloc(TAIL_BYTES);
|
|
1484
|
+
const { bytesRead } = await fh.read(tmp, 0, TAIL_BYTES, size - TAIL_BYTES);
|
|
1485
|
+
buf = new Uint8Array(tmp.buffer, 0, bytesRead);
|
|
1486
|
+
}
|
|
1487
|
+
} finally {
|
|
1488
|
+
await fh.close();
|
|
1489
|
+
}
|
|
1490
|
+
} catch {
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
try {
|
|
1494
|
+
return parseTaskCounts((await renderRawLog(buf, {
|
|
1495
|
+
mode: "cat",
|
|
1496
|
+
n: 0
|
|
1497
|
+
})).split("\n"));
|
|
1498
|
+
} catch {
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
let _cliDefaults = null;
|
|
1503
|
+
function cliDefaults() {
|
|
1504
|
+
return _cliDefaults ??= loadSharedCliDefaults().catch(() => ({}));
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Detect whether the agent is blocked on an interactive selection menu it didn't
|
|
1508
|
+
* auto-resolve (state `needs_input`). Reads the same 32 KB tail as extractActivity
|
|
1509
|
+
* and renders it through xterm, then runs the CLI's `needsInput`/`working`
|
|
1510
|
+
* patterns. Returns null when no menu is detected (or the CLI defines none).
|
|
1511
|
+
*/
|
|
1512
|
+
async function extractNeedsInput(logPath, cli) {
|
|
1513
|
+
const cfg = (await cliDefaults())[cli];
|
|
1514
|
+
if (!cfg?.needsInput?.length) return null;
|
|
1515
|
+
const TAIL_BYTES = 32 * 1024;
|
|
1516
|
+
let buf;
|
|
1517
|
+
try {
|
|
1518
|
+
const fh = await open(logPath, "r");
|
|
1519
|
+
try {
|
|
1520
|
+
const { size } = await fh.stat();
|
|
1521
|
+
if (size === 0) return null;
|
|
1522
|
+
if (size <= TAIL_BYTES) {
|
|
1523
|
+
const data = await fh.readFile();
|
|
1524
|
+
buf = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
1525
|
+
} else {
|
|
1526
|
+
const tmp = Buffer.alloc(TAIL_BYTES);
|
|
1527
|
+
const { bytesRead } = await fh.read(tmp, 0, TAIL_BYTES, size - TAIL_BYTES);
|
|
1528
|
+
buf = new Uint8Array(tmp.buffer, 0, bytesRead);
|
|
1529
|
+
}
|
|
1530
|
+
} finally {
|
|
1531
|
+
await fh.close();
|
|
1532
|
+
}
|
|
1533
|
+
} catch {
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
try {
|
|
1537
|
+
return classifyNeedsInput((await renderRawLog(buf, {
|
|
1538
|
+
mode: "tail",
|
|
1539
|
+
n: 40
|
|
1540
|
+
})).split("\n"), {
|
|
1541
|
+
needsInput: cfg.needsInput,
|
|
1542
|
+
working: cfg.working
|
|
1543
|
+
});
|
|
1544
|
+
} catch {
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1098
1548
|
function extractActivityFromLines(lines) {
|
|
1099
1549
|
const isChrome = (l) => {
|
|
1100
1550
|
const s = l.trim();
|
|
@@ -1261,8 +1711,8 @@ async function writeToIpc(ipcPath, payload) {
|
|
|
1261
1711
|
});
|
|
1262
1712
|
});
|
|
1263
1713
|
} else {
|
|
1264
|
-
const { openSync, writeFileSync, closeSync } = await import("fs");
|
|
1265
|
-
const fd = openSync(ipcPath,
|
|
1714
|
+
const { openSync, writeFileSync, closeSync, constants } = await import("fs");
|
|
1715
|
+
const fd = openSync(ipcPath, constants.O_WRONLY | constants.O_NONBLOCK);
|
|
1266
1716
|
try {
|
|
1267
1717
|
writeFileSync(fd, payload);
|
|
1268
1718
|
} finally {
|
|
@@ -1297,6 +1747,14 @@ async function cmdStop(rest) {
|
|
|
1297
1747
|
const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
|
|
1298
1748
|
if (!keyword) throw new Error("usage: ay stop <keyword> [--method=auto|graceful|double-ctrl-c]");
|
|
1299
1749
|
const record = await resolveOne(keyword, opts);
|
|
1750
|
+
if (!isPidAlive(record.pid)) {
|
|
1751
|
+
await updateGlobalPidStatus(record.pid, {
|
|
1752
|
+
status: "exited",
|
|
1753
|
+
exit_reason: "already-stopped"
|
|
1754
|
+
}).catch(() => {});
|
|
1755
|
+
process.stdout.write(`pid ${record.pid} (${record.cli}) already stopped — marked exited\n`);
|
|
1756
|
+
return 0;
|
|
1757
|
+
}
|
|
1300
1758
|
if (!record.fifo_file) throw new Error(`pid ${record.pid}: no fifo_file — cannot send shutdown command`);
|
|
1301
1759
|
const method = String(argv.method).toLowerCase();
|
|
1302
1760
|
const graceful = GRACEFUL_EXIT_COMMANDS[record.cli];
|
|
@@ -1572,6 +2030,14 @@ async function snapshotStatus(record) {
|
|
|
1572
2030
|
state = logMtimeMs !== null && Date.now() - logMtimeMs > IDLE_THRESHOLD_MS ? "idle" : "active";
|
|
1573
2031
|
} else state = "active";
|
|
1574
2032
|
const activity = state !== "stopped" && record.log_file ? await extractActivity(record.log_file) : null;
|
|
2033
|
+
let question = null;
|
|
2034
|
+
if (state !== "stopped" && record.log_file) {
|
|
2035
|
+
const ni = await extractNeedsInput(record.log_file, record.cli);
|
|
2036
|
+
if (ni) {
|
|
2037
|
+
state = "needs_input";
|
|
2038
|
+
question = ni.question;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
1575
2041
|
const note = (await readNotes()).get(record.pid) ?? null;
|
|
1576
2042
|
return {
|
|
1577
2043
|
pid: record.pid,
|
|
@@ -1579,6 +2045,7 @@ async function snapshotStatus(record) {
|
|
|
1579
2045
|
cwd: record.cwd,
|
|
1580
2046
|
state,
|
|
1581
2047
|
activity,
|
|
2048
|
+
question,
|
|
1582
2049
|
note,
|
|
1583
2050
|
log_mtime_ms: logMtimeMs,
|
|
1584
2051
|
started_at: record.started_at,
|
|
@@ -1594,13 +2061,17 @@ async function cmdStatus(rest) {
|
|
|
1594
2061
|
type: "boolean",
|
|
1595
2062
|
default: false,
|
|
1596
2063
|
description: "Stream changes as JSON"
|
|
2064
|
+
}).option("wait", {
|
|
2065
|
+
type: "boolean",
|
|
2066
|
+
default: false,
|
|
2067
|
+
description: "Block until the agent needs attention (needs_input | idle | stopped), then emit it. Exit 0 reached, 2 timeout. The JSON `state` says which — this is the primitive an orchestrator wants: it returns on a blocking question, not just on done."
|
|
1597
2068
|
}).option("wait-idle", {
|
|
1598
2069
|
type: "boolean",
|
|
1599
2070
|
default: false,
|
|
1600
|
-
description: "Block until state == idle. Exit 0 idle, 1 stopped, 2 timeout"
|
|
2071
|
+
description: "Block until state == idle. Exit 0 idle, 1 stopped, 2 timeout. Does NOT return on needs_input (a blocked menu) — use --wait for that."
|
|
1601
2072
|
}).option("timeout", {
|
|
1602
2073
|
type: "string",
|
|
1603
|
-
description: "Timeout for --wait-idle (e.g. 30s, 5m). Default: no timeout"
|
|
2074
|
+
description: "Timeout for --wait/--wait-idle (e.g. 30s, 5m). Default: no timeout"
|
|
1604
2075
|
}).option("interval", {
|
|
1605
2076
|
type: "number",
|
|
1606
2077
|
default: 2,
|
|
@@ -1621,12 +2092,13 @@ async function cmdStatus(rest) {
|
|
|
1621
2092
|
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null
|
|
1622
2093
|
};
|
|
1623
2094
|
const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
|
|
1624
|
-
if (!keyword) throw new Error("usage: ay status <keyword> [--watch | --wait-idle] [--timeout=Ns]");
|
|
2095
|
+
if (!keyword) throw new Error("usage: ay status <keyword> [--watch | --wait | --wait-idle] [--timeout=Ns]");
|
|
1625
2096
|
{
|
|
1626
2097
|
const remote = await resolveRemoteSpec(keyword);
|
|
1627
2098
|
if (remote) return runRemoteStatus(remote);
|
|
1628
2099
|
}
|
|
1629
2100
|
const watch = argv.watch;
|
|
2101
|
+
const wait = argv.wait;
|
|
1630
2102
|
const waitIdle = argv["wait-idle"];
|
|
1631
2103
|
const intervalFlag = argv.interval;
|
|
1632
2104
|
const intervalMs = Math.max(500, (Number.isFinite(intervalFlag) ? intervalFlag : 2) * 1e3);
|
|
@@ -1640,6 +2112,21 @@ async function cmdStatus(rest) {
|
|
|
1640
2112
|
} : snap;
|
|
1641
2113
|
process.stdout.write(JSON.stringify(out) + "\n");
|
|
1642
2114
|
};
|
|
2115
|
+
if (wait) {
|
|
2116
|
+
const startedAt = Date.now();
|
|
2117
|
+
for (;;) {
|
|
2118
|
+
const snap = await snapshotStatus(record);
|
|
2119
|
+
if (snap.state === "needs_input" || snap.state === "idle" || snap.state === "stopped") {
|
|
2120
|
+
emit(snap);
|
|
2121
|
+
return 0;
|
|
2122
|
+
}
|
|
2123
|
+
if (timeoutMs !== null && Date.now() - startedAt >= timeoutMs) {
|
|
2124
|
+
emit(snap);
|
|
2125
|
+
return 2;
|
|
2126
|
+
}
|
|
2127
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
1643
2130
|
if (waitIdle) {
|
|
1644
2131
|
const startedAt = Date.now();
|
|
1645
2132
|
for (;;) {
|
|
@@ -1667,11 +2154,12 @@ async function cmdStatus(rest) {
|
|
|
1667
2154
|
let prev = null;
|
|
1668
2155
|
const tick = async () => {
|
|
1669
2156
|
const snap = await snapshotStatus(record);
|
|
1670
|
-
if (prev === null || snap.state !== prev.state || snap.activity !== prev.activity || snap.exit_code !== prev.exit_code) {
|
|
2157
|
+
if (prev === null || snap.state !== prev.state || snap.activity !== prev.activity || snap.question !== prev.question || snap.exit_code !== prev.exit_code) {
|
|
1671
2158
|
emit(snap, Date.now());
|
|
1672
2159
|
prev = {
|
|
1673
2160
|
state: snap.state,
|
|
1674
2161
|
activity: snap.activity,
|
|
2162
|
+
question: snap.question,
|
|
1675
2163
|
exit_code: snap.exit_code
|
|
1676
2164
|
};
|
|
1677
2165
|
}
|
|
@@ -1686,7 +2174,134 @@ async function cmdStatus(rest) {
|
|
|
1686
2174
|
});
|
|
1687
2175
|
return 0;
|
|
1688
2176
|
}
|
|
2177
|
+
/** Read all of stdin as a UTF-8 string (for `ay result set -` / piped JSON). */
|
|
2178
|
+
async function readStdin() {
|
|
2179
|
+
const chunks = [];
|
|
2180
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
2181
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2182
|
+
}
|
|
2183
|
+
/** Load a persisted result envelope, or null if none has been deposited yet. */
|
|
2184
|
+
async function loadStoredResult(pid) {
|
|
2185
|
+
try {
|
|
2186
|
+
const raw = await readFile(resultPath(pid), "utf8");
|
|
2187
|
+
return JSON.parse(raw);
|
|
2188
|
+
} catch {
|
|
2189
|
+
return null;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* `ay result` — two modes:
|
|
2194
|
+
*
|
|
2195
|
+
* ay result set ['<json>' | -] write side, run BY the agent. Keyed off the
|
|
2196
|
+
* injected AGENT_YES_PID (or --pid N). Stores
|
|
2197
|
+
* the envelope to ~/.agent-yes/results/<pid>.json.
|
|
2198
|
+
*
|
|
2199
|
+
* ay result <keyword> [--wait] read side, run by the parent. Resolves the
|
|
2200
|
+
* agent and emits the stored envelope as JSON.
|
|
2201
|
+
*
|
|
2202
|
+
* Read-side exit codes (so an orchestrator can branch without parsing):
|
|
2203
|
+
* 0 envelope found and emitted
|
|
2204
|
+
* 1 agent stopped WITHOUT depositing one (it's done; there's no result)
|
|
2205
|
+
* 2 no envelope yet AND agent is still alive (pending) / --wait timed out
|
|
2206
|
+
*/
|
|
2207
|
+
async function cmdResult(rest) {
|
|
2208
|
+
if (rest[0] === "set") return await cmdResultSet(rest.slice(1));
|
|
2209
|
+
const argv = await yargs(rest).usage("Usage: ay result <keyword> [--wait] [--timeout Ns]").option("wait", {
|
|
2210
|
+
type: "boolean",
|
|
2211
|
+
default: false,
|
|
2212
|
+
description: "Block until the agent deposits its result envelope (exit 0), or exits without one (exit 1), or --timeout elapses (exit 2)."
|
|
2213
|
+
}).option("timeout", {
|
|
2214
|
+
type: "string",
|
|
2215
|
+
description: "Timeout for --wait (e.g. 30s, 5m)"
|
|
2216
|
+
}).option("interval", {
|
|
2217
|
+
type: "number",
|
|
2218
|
+
default: 2,
|
|
2219
|
+
description: "Poll interval in seconds"
|
|
2220
|
+
}).option("latest", {
|
|
2221
|
+
type: "boolean",
|
|
2222
|
+
default: false,
|
|
2223
|
+
description: "Use most recent match"
|
|
2224
|
+
}).option("cwd", {
|
|
2225
|
+
type: "string",
|
|
2226
|
+
description: "Restrict to agents under this dir"
|
|
2227
|
+
}).help(false).version(false).exitProcess(false).parseAsync();
|
|
2228
|
+
const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
|
|
2229
|
+
if (!keyword) throw new Error("usage: ay result <keyword> [--wait] | ay result set '<json>'");
|
|
2230
|
+
const record = await resolveOne(keyword, {
|
|
2231
|
+
all: true,
|
|
2232
|
+
active: false,
|
|
2233
|
+
json: false,
|
|
2234
|
+
latest: argv.latest,
|
|
2235
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null
|
|
2236
|
+
});
|
|
2237
|
+
const intervalMs = Math.max(500, (Number.isFinite(argv.interval) ? argv.interval : 2) * 1e3);
|
|
2238
|
+
const timeoutMs = typeof argv.timeout === "string" && argv.timeout.length > 0 ? ms(argv.timeout) ?? NaN : null;
|
|
2239
|
+
if (timeoutMs !== null && !Number.isFinite(timeoutMs)) throw new Error(`invalid --timeout value: ${argv.timeout}`);
|
|
2240
|
+
const emitFound = (stored) => {
|
|
2241
|
+
process.stdout.write(JSON.stringify({
|
|
2242
|
+
pid: record.pid,
|
|
2243
|
+
cli: record.cli,
|
|
2244
|
+
cwd: record.cwd,
|
|
2245
|
+
found: true,
|
|
2246
|
+
written_at: stored.written_at,
|
|
2247
|
+
result: stored.result
|
|
2248
|
+
}) + "\n");
|
|
2249
|
+
};
|
|
2250
|
+
const emitMissing = (state) => {
|
|
2251
|
+
process.stdout.write(JSON.stringify({
|
|
2252
|
+
pid: record.pid,
|
|
2253
|
+
cli: record.cli,
|
|
2254
|
+
cwd: record.cwd,
|
|
2255
|
+
found: false,
|
|
2256
|
+
state
|
|
2257
|
+
}) + "\n");
|
|
2258
|
+
};
|
|
2259
|
+
const startedAt = Date.now();
|
|
2260
|
+
for (;;) {
|
|
2261
|
+
const stored = await loadStoredResult(record.pid);
|
|
2262
|
+
if (stored) {
|
|
2263
|
+
emitFound(stored);
|
|
2264
|
+
return 0;
|
|
2265
|
+
}
|
|
2266
|
+
const snap = await snapshotStatus(record);
|
|
2267
|
+
if (snap.state === "stopped") {
|
|
2268
|
+
const last = await loadStoredResult(record.pid);
|
|
2269
|
+
if (last) {
|
|
2270
|
+
emitFound(last);
|
|
2271
|
+
return 0;
|
|
2272
|
+
}
|
|
2273
|
+
emitMissing("stopped");
|
|
2274
|
+
return 1;
|
|
2275
|
+
}
|
|
2276
|
+
if (!argv.wait) {
|
|
2277
|
+
emitMissing(snap.state);
|
|
2278
|
+
return 2;
|
|
2279
|
+
}
|
|
2280
|
+
if (timeoutMs !== null && Date.now() - startedAt >= timeoutMs) {
|
|
2281
|
+
emitMissing(snap.state);
|
|
2282
|
+
return 2;
|
|
2283
|
+
}
|
|
2284
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
/** `ay result set [<json> | -]` — deposit THIS agent's envelope. */
|
|
2288
|
+
async function cmdResultSet(rest) {
|
|
2289
|
+
const argv = await yargs(rest).usage("Usage: ay result set ['<json>' | -]").option("pid", {
|
|
2290
|
+
type: "number",
|
|
2291
|
+
description: "Target pid (default: $AGENT_YES_PID — the agent's own wrapper)"
|
|
2292
|
+
}).help(false).version(false).exitProcess(false).parseAsync();
|
|
2293
|
+
const pid = Number.isFinite(argv.pid) ? Number(argv.pid) : Number(process.env.AGENT_YES_PID);
|
|
2294
|
+
if (!Number.isFinite(pid) || pid <= 0) throw new Error("ay result set: no target pid — run inside an ay-managed agent (AGENT_YES_PID is set) or pass --pid");
|
|
2295
|
+
const positional = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
|
|
2296
|
+
const result = normalizeEnvelope(positional !== void 0 && positional !== "-" ? positional : await readStdin());
|
|
2297
|
+
if (result === null) throw new Error("ay result set: empty input — pass a JSON object, text, or pipe via stdin");
|
|
2298
|
+
await mkdir(resultsDir(), { recursive: true });
|
|
2299
|
+
const stored = buildStoredResult(pid, result, Date.now());
|
|
2300
|
+
await writeFile(resultPath(pid), JSON.stringify(stored) + "\n");
|
|
2301
|
+
process.stdout.write(`result envelope written for pid ${pid}\n`);
|
|
2302
|
+
return 0;
|
|
2303
|
+
}
|
|
1689
2304
|
|
|
1690
2305
|
//#endregion
|
|
1691
|
-
export {
|
|
1692
|
-
//# sourceMappingURL=subcommands-
|
|
2306
|
+
export { stopTipForCli as _, extractNeedsInput as a, isPidAlive as c, matchKeyword as d, readNotes as f, snapshotStatus as g, runSubcommand as h, cursorAbs as i, isSubcommand as l, resolveOne as m, cmdHelp as n, extractTaskCounts as o, renderRawLog as p, controlCodeFromName as r, finalizedLines as s, GRACEFUL_EXIT_COMMANDS as t, listRecords as u, writeToIpc as v };
|
|
2307
|
+
//# sourceMappingURL=subcommands-B13Kto-u.js.map
|