@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/dist/cli.js CHANGED
@@ -8760,6 +8760,55 @@ var require_public_api = __commonJS((exports) => {
8760
8760
  exports.stringify = stringify;
8761
8761
  });
8762
8762
 
8763
+ // node_modules/yaml/dist/index.js
8764
+ var require_dist = __commonJS((exports) => {
8765
+ var composer = require_composer();
8766
+ var Document = require_Document();
8767
+ var Schema = require_Schema();
8768
+ var errors = require_errors();
8769
+ var Alias = require_Alias();
8770
+ var identity = require_identity();
8771
+ var Pair = require_Pair();
8772
+ var Scalar = require_Scalar();
8773
+ var YAMLMap = require_YAMLMap();
8774
+ var YAMLSeq = require_YAMLSeq();
8775
+ var cst = require_cst();
8776
+ var lexer = require_lexer();
8777
+ var lineCounter = require_line_counter();
8778
+ var parser = require_parser();
8779
+ var publicApi = require_public_api();
8780
+ var visit = require_visit();
8781
+ exports.Composer = composer.Composer;
8782
+ exports.Document = Document.Document;
8783
+ exports.Schema = Schema.Schema;
8784
+ exports.YAMLError = errors.YAMLError;
8785
+ exports.YAMLParseError = errors.YAMLParseError;
8786
+ exports.YAMLWarning = errors.YAMLWarning;
8787
+ exports.Alias = Alias.Alias;
8788
+ exports.isAlias = identity.isAlias;
8789
+ exports.isCollection = identity.isCollection;
8790
+ exports.isDocument = identity.isDocument;
8791
+ exports.isMap = identity.isMap;
8792
+ exports.isNode = identity.isNode;
8793
+ exports.isPair = identity.isPair;
8794
+ exports.isScalar = identity.isScalar;
8795
+ exports.isSeq = identity.isSeq;
8796
+ exports.Pair = Pair.Pair;
8797
+ exports.Scalar = Scalar.Scalar;
8798
+ exports.YAMLMap = YAMLMap.YAMLMap;
8799
+ exports.YAMLSeq = YAMLSeq.YAMLSeq;
8800
+ exports.CST = cst;
8801
+ exports.Lexer = lexer.Lexer;
8802
+ exports.LineCounter = lineCounter.LineCounter;
8803
+ exports.Parser = parser.Parser;
8804
+ exports.parse = publicApi.parse;
8805
+ exports.parseAllDocuments = publicApi.parseAllDocuments;
8806
+ exports.parseDocument = publicApi.parseDocument;
8807
+ exports.stringify = publicApi.stringify;
8808
+ exports.visit = visit.visit;
8809
+ exports.visitAsync = visit.visitAsync;
8810
+ });
8811
+
8763
8812
  // node_modules/commander/esm.mjs
8764
8813
  var import__ = __toESM(require_commander(), 1);
8765
8814
  var {
@@ -8777,53 +8826,8 @@ var {
8777
8826
  } = import__.default;
8778
8827
 
8779
8828
  // src/cli.ts
8780
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
8781
-
8782
- // node_modules/yaml/dist/index.js
8783
- var composer = require_composer();
8784
- var Document = require_Document();
8785
- var Schema = require_Schema();
8786
- var errors = require_errors();
8787
- var Alias = require_Alias();
8788
- var identity = require_identity();
8789
- var Pair = require_Pair();
8790
- var Scalar = require_Scalar();
8791
- var YAMLMap = require_YAMLMap();
8792
- var YAMLSeq = require_YAMLSeq();
8793
- var cst = require_cst();
8794
- var lexer = require_lexer();
8795
- var lineCounter = require_line_counter();
8796
- var parser = require_parser();
8797
- var publicApi = require_public_api();
8798
- var visit = require_visit();
8799
- var $Composer = composer.Composer;
8800
- var $Document = Document.Document;
8801
- var $Schema = Schema.Schema;
8802
- var $YAMLError = errors.YAMLError;
8803
- var $YAMLParseError = errors.YAMLParseError;
8804
- var $YAMLWarning = errors.YAMLWarning;
8805
- var $Alias = Alias.Alias;
8806
- var $isAlias = identity.isAlias;
8807
- var $isCollection = identity.isCollection;
8808
- var $isDocument = identity.isDocument;
8809
- var $isMap = identity.isMap;
8810
- var $isNode = identity.isNode;
8811
- var $isPair = identity.isPair;
8812
- var $isScalar = identity.isScalar;
8813
- var $isSeq = identity.isSeq;
8814
- var $Pair = Pair.Pair;
8815
- var $Scalar = Scalar.Scalar;
8816
- var $YAMLMap = YAMLMap.YAMLMap;
8817
- var $YAMLSeq = YAMLSeq.YAMLSeq;
8818
- var $Lexer = lexer.Lexer;
8819
- var $LineCounter = lineCounter.LineCounter;
8820
- var $Parser = parser.Parser;
8821
- var $parse = publicApi.parse;
8822
- var $parseAllDocuments = publicApi.parseAllDocuments;
8823
- var $parseDocument = publicApi.parseDocument;
8824
- var $stringify = publicApi.stringify;
8825
- var $visit = visit.visit;
8826
- var $visitAsync = visit.visitAsync;
8829
+ var import_yaml2 = __toESM(require_dist(), 1);
8830
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
8827
8831
 
8828
8832
  // src/types.ts
8829
8833
  var DEFAULT_TOOLS = ["Read", "Edit", "Write", "Glob", "Grep"];
@@ -8833,8 +8837,11 @@ var DEFAULT_RETRY_COUNT = 3;
8833
8837
  var DEFAULT_RETRY_DELAY = 5;
8834
8838
  var DEFAULT_VERIFY_PROMPT = "Review what you just implemented. Check for correctness, completeness, and compile errors. Fix any issues you find.";
8835
8839
  var DEFAULT_STATE_FILE = ".overnight-state.json";
8840
+ var DEFAULT_GOAL_STATE_FILE = ".overnight-goal-state.json";
8836
8841
  var DEFAULT_NTFY_TOPIC = "overnight";
8837
8842
  var DEFAULT_MAX_TURNS = 100;
8843
+ var DEFAULT_MAX_ITERATIONS = 20;
8844
+ var DEFAULT_CONVERGENCE_THRESHOLD = 3;
8838
8845
  var DEFAULT_DENY_PATTERNS = [
8839
8846
  "**/.env",
8840
8847
  "**/.env.*",
@@ -8905,7 +8912,7 @@ function createSecurityHooks(config) {
8905
8912
  if (sandboxDir && !isPathWithinSandbox(filePath, sandboxDir)) {
8906
8913
  return {
8907
8914
  hookSpecificOutput: {
8908
- hookEventName,
8915
+ hookEventName: "PreToolUse",
8909
8916
  permissionDecision: "deny",
8910
8917
  permissionDecisionReason: `Path "${filePath}" is outside sandbox directory "${sandboxDir}"`
8911
8918
  }
@@ -8915,7 +8922,7 @@ function createSecurityHooks(config) {
8915
8922
  if (matchedPattern) {
8916
8923
  return {
8917
8924
  hookSpecificOutput: {
8918
- hookEventName,
8925
+ hookEventName: "PreToolUse",
8919
8926
  permissionDecision: "deny",
8920
8927
  permissionDecisionReason: `Path "${filePath}" matches deny pattern "${matchedPattern}"`
8921
8928
  }
@@ -15277,7 +15284,7 @@ var require_limit = __commonJS2((exports) => {
15277
15284
  };
15278
15285
  exports.default = formatLimitPlugin;
15279
15286
  });
15280
- var require_dist = __commonJS2((exports, module) => {
15287
+ var require_dist2 = __commonJS2((exports, module) => {
15281
15288
  Object.defineProperty(exports, "__esModule", { value: true });
15282
15289
  var formats_1 = require_formats();
15283
15290
  var limit_1 = require_limit();
@@ -25429,7 +25436,7 @@ var ServerResultSchema = union([
25429
25436
  var ignoreOverride = Symbol("Let zodToJsonSchema decide on which parser to use");
25430
25437
  var ALPHA_NUMERIC = new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789");
25431
25438
  var import_ajv = __toESM2(require_ajv(), 1);
25432
- var import_ajv_formats = __toESM2(require_dist(), 1);
25439
+ var import_ajv_formats = __toESM2(require_dist2(), 1);
25433
25440
  var COMPLETABLE_SYMBOL = Symbol.for("mcp.completable");
25434
25441
  var McpZodTypeKind;
25435
25442
  (function(McpZodTypeKind2) {
@@ -25711,7 +25718,7 @@ function getToolDetail(toolName, toolInput) {
25711
25718
  return "";
25712
25719
  }
25713
25720
  async function collectResultWithProgress(prompt, options, progress, onSessionId) {
25714
- let sessionId2;
25721
+ let sessionId;
25715
25722
  let result;
25716
25723
  let lastError;
25717
25724
  try {
@@ -25722,8 +25729,10 @@ async function collectResultWithProgress(prompt, options, progress, onSessionId)
25722
25729
  [DEBUG] message.type=${message.type}, keys=${Object.keys(message).join(",")}`);
25723
25730
  }
25724
25731
  if (message.type === "result") {
25725
- result = message.result;
25726
- sessionId2 = message.session_id;
25732
+ sessionId = message.session_id;
25733
+ if (message.subtype === "success") {
25734
+ result = message.result;
25735
+ }
25727
25736
  } else if (message.type === "assistant" && "message" in message) {
25728
25737
  const assistantMsg = message.message;
25729
25738
  if (assistantMsg.content) {
@@ -25739,9 +25748,9 @@ async function collectResultWithProgress(prompt, options, progress, onSessionId)
25739
25748
  }
25740
25749
  } else if (message.type === "system" && "subtype" in message) {
25741
25750
  if (message.subtype === "init") {
25742
- sessionId2 = message.session_id;
25743
- if (sessionId2 && onSessionId) {
25744
- onSessionId(sessionId2);
25751
+ sessionId = message.session_id;
25752
+ if (sessionId && onSessionId) {
25753
+ onSessionId(sessionId);
25745
25754
  }
25746
25755
  }
25747
25756
  }
@@ -25750,7 +25759,7 @@ async function collectResultWithProgress(prompt, options, progress, onSessionId)
25750
25759
  lastError = e.message;
25751
25760
  throw e;
25752
25761
  }
25753
- return { sessionId: sessionId2, result, error: lastError };
25762
+ return { sessionId, result, error: lastError };
25754
25763
  }
25755
25764
  async function runJob(config2, log, options) {
25756
25765
  const startTime = Date.now();
@@ -25787,6 +25796,7 @@ async function runJob(config2, log, options) {
25787
25796
  } else {
25788
25797
  logMsg(`\x1B[36m▶\x1B[0m ${taskPreview}`);
25789
25798
  }
25799
+ let sessionId;
25790
25800
  for (let attempt = 0;attempt <= retryCount; attempt++) {
25791
25801
  try {
25792
25802
  const securityHooks = config2.security ? createSecurityHooks(config2.security) : undefined;
@@ -25799,16 +25809,15 @@ async function runJob(config2, log, options) {
25799
25809
  ...securityHooks && { hooks: securityHooks },
25800
25810
  ...resumeSessionId && { resume: resumeSessionId }
25801
25811
  };
25802
- let sessionId2;
25803
25812
  let result;
25804
25813
  const prompt = resumeSessionId ? "Continue where you left off. Complete the original task." : config2.prompt;
25805
25814
  progress.start(resumeSessionId ? "Resuming" : "Working");
25806
25815
  try {
25807
25816
  const collected = await runWithTimeout(collectResultWithProgress(prompt, sdkOptions, progress, (id) => {
25808
- sessionId2 = id;
25817
+ sessionId = id;
25809
25818
  options?.onSessionId?.(id);
25810
25819
  }), timeout);
25811
- sessionId2 = collected.sessionId;
25820
+ sessionId = collected.sessionId;
25812
25821
  result = collected.result;
25813
25822
  progress.stop();
25814
25823
  } catch (e) {
@@ -25816,8 +25825,8 @@ async function runJob(config2, log, options) {
25816
25825
  if (e.message === "TIMEOUT") {
25817
25826
  if (attempt < retryCount) {
25818
25827
  retriesUsed = attempt + 1;
25819
- if (sessionId2) {
25820
- resumeSessionId = sessionId2;
25828
+ if (sessionId) {
25829
+ resumeSessionId = sessionId;
25821
25830
  }
25822
25831
  const delay = retryDelay * Math.pow(2, attempt);
25823
25832
  logMsg(`\x1B[33m⚠ Timeout after ${config2.timeout_seconds ?? DEFAULT_TIMEOUT}s, retrying in ${delay}s (${attempt + 1}/${retryCount})\x1B[0m`);
@@ -25836,11 +25845,11 @@ async function runJob(config2, log, options) {
25836
25845
  }
25837
25846
  throw e;
25838
25847
  }
25839
- if (config2.verify !== false && sessionId2) {
25848
+ if (config2.verify !== false && sessionId) {
25840
25849
  progress.start("Verifying");
25841
25850
  const verifyOptions = {
25842
25851
  allowedTools: tools,
25843
- resume: sessionId2,
25852
+ resume: sessionId,
25844
25853
  permissionMode: "acceptEdits",
25845
25854
  ...claudePath && { pathToClaudeCodeExecutable: claudePath },
25846
25855
  ...config2.working_dir && { cwd: config2.working_dir },
@@ -25849,7 +25858,7 @@ async function runJob(config2, log, options) {
25849
25858
  const fixPrompt = verifyPrompt + " If you find any issues, fix them now. Only report issues you cannot fix.";
25850
25859
  try {
25851
25860
  const verifyResult = await runWithTimeout(collectResultWithProgress(fixPrompt, verifyOptions, progress, (id) => {
25852
- sessionId2 = id;
25861
+ sessionId = id;
25853
25862
  options?.onSessionId?.(id);
25854
25863
  }), timeout / 2);
25855
25864
  progress.stop();
@@ -25928,7 +25937,7 @@ function taskKey(config2) {
25928
25937
  return createHash("sha256").update(config2.prompt).digest("hex").slice(0, 12);
25929
25938
  }
25930
25939
  function validateDag(configs) {
25931
- const ids = new Set(configs.map((c) => c.id).filter(Boolean));
25940
+ const ids = new Set(configs.map((c) => c.id).filter((id) => Boolean(id)));
25932
25941
  for (const c of configs) {
25933
25942
  for (const dep of c.depends_on ?? []) {
25934
25943
  if (!ids.has(dep)) {
@@ -26198,55 +26207,860 @@ function generateReport(results, totalDuration, outputPath) {
26198
26207
  return content;
26199
26208
  }
26200
26209
 
26210
+ // src/goal-runner.ts
26211
+ var import_yaml = __toESM(require_dist(), 1);
26212
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
26213
+ import { execSync as execSync2 } from "child_process";
26214
+ var ITERATION_DIR = ".overnight-iterations";
26215
+ function ensureIterationDir() {
26216
+ if (!existsSync5(ITERATION_DIR)) {
26217
+ mkdirSync3(ITERATION_DIR, { recursive: true });
26218
+ }
26219
+ }
26220
+ function saveGoalState(state, stateFile) {
26221
+ writeFileSync3(stateFile, JSON.stringify(state, null, 2));
26222
+ }
26223
+ function loadGoalState(stateFile) {
26224
+ if (!existsSync5(stateFile))
26225
+ return null;
26226
+ return JSON.parse(readFileSync3(stateFile, "utf-8"));
26227
+ }
26228
+ function saveIterationState(iteration, state) {
26229
+ ensureIterationDir();
26230
+ writeFileSync3(`${ITERATION_DIR}/iteration-${iteration}-state.yaml`, import_yaml.stringify(state));
26231
+ }
26232
+ function saveIterationNarrative(iteration, narrative) {
26233
+ ensureIterationDir();
26234
+ writeFileSync3(`${ITERATION_DIR}/iteration-${iteration}-summary.md`, narrative);
26235
+ }
26236
+ function loadPreviousIterationState(iteration) {
26237
+ const path = `${ITERATION_DIR}/iteration-${iteration}-state.yaml`;
26238
+ if (!existsSync5(path))
26239
+ return null;
26240
+ return import_yaml.parse(readFileSync3(path, "utf-8"));
26241
+ }
26242
+ function loadPreviousNarrative(iteration) {
26243
+ const path = `${ITERATION_DIR}/iteration-${iteration}-summary.md`;
26244
+ if (!existsSync5(path))
26245
+ return null;
26246
+ return readFileSync3(path, "utf-8");
26247
+ }
26248
+ function isConverging(states, threshold) {
26249
+ if (states.length < threshold)
26250
+ return true;
26251
+ const recent = states.slice(-threshold);
26252
+ const remainingCounts = recent.map((s) => s.remaining_items.length);
26253
+ for (let i = 1;i < remainingCounts.length; i++) {
26254
+ if (remainingCounts[i] < remainingCounts[i - 1]) {
26255
+ return true;
26256
+ }
26257
+ }
26258
+ return false;
26259
+ }
26260
+ var SPINNER_FRAMES2 = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
26261
+
26262
+ class ProgressDisplay2 {
26263
+ interval = null;
26264
+ frame = 0;
26265
+ startTime = Date.now();
26266
+ currentActivity = "Working";
26267
+ start(activity) {
26268
+ this.currentActivity = activity;
26269
+ this.startTime = Date.now();
26270
+ this.frame = 0;
26271
+ if (this.interval)
26272
+ return;
26273
+ this.interval = setInterval(() => {
26274
+ const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
26275
+ process.stdout.write(`\r\x1B[K${SPINNER_FRAMES2[this.frame]} ${this.currentActivity} (${elapsed}s)`);
26276
+ this.frame = (this.frame + 1) % SPINNER_FRAMES2.length;
26277
+ }, 100);
26278
+ }
26279
+ stop(finalMessage) {
26280
+ if (this.interval) {
26281
+ clearInterval(this.interval);
26282
+ this.interval = null;
26283
+ }
26284
+ process.stdout.write("\r\x1B[K");
26285
+ if (finalMessage)
26286
+ console.log(finalMessage);
26287
+ }
26288
+ }
26289
+ var claudeExecutablePath2;
26290
+ function findClaudeExecutable2() {
26291
+ if (claudeExecutablePath2 !== undefined)
26292
+ return claudeExecutablePath2;
26293
+ if (process.env.CLAUDE_CODE_PATH) {
26294
+ claudeExecutablePath2 = process.env.CLAUDE_CODE_PATH;
26295
+ return claudeExecutablePath2;
26296
+ }
26297
+ try {
26298
+ const cmd = process.platform === "win32" ? "where claude" : "which claude";
26299
+ claudeExecutablePath2 = execSync2(cmd, { encoding: "utf-8" }).trim().split(`
26300
+ `)[0];
26301
+ return claudeExecutablePath2;
26302
+ } catch {
26303
+ const commonPaths = [
26304
+ "/usr/local/bin/claude",
26305
+ "/opt/homebrew/bin/claude",
26306
+ `${process.env.HOME}/.local/bin/claude`
26307
+ ];
26308
+ for (const p of commonPaths) {
26309
+ if (existsSync5(p)) {
26310
+ claudeExecutablePath2 = p;
26311
+ return claudeExecutablePath2;
26312
+ }
26313
+ }
26314
+ }
26315
+ return;
26316
+ }
26317
+ async function runClaudePrompt(prompt, config2, log, progress, resumeSessionId) {
26318
+ const claudePath = findClaudeExecutable2();
26319
+ if (!claudePath) {
26320
+ throw new Error("Claude CLI not found. Install with: curl -fsSL https://claude.ai/install.sh | bash");
26321
+ }
26322
+ const tools = config2.defaults?.allowed_tools ?? DEFAULT_TOOLS;
26323
+ const timeout = (config2.defaults?.timeout_seconds ?? DEFAULT_TIMEOUT) * 1000;
26324
+ const security = config2.defaults?.security;
26325
+ const securityHooks = security ? createSecurityHooks(security) : undefined;
26326
+ const sdkOptions = {
26327
+ allowedTools: tools,
26328
+ permissionMode: "acceptEdits",
26329
+ pathToClaudeCodeExecutable: claudePath,
26330
+ ...security?.max_turns && { maxTurns: security.max_turns },
26331
+ ...securityHooks && { hooks: securityHooks },
26332
+ ...resumeSessionId && { resume: resumeSessionId }
26333
+ };
26334
+ let sessionId;
26335
+ let result;
26336
+ const conversation = query({ prompt, options: sdkOptions });
26337
+ for await (const message of conversation) {
26338
+ if (message.type === "result") {
26339
+ sessionId = message.session_id;
26340
+ if (message.subtype === "success") {
26341
+ result = message.result;
26342
+ }
26343
+ } else if (message.type === "system" && "subtype" in message) {
26344
+ if (message.subtype === "init") {
26345
+ sessionId = message.session_id;
26346
+ }
26347
+ }
26348
+ }
26349
+ return { result, sessionId };
26350
+ }
26351
+ function buildIterationPrompt(goal, iteration, previousState, previousNarrative) {
26352
+ const parts = [];
26353
+ parts.push(`# Goal
26354
+
26355
+ ${goal.goal}`);
26356
+ if (goal.acceptance_criteria && goal.acceptance_criteria.length > 0) {
26357
+ parts.push(`
26358
+ # Acceptance Criteria
26359
+
26360
+ ${goal.acceptance_criteria.map((c) => `- ${c}`).join(`
26361
+ `)}`);
26362
+ }
26363
+ if (goal.constraints && goal.constraints.length > 0) {
26364
+ parts.push(`
26365
+ # Constraints
26366
+
26367
+ ${goal.constraints.map((c) => `- ${c}`).join(`
26368
+ `)}`);
26369
+ }
26370
+ if (goal.verification_commands && goal.verification_commands.length > 0) {
26371
+ parts.push(`
26372
+ # Verification Commands (must pass)
26373
+
26374
+ ${goal.verification_commands.map((c) => `- \`${c}\``).join(`
26375
+ `)}`);
26376
+ }
26377
+ parts.push(`
26378
+ # Iteration ${iteration}`);
26379
+ if (previousState && previousNarrative) {
26380
+ parts.push(`
26381
+ ## Previous Iteration State
26382
+
26383
+ ### Completed Items
26384
+ ${previousState.completed_items.map((i) => `- ${i}`).join(`
26385
+ `) || "- (none yet)"}`);
26386
+ parts.push(`
26387
+ ### Remaining Items
26388
+ ${previousState.remaining_items.map((i) => `- ${i}`).join(`
26389
+ `) || "- (none)"}`);
26390
+ parts.push(`
26391
+ ### Known Issues
26392
+ ${previousState.known_issues.map((i) => `- ${i}`).join(`
26393
+ `) || "- (none)"}`);
26394
+ parts.push(`
26395
+ ### Files Modified
26396
+ ${previousState.files_modified.map((f) => `- ${f}`).join(`
26397
+ `) || "- (none)"}`);
26398
+ parts.push(`
26399
+ ### Previous Summary
26400
+
26401
+ ${previousNarrative}`);
26402
+ }
26403
+ parts.push(`
26404
+ # Instructions
26405
+
26406
+ You are iteration ${iteration} of an autonomous build loop working toward the goal above.
26407
+
26408
+ 1. Assess the current state of the project
26409
+ 2. Identify the highest-priority remaining work
26410
+ 3. Implement as much as you can in this iteration
26411
+ 4. When done, output your structured state update in the following EXACT format:
26412
+
26413
+ \`\`\`yaml
26414
+ completed_items:
26415
+ - "item 1 you completed"
26416
+ - "item 2 you completed"
26417
+ remaining_items:
26418
+ - "item still to do"
26419
+ - "another item still to do"
26420
+ known_issues:
26421
+ - "any issues found"
26422
+ files_modified:
26423
+ - "path/to/file1.ts"
26424
+ - "path/to/file2.ts"
26425
+ agent_done: false # Set to true ONLY if you believe the goal is fully met
26426
+ \`\`\`
26427
+
26428
+ 5. After the YAML block, write a brief narrative summary (2-3 paragraphs) of what you did, what challenges you encountered, and what the next iteration should focus on.
26429
+
26430
+ IMPORTANT: Always output the YAML block wrapped in \`\`\`yaml ... \`\`\` fences. This is how state is tracked between iterations.`);
26431
+ return parts.join(`
26432
+ `);
26433
+ }
26434
+ function parseIterationOutput(output, iteration) {
26435
+ const yamlMatch = output.match(/```yaml\n([\s\S]*?)\n```/);
26436
+ let state;
26437
+ if (yamlMatch) {
26438
+ try {
26439
+ const parsed = import_yaml.parse(yamlMatch[1]);
26440
+ state = {
26441
+ iteration,
26442
+ completed_items: parsed.completed_items ?? [],
26443
+ remaining_items: parsed.remaining_items ?? [],
26444
+ known_issues: parsed.known_issues ?? [],
26445
+ files_modified: parsed.files_modified ?? [],
26446
+ agent_done: parsed.agent_done ?? false,
26447
+ timestamp: new Date().toISOString()
26448
+ };
26449
+ } catch {
26450
+ state = {
26451
+ iteration,
26452
+ completed_items: [],
26453
+ remaining_items: ["(failed to parse agent output)"],
26454
+ known_issues: ["Agent output did not contain valid YAML state block"],
26455
+ files_modified: [],
26456
+ agent_done: false,
26457
+ timestamp: new Date().toISOString()
26458
+ };
26459
+ }
26460
+ } else {
26461
+ state = {
26462
+ iteration,
26463
+ completed_items: [],
26464
+ remaining_items: ["(no structured output from agent)"],
26465
+ known_issues: ["Agent did not output a YAML state block"],
26466
+ files_modified: [],
26467
+ agent_done: false,
26468
+ timestamp: new Date().toISOString()
26469
+ };
26470
+ }
26471
+ let narrative;
26472
+ if (yamlMatch) {
26473
+ const afterYaml = output.slice(output.indexOf("```", output.indexOf("```yaml") + 7) + 3).trim();
26474
+ narrative = afterYaml || "(no narrative provided)";
26475
+ } else {
26476
+ narrative = output;
26477
+ }
26478
+ return { state, narrative };
26479
+ }
26480
+ function buildGatePrompt(goal, iterationStates) {
26481
+ const lastState = iterationStates[iterationStates.length - 1];
26482
+ const parts = [];
26483
+ parts.push(`# Final Verification Gate
26484
+
26485
+ You are a dedicated verification agent. You did NOT write this code. Your only job is to determine if the goal has been met to production quality. Be rigorous and honest.
26486
+
26487
+ ## Goal
26488
+
26489
+ ${goal.goal}`);
26490
+ if (goal.acceptance_criteria && goal.acceptance_criteria.length > 0) {
26491
+ parts.push(`
26492
+ ## Acceptance Criteria (ALL must be met)
26493
+
26494
+ ${goal.acceptance_criteria.map((c, i) => `${i + 1}. ${c}`).join(`
26495
+ `)}`);
26496
+ }
26497
+ if (goal.verification_commands && goal.verification_commands.length > 0) {
26498
+ parts.push(`
26499
+ ## Required Verification Commands
26500
+
26501
+ Run ALL of these. Each must pass:
26502
+ ${goal.verification_commands.map((c) => `- \`${c}\``).join(`
26503
+ `)}`);
26504
+ }
26505
+ parts.push(`
26506
+ ## Build Agent's Final State
26507
+
26508
+ ### Completed Items
26509
+ ${lastState?.completed_items.map((i) => `- ${i}`).join(`
26510
+ `) || "- (none)"}
26511
+
26512
+ ### Claimed Remaining Items
26513
+ ${lastState?.remaining_items.map((i) => `- ${i}`).join(`
26514
+ `) || "- (none)"}
26515
+
26516
+ ### Known Issues
26517
+ ${lastState?.known_issues.map((i) => `- ${i}`).join(`
26518
+ `) || "- (none)"}
26519
+
26520
+ ## Instructions
26521
+
26522
+ Perform EVERY form of verification you can:
26523
+
26524
+ 1. **Build check**: Does the project compile/build without errors?
26525
+ 2. **Lint/type check**: Are there type errors or lint warnings?
26526
+ 3. **Unit tests**: Do all unit tests pass?
26527
+ 4. **E2E tests**: Do end-to-end tests pass?
26528
+ 5. **Visual review**: Check rendered output if applicable
26529
+ 6. **Manual walkthrough**: Trace key user flows through the code
26530
+ 7. **Acceptance criteria**: Verify each criterion explicitly
26531
+ 8. **Verification commands**: Run each command listed above
26532
+ 9. **Code quality**: Look for obvious bugs, missing error handling, broken imports
26533
+ 10. **Integration**: Is everything wired up? No dead code, no missing connections?
26534
+
26535
+ After your review, output your verdict in this EXACT format:
26536
+
26537
+ \`\`\`yaml
26538
+ passed: false # or true
26539
+ checks:
26540
+ - name: "Build"
26541
+ passed: true
26542
+ output: "npm run build succeeded"
26543
+ - name: "Unit tests"
26544
+ passed: false
26545
+ output: "3 tests failed: ..."
26546
+ summary: "Brief overall assessment"
26547
+ failures:
26548
+ - "Description of failure 1"
26549
+ - "Description of failure 2"
26550
+ \`\`\`
26551
+
26552
+ Be thorough. Do not let bad quality pass. If ANYTHING is broken, set passed: false.`);
26553
+ return parts.join(`
26554
+ `);
26555
+ }
26556
+ function parseGateOutput(output) {
26557
+ const yamlMatch = output.match(/```yaml\n([\s\S]*?)\n```/);
26558
+ if (yamlMatch) {
26559
+ try {
26560
+ const parsed = import_yaml.parse(yamlMatch[1]);
26561
+ return {
26562
+ passed: parsed.passed ?? false,
26563
+ checks: (parsed.checks ?? []).map((c) => ({
26564
+ name: c.name ?? "unknown",
26565
+ passed: c.passed ?? false,
26566
+ output: c.output ?? ""
26567
+ })),
26568
+ summary: parsed.summary ?? "",
26569
+ failures: parsed.failures ?? []
26570
+ };
26571
+ } catch {
26572
+ return {
26573
+ passed: false,
26574
+ checks: [],
26575
+ summary: "Failed to parse gate agent output",
26576
+ failures: ["Gate agent output was not valid YAML"]
26577
+ };
26578
+ }
26579
+ }
26580
+ return {
26581
+ passed: false,
26582
+ checks: [],
26583
+ summary: "Gate agent did not output a structured verdict",
26584
+ failures: ["No YAML verdict block found in gate agent output"]
26585
+ };
26586
+ }
26587
+ async function runGoal(goal, options = {}) {
26588
+ const stateFile = options.stateFile ?? DEFAULT_GOAL_STATE_FILE;
26589
+ const log = options.log ?? (() => {});
26590
+ const maxIterations = goal.max_iterations ?? DEFAULT_MAX_ITERATIONS;
26591
+ const convergenceThreshold = goal.convergence_threshold ?? DEFAULT_CONVERGENCE_THRESHOLD;
26592
+ const progress = new ProgressDisplay2;
26593
+ let runState = loadGoalState(stateFile) ?? {
26594
+ goal: goal.goal,
26595
+ iterations: [],
26596
+ gate_results: [],
26597
+ status: "running",
26598
+ timestamp: new Date().toISOString()
26599
+ };
26600
+ const startIteration = runState.iterations.length + 1;
26601
+ if (startIteration > 1) {
26602
+ log(`\x1B[1movernight: Resuming from iteration ${startIteration}\x1B[0m`);
26603
+ } else {
26604
+ log(`\x1B[1movernight: Starting goal loop\x1B[0m`);
26605
+ log(`\x1B[2mGoal: ${goal.goal.slice(0, 80)}${goal.goal.length > 80 ? "..." : ""}\x1B[0m`);
26606
+ log(`\x1B[2mMax iterations: ${maxIterations}, convergence threshold: ${convergenceThreshold}\x1B[0m`);
26607
+ }
26608
+ log("");
26609
+ for (let iteration = startIteration;iteration <= maxIterations; iteration++) {
26610
+ log(`\x1B[1m━━━ Iteration ${iteration}/${maxIterations} ━━━\x1B[0m`);
26611
+ const prevState = iteration > 1 ? loadPreviousIterationState(iteration - 1) : null;
26612
+ const prevNarrative = iteration > 1 ? loadPreviousNarrative(iteration - 1) : null;
26613
+ if (!isConverging(runState.iterations, convergenceThreshold)) {
26614
+ log(`\x1B[33m⚠ Build loop stalled — remaining items unchanged for ${convergenceThreshold} iterations\x1B[0m`);
26615
+ runState.status = "stalled";
26616
+ saveGoalState(runState, stateFile);
26617
+ break;
26618
+ }
26619
+ const prompt = buildIterationPrompt(goal, iteration, prevState, prevNarrative);
26620
+ progress.start(`Iteration ${iteration}`);
26621
+ try {
26622
+ const { result } = await runClaudePrompt(prompt, goal, log, progress);
26623
+ progress.stop();
26624
+ if (!result) {
26625
+ log(`\x1B[31m✗ No output from build agent\x1B[0m`);
26626
+ continue;
26627
+ }
26628
+ const { state: iterState, narrative } = parseIterationOutput(result, iteration);
26629
+ saveIterationState(iteration, iterState);
26630
+ saveIterationNarrative(iteration, narrative);
26631
+ runState.iterations.push(iterState);
26632
+ runState.timestamp = new Date().toISOString();
26633
+ saveGoalState(runState, stateFile);
26634
+ log(`\x1B[32m✓ Iteration ${iteration} complete\x1B[0m`);
26635
+ log(` Completed: ${iterState.completed_items.length} items`);
26636
+ log(` Remaining: ${iterState.remaining_items.length} items`);
26637
+ if (iterState.known_issues.length > 0) {
26638
+ log(` Issues: ${iterState.known_issues.length}`);
26639
+ }
26640
+ if (iterState.agent_done) {
26641
+ log(`
26642
+ \x1B[36m◆ Build agent reports goal is met — running final gate...\x1B[0m
26643
+ `);
26644
+ break;
26645
+ }
26646
+ } catch (e) {
26647
+ progress.stop();
26648
+ const error2 = e;
26649
+ log(`\x1B[31m✗ Iteration ${iteration} failed: ${error2.message}\x1B[0m`);
26650
+ if (error2.message === "TIMEOUT") {
26651
+ log(`\x1B[33m Continuing to next iteration...\x1B[0m`);
26652
+ continue;
26653
+ }
26654
+ continue;
26655
+ }
26656
+ log("");
26657
+ }
26658
+ if (runState.status === "running") {
26659
+ const maxGateAttempts = 3;
26660
+ for (let gateAttempt = 1;gateAttempt <= maxGateAttempts; gateAttempt++) {
26661
+ log(`\x1B[1m━━━ Final Gate (attempt ${gateAttempt}/${maxGateAttempts}) ━━━\x1B[0m`);
26662
+ const gatePrompt = buildGatePrompt(goal, runState.iterations);
26663
+ const gateGoalConfig = {
26664
+ ...goal,
26665
+ defaults: {
26666
+ ...goal.defaults,
26667
+ allowed_tools: [...goal.defaults?.allowed_tools ?? DEFAULT_TOOLS, "Bash"]
26668
+ }
26669
+ };
26670
+ progress.start("Running final gate");
26671
+ try {
26672
+ const { result } = await runClaudePrompt(gatePrompt, gateGoalConfig, log, progress);
26673
+ progress.stop();
26674
+ if (!result) {
26675
+ log(`\x1B[31m✗ No output from gate agent\x1B[0m`);
26676
+ continue;
26677
+ }
26678
+ const gateResult = parseGateOutput(result);
26679
+ runState.gate_results.push(gateResult);
26680
+ saveGoalState(runState, stateFile);
26681
+ if (gateResult.passed) {
26682
+ log(`\x1B[32m✓ GATE PASSED\x1B[0m`);
26683
+ log(` ${gateResult.summary}`);
26684
+ for (const check2 of gateResult.checks) {
26685
+ const icon = check2.passed ? "\x1B[32m✓\x1B[0m" : "\x1B[31m✗\x1B[0m";
26686
+ log(` ${icon} ${check2.name}`);
26687
+ }
26688
+ runState.status = "gate_passed";
26689
+ saveGoalState(runState, stateFile);
26690
+ break;
26691
+ } else {
26692
+ log(`\x1B[31m✗ GATE FAILED\x1B[0m`);
26693
+ log(` ${gateResult.summary}`);
26694
+ for (const failure of gateResult.failures) {
26695
+ log(` \x1B[31m- ${failure}\x1B[0m`);
26696
+ }
26697
+ if (gateAttempt < maxGateAttempts) {
26698
+ log(`
26699
+ \x1B[36m◆ Looping back to build agent with gate failures...\x1B[0m
26700
+ `);
26701
+ const fixIteration = runState.iterations.length + 1;
26702
+ const fixPrompt = buildGateFixPrompt(goal, gateResult, fixIteration);
26703
+ progress.start(`Fix iteration ${fixIteration}`);
26704
+ try {
26705
+ const { result: fixResult } = await runClaudePrompt(fixPrompt, goal, log, progress);
26706
+ progress.stop();
26707
+ if (fixResult) {
26708
+ const { state: fixState, narrative: fixNarrative } = parseIterationOutput(fixResult, fixIteration);
26709
+ saveIterationState(fixIteration, fixState);
26710
+ saveIterationNarrative(fixIteration, fixNarrative);
26711
+ runState.iterations.push(fixState);
26712
+ saveGoalState(runState, stateFile);
26713
+ log(`\x1B[32m✓ Fix iteration complete\x1B[0m`);
26714
+ log(` Fixed: ${fixState.completed_items.length} items`);
26715
+ }
26716
+ } catch (e) {
26717
+ progress.stop();
26718
+ log(`\x1B[31m✗ Fix iteration failed: ${e.message}\x1B[0m`);
26719
+ }
26720
+ } else {
26721
+ runState.status = "gate_failed";
26722
+ saveGoalState(runState, stateFile);
26723
+ }
26724
+ }
26725
+ } catch (e) {
26726
+ progress.stop();
26727
+ log(`\x1B[31m✗ Gate failed: ${e.message}\x1B[0m`);
26728
+ }
26729
+ log("");
26730
+ }
26731
+ }
26732
+ if (runState.status === "running") {
26733
+ const lastState = runState.iterations[runState.iterations.length - 1];
26734
+ if (!lastState?.agent_done) {
26735
+ log(`\x1B[33m⚠ Reached max iterations (${maxIterations}) without completion\x1B[0m`);
26736
+ runState.status = "max_iterations";
26737
+ saveGoalState(runState, stateFile);
26738
+ }
26739
+ }
26740
+ return runState;
26741
+ }
26742
+ function buildGateFixPrompt(goal, gateResult, iteration) {
26743
+ return `# Goal
26744
+
26745
+ ${goal.goal}
26746
+
26747
+ # Urgent: Fix Gate Failures
26748
+
26749
+ The final verification gate FAILED. You must fix these issues:
26750
+
26751
+ ## Failures
26752
+
26753
+ ${gateResult.failures.map((f) => `- ${f}`).join(`
26754
+ `)}
26755
+
26756
+ ## Check Results
26757
+
26758
+ ${gateResult.checks.map((c) => `- ${c.passed ? "PASS" : "FAIL"}: ${c.name} — ${c.output}`).join(`
26759
+ `)}
26760
+
26761
+ ## Gate Summary
26762
+
26763
+ ${gateResult.summary}
26764
+
26765
+ # Instructions
26766
+
26767
+ Fix ALL of the failures listed above. Focus exclusively on making the gate pass. Do not add new features.
26768
+
26769
+ When done, output your state update:
26770
+
26771
+ \`\`\`yaml
26772
+ completed_items:
26773
+ - "fixed: description of what you fixed"
26774
+ remaining_items:
26775
+ - "any remaining issues"
26776
+ known_issues:
26777
+ - "any issues you could not fix"
26778
+ files_modified:
26779
+ - "path/to/file.ts"
26780
+ agent_done: true
26781
+ \`\`\`
26782
+
26783
+ Then write a brief summary of what you fixed.`;
26784
+ }
26785
+ function parseGoalFile(path) {
26786
+ const content = readFileSync3(path, "utf-8");
26787
+ let data;
26788
+ try {
26789
+ data = import_yaml.parse(content);
26790
+ } catch (e) {
26791
+ const error2 = e;
26792
+ console.error(`\x1B[31mError parsing ${path}:\x1B[0m`);
26793
+ console.error(` ${error2.message.split(`
26794
+ `)[0]}`);
26795
+ process.exit(1);
26796
+ }
26797
+ if (!data.goal) {
26798
+ console.error(`\x1B[31mError: goal.yaml must have a 'goal' field\x1B[0m`);
26799
+ process.exit(1);
26800
+ }
26801
+ return data;
26802
+ }
26803
+
26804
+ // src/planner.ts
26805
+ import { writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
26806
+ import { execSync as execSync3 } from "child_process";
26807
+ import * as readline from "readline";
26808
+ var claudeExecutablePath3;
26809
+ function findClaudeExecutable3() {
26810
+ if (claudeExecutablePath3 !== undefined)
26811
+ return claudeExecutablePath3;
26812
+ if (process.env.CLAUDE_CODE_PATH) {
26813
+ claudeExecutablePath3 = process.env.CLAUDE_CODE_PATH;
26814
+ return claudeExecutablePath3;
26815
+ }
26816
+ try {
26817
+ const cmd = process.platform === "win32" ? "where claude" : "which claude";
26818
+ claudeExecutablePath3 = execSync3(cmd, { encoding: "utf-8" }).trim().split(`
26819
+ `)[0];
26820
+ return claudeExecutablePath3;
26821
+ } catch {
26822
+ const commonPaths = [
26823
+ "/usr/local/bin/claude",
26824
+ "/opt/homebrew/bin/claude",
26825
+ `${process.env.HOME}/.local/bin/claude`
26826
+ ];
26827
+ for (const p of commonPaths) {
26828
+ if (existsSync6(p)) {
26829
+ claudeExecutablePath3 = p;
26830
+ return claudeExecutablePath3;
26831
+ }
26832
+ }
26833
+ }
26834
+ return;
26835
+ }
26836
+ function createReadline() {
26837
+ return readline.createInterface({
26838
+ input: process.stdin,
26839
+ output: process.stdout
26840
+ });
26841
+ }
26842
+ function ask(rl, question) {
26843
+ return new Promise((resolve2) => {
26844
+ rl.question(question, (answer) => resolve2(answer.trim()));
26845
+ });
26846
+ }
26847
+ var PLANNER_SYSTEM_PROMPT = `You are an expert software architect helping plan an autonomous overnight build.
26848
+
26849
+ Your job is to have a focused design conversation with the user, then produce a goal.yaml file that an autonomous build agent will use to implement the project overnight.
26850
+
26851
+ Guidelines:
26852
+ - Ask clarifying questions about scope, technology choices, priorities, and constraints
26853
+ - Keep the conversation focused and efficient — 3-5 rounds max
26854
+ - When you have enough information, produce the goal.yaml
26855
+ - The goal.yaml should be specific enough for an agent to work autonomously
26856
+ - Include concrete acceptance criteria that can be verified
26857
+ - Include verification commands when possible (build, test, lint)
26858
+ - Set realistic constraints
26859
+
26860
+ When you're ready to produce the final plan, output it in this format:
26861
+
26862
+ \`\`\`yaml
26863
+ goal: "Clear description of what to build"
26864
+
26865
+ acceptance_criteria:
26866
+ - "Specific, verifiable criterion 1"
26867
+ - "Specific, verifiable criterion 2"
26868
+
26869
+ verification_commands:
26870
+ - "npm run build"
26871
+ - "npm test"
26872
+
26873
+ constraints:
26874
+ - "Don't modify existing API contracts"
26875
+
26876
+ max_iterations: 15
26877
+ convergence_threshold: 3
26878
+
26879
+ defaults:
26880
+ timeout_seconds: 600
26881
+ allowed_tools:
26882
+ - Read
26883
+ - Edit
26884
+ - Write
26885
+ - Glob
26886
+ - Grep
26887
+ - Bash
26888
+ security:
26889
+ sandbox_dir: "."
26890
+ max_turns: 150
26891
+ \`\`\`
26892
+
26893
+ IMPORTANT: Only output the yaml block when you and the user agree the plan is ready. Before that, ask questions and discuss.`;
26894
+ async function runPlanner(initialGoal, options = {}) {
26895
+ const log = options.log ?? ((msg) => console.log(msg));
26896
+ const outputFile = options.outputFile ?? "goal.yaml";
26897
+ const claudePath = findClaudeExecutable3();
26898
+ if (!claudePath) {
26899
+ log("\x1B[31m✗ Error: Could not find 'claude' CLI.\x1B[0m");
26900
+ return null;
26901
+ }
26902
+ log("\x1B[1movernight plan: Interactive design session\x1B[0m");
26903
+ log("\x1B[2mDescribe your goal and I'll help shape it into a plan.\x1B[0m");
26904
+ log(`\x1B[2mType 'done' to finalize, 'quit' to abort.\x1B[0m
26905
+ `);
26906
+ const rl = createReadline();
26907
+ const conversationHistory = [];
26908
+ let currentPrompt = `The user wants to plan the following project for an overnight autonomous build:
26909
+
26910
+ ${initialGoal}
26911
+
26912
+ Ask clarifying questions to understand scope, tech choices, priorities, and constraints. Be concise.`;
26913
+ try {
26914
+ let sessionId;
26915
+ for (let round = 0;round < 10; round++) {
26916
+ const sdkOptions = {
26917
+ allowedTools: ["Read", "Glob", "Grep"],
26918
+ systemPrompt: PLANNER_SYSTEM_PROMPT,
26919
+ permissionMode: "acceptEdits",
26920
+ pathToClaudeCodeExecutable: claudePath,
26921
+ ...sessionId && { resume: sessionId }
26922
+ };
26923
+ let result;
26924
+ const conversation = query({ prompt: currentPrompt, options: sdkOptions });
26925
+ for await (const message of conversation) {
26926
+ if (message.type === "result") {
26927
+ sessionId = message.session_id;
26928
+ if (message.subtype === "success") {
26929
+ result = message.result;
26930
+ }
26931
+ } else if (message.type === "system" && "subtype" in message) {
26932
+ if (message.subtype === "init") {
26933
+ sessionId = message.session_id;
26934
+ }
26935
+ }
26936
+ }
26937
+ if (!result) {
26938
+ log("\x1B[31m✗ No response from planner\x1B[0m");
26939
+ break;
26940
+ }
26941
+ conversationHistory.push({ role: "assistant", content: result });
26942
+ const yamlMatch = result.match(/```yaml\n([\s\S]*?)\n```/);
26943
+ if (yamlMatch) {
26944
+ log(`
26945
+ \x1B[1m━━━ Proposed Plan ━━━\x1B[0m
26946
+ `);
26947
+ log(yamlMatch[1]);
26948
+ log(`
26949
+ \x1B[1m━━━━━━━━━━━━━━━━━━━━\x1B[0m
26950
+ `);
26951
+ const answer = await ask(rl, "\x1B[36m?\x1B[0m Accept this plan? (yes/no/revise): ");
26952
+ if (answer.toLowerCase() === "yes" || answer.toLowerCase() === "y") {
26953
+ writeFileSync4(outputFile, yamlMatch[1]);
26954
+ log(`
26955
+ \x1B[32m✓ Plan saved to ${outputFile}\x1B[0m`);
26956
+ log(`Run with: \x1B[1movernight run ${outputFile}\x1B[0m`);
26957
+ rl.close();
26958
+ const { parse: parseYaml2 } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
26959
+ return parseYaml2(yamlMatch[1]);
26960
+ } else if (answer.toLowerCase() === "quit" || answer.toLowerCase() === "q") {
26961
+ log("\x1B[33mAborted\x1B[0m");
26962
+ rl.close();
26963
+ return null;
26964
+ } else {
26965
+ const revision = await ask(rl, "\x1B[36m?\x1B[0m What would you like to change? ");
26966
+ currentPrompt = revision;
26967
+ conversationHistory.push({ role: "user", content: revision });
26968
+ continue;
26969
+ }
26970
+ }
26971
+ log(`
26972
+ \x1B[2m─── Planner ───\x1B[0m
26973
+ `);
26974
+ log(result);
26975
+ log("");
26976
+ const userInput = await ask(rl, "\x1B[36m>\x1B[0m ");
26977
+ if (userInput.toLowerCase() === "done") {
26978
+ currentPrompt = "The user is satisfied. Please produce the final goal.yaml now based on our discussion.";
26979
+ conversationHistory.push({ role: "user", content: currentPrompt });
26980
+ continue;
26981
+ }
26982
+ if (userInput.toLowerCase() === "quit" || userInput.toLowerCase() === "q") {
26983
+ log("\x1B[33mAborted\x1B[0m");
26984
+ rl.close();
26985
+ return null;
26986
+ }
26987
+ currentPrompt = userInput;
26988
+ conversationHistory.push({ role: "user", content: userInput });
26989
+ }
26990
+ } finally {
26991
+ rl.close();
26992
+ }
26993
+ log("\x1B[33m⚠ Design session ended without producing a plan\x1B[0m");
26994
+ return null;
26995
+ }
26996
+
26201
26997
  // src/cli.ts
26202
26998
  var AGENT_HELP = `
26203
- # overnight - Batch Job Runner for Claude Code
26999
+ # overnight - Autonomous Build Runner for Claude Code
26204
27000
 
26205
- Queue tasks, run them unattended, get results. Designed for overnight/AFK use.
27001
+ Two modes: goal-driven autonomous loops, or task-list batch jobs.
26206
27002
 
26207
27003
  ## Quick Start
26208
27004
 
26209
27005
  \`\`\`bash
26210
- # Create a tasks.yaml file
26211
- overnight init
27006
+ # Hammer mode: just give it a goal and go
27007
+ overnight hammer "Build a multiplayer MMO"
26212
27008
 
26213
- # Run all tasks
26214
- overnight run tasks.yaml
27009
+ # Or: design session first, then autonomous build
27010
+ overnight plan "Build a multiplayer game" # Interactive design → goal.yaml
27011
+ overnight run goal.yaml --notify # Autonomous build loop
26215
27012
 
26216
- # Run with notifications and report
26217
- overnight run tasks.yaml --notify -r report.md
27013
+ # Task mode: explicit task list
27014
+ overnight run tasks.yaml --notify
26218
27015
  \`\`\`
26219
27016
 
26220
27017
  ## Commands
26221
27018
 
26222
27019
  | Command | Description |
26223
27020
  |---------|-------------|
26224
- | \`overnight run <file>\` | Run jobs from YAML file |
27021
+ | \`overnight hammer "<goal>"\` | Autonomous build loop from a string |
27022
+ | \`overnight plan "<goal>"\` | Interactive design session → goal.yaml |
27023
+ | \`overnight run <file>\` | Run goal.yaml (loop) or tasks.yaml (batch) |
26225
27024
  | \`overnight resume <file>\` | Resume interrupted run from checkpoint |
26226
27025
  | \`overnight single "<prompt>"\` | Run a single task directly |
26227
- | \`overnight init\` | Create example tasks.yaml |
27026
+ | \`overnight init\` | Create example goal.yaml or tasks.yaml |
27027
+
27028
+ ## Goal Mode (goal.yaml)
27029
+
27030
+ Autonomous convergence loop: agent iterates toward a goal, then a separate
27031
+ gate agent verifies everything before declaring done.
27032
+
27033
+ \`\`\`yaml
27034
+ goal: "Build a clone of Flappy Bird with leaderboard"
27035
+
27036
+ acceptance_criteria:
27037
+ - "Game renders and is playable in browser"
27038
+ - "Leaderboard persists scores to localStorage"
26228
27039
 
26229
- ## tasks.yaml Format
27040
+ verification_commands:
27041
+ - "npm run build"
27042
+ - "npm test"
27043
+
27044
+ constraints:
27045
+ - "Use vanilla JS, no frameworks"
27046
+
27047
+ max_iterations: 15
27048
+ \`\`\`
27049
+
27050
+ ## Task Mode (tasks.yaml)
27051
+
27052
+ Explicit task list with optional dependency DAG.
26230
27053
 
26231
27054
  \`\`\`yaml
26232
27055
  defaults:
26233
- timeout_seconds: 300 # Per-task timeout (default: 300)
26234
- verify: true # Run verification pass (default: true)
26235
- allowed_tools: # Whitelist tools (default: Read,Edit,Write,Glob,Grep)
26236
- - Read
26237
- - Edit
26238
- - Glob
26239
- - Grep
27056
+ timeout_seconds: 300
27057
+ verify: true
27058
+ allowed_tools: [Read, Edit, Write, Glob, Grep]
26240
27059
 
26241
27060
  tasks:
26242
- # Simple format
26243
27061
  - "Fix the bug in auth.py"
26244
-
26245
- # Detailed format
26246
27062
  - prompt: "Add input validation"
26247
27063
  timeout_seconds: 600
26248
- verify: false
26249
- allowed_tools: [Read, Edit, Bash, Glob, Grep]
26250
27064
  \`\`\`
26251
27065
 
26252
27066
  ## Key Options
@@ -26256,52 +27070,55 @@ tasks:
26256
27070
  | \`-o, --output <file>\` | Save results JSON |
26257
27071
  | \`-r, --report <file>\` | Generate markdown report |
26258
27072
  | \`-s, --state-file <file>\` | Custom checkpoint file |
27073
+ | \`--max-iterations <n>\` | Max build loop iterations (goal mode) |
26259
27074
  | \`--notify\` | Send push notification via ntfy.sh |
26260
- | \`--notify-topic <topic>\` | ntfy.sh topic (default: overnight) |
26261
27075
  | \`-q, --quiet\` | Minimal output |
26262
27076
 
26263
- ## Features
26264
-
26265
- 1. **Crash Recovery**: Auto-checkpoints after each job. Use \`overnight resume\` to continue.
26266
- 2. **Retry Logic**: Auto-retries 3x on API/network errors with exponential backoff.
26267
- 3. **Notifications**: \`--notify\` sends summary to ntfy.sh (free, no signup).
26268
- 4. **Reports**: \`-r report.md\` generates markdown summary with next steps.
26269
- 5. **Security**: No Bash by default. Whitelist tools per-task.
26270
-
26271
27077
  ## Example Workflows
26272
27078
 
26273
27079
  \`\`\`bash
26274
- # Development: run overnight, check in morning
26275
- nohup overnight run tasks.yaml --notify -r report.md -o results.json > overnight.log 2>&1 &
27080
+ # Simplest: just hammer a goal overnight
27081
+ nohup overnight hammer "Build a REST API with auth and tests" --notify > overnight.log 2>&1 &
26276
27082
 
26277
- # CI/CD: run and fail if any task fails
26278
- overnight run tasks.yaml -q
27083
+ # Design first, then run
27084
+ overnight plan "Build a REST API with auth"
27085
+ nohup overnight run goal.yaml --notify > overnight.log 2>&1 &
26279
27086
 
26280
- # Single task with Bash access
26281
- overnight single "Run tests and fix failures" -T Read -T Edit -T Bash -T Glob
27087
+ # Batch tasks overnight
27088
+ nohup overnight run tasks.yaml --notify -r report.md > overnight.log 2>&1 &
26282
27089
 
26283
- # Resume after crash/interrupt
26284
- overnight resume tasks.yaml
27090
+ # Resume after crash
27091
+ overnight resume goal.yaml
26285
27092
  \`\`\`
26286
27093
 
26287
27094
  ## Exit Codes
26288
27095
 
26289
- - 0: All tasks succeeded
26290
- - 1: One or more tasks failed
27096
+ - 0: All tasks succeeded / gate passed
27097
+ - 1: Failures occurred / gate failed
26291
27098
 
26292
27099
  ## Files Created
26293
27100
 
26294
- - \`.overnight-state.json\` - Checkpoint file (deleted on success)
27101
+ - \`.overnight-goal-state.json\` - Goal mode checkpoint
27102
+ - \`.overnight-iterations/\` - Per-iteration state + summaries
27103
+ - \`.overnight-state.json\` - Task mode checkpoint
26295
27104
  - \`report.md\` - Summary report (if -r used)
26296
- - \`results.json\` - Full results (if -o used)
26297
27105
 
26298
27106
  Run \`overnight <command> --help\` for command-specific options.
26299
27107
  `;
27108
+ function isGoalFile(path) {
27109
+ try {
27110
+ const content = readFileSync5(path, "utf-8");
27111
+ const data = import_yaml2.parse(content);
27112
+ return typeof data?.goal === "string";
27113
+ } catch {
27114
+ return false;
27115
+ }
27116
+ }
26300
27117
  function parseTasksFile(path, cliSecurity) {
26301
- const content = readFileSync3(path, "utf-8");
27118
+ const content = readFileSync5(path, "utf-8");
26302
27119
  let data;
26303
27120
  try {
26304
- data = $parse(content);
27121
+ data = import_yaml2.parse(content);
26305
27122
  } catch (e) {
26306
27123
  const error2 = e;
26307
27124
  console.error(`\x1B[31mError parsing ${path}:\x1B[0m`);
@@ -26368,69 +27185,146 @@ ${bold}Job Results${reset}`);
26368
27185
  ${bold}Summary:${reset} ${succeeded}/${results.length} succeeded`);
26369
27186
  }
26370
27187
  var program2 = new Command;
26371
- program2.name("overnight").description("Batch job runner for Claude Code").version("0.2.0").action(() => {
27188
+ program2.name("overnight").description("Batch job runner for Claude Code").version("0.3.0").action(() => {
26372
27189
  console.log(AGENT_HELP);
26373
27190
  });
26374
- program2.command("run").description("Run jobs from a YAML tasks file").argument("<tasks-file>", "Path to tasks.yaml file").option("-o, --output <file>", "Output file for results JSON").option("-q, --quiet", "Minimal output").option("-s, --state-file <file>", "Custom state file path").option("--notify", "Send push notification via ntfy.sh").option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC).option("-r, --report <file>", "Generate markdown report").option("--sandbox <dir>", "Sandbox directory (restrict file access)").option("--max-turns <n>", "Max agent iterations per task", String(DEFAULT_MAX_TURNS)).option("--audit-log <file>", "Audit log file path").option("--no-security", "Disable default security (deny patterns)").action(async (tasksFile, opts) => {
26375
- if (!existsSync5(tasksFile)) {
26376
- console.error(`Error: File not found: ${tasksFile}`);
26377
- process.exit(1);
26378
- }
26379
- const cliSecurity = opts.security === false ? undefined : {
26380
- ...opts.sandbox && { sandbox_dir: opts.sandbox },
26381
- ...opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) },
26382
- ...opts.auditLog && { audit_log: opts.auditLog }
26383
- };
26384
- const { configs, security } = parseTasksFile(tasksFile, cliSecurity);
26385
- if (configs.length === 0) {
26386
- console.error("No tasks found in file");
27191
+ program2.command("run").description("Run goal.yaml (autonomous loop) or tasks.yaml (batch jobs)").argument("<file>", "Path to goal.yaml or tasks.yaml").option("-o, --output <file>", "Output file for results JSON").option("-q, --quiet", "Minimal output").option("-s, --state-file <file>", "Custom state file path").option("--notify", "Send push notification via ntfy.sh").option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC).option("-r, --report <file>", "Generate markdown report").option("--sandbox <dir>", "Sandbox directory (restrict file access)").option("--max-turns <n>", "Max agent iterations per task", String(DEFAULT_MAX_TURNS)).option("--max-iterations <n>", "Max build loop iterations (goal mode)", String(DEFAULT_MAX_ITERATIONS)).option("--audit-log <file>", "Audit log file path").option("--no-security", "Disable default security (deny patterns)").action(async (inputFile, opts) => {
27192
+ if (!existsSync7(inputFile)) {
27193
+ console.error(`Error: File not found: ${inputFile}`);
26387
27194
  process.exit(1);
26388
27195
  }
26389
- const existingState = loadState(opts.stateFile ?? DEFAULT_STATE_FILE);
26390
- if (existingState) {
26391
- const done = Object.keys(existingState.completed).length;
26392
- const pending = configs.filter((c) => !(taskKey(c) in existingState.completed)).length;
26393
- console.log(`\x1B[1movernight: Resuming — ${done} done, ${pending} remaining\x1B[0m`);
26394
- console.log(`\x1B[2mLast checkpoint: ${existingState.timestamp}\x1B[0m`);
27196
+ if (isGoalFile(inputFile)) {
27197
+ const goal = parseGoalFile(inputFile);
27198
+ if (opts.maxIterations) {
27199
+ goal.max_iterations = parseInt(opts.maxIterations, 10);
27200
+ }
27201
+ if (opts.sandbox) {
27202
+ goal.defaults = goal.defaults ?? {};
27203
+ goal.defaults.security = goal.defaults.security ?? {};
27204
+ goal.defaults.security.sandbox_dir = opts.sandbox;
27205
+ }
27206
+ if (opts.maxTurns) {
27207
+ goal.defaults = goal.defaults ?? {};
27208
+ goal.defaults.security = goal.defaults.security ?? {};
27209
+ goal.defaults.security.max_turns = parseInt(opts.maxTurns, 10);
27210
+ }
27211
+ const log = opts.quiet ? undefined : (msg) => console.log(msg);
27212
+ const startTime = Date.now();
27213
+ const runState = await runGoal(goal, {
27214
+ stateFile: opts.stateFile ?? DEFAULT_GOAL_STATE_FILE,
27215
+ log
27216
+ });
27217
+ const totalDuration = (Date.now() - startTime) / 1000;
27218
+ if (opts.notify) {
27219
+ const passed = runState.status === "gate_passed";
27220
+ const title = passed ? `overnight: Goal completed (${runState.iterations.length} iterations)` : `overnight: ${runState.status} after ${runState.iterations.length} iterations`;
27221
+ const message = passed ? `Gate passed. ${runState.iterations.length} iterations.` : `Status: ${runState.status}. Check report for details.`;
27222
+ try {
27223
+ await fetch(`https://ntfy.sh/${opts.notifyTopic ?? DEFAULT_NTFY_TOPIC}`, {
27224
+ method: "POST",
27225
+ headers: {
27226
+ Title: title,
27227
+ Priority: passed ? "default" : "high",
27228
+ Tags: passed ? "white_check_mark" : "warning"
27229
+ },
27230
+ body: message
27231
+ });
27232
+ if (!opts.quiet)
27233
+ console.log(`\x1B[2mNotification sent\x1B[0m`);
27234
+ } catch {
27235
+ if (!opts.quiet)
27236
+ console.log("\x1B[33mWarning: Failed to send notification\x1B[0m");
27237
+ }
27238
+ }
27239
+ if (!opts.quiet) {
27240
+ console.log(`
27241
+ \x1B[1m━━━ Goal Run Summary ━━━\x1B[0m`);
27242
+ console.log(`Status: ${runState.status === "gate_passed" ? "\x1B[32m" : "\x1B[31m"}${runState.status}\x1B[0m`);
27243
+ console.log(`Iterations: ${runState.iterations.length}`);
27244
+ console.log(`Gate attempts: ${runState.gate_results.length}`);
27245
+ let durationStr;
27246
+ if (totalDuration >= 3600) {
27247
+ const hours = Math.floor(totalDuration / 3600);
27248
+ const mins = Math.floor(totalDuration % 3600 / 60);
27249
+ durationStr = `${hours}h ${mins}m`;
27250
+ } else if (totalDuration >= 60) {
27251
+ const mins = Math.floor(totalDuration / 60);
27252
+ const secs = Math.floor(totalDuration % 60);
27253
+ durationStr = `${mins}m ${secs}s`;
27254
+ } else {
27255
+ durationStr = `${totalDuration.toFixed(1)}s`;
27256
+ }
27257
+ console.log(`Duration: ${durationStr}`);
27258
+ if (runState.gate_results.length > 0) {
27259
+ const lastGate = runState.gate_results[runState.gate_results.length - 1];
27260
+ console.log(`
27261
+ Gate: ${lastGate.summary}`);
27262
+ for (const check2 of lastGate.checks) {
27263
+ const icon = check2.passed ? "\x1B[32m✓\x1B[0m" : "\x1B[31m✗\x1B[0m";
27264
+ console.log(` ${icon} ${check2.name}`);
27265
+ }
27266
+ }
27267
+ }
27268
+ if (runState.status !== "gate_passed") {
27269
+ process.exit(1);
27270
+ }
26395
27271
  } else {
26396
- console.log(`\x1B[1movernight: Running ${configs.length} jobs...\x1B[0m`);
26397
- }
26398
- if (security && !opts.quiet) {
26399
- console.log("\x1B[2mSecurity:\x1B[0m");
26400
- validateSecurityConfig(security);
26401
- }
26402
- console.log("");
26403
- const log = opts.quiet ? undefined : (msg) => console.log(msg);
26404
- const startTime = Date.now();
26405
- const reloadConfigs = () => parseTasksFile(tasksFile, cliSecurity).configs;
26406
- const results = await runJobsWithState(configs, {
26407
- stateFile: opts.stateFile,
26408
- log,
26409
- reloadConfigs
26410
- });
26411
- const totalDuration = (Date.now() - startTime) / 1000;
26412
- if (opts.notify) {
26413
- const success = await sendNtfyNotification(results, totalDuration, opts.notifyTopic);
26414
- if (success) {
26415
- console.log(`\x1B[2mNotification sent to ntfy.sh/${opts.notifyTopic}\x1B[0m`);
27272
+ const cliSecurity = opts.security === false ? undefined : {
27273
+ ...opts.sandbox && { sandbox_dir: opts.sandbox },
27274
+ ...opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) },
27275
+ ...opts.auditLog && { audit_log: opts.auditLog }
27276
+ };
27277
+ const { configs, security } = parseTasksFile(inputFile, cliSecurity);
27278
+ if (configs.length === 0) {
27279
+ console.error("No tasks found in file");
27280
+ process.exit(1);
27281
+ }
27282
+ const existingState = loadState(opts.stateFile ?? DEFAULT_STATE_FILE);
27283
+ if (existingState) {
27284
+ const done = Object.keys(existingState.completed).length;
27285
+ const pending = configs.filter((c) => !(taskKey(c) in existingState.completed)).length;
27286
+ console.log(`\x1B[1movernight: Resuming — ${done} done, ${pending} remaining\x1B[0m`);
27287
+ console.log(`\x1B[2mLast checkpoint: ${existingState.timestamp}\x1B[0m`);
26416
27288
  } else {
26417
- console.log("\x1B[33mWarning: Failed to send notification\x1B[0m");
27289
+ console.log(`\x1B[1movernight: Running ${configs.length} jobs...\x1B[0m`);
27290
+ }
27291
+ if (security && !opts.quiet) {
27292
+ console.log("\x1B[2mSecurity:\x1B[0m");
27293
+ validateSecurityConfig(security);
27294
+ }
27295
+ console.log("");
27296
+ const log = opts.quiet ? undefined : (msg) => console.log(msg);
27297
+ const startTime = Date.now();
27298
+ const reloadConfigs = () => parseTasksFile(inputFile, cliSecurity).configs;
27299
+ const results = await runJobsWithState(configs, {
27300
+ stateFile: opts.stateFile,
27301
+ log,
27302
+ reloadConfigs
27303
+ });
27304
+ const totalDuration = (Date.now() - startTime) / 1000;
27305
+ if (opts.notify) {
27306
+ const success = await sendNtfyNotification(results, totalDuration, opts.notifyTopic);
27307
+ if (success) {
27308
+ console.log(`\x1B[2mNotification sent to ntfy.sh/${opts.notifyTopic}\x1B[0m`);
27309
+ } else {
27310
+ console.log("\x1B[33mWarning: Failed to send notification\x1B[0m");
27311
+ }
26418
27312
  }
26419
- }
26420
- if (opts.report) {
26421
- generateReport(results, totalDuration, opts.report);
26422
- console.log(`\x1B[2mReport saved to ${opts.report}\x1B[0m`);
26423
- }
26424
- if (!opts.quiet) {
26425
- printSummary(results);
26426
- }
26427
- if (opts.output) {
26428
- writeFileSync3(opts.output, resultsToJson(results));
26429
- console.log(`
27313
+ if (opts.report) {
27314
+ generateReport(results, totalDuration, opts.report);
27315
+ console.log(`\x1B[2mReport saved to ${opts.report}\x1B[0m`);
27316
+ }
27317
+ if (!opts.quiet) {
27318
+ printSummary(results);
27319
+ }
27320
+ if (opts.output) {
27321
+ writeFileSync5(opts.output, resultsToJson(results));
27322
+ console.log(`
26430
27323
  \x1B[2mResults saved to ${opts.output}\x1B[0m`);
26431
- }
26432
- if (results.some((r) => r.status !== "success")) {
26433
- process.exit(1);
27324
+ }
27325
+ if (results.some((r) => r.status !== "success")) {
27326
+ process.exit(1);
27327
+ }
26434
27328
  }
26435
27329
  });
26436
27330
  program2.command("resume").description("Resume a previous run from saved state").argument("<tasks-file>", "Path to tasks.yaml file").option("-o, --output <file>", "Output file for results JSON").option("-q, --quiet", "Minimal output").option("-s, --state-file <file>", "Custom state file path").option("--notify", "Send push notification via ntfy.sh").option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC).option("-r, --report <file>", "Generate markdown report").option("--sandbox <dir>", "Sandbox directory (restrict file access)").option("--max-turns <n>", "Max agent iterations per task", String(DEFAULT_MAX_TURNS)).option("--audit-log <file>", "Audit log file path").option("--no-security", "Disable default security (deny patterns)").action(async (tasksFile, opts) => {
@@ -26441,7 +27335,7 @@ program2.command("resume").description("Resume a previous run from saved state")
26441
27335
  console.error("Run 'overnight run' first to start jobs.");
26442
27336
  process.exit(1);
26443
27337
  }
26444
- if (!existsSync5(tasksFile)) {
27338
+ if (!existsSync7(tasksFile)) {
26445
27339
  console.error(`Error: File not found: ${tasksFile}`);
26446
27340
  process.exit(1);
26447
27341
  }
@@ -26489,7 +27383,7 @@ program2.command("resume").description("Resume a previous run from saved state")
26489
27383
  printSummary(results);
26490
27384
  }
26491
27385
  if (opts.output) {
26492
- writeFileSync3(opts.output, resultsToJson(results));
27386
+ writeFileSync5(opts.output, resultsToJson(results));
26493
27387
  console.log(`
26494
27388
  \x1B[2mResults saved to ${opts.output}\x1B[0m`);
26495
27389
  }
@@ -26527,8 +27421,91 @@ program2.command("single").description("Run a single job directly").argument("<p
26527
27421
  process.exit(1);
26528
27422
  }
26529
27423
  });
26530
- program2.command("init").description("Create an example tasks.yaml file").action(() => {
26531
- const example = `# overnight task file
27424
+ program2.command("hammer").description("Autonomous build loop from an inline goal string").argument("<goal>", "The goal to work toward").option("--max-iterations <n>", "Max build loop iterations", String(DEFAULT_MAX_ITERATIONS)).option("--max-turns <n>", "Max agent turns per iteration", String(DEFAULT_MAX_TURNS)).option("-t, --timeout <seconds>", "Timeout per iteration in seconds", "600").option("-T, --tools <tool...>", "Allowed tools").option("--sandbox <dir>", "Sandbox directory").option("-s, --state-file <file>", "Custom state file path").option("--notify", "Send push notification via ntfy.sh").option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC).option("-q, --quiet", "Minimal output").option("--no-security", "Disable default security").action(async (goalStr, opts) => {
27425
+ const goal = {
27426
+ goal: goalStr,
27427
+ max_iterations: parseInt(opts.maxIterations, 10),
27428
+ defaults: {
27429
+ timeout_seconds: parseInt(opts.timeout, 10),
27430
+ allowed_tools: opts.tools ?? [...DEFAULT_TOOLS, "Bash"],
27431
+ security: opts.security === false ? undefined : {
27432
+ ...opts.sandbox && { sandbox_dir: opts.sandbox },
27433
+ max_turns: parseInt(opts.maxTurns, 10),
27434
+ deny_patterns: DEFAULT_DENY_PATTERNS
27435
+ }
27436
+ }
27437
+ };
27438
+ const log = opts.quiet ? undefined : (msg) => console.log(msg);
27439
+ const startTime = Date.now();
27440
+ const runState = await runGoal(goal, {
27441
+ stateFile: opts.stateFile ?? DEFAULT_GOAL_STATE_FILE,
27442
+ log
27443
+ });
27444
+ const totalDuration = (Date.now() - startTime) / 1000;
27445
+ if (opts.notify) {
27446
+ const passed = runState.status === "gate_passed";
27447
+ try {
27448
+ await fetch(`https://ntfy.sh/${opts.notifyTopic ?? DEFAULT_NTFY_TOPIC}`, {
27449
+ method: "POST",
27450
+ headers: {
27451
+ Title: passed ? `overnight: Goal completed (${runState.iterations.length} iterations)` : `overnight: ${runState.status} after ${runState.iterations.length} iterations`,
27452
+ Priority: passed ? "default" : "high",
27453
+ Tags: passed ? "white_check_mark" : "warning"
27454
+ },
27455
+ body: passed ? `Gate passed. ${runState.iterations.length} iterations.` : `Status: ${runState.status}. Check report for details.`
27456
+ });
27457
+ if (!opts.quiet)
27458
+ console.log(`\x1B[2mNotification sent\x1B[0m`);
27459
+ } catch {
27460
+ if (!opts.quiet)
27461
+ console.log("\x1B[33mWarning: Failed to send notification\x1B[0m");
27462
+ }
27463
+ }
27464
+ if (!opts.quiet) {
27465
+ console.log(`
27466
+ \x1B[1m━━━ Hammer Summary ━━━\x1B[0m`);
27467
+ console.log(`Status: ${runState.status === "gate_passed" ? "\x1B[32m" : "\x1B[31m"}${runState.status}\x1B[0m`);
27468
+ console.log(`Iterations: ${runState.iterations.length}`);
27469
+ console.log(`Gate attempts: ${runState.gate_results.length}`);
27470
+ let durationStr;
27471
+ if (totalDuration >= 3600) {
27472
+ const hours = Math.floor(totalDuration / 3600);
27473
+ const mins = Math.floor(totalDuration % 3600 / 60);
27474
+ durationStr = `${hours}h ${mins}m`;
27475
+ } else if (totalDuration >= 60) {
27476
+ const mins = Math.floor(totalDuration / 60);
27477
+ const secs = Math.floor(totalDuration % 60);
27478
+ durationStr = `${mins}m ${secs}s`;
27479
+ } else {
27480
+ durationStr = `${totalDuration.toFixed(1)}s`;
27481
+ }
27482
+ console.log(`Duration: ${durationStr}`);
27483
+ if (runState.gate_results.length > 0) {
27484
+ const lastGate = runState.gate_results[runState.gate_results.length - 1];
27485
+ console.log(`
27486
+ Gate: ${lastGate.summary}`);
27487
+ for (const check2 of lastGate.checks) {
27488
+ const icon = check2.passed ? "\x1B[32m✓\x1B[0m" : "\x1B[31m✗\x1B[0m";
27489
+ console.log(` ${icon} ${check2.name}`);
27490
+ }
27491
+ }
27492
+ }
27493
+ if (runState.status !== "gate_passed") {
27494
+ process.exit(1);
27495
+ }
27496
+ });
27497
+ program2.command("plan").description("Interactive design session to create a goal.yaml").argument("<goal>", "High-level goal description").option("-o, --output <file>", "Output file path", "goal.yaml").action(async (goal, opts) => {
27498
+ const result = await runPlanner(goal, {
27499
+ outputFile: opts.output,
27500
+ log: (msg) => console.log(msg)
27501
+ });
27502
+ if (!result) {
27503
+ process.exit(1);
27504
+ }
27505
+ });
27506
+ program2.command("init").description("Create an example goal.yaml or tasks.yaml").option("--tasks", "Create tasks.yaml instead of goal.yaml").action((opts) => {
27507
+ if (opts.tasks) {
27508
+ const example = `# overnight task file
26532
27509
  # Run with: overnight run tasks.yaml
26533
27510
 
26534
27511
  defaults:
@@ -26548,9 +27525,6 @@ defaults:
26548
27525
  sandbox_dir: "." # Restrict to current directory
26549
27526
  max_turns: 100 # Prevent runaway agents
26550
27527
  # audit_log: "overnight-audit.log" # Uncomment to enable
26551
- # deny_patterns: # Default patterns block .env, .key, .pem, etc.
26552
- # - "**/.env*"
26553
- # - "**/*.key"
26554
27528
 
26555
27529
  tasks:
26556
27530
  # Simple string format
@@ -26572,12 +27546,62 @@ tasks:
26572
27546
  - Glob
26573
27547
  - Grep
26574
27548
  `;
26575
- if (existsSync5("tasks.yaml")) {
26576
- console.log("\x1B[33mtasks.yaml already exists\x1B[0m");
26577
- process.exit(1);
27549
+ if (existsSync7("tasks.yaml")) {
27550
+ console.log("\x1B[33mtasks.yaml already exists\x1B[0m");
27551
+ process.exit(1);
27552
+ }
27553
+ writeFileSync5("tasks.yaml", example);
27554
+ console.log("\x1B[32mCreated tasks.yaml\x1B[0m");
27555
+ console.log("Edit the file, then run: \x1B[1movernight run tasks.yaml\x1B[0m");
27556
+ } else {
27557
+ const example = `# overnight goal file
27558
+ # Run with: overnight run goal.yaml
27559
+ #
27560
+ # Or use "overnight plan" for an interactive design session:
27561
+ # overnight plan "Build a multiplayer game"
27562
+
27563
+ goal: "Describe your project goal here"
27564
+
27565
+ acceptance_criteria:
27566
+ - "The project builds without errors"
27567
+ - "All tests pass"
27568
+ - "Core features are functional"
27569
+
27570
+ verification_commands:
27571
+ - "npm run build"
27572
+ - "npm test"
27573
+
27574
+ constraints:
27575
+ - "Don't modify existing API contracts"
27576
+ - "Keep dependencies minimal"
27577
+
27578
+ # How many build iterations before stopping
27579
+ max_iterations: 15
27580
+
27581
+ # Stop if remaining items don't shrink for this many iterations
27582
+ convergence_threshold: 3
27583
+
27584
+ defaults:
27585
+ timeout_seconds: 600 # 10 minutes per iteration
27586
+ allowed_tools:
27587
+ - Read
27588
+ - Edit
27589
+ - Write
27590
+ - Glob
27591
+ - Grep
27592
+ - Bash
27593
+ security:
27594
+ sandbox_dir: "."
27595
+ max_turns: 150
27596
+ `;
27597
+ if (existsSync7("goal.yaml")) {
27598
+ console.log("\x1B[33mgoal.yaml already exists\x1B[0m");
27599
+ process.exit(1);
27600
+ }
27601
+ writeFileSync5("goal.yaml", example);
27602
+ console.log("\x1B[32mCreated goal.yaml\x1B[0m");
27603
+ console.log("Edit the file, then run: \x1B[1movernight run goal.yaml\x1B[0m");
27604
+ console.log(`\x1B[2mTip: Use 'overnight plan "your goal"' for an interactive design session\x1B[0m`);
26578
27605
  }
26579
- writeFileSync3("tasks.yaml", example);
26580
- console.log("\x1B[32mCreated tasks.yaml\x1B[0m");
26581
- console.log("Edit the file, then run: \x1B[1movernight run tasks.yaml\x1B[0m");
26582
27606
  });
26583
27607
  program2.parse();