gnhf 0.1.0

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/cli.mjs +1247 -0
  4. package/package.json +46 -0
package/dist/cli.mjs ADDED
@@ -0,0 +1,1247 @@
1
+ #!/usr/bin/env node
2
+ import { appendFileSync, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import process$1 from "node:process";
4
+ import { createInterface } from "node:readline";
5
+ import { Command } from "commander";
6
+ import { join } from "node:path";
7
+ import { homedir } from "node:os";
8
+ import yaml from "js-yaml";
9
+ import { execSync, spawn } from "node:child_process";
10
+ import { EventEmitter } from "node:events";
11
+ import { createHash } from "node:crypto";
12
+ //#region src/core/config.ts
13
+ const DEFAULT_CONFIG = {
14
+ agent: "claude",
15
+ maxConsecutiveFailures: 5
16
+ };
17
+ function loadConfig(overrides) {
18
+ const configPath = join(homedir(), ".gnhf", "config.yml");
19
+ let fileConfig = {};
20
+ try {
21
+ const raw = readFileSync(configPath, "utf-8");
22
+ fileConfig = yaml.load(raw) ?? {};
23
+ } catch {}
24
+ return {
25
+ ...DEFAULT_CONFIG,
26
+ ...fileConfig,
27
+ ...overrides
28
+ };
29
+ }
30
+ //#endregion
31
+ //#region src/core/git.ts
32
+ function git(args, cwd) {
33
+ return execSync(`git ${args}`, {
34
+ cwd,
35
+ encoding: "utf-8",
36
+ stdio: "pipe"
37
+ }).trim();
38
+ }
39
+ function getCurrentBranch(cwd) {
40
+ return git("rev-parse --abbrev-ref HEAD", cwd);
41
+ }
42
+ function ensureCleanWorkingTree(cwd) {
43
+ if (git("status --porcelain", cwd)) throw new Error("Working tree is not clean. Commit or stash changes first.");
44
+ }
45
+ function createBranch(branchName, cwd) {
46
+ git(`checkout -b ${branchName}`, cwd);
47
+ }
48
+ function commitAll(message, cwd) {
49
+ git("add -A", cwd);
50
+ try {
51
+ git(`commit -m "${message.replace(/"/g, "\\\"")}"`, cwd);
52
+ } catch {}
53
+ }
54
+ function resetHard(cwd) {
55
+ git("reset --hard HEAD", cwd);
56
+ git("clean -fd", cwd);
57
+ }
58
+ //#endregion
59
+ //#region src/core/run.ts
60
+ const OUTPUT_SCHEMA$1 = JSON.stringify({
61
+ type: "object",
62
+ properties: {
63
+ success: { type: "boolean" },
64
+ summary: { type: "string" },
65
+ key_changes_made: {
66
+ type: "array",
67
+ items: { type: "string" }
68
+ },
69
+ key_learnings: {
70
+ type: "array",
71
+ items: { type: "string" }
72
+ }
73
+ },
74
+ required: [
75
+ "success",
76
+ "summary",
77
+ "key_changes_made",
78
+ "key_learnings"
79
+ ]
80
+ }, null, 2);
81
+ function ensureGitignore(cwd) {
82
+ const gitignorePath = join(cwd, ".gitignore");
83
+ const entry = ".gnhf/runs/";
84
+ if (existsSync(gitignorePath)) {
85
+ if (readFileSync(gitignorePath, "utf-8").split("\n").some((line) => line.trim() === entry)) return;
86
+ appendFileSync(gitignorePath, `\n${entry}\n`, "utf-8");
87
+ } else writeFileSync(gitignorePath, `${entry}\n`, "utf-8");
88
+ }
89
+ function setupRun(runId, prompt, cwd) {
90
+ ensureGitignore(cwd);
91
+ const runDir = join(cwd, ".gnhf", "runs", runId);
92
+ mkdirSync(runDir, { recursive: true });
93
+ const promptPath = join(runDir, "prompt.md");
94
+ writeFileSync(promptPath, prompt, "utf-8");
95
+ const notesPath = join(runDir, "notes.md");
96
+ writeFileSync(notesPath, `# gnhf run: ${runId}\n\nObjective: ${prompt}\n\n## Iteration Log\n`, "utf-8");
97
+ const schemaPath = join(runDir, "output-schema.json");
98
+ writeFileSync(schemaPath, OUTPUT_SCHEMA$1, "utf-8");
99
+ return {
100
+ runId,
101
+ runDir,
102
+ promptPath,
103
+ notesPath,
104
+ schemaPath
105
+ };
106
+ }
107
+ function resumeRun(runId, cwd) {
108
+ const runDir = join(cwd, ".gnhf", "runs", runId);
109
+ if (!existsSync(runDir)) throw new Error(`Run directory not found: ${runDir}`);
110
+ return {
111
+ runId,
112
+ runDir,
113
+ promptPath: join(runDir, "prompt.md"),
114
+ notesPath: join(runDir, "notes.md"),
115
+ schemaPath: join(runDir, "output-schema.json")
116
+ };
117
+ }
118
+ function getLastIterationNumber(runInfo) {
119
+ const files = readdirSync(runInfo.runDir);
120
+ let max = 0;
121
+ for (const f of files) {
122
+ const m = f.match(/^iteration-(\d+)\.jsonl$/);
123
+ if (m) {
124
+ const n = parseInt(m[1], 10);
125
+ if (n > max) max = n;
126
+ }
127
+ }
128
+ return max;
129
+ }
130
+ function appendNotes(notesPath, iteration, summary, changes, learnings) {
131
+ appendFileSync(notesPath, [
132
+ `\n### Iteration ${iteration}\n`,
133
+ `**Summary:** ${summary}\n`,
134
+ changes.length > 0 ? `**Changes:**\n${changes.map((c) => `- ${c}`).join("\n")}\n` : "",
135
+ learnings.length > 0 ? `**Learnings:**\n${learnings.map((l) => `- ${l}`).join("\n")}\n` : ""
136
+ ].join("\n"), "utf-8");
137
+ }
138
+ //#endregion
139
+ //#region src/core/agents/claude.ts
140
+ const OUTPUT_SCHEMA = JSON.stringify({
141
+ type: "object",
142
+ properties: {
143
+ success: { type: "boolean" },
144
+ summary: { type: "string" },
145
+ key_changes_made: {
146
+ type: "array",
147
+ items: { type: "string" }
148
+ },
149
+ key_learnings: {
150
+ type: "array",
151
+ items: { type: "string" }
152
+ }
153
+ },
154
+ required: [
155
+ "success",
156
+ "summary",
157
+ "key_changes_made",
158
+ "key_learnings"
159
+ ]
160
+ });
161
+ var ClaudeAgent = class {
162
+ name = "claude";
163
+ run(prompt, cwd, options) {
164
+ const { onUsage, onMessage, signal, logPath } = options ?? {};
165
+ return new Promise((resolve, reject) => {
166
+ const logStream = logPath ? createWriteStream(logPath) : null;
167
+ const child = spawn("claude", [
168
+ "-p",
169
+ prompt,
170
+ "--verbose",
171
+ "--output-format",
172
+ "stream-json",
173
+ "--json-schema",
174
+ OUTPUT_SCHEMA,
175
+ "--dangerously-skip-permissions"
176
+ ], {
177
+ cwd,
178
+ stdio: [
179
+ "ignore",
180
+ "pipe",
181
+ "pipe"
182
+ ],
183
+ env: process.env
184
+ });
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 = "";
199
+ let resultEvent = null;
200
+ const cumulative = {
201
+ inputTokens: 0,
202
+ outputTokens: 0,
203
+ cacheReadTokens: 0,
204
+ cacheCreationTokens: 0
205
+ };
206
+ child.stdout.on("data", (data) => {
207
+ logStream?.write(data);
208
+ buffer += data.toString();
209
+ const lines = buffer.split("\n");
210
+ buffer = lines.pop() ?? "";
211
+ for (const line of lines) {
212
+ if (!line.trim()) continue;
213
+ try {
214
+ const event = JSON.parse(line);
215
+ if (event.type === "assistant") {
216
+ const msg = event.message;
217
+ cumulative.inputTokens += (msg.usage.input_tokens ?? 0) + (msg.usage.cache_read_input_tokens ?? 0);
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
+ }
228
+ }
229
+ if (event.type === "result") resultEvent = event;
230
+ } catch {}
231
+ }
232
+ });
233
+ child.stderr.on("data", (data) => {
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
+ }
245
+ if (!resultEvent) {
246
+ reject(/* @__PURE__ */ new Error("claude returned no result event"));
247
+ return;
248
+ }
249
+ if (resultEvent.is_error || resultEvent.subtype !== "success") {
250
+ reject(/* @__PURE__ */ new Error(`claude reported error: ${JSON.stringify(resultEvent)}`));
251
+ return;
252
+ }
253
+ if (!resultEvent.structured_output) {
254
+ reject(/* @__PURE__ */ new Error("claude returned no structured_output"));
255
+ return;
256
+ }
257
+ const output = resultEvent.structured_output;
258
+ const usage = {
259
+ inputTokens: (resultEvent.usage.input_tokens ?? 0) + (resultEvent.usage.cache_read_input_tokens ?? 0),
260
+ outputTokens: resultEvent.usage.output_tokens ?? 0,
261
+ cacheReadTokens: resultEvent.usage.cache_read_input_tokens ?? 0,
262
+ cacheCreationTokens: resultEvent.usage.cache_creation_input_tokens ?? 0
263
+ };
264
+ onUsage?.(usage);
265
+ resolve({
266
+ output,
267
+ usage
268
+ });
269
+ });
270
+ });
271
+ }
272
+ };
273
+ //#endregion
274
+ //#region src/core/agents/codex.ts
275
+ var CodexAgent = class {
276
+ name = "codex";
277
+ schemaPath;
278
+ constructor(schemaPath) {
279
+ this.schemaPath = schemaPath;
280
+ }
281
+ run(prompt, cwd, options) {
282
+ const { onUsage, onMessage, signal, logPath } = options ?? {};
283
+ return new Promise((resolve, reject) => {
284
+ const logStream = logPath ? createWriteStream(logPath) : null;
285
+ const child = spawn("codex", [
286
+ "exec",
287
+ prompt,
288
+ "--json",
289
+ "--output-schema",
290
+ this.schemaPath,
291
+ "--dangerously-bypass-approvals-and-sandbox",
292
+ "--color",
293
+ "never"
294
+ ], {
295
+ cwd,
296
+ stdio: [
297
+ "ignore",
298
+ "pipe",
299
+ "pipe"
300
+ ],
301
+ env: process.env
302
+ });
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 = "";
316
+ let lastAgentMessage = null;
317
+ const usages = [];
318
+ let buffer = "";
319
+ child.stdout.on("data", (data) => {
320
+ logStream?.write(data);
321
+ buffer += data.toString();
322
+ const lines = buffer.split("\n");
323
+ buffer = lines.pop() ?? "";
324
+ for (const line of lines) {
325
+ if (!line.trim()) continue;
326
+ try {
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 {}
347
+ }
348
+ });
349
+ child.stderr.on("data", (data) => {
350
+ stderr += data.toString();
351
+ });
352
+ child.on("error", (err) => {
353
+ reject(/* @__PURE__ */ new Error(`Failed to spawn codex: ${err.message}`));
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;
360
+ }
361
+ if (!lastAgentMessage) {
362
+ reject(/* @__PURE__ */ new Error("codex returned no agent message"));
363
+ return;
364
+ }
365
+ try {
366
+ resolve({
367
+ output: JSON.parse(lastAgentMessage),
368
+ usage: usages.reduce((acc, u) => ({
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
+ })
379
+ });
380
+ } catch (err) {
381
+ reject(/* @__PURE__ */ new Error(`Failed to parse codex output: ${err instanceof Error ? err.message : err}`));
382
+ }
383
+ });
384
+ });
385
+ }
386
+ };
387
+ //#endregion
388
+ //#region src/core/agents/factory.ts
389
+ function createAgent(name, runInfo) {
390
+ switch (name) {
391
+ case "claude": return new ClaudeAgent();
392
+ case "codex": return new CodexAgent(runInfo.schemaPath);
393
+ }
394
+ }
395
+ //#endregion
396
+ //#region src/templates/iteration-prompt.ts
397
+ function buildIterationPrompt(params) {
398
+ return `You are working autonomously on an objective given below.
399
+ This is iteration ${params.n} of an ongoing loop to fully accomplish the objective.
400
+
401
+ ## Instructions
402
+
403
+ 1. Read .gnhf/runs/${params.runId}/notes.md first to understand what has been done in previous iterations.
404
+ 2. Focus on one smallest logical unit of work that's individually testable and would make incremental progress towards the objective. Do NOT try to do everything at once.
405
+ 3. Run build/tests/linters/formatters if available to validate your work.
406
+ 4. Do NOT make any git commits. Commits will be handled automatically by the gnhf orchestrator.
407
+ 5. When you are done, respond with a JSON object according to the provided schema.
408
+
409
+ ## Objective
410
+
411
+ ${params.prompt}`;
412
+ }
413
+ //#endregion
414
+ //#region src/core/orchestrator.ts
415
+ var Orchestrator = class extends EventEmitter {
416
+ config;
417
+ agent;
418
+ runInfo;
419
+ cwd;
420
+ prompt;
421
+ stopRequested = false;
422
+ activeAbortController = null;
423
+ state = {
424
+ status: "running",
425
+ currentIteration: 0,
426
+ totalInputTokens: 0,
427
+ totalOutputTokens: 0,
428
+ iterations: [],
429
+ successCount: 0,
430
+ failCount: 0,
431
+ consecutiveFailures: 0,
432
+ startTime: /* @__PURE__ */ new Date(),
433
+ waitingUntil: null,
434
+ lastMessage: null
435
+ };
436
+ constructor(config, agent, runInfo, prompt, cwd, startIteration = 0) {
437
+ super();
438
+ this.config = config;
439
+ this.agent = agent;
440
+ this.runInfo = runInfo;
441
+ this.prompt = prompt;
442
+ this.cwd = cwd;
443
+ this.state.currentIteration = startIteration;
444
+ }
445
+ getState() {
446
+ return { ...this.state };
447
+ }
448
+ stop() {
449
+ this.stopRequested = true;
450
+ this.activeAbortController?.abort();
451
+ resetHard(this.cwd);
452
+ this.state.status = "stopped";
453
+ this.emit("state", this.getState());
454
+ this.emit("stopped");
455
+ }
456
+ async start() {
457
+ this.state.startTime = /* @__PURE__ */ new Date();
458
+ this.state.status = "running";
459
+ this.emit("state", this.getState());
460
+ while (!this.stopRequested) {
461
+ this.state.currentIteration++;
462
+ this.state.status = "running";
463
+ this.emit("iteration:start", this.state.currentIteration);
464
+ this.emit("state", this.getState());
465
+ const iterationPrompt = buildIterationPrompt({
466
+ n: this.state.currentIteration,
467
+ runId: this.runInfo.runId,
468
+ prompt: this.prompt
469
+ });
470
+ let record;
471
+ const baseInputTokens = this.state.totalInputTokens;
472
+ const baseOutputTokens = this.state.totalOutputTokens;
473
+ this.activeAbortController = new AbortController();
474
+ const onUsage = (usage) => {
475
+ this.state.totalInputTokens = baseInputTokens + usage.inputTokens;
476
+ this.state.totalOutputTokens = baseOutputTokens + usage.outputTokens;
477
+ this.emit("state", this.getState());
478
+ };
479
+ const onMessage = (text) => {
480
+ this.state.lastMessage = text;
481
+ this.emit("state", this.getState());
482
+ };
483
+ const logPath = join(this.runInfo.runDir, `iteration-${this.state.currentIteration}.jsonl`);
484
+ try {
485
+ const result = await this.agent.run(iterationPrompt, this.cwd, {
486
+ onUsage,
487
+ onMessage,
488
+ signal: this.activeAbortController.signal,
489
+ logPath
490
+ });
491
+ if (result.output.success) {
492
+ appendNotes(this.runInfo.notesPath, this.state.currentIteration, result.output.summary, result.output.key_changes_made, result.output.key_learnings);
493
+ commitAll(`gnhf #${this.state.currentIteration}: ${result.output.summary}`, this.cwd);
494
+ this.state.successCount++;
495
+ this.state.consecutiveFailures = 0;
496
+ record = {
497
+ number: this.state.currentIteration,
498
+ success: true,
499
+ summary: result.output.summary,
500
+ keyChanges: result.output.key_changes_made,
501
+ keyLearnings: result.output.key_learnings,
502
+ timestamp: /* @__PURE__ */ new Date()
503
+ };
504
+ } else {
505
+ appendNotes(this.runInfo.notesPath, this.state.currentIteration, `[FAIL] ${result.output.summary}`, [], result.output.key_learnings);
506
+ resetHard(this.cwd);
507
+ this.state.failCount++;
508
+ this.state.consecutiveFailures++;
509
+ record = {
510
+ number: this.state.currentIteration,
511
+ success: false,
512
+ summary: result.output.summary,
513
+ keyChanges: [],
514
+ keyLearnings: result.output.key_learnings,
515
+ timestamp: /* @__PURE__ */ new Date()
516
+ };
517
+ }
518
+ } catch (err) {
519
+ const summary = err instanceof Error ? err.message : String(err);
520
+ appendNotes(this.runInfo.notesPath, this.state.currentIteration, `[ERROR] ${summary}`, [], []);
521
+ resetHard(this.cwd);
522
+ this.state.failCount++;
523
+ this.state.consecutiveFailures++;
524
+ record = {
525
+ number: this.state.currentIteration,
526
+ success: false,
527
+ summary,
528
+ keyChanges: [],
529
+ keyLearnings: [],
530
+ timestamp: /* @__PURE__ */ new Date()
531
+ };
532
+ }
533
+ this.state.iterations.push(record);
534
+ this.emit("iteration:end", record);
535
+ this.emit("state", this.getState());
536
+ if (this.state.consecutiveFailures >= this.config.maxConsecutiveFailures) {
537
+ this.state.status = "aborted";
538
+ const reason = `${this.config.maxConsecutiveFailures} consecutive failures`;
539
+ this.emit("abort", reason);
540
+ this.emit("state", this.getState());
541
+ break;
542
+ }
543
+ if (this.state.consecutiveFailures > 0 && !this.stopRequested) {
544
+ const backoffMs = 6e4 * Math.pow(2, this.state.consecutiveFailures - 1);
545
+ this.state.status = "waiting";
546
+ this.state.waitingUntil = new Date(Date.now() + backoffMs);
547
+ this.emit("state", this.getState());
548
+ await this.interruptibleSleep(backoffMs);
549
+ this.state.waitingUntil = null;
550
+ if (!this.stopRequested) {
551
+ this.state.status = "running";
552
+ this.emit("state", this.getState());
553
+ }
554
+ }
555
+ }
556
+ }
557
+ interruptibleSleep(ms) {
558
+ return new Promise((resolve) => {
559
+ this.activeAbortController = new AbortController();
560
+ const timer = setTimeout(() => {
561
+ this.activeAbortController = null;
562
+ resolve();
563
+ }, ms);
564
+ this.activeAbortController.signal.addEventListener("abort", () => {
565
+ clearTimeout(timer);
566
+ this.activeAbortController = null;
567
+ resolve();
568
+ });
569
+ });
570
+ }
571
+ };
572
+ //#endregion
573
+ //#region src/mock-orchestrator.ts
574
+ function mockIter(n, success, summary, agoMs) {
575
+ return {
576
+ number: n,
577
+ success,
578
+ summary,
579
+ keyChanges: [],
580
+ keyLearnings: [],
581
+ timestamp: new Date(Date.now() - agoMs)
582
+ };
583
+ }
584
+ const MOCK_ITERATIONS = [
585
+ mockIter(1, true, "Profiled cold start — identified 3 major bottlenecks", 252e5),
586
+ mockIter(2, true, "Lazy-loaded config module, shaved 340ms off init", 24e6),
587
+ mockIter(3, true, "Deferred plugin discovery to post-render", 228e5),
588
+ mockIter(4, false, "Attempted parallel module init — race condition in DI container", 21e6),
589
+ mockIter(5, true, "Fixed DI ordering, parallelized safe modules only", 192e5),
590
+ mockIter(6, true, "Replaced synchronous JSON parse with streaming decoder", 174e5),
591
+ mockIter(7, true, "Cached resolved dependency graph across restarts", 15e6),
592
+ mockIter(8, false, "Tree-shaking broke runtime dynamic import paths", 126e5),
593
+ mockIter(9, true, "Restored dynamic imports, added explicit entry chunks", 108e5),
594
+ mockIter(10, true, "Inlined critical-path CSS, deferred non-essential styles", 84e5),
595
+ mockIter(11, true, "Switched from full Intl polyfill to locale-on-demand", 6e6),
596
+ mockIter(12, true, "Pre-compiled handlebars templates at build time", 36e5),
597
+ mockIter(13, true, "Moved telemetry init behind requestIdleCallback", 18e5)
598
+ ];
599
+ const AGENT_MESSAGES = [
600
+ "Reading src/bootstrap.ts to trace the module init order",
601
+ "Let me profile the require() chain with --cpu-prof",
602
+ "Running integration tests after the lazy-load refactor",
603
+ "Let me make sure HMR still works in dev mode",
604
+ "Nice — startup dropped from 1.24s to 0.41s so far",
605
+ "Found it! There's a sync readFileSync in the config loader hot path — that's blocking the entire init sequence",
606
+ "I'm analyzing the import graph for circular dependencies. So far I've found 3 cycles that force eager evaluation",
607
+ "Now I'll move database pool creation behind a first-request gate so we don't pay the connection cost at boot time",
608
+ "Let me check if the logger init can be deferred safely. It looks like only the error handler depends on it early",
609
+ "Checking the bundle size delta after tree-shaking. Went from 847KB down to 612KB — solid improvement",
610
+ "I'm replacing those sync fs calls with a pre-cached config lookup that gets populated during the build step",
611
+ "Running a cold start benchmark to establish a baseline. Currently at 1.24s — I think we can get under 500ms",
612
+ "I'm looking at the startup flame graph and there are three clear bottlenecks: config parsing (310ms), plugin discovery (280ms), and template compilation (190ms)",
613
+ "Let me verify the middleware registration order is still correct after the lazy-load refactor. The auth middleware needs to run before any route handlers get registered",
614
+ "I need to test what happens when the first request arrives before deferred services finish initializing. Adding a readiness gate that blocks until all critical paths are up",
615
+ "Now I'll investigate whether route registration can be made async without breaking the Express contract. The docs say app.listen() waits, but I want to confirm with a test",
616
+ "Auditing all feature flags to see if any depend on early init. Looks like none of them do — they all read from a lazy-loaded remote config that we fetch on first access",
617
+ "I'm adding startup timing spans to the OpenTelemetry traces so we can track cold start regressions in prod. Each phase will get its own span: config, plugins, routes, middleware",
618
+ "Confirming all 47 tests still pass after these changes. The config loader tests needed updating since they were assuming synchronous initialization — fixing those now",
619
+ "Let me validate the health check endpoint still responds within 50ms even during the deferred init window. I'll add a lightweight synthetic probe that skips the full stack"
620
+ ];
621
+ function randInt(min, max) {
622
+ return Math.floor(Math.random() * (max - min + 1)) + min;
623
+ }
624
+ const INITIAL_ELAPSED_MS = 29237e3;
625
+ var MockOrchestrator = class extends EventEmitter {
626
+ state = {
627
+ status: "running",
628
+ currentIteration: 14,
629
+ totalInputTokens: 873e5,
630
+ totalOutputTokens: 86e4,
631
+ iterations: [...MOCK_ITERATIONS],
632
+ successCount: 11,
633
+ failCount: 2,
634
+ consecutiveFailures: 0,
635
+ startTime: new Date(Date.now() - INITIAL_ELAPSED_MS),
636
+ waitingUntil: null,
637
+ lastMessage: AGENT_MESSAGES[0]
638
+ };
639
+ tokenTimer = null;
640
+ messageTimer = null;
641
+ messageIndex = 0;
642
+ getState() {
643
+ return {
644
+ ...this.state,
645
+ iterations: [...this.state.iterations]
646
+ };
647
+ }
648
+ stop() {
649
+ if (this.tokenTimer) clearTimeout(this.tokenTimer);
650
+ if (this.messageTimer) clearTimeout(this.messageTimer);
651
+ this.tokenTimer = null;
652
+ this.messageTimer = null;
653
+ this.state.status = "stopped";
654
+ this.emit("state", this.getState());
655
+ this.emit("stopped");
656
+ }
657
+ start() {
658
+ this.emit("state", this.getState());
659
+ this.scheduleTokenBump();
660
+ this.scheduleNextMessage();
661
+ }
662
+ scheduleTokenBump() {
663
+ this.tokenTimer = setTimeout(() => {
664
+ this.state.totalInputTokens += randInt(4e4, 18e4);
665
+ this.state.totalOutputTokens += randInt(200, 2e3);
666
+ this.emit("state", this.getState());
667
+ if (this.state.status === "running") this.scheduleTokenBump();
668
+ }, randInt(1500, 7e3));
669
+ }
670
+ scheduleNextMessage() {
671
+ const delay = randInt(3e3, 7e3);
672
+ this.messageTimer = setTimeout(() => {
673
+ this.messageIndex = (this.messageIndex + 1) % AGENT_MESSAGES.length;
674
+ this.state.lastMessage = AGENT_MESSAGES[this.messageIndex];
675
+ this.emit("state", this.getState());
676
+ this.scheduleNextMessage();
677
+ }, delay);
678
+ }
679
+ };
680
+ //#endregion
681
+ //#region src/utils/stars.ts
682
+ const STAR_CHARS = [
683
+ "·",
684
+ "·",
685
+ "·",
686
+ "·",
687
+ "·",
688
+ "·",
689
+ "✧",
690
+ "⋆",
691
+ "⋆",
692
+ "⋆",
693
+ "°",
694
+ "°"
695
+ ];
696
+ function generateStarField(width, height, density, seed) {
697
+ const stars = [];
698
+ let s = seed;
699
+ const rand = () => {
700
+ s = (s * 16807 + 0) % 2147483647;
701
+ return s / 2147483647;
702
+ };
703
+ for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) if (rand() < density) {
704
+ const charIdx = Math.floor(rand() * STAR_CHARS.length);
705
+ const r = rand();
706
+ const rest = r < .15 ? "hidden" : r < .4 ? "dim" : "bright";
707
+ stars.push({
708
+ x,
709
+ y,
710
+ char: STAR_CHARS[charIdx],
711
+ phase: rand() * Math.PI * 2,
712
+ period: 1e4 + rand() * 15e3,
713
+ rest
714
+ });
715
+ }
716
+ return stars;
717
+ }
718
+ function getStarState(star, now) {
719
+ const t = (now % star.period / star.period + star.phase / (Math.PI * 2)) % 1;
720
+ if (t > .05) return star.rest;
721
+ if (star.rest === "bright") {
722
+ if (t > .0325) return "dim";
723
+ if (t > .0175) return "hidden";
724
+ return "dim";
725
+ }
726
+ if (star.rest === "hidden") {
727
+ if (t > .0325) return "dim";
728
+ if (t > .0175) return "bright";
729
+ return "dim";
730
+ }
731
+ if (t > .025) return "bright";
732
+ return "dim";
733
+ }
734
+ //#endregion
735
+ //#region src/utils/moon.ts
736
+ const MOON_PHASES = [
737
+ "🌑",
738
+ "🌒",
739
+ "🌓",
740
+ "🌔",
741
+ "🌕",
742
+ "🌖",
743
+ "🌗",
744
+ "🌘"
745
+ ];
746
+ function getMoonPhase(state, now = 0, periodMs = 4e3) {
747
+ if (state === "success") return "🌕";
748
+ if (state === "fail") return "🌑";
749
+ return MOON_PHASES[Math.floor(now % periodMs / periodMs * 8) % 8];
750
+ }
751
+ //#endregion
752
+ //#region src/utils/time.ts
753
+ function formatElapsed(ms) {
754
+ const s = Math.floor(ms / 1e3);
755
+ return `${String(Math.floor(s / 3600)).padStart(2, "0")}:${String(Math.floor(s % 3600 / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
756
+ }
757
+ //#endregion
758
+ //#region src/utils/tokens.ts
759
+ function formatTokens(count) {
760
+ if (count >= 0xe8d4a51000) return `${(count / 0xe8d4a51000).toFixed(1)}T`;
761
+ if (count >= 1e9) return `${(count / 1e9).toFixed(1)}B`;
762
+ if (count >= 1e6) return `${(count / 1e6).toFixed(1)}M`;
763
+ if (count >= 1e3) return `${Math.round(count / 1e3)}K`;
764
+ return String(count);
765
+ }
766
+ //#endregion
767
+ //#region src/utils/wordwrap.ts
768
+ function wordWrap(text, width, maxLines) {
769
+ if (!text) return [];
770
+ const words = text.split(/\s+/);
771
+ const lines = [];
772
+ let current = "";
773
+ for (const word of words) {
774
+ if (word.length > width) {
775
+ if (current) {
776
+ lines.push(current);
777
+ current = "";
778
+ }
779
+ for (let i = 0; i < word.length; i += width) lines.push(word.slice(i, i + width));
780
+ continue;
781
+ }
782
+ if (current && current.length + 1 + word.length > width) {
783
+ lines.push(current);
784
+ current = word;
785
+ } else current = current ? current + " " + word : word;
786
+ }
787
+ if (current) lines.push(current);
788
+ if (maxLines && lines.length > maxLines) {
789
+ const capped = lines.slice(0, maxLines);
790
+ const last = capped[maxLines - 1];
791
+ capped[maxLines - 1] = last.length >= width ? last.slice(0, width - 1) + "…" : last + "…";
792
+ return capped;
793
+ }
794
+ return lines;
795
+ }
796
+ //#endregion
797
+ //#region src/renderer-diff.ts
798
+ const SPACE = {
799
+ char: " ",
800
+ style: "normal",
801
+ width: 1
802
+ };
803
+ function makeCell(char, style) {
804
+ return {
805
+ char,
806
+ style,
807
+ width: (char.codePointAt(0) ?? 0) > 65535 ? 2 : 1
808
+ };
809
+ }
810
+ function textToCells(text, style) {
811
+ const cells = [];
812
+ for (const char of text) {
813
+ const cell = makeCell(char, style);
814
+ cells.push(cell);
815
+ if (cell.width === 2) cells.push({
816
+ char: "",
817
+ style: "normal",
818
+ width: 0
819
+ });
820
+ }
821
+ return cells;
822
+ }
823
+ function emptyCells(n) {
824
+ const cells = [];
825
+ for (let i = 0; i < n; i++) cells.push({ ...SPACE });
826
+ return cells;
827
+ }
828
+ function rowToString(cells) {
829
+ let result = "";
830
+ let currentStyle = "normal";
831
+ for (const cell of cells) {
832
+ if (cell.width === 0) continue;
833
+ if (cell.style !== currentStyle) {
834
+ if (currentStyle !== "normal") result += "\x1B[0m";
835
+ if (cell.style === "bold") result += "\x1B[1m";
836
+ else if (cell.style === "dim") result += "\x1B[2m";
837
+ currentStyle = cell.style;
838
+ }
839
+ result += cell.char;
840
+ }
841
+ if (currentStyle !== "normal") result += "\x1B[0m";
842
+ return result;
843
+ }
844
+ function diffFrames(prev, next) {
845
+ const changes = [];
846
+ const rows = Math.min(prev.length, next.length);
847
+ for (let r = 0; r < rows; r++) {
848
+ const prevRow = prev[r];
849
+ const nextRow = next[r];
850
+ const cols = Math.min(prevRow.length, nextRow.length);
851
+ for (let c = 0; c < cols; c++) {
852
+ const n = nextRow[c];
853
+ if (n.width === 0) continue;
854
+ const p = prevRow[c];
855
+ if (p.char !== n.char || p.style !== n.style || p.width !== n.width) changes.push({
856
+ row: r,
857
+ col: c,
858
+ cell: n
859
+ });
860
+ }
861
+ }
862
+ return changes;
863
+ }
864
+ function emitDiff(changes) {
865
+ if (changes.length === 0) return "";
866
+ let result = "";
867
+ let currentStyle = null;
868
+ let cursorRow = -1;
869
+ let cursorCol = -1;
870
+ for (const { row, col, cell } of changes) {
871
+ if (row !== cursorRow || col !== cursorCol) result += `\x1b[${row + 1};${col + 1}H`;
872
+ if (cell.style !== currentStyle) {
873
+ result += "\x1B[0m";
874
+ if (cell.style === "bold") result += "\x1B[1m";
875
+ else if (cell.style === "dim") result += "\x1B[2m";
876
+ currentStyle = cell.style;
877
+ }
878
+ result += cell.char;
879
+ cursorRow = row;
880
+ cursorCol = col + cell.width;
881
+ }
882
+ if (currentStyle !== "normal") result += "\x1B[0m";
883
+ return result;
884
+ }
885
+ //#endregion
886
+ //#region src/renderer.ts
887
+ const CONTENT_WIDTH = 63;
888
+ const MAX_PROMPT_LINES = 3;
889
+ const BASE_CONTENT_ROWS = 24;
890
+ const STAR_DENSITY = .035;
891
+ const TICK_MS = 200;
892
+ const MOONS_PER_ROW = 30;
893
+ const MOON_PHASE_PERIOD = 1600;
894
+ const MAX_MSG_LINES = 3;
895
+ const MAX_MSG_LINE_LEN = 64;
896
+ function renderTitleCells() {
897
+ return [
898
+ textToCells("g n h f", "dim"),
899
+ [],
900
+ textToCells("┏━╸┏━┓┏━┓╺┳┓ ┏┓╻╻┏━╸╻ ╻╺┳╸ ╻ ╻┏━┓╻ ╻┏━╸ ┏━╸╻ ╻┏┓╻", "bold"),
901
+ textToCells("┃╺┓┃ ┃┃ ┃ ┃┃ ┃┗┫┃┃╺┓┣━┫ ┃ ┣━┫┣━┫┃┏┛┣╸ ┣╸ ┃ ┃┃┗┫", "bold"),
902
+ textToCells("┗━┛┗━┛┗━┛╺┻┛ ╹ ╹╹┗━┛╹ ╹ ╹ ╹ ╹╹ ╹┗┛ ┗━╸ ╹ ┗━┛╹ ╹", "bold")
903
+ ];
904
+ }
905
+ function renderStatsCells(elapsed, inputTokens, outputTokens) {
906
+ return [
907
+ ...textToCells(elapsed, "bold"),
908
+ ...textToCells(" ", "normal"),
909
+ ...textToCells("·", "dim"),
910
+ ...textToCells(" ", "normal"),
911
+ ...textToCells(`${formatTokens(inputTokens)} in`, "normal"),
912
+ ...textToCells(" ", "normal"),
913
+ ...textToCells("·", "dim"),
914
+ ...textToCells(" ", "normal"),
915
+ ...textToCells(`${formatTokens(outputTokens)} out`, "normal")
916
+ ];
917
+ }
918
+ function renderAgentMessageCells(message, status) {
919
+ const lines = [];
920
+ if (status === "waiting") lines.push("waiting (backoff)...");
921
+ else if (status === "aborted" && !message) lines.push("max consecutive failures reached");
922
+ else if (!message) lines.push("working...");
923
+ else {
924
+ const wrapped = wordWrap(message, MAX_MSG_LINE_LEN, MAX_MSG_LINES);
925
+ for (const wl of wrapped) lines.push(wl);
926
+ }
927
+ while (lines.length < MAX_MSG_LINES) lines.push("");
928
+ return lines.map((l) => l ? textToCells(l, "dim") : []);
929
+ }
930
+ function renderMoonStripCells(iterations, isRunning, now) {
931
+ const moons = iterations.map((iter) => getMoonPhase(iter.success ? "success" : "fail"));
932
+ if (isRunning) moons.push(getMoonPhase("active", now, MOON_PHASE_PERIOD));
933
+ if (moons.length === 0) return [[]];
934
+ const rows = [];
935
+ for (let i = 0; i < moons.length; i += MOONS_PER_ROW) {
936
+ const slice = moons.slice(i, i + MOONS_PER_ROW);
937
+ const cells = [];
938
+ for (const moon of slice) cells.push(...textToCells(moon, "normal"));
939
+ rows.push(cells);
940
+ }
941
+ return rows;
942
+ }
943
+ function starStyle(state) {
944
+ if (state === "bright") return "bold";
945
+ if (state === "dim") return "dim";
946
+ return "normal";
947
+ }
948
+ function renderStarLineCells(stars, width, y, now) {
949
+ const cells = emptyCells(width);
950
+ for (const star of stars) {
951
+ if (star.y !== y) continue;
952
+ const state = getStarState(star, now);
953
+ if (state === "hidden") cells[star.x] = {
954
+ char: " ",
955
+ style: "normal",
956
+ width: 1
957
+ };
958
+ else cells[star.x] = {
959
+ char: star.char,
960
+ style: starStyle(state),
961
+ width: 1
962
+ };
963
+ }
964
+ return cells;
965
+ }
966
+ function renderSideStarsCells(stars, rowIndex, xOffset, sideWidth, now) {
967
+ if (sideWidth <= 0) return [];
968
+ const cells = emptyCells(sideWidth);
969
+ for (const star of stars) {
970
+ if (star.y !== rowIndex || star.x < xOffset || star.x >= xOffset + sideWidth) continue;
971
+ const localX = star.x - xOffset;
972
+ const state = getStarState(star, now);
973
+ if (state === "hidden") cells[localX] = {
974
+ char: " ",
975
+ style: "normal",
976
+ width: 1
977
+ };
978
+ else cells[localX] = {
979
+ char: star.char,
980
+ style: starStyle(state),
981
+ width: 1
982
+ };
983
+ }
984
+ return cells;
985
+ }
986
+ function centerLineCells(content, width) {
987
+ const w = content.length;
988
+ const pad = Math.max(0, Math.floor((width - w) / 2));
989
+ const rightPad = Math.max(0, width - w - pad);
990
+ return [
991
+ ...emptyCells(pad),
992
+ ...content,
993
+ ...emptyCells(rightPad)
994
+ ];
995
+ }
996
+ function buildContentCells(prompt, state, elapsed, now) {
997
+ const rows = [];
998
+ const isRunning = state.status === "running" || state.status === "waiting";
999
+ rows.push([]);
1000
+ rows.push(...renderTitleCells());
1001
+ rows.push([], []);
1002
+ const promptLines = wordWrap(prompt, CONTENT_WIDTH, MAX_PROMPT_LINES);
1003
+ for (let i = 0; i < MAX_PROMPT_LINES; i++) {
1004
+ const pl = promptLines[i] ?? "";
1005
+ rows.push(pl ? textToCells(pl, "dim") : []);
1006
+ }
1007
+ rows.push([], []);
1008
+ rows.push(renderStatsCells(elapsed, state.totalInputTokens, state.totalOutputTokens));
1009
+ rows.push([], []);
1010
+ rows.push(...renderAgentMessageCells(state.lastMessage, state.status));
1011
+ rows.push([], []);
1012
+ rows.push(...renderMoonStripCells(state.iterations, isRunning, now));
1013
+ return rows;
1014
+ }
1015
+ function buildFrameCells(prompt, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight) {
1016
+ const contentRows = buildContentCells(prompt, state, formatElapsed(now - state.startTime.getTime()), now);
1017
+ while (contentRows.length < BASE_CONTENT_ROWS) contentRows.push([]);
1018
+ const contentCount = contentRows.length;
1019
+ const remaining = Math.max(0, terminalHeight - contentCount);
1020
+ const topHeight = Math.ceil(remaining / 2) - 1;
1021
+ const bottomHeight = remaining - topHeight;
1022
+ const sideWidth = Math.max(0, Math.floor((terminalWidth - CONTENT_WIDTH) / 2));
1023
+ const frame = [];
1024
+ for (let y = 0; y < topHeight; y++) frame.push(renderStarLineCells(topStars, terminalWidth, y, now));
1025
+ for (let i = 0; i < contentRows.length; i++) {
1026
+ const left = renderSideStarsCells(sideStars, i, 0, sideWidth, now);
1027
+ const center = centerLineCells(contentRows[i], CONTENT_WIDTH);
1028
+ const right = renderSideStarsCells(sideStars, i, terminalWidth - sideWidth, sideWidth, now);
1029
+ frame.push([
1030
+ ...left,
1031
+ ...center,
1032
+ ...right
1033
+ ]);
1034
+ }
1035
+ for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, terminalWidth, y, now));
1036
+ return frame;
1037
+ }
1038
+ var Renderer = class {
1039
+ orchestrator;
1040
+ prompt;
1041
+ state;
1042
+ interval = null;
1043
+ exitResolve;
1044
+ exitPromise;
1045
+ topStars = [];
1046
+ bottomStars = [];
1047
+ sideStars = [];
1048
+ cachedWidth = 0;
1049
+ cachedHeight = 0;
1050
+ prevCells = [];
1051
+ isFirstFrame = true;
1052
+ constructor(orchestrator, prompt) {
1053
+ this.orchestrator = orchestrator;
1054
+ this.prompt = prompt;
1055
+ this.state = orchestrator.getState();
1056
+ this.exitPromise = new Promise((resolve) => {
1057
+ this.exitResolve = resolve;
1058
+ });
1059
+ }
1060
+ start() {
1061
+ this.orchestrator.on("state", (newState) => {
1062
+ this.state = {
1063
+ ...newState,
1064
+ iterations: [...newState.iterations]
1065
+ };
1066
+ });
1067
+ this.orchestrator.on("stopped", () => {
1068
+ this.stop();
1069
+ });
1070
+ if (process$1.stdin.isTTY) {
1071
+ process$1.stdin.setRawMode(true);
1072
+ process$1.stdin.resume();
1073
+ process$1.stdin.on("data", (data) => {
1074
+ if (data[0] === 3) this.orchestrator.stop();
1075
+ });
1076
+ }
1077
+ this.interval = setInterval(() => this.render(), TICK_MS);
1078
+ this.render();
1079
+ }
1080
+ stop() {
1081
+ if (this.interval) {
1082
+ clearInterval(this.interval);
1083
+ this.interval = null;
1084
+ }
1085
+ if (process$1.stdin.isTTY) {
1086
+ process$1.stdin.setRawMode(false);
1087
+ process$1.stdin.pause();
1088
+ process$1.stdin.removeAllListeners("data");
1089
+ }
1090
+ this.exitResolve();
1091
+ }
1092
+ waitUntilExit() {
1093
+ return this.exitPromise;
1094
+ }
1095
+ ensureStarFields(w, h) {
1096
+ if (w !== this.cachedWidth || h !== this.cachedHeight) {
1097
+ this.cachedWidth = w;
1098
+ this.cachedHeight = h;
1099
+ const contentStart = Math.max(0, Math.floor((w - CONTENT_WIDTH) / 2) - 8);
1100
+ const contentEnd = contentStart + CONTENT_WIDTH + 16;
1101
+ const remaining = Math.max(0, h - BASE_CONTENT_ROWS);
1102
+ const topHeight = Math.ceil(remaining / 2) - 1;
1103
+ const proximityRows = 8;
1104
+ const shrinkBig = (s, nearContentRow) => {
1105
+ if (!nearContentRow || s.x < contentStart || s.x >= contentEnd) return s;
1106
+ const star = s.char !== "·" ? {
1107
+ ...s,
1108
+ char: "·"
1109
+ } : s;
1110
+ return star.rest === "bright" ? {
1111
+ ...star,
1112
+ rest: "dim"
1113
+ } : star;
1114
+ };
1115
+ this.topStars = generateStarField(w, h, STAR_DENSITY, 42).map((s) => shrinkBig(s, s.y >= topHeight - proximityRows));
1116
+ this.bottomStars = generateStarField(w, h, STAR_DENSITY, 137).map((s) => shrinkBig(s, s.y < proximityRows));
1117
+ this.sideStars = generateStarField(w, BASE_CONTENT_ROWS, STAR_DENSITY, 99);
1118
+ return true;
1119
+ }
1120
+ return false;
1121
+ }
1122
+ render() {
1123
+ const now = Date.now();
1124
+ const w = process$1.stdout.columns || 80;
1125
+ const h = process$1.stdout.rows || 24;
1126
+ const resized = this.ensureStarFields(w, h);
1127
+ const nextCells = buildFrameCells(this.prompt, this.state, this.topStars, this.bottomStars, this.sideStars, now, w, h);
1128
+ if (this.isFirstFrame || resized) {
1129
+ process$1.stdout.write("\x1B[H" + nextCells.map(rowToString).join("\n"));
1130
+ this.isFirstFrame = false;
1131
+ } else {
1132
+ const changes = diffFrames(this.prevCells, nextCells);
1133
+ if (changes.length > 0) process$1.stdout.write(emitDiff(changes));
1134
+ }
1135
+ this.prevCells = nextCells;
1136
+ }
1137
+ };
1138
+ //#endregion
1139
+ //#region src/utils/slugify.ts
1140
+ function slugifyPrompt(prompt) {
1141
+ return `gnhf/${prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20).replace(/-+$/, "")}-${createHash("sha256").update(prompt).digest("hex").slice(0, 6)}`;
1142
+ }
1143
+ //#endregion
1144
+ //#region src/cli.ts
1145
+ function ask(question) {
1146
+ const rl = createInterface({
1147
+ input: process$1.stdin,
1148
+ output: process$1.stderr
1149
+ });
1150
+ return new Promise((resolve) => {
1151
+ rl.question(question, (answer) => {
1152
+ rl.close();
1153
+ resolve(answer.trim().toLowerCase());
1154
+ });
1155
+ });
1156
+ }
1157
+ const program = new Command();
1158
+ program.name("gnhf").description("Before I go to bed, I tell my agents: good night, have fun").version("0.1.0").argument("[prompt]", "The objective for the coding agent").option("--agent <agent>", "Agent to use (claude or codex)", "claude").option("--mock", "", false).action(async (promptArg, options) => {
1159
+ if (options.mock) {
1160
+ const mock = new MockOrchestrator();
1161
+ enterAltScreen();
1162
+ const renderer = new Renderer(mock, "let's minimize app startup latency without sacrificing any functionality");
1163
+ renderer.start();
1164
+ mock.start();
1165
+ await renderer.waitUntilExit();
1166
+ exitAltScreen();
1167
+ return;
1168
+ }
1169
+ let prompt = promptArg;
1170
+ if (!prompt && !process$1.stdin.isTTY) prompt = readFileSync("/dev/stdin", "utf-8").trim();
1171
+ const agentName = options.agent;
1172
+ if (agentName !== "claude" && agentName !== "codex") {
1173
+ console.error(`Unknown agent: ${options.agent}. Use "claude" or "codex".`);
1174
+ process$1.exit(1);
1175
+ }
1176
+ const config = loadConfig({ agent: agentName });
1177
+ const cwd = process$1.cwd();
1178
+ const currentBranch = getCurrentBranch(cwd);
1179
+ const onGnhfBranch = currentBranch.startsWith("gnhf/");
1180
+ let runInfo;
1181
+ let startIteration = 0;
1182
+ if (onGnhfBranch) {
1183
+ const existingRunId = currentBranch.slice(5);
1184
+ const existing = resumeRun(existingRunId, cwd);
1185
+ const existingPrompt = readFileSync(existing.promptPath, "utf-8");
1186
+ if (!prompt || prompt === existingPrompt) {
1187
+ prompt = existingPrompt;
1188
+ runInfo = existing;
1189
+ startIteration = getLastIterationNumber(existing);
1190
+ } else {
1191
+ 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]: `);
1192
+ if (answer === "o") {
1193
+ ensureCleanWorkingTree(cwd);
1194
+ runInfo = setupRun(existingRunId, prompt, cwd);
1195
+ commitAll(`gnhf: overwrite run ${existingRunId}`, cwd);
1196
+ } else if (answer === "n") {
1197
+ ensureCleanWorkingTree(cwd);
1198
+ const branchName = slugifyPrompt(prompt);
1199
+ createBranch(branchName, cwd);
1200
+ const runId = branchName.split("/")[1];
1201
+ runInfo = setupRun(runId, prompt, cwd);
1202
+ commitAll(`gnhf: initialize run ${runId}`, cwd);
1203
+ } else process$1.exit(0);
1204
+ }
1205
+ } else {
1206
+ if (!prompt) {
1207
+ program.help();
1208
+ return;
1209
+ }
1210
+ ensureCleanWorkingTree(cwd);
1211
+ const branchName = slugifyPrompt(prompt);
1212
+ createBranch(branchName, cwd);
1213
+ const runId = branchName.split("/")[1];
1214
+ runInfo = setupRun(runId, prompt, cwd);
1215
+ commitAll(`gnhf: initialize run ${runId}`, cwd);
1216
+ }
1217
+ const orchestrator = new Orchestrator(config, createAgent(agentName, runInfo), runInfo, prompt, cwd, startIteration);
1218
+ enterAltScreen();
1219
+ const renderer = new Renderer(orchestrator, prompt);
1220
+ renderer.start();
1221
+ orchestrator.start().catch((err) => {
1222
+ renderer.stop();
1223
+ exitAltScreen();
1224
+ die(err instanceof Error ? err.message : String(err));
1225
+ });
1226
+ await renderer.waitUntilExit();
1227
+ exitAltScreen();
1228
+ });
1229
+ function enterAltScreen() {
1230
+ process$1.stdout.write("\x1B[?1049h");
1231
+ process$1.stdout.write("\x1B[?25l");
1232
+ }
1233
+ function exitAltScreen() {
1234
+ process$1.stdout.write("\x1B[?25h");
1235
+ process$1.stdout.write("\x1B[?1049l");
1236
+ }
1237
+ function die(message) {
1238
+ console.error(`\n gnhf: ${message}\n`);
1239
+ process$1.exit(1);
1240
+ }
1241
+ try {
1242
+ await program.parseAsync();
1243
+ } catch (err) {
1244
+ die(err instanceof Error ? err.message : String(err));
1245
+ }
1246
+ //#endregion
1247
+ export {};