@yattalo/task-system 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/dist/commands/dashboard.d.ts +7 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +30 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +316 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/kanban.d.ts +7 -0
- package/dist/commands/kanban.d.ts.map +1 -0
- package/dist/commands/kanban.js +68 -0
- package/dist/commands/kanban.js.map +1 -0
- package/dist/commands/report.d.ts +8 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +60 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/seed.d.ts +6 -0
- package/dist/commands/seed.d.ts.map +1 -0
- package/dist/commands/seed.js +30 -0
- package/dist/commands/seed.js.map +1 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +62 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/generators/agent-ops-scripts.d.ts +8 -0
- package/dist/generators/agent-ops-scripts.d.ts.map +1 -0
- package/dist/generators/agent-ops-scripts.js +843 -0
- package/dist/generators/agent-ops-scripts.js.map +1 -0
- package/dist/generators/agent-ops.d.ts +6 -0
- package/dist/generators/agent-ops.d.ts.map +1 -0
- package/dist/generators/agent-ops.js +290 -0
- package/dist/generators/agent-ops.js.map +1 -0
- package/dist/generators/claude-hooks.d.ts +6 -0
- package/dist/generators/claude-hooks.d.ts.map +1 -0
- package/dist/generators/claude-hooks.js +260 -0
- package/dist/generators/claude-hooks.js.map +1 -0
- package/dist/generators/cli-aliases.d.ts +6 -0
- package/dist/generators/cli-aliases.d.ts.map +1 -0
- package/dist/generators/cli-aliases.js +187 -0
- package/dist/generators/cli-aliases.js.map +1 -0
- package/dist/generators/dashboard.d.ts +6 -0
- package/dist/generators/dashboard.d.ts.map +1 -0
- package/dist/generators/dashboard.js +732 -0
- package/dist/generators/dashboard.js.map +1 -0
- package/dist/generators/git-hook.d.ts +6 -0
- package/dist/generators/git-hook.d.ts.map +1 -0
- package/dist/generators/git-hook.js +163 -0
- package/dist/generators/git-hook.js.map +1 -0
- package/dist/generators/http.d.ts +6 -0
- package/dist/generators/http.d.ts.map +1 -0
- package/dist/generators/http.js +175 -0
- package/dist/generators/http.js.map +1 -0
- package/dist/generators/orchestrator.d.ts +6 -0
- package/dist/generators/orchestrator.d.ts.map +1 -0
- package/dist/generators/orchestrator.js +391 -0
- package/dist/generators/orchestrator.js.map +1 -0
- package/dist/generators/schema.d.ts +8 -0
- package/dist/generators/schema.d.ts.map +1 -0
- package/dist/generators/schema.js +470 -0
- package/dist/generators/schema.js.map +1 -0
- package/dist/generators/skill.d.ts +6 -0
- package/dist/generators/skill.d.ts.map +1 -0
- package/dist/generators/skill.js +147 -0
- package/dist/generators/skill.js.map +1 -0
- package/dist/generators/slash-commands.d.ts +6 -0
- package/dist/generators/slash-commands.d.ts.map +1 -0
- package/dist/generators/slash-commands.js +268 -0
- package/dist/generators/slash-commands.js.map +1 -0
- package/dist/generators/tasks.d.ts +6 -0
- package/dist/generators/tasks.d.ts.map +1 -0
- package/dist/generators/tasks.js +451 -0
- package/dist/generators/tasks.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/presets/research.d.ts +3 -0
- package/dist/presets/research.d.ts.map +1 -0
- package/dist/presets/research.js +56 -0
- package/dist/presets/research.js.map +1 -0
- package/dist/presets/software.d.ts +3 -0
- package/dist/presets/software.d.ts.map +1 -0
- package/dist/presets/software.js +56 -0
- package/dist/presets/software.js.map +1 -0
- package/dist/utils/detect.d.ts +3 -0
- package/dist/utils/detect.d.ts.map +1 -0
- package/dist/utils/detect.js +76 -0
- package/dist/utils/detect.js.map +1 -0
- package/dist/utils/merge.d.ts +14 -0
- package/dist/utils/merge.d.ts.map +1 -0
- package/dist/utils/merge.js +43 -0
- package/dist/utils/merge.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Generator: Layer 4 — Agent-Ops Scripts
|
|
3
|
+
// Generates scripts/agent-ops/*.ts
|
|
4
|
+
// ============================================================
|
|
5
|
+
import { writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
export function generateAgentOpsScripts(ctx) {
|
|
8
|
+
const results = [];
|
|
9
|
+
const base = join(ctx.targetDir, "scripts", "agent-ops");
|
|
10
|
+
results.push(generateTypes(ctx, base));
|
|
11
|
+
results.push(generateConfig(ctx, base));
|
|
12
|
+
results.push(generateScheduler(ctx, base));
|
|
13
|
+
results.push(generateLaneQueue(ctx, base));
|
|
14
|
+
results.push(generateGitIsolation(ctx, base));
|
|
15
|
+
results.push(generateAgentLauncher(ctx, base));
|
|
16
|
+
results.push(generateLogStreamer(ctx, base));
|
|
17
|
+
results.push(generatePrCreator(ctx, base));
|
|
18
|
+
results.push(generateCleanup(ctx, base));
|
|
19
|
+
results.push(generateRunner(ctx, base));
|
|
20
|
+
return results;
|
|
21
|
+
}
|
|
22
|
+
function writeIfNotDry(path, content, dryRun) {
|
|
23
|
+
const lineCount = content.split("\n").length;
|
|
24
|
+
if (!dryRun)
|
|
25
|
+
writeFileSync(path, content, "utf-8");
|
|
26
|
+
return lineCount;
|
|
27
|
+
}
|
|
28
|
+
// ── types.ts ──
|
|
29
|
+
function generateTypes(ctx, base) {
|
|
30
|
+
const content = `// Agent-Ops shared types — generated by @yattalo/task-system
|
|
31
|
+
|
|
32
|
+
export interface AgentOpsConfig {
|
|
33
|
+
convexUrl: string;
|
|
34
|
+
pollInterval: number;
|
|
35
|
+
maxConcurrentRuns: number;
|
|
36
|
+
maxRunsPerDay: number;
|
|
37
|
+
defaultTimeoutMinutes: number;
|
|
38
|
+
protectedBranches: string[];
|
|
39
|
+
agentCommands: Record<string, string>;
|
|
40
|
+
projectRoot: string;
|
|
41
|
+
worktreeBase: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface JobSnapshot {
|
|
45
|
+
jobId: string;
|
|
46
|
+
name: string;
|
|
47
|
+
agent: string;
|
|
48
|
+
prompt: string;
|
|
49
|
+
executionMode: string;
|
|
50
|
+
createPR: boolean;
|
|
51
|
+
prBaseBranch?: string;
|
|
52
|
+
maxDurationMinutes?: number;
|
|
53
|
+
laneId?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RunContext {
|
|
57
|
+
runId: string;
|
|
58
|
+
jobId: string;
|
|
59
|
+
agent: string;
|
|
60
|
+
prompt: string;
|
|
61
|
+
executionMode: string;
|
|
62
|
+
worktreePath: string;
|
|
63
|
+
branchName: string;
|
|
64
|
+
baseBranch: string;
|
|
65
|
+
startedAt: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface LogEntry {
|
|
69
|
+
timestamp: number;
|
|
70
|
+
level: "info" | "warn" | "error" | "debug";
|
|
71
|
+
message: string;
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
const lines = writeIfNotDry(join(base, "types.ts"), content, ctx.dryRun);
|
|
75
|
+
return { file: "scripts/agent-ops/types.ts", lines };
|
|
76
|
+
}
|
|
77
|
+
// ── config.ts ──
|
|
78
|
+
function generateConfig(ctx, base) {
|
|
79
|
+
const agentOps = ctx.config.agentOps ?? {};
|
|
80
|
+
const agentCmds = agentOps.agentCommands ?? {};
|
|
81
|
+
const cmdsStr = Object.entries(agentCmds)
|
|
82
|
+
.map(([k, v]) => ` ${k}: "${v}"`)
|
|
83
|
+
.join(",\n");
|
|
84
|
+
const content = `// Agent-Ops config loader — generated by @yattalo/task-system
|
|
85
|
+
import { resolve } from "node:path";
|
|
86
|
+
import { existsSync } from "node:fs";
|
|
87
|
+
import type { AgentOpsConfig } from "./types.js";
|
|
88
|
+
|
|
89
|
+
const DEFAULT_CONFIG: AgentOpsConfig = {
|
|
90
|
+
convexUrl: process.env.CONVEX_URL ?? "",
|
|
91
|
+
pollInterval: ${agentOps.pollInterval ?? 30000},
|
|
92
|
+
maxConcurrentRuns: ${agentOps.maxConcurrentRuns ?? 3},
|
|
93
|
+
maxRunsPerDay: ${agentOps.maxRunsPerDay ?? 100},
|
|
94
|
+
defaultTimeoutMinutes: ${agentOps.defaultTimeoutMinutes ?? 30},
|
|
95
|
+
protectedBranches: ${JSON.stringify(agentOps.protectedBranches ?? ["main", "master"])},
|
|
96
|
+
agentCommands: {
|
|
97
|
+
${cmdsStr}
|
|
98
|
+
},
|
|
99
|
+
projectRoot: process.cwd(),
|
|
100
|
+
worktreeBase: resolve(process.env.HOME ?? "~", ".agent-ops", "worktrees"),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export function loadConfig(): AgentOpsConfig {
|
|
104
|
+
// Try to load from task-system.config.ts at project root
|
|
105
|
+
const configPath = resolve(process.cwd(), "task-system.config.ts");
|
|
106
|
+
if (existsSync(configPath)) {
|
|
107
|
+
// Config is TypeScript — in production this would use tsx or similar
|
|
108
|
+
// For now, use defaults + env overrides
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
...DEFAULT_CONFIG,
|
|
113
|
+
convexUrl: process.env.CONVEX_URL ?? DEFAULT_CONFIG.convexUrl,
|
|
114
|
+
pollInterval: parseInt(process.env.AGENT_OPS_POLL_INTERVAL ?? String(DEFAULT_CONFIG.pollInterval), 10),
|
|
115
|
+
maxConcurrentRuns: parseInt(process.env.AGENT_OPS_MAX_CONCURRENT ?? String(DEFAULT_CONFIG.maxConcurrentRuns), 10),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
119
|
+
const lines = writeIfNotDry(join(base, "config.ts"), content, ctx.dryRun);
|
|
120
|
+
return { file: "scripts/agent-ops/config.ts", lines };
|
|
121
|
+
}
|
|
122
|
+
// ── scheduler.ts ──
|
|
123
|
+
function generateScheduler(ctx, base) {
|
|
124
|
+
const content = `// Agent-Ops cron scheduler — generated by @yattalo/task-system
|
|
125
|
+
//
|
|
126
|
+
// Minimal cron parser for Agent-Ops scheduling.
|
|
127
|
+
// Supports standard 5-field cron expressions: minute hour day month weekday
|
|
128
|
+
|
|
129
|
+
export function parseCron(expression: string): { minute: number[]; hour: number[]; day: number[]; month: number[]; weekday: number[] } {
|
|
130
|
+
const parts = expression.trim().split(/\\s+/);
|
|
131
|
+
if (parts.length !== 5) throw new Error(\`Invalid cron: \${expression}\`);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
minute: parseField(parts[0], 0, 59),
|
|
135
|
+
hour: parseField(parts[1], 0, 23),
|
|
136
|
+
day: parseField(parts[2], 1, 31),
|
|
137
|
+
month: parseField(parts[3], 1, 12),
|
|
138
|
+
weekday: parseField(parts[4], 0, 6),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseField(field: string, min: number, max: number): number[] {
|
|
143
|
+
if (field === "*") return range(min, max);
|
|
144
|
+
|
|
145
|
+
const values = new Set<number>();
|
|
146
|
+
for (const part of field.split(",")) {
|
|
147
|
+
if (part.includes("/")) {
|
|
148
|
+
const [rangeStr, stepStr] = part.split("/");
|
|
149
|
+
const step = parseInt(stepStr, 10);
|
|
150
|
+
const [start, end] = rangeStr === "*" ? [min, max] : rangeStr.split("-").map(Number);
|
|
151
|
+
for (let i = start; i <= (end ?? max); i += step) values.add(i);
|
|
152
|
+
} else if (part.includes("-")) {
|
|
153
|
+
const [start, end] = part.split("-").map(Number);
|
|
154
|
+
for (let i = start; i <= end; i++) values.add(i);
|
|
155
|
+
} else {
|
|
156
|
+
values.add(parseInt(part, 10));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return [...values].sort((a, b) => a - b);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function range(min: number, max: number): number[] {
|
|
163
|
+
return Array.from({ length: max - min + 1 }, (_, i) => min + i);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function getNextRun(expression: string, after: Date = new Date()): Date {
|
|
167
|
+
const cron = parseCron(expression);
|
|
168
|
+
const next = new Date(after.getTime() + 60000); // start from next minute
|
|
169
|
+
next.setSeconds(0, 0);
|
|
170
|
+
|
|
171
|
+
for (let attempts = 0; attempts < 525960; attempts++) { // max ~1 year of minutes
|
|
172
|
+
if (
|
|
173
|
+
cron.month.includes(next.getMonth() + 1) &&
|
|
174
|
+
cron.day.includes(next.getDate()) &&
|
|
175
|
+
cron.weekday.includes(next.getDay()) &&
|
|
176
|
+
cron.hour.includes(next.getHours()) &&
|
|
177
|
+
cron.minute.includes(next.getMinutes())
|
|
178
|
+
) {
|
|
179
|
+
return next;
|
|
180
|
+
}
|
|
181
|
+
next.setTime(next.getTime() + 60000);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw new Error(\`No next run found for: \${expression}\`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function shouldRunNow(expression: string, lastRunAt?: number): boolean {
|
|
188
|
+
const now = new Date();
|
|
189
|
+
const cron = parseCron(expression);
|
|
190
|
+
|
|
191
|
+
const matches =
|
|
192
|
+
cron.month.includes(now.getMonth() + 1) &&
|
|
193
|
+
cron.day.includes(now.getDate()) &&
|
|
194
|
+
cron.weekday.includes(now.getDay()) &&
|
|
195
|
+
cron.hour.includes(now.getHours()) &&
|
|
196
|
+
cron.minute.includes(now.getMinutes());
|
|
197
|
+
|
|
198
|
+
if (!matches) return false;
|
|
199
|
+
if (!lastRunAt) return true;
|
|
200
|
+
|
|
201
|
+
// Don't run again if already ran this minute
|
|
202
|
+
const lastRun = new Date(lastRunAt);
|
|
203
|
+
return (
|
|
204
|
+
lastRun.getMinutes() !== now.getMinutes() ||
|
|
205
|
+
lastRun.getHours() !== now.getHours() ||
|
|
206
|
+
lastRun.getDate() !== now.getDate()
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
`;
|
|
210
|
+
const lines = writeIfNotDry(join(base, "scheduler.ts"), content, ctx.dryRun);
|
|
211
|
+
return { file: "scripts/agent-ops/scheduler.ts", lines };
|
|
212
|
+
}
|
|
213
|
+
// ── lane-queue.ts ──
|
|
214
|
+
function generateLaneQueue(ctx, base) {
|
|
215
|
+
const content = `// Agent-Ops lane queue — generated by @yattalo/task-system
|
|
216
|
+
//
|
|
217
|
+
// Prevents concurrent runs on the same lane.
|
|
218
|
+
// A lane is a logical grouping (e.g., "frontend", "backend") to prevent conflicts.
|
|
219
|
+
|
|
220
|
+
const activeLanes = new Map<string, string>(); // laneId -> runId
|
|
221
|
+
|
|
222
|
+
export function acquireLane(laneId: string, runId: string): boolean {
|
|
223
|
+
if (activeLanes.has(laneId)) {
|
|
224
|
+
return false; // lane is busy
|
|
225
|
+
}
|
|
226
|
+
activeLanes.set(laneId, runId);
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function releaseLane(laneId: string, runId: string): void {
|
|
231
|
+
if (activeLanes.get(laneId) === runId) {
|
|
232
|
+
activeLanes.delete(laneId);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function isLaneBusy(laneId: string): boolean {
|
|
237
|
+
return activeLanes.has(laneId);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function getActiveLanes(): Map<string, string> {
|
|
241
|
+
return new Map(activeLanes);
|
|
242
|
+
}
|
|
243
|
+
`;
|
|
244
|
+
const lines = writeIfNotDry(join(base, "lane-queue.ts"), content, ctx.dryRun);
|
|
245
|
+
return { file: "scripts/agent-ops/lane-queue.ts", lines };
|
|
246
|
+
}
|
|
247
|
+
// ── git-isolation.ts ──
|
|
248
|
+
function generateGitIsolation(ctx, base) {
|
|
249
|
+
const protectedBranches = JSON.stringify(ctx.config.agentOps?.protectedBranches ?? ["main", "master"]);
|
|
250
|
+
const content = `// Agent-Ops git isolation — generated by @yattalo/task-system
|
|
251
|
+
//
|
|
252
|
+
// Creates and manages git worktrees for isolated agent execution.
|
|
253
|
+
// Each run gets its own worktree to prevent conflicts.
|
|
254
|
+
|
|
255
|
+
import { execSync } from "node:child_process";
|
|
256
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
257
|
+
import { join, resolve } from "node:path";
|
|
258
|
+
import type { AgentOpsConfig } from "./types.js";
|
|
259
|
+
|
|
260
|
+
const PROTECTED_BRANCHES = ${protectedBranches};
|
|
261
|
+
|
|
262
|
+
export interface WorktreeInfo {
|
|
263
|
+
path: string;
|
|
264
|
+
branch: string;
|
|
265
|
+
baseBranch: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function createWorktree(
|
|
269
|
+
config: AgentOpsConfig,
|
|
270
|
+
runId: string,
|
|
271
|
+
baseBranch: string = "main",
|
|
272
|
+
): WorktreeInfo {
|
|
273
|
+
const worktreeBase = config.worktreeBase;
|
|
274
|
+
mkdirSync(worktreeBase, { recursive: true });
|
|
275
|
+
|
|
276
|
+
const branchName = \`agent-ops/\${runId}\`;
|
|
277
|
+
const worktreePath = join(worktreeBase, runId);
|
|
278
|
+
|
|
279
|
+
if (existsSync(worktreePath)) {
|
|
280
|
+
throw new Error(\`Worktree already exists: \${worktreePath}\`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Validate base branch exists
|
|
284
|
+
try {
|
|
285
|
+
execSync(\`git rev-parse --verify \${baseBranch}\`, {
|
|
286
|
+
cwd: config.projectRoot,
|
|
287
|
+
stdio: "pipe",
|
|
288
|
+
});
|
|
289
|
+
} catch {
|
|
290
|
+
throw new Error(\`Base branch does not exist: \${baseBranch}\`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Validate not targeting protected branch directly
|
|
294
|
+
if (PROTECTED_BRANCHES.includes(branchName)) {
|
|
295
|
+
throw new Error(\`Cannot create worktree on protected branch: \${branchName}\`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Create worktree with new branch
|
|
299
|
+
execSync(
|
|
300
|
+
\`git worktree add -b "\${branchName}" "\${worktreePath}" "\${baseBranch}"\`,
|
|
301
|
+
{ cwd: config.projectRoot, stdio: "pipe" },
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
return { path: worktreePath, branch: branchName, baseBranch };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function removeWorktree(config: AgentOpsConfig, runId: string): void {
|
|
308
|
+
const worktreePath = join(config.worktreeBase, runId);
|
|
309
|
+
|
|
310
|
+
if (!existsSync(worktreePath)) return;
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
execSync(\`git worktree remove "\${worktreePath}" --force\`, {
|
|
314
|
+
cwd: config.projectRoot,
|
|
315
|
+
stdio: "pipe",
|
|
316
|
+
});
|
|
317
|
+
} catch {
|
|
318
|
+
// Fallback: manual cleanup
|
|
319
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
320
|
+
try {
|
|
321
|
+
execSync("git worktree prune", { cwd: config.projectRoot, stdio: "pipe" });
|
|
322
|
+
} catch {}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Clean up branch
|
|
326
|
+
const branchName = \`agent-ops/\${runId}\`;
|
|
327
|
+
try {
|
|
328
|
+
execSync(\`git branch -D "\${branchName}"\`, {
|
|
329
|
+
cwd: config.projectRoot,
|
|
330
|
+
stdio: "pipe",
|
|
331
|
+
});
|
|
332
|
+
} catch {}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function getWorktreeGitStats(
|
|
336
|
+
worktreePath: string,
|
|
337
|
+
baseBranch: string,
|
|
338
|
+
): { commitCount: number; filesChanged: number; linesAdded: number; linesRemoved: number } {
|
|
339
|
+
try {
|
|
340
|
+
const commitCount = parseInt(
|
|
341
|
+
execSync(\`git rev-list --count \${baseBranch}..HEAD\`, {
|
|
342
|
+
cwd: worktreePath,
|
|
343
|
+
encoding: "utf-8",
|
|
344
|
+
}).trim(),
|
|
345
|
+
10,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const diffStat = execSync(\`git diff --shortstat \${baseBranch}..HEAD\`, {
|
|
349
|
+
cwd: worktreePath,
|
|
350
|
+
encoding: "utf-8",
|
|
351
|
+
}).trim();
|
|
352
|
+
|
|
353
|
+
let filesChanged = 0, linesAdded = 0, linesRemoved = 0;
|
|
354
|
+
const filesMatch = diffStat.match(/(\\d+) files? changed/);
|
|
355
|
+
const addMatch = diffStat.match(/(\\d+) insertions?/);
|
|
356
|
+
const delMatch = diffStat.match(/(\\d+) deletions?/);
|
|
357
|
+
|
|
358
|
+
if (filesMatch) filesChanged = parseInt(filesMatch[1], 10);
|
|
359
|
+
if (addMatch) linesAdded = parseInt(addMatch[1], 10);
|
|
360
|
+
if (delMatch) linesRemoved = parseInt(delMatch[1], 10);
|
|
361
|
+
|
|
362
|
+
return { commitCount, filesChanged, linesAdded, linesRemoved };
|
|
363
|
+
} catch {
|
|
364
|
+
return { commitCount: 0, filesChanged: 0, linesAdded: 0, linesRemoved: 0 };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function listStaleWorktrees(config: AgentOpsConfig, maxAgeMs: number = 24 * 60 * 60 * 1000): string[] {
|
|
369
|
+
const worktreeBase = config.worktreeBase;
|
|
370
|
+
if (!existsSync(worktreeBase)) return [];
|
|
371
|
+
|
|
372
|
+
const { readdirSync, statSync } = require("node:fs");
|
|
373
|
+
const now = Date.now();
|
|
374
|
+
const stale: string[] = [];
|
|
375
|
+
|
|
376
|
+
for (const entry of readdirSync(worktreeBase)) {
|
|
377
|
+
const entryPath = join(worktreeBase, entry);
|
|
378
|
+
try {
|
|
379
|
+
const stat = statSync(entryPath);
|
|
380
|
+
if (now - stat.mtimeMs > maxAgeMs) {
|
|
381
|
+
stale.push(entry);
|
|
382
|
+
}
|
|
383
|
+
} catch {}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return stale;
|
|
387
|
+
}
|
|
388
|
+
`;
|
|
389
|
+
const lines = writeIfNotDry(join(base, "git-isolation.ts"), content, ctx.dryRun);
|
|
390
|
+
return { file: "scripts/agent-ops/git-isolation.ts", lines };
|
|
391
|
+
}
|
|
392
|
+
// ── agent-launcher.ts ──
|
|
393
|
+
function generateAgentLauncher(ctx, base) {
|
|
394
|
+
const content = `// Agent-Ops agent launcher — generated by @yattalo/task-system
|
|
395
|
+
//
|
|
396
|
+
// Spawns agent CLI processes (claude, gemini, codex) with proper environment.
|
|
397
|
+
|
|
398
|
+
import { spawn, ChildProcess } from "node:child_process";
|
|
399
|
+
import type { AgentOpsConfig, RunContext, LogEntry } from "./types.js";
|
|
400
|
+
|
|
401
|
+
export interface LaunchResult {
|
|
402
|
+
process: ChildProcess;
|
|
403
|
+
exitCode: Promise<number>;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function launchAgent(
|
|
407
|
+
config: AgentOpsConfig,
|
|
408
|
+
run: RunContext,
|
|
409
|
+
onLog: (entry: LogEntry) => void,
|
|
410
|
+
): LaunchResult {
|
|
411
|
+
const agentCmd = config.agentCommands[run.agent];
|
|
412
|
+
if (!agentCmd) {
|
|
413
|
+
throw new Error(\`No command configured for agent: \${run.agent}\`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const timeoutMs = (config.defaultTimeoutMinutes ?? 30) * 60 * 1000;
|
|
417
|
+
|
|
418
|
+
// Build agent-specific args
|
|
419
|
+
const args = buildAgentArgs(run);
|
|
420
|
+
|
|
421
|
+
onLog({
|
|
422
|
+
timestamp: Date.now(),
|
|
423
|
+
level: "info",
|
|
424
|
+
message: \`Launching \${run.agent}: \${agentCmd} \${args.join(" ")}\`,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const proc = spawn(agentCmd, args, {
|
|
428
|
+
cwd: run.worktreePath,
|
|
429
|
+
env: {
|
|
430
|
+
...process.env,
|
|
431
|
+
AGENT_OPS_RUN_ID: run.runId,
|
|
432
|
+
AGENT_OPS_JOB_ID: run.jobId,
|
|
433
|
+
},
|
|
434
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Stream stdout/stderr to log callback
|
|
438
|
+
proc.stdout?.on("data", (data: Buffer) => {
|
|
439
|
+
const text = data.toString().trim();
|
|
440
|
+
if (text) {
|
|
441
|
+
onLog({ timestamp: Date.now(), level: "info", message: text.slice(0, 500) });
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
proc.stderr?.on("data", (data: Buffer) => {
|
|
446
|
+
const text = data.toString().trim();
|
|
447
|
+
if (text) {
|
|
448
|
+
onLog({ timestamp: Date.now(), level: "warn", message: text.slice(0, 500) });
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Timeout handling
|
|
453
|
+
const timeoutId = setTimeout(() => {
|
|
454
|
+
onLog({ timestamp: Date.now(), level: "error", message: \`Timeout after \${config.defaultTimeoutMinutes}min\` });
|
|
455
|
+
proc.kill("SIGTERM");
|
|
456
|
+
setTimeout(() => proc.kill("SIGKILL"), 5000);
|
|
457
|
+
}, timeoutMs);
|
|
458
|
+
|
|
459
|
+
const exitCode = new Promise<number>((resolve) => {
|
|
460
|
+
proc.on("close", (code) => {
|
|
461
|
+
clearTimeout(timeoutId);
|
|
462
|
+
resolve(code ?? 1);
|
|
463
|
+
});
|
|
464
|
+
proc.on("error", (err) => {
|
|
465
|
+
clearTimeout(timeoutId);
|
|
466
|
+
onLog({ timestamp: Date.now(), level: "error", message: \`Process error: \${err.message}\` });
|
|
467
|
+
resolve(1);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
return { process: proc, exitCode };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function buildAgentArgs(run: RunContext): string[] {
|
|
475
|
+
switch (run.agent) {
|
|
476
|
+
case "claude":
|
|
477
|
+
return ["--print", "--dangerously-skip-permissions", "-p", run.prompt];
|
|
478
|
+
case "gemini":
|
|
479
|
+
return ["--prompt", run.prompt];
|
|
480
|
+
case "codex":
|
|
481
|
+
return ["--prompt", run.prompt, "--approval-mode", "full-auto"];
|
|
482
|
+
default:
|
|
483
|
+
return [run.prompt];
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
`;
|
|
487
|
+
const lines = writeIfNotDry(join(base, "agent-launcher.ts"), content, ctx.dryRun);
|
|
488
|
+
return { file: "scripts/agent-ops/agent-launcher.ts", lines };
|
|
489
|
+
}
|
|
490
|
+
// ── log-streamer.ts ──
|
|
491
|
+
function generateLogStreamer(ctx, base) {
|
|
492
|
+
const content = `// Agent-Ops log streamer — generated by @yattalo/task-system
|
|
493
|
+
//
|
|
494
|
+
// Batches log entries and uploads them to Convex periodically.
|
|
495
|
+
|
|
496
|
+
import { execSync } from "node:child_process";
|
|
497
|
+
import type { LogEntry } from "./types.js";
|
|
498
|
+
|
|
499
|
+
export class LogStreamer {
|
|
500
|
+
private buffer: LogEntry[] = [];
|
|
501
|
+
private intervalId: ReturnType<typeof setInterval> | null = null;
|
|
502
|
+
private readonly maxBatchSize = 20;
|
|
503
|
+
private readonly flushIntervalMs = 5000;
|
|
504
|
+
|
|
505
|
+
constructor(
|
|
506
|
+
private readonly runId: string,
|
|
507
|
+
private readonly projectRoot: string,
|
|
508
|
+
) {}
|
|
509
|
+
|
|
510
|
+
start(): void {
|
|
511
|
+
this.intervalId = setInterval(() => this.flush(), this.flushIntervalMs);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
stop(): void {
|
|
515
|
+
if (this.intervalId) {
|
|
516
|
+
clearInterval(this.intervalId);
|
|
517
|
+
this.intervalId = null;
|
|
518
|
+
}
|
|
519
|
+
this.flush(); // Final flush
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
push(entry: LogEntry): void {
|
|
523
|
+
this.buffer.push(entry);
|
|
524
|
+
if (this.buffer.length >= this.maxBatchSize) {
|
|
525
|
+
this.flush();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private flush(): void {
|
|
530
|
+
if (this.buffer.length === 0) return;
|
|
531
|
+
|
|
532
|
+
const entries = this.buffer.splice(0, this.maxBatchSize);
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
execSync(
|
|
536
|
+
\`npx convex run taskSystem/agentOps:appendRunLog '\${JSON.stringify({
|
|
537
|
+
runId: this.runId,
|
|
538
|
+
entries,
|
|
539
|
+
})}'\`,
|
|
540
|
+
{ cwd: this.projectRoot, stdio: "pipe", timeout: 10000 },
|
|
541
|
+
);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
// Re-add failed entries for next attempt
|
|
544
|
+
this.buffer.unshift(...entries);
|
|
545
|
+
console.error(\`[log-streamer] Failed to flush logs: \${(err as Error).message}\`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
`;
|
|
550
|
+
const lines = writeIfNotDry(join(base, "log-streamer.ts"), content, ctx.dryRun);
|
|
551
|
+
return { file: "scripts/agent-ops/log-streamer.ts", lines };
|
|
552
|
+
}
|
|
553
|
+
// ── pr-creator.ts ──
|
|
554
|
+
function generatePrCreator(ctx, base) {
|
|
555
|
+
const content = `// Agent-Ops PR creator — generated by @yattalo/task-system
|
|
556
|
+
//
|
|
557
|
+
// Creates GitHub PRs from agent run branches using gh CLI.
|
|
558
|
+
|
|
559
|
+
import { execSync } from "node:child_process";
|
|
560
|
+
import type { RunContext } from "./types.js";
|
|
561
|
+
|
|
562
|
+
export interface PRResult {
|
|
563
|
+
url: string;
|
|
564
|
+
number: number;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function createPR(run: RunContext, summary?: string): PRResult | null {
|
|
568
|
+
try {
|
|
569
|
+
// Check if there are commits to push
|
|
570
|
+
const commitCount = execSync(
|
|
571
|
+
\`git rev-list --count \${run.baseBranch}..HEAD\`,
|
|
572
|
+
{ cwd: run.worktreePath, encoding: "utf-8" },
|
|
573
|
+
).trim();
|
|
574
|
+
|
|
575
|
+
if (commitCount === "0") {
|
|
576
|
+
console.log("[pr-creator] No commits to create PR for");
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Push branch
|
|
581
|
+
execSync(\`git push -u origin \${run.branchName}\`, {
|
|
582
|
+
cwd: run.worktreePath,
|
|
583
|
+
stdio: "pipe",
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// Create PR
|
|
587
|
+
const title = \`[Agent-Ops] \${run.agent}/\${run.jobId}: \${summary?.slice(0, 60) ?? "Automated changes"}\`;
|
|
588
|
+
const body = [
|
|
589
|
+
"## Agent-Ops Automated PR",
|
|
590
|
+
"",
|
|
591
|
+
\`- **Job**: \${run.jobId}\`,
|
|
592
|
+
\`- **Run**: \${run.runId}\`,
|
|
593
|
+
\`- **Agent**: \${run.agent}\`,
|
|
594
|
+
\`- **Commits**: \${commitCount}\`,
|
|
595
|
+
"",
|
|
596
|
+
summary ? \`## Summary\\n\${summary}\` : "",
|
|
597
|
+
"",
|
|
598
|
+
"---",
|
|
599
|
+
"Generated by @yattalo/task-system Agent-Ops",
|
|
600
|
+
].join("\\n");
|
|
601
|
+
|
|
602
|
+
const prUrl = execSync(
|
|
603
|
+
\`gh pr create --title "\${title.replace(/"/g, '\\\\"')}" --body "\${body.replace(/"/g, '\\\\"')}" --base \${run.baseBranch}\`,
|
|
604
|
+
{ cwd: run.worktreePath, encoding: "utf-8" },
|
|
605
|
+
).trim();
|
|
606
|
+
|
|
607
|
+
// Extract PR number from URL
|
|
608
|
+
const match = prUrl.match(/\\/(\\d+)$/);
|
|
609
|
+
const prNumber = match ? parseInt(match[1], 10) : 0;
|
|
610
|
+
|
|
611
|
+
return { url: prUrl, number: prNumber };
|
|
612
|
+
} catch (err) {
|
|
613
|
+
console.error(\`[pr-creator] Failed: \${(err as Error).message}\`);
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
`;
|
|
618
|
+
const lines = writeIfNotDry(join(base, "pr-creator.ts"), content, ctx.dryRun);
|
|
619
|
+
return { file: "scripts/agent-ops/pr-creator.ts", lines };
|
|
620
|
+
}
|
|
621
|
+
// ── cleanup.ts ──
|
|
622
|
+
function generateCleanup(ctx, base) {
|
|
623
|
+
const content = `// Agent-Ops cleanup — generated by @yattalo/task-system
|
|
624
|
+
//
|
|
625
|
+
// Garbage collects stale worktrees older than 24h.
|
|
626
|
+
|
|
627
|
+
import { listStaleWorktrees, removeWorktree } from "./git-isolation.js";
|
|
628
|
+
import type { AgentOpsConfig } from "./types.js";
|
|
629
|
+
|
|
630
|
+
export function cleanupStaleWorktrees(config: AgentOpsConfig, maxAgeMs?: number): number {
|
|
631
|
+
const stale = listStaleWorktrees(config, maxAgeMs);
|
|
632
|
+
|
|
633
|
+
let cleaned = 0;
|
|
634
|
+
for (const runId of stale) {
|
|
635
|
+
try {
|
|
636
|
+
removeWorktree(config, runId);
|
|
637
|
+
console.log(\`[cleanup] Removed stale worktree: \${runId}\`);
|
|
638
|
+
cleaned++;
|
|
639
|
+
} catch (err) {
|
|
640
|
+
console.error(\`[cleanup] Failed to remove \${runId}: \${(err as Error).message}\`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return cleaned;
|
|
645
|
+
}
|
|
646
|
+
`;
|
|
647
|
+
const lines = writeIfNotDry(join(base, "cleanup.ts"), content, ctx.dryRun);
|
|
648
|
+
return { file: "scripts/agent-ops/cleanup.ts", lines };
|
|
649
|
+
}
|
|
650
|
+
// ── runner.ts (main loop) ──
|
|
651
|
+
function generateRunner(ctx, base) {
|
|
652
|
+
const content = `#!/usr/bin/env npx tsx
|
|
653
|
+
// ============================================================
|
|
654
|
+
// Agent-Ops Runner — Main polling loop
|
|
655
|
+
// Generated by @yattalo/task-system for ${ctx.config.projectName}
|
|
656
|
+
// ============================================================
|
|
657
|
+
//
|
|
658
|
+
// Usage: npx tsx scripts/agent-ops/runner.ts
|
|
659
|
+
//
|
|
660
|
+
// This is the main Agent-Ops daemon. It:
|
|
661
|
+
// 1. Polls Convex for pending runs every N seconds
|
|
662
|
+
// 2. Evaluates scheduled jobs for due runs
|
|
663
|
+
// 3. Creates git worktrees for isolation
|
|
664
|
+
// 4. Launches agent CLI processes
|
|
665
|
+
// 5. Streams logs back to Convex
|
|
666
|
+
// 6. Cleans up on completion
|
|
667
|
+
|
|
668
|
+
import { execSync } from "node:child_process";
|
|
669
|
+
import { loadConfig } from "./config.js";
|
|
670
|
+
import { shouldRunNow, getNextRun } from "./scheduler.js";
|
|
671
|
+
import { acquireLane, releaseLane } from "./lane-queue.js";
|
|
672
|
+
import { createWorktree, removeWorktree, getWorktreeGitStats } from "./git-isolation.js";
|
|
673
|
+
import { launchAgent } from "./agent-launcher.js";
|
|
674
|
+
import { LogStreamer } from "./log-streamer.js";
|
|
675
|
+
import { createPR } from "./pr-creator.js";
|
|
676
|
+
import { cleanupStaleWorktrees } from "./cleanup.js";
|
|
677
|
+
import type { AgentOpsConfig, RunContext, LogEntry } from "./types.js";
|
|
678
|
+
|
|
679
|
+
const config = loadConfig();
|
|
680
|
+
let activeRuns = 0;
|
|
681
|
+
let totalRunsToday = 0;
|
|
682
|
+
let lastCleanup = 0;
|
|
683
|
+
|
|
684
|
+
function convexRun(fn: string, args?: Record<string, unknown>): string {
|
|
685
|
+
const argsStr = args ? \` '\${JSON.stringify(args)}'\` : "";
|
|
686
|
+
return execSync(\`npx convex run taskSystem/agentOps:\${fn}\${argsStr}\`, {
|
|
687
|
+
cwd: config.projectRoot,
|
|
688
|
+
encoding: "utf-8",
|
|
689
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
690
|
+
}).trim();
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function pollOnce(): Promise<void> {
|
|
694
|
+
// 1. Check for pending runs
|
|
695
|
+
try {
|
|
696
|
+
const pendingRaw = convexRun("getPendingRuns");
|
|
697
|
+
const pendingRuns = JSON.parse(pendingRaw);
|
|
698
|
+
|
|
699
|
+
for (const run of pendingRuns) {
|
|
700
|
+
if (activeRuns >= config.maxConcurrentRuns) break;
|
|
701
|
+
if (totalRunsToday >= config.maxRunsPerDay) {
|
|
702
|
+
console.log("[runner] Daily run limit reached");
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Check lane availability
|
|
707
|
+
if (run.laneId && !acquireLane(run.laneId, run.runId)) {
|
|
708
|
+
console.log(\`[runner] Lane \${run.laneId} busy, skipping \${run.runId}\`);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
executeRun(run).catch((err) => {
|
|
713
|
+
console.error(\`[runner] Run \${run.runId} failed: \${err.message}\`);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
} catch (err) {
|
|
717
|
+
console.error(\`[runner] Poll error: \${(err as Error).message}\`);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// 2. Evaluate scheduled jobs
|
|
721
|
+
try {
|
|
722
|
+
const jobsRaw = convexRun("listJobs", { enabled: true });
|
|
723
|
+
const jobs = JSON.parse(jobsRaw);
|
|
724
|
+
|
|
725
|
+
for (const job of jobs) {
|
|
726
|
+
if (job.scheduleType === "cron" && job.cronExpression) {
|
|
727
|
+
if (shouldRunNow(job.cronExpression, job.lastRunAt)) {
|
|
728
|
+
console.log(\`[runner] Scheduling job \${job.jobId}\`);
|
|
729
|
+
convexRun("triggerManualRun", { jobId: job.jobId });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
} catch (err) {
|
|
734
|
+
console.error(\`[runner] Schedule check error: \${(err as Error).message}\`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// 3. Periodic cleanup (every hour)
|
|
738
|
+
if (Date.now() - lastCleanup > 3600000) {
|
|
739
|
+
cleanupStaleWorktrees(config);
|
|
740
|
+
lastCleanup = Date.now();
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function executeRun(run: any): Promise<void> {
|
|
745
|
+
activeRuns++;
|
|
746
|
+
totalRunsToday++;
|
|
747
|
+
const logStreamer = new LogStreamer(run.runId, config.projectRoot);
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
// Update status to provisioning
|
|
751
|
+
convexRun("updateRunStatus", { runId: run.runId, status: "provisioning" });
|
|
752
|
+
|
|
753
|
+
// Create worktree
|
|
754
|
+
const baseBranch = run.baseBranch ?? "main";
|
|
755
|
+
const worktree = createWorktree(config, run.runId, baseBranch);
|
|
756
|
+
convexRun("updateRunGitInfo", {
|
|
757
|
+
runId: run.runId,
|
|
758
|
+
worktreePath: worktree.path,
|
|
759
|
+
branchName: worktree.branch,
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// Update status to running
|
|
763
|
+
convexRun("updateRunStatus", { runId: run.runId, status: "running" });
|
|
764
|
+
|
|
765
|
+
// Start log streaming
|
|
766
|
+
logStreamer.start();
|
|
767
|
+
|
|
768
|
+
const runCtx: RunContext = {
|
|
769
|
+
runId: run.runId,
|
|
770
|
+
jobId: run.jobId,
|
|
771
|
+
agent: run.agent,
|
|
772
|
+
prompt: run.prompt,
|
|
773
|
+
executionMode: run.executionMode,
|
|
774
|
+
worktreePath: worktree.path,
|
|
775
|
+
branchName: worktree.branch,
|
|
776
|
+
baseBranch,
|
|
777
|
+
startedAt: Date.now(),
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
// Launch agent
|
|
781
|
+
const { exitCode } = launchAgent(config, runCtx, (entry) => logStreamer.push(entry));
|
|
782
|
+
const code = await exitCode;
|
|
783
|
+
|
|
784
|
+
// Collect git stats
|
|
785
|
+
const stats = getWorktreeGitStats(worktree.path, baseBranch);
|
|
786
|
+
convexRun("updateRunGitInfo", { runId: run.runId, ...stats });
|
|
787
|
+
|
|
788
|
+
// Create PR if configured
|
|
789
|
+
let prUrl: string | undefined;
|
|
790
|
+
let prNumber: number | undefined;
|
|
791
|
+
|
|
792
|
+
// Get job config for PR creation
|
|
793
|
+
try {
|
|
794
|
+
const jobRaw = convexRun("getJob", { jobId: run.jobId });
|
|
795
|
+
const job = JSON.parse(jobRaw);
|
|
796
|
+
if (job.createPR && stats.commitCount > 0) {
|
|
797
|
+
const pr = createPR(runCtx, \`Job \${run.jobId} completed\`);
|
|
798
|
+
if (pr) {
|
|
799
|
+
prUrl = pr.url;
|
|
800
|
+
prNumber = pr.number;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
} catch {}
|
|
804
|
+
|
|
805
|
+
// Complete run
|
|
806
|
+
const status = code === 0 ? "success" : "failed";
|
|
807
|
+
convexRun("completeRun", {
|
|
808
|
+
runId: run.runId,
|
|
809
|
+
status,
|
|
810
|
+
exitCode: code,
|
|
811
|
+
summary: \`Agent \${run.agent} exited with code \${code}. \${stats.commitCount} commits, \${stats.filesChanged} files changed.\`,
|
|
812
|
+
...(prUrl ? { prUrl, prNumber } : {}),
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Cleanup worktree (keep if PR was created)
|
|
816
|
+
if (!prUrl) {
|
|
817
|
+
removeWorktree(config, run.runId);
|
|
818
|
+
}
|
|
819
|
+
} catch (err) {
|
|
820
|
+
convexRun("completeRun", {
|
|
821
|
+
runId: run.runId,
|
|
822
|
+
status: "failed",
|
|
823
|
+
error: (err as Error).message,
|
|
824
|
+
});
|
|
825
|
+
} finally {
|
|
826
|
+
logStreamer.stop();
|
|
827
|
+
activeRuns--;
|
|
828
|
+
if (run.laneId) releaseLane(run.laneId, run.runId);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Main loop
|
|
833
|
+
console.log("[agent-ops] Starting runner...");
|
|
834
|
+
console.log(\`[agent-ops] Poll interval: \${config.pollInterval}ms\`);
|
|
835
|
+
console.log(\`[agent-ops] Max concurrent: \${config.maxConcurrentRuns}\`);
|
|
836
|
+
|
|
837
|
+
setInterval(pollOnce, config.pollInterval);
|
|
838
|
+
pollOnce(); // Run immediately
|
|
839
|
+
`;
|
|
840
|
+
const lines = writeIfNotDry(join(base, "runner.ts"), content, ctx.dryRun);
|
|
841
|
+
return { file: "scripts/agent-ops/runner.ts", lines };
|
|
842
|
+
}
|
|
843
|
+
//# sourceMappingURL=agent-ops-scripts.js.map
|