Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4123fb1
✨ Admit a REPL entry's retained repository history
taras Oct 9, 2026
7f42af7
🐛 Hold a retained repository record to the request and state beside it
taras Oct 9, 2026
381c01c
🐛 Let a completion's own result say whether anything changed
taras Oct 9, 2026
a47ccca
🐛 Recognize a retained refusal instead of inferring one from disagree…
taras Oct 9, 2026
1f050c1
🐛 Hold a Files refusal to the operation it is standing for
taras Oct 9, 2026
fe710ec
🚧 Slice B checkpoint: core's private file-mutation delivery
taras Oct 9, 2026
72fcdb4
✨ Deliver a canonical File mutation's own occurrence to the host
taras Oct 9, 2026
29cb118
🐛 Give one mutation one entry, one validated request and its own rece…
taras Oct 9, 2026
6f509bd
🚧 B2 checkpoint: the frontier inventory and the retained local protocol
taras Oct 9, 2026
e9c16fd
🚧 B2 checkpoint: retained file work proven against real Git
taras Oct 10, 2026
0668e6f
🚧 B2 checkpoint: the Dir and Git.Add claimant adapters
taras Oct 10, 2026
629e537
🚧 B2 checkpoint: the Git.Commit adapter, with its object id predicted
taras Oct 10, 2026
da504df
🚧 B2 checkpoint: the Git.Switch adapter and scratch prediction
taras Oct 10, 2026
31237dd
🚧 B2 checkpoint: all six claimant adapters, with Repository and Worktree
taras Oct 10, 2026
d47d41d
🚧 B2 checkpoint: lifecycle and generated-file coverage, complete grou…
taras Oct 10, 2026
bff7a1a
🚧 B2 checkpoint: the absent-destination record, and a partition regre…
taras Oct 10, 2026
e6e504c
✨ Fence a checkout creation behind its own acknowledged intent
taras Oct 10, 2026
3103e03
🐛 Hold a retained entry to the state its own history accounts for
taras Oct 10, 2026
fcb0a57
🐛 Settle what an entry knows about its checkouts, and what it cannot …
taras Oct 10, 2026
cb9147c
🐛 Authenticate a managed checkout where it is selected, and read a cr…
taras Oct 10, 2026
9d23475
🐛 Ask a managed selection where its checkout is, even when it is nowhere
taras Oct 10, 2026
b80d75a
✨ Let one entry publish exactly once, and ask again about nothing
taras Oct 10, 2026
bde0596
🐛 Hold an entry's receipts and observations to the entry they belong to
taras Oct 10, 2026
25c2d81
✨ Give every REPL entry a repository host of its own
taras Oct 10, 2026
1440ba3
✨ Let a frozen view read, and prove the whole journey at a terminal
taras Oct 11, 2026
05a9fc5
✨ Give a placement one identity, and leave at every boundary
taras Oct 11, 2026
984c636
♻️ Replace REPL recovery with one ordinary durable result (#181)
taras Oct 11, 2026
402ab3c
🐛 Read both branches of a saved file outcome (#181)
taras Oct 11, 2026
6ee9e2f
🎨 Infer the types the correction asserted (#181)
taras Oct 11, 2026
ddbb51c
🔀 Integrate current REPL history and lifecycle with entry repository …
taras Oct 11, 2026
559f446
🧪 Make retained REPL delivery fixtures portable and identity-independent
taras Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,15 @@ jobs:
- name: Smoke test repository composition with the compiled binary
run: deno run --allow-all --frozen scripts/smoke-run-composition.ts

# A retained REPL entry's repository host is assembled at a runtime-named
# entrypoint too, and reaches further than the ordinary run's: the Git
# host, the credential helper the binary re-enters as a subprocess of
# itself, and the records an interrupted entry is read back from. This
# drives the binary's own `xmd repl` on a pty, publishes out of an ambient
# checkout into a bare remote, leaves, and reopens the location it printed.
- name: Smoke test a REPL entry's repository work with the compiled binary
run: deno test --allow-all --frozen scripts/tests/repl-repositories-compiled.test.ts

# `<Fetch>` resolves from core's registry, requests through the contextual
# Fetch adapter, and detaches the response before binding it. All three
# live in the module graph, so only the binary shows they survived
Expand Down
245 changes: 240 additions & 5 deletions architecture.md

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ import { testingExecutionHost } from "./testing-host.ts";
import type { ChildPlanDeclaration } from "./testing-host.ts";
import { unsupportedRepositories } from "./run-repositories.ts";
import type { RepositoryInstaller } from "./run-repositories.ts";
import type { ReplEntryHostFactory } from "./repl-repositories.ts";
import { EVAL_ALIAS, EVAL_OPTION, evalGrammarError, readEvalFlags } from "./eval-source.ts";
import type { EvalFlags } from "./eval-source.ts";
import { STANDARD_INPUT_FAILURE, STANDARD_INPUT_PATH } from "./standard-input.ts";
Expand Down Expand Up @@ -2677,6 +2678,11 @@ function* dispatch(
workflowHost: WorkflowHost | undefined,
sessions: MachineSessionAssembly | undefined,
installRepl: ReplHostInstaller | undefined,
// What a REPL entry of this host obtains repository operations through, or
// nothing on a runtime that offers none. Carried rather than called: the
// `repl` command retains it in its profile, and the entry task builds the
// host.
replEntryHost: ReplEntryHostFactory | undefined,
/**
* What this invocation's Plugins installed.
*
Expand Down Expand Up @@ -2923,7 +2929,7 @@ function* dispatch(
// vocabulary and the ceiling a generated fragment runs under. Nothing is
// assembled again per entry, so two entries of one session cannot run
// under different rules.
const profile = yield* assembleReplProfile(replStack, plugins);
const profile = yield* assembleReplProfile(replStack, plugins, {}, replEntryHost);
yield* installRepl();
const ran = yield* runReplProgram({
profile,
Expand Down Expand Up @@ -3330,6 +3336,13 @@ export function* runXmd(
// names none has no REPL, and says so rather than opening a terminal it cannot
// restore.
installRepl?: ReplHostInstaller,
// How a REPL entry of this host obtains repository operations. A factory, so
// the host belongs to the executing entry's own task rather than to the
// command: two entries of one session are two owners, and a cold view builds
// none at all. Deno and the compiled binary supply the retained one; Node and
// Bun supply none, so an entry there refuses repository work before a lock, a
// credential, a subprocess or a request exists.
replEntryHost?: ReplEntryHostFactory,
): Operation<void> {
// Before every scanner and before anything reads a path: what a command line
// selects is read from the argv the caller wrote, and the tokens that
Expand Down Expand Up @@ -3364,6 +3377,7 @@ export function* runXmd(
installWorkflowHost,
sessions,
installRepl,
replEntryHost,
);
}

Expand Down Expand Up @@ -3426,6 +3440,7 @@ function* runCommand(
installWorkflowHost: HostWorkflowInstaller,
sessions: MachineSessionAssembly | undefined,
installRepl: ReplHostInstaller | undefined,
replEntryHost: ReplEntryHostFactory | undefined,
): Operation<void> {
// First, so that no later scanner — help, properties, agent flags — can
// mistake the inline document's own text for an option.
Expand Down Expand Up @@ -3491,6 +3506,7 @@ function* runCommand(
workflowHost,
sessions,
installRepl,
replEntryHost,
plugins,
);

Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/compiled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { importPluginModule } from "./host-plugin-modules.ts";
import { compiledUpgradeAssembly } from "./compiled-upgrade.ts";
import { useMachineSessions } from "./session-coordinator.ts";
import { useDenoWorkflowHost } from "./deno-workflow.ts";
import { denoRunRepositories } from "./deno-repositories.ts";
import { denoReplEntryHost, denoRunRepositories } from "./deno-repositories.ts";
import { isCredentialHelperMode, runCredentialHelper } from "@executablemd/git/credential-helper";
import type { HelperAssembly } from "@executablemd/git/credential-helper";
import { useCompiledService } from "./compiled-service.ts";
Expand Down Expand Up @@ -94,6 +94,9 @@ if (isCredentialHelperMode(process.argv.slice(2))) {
() => useDenoWorkflowHost(HELPER),
useMachineSessions(),
useCompiledRepl,
// The same retained host the source entrypoint supplies: a compiled
// binary is the same program, and an entry it runs resumes the same way.
denoReplEntryHost(HELPER),
);
});
}
48 changes: 46 additions & 2 deletions packages/cli/src/deno-repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@
*/

import type { Operation } from "effection";
import { cwd } from "@executablemd/runtime";
import { useRunComposition } from "@executablemd/git/deno";
import { cwd, hostFilesHandler } from "@executablemd/runtime";
import {
denoRepositoryHost,
useRetainedRepositoryEntry,
useRunComposition,
} from "@executablemd/git/deno";
import type { HelperAssembly } from "@executablemd/git/credential-helper";
import { DEFAULT_REPOSITORY_ROOT } from "./run-repositories.ts";
import type { RepositoryInstaller } from "./run-repositories.ts";
import type { ReplEntryHost, ReplEntryHostFactory } from "./repl-repositories.ts";

/**
* The live provider Deno and the compiled binary install.
Expand Down Expand Up @@ -47,3 +52,42 @@ export function denoRunRepositories(
});
};
}

/**
* The retained host one REPL entry runs its repository work through.
*
* Built per entry, inside that entry's own task, which is what the factory is
* for: the owner it retains under is this execution and this admitted entry,
* and everything it acquires — a lease, a credential session, a scratch
* directory — belongs to the task that asked for it and ends with it. A cold
* view calls none of this, because nothing executes there to ask.
*
* Managed checkouts live under the same root an ordinary run uses, for the same
* reason: what is in one is somebody's work, and which entry wrote it does not
* change where it lives.
*/
export function denoReplEntryHost(
helper: HelperAssembly,
root: string = DEFAULT_REPOSITORY_ROOT,
): ReplEntryHostFactory {
return function* (request): Operation<ReplEntryHost> {
const entry = yield* useRetainedRepositoryEntry({
executionId: request.executionId,
entryKey: request.entryKey,
source: request.source,
// Where this entry started, which is where its ambient checkout is
// looked for. Read through the contextual Api rather than from the
// process, and read once: a resume takes the root its own history
// recorded instead of looking again.
cwd: yield* cwd(),
managedRoot: root,
host: denoRepositoryHost({ helper }),
// Ordinary file behavior, for everything outside a selected checkout. The
// entry's own handlers perform it directly rather than routing back
// through the public chain, which is what keeps a write beside the
// document ordinary and a write inside a checkout retained.
files: hostFilesHandler(),
});
return { installation: entry.installation, files: entry.files };
};
}
7 changes: 6 additions & 1 deletion packages/cli/src/deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { importPluginModule } from "./host-plugin-modules.ts";
import type { UpgradeAssembly } from "./upgrade.ts";
import { useMachineSessions } from "./session-coordinator.ts";
import { useDenoWorkflowHost } from "./deno-workflow.ts";
import { denoRunRepositories } from "./deno-repositories.ts";
import { denoReplEntryHost, denoRunRepositories } from "./deno-repositories.ts";
import { isCredentialHelperMode, runCredentialHelper } from "@executablemd/git/credential-helper";
import type { HelperAssembly } from "@executablemd/git/credential-helper";
import { useDenoService } from "./deno-service.ts";
Expand Down Expand Up @@ -113,6 +113,11 @@ if (isCredentialHelperMode(process.argv.slice(2))) {
() => useDenoWorkflowHost(HELPER),
useMachineSessions(),
useDenoRepl,
// One retained repository host per REPL entry, under the same managed
// root and the same credential-helper assembly the ordinary provider
// uses. A factory, so nothing is built until an entry is actually
// executing.
denoReplEntryHost(HELPER),
);
});
}
31 changes: 31 additions & 0 deletions packages/cli/src/evaluation-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,37 @@ export function ordinaryEvaluationProfile(): FragmentEvaluationInput {
};
}

/**
* The same installations, with one entry's own file access bound into the
* ceiling.
*
* The tables are untouched: what a generated fragment may name, and in which
* spelling, is the command's single decision and is settled before any entry
* exists. What changes per entry is only *which* instance the admitted
* operations reach — this entry's retained one, so a fragment's write inside a
* checkout the entry has selected is retained exactly as an authored write is.
*
* Rebound rather than appended, because one execution offers one maximum
* permission: a second profile beside the first would be two ceilings for one
* fragment, and "whichever came last wins" is how permission stops being
* auditable.
*/
export function withEntryFileAccess(
installations: readonly ExecutionInstallation[],
files: FragmentFileAccess,
): readonly ExecutionInstallation[] {
return Object.freeze(
installations.map((installation) =>
installation.evaluation === undefined
? installation
: Object.freeze({
...installation,
evaluation: Object.freeze({ ...installation.evaluation, files }),
}),
),
);
}

/**
* Whether the host that attached this execution already stated a ceiling.
*
Expand Down
16 changes: 16 additions & 0 deletions packages/cli/src/repl-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { ordinaryEvaluationProfile } from "./evaluation-profile.ts";
import { planComponentDeclaration } from "./plan-component.ts";
import { planAgentContext } from "./plan-writer-profile.ts";
import type { CommandPlugins } from "./plugin-host.ts";
import type { ReplEntryHostFactory } from "./repl-repositories.ts";

/**
* Where a REPL entry looks for components.
Expand Down Expand Up @@ -66,6 +67,16 @@ export interface ReplExecutionProfile {
readonly installations: readonly ExecutionInstallation[];
/** How this REPL answers an Agent permission request. */
readonly permissionMode: PermissionMode;
/**
* How each entry of this execution obtains repository operations.
*
* A factory, retained and never called here: the host belongs to the
* executing entry's own task, and building one at assembly would give every
* entry of the session one owner and make a cold view somebody is only
* reading acquire a checkout and a lease. Absent on a runtime that supplies
* none, which leaves repository work refusing before anything is touched.
*/
readonly entryHost?: ReplEntryHostFactory;
}

/**
Expand All @@ -85,6 +96,10 @@ export function* assembleReplProfile(
stack: AgentStack,
plugins: CommandPlugins,
seams: ReplProfileSeams = {},
// What this runtime offers an entry for repository work, from the entrypoint
// that knows whether it has one. Retained as given: it is called by the entry
// task, not here.
entryHost?: ReplEntryHostFactory,
): Operation<ReplExecutionProfile> {
yield* installAgentProviderStack(stack, seams.acp);

Expand Down Expand Up @@ -119,6 +134,7 @@ export function* assembleReplProfile(
}),
]),
permissionMode: stack.permissionMode,
...(entryHost === undefined ? {} : { entryHost }),
});
}

Expand Down
83 changes: 83 additions & 0 deletions packages/cli/src/repl-repositories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* How a REPL entry obtains repository operations, or does not.
*
* `run-repositories.ts` beside this one answers the same question for `xmd
* run`, and the two differ in exactly one way that matters here: an ordinary
* run's provider is installed per execution and remembers nothing, while an
* entry's is installed per *entry* and retains what that entry did, so an
* entry interrupted partway through repository work can be reopened and
* continue instead of repeating it.
*
* What this module holds is the contract and nothing else. A factory rather
* than a value, for the reason the ordinary installer is one and then some: the
* host belongs to the executing entry's own task, holds that entry's owner,
* selections, leases, credentials, lanes and publication permission, and ends
* with it. Constructing one while the command is still assembling itself would
* give every entry of the session the same owner and make a passive view — a
* cold history somebody is only reading — acquire a lease and a checkout.
*
* Nothing here names a runtime. The shared command path cannot import the Deno
* adapter at all: its module graph reaches `node:sqlite`, which Bun does not
* have, so a static import would stop `xmd` loading there rather than refusing
* a repository operation. The implementation lives beside the entrypoints that
* can use it, in `deno-repositories.ts`, and a host that supplies none leaves
* its entries refusing repository work before a lock, a credential, a
* subprocess or a request exists.
*/

import type { Operation } from "effection";
import type { ExecutionInstallation, FragmentFileAccess } from "@executablemd/core/host";

/** What one entry tells its host about itself. */
export interface ReplEntryHostRequest {
/**
* The execution this entry belongs to, as the host established it.
*
* Opaque, and the first half of the owner every record this entry writes is
* retained under. It is the identifier the history file is named by, so a
* location reopens the entries of that execution and of no other.
*/
readonly executionId: string;
/**
* This entry's own key, in the order the entries were admitted.
*
* Immutable once admitted, which is what makes the owner stable across
* opens: the same entry of the same execution is the same owner however many
* times it is reopened, and the entry after it is a different one however
* identical its text.
*/
readonly entryKey: string;
/** The canonical identity of the source this entry is running. */
readonly source: string;
}

/** What an entry's repository host offers the task that built it. */
export interface ReplEntryHost {
/**
* What this entry declares to its own execution.
*
* Its journal admission, its canonical file-mutation handlers and the
* repository components it answers for — installed into that one execution
* and nowhere else.
*/
readonly installation: ExecutionInstallation;
/**
* The one captured file access this entry's generated fragments run on.
*
* Bound into the command's unchanged generated ceiling, so a fragment's write
* inside a checkout this entry has selected is retained exactly as an
* authored one is — and a fragment reaches it rather than `API.Files`, which
* is what keeps a document, a repository component or middleware out from
* between them.
*/
readonly files: FragmentFileAccess;
}

/**
* How one entry's repository host is built, when this runtime has one.
*
* Called by the sequential entry task, once per entry, with the trusted
* execution identity and the admission-order key. Everything it acquires
* belongs to that task.
*/
export type ReplEntryHostFactory = (request: ReplEntryHostRequest) => Operation<ReplEntryHost>;
11 changes: 11 additions & 0 deletions packages/cli/src/repl/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ export function* runReplProgram(options: ReplProgramOptions): Operation<Result<R
includes: options.profile.includes,
installations: options.profile.installations,
permissionMode: options.profile.permissionMode,
// Carried, not called: the session hands it to each entry's own task.
...(options.profile.entryHost === undefined ? {} : { entryHost: options.profile.entryHost }),
...(state.route.at === undefined ? {} : { selection: state.route.at }),
});

Expand Down Expand Up @@ -739,6 +741,15 @@ function* drive(
} else if (built !== undefined && built.ok) {
state = adopting;
reading = candidate;
// Standing at the head means the unfinished entry below is this
// process's to continue. Said here rather than at the open, because a
// location may name a frozen position: that view continues nothing, and
// the control that comes back to the head is what asks for the work.
// Idempotent, so a frame that merely redrew the head starts nothing and
// an entry already running is left exactly as it is.
if (state.route.at === undefined) {
yield* current.resume();
}
// The readiness may have moved while that refusal was on the screen: the
// entry it named has finished, or the teardown it named has completed.
// A refusal that is no longer true is worse than no refusal, because it
Expand Down
Loading
Loading