gnhf 0.1.11 → 0.1.13

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 (3) hide show
  1. package/README.md +3 -5
  2. package/dist/cli.mjs +731 -82
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -128,7 +128,7 @@ npm link
128
128
  ```
129
129
 
130
130
  - **Incremental commits** — each successful iteration is a separate git commit, so you can cherry-pick or revert individual changes
131
- - **Runtime caps** — `--max-iterations` stops before the next iteration begins, while `--max-tokens` can abort mid-iteration once reported usage reaches the cap; uncommitted work is rolled back in either case
131
+ - **Runtime caps** — `--max-iterations` stops before the next iteration begins, while `--max-tokens` can abort mid-iteration once reported usage reaches the cap; uncommitted work is rolled back in either case, and in the interactive TUI the final state remains visible until you press Ctrl+C to exit
132
132
  - **Shared memory** — the agent reads `notes.md` (built up from prior iterations) to communicate across iterations
133
133
  - **Local run metadata** — gnhf stores prompt, notes, and resume metadata under `.gnhf/runs/` and ignores it locally, so your branch only contains intentional work
134
134
  - **Resume support** — run `gnhf` while on an existing `gnhf/` branch to pick up where a previous run left off
@@ -192,11 +192,9 @@ When sleep prevention is enabled, `gnhf` uses the native mechanism for your OS:
192
192
 
193
193
  ## Debug Logs
194
194
 
195
- Set `GNHF_DEBUG_LOG_PATH` to capture lifecycle events as JSONL while debugging a run:
195
+ Every run writes a JSONL debug log to `.gnhf/runs/<runId>/gnhf.log` alongside `notes.md`. Lifecycle events for the orchestrator, agent, and HTTP requests are captured with elapsed timings and (for failures) the full `error.cause` chain — which is what you need to tell a bare `TypeError: fetch failed` apart from an undici `UND_ERR_HEADERS_TIMEOUT`. The agent's own streaming output still goes to the per-iteration `iteration-<n>.jsonl` file next to it.
196
196
 
197
- ```sh
198
- GNHF_DEBUG_LOG_PATH=/tmp/gnhf-debug.jsonl gnhf "ship it"
199
- ```
197
+ Including a snippet of `gnhf.log` is the single most useful thing you can attach when filing an issue.
200
198
 
201
199
  ## Agents
202
200
 
package/dist/cli.mjs CHANGED
@@ -135,18 +135,111 @@ function loadConfig(overrides) {
135
135
  }
136
136
  //#endregion
137
137
  //#region src/core/debug-log.ts
138
- function appendDebugLog(event, details = {}) {
139
- const logPath = process.env.GNHF_DEBUG_LOG_PATH;
140
- if (!logPath) return;
138
+ const PRE_INIT_BUFFER_CAPACITY = 1e3;
139
+ const STACK_LINE_LIMIT = 12;
140
+ const CAUSE_DEPTH_LIMIT = 6;
141
+ let logPath = null;
142
+ let preInitBuffer = [];
143
+ let preInitDroppedCount = 0;
144
+ function formatLine(event, details) {
145
+ const base = {
146
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
147
+ pid: process.pid,
148
+ event
149
+ };
141
150
  try {
142
- appendFileSync(logPath, `${JSON.stringify({
143
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
144
- pid: process.pid,
145
- event,
151
+ return `${JSON.stringify({
152
+ ...base,
146
153
  ...details
147
- })}\n`, "utf-8");
154
+ })}\n`;
155
+ } catch (error) {
156
+ return `${JSON.stringify({
157
+ ...base,
158
+ logError: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
159
+ detailsKeys: Object.keys(details)
160
+ })}\n`;
161
+ }
162
+ }
163
+ function initDebugLog(path) {
164
+ logPath = path;
165
+ try {
166
+ mkdirSync(dirname(path), { recursive: true });
167
+ } catch {}
168
+ if (preInitBuffer.length === 0 && preInitDroppedCount === 0) return;
169
+ const flushed = (preInitDroppedCount > 0 ? formatLine("debug-log:pre-init-overflow", {
170
+ droppedCount: preInitDroppedCount,
171
+ bufferCapacity: PRE_INIT_BUFFER_CAPACITY
172
+ }) : "") + preInitBuffer.join("");
173
+ preInitBuffer = [];
174
+ preInitDroppedCount = 0;
175
+ try {
176
+ appendFileSync(path, flushed, "utf-8");
177
+ } catch {}
178
+ }
179
+ function appendDebugLog(event, details = {}) {
180
+ const line = formatLine(event, details);
181
+ if (logPath === null) {
182
+ preInitBuffer.push(line);
183
+ if (preInitBuffer.length > PRE_INIT_BUFFER_CAPACITY) {
184
+ preInitBuffer.shift();
185
+ preInitDroppedCount += 1;
186
+ }
187
+ return;
188
+ }
189
+ try {
190
+ appendFileSync(logPath, line, "utf-8");
148
191
  } catch {}
149
192
  }
193
+ /**
194
+ * Serialize an error (including its cause chain) to a plain object that's
195
+ * safe to embed in a JSONL log line. This is critical for diagnosing
196
+ * fetch failures, where the surface message is often just "fetch failed"
197
+ * but `err.cause` holds the real undici error (e.g. UND_ERR_HEADERS_TIMEOUT).
198
+ *
199
+ * Contract: must never throw. Callers invoke this inside catch blocks and
200
+ * timer/EventEmitter handlers, and a throw here would mask the original
201
+ * error or crash the process.
202
+ */
203
+ function serializeError(error, depth = 0) {
204
+ try {
205
+ return serializeErrorUnsafe(error, depth);
206
+ } catch (serializationError) {
207
+ return {
208
+ value: "[serialization failed]",
209
+ serializationError: serializationError instanceof Error ? `${serializationError.name}: ${serializationError.message}` : String(serializationError)
210
+ };
211
+ }
212
+ }
213
+ function tryRead(read) {
214
+ try {
215
+ return read();
216
+ } catch {
217
+ return;
218
+ }
219
+ }
220
+ function serializeErrorUnsafe(error, depth) {
221
+ if (depth > CAUSE_DEPTH_LIMIT) return { value: "[cause chain truncated]" };
222
+ if (error instanceof Error) {
223
+ const result = {
224
+ name: tryRead(() => error.name) ?? "Error",
225
+ message: tryRead(() => error.message) ?? ""
226
+ };
227
+ const code = tryRead(() => error.code);
228
+ if (typeof code === "string" || typeof code === "number") result.code = code;
229
+ const stack = tryRead(() => error.stack);
230
+ if (typeof stack === "string") result.stack = stack.split("\n").slice(0, STACK_LINE_LIMIT).join("\n");
231
+ const cause = tryRead(() => "cause" in error ? error.cause : void 0);
232
+ if (cause !== void 0) result.cause = serializeError(cause, depth + 1);
233
+ return result;
234
+ }
235
+ if (error === null || error === void 0) return { value: String(error) };
236
+ if (typeof error === "object") try {
237
+ return { value: JSON.parse(JSON.stringify(error)) };
238
+ } catch {
239
+ return { value: String(error) };
240
+ }
241
+ return { value: String(error) };
242
+ }
150
243
  //#endregion
151
244
  //#region src/core/git.ts
152
245
  const NOT_GIT_REPOSITORY_MESSAGE = "This command must be run inside a Git repository. Change into a repo or run \"git init\" first.";
@@ -255,6 +348,7 @@ const AGENT_OUTPUT_SCHEMA = {
255
348
  };
256
349
  //#endregion
257
350
  //#region src/core/run.ts
351
+ const LOG_FILENAME = "gnhf.log";
258
352
  function writeSchemaFile(schemaPath) {
259
353
  writeFileSync(schemaPath, JSON.stringify(AGENT_OUTPUT_SCHEMA, null, 2), "utf-8");
260
354
  }
@@ -286,6 +380,7 @@ function setupRun(runId, prompt, baseCommit, cwd) {
286
380
  writeFileSync(notesPath, `# gnhf run: ${runId}\n\nObjective: ${prompt}\n\n## Iteration Log\n`, "utf-8");
287
381
  const schemaPath = join(runDir, "output-schema.json");
288
382
  writeSchemaFile(schemaPath);
383
+ const logPath = join(runDir, LOG_FILENAME);
289
384
  const baseCommitPath = join(runDir, "base-commit");
290
385
  const hasStoredBaseCommit = existsSync(baseCommitPath);
291
386
  const resolvedBaseCommit = hasStoredBaseCommit ? readFileSync(baseCommitPath, "utf-8").trim() : baseCommit;
@@ -296,6 +391,7 @@ function setupRun(runId, prompt, baseCommit, cwd) {
296
391
  promptPath,
297
392
  notesPath,
298
393
  schemaPath,
394
+ logPath,
299
395
  baseCommit: resolvedBaseCommit,
300
396
  baseCommitPath
301
397
  };
@@ -307,6 +403,7 @@ function resumeRun(runId, cwd) {
307
403
  const notesPath = join(runDir, "notes.md");
308
404
  const schemaPath = join(runDir, "output-schema.json");
309
405
  writeSchemaFile(schemaPath);
406
+ const logPath = join(runDir, LOG_FILENAME);
310
407
  const baseCommitPath = join(runDir, "base-commit");
311
408
  return {
312
409
  runId,
@@ -314,6 +411,7 @@ function resumeRun(runId, cwd) {
314
411
  promptPath,
315
412
  notesPath,
316
413
  schemaPath,
414
+ logPath,
317
415
  baseCommit: existsSync(baseCommitPath) ? readFileSync(baseCommitPath, "utf-8").trim() : backfillLegacyBaseCommit(runId, baseCommitPath, cwd),
318
416
  baseCommitPath
319
417
  };
@@ -1179,20 +1277,48 @@ var OpenCodeAgent = class {
1179
1277
  const logStream = logPath ? createWriteStream(logPath) : null;
1180
1278
  const runController = new AbortController();
1181
1279
  let sessionId = null;
1280
+ const runStartedAt = Date.now();
1281
+ appendDebugLog("opencode:run:start", {
1282
+ cwd,
1283
+ promptLength: prompt.length,
1284
+ hasLogPath: logPath !== void 0
1285
+ });
1182
1286
  const onAbort = () => {
1183
1287
  runController.abort();
1184
1288
  };
1185
1289
  if (signal?.aborted) {
1186
1290
  logStream?.end();
1291
+ appendDebugLog("opencode:run:aborted-early", {});
1187
1292
  throw createAbortError$1();
1188
1293
  }
1189
1294
  signal?.addEventListener("abort", onAbort, { once: true });
1190
1295
  try {
1191
1296
  const server = await this.ensureServer(cwd, runController.signal);
1192
1297
  sessionId = await this.createSession(server, cwd, runController.signal);
1193
- return await this.streamMessage(server, sessionId, buildPrompt(prompt), runController.signal, logStream, onUsage, onMessage);
1298
+ const result = await this.streamMessage(server, sessionId, buildPrompt(prompt), runController.signal, logStream, onUsage, onMessage);
1299
+ appendDebugLog("opencode:run:end", {
1300
+ sessionId,
1301
+ elapsedMs: Date.now() - runStartedAt,
1302
+ inputTokens: result.usage.inputTokens,
1303
+ outputTokens: result.usage.outputTokens
1304
+ });
1305
+ return result;
1194
1306
  } catch (error) {
1195
- if (runController.signal.aborted || isAbortError$1(error)) throw createAbortError$1();
1307
+ if (runController.signal.aborted || isAbortError$1(error)) {
1308
+ appendDebugLog("opencode:run:aborted", {
1309
+ sessionId,
1310
+ elapsedMs: Date.now() - runStartedAt
1311
+ });
1312
+ throw createAbortError$1();
1313
+ }
1314
+ appendDebugLog("opencode:run:error", {
1315
+ sessionId,
1316
+ elapsedMs: Date.now() - runStartedAt,
1317
+ error: serializeError(error),
1318
+ serverStderr: this.server?.stderr.slice(-2048),
1319
+ serverStdout: this.server?.stdout.slice(-2048),
1320
+ serverClosed: this.server?.closed ?? true
1321
+ });
1196
1322
  throw error;
1197
1323
  } finally {
1198
1324
  signal?.removeEventListener("abort", onAbort);
@@ -1249,25 +1375,82 @@ var OpenCodeAgent = class {
1249
1375
  stdout: ""
1250
1376
  };
1251
1377
  const maxOutput = 64 * 1024;
1378
+ const maxMirroredLineLength = 2048;
1379
+ const maxMirroredLinesPerRun = 500;
1380
+ let mirroredLineCount = 0;
1381
+ let mirroredSuppressionLogged = false;
1382
+ const stderrLineBuffer = { tail: "" };
1383
+ const mirrorStderrChunk = (chunk) => {
1384
+ stderrLineBuffer.tail += chunk;
1385
+ let newlineIndex = stderrLineBuffer.tail.indexOf("\n");
1386
+ while (newlineIndex !== -1) {
1387
+ const rawLine = stderrLineBuffer.tail.slice(0, newlineIndex);
1388
+ stderrLineBuffer.tail = stderrLineBuffer.tail.slice(newlineIndex + 1);
1389
+ const line = rawLine.replace(/\r$/, "");
1390
+ if (line.length > 0) if (mirroredLineCount >= maxMirroredLinesPerRun) {
1391
+ if (!mirroredSuppressionLogged) {
1392
+ appendDebugLog("opencode:server:stderr:suppressed", {
1393
+ port: server.port,
1394
+ cap: maxMirroredLinesPerRun
1395
+ });
1396
+ mirroredSuppressionLogged = true;
1397
+ }
1398
+ } else {
1399
+ mirroredLineCount += 1;
1400
+ appendDebugLog("opencode:server:stderr", {
1401
+ port: server.port,
1402
+ line: line.length > maxMirroredLineLength ? `${line.slice(0, maxMirroredLineLength)}…` : line,
1403
+ truncated: line.length > maxMirroredLineLength
1404
+ });
1405
+ }
1406
+ newlineIndex = stderrLineBuffer.tail.indexOf("\n");
1407
+ }
1408
+ };
1252
1409
  child.stdout.on("data", (data) => {
1253
1410
  server.stdout += data.toString();
1254
1411
  if (server.stdout.length > maxOutput) server.stdout = server.stdout.slice(-maxOutput);
1255
1412
  });
1256
1413
  child.stderr.on("data", (data) => {
1257
- server.stderr += data.toString();
1414
+ const text = data.toString();
1415
+ server.stderr += text;
1258
1416
  if (server.stderr.length > maxOutput) server.stderr = server.stderr.slice(-maxOutput);
1417
+ mirrorStderrChunk(text);
1259
1418
  });
1260
- child.on("close", () => {
1419
+ child.on("close", (code, closeSignal) => {
1261
1420
  server.closed = true;
1421
+ if (stderrLineBuffer.tail.length > 0) mirrorStderrChunk("\n");
1422
+ appendDebugLog("opencode:server:close", {
1423
+ cwd: server.cwd,
1424
+ port: server.port,
1425
+ code,
1426
+ signal: closeSignal,
1427
+ stderr: server.stderr.slice(-2048),
1428
+ stdout: server.stdout.slice(-2048)
1429
+ });
1262
1430
  if (this.server === server) this.server = null;
1263
1431
  });
1264
1432
  this.server = server;
1433
+ const spawnedAt = Date.now();
1265
1434
  appendDebugLog("opencode:spawn", {
1266
1435
  cwd,
1267
1436
  port,
1268
- detached
1437
+ detached,
1438
+ pid: child.pid,
1439
+ bin: this.bin
1269
1440
  });
1270
- server.readyPromise = this.waitForHealthy(server, signal).catch(async (error) => {
1441
+ server.readyPromise = this.waitForHealthy(server, signal).then(() => {
1442
+ appendDebugLog("opencode:server:ready", {
1443
+ port,
1444
+ elapsedMs: Date.now() - spawnedAt
1445
+ });
1446
+ }).catch(async (error) => {
1447
+ appendDebugLog("opencode:server:ready-failed", {
1448
+ port,
1449
+ elapsedMs: Date.now() - spawnedAt,
1450
+ error: serializeError(error),
1451
+ stderr: server.stderr.slice(-2048),
1452
+ stdout: server.stdout.slice(-2048)
1453
+ });
1271
1454
  await this.shutdownServer();
1272
1455
  throw error;
1273
1456
  });
@@ -1300,44 +1483,70 @@ var OpenCodeAgent = class {
1300
1483
  throw new Error(`Timed out waiting for opencode serve to become ready on port ${server.port}`);
1301
1484
  }
1302
1485
  async createSession(server, cwd, signal) {
1303
- return (await this.requestJSON(server, "/session", {
1486
+ const response = await this.requestJSON(server, "/session", {
1304
1487
  method: "POST",
1305
1488
  body: {
1306
1489
  directory: cwd,
1307
1490
  permission: BLANKET_PERMISSION_RULESET
1308
1491
  },
1309
1492
  signal
1310
- })).id;
1493
+ });
1494
+ appendDebugLog("opencode:session:create", { sessionId: response.id });
1495
+ return response.id;
1311
1496
  }
1312
1497
  async streamMessage(server, sessionId, prompt, signal, logStream, onUsage, onMessage) {
1313
1498
  const streamAbortController = new AbortController();
1314
1499
  const streamSignal = AbortSignal.any([signal, streamAbortController.signal]);
1500
+ const streamStartedAt = Date.now();
1501
+ appendDebugLog("opencode:stream:start", { sessionId });
1315
1502
  const eventResponse = await this.request(server, "/global/event", {
1316
1503
  method: "GET",
1317
1504
  headers: { accept: "text/event-stream" },
1318
1505
  signal: streamSignal
1319
1506
  });
1320
- if (!eventResponse.body) throw new Error("opencode returned no event stream body");
1507
+ if (!eventResponse.body) {
1508
+ appendDebugLog("opencode:stream:no-body", { sessionId });
1509
+ throw new Error("opencode returned no event stream body");
1510
+ }
1511
+ const messagePostStartedAt = Date.now();
1512
+ appendDebugLog("opencode:message-post:start", {
1513
+ sessionId,
1514
+ promptLength: prompt.length
1515
+ });
1321
1516
  let messageRequestError = null;
1322
1517
  const messageRequest = (async () => {
1323
1518
  try {
1519
+ const body = await this.requestText(server, `/session/${sessionId}/message`, {
1520
+ method: "POST",
1521
+ body: {
1522
+ role: "user",
1523
+ parts: [{
1524
+ type: "text",
1525
+ text: prompt
1526
+ }],
1527
+ format: STRUCTURED_OUTPUT_FORMAT
1528
+ },
1529
+ signal
1530
+ });
1531
+ appendDebugLog("opencode:message-post:end", {
1532
+ sessionId,
1533
+ elapsedMs: Date.now() - messagePostStartedAt,
1534
+ bodyLength: body.length
1535
+ });
1324
1536
  return {
1325
1537
  ok: true,
1326
- body: await this.requestText(server, `/session/${sessionId}/message`, {
1327
- method: "POST",
1328
- body: {
1329
- role: "user",
1330
- parts: [{
1331
- type: "text",
1332
- text: prompt
1333
- }],
1334
- format: STRUCTURED_OUTPUT_FORMAT
1335
- },
1336
- signal
1337
- })
1538
+ body
1338
1539
  };
1339
1540
  } catch (error) {
1340
1541
  messageRequestError = error;
1542
+ appendDebugLog("opencode:message-post:error", {
1543
+ sessionId,
1544
+ elapsedMs: Date.now() - messagePostStartedAt,
1545
+ error: serializeError(error),
1546
+ serverClosed: server.closed,
1547
+ serverStderr: server.stderr.slice(-2048),
1548
+ streamTelemetry: buildTelemetry()
1549
+ });
1341
1550
  streamAbortController.abort();
1342
1551
  return {
1343
1552
  ok: false,
@@ -1356,6 +1565,66 @@ var OpenCodeAgent = class {
1356
1565
  let lastText = null;
1357
1566
  let lastFinalAnswerText = null;
1358
1567
  let lastUsageSignature = "0:0:0:0";
1568
+ const eventCounts = {};
1569
+ let firstEventAtMs = null;
1570
+ let lastEventAtMs = null;
1571
+ let lastHeartbeatAtMs = null;
1572
+ const phaseTransitions = [];
1573
+ let currentPhase = null;
1574
+ const noteEvent = (type) => {
1575
+ const key = type ?? "unknown";
1576
+ eventCounts[key] = (eventCounts[key] ?? 0) + 1;
1577
+ const nowMs = Date.now() - streamStartedAt;
1578
+ if (type === "server.heartbeat") {
1579
+ lastHeartbeatAtMs = nowMs;
1580
+ return;
1581
+ }
1582
+ if (firstEventAtMs === null) firstEventAtMs = nowMs;
1583
+ lastEventAtMs = nowMs;
1584
+ };
1585
+ const notePhase = (phase) => {
1586
+ if (!phase || phase === currentPhase) return;
1587
+ currentPhase = phase;
1588
+ phaseTransitions.push({
1589
+ phase,
1590
+ atMs: Date.now() - streamStartedAt
1591
+ });
1592
+ };
1593
+ const buildTelemetry = () => ({
1594
+ eventCounts: { ...eventCounts },
1595
+ firstEventAtMs,
1596
+ lastEventAtMs,
1597
+ lastHeartbeatAtMs,
1598
+ msSinceLastEvent: lastEventAtMs === null ? null : Date.now() - streamStartedAt - lastEventAtMs,
1599
+ phaseTransitions: [...phaseTransitions],
1600
+ currentPhase,
1601
+ sawSessionIdle: (eventCounts["session.idle"] ?? 0) > 0
1602
+ });
1603
+ const STALL_THRESHOLDS_MS = [
1604
+ 6e4,
1605
+ 12e4,
1606
+ 24e4,
1607
+ 48e4
1608
+ ];
1609
+ let nextStallThresholdIndex = 0;
1610
+ const stallTimer = setInterval(() => {
1611
+ if (nextStallThresholdIndex >= STALL_THRESHOLDS_MS.length) return;
1612
+ const threshold = STALL_THRESHOLDS_MS[nextStallThresholdIndex];
1613
+ const referencePointMs = lastEventAtMs ?? firstEventAtMs ?? 0;
1614
+ const silenceMs = Date.now() - streamStartedAt - referencePointMs;
1615
+ if (silenceMs < threshold) return;
1616
+ nextStallThresholdIndex += 1;
1617
+ appendDebugLog("opencode:stream:stall", {
1618
+ sessionId,
1619
+ thresholdMs: threshold,
1620
+ silenceMs,
1621
+ currentPhase,
1622
+ lastEventAtMs,
1623
+ lastHeartbeatAtMs,
1624
+ eventCounts: { ...eventCounts }
1625
+ });
1626
+ }, 15e3);
1627
+ stallTimer.unref?.();
1359
1628
  const updateUsage = (messageId, tokens) => {
1360
1629
  if (!messageId || !tokens) return;
1361
1630
  usageByMessageId.set(messageId, toUsage(tokens));
@@ -1390,6 +1659,7 @@ var OpenCodeAgent = class {
1390
1659
  text: nextText,
1391
1660
  phase
1392
1661
  });
1662
+ notePhase(phase);
1393
1663
  if (!trimmed) return;
1394
1664
  lastText = nextText;
1395
1665
  if (phase === "final_answer") lastFinalAnswerText = nextText;
@@ -1432,7 +1702,9 @@ var OpenCodeAgent = class {
1432
1702
  const dataLines = rawEvent.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart());
1433
1703
  if (dataLines.length === 0) return;
1434
1704
  try {
1435
- if (handleEvent(JSON.parse(dataLines.join("\n")))) sawSessionIdle = true;
1705
+ const event = JSON.parse(dataLines.join("\n"));
1706
+ noteEvent(event.payload?.type);
1707
+ if (handleEvent(event)) sawSessionIdle = true;
1436
1708
  } catch {}
1437
1709
  };
1438
1710
  const processBufferedEvents = (flushRemainder = false) => {
@@ -1458,6 +1730,7 @@ var OpenCodeAgent = class {
1458
1730
  buffer = "";
1459
1731
  }
1460
1732
  };
1733
+ let bytesRead = 0;
1461
1734
  try {
1462
1735
  while (!sawSessionIdle) {
1463
1736
  let readResult;
@@ -1465,9 +1738,27 @@ var OpenCodeAgent = class {
1465
1738
  readResult = await reader.read();
1466
1739
  } catch (error) {
1467
1740
  if (messageRequestError) {
1741
+ appendDebugLog("opencode:stream:error", {
1742
+ sessionId,
1743
+ elapsedMs: Date.now() - streamStartedAt,
1744
+ bytesRead,
1745
+ reason: "message-post-failed",
1746
+ error: serializeError(messageRequestError),
1747
+ telemetry: buildTelemetry()
1748
+ });
1468
1749
  if (isAbortError$1(messageRequestError) || isAgentAbortError(messageRequestError)) throw createAbortError$1();
1469
1750
  throw messageRequestError;
1470
1751
  }
1752
+ appendDebugLog("opencode:stream:error", {
1753
+ sessionId,
1754
+ elapsedMs: Date.now() - streamStartedAt,
1755
+ bytesRead,
1756
+ reason: "reader-read-failed",
1757
+ error: serializeError(error),
1758
+ serverClosed: server.closed,
1759
+ serverStderr: server.stderr.slice(-2048),
1760
+ telemetry: buildTelemetry()
1761
+ });
1471
1762
  if (isAbortError$1(error)) throw createAbortError$1();
1472
1763
  throw error;
1473
1764
  }
@@ -1476,6 +1767,7 @@ var OpenCodeAgent = class {
1476
1767
  if (tail) {
1477
1768
  logStream?.write(tail);
1478
1769
  buffer += tail;
1770
+ bytesRead += tail.length;
1479
1771
  }
1480
1772
  processBufferedEvents(true);
1481
1773
  break;
@@ -1483,12 +1775,21 @@ var OpenCodeAgent = class {
1483
1775
  const chunk = decoder.decode(readResult.value, { stream: true });
1484
1776
  logStream?.write(chunk);
1485
1777
  buffer += chunk;
1778
+ bytesRead += chunk.length;
1486
1779
  processBufferedEvents();
1487
1780
  }
1488
1781
  } finally {
1782
+ clearInterval(stallTimer);
1489
1783
  streamAbortController.abort();
1490
1784
  await reader.cancel().catch(() => void 0);
1491
1785
  }
1786
+ appendDebugLog("opencode:stream:end", {
1787
+ sessionId,
1788
+ elapsedMs: Date.now() - streamStartedAt,
1789
+ bytesRead,
1790
+ sawSessionIdle,
1791
+ telemetry: buildTelemetry()
1792
+ });
1492
1793
  const messageResult = await messageRequest;
1493
1794
  if (!messageResult.ok) {
1494
1795
  if (isAbortError$1(messageResult.error) || isAgentAbortError(messageResult.error)) throw createAbortError$1();
@@ -1499,6 +1800,12 @@ var OpenCodeAgent = class {
1499
1800
  try {
1500
1801
  response = JSON.parse(body);
1501
1802
  } catch (error) {
1803
+ appendDebugLog("opencode:response:parse-error", {
1804
+ sessionId,
1805
+ bodyLength: body.length,
1806
+ bodySample: body.slice(0, 512),
1807
+ error: serializeError(error)
1808
+ });
1502
1809
  throw new Error(`Failed to parse opencode response: ${error instanceof Error ? error.message : String(error)}`);
1503
1810
  }
1504
1811
  if (response.info?.role === "assistant") updateUsage(response.info.id, response.info.tokens);
@@ -1508,18 +1815,43 @@ var OpenCodeAgent = class {
1508
1815
  lastText = part.text;
1509
1816
  if (part.metadata?.openai?.phase === "final_answer") lastFinalAnswerText = part.text;
1510
1817
  }
1511
- if (response.info?.structured) return {
1512
- output: response.info.structured,
1513
- usage
1514
- };
1818
+ if (response.info?.structured) {
1819
+ appendDebugLog("opencode:output:structured", {
1820
+ sessionId,
1821
+ source: "response.info.structured"
1822
+ });
1823
+ return {
1824
+ output: response.info.structured,
1825
+ usage
1826
+ };
1827
+ }
1515
1828
  const outputText = lastFinalAnswerText ?? lastText;
1516
- if (!outputText) throw new Error("opencode returned no text output");
1829
+ if (!outputText) {
1830
+ appendDebugLog("opencode:output:missing", {
1831
+ sessionId,
1832
+ hasInfo: response.info !== void 0,
1833
+ partCount: response.parts?.length ?? 0
1834
+ });
1835
+ throw new Error("opencode returned no text output");
1836
+ }
1517
1837
  try {
1838
+ const output = JSON.parse(outputText);
1839
+ appendDebugLog("opencode:output:structured", {
1840
+ sessionId,
1841
+ source: lastFinalAnswerText ? "final_answer" : "last_text",
1842
+ outputTextLength: outputText.length
1843
+ });
1518
1844
  return {
1519
- output: JSON.parse(outputText),
1845
+ output,
1520
1846
  usage
1521
1847
  };
1522
1848
  } catch (error) {
1849
+ appendDebugLog("opencode:output:parse-error", {
1850
+ sessionId,
1851
+ outputTextLength: outputText.length,
1852
+ outputTextSample: outputText.slice(0, 512),
1853
+ error: serializeError(error)
1854
+ });
1523
1855
  throw new Error(`Failed to parse opencode output: ${error instanceof Error ? error.message : String(error)}`);
1524
1856
  }
1525
1857
  }
@@ -1529,7 +1861,13 @@ var OpenCodeAgent = class {
1529
1861
  method: "DELETE",
1530
1862
  timeoutMs: 1e3
1531
1863
  });
1532
- } catch {}
1864
+ appendDebugLog("opencode:session:delete", { sessionId });
1865
+ } catch (error) {
1866
+ appendDebugLog("opencode:session:delete-failed", {
1867
+ sessionId,
1868
+ error: serializeError(error)
1869
+ });
1870
+ }
1533
1871
  }
1534
1872
  async abortSession(server, sessionId) {
1535
1873
  try {
@@ -1537,7 +1875,13 @@ var OpenCodeAgent = class {
1537
1875
  method: "POST",
1538
1876
  timeoutMs: 1e3
1539
1877
  });
1540
- } catch {}
1878
+ appendDebugLog("opencode:session:abort", { sessionId });
1879
+ } catch (error) {
1880
+ appendDebugLog("opencode:session:abort-failed", {
1881
+ sessionId,
1882
+ error: serializeError(error)
1883
+ });
1884
+ }
1541
1885
  }
1542
1886
  async shutdownServer() {
1543
1887
  if (!this.server || this.server.closed) {
@@ -1549,9 +1893,11 @@ var OpenCodeAgent = class {
1549
1893
  return;
1550
1894
  }
1551
1895
  const server = this.server;
1896
+ const shutdownStartedAt = Date.now();
1552
1897
  appendDebugLog("opencode:shutdown", {
1553
1898
  cwd: server.cwd,
1554
- port: server.port
1899
+ port: server.port,
1900
+ pid: server.child.pid
1555
1901
  });
1556
1902
  this.closingPromise = (this.platform === "win32" && server.child.pid ? killWindowsProcessTree(server.child.pid) : shutdownChildProcess(server.child, {
1557
1903
  detached: server.detached,
@@ -1560,6 +1906,10 @@ var OpenCodeAgent = class {
1560
1906
  })).finally(() => {
1561
1907
  if (this.server === server) this.server = null;
1562
1908
  this.closingPromise = null;
1909
+ appendDebugLog("opencode:shutdown:done", {
1910
+ port: server.port,
1911
+ elapsedMs: Date.now() - shutdownStartedAt
1912
+ });
1563
1913
  });
1564
1914
  await this.closingPromise;
1565
1915
  }
@@ -1574,14 +1924,35 @@ var OpenCodeAgent = class {
1574
1924
  const headers = new Headers(options.headers);
1575
1925
  if (options.body !== void 0) headers.set("content-type", "application/json");
1576
1926
  const signal = withTimeoutSignal$1(options.signal, options.timeoutMs);
1577
- const response = await this.fetchFn(`${server.baseUrl}${path}`, {
1578
- method: options.method,
1579
- headers,
1580
- body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
1581
- signal
1582
- });
1927
+ const startedAt = Date.now();
1928
+ let response;
1929
+ try {
1930
+ response = await this.fetchFn(`${server.baseUrl}${path}`, {
1931
+ method: options.method,
1932
+ headers,
1933
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
1934
+ signal
1935
+ });
1936
+ } catch (error) {
1937
+ appendDebugLog("opencode:request:error", {
1938
+ method: options.method,
1939
+ path,
1940
+ elapsedMs: Date.now() - startedAt,
1941
+ timeoutMs: options.timeoutMs,
1942
+ error: serializeError(error),
1943
+ serverClosed: server.closed
1944
+ });
1945
+ throw error;
1946
+ }
1583
1947
  if (!response.ok) {
1584
1948
  const body = await response.text();
1949
+ appendDebugLog("opencode:request:non-ok", {
1950
+ method: options.method,
1951
+ path,
1952
+ status: response.status,
1953
+ elapsedMs: Date.now() - startedAt,
1954
+ bodySample: body.slice(0, 1024)
1955
+ });
1585
1956
  throw new Error(`opencode ${options.method} ${path} failed with ${response.status}: ${body}`);
1586
1957
  }
1587
1958
  return response;
@@ -1706,11 +2077,18 @@ var RovoDevAgent = class {
1706
2077
  const logStream = logPath ? createWriteStream(logPath) : null;
1707
2078
  const runController = new AbortController();
1708
2079
  let sessionId = null;
2080
+ const runStartedAt = Date.now();
2081
+ appendDebugLog("rovodev:run:start", {
2082
+ cwd,
2083
+ promptLength: prompt.length,
2084
+ hasLogPath: logPath !== void 0
2085
+ });
1709
2086
  const onAbort = () => {
1710
2087
  runController.abort();
1711
2088
  };
1712
2089
  if (signal?.aborted) {
1713
2090
  logStream?.end();
2091
+ appendDebugLog("rovodev:run:aborted-early", {});
1714
2092
  throw createAbortError();
1715
2093
  }
1716
2094
  signal?.addEventListener("abort", onAbort, { once: true });
@@ -1719,9 +2097,30 @@ var RovoDevAgent = class {
1719
2097
  sessionId = await this.createSession(server, runController.signal);
1720
2098
  await this.setInlineSystemPrompt(server, sessionId, runController.signal);
1721
2099
  await this.setChatMessage(server, sessionId, prompt, runController.signal);
1722
- return await this.streamChat(server, sessionId, runController.signal, logStream, onUsage, onMessage);
2100
+ const result = await this.streamChat(server, sessionId, runController.signal, logStream, onUsage, onMessage);
2101
+ appendDebugLog("rovodev:run:end", {
2102
+ sessionId,
2103
+ elapsedMs: Date.now() - runStartedAt,
2104
+ inputTokens: result.usage.inputTokens,
2105
+ outputTokens: result.usage.outputTokens
2106
+ });
2107
+ return result;
1723
2108
  } catch (error) {
1724
- if (runController.signal.aborted || isAbortError(error)) throw createAbortError();
2109
+ if (runController.signal.aborted || isAbortError(error)) {
2110
+ appendDebugLog("rovodev:run:aborted", {
2111
+ sessionId,
2112
+ elapsedMs: Date.now() - runStartedAt
2113
+ });
2114
+ throw createAbortError();
2115
+ }
2116
+ appendDebugLog("rovodev:run:error", {
2117
+ sessionId,
2118
+ elapsedMs: Date.now() - runStartedAt,
2119
+ error: serializeError(error),
2120
+ serverStderr: this.server?.stderr.slice(-2048),
2121
+ serverStdout: this.server?.stdout.slice(-2048),
2122
+ serverClosed: this.server?.closed ?? true
2123
+ });
1725
2124
  throw error;
1726
2125
  } finally {
1727
2126
  signal?.removeEventListener("abort", onAbort);
@@ -1779,17 +2178,40 @@ var RovoDevAgent = class {
1779
2178
  server.stderr += data.toString();
1780
2179
  if (server.stderr.length > MAX_OUTPUT) server.stderr = server.stderr.slice(-MAX_OUTPUT);
1781
2180
  });
1782
- child.on("close", () => {
2181
+ child.on("close", (code, closeSignal) => {
1783
2182
  server.closed = true;
2183
+ appendDebugLog("rovodev:server:close", {
2184
+ cwd: server.cwd,
2185
+ port: server.port,
2186
+ code,
2187
+ signal: closeSignal,
2188
+ stderr: server.stderr.slice(-2048),
2189
+ stdout: server.stdout.slice(-2048)
2190
+ });
1784
2191
  if (this.server === server) this.server = null;
1785
2192
  });
1786
2193
  this.server = server;
2194
+ const spawnedAt = Date.now();
1787
2195
  appendDebugLog("rovodev:spawn", {
1788
2196
  cwd,
1789
2197
  port,
1790
- detached
2198
+ detached,
2199
+ pid: child.pid,
2200
+ bin: this.bin
1791
2201
  });
1792
- server.readyPromise = this.waitForHealthy(server, signal).catch(async (error) => {
2202
+ server.readyPromise = this.waitForHealthy(server, signal).then(() => {
2203
+ appendDebugLog("rovodev:server:ready", {
2204
+ port,
2205
+ elapsedMs: Date.now() - spawnedAt
2206
+ });
2207
+ }).catch(async (error) => {
2208
+ appendDebugLog("rovodev:server:ready-failed", {
2209
+ port,
2210
+ elapsedMs: Date.now() - spawnedAt,
2211
+ error: serializeError(error),
2212
+ stderr: server.stderr.slice(-2048),
2213
+ stdout: server.stdout.slice(-2048)
2214
+ });
1793
2215
  await this.shutdownServer();
1794
2216
  throw error;
1795
2217
  });
@@ -1822,11 +2244,13 @@ var RovoDevAgent = class {
1822
2244
  throw new Error(`Timed out waiting for rovodev serve to become ready on port ${server.port}`);
1823
2245
  }
1824
2246
  async createSession(server, signal) {
1825
- return (await this.requestJSON(server, "/v3/sessions/create", {
2247
+ const response = await this.requestJSON(server, "/v3/sessions/create", {
1826
2248
  method: "POST",
1827
2249
  body: { custom_title: "gnhf" },
1828
2250
  signal
1829
- })).session_id;
2251
+ });
2252
+ appendDebugLog("rovodev:session:create", { sessionId: response.session_id });
2253
+ return response.session_id;
1830
2254
  }
1831
2255
  async setInlineSystemPrompt(server, sessionId, signal) {
1832
2256
  const schema = readFileSync(this.schemaPath, "utf-8").trim();
@@ -1852,7 +2276,13 @@ var RovoDevAgent = class {
1852
2276
  sessionId,
1853
2277
  timeoutMs: 1e3
1854
2278
  });
1855
- } catch {}
2279
+ appendDebugLog("rovodev:session:cancel", { sessionId });
2280
+ } catch (error) {
2281
+ appendDebugLog("rovodev:session:cancel-failed", {
2282
+ sessionId,
2283
+ error: serializeError(error)
2284
+ });
2285
+ }
1856
2286
  }
1857
2287
  async deleteSession(server, sessionId) {
1858
2288
  try {
@@ -1861,16 +2291,27 @@ var RovoDevAgent = class {
1861
2291
  sessionId,
1862
2292
  timeoutMs: 1e3
1863
2293
  });
1864
- } catch {}
2294
+ appendDebugLog("rovodev:session:delete", { sessionId });
2295
+ } catch (error) {
2296
+ appendDebugLog("rovodev:session:delete-failed", {
2297
+ sessionId,
2298
+ error: serializeError(error)
2299
+ });
2300
+ }
1865
2301
  }
1866
2302
  async streamChat(server, sessionId, signal, logStream, onUsage, onMessage) {
2303
+ const streamStartedAt = Date.now();
2304
+ appendDebugLog("rovodev:stream:start", { sessionId });
1867
2305
  const response = await this.request(server, "/v3/stream_chat", {
1868
2306
  method: "GET",
1869
2307
  sessionId,
1870
2308
  headers: { accept: "text/event-stream" },
1871
2309
  signal
1872
2310
  });
1873
- if (!response.body) throw new Error("rovodev returned no response body");
2311
+ if (!response.body) {
2312
+ appendDebugLog("rovodev:stream:no-body", { sessionId });
2313
+ throw new Error("rovodev returned no response body");
2314
+ }
1874
2315
  const usage = {
1875
2316
  inputTokens: 0,
1876
2317
  outputTokens: 0,
@@ -1959,11 +2400,20 @@ var RovoDevAgent = class {
1959
2400
  }
1960
2401
  }
1961
2402
  };
2403
+ let bytesRead = 0;
1962
2404
  while (true) {
1963
2405
  let readResult;
1964
2406
  try {
1965
2407
  readResult = await reader.read();
1966
2408
  } catch (error) {
2409
+ appendDebugLog("rovodev:stream:error", {
2410
+ sessionId,
2411
+ elapsedMs: Date.now() - streamStartedAt,
2412
+ bytesRead,
2413
+ error: serializeError(error),
2414
+ serverClosed: server.closed,
2415
+ serverStderr: server.stderr.slice(-2048)
2416
+ });
1967
2417
  if (isAbortError(error)) throw createAbortError();
1968
2418
  throw error;
1969
2419
  }
@@ -1971,6 +2421,7 @@ var RovoDevAgent = class {
1971
2421
  const chunk = decoder.decode(readResult.value, { stream: true });
1972
2422
  logStream?.write(chunk);
1973
2423
  buffer += chunk;
2424
+ bytesRead += chunk.length;
1974
2425
  while (true) {
1975
2426
  const lfBoundary = buffer.indexOf("\n\n");
1976
2427
  const crlfBoundary = buffer.indexOf("\r\n\r\n");
@@ -1991,14 +2442,33 @@ var RovoDevAgent = class {
1991
2442
  }
1992
2443
  buffer += decoder.decode();
1993
2444
  if (buffer.trim()) handleEvent(buffer);
2445
+ appendDebugLog("rovodev:stream:end", {
2446
+ sessionId,
2447
+ elapsedMs: Date.now() - streamStartedAt,
2448
+ bytesRead
2449
+ });
1994
2450
  const finalText = latestTextSegment.trim();
1995
- if (!finalText) throw new Error("rovodev returned no text output");
2451
+ if (!finalText) {
2452
+ appendDebugLog("rovodev:output:missing", { sessionId });
2453
+ throw new Error("rovodev returned no text output");
2454
+ }
1996
2455
  try {
2456
+ const output = JSON.parse(finalText);
2457
+ appendDebugLog("rovodev:output:parsed", {
2458
+ sessionId,
2459
+ outputTextLength: finalText.length
2460
+ });
1997
2461
  return {
1998
- output: JSON.parse(finalText),
2462
+ output,
1999
2463
  usage
2000
2464
  };
2001
2465
  } catch (error) {
2466
+ appendDebugLog("rovodev:output:parse-error", {
2467
+ sessionId,
2468
+ outputTextLength: finalText.length,
2469
+ outputTextSample: finalText.slice(0, 512),
2470
+ error: serializeError(error)
2471
+ });
2002
2472
  throw new Error(`Failed to parse rovodev output: ${error instanceof Error ? error.message : String(error)}`);
2003
2473
  }
2004
2474
  }
@@ -2012,9 +2482,11 @@ var RovoDevAgent = class {
2012
2482
  return;
2013
2483
  }
2014
2484
  const server = this.server;
2485
+ const shutdownStartedAt = Date.now();
2015
2486
  appendDebugLog("rovodev:shutdown", {
2016
2487
  cwd: server.cwd,
2017
- port: server.port
2488
+ port: server.port,
2489
+ pid: server.child.pid
2018
2490
  });
2019
2491
  this.closingPromise = this.platform === "win32" ? new Promise((resolve) => {
2020
2492
  const handleClose = () => {
@@ -2041,6 +2513,10 @@ var RovoDevAgent = class {
2041
2513
  this.closingPromise = this.closingPromise.finally(() => {
2042
2514
  if (this.server === server) this.server = null;
2043
2515
  this.closingPromise = null;
2516
+ appendDebugLog("rovodev:shutdown:done", {
2517
+ port: server.port,
2518
+ elapsedMs: Date.now() - shutdownStartedAt
2519
+ });
2044
2520
  });
2045
2521
  await this.closingPromise;
2046
2522
  }
@@ -2052,14 +2528,35 @@ var RovoDevAgent = class {
2052
2528
  if (options.sessionId) headers.set("x-session-id", options.sessionId);
2053
2529
  if (options.body !== void 0 && !headers.has("content-type")) headers.set("content-type", "application/json");
2054
2530
  const signal = withTimeoutSignal(options.signal, options.timeoutMs);
2055
- const response = await this.fetchFn(`${server.baseUrl}${path}`, {
2056
- method: options.method,
2057
- headers,
2058
- body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
2059
- signal
2060
- });
2531
+ const startedAt = Date.now();
2532
+ let response;
2533
+ try {
2534
+ response = await this.fetchFn(`${server.baseUrl}${path}`, {
2535
+ method: options.method,
2536
+ headers,
2537
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
2538
+ signal
2539
+ });
2540
+ } catch (error) {
2541
+ appendDebugLog("rovodev:request:error", {
2542
+ method: options.method,
2543
+ path,
2544
+ elapsedMs: Date.now() - startedAt,
2545
+ timeoutMs: options.timeoutMs,
2546
+ error: serializeError(error),
2547
+ serverClosed: server.closed
2548
+ });
2549
+ throw error;
2550
+ }
2061
2551
  if (!response.ok) {
2062
2552
  const body = await response.text();
2553
+ appendDebugLog("rovodev:request:non-ok", {
2554
+ method: options.method,
2555
+ path,
2556
+ status: response.status,
2557
+ elapsedMs: Date.now() - startedAt,
2558
+ bodySample: body.slice(0, 1024)
2559
+ });
2063
2560
  throw new Error(`rovodev ${options.method} ${path} failed with ${response.status}: ${body}`);
2064
2561
  }
2065
2562
  return response;
@@ -2083,23 +2580,23 @@ function createAgent(name, runInfo, pathOverride) {
2083
2580
  //#endregion
2084
2581
  //#region src/templates/iteration-prompt.ts
2085
2582
  function buildIterationPrompt(params) {
2086
- return `You are working autonomously on an objective given below.
2087
- This is iteration ${params.n} of an ongoing loop to fully accomplish the objective.
2583
+ return `You are working autonomously towards an objective given below.
2584
+ This is iteration ${params.n}. Each iteration aims to make an incremental step forward, not to complete the entire objective.
2088
2585
 
2089
2586
  ## Instructions
2090
2587
 
2091
- 1. Read .gnhf/runs/${params.runId}/notes.md first to understand what has been done in previous iterations.
2092
- 2. Focus on the next smallest logical unit of work that's individually testable and would make incremental progress towards the objective - that's the scope of this iteration.
2093
- 3. If you made code changes, run build/tests/linters/formatters if available to validate your work.
2094
- 4. Do NOT make any git commits. Commits will be handled automatically by the gnhf orchestrator.
2095
- 5. When you are done, respond with a JSON object according to the provided schema.
2588
+ 1. Read .gnhf/runs/${params.runId}/notes.md first to understand what has been done in previous iterations
2589
+ 2. Identify the next smallest logical unit of work that's individually verifiable and would make incremental progress towards the objective, and treat that as the scope of this iteration
2590
+ 3. If you attempted a solution and it didn't end up moving the needle on the objective, document learnings and record success=false, then conclude the iteration rather than continuously pivoting
2591
+ 4. If you made code changes, run build/tests/linters/formatters if available to validate your work. Do NOT make any git commits - that will be handled automatically by the gnhf orchestrator
2592
+ 6. Finally, respond with a JSON object according to the provided schema
2096
2593
 
2097
2594
  ## Output
2098
2595
 
2099
- - success: whether you were able to complete your iteration. set to false only if something made it impossible for you to do your work
2596
+ - success: whether you were able to make a meaningful contribution that got us closer towards the objective. setting this to false means any code change you made should be discarded
2100
2597
  - summary: a concise one-sentence summary of the accomplishment in this iteration
2101
2598
  - key_changes_made: an array of descriptions for key changes you made. don't group this by file - group by logical units of work. don't describe activities - describe material outcomes
2102
- - key_learnings: an array of new learnings that were surprising and weren't captured by previous notes
2599
+ - key_learnings: an array of new learnings that were surprising, weren't captured by previous notes and would be informative for future iterations
2103
2600
 
2104
2601
  ## Objective
2105
2602
 
@@ -2120,6 +2617,7 @@ var Orchestrator = class extends EventEmitter {
2120
2617
  activeIterationPromise = null;
2121
2618
  activeAbortController = null;
2122
2619
  pendingAbortReason = null;
2620
+ loopDone = false;
2123
2621
  state = {
2124
2622
  status: "running",
2125
2623
  currentIteration: 0,
@@ -2150,7 +2648,16 @@ var Orchestrator = class extends EventEmitter {
2150
2648
  }
2151
2649
  stop() {
2152
2650
  this.stopRequested = true;
2651
+ appendDebugLog("orchestrator:stop-requested", {
2652
+ iteration: this.state.currentIteration,
2653
+ hasActiveIteration: this.activeIterationPromise !== null,
2654
+ loopDone: this.loopDone
2655
+ });
2153
2656
  this.activeAbortController?.abort();
2657
+ if (this.loopDone) {
2658
+ this.emit("stopped");
2659
+ return;
2660
+ }
2154
2661
  if (this.stopPromise) return;
2155
2662
  this.stopPromise = (async () => {
2156
2663
  if (this.activeIterationPromise) {
@@ -2180,6 +2687,16 @@ var Orchestrator = class extends EventEmitter {
2180
2687
  this.state.startTime = /* @__PURE__ */ new Date();
2181
2688
  this.state.status = "running";
2182
2689
  this.emit("state", this.getState());
2690
+ appendDebugLog("orchestrator:start", {
2691
+ agent: this.agent.name,
2692
+ runId: this.runInfo.runId,
2693
+ startIteration: this.state.currentIteration,
2694
+ maxIterations: this.limits.maxIterations,
2695
+ maxTokens: this.limits.maxTokens,
2696
+ maxConsecutiveFailures: this.config.maxConsecutiveFailures,
2697
+ baseCommit: this.runInfo.baseCommit,
2698
+ initialCommitCount: this.state.commitCount
2699
+ });
2183
2700
  try {
2184
2701
  while (!this.stopRequested) {
2185
2702
  const preIterationAbortReason = this.getPreIterationAbortReason();
@@ -2196,11 +2713,32 @@ var Orchestrator = class extends EventEmitter {
2196
2713
  runId: this.runInfo.runId,
2197
2714
  prompt: this.prompt
2198
2715
  });
2716
+ appendDebugLog("iteration:start", {
2717
+ iteration: this.state.currentIteration,
2718
+ promptLength: iterationPrompt.length,
2719
+ consecutiveFailures: this.state.consecutiveFailures,
2720
+ totalInputTokens: this.state.totalInputTokens,
2721
+ totalOutputTokens: this.state.totalOutputTokens,
2722
+ git: this.snapshotGitState()
2723
+ });
2724
+ const iterationStartedAt = Date.now();
2199
2725
  this.activeIterationPromise = this.runIteration(iterationPrompt);
2200
2726
  const result = await this.activeIterationPromise;
2201
2727
  this.activeIterationPromise = null;
2202
- if (result.type === "stopped") break;
2728
+ const iterationElapsedMs = Date.now() - iterationStartedAt;
2729
+ if (result.type === "stopped") {
2730
+ appendDebugLog("iteration:stopped", {
2731
+ iteration: this.state.currentIteration,
2732
+ elapsedMs: iterationElapsedMs
2733
+ });
2734
+ break;
2735
+ }
2203
2736
  if (result.type === "aborted") {
2737
+ appendDebugLog("iteration:aborted", {
2738
+ iteration: this.state.currentIteration,
2739
+ elapsedMs: iterationElapsedMs,
2740
+ reason: result.reason
2741
+ });
2204
2742
  this.abort(result.reason);
2205
2743
  break;
2206
2744
  }
@@ -2208,6 +2746,18 @@ var Orchestrator = class extends EventEmitter {
2208
2746
  this.state.iterations.push(record);
2209
2747
  this.emit("iteration:end", record);
2210
2748
  this.emit("state", this.getState());
2749
+ appendDebugLog("iteration:end", {
2750
+ iteration: record.number,
2751
+ elapsedMs: iterationElapsedMs,
2752
+ success: record.success,
2753
+ summary: record.summary,
2754
+ keyChanges: record.keyChanges.length,
2755
+ keyLearnings: record.keyLearnings.length,
2756
+ consecutiveFailures: this.state.consecutiveFailures,
2757
+ totalInputTokens: this.state.totalInputTokens,
2758
+ totalOutputTokens: this.state.totalOutputTokens,
2759
+ commitCount: this.state.commitCount
2760
+ });
2211
2761
  const postIterationAbortReason = this.getPostIterationAbortReason();
2212
2762
  if (postIterationAbortReason) {
2213
2763
  this.abort(postIterationAbortReason);
@@ -2222,7 +2772,16 @@ var Orchestrator = class extends EventEmitter {
2222
2772
  this.state.status = "waiting";
2223
2773
  this.state.waitingUntil = new Date(Date.now() + backoffMs);
2224
2774
  this.emit("state", this.getState());
2775
+ appendDebugLog("backoff:start", {
2776
+ iteration: this.state.currentIteration,
2777
+ consecutiveFailures: this.state.consecutiveFailures,
2778
+ backoffMs
2779
+ });
2225
2780
  await this.interruptibleSleep(backoffMs);
2781
+ appendDebugLog("backoff:end", {
2782
+ iteration: this.state.currentIteration,
2783
+ stopRequested: this.stopRequested
2784
+ });
2226
2785
  this.state.waitingUntil = null;
2227
2786
  if (!this.stopRequested) {
2228
2787
  this.state.status = "running";
@@ -2234,6 +2793,16 @@ var Orchestrator = class extends EventEmitter {
2234
2793
  this.activeIterationPromise = null;
2235
2794
  if (this.stopPromise) await this.stopPromise;
2236
2795
  else await this.closeAgent();
2796
+ this.loopDone = true;
2797
+ appendDebugLog("orchestrator:end", {
2798
+ status: this.state.status,
2799
+ iterations: this.state.currentIteration,
2800
+ successCount: this.state.successCount,
2801
+ failCount: this.state.failCount,
2802
+ totalInputTokens: this.state.totalInputTokens,
2803
+ totalOutputTokens: this.state.totalOutputTokens,
2804
+ commitCount: this.state.commitCount
2805
+ });
2237
2806
  }
2238
2807
  }
2239
2808
  async runIteration(prompt) {
@@ -2256,6 +2825,12 @@ var Orchestrator = class extends EventEmitter {
2256
2825
  this.emit("state", this.getState());
2257
2826
  };
2258
2827
  const logPath = join(this.runInfo.runDir, `iteration-${this.state.currentIteration}.jsonl`);
2828
+ const agentStartedAt = Date.now();
2829
+ appendDebugLog("agent:run:start", {
2830
+ iteration: this.state.currentIteration,
2831
+ agent: this.agent.name,
2832
+ logPath
2833
+ });
2259
2834
  try {
2260
2835
  const result = await this.agent.run(prompt, this.cwd, {
2261
2836
  onUsage,
@@ -2263,6 +2838,15 @@ var Orchestrator = class extends EventEmitter {
2263
2838
  signal: this.activeAbortController.signal,
2264
2839
  logPath
2265
2840
  });
2841
+ appendDebugLog("agent:run:end", {
2842
+ iteration: this.state.currentIteration,
2843
+ elapsedMs: Date.now() - agentStartedAt,
2844
+ success: result.output.success,
2845
+ inputTokens: result.usage.inputTokens,
2846
+ outputTokens: result.usage.outputTokens,
2847
+ cacheReadTokens: result.usage.cacheReadTokens,
2848
+ cacheCreationTokens: result.usage.cacheCreationTokens
2849
+ });
2266
2850
  if (this.stopRequested) return { type: "stopped" };
2267
2851
  if (result.output.success) return {
2268
2852
  type: "completed",
@@ -2273,14 +2857,31 @@ var Orchestrator = class extends EventEmitter {
2273
2857
  record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, result.output.key_learnings)
2274
2858
  };
2275
2859
  } catch (err) {
2860
+ const elapsedMs = Date.now() - agentStartedAt;
2276
2861
  if (this.pendingAbortReason && err instanceof Error && err.message === "Agent was aborted") {
2862
+ appendDebugLog("agent:run:aborted", {
2863
+ iteration: this.state.currentIteration,
2864
+ elapsedMs,
2865
+ reason: this.pendingAbortReason
2866
+ });
2277
2867
  resetHard(this.cwd);
2278
2868
  return {
2279
2869
  type: "aborted",
2280
2870
  reason: this.pendingAbortReason
2281
2871
  };
2282
2872
  }
2283
- if (this.stopRequested) return { type: "stopped" };
2873
+ if (this.stopRequested) {
2874
+ appendDebugLog("agent:run:stopped", {
2875
+ iteration: this.state.currentIteration,
2876
+ elapsedMs
2877
+ });
2878
+ return { type: "stopped" };
2879
+ }
2880
+ appendDebugLog("agent:run:error", {
2881
+ iteration: this.state.currentIteration,
2882
+ elapsedMs,
2883
+ error: serializeError(err)
2884
+ });
2284
2885
  const summary = err instanceof Error ? err.message : String(err);
2285
2886
  return {
2286
2887
  type: "completed",
@@ -2352,13 +2953,31 @@ var Orchestrator = class extends EventEmitter {
2352
2953
  this.state.status = "aborted";
2353
2954
  this.state.lastMessage = reason;
2354
2955
  this.state.waitingUntil = null;
2956
+ appendDebugLog("orchestrator:abort", {
2957
+ reason,
2958
+ iteration: this.state.currentIteration,
2959
+ consecutiveFailures: this.state.consecutiveFailures
2960
+ });
2355
2961
  this.emit("abort", reason);
2356
2962
  this.emit("state", this.getState());
2357
2963
  }
2358
2964
  async closeAgent() {
2359
2965
  try {
2360
2966
  await this.agent.close?.();
2361
- } catch {}
2967
+ } catch (err) {
2968
+ appendDebugLog("agent:close:error", { error: serializeError(err) });
2969
+ }
2970
+ }
2971
+ snapshotGitState() {
2972
+ try {
2973
+ return {
2974
+ head: getHeadCommit(this.cwd),
2975
+ branch: getCurrentBranch(this.cwd),
2976
+ commitCount: this.state.commitCount
2977
+ };
2978
+ } catch (err) {
2979
+ return { error: serializeError(err) };
2980
+ }
2362
2981
  }
2363
2982
  };
2364
2983
  //#endregion
@@ -2758,6 +3377,7 @@ const MOON_PHASE_PERIOD = 1600;
2758
3377
  const MAX_MSG_LINES = 3;
2759
3378
  const MAX_MSG_LINE_LEN = CONTENT_WIDTH;
2760
3379
  const RESUME_HINT = "[ctrl+c to stop, gnhf again to resume]";
3380
+ const DONE_HINT = "[ctrl+c to exit]";
2761
3381
  function spacedLabel(text) {
2762
3382
  return text.split("").join(" ");
2763
3383
  }
@@ -2878,8 +3498,8 @@ function centerLineCells(content, width) {
2878
3498
  ...emptyCells(rightPad)
2879
3499
  ];
2880
3500
  }
2881
- function renderResumeHintCells(width) {
2882
- return centerLineCells(textToCells(RESUME_HINT, "dim"), width);
3501
+ function renderResumeHintCells(width, done) {
3502
+ return centerLineCells(textToCells(done ? DONE_HINT : RESUME_HINT, "dim"), width);
2883
3503
  }
2884
3504
  /**
2885
3505
  * Builds the centered content viewport for the renderer.
@@ -2987,7 +3607,8 @@ function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideSt
2987
3607
  ]);
2988
3608
  }
2989
3609
  for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, terminalWidth, y, now));
2990
- frame.push(renderResumeHintCells(terminalWidth));
3610
+ const isDone = state.status === "aborted";
3611
+ frame.push(renderResumeHintCells(terminalWidth, isDone));
2991
3612
  frame.push(emptyCells(terminalWidth));
2992
3613
  return frame;
2993
3614
  }
@@ -3277,7 +3898,22 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
3277
3898
  if (!reexeced) persistedPrompt?.cleanup();
3278
3899
  }
3279
3900
  }
3280
- appendDebugLog("run:start", { args: process$1.argv.slice(2) });
3901
+ initDebugLog(runInfo.logPath);
3902
+ appendDebugLog("run:start", {
3903
+ args: process$1.argv.slice(2),
3904
+ runId: runInfo.runId,
3905
+ runDir: runInfo.runDir,
3906
+ agent: config.agent,
3907
+ promptLength: prompt.length,
3908
+ promptFromStdin,
3909
+ startIteration,
3910
+ maxIterations: options.maxIterations,
3911
+ maxTokens: options.maxTokens,
3912
+ preventSleep: config.preventSleep,
3913
+ platform: process$1.platform,
3914
+ nodeVersion: process$1.version,
3915
+ gnhfVersion: packageVersion
3916
+ });
3281
3917
  const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent]), runInfo, prompt, cwd, startIteration, {
3282
3918
  maxIterations: options.maxIterations,
3283
3919
  maxTokens: options.maxTokens
@@ -3298,8 +3934,9 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
3298
3934
  process$1.on("SIGINT", handleSigInt);
3299
3935
  process$1.on("SIGTERM", handleSigTerm);
3300
3936
  const orchestratorPromise = orchestrator.start().finally(() => {
3301
- renderer.stop();
3937
+ if (!(orchestrator.getState().status === "aborted" && process$1.stdin.isTTY)) renderer.stop();
3302
3938
  }).catch((err) => {
3939
+ appendDebugLog("orchestrator:fatal", { error: serializeError(err) });
3303
3940
  exitAltScreen();
3304
3941
  die(err instanceof Error ? err.message : String(err));
3305
3942
  });
@@ -3321,7 +3958,19 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
3321
3958
  process$1.off("SIGTERM", handleSigTerm);
3322
3959
  await sleepPreventionCleanup?.();
3323
3960
  }
3324
- appendDebugLog("run:complete", { signal: shutdownSignal });
3961
+ {
3962
+ const finalState = orchestrator.getState();
3963
+ appendDebugLog("run:complete", {
3964
+ signal: shutdownSignal,
3965
+ status: finalState.status,
3966
+ iterations: finalState.currentIteration,
3967
+ successCount: finalState.successCount,
3968
+ failCount: finalState.failCount,
3969
+ totalInputTokens: finalState.totalInputTokens,
3970
+ totalOutputTokens: finalState.totalOutputTokens,
3971
+ commitCount: finalState.commitCount
3972
+ });
3973
+ }
3325
3974
  if (shutdownSignal) process$1.exit(getSignalExitCode(shutdownSignal));
3326
3975
  });
3327
3976
  function enterAltScreen() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {