feat(worktree): add status + assert-clean read verbs (AB#3086, PR 1a/7) - #308
Merged
Conversation
PR 1a of the AB#3085 epic — bare-repo + per-run worktree model. Adds two
read-only worktree verbs that the launcher (PR 3) and apex driver (PR 4)
will both gate dispatch on, structurally preventing the recurring
worktree-hijack and worktree-dirty production bugs.
Surface (routing-style envelopes; always exit 0):
- polyphony worktree status [--path P]
Reports {path, is_clean, current_branch, dirty_paths, error}.
--path defaults to the current directory.
- polyphony worktree assert-clean [--path P] [--expected-branch B]
Pre-flight gate. Routes on {ok, reason, ...}. Reason values:
null — assertion passed
path_missing — path does not exist or is not a directory
not_a_worktree — git stderr matched "not a git repository"
git_failed — git status failed for some other reason
(locked index, dubious-ownership refusal,
permissions, etc.) — distinct remediation
from "not a worktree" so the consumer
prompts for the right fix
git_operation_in_progress — paused merge / rebase / cherry-pick /
revert / bisect; in_progress_operation
field carries which one
dirty — git status --porcelain returned entries
wrong_branch — current branch != expected_branch
internal_error — verb itself crashed (still exits 0)
Check ordering reflects what the operator must act on first:
path → not-a-worktree → git-failed → in-progress → dirty → wrong-branch.
A paused git operation can leave clean porcelain but is wholly unsafe to
dispatch into; dirty fires before wrong-branch because the operator must
reconcile dirt before they can switch branches.
Implementation hardening (rubber-duck adoptions):
- IGitClient.GetStatusAsync(workingDirectory) invokes
`git -C wd --no-optional-locks status --porcelain` so concurrent gate
callers (launcher + driver racing) do not contend on the index lock or
rewrite the index timestamp behind the operator's back.
- IGitClient.GetInProgressOperationAsync(workingDirectory) resolves the
per-worktree gitdir via `git rev-parse --git-dir` then probes the
canonical sentinel paths (rebase-merge/, rebase-apply/, MERGE_HEAD,
CHERRY_PICK_HEAD, REVERT_HEAD, BISECT_LOG).
- assert-clean's stderr discrimination separates "fatal: not a git
repository" (→ not_a_worktree) from "fatal: detected dubious
ownership" / "Unable to create '.git/index.lock'" (→ git_failed).
- All exception paths emit envelope + exit 0 — non-zero exits would
misroute downstream shell scripts before they parse stdout.
Sentinel exclusion: VerbHaltOnMissingTheory's auto-discovery treats any
empty-string default as a Move-#2 "missing required" sentinel. Status
and AssertClean use `path = ""` / `expectedBranch = ""` as
"derive-from-default" sentinels (path defaults to cwd; expected-branch
defaults to "skip the branch check"). Added an
OptionalEmptyStringSentinelZone bucket to opt them out, parallel to the
existing Stage4 manifest-path bucket.
Tests: 22 new unit tests covering all reason branches, in-progress
sentinels (rebase + merge), stderr-class discrimination, detached HEAD
with expected branch, and dirty-precedence ordering. FakeGitClient and
StubGitClient stubs in unrelated tests updated to satisfy the new
IGitClient overloads.
Build green; all 3217 tests pass; CI lint (jinja-resolver, version-drift,
prose-children, conductor-validate) green.
Refs AB#3086 — sub-issue under epic AB#3085.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PolyphonyRequiem
added a commit
that referenced
this pull request
May 11, 2026
#309) A small follow-up to PR #308 (PR 1a of the AB#3085 epic). Surfaces the bare-repo + per-run worktree layout requirement in `polyphony state preflight` so operators on the legacy non-bare clone — the source of the launcher-hijack and worktree-dirty production bugs — see the layout mismatch and a remediation pointer at the top of every SDLC apex run. Surface: polyphony state preflight --work-item N advisory_checks: - bare_repo: PASS Bare repo at /Users/{you}/projects/polyphony.git — bare-repo + per-run worktree layout (AB#3085). OR - bare_repo: FAIL Common-dir at {path} is a non-bare clone (legacy layout). The SDLC orchestrator's per-run worktree model requires a bare common-dir to prevent the launcher-hijack and worktree-dirty bug classes (AB#3085). Currently advisory; will become required once the migration script ships. Remediation: Migrate to the bare-repo + per-run worktree layout. See docs/per-run-worktree-layout.md (tracked by AB#3085). Implementation: - New `IGitClient.IsBareRepositoryAsync(commonDir, ct)`. Invokes `git --git-dir={commonDir} rev-parse --is-bare-repository`. Two empirically-verified non-obvious choices, both documented in the interface doc-comment: 1. Probe via `--git-dir`, NOT cwd discovery. From a linked worktree of a bare repo, `git rev-parse --is-bare-repository` returns false because git resolves the worktree-specific gitdir under `{commonDir}/worktrees/{name}/`, which is itself non-bare. Only the explicit `--git-dir={commonDir}` form returns true. 2. The explicit form also bypasses the `safe.bareRepository=explicit` guard set globally on many secured workstations (including Daniel's), which otherwise refuses plain `git -C {bare} rev-parse` with "fatal: cannot use bare repository ... (safe.bareRepository is 'explicit')". Both points are also captured in the new doc and as repo memories so PR 2 (migration script) and PR 3 (launcher rework) can pick up the same hard-won lessons. - New private `CheckBareRepoAsync` in StateCommands. Resolves common-dir, then probes is-bare-repository against it. Three failure routes, all carrying the same remediation string pointing at docs/per-run-worktree-layout.md: - common-dir resolution failed (not in a git repo, or rev-parse crashed) → "Not inside a git repository (no common-dir resolvable)." - is-bare-repository returned false → "Common-dir at {path} is a non-bare clone (legacy layout). ..." - is-bare-repository invocation crashed (incl. the safe.bareRepository=explicit case if the probe is ever invoked without the explicit form) → "git --is-bare-repository failed for {path}: {message}" - Wired into `polyphony state preflight` as ADVISORY (not required) for now. Required-now would create a chicken-and-egg gate: every SDLC apex run would block on a layout the operator has no tooling to fix. Inline comment marks the flip-to-required + extension-to-preflight- lite that lands once `scripts/Migrate-ToBareRepo.ps1` (PR 2) ships. - Stub `docs/per-run-worktree-layout.md` describing the why (the two bug classes), the target on-disk shape, the probe semantics (including the two non-obvious points above), and the manual migration procedure to use until the migration script ships. - Test stubs in LockCommandsTests.FakeGitClient and PolyphonyStatePathsTests.StubGitClient updated with the new IGitClient overload (returning false / NotSupportedException respectively). Tests: 4 new tests covering bare-passes / non-bare-fails-with-doc-link / common-dir-probe-fails / is-bare-probe-fails. Existing happy-path preflight tests updated with the new StubBareRepo helper. New preflight-lite test asserts bare_repo is NOT included there until the flip (regression guard for the transition plan). 3201 tests pass; 4 CI lint suites green; manual git transcript verification of bare-repo detection semantics included in the PR body. Refs AB#3093 — sub-issue under epic AB#3085. Co-authored-by: Daniel Green <dangreen@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced May 11, 2026
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.
PR 1a of 7 in the AB#3085 epic — bare-repo + per-run worktree model. Closes AB#3086.
Why
The current SDLC orchestration's shared-worktree model has caused two recurring production bugs:
Invoke-PolyphonySdlc.ps1defaultsWorktreeRoot = (Get-Location).Path. When invoked from~/projects/polyphony, the apex run yanks the operator's main worktree offmainonto animpl/{apex}-{item}branch mid-conversation.worktree_dirtycross-contamination — sibling apex runs and ad-hoc operator state on the shared main worktree race each other.The full epic plan replaces the shared-worktree model with
~/projects/polyphony.git/(bare) +~/projects/polyphony/(operator main, never targeted) +~/projects/polyphony-runs/apex-{N}/(plain container) + nested per-branch worktrees. Both production bugs become structurally impossible.This PR is the keystone: it adds the read-only verbs the launcher (PR 3) and apex driver (PR 4) need to gate dispatch on. Without these verbs first, the rest of the stack cannot land safely.
Surface
Two routing-style verbs (always exit 0; consumers branch on JSON envelope fields):
polyphony worktree status [--path P]Reports
{path, is_clean, current_branch, dirty_paths, error}.--pathdefaults to the current directory.polyphony worktree assert-clean [--path P] [--expected-branch B]Pre-flight gate. Routes on
{ok, reason, ...}. Reason values:nullpath_missingnot_a_worktreegit_failedgit_operation_in_progressin_progress_operationfield carries which onedirtygit status --porcelainreturned entries;dirty_pathscarries them verbatimwrong_branchexpected_branchinternal_errorCheck ordering reflects what the operator must act on first:
path→not_a_worktree→git_failed→git_operation_in_progress→dirty→wrong_branch. A paused git operation can leave clean porcelain but is wholly unsafe to dispatch into;dirtyfires beforewrong_branchbecause the operator must reconcile dirt before they can switch branches.Implementation hardening (rubber-duck adoptions)
The rubber-duck pass surfaced one blocker and three meaningful non-blockers; all adopted:
--no-optional-locks—IGitClient.GetStatusAsync(workingDirectory)invokesgit -C wd --no-optional-locks status --porcelainso concurrent gate callers (launcher + driver racing) do not contend on the index lock or rewrite the index timestamp behind the operator's back. These are pure read probes; the caller has explicitly opted out of the index-refresh side effect.IGitClient.GetInProgressOperationAsync(workingDirectory)resolves the per-worktree gitdir viagit rev-parse --git-dir, then probes the canonical sentinel paths (rebase-merge/,rebase-apply/,MERGE_HEAD,CHERRY_PICK_HEAD,REVERT_HEAD,BISECT_LOG). This catches the class of bug wherestatus --porcelainreports clean but the worktree is mid-rebase — the exact failure mode of the recurringworktree_dirtyincidents.assert-cleanseparates "fatal: not a git repository" (→not_a_worktree) from "fatal: detected dubious ownership" / "Unable to create '.git/index.lock'" (→git_failed). The remediation is genuinely different.ExitCodes.Success. Non-zero exits would misroute downstream shell scripts before they ever parse stdout.Sentinel exclusion
VerbHaltOnMissingTheory's auto-discovery treats any empty-string default as a Move-#2 "missing required" sentinel.StatusandAssertCleanusepath = ""/expectedBranch = ""as derive-from-default sentinels (path defaults to cwd; expected-branch defaults to "skip the branch check"). Added anOptionalEmptyStringSentinelZonebucket to opt them out, parallel to the existing Stage4 manifest-path bucket.Tests
22 new unit tests covering:
assert-clean(path-missing, not-a-worktree, git-failed × 2 stderr classes, in-progress × 2 operations, dirty, wrong-branch, dirty-precedence-over-wrong-branch, detached-HEAD-with-expected-branch)FakeGitClientandStubGitClientstubs in unrelated tests updated to satisfy the newIGitClientoverloadsAcceptance
artifacts/verb-output-schemas.jsonregenerated and matches embedded catalogworktree status,worktree assert-clean) visible in catalogStack
status,assert-clean)init-apex,create)Migrate-ToBareRepo.ps1)WorktreeRootfrom runs dir; refuses main-worktree target; subsumes #302docs/decisions/per-run-worktree-model.md)worktree gcPR 1b and onward are blocked on this landing.