Repository navigation
✨ Resume REPL entries in managed repositories and worktrees - #925
Conversation
A REPL entry can now write `<Repository name=".." url={..}>` and
`<Worktree name=".." branch={..}>`, work in the checkout it selected, leave at
a question and continue from what it recorded. The selector is the ordinary
managed one `xmd run` already uses, with its own creation, compatible-reuse,
conflict and interrupted-adoption policy; what this adds is a small routing
record of what the selector answered.
A completed selection is answered from that record: no selector call, no clone,
no `worktree add`, no sidecar read, no credential and no lock. It says where the
entry was working and nothing about what the checkout now holds, so continuing
works after somebody has edited or deleted it, while genuinely new work meets
today's filesystem and gets today's answer.
Two holds answer two questions. One entry at a time *owns* a managed slot, held
until teardown and reacquired lazily by the first new operation inside a
restored placement; separately, one entry at a time *changes* one repository,
which a new `<Worktree>` selector and a new mutation take and a read never
does.
Files routing gains managed coverage beside PR 1's ambient route, most specific
first, with the managed checkout root itself covered so a self-closing
`<Worktree>` and a sibling `<Dir>` are retained. Two bounded ownership
corrections arm cleanup before the native acquisition it releases:
`writeMetadata`'s exclusive sibling and `denoRepositoryHost.useDirectory`'s
temporary directory.
Local Git, publication and issues remain unavailable in an entry, generated
`<Repository>`, `<Worktree>` and `<Dir>` still refuse preflight, and nothing
inventories, snapshots, restores or deletes a managed checkout.
…ent admission (#181, 2/6) Four defects the Planner reproduced against c8ea831, each with the regression that reproduces it: A waiting slot turn that was cancelled resolved its own completion, which was all its successor waited for — so halting a waiter let the one behind it run a selector beside the active holder. The turn is now a queue with one holder: a waiter cancelled before it is granted the turn removes itself and signals nothing, and one granted the turn and cancelled before it resumed hands the turn on rather than stranding it. The selector ran without a scope of its own, so the authentication a clone opened was still being torn down after the placement had been appended and the body had begun writing. The selector now runs scoped, and that scope is joined before its answer becomes a record; a cleanup that fails takes the ordinary fatal path and publishes nothing. The Git session, its isolation directory and both holds stay entry-owned. A restored placement's slot lock was taken under today's configured root, while the slot itself belongs to the root the placement recorded — two different sidecars, so an ordinary owner of that placement excluded nobody. The recorded root now travels through private acquisition state, with one `Leases` per root and the root in the single-flight key. New selections keep ordinary current-root policy and completed replay still acquires nothing. An ambient placement's `managedRoot` was read through a parser that answers `undefined` for anything invalid, so a non-null value that is not a root passed as the required absence. Absence is now exactly `null`, and anything else is malformed before any document work. E5's assigned cuts are now real. A clone and a `worktree add` are each interrupted while their child is genuinely running and holding a descendant open, and both are joined before the entry finishes; what the `worktree add` wrote is accounted for by the ordinary interrupted-creation policy. The metadata open and the isolation-directory creation are each cut while still pending, and teardown waits for delivery before closing or removing what arrives. Three narrow package-private seams carry those cuts: `observe` forwarded to the one place a child exists, the metadata opener, and the host's directory creator.
…tory (#181, 2/6) `targetOf` resolved every request as `join(cwd, path)`, so the one spelling that hands a `<Dir>` an absolute path — a self-closing `<Repository>` or `<Worktree>` captured with `as`, rendered at by a sibling `<Dir>` — was routed as `/ambient/managed/…/checkout` rather than `/managed/…/checkout`. That is inside no registered placement, and two contracts followed it out: A completed ensure was never retained, so continuing an entry ran it again and recreated a checkout the person had removed. A new ensure took no ownership of the checkout it was making a directory in, so it proceeded while another entry held that managed slot. An absolute path names a place on its own and the working directory is no part of where it points, which is what the ordinary operation beneath has always done with it. A relative path keeps the existing canonical-cwd resolution, and with it the reason that resolution exists. Two regressions, each confirmed to fail without the correction: a captured Repository path entered by `<Dir>`, left at a question, replaying over a removed checkout without an ensure or a native command; and an absolute `<Dir>` beneath a restored root Worktree refusing before the ensure, the directory, the body or any local result while another owner holds that slot, with the independent acquisition as its positive control.
| // REPL host owns, because only a runtime-named module may name the native | ||
| // adapter and this runtime's own credential-helper assembly — and registering | ||
| // it acquires nothing, so a session that only reads history discovers no | ||
| // checkout, creates no managed root and takes no lease. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // checkout, creates no managed root and takes no lease. |
| // REPL host owns, because only a runtime-named module may name the native | ||
| // adapter and this runtime's own credential-helper assembly — and registering | ||
| // it acquires nothing, so a session that only reads history discovers no | ||
| // checkout, creates no managed root and takes no lease. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // checkout, creates no managed root and takes no lease. |
| yield* useHostFiles(); | ||
| yield* useDenoRepl(); | ||
| // The assembly this host selects, stated here as an entrypoint states it. | ||
| // Inert: this fixture selects no repository, so nothing ever reaches it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Inert: this fixture selects no repository, so nothing ever reaches it. |
| ].join("\n"); | ||
| } | ||
| // The self-closing spelling: it binds the checkout path, and a `<Dir>` | ||
| // sibling renders its descendants at exactly that path. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // sibling renders its descendants at exactly that path. |
| yield* sleep(500); | ||
| yield* first.type("\r"); | ||
|
|
||
| // The checkout the selection created, found under the managed layout. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // The checkout the selection created, found under the managed layout. |
| // No completed operation ran again — the selection included. A reselected | ||
| // placement would have reached a selector; a replayed write would have | ||
| // overwritten the shell's edit, and a replayed read would have bound | ||
| // "second". |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // "second". |
|
|
||
| const registration = yield* recordedPlacement(entry, recorded, { | ||
| slot: () => undefined, | ||
| // Nothing is held for it: this is the checkout the person was already in. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Nothing is held for it: this is the checkout the person was already in. |
| }); | ||
| return Object.freeze({ | ||
| kind: "ambient-owner" as const, | ||
| // Nothing placed it: this is the checkout the person was already in. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Nothing placed it: this is the checkout the person was already in. |
| release(key, here); | ||
| } else { | ||
| // Never granted: there is nothing to release, and the holder keeps | ||
| // what it has. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // what it has. |
PR #925: ✨ Resume REPL entries in managed repositories and worktrees28 files, +4820 / -158 Scope🔴 PR has 4978 lines changed. Split into focused PRs. 🟡 4978 lines changed. PRs under 400 receive more thorough review. 🟡 28 files changed. Are all changes related? 🟡 Changes span 8 directories. StructuralOxlint structural signals:
Slop
Static AnalysisOxlint: 9 diagnostics across 6 files (6 rules) no-shadow (3): packages/git/src/deno/retained-repository/provider.ts, packages/git/src/deno/composition/host.ts CorrectnessNo extraneous code patterns detected. |
…, 2/6) The managed selection suite clones and adds worktrees with real `git` children and holds their slots under a kernel-released advisory lock, so Node and Bun have nothing to run it with and every row timed out waiting for an entry that never reached its question. Declare it Deno-only, beside the sibling suites that already are. The placement record and its admission stay portable, and a runtime without the provider still proves it refuses a managed selector before a clone.
| // No completed operation ran again — the selection included. A reselected | ||
| // placement would have reached a selector; a replayed write would have | ||
| // overwritten the shell's edit, and a replayed read would have bound | ||
| // "second". |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // "second". |
| yield* pty.type("\r"); | ||
|
|
||
| yield* showing(pty, "no Repository composition provider is installed", ACT_MS); | ||
| // Nothing was placed: no managed layout was created at all. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Nothing was placed: no managed layout was created at all. |
| // A recorder with no managed root records file work and installs no selector, | ||
| // so `<Repository>` and `<Worktree>` reach their ordinary missing-provider | ||
| // refusal. Nothing below this line is reached, named or acquired by such an | ||
| // entry. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // entry. |
| // itself, so a root canonicalized before it was made would be one spelling on | ||
| // the entry that created it and another on every entry afterwards — two | ||
| // spellings, two digests, two slots for one Repository, and no lock between | ||
| // them. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // them. |
|
|
||
| const registration = yield* recordedPlacement(entry, recorded, { | ||
| slot: () => undefined, | ||
| // Nothing is held for it: this is the checkout the person was already in. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Nothing is held for it: this is the checkout the person was already in. |
| }); | ||
| return Object.freeze({ | ||
| kind: "ambient-owner" as const, | ||
| // Nothing placed it: this is the checkout the person was already in. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Nothing placed it: this is the checkout the person was already in. |
| ): ReplPlacementRefusalData | undefined { | ||
| if (request.kind === "ambient-owner") { | ||
| // Discovery answers with a repository or with nothing, and nothing is the | ||
| // existing actionable refusal rather than an outcome worth writing down. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // existing actionable refusal rather than an outcome worth writing down. |
Why
Let a Deno REPL entry select a managed repository or worktree, leave at a question, and resume with its recorded selections and file results. Previously,
xmd replrefused<Repository>and<Worktree>and retained only file work in the checkout where the entry started.Part 2 of #181, following the ambient File/Dir recording in #902.
What changes
<Repository>creates or reuses a checkout beneath~/.xmd/repositories.<Worktree>selects a linked checkout of the enclosing Repository, or of the ambient repository when used at the entry root. Paired forms scope their content; self-closing forms bind a checkout path for a later<Dir>.Leave at the question and reopen the entry: completed selections, File reads/writes/deletes and Dir ensures answer from their records. The write after the question runs when you answer. Completed replay performs no native selection or filesystem work, even if the checkout changed or disappeared. New operations encounter today's filesystem; a new entry selects the checkout afresh.
How it works
The source and compiled Deno REPL hosts install recording during entry initialization, using a snapshot of the selected helper configuration. Parsed placement records rebuild private routing for File/Dir operations, including absolute captured checkout paths. Passive history views acquire no provider resources.
Two leases have distinct jobs: the managed-slot lease owns a checkout for the entry, while the common-Git-directory lease serializes mutations across linked checkouts. Completed replay takes neither. The first new operation in a restored placement reacquires its recorded slot; mutations also take the common-directory lease. A selector's authentication cleanup finishes before its placement is recorded.
Review guide
placement-records.tsandadmission.tsunderpackages/git/src/retained-repository/: the closed placement format and history validation before document work.selections.ts,provider.tsandfiles.tsunderpackages/git/src/deno/retained-repository/: selector recording, entry ownership and effective-path routing.run-composition/checkouts.tsandcomposition/host.ts: cleanup registered before native acquisition, with pending acquisitions joined during teardown.What must stay true
How to verify it
Local feedback evidence on
79e42f3e:Run the focused selection/lifecycle evidence with:
deno task test packages/git/tests/retained-repository-{selection,records,local,lifecycle}.test.ts packages/durable-streams/tests/structured-concurrency.test.tsThe added absolute-Dir regressions prove that a completed ensure does not recreate a removed checkout, and that a new ensure refuses under slot contention before creation, body work or result append. Other rows cover coroutine-local replay, malformed-record admission and append cancellation/failure. Real-terminal journeys exercise leave/edit/reopen through both entrypoints.
Scope
Includes managed selection recording, File/Dir coverage, lease and resource ownership, Deno runtime assembly, and matching specifications. Ordinary selector reuse/refusal policy and Workflow encodings stay unchanged. Local Git, Push, pull requests and Issues remain later deliveries; Glob remains ordinary and unrecorded. Node and Bun retain ordinary File/Dir behavior and refuse managed selection before native work. Generated-component permissions are unchanged.
New abstractions
Risks and limitations
Replayed placements describe historical selections, not current checkout contents. Replay does not restore a checkout; new reads may fail and new Dir operations retain ordinary creation behavior. The clone cancellation barrier stops before destination creation; the worktree-add cut proves adoption of actual partial state. This PR's feedback evidence is separate from delivery verification.
Scope confirmation