@yail259/overnight 0.1.0 → 0.2.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
@@ -8831,9 +8831,142 @@ var DEFAULT_TIMEOUT = 300;
8831
8831
  var DEFAULT_STALL_TIMEOUT = 120;
8832
8832
  var DEFAULT_RETRY_COUNT = 3;
8833
8833
  var DEFAULT_RETRY_DELAY = 5;
8834
- var DEFAULT_VERIFY_PROMPT = "Verify this is complete and correct. If there are issues, list them.";
8834
+ var DEFAULT_VERIFY_PROMPT = "Review what you just implemented. Check for correctness, completeness, and compile errors. Fix any issues you find.";
8835
8835
  var DEFAULT_STATE_FILE = ".overnight-state.json";
8836
8836
  var DEFAULT_NTFY_TOPIC = "overnight";
8837
+ var DEFAULT_MAX_TURNS = 100;
8838
+ var DEFAULT_DENY_PATTERNS = [
8839
+ "**/.env",
8840
+ "**/.env.*",
8841
+ "**/.git/config",
8842
+ "**/credentials*",
8843
+ "**/*.key",
8844
+ "**/*.pem",
8845
+ "**/*.p12",
8846
+ "**/id_rsa*",
8847
+ "**/id_ed25519*",
8848
+ "**/.ssh/*",
8849
+ "**/.aws/*",
8850
+ "**/.npmrc",
8851
+ "**/.netrc"
8852
+ ];
8853
+
8854
+ // src/security.ts
8855
+ import { appendFileSync } from "fs";
8856
+ import { resolve, relative, isAbsolute } from "path";
8857
+ function matchesPattern(filePath, pattern) {
8858
+ const normalizedPath = filePath.replace(/\\/g, "/");
8859
+ let regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/{{GLOBSTAR}}/g, ".*");
8860
+ if (!pattern.startsWith("/")) {
8861
+ regex = `(^|/)${regex}`;
8862
+ }
8863
+ return new RegExp(regex + "$").test(normalizedPath);
8864
+ }
8865
+ function isPathWithinSandbox(filePath, sandboxDir) {
8866
+ const absolutePath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
8867
+ const absoluteSandbox = isAbsolute(sandboxDir) ? sandboxDir : resolve(process.cwd(), sandboxDir);
8868
+ const relativePath = relative(absoluteSandbox, absolutePath);
8869
+ return !relativePath.startsWith("..") && !isAbsolute(relativePath);
8870
+ }
8871
+ function isPathDenied(filePath, denyPatterns) {
8872
+ for (const pattern of denyPatterns) {
8873
+ if (matchesPattern(filePath, pattern)) {
8874
+ return pattern;
8875
+ }
8876
+ }
8877
+ return null;
8878
+ }
8879
+ function createSecurityHooks(config) {
8880
+ const sandboxDir = config.sandbox_dir;
8881
+ const denyPatterns = config.deny_patterns ?? DEFAULT_DENY_PATTERNS;
8882
+ const auditLog = config.audit_log;
8883
+ const preToolUseHook = async (input, _toolUseId, _context) => {
8884
+ const hookEventName = input.hook_event_name;
8885
+ if (hookEventName !== "PreToolUse")
8886
+ return {};
8887
+ const toolName = input.tool_name;
8888
+ const toolInput = input.tool_input;
8889
+ let filePath;
8890
+ if (toolName === "Read" || toolName === "Write" || toolName === "Edit") {
8891
+ filePath = toolInput.file_path;
8892
+ } else if (toolName === "Glob" || toolName === "Grep") {
8893
+ filePath = toolInput.path;
8894
+ } else if (toolName === "Bash") {
8895
+ const command = toolInput.command;
8896
+ if (auditLog) {
8897
+ const timestamp = new Date().toISOString();
8898
+ appendFileSync(auditLog, `${timestamp} [BASH] ${command}
8899
+ `);
8900
+ }
8901
+ return {};
8902
+ }
8903
+ if (!filePath)
8904
+ return {};
8905
+ if (sandboxDir && !isPathWithinSandbox(filePath, sandboxDir)) {
8906
+ return {
8907
+ hookSpecificOutput: {
8908
+ hookEventName,
8909
+ permissionDecision: "deny",
8910
+ permissionDecisionReason: `Path "${filePath}" is outside sandbox directory "${sandboxDir}"`
8911
+ }
8912
+ };
8913
+ }
8914
+ const matchedPattern = isPathDenied(filePath, denyPatterns);
8915
+ if (matchedPattern) {
8916
+ return {
8917
+ hookSpecificOutput: {
8918
+ hookEventName,
8919
+ permissionDecision: "deny",
8920
+ permissionDecisionReason: `Path "${filePath}" matches deny pattern "${matchedPattern}"`
8921
+ }
8922
+ };
8923
+ }
8924
+ return {};
8925
+ };
8926
+ const postToolUseHook = async (input, _toolUseId, _context) => {
8927
+ if (!auditLog)
8928
+ return {};
8929
+ const hookEventName = input.hook_event_name;
8930
+ if (hookEventName !== "PostToolUse")
8931
+ return {};
8932
+ const toolName = input.tool_name;
8933
+ const toolInput = input.tool_input;
8934
+ const timestamp = new Date().toISOString();
8935
+ let logEntry = `${timestamp} [${toolName}]`;
8936
+ if (toolName === "Read" || toolName === "Write" || toolName === "Edit") {
8937
+ logEntry += ` ${toolInput.file_path}`;
8938
+ } else if (toolName === "Glob") {
8939
+ logEntry += ` pattern=${toolInput.pattern} path=${toolInput.path ?? "."}`;
8940
+ } else if (toolName === "Grep") {
8941
+ logEntry += ` pattern=${toolInput.pattern}`;
8942
+ }
8943
+ appendFileSync(auditLog, logEntry + `
8944
+ `);
8945
+ return {};
8946
+ };
8947
+ return {
8948
+ PreToolUse: [
8949
+ { matcher: "Read|Write|Edit|Glob|Grep|Bash", hooks: [preToolUseHook] }
8950
+ ],
8951
+ PostToolUse: [
8952
+ { matcher: "Read|Write|Edit|Glob|Grep|Bash", hooks: [postToolUseHook] }
8953
+ ]
8954
+ };
8955
+ }
8956
+ function validateSecurityConfig(config) {
8957
+ if (config.sandbox_dir) {
8958
+ const resolved = isAbsolute(config.sandbox_dir) ? config.sandbox_dir : resolve(process.cwd(), config.sandbox_dir);
8959
+ console.log(` Sandbox: ${resolved}`);
8960
+ }
8961
+ const denyPatterns = config.deny_patterns ?? DEFAULT_DENY_PATTERNS;
8962
+ console.log(` Deny patterns: ${denyPatterns.length} patterns`);
8963
+ if (config.max_turns) {
8964
+ console.log(` Max turns: ${config.max_turns}`);
8965
+ }
8966
+ if (config.audit_log) {
8967
+ console.log(` Audit log: ${config.audit_log}`);
8968
+ }
8969
+ }
8837
8970
 
8838
8971
  // node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs
8839
8972
  import { join as join5 } from "path";
@@ -8850,7 +8983,7 @@ import { cwd } from "process";
8850
8983
  import { realpathSync as realpathSync2 } from "fs";
8851
8984
  import { randomUUID } from "crypto";
8852
8985
  import { randomUUID as randomUUID2 } from "crypto";
8853
- import { appendFileSync as appendFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
8986
+ import { appendFileSync as appendFileSync22, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
8854
8987
  import { join as join3 } from "path";
8855
8988
  import { randomUUID as randomUUID3 } from "crypto";
8856
8989
  var __create2 = Object.create;
@@ -11724,7 +11857,7 @@ var require_compile = __commonJS2((exports) => {
11724
11857
  const schOrFunc = root2.refs[ref];
11725
11858
  if (schOrFunc)
11726
11859
  return schOrFunc;
11727
- let _sch = resolve.call(this, root2, ref);
11860
+ let _sch = resolve2.call(this, root2, ref);
11728
11861
  if (_sch === undefined) {
11729
11862
  const schema = (_a = root2.localRefs) === null || _a === undefined ? undefined : _a[ref];
11730
11863
  const { schemaId } = this.opts;
@@ -11751,7 +11884,7 @@ var require_compile = __commonJS2((exports) => {
11751
11884
  function sameSchemaEnv(s1, s2) {
11752
11885
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
11753
11886
  }
11754
- function resolve(root2, ref) {
11887
+ function resolve2(root2, ref) {
11755
11888
  let sch;
11756
11889
  while (typeof (sch = this.refs[ref]) == "string")
11757
11890
  ref = sch;
@@ -12249,54 +12382,54 @@ var require_fast_uri = __commonJS2((exports, module) => {
12249
12382
  }
12250
12383
  return uri;
12251
12384
  }
12252
- function resolve(baseURI, relativeURI, options) {
12385
+ function resolve2(baseURI, relativeURI, options) {
12253
12386
  const schemelessOptions = Object.assign({ scheme: "null" }, options);
12254
12387
  const resolved = resolveComponents(parse6(baseURI, schemelessOptions), parse6(relativeURI, schemelessOptions), schemelessOptions, true);
12255
12388
  return serialize(resolved, { ...schemelessOptions, skipEscape: true });
12256
12389
  }
12257
- function resolveComponents(base, relative, options, skipNormalization) {
12390
+ function resolveComponents(base, relative2, options, skipNormalization) {
12258
12391
  const target = {};
12259
12392
  if (!skipNormalization) {
12260
12393
  base = parse6(serialize(base, options), options);
12261
- relative = parse6(serialize(relative, options), options);
12394
+ relative2 = parse6(serialize(relative2, options), options);
12262
12395
  }
12263
12396
  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;
12397
+ if (!options.tolerant && relative2.scheme) {
12398
+ target.scheme = relative2.scheme;
12399
+ target.userinfo = relative2.userinfo;
12400
+ target.host = relative2.host;
12401
+ target.port = relative2.port;
12402
+ target.path = removeDotSegments(relative2.path || "");
12403
+ target.query = relative2.query;
12271
12404
  } 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;
12405
+ if (relative2.userinfo !== undefined || relative2.host !== undefined || relative2.port !== undefined) {
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;
12278
12411
  } else {
12279
- if (!relative.path) {
12412
+ if (!relative2.path) {
12280
12413
  target.path = base.path;
12281
- if (relative.query !== undefined) {
12282
- target.query = relative.query;
12414
+ if (relative2.query !== undefined) {
12415
+ target.query = relative2.query;
12283
12416
  } else {
12284
12417
  target.query = base.query;
12285
12418
  }
12286
12419
  } else {
12287
- if (relative.path.charAt(0) === "/") {
12288
- target.path = removeDotSegments(relative.path);
12420
+ if (relative2.path.charAt(0) === "/") {
12421
+ target.path = removeDotSegments(relative2.path);
12289
12422
  } else {
12290
12423
  if ((base.userinfo !== undefined || base.host !== undefined || base.port !== undefined) && !base.path) {
12291
- target.path = "/" + relative.path;
12424
+ target.path = "/" + relative2.path;
12292
12425
  } else if (!base.path) {
12293
- target.path = relative.path;
12426
+ target.path = relative2.path;
12294
12427
  } else {
12295
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
12428
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
12296
12429
  }
12297
12430
  target.path = removeDotSegments(target.path);
12298
12431
  }
12299
- target.query = relative.query;
12432
+ target.query = relative2.query;
12300
12433
  }
12301
12434
  target.userinfo = base.userinfo;
12302
12435
  target.host = base.host;
@@ -12304,7 +12437,7 @@ var require_fast_uri = __commonJS2((exports, module) => {
12304
12437
  }
12305
12438
  target.scheme = base.scheme;
12306
12439
  }
12307
- target.fragment = relative.fragment;
12440
+ target.fragment = relative2.fragment;
12308
12441
  return target;
12309
12442
  }
12310
12443
  function equal(uriA, uriB, options) {
@@ -12482,7 +12615,7 @@ var require_fast_uri = __commonJS2((exports, module) => {
12482
12615
  var fastUri = {
12483
12616
  SCHEMES,
12484
12617
  normalize,
12485
- resolve,
12618
+ resolve: resolve2,
12486
12619
  resolveComponents,
12487
12620
  equal,
12488
12621
  serialize,
@@ -16072,7 +16205,7 @@ function logForSdkDebugging(message) {
16072
16205
  const timestamp = new Date().toISOString();
16073
16206
  const output = `${timestamp} ${message}
16074
16207
  `;
16075
- appendFileSync2(path, output);
16208
+ appendFileSync22(path, output);
16076
16209
  }
16077
16210
  function mergeSandboxIntoExtraArgs(extraArgs, sandbox) {
16078
16211
  const effectiveExtraArgs = { ...extraArgs };
@@ -16474,7 +16607,7 @@ class ProcessTransport {
16474
16607
  }
16475
16608
  return;
16476
16609
  }
16477
- return new Promise((resolve, reject) => {
16610
+ return new Promise((resolve2, reject) => {
16478
16611
  const exitHandler = (code, signal) => {
16479
16612
  if (this.abortController.signal.aborted) {
16480
16613
  reject(new AbortError("Operation aborted"));
@@ -16484,7 +16617,7 @@ class ProcessTransport {
16484
16617
  if (error) {
16485
16618
  reject(error);
16486
16619
  } else {
16487
- resolve();
16620
+ resolve2();
16488
16621
  }
16489
16622
  };
16490
16623
  this.process.once("exit", exitHandler);
@@ -16535,17 +16668,17 @@ class Stream {
16535
16668
  if (this.hasError) {
16536
16669
  return Promise.reject(this.hasError);
16537
16670
  }
16538
- return new Promise((resolve, reject) => {
16539
- this.readResolve = resolve;
16671
+ return new Promise((resolve2, reject) => {
16672
+ this.readResolve = resolve2;
16540
16673
  this.readReject = reject;
16541
16674
  });
16542
16675
  }
16543
16676
  enqueue(value) {
16544
16677
  if (this.readResolve) {
16545
- const resolve = this.readResolve;
16678
+ const resolve2 = this.readResolve;
16546
16679
  this.readResolve = undefined;
16547
16680
  this.readReject = undefined;
16548
- resolve({ done: false, value });
16681
+ resolve2({ done: false, value });
16549
16682
  } else {
16550
16683
  this.queue.push(value);
16551
16684
  }
@@ -16553,10 +16686,10 @@ class Stream {
16553
16686
  done() {
16554
16687
  this.isDone = true;
16555
16688
  if (this.readResolve) {
16556
- const resolve = this.readResolve;
16689
+ const resolve2 = this.readResolve;
16557
16690
  this.readResolve = undefined;
16558
16691
  this.readReject = undefined;
16559
- resolve({ done: true, value: undefined });
16692
+ resolve2({ done: true, value: undefined });
16560
16693
  }
16561
16694
  }
16562
16695
  error(error) {
@@ -16886,10 +17019,10 @@ class Query {
16886
17019
  type: "control_request",
16887
17020
  request
16888
17021
  };
16889
- return new Promise((resolve, reject) => {
17022
+ return new Promise((resolve2, reject) => {
16890
17023
  this.pendingControlResponses.set(requestId, (response) => {
16891
17024
  if (response.subtype === "success") {
16892
- resolve(response);
17025
+ resolve2(response);
16893
17026
  } else {
16894
17027
  reject(new Error(response.error));
16895
17028
  if (response.pending_permission_requests) {
@@ -16979,15 +17112,15 @@ class Query {
16979
17112
  logForDebugging(`[Query.waitForFirstResult] Result already received, returning immediately`);
16980
17113
  return Promise.resolve();
16981
17114
  }
16982
- return new Promise((resolve) => {
17115
+ return new Promise((resolve2) => {
16983
17116
  if (this.abortController?.signal.aborted) {
16984
- resolve();
17117
+ resolve2();
16985
17118
  return;
16986
17119
  }
16987
- this.abortController?.signal.addEventListener("abort", () => resolve(), {
17120
+ this.abortController?.signal.addEventListener("abort", () => resolve2(), {
16988
17121
  once: true
16989
17122
  });
16990
- this.firstResultReceivedResolve = resolve;
17123
+ this.firstResultReceivedResolve = resolve2;
16991
17124
  });
16992
17125
  }
16993
17126
  handleHookCallbacks(callbackId, input, toolUseID, abortSignal) {
@@ -17038,13 +17171,13 @@ class Query {
17038
17171
  handleMcpControlRequest(serverName, mcpRequest, transport) {
17039
17172
  const messageId = "id" in mcpRequest.message ? mcpRequest.message.id : null;
17040
17173
  const key = `${serverName}:${messageId}`;
17041
- return new Promise((resolve, reject) => {
17174
+ return new Promise((resolve2, reject) => {
17042
17175
  const cleanup = () => {
17043
17176
  this.pendingMcpResponses.delete(key);
17044
17177
  };
17045
17178
  const resolveAndCleanup = (response) => {
17046
17179
  cleanup();
17047
- resolve(response);
17180
+ resolve2(response);
17048
17181
  };
17049
17182
  const rejectAndCleanup = (error) => {
17050
17183
  cleanup();
@@ -25452,6 +25585,78 @@ function query({
25452
25585
 
25453
25586
  // src/runner.ts
25454
25587
  import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
25588
+ import { execSync } from "child_process";
25589
+ import { createHash } from "crypto";
25590
+ var SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
25591
+
25592
+ class ProgressDisplay {
25593
+ interval = null;
25594
+ frame = 0;
25595
+ startTime = Date.now();
25596
+ currentActivity = "Working";
25597
+ lastToolUse = "";
25598
+ start(activity) {
25599
+ this.currentActivity = activity;
25600
+ this.startTime = Date.now();
25601
+ this.frame = 0;
25602
+ if (this.interval)
25603
+ return;
25604
+ this.interval = setInterval(() => {
25605
+ const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
25606
+ const toolInfo = this.lastToolUse ? ` → ${this.lastToolUse}` : "";
25607
+ process.stdout.write(`\r\x1B[K${SPINNER_FRAMES[this.frame]} ${this.currentActivity} (${elapsed}s)${toolInfo}`);
25608
+ this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
25609
+ }, 100);
25610
+ }
25611
+ updateActivity(activity) {
25612
+ this.currentActivity = activity;
25613
+ }
25614
+ updateTool(toolName, detail) {
25615
+ this.lastToolUse = detail ? `${toolName}: ${detail}` : toolName;
25616
+ }
25617
+ stop(finalMessage) {
25618
+ if (this.interval) {
25619
+ clearInterval(this.interval);
25620
+ this.interval = null;
25621
+ }
25622
+ process.stdout.write("\r\x1B[K");
25623
+ if (finalMessage) {
25624
+ console.log(finalMessage);
25625
+ }
25626
+ }
25627
+ getElapsed() {
25628
+ return (Date.now() - this.startTime) / 1000;
25629
+ }
25630
+ }
25631
+ var claudeExecutablePath;
25632
+ function findClaudeExecutable() {
25633
+ if (claudeExecutablePath !== undefined)
25634
+ return claudeExecutablePath;
25635
+ if (process.env.CLAUDE_CODE_PATH) {
25636
+ claudeExecutablePath = process.env.CLAUDE_CODE_PATH;
25637
+ return claudeExecutablePath;
25638
+ }
25639
+ try {
25640
+ const cmd = process.platform === "win32" ? "where claude" : "which claude";
25641
+ claudeExecutablePath = execSync(cmd, { encoding: "utf-8" }).trim().split(`
25642
+ `)[0];
25643
+ return claudeExecutablePath;
25644
+ } catch {
25645
+ const commonPaths = [
25646
+ "/usr/local/bin/claude",
25647
+ "/opt/homebrew/bin/claude",
25648
+ `${process.env.HOME}/.local/bin/claude`,
25649
+ `${process.env.HOME}/.nvm/versions/node/v22.12.0/bin/claude`
25650
+ ];
25651
+ for (const p of commonPaths) {
25652
+ if (existsSync3(p)) {
25653
+ claudeExecutablePath = p;
25654
+ return claudeExecutablePath;
25655
+ }
25656
+ }
25657
+ }
25658
+ return;
25659
+ }
25455
25660
  function isRetryableError(error2) {
25456
25661
  const errorStr = error2.message.toLowerCase();
25457
25662
  const retryablePatterns = [
@@ -25469,7 +25674,7 @@ function isRetryableError(error2) {
25469
25674
  return retryablePatterns.some((pattern) => errorStr.includes(pattern));
25470
25675
  }
25471
25676
  async function sleep(ms) {
25472
- return new Promise((resolve) => setTimeout(resolve, ms));
25677
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
25473
25678
  }
25474
25679
  async function runWithTimeout(promise, timeoutMs) {
25475
25680
  let timeoutId;
@@ -25485,19 +25690,69 @@ async function runWithTimeout(promise, timeoutMs) {
25485
25690
  throw e;
25486
25691
  }
25487
25692
  }
25488
- async function collectResult(prompt, options) {
25489
- let sessionId;
25693
+ function getToolDetail(toolName, toolInput) {
25694
+ switch (toolName) {
25695
+ case "Read":
25696
+ case "Write":
25697
+ case "Edit":
25698
+ const filePath = toolInput.file_path;
25699
+ if (filePath) {
25700
+ return filePath.split("/").pop() || filePath;
25701
+ }
25702
+ break;
25703
+ case "Glob":
25704
+ return toolInput.pattern || "";
25705
+ case "Grep":
25706
+ return toolInput.pattern?.slice(0, 20) || "";
25707
+ case "Bash":
25708
+ const cmd = toolInput.command || "";
25709
+ return cmd.slice(0, 30) + (cmd.length > 30 ? "..." : "");
25710
+ }
25711
+ return "";
25712
+ }
25713
+ async function collectResultWithProgress(prompt, options, progress, onSessionId) {
25714
+ let sessionId2;
25490
25715
  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;
25716
+ let lastError;
25717
+ try {
25718
+ const conversation = query({ prompt, options });
25719
+ for await (const message of conversation) {
25720
+ if (process.env.OVERNIGHT_DEBUG) {
25721
+ console.error(`
25722
+ [DEBUG] message.type=${message.type}, keys=${Object.keys(message).join(",")}`);
25723
+ }
25724
+ if (message.type === "result") {
25725
+ result = message.result;
25726
+ sessionId2 = message.session_id;
25727
+ } else if (message.type === "assistant" && "message" in message) {
25728
+ const assistantMsg = message.message;
25729
+ if (assistantMsg.content) {
25730
+ for (const block of assistantMsg.content) {
25731
+ if (process.env.OVERNIGHT_DEBUG) {
25732
+ console.error(`[DEBUG] content block: type=${block.type}, name=${block.name}`);
25733
+ }
25734
+ if (block.type === "tool_use" && block.name) {
25735
+ const detail = block.input ? getToolDetail(block.name, block.input) : "";
25736
+ progress.updateTool(block.name, detail);
25737
+ }
25738
+ }
25739
+ }
25740
+ } else if (message.type === "system" && "subtype" in message) {
25741
+ if (message.subtype === "init") {
25742
+ sessionId2 = message.session_id;
25743
+ if (sessionId2 && onSessionId) {
25744
+ onSessionId(sessionId2);
25745
+ }
25746
+ }
25747
+ }
25496
25748
  }
25749
+ } catch (e) {
25750
+ lastError = e.message;
25751
+ throw e;
25497
25752
  }
25498
- return { sessionId, result };
25753
+ return { sessionId: sessionId2, result, error: lastError };
25499
25754
  }
25500
- async function runJob(config2, log) {
25755
+ async function runJob(config2, log, options) {
25501
25756
  const startTime = Date.now();
25502
25757
  const tools = config2.allowed_tools ?? DEFAULT_TOOLS;
25503
25758
  const timeout = (config2.timeout_seconds ?? DEFAULT_TIMEOUT) * 1000;
@@ -25505,31 +25760,71 @@ async function runJob(config2, log) {
25505
25760
  const retryDelay = config2.retry_delay ?? DEFAULT_RETRY_DELAY;
25506
25761
  const verifyPrompt = config2.verify_prompt ?? DEFAULT_VERIFY_PROMPT;
25507
25762
  let retriesUsed = 0;
25763
+ let resumeSessionId = options?.resumeSessionId;
25508
25764
  const logMsg = (msg) => log?.(msg);
25509
- logMsg(`Starting: ${config2.prompt.slice(0, 60)}...`);
25765
+ const progress = new ProgressDisplay;
25766
+ const claudePath = findClaudeExecutable();
25767
+ if (!claudePath) {
25768
+ logMsg("\x1B[31m✗ Error: Could not find 'claude' CLI.\x1B[0m");
25769
+ logMsg("\x1B[33m Install it with:\x1B[0m");
25770
+ logMsg(" curl -fsSL https://claude.ai/install.sh | bash");
25771
+ logMsg("\x1B[33m Or set CLAUDE_CODE_PATH environment variable.\x1B[0m");
25772
+ return {
25773
+ task: config2.prompt,
25774
+ status: "failed",
25775
+ error: "Claude CLI not found. Install with: curl -fsSL https://claude.ai/install.sh | bash",
25776
+ duration_seconds: 0,
25777
+ verified: false,
25778
+ retries: 0
25779
+ };
25780
+ }
25781
+ if (process.env.OVERNIGHT_DEBUG) {
25782
+ logMsg(`\x1B[2mDebug: Claude path = ${claudePath}\x1B[0m`);
25783
+ }
25784
+ const taskPreview = config2.prompt.slice(0, 60) + (config2.prompt.length > 60 ? "..." : "");
25785
+ if (resumeSessionId) {
25786
+ logMsg(`\x1B[36m▶\x1B[0m Resuming: ${taskPreview}`);
25787
+ } else {
25788
+ logMsg(`\x1B[36m▶\x1B[0m ${taskPreview}`);
25789
+ }
25510
25790
  for (let attempt = 0;attempt <= retryCount; attempt++) {
25511
25791
  try {
25512
- const options = {
25792
+ const securityHooks = config2.security ? createSecurityHooks(config2.security) : undefined;
25793
+ const sdkOptions = {
25513
25794
  allowedTools: tools,
25514
25795
  permissionMode: "acceptEdits",
25515
- ...config2.working_dir && { cwd: config2.working_dir }
25796
+ ...claudePath && { pathToClaudeCodeExecutable: claudePath },
25797
+ ...config2.working_dir && { cwd: config2.working_dir },
25798
+ ...config2.security?.max_turns && { maxTurns: config2.security.max_turns },
25799
+ ...securityHooks && { hooks: securityHooks },
25800
+ ...resumeSessionId && { resume: resumeSessionId }
25516
25801
  };
25517
- let sessionId;
25802
+ let sessionId2;
25518
25803
  let result;
25804
+ const prompt = resumeSessionId ? "Continue where you left off. Complete the original task." : config2.prompt;
25805
+ progress.start(resumeSessionId ? "Resuming" : "Working");
25519
25806
  try {
25520
- const collected = await runWithTimeout(collectResult(config2.prompt, options), timeout);
25521
- sessionId = collected.sessionId;
25807
+ const collected = await runWithTimeout(collectResultWithProgress(prompt, sdkOptions, progress, (id) => {
25808
+ sessionId2 = id;
25809
+ options?.onSessionId?.(id);
25810
+ }), timeout);
25811
+ sessionId2 = collected.sessionId;
25522
25812
  result = collected.result;
25813
+ progress.stop();
25523
25814
  } catch (e) {
25815
+ progress.stop();
25524
25816
  if (e.message === "TIMEOUT") {
25525
25817
  if (attempt < retryCount) {
25526
25818
  retriesUsed = attempt + 1;
25819
+ if (sessionId2) {
25820
+ resumeSessionId = sessionId2;
25821
+ }
25527
25822
  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})...`);
25823
+ logMsg(`\x1B[33m⚠ Timeout after ${config2.timeout_seconds ?? DEFAULT_TIMEOUT}s, retrying in ${delay}s (${attempt + 1}/${retryCount})\x1B[0m`);
25529
25824
  await sleep(delay * 1000);
25530
25825
  continue;
25531
25826
  }
25532
- logMsg(`Timeout after ${config2.timeout_seconds ?? DEFAULT_TIMEOUT}s (exhausted retries)`);
25827
+ logMsg(`\x1B[31m✗ Timeout after ${config2.timeout_seconds ?? DEFAULT_TIMEOUT}s (exhausted retries)\x1B[0m`);
25533
25828
  return {
25534
25829
  task: config2.prompt,
25535
25830
  status: "timeout",
@@ -25541,37 +25836,50 @@ async function runJob(config2, log) {
25541
25836
  }
25542
25837
  throw e;
25543
25838
  }
25544
- if (config2.verify !== false && sessionId) {
25545
- logMsg("Running verification...");
25839
+ if (config2.verify !== false && sessionId2) {
25840
+ progress.start("Verifying");
25546
25841
  const verifyOptions = {
25547
- resume: sessionId,
25548
- permissionMode: "acceptEdits"
25842
+ allowedTools: tools,
25843
+ resume: sessionId2,
25844
+ permissionMode: "acceptEdits",
25845
+ ...claudePath && { pathToClaudeCodeExecutable: claudePath },
25846
+ ...config2.working_dir && { cwd: config2.working_dir },
25847
+ ...config2.security?.max_turns && { maxTurns: config2.security.max_turns }
25549
25848
  };
25849
+ const fixPrompt = verifyPrompt + " If you find any issues, fix them now. Only report issues you cannot fix.";
25550
25850
  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");
25851
+ const verifyResult = await runWithTimeout(collectResultWithProgress(fixPrompt, verifyOptions, progress, (id) => {
25852
+ sessionId2 = id;
25853
+ options?.onSessionId?.(id);
25854
+ }), timeout / 2);
25855
+ progress.stop();
25856
+ if (verifyResult.result) {
25857
+ result = verifyResult.result;
25858
+ }
25859
+ const unfixableWords = ["cannot fix", "unable to", "blocked by", "requires manual"];
25860
+ if (verifyResult.result && unfixableWords.some((word) => verifyResult.result.toLowerCase().includes(word))) {
25861
+ logMsg(`\x1B[33m⚠ Verification found unfixable issues\x1B[0m`);
25555
25862
  return {
25556
25863
  task: config2.prompt,
25557
25864
  status: "verification_failed",
25558
25865
  result,
25559
- error: `Verification issues: ${verifyResult.result}`,
25866
+ error: `Unfixable issues: ${verifyResult.result}`,
25560
25867
  duration_seconds: (Date.now() - startTime) / 1000,
25561
25868
  verified: false,
25562
25869
  retries: retriesUsed
25563
25870
  };
25564
25871
  }
25565
25872
  } catch (e) {
25873
+ progress.stop();
25566
25874
  if (e.message === "TIMEOUT") {
25567
- logMsg("Verification timed out - continuing anyway");
25875
+ logMsg("\x1B[33m⚠ Verification timed out - continuing anyway\x1B[0m");
25568
25876
  } else {
25569
25877
  throw e;
25570
25878
  }
25571
25879
  }
25572
25880
  }
25573
25881
  const duration3 = (Date.now() - startTime) / 1000;
25574
- logMsg(`Completed in ${duration3.toFixed(1)}s`);
25882
+ logMsg(`\x1B[32m✓ Completed in ${duration3.toFixed(1)}s\x1B[0m`);
25575
25883
  return {
25576
25884
  task: config2.prompt,
25577
25885
  status: "success",
@@ -25581,16 +25889,20 @@ async function runJob(config2, log) {
25581
25889
  retries: retriesUsed
25582
25890
  };
25583
25891
  } catch (e) {
25892
+ progress.stop();
25584
25893
  const error2 = e;
25585
25894
  if (isRetryableError(error2) && attempt < retryCount) {
25586
25895
  retriesUsed = attempt + 1;
25896
+ if (sessionId) {
25897
+ resumeSessionId = sessionId;
25898
+ }
25587
25899
  const delay = retryDelay * Math.pow(2, attempt);
25588
- logMsg(`Retryable error: ${error2.message}, retrying in ${delay}s (attempt ${attempt + 1}/${retryCount})...`);
25900
+ logMsg(`\x1B[33m⚠ ${error2.message}, retrying in ${delay}s (${attempt + 1}/${retryCount})\x1B[0m`);
25589
25901
  await sleep(delay * 1000);
25590
25902
  continue;
25591
25903
  }
25592
25904
  const duration3 = (Date.now() - startTime) / 1000;
25593
- logMsg(`Failed: ${error2.message}`);
25905
+ logMsg(`\x1B[31m✗ Failed: ${error2.message}\x1B[0m`);
25594
25906
  return {
25595
25907
  task: config2.prompt,
25596
25908
  status: "failed",
@@ -25610,6 +25922,56 @@ async function runJob(config2, log) {
25610
25922
  retries: retriesUsed
25611
25923
  };
25612
25924
  }
25925
+ function taskKey(config2) {
25926
+ if (config2.id)
25927
+ return config2.id;
25928
+ return createHash("sha256").update(config2.prompt).digest("hex").slice(0, 12);
25929
+ }
25930
+ function validateDag(configs) {
25931
+ const ids = new Set(configs.map((c) => c.id).filter(Boolean));
25932
+ for (const c of configs) {
25933
+ for (const dep of c.depends_on ?? []) {
25934
+ if (!ids.has(dep)) {
25935
+ return `Task "${c.id ?? c.prompt.slice(0, 40)}" depends on unknown id "${dep}"`;
25936
+ }
25937
+ }
25938
+ }
25939
+ const visited = new Set;
25940
+ const inStack = new Set;
25941
+ const idToConfig = new Map(configs.filter((c) => c.id).map((c) => [c.id, c]));
25942
+ function hasCycle(id) {
25943
+ if (inStack.has(id))
25944
+ return true;
25945
+ if (visited.has(id))
25946
+ return false;
25947
+ visited.add(id);
25948
+ inStack.add(id);
25949
+ const config2 = idToConfig.get(id);
25950
+ for (const dep of config2?.depends_on ?? []) {
25951
+ if (hasCycle(dep))
25952
+ return true;
25953
+ }
25954
+ inStack.delete(id);
25955
+ return false;
25956
+ }
25957
+ for (const id of ids) {
25958
+ if (hasCycle(id))
25959
+ return `Dependency cycle detected involving "${id}"`;
25960
+ }
25961
+ return null;
25962
+ }
25963
+ function depsReady(config2, completed) {
25964
+ if (!config2.depends_on || config2.depends_on.length === 0)
25965
+ return "ready";
25966
+ for (const dep of config2.depends_on) {
25967
+ const result = completed[dep];
25968
+ if (!result)
25969
+ return "waiting";
25970
+ if (result.status !== "success")
25971
+ return "blocked";
25972
+ }
25973
+ return "ready";
25974
+ }
25613
25975
  function saveState(state, stateFile) {
25614
25976
  writeFileSync(stateFile, JSON.stringify(state, null, 2));
25615
25977
  }
@@ -25624,26 +25986,84 @@ function clearState(stateFile) {
25624
25986
  }
25625
25987
  async function runJobsWithState(configs, options = {}) {
25626
25988
  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;
25989
+ const dagError = validateDag(configs);
25990
+ if (dagError) {
25991
+ options.log?.(`\x1B[31m✗ DAG error: ${dagError}\x1B[0m`);
25992
+ return [];
25993
+ }
25994
+ const state = loadState(stateFile) ?? {
25995
+ completed: {},
25996
+ timestamp: new Date().toISOString()
25997
+ };
25998
+ let currentConfigs = configs;
25999
+ while (true) {
26000
+ const notDone = currentConfigs.filter((c) => !(taskKey(c) in state.completed));
26001
+ if (notDone.length === 0)
26002
+ break;
26003
+ const ready = notDone.filter((c) => depsReady(c, state.completed) === "ready");
26004
+ const blocked = notDone.filter((c) => depsReady(c, state.completed) === "blocked");
26005
+ for (const bc of blocked) {
26006
+ const key2 = taskKey(bc);
26007
+ if (key2 in state.completed)
26008
+ continue;
26009
+ const failedDeps = (bc.depends_on ?? []).filter((dep) => state.completed[dep] && state.completed[dep].status !== "success");
26010
+ const label2 = bc.id ?? bc.prompt.slice(0, 40);
26011
+ options.log?.(`
26012
+ \x1B[31m✗ Skipping "${label2}" — dependency failed: ${failedDeps.join(", ")}\x1B[0m`);
26013
+ state.completed[key2] = {
26014
+ task: bc.prompt,
26015
+ status: "failed",
26016
+ error: `Blocked by failed dependencies: ${failedDeps.join(", ")}`,
26017
+ duration_seconds: 0,
26018
+ verified: false,
26019
+ retries: 0
26020
+ };
26021
+ state.timestamp = new Date().toISOString();
26022
+ saveState(state, stateFile);
26023
+ }
26024
+ if (ready.length === 0)
26025
+ break;
26026
+ const config2 = ready[0];
26027
+ const key = taskKey(config2);
26028
+ const totalNotDone = notDone.length - blocked.length;
26029
+ const totalDone = Object.keys(state.completed).length;
26030
+ const label = config2.id ? `${config2.id}` : "";
25632
26031
  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
- };
26032
+ \x1B[1m[${totalDone + 1}/${totalDone + totalNotDone}]${label ? ` ${label}` : ""}\x1B[0m`);
26033
+ const resumeSessionId = state.inProgress?.hash === key ? state.inProgress.sessionId : undefined;
26034
+ if (resumeSessionId) {
26035
+ options.log?.(`\x1B[2mResuming session ${resumeSessionId.slice(0, 8)}...\x1B[0m`);
26036
+ }
26037
+ state.inProgress = { hash: key, prompt: config2.prompt, startedAt: new Date().toISOString() };
25642
26038
  saveState(state, stateFile);
25643
- if (i < configs.length - 1) {
26039
+ const result = await runJob(config2, options.log, {
26040
+ resumeSessionId,
26041
+ onSessionId: (id) => {
26042
+ state.inProgress = { hash: key, prompt: config2.prompt, sessionId: id, startedAt: state.inProgress.startedAt };
26043
+ saveState(state, stateFile);
26044
+ }
26045
+ });
26046
+ state.completed[key] = result;
26047
+ state.inProgress = undefined;
26048
+ state.timestamp = new Date().toISOString();
26049
+ saveState(state, stateFile);
26050
+ if (options.reloadConfigs) {
26051
+ try {
26052
+ currentConfigs = options.reloadConfigs();
26053
+ const newDagError = validateDag(currentConfigs);
26054
+ if (newDagError) {
26055
+ options.log?.(`\x1B[33m⚠ DAG error in updated YAML, ignoring reload: ${newDagError}\x1B[0m`);
26056
+ currentConfigs = configs;
26057
+ }
26058
+ } catch {}
26059
+ }
26060
+ const nextNotDone = currentConfigs.filter((c) => !(taskKey(c) in state.completed));
26061
+ const nextReady = nextNotDone.filter((c) => depsReady(c, state.completed) === "ready");
26062
+ if (nextReady.length > 0) {
25644
26063
  await sleep(1000);
25645
26064
  }
25646
26065
  }
26066
+ const results = currentConfigs.map((c) => state.completed[taskKey(c)]).filter((r) => r !== undefined);
25647
26067
  clearState(stateFile);
25648
26068
  return results;
25649
26069
  }
@@ -25877,12 +26297,27 @@ overnight resume tasks.yaml
25877
26297
 
25878
26298
  Run \`overnight <command> --help\` for command-specific options.
25879
26299
  `;
25880
- function parseTasksFile(path) {
26300
+ function parseTasksFile(path, cliSecurity) {
25881
26301
  const content = readFileSync3(path, "utf-8");
25882
- const data = $parse(content);
26302
+ let data;
26303
+ try {
26304
+ data = $parse(content);
26305
+ } catch (e) {
26306
+ const error2 = e;
26307
+ console.error(`\x1B[31mError parsing ${path}:\x1B[0m`);
26308
+ console.error(` ${error2.message.split(`
26309
+ `)[0]}`);
26310
+ process.exit(1);
26311
+ }
25883
26312
  const tasks = Array.isArray(data) ? data : data.tasks ?? [];
25884
26313
  const defaults = Array.isArray(data) ? {} : data.defaults ?? {};
25885
- return tasks.map((task) => {
26314
+ const fileSecurity = !Array.isArray(data) && data.defaults?.security || {};
26315
+ const security = cliSecurity || Object.keys(fileSecurity).length > 0 ? {
26316
+ ...fileSecurity,
26317
+ ...cliSecurity,
26318
+ deny_patterns: cliSecurity?.deny_patterns ?? fileSecurity.deny_patterns ?? DEFAULT_DENY_PATTERNS
26319
+ } : undefined;
26320
+ const configs = tasks.map((task) => {
25886
26321
  if (typeof task === "string") {
25887
26322
  return {
25888
26323
  prompt: task,
@@ -25890,19 +26325,24 @@ function parseTasksFile(path) {
25890
26325
  stall_timeout_seconds: defaults.stall_timeout_seconds ?? DEFAULT_STALL_TIMEOUT,
25891
26326
  verify: defaults.verify ?? true,
25892
26327
  verify_prompt: defaults.verify_prompt ?? DEFAULT_VERIFY_PROMPT,
25893
- allowed_tools: defaults.allowed_tools
26328
+ allowed_tools: defaults.allowed_tools,
26329
+ security
25894
26330
  };
25895
26331
  }
25896
26332
  return {
26333
+ id: task.id ?? undefined,
26334
+ depends_on: task.depends_on ?? undefined,
25897
26335
  prompt: task.prompt,
25898
26336
  working_dir: task.working_dir ?? undefined,
25899
26337
  timeout_seconds: task.timeout_seconds ?? defaults.timeout_seconds ?? DEFAULT_TIMEOUT,
25900
26338
  stall_timeout_seconds: task.stall_timeout_seconds ?? defaults.stall_timeout_seconds ?? DEFAULT_STALL_TIMEOUT,
25901
26339
  verify: task.verify ?? defaults.verify ?? true,
25902
26340
  verify_prompt: task.verify_prompt ?? defaults.verify_prompt ?? DEFAULT_VERIFY_PROMPT,
25903
- allowed_tools: task.allowed_tools ?? defaults.allowed_tools
26341
+ allowed_tools: task.allowed_tools ?? defaults.allowed_tools,
26342
+ security: task.security ?? security
25904
26343
  };
25905
26344
  });
26345
+ return { configs, security };
25906
26346
  }
25907
26347
  function printSummary(results) {
25908
26348
  const statusColors = {
@@ -25928,26 +26368,45 @@ ${bold}Job Results${reset}`);
25928
26368
  ${bold}Summary:${reset} ${succeeded}/${results.length} succeeded`);
25929
26369
  }
25930
26370
  var program2 = new Command;
25931
- program2.name("overnight").description("Batch job runner for Claude Code").version("0.1.0").action(() => {
26371
+ program2.name("overnight").description("Batch job runner for Claude Code").version("0.2.0").action(() => {
25932
26372
  console.log(AGENT_HELP);
25933
26373
  });
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) => {
26374
+ program2.command("run").description("Run jobs from a YAML tasks file").argument("<tasks-file>", "Path to tasks.yaml file").option("-o, --output <file>", "Output file for results JSON").option("-q, --quiet", "Minimal output").option("-s, --state-file <file>", "Custom state file path").option("--notify", "Send push notification via ntfy.sh").option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC).option("-r, --report <file>", "Generate markdown report").option("--sandbox <dir>", "Sandbox directory (restrict file access)").option("--max-turns <n>", "Max agent iterations per task", String(DEFAULT_MAX_TURNS)).option("--audit-log <file>", "Audit log file path").option("--no-security", "Disable default security (deny patterns)").action(async (tasksFile, opts) => {
25935
26375
  if (!existsSync5(tasksFile)) {
25936
26376
  console.error(`Error: File not found: ${tasksFile}`);
25937
26377
  process.exit(1);
25938
26378
  }
25939
- const configs = parseTasksFile(tasksFile);
26379
+ const cliSecurity = opts.security === false ? undefined : {
26380
+ ...opts.sandbox && { sandbox_dir: opts.sandbox },
26381
+ ...opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) },
26382
+ ...opts.auditLog && { audit_log: opts.auditLog }
26383
+ };
26384
+ const { configs, security } = parseTasksFile(tasksFile, cliSecurity);
25940
26385
  if (configs.length === 0) {
25941
26386
  console.error("No tasks found in file");
25942
26387
  process.exit(1);
25943
26388
  }
25944
- console.log(`\x1B[1movernight: Running ${configs.length} jobs...\x1B[0m
25945
- `);
26389
+ const existingState = loadState(opts.stateFile ?? DEFAULT_STATE_FILE);
26390
+ if (existingState) {
26391
+ const done = Object.keys(existingState.completed).length;
26392
+ const pending = configs.filter((c) => !(taskKey(c) in existingState.completed)).length;
26393
+ console.log(`\x1B[1movernight: Resuming — ${done} done, ${pending} remaining\x1B[0m`);
26394
+ console.log(`\x1B[2mLast checkpoint: ${existingState.timestamp}\x1B[0m`);
26395
+ } else {
26396
+ console.log(`\x1B[1movernight: Running ${configs.length} jobs...\x1B[0m`);
26397
+ }
26398
+ if (security && !opts.quiet) {
26399
+ console.log("\x1B[2mSecurity:\x1B[0m");
26400
+ validateSecurityConfig(security);
26401
+ }
26402
+ console.log("");
25946
26403
  const log = opts.quiet ? undefined : (msg) => console.log(msg);
25947
26404
  const startTime = Date.now();
26405
+ const reloadConfigs = () => parseTasksFile(tasksFile, cliSecurity).configs;
25948
26406
  const results = await runJobsWithState(configs, {
25949
26407
  stateFile: opts.stateFile,
25950
- log
26408
+ log,
26409
+ reloadConfigs
25951
26410
  });
25952
26411
  const totalDuration = (Date.now() - startTime) / 1000;
25953
26412
  if (opts.notify) {
@@ -25974,7 +26433,7 @@ program2.command("run").description("Run jobs from a YAML tasks file").argument(
25974
26433
  process.exit(1);
25975
26434
  }
25976
26435
  });
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) => {
26436
+ 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
26437
  const stateFile = opts.stateFile ?? DEFAULT_STATE_FILE;
25979
26438
  const state = loadState(stateFile);
25980
26439
  if (!state) {
@@ -25986,26 +26445,32 @@ program2.command("resume").description("Resume a previous run from saved state")
25986
26445
  console.error(`Error: File not found: ${tasksFile}`);
25987
26446
  process.exit(1);
25988
26447
  }
25989
- const configs = parseTasksFile(tasksFile);
26448
+ const cliSecurity = opts.security === false ? undefined : {
26449
+ ...opts.sandbox && { sandbox_dir: opts.sandbox },
26450
+ ...opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) },
26451
+ ...opts.auditLog && { audit_log: opts.auditLog }
26452
+ };
26453
+ const { configs, security } = parseTasksFile(tasksFile, cliSecurity);
25990
26454
  if (configs.length === 0) {
25991
26455
  console.error("No tasks found in file");
25992
26456
  process.exit(1);
25993
26457
  }
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);
26458
+ const completedCount = Object.keys(state.completed).length;
26459
+ const pendingCount = configs.filter((c) => !(taskKey(c) in state.completed)).length;
26460
+ console.log(`\x1B[1movernight: Resuming — ${completedCount} done, ${pendingCount} remaining\x1B[0m`);
26461
+ console.log(`\x1B[2mLast checkpoint: ${state.timestamp}\x1B[0m`);
26462
+ if (security && !opts.quiet) {
26463
+ console.log("\x1B[2mSecurity:\x1B[0m");
26464
+ validateSecurityConfig(security);
25997
26465
  }
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
- `);
26466
+ console.log("");
26002
26467
  const log = opts.quiet ? undefined : (msg) => console.log(msg);
26003
26468
  const startTime = Date.now();
26469
+ const reloadConfigs = () => parseTasksFile(tasksFile, cliSecurity).configs;
26004
26470
  const results = await runJobsWithState(configs, {
26005
26471
  stateFile,
26006
26472
  log,
26007
- startIndex,
26008
- priorResults: state.results
26473
+ reloadConfigs
26009
26474
  });
26010
26475
  const totalDuration = (Date.now() - startTime) / 1000;
26011
26476
  if (opts.notify) {
@@ -26032,12 +26497,18 @@ program2.command("resume").description("Resume a previous run from saved state")
26032
26497
  process.exit(1);
26033
26498
  }
26034
26499
  });
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) => {
26500
+ 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) => {
26501
+ const security = opts.security === false ? undefined : {
26502
+ ...opts.sandbox && { sandbox_dir: opts.sandbox },
26503
+ ...opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) },
26504
+ deny_patterns: DEFAULT_DENY_PATTERNS
26505
+ };
26036
26506
  const config2 = {
26037
26507
  prompt,
26038
26508
  timeout_seconds: parseInt(opts.timeout, 10),
26039
26509
  verify: opts.verify,
26040
- allowed_tools: opts.tools
26510
+ allowed_tools: opts.tools,
26511
+ security
26041
26512
  };
26042
26513
  const log = (msg) => console.log(msg);
26043
26514
  const result = await runJob(config2, log);
@@ -26063,6 +26534,7 @@ program2.command("init").description("Create an example tasks.yaml file").action
26063
26534
  defaults:
26064
26535
  timeout_seconds: 300 # 5 minutes per task
26065
26536
  verify: true # Run verification after each task
26537
+
26066
26538
  # Secure defaults - no Bash, just file operations
26067
26539
  allowed_tools:
26068
26540
  - Read
@@ -26071,6 +26543,15 @@ defaults:
26071
26543
  - Glob
26072
26544
  - Grep
26073
26545
 
26546
+ # Security settings (optional - deny_patterns enabled by default)
26547
+ security:
26548
+ sandbox_dir: "." # Restrict to current directory
26549
+ max_turns: 100 # Prevent runaway agents
26550
+ # audit_log: "overnight-audit.log" # Uncomment to enable
26551
+ # deny_patterns: # Default patterns block .env, .key, .pem, etc.
26552
+ # - "**/.env*"
26553
+ # - "**/*.key"
26554
+
26074
26555
  tasks:
26075
26556
  # Simple string format
26076
26557
  - "Find and fix any TODO comments in the codebase"