gnhf 0.1.22 → 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 +15 -10
- package/dist/cli.mjs +157 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -136,10 +136,10 @@ 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
|
-
- **Runtime caps**
|
|
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,16 +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
|
-
| `--
|
|
176
|
-
| `--
|
|
177
|
-
| `--
|
|
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 | |
|
|
178
181
|
|
|
179
182
|
## Configuration
|
|
180
183
|
|
|
@@ -248,6 +251,8 @@ Including a snippet of `gnhf.log` is the single most useful thing you can attach
|
|
|
248
251
|
|
|
249
252
|
## Development
|
|
250
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
|
+
|
|
251
256
|
```sh
|
|
252
257
|
npm run build # Build with tsdown
|
|
253
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: {
|
|
@@ -441,19 +439,29 @@ const AGENT_OUTPUT_SCHEMA = {
|
|
|
441
439
|
type: "array",
|
|
442
440
|
items: { type: "string" }
|
|
443
441
|
}
|
|
444
|
-
}
|
|
445
|
-
required
|
|
442
|
+
};
|
|
443
|
+
const required = [
|
|
446
444
|
"success",
|
|
447
445
|
"summary",
|
|
448
446
|
"key_changes_made",
|
|
449
447
|
"key_learnings"
|
|
450
|
-
]
|
|
451
|
-
|
|
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
|
+
}
|
|
452
460
|
//#endregion
|
|
453
461
|
//#region src/core/run.ts
|
|
454
462
|
const LOG_FILENAME = "gnhf.log";
|
|
455
|
-
function writeSchemaFile(schemaPath) {
|
|
456
|
-
writeFileSync(schemaPath, JSON.stringify(
|
|
463
|
+
function writeSchemaFile(schemaPath, includeStopField) {
|
|
464
|
+
writeFileSync(schemaPath, JSON.stringify(buildAgentOutputSchema({ includeStopField }), null, 2), "utf-8");
|
|
457
465
|
}
|
|
458
466
|
function ensureRunMetadataIgnored(cwd) {
|
|
459
467
|
const excludePath = execFileSync("git", [
|
|
@@ -473,7 +481,7 @@ function ensureRunMetadataIgnored(cwd) {
|
|
|
473
481
|
appendFileSync(resolved, `${content.length > 0 && !content.endsWith("\n") ? "\n" : ""}${entry}\n`, "utf-8");
|
|
474
482
|
} else writeFileSync(resolved, `${entry}\n`, "utf-8");
|
|
475
483
|
}
|
|
476
|
-
function setupRun(runId, prompt, baseCommit, cwd) {
|
|
484
|
+
function setupRun(runId, prompt, baseCommit, cwd, schemaOptions) {
|
|
477
485
|
ensureRunMetadataIgnored(cwd);
|
|
478
486
|
const runDir = join(cwd, ".gnhf", "runs", runId);
|
|
479
487
|
mkdirSync(runDir, { recursive: true });
|
|
@@ -482,7 +490,7 @@ function setupRun(runId, prompt, baseCommit, cwd) {
|
|
|
482
490
|
const notesPath = join(runDir, "notes.md");
|
|
483
491
|
writeFileSync(notesPath, `# gnhf run: ${runId}\n\nObjective: ${prompt}\n\n## Iteration Log\n`, "utf-8");
|
|
484
492
|
const schemaPath = join(runDir, "output-schema.json");
|
|
485
|
-
writeSchemaFile(schemaPath);
|
|
493
|
+
writeSchemaFile(schemaPath, schemaOptions.includeStopField);
|
|
486
494
|
const logPath = join(runDir, LOG_FILENAME);
|
|
487
495
|
const baseCommitPath = join(runDir, "base-commit");
|
|
488
496
|
const hasStoredBaseCommit = existsSync(baseCommitPath);
|
|
@@ -499,13 +507,13 @@ function setupRun(runId, prompt, baseCommit, cwd) {
|
|
|
499
507
|
baseCommitPath
|
|
500
508
|
};
|
|
501
509
|
}
|
|
502
|
-
function resumeRun(runId, cwd) {
|
|
510
|
+
function resumeRun(runId, cwd, schemaOptions) {
|
|
503
511
|
const runDir = join(cwd, ".gnhf", "runs", runId);
|
|
504
512
|
if (!existsSync(runDir)) throw new Error(`Run directory not found: ${runDir}`);
|
|
505
513
|
const promptPath = join(runDir, "prompt.md");
|
|
506
514
|
const notesPath = join(runDir, "notes.md");
|
|
507
515
|
const schemaPath = join(runDir, "output-schema.json");
|
|
508
|
-
writeSchemaFile(schemaPath);
|
|
516
|
+
writeSchemaFile(schemaPath, schemaOptions.includeStopField);
|
|
509
517
|
const logPath = join(runDir, LOG_FILENAME);
|
|
510
518
|
const baseCommitPath = join(runDir, "base-commit");
|
|
511
519
|
return {
|
|
@@ -1067,7 +1075,7 @@ function terminateClaudeProcess(child, platform) {
|
|
|
1067
1075
|
}
|
|
1068
1076
|
child.kill("SIGTERM");
|
|
1069
1077
|
}
|
|
1070
|
-
function buildClaudeArgs(prompt, extraArgs) {
|
|
1078
|
+
function buildClaudeArgs(prompt, schema, extraArgs) {
|
|
1071
1079
|
const userArgs = extraArgs ?? [];
|
|
1072
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="));
|
|
1073
1081
|
return [
|
|
@@ -1078,7 +1086,7 @@ function buildClaudeArgs(prompt, extraArgs) {
|
|
|
1078
1086
|
"--output-format",
|
|
1079
1087
|
"stream-json",
|
|
1080
1088
|
"--json-schema",
|
|
1081
|
-
JSON.stringify(
|
|
1089
|
+
JSON.stringify(schema),
|
|
1082
1090
|
...userSpecifiedPermissionMode ? [] : ["--dangerously-skip-permissions"]
|
|
1083
1091
|
];
|
|
1084
1092
|
}
|
|
@@ -1101,17 +1109,19 @@ var ClaudeAgent = class {
|
|
|
1101
1109
|
bin;
|
|
1102
1110
|
extraArgs;
|
|
1103
1111
|
platform;
|
|
1112
|
+
schema;
|
|
1104
1113
|
constructor(binOrDeps = {}) {
|
|
1105
1114
|
const deps = typeof binOrDeps === "string" ? { bin: binOrDeps } : binOrDeps;
|
|
1106
1115
|
this.bin = deps.bin ?? "claude";
|
|
1107
1116
|
this.extraArgs = deps.extraArgs;
|
|
1108
1117
|
this.platform = deps.platform ?? process.platform;
|
|
1118
|
+
this.schema = deps.schema ?? buildAgentOutputSchema({ includeStopField: false });
|
|
1109
1119
|
}
|
|
1110
1120
|
run(prompt, cwd, options) {
|
|
1111
1121
|
const { onUsage, onMessage, signal, logPath } = options ?? {};
|
|
1112
1122
|
return new Promise((resolve, reject) => {
|
|
1113
1123
|
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
1114
|
-
const child = spawn(this.bin, buildClaudeArgs(prompt, this.extraArgs), {
|
|
1124
|
+
const child = spawn(this.bin, buildClaudeArgs(prompt, this.schema, this.extraArgs), {
|
|
1115
1125
|
cwd,
|
|
1116
1126
|
shell: shouldUseWindowsShell$2(this.bin, this.platform),
|
|
1117
1127
|
stdio: [
|
|
@@ -1338,25 +1348,27 @@ const BLANKET_PERMISSION_RULESET = [{
|
|
|
1338
1348
|
pattern: "*",
|
|
1339
1349
|
action: "allow"
|
|
1340
1350
|
}];
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1351
|
+
function buildStructuredOutputFormat(schema) {
|
|
1352
|
+
return {
|
|
1353
|
+
type: "json_schema",
|
|
1354
|
+
schema,
|
|
1355
|
+
retryCount: 1
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1346
1358
|
function buildOpencodeChildEnv() {
|
|
1347
1359
|
const env = { ...process.env };
|
|
1348
1360
|
delete env.OPENCODE_SERVER_USERNAME;
|
|
1349
1361
|
delete env.OPENCODE_SERVER_PASSWORD;
|
|
1350
1362
|
return env;
|
|
1351
1363
|
}
|
|
1352
|
-
function buildPrompt(prompt) {
|
|
1364
|
+
function buildPrompt(prompt, schema) {
|
|
1353
1365
|
return [
|
|
1354
1366
|
prompt,
|
|
1355
1367
|
"",
|
|
1356
1368
|
"When you finish, reply with only valid JSON.",
|
|
1357
1369
|
"Do not wrap the JSON in markdown fences.",
|
|
1358
1370
|
"Do not include any prose before or after the JSON.",
|
|
1359
|
-
`The JSON must match this schema exactly: ${JSON.stringify(
|
|
1371
|
+
`The JSON must match this schema exactly: ${JSON.stringify(schema)}`
|
|
1360
1372
|
].join("\n");
|
|
1361
1373
|
}
|
|
1362
1374
|
/**
|
|
@@ -1451,6 +1463,7 @@ var OpenCodeAgent = class {
|
|
|
1451
1463
|
getPortFn;
|
|
1452
1464
|
killProcessFn;
|
|
1453
1465
|
platform;
|
|
1466
|
+
schema;
|
|
1454
1467
|
spawnFn;
|
|
1455
1468
|
server = null;
|
|
1456
1469
|
closingPromise = null;
|
|
@@ -1461,6 +1474,7 @@ var OpenCodeAgent = class {
|
|
|
1461
1474
|
this.getPortFn = deps.getPort ?? getAvailablePort$1;
|
|
1462
1475
|
this.killProcessFn = deps.killProcess ?? process.kill.bind(process);
|
|
1463
1476
|
this.platform = deps.platform ?? process.platform;
|
|
1477
|
+
this.schema = deps.schema ?? buildAgentOutputSchema({ includeStopField: false });
|
|
1464
1478
|
this.spawnFn = deps.spawn ?? spawn;
|
|
1465
1479
|
}
|
|
1466
1480
|
async run(prompt, cwd, options) {
|
|
@@ -1486,7 +1500,7 @@ var OpenCodeAgent = class {
|
|
|
1486
1500
|
try {
|
|
1487
1501
|
const server = await this.ensureServer(cwd, runController.signal);
|
|
1488
1502
|
sessionId = await this.createSession(server, cwd, runController.signal);
|
|
1489
|
-
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);
|
|
1490
1504
|
appendDebugLog("opencode:run:end", {
|
|
1491
1505
|
sessionId,
|
|
1492
1506
|
elapsedMs: Date.now() - runStartedAt,
|
|
@@ -1763,7 +1777,7 @@ var OpenCodeAgent = class {
|
|
|
1763
1777
|
type: "text",
|
|
1764
1778
|
text: prompt
|
|
1765
1779
|
}],
|
|
1766
|
-
format:
|
|
1780
|
+
format: buildStructuredOutputFormat(this.schema)
|
|
1767
1781
|
},
|
|
1768
1782
|
signal
|
|
1769
1783
|
});
|
|
@@ -2756,11 +2770,13 @@ function withTimeoutSignal(signal, timeoutMs) {
|
|
|
2756
2770
|
}
|
|
2757
2771
|
//#endregion
|
|
2758
2772
|
//#region src/core/agents/factory.ts
|
|
2759
|
-
function createAgent(name, runInfo, pathOverride, agentArgsOverride) {
|
|
2773
|
+
function createAgent(name, runInfo, pathOverride, agentArgsOverride, options) {
|
|
2774
|
+
const schema = buildAgentOutputSchema({ includeStopField: options.includeStopField });
|
|
2760
2775
|
switch (name) {
|
|
2761
2776
|
case "claude": return new ClaudeAgent({
|
|
2762
2777
|
bin: pathOverride,
|
|
2763
|
-
extraArgs: agentArgsOverride
|
|
2778
|
+
extraArgs: agentArgsOverride,
|
|
2779
|
+
schema
|
|
2764
2780
|
});
|
|
2765
2781
|
case "codex": return new CodexAgent(runInfo.schemaPath, {
|
|
2766
2782
|
bin: pathOverride,
|
|
@@ -2768,7 +2784,8 @@ function createAgent(name, runInfo, pathOverride, agentArgsOverride) {
|
|
|
2768
2784
|
});
|
|
2769
2785
|
case "opencode": return new OpenCodeAgent({
|
|
2770
2786
|
bin: pathOverride,
|
|
2771
|
-
extraArgs: agentArgsOverride
|
|
2787
|
+
extraArgs: agentArgsOverride,
|
|
2788
|
+
schema
|
|
2772
2789
|
});
|
|
2773
2790
|
case "rovodev": return new RovoDevAgent(runInfo.schemaPath, {
|
|
2774
2791
|
bin: pathOverride,
|
|
@@ -2779,6 +2796,14 @@ function createAgent(name, runInfo, pathOverride, agentArgsOverride) {
|
|
|
2779
2796
|
//#endregion
|
|
2780
2797
|
//#region src/templates/iteration-prompt.ts
|
|
2781
2798
|
function buildIterationPrompt(params) {
|
|
2799
|
+
const outputFields = [
|
|
2800
|
+
"- success: whether you were able to make a meaningful contribution that got us closer towards the objective. setting this to false means any code change you made should be discarded",
|
|
2801
|
+
"- summary: a concise one-sentence summary of the accomplishment in this iteration",
|
|
2802
|
+
"- key_changes_made: an array of descriptions for key changes you made. don't group this by file - group by logical units of work. don't describe activities - describe material outcomes",
|
|
2803
|
+
"- key_learnings: an array of new learnings that were surprising, weren't captured by previous notes and would be informative for future iterations"
|
|
2804
|
+
];
|
|
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");
|
|
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.` : "";
|
|
2782
2807
|
return `You are working autonomously towards an objective given below.
|
|
2783
2808
|
This is iteration ${params.n}. Each iteration aims to make an incremental step forward, not to complete the entire objective.
|
|
2784
2809
|
|
|
@@ -2792,10 +2817,7 @@ This is iteration ${params.n}. Each iteration aims to make an incremental step f
|
|
|
2792
2817
|
|
|
2793
2818
|
## Output
|
|
2794
2819
|
|
|
2795
|
-
|
|
2796
|
-
- summary: a concise one-sentence summary of the accomplishment in this iteration
|
|
2797
|
-
- key_changes_made: an array of descriptions for key changes you made. don't group this by file - group by logical units of work. don't describe activities - describe material outcomes
|
|
2798
|
-
- key_learnings: an array of new learnings that were surprising, weren't captured by previous notes and would be informative for future iterations
|
|
2820
|
+
${outputFields.join("\n")}${stopConditionSection}
|
|
2799
2821
|
|
|
2800
2822
|
## Objective
|
|
2801
2823
|
|
|
@@ -2910,7 +2932,8 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2910
2932
|
const iterationPrompt = buildIterationPrompt({
|
|
2911
2933
|
n: this.state.currentIteration,
|
|
2912
2934
|
runId: this.runInfo.runId,
|
|
2913
|
-
prompt: this.prompt
|
|
2935
|
+
prompt: this.prompt,
|
|
2936
|
+
stopWhen: this.limits.stopWhen
|
|
2914
2937
|
});
|
|
2915
2938
|
appendDebugLog("iteration:start", {
|
|
2916
2939
|
iteration: this.state.currentIteration,
|
|
@@ -2957,6 +2980,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2957
2980
|
totalOutputTokens: this.state.totalOutputTokens,
|
|
2958
2981
|
commitCount: this.state.commitCount
|
|
2959
2982
|
});
|
|
2983
|
+
if (this.limits.stopWhen !== void 0 && result.shouldFullyStop) {
|
|
2984
|
+
this.abort("stop condition met");
|
|
2985
|
+
break;
|
|
2986
|
+
}
|
|
2960
2987
|
const postIterationAbortReason = this.getPostIterationAbortReason();
|
|
2961
2988
|
if (postIterationAbortReason) {
|
|
2962
2989
|
this.abort(postIterationAbortReason);
|
|
@@ -3053,13 +3080,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3053
3080
|
cacheCreationTokens: result.usage.cacheCreationTokens
|
|
3054
3081
|
});
|
|
3055
3082
|
if (this.stopRequested) return { type: "stopped" };
|
|
3083
|
+
const shouldFullyStop = result.output.should_fully_stop === true;
|
|
3056
3084
|
if (result.output.success) return {
|
|
3057
3085
|
type: "completed",
|
|
3058
|
-
record: this.recordSuccess(result.output)
|
|
3086
|
+
record: this.recordSuccess(result.output),
|
|
3087
|
+
shouldFullyStop
|
|
3059
3088
|
};
|
|
3060
3089
|
return {
|
|
3061
3090
|
type: "completed",
|
|
3062
|
-
record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, toStringArray(result.output.key_learnings))
|
|
3091
|
+
record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, toStringArray(result.output.key_learnings)),
|
|
3092
|
+
shouldFullyStop
|
|
3063
3093
|
};
|
|
3064
3094
|
} catch (err) {
|
|
3065
3095
|
const elapsedMs = Date.now() - agentStartedAt;
|
|
@@ -3090,7 +3120,8 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3090
3120
|
const summary = err instanceof Error ? err.message : String(err);
|
|
3091
3121
|
return {
|
|
3092
3122
|
type: "completed",
|
|
3093
|
-
record: this.recordFailure(`[ERROR] ${summary}`, summary, [])
|
|
3123
|
+
record: this.recordFailure(`[ERROR] ${summary}`, summary, []),
|
|
3124
|
+
shouldFullyStop: false
|
|
3094
3125
|
};
|
|
3095
3126
|
} finally {
|
|
3096
3127
|
this.activeAbortController = null;
|
|
@@ -3983,6 +4014,12 @@ const GNHF_REEXEC_STDIN_PROMPT = "GNHF_REEXEC_STDIN_PROMPT";
|
|
|
3983
4014
|
const GNHF_REEXEC_STDIN_PROMPT_FILE = "GNHF_REEXEC_STDIN_PROMPT_FILE";
|
|
3984
4015
|
const GNHF_REEXEC_STDIN_PROMPT_DIR_PREFIX = "gnhf-stdin-";
|
|
3985
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
|
+
};
|
|
3986
4023
|
function parseNonNegativeInteger(value) {
|
|
3987
4024
|
if (!/^\d+$/.test(value)) throw new InvalidArgumentError("must be a non-negative integer");
|
|
3988
4025
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -3998,15 +4035,15 @@ function humanizeErrorMessage(message) {
|
|
|
3998
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.";
|
|
3999
4036
|
return message;
|
|
4000
4037
|
}
|
|
4001
|
-
function initializeNewBranch(prompt, cwd) {
|
|
4038
|
+
function initializeNewBranch(prompt, cwd, schemaOptions) {
|
|
4002
4039
|
ensureCleanWorkingTree(cwd);
|
|
4003
4040
|
const baseCommit = getHeadCommit(cwd);
|
|
4004
4041
|
const branchName = slugifyPrompt(prompt);
|
|
4005
4042
|
createBranch(branchName, cwd);
|
|
4006
4043
|
const runId = branchName.split("/")[1];
|
|
4007
|
-
return setupRun(runId, prompt, baseCommit, cwd);
|
|
4044
|
+
return setupRun(runId, prompt, baseCommit, cwd, schemaOptions);
|
|
4008
4045
|
}
|
|
4009
|
-
function initializeWorktreeRun(prompt, cwd) {
|
|
4046
|
+
function initializeWorktreeRun(prompt, cwd, schemaOptions) {
|
|
4010
4047
|
const repoRoot = getRepoRootDir(cwd);
|
|
4011
4048
|
const baseCommit = getHeadCommit(cwd);
|
|
4012
4049
|
const branchName = slugifyPrompt(prompt);
|
|
@@ -4014,19 +4051,80 @@ function initializeWorktreeRun(prompt, cwd) {
|
|
|
4014
4051
|
const worktreePath = join(dirname(repoRoot), `${basename(repoRoot)}-gnhf-worktrees`, runId);
|
|
4015
4052
|
createWorktree(repoRoot, worktreePath, branchName);
|
|
4016
4053
|
return {
|
|
4017
|
-
runInfo: setupRun(runId, prompt, baseCommit, worktreePath),
|
|
4054
|
+
runInfo: setupRun(runId, prompt, baseCommit, worktreePath, schemaOptions),
|
|
4018
4055
|
worktreePath,
|
|
4019
4056
|
effectiveCwd: worktreePath
|
|
4020
4057
|
};
|
|
4021
4058
|
}
|
|
4022
|
-
function
|
|
4023
|
-
|
|
4059
|
+
function openPromptTerminal() {
|
|
4060
|
+
if (process$1.stdin.isTTY) return {
|
|
4024
4061
|
input: process$1.stdin,
|
|
4025
|
-
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
|
|
4026
4106
|
});
|
|
4027
|
-
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);
|
|
4028
4123
|
rl.question(question, (answer) => {
|
|
4124
|
+
rl.off("close", handleClose);
|
|
4125
|
+
rl.off("SIGINT", handleSigInt);
|
|
4029
4126
|
rl.close();
|
|
4127
|
+
terminal.cleanup();
|
|
4030
4128
|
resolve(answer.trim().toLowerCase());
|
|
4031
4129
|
});
|
|
4032
4130
|
});
|
|
@@ -4081,7 +4179,7 @@ function readReexecStdinPrompt(env) {
|
|
|
4081
4179
|
}
|
|
4082
4180
|
}
|
|
4083
4181
|
const program = new Command();
|
|
4084
|
-
program.name("gnhf").description("Before I go to bed, I tell my agents: good night, have fun").version(packageVersion).argument("[prompt]", "The objective for the coding agent").option("--agent <agent>", "Agent to use (claude, codex, rovodev, or opencode)").option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).option("--prevent-sleep <mode>", "Prevent system sleep during the run (\"on\" or \"off\")", parseOnOffBoolean).option("--worktree", "Run in a separate git worktree (enables multiple agents on the same repo)", false).option("--mock", "", false).action(async (promptArg, options) => {
|
|
4182
|
+
program.name("gnhf").description("Before I go to bed, I tell my agents: good night, have fun").version(packageVersion).argument("[prompt]", "The objective for the coding agent").option("--agent <agent>", "Agent to use (claude, codex, rovodev, or opencode)").option("--max-iterations <n>", "Abort after N total iterations", parseNonNegativeInteger).option("--max-tokens <n>", "Abort after N total input+output tokens", parseNonNegativeInteger).option("--stop-when <condition>", "End the loop when the agent reports this natural-language condition is met").option("--prevent-sleep <mode>", "Prevent system sleep during the run (\"on\" or \"off\")", parseOnOffBoolean).option("--worktree", "Run in a separate git worktree (enables multiple agents on the same repo)", false).option("--mock", "", false).action(async (promptArg, options) => {
|
|
4085
4183
|
if (options.mock) {
|
|
4086
4184
|
const mock = new MockOrchestrator();
|
|
4087
4185
|
enterAltScreen();
|
|
@@ -4120,6 +4218,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4120
4218
|
let worktreeCleanup = null;
|
|
4121
4219
|
const currentBranch = getCurrentBranch(cwd);
|
|
4122
4220
|
const onGnhfBranch = currentBranch.startsWith("gnhf/");
|
|
4221
|
+
const schemaOptions = { includeStopField: options.stopWhen !== void 0 };
|
|
4123
4222
|
let runInfo;
|
|
4124
4223
|
let startIteration = 0;
|
|
4125
4224
|
if (options.worktree) {
|
|
@@ -4131,7 +4230,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4131
4230
|
console.error("Cannot use --worktree from a gnhf branch. Switch to the base branch first.");
|
|
4132
4231
|
process$1.exit(1);
|
|
4133
4232
|
}
|
|
4134
|
-
const wt = initializeWorktreeRun(prompt, cwd);
|
|
4233
|
+
const wt = initializeWorktreeRun(prompt, cwd, schemaOptions);
|
|
4135
4234
|
runInfo = wt.runInfo;
|
|
4136
4235
|
effectiveCwd = wt.effectiveCwd;
|
|
4137
4236
|
worktreePath = wt.worktreePath;
|
|
@@ -4146,18 +4245,18 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4146
4245
|
});
|
|
4147
4246
|
} else if (onGnhfBranch) {
|
|
4148
4247
|
const existingRunId = currentBranch.slice(5);
|
|
4149
|
-
const existing = resumeRun(existingRunId, cwd);
|
|
4248
|
+
const existing = resumeRun(existingRunId, cwd, schemaOptions);
|
|
4150
4249
|
const existingPrompt = readFileSync(existing.promptPath, "utf-8");
|
|
4151
4250
|
if (!prompt || prompt === existingPrompt) {
|
|
4152
4251
|
prompt = existingPrompt;
|
|
4153
4252
|
runInfo = existing;
|
|
4154
4253
|
startIteration = getLastIterationNumber(existing);
|
|
4155
4254
|
} else {
|
|
4156
|
-
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.");
|
|
4157
4256
|
if (answer === "o") {
|
|
4158
4257
|
ensureCleanWorkingTree(cwd);
|
|
4159
|
-
runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd);
|
|
4160
|
-
} 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);
|
|
4161
4260
|
else process$1.exit(0);
|
|
4162
4261
|
}
|
|
4163
4262
|
} else {
|
|
@@ -4165,7 +4264,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4165
4264
|
program.help();
|
|
4166
4265
|
return;
|
|
4167
4266
|
}
|
|
4168
|
-
runInfo = initializeNewBranch(prompt, cwd);
|
|
4267
|
+
runInfo = initializeNewBranch(prompt, cwd, schemaOptions);
|
|
4169
4268
|
}
|
|
4170
4269
|
let sleepPreventionCleanup = null;
|
|
4171
4270
|
if (config.preventSleep) {
|
|
@@ -4193,6 +4292,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4193
4292
|
startIteration,
|
|
4194
4293
|
maxIterations: options.maxIterations,
|
|
4195
4294
|
maxTokens: options.maxTokens,
|
|
4295
|
+
stopWhen: options.stopWhen,
|
|
4196
4296
|
preventSleep: config.preventSleep,
|
|
4197
4297
|
agentArgsOverride: config.agentArgsOverride?.[config.agent],
|
|
4198
4298
|
worktree: options.worktree,
|
|
@@ -4201,9 +4301,10 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4201
4301
|
nodeVersion: process$1.version,
|
|
4202
4302
|
gnhfVersion: packageVersion
|
|
4203
4303
|
});
|
|
4204
|
-
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, {
|
|
4205
4305
|
maxIterations: options.maxIterations,
|
|
4206
|
-
maxTokens: options.maxTokens
|
|
4306
|
+
maxTokens: options.maxTokens,
|
|
4307
|
+
stopWhen: options.stopWhen
|
|
4207
4308
|
});
|
|
4208
4309
|
let shutdownSignal = null;
|
|
4209
4310
|
enterAltScreen();
|
|
@@ -4284,6 +4385,7 @@ function die(message) {
|
|
|
4284
4385
|
try {
|
|
4285
4386
|
await program.parseAsync();
|
|
4286
4387
|
} catch (err) {
|
|
4388
|
+
if (err instanceof PromptSignalError) process$1.exit(getSignalExitCode(err.signal));
|
|
4287
4389
|
die(err instanceof Error ? err.message : String(err));
|
|
4288
4390
|
}
|
|
4289
4391
|
//#endregion
|