@prestyj/cli 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 (55) 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 +3 -3
  34. package/dist/ui/App.d.ts.map +1 -1
  35. package/dist/ui/App.js +186 -211
  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/hooks/useAgentLoop.d.ts +3 -1
  44. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  45. package/dist/ui/hooks/useAgentLoop.js +10 -2
  46. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  47. package/dist/ui/render.d.ts +7 -0
  48. package/dist/ui/render.d.ts.map +1 -1
  49. package/dist/ui/render.js +1 -0
  50. package/dist/ui/render.js.map +1 -1
  51. package/dist/ui/scroll-stabilization.test.js +2 -2
  52. package/dist/ui/scroll-stabilization.test.js.map +1 -1
  53. package/dist/ui/slash-command-images.test.js +10 -0
  54. package/dist/ui/slash-command-images.test.js.map +1 -1
  55. 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 ezcoder 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). */
@@ -481,11 +479,29 @@ function markTaskInProgress(cwd, taskId) {
481
479
  // ignore
482
480
  }
483
481
  }
482
+ /**
483
+ * Mark a task as done in the on-disk tasks.json. Used by run-all auto-advance
484
+ * so the in-flight task is recorded as completed even when the agent forgets
485
+ * to call the `tasks` tool. Errors are swallowed — if the file is missing or
486
+ * the id is stale, the next auto-advance step still proceeds correctly.
487
+ */
488
+ function markTaskDone(cwd, taskId) {
489
+ try {
490
+ const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
491
+ const filePath = join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json");
492
+ const data = readFileSync(filePath, "utf-8");
493
+ const tasks = JSON.parse(data);
494
+ const updated = tasks.map((t) => (t.id === taskId ? { ...t, status: "done" } : t));
495
+ writeFileSync(filePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
496
+ }
497
+ catch {
498
+ // ignore
499
+ }
500
+ }
484
501
  // ── App Component ──────────────────────────────────────────
485
502
  export function App(props) {
486
503
  const theme = useTheme();
487
504
  const switchTheme = useSetTheme();
488
- const { stdout } = useStdout();
489
505
  const { columns, resizeKey } = useTerminalSize();
490
506
  // Hoisted before terminal title hook so it can reference them
491
507
  const [lastUserMessage, setLastUserMessage] = useState("");
@@ -537,6 +553,11 @@ export function App(props) {
537
553
  // remount that startTask() triggers between tasks.
538
554
  const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
539
555
  const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
556
+ // Task id of the run-all task currently driving the agent. Tracked so the
557
+ // run-all `onDone` auto-advance can mark the just-finished task as done
558
+ // even when the agent forgets to call the tasks tool. Mirrored through
559
+ // sessionStore so the value survives the resetUI() remount between tasks.
560
+ const currentTaskIdRef = useRef(props.sessionStore?.currentTaskId ?? null);
540
561
  const startTaskRef = useRef(() => { });
541
562
  const agentRunningRef = useRef(false);
542
563
  const runningGoalIdsRef = useRef(new Set());
@@ -547,7 +568,6 @@ export function App(props) {
547
568
  const startPixelFixRef = useRef(() => { });
548
569
  const cwdRef = useRef(props.cwd);
549
570
  const [displayedCwd, setDisplayedCwd] = useState(props.cwd);
550
- const [staticKey, setStaticKey] = useState(0);
551
571
  const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
552
572
  // Suppress "done" status when a plan overlay is about to open
553
573
  const planOverlayPendingRef = useRef(false);
@@ -906,14 +926,6 @@ export function App(props) {
906
926
  // premature "done" status that fires when the agent loop finishes
907
927
  planOverlayPendingRef.current = true;
908
928
  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
929
  setPlanAutoExpand(true);
918
930
  setOverlay("plan");
919
931
  // Don't clear planOverlayPendingRef here — keep it true until
@@ -928,7 +940,7 @@ export function App(props) {
928
940
  planPath);
929
941
  };
930
942
  }
931
- }, [props.onExitPlanRef, replaceSystemPrompt, stdout]);
943
+ }, [props.onExitPlanRef, replaceSystemPrompt]);
932
944
  const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
933
945
  const sm = sessionManagerRef.current;
934
946
  if (!sm)
@@ -1674,7 +1686,7 @@ export function App(props) {
1674
1686
  return remaining;
1675
1687
  });
1676
1688
  }, []),
1677
- onDone: useCallback((durationMs, toolsUsed) => {
1689
+ onDone: useCallback((durationMs, toolsUsed, errored) => {
1678
1690
  log("INFO", "agent", `Agent done`, {
1679
1691
  duration: `${durationMs}ms`,
1680
1692
  toolsUsed: toolsUsed.join(",") || "none",
@@ -1701,6 +1713,18 @@ export function App(props) {
1701
1713
  if (runAllTasksRef.current) {
1702
1714
  setTimeout(() => {
1703
1715
  const cwd = cwdRef.current;
1716
+ // Clean turn: mark the in-flight task done so the row flips to ✓.
1717
+ // Error path: leave it at `in-progress` so the failed task is
1718
+ // visibly distinct from completed ones. The chain still advances
1719
+ // either way — matching how errors were handled before this fix.
1720
+ const finishedId = currentTaskIdRef.current;
1721
+ if (finishedId) {
1722
+ if (!errored)
1723
+ markTaskDone(cwd, finishedId);
1724
+ currentTaskIdRef.current = null;
1725
+ if (props.sessionStore)
1726
+ props.sessionStore.currentTaskId = null;
1727
+ }
1704
1728
  const next = getNextPendingTask(cwd);
1705
1729
  if (next) {
1706
1730
  markTaskInProgress(cwd, next.id);
@@ -1758,6 +1782,12 @@ export function App(props) {
1758
1782
  setRunAllTasks(false);
1759
1783
  setRunAllPixel(false);
1760
1784
  currentPixelFixRef.current = null;
1785
+ // Clear the run-all task id so a subsequent run-all does not
1786
+ // mistakenly mark this aborted task as done. The on-disk row stays
1787
+ // at `in-progress`, which is the correct truth for an aborted task.
1788
+ currentTaskIdRef.current = null;
1789
+ if (props.sessionStore)
1790
+ props.sessionStore.currentTaskId = null;
1761
1791
  setDoneStatus(null);
1762
1792
  setLiveItems((prev) => {
1763
1793
  const next = prev.map((item) => {
@@ -2045,14 +2075,10 @@ export function App(props) {
2045
2075
  process.exit(0);
2046
2076
  }
2047
2077
  // 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.
2078
+ // Avoid direct ANSI terminal clears here; they can erase scrollback.
2079
+ // Runtime state (model, provider, thinking) survives via renderApp's
2080
+ // closure-held `runtimeState`, mirrored from React state via the
2081
+ // useEffects above.
2056
2082
  if (trimmed === "/clear") {
2057
2083
  if (props.resetUI) {
2058
2084
  void (async () => {
@@ -2065,8 +2091,7 @@ export function App(props) {
2065
2091
  return;
2066
2092
  }
2067
2093
  // 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");
2094
+ // React state in place without touching terminal scrollback.
2070
2095
  pendingFlushRef.current = [];
2071
2096
  setHistory([{ kind: "banner", id: "banner" }]);
2072
2097
  setLiveItems([]);
@@ -2082,7 +2107,6 @@ export function App(props) {
2082
2107
  agentLoop.reset();
2083
2108
  setSessionTitle(undefined);
2084
2109
  sessionTitleGeneratedRef.current = false;
2085
- setStaticKey((k) => k + 1);
2086
2110
  setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
2087
2111
  return;
2088
2112
  }
@@ -2190,8 +2214,6 @@ export function App(props) {
2190
2214
  props.resetUI();
2191
2215
  }
2192
2216
  else {
2193
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2194
- setStaticKey((key) => key + 1);
2195
2217
  if (props.sessionStore) {
2196
2218
  props.sessionStore.overlay = "goal";
2197
2219
  props.sessionStore.planAutoExpand = false;
@@ -2549,11 +2571,23 @@ export function App(props) {
2549
2571
  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
2572
  case "goal_progress": {
2551
2573
  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;
2574
+ const color = isError
2575
+ ? theme.error
2576
+ : item.phase === "worker_finished"
2577
+ ? theme.success
2578
+ : item.phase === "verifier_finished"
2579
+ ? theme.accent
2580
+ : item.phase === "orchestrator_reviewing" || item.phase === "orchestrator_working"
2581
+ ? theme.secondary
2582
+ : item.phase === "continuing"
2583
+ ? theme.warning
2584
+ : item.phase === "verifier_started"
2585
+ ? theme.accent
2586
+ : item.phase === "worker_started"
2587
+ ? theme.primary
2588
+ : item.phase === "terminal"
2589
+ ? theme.success
2590
+ : theme.primary;
2557
2591
  const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
2558
2592
  ? "✓ "
2559
2593
  : item.phase === "terminal"
@@ -2662,8 +2696,14 @@ export function App(props) {
2662
2696
  // session creation is best-effort
2663
2697
  }
2664
2698
  }
2665
- if (props.sessionStore)
2699
+ if (props.sessionStore) {
2666
2700
  props.sessionStore.overlay = null;
2701
+ // Mirror through sessionStore so the post-resetUI remount can
2702
+ // re-seed the ref. markTaskDone() in the run-all onDone branch
2703
+ // reads currentTaskIdRef.current, which is seeded from this.
2704
+ props.sessionStore.currentTaskId = taskId;
2705
+ }
2706
+ currentTaskIdRef.current = taskId;
2667
2707
  props.resetUI?.({
2668
2708
  wipeSession: true,
2669
2709
  messages: newMessages,
@@ -2675,7 +2715,9 @@ export function App(props) {
2675
2715
  return;
2676
2716
  }
2677
2717
  // Fallback path (resetUI not wired — tests).
2678
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
2718
+ currentTaskIdRef.current = taskId;
2719
+ if (props.sessionStore)
2720
+ props.sessionStore.currentTaskId = taskId;
2679
2721
  setHistory([{ kind: "banner", id: "banner" }]);
2680
2722
  setLiveItems([]);
2681
2723
  messagesRef.current = messagesRef.current.slice(0, 1);
@@ -2709,15 +2751,7 @@ export function App(props) {
2709
2751
  setRunAllTasks(false);
2710
2752
  }
2711
2753
  })();
2712
- }, [
2713
- props.cwd,
2714
- props.resetUI,
2715
- props.sessionStore,
2716
- stdout,
2717
- agentLoop,
2718
- currentProvider,
2719
- currentModel,
2720
- ]);
2754
+ }, [props.cwd, props.resetUI, props.sessionStore, agentLoop, currentProvider, currentModel]);
2721
2755
  const openOverlay = useCallback((kind) => {
2722
2756
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2723
2757
  props.sessionStore.overlay = kind;
@@ -2738,7 +2772,7 @@ export function App(props) {
2738
2772
  setPlanAutoExpand(false);
2739
2773
  setOverlay(kind);
2740
2774
  }
2741
- }, [agentLoop.isRunning, props, stdout]);
2775
+ }, [agentLoop.isRunning, props]);
2742
2776
  const closeOverlay = useCallback(() => {
2743
2777
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
2744
2778
  props.sessionStore.overlay = null;
@@ -2750,7 +2784,7 @@ export function App(props) {
2750
2784
  }
2751
2785
  setOverlay(null);
2752
2786
  }
2753
- }, [agentLoop.isRunning, overlay, props, stdout]);
2787
+ }, [agentLoop.isRunning, overlay, props]);
2754
2788
  const runGoalSyntheticEvent = useCallback((eventText) => {
2755
2789
  const eventInfo = parseGoalSyntheticEvent(eventText);
2756
2790
  const detail = eventInfo?.kind === "worker"
@@ -2841,8 +2875,8 @@ export function App(props) {
2841
2875
  appendGoalProgress({
2842
2876
  kind: "goal_progress",
2843
2877
  phase: "continuing",
2844
- title: `Continuing Goal: ${latestRun.title}`,
2845
- detail: "Starting the next worker task or verifier step automatically.",
2878
+ title: `Choosing next Goal step: ${latestRun.title}`,
2879
+ detail: "Latest result is recorded; starting the next worker task or verifier automatically.",
2846
2880
  status: latestRun.status,
2847
2881
  });
2848
2882
  upsertGoalStatusEntry({
@@ -3138,143 +3172,89 @@ export function App(props) {
3138
3172
  detail: run.verifier.command,
3139
3173
  goalNumber: goalNumberForRun(run.id),
3140
3174
  });
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, {
3175
+ void runGoalVerifierCommand({
3148
3176
  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);
3177
+ runId: run.id,
3178
+ command: run.verifier.command,
3179
+ timeoutMs: verifierTimeoutMs,
3180
+ now: () => startedAt,
3181
+ })
3182
+ .then(async ({ verification, failureClass, durationMs }) => {
3186
3183
  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),
3184
+ const status = verification.status;
3185
+ const summary = verification.summary;
3186
+ const outputPath = verification.outputPath;
3187
+ const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
3188
+ const runWithVerifier = {
3189
+ ...latestRun,
3190
+ verifier: {
3191
+ ...latestRun.verifier,
3192
+ description: latestRun.verifier?.description ?? "Goal verifier",
3193
+ command: run.verifier?.command,
3194
+ lastResult: verification,
3195
+ },
3196
+ };
3197
+ const completionCheck = canCompleteGoalRun(runWithVerifier);
3198
+ const verifiedRun = await upsertGoalRun(props.cwd, {
3199
+ ...runWithVerifier,
3200
+ continueRequestedAt: undefined,
3201
+ status: status === "pass" && completionCheck.ok
3202
+ ? "passed"
3203
+ : status === "pass"
3204
+ ? "ready"
3205
+ : "failed",
3206
+ });
3207
+ await appendGoalEvidence(props.cwd, run.id, {
3208
+ kind: "command",
3209
+ label: `Verifier ${status}`,
3210
+ content: `${failureClass}: ${summary}`.slice(0, 4000),
3211
+ path: outputPath,
3212
+ });
3213
+ await appendGoalDecision(props.cwd, run.id, {
3214
+ kind: `verifier_${status}`,
3215
+ reason: `${failureClass}: verifier exited with code ${verification.exitCode ?? 1}.`,
3216
+ content: `outputPath=${outputPath ?? ""}; durationMs=${durationMs}`,
3217
+ });
3218
+ if (status === "fail" && shouldCreateVerifierFixTask(latestRun)) {
3219
+ await updateGoalTask(props.cwd, run.id, `fix-${Date.now()}`, {
3220
+ title: "Fix verifier failure",
3221
+ prompt: `Goal verifier failed after ${durationMs}ms. Original goal: ${run.goal}\n\n` +
3222
+ `Verifier command: ${run.verifier?.command}\n\n` +
3223
+ `Failure output:\n${summary.slice(-6000)}\n\nFix the cause, record evidence with the goals tool, and rerun relevant verification.`,
3224
+ status: "pending",
3263
3225
  });
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")]);
3226
+ const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
3227
+ await upsertGoalRun(props.cwd, { ...runWithPendingFix, status: "ready" });
3228
+ }
3229
+ setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3230
+ appendGoalProgress({
3231
+ kind: "goal_progress",
3232
+ phase: "verifier_finished",
3233
+ title: `Verifier ${status}: ${run.title}`,
3234
+ detail: summarizeGoalCompletion(summary),
3235
+ status,
3274
3236
  });
3275
- };
3276
- child.on("close", (code) => finishVerifier(code));
3277
- child.on("error", (err) => finishVerifier(1, `Verifier process error: ${err.message}`));
3237
+ upsertGoalStatusEntry({
3238
+ runId: run.id,
3239
+ label: run.title,
3240
+ phase: status === "pass" ? "reviewing" : "failed",
3241
+ startedAt: Date.now(),
3242
+ detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
3243
+ goalNumber: goalNumberForRun(run.id),
3244
+ });
3245
+ const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
3246
+ runGoalSyntheticEvent(eventText);
3247
+ const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
3248
+ if (continuationRun?.continueRequestedAt && status === "pass") {
3249
+ setTimeout(() => continueGoalRun(run.id), 500);
3250
+ }
3251
+ })
3252
+ .catch((err) => {
3253
+ activeVerifierRunIdsRef.current.delete(run.id);
3254
+ clearGoalStatusEntry(run.id);
3255
+ log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3256
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
3257
+ });
3278
3258
  }, [
3279
3259
  props.cwd,
3280
3260
  appendGoalProgress,
@@ -3308,8 +3288,12 @@ export function App(props) {
3308
3288
  startGoalRunRef.current = startGoalRun;
3309
3289
  useEffect(() => {
3310
3290
  runAllTasksRef.current = runAllTasks;
3311
- if (props.sessionStore)
3291
+ if (props.sessionStore) {
3312
3292
  props.sessionStore.runAllTasks = runAllTasks;
3293
+ // Keep currentTaskId in sync with the ref so a sessionStore-seeded
3294
+ // remount picks up the same value the imperative sites wrote.
3295
+ props.sessionStore.currentTaskId = currentTaskIdRef.current;
3296
+ }
3313
3297
  }, [runAllTasks, props.sessionStore]);
3314
3298
  useEffect(() => {
3315
3299
  agentRunningRef.current = agentLoop.isRunning;
@@ -3364,14 +3348,10 @@ export function App(props) {
3364
3348
  activeLanguages: detectedForPixelFix,
3365
3349
  tools: toolsForPixelFix,
3366
3350
  });
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");
3351
+ // Now that the cwd swap is committed, reset chat. Do not clear the
3352
+ // terminal here; terminal clear sequences can erase saved scrollback.
3372
3353
  setHistory([{ kind: "banner", id: "banner" }]);
3373
3354
  setLiveItems([]);
3374
- setStaticKey((k) => k + 1);
3375
3355
  messagesRef.current = messagesRef.current.slice(0, 1);
3376
3356
  agentLoop.reset();
3377
3357
  persistedIndexRef.current = messagesRef.current.length;
@@ -3403,7 +3383,7 @@ export function App(props) {
3403
3383
  setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3404
3384
  }
3405
3385
  })();
3406
- }, [props.cwd, stdout, agentLoop, currentProvider, currentModel]);
3386
+ }, [props.cwd, agentLoop, currentProvider, currentModel]);
3407
3387
  startPixelFixRef.current = startPixelFix;
3408
3388
  // Seed from sessionStore so "Fix All" chaining survives a deferred
3409
3389
  // resetUI() if it fires between pixel fixes (e.g. user toggled a pane).
@@ -3431,12 +3411,13 @@ export function App(props) {
3431
3411
  overlayPane: overlay,
3432
3412
  isAgentRunning: agentLoop.isRunning,
3433
3413
  });
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: () => {
3414
+ const staticItems = shouldHideStaticItemsForOverlayView({
3415
+ shouldHideHistoryForOverlay,
3416
+ stabilizeOverlayPaneRerender,
3417
+ })
3418
+ ? []
3419
+ : history;
3420
+ 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
3421
  if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
3441
3422
  props.sessionStore.overlay = null;
3442
3423
  props.resetUI();
@@ -3586,10 +3567,8 @@ export function App(props) {
3586
3567
  approvedPlanPathRef.current = planPath;
3587
3568
  planStepsRef.current = steps;
3588
3569
  setPlanSteps(steps);
3589
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
3590
3570
  setHistory([{ kind: "banner", id: "banner" }]);
3591
3571
  setLiveItems([]);
3592
- setStaticKey((k) => k + 1);
3593
3572
  setPlanAutoExpand(false);
3594
3573
  setOverlay(null);
3595
3574
  messagesRef.current = [{ role: "system", content: newPrompt }];
@@ -3630,8 +3609,6 @@ export function App(props) {
3630
3609
  });
3631
3610
  return;
3632
3611
  }
3633
- stdout?.write("\x1b[2J\x1b[3J\x1b[H");
3634
- setStaticKey((k) => k + 1);
3635
3612
  setPlanAutoExpand(false);
3636
3613
  setOverlay(null);
3637
3614
  setDoneStatus(null);
@@ -3646,8 +3623,6 @@ export function App(props) {
3646
3623
  });
3647
3624
  } })) : (_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
3625
  !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
3626
  // Just flip the overlay state — Ink's log-update handles the
3652
3627
  // live-area transition (chat input → TaskOverlay) natively, and
3653
3628
  // the chat history above stays in scrollback. When the overlay