Repository navigation
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #454 +/- ##
==========================================
+ Coverage 94.60% 94.80% +0.20%
==========================================
Files 15 15
Lines 1389 1406 +17
Branches 186 190 +4
==========================================
+ Hits 1314 1333 +19
- Misses 46 47 +1
+ Partials 29 26 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
It is a user-facing behavior change to CLI argument handling and exit-code semantics (previously-tolerated formats and env defaults now hard-fail at parse time), which warrants a human maintainer's compatibility sign-off despite being well-tested.
Review effort: Balanced
Findings: None
What changed in this PR
This PR makes con-duct run reject a malformed --summary-format at argparse parse time (exit 2, one-line error, no traceback) instead of crashing after the command runs and clobbering its exit code. It removes the silent str fallback in SummaryFormatter.format_field that used to mask bad specs, and adds a defensive guard around the post-run render so a render failure never costs the user a completed (potentially expensive) run. This implements the "fail fast, but never punish a finished run" philosophy now codified in CLAUDE.md, closing #192.
Changes:
- Add
validate_summary_format+VALIDATE_SUMMARY_PLACEHOLDERSand wire them into a new argparsetype=_summary_format_type, which also validates theDUCT_SUMMARY_FORMAT/.envdefault and emits a conversion-order hint for{field!conv:spec}. - Drop the
ValueError→strfallback informat_field; guard the post-run render inexecute()to log an error and preserve the command's exit code and logs. - Add tests for parse-time rejection, env-var rejection, the conversion-order hint, placeholder/type parity with a real run, and exit-code preservation on post-run render failure.
| File | Description |
|---|---|
src/con_duct/_formatter.py |
Removes silent str fallback; adds VALIDATE_SUMMARY_PLACEHOLDERS and validate_summary_format to render a template against typed placeholders. |
src/con_duct/cli.py |
Adds _summary_format_type argparse type (with conversion-order hint) and applies it to --summary-format. |
src/con_duct/_duct_main.py |
Wraps the post-run summary render in try/except, logging an error while preserving the command's exit code and logs. |
test/test_formatter.py |
Adds spec-mismatch, default-validation, and placeholder-parity tests; updates the conversion-order expectation to ValueError. |
test/test_cli.py |
Adds parse-time rejection, env-var rejection, pass-through, and conversion-order-hint tests. |
test/duct_main/test_execution.py |
Adds a test that a post-run render failure keeps the command's exit code and logs. |
CLAUDE.md |
Documents the fail-fast-before-run vs. loud-fallback-after-run convention. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-Authored-By: Claude Code 2.1.285 / Claude Opus 5.5 <noreply@anthropic.com>
The summary template was only rendered after the command finished, so a typo cost the whole run and its traceback replaced the command's exit code. Render it against typed placeholder values at parse time instead; argparse reports the error and exits 2. Since argparse also applies type= to string defaults, a bad DUCT_SUMMARY_FORMAT (env or .env) is caught the same way. Co-Authored-By: Claude Code 2.1.285 / Claude Opus 5.5 <noreply@anthropic.com>
SummaryFormatter.format_field caught ValueError and printed the raw value
with a warning, silently discarding the user's spec (e.g. {peak_rss:.2d}
printed 123456). With the template now validated before the command
runs, a bad spec fails there for free, so fail instead of guessing
(con#192).
Validation renders with colors both off and on, since the custom
conversions return colored strings: {wall_clock_time!X:.3f} (Python's
conversion-first order) works without colors but not with them. The
error suggests duct's {field:spec!conv} order when it spots the other.
Co-Authored-By: Claude Code 2.1.285 / Claude Opus 5.5 <noreply@anthropic.com>
execute() renders --summary-format after the command finished; an exception there replaced the command's exit code with 1. Log an error (pointing at info.json when it was written) and return the command's exit code. The CLI now validates the template up front, but execute() can be called directly, and after the run a duct failure must not cost the user the run. Co-Authored-By: Claude Code 2.1.285 / Claude Opus 5.5 <noreply@anthropic.com>
508b6ad to
73106ca
Compare
Co-Authored-By: Claude Code 2.1.319 / Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Note
Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.
🟡 Changes recommended
A moderate-severity issue remains in validate_summary_format() where placeholder-based rendering fails to reliably reject templates using attribute or index access (e.g., {command[0]}, {working_directory.__class__}), allowing unintended formats to pass validation.
1 open issue
Review effort: Balanced
| def validate_summary_format(summary_format: str) -> str: | ||
| """Render ``summary_format`` against placeholder values, raising if it is invalid. | ||
|
|
||
| Renders with colors both off and on: the custom conversions return colored | ||
| strings, so e.g. ``{wall_clock_time!X:.3f}`` fails only with colors. | ||
|
|
||
| Args: | ||
| summary_format (str): Template as given to --summary-format. | ||
|
|
||
| Returns: | ||
| str: ``summary_format`` unchanged, so this can serve as an argparse ``type``. | ||
| """ | ||
| for enable_colors in (False, True): | ||
| SummaryFormatter(enable_colors=enable_colors).format( | ||
| summary_format, **VALIDATE_SUMMARY_PLACEHOLDERS | ||
| ) | ||
| return summary_format |
There was a problem hiding this comment.
Good catch, though for a slightly different reason: the goal was for validation to match the post-run render, and {command[0]} renders fine there too. The real gap is that index access can depend on the value, not just its type ({command[20]} fails on the 7-character placeholder but may work on a real command), so a placeholder can't vouch for it. fcca2a1 now rejects attribute and index access in field names outright; they can be added explicitly later if someone needs them.
Generated by Claude Code
Validation renders the template against one typed placeholder per
field, which vouches for format specs because their errors depend
only on the value's type. Attribute and index access break that:
{command[0]} passes on a placeholder, and whether {command[20]}
renders depends on the actual command. Reject both outright; if
someone needs them, they can be added explicitly.
Co-Authored-By: Claude Code 2.1.319 / Claude Opus 5.5 <noreply@anthropic.com>
|
@yarikoptic I've had a look at this and I think its good to go. Requested review if you'd like, otherwise I'll plan to merge in a couple days if you dont have the time. |
| VALIDATE_SUMMARY_PLACEHOLDERS: dict[str, Any] = { | ||
| "exit_code": 0, | ||
| "command": "cmd arg", | ||
| "logs_prefix": ".duct/logs/prefix_", |
There was a problem hiding this comment.
I do not get what this structure is for, in particular why it duplicates some configurable values like this?
There was a problem hiding this comment.
in particular how that relates to the PR subject line of "Reject a bad --summary-format before running the command" -- how any particular value here is helpful to validate??
There was a problem hiding this comment.
It's a dummy. At the start we render this through the summary format so we catch any issues with the user provided summary format before we start executing the command

A malformed
--summary-formatis now rejected before the command runs (exit 2, one-line error, no traceback) instead of crashing after the run and replacing the command's exit code. Thestrfallback that silently discarded a bad format spec is removed.Changes
--summary-formatgets an argparsetype=that renders the template against typed placeholder values (VALIDATE_SUMMARY_PLACEHOLDERS), once with colors off and once on. argparse appliestype=to string defaults too, so a badDUCT_SUMMARY_FORMAT(env or.env) is caught the same way; the error names all three sources.SummaryFormatter.format_field:{peak_rss:.2d}used to print the raw123456plus a warning; it now fails validation.execute(): if rendering still fails (e.g.execute()called directly), log an error and return the command's exit code.{field!conv:spec}order: duct's conversions return colored strings, so{wall_clock_time!X:.3f}fails with colors; the error suggests{wall_clock_time:.3f!X}.Example:
Behavior changes to note
{wall_clock_time:d},{start_time:%Y},{average_rss:,d}), and{field!X:spec}when it only works without colors.{command[0]}) are rejected, even where the value supports them.test_execution_summary_formatted_wall_clock_time_nowvalidpinned the fallback for{wall_clock_time!X:.3f}with colors; it now expects theValueError.Verification
tox -e py313: 519 passed after rebasing on main;tox -e lint,typingclean; CI green on all platforms.Report'sexecution_summaryafter two samples (mutation-checked: an intaverage_rssplaceholder fails it);execute()with a bad template returns the command's exit code and keeps the logs.Why placeholders, and why the type test
A spec error depends only on the value's type (
Nonerenders as-before the spec is applied), so rendering against one correctly typed value per key reproduces exactly what the post-run render would do, no stricter and no looser. Python accepts float specs on ints ({peak_rss:.2f}) but notdon floats, and the validator inherits that exactly. The type test keeps the placeholders honest as summary fields change.Attribute and index access in a field name (
{command[0]},{peak_rss.real}) are rejected before rendering: whether they work can depend on the value, not just its type ({command[20]}fails on the placeholder but may work on a real command), so a placeholder cannot vouch for them. They can be added explicitly later if someone needs them.except Exceptionin the argparse type: any failure to render known-good placeholders means the template is bad, whatever the exception type; catching a fixed list left some as a traceback or argparse's generic "invalid value" message.🤖 Generated with Claude Code