Repository navigation
File and read GitHub issues in resumable REPL entries - #926
Conversation
An entry files an issue in its enclosing `<IssueTracker>` and reads one back, wherever the REPL was started: no repository is discovered for it, no managed root is made and no hold is taken. Both operations are retained, so leaving at a question and reopening the printed location answers from the records rather than asking the tracker again. The durable envelope moves to `issue/owned.ts`, shared by the two profiles that retain Issue work. It takes the live operation as a callback, which is what lets a workflow run keep calling `IssueApi` directly while an entry composes `IssueOperations` over a transport-facing default of its own — one envelope per operation either way. The workflow wrapper keeps its identity derivation, its inherited-prefix borrowing and its record encodings byte for byte. An entry's issue work is named by the owner the session derives from the execution and the entry key, and by the identity canonical core minted where it selected this implementation. Neither reaches the component through a prop, a tracker, a Context or a middleware answer, and the entry's `<Issue>` is an ordinary shadowable registration a repository component still outranks.
, 6/6) E1 had no production row where public middleware *answers* a read: the one there delegated, so a replay reaching the read chain was undiscriminated. A read answered by the chain without a transport is now retained and the partial entry is reopened with both IssueOperations and IssueApi set to fail if either is entered. E2 proved two authored sites independent but never one site evaluated twice. A bounded `<Each>` of two iterations writes the same text both times, so an identity derived from the element or from what it says would collapse them. E6 observed a successor refusal but not that leaving *waits*. An entry-owned finalizer registered around the live dispatch is held open through the cancellation: leaving stays unfinished while it is held and joins when it is released. The cancelled-append row gains a suffix sentinel and the absence of a root close.
| // The issue capability a REPL entry runs with, selected on the same terms | ||
| // and for the same reason: the adapter is this runtime's, and the entry that | ||
| // owns one is chosen by the session rather than by the host. Node and Bun | ||
| // select none, so `<Issue>` there reaches its missing-provider refusal. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // select none, so `<Issue>` there reaches its missing-provider refusal. |
| // The issue capability a REPL entry runs with, selected on the same terms | ||
| // and for the same reason: the adapter is this runtime's, and the entry that | ||
| // owns one is chosen by the session rather than by the host. Node and Bun | ||
| // select none, so `<Issue>` there reaches its missing-provider refusal. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // select none, so `<Issue>` there reaches its missing-provider refusal. |
| }); | ||
|
|
||
| // The host's selection, read once in the command scope. A value read per | ||
| // entry would be a value a later installation could have changed. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // entry would be a value a later installation could have changed. |
| yield* first.type("\r"); | ||
|
|
||
| // The prefix ran: one issue was filed, and the two facts the document | ||
| // asked for are on the screen. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // asked for are on the screen. |
| expect(creates(server)).toBe(1); | ||
| const mark = server.issues[0]?.body ?? ""; | ||
|
|
||
| // And the write after the question has not happened. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // And the write after the question has not happened. |
| const mark = server.issues[0]?.body ?? ""; | ||
|
|
||
| // Left while the request is in flight: the entry is cancelled and the | ||
| // command still prints the location it ended at. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // command still prints the location it ended at. |
| const second = yield* allocate(inside(["repl", "--deny-all", location]), home); | ||
| yield* showing(second, "[exit]", REACH_MS); | ||
| // The same owner and the same site, so the same key: the adapter finds its | ||
| // own mark on the issue that is already there and adopts it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // own mark on the issue that is already there and adopts it. |
| // Validated before it is retained. A provider that answered with more | ||
| // than the shared fields answered with something this boundary will not | ||
| // carry, and reading it anyway is how a provider's own shape reaches a | ||
| // document. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // document. |
| name: yield* issueRequestFingerprint(request), | ||
| // Carried in the description so a retained history describes what it asked | ||
| // for. The identity table reads it back to recognize a record a fork | ||
| // inherited, and it holds only what a document wrote. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // inherited, and it holds only what a document wrote. |
PR #926: File and read GitHub issues in resumable REPL entries31 files, +3826 / -213 Scope🔴 PR has 4039 lines changed. Split into focused PRs. 🟡 4039 lines changed. PRs under 400 receive more thorough review. 🟡 31 files changed. Are all changes related? 🟡 Changes span 9 directories. Structural🟡 Interface declarations with no consumers: Opened.
Oxlint structural signals:
Slop
Oxlint slop signals:
Static AnalysisOxlint: 53 diagnostics across 10 files (11 rules) no-shadow (14): packages/git/src/composition/components/Issue.ts, packages/cli/src/repl/session.ts, packages/cli/src/repl/program.ts CorrectnessNo extraneous code patterns detected. |
…bpath (#181, 6/6) `module-partition.test.ts` is in none of this delivery's frozen commands, so both of its rules went unchecked until CI ran them on every runtime. A production module belongs to exactly one declared half: `issue/owned.ts` is provider-neutral and `deno/retained-repository/issues.ts` is the Deno adapter's, beside the file and selection recording it sits with. And a test reaching `../src/issue/api.ts` bypasses the export map, so every name `/api` publishes is a violation there. `IssueApi` and the five contract types now come from `@executablemd/git/api`, which is the route a consumer has.
| // The issue capability a REPL entry runs with, selected on the same terms | ||
| // and for the same reason: the adapter is this runtime's, and the entry that | ||
| // owns one is chosen by the session rather than by the host. Node and Bun | ||
| // select none, so `<Issue>` there reaches its missing-provider refusal. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // select none, so `<Issue>` there reaches its missing-provider refusal. |
| // The issue capability a REPL entry runs with, selected on the same terms | ||
| // and for the same reason: the adapter is this runtime's, and the entry that | ||
| // owns one is chosen by the session rather than by the host. Node and Bun | ||
| // select none, so `<Issue>` there reaches its missing-provider refusal. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // select none, so `<Issue>` there reaches its missing-provider refusal. |
| yield* first.type("\r"); | ||
|
|
||
| // The prefix ran: one issue was filed, and the two facts the document | ||
| // asked for are on the screen. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // asked for are on the screen. |
| const mark = server.issues[0]?.body ?? ""; | ||
|
|
||
| // Left while the request is in flight: the entry is cancelled and the | ||
| // command still prints the location it ended at. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // command still prints the location it ended at. |
| // Validated before it is retained. A provider that answered with more | ||
| // than the shared fields answered with something this boundary will not | ||
| // carry, and reading it anyway is how a provider's own shape reaches a | ||
| // document. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // document. |
, 6/6) The inert factory was bound unconditionally at the Deno and compiled entrypoints, before any command was dispatched, so `xmd run` and `xmd --help` carried a choice only `xmd repl` has any use for. A host now answers two questions rather than one. `ReplHostAssembly` pairs the selection with the installation, and the REPL command path asks for the selection where it belongs — after every refusal, before the profile that captures it — while installing stays exactly where it was. Reordering the install instead would have inverted two `Execution.around` layers, because profile assembly installs one of its own and composition order is installation order. Node and Bun supply the install half and no selection, which is the same provider absence they had: `<Issue>` in an entry there reaches its ordinary missing-provider refusal.
| useBunRepl, | ||
| // A terminal and a history, and no entry capability: this runtime names | ||
| // no native adapter, so `<Issue>` in an entry reaches its ordinary | ||
| // missing-provider refusal. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // missing-provider refusal. |
| // entry capability is a fact about `xmd repl`, so no other command line | ||
| // selects one, and a line that refuses after this has still selected | ||
| // nothing but code. It is made before the profile below, because that is | ||
| // what captures it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // what captures it. |
| () => useCompiledRepl(HELPER), | ||
| // What this host selects for a REPL command, and what it installs for | ||
| // one. The selection is the issue capability an entry runs with; Node | ||
| // and Bun supply neither half. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // and Bun supply neither half. |
| () => useDenoRepl(HELPER), | ||
| // What this host selects for a REPL command, and what it installs for | ||
| // one. The selection is the issue capability an entry runs with; Node | ||
| // and Bun supply neither half. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // and Bun supply neither half. |
| useNodeRepl, | ||
| // A terminal and a history, and no entry capability: this runtime names | ||
| // no native adapter, so `<Issue>` in an entry reaches its ordinary | ||
| // missing-provider refusal. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // missing-provider refusal. |
| const second = yield* allocate(inside(["repl", "--deny-all", location]), home); | ||
| yield* showing(second, "[exit]", REACH_MS); | ||
| // The same owner and the same site, so the same key: the adapter finds its | ||
| // own mark on the issue that is already there and adopts it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // own mark on the issue that is already there and adopts it. |
|
|
||
| yield* showing(pty, "no Issue provider is installed", ACT_MS); | ||
| // Configured and reachable, and never reached: what is absent is the host | ||
| // that would have installed a lifecycle, not the operator's configuration. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // that would have installed a lifecycle, not the operator's configuration. |
| // Validated before it is retained. A provider that answered with more | ||
| // than the shared fields answered with something this boundary will not | ||
| // carry, and reading it anyway is how a provider's own shape reaches a | ||
| // document. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // document. |
| name: yield* issueRequestFingerprint(request), | ||
| // Carried in the description so a retained history describes what it asked | ||
| // for. The identity table reads it back to recognize a record a fork | ||
| // inherited, and it holds only what a document wrote. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // inherited, and it holds only what a document wrote. |
| request: issueRequestJson(request), | ||
| // Where the authored component asking for this effect was written — | ||
| // diagnostic journal data, never part of the fingerprint, the request, or | ||
| // the identity table. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // the identity table. |
Why
xmd replresolves<Issue>but currently refuses it because no Issue provider is installed. This PR adds GitHub issue reads and upserts to Deno source and compiled REPL entries, including continuation after leaving or interrupting an entry.Delivery 6 of #181, based on the managed Repository/Worktree support merged in #925.
What changes
An entry can file an issue, read its details and display the result from inside or outside a checkout:
When an entry leaves at a question and resumes from the printed location, completed upserts restore their URL and completed reads restore their saved details. Replay reaches neither middleware nor the tracker, even if the remote issue has since changed.
If the tracker accepted an upsert before its result reached the journal, continuation presents the original reconciliation key and adopts the existing issue. An interrupted read may repeat safely. Leaving cancels and joins entry-owned work; a failed or cancelled result append acknowledges no success and prevents suffix effects.
How it works
The owner and occurrence identity remain private to trusted code. Live middleware can answer, transform, refuse or delegate without changing the operation's identity. Child content renders before the parent's envelope, preserving nested journal order.
The shared worker also serves workflow Issues, preserving their record encodings and inherited-prefix borrowing. The entry's Issue registration is ordinarily shadowable; repository-local components retain precedence. Canonical form dispatch selects read versus upsert.
Review guide
packages/git/src/issue/owned.tsandissue/effect.ts: the shared envelope and workflow compatibility.packages/git/src/deno/retained-repository/issues.ts: claimant, admission, live composition and private key binding.packages/cli/src/{deno,compiled}.ts,repl-profile.tsandrepl/session.ts: factory capture before command assembly and installation for the exact entry.packages/cli/tests/repl-issues.test.ts: middleware/replay traps, repeated-site identity, cancellation and append controls.How to verify it
Focused evidence reviewed at
1dc2ac59d040ab8ba2fd84281051f5377644cd01:deno task builddist/xmdThe REPL suites were rerun for the final test-only successor; the other four results cover unchanged boundaries. Lint and typechecking also passed.
The tests distinguish:
IssueOperationsandIssueApiset to throw during completed replay.The compiled journey runs in the existing
smokejob. CI status is separate from this focused feedback evidence.Scope and limitations
Issue work needs no Git discovery, checkout lease or managed root. Host installation reads no configuration or credentials; new transport work uses the existing GitHub configuration, permission ceiling and secret gate.
Node and Bun continue to report an absent Issue provider; portable terminal tests verify that they send no requests. Local Git, Push and PullRequest support are outside this delivery. Managed selection from #925 and ordinary/workflow Issue contracts are preserved.
Provider reconciliation retains its existing refusal behavior for ambiguous or unprovable remote outcomes. The change introduces no new provider, dependency, record format or reconciliation state machine.
New abstractions
readOwnedIssue/upsertOwnedIssueshare durable execution between workflow and REPL profiles while each supplies its live operation.ReplEntryInstallationcarries an inert factory because the session supplies ownership separately for each entry.