pure-point-guard 0.3.0 → 0.3.2

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
@@ -95,7 +95,7 @@ var init_errors = __esm({
95
95
  constructor(names) {
96
96
  const list = names.map((n) => ` ${n}`).join("\n");
97
97
  super(
98
- `${names.length} worktree(s) have completed work that hasn't been merged or PR'd:
98
+ `${names.length} worktree(s) have unmerged work that hasn't been PR'd:
99
99
  ${list}
100
100
 
101
101
  Use --force to reset anyway, or create PRs first with: ppg pr <worktree-id>`,
@@ -163,7 +163,7 @@ function success(message) {
163
163
  function warn(message) {
164
164
  console.log(`${YELLOW}\u26A0${RESET} ${message}`);
165
165
  }
166
- var RESET, BOLD, DIM, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, GRAY, STATUS_COLORS;
166
+ var RESET, BOLD, DIM, RED, GREEN, YELLOW, BLUE, CYAN, GRAY, STATUS_COLORS;
167
167
  var init_output = __esm({
168
168
  "src/lib/output.ts"() {
169
169
  "use strict";
@@ -174,20 +174,17 @@ var init_output = __esm({
174
174
  GREEN = "\x1B[32m";
175
175
  YELLOW = "\x1B[33m";
176
176
  BLUE = "\x1B[34m";
177
- MAGENTA = "\x1B[35m";
178
177
  CYAN = "\x1B[36m";
179
178
  GRAY = "\x1B[90m";
180
179
  STATUS_COLORS = {
181
- spawning: YELLOW,
182
180
  running: GREEN,
183
- waiting: CYAN,
184
- completed: BLUE,
185
- failed: RED,
186
- killed: MAGENTA,
187
- lost: RED + BOLD,
181
+ idle: CYAN,
182
+ exited: BLUE,
183
+ gone: GRAY,
188
184
  active: GREEN,
189
185
  merging: YELLOW,
190
186
  merged: BLUE,
187
+ failed: RED,
191
188
  cleaned: GRAY
192
189
  };
193
190
  }
@@ -325,29 +322,17 @@ var init_config = __esm({
325
322
  claude: {
326
323
  name: "claude",
327
324
  command: "claude --dangerously-skip-permissions",
328
- interactive: true,
329
- resultInstructions: [
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:",
336
- "",
337
- "# Result: {{AGENT_ID}}",
338
- "",
339
- "## PR",
340
- "<the PR URL from step 3>",
341
- "",
342
- "## Summary",
343
- "<what you accomplished>",
344
- "",
345
- "## Changes",
346
- "<list of files changed>",
347
- "",
348
- "## Notes",
349
- "<any important observations>"
350
- ].join("\n")
325
+ interactive: true
326
+ },
327
+ codex: {
328
+ name: "codex",
329
+ command: "codex --yolo",
330
+ interactive: true
331
+ },
332
+ opencode: {
333
+ name: "opencode",
334
+ command: "opencode --yolo",
335
+ interactive: true
351
336
  }
352
337
  },
353
338
  envFiles: [".env", ".env.local"],
@@ -490,7 +475,7 @@ You are a senior engineer reviewing code for quality, readability, and maintaina
490
475
  - Documentation gaps for non-obvious logic
491
476
 
492
477
  ## Output
493
- Write a structured review to {{RESULT_FILE}} with specific file:line references and improvement suggestions.
478
+ Write a structured review with specific file:line references and improvement suggestions.
494
479
  `,
495
480
  "review-security": `# Security Review
496
481
 
@@ -508,7 +493,7 @@ You are a security engineer reviewing code for vulnerabilities and risks.
508
493
  - Secrets or credentials in code
509
494
 
510
495
  ## Output
511
- Write a structured review to {{RESULT_FILE}} with severity ratings and remediation guidance.
496
+ Write a structured review with severity ratings and remediation guidance.
512
497
  `,
513
498
  "review-regression": `# Regression & Risk Review
514
499
 
@@ -526,7 +511,7 @@ You are a QA engineer reviewing code for regression risks and test coverage gaps
526
511
  - Performance regressions
527
512
 
528
513
  ## Output
529
- Write a structured review to {{RESULT_FILE}} with risk ratings and recommended test additions.
514
+ Write a structured review with risk ratings and recommended test additions.
530
515
  `
531
516
  };
532
517
  }
@@ -977,11 +962,19 @@ var init_init = __esm({
977
962
 
978
963
  You are operating on the master branch of a ppg-managed project.
979
964
 
980
- ## Critical Rule
981
- **NEVER make code changes directly on the master branch.** Use \`ppg spawn\` to create worktrees.
965
+ ## When to Use ppg
966
+ Use \`ppg spawn\` whenever you want work to appear in the **ppg dashboard** \u2014 parallel tasks, code reviews,
967
+ batch issue work, multi-agent swarms. Agents spawned through ppg run in tmux panes the user can monitor,
968
+ interact with, and manage. Available agent types: \`claude\` (default), \`codex\`, \`opencode\` via \`--agent\`.
969
+
970
+ Direct edits, quick commands, and research are fine to do yourself \u2014 not everything needs an agent.
971
+ Never run \`claude\`, \`codex\`, or \`opencode\` directly as bash commands \u2014 they won't appear in the dashboard.
982
972
 
983
973
  ## Quick Reference
984
974
  - \`ppg spawn --name <name> --prompt "<task>" --json\` \u2014 Spawn worktree + agent
975
+ - \`ppg spawn --name <name> --agent codex --prompt "<task>" --json\` \u2014 Use Codex agent
976
+ - \`ppg spawn --name <name> --agent opencode --prompt "<task>" --json\` \u2014 Use OpenCode agent
977
+ - \`ppg spawn --worktree <id> --agent codex --prompt "review --base main" --json\` \u2014 Codex review
985
978
  - \`ppg status --json\` \u2014 Check statuses
986
979
  - \`ppg aggregate --all --json\` \u2014 Collect results (includes PR URLs)
987
980
  - \`ppg kill --agent <id> --json\` \u2014 Kill agent
@@ -1009,10 +1002,6 @@ Project root: {{PROJECT_ROOT}}
1009
1002
 
1010
1003
  ## Instructions
1011
1004
  {{PROMPT}}
1012
-
1013
- ## Result Reporting
1014
- When you have completed the task, write your results to:
1015
- {{RESULT_FILE}}
1016
1005
  `;
1017
1006
  }
1018
1007
  });
@@ -1046,6 +1035,11 @@ async function createWorktree(repoRoot, id, options) {
1046
1035
  await execa3("git", args, { ...execaEnv, cwd: repoRoot });
1047
1036
  return wtPath;
1048
1037
  }
1038
+ async function adoptWorktree(repoRoot, id, branch) {
1039
+ const wtPath = worktreePath(repoRoot, id);
1040
+ await execa3("git", ["worktree", "add", wtPath, branch], { ...execaEnv, cwd: repoRoot });
1041
+ return wtPath;
1042
+ }
1049
1043
  async function removeWorktree(repoRoot, wtPath, options) {
1050
1044
  const args = ["worktree", "remove", wtPath];
1051
1045
  if (options?.force) {
@@ -1169,31 +1163,12 @@ async function spawnAgent(options) {
1169
1163
  agentId: agentId2,
1170
1164
  agentConfig,
1171
1165
  prompt,
1172
- worktreePath: worktreePath2,
1173
1166
  tmuxTarget,
1174
- projectRoot,
1175
- branch
1167
+ projectRoot
1176
1168
  } = options;
1177
- const resFile = resultFile(projectRoot, agentId2);
1178
- let fullPrompt = prompt;
1179
- if (agentConfig.resultInstructions && !options.skipResultInstructions) {
1180
- const ctx = {
1181
- WORKTREE_PATH: worktreePath2,
1182
- BRANCH: branch,
1183
- AGENT_ID: agentId2,
1184
- RESULT_FILE: resFile,
1185
- PROJECT_ROOT: projectRoot
1186
- };
1187
- const instructions = renderTemplate(agentConfig.resultInstructions, ctx);
1188
- fullPrompt += `
1189
-
1190
- ---
1191
-
1192
- ${instructions}`;
1193
- }
1194
1169
  const pFile = agentPromptFile(projectRoot, agentId2);
1195
1170
  await fs6.mkdir(agentPromptsDir(projectRoot), { recursive: true });
1196
- await fs6.writeFile(pFile, fullPrompt, "utf-8");
1171
+ await fs6.writeFile(pFile, prompt, "utf-8");
1197
1172
  const command = buildAgentCommand(agentConfig, pFile, options.sessionId);
1198
1173
  await sendKeys(tmuxTarget, command);
1199
1174
  return {
@@ -1204,7 +1179,6 @@ ${instructions}`;
1204
1179
  tmuxTarget,
1205
1180
  prompt: prompt.slice(0, 500),
1206
1181
  // Truncate for manifest storage
1207
- resultFile: resFile,
1208
1182
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1209
1183
  ...options.sessionId ? { sessionId: options.sessionId } : {}
1210
1184
  };
@@ -1219,32 +1193,16 @@ function buildAgentCommand(agentConfig, promptFilePath, sessionId2) {
1219
1193
  }
1220
1194
  return `${envPrefix} ${command}${sessionFlag} ${catExpr}`;
1221
1195
  }
1222
- async function checkAgentStatus(agent, projectRoot, paneMap) {
1223
- if (["completed", "failed", "killed", "lost"].includes(agent.status)) {
1224
- return { status: agent.status, exitCode: agent.exitCode };
1225
- }
1226
- const hasResult = await fileExists(agent.resultFile);
1227
- if (hasResult) {
1228
- return { status: "completed" };
1229
- }
1196
+ async function checkAgentStatus(agent, _projectRoot, paneMap) {
1230
1197
  const paneInfo = paneMap ? paneMap.get(agent.tmuxTarget) ?? null : await getPaneInfo(agent.tmuxTarget);
1231
1198
  if (!paneInfo) {
1232
- return { status: "lost" };
1199
+ return { status: "gone" };
1233
1200
  }
1234
1201
  if (paneInfo.isDead) {
1235
- const exitCode = paneInfo.deadStatus;
1236
- const hasResultNow = await fileExists(agent.resultFile);
1237
- if (hasResultNow || exitCode === 0) {
1238
- return { status: "completed", exitCode: exitCode ?? 0 };
1239
- }
1240
- return { status: "failed", exitCode };
1202
+ return { status: "exited", exitCode: paneInfo.deadStatus };
1241
1203
  }
1242
1204
  if (SHELL_COMMANDS.has(paneInfo.currentCommand)) {
1243
- const hasResultNow = await fileExists(agent.resultFile);
1244
- if (hasResultNow) {
1245
- return { status: "completed", exitCode: 0 };
1246
- }
1247
- return { status: "failed", exitCode: void 0 };
1205
+ return { status: "idle" };
1248
1206
  }
1249
1207
  return { status: "running" };
1250
1208
  }
@@ -1260,27 +1218,18 @@ async function refreshAllAgentStatuses(manifest, projectRoot) {
1260
1218
  }
1261
1219
  }
1262
1220
  const results = await Promise.all(checks.map((c) => c.promise));
1263
- const now = (/* @__PURE__ */ new Date()).toISOString();
1264
1221
  for (let i = 0; i < checks.length; i++) {
1265
1222
  const { agent } = checks[i];
1266
1223
  const { status, exitCode } = results[i];
1267
- if (status !== agent.status) {
1268
- agent.status = status;
1269
- if (exitCode !== void 0) agent.exitCode = exitCode;
1270
- if (["completed", "failed", "lost"].includes(status) && !agent.completedAt) {
1271
- agent.completedAt = now;
1272
- }
1273
- }
1224
+ agent.status = status;
1225
+ if (exitCode !== void 0) agent.exitCode = exitCode;
1274
1226
  }
1275
1227
  const wtChecks = Object.values(manifest.worktrees).filter((wt) => wt.status === "active").map(async (wt) => {
1276
1228
  const exists = await fileExists(wt.path);
1277
1229
  if (!exists) {
1278
1230
  wt.status = "cleaned";
1279
1231
  for (const agent of Object.values(wt.agents)) {
1280
- if (!["completed", "failed", "killed"].includes(agent.status)) {
1281
- agent.status = "lost";
1282
- if (!agent.completedAt) agent.completedAt = now;
1283
- }
1232
+ agent.status = "gone";
1284
1233
  }
1285
1234
  }
1286
1235
  });
@@ -1356,7 +1305,6 @@ var init_agent = __esm({
1356
1305
  init_tmux();
1357
1306
  init_manifest();
1358
1307
  init_errors();
1359
- init_template();
1360
1308
  init_tmux();
1361
1309
  SHELL_COMMANDS = /* @__PURE__ */ new Set(["bash", "zsh", "sh", "fish", "dash", "tcsh", "csh"]);
1362
1310
  }
@@ -1478,6 +1426,12 @@ async function spawnCommand(options) {
1478
1426
  const count = options.count ?? 1;
1479
1427
  const userVars = parseVars(options.var ?? []);
1480
1428
  const promptText = await resolvePrompt(options, projectRoot);
1429
+ if (options.branch && options.worktree) {
1430
+ throw new PpgError("--branch and --worktree are mutually exclusive", "INVALID_ARGS");
1431
+ }
1432
+ if (options.branch && options.base) {
1433
+ throw new PpgError("--branch and --base are mutually exclusive (--base is for new branches)", "INVALID_ARGS");
1434
+ }
1481
1435
  if (options.worktree) {
1482
1436
  await spawnIntoExistingWorktree(
1483
1437
  projectRoot,
@@ -1489,6 +1443,17 @@ async function spawnCommand(options) {
1489
1443
  options,
1490
1444
  userVars
1491
1445
  );
1446
+ } else if (options.branch) {
1447
+ await spawnOnExistingBranch(
1448
+ projectRoot,
1449
+ config,
1450
+ agentConfig,
1451
+ options.branch,
1452
+ promptText,
1453
+ count,
1454
+ options,
1455
+ userVars
1456
+ );
1492
1457
  } else {
1493
1458
  await spawnNewWorktree(
1494
1459
  projectRoot,
@@ -1558,7 +1523,6 @@ async function spawnNewWorktree(projectRoot, config, agentConfig, promptText, co
1558
1523
  WORKTREE_PATH: wtPath,
1559
1524
  BRANCH: branchName,
1560
1525
  AGENT_ID: aId,
1561
- RESULT_FILE: resultFile(projectRoot, aId),
1562
1526
  PROJECT_ROOT: projectRoot,
1563
1527
  TASK_NAME: name,
1564
1528
  PROMPT: promptText
@@ -1611,6 +1575,102 @@ async function spawnNewWorktree(projectRoot, config, agentConfig, promptText, co
1611
1575
  info(`Attach: ppg attach ${wtId}`);
1612
1576
  }
1613
1577
  }
1578
+ async function spawnOnExistingBranch(projectRoot, config, agentConfig, branch, promptText, count, options, userVars) {
1579
+ const baseBranch = await getCurrentBranch(projectRoot);
1580
+ const wtId = worktreeId();
1581
+ const derivedName = branch.startsWith("ppg/") ? branch.slice(4) : branch;
1582
+ const name = options.name ? normalizeName(options.name, wtId) : normalizeName(derivedName, wtId);
1583
+ info(`Creating worktree ${wtId} from existing branch ${branch}`);
1584
+ const wtPath = await adoptWorktree(projectRoot, wtId, branch);
1585
+ await setupWorktreeEnv(projectRoot, wtPath, config);
1586
+ const manifest = await readManifest(projectRoot);
1587
+ const sessionName = manifest.sessionName;
1588
+ await ensureSession(sessionName);
1589
+ const windowTarget = await createWindow(sessionName, name, wtPath);
1590
+ const worktreeEntry = {
1591
+ id: wtId,
1592
+ name,
1593
+ path: wtPath,
1594
+ branch,
1595
+ baseBranch,
1596
+ status: "active",
1597
+ tmuxWindow: windowTarget,
1598
+ agents: {},
1599
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1600
+ };
1601
+ await updateManifest(projectRoot, (m) => {
1602
+ m.worktrees[wtId] = worktreeEntry;
1603
+ return m;
1604
+ });
1605
+ const agents = [];
1606
+ for (let i = 0; i < count; i++) {
1607
+ const aId = agentId();
1608
+ let target;
1609
+ if (i === 0) {
1610
+ target = windowTarget;
1611
+ } else if (options.split) {
1612
+ const direction = i % 2 === 1 ? "horizontal" : "vertical";
1613
+ const pane = await splitPane(windowTarget, direction, wtPath);
1614
+ target = pane.target;
1615
+ } else {
1616
+ target = await createWindow(sessionName, `${name}-${i}`, wtPath);
1617
+ }
1618
+ const ctx = {
1619
+ WORKTREE_PATH: wtPath,
1620
+ BRANCH: branch,
1621
+ AGENT_ID: aId,
1622
+ PROJECT_ROOT: projectRoot,
1623
+ TASK_NAME: name,
1624
+ PROMPT: promptText
1625
+ };
1626
+ Object.assign(ctx, userVars);
1627
+ const renderedPrompt = renderTemplate(promptText, ctx);
1628
+ const agentEntry = await spawnAgent({
1629
+ agentId: aId,
1630
+ agentConfig,
1631
+ prompt: renderedPrompt,
1632
+ worktreePath: wtPath,
1633
+ tmuxTarget: target,
1634
+ projectRoot,
1635
+ branch,
1636
+ sessionId: sessionId()
1637
+ });
1638
+ agents.push(agentEntry);
1639
+ await updateManifest(projectRoot, (m) => {
1640
+ if (m.worktrees[wtId]) {
1641
+ m.worktrees[wtId].agents[agentEntry.id] = agentEntry;
1642
+ }
1643
+ return m;
1644
+ });
1645
+ }
1646
+ if (options.open === true) {
1647
+ openTerminalWindow(sessionName, windowTarget, name).catch(() => {
1648
+ });
1649
+ }
1650
+ if (options.json) {
1651
+ output({
1652
+ success: true,
1653
+ worktree: {
1654
+ id: wtId,
1655
+ name,
1656
+ branch,
1657
+ path: wtPath,
1658
+ tmuxWindow: windowTarget
1659
+ },
1660
+ agents: agents.map((a) => ({
1661
+ id: a.id,
1662
+ tmuxTarget: a.tmuxTarget,
1663
+ sessionId: a.sessionId
1664
+ }))
1665
+ }, true);
1666
+ } else {
1667
+ success(`Spawned worktree ${wtId} from branch ${branch} with ${agents.length} agent(s)`);
1668
+ for (const a of agents) {
1669
+ info(` Agent ${a.id} \u2192 ${a.tmuxTarget}`);
1670
+ }
1671
+ info(`Attach: ppg attach ${wtId}`);
1672
+ }
1673
+ }
1614
1674
  async function spawnIntoExistingWorktree(projectRoot, config, agentConfig, worktreeRef, promptText, count, options, userVars) {
1615
1675
  const manifest = await readManifest(projectRoot);
1616
1676
  const wt = resolveWorktree(manifest, worktreeRef);
@@ -1637,7 +1697,6 @@ async function spawnIntoExistingWorktree(projectRoot, config, agentConfig, workt
1637
1697
  WORKTREE_PATH: wt.path,
1638
1698
  BRANCH: wt.branch,
1639
1699
  AGENT_ID: aId,
1640
- RESULT_FILE: resultFile(projectRoot, aId),
1641
1700
  PROJECT_ROOT: projectRoot,
1642
1701
  TASK_NAME: wt.name,
1643
1702
  PROMPT: promptText
@@ -1713,6 +1772,7 @@ var init_spawn = __esm({
1713
1772
  // src/commands/status.ts
1714
1773
  var status_exports = {};
1715
1774
  __export(status_exports, {
1775
+ computeLifecycle: () => computeLifecycle,
1716
1776
  statusCommand: () => statusCommand
1717
1777
  });
1718
1778
  async function statusCommand(worktreeFilter, options) {
@@ -1731,7 +1791,7 @@ async function statusCommand(worktreeFilter, options) {
1731
1791
  if (options?.json) {
1732
1792
  output({
1733
1793
  session: manifest.sessionName,
1734
- worktrees: Object.fromEntries(worktrees.map((wt) => [wt.id, wt]))
1794
+ worktrees: Object.fromEntries(worktrees.map((wt) => [wt.id, { ...wt, lifecycle: computeLifecycle(wt) }]))
1735
1795
  }, true);
1736
1796
  return;
1737
1797
  }
@@ -1770,12 +1830,6 @@ async function statusCommand(worktreeFilter, options) {
1770
1830
  }
1771
1831
  function printWorktreeStatus(wt) {
1772
1832
  const agents = Object.values(wt.agents);
1773
- const statusCounts = {
1774
- running: agents.filter((a) => a.status === "running").length,
1775
- completed: agents.filter((a) => a.status === "completed").length,
1776
- failed: agents.filter((a) => a.status === "failed").length,
1777
- lost: agents.filter((a) => a.status === "lost").length
1778
- };
1779
1833
  console.log(
1780
1834
  `
1781
1835
  ${wt.name} (${wt.id}) [${formatStatus(wt.status)}] branch:${wt.branch}`
@@ -1799,6 +1853,14 @@ ${wt.name} (${wt.id}) [${formatStatus(wt.status)}] branch:${wt.branch}`
1799
1853
  const table = formatTable(agents, columns);
1800
1854
  console.log(table.split("\n").map((l) => ` ${l}`).join("\n"));
1801
1855
  }
1856
+ function computeLifecycle(wt) {
1857
+ if (wt.status === "merged") return "merged";
1858
+ if (wt.status === "cleaned") return "cleaned";
1859
+ const agents = Object.values(wt.agents);
1860
+ if (agents.some((a) => a.status === "running")) return "busy";
1861
+ if (wt.prUrl) return "shipped";
1862
+ return "idle";
1863
+ }
1802
1864
  function formatTime(iso) {
1803
1865
  if (!iso) return "\u2014";
1804
1866
  const d = new Date(iso);
@@ -1999,7 +2061,7 @@ async function killSingleAgent(projectRoot, agentId2, options, selfPaneId, paneM
1999
2061
  const found = findAgent(manifest, agentId2);
2000
2062
  if (!found) throw new AgentNotFoundError(agentId2);
2001
2063
  const { agent } = found;
2002
- const isTerminal = ["completed", "failed", "killed", "lost"].includes(agent.status);
2064
+ const isTerminal = agent.status !== "running";
2003
2065
  if (selfPaneId && paneMap) {
2004
2066
  const { skipped } = excludeSelf([agent], selfPaneId, paneMap);
2005
2067
  if (skipped.length > 0) {
@@ -2042,8 +2104,7 @@ async function killSingleAgent(projectRoot, agentId2, options, selfPaneId, paneM
2042
2104
  await updateManifest(projectRoot, (m) => {
2043
2105
  const f = findAgent(m, agentId2);
2044
2106
  if (f) {
2045
- f.agent.status = "killed";
2046
- f.agent.completedAt = (/* @__PURE__ */ new Date()).toISOString();
2107
+ f.agent.status = "gone";
2047
2108
  }
2048
2109
  return m;
2049
2110
  });
@@ -2058,7 +2119,7 @@ async function killWorktreeAgents(projectRoot, worktreeRef, options, selfPaneId,
2058
2119
  const manifest = await readManifest(projectRoot);
2059
2120
  const wt = resolveWorktree(manifest, worktreeRef);
2060
2121
  if (!wt) throw new WorktreeNotFoundError(worktreeRef);
2061
- let toKill = Object.values(wt.agents).filter((a) => ["running", "spawning", "waiting"].includes(a.status));
2122
+ let toKill = Object.values(wt.agents).filter((a) => a.status === "running");
2062
2123
  const skippedIds = [];
2063
2124
  if (selfPaneId && paneMap) {
2064
2125
  const { safe, skipped } = excludeSelf(toKill, selfPaneId, paneMap);
@@ -2076,8 +2137,7 @@ async function killWorktreeAgents(projectRoot, worktreeRef, options, selfPaneId,
2076
2137
  if (mWt) {
2077
2138
  for (const agent of Object.values(mWt.agents)) {
2078
2139
  if (killedIds.includes(agent.id)) {
2079
- agent.status = "killed";
2080
- agent.completedAt = (/* @__PURE__ */ new Date()).toISOString();
2140
+ agent.status = "gone";
2081
2141
  }
2082
2142
  }
2083
2143
  }
@@ -2127,7 +2187,7 @@ async function killAllAgents(projectRoot, options, selfPaneId, paneMap) {
2127
2187
  let toKill = [];
2128
2188
  for (const wt of Object.values(manifest.worktrees)) {
2129
2189
  for (const agent of Object.values(wt.agents)) {
2130
- if (["running", "spawning", "waiting"].includes(agent.status)) {
2190
+ if (agent.status === "running") {
2131
2191
  toKill.push(agent);
2132
2192
  }
2133
2193
  }
@@ -2149,8 +2209,7 @@ async function killAllAgents(projectRoot, options, selfPaneId, paneMap) {
2149
2209
  for (const wt of Object.values(m.worktrees)) {
2150
2210
  for (const agent of Object.values(wt.agents)) {
2151
2211
  if (killedIds.includes(agent.id)) {
2152
- agent.status = "killed";
2153
- agent.completedAt = (/* @__PURE__ */ new Date()).toISOString();
2212
+ agent.status = "gone";
2154
2213
  }
2155
2214
  }
2156
2215
  }
@@ -2396,13 +2455,12 @@ async function aggregateCommand(worktreeId2, options) {
2396
2455
  worktrees = [wt];
2397
2456
  } else {
2398
2457
  worktrees = Object.values(manifest.worktrees).filter(
2399
- (wt) => Object.values(wt.agents).some((a) => a.status === "completed")
2458
+ (wt) => Object.values(wt.agents).some((a) => a.status !== "running")
2400
2459
  );
2401
2460
  }
2402
2461
  const results = [];
2403
2462
  for (const wt of worktrees) {
2404
2463
  for (const agent of Object.values(wt.agents)) {
2405
- if (agent.status !== "completed" && agent.status !== "failed") continue;
2406
2464
  const result = await collectAgentResult(agent, projectRoot);
2407
2465
  results.push({
2408
2466
  agentId: agent.id,
@@ -2418,7 +2476,7 @@ async function aggregateCommand(worktreeId2, options) {
2418
2476
  if (options?.json) {
2419
2477
  output({ results: [] }, true);
2420
2478
  } else {
2421
- console.log("No completed agent results to aggregate.");
2479
+ console.log("No agent results to aggregate.");
2422
2480
  }
2423
2481
  return;
2424
2482
  }
@@ -2447,20 +2505,18 @@ async function aggregateCommand(worktreeId2, options) {
2447
2505
  }
2448
2506
  }
2449
2507
  async function collectAgentResult(agent, projectRoot) {
2450
- try {
2451
- const content = await fs9.readFile(agent.resultFile, "utf-8");
2452
- return content;
2453
- } catch {
2454
- }
2455
2508
  try {
2456
2509
  const paneContent = await capturePane(agent.tmuxTarget, 500);
2457
- return `*[No result file \u2014 pane capture fallback]*
2458
-
2459
- \`\`\`
2510
+ return `\`\`\`
2460
2511
  ${paneContent}
2461
2512
  \`\`\``;
2462
2513
  } catch {
2463
- return "*[No result file and pane not available]*";
2514
+ }
2515
+ try {
2516
+ const content = await fs9.readFile(resultFile(projectRoot, agent.id), "utf-8");
2517
+ return content;
2518
+ } catch {
2519
+ return "*[Pane not available and no legacy result file]*";
2464
2520
  }
2465
2521
  }
2466
2522
  var init_aggregate = __esm({
@@ -2470,6 +2526,7 @@ var init_aggregate = __esm({
2470
2526
  init_agent();
2471
2527
  init_worktree();
2472
2528
  init_tmux();
2529
+ init_paths();
2473
2530
  init_errors();
2474
2531
  init_output();
2475
2532
  }
@@ -2490,7 +2547,7 @@ async function mergeCommand(worktreeId2, options) {
2490
2547
  const wt = resolveWorktree(manifest, worktreeId2);
2491
2548
  if (!wt) throw new WorktreeNotFoundError(worktreeId2);
2492
2549
  const agents = Object.values(wt.agents);
2493
- const incomplete = agents.filter((a) => !["completed", "failed", "killed"].includes(a.status));
2550
+ const incomplete = agents.filter((a) => a.status === "running");
2494
2551
  if (incomplete.length > 0 && !options.force) {
2495
2552
  const ids = incomplete.map((a) => a.id).join(", ");
2496
2553
  throw new PpgError(
@@ -2753,7 +2810,6 @@ async function spawnSwarmAgent(opts) {
2753
2810
  WORKTREE_PATH: wtPath,
2754
2811
  BRANCH: branchName,
2755
2812
  AGENT_ID: aId,
2756
- RESULT_FILE: resultFile(projectRoot, aId),
2757
2813
  PROJECT_ROOT: projectRoot,
2758
2814
  TASK_NAME: taskName,
2759
2815
  ...swarmAgent.vars ?? {},
@@ -3213,7 +3269,7 @@ async function restartCommand(agentRef, options) {
3213
3269
  const found = findAgent(manifest, agentRef);
3214
3270
  if (!found) throw new AgentNotFoundError(agentRef);
3215
3271
  const { worktree: wt, agent: oldAgent } = found;
3216
- if (["running", "spawning", "waiting"].includes(oldAgent.status)) {
3272
+ if (oldAgent.status === "running") {
3217
3273
  info(`Killing existing agent ${oldAgent.id}`);
3218
3274
  await killAgent(oldAgent);
3219
3275
  }
@@ -3239,7 +3295,6 @@ async function restartCommand(agentRef, options) {
3239
3295
  WORKTREE_PATH: wt.path,
3240
3296
  BRANCH: wt.branch,
3241
3297
  AGENT_ID: newAgentId,
3242
- RESULT_FILE: resultFile(projectRoot, newAgentId),
3243
3298
  PROJECT_ROOT: projectRoot,
3244
3299
  TASK_NAME: wt.name,
3245
3300
  PROMPT: promptText
@@ -3254,16 +3309,14 @@ async function restartCommand(agentRef, options) {
3254
3309
  tmuxTarget: windowTarget,
3255
3310
  projectRoot,
3256
3311
  branch: wt.branch,
3257
- sessionId: newSessionId,
3258
- skipResultInstructions: !options.prompt
3312
+ sessionId: newSessionId
3259
3313
  });
3260
3314
  await updateManifest(projectRoot, (m) => {
3261
3315
  const mWt = m.worktrees[wt.id];
3262
3316
  if (mWt) {
3263
3317
  const mOldAgent = mWt.agents[oldAgent.id];
3264
- if (mOldAgent && !["completed", "failed", "killed", "lost"].includes(mOldAgent.status)) {
3265
- mOldAgent.status = "killed";
3266
- mOldAgent.completedAt = (/* @__PURE__ */ new Date()).toISOString();
3318
+ if (mOldAgent && mOldAgent.status === "running") {
3319
+ mOldAgent.status = "gone";
3267
3320
  }
3268
3321
  mWt.agents[newAgentId] = agentEntry;
3269
3322
  }
@@ -3366,7 +3419,6 @@ __export(pr_exports, {
3366
3419
  prCommand: () => prCommand,
3367
3420
  truncateBody: () => truncateBody
3368
3421
  });
3369
- import fs15 from "fs/promises";
3370
3422
  import { execa as execa8 } from "execa";
3371
3423
  async function prCommand(worktreeRef, options) {
3372
3424
  const projectRoot = await getRepoRoot();
@@ -3441,16 +3493,11 @@ async function prCommand(worktreeRef, options) {
3441
3493
  }
3442
3494
  }
3443
3495
  async function buildBodyFromResults(agents) {
3444
- const reads = agents.map(async (agent) => {
3445
- try {
3446
- return await fs15.readFile(agent.resultFile, "utf-8");
3447
- } catch {
3448
- return null;
3449
- }
3450
- });
3451
- const contents = (await Promise.all(reads)).filter((c) => c !== null);
3452
- if (contents.length === 0) return "";
3453
- return truncateBody(contents.join("\n\n---\n\n"));
3496
+ if (agents.length === 0) return "";
3497
+ const sections = agents.map((a) => `## Agent: ${a.id}
3498
+
3499
+ ${a.prompt}`);
3500
+ return truncateBody(sections.join("\n\n---\n\n"));
3454
3501
  }
3455
3502
  function truncateBody(body) {
3456
3503
  if (body.length <= MAX_BODY_LENGTH) return body;
@@ -3480,7 +3527,7 @@ function findAtRiskWorktrees(worktrees) {
3480
3527
  return worktrees.filter((wt) => {
3481
3528
  if (wt.status === "merged" || wt.status === "cleaned") return false;
3482
3529
  if (wt.prUrl) return false;
3483
- return Object.values(wt.agents).some((a) => a.status === "completed");
3530
+ return Object.values(wt.agents).some((a) => a.status === "idle" || a.status === "exited");
3484
3531
  });
3485
3532
  }
3486
3533
  async function resetCommand(options) {
@@ -3517,7 +3564,7 @@ async function resetCommand(options) {
3517
3564
  const allAgents = [];
3518
3565
  for (const wt of worktrees) {
3519
3566
  for (const agent of Object.values(wt.agents)) {
3520
- if (["running", "spawning", "waiting"].includes(agent.status)) {
3567
+ if (agent.status === "running") {
3521
3568
  allAgents.push(agent);
3522
3569
  }
3523
3570
  }
@@ -3539,12 +3586,10 @@ async function resetCommand(options) {
3539
3586
  }
3540
3587
  if (killedIds.length > 0) {
3541
3588
  await updateManifest(projectRoot, (m) => {
3542
- const now = (/* @__PURE__ */ new Date()).toISOString();
3543
3589
  for (const wt of Object.values(m.worktrees)) {
3544
3590
  for (const agent of Object.values(wt.agents)) {
3545
3591
  if (killedIds.includes(agent.id)) {
3546
- agent.status = "killed";
3547
- agent.completedAt = now;
3592
+ agent.status = "gone";
3548
3593
  }
3549
3594
  }
3550
3595
  }
@@ -3847,9 +3892,9 @@ async function waitCommand(worktreeRef, options) {
3847
3892
  }
3848
3893
  const manifest = await refreshAndGet(projectRoot);
3849
3894
  const agents = collectAgents(manifest, worktreeRef, options.all);
3850
- const allTerminal = agents.every((a) => TERMINAL_STATUSES.includes(a.status));
3851
- if (allTerminal) {
3852
- const anyFailed = agents.some((a) => ["failed", "lost"].includes(a.status));
3895
+ const allDone = agents.every((a) => a.status !== "running");
3896
+ if (allDone) {
3897
+ const anyFailed = agents.some((a) => a.status === "exited" && a.exitCode !== void 0 && a.exitCode !== 0);
3853
3898
  if (options.json) {
3854
3899
  output({
3855
3900
  timedOut: false,
@@ -3892,11 +3937,9 @@ function formatAgent(a) {
3892
3937
  status: a.status,
3893
3938
  agentType: a.agentType,
3894
3939
  exitCode: a.exitCode,
3895
- startedAt: a.startedAt,
3896
- completedAt: a.completedAt
3940
+ startedAt: a.startedAt
3897
3941
  };
3898
3942
  }
3899
- var TERMINAL_STATUSES;
3900
3943
  var init_wait = __esm({
3901
3944
  "src/commands/wait.ts"() {
3902
3945
  "use strict";
@@ -3905,7 +3948,6 @@ var init_wait = __esm({
3905
3948
  init_worktree();
3906
3949
  init_errors();
3907
3950
  init_output();
3908
- TERMINAL_STATUSES = ["completed", "failed", "killed", "lost"];
3909
3951
  }
3910
3952
  });
3911
3953
 
@@ -3915,18 +3957,34 @@ __export(worktree_exports, {
3915
3957
  worktreeCreateCommand: () => worktreeCreateCommand
3916
3958
  });
3917
3959
  async function worktreeCreateCommand(options) {
3960
+ if (options.branch && options.base) {
3961
+ throw new PpgError("--branch and --base are mutually exclusive (--base is for new branches)", "INVALID_ARGS");
3962
+ }
3918
3963
  const projectRoot = await getRepoRoot();
3919
3964
  const config = await loadConfig(projectRoot);
3920
3965
  await requireManifest(projectRoot);
3921
- const baseBranch = options.base ?? await getCurrentBranch(projectRoot);
3922
3966
  const wtId = worktreeId();
3923
- const name = options.name ? normalizeName(options.name, wtId) : wtId;
3924
- const branchName = `ppg/${name}`;
3925
- info(`Creating worktree ${wtId} on branch ${branchName}`);
3926
- const wtPath = await createWorktree(projectRoot, wtId, {
3927
- branch: branchName,
3928
- base: baseBranch
3929
- });
3967
+ let name;
3968
+ let branchName;
3969
+ let baseBranch;
3970
+ let wtPath;
3971
+ if (options.branch) {
3972
+ branchName = options.branch;
3973
+ const derivedName = branchName.startsWith("ppg/") ? branchName.slice(4) : branchName;
3974
+ name = options.name ? normalizeName(options.name, wtId) : normalizeName(derivedName, wtId);
3975
+ baseBranch = await getCurrentBranch(projectRoot);
3976
+ info(`Creating worktree ${wtId} from existing branch ${branchName}`);
3977
+ wtPath = await adoptWorktree(projectRoot, wtId, branchName);
3978
+ } else {
3979
+ baseBranch = options.base ?? await getCurrentBranch(projectRoot);
3980
+ name = options.name ? normalizeName(options.name, wtId) : wtId;
3981
+ branchName = `ppg/${name}`;
3982
+ info(`Creating worktree ${wtId} on branch ${branchName}`);
3983
+ wtPath = await createWorktree(projectRoot, wtId, {
3984
+ branch: branchName,
3985
+ base: baseBranch
3986
+ });
3987
+ }
3930
3988
  await setupWorktreeEnv(projectRoot, wtPath, config);
3931
3989
  const worktreeEntry = {
3932
3990
  id: wtId,
@@ -3968,6 +4026,7 @@ var init_worktree2 = __esm({
3968
4026
  init_worktree();
3969
4027
  init_env2();
3970
4028
  init_id();
4029
+ init_errors();
3971
4030
  init_output();
3972
4031
  init_name();
3973
4032
  }
@@ -4162,14 +4221,14 @@ var init_install_dashboard = __esm({
4162
4221
  });
4163
4222
 
4164
4223
  // src/core/schedule.ts
4165
- import fs16 from "fs/promises";
4224
+ import fs15 from "fs/promises";
4166
4225
  import YAML3 from "yaml";
4167
4226
  import { CronExpressionParser } from "cron-parser";
4168
4227
  async function loadSchedules(projectRoot) {
4169
4228
  const filePath = schedulesPath(projectRoot);
4170
4229
  let raw;
4171
4230
  try {
4172
- raw = await fs16.readFile(filePath, "utf-8");
4231
+ raw = await fs15.readFile(filePath, "utf-8");
4173
4232
  } catch (err) {
4174
4233
  if (err.code === "ENOENT") {
4175
4234
  throw new PpgError("No schedules file found. Create .ppg/schedules.yaml first.", "INVALID_ARGS");
@@ -4254,16 +4313,16 @@ var init_schedule = __esm({
4254
4313
  });
4255
4314
 
4256
4315
  // src/core/cron.ts
4257
- import fs17 from "fs/promises";
4316
+ import fs16 from "fs/promises";
4258
4317
  import { createReadStream } from "fs";
4259
4318
  import path11 from "path";
4260
4319
  import readline from "readline";
4261
4320
  import { execa as execa11 } from "execa";
4262
4321
  async function runCronDaemon(projectRoot) {
4263
4322
  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 });
4323
+ await fs16.mkdir(path11.dirname(pidPath), { recursive: true });
4324
+ await fs16.writeFile(pidPath, String(process.pid), "utf-8");
4325
+ await fs16.mkdir(logsDir(projectRoot), { recursive: true });
4267
4326
  await logCron(projectRoot, "Cron daemon starting");
4268
4327
  let states = await loadScheduleStates(projectRoot);
4269
4328
  let lastConfigMtime = await getFileMtime(schedulesPath(projectRoot));
@@ -4274,7 +4333,7 @@ async function runCronDaemon(projectRoot) {
4274
4333
  const cleanup = async () => {
4275
4334
  await logCron(projectRoot, "Cron daemon stopping");
4276
4335
  try {
4277
- await fs17.unlink(pidPath);
4336
+ await fs16.unlink(pidPath);
4278
4337
  } catch {
4279
4338
  }
4280
4339
  process.exit(0);
@@ -4318,7 +4377,7 @@ async function loadScheduleStates(projectRoot) {
4318
4377
  }
4319
4378
  async function getFileMtime(filePath) {
4320
4379
  try {
4321
- const stat = await fs17.stat(filePath);
4380
+ const stat = await fs16.stat(filePath);
4322
4381
  return stat.mtimeMs;
4323
4382
  } catch {
4324
4383
  return 0;
@@ -4373,10 +4432,10 @@ async function logCron(projectRoot, message) {
4373
4432
  `;
4374
4433
  process.stdout.write(line);
4375
4434
  try {
4376
- await fs17.appendFile(logPath, line, "utf-8");
4435
+ await fs16.appendFile(logPath, line, "utf-8");
4377
4436
  } catch {
4378
- await fs17.mkdir(logsDir(projectRoot), { recursive: true });
4379
- await fs17.appendFile(logPath, line, "utf-8");
4437
+ await fs16.mkdir(logsDir(projectRoot), { recursive: true });
4438
+ await fs16.appendFile(logPath, line, "utf-8");
4380
4439
  }
4381
4440
  }
4382
4441
  async function isCronRunning(projectRoot) {
@@ -4386,7 +4445,7 @@ async function getCronPid(projectRoot) {
4386
4445
  const pidPath = cronPidPath(projectRoot);
4387
4446
  let raw;
4388
4447
  try {
4389
- raw = await fs17.readFile(pidPath, "utf-8");
4448
+ raw = await fs16.readFile(pidPath, "utf-8");
4390
4449
  } catch {
4391
4450
  return null;
4392
4451
  }
@@ -4405,14 +4464,14 @@ async function getCronPid(projectRoot) {
4405
4464
  }
4406
4465
  async function cleanupPidFile(pidPath) {
4407
4466
  try {
4408
- await fs17.unlink(pidPath);
4467
+ await fs16.unlink(pidPath);
4409
4468
  } catch {
4410
4469
  }
4411
4470
  }
4412
4471
  async function readCronLog(projectRoot, lines = 20) {
4413
4472
  const logPath = cronLogPath(projectRoot);
4414
4473
  try {
4415
- await fs17.access(logPath);
4474
+ await fs16.access(logPath);
4416
4475
  } catch {
4417
4476
  return [];
4418
4477
  }
@@ -4443,13 +4502,17 @@ var init_cron = __esm({
4443
4502
  // src/commands/cron.ts
4444
4503
  var cron_exports = {};
4445
4504
  __export(cron_exports, {
4505
+ cronAddCommand: () => cronAddCommand,
4446
4506
  cronDaemonCommand: () => cronDaemonCommand,
4447
4507
  cronListCommand: () => cronListCommand,
4508
+ cronRemoveCommand: () => cronRemoveCommand,
4448
4509
  cronStartCommand: () => cronStartCommand,
4449
4510
  cronStatusCommand: () => cronStatusCommand,
4450
4511
  cronStopCommand: () => cronStopCommand
4451
4512
  });
4452
- import fs18 from "fs/promises";
4513
+ import fs17 from "fs/promises";
4514
+ import path12 from "path";
4515
+ import YAML4 from "yaml";
4453
4516
  async function cronStartCommand(options) {
4454
4517
  const projectRoot = await getRepoRoot();
4455
4518
  await requireInit(projectRoot);
@@ -4500,7 +4563,7 @@ async function cronStopCommand(options) {
4500
4563
  } catch {
4501
4564
  }
4502
4565
  try {
4503
- await fs18.unlink(cronPidPath(projectRoot));
4566
+ await fs17.unlink(cronPidPath(projectRoot));
4504
4567
  } catch {
4505
4568
  }
4506
4569
  try {
@@ -4584,9 +4647,119 @@ async function cronDaemonCommand() {
4584
4647
  await requireInit(projectRoot);
4585
4648
  await runCronDaemon(projectRoot);
4586
4649
  }
4650
+ async function cronAddCommand(options) {
4651
+ const projectRoot = options.project ?? await getRepoRoot();
4652
+ await requireInit(projectRoot);
4653
+ const SAFE_NAME3 = /^[\w-]+$/;
4654
+ if (!options.name || !SAFE_NAME3.test(options.name)) {
4655
+ throw new PpgError(
4656
+ `Invalid schedule name "${options.name}" \u2014 must be alphanumeric, hyphens, or underscores`,
4657
+ "INVALID_ARGS"
4658
+ );
4659
+ }
4660
+ if (!options.swarm && !options.prompt) {
4661
+ throw new PpgError("Must specify either --swarm or --prompt", "INVALID_ARGS");
4662
+ }
4663
+ if (options.swarm && options.prompt) {
4664
+ throw new PpgError("Specify either --swarm or --prompt, not both", "INVALID_ARGS");
4665
+ }
4666
+ validateCronExpression(options.cron);
4667
+ const entry = {
4668
+ name: options.name,
4669
+ cron: options.cron
4670
+ };
4671
+ if (options.swarm) entry.swarm = options.swarm;
4672
+ if (options.prompt) entry.prompt = options.prompt;
4673
+ if (options.var && options.var.length > 0) {
4674
+ const vars = {};
4675
+ for (const v of options.var) {
4676
+ const eqIdx = v.indexOf("=");
4677
+ if (eqIdx === -1) {
4678
+ throw new PpgError(`Invalid --var format: "${v}" (expected KEY=VALUE)`, "INVALID_ARGS");
4679
+ }
4680
+ const key = v.slice(0, eqIdx);
4681
+ if (key.length === 0) {
4682
+ throw new PpgError(`Invalid --var format: "${v}" (key must not be empty)`, "INVALID_ARGS");
4683
+ }
4684
+ vars[key] = v.slice(eqIdx + 1);
4685
+ }
4686
+ entry.vars = vars;
4687
+ }
4688
+ const filePath = schedulesPath(projectRoot);
4689
+ const ppgDirPath = path12.dirname(filePath);
4690
+ await fs17.mkdir(ppgDirPath, { recursive: true });
4691
+ await updateSchedulesFile(filePath, (config) => {
4692
+ if (config.schedules.some((s) => s.name === options.name)) {
4693
+ throw new PpgError(`Schedule "${options.name}" already exists`, "INVALID_ARGS");
4694
+ }
4695
+ config.schedules.push(entry);
4696
+ return config;
4697
+ });
4698
+ if (options.json) {
4699
+ output({ success: true, name: options.name, schedule: entry }, true);
4700
+ } else {
4701
+ success(`Schedule "${options.name}" added`);
4702
+ }
4703
+ }
4704
+ async function cronRemoveCommand(options) {
4705
+ const projectRoot = options.project ?? await getRepoRoot();
4706
+ await requireInit(projectRoot);
4707
+ const filePath = schedulesPath(projectRoot);
4708
+ await updateSchedulesFile(filePath, (config) => {
4709
+ const idx = config.schedules.findIndex((s) => s.name === options.name);
4710
+ if (idx === -1) {
4711
+ throw new PpgError(`Schedule "${options.name}" not found`, "INVALID_ARGS");
4712
+ }
4713
+ config.schedules.splice(idx, 1);
4714
+ return config;
4715
+ });
4716
+ if (options.json) {
4717
+ output({ success: true, name: options.name }, true);
4718
+ } else {
4719
+ success(`Schedule "${options.name}" removed`);
4720
+ }
4721
+ }
4722
+ async function updateSchedulesFile(filePath, updater) {
4723
+ const lockfile = await getLockfile();
4724
+ const writeFileAtomic = await getWriteFileAtomic();
4725
+ let release;
4726
+ try {
4727
+ release = await lockfile.lock(filePath, {
4728
+ stale: 1e4,
4729
+ retries: { retries: 5, minTimeout: 100, maxTimeout: 1e3 },
4730
+ realpath: false
4731
+ });
4732
+ } catch {
4733
+ throw new PpgError("Could not acquire lock on schedules.yaml", "MANIFEST_LOCK");
4734
+ }
4735
+ try {
4736
+ let config;
4737
+ try {
4738
+ const raw = await fs17.readFile(filePath, "utf-8");
4739
+ const parsed = YAML4.parse(raw);
4740
+ if (!parsed || !Array.isArray(parsed.schedules)) {
4741
+ throw new PpgError(
4742
+ 'Invalid schedules.yaml: missing or malformed "schedules" array. Fix the file manually or delete it to start fresh.',
4743
+ "INVALID_ARGS"
4744
+ );
4745
+ }
4746
+ config = parsed;
4747
+ } catch (err) {
4748
+ if (err.code === "ENOENT") {
4749
+ config = { schedules: [] };
4750
+ } else {
4751
+ throw err;
4752
+ }
4753
+ }
4754
+ const updated = updater(config);
4755
+ await writeFileAtomic(filePath, YAML4.stringify(updated));
4756
+ } finally {
4757
+ if (release) await release();
4758
+ }
4759
+ }
4587
4760
  async function requireInit(projectRoot) {
4588
4761
  try {
4589
- await fs18.access(manifestPath(projectRoot));
4762
+ await fs17.access(manifestPath(projectRoot));
4590
4763
  } catch {
4591
4764
  throw new NotInitializedError(projectRoot);
4592
4765
  }
@@ -4601,6 +4774,7 @@ var init_cron2 = __esm({
4601
4774
  init_cron();
4602
4775
  init_tmux();
4603
4776
  init_paths();
4777
+ init_cjs_compat();
4604
4778
  init_errors();
4605
4779
  init_output();
4606
4780
  CRON_WINDOW_NAME = "ppg-cron";
@@ -4620,7 +4794,7 @@ program.command("init").description("Initialize Point Guard in the current git r
4620
4794
  const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
4621
4795
  await initCommand2(options);
4622
4796
  });
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) => {
4797
+ 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("--branch <name>", "Check out an existing branch into a new 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) => {
4624
4798
  const { spawnCommand: spawnCommand2 } = await Promise.resolve().then(() => (init_spawn(), spawn_exports));
4625
4799
  await spawnCommand2(options);
4626
4800
  });
@@ -4640,11 +4814,11 @@ program.command("logs").description("View agent pane output").argument("<agent-i
4640
4814
  const { logsCommand: logsCommand2 } = await Promise.resolve().then(() => (init_logs(), logs_exports));
4641
4815
  await logsCommand2(agentId2, options);
4642
4816
  });
4643
- program.command("aggregate").description("Aggregate results from completed agents").argument("[worktree-id]", "Worktree ID to aggregate results from").option("--all", "Aggregate from all worktrees").option("-o, --output <file>", "Write output to file").option("--json", "Output as JSON").action(async (worktreeId2, options) => {
4817
+ program.command("aggregate").description("Aggregate results from agents (captures pane output)").argument("[worktree-id]", "Worktree ID to aggregate results from").option("--all", "Aggregate from all worktrees").option("-o, --output <file>", "Write output to file").option("--json", "Output as JSON").action(async (worktreeId2, options) => {
4644
4818
  const { aggregateCommand: aggregateCommand2 } = await Promise.resolve().then(() => (init_aggregate(), aggregate_exports));
4645
4819
  await aggregateCommand2(worktreeId2, options);
4646
4820
  });
4647
- program.command("merge").description("Merge a worktree branch back into base").argument("<worktree-id>", "Worktree ID to merge").option("-s, --strategy <strategy>", "Merge strategy: squash or no-ff", "squash").option("--no-cleanup", "Do not remove worktree after merge").option("--dry-run", "Show what would be done without doing it").option("--force", "Merge even if agents are not completed").option("--json", "Output as JSON").action(async (worktreeId2, options) => {
4821
+ program.command("merge").description("Merge a worktree branch back into base").argument("<worktree-id>", "Worktree ID to merge").option("-s, --strategy <strategy>", "Merge strategy: squash or no-ff", "squash").option("--no-cleanup", "Do not remove worktree after merge").option("--dry-run", "Show what would be done without doing it").option("--force", "Merge even if agents are still running").option("--json", "Output as JSON").action(async (worktreeId2, options) => {
4648
4822
  const { mergeCommand: mergeCommand2 } = await Promise.resolve().then(() => (init_merge(), merge_exports));
4649
4823
  await mergeCommand2(worktreeId2, options);
4650
4824
  });
@@ -4660,7 +4834,7 @@ program.command("list").description("List available templates, swarms, or prompt
4660
4834
  const { listCommand: listCommand2 } = await Promise.resolve().then(() => (init_list(), list_exports));
4661
4835
  await listCommand2(type, options);
4662
4836
  });
4663
- program.command("restart").description("Restart a failed/killed agent in the same worktree").argument("<agent-id>", "Agent ID to restart").option("-p, --prompt <text>", "Override the original prompt").option("-a, --agent <type>", "Override the agent type").option("--open", "Open a Terminal window for the restarted agent").option("--json", "Output as JSON").action(async (agentId2, options) => {
4837
+ program.command("restart").description("Restart an agent in the same worktree").argument("<agent-id>", "Agent ID to restart").option("-p, --prompt <text>", "Override the original prompt").option("-a, --agent <type>", "Override the agent type").option("--open", "Open a Terminal window for the restarted agent").option("--json", "Output as JSON").action(async (agentId2, options) => {
4664
4838
  const { restartCommand: restartCommand2 } = await Promise.resolve().then(() => (init_restart(), restart_exports));
4665
4839
  await restartCommand2(agentId2, options);
4666
4840
  });
@@ -4689,7 +4863,7 @@ program.command("wait").description("Wait for agents to reach terminal state").a
4689
4863
  await waitCommand2(worktreeId2, options);
4690
4864
  });
4691
4865
  var worktreeCmd = program.command("worktree").description("Manage worktrees");
4692
- worktreeCmd.command("create").description("Create a standalone worktree without spawning agents").option("-n, --name <name>", "Name for the worktree").option("-b, --base <branch>", "Base branch for the worktree").option("--json", "Output as JSON").action(async (options) => {
4866
+ worktreeCmd.command("create").description("Create a standalone worktree without spawning agents").option("-n, --name <name>", "Name for the worktree").option("-b, --base <branch>", "Base branch for the worktree").option("--branch <name>", "Check out an existing branch into a new worktree").option("--json", "Output as JSON").action(async (options) => {
4693
4867
  const { worktreeCreateCommand: worktreeCreateCommand2 } = await Promise.resolve().then(() => (init_worktree2(), worktree_exports));
4694
4868
  await worktreeCreateCommand2(options);
4695
4869
  });
@@ -4722,6 +4896,14 @@ cronCmd.command("_daemon", { hidden: true }).description("Internal: run the cron
4722
4896
  const { cronDaemonCommand: cronDaemonCommand2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports));
4723
4897
  await cronDaemonCommand2();
4724
4898
  });
4899
+ cronCmd.command("add").description("Add a new schedule entry").requiredOption("--name <name>", "Schedule name").requiredOption("--cron <expression>", "Cron expression").option("--swarm <name>", "Swarm template name").option("--prompt <name>", "Prompt template name").option("--var <key=value...>", "Template variables", collectVars, []).option("--project <path>", "Project root path").option("--json", "Output as JSON").action(async (options) => {
4900
+ const { cronAddCommand: cronAddCommand2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports));
4901
+ await cronAddCommand2(options);
4902
+ });
4903
+ cronCmd.command("remove").description("Remove a schedule entry").requiredOption("--name <name>", "Schedule name to remove").option("--project <path>", "Project root path").option("--json", "Output as JSON").action(async (options) => {
4904
+ const { cronRemoveCommand: cronRemoveCommand2 } = await Promise.resolve().then(() => (init_cron2(), cron_exports));
4905
+ await cronRemoveCommand2(options);
4906
+ });
4725
4907
  program.exitOverride();
4726
4908
  function collectVars(value, previous) {
4727
4909
  return previous.concat([value]);