gnhf 0.1.25 → 0.1.26
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 +7 -6
- package/dist/cli.mjs +52 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,6 +138,7 @@ npm link
|
|
|
138
138
|
- **Incremental commits** — each successful iteration is a separate git commit, so you can cherry-pick or revert individual changes
|
|
139
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
|
|
140
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
|
|
141
|
+
- **Iteration finalization** - agents are expected to finish validation, stop any background processes they started, and only then emit the final JSON result for the iteration
|
|
141
142
|
- **Shared memory** — the agent reads `notes.md` (built up from prior iterations) to communicate across iterations
|
|
142
143
|
- **Local run metadata** — gnhf stores prompt, notes, and resume metadata under `.gnhf/runs/` and ignores it locally, so your branch only contains intentional work
|
|
143
144
|
- **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
|
|
@@ -243,12 +244,12 @@ Including a snippet of `gnhf.log` is the single most useful thing you can attach
|
|
|
243
244
|
|
|
244
245
|
`gnhf` supports four agents:
|
|
245
246
|
|
|
246
|
-
| Agent | Flag | Requirements | Notes
|
|
247
|
-
| ----------- | ------------------ | -------------------------------------------------------------------------- |
|
|
248
|
-
| Claude Code | `--agent claude` | Install Anthropic's `claude` CLI and sign in first. | `gnhf` invokes `claude` directly in non-interactive mode.
|
|
249
|
-
| Codex | `--agent codex` | Install OpenAI's `codex` CLI and sign in first. | `gnhf` invokes `codex exec` directly in non-interactive mode.
|
|
250
|
-
| Rovo Dev | `--agent rovodev` | Install Atlassian's `acli` and authenticate it with Rovo Dev first. | `gnhf` starts a local `acli rovodev serve --disable-session-token <port>` process automatically in the repo workspace.
|
|
251
|
-
| OpenCode | `--agent opencode` | Install `opencode` and configure at least one usable model provider first. | `gnhf` starts a local `opencode serve --hostname 127.0.0.1 --port <port> --print-logs` process automatically, creates a per-run session, and applies a blanket allow rule so tool calls do not block on prompts.
|
|
247
|
+
| Agent | Flag | Requirements | Notes |
|
|
248
|
+
| ----------- | ------------------ | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
249
|
+
| Claude Code | `--agent claude` | Install Anthropic's `claude` CLI and sign in first. | `gnhf` invokes `claude` directly in non-interactive mode. After Claude emits a successful structured result, `gnhf` treats that result as final and shuts down any lingering Claude process tree after a short grace period. |
|
|
250
|
+
| Codex | `--agent codex` | Install OpenAI's `codex` CLI and sign in first. | `gnhf` invokes `codex exec` directly in non-interactive mode. |
|
|
251
|
+
| Rovo Dev | `--agent rovodev` | Install Atlassian's `acli` and authenticate it with Rovo Dev first. | `gnhf` starts a local `acli rovodev serve --disable-session-token <port>` process automatically in the repo workspace. |
|
|
252
|
+
| OpenCode | `--agent opencode` | Install `opencode` and configure at least one usable model provider first. | `gnhf` starts a local `opencode serve --hostname 127.0.0.1 --port <port> --print-logs` process automatically, creates a per-run session, and applies a blanket allow rule so tool calls do not block on prompts. |
|
|
252
253
|
|
|
253
254
|
## Development
|
|
254
255
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1043,6 +1043,7 @@ function setupAbortHandler(signal, child, reject, abortChild = () => {
|
|
|
1043
1043
|
}
|
|
1044
1044
|
//#endregion
|
|
1045
1045
|
//#region src/core/agents/claude.ts
|
|
1046
|
+
const DEFAULT_FINAL_RESULT_EXIT_GRACE_MS = 15e3;
|
|
1046
1047
|
function shouldUseWindowsShell$2(bin, platform) {
|
|
1047
1048
|
if (platform !== "win32") return false;
|
|
1048
1049
|
if (/\.(cmd|bat)$/i.test(bin)) return true;
|
|
@@ -1073,8 +1074,22 @@ function terminateClaudeProcess(child, platform) {
|
|
|
1073
1074
|
} catch {}
|
|
1074
1075
|
return;
|
|
1075
1076
|
}
|
|
1077
|
+
if (child.pid) try {
|
|
1078
|
+
process.kill(-child.pid, "SIGTERM");
|
|
1079
|
+
return;
|
|
1080
|
+
} catch {}
|
|
1076
1081
|
child.kill("SIGTERM");
|
|
1077
1082
|
}
|
|
1083
|
+
async function shutdownClaudeProcess(child, platform) {
|
|
1084
|
+
if (platform === "win32") {
|
|
1085
|
+
terminateClaudeProcess(child, platform);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
await shutdownChildProcess(child, { detached: true });
|
|
1089
|
+
}
|
|
1090
|
+
function isFinalStructuredResult(event) {
|
|
1091
|
+
return !event.is_error && event.subtype === "success" && !!event.structured_output;
|
|
1092
|
+
}
|
|
1078
1093
|
function buildClaudeArgs(prompt, schema, extraArgs) {
|
|
1079
1094
|
const userArgs = extraArgs ?? [];
|
|
1080
1095
|
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="));
|
|
@@ -1108,12 +1123,14 @@ var ClaudeAgent = class {
|
|
|
1108
1123
|
name = "claude";
|
|
1109
1124
|
bin;
|
|
1110
1125
|
extraArgs;
|
|
1126
|
+
finalResultGraceMs;
|
|
1111
1127
|
platform;
|
|
1112
1128
|
schema;
|
|
1113
1129
|
constructor(binOrDeps = {}) {
|
|
1114
1130
|
const deps = typeof binOrDeps === "string" ? { bin: binOrDeps } : binOrDeps;
|
|
1115
1131
|
this.bin = deps.bin ?? "claude";
|
|
1116
1132
|
this.extraArgs = deps.extraArgs;
|
|
1133
|
+
this.finalResultGraceMs = deps.finalResultGraceMs ?? DEFAULT_FINAL_RESULT_EXIT_GRACE_MS;
|
|
1117
1134
|
this.platform = deps.platform ?? process.platform;
|
|
1118
1135
|
this.schema = deps.schema ?? buildAgentOutputSchema({ includeStopField: false });
|
|
1119
1136
|
}
|
|
@@ -1123,6 +1140,7 @@ var ClaudeAgent = class {
|
|
|
1123
1140
|
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
1124
1141
|
const child = spawn(this.bin, buildClaudeArgs(prompt, this.schema, this.extraArgs), {
|
|
1125
1142
|
cwd,
|
|
1143
|
+
detached: this.platform !== "win32",
|
|
1126
1144
|
shell: shouldUseWindowsShell$2(this.bin, this.platform),
|
|
1127
1145
|
stdio: [
|
|
1128
1146
|
"ignore",
|
|
@@ -1133,7 +1151,11 @@ var ClaudeAgent = class {
|
|
|
1133
1151
|
});
|
|
1134
1152
|
if (setupAbortHandler(signal, child, reject, () => terminateClaudeProcess(child, this.platform))) return;
|
|
1135
1153
|
let resultEvent = null;
|
|
1154
|
+
let finalStructuredResultEvent = null;
|
|
1136
1155
|
let latestResultUsage = null;
|
|
1156
|
+
let finalResultCleanupTimer = null;
|
|
1157
|
+
let closedAfterFinalCleanup = false;
|
|
1158
|
+
let stderr = "";
|
|
1137
1159
|
const cumulative = {
|
|
1138
1160
|
inputTokens: 0,
|
|
1139
1161
|
outputTokens: 0,
|
|
@@ -1145,6 +1167,12 @@ var ClaudeAgent = class {
|
|
|
1145
1167
|
let lastAnonymousAssistantId = null;
|
|
1146
1168
|
let lastAnonymousAssistantUsage = null;
|
|
1147
1169
|
let pendingAnonymousAssistantUsage = null;
|
|
1170
|
+
child.stderr.on("data", (data) => {
|
|
1171
|
+
stderr += data.toString();
|
|
1172
|
+
});
|
|
1173
|
+
child.on("error", (err) => {
|
|
1174
|
+
reject(/* @__PURE__ */ new Error(`Failed to spawn claude: ${err.message}`));
|
|
1175
|
+
});
|
|
1148
1176
|
parseJSONLStream(child.stdout, logStream, (event) => {
|
|
1149
1177
|
if (event.type === "assistant") {
|
|
1150
1178
|
const msg = event.message;
|
|
@@ -1205,24 +1233,38 @@ var ClaudeAgent = class {
|
|
|
1205
1233
|
if (event.type === "result") {
|
|
1206
1234
|
const next = event;
|
|
1207
1235
|
latestResultUsage = next.usage;
|
|
1208
|
-
if (next
|
|
1236
|
+
if (isFinalStructuredResult(next)) {
|
|
1237
|
+
finalStructuredResultEvent = next;
|
|
1238
|
+
if (finalResultCleanupTimer) clearTimeout(finalResultCleanupTimer);
|
|
1239
|
+
finalResultCleanupTimer = setTimeout(() => {
|
|
1240
|
+
closedAfterFinalCleanup = true;
|
|
1241
|
+
shutdownClaudeProcess(child, this.platform);
|
|
1242
|
+
}, this.finalResultGraceMs);
|
|
1243
|
+
} else if (!finalStructuredResultEvent && (next.is_error || next.subtype !== "success" || next.structured_output || !resultEvent)) resultEvent = next;
|
|
1209
1244
|
}
|
|
1210
1245
|
});
|
|
1211
|
-
|
|
1212
|
-
if (
|
|
1246
|
+
child.on("close", (code) => {
|
|
1247
|
+
if (finalResultCleanupTimer) clearTimeout(finalResultCleanupTimer);
|
|
1248
|
+
logStream?.end();
|
|
1249
|
+
if (code !== 0 && !closedAfterFinalCleanup) {
|
|
1250
|
+
reject(/* @__PURE__ */ new Error(`claude exited with code ${code}: ${stderr}`));
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const terminalResultEvent = finalStructuredResultEvent ?? resultEvent;
|
|
1254
|
+
if (!terminalResultEvent) {
|
|
1213
1255
|
reject(/* @__PURE__ */ new Error("claude returned no result event"));
|
|
1214
1256
|
return;
|
|
1215
1257
|
}
|
|
1216
|
-
if (
|
|
1217
|
-
reject(/* @__PURE__ */ new Error(`claude reported error: ${JSON.stringify(
|
|
1258
|
+
if (terminalResultEvent.is_error || terminalResultEvent.subtype !== "success") {
|
|
1259
|
+
reject(/* @__PURE__ */ new Error(`claude reported error: ${JSON.stringify(terminalResultEvent)}`));
|
|
1218
1260
|
return;
|
|
1219
1261
|
}
|
|
1220
|
-
if (!
|
|
1262
|
+
if (!terminalResultEvent.structured_output) {
|
|
1221
1263
|
reject(/* @__PURE__ */ new Error("claude returned no structured_output"));
|
|
1222
1264
|
return;
|
|
1223
1265
|
}
|
|
1224
|
-
const output =
|
|
1225
|
-
const usage = toTokenUsage(latestResultUsage ??
|
|
1266
|
+
const output = terminalResultEvent.structured_output;
|
|
1267
|
+
const usage = toTokenUsage(latestResultUsage ?? terminalResultEvent.usage);
|
|
1226
1268
|
onUsage?.(usage);
|
|
1227
1269
|
resolve({
|
|
1228
1270
|
output,
|
|
@@ -2818,7 +2860,8 @@ This is iteration ${params.n}. Each iteration aims to make an incremental step f
|
|
|
2818
2860
|
2. Identify the next smallest logical unit of work that's individually verifiable and would make incremental progress towards the objective, and treat that as the scope of this iteration
|
|
2819
2861
|
3. If you attempted a solution and it didn't end up moving the needle on the objective, document learnings and record success=false, then conclude the iteration rather than continuously pivoting
|
|
2820
2862
|
4. If you made code changes, run build/tests/linters/formatters if available to validate your work. Do NOT make any git commits - that will be handled automatically by the gnhf orchestrator
|
|
2821
|
-
|
|
2863
|
+
5. If you started any long-running background processes (dev servers, browsers, watchers, Electron, etc.), stop them before finishing the iteration
|
|
2864
|
+
6. Only submit the final JSON object after the result is final: your work is complete, validation is done, and you have stopped any background processes you started
|
|
2822
2865
|
|
|
2823
2866
|
## Output
|
|
2824
2867
|
|