Skip to content

feat(cfl): support --file - to read page body from stdin#364

Merged
rianjs merged 3 commits into
mainfrom
feat/INT-424-file-dash-stdin
May 15, 2026
Merged

feat(cfl): support --file - to read page body from stdin#364
rianjs merged 3 commits into
mainfrom
feat/INT-424-file-dash-stdin

Conversation

@rianjs
Copy link
Copy Markdown
Contributor

@rianjs rianjs commented May 15, 2026

Summary

cfl page create and cfl page edit already read piped stdin (the os.Stdin char-device fallthrough in getContent/getEditContent), but the canonical --file - idiom failed: the fail-fast os.Stat(opts.file) guard and the os.ReadFile(opts.file) path both ENOENT on -.

This special-cases - in those two spots for both commands, reading through root.Options.Stdin (a tiny shared stdinReader helper) so the path is unit-testable. Markdown / storage / ADF selection (--no-markdown/--storage/--legacy) is unchanged and composes with -.

Enables callers that can't share a filesystem with the cfl subprocess (e.g. a remote MCP wrapper) to stream a body via stdin:

pandoc -t html doc.md | cfl page edit 12345 --file - --storage

Changes

  • page.go: 4-line stdinReader(*root.Options) resolver (Options.Stdin in tests, os.Stdin in prod).
  • create.go / edit.go: skip os.Stat for -; read stdin for - in the content path; help text + one example each.
  • create_test.go / edit_test.go: --file - cases — markdown→ADF default, --storage passthrough, --no-markdown raw-ADF passthrough (symmetric create/edit), edit empty-stdin guard.

Scope

  • --body <string> intentionally out of scope (the pipe path already covers it; ticket marks it nice-to-have).
  • Pre-existing unrelated lint finding in internal/config/config_test.go:460 (gosec false-positive on a test token) is not touched.

Verification

From tools/cfl/: make build ✓ · go test -race ./... → 999 passed ✓ · golangci-lint run ./internal/cmd/page/... → clean ✓

[INT-424]
Closes #363

page create and page edit already read piped stdin via the os.Stdin
char-device fallthrough, but the canonical --file - idiom failed: the
fail-fast os.Stat guard and the os.ReadFile path did not special-case
"-". Special-case "-" in both spots for both commands, reading through
root.Options.Stdin so the path is unit-testable. Markdown/storage/ADF
format selection is unchanged and still composes with -.

[INT-424]
Closes #363
@rianjs
Copy link
Copy Markdown
Contributor Author

rianjs commented May 15, 2026

Findings

No findings.

The PR adheres to the INT-424 boundary and the broader MCP goal:

  • The four load-bearing spots are covered: create/edit os.Stat(opts.file) guards and create/edit os.ReadFile(opts.file) paths.
  • --file - is routed through root.Options.Stdin via the small stdinReader helper, so tests do not need a real pipe.
  • Markdown default for stdin-like content is preserved with useMarkdown("").
  • --storage and --no-markdown composition is proven for both create and edit, including the raw ADF passthrough shape INT-425 will use.
  • --body <string> remains out of scope.
  • No unrelated production files or broader abstractions are introduced.

I also ran the focused package check locally from tools/cfl:

go test -race ./internal/cmd/page/...

Result: 140 tests passed.


Codex architect review (batch session, ticket 1/3 — INT-424)

@rianjs
Copy link
Copy Markdown
Contributor Author

rianjs commented May 15, 2026

TDD coverage assessment (automated)

TDD assessment: adequate — one minor gap worth noting

All 140 tests pass (go test -race ./internal/cmd/page/...). The new --file - feature is well-exercised for its primary paths.

What's covered well

  • --file - → markdown→ADF (create + edit)
  • --file ---storage passthrough (create + edit)
  • --file ---no-markdown raw-ADF passthrough (create + edit)
  • --file - empty stdin → rejected by content guard (edit only, see gap below)
  • stdinReader helper: testable via root.Options.Stdin injection, production fallback to os.Stdin — pattern is solid

One actionable gap

TestRunCreate_FileDash_EmptyStdin is missing. create.go has the same empty-content guard (strings.TrimSpace(content) == "" → error) and edit.go already has TestRunEdit_FileDash_EmptyStdin. The create path should be symmetric. It's a low-risk omission (the guard was pre-existing), but for consistency and regression confidence it's worth adding.

Not flagging

  • --legacy + --file - combo: the legacy flag routes through the same getContent call after the - branch returns, and is already covered by existing --legacy integration tests on the wider runCreate/runEdit paths.
  • stdinReader nil-guard (o == nil): defensive only, unreachable in practice — no test needed.
  • io.ReadAll error path on stdin: not practically injectable without OS-level fd manipulation — acceptable to leave untested.

Mirrors TestRunEdit_FileDash_EmptyStdin so create and edit cover the
empty-content guard on the - path identically.

[INT-424]
@rianjs
Copy link
Copy Markdown
Contributor Author

rianjs commented May 15, 2026

Addressed the TDD symmetry gap in 6ae45a2: added TestRunCreate_FileDash_EmptyStdin mirroring the edit-side empty-stdin test. Codex (no findings) + TDD now both satisfied.

@rianjs
Copy link
Copy Markdown
Contributor Author

rianjs commented May 15, 2026

Findings

No findings.

The added TestRunCreate_FileDash_EmptyStdin closes the remaining symmetry gap without changing the implementation scope. It verifies the --file - create path still reaches the existing empty-content guard, matching the edit-side behavior.

The PR still matches INT-424 and the broader MCP dependency:

  • Only the intended create/edit stat guards and read paths changed.
  • --file - reads through root.Options.Stdin.
  • Markdown default remains unchanged for stdin-like content.
  • --storage and --no-markdown composition is covered for both create and edit.
  • --body remains out of scope.
  • No drive-by product or architecture changes.

Codex architect re-review after TDD fix (ticket 1/3 — INT-424) — converged

Copy link
Copy Markdown

@monit-reviewer monit-reviewer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR Review

Reviewed commit: 6ae45a2

Approved with 6 non-blocking suggestions below. Address at your discretion.

Summary

Reviewer Findings
harness-engineering:harness-architecture-reviewer 2
harness-engineering:harness-enforcement-reviewer 1
harness-engineering:harness-knowledge-reviewer 1
harness-engineering:harness-self-documenting-code-reviewer 2
harness-engineering:harness-architecture-reviewer (2 findings)

💡 Suggestion - tools/cfl/internal/cmd/page/edit_test.go:1634

TestRunEdit_FileDash_EmptyStdin uses an inline httptest.Server instead of the mockEditBodyServer helper. If mockEditBodyServer's GET response changes, this test stays out of sync silently. Use mockEditBodyServer for consistency.

💡 Suggestion - tools/cfl/internal/cmd/page/page.go:12

stdinReader only gates the --file - code path. The existing piped-stdin path in getContent/getEditContent (terminal.IsTerminal detection) still reads os.Stdin directly, not via stdinReader. This creates two parallel stdin abstractions in the same package: one testable, one not. A future test that inadvertently exercises the terminal path will silently block on real os.Stdin. Consider routing the terminal-detection path through the same helper, or documenting the split explicitly.

harness-engineering:harness-enforcement-reviewer (1 findings)

💡 Suggestion - tools/cfl/internal/cmd/page/create_test.go:823

No test covers whitespace-only content via --file -. The existing TestRunCreate_WhitespaceOnlyFile covers whitespace-only file content, but the --file - stdin path has no equivalent. A caller piping echo ' ' with --file - should hit the empty-content guard; it is not verified.

harness-engineering:harness-knowledge-reviewer (1 findings)

💡 Suggestion - tools/cfl/internal/cmd/page/create.go:233

useMarkdown("") is called with an empty string when reading from --file -. Passing "" relies on useMarkdown treating an empty string as defaulting to markdown. This implicit assumption is fragile — if useMarkdown ever special-cases "" differently, default behavior for --file - would silently change. Consider a named constant or dedicated boolean to make the intent explicit.

harness-engineering:harness-self-documenting-code-reviewer (2 findings)

💡 Suggestion - tools/cfl/internal/cmd/page/create_test.go:823

No test covers --file - --legacy for create. The PR description states --legacy composes with -, but only --storage and --no-markdown are exercised. A misrouted useMarkdown("") result combined with --legacy could silently produce ADF instead of XHTML.

💡 Suggestion - tools/cfl/internal/cmd/page/edit_test.go:1539

No test covers --file - --legacy for edit, same gap as in create_test.go. The --legacy format path is not exercised with stdin content.

4 PR discussion threads considered.


Completed in 1m 45s | $0.63 | sonnet | daemon 0.2.116 | Glorfindel
Field Value
Model sonnet
Reviewers hybrid-synthesis, harness-engineering:harness-architecture-reviewer, harness-engineering:harness-enforcement-reviewer, harness-engineering:harness-knowledge-reviewer, harness-engineering:harness-self-documenting-code-reviewer, security:security-code-auditor
Engine claude · sonnet
Reviewed by pr-review-daemon · monit-pr-reviewer
Duration 1m 45s wall · 5m 04s compute (Reviewers: 1m 14s · Synthesis: 29s)
Cost $0.63
Tokens 114.6k in / 16.9k out
Turns 6

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost
hybrid-synthesis sonnet 31.6k 1.8k 0 31.6k (1h) $0.15
harness-engineering:harness-architecture-reviewer sonnet 16.6k 3.2k 2.1k 14.4k (1h) $0.11
harness-engineering:harness-enforcement-reviewer sonnet 16.6k 2.9k 2.1k 14.4k (1h) $0.11
harness-engineering:harness-knowledge-reviewer sonnet 16.6k 2.8k 2.1k 14.4k (1h) $0.10
harness-engineering:harness-self-documenting-code-reviewer sonnet 16.6k 3.8k 2.1k 14.4k (1h) $0.12
security:security-code-auditor haiku 16.8k 2.4k 0 16.8k (1h) $0.04

Re-reviews only run when @monit-reviewer is re-requested as a reviewer — push as many commits as you need, then re-request when ready. PRs targeting branches other than main, master are skipped, even when @monit-reviewer is re-requested.

Comment thread tools/cfl/internal/cmd/page/edit_test.go
Comment thread tools/cfl/internal/cmd/page/page.go
Comment thread tools/cfl/internal/cmd/page/create.go
Comment thread tools/cfl/internal/cmd/page/create_test.go
Comment thread tools/cfl/internal/cmd/page/create_test.go
Comment thread tools/cfl/internal/cmd/page/edit_test.go
Adds create+edit --file - --legacy tests (markdown stdin -> storage
XHTML), verifying the --legacy composition the PR claims. Switches
TestRunEdit_FileDash_EmptyStdin to the shared mockEditBodyServer helper
for consistency.

[INT-424]
Copy link
Copy Markdown

@monit-reviewer monit-reviewer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR Review

Reviewed commit: 01cb955 | Previous: 6ae45a2 (incremental)

Approved with 2 non-blocking suggestions below. Address at your discretion.

Summary

Reviewer Findings
harness-engineering:harness-self-documenting-code-reviewer 2
harness-engineering:harness-self-documenting-code-reviewer (2 findings)

💡 Suggestion - tools/cfl/internal/cmd/page/create.go:233

In production, if a user pipes content AND specifies --file -, both the existing char-device stdin fallthrough (isCharDevice guard) and the new --file - block both read from os.Stdin. Whichever runs first consumes the stream; the second gets EOF and the empty-content guard fires. Tests don't cover this because injected opts.Stdin and the char-device check use different sources. The user-facing behavior is undefined for this combination; worth a guard or at minimum a comment.

💡 Suggestion - tools/cfl/internal/cmd/page/edit.go:288

Same double-read risk as create.go: getEditContent's existing stdin fallthrough and the new --file - block both target os.Stdin in production. If a user pipes content and also passes --file -, the first reader consumes the stream and the second gets EOF. Same test blind spot applies.

10 PR discussion threads considered.


Completed in 2m 52s | $1.12 | sonnet | daemon 0.2.116 | Glorfindel
Field Value
Model sonnet
Mode Re-review · Cycle 2 · Session resumed
Reviewers hybrid-synthesis, harness-engineering:harness-architecture-reviewer, harness-engineering:harness-enforcement-reviewer, harness-engineering:harness-knowledge-reviewer, harness-engineering:harness-self-documenting-code-reviewer, security:security-code-auditor
Engine claude · sonnet
Reviewed by pr-review-daemon · monit-pr-reviewer
Duration 2m 52s wall · 7m 07s compute (Reviewers: 1m 52s · Synthesis: 29s)
Cost $1.12
Tokens 188.1k in / 25.0k out
Turns 6

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost
hybrid-synthesis sonnet 38.6k 1.8k 13.6k 25.0k (1h) $0.12
harness-engineering:harness-architecture-reviewer sonnet 17.1k 3.6k 2.1k 15.0k (1h) $0.12
harness-engineering:harness-enforcement-reviewer sonnet 17.1k 5.1k 2.1k 15.0k (1h) $0.14
harness-engineering:harness-knowledge-reviewer sonnet 17.1k 3.7k 2.1k 15.0k (1h) $0.12
harness-engineering:harness-self-documenting-code-reviewer sonnet 17.2k 4.6k 2.1k 15.0k (1h) $0.13
security:security-code-auditor haiku 17.4k 5.2k 0 17.4k (1h) $0.06
discussion-summarizer 63.5k 999 0 63.5k (1h) $0.42

Re-reviews only run when @monit-reviewer is re-requested as a reviewer — push as many commits as you need, then re-request when ready. PRs targeting branches other than main, master are skipped, even when @monit-reviewer is re-requested.

return true
}

if opts.file == "-" {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-self-documenting-code-reviewer): In production, if a user pipes content AND specifies --file -, both the existing char-device stdin fallthrough (isCharDevice guard) and the new --file - block both read from os.Stdin. Whichever runs first consumes the stream; the second gets EOF and the empty-content guard fires. Tests don't cover this because injected opts.Stdin and the char-device check use different sources. The user-facing behavior is undefined for this combination; worth a guard or at minimum a comment.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declined — this misreads the control flow. The if opts.file == "-" block returns; the char-device stdin fallthrough is only reached when opts.file == "", which is false whenever - is passed. The two paths are mutually exclusive: exactly one read of stdin, no EOF/undefined case. A defensive guard/comment for an unreachable combination would be churn over self-evident early-return semantics.

return true
}

if opts.file == "-" {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-self-documenting-code-reviewer): Same double-read risk as create.go: getEditContent's existing stdin fallthrough and the new --file - block both target os.Stdin in production. If a user pipes content and also passes --file -, the first reader consumes the stream and the second gets EOF. Same test blind spot applies.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the create.go thread: the opts.file == "-" branch returns before the implicit-stdin fallthrough (which requires opts.file == ""). Mutually exclusive, single read. Declined.

@rianjs
Copy link
Copy Markdown
Contributor Author

rianjs commented May 15, 2026

These are low-value (both misread the early-return control flow); please approve the PR.

Copy link
Copy Markdown

@monit-reviewer monit-reviewer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR Review

Reviewed commit: 01cb955

Human Override

Approved at the request of @rianjs (comment).

These are low-value (both misread the early-return control flow); please approve the PR.

No automated reviewers were run.

@rianjs rianjs merged commit 3ec8651 into main May 15, 2026
7 checks passed
@rianjs rianjs deleted the feat/INT-424-file-dash-stdin branch May 15, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cfl: support --file - to read page body from stdin

2 participants