From dc426336106fc14218980b7228e1b9921a68a915 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 22 Mar 2026 21:38:28 -0700 Subject: [PATCH 1/9] =?UTF-8?q?feat!:=20rationalize=20hooks=20=E2=80=94=20?= =?UTF-8?q?rename=20post-create,=20add=20post-commit,=20background=20post-?= =?UTF-8?q?merge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the hook rationalization from #1670. Every lifecycle event now has a symmetric pre- (blocking) / post- (background) pair: | Event | pre- (blocking) | post- (background) | |---------|-----------------|---------------------------| | start | pre-start | post-start | | switch | pre-switch | post-switch | | commit | pre-commit | post-commit (new) | | merge | pre-merge | post-merge (was blocking) | | remove | pre-remove | post-remove | Changes: - **Rename post-create → pre-start**: accepts both names for one release cycle. Deprecation warning when post-create found in config. Migration file generated via `wt config update`. CLI alias `wt hook post-create` still works. - **New post-commit hook**: fires in background after successful commit in `wt commit` and `wt merge`. Respects --no-verify. Useful for CI triggers, notifications, or background linting. - **post-merge → background**: was blocking with Warn strategy, now spawns in background like all other post-* hooks. Removes PostMerge exit code propagation. `--foreground` flag added for debugging. Ref #1670 Co-Authored-By: Claude --- docs/content/hook.md | 3 +- docs/content/step.md | 4 +- skills/worktrunk/reference/hook.md | 3 +- skills/worktrunk/reference/step.md | 4 +- src/cli/hook.rs | 41 ++++++- src/cli/step.rs | 4 +- src/commands/command_approval.rs | 2 +- src/commands/commit.rs | 20 +++- src/commands/handle_switch.rs | 4 +- src/commands/hook_commands.rs | 80 +++++-------- src/commands/hooks.rs | 15 --- src/commands/merge.rs | 29 +++-- src/commands/process.rs | 6 +- src/commands/project_config.rs | 15 +-- src/commands/worktree/hooks.rs | 8 +- src/commands/worktree/switch.rs | 4 +- src/completion.rs | 3 +- src/config/deprecation.rs | 112 ++++++++++++++++++ src/config/hooks.rs | 22 +++- src/git/error.rs | 4 +- src/git/mod.rs | 3 +- src/main.rs | 28 ++++- tests/integration_tests/completion.rs | 9 +- tests/integration_tests/config_show.rs | 2 +- tests/integration_tests/git_error_display.rs | 2 +- tests/integration_tests/help.rs | 6 +- .../integration_tests/post_start_commands.rs | 4 +- ...lay__hook_command_failed_without_name.snap | 2 +- tests/integration_tests/user_hooks.rs | 4 +- ...oval_ui__already_approved_skip_prompt.snap | 3 +- ...pproval_ui__approval_fails_in_non_tty.snap | 3 +- ...i__approval_mixed_approved_unapproved.snap | 5 +- ...proval_ui__approval_multiple_commands.snap | 9 +- ..._approval_ui__approval_named_commands.snap | 7 +- ..._approval_ui__approval_single_command.snap | 3 +- ...ecline_approval_skips_only_unapproved.snap | 5 +- ...__approval_ui__yes_bypasses_tty_check.snap | 3 +- ...yes_does_not_save_approvals_first_run.snap | 3 +- ...es_does_not_save_approvals_second_run.snap | 3 +- ...s__add_approvals_all_already_approved.snap | 3 +- ...vals__add_approvals_all_none_approved.snap | 3 +- ...how_always_regenerates_migration_file.snap | 5 +- ...fig_show_displays_deprecation_details.snap | 5 +- ...ked_worktree_shows_main_worktree_hint.snap | 1 + ...show__config_show_with_project_config.snap | 14 +++ ...update_applies_template_var_migration.snap | 6 +- ...elp__nested_subcommand_hook_pre_start.snap | 39 ++++++ ...rge_cwd_removed_hint_fallback_to_list.snap | 11 +- ...e__merge_cwd_removed_hint_no_recovery.snap | 11 +- ...rge__merge_post_merge_command_failure.snap | 11 +- ...merge__merge_post_merge_command_named.snap | 5 +- ...rge__merge_post_merge_command_success.snap | 3 +- ...erge_post_merge_runs_from_main_branch.snap | 3 +- ...post_merge_runs_with_nothing_to_merge.snap | 3 +- ..._tests__merge__readme_example_complex.snap | 8 +- ...start_commands__both_create_and_start.snap | 3 +- ..._commands__post_create_base_variables.snap | 5 +- ...__post_create_default_branch_template.snap | 3 +- ...commands__post_create_failing_command.snap | 3 +- ...s__post_create_git_variables_template.snap | 9 +- ...st_create_multiline_control_structure.snap | 3 +- ..._commands__post_create_named_commands.snap | 5 +- ..._commands__post_create_single_command.snap | 3 +- ...mands__post_create_template_expansion.snap | 11 +- ...nds__post_create_upstream_conditional.snap | 3 +- ...mmands__post_create_upstream_template.snap | 3 +- ...ost_create_verbose_template_expansion.snap | 3 +- ...user_hooks__no_verify_skips_all_hooks.snap | 1 + ...__user_hooks__user_hook_template_vars.snap | 3 +- ...user_hooks__user_hooks_before_project.snap | 6 +- ...s__user_hooks__user_hooks_no_approval.snap | 3 +- ...user_hooks__user_hooks_preserve_order.snap | 9 +- ...user_hooks__user_post_create_executes.snap | 3 +- ..._user_hooks__user_post_create_failure.snap | 3 +- ..._user_hooks__user_post_merge_executes.snap | 7 +- 75 files changed, 479 insertions(+), 228 deletions(-) create mode 100644 tests/snapshots/integration__integration_tests__help__nested_subcommand_hook_pre_start.snap diff --git a/docs/content/hook.md b/docs/content/hook.md index df2c427854..67d4c44c01 100644 --- a/docs/content/hook.md +++ b/docs/content/hook.md @@ -474,10 +474,11 @@ Usage: wt hook [OPTIONS] Commands: show Show configured hooks pre-switch Run pre-switch hooks - post-create Run post-create hooks + pre-start Run pre-start hooks post-start Run post-start hooks post-switch Run post-switch hooks pre-commit Run pre-commit hooks + post-commit Run post-commit hooks pre-merge Run pre-merge hooks post-merge Run post-merge hooks pre-remove Run pre-remove hooks diff --git a/docs/content/step.md b/docs/content/step.md index 15b997a738..2c5b5a9849 100644 --- a/docs/content/step.md +++ b/docs/content/step.md @@ -384,7 +384,7 @@ Reflink copies share disk blocks until modified — no data is actually copied. Uses per-file reflink (like `cp -Rc`) — copy time scales with file count. -Use the `post-start` hook so the copy runs in the background. Use `post-create` instead if subsequent hooks or `--execute` command need the copied files immediately. +Use the `post-start` hook so the copy runs in the background. Use `pre-start` instead if subsequent hooks or `--execute` command need the copied files immediately. ### Language-specific notes @@ -397,7 +397,7 @@ The `target/` directory is huge (often 1-10GB). Copying with reflink cuts first `node_modules/` is large but mostly static. If the project has no native dependencies, symlinks are even faster: ```toml -[post-create] +[pre-start] deps = "ln -sf {{ primary_worktree_path }}/node_modules ." ``` diff --git a/skills/worktrunk/reference/hook.md b/skills/worktrunk/reference/hook.md index f04298fd7e..b5c95cf8ab 100644 --- a/skills/worktrunk/reference/hook.md +++ b/skills/worktrunk/reference/hook.md @@ -457,10 +457,11 @@ Usage: wt hook [OPTIONS] Commands: show Show configured hooks pre-switch Run pre-switch hooks - post-create Run post-create hooks + pre-start Run pre-start hooks post-start Run post-start hooks post-switch Run post-switch hooks pre-commit Run pre-commit hooks + post-commit Run post-commit hooks pre-merge Run pre-merge hooks post-merge Run post-merge hooks pre-remove Run pre-remove hooks diff --git a/skills/worktrunk/reference/step.md b/skills/worktrunk/reference/step.md index 5030230785..1a967039ca 100644 --- a/skills/worktrunk/reference/step.md +++ b/skills/worktrunk/reference/step.md @@ -362,7 +362,7 @@ Reflink copies share disk blocks until modified — no data is actually copied. Uses per-file reflink (like `cp -Rc`) — copy time scales with file count. -Use the `post-start` hook so the copy runs in the background. Use `post-create` instead if subsequent hooks or `--execute` command need the copied files immediately. +Use the `post-start` hook so the copy runs in the background. Use `pre-start` instead if subsequent hooks or `--execute` command need the copied files immediately. ### Language-specific notes @@ -375,7 +375,7 @@ The `target/` directory is huge (often 1-10GB). Copying with reflink cuts first `node_modules/` is large but mostly static. If the project has no native dependencies, symlinks are even faster: ```toml -[post-create] +[pre-start] deps = "ln -sf {{ primary_worktree_path }}/node_modules ." ``` diff --git a/src/cli/hook.rs b/src/cli/hook.rs index 59a84fd35e..85513e89bb 100644 --- a/src/cli/hook.rs +++ b/src/cli/hook.rs @@ -10,7 +10,7 @@ pub enum HookCommand { /// Lists user and project hooks. Project hooks show approval status (❓ = needs approval). Show { /// Hook type to show (default: all) - #[arg(value_parser = ["pre-switch", "post-create", "post-start", "post-switch", "pre-commit", "pre-merge", "post-merge", "pre-remove", "post-remove"])] + #[arg(value_parser = ["pre-switch", "pre-start", "post-create", "post-start", "post-switch", "pre-commit", "post-commit", "pre-merge", "post-merge", "pre-remove", "post-remove"])] hook_type: Option, /// Show expanded commands with current variables @@ -42,10 +42,11 @@ pub enum HookCommand { vars: Vec<(String, String)>, }, - /// Run post-create hooks + /// Run pre-start hooks /// /// Blocking — waits for completion before continuing. - PostCreate { + #[command(alias = "post-create")] + PreStart { /// Filter by command name /// /// Supports `user:name` or `project:name` to filter by source. @@ -144,6 +145,34 @@ pub enum HookCommand { vars: Vec<(String, String)>, }, + /// Run post-commit hooks + /// + /// Background by default. Use `--foreground` to run in foreground for debugging. + PostCommit { + /// Filter by command name + /// + /// Supports `user:name` or `project:name` to filter by source. + /// `user:` alone runs all user hooks; `project:` alone runs all project hooks. + #[arg(add = crate::completion::hook_command_name_completer())] + name: Option, + + /// Skip approval prompts + #[arg(short, long, help_heading = "Automation")] + yes: bool, + + /// Show what would run without executing + #[arg(long)] + dry_run: bool, + + /// Run in foreground (block until complete) + #[arg(long)] + foreground: bool, + + /// Override built-in template variable (KEY=VALUE) + #[arg(long = "var", value_name = "KEY=VALUE", value_parser = super::parse_key_val, action = clap::ArgAction::Append)] + vars: Vec<(String, String)>, + }, + /// Run pre-merge hooks PreMerge { /// Filter by command name @@ -167,6 +196,8 @@ pub enum HookCommand { }, /// Run post-merge hooks + /// + /// Background by default. Use `--foreground` to run in foreground for debugging. PostMerge { /// Filter by command name /// @@ -183,6 +214,10 @@ pub enum HookCommand { #[arg(long)] dry_run: bool, + /// Run in foreground (block until complete) + #[arg(long)] + foreground: bool, + /// Override built-in template variable (KEY=VALUE) #[arg(long = "var", value_name = "KEY=VALUE", value_parser = super::parse_key_val, action = clap::ArgAction::Append)] vars: Vec<(String, String)>, diff --git a/src/cli/step.rs b/src/cli/step.rs index eb7d425f70..8b62a3dfe6 100644 --- a/src/cli/step.rs +++ b/src/cli/step.rs @@ -282,7 +282,7 @@ Reflink copies share disk blocks until modified — no data is actually copied. Uses per-file reflink (like `cp -Rc`) — copy time scales with file count. -Use the `post-start` hook so the copy runs in the background. Use `post-create` instead if subsequent hooks or `--execute` command need the copied files immediately. +Use the `post-start` hook so the copy runs in the background. Use `pre-start` instead if subsequent hooks or `--execute` command need the copied files immediately. ## Language-specific notes @@ -295,7 +295,7 @@ The `target/` directory is huge (often 1-10GB). Copying with reflink cuts first `node_modules/` is large but mostly static. If the project has no native dependencies, symlinks are even faster: ```toml -[post-create] +[pre-start] deps = "ln -sf {{ primary_worktree_path }}/node_modules ." ``` diff --git a/src/commands/command_approval.rs b/src/commands/command_approval.rs index 042f208b1f..365d223e08 100644 --- a/src/commands/command_approval.rs +++ b/src/commands/command_approval.rs @@ -183,7 +183,7 @@ pub fn approve_alias( /// /// ```ignore /// let ctx = CommandContext::new(&repo, &config, &branch, &worktree_path, yes); -/// let approved = approve_hooks(&ctx, &[HookType::PostCreate, HookType::PostStart])?; +/// let approved = approve_hooks(&ctx, &[HookType::PreStart, HookType::PostStart])?; /// ``` pub fn approve_hooks( ctx: &super::command_executor::CommandContext<'_>, diff --git a/src/commands/commit.rs b/src/commands/commit.rs index 53a99d1773..1d20445518 100644 --- a/src/commands/commit.rs +++ b/src/commands/commit.rs @@ -7,7 +7,9 @@ use worktrunk::styling::{ }; use super::command_executor::CommandContext; -use super::hooks::{HookCommandSpec, HookFailureStrategy}; +use super::hooks::{ + HookCommandSpec, HookFailureStrategy, prepare_background_hooks, spawn_background_hooks, +}; use super::repository_ext::RepositoryCliExt; // Re-export StageMode from config for use by CLI @@ -227,7 +229,21 @@ impl CommitOptions<'_> { true, // show_progress self.show_no_squash_note, self.stage_mode, - ) + )?; + + // Spawn post-commit hooks in background (respects --no-verify) + if self.verify { + let extra_vars: Vec<(&str, &str)> = self + .target_branch + .into_iter() + .map(|target| ("target", target)) + .collect(); + let hooks = + prepare_background_hooks(self.ctx, HookType::PostCommit, &extra_vars, None)?; + spawn_background_hooks(self.ctx, hooks)?; + } + + Ok(()) } } diff --git a/src/commands/handle_switch.rs b/src/commands/handle_switch.rs index 37b53375c4..2b7158e60a 100644 --- a/src/commands/handle_switch.rs +++ b/src/commands/handle_switch.rs @@ -103,12 +103,12 @@ pub(crate) fn run_pre_switch_hooks( /// Hook types that apply after a switch operation. /// -/// Creates trigger post-create + post-start + post-switch hooks; +/// Creates trigger pre-start + post-start + post-switch hooks; /// existing worktrees trigger only post-switch. fn switch_post_hook_types(is_create: bool) -> &'static [HookType] { if is_create { &[ - HookType::PostCreate, + HookType::PreStart, HookType::PostStart, HookType::PostSwitch, ] diff --git a/src/commands/hook_commands.rs b/src/commands/hook_commands.rs index 7702c82bbc..a844b9535f 100644 --- a/src/commands/hook_commands.rs +++ b/src/commands/hook_commands.rs @@ -113,7 +113,9 @@ fn build_manual_hook_extra_vars<'a>( let branch = ctx.branch_or_head(); let mut vars: Vec<(&str, &str)> = match hook_type { // Merge/commit hooks: target = merge target (default branch for commit, current for merge) - HookType::PreCommit => default_branch.into_iter().map(|t| ("target", t)).collect(), + HookType::PreCommit | HookType::PostCommit => { + default_branch.into_iter().map(|t| ("target", t)).collect() + } HookType::PreMerge | HookType::PostMerge => { vec![ ("target", branch), @@ -121,7 +123,7 @@ fn build_manual_hook_extra_vars<'a>( ] } // Switch hooks: base = current (we're "switching from" here) - HookType::PreSwitch | HookType::PostCreate | HookType::PostStart | HookType::PostSwitch => { + HookType::PreSwitch | HookType::PreStart | HookType::PostStart | HookType::PostSwitch => { vec![ ("base", branch), ("base_worktree_path", worktree_path_str), @@ -257,9 +259,10 @@ pub fn run_hook( } // Execute the hook based on type + // pre-* hooks are blocking (fail-fast), post-* hooks run in background match hook_type { HookType::PreSwitch - | HookType::PostCreate + | HookType::PreStart | HookType::PreRemove | HookType::PreCommit | HookType::PreMerge => run_filtered_hook( @@ -271,7 +274,11 @@ pub fn run_hook( name_filter, HookFailureStrategy::FailFast, ), - HookType::PostStart | HookType::PostSwitch | HookType::PostRemove => run_post_hook( + HookType::PostStart + | HookType::PostSwitch + | HookType::PostCommit + | HookType::PostMerge + | HookType::PostRemove => run_post_hook( &ctx, foreground, user_config, @@ -280,15 +287,6 @@ pub fn run_hook( &extra_vars, name_filter, ), - HookType::PostMerge => run_filtered_hook( - &ctx, - user_config, - proj_config, - hook_type, - &extra_vars, - name_filter, - HookFailureStrategy::Warn, - ), } } @@ -426,10 +424,11 @@ pub fn handle_hook_show(hook_type_filter: Option<&str>, expanded: bool) -> anyho // Parse hook type filter if provided let filter: Option = hook_type_filter.map(|s| match s { "pre-switch" => HookType::PreSwitch, - "post-create" => HookType::PostCreate, + "pre-start" | "post-create" => HookType::PreStart, "post-start" => HookType::PostStart, "post-switch" => HookType::PostSwitch, "pre-commit" => HookType::PreCommit, + "post-commit" => HookType::PostCommit, "pre-merge" => HookType::PreMerge, "post-merge" => HookType::PostMerge, "pre-remove" => HookType::PreRemove, @@ -496,33 +495,24 @@ fn render_user_hooks( )?; // Collect all user hooks (global hooks from the user config file) - // Note: uses overrides.hooks for display, not the merged hooks() accessor + // Note: uses overrides.hooks for display, not the merged hooks() accessor. + // get() handles the post-create → pre-start deprecation merge. let user_hooks = &config.configs.hooks; - let hooks = [ - (HookType::PreSwitch, &user_hooks.pre_switch), - (HookType::PostCreate, &user_hooks.post_create), - (HookType::PostStart, &user_hooks.post_start), - (HookType::PostSwitch, &user_hooks.post_switch), - (HookType::PreCommit, &user_hooks.pre_commit), - (HookType::PreMerge, &user_hooks.pre_merge), - (HookType::PostMerge, &user_hooks.post_merge), - (HookType::PreRemove, &user_hooks.pre_remove), - (HookType::PostRemove, &user_hooks.post_remove), - ]; + let hooks: Vec<_> = HookType::iter() + .filter_map(|ht| user_hooks.get(ht).map(|cfg| (ht, cfg))) + .collect(); let mut has_any = false; - for (hook_type, hook_config) in hooks { + for (hook_type, cfg) in &hooks { // Apply filter if specified if let Some(f) = filter - && f != hook_type + && f != *hook_type { continue; } - if let Some(cfg) = hook_config { - has_any = true; - render_hook_commands(out, hook_type, cfg, None, ctx)?; - } + has_any = true; + render_hook_commands(out, *hook_type, cfg, None, ctx)?; } if !has_any { @@ -559,32 +549,22 @@ fn render_project_hooks( return Ok(()); }; - // Collect all project hooks - let hooks = [ - (HookType::PreSwitch, &config.hooks.pre_switch), - (HookType::PostCreate, &config.hooks.post_create), - (HookType::PostStart, &config.hooks.post_start), - (HookType::PostSwitch, &config.hooks.post_switch), - (HookType::PreCommit, &config.hooks.pre_commit), - (HookType::PreMerge, &config.hooks.pre_merge), - (HookType::PostMerge, &config.hooks.post_merge), - (HookType::PreRemove, &config.hooks.pre_remove), - (HookType::PostRemove, &config.hooks.post_remove), - ]; + // Collect all project hooks (get() handles post-create → pre-start merge) + let hooks: Vec<_> = HookType::iter() + .filter_map(|ht| config.hooks.get(ht).map(|cfg| (ht, cfg))) + .collect(); let mut has_any = false; - for (hook_type, hook_config) in hooks { + for (hook_type, cfg) in &hooks { // Apply filter if specified if let Some(f) = filter - && f != hook_type + && f != *hook_type { continue; } - if let Some(cfg) = hook_config { - has_any = true; - render_hook_commands(out, hook_type, cfg, Some((approvals, project_id)), ctx)?; - } + has_any = true; + render_hook_commands(out, *hook_type, cfg, Some((approvals, project_id)), ctx)?; } if !has_any { diff --git a/src/commands/hooks.rs b/src/commands/hooks.rs index 4dca504b7e..58ed67ea8a 100644 --- a/src/commands/hooks.rs +++ b/src/commands/hooks.rs @@ -68,7 +68,6 @@ pub enum HookFailureStrategy { /// Stop on first failure and surface a `HookCommandFailed` error. FailFast, /// Log warnings and continue executing remaining commands. - /// For PostMerge hooks, propagates exit code after all commands complete. Warn, } @@ -348,8 +347,6 @@ pub fn run_hook_with_filter( } // Track first failure's exit code for Warn strategy (to propagate after all commands run) - let mut first_failure_exit_code: Option = None; - for cmd in commands { cmd.announce()?; @@ -388,23 +385,11 @@ pub fn run_hook_with_filter( None => format!("Command failed: {err_msg}"), }; eprintln!("{}", error_message(message)); - - // Track first failure to propagate exit code later (only for PostMerge) - if first_failure_exit_code.is_none() && hook_type == HookType::PostMerge { - first_failure_exit_code = Some(exit_code.unwrap_or(1)); - } } } } } - // For Warn strategy with PostMerge: if any command failed, propagate the exit code - // This matches git's behavior: post-hooks can't stop the operation but affect exit status - // Don't show another error message — warnings were already printed inline - if let Some(exit_code) = first_failure_exit_code { - return Err(WorktrunkError::AlreadyDisplayed { exit_code }.into()); - } - Ok(()) } diff --git a/src/commands/merge.rs b/src/commands/merge.rs index 5906e36517..da97d719ab 100644 --- a/src/commands/merge.rs +++ b/src/commands/merge.rs @@ -8,7 +8,10 @@ use super::command_approval::approve_command_batch; use super::command_executor::CommandContext; use super::commit::CommitOptions; use super::context::CommandEnv; -use super::hooks::{HookFailureStrategy, execute_hook}; +use super::hooks::{ + HookCommandSpec, HookFailureStrategy, execute_hook, prepare_hook_commands, + spawn_background_hooks, +}; use super::project_config::{ApprovableCommand, collect_commands_for_hooks}; use super::repository_ext::{ RepositoryCliExt, check_not_default_branch, compute_integration_reason, is_primary_worktree, @@ -63,6 +66,7 @@ fn collect_merge_commands( let will_create_commit = repo.current_worktree().is_dirty()? || squash_enabled; if commit && verify && will_create_commit { hooks.push(HookType::PreCommit); + hooks.push(HookType::PostCommit); } if verify { @@ -352,14 +356,25 @@ pub fn handle_merge(opts: MergeOptions<'_>) -> anyhow::Result<()> { extra.push(("short_commit", sc)); } - execute_hook( - &ctx, + let project_config = ctx.repo.load_project_config()?; + let user_hooks_cfg = ctx.config.hooks(ctx.project_id().as_deref()); + let (user_cfg, proj_cfg) = super::hooks::lookup_hook_configs( + &user_hooks_cfg, + project_config.as_ref(), HookType::PostMerge, - &extra, - HookFailureStrategy::Warn, - None, - display_path, + ); + let commands = prepare_hook_commands( + &ctx, + HookCommandSpec { + user_config: user_cfg, + project_config: proj_cfg, + hook_type: HookType::PostMerge, + extra_vars: &extra, + name_filter: None, + display_path, + }, )?; + spawn_background_hooks(&ctx, commands)?; } Ok(()) diff --git a/src/commands/process.rs b/src/commands/process.rs index 62d49db10a..2a7110e5ca 100644 --- a/src/commands/process.rs +++ b/src/commands/process.rs @@ -626,10 +626,10 @@ mod tests { // Suffix includes sanitized name with hash for collision avoidance // Constructor and parse produce identical suffixes assert_snapshot!(HookLog::hook(HookSource::User, HookType::PostStart, "server").suffix(), @"user-post-start-server-f4t"); - assert_snapshot!(HookLog::hook(HookSource::Project, HookType::PostCreate, "build").suffix(), @"project-post-create-build-seq"); + assert_snapshot!(HookLog::hook(HookSource::Project, HookType::PreStart, "build").suffix(), @"project-pre-start-build-seq"); assert_snapshot!(HookLog::hook(HookSource::User, HookType::PreRemove, "cleanup").suffix(), @"user-pre-remove-cleanup-non"); assert_snapshot!(HookLog::parse("user:post-start:server").unwrap().suffix(), @"user-post-start-server-f4t"); - assert_snapshot!(HookLog::parse("project:post-create:build").unwrap().suffix(), @"project-post-create-build-seq"); + assert_snapshot!(HookLog::parse("project:pre-start:build").unwrap().suffix(), @"project-pre-start-build-seq"); // Internal operation suffix assert_eq!(HookLog::internal(InternalOp::Remove).suffix(), "remove"); @@ -660,7 +660,7 @@ mod tests { assert_snapshot!(HookLog::parse("invalid:post-start:server").unwrap_err(), @"Unknown source: invalid. Valid: user, project"); // Unknown hook type - assert_snapshot!(HookLog::parse("user:invalid-hook:server").unwrap_err(), @"Unknown hook type: invalid-hook. Valid: pre-switch, post-create, post-start, post-switch, pre-commit, pre-merge, post-merge, pre-remove, post-remove"); + assert_snapshot!(HookLog::parse("user:invalid-hook:server").unwrap_err(), @"Unknown hook type: invalid-hook. Valid: pre-switch, pre-start, post-start, post-switch, pre-commit, post-commit, pre-merge, post-merge, pre-remove, post-remove"); // Unknown internal operation assert_snapshot!(HookLog::parse("internal:unknown").unwrap_err(), @"Unknown internal operation: unknown. Valid: remove"); diff --git a/src/commands/project_config.rs b/src/commands/project_config.rs index e5dff840ea..1c262d33aa 100644 --- a/src/commands/project_config.rs +++ b/src/commands/project_config.rs @@ -72,7 +72,7 @@ pre-merge = "cargo test" #[test] fn test_collect_commands_for_hooks_single_hook() { let config = make_project_config_with_hooks(); - let commands = collect_commands_for_hooks(&config, &[HookType::PostCreate]); + let commands = collect_commands_for_hooks(&config, &[HookType::PreStart]); assert_eq!(commands.len(), 1); assert_eq!(commands[0].command.template, "npm install"); } @@ -81,7 +81,7 @@ pre-merge = "cargo test" fn test_collect_commands_for_hooks_multiple_hooks() { let config = make_project_config_with_hooks(); let commands = - collect_commands_for_hooks(&config, &[HookType::PostCreate, HookType::PreMerge]); + collect_commands_for_hooks(&config, &[HookType::PreStart, HookType::PreMerge]); assert_eq!(commands.len(), 2); assert_eq!(commands[0].command.template, "npm install"); assert_eq!(commands[1].command.template, "cargo test"); @@ -99,7 +99,7 @@ pre-merge = "cargo test" let config = make_project_config_with_hooks(); // Order should match the order of hooks provided let commands = - collect_commands_for_hooks(&config, &[HookType::PreMerge, HookType::PostCreate]); + collect_commands_for_hooks(&config, &[HookType::PreMerge, HookType::PreStart]); assert_eq!(commands.len(), 2); assert_eq!(commands[0].command.template, "cargo test"); assert_eq!(commands[1].command.template, "npm install"); @@ -124,7 +124,7 @@ install = "npm install" build = "npm run build" "#; let config: ProjectConfig = toml::from_str(toml_content).unwrap(); - let commands = collect_commands_for_hooks(&config, &[HookType::PostCreate]); + let commands = collect_commands_for_hooks(&config, &[HookType::PreStart]); assert_eq!(commands.len(), 2); // Named commands preserve order from TOML assert_eq!(commands[0].command.name, Some("install".to_string())); @@ -134,11 +134,8 @@ build = "npm run build" #[test] fn test_collect_commands_for_hooks_phase_is_set() { let config = make_project_config_with_hooks(); - let commands = collect_commands_for_hooks(&config, &[HookType::PostCreate]); + let commands = collect_commands_for_hooks(&config, &[HookType::PreStart]); assert_eq!(commands.len(), 1); - assert!(matches!( - commands[0].phase, - Phase::Hook(HookType::PostCreate) - )); + assert!(matches!(commands[0].phase, Phase::Hook(HookType::PreStart))); } } diff --git a/src/commands/worktree/hooks.rs b/src/commands/worktree/hooks.rs index 7d696efc63..1990bcdb4c 100644 --- a/src/commands/worktree/hooks.rs +++ b/src/commands/worktree/hooks.rs @@ -1,6 +1,6 @@ //! Hook execution for worktree operations. //! -//! CommandContext implementations for post-create and post-remove hooks. +//! CommandContext implementations for pre-start and post-remove hooks. use std::path::Path; @@ -13,17 +13,17 @@ use crate::commands::hooks::{ }; impl<'a> CommandContext<'a> { - /// Execute post-create commands sequentially (blocking) + /// Execute pre-start commands sequentially (blocking) /// /// Runs user hooks first, then project hooks. /// Shows path in hook announcements when shell integration isn't active (user's shell /// won't cd to the new worktree, so they need to know where hooks ran). /// /// `extra_vars`: Additional template variables (e.g., `base`, `base_worktree_path`). - pub fn execute_post_create_commands(&self, extra_vars: &[(&str, &str)]) -> anyhow::Result<()> { + pub fn execute_pre_start_commands(&self, extra_vars: &[(&str, &str)]) -> anyhow::Result<()> { execute_hook( self, - HookType::PostCreate, + HookType::PreStart, extra_vars, HookFailureStrategy::Warn, None, diff --git a/src/commands/worktree/switch.rs b/src/commands/worktree/switch.rs index 6925c862bb..12e1c170f0 100644 --- a/src/commands/worktree/switch.rs +++ b/src/commands/worktree/switch.rs @@ -874,7 +874,7 @@ pub fn execute_switch( .into_iter() .flatten() .collect(); - ctx.execute_post_create_commands(&extra_vars)?; + ctx.execute_pre_start_commands(&extra_vars)?; } CreationMethod::ForkRef { ref_type, @@ -889,7 +889,7 @@ pub fn execute_switch( }; let extra_vars: Vec<(&str, &str)> = vec![(num_key, &num_str), (url_key, ref_url)]; - ctx.execute_post_create_commands(&extra_vars)?; + ctx.execute_pre_start_commands(&extra_vars)?; } } } diff --git a/src/completion.rs b/src/completion.rs index e450f6af3c..a0728bd436 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -216,9 +216,10 @@ impl ValueCompleter for HookCommandCompleter { let hook_type = CONTEXT.with(|ctx| { ctx.borrow().as_ref().and_then(|ctx| { for hook in &[ - "post-create", + "pre-start", "post-start", "pre-commit", + "post-commit", "pre-merge", "post-merge", "pre-remove", diff --git a/src/config/deprecation.rs b/src/config/deprecation.rs index 55eda417df..cd3a93f65d 100644 --- a/src/config/deprecation.rs +++ b/src/config/deprecation.rs @@ -198,6 +198,8 @@ pub struct Deprecations { pub approved_commands: bool, /// Has `[select]` section (moved to `[switch.picker]`) pub select: bool, + /// Has `[hooks.post-create]` (renamed to `[hooks.pre-start]`) + pub post_create: bool, } impl Deprecations { @@ -207,6 +209,7 @@ impl Deprecations { && self.commit_gen.is_empty() && !self.approved_commands && !self.select + && !self.post_create } } @@ -220,6 +223,7 @@ pub fn detect_deprecations(content: &str) -> Deprecations { commit_gen: find_commit_generation_deprecations(content), approved_commands: find_approved_commands_deprecation(content), select: find_select_deprecation(content), + post_create: find_post_create_deprecation(content), } } @@ -510,6 +514,95 @@ pub fn find_select_deprecation(content: &str) -> bool { false } +/// Check if config has a deprecated `post-create` hook without a corresponding `pre-start`. +/// +/// Checks both top-level hooks (`post-create = "..."`) and project hooks +/// (`[projects."...".hooks] post-create = "..."`). This handles both user config +/// and project config formats. +pub fn find_post_create_deprecation(content: &str) -> bool { + let Ok(doc) = content.parse::() else { + return false; + }; + + // Check top-level hooks section (project config: `post-create = "..."`) + if doc.get("pre-start").is_none() && doc.get("post-create").is_some() { + return true; + } + + // Check [hooks] section (user config: `[hooks] post-create = "..."`) + if let Some(hooks) = doc.get("hooks").and_then(|h| h.as_table()) + && hooks.get("pre-start").is_none() + && hooks.get("post-create").is_some() + { + return true; + } + + // Check project-level hooks + if let Some(projects) = doc.get("projects").and_then(|p| p.as_table()) { + for (_key, project_value) in projects.iter() { + if let Some(project_table) = project_value.as_table() + && let Some(hooks) = project_table.get("hooks").and_then(|h| h.as_table()) + && hooks.get("pre-start").is_none() + && hooks.get("post-create").is_some() + { + return true; + } + } + } + + false +} + +/// Migrate `post-create` hooks to `pre-start`. +/// +/// Renames `post-create` to `pre-start` in hooks sections. Skips if `pre-start` already exists. +pub fn migrate_post_create_to_pre_start(content: &str) -> String { + let Ok(mut doc) = content.parse::() else { + return content.to_string(); + }; + + let mut modified = false; + + // Top-level (project config format) + if doc.get("pre-start").is_none() + && let Some(value) = doc.remove("post-create") + { + doc.insert("pre-start", value); + modified = true; + } + + // [hooks] section (user config format) + if let Some(hooks) = doc.get_mut("hooks").and_then(|h| h.as_table_mut()) + && hooks.get("pre-start").is_none() + && let Some(value) = hooks.remove("post-create") + { + hooks.insert("pre-start", value); + modified = true; + } + + // Project-level hooks + if let Some(projects) = doc.get_mut("projects").and_then(|p| p.as_table_mut()) { + for (_key, project_value) in projects.iter_mut() { + if let Some(project_table) = project_value.as_table_mut() + && let Some(hooks) = project_table + .get_mut("hooks") + .and_then(|h| h.as_table_mut()) + && hooks.get("pre-start").is_none() + && let Some(value) = hooks.remove("post-create") + { + hooks.insert("pre-start", value); + modified = true; + } + } + } + + if modified { + doc.to_string() + } else { + content.to_string() + } +} + /// Migrate `[select]` section to `[switch.picker]`. /// /// Renames `[select]` to `[switch.picker]`, preserving all fields. @@ -849,6 +942,9 @@ pub fn write_migration_file( if deprecations.select { new_content = migrate_select_to_switch_picker(&new_content); } + if deprecations.post_create { + new_content = migrate_post_create_to_pre_start(&new_content); + } if let Err(e) = std::fs::write(&new_path, &new_content) { // Log write failure but don't block config loading @@ -968,6 +1064,17 @@ pub fn format_deprecation_warnings(info: &DeprecationInfo) -> String { ); } + if info.deprecations.post_create { + let _ = writeln!( + out, + "{}", + warning_message(format!( + "{} uses deprecated hook name: post-create → pre-start", + info.label + )) + ); + } + out } @@ -2006,6 +2113,7 @@ approved-commands = ["npm install"] commit_gen: CommitGenerationDeprecations::default(), approved_commands: true, select: false, + post_create: false, }, label: "User config".to_string(), main_worktree_path: None, @@ -2034,6 +2142,7 @@ approved-commands = ["npm install"] commit_gen: CommitGenerationDeprecations::default(), approved_commands: true, select: false, + post_create: false, }, label: "User config".to_string(), main_worktree_path: None, @@ -2063,6 +2172,7 @@ approved-commands = ["npm install"] commit_gen: CommitGenerationDeprecations::default(), approved_commands: true, select: false, + post_create: false, }; let result = write_migration_file(&config_path, content, &deprecations, None); assert!( @@ -2321,6 +2431,7 @@ branches = true commit_gen: CommitGenerationDeprecations::default(), approved_commands: false, select: true, + post_create: false, }, label: "User config".to_string(), main_worktree_path: None, @@ -2353,6 +2464,7 @@ pager = "delta --paging=never" commit_gen: CommitGenerationDeprecations::default(), approved_commands: false, select: true, + post_create: false, }; let result = write_migration_file(&config_path, content, &deprecations, None); assert!(result.is_some(), "Should write migration file for select"); diff --git a/src/config/hooks.rs b/src/config/hooks.rs index 5f4404c852..a9f382fb7c 100644 --- a/src/config/hooks.rs +++ b/src/config/hooks.rs @@ -16,7 +16,11 @@ pub struct HooksConfig { )] pub pre_switch: Option, - /// Commands to execute after worktree creation (blocking) + /// Commands to execute before worktree start (blocking) + #[serde(default, rename = "pre-start", skip_serializing_if = "Option::is_none")] + pub pre_start: Option, + + /// Deprecated: use `pre-start` instead. #[serde( default, rename = "post-create", @@ -48,11 +52,19 @@ pub struct HooksConfig { )] pub pre_commit: Option, + /// Commands to execute after committing (background) + #[serde( + default, + rename = "post-commit", + skip_serializing_if = "Option::is_none" + )] + pub post_commit: Option, + /// Commands to execute before merging (blocking, fail-fast) #[serde(default, rename = "pre-merge", skip_serializing_if = "Option::is_none")] pub pre_merge: Option, - /// Commands to execute after successful merge (blocking, best-effort) + /// Commands to execute after successful merge (background) #[serde( default, rename = "post-merge", @@ -81,10 +93,12 @@ impl HooksConfig { pub fn get(&self, hook: HookType) -> Option<&CommandConfig> { match hook { HookType::PreSwitch => self.pre_switch.as_ref(), - HookType::PostCreate => self.post_create.as_ref(), + // pre-start merges with deprecated post-create (pre-start takes precedence) + HookType::PreStart => self.pre_start.as_ref().or(self.post_create.as_ref()), HookType::PostStart => self.post_start.as_ref(), HookType::PostSwitch => self.post_switch.as_ref(), HookType::PreCommit => self.pre_commit.as_ref(), + HookType::PostCommit => self.post_commit.as_ref(), HookType::PreMerge => self.pre_merge.as_ref(), HookType::PostMerge => self.post_merge.as_ref(), HookType::PreRemove => self.pre_remove.as_ref(), @@ -115,10 +129,12 @@ impl Merge for HooksConfig { fn merge_with(&self, other: &Self) -> Self { Self { pre_switch: merge_append_hooks(&self.pre_switch, &other.pre_switch), + pre_start: merge_append_hooks(&self.pre_start, &other.pre_start), post_create: merge_append_hooks(&self.post_create, &other.post_create), post_start: merge_append_hooks(&self.post_start, &other.post_start), post_switch: merge_append_hooks(&self.post_switch, &other.post_switch), pre_commit: merge_append_hooks(&self.pre_commit, &other.pre_commit), + post_commit: merge_append_hooks(&self.post_commit, &other.post_commit), pre_merge: merge_append_hooks(&self.pre_merge, &other.pre_merge), post_merge: merge_append_hooks(&self.post_merge, &other.post_merge), pre_remove: merge_append_hooks(&self.pre_remove, &other.pre_remove), diff --git a/src/git/error.rs b/src/git/error.rs index 578d8f4341..430dcb4f19 100644 --- a/src/git/error.rs +++ b/src/git/error.rs @@ -1247,12 +1247,12 @@ mod tests { assert_snapshot!(err.to_string(), @"✗ pre-merge command failed: lint: lint failed"); let err = WorktrunkError::HookCommandFailed { - hook_type: HookType::PostCreate, + hook_type: HookType::PreStart, command_name: None, error: "setup failed".into(), exit_code: None, }; - assert_snapshot!(err.to_string(), @"✗ post-create command failed: setup failed"); + assert_snapshot!(err.to_string(), @"✗ pre-start command failed: setup failed"); // Silent errors produce empty output assert_eq!(format!("{}", WorktrunkError::CommandNotApproved), ""); diff --git a/src/git/mod.rs b/src/git/mod.rs index 101224219b..32fe7aa40f 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs @@ -329,10 +329,11 @@ pub fn branch_tracks_ref( #[strum(serialize_all = "kebab-case")] pub enum HookType { PreSwitch, - PostCreate, + PreStart, PostStart, PostSwitch, PreCommit, + PostCommit, PreMerge, PostMerge, PreRemove, diff --git a/src/main.rs b/src/main.rs index 3b2f03f31b..d90fa39e82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -149,12 +149,12 @@ fn handle_hook_command(action: HookCommand) -> anyhow::Result<()> { dry_run, vars, } => run_non_toggle_hook(HookType::PreSwitch, yes, dry_run, name.as_deref(), &vars), - HookCommand::PostCreate { + HookCommand::PreStart { name, yes, dry_run, vars, - } => run_non_toggle_hook(HookType::PostCreate, yes, dry_run, name.as_deref(), &vars), + } => run_non_toggle_hook(HookType::PreStart, yes, dry_run, name.as_deref(), &vars), HookCommand::PostStart { name, yes, @@ -195,12 +195,34 @@ fn handle_hook_command(action: HookCommand) -> anyhow::Result<()> { dry_run, vars, } => run_non_toggle_hook(HookType::PreMerge, yes, dry_run, name.as_deref(), &vars), + HookCommand::PostCommit { + name, + yes, + dry_run, + foreground, + vars, + } => run_toggleable_hook( + HookType::PostCommit, + yes, + dry_run, + foreground, + name.as_deref(), + &vars, + ), HookCommand::PostMerge { name, yes, dry_run, + foreground, vars, - } => run_non_toggle_hook(HookType::PostMerge, yes, dry_run, name.as_deref(), &vars), + } => run_toggleable_hook( + HookType::PostMerge, + yes, + dry_run, + foreground, + name.as_deref(), + &vars, + ), HookCommand::PreRemove { name, yes, diff --git a/tests/integration_tests/completion.rs b/tests/integration_tests/completion.rs index 7faf9ed62c..612855883b 100644 --- a/tests/integration_tests/completion.rs +++ b/tests/integration_tests/completion.rs @@ -796,11 +796,12 @@ fn test_complete_hook_subcommands(repo: TestRepo) { let subcommands = value_suggestions(&stdout); // Hook types and commands assert!(subcommands.contains(&"show"), "Missing show"); - assert!(subcommands.contains(&"post-create"), "Missing post-create"); + assert!(subcommands.contains(&"pre-start"), "Missing pre-start"); assert!(subcommands.contains(&"post-start"), "Missing post-start"); assert!(subcommands.contains(&"post-switch"), "Missing post-switch"); assert!(subcommands.contains(&"pre-switch"), "Missing pre-switch"); assert!(subcommands.contains(&"pre-commit"), "Missing pre-commit"); + assert!(subcommands.contains(&"post-commit"), "Missing post-commit"); assert!(subcommands.contains(&"pre-merge"), "Missing pre-merge"); assert!(subcommands.contains(&"post-merge"), "Missing post-merge"); assert!(subcommands.contains(&"pre-remove"), "Missing pre-remove"); @@ -808,8 +809,8 @@ fn test_complete_hook_subcommands(repo: TestRepo) { assert!(subcommands.contains(&"approvals"), "Missing approvals"); assert_eq!( subcommands.len(), - 11, - "Should have exactly 11 hook subcommands" + 12, + "Should have exactly 12 hook subcommands" ); // Test 2: Partial input "po" - filters to post-* subcommands @@ -817,9 +818,9 @@ fn test_complete_hook_subcommands(repo: TestRepo) { assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let subcommands = value_suggestions(&stdout); - assert!(subcommands.contains(&"post-create")); assert!(subcommands.contains(&"post-start")); assert!(subcommands.contains(&"post-switch")); + assert!(subcommands.contains(&"post-commit")); assert!(subcommands.contains(&"post-merge")); assert!(subcommands.contains(&"post-remove")); assert!(!subcommands.contains(&"pre-commit")); diff --git a/tests/integration_tests/config_show.rs b/tests/integration_tests/config_show.rs index f601b025f3..25c9539d64 100644 --- a/tests/integration_tests/config_show.rs +++ b/tests/integration_tests/config_show.rs @@ -1786,7 +1786,7 @@ fn test_fixing_deprecated_config_clears_hint_for_future_deprecations( // User fixes the config (removes deprecation) fs::write( &project_config_path, - r#"post-create = "ln -sf {{ repo }}/node_modules" + r#"pre-start = "ln -sf {{ repo }}/node_modules" "#, ) .unwrap(); diff --git a/tests/integration_tests/git_error_display.rs b/tests/integration_tests/git_error_display.rs index 552857a9d4..3a040ac252 100644 --- a/tests/integration_tests/git_error_display.rs +++ b/tests/integration_tests/git_error_display.rs @@ -361,7 +361,7 @@ fn display_hook_command_failed_with_name() { #[test] fn display_hook_command_failed_without_name() { let err = WorktrunkError::HookCommandFailed { - hook_type: HookType::PostCreate, + hook_type: HookType::PreStart, command_name: None, error: "command not found".into(), exit_code: Some(127), diff --git a/tests/integration_tests/help.rs b/tests/integration_tests/help.rs index 230df31b2c..7b1c9415d1 100644 --- a/tests/integration_tests/help.rs +++ b/tests/integration_tests/help.rs @@ -203,11 +203,7 @@ fn test_help_description_unknown_command() { #[case("nested_subcommand_step_squash", "squash", "wt step squash")] #[case("nested_subcommand_step_commit", "commit", "wt step commit")] #[case("nested_subcommand_hook_pre_merge", "pre-merge", "wt hook pre-merge")] -#[case( - "nested_subcommand_hook_post_create", - "post-create", - "wt hook post-create" -)] +#[case("nested_subcommand_hook_pre_start", "pre-start", "wt hook pre-start")] fn test_nested_subcommand_suggestion( #[case] test_name: &str, #[case] subcommand: &str, diff --git a/tests/integration_tests/post_start_commands.rs b/tests/integration_tests/post_start_commands.rs index d381f5e5b1..143488fd76 100644 --- a/tests/integration_tests/post_start_commands.rs +++ b/tests/integration_tests/post_start_commands.rs @@ -570,7 +570,7 @@ approved-commands = ["cat > context.json"] ); assert_eq!( json["hook_type"].as_str(), - Some("post-create"), + Some("pre-start"), "JSON should contain hook_type" ); } @@ -657,7 +657,7 @@ approved-commands = ["./scripts/setup.py"] contents ); assert!( - contents.contains("hook_type=post-create"), + contents.contains("hook_type=pre-start"), "Output should contain hook_type: {}", contents ); diff --git a/tests/integration_tests/snapshots/integration__integration_tests__git_error_display__hook_command_failed_without_name.snap b/tests/integration_tests/snapshots/integration__integration_tests__git_error_display__hook_command_failed_without_name.snap index 92e6989fb0..cf18a1f797 100644 --- a/tests/integration_tests/snapshots/integration__integration_tests__git_error_display__hook_command_failed_without_name.snap +++ b/tests/integration_tests/snapshots/integration__integration_tests__git_error_display__hook_command_failed_without_name.snap @@ -2,4 +2,4 @@ source: tests/integration_tests/git_error_display.rs expression: err.to_string() --- -✗ post-create command failed: command not found +✗ pre-start command failed: command not found diff --git a/tests/integration_tests/user_hooks.rs b/tests/integration_tests/user_hooks.rs index e68a45ed9a..ef369d72dc 100644 --- a/tests/integration_tests/user_hooks.rs +++ b/tests/integration_tests/user_hooks.rs @@ -1290,7 +1290,7 @@ fn test_standalone_hook_no_hooks_configured(repo: TestRepo) { let mut cmd = crate::common::wt_command(); cmd.current_dir(repo.root_path()); cmd.env("WORKTRUNK_CONFIG_PATH", repo.test_config_path()); - cmd.args(["hook", "post-create", "--yes"]); + cmd.args(["hook", "pre-start", "--yes"]); let output = cmd.output().unwrap(); assert!( @@ -1300,7 +1300,7 @@ fn test_standalone_hook_no_hooks_configured(repo: TestRepo) { let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("No post-create hook configured"), + stderr.contains("No pre-start hook configured"), "Error should mention no hook configured, got: {stderr}" ); } diff --git a/tests/snapshots/integration__integration_tests__approval_ui__already_approved_skip_prompt.snap b/tests/snapshots/integration__integration_tests__approval_ui__already_approved_skip_prompt.snap index 3766d526dd..019dbd2f19 100644 --- a/tests/snapshots/integration__integration_tests__approval_ui__already_approved_skip_prompt.snap +++ b/tests/snapshots/integration__integration_tests__approval_ui__already_approved_skip_prompt.snap @@ -43,7 +43,8 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create project hook @ _REPO_.test-approved +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start project hook @ _REPO_.test-approved   echo 'approved' > output.txt ✓ Created branch test-approved from main and worktree @ _REPO_.test-approved ↳ To customize worktree locations, run wt config create diff --git a/tests/snapshots/integration__integration_tests__approval_ui__approval_fails_in_non_tty.snap b/tests/snapshots/integration__integration_tests__approval_ui__approval_fails_in_non_tty.snap index 17ddc724db..3fc945b372 100644 --- a/tests/snapshots/integration__integration_tests__approval_ui__approval_fails_in_non_tty.snap +++ b/tests/snapshots/integration__integration_tests__approval_ui__approval_fails_in_non_tty.snap @@ -43,8 +43,9 @@ exit_code: 1 ----- stdout ----- ----- stderr ----- +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates ▲ origin needs approval to execute 1 command: -○ post-create: +○ pre-start:   echo 'test command' ✗ Cannot prompt for approval in non-interactive environment ↳ To skip prompts in CI/CD, add --yes; to pre-approve commands, run wt hook approvals add diff --git a/tests/snapshots/integration__integration_tests__approval_ui__approval_mixed_approved_unapproved.snap b/tests/snapshots/integration__integration_tests__approval_ui__approval_mixed_approved_unapproved.snap index 37d2b1bfca..379e8cef4a 100644 --- a/tests/snapshots/integration__integration_tests__approval_ui__approval_mixed_approved_unapproved.snap +++ b/tests/snapshots/integration__integration_tests__approval_ui__approval_mixed_approved_unapproved.snap @@ -6,10 +6,11 @@ exit_code: 1 ----- stdout ----- ----- stderr ----- +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates ▲ repo needs approval to execute 2 commands: -○ post-create first: +○ pre-start first:   echo 'First command' -○ post-create third: +○ pre-start third:   echo 'Third command' ✗ Cannot prompt for approval in non-interactive environment ↳ To skip prompts in CI/CD, add --yes; to pre-approve commands, run wt hook approvals add diff --git a/tests/snapshots/integration__integration_tests__approval_ui__approval_multiple_commands.snap b/tests/snapshots/integration__integration_tests__approval_ui__approval_multiple_commands.snap index 8f300cc546..c2336f457c 100644 --- a/tests/snapshots/integration__integration_tests__approval_ui__approval_multiple_commands.snap +++ b/tests/snapshots/integration__integration_tests__approval_ui__approval_multiple_commands.snap @@ -6,14 +6,15 @@ exit_code: 1 ----- stdout ----- ----- stderr ----- +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates ▲ origin needs approval to execute 4 commands: -○ post-create branch: +○ pre-start branch:   echo 'Branch: {{ branch }}' -○ post-create worktree: +○ pre-start worktree:   echo 'Worktree: {{ worktree_path }}' -○ post-create repo: +○ pre-start repo:   echo 'Repo: {{ repo }}' -○ post-create pwd: +○ pre-start pwd:   cd {{ worktree_path }} && pwd ✗ Cannot prompt for approval in non-interactive environment ↳ To skip prompts in CI/CD, add --yes; to pre-approve commands, run wt hook approvals add diff --git a/tests/snapshots/integration__integration_tests__approval_ui__approval_named_commands.snap b/tests/snapshots/integration__integration_tests__approval_ui__approval_named_commands.snap index 71d796e23c..d6b2500bb0 100644 --- a/tests/snapshots/integration__integration_tests__approval_ui__approval_named_commands.snap +++ b/tests/snapshots/integration__integration_tests__approval_ui__approval_named_commands.snap @@ -6,12 +6,13 @@ exit_code: 1 ----- stdout ----- ----- stderr ----- +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates ▲ origin needs approval to execute 3 commands: -○ post-create install: +○ pre-start install:   echo 'Installing dependencies...' -○ post-create build: +○ pre-start build:   echo 'Building project...' -○ post-create test: +○ pre-start test:   echo 'Running tests...' ✗ Cannot prompt for approval in non-interactive environment ↳ To skip prompts in CI/CD, add --yes; to pre-approve commands, run wt hook approvals add diff --git a/tests/snapshots/integration__integration_tests__approval_ui__approval_single_command.snap b/tests/snapshots/integration__integration_tests__approval_ui__approval_single_command.snap index 81c7562643..61859228cc 100644 --- a/tests/snapshots/integration__integration_tests__approval_ui__approval_single_command.snap +++ b/tests/snapshots/integration__integration_tests__approval_ui__approval_single_command.snap @@ -6,8 +6,9 @@ exit_code: 1 ----- stdout ----- ----- stderr ----- +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates ▲ origin needs approval to execute 1 command: -○ post-create: +○ pre-start:   echo 'Worktree path: {{ worktree_path }}' ✗ Cannot prompt for approval in non-interactive environment ↳ To skip prompts in CI/CD, add --yes; to pre-approve commands, run wt hook approvals add diff --git a/tests/snapshots/integration__integration_tests__approval_ui__decline_approval_skips_only_unapproved.snap b/tests/snapshots/integration__integration_tests__approval_ui__decline_approval_skips_only_unapproved.snap index 37d2b1bfca..379e8cef4a 100644 --- a/tests/snapshots/integration__integration_tests__approval_ui__decline_approval_skips_only_unapproved.snap +++ b/tests/snapshots/integration__integration_tests__approval_ui__decline_approval_skips_only_unapproved.snap @@ -6,10 +6,11 @@ exit_code: 1 ----- stdout ----- ----- stderr ----- +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates ▲ repo needs approval to execute 2 commands: -○ post-create first: +○ pre-start first:   echo 'First command' -○ post-create third: +○ pre-start third:   echo 'Third command' ✗ Cannot prompt for approval in non-interactive environment ↳ To skip prompts in CI/CD, add --yes; to pre-approve commands, run wt hook approvals add diff --git a/tests/snapshots/integration__integration_tests__approval_ui__yes_bypasses_tty_check.snap b/tests/snapshots/integration__integration_tests__approval_ui__yes_bypasses_tty_check.snap index 8e1be1b0ef..72a5eb7368 100644 --- a/tests/snapshots/integration__integration_tests__approval_ui__yes_bypasses_tty_check.snap +++ b/tests/snapshots/integration__integration_tests__approval_ui__yes_bypasses_tty_check.snap @@ -44,7 +44,8 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create project hook @ _REPO_.test-yes-tty +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start project hook @ _REPO_.test-yes-tty   echo 'test command' test command ✓ Created branch test-yes-tty from main and worktree @ _REPO_.test-yes-tty diff --git a/tests/snapshots/integration__integration_tests__approval_ui__yes_does_not_save_approvals_first_run.snap b/tests/snapshots/integration__integration_tests__approval_ui__yes_does_not_save_approvals_first_run.snap index ec85d2c67b..62502e125d 100644 --- a/tests/snapshots/integration__integration_tests__approval_ui__yes_does_not_save_approvals_first_run.snap +++ b/tests/snapshots/integration__integration_tests__approval_ui__yes_does_not_save_approvals_first_run.snap @@ -44,7 +44,8 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create project hook @ _REPO_.test-yes +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start project hook @ _REPO_.test-yes   echo 'test command' > output.txt ✓ Created branch test-yes from main and worktree @ _REPO_.test-yes ↳ To customize worktree locations, run wt config create diff --git a/tests/snapshots/integration__integration_tests__approval_ui__yes_does_not_save_approvals_second_run.snap b/tests/snapshots/integration__integration_tests__approval_ui__yes_does_not_save_approvals_second_run.snap index 94c4b5fb00..7533c39145 100644 --- a/tests/snapshots/integration__integration_tests__approval_ui__yes_does_not_save_approvals_second_run.snap +++ b/tests/snapshots/integration__integration_tests__approval_ui__yes_does_not_save_approvals_second_run.snap @@ -6,8 +6,9 @@ exit_code: 1 ----- stdout ----- ----- stderr ----- +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates ▲ origin needs approval to execute 1 command: -○ post-create: +○ pre-start:   echo 'test command' > output.txt ✗ Cannot prompt for approval in non-interactive environment ↳ To skip prompts in CI/CD, add --yes; to pre-approve commands, run wt hook approvals add diff --git a/tests/snapshots/integration__integration_tests__approvals__add_approvals_all_already_approved.snap b/tests/snapshots/integration__integration_tests__approvals__add_approvals_all_already_approved.snap index c3220b4bd9..f33e2eeea9 100644 --- a/tests/snapshots/integration__integration_tests__approvals__add_approvals_all_already_approved.snap +++ b/tests/snapshots/integration__integration_tests__approvals__add_approvals_all_already_approved.snap @@ -43,8 +43,9 @@ exit_code: 1 ----- stdout ----- ----- stderr ----- +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates ▲ origin needs approval to execute 1 command: -○ post-create: +○ pre-start:   echo 'test' ✗ Cannot prompt for approval in non-interactive environment ↳ To skip prompts in CI/CD, add --yes; to pre-approve commands, run wt hook approvals add diff --git a/tests/snapshots/integration__integration_tests__approvals__add_approvals_all_none_approved.snap b/tests/snapshots/integration__integration_tests__approvals__add_approvals_all_none_approved.snap index 43354aa0ab..c30c6270a5 100644 --- a/tests/snapshots/integration__integration_tests__approvals__add_approvals_all_none_approved.snap +++ b/tests/snapshots/integration__integration_tests__approvals__add_approvals_all_none_approved.snap @@ -44,8 +44,9 @@ exit_code: 1 ----- stdout ----- ----- stderr ----- +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates ▲ origin needs approval to execute 1 command: -○ post-create: +○ pre-start:   echo 'test' ✗ Cannot prompt for approval in non-interactive environment ↳ To skip prompts in CI/CD, add --yes; to pre-approve commands, run wt hook approvals add diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_always_regenerates_migration_file.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_always_regenerates_migration_file.snap index 1d0236e6d9..c403c54c2c 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_always_regenerates_migration_file.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_always_regenerates_migration_file.snap @@ -47,15 +47,16 @@ exit_code: 0 PROJECT CONFIG @ _REPO_/.config/wt.toml ▲ Project config uses deprecated template variables: main_worktree → repo +▲ Project config uses deprecated hook name: post-create → pre-start ↳ To apply:   wt config update   diff --git a_REPO_/.config/wt.toml b_REPO_/.config/wt.toml.new -  index 8fcdb47..42cb136 100644 +  index 8fcdb47..7792aac 100644   --- a_REPO_/.config/wt.toml   +++ b_REPO_/.config/wt.toml.new   @@ -1 +1 @@   -post-create = "ln -sf {{ main_worktree }}/node_modules" -  +post-create = "ln -sf {{ repo }}/node_modules" +  +pre-start = "ln -sf {{ repo }}/node_modules" ○ Current config:   post-create = "ln -sf {{ main_worktree }}/node_modules" diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_displays_deprecation_details.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_displays_deprecation_details.snap index e93be731b0..5814868d2c 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_displays_deprecation_details.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_displays_deprecation_details.snap @@ -44,17 +44,18 @@ exit_code: 0 ----- stderr ----- USER CONFIG @ ~/.config/worktrunk/config.toml ▲ User config uses deprecated template variables: repo_root → repo_path, main_worktree → repo +▲ User config uses deprecated hook name: post-create → pre-start ↳ To apply:   wt config update   diff --git a[TEMP_HOME]/.config/worktrunk/config.toml b[TEMP_HOME]/.config/worktrunk/config.toml.new -  index d95d100..ceb381d 100644 +  index d95d100..5eef8a6 100644   --- a[TEMP_HOME]/.config/worktrunk/config.toml   +++ b[TEMP_HOME]/.config/worktrunk/config.toml.new   @@ -1,2 +1,2 @@   -worktree-path = "../{{ main_worktree }}.{{ branch }}"   -post-create = "ln -sf {{ repo_root }}/node_modules"   +worktree-path = "../{{ repo }}.{{ branch }}" -  +post-create = "ln -sf {{ repo_path }}/node_modules" +  +pre-start = "ln -sf {{ repo_path }}/node_modules" ○ Current config:   worktree-path = "../{{ main_worktree }}.{{ branch }}"   post-create = "ln -sf {{ repo_root }}/node_modules" diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_from_linked_worktree_shows_main_worktree_hint.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_from_linked_worktree_shows_main_worktree_hint.snap index 2b9ab6156f..6db1ed05eb 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_from_linked_worktree_shows_main_worktree_hint.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_from_linked_worktree_shows_main_worktree_hint.snap @@ -47,6 +47,7 @@ exit_code: 0 PROJECT CONFIG @ [PROJECT_ID] ▲ Project config uses deprecated template variables: main_worktree → repo +▲ Project config uses deprecated hook name: post-create → pre-start ↳ To apply:   wt -C _REPO_ config update ○ Current config: diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_with_project_config.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_with_project_config.snap index 8e641bc432..5d2b12d34d 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_with_project_config.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_with_project_config.snap @@ -47,6 +47,20 @@ exit_code: 0 ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml PROJECT CONFIG @ _REPO_/.config/wt.toml +▲ Project config uses deprecated hook name: post-create → pre-start +↳ To apply: +  wt config update +  diff --git a_REPO_/.config/wt.toml b_REPO_/.config/wt.toml.new +  index 13d6ba3..6ccc26d 100644 +  --- a_REPO_/.config/wt.toml +  +++ b_REPO_/.config/wt.toml.new +  @@ -1,4 +1,4 @@ +  -post-create = "npm install" +  +pre-start = "npm install" +   +  [post-start] +  server = "npm run dev" +○ Current config:   post-create = "npm install"     [post-start] diff --git a/tests/snapshots/integration__integration_tests__config_show__config_update_applies_template_var_migration.snap b/tests/snapshots/integration__integration_tests__config_show__config_update_applies_template_var_migration.snap index bc8fd8a1e6..744b5b00f2 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_update_applies_template_var_migration.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_update_applies_template_var_migration.snap @@ -20,6 +20,7 @@ info: LANG: C LC_ALL: C MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]" + NO_COLOR: "" PATH: "[PATH]" PSModulePath: "" RUST_LOG: warn @@ -43,13 +44,14 @@ exit_code: 0 ----- stderr ----- ▲ User config uses deprecated template variables: repo_root → repo_path, worktree → worktree_path, main_worktree → repo +▲ User config uses deprecated hook name: post-create → pre-start   diff --git a[TEST_CONFIG] b[TEST_CONFIG_NEW] -  index 89b3165..0643e7d 100644 +  index 89b3165..b3535c5 100644   --- a[TEST_CONFIG]   +++ b[TEST_CONFIG_NEW]   @@ -1,2 +1,2 @@   -worktree-path = "../{{ main_worktree }}.{{ branch }}"   -post-create = "ln -sf {{ repo_root }}/node_modules {{ worktree }}/node_modules"   +worktree-path = "../{{ repo }}.{{ branch }}" -  +post-create = "ln -sf {{ repo_path }}/node_modules {{ worktree_path }}/node_modules" +  +pre-start = "ln -sf {{ repo_path }}/node_modules {{ worktree_path }}/node_modules" ✓ Updated user config diff --git a/tests/snapshots/integration__integration_tests__help__nested_subcommand_hook_pre_start.snap b/tests/snapshots/integration__integration_tests__help__nested_subcommand_hook_pre_start.snap new file mode 100644 index 0000000000..31225ec46a --- /dev/null +++ b/tests/snapshots/integration__integration_tests__help__nested_subcommand_hook_pre_start.snap @@ -0,0 +1,39 @@ +--- +source: tests/integration_tests/help.rs +info: + program: wt + args: + - pre-start + env: + CLICOLOR_FORCE: "1" + COLUMNS: "500" + GIT_EDITOR: "" + LANG: C + LC_ALL: C + NO_COLOR: "" + PSModulePath: "" + RUST_LOG: warn + SHELL: "" + TERM: alacritty + WORKTRUNK_APPROVALS_PATH: /nonexistent/test/approvals.toml + WORKTRUNK_CONFIG_PATH: /nonexistent/test/config.toml + WORKTRUNK_SYSTEM_CONFIG_PATH: /etc/xdg/worktrunk/config.toml + WORKTRUNK_TEST_CLAUDE_INSTALLED: "0" + WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1" + WORKTRUNK_TEST_EPOCH: "1735776000" + WORKTRUNK_TEST_NUSHELL_ENV: "0" + WORKTRUNK_TEST_POWERSHELL_ENV: "0" + WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" +--- +success: false +exit_code: 2 +----- stdout ----- + +----- stderr ----- +error: unrecognized subcommand 'pre-start' + +Usage: wt [OPTIONS] [COMMAND] + +For more information, try '--help'. + + tip: perhaps wt hook pre-start? diff --git a/tests/snapshots/integration__integration_tests__merge__merge_cwd_removed_hint_fallback_to_list.snap b/tests/snapshots/integration__integration_tests__merge__merge_cwd_removed_hint_fallback_to_list.snap index 5b95fbbf8b..d37c8b4bda 100644 --- a/tests/snapshots/integration__integration_tests__merge__merge_cwd_removed_hint_fallback_to_list.snap +++ b/tests/snapshots/integration__integration_tests__merge__merge_cwd_removed_hint_fallback_to_list.snap @@ -23,7 +23,7 @@ info: NO_COLOR: "" PATH: "[PATH]" PSModulePath: "" - PWD: /private/var/folders/wf/s6ycxvvs4ln8qsdbfx40hnc40000gn/T/.tmpSjn7z3/repo + PWD: /private/var/folders/wf/s6ycxvvs4ln8qsdbfx40hnc40000gn/T/.tmpYBhGY0/repo RUST_LOG: warn SHELL: "" TERM: alacritty @@ -39,8 +39,8 @@ info: WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]" --- -success: false -exit_code: 1 +success: true +exit_code: 0 ----- stdout ----- ----- stderr ----- @@ -52,7 +52,4 @@ exit_code: 1 ◎ Removing feature worktree & branch in background (same commit as main, _) ▲ Cannot change directory — shell integration not installed ↳ To enable automatic cd, run wt config shell install -◎ Running post-merge project hook @ _REPO_ -  exit 1 -✗ Command failed: exit status: 1 -↳ Current directory was removed. Run wt list to see worktrees +◎ Running post-merge: project @ _REPO_ diff --git a/tests/snapshots/integration__integration_tests__merge__merge_cwd_removed_hint_no_recovery.snap b/tests/snapshots/integration__integration_tests__merge__merge_cwd_removed_hint_no_recovery.snap index 1d4577db53..16f01cfa12 100644 --- a/tests/snapshots/integration__integration_tests__merge__merge_cwd_removed_hint_no_recovery.snap +++ b/tests/snapshots/integration__integration_tests__merge__merge_cwd_removed_hint_no_recovery.snap @@ -23,7 +23,7 @@ info: NO_COLOR: "" PATH: "[PATH]" PSModulePath: "" - PWD: /private/var/folders/wf/s6ycxvvs4ln8qsdbfx40hnc40000gn/T/.tmpJw8YUo/repo.feature + PWD: /private/var/folders/wf/s6ycxvvs4ln8qsdbfx40hnc40000gn/T/.tmpwp292D/repo.feature RUST_LOG: warn SHELL: "" TERM: alacritty @@ -39,8 +39,8 @@ info: WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]" --- -success: false -exit_code: 1 +success: true +exit_code: 0 ----- stdout ----- ----- stderr ----- @@ -52,7 +52,4 @@ exit_code: 1 ◎ Removing feature worktree & branch in background (same commit as main, _) ▲ Cannot change directory — shell integration not installed ↳ To enable automatic cd, run wt config shell install -◎ Running post-merge project hook @ _REPO_ -  exit 1 -✗ Command failed: exit status: 1 -○ Current directory was removed +◎ Running post-merge: project @ _REPO_ diff --git a/tests/snapshots/integration__integration_tests__merge__merge_post_merge_command_failure.snap b/tests/snapshots/integration__integration_tests__merge__merge_post_merge_command_failure.snap index 6eed0bd549..431f50fab7 100644 --- a/tests/snapshots/integration__integration_tests__merge__merge_post_merge_command_failure.snap +++ b/tests/snapshots/integration__integration_tests__merge__merge_post_merge_command_failure.snap @@ -23,7 +23,7 @@ info: NO_COLOR: "" PATH: "[PATH]" PSModulePath: "" - PWD: /private/var/folders/wf/s6ycxvvs4ln8qsdbfx40hnc40000gn/T/.tmpzZbDpM/repo + PWD: /private/var/folders/wf/s6ycxvvs4ln8qsdbfx40hnc40000gn/T/.tmpqEGnx2/repo RUST_LOG: warn SHELL: "" TERM: alacritty @@ -39,8 +39,8 @@ info: WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]" --- -success: false -exit_code: 1 +success: true +exit_code: 0 ----- stdout ----- ----- stderr ----- @@ -52,7 +52,4 @@ exit_code: 1 ◎ Removing feature worktree & branch in background (same commit as main, _) ▲ Cannot change directory — shell integration not installed ↳ To enable automatic cd, run wt config shell install -◎ Running post-merge project hook @ _REPO_ -  exit 1 -✗ Command failed: exit status: 1 -↳ Current directory was removed. Try: wt switch ^ +◎ Running post-merge: project @ _REPO_ diff --git a/tests/snapshots/integration__integration_tests__merge__merge_post_merge_command_named.snap b/tests/snapshots/integration__integration_tests__merge__merge_post_merge_command_named.snap index 63db5f20bc..dd948888dc 100644 --- a/tests/snapshots/integration__integration_tests__merge__merge_post_merge_command_named.snap +++ b/tests/snapshots/integration__integration_tests__merge__merge_post_merge_command_named.snap @@ -51,7 +51,4 @@ exit_code: 0 ◎ Removing feature worktree & branch in background (same commit as main, _) ▲ Cannot change directory — shell integration not installed ↳ To enable automatic cd, run wt config shell install -◎ Running post-merge project:notify @ _REPO_ -  echo 'Merge to main complete' > notify.txt -◎ Running post-merge project:deploy @ _REPO_ -  echo 'Deploying branch feature' > deploy.txt +◎ Running post-merge: project:notify, project:deploy @ _REPO_ diff --git a/tests/snapshots/integration__integration_tests__merge__merge_post_merge_command_success.snap b/tests/snapshots/integration__integration_tests__merge__merge_post_merge_command_success.snap index 2d05a058d1..2d7e5c88ee 100644 --- a/tests/snapshots/integration__integration_tests__merge__merge_post_merge_command_success.snap +++ b/tests/snapshots/integration__integration_tests__merge__merge_post_merge_command_success.snap @@ -51,5 +51,4 @@ exit_code: 0 ◎ Removing feature worktree & branch in background (same commit as main, _) ▲ Cannot change directory — shell integration not installed ↳ To enable automatic cd, run wt config shell install -◎ Running post-merge project hook @ _REPO_ -  echo 'merged feature to main' > post-merge-ran.txt +◎ Running post-merge: project @ _REPO_ diff --git a/tests/snapshots/integration__integration_tests__merge__merge_post_merge_runs_from_main_branch.snap b/tests/snapshots/integration__integration_tests__merge__merge_post_merge_runs_from_main_branch.snap index 196230e83f..c096cb8524 100644 --- a/tests/snapshots/integration__integration_tests__merge__merge_post_merge_runs_from_main_branch.snap +++ b/tests/snapshots/integration__integration_tests__merge__merge_post_merge_runs_from_main_branch.snap @@ -44,5 +44,4 @@ exit_code: 0 ----- stderr ----- ○ Already up to date with main (no new commits, no rebase needed) ○ Worktree preserved (already on target branch) -◎ Running post-merge project hook -  echo 'post-merge ran from main' > post-merge-ran.txt +◎ Running post-merge: project diff --git a/tests/snapshots/integration__integration_tests__merge__merge_post_merge_runs_with_nothing_to_merge.snap b/tests/snapshots/integration__integration_tests__merge__merge_post_merge_runs_with_nothing_to_merge.snap index 3046e620fb..8ea5a3d0b5 100644 --- a/tests/snapshots/integration__integration_tests__merge__merge_post_merge_runs_with_nothing_to_merge.snap +++ b/tests/snapshots/integration__integration_tests__merge__merge_post_merge_runs_with_nothing_to_merge.snap @@ -47,5 +47,4 @@ exit_code: 0 ◎ Removing feature worktree & branch in background (same commit as main, _) ▲ Cannot change directory — shell integration not installed ↳ To enable automatic cd, run wt config shell install -◎ Running post-merge project hook @ _REPO_ -  echo 'post-merge ran' > post-merge-ran.txt +◎ Running post-merge: project @ _REPO_ diff --git a/tests/snapshots/integration__integration_tests__merge__readme_example_complex.snap b/tests/snapshots/integration__integration_tests__merge__readme_example_complex.snap index e13e0bd975..8214273141 100644 --- a/tests/snapshots/integration__integration_tests__merge__readme_example_complex.snap +++ b/tests/snapshots/integration__integration_tests__merge__readme_example_complex.snap @@ -81,10 +81,4 @@ test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fin ◎ Removing feature-auth worktree & branch in background (same commit as main, _) ▲ Cannot change directory — shell integration not installed ↳ To enable automatic cd, run wt config shell install -◎ Running post-merge project:install @ _REPO_ -  cargo install --path . - Installing worktrunk v0.1.0 - Compiling worktrunk v0.1.0 - Finished release [optimized] target(s) in 2.34s - Installing ~/.cargo/bin/wt - Installed package `worktrunk v0.1.0` (executable `wt`) +◎ Running post-merge: project:install @ _REPO_ diff --git a/tests/snapshots/integration__integration_tests__post_start_commands__both_create_and_start.snap b/tests/snapshots/integration__integration_tests__post_start_commands__both_create_and_start.snap index 240439b2c1..5e7e504ab1 100644 --- a/tests/snapshots/integration__integration_tests__post_start_commands__both_create_and_start.snap +++ b/tests/snapshots/integration__integration_tests__post_start_commands__both_create_and_start.snap @@ -43,7 +43,8 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create project hook @ _REPO_.feature +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start project hook @ _REPO_.feature   echo 'Setup done' > setup.txt ✓ Created branch feature from main and worktree @ _REPO_.feature ↳ To customize worktree locations, run wt config create diff --git a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_base_variables.snap b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_base_variables.snap index 4b078a388e..b342e628e6 100644 --- a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_base_variables.snap +++ b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_base_variables.snap @@ -45,9 +45,10 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create project:base @ _REPO_.feature +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start project:base @ _REPO_.feature   echo 'Base: main' > base_info.txt -◎ Running post-create project:base_path @ _REPO_.feature +◎ Running pre-start project:base_path @ _REPO_.feature   echo 'Base Path: _REPO_' >> base_info.txt ✓ Created branch feature from main and worktree @ _REPO_.feature ↳ To customize worktree locations, run wt config create diff --git a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_default_branch_template.snap b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_default_branch_template.snap index ac0c618b23..b3a4ee2630 100644 --- a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_default_branch_template.snap +++ b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_default_branch_template.snap @@ -44,7 +44,8 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create project hook @ _REPO_.feature +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start project hook @ _REPO_.feature   echo 'Default: main' > default.txt ✓ Created branch feature from main and worktree @ _REPO_.feature ↳ To customize worktree locations, run wt config create diff --git a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_failing_command.snap b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_failing_command.snap index c16d001294..aae955ba5f 100644 --- a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_failing_command.snap +++ b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_failing_command.snap @@ -43,7 +43,8 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create project hook @ _REPO_.feature +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start project hook @ _REPO_.feature   exit 1 ✗ Command failed: exit status: 1 ✓ Created branch feature from main and worktree @ _REPO_.feature diff --git a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_git_variables_template.snap b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_git_variables_template.snap index 4f0e4e42bc..a2ae057fc7 100644 --- a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_git_variables_template.snap +++ b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_git_variables_template.snap @@ -44,13 +44,14 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create project:commit @ _REPO_.feature +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start project:commit @ _REPO_.feature   echo 'Commit: f9680d376e0968fcb45a0029cf83898054742923' > git_vars.txt -◎ Running post-create project:short @ _REPO_.feature +◎ Running pre-start project:short @ _REPO_.feature   echo 'Short: f9680d3' >> git_vars.txt -◎ Running post-create project:remote @ _REPO_.feature +◎ Running pre-start project:remote @ _REPO_.feature   echo 'Remote: origin' >> git_vars.txt -◎ Running post-create project:worktree_name @ _REPO_.feature +◎ Running pre-start project:worktree_name @ _REPO_.feature   echo 'Worktree Name: repo.feature' >> git_vars.txt ✓ Created branch feature from main and worktree @ _REPO_.feature ↳ To customize worktree locations, run wt config create diff --git a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_multiline_control_structure.snap b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_multiline_control_structure.snap index 194d67692e..05e8ff842a 100644 --- a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_multiline_control_structure.snap +++ b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_multiline_control_structure.snap @@ -43,7 +43,8 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create project hook @ _REPO_.feature +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start project hook @ _REPO_.feature   if [ ! -f test.txt ]; then    echo 'File does not exist' > result.txt   else diff --git a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_named_commands.snap b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_named_commands.snap index 11fefb4304..1d61c89aa6 100644 --- a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_named_commands.snap +++ b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_named_commands.snap @@ -43,10 +43,11 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create project:install @ _REPO_.feature +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start project:install @ _REPO_.feature   echo 'Installing deps' Installing deps -◎ Running post-create project:setup @ _REPO_.feature +◎ Running pre-start project:setup @ _REPO_.feature   echo 'Running setup' Running setup ✓ Created branch feature from main and worktree @ _REPO_.feature diff --git a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_single_command.snap b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_single_command.snap index e0e0b56fc3..0885beab3e 100644 --- a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_single_command.snap +++ b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_single_command.snap @@ -43,7 +43,8 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create project hook @ _REPO_.feature +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start project hook @ _REPO_.feature   echo 'Setup complete' Setup complete ✓ Created branch feature from main and worktree @ _REPO_.feature diff --git a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_template_expansion.snap b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_template_expansion.snap index 0b20d6b417..93e3119771 100644 --- a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_template_expansion.snap +++ b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_template_expansion.snap @@ -43,15 +43,16 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create project:repo @ _REPO_.feature-test +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start project:repo @ _REPO_.feature-test   echo 'Repo: repo' > info.txt -◎ Running post-create project:branch @ _REPO_.feature-test +◎ Running pre-start project:branch @ _REPO_.feature-test   echo 'Branch: feature/test' >> info.txt -◎ Running post-create project:hash_port @ _REPO_.feature-test +◎ Running pre-start project:hash_port @ _REPO_.feature-test   echo 'Port: 18064' >> info.txt -◎ Running post-create project:worktree @ _REPO_.feature-test +◎ Running pre-start project:worktree @ _REPO_.feature-test   echo 'Worktree: _REPO_.feature-test' >> info.txt -◎ Running post-create project:root @ _REPO_.feature-test +◎ Running pre-start project:root @ _REPO_.feature-test   echo 'Root: _REPO_' >> info.txt ✓ Created branch feature/test from main and worktree @ _REPO_.feature-test ▲ Cannot change directory — shell integration not installed diff --git a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_upstream_conditional.snap b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_upstream_conditional.snap index 73c9f29a8d..7e9d319596 100644 --- a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_upstream_conditional.snap +++ b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_upstream_conditional.snap @@ -44,7 +44,8 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create project hook @ _REPO_.feature +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start project hook @ _REPO_.feature   echo 'no-upstream' > upstream.txt ✓ Created branch feature from main and worktree @ _REPO_.feature ↳ To customize worktree locations, run wt config create diff --git a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_upstream_template.snap b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_upstream_template.snap index f75c4c6180..d86bfd15da 100644 --- a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_upstream_template.snap +++ b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_upstream_template.snap @@ -44,6 +44,7 @@ exit_code: 1 ----- stdout ----- ----- stderr ----- -✗ Failed to expand project post-create hook: undefined value @ line 1 +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +✗ Failed to expand project pre-start hook: undefined value @ line 1   echo 'Upstream: {{ upstream }}' > upstream.txt ↳ Available variables: base, base_worktree_path, branch, commit, cwd, default_branch, hook_type, main_worktree, main_worktree_path, primary_worktree_path, remote, remote_url, repo, repo_path, repo_root, short_commit, worktree, worktree_name, worktree_path diff --git a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_verbose_template_expansion.snap b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_verbose_template_expansion.snap index 40a992d84f..5110757f0b 100644 --- a/tests/snapshots/integration__integration_tests__post_start_commands__post_create_verbose_template_expansion.snap +++ b/tests/snapshots/integration__integration_tests__post_start_commands__post_create_verbose_template_expansion.snap @@ -44,6 +44,7 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates ○ Expanding worktree-path   ../{{ repo }}.{{ branch | sanitize }}   → @@ -52,7 +53,7 @@ exit_code: 0   echo 'Setting up {{ branch | sanitize }} in {{ worktree_path }}'   →   echo 'Setting up verbose-hooks in _REPO_.verbose-hooks' -◎ Running post-create project:setup @ _REPO_.verbose-hooks +◎ Running pre-start project:setup @ _REPO_.verbose-hooks   echo 'Setting up verbose-hooks in _REPO_.verbose-hooks' Setting up verbose-hooks in _REPO_.verbose-hooks ✓ Created branch verbose-hooks from main and worktree @ _REPO_.verbose-hooks diff --git a/tests/snapshots/integration__integration_tests__user_hooks__no_verify_skips_all_hooks.snap b/tests/snapshots/integration__integration_tests__user_hooks__no_verify_skips_all_hooks.snap index 80b8765399..49edb219a3 100644 --- a/tests/snapshots/integration__integration_tests__user_hooks__no_verify_skips_all_hooks.snap +++ b/tests/snapshots/integration__integration_tests__user_hooks__no_verify_skips_all_hooks.snap @@ -44,6 +44,7 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- +▲ User config has deprecated settings. Run wt config show for details or wt config update to apply updates ✓ Created branch feature from main and worktree @ _REPO_.feature ↳ To customize worktree locations, run wt config create ▲ Cannot change directory — shell integration not installed diff --git a/tests/snapshots/integration__integration_tests__user_hooks__user_hook_template_vars.snap b/tests/snapshots/integration__integration_tests__user_hooks__user_hook_template_vars.snap index 74f3f1c426..55fdb9a15b 100644 --- a/tests/snapshots/integration__integration_tests__user_hooks__user_hook_template_vars.snap +++ b/tests/snapshots/integration__integration_tests__user_hooks__user_hook_template_vars.snap @@ -43,7 +43,8 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create user:vars @ _REPO_.feature +▲ User config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start user:vars @ _REPO_.feature   echo 'repo=repo branch=feature' > template_vars.txt ✓ Created branch feature from main and worktree @ _REPO_.feature ↳ To customize worktree locations, run wt config create diff --git a/tests/snapshots/integration__integration_tests__user_hooks__user_hooks_before_project.snap b/tests/snapshots/integration__integration_tests__user_hooks__user_hooks_before_project.snap index 83cbbc4406..8a7fb174dd 100644 --- a/tests/snapshots/integration__integration_tests__user_hooks__user_hooks_before_project.snap +++ b/tests/snapshots/integration__integration_tests__user_hooks__user_hooks_before_project.snap @@ -43,9 +43,11 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create user:log @ _REPO_.feature +▲ User config has deprecated settings. Run wt config show for details or wt config update to apply updates +▲ Project config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start user:log @ _REPO_.feature   echo 'USER_HOOK' >> hook_order.txt -◎ Running post-create project hook @ _REPO_.feature +◎ Running pre-start project hook @ _REPO_.feature   echo 'PROJECT_HOOK' >> hook_order.txt ✓ Created branch feature from main and worktree @ _REPO_.feature ↳ To customize worktree locations, run wt config create diff --git a/tests/snapshots/integration__integration_tests__user_hooks__user_hooks_no_approval.snap b/tests/snapshots/integration__integration_tests__user_hooks__user_hooks_no_approval.snap index 76611bb8f5..029242bf21 100644 --- a/tests/snapshots/integration__integration_tests__user_hooks__user_hooks_no_approval.snap +++ b/tests/snapshots/integration__integration_tests__user_hooks__user_hooks_no_approval.snap @@ -43,7 +43,8 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create user:setup @ _REPO_.feature +▲ User config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start user:setup @ _REPO_.feature   echo 'NO_APPROVAL_NEEDED' > no_approval.txt ✓ Created branch feature from main and worktree @ _REPO_.feature ↳ To customize worktree locations, run wt config create diff --git a/tests/snapshots/integration__integration_tests__user_hooks__user_hooks_preserve_order.snap b/tests/snapshots/integration__integration_tests__user_hooks__user_hooks_preserve_order.snap index f58700d5c3..de22d41ec7 100644 --- a/tests/snapshots/integration__integration_tests__user_hooks__user_hooks_preserve_order.snap +++ b/tests/snapshots/integration__integration_tests__user_hooks__user_hooks_preserve_order.snap @@ -43,13 +43,14 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create user:vscode @ _REPO_.feature +▲ User config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start user:vscode @ _REPO_.feature   echo '1' >> hook_order.txt -◎ Running post-create user:claude @ _REPO_.feature +◎ Running pre-start user:claude @ _REPO_.feature   echo '2' >> hook_order.txt -◎ Running post-create user:copy @ _REPO_.feature +◎ Running pre-start user:copy @ _REPO_.feature   echo '3' >> hook_order.txt -◎ Running post-create user:submodule @ _REPO_.feature +◎ Running pre-start user:submodule @ _REPO_.feature   echo '4' >> hook_order.txt ✓ Created branch feature from main and worktree @ _REPO_.feature ↳ To customize worktree locations, run wt config create diff --git a/tests/snapshots/integration__integration_tests__user_hooks__user_post_create_executes.snap b/tests/snapshots/integration__integration_tests__user_hooks__user_post_create_executes.snap index f389e1993a..84c80bc9d8 100644 --- a/tests/snapshots/integration__integration_tests__user_hooks__user_post_create_executes.snap +++ b/tests/snapshots/integration__integration_tests__user_hooks__user_post_create_executes.snap @@ -43,7 +43,8 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create user:log @ _REPO_.feature +▲ User config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start user:log @ _REPO_.feature   echo 'USER_POST_CREATE_RAN' > user_hook_marker.txt ✓ Created branch feature from main and worktree @ _REPO_.feature ↳ To customize worktree locations, run wt config create diff --git a/tests/snapshots/integration__integration_tests__user_hooks__user_post_create_failure.snap b/tests/snapshots/integration__integration_tests__user_hooks__user_post_create_failure.snap index 3aca54b29c..568e37ebb5 100644 --- a/tests/snapshots/integration__integration_tests__user_hooks__user_post_create_failure.snap +++ b/tests/snapshots/integration__integration_tests__user_hooks__user_post_create_failure.snap @@ -43,7 +43,8 @@ exit_code: 0 ----- stdout ----- ----- stderr ----- -◎ Running post-create user:failing @ _REPO_.feature +▲ User config has deprecated settings. Run wt config show for details or wt config update to apply updates +◎ Running pre-start user:failing @ _REPO_.feature   exit 1 ✗ Command failing failed: exit status: 1 ✓ Created branch feature from main and worktree @ _REPO_.feature diff --git a/tests/snapshots/integration__integration_tests__user_hooks__user_post_merge_executes.snap b/tests/snapshots/integration__integration_tests__user_hooks__user_post_merge_executes.snap index 7be9a209e5..356cc723b3 100644 --- a/tests/snapshots/integration__integration_tests__user_hooks__user_post_merge_executes.snap +++ b/tests/snapshots/integration__integration_tests__user_hooks__user_post_merge_executes.snap @@ -21,16 +21,20 @@ info: LANG: C LC_ALL: C MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]" + NO_COLOR: "" PATH: "[PATH]" PSModulePath: "" RUST_LOG: warn SHELL: "" TERM: alacritty USERPROFILE: "[TEST_HOME]" + WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]" WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]" + WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]" WORKTRUNK_TEST_CLAUDE_INSTALLED: "0" WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1" WORKTRUNK_TEST_EPOCH: "1735776000" + WORKTRUNK_TEST_NUSHELL_ENV: "0" WORKTRUNK_TEST_POWERSHELL_ENV: "0" WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]" @@ -46,5 +50,4 @@ exit_code: 0   1 file changed, 1 insertion(+) ✓ Merged to main (1 commit, 1 file, +1) ○ Worktree preserved (--no-remove) -◎ Running post-merge user:notify @ _REPO_ -  echo 'USER_POST_MERGE_RAN' > user_postmerge.txt +◎ Running post-merge: user:notify @ _REPO_ From 2edfc42de1a9db3da8afa8030a734a01b4d23f54 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 22 Mar 2026 21:50:23 -0700 Subject: [PATCH 2/9] docs: update hook docs for rationalized naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update `after_long_help` in cli/mod.rs (primary source) and all synced docs/skills files to reflect the hook rationalization: - post-create → pre-start in all examples, tables, and section headers - Add post-commit section with description and example - Mark post-merge as non-blocking (background) in hook types table - Update merge pipeline order to show background hooks - Update cli/step.rs copy-ignored help text Co-Authored-By: Claude --- dev/config.example.toml | 2 +- docs/content/config.md | 4 +- docs/content/hook.md | 74 +++++++++--------- docs/content/switch.md | 2 +- skills/worktrunk/reference/config.md | 4 +- skills/worktrunk/reference/hook.md | 72 ++++++++--------- skills/worktrunk/reference/switch.md | 2 +- src/cli/mod.rs | 78 ++++++++++--------- ...ation_tests__help__help_config_create.snap | 2 +- ...gration_tests__help__help_config_long.snap | 4 +- ...gration_tests__help__help_switch_long.snap | 2 +- 11 files changed, 126 insertions(+), 120 deletions(-) diff --git a/dev/config.example.toml b/dev/config.example.toml index f71078c48f..d431fb20fa 100644 --- a/dev/config.example.toml +++ b/dev/config.example.toml @@ -146,7 +146,7 @@ # worktree-path = ".worktrees/{{ branch | sanitize }}" # list.full = true # merge.squash = false -# post-create.env = "cp .env.example .env" +# pre-start.env = "cp .env.example .env" # aliases.deploy = "make deploy BRANCH={{ branch }}" # # ### Custom prompt templates diff --git a/docs/content/config.md b/docs/content/config.md index 28bd2cc200..2e9912359b 100644 --- a/docs/content/config.md +++ b/docs/content/config.md @@ -60,7 +60,7 @@ command = "CLAUDECODE= MAX_THINKING_TOKENS=0 claude -p --no-session-persistence ```toml # .config/wt.toml -[post-create] +[pre-start] deps = "npm ci" [pre-merge] @@ -239,7 +239,7 @@ Override global user config for a specific project. Scalar values (like `worktre worktree-path = ".worktrees/{{ branch | sanitize }}" list.full = true merge.squash = false -post-create.env = "cp .env.example .env" +pre-start.env = "cp .env.example .env" aliases.deploy = "make deploy BRANCH={{ branch }}" ``` diff --git a/docs/content/hook.md b/docs/content/hook.md index 67d4c44c01..0ab9917926 100644 --- a/docs/content/hook.md +++ b/docs/content/hook.md @@ -15,22 +15,15 @@ Hooks are shell commands that run at key points in the worktree lifecycle — au # Hook Types -| Hook | When | Blocking | Fail-fast | -|------|------|----------|-----------| -| `pre-switch` | Before every switch | Yes | Yes | -| `post-create` | After worktree created | Yes | No | -| `post-start` | After worktree created | No | No | -| `post-switch` | After every switch | No | No | -| `pre-commit` | Before commit during merge | Yes | Yes | -| `pre-merge` | Before merging to target | Yes | Yes | -| `post-merge` | After successful merge | Yes | No | -| `pre-remove` | Before worktree removed | Yes | Yes | -| `post-remove` | After worktree removed | No | No | - -**Blocking**: Command waits for hook to complete before continuing. -**Fail-fast**: First failure aborts the operation. - -Background hooks show a single-line summary by default. Use `-v` to see expanded command details. +| Event | `pre-` (blocking) | `post-` (background) | +|-------|-------------------|---------------------| +| **start** | `pre-start` | `post-start` | +| **switch** | `pre-switch` | `post-switch` | +| **commit** | `pre-commit` | `post-commit` | +| **merge** | `pre-merge` | `post-merge` | +| **remove** | `pre-remove` | `post-remove` | + +`pre-*` hooks block — failure aborts the operation (except `pre-start`, which warns and continues). `post-*` hooks run in the background. Use `-v` to see expanded command details for background hooks. The most common starting point is `post-start` — it runs background tasks (dev servers, file copying, builds) when creating a worktree. @@ -49,12 +42,12 @@ fi """ ``` -## post-create +## pre-start Tasks that must complete before `post-start` hooks or `--execute` run: dependency installation, environment file generation. ```toml -[post-create] +[pre-start] install = "npm ci" env = "echo 'PORT={{ branch | hash_port }}' > .env.local" ``` @@ -88,6 +81,15 @@ format = "cargo fmt -- --check" lint = "cargo clippy -- -D warnings" ``` +## post-commit + +Background tasks after a successful commit: CI triggers, notifications, background linting. Output logged to `.git/wt/logs/{branch}-{source}-post-commit-{name}.log`. Respects `--no-verify`. + +```toml +[post-commit] +notify = "curl -s https://ci.example.com/trigger?branch={{ branch }}" +``` + ## pre-merge Tests, security scans, build verification — runs after rebase, before merge to target. @@ -100,7 +102,7 @@ build = "cargo build --release" ## post-merge -Deployment, notifications, installing updated binaries. Runs in the target branch worktree if it exists, otherwise the main worktree. +Deployment, notifications, installing updated binaries. Runs in background in the target branch worktree if it exists, otherwise the main worktree. Output logged to `.git/wt/logs/{branch}-{source}-post-merge-{name}.log`. ```toml post-merge = "cargo install --path ." @@ -125,7 +127,7 @@ kill-server = "lsof -ti :{{ branch | hash_port }} -sTCP:LISTEN | xargs kill 2>/d remove-db = "docker stop {{ repo }}-{{ branch | sanitize }}-postgres 2>/dev/null || true" ``` -During `wt merge`, hooks run in this order: pre-commit → pre-merge → pre-remove → post-remove → post-merge. See [`wt merge`](@/merge.md#pipeline) for the complete pipeline. +During `wt merge`, hooks run in this order: pre-commit → post-commit (background) → pre-merge → pre-remove → post-remove + post-merge (background). See [`wt merge`](@/merge.md#pipeline) for the complete pipeline. # Security @@ -134,7 +136,7 @@ Project commands require approval on first run: ``` ▲ repo needs approval to execute 3 commands: -○ post-create install: +○ pre-start install: echo 'Installing dependencies...' ❯ Allow and remember? [y/N] @@ -157,7 +159,7 @@ Project hooks are defined in `.config/wt.toml`. They can be a single command or ```toml # Single command (string) -post-create = "npm install" +pre-start = "npm install" # Multiple commands (table) — run sequentially in declaration order [pre-merge] @@ -171,7 +173,7 @@ Define hooks in `~/.config/worktrunk/config.toml` to run for all repositories. U ```toml # ~/.config/worktrunk/config.toml -[post-create] +[pre-start] setup = "echo 'Setting up worktree...'" [pre-merge] @@ -219,7 +221,7 @@ Hooks can use template variables that expand at runtime: | `{{ default_branch }}` | Default branch name | | `{{ remote }}` | Primary remote name | | `{{ remote_url }}` | Remote URL | -| `{{ hook_type }}` | Hook type being run (e.g. `post-create`, `pre-merge`) | +| `{{ hook_type }}` | Hook type being run (e.g. `pre-start`, `pre-merge`) | | `{{ hook_name }}` | Hook command name (if named) | Bare variables (`branch`, `worktree_path`, `commit`) refer to the branch the operation acts on: the destination for switch/create, the source for merge/remove. `base` and `target` give the other side: @@ -235,7 +237,7 @@ Pre and post hooks share the same perspective — `{{ branch | hash_port }}` pro Some variables are conditional: `upstream` requires remote tracking; `base`/`target` are only in two-worktree hooks. Undefined variables error — use conditionals: ```toml -[post-create] +[pre-start] # Rebase onto upstream if tracking a remote branch (e.g., wt switch --create feature origin/feature) sync = "{% if upstream %}git fetch && git rebase {{ upstream }}{% endif %}" ``` @@ -286,7 +288,7 @@ Templates also support functions for dynamic lookups: The `worktree_path_of_branch` function returns the filesystem path of a worktree given a branch name, or an empty string if no worktree exists for that branch. This is useful for referencing files in other worktrees: ```toml -[post-create] +[pre-start] # Copy config from main worktree setup = "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path }}" ``` @@ -296,8 +298,8 @@ setup = "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path Hooks receive all template variables as JSON on stdin, enabling complex logic that templates can't express: ```toml -[post-create] -setup = "python3 scripts/post-create-setup.py" +[pre-start] +setup = "python3 scripts/pre-start-setup.py" ``` ```python @@ -319,7 +321,7 @@ wt hook pre-merge project: # Run all project hooks wt hook pre-merge user:test # Run only user's "test" hook wt hook pre-merge project:test # Run only project's "test" hook wt hook pre-merge --yes # Skip approval prompts (for CI) -wt hook post-create --var branch=feature/test # Override template variable +wt hook pre-start --var branch=feature/test # Override template variable ``` The `user:` and `project:` prefixes filter by source. Use `user:` or `project:` alone to run all hooks from that source, or `user:name` / `project:name` to run a specific hook. @@ -328,14 +330,14 @@ The `--var KEY=VALUE` flag overrides built-in template variables — useful for # Designing Effective Hooks -## post-start vs post-create +## post-start vs pre-start Both run when creating a worktree. The difference: | Hook | Execution | Best for | |------|-----------|----------| | `post-start` | Background, parallel | Long-running tasks that don't block worktree creation | -| `post-create` | Blocks until complete | Tasks the developer needs before working (dependency install) | +| `pre-start` | Blocks until complete | Tasks the developer needs before working (dependency install) | Many tasks work well in `post-start` — they'll likely be ready by the time they're needed, especially when the fallback is recompiling. If unsure, prefer `post-start` for faster worktree creation. @@ -350,10 +352,10 @@ Git worktrees share the repository but not untracked files. [`wt step copy-ignor copy = "wt step copy-ignored" ``` -Use `post-create` instead if subsequent hooks need the copied files — for example, copying `node_modules/` before `pnpm install` so the install reuses cached packages: +Use `pre-start` instead if subsequent hooks need the copied files — for example, copying `node_modules/` before `pnpm install` so the install reuses cached packages: ```toml -[post-create] +[pre-start] copy = "wt step copy-ignored" install = "pnpm install" ``` @@ -409,7 +411,7 @@ Jinja2's operator precedence has pipe `|` with higher precedence than concatenat Generate `.env.local` with the connection string: ```toml -[post-create] +[pre-start] env = """ cat > .env.local << EOF DATABASE_URL=postgres://postgres:dev@localhost:{{ ('db-' ~ branch) | hash_port }}/{{ branch | sanitize_db }} @@ -451,7 +453,7 @@ fi Use `uv sync` to recreate virtual environments (or `python -m venv .venv && .venv/bin/pip install -r requirements.txt` for pip-based projects): ```toml -[post-create] +[pre-start] install = "uv sync" ``` @@ -460,7 +462,7 @@ For copying dependencies and caches between worktrees, see [`wt step copy-ignore ## See also - [`wt merge`](@/merge.md) — Runs hooks automatically during merge -- [`wt switch`](@/switch.md) — Runs post-create/post-start hooks on `--create` +- [`wt switch`](@/switch.md) — Runs pre-start/post-start hooks on `--create` - [`wt config`](@/config.md) — Manage hook approvals - [`wt config state logs`](@/config.md#wt-config-state-logs) — Access background hook logs diff --git a/docs/content/switch.md b/docs/content/switch.md index b290a2fd52..47df0dd25c 100644 --- a/docs/content/switch.md +++ b/docs/content/switch.md @@ -43,7 +43,7 @@ When creating a worktree, worktrunk: 1. Runs [pre-switch hooks](@/hook.md#pre-switch) (blocking, fail-fast) 2. Creates worktree at configured path 3. Switches to new directory -4. Runs [post-create hooks](@/hook.md#post-create) (blocking) +4. Runs [pre-start hooks](@/hook.md#pre-start) (blocking) 5. Spawns [post-start hooks](@/hook.md#post-start) (background) 6. Spawns [post-switch hooks](@/hook.md#post-switch) (background) diff --git a/skills/worktrunk/reference/config.md b/skills/worktrunk/reference/config.md index ca7d368ab9..0cc1f191d0 100644 --- a/skills/worktrunk/reference/config.md +++ b/skills/worktrunk/reference/config.md @@ -51,7 +51,7 @@ command = "CLAUDECODE= MAX_THINKING_TOKENS=0 claude -p --no-session-persistence ```toml # .config/wt.toml -[post-create] +[pre-start] deps = "npm ci" [pre-merge] @@ -230,7 +230,7 @@ Override global user config for a specific project. Scalar values (like `worktre worktree-path = ".worktrees/{{ branch | sanitize }}" list.full = true merge.squash = false -post-create.env = "cp .env.example .env" +pre-start.env = "cp .env.example .env" aliases.deploy = "make deploy BRANCH={{ branch }}" ``` diff --git a/skills/worktrunk/reference/hook.md b/skills/worktrunk/reference/hook.md index b5c95cf8ab..a9f1bd0889 100644 --- a/skills/worktrunk/reference/hook.md +++ b/skills/worktrunk/reference/hook.md @@ -6,22 +6,15 @@ Hooks are shell commands that run at key points in the worktree lifecycle — au # Hook Types -| Hook | When | Blocking | Fail-fast | -|------|------|----------|-----------| -| `pre-switch` | Before every switch | Yes | Yes | -| `post-create` | After worktree created | Yes | No | -| `post-start` | After worktree created | No | No | -| `post-switch` | After every switch | No | No | -| `pre-commit` | Before commit during merge | Yes | Yes | -| `pre-merge` | Before merging to target | Yes | Yes | -| `post-merge` | After successful merge | Yes | No | -| `pre-remove` | Before worktree removed | Yes | Yes | -| `post-remove` | After worktree removed | No | No | - -**Blocking**: Command waits for hook to complete before continuing. -**Fail-fast**: First failure aborts the operation. - -Background hooks show a single-line summary by default. Use `-v` to see expanded command details. +| Event | `pre-` (blocking) | `post-` (background) | +|-------|-------------------|---------------------| +| **start** | `pre-start` | `post-start` | +| **switch** | `pre-switch` | `post-switch` | +| **commit** | `pre-commit` | `post-commit` | +| **merge** | `pre-merge` | `post-merge` | +| **remove** | `pre-remove` | `post-remove` | + +`pre-*` hooks block — failure aborts the operation (except `pre-start`, which warns and continues). `post-*` hooks run in the background. Use `-v` to see expanded command details for background hooks. The most common starting point is `post-start` — it runs background tasks (dev servers, file copying, builds) when creating a worktree. @@ -40,12 +33,12 @@ fi """ ``` -## post-create +## pre-start Tasks that must complete before `post-start` hooks or `--execute` run: dependency installation, environment file generation. ```toml -[post-create] +[pre-start] install = "npm ci" env = "echo 'PORT={{ branch | hash_port }}' > .env.local" ``` @@ -79,6 +72,15 @@ format = "cargo fmt -- --check" lint = "cargo clippy -- -D warnings" ``` +## post-commit + +Background tasks after a successful commit: CI triggers, notifications, background linting. Output logged to `.git/wt/logs/{branch}-{source}-post-commit-{name}.log`. Respects `--no-verify`. + +```toml +[post-commit] +notify = "curl -s https://ci.example.com/trigger?branch={{ branch }}" +``` + ## pre-merge Tests, security scans, build verification — runs after rebase, before merge to target. @@ -91,7 +93,7 @@ build = "cargo build --release" ## post-merge -Deployment, notifications, installing updated binaries. Runs in the target branch worktree if it exists, otherwise the main worktree. +Deployment, notifications, installing updated binaries. Runs in background in the target branch worktree if it exists, otherwise the main worktree. Output logged to `.git/wt/logs/{branch}-{source}-post-merge-{name}.log`. ```toml post-merge = "cargo install --path ." @@ -116,7 +118,7 @@ kill-server = "lsof -ti :{{ branch | hash_port }} -sTCP:LISTEN | xargs kill 2>/d remove-db = "docker stop {{ repo }}-{{ branch | sanitize }}-postgres 2>/dev/null || true" ``` -During `wt merge`, hooks run in this order: pre-commit → pre-merge → pre-remove → post-remove → post-merge. See [`wt merge`](https://worktrunk.dev/merge/#pipeline) for the complete pipeline. +During `wt merge`, hooks run in this order: pre-commit → post-commit (background) → pre-merge → pre-remove → post-remove + post-merge (background). See [`wt merge`](https://worktrunk.dev/merge/#pipeline) for the complete pipeline. # Security @@ -125,7 +127,7 @@ Project commands require approval on first run: ``` ▲ repo needs approval to execute 3 commands: -○ post-create install: +○ pre-start install: echo 'Installing dependencies...' ❯ Allow and remember? [y/N] @@ -148,7 +150,7 @@ Project hooks are defined in `.config/wt.toml`. They can be a single command or ```toml # Single command (string) -post-create = "npm install" +pre-start = "npm install" # Multiple commands (table) — run sequentially in declaration order [pre-merge] @@ -162,7 +164,7 @@ Define hooks in `~/.config/worktrunk/config.toml` to run for all repositories. U ```toml # ~/.config/worktrunk/config.toml -[post-create] +[pre-start] setup = "echo 'Setting up worktree...'" [pre-merge] @@ -210,7 +212,7 @@ Hooks can use template variables that expand at runtime: | `{{ default_branch }}` | Default branch name | | `{{ remote }}` | Primary remote name | | `{{ remote_url }}` | Remote URL | -| `{{ hook_type }}` | Hook type being run (e.g. `post-create`, `pre-merge`) | +| `{{ hook_type }}` | Hook type being run (e.g. `pre-start`, `pre-merge`) | | `{{ hook_name }}` | Hook command name (if named) | Bare variables (`branch`, `worktree_path`, `commit`) refer to the branch the operation acts on: the destination for switch/create, the source for merge/remove. `base` and `target` give the other side: @@ -226,7 +228,7 @@ Pre and post hooks share the same perspective — `{{ branch | hash_port }}` pro Some variables are conditional: `upstream` requires remote tracking; `base`/`target` are only in two-worktree hooks. Undefined variables error — use conditionals: ```toml -[post-create] +[pre-start] # Rebase onto upstream if tracking a remote branch (e.g., wt switch --create feature origin/feature) sync = "{% if upstream %}git fetch && git rebase {{ upstream }}{% endif %}" ``` @@ -277,7 +279,7 @@ Templates also support functions for dynamic lookups: The `worktree_path_of_branch` function returns the filesystem path of a worktree given a branch name, or an empty string if no worktree exists for that branch. This is useful for referencing files in other worktrees: ```toml -[post-create] +[pre-start] # Copy config from main worktree setup = "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path }}" ``` @@ -287,8 +289,8 @@ setup = "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path Hooks receive all template variables as JSON on stdin, enabling complex logic that templates can't express: ```toml -[post-create] -setup = "python3 scripts/post-create-setup.py" +[pre-start] +setup = "python3 scripts/pre-start-setup.py" ``` ```python @@ -310,7 +312,7 @@ wt hook pre-merge project: # Run all project hooks wt hook pre-merge user:test # Run only user's "test" hook wt hook pre-merge project:test # Run only project's "test" hook wt hook pre-merge --yes # Skip approval prompts (for CI) -wt hook post-create --var branch=feature/test # Override template variable +wt hook pre-start --var branch=feature/test # Override template variable ``` The `user:` and `project:` prefixes filter by source. Use `user:` or `project:` alone to run all hooks from that source, or `user:name` / `project:name` to run a specific hook. @@ -319,14 +321,14 @@ The `--var KEY=VALUE` flag overrides built-in template variables — useful for # Designing Effective Hooks -## post-start vs post-create +## post-start vs pre-start Both run when creating a worktree. The difference: | Hook | Execution | Best for | |------|-----------|----------| | `post-start` | Background, parallel | Long-running tasks that don't block worktree creation | -| `post-create` | Blocks until complete | Tasks the developer needs before working (dependency install) | +| `pre-start` | Blocks until complete | Tasks the developer needs before working (dependency install) | Many tasks work well in `post-start` — they'll likely be ready by the time they're needed, especially when the fallback is recompiling. If unsure, prefer `post-start` for faster worktree creation. @@ -341,10 +343,10 @@ Git worktrees share the repository but not untracked files. [`wt step copy-ignor copy = "wt step copy-ignored" ``` -Use `post-create` instead if subsequent hooks need the copied files — for example, copying `node_modules/` before `pnpm install` so the install reuses cached packages: +Use `pre-start` instead if subsequent hooks need the copied files — for example, copying `node_modules/` before `pnpm install` so the install reuses cached packages: ```toml -[post-create] +[pre-start] copy = "wt step copy-ignored" install = "pnpm install" ``` @@ -400,7 +402,7 @@ Jinja2's operator precedence has pipe `|` with higher precedence than concatenat Generate `.env.local` with the connection string: ```toml -[post-create] +[pre-start] env = """ cat > .env.local << EOF DATABASE_URL=postgres://postgres:dev@localhost:{{ ('db-' ~ branch) | hash_port }}/{{ branch | sanitize_db }} @@ -442,7 +444,7 @@ fi Use `uv sync` to recreate virtual environments (or `python -m venv .venv && .venv/bin/pip install -r requirements.txt` for pip-based projects): ```toml -[post-create] +[pre-start] install = "uv sync" ``` diff --git a/skills/worktrunk/reference/switch.md b/skills/worktrunk/reference/switch.md index 27c2c98a53..4105050d34 100644 --- a/skills/worktrunk/reference/switch.md +++ b/skills/worktrunk/reference/switch.md @@ -27,7 +27,7 @@ When creating a worktree, worktrunk: 1. Runs [pre-switch hooks](https://worktrunk.dev/hook/#pre-switch) (blocking, fail-fast) 2. Creates worktree at configured path 3. Switches to new directory -4. Runs [post-create hooks](https://worktrunk.dev/hook/#post-create) (blocking) +4. Runs [pre-start hooks](https://worktrunk.dev/hook/#pre-start) (blocking) 5. Spawns [post-start hooks](https://worktrunk.dev/hook/#post-start) (background) 6. Spawns [post-switch hooks](https://worktrunk.dev/hook/#post-switch) (background) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 08f41c63e1..43c7c254e8 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -275,7 +275,7 @@ When creating a worktree, worktrunk: 1. Runs [pre-switch hooks](@/hook.md#pre-switch) (blocking, fail-fast) 2. Creates worktree at configured path 3. Switches to new directory -4. Runs [post-create hooks](@/hook.md#post-create) (blocking) +4. Runs [pre-start hooks](@/hook.md#pre-start) (blocking) 5. Spawns [post-start hooks](@/hook.md#post-start) (background) 6. Spawns [post-switch hooks](@/hook.md#post-switch) (background) @@ -1102,22 +1102,15 @@ Alias names that match a built-in step command (`commit`, `squash`, etc.) are sh # Hook Types -| Hook | When | Blocking | Fail-fast | -|------|------|----------|-----------| -| `pre-switch` | Before every switch | Yes | Yes | -| `post-create` | After worktree created | Yes | No | -| `post-start` | After worktree created | No | No | -| `post-switch` | After every switch | No | No | -| `pre-commit` | Before commit during merge | Yes | Yes | -| `pre-merge` | Before merging to target | Yes | Yes | -| `post-merge` | After successful merge | Yes | No | -| `pre-remove` | Before worktree removed | Yes | Yes | -| `post-remove` | After worktree removed | No | No | +| Event | `pre-` (blocking) | `post-` (background) | +|-------|-------------------|---------------------| +| **start** | `pre-start` | `post-start` | +| **switch** | `pre-switch` | `post-switch` | +| **commit** | `pre-commit` | `post-commit` | +| **merge** | `pre-merge` | `post-merge` | +| **remove** | `pre-remove` | `post-remove` | -**Blocking**: Command waits for hook to complete before continuing. -**Fail-fast**: First failure aborts the operation. - -Background hooks show a single-line summary by default. Use `-v` to see expanded command details. +`pre-*` hooks block — failure aborts the operation (except `pre-start`, which warns and continues). `post-*` hooks run in the background. Use `-v` to see expanded command details for background hooks. The most common starting point is `post-start` — it runs background tasks (dev servers, file copying, builds) when creating a worktree. @@ -1136,12 +1129,12 @@ fi """ ``` -## post-create +## pre-start Tasks that must complete before `post-start` hooks or `--execute` run: dependency installation, environment file generation. ```toml -[post-create] +[pre-start] install = "npm ci" env = "echo 'PORT={{ branch | hash_port }}' > .env.local" ``` @@ -1175,6 +1168,15 @@ format = "cargo fmt -- --check" lint = "cargo clippy -- -D warnings" ``` +## post-commit + +Background tasks after a successful commit: CI triggers, notifications, background linting. Output logged to `.git/wt/logs/{branch}-{source}-post-commit-{name}.log`. Respects `--no-verify`. + +```toml +[post-commit] +notify = "curl -s https://ci.example.com/trigger?branch={{ branch }}" +``` + ## pre-merge Tests, security scans, build verification — runs after rebase, before merge to target. @@ -1187,7 +1189,7 @@ build = "cargo build --release" ## post-merge -Deployment, notifications, installing updated binaries. Runs in the target branch worktree if it exists, otherwise the main worktree. +Deployment, notifications, installing updated binaries. Runs in background in the target branch worktree if it exists, otherwise the main worktree. Output logged to `.git/wt/logs/{branch}-{source}-post-merge-{name}.log`. ```toml post-merge = "cargo install --path ." @@ -1212,7 +1214,7 @@ kill-server = "lsof -ti :{{ branch | hash_port }} -sTCP:LISTEN | xargs kill 2>/d remove-db = "docker stop {{ repo }}-{{ branch | sanitize }}-postgres 2>/dev/null || true" ``` -During `wt merge`, hooks run in this order: pre-commit → pre-merge → pre-remove → post-remove → post-merge. See [`wt merge`](@/merge.md#pipeline) for the complete pipeline. +During `wt merge`, hooks run in this order: pre-commit → post-commit (background) → pre-merge → pre-remove → post-remove + post-merge (background). See [`wt merge`](@/merge.md#pipeline) for the complete pipeline. # Security @@ -1221,7 +1223,7 @@ Project commands require approval on first run: ``` ▲ repo needs approval to execute 3 commands: -○ post-create install: +○ pre-start install: echo 'Installing dependencies...' ❯ Allow and remember? [y/N] @@ -1244,7 +1246,7 @@ Project hooks are defined in `.config/wt.toml`. They can be a single command or ```toml # Single command (string) -post-create = "npm install" +pre-start = "npm install" # Multiple commands (table) — run sequentially in declaration order [pre-merge] @@ -1258,7 +1260,7 @@ Define hooks in `~/.config/worktrunk/config.toml` to run for all repositories. U ```toml # ~/.config/worktrunk/config.toml -[post-create] +[pre-start] setup = "echo 'Setting up worktree...'" [pre-merge] @@ -1306,7 +1308,7 @@ Hooks can use template variables that expand at runtime: | `{{ default_branch }}` | Default branch name | | `{{ remote }}` | Primary remote name | | `{{ remote_url }}` | Remote URL | -| `{{ hook_type }}` | Hook type being run (e.g. `post-create`, `pre-merge`) | +| `{{ hook_type }}` | Hook type being run (e.g. `pre-start`, `pre-merge`) | | `{{ hook_name }}` | Hook command name (if named) | Bare variables (`branch`, `worktree_path`, `commit`) refer to the branch the operation acts on: the destination for switch/create, the source for merge/remove. `base` and `target` give the other side: @@ -1322,7 +1324,7 @@ Pre and post hooks share the same perspective — `{{ branch | hash_port }}` pro Some variables are conditional: `upstream` requires remote tracking; `base`/`target` are only in two-worktree hooks. Undefined variables error — use conditionals: ```toml -[post-create] +[pre-start] # Rebase onto upstream if tracking a remote branch (e.g., wt switch --create feature origin/feature) sync = "{% if upstream %}git fetch && git rebase {{ upstream }}{% endif %}" ``` @@ -1373,7 +1375,7 @@ Templates also support functions for dynamic lookups: The `worktree_path_of_branch` function returns the filesystem path of a worktree given a branch name, or an empty string if no worktree exists for that branch. This is useful for referencing files in other worktrees: ```toml -[post-create] +[pre-start] # Copy config from main worktree setup = "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path }}" ``` @@ -1383,8 +1385,8 @@ setup = "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path Hooks receive all template variables as JSON on stdin, enabling complex logic that templates can't express: ```toml -[post-create] -setup = "python3 scripts/post-create-setup.py" +[pre-start] +setup = "python3 scripts/pre-start-setup.py" ``` ```python @@ -1406,7 +1408,7 @@ wt hook pre-merge project: # Run all project hooks wt hook pre-merge user:test # Run only user's "test" hook wt hook pre-merge project:test # Run only project's "test" hook wt hook pre-merge --yes # Skip approval prompts (for CI) -wt hook post-create --var branch=feature/test # Override template variable +wt hook pre-start --var branch=feature/test # Override template variable ``` The `user:` and `project:` prefixes filter by source. Use `user:` or `project:` alone to run all hooks from that source, or `user:name` / `project:name` to run a specific hook. @@ -1415,14 +1417,14 @@ The `--var KEY=VALUE` flag overrides built-in template variables — useful for # Designing Effective Hooks -## post-start vs post-create +## post-start vs pre-start Both run when creating a worktree. The difference: | Hook | Execution | Best for | |------|-----------|----------| | `post-start` | Background, parallel | Long-running tasks that don't block worktree creation | -| `post-create` | Blocks until complete | Tasks the developer needs before working (dependency install) | +| `pre-start` | Blocks until complete | Tasks the developer needs before working (dependency install) | Many tasks work well in `post-start` — they'll likely be ready by the time they're needed, especially when the fallback is recompiling. If unsure, prefer `post-start` for faster worktree creation. @@ -1437,10 +1439,10 @@ Git worktrees share the repository but not untracked files. [`wt step copy-ignor copy = "wt step copy-ignored" ``` -Use `post-create` instead if subsequent hooks need the copied files — for example, copying `node_modules/` before `pnpm install` so the install reuses cached packages: +Use `pre-start` instead if subsequent hooks need the copied files — for example, copying `node_modules/` before `pnpm install` so the install reuses cached packages: ```toml -[post-create] +[pre-start] copy = "wt step copy-ignored" install = "pnpm install" ``` @@ -1496,7 +1498,7 @@ Jinja2's operator precedence has pipe `|` with higher precedence than concatenat Generate `.env.local` with the connection string: ```toml -[post-create] +[pre-start] env = """ cat > .env.local << EOF DATABASE_URL=postgres://postgres:dev@localhost:{{ ('db-' ~ branch) | hash_port }}/{{ branch | sanitize_db }} @@ -1538,7 +1540,7 @@ fi Use `uv sync` to recreate virtual environments (or `python -m venv .venv && .venv/bin/pip install -r requirements.txt` for pip-based projects): ```toml -[post-create] +[pre-start] install = "uv sync" ``` @@ -1547,7 +1549,7 @@ For copying dependencies and caches between worktrees, see [`wt step copy-ignore ## See also - [`wt merge`](@/merge.md) — Runs hooks automatically during merge -- [`wt switch`](@/switch.md) — Runs post-create/post-start hooks on `--create` +- [`wt switch`](@/switch.md) — Runs pre-start/post-start hooks on `--create` - [`wt config`](@/config.md) — Manage hook approvals - [`wt config state logs`](@/config.md#wt-config-state-logs) — Access background hook logs @@ -1612,7 +1614,7 @@ command = "CLAUDECODE= MAX_THINKING_TOKENS=0 claude -p --no-session-persistence ```toml # .config/wt.toml -[post-create] +[pre-start] deps = "npm ci" [pre-merge] @@ -1791,7 +1793,7 @@ Override global user config for a specific project. Scalar values (like `worktre worktree-path = ".worktrees/{{ branch | sanitize }}" list.full = true merge.squash = false -post-create.env = "cp .env.example .env" +pre-start.env = "cp .env.example .env" aliases.deploy = "make deploy BRANCH={{ branch }}" ``` diff --git a/tests/snapshots/integration__integration_tests__help__help_config_create.snap b/tests/snapshots/integration__integration_tests__help__help_config_create.snap index 67ec1120ed..8a7f1216ac 100644 --- a/tests/snapshots/integration__integration_tests__help__help_config_create.snap +++ b/tests/snapshots/integration__integration_tests__help__help_config_create.snap @@ -205,7 +205,7 @@ Creates ~/.config/worktrunk/config.toml with the following content:   # worktree-path = ".worktrees/{{ branch | sanitize }}"   # list.full = true   # merge.squash = false -  # post-create.env = "cp .env.example .env" +  # pre-start.env = "cp .env.example .env"   # aliases.deploy = "make deploy BRANCH={{ branch }}"   #   # ### Custom prompt templates diff --git a/tests/snapshots/integration__integration_tests__help__help_config_long.snap b/tests/snapshots/integration__integration_tests__help__help_config_long.snap index 71f49ca7bd..3157e33899 100644 --- a/tests/snapshots/integration__integration_tests__help__help_config_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_config_long.snap @@ -96,7 +96,7 @@ Organizations can also deploy a system-wide config file for shared defaults — Project config — shared team settings:   # .config/wt.toml -  [post-create] +  [pre-start]   deps = "npm ci"     [pre-merge] @@ -250,7 +250,7 @@ Override global user config for a specific project. Scalar values (like work   worktree-path = ".worktrees/{{ branch | sanitize }}"   list.full = true   merge.squash = false -  post-create.env = "cp .env.example .env" +  pre-start.env = "cp .env.example .env"   aliases.deploy = "make deploy BRANCH={{ branch }}" Custom prompt templates diff --git a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap index 7a89850b0f..e029b5d820 100644 --- a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap @@ -131,7 +131,7 @@ When creating a worktree, worktrunk: 1. Runs pre-switch hooks (blocking, fail-fast) 2. Creates worktree at configured path 3. Switches to new directory -4. Runs post-create hooks (blocking) +4. Runs pre-start hooks (blocking) 5. Spawns post-start hooks (background) 6. Spawns post-switch hooks (background) From cacc2eb7d6e2f8c2943d9b00eb66ae2313c7f496 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 22 Mar 2026 22:40:07 -0700 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20improve=20post-create=20deprecation?= =?UTF-8?q?=20UX=20=E2=80=94=20tests,=20merge=20safety,=20empty=20table=20?= =?UTF-8?q?handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 23 unit tests for post-create deprecation (detection, migration, integration, snapshot) matching the coverage pattern of existing deprecations (select, commit-generation) - Fix merge_with to fold post_create into pre_start so hooks from unmigrated configs aren't silently dropped when merging with configs that use the new name - Filter empty [post-create] tables from deprecation detection, consistent with [select] handling Co-Authored-By: Claude --- src/config/deprecation.rs | 301 +++++++++++++++++- src/config/hooks.rs | 12 +- ...shot_migrate_post_create_to_pre_start.snap | 9 + src/config/user/tests.rs | 58 +++- 4 files changed, 372 insertions(+), 8 deletions(-) create mode 100644 src/config/snapshots/worktrunk__config__deprecation__tests__snapshot_migrate_post_create_to_pre_start.snap diff --git a/src/config/deprecation.rs b/src/config/deprecation.rs index cd3a93f65d..42272aa8fa 100644 --- a/src/config/deprecation.rs +++ b/src/config/deprecation.rs @@ -518,21 +518,22 @@ pub fn find_select_deprecation(content: &str) -> bool { /// /// Checks both top-level hooks (`post-create = "..."`) and project hooks /// (`[projects."...".hooks] post-create = "..."`). This handles both user config -/// and project config formats. +/// and project config formats. Empty tables are ignored (no-op configs don't +/// need migration warnings). pub fn find_post_create_deprecation(content: &str) -> bool { let Ok(doc) = content.parse::() else { return false; }; // Check top-level hooks section (project config: `post-create = "..."`) - if doc.get("pre-start").is_none() && doc.get("post-create").is_some() { + if doc.get("pre-start").is_none() && doc.get("post-create").is_some_and(is_non_empty_item) { return true; } // Check [hooks] section (user config: `[hooks] post-create = "..."`) if let Some(hooks) = doc.get("hooks").and_then(|h| h.as_table()) && hooks.get("pre-start").is_none() - && hooks.get("post-create").is_some() + && hooks.get("post-create").is_some_and(is_non_empty_item) { return true; } @@ -543,7 +544,7 @@ pub fn find_post_create_deprecation(content: &str) -> bool { if let Some(project_table) = project_value.as_table() && let Some(hooks) = project_table.get("hooks").and_then(|h| h.as_table()) && hooks.get("pre-start").is_none() - && hooks.get("post-create").is_some() + && hooks.get("post-create").is_some_and(is_non_empty_item) { return true; } @@ -553,6 +554,15 @@ pub fn find_post_create_deprecation(content: &str) -> bool { false } +/// Check if a TOML item is non-empty (strings are always non-empty, tables must have entries). +fn is_non_empty_item(item: &toml_edit::Item) -> bool { + match item { + toml_edit::Item::Value(toml_edit::Value::InlineTable(t)) => !t.is_empty(), + toml_edit::Item::Table(t) => !t.is_empty(), + _ => true, // strings and other values are always "non-empty" + } +} + /// Migrate `post-create` hooks to `pre-start`. /// /// Renames `post-create` to `pre-start` in hooks sections. Skips if `pre-start` already exists. @@ -2479,4 +2489,287 @@ pager = "delta --paging=never" "Migrated content should not have [select]: {migrated}" ); } + + // --- post-create → pre-start deprecation tests --- + + #[test] + fn test_find_post_create_deprecation_none() { + // No post-create, no deprecation + let content = r#" +pre-start = "npm install" +"#; + assert!(!find_post_create_deprecation(content)); + } + + #[test] + fn test_find_post_create_deprecation_top_level() { + // Project config format: bare key + let content = r#" +post-create = "npm install" +"#; + assert!(find_post_create_deprecation(content)); + } + + #[test] + fn test_find_post_create_deprecation_hooks_section() { + // User config format: under [hooks] + let content = r#" +[hooks] +post-create = "npm install" +"#; + assert!(find_post_create_deprecation(content)); + } + + #[test] + fn test_find_post_create_deprecation_project_level() { + // User config format: under [projects."...".hooks] + let content = r#" +[projects."my-project".hooks] +post-create = "npm install" +"#; + assert!(find_post_create_deprecation(content)); + } + + #[test] + fn test_find_post_create_deprecation_named_commands() { + // Named command table format + let content = r#" +[post-create] +lint = "npm run lint" +build = "npm run build" +"#; + assert!(find_post_create_deprecation(content)); + } + + #[test] + fn test_find_post_create_deprecation_empty_table_not_flagged() { + // Empty [post-create] table is a no-op — don't warn + let content = r#" +[post-create] +"#; + assert!(!find_post_create_deprecation(content)); + } + + #[test] + fn test_find_post_create_deprecation_skips_when_pre_start_exists_top_level() { + // Both present at top level — don't flag + let content = r#" +post-create = "old" +pre-start = "new" +"#; + assert!(!find_post_create_deprecation(content)); + } + + #[test] + fn test_find_post_create_deprecation_skips_when_pre_start_exists_hooks() { + // Both present in [hooks] — don't flag + let content = r#" +[hooks] +post-create = "old" +pre-start = "new" +"#; + assert!(!find_post_create_deprecation(content)); + } + + #[test] + fn test_find_post_create_deprecation_skips_when_pre_start_exists_project() { + // Both present in project hooks — don't flag + let content = r#" +[projects."my-project".hooks] +post-create = "old" +pre-start = "new" +"#; + assert!(!find_post_create_deprecation(content)); + } + + #[test] + fn test_migrate_post_create_top_level() { + let content = r#" +post-create = "npm install" + +[post-start] +server = "npm run dev" +"#; + let result = migrate_post_create_to_pre_start(content); + assert!( + result.contains("pre-start"), + "Should have pre-start: {result}" + ); + assert!( + !result.contains("post-create"), + "Should not have post-create: {result}" + ); + assert!( + result.contains("[post-start]"), + "Should preserve other sections: {result}" + ); + } + + #[test] + fn test_migrate_post_create_hooks_section() { + let content = r#" +[hooks] +post-create = "npm install" +"#; + let result = migrate_post_create_to_pre_start(content); + assert!( + result.contains("pre-start"), + "Should have pre-start: {result}" + ); + assert!( + !result.contains("post-create"), + "Should not have post-create: {result}" + ); + } + + #[test] + fn test_migrate_post_create_project_level() { + let content = r#" +[projects."my-project".hooks] +post-create = "npm install" +"#; + let result = migrate_post_create_to_pre_start(content); + assert!( + result.contains("pre-start"), + "Should have pre-start: {result}" + ); + assert!( + !result.contains("post-create"), + "Should not have post-create: {result}" + ); + } + + #[test] + fn test_migrate_post_create_named_commands() { + let content = r#" +[post-create] +lint = "npm run lint" +build = "npm run build" +"#; + let result = migrate_post_create_to_pre_start(content); + assert!( + result.contains("[pre-start]"), + "Should rename section header: {result}" + ); + assert!( + !result.contains("[post-create]"), + "Should not have old section header: {result}" + ); + assert!( + result.contains("lint = \"npm run lint\""), + "Should preserve named commands: {result}" + ); + } + + #[test] + fn test_migrate_post_create_skips_when_pre_start_exists() { + let content = r#" +post-create = "old" +pre-start = "new" +"#; + let result = migrate_post_create_to_pre_start(content); + assert_eq!( + result, content, + "Should not migrate when pre-start already exists" + ); + } + + #[test] + fn test_migrate_post_create_invalid_toml() { + let content = "this is { not valid toml"; + let result = migrate_post_create_to_pre_start(content); + assert_eq!(result, content, "Invalid TOML should be returned unchanged"); + } + + #[test] + fn test_migrate_post_create_no_post_create() { + let content = r#" +pre-start = "npm install" +"#; + let result = migrate_post_create_to_pre_start(content); + assert_eq!(result, content, "No post-create means no migration"); + } + + #[test] + fn test_detect_deprecations_includes_post_create() { + let content = r#" +post-create = "npm install" +"#; + let deprecations = detect_deprecations(content); + assert!(deprecations.post_create); + assert!(!deprecations.is_empty()); + } + + #[test] + fn snapshot_migrate_post_create_to_pre_start() { + let content = r#"post-create = "npm install" + +[post-start] +server = "npm run dev" +"#; + let result = migrate_post_create_to_pre_start(content); + insta::assert_snapshot!(migration_diff(content, &result)); + } + + #[test] + fn test_format_deprecation_details_post_create() { + let info = DeprecationInfo { + config_path: std::path::PathBuf::from("/tmp/test-config.toml"), + migration_path: None, + deprecations: Deprecations { + vars: vec![], + commit_gen: CommitGenerationDeprecations::default(), + approved_commands: false, + select: false, + post_create: true, + }, + label: "Project config".to_string(), + main_worktree_path: None, + approvals_copied_to: None, + }; + let output = format_deprecation_details(&info); + assert!( + output.contains("post-create"), + "Should mention post-create: {output}" + ); + assert!( + output.contains("pre-start"), + "Should mention pre-start: {output}" + ); + } + + #[test] + fn test_write_migration_file_with_post_create() { + let temp_dir = tempfile::TempDir::new().unwrap(); + let config_path = temp_dir.path().join("wt.toml"); + let content = r#"post-create = "npm install" + +[post-start] +server = "npm run dev" +"#; + std::fs::write(&config_path, content).unwrap(); + + let deprecations = Deprecations { + vars: vec![], + commit_gen: CommitGenerationDeprecations::default(), + approved_commands: false, + select: false, + post_create: true, + }; + let result = write_migration_file(&config_path, content, &deprecations, None); + assert!( + result.is_some(), + "Should write migration file for post_create" + ); + let migration_path = result.unwrap(); + let migrated = std::fs::read_to_string(&migration_path).unwrap(); + assert!( + migrated.contains("pre-start"), + "Migrated content should have pre-start: {migrated}" + ); + assert!( + !migrated.contains("post-create"), + "Migrated content should not have post-create: {migrated}" + ); + } } diff --git a/src/config/hooks.rs b/src/config/hooks.rs index a9f382fb7c..e02add12a7 100644 --- a/src/config/hooks.rs +++ b/src/config/hooks.rs @@ -126,11 +126,19 @@ impl Merge for HooksConfig { /// Merge two hook configs using append semantics. /// /// Both global and per-project hooks run (global first, then per-project). + /// Deprecated `post_create` from either source is folded into `pre_start` + /// so hooks aren't silently dropped when one config uses the old name and + /// another uses the new name. fn merge_with(&self, other: &Self) -> Self { + // Fold post_create into pre_start for each source before merging, + // so cross-config old/new name combinations work correctly. + let self_pre_start = merge_append_hooks(&self.pre_start, &self.post_create); + let other_pre_start = merge_append_hooks(&other.pre_start, &other.post_create); + Self { pre_switch: merge_append_hooks(&self.pre_switch, &other.pre_switch), - pre_start: merge_append_hooks(&self.pre_start, &other.pre_start), - post_create: merge_append_hooks(&self.post_create, &other.post_create), + pre_start: merge_append_hooks(&self_pre_start, &other_pre_start), + post_create: None, // folded into pre_start above post_start: merge_append_hooks(&self.post_start, &other.post_start), post_switch: merge_append_hooks(&self.post_switch, &other.post_switch), pre_commit: merge_append_hooks(&self.pre_commit, &other.pre_commit), diff --git a/src/config/snapshots/worktrunk__config__deprecation__tests__snapshot_migrate_post_create_to_pre_start.snap b/src/config/snapshots/worktrunk__config__deprecation__tests__snapshot_migrate_post_create_to_pre_start.snap new file mode 100644 index 0000000000..60e2f8a4a9 --- /dev/null +++ b/src/config/snapshots/worktrunk__config__deprecation__tests__snapshot_migrate_post_create_to_pre_start.snap @@ -0,0 +1,9 @@ +--- +source: src/config/deprecation.rs +expression: "migration_diff(content, &result)" +--- +-post-create = "npm install" ++pre-start = "npm install" + + [post-start] + server = "npm run dev" diff --git a/src/config/user/tests.rs b/src/config/user/tests.rs index 36950859bf..a844a84439 100644 --- a/src/config/user/tests.rs +++ b/src/config/user/tests.rs @@ -1,6 +1,6 @@ use super::*; use crate::config::HooksConfig; -use crate::git::Repository; +use crate::git::{HookType, Repository}; /// Test fixture that creates a real temporary git repository. struct TestRepo { @@ -2029,7 +2029,6 @@ fn test_hooks_in_overridable_config_is_empty() { /// which matches the serde field names. #[test] fn test_valid_user_config_keys_includes_all_hook_types() { - use crate::git::HookType; use strum::IntoEnumIterator; let valid_keys = valid_user_config_keys(); @@ -2338,6 +2337,61 @@ fn test_hooks_merge_trait_appends_for_global_project_merge() { assert_eq!(commands[1].template, "project-lint"); // Project second } +#[test] +fn test_hooks_merge_folds_post_create_into_pre_start() { + // User config uses deprecated `post-create`, project uses `pre-start`. + // merge_with should combine them so the user's hook isn't silently dropped. + let user_hooks = parse_hooks("post-create = \"npm install\""); + let project_hooks = parse_hooks("pre-start = \"cargo test\""); + + let merged = user_hooks.merge_with(&project_hooks); + let pre_start = merged + .get(HookType::PreStart) + .expect("should have pre-start"); + let commands = pre_start.commands(); + assert_eq!(commands.len(), 2, "Both hooks should be present"); + assert_eq!(commands[0].template, "npm install"); // User's post-create first + assert_eq!(commands[1].template, "cargo test"); // Project's pre-start second +} + +#[test] +fn test_hooks_merge_same_source_both_pre_start_and_post_create() { + // Single config with both fields — merge_with folds post_create into pre_start. + // This is an unusual config (user wrote both), but if it goes through merge + // both commands should run rather than silently dropping one. + let both = parse_hooks("pre-start = \"npm install\"\npost-create = \"cargo build\""); + let empty = HooksConfig::default(); + + let merged = both.merge_with(&empty); + let pre_start = merged + .get(HookType::PreStart) + .expect("should have pre-start"); + let commands = pre_start.commands(); + assert_eq!( + commands.len(), + 2, + "Both commands from same source should be present" + ); + assert_eq!(commands[0].template, "npm install"); // pre-start first + assert_eq!(commands[1].template, "cargo build"); // post-create second +} + +#[test] +fn test_hooks_merge_post_create_both_sides() { + // Both configs use deprecated `post-create` — should still combine + let global = parse_hooks("post-create = \"npm install\""); + let project = parse_hooks("post-create = \"cargo build\""); + + let merged = global.merge_with(&project); + let pre_start = merged + .get(HookType::PreStart) + .expect("should have pre-start"); + let commands = pre_start.commands(); + assert_eq!(commands.len(), 2); + assert_eq!(commands[0].template, "npm install"); + assert_eq!(commands[1].template, "cargo build"); +} + /// Test that reload_projects_from handles permission errors /// when the config file exists but cannot be read. #[cfg(unix)] From 73c64e6131ceeab00e9f06e55b2636d3e4d7acec Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 22 Mar 2026 22:57:14 -0700 Subject: [PATCH 4/9] =?UTF-8?q?test:=20update=20PTY=20snapshots=20for=20po?= =?UTF-8?q?st-create=20=E2=86=92=20pre-start=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update test fixtures in approval_pty, config_update_pty, and shell_wrapper to use `pre-start` instead of `post-create`. Accept 12 updated PTY snapshots reflecting the new hook name in approval prompts and execution output. Co-Authored-By: Claude --- tests/integration_tests/approval_pty.rs | 18 ++++++++--------- tests/integration_tests/shell_wrapper.rs | 20 +++++++++---------- ..._approval_pty__approval_prompt_accept.snap | 4 ++-- ...approval_pty__approval_prompt_decline.snap | 2 +- ...ompt_mixed_approved_unapproved_accept.snap | 10 +++++----- ...mpt_mixed_approved_unapproved_decline.snap | 4 ++-- ...ty__approval_prompt_multiple_commands.snap | 12 +++++------ ...l_pty__approval_prompt_named_commands.snap | 12 +++++------ ...pty__approval_prompt_permission_error.snap | 4 ++-- ...date_pty__config_update_prompt_accept.snap | 5 +++-- ...ate_pty__config_update_prompt_decline.snap | 5 +++-- ...tests__readme_example_approval_prompt.snap | 6 +++--- ...sts__readme_example_hooks_post_create.snap | 2 +- ...er__unix_tests__switch_with_hooks_zsh.snap | 4 ++-- 14 files changed, 55 insertions(+), 53 deletions(-) diff --git a/tests/integration_tests/approval_pty.rs b/tests/integration_tests/approval_pty.rs index 823fb3a5d8..f93628e42e 100644 --- a/tests/integration_tests/approval_pty.rs +++ b/tests/integration_tests/approval_pty.rs @@ -70,7 +70,7 @@ fn test_approval_prompt_accept(repo: TestRepo) { // Remove origin so worktrunk uses directory name as project identifier repo.run_git(&["remote", "remove", "origin"]); - repo.write_project_config(r#"post-create = "echo 'test command'""#); + repo.write_project_config(r#"pre-start = "echo 'test command'""#); repo.commit("Add config"); // Configure shell integration so we get the "Restart shell" hint instead of the prompt @@ -94,7 +94,7 @@ fn test_approval_prompt_decline(repo: TestRepo) { // Remove origin so worktrunk uses directory name as project identifier repo.run_git(&["remote", "remove", "origin"]); - repo.write_project_config(r#"post-create = "echo 'test command'""#); + repo.write_project_config(r#"pre-start = "echo 'test command'""#); repo.commit("Add config"); // Configure shell integration so we get the "Restart shell" hint instead of the prompt @@ -119,7 +119,7 @@ fn test_approval_prompt_multiple_commands(repo: TestRepo) { repo.run_git(&["remote", "remove", "origin"]); repo.write_project_config( - r#"[post-create] + r#"[pre-start] first = "echo 'First command'" second = "echo 'Second command'" third = "echo 'Third command'" @@ -150,7 +150,7 @@ fn test_approval_prompt_permission_error(repo: TestRepo) { // Remove origin so worktrunk uses directory name as project identifier repo.run_git(&["remote", "remove", "origin"]); - repo.write_project_config(r#"post-create = "echo 'test command'""#); + repo.write_project_config(r#"pre-start = "echo 'test command'""#); repo.commit("Add config"); // Configure shell integration before making the approvals directory read-only @@ -224,7 +224,7 @@ fn test_approval_prompt_named_commands(repo: TestRepo) { repo.run_git(&["remote", "remove", "origin"]); repo.write_project_config( - r#"[post-create] + r#"[pre-start] install = "echo 'Installing dependencies...'" build = "echo 'Building project...'" test = "echo 'Running tests...'" @@ -266,7 +266,7 @@ fn test_approval_prompt_mixed_approved_unapproved_accept(repo: TestRepo) { repo.run_git(&["remote", "remove", "origin"]); repo.write_project_config( - r#"[post-create] + r#"[pre-start] first = "echo 'First command'" second = "echo 'Second command'" third = "echo 'Third command'" @@ -322,7 +322,7 @@ fn test_approval_prompt_mixed_approved_unapproved_decline(repo: TestRepo) { repo.run_git(&["remote", "remove", "origin"]); repo.write_project_config( - r#"[post-create] + r#"[pre-start] first = "echo 'First command'" second = "echo 'Second command'" third = "echo 'Third command'" @@ -363,9 +363,9 @@ approved-commands = ["echo 'Second command'"] "Should show 'Commands declined' message" ); // Commands appear in the prompt, but should not be executed - // Check for "Running post-create" which indicates execution + // Check for "Running pre-start" which indicates execution assert!( - !output.contains("Running post-create"), + !output.contains("Running pre-start"), "Should NOT execute any commands when declined" ); assert!( diff --git a/tests/integration_tests/shell_wrapper.rs b/tests/integration_tests/shell_wrapper.rs index 9eaaf87bd9..ead9cb05e7 100644 --- a/tests/integration_tests/shell_wrapper.rs +++ b/tests/integration_tests/shell_wrapper.rs @@ -1053,7 +1053,7 @@ mod unix_tests { ); } - /// Test switch --create with post-create (blocking) and post-start (background) + /// Test switch --create with pre-start (blocking) and post-start (background) /// Note: bash and fish disabled due to flaky PTY buffering race conditions /// /// TODO: Fix timing/race condition in bash where "Building project..." output appears @@ -1064,13 +1064,13 @@ mod unix_tests { #[case("zsh")] // #[case("fish")] // TODO: Fish shell has non-deterministic PTY output ordering fn test_wrapper_switch_with_hooks(#[case] shell: &str, repo: TestRepo) { - // Create project config with both post-create and post-start hooks + // Create project config with both pre-start and post-start hooks let config_dir = repo.root_path().join(".config"); fs::create_dir_all(&config_dir).unwrap(); fs::write( config_dir.join("wt.toml"), r#"# Blocking commands that run before worktree is ready -[post-create] +[pre-start] install = "echo 'Installing dependencies...'" build = "echo 'Building project...'" @@ -2620,10 +2620,10 @@ command = "{}" shell_wrapper_settings().bind(|| assert_snapshot!(&output.combined)); } - /// README example: Creating worktree with post-create and post-start hooks + /// README example: Creating worktree with pre-start and post-start hooks /// /// This test demonstrates: - /// - Post-create hooks (install dependencies) + /// - Pre-start hooks (install dependencies) /// - Post-start hooks (start dev server) /// /// Uses shell wrapper to avoid "To enable automatic cd" hint. @@ -2631,7 +2631,7 @@ command = "{}" /// Source: tests/snapshots/shell_wrapper__tests__readme_example_hooks_post_create.snap #[rstest] fn test_readme_example_hooks_post_create(repo: TestRepo) { - // Create project config with post-create and post-start hooks + // Create project config with pre-start and post-start hooks let config_dir = repo.root_path().join(".config"); fs::create_dir_all(&config_dir).unwrap(); @@ -2667,7 +2667,7 @@ fi } let config_content = r#" -[post-create] +[pre-start] "install" = "uv sync" [post-start] @@ -2700,7 +2700,7 @@ fi shell_wrapper_settings().bind(|| assert_snapshot!(&output.combined)); } - /// README example: approval prompt for post-create commands + /// README example: approval prompt for pre-start commands /// This test captures just the prompt (before responding) to show what users see. /// /// Note: This uses direct PTY execution (not shell wrapper) because interactive prompts @@ -2714,9 +2714,9 @@ fi // Remove origin so worktrunk uses directory name as project identifier repo.run_git(&["remote", "remove", "origin"]); - // Create project config with named post-create commands + // Create project config with named pre-start commands repo.write_project_config( - r#"[post-create] + r#"[pre-start] install = "echo 'Installing dependencies...'" build = "echo 'Building project...'" test = "echo 'Running tests...'" diff --git a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_accept.snap b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_accept.snap index 0eac35b30f..e1af7a28db 100644 --- a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_accept.snap +++ b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_accept.snap @@ -3,11 +3,11 @@ source: tests/integration_tests/approval_pty.rs expression: "&output" --- ▲ repo needs approval to execute 1 command: -○ post-create: +○ pre-start:   echo 'test command' ❯ Allow and remember? [y/N] y -◎ Running post-create project hook @ _REPO_.test-approve +◎ Running pre-start project hook @ _REPO_.test-approve   echo 'test command' test command ✓ Created branch test-approve from main and worktree @ _REPO_.test-approve diff --git a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_decline.snap b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_decline.snap index ce92576524..3c9e127763 100644 --- a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_decline.snap +++ b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_decline.snap @@ -3,7 +3,7 @@ source: tests/integration_tests/approval_pty.rs expression: "&output" --- ▲ repo needs approval to execute 1 command: -○ post-create: +○ pre-start:   echo 'test command' ❯ Allow and remember? [y/N] n diff --git a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_mixed_approved_unapproved_accept.snap b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_mixed_approved_unapproved_accept.snap index 8e8ffa7626..2fecbf9f12 100644 --- a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_mixed_approved_unapproved_accept.snap +++ b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_mixed_approved_unapproved_accept.snap @@ -3,19 +3,19 @@ source: tests/integration_tests/approval_pty.rs expression: "&output" --- ▲ repo needs approval to execute 2 commands: -○ post-create first: +○ pre-start first:   echo 'First command' -○ post-create third: +○ pre-start third:   echo 'Third command' ❯ Allow and remember? [y/N] y -◎ Running post-create project:first @ _REPO_.test-mixed-accept +◎ Running pre-start project:first @ _REPO_.test-mixed-accept   echo 'First command' First command -◎ Running post-create project:second @ _REPO_.test-mixed-accept +◎ Running pre-start project:second @ _REPO_.test-mixed-accept   echo 'Second command' Second command -◎ Running post-create project:third @ _REPO_.test-mixed-accept +◎ Running pre-start project:third @ _REPO_.test-mixed-accept   echo 'Third command' Third command ✓ Created branch test-mixed-accept from main and worktree @ _REPO_.test-mixed-accept diff --git a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_mixed_approved_unapproved_decline.snap b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_mixed_approved_unapproved_decline.snap index 0ab6a0c8fe..46a89f1576 100644 --- a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_mixed_approved_unapproved_decline.snap +++ b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_mixed_approved_unapproved_decline.snap @@ -3,9 +3,9 @@ source: tests/integration_tests/approval_pty.rs expression: "&output" --- ▲ repo needs approval to execute 2 commands: -○ post-create first: +○ pre-start first:   echo 'First command' -○ post-create third: +○ pre-start third:   echo 'Third command' ❯ Allow and remember? [y/N] n diff --git a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_multiple_commands.snap b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_multiple_commands.snap index 210a6a3421..b1f00710b4 100644 --- a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_multiple_commands.snap +++ b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_multiple_commands.snap @@ -3,21 +3,21 @@ source: tests/integration_tests/approval_pty.rs expression: "&output" --- ▲ repo needs approval to execute 3 commands: -○ post-create first: +○ pre-start first:   echo 'First command' -○ post-create second: +○ pre-start second:   echo 'Second command' -○ post-create third: +○ pre-start third:   echo 'Third command' ❯ Allow and remember? [y/N] y -◎ Running post-create project:first @ _REPO_.test-multi +◎ Running pre-start project:first @ _REPO_.test-multi   echo 'First command' First command -◎ Running post-create project:second @ _REPO_.test-multi +◎ Running pre-start project:second @ _REPO_.test-multi   echo 'Second command' Second command -◎ Running post-create project:third @ _REPO_.test-multi +◎ Running pre-start project:third @ _REPO_.test-multi   echo 'Third command' Third command ✓ Created branch test-multi from main and worktree @ _REPO_.test-multi diff --git a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_named_commands.snap b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_named_commands.snap index 624ac84a33..70df985299 100644 --- a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_named_commands.snap +++ b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_named_commands.snap @@ -3,21 +3,21 @@ source: tests/integration_tests/approval_pty.rs expression: "&output" --- ▲ repo needs approval to execute 3 commands: -○ post-create install: +○ pre-start install:   echo 'Installing dependencies...' -○ post-create build: +○ pre-start build:   echo 'Building project...' -○ post-create test: +○ pre-start test:   echo 'Running tests...' ❯ Allow and remember? [y/N] y -◎ Running post-create project:install @ _REPO_.test-named +◎ Running pre-start project:install @ _REPO_.test-named   echo 'Installing dependencies...' Installing dependencies... -◎ Running post-create project:build @ _REPO_.test-named +◎ Running pre-start project:build @ _REPO_.test-named   echo 'Building project...' Building project... -◎ Running post-create project:test @ _REPO_.test-named +◎ Running pre-start project:test @ _REPO_.test-named   echo 'Running tests...' Running tests... ✓ Created branch test-named from main and worktree @ _REPO_.test-named diff --git a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_permission_error.snap b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_permission_error.snap index 642f6fc6c6..38062520e2 100644 --- a/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_permission_error.snap +++ b/tests/snapshots/integration__integration_tests__approval_pty__approval_prompt_permission_error.snap @@ -3,13 +3,13 @@ source: tests/integration_tests/approval_pty.rs expression: "&output" --- ▲ repo needs approval to execute 1 command: -○ post-create: +○ pre-start:   echo 'test command' ❯ Allow and remember? [y/N] y ▲ Failed to save command approval: Failed to open lock file: Permission denied (os error 13) ↳ Approval will be requested again next time. -◎ Running post-create project hook @ _REPO_.test-permission +◎ Running pre-start project hook @ _REPO_.test-permission   echo 'test command' test command ✓ Created branch test-permission from main and worktree @ _REPO_.test-permission diff --git a/tests/snapshots/integration__integration_tests__config_update_pty__config_update_prompt_accept.snap b/tests/snapshots/integration__integration_tests__config_update_pty__config_update_prompt_accept.snap index ce4bebfca0..14a855d0ad 100644 --- a/tests/snapshots/integration__integration_tests__config_update_pty__config_update_prompt_accept.snap +++ b/tests/snapshots/integration__integration_tests__config_update_pty__config_update_prompt_accept.snap @@ -3,15 +3,16 @@ source: tests/integration_tests/config_update_pty.rs expression: "&output" --- ▲ User config uses deprecated template variables: repo_root → repo_path, main_worktree → repo +▲ User config uses deprecated hook name: post-create → pre-start   diff --git a[TEST_CONFIG] b[TEST_CONFIG_NEW] -  index d95d100..ceb381d 100644 +  index d95d100..5eef8a6 100644   --- a[TEST_CONFIG]   +++ b[TEST_CONFIG_NEW]   @@ -1,2 +1,2 @@   -worktree-path = "../{{ main_worktree }}.{{ branch }}"   -post-create = "ln -sf {{ repo_root }}/node_modules"   +worktree-path = "../{{ repo }}.{{ branch }}" -  +post-create = "ln -sf {{ repo_path }}/node_modules" +  +pre-start = "ln -sf {{ repo_path }}/node_modules" ❯ Apply updates? [y/N/?] y ✓ Updated user config diff --git a/tests/snapshots/integration__integration_tests__config_update_pty__config_update_prompt_decline.snap b/tests/snapshots/integration__integration_tests__config_update_pty__config_update_prompt_decline.snap index eb6deb515d..d89dd7b3b1 100644 --- a/tests/snapshots/integration__integration_tests__config_update_pty__config_update_prompt_decline.snap +++ b/tests/snapshots/integration__integration_tests__config_update_pty__config_update_prompt_decline.snap @@ -3,15 +3,16 @@ source: tests/integration_tests/config_update_pty.rs expression: "&output" --- ▲ User config uses deprecated template variables: repo_root → repo_path, main_worktree → repo +▲ User config uses deprecated hook name: post-create → pre-start   diff --git a[TEST_CONFIG] b[TEST_CONFIG_NEW] -  index d95d100..ceb381d 100644 +  index d95d100..5eef8a6 100644   --- a[TEST_CONFIG]   +++ b[TEST_CONFIG_NEW]   @@ -1,2 +1,2 @@   -worktree-path = "../{{ main_worktree }}.{{ branch }}"   -post-create = "ln -sf {{ repo_root }}/node_modules"   +worktree-path = "../{{ repo }}.{{ branch }}" -  +post-create = "ln -sf {{ repo_path }}/node_modules" +  +pre-start = "ln -sf {{ repo_path }}/node_modules" ❯ Apply updates? [y/N/?] n ○ Update cancelled diff --git a/tests/snapshots/integration__integration_tests__shell_wrapper__unix_tests__readme_example_approval_prompt.snap b/tests/snapshots/integration__integration_tests__shell_wrapper__unix_tests__readme_example_approval_prompt.snap index 5c92d9d602..16af8bb7a7 100644 --- a/tests/snapshots/integration__integration_tests__shell_wrapper__unix_tests__readme_example_approval_prompt.snap +++ b/tests/snapshots/integration__integration_tests__shell_wrapper__unix_tests__readme_example_approval_prompt.snap @@ -5,11 +5,11 @@ expression: prompt_only n ▲ repo needs approval to execute 3 commands: -○ post-create install: +○ pre-start install: echo 'Installing dependencies...' -○ post-create build: +○ pre-start build: echo 'Building project...' -○ post-create test: +○ pre-start test: echo 'Running tests...' ❯ Allow and remember? [y/N] diff --git a/tests/snapshots/integration__integration_tests__shell_wrapper__unix_tests__readme_example_hooks_post_create.snap b/tests/snapshots/integration__integration_tests__shell_wrapper__unix_tests__readme_example_hooks_post_create.snap index b32cef788c..61d8e4fa8c 100644 --- a/tests/snapshots/integration__integration_tests__shell_wrapper__unix_tests__readme_example_hooks_post_create.snap +++ b/tests/snapshots/integration__integration_tests__shell_wrapper__unix_tests__readme_example_hooks_post_create.snap @@ -3,7 +3,7 @@ source: tests/integration_tests/shell_wrapper.rs expression: "&output.combined" --- -◎ Running post-create project:install +◎ Running pre-start project:install   uv sync Resolved 24 packages in 145ms diff --git a/tests/snapshots/integration__integration_tests__shell_wrapper__unix_tests__switch_with_hooks_zsh.snap b/tests/snapshots/integration__integration_tests__shell_wrapper__unix_tests__switch_with_hooks_zsh.snap index 93fd5a129b..3c5c142fc4 100644 --- a/tests/snapshots/integration__integration_tests__shell_wrapper__unix_tests__switch_with_hooks_zsh.snap +++ b/tests/snapshots/integration__integration_tests__shell_wrapper__unix_tests__switch_with_hooks_zsh.snap @@ -3,10 +3,10 @@ source: tests/integration_tests/shell_wrapper.rs expression: "&output.combined" --- -◎ Running post-create project:install +◎ Running pre-start project:install   echo 'Installing dependencies...' Installing dependencies... -◎ Running post-create project:build +◎ Running pre-start project:build   echo 'Building project...' Building project... ✓ Created branch feature-hooks from main and worktree @ _REPO_.feature-hooks From 083fff242e200e6a9adf55756f01e312d1db6512 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 22 Mar 2026 23:10:19 -0700 Subject: [PATCH 5/9] chore: remove stale post-create help snapshot The help test was renamed to pre-start but the old snapshot file wasn't deleted. Co-Authored-By: Claude --- ...p__nested_subcommand_hook_post_create.snap | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 tests/snapshots/integration__integration_tests__help__nested_subcommand_hook_post_create.snap diff --git a/tests/snapshots/integration__integration_tests__help__nested_subcommand_hook_post_create.snap b/tests/snapshots/integration__integration_tests__help__nested_subcommand_hook_post_create.snap deleted file mode 100644 index 76b8556509..0000000000 --- a/tests/snapshots/integration__integration_tests__help__nested_subcommand_hook_post_create.snap +++ /dev/null @@ -1,38 +0,0 @@ ---- -source: tests/integration_tests/help.rs -info: - program: wt - args: - - post-create - env: - CLICOLOR_FORCE: "1" - COLUMNS: "500" - GIT_EDITOR: "" - LANG: C - LC_ALL: C - PSModulePath: "" - RUST_LOG: warn - SHELL: "" - TERM: alacritty - WORKTRUNK_APPROVALS_PATH: /nonexistent/test/approvals.toml - WORKTRUNK_CONFIG_PATH: /nonexistent/test/config.toml - WORKTRUNK_SYSTEM_CONFIG_PATH: /etc/xdg/worktrunk/config.toml - WORKTRUNK_TEST_CLAUDE_INSTALLED: "0" - WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1" - WORKTRUNK_TEST_EPOCH: "1735776000" - WORKTRUNK_TEST_NUSHELL_ENV: "0" - WORKTRUNK_TEST_POWERSHELL_ENV: "0" - WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" ---- -success: false -exit_code: 2 ------ stdout ----- - ------ stderr ----- -error: unrecognized subcommand 'post-create' - -Usage: wt [OPTIONS] [COMMAND] - -For more information, try '--help'. - - tip: perhaps wt hook post-create? From 022cabd0231882effcf1a859cc4ebf6e48f5370d Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 22 Mar 2026 23:18:48 -0700 Subject: [PATCH 6/9] docs: update merge pipeline for commit hooks and background post-merge - Add commit step (pre-commit, commit, post-commit) to pipeline - Fix post-merge description: runs in background, not blocking - Fix stale comment referencing post-create in hooks.rs Co-Authored-By: Claude --- docs/content/merge.md | 15 ++++++++------- skills/worktrunk/reference/merge.md | 15 ++++++++------- src/cli/mod.rs | 15 ++++++++------- src/commands/hooks.rs | 2 +- ...n__integration_tests__help__help_md_merge.snap | 15 ++++++++------- ..._integration_tests__help__help_merge_long.snap | 15 ++++++++------- 6 files changed, 41 insertions(+), 36 deletions(-) diff --git a/docs/content/merge.md b/docs/content/merge.md index 9dcbb546dc..a21f8f5907 100644 --- a/docs/content/merge.md +++ b/docs/content/merge.md @@ -62,13 +62,14 @@ wt merge --no-commit `wt merge` runs these steps: -1. **Squash** — Stages uncommitted changes, then combines all commits since target into one (like GitHub's "Squash and merge"). Use `--stage` to control what gets staged: `all` (default), `tracked`, or `none`. A backup ref is saved to `refs/wt-backup/`. With `--no-squash`, uncommitted changes become a separate commit and individual commits are preserved. -2. **Rebase** — Rebases onto target if behind. Skipped if already up-to-date. Conflicts abort immediately. -3. **Pre-merge hooks** — Hooks run after rebase, before merge. Failures abort. See [`wt hook`](@/hook.md). -4. **Merge** — Fast-forward merge to the target branch. With `--no-ff`, a merge commit is created instead (semi-linear history: rebased commits plus a merge commit). Non-fast-forward merges are rejected. -5. **Pre-remove hooks** — Hooks run before removing worktree. Failures abort. -6. **Cleanup** — Removes the worktree and branch. Use `--no-remove` to keep the worktree. When already on the target branch or in the primary worktree, the worktree is preserved. -7. **Post-merge hooks** — Hooks run after cleanup. Failures are logged but don't abort. +1. **Commit** — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in background. With `--no-squash`, this is the only commit step; with squash (default), this is skipped and changes are staged during squash instead. +2. **Squash** — Combines all commits since target into one (like GitHub's "Squash and merge"). Use `--stage` to control what gets staged: `all` (default), `tracked`, or `none`. A backup ref is saved to `refs/wt-backup/`. With `--no-squash`, individual commits are preserved. +3. **Rebase** — Rebases onto target if behind. Skipped if already up-to-date. Conflicts abort immediately. +4. **Pre-merge hooks** — Hooks run after rebase, before merge. Failures abort. See [`wt hook`](@/hook.md). +5. **Merge** — Fast-forward merge to the target branch. With `--no-ff`, a merge commit is created instead (semi-linear history: rebased commits plus a merge commit). Non-fast-forward merges are rejected. +6. **Pre-remove hooks** — Hooks run before removing worktree. Failures abort. +7. **Cleanup** — Removes the worktree and branch. Use `--no-remove` to keep the worktree. When already on the target branch or in the primary worktree, the worktree is preserved. +8. **Post-remove + post-merge hooks** — Run in background after cleanup. Use `--no-commit` to skip committing uncommitted changes and squashing; rebase still runs by default and can rewrite commits unless `--no-rebase` is passed. Useful after preparing commits manually with `wt step`. Requires a clean working tree. diff --git a/skills/worktrunk/reference/merge.md b/skills/worktrunk/reference/merge.md index 0a54924b7a..6e0297e1fa 100644 --- a/skills/worktrunk/reference/merge.md +++ b/skills/worktrunk/reference/merge.md @@ -46,13 +46,14 @@ wt merge --no-commit `wt merge` runs these steps: -1. **Squash** — Stages uncommitted changes, then combines all commits since target into one (like GitHub's "Squash and merge"). Use `--stage` to control what gets staged: `all` (default), `tracked`, or `none`. A backup ref is saved to `refs/wt-backup/`. With `--no-squash`, uncommitted changes become a separate commit and individual commits are preserved. -2. **Rebase** — Rebases onto target if behind. Skipped if already up-to-date. Conflicts abort immediately. -3. **Pre-merge hooks** — Hooks run after rebase, before merge. Failures abort. See [`wt hook`](https://worktrunk.dev/hook/). -4. **Merge** — Fast-forward merge to the target branch. With `--no-ff`, a merge commit is created instead (semi-linear history: rebased commits plus a merge commit). Non-fast-forward merges are rejected. -5. **Pre-remove hooks** — Hooks run before removing worktree. Failures abort. -6. **Cleanup** — Removes the worktree and branch. Use `--no-remove` to keep the worktree. When already on the target branch or in the primary worktree, the worktree is preserved. -7. **Post-merge hooks** — Hooks run after cleanup. Failures are logged but don't abort. +1. **Commit** — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in background. With `--no-squash`, this is the only commit step; with squash (default), this is skipped and changes are staged during squash instead. +2. **Squash** — Combines all commits since target into one (like GitHub's "Squash and merge"). Use `--stage` to control what gets staged: `all` (default), `tracked`, or `none`. A backup ref is saved to `refs/wt-backup/`. With `--no-squash`, individual commits are preserved. +3. **Rebase** — Rebases onto target if behind. Skipped if already up-to-date. Conflicts abort immediately. +4. **Pre-merge hooks** — Hooks run after rebase, before merge. Failures abort. See [`wt hook`](https://worktrunk.dev/hook/). +5. **Merge** — Fast-forward merge to the target branch. With `--no-ff`, a merge commit is created instead (semi-linear history: rebased commits plus a merge commit). Non-fast-forward merges are rejected. +6. **Pre-remove hooks** — Hooks run before removing worktree. Failures abort. +7. **Cleanup** — Removes the worktree and branch. Use `--no-remove` to keep the worktree. When already on the target branch or in the primary worktree, the worktree is preserved. +8. **Post-remove + post-merge hooks** — Run in background after cleanup. Use `--no-commit` to skip committing uncommitted changes and squashing; rebase still runs by default and can rewrite commits unless `--no-rebase` is passed. Useful after preparing commits manually with `wt step`. Requires a clean working tree. diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 43c7c254e8..5478ef6b99 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -905,13 +905,14 @@ wt merge --no-commit `wt merge` runs these steps: -1. **Squash** — Stages uncommitted changes, then combines all commits since target into one (like GitHub's "Squash and merge"). Use `--stage` to control what gets staged: `all` (default), `tracked`, or `none`. A backup ref is saved to `refs/wt-backup/`. With `--no-squash`, uncommitted changes become a separate commit and individual commits are preserved. -2. **Rebase** — Rebases onto target if behind. Skipped if already up-to-date. Conflicts abort immediately. -3. **Pre-merge hooks** — Hooks run after rebase, before merge. Failures abort. See [`wt hook`](@/hook.md). -4. **Merge** — Fast-forward merge to the target branch. With `--no-ff`, a merge commit is created instead (semi-linear history: rebased commits plus a merge commit). Non-fast-forward merges are rejected. -5. **Pre-remove hooks** — Hooks run before removing worktree. Failures abort. -6. **Cleanup** — Removes the worktree and branch. Use `--no-remove` to keep the worktree. When already on the target branch or in the primary worktree, the worktree is preserved. -7. **Post-merge hooks** — Hooks run after cleanup. Failures are logged but don't abort. +1. **Commit** — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in background. With `--no-squash`, this is the only commit step; with squash (default), this is skipped and changes are staged during squash instead. +2. **Squash** — Combines all commits since target into one (like GitHub's "Squash and merge"). Use `--stage` to control what gets staged: `all` (default), `tracked`, or `none`. A backup ref is saved to `refs/wt-backup/`. With `--no-squash`, individual commits are preserved. +3. **Rebase** — Rebases onto target if behind. Skipped if already up-to-date. Conflicts abort immediately. +4. **Pre-merge hooks** — Hooks run after rebase, before merge. Failures abort. See [`wt hook`](@/hook.md). +5. **Merge** — Fast-forward merge to the target branch. With `--no-ff`, a merge commit is created instead (semi-linear history: rebased commits plus a merge commit). Non-fast-forward merges are rejected. +6. **Pre-remove hooks** — Hooks run before removing worktree. Failures abort. +7. **Cleanup** — Removes the worktree and branch. Use `--no-remove` to keep the worktree. When already on the target branch or in the primary worktree, the worktree is preserved. +8. **Post-remove + post-merge hooks** — Run in background after cleanup. Use `--no-commit` to skip committing uncommitted changes and squashing; rebase still runs by default and can rewrite commits unless `--no-rebase` is passed. Useful after preparing commits manually with `wt step`. Requires a clean working tree. diff --git a/src/commands/hooks.rs b/src/commands/hooks.rs index 58ed67ea8a..301e54457c 100644 --- a/src/commands/hooks.rs +++ b/src/commands/hooks.rs @@ -34,7 +34,7 @@ impl SourcedCommand { /// Announce this command before execution. /// - /// Format: "Running pre-merge user:foo" for named, "Running post-create user hook" for unnamed + /// Format: "Running pre-merge user:foo" for named, "Running pre-start user hook" for unnamed /// When display_path is set, appends "@ path" to show where the command runs. fn announce(&self) -> anyhow::Result<()> { // Named: "Running post-switch user:foo" with "user:foo" bold diff --git a/tests/snapshots/integration__integration_tests__help__help_md_merge.snap b/tests/snapshots/integration__integration_tests__help__help_md_merge.snap index de3bc73308..57b3d341f3 100644 --- a/tests/snapshots/integration__integration_tests__help__help_md_merge.snap +++ b/tests/snapshots/integration__integration_tests__help__help_md_merge.snap @@ -130,13 +130,14 @@ wt merge --no-commit `wt merge` runs these steps: -1. **Squash** — Stages uncommitted changes, then combines all commits since target into one (like GitHub's "Squash and merge"). Use `--stage` to control what gets staged: `all` (default), `tracked`, or `none`. A backup ref is saved to `refs/wt-backup/`. With `--no-squash`, uncommitted changes become a separate commit and individual commits are preserved. -2. **Rebase** — Rebases onto target if behind. Skipped if already up-to-date. Conflicts abort immediately. -3. **Pre-merge hooks** — Hooks run after rebase, before merge. Failures abort. See [`wt hook`](@/hook.md). -4. **Merge** — Fast-forward merge to the target branch. With `--no-ff`, a merge commit is created instead (semi-linear history: rebased commits plus a merge commit). Non-fast-forward merges are rejected. -5. **Pre-remove hooks** — Hooks run before removing worktree. Failures abort. -6. **Cleanup** — Removes the worktree and branch. Use `--no-remove` to keep the worktree. When already on the target branch or in the primary worktree, the worktree is preserved. -7. **Post-merge hooks** — Hooks run after cleanup. Failures are logged but don't abort. +1. **Commit** — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in background. With `--no-squash`, this is the only commit step; with squash (default), this is skipped and changes are staged during squash instead. +2. **Squash** — Combines all commits since target into one (like GitHub's "Squash and merge"). Use `--stage` to control what gets staged: `all` (default), `tracked`, or `none`. A backup ref is saved to `refs/wt-backup/`. With `--no-squash`, individual commits are preserved. +3. **Rebase** — Rebases onto target if behind. Skipped if already up-to-date. Conflicts abort immediately. +4. **Pre-merge hooks** — Hooks run after rebase, before merge. Failures abort. See [`wt hook`](@/hook.md). +5. **Merge** — Fast-forward merge to the target branch. With `--no-ff`, a merge commit is created instead (semi-linear history: rebased commits plus a merge commit). Non-fast-forward merges are rejected. +6. **Pre-remove hooks** — Hooks run before removing worktree. Failures abort. +7. **Cleanup** — Removes the worktree and branch. Use `--no-remove` to keep the worktree. When already on the target branch or in the primary worktree, the worktree is preserved. +8. **Post-remove + post-merge hooks** — Run in background after cleanup. Use `--no-commit` to skip committing uncommitted changes and squashing; rebase still runs by default and can rewrite commits unless `--no-rebase` is passed. Useful after preparing commits manually with `wt step`. Requires a clean working tree. diff --git a/tests/snapshots/integration__integration_tests__help__help_merge_long.snap b/tests/snapshots/integration__integration_tests__help__help_merge_long.snap index 05eebeb46e..56e2118930 100644 --- a/tests/snapshots/integration__integration_tests__help__help_merge_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_merge_long.snap @@ -119,13 +119,14 @@ Skip committing/squashing (rebase still runs unless --no-rebase): wt merge runs these steps: -1. Squash — Stages uncommitted changes, then combines all commits since target into one (like GitHub's "Squash and merge"). Use --stage to control what gets staged: all (default), tracked, or none. A backup ref is saved to refs/wt-backup/. With --no-squash, uncommitted changes become a separate commit and individual commits are preserved. -2. Rebase — Rebases onto target if behind. Skipped if already up-to-date. Conflicts abort immediately. -3. Pre-merge hooks — Hooks run after rebase, before merge. Failures abort. See wt hook. -4. Merge — Fast-forward merge to the target branch. With --no-ff, a merge commit is created instead (semi-linear history: rebased commits plus a merge commit). Non-fast-forward merges are rejected. -5. Pre-remove hooks — Hooks run before removing worktree. Failures abort. -6. Cleanup — Removes the worktree and branch. Use --no-remove to keep the worktree. When already on the target branch or in the primary worktree, the worktree is preserved. -7. Post-merge hooks — Hooks run after cleanup. Failures are logged but don't abort. +1. Commit — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in background. With --no-squash, this is the only commit step; with squash (default), this is skipped and changes are staged during squash instead. +2. Squash — Combines all commits since target into one (like GitHub's "Squash and merge"). Use --stage to control what gets staged: all (default), tracked, or none. A backup ref is saved to refs/wt-backup/. With --no-squash, individual commits are preserved. +3. Rebase — Rebases onto target if behind. Skipped if already up-to-date. Conflicts abort immediately. +4. Pre-merge hooks — Hooks run after rebase, before merge. Failures abort. See wt hook. +5. Merge — Fast-forward merge to the target branch. With --no-ff, a merge commit is created instead (semi-linear history: rebased commits plus a merge commit). Non-fast-forward merges are rejected. +6. Pre-remove hooks — Hooks run before removing worktree. Failures abort. +7. Cleanup — Removes the worktree and branch. Use --no-remove to keep the worktree. When already on the target branch or in the primary worktree, the worktree is preserved. +8. Post-remove + post-merge hooks — Run in background after cleanup. Use --no-commit to skip committing uncommitted changes and squashing; rebase still runs by default and can rewrite commits unless --no-rebase is passed. Useful after preparing commits manually with wt step. Requires a clean working tree. From 29ebab584cb9fb83e28dad56c3d8641000d5a1e4 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 22 Mar 2026 23:46:36 -0700 Subject: [PATCH 7/9] fix: include post-commit in approval and fire after squash - Add PostCommit to approval batch in `wt step commit` and `wt step squash` so post-commit hooks get user consent - Spawn post-commit hooks after squash commit, matching the direct commit path in CommitOptions::commit() Co-Authored-By: Claude --- src/commands/step_commands.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/commands/step_commands.rs b/src/commands/step_commands.rs index 06db44f7e1..11f6921766 100644 --- a/src/commands/step_commands.rs +++ b/src/commands/step_commands.rs @@ -32,7 +32,10 @@ use worktrunk::styling::{ use super::command_approval::approve_hooks; use super::commit::{CommitGenerator, CommitOptions, StageMode}; use super::context::CommandEnv; -use super::hooks::{HookCommandSpec, HookFailureStrategy, run_hook_with_filter}; +use super::hooks::{ + HookCommandSpec, HookFailureStrategy, prepare_background_hooks, run_hook_with_filter, + spawn_background_hooks, +}; use super::repository_ext::{RemoveTarget, RepositoryCliExt}; use super::worktree::BranchDeletionMode; use crate::output::handle_remove_output; @@ -69,10 +72,10 @@ pub fn step_commit( // CLI flag overrides config value let stage_mode = stage.unwrap_or(env.resolved().commit.stage()); - // "Approve at the Gate": approve pre-commit hooks upfront (unless --no-verify) + // "Approve at the Gate": approve commit hooks upfront (unless --no-verify) // Shadow verify: if user declines approval, skip hooks but continue commit let verify = if verify { - let approved = approve_hooks(&ctx, &[HookType::PreCommit])?; + let approved = approve_hooks(&ctx, &[HookType::PreCommit, HookType::PostCommit])?; if !approved { eprintln!( "{}", @@ -146,10 +149,10 @@ pub fn handle_squash( ); let any_hooks_exist = user_cfg.is_some() || proj_cfg.is_some(); - // "Approve at the Gate": approve pre-commit hooks upfront (unless --no-verify) + // "Approve at the Gate": approve commit hooks upfront (unless --no-verify) // Shadow verify: if user declines approval, skip hooks but continue squash let verify = if verify { - let approved = approve_hooks(&ctx, &[HookType::PreCommit])?; + let approved = approve_hooks(&ctx, &[HookType::PreCommit, HookType::PostCommit])?; if !approved { eprintln!( "{}", @@ -352,6 +355,13 @@ pub fn handle_squash( success_message(cformat!("Squashed @ {commit_hash}")) ); + // Spawn post-commit hooks in background (respects --no-verify) + if verify { + let extra_vars: Vec<(&str, &str)> = vec![("target", integration_target.as_str())]; + let hooks = prepare_background_hooks(&ctx, HookType::PostCommit, &extra_vars, None)?; + spawn_background_hooks(&ctx, hooks)?; + } + Ok(SquashResult::Squashed) } From 9047e3eba9d9d699326e64d84d61bccdb90bb48e Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Mon, 23 Mar 2026 00:10:57 -0700 Subject: [PATCH 8/9] fix: preserve completion for deprecated post-create alias The hook command name completer didn't recognize `post-create` in the command context, so `wt hook post-create ` returned no completions. Map it to `pre-start` for the duration of the deprecation period. Co-Authored-By: Claude --- src/completion.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/completion.rs b/src/completion.rs index a0728bd436..06e8f33d96 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -228,6 +228,10 @@ impl ValueCompleter for HookCommandCompleter { return Some(*hook); } } + // Deprecated alias: post-create → pre-start + if ctx.contains("post-create") { + return Some("pre-start"); + } None }) }); From ffcfe5f4847a922039bf53a27a44604ed2a4f23c Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Mon, 23 Mar 2026 00:20:27 -0700 Subject: [PATCH 9/9] docs: consolidate post-* hook log path into intro Move the repeated log path template from each post-* hook section into the intro paragraph. Each hook section now focuses on use cases rather than mechanics. Also remove redundant "Failure aborts" from pre-switch (already stated in intro). Co-Authored-By: Claude --- docs/content/hook.md | 14 +++++++------- skills/worktrunk/reference/hook.md | 14 +++++++------- src/cli/mod.rs | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/content/hook.md b/docs/content/hook.md index 0ab9917926..b61935419d 100644 --- a/docs/content/hook.md +++ b/docs/content/hook.md @@ -23,13 +23,13 @@ Hooks are shell commands that run at key points in the worktree lifecycle — au | **merge** | `pre-merge` | `post-merge` | | **remove** | `pre-remove` | `post-remove` | -`pre-*` hooks block — failure aborts the operation (except `pre-start`, which warns and continues). `post-*` hooks run in the background. Use `-v` to see expanded command details for background hooks. +`pre-*` hooks block — failure aborts the operation (except `pre-start`, which warns and continues). `post-*` hooks run in the background with output logged to `.git/wt/logs/{branch}-{source}-{hook}-{name}.log`. Use `-v` to see expanded command details for background hooks. The most common starting point is `post-start` — it runs background tasks (dev servers, file copying, builds) when creating a worktree. ## pre-switch -Runs before every `wt switch` — before branch resolution or worktree creation. `{{ branch }}` is the destination branch argument as the user typed it (before resolution). Failure aborts the switch. +Runs before every `wt switch` — before branch resolution or worktree creation. `{{ branch }}` is the destination branch argument as the user typed it (before resolution). ```toml [pre-switch] @@ -54,7 +54,7 @@ env = "echo 'PORT={{ branch | hash_port }}' > .env.local" ## post-start -Dev servers, long builds, file watchers, copying caches. Output logged to `.git/wt/logs/{branch}-{source}-post-start-{name}.log`. +Dev servers, long builds, file watchers, copying caches. ```toml [post-start] @@ -64,7 +64,7 @@ server = "npm run dev -- --port {{ branch | hash_port }}" ## post-switch -Triggers on all switch results: creating new worktrees, switching to existing ones, or staying on current. Output logged to `.git/wt/logs/{branch}-{source}-post-switch-{name}.log`. +Triggers on all switch results: creating new worktrees, switching to existing ones, or staying on current. ```toml [post-switch] @@ -83,7 +83,7 @@ lint = "cargo clippy -- -D warnings" ## post-commit -Background tasks after a successful commit: CI triggers, notifications, background linting. Output logged to `.git/wt/logs/{branch}-{source}-post-commit-{name}.log`. Respects `--no-verify`. +CI triggers, notifications, background linting. ```toml [post-commit] @@ -102,7 +102,7 @@ build = "cargo build --release" ## post-merge -Deployment, notifications, installing updated binaries. Runs in background in the target branch worktree if it exists, otherwise the main worktree. Output logged to `.git/wt/logs/{branch}-{source}-post-merge-{name}.log`. +Deployment, notifications, installing updated binaries. Runs in the target branch worktree if it exists, otherwise the primary worktree. ```toml post-merge = "cargo install --path ." @@ -119,7 +119,7 @@ archive = "tar -czf ~/.wt-logs/{{ branch }}.tar.gz test-results/ logs/ 2>/dev/nu ## post-remove -Cleanup tasks after worktree removal: stopping dev servers, removing containers, notifying external systems. All template variables reference the removed worktree, so cleanup scripts can identify resources to clean up. Output logged to `.git/wt/logs/{branch}-{source}-post-remove-{name}.log`. +Stopping dev servers, removing containers, notifying external systems. Template variables reference the removed worktree, so cleanup scripts can identify resources to tear down. ```toml [post-remove] diff --git a/skills/worktrunk/reference/hook.md b/skills/worktrunk/reference/hook.md index a9f1bd0889..7ad732919b 100644 --- a/skills/worktrunk/reference/hook.md +++ b/skills/worktrunk/reference/hook.md @@ -14,13 +14,13 @@ Hooks are shell commands that run at key points in the worktree lifecycle — au | **merge** | `pre-merge` | `post-merge` | | **remove** | `pre-remove` | `post-remove` | -`pre-*` hooks block — failure aborts the operation (except `pre-start`, which warns and continues). `post-*` hooks run in the background. Use `-v` to see expanded command details for background hooks. +`pre-*` hooks block — failure aborts the operation (except `pre-start`, which warns and continues). `post-*` hooks run in the background with output logged to `.git/wt/logs/{branch}-{source}-{hook}-{name}.log`. Use `-v` to see expanded command details for background hooks. The most common starting point is `post-start` — it runs background tasks (dev servers, file copying, builds) when creating a worktree. ## pre-switch -Runs before every `wt switch` — before branch resolution or worktree creation. `{{ branch }}` is the destination branch argument as the user typed it (before resolution). Failure aborts the switch. +Runs before every `wt switch` — before branch resolution or worktree creation. `{{ branch }}` is the destination branch argument as the user typed it (before resolution). ```toml [pre-switch] @@ -45,7 +45,7 @@ env = "echo 'PORT={{ branch | hash_port }}' > .env.local" ## post-start -Dev servers, long builds, file watchers, copying caches. Output logged to `.git/wt/logs/{branch}-{source}-post-start-{name}.log`. +Dev servers, long builds, file watchers, copying caches. ```toml [post-start] @@ -55,7 +55,7 @@ server = "npm run dev -- --port {{ branch | hash_port }}" ## post-switch -Triggers on all switch results: creating new worktrees, switching to existing ones, or staying on current. Output logged to `.git/wt/logs/{branch}-{source}-post-switch-{name}.log`. +Triggers on all switch results: creating new worktrees, switching to existing ones, or staying on current. ```toml [post-switch] @@ -74,7 +74,7 @@ lint = "cargo clippy -- -D warnings" ## post-commit -Background tasks after a successful commit: CI triggers, notifications, background linting. Output logged to `.git/wt/logs/{branch}-{source}-post-commit-{name}.log`. Respects `--no-verify`. +CI triggers, notifications, background linting. ```toml [post-commit] @@ -93,7 +93,7 @@ build = "cargo build --release" ## post-merge -Deployment, notifications, installing updated binaries. Runs in background in the target branch worktree if it exists, otherwise the main worktree. Output logged to `.git/wt/logs/{branch}-{source}-post-merge-{name}.log`. +Deployment, notifications, installing updated binaries. Runs in the target branch worktree if it exists, otherwise the primary worktree. ```toml post-merge = "cargo install --path ." @@ -110,7 +110,7 @@ archive = "tar -czf ~/.wt-logs/{{ branch }}.tar.gz test-results/ logs/ 2>/dev/nu ## post-remove -Cleanup tasks after worktree removal: stopping dev servers, removing containers, notifying external systems. All template variables reference the removed worktree, so cleanup scripts can identify resources to clean up. Output logged to `.git/wt/logs/{branch}-{source}-post-remove-{name}.log`. +Stopping dev servers, removing containers, notifying external systems. Template variables reference the removed worktree, so cleanup scripts can identify resources to tear down. ```toml [post-remove] diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 5478ef6b99..09d7a48246 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1111,13 +1111,13 @@ Alias names that match a built-in step command (`commit`, `squash`, etc.) are sh | **merge** | `pre-merge` | `post-merge` | | **remove** | `pre-remove` | `post-remove` | -`pre-*` hooks block — failure aborts the operation (except `pre-start`, which warns and continues). `post-*` hooks run in the background. Use `-v` to see expanded command details for background hooks. +`pre-*` hooks block — failure aborts the operation (except `pre-start`, which warns and continues). `post-*` hooks run in the background with output logged to `.git/wt/logs/{branch}-{source}-{hook}-{name}.log`. Use `-v` to see expanded command details for background hooks. The most common starting point is `post-start` — it runs background tasks (dev servers, file copying, builds) when creating a worktree. ## pre-switch -Runs before every `wt switch` — before branch resolution or worktree creation. `{{ branch }}` is the destination branch argument as the user typed it (before resolution). Failure aborts the switch. +Runs before every `wt switch` — before branch resolution or worktree creation. `{{ branch }}` is the destination branch argument as the user typed it (before resolution). ```toml [pre-switch] @@ -1142,7 +1142,7 @@ env = "echo 'PORT={{ branch | hash_port }}' > .env.local" ## post-start -Dev servers, long builds, file watchers, copying caches. Output logged to `.git/wt/logs/{branch}-{source}-post-start-{name}.log`. +Dev servers, long builds, file watchers, copying caches. ```toml [post-start] @@ -1152,7 +1152,7 @@ server = "npm run dev -- --port {{ branch | hash_port }}" ## post-switch -Triggers on all switch results: creating new worktrees, switching to existing ones, or staying on current. Output logged to `.git/wt/logs/{branch}-{source}-post-switch-{name}.log`. +Triggers on all switch results: creating new worktrees, switching to existing ones, or staying on current. ```toml [post-switch] @@ -1171,7 +1171,7 @@ lint = "cargo clippy -- -D warnings" ## post-commit -Background tasks after a successful commit: CI triggers, notifications, background linting. Output logged to `.git/wt/logs/{branch}-{source}-post-commit-{name}.log`. Respects `--no-verify`. +CI triggers, notifications, background linting. ```toml [post-commit] @@ -1190,7 +1190,7 @@ build = "cargo build --release" ## post-merge -Deployment, notifications, installing updated binaries. Runs in background in the target branch worktree if it exists, otherwise the main worktree. Output logged to `.git/wt/logs/{branch}-{source}-post-merge-{name}.log`. +Deployment, notifications, installing updated binaries. Runs in the target branch worktree if it exists, otherwise the primary worktree. ```toml post-merge = "cargo install --path ." @@ -1207,7 +1207,7 @@ archive = "tar -czf ~/.wt-logs/{{ branch }}.tar.gz test-results/ logs/ 2>/dev/nu ## post-remove -Cleanup tasks after worktree removal: stopping dev servers, removing containers, notifying external systems. All template variables reference the removed worktree, so cleanup scripts can identify resources to clean up. Output logged to `.git/wt/logs/{branch}-{source}-post-remove-{name}.log`. +Stopping dev servers, removing containers, notifying external systems. Template variables reference the removed worktree, so cleanup scripts can identify resources to tear down. ```toml [post-remove]