@yail259/overnight 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.context/notes.md +0 -0
- package/.context/todos.md +0 -0
- package/README.md +73 -16
- package/dist/cli.js +1754 -249
- package/package.json +2 -2
- package/src/cli.ts +499 -112
- package/src/goal-runner.ts +709 -0
- package/src/planner.ts +238 -0
- package/src/runner.ts +427 -47
- package/src/security.ts +162 -0
- package/src/types.ts +85 -4
package/dist/cli.js
CHANGED
|
@@ -8760,6 +8760,55 @@ var require_public_api = __commonJS((exports) => {
|
|
|
8760
8760
|
exports.stringify = stringify;
|
|
8761
8761
|
});
|
|
8762
8762
|
|
|
8763
|
+
// node_modules/yaml/dist/index.js
|
|
8764
|
+
var require_dist = __commonJS((exports) => {
|
|
8765
|
+
var composer = require_composer();
|
|
8766
|
+
var Document = require_Document();
|
|
8767
|
+
var Schema = require_Schema();
|
|
8768
|
+
var errors = require_errors();
|
|
8769
|
+
var Alias = require_Alias();
|
|
8770
|
+
var identity = require_identity();
|
|
8771
|
+
var Pair = require_Pair();
|
|
8772
|
+
var Scalar = require_Scalar();
|
|
8773
|
+
var YAMLMap = require_YAMLMap();
|
|
8774
|
+
var YAMLSeq = require_YAMLSeq();
|
|
8775
|
+
var cst = require_cst();
|
|
8776
|
+
var lexer = require_lexer();
|
|
8777
|
+
var lineCounter = require_line_counter();
|
|
8778
|
+
var parser = require_parser();
|
|
8779
|
+
var publicApi = require_public_api();
|
|
8780
|
+
var visit = require_visit();
|
|
8781
|
+
exports.Composer = composer.Composer;
|
|
8782
|
+
exports.Document = Document.Document;
|
|
8783
|
+
exports.Schema = Schema.Schema;
|
|
8784
|
+
exports.YAMLError = errors.YAMLError;
|
|
8785
|
+
exports.YAMLParseError = errors.YAMLParseError;
|
|
8786
|
+
exports.YAMLWarning = errors.YAMLWarning;
|
|
8787
|
+
exports.Alias = Alias.Alias;
|
|
8788
|
+
exports.isAlias = identity.isAlias;
|
|
8789
|
+
exports.isCollection = identity.isCollection;
|
|
8790
|
+
exports.isDocument = identity.isDocument;
|
|
8791
|
+
exports.isMap = identity.isMap;
|
|
8792
|
+
exports.isNode = identity.isNode;
|
|
8793
|
+
exports.isPair = identity.isPair;
|
|
8794
|
+
exports.isScalar = identity.isScalar;
|
|
8795
|
+
exports.isSeq = identity.isSeq;
|
|
8796
|
+
exports.Pair = Pair.Pair;
|
|
8797
|
+
exports.Scalar = Scalar.Scalar;
|
|
8798
|
+
exports.YAMLMap = YAMLMap.YAMLMap;
|
|
8799
|
+
exports.YAMLSeq = YAMLSeq.YAMLSeq;
|
|
8800
|
+
exports.CST = cst;
|
|
8801
|
+
exports.Lexer = lexer.Lexer;
|
|
8802
|
+
exports.LineCounter = lineCounter.LineCounter;
|
|
8803
|
+
exports.Parser = parser.Parser;
|
|
8804
|
+
exports.parse = publicApi.parse;
|
|
8805
|
+
exports.parseAllDocuments = publicApi.parseAllDocuments;
|
|
8806
|
+
exports.parseDocument = publicApi.parseDocument;
|
|
8807
|
+
exports.stringify = publicApi.stringify;
|
|
8808
|
+
exports.visit = visit.visit;
|
|
8809
|
+
exports.visitAsync = visit.visitAsync;
|
|
8810
|
+
});
|
|
8811
|
+
|
|
8763
8812
|
// node_modules/commander/esm.mjs
|
|
8764
8813
|
var import__ = __toESM(require_commander(), 1);
|
|
8765
8814
|
var {
|
|
@@ -8777,53 +8826,8 @@ var {
|
|
|
8777
8826
|
} = import__.default;
|
|
8778
8827
|
|
|
8779
8828
|
// src/cli.ts
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
// node_modules/yaml/dist/index.js
|
|
8783
|
-
var composer = require_composer();
|
|
8784
|
-
var Document = require_Document();
|
|
8785
|
-
var Schema = require_Schema();
|
|
8786
|
-
var errors = require_errors();
|
|
8787
|
-
var Alias = require_Alias();
|
|
8788
|
-
var identity = require_identity();
|
|
8789
|
-
var Pair = require_Pair();
|
|
8790
|
-
var Scalar = require_Scalar();
|
|
8791
|
-
var YAMLMap = require_YAMLMap();
|
|
8792
|
-
var YAMLSeq = require_YAMLSeq();
|
|
8793
|
-
var cst = require_cst();
|
|
8794
|
-
var lexer = require_lexer();
|
|
8795
|
-
var lineCounter = require_line_counter();
|
|
8796
|
-
var parser = require_parser();
|
|
8797
|
-
var publicApi = require_public_api();
|
|
8798
|
-
var visit = require_visit();
|
|
8799
|
-
var $Composer = composer.Composer;
|
|
8800
|
-
var $Document = Document.Document;
|
|
8801
|
-
var $Schema = Schema.Schema;
|
|
8802
|
-
var $YAMLError = errors.YAMLError;
|
|
8803
|
-
var $YAMLParseError = errors.YAMLParseError;
|
|
8804
|
-
var $YAMLWarning = errors.YAMLWarning;
|
|
8805
|
-
var $Alias = Alias.Alias;
|
|
8806
|
-
var $isAlias = identity.isAlias;
|
|
8807
|
-
var $isCollection = identity.isCollection;
|
|
8808
|
-
var $isDocument = identity.isDocument;
|
|
8809
|
-
var $isMap = identity.isMap;
|
|
8810
|
-
var $isNode = identity.isNode;
|
|
8811
|
-
var $isPair = identity.isPair;
|
|
8812
|
-
var $isScalar = identity.isScalar;
|
|
8813
|
-
var $isSeq = identity.isSeq;
|
|
8814
|
-
var $Pair = Pair.Pair;
|
|
8815
|
-
var $Scalar = Scalar.Scalar;
|
|
8816
|
-
var $YAMLMap = YAMLMap.YAMLMap;
|
|
8817
|
-
var $YAMLSeq = YAMLSeq.YAMLSeq;
|
|
8818
|
-
var $Lexer = lexer.Lexer;
|
|
8819
|
-
var $LineCounter = lineCounter.LineCounter;
|
|
8820
|
-
var $Parser = parser.Parser;
|
|
8821
|
-
var $parse = publicApi.parse;
|
|
8822
|
-
var $parseAllDocuments = publicApi.parseAllDocuments;
|
|
8823
|
-
var $parseDocument = publicApi.parseDocument;
|
|
8824
|
-
var $stringify = publicApi.stringify;
|
|
8825
|
-
var $visit = visit.visit;
|
|
8826
|
-
var $visitAsync = visit.visitAsync;
|
|
8829
|
+
var import_yaml2 = __toESM(require_dist(), 1);
|
|
8830
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
|
|
8827
8831
|
|
|
8828
8832
|
// src/types.ts
|
|
8829
8833
|
var DEFAULT_TOOLS = ["Read", "Edit", "Write", "Glob", "Grep"];
|
|
@@ -8831,9 +8835,145 @@ var DEFAULT_TIMEOUT = 300;
|
|
|
8831
8835
|
var DEFAULT_STALL_TIMEOUT = 120;
|
|
8832
8836
|
var DEFAULT_RETRY_COUNT = 3;
|
|
8833
8837
|
var DEFAULT_RETRY_DELAY = 5;
|
|
8834
|
-
var DEFAULT_VERIFY_PROMPT = "
|
|
8838
|
+
var DEFAULT_VERIFY_PROMPT = "Review what you just implemented. Check for correctness, completeness, and compile errors. Fix any issues you find.";
|
|
8835
8839
|
var DEFAULT_STATE_FILE = ".overnight-state.json";
|
|
8840
|
+
var DEFAULT_GOAL_STATE_FILE = ".overnight-goal-state.json";
|
|
8836
8841
|
var DEFAULT_NTFY_TOPIC = "overnight";
|
|
8842
|
+
var DEFAULT_MAX_TURNS = 100;
|
|
8843
|
+
var DEFAULT_MAX_ITERATIONS = 20;
|
|
8844
|
+
var DEFAULT_CONVERGENCE_THRESHOLD = 3;
|
|
8845
|
+
var DEFAULT_DENY_PATTERNS = [
|
|
8846
|
+
"**/.env",
|
|
8847
|
+
"**/.env.*",
|
|
8848
|
+
"**/.git/config",
|
|
8849
|
+
"**/credentials*",
|
|
8850
|
+
"**/*.key",
|
|
8851
|
+
"**/*.pem",
|
|
8852
|
+
"**/*.p12",
|
|
8853
|
+
"**/id_rsa*",
|
|
8854
|
+
"**/id_ed25519*",
|
|
8855
|
+
"**/.ssh/*",
|
|
8856
|
+
"**/.aws/*",
|
|
8857
|
+
"**/.npmrc",
|
|
8858
|
+
"**/.netrc"
|
|
8859
|
+
];
|
|
8860
|
+
|
|
8861
|
+
// src/security.ts
|
|
8862
|
+
import { appendFileSync } from "fs";
|
|
8863
|
+
import { resolve, relative, isAbsolute } from "path";
|
|
8864
|
+
function matchesPattern(filePath, pattern) {
|
|
8865
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
8866
|
+
let regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/{{GLOBSTAR}}/g, ".*");
|
|
8867
|
+
if (!pattern.startsWith("/")) {
|
|
8868
|
+
regex = `(^|/)${regex}`;
|
|
8869
|
+
}
|
|
8870
|
+
return new RegExp(regex + "$").test(normalizedPath);
|
|
8871
|
+
}
|
|
8872
|
+
function isPathWithinSandbox(filePath, sandboxDir) {
|
|
8873
|
+
const absolutePath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
|
|
8874
|
+
const absoluteSandbox = isAbsolute(sandboxDir) ? sandboxDir : resolve(process.cwd(), sandboxDir);
|
|
8875
|
+
const relativePath = relative(absoluteSandbox, absolutePath);
|
|
8876
|
+
return !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
8877
|
+
}
|
|
8878
|
+
function isPathDenied(filePath, denyPatterns) {
|
|
8879
|
+
for (const pattern of denyPatterns) {
|
|
8880
|
+
if (matchesPattern(filePath, pattern)) {
|
|
8881
|
+
return pattern;
|
|
8882
|
+
}
|
|
8883
|
+
}
|
|
8884
|
+
return null;
|
|
8885
|
+
}
|
|
8886
|
+
function createSecurityHooks(config) {
|
|
8887
|
+
const sandboxDir = config.sandbox_dir;
|
|
8888
|
+
const denyPatterns = config.deny_patterns ?? DEFAULT_DENY_PATTERNS;
|
|
8889
|
+
const auditLog = config.audit_log;
|
|
8890
|
+
const preToolUseHook = async (input, _toolUseId, _context) => {
|
|
8891
|
+
const hookEventName = input.hook_event_name;
|
|
8892
|
+
if (hookEventName !== "PreToolUse")
|
|
8893
|
+
return {};
|
|
8894
|
+
const toolName = input.tool_name;
|
|
8895
|
+
const toolInput = input.tool_input;
|
|
8896
|
+
let filePath;
|
|
8897
|
+
if (toolName === "Read" || toolName === "Write" || toolName === "Edit") {
|
|
8898
|
+
filePath = toolInput.file_path;
|
|
8899
|
+
} else if (toolName === "Glob" || toolName === "Grep") {
|
|
8900
|
+
filePath = toolInput.path;
|
|
8901
|
+
} else if (toolName === "Bash") {
|
|
8902
|
+
const command = toolInput.command;
|
|
8903
|
+
if (auditLog) {
|
|
8904
|
+
const timestamp = new Date().toISOString();
|
|
8905
|
+
appendFileSync(auditLog, `${timestamp} [BASH] ${command}
|
|
8906
|
+
`);
|
|
8907
|
+
}
|
|
8908
|
+
return {};
|
|
8909
|
+
}
|
|
8910
|
+
if (!filePath)
|
|
8911
|
+
return {};
|
|
8912
|
+
if (sandboxDir && !isPathWithinSandbox(filePath, sandboxDir)) {
|
|
8913
|
+
return {
|
|
8914
|
+
hookSpecificOutput: {
|
|
8915
|
+
hookEventName: "PreToolUse",
|
|
8916
|
+
permissionDecision: "deny",
|
|
8917
|
+
permissionDecisionReason: `Path "${filePath}" is outside sandbox directory "${sandboxDir}"`
|
|
8918
|
+
}
|
|
8919
|
+
};
|
|
8920
|
+
}
|
|
8921
|
+
const matchedPattern = isPathDenied(filePath, denyPatterns);
|
|
8922
|
+
if (matchedPattern) {
|
|
8923
|
+
return {
|
|
8924
|
+
hookSpecificOutput: {
|
|
8925
|
+
hookEventName: "PreToolUse",
|
|
8926
|
+
permissionDecision: "deny",
|
|
8927
|
+
permissionDecisionReason: `Path "${filePath}" matches deny pattern "${matchedPattern}"`
|
|
8928
|
+
}
|
|
8929
|
+
};
|
|
8930
|
+
}
|
|
8931
|
+
return {};
|
|
8932
|
+
};
|
|
8933
|
+
const postToolUseHook = async (input, _toolUseId, _context) => {
|
|
8934
|
+
if (!auditLog)
|
|
8935
|
+
return {};
|
|
8936
|
+
const hookEventName = input.hook_event_name;
|
|
8937
|
+
if (hookEventName !== "PostToolUse")
|
|
8938
|
+
return {};
|
|
8939
|
+
const toolName = input.tool_name;
|
|
8940
|
+
const toolInput = input.tool_input;
|
|
8941
|
+
const timestamp = new Date().toISOString();
|
|
8942
|
+
let logEntry = `${timestamp} [${toolName}]`;
|
|
8943
|
+
if (toolName === "Read" || toolName === "Write" || toolName === "Edit") {
|
|
8944
|
+
logEntry += ` ${toolInput.file_path}`;
|
|
8945
|
+
} else if (toolName === "Glob") {
|
|
8946
|
+
logEntry += ` pattern=${toolInput.pattern} path=${toolInput.path ?? "."}`;
|
|
8947
|
+
} else if (toolName === "Grep") {
|
|
8948
|
+
logEntry += ` pattern=${toolInput.pattern}`;
|
|
8949
|
+
}
|
|
8950
|
+
appendFileSync(auditLog, logEntry + `
|
|
8951
|
+
`);
|
|
8952
|
+
return {};
|
|
8953
|
+
};
|
|
8954
|
+
return {
|
|
8955
|
+
PreToolUse: [
|
|
8956
|
+
{ matcher: "Read|Write|Edit|Glob|Grep|Bash", hooks: [preToolUseHook] }
|
|
8957
|
+
],
|
|
8958
|
+
PostToolUse: [
|
|
8959
|
+
{ matcher: "Read|Write|Edit|Glob|Grep|Bash", hooks: [postToolUseHook] }
|
|
8960
|
+
]
|
|
8961
|
+
};
|
|
8962
|
+
}
|
|
8963
|
+
function validateSecurityConfig(config) {
|
|
8964
|
+
if (config.sandbox_dir) {
|
|
8965
|
+
const resolved = isAbsolute(config.sandbox_dir) ? config.sandbox_dir : resolve(process.cwd(), config.sandbox_dir);
|
|
8966
|
+
console.log(` Sandbox: ${resolved}`);
|
|
8967
|
+
}
|
|
8968
|
+
const denyPatterns = config.deny_patterns ?? DEFAULT_DENY_PATTERNS;
|
|
8969
|
+
console.log(` Deny patterns: ${denyPatterns.length} patterns`);
|
|
8970
|
+
if (config.max_turns) {
|
|
8971
|
+
console.log(` Max turns: ${config.max_turns}`);
|
|
8972
|
+
}
|
|
8973
|
+
if (config.audit_log) {
|
|
8974
|
+
console.log(` Audit log: ${config.audit_log}`);
|
|
8975
|
+
}
|
|
8976
|
+
}
|
|
8837
8977
|
|
|
8838
8978
|
// node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs
|
|
8839
8979
|
import { join as join5 } from "path";
|
|
@@ -8850,7 +8990,7 @@ import { cwd } from "process";
|
|
|
8850
8990
|
import { realpathSync as realpathSync2 } from "fs";
|
|
8851
8991
|
import { randomUUID } from "crypto";
|
|
8852
8992
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8853
|
-
import { appendFileSync as
|
|
8993
|
+
import { appendFileSync as appendFileSync22, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
8854
8994
|
import { join as join3 } from "path";
|
|
8855
8995
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
8856
8996
|
var __create2 = Object.create;
|
|
@@ -11724,7 +11864,7 @@ var require_compile = __commonJS2((exports) => {
|
|
|
11724
11864
|
const schOrFunc = root2.refs[ref];
|
|
11725
11865
|
if (schOrFunc)
|
|
11726
11866
|
return schOrFunc;
|
|
11727
|
-
let _sch =
|
|
11867
|
+
let _sch = resolve2.call(this, root2, ref);
|
|
11728
11868
|
if (_sch === undefined) {
|
|
11729
11869
|
const schema = (_a = root2.localRefs) === null || _a === undefined ? undefined : _a[ref];
|
|
11730
11870
|
const { schemaId } = this.opts;
|
|
@@ -11751,7 +11891,7 @@ var require_compile = __commonJS2((exports) => {
|
|
|
11751
11891
|
function sameSchemaEnv(s1, s2) {
|
|
11752
11892
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
11753
11893
|
}
|
|
11754
|
-
function
|
|
11894
|
+
function resolve2(root2, ref) {
|
|
11755
11895
|
let sch;
|
|
11756
11896
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
11757
11897
|
ref = sch;
|
|
@@ -12249,54 +12389,54 @@ var require_fast_uri = __commonJS2((exports, module) => {
|
|
|
12249
12389
|
}
|
|
12250
12390
|
return uri;
|
|
12251
12391
|
}
|
|
12252
|
-
function
|
|
12392
|
+
function resolve2(baseURI, relativeURI, options) {
|
|
12253
12393
|
const schemelessOptions = Object.assign({ scheme: "null" }, options);
|
|
12254
12394
|
const resolved = resolveComponents(parse6(baseURI, schemelessOptions), parse6(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
12255
12395
|
return serialize(resolved, { ...schemelessOptions, skipEscape: true });
|
|
12256
12396
|
}
|
|
12257
|
-
function resolveComponents(base,
|
|
12397
|
+
function resolveComponents(base, relative2, options, skipNormalization) {
|
|
12258
12398
|
const target = {};
|
|
12259
12399
|
if (!skipNormalization) {
|
|
12260
12400
|
base = parse6(serialize(base, options), options);
|
|
12261
|
-
|
|
12401
|
+
relative2 = parse6(serialize(relative2, options), options);
|
|
12262
12402
|
}
|
|
12263
12403
|
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 =
|
|
12404
|
+
if (!options.tolerant && relative2.scheme) {
|
|
12405
|
+
target.scheme = relative2.scheme;
|
|
12406
|
+
target.userinfo = relative2.userinfo;
|
|
12407
|
+
target.host = relative2.host;
|
|
12408
|
+
target.port = relative2.port;
|
|
12409
|
+
target.path = removeDotSegments(relative2.path || "");
|
|
12410
|
+
target.query = relative2.query;
|
|
12271
12411
|
} else {
|
|
12272
|
-
if (
|
|
12273
|
-
target.userinfo =
|
|
12274
|
-
target.host =
|
|
12275
|
-
target.port =
|
|
12276
|
-
target.path = removeDotSegments(
|
|
12277
|
-
target.query =
|
|
12412
|
+
if (relative2.userinfo !== undefined || relative2.host !== undefined || relative2.port !== undefined) {
|
|
12413
|
+
target.userinfo = relative2.userinfo;
|
|
12414
|
+
target.host = relative2.host;
|
|
12415
|
+
target.port = relative2.port;
|
|
12416
|
+
target.path = removeDotSegments(relative2.path || "");
|
|
12417
|
+
target.query = relative2.query;
|
|
12278
12418
|
} else {
|
|
12279
|
-
if (!
|
|
12419
|
+
if (!relative2.path) {
|
|
12280
12420
|
target.path = base.path;
|
|
12281
|
-
if (
|
|
12282
|
-
target.query =
|
|
12421
|
+
if (relative2.query !== undefined) {
|
|
12422
|
+
target.query = relative2.query;
|
|
12283
12423
|
} else {
|
|
12284
12424
|
target.query = base.query;
|
|
12285
12425
|
}
|
|
12286
12426
|
} else {
|
|
12287
|
-
if (
|
|
12288
|
-
target.path = removeDotSegments(
|
|
12427
|
+
if (relative2.path.charAt(0) === "/") {
|
|
12428
|
+
target.path = removeDotSegments(relative2.path);
|
|
12289
12429
|
} else {
|
|
12290
12430
|
if ((base.userinfo !== undefined || base.host !== undefined || base.port !== undefined) && !base.path) {
|
|
12291
|
-
target.path = "/" +
|
|
12431
|
+
target.path = "/" + relative2.path;
|
|
12292
12432
|
} else if (!base.path) {
|
|
12293
|
-
target.path =
|
|
12433
|
+
target.path = relative2.path;
|
|
12294
12434
|
} else {
|
|
12295
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
12435
|
+
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
|
|
12296
12436
|
}
|
|
12297
12437
|
target.path = removeDotSegments(target.path);
|
|
12298
12438
|
}
|
|
12299
|
-
target.query =
|
|
12439
|
+
target.query = relative2.query;
|
|
12300
12440
|
}
|
|
12301
12441
|
target.userinfo = base.userinfo;
|
|
12302
12442
|
target.host = base.host;
|
|
@@ -12304,7 +12444,7 @@ var require_fast_uri = __commonJS2((exports, module) => {
|
|
|
12304
12444
|
}
|
|
12305
12445
|
target.scheme = base.scheme;
|
|
12306
12446
|
}
|
|
12307
|
-
target.fragment =
|
|
12447
|
+
target.fragment = relative2.fragment;
|
|
12308
12448
|
return target;
|
|
12309
12449
|
}
|
|
12310
12450
|
function equal(uriA, uriB, options) {
|
|
@@ -12482,7 +12622,7 @@ var require_fast_uri = __commonJS2((exports, module) => {
|
|
|
12482
12622
|
var fastUri = {
|
|
12483
12623
|
SCHEMES,
|
|
12484
12624
|
normalize,
|
|
12485
|
-
resolve,
|
|
12625
|
+
resolve: resolve2,
|
|
12486
12626
|
resolveComponents,
|
|
12487
12627
|
equal,
|
|
12488
12628
|
serialize,
|
|
@@ -15144,7 +15284,7 @@ var require_limit = __commonJS2((exports) => {
|
|
|
15144
15284
|
};
|
|
15145
15285
|
exports.default = formatLimitPlugin;
|
|
15146
15286
|
});
|
|
15147
|
-
var
|
|
15287
|
+
var require_dist2 = __commonJS2((exports, module) => {
|
|
15148
15288
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15149
15289
|
var formats_1 = require_formats();
|
|
15150
15290
|
var limit_1 = require_limit();
|
|
@@ -16072,7 +16212,7 @@ function logForSdkDebugging(message) {
|
|
|
16072
16212
|
const timestamp = new Date().toISOString();
|
|
16073
16213
|
const output = `${timestamp} ${message}
|
|
16074
16214
|
`;
|
|
16075
|
-
|
|
16215
|
+
appendFileSync22(path, output);
|
|
16076
16216
|
}
|
|
16077
16217
|
function mergeSandboxIntoExtraArgs(extraArgs, sandbox) {
|
|
16078
16218
|
const effectiveExtraArgs = { ...extraArgs };
|
|
@@ -16474,7 +16614,7 @@ class ProcessTransport {
|
|
|
16474
16614
|
}
|
|
16475
16615
|
return;
|
|
16476
16616
|
}
|
|
16477
|
-
return new Promise((
|
|
16617
|
+
return new Promise((resolve2, reject) => {
|
|
16478
16618
|
const exitHandler = (code, signal) => {
|
|
16479
16619
|
if (this.abortController.signal.aborted) {
|
|
16480
16620
|
reject(new AbortError("Operation aborted"));
|
|
@@ -16484,7 +16624,7 @@ class ProcessTransport {
|
|
|
16484
16624
|
if (error) {
|
|
16485
16625
|
reject(error);
|
|
16486
16626
|
} else {
|
|
16487
|
-
|
|
16627
|
+
resolve2();
|
|
16488
16628
|
}
|
|
16489
16629
|
};
|
|
16490
16630
|
this.process.once("exit", exitHandler);
|
|
@@ -16535,17 +16675,17 @@ class Stream {
|
|
|
16535
16675
|
if (this.hasError) {
|
|
16536
16676
|
return Promise.reject(this.hasError);
|
|
16537
16677
|
}
|
|
16538
|
-
return new Promise((
|
|
16539
|
-
this.readResolve =
|
|
16678
|
+
return new Promise((resolve2, reject) => {
|
|
16679
|
+
this.readResolve = resolve2;
|
|
16540
16680
|
this.readReject = reject;
|
|
16541
16681
|
});
|
|
16542
16682
|
}
|
|
16543
16683
|
enqueue(value) {
|
|
16544
16684
|
if (this.readResolve) {
|
|
16545
|
-
const
|
|
16685
|
+
const resolve2 = this.readResolve;
|
|
16546
16686
|
this.readResolve = undefined;
|
|
16547
16687
|
this.readReject = undefined;
|
|
16548
|
-
|
|
16688
|
+
resolve2({ done: false, value });
|
|
16549
16689
|
} else {
|
|
16550
16690
|
this.queue.push(value);
|
|
16551
16691
|
}
|
|
@@ -16553,10 +16693,10 @@ class Stream {
|
|
|
16553
16693
|
done() {
|
|
16554
16694
|
this.isDone = true;
|
|
16555
16695
|
if (this.readResolve) {
|
|
16556
|
-
const
|
|
16696
|
+
const resolve2 = this.readResolve;
|
|
16557
16697
|
this.readResolve = undefined;
|
|
16558
16698
|
this.readReject = undefined;
|
|
16559
|
-
|
|
16699
|
+
resolve2({ done: true, value: undefined });
|
|
16560
16700
|
}
|
|
16561
16701
|
}
|
|
16562
16702
|
error(error) {
|
|
@@ -16886,10 +17026,10 @@ class Query {
|
|
|
16886
17026
|
type: "control_request",
|
|
16887
17027
|
request
|
|
16888
17028
|
};
|
|
16889
|
-
return new Promise((
|
|
17029
|
+
return new Promise((resolve2, reject) => {
|
|
16890
17030
|
this.pendingControlResponses.set(requestId, (response) => {
|
|
16891
17031
|
if (response.subtype === "success") {
|
|
16892
|
-
|
|
17032
|
+
resolve2(response);
|
|
16893
17033
|
} else {
|
|
16894
17034
|
reject(new Error(response.error));
|
|
16895
17035
|
if (response.pending_permission_requests) {
|
|
@@ -16979,15 +17119,15 @@ class Query {
|
|
|
16979
17119
|
logForDebugging(`[Query.waitForFirstResult] Result already received, returning immediately`);
|
|
16980
17120
|
return Promise.resolve();
|
|
16981
17121
|
}
|
|
16982
|
-
return new Promise((
|
|
17122
|
+
return new Promise((resolve2) => {
|
|
16983
17123
|
if (this.abortController?.signal.aborted) {
|
|
16984
|
-
|
|
17124
|
+
resolve2();
|
|
16985
17125
|
return;
|
|
16986
17126
|
}
|
|
16987
|
-
this.abortController?.signal.addEventListener("abort", () =>
|
|
17127
|
+
this.abortController?.signal.addEventListener("abort", () => resolve2(), {
|
|
16988
17128
|
once: true
|
|
16989
17129
|
});
|
|
16990
|
-
this.firstResultReceivedResolve =
|
|
17130
|
+
this.firstResultReceivedResolve = resolve2;
|
|
16991
17131
|
});
|
|
16992
17132
|
}
|
|
16993
17133
|
handleHookCallbacks(callbackId, input, toolUseID, abortSignal) {
|
|
@@ -17038,13 +17178,13 @@ class Query {
|
|
|
17038
17178
|
handleMcpControlRequest(serverName, mcpRequest, transport) {
|
|
17039
17179
|
const messageId = "id" in mcpRequest.message ? mcpRequest.message.id : null;
|
|
17040
17180
|
const key = `${serverName}:${messageId}`;
|
|
17041
|
-
return new Promise((
|
|
17181
|
+
return new Promise((resolve2, reject) => {
|
|
17042
17182
|
const cleanup = () => {
|
|
17043
17183
|
this.pendingMcpResponses.delete(key);
|
|
17044
17184
|
};
|
|
17045
17185
|
const resolveAndCleanup = (response) => {
|
|
17046
17186
|
cleanup();
|
|
17047
|
-
|
|
17187
|
+
resolve2(response);
|
|
17048
17188
|
};
|
|
17049
17189
|
const rejectAndCleanup = (error) => {
|
|
17050
17190
|
cleanup();
|
|
@@ -25296,7 +25436,7 @@ var ServerResultSchema = union([
|
|
|
25296
25436
|
var ignoreOverride = Symbol("Let zodToJsonSchema decide on which parser to use");
|
|
25297
25437
|
var ALPHA_NUMERIC = new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789");
|
|
25298
25438
|
var import_ajv = __toESM2(require_ajv(), 1);
|
|
25299
|
-
var import_ajv_formats = __toESM2(
|
|
25439
|
+
var import_ajv_formats = __toESM2(require_dist2(), 1);
|
|
25300
25440
|
var COMPLETABLE_SYMBOL = Symbol.for("mcp.completable");
|
|
25301
25441
|
var McpZodTypeKind;
|
|
25302
25442
|
(function(McpZodTypeKind2) {
|
|
@@ -25452,6 +25592,78 @@ function query({
|
|
|
25452
25592
|
|
|
25453
25593
|
// src/runner.ts
|
|
25454
25594
|
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
25595
|
+
import { execSync } from "child_process";
|
|
25596
|
+
import { createHash } from "crypto";
|
|
25597
|
+
var SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
25598
|
+
|
|
25599
|
+
class ProgressDisplay {
|
|
25600
|
+
interval = null;
|
|
25601
|
+
frame = 0;
|
|
25602
|
+
startTime = Date.now();
|
|
25603
|
+
currentActivity = "Working";
|
|
25604
|
+
lastToolUse = "";
|
|
25605
|
+
start(activity) {
|
|
25606
|
+
this.currentActivity = activity;
|
|
25607
|
+
this.startTime = Date.now();
|
|
25608
|
+
this.frame = 0;
|
|
25609
|
+
if (this.interval)
|
|
25610
|
+
return;
|
|
25611
|
+
this.interval = setInterval(() => {
|
|
25612
|
+
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
25613
|
+
const toolInfo = this.lastToolUse ? ` → ${this.lastToolUse}` : "";
|
|
25614
|
+
process.stdout.write(`\r\x1B[K${SPINNER_FRAMES[this.frame]} ${this.currentActivity} (${elapsed}s)${toolInfo}`);
|
|
25615
|
+
this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
|
|
25616
|
+
}, 100);
|
|
25617
|
+
}
|
|
25618
|
+
updateActivity(activity) {
|
|
25619
|
+
this.currentActivity = activity;
|
|
25620
|
+
}
|
|
25621
|
+
updateTool(toolName, detail) {
|
|
25622
|
+
this.lastToolUse = detail ? `${toolName}: ${detail}` : toolName;
|
|
25623
|
+
}
|
|
25624
|
+
stop(finalMessage) {
|
|
25625
|
+
if (this.interval) {
|
|
25626
|
+
clearInterval(this.interval);
|
|
25627
|
+
this.interval = null;
|
|
25628
|
+
}
|
|
25629
|
+
process.stdout.write("\r\x1B[K");
|
|
25630
|
+
if (finalMessage) {
|
|
25631
|
+
console.log(finalMessage);
|
|
25632
|
+
}
|
|
25633
|
+
}
|
|
25634
|
+
getElapsed() {
|
|
25635
|
+
return (Date.now() - this.startTime) / 1000;
|
|
25636
|
+
}
|
|
25637
|
+
}
|
|
25638
|
+
var claudeExecutablePath;
|
|
25639
|
+
function findClaudeExecutable() {
|
|
25640
|
+
if (claudeExecutablePath !== undefined)
|
|
25641
|
+
return claudeExecutablePath;
|
|
25642
|
+
if (process.env.CLAUDE_CODE_PATH) {
|
|
25643
|
+
claudeExecutablePath = process.env.CLAUDE_CODE_PATH;
|
|
25644
|
+
return claudeExecutablePath;
|
|
25645
|
+
}
|
|
25646
|
+
try {
|
|
25647
|
+
const cmd = process.platform === "win32" ? "where claude" : "which claude";
|
|
25648
|
+
claudeExecutablePath = execSync(cmd, { encoding: "utf-8" }).trim().split(`
|
|
25649
|
+
`)[0];
|
|
25650
|
+
return claudeExecutablePath;
|
|
25651
|
+
} catch {
|
|
25652
|
+
const commonPaths = [
|
|
25653
|
+
"/usr/local/bin/claude",
|
|
25654
|
+
"/opt/homebrew/bin/claude",
|
|
25655
|
+
`${process.env.HOME}/.local/bin/claude`,
|
|
25656
|
+
`${process.env.HOME}/.nvm/versions/node/v22.12.0/bin/claude`
|
|
25657
|
+
];
|
|
25658
|
+
for (const p of commonPaths) {
|
|
25659
|
+
if (existsSync3(p)) {
|
|
25660
|
+
claudeExecutablePath = p;
|
|
25661
|
+
return claudeExecutablePath;
|
|
25662
|
+
}
|
|
25663
|
+
}
|
|
25664
|
+
}
|
|
25665
|
+
return;
|
|
25666
|
+
}
|
|
25455
25667
|
function isRetryableError(error2) {
|
|
25456
25668
|
const errorStr = error2.message.toLowerCase();
|
|
25457
25669
|
const retryablePatterns = [
|
|
@@ -25469,7 +25681,7 @@ function isRetryableError(error2) {
|
|
|
25469
25681
|
return retryablePatterns.some((pattern) => errorStr.includes(pattern));
|
|
25470
25682
|
}
|
|
25471
25683
|
async function sleep(ms) {
|
|
25472
|
-
return new Promise((
|
|
25684
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
25473
25685
|
}
|
|
25474
25686
|
async function runWithTimeout(promise, timeoutMs) {
|
|
25475
25687
|
let timeoutId;
|
|
@@ -25485,19 +25697,71 @@ async function runWithTimeout(promise, timeoutMs) {
|
|
|
25485
25697
|
throw e;
|
|
25486
25698
|
}
|
|
25487
25699
|
}
|
|
25488
|
-
|
|
25700
|
+
function getToolDetail(toolName, toolInput) {
|
|
25701
|
+
switch (toolName) {
|
|
25702
|
+
case "Read":
|
|
25703
|
+
case "Write":
|
|
25704
|
+
case "Edit":
|
|
25705
|
+
const filePath = toolInput.file_path;
|
|
25706
|
+
if (filePath) {
|
|
25707
|
+
return filePath.split("/").pop() || filePath;
|
|
25708
|
+
}
|
|
25709
|
+
break;
|
|
25710
|
+
case "Glob":
|
|
25711
|
+
return toolInput.pattern || "";
|
|
25712
|
+
case "Grep":
|
|
25713
|
+
return toolInput.pattern?.slice(0, 20) || "";
|
|
25714
|
+
case "Bash":
|
|
25715
|
+
const cmd = toolInput.command || "";
|
|
25716
|
+
return cmd.slice(0, 30) + (cmd.length > 30 ? "..." : "");
|
|
25717
|
+
}
|
|
25718
|
+
return "";
|
|
25719
|
+
}
|
|
25720
|
+
async function collectResultWithProgress(prompt, options, progress, onSessionId) {
|
|
25489
25721
|
let sessionId;
|
|
25490
25722
|
let result;
|
|
25491
|
-
|
|
25492
|
-
|
|
25493
|
-
|
|
25494
|
-
|
|
25495
|
-
|
|
25723
|
+
let lastError;
|
|
25724
|
+
try {
|
|
25725
|
+
const conversation = query({ prompt, options });
|
|
25726
|
+
for await (const message of conversation) {
|
|
25727
|
+
if (process.env.OVERNIGHT_DEBUG) {
|
|
25728
|
+
console.error(`
|
|
25729
|
+
[DEBUG] message.type=${message.type}, keys=${Object.keys(message).join(",")}`);
|
|
25730
|
+
}
|
|
25731
|
+
if (message.type === "result") {
|
|
25732
|
+
sessionId = message.session_id;
|
|
25733
|
+
if (message.subtype === "success") {
|
|
25734
|
+
result = message.result;
|
|
25735
|
+
}
|
|
25736
|
+
} else if (message.type === "assistant" && "message" in message) {
|
|
25737
|
+
const assistantMsg = message.message;
|
|
25738
|
+
if (assistantMsg.content) {
|
|
25739
|
+
for (const block of assistantMsg.content) {
|
|
25740
|
+
if (process.env.OVERNIGHT_DEBUG) {
|
|
25741
|
+
console.error(`[DEBUG] content block: type=${block.type}, name=${block.name}`);
|
|
25742
|
+
}
|
|
25743
|
+
if (block.type === "tool_use" && block.name) {
|
|
25744
|
+
const detail = block.input ? getToolDetail(block.name, block.input) : "";
|
|
25745
|
+
progress.updateTool(block.name, detail);
|
|
25746
|
+
}
|
|
25747
|
+
}
|
|
25748
|
+
}
|
|
25749
|
+
} else if (message.type === "system" && "subtype" in message) {
|
|
25750
|
+
if (message.subtype === "init") {
|
|
25751
|
+
sessionId = message.session_id;
|
|
25752
|
+
if (sessionId && onSessionId) {
|
|
25753
|
+
onSessionId(sessionId);
|
|
25754
|
+
}
|
|
25755
|
+
}
|
|
25756
|
+
}
|
|
25496
25757
|
}
|
|
25758
|
+
} catch (e) {
|
|
25759
|
+
lastError = e.message;
|
|
25760
|
+
throw e;
|
|
25497
25761
|
}
|
|
25498
|
-
return { sessionId, result };
|
|
25762
|
+
return { sessionId, result, error: lastError };
|
|
25499
25763
|
}
|
|
25500
|
-
async function runJob(config2, log) {
|
|
25764
|
+
async function runJob(config2, log, options) {
|
|
25501
25765
|
const startTime = Date.now();
|
|
25502
25766
|
const tools = config2.allowed_tools ?? DEFAULT_TOOLS;
|
|
25503
25767
|
const timeout = (config2.timeout_seconds ?? DEFAULT_TIMEOUT) * 1000;
|
|
@@ -25505,31 +25769,71 @@ async function runJob(config2, log) {
|
|
|
25505
25769
|
const retryDelay = config2.retry_delay ?? DEFAULT_RETRY_DELAY;
|
|
25506
25770
|
const verifyPrompt = config2.verify_prompt ?? DEFAULT_VERIFY_PROMPT;
|
|
25507
25771
|
let retriesUsed = 0;
|
|
25772
|
+
let resumeSessionId = options?.resumeSessionId;
|
|
25508
25773
|
const logMsg = (msg) => log?.(msg);
|
|
25509
|
-
|
|
25774
|
+
const progress = new ProgressDisplay;
|
|
25775
|
+
const claudePath = findClaudeExecutable();
|
|
25776
|
+
if (!claudePath) {
|
|
25777
|
+
logMsg("\x1B[31m✗ Error: Could not find 'claude' CLI.\x1B[0m");
|
|
25778
|
+
logMsg("\x1B[33m Install it with:\x1B[0m");
|
|
25779
|
+
logMsg(" curl -fsSL https://claude.ai/install.sh | bash");
|
|
25780
|
+
logMsg("\x1B[33m Or set CLAUDE_CODE_PATH environment variable.\x1B[0m");
|
|
25781
|
+
return {
|
|
25782
|
+
task: config2.prompt,
|
|
25783
|
+
status: "failed",
|
|
25784
|
+
error: "Claude CLI not found. Install with: curl -fsSL https://claude.ai/install.sh | bash",
|
|
25785
|
+
duration_seconds: 0,
|
|
25786
|
+
verified: false,
|
|
25787
|
+
retries: 0
|
|
25788
|
+
};
|
|
25789
|
+
}
|
|
25790
|
+
if (process.env.OVERNIGHT_DEBUG) {
|
|
25791
|
+
logMsg(`\x1B[2mDebug: Claude path = ${claudePath}\x1B[0m`);
|
|
25792
|
+
}
|
|
25793
|
+
const taskPreview = config2.prompt.slice(0, 60) + (config2.prompt.length > 60 ? "..." : "");
|
|
25794
|
+
if (resumeSessionId) {
|
|
25795
|
+
logMsg(`\x1B[36m▶\x1B[0m Resuming: ${taskPreview}`);
|
|
25796
|
+
} else {
|
|
25797
|
+
logMsg(`\x1B[36m▶\x1B[0m ${taskPreview}`);
|
|
25798
|
+
}
|
|
25799
|
+
let sessionId;
|
|
25510
25800
|
for (let attempt = 0;attempt <= retryCount; attempt++) {
|
|
25511
25801
|
try {
|
|
25512
|
-
const
|
|
25802
|
+
const securityHooks = config2.security ? createSecurityHooks(config2.security) : undefined;
|
|
25803
|
+
const sdkOptions = {
|
|
25513
25804
|
allowedTools: tools,
|
|
25514
25805
|
permissionMode: "acceptEdits",
|
|
25515
|
-
...
|
|
25806
|
+
...claudePath && { pathToClaudeCodeExecutable: claudePath },
|
|
25807
|
+
...config2.working_dir && { cwd: config2.working_dir },
|
|
25808
|
+
...config2.security?.max_turns && { maxTurns: config2.security.max_turns },
|
|
25809
|
+
...securityHooks && { hooks: securityHooks },
|
|
25810
|
+
...resumeSessionId && { resume: resumeSessionId }
|
|
25516
25811
|
};
|
|
25517
|
-
let sessionId;
|
|
25518
25812
|
let result;
|
|
25813
|
+
const prompt = resumeSessionId ? "Continue where you left off. Complete the original task." : config2.prompt;
|
|
25814
|
+
progress.start(resumeSessionId ? "Resuming" : "Working");
|
|
25519
25815
|
try {
|
|
25520
|
-
const collected = await runWithTimeout(
|
|
25816
|
+
const collected = await runWithTimeout(collectResultWithProgress(prompt, sdkOptions, progress, (id) => {
|
|
25817
|
+
sessionId = id;
|
|
25818
|
+
options?.onSessionId?.(id);
|
|
25819
|
+
}), timeout);
|
|
25521
25820
|
sessionId = collected.sessionId;
|
|
25522
25821
|
result = collected.result;
|
|
25822
|
+
progress.stop();
|
|
25523
25823
|
} catch (e) {
|
|
25824
|
+
progress.stop();
|
|
25524
25825
|
if (e.message === "TIMEOUT") {
|
|
25525
25826
|
if (attempt < retryCount) {
|
|
25526
25827
|
retriesUsed = attempt + 1;
|
|
25828
|
+
if (sessionId) {
|
|
25829
|
+
resumeSessionId = sessionId;
|
|
25830
|
+
}
|
|
25527
25831
|
const delay = retryDelay * Math.pow(2, attempt);
|
|
25528
|
-
logMsg(
|
|
25832
|
+
logMsg(`\x1B[33m⚠ Timeout after ${config2.timeout_seconds ?? DEFAULT_TIMEOUT}s, retrying in ${delay}s (${attempt + 1}/${retryCount})\x1B[0m`);
|
|
25529
25833
|
await sleep(delay * 1000);
|
|
25530
25834
|
continue;
|
|
25531
25835
|
}
|
|
25532
|
-
logMsg(
|
|
25836
|
+
logMsg(`\x1B[31m✗ Timeout after ${config2.timeout_seconds ?? DEFAULT_TIMEOUT}s (exhausted retries)\x1B[0m`);
|
|
25533
25837
|
return {
|
|
25534
25838
|
task: config2.prompt,
|
|
25535
25839
|
status: "timeout",
|
|
@@ -25542,36 +25846,49 @@ async function runJob(config2, log) {
|
|
|
25542
25846
|
throw e;
|
|
25543
25847
|
}
|
|
25544
25848
|
if (config2.verify !== false && sessionId) {
|
|
25545
|
-
|
|
25849
|
+
progress.start("Verifying");
|
|
25546
25850
|
const verifyOptions = {
|
|
25851
|
+
allowedTools: tools,
|
|
25547
25852
|
resume: sessionId,
|
|
25548
|
-
permissionMode: "acceptEdits"
|
|
25853
|
+
permissionMode: "acceptEdits",
|
|
25854
|
+
...claudePath && { pathToClaudeCodeExecutable: claudePath },
|
|
25855
|
+
...config2.working_dir && { cwd: config2.working_dir },
|
|
25856
|
+
...config2.security?.max_turns && { maxTurns: config2.security.max_turns }
|
|
25549
25857
|
};
|
|
25858
|
+
const fixPrompt = verifyPrompt + " If you find any issues, fix them now. Only report issues you cannot fix.";
|
|
25550
25859
|
try {
|
|
25551
|
-
const verifyResult = await runWithTimeout(
|
|
25552
|
-
|
|
25553
|
-
|
|
25554
|
-
|
|
25860
|
+
const verifyResult = await runWithTimeout(collectResultWithProgress(fixPrompt, verifyOptions, progress, (id) => {
|
|
25861
|
+
sessionId = id;
|
|
25862
|
+
options?.onSessionId?.(id);
|
|
25863
|
+
}), timeout / 2);
|
|
25864
|
+
progress.stop();
|
|
25865
|
+
if (verifyResult.result) {
|
|
25866
|
+
result = verifyResult.result;
|
|
25867
|
+
}
|
|
25868
|
+
const unfixableWords = ["cannot fix", "unable to", "blocked by", "requires manual"];
|
|
25869
|
+
if (verifyResult.result && unfixableWords.some((word) => verifyResult.result.toLowerCase().includes(word))) {
|
|
25870
|
+
logMsg(`\x1B[33m⚠ Verification found unfixable issues\x1B[0m`);
|
|
25555
25871
|
return {
|
|
25556
25872
|
task: config2.prompt,
|
|
25557
25873
|
status: "verification_failed",
|
|
25558
25874
|
result,
|
|
25559
|
-
error: `
|
|
25875
|
+
error: `Unfixable issues: ${verifyResult.result}`,
|
|
25560
25876
|
duration_seconds: (Date.now() - startTime) / 1000,
|
|
25561
25877
|
verified: false,
|
|
25562
25878
|
retries: retriesUsed
|
|
25563
25879
|
};
|
|
25564
25880
|
}
|
|
25565
25881
|
} catch (e) {
|
|
25882
|
+
progress.stop();
|
|
25566
25883
|
if (e.message === "TIMEOUT") {
|
|
25567
|
-
logMsg("Verification timed out - continuing anyway");
|
|
25884
|
+
logMsg("\x1B[33m⚠ Verification timed out - continuing anyway\x1B[0m");
|
|
25568
25885
|
} else {
|
|
25569
25886
|
throw e;
|
|
25570
25887
|
}
|
|
25571
25888
|
}
|
|
25572
25889
|
}
|
|
25573
25890
|
const duration3 = (Date.now() - startTime) / 1000;
|
|
25574
|
-
logMsg(
|
|
25891
|
+
logMsg(`\x1B[32m✓ Completed in ${duration3.toFixed(1)}s\x1B[0m`);
|
|
25575
25892
|
return {
|
|
25576
25893
|
task: config2.prompt,
|
|
25577
25894
|
status: "success",
|
|
@@ -25581,16 +25898,20 @@ async function runJob(config2, log) {
|
|
|
25581
25898
|
retries: retriesUsed
|
|
25582
25899
|
};
|
|
25583
25900
|
} catch (e) {
|
|
25901
|
+
progress.stop();
|
|
25584
25902
|
const error2 = e;
|
|
25585
25903
|
if (isRetryableError(error2) && attempt < retryCount) {
|
|
25586
25904
|
retriesUsed = attempt + 1;
|
|
25905
|
+
if (sessionId) {
|
|
25906
|
+
resumeSessionId = sessionId;
|
|
25907
|
+
}
|
|
25587
25908
|
const delay = retryDelay * Math.pow(2, attempt);
|
|
25588
|
-
logMsg(
|
|
25909
|
+
logMsg(`\x1B[33m⚠ ${error2.message}, retrying in ${delay}s (${attempt + 1}/${retryCount})\x1B[0m`);
|
|
25589
25910
|
await sleep(delay * 1000);
|
|
25590
25911
|
continue;
|
|
25591
25912
|
}
|
|
25592
25913
|
const duration3 = (Date.now() - startTime) / 1000;
|
|
25593
|
-
logMsg(
|
|
25914
|
+
logMsg(`\x1B[31m✗ Failed: ${error2.message}\x1B[0m`);
|
|
25594
25915
|
return {
|
|
25595
25916
|
task: config2.prompt,
|
|
25596
25917
|
status: "failed",
|
|
@@ -25610,6 +25931,56 @@ async function runJob(config2, log) {
|
|
|
25610
25931
|
retries: retriesUsed
|
|
25611
25932
|
};
|
|
25612
25933
|
}
|
|
25934
|
+
function taskKey(config2) {
|
|
25935
|
+
if (config2.id)
|
|
25936
|
+
return config2.id;
|
|
25937
|
+
return createHash("sha256").update(config2.prompt).digest("hex").slice(0, 12);
|
|
25938
|
+
}
|
|
25939
|
+
function validateDag(configs) {
|
|
25940
|
+
const ids = new Set(configs.map((c) => c.id).filter((id) => Boolean(id)));
|
|
25941
|
+
for (const c of configs) {
|
|
25942
|
+
for (const dep of c.depends_on ?? []) {
|
|
25943
|
+
if (!ids.has(dep)) {
|
|
25944
|
+
return `Task "${c.id ?? c.prompt.slice(0, 40)}" depends on unknown id "${dep}"`;
|
|
25945
|
+
}
|
|
25946
|
+
}
|
|
25947
|
+
}
|
|
25948
|
+
const visited = new Set;
|
|
25949
|
+
const inStack = new Set;
|
|
25950
|
+
const idToConfig = new Map(configs.filter((c) => c.id).map((c) => [c.id, c]));
|
|
25951
|
+
function hasCycle(id) {
|
|
25952
|
+
if (inStack.has(id))
|
|
25953
|
+
return true;
|
|
25954
|
+
if (visited.has(id))
|
|
25955
|
+
return false;
|
|
25956
|
+
visited.add(id);
|
|
25957
|
+
inStack.add(id);
|
|
25958
|
+
const config2 = idToConfig.get(id);
|
|
25959
|
+
for (const dep of config2?.depends_on ?? []) {
|
|
25960
|
+
if (hasCycle(dep))
|
|
25961
|
+
return true;
|
|
25962
|
+
}
|
|
25963
|
+
inStack.delete(id);
|
|
25964
|
+
return false;
|
|
25965
|
+
}
|
|
25966
|
+
for (const id of ids) {
|
|
25967
|
+
if (hasCycle(id))
|
|
25968
|
+
return `Dependency cycle detected involving "${id}"`;
|
|
25969
|
+
}
|
|
25970
|
+
return null;
|
|
25971
|
+
}
|
|
25972
|
+
function depsReady(config2, completed) {
|
|
25973
|
+
if (!config2.depends_on || config2.depends_on.length === 0)
|
|
25974
|
+
return "ready";
|
|
25975
|
+
for (const dep of config2.depends_on) {
|
|
25976
|
+
const result = completed[dep];
|
|
25977
|
+
if (!result)
|
|
25978
|
+
return "waiting";
|
|
25979
|
+
if (result.status !== "success")
|
|
25980
|
+
return "blocked";
|
|
25981
|
+
}
|
|
25982
|
+
return "ready";
|
|
25983
|
+
}
|
|
25613
25984
|
function saveState(state, stateFile) {
|
|
25614
25985
|
writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
25615
25986
|
}
|
|
@@ -25624,26 +25995,84 @@ function clearState(stateFile) {
|
|
|
25624
25995
|
}
|
|
25625
25996
|
async function runJobsWithState(configs, options = {}) {
|
|
25626
25997
|
const stateFile = options.stateFile ?? DEFAULT_STATE_FILE;
|
|
25627
|
-
const
|
|
25628
|
-
|
|
25629
|
-
|
|
25630
|
-
|
|
25631
|
-
|
|
25998
|
+
const dagError = validateDag(configs);
|
|
25999
|
+
if (dagError) {
|
|
26000
|
+
options.log?.(`\x1B[31m✗ DAG error: ${dagError}\x1B[0m`);
|
|
26001
|
+
return [];
|
|
26002
|
+
}
|
|
26003
|
+
const state = loadState(stateFile) ?? {
|
|
26004
|
+
completed: {},
|
|
26005
|
+
timestamp: new Date().toISOString()
|
|
26006
|
+
};
|
|
26007
|
+
let currentConfigs = configs;
|
|
26008
|
+
while (true) {
|
|
26009
|
+
const notDone = currentConfigs.filter((c) => !(taskKey(c) in state.completed));
|
|
26010
|
+
if (notDone.length === 0)
|
|
26011
|
+
break;
|
|
26012
|
+
const ready = notDone.filter((c) => depsReady(c, state.completed) === "ready");
|
|
26013
|
+
const blocked = notDone.filter((c) => depsReady(c, state.completed) === "blocked");
|
|
26014
|
+
for (const bc of blocked) {
|
|
26015
|
+
const key2 = taskKey(bc);
|
|
26016
|
+
if (key2 in state.completed)
|
|
26017
|
+
continue;
|
|
26018
|
+
const failedDeps = (bc.depends_on ?? []).filter((dep) => state.completed[dep] && state.completed[dep].status !== "success");
|
|
26019
|
+
const label2 = bc.id ?? bc.prompt.slice(0, 40);
|
|
26020
|
+
options.log?.(`
|
|
26021
|
+
\x1B[31m✗ Skipping "${label2}" — dependency failed: ${failedDeps.join(", ")}\x1B[0m`);
|
|
26022
|
+
state.completed[key2] = {
|
|
26023
|
+
task: bc.prompt,
|
|
26024
|
+
status: "failed",
|
|
26025
|
+
error: `Blocked by failed dependencies: ${failedDeps.join(", ")}`,
|
|
26026
|
+
duration_seconds: 0,
|
|
26027
|
+
verified: false,
|
|
26028
|
+
retries: 0
|
|
26029
|
+
};
|
|
26030
|
+
state.timestamp = new Date().toISOString();
|
|
26031
|
+
saveState(state, stateFile);
|
|
26032
|
+
}
|
|
26033
|
+
if (ready.length === 0)
|
|
26034
|
+
break;
|
|
26035
|
+
const config2 = ready[0];
|
|
26036
|
+
const key = taskKey(config2);
|
|
26037
|
+
const totalNotDone = notDone.length - blocked.length;
|
|
26038
|
+
const totalDone = Object.keys(state.completed).length;
|
|
26039
|
+
const label = config2.id ? `${config2.id}` : "";
|
|
25632
26040
|
options.log?.(`
|
|
25633
|
-
[${
|
|
25634
|
-
const
|
|
25635
|
-
|
|
25636
|
-
|
|
25637
|
-
|
|
25638
|
-
|
|
25639
|
-
|
|
25640
|
-
|
|
25641
|
-
|
|
26041
|
+
\x1B[1m[${totalDone + 1}/${totalDone + totalNotDone}]${label ? ` ${label}` : ""}\x1B[0m`);
|
|
26042
|
+
const resumeSessionId = state.inProgress?.hash === key ? state.inProgress.sessionId : undefined;
|
|
26043
|
+
if (resumeSessionId) {
|
|
26044
|
+
options.log?.(`\x1B[2mResuming session ${resumeSessionId.slice(0, 8)}...\x1B[0m`);
|
|
26045
|
+
}
|
|
26046
|
+
state.inProgress = { hash: key, prompt: config2.prompt, startedAt: new Date().toISOString() };
|
|
26047
|
+
saveState(state, stateFile);
|
|
26048
|
+
const result = await runJob(config2, options.log, {
|
|
26049
|
+
resumeSessionId,
|
|
26050
|
+
onSessionId: (id) => {
|
|
26051
|
+
state.inProgress = { hash: key, prompt: config2.prompt, sessionId: id, startedAt: state.inProgress.startedAt };
|
|
26052
|
+
saveState(state, stateFile);
|
|
26053
|
+
}
|
|
26054
|
+
});
|
|
26055
|
+
state.completed[key] = result;
|
|
26056
|
+
state.inProgress = undefined;
|
|
26057
|
+
state.timestamp = new Date().toISOString();
|
|
25642
26058
|
saveState(state, stateFile);
|
|
25643
|
-
if (
|
|
26059
|
+
if (options.reloadConfigs) {
|
|
26060
|
+
try {
|
|
26061
|
+
currentConfigs = options.reloadConfigs();
|
|
26062
|
+
const newDagError = validateDag(currentConfigs);
|
|
26063
|
+
if (newDagError) {
|
|
26064
|
+
options.log?.(`\x1B[33m⚠ DAG error in updated YAML, ignoring reload: ${newDagError}\x1B[0m`);
|
|
26065
|
+
currentConfigs = configs;
|
|
26066
|
+
}
|
|
26067
|
+
} catch {}
|
|
26068
|
+
}
|
|
26069
|
+
const nextNotDone = currentConfigs.filter((c) => !(taskKey(c) in state.completed));
|
|
26070
|
+
const nextReady = nextNotDone.filter((c) => depsReady(c, state.completed) === "ready");
|
|
26071
|
+
if (nextReady.length > 0) {
|
|
25644
26072
|
await sleep(1000);
|
|
25645
26073
|
}
|
|
25646
26074
|
}
|
|
26075
|
+
const results = currentConfigs.map((c) => state.completed[taskKey(c)]).filter((r) => r !== undefined);
|
|
25647
26076
|
clearState(stateFile);
|
|
25648
26077
|
return results;
|
|
25649
26078
|
}
|
|
@@ -25778,55 +26207,860 @@ function generateReport(results, totalDuration, outputPath) {
|
|
|
25778
26207
|
return content;
|
|
25779
26208
|
}
|
|
25780
26209
|
|
|
26210
|
+
// src/goal-runner.ts
|
|
26211
|
+
var import_yaml = __toESM(require_dist(), 1);
|
|
26212
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
26213
|
+
import { execSync as execSync2 } from "child_process";
|
|
26214
|
+
var ITERATION_DIR = ".overnight-iterations";
|
|
26215
|
+
function ensureIterationDir() {
|
|
26216
|
+
if (!existsSync5(ITERATION_DIR)) {
|
|
26217
|
+
mkdirSync3(ITERATION_DIR, { recursive: true });
|
|
26218
|
+
}
|
|
26219
|
+
}
|
|
26220
|
+
function saveGoalState(state, stateFile) {
|
|
26221
|
+
writeFileSync3(stateFile, JSON.stringify(state, null, 2));
|
|
26222
|
+
}
|
|
26223
|
+
function loadGoalState(stateFile) {
|
|
26224
|
+
if (!existsSync5(stateFile))
|
|
26225
|
+
return null;
|
|
26226
|
+
return JSON.parse(readFileSync3(stateFile, "utf-8"));
|
|
26227
|
+
}
|
|
26228
|
+
function saveIterationState(iteration, state) {
|
|
26229
|
+
ensureIterationDir();
|
|
26230
|
+
writeFileSync3(`${ITERATION_DIR}/iteration-${iteration}-state.yaml`, import_yaml.stringify(state));
|
|
26231
|
+
}
|
|
26232
|
+
function saveIterationNarrative(iteration, narrative) {
|
|
26233
|
+
ensureIterationDir();
|
|
26234
|
+
writeFileSync3(`${ITERATION_DIR}/iteration-${iteration}-summary.md`, narrative);
|
|
26235
|
+
}
|
|
26236
|
+
function loadPreviousIterationState(iteration) {
|
|
26237
|
+
const path = `${ITERATION_DIR}/iteration-${iteration}-state.yaml`;
|
|
26238
|
+
if (!existsSync5(path))
|
|
26239
|
+
return null;
|
|
26240
|
+
return import_yaml.parse(readFileSync3(path, "utf-8"));
|
|
26241
|
+
}
|
|
26242
|
+
function loadPreviousNarrative(iteration) {
|
|
26243
|
+
const path = `${ITERATION_DIR}/iteration-${iteration}-summary.md`;
|
|
26244
|
+
if (!existsSync5(path))
|
|
26245
|
+
return null;
|
|
26246
|
+
return readFileSync3(path, "utf-8");
|
|
26247
|
+
}
|
|
26248
|
+
function isConverging(states, threshold) {
|
|
26249
|
+
if (states.length < threshold)
|
|
26250
|
+
return true;
|
|
26251
|
+
const recent = states.slice(-threshold);
|
|
26252
|
+
const remainingCounts = recent.map((s) => s.remaining_items.length);
|
|
26253
|
+
for (let i = 1;i < remainingCounts.length; i++) {
|
|
26254
|
+
if (remainingCounts[i] < remainingCounts[i - 1]) {
|
|
26255
|
+
return true;
|
|
26256
|
+
}
|
|
26257
|
+
}
|
|
26258
|
+
return false;
|
|
26259
|
+
}
|
|
26260
|
+
var SPINNER_FRAMES2 = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
26261
|
+
|
|
26262
|
+
class ProgressDisplay2 {
|
|
26263
|
+
interval = null;
|
|
26264
|
+
frame = 0;
|
|
26265
|
+
startTime = Date.now();
|
|
26266
|
+
currentActivity = "Working";
|
|
26267
|
+
start(activity) {
|
|
26268
|
+
this.currentActivity = activity;
|
|
26269
|
+
this.startTime = Date.now();
|
|
26270
|
+
this.frame = 0;
|
|
26271
|
+
if (this.interval)
|
|
26272
|
+
return;
|
|
26273
|
+
this.interval = setInterval(() => {
|
|
26274
|
+
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
26275
|
+
process.stdout.write(`\r\x1B[K${SPINNER_FRAMES2[this.frame]} ${this.currentActivity} (${elapsed}s)`);
|
|
26276
|
+
this.frame = (this.frame + 1) % SPINNER_FRAMES2.length;
|
|
26277
|
+
}, 100);
|
|
26278
|
+
}
|
|
26279
|
+
stop(finalMessage) {
|
|
26280
|
+
if (this.interval) {
|
|
26281
|
+
clearInterval(this.interval);
|
|
26282
|
+
this.interval = null;
|
|
26283
|
+
}
|
|
26284
|
+
process.stdout.write("\r\x1B[K");
|
|
26285
|
+
if (finalMessage)
|
|
26286
|
+
console.log(finalMessage);
|
|
26287
|
+
}
|
|
26288
|
+
}
|
|
26289
|
+
var claudeExecutablePath2;
|
|
26290
|
+
function findClaudeExecutable2() {
|
|
26291
|
+
if (claudeExecutablePath2 !== undefined)
|
|
26292
|
+
return claudeExecutablePath2;
|
|
26293
|
+
if (process.env.CLAUDE_CODE_PATH) {
|
|
26294
|
+
claudeExecutablePath2 = process.env.CLAUDE_CODE_PATH;
|
|
26295
|
+
return claudeExecutablePath2;
|
|
26296
|
+
}
|
|
26297
|
+
try {
|
|
26298
|
+
const cmd = process.platform === "win32" ? "where claude" : "which claude";
|
|
26299
|
+
claudeExecutablePath2 = execSync2(cmd, { encoding: "utf-8" }).trim().split(`
|
|
26300
|
+
`)[0];
|
|
26301
|
+
return claudeExecutablePath2;
|
|
26302
|
+
} catch {
|
|
26303
|
+
const commonPaths = [
|
|
26304
|
+
"/usr/local/bin/claude",
|
|
26305
|
+
"/opt/homebrew/bin/claude",
|
|
26306
|
+
`${process.env.HOME}/.local/bin/claude`
|
|
26307
|
+
];
|
|
26308
|
+
for (const p of commonPaths) {
|
|
26309
|
+
if (existsSync5(p)) {
|
|
26310
|
+
claudeExecutablePath2 = p;
|
|
26311
|
+
return claudeExecutablePath2;
|
|
26312
|
+
}
|
|
26313
|
+
}
|
|
26314
|
+
}
|
|
26315
|
+
return;
|
|
26316
|
+
}
|
|
26317
|
+
async function runClaudePrompt(prompt, config2, log, progress, resumeSessionId) {
|
|
26318
|
+
const claudePath = findClaudeExecutable2();
|
|
26319
|
+
if (!claudePath) {
|
|
26320
|
+
throw new Error("Claude CLI not found. Install with: curl -fsSL https://claude.ai/install.sh | bash");
|
|
26321
|
+
}
|
|
26322
|
+
const tools = config2.defaults?.allowed_tools ?? DEFAULT_TOOLS;
|
|
26323
|
+
const timeout = (config2.defaults?.timeout_seconds ?? DEFAULT_TIMEOUT) * 1000;
|
|
26324
|
+
const security = config2.defaults?.security;
|
|
26325
|
+
const securityHooks = security ? createSecurityHooks(security) : undefined;
|
|
26326
|
+
const sdkOptions = {
|
|
26327
|
+
allowedTools: tools,
|
|
26328
|
+
permissionMode: "acceptEdits",
|
|
26329
|
+
pathToClaudeCodeExecutable: claudePath,
|
|
26330
|
+
...security?.max_turns && { maxTurns: security.max_turns },
|
|
26331
|
+
...securityHooks && { hooks: securityHooks },
|
|
26332
|
+
...resumeSessionId && { resume: resumeSessionId }
|
|
26333
|
+
};
|
|
26334
|
+
let sessionId;
|
|
26335
|
+
let result;
|
|
26336
|
+
const conversation = query({ prompt, options: sdkOptions });
|
|
26337
|
+
for await (const message of conversation) {
|
|
26338
|
+
if (message.type === "result") {
|
|
26339
|
+
sessionId = message.session_id;
|
|
26340
|
+
if (message.subtype === "success") {
|
|
26341
|
+
result = message.result;
|
|
26342
|
+
}
|
|
26343
|
+
} else if (message.type === "system" && "subtype" in message) {
|
|
26344
|
+
if (message.subtype === "init") {
|
|
26345
|
+
sessionId = message.session_id;
|
|
26346
|
+
}
|
|
26347
|
+
}
|
|
26348
|
+
}
|
|
26349
|
+
return { result, sessionId };
|
|
26350
|
+
}
|
|
26351
|
+
function buildIterationPrompt(goal, iteration, previousState, previousNarrative) {
|
|
26352
|
+
const parts = [];
|
|
26353
|
+
parts.push(`# Goal
|
|
26354
|
+
|
|
26355
|
+
${goal.goal}`);
|
|
26356
|
+
if (goal.acceptance_criteria && goal.acceptance_criteria.length > 0) {
|
|
26357
|
+
parts.push(`
|
|
26358
|
+
# Acceptance Criteria
|
|
26359
|
+
|
|
26360
|
+
${goal.acceptance_criteria.map((c) => `- ${c}`).join(`
|
|
26361
|
+
`)}`);
|
|
26362
|
+
}
|
|
26363
|
+
if (goal.constraints && goal.constraints.length > 0) {
|
|
26364
|
+
parts.push(`
|
|
26365
|
+
# Constraints
|
|
26366
|
+
|
|
26367
|
+
${goal.constraints.map((c) => `- ${c}`).join(`
|
|
26368
|
+
`)}`);
|
|
26369
|
+
}
|
|
26370
|
+
if (goal.verification_commands && goal.verification_commands.length > 0) {
|
|
26371
|
+
parts.push(`
|
|
26372
|
+
# Verification Commands (must pass)
|
|
26373
|
+
|
|
26374
|
+
${goal.verification_commands.map((c) => `- \`${c}\``).join(`
|
|
26375
|
+
`)}`);
|
|
26376
|
+
}
|
|
26377
|
+
parts.push(`
|
|
26378
|
+
# Iteration ${iteration}`);
|
|
26379
|
+
if (previousState && previousNarrative) {
|
|
26380
|
+
parts.push(`
|
|
26381
|
+
## Previous Iteration State
|
|
26382
|
+
|
|
26383
|
+
### Completed Items
|
|
26384
|
+
${previousState.completed_items.map((i) => `- ${i}`).join(`
|
|
26385
|
+
`) || "- (none yet)"}`);
|
|
26386
|
+
parts.push(`
|
|
26387
|
+
### Remaining Items
|
|
26388
|
+
${previousState.remaining_items.map((i) => `- ${i}`).join(`
|
|
26389
|
+
`) || "- (none)"}`);
|
|
26390
|
+
parts.push(`
|
|
26391
|
+
### Known Issues
|
|
26392
|
+
${previousState.known_issues.map((i) => `- ${i}`).join(`
|
|
26393
|
+
`) || "- (none)"}`);
|
|
26394
|
+
parts.push(`
|
|
26395
|
+
### Files Modified
|
|
26396
|
+
${previousState.files_modified.map((f) => `- ${f}`).join(`
|
|
26397
|
+
`) || "- (none)"}`);
|
|
26398
|
+
parts.push(`
|
|
26399
|
+
### Previous Summary
|
|
26400
|
+
|
|
26401
|
+
${previousNarrative}`);
|
|
26402
|
+
}
|
|
26403
|
+
parts.push(`
|
|
26404
|
+
# Instructions
|
|
26405
|
+
|
|
26406
|
+
You are iteration ${iteration} of an autonomous build loop working toward the goal above.
|
|
26407
|
+
|
|
26408
|
+
1. Assess the current state of the project
|
|
26409
|
+
2. Identify the highest-priority remaining work
|
|
26410
|
+
3. Implement as much as you can in this iteration
|
|
26411
|
+
4. When done, output your structured state update in the following EXACT format:
|
|
26412
|
+
|
|
26413
|
+
\`\`\`yaml
|
|
26414
|
+
completed_items:
|
|
26415
|
+
- "item 1 you completed"
|
|
26416
|
+
- "item 2 you completed"
|
|
26417
|
+
remaining_items:
|
|
26418
|
+
- "item still to do"
|
|
26419
|
+
- "another item still to do"
|
|
26420
|
+
known_issues:
|
|
26421
|
+
- "any issues found"
|
|
26422
|
+
files_modified:
|
|
26423
|
+
- "path/to/file1.ts"
|
|
26424
|
+
- "path/to/file2.ts"
|
|
26425
|
+
agent_done: false # Set to true ONLY if you believe the goal is fully met
|
|
26426
|
+
\`\`\`
|
|
26427
|
+
|
|
26428
|
+
5. After the YAML block, write a brief narrative summary (2-3 paragraphs) of what you did, what challenges you encountered, and what the next iteration should focus on.
|
|
26429
|
+
|
|
26430
|
+
IMPORTANT: Always output the YAML block wrapped in \`\`\`yaml ... \`\`\` fences. This is how state is tracked between iterations.`);
|
|
26431
|
+
return parts.join(`
|
|
26432
|
+
`);
|
|
26433
|
+
}
|
|
26434
|
+
function parseIterationOutput(output, iteration) {
|
|
26435
|
+
const yamlMatch = output.match(/```yaml\n([\s\S]*?)\n```/);
|
|
26436
|
+
let state;
|
|
26437
|
+
if (yamlMatch) {
|
|
26438
|
+
try {
|
|
26439
|
+
const parsed = import_yaml.parse(yamlMatch[1]);
|
|
26440
|
+
state = {
|
|
26441
|
+
iteration,
|
|
26442
|
+
completed_items: parsed.completed_items ?? [],
|
|
26443
|
+
remaining_items: parsed.remaining_items ?? [],
|
|
26444
|
+
known_issues: parsed.known_issues ?? [],
|
|
26445
|
+
files_modified: parsed.files_modified ?? [],
|
|
26446
|
+
agent_done: parsed.agent_done ?? false,
|
|
26447
|
+
timestamp: new Date().toISOString()
|
|
26448
|
+
};
|
|
26449
|
+
} catch {
|
|
26450
|
+
state = {
|
|
26451
|
+
iteration,
|
|
26452
|
+
completed_items: [],
|
|
26453
|
+
remaining_items: ["(failed to parse agent output)"],
|
|
26454
|
+
known_issues: ["Agent output did not contain valid YAML state block"],
|
|
26455
|
+
files_modified: [],
|
|
26456
|
+
agent_done: false,
|
|
26457
|
+
timestamp: new Date().toISOString()
|
|
26458
|
+
};
|
|
26459
|
+
}
|
|
26460
|
+
} else {
|
|
26461
|
+
state = {
|
|
26462
|
+
iteration,
|
|
26463
|
+
completed_items: [],
|
|
26464
|
+
remaining_items: ["(no structured output from agent)"],
|
|
26465
|
+
known_issues: ["Agent did not output a YAML state block"],
|
|
26466
|
+
files_modified: [],
|
|
26467
|
+
agent_done: false,
|
|
26468
|
+
timestamp: new Date().toISOString()
|
|
26469
|
+
};
|
|
26470
|
+
}
|
|
26471
|
+
let narrative;
|
|
26472
|
+
if (yamlMatch) {
|
|
26473
|
+
const afterYaml = output.slice(output.indexOf("```", output.indexOf("```yaml") + 7) + 3).trim();
|
|
26474
|
+
narrative = afterYaml || "(no narrative provided)";
|
|
26475
|
+
} else {
|
|
26476
|
+
narrative = output;
|
|
26477
|
+
}
|
|
26478
|
+
return { state, narrative };
|
|
26479
|
+
}
|
|
26480
|
+
function buildGatePrompt(goal, iterationStates) {
|
|
26481
|
+
const lastState = iterationStates[iterationStates.length - 1];
|
|
26482
|
+
const parts = [];
|
|
26483
|
+
parts.push(`# Final Verification Gate
|
|
26484
|
+
|
|
26485
|
+
You are a dedicated verification agent. You did NOT write this code. Your only job is to determine if the goal has been met to production quality. Be rigorous and honest.
|
|
26486
|
+
|
|
26487
|
+
## Goal
|
|
26488
|
+
|
|
26489
|
+
${goal.goal}`);
|
|
26490
|
+
if (goal.acceptance_criteria && goal.acceptance_criteria.length > 0) {
|
|
26491
|
+
parts.push(`
|
|
26492
|
+
## Acceptance Criteria (ALL must be met)
|
|
26493
|
+
|
|
26494
|
+
${goal.acceptance_criteria.map((c, i) => `${i + 1}. ${c}`).join(`
|
|
26495
|
+
`)}`);
|
|
26496
|
+
}
|
|
26497
|
+
if (goal.verification_commands && goal.verification_commands.length > 0) {
|
|
26498
|
+
parts.push(`
|
|
26499
|
+
## Required Verification Commands
|
|
26500
|
+
|
|
26501
|
+
Run ALL of these. Each must pass:
|
|
26502
|
+
${goal.verification_commands.map((c) => `- \`${c}\``).join(`
|
|
26503
|
+
`)}`);
|
|
26504
|
+
}
|
|
26505
|
+
parts.push(`
|
|
26506
|
+
## Build Agent's Final State
|
|
26507
|
+
|
|
26508
|
+
### Completed Items
|
|
26509
|
+
${lastState?.completed_items.map((i) => `- ${i}`).join(`
|
|
26510
|
+
`) || "- (none)"}
|
|
26511
|
+
|
|
26512
|
+
### Claimed Remaining Items
|
|
26513
|
+
${lastState?.remaining_items.map((i) => `- ${i}`).join(`
|
|
26514
|
+
`) || "- (none)"}
|
|
26515
|
+
|
|
26516
|
+
### Known Issues
|
|
26517
|
+
${lastState?.known_issues.map((i) => `- ${i}`).join(`
|
|
26518
|
+
`) || "- (none)"}
|
|
26519
|
+
|
|
26520
|
+
## Instructions
|
|
26521
|
+
|
|
26522
|
+
Perform EVERY form of verification you can:
|
|
26523
|
+
|
|
26524
|
+
1. **Build check**: Does the project compile/build without errors?
|
|
26525
|
+
2. **Lint/type check**: Are there type errors or lint warnings?
|
|
26526
|
+
3. **Unit tests**: Do all unit tests pass?
|
|
26527
|
+
4. **E2E tests**: Do end-to-end tests pass?
|
|
26528
|
+
5. **Visual review**: Check rendered output if applicable
|
|
26529
|
+
6. **Manual walkthrough**: Trace key user flows through the code
|
|
26530
|
+
7. **Acceptance criteria**: Verify each criterion explicitly
|
|
26531
|
+
8. **Verification commands**: Run each command listed above
|
|
26532
|
+
9. **Code quality**: Look for obvious bugs, missing error handling, broken imports
|
|
26533
|
+
10. **Integration**: Is everything wired up? No dead code, no missing connections?
|
|
26534
|
+
|
|
26535
|
+
After your review, output your verdict in this EXACT format:
|
|
26536
|
+
|
|
26537
|
+
\`\`\`yaml
|
|
26538
|
+
passed: false # or true
|
|
26539
|
+
checks:
|
|
26540
|
+
- name: "Build"
|
|
26541
|
+
passed: true
|
|
26542
|
+
output: "npm run build succeeded"
|
|
26543
|
+
- name: "Unit tests"
|
|
26544
|
+
passed: false
|
|
26545
|
+
output: "3 tests failed: ..."
|
|
26546
|
+
summary: "Brief overall assessment"
|
|
26547
|
+
failures:
|
|
26548
|
+
- "Description of failure 1"
|
|
26549
|
+
- "Description of failure 2"
|
|
26550
|
+
\`\`\`
|
|
26551
|
+
|
|
26552
|
+
Be thorough. Do not let bad quality pass. If ANYTHING is broken, set passed: false.`);
|
|
26553
|
+
return parts.join(`
|
|
26554
|
+
`);
|
|
26555
|
+
}
|
|
26556
|
+
function parseGateOutput(output) {
|
|
26557
|
+
const yamlMatch = output.match(/```yaml\n([\s\S]*?)\n```/);
|
|
26558
|
+
if (yamlMatch) {
|
|
26559
|
+
try {
|
|
26560
|
+
const parsed = import_yaml.parse(yamlMatch[1]);
|
|
26561
|
+
return {
|
|
26562
|
+
passed: parsed.passed ?? false,
|
|
26563
|
+
checks: (parsed.checks ?? []).map((c) => ({
|
|
26564
|
+
name: c.name ?? "unknown",
|
|
26565
|
+
passed: c.passed ?? false,
|
|
26566
|
+
output: c.output ?? ""
|
|
26567
|
+
})),
|
|
26568
|
+
summary: parsed.summary ?? "",
|
|
26569
|
+
failures: parsed.failures ?? []
|
|
26570
|
+
};
|
|
26571
|
+
} catch {
|
|
26572
|
+
return {
|
|
26573
|
+
passed: false,
|
|
26574
|
+
checks: [],
|
|
26575
|
+
summary: "Failed to parse gate agent output",
|
|
26576
|
+
failures: ["Gate agent output was not valid YAML"]
|
|
26577
|
+
};
|
|
26578
|
+
}
|
|
26579
|
+
}
|
|
26580
|
+
return {
|
|
26581
|
+
passed: false,
|
|
26582
|
+
checks: [],
|
|
26583
|
+
summary: "Gate agent did not output a structured verdict",
|
|
26584
|
+
failures: ["No YAML verdict block found in gate agent output"]
|
|
26585
|
+
};
|
|
26586
|
+
}
|
|
26587
|
+
async function runGoal(goal, options = {}) {
|
|
26588
|
+
const stateFile = options.stateFile ?? DEFAULT_GOAL_STATE_FILE;
|
|
26589
|
+
const log = options.log ?? (() => {});
|
|
26590
|
+
const maxIterations = goal.max_iterations ?? DEFAULT_MAX_ITERATIONS;
|
|
26591
|
+
const convergenceThreshold = goal.convergence_threshold ?? DEFAULT_CONVERGENCE_THRESHOLD;
|
|
26592
|
+
const progress = new ProgressDisplay2;
|
|
26593
|
+
let runState = loadGoalState(stateFile) ?? {
|
|
26594
|
+
goal: goal.goal,
|
|
26595
|
+
iterations: [],
|
|
26596
|
+
gate_results: [],
|
|
26597
|
+
status: "running",
|
|
26598
|
+
timestamp: new Date().toISOString()
|
|
26599
|
+
};
|
|
26600
|
+
const startIteration = runState.iterations.length + 1;
|
|
26601
|
+
if (startIteration > 1) {
|
|
26602
|
+
log(`\x1B[1movernight: Resuming from iteration ${startIteration}\x1B[0m`);
|
|
26603
|
+
} else {
|
|
26604
|
+
log(`\x1B[1movernight: Starting goal loop\x1B[0m`);
|
|
26605
|
+
log(`\x1B[2mGoal: ${goal.goal.slice(0, 80)}${goal.goal.length > 80 ? "..." : ""}\x1B[0m`);
|
|
26606
|
+
log(`\x1B[2mMax iterations: ${maxIterations}, convergence threshold: ${convergenceThreshold}\x1B[0m`);
|
|
26607
|
+
}
|
|
26608
|
+
log("");
|
|
26609
|
+
for (let iteration = startIteration;iteration <= maxIterations; iteration++) {
|
|
26610
|
+
log(`\x1B[1m━━━ Iteration ${iteration}/${maxIterations} ━━━\x1B[0m`);
|
|
26611
|
+
const prevState = iteration > 1 ? loadPreviousIterationState(iteration - 1) : null;
|
|
26612
|
+
const prevNarrative = iteration > 1 ? loadPreviousNarrative(iteration - 1) : null;
|
|
26613
|
+
if (!isConverging(runState.iterations, convergenceThreshold)) {
|
|
26614
|
+
log(`\x1B[33m⚠ Build loop stalled — remaining items unchanged for ${convergenceThreshold} iterations\x1B[0m`);
|
|
26615
|
+
runState.status = "stalled";
|
|
26616
|
+
saveGoalState(runState, stateFile);
|
|
26617
|
+
break;
|
|
26618
|
+
}
|
|
26619
|
+
const prompt = buildIterationPrompt(goal, iteration, prevState, prevNarrative);
|
|
26620
|
+
progress.start(`Iteration ${iteration}`);
|
|
26621
|
+
try {
|
|
26622
|
+
const { result } = await runClaudePrompt(prompt, goal, log, progress);
|
|
26623
|
+
progress.stop();
|
|
26624
|
+
if (!result) {
|
|
26625
|
+
log(`\x1B[31m✗ No output from build agent\x1B[0m`);
|
|
26626
|
+
continue;
|
|
26627
|
+
}
|
|
26628
|
+
const { state: iterState, narrative } = parseIterationOutput(result, iteration);
|
|
26629
|
+
saveIterationState(iteration, iterState);
|
|
26630
|
+
saveIterationNarrative(iteration, narrative);
|
|
26631
|
+
runState.iterations.push(iterState);
|
|
26632
|
+
runState.timestamp = new Date().toISOString();
|
|
26633
|
+
saveGoalState(runState, stateFile);
|
|
26634
|
+
log(`\x1B[32m✓ Iteration ${iteration} complete\x1B[0m`);
|
|
26635
|
+
log(` Completed: ${iterState.completed_items.length} items`);
|
|
26636
|
+
log(` Remaining: ${iterState.remaining_items.length} items`);
|
|
26637
|
+
if (iterState.known_issues.length > 0) {
|
|
26638
|
+
log(` Issues: ${iterState.known_issues.length}`);
|
|
26639
|
+
}
|
|
26640
|
+
if (iterState.agent_done) {
|
|
26641
|
+
log(`
|
|
26642
|
+
\x1B[36m◆ Build agent reports goal is met — running final gate...\x1B[0m
|
|
26643
|
+
`);
|
|
26644
|
+
break;
|
|
26645
|
+
}
|
|
26646
|
+
} catch (e) {
|
|
26647
|
+
progress.stop();
|
|
26648
|
+
const error2 = e;
|
|
26649
|
+
log(`\x1B[31m✗ Iteration ${iteration} failed: ${error2.message}\x1B[0m`);
|
|
26650
|
+
if (error2.message === "TIMEOUT") {
|
|
26651
|
+
log(`\x1B[33m Continuing to next iteration...\x1B[0m`);
|
|
26652
|
+
continue;
|
|
26653
|
+
}
|
|
26654
|
+
continue;
|
|
26655
|
+
}
|
|
26656
|
+
log("");
|
|
26657
|
+
}
|
|
26658
|
+
if (runState.status === "running") {
|
|
26659
|
+
const maxGateAttempts = 3;
|
|
26660
|
+
for (let gateAttempt = 1;gateAttempt <= maxGateAttempts; gateAttempt++) {
|
|
26661
|
+
log(`\x1B[1m━━━ Final Gate (attempt ${gateAttempt}/${maxGateAttempts}) ━━━\x1B[0m`);
|
|
26662
|
+
const gatePrompt = buildGatePrompt(goal, runState.iterations);
|
|
26663
|
+
const gateGoalConfig = {
|
|
26664
|
+
...goal,
|
|
26665
|
+
defaults: {
|
|
26666
|
+
...goal.defaults,
|
|
26667
|
+
allowed_tools: [...goal.defaults?.allowed_tools ?? DEFAULT_TOOLS, "Bash"]
|
|
26668
|
+
}
|
|
26669
|
+
};
|
|
26670
|
+
progress.start("Running final gate");
|
|
26671
|
+
try {
|
|
26672
|
+
const { result } = await runClaudePrompt(gatePrompt, gateGoalConfig, log, progress);
|
|
26673
|
+
progress.stop();
|
|
26674
|
+
if (!result) {
|
|
26675
|
+
log(`\x1B[31m✗ No output from gate agent\x1B[0m`);
|
|
26676
|
+
continue;
|
|
26677
|
+
}
|
|
26678
|
+
const gateResult = parseGateOutput(result);
|
|
26679
|
+
runState.gate_results.push(gateResult);
|
|
26680
|
+
saveGoalState(runState, stateFile);
|
|
26681
|
+
if (gateResult.passed) {
|
|
26682
|
+
log(`\x1B[32m✓ GATE PASSED\x1B[0m`);
|
|
26683
|
+
log(` ${gateResult.summary}`);
|
|
26684
|
+
for (const check2 of gateResult.checks) {
|
|
26685
|
+
const icon = check2.passed ? "\x1B[32m✓\x1B[0m" : "\x1B[31m✗\x1B[0m";
|
|
26686
|
+
log(` ${icon} ${check2.name}`);
|
|
26687
|
+
}
|
|
26688
|
+
runState.status = "gate_passed";
|
|
26689
|
+
saveGoalState(runState, stateFile);
|
|
26690
|
+
break;
|
|
26691
|
+
} else {
|
|
26692
|
+
log(`\x1B[31m✗ GATE FAILED\x1B[0m`);
|
|
26693
|
+
log(` ${gateResult.summary}`);
|
|
26694
|
+
for (const failure of gateResult.failures) {
|
|
26695
|
+
log(` \x1B[31m- ${failure}\x1B[0m`);
|
|
26696
|
+
}
|
|
26697
|
+
if (gateAttempt < maxGateAttempts) {
|
|
26698
|
+
log(`
|
|
26699
|
+
\x1B[36m◆ Looping back to build agent with gate failures...\x1B[0m
|
|
26700
|
+
`);
|
|
26701
|
+
const fixIteration = runState.iterations.length + 1;
|
|
26702
|
+
const fixPrompt = buildGateFixPrompt(goal, gateResult, fixIteration);
|
|
26703
|
+
progress.start(`Fix iteration ${fixIteration}`);
|
|
26704
|
+
try {
|
|
26705
|
+
const { result: fixResult } = await runClaudePrompt(fixPrompt, goal, log, progress);
|
|
26706
|
+
progress.stop();
|
|
26707
|
+
if (fixResult) {
|
|
26708
|
+
const { state: fixState, narrative: fixNarrative } = parseIterationOutput(fixResult, fixIteration);
|
|
26709
|
+
saveIterationState(fixIteration, fixState);
|
|
26710
|
+
saveIterationNarrative(fixIteration, fixNarrative);
|
|
26711
|
+
runState.iterations.push(fixState);
|
|
26712
|
+
saveGoalState(runState, stateFile);
|
|
26713
|
+
log(`\x1B[32m✓ Fix iteration complete\x1B[0m`);
|
|
26714
|
+
log(` Fixed: ${fixState.completed_items.length} items`);
|
|
26715
|
+
}
|
|
26716
|
+
} catch (e) {
|
|
26717
|
+
progress.stop();
|
|
26718
|
+
log(`\x1B[31m✗ Fix iteration failed: ${e.message}\x1B[0m`);
|
|
26719
|
+
}
|
|
26720
|
+
} else {
|
|
26721
|
+
runState.status = "gate_failed";
|
|
26722
|
+
saveGoalState(runState, stateFile);
|
|
26723
|
+
}
|
|
26724
|
+
}
|
|
26725
|
+
} catch (e) {
|
|
26726
|
+
progress.stop();
|
|
26727
|
+
log(`\x1B[31m✗ Gate failed: ${e.message}\x1B[0m`);
|
|
26728
|
+
}
|
|
26729
|
+
log("");
|
|
26730
|
+
}
|
|
26731
|
+
}
|
|
26732
|
+
if (runState.status === "running") {
|
|
26733
|
+
const lastState = runState.iterations[runState.iterations.length - 1];
|
|
26734
|
+
if (!lastState?.agent_done) {
|
|
26735
|
+
log(`\x1B[33m⚠ Reached max iterations (${maxIterations}) without completion\x1B[0m`);
|
|
26736
|
+
runState.status = "max_iterations";
|
|
26737
|
+
saveGoalState(runState, stateFile);
|
|
26738
|
+
}
|
|
26739
|
+
}
|
|
26740
|
+
return runState;
|
|
26741
|
+
}
|
|
26742
|
+
function buildGateFixPrompt(goal, gateResult, iteration) {
|
|
26743
|
+
return `# Goal
|
|
26744
|
+
|
|
26745
|
+
${goal.goal}
|
|
26746
|
+
|
|
26747
|
+
# Urgent: Fix Gate Failures
|
|
26748
|
+
|
|
26749
|
+
The final verification gate FAILED. You must fix these issues:
|
|
26750
|
+
|
|
26751
|
+
## Failures
|
|
26752
|
+
|
|
26753
|
+
${gateResult.failures.map((f) => `- ${f}`).join(`
|
|
26754
|
+
`)}
|
|
26755
|
+
|
|
26756
|
+
## Check Results
|
|
26757
|
+
|
|
26758
|
+
${gateResult.checks.map((c) => `- ${c.passed ? "PASS" : "FAIL"}: ${c.name} — ${c.output}`).join(`
|
|
26759
|
+
`)}
|
|
26760
|
+
|
|
26761
|
+
## Gate Summary
|
|
26762
|
+
|
|
26763
|
+
${gateResult.summary}
|
|
26764
|
+
|
|
26765
|
+
# Instructions
|
|
26766
|
+
|
|
26767
|
+
Fix ALL of the failures listed above. Focus exclusively on making the gate pass. Do not add new features.
|
|
26768
|
+
|
|
26769
|
+
When done, output your state update:
|
|
26770
|
+
|
|
26771
|
+
\`\`\`yaml
|
|
26772
|
+
completed_items:
|
|
26773
|
+
- "fixed: description of what you fixed"
|
|
26774
|
+
remaining_items:
|
|
26775
|
+
- "any remaining issues"
|
|
26776
|
+
known_issues:
|
|
26777
|
+
- "any issues you could not fix"
|
|
26778
|
+
files_modified:
|
|
26779
|
+
- "path/to/file.ts"
|
|
26780
|
+
agent_done: true
|
|
26781
|
+
\`\`\`
|
|
26782
|
+
|
|
26783
|
+
Then write a brief summary of what you fixed.`;
|
|
26784
|
+
}
|
|
26785
|
+
function parseGoalFile(path) {
|
|
26786
|
+
const content = readFileSync3(path, "utf-8");
|
|
26787
|
+
let data;
|
|
26788
|
+
try {
|
|
26789
|
+
data = import_yaml.parse(content);
|
|
26790
|
+
} catch (e) {
|
|
26791
|
+
const error2 = e;
|
|
26792
|
+
console.error(`\x1B[31mError parsing ${path}:\x1B[0m`);
|
|
26793
|
+
console.error(` ${error2.message.split(`
|
|
26794
|
+
`)[0]}`);
|
|
26795
|
+
process.exit(1);
|
|
26796
|
+
}
|
|
26797
|
+
if (!data.goal) {
|
|
26798
|
+
console.error(`\x1B[31mError: goal.yaml must have a 'goal' field\x1B[0m`);
|
|
26799
|
+
process.exit(1);
|
|
26800
|
+
}
|
|
26801
|
+
return data;
|
|
26802
|
+
}
|
|
26803
|
+
|
|
26804
|
+
// src/planner.ts
|
|
26805
|
+
import { writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
|
|
26806
|
+
import { execSync as execSync3 } from "child_process";
|
|
26807
|
+
import * as readline from "readline";
|
|
26808
|
+
var claudeExecutablePath3;
|
|
26809
|
+
function findClaudeExecutable3() {
|
|
26810
|
+
if (claudeExecutablePath3 !== undefined)
|
|
26811
|
+
return claudeExecutablePath3;
|
|
26812
|
+
if (process.env.CLAUDE_CODE_PATH) {
|
|
26813
|
+
claudeExecutablePath3 = process.env.CLAUDE_CODE_PATH;
|
|
26814
|
+
return claudeExecutablePath3;
|
|
26815
|
+
}
|
|
26816
|
+
try {
|
|
26817
|
+
const cmd = process.platform === "win32" ? "where claude" : "which claude";
|
|
26818
|
+
claudeExecutablePath3 = execSync3(cmd, { encoding: "utf-8" }).trim().split(`
|
|
26819
|
+
`)[0];
|
|
26820
|
+
return claudeExecutablePath3;
|
|
26821
|
+
} catch {
|
|
26822
|
+
const commonPaths = [
|
|
26823
|
+
"/usr/local/bin/claude",
|
|
26824
|
+
"/opt/homebrew/bin/claude",
|
|
26825
|
+
`${process.env.HOME}/.local/bin/claude`
|
|
26826
|
+
];
|
|
26827
|
+
for (const p of commonPaths) {
|
|
26828
|
+
if (existsSync6(p)) {
|
|
26829
|
+
claudeExecutablePath3 = p;
|
|
26830
|
+
return claudeExecutablePath3;
|
|
26831
|
+
}
|
|
26832
|
+
}
|
|
26833
|
+
}
|
|
26834
|
+
return;
|
|
26835
|
+
}
|
|
26836
|
+
function createReadline() {
|
|
26837
|
+
return readline.createInterface({
|
|
26838
|
+
input: process.stdin,
|
|
26839
|
+
output: process.stdout
|
|
26840
|
+
});
|
|
26841
|
+
}
|
|
26842
|
+
function ask(rl, question) {
|
|
26843
|
+
return new Promise((resolve2) => {
|
|
26844
|
+
rl.question(question, (answer) => resolve2(answer.trim()));
|
|
26845
|
+
});
|
|
26846
|
+
}
|
|
26847
|
+
var PLANNER_SYSTEM_PROMPT = `You are an expert software architect helping plan an autonomous overnight build.
|
|
26848
|
+
|
|
26849
|
+
Your job is to have a focused design conversation with the user, then produce a goal.yaml file that an autonomous build agent will use to implement the project overnight.
|
|
26850
|
+
|
|
26851
|
+
Guidelines:
|
|
26852
|
+
- Ask clarifying questions about scope, technology choices, priorities, and constraints
|
|
26853
|
+
- Keep the conversation focused and efficient — 3-5 rounds max
|
|
26854
|
+
- When you have enough information, produce the goal.yaml
|
|
26855
|
+
- The goal.yaml should be specific enough for an agent to work autonomously
|
|
26856
|
+
- Include concrete acceptance criteria that can be verified
|
|
26857
|
+
- Include verification commands when possible (build, test, lint)
|
|
26858
|
+
- Set realistic constraints
|
|
26859
|
+
|
|
26860
|
+
When you're ready to produce the final plan, output it in this format:
|
|
26861
|
+
|
|
26862
|
+
\`\`\`yaml
|
|
26863
|
+
goal: "Clear description of what to build"
|
|
26864
|
+
|
|
26865
|
+
acceptance_criteria:
|
|
26866
|
+
- "Specific, verifiable criterion 1"
|
|
26867
|
+
- "Specific, verifiable criterion 2"
|
|
26868
|
+
|
|
26869
|
+
verification_commands:
|
|
26870
|
+
- "npm run build"
|
|
26871
|
+
- "npm test"
|
|
26872
|
+
|
|
26873
|
+
constraints:
|
|
26874
|
+
- "Don't modify existing API contracts"
|
|
26875
|
+
|
|
26876
|
+
max_iterations: 15
|
|
26877
|
+
convergence_threshold: 3
|
|
26878
|
+
|
|
26879
|
+
defaults:
|
|
26880
|
+
timeout_seconds: 600
|
|
26881
|
+
allowed_tools:
|
|
26882
|
+
- Read
|
|
26883
|
+
- Edit
|
|
26884
|
+
- Write
|
|
26885
|
+
- Glob
|
|
26886
|
+
- Grep
|
|
26887
|
+
- Bash
|
|
26888
|
+
security:
|
|
26889
|
+
sandbox_dir: "."
|
|
26890
|
+
max_turns: 150
|
|
26891
|
+
\`\`\`
|
|
26892
|
+
|
|
26893
|
+
IMPORTANT: Only output the yaml block when you and the user agree the plan is ready. Before that, ask questions and discuss.`;
|
|
26894
|
+
async function runPlanner(initialGoal, options = {}) {
|
|
26895
|
+
const log = options.log ?? ((msg) => console.log(msg));
|
|
26896
|
+
const outputFile = options.outputFile ?? "goal.yaml";
|
|
26897
|
+
const claudePath = findClaudeExecutable3();
|
|
26898
|
+
if (!claudePath) {
|
|
26899
|
+
log("\x1B[31m✗ Error: Could not find 'claude' CLI.\x1B[0m");
|
|
26900
|
+
return null;
|
|
26901
|
+
}
|
|
26902
|
+
log("\x1B[1movernight plan: Interactive design session\x1B[0m");
|
|
26903
|
+
log("\x1B[2mDescribe your goal and I'll help shape it into a plan.\x1B[0m");
|
|
26904
|
+
log(`\x1B[2mType 'done' to finalize, 'quit' to abort.\x1B[0m
|
|
26905
|
+
`);
|
|
26906
|
+
const rl = createReadline();
|
|
26907
|
+
const conversationHistory = [];
|
|
26908
|
+
let currentPrompt = `The user wants to plan the following project for an overnight autonomous build:
|
|
26909
|
+
|
|
26910
|
+
${initialGoal}
|
|
26911
|
+
|
|
26912
|
+
Ask clarifying questions to understand scope, tech choices, priorities, and constraints. Be concise.`;
|
|
26913
|
+
try {
|
|
26914
|
+
let sessionId;
|
|
26915
|
+
for (let round = 0;round < 10; round++) {
|
|
26916
|
+
const sdkOptions = {
|
|
26917
|
+
allowedTools: ["Read", "Glob", "Grep"],
|
|
26918
|
+
systemPrompt: PLANNER_SYSTEM_PROMPT,
|
|
26919
|
+
permissionMode: "acceptEdits",
|
|
26920
|
+
pathToClaudeCodeExecutable: claudePath,
|
|
26921
|
+
...sessionId && { resume: sessionId }
|
|
26922
|
+
};
|
|
26923
|
+
let result;
|
|
26924
|
+
const conversation = query({ prompt: currentPrompt, options: sdkOptions });
|
|
26925
|
+
for await (const message of conversation) {
|
|
26926
|
+
if (message.type === "result") {
|
|
26927
|
+
sessionId = message.session_id;
|
|
26928
|
+
if (message.subtype === "success") {
|
|
26929
|
+
result = message.result;
|
|
26930
|
+
}
|
|
26931
|
+
} else if (message.type === "system" && "subtype" in message) {
|
|
26932
|
+
if (message.subtype === "init") {
|
|
26933
|
+
sessionId = message.session_id;
|
|
26934
|
+
}
|
|
26935
|
+
}
|
|
26936
|
+
}
|
|
26937
|
+
if (!result) {
|
|
26938
|
+
log("\x1B[31m✗ No response from planner\x1B[0m");
|
|
26939
|
+
break;
|
|
26940
|
+
}
|
|
26941
|
+
conversationHistory.push({ role: "assistant", content: result });
|
|
26942
|
+
const yamlMatch = result.match(/```yaml\n([\s\S]*?)\n```/);
|
|
26943
|
+
if (yamlMatch) {
|
|
26944
|
+
log(`
|
|
26945
|
+
\x1B[1m━━━ Proposed Plan ━━━\x1B[0m
|
|
26946
|
+
`);
|
|
26947
|
+
log(yamlMatch[1]);
|
|
26948
|
+
log(`
|
|
26949
|
+
\x1B[1m━━━━━━━━━━━━━━━━━━━━\x1B[0m
|
|
26950
|
+
`);
|
|
26951
|
+
const answer = await ask(rl, "\x1B[36m?\x1B[0m Accept this plan? (yes/no/revise): ");
|
|
26952
|
+
if (answer.toLowerCase() === "yes" || answer.toLowerCase() === "y") {
|
|
26953
|
+
writeFileSync4(outputFile, yamlMatch[1]);
|
|
26954
|
+
log(`
|
|
26955
|
+
\x1B[32m✓ Plan saved to ${outputFile}\x1B[0m`);
|
|
26956
|
+
log(`Run with: \x1B[1movernight run ${outputFile}\x1B[0m`);
|
|
26957
|
+
rl.close();
|
|
26958
|
+
const { parse: parseYaml2 } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
|
|
26959
|
+
return parseYaml2(yamlMatch[1]);
|
|
26960
|
+
} else if (answer.toLowerCase() === "quit" || answer.toLowerCase() === "q") {
|
|
26961
|
+
log("\x1B[33mAborted\x1B[0m");
|
|
26962
|
+
rl.close();
|
|
26963
|
+
return null;
|
|
26964
|
+
} else {
|
|
26965
|
+
const revision = await ask(rl, "\x1B[36m?\x1B[0m What would you like to change? ");
|
|
26966
|
+
currentPrompt = revision;
|
|
26967
|
+
conversationHistory.push({ role: "user", content: revision });
|
|
26968
|
+
continue;
|
|
26969
|
+
}
|
|
26970
|
+
}
|
|
26971
|
+
log(`
|
|
26972
|
+
\x1B[2m─── Planner ───\x1B[0m
|
|
26973
|
+
`);
|
|
26974
|
+
log(result);
|
|
26975
|
+
log("");
|
|
26976
|
+
const userInput = await ask(rl, "\x1B[36m>\x1B[0m ");
|
|
26977
|
+
if (userInput.toLowerCase() === "done") {
|
|
26978
|
+
currentPrompt = "The user is satisfied. Please produce the final goal.yaml now based on our discussion.";
|
|
26979
|
+
conversationHistory.push({ role: "user", content: currentPrompt });
|
|
26980
|
+
continue;
|
|
26981
|
+
}
|
|
26982
|
+
if (userInput.toLowerCase() === "quit" || userInput.toLowerCase() === "q") {
|
|
26983
|
+
log("\x1B[33mAborted\x1B[0m");
|
|
26984
|
+
rl.close();
|
|
26985
|
+
return null;
|
|
26986
|
+
}
|
|
26987
|
+
currentPrompt = userInput;
|
|
26988
|
+
conversationHistory.push({ role: "user", content: userInput });
|
|
26989
|
+
}
|
|
26990
|
+
} finally {
|
|
26991
|
+
rl.close();
|
|
26992
|
+
}
|
|
26993
|
+
log("\x1B[33m⚠ Design session ended without producing a plan\x1B[0m");
|
|
26994
|
+
return null;
|
|
26995
|
+
}
|
|
26996
|
+
|
|
25781
26997
|
// src/cli.ts
|
|
25782
26998
|
var AGENT_HELP = `
|
|
25783
|
-
# overnight -
|
|
26999
|
+
# overnight - Autonomous Build Runner for Claude Code
|
|
25784
27000
|
|
|
25785
|
-
|
|
27001
|
+
Two modes: goal-driven autonomous loops, or task-list batch jobs.
|
|
25786
27002
|
|
|
25787
27003
|
## Quick Start
|
|
25788
27004
|
|
|
25789
27005
|
\`\`\`bash
|
|
25790
|
-
#
|
|
25791
|
-
overnight
|
|
27006
|
+
# Hammer mode: just give it a goal and go
|
|
27007
|
+
overnight hammer "Build a multiplayer MMO"
|
|
25792
27008
|
|
|
25793
|
-
#
|
|
25794
|
-
overnight
|
|
27009
|
+
# Or: design session first, then autonomous build
|
|
27010
|
+
overnight plan "Build a multiplayer game" # Interactive design → goal.yaml
|
|
27011
|
+
overnight run goal.yaml --notify # Autonomous build loop
|
|
25795
27012
|
|
|
25796
|
-
#
|
|
25797
|
-
overnight run tasks.yaml --notify
|
|
27013
|
+
# Task mode: explicit task list
|
|
27014
|
+
overnight run tasks.yaml --notify
|
|
25798
27015
|
\`\`\`
|
|
25799
27016
|
|
|
25800
27017
|
## Commands
|
|
25801
27018
|
|
|
25802
27019
|
| Command | Description |
|
|
25803
27020
|
|---------|-------------|
|
|
25804
|
-
| \`overnight
|
|
27021
|
+
| \`overnight hammer "<goal>"\` | Autonomous build loop from a string |
|
|
27022
|
+
| \`overnight plan "<goal>"\` | Interactive design session → goal.yaml |
|
|
27023
|
+
| \`overnight run <file>\` | Run goal.yaml (loop) or tasks.yaml (batch) |
|
|
25805
27024
|
| \`overnight resume <file>\` | Resume interrupted run from checkpoint |
|
|
25806
27025
|
| \`overnight single "<prompt>"\` | Run a single task directly |
|
|
25807
|
-
| \`overnight init\` | Create example tasks.yaml |
|
|
27026
|
+
| \`overnight init\` | Create example goal.yaml or tasks.yaml |
|
|
27027
|
+
|
|
27028
|
+
## Goal Mode (goal.yaml)
|
|
27029
|
+
|
|
27030
|
+
Autonomous convergence loop: agent iterates toward a goal, then a separate
|
|
27031
|
+
gate agent verifies everything before declaring done.
|
|
27032
|
+
|
|
27033
|
+
\`\`\`yaml
|
|
27034
|
+
goal: "Build a clone of Flappy Bird with leaderboard"
|
|
27035
|
+
|
|
27036
|
+
acceptance_criteria:
|
|
27037
|
+
- "Game renders and is playable in browser"
|
|
27038
|
+
- "Leaderboard persists scores to localStorage"
|
|
27039
|
+
|
|
27040
|
+
verification_commands:
|
|
27041
|
+
- "npm run build"
|
|
27042
|
+
- "npm test"
|
|
27043
|
+
|
|
27044
|
+
constraints:
|
|
27045
|
+
- "Use vanilla JS, no frameworks"
|
|
27046
|
+
|
|
27047
|
+
max_iterations: 15
|
|
27048
|
+
\`\`\`
|
|
27049
|
+
|
|
27050
|
+
## Task Mode (tasks.yaml)
|
|
25808
27051
|
|
|
25809
|
-
|
|
27052
|
+
Explicit task list with optional dependency DAG.
|
|
25810
27053
|
|
|
25811
27054
|
\`\`\`yaml
|
|
25812
27055
|
defaults:
|
|
25813
|
-
timeout_seconds: 300
|
|
25814
|
-
verify: true
|
|
25815
|
-
allowed_tools:
|
|
25816
|
-
- Read
|
|
25817
|
-
- Edit
|
|
25818
|
-
- Glob
|
|
25819
|
-
- Grep
|
|
27056
|
+
timeout_seconds: 300
|
|
27057
|
+
verify: true
|
|
27058
|
+
allowed_tools: [Read, Edit, Write, Glob, Grep]
|
|
25820
27059
|
|
|
25821
27060
|
tasks:
|
|
25822
|
-
# Simple format
|
|
25823
27061
|
- "Fix the bug in auth.py"
|
|
25824
|
-
|
|
25825
|
-
# Detailed format
|
|
25826
27062
|
- prompt: "Add input validation"
|
|
25827
27063
|
timeout_seconds: 600
|
|
25828
|
-
verify: false
|
|
25829
|
-
allowed_tools: [Read, Edit, Bash, Glob, Grep]
|
|
25830
27064
|
\`\`\`
|
|
25831
27065
|
|
|
25832
27066
|
## Key Options
|
|
@@ -25836,53 +27070,71 @@ tasks:
|
|
|
25836
27070
|
| \`-o, --output <file>\` | Save results JSON |
|
|
25837
27071
|
| \`-r, --report <file>\` | Generate markdown report |
|
|
25838
27072
|
| \`-s, --state-file <file>\` | Custom checkpoint file |
|
|
27073
|
+
| \`--max-iterations <n>\` | Max build loop iterations (goal mode) |
|
|
25839
27074
|
| \`--notify\` | Send push notification via ntfy.sh |
|
|
25840
|
-
| \`--notify-topic <topic>\` | ntfy.sh topic (default: overnight) |
|
|
25841
27075
|
| \`-q, --quiet\` | Minimal output |
|
|
25842
27076
|
|
|
25843
|
-
## Features
|
|
25844
|
-
|
|
25845
|
-
1. **Crash Recovery**: Auto-checkpoints after each job. Use \`overnight resume\` to continue.
|
|
25846
|
-
2. **Retry Logic**: Auto-retries 3x on API/network errors with exponential backoff.
|
|
25847
|
-
3. **Notifications**: \`--notify\` sends summary to ntfy.sh (free, no signup).
|
|
25848
|
-
4. **Reports**: \`-r report.md\` generates markdown summary with next steps.
|
|
25849
|
-
5. **Security**: No Bash by default. Whitelist tools per-task.
|
|
25850
|
-
|
|
25851
27077
|
## Example Workflows
|
|
25852
27078
|
|
|
25853
27079
|
\`\`\`bash
|
|
25854
|
-
#
|
|
25855
|
-
nohup overnight
|
|
27080
|
+
# Simplest: just hammer a goal overnight
|
|
27081
|
+
nohup overnight hammer "Build a REST API with auth and tests" --notify > overnight.log 2>&1 &
|
|
25856
27082
|
|
|
25857
|
-
#
|
|
25858
|
-
overnight
|
|
27083
|
+
# Design first, then run
|
|
27084
|
+
overnight plan "Build a REST API with auth"
|
|
27085
|
+
nohup overnight run goal.yaml --notify > overnight.log 2>&1 &
|
|
25859
27086
|
|
|
25860
|
-
#
|
|
25861
|
-
overnight
|
|
27087
|
+
# Batch tasks overnight
|
|
27088
|
+
nohup overnight run tasks.yaml --notify -r report.md > overnight.log 2>&1 &
|
|
25862
27089
|
|
|
25863
|
-
# Resume after crash
|
|
25864
|
-
overnight resume
|
|
27090
|
+
# Resume after crash
|
|
27091
|
+
overnight resume goal.yaml
|
|
25865
27092
|
\`\`\`
|
|
25866
27093
|
|
|
25867
27094
|
## Exit Codes
|
|
25868
27095
|
|
|
25869
|
-
- 0: All tasks succeeded
|
|
25870
|
-
- 1:
|
|
27096
|
+
- 0: All tasks succeeded / gate passed
|
|
27097
|
+
- 1: Failures occurred / gate failed
|
|
25871
27098
|
|
|
25872
27099
|
## Files Created
|
|
25873
27100
|
|
|
25874
|
-
- \`.overnight-state.json\` -
|
|
27101
|
+
- \`.overnight-goal-state.json\` - Goal mode checkpoint
|
|
27102
|
+
- \`.overnight-iterations/\` - Per-iteration state + summaries
|
|
27103
|
+
- \`.overnight-state.json\` - Task mode checkpoint
|
|
25875
27104
|
- \`report.md\` - Summary report (if -r used)
|
|
25876
|
-
- \`results.json\` - Full results (if -o used)
|
|
25877
27105
|
|
|
25878
27106
|
Run \`overnight <command> --help\` for command-specific options.
|
|
25879
27107
|
`;
|
|
25880
|
-
function
|
|
25881
|
-
|
|
25882
|
-
|
|
27108
|
+
function isGoalFile(path) {
|
|
27109
|
+
try {
|
|
27110
|
+
const content = readFileSync5(path, "utf-8");
|
|
27111
|
+
const data = import_yaml2.parse(content);
|
|
27112
|
+
return typeof data?.goal === "string";
|
|
27113
|
+
} catch {
|
|
27114
|
+
return false;
|
|
27115
|
+
}
|
|
27116
|
+
}
|
|
27117
|
+
function parseTasksFile(path, cliSecurity) {
|
|
27118
|
+
const content = readFileSync5(path, "utf-8");
|
|
27119
|
+
let data;
|
|
27120
|
+
try {
|
|
27121
|
+
data = import_yaml2.parse(content);
|
|
27122
|
+
} catch (e) {
|
|
27123
|
+
const error2 = e;
|
|
27124
|
+
console.error(`\x1B[31mError parsing ${path}:\x1B[0m`);
|
|
27125
|
+
console.error(` ${error2.message.split(`
|
|
27126
|
+
`)[0]}`);
|
|
27127
|
+
process.exit(1);
|
|
27128
|
+
}
|
|
25883
27129
|
const tasks = Array.isArray(data) ? data : data.tasks ?? [];
|
|
25884
27130
|
const defaults = Array.isArray(data) ? {} : data.defaults ?? {};
|
|
25885
|
-
|
|
27131
|
+
const fileSecurity = !Array.isArray(data) && data.defaults?.security || {};
|
|
27132
|
+
const security = cliSecurity || Object.keys(fileSecurity).length > 0 ? {
|
|
27133
|
+
...fileSecurity,
|
|
27134
|
+
...cliSecurity,
|
|
27135
|
+
deny_patterns: cliSecurity?.deny_patterns ?? fileSecurity.deny_patterns ?? DEFAULT_DENY_PATTERNS
|
|
27136
|
+
} : undefined;
|
|
27137
|
+
const configs = tasks.map((task) => {
|
|
25886
27138
|
if (typeof task === "string") {
|
|
25887
27139
|
return {
|
|
25888
27140
|
prompt: task,
|
|
@@ -25890,19 +27142,24 @@ function parseTasksFile(path) {
|
|
|
25890
27142
|
stall_timeout_seconds: defaults.stall_timeout_seconds ?? DEFAULT_STALL_TIMEOUT,
|
|
25891
27143
|
verify: defaults.verify ?? true,
|
|
25892
27144
|
verify_prompt: defaults.verify_prompt ?? DEFAULT_VERIFY_PROMPT,
|
|
25893
|
-
allowed_tools: defaults.allowed_tools
|
|
27145
|
+
allowed_tools: defaults.allowed_tools,
|
|
27146
|
+
security
|
|
25894
27147
|
};
|
|
25895
27148
|
}
|
|
25896
27149
|
return {
|
|
27150
|
+
id: task.id ?? undefined,
|
|
27151
|
+
depends_on: task.depends_on ?? undefined,
|
|
25897
27152
|
prompt: task.prompt,
|
|
25898
27153
|
working_dir: task.working_dir ?? undefined,
|
|
25899
27154
|
timeout_seconds: task.timeout_seconds ?? defaults.timeout_seconds ?? DEFAULT_TIMEOUT,
|
|
25900
27155
|
stall_timeout_seconds: task.stall_timeout_seconds ?? defaults.stall_timeout_seconds ?? DEFAULT_STALL_TIMEOUT,
|
|
25901
27156
|
verify: task.verify ?? defaults.verify ?? true,
|
|
25902
27157
|
verify_prompt: task.verify_prompt ?? defaults.verify_prompt ?? DEFAULT_VERIFY_PROMPT,
|
|
25903
|
-
allowed_tools: task.allowed_tools ?? defaults.allowed_tools
|
|
27158
|
+
allowed_tools: task.allowed_tools ?? defaults.allowed_tools,
|
|
27159
|
+
security: task.security ?? security
|
|
25904
27160
|
};
|
|
25905
27161
|
});
|
|
27162
|
+
return { configs, security };
|
|
25906
27163
|
}
|
|
25907
27164
|
function printSummary(results) {
|
|
25908
27165
|
const statusColors = {
|
|
@@ -25928,53 +27185,149 @@ ${bold}Job Results${reset}`);
|
|
|
25928
27185
|
${bold}Summary:${reset} ${succeeded}/${results.length} succeeded`);
|
|
25929
27186
|
}
|
|
25930
27187
|
var program2 = new Command;
|
|
25931
|
-
program2.name("overnight").description("Batch job runner for Claude Code").version("0.
|
|
27188
|
+
program2.name("overnight").description("Batch job runner for Claude Code").version("0.3.0").action(() => {
|
|
25932
27189
|
console.log(AGENT_HELP);
|
|
25933
27190
|
});
|
|
25934
|
-
program2.command("run").description("Run
|
|
25935
|
-
if (!
|
|
25936
|
-
console.error(`Error: File not found: ${
|
|
25937
|
-
process.exit(1);
|
|
25938
|
-
}
|
|
25939
|
-
const configs = parseTasksFile(tasksFile);
|
|
25940
|
-
if (configs.length === 0) {
|
|
25941
|
-
console.error("No tasks found in file");
|
|
27191
|
+
program2.command("run").description("Run goal.yaml (autonomous loop) or tasks.yaml (batch jobs)").argument("<file>", "Path to goal.yaml or tasks.yaml").option("-o, --output <file>", "Output file for results JSON").option("-q, --quiet", "Minimal output").option("-s, --state-file <file>", "Custom state file path").option("--notify", "Send push notification via ntfy.sh").option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC).option("-r, --report <file>", "Generate markdown report").option("--sandbox <dir>", "Sandbox directory (restrict file access)").option("--max-turns <n>", "Max agent iterations per task", String(DEFAULT_MAX_TURNS)).option("--max-iterations <n>", "Max build loop iterations (goal mode)", String(DEFAULT_MAX_ITERATIONS)).option("--audit-log <file>", "Audit log file path").option("--no-security", "Disable default security (deny patterns)").action(async (inputFile, opts) => {
|
|
27192
|
+
if (!existsSync7(inputFile)) {
|
|
27193
|
+
console.error(`Error: File not found: ${inputFile}`);
|
|
25942
27194
|
process.exit(1);
|
|
25943
27195
|
}
|
|
25944
|
-
|
|
25945
|
-
|
|
25946
|
-
|
|
25947
|
-
|
|
25948
|
-
|
|
25949
|
-
|
|
25950
|
-
|
|
25951
|
-
|
|
25952
|
-
|
|
25953
|
-
|
|
25954
|
-
|
|
25955
|
-
|
|
25956
|
-
|
|
27196
|
+
if (isGoalFile(inputFile)) {
|
|
27197
|
+
const goal = parseGoalFile(inputFile);
|
|
27198
|
+
if (opts.maxIterations) {
|
|
27199
|
+
goal.max_iterations = parseInt(opts.maxIterations, 10);
|
|
27200
|
+
}
|
|
27201
|
+
if (opts.sandbox) {
|
|
27202
|
+
goal.defaults = goal.defaults ?? {};
|
|
27203
|
+
goal.defaults.security = goal.defaults.security ?? {};
|
|
27204
|
+
goal.defaults.security.sandbox_dir = opts.sandbox;
|
|
27205
|
+
}
|
|
27206
|
+
if (opts.maxTurns) {
|
|
27207
|
+
goal.defaults = goal.defaults ?? {};
|
|
27208
|
+
goal.defaults.security = goal.defaults.security ?? {};
|
|
27209
|
+
goal.defaults.security.max_turns = parseInt(opts.maxTurns, 10);
|
|
27210
|
+
}
|
|
27211
|
+
const log = opts.quiet ? undefined : (msg) => console.log(msg);
|
|
27212
|
+
const startTime = Date.now();
|
|
27213
|
+
const runState = await runGoal(goal, {
|
|
27214
|
+
stateFile: opts.stateFile ?? DEFAULT_GOAL_STATE_FILE,
|
|
27215
|
+
log
|
|
27216
|
+
});
|
|
27217
|
+
const totalDuration = (Date.now() - startTime) / 1000;
|
|
27218
|
+
if (opts.notify) {
|
|
27219
|
+
const passed = runState.status === "gate_passed";
|
|
27220
|
+
const title = passed ? `overnight: Goal completed (${runState.iterations.length} iterations)` : `overnight: ${runState.status} after ${runState.iterations.length} iterations`;
|
|
27221
|
+
const message = passed ? `Gate passed. ${runState.iterations.length} iterations.` : `Status: ${runState.status}. Check report for details.`;
|
|
27222
|
+
try {
|
|
27223
|
+
await fetch(`https://ntfy.sh/${opts.notifyTopic ?? DEFAULT_NTFY_TOPIC}`, {
|
|
27224
|
+
method: "POST",
|
|
27225
|
+
headers: {
|
|
27226
|
+
Title: title,
|
|
27227
|
+
Priority: passed ? "default" : "high",
|
|
27228
|
+
Tags: passed ? "white_check_mark" : "warning"
|
|
27229
|
+
},
|
|
27230
|
+
body: message
|
|
27231
|
+
});
|
|
27232
|
+
if (!opts.quiet)
|
|
27233
|
+
console.log(`\x1B[2mNotification sent\x1B[0m`);
|
|
27234
|
+
} catch {
|
|
27235
|
+
if (!opts.quiet)
|
|
27236
|
+
console.log("\x1B[33mWarning: Failed to send notification\x1B[0m");
|
|
27237
|
+
}
|
|
27238
|
+
}
|
|
27239
|
+
if (!opts.quiet) {
|
|
27240
|
+
console.log(`
|
|
27241
|
+
\x1B[1m━━━ Goal Run Summary ━━━\x1B[0m`);
|
|
27242
|
+
console.log(`Status: ${runState.status === "gate_passed" ? "\x1B[32m" : "\x1B[31m"}${runState.status}\x1B[0m`);
|
|
27243
|
+
console.log(`Iterations: ${runState.iterations.length}`);
|
|
27244
|
+
console.log(`Gate attempts: ${runState.gate_results.length}`);
|
|
27245
|
+
let durationStr;
|
|
27246
|
+
if (totalDuration >= 3600) {
|
|
27247
|
+
const hours = Math.floor(totalDuration / 3600);
|
|
27248
|
+
const mins = Math.floor(totalDuration % 3600 / 60);
|
|
27249
|
+
durationStr = `${hours}h ${mins}m`;
|
|
27250
|
+
} else if (totalDuration >= 60) {
|
|
27251
|
+
const mins = Math.floor(totalDuration / 60);
|
|
27252
|
+
const secs = Math.floor(totalDuration % 60);
|
|
27253
|
+
durationStr = `${mins}m ${secs}s`;
|
|
27254
|
+
} else {
|
|
27255
|
+
durationStr = `${totalDuration.toFixed(1)}s`;
|
|
27256
|
+
}
|
|
27257
|
+
console.log(`Duration: ${durationStr}`);
|
|
27258
|
+
if (runState.gate_results.length > 0) {
|
|
27259
|
+
const lastGate = runState.gate_results[runState.gate_results.length - 1];
|
|
27260
|
+
console.log(`
|
|
27261
|
+
Gate: ${lastGate.summary}`);
|
|
27262
|
+
for (const check2 of lastGate.checks) {
|
|
27263
|
+
const icon = check2.passed ? "\x1B[32m✓\x1B[0m" : "\x1B[31m✗\x1B[0m";
|
|
27264
|
+
console.log(` ${icon} ${check2.name}`);
|
|
27265
|
+
}
|
|
27266
|
+
}
|
|
27267
|
+
}
|
|
27268
|
+
if (runState.status !== "gate_passed") {
|
|
27269
|
+
process.exit(1);
|
|
27270
|
+
}
|
|
27271
|
+
} else {
|
|
27272
|
+
const cliSecurity = opts.security === false ? undefined : {
|
|
27273
|
+
...opts.sandbox && { sandbox_dir: opts.sandbox },
|
|
27274
|
+
...opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) },
|
|
27275
|
+
...opts.auditLog && { audit_log: opts.auditLog }
|
|
27276
|
+
};
|
|
27277
|
+
const { configs, security } = parseTasksFile(inputFile, cliSecurity);
|
|
27278
|
+
if (configs.length === 0) {
|
|
27279
|
+
console.error("No tasks found in file");
|
|
27280
|
+
process.exit(1);
|
|
27281
|
+
}
|
|
27282
|
+
const existingState = loadState(opts.stateFile ?? DEFAULT_STATE_FILE);
|
|
27283
|
+
if (existingState) {
|
|
27284
|
+
const done = Object.keys(existingState.completed).length;
|
|
27285
|
+
const pending = configs.filter((c) => !(taskKey(c) in existingState.completed)).length;
|
|
27286
|
+
console.log(`\x1B[1movernight: Resuming — ${done} done, ${pending} remaining\x1B[0m`);
|
|
27287
|
+
console.log(`\x1B[2mLast checkpoint: ${existingState.timestamp}\x1B[0m`);
|
|
25957
27288
|
} else {
|
|
25958
|
-
console.log(
|
|
27289
|
+
console.log(`\x1B[1movernight: Running ${configs.length} jobs...\x1B[0m`);
|
|
27290
|
+
}
|
|
27291
|
+
if (security && !opts.quiet) {
|
|
27292
|
+
console.log("\x1B[2mSecurity:\x1B[0m");
|
|
27293
|
+
validateSecurityConfig(security);
|
|
27294
|
+
}
|
|
27295
|
+
console.log("");
|
|
27296
|
+
const log = opts.quiet ? undefined : (msg) => console.log(msg);
|
|
27297
|
+
const startTime = Date.now();
|
|
27298
|
+
const reloadConfigs = () => parseTasksFile(inputFile, cliSecurity).configs;
|
|
27299
|
+
const results = await runJobsWithState(configs, {
|
|
27300
|
+
stateFile: opts.stateFile,
|
|
27301
|
+
log,
|
|
27302
|
+
reloadConfigs
|
|
27303
|
+
});
|
|
27304
|
+
const totalDuration = (Date.now() - startTime) / 1000;
|
|
27305
|
+
if (opts.notify) {
|
|
27306
|
+
const success = await sendNtfyNotification(results, totalDuration, opts.notifyTopic);
|
|
27307
|
+
if (success) {
|
|
27308
|
+
console.log(`\x1B[2mNotification sent to ntfy.sh/${opts.notifyTopic}\x1B[0m`);
|
|
27309
|
+
} else {
|
|
27310
|
+
console.log("\x1B[33mWarning: Failed to send notification\x1B[0m");
|
|
27311
|
+
}
|
|
25959
27312
|
}
|
|
25960
|
-
|
|
25961
|
-
|
|
25962
|
-
|
|
25963
|
-
|
|
25964
|
-
|
|
25965
|
-
|
|
25966
|
-
|
|
25967
|
-
|
|
25968
|
-
|
|
25969
|
-
|
|
25970
|
-
console.log(`
|
|
27313
|
+
if (opts.report) {
|
|
27314
|
+
generateReport(results, totalDuration, opts.report);
|
|
27315
|
+
console.log(`\x1B[2mReport saved to ${opts.report}\x1B[0m`);
|
|
27316
|
+
}
|
|
27317
|
+
if (!opts.quiet) {
|
|
27318
|
+
printSummary(results);
|
|
27319
|
+
}
|
|
27320
|
+
if (opts.output) {
|
|
27321
|
+
writeFileSync5(opts.output, resultsToJson(results));
|
|
27322
|
+
console.log(`
|
|
25971
27323
|
\x1B[2mResults saved to ${opts.output}\x1B[0m`);
|
|
25972
|
-
|
|
25973
|
-
|
|
25974
|
-
|
|
27324
|
+
}
|
|
27325
|
+
if (results.some((r) => r.status !== "success")) {
|
|
27326
|
+
process.exit(1);
|
|
27327
|
+
}
|
|
25975
27328
|
}
|
|
25976
27329
|
});
|
|
25977
|
-
program2.command("resume").description("Resume a previous run from saved state").argument("<tasks-file>", "Path to tasks.yaml file").option("-o, --output <file>", "Output file for results JSON").option("-q, --quiet", "Minimal output").option("-s, --state-file <file>", "Custom state file path").option("--notify", "Send push notification via ntfy.sh").option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC).option("-r, --report <file>", "Generate markdown report").action(async (tasksFile, opts) => {
|
|
27330
|
+
program2.command("resume").description("Resume a previous run from saved state").argument("<tasks-file>", "Path to tasks.yaml file").option("-o, --output <file>", "Output file for results JSON").option("-q, --quiet", "Minimal output").option("-s, --state-file <file>", "Custom state file path").option("--notify", "Send push notification via ntfy.sh").option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC).option("-r, --report <file>", "Generate markdown report").option("--sandbox <dir>", "Sandbox directory (restrict file access)").option("--max-turns <n>", "Max agent iterations per task", String(DEFAULT_MAX_TURNS)).option("--audit-log <file>", "Audit log file path").option("--no-security", "Disable default security (deny patterns)").action(async (tasksFile, opts) => {
|
|
25978
27331
|
const stateFile = opts.stateFile ?? DEFAULT_STATE_FILE;
|
|
25979
27332
|
const state = loadState(stateFile);
|
|
25980
27333
|
if (!state) {
|
|
@@ -25982,30 +27335,36 @@ program2.command("resume").description("Resume a previous run from saved state")
|
|
|
25982
27335
|
console.error("Run 'overnight run' first to start jobs.");
|
|
25983
27336
|
process.exit(1);
|
|
25984
27337
|
}
|
|
25985
|
-
if (!
|
|
27338
|
+
if (!existsSync7(tasksFile)) {
|
|
25986
27339
|
console.error(`Error: File not found: ${tasksFile}`);
|
|
25987
27340
|
process.exit(1);
|
|
25988
27341
|
}
|
|
25989
|
-
const
|
|
27342
|
+
const cliSecurity = opts.security === false ? undefined : {
|
|
27343
|
+
...opts.sandbox && { sandbox_dir: opts.sandbox },
|
|
27344
|
+
...opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) },
|
|
27345
|
+
...opts.auditLog && { audit_log: opts.auditLog }
|
|
27346
|
+
};
|
|
27347
|
+
const { configs, security } = parseTasksFile(tasksFile, cliSecurity);
|
|
25990
27348
|
if (configs.length === 0) {
|
|
25991
27349
|
console.error("No tasks found in file");
|
|
25992
27350
|
process.exit(1);
|
|
25993
27351
|
}
|
|
25994
|
-
|
|
25995
|
-
|
|
25996
|
-
|
|
27352
|
+
const completedCount = Object.keys(state.completed).length;
|
|
27353
|
+
const pendingCount = configs.filter((c) => !(taskKey(c) in state.completed)).length;
|
|
27354
|
+
console.log(`\x1B[1movernight: Resuming — ${completedCount} done, ${pendingCount} remaining\x1B[0m`);
|
|
27355
|
+
console.log(`\x1B[2mLast checkpoint: ${state.timestamp}\x1B[0m`);
|
|
27356
|
+
if (security && !opts.quiet) {
|
|
27357
|
+
console.log("\x1B[2mSecurity:\x1B[0m");
|
|
27358
|
+
validateSecurityConfig(security);
|
|
25997
27359
|
}
|
|
25998
|
-
|
|
25999
|
-
console.log(`\x1B[1movernight: Resuming from job ${startIndex + 1}/${configs.length}...\x1B[0m`);
|
|
26000
|
-
console.log(`\x1B[2mLast checkpoint: ${state.timestamp}\x1B[0m
|
|
26001
|
-
`);
|
|
27360
|
+
console.log("");
|
|
26002
27361
|
const log = opts.quiet ? undefined : (msg) => console.log(msg);
|
|
26003
27362
|
const startTime = Date.now();
|
|
27363
|
+
const reloadConfigs = () => parseTasksFile(tasksFile, cliSecurity).configs;
|
|
26004
27364
|
const results = await runJobsWithState(configs, {
|
|
26005
27365
|
stateFile,
|
|
26006
27366
|
log,
|
|
26007
|
-
|
|
26008
|
-
priorResults: state.results
|
|
27367
|
+
reloadConfigs
|
|
26009
27368
|
});
|
|
26010
27369
|
const totalDuration = (Date.now() - startTime) / 1000;
|
|
26011
27370
|
if (opts.notify) {
|
|
@@ -26024,7 +27383,7 @@ program2.command("resume").description("Resume a previous run from saved state")
|
|
|
26024
27383
|
printSummary(results);
|
|
26025
27384
|
}
|
|
26026
27385
|
if (opts.output) {
|
|
26027
|
-
|
|
27386
|
+
writeFileSync5(opts.output, resultsToJson(results));
|
|
26028
27387
|
console.log(`
|
|
26029
27388
|
\x1B[2mResults saved to ${opts.output}\x1B[0m`);
|
|
26030
27389
|
}
|
|
@@ -26032,12 +27391,18 @@ program2.command("resume").description("Resume a previous run from saved state")
|
|
|
26032
27391
|
process.exit(1);
|
|
26033
27392
|
}
|
|
26034
27393
|
});
|
|
26035
|
-
program2.command("single").description("Run a single job directly").argument("<prompt>", "The task prompt").option("-t, --timeout <seconds>", "Timeout in seconds", "300").option("--verify", "Run verification pass", true).option("--no-verify", "Skip verification pass").option("-T, --tools <tool...>", "Allowed tools").action(async (prompt, opts) => {
|
|
27394
|
+
program2.command("single").description("Run a single job directly").argument("<prompt>", "The task prompt").option("-t, --timeout <seconds>", "Timeout in seconds", "300").option("--verify", "Run verification pass", true).option("--no-verify", "Skip verification pass").option("-T, --tools <tool...>", "Allowed tools").option("--sandbox <dir>", "Sandbox directory (restrict file access)").option("--max-turns <n>", "Max agent iterations", String(DEFAULT_MAX_TURNS)).option("--no-security", "Disable default security (deny patterns)").action(async (prompt, opts) => {
|
|
27395
|
+
const security = opts.security === false ? undefined : {
|
|
27396
|
+
...opts.sandbox && { sandbox_dir: opts.sandbox },
|
|
27397
|
+
...opts.maxTurns && { max_turns: parseInt(opts.maxTurns, 10) },
|
|
27398
|
+
deny_patterns: DEFAULT_DENY_PATTERNS
|
|
27399
|
+
};
|
|
26036
27400
|
const config2 = {
|
|
26037
27401
|
prompt,
|
|
26038
27402
|
timeout_seconds: parseInt(opts.timeout, 10),
|
|
26039
27403
|
verify: opts.verify,
|
|
26040
|
-
allowed_tools: opts.tools
|
|
27404
|
+
allowed_tools: opts.tools,
|
|
27405
|
+
security
|
|
26041
27406
|
};
|
|
26042
27407
|
const log = (msg) => console.log(msg);
|
|
26043
27408
|
const result = await runJob(config2, log);
|
|
@@ -26056,13 +27421,97 @@ program2.command("single").description("Run a single job directly").argument("<p
|
|
|
26056
27421
|
process.exit(1);
|
|
26057
27422
|
}
|
|
26058
27423
|
});
|
|
26059
|
-
program2.command("
|
|
26060
|
-
const
|
|
27424
|
+
program2.command("hammer").description("Autonomous build loop from an inline goal string").argument("<goal>", "The goal to work toward").option("--max-iterations <n>", "Max build loop iterations", String(DEFAULT_MAX_ITERATIONS)).option("--max-turns <n>", "Max agent turns per iteration", String(DEFAULT_MAX_TURNS)).option("-t, --timeout <seconds>", "Timeout per iteration in seconds", "600").option("-T, --tools <tool...>", "Allowed tools").option("--sandbox <dir>", "Sandbox directory").option("-s, --state-file <file>", "Custom state file path").option("--notify", "Send push notification via ntfy.sh").option("--notify-topic <topic>", "ntfy.sh topic", DEFAULT_NTFY_TOPIC).option("-q, --quiet", "Minimal output").option("--no-security", "Disable default security").action(async (goalStr, opts) => {
|
|
27425
|
+
const goal = {
|
|
27426
|
+
goal: goalStr,
|
|
27427
|
+
max_iterations: parseInt(opts.maxIterations, 10),
|
|
27428
|
+
defaults: {
|
|
27429
|
+
timeout_seconds: parseInt(opts.timeout, 10),
|
|
27430
|
+
allowed_tools: opts.tools ?? [...DEFAULT_TOOLS, "Bash"],
|
|
27431
|
+
security: opts.security === false ? undefined : {
|
|
27432
|
+
...opts.sandbox && { sandbox_dir: opts.sandbox },
|
|
27433
|
+
max_turns: parseInt(opts.maxTurns, 10),
|
|
27434
|
+
deny_patterns: DEFAULT_DENY_PATTERNS
|
|
27435
|
+
}
|
|
27436
|
+
}
|
|
27437
|
+
};
|
|
27438
|
+
const log = opts.quiet ? undefined : (msg) => console.log(msg);
|
|
27439
|
+
const startTime = Date.now();
|
|
27440
|
+
const runState = await runGoal(goal, {
|
|
27441
|
+
stateFile: opts.stateFile ?? DEFAULT_GOAL_STATE_FILE,
|
|
27442
|
+
log
|
|
27443
|
+
});
|
|
27444
|
+
const totalDuration = (Date.now() - startTime) / 1000;
|
|
27445
|
+
if (opts.notify) {
|
|
27446
|
+
const passed = runState.status === "gate_passed";
|
|
27447
|
+
try {
|
|
27448
|
+
await fetch(`https://ntfy.sh/${opts.notifyTopic ?? DEFAULT_NTFY_TOPIC}`, {
|
|
27449
|
+
method: "POST",
|
|
27450
|
+
headers: {
|
|
27451
|
+
Title: passed ? `overnight: Goal completed (${runState.iterations.length} iterations)` : `overnight: ${runState.status} after ${runState.iterations.length} iterations`,
|
|
27452
|
+
Priority: passed ? "default" : "high",
|
|
27453
|
+
Tags: passed ? "white_check_mark" : "warning"
|
|
27454
|
+
},
|
|
27455
|
+
body: passed ? `Gate passed. ${runState.iterations.length} iterations.` : `Status: ${runState.status}. Check report for details.`
|
|
27456
|
+
});
|
|
27457
|
+
if (!opts.quiet)
|
|
27458
|
+
console.log(`\x1B[2mNotification sent\x1B[0m`);
|
|
27459
|
+
} catch {
|
|
27460
|
+
if (!opts.quiet)
|
|
27461
|
+
console.log("\x1B[33mWarning: Failed to send notification\x1B[0m");
|
|
27462
|
+
}
|
|
27463
|
+
}
|
|
27464
|
+
if (!opts.quiet) {
|
|
27465
|
+
console.log(`
|
|
27466
|
+
\x1B[1m━━━ Hammer Summary ━━━\x1B[0m`);
|
|
27467
|
+
console.log(`Status: ${runState.status === "gate_passed" ? "\x1B[32m" : "\x1B[31m"}${runState.status}\x1B[0m`);
|
|
27468
|
+
console.log(`Iterations: ${runState.iterations.length}`);
|
|
27469
|
+
console.log(`Gate attempts: ${runState.gate_results.length}`);
|
|
27470
|
+
let durationStr;
|
|
27471
|
+
if (totalDuration >= 3600) {
|
|
27472
|
+
const hours = Math.floor(totalDuration / 3600);
|
|
27473
|
+
const mins = Math.floor(totalDuration % 3600 / 60);
|
|
27474
|
+
durationStr = `${hours}h ${mins}m`;
|
|
27475
|
+
} else if (totalDuration >= 60) {
|
|
27476
|
+
const mins = Math.floor(totalDuration / 60);
|
|
27477
|
+
const secs = Math.floor(totalDuration % 60);
|
|
27478
|
+
durationStr = `${mins}m ${secs}s`;
|
|
27479
|
+
} else {
|
|
27480
|
+
durationStr = `${totalDuration.toFixed(1)}s`;
|
|
27481
|
+
}
|
|
27482
|
+
console.log(`Duration: ${durationStr}`);
|
|
27483
|
+
if (runState.gate_results.length > 0) {
|
|
27484
|
+
const lastGate = runState.gate_results[runState.gate_results.length - 1];
|
|
27485
|
+
console.log(`
|
|
27486
|
+
Gate: ${lastGate.summary}`);
|
|
27487
|
+
for (const check2 of lastGate.checks) {
|
|
27488
|
+
const icon = check2.passed ? "\x1B[32m✓\x1B[0m" : "\x1B[31m✗\x1B[0m";
|
|
27489
|
+
console.log(` ${icon} ${check2.name}`);
|
|
27490
|
+
}
|
|
27491
|
+
}
|
|
27492
|
+
}
|
|
27493
|
+
if (runState.status !== "gate_passed") {
|
|
27494
|
+
process.exit(1);
|
|
27495
|
+
}
|
|
27496
|
+
});
|
|
27497
|
+
program2.command("plan").description("Interactive design session to create a goal.yaml").argument("<goal>", "High-level goal description").option("-o, --output <file>", "Output file path", "goal.yaml").action(async (goal, opts) => {
|
|
27498
|
+
const result = await runPlanner(goal, {
|
|
27499
|
+
outputFile: opts.output,
|
|
27500
|
+
log: (msg) => console.log(msg)
|
|
27501
|
+
});
|
|
27502
|
+
if (!result) {
|
|
27503
|
+
process.exit(1);
|
|
27504
|
+
}
|
|
27505
|
+
});
|
|
27506
|
+
program2.command("init").description("Create an example goal.yaml or tasks.yaml").option("--tasks", "Create tasks.yaml instead of goal.yaml").action((opts) => {
|
|
27507
|
+
if (opts.tasks) {
|
|
27508
|
+
const example = `# overnight task file
|
|
26061
27509
|
# Run with: overnight run tasks.yaml
|
|
26062
27510
|
|
|
26063
27511
|
defaults:
|
|
26064
27512
|
timeout_seconds: 300 # 5 minutes per task
|
|
26065
27513
|
verify: true # Run verification after each task
|
|
27514
|
+
|
|
26066
27515
|
# Secure defaults - no Bash, just file operations
|
|
26067
27516
|
allowed_tools:
|
|
26068
27517
|
- Read
|
|
@@ -26071,6 +27520,12 @@ defaults:
|
|
|
26071
27520
|
- Glob
|
|
26072
27521
|
- Grep
|
|
26073
27522
|
|
|
27523
|
+
# Security settings (optional - deny_patterns enabled by default)
|
|
27524
|
+
security:
|
|
27525
|
+
sandbox_dir: "." # Restrict to current directory
|
|
27526
|
+
max_turns: 100 # Prevent runaway agents
|
|
27527
|
+
# audit_log: "overnight-audit.log" # Uncomment to enable
|
|
27528
|
+
|
|
26074
27529
|
tasks:
|
|
26075
27530
|
# Simple string format
|
|
26076
27531
|
- "Find and fix any TODO comments in the codebase"
|
|
@@ -26091,12 +27546,62 @@ tasks:
|
|
|
26091
27546
|
- Glob
|
|
26092
27547
|
- Grep
|
|
26093
27548
|
`;
|
|
26094
|
-
|
|
26095
|
-
|
|
26096
|
-
|
|
27549
|
+
if (existsSync7("tasks.yaml")) {
|
|
27550
|
+
console.log("\x1B[33mtasks.yaml already exists\x1B[0m");
|
|
27551
|
+
process.exit(1);
|
|
27552
|
+
}
|
|
27553
|
+
writeFileSync5("tasks.yaml", example);
|
|
27554
|
+
console.log("\x1B[32mCreated tasks.yaml\x1B[0m");
|
|
27555
|
+
console.log("Edit the file, then run: \x1B[1movernight run tasks.yaml\x1B[0m");
|
|
27556
|
+
} else {
|
|
27557
|
+
const example = `# overnight goal file
|
|
27558
|
+
# Run with: overnight run goal.yaml
|
|
27559
|
+
#
|
|
27560
|
+
# Or use "overnight plan" for an interactive design session:
|
|
27561
|
+
# overnight plan "Build a multiplayer game"
|
|
27562
|
+
|
|
27563
|
+
goal: "Describe your project goal here"
|
|
27564
|
+
|
|
27565
|
+
acceptance_criteria:
|
|
27566
|
+
- "The project builds without errors"
|
|
27567
|
+
- "All tests pass"
|
|
27568
|
+
- "Core features are functional"
|
|
27569
|
+
|
|
27570
|
+
verification_commands:
|
|
27571
|
+
- "npm run build"
|
|
27572
|
+
- "npm test"
|
|
27573
|
+
|
|
27574
|
+
constraints:
|
|
27575
|
+
- "Don't modify existing API contracts"
|
|
27576
|
+
- "Keep dependencies minimal"
|
|
27577
|
+
|
|
27578
|
+
# How many build iterations before stopping
|
|
27579
|
+
max_iterations: 15
|
|
27580
|
+
|
|
27581
|
+
# Stop if remaining items don't shrink for this many iterations
|
|
27582
|
+
convergence_threshold: 3
|
|
27583
|
+
|
|
27584
|
+
defaults:
|
|
27585
|
+
timeout_seconds: 600 # 10 minutes per iteration
|
|
27586
|
+
allowed_tools:
|
|
27587
|
+
- Read
|
|
27588
|
+
- Edit
|
|
27589
|
+
- Write
|
|
27590
|
+
- Glob
|
|
27591
|
+
- Grep
|
|
27592
|
+
- Bash
|
|
27593
|
+
security:
|
|
27594
|
+
sandbox_dir: "."
|
|
27595
|
+
max_turns: 150
|
|
27596
|
+
`;
|
|
27597
|
+
if (existsSync7("goal.yaml")) {
|
|
27598
|
+
console.log("\x1B[33mgoal.yaml already exists\x1B[0m");
|
|
27599
|
+
process.exit(1);
|
|
27600
|
+
}
|
|
27601
|
+
writeFileSync5("goal.yaml", example);
|
|
27602
|
+
console.log("\x1B[32mCreated goal.yaml\x1B[0m");
|
|
27603
|
+
console.log("Edit the file, then run: \x1B[1movernight run goal.yaml\x1B[0m");
|
|
27604
|
+
console.log(`\x1B[2mTip: Use 'overnight plan "your goal"' for an interactive design session\x1B[0m`);
|
|
26097
27605
|
}
|
|
26098
|
-
writeFileSync3("tasks.yaml", example);
|
|
26099
|
-
console.log("\x1B[32mCreated tasks.yaml\x1B[0m");
|
|
26100
|
-
console.log("Edit the file, then run: \x1B[1movernight run tasks.yaml\x1B[0m");
|
|
26101
27606
|
});
|
|
26102
27607
|
program2.parse();
|