Conversation
- `--plan` / `-P`: print a dry-run plan showing which steps would run, which would be skipped, and why — without executing any commands. - `--why [STEP]` / `-W`: show all reasons per step. Pass a step name to focus on one; omit the value to show details for every step. Implies `--plan`. - `--json` / `-J`: emit the plan as JSON (requires `--plan`) for tooling. The plan is built via static analysis (reusing `build_step_jobs` and `profile_skip_reason`) so it never executes step commands. It reports: step status (included/skipped), parallel group membership, dependencies, matched file count, and a structured list of reasons (filter match, profile include/exclude, condition true/false, CLI/env/config skip, missing required env, no command for run type). Removes the previous `--plan` stub that panicked via `todo!()`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Correctness fixes (reported by reviewers on #848): - Mirror runtime profile-skip when `job_condition` is set. `build_step_jobs` defers profile checks to runtime when a condition is present (job_builder.rs:153) so the old plan reported such steps as Included. `analyze_step` now calls `profile_skip_reason` explicitly in that path. - Match runtime truthiness for `condition`. The runtime only skips on a literal `Bool(false)` — non-boolean values (e.g. strings from `exec()`) are truthy. Plan used `as_bool().unwrap_or(false)` which treated them as falsy and reported Skipped incorrectly. - Evaluate `step_condition` (was ignored). The runtime checks it in execution.rs before building jobs; the plan now mirrors that. Flag fix: - `--json` no longer requires `--plan` at the clap level, so `--why --json` works. Validated in code: errors with "--json requires --plan or --why" otherwise. Refactors: - Extract `build_skip_steps` and `build_expr_ctx` helpers used by `plan`, `stats`, and `run`. - Add `RunType::as_str()` and `Display` so the "check"/"fix" match is no longer duplicated. Adds five regression bats tests covering each correctness fix and the `--why --json` / `--json` without `--plan` cases. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
build_step_jobs populates job.files before applying skip_reason (see job_builder.rs lines 81-158), so profile-skipped steps still have a meaningful matched file count. The plan was hardcoding zero. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Replace `Ok(val) if val == expr::Value::Bool(false)` with the direct pattern `Ok(expr::Value::Bool(false))`. The guard-style match was the newly-introduced CI failure on `mise run lint`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
analyze_step pushed ConditionTrue first when a truthy condition passed, so print_plan's headline (reasons.first()) contradicted the icon when the step was later skipped for another reason (e.g. no files matched). print_plan now picks the first skip-flavored reason as the headline when status is Skipped; verbose --why still shows every reason. Adds ReasonKind::is_skip to classify reason kinds. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
`print_plan` picks the headline reason by finding the first skip-flavored entry, which may be at any index. The verbose detail loop used `.skip(1)`, so when the headline came from index > 0 it silently dropped the first reason and reprinted the headline. Track the headline's index and `continue` past it instead. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The top-level CLI has a global --json flag for trace/log output. My prior validation (\"--json requires --plan or --why\") rejected \`hk --trace --json check\` because clap populates both the global \`args.json\` and the subcommand-level \`opts.json\` for the shared flag name. Remove the validation. --json alone continues to mean global JSON output (unchanged), and is only interpreted as plan-JSON when --plan or --why is also set (in which case plan() is entered). Drops the now-invalid regression test. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…m variant - JSON output of \`--why <step> --json\` was ignoring the step focus and emitting every step. It now retains only the focused step (and its parallel group entry), matching the text renderer. - Remove unused \`ReasonKind::Always\` — never constructed. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…e --json The previous commit dropped the validation to unbreak `hk --trace --json check`, but that made `hk check --json` silently run the hook with no JSON output. Reinstate the guard, gated on whether the user actually asked for tracing: if `--trace`, `HK_TRACE=json`, or `HK_JSON=1` is set, the global --json is legitimately active and we skip the error. Otherwise a bare `--json` without --plan/--why errors with "--json requires --plan or --why". Adds `CliSnapshot.trace` and `Settings::cli_trace()` to plumb the top-level --trace flag into HookOptions. Regression tests: both the error case and the `hk --trace --json` happy path are covered. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
`--why <step> --json` filtered `plan.steps` but left the retained group's `step_ids` pointing at siblings that were filtered out, producing dangling references for tooling that follows stepIds. Now also retain only the kept step names in each group and drop groups that become empty. Extends the regression test with a 3-step config so the group check actually has something to prune. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Greptile SummaryThis PR bumps the Confidence Score: 5/5This PR is safe to merge — it is a straightforward dependency rename with no logic changes. All changes are mechanical renaming of No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Cargo.toml] -->|was: communique| B[communique crate v0.x]
A -->|now: clx = 2.0| C[clx crate v2.0]
C --> D[clx::progress - ProgressJob, ProgressOutput, etc.]
C --> E[clx::osc - terminal OSC configure]
D --> F[src/main.rs]
D --> G[src/cli/mod.rs]
D --> H[src/hook.rs]
D --> I[src/step_group.rs]
E --> G
J[mise.lock] -->|tool: communique 0.1.9 → 1.0.1| K[communique CLI tool]
K -->|generates| L[docs/cli/*.md]
K -->|generates| M[hk.usage.kdl]
Reviews (1): Last reviewed commit: "chore: bump communique to 1.0.1" | Re-trigger Greptile |
There was a problem hiding this comment.
Code Review
This pull request introduces a robust planning and introspection feature for hooks, adding the --plan, --why, and --json CLI flags. These allow users to preview execution steps and understand the inclusion or exclusion logic for each step. The changes include a new plan module for serialization, a detailed analysis engine in hook.rs, and a suite of integration tests. The review feedback identifies a logic bug regarding empty job sets, the use of unstable Rust let-chains that would break stable builds, and opportunities to simplify code using newly implemented trait extensions.
| } | ||
| }; | ||
|
|
||
| let all_skipped = !jobs.is_empty() && jobs.iter().all(|j| j.skip_reason.is_some()); |
There was a problem hiding this comment.
The logic for all_skipped incorrectly assumes that an empty jobs vector means the step is included. If build_step_jobs returns an empty vector (e.g., when no files match and no specific skip reason is generated), the step should be considered skipped, not included. This results in steps with 0 matching files being marked with a success checkmark in the plan output.
| let all_skipped = !jobs.is_empty() && jobs.iter().all(|j| j.skip_reason.is_some()); | |
| let all_skipped = jobs.is_empty() || jobs.iter().all(|j| j.skip_reason.is_some()); |
| // in runner.rs. Mirror that here so a profile-skipped step isn't | ||
| // reported as Included. | ||
| if step.job_condition.is_some() | ||
| && let Some(reason) = step.profile_skip_reason() |
There was a problem hiding this comment.
This line uses the unstable let-chains feature (&& let Some(...)). Unless this project is strictly targeting nightly Rust, this will cause a compilation error on stable toolchains. Consider using a nested if let or matches! instead.
| && let Some(reason) = step.profile_skip_reason() | |
| if step.job_condition.is_some() { | |
| if let Some(reason) = step.profile_skip_reason() { | |
| reasons.push(skip_reason_to_reason(&reason)); | |
| return (StepStatus::Skipped, reasons, Some(file_count)); | |
| } | |
| } |
| let mut last_group: Option<String> = None; | ||
| for step in &plan.steps { | ||
| if let Some(focus) = &focus_step | ||
| && focus != &step.name |
There was a problem hiding this comment.
| continue; | ||
| } | ||
| if step.parallel_group_id != last_group | ||
| && let Some(gid) = &step.parallel_group_id |
There was a problem hiding this comment.
Another instance of an unstable let-chain. This should be refactored to support stable Rust toolchains.
| && let Some(gid) = &step.parallel_group_id | |
| if step.parallel_group_id != last_group { | |
| if let Some(gid) = &step.parallel_group_id { | |
| println!(" {} {}", style::ndim("[parallel group]"), style::ndim(gid)); | |
| } | |
| } |
| let rt_str = match rt { | ||
| RunType::Check => "check", | ||
| RunType::Fix => "fix", | ||
| }; | ||
| ( |
There was a problem hiding this comment.
This match block is redundant now that RunType implements Display and provides an as_str() method. You can simplify the formatting logic.
| let rt_str = match rt { | |
| RunType::Check => "check", | |
| RunType::Fix => "fix", | |
| }; | |
| ( | |
| SkipReason::NoCommandForRunType(rt) => ( | |
| ReasonKind::NoCommand, | |
| Some(format!("no command defined for run type: {rt}")), | |
| ), |
### 🚀 Features - **(check)** implement --plan, --why, and --json by [@jdx](https://github.com/jdx) in [#848](#848) - **(cocogitto)** add cocogitto conventional commits config to hk builtin config by [@hituzi-no-sippo](https://github.com/hituzi-no-sippo) in [#838](#838) - **(git)** support GIT_DIR/GIT_WORK_TREE for bare-repo dotfile managers by [@jdx](https://github.com/jdx) in [#847](#847) - **(install)** use Git 2.54 config-based hooks with --global support by [@jdx](https://github.com/jdx) in [#853](#853) ### 🐛 Bug Fixes - use text progress in CI by [@jdx](https://github.com/jdx) in [#845](#845) ### 📚 Documentation - generalize agent guidelines by [@jdx](https://github.com/jdx) in [#846](#846) - add releases nav and aube lock by [@jdx](https://github.com/jdx) in [#849](#849) ### 🔍 Other Changes - bump communique to 1.0.1 by [@jdx](https://github.com/jdx) in [#850](#850) ### 📦️ Dependency Updates - update actions-rust-lang/setup-rust-toolchain digest to 2b1f5e9 by [@renovate[bot]](https://github.com/renovate[bot]) in [#832](#832) - update anthropics/claude-code-action digest to c3d45e8 by [@renovate[bot]](https://github.com/renovate[bot]) in [#833](#833) - update rust crate tokio to v1.52.1 by [@renovate[bot]](https://github.com/renovate[bot]) in [#834](#834) - update actions/upload-pages-artifact action to v5 by [@renovate[bot]](https://github.com/renovate[bot]) in [#835](#835) - update taiki-e/upload-rust-binary-action digest to f0d45ae by [@renovate[bot]](https://github.com/renovate[bot]) in [#839](#839) - update rust crate clx to v2 by [@renovate[bot]](https://github.com/renovate[bot]) in [#836](#836) - update anthropics/claude-code-action digest to 0d2971c by [@renovate[bot]](https://github.com/renovate[bot]) in [#841](#841) - update anthropics/claude-code-action digest to 38ec876 by [@renovate[bot]](https://github.com/renovate[bot]) in [#842](#842) - lock file maintenance by [@renovate[bot]](https://github.com/renovate[bot]) in [#851](#851) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk release bookkeeping: version bumps and doc/CLI artifact updates, plus minor dependency patch updates in `Cargo.lock`. No functional Rust source changes are included in this diff. > > **Overview** > Bumps `hk` to **v1.44.0** and publishes the corresponding release notes in `CHANGELOG.md`. > > Updates generated/packaged artifacts to match the new version (CLI docs/specs and Pkl package URLs in docs/examples), and refreshes `Cargo.lock` for the release (including patch-level updates like `rustls` and `winnow`). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a36c7a6. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: mise-en-dev <[email protected]>
Summary
🤖 Generated with Claude Code
Note
Medium Risk
Adds new CLI flags and a new plan-generation path that changes how hooks can be invoked (
--whyimplies planning and--jsonis validated), which could affect user workflows and output compatibility. Includes a tool version bump (communique) but primary risk is in new planning/condition-evaluation logic matching runtime behavior.Overview
Adds richer hook planning introspection:
hk <hook> --why[=<STEP>]now prints a plan with detailed inclusion/exclusion reasons (and can focus on a single step), and--jsoncan emit the plan as structured JSON for--plan/--why.Implements a new
Planmodel (src/plan.rs) and rewritesHook::planto analyze steps (filters, profiles, skip-step sources, dependencies, and condition/step_condition evaluation) while suppressing progress noise;--why <step> --jsonfilters the JSON to the focused step and prunes parallel group references.Updates CLI docs/usage metadata to document
-J/--jsonand-W/--why, adds validation so--jsonerrors unless paired with planning (except when used for global trace JSON), and bumpscommuniqueto1.0.1inmise.lock.Reviewed by Cursor Bugbot for commit e25b701. Bugbot is set up for automated code reviews on this repo. Configure here.