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 +370 -188
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/skills/ppg-conductor/SKILL.md +5 -0
- package/skills/ppg-conductor/references/commands.md +32 -0
- package/skills/ppg-conductor/references/conductor.md +52 -5
- package/skills/ppg-conductor/references/modes.md +9 -0
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
|
|
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,
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
##
|
|
981
|
-
|
|
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,
|
|
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,
|
|
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: "
|
|
1199
|
+
return { status: "gone" };
|
|
1233
1200
|
}
|
|
1234
1201
|
if (paneInfo.isDead) {
|
|
1235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1268
|
-
|
|
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
|
-
|
|
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 =
|
|
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 = "
|
|
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) =>
|
|
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 = "
|
|
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 (
|
|
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 = "
|
|
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
|
|
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
|
|
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
|
|
2458
|
-
|
|
2459
|
-
\`\`\`
|
|
2510
|
+
return `\`\`\`
|
|
2460
2511
|
${paneContent}
|
|
2461
2512
|
\`\`\``;
|
|
2462
2513
|
} catch {
|
|
2463
|
-
|
|
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) =>
|
|
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 (
|
|
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 &&
|
|
3265
|
-
mOldAgent.status = "
|
|
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
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
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 === "
|
|
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 (
|
|
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 = "
|
|
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
|
|
3851
|
-
if (
|
|
3852
|
-
const anyFailed = agents.some((a) =>
|
|
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
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4265
|
-
await
|
|
4266
|
-
await
|
|
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
|
|
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
|
|
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
|
|
4435
|
+
await fs16.appendFile(logPath, line, "utf-8");
|
|
4377
4436
|
} catch {
|
|
4378
|
-
await
|
|
4379
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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]);
|