Add self-stopping CI watch tasks
This commit is contained in:
@@ -15,7 +15,7 @@ Your output is sent directly to the Discord group.
|
|||||||
- Keep answers concise unless more detail is genuinely needed
|
- Keep answers concise unless more detail is genuinely needed
|
||||||
- Give conclusions and concrete next steps, not hidden reasoning
|
- Give conclusions and concrete next steps, not hidden reasoning
|
||||||
- Use code blocks for commands or code when helpful
|
- Use code blocks for commands or code when helpful
|
||||||
- Do not claim you will keep watching, monitor later, report back later, or continue tracking unless you actually scheduled an EJClaw task with `schedule_task`
|
- Do not claim you will keep watching, monitor later, report back later, or continue tracking unless you actually scheduled an EJClaw task with `watch_ci` or `schedule_task`
|
||||||
- If no task was scheduled, do not imply that background tracking is active. If future follow-up is needed, tell the user to ping you again or explicitly ask for scheduling
|
- If no task was scheduled, do not imply that background tracking is active. If future follow-up is needed, tell the user to ping you again or explicitly ask for scheduling
|
||||||
- When you do schedule background follow-up, mention that it was scheduled. Include the task ID only when it is useful for later reference
|
- When you do schedule background follow-up, mention that it was scheduled. Include the task ID only when it is useful for later reference
|
||||||
|
|
||||||
@@ -24,4 +24,4 @@ Your output is sent directly to the Discord group.
|
|||||||
- Prefer reading the current workspace before making assumptions
|
- Prefer reading the current workspace before making assumptions
|
||||||
- Modify only what is needed for the task
|
- Modify only what is needed for the task
|
||||||
- Verify changes when you can instead of claiming they should work
|
- Verify changes when you can instead of claiming they should work
|
||||||
- For CI/status/watch requests that require future follow-up, prefer `schedule_task` over leaving the chat session idle
|
- For CI/status/watch requests that require future follow-up, prefer `watch_ci` over leaving the chat session idle
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import { z } from 'zod';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { CronExpressionParser } from 'cron-parser';
|
import { CronExpressionParser } from 'cron-parser';
|
||||||
|
import {
|
||||||
|
buildCiWatchPrompt,
|
||||||
|
normalizeWatchCiIntervalSeconds,
|
||||||
|
} from './watch-ci.js';
|
||||||
|
|
||||||
const IPC_DIR =
|
const IPC_DIR =
|
||||||
process.env.EJCLAW_IPC_DIR || process.env.NANOCLAW_IPC_DIR || '/workspace/ipc';
|
process.env.EJCLAW_IPC_DIR || process.env.NANOCLAW_IPC_DIR || '/workspace/ipc';
|
||||||
@@ -155,6 +159,77 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone):
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
'watch_ci',
|
||||||
|
'Schedule a background CI watcher that checks until a run or check reaches a terminal state, then sends one message and cancels itself.',
|
||||||
|
{
|
||||||
|
target: z.string().describe('What to watch, for example "PR #123 checks" or "GitHub Actions run 987654321".'),
|
||||||
|
check_instructions: z.string().describe('Exact steps or commands to check status and what details matter when it finishes.'),
|
||||||
|
poll_interval_seconds: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(30)
|
||||||
|
.max(3600)
|
||||||
|
.default(60)
|
||||||
|
.describe('How often to poll in seconds. Default 60, minimum 30.'),
|
||||||
|
context_mode: z
|
||||||
|
.enum(['group', 'isolated'])
|
||||||
|
.default('group')
|
||||||
|
.describe('group=runs with chat history and memory, isolated=fresh session (include all context in check_instructions)'),
|
||||||
|
target_group_jid: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('(Main group only) JID of the group to schedule the watcher for. Defaults to the current group.'),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
let pollSeconds: number;
|
||||||
|
try {
|
||||||
|
pollSeconds = normalizeWatchCiIntervalSeconds(args.poll_interval_seconds);
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: error instanceof Error ? error.message : String(error),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetJid = isMain && args.target_group_jid ? args.target_group_jid : chatJid;
|
||||||
|
const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
const prompt = buildCiWatchPrompt({
|
||||||
|
taskId,
|
||||||
|
target: args.target,
|
||||||
|
checkInstructions: args.check_instructions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
type: 'schedule_task',
|
||||||
|
taskId,
|
||||||
|
prompt,
|
||||||
|
schedule_type: 'interval' as const,
|
||||||
|
schedule_value: String(pollSeconds * 1000),
|
||||||
|
context_mode: args.context_mode || 'group',
|
||||||
|
targetJid,
|
||||||
|
createdBy: groupFolder,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
writeIpcFile(TASKS_DIR, data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `CI watcher scheduled: ${taskId} (${pollSeconds}s)`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
'list_tasks',
|
'list_tasks',
|
||||||
"List all scheduled tasks. From main: shows all tasks. From other groups: shows only that group's tasks.",
|
"List all scheduled tasks. From main: shows all tasks. From other groups: shows only that group's tasks.",
|
||||||
|
|||||||
62
runners/agent-runner/src/watch-ci.ts
Normal file
62
runners/agent-runner/src/watch-ci.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
export const DEFAULT_WATCH_CI_INTERVAL_SECONDS = 60;
|
||||||
|
export const MIN_WATCH_CI_INTERVAL_SECONDS = 30;
|
||||||
|
export const MAX_WATCH_CI_INTERVAL_SECONDS = 3600;
|
||||||
|
|
||||||
|
export interface BuildCiWatchPromptArgs {
|
||||||
|
taskId: string;
|
||||||
|
target: string;
|
||||||
|
checkInstructions: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeWatchCiIntervalSeconds(seconds?: number): number {
|
||||||
|
if (seconds === undefined) {
|
||||||
|
return DEFAULT_WATCH_CI_INTERVAL_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(seconds)) {
|
||||||
|
throw new Error('poll_interval_seconds must be an integer.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
seconds < MIN_WATCH_CI_INTERVAL_SECONDS ||
|
||||||
|
seconds > MAX_WATCH_CI_INTERVAL_SECONDS
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`poll_interval_seconds must be between ${MIN_WATCH_CI_INTERVAL_SECONDS} and ${MAX_WATCH_CI_INTERVAL_SECONDS}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCiWatchPrompt({
|
||||||
|
taskId,
|
||||||
|
target,
|
||||||
|
checkInstructions,
|
||||||
|
}: BuildCiWatchPromptArgs): string {
|
||||||
|
return `
|
||||||
|
[BACKGROUND CI WATCH]
|
||||||
|
|
||||||
|
You are running as an EJClaw background CI watcher.
|
||||||
|
|
||||||
|
Watch target:
|
||||||
|
${target}
|
||||||
|
|
||||||
|
Task ID:
|
||||||
|
${taskId}
|
||||||
|
|
||||||
|
Check instructions:
|
||||||
|
${checkInstructions}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- On each run, check whether the target is still queued, pending, running, in progress, or otherwise non-terminal.
|
||||||
|
- If it is still not finished, send no visible message and end this run quietly.
|
||||||
|
- If it reached a terminal state such as success, failure, cancelled, timed out, neutral, skipped, or action required:
|
||||||
|
1. Send exactly one concise completion message with \`send_message\`.
|
||||||
|
2. Include the final status and only the most important details.
|
||||||
|
3. Call \`cancel_task\` with task_id "${taskId}" so this watcher stops itself.
|
||||||
|
- If you hit a transient problem such as a rate limit, network issue, or temporary auth failure, send no visible message and leave the task active for the next retry.
|
||||||
|
- Prefer no normal final response. Use \`send_message\` for the completion message, and keep any non-user-facing notes inside \`<internal>\` tags if needed.
|
||||||
|
- Do not claim continued monitoring after you cancel the task.
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
40
runners/agent-runner/test/watch-ci.test.ts
Normal file
40
runners/agent-runner/test/watch-ci.test.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildCiWatchPrompt,
|
||||||
|
normalizeWatchCiIntervalSeconds,
|
||||||
|
} from '../src/watch-ci.js';
|
||||||
|
|
||||||
|
describe('watch-ci helpers', () => {
|
||||||
|
it('builds a self-cancelling CI watch prompt', () => {
|
||||||
|
const prompt = buildCiWatchPrompt({
|
||||||
|
taskId: 'task-123',
|
||||||
|
target: 'PR #42 checks',
|
||||||
|
checkInstructions: 'Use gh pr checks 42 and summarize only terminal results.',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(prompt).toContain('PR #42 checks');
|
||||||
|
expect(prompt).toContain('task-123');
|
||||||
|
expect(prompt).toContain('cancel_task');
|
||||||
|
expect(prompt).toContain('send_message');
|
||||||
|
expect(prompt).toContain('gh pr checks 42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes valid poll intervals', () => {
|
||||||
|
expect(normalizeWatchCiIntervalSeconds()).toBe(60);
|
||||||
|
expect(normalizeWatchCiIntervalSeconds(30)).toBe(30);
|
||||||
|
expect(normalizeWatchCiIntervalSeconds(600)).toBe(600);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects invalid poll intervals', () => {
|
||||||
|
expect(() => normalizeWatchCiIntervalSeconds(29)).toThrow(
|
||||||
|
/between 30 and 3600/i,
|
||||||
|
);
|
||||||
|
expect(() => normalizeWatchCiIntervalSeconds(3601)).toThrow(
|
||||||
|
/between 30 and 3600/i,
|
||||||
|
);
|
||||||
|
expect(() => normalizeWatchCiIntervalSeconds(30.5)).toThrow(
|
||||||
|
/integer/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user