@kenkaiiii/ggcoder 4.3.204 → 4.3.206
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.
- package/dist/cli.js +38 -26
- package/dist/cli.js.map +1 -1
- package/dist/config.js +2 -2
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +6 -0
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +16 -0
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/goal-controller.d.ts +57 -0
- package/dist/core/goal-controller.d.ts.map +1 -0
- package/dist/core/goal-controller.js +285 -0
- package/dist/core/goal-controller.js.map +1 -0
- package/dist/core/goal-controller.test.d.ts +2 -0
- package/dist/core/goal-controller.test.d.ts.map +1 -0
- package/dist/core/goal-controller.test.js +377 -0
- package/dist/core/goal-controller.test.js.map +1 -0
- package/dist/core/goal-lifecycle-smoke.test.d.ts +2 -0
- package/dist/core/goal-lifecycle-smoke.test.d.ts.map +1 -0
- package/dist/core/goal-lifecycle-smoke.test.js +207 -0
- package/dist/core/goal-lifecycle-smoke.test.js.map +1 -0
- package/dist/core/goal-store.d.ts +164 -0
- package/dist/core/goal-store.d.ts.map +1 -0
- package/dist/core/goal-store.js +721 -0
- package/dist/core/goal-store.js.map +1 -0
- package/dist/core/goal-store.test.d.ts +2 -0
- package/dist/core/goal-store.test.d.ts.map +1 -0
- package/dist/core/goal-store.test.js +341 -0
- package/dist/core/goal-store.test.js.map +1 -0
- package/dist/core/goal-verifier.d.ts +17 -0
- package/dist/core/goal-verifier.d.ts.map +1 -0
- package/dist/core/goal-verifier.js +84 -0
- package/dist/core/goal-verifier.js.map +1 -0
- package/dist/core/goal-verifier.test.d.ts +2 -0
- package/dist/core/goal-verifier.test.d.ts.map +1 -0
- package/dist/core/goal-verifier.test.js +88 -0
- package/dist/core/goal-verifier.test.js.map +1 -0
- package/dist/core/goal-worker.d.ts +47 -0
- package/dist/core/goal-worker.d.ts.map +1 -0
- package/dist/core/goal-worker.js +329 -0
- package/dist/core/goal-worker.js.map +1 -0
- package/dist/core/goal-worker.test.d.ts +2 -0
- package/dist/core/goal-worker.test.d.ts.map +1 -0
- package/dist/core/goal-worker.test.js +206 -0
- package/dist/core/goal-worker.test.js.map +1 -0
- package/dist/core/oauth/gemini.d.ts.map +1 -1
- package/dist/core/oauth/gemini.js +138 -30
- package/dist/core/oauth/gemini.js.map +1 -1
- package/dist/core/oauth/gemini.test.d.ts +2 -0
- package/dist/core/oauth/gemini.test.d.ts.map +1 -0
- package/dist/core/oauth/gemini.test.js +154 -0
- package/dist/core/oauth/gemini.test.js.map +1 -0
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +124 -0
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.js +36 -0
- package/dist/core/prompt-commands.test.js.map +1 -1
- package/dist/core/repomap.js +2 -0
- package/dist/core/repomap.js.map +1 -1
- package/dist/core/repomap.test.js +18 -0
- package/dist/core/repomap.test.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +20 -11
- package/dist/interactive.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +18 -50
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +124 -1
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/edit-diff.d.ts.map +1 -1
- package/dist/tools/edit-diff.js +71 -32
- package/dist/tools/edit-diff.js.map +1 -1
- package/dist/tools/edit-diff.test.js +14 -0
- package/dist/tools/edit-diff.test.js.map +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +16 -6
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +27 -0
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/tools/enter-plan.d.ts.map +1 -1
- package/dist/tools/enter-plan.js +1 -0
- package/dist/tools/enter-plan.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +2 -0
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/goals.d.ts +110 -0
- package/dist/tools/goals.d.ts.map +1 -0
- package/dist/tools/goals.js +500 -0
- package/dist/tools/goals.js.map +1 -0
- package/dist/tools/goals.test.d.ts +2 -0
- package/dist/tools/goals.test.d.ts.map +1 -0
- package/dist/tools/goals.test.js +431 -0
- package/dist/tools/goals.test.js.map +1 -0
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +2 -0
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/prompt-hints.d.ts.map +1 -1
- package/dist/tools/prompt-hints.js +2 -0
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/source-path.d.ts +9 -0
- package/dist/tools/source-path.d.ts.map +1 -0
- package/dist/tools/source-path.js +119 -0
- package/dist/tools/source-path.js.map +1 -0
- package/dist/tools/source-path.test.d.ts +2 -0
- package/dist/tools/source-path.test.d.ts.map +1 -0
- package/dist/tools/source-path.test.js +80 -0
- package/dist/tools/source-path.test.js.map +1 -0
- package/dist/tools/subagent.js +16 -0
- package/dist/tools/subagent.js.map +1 -1
- package/dist/ui/App.d.ts +36 -3
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +879 -57
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/activity-phrases.d.ts.map +1 -1
- package/dist/ui/activity-phrases.js +7 -3
- package/dist/ui/activity-phrases.js.map +1 -1
- package/dist/ui/app-state-persistence.test.d.ts +2 -0
- package/dist/ui/app-state-persistence.test.d.ts.map +1 -0
- package/dist/ui/app-state-persistence.test.js +56 -0
- package/dist/ui/app-state-persistence.test.js.map +1 -0
- package/dist/ui/components/BackgroundTasksBar.d.ts +16 -1
- package/dist/ui/components/BackgroundTasksBar.d.ts.map +1 -1
- package/dist/ui/components/BackgroundTasksBar.js +15 -2
- package/dist/ui/components/BackgroundTasksBar.js.map +1 -1
- package/dist/ui/components/Banner.d.ts +2 -1
- package/dist/ui/components/Banner.d.ts.map +1 -1
- package/dist/ui/components/Banner.js +3 -3
- package/dist/ui/components/Banner.js.map +1 -1
- package/dist/ui/components/GoalOverlay.d.ts +21 -0
- package/dist/ui/components/GoalOverlay.d.ts.map +1 -0
- package/dist/ui/components/GoalOverlay.js +336 -0
- package/dist/ui/components/GoalOverlay.js.map +1 -0
- package/dist/ui/components/GoalStatusBar.d.ts +24 -0
- package/dist/ui/components/GoalStatusBar.d.ts.map +1 -0
- package/dist/ui/components/GoalStatusBar.js +113 -0
- package/dist/ui/components/GoalStatusBar.js.map +1 -0
- package/dist/ui/components/InputArea.d.ts +2 -1
- package/dist/ui/components/InputArea.d.ts.map +1 -1
- package/dist/ui/components/InputArea.js +6 -1
- package/dist/ui/components/InputArea.js.map +1 -1
- package/dist/ui/components/ToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolExecution.js +94 -1
- package/dist/ui/components/ToolExecution.js.map +1 -1
- package/dist/ui/footer-status-layout.test.d.ts +2 -0
- package/dist/ui/footer-status-layout.test.d.ts.map +1 -0
- package/dist/ui/footer-status-layout.test.js +56 -0
- package/dist/ui/footer-status-layout.test.js.map +1 -0
- package/dist/ui/goal-events.d.ts +20 -0
- package/dist/ui/goal-events.d.ts.map +1 -0
- package/dist/ui/goal-events.js +102 -0
- package/dist/ui/goal-events.js.map +1 -0
- package/dist/ui/goal-events.test.d.ts +2 -0
- package/dist/ui/goal-events.test.d.ts.map +1 -0
- package/dist/ui/goal-events.test.js +208 -0
- package/dist/ui/goal-events.test.js.map +1 -0
- package/dist/ui/goal-overlay.test.d.ts +2 -0
- package/dist/ui/goal-overlay.test.d.ts.map +1 -0
- package/dist/ui/goal-overlay.test.js +122 -0
- package/dist/ui/goal-overlay.test.js.map +1 -0
- package/dist/ui/goal-status-bar.test.d.ts +2 -0
- package/dist/ui/goal-status-bar.test.d.ts.map +1 -0
- package/dist/ui/goal-status-bar.test.js +143 -0
- package/dist/ui/goal-status-bar.test.js.map +1 -0
- package/dist/ui/live-item-flush.test.js +48 -0
- package/dist/ui/live-item-flush.test.js.map +1 -1
- package/dist/ui/render.d.ts +8 -3
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +10 -3
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/scroll-stabilization.test.d.ts +2 -0
- package/dist/ui/scroll-stabilization.test.d.ts.map +1 -0
- package/dist/ui/scroll-stabilization.test.js +23 -0
- package/dist/ui/scroll-stabilization.test.js.map +1 -0
- package/dist/utils/format.js +44 -0
- package/dist/utils/format.js.map +1 -1
- package/package.json +5 -4
package/dist/ui/App.js
CHANGED
|
@@ -25,15 +25,17 @@ import { StreamingArea } from "./components/StreamingArea.js";
|
|
|
25
25
|
import { ActivityIndicator } from "./components/ActivityIndicator.js";
|
|
26
26
|
import { InputArea } from "./components/InputArea.js";
|
|
27
27
|
import { Footer } from "./components/Footer.js";
|
|
28
|
+
import { GoalStatusBar, reconcileGoalStatusEntriesWithRuns, removeGoalStatusEntry, syncGoalStatusEntries, } from "./components/GoalStatusBar.js";
|
|
28
29
|
import { Banner } from "./components/Banner.js";
|
|
29
30
|
import { PlanOverlay } from "./components/PlanOverlay.js";
|
|
30
31
|
import { ModelSelector } from "./components/ModelSelector.js";
|
|
31
32
|
import { TaskOverlay } from "./components/TaskOverlay.js";
|
|
33
|
+
import { GoalOverlay } from "./components/GoalOverlay.js";
|
|
32
34
|
import { PixelOverlay } from "./components/PixelOverlay.js";
|
|
33
35
|
import { SkillsOverlay } from "./components/SkillsOverlay.js";
|
|
34
36
|
import { EyesOverlay } from "./components/EyesOverlay.js";
|
|
35
37
|
import { ThemeSelector } from "./components/ThemeSelector.js";
|
|
36
|
-
import { BackgroundTasksBar } from "./components/BackgroundTasksBar.js";
|
|
38
|
+
import { BackgroundTasksBar, getFooterStatusLayoutDecision, } from "./components/BackgroundTasksBar.js";
|
|
37
39
|
import { useTheme, useSetTheme } from "./theme/theme.js";
|
|
38
40
|
import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
|
|
39
41
|
import { getGitBranch } from "../utils/git.js";
|
|
@@ -57,6 +59,10 @@ import { getLatestUserText, injectRepoMapContextMessages, stripRepoMapContextMes
|
|
|
57
59
|
import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
|
|
58
60
|
import { getMCPServers } from "../core/mcp/index.js";
|
|
59
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";
|
|
64
|
+
import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
|
|
65
|
+
import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, isGoalSyntheticEvent, parseGoalSyntheticEvent, } from "./goal-events.js";
|
|
60
66
|
/** Where ggcoder bugs should be reported. Surfaced in the guidance line. */
|
|
61
67
|
const GGCODER_BUG_REPORT_URL = "github.com/kenkaiiii/gg-framework/issues";
|
|
62
68
|
/**
|
|
@@ -114,6 +120,77 @@ function compactHistory(items) {
|
|
|
114
120
|
}
|
|
115
121
|
return compacted;
|
|
116
122
|
}
|
|
123
|
+
function summarizeGoalCompletion(summary) {
|
|
124
|
+
const lines = summary
|
|
125
|
+
.split("\n")
|
|
126
|
+
.map((line) => line.trim())
|
|
127
|
+
.filter((line) => line.length > 0 && line !== "[agent_done]");
|
|
128
|
+
const statusLine = lines.find((line) => /^Status:/i.test(line));
|
|
129
|
+
const changedLine = lines.find((line) => /^(Changed|Implemented|Fixed|Added|Key findings|Full verifier)/i.test(line));
|
|
130
|
+
const verificationLine = lines.find((line) => /^(Verification|Verified|Result):/i.test(line));
|
|
131
|
+
return statusLine ?? changedLine ?? verificationLine ?? lines[0];
|
|
132
|
+
}
|
|
133
|
+
function formatGoalWorkerFinishedTitle(taskTitle, status) {
|
|
134
|
+
return status === "done"
|
|
135
|
+
? `Worker finished: ${taskTitle}. Reporting back.`
|
|
136
|
+
: `Worker failed: ${taskTitle}. Reporting back.`;
|
|
137
|
+
}
|
|
138
|
+
export function formatGoalTerminalProgress(run) {
|
|
139
|
+
switch (run.status) {
|
|
140
|
+
case "passed":
|
|
141
|
+
return {
|
|
142
|
+
kind: "goal_progress",
|
|
143
|
+
phase: "terminal",
|
|
144
|
+
title: `Goal passed: ${run.title}`,
|
|
145
|
+
detail: "Verifier evidence is recorded; auto-continuation stopped.",
|
|
146
|
+
status: run.status,
|
|
147
|
+
};
|
|
148
|
+
case "failed":
|
|
149
|
+
return {
|
|
150
|
+
kind: "goal_progress",
|
|
151
|
+
phase: "terminal",
|
|
152
|
+
title: `Goal failed: ${run.title}`,
|
|
153
|
+
detail: "Auto-continuation stopped. Check Goal tasks for the failing step.",
|
|
154
|
+
status: run.status,
|
|
155
|
+
};
|
|
156
|
+
case "blocked":
|
|
157
|
+
return {
|
|
158
|
+
kind: "goal_progress",
|
|
159
|
+
phase: "terminal",
|
|
160
|
+
title: `Goal blocked: ${run.title}`,
|
|
161
|
+
detail: goalHasBlockingPrerequisites(run)
|
|
162
|
+
? formatGoalBlockingPrerequisites(run)
|
|
163
|
+
: (run.blockers[0] ?? "A prerequisite or missing verifier blocked progress."),
|
|
164
|
+
status: run.status,
|
|
165
|
+
};
|
|
166
|
+
case "paused":
|
|
167
|
+
return {
|
|
168
|
+
kind: "goal_progress",
|
|
169
|
+
phase: "terminal",
|
|
170
|
+
title: `Goal paused: ${run.title}`,
|
|
171
|
+
detail: run.blockers[0] ?? "Auto-continuation paused.",
|
|
172
|
+
status: run.status,
|
|
173
|
+
};
|
|
174
|
+
case "draft":
|
|
175
|
+
case "ready":
|
|
176
|
+
case "running":
|
|
177
|
+
case "verifying":
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export function shouldHideHistoryForOverlayView(isOverlayView, isAgentRunning) {
|
|
182
|
+
// Overlay panes should be clean, full-screen views like Tasks/Plans/Skills.
|
|
183
|
+
// When the agent is idle, pane open/close goes through resetUI(), so history
|
|
184
|
+
// remains in sessionStore and is reprinted after the pane closes. While the
|
|
185
|
+
// agent is running we keep Static mounted because resetUI would abort the run.
|
|
186
|
+
return isOverlayView && !isAgentRunning;
|
|
187
|
+
}
|
|
188
|
+
export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, }) {
|
|
189
|
+
return {
|
|
190
|
+
preserveStatic: isUserScrolled && hasNewOutput,
|
|
191
|
+
autoFollow: !isUserScrolled,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
117
194
|
// flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
|
|
118
195
|
/** Check whether an item is still active (running spinner, pending result). */
|
|
119
196
|
function isActiveItem(item) {
|
|
@@ -311,12 +388,16 @@ export function App(props) {
|
|
|
311
388
|
}
|
|
312
389
|
return [{ kind: "banner", id: "banner" }];
|
|
313
390
|
});
|
|
314
|
-
// Items from the current/last turn — rendered in the live area so they stay visible
|
|
315
|
-
|
|
391
|
+
// Items from the current/last turn — rendered in the live area so they stay visible.
|
|
392
|
+
// Seed from sessionStore so Goal progress/completion rows and other live output
|
|
393
|
+
// survive pane/overlay/resize remounts before they are flushed to <Static>.
|
|
394
|
+
const [liveItems, setLiveItems] = useState(() => props.sessionStore?.liveItems ?? []);
|
|
316
395
|
// overlay seeded from sessionStore (lives across remount). Falls back to
|
|
317
396
|
// props.initialOverlay (CLI launched with one), then null.
|
|
318
397
|
const [overlay, setOverlay] = useState(props.sessionStore?.overlay ?? props.initialOverlay ?? null);
|
|
319
398
|
const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
|
|
399
|
+
const [goalCount, setGoalCount] = useState(0);
|
|
400
|
+
const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
|
|
320
401
|
const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
321
402
|
const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
|
|
322
403
|
// Seed from sessionStore so "Run All" chaining survives the resetUI()
|
|
@@ -324,6 +405,10 @@ export function App(props) {
|
|
|
324
405
|
const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
|
|
325
406
|
const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
|
|
326
407
|
const startTaskRef = useRef(() => { });
|
|
408
|
+
const agentRunningRef = useRef(false);
|
|
409
|
+
const runningGoalIdsRef = useRef(new Set());
|
|
410
|
+
const activeVerifierRunIdsRef = useRef(new Set());
|
|
411
|
+
const startGoalRunRef = useRef(() => { });
|
|
327
412
|
const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
|
|
328
413
|
const currentPixelFixRef = useRef(null);
|
|
329
414
|
const startPixelFixRef = useRef(() => { });
|
|
@@ -363,9 +448,12 @@ export function App(props) {
|
|
|
363
448
|
// previous mount, triggering React's duplicate-key warning and causing
|
|
364
449
|
// duplicate/omitted renders.
|
|
365
450
|
const nextIdRef = useRef((() => {
|
|
366
|
-
const
|
|
451
|
+
const items = [
|
|
452
|
+
...(props.sessionStore?.history ?? props.initialHistory ?? []),
|
|
453
|
+
...(props.sessionStore?.liveItems ?? []),
|
|
454
|
+
];
|
|
367
455
|
let max = -1;
|
|
368
|
-
for (const item of
|
|
456
|
+
for (const item of items) {
|
|
369
457
|
const n = Number(item.id);
|
|
370
458
|
if (Number.isFinite(n) && n > max)
|
|
371
459
|
max = n;
|
|
@@ -401,7 +489,27 @@ export function App(props) {
|
|
|
401
489
|
* language-detection path when this cwd has never been audited before.
|
|
402
490
|
*/
|
|
403
491
|
const triggerAutoSetupRef = useRef(async () => { });
|
|
404
|
-
const getId = () =>
|
|
492
|
+
const getId = () => `ui-${nextIdRef.current++}`;
|
|
493
|
+
const appendGoalProgress = useCallback((item) => {
|
|
494
|
+
setLiveItems((prev) => [...prev, { ...item, id: getId() }]);
|
|
495
|
+
}, []);
|
|
496
|
+
const goalNumberForRun = useCallback((runId) => Math.max(1, goalStatusEntries.findIndex((entry) => entry.runId === runId) + 1), [goalStatusEntries]);
|
|
497
|
+
const clearGoalStatusEntry = useCallback((runId) => {
|
|
498
|
+
setGoalStatusEntries((prev) => {
|
|
499
|
+
const next = removeGoalStatusEntry(prev, runId);
|
|
500
|
+
if (props.sessionStore)
|
|
501
|
+
props.sessionStore.goalStatusEntries = next;
|
|
502
|
+
return next;
|
|
503
|
+
});
|
|
504
|
+
}, [props.sessionStore]);
|
|
505
|
+
const upsertGoalStatusEntry = useCallback((entry) => {
|
|
506
|
+
setGoalStatusEntries((prev) => {
|
|
507
|
+
const next = syncGoalStatusEntries(prev, entry);
|
|
508
|
+
if (props.sessionStore)
|
|
509
|
+
props.sessionStore.goalStatusEntries = next;
|
|
510
|
+
return next;
|
|
511
|
+
});
|
|
512
|
+
}, [props.sessionStore]);
|
|
405
513
|
// Two-phase flush: items waiting to be moved to Static history after the
|
|
406
514
|
// live area has been cleared and Ink has committed the smaller output.
|
|
407
515
|
const pendingFlushRef = useRef([]);
|
|
@@ -411,8 +519,12 @@ export function App(props) {
|
|
|
411
519
|
if (items.length === 0)
|
|
412
520
|
return;
|
|
413
521
|
pendingFlushRef.current = [...pendingFlushRef.current, ...items];
|
|
522
|
+
if (props.sessionStore) {
|
|
523
|
+
const queuedIds = new Set(items.map((item) => item.id));
|
|
524
|
+
props.sessionStore.liveItems = (props.sessionStore.liveItems ?? []).filter((item) => !queuedIds.has(item.id));
|
|
525
|
+
}
|
|
414
526
|
setFlushGeneration((g) => g + 1);
|
|
415
|
-
}, []);
|
|
527
|
+
}, [props.sessionStore]);
|
|
416
528
|
// Mirror runtime state choices (model/provider/thinking) into renderApp's
|
|
417
529
|
// closure so unmount/remount preserves them.
|
|
418
530
|
const onRuntimeStateChange = props.onRuntimeStateChange;
|
|
@@ -436,6 +548,10 @@ export function App(props) {
|
|
|
436
548
|
if (sessionStore)
|
|
437
549
|
sessionStore.history = history;
|
|
438
550
|
}, [history, sessionStore]);
|
|
551
|
+
useEffect(() => {
|
|
552
|
+
if (sessionStore)
|
|
553
|
+
sessionStore.liveItems = liveItems;
|
|
554
|
+
}, [liveItems, sessionStore]);
|
|
439
555
|
useEffect(() => {
|
|
440
556
|
if (sessionStore)
|
|
441
557
|
sessionStore.planSteps = planSteps;
|
|
@@ -448,6 +564,10 @@ export function App(props) {
|
|
|
448
564
|
if (sessionStore)
|
|
449
565
|
sessionStore.overlay = overlay;
|
|
450
566
|
}, [overlay, sessionStore]);
|
|
567
|
+
useEffect(() => {
|
|
568
|
+
if (sessionStore)
|
|
569
|
+
sessionStore.goalStatusEntries = goalStatusEntries;
|
|
570
|
+
}, [goalStatusEntries, sessionStore]);
|
|
451
571
|
// pendingAction is consumed via a useEffect AFTER agentLoop is created
|
|
452
572
|
// — see below where useAgentLoop is set up.
|
|
453
573
|
const pendingActionConsumedRef = useRef(false);
|
|
@@ -463,6 +583,36 @@ export function App(props) {
|
|
|
463
583
|
useEffect(() => {
|
|
464
584
|
getGitBranch(displayedCwd).then(setGitBranch);
|
|
465
585
|
}, [displayedCwd]);
|
|
586
|
+
useEffect(() => {
|
|
587
|
+
let cancelled = false;
|
|
588
|
+
const refreshGoalCount = () => {
|
|
589
|
+
void reconcileActiveGoalRuns(props.cwd, {
|
|
590
|
+
isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
|
|
591
|
+
}).then(({ runs }) => {
|
|
592
|
+
const counts = summarizeGoalCountsFromRuns(runs);
|
|
593
|
+
if (cancelled)
|
|
594
|
+
return;
|
|
595
|
+
setGoalCount(counts.active);
|
|
596
|
+
setGoalStatusEntries((prev) => {
|
|
597
|
+
const next = reconcileGoalStatusEntriesWithRuns(prev, runs, {
|
|
598
|
+
isWorkerActive: (workerId, run) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId &&
|
|
599
|
+
worker.goalRunId === run.id &&
|
|
600
|
+
worker.status === "running"),
|
|
601
|
+
isVerifierActive: (run) => activeVerifierRunIdsRef.current.has(run.id),
|
|
602
|
+
});
|
|
603
|
+
if (props.sessionStore)
|
|
604
|
+
props.sessionStore.goalStatusEntries = next;
|
|
605
|
+
return next;
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
};
|
|
609
|
+
refreshGoalCount();
|
|
610
|
+
const interval = setInterval(refreshGoalCount, 1000);
|
|
611
|
+
return () => {
|
|
612
|
+
cancelled = true;
|
|
613
|
+
clearInterval(interval);
|
|
614
|
+
};
|
|
615
|
+
}, [props.cwd]);
|
|
466
616
|
// Periodic update check during long sessions
|
|
467
617
|
useEffect(() => {
|
|
468
618
|
startPeriodicUpdateCheck(props.version, (msg) => {
|
|
@@ -712,7 +862,8 @@ export function App(props) {
|
|
|
712
862
|
});
|
|
713
863
|
}
|
|
714
864
|
}, [props.settingsFile]);
|
|
715
|
-
const
|
|
865
|
+
const compactionAbortRef = useRef(null);
|
|
866
|
+
const compactConversation = useCallback(async (messages, signal) => {
|
|
716
867
|
const contextWindow = getContextWindow(currentModel, contextWindowOptions);
|
|
717
868
|
const tokensBefore = estimateConversationTokens(messages);
|
|
718
869
|
const spinId = getId();
|
|
@@ -723,6 +874,10 @@ export function App(props) {
|
|
|
723
874
|
});
|
|
724
875
|
// Show animated spinner
|
|
725
876
|
setLiveItems((prev) => [...prev, { kind: "compacting", id: spinId }]);
|
|
877
|
+
const ownedAbort = signal ? null : new AbortController();
|
|
878
|
+
const compactionSignal = signal ?? ownedAbort?.signal;
|
|
879
|
+
if (ownedAbort)
|
|
880
|
+
compactionAbortRef.current = ownedAbort;
|
|
726
881
|
try {
|
|
727
882
|
// Resolve fresh credentials for compaction too
|
|
728
883
|
let compactApiKey = activeApiKey;
|
|
@@ -744,7 +899,7 @@ export function App(props) {
|
|
|
744
899
|
projectId: compactProjectId,
|
|
745
900
|
baseUrl: compactBaseUrl,
|
|
746
901
|
contextWindow,
|
|
747
|
-
signal:
|
|
902
|
+
signal: compactionSignal,
|
|
748
903
|
approvedPlanPath: approvedPlanPathRef.current,
|
|
749
904
|
});
|
|
750
905
|
if (result.result.compacted) {
|
|
@@ -769,10 +924,16 @@ export function App(props) {
|
|
|
769
924
|
}
|
|
770
925
|
catch (err) {
|
|
771
926
|
const msg = err instanceof Error ? err.message : String(err);
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
setLiveItems((prev) =>
|
|
775
|
-
|
|
927
|
+
const isAbort = compactionSignal?.aborted || msg.includes("aborted") || msg.includes("abort");
|
|
928
|
+
log(isAbort ? "WARN" : "ERROR", "compaction", isAbort ? "Compaction aborted" : `Compaction failed: ${msg}`);
|
|
929
|
+
setLiveItems((prev) => isAbort
|
|
930
|
+
? prev.filter((item) => item.id !== spinId)
|
|
931
|
+
: prev.map((item) => item.id === spinId ? toErrorItem(err, spinId, "Compaction failed") : item));
|
|
932
|
+
return messages; // Return unchanged on failure/abort
|
|
933
|
+
}
|
|
934
|
+
finally {
|
|
935
|
+
if (ownedAbort && compactionAbortRef.current === ownedAbort)
|
|
936
|
+
compactionAbortRef.current = null;
|
|
776
937
|
}
|
|
777
938
|
}, [
|
|
778
939
|
currentModel,
|
|
@@ -1414,6 +1575,11 @@ export function App(props) {
|
|
|
1414
1575
|
}
|
|
1415
1576
|
}, 500);
|
|
1416
1577
|
}
|
|
1578
|
+
// Goal loop: after the orchestrator handles a worker/verifier event,
|
|
1579
|
+
// continue the same Goal automatically until it reaches a terminal state.
|
|
1580
|
+
for (const runId of [...runningGoalIdsRef.current]) {
|
|
1581
|
+
setTimeout(() => continueGoalRun(runId), 500);
|
|
1582
|
+
}
|
|
1417
1583
|
// Pixel fix: observe branch + commits, patch status, optionally pick
|
|
1418
1584
|
// up the next open error if run-all is active.
|
|
1419
1585
|
const pendingFix = currentPixelFixRef.current;
|
|
@@ -1500,13 +1666,34 @@ export function App(props) {
|
|
|
1500
1666
|
}, []),
|
|
1501
1667
|
onQueuedStart: useCallback((content) => {
|
|
1502
1668
|
// When a queued message starts processing, show it as a UserItem
|
|
1503
|
-
// and flush prior items to history
|
|
1669
|
+
// and flush prior items to history. Synthetic system events are hidden
|
|
1670
|
+
// from the transcript but still routed through the main agent context.
|
|
1504
1671
|
const displayText = typeof content === "string"
|
|
1505
1672
|
? content
|
|
1506
1673
|
: content
|
|
1507
1674
|
.filter((c) => c.type === "text")
|
|
1508
1675
|
.map((c) => c.text)
|
|
1509
1676
|
.join("\n");
|
|
1677
|
+
if (isGoalSyntheticEvent(displayText)) {
|
|
1678
|
+
const eventInfo = parseGoalSyntheticEvent(displayText);
|
|
1679
|
+
setLiveItems((prev) => {
|
|
1680
|
+
if (prev.length > 0)
|
|
1681
|
+
queueFlush(prev);
|
|
1682
|
+
return [];
|
|
1683
|
+
});
|
|
1684
|
+
setDoneStatus(null);
|
|
1685
|
+
appendGoalProgress({
|
|
1686
|
+
kind: "goal_progress",
|
|
1687
|
+
phase: "orchestrator_reviewing",
|
|
1688
|
+
title: "Orchestrator reviewing Goal update",
|
|
1689
|
+
detail: eventInfo?.kind === "worker"
|
|
1690
|
+
? `Worker ${eventInfo.worker ?? "finished"} reported back${eventInfo.task ? ` on ${eventInfo.task}` : ""}. Inspecting Goal state.`
|
|
1691
|
+
: `Verifier reported ${eventInfo?.status ?? "status"}. Inspecting evidence and next action.`,
|
|
1692
|
+
workerId: eventInfo?.worker,
|
|
1693
|
+
status: eventInfo?.status,
|
|
1694
|
+
});
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1510
1697
|
const imageCount = typeof content === "string"
|
|
1511
1698
|
? undefined
|
|
1512
1699
|
: content.filter((c) => c.type === "image").length || undefined;
|
|
@@ -1605,7 +1792,14 @@ export function App(props) {
|
|
|
1605
1792
|
if (pendingFlushRef.current.length > 0) {
|
|
1606
1793
|
const items = pendingFlushRef.current;
|
|
1607
1794
|
pendingFlushRef.current = [];
|
|
1608
|
-
setHistory((h) =>
|
|
1795
|
+
setHistory((h) => {
|
|
1796
|
+
const next = compactHistory([...h, ...trimFlushedItems(items)]);
|
|
1797
|
+
if (sessionStore)
|
|
1798
|
+
sessionStore.history = next;
|
|
1799
|
+
return next;
|
|
1800
|
+
});
|
|
1801
|
+
if (sessionStore)
|
|
1802
|
+
sessionStore.liveItems = liveItems;
|
|
1609
1803
|
}
|
|
1610
1804
|
}, [flushGeneration]);
|
|
1611
1805
|
// Sync terminal title with agent loop state
|
|
@@ -1698,11 +1892,15 @@ export function App(props) {
|
|
|
1698
1892
|
}
|
|
1699
1893
|
// Handle /compact — compact conversation
|
|
1700
1894
|
if (trimmed === "/compact" || trimmed === "/c") {
|
|
1701
|
-
const
|
|
1702
|
-
|
|
1895
|
+
const ac = new AbortController();
|
|
1896
|
+
compactionAbortRef.current = ac;
|
|
1897
|
+
const compacted = await compactConversation(messagesRef.current, ac.signal);
|
|
1898
|
+
if (!ac.signal.aborted && compacted !== messagesRef.current) {
|
|
1703
1899
|
messagesRef.current = compacted;
|
|
1704
1900
|
await persistCompactedSession(compacted);
|
|
1705
1901
|
}
|
|
1902
|
+
if (compactionAbortRef.current === ac)
|
|
1903
|
+
compactionAbortRef.current = null;
|
|
1706
1904
|
return;
|
|
1707
1905
|
}
|
|
1708
1906
|
// Handle /quit — exit the agent
|
|
@@ -1847,6 +2045,27 @@ export function App(props) {
|
|
|
1847
2045
|
]);
|
|
1848
2046
|
return;
|
|
1849
2047
|
}
|
|
2048
|
+
// Handle /goals — open goal pane
|
|
2049
|
+
if (trimmed === "/goals") {
|
|
2050
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2051
|
+
props.sessionStore.overlay = "goal";
|
|
2052
|
+
props.sessionStore.planAutoExpand = false;
|
|
2053
|
+
props.resetUI();
|
|
2054
|
+
}
|
|
2055
|
+
else {
|
|
2056
|
+
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2057
|
+
setStaticKey((key) => key + 1);
|
|
2058
|
+
if (props.sessionStore) {
|
|
2059
|
+
props.sessionStore.overlay = "goal";
|
|
2060
|
+
props.sessionStore.planAutoExpand = false;
|
|
2061
|
+
if (agentLoop.isRunning)
|
|
2062
|
+
props.sessionStore.pendingResetUI = true;
|
|
2063
|
+
}
|
|
2064
|
+
setPlanAutoExpand(false);
|
|
2065
|
+
setOverlay("goal");
|
|
2066
|
+
}
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
1850
2069
|
// Handle /plans — open plan pane
|
|
1851
2070
|
if (trimmed === "/plans") {
|
|
1852
2071
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
@@ -2045,6 +2264,9 @@ export function App(props) {
|
|
|
2045
2264
|
agentLoop.clearQueue();
|
|
2046
2265
|
agentLoop.abort();
|
|
2047
2266
|
}
|
|
2267
|
+
else if (compactionAbortRef.current) {
|
|
2268
|
+
compactionAbortRef.current.abort();
|
|
2269
|
+
}
|
|
2048
2270
|
else {
|
|
2049
2271
|
handleDoubleExit();
|
|
2050
2272
|
}
|
|
@@ -2175,6 +2397,7 @@ export function App(props) {
|
|
|
2175
2397
|
};
|
|
2176
2398
|
const promptOrder = [
|
|
2177
2399
|
// Project audits / one-shot analysis
|
|
2400
|
+
"goal",
|
|
2178
2401
|
"init",
|
|
2179
2402
|
"research",
|
|
2180
2403
|
"scan",
|
|
@@ -2220,11 +2443,29 @@ export function App(props) {
|
|
|
2220
2443
|
case "tombstone":
|
|
2221
2444
|
return null;
|
|
2222
2445
|
case "banner":
|
|
2223
|
-
return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd, taskCount: taskCount }, item.id));
|
|
2446
|
+
return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: displayedCwd, taskCount: taskCount, goalCount: goalCount }, item.id));
|
|
2224
2447
|
case "user":
|
|
2225
2448
|
return (_jsx(UserMessage, { text: item.text, imageCount: item.imageCount, pasteInfo: item.pasteInfo }, item.id));
|
|
2226
2449
|
case "task":
|
|
2227
2450
|
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: "Task: " }), _jsx(Text, { color: theme.success, children: item.title })] }) }, item.id));
|
|
2451
|
+
case "goal":
|
|
2452
|
+
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));
|
|
2453
|
+
case "goal_progress": {
|
|
2454
|
+
const isError = item.status === "failed" || item.status === "fail" || item.status === "blocked";
|
|
2455
|
+
const color = item.phase === "terminal" && !isError
|
|
2456
|
+
? theme.success
|
|
2457
|
+
: isError
|
|
2458
|
+
? theme.warning
|
|
2459
|
+
: theme.primary;
|
|
2460
|
+
const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
|
|
2461
|
+
? "✓ "
|
|
2462
|
+
: item.phase === "terminal"
|
|
2463
|
+
? item.status === "passed"
|
|
2464
|
+
? "◆ "
|
|
2465
|
+
: "! "
|
|
2466
|
+
: "↻ ";
|
|
2467
|
+
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: color, bold: true, children: glyph }), _jsx(Text, { color: color, bold: true, children: item.title }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }), item.detail ? (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` ${item.detail}` })) : null] }, item.id));
|
|
2468
|
+
}
|
|
2228
2469
|
case "style_pack": {
|
|
2229
2470
|
const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
|
|
2230
2471
|
const headerLabel = item.added.length > 1 ? "STYLE PACKS ACTIVE" : "STYLE PACK ACTIVE";
|
|
@@ -2380,13 +2621,602 @@ export function App(props) {
|
|
|
2380
2621
|
currentProvider,
|
|
2381
2622
|
currentModel,
|
|
2382
2623
|
]);
|
|
2624
|
+
const openOverlay = useCallback((kind) => {
|
|
2625
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2626
|
+
props.sessionStore.overlay = kind;
|
|
2627
|
+
if (kind !== "plan")
|
|
2628
|
+
props.sessionStore.planAutoExpand = false;
|
|
2629
|
+
props.resetUI();
|
|
2630
|
+
}
|
|
2631
|
+
else {
|
|
2632
|
+
if (props.sessionStore) {
|
|
2633
|
+
props.sessionStore.overlay = kind;
|
|
2634
|
+
if (kind !== "plan")
|
|
2635
|
+
props.sessionStore.planAutoExpand = false;
|
|
2636
|
+
if (agentLoop.isRunning)
|
|
2637
|
+
props.sessionStore.pendingResetUI = true;
|
|
2638
|
+
}
|
|
2639
|
+
if (kind !== "plan")
|
|
2640
|
+
setPlanAutoExpand(false);
|
|
2641
|
+
setOverlay(kind);
|
|
2642
|
+
}
|
|
2643
|
+
}, [agentLoop.isRunning, props, stdout]);
|
|
2644
|
+
const closeOverlay = useCallback(() => {
|
|
2645
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2646
|
+
props.sessionStore.overlay = null;
|
|
2647
|
+
props.resetUI();
|
|
2648
|
+
}
|
|
2649
|
+
else {
|
|
2650
|
+
if (props.sessionStore) {
|
|
2651
|
+
props.sessionStore.overlay = null;
|
|
2652
|
+
if (agentLoop.isRunning)
|
|
2653
|
+
props.sessionStore.pendingResetUI = true;
|
|
2654
|
+
}
|
|
2655
|
+
setOverlay(null);
|
|
2656
|
+
}
|
|
2657
|
+
}, [agentLoop.isRunning, overlay, props, stdout]);
|
|
2658
|
+
const runGoalSyntheticEvent = useCallback((eventText) => {
|
|
2659
|
+
const eventInfo = parseGoalSyntheticEvent(eventText);
|
|
2660
|
+
const detail = eventInfo?.kind === "worker"
|
|
2661
|
+
? `Inspecting worker result${eventInfo.task ? ` for ${eventInfo.task}` : ""}.`
|
|
2662
|
+
: `Inspecting verifier result${eventInfo?.status ? ` (${eventInfo.status})` : ""}.`;
|
|
2663
|
+
if (agentRunningRef.current) {
|
|
2664
|
+
appendGoalProgress({
|
|
2665
|
+
kind: "goal_progress",
|
|
2666
|
+
phase: "orchestrator_reviewing",
|
|
2667
|
+
title: "Goal update queued for orchestrator",
|
|
2668
|
+
detail: `${detail} It will report back after the current turn.`,
|
|
2669
|
+
workerId: eventInfo?.worker,
|
|
2670
|
+
status: eventInfo?.status,
|
|
2671
|
+
});
|
|
2672
|
+
agentLoop.queueMessage(eventText);
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
appendGoalProgress({
|
|
2676
|
+
kind: "goal_progress",
|
|
2677
|
+
phase: "orchestrator_reviewing",
|
|
2678
|
+
title: "Orchestrator reviewing Goal update",
|
|
2679
|
+
detail,
|
|
2680
|
+
workerId: eventInfo?.worker,
|
|
2681
|
+
status: eventInfo?.status,
|
|
2682
|
+
});
|
|
2683
|
+
setLastUserMessage("");
|
|
2684
|
+
setDoneStatus(null);
|
|
2685
|
+
void agentLoop.run(eventText).catch((err) => {
|
|
2686
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2687
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2688
|
+
});
|
|
2689
|
+
}, [agentLoop, appendGoalProgress]);
|
|
2690
|
+
const continueGoalRun = useCallback((runId) => {
|
|
2691
|
+
void (async () => {
|
|
2692
|
+
const latestRun = await reconcileActiveGoalRuns(props.cwd, {
|
|
2693
|
+
isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
|
|
2694
|
+
}).then(({ runs }) => runs.find((item) => item.id === runId) ?? null);
|
|
2695
|
+
if (!latestRun) {
|
|
2696
|
+
runningGoalIdsRef.current.delete(runId);
|
|
2697
|
+
clearGoalStatusEntry(runId);
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2700
|
+
const decision = decideGoalNextAction(latestRun);
|
|
2701
|
+
if (decision.kind === "wait")
|
|
2702
|
+
return;
|
|
2703
|
+
if (decision.kind === "terminal" ||
|
|
2704
|
+
decision.kind === "blocked" ||
|
|
2705
|
+
decision.kind === "pause") {
|
|
2706
|
+
const status = decision.kind === "terminal"
|
|
2707
|
+
? decision.status
|
|
2708
|
+
: decision.kind === "blocked"
|
|
2709
|
+
? "blocked"
|
|
2710
|
+
: "paused";
|
|
2711
|
+
const nextRun = {
|
|
2712
|
+
...latestRun,
|
|
2713
|
+
status,
|
|
2714
|
+
continueRequestedAt: undefined,
|
|
2715
|
+
blockers: decision.kind === "blocked" || decision.kind === "pause"
|
|
2716
|
+
? Array.from(new Set([...latestRun.blockers, decision.reason]))
|
|
2717
|
+
: latestRun.blockers,
|
|
2718
|
+
};
|
|
2719
|
+
await upsertGoalRun(props.cwd, nextRun);
|
|
2720
|
+
await appendGoalDecision(props.cwd, latestRun.id, {
|
|
2721
|
+
kind: "continuation_stopped",
|
|
2722
|
+
reason: decision.reason,
|
|
2723
|
+
content: `terminal=${status}`,
|
|
2724
|
+
});
|
|
2725
|
+
const terminalProgress = formatGoalTerminalProgress(nextRun);
|
|
2726
|
+
if (terminalProgress)
|
|
2727
|
+
appendGoalProgress(terminalProgress);
|
|
2728
|
+
runningGoalIdsRef.current.delete(runId);
|
|
2729
|
+
clearGoalStatusEntry(runId);
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
let runForNextAction = latestRun;
|
|
2733
|
+
if (latestRun.continueRequestedAt &&
|
|
2734
|
+
!listGoalWorkers(props.cwd).some((worker) => worker.status === "running") &&
|
|
2735
|
+
activeVerifierRunIdsRef.current.size === 0) {
|
|
2736
|
+
await appendGoalDecision(props.cwd, latestRun.id, {
|
|
2737
|
+
kind: "continuation_consumed",
|
|
2738
|
+
reason: `Continuation request consumed by ${decision.kind}.`,
|
|
2739
|
+
});
|
|
2740
|
+
runForNextAction = await upsertGoalRun(props.cwd, {
|
|
2741
|
+
...latestRun,
|
|
2742
|
+
continueRequestedAt: undefined,
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
appendGoalProgress({
|
|
2746
|
+
kind: "goal_progress",
|
|
2747
|
+
phase: "continuing",
|
|
2748
|
+
title: `Continuing Goal: ${latestRun.title}`,
|
|
2749
|
+
detail: "Starting the next worker task or verifier step automatically.",
|
|
2750
|
+
status: latestRun.status,
|
|
2751
|
+
});
|
|
2752
|
+
upsertGoalStatusEntry({
|
|
2753
|
+
runId: latestRun.id,
|
|
2754
|
+
label: latestRun.title,
|
|
2755
|
+
phase: "orchestrating",
|
|
2756
|
+
startedAt: Date.now(),
|
|
2757
|
+
detail: "choosing next step",
|
|
2758
|
+
});
|
|
2759
|
+
startGoalRunRef.current(runForNextAction);
|
|
2760
|
+
})().catch((err) => {
|
|
2761
|
+
runningGoalIdsRef.current.delete(runId);
|
|
2762
|
+
clearGoalStatusEntry(runId);
|
|
2763
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2764
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2765
|
+
});
|
|
2766
|
+
}, [appendGoalProgress, clearGoalStatusEntry, props.cwd, upsertGoalStatusEntry]);
|
|
2767
|
+
const handleGoalWorkerComplete = useCallback((run, completion) => {
|
|
2768
|
+
const taskTitle = run.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
|
|
2769
|
+
completion.worker.goalTaskId;
|
|
2770
|
+
const eventText = formatGoalWorkerCompletionEvent(run, taskTitle, completion);
|
|
2771
|
+
void summarizeGoalCounts(completion.worker.cwd).then((counts) => setGoalCount(counts.active));
|
|
2772
|
+
appendGoalProgress({
|
|
2773
|
+
kind: "goal_progress",
|
|
2774
|
+
phase: "worker_finished",
|
|
2775
|
+
title: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
|
|
2776
|
+
detail: summarizeGoalCompletion(completion.summary),
|
|
2777
|
+
workerId: completion.worker.id,
|
|
2778
|
+
status: completion.status,
|
|
2779
|
+
});
|
|
2780
|
+
upsertGoalStatusEntry({
|
|
2781
|
+
runId: run.id,
|
|
2782
|
+
label: taskTitle,
|
|
2783
|
+
phase: completion.status === "done" ? "reviewing" : "failed",
|
|
2784
|
+
startedAt: Date.now(),
|
|
2785
|
+
detail: completion.status === "done" ? "reviewing result" : "task failed",
|
|
2786
|
+
workerId: completion.worker.id,
|
|
2787
|
+
goalNumber: goalNumberForRun(run.id),
|
|
2788
|
+
});
|
|
2789
|
+
runGoalSyntheticEvent(eventText);
|
|
2790
|
+
void (async () => {
|
|
2791
|
+
if (listGoalWorkers(completion.worker.cwd).some((worker) => worker.status === "running"))
|
|
2792
|
+
return;
|
|
2793
|
+
if (activeVerifierRunIdsRef.current.size > 0)
|
|
2794
|
+
return;
|
|
2795
|
+
const runs = await loadGoalRuns(completion.worker.cwd);
|
|
2796
|
+
const queued = runs.find((item) => item.continueRequestedAt && !goalHasBlockingPrerequisites(item));
|
|
2797
|
+
if (queued)
|
|
2798
|
+
setTimeout(() => continueGoalRun(queued.id), 750);
|
|
2799
|
+
})().catch((err) => log("ERROR", "goal", err instanceof Error ? err.message : String(err)));
|
|
2800
|
+
}, [
|
|
2801
|
+
appendGoalProgress,
|
|
2802
|
+
continueGoalRun,
|
|
2803
|
+
goalNumberForRun,
|
|
2804
|
+
runGoalSyntheticEvent,
|
|
2805
|
+
upsertGoalStatusEntry,
|
|
2806
|
+
]);
|
|
2807
|
+
useEffect(() => {
|
|
2808
|
+
return subscribeGoalWorkerCompletions((completion) => {
|
|
2809
|
+
void (async () => {
|
|
2810
|
+
const latestRun = (await loadGoalRuns(completion.worker.cwd)).find((item) => item.id === completion.worker.goalRunId) ?? null;
|
|
2811
|
+
if (!latestRun) {
|
|
2812
|
+
log("WARN", "goal", `Worker completion for unknown Goal ${completion.worker.goalRunId}`);
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
runningGoalIdsRef.current.add(latestRun.id);
|
|
2816
|
+
handleGoalWorkerComplete(latestRun, completion);
|
|
2817
|
+
})().catch((err) => {
|
|
2818
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2819
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2820
|
+
});
|
|
2821
|
+
}, props.cwd);
|
|
2822
|
+
}, [handleGoalWorkerComplete, props.cwd]);
|
|
2823
|
+
const startGoalRun = useCallback((run) => {
|
|
2824
|
+
runningGoalIdsRef.current.add(run.id);
|
|
2825
|
+
void (async () => {
|
|
2826
|
+
if (goalHasBlockingPrerequisites(run)) {
|
|
2827
|
+
setOverlay(null);
|
|
2828
|
+
const detail = formatGoalBlockingPrerequisites(run);
|
|
2829
|
+
await upsertGoalRun(props.cwd, {
|
|
2830
|
+
...run,
|
|
2831
|
+
status: "blocked",
|
|
2832
|
+
blockers: Array.from(new Set([...run.blockers, detail])),
|
|
2833
|
+
});
|
|
2834
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
2835
|
+
appendGoalProgress({
|
|
2836
|
+
kind: "goal_progress",
|
|
2837
|
+
phase: "terminal",
|
|
2838
|
+
title: `Goal blocked: ${run.title}`,
|
|
2839
|
+
detail,
|
|
2840
|
+
status: "blocked",
|
|
2841
|
+
});
|
|
2842
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
2843
|
+
clearGoalStatusEntry(run.id);
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
const decision = decideGoalNextAction(run);
|
|
2847
|
+
await appendGoalDecision(props.cwd, run.id, decision);
|
|
2848
|
+
if (decision.kind === "terminal") {
|
|
2849
|
+
const terminalProgress = formatGoalTerminalProgress(run);
|
|
2850
|
+
if (terminalProgress)
|
|
2851
|
+
appendGoalProgress(terminalProgress);
|
|
2852
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
2853
|
+
clearGoalStatusEntry(run.id);
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
if (decision.kind === "wait") {
|
|
2857
|
+
appendGoalProgress({
|
|
2858
|
+
kind: "goal_progress",
|
|
2859
|
+
phase: "worker_started",
|
|
2860
|
+
title: decision.workerId ? `Goal working: ${run.title}` : `Goal active: ${run.title}`,
|
|
2861
|
+
detail: decision.reason,
|
|
2862
|
+
workerId: decision.workerId,
|
|
2863
|
+
});
|
|
2864
|
+
upsertGoalStatusEntry({
|
|
2865
|
+
runId: run.id,
|
|
2866
|
+
label: run.title,
|
|
2867
|
+
phase: decision.workerId ? "worker" : "orchestrating",
|
|
2868
|
+
startedAt: Date.now(),
|
|
2869
|
+
detail: decision.reason,
|
|
2870
|
+
workerId: decision.workerId,
|
|
2871
|
+
goalNumber: goalNumberForRun(run.id),
|
|
2872
|
+
});
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
if (decision.kind === "complete") {
|
|
2876
|
+
await upsertGoalRun(props.cwd, { ...run, status: "passed" });
|
|
2877
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
2878
|
+
appendGoalProgress({
|
|
2879
|
+
kind: "goal_progress",
|
|
2880
|
+
phase: "terminal",
|
|
2881
|
+
title: `Goal passed: ${run.title}`,
|
|
2882
|
+
detail: decision.reason,
|
|
2883
|
+
status: "passed",
|
|
2884
|
+
});
|
|
2885
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
2886
|
+
clearGoalStatusEntry(run.id);
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
if (decision.kind === "run_verifier") {
|
|
2890
|
+
await verifyGoalRun(run);
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
if (decision.kind === "create_task") {
|
|
2894
|
+
await updateGoalTask(props.cwd, run.id, `auto-${Date.now()}`, {
|
|
2895
|
+
title: decision.title,
|
|
2896
|
+
prompt: decision.prompt,
|
|
2897
|
+
status: "pending",
|
|
2898
|
+
});
|
|
2899
|
+
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
2900
|
+
await upsertGoalRun(props.cwd, { ...latestRun, status: "ready" });
|
|
2901
|
+
setTimeout(() => continueGoalRun(run.id), 250);
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
if (decision.kind === "blocked") {
|
|
2905
|
+
await upsertGoalRun(props.cwd, {
|
|
2906
|
+
...run,
|
|
2907
|
+
status: "blocked",
|
|
2908
|
+
blockers: [...run.blockers, decision.reason],
|
|
2909
|
+
});
|
|
2910
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
2911
|
+
appendGoalProgress({
|
|
2912
|
+
kind: "goal_progress",
|
|
2913
|
+
phase: "terminal",
|
|
2914
|
+
title: `Goal blocked: ${run.title}`,
|
|
2915
|
+
detail: decision.reason,
|
|
2916
|
+
status: "blocked",
|
|
2917
|
+
});
|
|
2918
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
2919
|
+
clearGoalStatusEntry(run.id);
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2922
|
+
if (decision.kind === "pause") {
|
|
2923
|
+
await updateGoalTask(props.cwd, run.id, decision.task.id, {
|
|
2924
|
+
status: "blocked",
|
|
2925
|
+
attempts: decision.attempts,
|
|
2926
|
+
lastSummary: "Paused after worker attempt limit.",
|
|
2927
|
+
});
|
|
2928
|
+
await upsertGoalRun(props.cwd, {
|
|
2929
|
+
...run,
|
|
2930
|
+
status: "paused",
|
|
2931
|
+
blockers: [...run.blockers, decision.reason],
|
|
2932
|
+
});
|
|
2933
|
+
await appendGoalEvidence(props.cwd, run.id, {
|
|
2934
|
+
kind: "summary",
|
|
2935
|
+
label: "Goal paused",
|
|
2936
|
+
content: decision.reason,
|
|
2937
|
+
});
|
|
2938
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
2939
|
+
appendGoalProgress({
|
|
2940
|
+
kind: "goal_progress",
|
|
2941
|
+
phase: "terminal",
|
|
2942
|
+
title: `Goal paused: ${run.title}`,
|
|
2943
|
+
detail: decision.reason,
|
|
2944
|
+
status: "paused",
|
|
2945
|
+
});
|
|
2946
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
2947
|
+
clearGoalStatusEntry(run.id);
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
await updateGoalTask(props.cwd, run.id, decision.task.id, { attempts: decision.attempts });
|
|
2951
|
+
const worker = await startGoalWorker({
|
|
2952
|
+
cwd: props.cwd,
|
|
2953
|
+
provider: currentProvider,
|
|
2954
|
+
model: currentModel,
|
|
2955
|
+
goalRunId: run.id,
|
|
2956
|
+
goalTaskId: decision.task.id,
|
|
2957
|
+
prompt: decision.task.prompt,
|
|
2958
|
+
});
|
|
2959
|
+
await upsertGoalRun(props.cwd, {
|
|
2960
|
+
...run,
|
|
2961
|
+
status: "running",
|
|
2962
|
+
activeWorkerId: worker.id,
|
|
2963
|
+
continueRequestedAt: undefined,
|
|
2964
|
+
});
|
|
2965
|
+
setOverlay(null);
|
|
2966
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
2967
|
+
appendGoalProgress({
|
|
2968
|
+
kind: "goal_progress",
|
|
2969
|
+
phase: "worker_started",
|
|
2970
|
+
title: `Worker started: ${decision.task.title}`,
|
|
2971
|
+
detail: "Task is running in the background.",
|
|
2972
|
+
workerId: worker.id,
|
|
2973
|
+
status: worker.status,
|
|
2974
|
+
});
|
|
2975
|
+
upsertGoalStatusEntry({
|
|
2976
|
+
runId: run.id,
|
|
2977
|
+
label: decision.task.title,
|
|
2978
|
+
phase: "worker",
|
|
2979
|
+
startedAt: Date.now(),
|
|
2980
|
+
detail: "background worker running",
|
|
2981
|
+
workerId: worker.id,
|
|
2982
|
+
goalNumber: goalNumberForRun(run.id),
|
|
2983
|
+
});
|
|
2984
|
+
})().catch((err) => {
|
|
2985
|
+
clearGoalStatusEntry(run.id);
|
|
2986
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2987
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2988
|
+
});
|
|
2989
|
+
}, [
|
|
2990
|
+
props.cwd,
|
|
2991
|
+
currentProvider,
|
|
2992
|
+
currentModel,
|
|
2993
|
+
appendGoalProgress,
|
|
2994
|
+
clearGoalStatusEntry,
|
|
2995
|
+
goalNumberForRun,
|
|
2996
|
+
upsertGoalStatusEntry,
|
|
2997
|
+
]);
|
|
2998
|
+
const verifyGoalRun = useCallback(async (run) => {
|
|
2999
|
+
if (!run.verifier?.command) {
|
|
3000
|
+
await appendGoalEvidence(props.cwd, run.id, {
|
|
3001
|
+
kind: "summary",
|
|
3002
|
+
label: "Missing verifier",
|
|
3003
|
+
content: "No verifier command is configured.",
|
|
3004
|
+
});
|
|
3005
|
+
await upsertGoalRun(props.cwd, {
|
|
3006
|
+
...run,
|
|
3007
|
+
status: "blocked",
|
|
3008
|
+
blockers: [...run.blockers, "No verifier command configured."],
|
|
3009
|
+
});
|
|
3010
|
+
appendGoalProgress({
|
|
3011
|
+
kind: "goal_progress",
|
|
3012
|
+
phase: "terminal",
|
|
3013
|
+
title: `Goal blocked: ${run.title}`,
|
|
3014
|
+
detail: "No verifier command is configured.",
|
|
3015
|
+
status: "blocked",
|
|
3016
|
+
});
|
|
3017
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
3018
|
+
clearGoalStatusEntry(run.id);
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
3021
|
+
activeVerifierRunIdsRef.current.add(run.id);
|
|
3022
|
+
await upsertGoalRun(props.cwd, {
|
|
3023
|
+
...run,
|
|
3024
|
+
status: "verifying",
|
|
3025
|
+
continueRequestedAt: undefined,
|
|
3026
|
+
});
|
|
3027
|
+
appendGoalProgress({
|
|
3028
|
+
kind: "goal_progress",
|
|
3029
|
+
phase: "verifier_started",
|
|
3030
|
+
title: `Verifier started: ${run.title}`,
|
|
3031
|
+
detail: run.verifier.command,
|
|
3032
|
+
status: "verifying",
|
|
3033
|
+
});
|
|
3034
|
+
const startedAt = Date.now();
|
|
3035
|
+
const verifierTimeoutMs = Number(process.env.GG_GOAL_VERIFIER_TIMEOUT_MS ?? 10 * 60 * 1000);
|
|
3036
|
+
upsertGoalStatusEntry({
|
|
3037
|
+
runId: run.id,
|
|
3038
|
+
label: run.title,
|
|
3039
|
+
phase: "verifier",
|
|
3040
|
+
startedAt,
|
|
3041
|
+
detail: run.verifier.command,
|
|
3042
|
+
goalNumber: goalNumberForRun(run.id),
|
|
3043
|
+
});
|
|
3044
|
+
const { spawn } = await import("node:child_process");
|
|
3045
|
+
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
3046
|
+
const { join } = await import("node:path");
|
|
3047
|
+
const logDir = join(projectDir(props.cwd), "verifiers");
|
|
3048
|
+
await mkdir(logDir, { recursive: true });
|
|
3049
|
+
const outputPath = join(logDir, `${run.id}-${startedAt}.log`);
|
|
3050
|
+
const child = spawn(run.verifier.command, {
|
|
3051
|
+
cwd: props.cwd,
|
|
3052
|
+
shell: true,
|
|
3053
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3054
|
+
env: { ...process.env },
|
|
3055
|
+
});
|
|
3056
|
+
let output = "";
|
|
3057
|
+
child.stdout?.on("data", (chunk) => {
|
|
3058
|
+
output += chunk.toString("utf-8");
|
|
3059
|
+
if (output.length > 20_000)
|
|
3060
|
+
output = output.slice(output.length - 20_000);
|
|
3061
|
+
});
|
|
3062
|
+
child.stderr?.on("data", (chunk) => {
|
|
3063
|
+
output += chunk.toString("utf-8");
|
|
3064
|
+
if (output.length > 20_000)
|
|
3065
|
+
output = output.slice(output.length - 20_000);
|
|
3066
|
+
});
|
|
3067
|
+
let verifierSettled = false;
|
|
3068
|
+
let timedOut = false;
|
|
3069
|
+
const timeout = verifierTimeoutMs > 0
|
|
3070
|
+
? setTimeout(() => {
|
|
3071
|
+
timedOut = true;
|
|
3072
|
+
if (child.pid)
|
|
3073
|
+
child.kill("SIGTERM");
|
|
3074
|
+
const killTimer = setTimeout(() => {
|
|
3075
|
+
if (!verifierSettled && child.pid)
|
|
3076
|
+
child.kill("SIGKILL");
|
|
3077
|
+
}, 5000);
|
|
3078
|
+
killTimer.unref?.();
|
|
3079
|
+
finishVerifier(124, `Verifier timed out after ${verifierTimeoutMs}ms and was terminated.\n${output}`);
|
|
3080
|
+
}, verifierTimeoutMs)
|
|
3081
|
+
: undefined;
|
|
3082
|
+
timeout?.unref?.();
|
|
3083
|
+
const finishVerifier = (code, forcedOutput) => {
|
|
3084
|
+
if (verifierSettled)
|
|
3085
|
+
return;
|
|
3086
|
+
verifierSettled = true;
|
|
3087
|
+
if (timeout)
|
|
3088
|
+
clearTimeout(timeout);
|
|
3089
|
+
activeVerifierRunIdsRef.current.delete(run.id);
|
|
3090
|
+
void (async () => {
|
|
3091
|
+
const status = code === 0 ? "pass" : "fail";
|
|
3092
|
+
const failureClass = timedOut
|
|
3093
|
+
? "verifier_timeout"
|
|
3094
|
+
: forcedOutput?.startsWith("Verifier process error:")
|
|
3095
|
+
? "verifier_spawn_error"
|
|
3096
|
+
: status === "fail"
|
|
3097
|
+
? "verifier_failure"
|
|
3098
|
+
: "verifier_pass";
|
|
3099
|
+
const summary = (forcedOutput ?? output).trim() ||
|
|
3100
|
+
(code === 0 ? "Verifier passed." : "Verifier failed.");
|
|
3101
|
+
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
3102
|
+
await writeFile(outputPath, summary + "\n", "utf-8");
|
|
3103
|
+
const runWithVerifier = {
|
|
3104
|
+
...latestRun,
|
|
3105
|
+
verifier: {
|
|
3106
|
+
...latestRun.verifier,
|
|
3107
|
+
description: latestRun.verifier?.description ?? "Goal verifier",
|
|
3108
|
+
command: run.verifier?.command,
|
|
3109
|
+
lastResult: {
|
|
3110
|
+
status,
|
|
3111
|
+
summary,
|
|
3112
|
+
command: run.verifier?.command,
|
|
3113
|
+
exitCode: code ?? 1,
|
|
3114
|
+
outputPath,
|
|
3115
|
+
checkedAt: new Date().toISOString(),
|
|
3116
|
+
},
|
|
3117
|
+
},
|
|
3118
|
+
};
|
|
3119
|
+
const completionCheck = canCompleteGoalRun(runWithVerifier);
|
|
3120
|
+
const verifiedRun = await upsertGoalRun(props.cwd, {
|
|
3121
|
+
...runWithVerifier,
|
|
3122
|
+
continueRequestedAt: undefined,
|
|
3123
|
+
status: status === "pass" && completionCheck.ok
|
|
3124
|
+
? "passed"
|
|
3125
|
+
: status === "pass"
|
|
3126
|
+
? "ready"
|
|
3127
|
+
: "failed",
|
|
3128
|
+
});
|
|
3129
|
+
await appendGoalEvidence(props.cwd, run.id, {
|
|
3130
|
+
kind: "command",
|
|
3131
|
+
label: `Verifier ${status}`,
|
|
3132
|
+
content: `${failureClass}: ${summary}`.slice(0, 4000),
|
|
3133
|
+
path: outputPath,
|
|
3134
|
+
});
|
|
3135
|
+
await appendGoalDecision(props.cwd, run.id, {
|
|
3136
|
+
kind: `verifier_${status}`,
|
|
3137
|
+
reason: `${failureClass}: verifier exited with code ${code ?? 1}.`,
|
|
3138
|
+
content: `outputPath=${outputPath}; durationMs=${Date.now() - startedAt}`,
|
|
3139
|
+
});
|
|
3140
|
+
if (status === "fail" && shouldCreateVerifierFixTask(latestRun)) {
|
|
3141
|
+
await updateGoalTask(props.cwd, run.id, `fix-${Date.now()}`, {
|
|
3142
|
+
title: "Fix verifier failure",
|
|
3143
|
+
prompt: `Goal verifier failed after ${Date.now() - startedAt}ms. Original goal: ${run.goal}\n\n` +
|
|
3144
|
+
`Verifier command: ${run.verifier?.command}\n\n` +
|
|
3145
|
+
`Failure output:\n${summary.slice(-6000)}\n\nFix the cause, record evidence with the goals tool, and rerun relevant verification.`,
|
|
3146
|
+
status: "pending",
|
|
3147
|
+
});
|
|
3148
|
+
const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
|
|
3149
|
+
await upsertGoalRun(props.cwd, { ...runWithPendingFix, status: "ready" });
|
|
3150
|
+
}
|
|
3151
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3152
|
+
appendGoalProgress({
|
|
3153
|
+
kind: "goal_progress",
|
|
3154
|
+
phase: "verifier_finished",
|
|
3155
|
+
title: `Verifier ${status}: ${run.title}`,
|
|
3156
|
+
detail: summarizeGoalCompletion(summary),
|
|
3157
|
+
status,
|
|
3158
|
+
});
|
|
3159
|
+
upsertGoalStatusEntry({
|
|
3160
|
+
runId: run.id,
|
|
3161
|
+
label: run.title,
|
|
3162
|
+
phase: status === "pass" ? "reviewing" : "failed",
|
|
3163
|
+
startedAt: Date.now(),
|
|
3164
|
+
detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
|
|
3165
|
+
goalNumber: goalNumberForRun(run.id),
|
|
3166
|
+
});
|
|
3167
|
+
const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status, run.verifier?.command ?? "", code ?? 1, summary);
|
|
3168
|
+
runGoalSyntheticEvent(eventText);
|
|
3169
|
+
const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
|
|
3170
|
+
if (continuationRun?.continueRequestedAt && status === "pass") {
|
|
3171
|
+
setTimeout(() => continueGoalRun(run.id), 500);
|
|
3172
|
+
}
|
|
3173
|
+
})().catch((err) => {
|
|
3174
|
+
clearGoalStatusEntry(run.id);
|
|
3175
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3176
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
|
|
3177
|
+
});
|
|
3178
|
+
};
|
|
3179
|
+
child.on("close", (code) => finishVerifier(code));
|
|
3180
|
+
child.on("error", (err) => finishVerifier(1, `Verifier process error: ${err.message}`));
|
|
3181
|
+
}, [
|
|
3182
|
+
props.cwd,
|
|
3183
|
+
appendGoalProgress,
|
|
3184
|
+
clearGoalStatusEntry,
|
|
3185
|
+
goalNumberForRun,
|
|
3186
|
+
runGoalSyntheticEvent,
|
|
3187
|
+
upsertGoalStatusEntry,
|
|
3188
|
+
]);
|
|
3189
|
+
const pauseGoalRun = useCallback((run) => {
|
|
3190
|
+
void (async () => {
|
|
3191
|
+
runningGoalIdsRef.current.delete(run.id);
|
|
3192
|
+
if (run.activeWorkerId)
|
|
3193
|
+
await stopGoalWorker(run.activeWorkerId);
|
|
3194
|
+
await upsertGoalRun(props.cwd, { ...run, status: "paused", activeWorkerId: undefined });
|
|
3195
|
+
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3196
|
+
appendGoalProgress({
|
|
3197
|
+
kind: "goal_progress",
|
|
3198
|
+
phase: "terminal",
|
|
3199
|
+
title: `Goal paused: ${run.title}`,
|
|
3200
|
+
detail: "Auto-continuation stopped until resumed.",
|
|
3201
|
+
status: "paused",
|
|
3202
|
+
});
|
|
3203
|
+
clearGoalStatusEntry(run.id);
|
|
3204
|
+
})().catch((err) => {
|
|
3205
|
+
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3206
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
3207
|
+
});
|
|
3208
|
+
}, [appendGoalProgress, clearGoalStatusEntry, props.cwd]);
|
|
2383
3209
|
// Keep refs in sync for access from stale closures (onDone)
|
|
2384
3210
|
startTaskRef.current = startTask;
|
|
3211
|
+
startGoalRunRef.current = startGoalRun;
|
|
2385
3212
|
useEffect(() => {
|
|
2386
3213
|
runAllTasksRef.current = runAllTasks;
|
|
2387
3214
|
if (props.sessionStore)
|
|
2388
3215
|
props.sessionStore.runAllTasks = runAllTasks;
|
|
2389
3216
|
}, [runAllTasks, props.sessionStore]);
|
|
3217
|
+
useEffect(() => {
|
|
3218
|
+
agentRunningRef.current = agentLoop.isRunning;
|
|
3219
|
+
}, [agentLoop.isRunning]);
|
|
2390
3220
|
const startPixelFix = useCallback((errorId) => {
|
|
2391
3221
|
void (async () => {
|
|
2392
3222
|
try {
|
|
@@ -2487,12 +3317,20 @@ export function App(props) {
|
|
|
2487
3317
|
props.sessionStore.runAllPixel = runAllPixel;
|
|
2488
3318
|
}, [runAllPixel, props.sessionStore]);
|
|
2489
3319
|
const isTaskView = overlay === "tasks";
|
|
3320
|
+
const isGoalView = overlay === "goal";
|
|
2490
3321
|
const isSkillsView = overlay === "skills";
|
|
2491
3322
|
const isPlanView = overlay === "plan";
|
|
2492
3323
|
const isEyesView = overlay === "eyes";
|
|
3324
|
+
const footerStatusLayout = getFooterStatusLayoutDecision({
|
|
3325
|
+
columns,
|
|
3326
|
+
backgroundTaskCount: bgTasks.length,
|
|
3327
|
+
eyesCount,
|
|
3328
|
+
updatePending,
|
|
3329
|
+
});
|
|
2493
3330
|
const isPixelView = overlay === "pixel";
|
|
2494
|
-
const isOverlayView = isTaskView || isSkillsView || isPlanView || isEyesView || isPixelView;
|
|
2495
|
-
|
|
3331
|
+
const isOverlayView = isTaskView || isGoalView || isSkillsView || isPlanView || isEyesView || isPixelView;
|
|
3332
|
+
const shouldHideHistoryForOverlay = shouldHideHistoryForOverlayView(isOverlayView, agentLoop.isRunning);
|
|
3333
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: shouldHideHistoryForOverlay ? [] : history, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, `${resizeKey}-${staticKey}`), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
2496
3334
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2497
3335
|
props.sessionStore.overlay = null;
|
|
2498
3336
|
props.resetUI();
|
|
@@ -2517,6 +3355,16 @@ export function App(props) {
|
|
|
2517
3355
|
markTaskInProgress(props.cwd, next.id);
|
|
2518
3356
|
startTask(next.title, next.prompt, next.id);
|
|
2519
3357
|
}
|
|
3358
|
+
} })) : isGoalView ? (_jsx(GoalOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
3359
|
+
void summarizeGoalCounts(props.cwd).then((counts) => setGoalCount(counts.active));
|
|
3360
|
+
closeOverlay();
|
|
3361
|
+
}, onRunGoal: (run) => {
|
|
3362
|
+
setOverlay(null);
|
|
3363
|
+
startGoalRun(run);
|
|
3364
|
+
}, onVerifyGoal: (run) => {
|
|
3365
|
+
void verifyGoalRun(run);
|
|
3366
|
+
}, onPauseGoal: (run) => {
|
|
3367
|
+
pauseGoalRun(run);
|
|
2520
3368
|
} })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
2521
3369
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2522
3370
|
props.sessionStore.overlay = null;
|
|
@@ -2698,44 +3546,13 @@ export function App(props) {
|
|
|
2698
3546
|
// live-area transition (chat input → TaskOverlay) natively, and
|
|
2699
3547
|
// the chat history above stays in scrollback. When the overlay
|
|
2700
3548
|
// closes, the history is still there (banner included).
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
}
|
|
2705
|
-
else {
|
|
2706
|
-
if (props.sessionStore) {
|
|
2707
|
-
props.sessionStore.overlay = "tasks";
|
|
2708
|
-
if (agentLoop.isRunning)
|
|
2709
|
-
props.sessionStore.pendingResetUI = true;
|
|
2710
|
-
}
|
|
2711
|
-
setOverlay("tasks");
|
|
2712
|
-
}
|
|
3549
|
+
openOverlay("tasks");
|
|
3550
|
+
}, onToggleGoal: () => {
|
|
3551
|
+
openOverlay("goal");
|
|
2713
3552
|
}, onToggleSkills: () => {
|
|
2714
|
-
|
|
2715
|
-
props.sessionStore.overlay = "skills";
|
|
2716
|
-
props.resetUI();
|
|
2717
|
-
}
|
|
2718
|
-
else {
|
|
2719
|
-
if (props.sessionStore) {
|
|
2720
|
-
props.sessionStore.overlay = "skills";
|
|
2721
|
-
if (agentLoop.isRunning)
|
|
2722
|
-
props.sessionStore.pendingResetUI = true;
|
|
2723
|
-
}
|
|
2724
|
-
setOverlay("skills");
|
|
2725
|
-
}
|
|
3553
|
+
openOverlay("skills");
|
|
2726
3554
|
}, onTogglePixel: () => {
|
|
2727
|
-
|
|
2728
|
-
props.sessionStore.overlay = "pixel";
|
|
2729
|
-
props.resetUI();
|
|
2730
|
-
}
|
|
2731
|
-
else {
|
|
2732
|
-
if (props.sessionStore) {
|
|
2733
|
-
props.sessionStore.overlay = "pixel";
|
|
2734
|
-
if (agentLoop.isRunning)
|
|
2735
|
-
props.sessionStore.pendingResetUI = true;
|
|
2736
|
-
}
|
|
2737
|
-
setOverlay("pixel");
|
|
2738
|
-
}
|
|
3555
|
+
openOverlay("pixel");
|
|
2739
3556
|
}, onTogglePlanMode: () => {
|
|
2740
3557
|
const next = !planMode;
|
|
2741
3558
|
setPlanMode(next);
|
|
@@ -2749,7 +3566,12 @@ export function App(props) {
|
|
|
2749
3566
|
id: getId(),
|
|
2750
3567
|
},
|
|
2751
3568
|
]);
|
|
2752
|
-
}, cwd: props.cwd, commands: allCommands, eyesCount: eyesCount }), overlay === "model" ? (_jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider })) : overlay === "theme" ? (_jsx(ThemeSelector, { onSelect: handleThemeSelect, onCancel: () => setOverlay(null), currentTheme: theme.name })) : (_jsx(Footer, { model: currentModel, tokensIn: agentLoop.contextUsed, contextWindowOptions: contextWindowOptions, cwd: displayedCwd, gitBranch: gitBranch, thinkingLevel: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined, planMode: planMode, exitPending: exitPending })
|
|
3569
|
+
}, cwd: props.cwd, commands: allCommands, eyesCount: eyesCount }), overlay === "model" ? (_jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider })) : overlay === "theme" ? (_jsx(ThemeSelector, { onSelect: handleThemeSelect, onCancel: () => setOverlay(null), currentTheme: theme.name })) : (_jsxs(_Fragment, { children: [_jsx(Footer, { model: currentModel, tokensIn: agentLoop.contextUsed, contextWindowOptions: contextWindowOptions, cwd: displayedCwd, gitBranch: gitBranch, thinkingLevel: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined, planMode: planMode, exitPending: exitPending }), !exitPending && _jsx(GoalStatusBar, { entries: goalStatusEntries })] })), (footerStatusLayout.hasBackgroundTasks ||
|
|
3570
|
+
footerStatusLayout.hasEyesSignals ||
|
|
3571
|
+
footerStatusLayout.hasUpdateNotice) && (_jsxs(Box, { flexDirection: footerStatusLayout.stack ? "column" : "row", width: columns, children: [footerStatusLayout.hasBackgroundTasks && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate, compact: footerStatusLayout.compactBackgroundTasks })), footerStatusLayout.hasEyesSignals && (_jsx(Box, { paddingLeft: footerStatusLayout.stack || bgTasks.length === 0 ? 1 : 2, paddingRight: 1, children: _jsx(Text, { color: theme.accent, bold: true, wrap: "truncate", children: `${eyesCount} eyes signal${eyesCount === 1 ? "" : "s"} · Run /eyes-improve to enhance GG Coder` }) })), footerStatusLayout.hasUpdateNotice && (_jsx(Box, { paddingLeft: footerStatusLayout.stack ||
|
|
3572
|
+
(!footerStatusLayout.hasBackgroundTasks && !footerStatusLayout.hasEyesSignals)
|
|
3573
|
+
? 1
|
|
3574
|
+
: 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
|
|
2753
3575
|
}
|
|
2754
3576
|
function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
|
|
2755
3577
|
const status = enabled ? "on" : "off";
|