Skip to content

Commit d6b3d06

Browse files
ziggyclaude
andcommitted
Add pre_check to scheduler and PR babysitter skill (v0.5.3)
Scheduler pre_check feature: - Scheduled tasks can now include an optional `pre_check` bash script - Script runs before spawning the agent (zero API cost) - Exit 0 + no output = skip agent (nothing to do) - Exit 0 + output = pass output to agent via {{pre_check_output}} - Exit non-zero = spawn agent to investigate - Saves API credits on polling tasks that usually find nothing PR babysitter skill (/pr-babysitter): - Monitors GitHub repos for PRs with failing CI or unanswered reviews - Uses pre_check with `gh pr list` to only spawn agent when needed - Auto-fixes high-confidence issues (linting, type errors, simple tests) - Messages user when human judgement is needed - Configurable: repos, check interval, auto-fix behaviour Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dc183d2 commit d6b3d06

7 files changed

Lines changed: 181 additions & 4 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
name: pr-babysitter
3+
description: Set up automated PR monitoring. Watches GitHub repos for PRs with failing CI or unanswered review comments, fixes high-confidence issues automatically, and messages you when human input is needed. Triggers on "pr babysitter", "watch PRs", "monitor PRs", "pr monitor".
4+
---
5+
6+
# PR Babysitter
7+
8+
Automated PR monitoring that only spawns an agent when there's actual work to do. Uses the scheduler's `pre_check` feature to run a lightweight `gh` CLI check first — zero API credits burned when all PRs are clean.
9+
10+
## Setup
11+
12+
### 1. Verify GitHub CLI
13+
14+
```bash
15+
gh auth status
16+
```
17+
18+
If not authenticated, run `gh auth login`.
19+
20+
### 2. Ask the user
21+
22+
Use `AskUserQuestion` for each:
23+
24+
1. **Which repos to watch?** Accept either:
25+
- Specific repos: `owner/repo1 owner/repo2`
26+
- All repos: `gh repo list --json nameWithOwner -q '.[].nameWithOwner'`
27+
2. **How often to check?** Default: every 30 minutes (`*/30 * * * *`)
28+
3. **What to watch for?** Default: all of the below. Let user pick:
29+
- Failing CI checks
30+
- Unanswered review comments (especially from AI reviewers)
31+
- Stale PRs (no activity for 48h+)
32+
4. **Auto-fix?** Should the agent attempt high-confidence fixes (typos, linting, simple test fixes) or always ask first?
33+
34+
### 3. Create the scheduled task
35+
36+
Build a pre_check script and prompt based on user choices. Example for watching specific repos:
37+
38+
```bash
39+
cat > $GHOSTCLAW_IPC_DIR/tasks/pr-babysitter_$(date +%s).json << 'TASKEOF'
40+
{
41+
"type": "schedule_task",
42+
"pre_check": "REPOS=\"owner/repo1 owner/repo2\"; for repo in $REPOS; do gh pr list --repo $repo --state open --json number,title,headRefName,statusCheckRollup,reviewDecision,updatedAt --jq '.[] | select(.statusCheckRollup == \"FAILURE\" or .reviewDecision == \"CHANGES_REQUESTED\" or (.reviewDecision == \"\" and (.statusCheckRollup == \"PENDING\" or .statusCheckRollup == \"\")))' 2>/dev/null; done",
43+
"prompt": "You are a PR babysitter. These PRs need attention:\n\n{{pre_check_output}}\n\nFor each PR:\n1. Check CI status: `gh pr checks <number> --repo <repo>`\n2. Read review comments: `gh api repos/<owner>/<repo>/pulls/<number>/comments`\n3. If CI fails with a clear fix (linting, type error, simple test fix):\n - Clone the repo, checkout the branch, fix it, push\n - Comment on the PR: \"Fixed [issue] automatically\"\n4. If review comments are from AI reviewers and the suggestion is high-confidence:\n - Implement the change, push, reply to the comment\n5. If it needs human judgement:\n - Message me with: repo, PR number, what's wrong, what you think the fix is\n\nBe conservative. Only auto-fix things you're confident about. When in doubt, ask.",
44+
"schedule_type": "cron",
45+
"schedule_value": "*/30 * * * *",
46+
"context_mode": "isolated",
47+
"targetJid": "TARGET_JID"
48+
}
49+
TASKEOF
50+
```
51+
52+
Replace `TARGET_JID` with the user's chat JID (get from registered groups).
53+
54+
### 4. Confirm setup
55+
56+
Tell the user:
57+
- PR babysitter is active
58+
- Checking every [interval] for [repos]
59+
- Will auto-fix: [yes/no]
60+
- To pause: "pause the PR babysitter" or "stop watching PRs"
61+
- To check now: "check my PRs"
62+
63+
## Pre-check script details
64+
65+
The `pre_check` runs as a bash script before spawning the agent:
66+
- **Exit 0 + no output** → nothing to do, agent not spawned (free)
67+
- **Exit 0 + output** → PRs found, output passed to agent as `{{pre_check_output}}`
68+
- **Exit non-zero** → something's wrong, agent spawned to investigate
69+
70+
The `gh pr list` with `--jq` filter only outputs PRs that actually need attention. If all PRs are green with no pending reviews, the output is empty and the agent is skipped entirely.
71+
72+
## Customising the pre_check
73+
74+
For users who want to watch all their repos:
75+
76+
```bash
77+
"pre_check": "gh repo list --json nameWithOwner -q '.[].nameWithOwner' | while read repo; do gh pr list --repo $repo --state open --json number,title,headRefName,statusCheckRollup,reviewDecision --jq '.[] | select(.statusCheckRollup == \"FAILURE\" or .reviewDecision == \"CHANGES_REQUESTED\")' 2>/dev/null; done"
78+
```
79+
80+
For users who only care about failing CI (not review comments):
81+
82+
```bash
83+
"pre_check": "gh pr list --repo owner/repo --state open --json number,title,statusCheckRollup --jq '.[] | select(.statusCheckRollup == \"FAILURE\")'"
84+
```
85+
86+
## Managing the babysitter
87+
88+
- **Pause**: Update task status to `paused` via IPC or ask the agent
89+
- **Resume**: Update task status to `active`
90+
- **Change repos**: Cancel old task, create new one with updated pre_check
91+
- **Run now**: User can say "check my PRs now" — agent runs the same prompt manually

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Agents run directly on the host machine. `container-runner.ts` spawns `node` pro
4242
| `/add-voice-reply` | Voice replies via ElevenLabs TTS |
4343
| `/add-telegram-swarm` | Multi-bot agent teams |
4444
| `/add-slack` | Slack channel integration |
45+
| `/pr-babysitter` | Automated PR monitoring with CI fix and review resolution |
4546
| `/run-ralph` | Autonomous multi-task loop |
4647
| `/debug` | Troubleshooting guide |
4748
| `/customize` | Adding channels, integrations, changing behavior |

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ghostclaw",
3-
"version": "0.5.2",
3+
"version": "0.5.3",
44
"description": "Personal AI assistant. Bare metal, Telegram-first, no containers.",
55
"type": "module",
66
"main": "dist/index.js",

src/db.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ function createSchema(database: Database.Database): void {
9393
/* column already exists */
9494
}
9595

96+
// Add pre_check column if it doesn't exist (migration for existing DBs)
97+
try {
98+
database.exec(
99+
`ALTER TABLE scheduled_tasks ADD COLUMN pre_check TEXT DEFAULT NULL`,
100+
);
101+
} catch {
102+
/* column already exists */
103+
}
104+
96105
// Add is_bot_message column if it doesn't exist (migration for existing DBs)
97106
try {
98107
database.exec(
@@ -345,14 +354,15 @@ export function createTask(
345354
): void {
346355
db.prepare(
347356
`
348-
INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, schedule_type, schedule_value, context_mode, next_run, status, created_at)
349-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
357+
INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, pre_check, schedule_type, schedule_value, context_mode, next_run, status, created_at)
358+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
350359
`,
351360
).run(
352361
task.id,
353362
task.group_folder,
354363
task.chat_jid,
355364
task.prompt,
365+
task.pre_check || null,
356366
task.schedule_type,
357367
task.schedule_value,
358368
task.context_mode || 'isolated',

src/ipc.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export async function processTaskIpc(
162162
type: string;
163163
taskId?: string;
164164
prompt?: string;
165+
pre_check?: string;
165166
schedule_type?: string;
166167
schedule_value?: string;
167168
context_mode?: string;
@@ -267,6 +268,7 @@ export async function processTaskIpc(
267268
group_folder: targetFolder,
268269
chat_jid: targetJid,
269270
prompt: data.prompt,
271+
pre_check: data.pre_check || null,
270272
schedule_type: scheduleType,
271273
schedule_value: data.schedule_value,
272274
context_mode: contextMode,

src/task-scheduler.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChildProcess } from 'child_process';
1+
import { ChildProcess, execSync } from 'child_process';
22
import { CronExpressionParser } from 'cron-parser';
33
import fs from 'fs';
44

@@ -128,6 +128,78 @@ async function runTask(
128128
})),
129129
);
130130

131+
// Run pre_check script if defined — skip agent if script exits 0 with no output
132+
if (task.pre_check) {
133+
try {
134+
const preCheckOutput = execSync(task.pre_check, {
135+
timeout: 30000,
136+
encoding: 'utf-8',
137+
cwd: groupDir,
138+
env: { ...process.env, GHOSTCLAW_GROUP_DIR: groupDir },
139+
}).trim();
140+
141+
if (!preCheckOutput) {
142+
// Script exited 0 with no output — nothing to do, skip agent
143+
logger.info(
144+
{ taskId: task.id },
145+
'Pre-check passed with no output, skipping agent',
146+
);
147+
logTaskRun({
148+
task_id: task.id,
149+
run_at: new Date().toISOString(),
150+
duration_ms: Date.now() - startTime,
151+
status: 'success',
152+
result: 'Pre-check: nothing to do',
153+
error: null,
154+
});
155+
156+
// Still advance next_run
157+
let nextRun: string | null = null;
158+
if (task.schedule_type === 'cron') {
159+
const interval = CronExpressionParser.parse(task.schedule_value, {
160+
tz: TIMEZONE,
161+
});
162+
nextRun = interval.next().toISOString();
163+
} else if (task.schedule_type === 'interval') {
164+
const ms = parseInt(task.schedule_value, 10);
165+
nextRun = new Date(Date.now() + ms).toISOString();
166+
}
167+
updateTaskAfterRun(task.id, nextRun, 'Pre-check: nothing to do');
168+
return;
169+
}
170+
171+
// Script produced output — inject it into the prompt as context
172+
logger.info(
173+
{ taskId: task.id, outputLength: preCheckOutput.length },
174+
'Pre-check produced output, spawning agent',
175+
);
176+
task = {
177+
...task,
178+
prompt: task.prompt.includes('{{pre_check_output}}')
179+
? task.prompt.replace('{{pre_check_output}}', preCheckOutput)
180+
: `${task.prompt}\n\nPre-check output:\n${preCheckOutput}`,
181+
};
182+
} catch (err) {
183+
// Non-zero exit — treat as "needs attention", pass stderr/stdout to agent
184+
const execErr = err as { stdout?: string; stderr?: string; status?: number };
185+
const preCheckOutput = (execErr.stdout || execErr.stderr || '').trim();
186+
187+
if (preCheckOutput) {
188+
logger.info(
189+
{ taskId: task.id, exitCode: execErr.status },
190+
'Pre-check exited non-zero with output, spawning agent',
191+
);
192+
task = {
193+
...task,
194+
prompt: task.prompt.includes('{{pre_check_output}}')
195+
? task.prompt.replace('{{pre_check_output}}', preCheckOutput)
196+
: `${task.prompt}\n\nPre-check output:\n${preCheckOutput}`,
197+
};
198+
}
199+
// If no output at all, still proceed to agent (non-zero exit = something's wrong)
200+
}
201+
}
202+
131203
let result: string | null = null;
132204
let error: string | null = null;
133205

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export interface ScheduledTask {
5757
group_folder: string;
5858
chat_jid: string;
5959
prompt: string;
60+
pre_check?: string | null;
6061
schedule_type: 'cron' | 'interval' | 'once';
6162
schedule_value: string;
6263
context_mode: 'group' | 'isolated';

0 commit comments

Comments
 (0)