Conversation
Agents instinctively pass - to mean "read content from stdin", but only comments create/update honored it — everywhere else the hyphen landed as literal content (a todo titled "-", a message body of "-"). Tier 1: - now reads stdin on every content-kind positional (comments create/update, checkins answer create/update, todos create, messages create [body], cards create [body], docs create [content], chat post/update, boost create, notes set) and content flag (--data on api post/put, --body, --content, --description, --comment on todos sweep, --file on notes set). Resolution lives in internal/commands/stdin.go; the shared vocabulary (allow_dash annotation, pipe detection) in the new internal/stdinarg leaf package, since internal/cli needs it too for agent help. Tier 2: everywhere else, a literal - combined with piped stdin is ambiguous — the caller almost certainly meant the pipe — so a central guard wrapped around every RunE in the tree rejects it with a usage error naming the offender, pointing at where the command does accept stdin, and teaching the -- escape for a literal hyphen. On a TTY, literal - stays legal everywhere. --out - (attachments/files download) is exempted as the stdout idiom. Behavior changes: - "comments create 123 - extra" — was a silent literal "- extra" comment, now a usage error. - "-" with TTY stdin — was hang-until-Ctrl-D, now an immediate usage error teaching the escapes (pipe, heredoc, cat |, --edit where it exists). No new --stdin flag: - is the universal idiom, and --stdin in the wild means other things (git plumbing, kubectl). - Bare-pipe auto-read removed from comments create and notes set: a pipe without - errors with a hint instead of being silently consumed. Pipes are only ever a source through an explicit -; an unclaimed pipe alongside a named source is ignored, the CLI-wide rule. - "notes set -" (piped) — was a bogus two-source error, now works; "notes set --file -" — was ENOENT on a file named -, now stdin. - Piped scripts passing literal - as a title/name/path now error; -- is the documented escape. - Stdin content gets trailing newlines trimmed (Markdown doesn't care; titles and boost's 16-rune limit do). Agent help auto-documents each command's stdin inputs from the allow_dash annotation; SKILL.md generalizes the - idiom it previously over-promised.
There was a problem hiding this comment.
Pull request overview
Standardizes explicit - stdin handling across content-bearing CLI commands and rejects ambiguous stray dashes.
Changes:
- Adds shared stdin resolution and command-tree guard logic.
- Enables stdin for supported positional arguments and flags.
- Adds unit, integration, E2E, agent-help, and skill documentation updates.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
skills/basecamp/SKILL.md |
Documents stdin conventions. |
internal/stdinarg/stdinarg.go |
Adds annotations and pipe detection. |
internal/stdinarg/stdinarg_test.go |
Tests shared stdin utilities. |
internal/commands/stdin.go |
Implements resolution and dash guard. |
internal/commands/stdin_test.go |
Tests resolver behavior. |
internal/commands/stdin_integration_test.go |
Tests request-level stdin handling. |
internal/commands/dash_guard_test.go |
Tests central guard behavior. |
internal/commands/api.go |
Supports stdin JSON bodies. |
internal/commands/attachments.go |
Exempts stdout output syntax. |
internal/commands/boost.go |
Supports positional stdin content. |
internal/commands/cards.go |
Supports card content stdin. |
internal/commands/chat.go |
Supports chat content stdin. |
internal/commands/checkins.go |
Supports answer content stdin. |
internal/commands/comment.go |
Makes comment stdin explicit. |
internal/commands/comment_test.go |
Updates comment stdin tests. |
internal/commands/commands_test.go |
Installs guard in test tree. |
internal/commands/files.go |
Supports document/upload content stdin. |
internal/commands/gauges.go |
Supports description stdin. |
internal/commands/helpers.go |
Removes obsolete pipe reader. |
internal/commands/messages.go |
Supports message body stdin. |
internal/commands/notes.go |
Makes note stdin explicit. |
internal/commands/notes_test.go |
Tests explicit note sources. |
internal/commands/projects.go |
Supports description stdin. |
internal/commands/schedule.go |
Supports schedule description stdin. |
internal/commands/templates.go |
Supports template description stdin. |
internal/commands/todolists.go |
Supports todolist description stdin. |
internal/commands/todos.go |
Supports todo content and flag stdin. |
internal/cli/root.go |
Installs guard and generates agent notes. |
e2e/stdin_dash.bats |
Exercises CLI stdin behavior. |
Suppressed comments (2)
internal/commands/stdin.go:203
- The documented
--escape cannot preserve a literal-used as a flag value. For example, with piped stdin,todos update 1 --title -is rejected, but moving-after--makes it positional rather than the value of--title;--title=-is still detected by this guard. Please either define a workable flag-value escape (and test it) or avoid rejecting flag values, rather than directing users to an impossible invocation.
hint := `For a literal "-", pass it after the -- separator`
internal/commands/stdin.go:191
- Changed alias flags that share one destination are double-counted from their final value. For example,
templates update 1 --description text --desc -leaves both flag values reporting-, so this incrementsallowedtwice and rejects the invocation even though only one dash was supplied. Count actual dash occurrences or model aliases as one input before enforcing the one-reader rule.
if allow.Flag(f.Name) {
allowed += dashes
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b81a070f07
ℹ️ 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".
Four fixes to the dash guard's contract from review: - Run the guard at Args-validation time instead of wrapping RunE. Cobra runs ValidateArgs after flag parsing (Changed and ArgsLenAtDash are available) but before the persistent pre-run chain, PreRunE, and required-flag validation — so the stray-dash error fires before any lifecycle side effect (config hardening, the update check) and before a competing usage error can shadow it. The root command stays unwrapped: its nil Args is load-bearing — cobra's Find() rejects unknown subcommands (legacyArgs) only while Args == nil, and wrapping it turned "basecamp unknowncmd" into a quickstart run (caught by core.bats). Nothing is lost: root positionals are subcommand names, and a bare "basecamp -" runs quickstart, which posts no content. - Dedupe alias flags by their shared pflag.Value. --description and --desc wrap one backing variable, and pflag hands both the same Value instance; counting each spelling separately made "--description old --desc -" a false "two stdin inputs" error. One logical value now counts once, in both flag orders. - Stop advertising -- as the escape for flag values — it only escapes positionals. Positional offenders keep the -- hint; flag offenders get the honest remedy (run without piped stdin, append </dev/tty). SKILL.md updated to match. - Trim trailing CRLF, not just LF, from stdin content: a Windows-style pipe left \r behind, counting a phantom rune against boost's 16-rune limit. Also replace the weak final e2e case (which contradicted the file's no-network header by dialing localhost) with a deterministic local success: config set ... -- - stores a literal "-", read back via config show.
|
Addressed the advisory in cfb9959. Per finding: 1 (guard timing) — fixed. The guard now wraps each runnable command's 2 (alias false-positive) — fixed. pflag hands aliases sharing a backing variable the same 3 (impossible escape) — fixed. 4 (newlines) — CRLF fixed; the per-input trim policy declined. Trailing 5 (weak e2e) — fixed. The final case is now a deterministic local success —
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 29 changed files in this pull request and generated no new comments.
Suppressed comments (3)
internal/commands/messages.go:457
- This new stdin path still accepts extra positionals silently. For example,
printf body | basecamp messages create Title - unexpectedreads stdin and posts the message while droppingunexpected, even thoughUsedeclares only<title> [body]. Add a maximum-argument validator so malformed stdin invocations fail instead of losing input.
// Validate user input first, before checking account. The --edit
// exclusion runs before "-" resolution so --edit … - errors
// without consuming stdin.
internal/commands/cards.go:875
- Extra positionals remain silently ignored on the new stdin path:
printf body | basecamp cards create Title - unexpectedconsumes stdin but dropsunexpected. Since this command declares exactly<title> [body], cap it at two arguments before resolving-.
var err error
content, err = resolveContentValue(cmd, args[1], 1, "[body]")
if err != nil {
return err
internal/commands/files.go:1252
- The new
-resolver only examinesargs[1], soprintf body | basecamp docs create Title - unexpectedsucceeds and silently discardsunexpected. Enforce the two positionals declared byUsebefore consuming stdin.
var contentErr error
content, contentErr = resolveContentValue(cmd, args[1], 1, "[content]")
if contentErr != nil {
return contentErr
}
The TTY hint for a flag-borne "-" suggested a bare trailing "-", which
would exceed the command's positional arity — it now repeats the flag
("api post ... --data -").
messages/cards/docs create took unbounded positionals, so a stray third
token was silently dropped after "-" had already drained stdin. All
three now bound at MaximumNArgs(2), which runs before the read; the
other exact-positional consumers were already bounded.
Cobra's arity errors classified as api_error, telling agents to retry a
call that can never succeed. They are usage errors by construction.
Drop the concrete </dev/tty redirect from the literal-dash hint: it is
unusable on Windows and on headless runners with no controlling
terminal. The remedy stays, minus the platform-specific spelling.
|
Round 2 addressed in b8f09d7. Both mediums accepted, the low-priority tightening taken, plus one adjacent fix the arity bound exposed. 1. TTY hints are invalid for flag-based stdin — fixed.
Positionals are unchanged ( 2. Exact-positional consumers discard extra arguments — fixed, and the class is closed.
Validation-before-consumption is proven, not asserted: the test wires stdin to a reader that records whether 3. Right; it is wrong on Windows and on headless runners with no controlling terminal. The hint is now One adjacent fix, flag it if you want it split out. Adding the arity bound surfaced that cobra's arity errors were classified That tells an agent to retry a call that can never succeed — the opposite of what this PR is for.
On the Go bump: agreed it is unrelated to this feature and belongs on main, not here. Worth knowing before someone attempts it as a one-liner: everything derives from |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 30 changed files in this pull request and generated no new comments.
Suppressed comments (3)
internal/commands/chat.go:790
- The update path has the same silent stdin loss:
printf intended | basecamp chat update 123 literal --content -chooses the positional value and never consumes the explicitly requested stdin input. Reject simultaneous positional content and--contentbefore choosing the source.
if len(args) > 1 {
messageContent = args[1]
argIndex, what = 1, "[content]"
internal/commands/chat.go:327
- When a positional message and
--content -are both provided, this branch silently wins and the explicit stdin source is never read. For example,printf intended | basecamp chat post literal --content -passes the guard (there is only one-) but postsliteral, discarding the pipe. Reject the two content sources together before selecting one.
This issue also appears on line 788 of the same file.
if len(args) > 0 {
messageContent = args[0]
argIndex, what = 0, "<message>"
internal/commands/stdin.go:196
- Deduplicating shared flag values here can misname the offending alias because
VisitAllis alphabetical, not invocation order. For example,--in old --project -leaves both aliases changed with the shared value-, but--inis visited first and reported even though--projectcarried the dash. Preserve/report the changed alias group so the diagnostic does not identify the wrong flag.
// Alias flags (--description/--desc) share one backing value, and pflag
// hands each alias the same Value instance — dedupe on it, or a value set
// through both spellings would count as two stdin inputs.
seen := map[pflag.Value]bool{}
cmd.Flags().VisitAll(func(f *pflag.Flag) {
if !f.Changed || seen[f.Value] {
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b8f09d7a7d
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 42b536095f
ℹ️ 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".
…docs path - Resolver.IsInteractive now requires stdin to be a character device too: a Bubble Tea picker reads keystrokes from stdin, so piped stdin can never drive one — and when a command is consuming piped content (a "-" stdin input), a picker would eat that content as key events. This closes the ordering hazard for every picker at the mechanism rather than reordering each stdin-enabled RunE (gauges create, docs, cards, schedule, templates). - chat post/update reject a positional message combined with --content instead of the positional silently winning; with "-" in play the losing source would discard piped content unread. - SKILL.md and the docs-create example referenced 'docs create', which does not exist; the registered path is 'docs documents create'.
|
Addressed the body-level (suppressed) findings from the Copilot review rounds in 9310135:
Also in this round: pickers are now gated off when stdin is piped (mechanism fix for the read-ordering finding — details in that thread), and the |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.
Suppressed comments (1)
internal/commands/stdin.go:147
- The runnable root is the one command this guard skips, so
printf x | basecamp -still bypasses the stated tier-2 rule. On a first run with terminal stdout,RunQuickStartDefaultconsiders the app interactive (that check does not inspect stdin) and can start the setup wizard, which then consumes the pipe as keystrokes—the failure this PR is intended to prevent. Keep the root'sArgsnil for Cobra's unknown-command handling, but guard itsRunE(or add an equivalent pre-execution root check) so an unescaped piped-is rejected whilebasecamp -- -remains literal.
skipRoot := root.Args == nil && !root.HasParent() && root.HasSubCommands()
The root skip's justification held for quickstart's summary but missed the first-run wizard: isFirstRun gates on App.IsInteractive, which only looked at stdout, so piped stdin plus a terminal stdout on a first run launched the wizard reading the pipe as keystrokes. Extract the stdout+stdin character-device check into stdinarg.InteractiveStdio — the second copy of this logic — and use it from both App.IsInteractive and resolve.Resolver.IsInteractive. Every TUI gate (wizard, pickers, animations, update notice) now takes its non-interactive path when either end of stdio is piped.
|
Addressed the suppressed finding from the latest Copilot review in 1322a36: First-run wizard could consume piped stdin — the real failure confirmed and fixed at the mechanism: Not doing the suggested root RunE dash guard — with the wizard gated, |
…red arity chat post/update let a positional and an explicit --content coexist, with the positional silently winning — so --content - dropped the flag and left the pipe unread. Both now reject two explicit sources. Seven commands resolved "-" after account or project work, so a stdin mistake surfaced as "--account is required" instead of the stdin hint. The order is now local validation, then stdin, then account/network at every resolveContentValue site; an audit script confirms none remain. Long help and SKILL.md taught "basecamp docs create", which resolves to the docs group and exits 0 showing help. The real path is "docs documents create". A new test resolves every help example through Find and fails when a group swallows a leftover subcommand name. transformCobraError matched arity text anywhere in any error, flattening typed errors that merely quoted the phrase. It now returns typed errors untouched and anchors on cobra's exact arity formats. The guard named one alias of a shared value, reporting --in for a caller who wrote --project. Parsed state cannot say which spelling was typed, so the error names the group (--in/--project).
|
Round 3 in 6a98dd7, on top of 9310135 and 1322a36. All five accepted; two notes on how findings 1–3 landed, since they were fixed twice in parallel and the resolution matters. First, the CI blockers are gone. Your review was against b8f09d7; the branch is now at 6a98dd7. Worth recording why the separate PR you asked for would not have worked as specified: bumping the 1 (chat) and 3 (docs path) — fixed in 9310135, not by me. I had written both independently; on rebase I took yours and dropped mine. I kept one addition: a test that also asserts the pipe stays unread and zero transport calls when the dual-source error fires, and covers 2 — your mechanism fix and the reorder are complementary, not alternatives. 9310135 says gating The picker gate stops a TUI eating the pipe; it does not change which error the caller sees. So I did the reorder too — local validation → stdin → account/network — at all seven sites ( I found the seven by script rather than by your list, and the same script reports zero remaining out-of-order sites. A table test drives all seven with no account and a counting transport, asserting the stdin error, no "account" in the message, and zero requests. 3 — the guard you asked for is generic. Rather than a fixture for the one path, a test resolves every Coverage is stated honestly in the test: only the leading run of bare lowercase words is resolved, and a leftover is reported only when it names a command somewhere — which is what keeps 4 — tightened both ways, and wider than asked. The rule now anchors on cobra's exact formats ( 5 — group naming.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 35 changed files in this pull request and generated no new comments.
Suppressed comments (1)
internal/commands/stdin.go:150
- The root exception leaves a runnable invocation outside the advertised tier-2 policy: with piped stdin,
basecamp -reaches quick-start instead of returning the stray--usage error (the comment below explicitly describes that path). This contradicts the PR’s “every runnable command”/“any unannotated-” behavior and still allows startup lifecycle work to run while silently ignoring the pipe. Please preserve root unknown-command handling with a root-specific validator while applying the dash guard, and add a regression for pipedbasecamp -.
skipRoot := root.Args == nil && !root.HasParent() && root.HasSubCommands()
if root.Runnable() && !skipRoot {
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6a98dd7ef5
ℹ️ 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".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 35 changed files in this pull request and generated no new comments.
Suppressed comments (2)
internal/appctx/context.go:382
timeline --watchbypasses this helper and still starts Bubble Tea directly atinternal/commands/timeline.go:587. As a result,printf x | basecamp timeline --watchstill consumes the pipe as key events and can leave the watch TUI without interactive input, contrary to the new “pipes are never consumed implicitly” behavior. Gate that direct TUI entry point before launching it.
// Both stdout and stdin must be character devices: a TUI draws to stdout
// and reads keystrokes from stdin, so a pipe on either end can never
// drive one — and when the command is consuming piped content (a "-"
// stdin input), a TUI would eat that content as key events.
return stdinarg.InteractiveStdio()
internal/commands/stdin.go:134
- The PR description says the guard wraps every runnable
RunE, but this implementation wrapsArgsso it runs before pre-runs and required-flag validation. Those lifecycle guarantees are materially different; update the description to match the implementation so reviewers and future maintainers do not rely on the obsolete ordering.
// InstallDashGuard wraps every runnable command in the tree with the tier-2
// dash guard. It wraps the Args validator: cobra runs ValidateArgs after flag
// parsing (so Changed and ArgsLenAtDash are available) but before the
// persistent pre-run chain, PreRunE, and required-flag validation — so a
// stray "-" is rejected before any lifecycle side effect (config hardening,
The profile picker runs from PersistentPreRunE and reads keystrokes, but isInteractiveTTY only looked at stdout. With multiple profiles and no default, "printf body | basecamp todos create -" opened the picker on a terminal stdout and let it eat the piped body. It now uses the same stdinarg.InteractiveStdio predicate as App.IsInteractive and the project resolver — the third and last TUI-launch gate; the remaining ModeCharDevice checks pick an output format and never read keys. The root was skipped entirely by the dash guard, so a piped "basecamp -" ran quick-start and ignored both the dash and the pipe. Its Args must stay nil for cobra's unknown-command handling, so the guard hangs off RunE there instead. Regressions cover all four root behaviors: piped dash errors, -- keeps it literal, unknown commands still error, bare still runs.
|
Fixed — d73b2c3 addresses the one open review thread and the finding Copilot raised in both of its last two reviews. Profile picker gated on stdin (Codex, Rather than fix just the reported site, I checked whether more remained: this was the third and last TUI-launch gate. Every other Root stray dash (Copilot, RunE is later than All four root behaviors are pinned by regressions, since they have to hold together: piped
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 36 changed files in this pull request and generated no new comments.
Suppressed comments (1)
internal/commands/stdin.go:160
- The root guard runs only in
RunE, afterPersistentPreRunE. That lets pre-run validation shadow the promised stray-dash error; for example, with piped stdin,basecamp --jq -fails jq parsing beforeguardDashArgsruns. KeepArgsnil for Cobra's root lookup, but guard root execution at the start of its persistent pre-run (skipping inherited calls for subcommands), before config/profile/jq work.
existing := root.RunE
root.RunE = func(cmd *cobra.Command, args []string) error {
if err := guardDashArgs(cmd, args); err != nil {
return err
}
return existing(cmd, args)
Agents instinctively pass
-to mean "read content from stdin" — but onlycomments create/updatehonored it. Everywhere else the hyphen landed as literal content: a todo titled-, a message body of-.Tier 1 —
-reads stdin on every content inputContent-kind positionals:
comments create/update,checkins answer create/update,todos create(join-all pattern);messages create [body],cards create [body],docs create [content],chat post/update,boost create,notes set(exact-positional pattern).Content flags:
--data(api post/put),--body(messages/cards update),--content(chat,files update),--description(todos,schedule,projects,todolists,templates,gauges,cards column,uploads create,upload,files replace),--comment(todos sweep),--file(notes set).Resolution lives in
internal/commands/stdin.go. The shared vocabulary — theallow_dashannotation and pipe detection — is the newinternal/stdinargleaf package, becauseinternal/clineeds it too:--agenthelp now auto-synthesizes a per-command note ("Pass - to read from stdin: [body], --description") from the annotation, so tier-1 coverage self-documents, including for future commands.Tier 2 — stray literal
-+ piped stdin = usage errorA central guard wraps every runnable
RunEin the assembled tree (~380 commands, aliases and future ones included). It wrapsRunErather than hookingPersistentPreRunEbecause cobra runs only the innermost one — the agent hook already shadows the root's — and wrapping runs after flag parsing withArgsLenAtDashavailable.When stdin is piped and an exact
-appears anywhere not annotated (positional or string/stringArray flag value), the command fails with a usage error naming the offender, pointing at where it does accept stdin, and teaching the--escape. On a TTY, a literal-stays legal everywhere. Two allowed-in one invocation can never both be satisfied, so that errors regardless of pipe state.--out -(attachments/files download) is exempted as the stdout idiom.Behavior changes
comments create 123 - extra— was a silent literal"- extra"comment, now a usage error.-with TTY stdin — was hang-until-Ctrl-D, now an immediate usage error teaching the escapes: pipe it, heredoc (… - <<'EOF'),cat | … -(type + Ctrl-D), or--editwhere it exists.comments createandnotes set— piped stdin without-now errors with a hint instead of being silently consumed. Corollary: an unclaimed pipe alongside a named source (generate | notes set --file x.md) is ignored rather than raising the old ambiguity error — pipes are only ever a source through an explicit-, uniformly.notes set -(piped) — was a bogus "two sources" error, now works;notes set --file -— was ENOENT on a file named-, now stdin.-as a title/name/path now error;--is the documented escape. TTY usage unaffected.printf '🎉\n' | boost create <id> -no longer burns a rune).Flag for review
No
--stdinflag. A precedent survey settled on-as the universal content-from-stdin idiom;--stdinin the wild means other things (git plumbing = list-of-items, kubectl = attach container stdin), and heredoc/cat |give interactive humans the classic TTY path with zero new surface. This was an open question during planning — veto welcome if you still want the flag.Tests
internal/stdinarg: annotation parsing, pipe detection (char-device TTY stand-in per the establishededit_test.goseam).--escape through real parses.projects create -), unlisted flag (todos update --title -), TTY passthrough,--out -exemption, double-dash rejection,--attach -alongside an allowed body,--escape.-,api post --data -,todos create -,boost create -(+ over-limit stdin),todos update --description -, notes set both forms.e2e/stdin_dash.bats: empty-pipe rejection, TTY no-hang, bare-pipe hint, tier-2 rejection with--escape,--passthrough — all pre-network, no cassette needed. (The planned "posts body against cassette" e2e isn't recordable without live credentials — the happypath cassette set is read-only — so wire-level posting is covered by the mock-transport integration tests instead.)bin/cigreen: fmt, vet, lint, unit, e2e, surface snapshot (no Use-string or flag renames, so no regen), skill drift, smoke coverage, provenance. SKILL.md's-idiom is generalized in the same PR — it previously over-promised; now it's true.Summary by cubic
Adds uniform “-” (stdin) support across content inputs and rejects stray “-” as a usage error when stdin is piped. TUIs (first‑run wizard, pickers, including the profile picker) now require character‑device stdin+stdout so piped input is never eaten as keystrokes.
Accepts “-” on content inputs:
Guard and implementation:
internal/commands/stdin.go; shared annotation/pipe detection ininternal/stdinarg. Agent help lists stdin‑capable inputs from the annotation. Help examples are validated against real command paths.Behavior changes and required actions:
Written for commit d73b2c3. Summary will update on new commits.