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/CHANGELOG.md +35 -0
- package/README.md +2 -1
- package/dist/cli.js +1398 -351
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/skills/ppg-conductor/SKILL.md +3 -2
- package/skills/ppg-conductor/references/commands.md +135 -0
- package/skills/ppg-conductor/references/conductor.md +41 -27
- package/skills/ppg-conductor/references/modes.md +23 -8
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
|
|
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
|
-
|
|
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
|
|
473
|
-
if (!
|
|
474
|
-
const skillsDir = path2.join(
|
|
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
|
|
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
|
|
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
|
|
957
|
+
import { execa as execa3 } from "execa";
|
|
579
958
|
async function getRepoRoot(cwd) {
|
|
580
959
|
try {
|
|
581
|
-
const result = await
|
|
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
|
|
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
|
|
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
|
|
990
|
+
await execa3("git", args, { ...execaEnv, cwd: repoRoot });
|
|
610
991
|
if (options?.deleteBranch && options.branchName) {
|
|
611
992
|
try {
|
|
612
|
-
await
|
|
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
|
|
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
|
|
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
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
|
|
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
|
-
|
|
895
|
-
|
|
1070
|
+
function renderTemplate(content, context) {
|
|
1071
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
1072
|
+
return context[key] ?? `{{${key}}}`;
|
|
1073
|
+
});
|
|
896
1074
|
}
|
|
897
|
-
var
|
|
898
|
-
"src/core/
|
|
1075
|
+
var init_template = __esm({
|
|
1076
|
+
"src/core/template.ts"() {
|
|
899
1077
|
"use strict";
|
|
900
|
-
|
|
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 =
|
|
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
|
|
1050
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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/
|
|
1501
|
-
|
|
1502
|
-
|
|
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
|
-
|
|
1506
|
-
}));
|
|
1746
|
+
if (agent.tmuxTarget && wouldAffectSelf(agent.tmuxTarget, selfPaneId, paneMap)) {
|
|
1747
|
+
return true;
|
|
1507
1748
|
}
|
|
1508
1749
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
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.
|
|
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
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
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
|
|
2068
|
-
"src/commands/
|
|
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
|
-
|
|
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
|
|
2085
|
-
import
|
|
2797
|
+
import fs12 from "fs/promises";
|
|
2798
|
+
import path7 from "path";
|
|
2086
2799
|
async function listCommand(type, options) {
|
|
2087
|
-
if (type
|
|
2088
|
-
|
|
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 =
|
|
2099
|
-
const content = await
|
|
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
|
|
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 =
|
|
2914
|
+
const pFile = agentPromptFile(projectRoot, oldAgent.id);
|
|
2161
2915
|
try {
|
|
2162
|
-
promptText = await
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
2625
|
-
import { execa as
|
|
3659
|
+
import path8 from "path";
|
|
3660
|
+
import { execa as execa8 } from "execa";
|
|
2626
3661
|
async function findDashboardBinary(projectRoot) {
|
|
2627
|
-
const localBuild =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
3756
|
+
import path9 from "path";
|
|
2722
3757
|
import { pipeline } from "stream/promises";
|
|
2723
3758
|
import { Readable } from "stream";
|
|
2724
|
-
import { execa as
|
|
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 =
|
|
3785
|
+
const tmp = path9.join(tmpdir(), `ppg-dashboard-${Date.now()}`);
|
|
2751
3786
|
await mkdir(tmp, { recursive: true });
|
|
2752
|
-
const dmgPath =
|
|
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
|
|
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 =
|
|
2768
|
-
const destApp =
|
|
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
|
|
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
|
|
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("--
|
|
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("
|
|
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("--
|
|
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);
|