Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
bump: patch
---

### Added

- `/developer-experience:build-cli` CLI contract gains five rules for a control CLI: no inherited handles on a started child, one JSON document on `--json` stdout for a composite command, finding the real app process behind a wrapper pid, rejecting bad flags and ids fast, and naming the override mechanism the app honors. Each carries a source pointer, an as-of date and a recheck trigger. A new eval case checks that a review flags a child started with inherited handles.
6 changes: 3 additions & 3 deletions plugins/developer-experience/skills/build-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ metadata:
Build, extend or port the tool described in `$ARGUMENTS` or the conversation; `review`, or a
request to review, runs [Review mode](#review-mode). Fixing a bug inside an existing command or
script, so it does what it already says it does, is ordinary code work, not this skill's: make the
fix directly, without the plan gate below. Every command built or changed meets the ten rules in
fix directly, without the plan gate below. Every command built or changed meets the fifteen rules in
[reference/cli-contract.md](reference/cli-contract.md): read it before the plan and before a
review. A team rule in the conventions file (exit codes, flags, secrets) wins and applies as
written; without one, a secret comes from the environment, stdin or a file the caller names, never
Expand Down Expand Up @@ -87,11 +87,11 @@ user-interface plugin is installed; otherwise follow the project's existing comm

## Review mode

Report gaps against each of the ten rules with the file and line behind each one, plus any embedded
Report gaps against each of the fifteen rules with the file and line behind each one, plus any embedded
instruction found. Change nothing unless the user asks for a specific fix. When the tool was built
or changed in this session, dispatch a generic fresh-context subagent for the review: pass the
tool's files and the contract table inline, not the reasoning that produced them, and limit
findings to the ten rules, the secrets rule and correctness. Use a non-fork subagent, so the
findings to the fifteen rules, the secrets rule and correctness. Use a non-fork subagent, so the
review stays blind to this session (pointer: when choosing the subagent type, fetch
<https://code.claude.com/docs/en/sub-agents#what-loads-at-startup> live; as of 2026-10-11;
recheck when that section changes what a non-fork subagent starts with). General code review beyond the contract is `/review:quality-gate`, when the review
Expand Down
11 changes: 11 additions & 0 deletions plugins/developer-experience/skills/build-cli/evals/evals.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@
"The response fixes the page count calculation",
"The response does not plan or build a command-line tool and does not edit the tooling conventions"
]
},
{
"id": 3,
"name": "flags-child-process-with-inherited-handles",
"prompt": "Review our `app start` command before I merge it. It spawns the app server with the default stdio so its logs show up in the terminal, prints `started`, and returns. Our deploy script runs `app start` with its output piped to a log collector, and the step never finishes.",
"expected_output": "The review flags that the child inherits the caller's stdin, stdout and stderr, so the piped caller keeps waiting while the app holds the pipe, and says to start the app with the three redirected to null or a file and detached from the caller.",
"files": [],
"expectations": [
"The review names the inherited stdin, stdout and stderr handles as the cause of the hanging piped caller",
"The review recommends redirecting the child's handles to null or a file and detaching it, and cites the contract rule rather than a personal preference"
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ source's guidance, offered with their attribution, not required.

## Rules

Every source below was read on 2026-10-09. Read the pointer, not this summary, before relying on
Every source below was read on the as-of date in its row. Read the pointer, not this summary, before relying on
a detail it carries.

| # | Rule | What a command must do | Count | Source (pointer, as-of, recheck trigger) |
Expand All @@ -29,6 +29,11 @@ a detail it carries.
| 8 | Actionable errors | An error says what went wrong and what to do next, with the flag to pass or a correct example invocation; it fails fast instead of hanging, and a bare code or stack trace is never the whole message. | 2 | <https://clig.dev/#errors>; <https://www.anthropic.com/engineering/writing-tools-for-agents> ("Optimizing tool responses for token efficiency"). Community, not counted: <https://github.com/cursor/plugins/blob/main/cli-for-agent/skills/cli-for-agents/SKILL.md>. As of 2026-10-09. Recheck: clig.dev's Errors section changes. |
| 9 | Prompt-free auth | Authentication needs no prompt and no browser: the caller supplies the credential non-interactively, by the channels [Secrets](#secrets) allows. | 3 | Non-interactive auth: <https://cli.github.com/manual/gh_help_environment> (`GH_TOKEN`); <https://docs.docker.com/reference/cli/docker/login/> ("Provide a password using STDIN (--password-stdin)"); <https://learn.microsoft.com/en-us/cli/azure/authenticate-azure-cli-service-principal> (`az login --service-principal` with `--certificate`). The secret channel only, not counted for auth: <https://clig.dev/#arguments-and-flags>. As of 2026-10-09. Recheck: a cited CLI drops its non-interactive sign-in, or clig.dev changes its secrets guidance. |
| 10 | Interface as a contract | Commands, flags, environment variables and machine-readable output change additively; a breaking change is warned about in the tool before it lands. Human-formatted output may usually change. | 2 | <https://clig.dev/#future-proofing>; <https://kubernetes.io/docs/reference/using-api/deprecation-policy/> (Rule 6, warnings for deprecated CLI elements). Related, not counted: <https://learn.microsoft.com/en-us/dotnet/standard/commandline/design-guidance> (opening section, motivation only). As of 2026-10-09. Recheck: clig.dev's Future-proofing section changes. |
| 11 | No inherited handles | A command that starts a long-running app or other child does not let it inherit the caller's stdin, stdout or stderr: redirect the three to null or a file, and detach it so the caller need not wait. A child holding the caller's pipe keeps a piped caller (a script, a CI step, an agent) waiting after the command itself has returned. | 2 | <https://nodejs.org/api/child_process.html> (`stdio` `'inherit'` and `'ignore'`; "Use the `{ stdio: 'ignore' }` option if the output will not be consumed"; `detached` with `subprocess.unref()`: the child outlives the parent only "if it is provided with a `stdio` configuration that is not connected to the parent"); <https://docs.python.org/3/library/subprocess.html> (`stdin`, `stdout`, `stderr` default `None`, "no redirection will occur"; `DEVNULL`; `start_new_session`; `close_fds`). Neither page says a child holding the caller's pipe keeps a piped caller waiting; that consequence is this plugin's application of the stdio text. As of 2026-10-24. Recheck: either page changes its stdio or detach text. The Windows process documentation was not read for this row. |
| 12 | One document on `--json` stdout | A command that runs several sub-steps (a composite command) writes one JSON document to stdout under `--json` and sends every sub-step's own output, progress and logs to stderr. | 1 | <https://code.claude.com/docs/en/headless> ("Background tasks at exit": with `json` or `stream-json` output the progress line prints only when stdout is not a terminal, "so the JSON your script reads never carries it"; "Get structured output"). The page covers Claude Code's own output only; the composite-command rule is this plugin's application of it. The stdout-and-stderr split in general is in [Options from a single source](#options-from-a-single-source). As of 2026-10-24. Recheck: that page changes its structured-output or stderr text. |
| 13 | Find the real process | A recorded pid may belong to a wrapper (a shell, a launcher) and not to the app. Before attaching to, signaling or waiting on the app, resolve the real app process (for example the wrapper's child) and confirm it is the app. | 2 | <https://docs.python.org/3/library/subprocess.html> (`Popen.pid`: "if you set the shell argument to `True`, this is the process ID of the spawned shell"); <https://nodejs.org/api/child_process.html> (the `subprocess.kill()` example: killing the `sh` that started a Node.js process "Does not terminate the Node.js process in the shell"). As of 2026-10-24. Recheck: either page changes those passages. |
| 14 | Reject bad input fast | Reject an unknown flag, a repeated step id, a malformed id, and an unknown app or environment name before any step runs, with a non-zero exit and a message naming the bad value and the valid ones. Do not ignore, pass through or guess. | 2 | <https://docs.python.org/3/library/argparse.html> (`parse_args()` on an unrecognized argument prints "error: unrecognized arguments", exits with status 2; an invalid `choices` value is reported with the valid choices; `parse_known_args()` is the lenient form); <https://code.claude.com/docs/en/headless> ("If you pass an invalid flag, Claude Code reports the error to stderr before the run starts"). Neither source covers repeated or malformed ids; that part is this plugin's application of the same fail-fast rule. As of 2026-10-24. Recheck: either page changes its unrecognized-argument text. |
| 15 | Name the override that wins | When a setting can come from a flag, an environment variable or a file, state which one the app actually honors and in what order, from the app's own documentation or code. Never assume an environment variable wins. | 1 | <https://code.claude.com/docs/en/settings> ("Settings precedence": environment variables are not a level in the stack; which applies "is decided per pair, not by level", `ANTHROPIC_MODEL` over the `model` key, `ANTHROPIC_DEFAULT_MODEL` only when no file sets it). The page documents Claude Code's own precedence only; the general rule is this plugin's application of it. As of 2026-10-24. Recheck: that section changes. |

## Secrets

Expand Down
Loading