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/dist/cli.js CHANGED
@@ -10,19 +10,19 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/lib/errors.ts
13
- var PgError, TmuxNotFoundError, NotGitRepoError, NotInitializedError, ManifestLockError, WorktreeNotFoundError, AgentNotFoundError, MergeFailedError, GhNotFoundError, UnmergedWorkError;
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
- PgError = class extends Error {
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 = "PgError";
22
+ this.name = "PpgError";
23
23
  }
24
24
  };
25
- TmuxNotFoundError = class extends PgError {
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 PgError {
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 PgError {
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 PgError {
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 PgError {
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 PgError {
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 PgError {
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 PgError {
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 PgError {
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 pgDir(projectRoot) {
201
- return path.join(projectRoot, PG_DIR);
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(pgDir(projectRoot), "manifest.json");
215
+ return path.join(ppgDir(projectRoot), "manifest.json");
205
216
  }
206
217
  function configPath(projectRoot) {
207
- return path.join(pgDir(projectRoot), "config.yaml");
218
+ return path.join(ppgDir(projectRoot), "config.yaml");
208
219
  }
209
220
  function resultsDir(projectRoot) {
210
- return path.join(pgDir(projectRoot), "results");
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(pgDir(projectRoot), "templates");
227
+ return path.join(ppgDir(projectRoot), "templates");
217
228
  }
218
229
  function logsDir(projectRoot) {
219
- return path.join(pgDir(projectRoot), "logs");
230
+ return path.join(ppgDir(projectRoot), "logs");
220
231
  }
221
232
  function promptsDir(projectRoot) {
222
- return path.join(pgDir(projectRoot), "prompts");
233
+ return path.join(ppgDir(projectRoot), "prompts");
223
234
  }
224
235
  function swarmsDir(projectRoot) {
225
- return path.join(pgDir(projectRoot), "swarms");
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(pgDir(projectRoot), "agent-prompts");
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 PG_DIR;
262
+ var PPG_DIR;
243
263
  var init_paths = __esm({
244
264
  "src/lib/paths.ts"() {
245
265
  "use strict";
246
- PG_DIR = ".pg";
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, write a summary of what you did and any important notes",
304
- "to the result file at: {{RESULT_FILE}}",
305
- "Use this exact format:",
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
- selectPane: () => selectPane,
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 selectPane(target) {
730
- await execa("tmux", ["select-pane", "-t", target], execaEnv);
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
- pgDir(projectRoot),
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 .pg/ directory structure");
784
- await writeDefaultConfig(projectRoot);
785
- info("Wrote default config.yaml");
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(pgDir(projectRoot), "conductor-context.md");
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
- pgDir: pgDir(projectRoot),
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
- ".pg/results/",
877
- ".pg/logs/",
878
- ".pg/manifest.json",
879
- ".pg/prompts/",
880
- ".pg/agent-prompts/",
881
- ".pg/swarms/",
882
- ".pg/conductor-context.md"
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 --no-open\` \u2014 Spawn worktree + agent
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 each: \`ppg spawn --name <name> --prompt "<self-contained prompt>" --json --no-open\`
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 results to the user and ask what they'd like to do next
930
- 6. If the user wants PRs: \`ppg pr <wt-id> --json\`
931
- 7. If the user wants direct merge: \`ppg merge <wt-id> --json\`
932
- 8. Cleanup when done: \`ppg reset --json\` or \`ppg clean --json\`
933
-
934
- **Stop at step 5** \u2014 do not auto-merge or auto-PR. Let the user decide.
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 --no-open\`.
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 listTemplates(projectRoot) {
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 dir = templatesDir(projectRoot);
1067
- const filePath = path4.join(dir, `${name}.md`);
1068
- return fs5.readFile(filePath, "utf-8");
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} "$(cat ${promptFilePath})"`;
1218
+ return `${envPrefix} ${command}${sessionFlag} ${promptFlag} ${catExpr}`;
1135
1219
  }
1136
- return `${envPrefix} ${command}${sessionFlag} "$(cat ${promptFilePath})"`;
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 PgError(
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 tmuxCmd = `if [ -x /usr/libexec/path_helper ]; then eval $(/usr/libexec/path_helper -s); fi; [ -f ~/.zprofile ] && source ~/.zprofile; [ -f ~/.zshrc ] && source ~/.zshrc; tmux attach-session -t ${sessionName} \\\\; select-window -t ${windowTarget}`;
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 "${title}"
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
- const templateContent = await loadTemplate(projectRoot, options.template);
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 PgError("One of --prompt, --prompt-file, or --template is required", "INVALID_ARGS");
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 sessionName = config.sessionName;
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
- for (const v of options.var ?? []) {
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
- for (const v of options.var ?? []) {
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
- let manifest;
1604
- try {
1605
- manifest = await updateManifest(projectRoot, async (m) => {
1606
- return refreshAllAgentStatuses(m, projectRoot);
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 PgError("One of --agent, --worktree, or --all is required", "INVALID_ARGS");
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
- const shouldRemove = options.remove || options.delete;
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 activeWorktreeIds) {
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 activeWorktreeIds) {
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 ? activeWorktreeIds : [],
2025
- deleted: options.delete ? activeWorktreeIds : []
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 ${activeWorktreeIds.length} worktree(s)`);
2208
+ success(`Deleted ${worktreesToRemove.length} worktree(s)`);
2034
2209
  } else if (options.remove) {
2035
- success(`Removed ${activeWorktreeIds.length} worktree(s)`);
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
- let manifest;
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 PgError("Worktree has no tmux window. Spawn agents first with: ppg spawn --worktree " + wt.id + ' --prompt "your task"', "NO_TMUX_WINDOW");
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 PgError(`Could not resolve target: ${target}. Try a worktree ID, name, or agent ID.`, "TARGET_NOT_FOUND");
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
- let manifest;
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 PgError(`Could not capture pane for agent ${agentId2}. Pane may no longer exist.`, "PANE_NOT_FOUND");
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
- let manifest;
2221
- try {
2222
- manifest = await updateManifest(projectRoot, async (m) => {
2223
- return refreshAllAgentStatuses(m, projectRoot);
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 execa5 } from "execa";
2483
+ import { execa as execa6 } from "execa";
2322
2484
  async function mergeCommand(worktreeId2, options) {
2323
2485
  const projectRoot = await getRepoRoot();
2324
- let manifest;
2325
- try {
2326
- manifest = await updateManifest(projectRoot, async (m) => {
2327
- return refreshAllAgentStatuses(m, projectRoot);
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 PgError(
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 execa5("git", ["merge", "--squash", wt.branch], { ...execaEnv, cwd: projectRoot });
2362
- await execa5("git", ["commit", "-m", `ppg: merge ${wt.name} (${wt.branch})`], {
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 execa5("git", ["merge", "--no-ff", wt.branch, "-m", `ppg: merge ${wt.name} (${wt.branch})`], {
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 listSwarms(projectRoot) {
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 loadSwarm(projectRoot, name) {
2447
- if (!SAFE_NAME.test(name)) {
2448
- throw new PgError(
2449
- `Invalid swarm template name: "${name}" \u2014 must be alphanumeric, hyphens, or underscores`,
2450
- "INVALID_ARGS"
2451
- );
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
- const dir = swarmsDir(projectRoot);
2454
- let filePath = path5.join(dir, `${name}.yaml`);
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(filePath);
2627
+ await fs10.access(yamlPath);
2628
+ return yamlPath;
2457
2629
  } catch {
2458
- filePath = path5.join(dir, `${name}.yml`);
2630
+ const ymlPath = path5.join(dir, `${name}.yml`);
2459
2631
  try {
2460
- await fs10.access(filePath);
2632
+ await fs10.access(ymlPath);
2633
+ return ymlPath;
2461
2634
  } catch {
2462
- throw new PgError(`Swarm template not found: ${name}`, "INVALID_ARGS");
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 PgError(`Invalid swarm template: ${name} (empty or malformed YAML)`, "INVALID_ARGS");
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 PgError(`Invalid swarm template: ${name} (missing name or agents)`, "INVALID_ARGS");
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 PgError(
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 PgError(
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 PgError(
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 PgError(
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 filePath = path6.join(promptsDir(projectRoot), `${promptName}.md`);
2732
+ const localPath = path6.join(promptsDir(projectRoot), `${promptName}.md`);
2554
2733
  try {
2555
- return await fs11.readFile(filePath, "utf-8");
2734
+ return await fs11.readFile(localPath, "utf-8");
2556
2735
  } catch {
2557
- throw new PgError(`Prompt file not found: ${promptName}.md in .pg/prompts/`, "INVALID_ARGS");
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 sessionName = config.sessionName;
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 sessionName = config.sessionName;
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 = config.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/list.ts
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 fs12 from "fs/promises";
2798
- import path7 from "path";
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 PgError(`Unknown list type: ${type}. Available: templates, swarms`, "INVALID_ARGS");
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 templateNames = await listTemplates(projectRoot);
2811
- if (templateNames.length === 0) {
2812
- console.log("No templates found in .pg/templates/");
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
- templateNames.map(async (name) => {
2817
- const filePath = path7.join(templatesDir(projectRoot), `${name}.md`);
2818
- const content = await fs12.readFile(filePath, "utf-8");
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: 40 },
3076
+ { header: "Description", key: "description", width: 36 },
2833
3077
  {
2834
3078
  header: "Variables",
2835
3079
  key: "variables",
2836
- width: 30,
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 swarmNames = await listSwarms(projectRoot);
2845
- if (swarmNames.length === 0) {
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 .pg/swarms/");
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
- swarmNames.map(async (name) => {
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: 40 },
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 fs13 from "fs/promises";
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
- let manifest;
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 fs13.readFile(pFile, "utf-8");
3226
+ promptText = await fs14.readFile(pFile, "utf-8");
2917
3227
  } catch {
2918
- throw new PgError(
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 execa6 } from "execa";
3317
+ import { execa as execa7 } from "execa";
3007
3318
  async function diffCommand(worktreeRef, options) {
3008
3319
  const projectRoot = await getRepoRoot();
3009
- let manifest;
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 execa6("git", ["diff", "--numstat", diffRange], { ...execaEnv, cwd: projectRoot });
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 execa6("git", ["diff", "--stat", diffRange], { ...execaEnv, cwd: projectRoot });
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 execa6("git", ["diff", "--name-only", diffRange], { ...execaEnv, cwd: projectRoot });
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 execa6("git", ["diff", diffRange], { ...execaEnv, cwd: projectRoot });
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 fs14 from "fs/promises";
3064
- import { execa as execa7 } from "execa";
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 execa7("gh", ["--version"], execaEnv);
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 execa7("git", ["push", "-u", "origin", wt.branch], { ...execaEnv, cwd: projectRoot });
3390
+ await execa8("git", ["push", "-u", "origin", wt.branch], { ...execaEnv, cwd: projectRoot });
3085
3391
  } catch (err) {
3086
- throw new PgError(
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 execa7("gh", ghArgs, { ...execaEnv, cwd: projectRoot });
3417
+ const result = await execa8("gh", ghArgs, { ...execaEnv, cwd: projectRoot });
3112
3418
  prUrl = result.stdout.trim();
3113
3419
  } catch (err) {
3114
- throw new PgError(
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 fs14.readFile(agent.resultFile, "utf-8");
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 `.pg/results/`]*";
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 init_pr = __esm({
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
- let manifest;
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
- let manifest;
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 PgError("Specify a worktree ID or use --all", "INVALID_ARGS");
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 PgError("Timed out waiting for agents", "WAIT_TIMEOUT", 2);
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 PgError("Some agents failed", "AGENTS_FAILED", 1);
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
- try {
3541
- return await updateManifest(projectRoot, async (m) => {
3542
- return refreshAllAgentStatuses(m, projectRoot);
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
- try {
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 path8 from "path";
3660
- import { execa as execa8 } from "execa";
3983
+ import path9 from "path";
3984
+ import { execa as execa9 } from "execa";
3661
3985
  async function findDashboardBinary(projectRoot) {
3662
- const localBuild = path8.join(
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 execa8("mdfind", [
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 = path8.join(appPath, "Contents", "MacOS", "PPG CLI");
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 PgError(
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 = execa8(binaryPath, [
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 path9 from "path";
4080
+ import path10 from "path";
3757
4081
  import { pipeline } from "stream/promises";
3758
4082
  import { Readable } from "stream";
3759
- import { execa as execa9 } from "execa";
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 PgError(
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 PgError(
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 = path9.join(tmpdir(), `ppg-dashboard-${Date.now()}`);
4109
+ const tmp = path10.join(tmpdir(), `ppg-dashboard-${Date.now()}`);
3786
4110
  await mkdir(tmp, { recursive: true });
3787
- const dmgPath = path9.join(tmp, ASSET_NAME);
4111
+ const dmgPath = path10.join(tmp, ASSET_NAME);
3788
4112
  const body = res.body;
3789
- if (!body) throw new PgError("Empty response body", "DOWNLOAD_FAILED");
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 execa9("hdiutil", ["attach", dmgPath, "-nobrowse", "-quiet"]);
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 PgError("Failed to mount DMG \u2014 could not determine mount point", "INSTALL_FAILED");
4123
+ throw new PpgError("Failed to mount DMG \u2014 could not determine mount point", "INSTALL_FAILED");
3800
4124
  }
3801
4125
  try {
3802
- const srcApp = path9.join(mountPoint, APP_NAME);
3803
- const destApp = path9.join(dir, APP_NAME);
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 execa9("xattr", ["-dr", "com.apple.quarantine", destApp]);
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 execa9("hdiutil", ["detach", mountPoint, "-quiet"]).catch(() => {
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 PgError) throw err;
4146
+ if (err instanceof PpgError) throw err;
3823
4147
  const message = err instanceof Error ? err.message : String(err);
3824
- throw new PgError(`Dashboard installation failed: ${message}`, "INSTALL_FAILED");
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 .pg/templates/").option("--var <key=value...>", "Template variables", collectVars, []).option("-b, --base <branch>", "Base branch for the worktree").option("-w, --worktree <id>", "Add agent to existing worktree").option("-c, --count <n>", "Number of agents to spawn", (v) => Number(v), 1).option("--split", "Put all agents in one window as split panes").option("--open", "Open a Terminal window for the spawned agents").option("--json", "Output as JSON").action(async (options) => {
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 .pg/swarms/").option("-w, --worktree <ref>", "Target an existing worktree by ID, name, or branch").option("--var <key=value...>", "Template variables", collectVars, []).option("-n, --name <name>", "Override worktree name").option("-b, --base <branch>", "Base branch for new worktree(s)").option("--open", "Open Terminal windows for spawned agents").option("--json", "Output as JSON").action(async (template, options) => {
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("list").description("List available templates or swarms").argument("<type>", "What to list: templates, swarms").option("--json", "Output as JSON").action(async (type, options) => {
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(() => (init_pr(), pr_exports));
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", parseInt).option("--interval <seconds>", "Poll interval in seconds", parseInt).option("--json", "Output as JSON").action(async (worktreeId2, options) => {
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 PgError) {
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
  }