@yail259/overnight 0.1.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"];
@@ -8831,9 +8835,145 @@ var DEFAULT_TIMEOUT = 300;
8831
8835
  var DEFAULT_STALL_TIMEOUT = 120;
8832
8836
  var DEFAULT_RETRY_COUNT = 3;
8833
8837
  var DEFAULT_RETRY_DELAY = 5;
8834
- var DEFAULT_VERIFY_PROMPT = "Verify this is complete and correct. If there are issues, list them.";
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";
8842
+ var DEFAULT_MAX_TURNS = 100;
8843
+ var DEFAULT_MAX_ITERATIONS = 20;
8844
+ var DEFAULT_CONVERGENCE_THRESHOLD = 3;
8845
+ var DEFAULT_DENY_PATTERNS = [
8846
+ "**/.env",
8847
+ "**/.env.*",
8848
+ "**/.git/config",
8849
+ "**/credentials*",
8850
+ "**/*.key",
8851
+ "**/*.pem",
8852
+ "**/*.p12",
8853
+ "**/id_rsa*",
8854
+ "**/id_ed25519*",
8855
+ "**/.ssh/*",
8856
+ "**/.aws/*",
8857
+ "**/.npmrc",
8858
+ "**/.netrc"
8859
+ ];
8860
+
8861
+ // src/security.ts
8862
+ import { appendFileSync } from "fs";
8863
+ import { resolve, relative, isAbsolute } from "path";
8864
+ function matchesPattern(filePath, pattern) {
8865
+ const normalizedPath = filePath.replace(/\\/g, "/");
8866
+ let regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/{{GLOBSTAR}}/g, ".*");
8867
+ if (!pattern.startsWith("/")) {
8868
+ regex = `(^|/)${regex}`;
8869
+ }
8870
+ return new RegExp(regex + "$").test(normalizedPath);
8871
+ }
8872
+ function isPathWithinSandbox(filePath, sandboxDir) {
8873
+ const absolutePath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
8874
+ const absoluteSandbox = isAbsolute(sandboxDir) ? sandboxDir : resolve(process.cwd(), sandboxDir);
8875
+ const relativePath = relative(absoluteSandbox, absolutePath);
8876
+ return !relativePath.startsWith("..") && !isAbsolute(relativePath);
8877
+ }
8878
+ function isPathDenied(filePath, denyPatterns) {
8879
+ for (const pattern of denyPatterns) {
8880
+ if (matchesPattern(filePath, pattern)) {
8881
+ return pattern;
8882
+ }
8883
+ }
8884
+ return null;
8885
+ }
8886
+ function createSecurityHooks(config) {
8887
+ const sandboxDir = config.sandbox_dir;
8888
+ const denyPatterns = config.deny_patterns ?? DEFAULT_DENY_PATTERNS;
8889
+ const auditLog = config.audit_log;
8890
+ const preToolUseHook = async (input, _toolUseId, _context) => {
8891
+ const hookEventName = input.hook_event_name;
8892
+ if (hookEventName !== "PreToolUse")
8893
+ return {};
8894
+ const toolName = input.tool_name;
8895
+ const toolInput = input.tool_input;
8896
+ let filePath;
8897
+ if (toolName === "Read" || toolName === "Write" || toolName === "Edit") {
8898
+ filePath = toolInput.file_path;
8899
+ } else if (toolName === "Glob" || toolName === "Grep") {
8900
+ filePath = toolInput.path;
8901
+ } else if (toolName === "Bash") {
8902
+ const command = toolInput.command;
8903
+ if (auditLog) {
8904
+ const timestamp = new Date().toISOString();
8905
+ appendFileSync(auditLog, `${timestamp} [BASH] ${command}
8906
+ `);
8907
+ }
8908
+ return {};
8909
+ }
8910
+ if (!filePath)
8911
+ return {};
8912
+ if (sandboxDir && !isPathWithinSandbox(filePath, sandboxDir)) {
8913
+ return {
8914
+ hookSpecificOutput: {
8915
+ hookEventName: "PreToolUse",
8916
+ permissionDecision: "deny",
8917
+ permissionDecisionReason: `Path "${filePath}" is outside sandbox directory "${sandboxDir}"`
8918
+ }
8919
+ };
8920
+ }
8921
+ const matchedPattern = isPathDenied(filePath, denyPatterns);
8922
+ if (matchedPattern) {
8923
+ return {
8924
+ hookSpecificOutput: {
8925
+ hookEventName: "PreToolUse",
8926
+ permissionDecision: "deny",
8927
+ permissionDecisionReason: `Path "${filePath}" matches deny pattern "${matchedPattern}"`
8928
+ }
8929
+ };
8930
+ }
8931
+ return {};
8932
+ };
8933
+ const postToolUseHook = async (input, _toolUseId, _context) => {
8934
+ if (!auditLog)
8935
+ return {};
8936
+ const hookEventName = input.hook_event_name;
8937
+ if (hookEventName !== "PostToolUse")
8938
+ return {};
8939
+ const toolName = input.tool_name;
8940
+ const toolInput = input.tool_input;
8941
+ const timestamp = new Date().toISOString();
8942
+ let logEntry = `${timestamp} [${toolName}]`;
8943
+ if (toolName === "Read" || toolName === "Write" || toolName === "Edit") {
8944
+ logEntry += ` ${toolInput.file_path}`;
8945
+ } else if (toolName === "Glob") {
8946
+ logEntry += ` pattern=${toolInput.pattern} path=${toolInput.path ?? "."}`;
8947
+ } else if (toolName === "Grep") {
8948
+ logEntry += ` pattern=${toolInput.pattern}`;
8949
+ }
8950
+ appendFileSync(auditLog, logEntry + `
8951
+ `);
8952
+ return {};
8953
+ };
8954
+ return {
8955
+ PreToolUse: [
8956
+ { matcher: "Read|Write|Edit|Glob|Grep|Bash", hooks: [preToolUseHook] }
8957
+ ],
8958
+ PostToolUse: [
8959
+ { matcher: "Read|Write|Edit|Glob|Grep|Bash", hooks: [postToolUseHook] }
8960
+ ]
8961
+ };
8962
+ }
8963
+ function validateSecurityConfig(config) {
8964
+ if (config.sandbox_dir) {
8965
+ const resolved = isAbsolute(config.sandbox_dir) ? config.sandbox_dir : resolve(process.cwd(), config.sandbox_dir);
8966
+ console.log(` Sandbox: ${resolved}`);
8967
+ }
8968
+ const denyPatterns = config.deny_patterns ?? DEFAULT_DENY_PATTERNS;
8969
+ console.log(` Deny patterns: ${denyPatterns.length} patterns`);
8970
+ if (config.max_turns) {
8971
+ console.log(` Max turns: ${config.max_turns}`);
8972
+ }
8973
+ if (config.audit_log) {
8974
+ console.log(` Audit log: ${config.audit_log}`);
8975
+ }
8976
+ }
8837
8977
 
8838
8978
  // node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs
8839
8979
  import { join as join5 } from "path";
@@ -8850,7 +8990,7 @@ import { cwd } from "process";
8850
8990
  import { realpathSync as realpathSync2 } from "fs";
8851
8991
  import { randomUUID } from "crypto";
8852
8992
  import { randomUUID as randomUUID2 } from "crypto";
8853
- import { appendFileSync as appendFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
8993
+ import { appendFileSync as appendFileSync22, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
8854
8994
  import { join as join3 } from "path";
8855
8995
  import { randomUUID as randomUUID3 } from "crypto";
8856
8996
  var __create2 = Object.create;
@@ -11724,7 +11864,7 @@ var require_compile = __commonJS2((exports) => {
11724
11864
  const schOrFunc = root2.refs[ref];
11725
11865
  if (schOrFunc)
11726
11866
  return schOrFunc;
11727
- let _sch = resolve.call(this, root2, ref);
11867
+ let _sch = resolve2.call(this, root2, ref);
11728
11868
  if (_sch === undefined) {
11729
11869
  const schema = (_a = root2.localRefs) === null || _a === undefined ? undefined : _a[ref];
11730
11870
  const { schemaId } = this.opts;
@@ -11751,7 +11891,7 @@ var require_compile = __commonJS2((exports) => {
11751
11891
  function sameSchemaEnv(s1, s2) {
11752
11892
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
11753
11893
  }
11754
- function resolve(root2, ref) {
11894
+ function resolve2(root2, ref) {
11755
11895
  let sch;
11756
11896
  while (typeof (sch = this.refs[ref]) == "string")
11757
11897
  ref = sch;
@@ -12249,54 +12389,54 @@ var require_fast_uri = __commonJS2((exports, module) => {
12249
12389
  }
12250
12390
  return uri;
12251
12391
  }
12252
- function resolve(baseURI, relativeURI, options) {
12392
+ function resolve2(baseURI, relativeURI, options) {
12253
12393
  const schemelessOptions = Object.assign({ scheme: "null" }, options);
12254
12394
  const resolved = resolveComponents(parse6(baseURI, schemelessOptions), parse6(relativeURI, schemelessOptions), schemelessOptions, true);
12255
12395
  return serialize(resolved, { ...schemelessOptions, skipEscape: true });
12256
12396
  }
12257
- function resolveComponents(base, relative, options, skipNormalization) {
12397
+ function resolveComponents(base, relative2, options, skipNormalization) {
12258
12398
  const target = {};
12259
12399
  if (!skipNormalization) {
12260
12400
  base = parse6(serialize(base, options), options);
12261
- relative = parse6(serialize(relative, options), options);
12401
+ relative2 = parse6(serialize(relative2, options), options);
12262
12402
  }
12263
12403
  options = options || {};
12264
- if (!options.tolerant && relative.scheme) {
12265
- target.scheme = relative.scheme;
12266
- target.userinfo = relative.userinfo;
12267
- target.host = relative.host;
12268
- target.port = relative.port;
12269
- target.path = removeDotSegments(relative.path || "");
12270
- target.query = relative.query;
12404
+ if (!options.tolerant && relative2.scheme) {
12405
+ target.scheme = relative2.scheme;
12406
+ target.userinfo = relative2.userinfo;
12407
+ target.host = relative2.host;
12408
+ target.port = relative2.port;
12409
+ target.path = removeDotSegments(relative2.path || "");
12410
+ target.query = relative2.query;
12271
12411
  } else {
12272
- if (relative.userinfo !== undefined || relative.host !== undefined || relative.port !== undefined) {
12273
- target.userinfo = relative.userinfo;
12274
- target.host = relative.host;
12275
- target.port = relative.port;
12276
- target.path = removeDotSegments(relative.path || "");
12277
- target.query = relative.query;
12412
+ if (relative2.userinfo !== undefined || relative2.host !== undefined || relative2.port !== undefined) {
12413
+ target.userinfo = relative2.userinfo;
12414
+ target.host = relative2.host;
12415
+ target.port = relative2.port;
12416
+ target.path = removeDotSegments(relative2.path || "");
12417
+ target.query = relative2.query;
12278
12418
  } else {
12279
- if (!relative.path) {
12419
+ if (!relative2.path) {
12280
12420
  target.path = base.path;
12281
- if (relative.query !== undefined) {
12282
- target.query = relative.query;
12421
+ if (relative2.query !== undefined) {
12422
+ target.query = relative2.query;
12283
12423
  } else {
12284
12424
  target.query = base.query;
12285
12425
  }
12286
12426
  } else {
12287
- if (relative.path.charAt(0) === "/") {
12288
- target.path = removeDotSegments(relative.path);
12427
+ if (relative2.path.charAt(0) === "/") {
12428
+ target.path = removeDotSegments(relative2.path);
12289
12429
  } else {
12290
12430
  if ((base.userinfo !== undefined || base.host !== undefined || base.port !== undefined) && !base.path) {
12291
- target.path = "/" + relative.path;
12431
+ target.path = "/" + relative2.path;
12292
12432
  } else if (!base.path) {
12293
- target.path = relative.path;
12433
+ target.path = relative2.path;
12294
12434
  } else {
12295
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
12435
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
12296
12436
  }
12297
12437
  target.path = removeDotSegments(target.path);
12298
12438
  }
12299
- target.query = relative.query;
12439
+ target.query = relative2.query;
12300
12440
  }
12301
12441
  target.userinfo = base.userinfo;
12302
12442
  target.host = base.host;
@@ -12304,7 +12444,7 @@ var require_fast_uri = __commonJS2((exports, module) => {
12304
12444
  }
12305
12445
  target.scheme = base.scheme;
12306
12446
  }
12307
- target.fragment = relative.fragment;
12447
+ target.fragment = relative2.fragment;
12308
12448
  return target;
12309
12449
  }
12310
12450
  function equal(uriA, uriB, options) {
@@ -12482,7 +12622,7 @@ var require_fast_uri = __commonJS2((exports, module) => {
12482
12622
  var fastUri = {
12483
12623
  SCHEMES,
12484
12624
  normalize,
12485
- resolve,
12625
+ resolve: resolve2,
12486
12626
  resolveComponents,
12487
12627
  equal,
12488
12628
  serialize,
@@ -15144,7 +15284,7 @@ var require_limit = __commonJS2((exports) => {
15144
15284
  };
15145
15285
  exports.default = formatLimitPlugin;
15146
15286
  });
15147
- var require_dist = __commonJS2((exports, module) => {
15287
+ var require_dist2 = __commonJS2((exports, module) => {
15148
15288
  Object.defineProperty(exports, "__esModule", { value: true });
15149
15289
  var formats_1 = require_formats();
15150
15290
  var limit_1 = require_limit();
@@ -16072,7 +16212,7 @@ function logForSdkDebugging(message) {
16072
16212
  const timestamp = new Date().toISOString();
16073
16213
  const output = `${timestamp} ${message}
16074
16214
  `;
16075
- appendFileSync2(path, output);
16215
+ appendFileSync22(path, output);
16076
16216
  }
16077
16217
  function mergeSandboxIntoExtraArgs(extraArgs, sandbox) {
16078
16218
  const effectiveExtraArgs = { ...extraArgs };
@@ -16474,7 +16614,7 @@ class ProcessTransport {
16474
16614
  }
16475
16615
  return;
16476
16616
  }
16477
- return new Promise((resolve, reject) => {
16617
+ return new Promise((resolve2, reject) => {
16478
16618
  const exitHandler = (code, signal) => {
16479
16619
  if (this.abortController.signal.aborted) {
16480
16620
  reject(new AbortError("Operation aborted"));
@@ -16484,7 +16624,7 @@ class ProcessTransport {
16484
16624
  if (error) {
16485
16625
  reject(error);
16486
16626
  } else {
16487
- resolve();
16627
+ resolve2();
16488
16628
  }
16489
16629
  };
16490
16630
  this.process.once("exit", exitHandler);
@@ -16535,17 +16675,17 @@ class Stream {
16535
16675
  if (this.hasError) {
16536
16676
  return Promise.reject(this.hasError);
16537
16677
  }
16538
- return new Promise((resolve, reject) => {
16539
- this.readResolve = resolve;
16678
+ return new Promise((resolve2, reject) => {
16679
+ this.readResolve = resolve2;
16540
16680
  this.readReject = reject;
16541
16681
  });
16542
16682
  }
16543
16683
  enqueue(value) {
16544
16684
  if (this.readResolve) {
16545
- const resolve = this.readResolve;
16685
+ const resolve2 = this.readResolve;
16546
16686
  this.readResolve = undefined;
16547
16687
  this.readReject = undefined;
16548
- resolve({ done: false, value });
16688
+ resolve2({ done: false, value });
16549
16689
  } else {
16550
16690
  this.queue.push(value);
16551
16691
  }
@@ -16553,10 +16693,10 @@ class Stream {
16553
16693
  done() {
16554
16694
  this.isDone = true;
16555
16695
  if (this.readResolve) {
16556
- const resolve = this.readResolve;
16696
+ const resolve2 = this.readResolve;
16557
16697
  this.readResolve = undefined;
16558
16698
  this.readReject = undefined;
16559
- resolve({ done: true, value: undefined });
16699
+ resolve2({ done: true, value: undefined });
16560
16700
  }
16561
16701
  }
16562
16702
  error(error) {
@@ -16886,10 +17026,10 @@ class Query {
16886
17026
  type: "control_request",
16887
17027
  request
16888
17028
  };
16889
- return new Promise((resolve, reject) => {
17029
+ return new Promise((resolve2, reject) => {
16890
17030
  this.pendingControlResponses.set(requestId, (response) => {
16891
17031
  if (response.subtype === "success") {
16892
- resolve(response);
17032
+ resolve2(response);
16893
17033
  } else {
16894
17034
  reject(new Error(response.error));
16895
17035
  if (response.pending_permission_requests) {
@@ -16979,15 +17119,15 @@ class Query {
16979
17119
  logForDebugging(`[Query.waitForFirstResult] Result already received, returning immediately`);
16980
17120
  return Promise.resolve();
16981
17121
  }
16982
- return new Promise((resolve) => {
17122
+ return new Promise((resolve2) => {
16983
17123
  if (this.abortController?.signal.aborted) {
16984
- resolve();
17124
+ resolve2();
16985
17125
  return;
16986
17126
  }
16987
- this.abortController?.signal.addEventListener("abort", () => resolve(), {
17127
+ this.abortController?.signal.addEventListener("abort", () => resolve2(), {
16988
17128
  once: true
16989
17129
  });
16990
- this.firstResultReceivedResolve = resolve;
17130
+ this.firstResultReceivedResolve = resolve2;
16991
17131
  });
16992
17132
  }
16993
17133
  handleHookCallbacks(callbackId, input, toolUseID, abortSignal) {
@@ -17038,13 +17178,13 @@ class Query {
17038
17178
  handleMcpControlRequest(serverName, mcpRequest, transport) {
17039
17179
  const messageId = "id" in mcpRequest.message ? mcpRequest.message.id : null;
17040
17180
  const key = `${serverName}:${messageId}`;
17041
- return new Promise((resolve, reject) => {
17181
+ return new Promise((resolve2, reject) => {
17042
17182
  const cleanup = () => {
17043
17183
  this.pendingMcpResponses.delete(key);
17044
17184
  };
17045
17185
  const resolveAndCleanup = (response) => {
17046
17186
  cleanup();
17047
- resolve(response);
17187
+ resolve2(response);
17048
17188
  };
17049
17189
  const rejectAndCleanup = (error) => {
17050
17190
  cleanup();
@@ -25296,7 +25436,7 @@ var ServerResultSchema = union([
25296
25436
  var ignoreOverride = Symbol("Let zodToJsonSchema decide on which parser to use");
25297
25437
  var ALPHA_NUMERIC = new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789");
25298
25438
  var import_ajv = __toESM2(require_ajv(), 1);
25299
- var import_ajv_formats = __toESM2(require_dist(), 1);
25439
+ var import_ajv_formats = __toESM2(require_dist2(), 1);
25300
25440
  var COMPLETABLE_SYMBOL = Symbol.for("mcp.completable");
25301
25441
  var McpZodTypeKind;
25302
25442
  (function(McpZodTypeKind2) {
@@ -25452,6 +25592,78 @@ function query({
25452
25592
 
25453
25593
  // src/runner.ts
25454
25594
  import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
25595
+ import { execSync } from "child_process";
25596
+ import { createHash } from "crypto";
25597
+ var SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
25598
+
25599
+ class ProgressDisplay {
25600
+ interval = null;
25601
+ frame = 0;
25602
+ startTime = Date.now();
25603
+ currentActivity = "Working";
25604
+ lastToolUse = "";
25605
+ start(activity) {
25606
+ this.currentActivity = activity;
25607
+ this.startTime = Date.now();
25608
+ this.frame = 0;
25609
+ if (this.interval)
25610
+ return;
25611
+ this.interval = setInterval(() => {
25612
+ const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
25613
+ const toolInfo = this.lastToolUse ? ` → ${this.lastToolUse}` : "";
25614
+ process.stdout.write(`\r\x1B[K${SPINNER_FRAMES[this.frame]} ${this.currentActivity} (${elapsed}s)${toolInfo}`);
25615
+ this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
25616
+ }, 100);
25617
+ }
25618
+ updateActivity(activity) {
25619
+ this.currentActivity = activity;
25620
+ }
25621
+ updateTool(toolName, detail) {
25622
+ this.lastToolUse = detail ? `${toolName}: ${detail}` : toolName;
25623
+ }
25624
+ stop(finalMessage) {
25625
+ if (this.interval) {
25626
+ clearInterval(this.interval);
25627
+ this.interval = null;
25628
+ }
25629
+ process.stdout.write("\r\x1B[K");
25630
+ if (finalMessage) {
25631
+ console.log(finalMessage);
25632
+ }
25633
+ }
25634
+ getElapsed() {
25635
+ return (Date.now() - this.startTime) / 1000;
25636
+ }
25637
+ }
25638
+ var claudeExecutablePath;
25639
+ function findClaudeExecutable() {
25640
+ if (claudeExecutablePath !== undefined)
25641
+ return claudeExecutablePath;
25642
+ if (process.env.CLAUDE_CODE_PATH) {
25643
+ claudeExecutablePath = process.env.CLAUDE_CODE_PATH;
25644
+ return claudeExecutablePath;
25645
+ }
25646
+ try {
25647
+ const cmd = process.platform === "win32" ? "where claude" : "which claude";
25648
+ claudeExecutablePath = execSync(cmd, { encoding: "utf-8" }).trim().split(`
25649
+ `)[0];
25650
+ return claudeExecutablePath;
25651
+ } catch {
25652
+ const commonPaths = [
25653
+ "/usr/local/bin/claude",
25654
+ "/opt/homebrew/bin/claude",
25655
+ `${process.env.HOME}/.local/bin/claude`,
25656
+ `${process.env.HOME}/.nvm/versions/node/v22.12.0/bin/claude`
25657
+ ];
25658
+ for (const p of commonPaths) {
25659
+ if (existsSync3(p)) {
25660
+ claudeExecutablePath = p;
25661
+ return claudeExecutablePath;
25662
+ }
25663
+ }
25664
+ }
25665
+ return;
25666
+ }
25455
25667
  function isRetryableError(error2) {
25456
25668
  const errorStr = error2.message.toLowerCase();
25457
25669
  const retryablePatterns = [
@@ -25469,7 +25681,7 @@ function isRetryableError(error2) {
25469
25681
  return retryablePatterns.some((pattern) => errorStr.includes(pattern));
25470
25682
  }
25471
25683
  async function sleep(ms) {
25472
- return new Promise((resolve) => setTimeout(resolve, ms));
25684
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
25473
25685
  }
25474
25686
  async function runWithTimeout(promise, timeoutMs) {
25475
25687
  let timeoutId;
@@ -25485,19 +25697,71 @@ async function runWithTimeout(promise, timeoutMs) {
25485
25697
  throw e;
25486
25698
  }
25487
25699
  }
25488
- async function collectResult(prompt, options) {
25700
+ function getToolDetail(toolName, toolInput) {
25701
+ switch (toolName) {
25702
+ case "Read":
25703
+ case "Write":
25704
+ case "Edit":
25705
+ const filePath = toolInput.file_path;
25706
+ if (filePath) {
25707
+ return filePath.split("/").pop() || filePath;
25708
+ }
25709
+ break;
25710
+ case "Glob":
25711
+ return toolInput.pattern || "";
25712
+ case "Grep":
25713
+ return toolInput.pattern?.slice(0, 20) || "";
25714
+ case "Bash":
25715
+ const cmd = toolInput.command || "";
25716
+ return cmd.slice(0, 30) + (cmd.length > 30 ? "..." : "");
25717
+ }
25718
+ return "";
25719
+ }
25720
+ async function collectResultWithProgress(prompt, options, progress, onSessionId) {
25489
25721
  let sessionId;
25490
25722
  let result;
25491
- const conversation = query({ prompt, options });
25492
- for await (const message of conversation) {
25493
- if (message.type === "result") {
25494
- result = message.result;
25495
- sessionId = message.session_id;
25723
+ let lastError;
25724
+ try {
25725
+ const conversation = query({ prompt, options });
25726
+ for await (const message of conversation) {
25727
+ if (process.env.OVERNIGHT_DEBUG) {
25728
+ console.error(`
25729
+ [DEBUG] message.type=${message.type}, keys=${Object.keys(message).join(",")}`);
25730
+ }
25731
+ if (message.type === "result") {
25732
+ sessionId = message.session_id;
25733
+ if (message.subtype === "success") {
25734
+ result = message.result;
25735
+ }
25736
+ } else if (message.type === "assistant" && "message" in message) {
25737
+ const assistantMsg = message.message;
25738
+ if (assistantMsg.content) {
25739
+ for (const block of assistantMsg.content) {
25740
+ if (process.env.OVERNIGHT_DEBUG) {
25741
+ console.error(`[DEBUG] content block: type=${block.type}, name=${block.name}`);
25742
+ }
25743
+ if (block.type === "tool_use" && block.name) {
25744
+ const detail = block.input ? getToolDetail(block.name, block.input) : "";
25745
+ progress.updateTool(block.name, detail);
25746
+ }
25747
+ }
25748
+ }
25749
+ } else if (message.type === "system" && "subtype" in message) {
25750
+ if (message.subtype === "init") {
25751
+ sessionId = message.session_id;
25752
+ if (sessionId && onSessionId) {
25753
+ onSessionId(sessionId);
25754
+ }
25755
+ }
25756
+ }
25496
25757
  }
25758
+ } catch (e) {
25759
+ lastError = e.message;
25760
+ throw e;
25497
25761
  }
25498
- return { sessionId, result };
25762
+ return { sessionId, result, error: lastError };
25499
25763
  }
25500
- async function runJob(config2, log) {
25764
+ async function runJob(config2, log, options) {
25501
25765
  const startTime = Date.now();
25502
25766
  const tools = config2.allowed_tools ?? DEFAULT_TOOLS;
25503
25767
  const timeout = (config2.timeout_seconds ?? DEFAULT_TIMEOUT) * 1000;
@@ -25505,31 +25769,71 @@ async function runJob(config2, log) {
25505
25769
  const retryDelay = config2.retry_delay ?? DEFAULT_RETRY_DELAY;
25506
25770
  const verifyPrompt = config2.verify_prompt ?? DEFAULT_VERIFY_PROMPT;
25507
25771
  let retriesUsed = 0;
25772
+ let resumeSessionId = options?.resumeSessionId;
25508
25773
  const logMsg = (msg) => log?.(msg);
25509
- logMsg(`Starting: ${config2.prompt.slice(0, 60)}...`);
25774
+ const progress = new ProgressDisplay;
25775
+ const claudePath = findClaudeExecutable();
25776
+ if (!claudePath) {
25777
+ logMsg("\x1B[31m✗ Error: Could not find 'claude' CLI.\x1B[0m");
25778
+ logMsg("\x1B[33m Install it with:\x1B[0m");
25779
+ logMsg(" curl -fsSL https://claude.ai/install.sh | bash");
25780
+ logMsg("\x1B[33m Or set CLAUDE_CODE_PATH environment variable.\x1B[0m");
25781
+ return {
25782
+ task: config2.prompt,
25783
+ status: "failed",
25784
+ error: "Claude CLI not found. Install with: curl -fsSL https://claude.ai/install.sh | bash",
25785
+ duration_seconds: 0,
25786
+ verified: false,
25787
+ retries: 0
25788
+ };
25789
+ }
25790
+ if (process.env.OVERNIGHT_DEBUG) {
25791
+ logMsg(`\x1B[2mDebug: Claude path = ${claudePath}\x1B[0m`);
25792
+ }
25793
+ const taskPreview = config2.prompt.slice(0, 60) + (config2.prompt.length > 60 ? "..." : "");
25794
+ if (resumeSessionId) {
25795
+ logMsg(`\x1B[36m▶\x1B[0m Resuming: ${taskPreview}`);
25796
+ } else {
25797
+ logMsg(`\x1B[36m▶\x1B[0m ${taskPreview}`);
25798
+ }
25799
+ let sessionId;
25510
25800
  for (let attempt = 0;attempt <= retryCount; attempt++) {
25511
25801
  try {
25512
- const options = {
25802
+ const securityHooks = config2.security ? createSecurityHooks(config2.security) : undefined;
25803
+ const sdkOptions = {
25513
25804
  allowedTools: tools,
25514
25805
  permissionMode: "acceptEdits",
25515
- ...config2.working_dir && { cwd: config2.working_dir }
25806
+ ...claudePath && { pathToClaudeCodeExecutable: claudePath },
25807
+ ...config2.working_dir && { cwd: config2.working_dir },
25808
+ ...config2.security?.max_turns && { maxTurns: config2.security.max_turns },
25809
+ ...securityHooks && { hooks: securityHooks },
25810
+ ...resumeSessionId && { resume: resumeSessionId }
25516
25811
  };
25517
- let sessionId;
25518
25812
  let result;
25813
+ const prompt = resumeSessionId ? "Continue where you left off. Complete the original task." : config2.prompt;
25814
+ progress.start(resumeSessionId ? "Resuming" : "Working");
25519
25815
  try {
25520
- const collected = await runWithTimeout(collectResult(config2.prompt, options), timeout);
25816
+ const collected = await runWithTimeout(collectResultWithProgress(prompt, sdkOptions, progress, (id) => {
25817
+ sessionId = id;
25818
+ options?.onSessionId?.(id);
25819
+ }), timeout);
25521
25820
  sessionId = collected.sessionId;
25522
25821
  result = collected.result;
25822
+ progress.stop();
25523
25823
  } catch (e) {
25824
+ progress.stop();
25524
25825
  if (e.message === "TIMEOUT") {
25525
25826
  if (attempt < retryCount) {
25526
25827
  retriesUsed = attempt + 1;
25828
+ if (sessionId) {
25829
+ resumeSessionId = sessionId;
25830
+ }
25527
25831
  const delay = retryDelay * Math.pow(2, attempt);
25528
- logMsg(`Timeout after ${config2.timeout_seconds ?? DEFAULT_TIMEOUT}s, retrying in ${delay}s (attempt ${attempt + 1}/${retryCount})...`);
25832
+ logMsg(`\x1B[33m⚠ Timeout after ${config2.timeout_seconds ?? DEFAULT_TIMEOUT}s, retrying in ${delay}s (${attempt + 1}/${retryCount})\x1B[0m`);
25529
25833
  await sleep(delay * 1000);
25530
25834
  continue;
25531
25835
  }
25532
- logMsg(`Timeout after ${config2.timeout_seconds ?? DEFAULT_TIMEOUT}s (exhausted retries)`);
25836
+ logMsg(`\x1B[31m✗ Timeout after ${config2.timeout_seconds ?? DEFAULT_TIMEOUT}s (exhausted retries)\x1B[0m`);
25533
25837
  return {
25534
25838
  task: config2.prompt,
25535
25839
  status: "timeout",
@@ -25542,36 +25846,49 @@ async function runJob(config2, log) {
25542
25846
  throw e;
25543
25847
  }
25544
25848
  if (config2.verify !== false && sessionId) {
25545
- logMsg("Running verification...");
25849
+ progress.start("Verifying");
25546
25850
  const verifyOptions = {
25851
+ allowedTools: tools,
25547
25852
  resume: sessionId,
25548
- permissionMode: "acceptEdits"
25853
+ permissionMode: "acceptEdits",
25854
+ ...claudePath && { pathToClaudeCodeExecutable: claudePath },
25855
+ ...config2.working_dir && { cwd: config2.working_dir },
25856
+ ...config2.security?.max_turns && { maxTurns: config2.security.max_turns }
25549
25857
  };
25858
+ const fixPrompt = verifyPrompt + " If you find any issues, fix them now. Only report issues you cannot fix.";
25550
25859
  try {
25551
- const verifyResult = await runWithTimeout(collectResult(verifyPrompt, verifyOptions), timeout / 2);
25552
- const issueWords = ["issue", "error", "fail", "incorrect", "missing"];
25553
- if (verifyResult.result && issueWords.some((word) => verifyResult.result.toLowerCase().includes(word))) {
25554
- logMsg("Verification found potential issues");
25860
+ const verifyResult = await runWithTimeout(collectResultWithProgress(fixPrompt, verifyOptions, progress, (id) => {
25861
+ sessionId = id;
25862
+ options?.onSessionId?.(id);
25863
+ }), timeout / 2);
25864
+ progress.stop();
25865
+ if (verifyResult.result) {
25866
+ result = verifyResult.result;
25867
+ }
25868
+ const unfixableWords = ["cannot fix", "unable to", "blocked by", "requires manual"];
25869
+ if (verifyResult.result && unfixableWords.some((word) => verifyResult.result.toLowerCase().includes(word))) {
25870
+ logMsg(`\x1B[33m⚠ Verification found unfixable issues\x1B[0m`);
25555
25871
  return {
25556
25872
  task: config2.prompt,
25557
25873
  status: "verification_failed",
25558
25874
  result,
25559
- error: `Verification issues: ${verifyResult.result}`,
25875
+ error: `Unfixable issues: ${verifyResult.result}`,
25560
25876
  duration_seconds: (Date.now() - startTime) / 1000,
25561
25877
  verified: false,
25562
25878
  retries: retriesUsed
25563
25879
  };
25564
25880
  }
25565
25881
  } catch (e) {
25882
+ progress.stop();
25566
25883
  if (e.message === "TIMEOUT") {
25567
- logMsg("Verification timed out - continuing anyway");
25884
+ logMsg("\x1B[33m⚠ Verification timed out - continuing anyway\x1B[0m");
25568
25885
  } else {
25569
25886
  throw e;
25570
25887
  }
25571
25888
  }
25572
25889
  }
25573
25890
  const duration3 = (Date.now() - startTime) / 1000;
25574
- logMsg(`Completed in ${duration3.toFixed(1)}s`);
25891
+ logMsg(`\x1B[32m✓ Completed in ${duration3.toFixed(1)}s\x1B[0m`);
25575
25892
  return {
25576
25893
  task: config2.prompt,
25577
25894
  status: "success",
@@ -25581,16 +25898,20 @@ async function runJob(config2, log) {
25581
25898
  retries: retriesUsed
25582
25899
  };
25583
25900
  } catch (e) {
25901
+ progress.stop();
25584
25902
  const error2 = e;
25585
25903
  if (isRetryableError(error2) && attempt < retryCount) {
25586
25904
  retriesUsed = attempt + 1;
25905
+ if (sessionId) {
25906
+ resumeSessionId = sessionId;
25907
+ }
25587
25908
  const delay = retryDelay * Math.pow(2, attempt);
25588
- logMsg(`Retryable error: ${error2.message}, retrying in ${delay}s (attempt ${attempt + 1}/${retryCount})...`);
25909
+ logMsg(`\x1B[33m⚠ ${error2.message}, retrying in ${delay}s (${attempt + 1}/${retryCount})\x1B[0m`);
25589
25910
  await sleep(delay * 1000);
25590
25911
  continue;
25591
25912
  }
25592
25913
  const duration3 = (Date.now() - startTime) / 1000;
25593
- logMsg(`Failed: ${error2.message}`);
25914
+ logMsg(`\x1B[31m✗ Failed: ${error2.message}\x1B[0m`);
25594
25915
  return {
25595
25916
  task: config2.prompt,
25596
25917
  status: "failed",
@@ -25610,6 +25931,56 @@ async function runJob(config2, log) {
25610
25931
  retries: retriesUsed
25611
25932
  };
25612
25933
  }
25934
+ function taskKey(config2) {
25935
+ if (config2.id)
25936
+ return config2.id;
25937
+ return createHash("sha256").update(config2.prompt).digest("hex").slice(0, 12);
25938
+ }
25939
+ function validateDag(configs) {
25940
+ const ids = new Set(configs.map((c) => c.id).filter((id) => Boolean(id)));
25941
+ for (const c of configs) {
25942
+ for (const dep of c.depends_on ?? []) {
25943
+ if (!ids.has(dep)) {
25944
+ return `Task "${c.id ?? c.prompt.slice(0, 40)}" depends on unknown id "${dep}"`;
25945
+ }
25946
+ }
25947
+ }
25948
+ const visited = new Set;
25949
+ const inStack = new Set;
25950
+ const idToConfig = new Map(configs.filter((c) => c.id).map((c) => [c.id, c]));
25951
+ function hasCycle(id) {
25952
+ if (inStack.has(id))
25953
+ return true;
25954
+ if (visited.has(id))
25955
+ return false;
25956
+ visited.add(id);
25957
+ inStack.add(id);
25958
+ const config2 = idToConfig.get(id);
25959
+ for (const dep of config2?.depends_on ?? []) {
25960
+ if (hasCycle(dep))
25961
+ return true;
25962
+ }
25963
+ inStack.delete(id);
25964
+ return false;
25965
+ }
25966
+ for (const id of ids) {
25967
+ if (hasCycle(id))
25968
+ return `Dependency cycle detected involving "${id}"`;
25969
+ }
25970
+ return null;
25971
+ }
25972
+ function depsReady(config2, completed) {
25973
+ if (!config2.depends_on || config2.depends_on.length === 0)
25974
+ return "ready";
25975
+ for (const dep of config2.depends_on) {
25976
+ const result = completed[dep];
25977
+ if (!result)
25978
+ return "waiting";
25979
+ if (result.status !== "success")
25980
+ return "blocked";
25981
+ }
25982
+ return "ready";
25983
+ }
25613
25984
  function saveState(state, stateFile) {
25614
25985
  writeFileSync(stateFile, JSON.stringify(state, null, 2));
25615
25986
  }
@@ -25624,26 +25995,84 @@ function clearState(stateFile) {
25624
25995
  }
25625
25996
  async function runJobsWithState(configs, options = {}) {
25626
25997
  const stateFile = options.stateFile ?? DEFAULT_STATE_FILE;
25627
- const results = options.priorResults ? [...options.priorResults] : [];
25628
- const startIndex = options.startIndex ?? 0;
25629
- for (let i = 0;i < configs.length; i++) {
25630
- if (i < startIndex)
25631
- continue;
25998
+ const dagError = validateDag(configs);
25999
+ if (dagError) {
26000
+ options.log?.(`\x1B[31m✗ DAG error: ${dagError}\x1B[0m`);
26001
+ return [];
26002
+ }
26003
+ const state = loadState(stateFile) ?? {
26004
+ completed: {},
26005
+ timestamp: new Date().toISOString()
26006
+ };
26007
+ let currentConfigs = configs;
26008
+ while (true) {
26009
+ const notDone = currentConfigs.filter((c) => !(taskKey(c) in state.completed));
26010
+ if (notDone.length === 0)
26011
+ break;
26012
+ const ready = notDone.filter((c) => depsReady(c, state.completed) === "ready");
26013
+ const blocked = notDone.filter((c) => depsReady(c, state.completed) === "blocked");
26014
+ for (const bc of blocked) {
26015
+ const key2 = taskKey(bc);
26016
+ if (key2 in state.completed)
26017
+ continue;
26018
+ const failedDeps = (bc.depends_on ?? []).filter((dep) => state.completed[dep] && state.completed[dep].status !== "success");
26019
+ const label2 = bc.id ?? bc.prompt.slice(0, 40);
26020
+ options.log?.(`
26021
+ \x1B[31m✗ Skipping "${label2}" — dependency failed: ${failedDeps.join(", ")}\x1B[0m`);
26022
+ state.completed[key2] = {
26023
+ task: bc.prompt,
26024
+ status: "failed",
26025
+ error: `Blocked by failed dependencies: ${failedDeps.join(", ")}`,
26026
+ duration_seconds: 0,
26027
+ verified: false,
26028
+ retries: 0
26029
+ };
26030
+ state.timestamp = new Date().toISOString();
26031
+ saveState(state, stateFile);
26032
+ }
26033
+ if (ready.length === 0)
26034
+ break;
26035
+ const config2 = ready[0];
26036
+ const key = taskKey(config2);
26037
+ const totalNotDone = notDone.length - blocked.length;
26038
+ const totalDone = Object.keys(state.completed).length;
26039
+ const label = config2.id ? `${config2.id}` : "";
25632
26040
  options.log?.(`
25633
- [${i + 1}/${configs.length}] Running job...`);
25634
- const result = await runJob(configs[i], options.log);
25635
- results.push(result);
25636
- const state = {
25637
- completed_indices: Array.from({ length: results.length }, (_, i2) => i2),
25638
- results,
25639
- timestamp: new Date().toISOString(),
25640
- total_jobs: configs.length
25641
- };
26041
+ \x1B[1m[${totalDone + 1}/${totalDone + totalNotDone}]${label ? ` ${label}` : ""}\x1B[0m`);
26042
+ const resumeSessionId = state.inProgress?.hash === key ? state.inProgress.sessionId : undefined;
26043
+ if (resumeSessionId) {
26044
+ options.log?.(`\x1B[2mResuming session ${resumeSessionId.slice(0, 8)}...\x1B[0m`);
26045
+ }
26046
+ state.inProgress = { hash: key, prompt: config2.prompt, startedAt: new Date().toISOString() };
26047
+ saveState(state, stateFile);
26048
+ const result = await runJob(config2, options.log, {
26049
+ resumeSessionId,
26050
+ onSessionId: (id) => {
26051
+ state.inProgress = { hash: key, prompt: config2.prompt, sessionId: id, startedAt: state.inProgress.startedAt };
26052
+ saveState(state, stateFile);
26053
+ }
26054
+ });
26055
+ state.completed[key] = result;
26056
+ state.inProgress = undefined;
26057
+ state.timestamp = new Date().toISOString();
25642
26058
  saveState(state, stateFile);
25643
- if (i < configs.length - 1) {
26059
+ if (options.reloadConfigs) {
26060
+ try {
26061
+ currentConfigs = options.reloadConfigs();
26062
+ const newDagError = validateDag(currentConfigs);
26063
+ if (newDagError) {
26064
+ options.log?.(`\x1B[33m⚠ DAG error in updated YAML, ignoring reload: ${newDagError}\x1B[0m`);
26065
+ currentConfigs = configs;
26066
+ }
26067
+ } catch {}
26068
+ }
26069
+ const nextNotDone = currentConfigs.filter((c) => !(taskKey(c) in state.completed));
26070
+ const nextReady = nextNotDone.filter((c) => depsReady(c, state.completed) === "ready");
26071
+ if (nextReady.length > 0) {
25644
26072
  await sleep(1000);
25645
26073
  }
25646
26074
  }
26075
+ const results = currentConfigs.map((c) => state.completed[taskKey(c)]).filter((r) => r !== undefined);
25647
26076
  clearState(stateFile);
25648
26077
  return results;
25649
26078
  }
@@ -25778,55 +26207,860 @@ function generateReport(results, totalDuration, outputPath) {
25778
26207
  return content;
25779
26208
  }
25780
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
+
25781
26997
  // src/cli.ts
25782
26998
  var AGENT_HELP = `
25783
- # overnight - Batch Job Runner for Claude Code
26999
+ # overnight - Autonomous Build Runner for Claude Code
25784
27000
 
25785
- Queue tasks, run them unattended, get results. Designed for overnight/AFK use.
27001
+ Two modes: goal-driven autonomous loops, or task-list batch jobs.
25786
27002
 
25787
27003
  ## Quick Start
25788
27004
 
25789
27005
  \`\`\`bash
25790
- # Create a tasks.yaml file
25791
- overnight init
27006
+ # Hammer mode: just give it a goal and go
27007
+ overnight hammer "Build a multiplayer MMO"
25792
27008
 
25793
- # Run all tasks
25794
- 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
25795
27012
 
25796
- # Run with notifications and report
25797
- overnight run tasks.yaml --notify -r report.md
27013
+ # Task mode: explicit task list
27014
+ overnight run tasks.yaml --notify
25798
27015
  \`\`\`
25799
27016
 
25800
27017
  ## Commands
25801
27018
 
25802
27019
  | Command | Description |
25803
27020
  |---------|-------------|
25804
- | \`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) |
25805
27024
  | \`overnight resume <file>\` | Resume interrupted run from checkpoint |
25806
27025
  | \`overnight single "<prompt>"\` | Run a single task directly |
25807
- | \`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"
27039
+
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)
25808
27051
 
25809
- ## tasks.yaml Format
27052
+ Explicit task list with optional dependency DAG.
25810
27053
 
25811
27054
  \`\`\`yaml
25812
27055
  defaults:
25813
- timeout_seconds: 300 # Per-task timeout (default: 300)
25814
- verify: true # Run verification pass (default: true)
25815
- allowed_tools: # Whitelist tools (default: Read,Edit,Write,Glob,Grep)
25816
- - Read
25817
- - Edit
25818
- - Glob
25819
- - Grep
27056
+ timeout_seconds: 300
27057
+ verify: true
27058
+ allowed_tools: [Read, Edit, Write, Glob, Grep]
25820
27059
 
25821
27060
  tasks:
25822
- # Simple format
25823
27061
  - "Fix the bug in auth.py"
25824
-
25825
- # Detailed format
25826
27062
  - prompt: "Add input validation"
25827
27063
  timeout_seconds: 600
25828
- verify: false
25829
- allowed_tools: [Read, Edit, Bash, Glob, Grep]
25830
27064
  \`\`\`
25831
27065
 
25832
27066
  ## Key Options
@@ -25836,53 +27070,71 @@ tasks:
25836
27070
  | \`-o, --output <file>\` | Save results JSON |
25837
27071
  | \`-r, --report <file>\` | Generate markdown report |
25838
27072
  | \`-s, --state-file <file>\` | Custom checkpoint file |
27073
+ | \`--max-iterations <n>\` | Max build loop iterations (goal mode) |
25839
27074
  | \`--notify\` | Send push notification via ntfy.sh |
25840
- | \`--notify-topic <topic>\` | ntfy.sh topic (default: overnight) |
25841
27075
  | \`-q, --quiet\` | Minimal output |
25842
27076
 
25843
- ## Features
25844
-
25845
- 1. **Crash Recovery**: Auto-checkpoints after each job. Use \`overnight resume\` to continue.
25846
- 2. **Retry Logic**: Auto-retries 3x on API/network errors with exponential backoff.
25847
- 3. **Notifications**: \`--notify\` sends summary to ntfy.sh (free, no signup).
25848
- 4. **Reports**: \`-r report.md\` generates markdown summary with next steps.
25849
- 5. **Security**: No Bash by default. Whitelist tools per-task.
25850
-
25851
27077
  ## Example Workflows
25852
27078
 
25853
27079
  \`\`\`bash
25854
- # Development: run overnight, check in morning
25855
- 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 &
25856
27082
 
25857
- # CI/CD: run and fail if any task fails
25858
- 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 &
25859
27086
 
25860
- # Single task with Bash access
25861
- 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 &
25862
27089
 
25863
- # Resume after crash/interrupt
25864
- overnight resume tasks.yaml
27090
+ # Resume after crash
27091
+ overnight resume goal.yaml
25865
27092
  \`\`\`
25866
27093
 
25867
27094
  ## Exit Codes
25868
27095
 
25869
- - 0: All tasks succeeded
25870
- - 1: One or more tasks failed
27096
+ - 0: All tasks succeeded / gate passed
27097
+ - 1: Failures occurred / gate failed
25871
27098
 
25872
27099
  ## Files Created
25873
27100
 
25874
- - \`.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
25875
27104
  - \`report.md\` - Summary report (if -r used)
25876
- - \`results.json\` - Full results (if -o used)
25877
27105
 
25878
27106
  Run \`overnight <command> --help\` for command-specific options.
25879
27107
  `;
25880
- function parseTasksFile(path) {
25881
- const content = readFileSync3(path, "utf-8");
25882
- const data = $parse(content);
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
+ }
27117
+ function parseTasksFile(path, cliSecurity) {
27118
+ const content = readFileSync5(path, "utf-8");
27119
+ let data;
27120
+ try {
27121
+ data = import_yaml2.parse(content);
27122
+ } catch (e) {
27123
+ const error2 = e;
27124
+ console.error(`\x1B[31mError parsing ${path}:\x1B[0m`);
27125
+ console.error(` ${error2.message.split(`
27126
+ `)[0]}`);
27127
+ process.exit(1);
27128
+ }
25883
27129
  const tasks = Array.isArray(data) ? data : data.tasks ?? [];
25884
27130
  const defaults = Array.isArray(data) ? {} : data.defaults ?? {};
25885
- return tasks.map((task) => {
27131
+ const fileSecurity = !Array.isArray(data) && data.defaults?.security || {};
27132
+ const security = cliSecurity || Object.keys(fileSecurity).length > 0 ? {
27133
+ ...fileSecurity,
27134
+ ...cliSecurity,
27135
+ deny_patterns: cliSecurity?.deny_patterns ?? fileSecurity.deny_patterns ?? DEFAULT_DENY_PATTERNS
27136
+ } : undefined;
27137
+ const configs = tasks.map((task) => {
25886
27138
  if (typeof task === "string") {
25887
27139
  return {
25888
27140
  prompt: task,
@@ -25890,19 +27142,24 @@ function parseTasksFile(path) {
25890
27142
  stall_timeout_seconds: defaults.stall_timeout_seconds ?? DEFAULT_STALL_TIMEOUT,
25891
27143
  verify: defaults.verify ?? true,
25892
27144
  verify_prompt: defaults.verify_prompt ?? DEFAULT_VERIFY_PROMPT,
25893
- allowed_tools: defaults.allowed_tools
27145
+ allowed_tools: defaults.allowed_tools,
27146
+ security
25894
27147
  };
25895
27148
  }
25896
27149
  return {
27150
+ id: task.id ?? undefined,
27151
+ depends_on: task.depends_on ?? undefined,
25897
27152
  prompt: task.prompt,
25898
27153
  working_dir: task.working_dir ?? undefined,
25899
27154
  timeout_seconds: task.timeout_seconds ?? defaults.timeout_seconds ?? DEFAULT_TIMEOUT,
25900
27155
  stall_timeout_seconds: task.stall_timeout_seconds ?? defaults.stall_timeout_seconds ?? DEFAULT_STALL_TIMEOUT,
25901
27156
  verify: task.verify ?? defaults.verify ?? true,
25902
27157
  verify_prompt: task.verify_prompt ?? defaults.verify_prompt ?? DEFAULT_VERIFY_PROMPT,
25903
- allowed_tools: task.allowed_tools ?? defaults.allowed_tools
27158
+ allowed_tools: task.allowed_tools ?? defaults.allowed_tools,
27159
+ security: task.security ?? security
25904
27160
  };
25905
27161
  });
27162
+ return { configs, security };
25906
27163
  }
25907
27164
  function printSummary(results) {
25908
27165
  const statusColors = {
@@ -25928,53 +27185,149 @@ ${bold}Job Results${reset}`);
25928
27185
  ${bold}Summary:${reset} ${succeeded}/${results.length} succeeded`);
25929
27186
  }
25930
27187
  var program2 = new Command;
25931
- program2.name("overnight").description("Batch job runner for Claude Code").version("0.1.0").action(() => {
27188
+ program2.name("overnight").description("Batch job runner for Claude Code").version("0.3.0").action(() => {
25932
27189
  console.log(AGENT_HELP);
25933
27190
  });
25934
- 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").action(async (tasksFile, opts) => {
25935
- if (!existsSync5(tasksFile)) {
25936
- console.error(`Error: File not found: ${tasksFile}`);
25937
- process.exit(1);
25938
- }
25939
- const configs = parseTasksFile(tasksFile);
25940
- if (configs.length === 0) {
25941
- 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}`);
25942
27194
  process.exit(1);
25943
27195
  }
25944
- console.log(`\x1B[1movernight: Running ${configs.length} jobs...\x1B[0m
25945
- `);
25946
- const log = opts.quiet ? undefined : (msg) => console.log(msg);
25947
- const startTime = Date.now();
25948
- const results = await runJobsWithState(configs, {
25949
- stateFile: opts.stateFile,
25950
- log
25951
- });
25952
- const totalDuration = (Date.now() - startTime) / 1000;
25953
- if (opts.notify) {
25954
- const success = await sendNtfyNotification(results, totalDuration, opts.notifyTopic);
25955
- if (success) {
25956
- console.log(`\x1B[2mNotification sent to ntfy.sh/${opts.notifyTopic}\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
+ }
27271
+ } else {
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`);
25957
27288
  } else {
25958
- 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
+ }
25959
27312
  }
25960
- }
25961
- if (opts.report) {
25962
- generateReport(results, totalDuration, opts.report);
25963
- console.log(`\x1B[2mReport saved to ${opts.report}\x1B[0m`);
25964
- }
25965
- if (!opts.quiet) {
25966
- printSummary(results);
25967
- }
25968
- if (opts.output) {
25969
- writeFileSync3(opts.output, resultsToJson(results));
25970
- 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(`
25971
27323
  \x1B[2mResults saved to ${opts.output}\x1B[0m`);
25972
- }
25973
- if (results.some((r) => r.status !== "success")) {
25974
- process.exit(1);
27324
+ }
27325
+ if (results.some((r) => r.status !== "success")) {
27326
+ process.exit(1);
27327
+ }
25975
27328
  }
25976
27329
  });
25977
- 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").action(async (tasksFile, opts) => {
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) => {
25978
27331
  const stateFile = opts.stateFile ?? DEFAULT_STATE_FILE;
25979
27332
  const state = loadState(stateFile);
25980
27333
  if (!state) {
@@ -25982,30 +27335,36 @@ program2.command("resume").description("Resume a previous run from saved state")
25982
27335
  console.error("Run 'overnight run' first to start jobs.");
25983
27336
  process.exit(1);
25984
27337
  }
25985
- if (!existsSync5(tasksFile)) {
27338
+ if (!existsSync7(tasksFile)) {
25986
27339
  console.error(`Error: File not found: ${tasksFile}`);
25987
27340
  process.exit(1);
25988
27341
  }
25989
- const configs = parseTasksFile(tasksFile);
27342
+ const cliSecurity = opts.security === false ? undefined : {
27343
+ ...opts.sandbox && { sandbox_dir: opts.sandbox },
27344
+ ...opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) },
27345
+ ...opts.auditLog && { audit_log: opts.auditLog }
27346
+ };
27347
+ const { configs, security } = parseTasksFile(tasksFile, cliSecurity);
25990
27348
  if (configs.length === 0) {
25991
27349
  console.error("No tasks found in file");
25992
27350
  process.exit(1);
25993
27351
  }
25994
- if (configs.length !== state.total_jobs) {
25995
- console.error(`Task file has ${configs.length} jobs but state has ${state.total_jobs}`);
25996
- process.exit(1);
27352
+ const completedCount = Object.keys(state.completed).length;
27353
+ const pendingCount = configs.filter((c) => !(taskKey(c) in state.completed)).length;
27354
+ console.log(`\x1B[1movernight: Resuming — ${completedCount} done, ${pendingCount} remaining\x1B[0m`);
27355
+ console.log(`\x1B[2mLast checkpoint: ${state.timestamp}\x1B[0m`);
27356
+ if (security && !opts.quiet) {
27357
+ console.log("\x1B[2mSecurity:\x1B[0m");
27358
+ validateSecurityConfig(security);
25997
27359
  }
25998
- const startIndex = state.completed_indices.length;
25999
- console.log(`\x1B[1movernight: Resuming from job ${startIndex + 1}/${configs.length}...\x1B[0m`);
26000
- console.log(`\x1B[2mLast checkpoint: ${state.timestamp}\x1B[0m
26001
- `);
27360
+ console.log("");
26002
27361
  const log = opts.quiet ? undefined : (msg) => console.log(msg);
26003
27362
  const startTime = Date.now();
27363
+ const reloadConfigs = () => parseTasksFile(tasksFile, cliSecurity).configs;
26004
27364
  const results = await runJobsWithState(configs, {
26005
27365
  stateFile,
26006
27366
  log,
26007
- startIndex,
26008
- priorResults: state.results
27367
+ reloadConfigs
26009
27368
  });
26010
27369
  const totalDuration = (Date.now() - startTime) / 1000;
26011
27370
  if (opts.notify) {
@@ -26024,7 +27383,7 @@ program2.command("resume").description("Resume a previous run from saved state")
26024
27383
  printSummary(results);
26025
27384
  }
26026
27385
  if (opts.output) {
26027
- writeFileSync3(opts.output, resultsToJson(results));
27386
+ writeFileSync5(opts.output, resultsToJson(results));
26028
27387
  console.log(`
26029
27388
  \x1B[2mResults saved to ${opts.output}\x1B[0m`);
26030
27389
  }
@@ -26032,12 +27391,18 @@ program2.command("resume").description("Resume a previous run from saved state")
26032
27391
  process.exit(1);
26033
27392
  }
26034
27393
  });
26035
- program2.command("single").description("Run a single job directly").argument("<prompt>", "The task prompt").option("-t, --timeout <seconds>", "Timeout in seconds", "300").option("--verify", "Run verification pass", true).option("--no-verify", "Skip verification pass").option("-T, --tools <tool...>", "Allowed tools").action(async (prompt, opts) => {
27394
+ program2.command("single").description("Run a single job directly").argument("<prompt>", "The task prompt").option("-t, --timeout <seconds>", "Timeout in seconds", "300").option("--verify", "Run verification pass", true).option("--no-verify", "Skip verification pass").option("-T, --tools <tool...>", "Allowed tools").option("--sandbox <dir>", "Sandbox directory (restrict file access)").option("--max-turns <n>", "Max agent iterations", String(DEFAULT_MAX_TURNS)).option("--no-security", "Disable default security (deny patterns)").action(async (prompt, opts) => {
27395
+ const security = opts.security === false ? undefined : {
27396
+ ...opts.sandbox && { sandbox_dir: opts.sandbox },
27397
+ ...opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) },
27398
+ deny_patterns: DEFAULT_DENY_PATTERNS
27399
+ };
26036
27400
  const config2 = {
26037
27401
  prompt,
26038
27402
  timeout_seconds: parseInt(opts.timeout, 10),
26039
27403
  verify: opts.verify,
26040
- allowed_tools: opts.tools
27404
+ allowed_tools: opts.tools,
27405
+ security
26041
27406
  };
26042
27407
  const log = (msg) => console.log(msg);
26043
27408
  const result = await runJob(config2, log);
@@ -26056,13 +27421,97 @@ program2.command("single").description("Run a single job directly").argument("<p
26056
27421
  process.exit(1);
26057
27422
  }
26058
27423
  });
26059
- program2.command("init").description("Create an example tasks.yaml file").action(() => {
26060
- 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
26061
27509
  # Run with: overnight run tasks.yaml
26062
27510
 
26063
27511
  defaults:
26064
27512
  timeout_seconds: 300 # 5 minutes per task
26065
27513
  verify: true # Run verification after each task
27514
+
26066
27515
  # Secure defaults - no Bash, just file operations
26067
27516
  allowed_tools:
26068
27517
  - Read
@@ -26071,6 +27520,12 @@ defaults:
26071
27520
  - Glob
26072
27521
  - Grep
26073
27522
 
27523
+ # Security settings (optional - deny_patterns enabled by default)
27524
+ security:
27525
+ sandbox_dir: "." # Restrict to current directory
27526
+ max_turns: 100 # Prevent runaway agents
27527
+ # audit_log: "overnight-audit.log" # Uncomment to enable
27528
+
26074
27529
  tasks:
26075
27530
  # Simple string format
26076
27531
  - "Find and fix any TODO comments in the codebase"
@@ -26091,12 +27546,62 @@ tasks:
26091
27546
  - Glob
26092
27547
  - Grep
26093
27548
  `;
26094
- if (existsSync5("tasks.yaml")) {
26095
- console.log("\x1B[33mtasks.yaml already exists\x1B[0m");
26096
- 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`);
26097
27605
  }
26098
- writeFileSync3("tasks.yaml", example);
26099
- console.log("\x1B[32mCreated tasks.yaml\x1B[0m");
26100
- console.log("Edit the file, then run: \x1B[1movernight run tasks.yaml\x1B[0m");
26101
27606
  });
26102
27607
  program2.parse();