feat(review): review → fix → re-review loop + Settings restore built-ins - #60
Merged
Conversation
Adds a workflow loop body with per-iteration node_runs, a `loop review run` CLI driving the existing async daemon endpoint, and two seeded workflows (review-loop, review-fix-loop) wired into a ReviewPanel split button. The loop stops when a review iteration produces zero comments or the same comment-id set as the previous iteration. WorkflowGraph expands the body into per-iteration synthetic nodes inside a dashed group container.
The review-fix-loop's bash node defaulted to `loop review run --wait` with a 5m cap. Real PR reviews routinely run 5–15 minutes, so the CLI inside the agent container timed out before the daemon finished — comments still streamed to the FE, but the loop body errored on the review node and never reached fix/verify. Bumped the CLI default to 30m. The verify step previously just printed HEAD, leaving uncommitted fixes stranded when the agent forgot to commit. It now stages and commits any leftover changes (`git diff --cached --quiet` short- circuits when the agent already committed). Added migration #7 to patch existing configs in-place, preserving any user-customized verify script.
The agent container's stdout includes a `loop-dockerproxy started`
log line before `loop review run --wait` prints its JSON envelope.
parseReviewOutput's strict json.Unmarshal of the full stdout failed
on the preamble and fell into its error branch, which cleared
CommentsJSON and set SameAsPrev = (len(prev)==0). Two visible
symptoms followed:
1. The fix prompt rendered with `{{.Review.CommentsJSON}}` empty,
so the agent replied "the comments section is empty".
2. On iter 0 prev is always empty, so SameAsPrev flipped to true
and the outer loop's `or NoComments SameAsPrev` condition
exited after a single iteration — even when max_iterations
was set to 3.
extractReviewJSON now scans stdout backwards through non-empty
lines, returning on the first that parses as the envelope; falls
back to whole-trimmed-stdout for the (hypothetical) pretty-printed
case. The reversed walk lets the JSON win even if future log lines
trail it.
The review-fix loop's body flips the daemon's review session.status back to "ready" between iterations (after each `review` child completes) and `busy` clears as soon as the initial POST returns. Together this made `runDisabled = busy || session?.status !== "ready"` re-enable the primary button mid-loop, letting a user start a second concurrent workflow run on the same worktree (review session + git commits would race). Track a dedicated `loopActive` state that flips true at run start and false on workflow.run_completed, and add it to runDisabled. The verify script's `git add -A` swept every untracked/modified file in the worktree into the auto-generated `fix: address review feedback` commit — scratch files, debug logs, dependency caches. Switch to `git add -u` so only tracked changes are staged; the fix prompt still asks the agent to commit new files itself. Add a follow-up migration that patches the buggy `-A` variant in configs from dev installs that already advanced past migration #7.
…ures
cmd/loop/review.go: the --timeout flag only gated the inter-poll
time.Now().After(deadline) check — it never bounded an in-flight
client.Do call because http.DefaultClient has no Timeout set and
cobra's parent ctx has no deadline. A hung GET response (network
stall, daemon deadlock, partial write) blocked the polling loop
inside Do indefinitely and the 30m default never fired. Wrap ctx
in context.WithTimeout when --wait is set so cancellation flows
into every request, and centralise the "timed out after X"
message via a small wrapTimeout helper. New test
TestRunReviewWaitTimeoutDuringHungResponse uses a handler that
blocks until the request ctx is cancelled to prove the bound now
trips inside Do, not just between polls.
internal/db/migrations.go: `PRAGMA foreign_keys=OFF`/`=ON` wrapped
as sqlMigration entries were silently no-ops because
runSQLMigration wraps every SQL migration in its own BeginTx and
SQLite ignores `PRAGMA foreign_keys` inside an open transaction.
Today this is benign — no other table has an incoming FK to
workflow_node_runs, so the rebuild's INSERT…SELECT/DROP/RENAME
runs fine with FK enforcement on — but the dead PRAGMAs
misrepresented what the migration guaranteed and the pattern
would silently fail if copy-pasted to a table with incoming FKs.
Replace the two PRAGMA entries with `SELECT 1` no-op placeholders
(can't drop them outright without shifting later migrations'
version numbers and breaking partially-applied installs) and
document the actual safety property + the recipe future
similar-rebuild migrations should follow (lift into funcMigration
that issues PRAGMA outside any tx).
internal/workflow/dag.go: when extractReviewJSON returned false
on the first loop iteration, prev was nil and the parser set
SameAsPrev=true. The seeded loops' stop condition
`{{ or .Review.NoComments .Review.SameAsPrev }}` then terminated
the loop with `completed` after a single iteration even though
the review subprocess emitted unparseable stdout — the UI
reported "review with no findings" when the real outcome was
"the bash node succeeded but the JSON envelope was missing"
(daemon/CLI bug, $API_URL misconfig that returned an empty body,
future stdout pollution after the JSON line). Always set
SameAsPrev=false on parse failure so the loop keeps iterating up
to maxIter instead of silently treating the failure as clean.
Tests updated: TestEmptyStdoutSameAsPrevWhenPrevEmpty renamed to
TestEmptyStdoutDoesNotTerminateLoop and TestInvalidJSONResetsState
extended to assert the new SameAsPrev=false invariant.
extractReviewJSON now requires a recognized Status ("ready" or "error")
before accepting a candidate line as the review envelope, so an
unrelated JSON object (e.g. a future sidecar log line emitted after the
CLI's compact JSON) cannot masquerade as a clean review and prematurely
terminate the seeded review-fix loop.
ReviewPanel's primary-button gate now also checks
`loopRunId !== null && !isLoopTerminal` in addition to `loopActive`, so
the "no second concurrent run on this channel" invariant is explicit
from the visible chip state rather than only the loopActive boolean.
The daemon's reviewStore is in-memory and gets wiped on restart, while
the FE may still hold a stale session object in React state. Without
this check the workflow starts and immediately fails on the CLI's
POST /review/run with a cryptic "node loop failed: script exited with
status 1". Run a GET /api/channels/{id}/review first; if absent, surface
"No review session loaded — pick a PR first, then run the loop."
WorkflowGraph drew within-iteration edges only when body children had explicit deps; the seeded review-fix-loop relied on array order alone so review → fix → verify rendered as a single vertical column with no arrows, and only the cross-iteration verify[i-1] → review[i] links were visible. Declare fix depends_on review and verify depends_on fix in the seed, plus an fsmigrate patch that backfills the deps on the existing on-disk workflow (skips when a user has already set custom deps). Execution order is unchanged — executeLoopNode iterates body sequentially regardless.
Dropdowns anchored to right- or bottom-edge buttons (e.g. the Review panel's mode caret) overflowed the window because the menu rendered at the caller-supplied (x,y) unconditionally. Measure on mount via useLayoutEffect and shift left/up so the menu fits, leaving callers free to pass the natural anchor coordinates.
Adds a "Restore built-ins" bar under the Workflows and Prompt Shortcuts sections in the global Settings dialog. Re-seeds missing entries via the existing fsmigrate seeders (skip-if-name-exists, so user edits are preserved) and reports which were added vs. already present. - Refactors seedBuiltinCodeReviewShortcut and seedReviewLoopWorkflows to return the list of added names, callable from both the migration runner and the new RestoreBuiltinShortcuts / RestoreBuiltinWorkflows wrappers (internal/fsmigrate/restore.go). - New POST /api/builtins/restore endpoint dispatches by kind and reports Added/Skipped against a canonical name list. - FE: api/builtins.ts client + Settings.tsx RestoreBuiltinsBar with per-section toast feedback and config refetch on success.
radutopala
force-pushed
the
feat/review-loop-v1
branch
from
May 26, 2026 05:34
0b0f5a3 to
0d89386
Compare
Two scenarios verify the Restore-built-ins bar surfaces correctly: already-present skip for seeded workflows, and additive re-add for the cleared shortcut (plus idempotency on a second click). Adds a settings-panel data-testid so the scoped click step can disambiguate from the outer-app sidebar Workflows button.
radutopala
force-pushed
the
feat/review-loop-v1
branch
from
May 26, 2026 05:35
0d89386 to
f6e631c
Compare
When the runMode catch handler fires (session re-check fails, no session
loaded guard trips, or startWorkflowRun rejects), the chip used to clear
to empty — making the failure invisible in the header. Show "failed"
instead so the chip mirrors the workflow.run_completed("failed") path
and the user gets consistent visual feedback across both failure modes.
Addresses the secondary recommendation from the review comment on this
catch block: with the primary trap (runDisabled gating on stale
loopRunId) already removed in a0d8ee3, this completes the cleanup.
Add a default branch to executeLoopBody's child-type switch so a body child with a type other than prompt/bash returns an explicit error instead of persisting as Success with empty output. validateWorkflowDef catches this at StartRun, but executeDAGFromCheckpoint resumes pinned workflow_defs without re-validating — a stored definition from before the validator landed or a manually edited DB row would otherwise appear to succeed on recovery while doing nothing. Also pulls in the in-flight test/doc tweaks for the review-loop work: direct-call coverage for the new executor default branch, a regression test for runReview cancellation during the poll interval, README entry for `loop review run`, and a small cleanup of parseJSONValue in the HJSON migrations helper.
Bug fixes (high/medium severity from /code-review sweep): - cmd/loop/review.go: classify HTTP 4xx and present=false as permanent (wrap in pollPermanentError); bound !--wait POST with reviewPostTimeout (30s default, struct-injected for tests). - internal/fsmigrate/restore.go: chain patchReviewFixVerifyScript + patchReviewFixLoopBodyDeps after seedReviewLoopWorkflows so stale on-disk workflows get patched on restore. - internal/workflow/dag.go: nrEnd now sets StartedAt to avoid NULL rows when nrStart silently failed. - internal/db/db.go: UpdateNodeHeartbeat takes iteration and scopes the UPDATE by (run_id, node_id, iteration); mock + 7 engine tests updated. - app/src/components/panels/WorkflowGraph.tsx: preserve external depends_on entries when expanding loop bodies (non-body deps fall through to the rewire pass). - app/src/components/panels/ReviewPanel.tsx: hydrate loopRunId / loopActive from sessionStorage on mount; resync via fetchWorkflowRun and clear stale handles on terminal status. Review prompt + defaults: - internal/api/review_handler.go: rewrite defaultReviewPrompt as a 5-angle recall finder (line-by-line, removed-behavior, cross-file, language-pitfalls, wrapper/proxy) + 1-vote 3-state verifier + gap sweep, capped at 15 findings. Keeps the existing <review-comment path line side> XML envelope; explicit "do not fix anything" so the agent leaves triage to the Review panel. - internal/fsmigrate/migrations.go + app ReviewPanel.tsx + docs: default max_iterations for review-loop and review-fix-loop is now 1 (was 3); the recall-mode finder makes extra passes redundant for most reviews.
radutopala
commented
May 26, 2026
radutopala
commented
May 26, 2026
radutopala
commented
May 26, 2026
radutopala
commented
May 26, 2026
radutopala
commented
May 26, 2026
radutopala
commented
May 26, 2026
radutopala
commented
May 26, 2026
radutopala
commented
May 26, 2026
radutopala
commented
May 26, 2026
radutopala
commented
May 26, 2026
radutopala
commented
May 26, 2026
radutopala
commented
May 27, 2026
radutopala
commented
May 27, 2026
radutopala
commented
May 27, 2026
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
radutopala
commented
May 27, 2026
radutopala
commented
May 27, 2026
radutopala
commented
May 27, 2026
radutopala
commented
May 27, 2026
radutopala
commented
May 27, 2026
radutopala
commented
May 27, 2026
radutopala
commented
May 27, 2026
This comment has been minimized.
This comment has been minimized.
radutopala
commented
May 27, 2026
radutopala
commented
May 27, 2026
radutopala
commented
May 27, 2026
radutopala
commented
May 27, 2026
radutopala
commented
May 27, 2026
This comment has been minimized.
This comment has been minimized.
radutopala
commented
May 27, 2026
Closes the last batch of unresolved threads on PR #60: bound iteration state on error, gate review-output parsing on seeded workflows, serialize restore-builtins via a mutex, harden CLI polling against transient transport errors and oversized response bodies, scope expandLoopBodies and Settings restore state per-loop/per-kind, and preserve external deps when re-keying loop body children. Tests cover the new branches; lint and coverage are clean.
Ten issue-level conversation comments on PR #60: #1 review_handler.go — runReviewAsync now takes a cancellable ctx so session-delete and server-Stop detach the long-running agent run instead of leaking the container for 5–20 min after teardown. #2/#6 dag.go — executeWithRetry's retry-attempt UpsertNodeRun now sets Iteration so a body-child retry on iter>0 no longer clobbers the iteration-0 row. #3 engine.go — recoverPausedRun/RunningRun pick the highest-iteration Success row's Output deterministically for completedOutputs, so templated body children see the correct prior-iteration value on resume instead of whatever ListNodeRuns happened to return last. #4 engine.go — both recovery paths now re-validate the pinned definition before executeDAGFromCheckpoint, so a stored def that pre-dates a validator rule fails early instead of half-executing a loop body. #5 engine.go — explicit empty-string skip on opts.Inputs so external callers sending {"max_iterations":""} no longer wipe the default. #7 WorkflowGraph.tsx — auto-center useEffect is now actually one-shot (reads hasCentered.current); new iterations no longer reset the viewport mid-investigation. #8 workflows.ts — error throws use a new describeError() helper that reads the daemon's JSON body so HTTP/2 callers stop seeing "Failed to ___: " messages with empty suffix. #9 review_handler.go — pushOneComment skips MarkPushed when ghID==0 so the 422-fallback's unparseable-response branch doesn't strand a comment as permanently undeletable. #10 ContextMenu.tsx — WAI-ARIA menu pattern: role=menu/menuitem, focus-on-mount, focus-return-on-close, roving tabindex, ArrowUp/Down/Home/End nav skipping separators. Tests: 9 new (4 review handler, 5 engine recovery/start). make lint clean, make coverage-check 100.0%.
CI electron-typecheck reported TS2345 on three setFocusIdx calls because itemIndices[i] is typed `number | undefined` under noUncheckedIndexedAccess. Hoist first/last and guard the ArrowDown/Up lookup to keep types narrow.
radutopala
added a commit
that referenced
this pull request
May 27, 2026
Loopback-gate POST /api/builtins/restore, surface patcher results as a separate Patched field, cap loop iterations at 50, scope review-output parsing to seeded workflows only, preserve IDs on parse miss (no false "no findings" exits), reject body-child IDs that collide with sibling top-level nodes, and refuse to patch a non-array config key. FE: persist the last-used review mode only after the run actually starts, iterate all gate-approval sources for inline rendering, skip the post-restore global config refetch when the user has unsaved edits, and dedupe the WorkflowGraph defs-empty fallback by node_id so 3-iteration loops no longer collapse to one node.
radutopala
added a commit
that referenced
this pull request
May 27, 2026
- Settings.tsx: per-kind restoring/restoreMsg state so Workflows/Shortcuts tabs don't leak loading state across kinds - cmd/loop/review.go: fall back to a sentinel when daemon reports status=error with an empty message, instead of surfacing a blank error - workflow/dag.go executeLoopBody: honor body-child timeout: declarations via per-iteration context.WithTimeout + explicit cancel - workflow/dag.go extractReviewJSON: forward-scan (first valid envelope wins) so debug echoes after the real envelope can't displace it - workflow/validate.go: reject body-child IDs that collide within the same loop or across two loops (both race UPSERTs on (run_id, node_id, iteration) and inflate WorkflowGraph iter counts) Tests + 100% coverage maintained.
radutopala
added a commit
that referenced
this pull request
May 27, 2026
radutopala
added a commit
that referenced
this pull request
May 27, 2026
Closes the last batch of unresolved threads on PR #60: bound iteration state on error, gate review-output parsing on seeded workflows, serialize restore-builtins via a mutex, harden CLI polling against transient transport errors and oversized response bodies, scope expandLoopBodies and Settings restore state per-loop/per-kind, and preserve external deps when re-keying loop body children. Tests cover the new branches; lint and coverage are clean.
radutopala
added a commit
that referenced
this pull request
May 27, 2026
Ten issue-level conversation comments on PR #60: #1 review_handler.go — runReviewAsync now takes a cancellable ctx so session-delete and server-Stop detach the long-running agent run instead of leaking the container for 5–20 min after teardown. #2/#6 dag.go — executeWithRetry's retry-attempt UpsertNodeRun now sets Iteration so a body-child retry on iter>0 no longer clobbers the iteration-0 row. #3 engine.go — recoverPausedRun/RunningRun pick the highest-iteration Success row's Output deterministically for completedOutputs, so templated body children see the correct prior-iteration value on resume instead of whatever ListNodeRuns happened to return last. #4 engine.go — both recovery paths now re-validate the pinned definition before executeDAGFromCheckpoint, so a stored def that pre-dates a validator rule fails early instead of half-executing a loop body. #5 engine.go — explicit empty-string skip on opts.Inputs so external callers sending {"max_iterations":""} no longer wipe the default. #7 WorkflowGraph.tsx — auto-center useEffect is now actually one-shot (reads hasCentered.current); new iterations no longer reset the viewport mid-investigation. #8 workflows.ts — error throws use a new describeError() helper that reads the daemon's JSON body so HTTP/2 callers stop seeing "Failed to ___: " messages with empty suffix. #9 review_handler.go — pushOneComment skips MarkPushed when ghID==0 so the 422-fallback's unparseable-response branch doesn't strand a comment as permanently undeletable. #10 ContextMenu.tsx — WAI-ARIA menu pattern: role=menu/menuitem, focus-on-mount, focus-return-on-close, roving tabindex, ArrowUp/Down/Home/End nav skipping separators. Tests: 9 new (4 review handler, 5 engine recovery/start). make lint clean, make coverage-check 100.0%.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Headline feature is an iterative review→fix→re-review loop runnable from the Review panel; the PR also ships a Settings "Restore built-ins" affordance and a ContextMenu viewport-clamp fix that came out of the review-loop UI work.
Review loop
loopnodebodyso a loop can run an ordered sequence of children per iteration; per-iterationnode_runsrows via a newiterationcolumn + composite(run_id, node_id, iteration)unique index.loop review run --channel-id X [--api-url] [--wait] [--timeout]CLI subcommand drives the existing async/api/channels/{id}/review/runendpoint and emits JSON consumed by the workflow body parser.--waittimeout is bounded inside the HTTP client (not just between polls), so a hung response can't outlive the deadline.review-loop,review-fix-loop) shipped viafsmigrate. Both stop early when an iteration returns zero comments or the same comment-id set as the previous iteration (SameAsPrevgate).extractReviewJSONtolerates container preamble logs and requires a recognizedStatusto accept a candidate line as the envelope, so unrelated JSON can't masquerade as a clean review.review-fix-loopbody is chained with explicitdepends_on(review → fix → verify) so WorkflowGraph draws within-iteration edges; verify usesgit add -u(tracked-only) to avoid sweeping scratch files into the auto-commit.localStorage) + max-iter input + chip mirroring workflow events; primary button gated onloopActiveANDloopRunId && !isLoopTerminalso a second concurrent run can't start mid-loop. FE re-checks the daemon's review session before dispatching the workflow (avoids a cryptic CLI failure when the in-memory session was wiped by a daemon restart).ApprovalCardrendered when no chat panel is mounted, so gate approvals during the fix step don't strand the loop.Settings — Restore built-ins
fsmigrateseeders (skip-if-name-exists, so user edits are preserved) and reports added vs. already present.seedBuiltinCodeReviewShortcutandseedReviewLoopWorkflowsto return added names, callable from both the migration runner and newRestoreBuiltinShortcuts/RestoreBuiltinWorkflowswrappers.POST /api/builtins/restoredispatches by kind; emits[]notnullfor emptyadded/skippedarrays so the FE doesn't crash on.length.Misc
ContextMenuclamps to the viewport on mount (useLayoutEffectmeasures + shifts left/up). Dropdowns anchored to right- or bottom-edge buttons (e.g. the Review panel's mode caret) no longer overflow the window.Test plan
go test -race ./...green; backend touched packages (workflow,db,fsmigrate,cmd/loop,api) at 100% coverage of changed filesmake coverage-check— Coverage: 100.0%make lint— 0 issuesnpm run typecheck(app) — cleancomponent-bddgreen (Settings restore-built-ins + Global workflows panel flake fix)review-fix-loopend-to-end on a channel; verify chip transitions, group rect renders per iteration, ApprovalCard appears inline when chat is hiddenloop review run --channel-id <id> --waitblocks until ready and prints JSON