feat(workspace): surface worktree staleness at create-time (refs #52)#53
Merged
Conversation
refs #52 `worktree add` now unconditionally fetches `origin` before creating the new checkout and then compares the materialized branch (or the local base it was cut from) against its origin counterpart, surfacing the behind-count in the ability response and CLI output. Agents can decide whether to rebase before cooking instead of discovering day-one merge conflicts at PR-review time. Non-breaking: no gating, no auto-rebase, all new response fields are optional, CLI warns but never fails. The `--allow-stale` + `--rebase-base` behavior change lives in a follow-up PR (option 2 in #52). Changes: - `WorktreeStalenessProbe` helper — fetch + behind-count + output parsing, returns structured results instead of throwing (offline work still possible) - `Workspace::worktree_add()` — unconditional fetch at the top, post-creation staleness computation for both the existing-local-branch path and the new-branch-off-local-base path - `is_remote_tracking_ref()` helper — recognizes `refs/remotes/origin/*` (what `resolve_default_base()` returns) AND `origin/*` short form as "already at-tip post-fetch" - `WorkspaceAbilities` — output schema gains six optional fields: `fetch_failed`, `fetch_error`, `stale_commits_behind`, `upstream`, `base_stale_commits_behind`, `base_upstream` - `WorkspaceCommand::render_worktree_freshness()` — renders a `Freshness:` block after bootstrap output. Elides the line entirely when no signal is available rather than print a misleading "up to date" - `tests/smoke-worktree-staleness.php` — 23 assertions: parse_count, is_missing_upstream heuristics, real-git fixtures for fetch success, fetch failure (no origin), behind-count parsing, no-upstream → null, at-tip → 0
chubes4
added a commit
that referenced
this pull request
Apr 24, 2026
Closes #52 Builds on #53's non-breaking staleness signal. `worktree add` now refuses to materialize a worktree that would be more than `datamachine_worktree_stale_threshold` commits (default 50) behind its upstream, tearing the half-cooked checkout down and returning a `worktree_stale` WP_Error with remediation options. Pass `--allow-stale` to opt in, or `--rebase-base` to auto-rebase onto the upstream tip before returning. On rebase conflicts the rebase is aborted — `--rebase-base` is not a silent bypass. ## Behavior change Previously: `worktree add` happily cooked on top of any local branch, no matter how stale. Agents discovered multi-hundred-commit drift at PR-review time when GitHub flagged conflicts. Now: by default, a worktree that would land >50 commits behind upstream is rolled back at create-time with a WP_Error pointing at four concrete remediation paths: Worktree base is 51 commits behind origin/main (threshold: 50). Options: - workspace git-pull data-machine-code --allow-primary-mutation - worktree add … --from=origin/main - worktree add … --rebase-base - worktree add … --allow-stale Threshold is filterable per-site/per-repo via `datamachine_worktree_stale_threshold`. ## Rebase semantics `--rebase-base` picks the right target per path: - Existing-local-branch: rebase onto `@{upstream}` - New-branch-off-local-base: rebase onto `origin/<base>` Success clears the behind-count (worktree passes the gate cleanly). Failure aborts the rebase, the worktree stays at its pre-rebase HEAD, and `rebase_succeeded: false` + `rebase_error: <tail>` are surfaced. Critically, the staleness gate STILL fires after a failed rebase — `--rebase-base` alone on a conflicting rebase isn't a silent `--allow-stale` bypass. ## Changes - `Workspace::worktree_add()` — new `$allow_stale` + `$rebase_base` params. Rebase runs before the gate (success nullifies gate). Gate runs after staleness probe, computes filterable threshold, tears the worktree down + returns WP_Error on violation. - `effective_behind_count()` helper — picks the one behind-count that matters for gating (existing-branch or new-branch-off-local-base, whichever is present). - `try_rebase_worktree()` helper — selects upstream target, runs rebase, aborts on failure, zeroes the relevant behind-count on success. - `WorkspaceAbilities` — input schema gains `allow_stale` + `rebase_base` (both default false). Output schema gains `gate_threshold`, `rebase_attempted`, `rebase_target`, `rebase_succeeded`, `rebase_error`. - `WorkspaceCommand` — new `--allow-stale` + `--rebase-base` flags with full help blocks + examples. `render_worktree_freshness()` gains a rebase block that renders BEFORE staleness (success: "rebased onto <target>", failure: "⚠ rebase onto <target> failed" + error tail). - `tests/smoke-worktree-staleness.php` — 7 new real-git-fixture assertions for rebase success (branch 2 behind → 0 behind after rebase, consumer commit preserved) and rebase conflict handling (non-zero exit, abort restores pre-rebase SHA, behind-count preserved).
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
worktree addnow unconditionally fetchesoriginbefore creating the new checkout, then compares the materialized branch (or the local base it was cut from) against its origin counterpart and surfaces the behind-count in the ability response + CLI output. Agents see at creation time that they're about to cook on top of a 47-commit-stale base, instead of discovering it at PR-review time.Refs #52 (does not close — this is PR 1 of 2; the
--allow-stalegate +--rebase-baseopt-in live in a follow-up).What changed
Non-breaking and purely additive. No gating, no auto-rebase, no new required inputs. Default invocations behave identically except for a freshness line in CLI output when the probe has something meaningful to say.
New:
WorktreeStalenessProbePure helper under
inc/Workspace/. Two public static methods plus parse utilities:fetch( $repo_path )— wrapsgit fetch --quiet origin, returns['ok' => bool, 'error' => string?]. Never throws. Offline work stays possible; the caller just learns staleness data is untrustworthy.behind_count( $repo_path, $ref, $upstream )— returnsinton success,nullwhen no upstream is configured,WP_Erroron unexpected git failure. Callers MUST distinguishnullfrom0— 0 means "up to date", null means "cannot tell".parse_count()+is_missing_upstream()— tolerant parsing of rev-list output and a heuristic for git's many ways of saying "no upstream configured" (unknown revision / ambiguous argument / bad revision / etc.).Modified:
Workspace::worktree_add()Three tweaks:
Unconditional fetch at the top (right after input validation, before
show-ref). Moved out of the new-branch arm where it used to live. Cheap — shared object store. Fetch failure is recorded in the response asfetch_failed: true+fetch_error: <tail>but does not abort worktree creation.Post-creation staleness computation in two forms:
rev-list --count <branch>..@{upstream}from the new worktree. Surfacesstale_commits_behind+upstream(best-effort derived viarev-parse --abbrev-ref --symbolic-full-name). When no@{upstream}is configured, fields are silently omitted.origin/<base>. Surfacesbase_stale_commits_behind+base_upstream. When the origin counterpart doesn't exist, silently omitted.is_remote_tracking_ref()helper — recognizes bothrefs/remotes/origin/*(whatresolve_default_base()returns for the default-base path) and the shorterorigin/*form a caller might pass explicitly. Both are "already at-tip post-fetch"; comparing them against themselves would produce nonsense. Caught this in live testing before opening the PR — the first pass usedstr_starts_with($resolved_base, 'origin/')which missed the fully-qualified form.Modified:
WorkspaceAbilitiesdatamachine/workspace-worktree-addoutput schema gains six optional fields:fetch_failed,fetch_error,stale_commits_behind,upstream,base_stale_commits_behind,base_upstream. All optional — omitted when no meaningful value to report.Modified:
WorkspaceCommandNew
render_worktree_freshness()runs after the bootstrap block:Priority order:
fetch_failed→ existing-branch staleness → base staleness → up-to-date-with-label → elide. The "elide when no signal available" case is deliberate — a default invocation offorigin/HEADwith no explicit base has no comparison to make, and printing "up to date" there would be claiming something we can't actually vouch for.Testing
Smoke suite: 85/85 passing
Covers:
parse_count()tolerance,is_missing_upstream()heuristics across git phrasings, real-git fixtures for fetch success + fetch failure (repo with noorigin),behind_count()returningintwhen behind,nullwhen no upstream configured,nullwhen upstream ref doesn't exist,0when at tip (distinct from null).Live CLI tests on
intelligence-chubes4Studio site--fromorigin/mainfeat/workspace-worktrees(36 behind origin/main, no origin counterpart)origin/mainresolved torefs/remotes/origin/mainis_remote_tracking_ref()The first-pass bug is what motivated the
is_remote_tracking_ref()helper. Without it, default invocations would have tried to computerev-list refs/remotes/origin/main..origin/refs/remotes/origin/mainand produced garbage output. Caught before commit, test added implicitly via theorigin/short-form guard staying intact.Out of scope (follow-up PR)
--allow-stalegate with a configurable behind-threshold (default 50). This is the behavior-change half of Worktree add doesn't check staleness — primary drift and stale local branches ship PRs with day-one merge conflicts #52.--rebase-baseopt-in auto-rebase.AI assistance
data-machine-codeon this Studio site — caught therefs/remotes/origin/*guard bug mid-session and added theis_remote_tracking_ref()helper before commit. Chris steered the scope (PR 1 = non-breaking additive, PR 2 = gating behavior change) and caught the original session-spawn mistake that had the work happening in the wrong checkout.