feat(cli): route first-party CLI terminal I/O through Effect Terminal/Stdio; spell routed-CLI input errors in CLI terms (#465) - #505
Conversation
🦋 Changeset detectedLatest commit: 7e48cd7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 74dcc262fd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
d3de27f to
ebf2c46
Compare
417e2cb to
fc64516
Compare
commit: |
b8e0f92 to
979a8d0
Compare
979a8d0 to
9577a3b
Compare
…/Stdio; spell routed-CLI input errors in CLI terms (#465)
…tter after rebase
…input-issue expectations (review)
…assert the #465 flag error in the audiobook-curator dispatch proof
…erminal/Stdio at the NodeServices root; port uninstall output to the CLI's Effect services after rebase
… the dependency agent-bundle already carries (#508); drop @effect/platform-node
9577a3b to
7e48cd7
Compare
…art time (#530) * perf(cli): load the Effect terminal runtime lazily to restore cold-start time agent-bundle --version / --help / argv errors: ~300 ms -> ~60 ms (pre-#505 level); create-agent-bundle --help: ~70 ms -> ~40 ms. The cost was module loading (effect + effect/Terminal + platform-node-shared ~250 ms), not runtime construction (~6 ms), so runCli now builds the terminal runtime on the first command write via a dynamic import of src/effect/cli-runtime.ts, and Commander's own text is written synchronously. The scaffolder splits its Effect/NodeServices/Clack program into scaffold-cli.ts the same way. cli.test.ts records module loads under module.registerHooks and fails if the trivial invocations resolve an effect module. * chore: reference #530 in the changeset
Adopts Effect's
Terminal/Stdioservices for the first-partyagent-bundleCLI's terminal I/O and fixes #465: routed CLI executables now spellinputSchemarejections in CLI terms instead of printing the raw zod issue JSON.Fixes #465
Effect version and layer wiring
effect@4.0.0-rc.112.Terminaliseffect/Terminal,Stdioiseffect/Stdio; the Node implementations are@effect/platform-node-shared/NodeTerminaland/NodeStdio— the packageagent-bundlealready depends on since feat(api): stage temporary artifacts and Codex schema output in Effect scoped temp directories (FileSystem phase 1, module 2) #508 (src/effect/platform.ts). No new dependency: the first version of this branch added@effect/platform-node, which feat(api): stage temporary artifacts and Codex schema output in Effect scoped temp directories (FileSystem phase 1, module 2) #508 rules out foragent-bundlebecause its non-optionalredispeer adds ~23 MB to every consumer install;create-agent-bundlekeepsNodeServices.layerfrom@effect/platform-nodeas before.src/effect/terminal.ts:display(text)(user-facing text →Terminal.display, stdout),writeStderr(text)(diagnostics →Stdio.stderr()),writeStdout(text)(machine output →Stdio.stdout(), byte-exact), andnodeCliServices = Layer.mergeAll(NodeTerminal.layer, NodeStdio.layer).runCliinsrc/cli.tscreates onemakeScopedEffectRuntime(output.services ?? nodeCliServices)from the existingsrc/effect/boundary.tsand runs every write through it; the runtime closes when the command finishes (a foregrounddevsession keeps it until the session closes so close diagnostics can still be written). No other module provides the layers. Commander's lazy product imports are untouched.NodeServices.layer, measured: loading the full service set (child-process/crypto/filesystem, andundicivia the@effect/platform-nodebarrel) cost 582 ms vs 146 ms for the two subpaths on this machine — so the CLI composes the two narrow layers rather than the wholeplatformLayer, asdocs/effect-conventions.mdalready prescribes for platform services.tests/support/cli-terminal.ts(Terminal.make({ display })+Stdio.layerTest({ stdout, stderr })), passed asrunCli(args, { services }). The formerCliStreamsstream injection is gone; everyrunClitest now uses the capture layer. Noprocess.stdoutspies remain in migrated coverage.Terminal.displayis stdout-only (Terminal.ts,NodeTerminal.ts), so stderr diagnostics go throughStdio.stderr()and canonical JSON (--json, the diagnostics document) throughStdio, neverTerminal.Inventory
src/cli.ts— humanbuild/prepack/install/doctor/validate/eval/inspectoutput, Workbench startup URL line, Commander help/argv errors,--jsondocuments, diagnostics documentTerminal.display;--jsonand diagnostics viaStdio. Commander's synchronouswriteOut/writeErrare queued and replayed in order through the same services.src/cli.ts—dev proxywriteDiagnosticStdio.stderr()through a serial chain (the bridge's stdout is JSON-RPC and stays untouched).src/cli.ts—mcp runsrc/cli-entry.ts—runGeneratedCliEntrywriteOut/writeErrports, TTY detection,runGeneratedCliProcessexit/signal envelopesrc/build/entry-shell.ts— bin templateparseInput, worker stderr forwardingparseInputnow callscliInputError(command, input, error).src/mcp-entry.ts,src/services/mcp-run.ts,src/mcp-server-runtime.ts(worker/notice stderr)src/adapters/hook-contract.tssrc/install-entry.ts,src/install/surface.tsagent-bundle installoutput migrated viasrc/cli.ts.src/dev/playground/lifecycle-replay-service.ts,src/test/script.tspackages/create-agent-bundle/src/index.ts—--help(process.stdout.write(helpText)) and flag errors (process.stderr.write(message + helpText))NodeServices.layerroot):cliProgram(argv)is one Effect over `FileSystempackages/create-agent-bundle/src/index.ts— Clackintro/log/note/outro/promptsreadLine.#465 before/after
Typed
CliInputError.issues: { target, expected, received?, message }[]built bycliInputError(command, input, error)(exported fromagent-bundle/cli-entry, applied identically in the bin template,src/test/cli.ts, andsrc/test/render.ts). The first zod path segment maps throughCompiledCliOption.key → option: positionals render<name>, flags--kebab-name, projected MCP paths--input.<path>, non-attributable issuesinput. Exit code stays 2.Before (
curator await --max-wait-ms 300000 ticket):After:
Positional (
inventory ""):Invalid value for <shelf>: expected non-empty string; received "".Projected MCP (
harness echo --input '{"message":42}'):Invalid value for --input.message: expected string; received 42.--json: stdout empty, stderr one canonical line:{"error":{"code":"CLI_INPUT_INVALID","issues":[{"expected":"number <= 8","message":"Too big: expected number to be <=8","received":9,"target":"--limit"}],"usage":"Usage: route-harness inventory [options] <shelf>"}}--ndjson: one{"error":{...same error, plus the joined "message"...},"sequence":0,"type":"error"}event on stdout, stderr empty.Sizes and startup
examples/audiobook-curator,artifact/claude/bin/audiobook-curator.mjs): 3,145,847 → 3,151,702 bytes (+5,855 B, the issue-mapping code only);-flight.mjsunchanged at 3,350,707 B. Noeffect/Terminalor@effect/platform-nodereference in any emitted bin or installer.dist/cli.js: 33.7 kB,effectand the two platform-node subpaths stay external.agent-bundle --versioncold start on this machine: ~60 ms → ~290 ms. The cost is loadingeffect(barrel ~225 ms;effect/Terminalalone ~145 ms because it importsSchema) plus the two Node layers; the same imports viaeffect/*subpaths measured ~170 ms, so switchingterminal.ts/boundary.tsto subpath imports would recover ~60 ms if wanted. Recorded indocs/effect-conventions.mdas the re-pin check.Risks recorded
NodeTerminal.readLinemaps EOF toTerminal.QuitError; nothing reads stdin today, but a future prompt must handle it (the capture layer models it).Terminal.displaywrites text unchanged; every write keeps its explicit\n.@effect/platform-node-shared(not@effect/platform-node) is the source of the layers, so neitherundicinor theredispeer enters the dependency tree; the two subpaths are imported directly.@types/nodetransitively (@effect/platform-node-shared→@types/ws→@types/node); this is feat(api): stage temporary artifacts and Codex schema output in Effect scoped temp directories (FileSystem phase 1, module 2) #508's footprint, already documented there. The firstRelease gatesrun on this branch hit it asEEXISTin the packed-consumer proofs; the branch briefly carried its own fix, now dropped in favour of main'slinkWorkspaceTypes(feat(api): stage temporary artifacts and Codex schema output in Effect scoped temp directories (FileSystem phase 1, module 2) #508).Docs
docs/effect-conventions.md(new "Terminal and Stdio: user-facing CLI text" section + adoption-table row, merged over #501's FileSystem adoption: its intro paragraph gains one pointer sentence, its layer-wiring bullet now states the first-party CLI's actual root —makeScopedEffectRuntime(nodeCliServices)inrunCli— instead ofNodeServices.layerbeforerunPromise),docs/entry-conventions.md,docs/diagnostics.md(rendered into the site's diagnostics reference),website/docs/{en,zh}/guide/authoring/package-entries.mdx.Verification
pnpm typecheck,pnpm lint,pnpm docs:site:build(language parity OK).pnpm test:unit3110 passed,pnpm test:route-unit46,pnpm test:projection151 (includes newtests/projection/cli-input-errors.test.ts: text, positional + multi-issue,--input.<path>,--json,--ndjson).cli-routes-build.test.ts(packed executable asserts the exact stderr prose and the--jsonerror object),cli.test.ts,package-build.test.ts,dev-workbench.test.ts,artifact-cli-bin.test.ts,eval-cli.test.ts— all green.cli-routes.test.tsgains unit coverage of the mapping (bounds, enum, unknown key, missing value, nested MCP paths, non-schema fallback, every string refinement's operand) and the shell's text/--jsonrendering.examples/audiobook-curator/tests/route-unit/cli-dispatch.test.tsasserted the old raw zod text (maxFiles,expected number to be >=1) forinspect --max-files 0; it now asserts the exact Generated routed CLI prints raw ZodError JSON for input validation failures instead of a plain-language flag error #465 stderr (Invalid value for --max-files: expected number >= 1; received 0.+ usage + help hint). This was theExamples checkfailure onfc64516ba.Release gatesonfc64516baalso hadpacked-release.e2e.test.tsfail its Workbench outage-ledger quiet fence on anet::ERR_ABORTEDfor/api/logs/replayduring the "Logs page heading" phase — a browser navigation aborting an in-flight replay fetch, unrelated to this branch (no Workbench or dev-server code changed). Re-run if it recurs.Review status
74dcc262f(PR opened) and left one P2 thread oncli-entry.ts(invalid_formatlost thestartsWith/endsWith/includesoperand). Addressed infix(cli-entry): keep string-refinement operands and exact lengths in input-issue expectations: the expectation now readsstring starting with "/",string ending with ".json",string containing "@",string matching /.../u,string with exactly 4 characters, orURL, with unit coverage for each refinement in the bounded grammar. Per this branch's working rule no PR comments are posted; the thread is resolved from this note.main(#504moved thebuildaction and doctor host-validation lines;#507fixed the unrelated docs-site duplicate key that failed the firstDocs siterun). ThePublish pkg.pr.new previewfailure onebf2c467cwas a pkg.pr.new/Cloudflare 500 (put: ... (10043)), not a package problem.main(#501FileSystem phase 1 → conflict indocs/effect-conventions.mdresolved as above, and the scaffolder's two plain writes migrated now that itsNodeServicesroot exists;#452addeduninstall/lifecycle output tocli.tsagainst the oldOutput/writeMachineAPI — ported tohumanUninstall+machine/displayandtests/uninstall.test.tsto the capture layer).pull_requestworkflow runs stopped being created for this branch after thefc64516bapush (three pushes and a close/reopen produced check suites for every app except Actions); the same commits were verified byworkflow_dispatchruns ofci.ymlanddocs.ymlon the branch instead, recorded below.main(#508FileSystem phase 1 module 2 →docs/effect-conventions.mdconflicts merged,@effect/platform-nodedropped fromagent-bundlein favour of the@effect/platform-node-sharedit now depends on, my packed-fixture@typesfix replaced by main'slinkWorkspaceTypes;#519touchedcli.tswithout conflict).7e48cd792, all PR checks green (Changeset, Dependency review, Docs site, Examples check, Host install proofs, pkg.pr.new preview, RSC micro-eval, Release gates, Verify Node 24);9577a3be4before it also had all PR checks green (Changeset, Dependency review, Docs site, Examples check, Host install proofs, pkg.pr.new preview, RSC micro-eval, Release gates, Verify Node 24). Codex's last review is of74dcc262f; unreviewed since: the review fix (417e2cb32), the packed-fixture/example-test fix (979a8d0c2), and the third rebase + scaffolder/uninstall port (9577a3be4). No@codex reviewcomment was posted (this branch's working rule is body notes only); any thread opened after merge is answered in a follow-up PR.