Skip to content

feat(worktree): add status + assert-clean read verbs (AB#3086, PR 1a/7) - #308

Merged
PolyphonyRequiem merged 1 commit into
mainfrom
feature/3086-worktree-verbs
May 11, 2026
Merged

feat(worktree): add status + assert-clean read verbs (AB#3086, PR 1a/7)#308
PolyphonyRequiem merged 1 commit into
mainfrom
feature/3086-worktree-verbs

Conversation

@PolyphonyRequiem

Copy link
Copy Markdown
Owner

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:

  1. Launcher hijackInvoke-PolyphonySdlc.ps1 defaults WorktreeRoot = (Get-Location).Path. When invoked from ~/projects/polyphony, the apex run yanks the operator's main worktree off main onto an impl/{apex}-{item} branch mid-conversation.
  2. worktree_dirty cross-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}. --path defaults to the current directory.

polyphony worktree assert-clean [--path P] [--expected-branch B]

Pre-flight gate. Routes on {ok, reason, ...}. Reason values:

Reason Meaning
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; dirty_paths carries them verbatim
wrong_branch Current branch ≠ expected_branch
internal_error Verb itself crashed (still exits 0)

Check ordering reflects what the operator must act on first: pathnot_a_worktreegit_failedgit_operation_in_progressdirtywrong_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)

The rubber-duck pass surfaced one blocker and three meaningful non-blockers; all adopted:

  • --no-optional-locksIGitClient.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. These are pure read probes; the caller has explicitly opted out of the index-refresh side effect.
  • In-progress detectionIGitClient.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). This catches the class of bug where status --porcelain reports clean but the worktree is mid-rebase — the exact failure mode of the recurring worktree_dirty incidents.
  • Stderr discriminationassert-clean separates "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.
  • Always exit 0 — every exception path emits an envelope and returns 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. 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 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)
  • Status verb (path-missing, clean, dirty, detached HEAD, not-a-repo)
  • FakeGitClient and StubGitClient stubs in unrelated tests updated to satisfy the new IGitClient overloads

Acceptance

  • ✅ Build green
  • ✅ All 3217 tests pass
  • ✅ CI lint (jinja-resolver, version-drift, prose-children, conductor-validate) green
  • artifacts/verb-output-schemas.json regenerated and matches embedded catalog
  • ✅ Two new entries (worktree status, worktree assert-clean) visible in catalog

Stack

PR Status Description
PR 1a (this) this PR Worktree read verbs (status, assert-clean)
PR 1b next Worktree write verbs (init-apex, create)
PR 2 unstarted Migration script (two-phase Migrate-ToBareRepo.ps1)
PR 3 unstarted Launcher rework — derives WorktreeRoot from runs dir; refuses main-worktree target; subsumes #302
PR 4 unstarted Workflow integration + driver pre-flight refusal (combined)
PR 5 unstarted Concurrency-model ADR (docs/decisions/per-run-worktree-model.md)
PR 6 unstarted Documentation (onboarding guide + bootstrap skill)
PR 7 unstarted worktree gc

PR 1b and onward are blocked on this landing.

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
PolyphonyRequiem merged commit 41b0ea9 into main May 11, 2026
1 check passed
@PolyphonyRequiem
PolyphonyRequiem deleted the feature/3086-worktree-verbs branch May 11, 2026 20:07
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant