@yail259/overnight 0.3.0 → 1.0.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 +71 -225
- package/bin/overnight.js +2 -0
- package/dist/cli.js +103919 -24513
- package/package.json +27 -6
- package/.context/notes.md +0 -0
- package/.context/todos.md +0 -0
- package/bun.lock +0 -63
- package/src/cli.ts +0 -821
- package/src/goal-runner.ts +0 -709
- package/src/notify.ts +0 -50
- package/src/planner.ts +0 -238
- package/src/report.ts +0 -115
- package/src/runner.ts +0 -663
- package/src/security.ts +0 -162
- package/src/types.ts +0 -129
- package/tsconfig.json +0 -15
package/src/cli.ts
DELETED
|
@@ -1,821 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
4
|
-
import { parse as parseYaml } from "yaml";
|
|
5
|
-
import {
|
|
6
|
-
type JobConfig,
|
|
7
|
-
type JobResult,
|
|
8
|
-
type TasksFile,
|
|
9
|
-
type GoalConfig,
|
|
10
|
-
type SecurityConfig,
|
|
11
|
-
DEFAULT_TOOLS,
|
|
12
|
-
DEFAULT_TIMEOUT,
|
|
13
|
-
DEFAULT_STALL_TIMEOUT,
|
|
14
|
-
DEFAULT_VERIFY_PROMPT,
|
|
15
|
-
DEFAULT_STATE_FILE,
|
|
16
|
-
DEFAULT_GOAL_STATE_FILE,
|
|
17
|
-
DEFAULT_NTFY_TOPIC,
|
|
18
|
-
DEFAULT_MAX_TURNS,
|
|
19
|
-
DEFAULT_MAX_ITERATIONS,
|
|
20
|
-
DEFAULT_DENY_PATTERNS,
|
|
21
|
-
} from "./types.js";
|
|
22
|
-
import { validateSecurityConfig } from "./security.js";
|
|
23
|
-
import {
|
|
24
|
-
runJob,
|
|
25
|
-
runJobsWithState,
|
|
26
|
-
loadState,
|
|
27
|
-
resultsToJson,
|
|
28
|
-
taskKey,
|
|
29
|
-
} from "./runner.js";
|
|
30
|
-
import { sendNtfyNotification } from "./notify.js";
|
|
31
|
-
import { generateReport } from "./report.js";
|
|
32
|
-
import { runGoal, parseGoalFile } from "./goal-runner.js";
|
|
33
|
-
import { runPlanner } from "./planner.js";
|
|
34
|
-
|
|
35
|
-
const AGENT_HELP = `
|
|
36
|
-
# overnight - Autonomous Build Runner for Claude Code
|
|
37
|
-
|
|
38
|
-
Two modes: goal-driven autonomous loops, or task-list batch jobs.
|
|
39
|
-
|
|
40
|
-
## Quick Start
|
|
41
|
-
|
|
42
|
-
\`\`\`bash
|
|
43
|
-
# Hammer mode: just give it a goal and go
|
|
44
|
-
overnight hammer "Build a multiplayer MMO"
|
|
45
|
-
|
|
46
|
-
# Or: design session first, then autonomous build
|
|
47
|
-
overnight plan "Build a multiplayer game" # Interactive design → goal.yaml
|
|
48
|
-
overnight run goal.yaml --notify # Autonomous build loop
|
|
49
|
-
|
|
50
|
-
# Task mode: explicit task list
|
|
51
|
-
overnight run tasks.yaml --notify
|
|
52
|
-
\`\`\`
|
|
53
|
-
|
|
54
|
-
## Commands
|
|
55
|
-
|
|
56
|
-
| Command | Description |
|
|
57
|
-
|---------|-------------|
|
|
58
|
-
| \`overnight hammer "<goal>"\` | Autonomous build loop from a string |
|
|
59
|
-
| \`overnight plan "<goal>"\` | Interactive design session → goal.yaml |
|
|
60
|
-
| \`overnight run <file>\` | Run goal.yaml (loop) or tasks.yaml (batch) |
|
|
61
|
-
| \`overnight resume <file>\` | Resume interrupted run from checkpoint |
|
|
62
|
-
| \`overnight single "<prompt>"\` | Run a single task directly |
|
|
63
|
-
| \`overnight init\` | Create example goal.yaml or tasks.yaml |
|
|
64
|
-
|
|
65
|
-
## Goal Mode (goal.yaml)
|
|
66
|
-
|
|
67
|
-
Autonomous convergence loop: agent iterates toward a goal, then a separate
|
|
68
|
-
gate agent verifies everything before declaring done.
|
|
69
|
-
|
|
70
|
-
\`\`\`yaml
|
|
71
|
-
goal: "Build a clone of Flappy Bird with leaderboard"
|
|
72
|
-
|
|
73
|
-
acceptance_criteria:
|
|
74
|
-
- "Game renders and is playable in browser"
|
|
75
|
-
- "Leaderboard persists scores to localStorage"
|
|
76
|
-
|
|
77
|
-
verification_commands:
|
|
78
|
-
- "npm run build"
|
|
79
|
-
- "npm test"
|
|
80
|
-
|
|
81
|
-
constraints:
|
|
82
|
-
- "Use vanilla JS, no frameworks"
|
|
83
|
-
|
|
84
|
-
max_iterations: 15
|
|
85
|
-
\`\`\`
|
|
86
|
-
|
|
87
|
-
## Task Mode (tasks.yaml)
|
|
88
|
-
|
|
89
|
-
Explicit task list with optional dependency DAG.
|
|
90
|
-
|
|
91
|
-
\`\`\`yaml
|
|
92
|
-
defaults:
|
|
93
|
-
timeout_seconds: 300
|
|
94
|
-
verify: true
|
|
95
|
-
allowed_tools: [Read, Edit, Write, Glob, Grep]
|
|
96
|
-
|
|
97
|
-
tasks:
|
|
98
|
-
- "Fix the bug in auth.py"
|
|
99
|
-
- prompt: "Add input validation"
|
|
100
|
-
timeout_seconds: 600
|
|
101
|
-
\`\`\`
|
|
102
|
-
|
|
103
|
-
## Key Options
|
|
104
|
-
|
|
105
|
-
| Option | Description |
|
|
106
|
-
|--------|-------------|
|
|
107
|
-
| \`-o, --output <file>\` | Save results JSON |
|
|
108
|
-
| \`-r, --report <file>\` | Generate markdown report |
|
|
109
|
-
| \`-s, --state-file <file>\` | Custom checkpoint file |
|
|
110
|
-
| \`--max-iterations <n>\` | Max build loop iterations (goal mode) |
|
|
111
|
-
| \`--notify\` | Send push notification via ntfy.sh |
|
|
112
|
-
| \`-q, --quiet\` | Minimal output |
|
|
113
|
-
|
|
114
|
-
## Example Workflows
|
|
115
|
-
|
|
116
|
-
\`\`\`bash
|
|
117
|
-
# Simplest: just hammer a goal overnight
|
|
118
|
-
nohup overnight hammer "Build a REST API with auth and tests" --notify > overnight.log 2>&1 &
|
|
119
|
-
|
|
120
|
-
# Design first, then run
|
|
121
|
-
overnight plan "Build a REST API with auth"
|
|
122
|
-
nohup overnight run goal.yaml --notify > overnight.log 2>&1 &
|
|
123
|
-
|
|
124
|
-
# Batch tasks overnight
|
|
125
|
-
nohup overnight run tasks.yaml --notify -r report.md > overnight.log 2>&1 &
|
|
126
|
-
|
|
127
|
-
# Resume after crash
|
|
128
|
-
overnight resume goal.yaml
|
|
129
|
-
\`\`\`
|
|
130
|
-
|
|
131
|
-
## Exit Codes
|
|
132
|
-
|
|
133
|
-
- 0: All tasks succeeded / gate passed
|
|
134
|
-
- 1: Failures occurred / gate failed
|
|
135
|
-
|
|
136
|
-
## Files Created
|
|
137
|
-
|
|
138
|
-
- \`.overnight-goal-state.json\` - Goal mode checkpoint
|
|
139
|
-
- \`.overnight-iterations/\` - Per-iteration state + summaries
|
|
140
|
-
- \`.overnight-state.json\` - Task mode checkpoint
|
|
141
|
-
- \`report.md\` - Summary report (if -r used)
|
|
142
|
-
|
|
143
|
-
Run \`overnight <command> --help\` for command-specific options.
|
|
144
|
-
`;
|
|
145
|
-
|
|
146
|
-
// --- File type detection ---
|
|
147
|
-
|
|
148
|
-
function isGoalFile(path: string): boolean {
|
|
149
|
-
try {
|
|
150
|
-
const content = readFileSync(path, "utf-8");
|
|
151
|
-
const data = parseYaml(content) as Record<string, unknown>;
|
|
152
|
-
return typeof data?.goal === "string";
|
|
153
|
-
} catch {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
interface ParsedConfig {
|
|
159
|
-
configs: JobConfig[];
|
|
160
|
-
security?: SecurityConfig;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function parseTasksFile(path: string, cliSecurity?: Partial<SecurityConfig>): ParsedConfig {
|
|
164
|
-
const content = readFileSync(path, "utf-8");
|
|
165
|
-
let data: TasksFile | (string | JobConfig)[];
|
|
166
|
-
try {
|
|
167
|
-
data = parseYaml(content) as TasksFile | (string | JobConfig)[];
|
|
168
|
-
} catch (e) {
|
|
169
|
-
const error = e as Error;
|
|
170
|
-
console.error(`\x1b[31mError parsing ${path}:\x1b[0m`);
|
|
171
|
-
console.error(` ${error.message.split('\n')[0]}`);
|
|
172
|
-
process.exit(1);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const tasks = Array.isArray(data) ? data : data.tasks ?? [];
|
|
176
|
-
const defaults = Array.isArray(data) ? {} : data.defaults ?? {};
|
|
177
|
-
|
|
178
|
-
// Merge CLI security options with file security options (CLI takes precedence)
|
|
179
|
-
const fileSecurity = (!Array.isArray(data) && data.defaults?.security) || {};
|
|
180
|
-
const security: SecurityConfig | undefined = (cliSecurity || Object.keys(fileSecurity).length > 0)
|
|
181
|
-
? {
|
|
182
|
-
...fileSecurity,
|
|
183
|
-
...cliSecurity,
|
|
184
|
-
// Use default deny patterns if none specified
|
|
185
|
-
deny_patterns: cliSecurity?.deny_patterns ?? fileSecurity.deny_patterns ?? DEFAULT_DENY_PATTERNS,
|
|
186
|
-
}
|
|
187
|
-
: undefined;
|
|
188
|
-
|
|
189
|
-
const configs = tasks.map((task) => {
|
|
190
|
-
if (typeof task === "string") {
|
|
191
|
-
return {
|
|
192
|
-
prompt: task,
|
|
193
|
-
timeout_seconds: defaults.timeout_seconds ?? DEFAULT_TIMEOUT,
|
|
194
|
-
stall_timeout_seconds:
|
|
195
|
-
defaults.stall_timeout_seconds ?? DEFAULT_STALL_TIMEOUT,
|
|
196
|
-
verify: defaults.verify ?? true,
|
|
197
|
-
verify_prompt: defaults.verify_prompt ?? DEFAULT_VERIFY_PROMPT,
|
|
198
|
-
allowed_tools: defaults.allowed_tools,
|
|
199
|
-
security,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
203
|
-
id: task.id ?? undefined,
|
|
204
|
-
depends_on: task.depends_on ?? undefined,
|
|
205
|
-
prompt: task.prompt,
|
|
206
|
-
working_dir: task.working_dir ?? undefined,
|
|
207
|
-
timeout_seconds:
|
|
208
|
-
task.timeout_seconds ?? defaults.timeout_seconds ?? DEFAULT_TIMEOUT,
|
|
209
|
-
stall_timeout_seconds:
|
|
210
|
-
task.stall_timeout_seconds ??
|
|
211
|
-
defaults.stall_timeout_seconds ??
|
|
212
|
-
DEFAULT_STALL_TIMEOUT,
|
|
213
|
-
verify: task.verify ?? defaults.verify ?? true,
|
|
214
|
-
verify_prompt:
|
|
215
|
-
task.verify_prompt ?? defaults.verify_prompt ?? DEFAULT_VERIFY_PROMPT,
|
|
216
|
-
allowed_tools: task.allowed_tools ?? defaults.allowed_tools,
|
|
217
|
-
security: task.security ?? security,
|
|
218
|
-
};
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
return { configs, security };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function printSummary(results: JobResult[]): void {
|
|
225
|
-
const statusColors: Record<string, string> = {
|
|
226
|
-
success: "\x1b[32m",
|
|
227
|
-
failed: "\x1b[31m",
|
|
228
|
-
timeout: "\x1b[33m",
|
|
229
|
-
stalled: "\x1b[35m",
|
|
230
|
-
verification_failed: "\x1b[33m",
|
|
231
|
-
};
|
|
232
|
-
const reset = "\x1b[0m";
|
|
233
|
-
const bold = "\x1b[1m";
|
|
234
|
-
|
|
235
|
-
console.log(`\n${bold}Job Results${reset}`);
|
|
236
|
-
console.log("─".repeat(70));
|
|
237
|
-
|
|
238
|
-
results.forEach((r, i) => {
|
|
239
|
-
const color = statusColors[r.status] ?? "";
|
|
240
|
-
const task = r.task.length > 40 ? r.task.slice(0, 40) + "..." : r.task;
|
|
241
|
-
const verified = r.verified ? "✓" : "✗";
|
|
242
|
-
console.log(
|
|
243
|
-
`${i + 1}. ${color}${r.status.padEnd(12)}${reset} ${r.duration_seconds.toFixed(1).padStart(6)}s ${verified} ${task}`
|
|
244
|
-
);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
const succeeded = results.filter((r) => r.status === "success").length;
|
|
248
|
-
console.log(
|
|
249
|
-
`\n${bold}Summary:${reset} ${succeeded}/${results.length} succeeded`
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const program = new Command();
|
|
254
|
-
|
|
255
|
-
program
|
|
256
|
-
.name("overnight")
|
|
257
|
-
.description("Batch job runner for Claude Code")
|
|
258
|
-
.version("0.3.0")
|
|
259
|
-
.action(() => {
|
|
260
|
-
console.log(AGENT_HELP);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
program
|
|
264
|
-
.command("run")
|
|
265
|
-
.description("Run goal.yaml (autonomous loop) or tasks.yaml (batch jobs)")
|
|
266
|
-
.argument("<file>", "Path to goal.yaml or tasks.yaml")
|
|
267
|
-
.option("-o, --output <file>", "Output file for results JSON")
|
|
268
|
-
.option("-q, --quiet", "Minimal output")
|
|
269
|
-
.option("-s, --state-file <file>", "Custom state file path")
|
|
270
|
-
.option("--notify", "Send push notification via ntfy.sh")
|
|
271
|
-
.option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC)
|
|
272
|
-
.option("-r, --report <file>", "Generate markdown report")
|
|
273
|
-
.option("--sandbox <dir>", "Sandbox directory (restrict file access)")
|
|
274
|
-
.option("--max-turns <n>", "Max agent iterations per task", String(DEFAULT_MAX_TURNS))
|
|
275
|
-
.option("--max-iterations <n>", "Max build loop iterations (goal mode)", String(DEFAULT_MAX_ITERATIONS))
|
|
276
|
-
.option("--audit-log <file>", "Audit log file path")
|
|
277
|
-
.option("--no-security", "Disable default security (deny patterns)")
|
|
278
|
-
.action(async (inputFile, opts) => {
|
|
279
|
-
if (!existsSync(inputFile)) {
|
|
280
|
-
console.error(`Error: File not found: ${inputFile}`);
|
|
281
|
-
process.exit(1);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Detect file type and dispatch
|
|
285
|
-
if (isGoalFile(inputFile)) {
|
|
286
|
-
// --- Goal mode ---
|
|
287
|
-
const goal = parseGoalFile(inputFile);
|
|
288
|
-
|
|
289
|
-
// Apply CLI overrides
|
|
290
|
-
if (opts.maxIterations) {
|
|
291
|
-
goal.max_iterations = parseInt(opts.maxIterations, 10);
|
|
292
|
-
}
|
|
293
|
-
if (opts.sandbox) {
|
|
294
|
-
goal.defaults = goal.defaults ?? {};
|
|
295
|
-
goal.defaults.security = goal.defaults.security ?? {};
|
|
296
|
-
goal.defaults.security.sandbox_dir = opts.sandbox;
|
|
297
|
-
}
|
|
298
|
-
if (opts.maxTurns) {
|
|
299
|
-
goal.defaults = goal.defaults ?? {};
|
|
300
|
-
goal.defaults.security = goal.defaults.security ?? {};
|
|
301
|
-
goal.defaults.security.max_turns = parseInt(opts.maxTurns, 10);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const log = opts.quiet ? undefined : (msg: string) => console.log(msg);
|
|
305
|
-
const startTime = Date.now();
|
|
306
|
-
|
|
307
|
-
const runState = await runGoal(goal, {
|
|
308
|
-
stateFile: opts.stateFile ?? DEFAULT_GOAL_STATE_FILE,
|
|
309
|
-
log,
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
const totalDuration = (Date.now() - startTime) / 1000;
|
|
313
|
-
|
|
314
|
-
if (opts.notify) {
|
|
315
|
-
const passed = runState.status === "gate_passed";
|
|
316
|
-
const title = passed
|
|
317
|
-
? `overnight: Goal completed (${runState.iterations.length} iterations)`
|
|
318
|
-
: `overnight: ${runState.status} after ${runState.iterations.length} iterations`;
|
|
319
|
-
const message = passed
|
|
320
|
-
? `Gate passed. ${runState.iterations.length} iterations.`
|
|
321
|
-
: `Status: ${runState.status}. Check report for details.`;
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
await fetch(`https://ntfy.sh/${opts.notifyTopic ?? DEFAULT_NTFY_TOPIC}`, {
|
|
325
|
-
method: "POST",
|
|
326
|
-
headers: {
|
|
327
|
-
Title: title,
|
|
328
|
-
Priority: passed ? "default" : "high",
|
|
329
|
-
Tags: passed ? "white_check_mark" : "warning",
|
|
330
|
-
},
|
|
331
|
-
body: message,
|
|
332
|
-
});
|
|
333
|
-
if (!opts.quiet) console.log(`\x1b[2mNotification sent\x1b[0m`);
|
|
334
|
-
} catch {
|
|
335
|
-
if (!opts.quiet) console.log("\x1b[33mWarning: Failed to send notification\x1b[0m");
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Print summary
|
|
340
|
-
if (!opts.quiet) {
|
|
341
|
-
console.log(`\n\x1b[1m━━━ Goal Run Summary ━━━\x1b[0m`);
|
|
342
|
-
console.log(`Status: ${runState.status === "gate_passed" ? "\x1b[32m" : "\x1b[31m"}${runState.status}\x1b[0m`);
|
|
343
|
-
console.log(`Iterations: ${runState.iterations.length}`);
|
|
344
|
-
console.log(`Gate attempts: ${runState.gate_results.length}`);
|
|
345
|
-
|
|
346
|
-
// Duration formatting
|
|
347
|
-
let durationStr: string;
|
|
348
|
-
if (totalDuration >= 3600) {
|
|
349
|
-
const hours = Math.floor(totalDuration / 3600);
|
|
350
|
-
const mins = Math.floor((totalDuration % 3600) / 60);
|
|
351
|
-
durationStr = `${hours}h ${mins}m`;
|
|
352
|
-
} else if (totalDuration >= 60) {
|
|
353
|
-
const mins = Math.floor(totalDuration / 60);
|
|
354
|
-
const secs = Math.floor(totalDuration % 60);
|
|
355
|
-
durationStr = `${mins}m ${secs}s`;
|
|
356
|
-
} else {
|
|
357
|
-
durationStr = `${totalDuration.toFixed(1)}s`;
|
|
358
|
-
}
|
|
359
|
-
console.log(`Duration: ${durationStr}`);
|
|
360
|
-
|
|
361
|
-
if (runState.gate_results.length > 0) {
|
|
362
|
-
const lastGate = runState.gate_results[runState.gate_results.length - 1];
|
|
363
|
-
console.log(`\nGate: ${lastGate.summary}`);
|
|
364
|
-
for (const check of lastGate.checks) {
|
|
365
|
-
const icon = check.passed ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
|
|
366
|
-
console.log(` ${icon} ${check.name}`);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (runState.status !== "gate_passed") {
|
|
372
|
-
process.exit(1);
|
|
373
|
-
}
|
|
374
|
-
} else {
|
|
375
|
-
// --- Task mode (legacy) ---
|
|
376
|
-
const cliSecurity: Partial<SecurityConfig> | undefined = opts.security === false
|
|
377
|
-
? undefined
|
|
378
|
-
: {
|
|
379
|
-
...(opts.sandbox && { sandbox_dir: opts.sandbox }),
|
|
380
|
-
...(opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) }),
|
|
381
|
-
...(opts.auditLog && { audit_log: opts.auditLog }),
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
const { configs, security } = parseTasksFile(inputFile, cliSecurity);
|
|
385
|
-
if (configs.length === 0) {
|
|
386
|
-
console.error("No tasks found in file");
|
|
387
|
-
process.exit(1);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const existingState = loadState(opts.stateFile ?? DEFAULT_STATE_FILE);
|
|
391
|
-
if (existingState) {
|
|
392
|
-
const done = Object.keys(existingState.completed).length;
|
|
393
|
-
const pending = configs.filter(c => !(taskKey(c) in existingState.completed)).length;
|
|
394
|
-
console.log(`\x1b[1movernight: Resuming — ${done} done, ${pending} remaining\x1b[0m`);
|
|
395
|
-
console.log(`\x1b[2mLast checkpoint: ${existingState.timestamp}\x1b[0m`);
|
|
396
|
-
} else {
|
|
397
|
-
console.log(`\x1b[1movernight: Running ${configs.length} jobs...\x1b[0m`);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (security && !opts.quiet) {
|
|
401
|
-
console.log("\x1b[2mSecurity:\x1b[0m");
|
|
402
|
-
validateSecurityConfig(security);
|
|
403
|
-
}
|
|
404
|
-
console.log("");
|
|
405
|
-
|
|
406
|
-
const log = opts.quiet ? undefined : (msg: string) => console.log(msg);
|
|
407
|
-
const startTime = Date.now();
|
|
408
|
-
const reloadConfigs = () => parseTasksFile(inputFile, cliSecurity).configs;
|
|
409
|
-
|
|
410
|
-
const results = await runJobsWithState(configs, {
|
|
411
|
-
stateFile: opts.stateFile,
|
|
412
|
-
log,
|
|
413
|
-
reloadConfigs,
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
const totalDuration = (Date.now() - startTime) / 1000;
|
|
417
|
-
|
|
418
|
-
if (opts.notify) {
|
|
419
|
-
const success = await sendNtfyNotification(results, totalDuration, opts.notifyTopic);
|
|
420
|
-
if (success) {
|
|
421
|
-
console.log(`\x1b[2mNotification sent to ntfy.sh/${opts.notifyTopic}\x1b[0m`);
|
|
422
|
-
} else {
|
|
423
|
-
console.log("\x1b[33mWarning: Failed to send notification\x1b[0m");
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
if (opts.report) {
|
|
428
|
-
generateReport(results, totalDuration, opts.report);
|
|
429
|
-
console.log(`\x1b[2mReport saved to ${opts.report}\x1b[0m`);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
if (!opts.quiet) {
|
|
433
|
-
printSummary(results);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
if (opts.output) {
|
|
437
|
-
writeFileSync(opts.output, resultsToJson(results));
|
|
438
|
-
console.log(`\n\x1b[2mResults saved to ${opts.output}\x1b[0m`);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (results.some((r) => r.status !== "success")) {
|
|
442
|
-
process.exit(1);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
program
|
|
448
|
-
.command("resume")
|
|
449
|
-
.description("Resume a previous run from saved state")
|
|
450
|
-
.argument("<tasks-file>", "Path to tasks.yaml file")
|
|
451
|
-
.option("-o, --output <file>", "Output file for results JSON")
|
|
452
|
-
.option("-q, --quiet", "Minimal output")
|
|
453
|
-
.option("-s, --state-file <file>", "Custom state file path")
|
|
454
|
-
.option("--notify", "Send push notification via ntfy.sh")
|
|
455
|
-
.option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC)
|
|
456
|
-
.option("-r, --report <file>", "Generate markdown report")
|
|
457
|
-
.option("--sandbox <dir>", "Sandbox directory (restrict file access)")
|
|
458
|
-
.option("--max-turns <n>", "Max agent iterations per task", String(DEFAULT_MAX_TURNS))
|
|
459
|
-
.option("--audit-log <file>", "Audit log file path")
|
|
460
|
-
.option("--no-security", "Disable default security (deny patterns)")
|
|
461
|
-
.action(async (tasksFile, opts) => {
|
|
462
|
-
const stateFile = opts.stateFile ?? DEFAULT_STATE_FILE;
|
|
463
|
-
const state = loadState(stateFile);
|
|
464
|
-
|
|
465
|
-
if (!state) {
|
|
466
|
-
console.error(`No state file found at ${stateFile}`);
|
|
467
|
-
console.error("Run 'overnight run' first to start jobs.");
|
|
468
|
-
process.exit(1);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (!existsSync(tasksFile)) {
|
|
472
|
-
console.error(`Error: File not found: ${tasksFile}`);
|
|
473
|
-
process.exit(1);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Build CLI security config
|
|
477
|
-
const cliSecurity: Partial<SecurityConfig> | undefined = opts.security === false
|
|
478
|
-
? undefined
|
|
479
|
-
: {
|
|
480
|
-
...(opts.sandbox && { sandbox_dir: opts.sandbox }),
|
|
481
|
-
...(opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) }),
|
|
482
|
-
...(opts.auditLog && { audit_log: opts.auditLog }),
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
const { configs, security } = parseTasksFile(tasksFile, cliSecurity);
|
|
486
|
-
if (configs.length === 0) {
|
|
487
|
-
console.error("No tasks found in file");
|
|
488
|
-
process.exit(1);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const completedCount = Object.keys(state.completed).length;
|
|
492
|
-
const pendingCount = configs.filter(c => !(taskKey(c) in state.completed)).length;
|
|
493
|
-
console.log(
|
|
494
|
-
`\x1b[1movernight: Resuming — ${completedCount} done, ${pendingCount} remaining\x1b[0m`
|
|
495
|
-
);
|
|
496
|
-
console.log(`\x1b[2mLast checkpoint: ${state.timestamp}\x1b[0m`);
|
|
497
|
-
|
|
498
|
-
// Show security config if enabled
|
|
499
|
-
if (security && !opts.quiet) {
|
|
500
|
-
console.log("\x1b[2mSecurity:\x1b[0m");
|
|
501
|
-
validateSecurityConfig(security);
|
|
502
|
-
}
|
|
503
|
-
console.log("");
|
|
504
|
-
|
|
505
|
-
const log = opts.quiet ? undefined : (msg: string) => console.log(msg);
|
|
506
|
-
const startTime = Date.now();
|
|
507
|
-
const reloadConfigs = () => parseTasksFile(tasksFile, cliSecurity).configs;
|
|
508
|
-
|
|
509
|
-
const results = await runJobsWithState(configs, {
|
|
510
|
-
stateFile,
|
|
511
|
-
log,
|
|
512
|
-
reloadConfigs,
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
const totalDuration = (Date.now() - startTime) / 1000;
|
|
516
|
-
|
|
517
|
-
if (opts.notify) {
|
|
518
|
-
const success = await sendNtfyNotification(
|
|
519
|
-
results,
|
|
520
|
-
totalDuration,
|
|
521
|
-
opts.notifyTopic
|
|
522
|
-
);
|
|
523
|
-
if (success) {
|
|
524
|
-
console.log(`\x1b[2mNotification sent to ntfy.sh/${opts.notifyTopic}\x1b[0m`);
|
|
525
|
-
} else {
|
|
526
|
-
console.log("\x1b[33mWarning: Failed to send notification\x1b[0m");
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (opts.report) {
|
|
531
|
-
generateReport(results, totalDuration, opts.report);
|
|
532
|
-
console.log(`\x1b[2mReport saved to ${opts.report}\x1b[0m`);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (!opts.quiet) {
|
|
536
|
-
printSummary(results);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if (opts.output) {
|
|
540
|
-
writeFileSync(opts.output, resultsToJson(results));
|
|
541
|
-
console.log(`\n\x1b[2mResults saved to ${opts.output}\x1b[0m`);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (results.some((r) => r.status !== "success")) {
|
|
545
|
-
process.exit(1);
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
program
|
|
550
|
-
.command("single")
|
|
551
|
-
.description("Run a single job directly")
|
|
552
|
-
.argument("<prompt>", "The task prompt")
|
|
553
|
-
.option("-t, --timeout <seconds>", "Timeout in seconds", "300")
|
|
554
|
-
.option("--verify", "Run verification pass", true)
|
|
555
|
-
.option("--no-verify", "Skip verification pass")
|
|
556
|
-
.option("-T, --tools <tool...>", "Allowed tools")
|
|
557
|
-
.option("--sandbox <dir>", "Sandbox directory (restrict file access)")
|
|
558
|
-
.option("--max-turns <n>", "Max agent iterations", String(DEFAULT_MAX_TURNS))
|
|
559
|
-
.option("--no-security", "Disable default security (deny patterns)")
|
|
560
|
-
.action(async (prompt, opts) => {
|
|
561
|
-
// Build security config
|
|
562
|
-
const security: SecurityConfig | undefined = opts.security === false
|
|
563
|
-
? undefined
|
|
564
|
-
: {
|
|
565
|
-
...(opts.sandbox && { sandbox_dir: opts.sandbox }),
|
|
566
|
-
...(opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) }),
|
|
567
|
-
deny_patterns: DEFAULT_DENY_PATTERNS,
|
|
568
|
-
};
|
|
569
|
-
|
|
570
|
-
const config: JobConfig = {
|
|
571
|
-
prompt,
|
|
572
|
-
timeout_seconds: parseInt(opts.timeout, 10),
|
|
573
|
-
verify: opts.verify,
|
|
574
|
-
allowed_tools: opts.tools,
|
|
575
|
-
security,
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
const log = (msg: string) => console.log(msg);
|
|
579
|
-
const result = await runJob(config, log);
|
|
580
|
-
|
|
581
|
-
if (result.status === "success") {
|
|
582
|
-
console.log("\n\x1b[32mSuccess\x1b[0m");
|
|
583
|
-
if (result.result) {
|
|
584
|
-
console.log(result.result);
|
|
585
|
-
}
|
|
586
|
-
} else {
|
|
587
|
-
console.log(`\n\x1b[31m${result.status}\x1b[0m`);
|
|
588
|
-
if (result.error) {
|
|
589
|
-
console.log(`\x1b[31m${result.error}\x1b[0m`);
|
|
590
|
-
}
|
|
591
|
-
process.exit(1);
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
program
|
|
596
|
-
.command("hammer")
|
|
597
|
-
.description("Autonomous build loop from an inline goal string")
|
|
598
|
-
.argument("<goal>", "The goal to work toward")
|
|
599
|
-
.option("--max-iterations <n>", "Max build loop iterations", String(DEFAULT_MAX_ITERATIONS))
|
|
600
|
-
.option("--max-turns <n>", "Max agent turns per iteration", String(DEFAULT_MAX_TURNS))
|
|
601
|
-
.option("-t, --timeout <seconds>", "Timeout per iteration in seconds", "600")
|
|
602
|
-
.option("-T, --tools <tool...>", "Allowed tools")
|
|
603
|
-
.option("--sandbox <dir>", "Sandbox directory")
|
|
604
|
-
.option("-s, --state-file <file>", "Custom state file path")
|
|
605
|
-
.option("--notify", "Send push notification via ntfy.sh")
|
|
606
|
-
.option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC)
|
|
607
|
-
.option("-q, --quiet", "Minimal output")
|
|
608
|
-
.option("--no-security", "Disable default security")
|
|
609
|
-
.action(async (goalStr, opts) => {
|
|
610
|
-
const goal: GoalConfig = {
|
|
611
|
-
goal: goalStr,
|
|
612
|
-
max_iterations: parseInt(opts.maxIterations, 10),
|
|
613
|
-
defaults: {
|
|
614
|
-
timeout_seconds: parseInt(opts.timeout, 10),
|
|
615
|
-
allowed_tools: opts.tools ?? [...DEFAULT_TOOLS, "Bash"],
|
|
616
|
-
security: opts.security === false
|
|
617
|
-
? undefined
|
|
618
|
-
: {
|
|
619
|
-
...(opts.sandbox && { sandbox_dir: opts.sandbox }),
|
|
620
|
-
max_turns: parseInt(opts.maxTurns, 10),
|
|
621
|
-
deny_patterns: DEFAULT_DENY_PATTERNS,
|
|
622
|
-
},
|
|
623
|
-
},
|
|
624
|
-
};
|
|
625
|
-
|
|
626
|
-
const log = opts.quiet ? undefined : (msg: string) => console.log(msg);
|
|
627
|
-
const startTime = Date.now();
|
|
628
|
-
|
|
629
|
-
const runState = await runGoal(goal, {
|
|
630
|
-
stateFile: opts.stateFile ?? DEFAULT_GOAL_STATE_FILE,
|
|
631
|
-
log,
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
const totalDuration = (Date.now() - startTime) / 1000;
|
|
635
|
-
|
|
636
|
-
if (opts.notify) {
|
|
637
|
-
const passed = runState.status === "gate_passed";
|
|
638
|
-
try {
|
|
639
|
-
await fetch(`https://ntfy.sh/${opts.notifyTopic ?? DEFAULT_NTFY_TOPIC}`, {
|
|
640
|
-
method: "POST",
|
|
641
|
-
headers: {
|
|
642
|
-
Title: passed
|
|
643
|
-
? `overnight: Goal completed (${runState.iterations.length} iterations)`
|
|
644
|
-
: `overnight: ${runState.status} after ${runState.iterations.length} iterations`,
|
|
645
|
-
Priority: passed ? "default" : "high",
|
|
646
|
-
Tags: passed ? "white_check_mark" : "warning",
|
|
647
|
-
},
|
|
648
|
-
body: passed
|
|
649
|
-
? `Gate passed. ${runState.iterations.length} iterations.`
|
|
650
|
-
: `Status: ${runState.status}. Check report for details.`,
|
|
651
|
-
});
|
|
652
|
-
if (!opts.quiet) console.log(`\x1b[2mNotification sent\x1b[0m`);
|
|
653
|
-
} catch {
|
|
654
|
-
if (!opts.quiet) console.log("\x1b[33mWarning: Failed to send notification\x1b[0m");
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
if (!opts.quiet) {
|
|
659
|
-
console.log(`\n\x1b[1m━━━ Hammer Summary ━━━\x1b[0m`);
|
|
660
|
-
console.log(`Status: ${runState.status === "gate_passed" ? "\x1b[32m" : "\x1b[31m"}${runState.status}\x1b[0m`);
|
|
661
|
-
console.log(`Iterations: ${runState.iterations.length}`);
|
|
662
|
-
console.log(`Gate attempts: ${runState.gate_results.length}`);
|
|
663
|
-
|
|
664
|
-
let durationStr: string;
|
|
665
|
-
if (totalDuration >= 3600) {
|
|
666
|
-
const hours = Math.floor(totalDuration / 3600);
|
|
667
|
-
const mins = Math.floor((totalDuration % 3600) / 60);
|
|
668
|
-
durationStr = `${hours}h ${mins}m`;
|
|
669
|
-
} else if (totalDuration >= 60) {
|
|
670
|
-
const mins = Math.floor(totalDuration / 60);
|
|
671
|
-
const secs = Math.floor(totalDuration % 60);
|
|
672
|
-
durationStr = `${mins}m ${secs}s`;
|
|
673
|
-
} else {
|
|
674
|
-
durationStr = `${totalDuration.toFixed(1)}s`;
|
|
675
|
-
}
|
|
676
|
-
console.log(`Duration: ${durationStr}`);
|
|
677
|
-
|
|
678
|
-
if (runState.gate_results.length > 0) {
|
|
679
|
-
const lastGate = runState.gate_results[runState.gate_results.length - 1];
|
|
680
|
-
console.log(`\nGate: ${lastGate.summary}`);
|
|
681
|
-
for (const check of lastGate.checks) {
|
|
682
|
-
const icon = check.passed ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
|
|
683
|
-
console.log(` ${icon} ${check.name}`);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
if (runState.status !== "gate_passed") {
|
|
689
|
-
process.exit(1);
|
|
690
|
-
}
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
program
|
|
694
|
-
.command("plan")
|
|
695
|
-
.description("Interactive design session to create a goal.yaml")
|
|
696
|
-
.argument("<goal>", "High-level goal description")
|
|
697
|
-
.option("-o, --output <file>", "Output file path", "goal.yaml")
|
|
698
|
-
.action(async (goal, opts) => {
|
|
699
|
-
const result = await runPlanner(goal, {
|
|
700
|
-
outputFile: opts.output,
|
|
701
|
-
log: (msg: string) => console.log(msg),
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
if (!result) {
|
|
705
|
-
process.exit(1);
|
|
706
|
-
}
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
program
|
|
710
|
-
.command("init")
|
|
711
|
-
.description("Create an example goal.yaml or tasks.yaml")
|
|
712
|
-
.option("--tasks", "Create tasks.yaml instead of goal.yaml")
|
|
713
|
-
.action((opts) => {
|
|
714
|
-
if (opts.tasks) {
|
|
715
|
-
// Legacy tasks.yaml template
|
|
716
|
-
const example = `# overnight task file
|
|
717
|
-
# Run with: overnight run tasks.yaml
|
|
718
|
-
|
|
719
|
-
defaults:
|
|
720
|
-
timeout_seconds: 300 # 5 minutes per task
|
|
721
|
-
verify: true # Run verification after each task
|
|
722
|
-
|
|
723
|
-
# Secure defaults - no Bash, just file operations
|
|
724
|
-
allowed_tools:
|
|
725
|
-
- Read
|
|
726
|
-
- Edit
|
|
727
|
-
- Write
|
|
728
|
-
- Glob
|
|
729
|
-
- Grep
|
|
730
|
-
|
|
731
|
-
# Security settings (optional - deny_patterns enabled by default)
|
|
732
|
-
security:
|
|
733
|
-
sandbox_dir: "." # Restrict to current directory
|
|
734
|
-
max_turns: 100 # Prevent runaway agents
|
|
735
|
-
# audit_log: "overnight-audit.log" # Uncomment to enable
|
|
736
|
-
|
|
737
|
-
tasks:
|
|
738
|
-
# Simple string format
|
|
739
|
-
- "Find and fix any TODO comments in the codebase"
|
|
740
|
-
|
|
741
|
-
# Dict format with overrides
|
|
742
|
-
- prompt: "Add input validation to all form handlers"
|
|
743
|
-
timeout_seconds: 600 # Allow more time
|
|
744
|
-
|
|
745
|
-
- prompt: "Review code for security issues"
|
|
746
|
-
verify: false # Don't need to verify a review
|
|
747
|
-
|
|
748
|
-
# Can add Bash for specific tasks that need it
|
|
749
|
-
- prompt: "Run the test suite and fix any failures"
|
|
750
|
-
allowed_tools:
|
|
751
|
-
- Read
|
|
752
|
-
- Edit
|
|
753
|
-
- Bash
|
|
754
|
-
- Glob
|
|
755
|
-
- Grep
|
|
756
|
-
`;
|
|
757
|
-
|
|
758
|
-
if (existsSync("tasks.yaml")) {
|
|
759
|
-
console.log("\x1b[33mtasks.yaml already exists\x1b[0m");
|
|
760
|
-
process.exit(1);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
writeFileSync("tasks.yaml", example);
|
|
764
|
-
console.log("\x1b[32mCreated tasks.yaml\x1b[0m");
|
|
765
|
-
console.log("Edit the file, then run: \x1b[1movernight run tasks.yaml\x1b[0m");
|
|
766
|
-
} else {
|
|
767
|
-
// Goal mode template (new default)
|
|
768
|
-
const example = `# overnight goal file
|
|
769
|
-
# Run with: overnight run goal.yaml
|
|
770
|
-
#
|
|
771
|
-
# Or use "overnight plan" for an interactive design session:
|
|
772
|
-
# overnight plan "Build a multiplayer game"
|
|
773
|
-
|
|
774
|
-
goal: "Describe your project goal here"
|
|
775
|
-
|
|
776
|
-
acceptance_criteria:
|
|
777
|
-
- "The project builds without errors"
|
|
778
|
-
- "All tests pass"
|
|
779
|
-
- "Core features are functional"
|
|
780
|
-
|
|
781
|
-
verification_commands:
|
|
782
|
-
- "npm run build"
|
|
783
|
-
- "npm test"
|
|
784
|
-
|
|
785
|
-
constraints:
|
|
786
|
-
- "Don't modify existing API contracts"
|
|
787
|
-
- "Keep dependencies minimal"
|
|
788
|
-
|
|
789
|
-
# How many build iterations before stopping
|
|
790
|
-
max_iterations: 15
|
|
791
|
-
|
|
792
|
-
# Stop if remaining items don't shrink for this many iterations
|
|
793
|
-
convergence_threshold: 3
|
|
794
|
-
|
|
795
|
-
defaults:
|
|
796
|
-
timeout_seconds: 600 # 10 minutes per iteration
|
|
797
|
-
allowed_tools:
|
|
798
|
-
- Read
|
|
799
|
-
- Edit
|
|
800
|
-
- Write
|
|
801
|
-
- Glob
|
|
802
|
-
- Grep
|
|
803
|
-
- Bash
|
|
804
|
-
security:
|
|
805
|
-
sandbox_dir: "."
|
|
806
|
-
max_turns: 150
|
|
807
|
-
`;
|
|
808
|
-
|
|
809
|
-
if (existsSync("goal.yaml")) {
|
|
810
|
-
console.log("\x1b[33mgoal.yaml already exists\x1b[0m");
|
|
811
|
-
process.exit(1);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
writeFileSync("goal.yaml", example);
|
|
815
|
-
console.log("\x1b[32mCreated goal.yaml\x1b[0m");
|
|
816
|
-
console.log("Edit the file, then run: \x1b[1movernight run goal.yaml\x1b[0m");
|
|
817
|
-
console.log("\x1b[2mTip: Use 'overnight plan \"your goal\"' for an interactive design session\x1b[0m");
|
|
818
|
-
}
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
program.parse();
|