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