overengineeringstudio / overengineeringstudio/effect-utils

Define the Effect 4 human and machine CLI stream boundary

Open
#980 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area:tui system:ci-tools system:tui-react type:chore
Dominant language
TypeScript
Stars
82
Forks
2
Avg merge
1d 8h
Merged PRs (30d)
121

Description

Goal

Make CLI output channels explicit and enforceable while embracing Effect 4's formatter and command-runner boundaries. Machine output must remain parseable without process-global capture or a second argv parser.

Dotfiles adoption is tracked separately in schickling/dotfiles#2093.

Current state

The original claim that no stdout contract exists is stale. PR #1159 established:

  • native CliOutput.Formatter wrapping for version-stamped diagnostics;
  • jsonStdoutGuardLayer(args) for explicitly detected JSON/NDJSON flags;
  • handlerConsoleLayer to restore handler payload output; and
  • process snapshots for mr, Genie, notion-cli, and tui-stories.

That implementation is a partial compatibility guard, not the final boundary:

  • CliOutput.Formatter formats strings but does not choose a stream;
  • Effect 4 Command.runWith sends help attached to parse failure through Console.log and errors through Console.error;
  • built-in explicit help, version, and completions also use Console.log;
  • output mode can resolve from fd/environment state after argv parsing, so the lexical guard misses auto → JSON on non-TTY stdout;
  • restoring the ambient Console in a machine handler lets incidental handler/library logs share the payload channel.

The existing tui-react runResult path already demonstrates the desired split: schema-encoded payload bytes go directly to fd 1, while Console, logger, and view output are routed to fd 2.

Output classes

Every CLI command/mode must declare one class:

  1. human — stdout is presentation output; stderr is diagnostics;
  2. machine-exclusive — stdout is an owned protocol port; only the machine writer may write it;
  3. framed-mixed — stdout may contain prose plus versioned marker records that consumers explicitly extract.

ci-tools remains framed-mixed. WORKFLOW_REPORT_V1: records and optional report files are intentional contracts; whole stdout is not JSON.

Chosen stream contract

Before a handler runs
  • Explicit --help, --version, and completions remain successful informational products on stdout.
  • Automatic help attached to an invalid flag, missing value, invalid choice, missing positional, or unknown command is diagnostic context and goes to stderr with the error.
  • A parse failure exits non-zero and leaves stdout empty. Do not invent a universal JSON error envelope.
Inside a machine-exclusive handler
  • A narrow schema-backed MachineOutput capability exclusively owns fd 1.
  • JSON uses the command's named schema. NDJSON emits one independently valid schema value per non-empty line.
  • Explicit raw-string results remain possible when raw bytes are the declared protocol.
  • Console, Effect logger output, TUI views, library diagnostics, and failure prose go to fd 2.
Inside human and framed-mixed handlers
  • Human modes keep normal presentation output.
  • Framed-mixed commands preserve their versioned record markers and extraction parser. Tests validate marked records rather than pretending the whole stream is JSON.

Upstream Effect work

Prefer the smallest semantic change in Effect's unstable CLI runner:

  • route automatic parse-failure help to Console.error;
  • preserve explicit Help/Version/Completions stdout behavior;
  • keep CliOutput.Formatter as a pure formatting boundary.

Add focused upstream runner tests that distinguish explicit help from parse failure. If upstream requires configurability, propose a separate informational/diagnostic sink service. Do not add stream-writing methods to Formatter.

Until the upstream behavior is available at the pinned Effect revision, keep one shared interim runner boundary with an explicit deletion condition. Do not spread local unconditional Console rebinding or new argv parsing across CLIs.

effect-utils work

Typed payload emission
  • Reuse the runResult invariant for final-result, state, and event modes.
  • Move TuiApp final JSON and progressive JSON/NDJSON payloads from Console.log to the schema-backed machine writer.
  • Preserve schemas, exact bytes where already contractual, event ordering, initial snapshot, EOF/final-line behavior, backpressure, drain, and exit status.
CLI roots
  • Apply the boundary to mr, Genie, notion-cli, and tui-stories.
  • Keep CliVersion.formatterLayer; it owns real formatting and version identity.
  • Classify ci-tools as framed-mixed. Preserve marker records, human deployment output, GitHub workflow-command output, and optional file behavior.
Deletion ledger

After the upstream and machine-writer gates pass, delete:

  • argvRequestsJsonStdout;
  • jsonStdoutGuardLayer;
  • handlerConsoleLayer;
  • their lexical-routing unit tests; and
  • the repeated guard/restore layer graphs and comments in four CLI roots.

Do not delete CliVersion.formatterLayer, runResult, ViewOutputStreamTag, version fast paths, or ci-tools framing.

Proof

Machine-exclusive subprocess matrix

For each machine output family:

  • success stdout decodes with its named schema or equals its explicit raw-string contract;
  • invalid flag, missing value, invalid choice, missing positional, and unknown command produce empty stdout, non-zero exit, and help plus error on stderr;
  • explicit JSON, NDJSON, aliases, and non-TTY auto detection are covered;
  • an injected handler/library Console.log("contaminant") reaches stderr while payload stdout remains exact;
  • explicit help/version/completions remain on stdout;
  • every NDJSON line decodes independently and ordering/finalization stay pinned;
  • large/progressive output fully drains on non-zero exit where the current contract promises it.

The injected contaminant is the negative control that proves the gate can fail.

Framed-mixed matrix

For ci-tools:

  • unmarked human text is ignored by the record parser;
  • every marked line decodes;
  • malformed marked lines fail;
  • parse errors never masquerade as marked records; and
  • optional report-file bytes remain equivalent to the framed record contract.

Non-goals

  • No human help layout, color, prose, build identity, annotation ID, telemetry identity, or exit-policy redesign beyond required stream routing.
  • No process-global stdout monkey patch or buffered capture as production enforcement.
  • No default fd 3 or file-only migration.
  • No universal machine error envelope.
  • No forced conversion of ci-tools to pure JSON.
  • No behavior changes hidden under Effect 4 canonicalization.

Acceptance

  • Upstream parse-failure routing is fixed or one shared temporary boundary exists with a deletion condition.
  • Every effect-utils CLI command/mode is classified as human, machine-exclusive, or framed-mixed.
  • A schema-backed machine writer is the only fd-1 capability in machine-exclusive handlers.
  • The subprocess and negative-control matrices pass for all machine-output families.
  • ci-tools framing and file behavior remain explicitly tested.
  • The lexical argv guard and repeated restore plumbing are deleted.
  • dotfiles#2093 can adopt the same contract without a repository-specific output abstraction.
Posted on behalf of @schickling
field value
agent_identity dev3.direct.omp.3vjvwkb2
session dev3.3vjvwkb2
agent_persona generalist
agent_supervisor unavailable
agent_tool OMP
agent_tool_version 18.1.7
agent_runtime OMP 18.1.7
tooling_profile dotfiles@931583a

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with Effect's unstable CLI runner, especially Command.runWith and the existing tui-react runResult path, then review the TuiApp output modes and the mr, Genie, notion-cli, tui-stories, and ci-tools roots. Run the specified subprocess and framed-mixed matrices, including the injected Console.log contaminant. Done means machine payloads remain exact on stdout, diagnostics are separated, and ci-tools markers and report files remain valid.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
cli, testing-qa
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.