Skip to content

File and read GitHub issues in resumable REPL entries - #926

Merged
taras merged 4 commits into
mainfrom
agent/repl-issues-pr6
Oct 16, 2026
Merged

taras merged 4 commits into
mainfrom
agent/repl-issues-pr6

Conversation

@taras

@taras taras commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Why

xmd repl resolves <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:

<IssueTracker url="https://github.com/octo/project">
  <Issue title="The REPL loses a pasted newline" as="filed">
    Pasting two lines into the draft submits the first one.
  </Issue>
  <Issue url={filed.url} as="found" />
</IssueTracker>

Filed {found.url}, titled {found.title}.

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

Deno / compiled entrypoint selects an inert factory
  -> command captures it
  -> session derives owner from execution ID + entry key
  -> core supplies the canonical Issue occurrence identity
  -> one durable envelope
       completed replay -> saved normalized result
       live attempt     -> IssueOperations middleware
                         -> IssueApi -> GitHub adapter

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

  1. packages/git/src/issue/owned.ts and issue/effect.ts: the shared envelope and workflow compatibility.
  2. packages/git/src/deno/retained-repository/issues.ts: claimant, admission, live composition and private key binding.
  3. packages/cli/src/{deno,compiled}.ts, repl-profile.ts and repl/session.ts: factory capture before command assembly and installation for the exact entry.
  4. 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:

Check Result
Issue worker, records, Markdown, GitHub and remote-composition suites 14 tests / 62 steps passed
REPL Issues, repositories and both PTY suites 5 tests / 38 steps passed
deno task build Passed
Compiled Issues journey against dist/xmd 1 test / 2 steps passed
Runtime exclusions, test discovery and workflow structure 14 tests / 64 steps passed

The 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:

  • Middleware answering from delegation, with both IssueOperations and IssueApi set to throw during completed replay.
  • Two evaluations of one authored Issue site, which must create distinct issues and retain stable identities on reopen.
  • Remote acceptance before local acknowledgement, which must adopt one issue rather than create a duplicate.
  • Entry teardown held in a finalizer, which must keep leaving pending until cleanup completes.
  • Source and compiled terminal journeys: file, read, display, leave, reopen, answer and run one suffix effect. Reopening with a rejected credential catches accidental replay traffic.

The compiled journey runs in the existing smoke job. 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 / upsertOwnedIssue share durable execution between workflow and REPL profiles while each supplies its live operation.
  • CLI-private ReplEntryInstallation carries an inert factory because the session supplies ownership separately for each entry.

taras added 2 commits October 16, 2026 14:22
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.

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

Comment thread packages/cli/src/compiled.ts Outdated
// 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.

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
// select none, so `<Issue>` there reaches its missing-provider refusal.

Comment thread packages/cli/src/deno.ts Outdated
// 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.

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

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

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

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

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

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

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

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
// inherited, and it holds only what a document wrote.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

PR #926: File and read GitHub issues in resumable REPL entries

31 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.
Symbol Declared at Refs in diff Why flagged
Opened packages/cli/tests/repl-issues.test.ts:188 1 referenced ≤1× within the added diff (pre-existing usages not counted)

Oxlint structural signals:

  • no-unused-vars ×7: packages/cli/src/repl/session.ts, packages/cli/src/cli.ts, packages/cli/src/repl/program.ts
  • no-empty-function ×2: packages/cli/src/repl-profile.ts, packages/cli/src/cli.ts
  • no-unnecessary-type-arguments ×2: packages/cli/src/repl/session.ts

Slop

  • packages/cli/src/compiled.ts:97 (removed)
  • packages/cli/src/deno.ts:116 (removed)
  • packages/cli/src/compiled.ts:97 (removed)
  • packages/cli/src/deno.ts:116 (removed)
  • packages/cli/src/bun.ts:80 — // missing-provider refusal.
  • packages/cli/src/cli.ts:2941 — // what captures it.
  • packages/cli/src/compiled.ts:103 — // and Bun supply neither half.
  • packages/cli/src/deno.ts:122 — // and Bun supply neither half.
  • packages/cli/src/node.ts:88 — // missing-provider refusal.
  • packages/cli/src/repl-profile.ts:138 — // entry would be a value a later installation could have changed.
  • packages/cli/src/repl/session.ts:973 — // the execution never admitted.
  • packages/cli/tests/support/repl-issues-journey.ts:184 — // Typed as a person types it: one paste carrying newlines, then Enter.
  • packages/cli/tests/support/repl-issues-journey.ts:190 — // asked for are on the screen.
  • packages/cli/tests/support/repl-issues-journey.ts:197 — // And the write after the question has not happened.
  • packages/cli/tests/support/repl-issues-journey.ts:226 — // issue is the one issue, with the mark it was filed under untouched.
  • packages/cli/tests/support/repl-issues-journey.ts:254 — // to observe and adopt without being held as well.
  • packages/cli/tests/support/repl-issues-journey.ts:273 — // wire. Nothing about it is retained.
  • packages/cli/tests/support/repl-issues-journey.ts:279 — // command still prints the location it ended at.
  • packages/cli/tests/support/repl-issues-journey.ts:289 — // own mark on the issue that is already there and adopts it.
  • packages/cli/tests/support/repl-issues-journey.ts:327 — // that would have installed a lifecycle, not the operator's configuration.
  • packages/git/src/issue/owned.ts:140 — // document.
  • packages/git/src/issue/owned.ts:213 — // inherited, and it holds only what a document wrote.
  • packages/git/src/issue/owned.ts:217 — // the identity table.

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts

Static Analysis

Oxlint: 53 diagnostics across 10 files (11 rules)
Density: 0.014 violations/added-line

no-shadow (14): packages/git/src/composition/components/Issue.ts, packages/cli/src/repl/session.ts, packages/cli/src/repl/program.ts
no-floating-promises (13): packages/cli/src/repl/program.ts, packages/git/tests/support/issue-tracker-server.ts, packages/cli/src/repl/session.ts (+1)
no-unused-vars (7): packages/cli/src/repl/session.ts, packages/cli/src/cli.ts, packages/cli/src/repl/program.ts
no-useless-spread (3): packages/git/src/deno/retained-repository/issues.ts, packages/git/src/issue/effect.ts, packages/cli/src/cli.ts
no-console (3): packages/cli/src/cli.ts
unbound-method (3): packages/cli/src/deno-repositories.ts, packages/cli/src/cli.ts
consistent-return (3): packages/cli/src/repl/program.ts, packages/cli/src/cli.ts
no-empty-function (2): packages/cli/src/repl-profile.ts, packages/cli/src/cli.ts
no-unsafe-type-assertion (2): packages/cli/src/deno.ts, packages/cli/src/cli.ts
no-unnecessary-type-arguments (2): packages/cli/src/repl/session.ts
no-unmodified-loop-condition (1): packages/cli/src/cli.ts

Correctness

No extraneous code patterns detected.

@taras taras changed the title ✨ File and read issues from a REPL entry (#181, 6/6) File and read GitHub issues in resumable REPL entries Oct 16, 2026
…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.

@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 5 redundant comments. Inline suggestions to remove them below.

Comment thread packages/cli/src/compiled.ts Outdated
// 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.

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
// select none, so `<Issue>` there reaches its missing-provider refusal.

Comment thread packages/cli/src/deno.ts Outdated
// 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.

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

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

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

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

@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 19 redundant comments. Inline suggestions to remove them below.

Comment thread packages/cli/src/bun.ts
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.

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
// missing-provider refusal.

Comment thread packages/cli/src/cli.ts
// 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.

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

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
// and Bun supply neither half.

Comment thread packages/cli/src/deno.ts
() => 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.

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
// and Bun supply neither half.

Comment thread packages/cli/src/node.ts
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.

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

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

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

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

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

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 identity table.

@taras
taras marked this pull request as ready for review October 16, 2026 19:26
@taras
taras merged commit 4702890 into main Oct 16, 2026
44 checks passed
@taras
taras deleted the agent/repl-issues-pr6 branch October 16, 2026 19:39
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