Fix stale in_review flag handling and review lifecycle

Move in_review flag set inside try block so finally always clears it,
and actively clear stale flags in list when Claude is no longer active.
Previously a crash between setting the flag and entering try would
leave the session stuck in review state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Wanstrath 2026-03-18 23:28:42 -07:00
parent bc102e416c
commit 3ba550d80a
3 changed files with 14 additions and 10 deletions

View File

@ -151,7 +151,7 @@ Always use `.nothrow()` for commands that may fail non-fatally. Use `.quiet()` t
- `sandlot merge` and `sandlot squash` both delegate to `mergeAndClose()` in `helpers.ts`, which merges, resolves conflicts, commits, and then calls `closeAction()` to clean up
- `sandlot list` discovers missing session prompts by parsing Claude's `history.jsonl` from inside the container
- `sandlot list` shows five status icons: idle (dim `◯`), active (cyan `◎`), dirty/unsaved (yellow `◐`), saved (green `●`), review (magenta `⦿`)
- `sandlot review` sets `in_review` on the session during the review and clears it in a `finally` block on exit; `list` only shows review status if Claude is also active (self-heals stale flags)
- `sandlot review` sets `in_review` on the session during the review and clears it in a `finally` block on exit; `list` detects stale `in_review` flags (Claude not active) and clears them from state
- `sandlot new` and `sandlot open` auto-save changes when Claude exits (disable with `--no-save`)
- `sandlot close` has a hidden `rm` alias
- Default behavior (no subcommand): always runs `list` (which prints "No active sessions." if empty)

View File

@ -30,12 +30,11 @@ export async function action(opts: { json?: boolean }) {
} catch {}
}
if (opts.json) {
console.log(JSON.stringify(sessions, null, 2))
return
}
if (sessions.length === 0) {
if (opts.json) {
console.log("[]")
return
}
console.log("◆ No active sessions.")
if ((await vm.status()) !== "running") {
console.log(`\n${red}VM is not running.${reset}`)
@ -48,6 +47,11 @@ export async function action(opts: { json?: boolean }) {
sessions.map(async (s): Promise<[string, string]> => {
const active = await vm.isClaudeActive(s.worktree, s.branch)
if (active && s.in_review) return [s.branch, "review"]
// Self-heal: clear stale in_review flag if Claude is not active
if (!active && s.in_review) {
s.in_review = false
await state.setSession(root, s).catch(() => {})
}
if (active) return [s.branch, "active"]
const dirty = await git.isDirty(s.worktree)
if (dirty) return [s.branch, "dirty"]

View File

@ -9,10 +9,6 @@ export async function action(branch: string, extra: string | undefined, opts: {
const spin = spinner("Starting container", branch)
await vm.ensure((msg) => { spin.text = msg })
// Mark session as in review only after container is confirmed running
session.in_review = true
await state.setSession(root, session)
let prompt = `
You're a grumpy old senior software engineer. You need to review some code my co-worker wrote.
@ -73,6 +69,10 @@ Your thoughts, in brief.
if (extra) prompt += "\n\n" + extra
try {
// Mark session as in review inside try so finally always clears it
session.in_review = true
await state.setSession(root, session)
if (opts.print) {
spin.text = "Running review…"
const result = await vm.claude(session.worktree, { print: prompt })