From 27285f8b63820847670a1fecb02738b258779e4e Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Thu, 9 Jul 2026 22:26:02 -0700 Subject: [PATCH 1/3] docs(list): explain why the json-schema nag bypasses the deprecation system The unset nag looks like a deprecation warning but wt config update can't service it; record the rationale from design/list-json-v2.md (reviewed in #3357) at the nag site: unset is a pending choice between two valid schemas, so there's no behavior-preserving rewrite until the default flips and = 1 becomes a real DEPRECATION_RULES row. Co-Authored-By: Claude Fable 5 --- src/commands/list/mod.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/commands/list/mod.rs b/src/commands/list/mod.rs index f0daa7d992..b6260736e4 100644 --- a/src/commands/list/mod.rs +++ b/src/commands/list/mod.rs @@ -207,6 +207,16 @@ pub(crate) fn print_json(value: &T) -> anyhow::Result<()> { /// would corrupt the consumer's prompt, and the same user sees the nag on /// their next interactive run. The nag names both settings so the fix is /// copyable from the warning itself. +/// +/// The unset nag deliberately bypasses the config deprecation system +/// (`DEPRECATION_RULES` / `wt config update`): nothing in the file is +/// deprecated — unset is a pending choice between two valid schemas — so the +/// warning belongs on the surfaces that emit JSON rather than on every config +/// load, and `wt config update` has no behavior-preserving rewrite to offer: +/// inserting `= 1` would pin users to the schema the migration is moving away +/// from. The machinery joins at the default flip, when `= 1` becomes a +/// standard `DEPRECATION_RULES` row that `wt config update` strips (migration +/// plan in design/list-json-v2.md, reviewed in #3357). pub(crate) fn resolve_json_schema(repo: &Repository) -> u8 { use std::sync::Once; From 85bf36b0553e7f87e2f53d1f9d73e13e6f1f4012 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Fri, 10 Jul 2026 00:45:41 -0700 Subject: [PATCH 2/3] feat(config): wt config update pins [list] json-schema = 1 while unset The JSON-schema nag now comes with the standard fixer: a new PendingDefault row in DEPRECATION_RULES inserts json-schema = 1 on the update pass only, scoped to user config via a ConfigFileKind threaded through detection (replacing the string labels). The nag's hint offers wt config update when a user config file exists; config show renders the pending pin without dropping its TOML dump. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 2 +- docs/content/list.md | 5 +- skills/worktrunk/reference/list.md | 5 +- src/cli/config.rs | 9 +- src/cli/mod.rs | 5 +- src/commands/config/show.rs | 24 +- src/commands/config/update.rs | 16 +- src/commands/list/mod.rs | 37 +- src/config/deprecation.rs | 428 ++++++++++++++---- src/config/mod.rs | 1 + src/config/project.rs | 2 +- ...__snapshot_migrate_all_rules_combined.snap | 3 + ...sts__snapshot_migrate_create_to_start.snap | 3 + src/config/user/mod.rs | 4 +- tests/integration_tests/config_show.rs | 42 +- tests/integration_tests/config_update_pty.rs | 6 + tests/integration_tests/list.rs | 26 +- ...claude_available_plugin_not_installed.snap | 14 + ...w__config_show_clis_detected_via_path.snap | 14 + ...fig_show__config_show_codex_available.snap | 14 + ...fig_show_displays_deprecation_details.snap | 9 +- ...onfig_show_displays_no_cd_deprecation.snap | 9 +- ...onfig_show_displays_no_ff_deprecation.snap | 9 +- ...roject_commit_generation_deprecations.snap | 9 +- ...w_displays_select_section_deprecation.snap | 9 +- ...how__config_show_empty_project_config.snap | 14 + ...w__config_show_full_command_not_found.snap | 16 + ...g_show__config_show_full_gitea_remote.snap | 16 + ...show__config_show_full_not_configured.snap | 16 + ...ow__config_show_full_update_available.snap | 16 + ...g_show_full_version_check_unavailable.snap | 16 + ...ini_available_extension_not_installed.snap | 14 + ...onfig_show_gemini_extension_installed.snap | 14 + ...how_gemini_extension_invalid_manifest.snap | 14 + ...onfig_show__config_show_github_remote.snap | 14 + ...onfig_show__config_show_gitlab_remote.snap | 14 + ...how__config_show_invalid_project_toml.snap | 16 + ...g_show__config_show_no_project_config.snap | 14 + ...encode_available_plugin_not_installed.snap | 14 + ...config_show_opencode_plugin_installed.snap | 14 + ..._config_show_opencode_plugin_outdated.snap | 14 + ...ig_show__config_show_outside_git_repo.snap | 14 + ...ig_show__config_show_plugin_installed.snap | 14 + ...w_shell_active_but_not_in_config_file.snap | 14 + ..._config_show_shell_integration_active.snap | 14 + ...ow__config_show_statusline_configured.snap | 14 + ...g_show_suggests_project_config_for_ci.snap | 9 +- ...sts_user_config_for_commit_generation.snap | 16 + ...onfig_show_warns_unknown_project_keys.snap | 16 + ...__config_show_warns_unknown_user_keys.snap | 19 + ...g_show_whitespace_only_project_config.snap | 14 + ...show__config_show_with_project_config.snap | 14 + ..._show__config_show_with_system_config.snap | 14 + ...e_applies_approved_commands_migration.snap | 10 +- ...e_applies_commit_generation_migration.snap | 9 +- ...update_applies_template_var_migration.snap | 9 +- ..._show__config_update_pins_json_schema.snap | 62 +++ ...date_pty__config_update_prompt_accept.snap | 7 +- ...ate_pty__config_update_prompt_decline.snap | 7 +- ...tegration_tests__help__help_list_long.snap | 5 +- ...tion_tests__help__help_list_narrow_80.snap | 5 +- ...st_json_tree_matches_main_after_merge.snap | 2 +- ...__list__list_json_with_display_fields.snap | 2 +- ...s__list__list_json_with_git_operation.snap | 2 +- ..._tests__list__list_json_with_metadata.snap | 2 +- ...sts__list__list_json_with_user_marker.snap | 2 +- 66 files changed, 1073 insertions(+), 164 deletions(-) create mode 100644 tests/snapshots/integration__integration_tests__config_show__config_update_pins_json_schema.snap diff --git a/CLAUDE.md b/CLAUDE.md index 4bc72da2d4..5bb716dc93 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -173,7 +173,7 @@ Check `Cargo.toml` before hand-rolling a utility: All config deprecation lives in one layer: pre-deserialization TOML migration in `src/config/deprecation.rs`. `migrate_content()` rewrites deprecated patterns into canonical form before serde parses; `check_and_migrate()` reuses it, and additionally detects patterns and emits per-process-deduped warnings (the user materializes migrations via `wt config update`). **Never silently drop an old config key** — that's a silent behavior change for users; migrate it. -Every deprecation is one row in the `DEPRECATION_RULES` table: a single idempotent function that rewrites the pattern AND returns the `DeprecationKind`s for what it changed — there is no separate detection function, so detection and migration share one predicate and cannot drift. Detection runs the same functions against a scratch copy of the document (progressively, so a rule sees earlier rules' rewrites); the invariant for warning rules is **a warning fires exactly when `wt config update` would change the file**, pinned by `test_warning_fires_iff_update_changes` — add new edge cases to its battery. The row variant decides when the rewrite applies: `Structural` rewrites on every load; `UpdateOnly` only via `wt config update`, for deprecated forms that still work at runtime; `Silent` rewrites on every load with no warning — its function signature has no channel for a kind, which is what scopes the invariant to the other two variants. Table order is both the warning-emission order and the migration order. Each `DeprecationKind` carries its own display payload, so `format_deprecation_warnings()` is one match over the kinds. A config that can't be rewritten safely (a malformed value, an occupied destination key) is left untouched and unwarned — serde's type or unknown-field error is the messaging; an empty deprecated section is also left alone, with no message at all (it contributes no config). Adding a deprecation: (1) one idempotent migrate-and-report function; (2) a `DeprecationKind` variant plus its match arm in `format_deprecation_warnings()`; (3) a `DEPRECATION_RULES` row; (4) for a removed top-level section, add a `DeprecatedSection` to `DEPRECATED_SECTION_KEYS` (canonical key plus display form) so `warn_unknown_fields` defers to the deprecation messaging and suggests the correct config file. A silently-migrated rename (e.g. `pre-create` → `pre-start`) is a `Silent` row with no variant. Renaming a field within a section follows the same shape via a TOML-level rename function (see `migrate_negated_bool`); the struct never needs the old field since migration precedes serde. +Every deprecation is one row in the `DEPRECATION_RULES` table: a single idempotent function that rewrites the pattern AND returns the `DeprecationKind`s for what it changed — there is no separate detection function, so detection and migration share one predicate and cannot drift. Detection runs the same functions against a scratch copy of the document (progressively, so a rule sees earlier rules' rewrites); the invariant for warning rules is **a warning fires exactly when `wt config update` would change the file**, pinned by `test_warning_fires_iff_update_changes` — add new edge cases to its battery. The row variant decides when the rewrite applies: `Structural` rewrites on every load; `UpdateOnly` only via `wt config update`, for deprecated forms that still work at runtime; `Silent` rewrites on every load with no warning — its function signature has no channel for a kind, which is what scopes the invariant to the warning variants; `PendingDefault` pins a default a future release switches (currently `[list] json-schema = 1`) — update-pass only, scoped to the config kind that owns the key, and its kind warns at the surface that reads the setting (the `wt list` JSON nag) instead of at config load, so the iff invariant holds with the warning on that surface. Table order is both the warning-emission order and the migration order. Each `DeprecationKind` carries its own display payload, so `format_deprecation_warnings()` is one match over the kinds. A config that can't be rewritten safely (a malformed value, an occupied destination key) is left untouched and unwarned — serde's type or unknown-field error is the messaging; an empty deprecated section is also left alone, with no message at all (it contributes no config). Adding a deprecation: (1) one idempotent migrate-and-report function; (2) a `DeprecationKind` variant plus its match arm in `format_deprecation_warnings()`; (3) a `DEPRECATION_RULES` row; (4) for a removed top-level section, add a `DeprecatedSection` to `DEPRECATED_SECTION_KEYS` (canonical key plus display form) so `warn_unknown_fields` defers to the deprecation messaging and suggests the correct config file. A silently-migrated rename (e.g. `pre-create` → `pre-start`) is a `Silent` row with no variant. Renaming a field within a section follows the same shape via a TOML-level rename function (see `migrate_negated_bool`); the struct never needs the old field since migration precedes serde. ## Adding CLI Commands diff --git a/docs/content/list.md b/docs/content/list.md index c64bdc07c5..663d597689 100644 --- a/docs/content/list.md +++ b/docs/content/list.md @@ -219,8 +219,9 @@ These appear across all columns while the table is loading: `--format=json` emits structured data in one of two schemas while the format migrates: `[list] json-schema = 2` selects the envelope format below, `= 1` -the original bare-array format. Unset emits schema 1 with a warning; a future -release flips the default to schema 2 and later removes schema 1. +the original bare-array format. Unset emits schema 1 with a warning +(`wt config update` pins `= 1`); a future release flips the default to +schema 2 and later removes schema 1. ### Schema 2 diff --git a/skills/worktrunk/reference/list.md b/skills/worktrunk/reference/list.md index 9367fbda62..b255e1a519 100644 --- a/skills/worktrunk/reference/list.md +++ b/skills/worktrunk/reference/list.md @@ -204,8 +204,9 @@ These appear across all columns while the table is loading: `--format=json` emits structured data in one of two schemas while the format migrates: `[list] json-schema = 2` selects the envelope format below, `= 1` -the original bare-array format. Unset emits schema 1 with a warning; a future -release flips the default to schema 2 and later removes schema 1. +the original bare-array format. Unset emits schema 1 with a warning +(`wt config update` pins `= 1`); a future release flips the default to +schema 2 and later removes schema 1. ### Schema 2 diff --git a/src/cli/config.rs b/src/cli/config.rs index fa471bb2e1..0f0ae26ea4 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -513,11 +513,12 @@ This tests: /// Update deprecated config settings #[command( after_long_help = r#"Updates deprecated settings in user and project config files -to their current equivalents. Shows a diff and asks for confirmation. +to their current equivalents, and pins defaults that a future release +switches — currently `[list] json-schema = 1` — so upgrading doesn't change +behavior. Shows a diff and asks for confirmation. -Migrations are computed in memory on demand — worktrunk no longer writes -`.new` files as a side effect of loading config. Use `--print` to see the -migrated TOML without touching any file. +Migrations are computed in memory on demand; nothing is written outside this +command. Use `--print` to see the migrated TOML without touching any file. ## Examples diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 158d567b7a..038c35fa67 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -956,8 +956,9 @@ These appear across all columns while the table is loading: `--format=json` emits structured data in one of two schemas while the format migrates: `[list] json-schema = 2` selects the envelope format below, `= 1` -the original bare-array format. Unset emits schema 1 with a warning; a future -release flips the default to schema 2 and later removes schema 1. +the original bare-array format. Unset emits schema 1 with a warning +(`wt config update` pins `= 1`); a future release flips the default to +schema 2 and later removes schema 1. ### Schema 2 diff --git a/src/commands/config/show.rs b/src/commands/config/show.rs index 08c3473240..935c11b5b8 100644 --- a/src/commands/config/show.rs +++ b/src/commands/config/show.rs @@ -671,11 +671,14 @@ fn render_user_config(out: &mut String, has_system_config: bool) -> anyhow::Resu // Check for deprecations with emit_inline_warnings=false (silent mode) // User config is global, not tied to any repository - let has_deprecations = match worktrunk::config::check_and_migrate( + // Deprecated patterns supersede the TOML dump below (their diff covers + // the file); a pending-default pin is additive, so the dump stays. + let mut details_shown = false; + let skip_dump = match worktrunk::config::check_and_migrate( &config_path, &contents, true, - "User config", + worktrunk::config::ConfigFileKind::User, None, false, // silent mode - we'll format the output ourselves ) { @@ -684,7 +687,8 @@ fn render_user_config(out: &mut String, has_system_config: bool) -> anyhow::Resu out.push_str(&worktrunk::config::format_deprecation_details( &info, &contents, )); - true + details_shown = true; + info.has_deprecated_patterns() } else { false } @@ -706,7 +710,11 @@ fn render_user_config(out: &mut String, has_system_config: bool) -> anyhow::Resu // Display TOML with syntax highlighting (gutter at column 0). // Skip when deprecations were shown — the proposed diff already covers it. - if !has_deprecations { + if !skip_dump { + if details_shown { + // Pending-pin details above end in their diff; separate phases. + out.push('\n'); + } writeln!(out, "{}", format_toml(&contents))?; } @@ -837,11 +845,11 @@ fn render_project_config(out: &mut String) -> anyhow::Result<()> { // Check for deprecations with emit_inline_warnings=false (silent mode) // Only write migration file in main worktree, not linked worktrees let is_main_worktree = !repo.current_worktree().is_linked().unwrap_or(true); - let has_deprecations = match worktrunk::config::check_and_migrate( + let skip_dump = match worktrunk::config::check_and_migrate( &config_path, &contents, is_main_worktree, - "Project config", + worktrunk::config::ConfigFileKind::Project, Some(&repo), false, // silent mode - we'll format the output ourselves ) { @@ -850,7 +858,7 @@ fn render_project_config(out: &mut String) -> anyhow::Result<()> { out.push_str(&worktrunk::config::format_deprecation_details( &info, &contents, )); - true + info.has_deprecated_patterns() } else { false } @@ -872,7 +880,7 @@ fn render_project_config(out: &mut String) -> anyhow::Result<()> { // Display TOML with syntax highlighting (gutter at column 0). // Skip when deprecations were shown — the proposed diff already covers it. - if !has_deprecations { + if !skip_dump { writeln!(out, "{}", format_toml(&contents))?; } diff --git a/src/commands/config/update.rs b/src/commands/config/update.rs index 86bd51f78e..bd27b2d779 100644 --- a/src/commands/config/update.rs +++ b/src/commands/config/update.rs @@ -10,7 +10,7 @@ use std::path::PathBuf; use anyhow::Context; use color_print::cformat; use worktrunk::config::{ - DeprecationInfo, DeprecationKind, compute_migrated_content, config_path, + ConfigFileKind, DeprecationInfo, DeprecationKind, compute_migrated_content, config_path, copy_approved_commands_to_approvals_file, format_deprecation_warnings, format_migration_diff, }; use worktrunk::git::Repository; @@ -65,7 +65,7 @@ pub fn handle_config_update(yes: bool, print: bool) -> anyhow::Result<()> { } println!( "# {} ({})", - candidate.info.label, + candidate.info.label(), candidate.config_path.display() ); } @@ -112,10 +112,10 @@ pub fn handle_config_update(yes: bool, print: bool) -> anyhow::Result<()> { } std::fs::write(&candidate.config_path, &candidate.migrated) - .with_context(|| format!("Failed to update {}", candidate.info.label))?; + .with_context(|| format!("Failed to update {}", candidate.info.label()))?; eprintln!( "{}", - success_message(format!("Updated {}", candidate.info.label.to_lowercase())) + success_message(format!("Updated {}", candidate.info.label().to_lowercase())) ); } @@ -159,7 +159,7 @@ fn check_user_config() -> anyhow::Result> { &config_path, &original, true, // warn_and_migrate — user config always actionable - "User config", + ConfigFileKind::User, None, // no repo context for user config false, // emit_inline_warnings — we render the diff ourselves )?; @@ -168,7 +168,7 @@ fn check_user_config() -> anyhow::Result> { return Ok(None); }; - let migrated = compute_migrated_content(&original); + let migrated = compute_migrated_content(&original, ConfigFileKind::User); Ok(Some(UpdateCandidate { config_path, original, @@ -200,7 +200,7 @@ fn check_project_config() -> anyhow::Result> { &config_path, &original, !is_linked, // only actionable from main worktree - "Project config", + ConfigFileKind::Project, Some(&repo), false, )?; @@ -216,7 +216,7 @@ fn check_project_config() -> anyhow::Result> { return Ok(None); } - let migrated = compute_migrated_content(&original); + let migrated = compute_migrated_content(&original, ConfigFileKind::Project); Ok(Some(UpdateCandidate { config_path, original, diff --git a/src/commands/list/mod.rs b/src/commands/list/mod.rs index b6260736e4..1a24cc5416 100644 --- a/src/commands/list/mod.rs +++ b/src/commands/list/mod.rs @@ -205,17 +205,16 @@ pub(crate) fn print_json(value: &T) -> anyhow::Result<()> { /// type error in the same key (warn and degrade, never brick a command). /// Both messages honor warning suppression — on the statusline, stderr /// would corrupt the consumer's prompt, and the same user sees the nag on -/// their next interactive run. The nag names both settings so the fix is -/// copyable from the warning itself. +/// their next interactive run. /// -/// The unset nag deliberately bypasses the config deprecation system -/// (`DEPRECATION_RULES` / `wt config update`): nothing in the file is -/// deprecated — unset is a pending choice between two valid schemas — so the -/// warning belongs on the surfaces that emit JSON rather than on every config -/// load, and `wt config update` has no behavior-preserving rewrite to offer: -/// inserting `= 1` would pin users to the schema the migration is moving away -/// from. The machinery joins at the default flip, when `= 1` becomes a -/// standard `DEPRECATION_RULES` row that `wt config update` strips (migration +/// The unset state is the `PendingDefault` row in `DEPRECATION_RULES`, but +/// its warning fires here rather than at config load: the setting only +/// matters to JSON consumers, so a load-time warning would nag every command +/// for every user without the key. `wt config update` pins the current +/// `json-schema = 1` (the behavior-preserving choice; adopting schema 2 is a +/// deliberate manual edit), so the nag's hint offers that command when a user +/// config file exists to update — with no user config there is nothing for +/// update to rewrite, and the hint names both settings instead (migration /// plan in design/list-json-v2.md, reviewed in #3357). pub(crate) fn resolve_json_schema(repo: &Repository) -> u8 { use std::sync::Once; @@ -251,12 +250,18 @@ pub(crate) fn resolve_json_schema(repo: &Repository) -> u8 { "JSON output is schema 1; a future release switches the default to schema 2" ) ); - eprintln!( - "{}", - hint_message(cformat!( - "To keep this format set [list] json-schema = 1; to adopt the new one, json-schema = 2" - )) - ); + let has_user_config = + worktrunk::config::config_path().is_some_and(|p| p.exists()); + let hint = if has_user_config { + cformat!( + "To adopt the new schema set [list] json-schema = 2; to keep this format, run wt config update" + ) + } else { + cformat!( + "To adopt the new schema set [list] json-schema = 2; to keep this format, json-schema = 1" + ) + }; + eprintln!("{}", hint_message(hint)); }); 1 } diff --git a/src/config/deprecation.rs b/src/config/deprecation.rs index a9fb665908..e52d6ab292 100644 --- a/src/config/deprecation.rs +++ b/src/config/deprecation.rs @@ -14,6 +14,13 @@ //! one predicate and cannot drift. The table order is both the //! warning-emission order and the migration order. //! +//! The table also carries pending default changes +//! ([`DeprecationRule::PendingDefault`]): defaults a future release switches, +//! which `wt config update` pins to their current value. These share the +//! detection-equals-migration predicate but warn at the surface that reads +//! the setting (the `wt list` JSON nag) instead of at config load, and apply +//! only to the config kind that owns the key. +//! //! Detection is purely in-memory — nothing writes to the filesystem from a //! config load path. `check_and_migrate` returns the structurally migrated //! content (for serde) and a `DeprecationInfo` describing what needs fixing. @@ -42,6 +49,33 @@ use crate::styling::{ warning_message, }; +/// Which config file a deprecation pass is examining. +/// +/// Replaces the string labels that used to travel with each check: the kind +/// derives the display label, and kind-scoped rules +/// (`DeprecationRule::PendingDefault`) use it to apply only to the config +/// file that owns their key. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConfigFileKind { + /// `~/.config/worktrunk/config.toml` — the file `wt config update` rewrites. + User, + /// The system-wide config layer (same schema as user config; never + /// rewritten by `wt config update`). + System, + /// The repo's `.config/wt.toml`. + Project, +} + +impl ConfigFileKind { + pub fn label(self) -> &'static str { + match self { + Self::User => "User config", + Self::System => "System config", + Self::Project => "Project config", + } + } +} + /// Tracks which config paths have already shown deprecation warnings this process. /// Prevents repeated warnings when config is loaded multiple times. static WARNED_DEPRECATED_PATHS: LazyLock>> = @@ -450,6 +484,23 @@ pub enum DeprecationKind { NoCd, /// `timeout-ms` under `[switch.picker]` (removed — picker renders progressively). SwitchPickerTimeout, + /// `[list] json-schema` unset while the default is scheduled to switch to + /// schema 2 — `wt config update` pins the current `json-schema = 1`. + /// Warns at the JSON-emitting surface (`resolve_json_schema`), not at + /// config load. + JsonSchemaUnset, +} + +impl DeprecationKind { + /// Whether this kind pins a pending default change rather than rewriting + /// a deprecated pattern. Pending defaults warn at the surface that reads + /// the setting (the `wt list` JSON nag) instead of at config load — the + /// setting only matters to consumers of that surface — and still render + /// on the pull surfaces (`wt config show`, the `wt config update` + /// preview), where the user asked for details. + fn is_pending_default(&self) -> bool { + matches!(self, Self::JsonSchemaUnset) + } } /// All deprecation patterns detected in a config file, in the order their @@ -488,6 +539,30 @@ enum DeprecationRule { /// Silently-migrated rename: rewritten on every load like `Structural`, /// but with no warning by construction. Silent(SilentMigrateFn), + /// Pending default change: a default that a future release switches, which + /// `wt config update` pins to its current value. Applies only on the + /// update pass and only to the config kind that owns the key — the load + /// path must leave the document alone (an in-memory pin would read as an + /// explicit setting and silence the usage-site nag). Its + /// [`DeprecationKind`] returns true from `is_pending_default`, so the warning + /// fires where the setting is consumed rather than on every config load. + PendingDefault { + kind: ConfigFileKind, + migrate: MigrateFn, + }, +} + +/// Which pass rules run under. +/// +/// `Load` is the structural rewrite before serde parses — `Structural` and +/// `Silent` rows only. `Update` is what `wt config update` materializes — +/// every row, with kind-scoped rows applying only to their config kind. +/// Detection always runs the `Update` pass against a scratch copy, so a +/// rule's detection and migration share one predicate and cannot drift. +#[derive(Clone, Copy, PartialEq, Eq)] +enum RulePass { + Load, + Update(ConfigFileKind), } /// Every deprecation, one row each. The table order is the contract: @@ -522,7 +597,9 @@ enum DeprecationRule { /// [`DeprecationKind`] variant with its `format_deprecation_warnings` arm, /// and a row here (plus a [`DeprecatedSection`] entry for a removed top-level /// section). A silently-migrated rename is just a [`DeprecationRule::Silent`] -/// row. +/// row. A pending default change is a [`DeprecationRule::PendingDefault`] row +/// whose kind returns true from `is_pending_default` — the surface that reads the +/// setting owns the warning. const DEPRECATION_RULES: &[DeprecationRule] = &[ // Template variables: {{ repo_root }} → {{ repo_path }} etc., inside any // string value. @@ -569,17 +646,56 @@ const DEPRECATION_RULES: &[DeprecationRule] = &[ Vec::new() } }), + // [list] json-schema unset → pin json-schema = 1 ahead of the default + // switching to schema 2. User config only: the key isn't valid in project + // config, and the top-level pin covers every repo (per-project overrides + // in user config remain the user's own choice). + DeprecationRule::PendingDefault { + kind: ConfigFileKind::User, + migrate: pin_json_schema_doc, + }, ]; +/// Pin `[list] json-schema = 1` at the top level when the key is absent. +/// +/// A `list` slot occupied by a non-table is left alone (serde's type error is +/// the messaging); a present key of any value is the user's explicit choice, +/// including out-of-range values, which already warn at resolve time. +fn pin_json_schema_doc(doc: &mut toml_edit::DocumentMut) -> Deprecations { + match doc.get("list") { + None => {} + Some(toml_edit::Item::Table(t)) if !t.contains_key("json-schema") => {} + Some(toml_edit::Item::Value(toml_edit::Value::InlineTable(t))) + if !t.contains_key("json-schema") => {} + // A non-table occupant (serde's type error is the messaging) or a + // present key of any value — the user's explicit choice, including + // out-of-range values, which already warn at resolve time. + _ => return Vec::new(), + } + match doc + .entry("list") + .or_insert(toml_edit::Item::Table(toml_edit::Table::new())) + { + toml_edit::Item::Table(t) => { + t.insert("json-schema", toml_edit::value(1)); + } + toml_edit::Item::Value(toml_edit::Value::InlineTable(t)) => { + t.insert("json-schema", 1.into()); + } + _ => unreachable!("the match above verified the slot is absent or a table"), + } + vec![DeprecationKind::JsonSchemaUnset] +} + /// Detect deprecations in config content. Pure function, no I/O. /// /// Returns the detected deprecation patterns. This is the recommended entry /// point for deprecation detection. -pub fn detect_deprecations(content: &str) -> Deprecations { +pub fn detect_deprecations(content: &str, kind: ConfigFileKind) -> Deprecations { let Ok(doc) = content.parse::() else { return Vec::new(); }; - detect_deprecations_from_doc(&doc) + detect_deprecations_from_doc(&doc, kind) } /// Detect deprecations from an already-parsed document. @@ -588,24 +704,25 @@ pub fn detect_deprecations(content: &str) -> Deprecations { /// a warning fires exactly when `wt config update` would change the file. /// Pushes kinds in [`DEPRECATION_RULES`] order — the warning-emission order — /// so iterating the returned `Vec` reproduces the warning text byte-for-byte. -fn detect_deprecations_from_doc(doc: &toml_edit::DocumentMut) -> Deprecations { +fn detect_deprecations_from_doc( + doc: &toml_edit::DocumentMut, + kind: ConfigFileKind, +) -> Deprecations { let mut scratch = doc.clone(); let mut kinds = Vec::new(); - apply_rules(&mut scratch, true, &mut kinds); + apply_rules(&mut scratch, RulePass::Update(kind), &mut kinds); kinds } /// Run every rule in table order against `doc`, appending each rule's warning /// kinds to `kinds` and returning whether the document changed. /// -/// `include_update_only` is false on the load path: [`DeprecationRule::UpdateOnly`] -/// rewrites are cosmetic or still-valid serde fields, so serde doesn't need -/// them applied. Detection and [`compute_migrated_content`] pass true. -fn apply_rules( - doc: &mut toml_edit::DocumentMut, - include_update_only: bool, - kinds: &mut Deprecations, -) -> bool { +/// The load pass excludes [`DeprecationRule::UpdateOnly`] rewrites (cosmetic +/// or still-valid serde fields, so serde doesn't need them applied) and +/// [`DeprecationRule::PendingDefault`] pins (an in-memory pin would silence +/// the usage-site nag). Detection and [`compute_migrated_content`] run the +/// update pass. +fn apply_rules(doc: &mut toml_edit::DocumentMut, pass: RulePass, kinds: &mut Deprecations) -> bool { let mut modified = false; for rule in DEPRECATION_RULES { match rule { @@ -615,13 +732,20 @@ fn apply_rules( kinds.extend(new_kinds); } DeprecationRule::UpdateOnly(migrate) => { - if include_update_only { + if matches!(pass, RulePass::Update(_)) { let new_kinds = migrate(doc); modified |= !new_kinds.is_empty(); kinds.extend(new_kinds); } } DeprecationRule::Silent(migrate) => modified |= migrate(doc), + DeprecationRule::PendingDefault { kind, migrate } => { + if pass == RulePass::Update(*kind) { + let new_kinds = migrate(doc); + modified |= !new_kinds.is_empty(); + kinds.extend(new_kinds); + } + } } } modified @@ -990,7 +1114,7 @@ fn migrate_negated_bool_doc( /// renaming is cosmetic (would break `--var` overrides), and approved-commands /// is still a valid serde field. They apply in [`compute_migrated_content`]. fn migrate_content_doc(doc: &mut toml_edit::DocumentMut) -> bool { - apply_rules(doc, false, &mut Vec::new()) + apply_rules(doc, RulePass::Load, &mut Vec::new()) } /// Rename `old_key` to `new_key` at the top level and under each `[projects."..."]`. @@ -1195,8 +1319,9 @@ pub struct DeprecationInfo { pub config_path: PathBuf, /// All detected deprecations pub deprecations: Deprecations, - /// Label for this config (e.g., "User config", "Project config") - pub label: String, + /// Which config file this is; derives the display label and scopes + /// [`compute_migrated_content`] to the rules that apply to it. + pub kind: ConfigFileKind, /// Main worktree path when viewing from a linked worktree (for `-C` in hints) pub main_worktree_path: Option, } @@ -1206,6 +1331,19 @@ impl DeprecationInfo { pub fn has_deprecations(&self) -> bool { !self.deprecations.is_empty() } + + /// Display label for this config file (e.g., "User config"). + pub fn label(&self) -> &'static str { + self.kind.label() + } + + /// True when the file contains deprecated patterns, as opposed to only + /// pending-default pins, which deprecate nothing in the file. `wt config + /// show` dumps the config only when this is false: a deprecation diff + /// supersedes the dump, while a pending pin is additive. + pub fn has_deprecated_patterns(&self) -> bool { + self.deprecations.iter().any(|k| !k.is_pending_default()) + } } /// Result of checking config content for deprecations. @@ -1235,7 +1373,8 @@ pub struct CheckAndMigrateResult { /// the warning is only actionable from the main worktree where the user would /// run `wt config update`. /// -/// The `label` is used in the warning message (e.g., "User config" or "Project config"). +/// `kind` names the config file being checked; it derives the warning label +/// and scopes kind-specific rules (`DeprecationRule::PendingDefault`). /// /// `repo` is used to resolve the primary worktree path for the "run this from /// the main worktree" hint when viewing project config from a linked worktree. @@ -1252,7 +1391,7 @@ pub fn check_and_migrate( path: &Path, content: &str, warn_and_migrate: bool, - label: &str, + kind: ConfigFileKind, repo: Option<&crate::git::Repository>, emit_inline_warnings: bool, ) -> anyhow::Result { @@ -1262,7 +1401,7 @@ pub fn check_and_migrate( // `info` is `Some`) can assume the content parses. let (deprecations, migrated_content) = match content.parse::() { Ok(doc) => { - let deprecations = detect_deprecations_from_doc(&doc); + let deprecations = detect_deprecations_from_doc(&doc, kind); let migrated_content = migrate_content_from_doc(content, doc); (deprecations, migrated_content) } @@ -1279,7 +1418,7 @@ pub fn check_and_migrate( let info = DeprecationInfo { config_path: path.to_path_buf(), deprecations, - label: label.to_string(), + kind, main_worktree_path: if !warn_and_migrate { repo.and_then(|r| r.repo_path().ok()) .map(|p| p.to_path_buf()) @@ -1312,14 +1451,16 @@ pub fn check_and_migrate( } // For non-config-show commands, emit per-kind warnings but skip the diff. - // The diff is reserved for `wt config show`, where the user has opted into details. - // - // Some deprecations migrate silently (e.g. the `-create` → `-start` hook - // rename): `format_deprecation_warnings` emits nothing for them. The hint is - // gated on the warning text being non-empty so a silently-migrated config + // The diff is reserved for `wt config show`, where the user has opted into + // details. Pending-default kinds warn at their own usage surface instead + // of at load, so they are filtered out here, and the hint is gated on the + // remaining text being non-empty so a config with nothing to say at load // produces no stray "run wt config show" line with nothing above it. if emit_inline_warnings && !warnings_suppressed() { - let warnings = format_deprecation_warnings(&info); + let warnings = format_warning_lines( + info.deprecations.iter().filter(|k| !k.is_pending_default()), + info.label(), + ); if !warnings.is_empty() { eprint!("{warnings}"); if DEPRECATION_HINT_EMITTED.set(()).is_ok() { @@ -1350,14 +1491,14 @@ pub fn check_and_migrate( /// Pure function — no filesystem access. Idempotent: feeding its own output /// back in is a no-op. Callers materialize the result via `wt config update` /// or display it via `wt config show`. -pub fn compute_migrated_content(content: &str) -> String { +pub fn compute_migrated_content(content: &str, kind: ConfigFileKind) -> String { // Callers (`wt config show`, `wt config update`, `format_deprecation_details`) // all run content through `check_and_migrate` first, so it is known to parse. let mut doc = content .parse::() .expect("compute_migrated_content called with content that failed TOML parse; callers must funnel through check_and_migrate first"); - if apply_rules(&mut doc, true, &mut Vec::new()) { + if apply_rules(&mut doc, RulePass::Update(kind), &mut Vec::new()) { doc.to_string() } else { content.to_string() @@ -1402,15 +1543,20 @@ pub fn format_migration_diff(original: &str, migrated: &str, label: &str) -> Opt /// approved-commands. Used by both `format_deprecation_details` (which adds the /// `wt config update` hint and diff) and `wt config update` (which applies directly). pub fn format_deprecation_warnings(info: &DeprecationInfo) -> String { + format_warning_lines(&info.deprecations, info.label()) +} + +/// Render one `warning_message` line per kind (the commit-generation kind can +/// emit several). The kinds arrive in emission order, so a single pass +/// reproduces the original output verbatim. +fn format_warning_lines<'a>( + kinds: impl IntoIterator, + label: &str, +) -> String { use std::fmt::Write; - let label = &info.label; let mut out = String::new(); - // One `warning_message` line per emitted message. The kinds are stored in - // emission order, so a single pass reproduces the original output verbatim. - // Each arm pushes its own newline so a multi-line kind (commit-generation) - // can emit several lines. - for kind in &info.deprecations { + for kind in kinds { match kind { DeprecationKind::TemplateVar { old, new } => { let _ = writeln!( @@ -1495,6 +1641,15 @@ pub fn format_deprecation_warnings(info: &DeprecationInfo) -> String { )) ); } + DeprecationKind::JsonSchemaUnset => { + let _ = writeln!( + out, + "{}", + warning_message(cformat!( + "{label}: [list] json-schema is unset; a future release switches the JSON default to schema 2" + )) + ); + } } } @@ -1532,7 +1687,7 @@ pub fn format_deprecation_details(info: &DeprecationInfo, original_content: &str hint_message(cformat!("To apply: wt config update")) ); - let migrated = compute_migrated_content(original_content); + let migrated = compute_migrated_content(original_content, info.kind); let label = info .config_path .file_name() @@ -1757,7 +1912,7 @@ mod tests { } fn find_deprecated_vars(content: &str) -> Vec<(&'static str, &'static str)> { - detect_deprecations(content) + detect_deprecations(content, ConfigFileKind::User) .into_iter() .filter_map(|k| match k { DeprecationKind::TemplateVar { old, new } => Some((old, new)), @@ -1774,7 +1929,7 @@ mod tests { } fn find_commit_generation_deprecations(content: &str) -> CommitGenerationDeprecations { - detect_deprecations(content) + detect_deprecations(content, ConfigFileKind::User) .into_iter() .find_map(|k| match k { DeprecationKind::CommitGeneration(found) => Some(found), @@ -1784,13 +1939,13 @@ mod tests { } fn find_approved_commands_deprecation(content: &str) -> bool { - has_kind(&detect_deprecations(content), |k| { + has_kind(&detect_deprecations(content, ConfigFileKind::User), |k| { matches!(k, DeprecationKind::ApprovedCommands) }) } fn find_select_deprecation(content: &str) -> bool { - has_kind(&detect_deprecations(content), |k| { + has_kind(&detect_deprecations(content, ConfigFileKind::User), |k| { matches!(k, DeprecationKind::Select) }) } @@ -1936,7 +2091,7 @@ post-start = "cd {{ worktree_path }} && npm install" #[test] fn test_compute_migrated_content_escaped_quotes() { let content = "pre-start = \"echo \\\"{{ repo_root }}\\\"\"\n"; - let migrated = compute_migrated_content(content); + let migrated = compute_migrated_content(content, ConfigFileKind::User); assert!( !migrated.contains("repo_root"), "compute_migrated_content must migrate vars inside escaped strings; got: {migrated}" @@ -2033,20 +2188,25 @@ timeout = 30 /// Canonical config with no deprecations must round-trip through /// `compute_migrated_content` byte-for-byte (the unmodified branch). + /// Canonical includes an explicit json-schema value — without one the + /// pending-default row pins it. #[test] fn test_compute_migrated_content_noop_returns_input_unchanged() { - let content = "pre-start = \"echo {{ repo_path }}\"\n"; - assert_eq!(compute_migrated_content(content), content); + let content = "pre-start = \"echo {{ repo_path }}\"\n\n[list]\njson-schema = 1\n"; + assert_eq!( + compute_migrated_content(content, ConfigFileKind::User), + content + ); } #[test] fn test_compute_migrated_content_does_not_rewrite_literal_text_when_other_template_uses_deprecated_var() { - let content = "pre-merge = \"echo repo_root\"\npost-merge = \"echo {{ repo_root }}\"\n"; - let migrated = compute_migrated_content(content); + let content = "pre-merge = \"echo repo_root\"\npost-merge = \"echo {{ repo_root }}\"\nlist = { json-schema = 1 }\n"; + let migrated = compute_migrated_content(content, ConfigFileKind::User); assert_eq!( migrated, - "pre-merge = \"echo repo_root\"\npost-merge = \"echo {{ repo_path }}\"\n" + "pre-merge = \"echo repo_root\"\npost-merge = \"echo {{ repo_path }}\"\nlist = { json-schema = 1 }\n" ); } @@ -2277,8 +2437,14 @@ approved-commands = [ let non_existent_path = std::path::Path::new("/nonexistent/dir/config.toml"); // Should return Ok(Some(_)) even if write fails - the function logs error but doesn't fail - let result = - check_and_migrate(non_existent_path, content, true, "Test config", None, false); + let result = check_and_migrate( + non_existent_path, + content, + true, + ConfigFileKind::User, + None, + false, + ); assert!(result.is_ok()); assert!(result.unwrap().info.is_some()); } @@ -2291,12 +2457,26 @@ approved-commands = [ let unique_path = std::path::Path::new("/nonexistent/dedup_test_12345/config.toml"); // First call should process normally - let result1 = check_and_migrate(unique_path, content, true, "Test config", None, false); + let result1 = check_and_migrate( + unique_path, + content, + true, + ConfigFileKind::User, + None, + false, + ); assert!(result1.is_ok()); assert!(result1.unwrap().info.is_some()); // Second call with same path should early-return (hits the deduplication branch) - let result2 = check_and_migrate(unique_path, content, true, "Test config", None, false); + let result2 = check_and_migrate( + unique_path, + content, + true, + ConfigFileKind::User, + None, + false, + ); assert!(result2.is_ok()); assert!(result2.unwrap().info.is_some()); } @@ -2312,7 +2492,7 @@ pager = "delta" std::path::Path::new("/tmp/config.toml"), content, true, - "Test config", + ConfigFileKind::User, None, false, ) @@ -2938,16 +3118,22 @@ ff = false [ci] platform = "github" + +[list] +json-schema = 1 "#; assert_eq!(migrate_content(content), content); - assert!(detect_deprecations(content).is_empty()); + assert!(detect_deprecations(content, ConfigFileKind::User).is_empty()); } /// The framework invariant: a warning fires exactly when `wt config /// update` would change the file. Degenerate configs that can't be safely /// rewritten produce no warning and no rewrite; deprecated configs /// produce both. (Silent renames change the file without warning by - /// design and aren't part of this battery.) + /// design and aren't part of this battery. Every case gets an explicit + /// json-schema pin appended so only the rule under test drives the diff; + /// the pending-default row satisfies the same invariant with its warning + /// at the JSON-emitting surface — see `test_json_schema_pin_iff`.) #[test] fn test_warning_fires_iff_update_changes() { let untouched = [ @@ -2973,12 +3159,13 @@ platform = "github" "[projects.\"github.com/u/r\"]\napproved-commands = []\n", ]; for content in untouched { + let content = &format!("{content}\n[list]\njson-schema = 1\n"); assert!( - detect_deprecations(content).is_empty(), + detect_deprecations(content, ConfigFileKind::User).is_empty(), "no warning expected for:\n{content}" ); assert_eq!( - compute_migrated_content(content), + &compute_migrated_content(content, ConfigFileKind::User), content, "no rewrite expected for:\n{content}" ); @@ -2998,27 +3185,101 @@ platform = "github" "[projects.\"github.com/u/r\"]\napproved-commands = [\"npm test\"]\n", ]; for content in rewritten { + let content = &format!("{content}\n[list]\njson-schema = 1\n"); assert!( - !detect_deprecations(content).is_empty(), + !detect_deprecations(content, ConfigFileKind::User).is_empty(), "warning expected for:\n{content}" ); - let migrated = compute_migrated_content(content); - assert_ne!(migrated, content, "rewrite expected for:\n{content}"); + let migrated = compute_migrated_content(content, ConfigFileKind::User); + assert_ne!(&migrated, content, "rewrite expected for:\n{content}"); // The user-visible loop closes: applying the update silences the // warning. assert!( - detect_deprecations(&migrated).is_empty(), + detect_deprecations(&migrated, ConfigFileKind::User).is_empty(), "no warning expected after update for:\n{migrated}" ); } } + /// The pending-default row's own iff: the unset nag (fired by + /// `resolve_json_schema`, not at config load) corresponds exactly to + /// `wt config update` pinning the key. + #[test] + fn test_json_schema_pin_iff() { + // Unset → detected and pinned, and applying the update closes the loop. + let migrated = compute_migrated_content("", ConfigFileKind::User); + insta::assert_snapshot!(migrated, @r#" + [list] + json-schema = 1 + "#); + assert!(matches!( + detect_deprecations("", ConfigFileKind::User).as_slice(), + [DeprecationKind::JsonSchemaUnset] + )); + assert!(detect_deprecations(&migrated, ConfigFileKind::User).is_empty()); + + // Any present value is the user's explicit choice — including + // out-of-range ones, which warn at resolve time instead. + let untouched = [ + "[list]\njson-schema = 1\n", + "[list]\njson-schema = 2\n", + "[list]\njson-schema = 42\n", + "list = { json-schema = 2 }\n", + // a non-table `list` is left for serde's type error + "list = 5\n", + ]; + for content in untouched { + assert!( + detect_deprecations(content, ConfigFileKind::User).is_empty(), + "no detection expected for:\n{content}" + ); + assert_eq!( + compute_migrated_content(content, ConfigFileKind::User), + content, + "no rewrite expected for:\n{content}" + ); + } + } + + /// The pin lands inside an existing `[list]` section (either shape) + /// rather than duplicating it. + #[test] + fn test_json_schema_pin_joins_existing_list_section() { + let migrated = + compute_migrated_content("[list]\ncolumns = [\"ci\"]\n", ConfigFileKind::User); + insta::assert_snapshot!(migrated, @r#" + [list] + columns = ["ci"] + json-schema = 1 + "#); + + let migrated = + compute_migrated_content("list = { columns = [\"ci\"] }\n", ConfigFileKind::User); + insta::assert_snapshot!(migrated, @r#"list = { columns = ["ci"] , json-schema = 1 }"#); + } + + /// The pin is scoped to user config — project config doesn't own the key, + /// and the system layer isn't rewritten by `wt config update` — and to + /// the update pass: an in-memory pin at load would read as an explicit + /// setting and silence the usage-site nag. + #[test] + fn test_json_schema_pin_scope() { + for kind in [ConfigFileKind::System, ConfigFileKind::Project] { + assert!(detect_deprecations("", kind).is_empty()); + assert_eq!(compute_migrated_content("", kind), ""); + } + assert_eq!(migrate_content(""), ""); + } + /// A `timeout-ms` written under `[select]` is reported by the timeout /// rule after the `[select]` rule moves it under `[switch.picker]`. /// (Previously it was dropped on load with only the `[select]` warning.) #[test] fn test_select_timeout_ms_warns_both_kinds() { - let deprecations = detect_deprecations("[select]\npager = \"delta\"\ntimeout-ms = 500\n"); + let deprecations = detect_deprecations( + "[select]\npager = \"delta\"\ntimeout-ms = 500\n", + ConfigFileKind::User, + ); assert!(has_kind(&deprecations, |k| matches!( k, DeprecationKind::Select @@ -3265,7 +3526,7 @@ approved-commands = ["npm install"] [projects."github.com/user/repo"] approved-commands = ["npm install"] "#; - let deprecations = detect_deprecations(content); + let deprecations = detect_deprecations(content, ConfigFileKind::User); assert!(has_kind(&deprecations, |k| matches!( k, DeprecationKind::ApprovedCommands @@ -3289,7 +3550,7 @@ approved-commands = ["npm install"] let info = DeprecationInfo { config_path: std::path::PathBuf::from("/tmp/test-config.toml"), deprecations: vec![DeprecationKind::ApprovedCommands], - label: "User config".to_string(), + kind: ConfigFileKind::User, main_worktree_path: None, }; let output = format_deprecation_details(&info, content); @@ -3312,7 +3573,7 @@ approved-commands = ["npm install"] [projects."github.com/user/repo"] approved-commands = ["npm install"] "#; - let migrated = compute_migrated_content(content); + let migrated = compute_migrated_content(content, ConfigFileKind::User); assert!(!migrated.contains("approved-commands")); } @@ -3652,7 +3913,7 @@ full = true [select] pager = "delta" "#; - let deprecations = detect_deprecations(content); + let deprecations = detect_deprecations(content, ConfigFileKind::User); assert!(has_kind(&deprecations, |k| matches!( k, DeprecationKind::Select @@ -3682,7 +3943,7 @@ pager = "delta --paging=never" let info = DeprecationInfo { config_path: std::path::PathBuf::from("/tmp/test-config.toml"), deprecations: vec![DeprecationKind::Select], - label: "User config".to_string(), + kind: ConfigFileKind::User, main_worktree_path: None, }; let output = format_deprecation_details(&info, content); @@ -3703,7 +3964,7 @@ pager = "delta --paging=never" [select] pager = "delta --paging=never" "#; - let migrated = compute_migrated_content(content); + let migrated = compute_migrated_content(content, ConfigFileKind::User); assert!( migrated.contains("[switch.picker]"), "Migrated content should have [switch.picker]: {migrated}" @@ -3784,8 +4045,11 @@ post-start = "new" [post-create] server = "npm run dev" + +[list] +json-schema = 1 "#; - let migrated = compute_migrated_content(content); + let migrated = compute_migrated_content(content, ConfigFileKind::User); insta::assert_snapshot!(migration_diff(content, &migrated)); } @@ -3796,7 +4060,7 @@ server = "npm run dev" pager = "delta" timeout-ms = 500 "#; - let deprecations = detect_deprecations(content); + let deprecations = detect_deprecations(content, ConfigFileKind::User); assert!(has_kind(&deprecations, |k| matches!( k, DeprecationKind::SwitchPickerTimeout @@ -3810,7 +4074,7 @@ timeout-ms = 500 [projects."github.com/user/repo".switch.picker] timeout-ms = 300 "#; - let deprecations = detect_deprecations(content); + let deprecations = detect_deprecations(content, ConfigFileKind::User); assert!(has_kind(&deprecations, |k| matches!( k, DeprecationKind::SwitchPickerTimeout @@ -3823,7 +4087,7 @@ timeout-ms = 300 [switch] picker = { pager = "delta", timeout-ms = 500 } "#; - let deprecations = detect_deprecations(content); + let deprecations = detect_deprecations(content, ConfigFileKind::User); assert!(has_kind(&deprecations, |k| matches!( k, DeprecationKind::SwitchPickerTimeout @@ -3847,7 +4111,7 @@ picker = { pager = "delta", timeout-ms = 500 } [switch.picker] pager = "delta" "#; - let deprecations = detect_deprecations(content); + let deprecations = detect_deprecations(content, ConfigFileKind::User); assert!(!has_kind(&deprecations, |k| matches!( k, DeprecationKind::SwitchPickerTimeout @@ -3906,7 +4170,7 @@ pager = "delta" let info = DeprecationInfo { config_path: std::path::PathBuf::from("/tmp/test-config.toml"), deprecations: vec![DeprecationKind::SwitchPickerTimeout], - label: "User config".to_string(), + kind: ConfigFileKind::User, main_worktree_path: None, }; let output = format_deprecation_warnings(&info); @@ -3927,7 +4191,7 @@ pager = "delta" let info = DeprecationInfo { config_path: std::path::PathBuf::from("/tmp/test-config.toml"), deprecations: vec![DeprecationKind::NoFf, DeprecationKind::NoCd], - label: "User config".to_string(), + kind: ConfigFileKind::User, main_worktree_path: None, }; let output = format_deprecation_warnings(&info); @@ -3937,7 +4201,7 @@ pager = "delta" #[test] fn test_detect_no_ff_deprecation() { - let deprecations = detect_deprecations("[merge]\nno-ff = true\n"); + let deprecations = detect_deprecations("[merge]\nno-ff = true\n", ConfigFileKind::User); assert!(has_kind(&deprecations, |k| matches!( k, DeprecationKind::NoFf @@ -3951,7 +4215,7 @@ pager = "delta" #[test] fn test_no_ff_warned_and_removed_when_ff_exists() { let content = "[merge]\nff = true\nno-ff = true\n"; - let deprecations = detect_deprecations(content); + let deprecations = detect_deprecations(content, ConfigFileKind::User); assert!(has_kind(&deprecations, |k| matches!( k, DeprecationKind::NoFf @@ -3964,7 +4228,7 @@ pager = "delta" #[test] fn test_detect_no_cd_deprecation() { - let deprecations = detect_deprecations("[switch]\nno-cd = true\n"); + let deprecations = detect_deprecations("[switch]\nno-cd = true\n", ConfigFileKind::User); assert!(has_kind(&deprecations, |k| matches!( k, DeprecationKind::NoCd @@ -3977,7 +4241,7 @@ pager = "delta" [projects."github.com/user/repo".merge] no-ff = true "#; - let deprecations = detect_deprecations(content); + let deprecations = detect_deprecations(content, ConfigFileKind::User); assert!(has_kind(&deprecations, |k| matches!( k, DeprecationKind::NoFf @@ -4049,7 +4313,7 @@ no-ff = true [projects."github.com/user/repo".select] pager = "bat" "#; - let deprecations = detect_deprecations(content); + let deprecations = detect_deprecations(content, ConfigFileKind::User); assert!(has_kind(&deprecations, |k| matches!( k, DeprecationKind::Select @@ -4187,16 +4451,16 @@ server = "npm run dev" [projects."github.com/user/repo"] approved-commands = ["npm test"] "#; - let migrated = compute_migrated_content(content); + let migrated = compute_migrated_content(content, ConfigFileKind::User); assert_eq!( - compute_migrated_content(&migrated), + compute_migrated_content(&migrated, ConfigFileKind::User), migrated, "migration must be idempotent" ); assert!( - detect_deprecations(&migrated).is_empty(), + detect_deprecations(&migrated, ConfigFileKind::User).is_empty(), "applying the update must silence every warning; got {:?}", - detect_deprecations(&migrated) + detect_deprecations(&migrated, ConfigFileKind::User) ); insta::assert_snapshot!(migration_diff(content, &migrated)); } diff --git a/src/config/mod.rs b/src/config/mod.rs index 2a57186078..6c8c45944a 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -127,6 +127,7 @@ pub(crate) fn schema_top_level_keys() -> Vec { pub use approvals::{Approvals, approvals_path, require_approvals_path}; pub use commands::{Command, CommandConfig, HookStep, append_aliases}; pub use deprecation::CheckAndMigrateResult; +pub use deprecation::ConfigFileKind; pub use deprecation::DeprecationInfo; pub use deprecation::check_and_migrate; pub use deprecation::compute_migrated_content; diff --git a/src/config/project.rs b/src/config/project.rs index dba8acdd68..b1e3e253c2 100644 --- a/src/config/project.rs +++ b/src/config/project.rs @@ -276,7 +276,7 @@ impl ProjectConfig { &config_path, &contents, is_main_worktree, - "Project config", + super::ConfigFileKind::Project, repo_for_hints, true, // emit_inline_warnings ) diff --git a/src/config/snapshots/worktrunk__config__deprecation__tests__snapshot_migrate_all_rules_combined.snap b/src/config/snapshots/worktrunk__config__deprecation__tests__snapshot_migrate_all_rules_combined.snap index ed016bd783..473d43bea4 100644 --- a/src/config/snapshots/worktrunk__config__deprecation__tests__snapshot_migrate_all_rules_combined.snap +++ b/src/config/snapshots/worktrunk__config__deprecation__tests__snapshot_migrate_all_rules_combined.snap @@ -18,6 +18,9 @@ expression: "migration_diff(content, &migrated)" platform = "github" -[select] ++[list] ++json-schema = 1 ++ +[switch.picker] pager = "delta" -timeout-ms = 500 diff --git a/src/config/snapshots/worktrunk__config__deprecation__tests__snapshot_migrate_create_to_start.snap b/src/config/snapshots/worktrunk__config__deprecation__tests__snapshot_migrate_create_to_start.snap index c2b4169838..60563bf694 100644 --- a/src/config/snapshots/worktrunk__config__deprecation__tests__snapshot_migrate_create_to_start.snap +++ b/src/config/snapshots/worktrunk__config__deprecation__tests__snapshot_migrate_create_to_start.snap @@ -8,3 +8,6 @@ expression: "migration_diff(content, &migrated)" -[post-create] +[post-start] server = "npm run dev" + + [list] + json-schema = 1 diff --git a/src/config/user/mod.rs b/src/config/user/mod.rs index 0b3c787ece..d8ae6e9d50 100644 --- a/src/config/user/mod.rs +++ b/src/config/user/mod.rs @@ -450,7 +450,7 @@ impl UserConfig { &system_path, &content, true, - "System config", + super::ConfigFileKind::System, None, true, ) { @@ -483,7 +483,7 @@ impl UserConfig { config_path, &content, true, - "User config", + super::ConfigFileKind::User, None, true, ) { diff --git a/tests/integration_tests/config_show.rs b/tests/integration_tests/config_show.rs index e60d64bec2..4281ae410f 100644 --- a/tests/integration_tests/config_show.rs +++ b/tests/integration_tests/config_show.rs @@ -3265,6 +3265,9 @@ fn test_config_update_print_on_clean_config_is_silent(repo: TestRepo) { fs::write( repo.test_config_path(), r#"worktree-path = "../{{ repo }}.{{ branch }}" + +[list] +json-schema = 1 "#, ) .unwrap(); @@ -3325,10 +3328,13 @@ fn test_config_update_print_emits_migrated_without_writing(repo: TestRepo) { /// `wt config update` with no deprecated settings reports nothing to do #[rstest] fn test_config_update_no_deprecations(repo: TestRepo) { - // Write a clean config with no deprecated patterns + // Clean config: no deprecated patterns, json-schema explicitly pinned fs::write( repo.test_config_path(), r#"worktree-path = "../{{ repo }}.{{ branch }}" + +[list] +json-schema = 1 "#, ) .unwrap(); @@ -3342,6 +3348,40 @@ fn test_config_update_no_deprecations(repo: TestRepo) { }); } +/// `wt config update` pins `[list] json-schema = 1` when the key is unset, +/// and a second run has nothing left to do — the pending-default loop closes. +#[rstest] +fn test_config_update_pins_json_schema(repo: TestRepo) { + fs::write( + repo.test_config_path(), + "worktree-path = \"../{{ repo }}.{{ branch }}\"\n", + ) + .unwrap(); + + let settings = setup_snapshot_settings(&repo); + settings.bind(|| { + let mut cmd = repo.wt_command(); + cmd.args(["config", "update", "--yes"]); + + assert_cmd_snapshot!(cmd); + }); + + assert_eq!( + fs::read_to_string(repo.test_config_path()).unwrap(), + "worktree-path = \"../{{ repo }}.{{ branch }}\"\n\n[list]\njson-schema = 1\n" + ); + + let output = repo + .wt_command() + .args(["config", "update", "--yes"]) + .output() + .unwrap(); + assert!( + String::from_utf8_lossy(&output.stderr).contains("No deprecated settings found"), + "second run should have nothing to update" + ); +} + /// `wt config update --yes` applies template variable migration #[rstest] fn test_config_update_applies_template_var_migration(repo: TestRepo) { diff --git a/tests/integration_tests/config_update_pty.rs b/tests/integration_tests/config_update_pty.rs index 26a0680fb2..f4f1a98472 100644 --- a/tests/integration_tests/config_update_pty.rs +++ b/tests/integration_tests/config_update_pty.rs @@ -38,6 +38,9 @@ fn test_config_update_prompt_accept(repo: TestRepo) { config_path, r#"worktree-path = "../{{ main_worktree }}.{{ branch }}" pre-start = "ln -sf {{ repo_root }}/node_modules" + +[list] +json-schema = 1 "#, ) .unwrap(); @@ -61,6 +64,9 @@ fn test_config_update_prompt_decline(repo: TestRepo) { let config_path = repo.test_config_path(); let original_content = r#"worktree-path = "../{{ main_worktree }}.{{ branch }}" pre-start = "ln -sf {{ repo_root }}/node_modules" + +[list] +json-schema = 1 "#; fs::write(config_path, original_content).unwrap(); diff --git a/tests/integration_tests/list.rs b/tests/integration_tests/list.rs index bfffee55bd..a28621cdce 100644 --- a/tests/integration_tests/list.rs +++ b/tests/integration_tests/list.rs @@ -248,7 +248,8 @@ fn test_list_json_schema_2_envelope(mut repo: TestRepo) { /// 1 or 2 is an error. #[rstest] fn test_list_json_schema_selection(repo: TestRepo) { - // Unset → schema 1 (bare array) + nag on stderr. + // Unset with no user config file → nag names the setting to write by + // hand; there is no file for `wt config update` to rewrite. let output = repo .wt_command() .args(["list", "--format=json"]) @@ -259,8 +260,27 @@ fn test_list_json_schema_selection(repo: TestRepo) { assert!(!json.is_empty(), "schema 1 root is a bare array"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("json-schema"), - "unset key should nag: {stderr}" + stderr.contains("json-schema = 1"), + "unset key should nag with the manual setting: {stderr}" + ); + assert!( + !stderr.contains("config update"), + "no update hint without a config file to update: {stderr}" + ); + + // Unset with a user config file present → the hint offers wt config + // update, which pins json-schema = 1. + repo.write_test_config(""); + let output = repo + .wt_command() + .args(["list", "--format=json"]) + .output() + .unwrap(); + assert!(output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("wt config update"), + "unset key with a config file should offer the update command: {stderr}" ); // Explicit 1 → schema 1, no nag. diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_claude_available_plugin_not_installed.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_claude_available_plugin_not_installed.snap index 4f976cdbc2..7336700bc6 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_claude_available_plugin_not_installed.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_claude_available_plugin_not_installed.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_clis_detected_via_path.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_clis_detected_via_path.snap index 2e71382ff6..d44225f856 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_clis_detected_via_path.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_clis_detected_via_path.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -41,6 +42,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_codex_available.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_codex_available.snap index 7cb1c7a433..880d283382 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_codex_available.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_codex_available.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml 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 1127df86fd..ca5b39bf99 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 @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -47,17 +48,21 @@ exit_code: 0 USER CONFIG @ ~/.config/worktrunk/config.toml ▲ User config: template variable repo_root is deprecated in favor of repo_path ▲ User config: template variable main_worktree is deprecated in favor of repo +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 ↳ To apply: wt config update ○ Proposed diff:   diff --git a/config.toml/current b/config.toml/migrated -  index dee4883..5eef8a6 100644 +  index dee4883..e90e8fd 100644   --- a/config.toml/current   +++ b/config.toml/migrated -  @@ -1,2 +1,2 @@ +  @@ -1,2 +1,5 @@   -worktree-path = "../{{ main_worktree }}.{{ branch }}"   -pre-start = "ln -sf {{ repo_root }}/node_modules"   +worktree-path = "../{{ repo }}.{{ branch }}"   +pre-start = "ln -sf {{ repo_path }}/node_modules" +  + +  +[list] +  +json-schema = 1 ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_displays_no_cd_deprecation.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_displays_no_cd_deprecation.snap index c0aa50fd13..bf87829bbf 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_displays_no_cd_deprecation.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_displays_no_cd_deprecation.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,16 +47,20 @@ exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml ▲ User config: switch.no-cd is deprecated in favor of switch.cd (inverted) +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 ↳ To apply: wt config update ○ Proposed diff:   diff --git a/config.toml/current b/config.toml/migrated -  index ec5c19e..2c6162a 100644 +  index ec5c19e..5c6669a 100644   --- a/config.toml/current   +++ b/config.toml/migrated -  @@ -1,2 +1,2 @@ +  @@ -1,2 +1,5 @@   [switch]   -no-cd = true   +cd = false +  + +  +[list] +  +json-schema = 1 ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_displays_no_ff_deprecation.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_displays_no_ff_deprecation.snap index e88e8b9db5..44aacbe483 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_displays_no_ff_deprecation.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_displays_no_ff_deprecation.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,16 +47,20 @@ exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml ▲ User config: merge.no-ff is deprecated in favor of merge.ff (inverted) +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 ↳ To apply: wt config update ○ Proposed diff:   diff --git a/config.toml/current b/config.toml/migrated -  index f4de629..27f3868 100644 +  index f4de629..2ae2c3d 100644   --- a/config.toml/current   +++ b/config.toml/migrated -  @@ -1,2 +1,2 @@ +  @@ -1,2 +1,5 @@   [merge]   -no-ff = true   +ff = false +  + +  +[list] +  +json-schema = 1 ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_displays_project_commit_generation_deprecations.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_displays_project_commit_generation_deprecations.snap index bce6668215..5117db3712 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_displays_project_commit_generation_deprecations.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_displays_project_commit_generation_deprecations.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,18 +47,22 @@ exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml ▲ User config: [projects."github.com/example/repo".commit-generation] is deprecated in favor of [projects."github.com/example/repo".commit.generation] +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 ↳ To apply: wt config update ○ Proposed diff:   diff --git a/config.toml/current b/config.toml/migrated -  index de80729..131a2fd 100644 +  index de80729..c3295a3 100644   --- a/config.toml/current   +++ b/config.toml/migrated -  @@ -1,4 +1,4 @@ +  @@ -1,4 +1,7 @@   worktree-path = "../{{ repo }}.{{ branch }}"      -[projects."github.com/example/repo".commit-generation]   +[projects."github.com/example/repo".commit.generation]   command = "llm -m gpt-4" +  + +  +[list] +  +json-schema = 1 ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_displays_select_section_deprecation.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_displays_select_section_deprecation.snap index e93c87774c..e67912c3cc 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_displays_select_section_deprecation.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_displays_select_section_deprecation.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,16 +47,20 @@ exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml ▲ User config: [select] is deprecated in favor of [switch.picker] +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 ↳ To apply: wt config update ○ Proposed diff:   diff --git a/config.toml/current b/config.toml/migrated -  index 0d6352a..3e94895 100644 +  index 0d6352a..62cd06c 100644   --- a/config.toml/current   +++ b/config.toml/migrated -  @@ -1,2 +1,2 @@ +  @@ -1,2 +1,5 @@   -[select]   +[switch.picker]   pager = "delta --paging=never" +  + +  +[list] +  +json-schema = 1 ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_empty_project_config.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_empty_project_config.snap index bf7ac811a0..aa812c6d06 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_empty_project_config.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_empty_project_config.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_full_command_not_found.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_full_command_not_found.snap index cae655c0f8..e8b935af55 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_full_command_not_found.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_full_command_not_found.snap @@ -8,6 +8,7 @@ info: - "--full" env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -47,6 +48,21 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index 6114dbb..0167af2 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -2,3 +2,6 @@ worktree-path = "../{{ repo }}.{{ branch }}" +   +  [commit.generation] +  command = "nonexistent-llm-command-12345 -m test-model" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}"     [commit.generation] diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_full_gitea_remote.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_full_gitea_remote.snap index 875519ed6f..2f1a3bb965 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_full_gitea_remote.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_full_gitea_remote.snap @@ -8,6 +8,7 @@ info: - "--full" env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -47,6 +48,21 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a10f823..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  -worktree-path = "../{{ repo }}.{{ branch }}" +  \ No newline at end of file +  +worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_full_not_configured.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_full_not_configured.snap index 1484975815..48d45e35dd 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_full_not_configured.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_full_not_configured.snap @@ -8,6 +8,7 @@ info: - "--full" env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -47,6 +48,21 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a10f823..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  -worktree-path = "../{{ repo }}.{{ branch }}" +  \ No newline at end of file +  +worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_full_update_available.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_full_update_available.snap index 45f345ff03..d5c330d637 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_full_update_available.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_full_update_available.snap @@ -8,6 +8,7 @@ info: - "--full" env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -47,6 +48,21 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a10f823..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  -worktree-path = "../{{ repo }}.{{ branch }}" +  \ No newline at end of file +  +worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_full_version_check_unavailable.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_full_version_check_unavailable.snap index 4b87b07456..fb07f914a4 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_full_version_check_unavailable.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_full_version_check_unavailable.snap @@ -8,6 +8,7 @@ info: - "--full" env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -47,6 +48,21 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a10f823..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  -worktree-path = "../{{ repo }}.{{ branch }}" +  \ No newline at end of file +  +worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_gemini_available_extension_not_installed.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_gemini_available_extension_not_installed.snap index b40766f8c7..6daa52efd0 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_gemini_available_extension_not_installed.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_gemini_available_extension_not_installed.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_gemini_extension_installed.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_gemini_extension_installed.snap index 93b708a67f..4b9480488b 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_gemini_extension_installed.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_gemini_extension_installed.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_gemini_extension_invalid_manifest.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_gemini_extension_invalid_manifest.snap index b40766f8c7..6daa52efd0 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_gemini_extension_invalid_manifest.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_gemini_extension_invalid_manifest.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_github_remote.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_github_remote.snap index eb1d7b3680..e160c29742 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_github_remote.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_github_remote.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_gitlab_remote.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_gitlab_remote.snap index 6a711600f8..1db58d09b0 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_gitlab_remote.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_gitlab_remote.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_invalid_project_toml.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_invalid_project_toml.snap index bbf349a020..f42686bc00 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_invalid_project_toml.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_invalid_project_toml.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,21 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a10f823..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  -worktree-path = "../{{ repo }}.{{ branch }}" +  \ No newline at end of file +  +worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_no_project_config.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_no_project_config.snap index 5c17e82364..0b046f83e4 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_no_project_config.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_no_project_config.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_opencode_available_plugin_not_installed.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_opencode_available_plugin_not_installed.snap index 766c8a11dc..7b03de0b1f 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_opencode_available_plugin_not_installed.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_opencode_available_plugin_not_installed.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_opencode_plugin_installed.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_opencode_plugin_installed.snap index cd4c497e34..2429019383 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_opencode_plugin_installed.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_opencode_plugin_installed.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_opencode_plugin_outdated.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_opencode_plugin_outdated.snap index 25098ff2e3..a2252fbc62 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_opencode_plugin_outdated.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_opencode_plugin_outdated.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_outside_git_repo.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_outside_git_repo.snap index c95f9c5532..22d225930c 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_outside_git_repo.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_outside_git_repo.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" HOME: "[TEST_HOME]" @@ -40,6 +41,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_plugin_installed.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_plugin_installed.snap index 70da1e4098..aad627d20d 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_plugin_installed.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_plugin_installed.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_shell_active_but_not_in_config_file.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_shell_active_but_not_in_config_file.snap index 7a8a9fa55a..d5c7527701 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_shell_active_but_not_in_config_file.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_shell_active_but_not_in_config_file.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -47,6 +48,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_shell_integration_active.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_shell_integration_active.snap index 942915167c..99349e2e2a 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_shell_integration_active.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_shell_integration_active.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,6 +47,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_statusline_configured.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_statusline_configured.snap index 9b6089daff..d944ff1aa9 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_statusline_configured.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_statusline_configured.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_suggests_project_config_for_ci.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_suggests_project_config_for_ci.snap index 9667f6df50..47714e315b 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_suggests_project_config_for_ci.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_suggests_project_config_for_ci.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,13 +47,14 @@ exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml ▲ User config: [ci] is deprecated in favor of [forge] +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 ↳ To apply: wt config update ○ Proposed diff:   diff --git a/config.toml/current b/config.toml/migrated -  index 2e0e0bb..9d78d51 100644 +  index 2e0e0bb..9dc4e69 100644   --- a/config.toml/current   +++ b/config.toml/migrated -  @@ -1,4 +1,4 @@ +  @@ -1,4 +1,7 @@   worktree-path = "../{{ repo }}.{{ branch }}"      -[ci] @@ -60,6 +62,9 @@ exit_code: 0   \ No newline at end of file   +[forge]   +platform = "github" +  + +  +[list] +  +json-schema = 1 ▲ Key ci belongs in project config as [forge] ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_suggests_user_config_for_commit_generation.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_suggests_user_config_for_commit_generation.snap index 45fc9a51d3..cbebf5c5ae 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_suggests_user_config_for_commit_generation.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_suggests_user_config_for_commit_generation.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,21 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a10f823..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  -worktree-path = "../{{ repo }}.{{ branch }}" +  \ No newline at end of file +  +worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_warns_unknown_project_keys.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_warns_unknown_project_keys.snap index 7b5868448b..f7ad451b2e 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_warns_unknown_project_keys.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_warns_unknown_project_keys.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,21 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a10f823..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  -worktree-path = "../{{ repo }}.{{ branch }}" +  \ No newline at end of file +  +worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_warns_unknown_user_keys.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_warns_unknown_user_keys.snap index f98e4b5f04..8252064e8a 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_warns_unknown_user_keys.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_warns_unknown_user_keys.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,7 +46,25 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index 6019600..c468244 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1,4 +1,7 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +   +  [commit-gen] +  -command = "llm" +  \ No newline at end of file +  +command = "llm" +  + +  +[list] +  +json-schema = 1 ▲ Unknown key commit-gen will be ignored +   worktree-path = "../{{ repo }}.{{ branch }}"     [commit-gen] diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_whitespace_only_project_config.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_whitespace_only_project_config.snap index bf7ac811a0..aa812c6d06 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_whitespace_only_project_config.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_whitespace_only_project_config.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml 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 9754e83eff..faa5bbd0cb 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 @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,19 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" ↳ Optional system config not found @ /etc/xdg/worktrunk/config.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_with_system_config.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_with_system_config.snap index 385d7a4a80..2c300d4fe3 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_with_system_config.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_with_system_config.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -53,6 +54,19 @@ exit_code: 0   command = "company-llm-tool" USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +   worktree-path = "../{{ repo }}.{{ branch }}" PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_update_applies_approved_commands_migration.snap b/tests/snapshots/integration__integration_tests__config_show__config_update_applies_approved_commands_migration.snap index a2039c8af3..856e0b97c0 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_update_applies_approved_commands_migration.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_update_applies_approved_commands_migration.snap @@ -8,6 +8,7 @@ info: - "--yes" env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -48,15 +49,18 @@ exit_code: 0 ----- stderr ----- ▲ User config: approved-commands under [projects] is deprecated in favor of approvals.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 ○ Proposed diff:   diff --git a/test-config.toml/current b/test-config.toml/migrated -  index 51f8ab1..a30ca31 100644 +  index 51f8ab1..cea088e 100644   --- a/test-config.toml/current   +++ b/test-config.toml/migrated -  @@ -1,4 +1 @@ +  @@ -1,4 +1,4 @@   worktree-path = "../{{ repo }}.{{ branch }}" -  - +     -[projects."github.com/user/repo"]   -approved-commands = ["npm install", "npm test"] +  +[list] +  +json-schema = 1 ○ Copied approved commands to approvals.toml ✓ Updated user config diff --git a/tests/snapshots/integration__integration_tests__config_show__config_update_applies_commit_generation_migration.snap b/tests/snapshots/integration__integration_tests__config_show__config_update_applies_commit_generation_migration.snap index da55638498..c9b7737d14 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_update_applies_commit_generation_migration.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_update_applies_commit_generation_migration.snap @@ -8,6 +8,7 @@ info: - "--yes" env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -48,12 +49,13 @@ exit_code: 0 ----- stderr ----- ▲ User config: [commit-generation] is deprecated in favor of [commit.generation] +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 ○ Proposed diff:   diff --git a/test-config.toml/current b/test-config.toml/migrated -  index 40d0385..bc7273c 100644 +  index 40d0385..7f2d33c 100644   --- a/test-config.toml/current   +++ b/test-config.toml/migrated -  @@ -1,5 +1,4 @@ +  @@ -1,5 +1,7 @@   worktree-path = "../{{ repo }}.{{ branch }}"      -[commit-generation] @@ -61,4 +63,7 @@ exit_code: 0   -args = ["-m", "haiku"]   +[commit.generation]   +command = "llm -m haiku" +  + +  +[list] +  +json-schema = 1 ✓ Updated user config 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 3ac46efb2c..28830ee5b8 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 @@ -8,6 +8,7 @@ info: - "--yes" env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -50,14 +51,18 @@ exit_code: 0 ▲ User config: template variable repo_root is deprecated in favor of repo_path ▲ User config: template variable worktree is deprecated in favor of worktree_path ▲ User config: template variable main_worktree is deprecated in favor of repo +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 ○ Proposed diff:   diff --git a/test-config.toml/current b/test-config.toml/migrated -  index 7cc556b..b3535c5 100644 +  index 7cc556b..fbfd97d 100644   --- a/test-config.toml/current   +++ b/test-config.toml/migrated -  @@ -1,2 +1,2 @@ +  @@ -1,2 +1,5 @@   -worktree-path = "../{{ main_worktree }}.{{ branch }}"   -pre-start = "ln -sf {{ repo_root }}/node_modules {{ worktree }}/node_modules"   +worktree-path = "../{{ repo }}.{{ branch }}"   +pre-start = "ln -sf {{ repo_path }}/node_modules {{ worktree_path }}/node_modules" +  + +  +[list] +  +json-schema = 1 ✓ Updated user config diff --git a/tests/snapshots/integration__integration_tests__config_show__config_update_pins_json_schema.snap b/tests/snapshots/integration__integration_tests__config_show__config_update_pins_json_schema.snap new file mode 100644 index 0000000000..4527e543d7 --- /dev/null +++ b/tests/snapshots/integration__integration_tests__config_show__config_update_pins_json_schema.snap @@ -0,0 +1,62 @@ +--- +source: tests/integration_tests/config_show.rs +info: + program: wt + args: + - config + - update + - "--yes" + env: + APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" + CLICOLOR_FORCE: "1" + COLUMNS: "500" + GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" + GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z" + GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]" + GIT_CONFIG_SYSTEM: /dev/null + GIT_TERMINAL_PROMPT: "0" + HOME: "[TEST_HOME]" + LANG: C + LC_ALL: C + LLVM_PROFILE_FILE: "[LLVM_PROFILE_FILE]" + MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]" + OPENCODE_CONFIG_DIR: "[TEST_OPENCODE_CONFIG]" + PATH: "[PATH]" + TERM: alacritty + USERPROFILE: "[TEST_HOME]" + WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]" + WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]" + WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]" + WORKTRUNK_TEST_BASH_INSTALLED: "0" + WORKTRUNK_TEST_CLAUDE_INSTALLED: "0" + WORKTRUNK_TEST_CODEX_INSTALLED: "0" + WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1" + WORKTRUNK_TEST_EPOCH: "1735776000" + WORKTRUNK_TEST_FISH_INSTALLED: "0" + WORKTRUNK_TEST_GEMINI_INSTALLED: "0" + WORKTRUNK_TEST_NUSHELL_ENV: "0" + WORKTRUNK_TEST_OPENCODE_INSTALLED: "0" + WORKTRUNK_TEST_POWERSHELL_ENV: "0" + WORKTRUNK_TEST_POWERSHELL_INSTALLED: "0" + WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" + WORKTRUNK_TEST_ZSH_INSTALLED: "0" + XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]" +--- +success: true +exit_code: 0 +----- stdout ----- + +----- stderr ----- +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +○ Proposed diff: +  diff --git a/test-config.toml/current b/test-config.toml/migrated +  index a30ca31..cea088e 100644 +  --- a/test-config.toml/current +  +++ b/test-config.toml/migrated +  @@ -1 +1,4 @@ +  worktree-path = "../{{ repo }}.{{ branch }}" +  + +  +[list] +  +json-schema = 1 +✓ Updated user config 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 4c275aeb66..481a8d3e55 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 @@ -6,14 +6,17 @@ expression: "&output" ▲ User config: template variable main_worktree is deprecated in favor of repo ○ Proposed diff:   diff --git a/test-config.toml/current b/test-config.toml/migrated -  index dee4883..5eef8a6 100644 +  index 369f60a..e90e8fd 100644   --- a/test-config.toml/current   +++ b/test-config.toml/migrated -  @@ -1,2 +1,2 @@ +  @@ -1,5 +1,5 @@   -worktree-path = "../{{ main_worktree }}.{{ branch }}"   -pre-start = "ln -sf {{ repo_root }}/node_modules"   +worktree-path = "../{{ repo }}.{{ branch }}"   +pre-start = "ln -sf {{ repo_path }}/node_modules" +   +  [list] +  json-schema = 1 ❯ 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 b8bb8e69ec..c61e1c7afd 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 @@ -6,14 +6,17 @@ expression: "&output" ▲ User config: template variable main_worktree is deprecated in favor of repo ○ Proposed diff:   diff --git a/test-config.toml/current b/test-config.toml/migrated -  index dee4883..5eef8a6 100644 +  index 369f60a..e90e8fd 100644   --- a/test-config.toml/current   +++ b/test-config.toml/migrated -  @@ -1,2 +1,2 @@ +  @@ -1,5 +1,5 @@   -worktree-path = "../{{ main_worktree }}.{{ branch }}"   -pre-start = "ln -sf {{ repo_root }}/node_modules"   +worktree-path = "../{{ repo }}.{{ branch }}"   +pre-start = "ln -sf {{ repo_path }}/node_modules" +   +  [list] +  json-schema = 1 ❯ Apply updates? [y/N/?] n ○ Update cancelled diff --git a/tests/snapshots/integration__integration_tests__help__help_list_long.snap b/tests/snapshots/integration__integration_tests__help__help_list_long.snap index c59b0a7a2f..7057daa6ee 100644 --- a/tests/snapshots/integration__integration_tests__help__help_list_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_list_long.snap @@ -273,8 +273,9 @@ These appear across all columns while the table is loading: --format=json emits structured data in one of two schemas while the format migrates: [list] json-schema = 2 selects the envelope format below, = 1 -the original bare-array format. Unset emits schema 1 with a warning; a future -release flips the default to schema 2 and later removes schema 1. +the original bare-array format. Unset emits schema 1 with a warning +(wt config update pins = 1); a future release flips the default to +schema 2 and later removes schema 1. Schema 2 diff --git a/tests/snapshots/integration__integration_tests__help__help_list_narrow_80.snap b/tests/snapshots/integration__integration_tests__help__help_list_narrow_80.snap index 4858ef42e3..6d5bfc38d1 100644 --- a/tests/snapshots/integration__integration_tests__help__help_list_narrow_80.snap +++ b/tests/snapshots/integration__integration_tests__help__help_list_narrow_80.snap @@ -336,8 +336,9 @@ These appear across all columns while the table is loading: --format=json emits structured data in one of two schemas while the format migrates: [list] json-schema = 2 selects the envelope format below, = 1 -the original bare-array format. Unset emits schema 1 with a warning; a future -release flips the default to schema 2 and later removes schema 1. +the original bare-array format. Unset emits schema 1 with a warning +(wt config update pins = 1); a future release flips the default to +schema 2 and later removes schema 1. Schema 2 diff --git a/tests/snapshots/integration__integration_tests__list__list_json_tree_matches_main_after_merge.snap b/tests/snapshots/integration__integration_tests__list__list_json_tree_matches_main_after_merge.snap index 72f2b08363..e95922d88c 100644 --- a/tests/snapshots/integration__integration_tests__list__list_json_tree_matches_main_after_merge.snap +++ b/tests/snapshots/integration__integration_tests__list__list_json_tree_matches_main_after_merge.snap @@ -244,4 +244,4 @@ exit_code: 0 ----- stderr ----- ▲ JSON output is schema 1; a future release switches the default to schema 2 -↳ To keep this format set [list] json-schema = 1; to adopt the new one, json-schema = 2 +↳ To adopt the new schema set [list] json-schema = 2; to keep this format, json-schema = 1 diff --git a/tests/snapshots/integration__integration_tests__list__list_json_with_display_fields.snap b/tests/snapshots/integration__integration_tests__list__list_json_with_display_fields.snap index b48f03f7f5..42e7df7629 100644 --- a/tests/snapshots/integration__integration_tests__list__list_json_with_display_fields.snap +++ b/tests/snapshots/integration__integration_tests__list__list_json_with_display_fields.snap @@ -283,4 +283,4 @@ exit_code: 0 ----- stderr ----- ▲ JSON output is schema 1; a future release switches the default to schema 2 -↳ To keep this format set [list] json-schema = 1; to adopt the new one, json-schema = 2 +↳ To adopt the new schema set [list] json-schema = 2; to keep this format, json-schema = 1 diff --git a/tests/snapshots/integration__integration_tests__list__list_json_with_git_operation.snap b/tests/snapshots/integration__integration_tests__list__list_json_with_git_operation.snap index 4313d5886b..c9e4c22902 100644 --- a/tests/snapshots/integration__integration_tests__list__list_json_with_git_operation.snap +++ b/tests/snapshots/integration__integration_tests__list__list_json_with_git_operation.snap @@ -244,4 +244,4 @@ exit_code: 0 ----- stderr ----- ▲ JSON output is schema 1; a future release switches the default to schema 2 -↳ To keep this format set [list] json-schema = 1; to adopt the new one, json-schema = 2 +↳ To adopt the new schema set [list] json-schema = 2; to keep this format, json-schema = 1 diff --git a/tests/snapshots/integration__integration_tests__list__list_json_with_metadata.snap b/tests/snapshots/integration__integration_tests__list__list_json_with_metadata.snap index f7f1db5db8..1045af70d9 100644 --- a/tests/snapshots/integration__integration_tests__list__list_json_with_metadata.snap +++ b/tests/snapshots/integration__integration_tests__list__list_json_with_metadata.snap @@ -284,4 +284,4 @@ exit_code: 0 ----- stderr ----- ▲ JSON output is schema 1; a future release switches the default to schema 2 -↳ To keep this format set [list] json-schema = 1; to adopt the new one, json-schema = 2 +↳ To adopt the new schema set [list] json-schema = 2; to keep this format, json-schema = 1 diff --git a/tests/snapshots/integration__integration_tests__list__list_json_with_user_marker.snap b/tests/snapshots/integration__integration_tests__list__list_json_with_user_marker.snap index 629a3f1003..15bc79f99f 100644 --- a/tests/snapshots/integration__integration_tests__list__list_json_with_user_marker.snap +++ b/tests/snapshots/integration__integration_tests__list__list_json_with_user_marker.snap @@ -282,4 +282,4 @@ exit_code: 0 ----- stderr ----- ▲ JSON output is schema 1; a future release switches the default to schema 2 -↳ To keep this format set [list] json-schema = 1; to adopt the new one, json-schema = 2 +↳ To adopt the new schema set [list] json-schema = 2; to keep this format, json-schema = 1 From 9d904aec434fef97b76691eee26a9d7e8ef9ecc7 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Fri, 10 Jul 2026 01:05:41 -0700 Subject: [PATCH 3/3] fix(config): review fixes for the json-schema pin - Gate the pin on the system config layer not defining json-schema: a user-file pin would override a system-level = 2 and flip resolved JSON output, the opposite of behavior preservation. - Decide the nag hint by the same detection update runs, so a missing, unreadable, or malformed user config falls back to the manual-setting hint instead of offering an update that would no-op. - Skip the load-path dedup registry when an info carries only pins. - Render the pending pin in wt config show for empty config files too (update would rewrite them), and mirror the user/project renderers. - Collapse pin_json_schema_doc to one match; dedup the hint prefix; warn_unknown_fields takes ConfigFileKind instead of a label string. - Pin the PendingDefault-rule/kind coupling and the implicit-[list] promotion with tests; add a system-config integration test. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 2 +- src/commands/config/show.rs | 26 +++- src/commands/list/mod.rs | 38 +++-- src/config/deprecation.rs | 135 +++++++++++++----- src/config/project.rs | 2 +- src/config/user/mod.rs | 4 +- tests/integration_tests/config_show.rs | 26 ++++ ...how_bash_matched_exe_emits_alias_hint.snap | 11 ++ ...show__config_show_empty_system_config.snap | 11 ++ ...ow__config_show_fish_outdated_wrapper.snap | 11 ++ ...ow__config_show_fish_with_completions.snap | 11 ++ ..._config_show_fish_without_completions.snap | 11 ++ ...ow__config_show_invalid_system_config.snap | 11 ++ ..._config_show_nushell_outdated_wrapper.snap | 11 ++ ..._show_partial_shell_config_shows_hint.snap | 11 ++ ..._powershell_detected_via_psmodulepath.snap | 11 ++ ...ig_show_skipped_with_installed_binary.snap | 11 ++ ...atched_candidate_exe_emits_extra_hint.snap | 11 ++ ...d_candidate_not_suppressed_by_wrapper.snap | 11 ++ ...nfig_show_unmatched_candidate_warning.snap | 11 ++ ...onfig_show_zsh_compinit_correct_order.snap | 11 ++ ...how__config_show_zsh_compinit_warning.snap | 11 ++ 22 files changed, 341 insertions(+), 57 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5bb716dc93..4b4e5a5d16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -173,7 +173,7 @@ Check `Cargo.toml` before hand-rolling a utility: All config deprecation lives in one layer: pre-deserialization TOML migration in `src/config/deprecation.rs`. `migrate_content()` rewrites deprecated patterns into canonical form before serde parses; `check_and_migrate()` reuses it, and additionally detects patterns and emits per-process-deduped warnings (the user materializes migrations via `wt config update`). **Never silently drop an old config key** — that's a silent behavior change for users; migrate it. -Every deprecation is one row in the `DEPRECATION_RULES` table: a single idempotent function that rewrites the pattern AND returns the `DeprecationKind`s for what it changed — there is no separate detection function, so detection and migration share one predicate and cannot drift. Detection runs the same functions against a scratch copy of the document (progressively, so a rule sees earlier rules' rewrites); the invariant for warning rules is **a warning fires exactly when `wt config update` would change the file**, pinned by `test_warning_fires_iff_update_changes` — add new edge cases to its battery. The row variant decides when the rewrite applies: `Structural` rewrites on every load; `UpdateOnly` only via `wt config update`, for deprecated forms that still work at runtime; `Silent` rewrites on every load with no warning — its function signature has no channel for a kind, which is what scopes the invariant to the warning variants; `PendingDefault` pins a default a future release switches (currently `[list] json-schema = 1`) — update-pass only, scoped to the config kind that owns the key, and its kind warns at the surface that reads the setting (the `wt list` JSON nag) instead of at config load, so the iff invariant holds with the warning on that surface. Table order is both the warning-emission order and the migration order. Each `DeprecationKind` carries its own display payload, so `format_deprecation_warnings()` is one match over the kinds. A config that can't be rewritten safely (a malformed value, an occupied destination key) is left untouched and unwarned — serde's type or unknown-field error is the messaging; an empty deprecated section is also left alone, with no message at all (it contributes no config). Adding a deprecation: (1) one idempotent migrate-and-report function; (2) a `DeprecationKind` variant plus its match arm in `format_deprecation_warnings()`; (3) a `DEPRECATION_RULES` row; (4) for a removed top-level section, add a `DeprecatedSection` to `DEPRECATED_SECTION_KEYS` (canonical key plus display form) so `warn_unknown_fields` defers to the deprecation messaging and suggests the correct config file. A silently-migrated rename (e.g. `pre-create` → `pre-start`) is a `Silent` row with no variant. Renaming a field within a section follows the same shape via a TOML-level rename function (see `migrate_negated_bool`); the struct never needs the old field since migration precedes serde. +Every deprecation is one row in the `DEPRECATION_RULES` table: a single idempotent function that rewrites the pattern AND returns the `DeprecationKind`s for what it changed — there is no separate detection function, so detection and migration share one predicate and cannot drift. Detection runs the same functions against a scratch copy of the document (progressively, so a rule sees earlier rules' rewrites); the invariant for warning rules is **a warning fires exactly when `wt config update` would change the file**, pinned by `test_warning_fires_iff_update_changes` — add new edge cases to its battery. The row variant decides when the rewrite applies: `Structural` rewrites on every load; `UpdateOnly` only via `wt config update`, for deprecated forms that still work at runtime; `Silent` rewrites on every load with no warning — its function signature has no channel for a kind, which is what scopes the invariant to `Structural` and `UpdateOnly`; `PendingDefault` pins a default a future release switches (currently `[list] json-schema = 1`, inert while the system config layer defines the key) — update-pass only, scoped to the config kind that owns the key, and excluded from load warnings by `is_pending_default`: it satisfies the same iff at the surface that reads the setting, where the `wt list` JSON nag fires exactly when update would pin. Table order is both the warning-emission order and the migration order. Each `DeprecationKind` carries its own display payload, so `format_deprecation_warnings()` is one match over the kinds. A config that can't be rewritten safely (a malformed value, an occupied destination key) is left untouched and unwarned — serde's type or unknown-field error is the messaging; an empty deprecated section is also left alone, with no message at all (it contributes no config). Adding a deprecation: (1) one idempotent migrate-and-report function; (2) a `DeprecationKind` variant plus its match arm in `format_deprecation_warnings()`; (3) a `DEPRECATION_RULES` row; (4) for a removed top-level section, add a `DeprecatedSection` to `DEPRECATED_SECTION_KEYS` (canonical key plus display form) so `warn_unknown_fields` defers to the deprecation messaging and suggests the correct config file. A silently-migrated rename (e.g. `pre-create` → `pre-start`) is a `Silent` row with no variant. Renaming a field within a section follows the same shape via a TOML-level rename function (see `migrate_negated_bool`); the struct never needs the old field since migration precedes serde. ## Adding CLI Commands diff --git a/src/commands/config/show.rs b/src/commands/config/show.rs index 935c11b5b8..9c9637052a 100644 --- a/src/commands/config/show.rs +++ b/src/commands/config/show.rs @@ -664,15 +664,12 @@ fn render_user_config(out: &mut String, has_system_config: bool) -> anyhow::Resu // Read and display the file contents let contents = std::fs::read_to_string(&config_path).context("Failed to read config file")?; - if contents.trim().is_empty() { - writeln!(out, "{}", hint_message("Empty file (using defaults)"))?; - return Ok(()); - } - // Check for deprecations with emit_inline_warnings=false (silent mode) // User config is global, not tied to any repository // Deprecated patterns supersede the TOML dump below (their diff covers - // the file); a pending-default pin is additive, so the dump stays. + // the file); a pending-default pin is additive, so the dump stays. An + // empty file still gets the pending-pin details — `wt config update` + // would rewrite it — just no dump. let mut details_shown = false; let skip_dump = match worktrunk::config::check_and_migrate( &config_path, @@ -699,6 +696,11 @@ fn render_user_config(out: &mut String, has_system_config: bool) -> anyhow::Resu } }; + if contents.trim().is_empty() { + writeln!(out, "{}", hint_message("Empty file (using defaults)"))?; + return Ok(()); + } + // Validate config (syntax + schema) and warn if invalid if let Err(e) = toml::from_str::(&contents) { // Use gutter for error details to avoid markup interpretation of user content @@ -843,8 +845,13 @@ fn render_project_config(out: &mut String) -> anyhow::Result<()> { } // Check for deprecations with emit_inline_warnings=false (silent mode) - // Only write migration file in main worktree, not linked worktrees + // Only write migration file in main worktree, not linked worktrees. + // Deprecated patterns supersede the TOML dump below (their diff covers + // the file); a pending-default pin would be additive, so the dump stays — + // no pending-default rule targets project config today, but the shape + // mirrors render_user_config so the two stay interchangeable. let is_main_worktree = !repo.current_worktree().is_linked().unwrap_or(true); + let mut details_shown = false; let skip_dump = match worktrunk::config::check_and_migrate( &config_path, &contents, @@ -858,6 +865,7 @@ fn render_project_config(out: &mut String) -> anyhow::Result<()> { out.push_str(&worktrunk::config::format_deprecation_details( &info, &contents, )); + details_shown = true; info.has_deprecated_patterns() } else { false @@ -881,6 +889,10 @@ fn render_project_config(out: &mut String) -> anyhow::Result<()> { // Display TOML with syntax highlighting (gutter at column 0). // Skip when deprecations were shown — the proposed diff already covers it. if !skip_dump { + if details_shown { + // Pending-pin details above end in their diff; separate phases. + out.push('\n'); + } writeln!(out, "{}", format_toml(&contents))?; } diff --git a/src/commands/list/mod.rs b/src/commands/list/mod.rs index 1a24cc5416..593edac85f 100644 --- a/src/commands/list/mod.rs +++ b/src/commands/list/mod.rs @@ -212,10 +212,11 @@ pub(crate) fn print_json(value: &T) -> anyhow::Result<()> { /// matters to JSON consumers, so a load-time warning would nag every command /// for every user without the key. `wt config update` pins the current /// `json-schema = 1` (the behavior-preserving choice; adopting schema 2 is a -/// deliberate manual edit), so the nag's hint offers that command when a user -/// config file exists to update — with no user config there is nothing for -/// update to rewrite, and the hint names both settings instead (migration -/// plan in design/list-json-v2.md, reviewed in #3357). +/// deliberate manual edit), so the nag's hint offers that command exactly +/// when running it would write the pin — decided by the same detection +/// update runs, so a missing, unreadable, or malformed user config falls +/// back to naming the manual setting instead (migration plan in +/// design/list-json-v2.md, reviewed in #3357). pub(crate) fn resolve_json_schema(repo: &Repository) -> u8 { use std::sync::Once; @@ -250,18 +251,27 @@ pub(crate) fn resolve_json_schema(repo: &Repository) -> u8 { "JSON output is schema 1; a future release switches the default to schema 2" ) ); - let has_user_config = - worktrunk::config::config_path().is_some_and(|p| p.exists()); - let hint = if has_user_config { - cformat!( - "To adopt the new schema set [list] json-schema = 2; to keep this format, run wt config update" - ) + let update_would_pin = worktrunk::config::config_path() + .and_then(|p| std::fs::read_to_string(p).ok()) + .is_some_and(|content| { + worktrunk::config::detect_deprecations( + &content, + worktrunk::config::ConfigFileKind::User, + ) + .iter() + .any(|k| matches!(k, worktrunk::config::DeprecationKind::JsonSchemaUnset)) + }); + let keep = if update_would_pin { + cformat!("run wt config update") } else { - cformat!( - "To adopt the new schema set [list] json-schema = 2; to keep this format, json-schema = 1" - ) + cformat!("json-schema = 1") }; - eprintln!("{}", hint_message(hint)); + eprintln!( + "{}", + hint_message(cformat!( + "To adopt the new schema set [list] json-schema = 2; to keep this format, {keep}" + )) + ); }); 1 } diff --git a/src/config/deprecation.rs b/src/config/deprecation.rs index e52d6ab292..d937f40e6b 100644 --- a/src/config/deprecation.rs +++ b/src/config/deprecation.rs @@ -660,33 +660,59 @@ const DEPRECATION_RULES: &[DeprecationRule] = &[ /// /// A `list` slot occupied by a non-table is left alone (serde's type error is /// the messaging); a present key of any value is the user's explicit choice, -/// including out-of-range values, which already warn at resolve time. +/// including out-of-range values, which already warn at resolve time. When +/// the system config layer defines the key, resolution is already explicit +/// and a user-file pin would *override* the system value rather than +/// preserve behavior, so the rule stays inert — the one rule that reads a +/// second file (detection stays write-free; the common no-system-config case +/// costs one stat). fn pin_json_schema_doc(doc: &mut toml_edit::DocumentMut) -> Deprecations { - match doc.get("list") { - None => {} - Some(toml_edit::Item::Table(t)) if !t.contains_key("json-schema") => {} - Some(toml_edit::Item::Value(toml_edit::Value::InlineTable(t))) - if !t.contains_key("json-schema") => {} - // A non-table occupant (serde's type error is the messaging) or a - // present key of any value — the user's explicit choice, including - // out-of-range values, which already warn at resolve time. - _ => return Vec::new(), + if system_config_defines_json_schema() { + return Vec::new(); } + // `or_insert` only fills a vacant slot, so a bail through the fallthrough + // arm (scalar occupant, key already present) leaves the document + // unmodified; an absent `list` becomes the empty table the first arm + // then fills. match doc .entry("list") .or_insert(toml_edit::Item::Table(toml_edit::Table::new())) { - toml_edit::Item::Table(t) => { + toml_edit::Item::Table(t) if !t.contains_key("json-schema") => { t.insert("json-schema", toml_edit::value(1)); } - toml_edit::Item::Value(toml_edit::Value::InlineTable(t)) => { + toml_edit::Item::Value(toml_edit::Value::InlineTable(t)) + if !t.contains_key("json-schema") => + { t.insert("json-schema", 1.into()); } - _ => unreachable!("the match above verified the slot is absent or a table"), + _ => return Vec::new(), } vec![DeprecationKind::JsonSchemaUnset] } +/// Whether the system config layer sets `[list] json-schema` (any value, any +/// table shape). Unreadable or unparsable system config counts as not +/// defining it. +fn system_config_defines_json_schema() -> bool { + let Some(path) = crate::config::system_config_path() else { + return false; + }; + let Ok(content) = std::fs::read_to_string(path) else { + return false; + }; + let Ok(doc) = content.parse::() else { + return false; + }; + match doc.get("list") { + Some(toml_edit::Item::Table(t)) => t.contains_key("json-schema"), + Some(toml_edit::Item::Value(toml_edit::Value::InlineTable(t))) => { + t.contains_key("json-schema") + } + _ => false, + } +} + /// Detect deprecations in config content. Pure function, no I/O. /// /// Returns the detected deprecation patterns. This is the recommended entry @@ -1435,6 +1461,18 @@ pub fn check_and_migrate( }); } + // Pending-default kinds warn at their own usage surface instead of at + // load, so an info carrying only pins (most user configs until the + // json-schema default flips) skips the dedup registry and emission + // entirely — no canonicalize + lock on every load for a config with + // nothing to say here. + if !info.has_deprecated_patterns() { + return Ok(CheckAndMigrateResult { + info: Some(info), + migrated_content, + }); + } + // Deduplicate warnings per path per process let canonical_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); { @@ -1452,27 +1490,23 @@ pub fn check_and_migrate( // For non-config-show commands, emit per-kind warnings but skip the diff. // The diff is reserved for `wt config show`, where the user has opted into - // details. Pending-default kinds warn at their own usage surface instead - // of at load, so they are filtered out here, and the hint is gated on the - // remaining text being non-empty so a config with nothing to say at load - // produces no stray "run wt config show" line with nothing above it. + // details. Pending-default kinds still ride along in a mixed info, so + // they are filtered out of the emitted lines here. if emit_inline_warnings && !warnings_suppressed() { let warnings = format_warning_lines( info.deprecations.iter().filter(|k| !k.is_pending_default()), info.label(), ); - if !warnings.is_empty() { - eprint!("{warnings}"); - if DEPRECATION_HINT_EMITTED.set(()).is_ok() { - eprintln!( - "{}", - hint_message(cformat!( - "To see details, run wt config show; to apply updates, run wt config update" - )) - ); - } - std::io::stderr().flush().ok(); + eprint!("{warnings}"); + if DEPRECATION_HINT_EMITTED.set(()).is_ok() { + eprintln!( + "{}", + hint_message(cformat!( + "To see details, run wt config show; to apply updates, run wt config update" + )) + ); } + std::io::stderr().flush().ok(); } Ok(CheckAndMigrateResult { @@ -1792,9 +1826,13 @@ pub fn classify_unknown_key(key: &str) -> UnknownKeyKind { /// with `config show` via [`collect_unknown_warnings`](crate::config::collect_unknown_warnings); /// this wrapper adds per-path deduplication and stderr emission. /// -/// The `label` is used in the warning message (e.g., "User config" or -/// "Project config"). -pub fn warn_unknown_fields(raw_contents: &str, path: &Path, label: &str) { +/// `kind` derives the label shown in the warning message. +pub fn warn_unknown_fields( + raw_contents: &str, + path: &Path, + kind: ConfigFileKind, +) { + let label = kind.label(); if warnings_suppressed() { return; } @@ -3256,6 +3294,39 @@ json-schema = 1 let migrated = compute_migrated_content("list = { columns = [\"ci\"] }\n", ConfigFileKind::User); insta::assert_snapshot!(migrated, @r#"list = { columns = ["ci"] , json-schema = 1 }"#); + + // A `list` that exists only implicitly (via a subtable) hosts the pin + // too; toml_edit renders the now-explicit [list] header ahead of the + // subtable. + let migrated = compute_migrated_content( + "[list.custom-columns]\nflag = \"echo hi\"\n", + ConfigFileKind::User, + ); + insta::assert_snapshot!(migrated, @r#" + [list] + json-schema = 1 + [list.custom-columns] + flag = "echo hi" + "#); + } + + /// Every `PendingDefault` row's kinds must return true from + /// `is_pending_default` — that flag is what keeps their warning off the + /// load surface and out of `has_deprecated_patterns`. A row emitting a + /// non-pending kind would leak a load warning and suppress the + /// `wt config show` dump. + #[test] + fn test_pending_default_rules_emit_pending_kinds() { + for rule in DEPRECATION_RULES { + if let DeprecationRule::PendingDefault { migrate, .. } = rule { + let mut doc = toml_edit::DocumentMut::new(); + let kinds = migrate(&mut doc); + assert!(!kinds.is_empty(), "pending rule inert on an empty doc"); + for kind in kinds { + assert!(kind.is_pending_default(), "{kind:?}"); + } + } + } } /// The pin is scoped to user config — project config doesn't own the key, @@ -4413,7 +4484,7 @@ ff = true warn_unknown_fields::( "[commit-generation]\ncommand = \"llm\"\n", &path, - "Project config", + ConfigFileKind::Project, ); } diff --git a/src/config/project.rs b/src/config/project.rs index b1e3e253c2..df3d31d61c 100644 --- a/src/config/project.rs +++ b/src/config/project.rs @@ -290,7 +290,7 @@ impl ProjectConfig { super::deprecation::warn_unknown_fields::( &contents, &config_path, - "Project config", + super::ConfigFileKind::Project, ); } diff --git a/src/config/user/mod.rs b/src/config/user/mod.rs index d8ae6e9d50..04e5c6f89f 100644 --- a/src/config/user/mod.rs +++ b/src/config/user/mod.rs @@ -458,7 +458,7 @@ impl UserConfig { super::deprecation::warn_unknown_fields::( &content, &system_path, - "System config", + super::ConfigFileKind::System, ); match load_config_file(&system_path, &result.migrated_content, "System config") @@ -491,7 +491,7 @@ impl UserConfig { super::deprecation::warn_unknown_fields::( &content, config_path, - "User config", + super::ConfigFileKind::User, ); match load_config_file(config_path, &result.migrated_content, "User config") diff --git a/tests/integration_tests/config_show.rs b/tests/integration_tests/config_show.rs index 4281ae410f..a978702eaf 100644 --- a/tests/integration_tests/config_show.rs +++ b/tests/integration_tests/config_show.rs @@ -3382,6 +3382,32 @@ fn test_config_update_pins_json_schema(repo: TestRepo) { ); } +/// A system config that sets `[list] json-schema` makes the resolved value +/// explicit, so `wt config update` must not pin the user file — a user-file +/// pin would override the system value and flip JSON output. +#[rstest] +fn test_config_update_json_schema_pin_defers_to_system_config(repo: TestRepo) { + let system_config_dir = tempfile::tempdir().unwrap(); + let system_config_path = system_config_dir.path().join("config.toml"); + fs::write(&system_config_path, "[list]\njson-schema = 2\n").unwrap(); + + // A user config with an unrelated deprecation: update applies that + // rewrite but must not insert the pin alongside it. + fs::write(repo.test_config_path(), "[merge]\nno-ff = true\n").unwrap(); + + let mut cmd = repo.wt_command(); + cmd.args(["config", "update", "--yes"]); + cmd.env("WORKTRUNK_SYSTEM_CONFIG_PATH", &system_config_path); + let output = cmd.output().unwrap(); + assert!(output.status.success()); + + assert_eq!( + fs::read_to_string(repo.test_config_path()).unwrap(), + "[merge]\nff = false\n", + "the no-ff rewrite applies; the json-schema pin must not" + ); +} + /// `wt config update --yes` applies template variable migration #[rstest] fn test_config_update_applies_template_var_migration(repo: TestRepo) { diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_bash_matched_exe_emits_alias_hint.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_bash_matched_exe_emits_alias_hint.snap index 4f284d8673..37c8c8648d 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_bash_matched_exe_emits_alias_hint.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_bash_matched_exe_emits_alias_hint.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -47,6 +48,16 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_empty_system_config.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_empty_system_config.snap index 8bdab243b9..30a16100eb 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_empty_system_config.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_empty_system_config.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -48,6 +49,16 @@ exit_code: 0 ↳ Empty file (no system defaults) USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_fish_outdated_wrapper.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_fish_outdated_wrapper.snap index e809e59b83..953f87242f 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_fish_outdated_wrapper.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_fish_outdated_wrapper.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,16 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_fish_with_completions.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_fish_with_completions.snap index b0aca90404..fb24c49ba3 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_fish_with_completions.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_fish_with_completions.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,16 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_fish_without_completions.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_fish_without_completions.snap index f310c8f286..241d551505 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_fish_without_completions.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_fish_without_completions.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -45,6 +46,16 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_invalid_system_config.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_invalid_system_config.snap index d172eab94a..b043691cbb 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_invalid_system_config.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_invalid_system_config.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -54,6 +55,16 @@ exit_code: 0   invalid = [toml USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_nushell_outdated_wrapper.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_nushell_outdated_wrapper.snap index 3cb80ec37b..8a827db33f 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_nushell_outdated_wrapper.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_nushell_outdated_wrapper.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,6 +47,16 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_partial_shell_config_shows_hint.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_partial_shell_config_shows_hint.snap index bcdeaf6560..96e9ad8034 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_partial_shell_config_shows_hint.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_partial_shell_config_shows_hint.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -47,6 +48,16 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_powershell_detected_via_psmodulepath.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_powershell_detected_via_psmodulepath.snap index 1f0234d90a..4d81c3231f 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_powershell_detected_via_psmodulepath.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_powershell_detected_via_psmodulepath.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,6 +47,16 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_skipped_with_installed_binary.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_skipped_with_installed_binary.snap index 30056d7e0f..61c0f9a003 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_skipped_with_installed_binary.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_skipped_with_installed_binary.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,6 +47,16 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_unmatched_candidate_exe_emits_extra_hint.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_unmatched_candidate_exe_emits_extra_hint.snap index 39bc084161..5fd818fc97 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_unmatched_candidate_exe_emits_extra_hint.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_unmatched_candidate_exe_emits_extra_hint.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,6 +47,16 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_unmatched_candidate_not_suppressed_by_wrapper.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_unmatched_candidate_not_suppressed_by_wrapper.snap index eb93b1c083..928ee4c94f 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_unmatched_candidate_not_suppressed_by_wrapper.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_unmatched_candidate_not_suppressed_by_wrapper.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,6 +47,16 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_unmatched_candidate_warning.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_unmatched_candidate_warning.snap index 4ed887bf3f..6dcab69c4d 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_unmatched_candidate_warning.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_unmatched_candidate_warning.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,6 +47,16 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_zsh_compinit_correct_order.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_zsh_compinit_correct_order.snap index fb4f448deb..7c9bd7cdb1 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_zsh_compinit_correct_order.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_zsh_compinit_correct_order.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,6 +47,16 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml diff --git a/tests/snapshots/integration__integration_tests__config_show__config_show_zsh_compinit_warning.snap b/tests/snapshots/integration__integration_tests__config_show__config_show_zsh_compinit_warning.snap index 1cbc93ab86..8ec59e6084 100644 --- a/tests/snapshots/integration__integration_tests__config_show__config_show_zsh_compinit_warning.snap +++ b/tests/snapshots/integration__integration_tests__config_show__config_show_zsh_compinit_warning.snap @@ -7,6 +7,7 @@ info: - show env: APPDATA: "[TEST_CONFIG_HOME]" + CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]" CLICOLOR_FORCE: "1" COLUMNS: "500" GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" @@ -46,6 +47,16 @@ success: true exit_code: 0 ----- stdout ----- USER CONFIG @ ~/.config/worktrunk/config.toml +▲ User config: [list] json-schema is unset; a future release switches the JSON default to schema 2 +↳ To apply: wt config update +○ Proposed diff: +  diff --git a/config.toml/current b/config.toml/migrated +  index e69de29..83a9aa1 100644 +  --- a/config.toml/current +  +++ b/config.toml/migrated +  @@ -0,0 +1,2 @@ +  +[list] +  +json-schema = 1 ↳ Empty file (using defaults) PROJECT CONFIG @ _REPO_/.config/wt.toml