Skip to content

✨ Keep a REPL execution's chat beside its entries (#883, PR 2 of 2) - #921

Merged
taras merged 11 commits into
mainfrom
agent/issue-883-pr2
Oct 17, 2026
Merged

taras merged 11 commits into
mainfrom
agent/issue-883-pr2

Conversation

@taras

@taras taras commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #890. This PR's base is agent/issue-883-pr1, so the diff and
commit list below are this slice alone (71 commits). Review #890 first; when it
merges, this PR's base follows to main. Neither branch is rebased.

Closes the second half of #883.

Why

A person running a document in xmd repl had no way to ask about it from inside
the REPL. They had to leave the command, lose the execution they were reading,
and ask somewhere with no access to the entries, the generated source or the
history. PR 1 made a host that can evaluate, preview and converse. This PR gives
the REPL a Sidekick: a conversation that lives beside the entries, under the
agent and configuration the person chooses, and that can read the execution it
is talking about.

What changes

Before:

  • The REPL had entries and nothing to ask about them. There was no chat, no
    conversation history, and no way to reach an agent from the command.
  • The right pane showed Bindings only, at a fixed 36/28 columns.

After:

  • A Sidekick tab beside Bindings holds a conversation per execution. Messages
    are saved, replies stream as they arrive, and previous chats are readable
    through Sessions.
  • A reply can read the entries, the entry output windows, the generated root's
    own vocabulary and seven shipped teaching documents, and can ask the person a
    question that is answered in the pane that asked it.
  • Agent, model and effort are chosen per Sidekick and survive a reopened
    conversation only under the configuration that conversation has.
  • Transcript and the right pane share the space after the sidebar equally, the
    odd cell going to Transcript; the Sidekick body is inset two cells each side.
  • The message composer wraps at its measured width and grows to six value rows,
    then scrolls internally, reduced on small screens.

How it works

xmd repl <doc> → Sidekick tab → message → conversation provider → agent turn
  → reply reads entries/output/vocabulary/teaching → chat history beside the entries

A REPL execution owns one chat history. A message reserves an entry slot through
the same single decision every other submission uses, so Pause is offered from
the moment Run reserves it and a submission is admitted only when its entry
really was. The turn is established against the records for its own request,
which is what lets a cancelled, interrupted or refused turn be told apart from a
failed entry and recovered at its boundaries.

Frame preparation retains plain-text line cuts per line, keyed on text, measured
width and terminal size, in storage the running command owns, and skips chat
fitting entirely when the measured frame presents no chat viewport.

Review guide

Start with: packages/cli/src/repl/sidekick/owner.ts

Then review:

  1. packages/core/src/agent/conversation-provider.ts,
    provider-configuration.ts, session-setup.ts — the conversation contract
    and what a saved conversation may continue under.
  2. packages/cli/src/repl/sidekick/history.ts, model.ts, requests.ts — the
    chat, what a reply reads, and how a turn is established.
  3. packages/cli/src/repl/sidekick/admission.ts, gate.ts,
    packages/core/src/generated-root-owner.ts — entry reservation, the single
    slot decision, and the generated root's own environment.
  4. packages/cli/src/repl/{application,program,layout,fitting}.ts — pane
    geometry, the composer and retained fitting.

Look carefully at:

  • Cancellation and cleanup: a cancelled handoff is released, a cancelled entry
    retains no close, and an entry scope owns its agent cleanup.
  • Question ownership: a Sidekick question belongs to its provider's scope, and
    the drawn form owns the keys while the top drawer owns its controls.
  • Retained fitting invalidation: withdrawn Preview text must disappear
    immediately; cached cuts never authorize drawing text the current display
    facts have cleared.

What must stay true

  • A reply answers from the acknowledged history, not the screen — enforced by
    sidekick/history.ts and checked by repl-sidekick-journey.test.ts.
  • Each chat keeps its own reading; scrolling an older chat cannot move the current one — enforced by per-chat viewport state keyed on chat identity and
    checked by CF-P1 in repl-sidekick-presentation.test.ts.
  • An unchanged chat adds no text-fitting work to a keystroke — enforced by
    ReplPlainFitStore and checked by RS3–RS5 in repl-presentation-style.test.ts.
  • Send carries the original literal string; wrapping is display only — enforced
    by keeping the original string the sole editing value and checked by MC6.
  • Transcript and the right pane share the space after the sidebar, odd cell to Transcript — enforced by inspectionWidth in layout.ts and checked by PW1.
  • A saved conversation continues only under the configuration it has — enforced
    by provider-configuration.ts refusing an unstatable configuration with an
    Error, and checked by repl-sidekick-configuration.test.ts.

How to verify it

The three focused selections released with this slice's assignment, run against
this exact commit (d906c636), all green:

deno task test packages/cli/tests/repl-sidekick-presentation.test.ts packages/cli/tests/repl-sidekick-journey.test.ts packages/cli/tests/repl-sidekick-lifecycle.test.ts packages/cli/tests/repl-presentation-style.test.ts
deno task test packages/cli/tests/repl-sidekick-configuration.test.ts packages/cli/tests/repl-sidekick-controls.test.ts packages/cli/tests/repl-forms.test.ts
deno task test packages/cli/tests/repl-journey.test.ts packages/cli/tests/repl-agent-interface.test.ts packages/cli/tests/repl-agent-journey.test.ts packages/cli/tests/repl-execution.test.ts packages/cli/tests/repl-lifecycle.test.ts packages/cli/tests/repl-layout-admission.test.ts packages/cli/tests/repl-source-reading.test.ts
Selection Result
1 — Sidekick presentation/journey/lifecycle, style 51 tests / 171 steps, 0 failed
2 — configuration, controls, forms 24 / 132, 0 failed
3 — ordinary journey, agent interface, execution, lifecycle, layout, source 73 / 225, 0 failed

deno task check is clean.

  • RS3 proves typing reuses unchanged chat: 728 warm renders against 4 for the
    typed frame, and fails if a key refits history it did not change.
  • RS4 proves growth measures only its change exactly (97 − 14 = 83), and fails
    if earlier lines are refit when the newest Preview line grows.
  • MC5–MC7 prove the composer wraps, caps at six value rows and survives resize
    and a busy Send, and fail if the value and chat rows overlap again.
  • Independently measured typing latency went from ~152 ms/key to 6.8 ms/key with
    the chat visible, and 151 to 4.6 hidden. These are local diagnostics, not
    thresholds; the RS3–RS5 count assertions are what establish the fix.

Rendered manually in a real tmux terminal at 160x36 and 120x30.

Scope

Included

  • The Sidekick conversation, its chat history, streaming replies and Sessions
    reading.
  • Entry reservation and the single slot decision shared with every submission.
  • Agent/model/effort configuration and reopened-conversation refusal.
  • Pane width sharing, body inset, header bands and the wrapping composer.
  • Retained plain-text fitting and hidden-chat avoidance.

Intentionally unchanged

Across the whole PR:

  • REPL_DESCRIPTION and SIDEKICK_QUIET stay out of scope, as directed by the
    assignments.
  • The footer remains seven rows; ordinary route drawers keep their behavior.
  • Bindings keeps the body layout it has always had; the two-cell inset is the
    Sidekick's own.

This PR does add, as its accepted contracts: the conversation-provider contract,
the paired chat history beside entry History, and the sidekick= chat selector
on the route. The final presentation increment is what introduces no journal,
route or provider interface change and no new stored width — that exclusion
belongs to that increment, not to the branch.

New abstractions

  • ReplPlainFitStore (repl/fitting.ts) exists because every keystroke refit
    all chat text — 1,284 measurement renders per frame at twenty wrapping
    messages. Consumers: chat, composer and the guidance/refusal directions, each
    with its own store so one cannot evict another.
  • packages/cli/src/repl/sidekick/* — fifteen modules separating owner,
    history, model, admission, controls, questions, recovery and repository.
    Consumers are the REPL program and its tests.
  • packages/core/src/generated-root-owner.ts and
    packages/core/src/agent/{conversation-provider,provider-configuration,session-setup}.ts
    carry the host-side contract this slice needs from PR 1's host.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Generated or mechanical changes

  • packages/git/** (44+/44−), packages/workflow/** (19+/19−) and
    packages/test-agent/** (5+/5−) are the session/layout/structure vocabulary
    rename from 9f15dc51, with no intended behavior change.
  • packages/cli/src/documents/sidekick/*.md are the seven teaching documents a
    reply reads (c8c689e7); their index is derived, not hand-written.

Risks and limitations

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Delivered onto current main (2026-10-16)

This branch is the replacement for the original stack. Head 879aba84,
eight signed commits, stacked on #890 at dbc0fc92, which is itself on main
fd8ae521 (#927). It retargets to main once #890 merges.

Five of the eight commits were reviewed before this restack; three are the UI
corrections the Product Owner asked for after it was opened, each reviewed on
its own exact SHA:

  • matching body insets on all three columns and a 36-column sidebar, which
    is what keeps completed, not recorded yet and All conversations (filtered)
    whole at the medium profile;
  • a successful reply that selected no <Output> is out of the chat — no
    sentence about the absence, no empty row — while a failed reply keeps its
    recorded reason whether or not it spoke;
  • Escape closes Configure, by the same transition its [close] control
    produces, and Configure's window controls read scroll up / scroll down.

range-diff against the reviewed source reports seven of the eight commits
identical. The first carries the integration: main's entry-capability selection
and host install beside this PR's --sidekick-agent profile option, main's
entryInstallation beside the Sidekick profile members, and the two renamed Git
modules (run-composition/placement.ts → checkout-layout.ts) carrying the
five importers main added since.

The eighth commit is new and is the integration this PR owes main's newer tests.
The 36-column sidebar leaves the Transcript six columns narrower, so sentences
that used to end on one row now wrap onto the next. Nothing is lost on screen —
guidance keeps guidance:row:1, a caption keeps caption:output+33, and each
row keeps the space the fitting cut at — so seven assertions now read their
reading across the rows it was cut into rather than off the first one. A
truncation still fails every one of them. This is the EH1/UI10 obligation
resolved rather than waived; UI11 and UI13 turned out to be the same class.

Evidence on this exact head — the seven frozen selections:

Selection Result
core host-generated-root, generated-xmd-root, agent-conversation, agent-function-components, agent-session-launch, agent-session-setup 50 passed (278 steps)
acp provider, native-launch, host-conversation, loaded-copy 29 passed (224 steps)
generated-xmd-preview, secret-detection, workflow-agent-checkpoints, workflow-agent, workflow-agent-sessions 11 passed (114 steps)
repl-sidekick presentation, journey, lifecycle 39 passed (141 steps)
repl-sidekick storage, recovery, configuration, controls, output 32 passed (173 steps)
repl-entries, repl-history, repl-agent-execution, repl-journey, repl-forms 90 passed (265 steps)
repl-route, repl-layout-admission, repl-presentation-style, repl-source-reading, repl-execution, repl-lifecycle 46 passed (190 steps)

Plus the eight consumers main supplies for its moved repository and runtime
boundaries — repl-boundaries, repl-repositories, repl-repositories-pty,
repl-issues, repl-issues-pty, run-composition, testing-execution-host
and testing-execution-host-native: 17 passed (97 steps).

314 tests, 1,482 steps, zero failures. deno task check, deno task lint and
deno task validate:docs exit 0, and deno task check:jsr ends
Success Dry run complete — which needed one fix of its own, an explicit type
on the agent conversation provider's context, since a public symbol cannot have
an inferred one.

repl-lifecycle's L2 question row failed once and passed on re-run; it is the
recorded flake for that phase, and the first failure is preserved.

Deferred and deliberately not here: History self-stacking, conversation labels,
the narrow pending-question layout, Parse #922 and #928–#933.

What CI found that no frozen selection runs (d8890e40)

Three more failures, all the same accepted width meeting readers that assume a
row — and all outside every local selection, which is why only CI saw them:

  • repl-terminal.test.ts asserts the whole frame's geometry as an independent
    table and still held the pre-Add an interactive Sidekick to xmd repl #883 shares. It now states the accepted ones:
    36/62/62 from 160 columns and 36/42/42 from 120. The implementation was asked
    for those numbers and agrees.
  • The Node and Bun runtime refusals — no Repository composition provider, no
    Issue provider — are sentences the narrower Transcript wraps, so they are read
    across their rows. These only happen on the runtimes that install no such
    provider, which is why a Deno run cannot reach them.

Verified on the exact runtimes that failed: both PTY files under Bun and Node,
and the frame file under Deno and Node.

The last failing check was a cause, not a re-run (7e7ffd27)

repl-lifecycle.test.ts's L2 row sampled the lifecycle after a fixed five
sleep(0) turns. The phases a consumer is told travel through their own queue
and the question is published from inside the element, so how many turns that
takes depends on what else the runner has queued — and with another suite beside
it the sample catches Elicit:enter:, the phase immediately before the one the
row is about. It is recorded as flake #935.

It now waits for a signal deliberately narrower than its claim: that some
element has reported waiting on something. Which element, which phase, what it
waits on, and that it is the only one are all still asserted whole, so a wrong
phase or a second waiting element still fails.

Same revision either way: with the fixed count the six-file selection failed
once in three runs; with the wait it passed two of two six-file, three of three
two-file, and the file alone. The sibling fixed-turn loop below it keeps its
count, because that one asserts the reading stays empty and more turns only
strengthen it.

This branch was rebased onto main at 98cd350b (#890's merge) after that PR
merged. Verified faithful: all ten commits identical by range-diff and the
tree byte-identical to the previously published head.

@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

PR #921: ✨ Keep a REPL execution's chat beside its entries (#883, PR 2 of 2)

202 files, +50413 / -3029

Scope

🔴 PR has 53442 lines changed. Split into focused PRs.

🟡 53442 lines changed. PRs under 400 receive more thorough review.

🟡 202 files changed. Are all changes related?

🟡 Changes span 23 directories.

🟡 New abstraction files: packages/core/src/agent/conversation-provider.ts, packages/core/src/agent/provider-configuration.ts. Verify 3+ consumers.

Structural

Oxlint structural signals:

  • no-unused-vars ×80: packages/cli/tests/fixtures/repl/issue-883/sidekick-harness.ts, packages/cli/tests/fixtures/repl/issue-883/recovery-phase.ts, packages/cli/tests/fixtures/repl/issue-883/mounted-program.ts (+23)
  • no-unnecessary-type-assertion ×16: packages/testing/src/execution-harness.ts, packages/cli/src/repl/sidekick/feedback.ts, packages/cli/tests/fixtures/repl/issue-883/sidekick-harness.ts (+7)
  • no-empty-function ×10: packages/git/src/deno/retained-repository/selections.ts, packages/cli/src/repl-profile.ts, packages/acp/src/adapter-snapshots.ts (+5)
  • no-unnecessary-type-arguments ×3: packages/cli/src/repl/sidekick/owner.ts, packages/cli/src/repl/session.ts
  • no-redundant-type-constituents ×1: packages/git/src/deno/composition/operations.ts
  • no-unnecessary-boolean-literal-compare ×1: packages/cli/src/repl/application.ts

Slop

Oxlint slop signals:

  • no-console ×3: packages/cli/src/cli.ts
  • no-inferrable-types ×2: packages/core/src/expand.ts

Static Analysis

Oxlint: 447 diagnostics across 56 files (30 rules)
Density: 0.009 violations/added-line

no-shadow (110): packages/cli/tests/fixtures/repl/issue-883/recovery-phase.ts, packages/cli/src/repl/layout.ts, packages/cli/tests/fixtures/repl/issue-883/mounted-program.ts (+20)
no-unused-vars (80): packages/cli/tests/fixtures/repl/issue-883/sidekick-harness.ts, packages/cli/tests/fixtures/repl/issue-883/recovery-phase.ts, packages/cli/tests/fixtures/repl/issue-883/mounted-program.ts (+23)
unbound-method (44): packages/testing/src/execution-harness.ts, packages/cli/tests/fixtures/repl/issue-883/sidekick-harness.ts, packages/cli/tests/support/fake-acp.ts (+17)
consistent-function-scoping (31): packages/cli/tests/fixtures/repl/presentation.ts, packages/cli/tests/fixtures/repl/issue-883/mounted-program.ts, packages/cli/src/repl/session.ts (+7)
no-floating-promises (31): packages/core/src/agent/conversation.ts, packages/cli/src/repl/program.ts, packages/cli/src/repl/reconcile.ts (+9)
no-unsafe-type-assertion (25): packages/cli/src/repl/sidekick/questions.ts, packages/testing/src/execution-harness.ts, packages/cli/src/repl/sidekick/feedback.ts (+11)
no-array-sort (18): packages/cli/tests/fixtures/repl/presentation.ts, packages/cli/tests/fixtures/repl/issue-883/recovery-phase.ts, packages/core/src/agent/provider-configuration.ts (+10)
no-unnecessary-type-assertion (16): packages/testing/src/execution-harness.ts, packages/cli/src/repl/sidekick/feedback.ts, packages/cli/tests/fixtures/repl/issue-883/sidekick-harness.ts (+7)
no-useless-spread (14): packages/git/src/deno/run-composition/provider.ts, packages/cli/tests/support/fake-acp.ts, packages/cli/src/cli.ts (+2)
consistent-return (13): packages/testing/src/execution-harness.ts, packages/cli/src/repl/program.ts, packages/cli/tests/fixtures/repl/surface.ts (+5)
no-base-to-string (11): packages/core/src/agent/function-components.ts, packages/core/src/expand.ts, packages/cli/src/plan-component.ts
no-empty-function (10): packages/git/src/deno/retained-repository/selections.ts, packages/cli/src/repl-profile.ts, packages/acp/src/adapter-snapshots.ts (+5)
no-control-regex (6): packages/cli/tests/fixtures/repl/presentation.ts, packages/cli/tests/fixtures/repl/issue-883/mounted-program.ts, packages/cli/tests/support/pty.ts
no-duplicate-type-constituents (5): packages/core/src/execute.ts, packages/cli/src/repl/application.ts
no-unnecessary-template-expression (4): packages/cli/src/repl/sidekick/feedback.ts, packages/acp/src/adapter-snapshots.ts, packages/acp/src/provider.ts (+1)
no-useless-fallback-in-spread (3): packages/cli/src/repl/description.ts, packages/cli/tests/fixtures/repl/surface.ts, packages/core/src/expand.ts
no-console (3): packages/cli/src/cli.ts
no-unnecessary-type-arguments (3): packages/cli/src/repl/sidekick/owner.ts, packages/cli/src/repl/session.ts
no-useless-default-assignment (3): packages/cli/src/repl/program.ts, packages/cli/src/repl/application.ts
no-misused-spread (3): packages/cli/src/repl/sidekick/output.ts, packages/cli/src/repl/sidekick/controls.ts, packages/cli/src/repl/application.ts
no-unmodified-loop-condition (2): packages/cli/src/cli.ts, packages/cli/src/repl/journal.ts
no-inferrable-types (2): packages/core/src/expand.ts
no-unnecessary-type-conversion (2): packages/core/src/agent/function-components.ts, packages/cli/src/repl/sidekick/controls.ts
restrict-template-expressions (2): packages/core/src/expand.ts, scripts/lib/test-weights.ts
no-array-reverse (1): packages/cli/src/repl/reconcile.ts
no-unused-private-class-members (1): packages/core/src/secrets/policy.ts
no-underscore-dangle (1): packages/core/src/expand.ts
no-implied-eval (1): packages/core/src/expand.ts
no-redundant-type-constituents (1): packages/git/src/deno/composition/operations.ts
no-unnecessary-boolean-literal-compare (1): packages/cli/src/repl/application.ts

Correctness

No extraneous code patterns detected.

taras added 10 commits October 16, 2026 21:01
… PR 2 of 2)

Replacement PR2, re-implemented on main's captured-selection model rather than
rebased. C's remaining accepted behaviour — the paired chat storage, recovery,
the conversation owner, provider configuration and the direct provider, the
Sidekick surface, Sessions, controls, questions and the historical submission
correction — carried onto the accepted replacement PR1 at 85da74b9. The
bound-owner and root-vocabulary hunks that went into PR1 are not applied twice.

What main contributes, and what changed to meet it:

- A conversation's controls are captured once, when it opens. C passed
  observations and mutations with every evaluateGeneratedXmdRoot call; main
  removed that public request. Each control this CLI defines is now published as
  a component answer under its own name, the selection naming them is offered
  from the same installation, and canonical capture resolves and seals them
  before any response exists — so a response's request carries only which
  evaluation it is, the bytes, and the classes it may draw on. The provider
  answers each owned name once per execution and then settles, because a
  provider still answering while a response resolves its admitted name is
  substituting into that response, which canonical execution refuses. Per
  execution and not per owner: a reopening, a replacement and a switch are each
  a fresh capture.

- What stays each response's own is where a control's pair is filed and which
  slot a Submit binds. That travels through the owner's serving holder, named
  for the length of one evaluation and given up in an `ensure` registered before
  the work — so it outlives the evaluation's own cleanup and a control settling
  late still files under the response that ran it. A control invoked when no
  response is running refuses rather than filing under whichever came next.

- <Elicit> is selected, not pinned. The Architect decision removed the pinned*
  factories from the public surface, and nothing is lost: components/Elicit.ts
  already offers its paired spelling as a write under
  https://executable.md/components/elicit/ask, so selecting the protected name
  captures core's own definition and the form and the class are the
  declaration's. pinnedElicit() is therefore gone from core; a repository,
  registered or middleware component of that name still reaches a reply not at
  all. The controls tier compares the surface's Elicit props against the
  registry's own schema, so a core component that gained a prop fails the row
  rather than leaving a reply under-told.

- Each control's identity and effect live on the definition the capture reads,
  under https://executable.md/components/repl/sidekick/<key>, rather than being
  stated beside the entry. One declaration says what each admitted spelling
  runs as.

- The vocabulary rename lands: AgentSessionSetup, AcpxSessionRequest,
  ReplLayout, AnswersState and the rest. Where main had moved first — the Dir
  grant identity, validateBodyStructure, ordinaryGeneratedSelection,
  PlanWriterPlacement, the audit and session-identity prose — main's is kept and
  PR2's references follow it.

IN5's old-chat fixture is run-produced. It is the bytes a real run of the
pre-integration writer at d4b0e326 left behind, captured before this branch
existed and checked in with its receipt: one completed response, one interrupted
response holding a settled REPL.EntryInput beside a pending <Elicit>, and both
root evaluations marker 2 on their own generated_root:N sequences. Reopened
cold it changes no byte, sends no prompt and replays no suffix; what it said is
still what it says; and a new message continues the same native conversation,
allocates generated_root:3 and writes marker 5, with no replacement or migration
record and the old control not repeated.

Three dead exports and one dead type go with the port, because main's
dead-code gate is new relative to C: settled(), executionShape(),
openPairedEntries() and ExecutionShape. Eighty-one banned prose words and two
new lint rules — a suspending finally, two redundant test scopes — are corrected
for the same reason.

One fixture adaptation beyond the mechanical ones: Tier HI's `runs()` waited for
the entry's text to appear on screen, but the source is typed into the draft, so
that text is there from the keystroke — the wait returned while the entry was
still being admitted and the next read saw the entry before it. It now waits for
the recorded model to hold the entry, which is the fact the rows assert about.

EH1 and UI10 in repl-entries.test.ts remain red, byte-identically to the
recorded baseline, and command 6 is reported red for them.

Refs #883
#883)

Four bounded corrections to replacement PR2, inside the accepted architecture.

The four new casts are gone, with nothing standing in for them. The control
builders take a whole `FunctionComponentDefinition` and get their generated
metadata from `offering()`, whose table is annotated as the partial record
`GeneratedForms` already is — which is what makes a computed form key check
against `InvocationForm` instead of widening. `canonicalProps` narrows the
schema with core's own `isJsonObject` twice rather than asserting a shape, and
IN5's subprocess chat is read by `chatKinds()`, which narrows each item and
reports an unreadable one rather than taking it on trust.

IN6's capture and association evidence is complete, as four rows and one
extension:

- R3m: two owners' controls answer their own request handles. The same source
  in both, so what tells them apart is the handle each control closed over.
- R3n: a definition answered after setup reaches no response and runs nothing.
  The interception is demonstrably in the document's own chain — it is asked for
  the root and the holding element — and is never asked for the control's name
  at all, because a generated admission resolves an admitted name from the
  sealed definition and does not consult the document's chain for it.
- R3o: a selection composed after setup widens nothing. The omitted name is
  `File`, an ordinary component an entry may be given: a structural construct
  like `<Each>` would have proved nothing, since the language refuses one
  wherever it is written whatever any table says.
- R3p: the control for both, with nothing intercepting.
- R5 "a later response reads a fact that changed": a real entry publishes a root
  binding between responses, and a third response reads the new value through
  the control sealed when the conversation opened. A control closed over a
  reading taken at setup would still answer the empty map.
- R5 "each response files its own ordered pairs": one response's read and its
  failed fill, in admission order under its own response, and the next
  response's control at order zero under its own — read off the retained
  history, because a count of prompts cannot say which response a record
  belongs to.
- The held-cleanup Stop row now runs an actual control in the successor and
  reads the association back: the successor's read is the one answered pair, the
  held fill stays unfinished under the response that was stopped, and nothing of
  that response's control is filed under the successor. A late outcome moving is
  exactly what a response naming cleared before its cleanup had joined would
  produce.

IN5's identity assertion no longer reads the old turns. `canonicalTurns()` goes
through `parseEntryRecords` and `parsePromptRecord` to name the turn this
message became, and asserts *that* turn's native identity. The fixture already
holds `native-old` twice, so the old substring search held even when the new
turn ran somewhere else — which a break-it control confirms: pointing the new
turn at another conversation now fails with
`Expected: "native-old" / Received: "native-somewhere-else"`. The row also
compares the conversation records before and after, so no second conversation is
prepared or activated, and checks the old file is still a prefix of the new one.

The lost output-stream observation is restored where the Planner allocated it:
E1a now consumes `execution.output` through completion and requires the
document's own text in the chunks and the root's distinctive text absent from
both the chunks and the stream's close value. PR1's production code, branch and
worktree are untouched.

Three stale comments are corrected: `profile.ts` and `questions.ts` said
`pinnedElicit()` admits the question, and `respond()` said the ceiling is
established per turn.

The fixture bytes and receipt are unchanged, byte for byte.

Refs #883
#883)

Closes the one remaining R-E1 observation. The controls-suite rows reach the
canonical capture directly — each opens a HostExecution, evaluates a root and
closes it before the next begins — so they say nothing about two conversations
being alive together. A Sidekick assembly that handed a second conversation the
first one's captured controls satisfied every one of them.

The new journey row uses two whole owners. Each harness is held open in a task
of its own, because two cannot be acquired in one scope: each registers the
agent components, and the engine refuses two registrations of a name at one
scope rather than letting one silently win — it says to shadow in a nested
scope, and a task is one. Each owner's work runs in that same scope, since the
REPL host each harness installs is installed there, which is also what a
command does.

What distinguishes the two handles is a real REPL fact rather than anything
written into the reply: each session has run one entry of its own, and both
owners are sent the same control source byte for byte — a read naming
`entry-1`, which both sessions have holding different text, and then a write.

Observed: both conversations open and distinct before anything; the second
owner's whole cycle reading its own entry and filling its own draft while the
first owner's draft stays untouched; the first owner then left suspended inside
its own fill while the second runs a second cycle and still reads its own; the
first released and answering from its own REPL; three turns on one provider and
five on the other; and each chat holding its own controls in admission order,
under its own conversation and response, naming the other conversation nowhere.

A break-it control confirms it discriminates. Sharing one captured ceiling
across owners, first-opened winning, fails this row with
`Expected value: "Read it." / Received array: ["Open.", "", "Thanks."]` while
the whole sequential controls tier still passes.

R3m keeps its evidence and loses the claim it could not support: it is now
"each capture answers the handle its own installation sealed", and its tier
says where the live-owner observation lives. No production code is changed.

Refs #883
…#883)

The Product Owner asked for Bindings and Sidekick as horizontal tabs, each name
said once, and space between the top of the screen and the headings of all three
columns.

The right pane's two tabs are now its header: one row, Bindings then Sidekick,
each as wide as its own name. The pane's separate title is gone — it said
Sidekick twice while the Sidekick was the reading — and so is the literal
`[selected]` suffix, which the amendment supersedes. Which tab is selected is
the active styling the two have always differed by, and keyboard focus stays the
separate mark it was.

Each of the three shared columns keeps one blank row above its first heading, as
real layout padding, so the engine's own remainder is what the rows below are
measured against. It holds no text, reaches no pointer and is no Tab stop. The
narrow outlet heads no shared column and takes no gap.

That row moved the contextual guidance into the rectangle a route drawer's size
alone used to state, which would have stopped the screen saying how to close the
drawer in front of it — already true at 120x30 before this change. A route
drawer now begins below the whole *fitted* guidance block, measured from the
rows the frame published rather than counted. Its columns and bottom edge are
unchanged, so the difference is spent on drawer-window height.

Tier UH1-UH5 observe the one header row with selection and focus apart, both
tabs reached by pointer and keyboard, the gap holding nothing reachable, resize
through narrow and back, and the guidance unoccluded at three sizes with a
wrapped block.

Selection evidence had to change with the label: both names are drawn on every
frame, so `shows("Bindings")` is true of every screen either reading ever drew.
`paneShown`/`panePresented` read the selected pane from the body it draws.
… them (#883)

The Product Owner asked for the Transcript and Sessions bodies to have the same
two-cell padding the Sidekick's body already had, for the Transcript's title to
line up with what is under it, for the blank row above a heading to belong to
that heading, and for Earlier/Later to stop reading as dark action bars.

Every shared column now publishes two boxes: the pane, which its heading band
spans, and the body inset two cells on each side, which every row under that
heading is built to. The two are four cells apart and answer different
questions, so the manifest says which of the two each box is for, and each body
is found by its own id — the narrow outlet is one region for all three readings,
and a lookup by region handed the Transcript's directions the chat's width.
Guidance, refusal, reading and label fitting, the row boxes, the window counts
and the published targets all read the body that contains them.

The sidebar is 36 columns at both shared profiles, up from 32 and 28. Inset at
the old widths it could not hold a turn's state: `fitLabel` keeps that part
whole because a state cut in half is a row saying something untrue, and beneath
that guarantee is a hard cut the inset reached — at the medium profile it was
already being reached. Thirty-six leaves 30 columns past the focus marker, the
room the wide column had before the gutter, so the same rows fit at both sizes.
The Transcript and the right pane divide what is left, odd cell to the
Transcript.

The Transcript's title is inset by the engine's own padding inside a band that
still spans the column and draws its rule. The header band owns the blank row
above its heading and paints it, which moved ownership and no coordinate. A
`navigate` role gives a window's two controls the pane they move instead of the
draft's surface, keeping their ink, their focus cue, their target and their
action.

Tier BP1-BP5 observe the three matching bodies and their inert gutters, a
wrapped reading reaching its whole text through its own window, the mounted
consumers and narrow, the sidebar's longest rows whole at both sizes, and the
inset title, header-coloured gap and pane-coloured navigation.
A successful reply that selected no Output said nothing to the person, so
the chat shows nothing for it: no sentence about the absence, no empty
row and no bare agent name. The work is still a saved turn in Sessions
and still has its own records.

A reply that *failed* keeps its recorded reason whether or not it
selected Output — the screen-facing projection was dropping the retained
failure, so a failure's visibility had been depending on what the
response happened to say.

A Sessions reveal now picks its row from the composed and fitted rows the
frame will draw, so it cannot target a row nothing paints. Where a
turn's reply said nothing, what it reveals is the request that turn
answered: its own, or, for an automatic follow-up, the person request its
chain began with, read from the request boundaries the chat projection
already establishes. A turn whose request this history cannot bound
reveals nothing rather than a row chosen by resemblance.
Escape closed a route drawer and the Sidekick's question form and refused
everything else, so the one modal a person is most likely to press it in
did nothing. It now closes the presented Configure by the same transition
its visible Close control produces — written once, because two ways out
of a modal that disagreed about what closing means would make the
outcome depend on how somebody found the exit.

Which drawer owns the key is the frame's own answer rather than the held
open flag, so a Configure behind another pane or under History consumes
no Escape: History closes first and exposes the choices underneath.

Configure's window controls now read scroll up and scroll down. Earlier
and Later are words about a recorded order, and this is the one window
in the program that moves through a list of settings instead. The
actions, their order and their scrolling are unchanged, and every other
reading keeps its own vocabulary.
…em (#883)

Integrating #883's accepted sidebar width with the REPL work main gained
since. The thirty-six-column sidebar and the two-cell body gutters leave
the Transcript six columns narrower, so sentences that used to end on
one row now wrap onto the next — and seven assertions written against a
row reported a complete sentence as a truncated one.

Nothing is lost on the screen and nothing was weakened here. Guidance
keeps its continuations as `guidance:row:1`, a caption keeps its own as
`caption:output+33`, and each row keeps the space the fitting cut at.
Each reader now joins the rows its reading was cut into: the entry
reading's guidance, its Output caption and a failure reason, and, in the
two PTY journeys, a URL and a Git object id that are cut mid-token where
a column ends. A truncation still fails every one of them.

The two Git modules #883 renames carry main's newer importers with them:
`run-composition/placement.ts` is `checkout-layout.ts`, and five files
main added since reach it under that name.

The conversation provider's context gets the explicit type JSR requires
of a public symbol, which is the one thing `check:jsr` had to say about
this stack.
)

Three failures required CI to find, because no frozen selection runs
them: `repl-terminal.test.ts` is in none of them, and the two refusals
below only happen on the runtimes that install no repository or issue
provider — Node and Bun.

`repl-terminal.test.ts` asserts the whole frame's geometry as an
independent table, and still held the pre-#883 shares. It now states the
accepted ones: a thirty-six-column sidebar at both profiles, with what
is left split equally — 36/62/62 from 160 and 36/42/42 from 120. The
implementation was asked for these numbers and agrees with the table.

The two runtime refusals — no Repository composition provider, no Issue
provider — are sentences, and the narrower Transcript wraps them, so
they are read across their rows like the others. Verified on the exact
runtimes that failed: both PTY files under Bun and Node, and the frame
file under Deno and Node.
…883)

Two rows read a settled fact off whichever frame happened to be current.
A startup focus claim and a draft cleared by an admission are both
resolved in the frame that carries them, and on a loaded CI machine that
is not always the frame that arrives first — so both passed locally and
failed in CI twice, which is a divergence rather than a flake.

Both now wait for the screen they are about, through the fixture's own
`awaiting`. Neither claim moved: the startup row additionally states
that the Sidekick's box does *not* hold focus, which is the half that
was only implied before.

Verified under Bun as well as Deno, Bun being where the admission row
failed.
@taras
taras force-pushed the agent/issue-883-pr2 branch from c10a206 to 8b8c66d Compare October 17, 2026 01:01
@taras
taras marked this pull request as ready for review October 17, 2026 01:06
…#883)

The one check still failing CI, and the flake recorded as #935. The row
samples the lifecycle after a fixed five `sleep(0)` turns, but the phases
a consumer is told travel through its own queue and the question is
published from inside the element — so how many turns that takes depends
on what else the runner has queued. With another suite beside it the
sample catches `Elicit:enter:`, the phase immediately before the one the
row is about.

It now waits for a signal deliberately narrower than its claim: that
*some* element has reported waiting on something. Which element, which
phase, what it waits on and that it is the only one are all still
asserted whole, so a wrong phase or a second waiting element still fails.

Discriminating evidence, same revision either way: with the fixed count
the six-file selection failed once in three runs; with the wait it
passed two of two, the two-file selection three of three, and the file
alone. The sibling loop below it keeps its fixed count, because that one
asserts the reading stays empty and more turns only strengthen it.
@taras
taras merged commit ec4ba00 into main Oct 17, 2026
44 checks passed
@taras
taras deleted the agent/issue-883-pr2 branch October 17, 2026 02:33
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