Skip to content

✨ Resume REPL entries in managed repositories and worktrees - #925

Merged
taras merged 4 commits into
mainfrom
agent/repl-managed-selection-pr2
Oct 16, 2026
Merged

taras merged 4 commits into
mainfrom
agent/repl-managed-selection-pr2

Conversation

@taras

@taras taras commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

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 repl refused <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>.

<Repository name="project" url="/absolute/path/to/remote.git">
  <Worktree name="experiment" branch="experiment" as="worktree" />
  <Dir path={worktree}>
    <File path="note.md">first</File>
    <File path="note.md" as="held" />
    <Elicit schema={{ type: "string", enum: ["continue"] }} as="answer">Recorded value: {held}</Elicit>
    <File path="after.md">continued</File>
  </Dir>
</Repository>

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

  1. placement-records.ts and admission.ts under packages/git/src/retained-repository/: the closed placement format and history validation before document work.
  2. selections.ts, provider.ts and files.ts under packages/git/src/deno/retained-repository/: selector recording, entry ownership and effective-path routing.
  3. run-composition/checkouts.ts and composition/host.ts: cleanup registered before native acquisition, with pending acquisitions joined during teardown.
  4. CLI REPL assembly, source/compiled journeys and the updated REPL specifications.

What must stay true

  • Replay consumes recorded answers before path resolution or acquisition. Removal and call-count regressions catch accidental revalidation, recreation or repeated ensures.
  • One entry owns a managed slot. Cancellation must preserve the active holder, and restored placements must lock their recorded root. Queue cancellation, redirected-root and absolute-Dir contention regressions exercise these boundaries with independent lock controls.
  • Cleanup finishes before acknowledgement. Authentication ordering and running clone/worktree-add interruption tests verify publication and joined teardown; hand-released filesystem gates verify pending metadata-open and isolation-directory ownership.

How to verify it

Local feedback evidence on 79e42f3e:

Coverage Result
Selection, records, File/Dir lifecycle and structured concurrency (F1) 7 suites, 87 steps
REPL assembly, entries, history and permissions (F2) 28 suites, 112 steps
Ordinary managed/ambient selection and isolated children (F3) 21 suites, 85 steps
Workflow replay, module partition and REPL boundaries (F4) 11 suites, 51 steps
Real-terminal source journeys (F5) 3 passed
Build, then freshly compiled journeys (F5) Build exited 0; 3 passed
Portable REPL/profile tests under Node and Bun (F6) 12 passed each
Lint and typecheck 0 errors

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.ts

The 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

  • A versioned placement codec serves recorded selectors and history admission; private registrations route subsequent File/Dir work.
  • A local slot queue serializes live selectors without sharing their journal positions. A private error wrapper carries slot contention through the existing fatal path without recording it as a completed outcome.
  • Three package-private observation/acquisition seams make the assigned cancellation tests deterministic. Production defaults and public APIs are unchanged.

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

  • Every changed file supports managed REPL selection and its retained File/Dir work.
  • Unrelated cleanup and generated changes are excluded.
  • The description matches the final diff and reported verification.

taras added 3 commits October 16, 2026 08:17
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.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 9 redundant comments. Inline suggestions to remove them below.

// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// 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".

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// "second".


const registration = yield* recordedPlacement(entry, recorded, {
slot: () => undefined,
// Nothing is held for it: this is the checkout the person was already in.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// what it has.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

PR #925: ✨ Resume REPL entries in managed repositories and worktrees

28 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.

Structural

Oxlint structural signals:

  • no-empty-function ×1: packages/git/src/deno/retained-repository/selections.ts

Slop

  • packages/cli/tests/support/repl-files-journey.ts:496 — // "second".
  • packages/cli/tests/support/repl-files-journey.ts:560 — // Nothing was placed: no managed layout was created at all.
  • packages/git/src/deno/retained-repository/provider.ts:195 — // entry.
  • packages/git/src/deno/retained-repository/provider.ts:206 — // them.
  • packages/git/src/deno/retained-repository/selections.ts:315 — // Nothing is held for it: this is the checkout the person was already in.
  • packages/git/src/deno/retained-repository/selections.ts:333 — // Nothing placed it: this is the checkout the person was already in.
  • packages/git/src/deno/retained-repository/selections.ts:517 — // existing actionable refusal rather than an outcome worth writing down.

Static Analysis

Oxlint: 9 diagnostics across 6 files (6 rules)
Density: 0.002 violations/added-line

no-shadow (3): packages/git/src/deno/retained-repository/provider.ts, packages/git/src/deno/composition/host.ts
unbound-method (2): packages/git/src/deno/retained-repository/selections.ts, packages/cli/src/deno-repositories.ts
no-useless-fallback-in-spread (1): packages/git/src/deno/composition/host.ts
no-empty-function (1): packages/git/src/deno/retained-repository/selections.ts
no-array-sort (1): packages/cli/tests/support/repl-files-journey.ts
no-unsafe-type-assertion (1): packages/cli/src/deno.ts

Correctness

No extraneous code patterns detected.

@taras taras changed the title ✨ Select managed repositories and worktrees in the REPL (#181, 2/6) ✨ Resume REPL entries in managed repositories and worktrees Oct 16, 2026
…, 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.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 7 redundant comments. Inline suggestions to remove them below.

// 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".

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// "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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// them.


const registration = yield* recordedPlacement(entry, recorded, {
slot: () => undefined,
// Nothing is held for it: this is the checkout the person was already in.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// existing actionable refusal rather than an outcome worth writing down.

@taras
taras marked this pull request as ready for review October 16, 2026 16:54
@taras
taras merged commit 863225f into main Oct 16, 2026
44 checks passed
@taras
taras deleted the agent/repl-managed-selection-pr2 branch October 16, 2026 16:54
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