prodboard 0.0.0 → 0.1.1
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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +302 -0
- package/bin/prodboard.ts +4 -0
- package/config.schema.json +100 -0
- package/package.json +47 -6
- package/src/commands/comments.ts +86 -0
- package/src/commands/daemon.ts +112 -0
- package/src/commands/init.ts +83 -0
- package/src/commands/install.ts +127 -0
- package/src/commands/issues.ts +268 -0
- package/src/commands/schedules.ts +276 -0
- package/src/config.ts +155 -0
- package/src/confirm.ts +14 -0
- package/src/cron.ts +121 -0
- package/src/db.ts +157 -0
- package/src/format.ts +99 -0
- package/src/ids.ts +5 -0
- package/src/index.ts +227 -0
- package/src/invocation.ts +102 -0
- package/src/logger.ts +84 -0
- package/src/mcp.ts +543 -0
- package/src/queries/comments.ts +31 -0
- package/src/queries/issues.ts +155 -0
- package/src/queries/runs.ts +159 -0
- package/src/queries/schedules.ts +115 -0
- package/src/scheduler.ts +411 -0
- package/src/templates.ts +43 -0
- package/src/types.ts +82 -0
- package/templates/CLAUDE.md +12 -0
- package/templates/config.jsonc +38 -0
- package/templates/mcp.json +8 -0
- package/templates/system-prompt-nogit.md +33 -0
- package/templates/system-prompt.md +31 -0
package/src/scheduler.ts
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import type { Config, Schedule, Run } from "./types.ts";
|
|
5
|
+
import { PRODBOARD_DIR } from "./config.ts";
|
|
6
|
+
import { shouldFire } from "./cron.ts";
|
|
7
|
+
import { detectEnvironment, buildInvocation } from "./invocation.ts";
|
|
8
|
+
import { listSchedules } from "./queries/schedules.ts";
|
|
9
|
+
import { createRun, updateRun, getRunningRuns, pruneOldRuns } from "./queries/runs.ts";
|
|
10
|
+
import { resolveTemplate, buildTemplateContext } from "./templates.ts";
|
|
11
|
+
|
|
12
|
+
// Stream JSON event types
|
|
13
|
+
export interface StreamEvent {
|
|
14
|
+
type: string;
|
|
15
|
+
session_id?: string;
|
|
16
|
+
tool?: string;
|
|
17
|
+
tool_input?: any;
|
|
18
|
+
result?: {
|
|
19
|
+
tokens_in?: number;
|
|
20
|
+
tokens_out?: number;
|
|
21
|
+
cost_usd?: number;
|
|
22
|
+
};
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseStreamJson(line: string): StreamEvent | null {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(line.trim());
|
|
29
|
+
return parsed;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function extractCostData(events: StreamEvent[]): {
|
|
36
|
+
tokens_in: number;
|
|
37
|
+
tokens_out: number;
|
|
38
|
+
cost_usd: number;
|
|
39
|
+
session_id: string | null;
|
|
40
|
+
tools_used: string[];
|
|
41
|
+
issues_touched: string[];
|
|
42
|
+
} {
|
|
43
|
+
let tokens_in = 0;
|
|
44
|
+
let tokens_out = 0;
|
|
45
|
+
let cost_usd = 0;
|
|
46
|
+
let session_id: string | null = null;
|
|
47
|
+
const tools_used = new Set<string>();
|
|
48
|
+
const issues_touched = new Set<string>();
|
|
49
|
+
|
|
50
|
+
for (const event of events) {
|
|
51
|
+
if (event.type === "init" && event.session_id) {
|
|
52
|
+
session_id = event.session_id;
|
|
53
|
+
}
|
|
54
|
+
if (event.type === "tool_use" && event.tool) {
|
|
55
|
+
tools_used.add(event.tool);
|
|
56
|
+
// Track prodboard issue IDs from tool inputs
|
|
57
|
+
if (event.tool.startsWith("mcp__prodboard__") && event.tool_input?.id) {
|
|
58
|
+
issues_touched.add(event.tool_input.id);
|
|
59
|
+
}
|
|
60
|
+
if (event.tool.startsWith("mcp__prodboard__") && event.tool_input?.issue_id) {
|
|
61
|
+
issues_touched.add(event.tool_input.issue_id);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (event.type === "result") {
|
|
65
|
+
if (event.result?.tokens_in) tokens_in = event.result.tokens_in;
|
|
66
|
+
if (event.result?.tokens_out) tokens_out = event.result.tokens_out;
|
|
67
|
+
if (event.result?.cost_usd) cost_usd = event.result.cost_usd;
|
|
68
|
+
}
|
|
69
|
+
// Also handle top-level fields some stream formats use
|
|
70
|
+
if (event.tokens_in) tokens_in = event.tokens_in;
|
|
71
|
+
if (event.tokens_out) tokens_out = event.tokens_out;
|
|
72
|
+
if (event.cost_usd) cost_usd = event.cost_usd;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
tokens_in,
|
|
77
|
+
tokens_out,
|
|
78
|
+
cost_usd,
|
|
79
|
+
session_id,
|
|
80
|
+
tools_used: [...tools_used],
|
|
81
|
+
issues_touched: [...issues_touched],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class RingBuffer {
|
|
86
|
+
private buffer: string[] = [];
|
|
87
|
+
constructor(private maxSize: number) {}
|
|
88
|
+
|
|
89
|
+
push(line: string): void {
|
|
90
|
+
this.buffer.push(line);
|
|
91
|
+
if (this.buffer.length > this.maxSize) {
|
|
92
|
+
this.buffer.shift();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
toString(): string {
|
|
97
|
+
return this.buffer.join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get lines(): string[] {
|
|
101
|
+
return [...this.buffer];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class ExecutionManager {
|
|
106
|
+
constructor(private db: Database, private config: Config) {}
|
|
107
|
+
|
|
108
|
+
async executeRun(schedule: Schedule, run: Run): Promise<void> {
|
|
109
|
+
const env = detectEnvironment(schedule.workdir, this.config);
|
|
110
|
+
|
|
111
|
+
// Resolve prompt templates
|
|
112
|
+
let resolvedPrompt = schedule.prompt;
|
|
113
|
+
try {
|
|
114
|
+
const context = buildTemplateContext(this.db, schedule.name);
|
|
115
|
+
resolvedPrompt = resolveTemplate(schedule.prompt, context);
|
|
116
|
+
|
|
117
|
+
if (schedule.inject_context) {
|
|
118
|
+
resolvedPrompt = `[prodboard: ${context.boardSummary}]\n\n${resolvedPrompt}`;
|
|
119
|
+
}
|
|
120
|
+
} catch {}
|
|
121
|
+
|
|
122
|
+
const args = buildInvocation(schedule, run, this.config, env, resolvedPrompt, this.db);
|
|
123
|
+
|
|
124
|
+
// Update run with PID (will be set after spawn)
|
|
125
|
+
const stdoutBuffer = new RingBuffer(500);
|
|
126
|
+
const stderrBuffer = new RingBuffer(100);
|
|
127
|
+
const events: StreamEvent[] = [];
|
|
128
|
+
|
|
129
|
+
let proc: any;
|
|
130
|
+
let timeoutId: Timer | undefined;
|
|
131
|
+
try {
|
|
132
|
+
proc = Bun.spawn(args, {
|
|
133
|
+
cwd: schedule.workdir,
|
|
134
|
+
stdout: "pipe",
|
|
135
|
+
stderr: "pipe",
|
|
136
|
+
env: process.env,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
updateRun(this.db, run.id, { pid: proc.pid });
|
|
140
|
+
|
|
141
|
+
// Set up timeout
|
|
142
|
+
const timeoutMs = this.config.daemon.runTimeoutSeconds * 1000;
|
|
143
|
+
timeoutId = setTimeout(() => {
|
|
144
|
+
try {
|
|
145
|
+
proc.kill("SIGTERM");
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
try { proc.kill("SIGKILL"); } catch {}
|
|
148
|
+
}, 10000);
|
|
149
|
+
} catch {}
|
|
150
|
+
}, timeoutMs);
|
|
151
|
+
|
|
152
|
+
// Read stdout line by line
|
|
153
|
+
if (proc.stdout) {
|
|
154
|
+
const reader = proc.stdout.getReader();
|
|
155
|
+
let buffer = "";
|
|
156
|
+
try {
|
|
157
|
+
while (true) {
|
|
158
|
+
const { value, done } = await reader.read();
|
|
159
|
+
if (done) break;
|
|
160
|
+
buffer += new TextDecoder().decode(value);
|
|
161
|
+
const lines = buffer.split("\n");
|
|
162
|
+
buffer = lines.pop() ?? "";
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
if (line.trim()) {
|
|
165
|
+
stdoutBuffer.push(line);
|
|
166
|
+
const event = parseStreamJson(line);
|
|
167
|
+
if (event) events.push(event);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch {}
|
|
172
|
+
reader.releaseLock();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Read stderr
|
|
176
|
+
if (proc.stderr) {
|
|
177
|
+
const stderrText = await new Response(proc.stderr).text();
|
|
178
|
+
for (const line of stderrText.split("\n")) {
|
|
179
|
+
if (line.trim()) stderrBuffer.push(line);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const exitCode = await proc.exited;
|
|
184
|
+
|
|
185
|
+
const costData = extractCostData(events);
|
|
186
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
187
|
+
|
|
188
|
+
let status: string;
|
|
189
|
+
if (exitCode === 0) {
|
|
190
|
+
status = "success";
|
|
191
|
+
} else if (exitCode === null) {
|
|
192
|
+
status = "timeout";
|
|
193
|
+
} else {
|
|
194
|
+
status = "failed";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
updateRun(this.db, run.id, {
|
|
198
|
+
status,
|
|
199
|
+
finished_at: now,
|
|
200
|
+
exit_code: exitCode,
|
|
201
|
+
stdout_tail: stdoutBuffer.toString(),
|
|
202
|
+
stderr_tail: stderrBuffer.toString(),
|
|
203
|
+
session_id: costData.session_id,
|
|
204
|
+
tokens_in: costData.tokens_in,
|
|
205
|
+
tokens_out: costData.tokens_out,
|
|
206
|
+
cost_usd: costData.cost_usd,
|
|
207
|
+
tools_used: costData.tools_used.length > 0 ? JSON.stringify(costData.tools_used) : null,
|
|
208
|
+
issues_touched: costData.issues_touched.length > 0 ? JSON.stringify(costData.issues_touched) : null,
|
|
209
|
+
});
|
|
210
|
+
} catch (err: any) {
|
|
211
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
212
|
+
updateRun(this.db, run.id, {
|
|
213
|
+
status: "failed",
|
|
214
|
+
finished_at: now,
|
|
215
|
+
stderr_tail: err instanceof Error ? err.message : String(err),
|
|
216
|
+
});
|
|
217
|
+
} finally {
|
|
218
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export class CronLoop {
|
|
224
|
+
private interval: Timer | null = null;
|
|
225
|
+
private lastFired: Map<string, number> = new Map();
|
|
226
|
+
private isRunning = false;
|
|
227
|
+
|
|
228
|
+
constructor(
|
|
229
|
+
private db: Database,
|
|
230
|
+
private config: Config,
|
|
231
|
+
private executionManager: ExecutionManager
|
|
232
|
+
) {}
|
|
233
|
+
|
|
234
|
+
start(): void {
|
|
235
|
+
this.interval = setInterval(() => this.tick(), 30_000);
|
|
236
|
+
// Also tick immediately
|
|
237
|
+
this.tick();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
stop(): void {
|
|
241
|
+
if (this.interval) {
|
|
242
|
+
clearInterval(this.interval);
|
|
243
|
+
this.interval = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async tick(): Promise<void> {
|
|
248
|
+
if (this.isRunning) return;
|
|
249
|
+
this.isRunning = true;
|
|
250
|
+
try {
|
|
251
|
+
const now = new Date();
|
|
252
|
+
const minuteTs = Math.floor(now.getTime() / 60000);
|
|
253
|
+
|
|
254
|
+
const schedules = listSchedules(this.db);
|
|
255
|
+
|
|
256
|
+
for (const schedule of schedules) {
|
|
257
|
+
try {
|
|
258
|
+
if (!shouldFire(schedule.cron, now)) continue;
|
|
259
|
+
|
|
260
|
+
// Prevent double-fire within same minute
|
|
261
|
+
const lastFiredMinute = this.lastFired.get(schedule.id);
|
|
262
|
+
if (lastFiredMinute === minuteTs) continue;
|
|
263
|
+
|
|
264
|
+
// Check concurrent limit
|
|
265
|
+
const runningRuns = getRunningRuns(this.db);
|
|
266
|
+
if (runningRuns.length >= this.config.daemon.maxConcurrentRuns) continue;
|
|
267
|
+
|
|
268
|
+
const run = createRun(this.db, {
|
|
269
|
+
schedule_id: schedule.id,
|
|
270
|
+
prompt_used: schedule.prompt,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Mark fired only after successful run creation
|
|
274
|
+
this.lastFired.set(schedule.id, minuteTs);
|
|
275
|
+
|
|
276
|
+
// Execute async - don't block the loop
|
|
277
|
+
this.executionManager.executeRun(schedule, run).catch(() => {});
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.error(`[prodboard] Error evaluating schedule ${schedule.id}:`, err instanceof Error ? err.message : String(err));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.error("[prodboard] Error in tick:", err instanceof Error ? err.message : String(err));
|
|
284
|
+
} finally {
|
|
285
|
+
this.isRunning = false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export class CleanupWorker {
|
|
291
|
+
private interval: Timer | null = null;
|
|
292
|
+
|
|
293
|
+
constructor(private db: Database, private config: Config) {}
|
|
294
|
+
|
|
295
|
+
start(): void {
|
|
296
|
+
this.interval = setInterval(() => this.cleanup(), 3600_000); // 1 hour
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
stop(): void {
|
|
300
|
+
if (this.interval) {
|
|
301
|
+
clearInterval(this.interval);
|
|
302
|
+
this.interval = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async cleanup(): Promise<void> {
|
|
307
|
+
try {
|
|
308
|
+
const pruned = pruneOldRuns(this.db, this.config.daemon.runRetentionDays);
|
|
309
|
+
if (pruned > 0) {
|
|
310
|
+
console.error(`[prodboard] Cleaned up ${pruned} old runs`);
|
|
311
|
+
}
|
|
312
|
+
} catch {}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export class Daemon {
|
|
317
|
+
private cronLoop: CronLoop;
|
|
318
|
+
private cleanupWorker: CleanupWorker;
|
|
319
|
+
private executionManager: ExecutionManager;
|
|
320
|
+
|
|
321
|
+
constructor(private db: Database, private config: Config) {
|
|
322
|
+
this.executionManager = new ExecutionManager(db, config);
|
|
323
|
+
this.cronLoop = new CronLoop(db, config, this.executionManager);
|
|
324
|
+
this.cleanupWorker = new CleanupWorker(db, config);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async start(): Promise<void> {
|
|
328
|
+
this.recoverCrashedRuns();
|
|
329
|
+
this.writePidFile();
|
|
330
|
+
this.cronLoop.start();
|
|
331
|
+
this.cleanupWorker.start();
|
|
332
|
+
|
|
333
|
+
process.on("SIGTERM", () => this.stop());
|
|
334
|
+
process.on("SIGINT", () => this.stop());
|
|
335
|
+
|
|
336
|
+
console.error(`[prodboard] Daemon started (PID ${process.pid})`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async stop(): Promise<void> {
|
|
340
|
+
console.error("[prodboard] Shutting down...");
|
|
341
|
+
this.cronLoop.stop();
|
|
342
|
+
this.cleanupWorker.stop();
|
|
343
|
+
|
|
344
|
+
// Wait for running processes
|
|
345
|
+
const running = getRunningRuns(this.db);
|
|
346
|
+
if (running.length > 0) {
|
|
347
|
+
console.error(`[prodboard] Waiting for ${running.length} running process(es)...`);
|
|
348
|
+
// Give them 30 seconds
|
|
349
|
+
const deadline = Date.now() + 30_000;
|
|
350
|
+
while (Date.now() < deadline) {
|
|
351
|
+
const still = getRunningRuns(this.db);
|
|
352
|
+
if (still.length === 0) break;
|
|
353
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Kill remaining processes first, then mark cancelled
|
|
357
|
+
const stillRunning = getRunningRuns(this.db);
|
|
358
|
+
for (const run of stillRunning) {
|
|
359
|
+
if (run.pid) {
|
|
360
|
+
try { process.kill(run.pid, "SIGTERM"); } catch {}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Brief wait for processes to exit after SIGTERM
|
|
364
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
365
|
+
for (const run of stillRunning) {
|
|
366
|
+
if (run.pid) {
|
|
367
|
+
try { process.kill(run.pid, "SIGKILL"); } catch {}
|
|
368
|
+
}
|
|
369
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
370
|
+
updateRun(this.db, run.id, { status: "cancelled", finished_at: now });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
this.removePidFile();
|
|
375
|
+
process.exit(0);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private writePidFile(): void {
|
|
379
|
+
const pidFile = path.join(PRODBOARD_DIR, "daemon.pid");
|
|
380
|
+
fs.writeFileSync(pidFile, String(process.pid));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private removePidFile(): void {
|
|
384
|
+
try {
|
|
385
|
+
const pidFile = path.join(PRODBOARD_DIR, "daemon.pid");
|
|
386
|
+
fs.unlinkSync(pidFile);
|
|
387
|
+
} catch {}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private recoverCrashedRuns(): void {
|
|
391
|
+
const running = getRunningRuns(this.db);
|
|
392
|
+
for (const run of running) {
|
|
393
|
+
let alive = false;
|
|
394
|
+
if (run.pid) {
|
|
395
|
+
try {
|
|
396
|
+
process.kill(run.pid, 0);
|
|
397
|
+
alive = true;
|
|
398
|
+
} catch {}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!alive) {
|
|
402
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
403
|
+
updateRun(this.db, run.id, {
|
|
404
|
+
status: "failed",
|
|
405
|
+
finished_at: now,
|
|
406
|
+
stderr_tail: "Recovered from crash — process not found",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
package/src/templates.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { getIssueCounts } from "./queries/issues.ts";
|
|
3
|
+
|
|
4
|
+
export interface TemplateContext {
|
|
5
|
+
boardSummary: string;
|
|
6
|
+
todoCount: number;
|
|
7
|
+
inProgressCount: number;
|
|
8
|
+
datetime: string;
|
|
9
|
+
scheduleName: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveTemplate(template: string, context: TemplateContext): string {
|
|
13
|
+
return template
|
|
14
|
+
.replace(/\{\{board_summary\}\}/g, context.boardSummary)
|
|
15
|
+
.replace(/\{\{todo_count\}\}/g, String(context.todoCount))
|
|
16
|
+
.replace(/\{\{in_progress_count\}\}/g, String(context.inProgressCount))
|
|
17
|
+
.replace(/\{\{datetime\}\}/g, context.datetime)
|
|
18
|
+
.replace(/\{\{schedule_name\}\}/g, context.scheduleName);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildBoardSummaryLine(db: Database): string {
|
|
22
|
+
const counts = getIssueCounts(db);
|
|
23
|
+
const parts: string[] = [];
|
|
24
|
+
|
|
25
|
+
const statuses = ["todo", "in-progress", "review", "done"];
|
|
26
|
+
for (const status of statuses) {
|
|
27
|
+
const count = counts[status] ?? 0;
|
|
28
|
+
parts.push(`${count} ${status}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return parts.join(", ");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildTemplateContext(db: Database, scheduleName: string): TemplateContext {
|
|
35
|
+
const counts = getIssueCounts(db);
|
|
36
|
+
return {
|
|
37
|
+
boardSummary: buildBoardSummaryLine(db),
|
|
38
|
+
todoCount: counts.todo ?? 0,
|
|
39
|
+
inProgressCount: counts["in-progress"] ?? 0,
|
|
40
|
+
datetime: new Date().toISOString(),
|
|
41
|
+
scheduleName,
|
|
42
|
+
};
|
|
43
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
general: {
|
|
3
|
+
statuses: string[];
|
|
4
|
+
defaultStatus: string;
|
|
5
|
+
idPrefix: string;
|
|
6
|
+
};
|
|
7
|
+
daemon: {
|
|
8
|
+
maxConcurrentRuns: number;
|
|
9
|
+
maxTurns: number;
|
|
10
|
+
hardMaxTurns: number;
|
|
11
|
+
runTimeoutSeconds: number;
|
|
12
|
+
runRetentionDays: number;
|
|
13
|
+
logLevel: string;
|
|
14
|
+
logMaxSizeMb: number;
|
|
15
|
+
logMaxFiles: number;
|
|
16
|
+
defaultAllowedTools: string[];
|
|
17
|
+
nonGitDefaultAllowedTools: string[];
|
|
18
|
+
useWorktrees: "auto" | "always" | "never";
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Issue {
|
|
23
|
+
id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
description: string;
|
|
26
|
+
status: string;
|
|
27
|
+
created_at: string;
|
|
28
|
+
updated_at: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Comment {
|
|
32
|
+
id: string;
|
|
33
|
+
issue_id: string;
|
|
34
|
+
body: string;
|
|
35
|
+
author: string;
|
|
36
|
+
created_at: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Schedule {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
cron: string;
|
|
43
|
+
prompt: string;
|
|
44
|
+
workdir: string;
|
|
45
|
+
enabled: number;
|
|
46
|
+
max_turns: number | null;
|
|
47
|
+
allowed_tools: string | null;
|
|
48
|
+
use_worktree: number;
|
|
49
|
+
inject_context: number;
|
|
50
|
+
persist_session: number;
|
|
51
|
+
agents_json: string | null;
|
|
52
|
+
source: string;
|
|
53
|
+
created_at: string;
|
|
54
|
+
updated_at: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Run {
|
|
58
|
+
id: string;
|
|
59
|
+
schedule_id: string;
|
|
60
|
+
status: string;
|
|
61
|
+
prompt_used: string;
|
|
62
|
+
pid: number | null;
|
|
63
|
+
started_at: string;
|
|
64
|
+
finished_at: string | null;
|
|
65
|
+
exit_code: number | null;
|
|
66
|
+
stdout_tail: string | null;
|
|
67
|
+
stderr_tail: string | null;
|
|
68
|
+
session_id: string | null;
|
|
69
|
+
worktree_path: string | null;
|
|
70
|
+
tokens_in: number | null;
|
|
71
|
+
tokens_out: number | null;
|
|
72
|
+
cost_usd: number | null;
|
|
73
|
+
tools_used: string | null;
|
|
74
|
+
issues_touched: string | null;
|
|
75
|
+
schedule_name?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface EnvironmentInfo {
|
|
79
|
+
hasGit: boolean;
|
|
80
|
+
hasClaude: boolean;
|
|
81
|
+
worktreeSupported: boolean;
|
|
82
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# prodboard
|
|
2
|
+
|
|
3
|
+
This project uses prodboard for issue tracking. The prodboard MCP server is configured and available.
|
|
4
|
+
|
|
5
|
+
## Issue Workflow
|
|
6
|
+
- Check `board_summary` at the start of each session
|
|
7
|
+
- Use `pick_next_issue` to claim work
|
|
8
|
+
- Add comments to track progress
|
|
9
|
+
- Call `complete_issue` when done
|
|
10
|
+
|
|
11
|
+
## Statuses
|
|
12
|
+
todo → in-progress → review → done → archived
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
// prodboard configuration
|
|
3
|
+
// See config.schema.json for full documentation
|
|
4
|
+
|
|
5
|
+
"general": {
|
|
6
|
+
// Issue statuses (order matters for board display)
|
|
7
|
+
// "statuses": ["todo", "in-progress", "review", "done", "archived"],
|
|
8
|
+
|
|
9
|
+
// Default status for new issues
|
|
10
|
+
// "defaultStatus": "todo",
|
|
11
|
+
|
|
12
|
+
// Optional prefix for issue IDs (e.g., "PB-")
|
|
13
|
+
// "idPrefix": ""
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
"daemon": {
|
|
17
|
+
// Maximum concurrent scheduled runs
|
|
18
|
+
// "maxConcurrentRuns": 2,
|
|
19
|
+
|
|
20
|
+
// Default max turns for Claude invocations
|
|
21
|
+
// "maxTurns": 50,
|
|
22
|
+
|
|
23
|
+
// Absolute maximum turns (cannot be overridden by schedules)
|
|
24
|
+
// "hardMaxTurns": 200,
|
|
25
|
+
|
|
26
|
+
// Timeout per run in seconds (default: 30 minutes)
|
|
27
|
+
// "runTimeoutSeconds": 1800,
|
|
28
|
+
|
|
29
|
+
// Days to retain run history
|
|
30
|
+
// "runRetentionDays": 30,
|
|
31
|
+
|
|
32
|
+
// Log level: "debug", "info", "warn", "error"
|
|
33
|
+
// "logLevel": "info",
|
|
34
|
+
|
|
35
|
+
// Worktree usage: "auto", "always", "never"
|
|
36
|
+
// "useWorktrees": "auto"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# prodboard — Issue Tracker Context
|
|
2
|
+
|
|
3
|
+
You have access to a prodboard MCP server for issue tracking. Use these tools to manage work:
|
|
4
|
+
|
|
5
|
+
## Workflow
|
|
6
|
+
1. At the start of a session, call `board_summary` to see the current state
|
|
7
|
+
2. Use `pick_next_issue` to claim work (moves issue to in-progress)
|
|
8
|
+
3. Work on the issue, adding comments with `add_comment` to track progress
|
|
9
|
+
4. When done, call `complete_issue` with a summary of what was accomplished
|
|
10
|
+
|
|
11
|
+
## Tools Available
|
|
12
|
+
- `board_summary` — Overview of all issues and their statuses
|
|
13
|
+
- `list_issues` — List issues with optional status/search filters
|
|
14
|
+
- `get_issue` — Get full issue details including comments
|
|
15
|
+
- `create_issue` — Create a new issue
|
|
16
|
+
- `update_issue` — Update issue fields (title, description, status)
|
|
17
|
+
- `delete_issue` — Delete an issue
|
|
18
|
+
- `add_comment` — Add a comment to an issue
|
|
19
|
+
- `pick_next_issue` — Pick the next todo issue and start working on it
|
|
20
|
+
- `complete_issue` — Mark an issue as done with optional completion comment
|
|
21
|
+
|
|
22
|
+
## Guidelines
|
|
23
|
+
- Always check the board before starting work
|
|
24
|
+
- Add comments to track progress and decisions
|
|
25
|
+
- Move issues through statuses: todo → in-progress → review → done
|
|
26
|
+
- Create new issues for discovered work or bugs
|
|
27
|
+
- Use descriptive titles and add context in descriptions
|
|
28
|
+
|
|
29
|
+
## Non-Git Environment
|
|
30
|
+
- This environment does not have git version control
|
|
31
|
+
- Focus on file-based operations using Read, Edit, Write, Glob, Grep, and Bash tools
|
|
32
|
+
- Be extra careful with file modifications since there is no version history
|
|
33
|
+
- Consider creating backups before making significant changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# prodboard — Issue Tracker Context
|
|
2
|
+
|
|
3
|
+
You have access to a prodboard MCP server for issue tracking. Use these tools to manage work:
|
|
4
|
+
|
|
5
|
+
## Workflow
|
|
6
|
+
1. At the start of a session, call `board_summary` to see the current state
|
|
7
|
+
2. Use `pick_next_issue` to claim work (moves issue to in-progress)
|
|
8
|
+
3. Work on the issue, adding comments with `add_comment` to track progress
|
|
9
|
+
4. When done, call `complete_issue` with a summary of what was accomplished
|
|
10
|
+
|
|
11
|
+
## Tools Available
|
|
12
|
+
- `board_summary` — Overview of all issues and their statuses
|
|
13
|
+
- `list_issues` — List issues with optional status/search filters
|
|
14
|
+
- `get_issue` — Get full issue details including comments
|
|
15
|
+
- `create_issue` — Create a new issue
|
|
16
|
+
- `update_issue` — Update issue fields (title, description, status)
|
|
17
|
+
- `delete_issue` — Delete an issue
|
|
18
|
+
- `add_comment` — Add a comment to an issue
|
|
19
|
+
- `pick_next_issue` — Pick the next todo issue and start working on it
|
|
20
|
+
- `complete_issue` — Mark an issue as done with optional completion comment
|
|
21
|
+
|
|
22
|
+
## Guidelines
|
|
23
|
+
- Always check the board before starting work
|
|
24
|
+
- Add comments to track progress and decisions
|
|
25
|
+
- Move issues through statuses: todo → in-progress → review → done
|
|
26
|
+
- Create new issues for discovered work or bugs
|
|
27
|
+
- Use descriptive titles and add context in descriptions
|
|
28
|
+
|
|
29
|
+
## Git Integration
|
|
30
|
+
- Commit related changes together with descriptive messages
|
|
31
|
+
- Reference issue IDs in commit messages when relevant
|