feat: /pull-base lab command — user-driven base sync, pull-on-clear, behind-base badge #149
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#149
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Long-lived instances (grilling sessions especially) go stale when AFK runs land PRs: the operator grills, files an issue, an AFK agent merges it, and the next grill in the same session reasons against outdated code. Staleness has two layers — the worktree (git state behind
origin/<base>) and the context (the model's cached reads of old code). Worktrees are forked once at spawn (gitx.AddWorktree) and never re-synced;gitx.Fetchonly refreshes the bare reference repo.This issue adds a user-driven sync: a
/pull-baselab command, an automatic pull-before-clear, and a behind-base badge so the operator knows when to pull.Design decisions (grilled 2026-07-12)
/pull-basein the composer reply path; it is never forwarded to the provider. Git ops run on lab's existing surface (gitx.Engineheld by the services): fetch the bare reference repo, then mergeorigin/<base>insiderun.WorktreePath. Reuse thecrmergepatterns (internal/gitx/crmerge.go) for author identity, credential env (credentialEnv), and cancellation-immune cleanup. Cross-provider by construction (claude + codex).git merge --abort, worktree untouched, conflicting files reported to the operator. No extra pre-merge dirty check: git's own refusal (only when the merge would touch dirty files) is the right granularity; surface its message.working. If the chat tailer's conversational state isworking, fail fast with "agent is busy — retry when idle". No deferral/queue machinery. Allowed in idle / needs-input / question-pending.<old>..<new>SHAs), commit subjects (with PR/issue refs as they appear), a capped name-status file list, and a closing instruction that prior reads of these files may be stale — re-read what you rely on, rungit log/git diff <old>..<new>for detail. Errors (busy, conflict, fetch failure) surface to the operator as command feedback in the UI, never as an agent message.clearcommand text (already tagged viaprovider.CommandRoleClear, and backing the New-conversation action) and runs the pull before forwarding the clear, so the fresh context's first read sees fresh disk. Silent on success (nothing is stale post-clear, no digest needed). Loud on failure (toast/notice: base stayed old). If state isworking, skip the pull with a notice — never block the clear itself. Clears typed directly in tmux are out of scope (escape hatch).repos.default_branch(there is noruns.base_branchyet). Write the seam as "the run's base" so #130/#131 (per-run base branch) slots in without rework.rev-list --countoforigin/<base>not reachable from the run branch — mirror of the existinggitx.CommitsAhead) against the bare repo and exposes it on the runs/chat API. UI shows an "N behind" chip on the chat header and runs rail; hidden at 0. Freshness rides the existing fetch cadence (sweeps, reaper, and crmerge itself — lab knows the instant it advances a base); externally-pushed commits appear at the next fetch. The badge and the digest share the same comparison primitive./pull-baseis the first. Update the catalog entry description in CONTEXT.md accordingly.Implementation order (build server-first; if squeezed, the badge chip styling is the cuttable tail)
gitx: comparison primitive (CommitsBehind/ base-status: behind count + old/new SHAs + name-status list) against the bare repo; merge-into-worktree op with abort-on-conflict (model onmergeInTempWorktree, but targetrun.WorktreePath).crmerge.internal/chat/internal/httpapi: recognize/pull-base, execute, inject digest on success, return errors as command feedback. Surface/pull-basein the slash-command catalog (GET /runs/{id}/commands) as a lab-owned entry.cleartext → pull first (skip-with-notice when working or on failure) → forward the clear.Acceptance criteria
/pull-baseon an idle instance whose base advanced: worktree contains the new commits (ff or merge commit), and the chat shows an injected digest with the range, subjects, file list, and re-read instruction./pull-basewhile the agent is working: no git ops run; the operator sees "agent is busy"./pull-basewith a conflicting local commit: worktree left exactly as before (merge aborted), operator sees the conflicting files, no agent message./pull-basefor every instance including AFK runs.Out of scope / accepted residuals
--resolvemode handing conflicts to the agent is a possible extension — separate issue if wanted).Landing audit — PR #150
Verdict: PASS (with three non-blocking concerns, below).
Verification signal relied on
Forgejo Actions
ci / native— green (5m4s, run 165). That job is the repo's required check and covers the whole surface this diff touches: SPA lint +format:check+ vitest +vite build, theui-tagged Go build and fullgo test(with real git/tmux/prlimit on PATH), and golangci-lint. It was not re-run.ci-nixis path-gated to nix/dependency changes and this PR touches none, so its absence is correct.Checked
afk/149already containsorigin/main; merge-tree is clean. No conflict, no resolution commit.Closes #149; head isafk/149(AFK contract satisfied).--resolve, tmux-typed clears untouched, args rejected with a 400)./pull-baseis intercepted before any forward and is a 503 (never a forward) when unwired, asserted via an emptyprov.Replies()on every error path; gitx's worktree-untouched failure contract aborts under a cancellation-immune context and is pinned byte-identically; pulls of the same run serialize on a per-run mutex.TestAPI_RunCommitsBehindandTestMergePublishesRunChanged, plus 10 web).Concerns (non-blocking — none is a regression; each degrades to today's behaviour)
isClearCommandrequires an exactcmd == "/"+c.Name(internal/httpapi/chat.go:372), but claude-code's clear entry carriesArgHint: "[name]"(internal/provider/claudecode/commands.go:42). So/clear my-sessionforwards without a pull and without a notice. The bare/clear— the common case — works. Notable because the autocomplete gesture model (#122) makes an arg-hinted command insert rather than send, which actively invites the operator to type a name.internal/pull/pull.go:182-188returnsErrNoAuthorIdentitybefore any git runs, but a ff pull authors no commit and needs no identity. On a repo with nogit_author_name/email,/pull-basefails even where it would trivially fast-forward.GET /instancesnow forks onegit rev-listper active instance row (internal/httpapi/runs.go:112-121viainstances.go:104-105). The code comment's "O(1) queries" is true of the SQL but not of the git subprocesses, and this endpoint is refetched on everyrun.changed/ debouncedrun.messages.changed. Bounded by the active-instance count, so it is small today — but the cost is unremarked in the comment that claims otherwise.Concerns 1 and 2 are worth a follow-up issue; none blocks the merge.