feat(cli): auto-detect piped stdin for method run and workflow run#1402
Conversation
…wamp-club#336) Enable Unix pipe composition by auto-detecting piped stdin in method run and workflow run. Supports JSON objects, JSON arrays, NDJSON (one JSON per line), and YAML. Multiple items produce one discrete run per item with progress logging. --input overrides are deep-merged onto each stdin item. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
CLI UX Review
Blocking
-
No
--helpexamples for stdin in either command —model_method_run.tsandworkflow_run.tsreceived no new.example()calls showing stdin usage. Runningswamp model method run --helporswamp workflow run --helpgives zero indication that piping JSON/NDJSON/YAML is supported. The PR explicitly citesvault putas the design model, butvault putdocuments stdin prominently: once in the.description()body ("Piped via stdin: echo "val" | swamp vault put ") and again as a dedicated.example(\"Pipe from stdin (recommended for scripts)\", ...). Without matching treatment here, the feature is invisible to users who discover the CLI through its own help text.Suggested fix for
model_method_run.ts(mirror pattern inworkflow_run.ts):.example( "Pipe inputs from stdin", 'echo \'{"env":"prod"}\' | swamp model method run my-server deploy', ) .example( "Batch run via NDJSON from stdin", 'printf \'{"env":"dev"}\\n{"env":"prod"}\' | swamp model method run my-server deploy', )
Suggestions
-
--input-fileoption description not updated in command definitions — The SKILL.md was updated to say "Input values from YAML file (cannot combine with piped stdin)", but both command files still register the option as"Input values from YAML file". The conflict error message is clear when it fires, but--helpwon't surface the constraint. Low priority since the error message is actionable. -
JSON mode with multiple items emits multiple JSON objects — When piping NDJSON or a JSON array in
--jsonmode, each run emits its own JSON object to stdout (one per iteration). This is technically valid NDJSON output, but a user expecting a single JSON array of all results (common expectation for--jsonflags) may be surprised. Worth documenting or considering a JSON array wrapper for multi-item runs.
Verdict
NEEDS CHANGES — the stdin feature is well-designed and the error messages are clear, but --help gives users no hint it exists. Adding examples to both command definitions would make the feature discoverable and complete the consistency with vault put.
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
-
Lock release inconsistency before
Deno.exit(1)— Inworkflow_run.ts:410-411, locks are explicitly flushed beforeDeno.exit(1)inside the iteration loop (if (flushModelLocks) await flushModelLocks()), butmodel_method_run.ts:379callsDeno.exit(1)without flushing.Deno.exit()bypassesfinallyblocks, so the lock cleanup at line 389 won't run. Consider adding the same pre-exit flush for consistency:if (renderer.runFailed()) { if (flushModelLocks) await flushModelLocks(); Deno.exit(1); }
-
Multi-line docstring on
parseStdinContent(input_parser.ts:327-336) — CLAUDE.md says "one short line max" for comments. The format detection order is non-obvious behavior worth documenting, but the 10-line block could be condensed to a single line like// Detects JSON object/array, NDJSON, or YAML; returns one record per item.with the detailed order discoverable from the code itself.
What looks good
- Clean separation: stdin parsing stays in the CLI layer, not domain — correct DDD placement
- All libswamp imports go through
mod.ts— import boundary respected UserErrorused consistently for all user-facing error paths- Thorough unit test coverage (12 tests) for
parseStdinContentcovering all format paths and edge cases - NDJSON blank-line filtering, empty-array rejection, and non-object-element validation are solid
deepMergereuse (already tested) for--inputoverrides onto stdin items is clean- LogTape tagged template usage follows bare-interpolation convention
- Design doc, skill docs, and scenarios all updated consistently
Add --help examples for stdin piping to both method run and workflow run. Flush model locks before Deno.exit(1) in method run iteration loop (Deno.exit bypasses finally blocks). Update --input-file description to note stdin mutual exclusivity. Condense parseStdinContent docstring to one line per CLAUDE.md conventions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
YAML stdin not shown in help examples (
model_method_run.ts,workflow_run.ts.example()blocks)
The feature supports JSON, JSON array, NDJSON, and YAML — but only JSON and NDJSON appear in--helpexamples. A short third example would surface YAML discoverability:printf 'environment: prod\nreplicas: 3' | swamp model method run my-server deploy -
--inputoverride semantics not hinted in flag description (model_method_run.ts:113,workflow_run.ts:111)
The description"Input values (key=value or JSON)"doesn't mention that when combined with piped stdin,--inputvalues win on conflict. Users who pipe JSON and also pass--inputhave no help-text signal about precedence. A short addendum — e.g.,"…; overrides piped stdin values"— would make the behavior self-documenting without reading examples. -
Batch
--jsonmode produces NDJSON, not a single JSON document (undocumented)
When multiple items are piped and--jsonis set, each run emits its own JSON object —jq .would fail,jq -s .or NDJSON-aware consumers are needed. This is the right design for pipe composition, but it's surprising if discovered accidentally. A single note indesign/inputs.md(e.g., "In--jsonmode, each item produces one JSON object — output is NDJSON-shaped") would prevent confusion.
Verdict
PASS — feature is well-executed. Help text discoverability is good (examples cover the two most common cases), error messages are clear and actionable ("No input received from stdin. Pipe JSON, NDJSON, or YAML into the command." is excellent), progress logging [i/N] only appears in batch mode, exit-code and lock-flush handling is correct in both commands. Consistent with the existing vault put / model edit / workflow edit stdin pattern.
There was a problem hiding this comment.
Code Review
Clean, well-structured feature that adds piped stdin support to method run and workflow run. The implementation follows established patterns (readStdin from vault_put, model_edit, workflow_edit) and maintains proper layered architecture — stdin I/O in infrastructure, parsing in CLI layer, domain unchanged.
Blocking Issues
None.
Suggestions
-
Duplicate wiring logic — The stdin-detect → parse → merge → iterate pattern in
model_method_run.tsandworkflow_run.tsis nearly identical (~25 lines each). Extracting a shared helper (e.g.,buildInputSets(stdinContent, cliInputs, inputFile)) would reduce duplication, but the current approach is readable and the blast radius of a refactor may not be worth it right now. -
Single-line invalid JSON falls through to YAML — If a user pipes
{broken json(single line, fails JSON.parse), the NDJSON path is skipped (lines.lengthis 1, not > 1), and YAML parsing is attempted. The final error message is still correct ("Failed to parse stdin content…"), but a user intending to send JSON might find the "Expected JSON object, JSON array, NDJSON, or YAML" message slightly confusing. Low priority — the error is still actionable. -
No committed integration tests for the stdin piping flow — The PR body describes 15 e2e scenarios which is great, but they don't appear in the diff. The unit tests for
parseStdinContent(12 tests) cover the parsing logic thoroughly, so this isn't blocking. Adding a few integration tests inintegration/for the pipe composition flow would increase confidence for future regressions.
DDD compliance: Good. Stdin handling stays in the CLI/infrastructure layers. Domain services (modelMethodRun, workflowRun) continue to accept plain inputs: Record<string, unknown> — no domain boundary changes.
libswamp import boundary: Compliant. Both commands import parseTags from ../../libswamp/mod.ts, no internal path imports.
CLAUDE.md compliance: License headers present, named exports used, no any types, parseStdinContent tests live next to source (input_parser_test.ts), LogTape tagged templates use bare interpolation.
Summary
Closes swamp-club#336.
method runandworkflow run— no flag needed, consistent withvault put,model edit, andworkflow editparseStdinContent()detects JSON objects, JSON arrays, NDJSON (one JSON per line), and YAML[i/N]progress logging--inputkey=value overrides are deep-merged onto each stdin item (overrides win)--input-fileare mutually exclusive (clear error)design/inputs.md,swamp-modelskill, andswamp-workflowskill with stdin docs and pipe composition examplesTest Plan
parseStdinContent()covering all format detection paths (JSON object, JSON array, NDJSON, YAML, empty, malformed, non-object elements)data query --json | jq | method run--inputwithout pipe (regression)--inputoverride--input-filemutual exclusivity error--input-fileerror--inputwithout pipe (regression)--inputoverrides applied to every item🤖 Generated with Claude Code