@yail259/overnight 0.2.0 → 0.3.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/.context/notes.md +0 -0
- package/.context/todos.md +0 -0
- package/dist/cli.js +1205 -181
- package/package.json +2 -2
- package/src/cli.ts +410 -127
- package/src/goal-runner.ts +709 -0
- package/src/planner.ts +238 -0
- package/src/runner.ts +7 -4
- package/src/security.ts +6 -6
- package/src/types.ts +48 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yail259/overnight",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Autonomous build runner for Claude Code - goal-driven loops with verification gates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"overnight": "./dist/cli.js"
|
package/src/cli.ts
CHANGED
|
@@ -6,13 +6,17 @@ import {
|
|
|
6
6
|
type JobConfig,
|
|
7
7
|
type JobResult,
|
|
8
8
|
type TasksFile,
|
|
9
|
+
type GoalConfig,
|
|
9
10
|
type SecurityConfig,
|
|
11
|
+
DEFAULT_TOOLS,
|
|
10
12
|
DEFAULT_TIMEOUT,
|
|
11
13
|
DEFAULT_STALL_TIMEOUT,
|
|
12
14
|
DEFAULT_VERIFY_PROMPT,
|
|
13
15
|
DEFAULT_STATE_FILE,
|
|
16
|
+
DEFAULT_GOAL_STATE_FILE,
|
|
14
17
|
DEFAULT_NTFY_TOPIC,
|
|
15
18
|
DEFAULT_MAX_TURNS,
|
|
19
|
+
DEFAULT_MAX_ITERATIONS,
|
|
16
20
|
DEFAULT_DENY_PATTERNS,
|
|
17
21
|
} from "./types.js";
|
|
18
22
|
import { validateSecurityConfig } from "./security.js";
|
|
@@ -25,55 +29,75 @@ import {
|
|
|
25
29
|
} from "./runner.js";
|
|
26
30
|
import { sendNtfyNotification } from "./notify.js";
|
|
27
31
|
import { generateReport } from "./report.js";
|
|
32
|
+
import { runGoal, parseGoalFile } from "./goal-runner.js";
|
|
33
|
+
import { runPlanner } from "./planner.js";
|
|
28
34
|
|
|
29
35
|
const AGENT_HELP = `
|
|
30
|
-
# overnight -
|
|
36
|
+
# overnight - Autonomous Build Runner for Claude Code
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
Two modes: goal-driven autonomous loops, or task-list batch jobs.
|
|
33
39
|
|
|
34
40
|
## Quick Start
|
|
35
41
|
|
|
36
42
|
\`\`\`bash
|
|
37
|
-
#
|
|
38
|
-
overnight
|
|
43
|
+
# Hammer mode: just give it a goal and go
|
|
44
|
+
overnight hammer "Build a multiplayer MMO"
|
|
39
45
|
|
|
40
|
-
#
|
|
41
|
-
overnight
|
|
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
|
|
42
49
|
|
|
43
|
-
#
|
|
44
|
-
overnight run tasks.yaml --notify
|
|
50
|
+
# Task mode: explicit task list
|
|
51
|
+
overnight run tasks.yaml --notify
|
|
45
52
|
\`\`\`
|
|
46
53
|
|
|
47
54
|
## Commands
|
|
48
55
|
|
|
49
56
|
| Command | Description |
|
|
50
57
|
|---------|-------------|
|
|
51
|
-
| \`overnight
|
|
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) |
|
|
52
61
|
| \`overnight resume <file>\` | Resume interrupted run from checkpoint |
|
|
53
62
|
| \`overnight single "<prompt>"\` | Run a single task directly |
|
|
54
|
-
| \`overnight init\` | Create example tasks.yaml |
|
|
63
|
+
| \`overnight init\` | Create example goal.yaml or tasks.yaml |
|
|
55
64
|
|
|
56
|
-
##
|
|
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.
|
|
57
90
|
|
|
58
91
|
\`\`\`yaml
|
|
59
92
|
defaults:
|
|
60
|
-
timeout_seconds: 300
|
|
61
|
-
verify: true
|
|
62
|
-
allowed_tools:
|
|
63
|
-
- Read
|
|
64
|
-
- Edit
|
|
65
|
-
- Glob
|
|
66
|
-
- Grep
|
|
93
|
+
timeout_seconds: 300
|
|
94
|
+
verify: true
|
|
95
|
+
allowed_tools: [Read, Edit, Write, Glob, Grep]
|
|
67
96
|
|
|
68
97
|
tasks:
|
|
69
|
-
# Simple format
|
|
70
98
|
- "Fix the bug in auth.py"
|
|
71
|
-
|
|
72
|
-
# Detailed format
|
|
73
99
|
- prompt: "Add input validation"
|
|
74
100
|
timeout_seconds: 600
|
|
75
|
-
verify: false
|
|
76
|
-
allowed_tools: [Read, Edit, Bash, Glob, Grep]
|
|
77
101
|
\`\`\`
|
|
78
102
|
|
|
79
103
|
## Key Options
|
|
@@ -83,48 +107,54 @@ tasks:
|
|
|
83
107
|
| \`-o, --output <file>\` | Save results JSON |
|
|
84
108
|
| \`-r, --report <file>\` | Generate markdown report |
|
|
85
109
|
| \`-s, --state-file <file>\` | Custom checkpoint file |
|
|
110
|
+
| \`--max-iterations <n>\` | Max build loop iterations (goal mode) |
|
|
86
111
|
| \`--notify\` | Send push notification via ntfy.sh |
|
|
87
|
-
| \`--notify-topic <topic>\` | ntfy.sh topic (default: overnight) |
|
|
88
112
|
| \`-q, --quiet\` | Minimal output |
|
|
89
113
|
|
|
90
|
-
## Features
|
|
91
|
-
|
|
92
|
-
1. **Crash Recovery**: Auto-checkpoints after each job. Use \`overnight resume\` to continue.
|
|
93
|
-
2. **Retry Logic**: Auto-retries 3x on API/network errors with exponential backoff.
|
|
94
|
-
3. **Notifications**: \`--notify\` sends summary to ntfy.sh (free, no signup).
|
|
95
|
-
4. **Reports**: \`-r report.md\` generates markdown summary with next steps.
|
|
96
|
-
5. **Security**: No Bash by default. Whitelist tools per-task.
|
|
97
|
-
|
|
98
114
|
## Example Workflows
|
|
99
115
|
|
|
100
116
|
\`\`\`bash
|
|
101
|
-
#
|
|
102
|
-
nohup overnight
|
|
117
|
+
# Simplest: just hammer a goal overnight
|
|
118
|
+
nohup overnight hammer "Build a REST API with auth and tests" --notify > overnight.log 2>&1 &
|
|
103
119
|
|
|
104
|
-
#
|
|
105
|
-
overnight
|
|
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 &
|
|
106
123
|
|
|
107
|
-
#
|
|
108
|
-
overnight
|
|
124
|
+
# Batch tasks overnight
|
|
125
|
+
nohup overnight run tasks.yaml --notify -r report.md > overnight.log 2>&1 &
|
|
109
126
|
|
|
110
|
-
# Resume after crash
|
|
111
|
-
overnight resume
|
|
127
|
+
# Resume after crash
|
|
128
|
+
overnight resume goal.yaml
|
|
112
129
|
\`\`\`
|
|
113
130
|
|
|
114
131
|
## Exit Codes
|
|
115
132
|
|
|
116
|
-
- 0: All tasks succeeded
|
|
117
|
-
- 1:
|
|
133
|
+
- 0: All tasks succeeded / gate passed
|
|
134
|
+
- 1: Failures occurred / gate failed
|
|
118
135
|
|
|
119
136
|
## Files Created
|
|
120
137
|
|
|
121
|
-
- \`.overnight-state.json\` -
|
|
138
|
+
- \`.overnight-goal-state.json\` - Goal mode checkpoint
|
|
139
|
+
- \`.overnight-iterations/\` - Per-iteration state + summaries
|
|
140
|
+
- \`.overnight-state.json\` - Task mode checkpoint
|
|
122
141
|
- \`report.md\` - Summary report (if -r used)
|
|
123
|
-
- \`results.json\` - Full results (if -o used)
|
|
124
142
|
|
|
125
143
|
Run \`overnight <command> --help\` for command-specific options.
|
|
126
144
|
`;
|
|
127
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
|
+
|
|
128
158
|
interface ParsedConfig {
|
|
129
159
|
configs: JobConfig[];
|
|
130
160
|
security?: SecurityConfig;
|
|
@@ -225,15 +255,15 @@ const program = new Command();
|
|
|
225
255
|
program
|
|
226
256
|
.name("overnight")
|
|
227
257
|
.description("Batch job runner for Claude Code")
|
|
228
|
-
.version("0.
|
|
258
|
+
.version("0.3.0")
|
|
229
259
|
.action(() => {
|
|
230
260
|
console.log(AGENT_HELP);
|
|
231
261
|
});
|
|
232
262
|
|
|
233
263
|
program
|
|
234
264
|
.command("run")
|
|
235
|
-
.description("Run
|
|
236
|
-
.argument("<
|
|
265
|
+
.description("Run goal.yaml (autonomous loop) or tasks.yaml (batch jobs)")
|
|
266
|
+
.argument("<file>", "Path to goal.yaml or tasks.yaml")
|
|
237
267
|
.option("-o, --output <file>", "Output file for results JSON")
|
|
238
268
|
.option("-q, --quiet", "Minimal output")
|
|
239
269
|
.option("-s, --state-file <file>", "Custom state file path")
|
|
@@ -242,89 +272,175 @@ program
|
|
|
242
272
|
.option("-r, --report <file>", "Generate markdown report")
|
|
243
273
|
.option("--sandbox <dir>", "Sandbox directory (restrict file access)")
|
|
244
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))
|
|
245
276
|
.option("--audit-log <file>", "Audit log file path")
|
|
246
277
|
.option("--no-security", "Disable default security (deny patterns)")
|
|
247
|
-
.action(async (
|
|
248
|
-
if (!existsSync(
|
|
249
|
-
console.error(`Error: File not found: ${
|
|
278
|
+
.action(async (inputFile, opts) => {
|
|
279
|
+
if (!existsSync(inputFile)) {
|
|
280
|
+
console.error(`Error: File not found: ${inputFile}`);
|
|
250
281
|
process.exit(1);
|
|
251
282
|
}
|
|
252
283
|
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
...(opts.sandbox && { sandbox_dir: opts.sandbox }),
|
|
258
|
-
...(opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) }),
|
|
259
|
-
...(opts.auditLog && { audit_log: opts.auditLog }),
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const { configs, security } = parseTasksFile(tasksFile, cliSecurity);
|
|
263
|
-
if (configs.length === 0) {
|
|
264
|
-
console.error("No tasks found in file");
|
|
265
|
-
process.exit(1);
|
|
266
|
-
}
|
|
284
|
+
// Detect file type and dispatch
|
|
285
|
+
if (isGoalFile(inputFile)) {
|
|
286
|
+
// --- Goal mode ---
|
|
287
|
+
const goal = parseGoalFile(inputFile);
|
|
267
288
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
validateSecurityConfig(security);
|
|
283
|
-
}
|
|
284
|
-
console.log("");
|
|
285
|
-
|
|
286
|
-
const log = opts.quiet ? undefined : (msg: string) => console.log(msg);
|
|
287
|
-
const startTime = Date.now();
|
|
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
|
+
}
|
|
288
303
|
|
|
289
|
-
|
|
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
|
+
}
|
|
290
338
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
}
|
|
296
370
|
|
|
297
|
-
|
|
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
|
+
}
|
|
298
389
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (success) {
|
|
306
|
-
console.log(`\x1b[2mNotification sent to ntfy.sh/${opts.notifyTopic}\x1b[0m`);
|
|
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`);
|
|
307
396
|
} else {
|
|
308
|
-
console.log(
|
|
397
|
+
console.log(`\x1b[1movernight: Running ${configs.length} jobs...\x1b[0m`);
|
|
309
398
|
}
|
|
310
|
-
}
|
|
311
399
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
}
|
|
316
426
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
427
|
+
if (opts.report) {
|
|
428
|
+
generateReport(results, totalDuration, opts.report);
|
|
429
|
+
console.log(`\x1b[2mReport saved to ${opts.report}\x1b[0m`);
|
|
430
|
+
}
|
|
320
431
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
432
|
+
if (!opts.quiet) {
|
|
433
|
+
printSummary(results);
|
|
434
|
+
}
|
|
325
435
|
|
|
326
|
-
|
|
327
|
-
|
|
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
|
+
}
|
|
328
444
|
}
|
|
329
445
|
});
|
|
330
446
|
|
|
@@ -476,11 +592,128 @@ program
|
|
|
476
592
|
}
|
|
477
593
|
});
|
|
478
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
|
+
|
|
479
709
|
program
|
|
480
710
|
.command("init")
|
|
481
|
-
.description("Create an example tasks.yaml
|
|
482
|
-
.
|
|
483
|
-
|
|
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
|
|
484
717
|
# Run with: overnight run tasks.yaml
|
|
485
718
|
|
|
486
719
|
defaults:
|
|
@@ -500,9 +733,6 @@ defaults:
|
|
|
500
733
|
sandbox_dir: "." # Restrict to current directory
|
|
501
734
|
max_turns: 100 # Prevent runaway agents
|
|
502
735
|
# audit_log: "overnight-audit.log" # Uncomment to enable
|
|
503
|
-
# deny_patterns: # Default patterns block .env, .key, .pem, etc.
|
|
504
|
-
# - "**/.env*"
|
|
505
|
-
# - "**/*.key"
|
|
506
736
|
|
|
507
737
|
tasks:
|
|
508
738
|
# Simple string format
|
|
@@ -525,14 +755,67 @@ tasks:
|
|
|
525
755
|
- Grep
|
|
526
756
|
`;
|
|
527
757
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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"
|
|
532
773
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
+
}
|
|
536
819
|
});
|
|
537
820
|
|
|
538
821
|
program.parse();
|