agent-yes 1.83.0 → 1.85.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/ts/index.ts CHANGED
@@ -28,6 +28,7 @@ import { AgentContext } from "./core/context.ts";
28
28
  import { createTerminatorStream } from "./core/streamHelpers.ts";
29
29
  import { globalAgentRegistry } from "./agentRegistry.ts";
30
30
  import { notifyWebhook } from "./webhookNotifier.ts";
31
+ import { readGlobalPids } from "./globalPidIndex.ts";
31
32
 
32
33
  export { removeControlCharacters };
33
34
  export { AgentContext };
@@ -124,6 +125,7 @@ export default async function agentYes({
124
125
  useStdinAppend = false,
125
126
  autoYes = true,
126
127
  idleAction,
128
+ swarmHint = true,
127
129
  }: {
128
130
  cli: keyof typeof CLIS_CONFIG;
129
131
  cliArgs?: string[];
@@ -142,6 +144,7 @@ export default async function agentYes({
142
144
  useStdinAppend?: boolean; // if true, enable FIFO input stream on Linux, for additional stdin input
143
145
  autoYes?: boolean; // if true, auto-yes is enabled (default), toggle with Ctrl+Y during session
144
146
  idleAction?: string; // if set, type this message when idle instead of exiting
147
+ swarmHint?: boolean; // if true (default), inject peer discovery hint when other agents are running; --no-swarm-hint to opt out
145
148
  }) {
146
149
  if (!cli) throw new Error(`cli is required`);
147
150
  const conf =
@@ -274,6 +277,23 @@ export default async function agentYes({
274
277
  if (verbose) logger.warn("[skills] Failed to inject SKILL.md header:", { error });
275
278
  }
276
279
 
280
+ // Inject peer discovery hint when other agents are running
281
+ if (swarmHint) {
282
+ try {
283
+ const peers = await readGlobalPids({ liveOnly: true });
284
+ if (peers.length > 0) {
285
+ const hint = `${peers.length} peer agent${peers.length > 1 ? "s are" : " is"} running. Before asking the user for input on any domain-specific topic (design, testing, architecture, etc.), check for relevant peers first: \`ay ls --json\` (see \`prompt\` field for their role). Ask one: \`ay send <pid> <question>\`. Read reply: \`ay tail <pid>\`. Do not use interactive forms or user prompts when a peer can answer.`;
286
+ if (cli === "claude") {
287
+ cliArgs = ["--append-system-prompt", hint, ...cliArgs];
288
+ }
289
+ // Prepend to prompt for all CLIs (including claude) so it's read before the task
290
+ prompt = prompt ? `[${hint}]\n\n${prompt}` : hint;
291
+ }
292
+ } catch {
293
+ // Non-fatal
294
+ }
295
+ }
296
+
277
297
  // Handle --continue flag for codex session restoration
278
298
  if (resume) {
279
299
  if (cli === "codex" && resume) {
@@ -322,6 +342,7 @@ export default async function agentYes({
322
342
 
323
343
  // Spawn the agent CLI process
324
344
  const ptyEnv = { ...(env ?? (process.env as Record<string, string>)) };
345
+ ptyEnv.AGENT_YES_PID = String(process.pid);
325
346
  const ptyOptions = {
326
347
  name: "xterm-color",
327
348
  ...getTerminalDimensions(),
@@ -63,6 +63,12 @@ export function parseCliArgs(argv: string[], supportedClis?: readonly string[])
63
63
  "Prepend SKILL.md header from current directory to the prompt (helpful for non-Claude agents)",
64
64
  default: false,
65
65
  })
66
+ .option("swarm-hint", {
67
+ type: "boolean",
68
+ description:
69
+ "Inject peer discovery hint into agent system prompt when other agents are running (use --no-swarm-hint to opt out)",
70
+ default: true,
71
+ })
66
72
  .option("timeout", {
67
73
  type: "string",
68
74
  description: 'Exit after a period of inactivity, e.g., "5s" or "1m"',
@@ -313,6 +319,7 @@ export function parseCliArgs(argv: string[], supportedClis?: readonly string[])
313
319
  verbose: parsedArgv.verbose,
314
320
  resume: parsedArgv.continue, // Note: intentional use resume here to avoid preserved keyword (continue)
315
321
  useSkills: parsedArgv.useSkills,
322
+ swarmHint: parsedArgv.swarmHint,
316
323
  appendPrompt: parsedArgv.appendPrompt,
317
324
  useStdinAppend: Boolean(parsedArgv.stdpush || parsedArgv.ipc || parsedArgv.fifo), // Support --stdpush, --ipc, and --fifo (backward compatibility)
318
325
  showVersion: parsedArgv.version,
@@ -27,41 +27,6 @@ async function loadModule() {
27
27
  return await import("./subcommands.ts");
28
28
  }
29
29
 
30
- describe("subcommands.parseArgs", () => {
31
- it("collects positional args and bare flags", async () => {
32
- const { parseArgs } = await loadModule();
33
- const out = parseArgs(["foo", "bar", "--all"]);
34
- expect(out.positional).toEqual(["foo", "bar"]);
35
- expect(out.flags.all).toBe(true);
36
- });
37
-
38
- it("parses --key=value form", async () => {
39
- const { parseArgs } = await loadModule();
40
- const out = parseArgs(["--code=enter"]);
41
- expect(out.flags.code).toBe("enter");
42
- });
43
-
44
- it("parses --key value form for non-boolean keys", async () => {
45
- const { parseArgs } = await loadModule();
46
- const out = parseArgs(["--cwd", "/tmp/foo"]);
47
- expect(out.flags.cwd).toBe("/tmp/foo");
48
- });
49
-
50
- it("treats well-known boolean flags as boolean even with a following positional", async () => {
51
- const { parseArgs } = await loadModule();
52
- const out = parseArgs(["--all", "claude"]);
53
- expect(out.flags.all).toBe(true);
54
- expect(out.positional).toEqual(["claude"]);
55
- });
56
-
57
- it("supports -n N short form", async () => {
58
- const { parseArgs } = await loadModule();
59
- const out = parseArgs(["-n", "50", "keyword"]);
60
- expect(out.flags.n).toBe("50");
61
- expect(out.positional).toEqual(["keyword"]);
62
- });
63
- });
64
-
65
30
  describe("subcommands.controlCodeFromName", () => {
66
31
  it("maps named codes to the right control bytes", async () => {
67
32
  const { controlCodeFromName } = await loadModule();
@@ -525,6 +490,484 @@ describe("subcommands.cmdSend writes bytes to FIFO", () => {
525
490
  });
526
491
  });
527
492
 
493
+ // ---------------------------------------------------------------------------
494
+ // cmdLs additional arg coverage
495
+ // ---------------------------------------------------------------------------
496
+
497
+ describe("subcommands.cmdLs -h / --help", () => {
498
+ function captureStdout() {
499
+ const chunks: string[] = [];
500
+ const orig = process.stdout.write.bind(process.stdout);
501
+ (process.stdout as any).write = (s: any) => {
502
+ chunks.push(String(s));
503
+ return true;
504
+ };
505
+ return {
506
+ get text() {
507
+ return chunks.join("");
508
+ },
509
+ restore() {
510
+ process.stdout.write = orig;
511
+ },
512
+ };
513
+ }
514
+
515
+ it("ay ls -h prints usage to stdout and exits 0", async () => {
516
+ const { runSubcommand } = await loadModule();
517
+ const cap = captureStdout();
518
+ let code: number | null;
519
+ try {
520
+ code = await runSubcommand(["bun", "cli.js", "ls", "-h"]);
521
+ } finally {
522
+ cap.restore();
523
+ }
524
+ expect(code).toBe(0);
525
+ expect(cap.text).toMatch(/Usage:/);
526
+ expect(cap.text).toMatch(/--all/);
527
+ expect(cap.text).toMatch(/--json/);
528
+ });
529
+
530
+ it("ay ls --help prints usage to stdout and exits 0", async () => {
531
+ const { runSubcommand } = await loadModule();
532
+ const cap = captureStdout();
533
+ let code: number | null;
534
+ try {
535
+ code = await runSubcommand(["bun", "cli.js", "ls", "--help"]);
536
+ } finally {
537
+ cap.restore();
538
+ }
539
+ expect(code).toBe(0);
540
+ expect(cap.text).toMatch(/Usage:/);
541
+ });
542
+ });
543
+
544
+ describe("subcommands.cmdLs --all / --active / keyword filter / aliases", () => {
545
+ function captureOutput() {
546
+ const out: string[] = [];
547
+ const err: string[] = [];
548
+ const origOut = process.stdout.write.bind(process.stdout);
549
+ const origErr = process.stderr.write.bind(process.stderr);
550
+ (process.stdout as any).write = (s: any) => {
551
+ out.push(String(s));
552
+ return true;
553
+ };
554
+ (process.stderr as any).write = (s: any) => {
555
+ err.push(String(s));
556
+ return true;
557
+ };
558
+ return {
559
+ get stdout() {
560
+ return out.join("");
561
+ },
562
+ get stderr() {
563
+ return err.join("");
564
+ },
565
+ restore() {
566
+ process.stdout.write = origOut;
567
+ process.stderr.write = origErr;
568
+ },
569
+ };
570
+ }
571
+
572
+ it("--all shows exited agents", async () => {
573
+ const mod = await loadModule();
574
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
575
+ await appendGlobalPid({
576
+ pid: 1, // pid 1 is almost never the test process, so isPidAlive returns false
577
+ cli: "claude",
578
+ prompt: "exited agent",
579
+ cwd: process.cwd(),
580
+ log_file: null,
581
+ status: "exited",
582
+ exit_code: 0,
583
+ exit_reason: "done",
584
+ started_at: Date.now() - 10_000,
585
+ });
586
+
587
+ const cap = captureOutput();
588
+ let code: number | null;
589
+ try {
590
+ code = await mod.runSubcommand(["bun", "cli.js", "ls", "--all", "--json"]);
591
+ } finally {
592
+ cap.restore();
593
+ }
594
+ expect(code).toBe(0);
595
+ const parsed = JSON.parse(cap.stdout);
596
+ expect(Array.isArray(parsed)).toBe(true);
597
+ expect(parsed.some((r: any) => r.prompt === "exited agent")).toBe(true);
598
+ });
599
+
600
+ it("keyword filter restricts results to matching agents", async () => {
601
+ const mod = await loadModule();
602
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
603
+ await appendGlobalPid({
604
+ pid: process.pid,
605
+ cli: "claude",
606
+ prompt: "unique-xyzzy-prompt",
607
+ cwd: process.cwd(),
608
+ log_file: null,
609
+ status: "active",
610
+ exit_code: null,
611
+ exit_reason: null,
612
+ started_at: Date.now(),
613
+ });
614
+
615
+ const cap = captureOutput();
616
+ let code: number | null;
617
+ try {
618
+ code = await mod.runSubcommand(["bun", "cli.js", "ls", "--json", "unique-xyzzy-prompt"]);
619
+ } finally {
620
+ cap.restore();
621
+ }
622
+ expect(code).toBe(0);
623
+ const parsed = JSON.parse(cap.stdout);
624
+ expect(parsed.every((r: any) => r.prompt?.includes("unique-xyzzy-prompt"))).toBe(true);
625
+ });
626
+
627
+ it("keyword filter returns 'no running agents' when nothing matches", async () => {
628
+ const { runSubcommand } = await loadModule();
629
+ const stderr: string[] = [];
630
+ const orig = process.stderr.write.bind(process.stderr);
631
+ (process.stderr as any).write = (s: any) => {
632
+ stderr.push(String(s));
633
+ return true;
634
+ };
635
+ try {
636
+ const code = await runSubcommand(["bun", "cli.js", "ls", "no-match-zzzzzz"]);
637
+ expect(code).toBe(0);
638
+ expect(stderr.join("")).toMatch(/no running agents matched/);
639
+ } finally {
640
+ process.stderr.write = orig;
641
+ }
642
+ });
643
+
644
+ it("list alias routes to cmdLs", async () => {
645
+ const { runSubcommand } = await loadModule();
646
+ const stderr: string[] = [];
647
+ const orig = process.stderr.write.bind(process.stderr);
648
+ (process.stderr as any).write = (s: any) => {
649
+ stderr.push(String(s));
650
+ return true;
651
+ };
652
+ try {
653
+ const code = await runSubcommand(["bun", "cli.js", "list"]);
654
+ expect(code).toBe(0);
655
+ } finally {
656
+ process.stderr.write = orig;
657
+ }
658
+ });
659
+
660
+ it("ps alias routes to cmdLs", async () => {
661
+ const { runSubcommand } = await loadModule();
662
+ const stderr: string[] = [];
663
+ const orig = process.stderr.write.bind(process.stderr);
664
+ (process.stderr as any).write = (s: any) => {
665
+ stderr.push(String(s));
666
+ return true;
667
+ };
668
+ try {
669
+ const code = await runSubcommand(["bun", "cli.js", "ps"]);
670
+ expect(code).toBe(0);
671
+ } finally {
672
+ process.stderr.write = orig;
673
+ }
674
+ });
675
+ });
676
+
677
+ // ---------------------------------------------------------------------------
678
+ // cmdRead — head and cat modes
679
+ // ---------------------------------------------------------------------------
680
+
681
+ describe("subcommands.cmdRead head and cat modes", () => {
682
+ it("head emits first N lines", async () => {
683
+ const { runSubcommand } = await loadModule();
684
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
685
+ const tmp = await mkdtemp(path.join(tmpdir(), "ay-head-log-"));
686
+ try {
687
+ const logPath = path.join(tmp, "x.raw.log");
688
+ const lines: string[] = [];
689
+ for (let i = 0; i < 50; i++) lines.push(`line-${i}`);
690
+ await writeFile(logPath, lines.join("\r\n") + "\r\n");
691
+
692
+ await appendGlobalPid({
693
+ pid: process.pid,
694
+ cli: "claude",
695
+ prompt: null,
696
+ cwd: process.cwd(),
697
+ log_file: logPath,
698
+ status: "active",
699
+ exit_code: null,
700
+ exit_reason: null,
701
+ started_at: Date.now(),
702
+ });
703
+
704
+ const stdout: string[] = [];
705
+ const orig = process.stdout.write.bind(process.stdout);
706
+ (process.stdout as any).write = (s: any) => {
707
+ stdout.push(String(s));
708
+ return true;
709
+ };
710
+ const stderr_chunks: string[] = [];
711
+ const origErr = process.stderr.write.bind(process.stderr);
712
+ (process.stderr as any).write = (s: any) => {
713
+ stderr_chunks.push(String(s));
714
+ return true;
715
+ };
716
+ try {
717
+ const code = await runSubcommand(["bun", "cli.js", "head", String(process.pid), "-n", "5"]);
718
+ expect(code).toBe(0);
719
+ } finally {
720
+ process.stdout.write = orig;
721
+ process.stderr.write = origErr;
722
+ }
723
+ const text = stdout.join("");
724
+ expect(text).toMatch(/line-0/);
725
+ expect(text).toMatch(/line-4/);
726
+ expect(text).not.toMatch(/line-10\b/);
727
+ } finally {
728
+ await rm(tmp, { recursive: true, force: true }).catch(() => null);
729
+ }
730
+ });
731
+
732
+ it("cat emits all lines", async () => {
733
+ const { runSubcommand } = await loadModule();
734
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
735
+ const tmp = await mkdtemp(path.join(tmpdir(), "ay-cat-log-"));
736
+ try {
737
+ const logPath = path.join(tmp, "x.raw.log");
738
+ await writeFile(logPath, "alpha\r\nbeta\r\ngamma\r\n");
739
+
740
+ await appendGlobalPid({
741
+ pid: process.pid,
742
+ cli: "claude",
743
+ prompt: null,
744
+ cwd: process.cwd(),
745
+ log_file: logPath,
746
+ status: "active",
747
+ exit_code: null,
748
+ exit_reason: null,
749
+ started_at: Date.now(),
750
+ });
751
+
752
+ const stdout: string[] = [];
753
+ const orig = process.stdout.write.bind(process.stdout);
754
+ (process.stdout as any).write = (s: any) => {
755
+ stdout.push(String(s));
756
+ return true;
757
+ };
758
+ const stderr_chunks: string[] = [];
759
+ const origErr = process.stderr.write.bind(process.stderr);
760
+ (process.stderr as any).write = (s: any) => {
761
+ stderr_chunks.push(String(s));
762
+ return true;
763
+ };
764
+ try {
765
+ const code = await runSubcommand(["bun", "cli.js", "cat", String(process.pid)]);
766
+ expect(code).toBe(0);
767
+ } finally {
768
+ process.stdout.write = orig;
769
+ process.stderr.write = origErr;
770
+ }
771
+ const text = stdout.join("");
772
+ expect(text).toMatch(/alpha/);
773
+ expect(text).toMatch(/beta/);
774
+ expect(text).toMatch(/gamma/);
775
+ } finally {
776
+ await rm(tmp, { recursive: true, force: true }).catch(() => null);
777
+ }
778
+ });
779
+ });
780
+
781
+ // ---------------------------------------------------------------------------
782
+ // cmdNote
783
+ // ---------------------------------------------------------------------------
784
+
785
+ describe("subcommands.cmdNote", () => {
786
+ it("throws usage error when no keyword given", async () => {
787
+ const { runSubcommand } = await loadModule();
788
+ const stderr: string[] = [];
789
+ const orig = process.stderr.write.bind(process.stderr);
790
+ (process.stderr as any).write = (s: any) => {
791
+ stderr.push(String(s));
792
+ return true;
793
+ };
794
+ try {
795
+ const code = await runSubcommand(["bun", "cli.js", "note"]);
796
+ expect(code).toBe(1);
797
+ expect(stderr.join("")).toMatch(/usage:/i);
798
+ } finally {
799
+ process.stderr.write = orig;
800
+ }
801
+ });
802
+
803
+ it("sets a note on a matched agent", async () => {
804
+ const mod = await loadModule();
805
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
806
+ await appendGlobalPid({
807
+ pid: process.pid,
808
+ cli: "claude",
809
+ prompt: "note-target",
810
+ cwd: process.cwd(),
811
+ log_file: null,
812
+ status: "active",
813
+ exit_code: null,
814
+ exit_reason: null,
815
+ started_at: Date.now(),
816
+ });
817
+
818
+ const stdout: string[] = [];
819
+ const origOut = process.stdout.write.bind(process.stdout);
820
+ (process.stdout as any).write = (s: any) => {
821
+ stdout.push(String(s));
822
+ return true;
823
+ };
824
+ const origErr = process.stderr.write.bind(process.stderr);
825
+ (process.stderr as any).write = () => true;
826
+ try {
827
+ const code = await mod.runSubcommand([
828
+ "bun",
829
+ "cli.js",
830
+ "note",
831
+ String(process.pid),
832
+ "my note text",
833
+ ]);
834
+ expect(code).toBe(0);
835
+ expect(stdout.join("")).toMatch(/note set/);
836
+ } finally {
837
+ process.stdout.write = origOut;
838
+ process.stderr.write = origErr;
839
+ }
840
+ });
841
+
842
+ it("clears a note when no text given", async () => {
843
+ const mod = await loadModule();
844
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
845
+ await appendGlobalPid({
846
+ pid: process.pid,
847
+ cli: "claude",
848
+ prompt: "note-clear-target",
849
+ cwd: process.cwd(),
850
+ log_file: null,
851
+ status: "active",
852
+ exit_code: null,
853
+ exit_reason: null,
854
+ started_at: Date.now(),
855
+ });
856
+
857
+ const stdout: string[] = [];
858
+ const origOut = process.stdout.write.bind(process.stdout);
859
+ (process.stdout as any).write = (s: any) => {
860
+ stdout.push(String(s));
861
+ return true;
862
+ };
863
+ (process.stderr as any).write = () => true;
864
+ try {
865
+ const code = await mod.runSubcommand(["bun", "cli.js", "note", String(process.pid)]);
866
+ expect(code).toBe(0);
867
+ expect(stdout.join("")).toMatch(/cleared note/);
868
+ } finally {
869
+ process.stdout.write = origOut;
870
+ process.stderr.write = process.stderr.write; // no-op restore (silenced above)
871
+ }
872
+ });
873
+ });
874
+
875
+ // ---------------------------------------------------------------------------
876
+ // cmdStatus
877
+ // ---------------------------------------------------------------------------
878
+
879
+ describe("subcommands.cmdStatus", () => {
880
+ it("throws usage error when no keyword given", async () => {
881
+ const { runSubcommand } = await loadModule();
882
+ const stderr: string[] = [];
883
+ const orig = process.stderr.write.bind(process.stderr);
884
+ (process.stderr as any).write = (s: any) => {
885
+ stderr.push(String(s));
886
+ return true;
887
+ };
888
+ try {
889
+ const code = await runSubcommand(["bun", "cli.js", "status"]);
890
+ expect(code).toBe(1);
891
+ expect(stderr.join("")).toMatch(/usage:/i);
892
+ } finally {
893
+ process.stderr.write = orig;
894
+ }
895
+ });
896
+
897
+ it("emits JSON snapshot for a matched agent", async () => {
898
+ const mod = await loadModule();
899
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
900
+ await appendGlobalPid({
901
+ pid: process.pid,
902
+ cli: "claude",
903
+ prompt: "status-test",
904
+ cwd: process.cwd(),
905
+ log_file: null,
906
+ status: "active",
907
+ exit_code: null,
908
+ exit_reason: null,
909
+ started_at: Date.now() - 1000,
910
+ });
911
+
912
+ const stdout: string[] = [];
913
+ const origOut = process.stdout.write.bind(process.stdout);
914
+ (process.stdout as any).write = (s: any) => {
915
+ stdout.push(String(s));
916
+ return true;
917
+ };
918
+ (process.stderr as any).write = () => true;
919
+ try {
920
+ const code = await mod.runSubcommand(["bun", "cli.js", "status", String(process.pid)]);
921
+ expect(code).toBe(0);
922
+ } finally {
923
+ process.stdout.write = origOut;
924
+ }
925
+ const snap = JSON.parse(stdout.join(""));
926
+ expect(snap).toMatchObject({ pid: process.pid, cli: "claude" });
927
+ expect(typeof snap.age_ms).toBe("number");
928
+ });
929
+ });
930
+
931
+ // ---------------------------------------------------------------------------
932
+ // cmdRestart
933
+ // ---------------------------------------------------------------------------
934
+
935
+ describe("subcommands.cmdRestart", () => {
936
+ it("returns 1 and warns when the agent is still alive", async () => {
937
+ const mod = await loadModule();
938
+ const { appendGlobalPid } = await import("./globalPidIndex.ts");
939
+ await appendGlobalPid({
940
+ pid: process.pid,
941
+ cli: "claude",
942
+ prompt: "restart-live-test",
943
+ cwd: process.cwd(),
944
+ log_file: null,
945
+ status: "active",
946
+ exit_code: null,
947
+ exit_reason: null,
948
+ started_at: Date.now(),
949
+ });
950
+
951
+ const stderr: string[] = [];
952
+ const origErr = process.stderr.write.bind(process.stderr);
953
+ (process.stderr as any).write = (s: any) => {
954
+ stderr.push(String(s));
955
+ return true;
956
+ };
957
+ try {
958
+ const code = await mod.runSubcommand(["bun", "cli.js", "restart", String(process.pid)]);
959
+ expect(code).toBe(1);
960
+ expect(stderr.join("")).toMatch(/still running/);
961
+ } finally {
962
+ process.stderr.write = origErr;
963
+ }
964
+ });
965
+ });
966
+
967
+ // ---------------------------------------------------------------------------
968
+ // listRecords merges per-cwd TS file with global
969
+ // ---------------------------------------------------------------------------
970
+
528
971
  describe("subcommands.listRecords merges per-cwd TS file with global", () => {
529
972
  it("includes records from <cwd>/.agent-yes/pid-records.jsonl", async () => {
530
973
  // Write a fake per-cwd file that uses the live process pid so liveOnly