gnhf 0.1.23 → 0.1.24
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 +14 -10
- package/dist/cli.mjs +133 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ npm link
|
|
|
139
139
|
- **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
140
|
- **Shared memory** — the agent reads `notes.md` (built up from prior iterations) to communicate across iterations
|
|
141
141
|
- **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
|
|
142
|
+
- **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 overwrite the saved prompt, start a new branch, or quit
|
|
143
143
|
|
|
144
144
|
### Worktree Mode
|
|
145
145
|
|
|
@@ -165,17 +165,19 @@ Pass `--worktree` to run each agent in an isolated [git worktree](https://git-sc
|
|
|
165
165
|
| `echo "<prompt>" \| gnhf` | Pipe prompt via stdin |
|
|
166
166
|
| `cat prd.md \| gnhf` | Pipe a large spec or PRD via stdin |
|
|
167
167
|
|
|
168
|
+
If you run `gnhf` on an existing `gnhf/` branch with a different prompt, gnhf asks whether to overwrite the saved prompt, 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.
|
|
169
|
+
|
|
168
170
|
### Flags
|
|
169
171
|
|
|
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
|
|
172
|
+
| Flag | Description | Default |
|
|
173
|
+
| ------------------------ | -------------------------------------------------------------------------- | ---------------------- |
|
|
174
|
+
| `--agent <agent>` | Agent to use (`claude`, `codex`, `rovodev`, or `opencode`) | config file (`claude`) |
|
|
175
|
+
| `--max-iterations <n>` | Abort after `n` total iterations | unlimited |
|
|
176
|
+
| `--max-tokens <n>` | Abort after `n` total input+output tokens | unlimited |
|
|
177
|
+
| `--stop-when <cond>` | End the loop when the agent reports this natural-language condition is met | unlimited |
|
|
178
|
+
| `--prevent-sleep <mode>` | Prevent system sleep during the run (`on`/`off` or `true`/`false`) | config file (`on`) |
|
|
179
|
+
| `--worktree` | Run in a separate git worktree (enables multiple agents concurrently) | `false` |
|
|
180
|
+
| `--version` | Show version | |
|
|
179
181
|
|
|
180
182
|
## Configuration
|
|
181
183
|
|
|
@@ -249,6 +251,8 @@ Including a snippet of `gnhf.log` is the single most useful thing you can attach
|
|
|
249
251
|
|
|
250
252
|
## Development
|
|
251
253
|
|
|
254
|
+
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.
|
|
255
|
+
|
|
252
256
|
```sh
|
|
253
257
|
npm run build # Build with tsdown
|
|
254
258
|
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,7 +481,7 @@ 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 });
|
|
@@ -483,7 +490,7 @@ function setupRun(runId, prompt, baseCommit, cwd) {
|
|
|
483
490
|
const notesPath = join(runDir, "notes.md");
|
|
484
491
|
writeFileSync(notesPath, `# gnhf run: ${runId}\n\nObjective: ${prompt}\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: [
|
|
@@ -1339,25 +1348,27 @@ const BLANKET_PERMISSION_RULESET = [{
|
|
|
1339
1348
|
pattern: "*",
|
|
1340
1349
|
action: "allow"
|
|
1341
1350
|
}];
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1351
|
+
function buildStructuredOutputFormat(schema) {
|
|
1352
|
+
return {
|
|
1353
|
+
type: "json_schema",
|
|
1354
|
+
schema,
|
|
1355
|
+
retryCount: 1
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1347
1358
|
function buildOpencodeChildEnv() {
|
|
1348
1359
|
const env = { ...process.env };
|
|
1349
1360
|
delete env.OPENCODE_SERVER_USERNAME;
|
|
1350
1361
|
delete env.OPENCODE_SERVER_PASSWORD;
|
|
1351
1362
|
return env;
|
|
1352
1363
|
}
|
|
1353
|
-
function buildPrompt(prompt) {
|
|
1364
|
+
function buildPrompt(prompt, schema) {
|
|
1354
1365
|
return [
|
|
1355
1366
|
prompt,
|
|
1356
1367
|
"",
|
|
1357
1368
|
"When you finish, reply with only valid JSON.",
|
|
1358
1369
|
"Do not wrap the JSON in markdown fences.",
|
|
1359
1370
|
"Do not include any prose before or after the JSON.",
|
|
1360
|
-
`The JSON must match this schema exactly: ${JSON.stringify(
|
|
1371
|
+
`The JSON must match this schema exactly: ${JSON.stringify(schema)}`
|
|
1361
1372
|
].join("\n");
|
|
1362
1373
|
}
|
|
1363
1374
|
/**
|
|
@@ -1452,6 +1463,7 @@ var OpenCodeAgent = class {
|
|
|
1452
1463
|
getPortFn;
|
|
1453
1464
|
killProcessFn;
|
|
1454
1465
|
platform;
|
|
1466
|
+
schema;
|
|
1455
1467
|
spawnFn;
|
|
1456
1468
|
server = null;
|
|
1457
1469
|
closingPromise = null;
|
|
@@ -1462,6 +1474,7 @@ var OpenCodeAgent = class {
|
|
|
1462
1474
|
this.getPortFn = deps.getPort ?? getAvailablePort$1;
|
|
1463
1475
|
this.killProcessFn = deps.killProcess ?? process.kill.bind(process);
|
|
1464
1476
|
this.platform = deps.platform ?? process.platform;
|
|
1477
|
+
this.schema = deps.schema ?? buildAgentOutputSchema({ includeStopField: false });
|
|
1465
1478
|
this.spawnFn = deps.spawn ?? spawn;
|
|
1466
1479
|
}
|
|
1467
1480
|
async run(prompt, cwd, options) {
|
|
@@ -1487,7 +1500,7 @@ var OpenCodeAgent = class {
|
|
|
1487
1500
|
try {
|
|
1488
1501
|
const server = await this.ensureServer(cwd, runController.signal);
|
|
1489
1502
|
sessionId = await this.createSession(server, cwd, runController.signal);
|
|
1490
|
-
const result = await this.streamMessage(server, sessionId, buildPrompt(prompt), runController.signal, logStream, onUsage, onMessage);
|
|
1503
|
+
const result = await this.streamMessage(server, sessionId, buildPrompt(prompt, this.schema), runController.signal, logStream, onUsage, onMessage);
|
|
1491
1504
|
appendDebugLog("opencode:run:end", {
|
|
1492
1505
|
sessionId,
|
|
1493
1506
|
elapsedMs: Date.now() - runStartedAt,
|
|
@@ -1764,7 +1777,7 @@ var OpenCodeAgent = class {
|
|
|
1764
1777
|
type: "text",
|
|
1765
1778
|
text: prompt
|
|
1766
1779
|
}],
|
|
1767
|
-
format:
|
|
1780
|
+
format: buildStructuredOutputFormat(this.schema)
|
|
1768
1781
|
},
|
|
1769
1782
|
signal
|
|
1770
1783
|
});
|
|
@@ -2757,11 +2770,13 @@ function withTimeoutSignal(signal, timeoutMs) {
|
|
|
2757
2770
|
}
|
|
2758
2771
|
//#endregion
|
|
2759
2772
|
//#region src/core/agents/factory.ts
|
|
2760
|
-
function createAgent(name, runInfo, pathOverride, agentArgsOverride) {
|
|
2773
|
+
function createAgent(name, runInfo, pathOverride, agentArgsOverride, options) {
|
|
2774
|
+
const schema = buildAgentOutputSchema({ includeStopField: options.includeStopField });
|
|
2761
2775
|
switch (name) {
|
|
2762
2776
|
case "claude": return new ClaudeAgent({
|
|
2763
2777
|
bin: pathOverride,
|
|
2764
|
-
extraArgs: agentArgsOverride
|
|
2778
|
+
extraArgs: agentArgsOverride,
|
|
2779
|
+
schema
|
|
2765
2780
|
});
|
|
2766
2781
|
case "codex": return new CodexAgent(runInfo.schemaPath, {
|
|
2767
2782
|
bin: pathOverride,
|
|
@@ -2769,7 +2784,8 @@ function createAgent(name, runInfo, pathOverride, agentArgsOverride) {
|
|
|
2769
2784
|
});
|
|
2770
2785
|
case "opencode": return new OpenCodeAgent({
|
|
2771
2786
|
bin: pathOverride,
|
|
2772
|
-
extraArgs: agentArgsOverride
|
|
2787
|
+
extraArgs: agentArgsOverride,
|
|
2788
|
+
schema
|
|
2773
2789
|
});
|
|
2774
2790
|
case "rovodev": return new RovoDevAgent(runInfo.schemaPath, {
|
|
2775
2791
|
bin: pathOverride,
|
|
@@ -2787,7 +2803,7 @@ function buildIterationPrompt(params) {
|
|
|
2787
2803
|
"- key_learnings: an array of new learnings that were surprising, weren't captured by previous notes and would be informative for future iterations"
|
|
2788
2804
|
];
|
|
2789
2805
|
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
|
|
2806
|
+
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
2807
|
return `You are working autonomously towards an objective given below.
|
|
2792
2808
|
This is iteration ${params.n}. Each iteration aims to make an incremental step forward, not to complete the entire objective.
|
|
2793
2809
|
|
|
@@ -3998,6 +4014,12 @@ const GNHF_REEXEC_STDIN_PROMPT = "GNHF_REEXEC_STDIN_PROMPT";
|
|
|
3998
4014
|
const GNHF_REEXEC_STDIN_PROMPT_FILE = "GNHF_REEXEC_STDIN_PROMPT_FILE";
|
|
3999
4015
|
const GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX = "gnhf-stdin-";
|
|
4000
4016
|
const GNHF_REEXEC_STDIN_PROMPT_FILENAME = "prompt.txt";
|
|
4017
|
+
var PromptSignalError = class extends Error {
|
|
4018
|
+
constructor(signal) {
|
|
4019
|
+
super(signal);
|
|
4020
|
+
this.signal = signal;
|
|
4021
|
+
}
|
|
4022
|
+
};
|
|
4001
4023
|
function parseNonNegativeInteger(value) {
|
|
4002
4024
|
if (!/^\d+$/.test(value)) throw new InvalidArgumentError("must be a non-negative integer");
|
|
4003
4025
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -4013,15 +4035,15 @@ function humanizeErrorMessage(message) {
|
|
|
4013
4035
|
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
4036
|
return message;
|
|
4015
4037
|
}
|
|
4016
|
-
function initializeNewBranch(prompt, cwd) {
|
|
4038
|
+
function initializeNewBranch(prompt, cwd, schemaOptions) {
|
|
4017
4039
|
ensureCleanWorkingTree(cwd);
|
|
4018
4040
|
const baseCommit = getHeadCommit(cwd);
|
|
4019
4041
|
const branchName = slugifyPrompt(prompt);
|
|
4020
4042
|
createBranch(branchName, cwd);
|
|
4021
4043
|
const runId = branchName.split("/")[1];
|
|
4022
|
-
return setupRun(runId, prompt, baseCommit, cwd);
|
|
4044
|
+
return setupRun(runId, prompt, baseCommit, cwd, schemaOptions);
|
|
4023
4045
|
}
|
|
4024
|
-
function initializeWorktreeRun(prompt, cwd) {
|
|
4046
|
+
function initializeWorktreeRun(prompt, cwd, schemaOptions) {
|
|
4025
4047
|
const repoRoot = getRepoRootDir(cwd);
|
|
4026
4048
|
const baseCommit = getHeadCommit(cwd);
|
|
4027
4049
|
const branchName = slugifyPrompt(prompt);
|
|
@@ -4029,19 +4051,80 @@ function initializeWorktreeRun(prompt, cwd) {
|
|
|
4029
4051
|
const worktreePath = join(dirname(repoRoot), `${basename(repoRoot)}-gnhf-worktrees`, runId);
|
|
4030
4052
|
createWorktree(repoRoot, worktreePath, branchName);
|
|
4031
4053
|
return {
|
|
4032
|
-
runInfo: setupRun(runId, prompt, baseCommit, worktreePath),
|
|
4054
|
+
runInfo: setupRun(runId, prompt, baseCommit, worktreePath, schemaOptions),
|
|
4033
4055
|
worktreePath,
|
|
4034
4056
|
effectiveCwd: worktreePath
|
|
4035
4057
|
};
|
|
4036
4058
|
}
|
|
4037
|
-
function
|
|
4038
|
-
|
|
4059
|
+
function openPromptTerminal() {
|
|
4060
|
+
if (process$1.stdin.isTTY) return {
|
|
4039
4061
|
input: process$1.stdin,
|
|
4040
|
-
output: process$1.stderr
|
|
4062
|
+
output: process$1.stderr,
|
|
4063
|
+
cleanup: () => {}
|
|
4064
|
+
};
|
|
4065
|
+
const inputPath = process$1.platform === "win32" ? "CONIN$" : "/dev/tty";
|
|
4066
|
+
const outputPath = process$1.platform === "win32" ? "CONOUT$" : "/dev/tty";
|
|
4067
|
+
const inputFd = openSync(inputPath, "r");
|
|
4068
|
+
try {
|
|
4069
|
+
const outputFd = openSync(outputPath, "w");
|
|
4070
|
+
try {
|
|
4071
|
+
const input = createReadStream("", {
|
|
4072
|
+
autoClose: true,
|
|
4073
|
+
fd: inputFd
|
|
4074
|
+
});
|
|
4075
|
+
const output = createWriteStream("", {
|
|
4076
|
+
autoClose: true,
|
|
4077
|
+
fd: outputFd
|
|
4078
|
+
});
|
|
4079
|
+
return {
|
|
4080
|
+
input,
|
|
4081
|
+
output,
|
|
4082
|
+
cleanup: () => {
|
|
4083
|
+
input.destroy();
|
|
4084
|
+
output.destroy();
|
|
4085
|
+
}
|
|
4086
|
+
};
|
|
4087
|
+
} catch (error) {
|
|
4088
|
+
closeSync(outputFd);
|
|
4089
|
+
throw error;
|
|
4090
|
+
}
|
|
4091
|
+
} catch (error) {
|
|
4092
|
+
closeSync(inputFd);
|
|
4093
|
+
throw error;
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
function ask(question, closeMessage, unavailableMessage) {
|
|
4097
|
+
let terminal;
|
|
4098
|
+
try {
|
|
4099
|
+
terminal = openPromptTerminal();
|
|
4100
|
+
} catch {
|
|
4101
|
+
throw new Error(unavailableMessage);
|
|
4102
|
+
}
|
|
4103
|
+
const rl = createInterface({
|
|
4104
|
+
input: terminal.input,
|
|
4105
|
+
output: terminal.output
|
|
4041
4106
|
});
|
|
4042
|
-
return new Promise((resolve) => {
|
|
4107
|
+
return new Promise((resolve, reject) => {
|
|
4108
|
+
const handleClose = () => {
|
|
4109
|
+
terminal.cleanup();
|
|
4110
|
+
rl.off("close", handleClose);
|
|
4111
|
+
rl.off("SIGINT", handleSigInt);
|
|
4112
|
+
reject(new Error(closeMessage));
|
|
4113
|
+
};
|
|
4114
|
+
const handleSigInt = () => {
|
|
4115
|
+
rl.off("close", handleClose);
|
|
4116
|
+
rl.off("SIGINT", handleSigInt);
|
|
4117
|
+
rl.close();
|
|
4118
|
+
terminal.cleanup();
|
|
4119
|
+
reject(new PromptSignalError("SIGINT"));
|
|
4120
|
+
};
|
|
4121
|
+
rl.once("close", handleClose);
|
|
4122
|
+
rl.once("SIGINT", handleSigInt);
|
|
4043
4123
|
rl.question(question, (answer) => {
|
|
4124
|
+
rl.off("close", handleClose);
|
|
4125
|
+
rl.off("SIGINT", handleSigInt);
|
|
4044
4126
|
rl.close();
|
|
4127
|
+
terminal.cleanup();
|
|
4045
4128
|
resolve(answer.trim().toLowerCase());
|
|
4046
4129
|
});
|
|
4047
4130
|
});
|
|
@@ -4135,6 +4218,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4135
4218
|
let worktreeCleanup = null;
|
|
4136
4219
|
const currentBranch = getCurrentBranch(cwd);
|
|
4137
4220
|
const onGnhfBranch = currentBranch.startsWith("gnhf/");
|
|
4221
|
+
const schemaOptions = { includeStopField: options.stopWhen !== void 0 };
|
|
4138
4222
|
let runInfo;
|
|
4139
4223
|
let startIteration = 0;
|
|
4140
4224
|
if (options.worktree) {
|
|
@@ -4146,7 +4230,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4146
4230
|
console.error("Cannot use --worktree from a gnhf branch. Switch to the base branch first.");
|
|
4147
4231
|
process$1.exit(1);
|
|
4148
4232
|
}
|
|
4149
|
-
const wt = initializeWorktreeRun(prompt, cwd);
|
|
4233
|
+
const wt = initializeWorktreeRun(prompt, cwd, schemaOptions);
|
|
4150
4234
|
runInfo = wt.runInfo;
|
|
4151
4235
|
effectiveCwd = wt.effectiveCwd;
|
|
4152
4236
|
worktreePath = wt.worktreePath;
|
|
@@ -4161,18 +4245,18 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4161
4245
|
});
|
|
4162
4246
|
} else if (onGnhfBranch) {
|
|
4163
4247
|
const existingRunId = currentBranch.slice(5);
|
|
4164
|
-
const existing = resumeRun(existingRunId, cwd);
|
|
4248
|
+
const existing = resumeRun(existingRunId, cwd, schemaOptions);
|
|
4165
4249
|
const existingPrompt = readFileSync(existing.promptPath, "utf-8");
|
|
4166
4250
|
if (!prompt || prompt === existingPrompt) {
|
|
4167
4251
|
prompt = existingPrompt;
|
|
4168
4252
|
runInfo = existing;
|
|
4169
4253
|
startIteration = getLastIterationNumber(existing);
|
|
4170
4254
|
} else {
|
|
4171
|
-
const answer = await ask(`You are on gnhf branch "${currentBranch}".\n (o) Overwrite current run with new prompt\n (n) Start a new branch on top of this one\n (q) Quit\nChoose [o/n/q]:
|
|
4255
|
+
const answer = await ask(`You are on gnhf branch "${currentBranch}".\n (o) Overwrite current run with new prompt\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
4256
|
if (answer === "o") {
|
|
4173
4257
|
ensureCleanWorkingTree(cwd);
|
|
4174
|
-
runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd);
|
|
4175
|
-
} else if (answer === "n") runInfo = initializeNewBranch(prompt, cwd);
|
|
4258
|
+
runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd, schemaOptions);
|
|
4259
|
+
} else if (answer === "n") runInfo = initializeNewBranch(prompt, cwd, schemaOptions);
|
|
4176
4260
|
else process$1.exit(0);
|
|
4177
4261
|
}
|
|
4178
4262
|
} else {
|
|
@@ -4180,7 +4264,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4180
4264
|
program.help();
|
|
4181
4265
|
return;
|
|
4182
4266
|
}
|
|
4183
|
-
runInfo = initializeNewBranch(prompt, cwd);
|
|
4267
|
+
runInfo = initializeNewBranch(prompt, cwd, schemaOptions);
|
|
4184
4268
|
}
|
|
4185
4269
|
let sleepPreventionCleanup = null;
|
|
4186
4270
|
if (config.preventSleep) {
|
|
@@ -4217,7 +4301,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4217
4301
|
nodeVersion: process$1.version,
|
|
4218
4302
|
gnhfVersion: packageVersion
|
|
4219
4303
|
});
|
|
4220
|
-
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent], config.agentArgsOverride?.[config.agent]), runInfo, prompt, effectiveCwd, startIteration, {
|
|
4304
|
+
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent], config.agentArgsOverride?.[config.agent], schemaOptions), runInfo, prompt, effectiveCwd, startIteration, {
|
|
4221
4305
|
maxIterations: options.maxIterations,
|
|
4222
4306
|
maxTokens: options.maxTokens,
|
|
4223
4307
|
stopWhen: options.stopWhen
|
|
@@ -4301,6 +4385,7 @@ function die(message) {
|
|
|
4301
4385
|
try {
|
|
4302
4386
|
await program.parseAsync();
|
|
4303
4387
|
} catch (err) {
|
|
4388
|
+
if (err instanceof PromptSignalError) process$1.exit(getSignalExitCode(err.signal));
|
|
4304
4389
|
die(err instanceof Error ? err.message : String(err));
|
|
4305
4390
|
}
|
|
4306
4391
|
//#endregion
|