overengineeringstudio / overengineeringstudio/effect-utils
Define the Effect 4 human and machine CLI stream boundary
Nobody has claimed this yet.
- 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.Formatterwrapping for version-stamped diagnostics; jsonStdoutGuardLayer(args)for explicitly detected JSON/NDJSON flags;handlerConsoleLayerto 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.Formatterformats strings but does not choose a stream;- Effect 4
Command.runWithsends help attached to parse failure throughConsole.logand errors throughConsole.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 → JSONon non-TTY stdout; - restoring the ambient
Consolein 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:
- human — stdout is presentation output; stderr is diagnostics;
- machine-exclusive — stdout is an owned protocol port; only the machine writer may write it;
- 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
MachineOutputcapability 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.Formatteras 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
runResultinvariant for final-result, state, and event modes. - Move
TuiAppfinal JSON and progressive JSON/NDJSON payloads fromConsole.logto 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
autodetection 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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