gnhf 0.1.3 → 0.1.4
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 +8 -7
- package/dist/cli.mjs +325 -282
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
# gnhf
|
|
2
|
-
|
|
3
1
|
<p align="center">Before I go to bed, I tell my agents:</p>
|
|
4
2
|
<h1 align="center">good night, have fun</h1>
|
|
5
3
|
|
|
@@ -41,7 +39,7 @@
|
|
|
41
39
|
<img src="docs/splash.png" alt="gnhf — Good Night, Have Fun" width="800">
|
|
42
40
|
</p>
|
|
43
41
|
|
|
44
|
-
gnhf is a [ralph
|
|
42
|
+
gnhf is a [ralph](https://ghuntley.com/ralph/), [autoresearch](https://github.com/karpathy/autoresearch)-style orchestrator that keeps your agents running while you sleep — each iteration makes one small, committed, documented change towards an objective.
|
|
45
43
|
You wake up to a branch full of clean work and a log of everything that happened.
|
|
46
44
|
|
|
47
45
|
- **Dead simple** — one command starts an autonomous loop that runs until you Ctrl+C
|
|
@@ -119,6 +117,7 @@ npm link
|
|
|
119
117
|
|
|
120
118
|
- **Incremental commits** — each successful iteration is a separate git commit, so you can cherry-pick or revert individual changes
|
|
121
119
|
- **Shared memory** — the agent reads `notes.md` (built up from prior iterations) to communicate across iterations
|
|
120
|
+
- **Local run metadata** — gnhf stores prompt, notes, and resume metadata under `.gnhf/runs/` and ignores it locally, so your branch only contains intentional work
|
|
122
121
|
- **Resume support** — run `gnhf` while on an existing `gnhf/` branch to pick up where a previous run left off
|
|
123
122
|
|
|
124
123
|
## CLI Reference
|
|
@@ -132,10 +131,10 @@ npm link
|
|
|
132
131
|
|
|
133
132
|
### Flags
|
|
134
133
|
|
|
135
|
-
| Flag | Description | Default
|
|
136
|
-
| ----------------- | ---------------------------------- |
|
|
137
|
-
| `--agent <agent>` | Agent to use (`claude` or `codex`) | `claude` |
|
|
138
|
-
| `--version` | Show version |
|
|
134
|
+
| Flag | Description | Default |
|
|
135
|
+
| ----------------- | ---------------------------------- | ---------------------- |
|
|
136
|
+
| `--agent <agent>` | Agent to use (`claude` or `codex`) | config file (`claude`) |
|
|
137
|
+
| `--version` | Show version | |
|
|
139
138
|
|
|
140
139
|
## Configuration
|
|
141
140
|
|
|
@@ -149,6 +148,8 @@ agent: claude
|
|
|
149
148
|
maxConsecutiveFailures: 3
|
|
150
149
|
```
|
|
151
150
|
|
|
151
|
+
If the file does not exist yet, `gnhf` creates it on first run using the resolved defaults.
|
|
152
|
+
|
|
152
153
|
CLI flags override config file values.
|
|
153
154
|
|
|
154
155
|
## Development
|
package/dist/cli.mjs
CHANGED
|
@@ -3,10 +3,10 @@ import { appendFileSync, createWriteStream, existsSync, mkdirSync, readFileSync,
|
|
|
3
3
|
import process$1 from "node:process";
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
import { join } from "node:path";
|
|
6
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import yaml from "js-yaml";
|
|
9
|
-
import { execSync, spawn } from "node:child_process";
|
|
9
|
+
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
10
10
|
import { EventEmitter } from "node:events";
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
//#region src/core/config.ts
|
|
@@ -14,18 +14,39 @@ const DEFAULT_CONFIG = {
|
|
|
14
14
|
agent: "claude",
|
|
15
15
|
maxConsecutiveFailures: 3
|
|
16
16
|
};
|
|
17
|
+
function isMissingConfigError(error) {
|
|
18
|
+
if (!(error instanceof Error)) return false;
|
|
19
|
+
return "code" in error ? error.code === "ENOENT" : error.message.includes("ENOENT");
|
|
20
|
+
}
|
|
21
|
+
function serializeConfig(config) {
|
|
22
|
+
return `# Agent to use by default
|
|
23
|
+
agent: ${config.agent}
|
|
24
|
+
|
|
25
|
+
# Abort after this many consecutive failures
|
|
26
|
+
maxConsecutiveFailures: ${config.maxConsecutiveFailures}
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
17
29
|
function loadConfig(overrides) {
|
|
18
|
-
const
|
|
30
|
+
const configDir = join(homedir(), ".gnhf");
|
|
31
|
+
const configPath = join(configDir, "config.yml");
|
|
19
32
|
let fileConfig = {};
|
|
33
|
+
let shouldBootstrapConfig = false;
|
|
20
34
|
try {
|
|
21
35
|
const raw = readFileSync(configPath, "utf-8");
|
|
22
36
|
fileConfig = yaml.load(raw) ?? {};
|
|
23
|
-
} catch {
|
|
24
|
-
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (isMissingConfigError(error)) shouldBootstrapConfig = true;
|
|
39
|
+
}
|
|
40
|
+
const resolvedConfig = {
|
|
25
41
|
...DEFAULT_CONFIG,
|
|
26
42
|
...fileConfig,
|
|
27
43
|
...overrides
|
|
28
44
|
};
|
|
45
|
+
if (shouldBootstrapConfig) try {
|
|
46
|
+
mkdirSync(configDir, { recursive: true });
|
|
47
|
+
writeFileSync(configPath, serializeConfig(resolvedConfig), "utf-8");
|
|
48
|
+
} catch {}
|
|
49
|
+
return resolvedConfig;
|
|
29
50
|
}
|
|
30
51
|
//#endregion
|
|
31
52
|
//#region src/core/git.ts
|
|
@@ -45,6 +66,28 @@ function ensureCleanWorkingTree(cwd) {
|
|
|
45
66
|
function createBranch(branchName, cwd) {
|
|
46
67
|
git(`checkout -b ${branchName}`, cwd);
|
|
47
68
|
}
|
|
69
|
+
function getHeadCommit(cwd) {
|
|
70
|
+
return git("rev-parse HEAD", cwd);
|
|
71
|
+
}
|
|
72
|
+
function findLegacyRunBaseCommit(runId, cwd) {
|
|
73
|
+
try {
|
|
74
|
+
const marker = git("log --first-parent --reverse --format=%H%x09%s HEAD", cwd).split("\n").map((line) => {
|
|
75
|
+
const [sha, ...subjectParts] = line.split(" ");
|
|
76
|
+
return {
|
|
77
|
+
sha,
|
|
78
|
+
subject: subjectParts.join(" ")
|
|
79
|
+
};
|
|
80
|
+
}).find(({ subject }) => subject === `gnhf: initialize run ${runId}` || subject === `gnhf: overwrite run ${runId}`);
|
|
81
|
+
if (!marker?.sha) return null;
|
|
82
|
+
return git(`rev-parse ${marker.sha}^`, cwd);
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function getBranchCommitCount(baseCommit, cwd) {
|
|
88
|
+
if (!baseCommit) return 0;
|
|
89
|
+
return Number.parseInt(git(`rev-list --count --first-parent ${baseCommit}..HEAD`, cwd), 10);
|
|
90
|
+
}
|
|
48
91
|
function commitAll(message, cwd) {
|
|
49
92
|
git("add -A", cwd);
|
|
50
93
|
try {
|
|
@@ -56,8 +99,8 @@ function resetHard(cwd) {
|
|
|
56
99
|
git("clean -fd", cwd);
|
|
57
100
|
}
|
|
58
101
|
//#endregion
|
|
59
|
-
//#region src/core/
|
|
60
|
-
const
|
|
102
|
+
//#region src/core/agents/types.ts
|
|
103
|
+
const AGENT_OUTPUT_SCHEMA = {
|
|
61
104
|
type: "object",
|
|
62
105
|
properties: {
|
|
63
106
|
success: { type: "boolean" },
|
|
@@ -77,17 +120,29 @@ const OUTPUT_SCHEMA$1 = JSON.stringify({
|
|
|
77
120
|
"key_changes_made",
|
|
78
121
|
"key_learnings"
|
|
79
122
|
]
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/core/run.ts
|
|
126
|
+
function ensureRunMetadataIgnored(cwd) {
|
|
127
|
+
const excludePath = execFileSync("git", [
|
|
128
|
+
"rev-parse",
|
|
129
|
+
"--git-path",
|
|
130
|
+
"info/exclude"
|
|
131
|
+
], {
|
|
132
|
+
cwd,
|
|
133
|
+
encoding: "utf-8"
|
|
134
|
+
}).trim();
|
|
135
|
+
const resolved = isAbsolute(excludePath) ? excludePath : join(cwd, excludePath);
|
|
83
136
|
const entry = ".gnhf/runs/";
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
137
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
138
|
+
if (existsSync(resolved)) {
|
|
139
|
+
const content = readFileSync(resolved, "utf-8");
|
|
140
|
+
if (content.split("\n").some((line) => line.trim() === entry)) return;
|
|
141
|
+
appendFileSync(resolved, `${content.length > 0 && !content.endsWith("\n") ? "\n" : ""}${entry}\n`, "utf-8");
|
|
142
|
+
} else writeFileSync(resolved, `${entry}\n`, "utf-8");
|
|
88
143
|
}
|
|
89
|
-
function setupRun(runId, prompt, cwd) {
|
|
90
|
-
|
|
144
|
+
function setupRun(runId, prompt, baseCommit, cwd) {
|
|
145
|
+
ensureRunMetadataIgnored(cwd);
|
|
91
146
|
const runDir = join(cwd, ".gnhf", "runs", runId);
|
|
92
147
|
mkdirSync(runDir, { recursive: true });
|
|
93
148
|
const promptPath = join(runDir, "prompt.md");
|
|
@@ -95,26 +150,43 @@ function setupRun(runId, prompt, cwd) {
|
|
|
95
150
|
const notesPath = join(runDir, "notes.md");
|
|
96
151
|
writeFileSync(notesPath, `# gnhf run: ${runId}\n\nObjective: ${prompt}\n\n## Iteration Log\n`, "utf-8");
|
|
97
152
|
const schemaPath = join(runDir, "output-schema.json");
|
|
98
|
-
writeFileSync(schemaPath,
|
|
153
|
+
writeFileSync(schemaPath, JSON.stringify(AGENT_OUTPUT_SCHEMA, null, 2), "utf-8");
|
|
154
|
+
const baseCommitPath = join(runDir, "base-commit");
|
|
155
|
+
const hasStoredBaseCommit = existsSync(baseCommitPath);
|
|
156
|
+
const resolvedBaseCommit = hasStoredBaseCommit ? readFileSync(baseCommitPath, "utf-8").trim() : baseCommit;
|
|
157
|
+
if (!hasStoredBaseCommit) writeFileSync(baseCommitPath, `${baseCommit}\n`, "utf-8");
|
|
99
158
|
return {
|
|
100
159
|
runId,
|
|
101
160
|
runDir,
|
|
102
161
|
promptPath,
|
|
103
162
|
notesPath,
|
|
104
|
-
schemaPath
|
|
163
|
+
schemaPath,
|
|
164
|
+
baseCommit: resolvedBaseCommit,
|
|
165
|
+
baseCommitPath
|
|
105
166
|
};
|
|
106
167
|
}
|
|
107
168
|
function resumeRun(runId, cwd) {
|
|
108
169
|
const runDir = join(cwd, ".gnhf", "runs", runId);
|
|
109
170
|
if (!existsSync(runDir)) throw new Error(`Run directory not found: ${runDir}`);
|
|
171
|
+
const promptPath = join(runDir, "prompt.md");
|
|
172
|
+
const notesPath = join(runDir, "notes.md");
|
|
173
|
+
const schemaPath = join(runDir, "output-schema.json");
|
|
174
|
+
const baseCommitPath = join(runDir, "base-commit");
|
|
110
175
|
return {
|
|
111
176
|
runId,
|
|
112
177
|
runDir,
|
|
113
|
-
promptPath
|
|
114
|
-
notesPath
|
|
115
|
-
schemaPath
|
|
178
|
+
promptPath,
|
|
179
|
+
notesPath,
|
|
180
|
+
schemaPath,
|
|
181
|
+
baseCommit: existsSync(baseCommitPath) ? readFileSync(baseCommitPath, "utf-8").trim() : backfillLegacyBaseCommit(runId, baseCommitPath, cwd),
|
|
182
|
+
baseCommitPath
|
|
116
183
|
};
|
|
117
184
|
}
|
|
185
|
+
function backfillLegacyBaseCommit(runId, baseCommitPath, cwd) {
|
|
186
|
+
const baseCommit = findLegacyRunBaseCommit(runId, cwd) ?? getHeadCommit(cwd);
|
|
187
|
+
writeFileSync(baseCommitPath, `${baseCommit}\n`, "utf-8");
|
|
188
|
+
return baseCommit;
|
|
189
|
+
}
|
|
118
190
|
function getLastIterationNumber(runInfo) {
|
|
119
191
|
const files = readdirSync(runInfo.runDir);
|
|
120
192
|
let max = 0;
|
|
@@ -127,37 +199,81 @@ function getLastIterationNumber(runInfo) {
|
|
|
127
199
|
}
|
|
128
200
|
return max;
|
|
129
201
|
}
|
|
202
|
+
function formatListSection(title, items) {
|
|
203
|
+
if (items.length === 0) return "";
|
|
204
|
+
return `**${title}:**\n${items.map((item) => `- ${item}`).join("\n")}\n`;
|
|
205
|
+
}
|
|
130
206
|
function appendNotes(notesPath, iteration, summary, changes, learnings) {
|
|
131
207
|
appendFileSync(notesPath, [
|
|
132
208
|
`\n### Iteration ${iteration}\n`,
|
|
133
209
|
`**Summary:** ${summary}\n`,
|
|
134
|
-
|
|
135
|
-
|
|
210
|
+
formatListSection("Changes", changes),
|
|
211
|
+
formatListSection("Learnings", learnings)
|
|
136
212
|
].join("\n"), "utf-8");
|
|
137
213
|
}
|
|
138
214
|
//#endregion
|
|
139
|
-
//#region src/core/agents/
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
215
|
+
//#region src/core/agents/stream-utils.ts
|
|
216
|
+
/**
|
|
217
|
+
* Wire stderr collection, spawn-error handling, and the common close-handler
|
|
218
|
+
* prefix (logStream.end + non-zero exit code rejection) for a child process.
|
|
219
|
+
* Calls `onSuccess` only when the process exits with code 0.
|
|
220
|
+
*/
|
|
221
|
+
function setupChildProcessHandlers(child, agentName, logStream, reject, onSuccess) {
|
|
222
|
+
let stderr = "";
|
|
223
|
+
child.stderr.on("data", (data) => {
|
|
224
|
+
stderr += data.toString();
|
|
225
|
+
});
|
|
226
|
+
child.on("error", (err) => {
|
|
227
|
+
reject(/* @__PURE__ */ new Error(`Failed to spawn ${agentName}: ${err.message}`));
|
|
228
|
+
});
|
|
229
|
+
child.on("close", (code) => {
|
|
230
|
+
logStream?.end();
|
|
231
|
+
if (code !== 0) {
|
|
232
|
+
reject(/* @__PURE__ */ new Error(`${agentName} exited with code ${code}: ${stderr}`));
|
|
233
|
+
return;
|
|
152
234
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
235
|
+
onSuccess();
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Parse a JSONL stream, calling the callback for each parsed event.
|
|
240
|
+
* Handles buffering of incomplete lines and skips unparseable lines.
|
|
241
|
+
*/
|
|
242
|
+
function parseJSONLStream(stream, logStream, callback) {
|
|
243
|
+
let buffer = "";
|
|
244
|
+
stream.on("data", (data) => {
|
|
245
|
+
logStream?.write(data);
|
|
246
|
+
buffer += data.toString();
|
|
247
|
+
const lines = buffer.split("\n");
|
|
248
|
+
buffer = lines.pop() ?? "";
|
|
249
|
+
for (const line of lines) {
|
|
250
|
+
if (!line.trim()) continue;
|
|
251
|
+
try {
|
|
252
|
+
callback(JSON.parse(line));
|
|
253
|
+
} catch {}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Wire an AbortSignal to kill a child process.
|
|
259
|
+
* Returns true if the signal was already aborted (caller should return early).
|
|
260
|
+
*/
|
|
261
|
+
function setupAbortHandler(signal, child, reject) {
|
|
262
|
+
if (!signal) return false;
|
|
263
|
+
const onAbort = () => {
|
|
264
|
+
child.kill("SIGTERM");
|
|
265
|
+
reject(/* @__PURE__ */ new Error("Agent was aborted"));
|
|
266
|
+
};
|
|
267
|
+
if (signal.aborted) {
|
|
268
|
+
onAbort();
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
272
|
+
child.on("close", () => signal.removeEventListener("abort", onAbort));
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/core/agents/claude.ts
|
|
161
277
|
var ClaudeAgent = class {
|
|
162
278
|
name = "claude";
|
|
163
279
|
run(prompt, cwd, options) {
|
|
@@ -171,7 +287,7 @@ var ClaudeAgent = class {
|
|
|
171
287
|
"--output-format",
|
|
172
288
|
"stream-json",
|
|
173
289
|
"--json-schema",
|
|
174
|
-
|
|
290
|
+
JSON.stringify(AGENT_OUTPUT_SCHEMA),
|
|
175
291
|
"--dangerously-skip-permissions"
|
|
176
292
|
], {
|
|
177
293
|
cwd,
|
|
@@ -182,20 +298,7 @@ var ClaudeAgent = class {
|
|
|
182
298
|
],
|
|
183
299
|
env: process.env
|
|
184
300
|
});
|
|
185
|
-
if (signal)
|
|
186
|
-
const onAbort = () => {
|
|
187
|
-
child.kill("SIGTERM");
|
|
188
|
-
reject(/* @__PURE__ */ new Error("Agent was aborted"));
|
|
189
|
-
};
|
|
190
|
-
if (signal.aborted) {
|
|
191
|
-
onAbort();
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
195
|
-
child.on("close", () => signal.removeEventListener("abort", onAbort));
|
|
196
|
-
}
|
|
197
|
-
let stderr = "";
|
|
198
|
-
let buffer = "";
|
|
301
|
+
if (setupAbortHandler(signal, child, reject)) return;
|
|
199
302
|
let resultEvent = null;
|
|
200
303
|
const cumulative = {
|
|
201
304
|
inputTokens: 0,
|
|
@@ -203,45 +306,24 @@ var ClaudeAgent = class {
|
|
|
203
306
|
cacheReadTokens: 0,
|
|
204
307
|
cacheCreationTokens: 0
|
|
205
308
|
};
|
|
206
|
-
child.stdout
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
cumulative.outputTokens += msg.usage.output_tokens ?? 0;
|
|
219
|
-
cumulative.cacheReadTokens += msg.usage.cache_read_input_tokens ?? 0;
|
|
220
|
-
cumulative.cacheCreationTokens += msg.usage.cache_creation_input_tokens ?? 0;
|
|
221
|
-
onUsage?.({ ...cumulative });
|
|
222
|
-
if (onMessage) {
|
|
223
|
-
const content = msg.content;
|
|
224
|
-
if (Array.isArray(content)) {
|
|
225
|
-
for (const block of content) if (block?.type === "text" && typeof block.text === "string" && block.text.trim()) onMessage(block.text.trim());
|
|
226
|
-
}
|
|
227
|
-
}
|
|
309
|
+
parseJSONLStream(child.stdout, logStream, (event) => {
|
|
310
|
+
if (event.type === "assistant") {
|
|
311
|
+
const msg = event.message;
|
|
312
|
+
cumulative.inputTokens += (msg.usage.input_tokens ?? 0) + (msg.usage.cache_read_input_tokens ?? 0);
|
|
313
|
+
cumulative.outputTokens += msg.usage.output_tokens ?? 0;
|
|
314
|
+
cumulative.cacheReadTokens += msg.usage.cache_read_input_tokens ?? 0;
|
|
315
|
+
cumulative.cacheCreationTokens += msg.usage.cache_creation_input_tokens ?? 0;
|
|
316
|
+
onUsage?.({ ...cumulative });
|
|
317
|
+
if (onMessage) {
|
|
318
|
+
const content = msg.content;
|
|
319
|
+
if (Array.isArray(content)) {
|
|
320
|
+
for (const block of content) if (block?.type === "text" && typeof block.text === "string" && block.text.trim()) onMessage(block.text.trim());
|
|
228
321
|
}
|
|
229
|
-
|
|
230
|
-
} catch {}
|
|
322
|
+
}
|
|
231
323
|
}
|
|
324
|
+
if (event.type === "result") resultEvent = event;
|
|
232
325
|
});
|
|
233
|
-
child
|
|
234
|
-
stderr += data.toString();
|
|
235
|
-
});
|
|
236
|
-
child.on("error", (err) => {
|
|
237
|
-
reject(/* @__PURE__ */ new Error(`Failed to spawn claude: ${err.message}`));
|
|
238
|
-
});
|
|
239
|
-
child.on("close", (code) => {
|
|
240
|
-
logStream?.end();
|
|
241
|
-
if (code !== 0) {
|
|
242
|
-
reject(/* @__PURE__ */ new Error(`claude exited with code ${code}: ${stderr}`));
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
326
|
+
setupChildProcessHandlers(child, "claude", logStream, reject, () => {
|
|
245
327
|
if (!resultEvent) {
|
|
246
328
|
reject(/* @__PURE__ */ new Error("claude returned no result event"));
|
|
247
329
|
return;
|
|
@@ -300,64 +382,28 @@ var CodexAgent = class {
|
|
|
300
382
|
],
|
|
301
383
|
env: process.env
|
|
302
384
|
});
|
|
303
|
-
if (signal)
|
|
304
|
-
const onAbort = () => {
|
|
305
|
-
child.kill("SIGTERM");
|
|
306
|
-
reject(/* @__PURE__ */ new Error("Agent was aborted"));
|
|
307
|
-
};
|
|
308
|
-
if (signal.aborted) {
|
|
309
|
-
onAbort();
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
313
|
-
child.on("close", () => signal.removeEventListener("abort", onAbort));
|
|
314
|
-
}
|
|
315
|
-
let stderr = "";
|
|
385
|
+
if (setupAbortHandler(signal, child, reject)) return;
|
|
316
386
|
let lastAgentMessage = null;
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const event = JSON.parse(line);
|
|
328
|
-
if (event.type === "item.completed" && "item" in event && event.item.type === "agent_message") {
|
|
329
|
-
lastAgentMessage = event.item.text;
|
|
330
|
-
onMessage?.(lastAgentMessage);
|
|
331
|
-
}
|
|
332
|
-
if (event.type === "turn.completed" && "usage" in event) {
|
|
333
|
-
usages.push(event.usage);
|
|
334
|
-
if (onUsage) onUsage(usages.reduce((acc, u) => ({
|
|
335
|
-
inputTokens: acc.inputTokens + (u.input_tokens ?? 0),
|
|
336
|
-
outputTokens: acc.outputTokens + (u.output_tokens ?? 0),
|
|
337
|
-
cacheReadTokens: acc.cacheReadTokens + (u.cached_input_tokens ?? 0),
|
|
338
|
-
cacheCreationTokens: 0
|
|
339
|
-
}), {
|
|
340
|
-
inputTokens: 0,
|
|
341
|
-
outputTokens: 0,
|
|
342
|
-
cacheReadTokens: 0,
|
|
343
|
-
cacheCreationTokens: 0
|
|
344
|
-
}));
|
|
345
|
-
}
|
|
346
|
-
} catch {}
|
|
387
|
+
const cumulative = {
|
|
388
|
+
inputTokens: 0,
|
|
389
|
+
outputTokens: 0,
|
|
390
|
+
cacheReadTokens: 0,
|
|
391
|
+
cacheCreationTokens: 0
|
|
392
|
+
};
|
|
393
|
+
parseJSONLStream(child.stdout, logStream, (event) => {
|
|
394
|
+
if (event.type === "item.completed" && "item" in event && event.item.type === "agent_message") {
|
|
395
|
+
lastAgentMessage = event.item.text;
|
|
396
|
+
onMessage?.(lastAgentMessage);
|
|
347
397
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
});
|
|
355
|
-
child.on("close", (code) => {
|
|
356
|
-
logStream?.end();
|
|
357
|
-
if (code !== 0) {
|
|
358
|
-
reject(/* @__PURE__ */ new Error(`codex exited with code ${code}: ${stderr}`));
|
|
359
|
-
return;
|
|
398
|
+
if (event.type === "turn.completed" && "usage" in event) {
|
|
399
|
+
const u = event.usage;
|
|
400
|
+
cumulative.inputTokens += u.input_tokens ?? 0;
|
|
401
|
+
cumulative.outputTokens += u.output_tokens ?? 0;
|
|
402
|
+
cumulative.cacheReadTokens += u.cached_input_tokens ?? 0;
|
|
403
|
+
onUsage?.({ ...cumulative });
|
|
360
404
|
}
|
|
405
|
+
});
|
|
406
|
+
setupChildProcessHandlers(child, "codex", logStream, reject, () => {
|
|
361
407
|
if (!lastAgentMessage) {
|
|
362
408
|
reject(/* @__PURE__ */ new Error("codex returned no agent message"));
|
|
363
409
|
return;
|
|
@@ -365,17 +411,7 @@ var CodexAgent = class {
|
|
|
365
411
|
try {
|
|
366
412
|
resolve({
|
|
367
413
|
output: JSON.parse(lastAgentMessage),
|
|
368
|
-
usage:
|
|
369
|
-
inputTokens: acc.inputTokens + (u.input_tokens ?? 0),
|
|
370
|
-
outputTokens: acc.outputTokens + (u.output_tokens ?? 0),
|
|
371
|
-
cacheReadTokens: acc.cacheReadTokens + (u.cached_input_tokens ?? 0),
|
|
372
|
-
cacheCreationTokens: acc.cacheCreationTokens
|
|
373
|
-
}), {
|
|
374
|
-
inputTokens: 0,
|
|
375
|
-
outputTokens: 0,
|
|
376
|
-
cacheReadTokens: 0,
|
|
377
|
-
cacheCreationTokens: 0
|
|
378
|
-
})
|
|
414
|
+
usage: cumulative
|
|
379
415
|
});
|
|
380
416
|
} catch (err) {
|
|
381
417
|
reject(/* @__PURE__ */ new Error(`Failed to parse codex output: ${err instanceof Error ? err.message : err}`));
|
|
@@ -432,6 +468,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
432
468
|
currentIteration: 0,
|
|
433
469
|
totalInputTokens: 0,
|
|
434
470
|
totalOutputTokens: 0,
|
|
471
|
+
commitCount: 0,
|
|
435
472
|
iterations: [],
|
|
436
473
|
successCount: 0,
|
|
437
474
|
failCount: 0,
|
|
@@ -448,6 +485,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
448
485
|
this.prompt = prompt;
|
|
449
486
|
this.cwd = cwd;
|
|
450
487
|
this.state.currentIteration = startIteration;
|
|
488
|
+
this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
|
|
451
489
|
}
|
|
452
490
|
getState() {
|
|
453
491
|
return { ...this.state };
|
|
@@ -474,69 +512,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
474
512
|
runId: this.runInfo.runId,
|
|
475
513
|
prompt: this.prompt
|
|
476
514
|
});
|
|
477
|
-
|
|
478
|
-
const baseInputTokens = this.state.totalInputTokens;
|
|
479
|
-
const baseOutputTokens = this.state.totalOutputTokens;
|
|
480
|
-
this.activeAbortController = new AbortController();
|
|
481
|
-
const onUsage = (usage) => {
|
|
482
|
-
this.state.totalInputTokens = baseInputTokens + usage.inputTokens;
|
|
483
|
-
this.state.totalOutputTokens = baseOutputTokens + usage.outputTokens;
|
|
484
|
-
this.emit("state", this.getState());
|
|
485
|
-
};
|
|
486
|
-
const onMessage = (text) => {
|
|
487
|
-
this.state.lastMessage = text;
|
|
488
|
-
this.emit("state", this.getState());
|
|
489
|
-
};
|
|
490
|
-
const logPath = join(this.runInfo.runDir, `iteration-${this.state.currentIteration}.jsonl`);
|
|
491
|
-
try {
|
|
492
|
-
const result = await this.agent.run(iterationPrompt, this.cwd, {
|
|
493
|
-
onUsage,
|
|
494
|
-
onMessage,
|
|
495
|
-
signal: this.activeAbortController.signal,
|
|
496
|
-
logPath
|
|
497
|
-
});
|
|
498
|
-
if (result.output.success) {
|
|
499
|
-
appendNotes(this.runInfo.notesPath, this.state.currentIteration, result.output.summary, result.output.key_changes_made, result.output.key_learnings);
|
|
500
|
-
commitAll(`gnhf #${this.state.currentIteration}: ${result.output.summary}`, this.cwd);
|
|
501
|
-
this.state.successCount++;
|
|
502
|
-
this.state.consecutiveFailures = 0;
|
|
503
|
-
record = {
|
|
504
|
-
number: this.state.currentIteration,
|
|
505
|
-
success: true,
|
|
506
|
-
summary: result.output.summary,
|
|
507
|
-
keyChanges: result.output.key_changes_made,
|
|
508
|
-
keyLearnings: result.output.key_learnings,
|
|
509
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
510
|
-
};
|
|
511
|
-
} else {
|
|
512
|
-
appendNotes(this.runInfo.notesPath, this.state.currentIteration, `[FAIL] ${result.output.summary}`, [], result.output.key_learnings);
|
|
513
|
-
resetHard(this.cwd);
|
|
514
|
-
this.state.failCount++;
|
|
515
|
-
this.state.consecutiveFailures++;
|
|
516
|
-
record = {
|
|
517
|
-
number: this.state.currentIteration,
|
|
518
|
-
success: false,
|
|
519
|
-
summary: result.output.summary,
|
|
520
|
-
keyChanges: [],
|
|
521
|
-
keyLearnings: result.output.key_learnings,
|
|
522
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
} catch (err) {
|
|
526
|
-
const summary = err instanceof Error ? err.message : String(err);
|
|
527
|
-
appendNotes(this.runInfo.notesPath, this.state.currentIteration, `[ERROR] ${summary}`, [], []);
|
|
528
|
-
resetHard(this.cwd);
|
|
529
|
-
this.state.failCount++;
|
|
530
|
-
this.state.consecutiveFailures++;
|
|
531
|
-
record = {
|
|
532
|
-
number: this.state.currentIteration,
|
|
533
|
-
success: false,
|
|
534
|
-
summary,
|
|
535
|
-
keyChanges: [],
|
|
536
|
-
keyLearnings: [],
|
|
537
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
538
|
-
};
|
|
539
|
-
}
|
|
515
|
+
const record = await this.runIteration(iterationPrompt);
|
|
540
516
|
this.state.iterations.push(record);
|
|
541
517
|
this.emit("iteration:end", record);
|
|
542
518
|
this.emit("state", this.getState());
|
|
@@ -561,6 +537,63 @@ var Orchestrator = class extends EventEmitter {
|
|
|
561
537
|
}
|
|
562
538
|
}
|
|
563
539
|
}
|
|
540
|
+
async runIteration(prompt) {
|
|
541
|
+
const baseInputTokens = this.state.totalInputTokens;
|
|
542
|
+
const baseOutputTokens = this.state.totalOutputTokens;
|
|
543
|
+
this.activeAbortController = new AbortController();
|
|
544
|
+
const onUsage = (usage) => {
|
|
545
|
+
this.state.totalInputTokens = baseInputTokens + usage.inputTokens;
|
|
546
|
+
this.state.totalOutputTokens = baseOutputTokens + usage.outputTokens;
|
|
547
|
+
this.emit("state", this.getState());
|
|
548
|
+
};
|
|
549
|
+
const onMessage = (text) => {
|
|
550
|
+
this.state.lastMessage = text;
|
|
551
|
+
this.emit("state", this.getState());
|
|
552
|
+
};
|
|
553
|
+
const logPath = join(this.runInfo.runDir, `iteration-${this.state.currentIteration}.jsonl`);
|
|
554
|
+
try {
|
|
555
|
+
const result = await this.agent.run(prompt, this.cwd, {
|
|
556
|
+
onUsage,
|
|
557
|
+
onMessage,
|
|
558
|
+
signal: this.activeAbortController.signal,
|
|
559
|
+
logPath
|
|
560
|
+
});
|
|
561
|
+
if (result.output.success) return this.recordSuccess(result.output);
|
|
562
|
+
return this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, result.output.key_learnings);
|
|
563
|
+
} catch (err) {
|
|
564
|
+
const summary = err instanceof Error ? err.message : String(err);
|
|
565
|
+
return this.recordFailure(`[ERROR] ${summary}`, summary, []);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
recordSuccess(output) {
|
|
569
|
+
appendNotes(this.runInfo.notesPath, this.state.currentIteration, output.summary, output.key_changes_made, output.key_learnings);
|
|
570
|
+
commitAll(`gnhf #${this.state.currentIteration}: ${output.summary}`, this.cwd);
|
|
571
|
+
this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
|
|
572
|
+
this.state.successCount++;
|
|
573
|
+
this.state.consecutiveFailures = 0;
|
|
574
|
+
return {
|
|
575
|
+
number: this.state.currentIteration,
|
|
576
|
+
success: true,
|
|
577
|
+
summary: output.summary,
|
|
578
|
+
keyChanges: output.key_changes_made,
|
|
579
|
+
keyLearnings: output.key_learnings,
|
|
580
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
recordFailure(notesSummary, recordSummary, learnings) {
|
|
584
|
+
appendNotes(this.runInfo.notesPath, this.state.currentIteration, notesSummary, [], learnings);
|
|
585
|
+
resetHard(this.cwd);
|
|
586
|
+
this.state.failCount++;
|
|
587
|
+
this.state.consecutiveFailures++;
|
|
588
|
+
return {
|
|
589
|
+
number: this.state.currentIteration,
|
|
590
|
+
success: false,
|
|
591
|
+
summary: recordSummary,
|
|
592
|
+
keyChanges: [],
|
|
593
|
+
keyLearnings: learnings,
|
|
594
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
595
|
+
};
|
|
596
|
+
}
|
|
564
597
|
interruptibleSleep(ms) {
|
|
565
598
|
return new Promise((resolve) => {
|
|
566
599
|
this.activeAbortController = new AbortController();
|
|
@@ -635,6 +668,7 @@ var MockOrchestrator = class extends EventEmitter {
|
|
|
635
668
|
currentIteration: 14,
|
|
636
669
|
totalInputTokens: 873e5,
|
|
637
670
|
totalOutputTokens: 86e4,
|
|
671
|
+
commitCount: 11,
|
|
638
672
|
iterations: [...MOCK_ITERATIONS],
|
|
639
673
|
successCount: 11,
|
|
640
674
|
failCount: 2,
|
|
@@ -725,14 +759,10 @@ function generateStarField(width, height, density, seed) {
|
|
|
725
759
|
function getStarState(star, now) {
|
|
726
760
|
const t = (now % star.period / star.period + star.phase / (Math.PI * 2)) % 1;
|
|
727
761
|
if (t > .05) return star.rest;
|
|
728
|
-
if (star.rest === "bright") {
|
|
762
|
+
if (star.rest === "bright" || star.rest === "hidden") {
|
|
763
|
+
const opposite = star.rest === "bright" ? "hidden" : "bright";
|
|
729
764
|
if (t > .0325) return "dim";
|
|
730
|
-
if (t > .0175) return
|
|
731
|
-
return "dim";
|
|
732
|
-
}
|
|
733
|
-
if (star.rest === "hidden") {
|
|
734
|
-
if (t > .0325) return "dim";
|
|
735
|
-
if (t > .0175) return "bright";
|
|
765
|
+
if (t > .0175) return opposite;
|
|
736
766
|
return "dim";
|
|
737
767
|
}
|
|
738
768
|
if (t > .025) return "bright";
|
|
@@ -906,6 +936,7 @@ const MOONS_PER_ROW = 30;
|
|
|
906
936
|
const MOON_PHASE_PERIOD = 1600;
|
|
907
937
|
const MAX_MSG_LINES = 3;
|
|
908
938
|
const MAX_MSG_LINE_LEN = 64;
|
|
939
|
+
const RESUME_HINT = "[ctrl+c to stop, gnhf again to resume]";
|
|
909
940
|
function renderTitleCells() {
|
|
910
941
|
return [
|
|
911
942
|
textToCells("g n h f", "dim"),
|
|
@@ -915,7 +946,8 @@ function renderTitleCells() {
|
|
|
915
946
|
textToCells("┗━┛┗━┛┗━┛╺┻┛ ╹ ╹╹┗━┛╹ ╹ ╹ ╹ ╹╹ ╹┗┛ ┗━╸ ╹ ┗━┛╹ ╹", "bold")
|
|
916
947
|
];
|
|
917
948
|
}
|
|
918
|
-
function renderStatsCells(elapsed, inputTokens, outputTokens) {
|
|
949
|
+
function renderStatsCells(elapsed, inputTokens, outputTokens, commitCount) {
|
|
950
|
+
const commitLabel = commitCount === 1 ? "commit" : "commits";
|
|
919
951
|
return [
|
|
920
952
|
...textToCells(elapsed, "bold"),
|
|
921
953
|
...textToCells(" ", "normal"),
|
|
@@ -925,7 +957,11 @@ function renderStatsCells(elapsed, inputTokens, outputTokens) {
|
|
|
925
957
|
...textToCells(" ", "normal"),
|
|
926
958
|
...textToCells("·", "dim"),
|
|
927
959
|
...textToCells(" ", "normal"),
|
|
928
|
-
...textToCells(`${formatTokens(outputTokens)} out`, "normal")
|
|
960
|
+
...textToCells(`${formatTokens(outputTokens)} out`, "normal"),
|
|
961
|
+
...textToCells(" ", "normal"),
|
|
962
|
+
...textToCells("·", "dim"),
|
|
963
|
+
...textToCells(" ", "normal"),
|
|
964
|
+
...textToCells(`${commitCount} ${commitLabel}`, "normal")
|
|
929
965
|
];
|
|
930
966
|
}
|
|
931
967
|
function renderAgentMessageCells(message, status) {
|
|
@@ -958,42 +994,31 @@ function starStyle(state) {
|
|
|
958
994
|
if (state === "dim") return "dim";
|
|
959
995
|
return "normal";
|
|
960
996
|
}
|
|
961
|
-
function
|
|
962
|
-
const cells = emptyCells(width);
|
|
997
|
+
function placeStarsInCells(cells, stars, row, xMin, xMax, xOffset, now) {
|
|
963
998
|
for (const star of stars) {
|
|
964
|
-
if (star.y !==
|
|
999
|
+
if (star.y !== row || star.x < xMin || star.x >= xMax) continue;
|
|
965
1000
|
const state = getStarState(star, now);
|
|
966
|
-
|
|
1001
|
+
const localX = star.x - xOffset;
|
|
1002
|
+
cells[localX] = state === "hidden" ? {
|
|
967
1003
|
char: " ",
|
|
968
1004
|
style: "normal",
|
|
969
1005
|
width: 1
|
|
970
|
-
}
|
|
971
|
-
else cells[star.x] = {
|
|
1006
|
+
} : {
|
|
972
1007
|
char: star.char,
|
|
973
1008
|
style: starStyle(state),
|
|
974
1009
|
width: 1
|
|
975
1010
|
};
|
|
976
1011
|
}
|
|
1012
|
+
}
|
|
1013
|
+
function renderStarLineCells(stars, width, y, now) {
|
|
1014
|
+
const cells = emptyCells(width);
|
|
1015
|
+
placeStarsInCells(cells, stars, y, 0, width, 0, now);
|
|
977
1016
|
return cells;
|
|
978
1017
|
}
|
|
979
1018
|
function renderSideStarsCells(stars, rowIndex, xOffset, sideWidth, now) {
|
|
980
1019
|
if (sideWidth <= 0) return [];
|
|
981
1020
|
const cells = emptyCells(sideWidth);
|
|
982
|
-
|
|
983
|
-
if (star.y !== rowIndex || star.x < xOffset || star.x >= xOffset + sideWidth) continue;
|
|
984
|
-
const localX = star.x - xOffset;
|
|
985
|
-
const state = getStarState(star, now);
|
|
986
|
-
if (state === "hidden") cells[localX] = {
|
|
987
|
-
char: " ",
|
|
988
|
-
style: "normal",
|
|
989
|
-
width: 1
|
|
990
|
-
};
|
|
991
|
-
else cells[localX] = {
|
|
992
|
-
char: star.char,
|
|
993
|
-
style: starStyle(state),
|
|
994
|
-
width: 1
|
|
995
|
-
};
|
|
996
|
-
}
|
|
1021
|
+
placeStarsInCells(cells, stars, rowIndex, xOffset, xOffset + sideWidth, xOffset, now);
|
|
997
1022
|
return cells;
|
|
998
1023
|
}
|
|
999
1024
|
function centerLineCells(content, width) {
|
|
@@ -1006,6 +1031,19 @@ function centerLineCells(content, width) {
|
|
|
1006
1031
|
...emptyCells(rightPad)
|
|
1007
1032
|
];
|
|
1008
1033
|
}
|
|
1034
|
+
function renderResumeHintCells(width) {
|
|
1035
|
+
return centerLineCells(textToCells(RESUME_HINT, "dim"), width);
|
|
1036
|
+
}
|
|
1037
|
+
function fitContentRows(contentRows, maxRows) {
|
|
1038
|
+
if (contentRows.length <= maxRows) return contentRows;
|
|
1039
|
+
const fitted = [...contentRows];
|
|
1040
|
+
while (fitted.length > maxRows) {
|
|
1041
|
+
const emptyRowIndex = fitted.findIndex((row) => row.length === 0);
|
|
1042
|
+
if (emptyRowIndex === -1) break;
|
|
1043
|
+
fitted.splice(emptyRowIndex, 1);
|
|
1044
|
+
}
|
|
1045
|
+
return fitted.length > maxRows ? fitted.slice(fitted.length - maxRows) : fitted;
|
|
1046
|
+
}
|
|
1009
1047
|
function buildContentCells(prompt, state, elapsed, now) {
|
|
1010
1048
|
const rows = [];
|
|
1011
1049
|
const isRunning = state.status === "running" || state.status === "waiting";
|
|
@@ -1018,7 +1056,7 @@ function buildContentCells(prompt, state, elapsed, now) {
|
|
|
1018
1056
|
rows.push(pl ? textToCells(pl, "dim") : []);
|
|
1019
1057
|
}
|
|
1020
1058
|
rows.push([], []);
|
|
1021
|
-
rows.push(renderStatsCells(elapsed, state.totalInputTokens, state.totalOutputTokens));
|
|
1059
|
+
rows.push(renderStatsCells(elapsed, state.totalInputTokens, state.totalOutputTokens, state.commitCount));
|
|
1022
1060
|
rows.push([], []);
|
|
1023
1061
|
rows.push(...renderAgentMessageCells(state.lastMessage, state.status));
|
|
1024
1062
|
rows.push([], []);
|
|
@@ -1026,11 +1064,13 @@ function buildContentCells(prompt, state, elapsed, now) {
|
|
|
1026
1064
|
return rows;
|
|
1027
1065
|
}
|
|
1028
1066
|
function buildFrameCells(prompt, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight) {
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1067
|
+
const elapsed = formatElapsed(now - state.startTime.getTime());
|
|
1068
|
+
const availableHeight = Math.max(0, terminalHeight - 2);
|
|
1069
|
+
const contentRows = fitContentRows(buildContentCells(prompt, state, elapsed, now), availableHeight);
|
|
1070
|
+
while (contentRows.length < Math.min(BASE_CONTENT_ROWS, availableHeight)) contentRows.push([]);
|
|
1031
1071
|
const contentCount = contentRows.length;
|
|
1032
|
-
const remaining = Math.max(0,
|
|
1033
|
-
const topHeight = Math.ceil(remaining / 2)
|
|
1072
|
+
const remaining = Math.max(0, availableHeight - contentCount);
|
|
1073
|
+
const topHeight = Math.max(0, Math.ceil(remaining / 2));
|
|
1034
1074
|
const bottomHeight = remaining - topHeight;
|
|
1035
1075
|
const sideWidth = Math.max(0, Math.floor((terminalWidth - CONTENT_WIDTH) / 2));
|
|
1036
1076
|
const frame = [];
|
|
@@ -1046,6 +1086,8 @@ function buildFrameCells(prompt, state, topStars, bottomStars, sideStars, now, t
|
|
|
1046
1086
|
]);
|
|
1047
1087
|
}
|
|
1048
1088
|
for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, terminalWidth, y, now));
|
|
1089
|
+
frame.push(renderResumeHintCells(terminalWidth));
|
|
1090
|
+
frame.push(emptyCells(terminalWidth));
|
|
1049
1091
|
return frame;
|
|
1050
1092
|
}
|
|
1051
1093
|
var Renderer = class {
|
|
@@ -1111,8 +1153,9 @@ var Renderer = class {
|
|
|
1111
1153
|
this.cachedHeight = h;
|
|
1112
1154
|
const contentStart = Math.max(0, Math.floor((w - CONTENT_WIDTH) / 2) - 8);
|
|
1113
1155
|
const contentEnd = contentStart + CONTENT_WIDTH + 16;
|
|
1114
|
-
const
|
|
1115
|
-
const
|
|
1156
|
+
const availableHeight = Math.max(0, h - 2);
|
|
1157
|
+
const remaining = Math.max(0, availableHeight - BASE_CONTENT_ROWS);
|
|
1158
|
+
const topHeight = Math.max(0, Math.ceil(remaining / 2));
|
|
1116
1159
|
const proximityRows = 8;
|
|
1117
1160
|
const shrinkBig = (s, nearContentRow) => {
|
|
1118
1161
|
if (!nearContentRow || s.x < contentStart || s.x >= contentEnd) return s;
|
|
@@ -1127,7 +1170,7 @@ var Renderer = class {
|
|
|
1127
1170
|
};
|
|
1128
1171
|
this.topStars = generateStarField(w, h, STAR_DENSITY, 42).map((s) => shrinkBig(s, s.y >= topHeight - proximityRows));
|
|
1129
1172
|
this.bottomStars = generateStarField(w, h, STAR_DENSITY, 137).map((s) => shrinkBig(s, s.y < proximityRows));
|
|
1130
|
-
this.sideStars = generateStarField(w, BASE_CONTENT_ROWS, STAR_DENSITY, 99);
|
|
1173
|
+
this.sideStars = generateStarField(w, Math.max(BASE_CONTENT_ROWS, availableHeight), STAR_DENSITY, 99);
|
|
1131
1174
|
return true;
|
|
1132
1175
|
}
|
|
1133
1176
|
return false;
|
|
@@ -1156,6 +1199,14 @@ function slugifyPrompt(prompt) {
|
|
|
1156
1199
|
//#endregion
|
|
1157
1200
|
//#region src/cli.ts
|
|
1158
1201
|
const packageVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
1202
|
+
function initializeNewBranch(prompt, cwd) {
|
|
1203
|
+
ensureCleanWorkingTree(cwd);
|
|
1204
|
+
const baseCommit = getHeadCommit(cwd);
|
|
1205
|
+
const branchName = slugifyPrompt(prompt);
|
|
1206
|
+
createBranch(branchName, cwd);
|
|
1207
|
+
const runId = branchName.split("/")[1];
|
|
1208
|
+
return setupRun(runId, prompt, baseCommit, cwd);
|
|
1209
|
+
}
|
|
1159
1210
|
function ask(question) {
|
|
1160
1211
|
const rl = createInterface({
|
|
1161
1212
|
input: process$1.stdin,
|
|
@@ -1169,7 +1220,7 @@ function ask(question) {
|
|
|
1169
1220
|
});
|
|
1170
1221
|
}
|
|
1171
1222
|
const program = new Command();
|
|
1172
|
-
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 or codex)"
|
|
1223
|
+
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 or codex)").option("--mock", "", false).action(async (promptArg, options) => {
|
|
1173
1224
|
if (options.mock) {
|
|
1174
1225
|
const mock = new MockOrchestrator();
|
|
1175
1226
|
enterAltScreen();
|
|
@@ -1183,11 +1234,15 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
1183
1234
|
let prompt = promptArg;
|
|
1184
1235
|
if (!prompt && !process$1.stdin.isTTY) prompt = readFileSync("/dev/stdin", "utf-8").trim();
|
|
1185
1236
|
const agentName = options.agent;
|
|
1186
|
-
if (agentName !== "claude" && agentName !== "codex") {
|
|
1237
|
+
if (agentName !== void 0 && agentName !== "claude" && agentName !== "codex") {
|
|
1187
1238
|
console.error(`Unknown agent: ${options.agent}. Use "claude" or "codex".`);
|
|
1188
1239
|
process$1.exit(1);
|
|
1189
1240
|
}
|
|
1190
|
-
const config = loadConfig({ agent: agentName });
|
|
1241
|
+
const config = loadConfig(agentName ? { agent: agentName } : void 0);
|
|
1242
|
+
if (config.agent !== "claude" && config.agent !== "codex") {
|
|
1243
|
+
console.error(`Unknown agent: ${config.agent}. Use "claude" or "codex".`);
|
|
1244
|
+
process$1.exit(1);
|
|
1245
|
+
}
|
|
1191
1246
|
const cwd = process$1.cwd();
|
|
1192
1247
|
const currentBranch = getCurrentBranch(cwd);
|
|
1193
1248
|
const onGnhfBranch = currentBranch.startsWith("gnhf/");
|
|
@@ -1205,30 +1260,18 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
1205
1260
|
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]: `);
|
|
1206
1261
|
if (answer === "o") {
|
|
1207
1262
|
ensureCleanWorkingTree(cwd);
|
|
1208
|
-
runInfo = setupRun(existingRunId, prompt, cwd);
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
ensureCleanWorkingTree(cwd);
|
|
1212
|
-
const branchName = slugifyPrompt(prompt);
|
|
1213
|
-
createBranch(branchName, cwd);
|
|
1214
|
-
const runId = branchName.split("/")[1];
|
|
1215
|
-
runInfo = setupRun(runId, prompt, cwd);
|
|
1216
|
-
commitAll(`gnhf: initialize run ${runId}`, cwd);
|
|
1217
|
-
} else process$1.exit(0);
|
|
1263
|
+
runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd);
|
|
1264
|
+
} else if (answer === "n") runInfo = initializeNewBranch(prompt, cwd);
|
|
1265
|
+
else process$1.exit(0);
|
|
1218
1266
|
}
|
|
1219
1267
|
} else {
|
|
1220
1268
|
if (!prompt) {
|
|
1221
1269
|
program.help();
|
|
1222
1270
|
return;
|
|
1223
1271
|
}
|
|
1224
|
-
|
|
1225
|
-
const branchName = slugifyPrompt(prompt);
|
|
1226
|
-
createBranch(branchName, cwd);
|
|
1227
|
-
const runId = branchName.split("/")[1];
|
|
1228
|
-
runInfo = setupRun(runId, prompt, cwd);
|
|
1229
|
-
commitAll(`gnhf: initialize run ${runId}`, cwd);
|
|
1272
|
+
runInfo = initializeNewBranch(prompt, cwd);
|
|
1230
1273
|
}
|
|
1231
|
-
const orchestrator = new Orchestrator(config, createAgent(
|
|
1274
|
+
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo), runInfo, prompt, cwd, startIteration);
|
|
1232
1275
|
enterAltScreen();
|
|
1233
1276
|
const renderer = new Renderer(orchestrator, prompt);
|
|
1234
1277
|
renderer.start();
|