@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/README.md +73 -16
- package/dist/cli.js +599 -118
- package/package.json +1 -1
- package/src/cli.ts +123 -19
- package/src/runner.ts +422 -45
- package/src/security.ts +162 -0
- package/src/types.ts +37 -4
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 = "
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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,
|
|
12390
|
+
function resolveComponents(base, relative2, options, skipNormalization) {
|
|
12258
12391
|
const target = {};
|
|
12259
12392
|
if (!skipNormalization) {
|
|
12260
12393
|
base = parse6(serialize(base, options), options);
|
|
12261
|
-
|
|
12394
|
+
relative2 = parse6(serialize(relative2, options), options);
|
|
12262
12395
|
}
|
|
12263
12396
|
options = options || {};
|
|
12264
|
-
if (!options.tolerant &&
|
|
12265
|
-
target.scheme =
|
|
12266
|
-
target.userinfo =
|
|
12267
|
-
target.host =
|
|
12268
|
-
target.port =
|
|
12269
|
-
target.path = removeDotSegments(
|
|
12270
|
-
target.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 (
|
|
12273
|
-
target.userinfo =
|
|
12274
|
-
target.host =
|
|
12275
|
-
target.port =
|
|
12276
|
-
target.path = removeDotSegments(
|
|
12277
|
-
target.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 (!
|
|
12412
|
+
if (!relative2.path) {
|
|
12280
12413
|
target.path = base.path;
|
|
12281
|
-
if (
|
|
12282
|
-
target.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 (
|
|
12288
|
-
target.path = removeDotSegments(
|
|
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 = "/" +
|
|
12424
|
+
target.path = "/" + relative2.path;
|
|
12292
12425
|
} else if (!base.path) {
|
|
12293
|
-
target.path =
|
|
12426
|
+
target.path = relative2.path;
|
|
12294
12427
|
} else {
|
|
12295
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
16539
|
-
this.readResolve =
|
|
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
|
|
16678
|
+
const resolve2 = this.readResolve;
|
|
16546
16679
|
this.readResolve = undefined;
|
|
16547
16680
|
this.readReject = undefined;
|
|
16548
|
-
|
|
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
|
|
16689
|
+
const resolve2 = this.readResolve;
|
|
16557
16690
|
this.readResolve = undefined;
|
|
16558
16691
|
this.readReject = undefined;
|
|
16559
|
-
|
|
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((
|
|
17022
|
+
return new Promise((resolve2, reject) => {
|
|
16890
17023
|
this.pendingControlResponses.set(requestId, (response) => {
|
|
16891
17024
|
if (response.subtype === "success") {
|
|
16892
|
-
|
|
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((
|
|
17115
|
+
return new Promise((resolve2) => {
|
|
16983
17116
|
if (this.abortController?.signal.aborted) {
|
|
16984
|
-
|
|
17117
|
+
resolve2();
|
|
16985
17118
|
return;
|
|
16986
17119
|
}
|
|
16987
|
-
this.abortController?.signal.addEventListener("abort", () =>
|
|
17120
|
+
this.abortController?.signal.addEventListener("abort", () => resolve2(), {
|
|
16988
17121
|
once: true
|
|
16989
17122
|
});
|
|
16990
|
-
this.firstResultReceivedResolve =
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
25489
|
-
|
|
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
|
-
|
|
25492
|
-
|
|
25493
|
-
|
|
25494
|
-
|
|
25495
|
-
|
|
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
|
-
|
|
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
|
|
25792
|
+
const securityHooks = config2.security ? createSecurityHooks(config2.security) : undefined;
|
|
25793
|
+
const sdkOptions = {
|
|
25513
25794
|
allowedTools: tools,
|
|
25514
25795
|
permissionMode: "acceptEdits",
|
|
25515
|
-
...
|
|
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
|
|
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(
|
|
25521
|
-
|
|
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(
|
|
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(
|
|
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 &&
|
|
25545
|
-
|
|
25839
|
+
if (config2.verify !== false && sessionId2) {
|
|
25840
|
+
progress.start("Verifying");
|
|
25546
25841
|
const verifyOptions = {
|
|
25547
|
-
|
|
25548
|
-
|
|
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(
|
|
25552
|
-
|
|
25553
|
-
|
|
25554
|
-
|
|
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: `
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
25628
|
-
|
|
25629
|
-
|
|
25630
|
-
|
|
25631
|
-
|
|
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
|
-
[${
|
|
25634
|
-
const
|
|
25635
|
-
|
|
25636
|
-
|
|
25637
|
-
|
|
25638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
25995
|
-
|
|
25996
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|