agent-yes 1.263.0 → 1.264.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.
Files changed (49) hide show
  1. package/dist/SUPPORTED_CLIS-DlxOPg-j.js +11 -0
  2. package/dist/{SUPPORTED_CLIS-DjFlWHpg.js → SUPPORTED_CLIS-JcVs6jt7.js} +2 -2
  3. package/dist/{agentShare-BP6wiu_5.js → agentShare-B8h_JkOw.js} +3 -3
  4. package/dist/{callback-B0PQUZcK.js → callback-C9rkbjgR.js} +3 -3
  5. package/dist/{callback-DFUFLIZQ.js → callback-DlJ_glcF.js} +3 -3
  6. package/dist/cli.js +9 -9
  7. package/dist/{cwdConflictWarn-B_047KCF.js → cwdConflictWarn-DZTxTGXq.js} +2 -2
  8. package/dist/{forkNested-CBdhC852.js → forkNested-CgJ1rTnT.js} +2 -2
  9. package/dist/globalPidIndex-CQX_0JSB.js +4 -0
  10. package/dist/{globalPidIndex-pwp7Vb7o.js → globalPidIndex-Dq4KY2IV.js} +2 -8
  11. package/dist/index.js +4 -4
  12. package/dist/{notifyDaemon-BskLhHdg.js → notifyDaemon--TDDJupp.js} +3 -3
  13. package/dist/pidStore-D-b8I6Qx.js +6 -0
  14. package/dist/{pidStore-DfSm1jog.js → pidStore-DJqSZPJi.js} +11 -7
  15. package/dist/{rustBinary-Borvwmya.js → rustBinary-DA-C6PjH.js} +2 -2
  16. package/dist/{schedule-BSEIVKQ0.js → schedule-CdgYYRti.js} +6 -6
  17. package/dist/{serve-BEdLsSpv.js → serve-Z8FAjest.js} +20 -20
  18. package/dist/{setup-DFRvZ_ku.js → setup-D6B85mUe.js} +2 -2
  19. package/dist/{spawnGate-D-68B907.js → spawnGate-D0E1uRP7.js} +2 -2
  20. package/dist/spawnGate-DqKBA07k.js +5 -0
  21. package/dist/{subcommands-B1YlmRaZ.js → subcommands-C2ipqBtq.js} +2 -2
  22. package/dist/{subcommands-Bo1MSwuC.js → subcommands-C3w4bg6x.js} +134 -90
  23. package/dist/{terminal-BMQWojIp.js → terminal-iObUHvkj.js} +2 -2
  24. package/dist/{todoCli-DNOk38lq.js → todoCli-CngDeaT2.js} +2 -2
  25. package/dist/{trayApp-CjB5QhdV.js → trayApp-BUI7SXiY.js} +2 -2
  26. package/dist/trayApp-Cb9a-quB.js +5 -0
  27. package/dist/{ts-BSHR_dga.js → ts-ClHKe4d8.js} +123 -5
  28. package/dist/{versionChecker-BTLZ3PAb.js → versionChecker-DRAbaxo-.js} +2 -2
  29. package/dist/{widget-BVss0Z9Y.js → widget-BRBsnbLn.js} +3 -3
  30. package/dist/{ws-DMudFezi.js → ws-BppNz49U.js} +3 -3
  31. package/dist/{ws-DxNNwnrc.js → ws-CAFA4oXH.js} +2 -2
  32. package/package.json +1 -1
  33. package/ts/globalPidIndex.spec.ts +5 -20
  34. package/ts/globalPidIndex.ts +9 -19
  35. package/ts/identity.spec.ts +108 -0
  36. package/ts/identity.ts +124 -0
  37. package/ts/index.ts +12 -4
  38. package/ts/messageLog.ts +0 -2
  39. package/ts/pidStore.spec.ts +14 -24
  40. package/ts/pidStore.ts +13 -10
  41. package/ts/subcommands.spec.ts +11 -120
  42. package/ts/subcommands.ts +13 -103
  43. package/ts/titleScanner.spec.ts +65 -0
  44. package/ts/titleScanner.ts +137 -0
  45. package/dist/SUPPORTED_CLIS--5E9wO4L.js +0 -11
  46. package/dist/globalPidIndex-CHA__9Xf.js +0 -4
  47. package/dist/pidStore-1PQ4ARnh.js +0 -6
  48. package/dist/spawnGate-DbSHnfqA.js +0 -5
  49. package/dist/trayApp-DrdktFnW.js +0 -5
@@ -1,4 +1,4 @@
1
- import { T as listRecords } from "./subcommands-Bo1MSwuC.js";
1
+ import { T as listRecords } from "./subcommands-C3w4bg6x.js";
2
2
  import { a as getProvisionRoot } from "./workspaceConfig-Bxuk_MOq.js";
3
3
  import { existsSync } from "fs";
4
4
  import { lstat, opendir } from "fs/promises";
@@ -426,4 +426,4 @@ async function cmdWs(args) {
426
426
 
427
427
  //#endregion
428
428
  export { isPathInside as a, walkWorkspaces as c, importProvisionModule as i, workspaceStatus as l, cmdWs as n, loadProvision as o, collectWorkspaces as r, resolveOperand as s, WS_JSON_SCHEMA as t };
429
- //# sourceMappingURL=ws-DxNNwnrc.js.map
429
+ //# sourceMappingURL=ws-CAFA4oXH.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.263.0",
3
+ "version": "1.264.0",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
@@ -158,22 +158,7 @@ describe("globalPidIndex", () => {
158
158
  expect(records).toEqual([]);
159
159
  });
160
160
 
161
- it("sanitizeRole accepts safe lane names and rejects header-forging values", async () => {
162
- const mod = await loadModule();
163
- expect(mod.sanitizeRole("pm")).toBe("pm");
164
- expect(mod.sanitizeRole(" release-console ")).toBe("release-console");
165
- expect(mod.sanitizeRole("crm_v2.a")).toBe("crm_v2.a");
166
- // Anything that could forge the (non-nonce-protected) open tag is rejected:
167
- expect(mod.sanitizeRole('x") @ /fake — reply: ay send 1 "')).toBeNull();
168
- expect(mod.sanitizeRole("a b")).toBeNull();
169
- expect(mod.sanitizeRole("a>b")).toBeNull();
170
- expect(mod.sanitizeRole("a\nb")).toBeNull();
171
- expect(mod.sanitizeRole("")).toBeNull();
172
- expect(mod.sanitizeRole("x".repeat(33))).toBeNull();
173
- expect(mod.sanitizeRole(undefined)).toBeNull();
174
- });
175
-
176
- it("updateGlobalPidStatus can set and clear role", async () => {
161
+ it("updateGlobalPidStatus can set and clear title", async () => {
177
162
  const mod = await loadModule();
178
163
  await mod.appendGlobalPid({
179
164
  pid: 4242,
@@ -186,12 +171,12 @@ describe("globalPidIndex", () => {
186
171
  exit_reason: null,
187
172
  started_at: Date.now(),
188
173
  });
189
- await mod.updateGlobalPidStatus(4242, { role: "pm" });
174
+ await mod.updateGlobalPidStatus(4242, { title: "✳ fixing tests" });
190
175
  let rec = (await mod.readGlobalPids()).find((r: any) => r.pid === 4242);
191
- expect(rec?.role).toBe("pm");
192
- await mod.updateGlobalPidStatus(4242, { role: null });
176
+ expect(rec?.title).toBe("✳ fixing tests");
177
+ await mod.updateGlobalPidStatus(4242, { title: null });
193
178
  rec = (await mod.readGlobalPids()).find((r: any) => r.pid === 4242);
194
- expect(rec?.role).toBeNull();
179
+ expect(rec?.title).toBeNull();
195
180
  });
196
181
 
197
182
  it("updateGlobalPidStatus is a no-op for unknown pids", async () => {
@@ -65,24 +65,12 @@ export interface GlobalPidRecord {
65
65
  // permission checks off?" is unanswerable after the fact. See
66
66
  // ts/agentPermissions.ts (mirrored in rs/src/agent_permissions.rs).
67
67
  permissions?: AgentPermissions | null;
68
- // Self-declared lane/role name (e.g. "pm", "crm", "release-console"), shown in
69
- // the `ay send` envelope so recipients don't have to translate cwd/pid into a
70
- // role by hand. Set via $AGENT_YES_ROLE at spawn or `ay role <name>` later.
71
- // Mirrors Rust's `role`; must pass sanitizeRole (it is rendered into the
72
- // envelope header line, so an unconstrained value could forge header syntax).
73
- role?: string | null;
74
- }
75
-
76
- // Roles render verbatim into the `<ay-msg …>` header line, whose OPEN tag is not
77
- // nonce-protected (only the close tag is) — whitespace, ">", or control chars in
78
- // a role could forge header structure. Clamp at every entry point (env pickup,
79
- // `ay role`), not at render time, so stored records stay clean.
80
- const ROLE_RE = /^[A-Za-z0-9._-]{1,32}$/;
81
-
82
- /** Validate a role name; returns the trimmed role or null when unusable. */
83
- export function sanitizeRole(raw: string | undefined | null): string | null {
84
- const trimmed = raw?.trim();
85
- return trimmed && ROLE_RE.test(trimmed) ? trimmed : null;
68
+ // The child CLI's most recent terminal title (OSC 0/2 from its PTY stream —
69
+ // claude/opencode continuously set it to a task summary). The wrapper's
70
+ // title scanner keeps this fresh so `ay whoami` / `ay ls --json` can answer
71
+ // "what is this agent doing" without reading its screen. Mirrors Rust's
72
+ // `title`.
73
+ title?: string | null;
86
74
  }
87
75
 
88
76
  /**
@@ -136,7 +124,9 @@ export async function appendGlobalPid(record: GlobalPidRecord): Promise<void> {
136
124
  /** Append a partial update by pid (status, exit_code, exit_reason, log_file). */
137
125
  export async function updateGlobalPidStatus(
138
126
  pid: number,
139
- patch: Partial<Pick<GlobalPidRecord, "status" | "exit_code" | "exit_reason" | "log_file" | "role">>,
127
+ patch: Partial<
128
+ Pick<GlobalPidRecord, "status" | "exit_code" | "exit_reason" | "log_file" | "title">
129
+ >,
140
130
  ): Promise<void> {
141
131
  const file = resolveGlobalFile(); // capture at call time (see withLock)
142
132
  try {
@@ -0,0 +1,108 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "fs/promises";
3
+ import { homedir, tmpdir } from "os";
4
+ import path from "path";
5
+ import { formatIdentity, localHost, localUser, readGitBranch, tildify } from "./identity.ts";
6
+
7
+ let root: string;
8
+
9
+ beforeEach(async () => {
10
+ root = await mkdtemp(path.join(tmpdir(), "ay-ident-"));
11
+ });
12
+
13
+ afterEach(async () => {
14
+ await rm(root, { recursive: true, force: true }).catch(() => null);
15
+ });
16
+
17
+ async function gitFixture(rel: string, head: string): Promise<string> {
18
+ const repo = path.join(root, rel);
19
+ await mkdir(path.join(repo, ".git"), { recursive: true });
20
+ await writeFile(path.join(repo, ".git", "HEAD"), head);
21
+ return repo;
22
+ }
23
+
24
+ describe("readGitBranch", () => {
25
+ it("reads the branch from a normal checkout", async () => {
26
+ const repo = await gitFixture("repo", "ref: refs/heads/main\n");
27
+ expect(readGitBranch(repo)).toBe("main");
28
+ });
29
+
30
+ it("walks up from a subdirectory", async () => {
31
+ const repo = await gitFixture("repo2", "ref: refs/heads/feat/x-1.2\n");
32
+ const sub = path.join(repo, "a", "b");
33
+ await mkdir(sub, { recursive: true });
34
+ expect(readGitBranch(sub)).toBe("feat/x-1.2");
35
+ });
36
+
37
+ it("follows a worktree's gitdir indirection file", async () => {
38
+ const gitdir = path.join(root, "main-repo", ".git", "worktrees", "wt1");
39
+ await mkdir(gitdir, { recursive: true });
40
+ await writeFile(path.join(gitdir, "HEAD"), "ref: refs/heads/crm-yamamoto-wifi\n");
41
+ const wt = path.join(root, "wt-checkout");
42
+ await mkdir(wt, { recursive: true });
43
+ await writeFile(path.join(wt, ".git"), `gitdir: ${gitdir}\n`);
44
+ expect(readGitBranch(wt)).toBe("crm-yamamoto-wifi");
45
+ });
46
+
47
+ it("resolves a RELATIVE gitdir against the checkout", async () => {
48
+ const gitdir = path.join(root, "gd");
49
+ await mkdir(gitdir, { recursive: true });
50
+ await writeFile(path.join(gitdir, "HEAD"), "ref: refs/heads/rel\n");
51
+ const wt = path.join(root, "rel-checkout");
52
+ await mkdir(wt, { recursive: true });
53
+ await writeFile(path.join(wt, ".git"), "gitdir: ../gd\n");
54
+ expect(readGitBranch(wt)).toBe("rel");
55
+ });
56
+
57
+ it("returns a short commit id for a detached HEAD", async () => {
58
+ const sha = "0123456789abcdef0123456789abcdef01234567";
59
+ const repo = await gitFixture("det", sha + "\n");
60
+ expect(readGitBranch(repo)).toBe(sha.slice(0, 12));
61
+ });
62
+
63
+ it("returns null outside any git checkout", async () => {
64
+ const plain = path.join(root, "plain");
65
+ await mkdir(plain, { recursive: true });
66
+ expect(readGitBranch(plain)).toBeNull();
67
+ });
68
+
69
+ it("returns null on an unreadable/garbled HEAD", async () => {
70
+ const repo = await gitFixture("bad", "something else entirely\n");
71
+ expect(readGitBranch(repo)).toBeNull();
72
+ });
73
+ });
74
+
75
+ describe("formatIdentity", () => {
76
+ it("renders user@host:path:branch#pid", async () => {
77
+ const repo = await gitFixture("fmt", "ref: refs/heads/main\n");
78
+ const id = formatIdentity({ user: "sno", host: "Mac", cwd: repo, pid: 30402 });
79
+ expect(id).toBe(`sno@Mac:${repo}:main#30402`);
80
+ });
81
+
82
+ it("omits the branch segment outside a repo", async () => {
83
+ const plain = path.join(root, "nofmt");
84
+ await mkdir(plain, { recursive: true });
85
+ const id = formatIdentity({ user: "sno", host: "Mac", cwd: plain, pid: 7 });
86
+ expect(id).toBe(`sno@Mac:${plain}#7`);
87
+ });
88
+
89
+ it("accepts an explicit branch (and null to suppress detection)", () => {
90
+ expect(formatIdentity({ user: "u", host: "h", cwd: "/x", branch: "b", pid: 1 })).toBe(
91
+ "u@h:/x:b#1",
92
+ );
93
+ expect(formatIdentity({ user: "u", host: "h", cwd: "/x", branch: null, pid: 1 })).toBe(
94
+ "u@h:/x#1",
95
+ );
96
+ });
97
+
98
+ it("defaults user/host to header-safe local values", () => {
99
+ const id = formatIdentity({ cwd: "/x", branch: null, pid: 1 });
100
+ expect(id).toMatch(/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:\/x#1$/);
101
+ expect(id).toContain(`${localUser()}@${localHost()}`);
102
+ });
103
+
104
+ it("tildifies the home directory like ay ls does", () => {
105
+ expect(tildify(path.join(homedir(), "ws"))).toBe(`~${path.sep}ws`);
106
+ expect(tildify("/opt/x")).toBe("/opt/x");
107
+ });
108
+ });
package/ts/identity.ts ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The standardized agent identity string:
3
+ *
4
+ * <username>@<hostname>:<path>:<branch>#<pid>
5
+ * e.g. sno@Mac:~/ws/symval/symval/tree/crm:main#30402
6
+ *
7
+ * One line answers "who is this agent" across every surface that names one —
8
+ * the `ay send` envelope header, `ay whoami`, logs. The branch segment usually
9
+ * carries the lane name for free (worktree checkouts like `crm-yamamoto-wifi`
10
+ * or `fix/t173-tone-core` name their purpose), which is why there is no
11
+ * separate role concept.
12
+ *
13
+ * Parsing (right-to-left, for consumers that need it): `#<pid>` from the end;
14
+ * the branch is after the last `:` (git forbids `:` in ref names); the
15
+ * username is before the first `@`; the hostname is between that `@` and the
16
+ * next `:` (hostnames contain no `:`). The path in the middle may itself
17
+ * contain `@`/`:` (Windows drives), which is why parsing anchors on the ends.
18
+ *
19
+ * Every token is clamped before rendering: the identity is embedded in the
20
+ * `<ay-msg …>` envelope's OPEN tag, which is not nonce-protected — whitespace
21
+ * or `>` in a token could forge header structure.
22
+ */
23
+
24
+ import { readFileSync } from "node:fs";
25
+ import { homedir, hostname, userInfo } from "node:os";
26
+ import path from "node:path";
27
+
28
+ /** Replace anything outside a conservative charset so a token can never break
29
+ * out of the envelope header line. Collapses runs to a single `-`. */
30
+ function safeToken(raw: string, max: number): string {
31
+ const cleaned = raw.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
32
+ return cleaned.slice(0, max);
33
+ }
34
+
35
+ /** The local username, header-safe ("John Smith" → "John-Smith"). */
36
+ export function localUser(): string {
37
+ try {
38
+ return safeToken(userInfo().username, 32) || "unknown";
39
+ } catch {
40
+ return "unknown";
41
+ }
42
+ }
43
+
44
+ /** The local hostname's first label ("Macs-MBP.local" → "Macs-MBP"). */
45
+ export function localHost(): string {
46
+ const first = hostname().split(".")[0] ?? "";
47
+ return safeToken(first, 32) || "unknown";
48
+ }
49
+
50
+ /**
51
+ * The checked-out branch of the git repo containing `cwd`, or a short commit
52
+ * id when detached, or null when `cwd` is not inside a git checkout. Pure
53
+ * file reads — never spawns git — so it is cheap enough to run on every send:
54
+ * walk up to the nearest `.git`, follow a worktree's `gitdir:` indirection
55
+ * file, then parse `HEAD`.
56
+ */
57
+ export function readGitBranch(cwd: string): string | null {
58
+ let dir = path.resolve(cwd);
59
+ for (let depth = 0; depth < 32; depth++) {
60
+ const dotGit = path.join(dir, ".git");
61
+ let gitDir: string | null = null;
62
+ try {
63
+ const content = readFileSync(dotGit, "utf8");
64
+ // A worktree/submodule checkout: `.git` is a FILE containing
65
+ // `gitdir: <path>` (absolute, or relative to the checkout).
66
+ const m = /^gitdir:\s*(.+)\s*$/m.exec(content);
67
+ gitDir = m ? path.resolve(dir, m[1]!.trim()) : null;
68
+ } catch (err: any) {
69
+ if (err?.code === "EISDIR") gitDir = dotGit;
70
+ // ENOENT/EACCES: no .git here — walk up.
71
+ }
72
+ if (gitDir) {
73
+ try {
74
+ const head = readFileSync(path.join(gitDir, "HEAD"), "utf8").trim();
75
+ const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(head);
76
+ if (ref) return safeBranch(ref[1]!);
77
+ if (/^[0-9a-f]{40}$/i.test(head)) return head.slice(0, 12); // detached
78
+ return null; // unrecognized HEAD — don't guess
79
+ } catch {
80
+ return null; // .git exists but HEAD unreadable
81
+ }
82
+ }
83
+ const parent = path.dirname(dir);
84
+ if (parent === dir) return null;
85
+ dir = parent;
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /** Branch names never contain `:`/space/`~`/`^` (git forbids them), but they
91
+ * DO allow `/` and `#` — keep `/` (readable, unambiguous: parsing anchors on
92
+ * `#<pid>` at the end) and clamp everything else defensively. */
93
+ function safeBranch(raw: string): string {
94
+ const cleaned = raw.replace(/[^A-Za-z0-9._/#-]+/g, "-").replace(/^-+|-+$/g, "");
95
+ return cleaned.slice(0, 64);
96
+ }
97
+
98
+ /** Abbreviate the home directory to `~`, mirroring how `ay ls` prints cwds. */
99
+ export function tildify(p: string): string {
100
+ const home = homedir();
101
+ return p.startsWith(home) ? "~" + p.slice(home.length) : p;
102
+ }
103
+
104
+ export interface IdentityParts {
105
+ user?: string;
106
+ host?: string;
107
+ cwd: string;
108
+ /** Pass null to omit the branch segment; omit the key to auto-detect. */
109
+ branch?: string | null;
110
+ pid: number;
111
+ }
112
+
113
+ /**
114
+ * Render the standardized identity for an agent. `user`/`host` default to the
115
+ * local machine (correct for the envelope: the sending wrapper runs on the
116
+ * agent's own host), `branch` auto-detects from `cwd` unless given.
117
+ */
118
+ export function formatIdentity(parts: IdentityParts): string {
119
+ const user = parts.user ?? localUser();
120
+ const host = parts.host ?? localHost();
121
+ const branch = parts.branch === undefined ? readGitBranch(parts.cwd) : parts.branch;
122
+ const where = tildify(parts.cwd);
123
+ return `${user}@${host}:${where}${branch ? `:${branch}` : ""}#${parts.pid}`;
124
+ }
package/ts/index.ts CHANGED
@@ -18,6 +18,7 @@ import { acquireLock, releaseLock, shouldUseLock } from "./runningLock.ts";
18
18
  import { logger } from "./logger.ts";
19
19
  import { createFifoStream } from "./beta/fifo.ts";
20
20
  import { PidStore } from "./pidStore.ts";
21
+ import { TitlePublisher, TitleScanner } from "./titleScanner.ts";
21
22
  import { sendEnter, sendMessage } from "./core/messaging.ts";
22
23
  import {
23
24
  AUTO_RETRY_GIVE_UP_MS,
@@ -460,10 +461,6 @@ export default async function agentYes({
460
461
  // `ay`) don't inherit it and register under the same id (which would make that
461
462
  // id ambiguous). The wrapper's own process.env still carries it for pidStore.
462
463
  delete ptyEnv.AGENT_YES_AGENT_ID;
463
- // Same reasoning for AGENT_YES_ROLE: it names THIS agent's lane (pidStore
464
- // reads it from our own env at registration). A subagent is its own lane, so
465
- // don't let it inherit the parent's role.
466
- delete ptyEnv.AGENT_YES_ROLE;
467
464
  // Strip the parent Claude Code session markers so the wrapped CLI is a CLEAN
468
465
  // top-level session. Without this, an `ay claude` launched from inside another
469
466
  // Claude Code session (or this claude's Bash tool) inherits
@@ -524,12 +521,23 @@ export default async function agentYes({
524
521
  // Wire up the xterm proxy to write back to the PTY
525
522
  shellWrite = (data: string) => shell.write(data);
526
523
 
524
+ // Capture the child CLI's terminal title (OSC 0/2) into the registry so
525
+ // `ay whoami` / `ay ls --json` can answer "what is this agent doing".
526
+ // TitlePublisher change-gates + rate-limits the registry writes.
527
+ const titleScanner = new TitleScanner();
528
+ const titlePublisher = new TitlePublisher((title) => {
529
+ pidStore.updateTitle(shell.pid, title).catch(() => null);
530
+ });
531
+
527
532
  // Attach data handler IMMEDIATELY after spawn to avoid losing early PTY output.
528
533
  // node-pty emits 'data' events eagerly — if no listener is attached, events are lost.
529
534
  function onData(data: string) {
530
535
  const currentPid = shell.pid;
531
536
  xtermProxy.write(data);
532
537
  globalAgentRegistry.appendStdout(currentPid, data);
538
+ const title = titleScanner.feed(data);
539
+ if (title) titlePublisher.observe(title);
540
+ else titlePublisher.poll();
533
541
  }
534
542
  shell.onData(onData);
535
543
 
package/ts/messageLog.ts CHANGED
@@ -25,8 +25,6 @@ export interface MailParty {
25
25
  cli: string;
26
26
  cwd: string;
27
27
  agent_id?: string | null;
28
- /** Self-declared lane/role name at send time, when the party had one set. */
29
- role?: string;
30
28
  }
31
29
 
32
30
  /** A single delivered inter-agent message, stored verbatim in both mailboxes. */
@@ -63,30 +63,20 @@ describe("PidStore", () => {
63
63
  expect(rec._id).toBeTypeOf("string");
64
64
  });
65
65
 
66
- it("picks up a sane AGENT_YES_ROLE and ignores a forgeable one", async () => {
67
- const saved = process.env.AGENT_YES_ROLE;
68
- try {
69
- process.env.AGENT_YES_ROLE = "pm";
70
- const rec = await store.registerProcess({
71
- pid: 12346,
72
- cli: "claude",
73
- args: [],
74
- cwd: "/tmp",
75
- });
76
- expect(rec.role).toBe("pm");
77
-
78
- process.env.AGENT_YES_ROLE = "a b > forged";
79
- const rec2 = await store.registerProcess({
80
- pid: 12347,
81
- cli: "claude",
82
- args: [],
83
- cwd: "/tmp",
84
- });
85
- expect(rec2.role).toBeUndefined();
86
- } finally {
87
- if (saved === undefined) delete process.env.AGENT_YES_ROLE;
88
- else process.env.AGENT_YES_ROLE = saved;
89
- }
66
+ it("updateTitle stores the latest terminal title and dedupes", async () => {
67
+ await store.registerProcess({
68
+ pid: 12346,
69
+ cli: "claude",
70
+ args: [],
71
+ cwd: "/tmp",
72
+ });
73
+ await store.updateTitle(12346, "✳ fixing tests");
74
+ let rec = store.getAllRecords().find((r) => r.pid === 12346);
75
+ expect(rec?.title).toBe("✳ fixing tests");
76
+ await store.updateTitle(12346, "✳ fixing tests"); // unchanged — no-op
77
+ await store.updateTitle(12346, "done");
78
+ rec = store.getAllRecords().find((r) => r.pid === 12346);
79
+ expect(rec?.title).toBe("done");
90
80
  });
91
81
 
92
82
  it("should upsert when registering same pid", async () => {
package/ts/pidStore.ts CHANGED
@@ -9,7 +9,6 @@ import {
9
9
  appendGlobalPid,
10
10
  maybeCompactGlobalPids,
11
11
  pruneOldLogs,
12
- sanitizeRole,
13
12
  updateGlobalPidStatus,
14
13
  } from "./globalPidIndex.ts";
15
14
 
@@ -30,8 +29,8 @@ export interface PidRecord {
30
29
  agentId?: string;
31
30
  /** Permission posture at spawn time; mirrored to the global index. */
32
31
  permissions?: AgentPermissions;
33
- /** Self-declared lane/role name; mirrored to the global index as `role`. */
34
- role?: string;
32
+ /** The child CLI's latest terminal title (OSC 0/2); mirrored as `title`. */
33
+ title?: string;
35
34
  }
36
35
 
37
36
  export class PidStore {
@@ -95,11 +94,6 @@ export class PidStore {
95
94
  existing?.agentId ??
96
95
  (injected && /^[0-9a-f]{6,32}$/i.test(injected) ? injected : randomBytes(6).toString("hex"));
97
96
 
98
- // A caller-set AGENT_YES_ROLE names the lane this agent plays ("pm", "crm",
99
- // …) for the `ay send` envelope. Picked up once at registration; index.ts
100
- // strips it from the wrapped CLI's env so subagents don't inherit it.
101
- const role = sanitizeRole(process.env.AGENT_YES_ROLE) ?? existing?.role;
102
-
103
97
  const record: Omit<PidRecord, "_id"> = {
104
98
  pid,
105
99
  cli,
@@ -113,7 +107,6 @@ export class PidStore {
113
107
  exitReason: "",
114
108
  startedAt: now,
115
109
  agentId,
116
- ...(role ? { role } : {}),
117
110
  };
118
111
 
119
112
  if (existing) {
@@ -149,7 +142,6 @@ export class PidStore {
149
142
  parent_pid: parentPid ?? null,
150
143
  agent_id: agentId,
151
144
  permissions: permissions ?? null,
152
- role: role ?? null,
153
145
  })
154
146
  .then(() => maybeCompactGlobalPids())
155
147
  .catch(() => null);
@@ -180,6 +172,17 @@ export class PidStore {
180
172
  }).catch(() => null);
181
173
  }
182
174
 
175
+ /** Record the child CLI's latest terminal title (see ts/titleScanner.ts —
176
+ * callers are change-gated + rate-limited, so each call may write). */
177
+ async updateTitle(pid: number, title: string): Promise<void> {
178
+ const existing = this.store.findOne((doc) => doc.pid === pid);
179
+ if (existing && existing.title !== title) {
180
+ await this.store.updateById(existing._id!, { title });
181
+ }
182
+ // Mirror to global index. Same fire-and-forget policy as updateStatus.
183
+ updateGlobalPidStatus(pid, { title }).catch(() => null);
184
+ }
185
+
183
186
  getAllRecords(): PidRecord[] {
184
187
  return this.store.getAll();
185
188
  }