@kenkaiiii/ggcoder 4.3.207 → 4.3.209

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 (47) hide show
  1. package/dist/cli.js +10 -7
  2. package/dist/cli.js.map +1 -1
  3. package/dist/core/process-manager-dev-server-repro.test.js +37 -1
  4. package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
  5. package/dist/core/process-manager.d.ts +9 -0
  6. package/dist/core/process-manager.d.ts.map +1 -1
  7. package/dist/core/process-manager.js +20 -6
  8. package/dist/core/process-manager.js.map +1 -1
  9. package/dist/core/repomap.js +8 -1
  10. package/dist/core/repomap.js.map +1 -1
  11. package/dist/core/repomap.test.js +32 -0
  12. package/dist/core/repomap.test.js.map +1 -1
  13. package/dist/tools/bash.d.ts.map +1 -1
  14. package/dist/tools/bash.js +2 -51
  15. package/dist/tools/bash.js.map +1 -1
  16. package/dist/tools/grep.d.ts.map +1 -1
  17. package/dist/tools/grep.js +22 -5
  18. package/dist/tools/grep.js.map +1 -1
  19. package/dist/tools/grep.test.d.ts +2 -0
  20. package/dist/tools/grep.test.d.ts.map +1 -0
  21. package/dist/tools/grep.test.js +20 -0
  22. package/dist/tools/grep.test.js.map +1 -0
  23. package/dist/tools/safe-env.d.ts +2 -0
  24. package/dist/tools/safe-env.d.ts.map +1 -0
  25. package/dist/tools/safe-env.js +52 -0
  26. package/dist/tools/safe-env.js.map +1 -0
  27. package/dist/tools/web-fetch.d.ts +5 -0
  28. package/dist/tools/web-fetch.d.ts.map +1 -1
  29. package/dist/tools/web-fetch.js +12 -1
  30. package/dist/tools/web-fetch.js.map +1 -1
  31. package/dist/tools/web-fetch.test.js +9 -0
  32. package/dist/tools/web-fetch.test.js.map +1 -1
  33. package/dist/ui/App.d.ts +2 -3
  34. package/dist/ui/App.d.ts.map +1 -1
  35. package/dist/ui/App.js +128 -208
  36. package/dist/ui/App.js.map +1 -1
  37. package/dist/ui/app-state-persistence.test.js +60 -4
  38. package/dist/ui/app-state-persistence.test.js.map +1 -1
  39. package/dist/ui/goal-events.test.js +19 -0
  40. package/dist/ui/goal-events.test.js.map +1 -1
  41. package/dist/ui/goal-overlay.test.js +30 -0
  42. package/dist/ui/goal-overlay.test.js.map +1 -1
  43. package/dist/ui/scroll-stabilization.test.js +2 -2
  44. package/dist/ui/scroll-stabilization.test.js.map +1 -1
  45. package/dist/ui/slash-command-images.test.js +10 -0
  46. package/dist/ui/slash-command-images.test.js.map +1 -1
  47. package/package.json +6 -5
package/dist/ui/App.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React, { useState, useRef, useCallback, useEffect, useMemo } from "react";
3
- import { Box, Text, Static, useStdout } from "ink";
3
+ import { Box, Text, Static } from "ink";
4
4
  import { useTerminalSize } from "./hooks/useTerminalSize.js";
5
5
  import { useDoublePress } from "./hooks/useDoublePress.js";
6
6
  import { useTaskBarStore, useTaskBarPolling, focusTaskBar, exitTaskBar, expandTaskBar, collapseTaskBar, navigateTaskBar, killTask, } from "./stores/taskbar-store.js";
@@ -59,8 +59,9 @@ import { getLatestUserText, injectRepoMapContextMessages, stripRepoMapContextMes
59
59
  import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
60
60
  import { getMCPServers } from "../core/mcp/index.js";
61
61
  import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
62
- import { appendGoalDecision, appendGoalEvidence, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, projectDir, summarizeGoalCounts, summarizeGoalCountsFromRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
62
+ import { appendGoalDecision, appendGoalEvidence, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, summarizeGoalCounts, summarizeGoalCountsFromRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
63
63
  import { canCompleteGoalRun, decideGoalNextAction, shouldCreateVerifierFixTask, } from "../core/goal-controller.js";
64
+ import { runGoalVerifierCommand } from "../core/goal-verifier.js";
64
65
  import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
65
66
  import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, isGoalSyntheticEvent, parseGoalSyntheticEvent, } from "./goal-events.js";
66
67
  /** Where ggcoder bugs should be reported. Surfaced in the guidance line. */
@@ -189,9 +190,7 @@ function summarizeGoalCompletion(summary) {
189
190
  return statusLine ?? changedLine ?? verificationLine ?? lines[0];
190
191
  }
191
192
  function formatGoalWorkerFinishedTitle(taskTitle, status) {
192
- return status === "done"
193
- ? `Worker finished: ${taskTitle}. Reporting back.`
194
- : `Worker failed: ${taskTitle}. Reporting back.`;
193
+ return status === "done" ? `Done: ${taskTitle}` : `Failed: ${taskTitle}`;
195
194
  }
196
195
  function countGoalTasksByStatus(tasks, status) {
197
196
  return tasks.filter((task) => task.status === status).length;
@@ -298,12 +297,11 @@ export function formatGoalTerminalProgress(run) {
298
297
  return null;
299
298
  }
300
299
  }
301
- export function shouldHideHistoryForOverlayView(isOverlayView, isAgentRunning) {
302
- // Overlay panes should be clean, full-screen views like Tasks/Plans/Skills.
303
- // When the agent is idle, pane open/close goes through resetUI(), so history
304
- // remains in sessionStore and is reprinted after the pane closes. While the
305
- // agent is running we keep Static mounted because resetUI would abort the run.
306
- return isOverlayView && !isAgentRunning;
300
+ export function shouldHideHistoryForOverlayView(_isOverlayView, _isAgentRunning) {
301
+ // Ink Static is append-only. Passing [] for overlay panes rewrites the Static
302
+ // accumulator and can destroy scrollback when the pane closes. Keep history
303
+ // mounted and let overlays render below it.
304
+ return false;
307
305
  }
308
306
  export function shouldStabilizeOverlayPaneRerender({ overlayPane, isAgentRunning, }) {
309
307
  return isAgentRunning && (overlayPane === "goal" || overlayPane === "plan");
@@ -321,8 +319,8 @@ export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, h
321
319
  export function isTallLiveUserMessage(text, rows) {
322
320
  return text.split("\n").length > Math.max(8, Math.floor(rows * 0.6));
323
321
  }
324
- export function getStaticHistoryKey({ resizeKey, staticKey, }) {
325
- return `${resizeKey}-${staticKey}`;
322
+ export function getStaticHistoryKey({ resizeKey }) {
323
+ return `${resizeKey}`;
326
324
  }
327
325
  // flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
328
326
  /** Check whether an item is still active (running spinner, pending result). */
@@ -485,7 +483,6 @@ function markTaskInProgress(cwd, taskId) {
485
483
  export function App(props) {
486
484
  const theme = useTheme();
487
485
  const switchTheme = useSetTheme();
488
- const { stdout } = useStdout();
489
486
  const { columns, resizeKey } = useTerminalSize();
490
487
  // Hoisted before terminal title hook so it can reference them
491
488
  const [lastUserMessage, setLastUserMessage] = useState("");
@@ -547,7 +544,6 @@ export function App(props) {
547
544
  const startPixelFixRef = useRef(() => { });
548
545
  const cwdRef = useRef(props.cwd);
549
546
  const [displayedCwd, setDisplayedCwd] = useState(props.cwd);
550
- const [staticKey, setStaticKey] = useState(0);
551
547
  const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
552
548
  // Suppress "done" status when a plan overlay is about to open
553
549
  const planOverlayPendingRef = useRef(false);
@@ -906,14 +902,6 @@ export function App(props) {
906
902
  // premature "done" status that fires when the agent loop finishes
907
903
  planOverlayPendingRef.current = true;
908
904
  setTimeout(() => {
909
- // NOTE: this is the one open-overlay path that does NOT remount via
910
- // resetUI. It runs while the agent is still mid-turn (after the
911
- // exit_plan tool returned but before onDone fires), and unmounting
912
- // here would kill the in-flight agent stream. Keep the bare ANSI
913
- // clear; the drift bug is tolerable across just the agent's
914
- // wrap-up turn, and onApprove/onReject both remount cleanly via
915
- // resetUI when the user resolves the plan.
916
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
917
905
  setPlanAutoExpand(true);
918
906
  setOverlay("plan");
919
907
  // Don't clear planOverlayPendingRef here — keep it true until
@@ -928,7 +916,7 @@ export function App(props) {
928
916
  planPath);
929
917
  };
930
918
  }
931
- }, [props.onExitPlanRef, replaceSystemPrompt, stdout]);
919
+ }, [props.onExitPlanRef, replaceSystemPrompt]);
932
920
  const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
933
921
  const sm = sessionManagerRef.current;
934
922
  if (!sm)
@@ -2045,14 +2033,10 @@ export function App(props) {
2045
2033
  process.exit(0);
2046
2034
  }
2047
2035
  // Handle /clear — tear down the entire Ink instance and rebuild fresh.
2048
- // Patching Ink's internal frame tracking in place (log-update reset,
2049
- // lastOutput cleared, fullStaticOutput dropped, staticKey bump) all
2050
- // looked correct for one frame but left the live area drifting on
2051
- // subsequent streaming responses — Ink's cursor math depends on
2052
- // terminal-state assumptions that ANSI clearing breaks. The reliable
2053
- // fix is unmount + render again. Runtime state (model, provider,
2054
- // thinking) survives via renderApp's closure-held `runtimeState`,
2055
- // mirrored from React state via the useEffects above.
2036
+ // Avoid direct ANSI terminal clears here; they can erase scrollback.
2037
+ // Runtime state (model, provider, thinking) survives via renderApp's
2038
+ // closure-held `runtimeState`, mirrored from React state via the
2039
+ // useEffects above.
2056
2040
  if (trimmed === "/clear") {
2057
2041
  if (props.resetUI) {
2058
2042
  void (async () => {
@@ -2065,8 +2049,7 @@ export function App(props) {
2065
2049
  return;
2066
2050
  }
2067
2051
  // Fallback path (resetUI not wired — e.g. tests). Best-effort: clear
2068
- // React state in place. The Ink-internal drift bug remains here.
2069
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2052
+ // React state in place without touching terminal scrollback.
2070
2053
  pendingFlushRef.current = [];
2071
2054
  setHistory([{ kind: "banner", id: "banner" }]);
2072
2055
  setLiveItems([]);
@@ -2082,7 +2065,6 @@ export function App(props) {
2082
2065
  agentLoop.reset();
2083
2066
  setSessionTitle(undefined);
2084
2067
  sessionTitleGeneratedRef.current = false;
2085
- setStaticKey((k) => k + 1);
2086
2068
  setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
2087
2069
  return;
2088
2070
  }
@@ -2190,8 +2172,6 @@ export function App(props) {
2190
2172
  props.resetUI();
2191
2173
  }
2192
2174
  else {
2193
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2194
- setStaticKey((key) => key + 1);
2195
2175
  if (props.sessionStore) {
2196
2176
  props.sessionStore.overlay = "goal";
2197
2177
  props.sessionStore.planAutoExpand = false;
@@ -2549,11 +2529,23 @@ export function App(props) {
2549
2529
  return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "▶ " }), _jsx(Text, { color: theme.textDim, children: "Goal: " }), _jsx(Text, { color: theme.success, children: item.title }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }) }, item.id));
2550
2530
  case "goal_progress": {
2551
2531
  const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
2552
- const color = item.phase === "terminal" && !isError
2553
- ? theme.success
2554
- : isError
2555
- ? theme.warning
2556
- : theme.primary;
2532
+ const color = isError
2533
+ ? theme.error
2534
+ : item.phase === "worker_finished"
2535
+ ? theme.success
2536
+ : item.phase === "verifier_finished"
2537
+ ? theme.accent
2538
+ : item.phase === "orchestrator_reviewing" || item.phase === "orchestrator_working"
2539
+ ? theme.secondary
2540
+ : item.phase === "continuing"
2541
+ ? theme.warning
2542
+ : item.phase === "verifier_started"
2543
+ ? theme.accent
2544
+ : item.phase === "worker_started"
2545
+ ? theme.primary
2546
+ : item.phase === "terminal"
2547
+ ? theme.success
2548
+ : theme.primary;
2557
2549
  const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
2558
2550
  ? "✓ "
2559
2551
  : item.phase === "terminal"
@@ -2675,7 +2667,6 @@ export function App(props) {
2675
2667
  return;
2676
2668
  }
2677
2669
  // Fallback path (resetUI not wired — tests).
2678
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2679
2670
  setHistory([{ kind: "banner", id: "banner" }]);
2680
2671
  setLiveItems([]);
2681
2672
  messagesRef.current = messagesRef.current.slice(0, 1);
@@ -2709,15 +2700,7 @@ export function App(props) {
2709
2700
  setRunAllTasks(false);
2710
2701
  }
2711
2702
  })();
2712
- }, [
2713
- props.cwd,
2714
- props.resetUI,
2715
- props.sessionStore,
2716
- stdout,
2717
- agentLoop,
2718
- currentProvider,
2719
- currentModel,
2720
- ]);
2703
+ }, [props.cwd, props.resetUI, props.sessionStore, agentLoop, currentProvider, currentModel]);
2721
2704
  const openOverlay = useCallback((kind) => {
2722
2705
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2723
2706
  props.sessionStore.overlay = kind;
@@ -2738,7 +2721,7 @@ export function App(props) {
2738
2721
  setPlanAutoExpand(false);
2739
2722
  setOverlay(kind);
2740
2723
  }
2741
- }, [agentLoop.isRunning, props, stdout]);
2724
+ }, [agentLoop.isRunning, props]);
2742
2725
  const closeOverlay = useCallback(() => {
2743
2726
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2744
2727
  props.sessionStore.overlay = null;
@@ -2750,7 +2733,7 @@ export function App(props) {
2750
2733
  }
2751
2734
  setOverlay(null);
2752
2735
  }
2753
- }, [agentLoop.isRunning, overlay, props, stdout]);
2736
+ }, [agentLoop.isRunning, overlay, props]);
2754
2737
  const runGoalSyntheticEvent = useCallback((eventText) => {
2755
2738
  const eventInfo = parseGoalSyntheticEvent(eventText);
2756
2739
  const detail = eventInfo?.kind === "worker"
@@ -2841,8 +2824,8 @@ export function App(props) {
2841
2824
  appendGoalProgress({
2842
2825
  kind: "goal_progress",
2843
2826
  phase: "continuing",
2844
- title: `Continuing Goal: ${latestRun.title}`,
2845
- detail: "Starting the next worker task or verifier step automatically.",
2827
+ title: `Choosing next Goal step: ${latestRun.title}`,
2828
+ detail: "Latest result is recorded; starting the next worker task or verifier automatically.",
2846
2829
  status: latestRun.status,
2847
2830
  });
2848
2831
  upsertGoalStatusEntry({
@@ -3138,143 +3121,89 @@ export function App(props) {
3138
3121
  detail: run.verifier.command,
3139
3122
  goalNumber: goalNumberForRun(run.id),
3140
3123
  });
3141
- const { spawn } = await import("node:child_process");
3142
- const { mkdir, writeFile } = await import("node:fs/promises");
3143
- const { join } = await import("node:path");
3144
- const logDir = join(projectDir(props.cwd), "verifiers");
3145
- await mkdir(logDir, { recursive: true });
3146
- const outputPath = join(logDir, `${run.id}-${startedAt}.log`);
3147
- const child = spawn(run.verifier.command, {
3124
+ void runGoalVerifierCommand({
3148
3125
  cwd: props.cwd,
3149
- shell: true,
3150
- stdio: ["ignore", "pipe", "pipe"],
3151
- env: { ...process.env },
3152
- });
3153
- let output = "";
3154
- child.stdout?.on("data", (chunk) => {
3155
- output += chunk.toString("utf-8");
3156
- if (output.length > 20_000)
3157
- output = output.slice(output.length - 20_000);
3158
- });
3159
- child.stderr?.on("data", (chunk) => {
3160
- output += chunk.toString("utf-8");
3161
- if (output.length > 20_000)
3162
- output = output.slice(output.length - 20_000);
3163
- });
3164
- let verifierSettled = false;
3165
- let timedOut = false;
3166
- const timeout = verifierTimeoutMs > 0
3167
- ? setTimeout(() => {
3168
- timedOut = true;
3169
- if (child.pid)
3170
- child.kill("SIGTERM");
3171
- const killTimer = setTimeout(() => {
3172
- if (!verifierSettled && child.pid)
3173
- child.kill("SIGKILL");
3174
- }, 5000);
3175
- killTimer.unref?.();
3176
- finishVerifier(124, `Verifier timed out after ${verifierTimeoutMs}ms and was terminated.\n${output}`);
3177
- }, verifierTimeoutMs)
3178
- : undefined;
3179
- timeout?.unref?.();
3180
- const finishVerifier = (code, forcedOutput) => {
3181
- if (verifierSettled)
3182
- return;
3183
- verifierSettled = true;
3184
- if (timeout)
3185
- clearTimeout(timeout);
3126
+ runId: run.id,
3127
+ command: run.verifier.command,
3128
+ timeoutMs: verifierTimeoutMs,
3129
+ now: () => startedAt,
3130
+ })
3131
+ .then(async ({ verification, failureClass, durationMs }) => {
3186
3132
  activeVerifierRunIdsRef.current.delete(run.id);
3187
- void (async () => {
3188
- const status = code === 0 ? "pass" : "fail";
3189
- const failureClass = timedOut
3190
- ? "verifier_timeout"
3191
- : forcedOutput?.startsWith("Verifier process error:")
3192
- ? "verifier_spawn_error"
3193
- : status === "fail"
3194
- ? "verifier_failure"
3195
- : "verifier_pass";
3196
- const summary = (forcedOutput ?? output).trim() ||
3197
- (code === 0 ? "Verifier passed." : "Verifier failed.");
3198
- const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
3199
- await writeFile(outputPath, summary + "\n", "utf-8");
3200
- const runWithVerifier = {
3201
- ...latestRun,
3202
- verifier: {
3203
- ...latestRun.verifier,
3204
- description: latestRun.verifier?.description ?? "Goal verifier",
3205
- command: run.verifier?.command,
3206
- lastResult: {
3207
- status,
3208
- summary,
3209
- command: run.verifier?.command,
3210
- exitCode: code ?? 1,
3211
- outputPath,
3212
- checkedAt: new Date().toISOString(),
3213
- },
3214
- },
3215
- };
3216
- const completionCheck = canCompleteGoalRun(runWithVerifier);
3217
- const verifiedRun = await upsertGoalRun(props.cwd, {
3218
- ...runWithVerifier,
3219
- continueRequestedAt: undefined,
3220
- status: status === "pass" && completionCheck.ok
3221
- ? "passed"
3222
- : status === "pass"
3223
- ? "ready"
3224
- : "failed",
3225
- });
3226
- await appendGoalEvidence(props.cwd, run.id, {
3227
- kind: "command",
3228
- label: `Verifier ${status}`,
3229
- content: `${failureClass}: ${summary}`.slice(0, 4000),
3230
- path: outputPath,
3231
- });
3232
- await appendGoalDecision(props.cwd, run.id, {
3233
- kind: `verifier_${status}`,
3234
- reason: `${failureClass}: verifier exited with code ${code ?? 1}.`,
3235
- content: `outputPath=${outputPath}; durationMs=${Date.now() - startedAt}`,
3236
- });
3237
- if (status === "fail" && shouldCreateVerifierFixTask(latestRun)) {
3238
- await updateGoalTask(props.cwd, run.id, `fix-${Date.now()}`, {
3239
- title: "Fix verifier failure",
3240
- prompt: `Goal verifier failed after ${Date.now() - startedAt}ms. Original goal: ${run.goal}\n\n` +
3241
- `Verifier command: ${run.verifier?.command}\n\n` +
3242
- `Failure output:\n${summary.slice(-6000)}\n\nFix the cause, record evidence with the goals tool, and rerun relevant verification.`,
3243
- status: "pending",
3244
- });
3245
- const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
3246
- await upsertGoalRun(props.cwd, { ...runWithPendingFix, status: "ready" });
3247
- }
3248
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3249
- appendGoalProgress({
3250
- kind: "goal_progress",
3251
- phase: "verifier_finished",
3252
- title: `Verifier ${status}: ${run.title}`,
3253
- detail: summarizeGoalCompletion(summary),
3254
- status,
3255
- });
3256
- upsertGoalStatusEntry({
3257
- runId: run.id,
3258
- label: run.title,
3259
- phase: status === "pass" ? "reviewing" : "failed",
3260
- startedAt: Date.now(),
3261
- detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
3262
- goalNumber: goalNumberForRun(run.id),
3133
+ const status = verification.status;
3134
+ const summary = verification.summary;
3135
+ const outputPath = verification.outputPath;
3136
+ const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
3137
+ const runWithVerifier = {
3138
+ ...latestRun,
3139
+ verifier: {
3140
+ ...latestRun.verifier,
3141
+ description: latestRun.verifier?.description ?? "Goal verifier",
3142
+ command: run.verifier?.command,
3143
+ lastResult: verification,
3144
+ },
3145
+ };
3146
+ const completionCheck = canCompleteGoalRun(runWithVerifier);
3147
+ const verifiedRun = await upsertGoalRun(props.cwd, {
3148
+ ...runWithVerifier,
3149
+ continueRequestedAt: undefined,
3150
+ status: status === "pass" && completionCheck.ok
3151
+ ? "passed"
3152
+ : status === "pass"
3153
+ ? "ready"
3154
+ : "failed",
3155
+ });
3156
+ await appendGoalEvidence(props.cwd, run.id, {
3157
+ kind: "command",
3158
+ label: `Verifier ${status}`,
3159
+ content: `${failureClass}: ${summary}`.slice(0, 4000),
3160
+ path: outputPath,
3161
+ });
3162
+ await appendGoalDecision(props.cwd, run.id, {
3163
+ kind: `verifier_${status}`,
3164
+ reason: `${failureClass}: verifier exited with code ${verification.exitCode ?? 1}.`,
3165
+ content: `outputPath=${outputPath ?? ""}; durationMs=${durationMs}`,
3166
+ });
3167
+ if (status === "fail" && shouldCreateVerifierFixTask(latestRun)) {
3168
+ await updateGoalTask(props.cwd, run.id, `fix-${Date.now()}`, {
3169
+ title: "Fix verifier failure",
3170
+ prompt: `Goal verifier failed after ${durationMs}ms. Original goal: ${run.goal}\n\n` +
3171
+ `Verifier command: ${run.verifier?.command}\n\n` +
3172
+ `Failure output:\n${summary.slice(-6000)}\n\nFix the cause, record evidence with the goals tool, and rerun relevant verification.`,
3173
+ status: "pending",
3263
3174
  });
3264
- const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status, run.verifier?.command ?? "", code ?? 1, summary);
3265
- runGoalSyntheticEvent(eventText);
3266
- const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
3267
- if (continuationRun?.continueRequestedAt && status === "pass") {
3268
- setTimeout(() => continueGoalRun(run.id), 500);
3269
- }
3270
- })().catch((err) => {
3271
- clearGoalStatusEntry(run.id);
3272
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3273
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
3175
+ const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
3176
+ await upsertGoalRun(props.cwd, { ...runWithPendingFix, status: "ready" });
3177
+ }
3178
+ setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3179
+ appendGoalProgress({
3180
+ kind: "goal_progress",
3181
+ phase: "verifier_finished",
3182
+ title: `Verifier ${status}: ${run.title}`,
3183
+ detail: summarizeGoalCompletion(summary),
3184
+ status,
3274
3185
  });
3275
- };
3276
- child.on("close", (code) => finishVerifier(code));
3277
- child.on("error", (err) => finishVerifier(1, `Verifier process error: ${err.message}`));
3186
+ upsertGoalStatusEntry({
3187
+ runId: run.id,
3188
+ label: run.title,
3189
+ phase: status === "pass" ? "reviewing" : "failed",
3190
+ startedAt: Date.now(),
3191
+ detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
3192
+ goalNumber: goalNumberForRun(run.id),
3193
+ });
3194
+ const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
3195
+ runGoalSyntheticEvent(eventText);
3196
+ const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
3197
+ if (continuationRun?.continueRequestedAt && status === "pass") {
3198
+ setTimeout(() => continueGoalRun(run.id), 500);
3199
+ }
3200
+ })
3201
+ .catch((err) => {
3202
+ activeVerifierRunIdsRef.current.delete(run.id);
3203
+ clearGoalStatusEntry(run.id);
3204
+ log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3205
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
3206
+ });
3278
3207
  }, [
3279
3208
  props.cwd,
3280
3209
  appendGoalProgress,
@@ -3364,14 +3293,10 @@ export function App(props) {
3364
3293
  activeLanguages: detectedForPixelFix,
3365
3294
  tools: toolsForPixelFix,
3366
3295
  });
3367
- // Now that the cwd swap is committed, reset chat. Doing this BEFORE
3368
- // the chdir would print a banner with the old cwd, then bumping
3369
- // staticKey would print a second banner with the new cwd — leaving
3370
- // two banners stacked in the scrollback.
3371
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
3296
+ // Now that the cwd swap is committed, reset chat. Do not clear the
3297
+ // terminal here; terminal clear sequences can erase saved scrollback.
3372
3298
  setHistory([{ kind: "banner", id: "banner" }]);
3373
3299
  setLiveItems([]);
3374
- setStaticKey((k) => k + 1);
3375
3300
  messagesRef.current = messagesRef.current.slice(0, 1);
3376
3301
  agentLoop.reset();
3377
3302
  persistedIndexRef.current = messagesRef.current.length;
@@ -3403,7 +3328,7 @@ export function App(props) {
3403
3328
  setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3404
3329
  }
3405
3330
  })();
3406
- }, [props.cwd, stdout, agentLoop, currentProvider, currentModel]);
3331
+ }, [props.cwd, agentLoop, currentProvider, currentModel]);
3407
3332
  startPixelFixRef.current = startPixelFix;
3408
3333
  // Seed from sessionStore so "Fix All" chaining survives a deferred
3409
3334
  // resetUI() if it fires between pixel fixes (e.g. user toggled a pane).
@@ -3431,12 +3356,13 @@ export function App(props) {
3431
3356
  overlayPane: overlay,
3432
3357
  isAgentRunning: agentLoop.isRunning,
3433
3358
  });
3434
- return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: shouldHideStaticItemsForOverlayView({
3435
- shouldHideHistoryForOverlay,
3436
- stabilizeOverlayPaneRerender,
3437
- })
3438
- ? []
3439
- : history, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, getStaticHistoryKey({ resizeKey, staticKey })), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
3359
+ const staticItems = shouldHideStaticItemsForOverlayView({
3360
+ shouldHideHistoryForOverlay,
3361
+ stabilizeOverlayPaneRerender,
3362
+ })
3363
+ ? []
3364
+ : history;
3365
+ return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: staticItems, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, getStaticHistoryKey({ resizeKey })), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
3440
3366
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3441
3367
  props.sessionStore.overlay = null;
3442
3368
  props.resetUI();
@@ -3586,10 +3512,8 @@ export function App(props) {
3586
3512
  approvedPlanPathRef.current = planPath;
3587
3513
  planStepsRef.current = steps;
3588
3514
  setPlanSteps(steps);
3589
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
3590
3515
  setHistory([{ kind: "banner", id: "banner" }]);
3591
3516
  setLiveItems([]);
3592
- setStaticKey((k) => k + 1);
3593
3517
  setPlanAutoExpand(false);
3594
3518
  setOverlay(null);
3595
3519
  messagesRef.current = [{ role: "system", content: newPrompt }];
@@ -3630,8 +3554,6 @@ export function App(props) {
3630
3554
  });
3631
3555
  return;
3632
3556
  }
3633
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
3634
- setStaticKey((k) => k + 1);
3635
3557
  setPlanAutoExpand(false);
3636
3558
  setOverlay(null);
3637
3559
  setDoneStatus(null);
@@ -3646,8 +3568,6 @@ export function App(props) {
3646
3568
  });
3647
3569
  } })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingRight: 1, children: [liveItems.map((item) => renderItem(item)), _jsx(StreamingArea, { isRunning: agentLoop.isRunning, streamingText: agentLoop.streamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, planMode: planMode })] }), agentLoop.isRunning && agentLoop.activityPhase !== "idle" ? (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: agentLoop.activityPhase === "thinking" ? THINKING_BORDER_COLORS[0] : "transparent", paddingLeft: 1, paddingRight: 1, width: columns, children: _jsx(ActivityIndicator, { phase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, runStartRef: agentLoop.runStartRef, thinkingMs: agentLoop.thinkingMs, isThinking: agentLoop.isThinking, thinkingEnabled: thinkingEnabled, tokenEstimate: agentLoop.streamedTokenEstimate, charCountRef: agentLoop.charCountRef, realTokensAccumRef: agentLoop.realTokensAccumRef, userMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name), planMode: planMode, retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length, staticDisplay: true }) })) : agentLoop.stallError ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.warning, children: "⚠ API provider stream interrupted — retries exhausted." }), _jsx(Text, { color: theme.textDim, children: " Your conversation is preserved. Send a message to continue." })] })) : (doneStatus &&
3648
3570
  !agentLoop.isRunning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.success, children: ["✻ ", doneStatus.verb, " ", formatDuration(doneStatus.durationMs)] }) }))), agentLoop.queuedCount > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.accent, children: ["⏳ ", agentLoop.queuedCount, " message", agentLoop.queuedCount > 1 ? "s" : "", " queued"] }) })), _jsx(InputArea, { onSubmit: handleSubmit, onAbort: handleAbort, disabled: agentLoop.isRunning, isActive: !taskBarFocused && !overlay, onDownAtEnd: handleFocusTaskBar, onShiftTab: handleToggleThinking, onToggleTasks: () => {
3649
- // While the agent is running, skip the screen-clear + staticKey
3650
- // bump that would otherwise wipe the chat history from scrollback.
3651
3571
  // Just flip the overlay state — Ink's log-update handles the
3652
3572
  // live-area transition (chat input → TaskOverlay) natively, and
3653
3573
  // the chat history above stays in scrollback. When the overlay