Repository navigation
feat: private visibility keeps the metadata branch on a local remote with neutral naming - #399
Merged
danielhanold merged 20 commits intoOct 6, 2026
Conversation
…havior unchanged) Every path beneath <git-common-dir>/docket/ (run tracker, resume lock, guardian marker, transactions, workspaces, gate-drive store roots, the suite budget-state file, the empty hooks dir) is now built from layout.StateDirOf, and reposeed.RecordPath takes the state name, which ResolveRepoPhase derives from layout.CommonDirOf + layout.StateName. With no <common>/dckt directory every path is byte-identical to before. The new repoguard TestNoInlineStateFolderSpelling rejects any filepath.Join/path.Join with a literal "docket" argument outside the layout package and the two machine-path files (mutation-tested by re-inlining "docket" in workspacesRoot). The workspace import boundary admits the stdlib-only internal/layout leaf. Default-tag tests prove gatedrive.OpenStore, workspacesRoot, transactionsRoot and RecordPath follow a <common>/dckt folder. The git-resolved sites (run tracker, suiterunner, hooks) are proven by Task 10's private-lifecycle walk.
… StatusPin The operational loader resolves the repository's layout right after discovery (resolveLayout: shared reads no origin URL; private locates the store from origin's URL and the data home) and carries it on StatusPin.Layout. The metadata pin fetch, the sweep session's fetch, all 22 metadata transaction.Request sites (Remote/TargetRef via metadataRemote/ metadataRef of the pinned layout), the link context, the implementation context's metadata ref, and the human status line now read it. reposetup.MetadataBranchName is deleted. Repository-family sites alias layout.SharedName with a Task 4 marker until Task 4 threads sc.layout. render.LinkContext gains PrivateMetadata: a private metadata branch has no web page, so its blob links render repo-relative. The status JSON keeps omitting metadata_branch (the 0363 protocol guard TestStatusContextProtocolOmitsModeFields); StatusContext.MetadataBranch is json:"-" and feeds only the human line. Guards: TestTransactionRequestsUseResolvedMetadataRemote (new) and TestNoIntegrationPushOutsidePRMerge shape 1 (now keyed on metadataRef calls) were mutation-tested together with the wiring test TestIntegrationRecordOpsClaimTransactionTargetsPinnedMetadataLayout.
…hrough the layout The repository command family (check, init, prepare, repair, migrate) and the shared fact gatherer now read the metadata remote, branch ref, and worktree path from setupContext.layout, resolved from state by gatherSetupFacts right after Discover (a resolution failure is an external failure). loadOperationalContext hands its pinned layout to gatherRepoFacts. - delete docketWorktreeName; docketWorktreeFact takes the worktree path - reposetup.Facts gains MetadataWorktreeRef and Private; every worktree finding names the resolved ref (.docket when shared) - InterruptedFastForwardMessage/Remedy become functions of the worktree - PrepareContext gains metadata_remote and metadata_tracking_ref - publishOrAdoptMetadataRoot and publishSeed take the metadata remote - prepareAttachFresh creates the worktree's parent first - integration-branch work (migrate prune, primary sync) stays on origin
….yml repository layer
…ocal bare remote with neutral naming `docket repository init` decides the mode with decideInitMode: a fresh repository takes the effective `visibility` value, overridden by --private or --shared; an already-set-up repository keeps its mode and a switching flag refuses. A private init writes the `# dckt:` block into .git/info/exclude and .git/dckt/config.yml, creates the bare store (or uses --metadata-remote), adds the dckt remote (refusing a different existing URL), publishes or adopts the dckt branch, and attaches this clone's checkout under the store. Nothing is written into the working tree and nothing reaches origin. Every failure after the config write names the re-run recovery, and a re-run whose dckt remote was never added reads the metadata branch as absent so it can finish. The generated test policy writes through repoConfigTarget (private: .git/dckt/config.yml, not a pending path). Migrate refuses a private repository. resolveLayout spells the data home symlink-canonical, so a re-run recognizes its own checkout registration.
…mismatch and orphaned-checkout findings
…ata remote explicitly
…ms, resolver mutations TestIntegrationWorkflowLifecyclePrivateInitToImplemented drives a private repository from init --private through create, groom, claim, plan, build, PR and mark-implemented against the dckt store, then proves origin gained only the feature branch, the only ignored entry is .worktrees/, git branch and git remote show dckt, and a walk of the whole clone (.git included) finds no docket-named path. TestRaceIntegrationAppConcurrencyPrivateClaims OneAppliesOneContends races two clones' keyed claims of one change: one applies, the other contends, and nothing reaches origin. The race exposed a product bug, shared mode too: claimRequestID was claim-<id>-<revision> for every claim while the digest also carried the run-context hash. Two dispatches with different run contexts shared the request id, so the loser's retry after its lost lease found the winner's receipt under a different digest and failed request-id-reused (invalid-input) instead of reporting `contended`. A keyed claim's request id now carries a 16-hex prefix of the hashed run context. The loser misses the winner's receipt and the exact-revision expectation reports it contended. An identical keyed retry still replays as already-claimed, and an ungated claim keeps the bare claim-<id>-<revision> id, so ungated replays and existing receipts are unchanged. The digest still binds the full hash, so a prefix collision falls back to the request-id-reused refusal and can never replay another context's receipt. TestIntegrationWorkflowRepoClaimForeignRunContextSameRevisionContends is the deterministic regression; TestClaimRequestIDIdentity pins the id's shape. Resolver mutations (each reddens the lifecycle test): MetadataRemote -> SharedRemote fails init on the remote-URL conflict; MetadataBranch -> SharedName loses the dckt branch on the store; StateName -> SharedName leaves .git/docket in the walk; MetadataWorktree -> <primary>/.docket shows ?? .docket/ and .docket in the walk. Budgets, worst serial reading on a quiet machine: workflowlifecycle 46.16s (row 35 -> 55; 34.82s without the new 7.8s private test), concurrency 35.56s (35 -> 45), reposetup 27.73s (15 -> 35; Tasks 7-9's private tests), workflow 26.36s (row 35 kept, 8.64s margin). Raised per the plan's rule; the prefixes the plan fixes leave no non-overlapping sibling shard to split into.
… instead of .docket/ and origin/docket Adds TestSkillsSpellNoMetadataLiterals, ratchets the touched skill budgets, repoints the auto-groom draft pin, and notes the private config layer in the .docket.example.yml layer header.
… instead of republishing A re-run inside the same second rebuilt a byte-identical init root, and the create-only push reported the up-to-date ref as applied, so a finished private init flaked to applied instead of no-op. Private init now probes the store first and adopts a branch it already holds through the shared ownership verifier (extracted as adoptPublishedMetadataRoot), publishing only when the branch is absent. The re-run test pins commit dates to cover both the identical-root and the different-root case deterministically, and a new test proves a foreign store branch still refuses untouched. Also moves operational_config_sources_test.go onto testsupport.TempDir, which the real-process TempDir repoguard requires.
…kout instead of wedging Review finding (blocker): deleting a private clone and re-cloning it at the same path left the repository stuck. The clone id depends only on the path, so the re-clone resolved the old checkout under the surviving store, whose .git file names a gitdir the new clone no longer has. The fact gatherer read it as foreign, init's `git worktree add` failed on the non-empty directory (and its "re-run init" recovery failed the same way), and the orphan scan skips this clone's own path, so repair never pruned it. This clone's own checkout is now stale when its .git file names a gitdir that no longer exists (staleOwnCheckout). The gatherer reads a stale checkout as absent, and private init and prepare's attach remove it (re-verified on the copy they act on; a live, absent, or unreadable checkout is never touched) before attaching. TestIntegrationRepoSetupPrivateRecloneSamePathReattaches covers init --private, delete, same-path re-clone, init, prepare, plus prepare over a checkout that lost its gitdir. Each of the three sites was mutation-tested: removing any one reddens the test.
…remote dckt remote Review finding: runPrivateInit fell back to the default bare store whenever no --metadata-remote flag was given, so re-running init on a repository set up with --metadata-remote created a stray empty default store and then refused the healthy repository with a false dckt-remote conflict. A repository that already detected private now keeps its configured dckt remote URL, and only a missing remote creates the default store.
…re any write Review finding: runPrivateInit never checked for <primary>/.docket.local.yml, so it wrote .git/dckt/config.yml, the store, remote, branch, and checkout, and only then failed with ConflictingLocalConfigError — after which every operation, the recommended init re-run included, refused. Init now refuses up front with the same both-paths unsupported-config refusal, writing nothing.
…ng migrate Review finding: conditionFinding's missing-attachment remedies told a private repository (e.g. a moved clone) to run `docket repository migrate`, which refuses every private repository, and repairApplied's local-sync note named `.docket` even when the metadata worktree is the private checkout. Private repositories now name `docket repository prepare` (it attaches an absent private checkout and its local branch, idempotently), and the repair note names the resolved metadata worktree. Shared text is byte-identical.
…different origin Review finding: layout.OwnerRepo collapses distinct origins (acme/web-app vs acme-web/app, or one owner/repo on two hosts) onto one <owner>-<repo> store name, and adoptPublishedMetadataRoot accepted that store's init root from any repository, so an unrelated repository silently adopted another's backlog. The default store now records the creating repository's origin URL in origin-url beside remote.git. Private init refuses (invalid-state/conflict, before creating the store, adding the dckt remote, or attaching a checkout) when the recorded origin names a different repository, and points at --metadata-remote. A store with no record is adopted and gains one. A --metadata-remote URL is not checked.
…tadata branch Review fixes for change 0531: - F7: a local-path --metadata-remote is made absolute and clean before AddRemote and the existing-remote comparison (URLs and scp-style host:path stay as typed); unit test TestAbsMetadataRemote. - F8: skills say "the metadata branch" instead of "the `docket` branch"; the convention's branch model names `docket` once for shared mode. Assets regenerated; convention word budget 7155 -> 7160.
danielhanold
deleted the
feat/private-visibility-keep-the-metadata-branch-on-a-local-remot
branch
October 6, 2026 13:40
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.
Adds private visibility. A repository set up with
docket repository init --privatekeeps docket's single metadata layout, with these differences:dcktand is pushed to a bare repository on the user's machine.--metadata-remotecan point it elsewhere. It never goes toorigin..git/dckt/..git/info/exclude.Shared repositories behave as before.
Main pieces:
internal/layoutresolves four values that used to be hard-coded (metadata remote, metadata branch, state folder, metadata worktree) from repository state. Every metadata call site goes through them, and new repoguard guards stop hard-coded spellings from coming back.visibilityconfig key, and.git/dckt/config.ymlas the repository config layer in private repositories.init --private.check,prepareandrepair, with newvisibility-mismatchandorphaned-checkoutfindings.metadata_worktree_path,metadata_remote,metadata_tracking_ref).contendedinstead ofinvalid-input.ADRs: 0142 (private visibility layout), 0143 (claim request identity).
Runtime budgets were raised for the workflowlifecycle (35→55), concurrency (35→45) and reposetup (15→35) shards. Please confirm the raise, or ask for a split.
Review (deep tier) — dispositions
--metadata-remoterefused with a false conflict and created a stray store.docket.local.ymlexistsmigrate, which refuses in private repositories--metadata-remotewas stored as typeddocketbranch"