@kenkaiiii/ggcoder 4.3.208 → 4.3.210

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 (85) hide show
  1. package/dist/core/compaction/compactor.d.ts +4 -0
  2. package/dist/core/compaction/compactor.d.ts.map +1 -1
  3. package/dist/core/compaction/compactor.js +34 -3
  4. package/dist/core/compaction/compactor.js.map +1 -1
  5. package/dist/core/compaction/compactor.test.js +20 -1
  6. package/dist/core/compaction/compactor.test.js.map +1 -1
  7. package/dist/core/goal-controller.js +9 -9
  8. package/dist/core/goal-controller.js.map +1 -1
  9. package/dist/core/goal-controller.test.js +76 -12
  10. package/dist/core/goal-controller.test.js.map +1 -1
  11. package/dist/core/goal-lifecycle-smoke.test.js +2 -3
  12. package/dist/core/goal-lifecycle-smoke.test.js.map +1 -1
  13. package/dist/core/goal-store.d.ts +1 -1
  14. package/dist/core/goal-store.d.ts.map +1 -1
  15. package/dist/core/goal-store.js +38 -2
  16. package/dist/core/goal-store.js.map +1 -1
  17. package/dist/core/goal-store.test.js +32 -0
  18. package/dist/core/goal-store.test.js.map +1 -1
  19. package/dist/core/process-manager-dev-server-repro.test.js +37 -1
  20. package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
  21. package/dist/core/process-manager.d.ts +9 -0
  22. package/dist/core/process-manager.d.ts.map +1 -1
  23. package/dist/core/process-manager.js +20 -6
  24. package/dist/core/process-manager.js.map +1 -1
  25. package/dist/core/prompt-commands.d.ts.map +1 -1
  26. package/dist/core/prompt-commands.js +41 -21
  27. package/dist/core/prompt-commands.js.map +1 -1
  28. package/dist/core/prompt-commands.test.js +87 -21
  29. package/dist/core/prompt-commands.test.js.map +1 -1
  30. package/dist/system-prompt.js +1 -1
  31. package/dist/system-prompt.js.map +1 -1
  32. package/dist/system-prompt.test.js +58 -0
  33. package/dist/system-prompt.test.js.map +1 -1
  34. package/dist/tools/bash.d.ts.map +1 -1
  35. package/dist/tools/bash.js +2 -51
  36. package/dist/tools/bash.js.map +1 -1
  37. package/dist/tools/goals.d.ts +1 -0
  38. package/dist/tools/goals.d.ts.map +1 -1
  39. package/dist/tools/goals.js +10 -2
  40. package/dist/tools/goals.js.map +1 -1
  41. package/dist/tools/goals.test.js +85 -0
  42. package/dist/tools/goals.test.js.map +1 -1
  43. package/dist/tools/grep.d.ts.map +1 -1
  44. package/dist/tools/grep.js +22 -5
  45. package/dist/tools/grep.js.map +1 -1
  46. package/dist/tools/grep.test.d.ts +2 -0
  47. package/dist/tools/grep.test.d.ts.map +1 -0
  48. package/dist/tools/grep.test.js +20 -0
  49. package/dist/tools/grep.test.js.map +1 -0
  50. package/dist/tools/safe-env.d.ts +2 -0
  51. package/dist/tools/safe-env.d.ts.map +1 -0
  52. package/dist/tools/safe-env.js +52 -0
  53. package/dist/tools/safe-env.js.map +1 -0
  54. package/dist/tools/web-fetch.d.ts +5 -0
  55. package/dist/tools/web-fetch.d.ts.map +1 -1
  56. package/dist/tools/web-fetch.js +12 -1
  57. package/dist/tools/web-fetch.js.map +1 -1
  58. package/dist/tools/web-fetch.test.js +9 -0
  59. package/dist/tools/web-fetch.test.js.map +1 -1
  60. package/dist/ui/App.d.ts +3 -0
  61. package/dist/ui/App.d.ts.map +1 -1
  62. package/dist/ui/App.js +171 -164
  63. package/dist/ui/App.js.map +1 -1
  64. package/dist/ui/app-state-persistence.test.js +56 -0
  65. package/dist/ui/app-state-persistence.test.js.map +1 -1
  66. package/dist/ui/components/GoalStatusBar.d.ts.map +1 -1
  67. package/dist/ui/components/GoalStatusBar.js +2 -1
  68. package/dist/ui/components/GoalStatusBar.js.map +1 -1
  69. package/dist/ui/goal-events.test.js +19 -0
  70. package/dist/ui/goal-events.test.js.map +1 -1
  71. package/dist/ui/goal-lifecycle-orchestration.test.d.ts +2 -0
  72. package/dist/ui/goal-lifecycle-orchestration.test.d.ts.map +1 -0
  73. package/dist/ui/goal-lifecycle-orchestration.test.js +445 -0
  74. package/dist/ui/goal-lifecycle-orchestration.test.js.map +1 -0
  75. package/dist/ui/goal-overlay.test.js +30 -0
  76. package/dist/ui/goal-overlay.test.js.map +1 -1
  77. package/dist/ui/goal-status-bar.test.js +29 -0
  78. package/dist/ui/goal-status-bar.test.js.map +1 -1
  79. package/dist/ui/queued-message.test.d.ts +2 -0
  80. package/dist/ui/queued-message.test.d.ts.map +1 -0
  81. package/dist/ui/queued-message.test.js +30 -0
  82. package/dist/ui/queued-message.test.js.map +1 -0
  83. package/dist/ui/slash-command-images.test.js +10 -0
  84. package/dist/ui/slash-command-images.test.js.map +1 -1
  85. package/package.json +6 -5
package/dist/ui/App.js CHANGED
@@ -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";
63
- import { canCompleteGoalRun, decideGoalNextAction, shouldCreateVerifierFixTask, } from "../core/goal-controller.js";
62
+ import { appendGoalDecision, appendGoalEvidence, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, summarizeGoalCounts, summarizeGoalCountsFromRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
63
+ import { canCompleteGoalRun, decideGoalNextAction } 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;
@@ -251,6 +250,53 @@ export function buildGoalSummaryRows(run) {
251
250
  }
252
251
  return rows.slice(0, 4);
253
252
  }
253
+ function goalTerminalProgressId(run) {
254
+ return `goal-terminal-${run.id}`;
255
+ }
256
+ function goalTerminalRunIdFromItem(item) {
257
+ if (item.kind !== "goal_progress" || item.phase !== "terminal")
258
+ return undefined;
259
+ if (!item.id.startsWith("goal-terminal-"))
260
+ return undefined;
261
+ return item.id.slice("goal-terminal-".length);
262
+ }
263
+ function goalProgressMatchesDraft(item, draft) {
264
+ return (item.title === draft.title &&
265
+ item.detail === draft.detail &&
266
+ item.status === draft.status &&
267
+ JSON.stringify(item.summaryRows ?? []) === JSON.stringify(draft.summaryRows ?? []));
268
+ }
269
+ export function completedItemsWithDurableGoalTerminalProgress(items, runs) {
270
+ const runIds = new Set(runs.map((run) => run.id));
271
+ const terminalByRun = new Map(runs
272
+ .map((run) => [run.id, formatGoalTerminalProgress(run)])
273
+ .filter((entry) => entry[1] !== null));
274
+ if (runIds.size === 0)
275
+ return items;
276
+ const upToDateRunIds = new Set();
277
+ let changed = false;
278
+ const reconciled = items.map((item, index) => {
279
+ const runId = goalTerminalRunIdFromItem(item);
280
+ if (!runId || !runIds.has(runId))
281
+ return item;
282
+ const draft = terminalByRun.get(runId);
283
+ if (draft && goalProgressMatchesDraft(item, draft)) {
284
+ upToDateRunIds.add(runId);
285
+ return item;
286
+ }
287
+ changed = true;
288
+ return { kind: "tombstone", id: `tombstone-${item.id}-${index}` };
289
+ });
290
+ const additions = [];
291
+ for (const [runId, progress] of terminalByRun) {
292
+ if (upToDateRunIds.has(runId))
293
+ continue;
294
+ additions.push({ ...progress, id: goalTerminalProgressId({ id: runId }) });
295
+ }
296
+ if (!changed && additions.length === 0)
297
+ return items;
298
+ return [...reconciled, ...additions];
299
+ }
254
300
  export function formatGoalTerminalProgress(run) {
255
301
  switch (run.status) {
256
302
  case "passed":
@@ -325,10 +371,11 @@ export function getStaticHistoryKey({ resizeKey }) {
325
371
  }
326
372
  // flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
327
373
  /** Check whether an item is still active (running spinner, pending result). */
328
- function isActiveItem(item) {
374
+ export function isActiveItem(item) {
329
375
  switch (item.kind) {
330
376
  case "tool_start":
331
377
  case "server_tool_start":
378
+ case "queued":
332
379
  case "compacting":
333
380
  return true;
334
381
  case "tool_group":
@@ -727,6 +774,7 @@ export function App(props) {
727
774
  if (cancelled)
728
775
  return;
729
776
  setGoalCount(counts.active);
777
+ setHistory((prev) => completedItemsWithDurableGoalTerminalProgress(prev, runs));
730
778
  setGoalStatusEntries((prev) => {
731
779
  const next = reconcileGoalStatusEntriesWithRuns(prev, runs, {
732
780
  isWorkerActive: (workerId, run) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId &&
@@ -2530,11 +2578,23 @@ export function App(props) {
2530
2578
  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));
2531
2579
  case "goal_progress": {
2532
2580
  const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
2533
- const color = item.phase === "terminal" && !isError
2534
- ? theme.success
2535
- : isError
2536
- ? theme.warning
2537
- : theme.primary;
2581
+ const color = isError
2582
+ ? theme.error
2583
+ : item.phase === "worker_finished"
2584
+ ? theme.success
2585
+ : item.phase === "verifier_finished"
2586
+ ? theme.accent
2587
+ : item.phase === "orchestrator_reviewing" || item.phase === "orchestrator_working"
2588
+ ? theme.secondary
2589
+ : item.phase === "continuing"
2590
+ ? theme.warning
2591
+ : item.phase === "verifier_started"
2592
+ ? theme.accent
2593
+ : item.phase === "worker_started"
2594
+ ? theme.primary
2595
+ : item.phase === "terminal"
2596
+ ? theme.success
2597
+ : theme.primary;
2538
2598
  const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
2539
2599
  ? "✓ "
2540
2600
  : item.phase === "terminal"
@@ -2604,7 +2664,7 @@ export function App(props) {
2604
2664
  case "step_done":
2605
2665
  return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "✓ " }), _jsx(Text, { color: theme.success, bold: true, children: `Step ${item.stepNum} done` }), item.description ? (_jsx(Text, { color: theme.textDim, children: ` — ${item.description}` })) : null] }) }, item.id));
2606
2666
  case "queued":
2607
- return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: " Queued: " }), _jsxs(Text, { color: theme.text, wrap: "wrap", children: [item.text, item.imageCount
2667
+ return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.warning, bold: true, children: " " }), _jsx(Text, { color: theme.textDim, children: "Queued: " }), _jsxs(Text, { color: theme.text, wrap: "wrap", children: [item.text, item.imageCount
2608
2668
  ? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
2609
2669
  : ""] })] }, item.id));
2610
2670
  case "compacting":
@@ -2791,8 +2851,10 @@ export function App(props) {
2791
2851
  content: `terminal=${status}`,
2792
2852
  });
2793
2853
  const terminalProgress = formatGoalTerminalProgress(nextRun);
2794
- if (terminalProgress)
2795
- appendGoalProgress(terminalProgress);
2854
+ if (terminalProgress) {
2855
+ const item = { ...terminalProgress, id: goalTerminalProgressId(nextRun) };
2856
+ setLiveItems((prev) => completedItemsWithDurableGoalTerminalProgress([...prev, item], [nextRun]));
2857
+ }
2796
2858
  runningGoalIdsRef.current.delete(runId);
2797
2859
  clearGoalStatusEntry(runId);
2798
2860
  return;
@@ -2813,8 +2875,8 @@ export function App(props) {
2813
2875
  appendGoalProgress({
2814
2876
  kind: "goal_progress",
2815
2877
  phase: "continuing",
2816
- title: `Continuing Goal: ${latestRun.title}`,
2817
- 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.",
2818
2880
  status: latestRun.status,
2819
2881
  });
2820
2882
  upsertGoalStatusEntry({
@@ -2915,8 +2977,10 @@ export function App(props) {
2915
2977
  await appendGoalDecision(props.cwd, run.id, decision);
2916
2978
  if (decision.kind === "terminal") {
2917
2979
  const terminalProgress = formatGoalTerminalProgress(run);
2918
- if (terminalProgress)
2919
- appendGoalProgress(terminalProgress);
2980
+ if (terminalProgress) {
2981
+ const item = { ...terminalProgress, id: goalTerminalProgressId(run) };
2982
+ setLiveItems((prev) => completedItemsWithDurableGoalTerminalProgress([...prev, item], [run]));
2983
+ }
2920
2984
  runningGoalIdsRef.current.delete(run.id);
2921
2985
  clearGoalStatusEntry(run.id);
2922
2986
  return;
@@ -2988,20 +3052,21 @@ export function App(props) {
2988
3052
  return;
2989
3053
  }
2990
3054
  if (decision.kind === "pause") {
2991
- await updateGoalTask(props.cwd, run.id, decision.task.id, {
3055
+ const runWithBlockedTask = (await updateGoalTask(props.cwd, run.id, decision.task.id, {
2992
3056
  status: "blocked",
2993
3057
  attempts: decision.attempts,
2994
3058
  lastSummary: "Paused after worker attempt limit.",
2995
- });
2996
- await upsertGoalRun(props.cwd, {
2997
- ...run,
2998
- status: "paused",
2999
- blockers: [...run.blockers, decision.reason],
3000
- });
3001
- await appendGoalEvidence(props.cwd, run.id, {
3059
+ })) ?? run;
3060
+ const runWithPauseEvidence = (await appendGoalEvidence(props.cwd, run.id, {
3002
3061
  kind: "summary",
3003
3062
  label: "Goal paused",
3004
3063
  content: decision.reason,
3064
+ })) ?? runWithBlockedTask;
3065
+ await upsertGoalRun(props.cwd, {
3066
+ ...runWithPauseEvidence,
3067
+ status: "paused",
3068
+ continueRequestedAt: undefined,
3069
+ blockers: Array.from(new Set([...runWithPauseEvidence.blockers, decision.reason])),
3005
3070
  });
3006
3071
  setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3007
3072
  appendGoalProgress({
@@ -3015,7 +3080,9 @@ export function App(props) {
3015
3080
  clearGoalStatusEntry(run.id);
3016
3081
  return;
3017
3082
  }
3018
- await updateGoalTask(props.cwd, run.id, decision.task.id, { attempts: decision.attempts });
3083
+ const runWithAttempt = (await updateGoalTask(props.cwd, run.id, decision.task.id, {
3084
+ attempts: decision.attempts,
3085
+ })) ?? run;
3019
3086
  const worker = await startGoalWorker({
3020
3087
  cwd: props.cwd,
3021
3088
  provider: currentProvider,
@@ -3025,11 +3092,15 @@ export function App(props) {
3025
3092
  taskTitle: decision.task.title,
3026
3093
  prompt: decision.task.prompt,
3027
3094
  });
3095
+ const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? runWithAttempt;
3028
3096
  await upsertGoalRun(props.cwd, {
3029
- ...run,
3097
+ ...latestRun,
3030
3098
  status: "running",
3031
3099
  activeWorkerId: worker.id,
3032
3100
  continueRequestedAt: undefined,
3101
+ tasks: latestRun.tasks.map((item) => item.id === decision.task.id
3102
+ ? { ...item, status: "running", workerId: worker.id, attempts: decision.attempts }
3103
+ : item),
3033
3104
  });
3034
3105
  setOverlay(null);
3035
3106
  setGoalCount((await summarizeGoalCounts(props.cwd)).active);
@@ -3110,143 +3181,74 @@ export function App(props) {
3110
3181
  detail: run.verifier.command,
3111
3182
  goalNumber: goalNumberForRun(run.id),
3112
3183
  });
3113
- const { spawn } = await import("node:child_process");
3114
- const { mkdir, writeFile } = await import("node:fs/promises");
3115
- const { join } = await import("node:path");
3116
- const logDir = join(projectDir(props.cwd), "verifiers");
3117
- await mkdir(logDir, { recursive: true });
3118
- const outputPath = join(logDir, `${run.id}-${startedAt}.log`);
3119
- const child = spawn(run.verifier.command, {
3184
+ void runGoalVerifierCommand({
3120
3185
  cwd: props.cwd,
3121
- shell: true,
3122
- stdio: ["ignore", "pipe", "pipe"],
3123
- env: { ...process.env },
3124
- });
3125
- let output = "";
3126
- child.stdout?.on("data", (chunk) => {
3127
- output += chunk.toString("utf-8");
3128
- if (output.length > 20_000)
3129
- output = output.slice(output.length - 20_000);
3130
- });
3131
- child.stderr?.on("data", (chunk) => {
3132
- output += chunk.toString("utf-8");
3133
- if (output.length > 20_000)
3134
- output = output.slice(output.length - 20_000);
3135
- });
3136
- let verifierSettled = false;
3137
- let timedOut = false;
3138
- const timeout = verifierTimeoutMs > 0
3139
- ? setTimeout(() => {
3140
- timedOut = true;
3141
- if (child.pid)
3142
- child.kill("SIGTERM");
3143
- const killTimer = setTimeout(() => {
3144
- if (!verifierSettled && child.pid)
3145
- child.kill("SIGKILL");
3146
- }, 5000);
3147
- killTimer.unref?.();
3148
- finishVerifier(124, `Verifier timed out after ${verifierTimeoutMs}ms and was terminated.\n${output}`);
3149
- }, verifierTimeoutMs)
3150
- : undefined;
3151
- timeout?.unref?.();
3152
- const finishVerifier = (code, forcedOutput) => {
3153
- if (verifierSettled)
3154
- return;
3155
- verifierSettled = true;
3156
- if (timeout)
3157
- clearTimeout(timeout);
3186
+ runId: run.id,
3187
+ command: run.verifier.command,
3188
+ timeoutMs: verifierTimeoutMs,
3189
+ now: () => startedAt,
3190
+ })
3191
+ .then(async ({ verification, failureClass, durationMs }) => {
3158
3192
  activeVerifierRunIdsRef.current.delete(run.id);
3159
- void (async () => {
3160
- const status = code === 0 ? "pass" : "fail";
3161
- const failureClass = timedOut
3162
- ? "verifier_timeout"
3163
- : forcedOutput?.startsWith("Verifier process error:")
3164
- ? "verifier_spawn_error"
3165
- : status === "fail"
3166
- ? "verifier_failure"
3167
- : "verifier_pass";
3168
- const summary = (forcedOutput ?? output).trim() ||
3169
- (code === 0 ? "Verifier passed." : "Verifier failed.");
3170
- const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
3171
- await writeFile(outputPath, summary + "\n", "utf-8");
3172
- const runWithVerifier = {
3173
- ...latestRun,
3174
- verifier: {
3175
- ...latestRun.verifier,
3176
- description: latestRun.verifier?.description ?? "Goal verifier",
3177
- command: run.verifier?.command,
3178
- lastResult: {
3179
- status,
3180
- summary,
3181
- command: run.verifier?.command,
3182
- exitCode: code ?? 1,
3183
- outputPath,
3184
- checkedAt: new Date().toISOString(),
3185
- },
3186
- },
3187
- };
3188
- const completionCheck = canCompleteGoalRun(runWithVerifier);
3189
- const verifiedRun = await upsertGoalRun(props.cwd, {
3190
- ...runWithVerifier,
3191
- continueRequestedAt: undefined,
3192
- status: status === "pass" && completionCheck.ok
3193
- ? "passed"
3194
- : status === "pass"
3195
- ? "ready"
3196
- : "failed",
3197
- });
3198
- await appendGoalEvidence(props.cwd, run.id, {
3199
- kind: "command",
3200
- label: `Verifier ${status}`,
3201
- content: `${failureClass}: ${summary}`.slice(0, 4000),
3202
- path: outputPath,
3203
- });
3204
- await appendGoalDecision(props.cwd, run.id, {
3205
- kind: `verifier_${status}`,
3206
- reason: `${failureClass}: verifier exited with code ${code ?? 1}.`,
3207
- content: `outputPath=${outputPath}; durationMs=${Date.now() - startedAt}`,
3208
- });
3209
- if (status === "fail" && shouldCreateVerifierFixTask(latestRun)) {
3210
- await updateGoalTask(props.cwd, run.id, `fix-${Date.now()}`, {
3211
- title: "Fix verifier failure",
3212
- prompt: `Goal verifier failed after ${Date.now() - startedAt}ms. Original goal: ${run.goal}\n\n` +
3213
- `Verifier command: ${run.verifier?.command}\n\n` +
3214
- `Failure output:\n${summary.slice(-6000)}\n\nFix the cause, record evidence with the goals tool, and rerun relevant verification.`,
3215
- status: "pending",
3216
- });
3217
- const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
3218
- await upsertGoalRun(props.cwd, { ...runWithPendingFix, status: "ready" });
3219
- }
3220
- setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3221
- appendGoalProgress({
3222
- kind: "goal_progress",
3223
- phase: "verifier_finished",
3224
- title: `Verifier ${status}: ${run.title}`,
3225
- detail: summarizeGoalCompletion(summary),
3226
- status,
3227
- });
3228
- upsertGoalStatusEntry({
3229
- runId: run.id,
3230
- label: run.title,
3231
- phase: status === "pass" ? "reviewing" : "failed",
3232
- startedAt: Date.now(),
3233
- detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
3234
- goalNumber: goalNumberForRun(run.id),
3235
- });
3236
- const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status, run.verifier?.command ?? "", code ?? 1, summary);
3237
- runGoalSyntheticEvent(eventText);
3238
- const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
3239
- if (continuationRun?.continueRequestedAt && status === "pass") {
3240
- setTimeout(() => continueGoalRun(run.id), 500);
3241
- }
3242
- })().catch((err) => {
3243
- clearGoalStatusEntry(run.id);
3244
- log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3245
- setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
3193
+ const status = verification.status;
3194
+ const summary = verification.summary;
3195
+ const outputPath = verification.outputPath;
3196
+ const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
3197
+ const runWithVerifier = {
3198
+ ...latestRun,
3199
+ verifier: {
3200
+ ...latestRun.verifier,
3201
+ description: latestRun.verifier?.description ?? "Goal verifier",
3202
+ command: run.verifier?.command,
3203
+ lastResult: verification,
3204
+ },
3205
+ };
3206
+ const completionCheck = canCompleteGoalRun(runWithVerifier);
3207
+ const verifiedRun = await upsertGoalRun(props.cwd, {
3208
+ ...runWithVerifier,
3209
+ continueRequestedAt: status === "pass" && completionCheck.ok ? undefined : latestRun.continueRequestedAt,
3210
+ status: status === "pass" && completionCheck.ok ? "passed" : "ready",
3246
3211
  });
3247
- };
3248
- child.on("close", (code) => finishVerifier(code));
3249
- child.on("error", (err) => finishVerifier(1, `Verifier process error: ${err.message}`));
3212
+ await appendGoalEvidence(props.cwd, run.id, {
3213
+ kind: "command",
3214
+ label: `Verifier ${status}`,
3215
+ content: `${failureClass}: ${summary}`.slice(0, 4000),
3216
+ path: outputPath,
3217
+ });
3218
+ await appendGoalDecision(props.cwd, run.id, {
3219
+ kind: `verifier_${status}`,
3220
+ reason: `${failureClass}: verifier exited with code ${verification.exitCode ?? 1}.`,
3221
+ content: `outputPath=${outputPath ?? ""}; durationMs=${durationMs}`,
3222
+ });
3223
+ setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3224
+ appendGoalProgress({
3225
+ kind: "goal_progress",
3226
+ phase: "verifier_finished",
3227
+ title: `Verifier ${status}: ${run.title}`,
3228
+ detail: summarizeGoalCompletion(summary),
3229
+ status,
3230
+ });
3231
+ upsertGoalStatusEntry({
3232
+ runId: run.id,
3233
+ label: run.title,
3234
+ phase: status === "pass" ? "reviewing" : "failed",
3235
+ startedAt: Date.now(),
3236
+ detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
3237
+ goalNumber: goalNumberForRun(run.id),
3238
+ });
3239
+ const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
3240
+ runGoalSyntheticEvent(eventText);
3241
+ const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
3242
+ if (continuationRun?.continueRequestedAt || status === "fail") {
3243
+ setTimeout(() => continueGoalRun(run.id), 500);
3244
+ }
3245
+ })
3246
+ .catch((err) => {
3247
+ activeVerifierRunIdsRef.current.delete(run.id);
3248
+ clearGoalStatusEntry(run.id);
3249
+ log("ERROR", "goal", err instanceof Error ? err.message : String(err));
3250
+ setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
3251
+ });
3250
3252
  }, [
3251
3253
  props.cwd,
3252
3254
  appendGoalProgress,
@@ -3260,7 +3262,12 @@ export function App(props) {
3260
3262
  runningGoalIdsRef.current.delete(run.id);
3261
3263
  if (run.activeWorkerId)
3262
3264
  await stopGoalWorker(run.activeWorkerId);
3263
- await upsertGoalRun(props.cwd, { ...run, status: "paused", activeWorkerId: undefined });
3265
+ const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
3266
+ await upsertGoalRun(props.cwd, {
3267
+ ...latestRun,
3268
+ status: "paused",
3269
+ activeWorkerId: undefined,
3270
+ });
3264
3271
  setGoalCount((await summarizeGoalCounts(props.cwd)).active);
3265
3272
  appendGoalProgress({
3266
3273
  kind: "goal_progress",
@@ -3610,7 +3617,7 @@ export function App(props) {
3610
3617
  setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
3611
3618
  });
3612
3619
  } })) : (_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 &&
3613
- !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: () => {
3620
+ !agentLoop.isRunning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.success, children: ["✻ ", doneStatus.verb, " ", formatDuration(doneStatus.durationMs)] }) }))), agentLoop.queuedCount > 0 && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.warning, bold: true, children: " " }), _jsxs(Text, { color: theme.textDim, 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: () => {
3614
3621
  // Just flip the overlay state — Ink's log-update handles the
3615
3622
  // live-area transition (chat input → TaskOverlay) natively, and
3616
3623
  // the chat history above stays in scrollback. When the overlay