gnhf 0.1.3 → 0.1.5
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 +10 -7
- package/dist/cli.mjs +368 -289
- 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
|
|
@@ -55,6 +53,8 @@ $ gnhf "reduce complexity of the codebase without changing functionality"
|
|
|
55
53
|
# go to sleep
|
|
56
54
|
```
|
|
57
55
|
|
|
56
|
+
Run `gnhf` from inside a Git repository with a clean working tree. If you are starting from a plain directory, run `git init` first.
|
|
57
|
+
|
|
58
58
|
## Install
|
|
59
59
|
|
|
60
60
|
**npm**
|
|
@@ -119,6 +119,7 @@ npm link
|
|
|
119
119
|
|
|
120
120
|
- **Incremental commits** — each successful iteration is a separate git commit, so you can cherry-pick or revert individual changes
|
|
121
121
|
- **Shared memory** — the agent reads `notes.md` (built up from prior iterations) to communicate across iterations
|
|
122
|
+
- **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
123
|
- **Resume support** — run `gnhf` while on an existing `gnhf/` branch to pick up where a previous run left off
|
|
123
124
|
|
|
124
125
|
## CLI Reference
|
|
@@ -132,10 +133,10 @@ npm link
|
|
|
132
133
|
|
|
133
134
|
### Flags
|
|
134
135
|
|
|
135
|
-
| Flag | Description | Default
|
|
136
|
-
| ----------------- | ---------------------------------- |
|
|
137
|
-
| `--agent <agent>` | Agent to use (`claude` or `codex`) | `claude` |
|
|
138
|
-
| `--version` | Show version |
|
|
136
|
+
| Flag | Description | Default |
|
|
137
|
+
| ----------------- | ---------------------------------- | ---------------------- |
|
|
138
|
+
| `--agent <agent>` | Agent to use (`claude` or `codex`) | config file (`claude`) |
|
|
139
|
+
| `--version` | Show version | |
|
|
139
140
|
|
|
140
141
|
## Configuration
|
|
141
142
|
|
|
@@ -149,6 +150,8 @@ agent: claude
|
|
|
149
150
|
maxConsecutiveFailures: 3
|
|
150
151
|
```
|
|
151
152
|
|
|
153
|
+
If the file does not exist yet, `gnhf` creates it on first run using the resolved defaults.
|
|
154
|
+
|
|
152
155
|
CLI flags override config file values.
|
|
153
156
|
|
|
154
157
|
## 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,30 +14,83 @@ 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
|
|
53
|
+
const NOT_GIT_REPOSITORY_MESSAGE = "This command must be run inside a Git repository. Change into a repo or run \"git init\" first.";
|
|
54
|
+
function translateGitError(error) {
|
|
55
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
56
|
+
}
|
|
32
57
|
function git(args, cwd) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
58
|
+
try {
|
|
59
|
+
return execSync(`git ${args}`, {
|
|
60
|
+
cwd,
|
|
61
|
+
encoding: "utf-8",
|
|
62
|
+
stdio: "pipe"
|
|
63
|
+
}).trim();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw translateGitError(error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function isGitRepository(cwd) {
|
|
69
|
+
try {
|
|
70
|
+
execSync("git rev-parse --git-dir", {
|
|
71
|
+
cwd,
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
stdio: "pipe",
|
|
74
|
+
env: {
|
|
75
|
+
...process.env,
|
|
76
|
+
LC_ALL: "C"
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function ensureGitRepository(cwd) {
|
|
85
|
+
if (!isGitRepository(cwd)) throw new Error(NOT_GIT_REPOSITORY_MESSAGE);
|
|
38
86
|
}
|
|
39
87
|
function getCurrentBranch(cwd) {
|
|
40
|
-
|
|
88
|
+
ensureGitRepository(cwd);
|
|
89
|
+
try {
|
|
90
|
+
return git("symbolic-ref --short HEAD", cwd);
|
|
91
|
+
} catch {
|
|
92
|
+
return git("rev-parse --abbrev-ref HEAD", cwd);
|
|
93
|
+
}
|
|
41
94
|
}
|
|
42
95
|
function ensureCleanWorkingTree(cwd) {
|
|
43
96
|
if (git("status --porcelain", cwd)) throw new Error("Working tree is not clean. Commit or stash changes first.");
|
|
@@ -45,6 +98,28 @@ function ensureCleanWorkingTree(cwd) {
|
|
|
45
98
|
function createBranch(branchName, cwd) {
|
|
46
99
|
git(`checkout -b ${branchName}`, cwd);
|
|
47
100
|
}
|
|
101
|
+
function getHeadCommit(cwd) {
|
|
102
|
+
return git("rev-parse HEAD", cwd);
|
|
103
|
+
}
|
|
104
|
+
function findLegacyRunBaseCommit(runId, cwd) {
|
|
105
|
+
try {
|
|
106
|
+
const marker = git("log --first-parent --reverse --format=%H%x09%s HEAD", cwd).split("\n").map((line) => {
|
|
107
|
+
const [sha, ...subjectParts] = line.split(" ");
|
|
108
|
+
return {
|
|
109
|
+
sha,
|
|
110
|
+
subject: subjectParts.join(" ")
|
|
111
|
+
};
|
|
112
|
+
}).find(({ subject }) => subject === `gnhf: initialize run ${runId}` || subject === `gnhf: overwrite run ${runId}`);
|
|
113
|
+
if (!marker?.sha) return null;
|
|
114
|
+
return git(`rev-parse ${marker.sha}^`, cwd);
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function getBranchCommitCount(baseCommit, cwd) {
|
|
120
|
+
if (!baseCommit) return 0;
|
|
121
|
+
return Number.parseInt(git(`rev-list --count --first-parent ${baseCommit}..HEAD`, cwd), 10);
|
|
122
|
+
}
|
|
48
123
|
function commitAll(message, cwd) {
|
|
49
124
|
git("add -A", cwd);
|
|
50
125
|
try {
|
|
@@ -56,8 +131,8 @@ function resetHard(cwd) {
|
|
|
56
131
|
git("clean -fd", cwd);
|
|
57
132
|
}
|
|
58
133
|
//#endregion
|
|
59
|
-
//#region src/core/
|
|
60
|
-
const
|
|
134
|
+
//#region src/core/agents/types.ts
|
|
135
|
+
const AGENT_OUTPUT_SCHEMA = {
|
|
61
136
|
type: "object",
|
|
62
137
|
properties: {
|
|
63
138
|
success: { type: "boolean" },
|
|
@@ -77,17 +152,29 @@ const OUTPUT_SCHEMA$1 = JSON.stringify({
|
|
|
77
152
|
"key_changes_made",
|
|
78
153
|
"key_learnings"
|
|
79
154
|
]
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
155
|
+
};
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/core/run.ts
|
|
158
|
+
function ensureRunMetadataIgnored(cwd) {
|
|
159
|
+
const excludePath = execFileSync("git", [
|
|
160
|
+
"rev-parse",
|
|
161
|
+
"--git-path",
|
|
162
|
+
"info/exclude"
|
|
163
|
+
], {
|
|
164
|
+
cwd,
|
|
165
|
+
encoding: "utf-8"
|
|
166
|
+
}).trim();
|
|
167
|
+
const resolved = isAbsolute(excludePath) ? excludePath : join(cwd, excludePath);
|
|
83
168
|
const entry = ".gnhf/runs/";
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
169
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
170
|
+
if (existsSync(resolved)) {
|
|
171
|
+
const content = readFileSync(resolved, "utf-8");
|
|
172
|
+
if (content.split("\n").some((line) => line.trim() === entry)) return;
|
|
173
|
+
appendFileSync(resolved, `${content.length > 0 && !content.endsWith("\n") ? "\n" : ""}${entry}\n`, "utf-8");
|
|
174
|
+
} else writeFileSync(resolved, `${entry}\n`, "utf-8");
|
|
88
175
|
}
|
|
89
|
-
function setupRun(runId, prompt, cwd) {
|
|
90
|
-
|
|
176
|
+
function setupRun(runId, prompt, baseCommit, cwd) {
|
|
177
|
+
ensureRunMetadataIgnored(cwd);
|
|
91
178
|
const runDir = join(cwd, ".gnhf", "runs", runId);
|
|
92
179
|
mkdirSync(runDir, { recursive: true });
|
|
93
180
|
const promptPath = join(runDir, "prompt.md");
|
|
@@ -95,26 +182,43 @@ function setupRun(runId, prompt, cwd) {
|
|
|
95
182
|
const notesPath = join(runDir, "notes.md");
|
|
96
183
|
writeFileSync(notesPath, `# gnhf run: ${runId}\n\nObjective: ${prompt}\n\n## Iteration Log\n`, "utf-8");
|
|
97
184
|
const schemaPath = join(runDir, "output-schema.json");
|
|
98
|
-
writeFileSync(schemaPath,
|
|
185
|
+
writeFileSync(schemaPath, JSON.stringify(AGENT_OUTPUT_SCHEMA, null, 2), "utf-8");
|
|
186
|
+
const baseCommitPath = join(runDir, "base-commit");
|
|
187
|
+
const hasStoredBaseCommit = existsSync(baseCommitPath);
|
|
188
|
+
const resolvedBaseCommit = hasStoredBaseCommit ? readFileSync(baseCommitPath, "utf-8").trim() : baseCommit;
|
|
189
|
+
if (!hasStoredBaseCommit) writeFileSync(baseCommitPath, `${baseCommit}\n`, "utf-8");
|
|
99
190
|
return {
|
|
100
191
|
runId,
|
|
101
192
|
runDir,
|
|
102
193
|
promptPath,
|
|
103
194
|
notesPath,
|
|
104
|
-
schemaPath
|
|
195
|
+
schemaPath,
|
|
196
|
+
baseCommit: resolvedBaseCommit,
|
|
197
|
+
baseCommitPath
|
|
105
198
|
};
|
|
106
199
|
}
|
|
107
200
|
function resumeRun(runId, cwd) {
|
|
108
201
|
const runDir = join(cwd, ".gnhf", "runs", runId);
|
|
109
202
|
if (!existsSync(runDir)) throw new Error(`Run directory not found: ${runDir}`);
|
|
203
|
+
const promptPath = join(runDir, "prompt.md");
|
|
204
|
+
const notesPath = join(runDir, "notes.md");
|
|
205
|
+
const schemaPath = join(runDir, "output-schema.json");
|
|
206
|
+
const baseCommitPath = join(runDir, "base-commit");
|
|
110
207
|
return {
|
|
111
208
|
runId,
|
|
112
209
|
runDir,
|
|
113
|
-
promptPath
|
|
114
|
-
notesPath
|
|
115
|
-
schemaPath
|
|
210
|
+
promptPath,
|
|
211
|
+
notesPath,
|
|
212
|
+
schemaPath,
|
|
213
|
+
baseCommit: existsSync(baseCommitPath) ? readFileSync(baseCommitPath, "utf-8").trim() : backfillLegacyBaseCommit(runId, baseCommitPath, cwd),
|
|
214
|
+
baseCommitPath
|
|
116
215
|
};
|
|
117
216
|
}
|
|
217
|
+
function backfillLegacyBaseCommit(runId, baseCommitPath, cwd) {
|
|
218
|
+
const baseCommit = findLegacyRunBaseCommit(runId, cwd) ?? getHeadCommit(cwd);
|
|
219
|
+
writeFileSync(baseCommitPath, `${baseCommit}\n`, "utf-8");
|
|
220
|
+
return baseCommit;
|
|
221
|
+
}
|
|
118
222
|
function getLastIterationNumber(runInfo) {
|
|
119
223
|
const files = readdirSync(runInfo.runDir);
|
|
120
224
|
let max = 0;
|
|
@@ -127,37 +231,81 @@ function getLastIterationNumber(runInfo) {
|
|
|
127
231
|
}
|
|
128
232
|
return max;
|
|
129
233
|
}
|
|
234
|
+
function formatListSection(title, items) {
|
|
235
|
+
if (items.length === 0) return "";
|
|
236
|
+
return `**${title}:**\n${items.map((item) => `- ${item}`).join("\n")}\n`;
|
|
237
|
+
}
|
|
130
238
|
function appendNotes(notesPath, iteration, summary, changes, learnings) {
|
|
131
239
|
appendFileSync(notesPath, [
|
|
132
240
|
`\n### Iteration ${iteration}\n`,
|
|
133
241
|
`**Summary:** ${summary}\n`,
|
|
134
|
-
|
|
135
|
-
|
|
242
|
+
formatListSection("Changes", changes),
|
|
243
|
+
formatListSection("Learnings", learnings)
|
|
136
244
|
].join("\n"), "utf-8");
|
|
137
245
|
}
|
|
138
246
|
//#endregion
|
|
139
|
-
//#region src/core/agents/
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
247
|
+
//#region src/core/agents/stream-utils.ts
|
|
248
|
+
/**
|
|
249
|
+
* Wire stderr collection, spawn-error handling, and the common close-handler
|
|
250
|
+
* prefix (logStream.end + non-zero exit code rejection) for a child process.
|
|
251
|
+
* Calls `onSuccess` only when the process exits with code 0.
|
|
252
|
+
*/
|
|
253
|
+
function setupChildProcessHandlers(child, agentName, logStream, reject, onSuccess) {
|
|
254
|
+
let stderr = "";
|
|
255
|
+
child.stderr.on("data", (data) => {
|
|
256
|
+
stderr += data.toString();
|
|
257
|
+
});
|
|
258
|
+
child.on("error", (err) => {
|
|
259
|
+
reject(/* @__PURE__ */ new Error(`Failed to spawn ${agentName}: ${err.message}`));
|
|
260
|
+
});
|
|
261
|
+
child.on("close", (code) => {
|
|
262
|
+
logStream?.end();
|
|
263
|
+
if (code !== 0) {
|
|
264
|
+
reject(/* @__PURE__ */ new Error(`${agentName} exited with code ${code}: ${stderr}`));
|
|
265
|
+
return;
|
|
152
266
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
267
|
+
onSuccess();
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Parse a JSONL stream, calling the callback for each parsed event.
|
|
272
|
+
* Handles buffering of incomplete lines and skips unparseable lines.
|
|
273
|
+
*/
|
|
274
|
+
function parseJSONLStream(stream, logStream, callback) {
|
|
275
|
+
let buffer = "";
|
|
276
|
+
stream.on("data", (data) => {
|
|
277
|
+
logStream?.write(data);
|
|
278
|
+
buffer += data.toString();
|
|
279
|
+
const lines = buffer.split("\n");
|
|
280
|
+
buffer = lines.pop() ?? "";
|
|
281
|
+
for (const line of lines) {
|
|
282
|
+
if (!line.trim()) continue;
|
|
283
|
+
try {
|
|
284
|
+
callback(JSON.parse(line));
|
|
285
|
+
} catch {}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Wire an AbortSignal to kill a child process.
|
|
291
|
+
* Returns true if the signal was already aborted (caller should return early).
|
|
292
|
+
*/
|
|
293
|
+
function setupAbortHandler(signal, child, reject) {
|
|
294
|
+
if (!signal) return false;
|
|
295
|
+
const onAbort = () => {
|
|
296
|
+
child.kill("SIGTERM");
|
|
297
|
+
reject(/* @__PURE__ */ new Error("Agent was aborted"));
|
|
298
|
+
};
|
|
299
|
+
if (signal.aborted) {
|
|
300
|
+
onAbort();
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
304
|
+
child.on("close", () => signal.removeEventListener("abort", onAbort));
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/core/agents/claude.ts
|
|
161
309
|
var ClaudeAgent = class {
|
|
162
310
|
name = "claude";
|
|
163
311
|
run(prompt, cwd, options) {
|
|
@@ -171,7 +319,7 @@ var ClaudeAgent = class {
|
|
|
171
319
|
"--output-format",
|
|
172
320
|
"stream-json",
|
|
173
321
|
"--json-schema",
|
|
174
|
-
|
|
322
|
+
JSON.stringify(AGENT_OUTPUT_SCHEMA),
|
|
175
323
|
"--dangerously-skip-permissions"
|
|
176
324
|
], {
|
|
177
325
|
cwd,
|
|
@@ -182,20 +330,7 @@ var ClaudeAgent = class {
|
|
|
182
330
|
],
|
|
183
331
|
env: process.env
|
|
184
332
|
});
|
|
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 = "";
|
|
333
|
+
if (setupAbortHandler(signal, child, reject)) return;
|
|
199
334
|
let resultEvent = null;
|
|
200
335
|
const cumulative = {
|
|
201
336
|
inputTokens: 0,
|
|
@@ -203,45 +338,24 @@ var ClaudeAgent = class {
|
|
|
203
338
|
cacheReadTokens: 0,
|
|
204
339
|
cacheCreationTokens: 0
|
|
205
340
|
};
|
|
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
|
-
}
|
|
341
|
+
parseJSONLStream(child.stdout, logStream, (event) => {
|
|
342
|
+
if (event.type === "assistant") {
|
|
343
|
+
const msg = event.message;
|
|
344
|
+
cumulative.inputTokens += (msg.usage.input_tokens ?? 0) + (msg.usage.cache_read_input_tokens ?? 0);
|
|
345
|
+
cumulative.outputTokens += msg.usage.output_tokens ?? 0;
|
|
346
|
+
cumulative.cacheReadTokens += msg.usage.cache_read_input_tokens ?? 0;
|
|
347
|
+
cumulative.cacheCreationTokens += msg.usage.cache_creation_input_tokens ?? 0;
|
|
348
|
+
onUsage?.({ ...cumulative });
|
|
349
|
+
if (onMessage) {
|
|
350
|
+
const content = msg.content;
|
|
351
|
+
if (Array.isArray(content)) {
|
|
352
|
+
for (const block of content) if (block?.type === "text" && typeof block.text === "string" && block.text.trim()) onMessage(block.text.trim());
|
|
228
353
|
}
|
|
229
|
-
|
|
230
|
-
} catch {}
|
|
354
|
+
}
|
|
231
355
|
}
|
|
356
|
+
if (event.type === "result") resultEvent = event;
|
|
232
357
|
});
|
|
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
|
-
}
|
|
358
|
+
setupChildProcessHandlers(child, "claude", logStream, reject, () => {
|
|
245
359
|
if (!resultEvent) {
|
|
246
360
|
reject(/* @__PURE__ */ new Error("claude returned no result event"));
|
|
247
361
|
return;
|
|
@@ -300,64 +414,28 @@ var CodexAgent = class {
|
|
|
300
414
|
],
|
|
301
415
|
env: process.env
|
|
302
416
|
});
|
|
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 = "";
|
|
417
|
+
if (setupAbortHandler(signal, child, reject)) return;
|
|
316
418
|
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 {}
|
|
419
|
+
const cumulative = {
|
|
420
|
+
inputTokens: 0,
|
|
421
|
+
outputTokens: 0,
|
|
422
|
+
cacheReadTokens: 0,
|
|
423
|
+
cacheCreationTokens: 0
|
|
424
|
+
};
|
|
425
|
+
parseJSONLStream(child.stdout, logStream, (event) => {
|
|
426
|
+
if (event.type === "item.completed" && "item" in event && event.item.type === "agent_message") {
|
|
427
|
+
lastAgentMessage = event.item.text;
|
|
428
|
+
onMessage?.(lastAgentMessage);
|
|
347
429
|
}
|
|
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;
|
|
430
|
+
if (event.type === "turn.completed" && "usage" in event) {
|
|
431
|
+
const u = event.usage;
|
|
432
|
+
cumulative.inputTokens += u.input_tokens ?? 0;
|
|
433
|
+
cumulative.outputTokens += u.output_tokens ?? 0;
|
|
434
|
+
cumulative.cacheReadTokens += u.cached_input_tokens ?? 0;
|
|
435
|
+
onUsage?.({ ...cumulative });
|
|
360
436
|
}
|
|
437
|
+
});
|
|
438
|
+
setupChildProcessHandlers(child, "codex", logStream, reject, () => {
|
|
361
439
|
if (!lastAgentMessage) {
|
|
362
440
|
reject(/* @__PURE__ */ new Error("codex returned no agent message"));
|
|
363
441
|
return;
|
|
@@ -365,17 +443,7 @@ var CodexAgent = class {
|
|
|
365
443
|
try {
|
|
366
444
|
resolve({
|
|
367
445
|
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
|
-
})
|
|
446
|
+
usage: cumulative
|
|
379
447
|
});
|
|
380
448
|
} catch (err) {
|
|
381
449
|
reject(/* @__PURE__ */ new Error(`Failed to parse codex output: ${err instanceof Error ? err.message : err}`));
|
|
@@ -432,6 +500,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
432
500
|
currentIteration: 0,
|
|
433
501
|
totalInputTokens: 0,
|
|
434
502
|
totalOutputTokens: 0,
|
|
503
|
+
commitCount: 0,
|
|
435
504
|
iterations: [],
|
|
436
505
|
successCount: 0,
|
|
437
506
|
failCount: 0,
|
|
@@ -448,6 +517,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
448
517
|
this.prompt = prompt;
|
|
449
518
|
this.cwd = cwd;
|
|
450
519
|
this.state.currentIteration = startIteration;
|
|
520
|
+
this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
|
|
451
521
|
}
|
|
452
522
|
getState() {
|
|
453
523
|
return { ...this.state };
|
|
@@ -474,69 +544,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
474
544
|
runId: this.runInfo.runId,
|
|
475
545
|
prompt: this.prompt
|
|
476
546
|
});
|
|
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
|
-
}
|
|
547
|
+
const record = await this.runIteration(iterationPrompt);
|
|
540
548
|
this.state.iterations.push(record);
|
|
541
549
|
this.emit("iteration:end", record);
|
|
542
550
|
this.emit("state", this.getState());
|
|
@@ -561,6 +569,63 @@ var Orchestrator = class extends EventEmitter {
|
|
|
561
569
|
}
|
|
562
570
|
}
|
|
563
571
|
}
|
|
572
|
+
async runIteration(prompt) {
|
|
573
|
+
const baseInputTokens = this.state.totalInputTokens;
|
|
574
|
+
const baseOutputTokens = this.state.totalOutputTokens;
|
|
575
|
+
this.activeAbortController = new AbortController();
|
|
576
|
+
const onUsage = (usage) => {
|
|
577
|
+
this.state.totalInputTokens = baseInputTokens + usage.inputTokens;
|
|
578
|
+
this.state.totalOutputTokens = baseOutputTokens + usage.outputTokens;
|
|
579
|
+
this.emit("state", this.getState());
|
|
580
|
+
};
|
|
581
|
+
const onMessage = (text) => {
|
|
582
|
+
this.state.lastMessage = text;
|
|
583
|
+
this.emit("state", this.getState());
|
|
584
|
+
};
|
|
585
|
+
const logPath = join(this.runInfo.runDir, `iteration-${this.state.currentIteration}.jsonl`);
|
|
586
|
+
try {
|
|
587
|
+
const result = await this.agent.run(prompt, this.cwd, {
|
|
588
|
+
onUsage,
|
|
589
|
+
onMessage,
|
|
590
|
+
signal: this.activeAbortController.signal,
|
|
591
|
+
logPath
|
|
592
|
+
});
|
|
593
|
+
if (result.output.success) return this.recordSuccess(result.output);
|
|
594
|
+
return this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, result.output.key_learnings);
|
|
595
|
+
} catch (err) {
|
|
596
|
+
const summary = err instanceof Error ? err.message : String(err);
|
|
597
|
+
return this.recordFailure(`[ERROR] ${summary}`, summary, []);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
recordSuccess(output) {
|
|
601
|
+
appendNotes(this.runInfo.notesPath, this.state.currentIteration, output.summary, output.key_changes_made, output.key_learnings);
|
|
602
|
+
commitAll(`gnhf #${this.state.currentIteration}: ${output.summary}`, this.cwd);
|
|
603
|
+
this.state.commitCount = getBranchCommitCount(this.runInfo.baseCommit, this.cwd);
|
|
604
|
+
this.state.successCount++;
|
|
605
|
+
this.state.consecutiveFailures = 0;
|
|
606
|
+
return {
|
|
607
|
+
number: this.state.currentIteration,
|
|
608
|
+
success: true,
|
|
609
|
+
summary: output.summary,
|
|
610
|
+
keyChanges: output.key_changes_made,
|
|
611
|
+
keyLearnings: output.key_learnings,
|
|
612
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
recordFailure(notesSummary, recordSummary, learnings) {
|
|
616
|
+
appendNotes(this.runInfo.notesPath, this.state.currentIteration, notesSummary, [], learnings);
|
|
617
|
+
resetHard(this.cwd);
|
|
618
|
+
this.state.failCount++;
|
|
619
|
+
this.state.consecutiveFailures++;
|
|
620
|
+
return {
|
|
621
|
+
number: this.state.currentIteration,
|
|
622
|
+
success: false,
|
|
623
|
+
summary: recordSummary,
|
|
624
|
+
keyChanges: [],
|
|
625
|
+
keyLearnings: learnings,
|
|
626
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
627
|
+
};
|
|
628
|
+
}
|
|
564
629
|
interruptibleSleep(ms) {
|
|
565
630
|
return new Promise((resolve) => {
|
|
566
631
|
this.activeAbortController = new AbortController();
|
|
@@ -635,6 +700,7 @@ var MockOrchestrator = class extends EventEmitter {
|
|
|
635
700
|
currentIteration: 14,
|
|
636
701
|
totalInputTokens: 873e5,
|
|
637
702
|
totalOutputTokens: 86e4,
|
|
703
|
+
commitCount: 11,
|
|
638
704
|
iterations: [...MOCK_ITERATIONS],
|
|
639
705
|
successCount: 11,
|
|
640
706
|
failCount: 2,
|
|
@@ -725,14 +791,10 @@ function generateStarField(width, height, density, seed) {
|
|
|
725
791
|
function getStarState(star, now) {
|
|
726
792
|
const t = (now % star.period / star.period + star.phase / (Math.PI * 2)) % 1;
|
|
727
793
|
if (t > .05) return star.rest;
|
|
728
|
-
if (star.rest === "bright") {
|
|
794
|
+
if (star.rest === "bright" || star.rest === "hidden") {
|
|
795
|
+
const opposite = star.rest === "bright" ? "hidden" : "bright";
|
|
729
796
|
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";
|
|
797
|
+
if (t > .0175) return opposite;
|
|
736
798
|
return "dim";
|
|
737
799
|
}
|
|
738
800
|
if (t > .025) return "bright";
|
|
@@ -906,6 +968,7 @@ const MOONS_PER_ROW = 30;
|
|
|
906
968
|
const MOON_PHASE_PERIOD = 1600;
|
|
907
969
|
const MAX_MSG_LINES = 3;
|
|
908
970
|
const MAX_MSG_LINE_LEN = 64;
|
|
971
|
+
const RESUME_HINT = "[ctrl+c to stop, gnhf again to resume]";
|
|
909
972
|
function renderTitleCells() {
|
|
910
973
|
return [
|
|
911
974
|
textToCells("g n h f", "dim"),
|
|
@@ -915,7 +978,8 @@ function renderTitleCells() {
|
|
|
915
978
|
textToCells("┗━┛┗━┛┗━┛╺┻┛ ╹ ╹╹┗━┛╹ ╹ ╹ ╹ ╹╹ ╹┗┛ ┗━╸ ╹ ┗━┛╹ ╹", "bold")
|
|
916
979
|
];
|
|
917
980
|
}
|
|
918
|
-
function renderStatsCells(elapsed, inputTokens, outputTokens) {
|
|
981
|
+
function renderStatsCells(elapsed, inputTokens, outputTokens, commitCount) {
|
|
982
|
+
const commitLabel = commitCount === 1 ? "commit" : "commits";
|
|
919
983
|
return [
|
|
920
984
|
...textToCells(elapsed, "bold"),
|
|
921
985
|
...textToCells(" ", "normal"),
|
|
@@ -925,7 +989,11 @@ function renderStatsCells(elapsed, inputTokens, outputTokens) {
|
|
|
925
989
|
...textToCells(" ", "normal"),
|
|
926
990
|
...textToCells("·", "dim"),
|
|
927
991
|
...textToCells(" ", "normal"),
|
|
928
|
-
...textToCells(`${formatTokens(outputTokens)} out`, "normal")
|
|
992
|
+
...textToCells(`${formatTokens(outputTokens)} out`, "normal"),
|
|
993
|
+
...textToCells(" ", "normal"),
|
|
994
|
+
...textToCells("·", "dim"),
|
|
995
|
+
...textToCells(" ", "normal"),
|
|
996
|
+
...textToCells(`${commitCount} ${commitLabel}`, "normal")
|
|
929
997
|
];
|
|
930
998
|
}
|
|
931
999
|
function renderAgentMessageCells(message, status) {
|
|
@@ -958,42 +1026,31 @@ function starStyle(state) {
|
|
|
958
1026
|
if (state === "dim") return "dim";
|
|
959
1027
|
return "normal";
|
|
960
1028
|
}
|
|
961
|
-
function
|
|
962
|
-
const cells = emptyCells(width);
|
|
1029
|
+
function placeStarsInCells(cells, stars, row, xMin, xMax, xOffset, now) {
|
|
963
1030
|
for (const star of stars) {
|
|
964
|
-
if (star.y !==
|
|
1031
|
+
if (star.y !== row || star.x < xMin || star.x >= xMax) continue;
|
|
965
1032
|
const state = getStarState(star, now);
|
|
966
|
-
|
|
1033
|
+
const localX = star.x - xOffset;
|
|
1034
|
+
cells[localX] = state === "hidden" ? {
|
|
967
1035
|
char: " ",
|
|
968
1036
|
style: "normal",
|
|
969
1037
|
width: 1
|
|
970
|
-
}
|
|
971
|
-
else cells[star.x] = {
|
|
1038
|
+
} : {
|
|
972
1039
|
char: star.char,
|
|
973
1040
|
style: starStyle(state),
|
|
974
1041
|
width: 1
|
|
975
1042
|
};
|
|
976
1043
|
}
|
|
1044
|
+
}
|
|
1045
|
+
function renderStarLineCells(stars, width, y, now) {
|
|
1046
|
+
const cells = emptyCells(width);
|
|
1047
|
+
placeStarsInCells(cells, stars, y, 0, width, 0, now);
|
|
977
1048
|
return cells;
|
|
978
1049
|
}
|
|
979
1050
|
function renderSideStarsCells(stars, rowIndex, xOffset, sideWidth, now) {
|
|
980
1051
|
if (sideWidth <= 0) return [];
|
|
981
1052
|
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
|
-
}
|
|
1053
|
+
placeStarsInCells(cells, stars, rowIndex, xOffset, xOffset + sideWidth, xOffset, now);
|
|
997
1054
|
return cells;
|
|
998
1055
|
}
|
|
999
1056
|
function centerLineCells(content, width) {
|
|
@@ -1006,6 +1063,19 @@ function centerLineCells(content, width) {
|
|
|
1006
1063
|
...emptyCells(rightPad)
|
|
1007
1064
|
];
|
|
1008
1065
|
}
|
|
1066
|
+
function renderResumeHintCells(width) {
|
|
1067
|
+
return centerLineCells(textToCells(RESUME_HINT, "dim"), width);
|
|
1068
|
+
}
|
|
1069
|
+
function fitContentRows(contentRows, maxRows) {
|
|
1070
|
+
if (contentRows.length <= maxRows) return contentRows;
|
|
1071
|
+
const fitted = [...contentRows];
|
|
1072
|
+
while (fitted.length > maxRows) {
|
|
1073
|
+
const emptyRowIndex = fitted.findIndex((row) => row.length === 0);
|
|
1074
|
+
if (emptyRowIndex === -1) break;
|
|
1075
|
+
fitted.splice(emptyRowIndex, 1);
|
|
1076
|
+
}
|
|
1077
|
+
return fitted.length > maxRows ? fitted.slice(fitted.length - maxRows) : fitted;
|
|
1078
|
+
}
|
|
1009
1079
|
function buildContentCells(prompt, state, elapsed, now) {
|
|
1010
1080
|
const rows = [];
|
|
1011
1081
|
const isRunning = state.status === "running" || state.status === "waiting";
|
|
@@ -1018,7 +1088,7 @@ function buildContentCells(prompt, state, elapsed, now) {
|
|
|
1018
1088
|
rows.push(pl ? textToCells(pl, "dim") : []);
|
|
1019
1089
|
}
|
|
1020
1090
|
rows.push([], []);
|
|
1021
|
-
rows.push(renderStatsCells(elapsed, state.totalInputTokens, state.totalOutputTokens));
|
|
1091
|
+
rows.push(renderStatsCells(elapsed, state.totalInputTokens, state.totalOutputTokens, state.commitCount));
|
|
1022
1092
|
rows.push([], []);
|
|
1023
1093
|
rows.push(...renderAgentMessageCells(state.lastMessage, state.status));
|
|
1024
1094
|
rows.push([], []);
|
|
@@ -1026,11 +1096,13 @@ function buildContentCells(prompt, state, elapsed, now) {
|
|
|
1026
1096
|
return rows;
|
|
1027
1097
|
}
|
|
1028
1098
|
function buildFrameCells(prompt, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight) {
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1099
|
+
const elapsed = formatElapsed(now - state.startTime.getTime());
|
|
1100
|
+
const availableHeight = Math.max(0, terminalHeight - 2);
|
|
1101
|
+
const contentRows = fitContentRows(buildContentCells(prompt, state, elapsed, now), availableHeight);
|
|
1102
|
+
while (contentRows.length < Math.min(BASE_CONTENT_ROWS, availableHeight)) contentRows.push([]);
|
|
1031
1103
|
const contentCount = contentRows.length;
|
|
1032
|
-
const remaining = Math.max(0,
|
|
1033
|
-
const topHeight = Math.ceil(remaining / 2)
|
|
1104
|
+
const remaining = Math.max(0, availableHeight - contentCount);
|
|
1105
|
+
const topHeight = Math.max(0, Math.ceil(remaining / 2));
|
|
1034
1106
|
const bottomHeight = remaining - topHeight;
|
|
1035
1107
|
const sideWidth = Math.max(0, Math.floor((terminalWidth - CONTENT_WIDTH) / 2));
|
|
1036
1108
|
const frame = [];
|
|
@@ -1046,6 +1118,8 @@ function buildFrameCells(prompt, state, topStars, bottomStars, sideStars, now, t
|
|
|
1046
1118
|
]);
|
|
1047
1119
|
}
|
|
1048
1120
|
for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, terminalWidth, y, now));
|
|
1121
|
+
frame.push(renderResumeHintCells(terminalWidth));
|
|
1122
|
+
frame.push(emptyCells(terminalWidth));
|
|
1049
1123
|
return frame;
|
|
1050
1124
|
}
|
|
1051
1125
|
var Renderer = class {
|
|
@@ -1111,8 +1185,9 @@ var Renderer = class {
|
|
|
1111
1185
|
this.cachedHeight = h;
|
|
1112
1186
|
const contentStart = Math.max(0, Math.floor((w - CONTENT_WIDTH) / 2) - 8);
|
|
1113
1187
|
const contentEnd = contentStart + CONTENT_WIDTH + 16;
|
|
1114
|
-
const
|
|
1115
|
-
const
|
|
1188
|
+
const availableHeight = Math.max(0, h - 2);
|
|
1189
|
+
const remaining = Math.max(0, availableHeight - BASE_CONTENT_ROWS);
|
|
1190
|
+
const topHeight = Math.max(0, Math.ceil(remaining / 2));
|
|
1116
1191
|
const proximityRows = 8;
|
|
1117
1192
|
const shrinkBig = (s, nearContentRow) => {
|
|
1118
1193
|
if (!nearContentRow || s.x < contentStart || s.x >= contentEnd) return s;
|
|
@@ -1127,7 +1202,7 @@ var Renderer = class {
|
|
|
1127
1202
|
};
|
|
1128
1203
|
this.topStars = generateStarField(w, h, STAR_DENSITY, 42).map((s) => shrinkBig(s, s.y >= topHeight - proximityRows));
|
|
1129
1204
|
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);
|
|
1205
|
+
this.sideStars = generateStarField(w, Math.max(BASE_CONTENT_ROWS, availableHeight), STAR_DENSITY, 99);
|
|
1131
1206
|
return true;
|
|
1132
1207
|
}
|
|
1133
1208
|
return false;
|
|
@@ -1156,6 +1231,18 @@ function slugifyPrompt(prompt) {
|
|
|
1156
1231
|
//#endregion
|
|
1157
1232
|
//#region src/cli.ts
|
|
1158
1233
|
const packageVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
1234
|
+
function humanizeErrorMessage(message) {
|
|
1235
|
+
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.";
|
|
1236
|
+
return message;
|
|
1237
|
+
}
|
|
1238
|
+
function initializeNewBranch(prompt, cwd) {
|
|
1239
|
+
ensureCleanWorkingTree(cwd);
|
|
1240
|
+
const baseCommit = getHeadCommit(cwd);
|
|
1241
|
+
const branchName = slugifyPrompt(prompt);
|
|
1242
|
+
createBranch(branchName, cwd);
|
|
1243
|
+
const runId = branchName.split("/")[1];
|
|
1244
|
+
return setupRun(runId, prompt, baseCommit, cwd);
|
|
1245
|
+
}
|
|
1159
1246
|
function ask(question) {
|
|
1160
1247
|
const rl = createInterface({
|
|
1161
1248
|
input: process$1.stdin,
|
|
@@ -1169,7 +1256,7 @@ function ask(question) {
|
|
|
1169
1256
|
});
|
|
1170
1257
|
}
|
|
1171
1258
|
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)"
|
|
1259
|
+
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
1260
|
if (options.mock) {
|
|
1174
1261
|
const mock = new MockOrchestrator();
|
|
1175
1262
|
enterAltScreen();
|
|
@@ -1183,11 +1270,15 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
1183
1270
|
let prompt = promptArg;
|
|
1184
1271
|
if (!prompt && !process$1.stdin.isTTY) prompt = readFileSync("/dev/stdin", "utf-8").trim();
|
|
1185
1272
|
const agentName = options.agent;
|
|
1186
|
-
if (agentName !== "claude" && agentName !== "codex") {
|
|
1273
|
+
if (agentName !== void 0 && agentName !== "claude" && agentName !== "codex") {
|
|
1187
1274
|
console.error(`Unknown agent: ${options.agent}. Use "claude" or "codex".`);
|
|
1188
1275
|
process$1.exit(1);
|
|
1189
1276
|
}
|
|
1190
|
-
const config = loadConfig({ agent: agentName });
|
|
1277
|
+
const config = loadConfig(agentName ? { agent: agentName } : void 0);
|
|
1278
|
+
if (config.agent !== "claude" && config.agent !== "codex") {
|
|
1279
|
+
console.error(`Unknown agent: ${config.agent}. Use "claude" or "codex".`);
|
|
1280
|
+
process$1.exit(1);
|
|
1281
|
+
}
|
|
1191
1282
|
const cwd = process$1.cwd();
|
|
1192
1283
|
const currentBranch = getCurrentBranch(cwd);
|
|
1193
1284
|
const onGnhfBranch = currentBranch.startsWith("gnhf/");
|
|
@@ -1205,30 +1296,18 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
1205
1296
|
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
1297
|
if (answer === "o") {
|
|
1207
1298
|
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);
|
|
1299
|
+
runInfo = setupRun(existingRunId, prompt, existing.baseCommit, cwd);
|
|
1300
|
+
} else if (answer === "n") runInfo = initializeNewBranch(prompt, cwd);
|
|
1301
|
+
else process$1.exit(0);
|
|
1218
1302
|
}
|
|
1219
1303
|
} else {
|
|
1220
1304
|
if (!prompt) {
|
|
1221
1305
|
program.help();
|
|
1222
1306
|
return;
|
|
1223
1307
|
}
|
|
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);
|
|
1308
|
+
runInfo = initializeNewBranch(prompt, cwd);
|
|
1230
1309
|
}
|
|
1231
|
-
const orchestrator = new Orchestrator(config, createAgent(
|
|
1310
|
+
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo), runInfo, prompt, cwd, startIteration);
|
|
1232
1311
|
enterAltScreen();
|
|
1233
1312
|
const renderer = new Renderer(orchestrator, prompt);
|
|
1234
1313
|
renderer.start();
|
|
@@ -1249,7 +1328,7 @@ function exitAltScreen() {
|
|
|
1249
1328
|
process$1.stdout.write("\x1B[?1049l");
|
|
1250
1329
|
}
|
|
1251
1330
|
function die(message) {
|
|
1252
|
-
console.error(`\n gnhf: ${message}\n`);
|
|
1331
|
+
console.error(`\n gnhf: ${humanizeErrorMessage(message)}\n`);
|
|
1253
1332
|
process$1.exit(1);
|
|
1254
1333
|
}
|
|
1255
1334
|
try {
|