pure-point-guard 0.2.0 → 0.3.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 +17 -0
- package/README.md +134 -332
- package/dist/cli.js +1153 -349
- package/dist/cli.js.map +1 -1
- package/package.json +3 -2
- package/skills/ppg/SKILL.md +1 -1
- package/skills/ppg-conductor/SKILL.md +3 -2
- package/skills/ppg-conductor/references/commands.md +7 -7
- package/skills/ppg-conductor/references/conductor.md +15 -15
- package/skills/ppg-conductor/references/modes.md +6 -5
package/dist/cli.js
CHANGED
|
@@ -10,19 +10,19 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/lib/errors.ts
|
|
13
|
-
var
|
|
13
|
+
var PpgError, 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";
|
|
17
|
-
|
|
17
|
+
PpgError = class extends Error {
|
|
18
18
|
constructor(message, code, exitCode = 1) {
|
|
19
19
|
super(message);
|
|
20
20
|
this.code = code;
|
|
21
21
|
this.exitCode = exitCode;
|
|
22
|
-
this.name = "
|
|
22
|
+
this.name = "PpgError";
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
|
-
TmuxNotFoundError = class extends
|
|
25
|
+
TmuxNotFoundError = class extends PpgError {
|
|
26
26
|
constructor() {
|
|
27
27
|
super(
|
|
28
28
|
"tmux is not installed or not in PATH. Install it with: brew install tmux",
|
|
@@ -31,7 +31,7 @@ var init_errors = __esm({
|
|
|
31
31
|
this.name = "TmuxNotFoundError";
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
-
NotGitRepoError = class extends
|
|
34
|
+
NotGitRepoError = class extends PpgError {
|
|
35
35
|
constructor(dir) {
|
|
36
36
|
super(
|
|
37
37
|
`Not a git repository: ${dir}`,
|
|
@@ -40,7 +40,7 @@ var init_errors = __esm({
|
|
|
40
40
|
this.name = "NotGitRepoError";
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
|
-
NotInitializedError = class extends
|
|
43
|
+
NotInitializedError = class extends PpgError {
|
|
44
44
|
constructor(dir) {
|
|
45
45
|
super(
|
|
46
46
|
`Point Guard not initialized in ${dir}. Run 'ppg init' first.`,
|
|
@@ -49,7 +49,7 @@ var init_errors = __esm({
|
|
|
49
49
|
this.name = "NotInitializedError";
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
|
-
ManifestLockError = class extends
|
|
52
|
+
ManifestLockError = class extends PpgError {
|
|
53
53
|
constructor() {
|
|
54
54
|
super(
|
|
55
55
|
"Could not acquire manifest lock. Another ppg process may be running.",
|
|
@@ -58,7 +58,7 @@ var init_errors = __esm({
|
|
|
58
58
|
this.name = "ManifestLockError";
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
|
-
WorktreeNotFoundError = class extends
|
|
61
|
+
WorktreeNotFoundError = class extends PpgError {
|
|
62
62
|
constructor(id) {
|
|
63
63
|
super(
|
|
64
64
|
`Worktree not found: ${id}`,
|
|
@@ -67,7 +67,7 @@ var init_errors = __esm({
|
|
|
67
67
|
this.name = "WorktreeNotFoundError";
|
|
68
68
|
}
|
|
69
69
|
};
|
|
70
|
-
AgentNotFoundError = class extends
|
|
70
|
+
AgentNotFoundError = class extends PpgError {
|
|
71
71
|
constructor(id) {
|
|
72
72
|
super(
|
|
73
73
|
`Agent not found: ${id}`,
|
|
@@ -76,13 +76,13 @@ var init_errors = __esm({
|
|
|
76
76
|
this.name = "AgentNotFoundError";
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
|
-
MergeFailedError = class extends
|
|
79
|
+
MergeFailedError = class extends PpgError {
|
|
80
80
|
constructor(message) {
|
|
81
81
|
super(message, "MERGE_FAILED");
|
|
82
82
|
this.name = "MergeFailedError";
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
|
-
GhNotFoundError = class extends
|
|
85
|
+
GhNotFoundError = class extends PpgError {
|
|
86
86
|
constructor() {
|
|
87
87
|
super(
|
|
88
88
|
"GitHub CLI (gh) is not installed or not in PATH. Install it with: brew install gh",
|
|
@@ -91,7 +91,7 @@ var init_errors = __esm({
|
|
|
91
91
|
this.name = "GhNotFoundError";
|
|
92
92
|
}
|
|
93
93
|
};
|
|
94
|
-
UnmergedWorkError = class extends
|
|
94
|
+
UnmergedWorkError = class extends PpgError {
|
|
95
95
|
constructor(names) {
|
|
96
96
|
const list = names.map((n) => ` ${n}`).join("\n");
|
|
97
97
|
super(
|
|
@@ -115,8 +115,6 @@ function formatStatus(status) {
|
|
|
115
115
|
function output(data, json) {
|
|
116
116
|
if (json) {
|
|
117
117
|
console.log(JSON.stringify(data, null, 2));
|
|
118
|
-
} else if (typeof data === "string") {
|
|
119
|
-
console.log(data);
|
|
120
118
|
} else {
|
|
121
119
|
console.log(data);
|
|
122
120
|
}
|
|
@@ -196,54 +194,76 @@ var init_output = __esm({
|
|
|
196
194
|
});
|
|
197
195
|
|
|
198
196
|
// src/lib/paths.ts
|
|
197
|
+
import os from "os";
|
|
199
198
|
import path from "path";
|
|
200
|
-
function
|
|
201
|
-
return path.join(
|
|
199
|
+
function globalPpgDir() {
|
|
200
|
+
return path.join(os.homedir(), PPG_DIR);
|
|
201
|
+
}
|
|
202
|
+
function globalPromptsDir() {
|
|
203
|
+
return path.join(globalPpgDir(), "prompts");
|
|
204
|
+
}
|
|
205
|
+
function globalTemplatesDir() {
|
|
206
|
+
return path.join(globalPpgDir(), "templates");
|
|
207
|
+
}
|
|
208
|
+
function globalSwarmsDir() {
|
|
209
|
+
return path.join(globalPpgDir(), "swarms");
|
|
210
|
+
}
|
|
211
|
+
function ppgDir(projectRoot) {
|
|
212
|
+
return path.join(projectRoot, PPG_DIR);
|
|
202
213
|
}
|
|
203
214
|
function manifestPath(projectRoot) {
|
|
204
|
-
return path.join(
|
|
215
|
+
return path.join(ppgDir(projectRoot), "manifest.json");
|
|
205
216
|
}
|
|
206
217
|
function configPath(projectRoot) {
|
|
207
|
-
return path.join(
|
|
218
|
+
return path.join(ppgDir(projectRoot), "config.yaml");
|
|
208
219
|
}
|
|
209
220
|
function resultsDir(projectRoot) {
|
|
210
|
-
return path.join(
|
|
221
|
+
return path.join(ppgDir(projectRoot), "results");
|
|
211
222
|
}
|
|
212
223
|
function resultFile(projectRoot, agentId2) {
|
|
213
224
|
return path.join(resultsDir(projectRoot), `${agentId2}.md`);
|
|
214
225
|
}
|
|
215
226
|
function templatesDir(projectRoot) {
|
|
216
|
-
return path.join(
|
|
227
|
+
return path.join(ppgDir(projectRoot), "templates");
|
|
217
228
|
}
|
|
218
229
|
function logsDir(projectRoot) {
|
|
219
|
-
return path.join(
|
|
230
|
+
return path.join(ppgDir(projectRoot), "logs");
|
|
220
231
|
}
|
|
221
232
|
function promptsDir(projectRoot) {
|
|
222
|
-
return path.join(
|
|
233
|
+
return path.join(ppgDir(projectRoot), "prompts");
|
|
223
234
|
}
|
|
224
235
|
function swarmsDir(projectRoot) {
|
|
225
|
-
return path.join(
|
|
236
|
+
return path.join(ppgDir(projectRoot), "swarms");
|
|
226
237
|
}
|
|
227
238
|
function promptFile(projectRoot, agentId2) {
|
|
228
239
|
return path.join(promptsDir(projectRoot), `${agentId2}.md`);
|
|
229
240
|
}
|
|
230
241
|
function agentPromptsDir(projectRoot) {
|
|
231
|
-
return path.join(
|
|
242
|
+
return path.join(ppgDir(projectRoot), "agent-prompts");
|
|
232
243
|
}
|
|
233
244
|
function agentPromptFile(projectRoot, agentId2) {
|
|
234
245
|
return path.join(agentPromptsDir(projectRoot), `${agentId2}.md`);
|
|
235
246
|
}
|
|
247
|
+
function schedulesPath(projectRoot) {
|
|
248
|
+
return path.join(ppgDir(projectRoot), "schedules.yaml");
|
|
249
|
+
}
|
|
250
|
+
function cronLogPath(projectRoot) {
|
|
251
|
+
return path.join(logsDir(projectRoot), "cron.log");
|
|
252
|
+
}
|
|
253
|
+
function cronPidPath(projectRoot) {
|
|
254
|
+
return path.join(ppgDir(projectRoot), "cron.pid");
|
|
255
|
+
}
|
|
236
256
|
function worktreeBaseDir(projectRoot) {
|
|
237
257
|
return path.join(projectRoot, ".worktrees");
|
|
238
258
|
}
|
|
239
259
|
function worktreePath(projectRoot, id) {
|
|
240
260
|
return path.join(worktreeBaseDir(projectRoot), id);
|
|
241
261
|
}
|
|
242
|
-
var
|
|
262
|
+
var PPG_DIR;
|
|
243
263
|
var init_paths = __esm({
|
|
244
264
|
"src/lib/paths.ts"() {
|
|
245
265
|
"use strict";
|
|
246
|
-
|
|
266
|
+
PPG_DIR = ".ppg";
|
|
247
267
|
}
|
|
248
268
|
});
|
|
249
269
|
|
|
@@ -264,13 +284,20 @@ async function loadConfig(projectRoot) {
|
|
|
264
284
|
}
|
|
265
285
|
}
|
|
266
286
|
function mergeConfig(defaults, overrides) {
|
|
287
|
+
const mergedAgents = { ...defaults.agents };
|
|
288
|
+
if (overrides.agents) {
|
|
289
|
+
for (const [key, override] of Object.entries(overrides.agents)) {
|
|
290
|
+
if (mergedAgents[key]) {
|
|
291
|
+
mergedAgents[key] = { ...mergedAgents[key], ...override };
|
|
292
|
+
} else {
|
|
293
|
+
mergedAgents[key] = override;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
267
297
|
return {
|
|
268
298
|
...defaults,
|
|
269
299
|
...overrides,
|
|
270
|
-
agents:
|
|
271
|
-
...defaults.agents,
|
|
272
|
-
...overrides.agents ?? {}
|
|
273
|
-
}
|
|
300
|
+
agents: mergedAgents
|
|
274
301
|
};
|
|
275
302
|
}
|
|
276
303
|
async function writeDefaultConfig(projectRoot) {
|
|
@@ -300,12 +327,18 @@ var init_config = __esm({
|
|
|
300
327
|
command: "claude --dangerously-skip-permissions",
|
|
301
328
|
interactive: true,
|
|
302
329
|
resultInstructions: [
|
|
303
|
-
"When you have completed the task
|
|
304
|
-
"
|
|
305
|
-
"
|
|
330
|
+
"When you have completed the task:",
|
|
331
|
+
"",
|
|
332
|
+
"1. Stage and commit all your changes with a descriptive commit message",
|
|
333
|
+
"2. Push your branch: git push -u origin {{BRANCH}}",
|
|
334
|
+
"3. Create a pull request: gh pr create --head {{BRANCH}} --base main --fill",
|
|
335
|
+
"4. Write your results to {{RESULT_FILE}} in this format:",
|
|
306
336
|
"",
|
|
307
337
|
"# Result: {{AGENT_ID}}",
|
|
308
338
|
"",
|
|
339
|
+
"## PR",
|
|
340
|
+
"<the PR URL from step 3>",
|
|
341
|
+
"",
|
|
309
342
|
"## Summary",
|
|
310
343
|
"<what you accomplished>",
|
|
311
344
|
"",
|
|
@@ -317,10 +350,6 @@ var init_config = __esm({
|
|
|
317
350
|
].join("\n")
|
|
318
351
|
}
|
|
319
352
|
},
|
|
320
|
-
worktreeBase: ".worktrees",
|
|
321
|
-
templateDir: ".pg/templates",
|
|
322
|
-
resultDir: ".pg/results",
|
|
323
|
-
logDir: ".pg/logs",
|
|
324
353
|
envFiles: [".env", ".env.local"],
|
|
325
354
|
symlinkNodeModules: true
|
|
326
355
|
};
|
|
@@ -367,6 +396,16 @@ async function readManifest(projectRoot) {
|
|
|
367
396
|
const raw = await fs2.readFile(mPath, "utf-8");
|
|
368
397
|
return JSON.parse(raw);
|
|
369
398
|
}
|
|
399
|
+
async function requireManifest(projectRoot) {
|
|
400
|
+
try {
|
|
401
|
+
return await readManifest(projectRoot);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
if (err.code === "ENOENT") {
|
|
404
|
+
throw new NotInitializedError(projectRoot);
|
|
405
|
+
}
|
|
406
|
+
throw err;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
370
409
|
async function writeManifest(projectRoot, manifest) {
|
|
371
410
|
const writeFileAtomic = await getWriteFileAtomic();
|
|
372
411
|
const mPath = manifestPath(projectRoot);
|
|
@@ -537,18 +576,17 @@ var init_env = __esm({
|
|
|
537
576
|
// src/core/tmux.ts
|
|
538
577
|
var tmux_exports = {};
|
|
539
578
|
__export(tmux_exports, {
|
|
540
|
-
attachSession: () => attachSession,
|
|
541
579
|
capturePane: () => capturePane,
|
|
542
580
|
checkTmux: () => checkTmux,
|
|
543
581
|
createWindow: () => createWindow,
|
|
544
582
|
ensureSession: () => ensureSession,
|
|
545
583
|
getPaneInfo: () => getPaneInfo,
|
|
546
584
|
isInsideTmux: () => isInsideTmux,
|
|
585
|
+
killOrphanWindows: () => killOrphanWindows,
|
|
547
586
|
killPane: () => killPane,
|
|
548
587
|
killWindow: () => killWindow,
|
|
549
|
-
listPanes: () => listPanes,
|
|
550
588
|
listSessionPanes: () => listSessionPanes,
|
|
551
|
-
|
|
589
|
+
listSessionWindows: () => listSessionWindows,
|
|
552
590
|
selectWindow: () => selectWindow,
|
|
553
591
|
sendCtrlC: () => sendCtrlC,
|
|
554
592
|
sendKeys: () => sendKeys,
|
|
@@ -651,29 +689,6 @@ function isTmuxNotFoundError(err) {
|
|
|
651
689
|
const msg = String(err.stderr ?? "").toLowerCase();
|
|
652
690
|
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
691
|
}
|
|
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
692
|
async function getPaneInfo(target) {
|
|
678
693
|
try {
|
|
679
694
|
const result = await execa("tmux", [
|
|
@@ -726,15 +741,61 @@ async function listSessionPanes(session) {
|
|
|
726
741
|
}
|
|
727
742
|
return map;
|
|
728
743
|
}
|
|
729
|
-
async function
|
|
730
|
-
|
|
744
|
+
async function listSessionWindows(session) {
|
|
745
|
+
try {
|
|
746
|
+
const result = await execa("tmux", [
|
|
747
|
+
"list-windows",
|
|
748
|
+
"-t",
|
|
749
|
+
`=${session}`,
|
|
750
|
+
"-F",
|
|
751
|
+
"#{window_index} #{window_name}"
|
|
752
|
+
], execaEnv);
|
|
753
|
+
return result.stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
754
|
+
const spaceIdx = line.indexOf(" ");
|
|
755
|
+
return {
|
|
756
|
+
index: parseInt(line.slice(0, spaceIdx), 10),
|
|
757
|
+
name: line.slice(spaceIdx + 1)
|
|
758
|
+
};
|
|
759
|
+
});
|
|
760
|
+
} catch {
|
|
761
|
+
return [];
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
async function killOrphanWindows(session, selfPaneId) {
|
|
765
|
+
const windows = await listSessionWindows(session);
|
|
766
|
+
let killed = 0;
|
|
767
|
+
let paneMap;
|
|
768
|
+
if (selfPaneId) {
|
|
769
|
+
paneMap = await listSessionPanes(session);
|
|
770
|
+
}
|
|
771
|
+
for (const win of windows) {
|
|
772
|
+
if (win.index === 0) continue;
|
|
773
|
+
if (selfPaneId && paneMap) {
|
|
774
|
+
const windowTarget = `${session}:${win.index}`;
|
|
775
|
+
let containsSelf = false;
|
|
776
|
+
for (const [key, info2] of paneMap) {
|
|
777
|
+
if (key.startsWith(windowTarget + ".") && info2.paneId === selfPaneId) {
|
|
778
|
+
containsSelf = true;
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
if (key === windowTarget && info2.paneId === selfPaneId) {
|
|
782
|
+
containsSelf = true;
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (containsSelf) continue;
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
await killWindow(`${session}:${win.index}`);
|
|
790
|
+
killed++;
|
|
791
|
+
} catch {
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return killed;
|
|
731
795
|
}
|
|
732
796
|
async function selectWindow(target) {
|
|
733
797
|
await execa("tmux", ["select-window", "-t", target], execaEnv);
|
|
734
798
|
}
|
|
735
|
-
async function attachSession(session) {
|
|
736
|
-
await execa("tmux", ["attach-session", "-t", `=${session}`], { ...execaEnv, stdio: "inherit" });
|
|
737
|
-
}
|
|
738
799
|
async function isInsideTmux() {
|
|
739
800
|
return !!process.env.TMUX;
|
|
740
801
|
}
|
|
@@ -769,7 +830,7 @@ async function initCommand(options) {
|
|
|
769
830
|
}
|
|
770
831
|
await checkTmux();
|
|
771
832
|
const dirs = [
|
|
772
|
-
|
|
833
|
+
ppgDir(projectRoot),
|
|
773
834
|
resultsDir(projectRoot),
|
|
774
835
|
logsDir(projectRoot),
|
|
775
836
|
templatesDir(projectRoot),
|
|
@@ -780,9 +841,15 @@ async function initCommand(options) {
|
|
|
780
841
|
for (const dir of dirs) {
|
|
781
842
|
await fs3.mkdir(dir, { recursive: true });
|
|
782
843
|
}
|
|
783
|
-
info("Created .
|
|
784
|
-
|
|
785
|
-
|
|
844
|
+
info("Created .ppg/ directory structure");
|
|
845
|
+
const cfgPath = configPath(projectRoot);
|
|
846
|
+
try {
|
|
847
|
+
await fs3.access(cfgPath);
|
|
848
|
+
info("config.yaml already exists, skipping");
|
|
849
|
+
} catch {
|
|
850
|
+
await writeDefaultConfig(projectRoot);
|
|
851
|
+
info("Wrote default config.yaml");
|
|
852
|
+
}
|
|
786
853
|
const dirName = path2.basename(projectRoot);
|
|
787
854
|
const sessionName = `ppg-${dirName}`;
|
|
788
855
|
const manifest = createEmptyManifest(projectRoot, sessionName);
|
|
@@ -815,7 +882,7 @@ async function initCommand(options) {
|
|
|
815
882
|
info(`Wrote swarm template: ${name}.yaml`);
|
|
816
883
|
}
|
|
817
884
|
}
|
|
818
|
-
const conductorPath = path2.join(
|
|
885
|
+
const conductorPath = path2.join(ppgDir(projectRoot), "conductor-context.md");
|
|
819
886
|
await fs3.writeFile(conductorPath, CONDUCTOR_CONTEXT, "utf-8");
|
|
820
887
|
info("Wrote conductor-context.md");
|
|
821
888
|
const pluginRegistered = await registerClaudePlugin();
|
|
@@ -827,7 +894,7 @@ async function initCommand(options) {
|
|
|
827
894
|
success: true,
|
|
828
895
|
projectRoot,
|
|
829
896
|
sessionName,
|
|
830
|
-
|
|
897
|
+
ppgDir: ppgDir(projectRoot),
|
|
831
898
|
pluginRegistered
|
|
832
899
|
}));
|
|
833
900
|
} else {
|
|
@@ -873,13 +940,13 @@ async function registerClaudePlugin() {
|
|
|
873
940
|
async function updateGitignore(projectRoot) {
|
|
874
941
|
const gitignorePath = path2.join(projectRoot, ".gitignore");
|
|
875
942
|
const entriesToAdd = [
|
|
876
|
-
".
|
|
877
|
-
".
|
|
878
|
-
".
|
|
879
|
-
".
|
|
880
|
-
".
|
|
881
|
-
".
|
|
882
|
-
".
|
|
943
|
+
".ppg/results/",
|
|
944
|
+
".ppg/logs/",
|
|
945
|
+
".ppg/manifest.json",
|
|
946
|
+
".ppg/prompts/",
|
|
947
|
+
".ppg/agent-prompts/",
|
|
948
|
+
".ppg/swarms/",
|
|
949
|
+
".ppg/conductor-context.md"
|
|
883
950
|
];
|
|
884
951
|
let content = "";
|
|
885
952
|
try {
|
|
@@ -914,27 +981,24 @@ You are operating on the master branch of a ppg-managed project.
|
|
|
914
981
|
**NEVER make code changes directly on the master branch.** Use \`ppg spawn\` to create worktrees.
|
|
915
982
|
|
|
916
983
|
## Quick Reference
|
|
917
|
-
- \`ppg spawn --name <name> --prompt "<task>" --json
|
|
984
|
+
- \`ppg spawn --name <name> --prompt "<task>" --json\` \u2014 Spawn worktree + agent
|
|
918
985
|
- \`ppg status --json\` \u2014 Check statuses
|
|
919
|
-
- \`ppg aggregate --all --json\` \u2014 Collect results
|
|
920
|
-
- \`ppg pr <wt-id> --json\` \u2014 Create PR from worktree branch
|
|
986
|
+
- \`ppg aggregate --all --json\` \u2014 Collect results (includes PR URLs)
|
|
921
987
|
- \`ppg kill --agent <id> --json\` \u2014 Kill agent
|
|
922
|
-
- \`ppg reset --json\` \u2014 Clean up all worktrees
|
|
988
|
+
- \`ppg reset --json\` \u2014 Clean up all worktrees (skips worktrees with open PRs)
|
|
923
989
|
|
|
924
990
|
## Workflow
|
|
925
991
|
1. Break request into parallelizable tasks
|
|
926
|
-
2. Spawn
|
|
992
|
+
2. Spawn: \`ppg spawn --name <name> --prompt "<prompt>" --json\`
|
|
927
993
|
3. Poll: \`ppg status --json\` every 5s
|
|
928
|
-
4. Aggregate: \`ppg aggregate --all --json\`
|
|
929
|
-
5. Present
|
|
930
|
-
6.
|
|
931
|
-
7.
|
|
932
|
-
8. Cleanup
|
|
933
|
-
|
|
934
|
-
**Stop at step 5** \u2014 do not auto-merge or auto-PR. Let the user decide.
|
|
994
|
+
4. Aggregate: \`ppg aggregate --all --json\` \u2014 result files include PR URLs
|
|
995
|
+
5. Present PR links and summaries \u2014 let user decide next steps
|
|
996
|
+
6. To merge remotely: \`gh pr merge <url> --squash --delete-branch\`
|
|
997
|
+
7. To merge locally (power-user): \`ppg merge <wt-id> --json\`
|
|
998
|
+
8. Cleanup: \`ppg reset --json\` (skips worktrees with open PRs)
|
|
935
999
|
|
|
936
1000
|
Each agent prompt must be self-contained \u2014 agents have no memory of this conversation.
|
|
937
|
-
Always use \`--json
|
|
1001
|
+
Always use \`--json\`.
|
|
938
1002
|
`;
|
|
939
1003
|
DEFAULT_TEMPLATE = `# Task: {{TASK_NAME}}
|
|
940
1004
|
|
|
@@ -1053,8 +1117,7 @@ var init_env2 = __esm({
|
|
|
1053
1117
|
// src/core/template.ts
|
|
1054
1118
|
import fs5 from "fs/promises";
|
|
1055
1119
|
import path4 from "path";
|
|
1056
|
-
async function
|
|
1057
|
-
const dir = templatesDir(projectRoot);
|
|
1120
|
+
async function readMdNames(dir) {
|
|
1058
1121
|
try {
|
|
1059
1122
|
const files = await fs5.readdir(dir);
|
|
1060
1123
|
return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
|
|
@@ -1062,10 +1125,30 @@ async function listTemplates(projectRoot) {
|
|
|
1062
1125
|
return [];
|
|
1063
1126
|
}
|
|
1064
1127
|
}
|
|
1128
|
+
async function listTemplatesWithSource(projectRoot) {
|
|
1129
|
+
const localNames = await readMdNames(templatesDir(projectRoot));
|
|
1130
|
+
const globalNames = await readMdNames(globalTemplatesDir());
|
|
1131
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1132
|
+
const result = [];
|
|
1133
|
+
for (const name of localNames) {
|
|
1134
|
+
seen.add(name);
|
|
1135
|
+
result.push({ name, source: "local" });
|
|
1136
|
+
}
|
|
1137
|
+
for (const name of globalNames) {
|
|
1138
|
+
if (!seen.has(name)) {
|
|
1139
|
+
result.push({ name, source: "global" });
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return result;
|
|
1143
|
+
}
|
|
1065
1144
|
async function loadTemplate(projectRoot, name) {
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1145
|
+
const localPath = path4.join(templatesDir(projectRoot), `${name}.md`);
|
|
1146
|
+
try {
|
|
1147
|
+
return await fs5.readFile(localPath, "utf-8");
|
|
1148
|
+
} catch {
|
|
1149
|
+
const globalPath = path4.join(globalTemplatesDir(), `${name}.md`);
|
|
1150
|
+
return fs5.readFile(globalPath, "utf-8");
|
|
1151
|
+
}
|
|
1069
1152
|
}
|
|
1070
1153
|
function renderTemplate(content, context) {
|
|
1071
1154
|
return content.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
@@ -1093,7 +1176,7 @@ async function spawnAgent(options) {
|
|
|
1093
1176
|
} = options;
|
|
1094
1177
|
const resFile = resultFile(projectRoot, agentId2);
|
|
1095
1178
|
let fullPrompt = prompt;
|
|
1096
|
-
if (agentConfig.resultInstructions) {
|
|
1179
|
+
if (agentConfig.resultInstructions && !options.skipResultInstructions) {
|
|
1097
1180
|
const ctx = {
|
|
1098
1181
|
WORKTREE_PATH: worktreePath2,
|
|
1099
1182
|
BRANCH: branch,
|
|
@@ -1130,10 +1213,11 @@ function buildAgentCommand(agentConfig, promptFilePath, sessionId2) {
|
|
|
1130
1213
|
const envPrefix = "unset CLAUDECODE;";
|
|
1131
1214
|
const { command, promptFlag } = agentConfig;
|
|
1132
1215
|
const sessionFlag = sessionId2 && command.includes("claude") ? ` --session-id ${sessionId2}` : "";
|
|
1216
|
+
const catExpr = `"$(cat '${promptFilePath}')"`;
|
|
1133
1217
|
if (promptFlag) {
|
|
1134
|
-
return `${envPrefix} ${command}${sessionFlag} ${promptFlag}
|
|
1218
|
+
return `${envPrefix} ${command}${sessionFlag} ${promptFlag} ${catExpr}`;
|
|
1135
1219
|
}
|
|
1136
|
-
return `${envPrefix} ${command}${sessionFlag}
|
|
1220
|
+
return `${envPrefix} ${command}${sessionFlag} ${catExpr}`;
|
|
1137
1221
|
}
|
|
1138
1222
|
async function checkAgentStatus(agent, projectRoot, paneMap) {
|
|
1139
1223
|
if (["completed", "failed", "killed", "lost"].includes(agent.status)) {
|
|
@@ -1206,7 +1290,7 @@ async function refreshAllAgentStatuses(manifest, projectRoot) {
|
|
|
1206
1290
|
async function resumeAgent(options) {
|
|
1207
1291
|
const { agent, worktreeId: worktreeId2, sessionName, cwd, windowName, projectRoot } = options;
|
|
1208
1292
|
if (!agent.sessionId) {
|
|
1209
|
-
throw new
|
|
1293
|
+
throw new PpgError(
|
|
1210
1294
|
`Agent ${agent.id} has no session ID. Cannot resume agents spawned before session tracking was added.`,
|
|
1211
1295
|
"NO_SESSION_ID"
|
|
1212
1296
|
);
|
|
@@ -1278,15 +1362,28 @@ var init_agent = __esm({
|
|
|
1278
1362
|
}
|
|
1279
1363
|
});
|
|
1280
1364
|
|
|
1365
|
+
// src/lib/shell.ts
|
|
1366
|
+
function shellEscape(text) {
|
|
1367
|
+
return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$").replace(/`/g, "\\`");
|
|
1368
|
+
}
|
|
1369
|
+
var init_shell = __esm({
|
|
1370
|
+
"src/lib/shell.ts"() {
|
|
1371
|
+
"use strict";
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1281
1375
|
// src/core/terminal.ts
|
|
1282
1376
|
import { execa as execa4 } from "execa";
|
|
1283
1377
|
async function openTerminalWindow(sessionName, windowTarget, title) {
|
|
1284
|
-
const
|
|
1378
|
+
const safeSession = shellEscape(sessionName);
|
|
1379
|
+
const safeWindow = shellEscape(windowTarget);
|
|
1380
|
+
const safeTitle = shellEscape(title);
|
|
1381
|
+
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 ${safeSession} \\\\; select-window -t ${safeWindow}`;
|
|
1285
1382
|
const script = `
|
|
1286
1383
|
tell application "Terminal"
|
|
1287
1384
|
activate
|
|
1288
1385
|
set newTab to do script "${tmuxCmd}"
|
|
1289
|
-
set custom title of newTab to "${
|
|
1386
|
+
set custom title of newTab to "${safeTitle}"
|
|
1290
1387
|
end tell
|
|
1291
1388
|
`;
|
|
1292
1389
|
try {
|
|
@@ -1299,6 +1396,7 @@ var init_terminal = __esm({
|
|
|
1299
1396
|
"src/core/terminal.ts"() {
|
|
1300
1397
|
"use strict";
|
|
1301
1398
|
init_output();
|
|
1399
|
+
init_shell();
|
|
1302
1400
|
}
|
|
1303
1401
|
});
|
|
1304
1402
|
|
|
@@ -1343,6 +1441,25 @@ var init_name = __esm({
|
|
|
1343
1441
|
}
|
|
1344
1442
|
});
|
|
1345
1443
|
|
|
1444
|
+
// src/lib/vars.ts
|
|
1445
|
+
function parseVars(vars) {
|
|
1446
|
+
const result = {};
|
|
1447
|
+
for (const v of vars) {
|
|
1448
|
+
const eqIdx = v.indexOf("=");
|
|
1449
|
+
if (eqIdx < 1) {
|
|
1450
|
+
throw new PpgError(`Invalid --var format: "${v}" \u2014 expected KEY=value`, "INVALID_ARGS");
|
|
1451
|
+
}
|
|
1452
|
+
result[v.slice(0, eqIdx)] = v.slice(eqIdx + 1);
|
|
1453
|
+
}
|
|
1454
|
+
return result;
|
|
1455
|
+
}
|
|
1456
|
+
var init_vars = __esm({
|
|
1457
|
+
"src/lib/vars.ts"() {
|
|
1458
|
+
"use strict";
|
|
1459
|
+
init_errors();
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1346
1463
|
// src/commands/spawn.ts
|
|
1347
1464
|
var spawn_exports = {};
|
|
1348
1465
|
__export(spawn_exports, {
|
|
@@ -1359,6 +1476,7 @@ async function spawnCommand(options) {
|
|
|
1359
1476
|
}
|
|
1360
1477
|
const agentConfig = resolveAgentConfig(config, options.agent);
|
|
1361
1478
|
const count = options.count ?? 1;
|
|
1479
|
+
const userVars = parseVars(options.var ?? []);
|
|
1362
1480
|
const promptText = await resolvePrompt(options, projectRoot);
|
|
1363
1481
|
if (options.worktree) {
|
|
1364
1482
|
await spawnIntoExistingWorktree(
|
|
@@ -1368,7 +1486,8 @@ async function spawnCommand(options) {
|
|
|
1368
1486
|
options.worktree,
|
|
1369
1487
|
promptText,
|
|
1370
1488
|
count,
|
|
1371
|
-
options
|
|
1489
|
+
options,
|
|
1490
|
+
userVars
|
|
1372
1491
|
);
|
|
1373
1492
|
} else {
|
|
1374
1493
|
await spawnNewWorktree(
|
|
@@ -1377,7 +1496,8 @@ async function spawnCommand(options) {
|
|
|
1377
1496
|
agentConfig,
|
|
1378
1497
|
promptText,
|
|
1379
1498
|
count,
|
|
1380
|
-
options
|
|
1499
|
+
options,
|
|
1500
|
+
userVars
|
|
1381
1501
|
);
|
|
1382
1502
|
}
|
|
1383
1503
|
}
|
|
@@ -1387,17 +1507,11 @@ async function resolvePrompt(options, projectRoot) {
|
|
|
1387
1507
|
return fs7.readFile(options.promptFile, "utf-8");
|
|
1388
1508
|
}
|
|
1389
1509
|
if (options.template) {
|
|
1390
|
-
|
|
1391
|
-
const vars = {};
|
|
1392
|
-
for (const v of options.var ?? []) {
|
|
1393
|
-
const [key, ...rest] = v.split("=");
|
|
1394
|
-
vars[key] = rest.join("=");
|
|
1395
|
-
}
|
|
1396
|
-
return templateContent;
|
|
1510
|
+
return loadTemplate(projectRoot, options.template);
|
|
1397
1511
|
}
|
|
1398
|
-
throw new
|
|
1512
|
+
throw new PpgError("One of --prompt, --prompt-file, or --template is required", "INVALID_ARGS");
|
|
1399
1513
|
}
|
|
1400
|
-
async function spawnNewWorktree(projectRoot, config, agentConfig, promptText, count, options) {
|
|
1514
|
+
async function spawnNewWorktree(projectRoot, config, agentConfig, promptText, count, options, userVars) {
|
|
1401
1515
|
const baseBranch = options.base ?? await getCurrentBranch(projectRoot);
|
|
1402
1516
|
const wtId = worktreeId();
|
|
1403
1517
|
const name = options.name ? normalizeName(options.name, wtId) : wtId;
|
|
@@ -1408,9 +1522,25 @@ async function spawnNewWorktree(projectRoot, config, agentConfig, promptText, co
|
|
|
1408
1522
|
base: baseBranch
|
|
1409
1523
|
});
|
|
1410
1524
|
await setupWorktreeEnv(projectRoot, wtPath, config);
|
|
1411
|
-
const
|
|
1525
|
+
const manifest = await readManifest(projectRoot);
|
|
1526
|
+
const sessionName = manifest.sessionName;
|
|
1412
1527
|
await ensureSession(sessionName);
|
|
1413
1528
|
const windowTarget = await createWindow(sessionName, name, wtPath);
|
|
1529
|
+
const worktreeEntry = {
|
|
1530
|
+
id: wtId,
|
|
1531
|
+
name,
|
|
1532
|
+
path: wtPath,
|
|
1533
|
+
branch: branchName,
|
|
1534
|
+
baseBranch,
|
|
1535
|
+
status: "active",
|
|
1536
|
+
tmuxWindow: windowTarget,
|
|
1537
|
+
agents: {},
|
|
1538
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1539
|
+
};
|
|
1540
|
+
await updateManifest(projectRoot, (m) => {
|
|
1541
|
+
m.worktrees[wtId] = worktreeEntry;
|
|
1542
|
+
return m;
|
|
1543
|
+
});
|
|
1414
1544
|
const agents = [];
|
|
1415
1545
|
for (let i = 0; i < count; i++) {
|
|
1416
1546
|
const aId = agentId();
|
|
@@ -1433,10 +1563,7 @@ async function spawnNewWorktree(projectRoot, config, agentConfig, promptText, co
|
|
|
1433
1563
|
TASK_NAME: name,
|
|
1434
1564
|
PROMPT: promptText
|
|
1435
1565
|
};
|
|
1436
|
-
|
|
1437
|
-
const [key, ...rest] = v.split("=");
|
|
1438
|
-
ctx[key] = rest.join("=");
|
|
1439
|
-
}
|
|
1566
|
+
Object.assign(ctx, userVars);
|
|
1440
1567
|
const renderedPrompt = renderTemplate(promptText, ctx);
|
|
1441
1568
|
const agentEntry = await spawnAgent({
|
|
1442
1569
|
agentId: aId,
|
|
@@ -1449,22 +1576,13 @@ async function spawnNewWorktree(projectRoot, config, agentConfig, promptText, co
|
|
|
1449
1576
|
sessionId: sessionId()
|
|
1450
1577
|
});
|
|
1451
1578
|
agents.push(agentEntry);
|
|
1579
|
+
await updateManifest(projectRoot, (m) => {
|
|
1580
|
+
if (m.worktrees[wtId]) {
|
|
1581
|
+
m.worktrees[wtId].agents[agentEntry.id] = agentEntry;
|
|
1582
|
+
}
|
|
1583
|
+
return m;
|
|
1584
|
+
});
|
|
1452
1585
|
}
|
|
1453
|
-
const worktreeEntry = {
|
|
1454
|
-
id: wtId,
|
|
1455
|
-
name,
|
|
1456
|
-
path: wtPath,
|
|
1457
|
-
branch: branchName,
|
|
1458
|
-
baseBranch,
|
|
1459
|
-
status: "active",
|
|
1460
|
-
tmuxWindow: windowTarget,
|
|
1461
|
-
agents: Object.fromEntries(agents.map((a) => [a.id, a])),
|
|
1462
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1463
|
-
};
|
|
1464
|
-
await updateManifest(projectRoot, (m) => {
|
|
1465
|
-
m.worktrees[wtId] = worktreeEntry;
|
|
1466
|
-
return m;
|
|
1467
|
-
});
|
|
1468
1586
|
if (options.open === true) {
|
|
1469
1587
|
openTerminalWindow(sessionName, windowTarget, name).catch(() => {
|
|
1470
1588
|
});
|
|
@@ -1493,7 +1611,7 @@ async function spawnNewWorktree(projectRoot, config, agentConfig, promptText, co
|
|
|
1493
1611
|
info(`Attach: ppg attach ${wtId}`);
|
|
1494
1612
|
}
|
|
1495
1613
|
}
|
|
1496
|
-
async function spawnIntoExistingWorktree(projectRoot, config, agentConfig, worktreeRef, promptText, count, options) {
|
|
1614
|
+
async function spawnIntoExistingWorktree(projectRoot, config, agentConfig, worktreeRef, promptText, count, options, userVars) {
|
|
1497
1615
|
const manifest = await readManifest(projectRoot);
|
|
1498
1616
|
const wt = resolveWorktree(manifest, worktreeRef);
|
|
1499
1617
|
if (!wt) throw new WorktreeNotFoundError(worktreeRef);
|
|
@@ -1524,10 +1642,7 @@ async function spawnIntoExistingWorktree(projectRoot, config, agentConfig, workt
|
|
|
1524
1642
|
TASK_NAME: wt.name,
|
|
1525
1643
|
PROMPT: promptText
|
|
1526
1644
|
};
|
|
1527
|
-
|
|
1528
|
-
const [key, ...rest] = v.split("=");
|
|
1529
|
-
ctx[key] = rest.join("=");
|
|
1530
|
-
}
|
|
1645
|
+
Object.assign(ctx, userVars);
|
|
1531
1646
|
const renderedPrompt = renderTemplate(promptText, ctx);
|
|
1532
1647
|
const agentEntry = await spawnAgent({
|
|
1533
1648
|
agentId: aId,
|
|
@@ -1543,6 +1658,7 @@ async function spawnIntoExistingWorktree(projectRoot, config, agentConfig, workt
|
|
|
1543
1658
|
}
|
|
1544
1659
|
await updateManifest(projectRoot, (m) => {
|
|
1545
1660
|
const mWt = m.worktrees[wt.id];
|
|
1661
|
+
if (!mWt) return m;
|
|
1546
1662
|
if (!mWt.tmuxWindow) {
|
|
1547
1663
|
mWt.tmuxWindow = windowTarget;
|
|
1548
1664
|
}
|
|
@@ -1590,6 +1706,7 @@ var init_spawn = __esm({
|
|
|
1590
1706
|
init_errors();
|
|
1591
1707
|
init_output();
|
|
1592
1708
|
init_name();
|
|
1709
|
+
init_vars();
|
|
1593
1710
|
}
|
|
1594
1711
|
});
|
|
1595
1712
|
|
|
@@ -1600,14 +1717,10 @@ __export(status_exports, {
|
|
|
1600
1717
|
});
|
|
1601
1718
|
async function statusCommand(worktreeFilter, options) {
|
|
1602
1719
|
const projectRoot = await getRepoRoot();
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
});
|
|
1608
|
-
} catch {
|
|
1609
|
-
throw new NotInitializedError(projectRoot);
|
|
1610
|
-
}
|
|
1720
|
+
await requireManifest(projectRoot);
|
|
1721
|
+
const manifest = await updateManifest(projectRoot, async (m) => {
|
|
1722
|
+
return refreshAllAgentStatuses(m, projectRoot);
|
|
1723
|
+
});
|
|
1611
1724
|
const filter = worktreeFilter ?? options?.worktree;
|
|
1612
1725
|
let worktrees = Object.values(manifest.worktrees);
|
|
1613
1726
|
if (filter) {
|
|
@@ -1703,11 +1816,35 @@ var init_status = __esm({
|
|
|
1703
1816
|
init_manifest();
|
|
1704
1817
|
init_agent();
|
|
1705
1818
|
init_worktree();
|
|
1706
|
-
init_errors();
|
|
1707
1819
|
init_output();
|
|
1708
1820
|
}
|
|
1709
1821
|
});
|
|
1710
1822
|
|
|
1823
|
+
// src/core/pr.ts
|
|
1824
|
+
import { execa as execa5 } from "execa";
|
|
1825
|
+
async function checkPrState(branch) {
|
|
1826
|
+
try {
|
|
1827
|
+
const result = await execa5(
|
|
1828
|
+
"gh",
|
|
1829
|
+
["pr", "view", branch, "--json", "state", "--jq", ".state"],
|
|
1830
|
+
execaEnv
|
|
1831
|
+
);
|
|
1832
|
+
const state = result.stdout.trim().toUpperCase();
|
|
1833
|
+
if (state === "MERGED" || state === "OPEN" || state === "CLOSED") {
|
|
1834
|
+
return state;
|
|
1835
|
+
}
|
|
1836
|
+
return "UNKNOWN";
|
|
1837
|
+
} catch {
|
|
1838
|
+
return "UNKNOWN";
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
var init_pr = __esm({
|
|
1842
|
+
"src/core/pr.ts"() {
|
|
1843
|
+
"use strict";
|
|
1844
|
+
init_env();
|
|
1845
|
+
}
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1711
1848
|
// src/core/self.ts
|
|
1712
1849
|
function getCurrentPaneId() {
|
|
1713
1850
|
return process.env.TMUX_PANE ?? null;
|
|
@@ -1841,7 +1978,7 @@ __export(kill_exports, {
|
|
|
1841
1978
|
async function killCommand(options) {
|
|
1842
1979
|
const projectRoot = await getRepoRoot();
|
|
1843
1980
|
if (!options.agent && !options.worktree && !options.all) {
|
|
1844
|
-
throw new
|
|
1981
|
+
throw new PpgError("One of --agent, --worktree, or --all is required", "INVALID_ARGS");
|
|
1845
1982
|
}
|
|
1846
1983
|
const selfPaneId = getCurrentPaneId();
|
|
1847
1984
|
let paneMap;
|
|
@@ -1892,6 +2029,14 @@ async function killSingleAgent(projectRoot, agentId2, options, selfPaneId, paneM
|
|
|
1892
2029
|
success(`Deleted agent ${agentId2}`);
|
|
1893
2030
|
}
|
|
1894
2031
|
} else {
|
|
2032
|
+
if (isTerminal) {
|
|
2033
|
+
if (options.json) {
|
|
2034
|
+
output({ success: true, killed: [], message: `Agent ${agentId2} already ${agent.status}` }, true);
|
|
2035
|
+
} else {
|
|
2036
|
+
info(`Agent ${agentId2} already ${agent.status}, skipping kill`);
|
|
2037
|
+
}
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
1895
2040
|
info(`Killing agent ${agentId2}`);
|
|
1896
2041
|
await killAgent(agent);
|
|
1897
2042
|
await updateManifest(projectRoot, (m) => {
|
|
@@ -1938,11 +2083,19 @@ async function killWorktreeAgents(projectRoot, worktreeRef, options, selfPaneId,
|
|
|
1938
2083
|
}
|
|
1939
2084
|
return m;
|
|
1940
2085
|
});
|
|
1941
|
-
|
|
2086
|
+
let skippedOpenPr = false;
|
|
2087
|
+
if (options.delete && !options.includeOpenPrs) {
|
|
2088
|
+
const prState = await checkPrState(wt.branch);
|
|
2089
|
+
if (prState === "OPEN") {
|
|
2090
|
+
skippedOpenPr = true;
|
|
2091
|
+
warn(`Skipping deletion of worktree ${wt.id} (${wt.name}) \u2014 has open PR on branch ${wt.branch}. Use --include-open-prs to override.`);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
const shouldRemove = (options.remove || options.delete) && !skippedOpenPr;
|
|
1942
2095
|
if (shouldRemove) {
|
|
1943
2096
|
await removeWorktreeCleanup(projectRoot, wt.id, selfPaneId, paneMap);
|
|
1944
2097
|
}
|
|
1945
|
-
if (options.delete) {
|
|
2098
|
+
if (options.delete && !skippedOpenPr) {
|
|
1946
2099
|
await updateManifest(projectRoot, (m) => {
|
|
1947
2100
|
delete m.worktrees[wt.id];
|
|
1948
2101
|
return m;
|
|
@@ -1954,16 +2107,17 @@ async function killWorktreeAgents(projectRoot, worktreeRef, options, selfPaneId,
|
|
|
1954
2107
|
killed: killedIds,
|
|
1955
2108
|
skipped: skippedIds.length > 0 ? skippedIds : void 0,
|
|
1956
2109
|
removed: shouldRemove ? [wt.id] : [],
|
|
1957
|
-
deleted: options.delete ? [wt.id] : []
|
|
2110
|
+
deleted: options.delete && !skippedOpenPr ? [wt.id] : [],
|
|
2111
|
+
skippedOpenPrs: skippedOpenPr ? [wt.id] : void 0
|
|
1958
2112
|
}, true);
|
|
1959
2113
|
} else {
|
|
1960
2114
|
success(`Killed ${killedIds.length} agent(s) in worktree ${wt.id}`);
|
|
1961
2115
|
if (skippedIds.length > 0) {
|
|
1962
2116
|
warn(`Skipped ${skippedIds.length} agent(s) due to self-protection`);
|
|
1963
2117
|
}
|
|
1964
|
-
if (options.delete) {
|
|
2118
|
+
if (options.delete && !skippedOpenPr) {
|
|
1965
2119
|
success(`Deleted worktree ${wt.id}`);
|
|
1966
|
-
} else if (options.remove) {
|
|
2120
|
+
} else if (options.remove && !skippedOpenPr) {
|
|
1967
2121
|
success(`Removed worktree ${wt.id}`);
|
|
1968
2122
|
}
|
|
1969
2123
|
}
|
|
@@ -2002,15 +2156,32 @@ async function killAllAgents(projectRoot, options, selfPaneId, paneMap) {
|
|
|
2002
2156
|
}
|
|
2003
2157
|
return m;
|
|
2004
2158
|
});
|
|
2159
|
+
let worktreesToRemove = activeWorktreeIds;
|
|
2160
|
+
const openPrWorktreeIds = [];
|
|
2161
|
+
if (options.delete && !options.includeOpenPrs) {
|
|
2162
|
+
worktreesToRemove = [];
|
|
2163
|
+
for (const wtId of activeWorktreeIds) {
|
|
2164
|
+
const wt = manifest.worktrees[wtId];
|
|
2165
|
+
if (wt) {
|
|
2166
|
+
const prState = await checkPrState(wt.branch);
|
|
2167
|
+
if (prState === "OPEN") {
|
|
2168
|
+
openPrWorktreeIds.push(wtId);
|
|
2169
|
+
warn(`Skipping deletion of worktree ${wtId} (${wt.name}) \u2014 has open PR`);
|
|
2170
|
+
} else {
|
|
2171
|
+
worktreesToRemove.push(wtId);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2005
2176
|
const shouldRemove = options.remove || options.delete;
|
|
2006
2177
|
if (shouldRemove) {
|
|
2007
|
-
for (const wtId of
|
|
2178
|
+
for (const wtId of worktreesToRemove) {
|
|
2008
2179
|
await removeWorktreeCleanup(projectRoot, wtId, selfPaneId, paneMap);
|
|
2009
2180
|
}
|
|
2010
2181
|
}
|
|
2011
2182
|
if (options.delete) {
|
|
2012
2183
|
await updateManifest(projectRoot, (m) => {
|
|
2013
|
-
for (const wtId of
|
|
2184
|
+
for (const wtId of worktreesToRemove) {
|
|
2014
2185
|
delete m.worktrees[wtId];
|
|
2015
2186
|
}
|
|
2016
2187
|
return m;
|
|
@@ -2021,18 +2192,22 @@ async function killAllAgents(projectRoot, options, selfPaneId, paneMap) {
|
|
|
2021
2192
|
success: true,
|
|
2022
2193
|
killed: killedIds,
|
|
2023
2194
|
skipped: skippedIds.length > 0 ? skippedIds : void 0,
|
|
2024
|
-
removed: shouldRemove ?
|
|
2025
|
-
deleted: options.delete ?
|
|
2195
|
+
removed: shouldRemove ? worktreesToRemove : [],
|
|
2196
|
+
deleted: options.delete ? worktreesToRemove : [],
|
|
2197
|
+
skippedOpenPrs: openPrWorktreeIds.length > 0 ? openPrWorktreeIds : void 0
|
|
2026
2198
|
}, true);
|
|
2027
2199
|
} else {
|
|
2028
2200
|
success(`Killed ${killedIds.length} agent(s) across ${activeWorktreeIds.length} worktree(s)`);
|
|
2029
2201
|
if (skippedIds.length > 0) {
|
|
2030
2202
|
warn(`Skipped ${skippedIds.length} agent(s) due to self-protection`);
|
|
2031
2203
|
}
|
|
2204
|
+
if (openPrWorktreeIds.length > 0) {
|
|
2205
|
+
warn(`Skipped deletion of ${openPrWorktreeIds.length} worktree(s) with open PRs`);
|
|
2206
|
+
}
|
|
2032
2207
|
if (options.delete) {
|
|
2033
|
-
success(`Deleted ${
|
|
2208
|
+
success(`Deleted ${worktreesToRemove.length} worktree(s)`);
|
|
2034
2209
|
} else if (options.remove) {
|
|
2035
|
-
success(`Removed ${
|
|
2210
|
+
success(`Removed ${worktreesToRemove.length} worktree(s)`);
|
|
2036
2211
|
}
|
|
2037
2212
|
}
|
|
2038
2213
|
}
|
|
@@ -2050,6 +2225,7 @@ var init_kill = __esm({
|
|
|
2050
2225
|
"use strict";
|
|
2051
2226
|
init_manifest();
|
|
2052
2227
|
init_agent();
|
|
2228
|
+
init_pr();
|
|
2053
2229
|
init_worktree();
|
|
2054
2230
|
init_cleanup();
|
|
2055
2231
|
init_self();
|
|
@@ -2066,12 +2242,7 @@ __export(attach_exports, {
|
|
|
2066
2242
|
});
|
|
2067
2243
|
async function attachCommand(target) {
|
|
2068
2244
|
const projectRoot = await getRepoRoot();
|
|
2069
|
-
|
|
2070
|
-
try {
|
|
2071
|
-
manifest = await readManifest(projectRoot);
|
|
2072
|
-
} catch {
|
|
2073
|
-
throw new NotInitializedError(projectRoot);
|
|
2074
|
-
}
|
|
2245
|
+
const manifest = await requireManifest(projectRoot);
|
|
2075
2246
|
let tmuxTarget;
|
|
2076
2247
|
const sessionName = manifest.sessionName;
|
|
2077
2248
|
let agent;
|
|
@@ -2079,7 +2250,7 @@ async function attachCommand(target) {
|
|
|
2079
2250
|
const wt = resolveWorktree(manifest, target);
|
|
2080
2251
|
if (wt) {
|
|
2081
2252
|
if (!wt.tmuxWindow) {
|
|
2082
|
-
throw new
|
|
2253
|
+
throw new PpgError("Worktree has no tmux window. Spawn agents first with: ppg spawn --worktree " + wt.id + ' --prompt "your task"', "NO_TMUX_WINDOW");
|
|
2083
2254
|
}
|
|
2084
2255
|
tmuxTarget = wt.tmuxWindow;
|
|
2085
2256
|
} else {
|
|
@@ -2091,7 +2262,7 @@ async function attachCommand(target) {
|
|
|
2091
2262
|
}
|
|
2092
2263
|
}
|
|
2093
2264
|
if (!tmuxTarget) {
|
|
2094
|
-
throw new
|
|
2265
|
+
throw new PpgError(`Could not resolve target: ${target}. Try a worktree ID, name, or agent ID.`, "TARGET_NOT_FOUND");
|
|
2095
2266
|
}
|
|
2096
2267
|
if (agent?.sessionId && worktreeId2) {
|
|
2097
2268
|
const paneInfo = await getPaneInfo(tmuxTarget);
|
|
@@ -2142,12 +2313,7 @@ __export(logs_exports, {
|
|
|
2142
2313
|
});
|
|
2143
2314
|
async function logsCommand(agentId2, options) {
|
|
2144
2315
|
const projectRoot = await getRepoRoot();
|
|
2145
|
-
|
|
2146
|
-
try {
|
|
2147
|
-
manifest = await readManifest(projectRoot);
|
|
2148
|
-
} catch {
|
|
2149
|
-
throw new NotInitializedError(projectRoot);
|
|
2150
|
-
}
|
|
2316
|
+
const manifest = await requireManifest(projectRoot);
|
|
2151
2317
|
const found = findAgent(manifest, agentId2);
|
|
2152
2318
|
if (!found) throw new AgentNotFoundError(agentId2);
|
|
2153
2319
|
const { agent } = found;
|
|
@@ -2194,7 +2360,7 @@ async function logsCommand(agentId2, options) {
|
|
|
2194
2360
|
console.log(content);
|
|
2195
2361
|
}
|
|
2196
2362
|
} catch {
|
|
2197
|
-
throw new
|
|
2363
|
+
throw new PpgError(`Could not capture pane for agent ${agentId2}. Pane may no longer exist.`, "PANE_NOT_FOUND");
|
|
2198
2364
|
}
|
|
2199
2365
|
}
|
|
2200
2366
|
}
|
|
@@ -2217,14 +2383,10 @@ __export(aggregate_exports, {
|
|
|
2217
2383
|
import fs9 from "fs/promises";
|
|
2218
2384
|
async function aggregateCommand(worktreeId2, options) {
|
|
2219
2385
|
const projectRoot = await getRepoRoot();
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
});
|
|
2225
|
-
} catch {
|
|
2226
|
-
throw new NotInitializedError(projectRoot);
|
|
2227
|
-
}
|
|
2386
|
+
await requireManifest(projectRoot);
|
|
2387
|
+
const manifest = await updateManifest(projectRoot, async (m) => {
|
|
2388
|
+
return refreshAllAgentStatuses(m, projectRoot);
|
|
2389
|
+
});
|
|
2228
2390
|
let worktrees;
|
|
2229
2391
|
if (options?.all) {
|
|
2230
2392
|
worktrees = Object.values(manifest.worktrees);
|
|
@@ -2318,24 +2480,20 @@ var merge_exports = {};
|
|
|
2318
2480
|
__export(merge_exports, {
|
|
2319
2481
|
mergeCommand: () => mergeCommand
|
|
2320
2482
|
});
|
|
2321
|
-
import { execa as
|
|
2483
|
+
import { execa as execa6 } from "execa";
|
|
2322
2484
|
async function mergeCommand(worktreeId2, options) {
|
|
2323
2485
|
const projectRoot = await getRepoRoot();
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
});
|
|
2329
|
-
} catch {
|
|
2330
|
-
throw new NotInitializedError(projectRoot);
|
|
2331
|
-
}
|
|
2486
|
+
await requireManifest(projectRoot);
|
|
2487
|
+
const manifest = await updateManifest(projectRoot, async (m) => {
|
|
2488
|
+
return refreshAllAgentStatuses(m, projectRoot);
|
|
2489
|
+
});
|
|
2332
2490
|
const wt = resolveWorktree(manifest, worktreeId2);
|
|
2333
2491
|
if (!wt) throw new WorktreeNotFoundError(worktreeId2);
|
|
2334
2492
|
const agents = Object.values(wt.agents);
|
|
2335
2493
|
const incomplete = agents.filter((a) => !["completed", "failed", "killed"].includes(a.status));
|
|
2336
2494
|
if (incomplete.length > 0 && !options.force) {
|
|
2337
2495
|
const ids = incomplete.map((a) => a.id).join(", ");
|
|
2338
|
-
throw new
|
|
2496
|
+
throw new PpgError(
|
|
2339
2497
|
`${incomplete.length} agent(s) still running: ${ids}. Use --force to merge anyway.`,
|
|
2340
2498
|
"AGENTS_RUNNING"
|
|
2341
2499
|
);
|
|
@@ -2356,15 +2514,20 @@ async function mergeCommand(worktreeId2, options) {
|
|
|
2356
2514
|
});
|
|
2357
2515
|
const strategy = options.strategy ?? "squash";
|
|
2358
2516
|
try {
|
|
2517
|
+
const currentBranch = await getCurrentBranch(projectRoot);
|
|
2518
|
+
if (currentBranch !== wt.baseBranch) {
|
|
2519
|
+
info(`Switching to base branch ${wt.baseBranch}`);
|
|
2520
|
+
await execa6("git", ["checkout", wt.baseBranch], { ...execaEnv, cwd: projectRoot });
|
|
2521
|
+
}
|
|
2359
2522
|
info(`Merging ${wt.branch} into ${wt.baseBranch} (${strategy})`);
|
|
2360
2523
|
if (strategy === "squash") {
|
|
2361
|
-
await
|
|
2362
|
-
await
|
|
2524
|
+
await execa6("git", ["merge", "--squash", wt.branch], { ...execaEnv, cwd: projectRoot });
|
|
2525
|
+
await execa6("git", ["commit", "-m", `ppg: merge ${wt.name} (${wt.branch})`], {
|
|
2363
2526
|
...execaEnv,
|
|
2364
2527
|
cwd: projectRoot
|
|
2365
2528
|
});
|
|
2366
2529
|
} else {
|
|
2367
|
-
await
|
|
2530
|
+
await execa6("git", ["merge", "--no-ff", wt.branch, "-m", `ppg: merge ${wt.name} (${wt.branch})`], {
|
|
2368
2531
|
...execaEnv,
|
|
2369
2532
|
cwd: projectRoot
|
|
2370
2533
|
});
|
|
@@ -2434,8 +2597,7 @@ var init_merge = __esm({
|
|
|
2434
2597
|
import fs10 from "fs/promises";
|
|
2435
2598
|
import path5 from "path";
|
|
2436
2599
|
import YAML2 from "yaml";
|
|
2437
|
-
async function
|
|
2438
|
-
const dir = swarmsDir(projectRoot);
|
|
2600
|
+
async function readSwarmNames(dir) {
|
|
2439
2601
|
try {
|
|
2440
2602
|
const files = await fs10.readdir(dir);
|
|
2441
2603
|
return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => f.replace(/\.ya?ml$/, "")).sort();
|
|
@@ -2443,41 +2605,69 @@ async function listSwarms(projectRoot) {
|
|
|
2443
2605
|
return [];
|
|
2444
2606
|
}
|
|
2445
2607
|
}
|
|
2446
|
-
async function
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2608
|
+
async function listSwarmsWithSource(projectRoot) {
|
|
2609
|
+
const localNames = await readSwarmNames(swarmsDir(projectRoot));
|
|
2610
|
+
const globalNames = await readSwarmNames(globalSwarmsDir());
|
|
2611
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2612
|
+
const result = [];
|
|
2613
|
+
for (const name of localNames) {
|
|
2614
|
+
seen.add(name);
|
|
2615
|
+
result.push({ name, source: "local" });
|
|
2616
|
+
}
|
|
2617
|
+
for (const name of globalNames) {
|
|
2618
|
+
if (!seen.has(name)) {
|
|
2619
|
+
result.push({ name, source: "global" });
|
|
2620
|
+
}
|
|
2452
2621
|
}
|
|
2453
|
-
|
|
2454
|
-
|
|
2622
|
+
return result;
|
|
2623
|
+
}
|
|
2624
|
+
async function trySwarmFile(dir, name) {
|
|
2625
|
+
const yamlPath = path5.join(dir, `${name}.yaml`);
|
|
2455
2626
|
try {
|
|
2456
|
-
await fs10.access(
|
|
2627
|
+
await fs10.access(yamlPath);
|
|
2628
|
+
return yamlPath;
|
|
2457
2629
|
} catch {
|
|
2458
|
-
|
|
2630
|
+
const ymlPath = path5.join(dir, `${name}.yml`);
|
|
2459
2631
|
try {
|
|
2460
|
-
await fs10.access(
|
|
2632
|
+
await fs10.access(ymlPath);
|
|
2633
|
+
return ymlPath;
|
|
2461
2634
|
} catch {
|
|
2462
|
-
|
|
2635
|
+
return null;
|
|
2463
2636
|
}
|
|
2464
2637
|
}
|
|
2638
|
+
}
|
|
2639
|
+
async function resolveSwarmFile(projectRoot, name) {
|
|
2640
|
+
const local = await trySwarmFile(swarmsDir(projectRoot), name);
|
|
2641
|
+
if (local) return local;
|
|
2642
|
+
return trySwarmFile(globalSwarmsDir(), name);
|
|
2643
|
+
}
|
|
2644
|
+
async function loadSwarm(projectRoot, name) {
|
|
2645
|
+
if (!SAFE_NAME.test(name)) {
|
|
2646
|
+
throw new PpgError(
|
|
2647
|
+
`Invalid swarm template name: "${name}" \u2014 must be alphanumeric, hyphens, or underscores`,
|
|
2648
|
+
"INVALID_ARGS"
|
|
2649
|
+
);
|
|
2650
|
+
}
|
|
2651
|
+
const filePath = await resolveSwarmFile(projectRoot, name);
|
|
2652
|
+
if (!filePath) {
|
|
2653
|
+
throw new PpgError(`Swarm template not found: ${name}`, "INVALID_ARGS");
|
|
2654
|
+
}
|
|
2465
2655
|
const raw = await fs10.readFile(filePath, "utf-8");
|
|
2466
2656
|
const parsed = YAML2.parse(raw);
|
|
2467
2657
|
if (!parsed || typeof parsed !== "object") {
|
|
2468
|
-
throw new
|
|
2658
|
+
throw new PpgError(`Invalid swarm template: ${name} (empty or malformed YAML)`, "INVALID_ARGS");
|
|
2469
2659
|
}
|
|
2470
2660
|
if (!parsed.name || !parsed.agents || !Array.isArray(parsed.agents)) {
|
|
2471
|
-
throw new
|
|
2661
|
+
throw new PpgError(`Invalid swarm template: ${name} (missing name or agents)`, "INVALID_ARGS");
|
|
2472
2662
|
}
|
|
2473
2663
|
if (!SAFE_NAME.test(parsed.name)) {
|
|
2474
|
-
throw new
|
|
2664
|
+
throw new PpgError(
|
|
2475
2665
|
`Invalid swarm name: "${parsed.name}" \u2014 must be alphanumeric, hyphens, or underscores`,
|
|
2476
2666
|
"INVALID_ARGS"
|
|
2477
2667
|
);
|
|
2478
2668
|
}
|
|
2479
2669
|
if (parsed.strategy && parsed.strategy !== "shared" && parsed.strategy !== "isolated") {
|
|
2480
|
-
throw new
|
|
2670
|
+
throw new PpgError(
|
|
2481
2671
|
`Invalid swarm strategy: ${parsed.strategy}. Must be 'shared' or 'isolated'`,
|
|
2482
2672
|
"INVALID_ARGS"
|
|
2483
2673
|
);
|
|
@@ -2485,13 +2675,13 @@ async function loadSwarm(projectRoot, name) {
|
|
|
2485
2675
|
for (let i = 0; i < parsed.agents.length; i++) {
|
|
2486
2676
|
const agent = parsed.agents[i];
|
|
2487
2677
|
if (!agent.prompt || typeof agent.prompt !== "string") {
|
|
2488
|
-
throw new
|
|
2678
|
+
throw new PpgError(
|
|
2489
2679
|
`Invalid swarm template: ${name} \u2014 agent[${i}] missing prompt field`,
|
|
2490
2680
|
"INVALID_ARGS"
|
|
2491
2681
|
);
|
|
2492
2682
|
}
|
|
2493
2683
|
if (!SAFE_NAME.test(agent.prompt)) {
|
|
2494
|
-
throw new
|
|
2684
|
+
throw new PpgError(
|
|
2495
2685
|
`Invalid prompt name: "${agent.prompt}" \u2014 must be alphanumeric, hyphens, or underscores`,
|
|
2496
2686
|
"INVALID_ARGS"
|
|
2497
2687
|
);
|
|
@@ -2538,23 +2728,20 @@ async function swarmCommand(templateName, options) {
|
|
|
2538
2728
|
await swarmShared(projectRoot, config, swarm, options, userVars);
|
|
2539
2729
|
}
|
|
2540
2730
|
}
|
|
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
2731
|
async function loadPromptFile(projectRoot, promptName) {
|
|
2553
|
-
const
|
|
2732
|
+
const localPath = path6.join(promptsDir(projectRoot), `${promptName}.md`);
|
|
2554
2733
|
try {
|
|
2555
|
-
return await fs11.readFile(
|
|
2734
|
+
return await fs11.readFile(localPath, "utf-8");
|
|
2556
2735
|
} catch {
|
|
2557
|
-
|
|
2736
|
+
const globalPath = path6.join(globalPromptsDir(), `${promptName}.md`);
|
|
2737
|
+
try {
|
|
2738
|
+
return await fs11.readFile(globalPath, "utf-8");
|
|
2739
|
+
} catch {
|
|
2740
|
+
throw new PpgError(
|
|
2741
|
+
`Prompt file not found: ${promptName}.md (checked .ppg/prompts/ and ~/.ppg/prompts/)`,
|
|
2742
|
+
"INVALID_ARGS"
|
|
2743
|
+
);
|
|
2744
|
+
}
|
|
2558
2745
|
}
|
|
2559
2746
|
}
|
|
2560
2747
|
async function spawnSwarmAgent(opts) {
|
|
@@ -2595,7 +2782,8 @@ async function swarmShared(projectRoot, config, swarm, options, userVars) {
|
|
|
2595
2782
|
base: baseBranch
|
|
2596
2783
|
});
|
|
2597
2784
|
await setupWorktreeEnv(projectRoot, wtPath, config);
|
|
2598
|
-
const
|
|
2785
|
+
const manifest = await readManifest(projectRoot);
|
|
2786
|
+
const sessionName = manifest.sessionName;
|
|
2599
2787
|
await ensureSession(sessionName);
|
|
2600
2788
|
const windowTarget = await createWindow(sessionName, name, wtPath);
|
|
2601
2789
|
await updateManifest(projectRoot, (m) => {
|
|
@@ -2640,7 +2828,8 @@ async function swarmShared(projectRoot, config, swarm, options, userVars) {
|
|
|
2640
2828
|
async function swarmIsolated(projectRoot, config, swarm, options, userVars) {
|
|
2641
2829
|
const baseBranch = options.base ?? await getCurrentBranch(projectRoot);
|
|
2642
2830
|
const baseName = options.name ? normalizeName(options.name, swarm.name) : swarm.name;
|
|
2643
|
-
const
|
|
2831
|
+
const manifest = await readManifest(projectRoot);
|
|
2832
|
+
const sessionName = manifest.sessionName;
|
|
2644
2833
|
await ensureSession(sessionName);
|
|
2645
2834
|
const worktrees = [];
|
|
2646
2835
|
const allAgents = [];
|
|
@@ -2715,7 +2904,7 @@ async function swarmIntoExistingWorktree(projectRoot, config, swarm, options, us
|
|
|
2715
2904
|
const manifest = await readManifest(projectRoot);
|
|
2716
2905
|
const wt = resolveWorktree(manifest, options.worktree);
|
|
2717
2906
|
if (!wt) throw new WorktreeNotFoundError(options.worktree);
|
|
2718
|
-
const sessionName =
|
|
2907
|
+
const sessionName = manifest.sessionName;
|
|
2719
2908
|
let windowTarget = wt.tmuxWindow;
|
|
2720
2909
|
if (!windowTarget) {
|
|
2721
2910
|
await ensureSession(sessionName);
|
|
@@ -2786,41 +2975,96 @@ var init_swarm2 = __esm({
|
|
|
2786
2975
|
init_errors();
|
|
2787
2976
|
init_output();
|
|
2788
2977
|
init_name();
|
|
2978
|
+
init_vars();
|
|
2789
2979
|
}
|
|
2790
2980
|
});
|
|
2791
2981
|
|
|
2792
|
-
// src/commands/
|
|
2982
|
+
// src/commands/prompt.ts
|
|
2983
|
+
var prompt_exports = {};
|
|
2984
|
+
__export(prompt_exports, {
|
|
2985
|
+
promptCommand: () => promptCommand
|
|
2986
|
+
});
|
|
2987
|
+
import fs12 from "fs/promises";
|
|
2988
|
+
import path7 from "path";
|
|
2989
|
+
async function resolvePromptFile(projectRoot, promptName) {
|
|
2990
|
+
const localPath = path7.join(promptsDir(projectRoot), `${promptName}.md`);
|
|
2991
|
+
try {
|
|
2992
|
+
await fs12.access(localPath);
|
|
2993
|
+
return localPath;
|
|
2994
|
+
} catch {
|
|
2995
|
+
const globalPath = path7.join(globalPromptsDir(), `${promptName}.md`);
|
|
2996
|
+
try {
|
|
2997
|
+
await fs12.access(globalPath);
|
|
2998
|
+
return globalPath;
|
|
2999
|
+
} catch {
|
|
3000
|
+
throw new PpgError(
|
|
3001
|
+
`Prompt not found: ${promptName} (checked .ppg/prompts/ and ~/.ppg/prompts/)`,
|
|
3002
|
+
"INVALID_ARGS"
|
|
3003
|
+
);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
async function promptCommand(promptName, options) {
|
|
3008
|
+
const projectRoot = await getRepoRoot();
|
|
3009
|
+
const promptFilePath = await resolvePromptFile(projectRoot, promptName);
|
|
3010
|
+
const spawnOpts = {
|
|
3011
|
+
name: options.name ?? promptName,
|
|
3012
|
+
agent: options.agent,
|
|
3013
|
+
promptFile: promptFilePath,
|
|
3014
|
+
var: options.var,
|
|
3015
|
+
base: options.base,
|
|
3016
|
+
count: options.count,
|
|
3017
|
+
split: options.split,
|
|
3018
|
+
open: options.open,
|
|
3019
|
+
json: options.json
|
|
3020
|
+
};
|
|
3021
|
+
await spawnCommand(spawnOpts);
|
|
3022
|
+
}
|
|
3023
|
+
var init_prompt = __esm({
|
|
3024
|
+
"src/commands/prompt.ts"() {
|
|
3025
|
+
"use strict";
|
|
3026
|
+
init_worktree();
|
|
3027
|
+
init_paths();
|
|
3028
|
+
init_errors();
|
|
3029
|
+
init_spawn();
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
3032
|
+
|
|
3033
|
+
// src/commands/list.ts
|
|
2793
3034
|
var list_exports = {};
|
|
2794
3035
|
__export(list_exports, {
|
|
2795
3036
|
listCommand: () => listCommand
|
|
2796
3037
|
});
|
|
2797
|
-
import
|
|
2798
|
-
import
|
|
3038
|
+
import fs13 from "fs/promises";
|
|
3039
|
+
import path8 from "path";
|
|
2799
3040
|
async function listCommand(type, options) {
|
|
2800
3041
|
if (type === "templates") {
|
|
2801
3042
|
await listTemplatesCommand(options);
|
|
2802
3043
|
} else if (type === "swarms") {
|
|
2803
3044
|
await listSwarmsCommand(options);
|
|
3045
|
+
} else if (type === "prompts") {
|
|
3046
|
+
await listPromptsCommand(options);
|
|
2804
3047
|
} else {
|
|
2805
|
-
throw new
|
|
3048
|
+
throw new PpgError(`Unknown list type: ${type}. Available: templates, swarms, prompts`, "INVALID_ARGS");
|
|
2806
3049
|
}
|
|
2807
3050
|
}
|
|
2808
3051
|
async function listTemplatesCommand(options) {
|
|
2809
3052
|
const projectRoot = await getRepoRoot();
|
|
2810
|
-
const
|
|
2811
|
-
if (
|
|
2812
|
-
console.log("No templates found in .
|
|
3053
|
+
const entries = await listTemplatesWithSource(projectRoot);
|
|
3054
|
+
if (entries.length === 0) {
|
|
3055
|
+
console.log("No templates found in .ppg/templates/ or ~/.ppg/templates/");
|
|
2813
3056
|
return;
|
|
2814
3057
|
}
|
|
2815
3058
|
const templates = await Promise.all(
|
|
2816
|
-
|
|
2817
|
-
const
|
|
2818
|
-
const
|
|
3059
|
+
entries.map(async ({ name, source }) => {
|
|
3060
|
+
const dir = source === "local" ? templatesDir(projectRoot) : globalTemplatesDir();
|
|
3061
|
+
const filePath = path8.join(dir, `${name}.md`);
|
|
3062
|
+
const content = await fs13.readFile(filePath, "utf-8");
|
|
2819
3063
|
const firstLine = content.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
2820
3064
|
const description = firstLine.replace(/^#+\s*/, "").trim();
|
|
2821
3065
|
const vars = [...content.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]);
|
|
2822
3066
|
const uniqueVars = [...new Set(vars)];
|
|
2823
|
-
return { name, description, variables: uniqueVars };
|
|
3067
|
+
return { name, description, variables: uniqueVars, source };
|
|
2824
3068
|
})
|
|
2825
3069
|
);
|
|
2826
3070
|
if (options.json) {
|
|
@@ -2829,35 +3073,37 @@ async function listTemplatesCommand(options) {
|
|
|
2829
3073
|
}
|
|
2830
3074
|
const columns = [
|
|
2831
3075
|
{ header: "Name", key: "name", width: 20 },
|
|
2832
|
-
{ header: "Description", key: "description", width:
|
|
3076
|
+
{ header: "Description", key: "description", width: 36 },
|
|
2833
3077
|
{
|
|
2834
3078
|
header: "Variables",
|
|
2835
3079
|
key: "variables",
|
|
2836
|
-
width:
|
|
3080
|
+
width: 24,
|
|
2837
3081
|
format: (v) => v.join(", ")
|
|
2838
|
-
}
|
|
3082
|
+
},
|
|
3083
|
+
{ header: "Source", key: "source", width: 8 }
|
|
2839
3084
|
];
|
|
2840
3085
|
console.log(formatTable(templates, columns));
|
|
2841
3086
|
}
|
|
2842
3087
|
async function listSwarmsCommand(options) {
|
|
2843
3088
|
const projectRoot = await getRepoRoot();
|
|
2844
|
-
const
|
|
2845
|
-
if (
|
|
3089
|
+
const entries = await listSwarmsWithSource(projectRoot);
|
|
3090
|
+
if (entries.length === 0) {
|
|
2846
3091
|
if (options.json) {
|
|
2847
3092
|
output({ swarms: [] }, true);
|
|
2848
3093
|
} else {
|
|
2849
|
-
console.log("No swarm templates found in .
|
|
3094
|
+
console.log("No swarm templates found in .ppg/swarms/ or ~/.ppg/swarms/");
|
|
2850
3095
|
}
|
|
2851
3096
|
return;
|
|
2852
3097
|
}
|
|
2853
3098
|
const swarms = await Promise.all(
|
|
2854
|
-
|
|
3099
|
+
entries.map(async ({ name, source }) => {
|
|
2855
3100
|
const swarm = await loadSwarm(projectRoot, name);
|
|
2856
3101
|
return {
|
|
2857
3102
|
name,
|
|
2858
3103
|
description: swarm.description,
|
|
2859
3104
|
strategy: swarm.strategy,
|
|
2860
|
-
agents: swarm.agents.length
|
|
3105
|
+
agents: swarm.agents.length,
|
|
3106
|
+
source
|
|
2861
3107
|
};
|
|
2862
3108
|
})
|
|
2863
3109
|
);
|
|
@@ -2867,12 +3113,81 @@ async function listSwarmsCommand(options) {
|
|
|
2867
3113
|
}
|
|
2868
3114
|
const columns = [
|
|
2869
3115
|
{ header: "Name", key: "name", width: 20 },
|
|
2870
|
-
{ header: "Description", key: "description", width:
|
|
3116
|
+
{ header: "Description", key: "description", width: 34 },
|
|
2871
3117
|
{ header: "Strategy", key: "strategy", width: 10 },
|
|
2872
|
-
{ header: "Agents", key: "agents", width: 8 }
|
|
3118
|
+
{ header: "Agents", key: "agents", width: 8 },
|
|
3119
|
+
{ header: "Source", key: "source", width: 8 }
|
|
2873
3120
|
];
|
|
2874
3121
|
console.log(formatTable(swarms, columns));
|
|
2875
3122
|
}
|
|
3123
|
+
async function listPromptEntries(projectRoot) {
|
|
3124
|
+
const localDir = promptsDir(projectRoot);
|
|
3125
|
+
const globalDir = globalPromptsDir();
|
|
3126
|
+
let localFiles = [];
|
|
3127
|
+
try {
|
|
3128
|
+
localFiles = (await fs13.readdir(localDir)).filter((f) => f.endsWith(".md")).sort();
|
|
3129
|
+
} catch {
|
|
3130
|
+
}
|
|
3131
|
+
let globalFiles = [];
|
|
3132
|
+
try {
|
|
3133
|
+
globalFiles = (await fs13.readdir(globalDir)).filter((f) => f.endsWith(".md")).sort();
|
|
3134
|
+
} catch {
|
|
3135
|
+
}
|
|
3136
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3137
|
+
const result = [];
|
|
3138
|
+
for (const file of localFiles) {
|
|
3139
|
+
const name = file.replace(/\.md$/, "");
|
|
3140
|
+
seen.add(name);
|
|
3141
|
+
result.push({ name, source: "local" });
|
|
3142
|
+
}
|
|
3143
|
+
for (const file of globalFiles) {
|
|
3144
|
+
const name = file.replace(/\.md$/, "");
|
|
3145
|
+
if (!seen.has(name)) {
|
|
3146
|
+
result.push({ name, source: "global" });
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
return result;
|
|
3150
|
+
}
|
|
3151
|
+
async function listPromptsCommand(options) {
|
|
3152
|
+
const projectRoot = await getRepoRoot();
|
|
3153
|
+
const entries = await listPromptEntries(projectRoot);
|
|
3154
|
+
if (entries.length === 0) {
|
|
3155
|
+
if (options.json) {
|
|
3156
|
+
output({ prompts: [] }, true);
|
|
3157
|
+
} else {
|
|
3158
|
+
console.log("No prompts found in .ppg/prompts/ or ~/.ppg/prompts/");
|
|
3159
|
+
}
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
const prompts = await Promise.all(
|
|
3163
|
+
entries.map(async ({ name, source }) => {
|
|
3164
|
+
const dir = source === "local" ? promptsDir(projectRoot) : globalPromptsDir();
|
|
3165
|
+
const filePath = path8.join(dir, `${name}.md`);
|
|
3166
|
+
const content = await fs13.readFile(filePath, "utf-8");
|
|
3167
|
+
const firstLine = content.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
3168
|
+
const description = firstLine.replace(/^#+\s*/, "").trim();
|
|
3169
|
+
const vars = [...content.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]);
|
|
3170
|
+
const uniqueVars = [...new Set(vars)];
|
|
3171
|
+
return { name, description, variables: uniqueVars, source };
|
|
3172
|
+
})
|
|
3173
|
+
);
|
|
3174
|
+
if (options.json) {
|
|
3175
|
+
output({ prompts }, true);
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
const columns = [
|
|
3179
|
+
{ header: "Name", key: "name", width: 20 },
|
|
3180
|
+
{ header: "Description", key: "description", width: 36 },
|
|
3181
|
+
{
|
|
3182
|
+
header: "Variables",
|
|
3183
|
+
key: "variables",
|
|
3184
|
+
width: 24,
|
|
3185
|
+
format: (v) => v.join(", ")
|
|
3186
|
+
},
|
|
3187
|
+
{ header: "Source", key: "source", width: 8 }
|
|
3188
|
+
];
|
|
3189
|
+
console.log(formatTable(prompts, columns));
|
|
3190
|
+
}
|
|
2876
3191
|
var init_list = __esm({
|
|
2877
3192
|
"src/commands/list.ts"() {
|
|
2878
3193
|
"use strict";
|
|
@@ -2890,16 +3205,11 @@ var restart_exports = {};
|
|
|
2890
3205
|
__export(restart_exports, {
|
|
2891
3206
|
restartCommand: () => restartCommand
|
|
2892
3207
|
});
|
|
2893
|
-
import
|
|
3208
|
+
import fs14 from "fs/promises";
|
|
2894
3209
|
async function restartCommand(agentRef, options) {
|
|
2895
3210
|
const projectRoot = await getRepoRoot();
|
|
2896
3211
|
const config = await loadConfig(projectRoot);
|
|
2897
|
-
|
|
2898
|
-
try {
|
|
2899
|
-
manifest = await readManifest(projectRoot);
|
|
2900
|
-
} catch {
|
|
2901
|
-
throw new NotInitializedError(projectRoot);
|
|
2902
|
-
}
|
|
3212
|
+
const manifest = await requireManifest(projectRoot);
|
|
2903
3213
|
const found = findAgent(manifest, agentRef);
|
|
2904
3214
|
if (!found) throw new AgentNotFoundError(agentRef);
|
|
2905
3215
|
const { worktree: wt, agent: oldAgent } = found;
|
|
@@ -2913,9 +3223,9 @@ async function restartCommand(agentRef, options) {
|
|
|
2913
3223
|
} else {
|
|
2914
3224
|
const pFile = agentPromptFile(projectRoot, oldAgent.id);
|
|
2915
3225
|
try {
|
|
2916
|
-
promptText = await
|
|
3226
|
+
promptText = await fs14.readFile(pFile, "utf-8");
|
|
2917
3227
|
} catch {
|
|
2918
|
-
throw new
|
|
3228
|
+
throw new PpgError(
|
|
2919
3229
|
`Could not read original prompt for agent ${oldAgent.id}. Use --prompt to provide one.`,
|
|
2920
3230
|
"PROMPT_NOT_FOUND"
|
|
2921
3231
|
);
|
|
@@ -2944,7 +3254,8 @@ async function restartCommand(agentRef, options) {
|
|
|
2944
3254
|
tmuxTarget: windowTarget,
|
|
2945
3255
|
projectRoot,
|
|
2946
3256
|
branch: wt.branch,
|
|
2947
|
-
sessionId: newSessionId
|
|
3257
|
+
sessionId: newSessionId,
|
|
3258
|
+
skipResultInstructions: !options.prompt
|
|
2948
3259
|
});
|
|
2949
3260
|
await updateManifest(projectRoot, (m) => {
|
|
2950
3261
|
const mWt = m.worktrees[wt.id];
|
|
@@ -3003,20 +3314,15 @@ var diff_exports = {};
|
|
|
3003
3314
|
__export(diff_exports, {
|
|
3004
3315
|
diffCommand: () => diffCommand
|
|
3005
3316
|
});
|
|
3006
|
-
import { execa as
|
|
3317
|
+
import { execa as execa7 } from "execa";
|
|
3007
3318
|
async function diffCommand(worktreeRef, options) {
|
|
3008
3319
|
const projectRoot = await getRepoRoot();
|
|
3009
|
-
|
|
3010
|
-
try {
|
|
3011
|
-
manifest = await readManifest(projectRoot);
|
|
3012
|
-
} catch {
|
|
3013
|
-
throw new NotInitializedError(projectRoot);
|
|
3014
|
-
}
|
|
3320
|
+
const manifest = await requireManifest(projectRoot);
|
|
3015
3321
|
const wt = resolveWorktree(manifest, worktreeRef);
|
|
3016
3322
|
if (!wt) throw new WorktreeNotFoundError(worktreeRef);
|
|
3017
3323
|
const diffRange = `${wt.baseBranch}...${wt.branch}`;
|
|
3018
3324
|
if (options.json) {
|
|
3019
|
-
const result = await
|
|
3325
|
+
const result = await execa7("git", ["diff", "--numstat", diffRange], { ...execaEnv, cwd: projectRoot });
|
|
3020
3326
|
const files = result.stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
3021
3327
|
const [added, removed, file] = line.split(" ");
|
|
3022
3328
|
return {
|
|
@@ -3032,13 +3338,13 @@ async function diffCommand(worktreeRef, options) {
|
|
|
3032
3338
|
files
|
|
3033
3339
|
}, true);
|
|
3034
3340
|
} else if (options.stat) {
|
|
3035
|
-
const result = await
|
|
3341
|
+
const result = await execa7("git", ["diff", "--stat", diffRange], { ...execaEnv, cwd: projectRoot });
|
|
3036
3342
|
console.log(result.stdout);
|
|
3037
3343
|
} else if (options.nameOnly) {
|
|
3038
|
-
const result = await
|
|
3344
|
+
const result = await execa7("git", ["diff", "--name-only", diffRange], { ...execaEnv, cwd: projectRoot });
|
|
3039
3345
|
console.log(result.stdout);
|
|
3040
3346
|
} else {
|
|
3041
|
-
const result = await
|
|
3347
|
+
const result = await execa7("git", ["diff", diffRange], { ...execaEnv, cwd: projectRoot });
|
|
3042
3348
|
console.log(result.stdout);
|
|
3043
3349
|
}
|
|
3044
3350
|
}
|
|
@@ -3060,8 +3366,8 @@ __export(pr_exports, {
|
|
|
3060
3366
|
prCommand: () => prCommand,
|
|
3061
3367
|
truncateBody: () => truncateBody
|
|
3062
3368
|
});
|
|
3063
|
-
import
|
|
3064
|
-
import { execa as
|
|
3369
|
+
import fs15 from "fs/promises";
|
|
3370
|
+
import { execa as execa8 } from "execa";
|
|
3065
3371
|
async function prCommand(worktreeRef, options) {
|
|
3066
3372
|
const projectRoot = await getRepoRoot();
|
|
3067
3373
|
let manifest;
|
|
@@ -3075,15 +3381,15 @@ async function prCommand(worktreeRef, options) {
|
|
|
3075
3381
|
const wt = resolveWorktree(manifest, worktreeRef);
|
|
3076
3382
|
if (!wt) throw new WorktreeNotFoundError(worktreeRef);
|
|
3077
3383
|
try {
|
|
3078
|
-
await
|
|
3384
|
+
await execa8("gh", ["--version"], execaEnv);
|
|
3079
3385
|
} catch {
|
|
3080
3386
|
throw new GhNotFoundError();
|
|
3081
3387
|
}
|
|
3082
3388
|
info(`Pushing branch ${wt.branch} to origin`);
|
|
3083
3389
|
try {
|
|
3084
|
-
await
|
|
3390
|
+
await execa8("git", ["push", "-u", "origin", wt.branch], { ...execaEnv, cwd: projectRoot });
|
|
3085
3391
|
} catch (err) {
|
|
3086
|
-
throw new
|
|
3392
|
+
throw new PpgError(
|
|
3087
3393
|
`Failed to push branch ${wt.branch}: ${err instanceof Error ? err.message : err}`,
|
|
3088
3394
|
"INVALID_ARGS"
|
|
3089
3395
|
);
|
|
@@ -3108,10 +3414,10 @@ async function prCommand(worktreeRef, options) {
|
|
|
3108
3414
|
info(`Creating PR: ${title}`);
|
|
3109
3415
|
let prUrl;
|
|
3110
3416
|
try {
|
|
3111
|
-
const result = await
|
|
3417
|
+
const result = await execa8("gh", ghArgs, { ...execaEnv, cwd: projectRoot });
|
|
3112
3418
|
prUrl = result.stdout.trim();
|
|
3113
3419
|
} catch (err) {
|
|
3114
|
-
throw new
|
|
3420
|
+
throw new PpgError(
|
|
3115
3421
|
`Failed to create PR: ${err instanceof Error ? err.message : err}`,
|
|
3116
3422
|
"INVALID_ARGS"
|
|
3117
3423
|
);
|
|
@@ -3137,7 +3443,7 @@ async function prCommand(worktreeRef, options) {
|
|
|
3137
3443
|
async function buildBodyFromResults(agents) {
|
|
3138
3444
|
const reads = agents.map(async (agent) => {
|
|
3139
3445
|
try {
|
|
3140
|
-
return await
|
|
3446
|
+
return await fs15.readFile(agent.resultFile, "utf-8");
|
|
3141
3447
|
} catch {
|
|
3142
3448
|
return null;
|
|
3143
3449
|
}
|
|
@@ -3148,10 +3454,10 @@ async function buildBodyFromResults(agents) {
|
|
|
3148
3454
|
}
|
|
3149
3455
|
function truncateBody(body) {
|
|
3150
3456
|
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 `.
|
|
3457
|
+
return body.slice(0, MAX_BODY_LENGTH) + "\n\n---\n\n*[Truncated \u2014 full results available in `.ppg/results/`]*";
|
|
3152
3458
|
}
|
|
3153
3459
|
var MAX_BODY_LENGTH;
|
|
3154
|
-
var
|
|
3460
|
+
var init_pr2 = __esm({
|
|
3155
3461
|
"src/commands/pr.ts"() {
|
|
3156
3462
|
"use strict";
|
|
3157
3463
|
init_manifest();
|
|
@@ -3245,9 +3551,23 @@ async function resetCommand(options) {
|
|
|
3245
3551
|
return m;
|
|
3246
3552
|
});
|
|
3247
3553
|
}
|
|
3554
|
+
const openPrWorktreeIds = [];
|
|
3555
|
+
if (!options.includeOpenPrs) {
|
|
3556
|
+
for (const wt of worktrees) {
|
|
3557
|
+
if (wt.status === "cleaned") continue;
|
|
3558
|
+
const prState = await checkPrState(wt.branch);
|
|
3559
|
+
if (prState === "OPEN") {
|
|
3560
|
+
openPrWorktreeIds.push(wt.id);
|
|
3561
|
+
warn(`Skipping worktree ${wt.id} (${wt.name}) \u2014 has open PR on branch ${wt.branch}`);
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3248
3565
|
const removedIds = [];
|
|
3249
3566
|
const skippedWorktreeIds = [];
|
|
3250
3567
|
for (const wt of worktrees) {
|
|
3568
|
+
if (openPrWorktreeIds.includes(wt.id)) {
|
|
3569
|
+
continue;
|
|
3570
|
+
}
|
|
3251
3571
|
if (selfPaneId && paneMap && wouldCleanupAffectSelf(wt, selfPaneId, paneMap)) {
|
|
3252
3572
|
warn(`Skipping cleanup of worktree ${wt.id} (${wt.name}) \u2014 contains current ppg process`);
|
|
3253
3573
|
skippedWorktreeIds.push(wt.id);
|
|
@@ -3267,6 +3587,10 @@ async function resetCommand(options) {
|
|
|
3267
3587
|
return m;
|
|
3268
3588
|
});
|
|
3269
3589
|
}
|
|
3590
|
+
const orphansKilled = await killOrphanWindows(manifest.sessionName, selfPaneId);
|
|
3591
|
+
if (orphansKilled > 0) {
|
|
3592
|
+
info(`Killed ${orphansKilled} orphaned tmux window(s)`);
|
|
3593
|
+
}
|
|
3270
3594
|
if (options.prune) {
|
|
3271
3595
|
info("Pruning stale git worktrees");
|
|
3272
3596
|
await pruneWorktrees(projectRoot);
|
|
@@ -3279,6 +3603,7 @@ async function resetCommand(options) {
|
|
|
3279
3603
|
removed: removedIds,
|
|
3280
3604
|
warned: warnedNames.length > 0 ? warnedNames : void 0,
|
|
3281
3605
|
skipped: skippedWorktreeIds.length > 0 ? skippedWorktreeIds : void 0,
|
|
3606
|
+
skippedOpenPrs: openPrWorktreeIds.length > 0 ? openPrWorktreeIds : void 0,
|
|
3282
3607
|
pruned: options.prune ?? false
|
|
3283
3608
|
}, true);
|
|
3284
3609
|
} else {
|
|
@@ -3306,6 +3631,7 @@ var init_reset = __esm({
|
|
|
3306
3631
|
"use strict";
|
|
3307
3632
|
init_manifest();
|
|
3308
3633
|
init_agent();
|
|
3634
|
+
init_pr();
|
|
3309
3635
|
init_worktree();
|
|
3310
3636
|
init_cleanup();
|
|
3311
3637
|
init_self();
|
|
@@ -3322,12 +3648,7 @@ __export(clean_exports, {
|
|
|
3322
3648
|
});
|
|
3323
3649
|
async function cleanCommand(options) {
|
|
3324
3650
|
const projectRoot = await getRepoRoot();
|
|
3325
|
-
|
|
3326
|
-
try {
|
|
3327
|
-
manifest = await readManifest(projectRoot);
|
|
3328
|
-
} catch {
|
|
3329
|
-
throw new NotInitializedError(projectRoot);
|
|
3330
|
-
}
|
|
3651
|
+
const manifest = await requireManifest(projectRoot);
|
|
3331
3652
|
const selfPaneId = getCurrentPaneId();
|
|
3332
3653
|
let paneMap;
|
|
3333
3654
|
if (selfPaneId) {
|
|
@@ -3369,10 +3690,25 @@ async function cleanCommand(options) {
|
|
|
3369
3690
|
}
|
|
3370
3691
|
return;
|
|
3371
3692
|
}
|
|
3693
|
+
const openPrWorktreeIds = [];
|
|
3694
|
+
if (options.all && !options.includeOpenPrs) {
|
|
3695
|
+
for (const wt of toClean) {
|
|
3696
|
+
if (wt.status === "failed") {
|
|
3697
|
+
const prState = await checkPrState(wt.branch);
|
|
3698
|
+
if (prState === "OPEN") {
|
|
3699
|
+
openPrWorktreeIds.push(wt.id);
|
|
3700
|
+
warn(`Skipping worktree ${wt.id} (${wt.name}) \u2014 has open PR on branch ${wt.branch}`);
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3372
3705
|
const cleaned = [];
|
|
3373
3706
|
const skipped = [];
|
|
3374
3707
|
const removed = [];
|
|
3375
3708
|
for (const wt of toClean) {
|
|
3709
|
+
if (openPrWorktreeIds.includes(wt.id)) {
|
|
3710
|
+
continue;
|
|
3711
|
+
}
|
|
3376
3712
|
if (wt.status !== "cleaned") {
|
|
3377
3713
|
if (selfPaneId && paneMap && wouldCleanupAffectSelf(wt, selfPaneId, paneMap)) {
|
|
3378
3714
|
warn(`Skipping cleanup of worktree ${wt.id} (${wt.name}) \u2014 contains current ppg process`);
|
|
@@ -3406,6 +3742,7 @@ async function cleanCommand(options) {
|
|
|
3406
3742
|
success: true,
|
|
3407
3743
|
cleaned,
|
|
3408
3744
|
skipped: skipped.length > 0 ? skipped : void 0,
|
|
3745
|
+
skippedOpenPrs: openPrWorktreeIds.length > 0 ? openPrWorktreeIds : void 0,
|
|
3409
3746
|
removedFromManifest: removed,
|
|
3410
3747
|
pruned: options.prune ?? false
|
|
3411
3748
|
}, true);
|
|
@@ -3431,11 +3768,11 @@ var init_clean = __esm({
|
|
|
3431
3768
|
"src/commands/clean.ts"() {
|
|
3432
3769
|
"use strict";
|
|
3433
3770
|
init_manifest();
|
|
3771
|
+
init_pr();
|
|
3434
3772
|
init_worktree();
|
|
3435
3773
|
init_cleanup();
|
|
3436
3774
|
init_self();
|
|
3437
3775
|
init_tmux();
|
|
3438
|
-
init_errors();
|
|
3439
3776
|
init_output();
|
|
3440
3777
|
}
|
|
3441
3778
|
});
|
|
@@ -3447,12 +3784,7 @@ __export(send_exports, {
|
|
|
3447
3784
|
});
|
|
3448
3785
|
async function sendCommand(agentId2, text, options) {
|
|
3449
3786
|
const projectRoot = await getRepoRoot();
|
|
3450
|
-
|
|
3451
|
-
try {
|
|
3452
|
-
manifest = await readManifest(projectRoot);
|
|
3453
|
-
} catch {
|
|
3454
|
-
throw new NotInitializedError(projectRoot);
|
|
3455
|
-
}
|
|
3787
|
+
const manifest = await requireManifest(projectRoot);
|
|
3456
3788
|
const found = findAgent(manifest, agentId2);
|
|
3457
3789
|
if (!found) throw new AgentNotFoundError(agentId2);
|
|
3458
3790
|
const { agent } = found;
|
|
@@ -3496,7 +3828,7 @@ async function waitCommand(worktreeRef, options) {
|
|
|
3496
3828
|
const timeout = options.timeout ? options.timeout * 1e3 : void 0;
|
|
3497
3829
|
const startTime = Date.now();
|
|
3498
3830
|
if (!worktreeRef && !options.all) {
|
|
3499
|
-
throw new
|
|
3831
|
+
throw new PpgError("Specify a worktree ID or use --all", "INVALID_ARGS");
|
|
3500
3832
|
}
|
|
3501
3833
|
if (!options.json) {
|
|
3502
3834
|
info("Waiting for agents to complete...");
|
|
@@ -3511,7 +3843,7 @@ async function waitCommand(worktreeRef, options) {
|
|
|
3511
3843
|
agents: agents2.map(formatAgent)
|
|
3512
3844
|
}, true);
|
|
3513
3845
|
}
|
|
3514
|
-
throw new
|
|
3846
|
+
throw new PpgError("Timed out waiting for agents", "WAIT_TIMEOUT", 2);
|
|
3515
3847
|
}
|
|
3516
3848
|
const manifest = await refreshAndGet(projectRoot);
|
|
3517
3849
|
const agents = collectAgents(manifest, worktreeRef, options.all);
|
|
@@ -3529,7 +3861,7 @@ async function waitCommand(worktreeRef, options) {
|
|
|
3529
3861
|
}
|
|
3530
3862
|
}
|
|
3531
3863
|
if (anyFailed) {
|
|
3532
|
-
throw new
|
|
3864
|
+
throw new PpgError("Some agents failed", "AGENTS_FAILED", 1);
|
|
3533
3865
|
}
|
|
3534
3866
|
return;
|
|
3535
3867
|
}
|
|
@@ -3537,13 +3869,10 @@ async function waitCommand(worktreeRef, options) {
|
|
|
3537
3869
|
}
|
|
3538
3870
|
}
|
|
3539
3871
|
async function refreshAndGet(projectRoot) {
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
} catch {
|
|
3545
|
-
throw new NotInitializedError(projectRoot);
|
|
3546
|
-
}
|
|
3872
|
+
await requireManifest(projectRoot);
|
|
3873
|
+
return await updateManifest(projectRoot, async (m) => {
|
|
3874
|
+
return refreshAllAgentStatuses(m, projectRoot);
|
|
3875
|
+
});
|
|
3547
3876
|
}
|
|
3548
3877
|
function collectAgents(manifest, worktreeRef, all) {
|
|
3549
3878
|
if (all) {
|
|
@@ -3588,11 +3917,7 @@ __export(worktree_exports, {
|
|
|
3588
3917
|
async function worktreeCreateCommand(options) {
|
|
3589
3918
|
const projectRoot = await getRepoRoot();
|
|
3590
3919
|
const config = await loadConfig(projectRoot);
|
|
3591
|
-
|
|
3592
|
-
await readManifest(projectRoot);
|
|
3593
|
-
} catch {
|
|
3594
|
-
throw new NotInitializedError(projectRoot);
|
|
3595
|
-
}
|
|
3920
|
+
await requireManifest(projectRoot);
|
|
3596
3921
|
const baseBranch = options.base ?? await getCurrentBranch(projectRoot);
|
|
3597
3922
|
const wtId = worktreeId();
|
|
3598
3923
|
const name = options.name ? normalizeName(options.name, wtId) : wtId;
|
|
@@ -3643,7 +3968,6 @@ var init_worktree2 = __esm({
|
|
|
3643
3968
|
init_worktree();
|
|
3644
3969
|
init_env2();
|
|
3645
3970
|
init_id();
|
|
3646
|
-
init_errors();
|
|
3647
3971
|
init_output();
|
|
3648
3972
|
init_name();
|
|
3649
3973
|
}
|
|
@@ -3656,10 +3980,10 @@ __export(ui_exports, {
|
|
|
3656
3980
|
uiCommand: () => uiCommand
|
|
3657
3981
|
});
|
|
3658
3982
|
import { access } from "fs/promises";
|
|
3659
|
-
import
|
|
3660
|
-
import { execa as
|
|
3983
|
+
import path9 from "path";
|
|
3984
|
+
import { execa as execa9 } from "execa";
|
|
3661
3985
|
async function findDashboardBinary(projectRoot) {
|
|
3662
|
-
const localBuild =
|
|
3986
|
+
const localBuild = path9.join(
|
|
3663
3987
|
projectRoot,
|
|
3664
3988
|
"PPG CLI",
|
|
3665
3989
|
"build",
|
|
@@ -3683,12 +4007,12 @@ async function findDashboardBinary(projectRoot) {
|
|
|
3683
4007
|
} catch {
|
|
3684
4008
|
}
|
|
3685
4009
|
try {
|
|
3686
|
-
const result = await
|
|
4010
|
+
const result = await execa9("mdfind", [
|
|
3687
4011
|
'kMDItemCFBundleIdentifier == "com.2wit.PPG-CLI"'
|
|
3688
4012
|
]);
|
|
3689
4013
|
const appPath = result.stdout.trim().split("\n")[0];
|
|
3690
4014
|
if (appPath) {
|
|
3691
|
-
const binaryPath =
|
|
4015
|
+
const binaryPath = path9.join(appPath, "Contents", "MacOS", "PPG CLI");
|
|
3692
4016
|
try {
|
|
3693
4017
|
await access(binaryPath);
|
|
3694
4018
|
return binaryPath;
|
|
@@ -3709,7 +4033,7 @@ async function uiCommand() {
|
|
|
3709
4033
|
}
|
|
3710
4034
|
const binaryPath = await findDashboardBinary(projectRoot);
|
|
3711
4035
|
if (!binaryPath) {
|
|
3712
|
-
throw new
|
|
4036
|
+
throw new PpgError(
|
|
3713
4037
|
`Dashboard app not found. Install it with:
|
|
3714
4038
|
ppg install-dashboard
|
|
3715
4039
|
|
|
@@ -3719,7 +4043,7 @@ Or build from source:
|
|
|
3719
4043
|
);
|
|
3720
4044
|
}
|
|
3721
4045
|
const mPath = manifestPath(projectRoot);
|
|
3722
|
-
const proc =
|
|
4046
|
+
const proc = execa9(binaryPath, [
|
|
3723
4047
|
"--manifest-path",
|
|
3724
4048
|
mPath,
|
|
3725
4049
|
"--session-name",
|
|
@@ -3753,10 +4077,10 @@ import { createWriteStream } from "fs";
|
|
|
3753
4077
|
import { mkdir, cp, rm } from "fs/promises";
|
|
3754
4078
|
import { createRequire } from "module";
|
|
3755
4079
|
import { tmpdir } from "os";
|
|
3756
|
-
import
|
|
4080
|
+
import path10 from "path";
|
|
3757
4081
|
import { pipeline } from "stream/promises";
|
|
3758
4082
|
import { Readable } from "stream";
|
|
3759
|
-
import { execa as
|
|
4083
|
+
import { execa as execa10 } from "execa";
|
|
3760
4084
|
function getVersion() {
|
|
3761
4085
|
const pkg2 = require2("../package.json");
|
|
3762
4086
|
return pkg2.version;
|
|
@@ -3771,41 +4095,41 @@ async function installDashboardCommand(options) {
|
|
|
3771
4095
|
const res = await fetch(url);
|
|
3772
4096
|
if (!res.ok) {
|
|
3773
4097
|
if (res.status === 404) {
|
|
3774
|
-
throw new
|
|
4098
|
+
throw new PpgError(
|
|
3775
4099
|
`Dashboard release not found for ${tag}. The dashboard may not be available for this version yet.
|
|
3776
4100
|
Check: https://github.com/${REPO}/releases/tag/${tag}`,
|
|
3777
4101
|
"DASHBOARD_NOT_FOUND"
|
|
3778
4102
|
);
|
|
3779
4103
|
}
|
|
3780
|
-
throw new
|
|
4104
|
+
throw new PpgError(
|
|
3781
4105
|
`Failed to download dashboard: HTTP ${res.status} ${res.statusText}`,
|
|
3782
4106
|
"DOWNLOAD_FAILED"
|
|
3783
4107
|
);
|
|
3784
4108
|
}
|
|
3785
|
-
const tmp =
|
|
4109
|
+
const tmp = path10.join(tmpdir(), `ppg-dashboard-${Date.now()}`);
|
|
3786
4110
|
await mkdir(tmp, { recursive: true });
|
|
3787
|
-
const dmgPath =
|
|
4111
|
+
const dmgPath = path10.join(tmp, ASSET_NAME);
|
|
3788
4112
|
const body = res.body;
|
|
3789
|
-
if (!body) throw new
|
|
4113
|
+
if (!body) throw new PpgError("Empty response body", "DOWNLOAD_FAILED");
|
|
3790
4114
|
await pipeline(
|
|
3791
4115
|
Readable.fromWeb(body),
|
|
3792
4116
|
createWriteStream(dmgPath)
|
|
3793
4117
|
);
|
|
3794
4118
|
if (!json) info("Mounting\u2026");
|
|
3795
|
-
const mountResult = await
|
|
4119
|
+
const mountResult = await execa10("hdiutil", ["attach", dmgPath, "-nobrowse", "-quiet"]);
|
|
3796
4120
|
const mountLine = mountResult.stdout.trim().split("\n").pop() ?? "";
|
|
3797
4121
|
const mountPoint = mountLine.split(" ").pop()?.trim();
|
|
3798
4122
|
if (!mountPoint) {
|
|
3799
|
-
throw new
|
|
4123
|
+
throw new PpgError("Failed to mount DMG \u2014 could not determine mount point", "INSTALL_FAILED");
|
|
3800
4124
|
}
|
|
3801
4125
|
try {
|
|
3802
|
-
const srcApp =
|
|
3803
|
-
const destApp =
|
|
4126
|
+
const srcApp = path10.join(mountPoint, APP_NAME);
|
|
4127
|
+
const destApp = path10.join(dir, APP_NAME);
|
|
3804
4128
|
if (!json) info("Installing\u2026");
|
|
3805
4129
|
await rm(destApp, { recursive: true, force: true });
|
|
3806
4130
|
await cp(srcApp, destApp, { recursive: true });
|
|
3807
4131
|
try {
|
|
3808
|
-
await
|
|
4132
|
+
await execa10("xattr", ["-dr", "com.apple.quarantine", destApp]);
|
|
3809
4133
|
} catch {
|
|
3810
4134
|
}
|
|
3811
4135
|
if (json) {
|
|
@@ -3814,14 +4138,14 @@ Check: https://github.com/${REPO}/releases/tag/${tag}`,
|
|
|
3814
4138
|
success(`Dashboard ${tag} installed to ${destApp}`);
|
|
3815
4139
|
}
|
|
3816
4140
|
} finally {
|
|
3817
|
-
await
|
|
4141
|
+
await execa10("hdiutil", ["detach", mountPoint, "-quiet"]).catch(() => {
|
|
3818
4142
|
});
|
|
3819
4143
|
}
|
|
3820
4144
|
await rm(tmp, { recursive: true, force: true });
|
|
3821
4145
|
} catch (err) {
|
|
3822
|
-
if (err instanceof
|
|
4146
|
+
if (err instanceof PpgError) throw err;
|
|
3823
4147
|
const message = err instanceof Error ? err.message : String(err);
|
|
3824
|
-
throw new
|
|
4148
|
+
throw new PpgError(`Dashboard installation failed: ${message}`, "INSTALL_FAILED");
|
|
3825
4149
|
}
|
|
3826
4150
|
}
|
|
3827
4151
|
var require2, REPO, ASSET_NAME, APP_NAME;
|
|
@@ -3837,6 +4161,452 @@ var init_install_dashboard = __esm({
|
|
|
3837
4161
|
}
|
|
3838
4162
|
});
|
|
3839
4163
|
|
|
4164
|
+
// src/core/schedule.ts
|
|
4165
|
+
import fs16 from "fs/promises";
|
|
4166
|
+
import YAML3 from "yaml";
|
|
4167
|
+
import { CronExpressionParser } from "cron-parser";
|
|
4168
|
+
async function loadSchedules(projectRoot) {
|
|
4169
|
+
const filePath = schedulesPath(projectRoot);
|
|
4170
|
+
let raw;
|
|
4171
|
+
try {
|
|
4172
|
+
raw = await fs16.readFile(filePath, "utf-8");
|
|
4173
|
+
} catch (err) {
|
|
4174
|
+
if (err.code === "ENOENT") {
|
|
4175
|
+
throw new PpgError("No schedules file found. Create .ppg/schedules.yaml first.", "INVALID_ARGS");
|
|
4176
|
+
}
|
|
4177
|
+
throw err;
|
|
4178
|
+
}
|
|
4179
|
+
const parsed = YAML3.parse(raw);
|
|
4180
|
+
if (!parsed || !Array.isArray(parsed.schedules)) {
|
|
4181
|
+
throw new PpgError('Invalid schedules.yaml: missing "schedules" array', "INVALID_ARGS");
|
|
4182
|
+
}
|
|
4183
|
+
for (let i = 0; i < parsed.schedules.length; i++) {
|
|
4184
|
+
validateScheduleEntry(parsed.schedules[i], i);
|
|
4185
|
+
}
|
|
4186
|
+
return parsed.schedules;
|
|
4187
|
+
}
|
|
4188
|
+
function validateScheduleEntry(entry, index) {
|
|
4189
|
+
if (!entry.name || typeof entry.name !== "string") {
|
|
4190
|
+
throw new PpgError(`schedules[${index}]: missing "name"`, "INVALID_ARGS");
|
|
4191
|
+
}
|
|
4192
|
+
if (!SAFE_NAME2.test(entry.name)) {
|
|
4193
|
+
throw new PpgError(
|
|
4194
|
+
`schedules[${index}]: invalid name "${entry.name}" \u2014 must be alphanumeric, hyphens, or underscores`,
|
|
4195
|
+
"INVALID_ARGS"
|
|
4196
|
+
);
|
|
4197
|
+
}
|
|
4198
|
+
if (!entry.cron || typeof entry.cron !== "string") {
|
|
4199
|
+
throw new PpgError(`schedules[${index}]: missing "cron" expression`, "INVALID_ARGS");
|
|
4200
|
+
}
|
|
4201
|
+
validateCronExpression(entry.cron, index);
|
|
4202
|
+
const hasSwarm = entry.swarm && typeof entry.swarm === "string";
|
|
4203
|
+
const hasPrompt = entry.prompt && typeof entry.prompt === "string";
|
|
4204
|
+
if (!hasSwarm && !hasPrompt) {
|
|
4205
|
+
throw new PpgError(`schedules[${index}]: must specify either "swarm" or "prompt"`, "INVALID_ARGS");
|
|
4206
|
+
}
|
|
4207
|
+
if (hasSwarm && hasPrompt) {
|
|
4208
|
+
throw new PpgError(`schedules[${index}]: specify either "swarm" or "prompt", not both`, "INVALID_ARGS");
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
function validateCronExpression(expr, index) {
|
|
4212
|
+
const prefix = index !== void 0 ? `schedules[${index}]: ` : "";
|
|
4213
|
+
if (!expr || !expr.trim()) {
|
|
4214
|
+
throw new PpgError(`${prefix}invalid cron expression: "${expr}"`, "INVALID_ARGS");
|
|
4215
|
+
}
|
|
4216
|
+
try {
|
|
4217
|
+
CronExpressionParser.parse(expr);
|
|
4218
|
+
} catch {
|
|
4219
|
+
throw new PpgError(`${prefix}invalid cron expression: "${expr}"`, "INVALID_ARGS");
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
function getNextRun(cronExpr) {
|
|
4223
|
+
const expr = CronExpressionParser.parse(cronExpr);
|
|
4224
|
+
return expr.next().toDate();
|
|
4225
|
+
}
|
|
4226
|
+
function formatCronHuman(cronExpr) {
|
|
4227
|
+
const parts = cronExpr.trim().split(/\s+/);
|
|
4228
|
+
if (parts.length !== 5) return cronExpr;
|
|
4229
|
+
const [min, hour, dom, mon, dow] = parts;
|
|
4230
|
+
if (min === "0" && hour !== "*" && dom === "*" && mon === "*" && dow === "*") {
|
|
4231
|
+
return `daily at ${hour}:00`;
|
|
4232
|
+
}
|
|
4233
|
+
if (min.startsWith("*/") && hour === "*" && dom === "*" && mon === "*" && dow === "*") {
|
|
4234
|
+
return `every ${min.slice(2)} minutes`;
|
|
4235
|
+
}
|
|
4236
|
+
if (min !== "*" && hour === "*" && dom === "*" && mon === "*" && dow === "*") {
|
|
4237
|
+
return `every hour at :${min.padStart(2, "0")}`;
|
|
4238
|
+
}
|
|
4239
|
+
if (dow !== "*" && dom === "*" && mon === "*") {
|
|
4240
|
+
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
4241
|
+
const dayName = days[Number(dow)] ?? dow;
|
|
4242
|
+
return `${dayName} at ${hour}:${min.padStart(2, "0")}`;
|
|
4243
|
+
}
|
|
4244
|
+
return cronExpr;
|
|
4245
|
+
}
|
|
4246
|
+
var SAFE_NAME2;
|
|
4247
|
+
var init_schedule = __esm({
|
|
4248
|
+
"src/core/schedule.ts"() {
|
|
4249
|
+
"use strict";
|
|
4250
|
+
init_paths();
|
|
4251
|
+
init_errors();
|
|
4252
|
+
SAFE_NAME2 = /^[\w-]+$/;
|
|
4253
|
+
}
|
|
4254
|
+
});
|
|
4255
|
+
|
|
4256
|
+
// src/core/cron.ts
|
|
4257
|
+
import fs17 from "fs/promises";
|
|
4258
|
+
import { createReadStream } from "fs";
|
|
4259
|
+
import path11 from "path";
|
|
4260
|
+
import readline from "readline";
|
|
4261
|
+
import { execa as execa11 } from "execa";
|
|
4262
|
+
async function runCronDaemon(projectRoot) {
|
|
4263
|
+
const pidPath = cronPidPath(projectRoot);
|
|
4264
|
+
await fs17.mkdir(path11.dirname(pidPath), { recursive: true });
|
|
4265
|
+
await fs17.writeFile(pidPath, String(process.pid), "utf-8");
|
|
4266
|
+
await fs17.mkdir(logsDir(projectRoot), { recursive: true });
|
|
4267
|
+
await logCron(projectRoot, "Cron daemon starting");
|
|
4268
|
+
let states = await loadScheduleStates(projectRoot);
|
|
4269
|
+
let lastConfigMtime = await getFileMtime(schedulesPath(projectRoot));
|
|
4270
|
+
await logCron(projectRoot, `Loaded ${states.length} schedule(s)`);
|
|
4271
|
+
for (const s of states) {
|
|
4272
|
+
await logCron(projectRoot, ` ${s.entry.name}: next run at ${s.nextRun.toISOString()}`);
|
|
4273
|
+
}
|
|
4274
|
+
const cleanup = async () => {
|
|
4275
|
+
await logCron(projectRoot, "Cron daemon stopping");
|
|
4276
|
+
try {
|
|
4277
|
+
await fs17.unlink(pidPath);
|
|
4278
|
+
} catch {
|
|
4279
|
+
}
|
|
4280
|
+
process.exit(0);
|
|
4281
|
+
};
|
|
4282
|
+
process.on("SIGTERM", cleanup);
|
|
4283
|
+
process.on("SIGINT", cleanup);
|
|
4284
|
+
const tick = async () => {
|
|
4285
|
+
const currentMtime = await getFileMtime(schedulesPath(projectRoot));
|
|
4286
|
+
if (currentMtime !== lastConfigMtime) {
|
|
4287
|
+
try {
|
|
4288
|
+
states = await loadScheduleStates(projectRoot);
|
|
4289
|
+
lastConfigMtime = currentMtime;
|
|
4290
|
+
await logCron(projectRoot, `Reloaded schedules (${states.length} schedule(s))`);
|
|
4291
|
+
} catch (err) {
|
|
4292
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4293
|
+
await logCron(projectRoot, `Failed to reload schedules: ${msg}`);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
const now = /* @__PURE__ */ new Date();
|
|
4297
|
+
const due = states.filter((s) => now >= s.nextRun);
|
|
4298
|
+
await Promise.allSettled(
|
|
4299
|
+
due.map(async (state) => {
|
|
4300
|
+
await triggerSchedule(state, projectRoot);
|
|
4301
|
+
state.nextRun = getNextRun(state.entry.cron);
|
|
4302
|
+
state.lastTriggered = now;
|
|
4303
|
+
await logCron(projectRoot, ` ${state.entry.name}: next run at ${state.nextRun.toISOString()}`);
|
|
4304
|
+
})
|
|
4305
|
+
);
|
|
4306
|
+
};
|
|
4307
|
+
await tick();
|
|
4308
|
+
setInterval(tick, CHECK_INTERVAL_MS);
|
|
4309
|
+
await new Promise(() => {
|
|
4310
|
+
});
|
|
4311
|
+
}
|
|
4312
|
+
async function loadScheduleStates(projectRoot) {
|
|
4313
|
+
const schedules = await loadSchedules(projectRoot);
|
|
4314
|
+
return schedules.map((entry) => ({
|
|
4315
|
+
entry,
|
|
4316
|
+
nextRun: getNextRun(entry.cron)
|
|
4317
|
+
}));
|
|
4318
|
+
}
|
|
4319
|
+
async function getFileMtime(filePath) {
|
|
4320
|
+
try {
|
|
4321
|
+
const stat = await fs17.stat(filePath);
|
|
4322
|
+
return stat.mtimeMs;
|
|
4323
|
+
} catch {
|
|
4324
|
+
return 0;
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
async function triggerSchedule(state, projectRoot) {
|
|
4328
|
+
const { entry } = state;
|
|
4329
|
+
await logCron(projectRoot, `Triggering schedule: ${entry.name}`);
|
|
4330
|
+
const varArgs = [];
|
|
4331
|
+
if (entry.vars) {
|
|
4332
|
+
for (const [key, value] of Object.entries(entry.vars)) {
|
|
4333
|
+
varArgs.push("--var", `${key}=${value}`);
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
try {
|
|
4337
|
+
if (entry.swarm) {
|
|
4338
|
+
const args = ["swarm", entry.swarm, ...varArgs, "--json"];
|
|
4339
|
+
await logCron(projectRoot, ` Running: ppg ${args.join(" ")}`);
|
|
4340
|
+
const result = await execa11("ppg", args, { cwd: projectRoot, reject: false });
|
|
4341
|
+
if (result.exitCode === 0) {
|
|
4342
|
+
await logCron(projectRoot, ` Success: ${entry.name} (swarm: ${entry.swarm})`);
|
|
4343
|
+
} else {
|
|
4344
|
+
await logCron(projectRoot, ` Failed: ${entry.name} \u2014 ${result.stderr || result.stdout}`);
|
|
4345
|
+
}
|
|
4346
|
+
} else if (entry.prompt) {
|
|
4347
|
+
const args = [
|
|
4348
|
+
"spawn",
|
|
4349
|
+
"--name",
|
|
4350
|
+
`cron-${entry.name}-${Date.now()}`,
|
|
4351
|
+
"--template",
|
|
4352
|
+
entry.prompt,
|
|
4353
|
+
...varArgs,
|
|
4354
|
+
"--json"
|
|
4355
|
+
];
|
|
4356
|
+
await logCron(projectRoot, ` Running: ppg ${args.join(" ")}`);
|
|
4357
|
+
const result = await execa11("ppg", args, { cwd: projectRoot, reject: false });
|
|
4358
|
+
if (result.exitCode === 0) {
|
|
4359
|
+
await logCron(projectRoot, ` Success: ${entry.name} (prompt: ${entry.prompt})`);
|
|
4360
|
+
} else {
|
|
4361
|
+
await logCron(projectRoot, ` Failed: ${entry.name} \u2014 ${result.stderr || result.stdout}`);
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4364
|
+
} catch (err) {
|
|
4365
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4366
|
+
await logCron(projectRoot, ` Error triggering ${entry.name}: ${message}`);
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
async function logCron(projectRoot, message) {
|
|
4370
|
+
const logPath = cronLogPath(projectRoot);
|
|
4371
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4372
|
+
const line = `[${timestamp}] ${message}
|
|
4373
|
+
`;
|
|
4374
|
+
process.stdout.write(line);
|
|
4375
|
+
try {
|
|
4376
|
+
await fs17.appendFile(logPath, line, "utf-8");
|
|
4377
|
+
} catch {
|
|
4378
|
+
await fs17.mkdir(logsDir(projectRoot), { recursive: true });
|
|
4379
|
+
await fs17.appendFile(logPath, line, "utf-8");
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
async function isCronRunning(projectRoot) {
|
|
4383
|
+
return await getCronPid(projectRoot) !== null;
|
|
4384
|
+
}
|
|
4385
|
+
async function getCronPid(projectRoot) {
|
|
4386
|
+
const pidPath = cronPidPath(projectRoot);
|
|
4387
|
+
let raw;
|
|
4388
|
+
try {
|
|
4389
|
+
raw = await fs17.readFile(pidPath, "utf-8");
|
|
4390
|
+
} catch {
|
|
4391
|
+
return null;
|
|
4392
|
+
}
|
|
4393
|
+
const pid = parseInt(raw, 10);
|
|
4394
|
+
if (isNaN(pid)) {
|
|
4395
|
+
await cleanupPidFile(pidPath);
|
|
4396
|
+
return null;
|
|
4397
|
+
}
|
|
4398
|
+
try {
|
|
4399
|
+
process.kill(pid, 0);
|
|
4400
|
+
return pid;
|
|
4401
|
+
} catch {
|
|
4402
|
+
await cleanupPidFile(pidPath);
|
|
4403
|
+
return null;
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
async function cleanupPidFile(pidPath) {
|
|
4407
|
+
try {
|
|
4408
|
+
await fs17.unlink(pidPath);
|
|
4409
|
+
} catch {
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
async function readCronLog(projectRoot, lines = 20) {
|
|
4413
|
+
const logPath = cronLogPath(projectRoot);
|
|
4414
|
+
try {
|
|
4415
|
+
await fs17.access(logPath);
|
|
4416
|
+
} catch {
|
|
4417
|
+
return [];
|
|
4418
|
+
}
|
|
4419
|
+
const result = [];
|
|
4420
|
+
const rl = readline.createInterface({
|
|
4421
|
+
input: createReadStream(logPath, { encoding: "utf-8" }),
|
|
4422
|
+
crlfDelay: Infinity
|
|
4423
|
+
});
|
|
4424
|
+
for await (const line of rl) {
|
|
4425
|
+
if (!line) continue;
|
|
4426
|
+
result.push(line);
|
|
4427
|
+
if (result.length > lines) {
|
|
4428
|
+
result.shift();
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
return result;
|
|
4432
|
+
}
|
|
4433
|
+
var CHECK_INTERVAL_MS;
|
|
4434
|
+
var init_cron = __esm({
|
|
4435
|
+
"src/core/cron.ts"() {
|
|
4436
|
+
"use strict";
|
|
4437
|
+
init_schedule();
|
|
4438
|
+
init_paths();
|
|
4439
|
+
CHECK_INTERVAL_MS = 3e4;
|
|
4440
|
+
}
|
|
4441
|
+
});
|
|
4442
|
+
|
|
4443
|
+
// src/commands/cron.ts
|
|
4444
|
+
var cron_exports = {};
|
|
4445
|
+
__export(cron_exports, {
|
|
4446
|
+
cronDaemonCommand: () => cronDaemonCommand,
|
|
4447
|
+
cronListCommand: () => cronListCommand,
|
|
4448
|
+
cronStartCommand: () => cronStartCommand,
|
|
4449
|
+
cronStatusCommand: () => cronStatusCommand,
|
|
4450
|
+
cronStopCommand: () => cronStopCommand
|
|
4451
|
+
});
|
|
4452
|
+
import fs18 from "fs/promises";
|
|
4453
|
+
async function cronStartCommand(options) {
|
|
4454
|
+
const projectRoot = await getRepoRoot();
|
|
4455
|
+
await requireInit(projectRoot);
|
|
4456
|
+
if (await isCronRunning(projectRoot)) {
|
|
4457
|
+
const pid = await getCronPid(projectRoot);
|
|
4458
|
+
if (options.json) {
|
|
4459
|
+
output({ success: false, error: "Cron daemon is already running", pid }, true);
|
|
4460
|
+
} else {
|
|
4461
|
+
warn(`Cron daemon is already running (PID: ${pid})`);
|
|
4462
|
+
}
|
|
4463
|
+
return;
|
|
4464
|
+
}
|
|
4465
|
+
const schedules = await loadSchedules(projectRoot);
|
|
4466
|
+
if (schedules.length === 0) {
|
|
4467
|
+
throw new PpgError("No schedules defined in .ppg/schedules.yaml", "INVALID_ARGS");
|
|
4468
|
+
}
|
|
4469
|
+
const manifest = await readManifest(projectRoot);
|
|
4470
|
+
const sessionName = manifest.sessionName;
|
|
4471
|
+
await ensureSession(sessionName);
|
|
4472
|
+
const windowTarget = await createWindow(sessionName, CRON_WINDOW_NAME, projectRoot);
|
|
4473
|
+
const command = `ppg cron _daemon`;
|
|
4474
|
+
await sendKeys(windowTarget, command);
|
|
4475
|
+
if (options.json) {
|
|
4476
|
+
output({
|
|
4477
|
+
success: true,
|
|
4478
|
+
tmuxWindow: windowTarget,
|
|
4479
|
+
scheduleCount: schedules.length
|
|
4480
|
+
}, true);
|
|
4481
|
+
} else {
|
|
4482
|
+
success(`Cron daemon started in tmux window: ${windowTarget}`);
|
|
4483
|
+
info(`${schedules.length} schedule(s) loaded`);
|
|
4484
|
+
info(`Attach: tmux select-window -t ${windowTarget}`);
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
async function cronStopCommand(options) {
|
|
4488
|
+
const projectRoot = await getRepoRoot();
|
|
4489
|
+
const pid = await getCronPid(projectRoot);
|
|
4490
|
+
if (!pid) {
|
|
4491
|
+
if (options.json) {
|
|
4492
|
+
output({ success: false, error: "Cron daemon is not running" }, true);
|
|
4493
|
+
} else {
|
|
4494
|
+
warn("Cron daemon is not running");
|
|
4495
|
+
}
|
|
4496
|
+
return;
|
|
4497
|
+
}
|
|
4498
|
+
try {
|
|
4499
|
+
process.kill(pid, "SIGTERM");
|
|
4500
|
+
} catch {
|
|
4501
|
+
}
|
|
4502
|
+
try {
|
|
4503
|
+
await fs18.unlink(cronPidPath(projectRoot));
|
|
4504
|
+
} catch {
|
|
4505
|
+
}
|
|
4506
|
+
try {
|
|
4507
|
+
const manifest = await readManifest(projectRoot);
|
|
4508
|
+
const windows = await listSessionWindows(manifest.sessionName);
|
|
4509
|
+
const cronWindow = windows.find((w) => w.name === CRON_WINDOW_NAME);
|
|
4510
|
+
if (cronWindow) {
|
|
4511
|
+
await killWindow(`${manifest.sessionName}:${cronWindow.index}`);
|
|
4512
|
+
}
|
|
4513
|
+
} catch {
|
|
4514
|
+
}
|
|
4515
|
+
if (options.json) {
|
|
4516
|
+
output({ success: true, pid }, true);
|
|
4517
|
+
} else {
|
|
4518
|
+
success(`Cron daemon stopped (PID: ${pid})`);
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
async function cronListCommand(options) {
|
|
4522
|
+
const projectRoot = await getRepoRoot();
|
|
4523
|
+
await requireInit(projectRoot);
|
|
4524
|
+
const schedules = await loadSchedules(projectRoot);
|
|
4525
|
+
if (options.json) {
|
|
4526
|
+
const data = schedules.map((s) => ({
|
|
4527
|
+
name: s.name,
|
|
4528
|
+
type: s.swarm ? "swarm" : "prompt",
|
|
4529
|
+
target: s.swarm ?? s.prompt,
|
|
4530
|
+
cron: s.cron,
|
|
4531
|
+
nextRun: getNextRun(s.cron).toISOString(),
|
|
4532
|
+
vars: s.vars ?? {}
|
|
4533
|
+
}));
|
|
4534
|
+
output({ schedules: data }, true);
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
const columns = [
|
|
4538
|
+
{ header: "NAME", key: "name" },
|
|
4539
|
+
{ header: "TYPE", key: "type" },
|
|
4540
|
+
{ header: "TARGET", key: "target" },
|
|
4541
|
+
{ header: "CRON", key: "cron" },
|
|
4542
|
+
{ header: "SCHEDULE", key: "human" },
|
|
4543
|
+
{ header: "NEXT RUN", key: "nextRun" }
|
|
4544
|
+
];
|
|
4545
|
+
const rows = schedules.map((s) => ({
|
|
4546
|
+
name: s.name,
|
|
4547
|
+
type: s.swarm ? "swarm" : "prompt",
|
|
4548
|
+
target: s.swarm ?? s.prompt,
|
|
4549
|
+
cron: s.cron,
|
|
4550
|
+
human: formatCronHuman(s.cron),
|
|
4551
|
+
nextRun: getNextRun(s.cron).toLocaleString()
|
|
4552
|
+
}));
|
|
4553
|
+
console.log(formatTable(rows, columns));
|
|
4554
|
+
}
|
|
4555
|
+
async function cronStatusCommand(options) {
|
|
4556
|
+
const projectRoot = await getRepoRoot();
|
|
4557
|
+
const running = await isCronRunning(projectRoot);
|
|
4558
|
+
const pid = running ? await getCronPid(projectRoot) : null;
|
|
4559
|
+
const recentLines = await readCronLog(projectRoot, options.lines ?? 20);
|
|
4560
|
+
if (options.json) {
|
|
4561
|
+
output({
|
|
4562
|
+
running,
|
|
4563
|
+
pid,
|
|
4564
|
+
recentLog: recentLines
|
|
4565
|
+
}, true);
|
|
4566
|
+
return;
|
|
4567
|
+
}
|
|
4568
|
+
if (running) {
|
|
4569
|
+
success(`Cron daemon is running (PID: ${pid})`);
|
|
4570
|
+
} else {
|
|
4571
|
+
warn("Cron daemon is not running");
|
|
4572
|
+
}
|
|
4573
|
+
if (recentLines.length > 0) {
|
|
4574
|
+
console.log("\nRecent log:");
|
|
4575
|
+
for (const line of recentLines) {
|
|
4576
|
+
console.log(` ${line}`);
|
|
4577
|
+
}
|
|
4578
|
+
} else {
|
|
4579
|
+
info("No cron log entries yet");
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
async function cronDaemonCommand() {
|
|
4583
|
+
const projectRoot = await getRepoRoot();
|
|
4584
|
+
await requireInit(projectRoot);
|
|
4585
|
+
await runCronDaemon(projectRoot);
|
|
4586
|
+
}
|
|
4587
|
+
async function requireInit(projectRoot) {
|
|
4588
|
+
try {
|
|
4589
|
+
await fs18.access(manifestPath(projectRoot));
|
|
4590
|
+
} catch {
|
|
4591
|
+
throw new NotInitializedError(projectRoot);
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
var CRON_WINDOW_NAME;
|
|
4595
|
+
var init_cron2 = __esm({
|
|
4596
|
+
"src/commands/cron.ts"() {
|
|
4597
|
+
"use strict";
|
|
4598
|
+
init_worktree();
|
|
4599
|
+
init_manifest();
|
|
4600
|
+
init_schedule();
|
|
4601
|
+
init_cron();
|
|
4602
|
+
init_tmux();
|
|
4603
|
+
init_paths();
|
|
4604
|
+
init_errors();
|
|
4605
|
+
init_output();
|
|
4606
|
+
CRON_WINDOW_NAME = "ppg-cron";
|
|
4607
|
+
}
|
|
4608
|
+
});
|
|
4609
|
+
|
|
3840
4610
|
// src/cli.ts
|
|
3841
4611
|
init_errors();
|
|
3842
4612
|
init_output();
|
|
@@ -3845,12 +4615,12 @@ import { Command } from "commander";
|
|
|
3845
4615
|
var require3 = createRequire2(import.meta.url);
|
|
3846
4616
|
var pkg = require3("../package.json");
|
|
3847
4617
|
var program = new Command();
|
|
3848
|
-
program.name("ppg").description("Pure Point Guard \u2014 local orchestration runtime for parallel CLI coding agents").version(pkg.version);
|
|
4618
|
+
program.name("ppg").description("Pure Point Guard \u2014 local orchestration runtime for parallel CLI coding agents").version(pkg.version).option("--json", "Output as JSON");
|
|
3849
4619
|
program.command("init").description("Initialize Point Guard in the current git repository").option("--json", "Output as JSON").action(async (options) => {
|
|
3850
4620
|
const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
3851
4621
|
await initCommand2(options);
|
|
3852
4622
|
});
|
|
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 .
|
|
4623
|
+
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 .ppg/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", parsePositiveInt("count"), 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) => {
|
|
3854
4624
|
const { spawnCommand: spawnCommand2 } = await Promise.resolve().then(() => (init_spawn(), spawn_exports));
|
|
3855
4625
|
await spawnCommand2(options);
|
|
3856
4626
|
});
|
|
@@ -3858,7 +4628,7 @@ program.command("status").description("Show status of worktrees and agents").arg
|
|
|
3858
4628
|
const { statusCommand: statusCommand2 } = await Promise.resolve().then(() => (init_status(), status_exports));
|
|
3859
4629
|
await statusCommand2(worktree, options);
|
|
3860
4630
|
});
|
|
3861
|
-
program.command("kill").description("Kill agents or worktrees").option("-a, --agent <id>", "Kill a specific agent").option("-w, --worktree <id>", "Kill all agents in a worktree").option("--all", "Kill all agents in all worktrees").option("-r, --remove", "Also remove the worktree after killing").option("-d, --delete", "Delete agent/worktree entry from manifest after killing").option("--json", "Output as JSON").action(async (options) => {
|
|
4631
|
+
program.command("kill").description("Kill agents or worktrees").option("-a, --agent <id>", "Kill a specific agent").option("-w, --worktree <id>", "Kill all agents in a worktree").option("--all", "Kill all agents in all worktrees").option("-r, --remove", "Also remove the worktree after killing").option("-d, --delete", "Delete agent/worktree entry from manifest after killing").option("--include-open-prs", "Include worktrees with open GitHub PRs in deletion").option("--json", "Output as JSON").action(async (options) => {
|
|
3862
4632
|
const { killCommand: killCommand2 } = await Promise.resolve().then(() => (init_kill(), kill_exports));
|
|
3863
4633
|
await killCommand2(options);
|
|
3864
4634
|
});
|
|
@@ -3878,11 +4648,15 @@ program.command("merge").description("Merge a worktree branch back into base").a
|
|
|
3878
4648
|
const { mergeCommand: mergeCommand2 } = await Promise.resolve().then(() => (init_merge(), merge_exports));
|
|
3879
4649
|
await mergeCommand2(worktreeId2, options);
|
|
3880
4650
|
});
|
|
3881
|
-
program.command("swarm").description("Run a swarm template \u2014 spawn multiple agents from a predefined workflow").argument("<template>", "Swarm template name from .
|
|
4651
|
+
program.command("swarm").description("Run a swarm template \u2014 spawn multiple agents from a predefined workflow").argument("<template>", "Swarm template name from .ppg/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
4652
|
const { swarmCommand: swarmCommand2 } = await Promise.resolve().then(() => (init_swarm2(), swarm_exports));
|
|
3883
4653
|
await swarmCommand2(template, options);
|
|
3884
4654
|
});
|
|
3885
|
-
program.command("
|
|
4655
|
+
program.command("prompt").description("Spawn a worktree+agent using a named prompt from .ppg/prompts/").argument("<name>", "Prompt name (filename without .md)").option("-n, --name <name>", "Name for the worktree").option("-a, --agent <type>", "Agent type to use (default: claude)").option("--var <key=value...>", "Template variables", collectVars, []).option("-b, --base <branch>", "Base branch for the worktree").option("-c, --count <n>", "Number of agents to spawn", parsePositiveInt("count"), 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 (name, options) => {
|
|
4656
|
+
const { promptCommand: promptCommand2 } = await Promise.resolve().then(() => (init_prompt(), prompt_exports));
|
|
4657
|
+
await promptCommand2(name, options);
|
|
4658
|
+
});
|
|
4659
|
+
program.command("list").description("List available templates, swarms, or prompts").argument("<type>", "What to list: templates, swarms, prompts").option("--json", "Output as JSON").action(async (type, options) => {
|
|
3886
4660
|
const { listCommand: listCommand2 } = await Promise.resolve().then(() => (init_list(), list_exports));
|
|
3887
4661
|
await listCommand2(type, options);
|
|
3888
4662
|
});
|
|
@@ -3895,14 +4669,14 @@ program.command("diff").description("Show changes made in a worktree branch").ar
|
|
|
3895
4669
|
await diffCommand2(worktreeId2, options);
|
|
3896
4670
|
});
|
|
3897
4671
|
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(() => (
|
|
4672
|
+
const { prCommand: prCommand2 } = await Promise.resolve().then(() => (init_pr2(), pr_exports));
|
|
3899
4673
|
await prCommand2(worktreeId2, options);
|
|
3900
4674
|
});
|
|
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) => {
|
|
4675
|
+
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("--include-open-prs", "Include worktrees with open GitHub PRs in cleanup").option("--json", "Output as JSON").action(async (options) => {
|
|
3902
4676
|
const { resetCommand: resetCommand2 } = await Promise.resolve().then(() => (init_reset(), reset_exports));
|
|
3903
4677
|
await resetCommand2(options);
|
|
3904
4678
|
});
|
|
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) => {
|
|
4679
|
+
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("--include-open-prs", "Include worktrees with open GitHub PRs in cleanup").option("--json", "Output as JSON").action(async (options) => {
|
|
3906
4680
|
const { cleanCommand: cleanCommand2 } = await Promise.resolve().then(() => (init_clean(), clean_exports));
|
|
3907
4681
|
await cleanCommand2(options);
|
|
3908
4682
|
});
|
|
@@ -3910,7 +4684,7 @@ program.command("send").description("Send text to an agent's tmux pane").argumen
|
|
|
3910
4684
|
const { sendCommand: sendCommand2 } = await Promise.resolve().then(() => (init_send(), send_exports));
|
|
3911
4685
|
await sendCommand2(agentId2, text, options);
|
|
3912
4686
|
});
|
|
3913
|
-
program.command("wait").description("Wait for agents to reach terminal state").argument("[worktree-id]", "Worktree ID or name").option("--all", "Wait for all agents across all worktrees").option("--timeout <seconds>", "Timeout in seconds",
|
|
4687
|
+
program.command("wait").description("Wait for agents to reach terminal state").argument("[worktree-id]", "Worktree ID or name").option("--all", "Wait for all agents across all worktrees").option("--timeout <seconds>", "Timeout in seconds", parsePositiveInt("timeout")).option("--interval <seconds>", "Poll interval in seconds", parsePositiveInt("interval")).option("--json", "Output as JSON").action(async (worktreeId2, options) => {
|
|
3914
4688
|
const { waitCommand: waitCommand2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
|
|
3915
4689
|
await waitCommand2(worktreeId2, options);
|
|
3916
4690
|
});
|
|
@@ -3927,15 +4701,45 @@ program.command("install-dashboard").description("Download and install the macOS
|
|
|
3927
4701
|
const { installDashboardCommand: installDashboardCommand2 } = await Promise.resolve().then(() => (init_install_dashboard(), install_dashboard_exports));
|
|
3928
4702
|
await installDashboardCommand2(options);
|
|
3929
4703
|
});
|
|
4704
|
+
var cronCmd = program.command("cron").description("Manage scheduled runs");
|
|
4705
|
+
cronCmd.command("start").description("Start the cron scheduler daemon in a tmux window").option("--json", "Output as JSON").action(async (options) => {
|
|
4706
|
+
const { cronStartCommand: cronStartCommand2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports));
|
|
4707
|
+
await cronStartCommand2(options);
|
|
4708
|
+
});
|
|
4709
|
+
cronCmd.command("stop").description("Stop the cron scheduler daemon").option("--json", "Output as JSON").action(async (options) => {
|
|
4710
|
+
const { cronStopCommand: cronStopCommand2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports));
|
|
4711
|
+
await cronStopCommand2(options);
|
|
4712
|
+
});
|
|
4713
|
+
cronCmd.command("list").description("List configured schedules and next run times").option("--json", "Output as JSON").action(async (options) => {
|
|
4714
|
+
const { cronListCommand: cronListCommand2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports));
|
|
4715
|
+
await cronListCommand2(options);
|
|
4716
|
+
});
|
|
4717
|
+
cronCmd.command("status").description("Show cron daemon status and recent log").option("-l, --lines <n>", "Number of recent log lines to show", (v) => Number(v), 20).option("--json", "Output as JSON").action(async (options) => {
|
|
4718
|
+
const { cronStatusCommand: cronStatusCommand2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports));
|
|
4719
|
+
await cronStatusCommand2(options);
|
|
4720
|
+
});
|
|
4721
|
+
cronCmd.command("_daemon", { hidden: true }).description("Internal: run the cron daemon (called by ppg cron start)").action(async () => {
|
|
4722
|
+
const { cronDaemonCommand: cronDaemonCommand2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports));
|
|
4723
|
+
await cronDaemonCommand2();
|
|
4724
|
+
});
|
|
3930
4725
|
program.exitOverride();
|
|
3931
4726
|
function collectVars(value, previous) {
|
|
3932
4727
|
return previous.concat([value]);
|
|
3933
4728
|
}
|
|
4729
|
+
function parsePositiveInt(optionName) {
|
|
4730
|
+
return (v) => {
|
|
4731
|
+
const n = Number(v);
|
|
4732
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
4733
|
+
throw new Error(`--${optionName} must be a positive integer`);
|
|
4734
|
+
}
|
|
4735
|
+
return n;
|
|
4736
|
+
};
|
|
4737
|
+
}
|
|
3934
4738
|
async function main() {
|
|
3935
4739
|
try {
|
|
3936
4740
|
await program.parseAsync(process.argv);
|
|
3937
4741
|
} catch (err) {
|
|
3938
|
-
if (err instanceof
|
|
4742
|
+
if (err instanceof PpgError) {
|
|
3939
4743
|
outputError(err, program.opts().json ?? false);
|
|
3940
4744
|
process.exit(err.exitCode);
|
|
3941
4745
|
}
|
|
@@ -3945,7 +4749,7 @@ async function main() {
|
|
|
3945
4749
|
process.exit(0);
|
|
3946
4750
|
}
|
|
3947
4751
|
}
|
|
3948
|
-
outputError(err, false);
|
|
4752
|
+
outputError(err, program.opts().json ?? false);
|
|
3949
4753
|
process.exit(1);
|
|
3950
4754
|
}
|
|
3951
4755
|
}
|