agent-yes 1.258.3 → 1.259.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 (57) hide show
  1. package/dist/{SUPPORTED_CLIS-DD6bhkyI.js → SUPPORTED_CLIS-BBeIFAZD.js} +2 -2
  2. package/dist/SUPPORTED_CLIS-BRgPb6Pz.js +11 -0
  3. package/dist/{agentShare-CoI7IHeq.js → agentShare-90Y_SFqK.js} +4 -4
  4. package/dist/{callback-CbdkyTRw.js → callback-CLzRxjEn.js} +3 -3
  5. package/dist/{callback-DNI__ASS.js → callback-DYQ2jQi2.js} +4 -4
  6. package/dist/cli.js +11 -11
  7. package/dist/{cwdConflictWarn-DZTxTGXq.js → cwdConflictWarn-B_047KCF.js} +2 -2
  8. package/dist/{forkNested-CgJ1rTnT.js → forkNested-CBdhC852.js} +2 -2
  9. package/dist/globalPidIndex-CHA__9Xf.js +4 -0
  10. package/dist/{globalPidIndex-Dq4KY2IV.js → globalPidIndex-pwp7Vb7o.js} +8 -2
  11. package/dist/index.js +4 -4
  12. package/dist/{notifyDaemon-BQ6_UbJS.js → notifyDaemon-D5RtFwMq.js} +4 -4
  13. package/dist/pidStore-1PQ4ARnh.js +6 -0
  14. package/dist/{pidStore-Cmcc8Oej.js → pidStore-DfSm1jog.js} +7 -4
  15. package/dist/{remotes-Dy4Rg8Yp.js → remotes-Cn-wszbK.js} +2 -2
  16. package/dist/{remotes-CNe85Icx.js → remotes-lOhHSoSS.js} +1 -1
  17. package/dist/{rustBinary-orhBmTNo.js → rustBinary-BEscpLLP.js} +2 -2
  18. package/dist/{schedule-BQy6gXYk.js → schedule-Cw5nW_di.js} +6 -6
  19. package/dist/{serve-C2mQMjUR.js → serve-DctJ7tY-.js} +19 -19
  20. package/dist/{setup-EEF1vfhi.js → setup-BA9D1ExO.js} +10 -2
  21. package/dist/{spawnGate-D0E1uRP7.js → spawnGate-D-68B907.js} +2 -2
  22. package/dist/spawnGate-DbSHnfqA.js +5 -0
  23. package/dist/{subcommands-CiX7J7hA.js → subcommands-D_cPHm34.js} +93 -24
  24. package/dist/{subcommands-S7uULM_d.js → subcommands-Dg7KkEi6.js} +3 -3
  25. package/dist/systemPathLink-DTAhFXFx.js +91 -0
  26. package/dist/{terminal-i7IimD_R.js → terminal-CB5K4Jjh.js} +2 -2
  27. package/dist/{todoCli-B2kqgqPh.js → todoCli-DNOk38lq.js} +2 -2
  28. package/dist/{tray-CgkwxtG6.js → tray-wWMvGWeY.js} +1 -1
  29. package/dist/trayApp-B1OhYfj3.js +5 -0
  30. package/dist/{trayApp-4sZgxxP-.js → trayApp-Ck3kWM-7.js} +2 -2
  31. package/dist/{ts-D-E4xqpA.js → ts-Bd_7sgVu.js} +5 -4
  32. package/dist/{versionChecker-YDl8a9il.js → versionChecker-DRijpksb.js} +2 -2
  33. package/dist/{webrtcRemote-C3OlHYSt.js → webrtcRemote-BkZcA5yq.js} +1 -1
  34. package/dist/{widget-7j9RrYZR.js → widget-DiRKbtng.js} +3 -3
  35. package/dist/{ws-DjyQ20AD.js → ws-Bp8eOEoV.js} +2 -2
  36. package/dist/{ws-Clem1WQ-.js → ws-Cqn8mxsS.js} +4 -4
  37. package/package.json +1 -1
  38. package/ts/globalPidIndex.spec.ts +36 -0
  39. package/ts/globalPidIndex.ts +19 -1
  40. package/ts/hist.spec.ts +219 -0
  41. package/ts/hist.ts +136 -0
  42. package/ts/histStore.spec.ts +351 -0
  43. package/ts/histStore.ts +494 -0
  44. package/ts/index.ts +4 -0
  45. package/ts/messageLog.ts +2 -0
  46. package/ts/pidStore.spec.ts +26 -0
  47. package/ts/pidStore.ts +10 -0
  48. package/ts/setup.ts +17 -0
  49. package/ts/subcommands.spec.ts +180 -0
  50. package/ts/subcommands.ts +103 -3
  51. package/ts/systemPathLink.spec.ts +69 -0
  52. package/ts/systemPathLink.ts +134 -0
  53. package/dist/SUPPORTED_CLIS-D1BoydrZ.js +0 -11
  54. package/dist/globalPidIndex-CQX_0JSB.js +0 -4
  55. package/dist/pidStore-BbKqQOS8.js +0 -6
  56. package/dist/spawnGate-DqKBA07k.js +0 -5
  57. package/dist/trayApp-C35IkXnz.js +0 -5
@@ -1,11 +1,11 @@
1
1
  import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
2
2
  import { n as logger } from "./logger-CDIsZ-Pp.js";
3
3
  import { t as agentYesHome } from "./agentYesHome-CtmsY6I-.js";
4
- import { a as updateGlobalPidStatus, i as readGlobalPids } from "./globalPidIndex-Dq4KY2IV.js";
4
+ import { a as sanitizeRole, i as readGlobalPids, o as updateGlobalPidStatus } from "./globalPidIndex-pwp7Vb7o.js";
5
5
  import { a as recordOutbox, i as recordMessage, n as readMailbox, t as partyMatches } from "./messageLog-BYFwioFY.js";
6
6
  import { t as loadSharedCliDefaults } from "./configShared-DtHMBCl7.js";
7
7
  import { n as isWebrtcSpec } from "./webrtcLink-CS4D-3G1.js";
8
- import { a as resolveRemoteSpec, i as readRemotes } from "./remotes-Dy4Rg8Yp.js";
8
+ import { a as resolveRemoteSpec, i as readRemotes } from "./remotes-Cn-wszbK.js";
9
9
  import ms from "ms";
10
10
  import yargs from "yargs";
11
11
  import { appendFile, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "fs/promises";
@@ -1211,6 +1211,7 @@ async function cmdWhoami(rest) {
1211
1211
  const ageMin = Math.max(0, Math.round((Date.now() - self.started_at) / 6e4));
1212
1212
  const lines = [
1213
1213
  `agent ${self.cli} #${self.pid}${self.agent_id ? ` (agent_id ${self.agent_id})` : ""}`,
1214
+ `role ${self.role ?? "- (set with: ay role <name>)"}`,
1214
1215
  `state ${state}${question ? ` — ${question}` : ""}`,
1215
1216
  `cwd ${self.cwd}`,
1216
1217
  `started ${new Date(self.started_at).toISOString()} (${ageMin}m ago)`,
@@ -1218,7 +1219,7 @@ async function cmdWhoami(rest) {
1218
1219
  `log ${self.log_file ?? "-"}`,
1219
1220
  `fifo ${self.fifo_file ?? "-"}`,
1220
1221
  `reply ${reply} "..."`,
1221
- `envelope <ay-msg from ${self.cli} #${self.pid} @ ${self.cwd} — reply: ${reply} "...">…</ay-msg>`
1222
+ `envelope <ay-msg from ${self.cli} #${self.pid}${self.role ? ` (${self.role})` : ""} @ ${self.cwd} — reply: ${reply} "...">…</ay-msg>`
1222
1223
  ];
1223
1224
  process.stdout.write(lines.join("\n") + "\n");
1224
1225
  return 0;
@@ -1264,7 +1265,8 @@ async function readLocalTsPids(cwd) {
1264
1265
  status: d.status ?? "active",
1265
1266
  exit_code: d.exitCode ?? null,
1266
1267
  exit_reason: d.exitReason ?? null,
1267
- started_at: d.startedAt ?? 0
1268
+ started_at: d.startedAt ?? 0,
1269
+ role: d.role ?? null
1268
1270
  }));
1269
1271
  }
1270
1272
  /** Merge by pid; later entries (typically from the global file) win. */
@@ -1294,6 +1296,7 @@ const SUBCOMMANDS = new Set([
1294
1296
  "head",
1295
1297
  "send",
1296
1298
  "msgs",
1299
+ "role",
1297
1300
  "key",
1298
1301
  "select",
1299
1302
  "spawn",
@@ -1380,6 +1383,7 @@ async function runSubcommand(argv) {
1380
1383
  case "head": return await cmdRead(rest, { mode: "head" });
1381
1384
  case "send": return await cmdSend(rest);
1382
1385
  case "msgs": return await cmdMsgs(rest);
1386
+ case "role": return await cmdRole(rest);
1383
1387
  case "key": return await cmdKey(rest);
1384
1388
  case "select": return await cmdSelect(rest);
1385
1389
  case "spawn": return await cmdSpawn(rest);
@@ -1389,7 +1393,7 @@ async function runSubcommand(argv) {
1389
1393
  case "restart": return await cmdRestart(rest);
1390
1394
  case "note": return await cmdNote(rest);
1391
1395
  case "todo": {
1392
- const { runTodoSubcommand } = await import("./todoCli-B2kqgqPh.js");
1396
+ const { runTodoSubcommand } = await import("./todoCli-DNOk38lq.js");
1393
1397
  return runTodoSubcommand(rest);
1394
1398
  }
1395
1399
  case "ch":
@@ -1398,39 +1402,39 @@ async function runSubcommand(argv) {
1398
1402
  return cmdCh(rest);
1399
1403
  }
1400
1404
  case "term": {
1401
- const { cmdTerm } = await import("./terminal-i7IimD_R.js");
1405
+ const { cmdTerm } = await import("./terminal-CB5K4Jjh.js");
1402
1406
  return cmdTerm(rest);
1403
1407
  }
1404
1408
  case "widget": {
1405
- const { cmdWidget } = await import("./widget-7j9RrYZR.js");
1409
+ const { cmdWidget } = await import("./widget-DiRKbtng.js");
1406
1410
  return cmdWidget(rest);
1407
1411
  }
1408
1412
  case "mint": {
1409
- const { cmdMint } = await import("./widget-7j9RrYZR.js");
1413
+ const { cmdMint } = await import("./widget-DiRKbtng.js");
1410
1414
  return cmdMint(rest);
1411
1415
  }
1412
1416
  case "serve": {
1413
- const { cmdServe } = await import("./serve-C2mQMjUR.js");
1417
+ const { cmdServe } = await import("./serve-DctJ7tY-.js");
1414
1418
  return cmdServe(rest);
1415
1419
  }
1416
1420
  case "tray": {
1417
- const { cmdTray } = await import("./trayApp-C35IkXnz.js");
1421
+ const { cmdTray } = await import("./trayApp-B1OhYfj3.js");
1418
1422
  return cmdTray(rest);
1419
1423
  }
1420
1424
  case "setup": {
1421
- const { cmdSetup } = await import("./setup-EEF1vfhi.js");
1425
+ const { cmdSetup } = await import("./setup-BA9D1ExO.js");
1422
1426
  return cmdSetup(rest);
1423
1427
  }
1424
1428
  case "ws": {
1425
- const { cmdWs } = await import("./ws-Clem1WQ-.js");
1429
+ const { cmdWs } = await import("./ws-Cqn8mxsS.js");
1426
1430
  return cmdWs(rest);
1427
1431
  }
1428
1432
  case "schedule": {
1429
- const { cmdSchedule } = await import("./schedule-BQy6gXYk.js");
1433
+ const { cmdSchedule } = await import("./schedule-Cw5nW_di.js");
1430
1434
  return cmdSchedule(rest);
1431
1435
  }
1432
1436
  case "remote": {
1433
- const { cmdRemote } = await import("./remotes-CNe85Icx.js");
1437
+ const { cmdRemote } = await import("./remotes-lOhHSoSS.js");
1434
1438
  return cmdRemote(rest);
1435
1439
  }
1436
1440
  case "expose": {
@@ -1438,7 +1442,7 @@ async function runSubcommand(argv) {
1438
1442
  return cmdExpose(rest);
1439
1443
  }
1440
1444
  case "callback": {
1441
- const { cmdCallback } = await import("./callback-DNI__ASS.js");
1445
+ const { cmdCallback } = await import("./callback-DYQ2jQi2.js");
1442
1446
  return cmdCallback(rest);
1443
1447
  }
1444
1448
  case "reap":
@@ -1476,7 +1480,7 @@ async function cmdHelp(managerCommands = true) {
1476
1480
  const wsLines = managerCommands ? " ay ws ls [--status] list <owner>/<repo>/tree/<branch> workspaces\n ay ws new <owner>/<repo>[@branch] clone/refresh a workspace (ay ws help for more)\n" : ``;
1477
1481
  const self = process.env.AGENT_YES_PID ? await resolveSender() : null;
1478
1482
  const agentSection = self ? await buildAgentContextSection(self) : "";
1479
- process.stdout.write(agentSection + "ay - agent-yes CLI\n\nManagement:\n ay ls [keyword] list running agents\n ay tail [-f] [-n N] <keyword> last N lines (96), -f to follow\n ay read <keyword> [page opts] paginate: --last/--head N, --range A:B,\n --before-line L [--limit N]\n ay cat <keyword> full log\n ay head <keyword> first N lines\n ay send <keyword> <msg> send a message (keyword '.' = agent in this cwd)\n ay msgs [keyword] [--in|--out] inter-agent message log (sent + received)\n ay ch mk|join|send|read|tail <topic> local-first E2E channels: AI ↔ humans on a topic (ay ch help)\n ay term embed <pid> <script> to embed a live read-only agent terminal in a page (ay term help)\n ay widget ls | read selection|dom read an opted-in page widget's context (selection/DOM) (ay widget help)\n ay mint <target> --caps ... mint a scoped, short-TTL capability token (embeddable in a page)\n ay key <keyword> <key...> send raw keystrokes (down/up/enter/esc/…) — drives menus\n ay select <keyword> <N> pick option N of a needs_input selection menu\n ay attach <keyword> interactive attach (detach: Ctrl-\\)\n ay stop <keyword> graceful shutdown (/exit for claude/codex)\n ay exit <keyword> [reason] graceful shutdown, recording who/why (= 'ay send <kw> exit')\n ay restart <keyword> [--fresh] stop (if live) + relaunch resuming the session; --fresh replays the prompt\n ay status <keyword> agent status snapshot\n ay whoami [--json] (inside an agent) your own registry identity + reply address\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" + wsLines + "\nRemote:\n" + setupLine + " 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 expose <port> share localhost:<port> at https://<id>.agent-yes.com (private link)\n ay callback --expires 7d mint an embeddable message-me widget for one agent\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");
1483
+ process.stdout.write(agentSection + "ay - agent-yes CLI\n\nManagement:\n ay ls [keyword] list running agents\n ay tail [-f] [-n N] <keyword> last N lines (96), -f to follow\n ay read <keyword> [page opts] paginate: --last/--head N, --range A:B,\n --before-line L [--limit N]\n ay cat <keyword> full log\n ay head <keyword> first N lines\n ay send <keyword> <msg> send a message (keyword '.' = agent in this cwd)\n ay msgs [keyword] [--in|--out] inter-agent message log (sent + received)\n ay role [name] | ay role <kw> <name> lane/role name shown in the send envelope\n ay ch mk|join|send|read|tail <topic> local-first E2E channels: AI ↔ humans on a topic (ay ch help)\n ay term embed <pid> <script> to embed a live read-only agent terminal in a page (ay term help)\n ay widget ls | read selection|dom read an opted-in page widget's context (selection/DOM) (ay widget help)\n ay mint <target> --caps ... mint a scoped, short-TTL capability token (embeddable in a page)\n ay key <keyword> <key...> send raw keystrokes (down/up/enter/esc/…) — drives menus\n ay select <keyword> <N> pick option N of a needs_input selection menu\n ay attach <keyword> interactive attach (detach: Ctrl-\\)\n ay stop <keyword> graceful shutdown (/exit for claude/codex)\n ay exit <keyword> [reason] graceful shutdown, recording who/why (= 'ay send <kw> exit')\n ay restart <keyword> [--fresh] stop (if live) + relaunch resuming the session; --fresh replays the prompt\n ay status <keyword> agent status snapshot\n ay whoami [--json] (inside an agent) your own registry identity + reply address\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" + wsLines + "\nRemote:\n" + setupLine + " 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 expose <port> share localhost:<port> at https://<id>.agent-yes.com (private link)\n ay callback --expires 7d mint an embeddable message-me widget for one agent\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");
1480
1484
  return 0;
1481
1485
  }
1482
1486
  function matchKeyword(record, keyword) {
@@ -1856,7 +1860,7 @@ async function fetchRemoteRecordsRaw(url, token, opts) {
1856
1860
  let base = url;
1857
1861
  let bearer = token;
1858
1862
  if (isWebrtcSpec(url)) {
1859
- const { startWebrtcBridge } = await import("./webrtcRemote-C3OlHYSt.js");
1863
+ const { startWebrtcBridge } = await import("./webrtcRemote-BkZcA5yq.js");
1860
1864
  bridge = await startWebrtcBridge(url);
1861
1865
  base = bridge.baseUrl;
1862
1866
  bearer = bridge.token;
@@ -3294,7 +3298,9 @@ async function cmdSend(rest) {
3294
3298
  let nonce;
3295
3299
  if (sender.agent && !isSlashCommand(body) && !raw) {
3296
3300
  nonce = randomBytes$1(4).toString("hex");
3297
- prefix = `<ay-msg ${nonce} from ${sender.agent.cli} #${sender.agent.pid} @ ${shortenPath(sender.agent.cwd)} — reply: ay send ${replyTarget} "...">\n`;
3301
+ const senderRole = sanitizeRole(sender.agent.role);
3302
+ const roleTag = senderRole ? ` (${senderRole})` : "";
3303
+ prefix = `<ay-msg ${nonce} from ${sender.agent.cli} #${sender.agent.pid}${roleTag} @ ${shortenPath(sender.agent.cwd)} — reply: ay send ${replyTarget} "...">\n`;
3298
3304
  suffix = `\n</ay-msg ${nonce}>`;
3299
3305
  if (/^\s*<ay-msg\s/.test(body)) process.stderr.write("warning: body already starts with an <ay-msg …> header — ay send adds the envelope automatically, so the recipient will see a DOUBLE wrapper. Send the bare body instead (or pass --raw if you really mean to deliver a pre-built envelope verbatim; forwarding a quoted message inside a plain body is fine).\n");
3300
3306
  }
@@ -3328,13 +3334,15 @@ async function cmdSend(rest) {
3328
3334
  pid: sender.agent.pid,
3329
3335
  cli: sender.agent.cli,
3330
3336
  cwd: sender.agent.cwd,
3331
- agent_id: sender.agent.agent_id
3337
+ agent_id: sender.agent.agent_id,
3338
+ role: sender.agent.role ?? void 0
3332
3339
  } : null,
3333
3340
  to: {
3334
3341
  pid: record.pid,
3335
3342
  cli: record.cli,
3336
3343
  cwd: record.cwd,
3337
- agent_id: record.agent_id
3344
+ agent_id: record.agent_id,
3345
+ role: record.role ?? void 0
3338
3346
  },
3339
3347
  body,
3340
3348
  code: trailing === "\r" ? void 0 : codeName,
@@ -3455,6 +3463,67 @@ async function cmdMsgs(rest) {
3455
3463
  }
3456
3464
  return 0;
3457
3465
  }
3466
+ /**
3467
+ * `ay role [name]` / `ay role <keyword> <name>` — read or set an agent's
3468
+ * self-declared lane/role name ("pm", "crm", …). The role rides in the
3469
+ * `ay send` envelope header (`from claude #123 (pm) @ …`) so recipients don't
3470
+ * translate cwd/pid into a lane by hand — the emergent `[pm→qa]` body prefixes
3471
+ * exist exactly because the envelope lacked this. With no args: print the
3472
+ * calling agent's role. With one arg: set the calling agent's role (requires
3473
+ * agent context). With two args: resolve the keyword and set that agent's role.
3474
+ * `--clear` removes it. Roles are clamped by sanitizeRole (they render into the
3475
+ * non-nonce-protected open tag).
3476
+ */
3477
+ const ROLE_RESOLVE_OPTS = {
3478
+ all: false,
3479
+ active: false,
3480
+ cwdScope: null,
3481
+ latest: false,
3482
+ json: false
3483
+ };
3484
+ async function cmdRole(rest) {
3485
+ const argv = await yargs(rest).usage("Usage: ay role [name] | ay role <keyword> <name> | ay role [keyword] --clear").option("clear", {
3486
+ type: "boolean",
3487
+ default: false,
3488
+ description: "Remove the role"
3489
+ }).help(false).version(false).exitProcess(false).parseAsync();
3490
+ const args = argv._.slice(0).map(String);
3491
+ let target = null;
3492
+ let newRole;
3493
+ if (args.length === 2) {
3494
+ target = await resolveOne(args[0], ROLE_RESOLVE_OPTS);
3495
+ newRole = args[1];
3496
+ } else if (args.length === 1 && argv.clear) target = await resolveOne(args[0], ROLE_RESOLVE_OPTS);
3497
+ else if (args.length === 1) {
3498
+ target = await resolveSender();
3499
+ if (!target) {
3500
+ process.stderr.write("ay role: no agent context (AGENT_YES_PID unset or unregistered) — from a human shell, name the agent: ay role <keyword> <name>\n");
3501
+ return 1;
3502
+ }
3503
+ newRole = args[0];
3504
+ } else {
3505
+ target = await resolveSender();
3506
+ if (!target) {
3507
+ process.stderr.write("ay role: no agent context — from a human shell, name the agent: ay role <keyword> [<name>|--clear]\n");
3508
+ return 1;
3509
+ }
3510
+ }
3511
+ if (newRole === void 0 && !argv.clear) {
3512
+ process.stdout.write(`${target.role ?? ""}\n`);
3513
+ return 0;
3514
+ }
3515
+ let role = null;
3516
+ if (!argv.clear) {
3517
+ role = sanitizeRole(newRole);
3518
+ if (!role) {
3519
+ process.stderr.write(`ay role: invalid role ${JSON.stringify(newRole)} — use 1-32 chars of [A-Za-z0-9._-]\n`);
3520
+ return 1;
3521
+ }
3522
+ }
3523
+ await updateGlobalPidStatus(target.pid, { role });
3524
+ process.stdout.write(role ? `pid ${target.pid} (${target.cli}): role set to "${role}" — envelopes now read: from ${target.cli} #${target.pid} (${role}) @ ${shortenPath(target.cwd)}\n` : `pid ${target.pid} (${target.cli}): role cleared\n`);
3525
+ return 0;
3526
+ }
3458
3527
  async function resolveWritableAgent(keyword, opts) {
3459
3528
  const record = await resolveOne(keyword, opts);
3460
3529
  if (!record.fifo_file) throw new Error(`pid ${record.pid}: no fifo_file recorded — this agent didn't register a stdin FIFO (an older agent, or one not started with --stdpush). Restarting it (ay restart ${record.pid}) re-registers one.`);
@@ -3887,7 +3956,7 @@ async function cmdAttach(rest) {
3887
3956
  const winsizeDir = path.join(ayHome, "winsize");
3888
3957
  await mkdir(winsizeDir, { recursive: true });
3889
3958
  const winsizePath = path.join(winsizeDir, String(record.pid));
3890
- const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-C2mQMjUR.js");
3959
+ const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-DctJ7tY-.js");
3891
3960
  const daemonBase = await resolveDaemonHttpBase().catch(() => null);
3892
3961
  const daemonToken = daemonBase ? await loadTokenReadOnly().catch(() => null) ?? "" : "";
3893
3962
  const capViewer = `attach:${process.pid}`;
@@ -4597,7 +4666,7 @@ async function cmdNotify(rest) {
4597
4666
  }
4598
4667
  const ensure = async () => {
4599
4668
  if (!argv["ensure-daemon"]) return;
4600
- const { ensureDaemon } = await import("./notifyDaemon-BQ6_UbJS.js");
4669
+ const { ensureDaemon } = await import("./notifyDaemon-D5RtFwMq.js");
4601
4670
  await ensureDaemon().catch(() => null);
4602
4671
  };
4603
4672
  await heartbeatWatcher(parent, selfStartedAt);
@@ -4671,7 +4740,7 @@ async function cmdNotifyCursor(args) {
4671
4740
  }
4672
4741
  async function cmdNotifyd(rest) {
4673
4742
  const sub = rest[0] ?? "status";
4674
- const daemon = await import("./notifyDaemon-BQ6_UbJS.js");
4743
+ const daemon = await import("./notifyDaemon-D5RtFwMq.js");
4675
4744
  switch (sub) {
4676
4745
  case "run": return daemon.runDaemon();
4677
4746
  case "once": return daemon.runDaemon({ once: true });
@@ -4698,4 +4767,4 @@ async function cmdNotifyd(rest) {
4698
4767
 
4699
4768
  //#endregion
4700
4769
  export { liveWatchers as $, readPtysize as A, runSubcommand as B, lastStdinAt as C, readAgentPtysize as D, menuSelectKeys as E, renderRawLogLines as F, waitForLogQuiet as G, stdinActivityPath as H, resolveOne as I, appendEvent as J, writeKeysPaced as K, resolveReadWindow as L, recentReadEdges as M, renderLogTailLines as N, readLogForRender as O, renderRawLog as P, listInboxParents as Q, resolveResumeArgs as R, isUserTyping as S, matchKeyword as T, stopTipForCli as U, snapshotStatus as V, submitAndConfirm as W, hostId as X, gcInboxes as Y, isTransientLockMkdirError as Z, isExitRequest as _, backoffWhileTyping as a, TYPING_BADGE as at, isSubcommand as b, cursorAbs as c, extractBadges as d, readInbox as et, extractMenu as f, isAgentStuck as g, finalizedLines as h, TYPING_WINDOW_MS as i, notifyDir as it, recentMessageEdges as j, readNotes as k, deriveLiveState as l, extractTaskCounts as m, MAX_RENDER_BYTES as n, daemonLockDir as nt, cmdHelp as o, extractNeedsInput as p, writeToIpc as q, READ_PAGE_DEFAULT as r, daemonLockOwnerPath as rt, controlCodeFromName as s, GRACEFUL_EXIT_COMMANDS as t, shouldStealLock as tt, deriveLiveStatus as u, isPidAlive as v, listRecords as w, isUnknownManagerToken as x, isSlashCommand as y, restartHintLines as z };
4701
- //# sourceMappingURL=subcommands-CiX7J7hA.js.map
4770
+ //# sourceMappingURL=subcommands-D_cPHm34.js.map
@@ -1,10 +1,10 @@
1
1
  import "./logger-CDIsZ-Pp.js";
2
- import "./globalPidIndex-Dq4KY2IV.js";
2
+ import "./globalPidIndex-pwp7Vb7o.js";
3
3
  import "./messageLog-BYFwioFY.js";
4
4
  import "./e2e-CUMZC53I.js";
5
5
  import "./configShared-DtHMBCl7.js";
6
- import { A as readPtysize, B as runSubcommand, C as lastStdinAt, D as readAgentPtysize, E as menuSelectKeys, F as renderRawLogLines, G as waitForLogQuiet, H as stdinActivityPath, I as resolveOne, K as writeKeysPaced, L as resolveReadWindow, M as recentReadEdges, N as renderLogTailLines, O as readLogForRender, P as renderRawLog, R as resolveResumeArgs, S as isUserTyping, T as matchKeyword, U as stopTipForCli, V as snapshotStatus, W as submitAndConfirm, _ as isExitRequest, a as backoffWhileTyping, b as isSubcommand, c as cursorAbs, d as extractBadges, f as extractMenu, g as isAgentStuck, h as finalizedLines, i as TYPING_WINDOW_MS, j as recentMessageEdges, k as readNotes, l as deriveLiveState, m as extractTaskCounts, n as MAX_RENDER_BYTES, o as cmdHelp, p as extractNeedsInput, q as writeToIpc, r as READ_PAGE_DEFAULT, s as controlCodeFromName, t as GRACEFUL_EXIT_COMMANDS, u as deriveLiveStatus, v as isPidAlive, w as listRecords, x as isUnknownManagerToken, y as isSlashCommand, z as restartHintLines } from "./subcommands-CiX7J7hA.js";
6
+ import { A as readPtysize, B as runSubcommand, C as lastStdinAt, D as readAgentPtysize, E as menuSelectKeys, F as renderRawLogLines, G as waitForLogQuiet, H as stdinActivityPath, I as resolveOne, K as writeKeysPaced, L as resolveReadWindow, M as recentReadEdges, N as renderLogTailLines, O as readLogForRender, P as renderRawLog, R as resolveResumeArgs, S as isUserTyping, T as matchKeyword, U as stopTipForCli, V as snapshotStatus, W as submitAndConfirm, _ as isExitRequest, a as backoffWhileTyping, b as isSubcommand, c as cursorAbs, d as extractBadges, f as extractMenu, g as isAgentStuck, h as finalizedLines, i as TYPING_WINDOW_MS, j as recentMessageEdges, k as readNotes, l as deriveLiveState, m as extractTaskCounts, n as MAX_RENDER_BYTES, o as cmdHelp, p as extractNeedsInput, q as writeToIpc, r as READ_PAGE_DEFAULT, s as controlCodeFromName, t as GRACEFUL_EXIT_COMMANDS, u as deriveLiveStatus, v as isPidAlive, w as listRecords, x as isUnknownManagerToken, y as isSlashCommand, z as restartHintLines } from "./subcommands-D_cPHm34.js";
7
7
  import "./webrtcLink-CS4D-3G1.js";
8
- import "./remotes-Dy4Rg8Yp.js";
8
+ import "./remotes-Cn-wszbK.js";
9
9
 
10
10
  export { cmdHelp, isSubcommand, isUnknownManagerToken, runSubcommand };
@@ -0,0 +1,91 @@
1
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, symlinkSync, unlinkSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ //#region ts/systemPathLink.ts
6
+ /** Did a (now-dangling) symlink at `linkPath` point into `dir`? Used to adopt
7
+ * only stale links we previously created, never a foreign broken link. */
8
+ function danglingPointsInto(linkPath, dir) {
9
+ try {
10
+ const oldTarget = path.resolve(path.dirname(linkPath), readlinkSync(linkPath));
11
+ return path.resolve(path.dirname(oldTarget)) === path.resolve(dir);
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+ /** The directory bun installs global binaries into, or null if absent. */
17
+ function bunBinDir(env = process.env) {
18
+ const install = env.BUN_INSTALL || path.join(homedir(), ".bun");
19
+ const dir = path.join(install, "bin");
20
+ return existsSync(dir) ? dir : null;
21
+ }
22
+ /**
23
+ * Symlink each entry of the bun-global bin dir into `target` (default
24
+ * /usr/local/bin). Skips a name that already exists UNLESS it's a symlink we
25
+ * previously created into the same bun dir (those we refresh, so a rebuilt/moved
26
+ * bun bin stays valid). Returns null on an unsupported platform or when neither
27
+ * the bun dir nor the target is usable. Never throws.
28
+ */
29
+ function linkBunBinsToSystemPath(opts) {
30
+ if (process.platform === "win32") return null;
31
+ const env = opts?.env ?? process.env;
32
+ const bunDir = opts?.bunDir ?? bunBinDir(env);
33
+ if (!bunDir) return null;
34
+ const target = opts?.target ?? "/usr/local/bin";
35
+ try {
36
+ if (!existsSync(target)) mkdirSync(target, { recursive: true });
37
+ } catch {
38
+ return null;
39
+ }
40
+ const result = {
41
+ target,
42
+ linked: [],
43
+ skipped: [],
44
+ refreshed: []
45
+ };
46
+ let entries;
47
+ try {
48
+ entries = readdirSync(bunDir);
49
+ } catch {
50
+ return null;
51
+ }
52
+ for (const name of entries) {
53
+ const src = path.join(bunDir, name);
54
+ const dest = path.join(target, name);
55
+ let existing = null;
56
+ try {
57
+ existing = lstatSync(dest);
58
+ } catch {
59
+ existing = null;
60
+ }
61
+ if (existing) {
62
+ let ours = false;
63
+ if (existing.isSymbolicLink()) try {
64
+ ours = path.resolve(target, readlinkSync(dest)) === src;
65
+ } catch {
66
+ ours = false;
67
+ }
68
+ if (ours) result.skipped.push(name);
69
+ else if (existing.isSymbolicLink() && !existsSync(dest) && danglingPointsInto(dest, bunDir)) try {
70
+ unlinkSync(dest);
71
+ symlinkSync(src, dest);
72
+ result.refreshed.push(name);
73
+ } catch {
74
+ result.skipped.push(name);
75
+ }
76
+ else result.skipped.push(name);
77
+ continue;
78
+ }
79
+ try {
80
+ symlinkSync(src, dest);
81
+ result.linked.push(name);
82
+ } catch {
83
+ result.skipped.push(name);
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+
89
+ //#endregion
90
+ export { linkBunBinsToSystemPath };
91
+ //# sourceMappingURL=systemPathLink-DTAhFXFx.js.map
@@ -98,7 +98,7 @@ async function cmdTermMint(args) {
98
98
  if (flags.ro && flags.interactive) throw new Error("--ro and --interactive are mutually exclusive");
99
99
  const ttlSec = parseTtlSec(typeof flags.ttl === "string" ? flags.ttl : "15m");
100
100
  const canSend = flags.interactive === true;
101
- const { mintScopedTermToken } = await import("./serve-C2mQMjUR.js");
101
+ const { mintScopedTermToken } = await import("./serve-DctJ7tY-.js");
102
102
  const r = await mintScopedTermToken(pid, {
103
103
  ttlSec,
104
104
  canSend
@@ -135,4 +135,4 @@ async function cmdTerm(args) {
135
135
 
136
136
  //#endregion
137
137
  export { cmdTerm };
138
- //# sourceMappingURL=terminal-i7IimD_R.js.map
138
+ //# sourceMappingURL=terminal-CB5K4Jjh.js.map
@@ -1,7 +1,7 @@
1
1
  import "./logger-CDIsZ-Pp.js";
2
2
  import { _ as isKnownKind, a as renderTree, d as openStore, i as renderDigest, l as CycleError, n as buildTreeJSON, o as unblockedTasks, p as LIFECYCLES, s as describeBlock, t as reconcileTodos, x as removeControlCharacters } from "./todoAutomation-BQRW614S.js";
3
3
  import "./JsonlStore-CPGIR0Wy.js";
4
- import { i as readGlobalPids } from "./globalPidIndex-Dq4KY2IV.js";
4
+ import { i as readGlobalPids } from "./globalPidIndex-pwp7Vb7o.js";
5
5
  import yargs from "yargs";
6
6
 
7
7
  //#region ts/todoCli.ts
@@ -456,4 +456,4 @@ async function runTodoSubcommand(rest0) {
456
456
 
457
457
  //#endregion
458
458
  export { runTodoSubcommand };
459
- //# sourceMappingURL=todoCli-B2kqgqPh.js.map
459
+ //# sourceMappingURL=todoCli-DNOk38lq.js.map
@@ -187,4 +187,4 @@ async function startTray() {
187
187
 
188
188
  //#endregion
189
189
  export { ensureTray, startTray };
190
- //# sourceMappingURL=tray-CgkwxtG6.js.map
190
+ //# sourceMappingURL=tray-wWMvGWeY.js.map
@@ -0,0 +1,5 @@
1
+ import "./versionChecker-DRijpksb.js";
2
+ import "./rustBinary-BEscpLLP.js";
3
+ import { a as trayHiddenMarker, i as launchTray, n as hasDesktop, r as isTrayHidden, t as cmdTray } from "./trayApp-Ck3kWM-7.js";
4
+
5
+ export { cmdTray };
@@ -1,4 +1,4 @@
1
- import { n as findTrayLauncher } from "./rustBinary-orhBmTNo.js";
1
+ import { n as findTrayLauncher } from "./rustBinary-BEscpLLP.js";
2
2
  import { t as agentYesHome } from "./agentYesHome-CtmsY6I-.js";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
@@ -97,4 +97,4 @@ async function cmdTray(args) {
97
97
 
98
98
  //#endregion
99
99
  export { trayHiddenMarker as a, launchTray as i, hasDesktop as n, isTrayHidden as r, cmdTray as t };
100
- //# sourceMappingURL=trayApp-4sZgxxP-.js.map
100
+ //# sourceMappingURL=trayApp-Ck3kWM-7.js.map
@@ -1,11 +1,11 @@
1
1
  import { n as __esmMin, r as __exportAll } from "./chunk-cZBX9HZv.js";
2
2
  import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
3
- import { r as getInstalledPackage } from "./versionChecker-YDl8a9il.js";
3
+ import { r as getInstalledPackage } from "./versionChecker-DRijpksb.js";
4
4
  import { t as agentYesHome } from "./agentYesHome-CtmsY6I-.js";
5
5
  import { d as openStore, p as LIFECYCLES, x as removeControlCharacters } from "./todoAutomation-BQRW614S.js";
6
6
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-DBoONoM-.js";
7
- import { i as readGlobalPids } from "./globalPidIndex-Dq4KY2IV.js";
8
- import { t as PidStore } from "./pidStore-Cmcc8Oej.js";
7
+ import { i as readGlobalPids } from "./globalPidIndex-pwp7Vb7o.js";
8
+ import { t as PidStore } from "./pidStore-DfSm1jog.js";
9
9
  import { r as recordInbox } from "./messageLog-BYFwioFY.js";
10
10
  import { n as register, r as sweep } from "./reaper-C4BvNylH.js";
11
11
  import { arch, platform } from "process";
@@ -1569,6 +1569,7 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
1569
1569
  const parentPid = Number.isInteger(inheritedAyPid) && inheritedAyPid > 0 ? inheritedAyPid : void 0;
1570
1570
  ptyEnv.AGENT_YES_PID = String(process.pid);
1571
1571
  delete ptyEnv.AGENT_YES_AGENT_ID;
1572
+ delete ptyEnv.AGENT_YES_ROLE;
1572
1573
  stripClaudeSessionPin(ptyEnv);
1573
1574
  if (cliConf?.env) for (const [key, raw] of Object.entries(cliConf.env)) {
1574
1575
  let unresolved = false;
@@ -2180,4 +2181,4 @@ function sleep(ms) {
2180
2181
 
2181
2182
  //#endregion
2182
2183
  export { hasTodoStore as a, permissionBadge as c, answerAsk as i, AgentContext as l, agentYes as n, listAsks as o, config as r, listAsksForProject as s, CLIS_CONFIG as t, CLAUDE_SESSION_PIN_ENV as u };
2183
- //# sourceMappingURL=ts-D-E4xqpA.js.map
2184
+ //# sourceMappingURL=ts-Bd_7sgVu.js.map
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
7
7
 
8
8
  //#region package.json
9
9
  var name = "agent-yes";
10
- var version = "1.258.3";
10
+ var version = "1.259.0";
11
11
 
12
12
  //#endregion
13
13
  //#region ts/versionChecker.ts
@@ -215,4 +215,4 @@ async function displayVersion() {
215
215
 
216
216
  //#endregion
217
217
  export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
218
- //# sourceMappingURL=versionChecker-YDl8a9il.js.map
218
+ //# sourceMappingURL=versionChecker-DRijpksb.js.map
@@ -256,4 +256,4 @@ async function startWebrtcBridge(link) {
256
256
 
257
257
  //#endregion
258
258
  export { startWebrtcBridge };
259
- //# sourceMappingURL=webrtcRemote-C3OlHYSt.js.map
259
+ //# sourceMappingURL=webrtcRemote-BkZcA5yq.js.map
@@ -28,7 +28,7 @@ function parseFlags(args, known) {
28
28
  }
29
29
  /** Resolve the local daemon base URL + token (flags override discovery). */
30
30
  async function daemonTarget(flags) {
31
- const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-C2mQMjUR.js");
31
+ const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-DctJ7tY-.js");
32
32
  const base = typeof flags.base === "string" ? flags.base : await resolveDaemonHttpBase();
33
33
  if (!base) throw new Error("no running ay serve daemon found — start `ay serve` or pass --base <url> (e.g. http://127.0.0.1:PORT)");
34
34
  const token = typeof flags.token === "string" ? flags.token : await loadTokenReadOnly() ?? "";
@@ -186,7 +186,7 @@ async function cmdMint(args) {
186
186
  const bad = caps.filter((c) => !KNOWN_CAPS.has(c));
187
187
  if (bad.length) throw new Error(`unknown cap(s): ${bad.join(", ")} (valid: ${[...KNOWN_CAPS].join(", ")})`);
188
188
  const ttlSec = parseTtlSec(typeof flags.ttl === "string" ? flags.ttl : "15m");
189
- const { mintScopedTermToken } = await import("./serve-C2mQMjUR.js");
189
+ const { mintScopedTermToken } = await import("./serve-DctJ7tY-.js");
190
190
  const r = await mintScopedTermToken(target, {
191
191
  ttlSec,
192
192
  caps
@@ -202,4 +202,4 @@ async function cmdMint(args) {
202
202
 
203
203
  //#endregion
204
204
  export { cmdMint, cmdWidget };
205
- //# sourceMappingURL=widget-7j9RrYZR.js.map
205
+ //# sourceMappingURL=widget-DiRKbtng.js.map
@@ -1,4 +1,4 @@
1
- import { w as listRecords } from "./subcommands-CiX7J7hA.js";
1
+ import { w as listRecords } from "./subcommands-D_cPHm34.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-DjyQ20AD.js.map
429
+ //# sourceMappingURL=ws-Bp8eOEoV.js.map
@@ -1,11 +1,11 @@
1
1
  import "./logger-CDIsZ-Pp.js";
2
- import "./globalPidIndex-Dq4KY2IV.js";
2
+ import "./globalPidIndex-pwp7Vb7o.js";
3
3
  import "./messageLog-BYFwioFY.js";
4
4
  import "./e2e-CUMZC53I.js";
5
5
  import "./configShared-DtHMBCl7.js";
6
- import "./subcommands-CiX7J7hA.js";
6
+ import "./subcommands-D_cPHm34.js";
7
7
  import "./webrtcLink-CS4D-3G1.js";
8
- import "./remotes-Dy4Rg8Yp.js";
9
- import { a as isPathInside, c as walkWorkspaces, i as importProvisionModule, l as workspaceStatus, n as cmdWs, o as loadProvision, r as collectWorkspaces, s as resolveOperand, t as WS_JSON_SCHEMA } from "./ws-DjyQ20AD.js";
8
+ import "./remotes-Cn-wszbK.js";
9
+ import { a as isPathInside, c as walkWorkspaces, i as importProvisionModule, l as workspaceStatus, n as cmdWs, o as loadProvision, r as collectWorkspaces, s as resolveOperand, t as WS_JSON_SCHEMA } from "./ws-Bp8eOEoV.js";
10
10
 
11
11
  export { WS_JSON_SCHEMA, cmdWs, collectWorkspaces, isPathInside, loadProvision, workspaceStatus };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.258.3",
3
+ "version": "1.259.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,6 +158,42 @@ 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 () => {
177
+ const mod = await loadModule();
178
+ await mod.appendGlobalPid({
179
+ pid: 4242,
180
+ cli: "claude",
181
+ prompt: null,
182
+ cwd: "/repo",
183
+ log_file: null,
184
+ status: "active",
185
+ exit_code: null,
186
+ exit_reason: null,
187
+ started_at: Date.now(),
188
+ });
189
+ await mod.updateGlobalPidStatus(4242, { role: "pm" });
190
+ let rec = (await mod.readGlobalPids()).find((r: any) => r.pid === 4242);
191
+ expect(rec?.role).toBe("pm");
192
+ await mod.updateGlobalPidStatus(4242, { role: null });
193
+ rec = (await mod.readGlobalPids()).find((r: any) => r.pid === 4242);
194
+ expect(rec?.role).toBeNull();
195
+ });
196
+
161
197
  it("updateGlobalPidStatus is a no-op for unknown pids", async () => {
162
198
  const mod = await loadModule();
163
199
  await mod.updateGlobalPidStatus(7777, { status: "exited" });
@@ -65,6 +65,24 @@ 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
86
  }
69
87
 
70
88
  /**
@@ -118,7 +136,7 @@ export async function appendGlobalPid(record: GlobalPidRecord): Promise<void> {
118
136
  /** Append a partial update by pid (status, exit_code, exit_reason, log_file). */
119
137
  export async function updateGlobalPidStatus(
120
138
  pid: number,
121
- patch: Partial<Pick<GlobalPidRecord, "status" | "exit_code" | "exit_reason" | "log_file">>,
139
+ patch: Partial<Pick<GlobalPidRecord, "status" | "exit_code" | "exit_reason" | "log_file" | "role">>,
122
140
  ): Promise<void> {
123
141
  const file = resolveGlobalFile(); // capture at call time (see withLock)
124
142
  try {