Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
62 changes: 62 additions & 0 deletions packages/terminal-tmux/poc/repl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Black-box terminal REPL POC (issue #774) β€” result: VIEW_ONLY

A finite, disposable proof β€” not the production REPL, and not part of any package
export. It asked one question:

> Can generic terminal-state convergence make black-box tmux input delivery
> reliable enough while provider session files supply authoritative acceptance and
> completion?

**The decision is VIEW_ONLY.** Passive session-file observation is sound, but
reliable message dispatch cannot be established over black-box tmux input and
stays ACP-owned. The reasoning and the exact race are in [`RESULT.md`](./RESULT.md).

Nothing here is exported from `@executablemd/terminal-tmux`. It is reached only by
the deterministic evidence in `packages/terminal-tmux/tests/repl-poc.test.ts`. It
adds no Workflow capability, no journal record, no replay rule, and no workflow
syntax, and it changes no production, architecture or specification file.

## What the evidence establishes

- `state.ts`, `actions.ts`, `store.ts` β€” a Flux-style immutable store with a
closed action vocabulary and a sequence-numbered, staged-write log that replays
on restart and refuses gaps, duplicates, malformed records and illegal
transitions.
- `observer.ts` with `claude-observer.ts` and `codex-observer.ts` β€” a strict,
read-only session-file observer. It locates by exact native identity and project
from bounded header reads, advances the cursor only past a complete record,
groups Claude output by its `requestId` turn, and refuses ambiguity, truncation,
rotation, identity mismatch and unsupported shapes. It never writes a provider
file. **This is the reusable outcome.**
- `convergence.ts` β€” the generic terminal-convergence algorithm. No screen-text
parsing: two structurally equal pane samples across an acknowledged barrier,
with the provider's open-turn, cursor, event count and physical size unchanged.
- `delivery.ts` β€” literal delivery through a private `0600` file and a uniquely
named tmux buffer, prepared before the final sample, with a separate submit key.
- `controller.ts` β€” the message lifecycle: converge, prepare, final sample,
durable `AttemptStarted`, one guarded paste, then acceptance from the provider
file. Any unproved outcome becomes uncertain and is never retried.
- `report.ts` with `report.schema.json` β€” the `terminal-repl-poc-report.v1`
artifact, its validator, and the overall aggregator whose `PASS` was reachable
only with both live provider journeys.
- `live-worker.ts` β€” the terminal boundary kept as evidence: the pane probe over
an injectable tmux command seam and a real control-mode activity source, with
the single conditional guard the boundary would use.

## The live journey is closed out

The POC reached its decision without a live model turn, and the live-delivery
journey is permanently disabled. `live-supervisor.ts`'s `runLiveProof` launches no
coding agent and spends no turn under any environment; it returns the VIEW_ONLY
conclusion. The grid launch documents and the two live proof documents have been
removed.

## Running the evidence

```sh
deno task test packages/terminal-tmux/tests/repl-poc.test.ts
```

The deterministic suite freezes RP1–RP18 plus supporting boundary rows and passes
under Deno, Node and Bun. It records the VIEW_ONLY conclusion in a schema-valid
overall report.
54 changes: 54 additions & 0 deletions packages/terminal-tmux/poc/repl/RESULT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Result: VIEW_ONLY

Issue #774 asked whether generic terminal-state convergence can make black-box
tmux input delivery reliable enough while provider session files supply
authoritative acceptance and completion. After the deterministic evidence and two
architecture reviews, the POC's decision is **VIEW_ONLY**.

- **Passive observation works.** A strict, read-only observer can follow a
Claude session file and a Codex rollout file, match the exact native identity
and project, advance a durable cursor only past a complete record, and refuse
ambiguity, truncation, rotation, identity mismatch and unsupported shapes. That
half is sound and is the reusable outcome.
- **Reliable dispatch is not established.** Terminal convergence can only ever
authorize an attempt; it cannot guarantee one.

## The exact race

The controller samples the pane and the provider across an acknowledged barrier,
prepares the private file and tmux buffer, then takes one final combined sample
before recording `AttemptStarted` and pasting. Every observable change up to that
final sample refuses with zero paste.

What cannot be closed is the interval **between that final sample and the single
guarded paste**. A provider turn can open in that window. It is not observable
before the paste, and a tmux-only guard β€” which can atomically recheck pane
generation, process, liveness and mode, but not the provider's session file β€”
cannot atomically refuse it. A delivery admitted there is only ever settled as
`uncertain` after the fact, never proved safe before the bytes are sent.

Because a safe input point cannot be identified often enough to guarantee no
delivery lands on an open turn, tmux panes remain **view-only** for coordinated
work, and reliable REPL interaction stays **ACP-owned**.

## What this PR is

Evidence, not a shipped REPL. It contains:

- the deterministic RP1–RP18 matrix plus supporting boundary rows, all passing
under Deno, Node and Bun, that prove the observation, convergence, store and
report contracts and the safe refusals;
- the report schema and aggregator whose overall `PASS` was, by design,
reachable only with both live provider journeys β€” which are not run;
- no production, architecture, specification, dependency or lockfile change.

## What was removed at closeout

The live-delivery journeys are permanently disabled. The supervisor no longer
launches a coding agent or delivers a message under any gate; it returns this
`VIEW_ONLY` conclusion. The grid launch documents and the two live proof
documents are removed. No Claude or Codex model turn was ever spent.

A production retained REPL and action store are not authorized by this result.
Should coordinated multi-agent messaging be pursued, it belongs on ACP, whose
exchanges XMD already drives reliably, rather than on black-box tmux input.
206 changes: 206 additions & 0 deletions packages/terminal-tmux/poc/repl/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/**
* Issue #774 POC β€” the fixed action vocabulary.
*
* Actions are the only way the REPL store changes. The vocabulary is closed:
* every transition the REPL, the delivery worker and the two session-file
* observers can cause is one of the shapes below, and the reducer in
* `state.ts` is the only place they are applied.
*
* Each action is a plain value with a `type` discriminant. The store stamps a
* monotonic sequence number onto every one it accepts and persists it under
* that number, so the retained history is an ordered, gap-free log that replays
* to the exact state the run held.
*/

import type { NativeIdentity, Readiness } from "./state.ts";

/** The one action that has no role: opening the REPL session itself. */
export interface ReplOpened {
readonly type: "ReplOpened";
readonly replSession: string;
}

/** Bind one authored role to its native identity and initial pane generation. */
export interface RoleBound {
readonly type: "RoleBound";
readonly key: string;
readonly role: string;
readonly issue: string;
readonly identity: NativeIdentity;
readonly paneGeneration: number;
}

/** A message the operator asked to send, queued behind any earlier work. */
export interface MessageQueued {
readonly type: "MessageQueued";
readonly key: string;
readonly id: string;
readonly text: string;
readonly marker: string;
}

/** Terminal convergence reported a readiness for a role's pane. */
export interface TerminalObserved {
readonly type: "TerminalObserved";
readonly key: string;
readonly readiness: Readiness;
}

/** The provider observer reports an open turn: the pane must not be written to. */
export interface ProviderBusy {
readonly type: "ProviderBusy";
readonly key: string;
}

/** The provider observer reports no open turn. */
export interface ProviderIdle {
readonly type: "ProviderIdle";
readonly key: string;
}

/** Convergence began for the queue head. */
export interface ConvergenceStarted {
readonly type: "ConvergenceStarted";
readonly key: string;
readonly id: string;
}

/** A convergence attempt was invalidated before any byte was sent. */
export interface ConvergenceInvalidated {
readonly type: "ConvergenceInvalidated";
readonly key: string;
readonly id: string;
readonly reason: string;
}

/**
* The intent to deliver, recorded durably *before* the first terminal byte.
*
* This is the record a restart reads to know an outcome is uncertain rather
* than un-attempted.
*/
export interface AttemptStarted {
readonly type: "AttemptStarted";
readonly key: string;
readonly id: string;
}

/** The final server-side guard declined the paste; nothing was sent. */
export interface AttemptDeclined {
readonly type: "AttemptDeclined";
readonly key: string;
readonly id: string;
readonly reason: string;
}

/** An attempt whose outcome could not be established; never retried. */
export interface AttemptUncertain {
readonly type: "AttemptUncertain";
readonly key: string;
readonly id: string;
readonly reason: string;
}

/** The exact attempted bytes were observed as a user event under the identity. */
export interface UserAccepted {
readonly type: "UserAccepted";
readonly key: string;
readonly id: string;
readonly eventKey: string;
readonly identity: string;
readonly text: string;
/** The provider turn this acceptance belongs to, when grouped by turn. */
readonly turn?: string;
}

/** Assistant output observed under the intended identity. */
export interface AssistantObserved {
readonly type: "AssistantObserved";
readonly key: string;
readonly eventKey: string;
readonly identity: string;
readonly text: string;
/** The provider turn this output belongs to, when grouped by turn. */
readonly turn?: string;
}

/** An explicit provider completion boundary closed the turn. */
export interface AssistantCompleted {
readonly type: "AssistantCompleted";
readonly key: string;
readonly id: string;
readonly eventKey: string;
readonly identity: string;
/** The provider turn this completion closes, when grouped by turn. */
readonly turn?: string;
}

/** The observer cursor advanced past a complete, strictly parsed record. */
export interface ObserverAdvanced {
readonly type: "ObserverAdvanced";
readonly key: string;
readonly cursor: number;
readonly source: string;
}

/** The pane exited or was replaced; the role is no longer writable. */
export interface PaneUnavailable {
readonly type: "PaneUnavailable";
readonly key: string;
readonly reason: string;
}

/** The observer refused to advance: ambiguity, truncation, rotation, mismatch. */
export interface ObserverRefused {
readonly type: "ObserverRefused";
readonly key: string;
readonly reason: string;
}

/** The REPL session closed. */
export interface ReplClosed {
readonly type: "ReplClosed";
}

/** The whole closed vocabulary. */
export type ReplAction =
| ReplOpened
| RoleBound
| MessageQueued
| TerminalObserved
| ProviderBusy
| ProviderIdle
| ConvergenceStarted
| ConvergenceInvalidated
| AttemptStarted
| AttemptDeclined
| AttemptUncertain
| UserAccepted
| AssistantObserved
| AssistantCompleted
| ObserverAdvanced
| PaneUnavailable
| ObserverRefused
| ReplClosed;

/** Every action type name, for a persisted record to be validated against. */
export const ACTION_TYPES: readonly ReplAction["type"][] = [
"ReplOpened",
"RoleBound",
"MessageQueued",
"TerminalObserved",
"ProviderBusy",
"ProviderIdle",
"ConvergenceStarted",
"ConvergenceInvalidated",
"AttemptStarted",
"AttemptDeclined",
"AttemptUncertain",
"UserAccepted",
"AssistantObserved",
"AssistantCompleted",
"ObserverAdvanced",
"PaneUnavailable",
"ObserverRefused",
"ReplClosed",
];
Loading
Loading