feat: /pull-base lab command — user-driven base sync, pull-on-clear, behind-base badge #150

Merged
dominik.polakovics merged 1 commit from afk/149 into main 2026-07-13 00:06:18 +02:00

Closes #149

What

Long-lived instances (grilling sessions especially) go stale when AFK runs land PRs: the worktree falls behind origin/<base> and the model's context caches reads of old code. This PR ships the full design from #149:

  • /pull-base lab command — the first lab command (new CONTEXT.md concept): the server intercepts it in the composer reply path and it never reaches the provider. It fetches the bare reference repo, merges origin/<base> into the run's live worktree (ff when possible, real merge commit otherwise), and repairs the context layer by injecting a compact digest (merge range, commit subjects, capped name-status file list, re-read instruction) down the ordinary reply path.
  • Merge policy — abort on conflict: worktree left byte-identical, conflicting files reported to the operator (409), no agent message. Git's own dirty-clobber refusal is surfaced verbatim (502). No pre-merge dirty check.
  • Turn gating — a positively working agent gets "agent is busy — retry when idle" (409) with no git ops; idle / needs-input / question-pending proceed. If a question dialog blocks digest delivery after a successful merge, the operator gets a 200 notice ("digest was not delivered") rather than a false error.
  • Pull-on-clear — a reply matching the provider's role=clear command pulls the base before forwarding the clear, so the fresh context's first read sees fresh disk. Silent on success; loud (200 notice) when skipped-busy or failed; the clear itself always goes through.
  • Behind-base badgecommits_behind on run detail + instance rows (computed from the bare repo's local refs, no fetch in the request path; freshness rides the existing fetch cadence), an "N behind" chip on the chat header and runs rail (hidden at 0), and a new run.changed publish from crmerge the instant it advances a base.
  • Base derivation — v1 uses repos.default_branch, written as "the run's base" seam so #130/#131 slot in without rework.

Layering

gitx.PullBase/CommitsBehind/SummarizeRange (primitives, worktree-untouched failure contract) → internal/pull (crmerge-sibling service: per-run lock, credential env, real author identity, digest rendering, bus publish) → httpapi reply-path interception + catalog entry (source: "lab") → web chips/notice banner.

Testing

  • 9 gitx integration tests (ff / merge / conflict-abort byte-identical / dirty-clobber / dirty-unrelated survives / fetch failure / summaries with caps+renames).
  • 8 pull service tests (digest goldens, identity, conflict, up-to-date, no-identity refusal, event publish).
  • 12 httpapi interception tests (happy path, never-forwarded guarantee, args 400, unwired 503, busy 409, conflict 409 naming files, up-to-date notice, digest-undelivered notice, clear-hook × pull/busy/failure, non-clear commands unaffected) + behind-count and crmerge run.changed tests.
  • 10 web tests (chip render/hide on header + rail, notice vs error banner, reply contract).
  • Full go test ./... green except the pre-existing environmental internal/tmuxx real-tmux failures (package byte-identical to main; sessions cannot survive in this sandbox). go vet ./..., gofmt, npm test (743), npm run lint, format:check, vite build, and the CI-equivalent -tags ui build with embedded dist all green. golangci-lint is not available in the sandbox — CI's lint gate is the authority there.

Out of scope (per issue)

Manual conflict resolution only (no --resolve mode), tmux-typed clears bypass the pull, staleness between fetches for out-of-band pushes, no command arguments in v1.

🤖 Generated with Claude Code

https://claude.ai/code/session_01F3HJWv15fwMgR7iyL5UK91

Closes #149 ## What Long-lived instances (grilling sessions especially) go stale when AFK runs land PRs: the worktree falls behind `origin/<base>` and the model's context caches reads of old code. This PR ships the full design from #149: - **`/pull-base` lab command** — the first *lab command* (new CONTEXT.md concept): the server intercepts it in the composer reply path and it never reaches the provider. It fetches the bare reference repo, merges `origin/<base>` into the run's live worktree (ff when possible, real merge commit otherwise), and repairs the context layer by injecting a compact digest (merge range, commit subjects, capped name-status file list, re-read instruction) down the ordinary reply path. - **Merge policy** — abort on conflict: worktree left byte-identical, conflicting files reported to the operator (409), no agent message. Git's own dirty-clobber refusal is surfaced verbatim (502). No pre-merge dirty check. - **Turn gating** — a positively `working` agent gets "agent is busy — retry when idle" (409) with no git ops; idle / needs-input / question-pending proceed. If a question dialog blocks digest delivery after a successful merge, the operator gets a 200 notice ("digest was not delivered") rather than a false error. - **Pull-on-clear** — a reply matching the provider's `role=clear` command pulls the base *before* forwarding the clear, so the fresh context's first read sees fresh disk. Silent on success; loud (200 notice) when skipped-busy or failed; the clear itself always goes through. - **Behind-base badge** — `commits_behind` on run detail + instance rows (computed from the bare repo's local refs, no fetch in the request path; freshness rides the existing fetch cadence), an "N behind" chip on the chat header and runs rail (hidden at 0), and a new `run.changed` publish from crmerge the instant it advances a base. - **Base derivation** — v1 uses `repos.default_branch`, written as "the run's base" seam so #130/#131 slot in without rework. ## Layering `gitx.PullBase`/`CommitsBehind`/`SummarizeRange` (primitives, worktree-untouched failure contract) → `internal/pull` (crmerge-sibling service: per-run lock, credential env, real author identity, digest rendering, bus publish) → `httpapi` reply-path interception + catalog entry (`source: "lab"`) → web chips/notice banner. ## Testing - 9 gitx integration tests (ff / merge / conflict-abort byte-identical / dirty-clobber / dirty-unrelated survives / fetch failure / summaries with caps+renames). - 8 pull service tests (digest goldens, identity, conflict, up-to-date, no-identity refusal, event publish). - 12 httpapi interception tests (happy path, never-forwarded guarantee, args 400, unwired 503, busy 409, conflict 409 naming files, up-to-date notice, digest-undelivered notice, clear-hook × pull/busy/failure, non-clear commands unaffected) + behind-count and crmerge run.changed tests. - 10 web tests (chip render/hide on header + rail, notice vs error banner, reply contract). - Full `go test ./...` green except the pre-existing environmental `internal/tmuxx` real-tmux failures (package byte-identical to main; sessions cannot survive in this sandbox). `go vet ./...`, `gofmt`, `npm test` (743), `npm run lint`, `format:check`, `vite build`, and the CI-equivalent `-tags ui` build with embedded dist all green. golangci-lint is not available in the sandbox — CI's lint gate is the authority there. ## Out of scope (per issue) Manual conflict resolution only (no `--resolve` mode), tmux-typed clears bypass the pull, staleness between fetches for out-of-band pushes, no command arguments in v1. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01F3HJWv15fwMgR7iyL5UK91
feat: /pull-base lab command — base sync, pull-on-clear, behind-base badge
All checks were successful
ci / native (pull_request) Successful in 5m4s
5c8bb34ca7
Long-lived instances go stale when AFK runs land PRs on their base; this
adds the user-driven sync designed in #149:

- gitx: CommitsBehind (mirror of CommitsAhead) plus PullBase — fetch the
  bare reference repo, merge origin/<base> into the run's LIVE worktree
  (ff when possible), abort back to a byte-identical worktree on conflict
  with the conflicted paths typed on *ConflictError — and SummarizeRange
  (capped subjects + name-status list) for the digest.
- internal/pull: the orchestration service (crmerge sibling) — per-run
  lock, credential env, repo-over-settings author identity, run.changed
  publish, and the compact agent-facing digest renderer.
- httpapi: the reply path now intercepts lab commands. /pull-base is
  executed server-side and never reaches the provider: 409 while the
  agent is positively working, 409 with the file list on conflict, 200
  {"notice"} when up to date or when the digest could not be delivered,
  204 with the digest injected down the ordinary reply path on success.
  A role=clear reply pulls the base BEFORE the clear is forwarded (skip
  with a notice when busy or failing — the clear always goes through).
  The commands catalog appends the lab-owned pull-base entry.
- API: commits_behind on run detail and instance rows (local refs only,
  no fetch in the request path); crmerge publishes run.changed after
  advancing a base so badges refresh instantly.
- web: "N behind" chip on the chat header and runs rail (hidden at 0),
  notice banner variant (role=status) beside the error banner.
- CONTEXT.md: "Lab command" vocabulary + catalog entry wording.

Issue: #149

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3HJWv15fwMgR7iyL5UK91
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!150
No description provided.