gnhf 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -12
- package/dist/cli.mjs +153 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ gnhf is a [ralph](https://ghuntley.com/ralph/), [autoresearch](https://github.co
|
|
|
45
45
|
You wake up to a branch full of clean work and a log of everything that happened.
|
|
46
46
|
|
|
47
47
|
- **Dead simple** — one command starts an autonomous loop that runs until you Ctrl+C or a configured runtime cap is reached
|
|
48
|
-
- **Long running** — each iteration is committed on success, rolled back on failure, with sensible retries
|
|
48
|
+
- **Long running** — each iteration is committed on success, rolled back on failure, with sensible retries; hard agent errors back off exponentially while agent-reported failures continue immediately
|
|
49
49
|
- **Live terminal title** — interactive runs keep your terminal title updated with live status, token totals, and commit count, then restore the previous title on exit
|
|
50
50
|
- **Agent-agnostic** — works with Claude Code, Codex, Rovo Dev, or OpenCode out of the box
|
|
51
51
|
|
|
@@ -122,7 +122,7 @@ npm link
|
|
|
122
122
|
┌──────────┐ ┌───────────┐ │
|
|
123
123
|
│ commit │ │ git reset │ │
|
|
124
124
|
│ append │ │ --hard │ │
|
|
125
|
-
│ notes.md │ │
|
|
125
|
+
│ notes.md │ │ maybe wait│ │
|
|
126
126
|
└────┬─────┘ └─────┬─────┘ │
|
|
127
127
|
│ │ │
|
|
128
128
|
│ ┌──────────┘ │
|
|
@@ -136,10 +136,11 @@ npm link
|
|
|
136
136
|
```
|
|
137
137
|
|
|
138
138
|
- **Incremental commits** — each successful iteration is a separate git commit, so you can cherry-pick or revert individual changes
|
|
139
|
+
- **Failure handling** - all failed iterations are rolled back with `git reset --hard`; agent-reported failures proceed to the next iteration immediately, while hard agent errors use exponential backoff
|
|
139
140
|
- **Runtime caps** - `--max-iterations` stops before the next iteration begins, `--max-tokens` can abort mid-iteration once reported usage reaches the cap, and `--stop-when` ends the loop after an iteration whose agent output reports the natural-language condition is met; uncommitted work is rolled back in either case, and in the interactive TUI the final state remains visible until you press Ctrl+C to exit
|
|
140
141
|
- **Shared memory** — the agent reads `notes.md` (built up from prior iterations) to communicate across iterations
|
|
141
142
|
- **Local run metadata** — gnhf stores prompt, notes, and resume metadata under `.gnhf/runs/` and ignores it locally, so your branch only contains intentional work
|
|
142
|
-
- **Resume support** — run `gnhf` while on an existing `gnhf/` branch to pick up where a previous run left off
|
|
143
|
+
- **Resume support** — run `gnhf` while on an existing `gnhf/` branch to pick up where a previous run left off; if you provide a different prompt, gnhf asks whether to update the saved prompt and continue with the existing history, start a new branch, or quit
|
|
143
144
|
|
|
144
145
|
### Worktree Mode
|
|
145
146
|
|
|
@@ -165,17 +166,19 @@ Pass `--worktree` to run each agent in an isolated [git worktree](https://git-sc
|
|
|
165
166
|
| `echo "<prompt>" \| gnhf` | Pipe prompt via stdin |
|
|
166
167
|
| `cat prd.md \| gnhf` | Pipe a large spec or PRD via stdin |
|
|
167
168
|
|
|
169
|
+
If you run `gnhf` on an existing `gnhf/` branch with a different prompt, gnhf asks whether to update `prompt.md` and continue the existing run history, start a new branch, or quit. When the prompt came from stdin, that confirmation is read from the controlling terminal, so it must be available.
|
|
170
|
+
|
|
168
171
|
### Flags
|
|
169
172
|
|
|
170
|
-
| Flag | Description
|
|
171
|
-
| ------------------------ |
|
|
172
|
-
| `--agent <agent>` | Agent to use (`claude`, `codex`, `rovodev`, or `opencode`)
|
|
173
|
-
| `--max-iterations <n>` | Abort after `n` total iterations
|
|
174
|
-
| `--max-tokens <n>` | Abort after `n` total input+output tokens
|
|
175
|
-
| `--stop-when <cond>` | End the loop when the agent reports this natural-language condition is met | unlimited
|
|
176
|
-
| `--prevent-sleep <mode>` | Prevent system sleep during the run (`on`/`off` or `true`/`false`)
|
|
177
|
-
| `--worktree` | Run in a separate git worktree (enables multiple agents concurrently)
|
|
178
|
-
| `--version` | Show version
|
|
173
|
+
| Flag | Description | Default |
|
|
174
|
+
| ------------------------ | -------------------------------------------------------------------------- | ---------------------- |
|
|
175
|
+
| `--agent <agent>` | Agent to use (`claude`, `codex`, `rovodev`, or `opencode`) | config file (`claude`) |
|
|
176
|
+
| `--max-iterations <n>` | Abort after `n` total iterations | unlimited |
|
|
177
|
+
| `--max-tokens <n>` | Abort after `n` total input+output tokens | unlimited |
|
|
178
|
+
| `--stop-when <cond>` | End the loop when the agent reports this natural-language condition is met | unlimited |
|
|
179
|
+
| `--prevent-sleep <mode>` | Prevent system sleep during the run (`on`/`off` or `true`/`false`) | config file (`on`) |
|
|
180
|
+
| `--worktree` | Run in a separate git worktree (enables multiple agents concurrently) | `false` |
|
|
181
|
+
| `--version` | Show version | |
|
|
179
182
|
|
|
180
183
|
## Configuration
|
|
181
184
|
|
|
@@ -249,6 +252,8 @@ Including a snippet of `gnhf.log` is the single most useful thing you can attach
|
|
|
249
252
|
|
|
250
253
|
## Development
|
|
251
254
|
|
|
255
|
+
If you want to contribute changes back to this repo, see [`CONTRIBUTING.md`](./CONTRIBUTING.md). Human-authored PRs targeting `main` must be opened via `git push no-mistakes` so the required `Require no-mistakes` check passes.
|
|
256
|
+
|
|
252
257
|
```sh
|
|
253
258
|
npm run build # Build with tsdown
|
|
254
259
|
npm run dev # Watch mode
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { appendFileSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, closeSync, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import process$1 from "node:process";
|
|
@@ -427,10 +427,8 @@ function removeWorktree(baseCwd, worktreePath) {
|
|
|
427
427
|
}
|
|
428
428
|
//#endregion
|
|
429
429
|
//#region src/core/agents/types.ts
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
additionalProperties: false,
|
|
433
|
-
properties: {
|
|
430
|
+
function buildAgentOutputSchema(opts) {
|
|
431
|
+
const properties = {
|
|
434
432
|
success: { type: "boolean" },
|
|
435
433
|
summary: { type: "string" },
|
|
436
434
|
key_changes_made: {
|
|
@@ -440,21 +438,30 @@ const AGENT_OUTPUT_SCHEMA = {
|
|
|
440
438
|
key_learnings: {
|
|
441
439
|
type: "array",
|
|
442
440
|
items: { type: "string" }
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
required: [
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
const required = [
|
|
447
444
|
"success",
|
|
448
445
|
"summary",
|
|
449
446
|
"key_changes_made",
|
|
450
447
|
"key_learnings"
|
|
451
|
-
]
|
|
452
|
-
|
|
448
|
+
];
|
|
449
|
+
if (opts.includeStopField) {
|
|
450
|
+
properties.should_fully_stop = { type: "boolean" };
|
|
451
|
+
required.push("should_fully_stop");
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
type: "object",
|
|
455
|
+
additionalProperties: false,
|
|
456
|
+
properties,
|
|
457
|
+
required
|
|
458
|
+
};
|
|
459
|
+
}
|
|
453
460
|
//#endregion
|
|
454
461
|
//#region src/core/run.ts
|
|
455
462
|
const LOG_FILENAME = "gnhf.log";
|
|
456
|
-
function writeSchemaFile(schemaPath) {
|
|
457
|
-
writeFileSync(schemaPath, JSON.stringify(
|
|
463
|
+
function writeSchemaFile(schemaPath, includeStopField) {
|
|
464
|
+
writeFileSync(schemaPath, JSON.stringify(buildAgentOutputSchema({ includeStopField }), null, 2), "utf-8");
|
|
458
465
|
}
|
|
459
466
|
function ensureRunMetadataIgnored(cwd) {
|
|
460
467
|
const excludePath = execFileSync("git", [
|
|
@@ -474,16 +481,16 @@ function ensureRunMetadataIgnored(cwd) {
|
|
|
474
481
|
appendFileSync(resolved, `${content.length > 0 && !content.endsWith("\n") ? "\n" : ""}${entry}\n`, "utf-8");
|
|
475
482
|
} else writeFileSync(resolved, `${entry}\n`, "utf-8");
|
|
476
483
|
}
|
|
477
|
-
function setupRun(runId, prompt, baseCommit, cwd) {
|
|
484
|
+
function setupRun(runId, prompt, baseCommit, cwd, schemaOptions) {
|
|
478
485
|
ensureRunMetadataIgnored(cwd);
|
|
479
486
|
const runDir = join(cwd, ".gnhf", "runs", runId);
|
|
480
487
|
mkdirSync(runDir, { recursive: true });
|
|
481
488
|
const promptPath = join(runDir, "prompt.md");
|
|
482
489
|
writeFileSync(promptPath, prompt, "utf-8");
|
|
483
490
|
const notesPath = join(runDir, "notes.md");
|
|
484
|
-
writeFileSync(notesPath, `# gnhf run: ${runId}\n\nObjective:
|
|
491
|
+
if (!existsSync(notesPath)) writeFileSync(notesPath, `# gnhf run: ${runId}\n\nObjective: see .gnhf/runs/${runId}/prompt.md\n\n## Iteration Log\n`, "utf-8");
|
|
485
492
|
const schemaPath = join(runDir, "output-schema.json");
|
|
486
|
-
writeSchemaFile(schemaPath);
|
|
493
|
+
writeSchemaFile(schemaPath, schemaOptions.includeStopField);
|
|
487
494
|
const logPath = join(runDir, LOG_FILENAME);
|
|
488
495
|
const baseCommitPath = join(runDir, "base-commit");
|
|
489
496
|
const hasStoredBaseCommit = existsSync(baseCommitPath);
|
|
@@ -500,13 +507,13 @@ function setupRun(runId, prompt, baseCommit, cwd) {
|
|
|
500
507
|
baseCommitPath
|
|
501
508
|
};
|
|
502
509
|
}
|
|
503
|
-
function resumeRun(runId, cwd) {
|
|
510
|
+
function resumeRun(runId, cwd, schemaOptions) {
|
|
504
511
|
const runDir = join(cwd, ".gnhf", "runs", runId);
|
|
505
512
|
if (!existsSync(runDir)) throw new Error(`Run directory not found: ${runDir}`);
|
|
506
513
|
const promptPath = join(runDir, "prompt.md");
|
|
507
514
|
const notesPath = join(runDir, "notes.md");
|
|
508
515
|
const schemaPath = join(runDir, "output-schema.json");
|
|
509
|
-
writeSchemaFile(schemaPath);
|
|
516
|
+
writeSchemaFile(schemaPath, schemaOptions.includeStopField);
|
|
510
517
|
const logPath = join(runDir, LOG_FILENAME);
|
|
511
518
|
const baseCommitPath = join(runDir, "base-commit");
|
|
512
519
|
return {
|
|
@@ -1068,7 +1075,7 @@ function terminateClaudeProcess(child, platform) {
|
|
|
1068
1075
|
}
|
|
1069
1076
|
child.kill("SIGTERM");
|
|
1070
1077
|
}
|
|
1071
|
-
function buildClaudeArgs(prompt, extraArgs) {
|
|
1078
|
+
function buildClaudeArgs(prompt, schema, extraArgs) {
|
|
1072
1079
|
const userArgs = extraArgs ?? [];
|
|
1073
1080
|
const userSpecifiedPermissionMode = userArgs.some((arg) => arg === "--dangerously-skip-permissions" || arg === "--permission-mode" || arg.startsWith("--permission-mode=") || arg === "--permission-prompt-tool" || arg.startsWith("--permission-prompt-tool="));
|
|
1074
1081
|
return [
|
|
@@ -1079,7 +1086,7 @@ function buildClaudeArgs(prompt, extraArgs) {
|
|
|
1079
1086
|
"--output-format",
|
|
1080
1087
|
"stream-json",
|
|
1081
1088
|
"--json-schema",
|
|
1082
|
-
JSON.stringify(
|
|
1089
|
+
JSON.stringify(schema),
|
|
1083
1090
|
...userSpecifiedPermissionMode ? [] : ["--dangerously-skip-permissions"]
|
|
1084
1091
|
];
|
|
1085
1092
|
}
|
|
@@ -1102,17 +1109,19 @@ var ClaudeAgent = class {
|
|
|
1102
1109
|
bin;
|
|
1103
1110
|
extraArgs;
|
|
1104
1111
|
platform;
|
|
1112
|
+
schema;
|
|
1105
1113
|
constructor(binOrDeps = {}) {
|
|
1106
1114
|
const deps = typeof binOrDeps === "string" ? { bin: binOrDeps } : binOrDeps;
|
|
1107
1115
|
this.bin = deps.bin ?? "claude";
|
|
1108
1116
|
this.extraArgs = deps.extraArgs;
|
|
1109
1117
|
this.platform = deps.platform ?? process.platform;
|
|
1118
|
+
this.schema = deps.schema ?? buildAgentOutputSchema({ includeStopField: false });
|
|
1110
1119
|
}
|
|
1111
1120
|
run(prompt, cwd, options) {
|
|
1112
1121
|
const { onUsage, onMessage, signal, logPath } = options ?? {};
|
|
1113
1122
|
return new Promise((resolve, reject) => {
|
|
1114
1123
|
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
1115
|
-
const child = spawn(this.bin, buildClaudeArgs(prompt, this.extraArgs), {
|
|
1124
|
+
const child = spawn(this.bin, buildClaudeArgs(prompt, this.schema, this.extraArgs), {
|
|
1116
1125
|
cwd,
|
|
1117
1126
|
shell: shouldUseWindowsShell$2(this.bin, this.platform),
|
|
1118
1127
|
stdio: [
|
|
@@ -1124,6 +1133,7 @@ var ClaudeAgent = class {
|
|
|
1124
1133
|
});
|
|
1125
1134
|
if (setupAbortHandler(signal, child, reject, () => terminateClaudeProcess(child, this.platform))) return;
|
|
1126
1135
|
let resultEvent = null;
|
|
1136
|
+
let latestResultUsage = null;
|
|
1127
1137
|
const cumulative = {
|
|
1128
1138
|
inputTokens: 0,
|
|
1129
1139
|
outputTokens: 0,
|
|
@@ -1192,7 +1202,11 @@ var ClaudeAgent = class {
|
|
|
1192
1202
|
}
|
|
1193
1203
|
}
|
|
1194
1204
|
}
|
|
1195
|
-
if (event.type === "result")
|
|
1205
|
+
if (event.type === "result") {
|
|
1206
|
+
const next = event;
|
|
1207
|
+
latestResultUsage = next.usage;
|
|
1208
|
+
if (next.is_error || next.subtype !== "success" || next.structured_output || !resultEvent) resultEvent = next;
|
|
1209
|
+
}
|
|
1196
1210
|
});
|
|
1197
1211
|
setupChildProcessHandlers(child, "claude", logStream, reject, () => {
|
|
1198
1212
|
if (!resultEvent) {
|
|
@@ -1208,7 +1222,7 @@ var ClaudeAgent = class {
|
|
|
1208
1222
|
return;
|
|
1209
1223
|
}
|
|
1210
1224
|
const output = resultEvent.structured_output;
|
|
1211
|
-
const usage = toTokenUsage(resultEvent.usage);
|
|
1225
|
+
const usage = toTokenUsage(latestResultUsage ?? resultEvent.usage);
|
|
1212
1226
|
onUsage?.(usage);
|
|
1213
1227
|
resolve({
|
|
1214
1228
|
output,
|
|
@@ -1339,25 +1353,27 @@ const BLANKET_PERMISSION_RULESET = [{
|
|
|
1339
1353
|
pattern: "*",
|
|
1340
1354
|
action: "allow"
|
|
1341
1355
|
}];
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1356
|
+
function buildStructuredOutputFormat(schema) {
|
|
1357
|
+
return {
|
|
1358
|
+
type: "json_schema",
|
|
1359
|
+
schema,
|
|
1360
|
+
retryCount: 1
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1347
1363
|
function buildOpencodeChildEnv() {
|
|
1348
1364
|
const env = { ...process.env };
|
|
1349
1365
|
delete env.OPENCODE_SERVER_USERNAME;
|
|
1350
1366
|
delete env.OPENCODE_SERVER_PASSWORD;
|
|
1351
1367
|
return env;
|
|
1352
1368
|
}
|
|
1353
|
-
function buildPrompt(prompt) {
|
|
1369
|
+
function buildPrompt(prompt, schema) {
|
|
1354
1370
|
return [
|
|
1355
1371
|
prompt,
|
|
1356
1372
|
"",
|
|
1357
1373
|
"When you finish, reply with only valid JSON.",
|
|
1358
1374
|
"Do not wrap the JSON in markdown fences.",
|
|
1359
1375
|
"Do not include any prose before or after the JSON.",
|
|
1360
|
-
`The JSON must match this schema exactly: ${JSON.stringify(
|
|
1376
|
+
`The JSON must match this schema exactly: ${JSON.stringify(schema)}`
|
|
1361
1377
|
].join("\n");
|
|
1362
1378
|
}
|
|
1363
1379
|
/**
|
|
@@ -1452,6 +1468,7 @@ var OpenCodeAgent = class {
|
|
|
1452
1468
|
getPortFn;
|
|
1453
1469
|
killProcessFn;
|
|
1454
1470
|
platform;
|
|
1471
|
+
schema;
|
|
1455
1472
|
spawnFn;
|
|
1456
1473
|
server = null;
|
|
1457
1474
|
closingPromise = null;
|
|
@@ -1462,6 +1479,7 @@ var OpenCodeAgent = class {
|
|
|
1462
1479
|
this.getPortFn = deps.getPort ?? getAvailablePort$1;
|
|
1463
1480
|
this.killProcessFn = deps.killProcess ?? process.kill.bind(process);
|
|
1464
1481
|
this.platform = deps.platform ?? process.platform;
|
|
1482
|
+
this.schema = deps.schema ?? buildAgentOutputSchema({ includeStopField: false });
|
|
1465
1483
|
this.spawnFn = deps.spawn ?? spawn;
|
|
1466
1484
|
}
|
|
1467
1485
|
async run(prompt, cwd, options) {
|
|
@@ -1487,7 +1505,7 @@ var OpenCodeAgent = class {
|
|
|
1487
1505
|
try {
|
|
1488
1506
|
const server = await this.ensureServer(cwd, runController.signal);
|
|
1489
1507
|
sessionId = await this.createSession(server, cwd, runController.signal);
|
|
1490
|
-
const result = await this.streamMessage(server, sessionId, buildPrompt(prompt), runController.signal, logStream, onUsage, onMessage);
|
|
1508
|
+
const result = await this.streamMessage(server, sessionId, buildPrompt(prompt, this.schema), runController.signal, logStream, onUsage, onMessage);
|
|
1491
1509
|
appendDebugLog("opencode:run:end", {
|
|
1492
1510
|
sessionId,
|
|
1493
1511
|
elapsedMs: Date.now() - runStartedAt,
|
|
@@ -1764,7 +1782,7 @@ var OpenCodeAgent = class {
|
|
|
1764
1782
|
type: "text",
|
|
1765
1783
|
text: prompt
|
|
1766
1784
|
}],
|
|
1767
|
-
format:
|
|
1785
|
+
format: buildStructuredOutputFormat(this.schema)
|
|
1768
1786
|
},
|
|
1769
1787
|
signal
|
|
1770
1788
|
});
|
|
@@ -2757,11 +2775,13 @@ function withTimeoutSignal(signal, timeoutMs) {
|
|
|
2757
2775
|
}
|
|
2758
2776
|
//#endregion
|
|
2759
2777
|
//#region src/core/agents/factory.ts
|
|
2760
|
-
function createAgent(name, runInfo, pathOverride, agentArgsOverride) {
|
|
2778
|
+
function createAgent(name, runInfo, pathOverride, agentArgsOverride, options) {
|
|
2779
|
+
const schema = buildAgentOutputSchema({ includeStopField: options.includeStopField });
|
|
2761
2780
|
switch (name) {
|
|
2762
2781
|
case "claude": return new ClaudeAgent({
|
|
2763
2782
|
bin: pathOverride,
|
|
2764
|
-
extraArgs: agentArgsOverride
|
|
2783
|
+
extraArgs: agentArgsOverride,
|
|
2784
|
+
schema
|
|
2765
2785
|
});
|
|
2766
2786
|
case "codex": return new CodexAgent(runInfo.schemaPath, {
|
|
2767
2787
|
bin: pathOverride,
|
|
@@ -2769,7 +2789,8 @@ function createAgent(name, runInfo, pathOverride, agentArgsOverride) {
|
|
|
2769
2789
|
});
|
|
2770
2790
|
case "opencode": return new OpenCodeAgent({
|
|
2771
2791
|
bin: pathOverride,
|
|
2772
|
-
extraArgs: agentArgsOverride
|
|
2792
|
+
extraArgs: agentArgsOverride,
|
|
2793
|
+
schema
|
|
2773
2794
|
});
|
|
2774
2795
|
case "rovodev": return new RovoDevAgent(runInfo.schemaPath, {
|
|
2775
2796
|
bin: pathOverride,
|
|
@@ -2787,7 +2808,7 @@ function buildIterationPrompt(params) {
|
|
|
2787
2808
|
"- key_learnings: an array of new learnings that were surprising, weren't captured by previous notes and would be informative for future iterations"
|
|
2788
2809
|
];
|
|
2789
2810
|
if (params.stopWhen !== void 0) outputFields.push("- should_fully_stop: set to true ONLY when the stop condition below is fully met and the entire loop should end. default to false");
|
|
2790
|
-
const stopConditionSection = params.stopWhen !== void 0 ? `\n\n## Stop Condition\n\nThe user has configured a condition to end the loop: ${params.stopWhen}\nIf this condition is fully met after this iteration's work, set should_fully_stop=true in your output. Otherwise set it to false
|
|
2811
|
+
const stopConditionSection = params.stopWhen !== void 0 ? `\n\n## Stop Condition\n\nThe user has configured a condition to end the loop: ${params.stopWhen}\nIf this condition is fully met after this iteration's work, set should_fully_stop=true in your output. Otherwise set it to false.` : "";
|
|
2791
2812
|
return `You are working autonomously towards an objective given below.
|
|
2792
2813
|
This is iteration ${params.n}. Each iteration aims to make an incremental step forward, not to complete the entire objective.
|
|
2793
2814
|
|
|
@@ -2833,6 +2854,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2833
2854
|
successCount: 0,
|
|
2834
2855
|
failCount: 0,
|
|
2835
2856
|
consecutiveFailures: 0,
|
|
2857
|
+
consecutiveErrors: 0,
|
|
2836
2858
|
startTime: /* @__PURE__ */ new Date(),
|
|
2837
2859
|
waitingUntil: null,
|
|
2838
2860
|
lastMessage: null
|
|
@@ -2977,14 +2999,14 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2977
2999
|
this.abort(`${this.config.maxConsecutiveFailures} consecutive failures`);
|
|
2978
3000
|
break;
|
|
2979
3001
|
}
|
|
2980
|
-
if (this.state.
|
|
2981
|
-
const backoffMs = 6e4 * Math.pow(2, this.state.
|
|
3002
|
+
if (this.state.consecutiveErrors > 0 && !this.stopRequested) {
|
|
3003
|
+
const backoffMs = 6e4 * Math.pow(2, this.state.consecutiveErrors - 1);
|
|
2982
3004
|
this.state.status = "waiting";
|
|
2983
3005
|
this.state.waitingUntil = new Date(Date.now() + backoffMs);
|
|
2984
3006
|
this.emit("state", this.getState());
|
|
2985
3007
|
appendDebugLog("backoff:start", {
|
|
2986
3008
|
iteration: this.state.currentIteration,
|
|
2987
|
-
|
|
3009
|
+
consecutiveErrors: this.state.consecutiveErrors,
|
|
2988
3010
|
backoffMs
|
|
2989
3011
|
});
|
|
2990
3012
|
await this.interruptibleSleep(backoffMs);
|
|
@@ -3072,7 +3094,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3072
3094
|
};
|
|
3073
3095
|
return {
|
|
3074
3096
|
type: "completed",
|
|
3075
|
-
record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, toStringArray(result.output.key_learnings)),
|
|
3097
|
+
record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, toStringArray(result.output.key_learnings), "reported"),
|
|
3076
3098
|
shouldFullyStop
|
|
3077
3099
|
};
|
|
3078
3100
|
} catch (err) {
|
|
@@ -3104,7 +3126,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3104
3126
|
const summary = err instanceof Error ? err.message : String(err);
|
|
3105
3127
|
return {
|
|
3106
3128
|
type: "completed",
|
|
3107
|
-
record: this.recordFailure(`[ERROR] ${summary}`, summary, []),
|
|
3129
|
+
record: this.recordFailure(`[ERROR] ${summary}`, summary, [], "error"),
|
|
3108
3130
|
shouldFullyStop: false
|
|
3109
3131
|
};
|
|
3110
3132
|
} finally {
|
|
@@ -3118,6 +3140,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3118
3140
|
this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
|
|
3119
3141
|
this.state.successCount++;
|
|
3120
3142
|
this.state.consecutiveFailures = 0;
|
|
3143
|
+
this.state.consecutiveErrors = 0;
|
|
3121
3144
|
return {
|
|
3122
3145
|
number: this.state.currentIteration,
|
|
3123
3146
|
success: true,
|
|
@@ -3127,11 +3150,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3127
3150
|
timestamp: /* @__PURE__ */ new Date()
|
|
3128
3151
|
};
|
|
3129
3152
|
}
|
|
3130
|
-
recordFailure(notesSummary, recordSummary, learnings) {
|
|
3153
|
+
recordFailure(notesSummary, recordSummary, learnings, kind) {
|
|
3131
3154
|
appendNotes(this.runInfo.notesPath, this.state.currentIteration, notesSummary, [], toStringArray(learnings));
|
|
3132
3155
|
resetHard(this.cwd);
|
|
3133
3156
|
this.state.failCount++;
|
|
3134
3157
|
this.state.consecutiveFailures++;
|
|
3158
|
+
if (kind === "error") this.state.consecutiveErrors++;
|
|
3159
|
+
else this.state.consecutiveErrors = 0;
|
|
3135
3160
|
return {
|
|
3136
3161
|
number: this.state.currentIteration,
|
|
3137
3162
|
success: false,
|
|
@@ -3264,6 +3289,7 @@ var MockOrchestrator = class extends EventEmitter {
|
|
|
3264
3289
|
successCount: 11,
|
|
3265
3290
|
failCount: 2,
|
|
3266
3291
|
consecutiveFailures: 0,
|
|
3292
|
+
consecutiveErrors: 0,
|
|
3267
3293
|
startTime: new Date(Date.now() - INITIAL_ELAPSED_MS),
|
|
3268
3294
|
waitingUntil: null,
|
|
3269
3295
|
lastMessage: AGENT_MESSAGES[0]
|
|
@@ -3998,6 +4024,12 @@ const GNHF_REEXEC_STDIN_PROMPT = "GNHF_REEXEC_STDIN_PROMPT";
|
|
|
3998
4024
|
const GNHF_REEXEC_STDIN_PROMPT_FILE = "GNHF_REEXEC_STDIN_PROMPT_FILE";
|
|
3999
4025
|
const GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX = "gnhf-stdin-";
|
|
4000
4026
|
const GNHF_REEXEC_STDIN_PROMPT_FILENAME = "prompt.txt";
|
|
4027
|
+
var PromptSignalError = class extends Error {
|
|
4028
|
+
constructor(signal) {
|
|
4029
|
+
super(signal);
|
|
4030
|
+
this.signal = signal;
|
|
4031
|
+
}
|
|
4032
|
+
};
|
|
4001
4033
|
function parseNonNegativeInteger(value) {
|
|
4002
4034
|
if (!/^\d+$/.test(value)) throw new InvalidArgumentError("must be a non-negative integer");
|
|
4003
4035
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -4013,15 +4045,15 @@ function humanizeErrorMessage(message) {
|
|
|
4013
4045
|
if (message.includes("not a git repository")) return "This command must be run inside a Git repository. Change into a repo or run \"git init\" first.";
|
|
4014
4046
|
return message;
|
|
4015
4047
|
}
|
|
4016
|
-
function initializeNewBranch(prompt, cwd) {
|
|
4048
|
+
function initializeNewBranch(prompt, cwd, schemaOptions) {
|
|
4017
4049
|
ensureCleanWorkingTree(cwd);
|
|
4018
4050
|
const baseCommit = getHeadCommit(cwd);
|
|
4019
4051
|
const branchName = slugifyPrompt(prompt);
|
|
4020
4052
|
createBranch(branchName, cwd);
|
|
4021
4053
|
const runId = branchName.split("/")[1];
|
|
4022
|
-
return setupRun(runId, prompt, baseCommit, cwd);
|
|
4054
|
+
return setupRun(runId, prompt, baseCommit, cwd, schemaOptions);
|
|
4023
4055
|
}
|
|
4024
|
-
function initializeWorktreeRun(prompt, cwd) {
|
|
4056
|
+
function initializeWorktreeRun(prompt, cwd, schemaOptions) {
|
|
4025
4057
|
const repoRoot = getRepoRootDir(cwd);
|
|
4026
4058
|
const baseCommit = getHeadCommit(cwd);
|
|
4027
4059
|
const branchName = slugifyPrompt(prompt);
|
|
@@ -4029,19 +4061,80 @@ function initializeWorktreeRun(prompt, cwd) {
|
|
|
4029
4061
|
const worktreePath = join(dirname(repoRoot), `${basename(repoRoot)}-gnhf-worktrees`, runId);
|
|
4030
4062
|
createWorktree(repoRoot, worktreePath, branchName);
|
|
4031
4063
|
return {
|
|
4032
|
-
runInfo: setupRun(runId, prompt, baseCommit, worktreePath),
|
|
4064
|
+
runInfo: setupRun(runId, prompt, baseCommit, worktreePath, schemaOptions),
|
|
4033
4065
|
worktreePath,
|
|
4034
4066
|
effectiveCwd: worktreePath
|
|
4035
4067
|
};
|
|
4036
4068
|
}
|
|
4037
|
-
function
|
|
4038
|
-
|
|
4069
|
+
function openPromptTerminal() {
|
|
4070
|
+
if (process$1.stdin.isTTY) return {
|
|
4039
4071
|
input: process$1.stdin,
|
|
4040
|
-
output: process$1.stderr
|
|
4072
|
+
output: process$1.stderr,
|
|
4073
|
+
cleanup: () => {}
|
|
4074
|
+
};
|
|
4075
|
+
const inputPath = process$1.platform === "win32" ? "CONIN$" : "/dev/tty";
|
|
4076
|
+
const outputPath = process$1.platform === "win32" ? "CONOUT$" : "/dev/tty";
|
|
4077
|
+
const inputFd = openSync(inputPath, "r");
|
|
4078
|
+
try {
|
|
4079
|
+
const outputFd = openSync(outputPath, "w");
|
|
4080
|
+
try {
|
|
4081
|
+
const input = createReadStream("", {
|
|
4082
|
+
autoClose: true,
|
|
4083
|
+
fd: inputFd
|
|
4084
|
+
});
|
|
4085
|
+
const output = createWriteStream("", {
|
|
4086
|
+
autoClose: true,
|
|
4087
|
+
fd: outputFd
|
|
4088
|
+
});
|
|
4089
|
+
return {
|
|
4090
|
+
input,
|
|
4091
|
+
output,
|
|
4092
|
+
cleanup: () => {
|
|
4093
|
+
input.destroy();
|
|
4094
|
+
output.destroy();
|
|
4095
|
+
}
|
|
4096
|
+
};
|
|
4097
|
+
} catch (error) {
|
|
4098
|
+
closeSync(outputFd);
|
|
4099
|
+
throw error;
|
|
4100
|
+
}
|
|
4101
|
+
} catch (error) {
|
|
4102
|
+
closeSync(inputFd);
|
|
4103
|
+
throw error;
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
function ask(question, closeMessage, unavailableMessage) {
|
|
4107
|
+
let terminal;
|
|
4108
|
+
try {
|
|
4109
|
+
terminal = openPromptTerminal();
|
|
4110
|
+
} catch {
|
|
4111
|
+
throw new Error(unavailableMessage);
|
|
4112
|
+
}
|
|
4113
|
+
const rl = createInterface({
|
|
4114
|
+
input: terminal.input,
|
|
4115
|
+
output: terminal.output
|
|
4041
4116
|
});
|
|
4042
|
-
return new Promise((resolve) => {
|
|
4117
|
+
return new Promise((resolve, reject) => {
|
|
4118
|
+
const handleClose = () => {
|
|
4119
|
+
terminal.cleanup();
|
|
4120
|
+
rl.off("close", handleClose);
|
|
4121
|
+
rl.off("SIGINT", handleSigInt);
|
|
4122
|
+
reject(new Error(closeMessage));
|
|
4123
|
+
};
|
|
4124
|
+
const handleSigInt = () => {
|
|
4125
|
+
rl.off("close", handleClose);
|
|
4126
|
+
rl.off("SIGINT", handleSigInt);
|
|
4127
|
+
rl.close();
|
|
4128
|
+
terminal.cleanup();
|
|
4129
|
+
reject(new PromptSignalError("SIGINT"));
|
|
4130
|
+
};
|
|
4131
|
+
rl.once("close", handleClose);
|
|
4132
|
+
rl.once("SIGINT", handleSigInt);
|
|
4043
4133
|
rl.question(question, (answer) => {
|
|
4134
|
+
rl.off("close", handleClose);
|
|
4135
|
+
rl.off("SIGINT", handleSigInt);
|
|
4044
4136
|
rl.close();
|
|
4137
|
+
terminal.cleanup();
|
|
4045
4138
|
resolve(answer.trim().toLowerCase());
|
|
4046
4139
|
});
|
|
4047
4140
|
});
|
|
@@ -4135,6 +4228,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4135
4228
|
let worktreeCleanup = null;
|
|
4136
4229
|
const currentBranch = getCurrentBranch(cwd);
|
|
4137
4230
|
const onGnhfBranch = currentBranch.startsWith("gnhf/");
|
|
4231
|
+
const schemaOptions = { includeStopField: options.stopWhen !== void 0 };
|
|
4138
4232
|
let runInfo;
|
|
4139
4233
|
let startIteration = 0;
|
|
4140
4234
|
if (options.worktree) {
|
|
@@ -4146,7 +4240,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4146
4240
|
console.error("Cannot use --worktree from a gnhf branch. Switch to the base branch first.");
|
|
4147
4241
|
process$1.exit(1);
|
|
4148
4242
|
}
|
|
4149
|
-
const wt = initializeWorktreeRun(prompt, cwd);
|
|
4243
|
+
const wt = initializeWorktreeRun(prompt, cwd, schemaOptions);
|
|
4150
4244
|
runInfo = wt.runInfo;
|
|
4151
4245
|
effectiveCwd = wt.effectiveCwd;
|
|
4152
4246
|
worktreePath = wt.worktreePath;
|
|
@@ -4161,18 +4255,19 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4161
4255
|
});
|
|
4162
4256
|
} else if (onGnhfBranch) {
|
|
4163
4257
|
const existingRunId = currentBranch.slice(5);
|
|
4164
|
-
const existing = resumeRun(existingRunId, cwd);
|
|
4258
|
+
const existing = resumeRun(existingRunId, cwd, schemaOptions);
|
|
4165
4259
|
const existingPrompt = readFileSync(existing.promptPath, "utf-8");
|
|
4166
4260
|
if (!prompt || prompt === existingPrompt) {
|
|
4167
4261
|
prompt = existingPrompt;
|
|
4168
4262
|
runInfo = existing;
|
|
4169
4263
|
startIteration = getLastIterationNumber(existing);
|
|
4170
4264
|
} else {
|
|
4171
|
-
const answer = await ask(`You are on gnhf branch "${currentBranch}".\n (o)
|
|
4265
|
+
const answer = await ask(`You are on gnhf branch "${currentBranch}".\n (o) Update prompt and continue current run\n (n) Start a new branch on top of this one\n (q) Quit\nChoose [o/n/q]: `, "The overwrite prompt closed before a choice was entered. Re-run gnhf from an interactive terminal and choose o, n, or q.", "Cannot show the overwrite prompt because stdin is not interactive. Re-run gnhf from an interactive terminal and choose o, n, or q.");
|
|
4172
4266
|
if (answer === "o") {
|
|
4173
4267
|
ensureCleanWorkingTree(cwd);
|
|
4174
|
-
runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd);
|
|
4175
|
-
|
|
4268
|
+
runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd, schemaOptions);
|
|
4269
|
+
startIteration = getLastIterationNumber(existing);
|
|
4270
|
+
} else if (answer === "n") runInfo = initializeNewBranch(prompt, cwd, schemaOptions);
|
|
4176
4271
|
else process$1.exit(0);
|
|
4177
4272
|
}
|
|
4178
4273
|
} else {
|
|
@@ -4180,7 +4275,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4180
4275
|
program.help();
|
|
4181
4276
|
return;
|
|
4182
4277
|
}
|
|
4183
|
-
runInfo = initializeNewBranch(prompt, cwd);
|
|
4278
|
+
runInfo = initializeNewBranch(prompt, cwd, schemaOptions);
|
|
4184
4279
|
}
|
|
4185
4280
|
let sleepPreventionCleanup = null;
|
|
4186
4281
|
if (config.preventSleep) {
|
|
@@ -4217,7 +4312,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4217
4312
|
nodeVersion: process$1.version,
|
|
4218
4313
|
gnhfVersion: packageVersion
|
|
4219
4314
|
});
|
|
4220
|
-
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent], config.agentArgsOverride?.[config.agent]), runInfo, prompt, effectiveCwd, startIteration, {
|
|
4315
|
+
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent], config.agentArgsOverride?.[config.agent], schemaOptions), runInfo, prompt, effectiveCwd, startIteration, {
|
|
4221
4316
|
maxIterations: options.maxIterations,
|
|
4222
4317
|
maxTokens: options.maxTokens,
|
|
4223
4318
|
stopWhen: options.stopWhen
|
|
@@ -4301,6 +4396,7 @@ function die(message) {
|
|
|
4301
4396
|
try {
|
|
4302
4397
|
await program.parseAsync();
|
|
4303
4398
|
} catch (err) {
|
|
4399
|
+
if (err instanceof PromptSignalError) process$1.exit(getSignalExitCode(err.signal));
|
|
4304
4400
|
die(err instanceof Error ? err.message : String(err));
|
|
4305
4401
|
}
|
|
4306
4402
|
//#endregion
|