pure-point-guard 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/lib/errors.ts
13
- var PgError, TmuxNotFoundError, NotGitRepoError, NotInitializedError, ManifestLockError, WorktreeNotFoundError, AgentNotFoundError, MergeFailedError;
13
+ var PgError, TmuxNotFoundError, NotGitRepoError, NotInitializedError, ManifestLockError, WorktreeNotFoundError, AgentNotFoundError, MergeFailedError, GhNotFoundError, UnmergedWorkError;
14
14
  var init_errors = __esm({
15
15
  "src/lib/errors.ts"() {
16
16
  "use strict";
@@ -82,6 +82,28 @@ var init_errors = __esm({
82
82
  this.name = "MergeFailedError";
83
83
  }
84
84
  };
85
+ GhNotFoundError = class extends PgError {
86
+ constructor() {
87
+ super(
88
+ "GitHub CLI (gh) is not installed or not in PATH. Install it with: brew install gh",
89
+ "GH_NOT_FOUND"
90
+ );
91
+ this.name = "GhNotFoundError";
92
+ }
93
+ };
94
+ UnmergedWorkError = class extends PgError {
95
+ constructor(names) {
96
+ const list = names.map((n) => ` ${n}`).join("\n");
97
+ super(
98
+ `${names.length} worktree(s) have completed work that hasn't been merged or PR'd:
99
+ ${list}
100
+
101
+ Use --force to reset anyway, or create PRs first with: ppg pr <worktree-id>`,
102
+ "UNMERGED_WORK"
103
+ );
104
+ this.name = "UnmergedWorkError";
105
+ }
106
+ };
85
107
  }
86
108
  });
87
109
 
@@ -199,9 +221,18 @@ function logsDir(projectRoot) {
199
221
  function promptsDir(projectRoot) {
200
222
  return path.join(pgDir(projectRoot), "prompts");
201
223
  }
224
+ function swarmsDir(projectRoot) {
225
+ return path.join(pgDir(projectRoot), "swarms");
226
+ }
202
227
  function promptFile(projectRoot, agentId2) {
203
228
  return path.join(promptsDir(projectRoot), `${agentId2}.md`);
204
229
  }
230
+ function agentPromptsDir(projectRoot) {
231
+ return path.join(pgDir(projectRoot), "agent-prompts");
232
+ }
233
+ function agentPromptFile(projectRoot, agentId2) {
234
+ return path.join(agentPromptsDir(projectRoot), `${agentId2}.md`);
235
+ }
205
236
  function worktreeBaseDir(projectRoot) {
206
237
  return path.join(projectRoot, ".worktrees");
207
238
  }
@@ -398,6 +429,326 @@ var init_manifest = __esm({
398
429
  }
399
430
  });
400
431
 
432
+ // src/bundled/prompts.ts
433
+ var bundledPrompts;
434
+ var init_prompts = __esm({
435
+ "src/bundled/prompts.ts"() {
436
+ "use strict";
437
+ bundledPrompts = {
438
+ "review-quality": `# Code Quality Review
439
+
440
+ ## What to Review
441
+ {{CONTEXT}}
442
+
443
+ ## Your Focus
444
+ You are a senior engineer reviewing code for quality, readability, and maintainability.
445
+
446
+ - Code clarity and naming conventions
447
+ - Function and module organization
448
+ - Error handling completeness
449
+ - DRY violations and unnecessary complexity
450
+ - API design and consistency
451
+ - Documentation gaps for non-obvious logic
452
+
453
+ ## Output
454
+ Write a structured review to {{RESULT_FILE}} with specific file:line references and improvement suggestions.
455
+ `,
456
+ "review-security": `# Security Review
457
+
458
+ ## What to Review
459
+ {{CONTEXT}}
460
+
461
+ ## Your Focus
462
+ You are a security engineer reviewing code for vulnerabilities and risks.
463
+
464
+ - Input validation and sanitization
465
+ - Injection vulnerabilities (SQL, XSS, command)
466
+ - Authentication and authorization issues
467
+ - Sensitive data exposure
468
+ - Dependency vulnerabilities
469
+ - Secrets or credentials in code
470
+
471
+ ## Output
472
+ Write a structured review to {{RESULT_FILE}} with severity ratings and remediation guidance.
473
+ `,
474
+ "review-regression": `# Regression & Risk Review
475
+
476
+ ## What to Review
477
+ {{CONTEXT}}
478
+
479
+ ## Your Focus
480
+ You are a QA engineer reviewing code for regression risks and test coverage gaps.
481
+
482
+ - Behavioral changes that could break existing functionality
483
+ - Edge cases and boundary conditions not covered
484
+ - Missing or inadequate test coverage
485
+ - Integration points that may be affected
486
+ - Data migration or compatibility concerns
487
+ - Performance regressions
488
+
489
+ ## Output
490
+ Write a structured review to {{RESULT_FILE}} with risk ratings and recommended test additions.
491
+ `
492
+ };
493
+ }
494
+ });
495
+
496
+ // src/bundled/swarms.ts
497
+ var bundledSwarms;
498
+ var init_swarms = __esm({
499
+ "src/bundled/swarms.ts"() {
500
+ "use strict";
501
+ bundledSwarms = {
502
+ "code-review": `name: code-review
503
+ description: Multi-perspective code review
504
+ strategy: shared
505
+
506
+ agents:
507
+ - prompt: review-quality
508
+ - prompt: review-security
509
+ - prompt: review-regression
510
+ `
511
+ };
512
+ }
513
+ });
514
+
515
+ // src/lib/env.ts
516
+ var extraDirs, home, augmentedPath, execaEnv;
517
+ var init_env = __esm({
518
+ "src/lib/env.ts"() {
519
+ "use strict";
520
+ extraDirs = [
521
+ "/opt/homebrew/bin",
522
+ "/opt/homebrew/sbin",
523
+ "/opt/local/bin"
524
+ // MacPorts
525
+ ];
526
+ home = process.env.HOME ?? "";
527
+ if (home) {
528
+ extraDirs.push(`${home}/.nix-profile/bin`);
529
+ }
530
+ augmentedPath = [...extraDirs, process.env.PATH].filter(Boolean).join(":");
531
+ execaEnv = {
532
+ env: { PATH: augmentedPath }
533
+ };
534
+ }
535
+ });
536
+
537
+ // src/core/tmux.ts
538
+ var tmux_exports = {};
539
+ __export(tmux_exports, {
540
+ attachSession: () => attachSession,
541
+ capturePane: () => capturePane,
542
+ checkTmux: () => checkTmux,
543
+ createWindow: () => createWindow,
544
+ ensureSession: () => ensureSession,
545
+ getPaneInfo: () => getPaneInfo,
546
+ isInsideTmux: () => isInsideTmux,
547
+ killPane: () => killPane,
548
+ killWindow: () => killWindow,
549
+ listPanes: () => listPanes,
550
+ listSessionPanes: () => listSessionPanes,
551
+ selectPane: () => selectPane,
552
+ selectWindow: () => selectWindow,
553
+ sendCtrlC: () => sendCtrlC,
554
+ sendKeys: () => sendKeys,
555
+ sendLiteral: () => sendLiteral,
556
+ sendRawKeys: () => sendRawKeys,
557
+ sessionExists: () => sessionExists,
558
+ splitPane: () => splitPane
559
+ });
560
+ import { execa, ExecaError } from "execa";
561
+ async function checkTmux() {
562
+ try {
563
+ await execa("tmux", ["-V"], execaEnv);
564
+ } catch {
565
+ throw new TmuxNotFoundError();
566
+ }
567
+ }
568
+ async function sessionExists(name) {
569
+ try {
570
+ await execa("tmux", ["has-session", "-t", `=${name}`], execaEnv);
571
+ return true;
572
+ } catch {
573
+ return false;
574
+ }
575
+ }
576
+ async function ensureSession(name) {
577
+ if (!await sessionExists(name)) {
578
+ await execa("tmux", ["new-session", "-d", "-s", name, "-x", "220", "-y", "50"], execaEnv);
579
+ await execa("tmux", ["set-option", "-t", name, "mouse", "on"], execaEnv);
580
+ await execa("tmux", ["set-option", "-t", name, "history-limit", "50000"], execaEnv);
581
+ }
582
+ }
583
+ async function createWindow(session, name, cwd) {
584
+ const result = await execa("tmux", [
585
+ "new-window",
586
+ "-t",
587
+ `=${session}`,
588
+ "-n",
589
+ name,
590
+ "-c",
591
+ cwd,
592
+ "-P",
593
+ "-F",
594
+ "#{window_index}"
595
+ ], execaEnv);
596
+ const windowIndex = result.stdout.trim();
597
+ return `${session}:${windowIndex}`;
598
+ }
599
+ async function splitPane(target, direction, cwd) {
600
+ const flag = direction === "horizontal" ? "-h" : "-v";
601
+ const result = await execa("tmux", [
602
+ "split-window",
603
+ flag,
604
+ "-t",
605
+ target,
606
+ "-c",
607
+ cwd,
608
+ "-P",
609
+ "-F",
610
+ "#{session_name}:#{window_index}.#{pane_index}|#{pane_id}"
611
+ ], execaEnv);
612
+ const [canonicalTarget, paneId] = result.stdout.trim().split("|");
613
+ return { paneId, target: canonicalTarget };
614
+ }
615
+ async function sendKeys(target, command) {
616
+ await execa("tmux", ["send-keys", "-t", target, "-l", command], execaEnv);
617
+ await execa("tmux", ["send-keys", "-t", target, "Enter"], execaEnv);
618
+ }
619
+ async function sendLiteral(target, text) {
620
+ await execa("tmux", ["send-keys", "-t", target, "-l", text], execaEnv);
621
+ }
622
+ async function sendRawKeys(target, keys) {
623
+ await execa("tmux", ["send-keys", "-t", target, keys], execaEnv);
624
+ }
625
+ async function capturePane(target, lines) {
626
+ const args = ["capture-pane", "-t", target, "-p"];
627
+ if (lines) {
628
+ args.push("-S", `-${lines}`);
629
+ }
630
+ const result = await execa("tmux", args, execaEnv);
631
+ return result.stdout;
632
+ }
633
+ async function killPane(target) {
634
+ try {
635
+ await execa("tmux", ["kill-pane", "-t", target], execaEnv);
636
+ } catch (err) {
637
+ if (isTmuxNotFoundError(err)) return;
638
+ throw err;
639
+ }
640
+ }
641
+ async function killWindow(target) {
642
+ try {
643
+ await execa("tmux", ["kill-window", "-t", target], execaEnv);
644
+ } catch (err) {
645
+ if (isTmuxNotFoundError(err)) return;
646
+ throw err;
647
+ }
648
+ }
649
+ function isTmuxNotFoundError(err) {
650
+ if (!(err instanceof ExecaError)) return false;
651
+ const msg = String(err.stderr ?? "").toLowerCase();
652
+ return msg.includes("can't find") || msg.includes("not found") || msg.includes("no such") || msg.includes("session not found") || msg.includes("window not found") || msg.includes("pane not found");
653
+ }
654
+ async function listPanes(target) {
655
+ try {
656
+ const result = await execa("tmux", [
657
+ "list-panes",
658
+ "-t",
659
+ target,
660
+ "-F",
661
+ "#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_dead}|#{pane_dead_status}"
662
+ ], execaEnv);
663
+ return result.stdout.trim().split("\n").filter(Boolean).map((line) => {
664
+ const [paneId, panePid, currentCommand, dead, deadStatus] = line.split("|");
665
+ return {
666
+ paneId,
667
+ panePid,
668
+ currentCommand,
669
+ isDead: dead === "1",
670
+ deadStatus: deadStatus ? parseInt(deadStatus, 10) : void 0
671
+ };
672
+ });
673
+ } catch {
674
+ return [];
675
+ }
676
+ }
677
+ async function getPaneInfo(target) {
678
+ try {
679
+ const result = await execa("tmux", [
680
+ "display-message",
681
+ "-t",
682
+ target,
683
+ "-p",
684
+ "#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_dead}|#{pane_dead_status}"
685
+ ], execaEnv);
686
+ const [paneId, panePid, currentCommand, dead, deadStatus] = result.stdout.trim().split("|");
687
+ return {
688
+ paneId,
689
+ panePid,
690
+ currentCommand,
691
+ isDead: dead === "1",
692
+ deadStatus: deadStatus ? parseInt(deadStatus, 10) : void 0
693
+ };
694
+ } catch {
695
+ return null;
696
+ }
697
+ }
698
+ async function listSessionPanes(session) {
699
+ const map = /* @__PURE__ */ new Map();
700
+ try {
701
+ const result = await execa("tmux", [
702
+ "list-panes",
703
+ "-s",
704
+ "-t",
705
+ `=${session}`,
706
+ "-F",
707
+ "#{session_name}:#{window_index}.#{pane_index}|#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_dead}|#{pane_dead_status}"
708
+ ], execaEnv);
709
+ for (const line of result.stdout.trim().split("\n").filter(Boolean)) {
710
+ const [target, paneId, panePid, currentCommand, dead, deadStatus] = line.split("|");
711
+ const info2 = {
712
+ paneId,
713
+ panePid,
714
+ currentCommand,
715
+ isDead: dead === "1",
716
+ deadStatus: deadStatus ? parseInt(deadStatus, 10) : void 0
717
+ };
718
+ map.set(target, info2);
719
+ map.set(paneId, info2);
720
+ const dotIdx = target.lastIndexOf(".");
721
+ if (dotIdx !== -1) {
722
+ map.set(target.slice(0, dotIdx), info2);
723
+ }
724
+ }
725
+ } catch {
726
+ }
727
+ return map;
728
+ }
729
+ async function selectPane(target) {
730
+ await execa("tmux", ["select-pane", "-t", target], execaEnv);
731
+ }
732
+ async function selectWindow(target) {
733
+ await execa("tmux", ["select-window", "-t", target], execaEnv);
734
+ }
735
+ async function attachSession(session) {
736
+ await execa("tmux", ["attach-session", "-t", `=${session}`], { ...execaEnv, stdio: "inherit" });
737
+ }
738
+ async function isInsideTmux() {
739
+ return !!process.env.TMUX;
740
+ }
741
+ async function sendCtrlC(target) {
742
+ await execa("tmux", ["send-keys", "-t", target, "C-c"], execaEnv);
743
+ }
744
+ var init_tmux = __esm({
745
+ "src/core/tmux.ts"() {
746
+ "use strict";
747
+ init_errors();
748
+ init_env();
749
+ }
750
+ });
751
+
401
752
  // src/commands/init.ts
402
753
  var init_exports = {};
403
754
  __export(init_exports, {
@@ -406,27 +757,25 @@ __export(init_exports, {
406
757
  import fs3 from "fs/promises";
407
758
  import path2 from "path";
408
759
  import { fileURLToPath } from "url";
409
- import { execa } from "execa";
760
+ import { execa as execa2 } from "execa";
410
761
  async function initCommand(options) {
411
762
  const cwd = process.cwd();
412
763
  let projectRoot;
413
764
  try {
414
- const result = await execa("git", ["rev-parse", "--show-toplevel"], { cwd });
765
+ const result = await execa2("git", ["rev-parse", "--show-toplevel"], { ...execaEnv, cwd });
415
766
  projectRoot = result.stdout.trim();
416
767
  } catch {
417
768
  throw new NotGitRepoError(cwd);
418
769
  }
419
- try {
420
- await execa("tmux", ["-V"]);
421
- } catch {
422
- throw new TmuxNotFoundError();
423
- }
770
+ await checkTmux();
424
771
  const dirs = [
425
772
  pgDir(projectRoot),
426
773
  resultsDir(projectRoot),
427
774
  logsDir(projectRoot),
428
775
  templatesDir(projectRoot),
429
- promptsDir(projectRoot)
776
+ promptsDir(projectRoot),
777
+ agentPromptsDir(projectRoot),
778
+ swarmsDir(projectRoot)
430
779
  ];
431
780
  for (const dir of dirs) {
432
781
  await fs3.mkdir(dir, { recursive: true });
@@ -448,6 +797,24 @@ async function initCommand(options) {
448
797
  await fs3.writeFile(templatePath, DEFAULT_TEMPLATE, "utf-8");
449
798
  info("Wrote sample template: default.md");
450
799
  }
800
+ for (const [name, content] of Object.entries(bundledPrompts)) {
801
+ const pPath = promptFile(projectRoot, name);
802
+ try {
803
+ await fs3.access(pPath);
804
+ } catch {
805
+ await fs3.writeFile(pPath, content, "utf-8");
806
+ info(`Wrote prompt: ${name}.md`);
807
+ }
808
+ }
809
+ for (const [name, content] of Object.entries(bundledSwarms)) {
810
+ const sPath = path2.join(swarmsDir(projectRoot), `${name}.yaml`);
811
+ try {
812
+ await fs3.access(sPath);
813
+ } catch {
814
+ await fs3.writeFile(sPath, content, "utf-8");
815
+ info(`Wrote swarm template: ${name}.yaml`);
816
+ }
817
+ }
451
818
  const conductorPath = path2.join(pgDir(projectRoot), "conductor-context.md");
452
819
  await fs3.writeFile(conductorPath, CONDUCTOR_CONTEXT, "utf-8");
453
820
  info("Wrote conductor-context.md");
@@ -469,9 +836,9 @@ async function initCommand(options) {
469
836
  }
470
837
  async function registerClaudePlugin() {
471
838
  try {
472
- const home = process.env.HOME;
473
- if (!home) return false;
474
- const skillsDir = path2.join(home, ".claude", "skills");
839
+ const home2 = process.env.HOME;
840
+ if (!home2) return false;
841
+ const skillsDir = path2.join(home2, ".claude", "skills");
475
842
  const __dirname = path2.dirname(fileURLToPath(import.meta.url));
476
843
  let srcSkillsDir = path2.resolve(__dirname, "..", "skills");
477
844
  try {
@@ -510,6 +877,8 @@ async function updateGitignore(projectRoot) {
510
877
  ".pg/logs/",
511
878
  ".pg/manifest.json",
512
879
  ".pg/prompts/",
880
+ ".pg/agent-prompts/",
881
+ ".pg/swarms/",
513
882
  ".pg/conductor-context.md"
514
883
  ];
515
884
  let content = "";
@@ -533,6 +902,10 @@ var init_init = __esm({
533
902
  init_output();
534
903
  init_config();
535
904
  init_manifest();
905
+ init_prompts();
906
+ init_swarms();
907
+ init_env();
908
+ init_tmux();
536
909
  CONDUCTOR_CONTEXT = `# PPG Conductor Context
537
910
 
538
911
  You are operating on the master branch of a ppg-managed project.
@@ -544,15 +917,21 @@ You are operating on the master branch of a ppg-managed project.
544
917
  - \`ppg spawn --name <name> --prompt "<task>" --json --no-open\` \u2014 Spawn worktree + agent
545
918
  - \`ppg status --json\` \u2014 Check statuses
546
919
  - \`ppg aggregate --all --json\` \u2014 Collect results
547
- - \`ppg merge <wt-id> --json\` \u2014 Merge completed work
920
+ - \`ppg pr <wt-id> --json\` \u2014 Create PR from worktree branch
548
921
  - \`ppg kill --agent <id> --json\` \u2014 Kill agent
922
+ - \`ppg reset --json\` \u2014 Clean up all worktrees
549
923
 
550
924
  ## Workflow
551
925
  1. Break request into parallelizable tasks
552
926
  2. Spawn each: \`ppg spawn --name <name> --prompt "<self-contained prompt>" --json --no-open\`
553
927
  3. Poll: \`ppg status --json\` every 5s
554
928
  4. Aggregate: \`ppg aggregate --all --json\`
555
- 5. Present results, then merge confirmed worktrees
929
+ 5. Present results to the user and ask what they'd like to do next
930
+ 6. If the user wants PRs: \`ppg pr <wt-id> --json\`
931
+ 7. If the user wants direct merge: \`ppg merge <wt-id> --json\`
932
+ 8. Cleanup when done: \`ppg reset --json\` or \`ppg clean --json\`
933
+
934
+ **Stop at step 5** \u2014 do not auto-merge or auto-PR. Let the user decide.
556
935
 
557
936
  Each agent prompt must be self-contained \u2014 agents have no memory of this conversation.
558
937
  Always use \`--json --no-open\`.
@@ -575,10 +954,11 @@ When you have completed the task, write your results to:
575
954
  });
576
955
 
577
956
  // src/core/worktree.ts
578
- import { execa as execa2 } from "execa";
957
+ import { execa as execa3 } from "execa";
579
958
  async function getRepoRoot(cwd) {
580
959
  try {
581
- const result = await execa2("git", ["rev-parse", "--show-toplevel"], {
960
+ const result = await execa3("git", ["rev-parse", "--show-toplevel"], {
961
+ ...execaEnv,
582
962
  cwd: cwd ?? process.cwd()
583
963
  });
584
964
  return result.stdout.trim();
@@ -587,7 +967,8 @@ async function getRepoRoot(cwd) {
587
967
  }
588
968
  }
589
969
  async function getCurrentBranch(cwd) {
590
- const result = await execa2("git", ["branch", "--show-current"], {
970
+ const result = await execa3("git", ["branch", "--show-current"], {
971
+ ...execaEnv,
591
972
  cwd: cwd ?? process.cwd()
592
973
  });
593
974
  return result.stdout.trim();
@@ -598,7 +979,7 @@ async function createWorktree(repoRoot, id, options) {
598
979
  if (options.base) {
599
980
  args.push(options.base);
600
981
  }
601
- await execa2("git", args, { cwd: repoRoot });
982
+ await execa3("git", args, { ...execaEnv, cwd: repoRoot });
602
983
  return wtPath;
603
984
  }
604
985
  async function removeWorktree(repoRoot, wtPath, options) {
@@ -606,22 +987,23 @@ async function removeWorktree(repoRoot, wtPath, options) {
606
987
  if (options?.force) {
607
988
  args.push("--force");
608
989
  }
609
- await execa2("git", args, { cwd: repoRoot });
990
+ await execa3("git", args, { ...execaEnv, cwd: repoRoot });
610
991
  if (options?.deleteBranch && options.branchName) {
611
992
  try {
612
- await execa2("git", ["branch", "-D", options.branchName], { cwd: repoRoot });
993
+ await execa3("git", ["branch", "-D", options.branchName], { ...execaEnv, cwd: repoRoot });
613
994
  } catch {
614
995
  }
615
996
  }
616
997
  }
617
998
  async function pruneWorktrees(repoRoot) {
618
- await execa2("git", ["worktree", "prune"], { cwd: repoRoot });
999
+ await execa3("git", ["worktree", "prune"], { ...execaEnv, cwd: repoRoot });
619
1000
  }
620
1001
  var init_worktree = __esm({
621
1002
  "src/core/worktree.ts"() {
622
1003
  "use strict";
623
1004
  init_paths();
624
1005
  init_errors();
1006
+ init_env();
625
1007
  }
626
1008
  });
627
1009
 
@@ -662,7 +1044,7 @@ async function teardownWorktreeEnv(wtPath) {
662
1044
  } catch {
663
1045
  }
664
1046
  }
665
- var init_env = __esm({
1047
+ var init_env2 = __esm({
666
1048
  "src/core/env.ts"() {
667
1049
  "use strict";
668
1050
  }
@@ -672,232 +1054,28 @@ var init_env = __esm({
672
1054
  import fs5 from "fs/promises";
673
1055
  import path4 from "path";
674
1056
  async function listTemplates(projectRoot) {
675
- const dir = templatesDir(projectRoot);
676
- try {
677
- const files = await fs5.readdir(dir);
678
- return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
679
- } catch {
680
- return [];
681
- }
682
- }
683
- async function loadTemplate(projectRoot, name) {
684
- const dir = templatesDir(projectRoot);
685
- const filePath = path4.join(dir, `${name}.md`);
686
- return fs5.readFile(filePath, "utf-8");
687
- }
688
- function renderTemplate(content, context) {
689
- return content.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
690
- return context[key] ?? `{{${key}}}`;
691
- });
692
- }
693
- var init_template = __esm({
694
- "src/core/template.ts"() {
695
- "use strict";
696
- init_paths();
697
- }
698
- });
699
-
700
- // src/core/tmux.ts
701
- var tmux_exports = {};
702
- __export(tmux_exports, {
703
- attachSession: () => attachSession,
704
- capturePane: () => capturePane,
705
- checkTmux: () => checkTmux,
706
- createWindow: () => createWindow,
707
- ensureSession: () => ensureSession,
708
- getPaneInfo: () => getPaneInfo,
709
- isInsideTmux: () => isInsideTmux,
710
- killPane: () => killPane,
711
- killWindow: () => killWindow,
712
- listPanes: () => listPanes,
713
- listSessionPanes: () => listSessionPanes,
714
- selectPane: () => selectPane,
715
- selectWindow: () => selectWindow,
716
- sendCtrlC: () => sendCtrlC,
717
- sendKeys: () => sendKeys,
718
- sendLiteral: () => sendLiteral,
719
- sendRawKeys: () => sendRawKeys,
720
- sessionExists: () => sessionExists,
721
- splitPane: () => splitPane
722
- });
723
- import { execa as execa3 } from "execa";
724
- async function checkTmux() {
725
- try {
726
- await execa3("tmux", ["-V"]);
727
- } catch {
728
- throw new TmuxNotFoundError();
729
- }
730
- }
731
- async function sessionExists(name) {
732
- try {
733
- await execa3("tmux", ["has-session", "-t", name]);
734
- return true;
735
- } catch {
736
- return false;
737
- }
738
- }
739
- async function ensureSession(name) {
740
- if (!await sessionExists(name)) {
741
- await execa3("tmux", ["new-session", "-d", "-s", name, "-x", "220", "-y", "50"]);
742
- await execa3("tmux", ["set-option", "-t", name, "mouse", "on"]);
743
- await execa3("tmux", ["set-option", "-t", name, "history-limit", "50000"]);
744
- }
745
- }
746
- async function createWindow(session, name, cwd) {
747
- const result = await execa3("tmux", [
748
- "new-window",
749
- "-t",
750
- session,
751
- "-n",
752
- name,
753
- "-c",
754
- cwd,
755
- "-P",
756
- "-F",
757
- "#{window_index}"
758
- ]);
759
- const windowIndex = result.stdout.trim();
760
- return `${session}:${windowIndex}`;
761
- }
762
- async function splitPane(target, direction, cwd) {
763
- const flag = direction === "horizontal" ? "-h" : "-v";
764
- const result = await execa3("tmux", [
765
- "split-window",
766
- flag,
767
- "-t",
768
- target,
769
- "-c",
770
- cwd,
771
- "-P",
772
- "-F",
773
- "#{session_name}:#{window_index}.#{pane_index}|#{pane_id}"
774
- ]);
775
- const [canonicalTarget, paneId] = result.stdout.trim().split("|");
776
- return { paneId, target: canonicalTarget };
777
- }
778
- async function sendKeys(target, command) {
779
- await execa3("tmux", ["send-keys", "-t", target, "-l", command + "\n"]);
780
- }
781
- async function sendLiteral(target, text) {
782
- await execa3("tmux", ["send-keys", "-t", target, "-l", text]);
783
- }
784
- async function sendRawKeys(target, keys) {
785
- await execa3("tmux", ["send-keys", "-t", target, keys]);
786
- }
787
- async function capturePane(target, lines) {
788
- const args = ["capture-pane", "-t", target, "-p"];
789
- if (lines) {
790
- args.push("-S", `-${lines}`);
791
- }
792
- const result = await execa3("tmux", args);
793
- return result.stdout;
794
- }
795
- async function killPane(target) {
796
- try {
797
- await execa3("tmux", ["kill-pane", "-t", target]);
798
- } catch {
799
- }
800
- }
801
- async function killWindow(target) {
802
- try {
803
- await execa3("tmux", ["kill-window", "-t", target]);
804
- } catch {
805
- }
806
- }
807
- async function listPanes(target) {
808
- try {
809
- const result = await execa3("tmux", [
810
- "list-panes",
811
- "-t",
812
- target,
813
- "-F",
814
- "#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_dead}|#{pane_dead_status}"
815
- ]);
816
- return result.stdout.trim().split("\n").filter(Boolean).map((line) => {
817
- const [paneId, panePid, currentCommand, dead, deadStatus] = line.split("|");
818
- return {
819
- paneId,
820
- panePid,
821
- currentCommand,
822
- isDead: dead === "1",
823
- deadStatus: deadStatus ? parseInt(deadStatus, 10) : void 0
824
- };
825
- });
826
- } catch {
827
- return [];
828
- }
829
- }
830
- async function getPaneInfo(target) {
831
- try {
832
- const result = await execa3("tmux", [
833
- "display-message",
834
- "-t",
835
- target,
836
- "-p",
837
- "#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_dead}|#{pane_dead_status}"
838
- ]);
839
- const [paneId, panePid, currentCommand, dead, deadStatus] = result.stdout.trim().split("|");
840
- return {
841
- paneId,
842
- panePid,
843
- currentCommand,
844
- isDead: dead === "1",
845
- deadStatus: deadStatus ? parseInt(deadStatus, 10) : void 0
846
- };
847
- } catch {
848
- return null;
849
- }
850
- }
851
- async function listSessionPanes(session) {
852
- const map = /* @__PURE__ */ new Map();
853
- try {
854
- const result = await execa3("tmux", [
855
- "list-panes",
856
- "-s",
857
- "-t",
858
- session,
859
- "-F",
860
- "#{session_name}:#{window_index}.#{pane_index}|#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_dead}|#{pane_dead_status}"
861
- ]);
862
- for (const line of result.stdout.trim().split("\n").filter(Boolean)) {
863
- const [target, paneId, panePid, currentCommand, dead, deadStatus] = line.split("|");
864
- const info2 = {
865
- paneId,
866
- panePid,
867
- currentCommand,
868
- isDead: dead === "1",
869
- deadStatus: deadStatus ? parseInt(deadStatus, 10) : void 0
870
- };
871
- map.set(target, info2);
872
- map.set(paneId, info2);
873
- const dotIdx = target.lastIndexOf(".");
874
- if (dotIdx !== -1) {
875
- map.set(target.slice(0, dotIdx), info2);
876
- }
877
- }
1057
+ const dir = templatesDir(projectRoot);
1058
+ try {
1059
+ const files = await fs5.readdir(dir);
1060
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
878
1061
  } catch {
1062
+ return [];
879
1063
  }
880
- return map;
881
- }
882
- async function selectPane(target) {
883
- await execa3("tmux", ["select-pane", "-t", target]);
884
1064
  }
885
- async function selectWindow(target) {
886
- await execa3("tmux", ["select-window", "-t", target]);
887
- }
888
- async function attachSession(session) {
889
- await execa3("tmux", ["attach-session", "-t", session], { stdio: "inherit" });
890
- }
891
- async function isInsideTmux() {
892
- return !!process.env.TMUX;
1065
+ async function loadTemplate(projectRoot, name) {
1066
+ const dir = templatesDir(projectRoot);
1067
+ const filePath = path4.join(dir, `${name}.md`);
1068
+ return fs5.readFile(filePath, "utf-8");
893
1069
  }
894
- async function sendCtrlC(target) {
895
- await execa3("tmux", ["send-keys", "-t", target, "C-c"]);
1070
+ function renderTemplate(content, context) {
1071
+ return content.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
1072
+ return context[key] ?? `{{${key}}}`;
1073
+ });
896
1074
  }
897
- var init_tmux = __esm({
898
- "src/core/tmux.ts"() {
1075
+ var init_template = __esm({
1076
+ "src/core/template.ts"() {
899
1077
  "use strict";
900
- init_errors();
1078
+ init_paths();
901
1079
  }
902
1080
  });
903
1081
 
@@ -930,7 +1108,8 @@ async function spawnAgent(options) {
930
1108
 
931
1109
  ${instructions}`;
932
1110
  }
933
- const pFile = promptFile(projectRoot, agentId2);
1111
+ const pFile = agentPromptFile(projectRoot, agentId2);
1112
+ await fs6.mkdir(agentPromptsDir(projectRoot), { recursive: true });
934
1113
  await fs6.writeFile(pFile, fullPrompt, "utf-8");
935
1114
  const command = buildAgentCommand(agentConfig, pFile, options.sessionId);
936
1115
  await sendKeys(tmuxTarget, command);
@@ -1046,8 +1225,14 @@ async function resumeAgent(options) {
1046
1225
  return newTarget;
1047
1226
  }
1048
1227
  async function killAgent(agent) {
1049
- await sendCtrlC(agent.tmuxTarget);
1050
- await new Promise((resolve) => setTimeout(resolve, 500));
1228
+ const initialInfo = await getPaneInfo(agent.tmuxTarget);
1229
+ if (!initialInfo || initialInfo.isDead) return;
1230
+ try {
1231
+ await sendCtrlC(agent.tmuxTarget);
1232
+ } catch {
1233
+ return;
1234
+ }
1235
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
1051
1236
  const paneInfo = await getPaneInfo(agent.tmuxTarget);
1052
1237
  if (paneInfo && !paneInfo.isDead) {
1053
1238
  await killPane(agent.tmuxTarget);
@@ -1055,10 +1240,16 @@ async function killAgent(agent) {
1055
1240
  }
1056
1241
  async function killAgents(agents) {
1057
1242
  if (agents.length === 0) return;
1058
- await Promise.all(agents.map((a) => sendCtrlC(a.tmuxTarget).catch(() => {
1059
- })));
1060
- await new Promise((resolve) => setTimeout(resolve, 500));
1243
+ const alive = [];
1061
1244
  await Promise.all(agents.map(async (a) => {
1245
+ const info2 = await getPaneInfo(a.tmuxTarget);
1246
+ if (info2 && !info2.isDead) alive.push(a);
1247
+ }));
1248
+ if (alive.length === 0) return;
1249
+ await Promise.all(alive.map((a) => sendCtrlC(a.tmuxTarget).catch(() => {
1250
+ })));
1251
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
1252
+ await Promise.all(alive.map(async (a) => {
1062
1253
  const paneInfo = await getPaneInfo(a.tmuxTarget);
1063
1254
  if (paneInfo && !paneInfo.isDead) {
1064
1255
  await killPane(a.tmuxTarget);
@@ -1090,7 +1281,7 @@ var init_agent = __esm({
1090
1281
  // src/core/terminal.ts
1091
1282
  import { execa as execa4 } from "execa";
1092
1283
  async function openTerminalWindow(sessionName, windowTarget, title) {
1093
- const tmuxCmd = `tmux attach-session -t ${sessionName} \\\\; select-window -t ${windowTarget}`;
1284
+ const tmuxCmd = `if [ -x /usr/libexec/path_helper ]; then eval $(/usr/libexec/path_helper -s); fi; [ -f ~/.zprofile ] && source ~/.zprofile; [ -f ~/.zshrc ] && source ~/.zshrc; tmux attach-session -t ${sessionName} \\\\; select-window -t ${windowTarget}`;
1094
1285
  const script = `
1095
1286
  tell application "Terminal"
1096
1287
  activate
@@ -1133,6 +1324,25 @@ var init_id = __esm({
1133
1324
  }
1134
1325
  });
1135
1326
 
1327
+ // src/lib/name.ts
1328
+ function normalizeName(raw, fallback) {
1329
+ const name = raw.toLowerCase().replace(/[\x00-\x1f\x7f]+/g, "").replace(/[\s_]+/g, "-").replace(/[~^:?*[\]\\@{}<>]+/g, "-").replace(/\.{2,}/g, ".").replace(/-{2,}/g, "-").split("/").map(normalizeSegment).filter(Boolean).join("/");
1330
+ return name || fallback;
1331
+ }
1332
+ function normalizeSegment(segment) {
1333
+ let s = segment;
1334
+ while (s.endsWith(".lock")) {
1335
+ s = s.slice(0, -5);
1336
+ }
1337
+ s = s.replace(/^[-.]+|[-.]+$/g, "");
1338
+ return s;
1339
+ }
1340
+ var init_name = __esm({
1341
+ "src/lib/name.ts"() {
1342
+ "use strict";
1343
+ }
1344
+ });
1345
+
1136
1346
  // src/commands/spawn.ts
1137
1347
  var spawn_exports = {};
1138
1348
  __export(spawn_exports, {
@@ -1190,7 +1400,7 @@ async function resolvePrompt(options, projectRoot) {
1190
1400
  async function spawnNewWorktree(projectRoot, config, agentConfig, promptText, count, options) {
1191
1401
  const baseBranch = options.base ?? await getCurrentBranch(projectRoot);
1192
1402
  const wtId = worktreeId();
1193
- const name = options.name ?? wtId;
1403
+ const name = options.name ? normalizeName(options.name, wtId) : wtId;
1194
1404
  const branchName = `ppg/${name}`;
1195
1405
  info(`Creating worktree ${wtId} on branch ${branchName}`);
1196
1406
  const wtPath = await createWorktree(projectRoot, wtId, {
@@ -1255,7 +1465,7 @@ async function spawnNewWorktree(projectRoot, config, agentConfig, promptText, co
1255
1465
  m.worktrees[wtId] = worktreeEntry;
1256
1466
  return m;
1257
1467
  });
1258
- if (options.open !== false) {
1468
+ if (options.open === true) {
1259
1469
  openTerminalWindow(sessionName, windowTarget, name).catch(() => {
1260
1470
  });
1261
1471
  }
@@ -1341,7 +1551,7 @@ async function spawnIntoExistingWorktree(projectRoot, config, agentConfig, workt
1341
1551
  }
1342
1552
  return m;
1343
1553
  });
1344
- if (options.open !== false) {
1554
+ if (options.open === true) {
1345
1555
  openTerminalWindow(manifest.sessionName, windowTarget, wt.name).catch(() => {
1346
1556
  });
1347
1557
  }
@@ -1370,7 +1580,7 @@ var init_spawn = __esm({
1370
1580
  init_config();
1371
1581
  init_manifest();
1372
1582
  init_worktree();
1373
- init_env();
1583
+ init_env2();
1374
1584
  init_template();
1375
1585
  init_agent();
1376
1586
  init_tmux();
@@ -1379,6 +1589,7 @@ var init_spawn = __esm({
1379
1589
  init_paths();
1380
1590
  init_errors();
1381
1591
  init_output();
1592
+ init_name();
1382
1593
  }
1383
1594
  });
1384
1595
 
@@ -1497,20 +1708,103 @@ var init_status = __esm({
1497
1708
  }
1498
1709
  });
1499
1710
 
1500
- // src/core/cleanup.ts
1501
- async function cleanupWorktree(projectRoot, wt) {
1502
- const windowKills = [];
1711
+ // src/core/self.ts
1712
+ function getCurrentPaneId() {
1713
+ return process.env.TMUX_PANE ?? null;
1714
+ }
1715
+ function wouldAffectSelf(target, selfPaneId, paneMap) {
1716
+ if (target === selfPaneId) return true;
1717
+ const targetInfo = paneMap.get(target);
1718
+ if (!targetInfo) return false;
1719
+ if (targetInfo.paneId === selfPaneId) return true;
1720
+ if (!target.includes(".") && target.includes(":")) {
1721
+ for (const [key, info2] of paneMap) {
1722
+ if (key.startsWith(target + ".") && info2.paneId === selfPaneId) {
1723
+ return true;
1724
+ }
1725
+ }
1726
+ }
1727
+ return false;
1728
+ }
1729
+ function excludeSelf(agents, selfPaneId, paneMap) {
1730
+ const safe = [];
1731
+ const skipped = [];
1732
+ for (const agent of agents) {
1733
+ if (wouldAffectSelf(agent.tmuxTarget, selfPaneId, paneMap)) {
1734
+ skipped.push(agent);
1735
+ } else {
1736
+ safe.push(agent);
1737
+ }
1738
+ }
1739
+ return { safe, skipped };
1740
+ }
1741
+ function wouldCleanupAffectSelf(wt, selfPaneId, paneMap) {
1742
+ if (wt.tmuxWindow && wouldAffectSelf(wt.tmuxWindow, selfPaneId, paneMap)) {
1743
+ return true;
1744
+ }
1503
1745
  for (const agent of Object.values(wt.agents)) {
1504
- if (agent.tmuxTarget) {
1505
- windowKills.push(killWindow(agent.tmuxTarget).catch(() => {
1506
- }));
1746
+ if (agent.tmuxTarget && wouldAffectSelf(agent.tmuxTarget, selfPaneId, paneMap)) {
1747
+ return true;
1507
1748
  }
1508
1749
  }
1509
- if (wt.tmuxWindow) {
1510
- windowKills.push(killWindow(wt.tmuxWindow).catch(() => {
1511
- }));
1750
+ return false;
1751
+ }
1752
+ var init_self = __esm({
1753
+ "src/core/self.ts"() {
1754
+ "use strict";
1755
+ }
1756
+ });
1757
+
1758
+ // src/core/cleanup.ts
1759
+ import fs8 from "fs/promises";
1760
+ async function cleanupWorktree(projectRoot, wt, options) {
1761
+ const selfPaneId = options?.selfPaneId ?? null;
1762
+ const paneMap = options?.paneMap ?? /* @__PURE__ */ new Map();
1763
+ const result = {
1764
+ worktreeId: wt.id,
1765
+ manifestUpdated: false,
1766
+ tmuxKilled: 0,
1767
+ tmuxSkipped: 0,
1768
+ tmuxFailed: 0,
1769
+ selfProtected: false,
1770
+ selfProtectedTargets: []
1771
+ };
1772
+ const alreadyCleaned = wt.status === "cleaned";
1773
+ if (!alreadyCleaned) {
1774
+ await updateManifest(projectRoot, (m) => {
1775
+ if (m.worktrees[wt.id]) {
1776
+ m.worktrees[wt.id].status = "cleaned";
1777
+ }
1778
+ return m;
1779
+ });
1780
+ result.manifestUpdated = true;
1781
+ }
1782
+ if (!alreadyCleaned) {
1783
+ const targets = /* @__PURE__ */ new Set();
1784
+ for (const agent of Object.values(wt.agents)) {
1785
+ if (agent.tmuxTarget) targets.add(agent.tmuxTarget);
1786
+ }
1787
+ if (wt.tmuxWindow) targets.add(wt.tmuxWindow);
1788
+ for (const target of targets) {
1789
+ if (selfPaneId && wouldAffectSelf(target, selfPaneId, paneMap)) {
1790
+ result.selfProtected = true;
1791
+ result.selfProtectedTargets.push(target);
1792
+ warn(`Skipping tmux kill for ${target} \u2014 contains current process`);
1793
+ continue;
1794
+ }
1795
+ try {
1796
+ await killWindow(target);
1797
+ result.tmuxKilled++;
1798
+ } catch (err) {
1799
+ result.tmuxFailed++;
1800
+ warn(`Failed to kill tmux window ${target}: ${err instanceof Error ? err.message : err}`);
1801
+ }
1802
+ }
1803
+ }
1804
+ for (const agent of Object.values(wt.agents)) {
1805
+ const pFile = agentPromptFile(projectRoot, agent.id);
1806
+ await fs8.rm(pFile, { force: true });
1512
1807
  }
1513
- await Promise.all(windowKills);
1514
1808
  try {
1515
1809
  await teardownWorktreeEnv(wt.path);
1516
1810
  } catch {
@@ -1524,21 +1818,18 @@ async function cleanupWorktree(projectRoot, wt) {
1524
1818
  } catch (err) {
1525
1819
  warn(`Could not fully remove worktree ${wt.id}: ${err instanceof Error ? err.message : err}`);
1526
1820
  }
1527
- await updateManifest(projectRoot, (m) => {
1528
- if (m.worktrees[wt.id]) {
1529
- m.worktrees[wt.id].status = "cleaned";
1530
- }
1531
- return m;
1532
- });
1821
+ return result;
1533
1822
  }
1534
1823
  var init_cleanup = __esm({
1535
1824
  "src/core/cleanup.ts"() {
1536
1825
  "use strict";
1537
1826
  init_manifest();
1538
1827
  init_worktree();
1539
- init_env();
1828
+ init_env2();
1540
1829
  init_tmux();
1830
+ init_paths();
1541
1831
  init_output();
1832
+ init_self();
1542
1833
  }
1543
1834
  });
1544
1835
 
@@ -1552,20 +1843,36 @@ async function killCommand(options) {
1552
1843
  if (!options.agent && !options.worktree && !options.all) {
1553
1844
  throw new PgError("One of --agent, --worktree, or --all is required", "INVALID_ARGS");
1554
1845
  }
1846
+ const selfPaneId = getCurrentPaneId();
1847
+ let paneMap;
1848
+ if (selfPaneId) {
1849
+ const manifest = await readManifest(projectRoot);
1850
+ paneMap = await listSessionPanes(manifest.sessionName);
1851
+ }
1555
1852
  if (options.agent) {
1556
- await killSingleAgent(projectRoot, options.agent, options);
1853
+ await killSingleAgent(projectRoot, options.agent, options, selfPaneId, paneMap);
1557
1854
  } else if (options.worktree) {
1558
- await killWorktreeAgents(projectRoot, options.worktree, options);
1855
+ await killWorktreeAgents(projectRoot, options.worktree, options, selfPaneId, paneMap);
1559
1856
  } else if (options.all) {
1560
- await killAllAgents(projectRoot, options);
1857
+ await killAllAgents(projectRoot, options, selfPaneId, paneMap);
1561
1858
  }
1562
1859
  }
1563
- async function killSingleAgent(projectRoot, agentId2, options) {
1860
+ async function killSingleAgent(projectRoot, agentId2, options, selfPaneId, paneMap) {
1564
1861
  const manifest = await readManifest(projectRoot);
1565
1862
  const found = findAgent(manifest, agentId2);
1566
1863
  if (!found) throw new AgentNotFoundError(agentId2);
1567
1864
  const { agent } = found;
1568
1865
  const isTerminal = ["completed", "failed", "killed", "lost"].includes(agent.status);
1866
+ if (selfPaneId && paneMap) {
1867
+ const { skipped } = excludeSelf([agent], selfPaneId, paneMap);
1868
+ if (skipped.length > 0) {
1869
+ warn(`Cannot kill agent ${agentId2} \u2014 it contains the current ppg process`);
1870
+ if (options.json) {
1871
+ output({ success: false, skipped: [agentId2], reason: "self-protection" }, true);
1872
+ }
1873
+ return;
1874
+ }
1875
+ }
1569
1876
  if (options.delete) {
1570
1877
  if (!isTerminal) {
1571
1878
  info(`Killing agent ${agentId2}`);
@@ -1602,11 +1909,20 @@ async function killSingleAgent(projectRoot, agentId2, options) {
1602
1909
  }
1603
1910
  }
1604
1911
  }
1605
- async function killWorktreeAgents(projectRoot, worktreeRef, options) {
1912
+ async function killWorktreeAgents(projectRoot, worktreeRef, options, selfPaneId, paneMap) {
1606
1913
  const manifest = await readManifest(projectRoot);
1607
1914
  const wt = resolveWorktree(manifest, worktreeRef);
1608
1915
  if (!wt) throw new WorktreeNotFoundError(worktreeRef);
1609
- const toKill = Object.values(wt.agents).filter((a) => ["running", "spawning", "waiting"].includes(a.status));
1916
+ let toKill = Object.values(wt.agents).filter((a) => ["running", "spawning", "waiting"].includes(a.status));
1917
+ const skippedIds = [];
1918
+ if (selfPaneId && paneMap) {
1919
+ const { safe, skipped } = excludeSelf(toKill, selfPaneId, paneMap);
1920
+ toKill = safe;
1921
+ for (const a of skipped) {
1922
+ skippedIds.push(a.id);
1923
+ warn(`Skipping agent ${a.id} \u2014 contains current ppg process`);
1924
+ }
1925
+ }
1610
1926
  const killedIds = toKill.map((a) => a.id);
1611
1927
  for (const a of toKill) info(`Killing agent ${a.id}`);
1612
1928
  await killAgents(toKill);
@@ -1624,7 +1940,7 @@ async function killWorktreeAgents(projectRoot, worktreeRef, options) {
1624
1940
  });
1625
1941
  const shouldRemove = options.remove || options.delete;
1626
1942
  if (shouldRemove) {
1627
- await removeWorktreeCleanup(projectRoot, wt.id);
1943
+ await removeWorktreeCleanup(projectRoot, wt.id, selfPaneId, paneMap);
1628
1944
  }
1629
1945
  if (options.delete) {
1630
1946
  await updateManifest(projectRoot, (m) => {
@@ -1636,11 +1952,15 @@ async function killWorktreeAgents(projectRoot, worktreeRef, options) {
1636
1952
  output({
1637
1953
  success: true,
1638
1954
  killed: killedIds,
1955
+ skipped: skippedIds.length > 0 ? skippedIds : void 0,
1639
1956
  removed: shouldRemove ? [wt.id] : [],
1640
1957
  deleted: options.delete ? [wt.id] : []
1641
1958
  }, true);
1642
1959
  } else {
1643
1960
  success(`Killed ${killedIds.length} agent(s) in worktree ${wt.id}`);
1961
+ if (skippedIds.length > 0) {
1962
+ warn(`Skipped ${skippedIds.length} agent(s) due to self-protection`);
1963
+ }
1644
1964
  if (options.delete) {
1645
1965
  success(`Deleted worktree ${wt.id}`);
1646
1966
  } else if (options.remove) {
@@ -1648,9 +1968,9 @@ async function killWorktreeAgents(projectRoot, worktreeRef, options) {
1648
1968
  }
1649
1969
  }
1650
1970
  }
1651
- async function killAllAgents(projectRoot, options) {
1971
+ async function killAllAgents(projectRoot, options, selfPaneId, paneMap) {
1652
1972
  const manifest = await readManifest(projectRoot);
1653
- const toKill = [];
1973
+ let toKill = [];
1654
1974
  for (const wt of Object.values(manifest.worktrees)) {
1655
1975
  for (const agent of Object.values(wt.agents)) {
1656
1976
  if (["running", "spawning", "waiting"].includes(agent.status)) {
@@ -1658,6 +1978,15 @@ async function killAllAgents(projectRoot, options) {
1658
1978
  }
1659
1979
  }
1660
1980
  }
1981
+ const skippedIds = [];
1982
+ if (selfPaneId && paneMap) {
1983
+ const { safe, skipped } = excludeSelf(toKill, selfPaneId, paneMap);
1984
+ toKill = safe;
1985
+ for (const a of skipped) {
1986
+ skippedIds.push(a.id);
1987
+ warn(`Skipping agent ${a.id} \u2014 contains current ppg process`);
1988
+ }
1989
+ }
1661
1990
  const killedIds = toKill.map((a) => a.id);
1662
1991
  for (const a of toKill) info(`Killing agent ${a.id}`);
1663
1992
  await killAgents(toKill);
@@ -1676,7 +2005,7 @@ async function killAllAgents(projectRoot, options) {
1676
2005
  const shouldRemove = options.remove || options.delete;
1677
2006
  if (shouldRemove) {
1678
2007
  for (const wtId of activeWorktreeIds) {
1679
- await removeWorktreeCleanup(projectRoot, wtId);
2008
+ await removeWorktreeCleanup(projectRoot, wtId, selfPaneId, paneMap);
1680
2009
  }
1681
2010
  }
1682
2011
  if (options.delete) {
@@ -1691,11 +2020,15 @@ async function killAllAgents(projectRoot, options) {
1691
2020
  output({
1692
2021
  success: true,
1693
2022
  killed: killedIds,
2023
+ skipped: skippedIds.length > 0 ? skippedIds : void 0,
1694
2024
  removed: shouldRemove ? activeWorktreeIds : [],
1695
2025
  deleted: options.delete ? activeWorktreeIds : []
1696
2026
  }, true);
1697
2027
  } else {
1698
2028
  success(`Killed ${killedIds.length} agent(s) across ${activeWorktreeIds.length} worktree(s)`);
2029
+ if (skippedIds.length > 0) {
2030
+ warn(`Skipped ${skippedIds.length} agent(s) due to self-protection`);
2031
+ }
1699
2032
  if (options.delete) {
1700
2033
  success(`Deleted ${activeWorktreeIds.length} worktree(s)`);
1701
2034
  } else if (options.remove) {
@@ -1703,11 +2036,14 @@ async function killAllAgents(projectRoot, options) {
1703
2036
  }
1704
2037
  }
1705
2038
  }
1706
- async function removeWorktreeCleanup(projectRoot, wtId) {
2039
+ async function removeWorktreeCleanup(projectRoot, wtId, selfPaneId, paneMap) {
1707
2040
  const manifest = await readManifest(projectRoot);
1708
2041
  const wt = resolveWorktree(manifest, wtId);
1709
2042
  if (!wt) return;
1710
- await cleanupWorktree(projectRoot, wt);
2043
+ await cleanupWorktree(projectRoot, wt, {
2044
+ selfPaneId,
2045
+ paneMap
2046
+ });
1711
2047
  }
1712
2048
  var init_kill = __esm({
1713
2049
  "src/commands/kill.ts"() {
@@ -1716,6 +2052,8 @@ var init_kill = __esm({
1716
2052
  init_agent();
1717
2053
  init_worktree();
1718
2054
  init_cleanup();
2055
+ init_self();
2056
+ init_tmux();
1719
2057
  init_errors();
1720
2058
  init_output();
1721
2059
  }
@@ -1876,7 +2214,7 @@ var aggregate_exports = {};
1876
2214
  __export(aggregate_exports, {
1877
2215
  aggregateCommand: () => aggregateCommand
1878
2216
  });
1879
- import fs8 from "fs/promises";
2217
+ import fs9 from "fs/promises";
1880
2218
  async function aggregateCommand(worktreeId2, options) {
1881
2219
  const projectRoot = await getRepoRoot();
1882
2220
  let manifest;
@@ -1940,7 +2278,7 @@ async function aggregateCommand(worktreeId2, options) {
1940
2278
  ].join("\n");
1941
2279
  }).join("\n");
1942
2280
  if (options?.output) {
1943
- await fs8.writeFile(options.output, combined, "utf-8");
2281
+ await fs9.writeFile(options.output, combined, "utf-8");
1944
2282
  success(`Wrote ${results.length} result(s) to ${options.output}`);
1945
2283
  } else {
1946
2284
  console.log(combined);
@@ -1948,7 +2286,7 @@ async function aggregateCommand(worktreeId2, options) {
1948
2286
  }
1949
2287
  async function collectAgentResult(agent, projectRoot) {
1950
2288
  try {
1951
- const content = await fs8.readFile(agent.resultFile, "utf-8");
2289
+ const content = await fs9.readFile(agent.resultFile, "utf-8");
1952
2290
  return content;
1953
2291
  } catch {
1954
2292
  }
@@ -2020,59 +2358,434 @@ async function mergeCommand(worktreeId2, options) {
2020
2358
  try {
2021
2359
  info(`Merging ${wt.branch} into ${wt.baseBranch} (${strategy})`);
2022
2360
  if (strategy === "squash") {
2023
- await execa5("git", ["merge", "--squash", wt.branch], { cwd: projectRoot });
2361
+ await execa5("git", ["merge", "--squash", wt.branch], { ...execaEnv, cwd: projectRoot });
2024
2362
  await execa5("git", ["commit", "-m", `ppg: merge ${wt.name} (${wt.branch})`], {
2363
+ ...execaEnv,
2025
2364
  cwd: projectRoot
2026
2365
  });
2027
2366
  } else {
2028
2367
  await execa5("git", ["merge", "--no-ff", wt.branch, "-m", `ppg: merge ${wt.name} (${wt.branch})`], {
2368
+ ...execaEnv,
2029
2369
  cwd: projectRoot
2030
2370
  });
2031
2371
  }
2032
- success(`Merged ${wt.branch} into ${wt.baseBranch}`);
2033
- } catch (err) {
2372
+ success(`Merged ${wt.branch} into ${wt.baseBranch}`);
2373
+ } catch (err) {
2374
+ await updateManifest(projectRoot, (m) => {
2375
+ if (m.worktrees[wt.id]) {
2376
+ m.worktrees[wt.id].status = "failed";
2377
+ }
2378
+ return m;
2379
+ });
2380
+ throw new MergeFailedError(
2381
+ `Merge failed: ${err instanceof Error ? err.message : err}`
2382
+ );
2383
+ }
2384
+ await updateManifest(projectRoot, (m) => {
2385
+ if (m.worktrees[wt.id]) {
2386
+ m.worktrees[wt.id].status = "merged";
2387
+ m.worktrees[wt.id].mergedAt = (/* @__PURE__ */ new Date()).toISOString();
2388
+ }
2389
+ return m;
2390
+ });
2391
+ let selfProtected = false;
2392
+ if (options.cleanup !== false) {
2393
+ info("Cleaning up...");
2394
+ const selfPaneId = getCurrentPaneId();
2395
+ let paneMap;
2396
+ if (selfPaneId) {
2397
+ paneMap = await listSessionPanes(manifest.sessionName);
2398
+ }
2399
+ const cleanupResult = await cleanupWorktree(projectRoot, wt, { selfPaneId, paneMap });
2400
+ selfProtected = cleanupResult.selfProtected;
2401
+ if (selfProtected) {
2402
+ warn(`Some tmux targets skipped during cleanup \u2014 contains current ppg process`);
2403
+ }
2404
+ success(`Cleaned up worktree ${wt.id}`);
2405
+ }
2406
+ if (options.json) {
2407
+ output({
2408
+ success: true,
2409
+ worktreeId: wt.id,
2410
+ branch: wt.branch,
2411
+ baseBranch: wt.baseBranch,
2412
+ strategy,
2413
+ cleaned: options.cleanup !== false,
2414
+ selfProtected: selfProtected || void 0
2415
+ }, true);
2416
+ }
2417
+ }
2418
+ var init_merge = __esm({
2419
+ "src/commands/merge.ts"() {
2420
+ "use strict";
2421
+ init_manifest();
2422
+ init_agent();
2423
+ init_worktree();
2424
+ init_cleanup();
2425
+ init_self();
2426
+ init_tmux();
2427
+ init_errors();
2428
+ init_output();
2429
+ init_env();
2430
+ }
2431
+ });
2432
+
2433
+ // src/core/swarm.ts
2434
+ import fs10 from "fs/promises";
2435
+ import path5 from "path";
2436
+ import YAML2 from "yaml";
2437
+ async function listSwarms(projectRoot) {
2438
+ const dir = swarmsDir(projectRoot);
2439
+ try {
2440
+ const files = await fs10.readdir(dir);
2441
+ return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => f.replace(/\.ya?ml$/, "")).sort();
2442
+ } catch {
2443
+ return [];
2444
+ }
2445
+ }
2446
+ async function loadSwarm(projectRoot, name) {
2447
+ if (!SAFE_NAME.test(name)) {
2448
+ throw new PgError(
2449
+ `Invalid swarm template name: "${name}" \u2014 must be alphanumeric, hyphens, or underscores`,
2450
+ "INVALID_ARGS"
2451
+ );
2452
+ }
2453
+ const dir = swarmsDir(projectRoot);
2454
+ let filePath = path5.join(dir, `${name}.yaml`);
2455
+ try {
2456
+ await fs10.access(filePath);
2457
+ } catch {
2458
+ filePath = path5.join(dir, `${name}.yml`);
2459
+ try {
2460
+ await fs10.access(filePath);
2461
+ } catch {
2462
+ throw new PgError(`Swarm template not found: ${name}`, "INVALID_ARGS");
2463
+ }
2464
+ }
2465
+ const raw = await fs10.readFile(filePath, "utf-8");
2466
+ const parsed = YAML2.parse(raw);
2467
+ if (!parsed || typeof parsed !== "object") {
2468
+ throw new PgError(`Invalid swarm template: ${name} (empty or malformed YAML)`, "INVALID_ARGS");
2469
+ }
2470
+ if (!parsed.name || !parsed.agents || !Array.isArray(parsed.agents)) {
2471
+ throw new PgError(`Invalid swarm template: ${name} (missing name or agents)`, "INVALID_ARGS");
2472
+ }
2473
+ if (!SAFE_NAME.test(parsed.name)) {
2474
+ throw new PgError(
2475
+ `Invalid swarm name: "${parsed.name}" \u2014 must be alphanumeric, hyphens, or underscores`,
2476
+ "INVALID_ARGS"
2477
+ );
2478
+ }
2479
+ if (parsed.strategy && parsed.strategy !== "shared" && parsed.strategy !== "isolated") {
2480
+ throw new PgError(
2481
+ `Invalid swarm strategy: ${parsed.strategy}. Must be 'shared' or 'isolated'`,
2482
+ "INVALID_ARGS"
2483
+ );
2484
+ }
2485
+ for (let i = 0; i < parsed.agents.length; i++) {
2486
+ const agent = parsed.agents[i];
2487
+ if (!agent.prompt || typeof agent.prompt !== "string") {
2488
+ throw new PgError(
2489
+ `Invalid swarm template: ${name} \u2014 agent[${i}] missing prompt field`,
2490
+ "INVALID_ARGS"
2491
+ );
2492
+ }
2493
+ if (!SAFE_NAME.test(agent.prompt)) {
2494
+ throw new PgError(
2495
+ `Invalid prompt name: "${agent.prompt}" \u2014 must be alphanumeric, hyphens, or underscores`,
2496
+ "INVALID_ARGS"
2497
+ );
2498
+ }
2499
+ }
2500
+ return {
2501
+ ...parsed,
2502
+ strategy: parsed.strategy ?? "shared",
2503
+ description: parsed.description ?? ""
2504
+ };
2505
+ }
2506
+ var SAFE_NAME;
2507
+ var init_swarm = __esm({
2508
+ "src/core/swarm.ts"() {
2509
+ "use strict";
2510
+ init_paths();
2511
+ init_errors();
2512
+ SAFE_NAME = /^[\w-]+$/;
2513
+ }
2514
+ });
2515
+
2516
+ // src/commands/swarm.ts
2517
+ var swarm_exports = {};
2518
+ __export(swarm_exports, {
2519
+ swarmCommand: () => swarmCommand
2520
+ });
2521
+ import fs11 from "fs/promises";
2522
+ import path6 from "path";
2523
+ async function swarmCommand(templateName, options) {
2524
+ const projectRoot = await getRepoRoot();
2525
+ const config = await loadConfig(projectRoot);
2526
+ try {
2527
+ await fs11.access(manifestPath(projectRoot));
2528
+ } catch {
2529
+ throw new NotInitializedError(projectRoot);
2530
+ }
2531
+ const swarm = await loadSwarm(projectRoot, templateName);
2532
+ const userVars = parseVars(options.var ?? []);
2533
+ if (options.worktree) {
2534
+ await swarmIntoExistingWorktree(projectRoot, config, swarm, options, userVars);
2535
+ } else if (swarm.strategy === "isolated") {
2536
+ await swarmIsolated(projectRoot, config, swarm, options, userVars);
2537
+ } else {
2538
+ await swarmShared(projectRoot, config, swarm, options, userVars);
2539
+ }
2540
+ }
2541
+ function parseVars(vars) {
2542
+ const result = {};
2543
+ for (const v of vars) {
2544
+ const eqIdx = v.indexOf("=");
2545
+ if (eqIdx < 1) {
2546
+ throw new PgError(`Invalid --var format: "${v}" \u2014 expected KEY=value`, "INVALID_ARGS");
2547
+ }
2548
+ result[v.slice(0, eqIdx)] = v.slice(eqIdx + 1);
2549
+ }
2550
+ return result;
2551
+ }
2552
+ async function loadPromptFile(projectRoot, promptName) {
2553
+ const filePath = path6.join(promptsDir(projectRoot), `${promptName}.md`);
2554
+ try {
2555
+ return await fs11.readFile(filePath, "utf-8");
2556
+ } catch {
2557
+ throw new PgError(`Prompt file not found: ${promptName}.md in .pg/prompts/`, "INVALID_ARGS");
2558
+ }
2559
+ }
2560
+ async function spawnSwarmAgent(opts) {
2561
+ const { projectRoot, config, swarmAgent, wtPath, branchName, tmuxTarget, taskName, userVars } = opts;
2562
+ const agentConfig = resolveAgentConfig(config, swarmAgent.agent);
2563
+ const aId = agentId();
2564
+ const promptContent = await loadPromptFile(projectRoot, swarmAgent.prompt);
2565
+ const ctx = {
2566
+ WORKTREE_PATH: wtPath,
2567
+ BRANCH: branchName,
2568
+ AGENT_ID: aId,
2569
+ RESULT_FILE: resultFile(projectRoot, aId),
2570
+ PROJECT_ROOT: projectRoot,
2571
+ TASK_NAME: taskName,
2572
+ ...swarmAgent.vars ?? {},
2573
+ ...userVars
2574
+ };
2575
+ const renderedPrompt = renderTemplate(promptContent, ctx);
2576
+ return spawnAgent({
2577
+ agentId: aId,
2578
+ agentConfig,
2579
+ prompt: renderedPrompt,
2580
+ worktreePath: wtPath,
2581
+ tmuxTarget,
2582
+ projectRoot,
2583
+ branch: branchName,
2584
+ sessionId: sessionId()
2585
+ });
2586
+ }
2587
+ async function swarmShared(projectRoot, config, swarm, options, userVars) {
2588
+ const baseBranch = options.base ?? await getCurrentBranch(projectRoot);
2589
+ const wtId = worktreeId();
2590
+ const name = options.name ? normalizeName(options.name, swarm.name) : swarm.name;
2591
+ const branchName = `ppg/${name}`;
2592
+ info(`Creating worktree ${wtId} on branch ${branchName}`);
2593
+ const wtPath = await createWorktree(projectRoot, wtId, {
2594
+ branch: branchName,
2595
+ base: baseBranch
2596
+ });
2597
+ await setupWorktreeEnv(projectRoot, wtPath, config);
2598
+ const sessionName = config.sessionName;
2599
+ await ensureSession(sessionName);
2600
+ const windowTarget = await createWindow(sessionName, name, wtPath);
2601
+ await updateManifest(projectRoot, (m) => {
2602
+ m.worktrees[wtId] = {
2603
+ id: wtId,
2604
+ name,
2605
+ path: wtPath,
2606
+ branch: branchName,
2607
+ baseBranch,
2608
+ status: "active",
2609
+ tmuxWindow: windowTarget,
2610
+ agents: {},
2611
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2612
+ };
2613
+ return m;
2614
+ });
2615
+ const agents = [];
2616
+ for (let i = 0; i < swarm.agents.length; i++) {
2617
+ const target = i === 0 ? windowTarget : await createWindow(sessionName, `${name}-${i}`, wtPath);
2618
+ const agentEntry = await spawnSwarmAgent({
2619
+ projectRoot,
2620
+ config,
2621
+ swarmAgent: swarm.agents[i],
2622
+ wtPath,
2623
+ branchName,
2624
+ tmuxTarget: target,
2625
+ taskName: name,
2626
+ userVars
2627
+ });
2628
+ agents.push(agentEntry);
2629
+ await updateManifest(projectRoot, (m) => {
2630
+ m.worktrees[wtId].agents[agentEntry.id] = agentEntry;
2631
+ return m;
2632
+ });
2633
+ }
2634
+ if (options.open === true) {
2635
+ openTerminalWindow(sessionName, windowTarget, name).catch(() => {
2636
+ });
2637
+ }
2638
+ outputResult(options.json, swarm, wtId, name, branchName, wtPath, windowTarget, agents);
2639
+ }
2640
+ async function swarmIsolated(projectRoot, config, swarm, options, userVars) {
2641
+ const baseBranch = options.base ?? await getCurrentBranch(projectRoot);
2642
+ const baseName = options.name ? normalizeName(options.name, swarm.name) : swarm.name;
2643
+ const sessionName = config.sessionName;
2644
+ await ensureSession(sessionName);
2645
+ const worktrees = [];
2646
+ const allAgents = [];
2647
+ const usedNames = /* @__PURE__ */ new Set();
2648
+ for (const swarmAgent of swarm.agents) {
2649
+ const wtId = worktreeId();
2650
+ let wtName = `${baseName}-${swarmAgent.prompt}`;
2651
+ if (usedNames.has(wtName)) {
2652
+ let suffix = 2;
2653
+ while (usedNames.has(`${wtName}-${suffix}`)) suffix++;
2654
+ wtName = `${wtName}-${suffix}`;
2655
+ }
2656
+ usedNames.add(wtName);
2657
+ const branchName = `ppg/${wtName}`;
2658
+ info(`Creating worktree ${wtId} on branch ${branchName}`);
2659
+ const wtPath = await createWorktree(projectRoot, wtId, {
2660
+ branch: branchName,
2661
+ base: baseBranch
2662
+ });
2663
+ await setupWorktreeEnv(projectRoot, wtPath, config);
2664
+ const windowTarget = await createWindow(sessionName, wtName, wtPath);
2665
+ const agentEntry = await spawnSwarmAgent({
2666
+ projectRoot,
2667
+ config,
2668
+ swarmAgent,
2669
+ wtPath,
2670
+ branchName,
2671
+ tmuxTarget: windowTarget,
2672
+ taskName: wtName,
2673
+ userVars
2674
+ });
2675
+ const worktreeEntry = {
2676
+ id: wtId,
2677
+ name: wtName,
2678
+ path: wtPath,
2679
+ branch: branchName,
2680
+ baseBranch,
2681
+ status: "active",
2682
+ tmuxWindow: windowTarget,
2683
+ agents: { [agentEntry.id]: agentEntry },
2684
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2685
+ };
2686
+ await updateManifest(projectRoot, (m) => {
2687
+ m.worktrees[wtId] = worktreeEntry;
2688
+ return m;
2689
+ });
2690
+ if (options.open === true) {
2691
+ openTerminalWindow(sessionName, windowTarget, wtName).catch(() => {
2692
+ });
2693
+ }
2694
+ worktrees.push({ id: wtId, name: wtName, branch: branchName, path: wtPath, tmuxWindow: windowTarget });
2695
+ allAgents.push(agentEntry);
2696
+ }
2697
+ if (options.json) {
2698
+ output({
2699
+ success: true,
2700
+ swarm: swarm.name,
2701
+ strategy: "isolated",
2702
+ worktrees: worktrees.map((wt, i) => ({
2703
+ ...wt,
2704
+ agents: [{ id: allAgents[i].id, tmuxTarget: allAgents[i].tmuxTarget, sessionId: allAgents[i].sessionId }]
2705
+ }))
2706
+ }, true);
2707
+ } else {
2708
+ success(`Swarm "${swarm.name}" spawned ${swarm.agents.length} agent(s) in isolated worktrees`);
2709
+ for (let i = 0; i < worktrees.length; i++) {
2710
+ info(` ${worktrees[i].name} (${worktrees[i].id}) \u2192 agent ${allAgents[i].id}`);
2711
+ }
2712
+ }
2713
+ }
2714
+ async function swarmIntoExistingWorktree(projectRoot, config, swarm, options, userVars) {
2715
+ const manifest = await readManifest(projectRoot);
2716
+ const wt = resolveWorktree(manifest, options.worktree);
2717
+ if (!wt) throw new WorktreeNotFoundError(options.worktree);
2718
+ const sessionName = config.sessionName;
2719
+ let windowTarget = wt.tmuxWindow;
2720
+ if (!windowTarget) {
2721
+ await ensureSession(sessionName);
2722
+ windowTarget = await createWindow(sessionName, wt.name, wt.path);
2723
+ }
2724
+ if (!wt.tmuxWindow) {
2034
2725
  await updateManifest(projectRoot, (m) => {
2035
- if (m.worktrees[wt.id]) {
2036
- m.worktrees[wt.id].status = "failed";
2037
- }
2726
+ m.worktrees[wt.id].tmuxWindow = windowTarget;
2038
2727
  return m;
2039
2728
  });
2040
- throw new MergeFailedError(
2041
- `Merge failed: ${err instanceof Error ? err.message : err}`
2042
- );
2043
2729
  }
2044
- await updateManifest(projectRoot, (m) => {
2045
- if (m.worktrees[wt.id]) {
2046
- m.worktrees[wt.id].status = "merged";
2047
- m.worktrees[wt.id].mergedAt = (/* @__PURE__ */ new Date()).toISOString();
2048
- }
2049
- return m;
2050
- });
2051
- if (options.cleanup !== false) {
2052
- info("Cleaning up...");
2053
- await cleanupWorktree(projectRoot, wt);
2054
- success(`Cleaned up worktree ${wt.id}`);
2730
+ const agents = [];
2731
+ for (let i = 0; i < swarm.agents.length; i++) {
2732
+ const target = i === 0 ? windowTarget : await createWindow(sessionName, `${wt.name}-${swarm.name}-${i}`, wt.path);
2733
+ const agentEntry = await spawnSwarmAgent({
2734
+ projectRoot,
2735
+ config,
2736
+ swarmAgent: swarm.agents[i],
2737
+ wtPath: wt.path,
2738
+ branchName: wt.branch,
2739
+ tmuxTarget: target,
2740
+ taskName: wt.name,
2741
+ userVars
2742
+ });
2743
+ agents.push(agentEntry);
2744
+ await updateManifest(projectRoot, (m) => {
2745
+ m.worktrees[wt.id].agents[agentEntry.id] = agentEntry;
2746
+ return m;
2747
+ });
2055
2748
  }
2056
- if (options.json) {
2749
+ if (options.open === true) {
2750
+ openTerminalWindow(sessionName, windowTarget, wt.name).catch(() => {
2751
+ });
2752
+ }
2753
+ outputResult(options.json, swarm, wt.id, wt.name, wt.branch, wt.path, windowTarget, agents);
2754
+ }
2755
+ function outputResult(json, swarm, wtId, name, branch, wtPath, tmuxWindow, agents) {
2756
+ if (json) {
2057
2757
  output({
2058
2758
  success: true,
2059
- worktreeId: wt.id,
2060
- branch: wt.branch,
2061
- baseBranch: wt.baseBranch,
2062
- strategy,
2063
- cleaned: options.cleanup !== false
2759
+ swarm: swarm.name,
2760
+ strategy: swarm.strategy,
2761
+ worktree: { id: wtId, name, branch, path: wtPath, tmuxWindow },
2762
+ agents: agents.map((a) => ({ id: a.id, tmuxTarget: a.tmuxTarget, sessionId: a.sessionId }))
2064
2763
  }, true);
2764
+ } else {
2765
+ success(`Swarm "${swarm.name}" spawned ${agents.length} agent(s) in worktree ${wtId}`);
2766
+ for (const a of agents) {
2767
+ info(` Agent ${a.id} \u2192 ${a.tmuxTarget}`);
2768
+ }
2769
+ info(`Attach: ppg attach ${wtId}`);
2065
2770
  }
2066
2771
  }
2067
- var init_merge = __esm({
2068
- "src/commands/merge.ts"() {
2772
+ var init_swarm2 = __esm({
2773
+ "src/commands/swarm.ts"() {
2069
2774
  "use strict";
2775
+ init_config();
2070
2776
  init_manifest();
2071
- init_agent();
2072
2777
  init_worktree();
2073
- init_cleanup();
2778
+ init_env2();
2779
+ init_template();
2780
+ init_swarm();
2781
+ init_agent();
2782
+ init_tmux();
2783
+ init_terminal();
2784
+ init_id();
2785
+ init_paths();
2074
2786
  init_errors();
2075
2787
  init_output();
2788
+ init_name();
2076
2789
  }
2077
2790
  });
2078
2791
 
@@ -2081,12 +2794,18 @@ var list_exports = {};
2081
2794
  __export(list_exports, {
2082
2795
  listCommand: () => listCommand
2083
2796
  });
2084
- import fs9 from "fs/promises";
2085
- import path5 from "path";
2797
+ import fs12 from "fs/promises";
2798
+ import path7 from "path";
2086
2799
  async function listCommand(type, options) {
2087
- if (type !== "templates") {
2088
- throw new PgError(`Unknown list type: ${type}. Available: templates`, "INVALID_ARGS");
2800
+ if (type === "templates") {
2801
+ await listTemplatesCommand(options);
2802
+ } else if (type === "swarms") {
2803
+ await listSwarmsCommand(options);
2804
+ } else {
2805
+ throw new PgError(`Unknown list type: ${type}. Available: templates, swarms`, "INVALID_ARGS");
2089
2806
  }
2807
+ }
2808
+ async function listTemplatesCommand(options) {
2090
2809
  const projectRoot = await getRepoRoot();
2091
2810
  const templateNames = await listTemplates(projectRoot);
2092
2811
  if (templateNames.length === 0) {
@@ -2095,8 +2814,8 @@ async function listCommand(type, options) {
2095
2814
  }
2096
2815
  const templates = await Promise.all(
2097
2816
  templateNames.map(async (name) => {
2098
- const filePath = path5.join(templatesDir(projectRoot), `${name}.md`);
2099
- const content = await fs9.readFile(filePath, "utf-8");
2817
+ const filePath = path7.join(templatesDir(projectRoot), `${name}.md`);
2818
+ const content = await fs12.readFile(filePath, "utf-8");
2100
2819
  const firstLine = content.split("\n").find((l) => l.trim().length > 0) ?? "";
2101
2820
  const description = firstLine.replace(/^#+\s*/, "").trim();
2102
2821
  const vars = [...content.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]);
@@ -2120,11 +2839,46 @@ async function listCommand(type, options) {
2120
2839
  ];
2121
2840
  console.log(formatTable(templates, columns));
2122
2841
  }
2842
+ async function listSwarmsCommand(options) {
2843
+ const projectRoot = await getRepoRoot();
2844
+ const swarmNames = await listSwarms(projectRoot);
2845
+ if (swarmNames.length === 0) {
2846
+ if (options.json) {
2847
+ output({ swarms: [] }, true);
2848
+ } else {
2849
+ console.log("No swarm templates found in .pg/swarms/");
2850
+ }
2851
+ return;
2852
+ }
2853
+ const swarms = await Promise.all(
2854
+ swarmNames.map(async (name) => {
2855
+ const swarm = await loadSwarm(projectRoot, name);
2856
+ return {
2857
+ name,
2858
+ description: swarm.description,
2859
+ strategy: swarm.strategy,
2860
+ agents: swarm.agents.length
2861
+ };
2862
+ })
2863
+ );
2864
+ if (options.json) {
2865
+ output({ swarms }, true);
2866
+ return;
2867
+ }
2868
+ const columns = [
2869
+ { header: "Name", key: "name", width: 20 },
2870
+ { header: "Description", key: "description", width: 40 },
2871
+ { header: "Strategy", key: "strategy", width: 10 },
2872
+ { header: "Agents", key: "agents", width: 8 }
2873
+ ];
2874
+ console.log(formatTable(swarms, columns));
2875
+ }
2123
2876
  var init_list = __esm({
2124
2877
  "src/commands/list.ts"() {
2125
2878
  "use strict";
2126
2879
  init_worktree();
2127
2880
  init_template();
2881
+ init_swarm();
2128
2882
  init_paths();
2129
2883
  init_errors();
2130
2884
  init_output();
@@ -2136,7 +2890,7 @@ var restart_exports = {};
2136
2890
  __export(restart_exports, {
2137
2891
  restartCommand: () => restartCommand
2138
2892
  });
2139
- import fs10 from "fs/promises";
2893
+ import fs13 from "fs/promises";
2140
2894
  async function restartCommand(agentRef, options) {
2141
2895
  const projectRoot = await getRepoRoot();
2142
2896
  const config = await loadConfig(projectRoot);
@@ -2157,9 +2911,9 @@ async function restartCommand(agentRef, options) {
2157
2911
  if (options.prompt) {
2158
2912
  promptText = options.prompt;
2159
2913
  } else {
2160
- const pFile = promptFile(projectRoot, oldAgent.id);
2914
+ const pFile = agentPromptFile(projectRoot, oldAgent.id);
2161
2915
  try {
2162
- promptText = await fs10.readFile(pFile, "utf-8");
2916
+ promptText = await fs13.readFile(pFile, "utf-8");
2163
2917
  } catch {
2164
2918
  throw new PgError(
2165
2919
  `Could not read original prompt for agent ${oldAgent.id}. Use --prompt to provide one.`,
@@ -2204,7 +2958,7 @@ async function restartCommand(agentRef, options) {
2204
2958
  }
2205
2959
  return m;
2206
2960
  });
2207
- if (options.open !== false) {
2961
+ if (options.open === true) {
2208
2962
  openTerminalWindow(manifest.sessionName, windowTarget, `${wt.name}-restart`).catch(() => {
2209
2963
  });
2210
2964
  }
@@ -2262,7 +3016,7 @@ async function diffCommand(worktreeRef, options) {
2262
3016
  if (!wt) throw new WorktreeNotFoundError(worktreeRef);
2263
3017
  const diffRange = `${wt.baseBranch}...${wt.branch}`;
2264
3018
  if (options.json) {
2265
- const result = await execa6("git", ["diff", "--numstat", diffRange], { cwd: projectRoot });
3019
+ const result = await execa6("git", ["diff", "--numstat", diffRange], { ...execaEnv, cwd: projectRoot });
2266
3020
  const files = result.stdout.trim().split("\n").filter(Boolean).map((line) => {
2267
3021
  const [added, removed, file] = line.split(" ");
2268
3022
  return {
@@ -2278,13 +3032,13 @@ async function diffCommand(worktreeRef, options) {
2278
3032
  files
2279
3033
  }, true);
2280
3034
  } else if (options.stat) {
2281
- const result = await execa6("git", ["diff", "--stat", diffRange], { cwd: projectRoot });
3035
+ const result = await execa6("git", ["diff", "--stat", diffRange], { ...execaEnv, cwd: projectRoot });
2282
3036
  console.log(result.stdout);
2283
3037
  } else if (options.nameOnly) {
2284
- const result = await execa6("git", ["diff", "--name-only", diffRange], { cwd: projectRoot });
3038
+ const result = await execa6("git", ["diff", "--name-only", diffRange], { ...execaEnv, cwd: projectRoot });
2285
3039
  console.log(result.stdout);
2286
3040
  } else {
2287
- const result = await execa6("git", ["diff", diffRange], { cwd: projectRoot });
3041
+ const result = await execa6("git", ["diff", diffRange], { ...execaEnv, cwd: projectRoot });
2288
3042
  console.log(result.stdout);
2289
3043
  }
2290
3044
  }
@@ -2295,6 +3049,269 @@ var init_diff = __esm({
2295
3049
  init_worktree();
2296
3050
  init_errors();
2297
3051
  init_output();
3052
+ init_env();
3053
+ }
3054
+ });
3055
+
3056
+ // src/commands/pr.ts
3057
+ var pr_exports = {};
3058
+ __export(pr_exports, {
3059
+ buildBodyFromResults: () => buildBodyFromResults,
3060
+ prCommand: () => prCommand,
3061
+ truncateBody: () => truncateBody
3062
+ });
3063
+ import fs14 from "fs/promises";
3064
+ import { execa as execa7 } from "execa";
3065
+ async function prCommand(worktreeRef, options) {
3066
+ const projectRoot = await getRepoRoot();
3067
+ let manifest;
3068
+ try {
3069
+ manifest = await updateManifest(projectRoot, async (m) => {
3070
+ return refreshAllAgentStatuses(m, projectRoot);
3071
+ });
3072
+ } catch {
3073
+ throw new NotInitializedError(projectRoot);
3074
+ }
3075
+ const wt = resolveWorktree(manifest, worktreeRef);
3076
+ if (!wt) throw new WorktreeNotFoundError(worktreeRef);
3077
+ try {
3078
+ await execa7("gh", ["--version"], execaEnv);
3079
+ } catch {
3080
+ throw new GhNotFoundError();
3081
+ }
3082
+ info(`Pushing branch ${wt.branch} to origin`);
3083
+ try {
3084
+ await execa7("git", ["push", "-u", "origin", wt.branch], { ...execaEnv, cwd: projectRoot });
3085
+ } catch (err) {
3086
+ throw new PgError(
3087
+ `Failed to push branch ${wt.branch}: ${err instanceof Error ? err.message : err}`,
3088
+ "INVALID_ARGS"
3089
+ );
3090
+ }
3091
+ const title = options.title ?? wt.name;
3092
+ const body = options.body ?? await buildBodyFromResults(Object.values(wt.agents));
3093
+ const ghArgs = [
3094
+ "pr",
3095
+ "create",
3096
+ "--head",
3097
+ wt.branch,
3098
+ "--base",
3099
+ wt.baseBranch,
3100
+ "--title",
3101
+ title,
3102
+ "--body",
3103
+ body
3104
+ ];
3105
+ if (options.draft) {
3106
+ ghArgs.push("--draft");
3107
+ }
3108
+ info(`Creating PR: ${title}`);
3109
+ let prUrl;
3110
+ try {
3111
+ const result = await execa7("gh", ghArgs, { ...execaEnv, cwd: projectRoot });
3112
+ prUrl = result.stdout.trim();
3113
+ } catch (err) {
3114
+ throw new PgError(
3115
+ `Failed to create PR: ${err instanceof Error ? err.message : err}`,
3116
+ "INVALID_ARGS"
3117
+ );
3118
+ }
3119
+ await updateManifest(projectRoot, (m) => {
3120
+ if (m.worktrees[wt.id]) {
3121
+ m.worktrees[wt.id].prUrl = prUrl;
3122
+ }
3123
+ return m;
3124
+ });
3125
+ if (options.json) {
3126
+ output({
3127
+ success: true,
3128
+ worktreeId: wt.id,
3129
+ branch: wt.branch,
3130
+ baseBranch: wt.baseBranch,
3131
+ prUrl
3132
+ }, true);
3133
+ } else {
3134
+ success(`PR created: ${prUrl}`);
3135
+ }
3136
+ }
3137
+ async function buildBodyFromResults(agents) {
3138
+ const reads = agents.map(async (agent) => {
3139
+ try {
3140
+ return await fs14.readFile(agent.resultFile, "utf-8");
3141
+ } catch {
3142
+ return null;
3143
+ }
3144
+ });
3145
+ const contents = (await Promise.all(reads)).filter((c) => c !== null);
3146
+ if (contents.length === 0) return "";
3147
+ return truncateBody(contents.join("\n\n---\n\n"));
3148
+ }
3149
+ function truncateBody(body) {
3150
+ if (body.length <= MAX_BODY_LENGTH) return body;
3151
+ return body.slice(0, MAX_BODY_LENGTH) + "\n\n---\n\n*[Truncated \u2014 full results available in `.pg/results/`]*";
3152
+ }
3153
+ var MAX_BODY_LENGTH;
3154
+ var init_pr = __esm({
3155
+ "src/commands/pr.ts"() {
3156
+ "use strict";
3157
+ init_manifest();
3158
+ init_agent();
3159
+ init_worktree();
3160
+ init_errors();
3161
+ init_output();
3162
+ init_env();
3163
+ MAX_BODY_LENGTH = 6e4;
3164
+ }
3165
+ });
3166
+
3167
+ // src/commands/reset.ts
3168
+ var reset_exports = {};
3169
+ __export(reset_exports, {
3170
+ findAtRiskWorktrees: () => findAtRiskWorktrees,
3171
+ resetCommand: () => resetCommand
3172
+ });
3173
+ function findAtRiskWorktrees(worktrees) {
3174
+ return worktrees.filter((wt) => {
3175
+ if (wt.status === "merged" || wt.status === "cleaned") return false;
3176
+ if (wt.prUrl) return false;
3177
+ return Object.values(wt.agents).some((a) => a.status === "completed");
3178
+ });
3179
+ }
3180
+ async function resetCommand(options) {
3181
+ const projectRoot = await getRepoRoot();
3182
+ let manifest;
3183
+ try {
3184
+ manifest = await updateManifest(projectRoot, async (m) => {
3185
+ return refreshAllAgentStatuses(m, projectRoot);
3186
+ });
3187
+ } catch {
3188
+ throw new NotInitializedError(projectRoot);
3189
+ }
3190
+ const worktrees = Object.values(manifest.worktrees);
3191
+ if (worktrees.length === 0) {
3192
+ if (options.json) {
3193
+ output({ success: true, killed: [], removed: [], warned: [] }, true);
3194
+ } else {
3195
+ info("Nothing to reset \u2014 no worktrees in manifest");
3196
+ }
3197
+ return;
3198
+ }
3199
+ const atRisk = findAtRiskWorktrees(worktrees);
3200
+ if (atRisk.length > 0 && !options.force) {
3201
+ throw new UnmergedWorkError(atRisk.map((wt) => `${wt.name} (${wt.branch})`));
3202
+ }
3203
+ if (atRisk.length > 0) {
3204
+ warn(`${atRisk.length} worktree(s) have unmerged/un-PR'd work \u2014 proceeding with --force`);
3205
+ }
3206
+ const selfPaneId = getCurrentPaneId();
3207
+ let paneMap;
3208
+ if (selfPaneId) {
3209
+ paneMap = await listSessionPanes(manifest.sessionName);
3210
+ }
3211
+ const allAgents = [];
3212
+ for (const wt of worktrees) {
3213
+ for (const agent of Object.values(wt.agents)) {
3214
+ if (["running", "spawning", "waiting"].includes(agent.status)) {
3215
+ allAgents.push(agent);
3216
+ }
3217
+ }
3218
+ }
3219
+ let agentsToKill = allAgents;
3220
+ const skippedAgentIds = [];
3221
+ if (selfPaneId && paneMap) {
3222
+ const { safe, skipped } = excludeSelf(allAgents, selfPaneId, paneMap);
3223
+ agentsToKill = safe;
3224
+ for (const a of skipped) {
3225
+ skippedAgentIds.push(a.id);
3226
+ warn(`Skipping agent ${a.id} \u2014 contains current ppg process`);
3227
+ }
3228
+ }
3229
+ const killedIds = agentsToKill.map((a) => a.id);
3230
+ if (agentsToKill.length > 0) {
3231
+ info(`Killing ${agentsToKill.length} running agent(s)`);
3232
+ await killAgents(agentsToKill);
3233
+ }
3234
+ if (killedIds.length > 0) {
3235
+ await updateManifest(projectRoot, (m) => {
3236
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3237
+ for (const wt of Object.values(m.worktrees)) {
3238
+ for (const agent of Object.values(wt.agents)) {
3239
+ if (killedIds.includes(agent.id)) {
3240
+ agent.status = "killed";
3241
+ agent.completedAt = now;
3242
+ }
3243
+ }
3244
+ }
3245
+ return m;
3246
+ });
3247
+ }
3248
+ const removedIds = [];
3249
+ const skippedWorktreeIds = [];
3250
+ for (const wt of worktrees) {
3251
+ if (selfPaneId && paneMap && wouldCleanupAffectSelf(wt, selfPaneId, paneMap)) {
3252
+ warn(`Skipping cleanup of worktree ${wt.id} (${wt.name}) \u2014 contains current ppg process`);
3253
+ skippedWorktreeIds.push(wt.id);
3254
+ continue;
3255
+ }
3256
+ if (wt.status !== "cleaned") {
3257
+ info(`Removing worktree ${wt.id} (${wt.name})`);
3258
+ await cleanupWorktree(projectRoot, wt, { selfPaneId, paneMap });
3259
+ }
3260
+ removedIds.push(wt.id);
3261
+ }
3262
+ if (removedIds.length > 0) {
3263
+ await updateManifest(projectRoot, (m) => {
3264
+ for (const id of removedIds) {
3265
+ delete m.worktrees[id];
3266
+ }
3267
+ return m;
3268
+ });
3269
+ }
3270
+ if (options.prune) {
3271
+ info("Pruning stale git worktrees");
3272
+ await pruneWorktrees(projectRoot);
3273
+ }
3274
+ const warnedNames = atRisk.map((wt) => wt.name);
3275
+ if (options.json) {
3276
+ output({
3277
+ success: true,
3278
+ killed: killedIds,
3279
+ removed: removedIds,
3280
+ warned: warnedNames.length > 0 ? warnedNames : void 0,
3281
+ skipped: skippedWorktreeIds.length > 0 ? skippedWorktreeIds : void 0,
3282
+ pruned: options.prune ?? false
3283
+ }, true);
3284
+ } else {
3285
+ if (killedIds.length > 0) {
3286
+ success(`Killed ${killedIds.length} agent(s)`);
3287
+ }
3288
+ if (removedIds.length > 0) {
3289
+ success(`Removed ${removedIds.length} worktree(s)`);
3290
+ }
3291
+ if (skippedWorktreeIds.length > 0) {
3292
+ warn(`Skipped ${skippedWorktreeIds.length} worktree(s) due to self-protection`);
3293
+ }
3294
+ if (options.prune) {
3295
+ success("Pruned stale git worktrees");
3296
+ }
3297
+ if (killedIds.length > 0 || removedIds.length > 0) {
3298
+ success("Reset complete");
3299
+ } else {
3300
+ info("Nothing to reset");
3301
+ }
3302
+ }
3303
+ }
3304
+ var init_reset = __esm({
3305
+ "src/commands/reset.ts"() {
3306
+ "use strict";
3307
+ init_manifest();
3308
+ init_agent();
3309
+ init_worktree();
3310
+ init_cleanup();
3311
+ init_self();
3312
+ init_tmux();
3313
+ init_errors();
3314
+ init_output();
2298
3315
  }
2299
3316
  });
2300
3317
 
@@ -2311,6 +3328,11 @@ async function cleanCommand(options) {
2311
3328
  } catch {
2312
3329
  throw new NotInitializedError(projectRoot);
2313
3330
  }
3331
+ const selfPaneId = getCurrentPaneId();
3332
+ let paneMap;
3333
+ if (selfPaneId) {
3334
+ paneMap = await listSessionPanes(manifest.sessionName);
3335
+ }
2314
3336
  const terminalStatuses = ["merged", "cleaned"];
2315
3337
  if (options.all) {
2316
3338
  terminalStatuses.push("failed");
@@ -2348,11 +3370,17 @@ async function cleanCommand(options) {
2348
3370
  return;
2349
3371
  }
2350
3372
  const cleaned = [];
3373
+ const skipped = [];
2351
3374
  const removed = [];
2352
3375
  for (const wt of toClean) {
2353
3376
  if (wt.status !== "cleaned") {
3377
+ if (selfPaneId && paneMap && wouldCleanupAffectSelf(wt, selfPaneId, paneMap)) {
3378
+ warn(`Skipping cleanup of worktree ${wt.id} (${wt.name}) \u2014 contains current ppg process`);
3379
+ skipped.push(wt.id);
3380
+ continue;
3381
+ }
2354
3382
  info(`Cleaning worktree ${wt.id} (${wt.name})`);
2355
- await cleanupWorktree(projectRoot, wt);
3383
+ await cleanupWorktree(projectRoot, wt, { selfPaneId, paneMap });
2356
3384
  cleaned.push(wt.id);
2357
3385
  }
2358
3386
  }
@@ -2377,6 +3405,7 @@ async function cleanCommand(options) {
2377
3405
  output({
2378
3406
  success: true,
2379
3407
  cleaned,
3408
+ skipped: skipped.length > 0 ? skipped : void 0,
2380
3409
  removedFromManifest: removed,
2381
3410
  pruned: options.prune ?? false
2382
3411
  }, true);
@@ -2384,10 +3413,13 @@ async function cleanCommand(options) {
2384
3413
  if (cleaned.length > 0) {
2385
3414
  success(`Cleaned ${cleaned.length} worktree(s)`);
2386
3415
  }
3416
+ if (skipped.length > 0) {
3417
+ warn(`Skipped ${skipped.length} worktree(s) due to self-protection`);
3418
+ }
2387
3419
  if (removed.length > 0) {
2388
3420
  success(`Removed ${removed.length} worktree(s) from manifest`);
2389
3421
  }
2390
- if (cleaned.length === 0 && removed.length === 0) {
3422
+ if (cleaned.length === 0 && removed.length === 0 && skipped.length === 0) {
2391
3423
  info("Nothing to clean");
2392
3424
  }
2393
3425
  if (options.prune) {
@@ -2401,6 +3433,8 @@ var init_clean = __esm({
2401
3433
  init_manifest();
2402
3434
  init_worktree();
2403
3435
  init_cleanup();
3436
+ init_self();
3437
+ init_tmux();
2404
3438
  init_errors();
2405
3439
  init_output();
2406
3440
  }
@@ -2561,7 +3595,7 @@ async function worktreeCreateCommand(options) {
2561
3595
  }
2562
3596
  const baseBranch = options.base ?? await getCurrentBranch(projectRoot);
2563
3597
  const wtId = worktreeId();
2564
- const name = options.name ?? wtId;
3598
+ const name = options.name ? normalizeName(options.name, wtId) : wtId;
2565
3599
  const branchName = `ppg/${name}`;
2566
3600
  info(`Creating worktree ${wtId} on branch ${branchName}`);
2567
3601
  const wtPath = await createWorktree(projectRoot, wtId, {
@@ -2607,10 +3641,11 @@ var init_worktree2 = __esm({
2607
3641
  init_config();
2608
3642
  init_manifest();
2609
3643
  init_worktree();
2610
- init_env();
3644
+ init_env2();
2611
3645
  init_id();
2612
3646
  init_errors();
2613
3647
  init_output();
3648
+ init_name();
2614
3649
  }
2615
3650
  });
2616
3651
 
@@ -2621,10 +3656,10 @@ __export(ui_exports, {
2621
3656
  uiCommand: () => uiCommand
2622
3657
  });
2623
3658
  import { access } from "fs/promises";
2624
- import path6 from "path";
2625
- import { execa as execa7 } from "execa";
3659
+ import path8 from "path";
3660
+ import { execa as execa8 } from "execa";
2626
3661
  async function findDashboardBinary(projectRoot) {
2627
- const localBuild = path6.join(
3662
+ const localBuild = path8.join(
2628
3663
  projectRoot,
2629
3664
  "PPG CLI",
2630
3665
  "build",
@@ -2648,12 +3683,12 @@ async function findDashboardBinary(projectRoot) {
2648
3683
  } catch {
2649
3684
  }
2650
3685
  try {
2651
- const result = await execa7("mdfind", [
3686
+ const result = await execa8("mdfind", [
2652
3687
  'kMDItemCFBundleIdentifier == "com.2wit.PPG-CLI"'
2653
3688
  ]);
2654
3689
  const appPath = result.stdout.trim().split("\n")[0];
2655
3690
  if (appPath) {
2656
- const binaryPath = path6.join(appPath, "Contents", "MacOS", "PPG CLI");
3691
+ const binaryPath = path8.join(appPath, "Contents", "MacOS", "PPG CLI");
2657
3692
  try {
2658
3693
  await access(binaryPath);
2659
3694
  return binaryPath;
@@ -2684,7 +3719,7 @@ Or build from source:
2684
3719
  );
2685
3720
  }
2686
3721
  const mPath = manifestPath(projectRoot);
2687
- const proc = execa7(binaryPath, [
3722
+ const proc = execa8(binaryPath, [
2688
3723
  "--manifest-path",
2689
3724
  mPath,
2690
3725
  "--session-name",
@@ -2718,10 +3753,10 @@ import { createWriteStream } from "fs";
2718
3753
  import { mkdir, cp, rm } from "fs/promises";
2719
3754
  import { createRequire } from "module";
2720
3755
  import { tmpdir } from "os";
2721
- import path7 from "path";
3756
+ import path9 from "path";
2722
3757
  import { pipeline } from "stream/promises";
2723
3758
  import { Readable } from "stream";
2724
- import { execa as execa8 } from "execa";
3759
+ import { execa as execa9 } from "execa";
2725
3760
  function getVersion() {
2726
3761
  const pkg2 = require2("../package.json");
2727
3762
  return pkg2.version;
@@ -2747,9 +3782,9 @@ Check: https://github.com/${REPO}/releases/tag/${tag}`,
2747
3782
  "DOWNLOAD_FAILED"
2748
3783
  );
2749
3784
  }
2750
- const tmp = path7.join(tmpdir(), `ppg-dashboard-${Date.now()}`);
3785
+ const tmp = path9.join(tmpdir(), `ppg-dashboard-${Date.now()}`);
2751
3786
  await mkdir(tmp, { recursive: true });
2752
- const dmgPath = path7.join(tmp, ASSET_NAME);
3787
+ const dmgPath = path9.join(tmp, ASSET_NAME);
2753
3788
  const body = res.body;
2754
3789
  if (!body) throw new PgError("Empty response body", "DOWNLOAD_FAILED");
2755
3790
  await pipeline(
@@ -2757,20 +3792,20 @@ Check: https://github.com/${REPO}/releases/tag/${tag}`,
2757
3792
  createWriteStream(dmgPath)
2758
3793
  );
2759
3794
  if (!json) info("Mounting\u2026");
2760
- const mountResult = await execa8("hdiutil", ["attach", dmgPath, "-nobrowse", "-quiet"]);
3795
+ const mountResult = await execa9("hdiutil", ["attach", dmgPath, "-nobrowse", "-quiet"]);
2761
3796
  const mountLine = mountResult.stdout.trim().split("\n").pop() ?? "";
2762
3797
  const mountPoint = mountLine.split(" ").pop()?.trim();
2763
3798
  if (!mountPoint) {
2764
3799
  throw new PgError("Failed to mount DMG \u2014 could not determine mount point", "INSTALL_FAILED");
2765
3800
  }
2766
3801
  try {
2767
- const srcApp = path7.join(mountPoint, APP_NAME);
2768
- const destApp = path7.join(dir, APP_NAME);
3802
+ const srcApp = path9.join(mountPoint, APP_NAME);
3803
+ const destApp = path9.join(dir, APP_NAME);
2769
3804
  if (!json) info("Installing\u2026");
2770
3805
  await rm(destApp, { recursive: true, force: true });
2771
3806
  await cp(srcApp, destApp, { recursive: true });
2772
3807
  try {
2773
- await execa8("xattr", ["-dr", "com.apple.quarantine", destApp]);
3808
+ await execa9("xattr", ["-dr", "com.apple.quarantine", destApp]);
2774
3809
  } catch {
2775
3810
  }
2776
3811
  if (json) {
@@ -2779,7 +3814,7 @@ Check: https://github.com/${REPO}/releases/tag/${tag}`,
2779
3814
  success(`Dashboard ${tag} installed to ${destApp}`);
2780
3815
  }
2781
3816
  } finally {
2782
- await execa8("hdiutil", ["detach", mountPoint, "-quiet"]).catch(() => {
3817
+ await execa9("hdiutil", ["detach", mountPoint, "-quiet"]).catch(() => {
2783
3818
  });
2784
3819
  }
2785
3820
  await rm(tmp, { recursive: true, force: true });
@@ -2815,7 +3850,7 @@ program.command("init").description("Initialize Point Guard in the current git r
2815
3850
  const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2816
3851
  await initCommand2(options);
2817
3852
  });
2818
- program.command("spawn").description("Spawn a new worktree and agent(s), or add agents to an existing worktree").option("-n, --name <name>", "Name for the worktree/task").option("-a, --agent <type>", "Agent type to use (default: claude)").option("-p, --prompt <text>", "Prompt text for the agent").option("-f, --prompt-file <path>", "File containing the prompt").option("-t, --template <name>", "Template name from .pg/templates/").option("--var <key=value...>", "Template variables", collectVars, []).option("-b, --base <branch>", "Base branch for the worktree").option("-w, --worktree <id>", "Add agent to existing worktree").option("-c, --count <n>", "Number of agents to spawn", (v) => Number(v), 1).option("--split", "Put all agents in one window as split panes").option("--no-open", "Do not open a Terminal window for the spawned agents").option("--json", "Output as JSON").action(async (options) => {
3853
+ program.command("spawn").description("Spawn a new worktree and agent(s), or add agents to an existing worktree").option("-n, --name <name>", "Name for the worktree/task").option("-a, --agent <type>", "Agent type to use (default: claude)").option("-p, --prompt <text>", "Prompt text for the agent").option("-f, --prompt-file <path>", "File containing the prompt").option("-t, --template <name>", "Template name from .pg/templates/").option("--var <key=value...>", "Template variables", collectVars, []).option("-b, --base <branch>", "Base branch for the worktree").option("-w, --worktree <id>", "Add agent to existing worktree").option("-c, --count <n>", "Number of agents to spawn", (v) => Number(v), 1).option("--split", "Put all agents in one window as split panes").option("--open", "Open a Terminal window for the spawned agents").option("--json", "Output as JSON").action(async (options) => {
2819
3854
  const { spawnCommand: spawnCommand2 } = await Promise.resolve().then(() => (init_spawn(), spawn_exports));
2820
3855
  await spawnCommand2(options);
2821
3856
  });
@@ -2843,11 +3878,15 @@ program.command("merge").description("Merge a worktree branch back into base").a
2843
3878
  const { mergeCommand: mergeCommand2 } = await Promise.resolve().then(() => (init_merge(), merge_exports));
2844
3879
  await mergeCommand2(worktreeId2, options);
2845
3880
  });
2846
- program.command("list").description("List available templates").argument("<type>", "What to list: templates").option("--json", "Output as JSON").action(async (type, options) => {
3881
+ program.command("swarm").description("Run a swarm template \u2014 spawn multiple agents from a predefined workflow").argument("<template>", "Swarm template name from .pg/swarms/").option("-w, --worktree <ref>", "Target an existing worktree by ID, name, or branch").option("--var <key=value...>", "Template variables", collectVars, []).option("-n, --name <name>", "Override worktree name").option("-b, --base <branch>", "Base branch for new worktree(s)").option("--open", "Open Terminal windows for spawned agents").option("--json", "Output as JSON").action(async (template, options) => {
3882
+ const { swarmCommand: swarmCommand2 } = await Promise.resolve().then(() => (init_swarm2(), swarm_exports));
3883
+ await swarmCommand2(template, options);
3884
+ });
3885
+ program.command("list").description("List available templates or swarms").argument("<type>", "What to list: templates, swarms").option("--json", "Output as JSON").action(async (type, options) => {
2847
3886
  const { listCommand: listCommand2 } = await Promise.resolve().then(() => (init_list(), list_exports));
2848
3887
  await listCommand2(type, options);
2849
3888
  });
2850
- program.command("restart").description("Restart a failed/killed agent in the same worktree").argument("<agent-id>", "Agent ID to restart").option("-p, --prompt <text>", "Override the original prompt").option("-a, --agent <type>", "Override the agent type").option("--no-open", "Do not open a Terminal window").option("--json", "Output as JSON").action(async (agentId2, options) => {
3889
+ program.command("restart").description("Restart a failed/killed agent in the same worktree").argument("<agent-id>", "Agent ID to restart").option("-p, --prompt <text>", "Override the original prompt").option("-a, --agent <type>", "Override the agent type").option("--open", "Open a Terminal window for the restarted agent").option("--json", "Output as JSON").action(async (agentId2, options) => {
2851
3890
  const { restartCommand: restartCommand2 } = await Promise.resolve().then(() => (init_restart(), restart_exports));
2852
3891
  await restartCommand2(agentId2, options);
2853
3892
  });
@@ -2855,6 +3894,14 @@ program.command("diff").description("Show changes made in a worktree branch").ar
2855
3894
  const { diffCommand: diffCommand2 } = await Promise.resolve().then(() => (init_diff(), diff_exports));
2856
3895
  await diffCommand2(worktreeId2, options);
2857
3896
  });
3897
+ program.command("pr").description("Create a GitHub PR from a worktree branch").argument("<worktree-id>", "Worktree ID or name").option("--title <text>", "PR title (default: worktree name)").option("--body <text>", "PR body (default: agent result content)").option("--draft", "Create as draft PR").option("--json", "Output as JSON").action(async (worktreeId2, options) => {
3898
+ const { prCommand: prCommand2 } = await Promise.resolve().then(() => (init_pr(), pr_exports));
3899
+ await prCommand2(worktreeId2, options);
3900
+ });
3901
+ program.command("reset").description("Kill all agents, remove all worktrees, and wipe manifest").option("--force", "Reset even if worktrees have unmerged/un-PR'd work").option("--prune", "Also run git worktree prune").option("--json", "Output as JSON").action(async (options) => {
3902
+ const { resetCommand: resetCommand2 } = await Promise.resolve().then(() => (init_reset(), reset_exports));
3903
+ await resetCommand2(options);
3904
+ });
2858
3905
  program.command("clean").description("Remove worktrees in terminal states (merged/cleaned/failed)").option("--all", "Also clean failed worktrees").option("--dry-run", "Show what would be done without doing it").option("--prune", "Also run git worktree prune").option("--json", "Output as JSON").action(async (options) => {
2859
3906
  const { cleanCommand: cleanCommand2 } = await Promise.resolve().then(() => (init_clean(), clean_exports));
2860
3907
  await cleanCommand2(options);