gnhf 0.1.21 → 0.1.23
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 +2 -1
- package/dist/cli.mjs +94 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ 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
142
|
- **Resume support** — run `gnhf` while on an existing `gnhf/` branch to pick up where a previous run left off
|
|
@@ -172,6 +172,7 @@ Pass `--worktree` to run each agent in an isolated [git worktree](https://git-sc
|
|
|
172
172
|
| `--agent <agent>` | Agent to use (`claude`, `codex`, `rovodev`, or `opencode`) | config file (`claude`) |
|
|
173
173
|
| `--max-iterations <n>` | Abort after `n` total iterations | unlimited |
|
|
174
174
|
| `--max-tokens <n>` | Abort after `n` total input+output tokens | unlimited |
|
|
175
|
+
| `--stop-when <cond>` | End the loop when the agent reports this natural-language condition is met | unlimited |
|
|
175
176
|
| `--prevent-sleep <mode>` | Prevent system sleep during the run (`on`/`off` or `true`/`false`) | config file (`on`) |
|
|
176
177
|
| `--worktree` | Run in a separate git worktree (enables multiple agents concurrently) | `false` |
|
|
177
178
|
| `--version` | Show version | |
|
package/dist/cli.mjs
CHANGED
|
@@ -440,7 +440,8 @@ const AGENT_OUTPUT_SCHEMA = {
|
|
|
440
440
|
key_learnings: {
|
|
441
441
|
type: "array",
|
|
442
442
|
items: { type: "string" }
|
|
443
|
-
}
|
|
443
|
+
},
|
|
444
|
+
should_fully_stop: { type: "boolean" }
|
|
444
445
|
},
|
|
445
446
|
required: [
|
|
446
447
|
"success",
|
|
@@ -1082,6 +1083,20 @@ function buildClaudeArgs(prompt, extraArgs) {
|
|
|
1082
1083
|
...userSpecifiedPermissionMode ? [] : ["--dangerously-skip-permissions"]
|
|
1083
1084
|
];
|
|
1084
1085
|
}
|
|
1086
|
+
function toTokenUsage(usage) {
|
|
1087
|
+
return {
|
|
1088
|
+
inputTokens: (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0),
|
|
1089
|
+
outputTokens: usage.output_tokens ?? 0,
|
|
1090
|
+
cacheReadTokens: usage.cache_read_input_tokens ?? 0,
|
|
1091
|
+
cacheCreationTokens: usage.cache_creation_input_tokens ?? 0
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
function isSameUsage(a, b) {
|
|
1095
|
+
return a.inputTokens === b.inputTokens && a.outputTokens === b.outputTokens && a.cacheReadTokens === b.cacheReadTokens && a.cacheCreationTokens === b.cacheCreationTokens;
|
|
1096
|
+
}
|
|
1097
|
+
function extendsUsage(next, previous) {
|
|
1098
|
+
return next.inputTokens >= previous.inputTokens && next.outputTokens >= previous.outputTokens && next.cacheReadTokens >= previous.cacheReadTokens && next.cacheCreationTokens >= previous.cacheCreationTokens && !isSameUsage(next, previous);
|
|
1099
|
+
}
|
|
1085
1100
|
var ClaudeAgent = class {
|
|
1086
1101
|
name = "claude";
|
|
1087
1102
|
bin;
|
|
@@ -1115,13 +1130,60 @@ var ClaudeAgent = class {
|
|
|
1115
1130
|
cacheReadTokens: 0,
|
|
1116
1131
|
cacheCreationTokens: 0
|
|
1117
1132
|
};
|
|
1133
|
+
const usageByMessageId = /* @__PURE__ */ new Map();
|
|
1134
|
+
let anonymousAssistantCount = 0;
|
|
1135
|
+
let lastAnonymousAssistantId = null;
|
|
1136
|
+
let lastAnonymousAssistantUsage = null;
|
|
1137
|
+
let pendingAnonymousAssistantUsage = null;
|
|
1118
1138
|
parseJSONLStream(child.stdout, logStream, (event) => {
|
|
1119
1139
|
if (event.type === "assistant") {
|
|
1120
1140
|
const msg = event.message;
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1141
|
+
const nextUsage = toTokenUsage(msg.usage);
|
|
1142
|
+
let messageId = msg.id;
|
|
1143
|
+
let previousUsage;
|
|
1144
|
+
if (messageId) {
|
|
1145
|
+
previousUsage = usageByMessageId.get(messageId);
|
|
1146
|
+
lastAnonymousAssistantId = null;
|
|
1147
|
+
lastAnonymousAssistantUsage = null;
|
|
1148
|
+
pendingAnonymousAssistantUsage = null;
|
|
1149
|
+
} else if (pendingAnonymousAssistantUsage && extendsUsage(nextUsage, pendingAnonymousAssistantUsage)) {
|
|
1150
|
+
messageId = `assistant-${anonymousAssistantCount++}`;
|
|
1151
|
+
previousUsage = pendingAnonymousAssistantUsage;
|
|
1152
|
+
cumulative.inputTokens += pendingAnonymousAssistantUsage.inputTokens;
|
|
1153
|
+
cumulative.outputTokens += pendingAnonymousAssistantUsage.outputTokens;
|
|
1154
|
+
cumulative.cacheReadTokens += pendingAnonymousAssistantUsage.cacheReadTokens;
|
|
1155
|
+
cumulative.cacheCreationTokens += pendingAnonymousAssistantUsage.cacheCreationTokens;
|
|
1156
|
+
usageByMessageId.set(messageId, pendingAnonymousAssistantUsage);
|
|
1157
|
+
pendingAnonymousAssistantUsage = null;
|
|
1158
|
+
lastAnonymousAssistantId = messageId;
|
|
1159
|
+
lastAnonymousAssistantUsage = nextUsage;
|
|
1160
|
+
} else if (lastAnonymousAssistantId && lastAnonymousAssistantUsage && extendsUsage(nextUsage, lastAnonymousAssistantUsage)) {
|
|
1161
|
+
messageId = lastAnonymousAssistantId;
|
|
1162
|
+
previousUsage = usageByMessageId.get(messageId);
|
|
1163
|
+
pendingAnonymousAssistantUsage = null;
|
|
1164
|
+
lastAnonymousAssistantUsage = nextUsage;
|
|
1165
|
+
} else if (lastAnonymousAssistantId && lastAnonymousAssistantUsage && isSameUsage(nextUsage, lastAnonymousAssistantUsage)) {
|
|
1166
|
+
messageId = lastAnonymousAssistantId;
|
|
1167
|
+
previousUsage = usageByMessageId.get(messageId);
|
|
1168
|
+
pendingAnonymousAssistantUsage ??= nextUsage;
|
|
1169
|
+
} else {
|
|
1170
|
+
messageId = `assistant-${anonymousAssistantCount++}`;
|
|
1171
|
+
pendingAnonymousAssistantUsage = null;
|
|
1172
|
+
lastAnonymousAssistantId = messageId;
|
|
1173
|
+
lastAnonymousAssistantUsage = nextUsage;
|
|
1174
|
+
}
|
|
1175
|
+
if (previousUsage) {
|
|
1176
|
+
cumulative.inputTokens += nextUsage.inputTokens - previousUsage.inputTokens;
|
|
1177
|
+
cumulative.outputTokens += nextUsage.outputTokens - previousUsage.outputTokens;
|
|
1178
|
+
cumulative.cacheReadTokens += nextUsage.cacheReadTokens - previousUsage.cacheReadTokens;
|
|
1179
|
+
cumulative.cacheCreationTokens += nextUsage.cacheCreationTokens - previousUsage.cacheCreationTokens;
|
|
1180
|
+
} else {
|
|
1181
|
+
cumulative.inputTokens += nextUsage.inputTokens;
|
|
1182
|
+
cumulative.outputTokens += nextUsage.outputTokens;
|
|
1183
|
+
cumulative.cacheReadTokens += nextUsage.cacheReadTokens;
|
|
1184
|
+
cumulative.cacheCreationTokens += nextUsage.cacheCreationTokens;
|
|
1185
|
+
}
|
|
1186
|
+
usageByMessageId.set(messageId, nextUsage);
|
|
1125
1187
|
onUsage?.({ ...cumulative });
|
|
1126
1188
|
if (onMessage) {
|
|
1127
1189
|
const content = msg.content;
|
|
@@ -1146,12 +1208,7 @@ var ClaudeAgent = class {
|
|
|
1146
1208
|
return;
|
|
1147
1209
|
}
|
|
1148
1210
|
const output = resultEvent.structured_output;
|
|
1149
|
-
const usage =
|
|
1150
|
-
inputTokens: (resultEvent.usage.input_tokens ?? 0) + (resultEvent.usage.cache_read_input_tokens ?? 0),
|
|
1151
|
-
outputTokens: resultEvent.usage.output_tokens ?? 0,
|
|
1152
|
-
cacheReadTokens: resultEvent.usage.cache_read_input_tokens ?? 0,
|
|
1153
|
-
cacheCreationTokens: resultEvent.usage.cache_creation_input_tokens ?? 0
|
|
1154
|
-
};
|
|
1211
|
+
const usage = toTokenUsage(resultEvent.usage);
|
|
1155
1212
|
onUsage?.(usage);
|
|
1156
1213
|
resolve({
|
|
1157
1214
|
output,
|
|
@@ -2723,6 +2780,14 @@ function createAgent(name, runInfo, pathOverride, agentArgsOverride) {
|
|
|
2723
2780
|
//#endregion
|
|
2724
2781
|
//#region src/templates/iteration-prompt.ts
|
|
2725
2782
|
function buildIterationPrompt(params) {
|
|
2783
|
+
const outputFields = [
|
|
2784
|
+
"- 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",
|
|
2785
|
+
"- summary: a concise one-sentence summary of the accomplishment in this iteration",
|
|
2786
|
+
"- 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",
|
|
2787
|
+
"- key_learnings: an array of new learnings that were surprising, weren't captured by previous notes and would be informative for future iterations"
|
|
2788
|
+
];
|
|
2789
|
+
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 (or omit it).` : "";
|
|
2726
2791
|
return `You are working autonomously towards an objective given below.
|
|
2727
2792
|
This is iteration ${params.n}. Each iteration aims to make an incremental step forward, not to complete the entire objective.
|
|
2728
2793
|
|
|
@@ -2736,10 +2801,7 @@ This is iteration ${params.n}. Each iteration aims to make an incremental step f
|
|
|
2736
2801
|
|
|
2737
2802
|
## Output
|
|
2738
2803
|
|
|
2739
|
-
|
|
2740
|
-
- summary: a concise one-sentence summary of the accomplishment in this iteration
|
|
2741
|
-
- 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
|
|
2742
|
-
- key_learnings: an array of new learnings that were surprising, weren't captured by previous notes and would be informative for future iterations
|
|
2804
|
+
${outputFields.join("\n")}${stopConditionSection}
|
|
2743
2805
|
|
|
2744
2806
|
## Objective
|
|
2745
2807
|
|
|
@@ -2854,7 +2916,8 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2854
2916
|
const iterationPrompt = buildIterationPrompt({
|
|
2855
2917
|
n: this.state.currentIteration,
|
|
2856
2918
|
runId: this.runInfo.runId,
|
|
2857
|
-
prompt: this.prompt
|
|
2919
|
+
prompt: this.prompt,
|
|
2920
|
+
stopWhen: this.limits.stopWhen
|
|
2858
2921
|
});
|
|
2859
2922
|
appendDebugLog("iteration:start", {
|
|
2860
2923
|
iteration: this.state.currentIteration,
|
|
@@ -2901,6 +2964,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2901
2964
|
totalOutputTokens: this.state.totalOutputTokens,
|
|
2902
2965
|
commitCount: this.state.commitCount
|
|
2903
2966
|
});
|
|
2967
|
+
if (this.limits.stopWhen !== void 0 && result.shouldFullyStop) {
|
|
2968
|
+
this.abort("stop condition met");
|
|
2969
|
+
break;
|
|
2970
|
+
}
|
|
2904
2971
|
const postIterationAbortReason = this.getPostIterationAbortReason();
|
|
2905
2972
|
if (postIterationAbortReason) {
|
|
2906
2973
|
this.abort(postIterationAbortReason);
|
|
@@ -2997,13 +3064,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2997
3064
|
cacheCreationTokens: result.usage.cacheCreationTokens
|
|
2998
3065
|
});
|
|
2999
3066
|
if (this.stopRequested) return { type: "stopped" };
|
|
3067
|
+
const shouldFullyStop = result.output.should_fully_stop === true;
|
|
3000
3068
|
if (result.output.success) return {
|
|
3001
3069
|
type: "completed",
|
|
3002
|
-
record: this.recordSuccess(result.output)
|
|
3070
|
+
record: this.recordSuccess(result.output),
|
|
3071
|
+
shouldFullyStop
|
|
3003
3072
|
};
|
|
3004
3073
|
return {
|
|
3005
3074
|
type: "completed",
|
|
3006
|
-
record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, toStringArray(result.output.key_learnings))
|
|
3075
|
+
record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, toStringArray(result.output.key_learnings)),
|
|
3076
|
+
shouldFullyStop
|
|
3007
3077
|
};
|
|
3008
3078
|
} catch (err) {
|
|
3009
3079
|
const elapsedMs = Date.now() - agentStartedAt;
|
|
@@ -3034,7 +3104,8 @@ var Orchestrator = class extends EventEmitter {
|
|
|
3034
3104
|
const summary = err instanceof Error ? err.message : String(err);
|
|
3035
3105
|
return {
|
|
3036
3106
|
type: "completed",
|
|
3037
|
-
record: this.recordFailure(`[ERROR] ${summary}`, summary, [])
|
|
3107
|
+
record: this.recordFailure(`[ERROR] ${summary}`, summary, []),
|
|
3108
|
+
shouldFullyStop: false
|
|
3038
3109
|
};
|
|
3039
3110
|
} finally {
|
|
3040
3111
|
this.activeAbortController = null;
|
|
@@ -4025,7 +4096,7 @@ function readReexecStdinPrompt(env) {
|
|
|
4025
4096
|
}
|
|
4026
4097
|
}
|
|
4027
4098
|
const program = new Command();
|
|
4028
|
-
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) => {
|
|
4099
|
+
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) => {
|
|
4029
4100
|
if (options.mock) {
|
|
4030
4101
|
const mock = new MockOrchestrator();
|
|
4031
4102
|
enterAltScreen();
|
|
@@ -4137,6 +4208,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4137
4208
|
startIteration,
|
|
4138
4209
|
maxIterations: options.maxIterations,
|
|
4139
4210
|
maxTokens: options.maxTokens,
|
|
4211
|
+
stopWhen: options.stopWhen,
|
|
4140
4212
|
preventSleep: config.preventSleep,
|
|
4141
4213
|
agentArgsOverride: config.agentArgsOverride?.[config.agent],
|
|
4142
4214
|
worktree: options.worktree,
|
|
@@ -4147,7 +4219,8 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
4147
4219
|
});
|
|
4148
4220
|
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent], config.agentArgsOverride?.[config.agent]), runInfo, prompt, effectiveCwd, startIteration, {
|
|
4149
4221
|
maxIterations: options.maxIterations,
|
|
4150
|
-
maxTokens: options.maxTokens
|
|
4222
|
+
maxTokens: options.maxTokens,
|
|
4223
|
+
stopWhen: options.stopWhen
|
|
4151
4224
|
});
|
|
4152
4225
|
let shutdownSignal = null;
|
|
4153
4226
|
enterAltScreen();
|