8.8 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/platform/workspace-git-status.md | 2026-07-30 | implemented |
Implementation Plan: Git Stale-Base Refresh (inflated commits panel)
Overview
The Changes panel's COMMITS count is computed by agentctl as git log <base>..HEAD,
where <base> comes from the session's stored base_branch via
computeMergeBase(HEAD, <target_branch>). When the stored target branch is a stacked-PR
parent whose upstream ref has been merged/deleted, computeMergeBase falls back to a
stale local ref and returns an old branch point, sweeping in commits that already
landed on the integration branch. The fix makes base resolution detect that the resolved
merge-base is a strict ancestor of the integration merge-base (origin/main → origin/master)
and, when so, anchor to the integration merge-base instead. This is a read-time correction in
agentctl; no persistence, route, or payload changes.
Confirmed root cause. For the reported session, stored base_branch =
feature/CLIP-8304-rust-mock-server-adopt (a merged/deleted stacked parent). Its origin/
ref is gone, so computeMergeBase fell back to the bare local ref → ed630b8446 → 31 commits.
The true merge-base(HEAD, origin/master) is 19646efc83 → 1 commit (matches the PR and
git rev-list --first-parent --count).
Backend
Area 1 — Stale-base detection in the commits/diff base resolver
apps/backend/internal/agentctl/server/process/git_log.go
- Add
GitOperator.IsAncestor(ctx, ancestor, descendant string) (bool, error)wrappinggit merge-base --is-ancestor <ancestor> <descendant>(exit 0 = true, exit 1 = false, other = error). Reuse the existingrunGitCommandplumbing; classify exit status like the otherGitOperatorhelpers.
apps/backend/internal/agentctl/server/api/git.go
- Add
integrationMergeBaseCandidates = []string{"main", "master"}(bare names;computeMergeBasealready triesorigin/<name>first). Mirror the existingbranchDiffCandidatespriority inprocess/workspace_git_status.goso the commits panel and the task-card stats land on the same anchor. - Add
Server.integrationMergeBase(ctx, gitOp) (string, error): walks the candidates and returns the first successfulcomputeMergeBase(HEAD, <candidate>). - In
runGitLogForRepo, after resolvingbaseCommitfrom the target-branch merge-base: compute the integration merge-base; if it is non-empty and the resolvedbaseCommitIsAncestorof it (strict — not equal), replacebaseCommitwith the integration merge-base. Skip the correction when the target branch already IS an integration candidate (avoid a redundant self-compare), when no integration merge-base resolves, or whenIsAncestorerrors (fall back to current behavior). Keep the existing sanitiser barriers inline. - Keep the function within the golangci limits (≤80 lines / ≤50 statements / complexity ≤15);
extract the correction into
Server.correctStaleBase(ctx, gitOp, baseCommit, targetBranch)helper ifrunGitLogForRepogrows past the limit.
Area 2 — Cumulative diff parity (verify only, extend if needed)
apps/backend/internal/agentctl/server/api/git.go (cumulative-diff handler) and
apps/backend/internal/agentctl/server/process/workspace_git_status.go
(resolveBaseBranch/computeBaseCommit).
- The spec requires the cumulative diff to use the same corrected base as the commits panel.
Confirm whether the live cumulative-diff path shares
runGitLogForRepo's base resolution; if it computes its own base, apply the same strict-ancestor correction there. Do NOT change the orchestrator snapshot path (lifecycleAdapter.GetCumulativeDiff), which intentionally anchors to a caller-provided SHA (see comment inbackendapp/adapters.go).
Frontend
No new UI surface or payload contract. The panel still renders the agentctl result.
PR validation exposed three existing client races that made the new session-isolation E2E coverage flaky. The remediation keeps those follow-ups explicit rather than hiding production changes inside test-only commits:
apps/web/hooks/domains/session/session-state-reconciler.tsowns one keyed, reference-counted HTTP reconciliation loop per store/session.useSessionconsumers share that owner, stale HTTP snapshots cannot replace newer WebSocket state, and the last consumer cancels pending work.apps/web/hooks/use-task-removal.tsvalidates ordered next-task candidates through the authoritative task API before navigation. Cached workflow snapshots are candidate sources, not proof that a deleted or archived task remains live.apps/web/components/task/chat/clamped-scroll-restore.tsbounds delayed scroll restoration and stops when Dockview removes the target element.
Tests
- Stale stacked-parent base is corrected to integration merge-base (maps to spec scenario 1).
File:
apps/backend/internal/agentctl/server/api/git_log_merge_base_test.go. How: extend withsetupAPITestRepo; build integrationmain/origin/main, a parent branch branched off an oldmain, a feature branch off the parent, then advance+merge the parent intoorigin/mainand delete the parent'sorigin/ref. Assert the resolved base equalsmerge-base(HEAD, origin/main)and thatgit log <base>..HEAD --first-parentreturns only the feature commits. This is the regression test — it MUST fail before the code change. - Stale local ref, upstream integration ref present (spec scenario 2). Same file; assert the
origin/mainmerge-base wins over the stale local parent ref. - Base already current is unchanged (spec scenario 3). Same file; a base equal to / descendant of the integration merge-base is returned unchanged (no over-correction).
- No origin, unrelated history falls back (spec scenario 4). Same file; assert no error and the existing stored-base / branch-tip fallback path is taken.
GitOperator.IsAncestorunit test. File:apps/backend/internal/agentctl/server/process/git_log_test.go. Table-driven: ancestor, descendant, equal, and unrelated cases viasetupTestRepo.- Production route wiring. Files:
apps/backend/internal/agentctl/server/api/git_log_merge_base_test.goandgit_multi_repo_review_test.go. Exercise commits and cumulative-diff routes for single- and multi-repository stale-parent histories. - Shared tracker policy. File:
apps/backend/internal/agentctl/server/process/workspace_git_status_base_branch_test.go. Assert task-card status, commits, and cumulative diff all resolve the same corrected anchor. - Client race coverage. Files:
apps/web/hooks/domains/session/use-session.test.ts,apps/web/hooks/use-task-removal.test.ts, andapps/web/components/task/chat/clamped-scroll-restore.test.ts. Cover poller deduplication, stale/late snapshots, authoritative next-task validation, and bounded/detached scroll restore. - Cross-platform Git shim coverage. File:
apps/web/lib/git-shim-integration.test.ts. Exercise single-token, two-token, and equals-form Git global options beforepush.
Targeted commands:
cd apps/backend && go test -run 'TestComputeMergeBase|TestRunGitLogForRepo|TestGetLog|IsAncestor' ./internal/agentctl/server/api/... ./internal/agentctl/server/process/...
E2E Tests
- The stale Git-anchor behavior remains at the agentctl integration layer; a browser scenario would require nondeterministic upstream merge/deletion setup.
apps/web/e2e/tests/task/session-isolation.spec.tscovers the user-visible session state and task-removal races uncovered while validating the PR.apps/web/e2e/tests/chat/mobile-auto-scroll-toggle.spec.tsalready covers the same scroll restoration value on the mobile composition; the remediation changes state/scroll mechanics, not responsive layout or touch entry points.- The backend fixture uses a Node Git shim and
path.delimiter; the runner summary avoids GNU-onlypastebehavior so host execution remains portable on macOS.
Implementation Waves And Parallel Candidates
Small fix (2 tasks). Sequential by default in the primary conversation.
Wave 1:
- [x] [task-01-git-operator-is-ancestor](task-01-git-operator-is-ancestor.md)
Wave 2:
- [x] [task-02-stale-base-correction](task-02-stale-base-correction.md)
Task 02 depends on task 01 (IsAncestor helper). Not parallel-safe: both touch the same
agentctl git area and task 02 imports the task 01 helper.
Open Questions
- RESOLVED (Area 2): the live cumulative-diff handler
runGitCumulativeDiffForRepocomputes its own base via the sharedcomputeMergeBase(a separate call site fromrunGitLogForRepo, not a shared resolver). Task 02 applied the samecorrectStaleBasecall in both places, so the commits panel and the cumulative diff re-anchor identically. The orchestrator snapshot path (lifecycleAdapter.GetCumulativeDiff) was left untouched — it anchors to a caller-provided SHA by design.