From 7ed94a5c491a8177bcc7073f8549233c81893dc3 Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 00:23:48 +0800 Subject: [PATCH 01/15] chore: normalize docs and add Makefile formatting markdown targets --- AGENTS.md | 38 ++++++++---- Makefile | 23 +++++++- README.md | 58 ++++++++++--------- ...1-12-cleanup-behavior-and-branch-output.md | 3 + docs/plans/jobs/2026-01-12-color-output.md | 2 + ...01-12-create-existing-worktree-and-path.md | 1 + .../2026-01-12-create-output-and-clipboard.md | 2 + .../jobs/2026-01-12-create-raw-output.md | 2 + .../2026-01-12-disable-default-completion.md | 3 + .../jobs/2026-01-12-init-implementation.md | 4 ++ docs/plans/jobs/2026-01-12-themes-and-rwd.md | 4 ++ .../jobs/2026-01-13-align-output-flags.md | 3 + .../2026-01-13-create-auto-detect-branch.md | 2 + ...6-01-13-deprecate-copy-cd-and-task-flag.md | 7 ++- .../2026-01-13-implement-gwtt-workflow.md | 17 +++++- .../plans/jobs/2026-01-13-lint-and-cleanup.md | 2 + .../jobs/2026-01-13-list-status-search.md | 3 + ...026-01-13-readme-usage-guide-refinement.md | 13 +++-- ...2026-01-13-remove-copy-cd-and-list-task.md | 3 + .../jobs/2026-01-13-theme-config-and-env.md | 3 + .../2026-01-13-verify-root-command-config.md | 3 +- .../2026-01-13-worktree-raw-fallback-impl.md | 2 + .../2026-01-19-add-man-page-generation.md | 3 + .../2026-01-19-cleanup-skip-main-worktree.md | 2 + .../2026-01-19-consolidate-normalize-path.md | 2 +- ...-19-create-skip-existing-branch-message.md | 2 + .../2026-01-19-dynamic-short-hash-length.md | 12 ++-- .../jobs/2026-01-19-empty-history-handling.md | 3 + ...-19-fix-cleanup-yes-worktree-resolution.md | 3 + .../2026-01-19-fix-goreleaser-previous-tag.md | 3 + .../jobs/2026-01-19-integration-tests-ci.md | 3 + ...26-01-19-update-go-install-man-fallback.md | 3 + ...026-01-21-release-workflow-improvements.md | 3 + .../jobs/2026-01-26-cli-context-and-status.md | 4 ++ .../jobs/2026-01-27-config-grid-fallback.md | 2 + .../2026-01-27-config-refactor-grid-flags.md | 2 + .../2026-01-27-extensible-config-schema.md | 3 + ...1-27-fix-project-config-root-resolution.md | 3 + .../jobs/2026-01-27-phase2-config-loader.md | 3 + docs/plans/jobs/2026-01-27-phase3-docs.md | 3 + docs/plans/jobs/2026-01-27-phase3-tests.md | 3 + .../jobs/2026-01-27-project-root-tests.md | 22 +++---- .../2026-02-04-codex-apply-terminology.md | 3 + ...-02-04-fix-codex-opaque-id-and-raw-path.md | 4 +- .../jobs/2026-02-04-fix-codex-raw-paths.md | 3 +- ...026-02-04-fix-codex-raw-relative-to-cwd.md | 2 +- ...026-02-04-fuzzy-first-match-list-status.md | 2 +- .../2026-02-04-implement-mode-flag-phase2.md | 4 +- docs/plans/jobs/2026-02-04-phase-3-tests.md | 1 + .../2026-02-04-skill-location-refactor.md | 4 ++ ...-12-cleanup-modes-and-branch-visibility.md | 5 ++ docs/plans/plan-2026-01-12-color-output.md | 2 + docs/plans/plan-2026-01-12-init-phase.md | 9 +++ .../plan-2026-01-12-path-display-readme.md | 6 ++ docs/plans/plan-2026-01-12-themes-and-rwd.md | 4 ++ ...-2026-01-13-build-and-distribution-flow.md | 18 +++++- .../plan-2026-01-13-cicd-release-flow.md | 5 ++ docs/plans/plan-2026-01-13-list-copy-flag.md | 6 ++ .../plan-2026-01-13-theme-config-and-env.md | 6 ++ docs/plans/plan-2026-01-13-themes-flag.md | 2 + .../plan-2026-01-13-worktree-raw-fallback.md | 19 ++++-- .../plan-2026-01-18-open-source-readiness.md | 5 ++ ...026-01-21-release-workflow-improvements.md | 6 ++ ...an-2026-01-26-go-cli-context-and-status.md | 18 ++++-- ...an-2026-01-26-install-script-and-readme.md | 10 +++- .../plan-2026-01-27-extensible-config.md | 12 ++++ ...1-27-fix-project-config-root-resolution.md | 4 ++ .../plan-2026-02-04-mode-classic-and-codex.md | 12 ++++ docs/research-2026-01-12-themes-and-table.md | 6 ++ ...research-2026-01-12-worktree-ops-matrix.md | 11 ++++ ...esearch-2026-01-13-homebrew-integration.md | 12 ++++ ...research-2026-01-13-toml-config-options.md | 15 +++++ ...research-2026-01-19-create-default-base.md | 7 +++ docs/research-2026-01-27-extensible-config.md | 16 +++++ ...search-2026-02-04-mode-classic-vs-codex.md | 30 +++++++++- docs/schemas/config-gwtt.md | 17 ++++++ 76 files changed, 483 insertions(+), 85 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c4f0cc7..742de0c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,30 +27,35 @@ Use the following skill for this repository. Skills are defined under `.agents/s All meaningful agent work SHOULD be documented. -Optional metadata: -- If you update an existing plan, research doc, or job record, you MAY add a `modified-date: YYYY-MM-DD` field to the front-matter. -- Keep the original `date` value unchanged; `modified-date` is for the latest update. +### Date Policy -### Timezone - -- Unless a document or request specifies otherwise, record dates in UTC. +- Use `created-date` for when the document first begins. +- Use `modified-date` only when a later update is made. +- Keep `created-date` unchanged after initial creation. +- All dates are UTC calendar dates in `YYYY-MM-DD`. +- Do not include time-of-day or timezone suffix in front-matter date fields. +- When local and UTC dates differ, use the UTC date. ### Plan Documents Location: + ```text docs/plans/plan-YYYY-MM-DD-.md ``` Notes: + - Do not create or edit `docs/plan.md`. -- Use the date for when the plan is created and a short, kebab-case title. +- Use the creation date and a short, kebab-case title. Front-matter format: + ```yaml --- title: "" -date: YYYY-MM-DD +created-date: YYYY-MM-DD +modified-date: YYYY-MM-DD # optional status: draft | active | completed agent: --- @@ -63,26 +68,31 @@ agent: Use research docs for exploratory work that is not yet ready for a plan but may inform one. Location: + ```text docs/research-YYYY-MM-DD-.md ``` Notes: -- Use the date the research starts and a short, kebab-case title. + +- Use the creation date and a short, kebab-case title. - Keep scope focused on a single topic or question. - If research becomes actionable, create a plan doc and link to it. Front-matter format: + ```yaml --- title: "" -date: YYYY-MM-DD +created-date: YYYY-MM-DD +modified-date: YYYY-MM-DD # optional status: draft | in-progress | completed agent: --- ``` Suggested sections: + - Goal - Key Findings - Implications or Recommendations @@ -90,6 +100,7 @@ Suggested sections: - References (use footnote-style links) Traceability: + - Research docs should include a short "Related Plans" section when applicable, with links to plan docs. - Plan docs should include a short "Related Research" section when applicable, with links to research docs. - Use those exact section titles for consistency. @@ -102,15 +113,18 @@ Traceability: For concrete tasks or implementations, create a job record. Location: + ```text docs/plans/jobs/YYYY-MM-DD-.md ``` Front-matter format: + ```yaml --- title: "" -date: YYYY-MM-DD +created-date: YYYY-MM-DD +modified-date: YYYY-MM-DD # optional status: draft | in-progress | completed | blocked agent: --- @@ -131,7 +145,7 @@ agent: ## Writing Guidelines - Prefer clarity over verbosity -- Record *what changed* and *why* +- Record _what changed_ and _why_ - Avoid repeating information already in other documents - Assume future agents will read this without prior context diff --git a/Makefile b/Makefile index ac98085..3646460 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ -.PHONY: help build man go-build install uninstall go-install go-uninstall clean +.PHONY: help build man go-build install uninstall go-install go-uninstall markdown-fmt markdown-fmt-check clean # Build directory DIST_DIR := dist +MD_FILES := $(shell git ls-files '*.md' | rg -v '^\.agents/skills/' || true) # Default target help: @@ -22,6 +23,8 @@ help: @echo "" @echo "Development:" @echo " make help Show this help message" + @echo " make markdown-fmt Format Markdown files with oxfmt" + @echo " make markdown-fmt-check Check Markdown formatting with oxfmt" @echo "" # Build both binaries to dist/ directory @@ -73,6 +76,24 @@ go-install: go-uninstall: @bash ./scripts/go-uninstall.sh +# Format Markdown files in-place using oxfmt +.PHONY: markdown-fmt +markdown-fmt: + @if [ -z "$(MD_FILES)" ]; then \ + echo "No Markdown files found."; \ + else \ + npx oxfmt --write $(MD_FILES); \ + fi + +# Check Markdown formatting using oxfmt +.PHONY: markdown-fmt-check +markdown-fmt-check: + @if [ -z "$(MD_FILES)" ]; then \ + echo "No Markdown files found."; \ + else \ + npx oxfmt --check $(MD_FILES); \ + fi + # Clean up binaries in dist/ directory .PHONY: clean clean: diff --git a/README.md b/README.md index 792e2d2..cb6f7ab 100644 --- a/README.md +++ b/README.md @@ -127,15 +127,13 @@ make go-uninstall > **Windows PATH Note:** If you install `gwtt.exe` into a custom folder (e.g., `C:\Users\\bin`), add that folder to your PATH and open a new terminal to pick it up. - > [!Note] > **Ownership Change (v0.0.7+)** -> +> > The repository ownership changed after v0.0.6. The old `dev-pi2pie` path no longer exists, so use the new module path for all installs and imports: -> +> > - **v0.0.7 and later:** `github.com/pi2pie/git-worktree-tasks` - ## Binary Naming and Shell Configuration - Release assets ship the `gwtt` binary. @@ -144,15 +142,16 @@ make go-uninstall Set up the `gwtt` alias for convenience: -| Shell | Config File | Alias Syntax | -|-------|-------------|--------------| -| Bash | `~/.bashrc` | `alias gwtt="git-worktree-tasks"` | -| Zsh | `~/.zshrc` | `alias gwtt="git-worktree-tasks"` | -| Fish | `~/.config/fish/config.fish` | `alias gwtt git-worktree-tasks` | +| Shell | Config File | Alias Syntax | +| ----- | ---------------------------- | --------------------------------- | +| Bash | `~/.bashrc` | `alias gwtt="git-worktree-tasks"` | +| Zsh | `~/.zshrc` | `alias gwtt="git-worktree-tasks"` | +| Fish | `~/.config/fish/config.fish` | `alias gwtt git-worktree-tasks` | After adding, reload your shell (`source ~/.bashrc`, `source ~/.zshrc`, or `exec fish`). **Alternative:** Create a symlink: + ```bash ln -s $(which git-worktree-tasks) $(dirname $(which git-worktree-tasks))/gwtt ``` @@ -244,6 +243,7 @@ Project: `gwtt.config.toml` or `gwtt.toml` in the repo root User: `$HOME/.config/gwtt/config.toml` **Minimal config:** + ```toml [theme] name = "nord" @@ -255,14 +255,14 @@ name = "nord" ### Commands Overview -| Command | Alias | Description | -|-----------|-------|-------------| +| Command | Alias | Description | +| --------- | ----- | -------------------------------------------------------------------- | | `apply` | | Apply Codex worktree changes to the local checkout (codex mode only) | -| `create` | | Create a worktree and branch for a task | -| `list` | `ls` | List task worktrees | -| `status` | | Show detailed worktree status | -| `finish` | | Merge a task branch into target | -| `cleanup` | `rm` | Remove a task worktree and/or branch | +| `create` | | Create a worktree and branch for a task | +| `list` | `ls` | List task worktrees | +| `status` | | Show detailed worktree status | +| `finish` | | Merge a task branch into target | +| `cleanup` | `rm` | Remove a task worktree and/or branch | ### Creating Worktrees @@ -293,6 +293,7 @@ gwtt create "my-task" --dry-run | `--dry-run` | | Show git commands without executing | **Notes:** + - The default base is the current local branch (for example `main`, `master`, or `dev`). - If you are in a detached HEAD state, you must pass `--base` explicitly. @@ -409,6 +410,7 @@ gwtt --mode codex apply --dry-run ``` **Notes:** + - In codex mode, `` is the directory directly under `$CODEX_HOME/worktrees`. - If conflicts are detected, `gwtt` prompts to overwrite the Codex worktree (second confirmation). `--yes` skips prompts. @@ -452,23 +454,23 @@ The `--output` (`-o`) and `--field` (`-f`) flags enable powerful shell integrati ### Output Formats -| Format | Description | Available In | -|--------|-------------|--------------| +| Format | Description | Available In | +| ------- | ------------------------------ | ---------------- | | `table` | Human-readable table (default) | `list`, `status` | -| `json` | JSON array | `list`, `status` | -| `csv` | CSV with headers | `list`, `status` | -| `raw` | Single value, no decoration | `create`, `list` | -| `text` | Styled text output (default) | `create` | +| `json` | JSON array | `list`, `status` | +| `csv` | CSV with headers | `list`, `status` | +| `raw` | Single value, no decoration | `create`, `list` | +| `text` | Styled text output (default) | `create` | ### Field Selection (for `--output raw`) When using `--output raw` with `list`, specify which field to output: -| Field | Description | -|-------|-------------| -| `path` | Worktree path (default) | -| `task` | Task name | -| `branch` | Branch name | +| Field | Description | +| -------- | ----------------------- | +| `path` | Worktree path (default) | +| `task` | Task name | +| `branch` | Branch name | ### Piping Examples @@ -547,6 +549,7 @@ gwtt-new() { ### Raw Output Fallback When using `--output raw` with `list`: + - If no matching worktree exists but the branch does, returns the main worktree path - Requires either a task filter or `--branch` flag @@ -617,6 +620,7 @@ export PATH="$(go env GOPATH)/bin:$PATH" ### Shell Alias Not Working Reload your shell after adding the alias: + ```bash source ~/.bashrc # Bash source ~/.zshrc # Zsh diff --git a/docs/plans/jobs/2026-01-12-cleanup-behavior-and-branch-output.md b/docs/plans/jobs/2026-01-12-cleanup-behavior-and-branch-output.md index 147f31f..e3022a5 100644 --- a/docs/plans/jobs/2026-01-12-cleanup-behavior-and-branch-output.md +++ b/docs/plans/jobs/2026-01-12-cleanup-behavior-and-branch-output.md @@ -6,14 +6,17 @@ agent: codex --- ## Summary + - Updated cleanup defaults to remove both worktree and branch with separate confirmations. - Added worktree-only cleanup mode and improved missing-worktree messaging. - Added create flag to print a ready-to-run `cd` command. - Clarified README usage and notes for branch output and cleanup modes. ## Why + - Align cleanup behavior with expected confirmation flow and missing-worktree scenarios. - Make it easier to jump into new worktrees and understand branch visibility. ## Notes + - Cleanup now checks branch existence and worktree presence before prompting. diff --git a/docs/plans/jobs/2026-01-12-color-output.md b/docs/plans/jobs/2026-01-12-color-output.md index 00045d9..ac14ce3 100644 --- a/docs/plans/jobs/2026-01-12-color-output.md +++ b/docs/plans/jobs/2026-01-12-color-output.md @@ -6,8 +6,10 @@ agent: codex --- ## Scope + - Add a global `--nocolor` flag to disable ANSI styling. - Use lipgloss for CLI prompts, success messages, and table output. ## Notes + - Preserve JSON/raw outputs without ANSI codes. diff --git a/docs/plans/jobs/2026-01-12-create-existing-worktree-and-path.md b/docs/plans/jobs/2026-01-12-create-existing-worktree-and-path.md index 064afd9..17f69c4 100644 --- a/docs/plans/jobs/2026-01-12-create-existing-worktree-and-path.md +++ b/docs/plans/jobs/2026-01-12-create-existing-worktree-and-path.md @@ -6,6 +6,7 @@ agent: codex --- ## Summary + - Create now returns existing task worktree path instead of failing. - Added `--path/-p` to override worktree location (relative to repo root or absolute). - Documented the new behavior and option in README. diff --git a/docs/plans/jobs/2026-01-12-create-output-and-clipboard.md b/docs/plans/jobs/2026-01-12-create-output-and-clipboard.md index 4d0a57e..7c9dfa2 100644 --- a/docs/plans/jobs/2026-01-12-create-output-and-clipboard.md +++ b/docs/plans/jobs/2026-01-12-create-output-and-clipboard.md @@ -6,9 +6,11 @@ agent: codex --- ## Summary + - Changed create output to show relative worktree paths. - Replaced print-cd with copy-cd flag to copy a `cd` command to clipboard. - Added cross-platform clipboard helper with common OS commands. ## Notes + - Clipboard support uses pbcopy (macOS), clip (Windows), wl-copy or xclip (Linux). diff --git a/docs/plans/jobs/2026-01-12-create-raw-output.md b/docs/plans/jobs/2026-01-12-create-raw-output.md index b7220e5..22536bb 100644 --- a/docs/plans/jobs/2026-01-12-create-raw-output.md +++ b/docs/plans/jobs/2026-01-12-create-raw-output.md @@ -6,8 +6,10 @@ agent: codex --- ## Summary + - Added create output flag supporting raw mode that prints only the worktree path. - Documented piping `--output raw` into `cd` and noted behavior in README. ## Notes + - Raw output is text-only and intended for scripting/pipes. diff --git a/docs/plans/jobs/2026-01-12-disable-default-completion.md b/docs/plans/jobs/2026-01-12-disable-default-completion.md index 2d0394a..f2c54af 100644 --- a/docs/plans/jobs/2026-01-12-disable-default-completion.md +++ b/docs/plans/jobs/2026-01-12-disable-default-completion.md @@ -6,10 +6,13 @@ agent: codex --- ## Summary + - Disabled Cobra's auto-registered completion subcommand so help output matches intended commands. ## Changes + - Set `cmd.CompletionOptions.DisableDefaultCmd = true` in `cli/root.go`. ## Rationale + - The tool does not define a completion command, so the default Cobra command was misleading. diff --git a/docs/plans/jobs/2026-01-12-init-implementation.md b/docs/plans/jobs/2026-01-12-init-implementation.md index bfbff6c..9fb4839 100644 --- a/docs/plans/jobs/2026-01-12-init-implementation.md +++ b/docs/plans/jobs/2026-01-12-init-implementation.md @@ -6,17 +6,21 @@ agent: codex --- ## Goal + Implement the initial CLI scaffolding, command tree, and supporting utilities for git-worktree-tasks. ## Scope + - Cobra command tree with create/finish/cleanup/list/status and aliases. - Base utilities for slugification and worktree path derivation. - List/status data extraction scaffolding. - Minimal styling/TUI placeholders wired into the CLI. ## Out of Scope + - Full business logic for worktree manipulation. - Advanced TUI flows or persistence. ## Notes + - Defaults and behaviors follow the init plan and worktree ops matrix. diff --git a/docs/plans/jobs/2026-01-12-themes-and-rwd.md b/docs/plans/jobs/2026-01-12-themes-and-rwd.md index 19ea6c0..030b598 100644 --- a/docs/plans/jobs/2026-01-12-themes-and-rwd.md +++ b/docs/plans/jobs/2026-01-12-themes-and-rwd.md @@ -6,14 +6,18 @@ agent: codex --- ## Context + Add multiple UI themes selectable via `--theme` and improve table rendering for narrow terminals. ## Work Summary + - Add theme palette definitions with role-based colors and runtime selection via `--theme`. - Extend table rendering to support terminal-width-aware truncation for flexible columns. ## Next Steps + - Implement TUI table usage (Bubble Tea) if desired. ## Related Research + - ../research-2026-01-12-themes-and-table.md diff --git a/docs/plans/jobs/2026-01-13-align-output-flags.md b/docs/plans/jobs/2026-01-13-align-output-flags.md index 6028d55..cfc05e2 100644 --- a/docs/plans/jobs/2026-01-13-align-output-flags.md +++ b/docs/plans/jobs/2026-01-13-align-output-flags.md @@ -6,13 +6,16 @@ agent: codex --- ## Summary + - Added `--skip-existing`/`--skip` to `create` and defaulted existing-worktree handling to a non-zero error. - Added `-o` alias plus `csv` output to `list` and `status`, and `raw` output to `list` only. - Enabled `list [task]` arg filtering with slugification to streamline single-task lookups. ## Rationale + - Make output behavior consistent and predictable across subcommands. - Support composition with shell workflows (raw path for `cd`, CSV for tooling). ## Notes + - `list --output raw` now requires a task or branch filter and prints the first match. diff --git a/docs/plans/jobs/2026-01-13-create-auto-detect-branch.md b/docs/plans/jobs/2026-01-13-create-auto-detect-branch.md index b2a7652..c748a4b 100644 --- a/docs/plans/jobs/2026-01-13-create-auto-detect-branch.md +++ b/docs/plans/jobs/2026-01-13-create-auto-detect-branch.md @@ -6,8 +6,10 @@ agent: codex --- ## Summary + - `create` now detects an existing local branch for the task and reuses it when adding the worktree. - Updated README to document the behavior. ## Rationale + - Simplify workflow when a task branch already exists but no worktree is present. diff --git a/docs/plans/jobs/2026-01-13-deprecate-copy-cd-and-task-flag.md b/docs/plans/jobs/2026-01-13-deprecate-copy-cd-and-task-flag.md index 9067802..022a45b 100644 --- a/docs/plans/jobs/2026-01-13-deprecate-copy-cd-and-task-flag.md +++ b/docs/plans/jobs/2026-01-13-deprecate-copy-cd-and-task-flag.md @@ -6,19 +6,24 @@ agent: codex --- # Goal + Deprecate redundant CLI flags: `create --copy-cd` and `list --task`. # Rationale + - Clipboard copy is better served via shell piping of raw output. - `list ` already covers task filtering; `--task` is redundant. # Notes + - No user-facing deprecation notice requested at this time. - Commit message will be added separately by the user. - - Deprecations were fully removed in implementation. +- Deprecations were fully removed in implementation. # Related Plans + - docs/plans/plan-2026-01-13-list-copy-flag.md # Related Jobs + - docs/plans/jobs/2026-01-13-remove-copy-cd-and-list-task.md diff --git a/docs/plans/jobs/2026-01-13-implement-gwtt-workflow.md b/docs/plans/jobs/2026-01-13-implement-gwtt-workflow.md index 3903866..d36c1f6 100644 --- a/docs/plans/jobs/2026-01-13-implement-gwtt-workflow.md +++ b/docs/plans/jobs/2026-01-13-implement-gwtt-workflow.md @@ -16,6 +16,7 @@ Implemented the build and installation workflow plan for providing both `git-wor Created two shell scripts for Go developers: #### `scripts/go-install.sh` + - Detects Go installation and validates environment - Builds both `git-worktree-tasks` and `gwtt` binaries from source - Installs to `$GOPATH/bin` (or custom path via argument) @@ -26,6 +27,7 @@ Created two shell scripts for Go developers: - Handles errors gracefully with informative messages **Features:** + - Go version detection - Automatic GOPATH/GOBIN detection - Custom installation path support @@ -34,6 +36,7 @@ Created two shell scripts for Go developers: - Symlink alternative instructions #### `scripts/go-uninstall.sh` + - Detects and removes both binaries from installation directory - Provides clear feedback on what was removed - Shows instructions for cleaning up shell aliases @@ -52,6 +55,7 @@ Created comprehensive Makefile with targets: - **`make clean`** — Remove local build artifacts **Design:** + - Clear target naming with `go-*` prefix for Go developer workflows - Simple, self-documenting help system - Delegates to shell scripts for complex operations @@ -62,6 +66,7 @@ Created comprehensive Makefile with targets: Comprehensively updated documentation: #### New Sections + - **Installation** — Quick start and multiple installation options - **Shell Configuration** — Three options (alias, symlink, full name) - **Uninstallation** — Clear removal instructions for all approaches @@ -70,17 +75,20 @@ Comprehensively updated documentation: - **Troubleshooting** — Common issues and solutions #### Installation Options Documented + 1. Using Makefile (recommended for developers) 2. Using installation script directly 3. Standard `go install` (basic approach) 4. Local build without installation #### Shell Configuration Examples + - Bash (`~/.bashrc`) - Zsh (`~/.zshrc`) - Fish (`~/.config/fish/config.fish`) #### Removal Instructions + - Alias removal per shell - Symlink removal - Script-based uninstallation @@ -101,6 +109,7 @@ Performed end-to-end testing: 10. ✅ Verified Makefile syntax and targets **Test Output:** + ``` make build ✓ Both binaries built successfully @@ -115,14 +124,17 @@ git-worktree-tasks version 0.0.6-canary.1 ## Files Created/Modified ### New Files + - `scripts/go-install.sh` (executable, 3222 bytes) - `scripts/go-uninstall.sh` (executable, 2939 bytes) - `Makefile` (1713 bytes) ### Modified Files + - `README.md` — Comprehensive rewrite with installation and configuration sections ### Project Structure + ``` ./scripts/ ├── go-install.sh ✅ Created @@ -136,6 +148,7 @@ README.md ✅ Updated with dist/ references ## Key Features Implemented ### For Users + - ✅ Simple `make go-install` command for installation - ✅ Support for custom installation paths - ✅ Shell-agnostic approach (alias, symlink, or full name) @@ -144,6 +157,7 @@ README.md ✅ Updated with dist/ references - ✅ Comprehensive documentation in README ### For Developers + - ✅ `make build` for local development - ✅ `make clean` for cleanup - ✅ Go-specific naming (`go-*` prefix) clarifies Go requirement @@ -151,6 +165,7 @@ README.md ✅ Updated with dist/ references - ✅ Makefile delegates to scripts (separation of concerns) ### For Documentation + - ✅ Multiple installation methods documented - ✅ Shell configuration examples for all major shells - ✅ Troubleshooting section for common issues @@ -201,4 +216,4 @@ README.md ✅ Updated with dist/ references - Build output organized in `dist/` directory for clean project root - `make build` automatically creates `dist/` directory if needed - `make clean` removes entire `dist/` directory -- All tests passed successfully \ No newline at end of file +- All tests passed successfully diff --git a/docs/plans/jobs/2026-01-13-lint-and-cleanup.md b/docs/plans/jobs/2026-01-13-lint-and-cleanup.md index c0fcd9e..0e3df3a 100644 --- a/docs/plans/jobs/2026-01-13-lint-and-cleanup.md +++ b/docs/plans/jobs/2026-01-13-lint-and-cleanup.md @@ -6,9 +6,11 @@ agent: GitHub Copilot --- ## Description + Removing unused `state *runState` parameters from CLI subcommand constructors to improve code quality and reduce noise. ## Tasks + - [x] Remove `state` param from `cli/cleanup.go` - [x] Remove `state` param from `cli/create.go` - [x] Remove `state` param from `cli/finish.go` diff --git a/docs/plans/jobs/2026-01-13-list-status-search.md b/docs/plans/jobs/2026-01-13-list-status-search.md index fb1c1dd..5b43dd9 100644 --- a/docs/plans/jobs/2026-01-13-list-status-search.md +++ b/docs/plans/jobs/2026-01-13-list-status-search.md @@ -6,14 +6,17 @@ agent: codex --- ## Summary + - Added repo base name resolution via git common dir so task derivation works from any worktree. - Added positional `[task]` filtering for `list`/`status` with contains matching and `--strict` for exact match. - Updated docs with new list/status filtering behavior. ## Rationale + - Ensure task names resolve consistently when running from a worktree. - Provide zoxide-like, low-friction task search without extra dependencies. ## Notes + - `--task` remains exact and now normalizes via slugification. - `list --output raw` still prints the first match when filters are used. diff --git a/docs/plans/jobs/2026-01-13-readme-usage-guide-refinement.md b/docs/plans/jobs/2026-01-13-readme-usage-guide-refinement.md index 795883e..5638d80 100644 --- a/docs/plans/jobs/2026-01-13-readme-usage-guide-refinement.md +++ b/docs/plans/jobs/2026-01-13-readme-usage-guide-refinement.md @@ -29,15 +29,16 @@ Restructured and refined the README to provide a clearer usage guide with improv Documented the `--output` (`-o`) and `--field` (`-f`) flag combinations: -| Format | Description | Available In | -|--------|-------------|--------------| +| Format | Description | Available In | +| ------- | ------------------------------ | ---------------- | | `table` | Human-readable table (default) | `list`, `status` | -| `json` | JSON array | `list`, `status` | -| `csv` | CSV with headers | `list`, `status` | -| `raw` | Single value, no decoration | `create`, `list` | -| `text` | Styled text output (default) | `create` | +| `json` | JSON array | `list`, `status` | +| `csv` | CSV with headers | `list`, `status` | +| `raw` | Single value, no decoration | `create`, `list` | +| `text` | Styled text output (default) | `create` | Added practical piping examples: + - Navigate to worktree: `cd "$(gwtt list my-task -o raw)"` - Copy to clipboard: `gwtt list my-task -o raw -f task | pbcopy` - Open in editor: `code "$(gwtt list my-task -o raw)"` diff --git a/docs/plans/jobs/2026-01-13-remove-copy-cd-and-list-task.md b/docs/plans/jobs/2026-01-13-remove-copy-cd-and-list-task.md index 0f5c1cc..de6573e 100644 --- a/docs/plans/jobs/2026-01-13-remove-copy-cd-and-list-task.md +++ b/docs/plans/jobs/2026-01-13-remove-copy-cd-and-list-task.md @@ -6,12 +6,15 @@ agent: codex --- # Goal + Remove deprecated CLI flags `create --copy-cd` and `list --task`. # Summary + - Removed `create --copy-cd` flag and clipboard helper. - Removed `list --task` flag (use `list ` instead). - Updated README usage examples to prefer piping raw output. # Related Plans + - docs/plans/plan-2026-01-13-list-copy-flag.md diff --git a/docs/plans/jobs/2026-01-13-theme-config-and-env.md b/docs/plans/jobs/2026-01-13-theme-config-and-env.md index 7dc8f81..28c05c3 100644 --- a/docs/plans/jobs/2026-01-13-theme-config-and-env.md +++ b/docs/plans/jobs/2026-01-13-theme-config-and-env.md @@ -6,11 +6,13 @@ agent: codex --- ## Summary + - Added TOML-backed theme config resolution with env and file precedence. - Wired config resolution into CLI theme selection logic. - Documented configuration usage in README and recorded TOML library research. ## Changes + - `internal/config/theme.go` — resolve theme name from `GWTT_THEME`, project config, and user config. - `cli/root.go` — respect config/env when `--theme` is not explicitly set. - `README.md` — configuration section with precedence and examples. @@ -18,4 +20,5 @@ agent: codex - `internal/config/theme_test.go` — tests for precedence and parsing errors. ## Rationale + Users need a default theme without always passing `--theme`. Config/env-based selection keeps behavior explicit and consistent with CLI overrides. diff --git a/docs/plans/jobs/2026-01-13-verify-root-command-config.md b/docs/plans/jobs/2026-01-13-verify-root-command-config.md index 44b099e..3b9f6a6 100644 --- a/docs/plans/jobs/2026-01-13-verify-root-command-config.md +++ b/docs/plans/jobs/2026-01-13-verify-root-command-config.md @@ -20,6 +20,7 @@ Examined the `gitWorkTreeCommand()` function (lines 47-53 in `cli/root.go`) to c ## Findings The configuration is **correct**. The `gwtt` alias is properly defined and provides users with a convenient shorthand for invoking the CLI tool. Users can call the tool using either: + - `git-worktree-tasks [subcommand]` (full name) - `gwtt [subcommand]` (alias shortcut) @@ -27,4 +28,4 @@ This is standard Cobra practice and introduces no redundancy or configuration er ## Status -✅ Verified and confirmed correct. \ No newline at end of file +✅ Verified and confirmed correct. diff --git a/docs/plans/jobs/2026-01-13-worktree-raw-fallback-impl.md b/docs/plans/jobs/2026-01-13-worktree-raw-fallback-impl.md index cdbd41f..ab9d9c5 100644 --- a/docs/plans/jobs/2026-01-13-worktree-raw-fallback-impl.md +++ b/docs/plans/jobs/2026-01-13-worktree-raw-fallback-impl.md @@ -6,9 +6,11 @@ agent: codex --- ## Summary + Start implementing the raw output fallback for `list` and `status` so missing worktree paths fall back to the main worktree path. ## Work Notes + - Source plan: `docs/plans/plan-2026-01-13-worktree-raw-fallback.md` - Goal: Align `list --output raw` and `status` path resolution with a shared fallback rule. - Added fallback helper to resolve main worktree path when a task branch exists without a worktree. diff --git a/docs/plans/jobs/2026-01-19-add-man-page-generation.md b/docs/plans/jobs/2026-01-19-add-man-page-generation.md index 643944c..ef0a516 100644 --- a/docs/plans/jobs/2026-01-19-add-man-page-generation.md +++ b/docs/plans/jobs/2026-01-19-add-man-page-generation.md @@ -6,9 +6,11 @@ agent: Codex --- ## Overview + Added Cobra-based man(1) generation and wired it into the Go install workflow so man pages are built and installed alongside binaries. ## Changes + - Added a man page generator (`scripts/generate-man.go`) using Cobra doc helpers (run twice for `git-worktree-tasks` and `gwtt`). - Generated and committed man pages under `man/man1` for installation packaging. - Exposed a `cli.RootCommand()` helper for documentation tooling. @@ -17,6 +19,7 @@ Added Cobra-based man(1) generation and wired it into the Go install workflow so - Updated `scripts/go-uninstall.sh` to remove installed man pages. ## Files Touched + - `cli/root.go` - `scripts/generate-man.go` - `Makefile` diff --git a/docs/plans/jobs/2026-01-19-cleanup-skip-main-worktree.md b/docs/plans/jobs/2026-01-19-cleanup-skip-main-worktree.md index 01ca8e5..0a47853 100644 --- a/docs/plans/jobs/2026-01-19-cleanup-skip-main-worktree.md +++ b/docs/plans/jobs/2026-01-19-cleanup-skip-main-worktree.md @@ -6,7 +6,9 @@ agent: codex --- ## Summary + - skip the main checkout when matching worktrees by branch during cleanup so branch-only cleanup still works when a task branch is checked out in the root worktree. ## Rationale + - branch-first resolution can resolve to the repo root, which Git refuses to remove, blocking cleanup of the branch. diff --git a/docs/plans/jobs/2026-01-19-consolidate-normalize-path.md b/docs/plans/jobs/2026-01-19-consolidate-normalize-path.md index ab84f38..6c6c043 100644 --- a/docs/plans/jobs/2026-01-19-consolidate-normalize-path.md +++ b/docs/plans/jobs/2026-01-19-consolidate-normalize-path.md @@ -20,4 +20,4 @@ agent: Zed Agent ## Related Plans -- ../plan-2026-01-18-open-source-readiness.md \ No newline at end of file +- ../plan-2026-01-18-open-source-readiness.md diff --git a/docs/plans/jobs/2026-01-19-create-skip-existing-branch-message.md b/docs/plans/jobs/2026-01-19-create-skip-existing-branch-message.md index 4a5760c..538d04d 100644 --- a/docs/plans/jobs/2026-01-19-create-skip-existing-branch-message.md +++ b/docs/plans/jobs/2026-01-19-create-skip-existing-branch-message.md @@ -6,8 +6,10 @@ agent: Codex --- ## Summary + - Updated `create --skip-existing` output to show the actual branch associated with the existing worktree. - Added a worktree lookup helper to resolve the branch by path, falling back to "detached" or the task name. ## Rationale + - The previous message always echoed the task name, which could mislead when the worktree was on a different branch. diff --git a/docs/plans/jobs/2026-01-19-dynamic-short-hash-length.md b/docs/plans/jobs/2026-01-19-dynamic-short-hash-length.md index a6f37db..055797b 100644 --- a/docs/plans/jobs/2026-01-19-dynamic-short-hash-length.md +++ b/docs/plans/jobs/2026-01-19-dynamic-short-hash-length.md @@ -53,12 +53,12 @@ Git uses **packed object count**, not commit count. Object count is typically 5- ### Real-World Examples -| Repository | Commits | Objects (est.) | Hash Length | -|------------|---------|----------------|-------------| -| Small project | < 1K | < 10K | 7 chars | -| Medium project | ~3K | ~30K | 7-8 chars | -| Large project | ~30K | ~300K | 8-10 chars | -| Linux kernel | ~1M | ~7M | 12 chars | +| Repository | Commits | Objects (est.) | Hash Length | +| -------------- | ------- | -------------- | ----------- | +| Small project | < 1K | < 10K | 7 chars | +| Medium project | ~3K | ~30K | 7-8 chars | +| Large project | ~30K | ~300K | 8-10 chars | +| Linux kernel | ~1M | ~7M | 12 chars | ## References diff --git a/docs/plans/jobs/2026-01-19-empty-history-handling.md b/docs/plans/jobs/2026-01-19-empty-history-handling.md index 7fa8123..a043952 100644 --- a/docs/plans/jobs/2026-01-19-empty-history-handling.md +++ b/docs/plans/jobs/2026-01-19-empty-history-handling.md @@ -6,16 +6,19 @@ agent: Codex --- ## What changed + - Added Git stderr classification for friendlier "not a repo" and "no commits yet" errors in core helpers. - Made short-hash lookup tolerant of empty-history repos (returns default length without error). - Labeled status output as "empty history" for last commit/base when HEAD is missing. - Standardized empty-history behavior across subcommands by adding current-branch checks where needed. ## Why + - Avoid exposing raw Git stderr while keeping users informed about missing history. - Keep CLI behavior consistent when repositories have no commits. ## Current behavior + - When run outside a Git repo, commands return: "not a git repository (run inside a git repository)". - In empty-history repos, `status`, `list`, `finish`, `create`, and `cleanup` all error with: "no commits yet (empty history)". - `status` labels `LastCommit`/`Base` as "empty history" only if it reaches worktree evaluation. diff --git a/docs/plans/jobs/2026-01-19-fix-cleanup-yes-worktree-resolution.md b/docs/plans/jobs/2026-01-19-fix-cleanup-yes-worktree-resolution.md index c250660..b54ae58 100644 --- a/docs/plans/jobs/2026-01-19-fix-cleanup-yes-worktree-resolution.md +++ b/docs/plans/jobs/2026-01-19-fix-cleanup-yes-worktree-resolution.md @@ -6,11 +6,14 @@ agent: codex --- ## Summary + - Resolve cleanup worktree path via branch-backed worktree list before deleting. - Fall back to path matching when no branch match is found. ## Rationale + - Cleanup should delete the correct worktree even when the path was overridden or differs from the default naming convention. ## Notes + - No user-facing flags changed; behavior is stricter about finding existing worktrees by branch. diff --git a/docs/plans/jobs/2026-01-19-fix-goreleaser-previous-tag.md b/docs/plans/jobs/2026-01-19-fix-goreleaser-previous-tag.md index b1bd4cb..d1329ae 100644 --- a/docs/plans/jobs/2026-01-19-fix-goreleaser-previous-tag.md +++ b/docs/plans/jobs/2026-01-19-fix-goreleaser-previous-tag.md @@ -6,10 +6,13 @@ agent: codex --- ## Summary + - Updated release workflow checkout settings to fetch full history and tags so GoReleaser can resolve the correct previous tag for changelog compare links. ## Rationale + - Shallow fetches of a specific tag can omit other tags, which causes GoReleaser to compute an incorrect or empty `PreviousTag` for release compare links. ## Changes + - Set `fetch-depth: 0` and `fetch-tags: true` for release workflow checkouts. diff --git a/docs/plans/jobs/2026-01-19-integration-tests-ci.md b/docs/plans/jobs/2026-01-19-integration-tests-ci.md index d680644..9554443 100644 --- a/docs/plans/jobs/2026-01-19-integration-tests-ci.md +++ b/docs/plans/jobs/2026-01-19-integration-tests-ci.md @@ -6,12 +6,15 @@ agent: Codex --- ## Summary + - Added integration tests for `create`, `list`, `status`, `finish`, and `cleanup`, covering no-commit repos, detached HEAD with explicit `--base`, and prunable worktree entries. - Added a CI workflow to run `go test ./...` and `golangci-lint`. ## Details + - Tests live in `cli/integration_test.go` and use temporary repos with real git worktree operations. - CI workflow added at `.github/workflows/ci.yml`. ## Related Plans + - ../plan-2026-01-18-open-source-readiness.md diff --git a/docs/plans/jobs/2026-01-19-update-go-install-man-fallback.md b/docs/plans/jobs/2026-01-19-update-go-install-man-fallback.md index 6c08c8a..981acd0 100644 --- a/docs/plans/jobs/2026-01-19-update-go-install-man-fallback.md +++ b/docs/plans/jobs/2026-01-19-update-go-install-man-fallback.md @@ -6,10 +6,13 @@ agent: codex --- ## Summary + - Updated the go-install fallback man page generation to emit both `git-worktree-tasks` and `gwtt` man pages. ## Rationale + - Ensure installs from source archives without prebuilt man pages still include the `gwtt` alias documentation. ## Files Touched + - `scripts/go-install.sh` diff --git a/docs/plans/jobs/2026-01-21-release-workflow-improvements.md b/docs/plans/jobs/2026-01-21-release-workflow-improvements.md index ea8dc69..27a6228 100644 --- a/docs/plans/jobs/2026-01-21-release-workflow-improvements.md +++ b/docs/plans/jobs/2026-01-21-release-workflow-improvements.md @@ -6,15 +6,18 @@ agent: codex --- ## Summary + - Upgraded GitHub Actions versions in the release workflow to current major pins. - Added logic to compute the previous stable tag for stable releases so changelogs include all pre-release history. - Grouped changelog entries in GoReleaser output for clearer release notes. - Added tag-branch enforcement and optional shallow-since fetching for light checkouts. ## Rationale + - Stable releases should aggregate changes since the last stable tag, even when multiple pre-releases occurred. - Grouped release notes make it easier to scan and align with expected formatting. ## Files Touched + - `.github/workflows/release.yml` - `.goreleaser.yml` diff --git a/docs/plans/jobs/2026-01-26-cli-context-and-status.md b/docs/plans/jobs/2026-01-26-cli-context-and-status.md index cc226dd..58a7b89 100644 --- a/docs/plans/jobs/2026-01-26-cli-context-and-status.md +++ b/docs/plans/jobs/2026-01-26-cli-context-and-status.md @@ -6,15 +6,18 @@ agent: codex --- ## Summary + Implemented context propagation for CLI git operations, improved dry-run/error command formatting, and made status ahead/behind parsing explicit with tests. ## Changes + - Threaded `cmd.Context()` through CLI commands and `runGit`. - Added `formatGitCommand`/`shellQuote` helpers for readable command output. - Added explicit parse errors for `rev-list --count` output. - Added tests for git command formatting and status parsing errors. ## Files Touched + - cli/cleanup.go - cli/common.go - cli/common_test.go @@ -26,4 +29,5 @@ Implemented context propagation for CLI git operations, improved dry-run/error c - internal/worktree/status_test.go ## Related Plan + - docs/plans/plan-2026-01-26-go-cli-context-and-status.md diff --git a/docs/plans/jobs/2026-01-27-config-grid-fallback.md b/docs/plans/jobs/2026-01-27-config-grid-fallback.md index 7947500..c22f511 100644 --- a/docs/plans/jobs/2026-01-27-config-grid-fallback.md +++ b/docs/plans/jobs/2026-01-27-config-grid-fallback.md @@ -6,7 +6,9 @@ agent: codex --- ## Summary + - Updated config loading so table grid defaults apply per config source, allowing higher-precedence table settings to override lower-precedence list/status grids. ## Rationale + - Reviewer feedback noted that list/status grid flags could block later table defaults; the change ensures precedence is respected. diff --git a/docs/plans/jobs/2026-01-27-config-refactor-grid-flags.md b/docs/plans/jobs/2026-01-27-config-refactor-grid-flags.md index 71fb004..1e23226 100644 --- a/docs/plans/jobs/2026-01-27-config-refactor-grid-flags.md +++ b/docs/plans/jobs/2026-01-27-config-refactor-grid-flags.md @@ -36,6 +36,7 @@ for _, path := range paths { ``` Benefits: + - Eliminates code duplication - Easier to add more fallback paths in the future - Maintains "first match wins" semantics @@ -65,6 +66,7 @@ Fixed grid cascade behavior to preserve explicit settings across config layers: ## Test Coverage Added `TestLoadConfigExplicitGridPreserved` to verify: + - User config sets `list.grid = true` - Project config sets `table.grid = false` (no explicit `list.grid`) - Result: `list.grid` stays `true`, `status.grid` cascades to `false` diff --git a/docs/plans/jobs/2026-01-27-extensible-config-schema.md b/docs/plans/jobs/2026-01-27-extensible-config-schema.md index 56314fc..0d40345 100644 --- a/docs/plans/jobs/2026-01-27-extensible-config-schema.md +++ b/docs/plans/jobs/2026-01-27-extensible-config-schema.md @@ -6,11 +6,14 @@ agent: codex --- ## Goal + Create the authoritative config schema document for gwtt and link it to the extensible config plan. ## Work Items + - Draft `docs/schemas/config-gwtt.md` with schema, precedence, decisions, and examples. - Update plan to reference the schema doc as a design deliverable. ## Related Plan + - `docs/plans/plan-2026-01-27-extensible-config.md` diff --git a/docs/plans/jobs/2026-01-27-fix-project-config-root-resolution.md b/docs/plans/jobs/2026-01-27-fix-project-config-root-resolution.md index d0643e2..3ff6ec3 100644 --- a/docs/plans/jobs/2026-01-27-fix-project-config-root-resolution.md +++ b/docs/plans/jobs/2026-01-27-fix-project-config-root-resolution.md @@ -9,14 +9,17 @@ modified-date: 2026-01-27 # Fix project config root resolution ## Summary + - Resolve project config from the repo root (identified by a `.git` marker) instead of only the current working directory. - Share repo-root resolution between the main config loader and theme resolution to keep behavior consistent. - Add a regression test to ensure project config is applied when invoked from a subdirectory. ## Why + The config loader was anchored to `os.Getwd()`, so running `gwtt` from a repo subdirectory ignored `gwtt.config.toml` at the repo root, contradicting documented behavior and default expectations. ## Files + - `internal/config/project_root.go` - `internal/config/config.go` - `internal/config/theme.go` diff --git a/docs/plans/jobs/2026-01-27-phase2-config-loader.md b/docs/plans/jobs/2026-01-27-phase2-config-loader.md index 48fc406..a8a2385 100644 --- a/docs/plans/jobs/2026-01-27-phase2-config-loader.md +++ b/docs/plans/jobs/2026-01-27-phase2-config-loader.md @@ -6,13 +6,16 @@ agent: codex --- ## Goal + Implement Phase 2 of extensible config: loader, env support, defaults wiring, and merge-mode validation. ## Work Items + - Add config loader with precedence: flags > env > project > user > defaults. - Introduce `GWTT_COLOR` env (mirrors `--nocolor`, inverted boolean). - Implement config-backed defaults for list/status output, grid, absolute path, confirm toggles. - Add `merge_mode` enum and enforce exclusivity across CLI flags. ## Related Plan + - `docs/plans/plan-2026-01-27-extensible-config.md` diff --git a/docs/plans/jobs/2026-01-27-phase3-docs.md b/docs/plans/jobs/2026-01-27-phase3-docs.md index e4fa04f..8751315 100644 --- a/docs/plans/jobs/2026-01-27-phase3-docs.md +++ b/docs/plans/jobs/2026-01-27-phase3-docs.md @@ -6,11 +6,14 @@ agent: codex --- ## Goal + Update README and examples to reflect extensible config defaults and new env settings. ## Work Items + - Update README configuration section. - Expand `examples/gwtt.config.toml` with new config options. ## Related Plan + - `docs/plans/plan-2026-01-27-extensible-config.md` diff --git a/docs/plans/jobs/2026-01-27-phase3-tests.md b/docs/plans/jobs/2026-01-27-phase3-tests.md index 33e9c95..c1ddffa 100644 --- a/docs/plans/jobs/2026-01-27-phase3-tests.md +++ b/docs/plans/jobs/2026-01-27-phase3-tests.md @@ -6,12 +6,15 @@ agent: codex --- ## Goal + Add tests covering config resolution and merge-mode validation, and run the test suite. ## Work Items + - Add config loader precedence tests and env validation. - Add merge-mode mapping and exclusivity tests. - Run `go test ./...`. ## Related Plan + - `docs/plans/plan-2026-01-27-extensible-config.md` diff --git a/docs/plans/jobs/2026-01-27-project-root-tests.md b/docs/plans/jobs/2026-01-27-project-root-tests.md index 52b36b2..5d40330 100644 --- a/docs/plans/jobs/2026-01-27-project-root-tests.md +++ b/docs/plans/jobs/2026-01-27-project-root-tests.md @@ -10,12 +10,14 @@ agent: copilot ## Summary Follow-up to the project config root resolution fix. Adds: + - Direct unit tests for `project_root.go` edge cases - Documentation comments clarifying `.git` file handling for worktrees/submodules ## Why The initial fix had coverage only via integration tests in `config_test.go`. Direct unit tests improve: + - Edge case coverage (no `.git` found, `.git` as file) - Documentation of intentional behavior - Faster feedback during refactoring @@ -27,16 +29,16 @@ The initial fix had coverage only via integration tests in `config_test.go`. Dir ## Test Cases -| Test | Scenario | -|------|----------| -| `TestFindRepoRoot_Found` | Nested directory with `.git` at ancestor | -| `TestFindRepoRoot_NotFound` | No `.git` in hierarchy | -| `TestFindRepoRoot_GitFile` | `.git` is a file (worktree scenario) | -| `TestHasGitDir_Directory` | `.git` directory exists | -| `TestHasGitDir_File` | `.git` file exists | -| `TestHasGitDir_NotExists` | No `.git` present | -| `TestProjectConfigRoot_WithGit` | Returns repo root from subdir | -| `TestProjectConfigRoot_NoGit` | Falls back to cwd | +| Test | Scenario | +| ------------------------------- | ---------------------------------------- | +| `TestFindRepoRoot_Found` | Nested directory with `.git` at ancestor | +| `TestFindRepoRoot_NotFound` | No `.git` in hierarchy | +| `TestFindRepoRoot_GitFile` | `.git` is a file (worktree scenario) | +| `TestHasGitDir_Directory` | `.git` directory exists | +| `TestHasGitDir_File` | `.git` file exists | +| `TestHasGitDir_NotExists` | No `.git` present | +| `TestProjectConfigRoot_WithGit` | Returns repo root from subdir | +| `TestProjectConfigRoot_NoGit` | Falls back to cwd | ## Related Plans diff --git a/docs/plans/jobs/2026-02-04-codex-apply-terminology.md b/docs/plans/jobs/2026-02-04-codex-apply-terminology.md index dbb4462..374e089 100644 --- a/docs/plans/jobs/2026-02-04-codex-apply-terminology.md +++ b/docs/plans/jobs/2026-02-04-codex-apply-terminology.md @@ -6,16 +6,19 @@ agent: codex --- ## Summary + - Updated docs to align Codex App UI terminology with "Hand off changes" and clarified that app-side worktree/shell issues are out of CLI scope. - Renamed codex-mode command references from `sync` to `apply` across research, plan, and config docs. - Renamed CLI implementation from `sync` to `apply` (including file rename, symbols, and user-facing messages). ## Why + - Codex App UI now uses "Hand off changes" with directions "To local" / "From local", while official docs still say "Sync with local". - Avoid confusion with "apply" terminology in the Codex CLI by making the gwtt command name explicit and consistent with the app wording. - Track app-side worktree issues separately from CLI responsibilities. ## Files Updated + - `docs/research-2026-02-04-mode-classic-vs-codex.md` - `docs/plans/plan-2026-02-04-mode-classic-and-codex.md` - `docs/schemas/config-gwtt.md` diff --git a/docs/plans/jobs/2026-02-04-fix-codex-opaque-id-and-raw-path.md b/docs/plans/jobs/2026-02-04-fix-codex-opaque-id-and-raw-path.md index 7c8d9b7..9ba66b4 100644 --- a/docs/plans/jobs/2026-02-04-fix-codex-opaque-id-and-raw-path.md +++ b/docs/plans/jobs/2026-02-04-fix-codex-opaque-id-and-raw-path.md @@ -6,12 +6,14 @@ agent: codex --- ## Summary + Adjusted codex-mode worktree handling to match Codex App directory layout: + - Derive `` from the first path segment under `$CODEX_HOME/worktrees`. - Hide codex-owned worktrees in classic mode. - Make `--output raw` in codex mode return a composable path relative to `$CODEX_HOME`. - Updated sync/cleanup resolution to use the corrected opaque-id mapping. ## Notes -- Tests run with `GOCACHE=/tmp/gocache` due to sandbox cache restrictions. +- Tests run with `GOCACHE=/tmp/gocache` due to sandbox cache restrictions. diff --git a/docs/plans/jobs/2026-02-04-fix-codex-raw-paths.md b/docs/plans/jobs/2026-02-04-fix-codex-raw-paths.md index 76e7325..13bbe38 100644 --- a/docs/plans/jobs/2026-02-04-fix-codex-raw-paths.md +++ b/docs/plans/jobs/2026-02-04-fix-codex-raw-paths.md @@ -6,7 +6,8 @@ agent: codex --- ## Summary + Adjusted codex-mode list output so: + - `--output raw` returns a relative path to `$CODEX_HOME` by default. - `--output raw --abs` returns an absolute path (no `$CODEX_HOME` placeholder). - diff --git a/docs/plans/jobs/2026-02-04-fix-codex-raw-relative-to-cwd.md b/docs/plans/jobs/2026-02-04-fix-codex-raw-relative-to-cwd.md index 4e02dad..5aa44c2 100644 --- a/docs/plans/jobs/2026-02-04-fix-codex-raw-relative-to-cwd.md +++ b/docs/plans/jobs/2026-02-04-fix-codex-raw-relative-to-cwd.md @@ -6,5 +6,5 @@ agent: codex --- ## Summary -Adjusted codex-mode `list --output raw` to return paths relative to the current working directory when `--abs` is not set. +Adjusted codex-mode `list --output raw` to return paths relative to the current working directory when `--abs` is not set. diff --git a/docs/plans/jobs/2026-02-04-fuzzy-first-match-list-status.md b/docs/plans/jobs/2026-02-04-fuzzy-first-match-list-status.md index 8ab7e5d..1211d54 100644 --- a/docs/plans/jobs/2026-02-04-fuzzy-first-match-list-status.md +++ b/docs/plans/jobs/2026-02-04-fuzzy-first-match-list-status.md @@ -6,5 +6,5 @@ agent: codex --- ## Summary -When a task query is provided without `--strict`, `list` and `status` now return only the first matching worktree (classic and codex modes) for consistent fuzzy retrieval. +When a task query is provided without `--strict`, `list` and `status` now return only the first matching worktree (classic and codex modes) for consistent fuzzy retrieval. diff --git a/docs/plans/jobs/2026-02-04-implement-mode-flag-phase2.md b/docs/plans/jobs/2026-02-04-implement-mode-flag-phase2.md index 905b34a..9ca8775 100644 --- a/docs/plans/jobs/2026-02-04-implement-mode-flag-phase2.md +++ b/docs/plans/jobs/2026-02-04-implement-mode-flag-phase2.md @@ -6,7 +6,9 @@ agent: codex --- ## Summary + Implemented Phase 2 of `classic` vs `codex` mode support: + - Added global `--mode` flag and mode normalization/validation. - Added config/env support for `mode` (`GWTT_MODE`, `mode = "..."` in TOML). - Implemented codex-mode behavior for `list`, `status` (repo-scoped, `$CODEX_HOME` path display), and added `modified_time` to status output. @@ -14,5 +16,5 @@ Implemented Phase 2 of `classic` vs `codex` mode support: - Updated `cleanup` to support codex-mode worktree removal under `$CODEX_HOME/worktrees/` with additional warnings/confirmation. ## Notes -- Tests were executed with `GOCACHE` pointed at a writable location due to sandbox constraints. +- Tests were executed with `GOCACHE` pointed at a writable location due to sandbox constraints. diff --git a/docs/plans/jobs/2026-02-04-phase-3-tests.md b/docs/plans/jobs/2026-02-04-phase-3-tests.md index 9d80ba4..7e6ec40 100644 --- a/docs/plans/jobs/2026-02-04-phase-3-tests.md +++ b/docs/plans/jobs/2026-02-04-phase-3-tests.md @@ -7,6 +7,7 @@ agent: codex --- ## Summary + - Added unit tests for mode precedence/validation, codex worktree parsing, and apply conflict detection. - Added integration tests for codex list/status filtering, apply confirmation gating, codex cleanup scope/confirmation, and modified_time outputs. - Added CSV output validation for the modified_time field. diff --git a/docs/plans/jobs/2026-02-04-skill-location-refactor.md b/docs/plans/jobs/2026-02-04-skill-location-refactor.md index b27c75b..b91b81a 100644 --- a/docs/plans/jobs/2026-02-04-skill-location-refactor.md +++ b/docs/plans/jobs/2026-02-04-skill-location-refactor.md @@ -6,14 +6,18 @@ agent: codex --- ## Summary + Documented the refactor to align skill paths with Codex v0.94.0, moving references from `.codex/skills` to `.agents/skills`. ## Changes + - Updated skill location references to the new `.agents/skills` path. - Noted the change in repository documentation and context. ## Rationale + Codex v0.94.0 now expects skills under `.agents/skills`, so references must match the new location to avoid broken lookups. ## References + - [Codex v0.94.0 release](https://github.com/openai/codex/releases/tag/rust-v0.94.0) diff --git a/docs/plans/plan-2026-01-12-cleanup-modes-and-branch-visibility.md b/docs/plans/plan-2026-01-12-cleanup-modes-and-branch-visibility.md index e49e300..35af4e8 100644 --- a/docs/plans/plan-2026-01-12-cleanup-modes-and-branch-visibility.md +++ b/docs/plans/plan-2026-01-12-cleanup-modes-and-branch-visibility.md @@ -6,26 +6,31 @@ agent: codex --- ## Goal + Clarify expected behavior around worktree creation, cleanup modes, and branch visibility in list/status. ## Scope + - Confirm desired semantics for cleanup (default dual removal, worktree-only, branch-only). - Define behavior when a worktree is missing but a task branch exists. - Decide whether to surface branch info in list/status output or document the current behavior. - Update README/CLI help to reduce confusion around worktree paths and cleanup. ## Proposed Approach + 1. Inspect current CLI behavior for create/list/status/cleanup and identify gaps. 2. Specify updated cleanup flow, including confirmations and no-worktree handling. 3. Implement CLI changes (flags, prompts, outputs) and adjust tests if present. 4. Update README examples and notes to document worktree/branch visibility. ## Open Questions + - Should list/status include branch columns, or should README clarify that they only show worktrees? - If no worktree exists but the task branch does, should cleanup with default mode remove the branch after confirmation? - When creating a worktree, should the CLI suggest `cd ` or add an optional flag to auto-print a `cd` command? ## Resolution + - Added explicit branch output guidance in README (list/status already include branch columns). - Cleanup defaults to removing both worktree and branch with a second confirmation for the branch, including missing-worktree messaging. - Added a create flag to copy a ready-to-run `cd` command and adjusted create output to show relative paths. diff --git a/docs/plans/plan-2026-01-12-color-output.md b/docs/plans/plan-2026-01-12-color-output.md index 06d4964..3cf7f53 100644 --- a/docs/plans/plan-2026-01-12-color-output.md +++ b/docs/plans/plan-2026-01-12-color-output.md @@ -6,9 +6,11 @@ agent: codex --- ## Goal + Add colorful default CLI output with a `--nocolor` escape hatch while using lipgloss styling consistently. ## Plan + - Define shared styles with a runtime color toggle. - Apply styles to CLI messages and table output without breaking JSON/raw outputs. - Update docs to reflect the implementation work. diff --git a/docs/plans/plan-2026-01-12-init-phase.md b/docs/plans/plan-2026-01-12-init-phase.md index 480d189..2ab1fce 100644 --- a/docs/plans/plan-2026-01-12-init-phase.md +++ b/docs/plans/plan-2026-01-12-init-phase.md @@ -6,26 +6,31 @@ agent: codex --- ## Goal + Establish the initial CLI structure, UX patterns, and repository scaffolding for git worktree task management. ## Scope + - CLI skeleton using cobra with core command groups and flags. - Shared styling utilities using lipgloss. - Initial Bubbletea TUI entry point (basic navigation and layout only). - Documentation and examples to onboard contributors. ## Non-Goals + - Full feature-complete worktree workflows. - Persistent config or integration with external services. - Advanced TUI flows beyond a simple shell. ## Milestones + 1. Define command tree and CLI UX conventions, including fixed worktree path rules, slugification, and list/status output. 2. Create base packages/modules for git/worktree operations and UI, aligned with the operations matrix. 3. Add a minimal TUI with placeholder screens for create/merge/cleanup and confirmation flows. 4. Add docs for the init phase and initial usage examples. ## Deliverables + - Cobra command structure with placeholders for `create`, `finish`, `cleanup`, and `list`, plus explicit remove-worktree/remove-branch flags. - Lipgloss style package with consistent typography and colors. - Bubbletea app with navigation between placeholder views and double-confirmation for destructive actions. @@ -33,11 +38,13 @@ Establish the initial CLI structure, UX patterns, and repository scaffolding for - `list` output defaults to `table` with `--output json` option. ## Decisions + - Worktree path is fixed to `../_` with consistent task slugification. - `finish` requires a second confirmation before deleting worktree/branch, with a bypass flag for full cleanup. - Merge modes support `--squash`, `--no-ff`, and `--rebase`; default follows standard `git merge` behavior. ## Checklist + - [x] Confirm final command names and subcommand hierarchy. - [x] Confirm slugification rules and worktree path derivation. - [x] Confirm `finish` confirmation flow and bypass flag naming. @@ -47,9 +54,11 @@ Establish the initial CLI structure, UX patterns, and repository scaffolding for - [x] Confirm merge strategy flag mapping and defaults. ## Risks / Open Questions + - Final command naming and subcommand hierarchy. - Merge strategy defaults and how they map to CLI flags. - Git worktree edge cases (existing branches, detached HEAD, conflicts). ## Related Research + - docs/research-2026-01-12-worktree-ops-matrix.md diff --git a/docs/plans/plan-2026-01-12-path-display-readme.md b/docs/plans/plan-2026-01-12-path-display-readme.md index e8dc77b..7aca293 100644 --- a/docs/plans/plan-2026-01-12-path-display-readme.md +++ b/docs/plans/plan-2026-01-12-path-display-readme.md @@ -6,23 +6,28 @@ agent: codex --- ## Goal + Default list/status output to relative paths with a flag to show absolute paths, add a root README describing the project, and introduce unit tests for key logic. ## Scope + - Add a CLI flag (name TBD) to toggle absolute vs relative path display. - Default to relative paths in list/status outputs. - Add a root-level README.md with a concise project overview, install/run basics, and command summary. - Add unit tests for core helpers (path derivation, slugification, and list/status mapping as appropriate). ## Out of Scope + - Changing worktree creation path behavior. - Full command reference or extensive docs overhaul. - End-to-end CLI tests or integration tests. ## Related Research + - docs/research-2026-01-12-worktree-ops-matrix.md ## Plan + 1. Review current list/status output formatting and path rendering. 2. Decide flag name, placement, and default behavior; update list/status output accordingly. 3. Draft README.md content and add at repo root. @@ -30,4 +35,5 @@ Default list/status output to relative paths with a flag to show absolute paths, 5. Verify CLI output for both relative and absolute paths and update docs if needed. ## Notes + - Keep relative paths anchored to repo root for readability. diff --git a/docs/plans/plan-2026-01-12-themes-and-rwd.md b/docs/plans/plan-2026-01-12-themes-and-rwd.md index 9233aba..01b9bf0 100644 --- a/docs/plans/plan-2026-01-12-themes-and-rwd.md +++ b/docs/plans/plan-2026-01-12-themes-and-rwd.md @@ -6,17 +6,21 @@ agent: codex --- ## Goal + Deliver multiple selectable themes and improve CLI/TUI table responsiveness for narrow terminals. ## Scope + - Add named themes with role-based colors and a `--theme` selector. - Add `--themes` to print available themes. - Make CLI tables width-aware and optionally render grid borders. - Provide a TUI list view using `bubbles/table` with resize-aware column widths. ## Out of Scope + - Config file, env var, or auto-detect theme selection. - User-defined/custom theme definitions. ## Related Research + - ../research-2026-01-12-themes-and-table.md diff --git a/docs/plans/plan-2026-01-13-build-and-distribution-flow.md b/docs/plans/plan-2026-01-13-build-and-distribution-flow.md index 2752673..7456574 100644 --- a/docs/plans/plan-2026-01-13-build-and-distribution-flow.md +++ b/docs/plans/plan-2026-01-13-build-and-distribution-flow.md @@ -8,6 +8,7 @@ agent: Zed Agent ## Problem Currently, the CLI tool has two command names: + - `git-worktree-tasks` (primary name in `go.mod`) - `gwtt` (alias defined in Cobra) @@ -46,12 +47,14 @@ Create a dedicated scripts directory for Go-specific workflows: ### 2. Build and Installation **Makefile Targets:** + - `make build` — Build both `git-worktree-tasks` and `gwtt` binaries locally - `make go-install` — Build and install both binaries to `$GOPATH/bin` (requires Go) - `make go-uninstall` — Remove both binaries from `$GOPATH/bin` (requires Go) - `make clean` — Clean up local build artifacts **Shell Script (`./scripts/go-install.sh`):** + - Requires Go to be installed - Builds both binaries - Installs to `$GOPATH/bin` (or custom path if provided) @@ -65,27 +68,32 @@ Document three approaches for users across different shells: #### Option A: Shell Alias (Recommended for Simplicity) **For Bash (`~/.bashrc`):** + ```bash alias gwtt="git-worktree-tasks" ``` **For Zsh (`~/.zshrc`):** + ```bash alias gwtt="git-worktree-tasks" ``` **For Fish (`~/.config/fish/config.fish`):** + ```fish alias gwtt git-worktree-tasks ``` **Pros:** + - Easy to add and remove - No filesystem clutter - Works across systems - Can be disabled quickly **Cons:** + - Must be added to each shell profile - Not available to non-shell tools @@ -100,11 +108,13 @@ ln -s $(which git-worktree-tasks) $(dirname $(which git-worktree-tasks))/gwtt ``` **Pros:** + - Works everywhere (shell, scripts, IDEs) - Binary-level shortcut - No shell-specific configuration needed **Cons:** + - Requires cleanup - May conflict with other installations - Requires manual removal @@ -120,18 +130,21 @@ rm $(dirname $(which git-worktree-tasks))/gwtt Users have three paths: **Path 1: Using Makefile (Easiest for developers)** + ```bash make go-install # Then add alias to shell config (Bash, Zsh, or Fish), or create symlink manually ``` **Path 2: Using Script Directly** + ```bash ./scripts/go-install.sh # Then add alias to shell config (Bash, Zsh, or Fish), or create symlink manually ``` **Path 3: Standard go install (Basic, Go-native)** + ```bash go install ./ # Only git-worktree-tasks available; use alias or symlink for gwtt @@ -140,18 +153,21 @@ go install ./ ### 5. Uninstallation Instructions **Using Makefile:** + ```bash make go-uninstall # Also remove alias from shell config or delete symlink if created ``` **Using Script Directly:** + ```bash ./scripts/go-uninstall.sh # Also remove alias from shell config or delete symlink if created ``` **Manual Cleanup:** + ```bash rm $(which git-worktree-tasks) rm $(which gwtt) # If symlink was created @@ -205,4 +221,4 @@ rm $(which gwtt) # If symlink was created - Document both setup and removal procedures clearly for user convenience - This plan sets foundation for future automated distribution methods - Go-specific targets (`go-install`, `go-uninstall`) make it clear this requires Go -- Fish shell uses different syntax for aliases (no `=` operator) \ No newline at end of file +- Fish shell uses different syntax for aliases (no `=` operator) diff --git a/docs/plans/plan-2026-01-13-cicd-release-flow.md b/docs/plans/plan-2026-01-13-cicd-release-flow.md index 987433f..7d9b945 100644 --- a/docs/plans/plan-2026-01-13-cicd-release-flow.md +++ b/docs/plans/plan-2026-01-13-cicd-release-flow.md @@ -6,15 +6,18 @@ agent: Codex --- ## Goal + Design a GitHub Actions release pipeline that adds a pre-release verification stage before publishing, supports manual invocation, and aligns Go tooling with the repository’s Go version. ## Scope + - Release workflow structure (jobs, triggers, permissions, prerelease detection) - Pre-release checks (lint/test/vet, snapshot build) - Manual trigger inputs and behavior - Local developer commands for lint/test/vet ## Plan + 1. Review the current release workflow and identify required changes (permissions, Go version, prerelease logic, job separation). 2. Define the pre-release verification job (lint/test/vet and GoReleaser snapshot build). 3. Define the publish job (GoReleaser release) and prerelease tagging logic. @@ -22,9 +25,11 @@ Design a GitHub Actions release pipeline that adds a pre-release verification st 5. Document local lint/test/vet commands and how they mirror CI. ## Decisions Needed + - Whether to add golangci-lint config (`.golangci.yml`) and which checks to enable. - Which GoReleaser config file to use (default `.goreleaser.yml` or a specific file). - Whether to require tests for release (fail release if tests fail). ## Related Research + None. diff --git a/docs/plans/plan-2026-01-13-list-copy-flag.md b/docs/plans/plan-2026-01-13-list-copy-flag.md index 384cb27..61154a9 100644 --- a/docs/plans/plan-2026-01-13-list-copy-flag.md +++ b/docs/plans/plan-2026-01-13-list-copy-flag.md @@ -6,13 +6,16 @@ agent: codex --- # Goal + Define a copy-to-clipboard flag for `list`/`ls` that mirrors `create --copy-cd`, covering task worktree paths and/or branch names. # Context + - `create --copy-cd` copies a `cd ` command to the clipboard. - `list --output raw` prints a single path when filtered (task/branch). # Proposed UX (draft) + - No clipboard flags; rely on shell piping (e.g., `command | pbcopy`) with `--output raw`. - Require a specific task or branch filter when using `--output raw` (already enforced). - Ensure `--output raw` continues to return a single resolved path for the first match. @@ -20,6 +23,7 @@ Define a copy-to-clipboard flag for `list`/`ls` that mirrors `create --copy-cd`, - Add a field selector for raw output so users can copy task/branch names for cleanup workflows. # Implementation Plan + 1. Confirm `--output raw` requires a specific task or branch name (already enforced). 2. Add `--field`/`-f` (path|task|branch) for `--output raw`, defaulting to `path`. 3. Treat empty `--field=""` as default `path`. @@ -27,8 +31,10 @@ Define a copy-to-clipboard flag for `list`/`ls` that mirrors `create --copy-cd`, 5. Clarify behavior in docs/help: recommend piping `--output raw --field task` to clipboard tools. # Open Questions + - `--field` applies only to `--output raw` and is ignored for other formats. # Completion Notes + - Implemented `--field/-f` for `list --output raw` (path/task/branch, default path). - Removed `create --copy-cd` and `list --task`; updated README to prefer piping raw output. diff --git a/docs/plans/plan-2026-01-13-theme-config-and-env.md b/docs/plans/plan-2026-01-13-theme-config-and-env.md index 819b48f..717954e 100644 --- a/docs/plans/plan-2026-01-13-theme-config-and-env.md +++ b/docs/plans/plan-2026-01-13-theme-config-and-env.md @@ -6,9 +6,11 @@ agent: codex --- ## Goal + Add config/env-driven theme selection so users can set a default theme without always passing `--theme`. ## Scope + - Define config sources, schema, and precedence for theme selection only. - Implement config discovery for: - User config: `$HOME/.config/gwtt/config.toml` @@ -18,13 +20,16 @@ Add config/env-driven theme selection so users can set a default theme without a - Document the config options in `README.md`. ## Out of Scope + - Custom theme palette definitions in config. - Other configuration options beyond theme selection. ## Related Research + - ../research-2026-01-13-toml-config-options.md ## Design Decisions + - **Precedence (highest → lowest)**: CLI flag `--theme` → `GWTT_THEME` → project config (`gwtt.config.toml`, then `gwtt.toml`) → user config → default theme. - **Config schema (TOML)**: - Prefer a focused `theme` table. @@ -39,6 +44,7 @@ Add config/env-driven theme selection so users can set a default theme without a - If both project files exist, `gwtt.config.toml` wins. ## Milestones + 1. Add a small config package to parse TOML and resolve precedence. 2. Wire config into CLI pre-run logic. 3. Update docs and examples. diff --git a/docs/plans/plan-2026-01-13-themes-flag.md b/docs/plans/plan-2026-01-13-themes-flag.md index 96abe3e..e864dd7 100644 --- a/docs/plans/plan-2026-01-13-themes-flag.md +++ b/docs/plans/plan-2026-01-13-themes-flag.md @@ -6,9 +6,11 @@ agent: codex --- ## Goal + - Allow `gwtt --themes` to list themes without requiring a subcommand. ## Plan + - Add root command handling so `--themes` prints the theme list when no subcommand is provided. - Verify help still shows when no args and `--themes` is not set. - Confirm README examples match the updated behavior. diff --git a/docs/plans/plan-2026-01-13-worktree-raw-fallback.md b/docs/plans/plan-2026-01-13-worktree-raw-fallback.md index 970f829..f40af97 100644 --- a/docs/plans/plan-2026-01-13-worktree-raw-fallback.md +++ b/docs/plans/plan-2026-01-13-worktree-raw-fallback.md @@ -6,45 +6,54 @@ agent: codex --- ## Context + The `list --output raw` and `status` subcommands are used in shell flows (e.g., `cd $(...)`). When a task has no worktree path, the current output is not usable and causes unexpected behavior. We need a deterministic fallback to the main worktree path so these flows are stable. ## Goals + - Define a clear fallback rule for tasks with no worktree path. - Align `list --output raw` and `status` so they resolve paths consistently. - Keep existing behavior unchanged for tasks with explicit worktree paths. ## Non-Goals + - Redesigning other output formats beyond `--output raw`. - Changing worktree creation or task discovery logic. ## Proposed Behavior + - If a task has a worktree path, use it. - If a task has no worktree path (even if the branch exists), fall back to the main worktree path. - The main worktree path should be derived from the repo’s primary worktree/root, not inferred from the task branch. ## Work Plan -1) Inspect current `list` and `status` path resolution logic and identify where raw output is generated. -2) Add a shared fallback helper (or equivalent) to resolve task path with main worktree fallback. -3) Update `list --output raw` and `status` to use the shared fallback. -4) Add or update tests/fixtures covering: + +1. Inspect current `list` and `status` path resolution logic and identify where raw output is generated. +2. Add a shared fallback helper (or equivalent) to resolve task path with main worktree fallback. +3. Update `list --output raw` and `status` to use the shared fallback. +4. Add or update tests/fixtures covering: - task with worktree path - task with branch but no worktree path - task without branch and without worktree path -5) Document the fallback behavior in CLI docs/README if applicable. +5. Document the fallback behavior in CLI docs/README if applicable. ## Acceptance Criteria + - `list --output raw` returns a usable path for all tasks. - `status` path reporting matches the same fallback rule. - No behavior change for tasks that already have a worktree path. ## Risks + - Repos with multiple main worktrees or non-standard default branch names. - Detached HEAD when determining the primary worktree. ## Open Questions + - Should `--output raw` for `list` ever return blank paths when fallback is unavailable? - How should we detect the "main worktree path" in a repo with multiple worktrees? ## Related Jobs + - `docs/plans/jobs/2026-01-13-align-output-flags.md` - `docs/plans/jobs/2026-01-13-list-status-search.md` diff --git a/docs/plans/plan-2026-01-18-open-source-readiness.md b/docs/plans/plan-2026-01-18-open-source-readiness.md index 04b06e2..8ed926a 100644 --- a/docs/plans/plan-2026-01-18-open-source-readiness.md +++ b/docs/plans/plan-2026-01-18-open-source-readiness.md @@ -7,9 +7,11 @@ agent: Codex --- ## Goal + Prepare the project for an open-source release by addressing correctness gaps, UX alignment, and documentation/CI needs ahead of the next version. ## Proposed Work + - [x] Add LICENSE (MIT) file. - [x] Add man(1) page generation (Cobra doc) and wire it into install packaging (Makefile target). - [x] Revisit short commit hash behavior in `internal/worktree/status.go` to allow dynamic length (7/8/10) based on repo size; document rationale and references. @@ -23,6 +25,7 @@ Prepare the project for an open-source release by addressing correctness gaps, U - [x] Add CI workflow for `go test ./...` and a linter. ## Rationale + - Open-source release requires clear licensing. - Ship a man(1) page and install it via the build tooling to align with standard CLI packaging expectations. - Short hash length can vary by repository size; avoid assuming a fixed 7-char hash and capture the policy. @@ -31,7 +34,9 @@ Prepare the project for an open-source release by addressing correctness gaps, U - Tests and CI reduce regressions before the next version. ## Current Artifacts + - LICENSE (MIT) added. ## Related Research + - docs/research-2026-01-19-create-default-base.md diff --git a/docs/plans/plan-2026-01-21-release-workflow-improvements.md b/docs/plans/plan-2026-01-21-release-workflow-improvements.md index 2fffdad..de59cb5 100644 --- a/docs/plans/plan-2026-01-21-release-workflow-improvements.md +++ b/docs/plans/plan-2026-01-21-release-workflow-improvements.md @@ -6,20 +6,24 @@ agent: codex --- ## Goal + Improve the GitHub Actions release workflow for GoReleaser by upgrading action versions, supporting optional shallow checkouts, enforcing allowed tag branches, and ensuring final releases include cumulative pre-release changes. ## Scope + - Update action versions in `.github/workflows/release.yml` (checkout, setup-go, goreleaser action). - Add optional `shallow_since` fetching for lighter checkouts while preserving tag history. - Enforce tags only from `main` and `dev*/beta*/alpha*/canary*` branches. - Adjust changelog/release behavior so stable releases include all changes since the last stable release, including pre-release history. ## Non-Goals + - Replacing GoReleaser with another release tool. - Changing the semantic versioning scheme or tag naming conventions. - Altering build matrix or artifact formats unless required by release notes behavior. ## Plan + 1. Audit the current release workflow and GoReleaser config to identify where changelog data is sourced and how tags are selected. 2. Verify current supported versions for `actions/checkout`, `actions/setup-go`, and `goreleaser/goreleaser-action`, and note any required config changes for upgrades (e.g., permissions, default inputs). 3. Decide on shallow history strategy: @@ -32,9 +36,11 @@ Improve the GitHub Actions release workflow for GoReleaser by upgrading action v 6. Document rationale in the workflow comments or a short note in the plan job record. ## Open Questions + - Are there existing release notes expectations (format or tool output) that we must preserve beyond grouped sections? ## Success Criteria + - Workflow uses updated actions with confirmed compatibility. - Release notes for a stable tag include all changes since the last stable tag, even if intermediate pre-releases existed. - Changelog generation is deterministic and documented. diff --git a/docs/plans/plan-2026-01-26-go-cli-context-and-status.md b/docs/plans/plan-2026-01-26-go-cli-context-and-status.md index ae26bab..850af2e 100644 --- a/docs/plans/plan-2026-01-26-go-cli-context-and-status.md +++ b/docs/plans/plan-2026-01-26-go-cli-context-and-status.md @@ -7,46 +7,56 @@ agent: codex --- ## Goal + Improve CLI correctness and UX by propagating contexts, making dry-run output readable, and handling status parsing errors explicitly. ## Scope + - Thread `cmd.Context()` through CLI commands and `runGit`. - Replace slice-style git arg printing with readable, shell-like output. - Make `worktree.Status` parsing explicit (error or warning path) when git output is unexpected. ## Issues Memo + - Context is hardcoded to `context.Background()` in CLI command handlers and `runGit`, preventing cancellation/timeouts from `cmd.Context()`. - Dry-run and git error messages print args as Go slices, which are not copy/paste-friendly for users. - `worktree.Status` ignores `strconv.Atoi` errors, silently mapping unexpected output to zero ahead/behind values. ## Non-Goals + - Changing command semantics or defaults beyond the above. - Adding new CLI flags unrelated to the issues. ## Proposed Steps -1) Introduce context plumbing in CLI entry points and helper functions. -2) Implement a small helper to format git commands for dry-run and error messages. -3) Update `worktree.Status` to handle `Atoi` errors deterministically. -4) Add or update tests to cover the new behavior (as needed). + +1. Introduce context plumbing in CLI entry points and helper functions. +2. Implement a small helper to format git commands for dry-run and error messages. +3. Update `worktree.Status` to handle `Atoi` errors deterministically. +4. Add or update tests to cover the new behavior (as needed). ## Task Checklist + - [x] Propagate `cmd.Context()` through CLI commands and `runGit`. - [x] Add git command formatting helper for dry-run/error output. - [x] Handle `rev-list` parsing errors explicitly in `worktree.Status`. - [x] Update/add tests for context usage and status parsing behavior. ## Related Jobs + - docs/plans/jobs/2026-01-26-cli-context-and-status.md ## Risks + - Minor behavior changes if callers relied on silent parsing failures. - Any quoting changes may affect copy/paste expectations; keep it conservative. - Context propagation should not affect theme config or theme listing, since those are handled in `PersistentPreRunE` before subcommand execution. ## Acceptance Criteria + - Git commands in dry-run and errors are readable and copy/paste-friendly. - CLI respects cancellation via `cmd.Context()` where applicable. - Status parsing errors are surfaced in a consistent and documented way. ## Related Research + - None. diff --git a/docs/plans/plan-2026-01-26-install-script-and-readme.md b/docs/plans/plan-2026-01-26-install-script-and-readme.md index e8a99d4..0f3728f 100644 --- a/docs/plans/plan-2026-01-26-install-script-and-readme.md +++ b/docs/plans/plan-2026-01-26-install-script-and-readme.md @@ -6,9 +6,11 @@ agent: codex --- ## Goal + Create a dedicated install/uninstall experience that detects platform, downloads the correct release asset, and clarifies the `gwtt` vs `git-worktree-tasks` command naming in the README. ## Context + - Release assets built via GoReleaser ship the binary as `gwtt` (or `gwtt.exe`). - `go install` yields a `git-worktree-tasks` binary name in GOPATH. - Users need a clear explanation of why both names exist and how to use them. @@ -17,6 +19,7 @@ Create a dedicated install/uninstall experience that detects platform, downloads - Asset naming and packaging are controlled by `.goreleaser.yml`, which must be aligned with install logic. ## Plan + 1. Review current release assets, README install wording, and existing scripts to understand gaps and naming inconsistencies. 2. Define the install/uninstall approach: - Platform/arch detection and asset naming rules. @@ -31,6 +34,7 @@ Create a dedicated install/uninstall experience that detects platform, downloads - Note that release-asset install uses `curl` or `wget`, and document prerequisites. ## Draft Install Logic (to implement) + - **Policy (explicit)** - Primary command: `gwtt` (short and consistent with GoReleaser release assets). - Release-asset install: downloads and installs `gwtt` into the **current directory by default** (or a provided path). @@ -49,23 +53,27 @@ Create a dedicated install/uninstall experience that detects platform, downloads - **Download & install** - Use `curl -fsSL` or `wget -qO` to download to a temp dir. - Extract `gwtt` (or `gwtt.exe`) from the archive. - - Install to target dir (default current directory, or user-specified arg), ensure executable bit on *nix. + - Install to target dir (default current directory, or user-specified arg), ensure executable bit on \*nix. - Do not create symlinks or install man pages; document optional manual setup in README. ## Packaging Notes + - `.goreleaser.yml` should include man pages for manual install (even if the script does not auto-install them). - **Uninstall** - Remove installed `gwtt` (or `gwtt.exe`) from the target dir (default current directory). - Keep uninstall script simple and path-scoped; no global cleanup beyond installed files. ## Makefile Alignment + - Add new Makefile targets: `install` and `uninstall` for the release-asset scripts. - Keep existing Go-based install targets (e.g., `go-install`, `go-uninstall`) for developers or `go install` flow. ## Risks / Open Questions + - How to handle Windows PATH guidance for `.exe` installs. ## Success Criteria + - A single, clear install path that auto-selects platform/arch and fetches the correct asset. - README clearly explains binary naming and offers consistent commands. - Uninstall instructions remove installed artifacts cleanly. diff --git a/docs/plans/plan-2026-01-27-extensible-config.md b/docs/plans/plan-2026-01-27-extensible-config.md index af5b1c1..32d5480 100644 --- a/docs/plans/plan-2026-01-27-extensible-config.md +++ b/docs/plans/plan-2026-01-27-extensible-config.md @@ -6,20 +6,25 @@ agent: codex --- ## Goal + Ship an extensible config system beyond theme, prioritizing low-risk defaults and preserving current-branch semantics. ## Scope + - Config for UI and output defaults, confirmation toggles, and merge strategy. - No config default for `create.base` or `status/finish.target`. - Path templating only if `{task}` placeholder is enforced. ## Non-Goals + - No change to theme selection precedence. - No dynamic target defaults tied to repo state beyond current behavior. - No new dependency for config parsing beyond existing TOML usage. ## Plan + ### Phase 1: Design + - [x] Create schema doc in `docs/schemas/config-gwtt.md`. - [x] Define config schema for `ui`, `create`, `list`, `status`, `finish`, `cleanup` (exclude `base`/`target`). - [x] Decide whether to include `create.path.format` in this phase; if yes, require `{task}` and document reversibility constraints. @@ -27,12 +32,14 @@ Ship an extensible config system beyond theme, prioritizing low-risk defaults an - [x] Document env/config/flag precedence for new settings. ### Phase 2: Implementation + - [x] Add config loader with precedence: flags > env > project > user > defaults. - [x] Introduce `GWTT_COLOR` env (mirrors `--nocolor`, inverted boolean). - [x] Implement config-backed defaults for list/status output, grid, absolute path, confirm toggles. - [x] Add `merge_mode` enum and enforce exclusivity across CLI flags. ### Phase 3: Verification & Docs + - [x] Add tests for config resolution (env/project/user) and merge-mode validation. - [x] Update examples and README config section. - [x] Verify related docs status and update as phases complete: @@ -42,23 +49,28 @@ Ship an extensible config system beyond theme, prioritizing low-risk defaults an - [x] Job records reflect actual work status. ## Dependencies + - Current TOML parsing via `internal/config` (BurntSushi/toml). - CLI flag wiring in `cli/*` must remain backward compatible. - Worktree naming utilities in `internal/worktree/naming.go` if path templating is introduced. ## Acceptance Criteria + - CLI behavior is unchanged when no config is present. - `GWTT_COLOR` and config honor existing precedence rules. - Only one merge strategy is allowed at a time (`ff`, `no-ff`, `squash`, `rebase`). - Config defaults never override current-branch target selection. ## Risks / Notes + - Path templating can break `TaskFromPath` discovery unless `{task}` is enforced. - Merge-mode validation must stay consistent across flags and config to avoid drift. ## Process Notes + - Update doc statuses immediately after each task completes. - Use Phase 3 verification as a final guardrail to confirm status alignment. ## Related Research + - `docs/research-2026-01-27-extensible-config.md` diff --git a/docs/plans/plan-2026-01-27-fix-project-config-root-resolution.md b/docs/plans/plan-2026-01-27-fix-project-config-root-resolution.md index e602d67..9dab960 100644 --- a/docs/plans/plan-2026-01-27-fix-project-config-root-resolution.md +++ b/docs/plans/plan-2026-01-27-fix-project-config-root-resolution.md @@ -7,14 +7,17 @@ modified-date: 2026-01-27 --- ## Goal + Ensure project config (`gwtt.config.toml`) is resolved from the repo root even when `gwtt` is run from a subdirectory, preserving documented precedence and defaults. ## Scope + - Update config loader to resolve project config relative to the Git repo root (or upward search) instead of only `os.Getwd()`. - Keep precedence order consistent with existing docs and behavior. - Add/adjust tests to cover subdirectory invocation. ## Plan + 1. Identify current config resolution flow and where `os.Getwd()` anchors project config paths. 2. Reuse existing repo-root discovery logic (or add a shared helper) to resolve project config from the repo root or an upward search. 3. Update config loader to use the new root resolution while preserving precedence and error handling. @@ -22,6 +25,7 @@ Ensure project config (`gwtt.config.toml`) is resolved from the repo root even w 5. Update documentation if needed to clarify repo-root resolution rules. ## Success Criteria + - Running `gwtt` from any subdirectory in a repo applies `gwtt.config.toml` located at the repo root. - Config precedence matches documented behavior. - Tests cover subdirectory execution and pass. diff --git a/docs/plans/plan-2026-02-04-mode-classic-and-codex.md b/docs/plans/plan-2026-02-04-mode-classic-and-codex.md index b67c539..699d5cf 100644 --- a/docs/plans/plan-2026-02-04-mode-classic-and-codex.md +++ b/docs/plans/plan-2026-02-04-mode-classic-and-codex.md @@ -7,9 +7,11 @@ agent: codex --- ## Goal + Add a new global `--mode` (`classic` default, `codex` optional) to support Codex App-style worktrees while keeping current behavior stable and non-breaking. ## Scope + - `--mode` flag + `GWTT_MODE` env + config default (flag > env > config > default). - Codex-mode worktree discovery under `$CODEX_HOME/worktrees/**` with repo-scoped filtering via `git worktree list --porcelain`. - Codex-mode selection model: `` is the exact `` directory name under `$CODEX_HOME/worktrees`. @@ -19,12 +21,15 @@ Add a new global `--mode` (`classic` default, `codex` optional) to support Codex - Add `modified_time` to `status` output (RFC3339 UTC). ## Non-Goals + - No registry/state files (no `registry.json` / extra TOML) for codex mode. - No branch-based workflows in codex mode (no `finish` in codex mode; no `create --branch` in codex mode). - No `restore` command in this phase. ## Plan + ### Phase 1: Docs & Spec Design + - [x] Update `docs/research-2026-02-04-mode-classic-vs-codex.md` to reflect final decisions as implementation progresses. - [x] Update `docs/schemas/config-gwtt.md` to include `mode` and env var `GWTT_MODE`. - [x] Write a short CLI spec section (either in the research doc or a new schema doc) covering: @@ -34,6 +39,7 @@ Add a new global `--mode` (`classic` default, `codex` optional) to support Codex - [x] `cleanup` safety model (scope restriction, warnings, second confirmation, and “restore is best-effort” note). ### Phase 2: Code Implementation + - [x] Add global `--mode` persistent flag on `cli/root.go` and plumb mode into command execution (context/config). - [x] Add mode to config resolution: - [x] Env: `GWTT_MODE`. @@ -63,6 +69,7 @@ Add a new global `--mode` (`classic` default, `codex` optional) to support Codex - [x] Confirmed app-side worktree shell/run-script issues are out of CLI scope and tracked upstream. ### Phase 3: Unit Test Verification + - [x] Add tests for `mode` precedence and validation (flag/env/config/default). - [x] Add tests for codex-mode list/status filtering (repo-scoped via `git worktree list` + `$CODEX_HOME/worktrees` prefix filter). - [x] Add tests for `` derivation and path rendering (`$CODEX_HOME` display). @@ -71,6 +78,7 @@ Add a new global `--mode` (`classic` default, `codex` optional) to support Codex - [x] Add tests for codex cleanup scope restriction + confirmation flow. ### Phase 4: README / CLI Docs Update + - [x] Update `README.md`: - [x] Document `--mode`, `GWTT_MODE`, and config `mode`. - [x] Add codex-mode usage examples for `list/status/apply/cleanup`. @@ -79,11 +87,13 @@ Add a new global `--mode` (`classic` default, `codex` optional) to support Codex - [x] If applicable, update any man/help text sources under `man/` to reflect new commands/flags. ### Phase 5: Verify Doc Statuses + - [x] Ensure this plan’s `status` matches the actual phase progress (`active` -> `completed` when done). - [x] Update the research doc’s `status` to `completed` once decisions are implemented and verified. - [x] Ensure any schema/doc updates have consistent status and dates (`modified-date` as needed). ## Acceptance Criteria + - Default behavior (no `--mode`, no `GWTT_MODE`, no config) remains unchanged. - `--mode=codex` enables codex-specific list/status/apply/cleanup without impacting classic users. - Codex-mode selection uses `` reliably and errors clearly when not found/ambiguous. @@ -91,9 +101,11 @@ Add a new global `--mode` (`classic` default, `codex` optional) to support Codex - Codex-mode cleanup is narrowly scoped and always warns + confirms before deletion. ## Risks / Notes + - Codex App “pinned/sidebar/thread linkage” signals may not be detectable from disk without reading Codex’s internal state; default to warnings + a second confirmation when uncertain. - `apply` semantics are easy to get subtly wrong; keep the initial implementation conservative and well-tested. - Track open worktree issues and shell/run-script reports: https://github.com/openai/codex/issues?q=is%3Aissue%20state%3Aopen%20worktree (example: “Worktrees keep forgetting the "Run" script”, Feb 3, 2026). ## Related Research + - `docs/research-2026-02-04-mode-classic-vs-codex.md` diff --git a/docs/research-2026-01-12-themes-and-table.md b/docs/research-2026-01-12-themes-and-table.md index 4300130..555166e 100644 --- a/docs/research-2026-01-12-themes-and-table.md +++ b/docs/research-2026-01-12-themes-and-table.md @@ -6,9 +6,11 @@ agent: codex --- ## Goal + Identify 3–5 UI themes suitable for CLI/TUI output and propose a responsive table strategy for narrow terminals. ## Key Findings + - Theme candidates (5 total including default): - Default: balanced cyan/magenta highlights for general usage. - Nord: cool blue/ice palette for low-glare terminals. @@ -20,16 +22,20 @@ Identify 3–5 UI themes suitable for CLI/TUI output and propose a responsive ta - Bubble Tea `bubbles/table` is best used in TUI views with fixed column roles and width adjustments on `tea.WindowSizeMsg`. ## Implications or Recommendations + - Implement `--theme` for CLI/TUI with a small set of named palettes and a shared role-based style map. - Add width-aware truncation in CLI tables so narrow terminals remain usable without dropping essential columns. - For TUI, model the list view using `bubbles/table` and update column widths when the terminal resizes. ## Open Questions + - Should the theme list allow user-defined palettes (future config file)? - Which columns should be hidden when terminal width is extremely small (if truncation is insufficient)? ## References + - None (internal reasoning) ## Related Plans + - plans/plan-2026-01-12-themes-and-rwd.md diff --git a/docs/research-2026-01-12-worktree-ops-matrix.md b/docs/research-2026-01-12-worktree-ops-matrix.md index 55454dd..d1fdafd 100644 --- a/docs/research-2026-01-12-worktree-ops-matrix.md +++ b/docs/research-2026-01-12-worktree-ops-matrix.md @@ -6,9 +6,11 @@ agent: codex --- ## Goal + Define detailed, consistent operation behaviors for creating, merging, and cleaning up git worktrees tied to task-name branches. ## Key Findings + - A clean separation of concerns keeps the CLI predictable: (1) create worktree+branch, (2) merge into target, (3) cleanup (worktree, branch) with explicit flags. - The shell reference implies a naming convention: `../_` for worktree paths and a sanitized `` for branch names. We should codify the same logic with explicit validation. - Avoid implicit destructive actions: deletion of worktrees and branches should be explicit, or controlled with flags on `finish`. @@ -17,6 +19,7 @@ Define detailed, consistent operation behaviors for creating, merging, and clean ## Operations Matrix ### Create (worktree + branch) + - Input: `task` (required), `base` (optional, default `main`), `path` (optional). - Behavior: - Sanitize branch name from task (replace non `[A-Za-z0-9_/-]` with `-`). @@ -25,6 +28,7 @@ Define detailed, consistent operation behaviors for creating, merging, and clean - Command: `git worktree add -b `. ### Finish (merge + optional cleanup) + - Input: `task` (required), `target` (default `main`). - Behavior: - Ensure clean index in target branch before merge. @@ -36,6 +40,7 @@ Define detailed, consistent operation behaviors for creating, merging, and clean - Always `git worktree prune` when removing worktree. ### Cleanup (no merge) + - Input: `task` (required). - Flags: - `--remove-worktree` (default true) @@ -45,6 +50,7 @@ Define detailed, consistent operation behaviors for creating, merging, and clean - If `--remove-branch`, delete branch (`-d` by default, `-D` with `--force`). ### List / Status + - Input: none; optional filters by task or branch. - Modes: - Simple: show task, branch, path, and whether the worktree is present. @@ -55,24 +61,29 @@ Define detailed, consistent operation behaviors for creating, merging, and clean - Behavior: read from `git worktree list --porcelain` and map paths to tasks using the fixed naming convention. ## Safety Checks + - Ensure task worktree is not the current working directory before removal. - Provide `--dry-run` for every destructive command. - Require confirmation (or `--yes`) when deleting branches or worktrees. ## Edge Cases + - Branch exists but worktree missing: allow `cleanup --remove-branch`. - Worktree exists but branch deleted: allow `cleanup --remove-worktree` and print warning. - Merge conflicts: abort `finish` and keep worktree/branch intact. - Base/target branch doesn't exist locally: optionally fetch or error. ## Implications or Recommendations + - Provide a `validate` subcommand (or internal validator) to preflight operations. - Surface a `status`/`list` command to show active task worktrees. ## Open Questions + - (resolved) Worktree path is fixed to `../_`; task name should be slugified consistently. - (resolved) `finish` requires a second confirmation before deleting worktree/branch; provide bypass flag for full cleanup. Support combinations like remove-worktree-only. Confirm which worktree to remove is scoped to the task, not a global prune. - (resolved) Merge modes should support `--squash`, `--no-ff`, and `--rebase`; default is standard `git merge` behavior. ## Related Plans + - docs/plans/plan-2026-01-12-init-phase.md diff --git a/docs/research-2026-01-13-homebrew-integration.md b/docs/research-2026-01-13-homebrew-integration.md index f391fe9..4db292c 100644 --- a/docs/research-2026-01-13-homebrew-integration.md +++ b/docs/research-2026-01-13-homebrew-integration.md @@ -22,23 +22,27 @@ Homebrew is a popular package manager for macOS and Linux. It uses "formulas" (R ### Approach 1: Global Homebrew PR (homebrew-core) **Process:** + - Submit `git-worktree-tasks.rb` formula to `homebrew/homebrew-core` GitHub repository - Homebrew maintainers review and merge - Users install directly: `brew install git-worktree-tasks` **Advantages:** + - Single, standard installation method - Official Homebrew distribution - No additional repository to maintain - Higher discoverability **Disadvantages:** + - Longer review process (days to weeks) - Strict Homebrew guidelines and requirements - Less control over updates and versioning - Requires stable, tagged releases in main repository **Requirements:** + - Project must be stable and actively maintained - Clear, tagged releases (e.g., `v0.0.5`) - Open source with compatible license @@ -47,12 +51,14 @@ Homebrew is a popular package manager for macOS and Linux. It uses "formulas" (R ### Approach 2: Custom Tap Repository **Process:** + - Create separate GitHub repository: `pi2pie/homebrew-git-worktree-tasks` - Maintain `Formula/git-worktree-tasks.rb` in the tap repository - Users add tap first: `brew tap pi2pie/git-worktree-tasks` - Then install: `brew install git-worktree-tasks` **Advantages:** + - Complete control over formula and updates - Faster deployment (no review process) - Can update formula independently of main project @@ -60,12 +66,14 @@ Homebrew is a popular package manager for macOS and Linux. It uses "formulas" (R - Can include additional formulas if needed **Disadvantages:** + - Users must remember to tap first (extra step) - Requires maintaining additional repository - Lower discoverability compared to homebrew-core - User must manage tap updates separately **Requirements:** + - Separate GitHub repository - Proper repository naming convention - CI/CD to automate formula updates @@ -143,6 +151,7 @@ brew install git-worktree-tasks ``` One-liner variant: + ```bash brew install pi2pie/git-worktree-tasks/git-worktree-tasks ``` @@ -156,16 +165,19 @@ brew install git-worktree-tasks ## Implementation Roadmap ### Phase 1: Foundation (Current) + - ✅ Establish build process (Makefile) - ⏳ Set up stable CI/CD with goreleaser ### Phase 2: Custom Tap + - Create `homebrew-git-worktree-tasks` repository - Write and test `git-worktree-tasks.rb` formula - Document custom tap installation in README - Automate formula updates via CI/CD ### Phase 3: Homebrew Core (Optional, Future) + - Prepare project for homebrew-core submission - Follow Homebrew guidelines and standards - Submit PR to `homebrew/homebrew-core` diff --git a/docs/research-2026-01-13-toml-config-options.md b/docs/research-2026-01-13-toml-config-options.md index 68a2f27..1af668f 100644 --- a/docs/research-2026-01-13-toml-config-options.md +++ b/docs/research-2026-01-13-toml-config-options.md @@ -6,41 +6,56 @@ agent: codex --- ## Goal + Survey Go TOML/config library options and document pros/cons to select an implementation for theme configuration. ## Key Findings + - **Direct TOML parsers**: `BurntSushi/toml` and `pelletier/go-toml` provide TOML decoding with struct mapping; go-toml v2 includes CLI tools like `tomljson`, `jsontoml`, and `tomll`. [^burntsushi] [^go-toml] [^go-toml-tools] - **Full config frameworks**: `spf13/viper` supports TOML alongside env vars, flags, and multiple config sources/formats. [^viper-pkg] - **Lightweight config manager**: `knadh/koanf` is a modular, lightweight alternative to viper that composes providers (file/env/flags) and parsers (including TOML). [^koanf] ## Pros and Cons + ### BurntSushi/toml + - Pros: Small, focused TOML parser; standard struct decoding; supports current TOML versions; MIT license. [^burntsushi] [^burntsushi-pkg] - Cons: No built-in config layering/precedence; app must handle file discovery and env overrides (inference based on scope). [^burntsushi] ### pelletier/go-toml (v2) + - Pros: TOML v1.0 support; v2 provides tools (`tomljson`, `jsontoml`, `tomll`) and published benchmarks. [^go-toml] [^go-toml-tools] - Cons: Includes an "unstable" AST API; may be more than needed for a small config (inference). [^go-toml] ### spf13/viper + - Pros: Full config stack: files + env + flags + defaults; supports TOML and multiple formats. [^viper-pkg] - Cons: Larger dependency surface and broader feature set than required for a single `theme` setting (inference). [^viper-pkg] ### knadh/koanf + - Pros: Modular providers/parsers; lightweight alternative to viper; explicit merge order control; TOML parser available. [^koanf] - Cons: Requires selecting providers/parsers explicitly; more setup than a simple TOML unmarshal for one setting (inference). [^koanf] ## Implications or Recommendations + - For **minimal config (single theme)**, prefer a direct TOML parser and implement explicit precedence in our code. BurntSushi/toml or go-toml v2 are both viable; BurntSushi is simplest if we only need decoding. [^burntsushi] [^go-toml] - If future scope grows to include more config sources (flags/env/remote), evaluate koanf or viper then. [^koanf] [^viper-pkg] ## Related Plans + - `docs/plans/plan-2026-01-13-theme-config-and-env.md` ## References + [^burntsushi]: BurntSushi/toml repository and documentation. https://github.com/BurntSushi/toml + [^burntsushi-pkg]: BurntSushi/toml package docs (compatibility notes). https://pkg.go.dev/github.com/BurntSushi/toml + [^go-toml]: pelletier/go-toml repository (v2) and tools/benchmarks notes. https://github.com/pelletier/go-toml + [^go-toml-tools]: go-toml CLI tools docs. https://github.com/pelletier/go-toml/tree/master/cmd + [^viper-pkg]: Viper package docs (format support and config sources). https://pkg.go.dev/github.com/spf13/viper + [^koanf]: knadh/koanf repository (lightweight config, providers/parsers). https://github.com/knadh/koanf diff --git a/docs/research-2026-01-19-create-default-base.md b/docs/research-2026-01-19-create-default-base.md index 4cdb8a7..e857452 100644 --- a/docs/research-2026-01-19-create-default-base.md +++ b/docs/research-2026-01-19-create-default-base.md @@ -6,29 +6,36 @@ agent: Codex --- ## Goal + Define how `create` selects a base branch by default when invoked from different contexts, including older Git default branches (`master`) and detached HEAD cases. ## Key Findings + - Current behavior: `create` defaults to the current branch when on a named branch. - Detached HEAD requires an explicit `--base`. - Worktrees always share the same Git common directory; creating a worktree from another worktree does not create a new repo, it creates another worktree in the same repo. ## Implications or Recommendations + - Prefer base = current branch when on a named branch. - If detached HEAD, allow `create` only when `--base` is explicitly provided (flexible, explicit intent). - `--base` always overrides defaults. ## Open Questions + - None. (Detached HEAD: allow only with explicit `--base`; default base: follow current local branch.) ## Rationale Notes + - Detached HEAD behavior options: - Allow explicit `--base`: `create --base dev my-task` succeeds even when detached; explicit base removes ambiguity. - Require checkout: `create` fails on detached HEAD even with `--base`; stricter safety, less flexible. - Current behavior matches the explicit-`--base` rule, so detached HEAD without `--base` should produce a clear error. ## References + - None. ## Related Plans + - docs/plans/plan-2026-01-18-open-source-readiness.md diff --git a/docs/research-2026-01-27-extensible-config.md b/docs/research-2026-01-27-extensible-config.md index c4cf73a..4f3bbf3 100644 --- a/docs/research-2026-01-27-extensible-config.md +++ b/docs/research-2026-01-27-extensible-config.md @@ -6,9 +6,11 @@ agent: codex --- ## Goal + Identify optional configuration areas beyond theme that could be safely exposed to users, and outline a TOML shape that stays extensible and idiomatic for Go. ## Key Findings + - **Current config scope is theme-only** with fixed file locations and precedence (flag > env > project config > user config > default). This flow can be generalized for other settings without adding new dependencies. [^theme-config] [^readme-config] - **Most CLI flags map cleanly to defaults** that users might want to set once: output format (`list`, `status`, `create`), table grid, absolute paths, strict task matching, default targets, and cleanup behaviors. [^cli-create] [^cli-list] [^cli-status] [^cli-finish] [^cli-cleanup] - **Naming and path conventions are centralized** in `internal/worktree/naming.go`, which makes them a natural candidate for a configurable strategy (prefix, separator, or template) but requires compatibility safeguards because `TaskFromPath` assumes a fixed prefix format. [^worktree-naming] @@ -20,6 +22,7 @@ Identify optional configuration areas beyond theme that could be safely exposed - **Merge mode should be exclusive**; only one of `ff` (default), `no-ff`, `squash`, or `rebase` should be active to avoid ambiguous or conflicting semantics. [^cli-finish] ## Candidate Optional Config Sections + The following are low-risk candidates because they are already CLI flags or deterministic defaults: 1. **[ui]** @@ -57,6 +60,7 @@ The following are low-risk candidates because they are already CLI flags or dete - `confirm` ## Extensible TOML Shape (Draft) + A minimal, additive structure that avoids breaking existing `theme` parsing: ```toml @@ -106,10 +110,12 @@ confirm = true ``` Notes: + - Keep `theme` as-is to preserve compatibility. - `create.path.format` would require updating `TaskFromPath` logic or providing a parallel lookup strategy to avoid breaking list/status discovery for non-default naming. ## Implications or Recommendations + - **Start with config for existing CLI defaults** (output format, grid, absolute paths, confirm toggles) because they are low-risk and align with current option handling. - **Treat naming/path customization as a second phase** due to the coupling in `TaskFromPath` and potential mismatch with existing worktrees; consider supporting a templated suffix/prefix that still allows parsing. [^worktree-naming] - **Keep precedence consistent with theme** (flags > env > project > user > default) to reduce user confusion and keep logic predictable. [^theme-config] @@ -118,15 +124,25 @@ Notes: - **Prefer explicit, typed config structs** for each section to keep Go code simple and avoid "magic" config behavior. (Inference based on current code style.) ## Open Questions + - None. ## References + [^theme-config]: `internal/config/theme.go` + [^cli-root]: `cli/root.go` + [^cli-create]: `cli/create.go` + [^cli-list]: `cli/list.go` + [^cli-status]: `cli/status.go` + [^cli-finish]: `cli/finish.go` + [^cli-cleanup]: `cli/cleanup.go` + [^worktree-naming]: `internal/worktree/naming.go` + [^readme-config]: `README.md` diff --git a/docs/research-2026-02-04-mode-classic-vs-codex.md b/docs/research-2026-02-04-mode-classic-vs-codex.md index 5865337..e97d984 100644 --- a/docs/research-2026-02-04-mode-classic-vs-codex.md +++ b/docs/research-2026-02-04-mode-classic-vs-codex.md @@ -7,11 +7,13 @@ agent: codex --- ## Goal + Define what a new global `--mode` flag should mean for this CLI, so we can support Codex App-style worktrees without breaking the current (“classic”) behavior. ## Key Findings ### Current CLI behavior (“classic”) + - **Create** couples “task” to both branch and path: - Branch: `` (slugified). - Path: `../_` (relative to repo root’s parent). @@ -19,7 +21,9 @@ Define what a new global `--mode` flag should mean for this CLI, so we can suppo - **Paths are displayed** relative to the repo root by default; `--abs`/`--absolute-path` shows absolute paths. ### Codex App worktree behavior (“codex”) + Based on Codex App documentation (and current app UI), the worktree model is intentionally different from this CLI’s task/branch model: + - **Worktree location is not per-worktree user-chosen**: worktrees are created under `$CODEX_HOME/worktrees` so the app can manage them consistently. - **Worktrees start in detached HEAD** by default (to avoid Git’s restriction that a branch cannot be checked out in two worktrees at once). - **Local changes may be applied** when the worktree is created from an existing local branch with uncommitted changes. @@ -32,12 +36,15 @@ Based on Codex App documentation (and current app UI), the worktree model is int - **Cleanup is app-governed and tied to threads**: the Codex app cleans up worktrees when you archive threads (or on startup for worktrees with no associated threads), and it preserves a snapshot for later restore. ### Codex App FAQ takeaways (constraints we should mirror) + - Worktrees are created under `$CODEX_HOME/worktrees` so Codex can manage them consistently. - Sessions cannot be moved between worktrees: to change environments, you start a new thread in the target environment and restate the prompt. - Threads can remain even if the worktree directory is cleaned up; Codex snapshots work before cleanup and can offer restore when reopening the thread. ### Decisions for `--mode=codex` (CLI alignment) + To keep `classic` stable and keep `codex` aligned with Codex App: + - **Identity & mapping via inspection (no extra registry files):** - Do not introduce new state files like `registry.json`/TOML for codex mode. - For `list`/`status`, inspect `$CODEX_HOME/worktrees/**` (and/or `git worktree list --porcelain` scoped to the local checkout) to discover worktrees and compute status. @@ -51,22 +58,26 @@ To keep `classic` stable and keep `codex` aligned with Codex App: - **Cleanup in codex mode:** free disk by deleting the on-disk directory under `$CODEX_HOME/worktrees/`, but only when it is safe: - Skip worktrees that fall under Codex App’s “never clean up if …” restrictions. - If we cannot verify a restriction (e.g., pinned/sidebar linkage), still allow deletion but show a prominent warning and require a second confirmation (skippable with `--yes`). - - Treat “Codex can restore later” as best-effort: Codex App snapshots before *its own* cleanup; `gwtt` cannot guarantee a snapshot exists before manual deletion. + - Treat “Codex can restore later” as best-effort: Codex App snapshots before _its own_ cleanup; `gwtt` cannot guarantee a snapshot exists before manual deletion. ### Practical restrictions implied by `--mode=codex` + - **No “task branch” assumption:** detached worktrees mean we can’t infer branch names from task names. - **No arbitrary `--path` override:** codex-mode worktrees live under `$CODEX_HOME/worktrees`; allowing arbitrary paths would complicate cleanup, display, and registry invariants. - **Different command surface:** branch-merge workflows (`finish`) are replaced by apply workflows (`apply` / `overwrite`). - **Cleanup restrictions from Codex App:** Codex App’s auto-cleanup is disabled in some cases (e.g., pinned conversation, added to sidebar, age > 4 days, worktree count > 10). - Note: the “age > 4 days” / “count > 10” conditions are counterintuitive, but this is the wording in the official docs as of 2026-02-04. - - **Detached HEAD as codex marker:** codex-mode worktrees are detached; classic-mode worktrees are expected to be on a branch. Use this to keep classic commands from “seeing” codex worktrees. +- **Detached HEAD as codex marker:** codex-mode worktrees are detached; classic-mode worktrees are expected to be on a branch. Use this to keep classic commands from “seeing” codex worktrees. ### Path display differences (UX) + Codex App uses a “variable-aware” presentation of paths (and the user specifically called out `$CODEX_HOME`): + - In `codex` mode, prefer showing worktree paths under `$CODEX_HOME` as `$CODEX_HOME/...` rather than a long absolute path. - In both modes, consider shortening the home directory to `~` (or `$HOME`) when rendering absolute paths. ## Implications or Recommendations + - Add a global flag `--mode` with values: - `classic` (default): current behavior and naming convention. - `codex`: Codex App-aligned behavior (detached worktrees, `$CODEX_HOME` root, ID-based mapping, apply-oriented workflow). @@ -89,7 +100,7 @@ Codex App uses a “variable-aware” presentation of paths (and the user specif - Only target paths under `$CODEX_HOME/worktrees/` (no arbitrary deletion). - Attempt to mirror Codex App’s “never clean up if …” rules; if we cannot verify a rule (e.g., pinned/sidebar linkage), show a prominent warning and require a second confirmation (skippable with `--yes`). - Repo scoping in codex mode should come from Git, not naming: - - To list/status only the Codex worktrees for the *current repo*, run `git -C worktree list --porcelain` and include only entries whose `worktree` path is under `$CODEX_HOME/worktrees/`. + - To list/status only the Codex worktrees for the _current repo_, run `git -C worktree list --porcelain` and include only entries whose `worktree` path is under `$CODEX_HOME/worktrees/`. - This avoids relying on the opaque directory name to encode repo identity. - Consider adding `modified_time` to `status` (and optionally `list`) rows: - Use the filesystem `mtime` of the worktree directory as a pragmatic “last touched” signal. @@ -98,27 +109,32 @@ Codex App uses a “variable-aware” presentation of paths (and the user specif ## CLI Spec (Draft) ### Mode resolution + - Flag: `--mode` (`classic` or `codex`). - Env: `GWTT_MODE`. - Config: top-level `mode = "classic"|"codex"`. - Default: `classic`. ### Codex home + worktrees root + - Resolve Codex home from the `CODEX_HOME` env var; if unset, default to `~/.codex` (Codex App default). - Managed codex-mode worktrees are always under `$CODEX_HOME/worktrees/`. - Display paths under this root as `$CODEX_HOME/...` by default (unless `--abs` forces absolute). ### Selection model (`` in codex mode) + - `` is the **exact opaque ID** directory name under `$CODEX_HOME/worktrees` (the first path segment). - `gwtt list/status --mode=codex` should render `TASK=` to make the identifier discoverable and copy/paste friendly. ### Repo scoping (`list/status` in codex mode) + - Use Git as the source of truth for “worktrees belonging to this repo”: - Run `git -C worktree list --porcelain`. - Filter entries whose worktree path is under `$CODEX_HOME/worktrees/`. - Do not attempt to infer repo identity from `` naming. ### `apply` (codex mode) + - CLI: `gwtt apply ` (default operation: apply worktree changes into the local checkout). - Conflict detection signals (predictable, conservative): - Local checkout is dirty, or @@ -128,36 +144,44 @@ Codex App uses a “variable-aware” presentation of paths (and the user specif - Keep Codex App parity: ignored files are not transferred. ### `cleanup` (codex mode) + - Scope: only delete the on-disk directory at `$CODEX_HOME/worktrees/` (free disk; do not touch classic paths). - Since pinned/sidebar/thread linkage is not reliably detectable without reading Codex App state: - Always show a prominent warning (“may break pinned/sidebar restore expectations”) and require an extra confirmation (skippable with `--yes`). - Treat restore as best-effort: Codex saves a snapshot **before its own cleanup**; `gwtt`-initiated deletion cannot guarantee a snapshot exists unless Codex already took one. ### `status` metadata addition: `modified_time` + - Add `modified_time` derived from filesystem `mtime` of the worktree directory. - Format: RFC3339 UTC for JSON/CSV; table output prints the same string. - No date-format config in the first iteration. ### Raw output (codex mode) + - `--output raw` should return a **composable path** (relative to `$CODEX_HOME`), e.g. `worktrees/bf15/git-worktree-tasks`. - Display output (table/text) should continue to render `$CODEX_HOME/...` for readability. ## Open Questions (Remaining) + - Can we (safely) detect any Codex cleanup-restriction signals from disk without coupling `gwtt` to Codex’s internal storage formats? - Likely no; default to warnings + a second confirmation (skippable with `--yes`) for codex cleanup. - What is the most user-friendly confirmation wording for “overwrite” (apply) and “yolo delete” (cleanup) that still prevents accidents? - Pending: depends on user confidence that Codex can restore a worktree after manual deletion (docs only guarantee snapshots before **Codex-managed** cleanup). ## Notes + - Restoration remains out of scope: keep to create/apply/list/status only for now; a future `restore` likely needs to integrate with Codex App snapshot state. ## Open Issues (Worktrees + Shell/Run Scripts) + - App-side issues only (not CLI behavior). Track open worktree-related issues: https://github.com/openai/codex/issues?q=is%3Aissue%20state%3Aopen%20worktree - Recent example: “Codex app: Worktrees keep forgetting the "Run" script” (open, Feb 3, 2026). https://github.com/openai/codex/issues/10476 ## References + - Codex App worktrees documentation (includes cleanup + FAQ): https://developers.openai.com/codex/app/worktrees/ - Git worktree manual: https://git-scm.com/docs/git-worktree ## Related Plans + - `docs/plans/plan-2026-02-04-mode-classic-and-codex.md` diff --git a/docs/schemas/config-gwtt.md b/docs/schemas/config-gwtt.md index 8813c47..e93f0e7 100644 --- a/docs/schemas/config-gwtt.md +++ b/docs/schemas/config-gwtt.md @@ -7,9 +7,11 @@ agent: codex --- ## Goal + Define the authoritative configuration schema for `gwtt`, including keys, types, defaults, and precedence. ## Precedence + 1. CLI flags 2. Environment variables 3. Project config (`gwtt.config.toml` or `gwtt.toml`) @@ -17,6 +19,7 @@ Define the authoritative configuration schema for `gwtt`, including keys, types, 5. Built-in defaults ## Environment variables + - `GWTT_THEME` overrides `[theme].name`. - `GWTT_COLOR` overrides `[ui].color_enabled`. - `GWTT_MODE` overrides `mode`. @@ -25,29 +28,37 @@ Define the authoritative configuration schema for `gwtt`, including keys, types, - Note: confirm the default path against current Codex App/Codex CLI docs when implementing (and prefer matching their behavior over introducing a new `gwtt`-specific default). ## Schema + ### Root + - `mode` (string enum: `classic`, `codex`; default: `classic`) ### `[theme]` + - `name` (string, default: `"default"`) ### `[ui]` + - `color_enabled` (bool, default: `true`) ### `[table]` + - `grid` (bool, default: `false`) ### `[create]` + - `output` (string enum: `text`, `raw`; default: `text`) - `skip_existing` (bool, default: `false`) #### `[create.path]` + - `root` (string, default: `"../"`) - `format` (string, default: `"{repo}_{task}"`) - Must include `{task}`. - `{repo}` is optional. ### `[list]` + - `output` (string enum: `table`, `json`, `csv`, `raw`; default: `table`) - `field` (string enum: `path`, `task`, `branch`; default: `path`) - `absolute_path` (bool, default: `false`) @@ -55,12 +66,14 @@ Define the authoritative configuration schema for `gwtt`, including keys, types, - `strict` (bool, default: `false`) ### `[status]` + - `output` (string enum: `table`, `json`, `csv`; default: `table`) - `absolute_path` (bool, default: `false`) - `grid` (bool, default: `false`) - `strict` (bool, default: `false`) ### `[finish]` + - `cleanup` (bool, default: `false`) - `remove_worktree` (bool, default: `false`) - `remove_branch` (bool, default: `false`) @@ -70,6 +83,7 @@ Define the authoritative configuration schema for `gwtt`, including keys, types, - `false` bypasses prompts (equivalent to `--yes`). #### Merge strategy mapping + - `ff` (default): no merge flags. - `no-ff`: adds `--no-ff`. - `squash`: adds `--squash`. @@ -77,6 +91,7 @@ Define the authoritative configuration schema for `gwtt`, including keys, types, - Exactly one merge strategy may be active at a time; config and flags must agree. ### `[cleanup]` + - `remove_worktree` (bool, default: `true`) - `remove_branch` (bool, default: `true`) - `worktree_only` (bool, default: `false`) @@ -85,12 +100,14 @@ Define the authoritative configuration schema for `gwtt`, including keys, types, - `false` bypasses prompts (equivalent to `--yes`). ## Decisions + - `create.path.format` must include `{task}` to preserve task discovery. - `merge_mode` is exclusive; only one strategy may be active at a time. - Codex-mode uses an `apply` command for hand-off changes; there are no config keys for it yet. - No config defaults for `create.base` or `status/finish.target`. ## Examples + ```toml mode = "classic" From 08fdb1377ac9cb0675574b424296bc4e1e3c6dda Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 00:30:43 +0800 Subject: [PATCH 02/15] Align docs date policy --- .../plans/jobs/2026-01-12-cleanup-behavior-and-branch-output.md | 2 +- docs/plans/jobs/2026-01-12-color-output.md | 2 +- docs/plans/jobs/2026-01-12-create-existing-worktree-and-path.md | 2 +- docs/plans/jobs/2026-01-12-create-output-and-clipboard.md | 2 +- docs/plans/jobs/2026-01-12-create-raw-output.md | 2 +- docs/plans/jobs/2026-01-12-disable-default-completion.md | 2 +- docs/plans/jobs/2026-01-12-init-implementation.md | 2 +- docs/plans/jobs/2026-01-12-themes-and-rwd.md | 2 +- docs/plans/jobs/2026-01-13-align-output-flags.md | 2 +- docs/plans/jobs/2026-01-13-create-auto-detect-branch.md | 2 +- docs/plans/jobs/2026-01-13-deprecate-copy-cd-and-task-flag.md | 2 +- docs/plans/jobs/2026-01-13-implement-gwtt-workflow.md | 2 +- docs/plans/jobs/2026-01-13-lint-and-cleanup.md | 2 +- docs/plans/jobs/2026-01-13-list-status-search.md | 2 +- docs/plans/jobs/2026-01-13-readme-usage-guide-refinement.md | 2 +- docs/plans/jobs/2026-01-13-remove-copy-cd-and-list-task.md | 2 +- docs/plans/jobs/2026-01-13-theme-config-and-env.md | 2 +- docs/plans/jobs/2026-01-13-verify-root-command-config.md | 2 +- docs/plans/jobs/2026-01-13-worktree-raw-fallback-impl.md | 2 +- docs/plans/jobs/2026-01-18-transfer-module-path.md | 2 +- docs/plans/jobs/2026-01-19-add-man-page-generation.md | 2 +- docs/plans/jobs/2026-01-19-cleanup-skip-main-worktree.md | 2 +- docs/plans/jobs/2026-01-19-consolidate-normalize-path.md | 2 +- .../jobs/2026-01-19-create-skip-existing-branch-message.md | 2 +- docs/plans/jobs/2026-01-19-dynamic-short-hash-length.md | 2 +- docs/plans/jobs/2026-01-19-empty-history-handling.md | 2 +- .../jobs/2026-01-19-fix-cleanup-yes-worktree-resolution.md | 2 +- docs/plans/jobs/2026-01-19-fix-goreleaser-previous-tag.md | 2 +- docs/plans/jobs/2026-01-19-integration-tests-ci.md | 2 +- docs/plans/jobs/2026-01-19-update-go-install-man-fallback.md | 2 +- docs/plans/jobs/2026-01-21-release-workflow-improvements.md | 2 +- docs/plans/jobs/2026-01-26-cli-context-and-status.md | 2 +- docs/plans/jobs/2026-01-27-config-grid-fallback.md | 2 +- docs/plans/jobs/2026-01-27-config-refactor-grid-flags.md | 2 +- docs/plans/jobs/2026-01-27-extensible-config-schema.md | 2 +- .../plans/jobs/2026-01-27-fix-project-config-root-resolution.md | 2 +- docs/plans/jobs/2026-01-27-phase2-config-loader.md | 2 +- docs/plans/jobs/2026-01-27-phase3-docs.md | 2 +- docs/plans/jobs/2026-01-27-phase3-tests.md | 2 +- docs/plans/jobs/2026-01-27-project-root-tests.md | 2 +- docs/plans/jobs/2026-02-04-codex-apply-terminology.md | 2 +- docs/plans/jobs/2026-02-04-fix-codex-opaque-id-and-raw-path.md | 2 +- docs/plans/jobs/2026-02-04-fix-codex-raw-paths.md | 2 +- docs/plans/jobs/2026-02-04-fix-codex-raw-relative-to-cwd.md | 2 +- docs/plans/jobs/2026-02-04-fuzzy-first-match-list-status.md | 2 +- docs/plans/jobs/2026-02-04-implement-mode-flag-phase2.md | 2 +- docs/plans/jobs/2026-02-04-phase-3-tests.md | 2 +- docs/plans/jobs/2026-02-04-skill-location-refactor.md | 2 +- .../plan-2026-01-12-cleanup-modes-and-branch-visibility.md | 2 +- docs/plans/plan-2026-01-12-color-output.md | 2 +- docs/plans/plan-2026-01-12-init-phase.md | 2 +- docs/plans/plan-2026-01-12-path-display-readme.md | 2 +- docs/plans/plan-2026-01-12-themes-and-rwd.md | 2 +- docs/plans/plan-2026-01-13-build-and-distribution-flow.md | 2 +- docs/plans/plan-2026-01-13-cicd-release-flow.md | 2 +- docs/plans/plan-2026-01-13-lint-and-cleanup.md | 2 +- docs/plans/plan-2026-01-13-list-copy-flag.md | 2 +- docs/plans/plan-2026-01-13-theme-config-and-env.md | 2 +- docs/plans/plan-2026-01-13-themes-flag.md | 2 +- docs/plans/plan-2026-01-13-worktree-raw-fallback.md | 2 +- docs/plans/plan-2026-01-18-module-path-transfer.md | 2 +- docs/plans/plan-2026-01-18-open-source-readiness.md | 2 +- docs/plans/plan-2026-01-21-release-workflow-improvements.md | 2 +- docs/plans/plan-2026-01-26-go-cli-context-and-status.md | 2 +- docs/plans/plan-2026-01-26-install-script-and-readme.md | 2 +- docs/plans/plan-2026-01-27-extensible-config.md | 2 +- .../plans/plan-2026-01-27-fix-project-config-root-resolution.md | 2 +- docs/plans/plan-2026-02-04-mode-classic-and-codex.md | 2 +- docs/research-2026-01-12-themes-and-table.md | 2 +- docs/research-2026-01-12-worktree-ops-matrix.md | 2 +- docs/research-2026-01-13-homebrew-integration.md | 2 +- docs/research-2026-01-13-toml-config-options.md | 2 +- docs/research-2026-01-19-create-default-base.md | 2 +- docs/research-2026-01-27-extensible-config.md | 2 +- docs/research-2026-02-04-mode-classic-vs-codex.md | 2 +- docs/schemas/config-gwtt.md | 2 +- 76 files changed, 76 insertions(+), 76 deletions(-) diff --git a/docs/plans/jobs/2026-01-12-cleanup-behavior-and-branch-output.md b/docs/plans/jobs/2026-01-12-cleanup-behavior-and-branch-output.md index e3022a5..67a41f4 100644 --- a/docs/plans/jobs/2026-01-12-cleanup-behavior-and-branch-output.md +++ b/docs/plans/jobs/2026-01-12-cleanup-behavior-and-branch-output.md @@ -1,6 +1,6 @@ --- title: "Cleanup behavior and branch output updates" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-12-color-output.md b/docs/plans/jobs/2026-01-12-color-output.md index ac14ce3..f2c79cf 100644 --- a/docs/plans/jobs/2026-01-12-color-output.md +++ b/docs/plans/jobs/2026-01-12-color-output.md @@ -1,6 +1,6 @@ --- title: "Implement CLI color output and --nocolor flag" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-12-create-existing-worktree-and-path.md b/docs/plans/jobs/2026-01-12-create-existing-worktree-and-path.md index 17f69c4..2833473 100644 --- a/docs/plans/jobs/2026-01-12-create-existing-worktree-and-path.md +++ b/docs/plans/jobs/2026-01-12-create-existing-worktree-and-path.md @@ -1,6 +1,6 @@ --- title: "Create existing worktree handling and path override" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-12-create-output-and-clipboard.md b/docs/plans/jobs/2026-01-12-create-output-and-clipboard.md index 7c9dfa2..9d5a616 100644 --- a/docs/plans/jobs/2026-01-12-create-output-and-clipboard.md +++ b/docs/plans/jobs/2026-01-12-create-output-and-clipboard.md @@ -1,6 +1,6 @@ --- title: "Create output and clipboard support" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-12-create-raw-output.md b/docs/plans/jobs/2026-01-12-create-raw-output.md index 22536bb..ae527bb 100644 --- a/docs/plans/jobs/2026-01-12-create-raw-output.md +++ b/docs/plans/jobs/2026-01-12-create-raw-output.md @@ -1,6 +1,6 @@ --- title: "Create raw output mode" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-12-disable-default-completion.md b/docs/plans/jobs/2026-01-12-disable-default-completion.md index f2c54af..7dda6a2 100644 --- a/docs/plans/jobs/2026-01-12-disable-default-completion.md +++ b/docs/plans/jobs/2026-01-12-disable-default-completion.md @@ -1,6 +1,6 @@ --- title: "Disable default completion subcommand" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-12-init-implementation.md b/docs/plans/jobs/2026-01-12-init-implementation.md index 9fb4839..a215761 100644 --- a/docs/plans/jobs/2026-01-12-init-implementation.md +++ b/docs/plans/jobs/2026-01-12-init-implementation.md @@ -1,6 +1,6 @@ --- title: "Init Phase Implementation" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-12-themes-and-rwd.md b/docs/plans/jobs/2026-01-12-themes-and-rwd.md index 030b598..59694e7 100644 --- a/docs/plans/jobs/2026-01-12-themes-and-rwd.md +++ b/docs/plans/jobs/2026-01-12-themes-and-rwd.md @@ -1,6 +1,6 @@ --- title: "Add theme selection and responsive tables" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-13-align-output-flags.md b/docs/plans/jobs/2026-01-13-align-output-flags.md index cfc05e2..478faad 100644 --- a/docs/plans/jobs/2026-01-13-align-output-flags.md +++ b/docs/plans/jobs/2026-01-13-align-output-flags.md @@ -1,6 +1,6 @@ --- title: "Align output flags and list filtering" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-13-create-auto-detect-branch.md b/docs/plans/jobs/2026-01-13-create-auto-detect-branch.md index c748a4b..1116470 100644 --- a/docs/plans/jobs/2026-01-13-create-auto-detect-branch.md +++ b/docs/plans/jobs/2026-01-13-create-auto-detect-branch.md @@ -1,6 +1,6 @@ --- title: "Auto-detect existing branch on create" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-13-deprecate-copy-cd-and-task-flag.md b/docs/plans/jobs/2026-01-13-deprecate-copy-cd-and-task-flag.md index 022a45b..e8459f3 100644 --- a/docs/plans/jobs/2026-01-13-deprecate-copy-cd-and-task-flag.md +++ b/docs/plans/jobs/2026-01-13-deprecate-copy-cd-and-task-flag.md @@ -1,6 +1,6 @@ --- title: "Deprecate copy-cd and list --task" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-13-implement-gwtt-workflow.md b/docs/plans/jobs/2026-01-13-implement-gwtt-workflow.md index d36c1f6..5b7298b 100644 --- a/docs/plans/jobs/2026-01-13-implement-gwtt-workflow.md +++ b/docs/plans/jobs/2026-01-13-implement-gwtt-workflow.md @@ -1,6 +1,6 @@ --- title: "Implement build and installation workflow for gwtt command" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: Zed Agent --- diff --git a/docs/plans/jobs/2026-01-13-lint-and-cleanup.md b/docs/plans/jobs/2026-01-13-lint-and-cleanup.md index 0e3df3a..ea3d662 100644 --- a/docs/plans/jobs/2026-01-13-lint-and-cleanup.md +++ b/docs/plans/jobs/2026-01-13-lint-and-cleanup.md @@ -1,6 +1,6 @@ --- title: "Lint and Cleanup Implementation" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: GitHub Copilot --- diff --git a/docs/plans/jobs/2026-01-13-list-status-search.md b/docs/plans/jobs/2026-01-13-list-status-search.md index 5b43dd9..67f1e48 100644 --- a/docs/plans/jobs/2026-01-13-list-status-search.md +++ b/docs/plans/jobs/2026-01-13-list-status-search.md @@ -1,6 +1,6 @@ --- title: "Add list/status search and repo base name resolution" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-13-readme-usage-guide-refinement.md b/docs/plans/jobs/2026-01-13-readme-usage-guide-refinement.md index 5638d80..7c40b53 100644 --- a/docs/plans/jobs/2026-01-13-readme-usage-guide-refinement.md +++ b/docs/plans/jobs/2026-01-13-readme-usage-guide-refinement.md @@ -1,6 +1,6 @@ --- title: "README Usage Guide Refinement" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: GitHub Copilot --- diff --git a/docs/plans/jobs/2026-01-13-remove-copy-cd-and-list-task.md b/docs/plans/jobs/2026-01-13-remove-copy-cd-and-list-task.md index de6573e..06f6d84 100644 --- a/docs/plans/jobs/2026-01-13-remove-copy-cd-and-list-task.md +++ b/docs/plans/jobs/2026-01-13-remove-copy-cd-and-list-task.md @@ -1,6 +1,6 @@ --- title: "Remove create copy-cd and list --task" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-13-theme-config-and-env.md b/docs/plans/jobs/2026-01-13-theme-config-and-env.md index 28c05c3..5b60c4c 100644 --- a/docs/plans/jobs/2026-01-13-theme-config-and-env.md +++ b/docs/plans/jobs/2026-01-13-theme-config-and-env.md @@ -1,6 +1,6 @@ --- title: "Implement theme config and env" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-13-verify-root-command-config.md b/docs/plans/jobs/2026-01-13-verify-root-command-config.md index 3b9f6a6..ff262a6 100644 --- a/docs/plans/jobs/2026-01-13-verify-root-command-config.md +++ b/docs/plans/jobs/2026-01-13-verify-root-command-config.md @@ -1,6 +1,6 @@ --- title: "Verify root command config with gwtt alias" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: Zed Agent --- diff --git a/docs/plans/jobs/2026-01-13-worktree-raw-fallback-impl.md b/docs/plans/jobs/2026-01-13-worktree-raw-fallback-impl.md index ab9d9c5..b6e34ff 100644 --- a/docs/plans/jobs/2026-01-13-worktree-raw-fallback-impl.md +++ b/docs/plans/jobs/2026-01-13-worktree-raw-fallback-impl.md @@ -1,6 +1,6 @@ --- title: "Implement raw output fallback" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-18-transfer-module-path.md b/docs/plans/jobs/2026-01-18-transfer-module-path.md index 89b1be9..cc3e78c 100644 --- a/docs/plans/jobs/2026-01-18-transfer-module-path.md +++ b/docs/plans/jobs/2026-01-18-transfer-module-path.md @@ -1,6 +1,6 @@ --- title: "Transfer module path to pi2pie and bump version" -date: 2026-01-18 +created-date: 2026-01-18 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-19-add-man-page-generation.md b/docs/plans/jobs/2026-01-19-add-man-page-generation.md index ef0a516..adf4d86 100644 --- a/docs/plans/jobs/2026-01-19-add-man-page-generation.md +++ b/docs/plans/jobs/2026-01-19-add-man-page-generation.md @@ -1,6 +1,6 @@ --- title: "Add man(1) generation and install wiring" -date: 2026-01-19 +created-date: 2026-01-19 status: completed agent: Codex --- diff --git a/docs/plans/jobs/2026-01-19-cleanup-skip-main-worktree.md b/docs/plans/jobs/2026-01-19-cleanup-skip-main-worktree.md index 0a47853..3cf7aed 100644 --- a/docs/plans/jobs/2026-01-19-cleanup-skip-main-worktree.md +++ b/docs/plans/jobs/2026-01-19-cleanup-skip-main-worktree.md @@ -1,6 +1,6 @@ --- title: "Skip main worktree when resolving cleanup by branch" -date: 2026-01-19 +created-date: 2026-01-19 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-19-consolidate-normalize-path.md b/docs/plans/jobs/2026-01-19-consolidate-normalize-path.md index 6c6c043..b840d16 100644 --- a/docs/plans/jobs/2026-01-19-consolidate-normalize-path.md +++ b/docs/plans/jobs/2026-01-19-consolidate-normalize-path.md @@ -1,6 +1,6 @@ --- title: "Consolidate duplicate path normalization" -date: 2026-01-19 +created-date: 2026-01-19 status: completed agent: Zed Agent --- diff --git a/docs/plans/jobs/2026-01-19-create-skip-existing-branch-message.md b/docs/plans/jobs/2026-01-19-create-skip-existing-branch-message.md index 538d04d..5830fa0 100644 --- a/docs/plans/jobs/2026-01-19-create-skip-existing-branch-message.md +++ b/docs/plans/jobs/2026-01-19-create-skip-existing-branch-message.md @@ -1,6 +1,6 @@ --- title: "Create skip-existing branch messaging" -date: 2026-01-19 +created-date: 2026-01-19 status: completed agent: Codex --- diff --git a/docs/plans/jobs/2026-01-19-dynamic-short-hash-length.md b/docs/plans/jobs/2026-01-19-dynamic-short-hash-length.md index 055797b..5441024 100644 --- a/docs/plans/jobs/2026-01-19-dynamic-short-hash-length.md +++ b/docs/plans/jobs/2026-01-19-dynamic-short-hash-length.md @@ -1,6 +1,6 @@ --- title: "Dynamic short hash length" -date: 2026-01-19 +created-date: 2026-01-19 status: completed agent: "Codex, GitHub Copilot" --- diff --git a/docs/plans/jobs/2026-01-19-empty-history-handling.md b/docs/plans/jobs/2026-01-19-empty-history-handling.md index a043952..91c644b 100644 --- a/docs/plans/jobs/2026-01-19-empty-history-handling.md +++ b/docs/plans/jobs/2026-01-19-empty-history-handling.md @@ -1,6 +1,6 @@ --- title: "Empty-history handling and friendly errors" -date: 2026-01-19 +created-date: 2026-01-19 status: completed agent: Codex --- diff --git a/docs/plans/jobs/2026-01-19-fix-cleanup-yes-worktree-resolution.md b/docs/plans/jobs/2026-01-19-fix-cleanup-yes-worktree-resolution.md index b54ae58..9a07d58 100644 --- a/docs/plans/jobs/2026-01-19-fix-cleanup-yes-worktree-resolution.md +++ b/docs/plans/jobs/2026-01-19-fix-cleanup-yes-worktree-resolution.md @@ -1,6 +1,6 @@ --- title: "Fix cleanup worktree resolution" -date: 2026-01-19 +created-date: 2026-01-19 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-19-fix-goreleaser-previous-tag.md b/docs/plans/jobs/2026-01-19-fix-goreleaser-previous-tag.md index d1329ae..90fd741 100644 --- a/docs/plans/jobs/2026-01-19-fix-goreleaser-previous-tag.md +++ b/docs/plans/jobs/2026-01-19-fix-goreleaser-previous-tag.md @@ -1,6 +1,6 @@ --- title: "Fix GoReleaser previous tag detection" -date: 2026-01-19 +created-date: 2026-01-19 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-19-integration-tests-ci.md b/docs/plans/jobs/2026-01-19-integration-tests-ci.md index 9554443..9ebf3aa 100644 --- a/docs/plans/jobs/2026-01-19-integration-tests-ci.md +++ b/docs/plans/jobs/2026-01-19-integration-tests-ci.md @@ -1,6 +1,6 @@ --- title: "Integration tests and CI workflow" -date: 2026-01-19 +created-date: 2026-01-19 status: completed agent: Codex --- diff --git a/docs/plans/jobs/2026-01-19-update-go-install-man-fallback.md b/docs/plans/jobs/2026-01-19-update-go-install-man-fallback.md index 981acd0..716c73c 100644 --- a/docs/plans/jobs/2026-01-19-update-go-install-man-fallback.md +++ b/docs/plans/jobs/2026-01-19-update-go-install-man-fallback.md @@ -1,6 +1,6 @@ --- title: "Update go-install man page fallback" -date: 2026-01-19 +created-date: 2026-01-19 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-21-release-workflow-improvements.md b/docs/plans/jobs/2026-01-21-release-workflow-improvements.md index 27a6228..a4950d9 100644 --- a/docs/plans/jobs/2026-01-21-release-workflow-improvements.md +++ b/docs/plans/jobs/2026-01-21-release-workflow-improvements.md @@ -1,6 +1,6 @@ --- title: "Release workflow improvements" -date: 2026-01-21 +created-date: 2026-01-21 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-26-cli-context-and-status.md b/docs/plans/jobs/2026-01-26-cli-context-and-status.md index 58a7b89..8fb44ce 100644 --- a/docs/plans/jobs/2026-01-26-cli-context-and-status.md +++ b/docs/plans/jobs/2026-01-26-cli-context-and-status.md @@ -1,6 +1,6 @@ --- title: "CLI context propagation and status parsing" -date: 2026-01-26 +created-date: 2026-01-26 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-27-config-grid-fallback.md b/docs/plans/jobs/2026-01-27-config-grid-fallback.md index c22f511..4f531d4 100644 --- a/docs/plans/jobs/2026-01-27-config-grid-fallback.md +++ b/docs/plans/jobs/2026-01-27-config-grid-fallback.md @@ -1,6 +1,6 @@ --- title: "Apply table grid fallback per config source" -date: 2026-01-27 +created-date: 2026-01-27 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-27-config-refactor-grid-flags.md b/docs/plans/jobs/2026-01-27-config-refactor-grid-flags.md index 1e23226..fbf52e1 100644 --- a/docs/plans/jobs/2026-01-27-config-refactor-grid-flags.md +++ b/docs/plans/jobs/2026-01-27-config-refactor-grid-flags.md @@ -1,6 +1,6 @@ --- title: "Config refactor and grid flags preservation" -date: 2026-01-27 +created-date: 2026-01-27 status: completed agent: github-copilot --- diff --git a/docs/plans/jobs/2026-01-27-extensible-config-schema.md b/docs/plans/jobs/2026-01-27-extensible-config-schema.md index 0d40345..760989d 100644 --- a/docs/plans/jobs/2026-01-27-extensible-config-schema.md +++ b/docs/plans/jobs/2026-01-27-extensible-config-schema.md @@ -1,6 +1,6 @@ --- title: "Extensible config schema doc" -date: 2026-01-27 +created-date: 2026-01-27 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-27-fix-project-config-root-resolution.md b/docs/plans/jobs/2026-01-27-fix-project-config-root-resolution.md index 3ff6ec3..560cbc5 100644 --- a/docs/plans/jobs/2026-01-27-fix-project-config-root-resolution.md +++ b/docs/plans/jobs/2026-01-27-fix-project-config-root-resolution.md @@ -1,6 +1,6 @@ --- title: "Fix project config root resolution" -date: 2026-01-27 +created-date: 2026-01-27 status: completed agent: codex modified-date: 2026-01-27 diff --git a/docs/plans/jobs/2026-01-27-phase2-config-loader.md b/docs/plans/jobs/2026-01-27-phase2-config-loader.md index a8a2385..3868e5d 100644 --- a/docs/plans/jobs/2026-01-27-phase2-config-loader.md +++ b/docs/plans/jobs/2026-01-27-phase2-config-loader.md @@ -1,6 +1,6 @@ --- title: "Phase 2 config loader implementation" -date: 2026-01-27 +created-date: 2026-01-27 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-27-phase3-docs.md b/docs/plans/jobs/2026-01-27-phase3-docs.md index 8751315..41b0e77 100644 --- a/docs/plans/jobs/2026-01-27-phase3-docs.md +++ b/docs/plans/jobs/2026-01-27-phase3-docs.md @@ -1,6 +1,6 @@ --- title: "Phase 3 docs update" -date: 2026-01-27 +created-date: 2026-01-27 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-27-phase3-tests.md b/docs/plans/jobs/2026-01-27-phase3-tests.md index c1ddffa..da2cd1c 100644 --- a/docs/plans/jobs/2026-01-27-phase3-tests.md +++ b/docs/plans/jobs/2026-01-27-phase3-tests.md @@ -1,6 +1,6 @@ --- title: "Phase 3 tests for config and merge-mode" -date: 2026-01-27 +created-date: 2026-01-27 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-01-27-project-root-tests.md b/docs/plans/jobs/2026-01-27-project-root-tests.md index 5d40330..7edbf1a 100644 --- a/docs/plans/jobs/2026-01-27-project-root-tests.md +++ b/docs/plans/jobs/2026-01-27-project-root-tests.md @@ -1,6 +1,6 @@ --- title: "Add project_root.go unit tests and documentation" -date: 2026-01-27 +created-date: 2026-01-27 status: completed agent: copilot --- diff --git a/docs/plans/jobs/2026-02-04-codex-apply-terminology.md b/docs/plans/jobs/2026-02-04-codex-apply-terminology.md index 374e089..ace2d35 100644 --- a/docs/plans/jobs/2026-02-04-codex-apply-terminology.md +++ b/docs/plans/jobs/2026-02-04-codex-apply-terminology.md @@ -1,6 +1,6 @@ --- title: "Codex apply terminology updates" -date: 2026-02-04 +created-date: 2026-02-04 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-02-04-fix-codex-opaque-id-and-raw-path.md b/docs/plans/jobs/2026-02-04-fix-codex-opaque-id-and-raw-path.md index 9ba66b4..12f305d 100644 --- a/docs/plans/jobs/2026-02-04-fix-codex-opaque-id-and-raw-path.md +++ b/docs/plans/jobs/2026-02-04-fix-codex-opaque-id-and-raw-path.md @@ -1,6 +1,6 @@ --- title: "Fix codex opaque-id mapping and raw output paths" -date: 2026-02-04 +created-date: 2026-02-04 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-02-04-fix-codex-raw-paths.md b/docs/plans/jobs/2026-02-04-fix-codex-raw-paths.md index 13bbe38..cbea012 100644 --- a/docs/plans/jobs/2026-02-04-fix-codex-raw-paths.md +++ b/docs/plans/jobs/2026-02-04-fix-codex-raw-paths.md @@ -1,6 +1,6 @@ --- title: "Fix codex raw/absolute path handling" -date: 2026-02-04 +created-date: 2026-02-04 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-02-04-fix-codex-raw-relative-to-cwd.md b/docs/plans/jobs/2026-02-04-fix-codex-raw-relative-to-cwd.md index 5aa44c2..f86dc0d 100644 --- a/docs/plans/jobs/2026-02-04-fix-codex-raw-relative-to-cwd.md +++ b/docs/plans/jobs/2026-02-04-fix-codex-raw-relative-to-cwd.md @@ -1,6 +1,6 @@ --- title: "Fix codex raw paths to be relative to cwd" -date: 2026-02-04 +created-date: 2026-02-04 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-02-04-fuzzy-first-match-list-status.md b/docs/plans/jobs/2026-02-04-fuzzy-first-match-list-status.md index 1211d54..014544a 100644 --- a/docs/plans/jobs/2026-02-04-fuzzy-first-match-list-status.md +++ b/docs/plans/jobs/2026-02-04-fuzzy-first-match-list-status.md @@ -1,6 +1,6 @@ --- title: "Make fuzzy query return first match (list/status)" -date: 2026-02-04 +created-date: 2026-02-04 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-02-04-implement-mode-flag-phase2.md b/docs/plans/jobs/2026-02-04-implement-mode-flag-phase2.md index 9ca8775..08cf2ba 100644 --- a/docs/plans/jobs/2026-02-04-implement-mode-flag-phase2.md +++ b/docs/plans/jobs/2026-02-04-implement-mode-flag-phase2.md @@ -1,6 +1,6 @@ --- title: "Implement --mode and codex-mode commands (Phase 2)" -date: 2026-02-04 +created-date: 2026-02-04 status: completed agent: codex --- diff --git a/docs/plans/jobs/2026-02-04-phase-3-tests.md b/docs/plans/jobs/2026-02-04-phase-3-tests.md index 7e6ec40..4d206ab 100644 --- a/docs/plans/jobs/2026-02-04-phase-3-tests.md +++ b/docs/plans/jobs/2026-02-04-phase-3-tests.md @@ -1,6 +1,6 @@ --- title: "Phase 3 tests for codex mode" -date: 2026-02-04 +created-date: 2026-02-04 modified-date: 2026-02-04 status: completed agent: codex diff --git a/docs/plans/jobs/2026-02-04-skill-location-refactor.md b/docs/plans/jobs/2026-02-04-skill-location-refactor.md index b91b81a..1201d6b 100644 --- a/docs/plans/jobs/2026-02-04-skill-location-refactor.md +++ b/docs/plans/jobs/2026-02-04-skill-location-refactor.md @@ -1,6 +1,6 @@ --- title: "Codex Skill Path Refactor" -date: 2026-02-04 +created-date: 2026-02-04 status: completed agent: codex --- diff --git a/docs/plans/plan-2026-01-12-cleanup-modes-and-branch-visibility.md b/docs/plans/plan-2026-01-12-cleanup-modes-and-branch-visibility.md index 35af4e8..fe77392 100644 --- a/docs/plans/plan-2026-01-12-cleanup-modes-and-branch-visibility.md +++ b/docs/plans/plan-2026-01-12-cleanup-modes-and-branch-visibility.md @@ -1,6 +1,6 @@ --- title: "Clarify cleanup modes and branch visibility" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/plans/plan-2026-01-12-color-output.md b/docs/plans/plan-2026-01-12-color-output.md index 3cf7f53..afa2618 100644 --- a/docs/plans/plan-2026-01-12-color-output.md +++ b/docs/plans/plan-2026-01-12-color-output.md @@ -1,6 +1,6 @@ --- title: "Colorized CLI output and --nocolor flag" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/plans/plan-2026-01-12-init-phase.md b/docs/plans/plan-2026-01-12-init-phase.md index 2ab1fce..fa0606a 100644 --- a/docs/plans/plan-2026-01-12-init-phase.md +++ b/docs/plans/plan-2026-01-12-init-phase.md @@ -1,6 +1,6 @@ --- title: "Init Phase Plan" -date: 2026-01-12 +created-date: 2026-01-12 status: active agent: codex --- diff --git a/docs/plans/plan-2026-01-12-path-display-readme.md b/docs/plans/plan-2026-01-12-path-display-readme.md index 7aca293..9299fa2 100644 --- a/docs/plans/plan-2026-01-12-path-display-readme.md +++ b/docs/plans/plan-2026-01-12-path-display-readme.md @@ -1,6 +1,6 @@ --- title: "Relative Paths Flag and README" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/plans/plan-2026-01-12-themes-and-rwd.md b/docs/plans/plan-2026-01-12-themes-and-rwd.md index 01b9bf0..7217e46 100644 --- a/docs/plans/plan-2026-01-12-themes-and-rwd.md +++ b/docs/plans/plan-2026-01-12-themes-and-rwd.md @@ -1,6 +1,6 @@ --- title: "Themes and responsive tables" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/plans/plan-2026-01-13-build-and-distribution-flow.md b/docs/plans/plan-2026-01-13-build-and-distribution-flow.md index 7456574..79d88c6 100644 --- a/docs/plans/plan-2026-01-13-build-and-distribution-flow.md +++ b/docs/plans/plan-2026-01-13-build-and-distribution-flow.md @@ -1,6 +1,6 @@ --- title: "Build flow and installation strategy for gwtt command" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: Zed Agent --- diff --git a/docs/plans/plan-2026-01-13-cicd-release-flow.md b/docs/plans/plan-2026-01-13-cicd-release-flow.md index 7d9b945..f15c1f1 100644 --- a/docs/plans/plan-2026-01-13-cicd-release-flow.md +++ b/docs/plans/plan-2026-01-13-cicd-release-flow.md @@ -1,6 +1,6 @@ --- title: "CI/CD Release Flow with Pre-Release Gate" -date: 2026-01-13 +created-date: 2026-01-13 status: draft agent: Codex --- diff --git a/docs/plans/plan-2026-01-13-lint-and-cleanup.md b/docs/plans/plan-2026-01-13-lint-and-cleanup.md index 660a587..8965347 100644 --- a/docs/plans/plan-2026-01-13-lint-and-cleanup.md +++ b/docs/plans/plan-2026-01-13-lint-and-cleanup.md @@ -1,6 +1,6 @@ --- title: "Lint and Cleanup" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: GitHub Copilot --- diff --git a/docs/plans/plan-2026-01-13-list-copy-flag.md b/docs/plans/plan-2026-01-13-list-copy-flag.md index 61154a9..36c7d5b 100644 --- a/docs/plans/plan-2026-01-13-list-copy-flag.md +++ b/docs/plans/plan-2026-01-13-list-copy-flag.md @@ -1,6 +1,6 @@ --- title: "List copy flag" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: codex --- diff --git a/docs/plans/plan-2026-01-13-theme-config-and-env.md b/docs/plans/plan-2026-01-13-theme-config-and-env.md index 717954e..565843e 100644 --- a/docs/plans/plan-2026-01-13-theme-config-and-env.md +++ b/docs/plans/plan-2026-01-13-theme-config-and-env.md @@ -1,6 +1,6 @@ --- title: "Theme config and env" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: codex --- diff --git a/docs/plans/plan-2026-01-13-themes-flag.md b/docs/plans/plan-2026-01-13-themes-flag.md index e864dd7..629c12e 100644 --- a/docs/plans/plan-2026-01-13-themes-flag.md +++ b/docs/plans/plan-2026-01-13-themes-flag.md @@ -1,6 +1,6 @@ --- title: "Make --themes available at root" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: codex --- diff --git a/docs/plans/plan-2026-01-13-worktree-raw-fallback.md b/docs/plans/plan-2026-01-13-worktree-raw-fallback.md index f40af97..2439548 100644 --- a/docs/plans/plan-2026-01-13-worktree-raw-fallback.md +++ b/docs/plans/plan-2026-01-13-worktree-raw-fallback.md @@ -1,6 +1,6 @@ --- title: "Worktree raw output fallback" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: codex --- diff --git a/docs/plans/plan-2026-01-18-module-path-transfer.md b/docs/plans/plan-2026-01-18-module-path-transfer.md index 6deaa9c..40ce28a 100644 --- a/docs/plans/plan-2026-01-18-module-path-transfer.md +++ b/docs/plans/plan-2026-01-18-module-path-transfer.md @@ -1,6 +1,6 @@ --- title: "Module path transfer to pi2pie" -date: 2026-01-18 +created-date: 2026-01-18 status: completed agent: codex --- diff --git a/docs/plans/plan-2026-01-18-open-source-readiness.md b/docs/plans/plan-2026-01-18-open-source-readiness.md index 8ed926a..2694178 100644 --- a/docs/plans/plan-2026-01-18-open-source-readiness.md +++ b/docs/plans/plan-2026-01-18-open-source-readiness.md @@ -1,6 +1,6 @@ --- title: "Open-source readiness improvements" -date: 2026-01-18 +created-date: 2026-01-18 modified-date: 2026-01-19 status: active agent: Codex diff --git a/docs/plans/plan-2026-01-21-release-workflow-improvements.md b/docs/plans/plan-2026-01-21-release-workflow-improvements.md index de59cb5..6a0006e 100644 --- a/docs/plans/plan-2026-01-21-release-workflow-improvements.md +++ b/docs/plans/plan-2026-01-21-release-workflow-improvements.md @@ -1,6 +1,6 @@ --- title: "Release workflow improvements" -date: 2026-01-21 +created-date: 2026-01-21 status: completed agent: codex --- diff --git a/docs/plans/plan-2026-01-26-go-cli-context-and-status.md b/docs/plans/plan-2026-01-26-go-cli-context-and-status.md index 850af2e..7d830aa 100644 --- a/docs/plans/plan-2026-01-26-go-cli-context-and-status.md +++ b/docs/plans/plan-2026-01-26-go-cli-context-and-status.md @@ -1,6 +1,6 @@ --- title: "Go CLI context propagation and status robustness" -date: 2026-01-26 +created-date: 2026-01-26 status: completed modified-date: 2026-01-27 agent: codex diff --git a/docs/plans/plan-2026-01-26-install-script-and-readme.md b/docs/plans/plan-2026-01-26-install-script-and-readme.md index 0f3728f..638097d 100644 --- a/docs/plans/plan-2026-01-26-install-script-and-readme.md +++ b/docs/plans/plan-2026-01-26-install-script-and-readme.md @@ -1,6 +1,6 @@ --- title: "Install Script + README Clarity for Binary Names" -date: 2026-01-26 +created-date: 2026-01-26 status: active agent: codex --- diff --git a/docs/plans/plan-2026-01-27-extensible-config.md b/docs/plans/plan-2026-01-27-extensible-config.md index 32d5480..ee6521d 100644 --- a/docs/plans/plan-2026-01-27-extensible-config.md +++ b/docs/plans/plan-2026-01-27-extensible-config.md @@ -1,6 +1,6 @@ --- title: "Extensible config rollout" -date: 2026-01-27 +created-date: 2026-01-27 status: completed agent: codex --- diff --git a/docs/plans/plan-2026-01-27-fix-project-config-root-resolution.md b/docs/plans/plan-2026-01-27-fix-project-config-root-resolution.md index 9dab960..a306c6f 100644 --- a/docs/plans/plan-2026-01-27-fix-project-config-root-resolution.md +++ b/docs/plans/plan-2026-01-27-fix-project-config-root-resolution.md @@ -1,6 +1,6 @@ --- title: "Fix project config root resolution" -date: 2026-01-27 +created-date: 2026-01-27 status: completed agent: codex modified-date: 2026-01-27 diff --git a/docs/plans/plan-2026-02-04-mode-classic-and-codex.md b/docs/plans/plan-2026-02-04-mode-classic-and-codex.md index 699d5cf..0aee80c 100644 --- a/docs/plans/plan-2026-02-04-mode-classic-and-codex.md +++ b/docs/plans/plan-2026-02-04-mode-classic-and-codex.md @@ -1,6 +1,6 @@ --- title: "Mode flag: classic and codex" -date: 2026-02-04 +created-date: 2026-02-04 modified-date: 2026-02-04 status: completed agent: codex diff --git a/docs/research-2026-01-12-themes-and-table.md b/docs/research-2026-01-12-themes-and-table.md index 555166e..8ffe044 100644 --- a/docs/research-2026-01-12-themes-and-table.md +++ b/docs/research-2026-01-12-themes-and-table.md @@ -1,6 +1,6 @@ --- title: "Theme candidates and responsive table approach" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/research-2026-01-12-worktree-ops-matrix.md b/docs/research-2026-01-12-worktree-ops-matrix.md index d1fdafd..1e93431 100644 --- a/docs/research-2026-01-12-worktree-ops-matrix.md +++ b/docs/research-2026-01-12-worktree-ops-matrix.md @@ -1,6 +1,6 @@ --- title: "Worktree Operations Matrix" -date: 2026-01-12 +created-date: 2026-01-12 status: completed agent: codex --- diff --git a/docs/research-2026-01-13-homebrew-integration.md b/docs/research-2026-01-13-homebrew-integration.md index 4db292c..f76ff8b 100644 --- a/docs/research-2026-01-13-homebrew-integration.md +++ b/docs/research-2026-01-13-homebrew-integration.md @@ -1,6 +1,6 @@ --- title: "Homebrew integration strategies for git-worktree-tasks" -date: 2026-01-13 +created-date: 2026-01-13 modified: 2026-01-18 status: draft agent: Zed Agent diff --git a/docs/research-2026-01-13-toml-config-options.md b/docs/research-2026-01-13-toml-config-options.md index 1af668f..ef7aef6 100644 --- a/docs/research-2026-01-13-toml-config-options.md +++ b/docs/research-2026-01-13-toml-config-options.md @@ -1,6 +1,6 @@ --- title: "TOML config options for gwtt" -date: 2026-01-13 +created-date: 2026-01-13 status: completed agent: codex --- diff --git a/docs/research-2026-01-19-create-default-base.md b/docs/research-2026-01-19-create-default-base.md index e857452..6aa9b99 100644 --- a/docs/research-2026-01-19-create-default-base.md +++ b/docs/research-2026-01-19-create-default-base.md @@ -1,6 +1,6 @@ --- title: "Create default base selection" -date: 2026-01-19 +created-date: 2026-01-19 status: completed agent: Codex --- diff --git a/docs/research-2026-01-27-extensible-config.md b/docs/research-2026-01-27-extensible-config.md index 4f3bbf3..fd3d986 100644 --- a/docs/research-2026-01-27-extensible-config.md +++ b/docs/research-2026-01-27-extensible-config.md @@ -1,6 +1,6 @@ --- title: "Extensible config options beyond theme" -date: 2026-01-27 +created-date: 2026-01-27 status: completed agent: codex --- diff --git a/docs/research-2026-02-04-mode-classic-vs-codex.md b/docs/research-2026-02-04-mode-classic-vs-codex.md index e97d984..0e02f2b 100644 --- a/docs/research-2026-02-04-mode-classic-vs-codex.md +++ b/docs/research-2026-02-04-mode-classic-vs-codex.md @@ -1,6 +1,6 @@ --- title: "Mode Flag: classic vs codex" -date: 2026-02-04 +created-date: 2026-02-04 modified-date: 2026-02-04 status: completed agent: codex diff --git a/docs/schemas/config-gwtt.md b/docs/schemas/config-gwtt.md index e93f0e7..208ab32 100644 --- a/docs/schemas/config-gwtt.md +++ b/docs/schemas/config-gwtt.md @@ -1,6 +1,6 @@ --- title: "gwtt configuration schema" -date: 2026-01-27 +created-date: 2026-01-27 modified-date: 2026-02-04 status: in-progress agent: codex From e25651e123f41be1e01169f5b102595309e73476 Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 00:32:55 +0800 Subject: [PATCH 03/15] chore: bump version to 0.1.2-alpha.0 --- cli/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/root.go b/cli/root.go index 75bdf72..d550dde 100644 --- a/cli/root.go +++ b/cli/root.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -var Version = "0.1.1" +var Version = "0.1.2-alpha.0" var ( errCanceled = errors.New("git worktree task process canceled") From 234d980a28a9e74479a52216213c0d1242f2b0b5 Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 15:43:43 +0800 Subject: [PATCH 04/15] docs(research): add codex apply research --- cli/root.go | 2 +- ...dex-apply-direction-and-source-checkout.md | 178 ++++++++++++++++++ 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md diff --git a/cli/root.go b/cli/root.go index d550dde..bd23175 100644 --- a/cli/root.go +++ b/cli/root.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -var Version = "0.1.2-alpha.0" +var Version = "0.1.2-alpha.1" var ( errCanceled = errors.New("git worktree task process canceled") diff --git a/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md b/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md new file mode 100644 index 0000000..309bf79 --- /dev/null +++ b/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md @@ -0,0 +1,178 @@ +--- +title: "Codex apply direction and source checkout behavior" +created-date: 2026-02-07 +modified-date: 2026-02-07 +status: in-progress +agent: codex +--- + +## Goal + +Define a clearer, less surprising `gwtt apply` model in `--mode=codex` by making direction explicit, defining overwrite semantics for both directions, and redesigning `--dry-run` output so users can see what will happen before mutation. + +## Key Findings + +### Current `apply` behavior is directionally ambiguous + +- `gwtt apply ` currently starts as worktree -> local, but on conflict it can switch to local -> worktree after confirmation. +- This fallback is implemented in `cli/apply.go` via: + - `applyWorktreeChanges(...)` for worktree -> local. + - `overwriteWorktreeChanges(...)` for local -> worktree (hard reset + clean on worktree first). +- Result: users can enter `apply` expecting "to local" but end in an overwrite flow that discards worktree changes. + +### Direction flag fits the Codex App mental model + +- Codex App exposes two explicit directions ("To local" / "From local") for the same handoff concept. +- CLI parity is straightforward with a destination-oriented flag: + - `gwtt apply ` as safe default (`--to local`). + - `gwtt apply --to worktree ` for reverse flow. + +### Overwrite should be destination-scoped, not direction-switching + +- In a two-way model, overwrite must mean "replace destination with source" regardless of direction. +- This is safer and more predictable than "on conflict, flip direction." +- The current hard reset + clean logic can be reused, but should only run when overwrite is explicitly requested. +- Since Codex App presents "Apply" and "Overwrite" as separate top-level actions, CLI parity is best achieved with separate commands rather than hiding overwrite behind a flag. + +### The "source local checkout removed" case should be scoped narrowly for now + +- Current CLI design is intentionally registry-free for codex mode and identifies worktrees by Git + `$CODEX_HOME/worktrees`. +- App-style "wire this worktree to a new/existing branch" implies extra state and UX that `gwtt` does not currently persist. +- Adding branch wiring now would expand scope beyond `apply` direction clarification. + +### Current `--dry-run` output lacks operation context + +- Today, dry-run mostly prints raw git commands and per-file copy lines. +- It does not clearly tell users: + - selected direction, + - source/destination paths, + - whether overwrite/reset will occur, + - summary of what is expected to change. + +### `cli/apply.go` is doing too much in one file + +- The file contains command wiring, worktree resolution, conflict detection, transfer logic, patch I/O, and file copying. +- There is duplicated transfer logic between forward/reverse paths, increasing maintenance cost and making direction changes riskier. + +## Implications or Recommendations + +- Add `--to` with enum values `local|worktree` (default: `local`). +- Keep backward compatibility: + - `gwtt apply ` behaves as today’s safe default (worktree -> local). +- Introduce explicit overwrite behavior as a sibling command: + - `gwtt apply ...` for non-destructive transfer attempt. + - `gwtt overwrite ...` for destructive destination replacement. +- Optional compatibility alias: + - `gwtt apply --force ...` can be supported as shorthand that dispatches to overwrite behavior. + - Document it as compatibility/convenience, not the primary UX. +- Stop implicit direction switching: + - On conflict in `apply`, return a clear conflict error. + - Suggest rerun with `overwrite` for the same `--to` direction. +- Make overwrite direction-agnostic: + - `overwrite --to local` discards local destination changes and applies worktree -> local. + - `overwrite --to worktree` discards worktree destination changes and applies local -> worktree. +- For "source local checkout gone", do not add wiring state in this phase: + - If selected `` is not resolvable from the current repository context, fail with an actionable message. + - Message should instruct user to run from a checkout sharing the same Git common dir or recreate/relink manually. +- Redesign `--dry-run` output to be plan-oriented: + - Print operation header (direction, source, destination, overwrite mode). + - Print preflight summary (destination dirty, overlap, tracked patch state, untracked count). + - Print ordered action plan (check/apply/reset/clean/copy steps). + - Keep command echo for transparency, but make it secondary to the plan summary. +- Split `apply` implementation into focused files before/with feature work: + - `cli/apply_command.go`: Cobra command, flags, and top-level flow. + - `cli/apply_resolve.go`: codex worktree resolution and validation. + - `cli/apply_conflicts.go`: conflict detection helpers. + - `cli/apply_transfer.go`: direction-agnostic transfer operations. + - `cli/apply_files.go`: temp patch + copy helpers. +- Refactor transfer logic to one core function: + - `transferChanges(sourceRoot, destRoot, opts)` with optional `resetDest` behavior. + - This keeps direction implementation DRY and lowers regression risk. + +## Proposed CLI Spec (Draft) + +```bash +# safe default +gwtt apply # same as --to local + +# non-destructive apply, explicit direction +gwtt apply --to local # worktree -> local +gwtt apply --to worktree # local -> worktree + +# destructive overwrite as peer command +gwtt overwrite --to local # destination local reset/clean first +gwtt overwrite --to worktree # destination worktree reset/clean first + +# optional compatibility alias +gwtt apply --force --to worktree # shorthand for overwrite behavior +``` + +Apply vs overwrite matrix: + +- `apply`: + - never reset/clean destination; + - on destination dirtiness or patch conflict, exit non-zero and print next-step hint. +- `overwrite`: + - require confirmation unless `--yes`; + - reset and clean destination before transfer; + - then apply tracked diff + copy untracked files from source. + +## Open Questions + +- Should we ship `apply --force` alias in v1, or keep only explicit `overwrite` to keep UX strict? +- Do we need `--output json` for dry-run planning (machine-readable action plan), or is text-first enough initially? +- Do we need a future `relink` command for app-like branch wiring, or is explicit error + manual Git workflow sufficient? + +## Dry-Run Output Redesign (Draft) + +Proposed dry-run output shape: + +```text +apply plan + to: local + source: $CODEX_HOME/worktrees/bf15/repo + destination: /path/to/repo + overwrite: false + +preflight + destination_dirty: true + overlapping_files: 2 + tracked_patch: present + untracked_files: 3 + +actions + 1. git -C /path/to/repo apply --check + 2. git -C /path/to/repo apply + 3. copy /a.txt -> /a.txt + 4. copy /b.txt -> /b.txt +``` + +Overwrite mode should show destructive steps explicitly: + +```text +actions + 1. git -C reset --hard + 2. git -C clean -fd + ... +``` + +Output requirements: + +- Must clearly indicate whether destination will be reset/cleaned. +- Must include source and destination path labels. +- Must preserve command transparency for debugging. +- Must avoid ambiguous "apply complete" style messaging in dry-run mode. + +## References + +- Current implementation: [^apply-code] +- Prior codex mode research: [^mode-research] +- Related mode plan: [^mode-plan] + +[^apply-code]: `cli/apply.go` +[^mode-research]: `docs/research-2026-02-04-mode-classic-vs-codex.md` +[^mode-plan]: `docs/plans/plan-2026-02-04-mode-classic-and-codex.md` + +## Related Plans + +- `docs/plans/plan-2026-02-04-mode-classic-and-codex.md` From d294ac969581c55c791fa62f9d1841aa1937769a Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 16:01:14 +0800 Subject: [PATCH 05/15] chore: extract CLI helpers for codex commands - Add a modeContext resolver to centralize mode and codex path resolution. - Move runGit to git_exec.go and extract codex worktree lookup helpers. - Update apply, cleanup, list, status, and finish to use the new helpers. - Add job plan and implementation plan docs for upcoming two-way apply work. --- cli/apply.go | 34 ++------- cli/cleanup.go | 33 +++----- cli/codex_lookup.go | 31 ++++++++ cli/finish.go | 18 ----- cli/git_exec.go | 26 +++++++ cli/list.go | 24 ++---- cli/mode_context.go | 40 ++++++++++ cli/status.go | 24 ++---- ...-cli-helper-extraction-codex-apply-prep.md | 40 ++++++++++ ...26-02-07-codex-apply-two-way-directions.md | 76 +++++++++++++++++++ 10 files changed, 242 insertions(+), 104 deletions(-) create mode 100644 cli/codex_lookup.go create mode 100644 cli/git_exec.go create mode 100644 cli/mode_context.go create mode 100644 docs/plans/jobs/2026-02-07-cli-helper-extraction-codex-apply-prep.md create mode 100644 docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md diff --git a/cli/apply.go b/cli/apply.go index 0bd6e9e..cf9ceda 100644 --- a/cli/apply.go +++ b/cli/apply.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/pi2pie/git-worktree-tasks/internal/git" - "github.com/pi2pie/git-worktree-tasks/internal/worktree" "github.com/pi2pie/git-worktree-tasks/ui" "github.com/spf13/cobra" ) @@ -42,8 +41,12 @@ func newApplyCommand() *cobra.Command { Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() + modeCtx, err := resolveModeContext(cmd, false) + if err != nil { + return err + } cfg, ok := configFromContext(ctx) - if !ok || cfg.Mode != modeCodex { + if !ok || modeCtx.mode != modeCodex { return fmt.Errorf("apply is only supported in --mode=codex") } @@ -65,13 +68,7 @@ func newApplyCommand() *cobra.Command { return fmt.Errorf("task query cannot be empty") } - codexHome, err := codexHomeDir() - if err != nil { - return err - } - codexWorktrees := codexWorktreesRoot(codexHome) - - wtPath, ok, err := resolveCodexWorktreePath(ctx, runner, repoRoot, codexWorktrees, opaqueID) + wtPath, ok, err := resolveCodexWorktreePath(ctx, runner, repoRoot, modeCtx.codexWorktrees, opaqueID) if err != nil { return err } @@ -151,25 +148,6 @@ func newApplyCommand() *cobra.Command { return cmd } -func resolveCodexWorktreePath(ctx context.Context, runner git.Runner, repoRoot, codexWorktreesRoot, opaqueID string) (string, bool, error) { - worktrees, err := worktree.List(ctx, runner, repoRoot) - if err != nil { - return "", false, err - } - for _, wt := range worktrees { - wtAbs, err := worktree.NormalizePath(repoRoot, wt.Path) - if err != nil { - return "", false, err - } - id, _, ok := codexWorktreeInfo(codexWorktreesRoot, wtAbs) - if !ok || id != opaqueID { - continue - } - return wtAbs, true, nil - } - return "", false, nil -} - func detectApplyConflicts(ctx context.Context, runner git.Runner, repoRoot, worktreePath string) ([]string, error) { var reasons []string diff --git a/cli/cleanup.go b/cli/cleanup.go index aff6b1b..8f5b172 100644 --- a/cli/cleanup.go +++ b/cli/cleanup.go @@ -29,19 +29,13 @@ func newCleanupCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() runner := defaultRunner() - mode := modeClassic - var codexHome string - var codexWorktrees string + modeCtx, err := resolveModeContext(cmd, false) + if err != nil { + return err + } + mode := modeCtx.mode + codexWorktrees := modeCtx.codexWorktrees if cfg, ok := configFromContext(cmd.Context()); ok { - mode = cfg.Mode - if mode == modeCodex { - var err error - codexHome, err = codexHomeDir() - if err != nil { - return err - } - codexWorktrees = codexWorktreesRoot(codexHome) - } if !cmd.Flags().Changed("yes") { opts.yes = !cfg.Cleanup.Confirm } @@ -109,18 +103,9 @@ func newCleanupCommand() *cobra.Command { if query == "" { return fmt.Errorf("task query cannot be empty") } - for _, wt := range worktrees { - wtAbs, err := worktree.NormalizePath(repoRoot, wt.Path) - if err != nil { - return err - } - opaqueID, _, ok := codexWorktreeInfo(codexWorktrees, wtAbs) - if !ok || opaqueID != query { - continue - } - resolvedPath = wtAbs - worktreeExists = true - break + resolvedPath, worktreeExists, err = resolveCodexWorktreePathFromList(worktrees, repoRoot, codexWorktrees, query) + if err != nil { + return err } } else { branchRef := "refs/heads/" + branch diff --git a/cli/codex_lookup.go b/cli/codex_lookup.go new file mode 100644 index 0000000..5486d30 --- /dev/null +++ b/cli/codex_lookup.go @@ -0,0 +1,31 @@ +package cli + +import ( + "context" + + "github.com/pi2pie/git-worktree-tasks/internal/git" + "github.com/pi2pie/git-worktree-tasks/internal/worktree" +) + +func resolveCodexWorktreePath(ctx context.Context, runner git.Runner, repoRoot, codexWorktreesRoot, opaqueID string) (string, bool, error) { + worktrees, err := worktree.List(ctx, runner, repoRoot) + if err != nil { + return "", false, err + } + return resolveCodexWorktreePathFromList(worktrees, repoRoot, codexWorktreesRoot, opaqueID) +} + +func resolveCodexWorktreePathFromList(worktrees []worktree.Worktree, repoRoot, codexWorktreesRoot, opaqueID string) (string, bool, error) { + for _, wt := range worktrees { + wtAbs, err := worktree.NormalizePath(repoRoot, wt.Path) + if err != nil { + return "", false, err + } + id, _, ok := codexWorktreeInfo(codexWorktreesRoot, wtAbs) + if !ok || id != opaqueID { + continue + } + return wtAbs, true, nil + } + return "", false, nil +} diff --git a/cli/finish.go b/cli/finish.go index bc15929..2fc97a8 100644 --- a/cli/finish.go +++ b/cli/finish.go @@ -1,7 +1,6 @@ package cli import ( - "context" "fmt" "strings" @@ -211,20 +210,3 @@ func validateMergeStrategy(opts *finishOptions) error { } return nil } - -func runGit(ctx context.Context, cmd *cobra.Command, dryRun bool, runner git.Runner, args ...string) error { - if dryRun { - if _, err := fmt.Fprintln(cmd.OutOrStdout(), formatGitCommand(args)); err != nil { - return err - } - return nil - } - _, stderr, err := runner.Run(ctx, args...) - if err != nil { - if stderr != "" { - return fmt.Errorf("%s: %w: %s", formatGitCommand(args), err, stderr) - } - return fmt.Errorf("%s: %w", formatGitCommand(args), err) - } - return nil -} diff --git a/cli/git_exec.go b/cli/git_exec.go new file mode 100644 index 0000000..a298b8c --- /dev/null +++ b/cli/git_exec.go @@ -0,0 +1,26 @@ +package cli + +import ( + "context" + "fmt" + + "github.com/pi2pie/git-worktree-tasks/internal/git" + "github.com/spf13/cobra" +) + +func runGit(ctx context.Context, cmd *cobra.Command, dryRun bool, runner git.Runner, args ...string) error { + if dryRun { + if _, err := fmt.Fprintln(cmd.OutOrStdout(), formatGitCommand(args)); err != nil { + return err + } + return nil + } + _, stderr, err := runner.Run(ctx, args...) + if err != nil { + if stderr != "" { + return fmt.Errorf("%s: %w: %s", formatGitCommand(args), err, stderr) + } + return fmt.Errorf("%s: %w", formatGitCommand(args), err) + } + return nil +} diff --git a/cli/list.go b/cli/list.go index e5c0c52..703c5b4 100644 --- a/cli/list.go +++ b/cli/list.go @@ -43,25 +43,15 @@ func newListCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() runner := defaultRunner() - mode := modeClassic - var codexHome string - var codexWorktrees string + modeCtx, err := resolveModeContext(cmd, true) + if err != nil { + return err + } + mode := modeCtx.mode + codexHome := modeCtx.codexHome + codexWorktrees := modeCtx.codexWorktrees var rawBase string if cfg, ok := configFromContext(cmd.Context()); ok { - mode = cfg.Mode - if mode == modeCodex { - var err error - codexHome, err = codexHomeDir() - if err != nil { - return err - } - codexWorktrees = codexWorktreesRoot(codexHome) - } else { - if home, err := codexHomeDir(); err == nil { - codexHome = home - codexWorktrees = codexWorktreesRoot(codexHome) - } - } if !cmd.Flags().Changed("output") { opts.output = cfg.List.Output } diff --git a/cli/mode_context.go b/cli/mode_context.go new file mode 100644 index 0000000..823a86e --- /dev/null +++ b/cli/mode_context.go @@ -0,0 +1,40 @@ +package cli + +import "github.com/spf13/cobra" + +type modeContext struct { + mode string + codexHome string + codexWorktrees string +} + +// resolveModeContext returns command mode and, when needed, codex path context. +// If includeClassicCodex is true, codex paths are also resolved in classic mode +// on a best-effort basis (errors are ignored to preserve existing behavior). +func resolveModeContext(cmd *cobra.Command, includeClassicCodex bool) (modeContext, error) { + ctx := modeContext{mode: modeClassic} + cfg, ok := configFromContext(cmd.Context()) + if !ok { + return ctx, nil + } + + ctx.mode = cfg.Mode + switch ctx.mode { + case modeCodex: + home, err := codexHomeDir() + if err != nil { + return ctx, err + } + ctx.codexHome = home + ctx.codexWorktrees = codexWorktreesRoot(home) + default: + if includeClassicCodex { + if home, err := codexHomeDir(); err == nil { + ctx.codexHome = home + ctx.codexWorktrees = codexWorktreesRoot(home) + } + } + } + + return ctx, nil +} diff --git a/cli/status.go b/cli/status.go index 75f7895..ba6a4ea 100644 --- a/cli/status.go +++ b/cli/status.go @@ -49,24 +49,14 @@ func newStatusCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() runner := defaultRunner() - mode := modeClassic - var codexHome string - var codexWorktrees string + modeCtx, err := resolveModeContext(cmd, true) + if err != nil { + return err + } + mode := modeCtx.mode + codexHome := modeCtx.codexHome + codexWorktrees := modeCtx.codexWorktrees if cfg, ok := configFromContext(cmd.Context()); ok { - mode = cfg.Mode - if mode == modeCodex { - var err error - codexHome, err = codexHomeDir() - if err != nil { - return err - } - codexWorktrees = codexWorktreesRoot(codexHome) - } else { - if home, err := codexHomeDir(); err == nil { - codexHome = home - codexWorktrees = codexWorktreesRoot(codexHome) - } - } if !cmd.Flags().Changed("output") { opts.output = cfg.Status.Output } diff --git a/docs/plans/jobs/2026-02-07-cli-helper-extraction-codex-apply-prep.md b/docs/plans/jobs/2026-02-07-cli-helper-extraction-codex-apply-prep.md new file mode 100644 index 0000000..8f67efd --- /dev/null +++ b/docs/plans/jobs/2026-02-07-cli-helper-extraction-codex-apply-prep.md @@ -0,0 +1,40 @@ +--- +title: "CLI helper extraction for codex apply prep" +created-date: 2026-02-07 +status: completed +agent: codex +--- + +## Summary + +Completed no-behavior-change refactor work to prepare for two-way `apply`/`overwrite` implementation. + +- Added shared mode/codex context resolver for CLI commands. +- Added shared codex worktree lookup helper (including list-based reuse path). +- Moved shared `runGit` execution helper out of `finish.go` into a neutral helper file. +- Updated `apply`, `cleanup`, `list`, and `status` to use extracted helpers. + +## Why + +- Reduce duplicated setup logic before introducing directional command behavior. +- Lower implementation risk for upcoming `apply`/`overwrite` feature work. +- Keep command files more focused and easier to evolve. + +## Files Updated + +- `cli/mode_context.go` +- `cli/codex_lookup.go` +- `cli/git_exec.go` +- `cli/apply.go` +- `cli/cleanup.go` +- `cli/list.go` +- `cli/status.go` +- `cli/finish.go` + +## Verification + +- Ran `go test ./...` successfully. + +## Related Plans + +- `docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md` diff --git a/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md b/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md new file mode 100644 index 0000000..8566ef4 --- /dev/null +++ b/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md @@ -0,0 +1,76 @@ +--- +title: "Codex apply two-way directions and cli extraction prep" +created-date: 2026-02-07 +modified-date: 2026-02-07 +status: active +agent: codex +--- + +## Goal + +Implement a two-way `apply`/`overwrite` command model in codex mode with explicit direction semantics, while first extracting high-reuse CLI helpers to reduce risk before behavior changes. + +## Scope + +- Define and implement two-way direction handling for codex handoff flows. +- Align CLI command surface with Codex app-level concepts (`apply` and `overwrite` as peer actions). +- Improve dry-run clarity for direction and destructive operations. +- Perform foundational CLI refactors first (no behavior change). + +## Non-Goals + +- No registry/state file for branch wiring in this phase. +- No new classic-mode behavior. +- No sub-package split for `cli` command internals yet. + +## Phase Checklist + +### Phase 1: Spec and UX Lock + +- [ ] Finalize command matrix for: + - `apply --to local|worktree` + - `overwrite --to local|worktree` + - optional `apply --force` alias policy +- [ ] Finalize conflict behavior for non-destructive apply (no implicit direction switching). +- [ ] Finalize dry-run output schema (header + preflight + action list). + +### Phase 2: Foundational Refactor (No Behavior Change) + +- [x] Extract mode/codex context helper used by command handlers. +- [x] Extract/reuse codex worktree lookup helper by opaque ID. +- [x] Relocate shared `runGit` helper from `finish.go` into a neutral CLI helper file. +- [x] Keep output/error semantics unchanged in this phase. + +### Phase 3: Apply/Overwrite Implementation + +- [ ] Implement `--to` direction support in codex handoff commands. +- [ ] Introduce `overwrite` as peer command (or locked alias strategy per Phase 1). +- [ ] Remove implicit overwrite fallback from `apply` conflict path. +- [ ] Preserve confirmation gating (`--yes`) for destructive paths. + +### Phase 4: Dry-Run and Messages + +- [ ] Implement structured dry-run plan output with explicit source/destination and destructive markers. +- [ ] Update user-facing conflict and next-step guidance. + +### Phase 5: Tests and Docs + +- [ ] Add/adjust unit and integration tests for direction + overwrite behaviors. +- [ ] Update README/man pages/help text for new command semantics. +- [ ] Update related research and mark this plan status appropriately. + +## Acceptance Criteria + +- `apply` and `overwrite` semantics are explicit and direction-stable. +- No hidden mutation of the opposite side on `apply` conflict. +- Dry-run clearly shows operation intent and action sequence. +- Foundational helper extraction lands without behavior regressions. + +## Risks / Notes + +- Confirmation wording must remain clear when destructive destination reset is involved. +- Refactor-first approach lowers risk but can surface latent coupling in `cli`. + +## Related Research + +- `docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md` From efce502784ec38af3bf340f23cd9ad27100cfea0 Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 16:28:37 +0800 Subject: [PATCH 06/15] feat: add overwrite command and direction flag for codex --- cli/apply.go | 358 +++++++++++------- cli/apply_test.go | 62 ++- cli/integration_test.go | 86 ++++- cli/root.go | 1 + .../2026-02-07-codex-apply-phase1-phase3.md | 41 ++ ...26-02-07-codex-apply-two-way-directions.md | 32 +- ...dex-apply-direction-and-source-checkout.md | 22 +- 7 files changed, 432 insertions(+), 170 deletions(-) create mode 100644 docs/plans/jobs/2026-02-07-codex-apply-phase1-phase3.md diff --git a/cli/apply.go b/cli/apply.go index cf9ceda..efe5405 100644 --- a/cli/apply.go +++ b/cli/apply.go @@ -14,9 +14,37 @@ import ( "github.com/spf13/cobra" ) +const ( + transferToLocal = "local" + transferToWorktree = "worktree" +) + +type handoffMode string + +const ( + handoffApply handoffMode = "apply" + handoffOverwrite handoffMode = "overwrite" +) + type applyOptions struct { yes bool dryRun bool + to string + force bool +} + +type handoffOptions struct { + yes bool + dryRun bool + to string +} + +type transferPlan struct { + to string + sourceRoot string + sourceName string + destinationRoot string + destinationName string } type applyConflictError struct { @@ -37,137 +65,212 @@ func newApplyCommand() *cobra.Command { opts := &applyOptions{} cmd := &cobra.Command{ Use: "apply ", - Short: "Apply changes between a Codex worktree and the local checkout", + Short: "Apply non-destructive changes between a Codex worktree and local checkout", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - modeCtx, err := resolveModeContext(cmd, false) - if err != nil { - return err - } - cfg, ok := configFromContext(ctx) - if !ok || modeCtx.mode != modeCodex { - return fmt.Errorf("apply is only supported in --mode=codex") + mode := handoffApply + if opts.force { + mode = handoffOverwrite } + return runCodexHandoff(cmd, strings.TrimSpace(args[0]), handoffOptions{ + yes: opts.yes, + dryRun: opts.dryRun, + to: opts.to, + }, mode) + }, + } - runner := defaultRunner() - repoRoot, err := repoRoot(ctx, runner) - if err != nil { - return err - } - if _, err := git.CurrentBranch(ctx, runner); err != nil { - return err - } + cmd.Flags().BoolVar(&opts.yes, "yes", false, "skip confirmation prompts") + cmd.Flags().BoolVar(&opts.dryRun, "dry-run", false, "show git commands without executing") + cmd.Flags().StringVar(&opts.to, "to", transferToLocal, "transfer destination: local or worktree") + cmd.Flags().BoolVar(&opts.force, "force", false, "compatibility alias for overwrite behavior") + return cmd +} - if !cmd.Flags().Changed("yes") { - opts.yes = !cfg.Cleanup.Confirm - } +func newOverwriteCommand() *cobra.Command { + opts := &handoffOptions{} + cmd := &cobra.Command{ + Use: "overwrite ", + Short: "Overwrite destination with source changes in codex mode", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runCodexHandoff(cmd, strings.TrimSpace(args[0]), *opts, handoffOverwrite) + }, + } - opaqueID := strings.TrimSpace(args[0]) - if opaqueID == "" { - return fmt.Errorf("task query cannot be empty") - } + cmd.Flags().BoolVar(&opts.yes, "yes", false, "skip confirmation prompts") + cmd.Flags().BoolVar(&opts.dryRun, "dry-run", false, "show git commands without executing") + cmd.Flags().StringVar(&opts.to, "to", transferToLocal, "transfer destination: local or worktree") + return cmd +} + +func runCodexHandoff(cmd *cobra.Command, opaqueID string, opts handoffOptions, mode handoffMode) error { + ctx := cmd.Context() + modeCtx, err := resolveModeContext(cmd, false) + if err != nil { + return err + } + cfg, ok := configFromContext(ctx) + if !ok || modeCtx.mode != modeCodex { + return fmt.Errorf("%s is only supported in --mode=codex", mode) + } + + if !cmd.Flags().Changed("yes") { + opts.yes = !cfg.Cleanup.Confirm + } + + if opaqueID == "" { + return fmt.Errorf("task query cannot be empty") + } + + runner := defaultRunner() + repoRoot, err := repoRoot(ctx, runner) + if err != nil { + return err + } + if _, err := git.CurrentBranch(ctx, runner); err != nil { + return err + } + + wtPath, found, err := resolveCodexWorktreePath(ctx, runner, repoRoot, modeCtx.codexWorktrees, opaqueID) + if err != nil { + return err + } + if !found { + return fmt.Errorf("no Codex worktree found for %q under %s", opaqueID, filepath.Join("$CODEX_HOME", "worktrees")) + } + + plan, err := resolveTransferPlan(repoRoot, wtPath, opts.to) + if err != nil { + return err + } - wtPath, ok, err := resolveCodexWorktreePath(ctx, runner, repoRoot, modeCtx.codexWorktrees, opaqueID) - if err != nil { + if mode == handoffApply { + reasons, err := detectApplyConflicts(ctx, runner, plan.destinationRoot, plan.destinationName, plan.sourceRoot) + if err != nil { + return err + } + if len(reasons) > 0 { + if err := printConflictReasons(cmd.OutOrStdout(), reasons); err != nil { return err } - if !ok { - return fmt.Errorf("no Codex worktree found for %q under %s", opaqueID, filepath.Join("$CODEX_HOME", "worktrees")) - } - - conflictReasons, err := detectApplyConflicts(ctx, runner, repoRoot, wtPath) - if err != nil { + if err := printOverwriteHint(cmd.OutOrStdout(), plan.to, opaqueID); err != nil { return err } - if len(conflictReasons) > 0 { - if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\n", ui.WarningStyle.Render("apply conflict detected:")); err != nil { + return fmt.Errorf("apply aborted due to conflicts") + } + } + + if mode == handoffOverwrite { + if err := confirmOverwrite(cmd, opts.yes, plan); err != nil { + return err + } + } + + err = transferChanges(ctx, cmd, runner, plan.sourceRoot, plan.destinationRoot, opts.dryRun, mode == handoffOverwrite) + if err != nil { + if mode == handoffApply { + var conflictErr *applyConflictError + if errors.As(err, &conflictErr) { + if err := printConflictReasons(cmd.OutOrStdout(), []string{conflictErr.reason}); err != nil { return err } - for _, reason := range conflictReasons { - if _, err := fmt.Fprintf(cmd.OutOrStdout(), "- %s\n", ui.WarningStyle.Render(reason)); err != nil { - return err - } + if err := printOverwriteHint(cmd.OutOrStdout(), plan.to, opaqueID); err != nil { + return err } + return fmt.Errorf("apply aborted due to conflicts") + } + } + return err + } - if !opts.yes { - ok, err := confirmPrompt(cmd.InOrStdin(), cmd.OutOrStdout(), "Overwrite the Codex worktree from the local checkout?") - if err != nil { - return err - } - if !ok { - return errCanceled - } - ok, err = confirmPrompt(cmd.InOrStdin(), cmd.OutOrStdout(), "This will discard worktree changes. Continue?") - if err != nil { - return err - } - if !ok { - return errCanceled - } - } + if _, err := fmt.Fprintln(cmd.OutOrStdout(), ui.SuccessStyle.Render(fmt.Sprintf("%s complete", mode))); err != nil { + return err + } + return nil +} - return overwriteWorktreeChanges(ctx, cmd, runner, repoRoot, wtPath, opts.dryRun) - } +func resolveTransferPlan(repoRoot, worktreePath, to string) (transferPlan, error) { + switch strings.TrimSpace(to) { + case transferToLocal: + return transferPlan{ + to: transferToLocal, + sourceRoot: worktreePath, + sourceName: "Codex worktree", + destinationRoot: repoRoot, + destinationName: "local checkout", + }, nil + case transferToWorktree: + return transferPlan{ + to: transferToWorktree, + sourceRoot: repoRoot, + sourceName: "local checkout", + destinationRoot: worktreePath, + destinationName: "Codex worktree", + }, nil + default: + return transferPlan{}, fmt.Errorf("invalid --to value %q (expected local or worktree)", to) + } +} - if err := applyWorktreeChanges(ctx, cmd, runner, repoRoot, wtPath, opts.dryRun); err != nil { - var conflictErr *applyConflictError - if errors.As(err, &conflictErr) { - if !opts.yes { - if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\n", ui.WarningStyle.Render(conflictErr.reason)); err != nil { - return err - } - ok, err := confirmPrompt(cmd.InOrStdin(), cmd.OutOrStdout(), "Overwrite the Codex worktree from the local checkout?") - if err != nil { - return err - } - if !ok { - return errCanceled - } - ok, err = confirmPrompt(cmd.InOrStdin(), cmd.OutOrStdout(), "This will discard worktree changes. Continue?") - if err != nil { - return err - } - if !ok { - return errCanceled - } - } - return overwriteWorktreeChanges(ctx, cmd, runner, repoRoot, wtPath, opts.dryRun) - } - return err - } - if _, err := fmt.Fprintln(cmd.OutOrStdout(), ui.SuccessStyle.Render("apply complete")); err != nil { - return err - } - return nil - }, +func printConflictReasons(out io.Writer, reasons []string) error { + if _, err := fmt.Fprintf(out, "%s\n", ui.WarningStyle.Render("apply conflict detected:")); err != nil { + return err } + for _, reason := range reasons { + if _, err := fmt.Fprintf(out, "- %s\n", ui.WarningStyle.Render(reason)); err != nil { + return err + } + } + return nil +} - cmd.Flags().BoolVar(&opts.yes, "yes", false, "skip confirmation prompts") - cmd.Flags().BoolVar(&opts.dryRun, "dry-run", false, "show git commands without executing") - return cmd +func printOverwriteHint(out io.Writer, to, opaqueID string) error { + _, err := fmt.Fprintf(out, "%s\n", ui.WarningStyle.Render(fmt.Sprintf("rerun with overwrite: gwtt overwrite --to %s %s", to, opaqueID))) + return err } -func detectApplyConflicts(ctx context.Context, runner git.Runner, repoRoot, worktreePath string) ([]string, error) { +func confirmOverwrite(cmd *cobra.Command, yes bool, plan transferPlan) error { + if yes { + return nil + } + ok, err := confirmPrompt(cmd.InOrStdin(), cmd.OutOrStdout(), fmt.Sprintf("Overwrite the %s from the %s?", plan.destinationName, plan.sourceName)) + if err != nil { + return err + } + if !ok { + return errCanceled + } + ok, err = confirmPrompt(cmd.InOrStdin(), cmd.OutOrStdout(), fmt.Sprintf("This will discard %s changes. Continue?", plan.destinationName)) + if err != nil { + return err + } + if !ok { + return errCanceled + } + return nil +} + +func detectApplyConflicts(ctx context.Context, runner git.Runner, destinationRoot, destinationName, sourceRoot string) ([]string, error) { var reasons []string - dirty, err := isDirty(ctx, runner, repoRoot) + dirty, err := isDirty(ctx, runner, destinationRoot) if err != nil { return nil, err } if dirty { - reasons = append(reasons, "local checkout has uncommitted changes") + reasons = append(reasons, fmt.Sprintf("%s has uncommitted changes", destinationName)) } - localModified, err := modifiedFiles(ctx, runner, repoRoot) + sourceModified, err := modifiedFiles(ctx, runner, sourceRoot) if err != nil { return nil, err } - worktreeModified, err := modifiedFiles(ctx, runner, worktreePath) + destinationModified, err := modifiedFiles(ctx, runner, destinationRoot) if err != nil { return nil, err } - if intersects(localModified, worktreeModified) { + if intersects(sourceModified, destinationModified) { reasons = append(reasons, "both sides modified the same file(s)") } @@ -236,56 +339,21 @@ func intersects(left, right map[string]struct{}) bool { return false } -func applyWorktreeChanges(ctx context.Context, cmd *cobra.Command, runner git.Runner, repoRoot, worktreePath string, dryRun bool) error { - patch, err := gitDiff(ctx, runner, worktreePath) - if err != nil { - return err - } - - patchFile, err := writeTempPatch(patch) - if err != nil { - return err - } - defer func() { - if err := removeTempPatch(patchFile); err != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to remove temp patch %s: %v\n", patchFile, err) - } - }() - - if patch != "" { - if err := runGit(ctx, cmd, dryRun, runner, "-C", repoRoot, "apply", "--check", patchFile); err != nil { - return &applyConflictError{reason: "apply patch check failed", err: err} - } - if err := runGit(ctx, cmd, dryRun, runner, "-C", repoRoot, "apply", patchFile); err != nil { - return fmt.Errorf("apply patch: %w", err) +func transferChanges(ctx context.Context, cmd *cobra.Command, runner git.Runner, sourceRoot, destinationRoot string, dryRun, resetDestination bool) error { + if resetDestination { + if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "reset", "--hard"); err != nil { + return err } - } - - untracked, err := listUntracked(ctx, runner, worktreePath) - if err != nil { - return err - } - for _, rel := range untracked { - if err := copyFile(worktreePath, repoRoot, rel, dryRun, cmd.OutOrStdout()); err != nil { + if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "clean", "-fd"); err != nil { return err } } - return nil -} - -func overwriteWorktreeChanges(ctx context.Context, cmd *cobra.Command, runner git.Runner, repoRoot, worktreePath string, dryRun bool) error { - if err := runGit(ctx, cmd, dryRun, runner, "-C", worktreePath, "reset", "--hard"); err != nil { - return err - } - if err := runGit(ctx, cmd, dryRun, runner, "-C", worktreePath, "clean", "-fd"); err != nil { - return err - } - - patch, err := gitDiff(ctx, runner, repoRoot) + patch, err := gitDiff(ctx, runner, sourceRoot) if err != nil { return err } + patchFile, err := writeTempPatch(patch) if err != nil { return err @@ -297,27 +365,23 @@ func overwriteWorktreeChanges(ctx context.Context, cmd *cobra.Command, runner gi }() if patch != "" { - if err := runGit(ctx, cmd, dryRun, runner, "-C", worktreePath, "apply", "--check", patchFile); err != nil { + if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "apply", "--check", patchFile); err != nil { return &applyConflictError{reason: "apply patch check failed", err: err} } - if err := runGit(ctx, cmd, dryRun, runner, "-C", worktreePath, "apply", patchFile); err != nil { - return err + if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "apply", patchFile); err != nil { + return fmt.Errorf("apply patch: %w", err) } } - untracked, err := listUntracked(ctx, runner, repoRoot) + untracked, err := listUntracked(ctx, runner, sourceRoot) if err != nil { return err } for _, rel := range untracked { - if err := copyFile(repoRoot, worktreePath, rel, dryRun, cmd.OutOrStdout()); err != nil { + if err := copyFile(sourceRoot, destinationRoot, rel, dryRun, cmd.OutOrStdout()); err != nil { return err } } - - if _, err := fmt.Fprintln(cmd.OutOrStdout(), ui.SuccessStyle.Render("overwrite complete")); err != nil { - return err - } return nil } diff --git a/cli/apply_test.go b/cli/apply_test.go index 15be2d5..05b3cdc 100644 --- a/cli/apply_test.go +++ b/cli/apply_test.go @@ -17,7 +17,7 @@ func TestDetectApplyConflicts(t *testing.T) { }, } - reasons, err := detectApplyConflicts(context.Background(), runner, "/repo", "/codex") + reasons, err := detectApplyConflicts(context.Background(), runner, "/repo", "local checkout", "/codex") if err != nil { t.Fatalf("detectApplyConflicts error: %v", err) } @@ -53,7 +53,7 @@ func TestDetectApplyConflictsNone(t *testing.T) { }, } - reasons, err := detectApplyConflicts(context.Background(), runner, "/repo", "/codex") + reasons, err := detectApplyConflicts(context.Background(), runner, "/repo", "local checkout", "/codex") if err != nil { t.Fatalf("detectApplyConflicts error: %v", err) } @@ -61,3 +61,61 @@ func TestDetectApplyConflictsNone(t *testing.T) { t.Fatalf("expected no conflict reasons, got %v", reasons) } } + +func TestResolveTransferPlan(t *testing.T) { + repoRoot := "/repo" + worktreePath := "/codex" + + tests := []struct { + name string + to string + wantSrc string + wantDst string + wantSrcN string + wantDstN string + expectErr bool + }{ + { + name: "to local", + to: transferToLocal, + wantSrc: worktreePath, + wantDst: repoRoot, + wantSrcN: "Codex worktree", + wantDstN: "local checkout", + }, + { + name: "to worktree", + to: transferToWorktree, + wantSrc: repoRoot, + wantDst: worktreePath, + wantSrcN: "local checkout", + wantDstN: "Codex worktree", + }, + { + name: "invalid destination", + to: "somewhere", + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := resolveTransferPlan(repoRoot, worktreePath, tt.to) + if tt.expectErr { + if err == nil { + t.Fatalf("expected error") + } + return + } + if err != nil { + t.Fatalf("resolveTransferPlan() error: %v", err) + } + if got.sourceRoot != tt.wantSrc || got.destinationRoot != tt.wantDst { + t.Fatalf("resolveTransferPlan() roots = (%q -> %q), want (%q -> %q)", got.sourceRoot, got.destinationRoot, tt.wantSrc, tt.wantDst) + } + if got.sourceName != tt.wantSrcN || got.destinationName != tt.wantDstN { + t.Fatalf("resolveTransferPlan() names = (%q -> %q), want (%q -> %q)", got.sourceName, got.destinationName, tt.wantSrcN, tt.wantDstN) + } + }) + } +} diff --git a/cli/integration_test.go b/cli/integration_test.go index 999fe75..5028cac 100644 --- a/cli/integration_test.go +++ b/cli/integration_test.go @@ -245,7 +245,7 @@ func TestIntegrationCodexListStatusFiltering(t *testing.T) { } } -func TestIntegrationApplyConflictConfirmation(t *testing.T) { +func TestIntegrationApplyConflictRequiresExplicitOverwrite(t *testing.T) { repoDir := initRepo(t, true) codexHome := setCodexHome(t) opaqueID := "apply01" @@ -254,26 +254,94 @@ func TestIntegrationApplyConflictConfirmation(t *testing.T) { writeFile(t, repoDir, "shared.txt", "local change\n") writeFile(t, codexPath, "shared.txt", "codex change\n") - _, err := runCLIError(t, repoDir, "no\n", "--nocolor", "--mode", "codex", "apply", opaqueID) - if err == nil || !strings.Contains(err.Error(), "canceled") { - t.Fatalf("expected apply to be canceled, got %v", err) + _, err := runCLIError(t, repoDir, "", "--nocolor", "--mode", "codex", "apply", opaqueID) + if err == nil || !strings.Contains(err.Error(), "apply aborted due to conflicts") { + t.Fatalf("expected apply conflict abort, got %v", err) } - content, err := os.ReadFile(filepath.Join(codexPath, "shared.txt")) + content, err := os.ReadFile(filepath.Join(repoDir, "shared.txt")) + if err != nil { + t.Fatalf("read local file: %v", err) + } + if string(content) != "local change\n" { + t.Fatalf("expected local content to remain, got %q", string(content)) + } + + content, err = os.ReadFile(filepath.Join(codexPath, "shared.txt")) if err != nil { t.Fatalf("read codex file: %v", err) } if string(content) != "codex change\n" { t.Fatalf("expected codex content to remain, got %q", string(content)) } +} - runCLI(t, repoDir, "", "--nocolor", "--mode", "codex", "apply", opaqueID, "--yes") - content, err = os.ReadFile(filepath.Join(codexPath, "shared.txt")) +func TestIntegrationOverwriteToLocalConfirmation(t *testing.T) { + repoDir := initRepo(t, true) + codexHome := setCodexHome(t) + opaqueID := "apply02" + codexPath := addCodexWorktree(t, repoDir, codexHome, opaqueID) + + writeFile(t, repoDir, "shared.txt", "local change\n") + writeFile(t, codexPath, "shared.txt", "codex change\n") + + _, err := runCLIError(t, repoDir, "no\n", "--nocolor", "--mode", "codex", "overwrite", opaqueID) + if err == nil || !strings.Contains(err.Error(), "canceled") { + t.Fatalf("expected overwrite to be canceled, got %v", err) + } + + content, err := os.ReadFile(filepath.Join(repoDir, "shared.txt")) if err != nil { - t.Fatalf("read codex file after apply: %v", err) + t.Fatalf("read local file after cancel: %v", err) } if string(content) != "local change\n" { - t.Fatalf("expected codex content to be overwritten, got %q", string(content)) + t.Fatalf("expected local content unchanged after cancel, got %q", string(content)) + } + + runCLI(t, repoDir, "", "--nocolor", "--mode", "codex", "overwrite", opaqueID, "--to", "local", "--yes") + content, err = os.ReadFile(filepath.Join(repoDir, "shared.txt")) + if err != nil { + t.Fatalf("read local file after overwrite: %v", err) + } + if string(content) != "codex change\n" { + t.Fatalf("expected local content overwritten from codex, got %q", string(content)) + } +} + +func TestIntegrationApplyAndOverwriteToWorktree(t *testing.T) { + repoDir := initRepo(t, true) + codexHome := setCodexHome(t) + opaqueID := "apply03" + codexPath := addCodexWorktree(t, repoDir, codexHome, opaqueID) + + writeFile(t, repoDir, "shared.txt", "from local\n") + + runCLI(t, repoDir, "", "--nocolor", "--mode", "codex", "apply", opaqueID, "--to", "worktree") + + content, err := os.ReadFile(filepath.Join(codexPath, "shared.txt")) + if err != nil { + t.Fatalf("read codex file after apply --to worktree: %v", err) + } + if string(content) != "from local\n" { + t.Fatalf("expected worktree content from local apply, got %q", string(content)) + } + + writeFile(t, repoDir, "shared.txt", "source overwrite\n") + writeFile(t, codexPath, "shared.txt", "dest dirty\n") + + _, err = runCLIError(t, repoDir, "", "--nocolor", "--mode", "codex", "apply", opaqueID, "--to", "worktree") + if err == nil || !strings.Contains(err.Error(), "apply aborted due to conflicts") { + t.Fatalf("expected apply --to worktree conflict abort, got %v", err) + } + + runCLI(t, repoDir, "", "--nocolor", "--mode", "codex", "apply", opaqueID, "--to", "worktree", "--force", "--yes") + + content, err = os.ReadFile(filepath.Join(codexPath, "shared.txt")) + if err != nil { + t.Fatalf("read codex file after apply --force: %v", err) + } + if string(content) != "source overwrite\n" { + t.Fatalf("expected apply --force to overwrite worktree, got %q", string(content)) } } diff --git a/cli/root.go b/cli/root.go index bd23175..9144012 100644 --- a/cli/root.go +++ b/cli/root.go @@ -124,6 +124,7 @@ func gitWorkTreeCommand() (*cobra.Command, *runState) { newListCommand(), newStatusCommand(), newApplyCommand(), + newOverwriteCommand(), newTUICommand(), ) diff --git a/docs/plans/jobs/2026-02-07-codex-apply-phase1-phase3.md b/docs/plans/jobs/2026-02-07-codex-apply-phase1-phase3.md new file mode 100644 index 0000000..1042e2f --- /dev/null +++ b/docs/plans/jobs/2026-02-07-codex-apply-phase1-phase3.md @@ -0,0 +1,41 @@ +--- +title: "Implement codex apply/overwrite Phase 1 and Phase 3" +created-date: 2026-02-07 +status: completed +agent: codex +--- + +## Summary + +- Locked Phase 1 command/UX spec in implementation and documentation: + - `apply --to local|worktree` (default: `local`) + - `overwrite --to local|worktree` as a peer command + - `apply --force` as compatibility alias to overwrite behavior +- Implemented Phase 3 behavior in codex mode: + - direction-aware transfer for both `local` and `worktree` destinations + - explicit `overwrite` command with confirmation gating and `--yes` bypass + - removed implicit overwrite fallback from `apply` conflict flow + - conflict handling now exits with guidance to rerun `overwrite --to ...` +- Added/updated tests for direction handling, overwrite confirmation, and apply conflict behavior. +- Updated plan/research documents to reflect Phase 1 and Phase 3 completion/decisions. + +## Files Updated + +- `cli/apply.go` +- `cli/root.go` +- `cli/apply_test.go` +- `cli/integration_test.go` +- `docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md` +- `docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md` + +## Verification + +- `GOCACHE=/tmp/go-build go test ./...` + +## Related Plans + +- `docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md` + +## Related Research + +- `docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md` diff --git a/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md b/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md index 8566ef4..ceed06c 100644 --- a/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md +++ b/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md @@ -16,6 +16,7 @@ Implement a two-way `apply`/`overwrite` command model in codex mode with explici - Align CLI command surface with Codex app-level concepts (`apply` and `overwrite` as peer actions). - Improve dry-run clarity for direction and destructive operations. - Perform foundational CLI refactors first (no behavior change). +- Split `apply` implementation into focused files after direction behavior lands. ## Non-Goals @@ -27,12 +28,12 @@ Implement a two-way `apply`/`overwrite` command model in codex mode with explici ### Phase 1: Spec and UX Lock -- [ ] Finalize command matrix for: +- [x] Finalize command matrix for: - `apply --to local|worktree` - `overwrite --to local|worktree` - optional `apply --force` alias policy -- [ ] Finalize conflict behavior for non-destructive apply (no implicit direction switching). -- [ ] Finalize dry-run output schema (header + preflight + action list). +- [x] Finalize conflict behavior for non-destructive apply (no implicit direction switching). +- [x] Finalize dry-run output schema (header + preflight + action list). ### Phase 2: Foundational Refactor (No Behavior Change) @@ -43,10 +44,10 @@ Implement a two-way `apply`/`overwrite` command model in codex mode with explici ### Phase 3: Apply/Overwrite Implementation -- [ ] Implement `--to` direction support in codex handoff commands. -- [ ] Introduce `overwrite` as peer command (or locked alias strategy per Phase 1). -- [ ] Remove implicit overwrite fallback from `apply` conflict path. -- [ ] Preserve confirmation gating (`--yes`) for destructive paths. +- [x] Implement `--to` direction support in codex handoff commands. +- [x] Introduce `overwrite` as peer command (or locked alias strategy per Phase 1). +- [x] Remove implicit overwrite fallback from `apply` conflict path. +- [x] Preserve confirmation gating (`--yes`) for destructive paths. ### Phase 4: Dry-Run and Messages @@ -59,6 +60,22 @@ Implement a two-way `apply`/`overwrite` command model in codex mode with explici - [ ] Update README/man pages/help text for new command semantics. - [ ] Update related research and mark this plan status appropriately. +### Phase 6: Apply File-Split Refactor + +- [ ] Split command wiring/flags into `cli/apply_command.go`. +- [ ] Split codex worktree resolution and validation into `cli/apply_resolve.go`. +- [ ] Split conflict detection helpers into `cli/apply_conflicts.go`. +- [ ] Split direction-agnostic transfer logic into `cli/apply_transfer.go`. +- [ ] Split temp patch + file copy helpers into `cli/apply_files.go`. + +### Phase 7: Post-Refactor Verify and Docs Pass + +- [ ] Run full Go test suite and ensure no behavior regressions. +- [ ] Update/add tests where refactor changed package/file boundaries. +- [ ] Re-verify command help text for `apply`/`overwrite`/flags. +- [ ] Reconcile README and man pages with final refactored behavior. +- [ ] Update related plan/research/job docs and mark completion status. + ## Acceptance Criteria - `apply` and `overwrite` semantics are explicit and direction-stable. @@ -70,6 +87,7 @@ Implement a two-way `apply`/`overwrite` command model in codex mode with explici - Confirmation wording must remain clear when destructive destination reset is involved. - Refactor-first approach lowers risk but can surface latent coupling in `cli`. +- `cli/apply.go` remains intentionally consolidated for the first behavior cut; file split refactor is tracked in Phase 6. ## Related Research diff --git a/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md b/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md index 309bf79..0fbac1a 100644 --- a/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md +++ b/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md @@ -117,11 +117,23 @@ Apply vs overwrite matrix: - reset and clean destination before transfer; - then apply tracked diff + copy untracked files from source. -## Open Questions - -- Should we ship `apply --force` alias in v1, or keep only explicit `overwrite` to keep UX strict? -- Do we need `--output json` for dry-run planning (machine-readable action plan), or is text-first enough initially? -- Do we need a future `relink` command for app-like branch wiring, or is explicit error + manual Git workflow sufficient? +## Phase 1 Decisions (Locked) + +- Ship `overwrite` as a peer command and keep `apply --force` as compatibility alias. +- `apply` remains non-destructive and direction-stable; conflicts now fail with overwrite guidance. +- Keep dry-run redesign text-first for now; JSON planning output is deferred. +- Keep relink/wiring out of scope in this phase. + +## Implementation Status (Current) + +- Implemented: + - `apply --to local|worktree` + - `overwrite --to local|worktree` + - `apply --force` compatibility alias + - no implicit direction switching on `apply` conflicts +- Pending: + - dry-run plan-style output redesign + - split `cli/apply.go` into focused files (`apply_command`, `apply_resolve`, `apply_conflicts`, `apply_transfer`, `apply_files`) ## Dry-Run Output Redesign (Draft) From d5a2cd91c75abbcb8aeddbdd38c7b8d586b4a57d Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 16:58:13 +0800 Subject: [PATCH 07/15] feat: add codex overwrite and dry-run plan output --- README.md | 23 ++- cli/apply.go | 189 +++++++++++++++--- cli/apply_test.go | 48 +++++ cli/integration_test.go | 53 ++++- .../2026-02-07-codex-apply-phase4-phase5.md | 48 +++++ ...26-02-07-codex-apply-two-way-directions.md | 10 +- ...dex-apply-direction-and-source-checkout.md | 5 +- man/man1/git-worktree-tasks.1 | 2 +- man/man1/git-worktree-tasks_apply.1 | 14 +- man/man1/git-worktree-tasks_overwrite.1 | 51 +++++ man/man1/gwtt.1 | 2 +- man/man1/gwtt_apply.1 | 14 +- man/man1/gwtt_overwrite.1 | 51 +++++ 13 files changed, 460 insertions(+), 50 deletions(-) create mode 100644 docs/plans/jobs/2026-02-07-codex-apply-phase4-phase5.md create mode 100644 man/man1/git-worktree-tasks_overwrite.1 create mode 100644 man/man1/gwtt_overwrite.1 diff --git a/README.md b/README.md index cb6f7ab..ac38fd6 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,8 @@ name = "nord" | Command | Alias | Description | | --------- | ----- | -------------------------------------------------------------------- | -| `apply` | | Apply Codex worktree changes to the local checkout (codex mode only) | +| `apply` | | Apply non-destructive changes between Codex worktree and local checkout (codex mode only) | +| `overwrite` | | Destructively replace destination with source changes in codex mode | | `create` | | Create a worktree and branch for a task | | `list` | `ls` | List task worktrees | | `status` | | Show detailed worktree status | @@ -399,20 +400,30 @@ gwtt finish "my-task" --cleanup --yes ### Applying Changes (Codex Mode) ```bash -# Apply Codex worktree changes to local checkout +# Non-destructive apply (default direction: worktree -> local) gwtt --mode codex apply -# Overwrite Codex worktree from local checkout without prompts -gwtt --mode codex apply --yes +# Reverse non-destructive apply (local -> worktree) +gwtt --mode codex apply --to worktree -# Preview without executing +# Destructive overwrite (requires confirmation unless --yes) +gwtt --mode codex overwrite --to local +gwtt --mode codex overwrite --to worktree --yes + +# Compatibility alias for overwrite +gwtt --mode codex apply --to worktree --force --yes + +# Preview with structured plan + command echo gwtt --mode codex apply --dry-run ``` **Notes:** - In codex mode, `` is the directory directly under `$CODEX_HOME/worktrees`. -- If conflicts are detected, `gwtt` prompts to overwrite the Codex worktree (second confirmation). `--yes` skips prompts. +- `apply` is non-destructive and will not switch direction automatically on conflict. +- On conflict, `apply` exits with a next-step hint for `overwrite --to ...`. +- `overwrite` resets/cleans the destination before transfer and is destructive by design. +- `--dry-run` prints `plan`, `preflight`, and `actions` sections, then echoes the underlying git/copy operations. ### Cleanup diff --git a/cli/apply.go b/cli/apply.go index efe5405..93606a9 100644 --- a/cli/apply.go +++ b/cli/apply.go @@ -47,6 +47,13 @@ type transferPlan struct { destinationName string } +type transferPreflight struct { + destinationDirty bool + overlappingFiles int + trackedPatch bool + untrackedFiles []string +} + type applyConflictError struct { reason string err error @@ -145,11 +152,22 @@ func runCodexHandoff(cmd *cobra.Command, opaqueID string, opts handoffOptions, m return err } - if mode == handoffApply { - reasons, err := detectApplyConflicts(ctx, runner, plan.destinationRoot, plan.destinationName, plan.sourceRoot) + preflight := transferPreflight{} + if opts.dryRun || mode == handoffApply { + preflight, err = collectTransferPreflight(ctx, runner, plan.sourceRoot, plan.destinationRoot, opts.dryRun) if err != nil { return err } + } + + if opts.dryRun { + if err := printDryRunPlan(cmd.OutOrStdout(), mode, plan, preflight); err != nil { + return err + } + } + + if mode == handoffApply { + reasons := conflictReasonsForApply(preflight, plan.destinationName) if len(reasons) > 0 { if err := printConflictReasons(cmd.OutOrStdout(), reasons); err != nil { return err @@ -161,7 +179,7 @@ func runCodexHandoff(cmd *cobra.Command, opaqueID string, opts handoffOptions, m } } - if mode == handoffOverwrite { + if mode == handoffOverwrite && !opts.dryRun { if err := confirmOverwrite(cmd, opts.yes, plan); err != nil { return err } @@ -214,7 +232,7 @@ func resolveTransferPlan(repoRoot, worktreePath, to string) (transferPlan, error } func printConflictReasons(out io.Writer, reasons []string) error { - if _, err := fmt.Fprintf(out, "%s\n", ui.WarningStyle.Render("apply conflict detected:")); err != nil { + if _, err := fmt.Fprintf(out, "%s\n", ui.WarningStyle.Render("apply blocked (non-destructive mode):")); err != nil { return err } for _, reason := range reasons { @@ -226,7 +244,10 @@ func printConflictReasons(out io.Writer, reasons []string) error { } func printOverwriteHint(out io.Writer, to, opaqueID string) error { - _, err := fmt.Fprintf(out, "%s\n", ui.WarningStyle.Render(fmt.Sprintf("rerun with overwrite: gwtt overwrite --to %s %s", to, opaqueID))) + if _, err := fmt.Fprintf(out, "%s\n", ui.WarningStyle.Render(fmt.Sprintf("next step: gwtt overwrite --to %s %s", to, opaqueID))); err != nil { + return err + } + _, err := fmt.Fprintf(out, "%s\n", ui.WarningStyle.Render("add --yes to skip overwrite confirmation prompts")) return err } @@ -252,29 +273,11 @@ func confirmOverwrite(cmd *cobra.Command, yes bool, plan transferPlan) error { } func detectApplyConflicts(ctx context.Context, runner git.Runner, destinationRoot, destinationName, sourceRoot string) ([]string, error) { - var reasons []string - - dirty, err := isDirty(ctx, runner, destinationRoot) - if err != nil { - return nil, err - } - if dirty { - reasons = append(reasons, fmt.Sprintf("%s has uncommitted changes", destinationName)) - } - - sourceModified, err := modifiedFiles(ctx, runner, sourceRoot) - if err != nil { - return nil, err - } - destinationModified, err := modifiedFiles(ctx, runner, destinationRoot) + preflight, err := collectTransferPreflight(ctx, runner, sourceRoot, destinationRoot, false) if err != nil { return nil, err } - if intersects(sourceModified, destinationModified) { - reasons = append(reasons, "both sides modified the same file(s)") - } - - return reasons, nil + return conflictReasonsForApply(preflight, destinationName), nil } func isDirty(ctx context.Context, runner git.Runner, repoRoot string) (bool, error) { @@ -324,19 +327,147 @@ func modifiedFiles(ctx context.Context, runner git.Runner, repoRoot string) (map return files, nil } -func intersects(left, right map[string]struct{}) bool { +func intersectCount(left, right map[string]struct{}) int { if len(left) == 0 || len(right) == 0 { - return false + return 0 } if len(left) > len(right) { left, right = right, left } + count := 0 for key := range left { if _, ok := right[key]; ok { - return true + count++ + } + } + return count +} + +func collectTransferPreflight(ctx context.Context, runner git.Runner, sourceRoot, destinationRoot string, includeTransferState bool) (transferPreflight, error) { + destinationDirty, err := isDirty(ctx, runner, destinationRoot) + if err != nil { + return transferPreflight{}, err + } + + sourceModified, err := modifiedFiles(ctx, runner, sourceRoot) + if err != nil { + return transferPreflight{}, err + } + destinationModified, err := modifiedFiles(ctx, runner, destinationRoot) + if err != nil { + return transferPreflight{}, err + } + + preflight := transferPreflight{ + destinationDirty: destinationDirty, + overlappingFiles: intersectCount(sourceModified, destinationModified), + } + + if includeTransferState { + patch, err := gitDiff(ctx, runner, sourceRoot) + if err != nil { + return transferPreflight{}, err + } + preflight.trackedPatch = patch != "" + + untracked, err := listUntracked(ctx, runner, sourceRoot) + if err != nil { + return transferPreflight{}, err + } + preflight.untrackedFiles = untracked + } + + return preflight, nil +} + +func conflictReasonsForApply(preflight transferPreflight, destinationName string) []string { + var reasons []string + if preflight.destinationDirty { + reasons = append(reasons, fmt.Sprintf("%s has uncommitted changes", destinationName)) + } + if preflight.overlappingFiles > 0 { + reasons = append(reasons, fmt.Sprintf("both sides modified %d overlapping file(s)", preflight.overlappingFiles)) + } + return reasons +} + +func printDryRunPlan(out io.Writer, mode handoffMode, plan transferPlan, preflight transferPreflight) error { + if _, err := fmt.Fprintf(out, "%s plan\n", mode); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " to: %s\n", plan.to); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " source: %s\n", plan.sourceRoot); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " destination: %s\n", plan.destinationRoot); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " overwrite: %t\n", mode == handoffOverwrite); err != nil { + return err + } + + if _, err := fmt.Fprintln(out, ""); err != nil { + return err + } + if _, err := fmt.Fprintln(out, "preflight"); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " destination_dirty: %t\n", preflight.destinationDirty); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " overlapping_files: %d\n", preflight.overlappingFiles); err != nil { + return err + } + trackedPatch := "none" + if preflight.trackedPatch { + trackedPatch = "present" + } + if _, err := fmt.Fprintf(out, " tracked_patch: %s\n", trackedPatch); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " untracked_files: %d\n", len(preflight.untrackedFiles)); err != nil { + return err + } + + if _, err := fmt.Fprintln(out, ""); err != nil { + return err + } + if _, err := fmt.Fprintln(out, "actions"); err != nil { + return err + } + actions := dryRunActions(mode, plan, preflight) + for idx, action := range actions { + if _, err := fmt.Fprintf(out, " %d. %s\n", idx+1, action); err != nil { + return err } } - return false + return nil +} + +func dryRunActions(mode handoffMode, plan transferPlan, preflight transferPreflight) []string { + actions := make([]string, 0, len(preflight.untrackedFiles)+4) + + if mode == handoffOverwrite { + actions = append(actions, "[destructive] "+formatGitCommand([]string{"-C", plan.destinationRoot, "reset", "--hard"})) + actions = append(actions, "[destructive] "+formatGitCommand([]string{"-C", plan.destinationRoot, "clean", "-fd"})) + } + + if preflight.trackedPatch { + actions = append(actions, formatGitCommand([]string{"-C", plan.destinationRoot, "apply", "--check", ""})) + actions = append(actions, formatGitCommand([]string{"-C", plan.destinationRoot, "apply", ""})) + } + + for _, rel := range preflight.untrackedFiles { + actions = append(actions, fmt.Sprintf("copy %s -> %s", filepath.Join(plan.sourceRoot, rel), filepath.Join(plan.destinationRoot, rel))) + } + + if len(actions) == 0 { + actions = append(actions, "no tracked or untracked changes detected") + } + + return actions } func transferChanges(ctx context.Context, cmd *cobra.Command, runner git.Runner, sourceRoot, destinationRoot string, dryRun, resetDestination bool) error { diff --git a/cli/apply_test.go b/cli/apply_test.go index 05b3cdc..dfbab4a 100644 --- a/cli/apply_test.go +++ b/cli/apply_test.go @@ -119,3 +119,51 @@ func TestResolveTransferPlan(t *testing.T) { }) } } + +func TestConflictReasonsForApply(t *testing.T) { + reasons := conflictReasonsForApply(transferPreflight{ + destinationDirty: true, + overlappingFiles: 3, + }, "local checkout") + + if len(reasons) != 2 { + t.Fatalf("expected 2 reasons, got %d", len(reasons)) + } + if !strings.Contains(reasons[0], "local checkout has uncommitted changes") { + t.Fatalf("unexpected dirty reason: %v", reasons) + } + if !strings.Contains(reasons[1], "both sides modified 3 overlapping file(s)") { + t.Fatalf("unexpected overlap reason: %v", reasons) + } +} + +func TestDryRunActions(t *testing.T) { + plan := transferPlan{ + destinationRoot: "/repo", + sourceRoot: "/codex", + } + preflight := transferPreflight{ + trackedPatch: true, + untrackedFiles: []string{"a.txt"}, + } + + actions := dryRunActions(handoffOverwrite, plan, preflight) + if len(actions) != 5 { + t.Fatalf("expected 5 actions, got %d (%v)", len(actions), actions) + } + if !strings.Contains(actions[0], "[destructive] git -C /repo reset --hard") { + t.Fatalf("expected destructive reset action, got %q", actions[0]) + } + if !strings.Contains(actions[1], "[destructive] git -C /repo clean -fd") { + t.Fatalf("expected destructive clean action, got %q", actions[1]) + } + if !strings.Contains(actions[2], "apply --check ") { + t.Fatalf("expected apply check action, got %q", actions[2]) + } + if !strings.Contains(actions[3], "apply ") { + t.Fatalf("expected apply action, got %q", actions[3]) + } + if !strings.Contains(actions[4], "copy /codex/a.txt -> /repo/a.txt") { + t.Fatalf("expected copy action, got %q", actions[4]) + } +} diff --git a/cli/integration_test.go b/cli/integration_test.go index 5028cac..be9c071 100644 --- a/cli/integration_test.go +++ b/cli/integration_test.go @@ -254,10 +254,13 @@ func TestIntegrationApplyConflictRequiresExplicitOverwrite(t *testing.T) { writeFile(t, repoDir, "shared.txt", "local change\n") writeFile(t, codexPath, "shared.txt", "codex change\n") - _, err := runCLIError(t, repoDir, "", "--nocolor", "--mode", "codex", "apply", opaqueID) + output, err := runCLIError(t, repoDir, "", "--nocolor", "--mode", "codex", "apply", opaqueID) if err == nil || !strings.Contains(err.Error(), "apply aborted due to conflicts") { t.Fatalf("expected apply conflict abort, got %v", err) } + if !strings.Contains(output, "next step: gwtt overwrite --to local "+opaqueID) { + t.Fatalf("expected overwrite next-step guidance, got output:\n%s", output) + } content, err := os.ReadFile(filepath.Join(repoDir, "shared.txt")) if err != nil { @@ -276,6 +279,54 @@ func TestIntegrationApplyConflictRequiresExplicitOverwrite(t *testing.T) { } } +func TestIntegrationApplyDryRunPlanOutput(t *testing.T) { + repoDir := initRepo(t, true) + codexHome := setCodexHome(t) + opaqueID := "applydry1" + codexPath := addCodexWorktree(t, repoDir, codexHome, opaqueID) + + writeFile(t, codexPath, "dry.txt", "codex dry-run\n") + + output := runCLI(t, repoDir, "", "--nocolor", "--mode", "codex", "apply", opaqueID, "--dry-run") + for _, want := range []string{ + "apply plan", + " to: local", + "preflight", + "actions", + "tracked_patch:", + "untracked_files:", + "copy ", + } { + if !strings.Contains(output, want) { + t.Fatalf("expected output to contain %q, got:\n%s", want, output) + } + } +} + +func TestIntegrationOverwriteDryRunPlanOutput(t *testing.T) { + repoDir := initRepo(t, true) + codexHome := setCodexHome(t) + opaqueID := "applydry2" + codexPath := addCodexWorktree(t, repoDir, codexHome, opaqueID) + + writeFile(t, repoDir, "dry-overwrite.txt", "from local\n") + writeFile(t, codexPath, "dry-overwrite.txt", "from codex\n") + + output := runCLI(t, repoDir, "", "--nocolor", "--mode", "codex", "overwrite", opaqueID, "--to", "worktree", "--dry-run") + for _, want := range []string{ + "overwrite plan", + " to: worktree", + " overwrite: true", + "[destructive] git -C ", + "reset --hard", + "clean -fd", + } { + if !strings.Contains(output, want) { + t.Fatalf("expected output to contain %q, got:\n%s", want, output) + } + } +} + func TestIntegrationOverwriteToLocalConfirmation(t *testing.T) { repoDir := initRepo(t, true) codexHome := setCodexHome(t) diff --git a/docs/plans/jobs/2026-02-07-codex-apply-phase4-phase5.md b/docs/plans/jobs/2026-02-07-codex-apply-phase4-phase5.md new file mode 100644 index 0000000..800fa20 --- /dev/null +++ b/docs/plans/jobs/2026-02-07-codex-apply-phase4-phase5.md @@ -0,0 +1,48 @@ +--- +title: "Implement codex apply Phase 4 and Phase 5" +created-date: 2026-02-07 +status: completed +agent: codex +--- + +## Summary + +- Implemented structured dry-run output for codex handoff commands with: + - operation header (`plan`) + - transfer preflight summary (`preflight`) + - ordered action list (`actions`) including destructive markers for overwrite +- Updated apply conflict messages to be explicit, non-destructive, and action-oriented: + - shows why apply was blocked + - prints concrete next-step command for `overwrite --to ...` + - includes `--yes` hint for confirmation bypass +- Added tests covering dry-run schema and next-step guidance. +- Updated README command semantics for `apply`/`overwrite`. +- Regenerated man pages, including `gwtt_overwrite(1)` and `git-worktree-tasks_overwrite(1)`. + +## Files Updated + +- `cli/apply.go` +- `cli/apply_test.go` +- `cli/integration_test.go` +- `README.md` +- `man/man1/gwtt_apply.1` +- `man/man1/git-worktree-tasks_apply.1` +- `man/man1/gwtt_overwrite.1` +- `man/man1/git-worktree-tasks_overwrite.1` +- `man/man1/gwtt.1` +- `man/man1/git-worktree-tasks.1` +- `docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md` +- `docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md` + +## Verification + +- `GOCACHE=/tmp/go-build go test ./...` +- `GOCACHE=/tmp/go-build make man` + +## Related Plans + +- `docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md` + +## Related Research + +- `docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md` diff --git a/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md b/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md index ceed06c..4436a86 100644 --- a/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md +++ b/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md @@ -51,14 +51,14 @@ Implement a two-way `apply`/`overwrite` command model in codex mode with explici ### Phase 4: Dry-Run and Messages -- [ ] Implement structured dry-run plan output with explicit source/destination and destructive markers. -- [ ] Update user-facing conflict and next-step guidance. +- [x] Implement structured dry-run plan output with explicit source/destination and destructive markers. +- [x] Update user-facing conflict and next-step guidance. ### Phase 5: Tests and Docs -- [ ] Add/adjust unit and integration tests for direction + overwrite behaviors. -- [ ] Update README/man pages/help text for new command semantics. -- [ ] Update related research and mark this plan status appropriately. +- [x] Add/adjust unit and integration tests for direction + overwrite behaviors. +- [x] Update README/man pages/help text for new command semantics. +- [x] Update related research and mark this plan status appropriately. ### Phase 6: Apply File-Split Refactor diff --git a/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md b/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md index 0fbac1a..354c8d5 100644 --- a/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md +++ b/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md @@ -131,9 +131,12 @@ Apply vs overwrite matrix: - `overwrite --to local|worktree` - `apply --force` compatibility alias - no implicit direction switching on `apply` conflicts + - dry-run plan-style output with `plan`, `preflight`, and `actions` sections + - conflict output updated with explicit overwrite next-step guidance + - README/man/help updates for `apply` + `overwrite` semantics - Pending: - - dry-run plan-style output redesign - split `cli/apply.go` into focused files (`apply_command`, `apply_resolve`, `apply_conflicts`, `apply_transfer`, `apply_files`) + - post-refactor verification/doc pass after the file-split phase ## Dry-Run Output Redesign (Draft) diff --git a/man/man1/git-worktree-tasks.1 b/man/man1/git-worktree-tasks.1 index 0a8b4ef..f318d5e 100644 --- a/man/man1/git-worktree-tasks.1 +++ b/man/man1/git-worktree-tasks.1 @@ -35,4 +35,4 @@ Create, manage, and clean up git worktrees based on task names. .SH SEE ALSO -\fBgit-worktree-tasks-apply(1)\fP, \fBgit-worktree-tasks-cleanup(1)\fP, \fBgit-worktree-tasks-create(1)\fP, \fBgit-worktree-tasks-finish(1)\fP, \fBgit-worktree-tasks-list(1)\fP, \fBgit-worktree-tasks-status(1)\fP +\fBgit-worktree-tasks-apply(1)\fP, \fBgit-worktree-tasks-cleanup(1)\fP, \fBgit-worktree-tasks-create(1)\fP, \fBgit-worktree-tasks-finish(1)\fP, \fBgit-worktree-tasks-list(1)\fP, \fBgit-worktree-tasks-overwrite(1)\fP, \fBgit-worktree-tasks-status(1)\fP diff --git a/man/man1/git-worktree-tasks_apply.1 b/man/man1/git-worktree-tasks_apply.1 index 0c1b98f..5fcc133 100644 --- a/man/man1/git-worktree-tasks_apply.1 +++ b/man/man1/git-worktree-tasks_apply.1 @@ -2,25 +2,33 @@ .TH "GIT-WORKTREE-TASKS" "1" "Feb 2026" "git-worktree-tasks" "Git Worktree Tasks Manual" .SH NAME -git-worktree-tasks-apply - Apply changes between a Codex worktree and the local checkout +git-worktree-tasks-apply - Apply non-destructive changes between a Codex worktree and local checkout .SH SYNOPSIS -\fBgit-worktree-tasks apply [flags]\fP +\fBgit-worktree-tasks apply [flags]\fP .SH DESCRIPTION -Apply changes between a Codex worktree and the local checkout +Apply non-destructive changes between a Codex worktree and local checkout .SH OPTIONS \fB--dry-run\fP[=false] show git commands without executing +.PP +\fB--force\fP[=false] + compatibility alias for overwrite behavior + .PP \fB-h\fP, \fB--help\fP[=false] help for apply +.PP +\fB--to\fP="local" + transfer destination: local or worktree + .PP \fB--yes\fP[=false] skip confirmation prompts diff --git a/man/man1/git-worktree-tasks_overwrite.1 b/man/man1/git-worktree-tasks_overwrite.1 new file mode 100644 index 0000000..e4e9eb3 --- /dev/null +++ b/man/man1/git-worktree-tasks_overwrite.1 @@ -0,0 +1,51 @@ +.nh +.TH "GIT-WORKTREE-TASKS" "1" "Feb 2026" "git-worktree-tasks" "Git Worktree Tasks Manual" + +.SH NAME +git-worktree-tasks-overwrite - Overwrite destination with source changes in codex mode + + +.SH SYNOPSIS +\fBgit-worktree-tasks overwrite [flags]\fP + + +.SH DESCRIPTION +Overwrite destination with source changes in codex mode + + +.SH OPTIONS +\fB--dry-run\fP[=false] + show git commands without executing + +.PP +\fB-h\fP, \fB--help\fP[=false] + help for overwrite + +.PP +\fB--to\fP="local" + transfer destination: local or worktree + +.PP +\fB--yes\fP[=false] + skip confirmation prompts + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mode\fP="classic" + execution mode: classic or codex + +.PP +\fB--nocolor\fP[=false] + disable color output + +.PP +\fB--theme\fP="default" + color theme: default, dracula, gruvbox, nord, solarized + +.PP +\fB--themes\fP[=false] + print available themes and exit + + +.SH SEE ALSO +\fBgit-worktree-tasks(1)\fP diff --git a/man/man1/gwtt.1 b/man/man1/gwtt.1 index 048f778..cd25b53 100644 --- a/man/man1/gwtt.1 +++ b/man/man1/gwtt.1 @@ -35,4 +35,4 @@ Create, manage, and clean up git worktrees based on task names. .SH SEE ALSO -\fBgwtt-apply(1)\fP, \fBgwtt-cleanup(1)\fP, \fBgwtt-create(1)\fP, \fBgwtt-finish(1)\fP, \fBgwtt-list(1)\fP, \fBgwtt-status(1)\fP +\fBgwtt-apply(1)\fP, \fBgwtt-cleanup(1)\fP, \fBgwtt-create(1)\fP, \fBgwtt-finish(1)\fP, \fBgwtt-list(1)\fP, \fBgwtt-overwrite(1)\fP, \fBgwtt-status(1)\fP diff --git a/man/man1/gwtt_apply.1 b/man/man1/gwtt_apply.1 index f3d505f..90c5b87 100644 --- a/man/man1/gwtt_apply.1 +++ b/man/man1/gwtt_apply.1 @@ -2,25 +2,33 @@ .TH "GWTT" "1" "Feb 2026" "gwtt" "Git Worktree Tasks Manual" .SH NAME -gwtt-apply - Apply changes between a Codex worktree and the local checkout +gwtt-apply - Apply non-destructive changes between a Codex worktree and local checkout .SH SYNOPSIS -\fBgwtt apply [flags]\fP +\fBgwtt apply [flags]\fP .SH DESCRIPTION -Apply changes between a Codex worktree and the local checkout +Apply non-destructive changes between a Codex worktree and local checkout .SH OPTIONS \fB--dry-run\fP[=false] show git commands without executing +.PP +\fB--force\fP[=false] + compatibility alias for overwrite behavior + .PP \fB-h\fP, \fB--help\fP[=false] help for apply +.PP +\fB--to\fP="local" + transfer destination: local or worktree + .PP \fB--yes\fP[=false] skip confirmation prompts diff --git a/man/man1/gwtt_overwrite.1 b/man/man1/gwtt_overwrite.1 new file mode 100644 index 0000000..f39476c --- /dev/null +++ b/man/man1/gwtt_overwrite.1 @@ -0,0 +1,51 @@ +.nh +.TH "GWTT" "1" "Feb 2026" "gwtt" "Git Worktree Tasks Manual" + +.SH NAME +gwtt-overwrite - Overwrite destination with source changes in codex mode + + +.SH SYNOPSIS +\fBgwtt overwrite [flags]\fP + + +.SH DESCRIPTION +Overwrite destination with source changes in codex mode + + +.SH OPTIONS +\fB--dry-run\fP[=false] + show git commands without executing + +.PP +\fB-h\fP, \fB--help\fP[=false] + help for overwrite + +.PP +\fB--to\fP="local" + transfer destination: local or worktree + +.PP +\fB--yes\fP[=false] + skip confirmation prompts + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mode\fP="classic" + execution mode: classic or codex + +.PP +\fB--nocolor\fP[=false] + disable color output + +.PP +\fB--theme\fP="default" + color theme: default, dracula, gruvbox, nord, solarized + +.PP +\fB--themes\fP[=false] + print available themes and exit + + +.SH SEE ALSO +\fBgwtt(1)\fP From 55474445f94a3081b1be12293b83bf5c5c7e970b Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 17:23:11 +0800 Subject: [PATCH 08/15] chore: add -m shorthand for `--mode` flag --- README.md | 4 +-- cli/mode_test.go | 14 +++++++++++ cli/root.go | 4 +-- .../jobs/2026-02-07-mode-short-flag-alias.md | 25 +++++++++++++++++++ man/man1/git-worktree-tasks.1 | 2 +- man/man1/git-worktree-tasks_apply.1 | 2 +- man/man1/git-worktree-tasks_cleanup.1 | 2 +- man/man1/git-worktree-tasks_create.1 | 2 +- man/man1/git-worktree-tasks_finish.1 | 2 +- man/man1/git-worktree-tasks_list.1 | 2 +- man/man1/git-worktree-tasks_overwrite.1 | 2 +- man/man1/git-worktree-tasks_status.1 | 2 +- man/man1/gwtt.1 | 2 +- man/man1/gwtt_apply.1 | 2 +- man/man1/gwtt_cleanup.1 | 2 +- man/man1/gwtt_create.1 | 2 +- man/man1/gwtt_finish.1 | 2 +- man/man1/gwtt_list.1 | 2 +- man/man1/gwtt_overwrite.1 | 2 +- man/man1/gwtt_status.1 | 2 +- 20 files changed, 59 insertions(+), 20 deletions(-) create mode 100644 docs/plans/jobs/2026-02-07-mode-short-flag-alias.md diff --git a/README.md b/README.md index ac38fd6..1ca2a42 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ ln -s $(which git-worktree-tasks) $(dirname $(which git-worktree-tasks))/gwtt Settings resolve in this order (highest precedence first): -1. `--theme` / `--mode` flags +1. `--theme` / `--mode` (`-m`) flags 2. Environment variables 3. Project config (`gwtt.config.toml` or `gwtt.toml` in repo root) 4. User config (`$HOME/.config/gwtt/config.toml`) @@ -652,4 +652,4 @@ This project is licensed under the MIT License — see the [LICENSE](https://git - Task names are slugified (lowercase, hyphens replace spaces) - Paths are relative by default; use `--abs` for absolute - Use `--dry-run` to preview git commands -- Global flags: `--mode`, `--theme`, `--nocolor`, `--themes` +- Global flags: `--mode` (`-m`), `--theme`, `--nocolor`, `--themes` diff --git a/cli/mode_test.go b/cli/mode_test.go index ee51d88..870c6d9 100644 --- a/cli/mode_test.go +++ b/cli/mode_test.go @@ -69,6 +69,20 @@ func TestModePrecedence(t *testing.T) { t.Fatalf("mode = %q, want %q", got, modeClassic) } }) + + t.Run("short_flag_over_env", func(t *testing.T) { + project := t.TempDir() + t.Setenv("HOME", t.TempDir()) + t.Setenv("GWTT_MODE", "codex") + + got, err := runModeCommand(t, project, "-m", "classic") + if err != nil { + t.Fatalf("run command: %v", err) + } + if got != modeClassic { + t.Fatalf("mode = %q, want %q", got, modeClassic) + } + }) } func TestModeValidation(t *testing.T) { diff --git a/cli/root.go b/cli/root.go index 9144012..844c545 100644 --- a/cli/root.go +++ b/cli/root.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -var Version = "0.1.2-alpha.1" +var Version = "0.1.2-alpha.2" var ( errCanceled = errors.New("git worktree task process canceled") @@ -77,7 +77,7 @@ func gitWorkTreeCommand() (*cobra.Command, *runState) { cmd.SetErr(os.Stderr) cmd.PersistentFlags().BoolVar(&state.noColor, "nocolor", false, "disable color output") cmd.PersistentFlags().StringVar(&state.theme, "theme", ui.DefaultThemeName(), "color theme: "+strings.Join(ui.ThemeNames(), ", ")) - cmd.PersistentFlags().StringVar(&state.mode, "mode", "classic", "execution mode: classic or codex") + cmd.PersistentFlags().StringVarP(&state.mode, "mode", "m", "classic", "execution mode: classic or codex") cmd.PersistentFlags().BoolVar(&state.listThemes, "themes", false, "print available themes and exit") cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { if state.listThemes { diff --git a/docs/plans/jobs/2026-02-07-mode-short-flag-alias.md b/docs/plans/jobs/2026-02-07-mode-short-flag-alias.md new file mode 100644 index 0000000..9bd8b2b --- /dev/null +++ b/docs/plans/jobs/2026-02-07-mode-short-flag-alias.md @@ -0,0 +1,25 @@ +--- +title: "Add -m shorthand alias for --mode" +created-date: 2026-02-07 +status: completed +agent: codex +--- + +## Summary + +- Added persistent shorthand `-m` for the global `--mode` flag. +- Added mode precedence test coverage for short flag usage (`-m` overriding env/config). +- Updated README global flag references to include `-m`. +- Regenerated man pages so help output documents the short alias. + +## Files Updated + +- `cli/root.go` +- `cli/mode_test.go` +- `README.md` +- `man/man1/*` (regenerated) + +## Verification + +- `GOCACHE=/tmp/go-build go test ./...` +- `GOCACHE=/tmp/go-build make man` diff --git a/man/man1/git-worktree-tasks.1 b/man/man1/git-worktree-tasks.1 index f318d5e..df286db 100644 --- a/man/man1/git-worktree-tasks.1 +++ b/man/man1/git-worktree-tasks.1 @@ -18,7 +18,7 @@ Create, manage, and clean up git worktrees based on task names. help for git-worktree-tasks .PP -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/git-worktree-tasks_apply.1 b/man/man1/git-worktree-tasks_apply.1 index 5fcc133..ddf4033 100644 --- a/man/man1/git-worktree-tasks_apply.1 +++ b/man/man1/git-worktree-tasks_apply.1 @@ -35,7 +35,7 @@ Apply non-destructive changes between a Codex worktree and local checkout .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/git-worktree-tasks_cleanup.1 b/man/man1/git-worktree-tasks_cleanup.1 index a4a77dd..9817e1f 100644 --- a/man/man1/git-worktree-tasks_cleanup.1 +++ b/man/man1/git-worktree-tasks_cleanup.1 @@ -43,7 +43,7 @@ Remove a task worktree and/or branch .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/git-worktree-tasks_create.1 b/man/man1/git-worktree-tasks_create.1 index ac28d10..5c7e029 100644 --- a/man/man1/git-worktree-tasks_create.1 +++ b/man/man1/git-worktree-tasks_create.1 @@ -43,7 +43,7 @@ Create a worktree and branch for a task .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/git-worktree-tasks_finish.1 b/man/man1/git-worktree-tasks_finish.1 index c30f210..f960646 100644 --- a/man/man1/git-worktree-tasks_finish.1 +++ b/man/man1/git-worktree-tasks_finish.1 @@ -59,7 +59,7 @@ Merge a task branch into a target branch .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/git-worktree-tasks_list.1 b/man/man1/git-worktree-tasks_list.1 index fd36d22..23cf8ad 100644 --- a/man/man1/git-worktree-tasks_list.1 +++ b/man/man1/git-worktree-tasks_list.1 @@ -47,7 +47,7 @@ List task worktrees .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/git-worktree-tasks_overwrite.1 b/man/man1/git-worktree-tasks_overwrite.1 index e4e9eb3..a94888f 100644 --- a/man/man1/git-worktree-tasks_overwrite.1 +++ b/man/man1/git-worktree-tasks_overwrite.1 @@ -31,7 +31,7 @@ Overwrite destination with source changes in codex mode .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/git-worktree-tasks_status.1 b/man/man1/git-worktree-tasks_status.1 index 1c1f7c2..312e12a 100644 --- a/man/man1/git-worktree-tasks_status.1 +++ b/man/man1/git-worktree-tasks_status.1 @@ -51,7 +51,7 @@ Show detailed worktree status .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/gwtt.1 b/man/man1/gwtt.1 index cd25b53..b543a7d 100644 --- a/man/man1/gwtt.1 +++ b/man/man1/gwtt.1 @@ -18,7 +18,7 @@ Create, manage, and clean up git worktrees based on task names. help for gwtt .PP -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/gwtt_apply.1 b/man/man1/gwtt_apply.1 index 90c5b87..a96b80d 100644 --- a/man/man1/gwtt_apply.1 +++ b/man/man1/gwtt_apply.1 @@ -35,7 +35,7 @@ Apply non-destructive changes between a Codex worktree and local checkout .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/gwtt_cleanup.1 b/man/man1/gwtt_cleanup.1 index ec26829..d8e24a6 100644 --- a/man/man1/gwtt_cleanup.1 +++ b/man/man1/gwtt_cleanup.1 @@ -43,7 +43,7 @@ Remove a task worktree and/or branch .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/gwtt_create.1 b/man/man1/gwtt_create.1 index 2142d44..1c4bdf6 100644 --- a/man/man1/gwtt_create.1 +++ b/man/man1/gwtt_create.1 @@ -43,7 +43,7 @@ Create a worktree and branch for a task .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/gwtt_finish.1 b/man/man1/gwtt_finish.1 index 92c38f8..6f7138c 100644 --- a/man/man1/gwtt_finish.1 +++ b/man/man1/gwtt_finish.1 @@ -59,7 +59,7 @@ Merge a task branch into a target branch .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/gwtt_list.1 b/man/man1/gwtt_list.1 index 27e49b1..77c2e9e 100644 --- a/man/man1/gwtt_list.1 +++ b/man/man1/gwtt_list.1 @@ -47,7 +47,7 @@ List task worktrees .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/gwtt_overwrite.1 b/man/man1/gwtt_overwrite.1 index f39476c..9bff581 100644 --- a/man/man1/gwtt_overwrite.1 +++ b/man/man1/gwtt_overwrite.1 @@ -31,7 +31,7 @@ Overwrite destination with source changes in codex mode .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP diff --git a/man/man1/gwtt_status.1 b/man/man1/gwtt_status.1 index 244fbe5..e199029 100644 --- a/man/man1/gwtt_status.1 +++ b/man/man1/gwtt_status.1 @@ -51,7 +51,7 @@ Show detailed worktree status .SH OPTIONS INHERITED FROM PARENT COMMANDS -\fB--mode\fP="classic" +\fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex .PP From 7f91c5ddc12193429323163100420e5245bf34af Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 17:40:01 +0800 Subject: [PATCH 09/15] refactor: split apply into smaller CLI modules --- cli/apply.go | 639 ------------------ cli/apply_command.go | 226 +++++++ cli/apply_conflicts.go | 128 ++++ cli/apply_files.go | 102 +++ cli/apply_resolve.go | 57 ++ cli/apply_transfer.go | 167 +++++ .../2026-02-07-codex-apply-phase6-phase7.md | 46 ++ ...26-02-07-codex-apply-two-way-directions.md | 24 +- ...dex-apply-direction-and-source-checkout.md | 19 +- 9 files changed, 751 insertions(+), 657 deletions(-) delete mode 100644 cli/apply.go create mode 100644 cli/apply_command.go create mode 100644 cli/apply_conflicts.go create mode 100644 cli/apply_files.go create mode 100644 cli/apply_resolve.go create mode 100644 cli/apply_transfer.go create mode 100644 docs/plans/jobs/2026-02-07-codex-apply-phase6-phase7.md diff --git a/cli/apply.go b/cli/apply.go deleted file mode 100644 index 93606a9..0000000 --- a/cli/apply.go +++ /dev/null @@ -1,639 +0,0 @@ -package cli - -import ( - "context" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "strings" - - "github.com/pi2pie/git-worktree-tasks/internal/git" - "github.com/pi2pie/git-worktree-tasks/ui" - "github.com/spf13/cobra" -) - -const ( - transferToLocal = "local" - transferToWorktree = "worktree" -) - -type handoffMode string - -const ( - handoffApply handoffMode = "apply" - handoffOverwrite handoffMode = "overwrite" -) - -type applyOptions struct { - yes bool - dryRun bool - to string - force bool -} - -type handoffOptions struct { - yes bool - dryRun bool - to string -} - -type transferPlan struct { - to string - sourceRoot string - sourceName string - destinationRoot string - destinationName string -} - -type transferPreflight struct { - destinationDirty bool - overlappingFiles int - trackedPatch bool - untrackedFiles []string -} - -type applyConflictError struct { - reason string - err error -} - -func (e *applyConflictError) Error() string { - if e.err == nil { - return e.reason - } - return fmt.Sprintf("%s: %v", e.reason, e.err) -} - -func (e *applyConflictError) Unwrap() error { return e.err } - -func newApplyCommand() *cobra.Command { - opts := &applyOptions{} - cmd := &cobra.Command{ - Use: "apply ", - Short: "Apply non-destructive changes between a Codex worktree and local checkout", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - mode := handoffApply - if opts.force { - mode = handoffOverwrite - } - return runCodexHandoff(cmd, strings.TrimSpace(args[0]), handoffOptions{ - yes: opts.yes, - dryRun: opts.dryRun, - to: opts.to, - }, mode) - }, - } - - cmd.Flags().BoolVar(&opts.yes, "yes", false, "skip confirmation prompts") - cmd.Flags().BoolVar(&opts.dryRun, "dry-run", false, "show git commands without executing") - cmd.Flags().StringVar(&opts.to, "to", transferToLocal, "transfer destination: local or worktree") - cmd.Flags().BoolVar(&opts.force, "force", false, "compatibility alias for overwrite behavior") - return cmd -} - -func newOverwriteCommand() *cobra.Command { - opts := &handoffOptions{} - cmd := &cobra.Command{ - Use: "overwrite ", - Short: "Overwrite destination with source changes in codex mode", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runCodexHandoff(cmd, strings.TrimSpace(args[0]), *opts, handoffOverwrite) - }, - } - - cmd.Flags().BoolVar(&opts.yes, "yes", false, "skip confirmation prompts") - cmd.Flags().BoolVar(&opts.dryRun, "dry-run", false, "show git commands without executing") - cmd.Flags().StringVar(&opts.to, "to", transferToLocal, "transfer destination: local or worktree") - return cmd -} - -func runCodexHandoff(cmd *cobra.Command, opaqueID string, opts handoffOptions, mode handoffMode) error { - ctx := cmd.Context() - modeCtx, err := resolveModeContext(cmd, false) - if err != nil { - return err - } - cfg, ok := configFromContext(ctx) - if !ok || modeCtx.mode != modeCodex { - return fmt.Errorf("%s is only supported in --mode=codex", mode) - } - - if !cmd.Flags().Changed("yes") { - opts.yes = !cfg.Cleanup.Confirm - } - - if opaqueID == "" { - return fmt.Errorf("task query cannot be empty") - } - - runner := defaultRunner() - repoRoot, err := repoRoot(ctx, runner) - if err != nil { - return err - } - if _, err := git.CurrentBranch(ctx, runner); err != nil { - return err - } - - wtPath, found, err := resolveCodexWorktreePath(ctx, runner, repoRoot, modeCtx.codexWorktrees, opaqueID) - if err != nil { - return err - } - if !found { - return fmt.Errorf("no Codex worktree found for %q under %s", opaqueID, filepath.Join("$CODEX_HOME", "worktrees")) - } - - plan, err := resolveTransferPlan(repoRoot, wtPath, opts.to) - if err != nil { - return err - } - - preflight := transferPreflight{} - if opts.dryRun || mode == handoffApply { - preflight, err = collectTransferPreflight(ctx, runner, plan.sourceRoot, plan.destinationRoot, opts.dryRun) - if err != nil { - return err - } - } - - if opts.dryRun { - if err := printDryRunPlan(cmd.OutOrStdout(), mode, plan, preflight); err != nil { - return err - } - } - - if mode == handoffApply { - reasons := conflictReasonsForApply(preflight, plan.destinationName) - if len(reasons) > 0 { - if err := printConflictReasons(cmd.OutOrStdout(), reasons); err != nil { - return err - } - if err := printOverwriteHint(cmd.OutOrStdout(), plan.to, opaqueID); err != nil { - return err - } - return fmt.Errorf("apply aborted due to conflicts") - } - } - - if mode == handoffOverwrite && !opts.dryRun { - if err := confirmOverwrite(cmd, opts.yes, plan); err != nil { - return err - } - } - - err = transferChanges(ctx, cmd, runner, plan.sourceRoot, plan.destinationRoot, opts.dryRun, mode == handoffOverwrite) - if err != nil { - if mode == handoffApply { - var conflictErr *applyConflictError - if errors.As(err, &conflictErr) { - if err := printConflictReasons(cmd.OutOrStdout(), []string{conflictErr.reason}); err != nil { - return err - } - if err := printOverwriteHint(cmd.OutOrStdout(), plan.to, opaqueID); err != nil { - return err - } - return fmt.Errorf("apply aborted due to conflicts") - } - } - return err - } - - if _, err := fmt.Fprintln(cmd.OutOrStdout(), ui.SuccessStyle.Render(fmt.Sprintf("%s complete", mode))); err != nil { - return err - } - return nil -} - -func resolveTransferPlan(repoRoot, worktreePath, to string) (transferPlan, error) { - switch strings.TrimSpace(to) { - case transferToLocal: - return transferPlan{ - to: transferToLocal, - sourceRoot: worktreePath, - sourceName: "Codex worktree", - destinationRoot: repoRoot, - destinationName: "local checkout", - }, nil - case transferToWorktree: - return transferPlan{ - to: transferToWorktree, - sourceRoot: repoRoot, - sourceName: "local checkout", - destinationRoot: worktreePath, - destinationName: "Codex worktree", - }, nil - default: - return transferPlan{}, fmt.Errorf("invalid --to value %q (expected local or worktree)", to) - } -} - -func printConflictReasons(out io.Writer, reasons []string) error { - if _, err := fmt.Fprintf(out, "%s\n", ui.WarningStyle.Render("apply blocked (non-destructive mode):")); err != nil { - return err - } - for _, reason := range reasons { - if _, err := fmt.Fprintf(out, "- %s\n", ui.WarningStyle.Render(reason)); err != nil { - return err - } - } - return nil -} - -func printOverwriteHint(out io.Writer, to, opaqueID string) error { - if _, err := fmt.Fprintf(out, "%s\n", ui.WarningStyle.Render(fmt.Sprintf("next step: gwtt overwrite --to %s %s", to, opaqueID))); err != nil { - return err - } - _, err := fmt.Fprintf(out, "%s\n", ui.WarningStyle.Render("add --yes to skip overwrite confirmation prompts")) - return err -} - -func confirmOverwrite(cmd *cobra.Command, yes bool, plan transferPlan) error { - if yes { - return nil - } - ok, err := confirmPrompt(cmd.InOrStdin(), cmd.OutOrStdout(), fmt.Sprintf("Overwrite the %s from the %s?", plan.destinationName, plan.sourceName)) - if err != nil { - return err - } - if !ok { - return errCanceled - } - ok, err = confirmPrompt(cmd.InOrStdin(), cmd.OutOrStdout(), fmt.Sprintf("This will discard %s changes. Continue?", plan.destinationName)) - if err != nil { - return err - } - if !ok { - return errCanceled - } - return nil -} - -func detectApplyConflicts(ctx context.Context, runner git.Runner, destinationRoot, destinationName, sourceRoot string) ([]string, error) { - preflight, err := collectTransferPreflight(ctx, runner, sourceRoot, destinationRoot, false) - if err != nil { - return nil, err - } - return conflictReasonsForApply(preflight, destinationName), nil -} - -func isDirty(ctx context.Context, runner git.Runner, repoRoot string) (bool, error) { - stdout, stderr, err := runner.Run(ctx, "-C", repoRoot, "status", "--porcelain") - if err != nil { - if stderr != "" { - return false, fmt.Errorf("git status: %w: %s", err, stderr) - } - return false, fmt.Errorf("git status: %w", err) - } - return strings.TrimSpace(stdout) != "", nil -} - -func modifiedFiles(ctx context.Context, runner git.Runner, repoRoot string) (map[string]struct{}, error) { - files := map[string]struct{}{} - - diffNames, stderr, err := runner.Run(ctx, "-C", repoRoot, "diff", "--name-only", "HEAD") - if err != nil { - if stderr != "" { - return nil, fmt.Errorf("git diff --name-only: %w: %s", err, stderr) - } - return nil, fmt.Errorf("git diff --name-only: %w", err) - } - for _, line := range strings.Split(diffNames, "\n") { - trimmed := strings.TrimSpace(line) - if trimmed == "" { - continue - } - files[trimmed] = struct{}{} - } - - untracked, stderr, err := runner.Run(ctx, "-C", repoRoot, "ls-files", "--others", "--exclude-standard") - if err != nil { - if stderr != "" { - return nil, fmt.Errorf("git ls-files: %w: %s", err, stderr) - } - return nil, fmt.Errorf("git ls-files: %w", err) - } - for _, line := range strings.Split(untracked, "\n") { - trimmed := strings.TrimSpace(line) - if trimmed == "" { - continue - } - files[trimmed] = struct{}{} - } - - return files, nil -} - -func intersectCount(left, right map[string]struct{}) int { - if len(left) == 0 || len(right) == 0 { - return 0 - } - if len(left) > len(right) { - left, right = right, left - } - count := 0 - for key := range left { - if _, ok := right[key]; ok { - count++ - } - } - return count -} - -func collectTransferPreflight(ctx context.Context, runner git.Runner, sourceRoot, destinationRoot string, includeTransferState bool) (transferPreflight, error) { - destinationDirty, err := isDirty(ctx, runner, destinationRoot) - if err != nil { - return transferPreflight{}, err - } - - sourceModified, err := modifiedFiles(ctx, runner, sourceRoot) - if err != nil { - return transferPreflight{}, err - } - destinationModified, err := modifiedFiles(ctx, runner, destinationRoot) - if err != nil { - return transferPreflight{}, err - } - - preflight := transferPreflight{ - destinationDirty: destinationDirty, - overlappingFiles: intersectCount(sourceModified, destinationModified), - } - - if includeTransferState { - patch, err := gitDiff(ctx, runner, sourceRoot) - if err != nil { - return transferPreflight{}, err - } - preflight.trackedPatch = patch != "" - - untracked, err := listUntracked(ctx, runner, sourceRoot) - if err != nil { - return transferPreflight{}, err - } - preflight.untrackedFiles = untracked - } - - return preflight, nil -} - -func conflictReasonsForApply(preflight transferPreflight, destinationName string) []string { - var reasons []string - if preflight.destinationDirty { - reasons = append(reasons, fmt.Sprintf("%s has uncommitted changes", destinationName)) - } - if preflight.overlappingFiles > 0 { - reasons = append(reasons, fmt.Sprintf("both sides modified %d overlapping file(s)", preflight.overlappingFiles)) - } - return reasons -} - -func printDryRunPlan(out io.Writer, mode handoffMode, plan transferPlan, preflight transferPreflight) error { - if _, err := fmt.Fprintf(out, "%s plan\n", mode); err != nil { - return err - } - if _, err := fmt.Fprintf(out, " to: %s\n", plan.to); err != nil { - return err - } - if _, err := fmt.Fprintf(out, " source: %s\n", plan.sourceRoot); err != nil { - return err - } - if _, err := fmt.Fprintf(out, " destination: %s\n", plan.destinationRoot); err != nil { - return err - } - if _, err := fmt.Fprintf(out, " overwrite: %t\n", mode == handoffOverwrite); err != nil { - return err - } - - if _, err := fmt.Fprintln(out, ""); err != nil { - return err - } - if _, err := fmt.Fprintln(out, "preflight"); err != nil { - return err - } - if _, err := fmt.Fprintf(out, " destination_dirty: %t\n", preflight.destinationDirty); err != nil { - return err - } - if _, err := fmt.Fprintf(out, " overlapping_files: %d\n", preflight.overlappingFiles); err != nil { - return err - } - trackedPatch := "none" - if preflight.trackedPatch { - trackedPatch = "present" - } - if _, err := fmt.Fprintf(out, " tracked_patch: %s\n", trackedPatch); err != nil { - return err - } - if _, err := fmt.Fprintf(out, " untracked_files: %d\n", len(preflight.untrackedFiles)); err != nil { - return err - } - - if _, err := fmt.Fprintln(out, ""); err != nil { - return err - } - if _, err := fmt.Fprintln(out, "actions"); err != nil { - return err - } - actions := dryRunActions(mode, plan, preflight) - for idx, action := range actions { - if _, err := fmt.Fprintf(out, " %d. %s\n", idx+1, action); err != nil { - return err - } - } - return nil -} - -func dryRunActions(mode handoffMode, plan transferPlan, preflight transferPreflight) []string { - actions := make([]string, 0, len(preflight.untrackedFiles)+4) - - if mode == handoffOverwrite { - actions = append(actions, "[destructive] "+formatGitCommand([]string{"-C", plan.destinationRoot, "reset", "--hard"})) - actions = append(actions, "[destructive] "+formatGitCommand([]string{"-C", plan.destinationRoot, "clean", "-fd"})) - } - - if preflight.trackedPatch { - actions = append(actions, formatGitCommand([]string{"-C", plan.destinationRoot, "apply", "--check", ""})) - actions = append(actions, formatGitCommand([]string{"-C", plan.destinationRoot, "apply", ""})) - } - - for _, rel := range preflight.untrackedFiles { - actions = append(actions, fmt.Sprintf("copy %s -> %s", filepath.Join(plan.sourceRoot, rel), filepath.Join(plan.destinationRoot, rel))) - } - - if len(actions) == 0 { - actions = append(actions, "no tracked or untracked changes detected") - } - - return actions -} - -func transferChanges(ctx context.Context, cmd *cobra.Command, runner git.Runner, sourceRoot, destinationRoot string, dryRun, resetDestination bool) error { - if resetDestination { - if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "reset", "--hard"); err != nil { - return err - } - if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "clean", "-fd"); err != nil { - return err - } - } - - patch, err := gitDiff(ctx, runner, sourceRoot) - if err != nil { - return err - } - - patchFile, err := writeTempPatch(patch) - if err != nil { - return err - } - defer func() { - if err := removeTempPatch(patchFile); err != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to remove temp patch %s: %v\n", patchFile, err) - } - }() - - if patch != "" { - if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "apply", "--check", patchFile); err != nil { - return &applyConflictError{reason: "apply patch check failed", err: err} - } - if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "apply", patchFile); err != nil { - return fmt.Errorf("apply patch: %w", err) - } - } - - untracked, err := listUntracked(ctx, runner, sourceRoot) - if err != nil { - return err - } - for _, rel := range untracked { - if err := copyFile(sourceRoot, destinationRoot, rel, dryRun, cmd.OutOrStdout()); err != nil { - return err - } - } - return nil -} - -func gitDiff(ctx context.Context, runner git.Runner, repoRoot string) (string, error) { - stdout, stderr, err := runner.Run(ctx, "-C", repoRoot, "diff", "--binary", "HEAD") - if err != nil { - if stderr != "" { - return "", fmt.Errorf("git diff: %w: %s", err, stderr) - } - return "", fmt.Errorf("git diff: %w", err) - } - return stdout, nil -} - -func listUntracked(ctx context.Context, runner git.Runner, repoRoot string) ([]string, error) { - stdout, stderr, err := runner.Run(ctx, "-C", repoRoot, "ls-files", "--others", "--exclude-standard") - if err != nil { - if stderr != "" { - return nil, fmt.Errorf("git ls-files: %w: %s", err, stderr) - } - return nil, fmt.Errorf("git ls-files: %w", err) - } - var out []string - for _, line := range strings.Split(stdout, "\n") { - trimmed := strings.TrimSpace(line) - if trimmed == "" { - continue - } - out = append(out, trimmed) - } - return out, nil -} - -func writeTempPatch(contents string) (string, error) { - tmp, err := os.CreateTemp("", "gwtt-apply-*.patch") - if err != nil { - return "", err - } - if _, err := io.WriteString(tmp, contents); err != nil { - if closeErr := tmp.Close(); closeErr != nil { - return "", fmt.Errorf("write patch: %w (close error: %v)", err, closeErr) - } - return "", err - } - if err := tmp.Close(); err != nil { - return "", err - } - return tmp.Name(), nil -} - -func copyFile(srcRoot, dstRoot, rel string, dryRun bool, out io.Writer) (err error) { - srcPath := filepath.Join(srcRoot, rel) - dstPath := filepath.Join(dstRoot, rel) - - info, err := os.Lstat(srcPath) - if err != nil { - return err - } - - if info.Mode()&os.ModeSymlink != 0 { - target, err := os.Readlink(srcPath) - if err != nil { - return err - } - if dryRun { - _, err := fmt.Fprintf(out, "symlink %s -> %s (%s)\n", srcPath, dstPath, target) - return err - } - if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil { - return err - } - if err := os.Remove(dstPath); err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - return os.Symlink(target, dstPath) - } - - if !info.Mode().IsRegular() { - return fmt.Errorf("unsupported file type for copy: %s", srcPath) - } - - if dryRun { - _, err := fmt.Fprintf(out, "copy %s -> %s\n", srcPath, dstPath) - return err - } - - if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil { - return err - } - in, err := os.Open(srcPath) - if err != nil { - return err - } - defer func() { - if closeErr := in.Close(); closeErr != nil && err == nil { - err = closeErr - } - }() - - outFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) - if err != nil { - return err - } - defer func() { - if closeErr := outFile.Close(); closeErr != nil && err == nil { - err = closeErr - } - }() - - if _, err := io.Copy(outFile, in); err != nil { - return err - } - return nil -} - -func removeTempPatch(path string) error { - if strings.TrimSpace(path) == "" { - return nil - } - if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - return nil -} diff --git a/cli/apply_command.go b/cli/apply_command.go new file mode 100644 index 0000000..f7e9f9b --- /dev/null +++ b/cli/apply_command.go @@ -0,0 +1,226 @@ +package cli + +import ( + "errors" + "fmt" + "io" + "strings" + + "github.com/pi2pie/git-worktree-tasks/ui" + "github.com/spf13/cobra" +) + +const ( + transferToLocal = "local" + transferToWorktree = "worktree" +) + +type handoffMode string + +const ( + handoffApply handoffMode = "apply" + handoffOverwrite handoffMode = "overwrite" +) + +type applyOptions struct { + yes bool + dryRun bool + to string + force bool +} + +type handoffOptions struct { + yes bool + dryRun bool + to string +} + +type transferPlan struct { + to string + sourceRoot string + sourceName string + destinationRoot string + destinationName string +} + +type transferPreflight struct { + destinationDirty bool + overlappingFiles int + trackedPatch bool + untrackedFiles []string +} + +type applyConflictError struct { + reason string + err error +} + +func (e *applyConflictError) Error() string { + if e.err == nil { + return e.reason + } + return fmt.Sprintf("%s: %v", e.reason, e.err) +} + +func (e *applyConflictError) Unwrap() error { return e.err } + +func newApplyCommand() *cobra.Command { + opts := &applyOptions{} + cmd := &cobra.Command{ + Use: "apply ", + Short: "Apply non-destructive changes between a Codex worktree and local checkout", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + mode := handoffApply + if opts.force { + mode = handoffOverwrite + } + return runCodexHandoff(cmd, strings.TrimSpace(args[0]), handoffOptions{ + yes: opts.yes, + dryRun: opts.dryRun, + to: opts.to, + }, mode) + }, + } + + cmd.Flags().BoolVar(&opts.yes, "yes", false, "skip confirmation prompts") + cmd.Flags().BoolVar(&opts.dryRun, "dry-run", false, "show git commands without executing") + cmd.Flags().StringVar(&opts.to, "to", transferToLocal, "transfer destination: local or worktree") + cmd.Flags().BoolVar(&opts.force, "force", false, "compatibility alias for overwrite behavior") + return cmd +} + +func newOverwriteCommand() *cobra.Command { + opts := &handoffOptions{} + cmd := &cobra.Command{ + Use: "overwrite ", + Short: "Overwrite destination with source changes in codex mode", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runCodexHandoff(cmd, strings.TrimSpace(args[0]), *opts, handoffOverwrite) + }, + } + + cmd.Flags().BoolVar(&opts.yes, "yes", false, "skip confirmation prompts") + cmd.Flags().BoolVar(&opts.dryRun, "dry-run", false, "show git commands without executing") + cmd.Flags().StringVar(&opts.to, "to", transferToLocal, "transfer destination: local or worktree") + return cmd +} + +func runCodexHandoff(cmd *cobra.Command, opaqueID string, opts handoffOptions, mode handoffMode) error { + ctx := cmd.Context() + modeCtx, err := resolveModeContext(cmd, false) + if err != nil { + return err + } + cfg, ok := configFromContext(ctx) + if !ok || modeCtx.mode != modeCodex { + return fmt.Errorf("%s is only supported in --mode=codex", mode) + } + + if !cmd.Flags().Changed("yes") { + opts.yes = !cfg.Cleanup.Confirm + } + + runner := defaultRunner() + plan, err := resolveCodexHandoffPlan(ctx, runner, modeCtx, opaqueID, opts.to) + if err != nil { + return err + } + + preflight := transferPreflight{} + if opts.dryRun || mode == handoffApply { + preflight, err = collectTransferPreflight(ctx, runner, plan.sourceRoot, plan.destinationRoot, opts.dryRun) + if err != nil { + return err + } + } + + if opts.dryRun { + if err := printDryRunPlan(cmd.OutOrStdout(), mode, plan, preflight); err != nil { + return err + } + } + + if mode == handoffApply { + reasons := conflictReasonsForApply(preflight, plan.destinationName) + if len(reasons) > 0 { + if err := printConflictReasons(cmd.OutOrStdout(), reasons); err != nil { + return err + } + if err := printOverwriteHint(cmd.OutOrStdout(), plan.to, opaqueID); err != nil { + return err + } + return fmt.Errorf("apply aborted due to conflicts") + } + } + + if mode == handoffOverwrite && !opts.dryRun { + if err := confirmOverwrite(cmd, opts.yes, plan); err != nil { + return err + } + } + + err = transferChanges(ctx, cmd, runner, plan.sourceRoot, plan.destinationRoot, opts.dryRun, mode == handoffOverwrite) + if err != nil { + if mode == handoffApply { + var conflictErr *applyConflictError + if errors.As(err, &conflictErr) { + if err := printConflictReasons(cmd.OutOrStdout(), []string{conflictErr.reason}); err != nil { + return err + } + if err := printOverwriteHint(cmd.OutOrStdout(), plan.to, opaqueID); err != nil { + return err + } + return fmt.Errorf("apply aborted due to conflicts") + } + } + return err + } + + if _, err := fmt.Fprintln(cmd.OutOrStdout(), ui.SuccessStyle.Render(fmt.Sprintf("%s complete", mode))); err != nil { + return err + } + return nil +} + +func printConflictReasons(out io.Writer, reasons []string) error { + if _, err := fmt.Fprintf(out, "%s\n", ui.WarningStyle.Render("apply blocked (non-destructive mode):")); err != nil { + return err + } + for _, reason := range reasons { + if _, err := fmt.Fprintf(out, "- %s\n", ui.WarningStyle.Render(reason)); err != nil { + return err + } + } + return nil +} + +func printOverwriteHint(out io.Writer, to, opaqueID string) error { + if _, err := fmt.Fprintf(out, "%s\n", ui.WarningStyle.Render(fmt.Sprintf("next step: gwtt overwrite --to %s %s", to, opaqueID))); err != nil { + return err + } + _, err := fmt.Fprintf(out, "%s\n", ui.WarningStyle.Render("add --yes to skip overwrite confirmation prompts")) + return err +} + +func confirmOverwrite(cmd *cobra.Command, yes bool, plan transferPlan) error { + if yes { + return nil + } + ok, err := confirmPrompt(cmd.InOrStdin(), cmd.OutOrStdout(), fmt.Sprintf("Overwrite the %s from the %s?", plan.destinationName, plan.sourceName)) + if err != nil { + return err + } + if !ok { + return errCanceled + } + ok, err = confirmPrompt(cmd.InOrStdin(), cmd.OutOrStdout(), fmt.Sprintf("This will discard %s changes. Continue?", plan.destinationName)) + if err != nil { + return err + } + if !ok { + return errCanceled + } + return nil +} diff --git a/cli/apply_conflicts.go b/cli/apply_conflicts.go new file mode 100644 index 0000000..5170127 --- /dev/null +++ b/cli/apply_conflicts.go @@ -0,0 +1,128 @@ +package cli + +import ( + "context" + "fmt" + "strings" + + "github.com/pi2pie/git-worktree-tasks/internal/git" +) + +func detectApplyConflicts(ctx context.Context, runner git.Runner, destinationRoot, destinationName, sourceRoot string) ([]string, error) { + preflight, err := collectTransferPreflight(ctx, runner, sourceRoot, destinationRoot, false) + if err != nil { + return nil, err + } + return conflictReasonsForApply(preflight, destinationName), nil +} + +func isDirty(ctx context.Context, runner git.Runner, repoRoot string) (bool, error) { + stdout, stderr, err := runner.Run(ctx, "-C", repoRoot, "status", "--porcelain") + if err != nil { + if stderr != "" { + return false, fmt.Errorf("git status: %w: %s", err, stderr) + } + return false, fmt.Errorf("git status: %w", err) + } + return strings.TrimSpace(stdout) != "", nil +} + +func modifiedFiles(ctx context.Context, runner git.Runner, repoRoot string) (map[string]struct{}, error) { + files := map[string]struct{}{} + + diffNames, stderr, err := runner.Run(ctx, "-C", repoRoot, "diff", "--name-only", "HEAD") + if err != nil { + if stderr != "" { + return nil, fmt.Errorf("git diff --name-only: %w: %s", err, stderr) + } + return nil, fmt.Errorf("git diff --name-only: %w", err) + } + for _, line := range strings.Split(diffNames, "\n") { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + continue + } + files[trimmed] = struct{}{} + } + + untracked, stderr, err := runner.Run(ctx, "-C", repoRoot, "ls-files", "--others", "--exclude-standard") + if err != nil { + if stderr != "" { + return nil, fmt.Errorf("git ls-files: %w: %s", err, stderr) + } + return nil, fmt.Errorf("git ls-files: %w", err) + } + for _, line := range strings.Split(untracked, "\n") { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + continue + } + files[trimmed] = struct{}{} + } + + return files, nil +} + +func intersectCount(left, right map[string]struct{}) int { + if len(left) == 0 || len(right) == 0 { + return 0 + } + if len(left) > len(right) { + left, right = right, left + } + count := 0 + for key := range left { + if _, ok := right[key]; ok { + count++ + } + } + return count +} + +func collectTransferPreflight(ctx context.Context, runner git.Runner, sourceRoot, destinationRoot string, includeTransferState bool) (transferPreflight, error) { + destinationDirty, err := isDirty(ctx, runner, destinationRoot) + if err != nil { + return transferPreflight{}, err + } + + sourceModified, err := modifiedFiles(ctx, runner, sourceRoot) + if err != nil { + return transferPreflight{}, err + } + destinationModified, err := modifiedFiles(ctx, runner, destinationRoot) + if err != nil { + return transferPreflight{}, err + } + + preflight := transferPreflight{ + destinationDirty: destinationDirty, + overlappingFiles: intersectCount(sourceModified, destinationModified), + } + + if includeTransferState { + patch, err := gitDiff(ctx, runner, sourceRoot) + if err != nil { + return transferPreflight{}, err + } + preflight.trackedPatch = patch != "" + + untracked, err := listUntracked(ctx, runner, sourceRoot) + if err != nil { + return transferPreflight{}, err + } + preflight.untrackedFiles = untracked + } + + return preflight, nil +} + +func conflictReasonsForApply(preflight transferPreflight, destinationName string) []string { + var reasons []string + if preflight.destinationDirty { + reasons = append(reasons, fmt.Sprintf("%s has uncommitted changes", destinationName)) + } + if preflight.overlappingFiles > 0 { + reasons = append(reasons, fmt.Sprintf("both sides modified %d overlapping file(s)", preflight.overlappingFiles)) + } + return reasons +} diff --git a/cli/apply_files.go b/cli/apply_files.go new file mode 100644 index 0000000..40fe276 --- /dev/null +++ b/cli/apply_files.go @@ -0,0 +1,102 @@ +package cli + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +func writeTempPatch(contents string) (string, error) { + tmp, err := os.CreateTemp("", "gwtt-apply-*.patch") + if err != nil { + return "", err + } + if _, err := io.WriteString(tmp, contents); err != nil { + if closeErr := tmp.Close(); closeErr != nil { + return "", fmt.Errorf("write patch: %w (close error: %v)", err, closeErr) + } + return "", err + } + if err := tmp.Close(); err != nil { + return "", err + } + return tmp.Name(), nil +} + +func copyFile(srcRoot, dstRoot, rel string, dryRun bool, out io.Writer) (err error) { + srcPath := filepath.Join(srcRoot, rel) + dstPath := filepath.Join(dstRoot, rel) + + info, err := os.Lstat(srcPath) + if err != nil { + return err + } + + if info.Mode()&os.ModeSymlink != 0 { + target, err := os.Readlink(srcPath) + if err != nil { + return err + } + if dryRun { + _, err := fmt.Fprintf(out, "symlink %s -> %s (%s)\n", srcPath, dstPath, target) + return err + } + if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil { + return err + } + if err := os.Remove(dstPath); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + return os.Symlink(target, dstPath) + } + + if !info.Mode().IsRegular() { + return fmt.Errorf("unsupported file type for copy: %s", srcPath) + } + + if dryRun { + _, err := fmt.Fprintf(out, "copy %s -> %s\n", srcPath, dstPath) + return err + } + + if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil { + return err + } + in, err := os.Open(srcPath) + if err != nil { + return err + } + defer func() { + if closeErr := in.Close(); closeErr != nil && err == nil { + err = closeErr + } + }() + + outFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) + if err != nil { + return err + } + defer func() { + if closeErr := outFile.Close(); closeErr != nil && err == nil { + err = closeErr + } + }() + + if _, err := io.Copy(outFile, in); err != nil { + return err + } + return nil +} + +func removeTempPatch(path string) error { + if strings.TrimSpace(path) == "" { + return nil + } + if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + return nil +} diff --git a/cli/apply_resolve.go b/cli/apply_resolve.go new file mode 100644 index 0000000..28156e1 --- /dev/null +++ b/cli/apply_resolve.go @@ -0,0 +1,57 @@ +package cli + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "github.com/pi2pie/git-worktree-tasks/internal/git" +) + +func resolveCodexHandoffPlan(ctx context.Context, runner git.Runner, modeCtx modeContext, opaqueID, to string) (transferPlan, error) { + if strings.TrimSpace(opaqueID) == "" { + return transferPlan{}, fmt.Errorf("task query cannot be empty") + } + + repoRoot, err := repoRoot(ctx, runner) + if err != nil { + return transferPlan{}, err + } + if _, err := git.CurrentBranch(ctx, runner); err != nil { + return transferPlan{}, err + } + + wtPath, found, err := resolveCodexWorktreePath(ctx, runner, repoRoot, modeCtx.codexWorktrees, opaqueID) + if err != nil { + return transferPlan{}, err + } + if !found { + return transferPlan{}, fmt.Errorf("no Codex worktree found for %q under %s", opaqueID, filepath.Join("$CODEX_HOME", "worktrees")) + } + + return resolveTransferPlan(repoRoot, wtPath, to) +} + +func resolveTransferPlan(repoRoot, worktreePath, to string) (transferPlan, error) { + switch strings.TrimSpace(to) { + case transferToLocal: + return transferPlan{ + to: transferToLocal, + sourceRoot: worktreePath, + sourceName: "Codex worktree", + destinationRoot: repoRoot, + destinationName: "local checkout", + }, nil + case transferToWorktree: + return transferPlan{ + to: transferToWorktree, + sourceRoot: repoRoot, + sourceName: "local checkout", + destinationRoot: worktreePath, + destinationName: "Codex worktree", + }, nil + default: + return transferPlan{}, fmt.Errorf("invalid --to value %q (expected local or worktree)", to) + } +} diff --git a/cli/apply_transfer.go b/cli/apply_transfer.go new file mode 100644 index 0000000..53b9c88 --- /dev/null +++ b/cli/apply_transfer.go @@ -0,0 +1,167 @@ +package cli + +import ( + "context" + "fmt" + "io" + "path/filepath" + "strings" + + "github.com/pi2pie/git-worktree-tasks/internal/git" + "github.com/spf13/cobra" +) + +func printDryRunPlan(out io.Writer, mode handoffMode, plan transferPlan, preflight transferPreflight) error { + if _, err := fmt.Fprintf(out, "%s plan\n", mode); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " to: %s\n", plan.to); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " source: %s\n", plan.sourceRoot); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " destination: %s\n", plan.destinationRoot); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " overwrite: %t\n", mode == handoffOverwrite); err != nil { + return err + } + + if _, err := fmt.Fprintln(out, ""); err != nil { + return err + } + if _, err := fmt.Fprintln(out, "preflight"); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " destination_dirty: %t\n", preflight.destinationDirty); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " overlapping_files: %d\n", preflight.overlappingFiles); err != nil { + return err + } + trackedPatch := "none" + if preflight.trackedPatch { + trackedPatch = "present" + } + if _, err := fmt.Fprintf(out, " tracked_patch: %s\n", trackedPatch); err != nil { + return err + } + if _, err := fmt.Fprintf(out, " untracked_files: %d\n", len(preflight.untrackedFiles)); err != nil { + return err + } + + if _, err := fmt.Fprintln(out, ""); err != nil { + return err + } + if _, err := fmt.Fprintln(out, "actions"); err != nil { + return err + } + actions := dryRunActions(mode, plan, preflight) + for idx, action := range actions { + if _, err := fmt.Fprintf(out, " %d. %s\n", idx+1, action); err != nil { + return err + } + } + return nil +} + +func dryRunActions(mode handoffMode, plan transferPlan, preflight transferPreflight) []string { + actions := make([]string, 0, len(preflight.untrackedFiles)+4) + + if mode == handoffOverwrite { + actions = append(actions, "[destructive] "+formatGitCommand([]string{"-C", plan.destinationRoot, "reset", "--hard"})) + actions = append(actions, "[destructive] "+formatGitCommand([]string{"-C", plan.destinationRoot, "clean", "-fd"})) + } + + if preflight.trackedPatch { + actions = append(actions, formatGitCommand([]string{"-C", plan.destinationRoot, "apply", "--check", ""})) + actions = append(actions, formatGitCommand([]string{"-C", plan.destinationRoot, "apply", ""})) + } + + for _, rel := range preflight.untrackedFiles { + actions = append(actions, fmt.Sprintf("copy %s -> %s", filepath.Join(plan.sourceRoot, rel), filepath.Join(plan.destinationRoot, rel))) + } + + if len(actions) == 0 { + actions = append(actions, "no tracked or untracked changes detected") + } + + return actions +} + +func transferChanges(ctx context.Context, cmd *cobra.Command, runner git.Runner, sourceRoot, destinationRoot string, dryRun, resetDestination bool) error { + if resetDestination { + if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "reset", "--hard"); err != nil { + return err + } + if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "clean", "-fd"); err != nil { + return err + } + } + + patch, err := gitDiff(ctx, runner, sourceRoot) + if err != nil { + return err + } + + patchFile, err := writeTempPatch(patch) + if err != nil { + return err + } + defer func() { + if err := removeTempPatch(patchFile); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to remove temp patch %s: %v\n", patchFile, err) + } + }() + + if patch != "" { + if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "apply", "--check", patchFile); err != nil { + return &applyConflictError{reason: "apply patch check failed", err: err} + } + if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "apply", patchFile); err != nil { + return fmt.Errorf("apply patch: %w", err) + } + } + + untracked, err := listUntracked(ctx, runner, sourceRoot) + if err != nil { + return err + } + for _, rel := range untracked { + if err := copyFile(sourceRoot, destinationRoot, rel, dryRun, cmd.OutOrStdout()); err != nil { + return err + } + } + return nil +} + +func gitDiff(ctx context.Context, runner git.Runner, repoRoot string) (string, error) { + stdout, stderr, err := runner.Run(ctx, "-C", repoRoot, "diff", "--binary", "HEAD") + if err != nil { + if stderr != "" { + return "", fmt.Errorf("git diff: %w: %s", err, stderr) + } + return "", fmt.Errorf("git diff: %w", err) + } + return stdout, nil +} + +func listUntracked(ctx context.Context, runner git.Runner, repoRoot string) ([]string, error) { + stdout, stderr, err := runner.Run(ctx, "-C", repoRoot, "ls-files", "--others", "--exclude-standard") + if err != nil { + if stderr != "" { + return nil, fmt.Errorf("git ls-files: %w: %s", err, stderr) + } + return nil, fmt.Errorf("git ls-files: %w", err) + } + var out []string + for _, line := range strings.Split(stdout, "\n") { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + continue + } + out = append(out, trimmed) + } + return out, nil +} diff --git a/docs/plans/jobs/2026-02-07-codex-apply-phase6-phase7.md b/docs/plans/jobs/2026-02-07-codex-apply-phase6-phase7.md new file mode 100644 index 0000000..a82a94f --- /dev/null +++ b/docs/plans/jobs/2026-02-07-codex-apply-phase6-phase7.md @@ -0,0 +1,46 @@ +--- +title: "Implement codex apply Phase 6 and Phase 7" +created-date: 2026-02-07 +status: completed +agent: codex +--- + +## Summary + +- Completed Phase 6 by splitting monolithic `apply` logic into focused files: + - command wiring and handoff flow in `cli/apply_command.go` + - codex worktree resolution and transfer planning in `cli/apply_resolve.go` + - conflict/preflight helpers in `cli/apply_conflicts.go` + - transfer and dry-run action logic in `cli/apply_transfer.go` + - patch/file copy helpers in `cli/apply_files.go` +- Removed `cli/apply.go` after migrating all behavior and preserving existing test-targeted function signatures. +- Completed Phase 7 verification and docs pass: + - full Go test suite passed + - `apply`/`overwrite` help text re-verified + - README/man alignment re-checked + - plan and research docs updated to reflect completion + +## Files Updated + +- `cli/apply_command.go` +- `cli/apply_resolve.go` +- `cli/apply_conflicts.go` +- `cli/apply_transfer.go` +- `cli/apply_files.go` +- `cli/apply.go` (removed) +- `docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md` +- `docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md` + +## Verification + +- `go test ./...` +- `go run . --nocolor apply --help` +- `go run . --nocolor overwrite --help` + +## Related Plans + +- `docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md` + +## Related Research + +- `docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md` diff --git a/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md b/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md index 4436a86..8f742c9 100644 --- a/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md +++ b/docs/plans/plan-2026-02-07-codex-apply-two-way-directions.md @@ -2,7 +2,7 @@ title: "Codex apply two-way directions and cli extraction prep" created-date: 2026-02-07 modified-date: 2026-02-07 -status: active +status: completed agent: codex --- @@ -62,19 +62,19 @@ Implement a two-way `apply`/`overwrite` command model in codex mode with explici ### Phase 6: Apply File-Split Refactor -- [ ] Split command wiring/flags into `cli/apply_command.go`. -- [ ] Split codex worktree resolution and validation into `cli/apply_resolve.go`. -- [ ] Split conflict detection helpers into `cli/apply_conflicts.go`. -- [ ] Split direction-agnostic transfer logic into `cli/apply_transfer.go`. -- [ ] Split temp patch + file copy helpers into `cli/apply_files.go`. +- [x] Split command wiring/flags into `cli/apply_command.go`. +- [x] Split codex worktree resolution and validation into `cli/apply_resolve.go`. +- [x] Split conflict detection helpers into `cli/apply_conflicts.go`. +- [x] Split direction-agnostic transfer logic into `cli/apply_transfer.go`. +- [x] Split temp patch + file copy helpers into `cli/apply_files.go`. ### Phase 7: Post-Refactor Verify and Docs Pass -- [ ] Run full Go test suite and ensure no behavior regressions. -- [ ] Update/add tests where refactor changed package/file boundaries. -- [ ] Re-verify command help text for `apply`/`overwrite`/flags. -- [ ] Reconcile README and man pages with final refactored behavior. -- [ ] Update related plan/research/job docs and mark completion status. +- [x] Run full Go test suite and ensure no behavior regressions. +- [x] Update/add tests where refactor changed package/file boundaries. +- [x] Re-verify command help text for `apply`/`overwrite`/flags. +- [x] Reconcile README and man pages with final refactored behavior. +- [x] Update related plan/research/job docs and mark completion status. ## Acceptance Criteria @@ -87,7 +87,7 @@ Implement a two-way `apply`/`overwrite` command model in codex mode with explici - Confirmation wording must remain clear when destructive destination reset is involved. - Refactor-first approach lowers risk but can surface latent coupling in `cli`. -- `cli/apply.go` remains intentionally consolidated for the first behavior cut; file split refactor is tracked in Phase 6. +- Phase 6 completed the `apply` file split (`apply_command`, `apply_resolve`, `apply_conflicts`, `apply_transfer`, `apply_files`), reducing coupling in command internals. ## Related Research diff --git a/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md b/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md index 354c8d5..8bcb61c 100644 --- a/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md +++ b/docs/research-2026-02-07-codex-apply-direction-and-source-checkout.md @@ -2,7 +2,7 @@ title: "Codex apply direction and source checkout behavior" created-date: 2026-02-07 modified-date: 2026-02-07 -status: in-progress +status: completed agent: codex --- @@ -49,7 +49,7 @@ Define a clearer, less surprising `gwtt apply` model in `--mode=codex` by making - whether overwrite/reset will occur, - summary of what is expected to change. -### `cli/apply.go` is doing too much in one file +### `cli/apply.go` was doing too much in one file (resolved in Phase 6) - The file contains command wiring, worktree resolution, conflict detection, transfer logic, patch I/O, and file copying. - There is duplicated transfer logic between forward/reverse paths, increasing maintenance cost and making direction changes riskier. @@ -134,9 +134,16 @@ Apply vs overwrite matrix: - dry-run plan-style output with `plan`, `preflight`, and `actions` sections - conflict output updated with explicit overwrite next-step guidance - README/man/help updates for `apply` + `overwrite` semantics -- Pending: - - split `cli/apply.go` into focused files (`apply_command`, `apply_resolve`, `apply_conflicts`, `apply_transfer`, `apply_files`) - - post-refactor verification/doc pass after the file-split phase + - split implementation into focused files: + - `cli/apply_command.go` + - `cli/apply_resolve.go` + - `cli/apply_conflicts.go` + - `cli/apply_transfer.go` + - `cli/apply_files.go` + - post-refactor verification pass completed: + - full `go test ./...` + - help text verification for `apply`/`overwrite` + - README/man reconciliation with final behavior ## Dry-Run Output Redesign (Draft) @@ -184,7 +191,7 @@ Output requirements: - Prior codex mode research: [^mode-research] - Related mode plan: [^mode-plan] -[^apply-code]: `cli/apply.go` +[^apply-code]: `cli/apply_command.go`, `cli/apply_resolve.go`, `cli/apply_conflicts.go`, `cli/apply_transfer.go`, `cli/apply_files.go` [^mode-research]: `docs/research-2026-02-04-mode-classic-vs-codex.md` [^mode-plan]: `docs/plans/plan-2026-02-04-mode-classic-and-codex.md` From f5a421341dd1d53f200bde4082e43e032a3ccccd Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 18:50:10 +0800 Subject: [PATCH 10/15] docs(plan): add plan for dry-run path masking --- .../plan-2026-02-07-dry-run-path-masking.md | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/plans/plan-2026-02-07-dry-run-path-masking.md diff --git a/docs/plans/plan-2026-02-07-dry-run-path-masking.md b/docs/plans/plan-2026-02-07-dry-run-path-masking.md new file mode 100644 index 0000000..1da6aa9 --- /dev/null +++ b/docs/plans/plan-2026-02-07-dry-run-path-masking.md @@ -0,0 +1,87 @@ +--- +title: "Dry-run path masking for sensitive local paths" +created-date: 2026-02-07 +modified-date: 2026-02-07 +status: draft +agent: codex +--- + +## Goal + +Reduce accidental disclosure of user-identifying local paths in `--dry-run` output by masking home-directory prefixes (for example, `/Users/alice/...` -> `$HOME/...` on POSIX, `C:\Users\Alice\...` -> `%USERPROFILE%\...` on Windows) while keeping output readable and executable. + +## Scope + +- Add a configurable masking behavior for `--dry-run` output. +- Default masking to enabled. +- Apply masking consistently across all current dry-run-enabled commands: + - `apply` + - `overwrite` + - `create` + - `cleanup` + - `finish` +- Keep non-dry-run behavior unchanged. + +## Proposed Config + +- New config section: `[dry_run]` +- New key: `mask_sensitive_paths = true` +- Default value in code: `true` +- CLI behavior: + - When `true`, paths under user home are rendered with a platform-specific home token in dry-run output: + - POSIX: `$HOME` + - Windows: `%USERPROFILE%` + - When `false`, dry-run output keeps current raw absolute paths. + +## Design Notes + +- Helper placement: `cli` package (presentation concern only). +- Suggested helper file: `cli/path_mask.go`. +- Suggested API shape: + - `maskHomePath(path string) string` + - `formatGitCommandForDryRun(args []string, mask bool) string` +- Matching rules: + - Exact home path maps to platform home token (`$HOME` or `%USERPROFILE%`). + - Descendants map to `/` on POSIX and `\` on Windows. + - Prefix-safe matching only (separator-aware). + - On Windows, matching is case-insensitive. + - If home lookup fails, path is left unchanged. + +## Implementation Plan + +1. Extend config structs/loader/defaults with `DryRun.MaskSensitivePaths`. +2. Add path masking helper(s) in `cli`. +3. Route dry-run command rendering through masking-aware formatter: + - `cli/git_exec.go` + - dry-run print in `cli/create.go` +4. Mask path fields in codex transfer dry-run plan/actions: + - `cli/apply_transfer.go` +5. Add/adjust tests: + - Unit tests for path masking edge cases. + - OS-specific masking tests for POSIX and Windows token behavior. + - Update existing dry-run output tests to assert masking behavior. + - Add config loading tests for default and explicit false. +6. Update docs: + - README option docs for new config. + - Config schema docs at `docs/schemas/config-gwtt.md`. + - Regenerate/update man page docs via `make man`. + +## Non-Goals + +- No masking changes for non-dry-run command output. +- No broad secret redaction framework (tokens, hostnames, etc.) in this change. +- No environment variable override for this feature in this phase. + +## Acceptance Criteria + +- With default config, dry-run output does not expose the local username via home-absolute paths. +- Dry-run output uses `$HOME` on POSIX and `%USERPROFILE%` on Windows for home-path masking. +- Dry-run output remains copy/paste-usable for the platform's common shell conventions. +- Setting `[dry_run].mask_sensitive_paths = false` restores current raw-path dry-run output. +- Existing command behavior outside dry-run remains unchanged. + +## Risks / Notes + +- Snapshot/integration tests that currently assert raw absolute paths will require updates. +- Home-token replacement should be limited to path arguments and displayed path fields to avoid over-masking unrelated string content. +- Cross-platform behavior should rely on deterministic helper tests to reduce dependence on running CI on every OS for basic masking validation. From c8109ef886fca61ee1e3804b25dd4fd06e86cd81 Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 19:24:42 +0800 Subject: [PATCH 11/15] fix: return sentinel on apply conflicts and add fallback --- cli/apply_command.go | 4 +- cli/apply_test.go | 153 +++++++++++++++++- cli/apply_transfer.go | 115 ++++++++++++- cli/root.go | 4 + .../2026-02-07-overwrite-apply-log-fix.md | 32 ++++ 5 files changed, 294 insertions(+), 14 deletions(-) create mode 100644 docs/plans/jobs/2026-02-07-overwrite-apply-log-fix.md diff --git a/cli/apply_command.go b/cli/apply_command.go index f7e9f9b..723bc79 100644 --- a/cli/apply_command.go +++ b/cli/apply_command.go @@ -151,7 +151,7 @@ func runCodexHandoff(cmd *cobra.Command, opaqueID string, opts handoffOptions, m if err := printOverwriteHint(cmd.OutOrStdout(), plan.to, opaqueID); err != nil { return err } - return fmt.Errorf("apply aborted due to conflicts") + return errApplyBlocked } } @@ -172,7 +172,7 @@ func runCodexHandoff(cmd *cobra.Command, opaqueID string, opts handoffOptions, m if err := printOverwriteHint(cmd.OutOrStdout(), plan.to, opaqueID); err != nil { return err } - return fmt.Errorf("apply aborted due to conflicts") + return errApplyBlocked } } return err diff --git a/cli/apply_test.go b/cli/apply_test.go index dfbab4a..a046c1c 100644 --- a/cli/apply_test.go +++ b/cli/apply_test.go @@ -2,8 +2,14 @@ package cli import ( "context" + "fmt" + "io" + "os" + "path/filepath" "strings" "testing" + + "github.com/spf13/cobra" ) func TestDetectApplyConflicts(t *testing.T) { @@ -148,8 +154,8 @@ func TestDryRunActions(t *testing.T) { } actions := dryRunActions(handoffOverwrite, plan, preflight) - if len(actions) != 5 { - t.Fatalf("expected 5 actions, got %d (%v)", len(actions), actions) + if len(actions) != 4 { + t.Fatalf("expected 4 actions, got %d (%v)", len(actions), actions) } if !strings.Contains(actions[0], "[destructive] git -C /repo reset --hard") { t.Fatalf("expected destructive reset action, got %q", actions[0]) @@ -157,13 +163,144 @@ func TestDryRunActions(t *testing.T) { if !strings.Contains(actions[1], "[destructive] git -C /repo clean -fd") { t.Fatalf("expected destructive clean action, got %q", actions[1]) } - if !strings.Contains(actions[2], "apply --check ") { - t.Fatalf("expected apply check action, got %q", actions[2]) + if !strings.Contains(actions[2], "apply ") { + t.Fatalf("expected apply action, got %q", actions[2]) + } + if strings.Contains(actions[2], "--check") { + t.Fatalf("overwrite dry-run should not include apply --check, got %q", actions[2]) + } + if !strings.Contains(actions[3], "copy /codex/a.txt -> /repo/a.txt") { + t.Fatalf("expected copy action, got %q", actions[3]) + } +} + +func TestDryRunActionsApplyIncludesPatchCheck(t *testing.T) { + plan := transferPlan{ + destinationRoot: "/repo", + sourceRoot: "/codex", + } + preflight := transferPreflight{ + trackedPatch: true, + } + + actions := dryRunActions(handoffApply, plan, preflight) + if len(actions) != 2 { + t.Fatalf("expected 2 actions, got %d (%v)", len(actions), actions) + } + if !strings.Contains(actions[0], "apply --check ") { + t.Fatalf("expected apply --check action, got %q", actions[0]) + } + if !strings.Contains(actions[1], "apply ") { + t.Fatalf("expected apply action, got %q", actions[1]) + } +} + +type overwriteRunner struct { + source string + dest string + seenCheck bool + seenApply bool +} + +func (r *overwriteRunner) Run(_ context.Context, args ...string) (string, string, error) { + switch { + case len(args) == 4 && args[0] == "-C" && args[1] == r.dest && args[2] == "reset" && args[3] == "--hard": + return "", "", nil + case len(args) == 4 && args[0] == "-C" && args[1] == r.dest && args[2] == "clean" && args[3] == "-fd": + return "", "", nil + case len(args) == 5 && args[0] == "-C" && args[1] == r.source && args[2] == "diff" && args[3] == "--binary" && args[4] == "HEAD": + return "diff --git a/a.txt b/a.txt\n", "", nil + case len(args) == 5 && args[0] == "-C" && args[1] == r.source && args[2] == "ls-files" && args[3] == "--others" && args[4] == "--exclude-standard": + return "", "", nil + case len(args) == 5 && args[0] == "-C" && args[1] == r.dest && args[2] == "apply" && args[3] == "--check": + r.seenCheck = true + return "", "", fmt.Errorf("unexpected apply --check in overwrite mode") + case len(args) == 4 && args[0] == "-C" && args[1] == r.dest && args[2] == "apply": + r.seenApply = true + return "", "", nil + default: + return "", "", fmt.Errorf("unexpected args: %s", strings.Join(args, " ")) + } +} + +func TestTransferChangesOverwriteSkipsPatchCheck(t *testing.T) { + runner := &overwriteRunner{ + source: "/codex", + dest: "/repo", + } + cmd := &cobra.Command{} + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + + err := transferChanges(context.Background(), cmd, runner, "/codex", "/repo", false, true) + if err != nil { + t.Fatalf("transferChanges() error = %v", err) + } + if runner.seenCheck { + t.Fatalf("overwrite flow should skip apply --check") + } + if !runner.seenApply { + t.Fatalf("expected overwrite flow to apply patch") + } +} + +type overwriteFallbackRunner struct { + source string + dest string +} + +func (r *overwriteFallbackRunner) Run(_ context.Context, args ...string) (string, string, error) { + switch { + case len(args) == 4 && args[0] == "-C" && args[1] == r.dest && args[2] == "reset" && args[3] == "--hard": + return "", "", nil + case len(args) == 4 && args[0] == "-C" && args[1] == r.dest && args[2] == "clean" && args[3] == "-fd": + return "", "", nil + case len(args) == 5 && args[0] == "-C" && args[1] == r.source && args[2] == "diff" && args[3] == "--binary" && args[4] == "HEAD": + return "diff --git a/tracked.txt b/tracked.txt\n", "", nil + case len(args) == 5 && args[0] == "-C" && args[1] == r.dest && args[2] == "apply": + return "", "", fmt.Errorf("simulated apply failure") + case len(args) == 5 && args[0] == "-C" && args[1] == r.source && args[2] == "diff" && args[3] == "--name-status" && args[4] == "HEAD": + return "M\ttracked.txt\n", "", nil + case len(args) == 5 && args[0] == "-C" && args[1] == r.source && args[2] == "ls-files" && args[3] == "--others" && args[4] == "--exclude-standard": + return "", "", nil + default: + return "", "", fmt.Errorf("unexpected args: %s", strings.Join(args, " ")) + } +} + +func TestTransferChangesOverwriteFallsBackAfterApplyFailure(t *testing.T) { + sourceRoot := t.TempDir() + destinationRoot := t.TempDir() + writeFile := func(root, rel, content string) { + t.Helper() + path := filepath.Join(root, rel) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("MkdirAll(%s): %v", path, err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("WriteFile(%s): %v", path, err) + } } - if !strings.Contains(actions[3], "apply ") { - t.Fatalf("expected apply action, got %q", actions[3]) + writeFile(sourceRoot, "tracked.txt", "new content\n") + writeFile(destinationRoot, "tracked.txt", "old content\n") + + runner := &overwriteFallbackRunner{ + source: sourceRoot, + dest: destinationRoot, + } + cmd := &cobra.Command{} + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + + err := transferChanges(context.Background(), cmd, runner, sourceRoot, destinationRoot, false, true) + if err != nil { + t.Fatalf("transferChanges() error = %v", err) + } + got, err := os.ReadFile(filepath.Join(destinationRoot, "tracked.txt")) + if err != nil { + t.Fatalf("ReadFile(tracked.txt): %v", err) } - if !strings.Contains(actions[4], "copy /codex/a.txt -> /repo/a.txt") { - t.Fatalf("expected copy action, got %q", actions[4]) + if string(got) != "new content\n" { + t.Fatalf("destination tracked.txt = %q, want %q", string(got), "new content\n") } } diff --git a/cli/apply_transfer.go b/cli/apply_transfer.go index 53b9c88..c5d431a 100644 --- a/cli/apply_transfer.go +++ b/cli/apply_transfer.go @@ -2,8 +2,10 @@ package cli import ( "context" + "errors" "fmt" "io" + "os" "path/filepath" "strings" @@ -75,7 +77,9 @@ func dryRunActions(mode handoffMode, plan transferPlan, preflight transferPrefli } if preflight.trackedPatch { - actions = append(actions, formatGitCommand([]string{"-C", plan.destinationRoot, "apply", "--check", ""})) + if mode == handoffApply { + actions = append(actions, formatGitCommand([]string{"-C", plan.destinationRoot, "apply", "--check", ""})) + } actions = append(actions, formatGitCommand([]string{"-C", plan.destinationRoot, "apply", ""})) } @@ -116,11 +120,20 @@ func transferChanges(ctx context.Context, cmd *cobra.Command, runner git.Runner, }() if patch != "" { - if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "apply", "--check", patchFile); err != nil { - return &applyConflictError{reason: "apply patch check failed", err: err} + if !resetDestination { + if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "apply", "--check", patchFile); err != nil { + return &applyConflictError{reason: "apply patch check failed", err: err} + } } if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "apply", patchFile); err != nil { - return fmt.Errorf("apply patch: %w", err) + if resetDestination { + if fallbackErr := syncTrackedChangesFallback(ctx, runner, sourceRoot, destinationRoot); fallbackErr != nil { + return fmt.Errorf("overwrite apply patch: %w (fallback sync failed: %v)", err, fallbackErr) + } + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "warning: overwrite apply patch failed; used tracked-file fallback sync: %v\n", err) + } else { + return fmt.Errorf("apply patch: %w", err) + } } } @@ -136,6 +149,100 @@ func transferChanges(ctx context.Context, cmd *cobra.Command, runner git.Runner, return nil } +type trackedChange struct { + status byte + oldRel string + newRel string +} + +func syncTrackedChangesFallback(ctx context.Context, runner git.Runner, sourceRoot, destinationRoot string) error { + changes, err := listTrackedChanges(ctx, runner, sourceRoot) + if err != nil { + return err + } + for _, change := range changes { + switch change.status { + case 'D': + if err := removeTrackedPath(destinationRoot, change.newRel); err != nil { + return err + } + case 'R': + if err := removeTrackedPath(destinationRoot, change.oldRel); err != nil { + return err + } + if err := copyFile(sourceRoot, destinationRoot, change.newRel, false, io.Discard); err != nil { + return err + } + case 'A', 'M', 'T', 'C': + if err := copyFile(sourceRoot, destinationRoot, change.newRel, false, io.Discard); err != nil { + return err + } + default: + return fmt.Errorf("unsupported tracked change status %q", string(change.status)) + } + } + return nil +} + +func listTrackedChanges(ctx context.Context, runner git.Runner, repoRoot string) ([]trackedChange, error) { + stdout, stderr, err := runner.Run(ctx, "-C", repoRoot, "diff", "--name-status", "HEAD") + if err != nil { + if stderr != "" { + return nil, fmt.Errorf("git diff --name-status: %w: %s", err, stderr) + } + return nil, fmt.Errorf("git diff --name-status: %w", err) + } + var out []trackedChange + for _, line := range strings.Split(stdout, "\n") { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + continue + } + change, err := parseTrackedChange(trimmed) + if err != nil { + return nil, err + } + out = append(out, change) + } + return out, nil +} + +func parseTrackedChange(line string) (trackedChange, error) { + parts := strings.Split(line, "\t") + if len(parts) < 2 { + return trackedChange{}, fmt.Errorf("invalid tracked change line: %q", line) + } + statusText := strings.TrimSpace(parts[0]) + if statusText == "" { + return trackedChange{}, fmt.Errorf("missing tracked change status: %q", line) + } + status := statusText[0] + switch status { + case 'R', 'C': + if len(parts) < 3 { + return trackedChange{}, fmt.Errorf("invalid rename/copy tracked change line: %q", line) + } + return trackedChange{ + status: status, + oldRel: strings.TrimSpace(parts[1]), + newRel: strings.TrimSpace(parts[2]), + }, nil + default: + return trackedChange{ + status: status, + newRel: strings.TrimSpace(parts[1]), + }, nil + } +} + +func removeTrackedPath(root, rel string) error { + path := filepath.Join(root, rel) + if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + return nil +} + func gitDiff(ctx context.Context, runner git.Runner, repoRoot string) (string, error) { stdout, stderr, err := runner.Run(ctx, "-C", repoRoot, "diff", "--binary", "HEAD") if err != nil { diff --git a/cli/root.go b/cli/root.go index 844c545..3c3204c 100644 --- a/cli/root.go +++ b/cli/root.go @@ -16,6 +16,7 @@ var Version = "0.1.2-alpha.2" var ( errCanceled = errors.New("git worktree task process canceled") errThemesListed = errors.New("themes listed") + errApplyBlocked = errors.New("apply aborted due to conflicts") ) func Execute() int { @@ -28,6 +29,9 @@ func Execute() int { _, _ = fmt.Fprintln(cmd.ErrOrStderr(), ui.WarningStyle.Render("git worktree task process canceled")) return 3 } + if errors.Is(err, errApplyBlocked) { + return 2 + } _, _ = fmt.Fprintln(cmd.ErrOrStderr(), ui.ErrorStyle.Render(err.Error())) return 1 } diff --git a/docs/plans/jobs/2026-02-07-overwrite-apply-log-fix.md b/docs/plans/jobs/2026-02-07-overwrite-apply-log-fix.md new file mode 100644 index 0000000..ba165a6 --- /dev/null +++ b/docs/plans/jobs/2026-02-07-overwrite-apply-log-fix.md @@ -0,0 +1,32 @@ +--- +title: "Fix overwrite/apply conflict logging and fallback behavior" +created-date: 2026-02-07 +status: completed +agent: codex +--- + +## Summary + +- Fixed misleading failure-style output for expected `apply` conflict guardrails by returning a dedicated sentinel (`errApplyBlocked`) and handling it as warning-level flow in root command execution. +- Adjusted overwrite behavior to skip `git apply --check` (check remains for non-destructive `apply`). +- Added overwrite fallback sync for tracked changes when direct `git apply` fails after reset/clean, so overwrite can still complete when patch application is brittle but file-level sync is possible. +- Updated dry-run action rendering and tests to reflect the overwrite flow changes. + +## User-Visible Outcome + +- `gwtt --mode codex apply ...` conflict hints no longer produce an additional scary red error line for expected non-destructive blocking. +- `gwtt --mode codex apply ... --force` / `gwtt --mode codex overwrite ...` no longer fails early on `apply patch check failed`. +- In overwrite mode, if `git apply` fails but fallback sync succeeds, command completes and emits a warning instead of an error exit. + +## Files Updated + +- `cli/root.go` +- `cli/apply_command.go` +- `cli/apply_transfer.go` +- `cli/apply_test.go` + +## Verification + +- `go test ./cli` +- `go test ./...` + From b21f6206e3e82b2013cd8068c3d0ffc5f4f036b0 Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 19:58:50 +0800 Subject: [PATCH 12/15] feat: add configurable dry-run path masking --- README.md | 13 +- cli/apply_command.go | 2 +- cli/apply_files.go | 6 +- cli/apply_test.go | 57 ++++- cli/apply_transfer.go | 33 +-- cli/create.go | 2 +- cli/dry_run_mask_test.go | 137 ++++++++++++ cli/git_exec.go | 2 +- cli/git_exec_test.go | 58 +++++ cli/path_mask.go | 199 ++++++++++++++++++ cli/path_mask_test.go | 116 ++++++++++ cli/root.go | 27 ++- ...-07-dry-run-path-masking-implementation.md | 53 +++++ .../plan-2026-02-07-dry-run-path-masking.md | 2 +- docs/schemas/config-gwtt.md | 17 +- examples/gwtt.config.toml | 3 + internal/config/config.go | 26 ++- internal/config/config_test.go | 55 +++++ man/man1/git-worktree-tasks.1 | 8 + man/man1/git-worktree-tasks_apply.1 | 8 + man/man1/git-worktree-tasks_cleanup.1 | 8 + man/man1/git-worktree-tasks_create.1 | 8 + man/man1/git-worktree-tasks_finish.1 | 8 + man/man1/git-worktree-tasks_list.1 | 8 + man/man1/git-worktree-tasks_overwrite.1 | 8 + man/man1/git-worktree-tasks_status.1 | 8 + man/man1/gwtt.1 | 8 + man/man1/gwtt_apply.1 | 8 + man/man1/gwtt_cleanup.1 | 8 + man/man1/gwtt_create.1 | 8 + man/man1/gwtt_finish.1 | 8 + man/man1/gwtt_list.1 | 8 + man/man1/gwtt_overwrite.1 | 8 + man/man1/gwtt_status.1 | 8 + 34 files changed, 902 insertions(+), 34 deletions(-) create mode 100644 cli/dry_run_mask_test.go create mode 100644 cli/git_exec_test.go create mode 100644 cli/path_mask.go create mode 100644 cli/path_mask_test.go create mode 100644 docs/plans/jobs/2026-02-07-dry-run-path-masking-implementation.md diff --git a/README.md b/README.md index 1ca2a42..9212a2d 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ ln -s $(which git-worktree-tasks) $(dirname $(which git-worktree-tasks))/gwtt Settings resolve in this order (highest precedence first): -1. `--theme` / `--mode` (`-m`) flags +1. CLI flags (for example `--theme`, `--mode` / `-m`, `--mask-sensitive-paths`, `--no-mask-sensitive-paths`) 2. Environment variables 3. Project config (`gwtt.config.toml` or `gwtt.toml` in repo root) 4. User config (`$HOME/.config/gwtt/config.toml`) @@ -180,6 +180,9 @@ export GWTT_COLOR=0 # Mode selection export GWTT_MODE=codex +# Dry-run path masking (1/true/on/yes to enable, 0/false/off/no to disable) +export GWTT_DRY_RUN_MASK_SENSITIVE_PATHS=1 + # List available themes gwtt --themes ``` @@ -208,6 +211,9 @@ color_enabled = true [table] grid = false +[dry_run] +mask_sensitive_paths = true # mask $HOME/%USERPROFILE% prefixes in --dry-run output + [list] output = "table" field = "path" @@ -237,6 +243,11 @@ worktree_only = false force_branch = false ``` +`[dry_run].mask_sensitive_paths` defaults to `true`. Set it to `false` if you need raw absolute paths in `--dry-run` output. +When enabled, home-prefixed paths are rendered as `$HOME/...` on POSIX and `%USERPROFILE%\\...` on Windows. +You can override this per-invocation with `--mask-sensitive-paths=true|false`, `--no-mask-sensitive-paths`, or via `GWTT_DRY_RUN_MASK_SENSITIVE_PATHS`. +For bool flags, prefer `--mask-sensitive-paths=false` (with `=`) rather than `--mask-sensitive-paths false`. + ### Config File Location Project: `gwtt.config.toml` or `gwtt.toml` in the repo root diff --git a/cli/apply_command.go b/cli/apply_command.go index 723bc79..a2249d9 100644 --- a/cli/apply_command.go +++ b/cli/apply_command.go @@ -137,7 +137,7 @@ func runCodexHandoff(cmd *cobra.Command, opaqueID string, opts handoffOptions, m } if opts.dryRun { - if err := printDryRunPlan(cmd.OutOrStdout(), mode, plan, preflight); err != nil { + if err := printDryRunPlan(cmd.OutOrStdout(), mode, plan, preflight, shouldMaskSensitivePaths(ctx)); err != nil { return err } } diff --git a/cli/apply_files.go b/cli/apply_files.go index 40fe276..d8b9672 100644 --- a/cli/apply_files.go +++ b/cli/apply_files.go @@ -26,7 +26,7 @@ func writeTempPatch(contents string) (string, error) { return tmp.Name(), nil } -func copyFile(srcRoot, dstRoot, rel string, dryRun bool, out io.Writer) (err error) { +func copyFile(srcRoot, dstRoot, rel string, dryRun bool, out io.Writer, maskPaths bool) (err error) { srcPath := filepath.Join(srcRoot, rel) dstPath := filepath.Join(dstRoot, rel) @@ -41,7 +41,7 @@ func copyFile(srcRoot, dstRoot, rel string, dryRun bool, out io.Writer) (err err return err } if dryRun { - _, err := fmt.Fprintf(out, "symlink %s -> %s (%s)\n", srcPath, dstPath, target) + _, err := fmt.Fprintf(out, "symlink %s -> %s (%s)\n", maskPathForDryRun(srcPath, maskPaths), maskPathForDryRun(dstPath, maskPaths), target) return err } if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil { @@ -58,7 +58,7 @@ func copyFile(srcRoot, dstRoot, rel string, dryRun bool, out io.Writer) (err err } if dryRun { - _, err := fmt.Fprintf(out, "copy %s -> %s\n", srcPath, dstPath) + _, err := fmt.Fprintf(out, "copy %s -> %s\n", maskPathForDryRun(srcPath, maskPaths), maskPathForDryRun(dstPath, maskPaths)) return err } diff --git a/cli/apply_test.go b/cli/apply_test.go index a046c1c..0068d46 100644 --- a/cli/apply_test.go +++ b/cli/apply_test.go @@ -1,6 +1,7 @@ package cli import ( + "bytes" "context" "fmt" "io" @@ -153,7 +154,7 @@ func TestDryRunActions(t *testing.T) { untrackedFiles: []string{"a.txt"}, } - actions := dryRunActions(handoffOverwrite, plan, preflight) + actions := dryRunActions(handoffOverwrite, plan, preflight, false) if len(actions) != 4 { t.Fatalf("expected 4 actions, got %d (%v)", len(actions), actions) } @@ -183,7 +184,7 @@ func TestDryRunActionsApplyIncludesPatchCheck(t *testing.T) { trackedPatch: true, } - actions := dryRunActions(handoffApply, plan, preflight) + actions := dryRunActions(handoffApply, plan, preflight, false) if len(actions) != 2 { t.Fatalf("expected 2 actions, got %d (%v)", len(actions), actions) } @@ -195,6 +196,58 @@ func TestDryRunActionsApplyIncludesPatchCheck(t *testing.T) { } } +func TestPrintDryRunPlanMasksPaths(t *testing.T) { + t.Setenv("HOME", "/Users/alice") + plan := transferPlan{ + to: transferToLocal, + sourceRoot: "/Users/alice/codex/repo", + destinationRoot: "/Users/alice/repo", + } + preflight := transferPreflight{ + trackedPatch: true, + untrackedFiles: []string{"a.txt"}, + } + + var out bytes.Buffer + if err := printDryRunPlan(&out, handoffApply, plan, preflight, true); err != nil { + t.Fatalf("printDryRunPlan() error = %v", err) + } + text := out.String() + if !strings.Contains(text, "source: $HOME/codex/repo") { + t.Fatalf("expected masked source path, got:\n%s", text) + } + if !strings.Contains(text, "destination: $HOME/repo") { + t.Fatalf("expected masked destination path, got:\n%s", text) + } + if strings.Contains(text, "/Users/alice/codex/repo") || strings.Contains(text, "/Users/alice/repo") { + t.Fatalf("did not expect raw home paths when masking is enabled, got:\n%s", text) + } +} + +func TestPrintDryRunPlanMaskDisabled(t *testing.T) { + t.Setenv("HOME", "/Users/alice") + plan := transferPlan{ + to: transferToLocal, + sourceRoot: "/Users/alice/codex/repo", + destinationRoot: "/Users/alice/repo", + } + preflight := transferPreflight{ + trackedPatch: true, + } + + var out bytes.Buffer + if err := printDryRunPlan(&out, handoffApply, plan, preflight, false); err != nil { + t.Fatalf("printDryRunPlan() error = %v", err) + } + text := out.String() + if !strings.Contains(text, "source: /Users/alice/codex/repo") { + t.Fatalf("expected raw source path, got:\n%s", text) + } + if !strings.Contains(text, "destination: /Users/alice/repo") { + t.Fatalf("expected raw destination path, got:\n%s", text) + } +} + type overwriteRunner struct { source string dest string diff --git a/cli/apply_transfer.go b/cli/apply_transfer.go index c5d431a..07fe694 100644 --- a/cli/apply_transfer.go +++ b/cli/apply_transfer.go @@ -13,17 +13,17 @@ import ( "github.com/spf13/cobra" ) -func printDryRunPlan(out io.Writer, mode handoffMode, plan transferPlan, preflight transferPreflight) error { +func printDryRunPlan(out io.Writer, mode handoffMode, plan transferPlan, preflight transferPreflight, maskPaths bool) error { if _, err := fmt.Fprintf(out, "%s plan\n", mode); err != nil { return err } if _, err := fmt.Fprintf(out, " to: %s\n", plan.to); err != nil { return err } - if _, err := fmt.Fprintf(out, " source: %s\n", plan.sourceRoot); err != nil { + if _, err := fmt.Fprintf(out, " source: %s\n", maskPathForDryRun(plan.sourceRoot, maskPaths)); err != nil { return err } - if _, err := fmt.Fprintf(out, " destination: %s\n", plan.destinationRoot); err != nil { + if _, err := fmt.Fprintf(out, " destination: %s\n", maskPathForDryRun(plan.destinationRoot, maskPaths)); err != nil { return err } if _, err := fmt.Fprintf(out, " overwrite: %t\n", mode == handoffOverwrite); err != nil { @@ -59,7 +59,7 @@ func printDryRunPlan(out io.Writer, mode handoffMode, plan transferPlan, preflig if _, err := fmt.Fprintln(out, "actions"); err != nil { return err } - actions := dryRunActions(mode, plan, preflight) + actions := dryRunActions(mode, plan, preflight, maskPaths) for idx, action := range actions { if _, err := fmt.Fprintf(out, " %d. %s\n", idx+1, action); err != nil { return err @@ -68,23 +68,27 @@ func printDryRunPlan(out io.Writer, mode handoffMode, plan transferPlan, preflig return nil } -func dryRunActions(mode handoffMode, plan transferPlan, preflight transferPreflight) []string { +func dryRunActions(mode handoffMode, plan transferPlan, preflight transferPreflight, maskPaths bool) []string { actions := make([]string, 0, len(preflight.untrackedFiles)+4) if mode == handoffOverwrite { - actions = append(actions, "[destructive] "+formatGitCommand([]string{"-C", plan.destinationRoot, "reset", "--hard"})) - actions = append(actions, "[destructive] "+formatGitCommand([]string{"-C", plan.destinationRoot, "clean", "-fd"})) + actions = append(actions, "[destructive] "+formatGitCommandForDryRun([]string{"-C", plan.destinationRoot, "reset", "--hard"}, maskPaths)) + actions = append(actions, "[destructive] "+formatGitCommandForDryRun([]string{"-C", plan.destinationRoot, "clean", "-fd"}, maskPaths)) } if preflight.trackedPatch { if mode == handoffApply { - actions = append(actions, formatGitCommand([]string{"-C", plan.destinationRoot, "apply", "--check", ""})) + actions = append(actions, formatGitCommandForDryRun([]string{"-C", plan.destinationRoot, "apply", "--check", ""}, maskPaths)) } - actions = append(actions, formatGitCommand([]string{"-C", plan.destinationRoot, "apply", ""})) + actions = append(actions, formatGitCommandForDryRun([]string{"-C", plan.destinationRoot, "apply", ""}, maskPaths)) } for _, rel := range preflight.untrackedFiles { - actions = append(actions, fmt.Sprintf("copy %s -> %s", filepath.Join(plan.sourceRoot, rel), filepath.Join(plan.destinationRoot, rel))) + actions = append(actions, fmt.Sprintf( + "copy %s -> %s", + maskPathForDryRun(filepath.Join(plan.sourceRoot, rel), maskPaths), + maskPathForDryRun(filepath.Join(plan.destinationRoot, rel), maskPaths), + )) } if len(actions) == 0 { @@ -95,6 +99,7 @@ func dryRunActions(mode handoffMode, plan transferPlan, preflight transferPrefli } func transferChanges(ctx context.Context, cmd *cobra.Command, runner git.Runner, sourceRoot, destinationRoot string, dryRun, resetDestination bool) error { + maskPaths := shouldMaskSensitivePaths(ctx) if resetDestination { if err := runGit(ctx, cmd, dryRun, runner, "-C", destinationRoot, "reset", "--hard"); err != nil { return err @@ -115,7 +120,7 @@ func transferChanges(ctx context.Context, cmd *cobra.Command, runner git.Runner, } defer func() { if err := removeTempPatch(patchFile); err != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to remove temp patch %s: %v\n", patchFile, err) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to remove temp patch %s: %v\n", maskPathForDryRun(patchFile, maskPaths), err) } }() @@ -142,7 +147,7 @@ func transferChanges(ctx context.Context, cmd *cobra.Command, runner git.Runner, return err } for _, rel := range untracked { - if err := copyFile(sourceRoot, destinationRoot, rel, dryRun, cmd.OutOrStdout()); err != nil { + if err := copyFile(sourceRoot, destinationRoot, rel, dryRun, cmd.OutOrStdout(), maskPaths); err != nil { return err } } @@ -170,11 +175,11 @@ func syncTrackedChangesFallback(ctx context.Context, runner git.Runner, sourceRo if err := removeTrackedPath(destinationRoot, change.oldRel); err != nil { return err } - if err := copyFile(sourceRoot, destinationRoot, change.newRel, false, io.Discard); err != nil { + if err := copyFile(sourceRoot, destinationRoot, change.newRel, false, io.Discard, false); err != nil { return err } case 'A', 'M', 'T', 'C': - if err := copyFile(sourceRoot, destinationRoot, change.newRel, false, io.Discard); err != nil { + if err := copyFile(sourceRoot, destinationRoot, change.newRel, false, io.Discard, false); err != nil { return err } default: diff --git a/cli/create.go b/cli/create.go index 0e2e565..608ee01 100644 --- a/cli/create.go +++ b/cli/create.go @@ -82,7 +82,7 @@ func newCreateCommand() *cobra.Command { } gitArgs := buildCreateWorktreeArgs(repoRoot, path, branch, base, branchExists) if opts.dryRun { - if _, err := fmt.Fprintln(cmd.OutOrStdout(), formatGitCommand(gitArgs)); err != nil { + if _, err := fmt.Fprintln(cmd.OutOrStdout(), formatGitCommandForDryRun(gitArgs, shouldMaskSensitivePaths(ctx))); err != nil { return err } return nil diff --git a/cli/dry_run_mask_test.go b/cli/dry_run_mask_test.go new file mode 100644 index 0000000..bdd6e12 --- /dev/null +++ b/cli/dry_run_mask_test.go @@ -0,0 +1,137 @@ +package cli + +import ( + "fmt" + "io" + "testing" + + "github.com/spf13/cobra" +) + +func TestMaskSensitivePathsPrecedence(t *testing.T) { + t.Run("default", func(t *testing.T) { + project := t.TempDir() + t.Setenv("HOME", t.TempDir()) + t.Setenv("GWTT_DRY_RUN_MASK_SENSITIVE_PATHS", "") + + got, err := runMaskCommand(t, project) + if err != nil { + t.Fatalf("run command: %v", err) + } + if !got { + t.Fatalf("mask_sensitive_paths = false, want true") + } + }) + + t.Run("config", func(t *testing.T) { + project := t.TempDir() + t.Setenv("HOME", t.TempDir()) + t.Setenv("GWTT_DRY_RUN_MASK_SENSITIVE_PATHS", "") + writeConfig(t, project, "[dry_run]\nmask_sensitive_paths = false\n") + + got, err := runMaskCommand(t, project) + if err != nil { + t.Fatalf("run command: %v", err) + } + if got { + t.Fatalf("mask_sensitive_paths = true, want false") + } + }) + + t.Run("env_over_config", func(t *testing.T) { + project := t.TempDir() + t.Setenv("HOME", t.TempDir()) + writeConfig(t, project, "[dry_run]\nmask_sensitive_paths = true\n") + t.Setenv("GWTT_DRY_RUN_MASK_SENSITIVE_PATHS", "false") + + got, err := runMaskCommand(t, project) + if err != nil { + t.Fatalf("run command: %v", err) + } + if got { + t.Fatalf("mask_sensitive_paths = true, want false") + } + }) + + t.Run("flag_over_env", func(t *testing.T) { + project := t.TempDir() + t.Setenv("HOME", t.TempDir()) + t.Setenv("GWTT_DRY_RUN_MASK_SENSITIVE_PATHS", "true") + + got, err := runMaskCommand(t, project, "--mask-sensitive-paths=false") + if err != nil { + t.Fatalf("run command: %v", err) + } + if got { + t.Fatalf("mask_sensitive_paths = true, want false") + } + }) + + t.Run("flag_true_over_env_false", func(t *testing.T) { + project := t.TempDir() + t.Setenv("HOME", t.TempDir()) + t.Setenv("GWTT_DRY_RUN_MASK_SENSITIVE_PATHS", "false") + + got, err := runMaskCommand(t, project, "--mask-sensitive-paths") + if err != nil { + t.Fatalf("run command: %v", err) + } + if !got { + t.Fatalf("mask_sensitive_paths = false, want true") + } + }) + + t.Run("no_mask_flag_disables", func(t *testing.T) { + project := t.TempDir() + t.Setenv("HOME", t.TempDir()) + t.Setenv("GWTT_DRY_RUN_MASK_SENSITIVE_PATHS", "true") + + got, err := runMaskCommand(t, project, "--no-mask-sensitive-paths") + if err != nil { + t.Fatalf("run command: %v", err) + } + if got { + t.Fatalf("mask_sensitive_paths = true, want false") + } + }) + + t.Run("mask_and_no_mask_conflict", func(t *testing.T) { + project := t.TempDir() + t.Setenv("HOME", t.TempDir()) + t.Setenv("GWTT_DRY_RUN_MASK_SENSITIVE_PATHS", "") + + _, err := runMaskCommand(t, project, "--mask-sensitive-paths=false", "--no-mask-sensitive-paths") + if err == nil { + t.Fatalf("expected conflict error") + } + if err.Error() != "cannot use both --mask-sensitive-paths and --no-mask-sensitive-paths" { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func runMaskCommand(t *testing.T, cwd string, args ...string) (bool, error) { + t.Helper() + cmd, _ := gitWorkTreeCommand() + var got bool + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.AddCommand(&cobra.Command{ + Use: "inspect-mask", + RunE: func(cmd *cobra.Command, args []string) error { + cfg, ok := configFromContext(cmd.Context()) + if !ok { + return fmt.Errorf("config missing from context") + } + got = cfg.DryRun.MaskSensitivePaths + return nil + }, + }) + cmd.SetArgs(append(args, "inspect-mask")) + + restore := chdir(t, cwd) + defer restore() + + err := cmd.Execute() + return got, err +} diff --git a/cli/git_exec.go b/cli/git_exec.go index a298b8c..5a9d1fc 100644 --- a/cli/git_exec.go +++ b/cli/git_exec.go @@ -10,7 +10,7 @@ import ( func runGit(ctx context.Context, cmd *cobra.Command, dryRun bool, runner git.Runner, args ...string) error { if dryRun { - if _, err := fmt.Fprintln(cmd.OutOrStdout(), formatGitCommand(args)); err != nil { + if _, err := fmt.Fprintln(cmd.OutOrStdout(), formatGitCommandForDryRun(args, shouldMaskSensitivePaths(ctx))); err != nil { return err } return nil diff --git a/cli/git_exec_test.go b/cli/git_exec_test.go new file mode 100644 index 0000000..c42b4db --- /dev/null +++ b/cli/git_exec_test.go @@ -0,0 +1,58 @@ +package cli + +import ( + "bytes" + "context" + "strings" + "testing" + + "github.com/pi2pie/git-worktree-tasks/internal/config" + "github.com/spf13/cobra" +) + +type neverRunRunner struct{} + +func (neverRunRunner) Run(_ context.Context, _ ...string) (string, string, error) { + return "", "", nil +} + +func TestRunGitDryRunMasksPathsByDefault(t *testing.T) { + t.Setenv("HOME", "/Users/alice") + + cmd := &cobra.Command{} + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + + if err := runGit(context.Background(), cmd, true, neverRunRunner{}, "-C", "/Users/alice/repo", "status"); err != nil { + t.Fatalf("runGit() error = %v", err) + } + got := strings.TrimSpace(out.String()) + if got != "git -C '$HOME/repo' status" { + t.Fatalf("runGit() output = %q, want %q", got, "git -C '$HOME/repo' status") + } +} + +func TestRunGitDryRunRespectsConfigMaskDisabled(t *testing.T) { + t.Setenv("HOME", "/Users/alice") + + cmd := &cobra.Command{} + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + + cfg := &config.Config{ + DryRun: config.DryRunConfig{ + MaskSensitivePaths: false, + }, + } + ctx := withConfig(context.Background(), cfg) + + if err := runGit(ctx, cmd, true, neverRunRunner{}, "-C", "/Users/alice/repo", "status"); err != nil { + t.Fatalf("runGit() error = %v", err) + } + got := strings.TrimSpace(out.String()) + if got != "git -C /Users/alice/repo status" { + t.Fatalf("runGit() output = %q, want %q", got, "git -C /Users/alice/repo status") + } +} diff --git a/cli/path_mask.go b/cli/path_mask.go new file mode 100644 index 0000000..f787ed5 --- /dev/null +++ b/cli/path_mask.go @@ -0,0 +1,199 @@ +package cli + +import ( + "context" + "os" + "path/filepath" + "runtime" + "strings" +) + +const ( + posixHomeToken = "$HOME" + windowsHomeToken = "%USERPROFILE%" +) + +type pathMaskContext struct { + enabled bool + home string + windows bool +} + +func shouldMaskSensitivePaths(ctx context.Context) bool { + cfg, ok := configFromContext(ctx) + if !ok || cfg == nil { + return true + } + return cfg.DryRun.MaskSensitivePaths +} + +func resolvePathMaskContext(mask bool) pathMaskContext { + if !mask { + return pathMaskContext{} + } + home, err := os.UserHomeDir() + if err != nil || strings.TrimSpace(home) == "" { + return pathMaskContext{} + } + return pathMaskContext{ + enabled: true, + home: home, + windows: runtime.GOOS == "windows", + } +} + +func formatGitCommandForDryRun(args []string, mask bool) string { + return formatGitCommandForDryRunWithContext(args, resolvePathMaskContext(mask)) +} + +func formatGitCommandForDryRunWithContext(args []string, maskCtx pathMaskContext) string { + if !maskCtx.enabled { + return formatGitCommand(args) + } + return "git " + formatArgs(maskGitArgs(args, maskCtx)) +} + +func maskGitArgs(args []string, maskCtx pathMaskContext) []string { + masked := make([]string, len(args)) + pathValue := false + for i, arg := range args { + value := arg + if pathValue || looksLikeAbsolutePath(arg, maskCtx.windows) { + value = maskHomePathWithContext(arg, maskCtx) + } + masked[i] = value + pathValue = expectsPathValue(arg) + } + return masked +} + +func expectsPathValue(arg string) bool { + switch arg { + case "-C", "--git-dir", "--work-tree": + return true + default: + return false + } +} + +func looksLikeAbsolutePath(value string, windows bool) bool { + if windows { + return isWindowsAbsolutePath(value) + } + return filepath.IsAbs(value) +} + +func isWindowsAbsolutePath(value string) bool { + if len(value) >= 3 && isASCIILetter(value[0]) && value[1] == ':' && (value[2] == '\\' || value[2] == '/') { + return true + } + if strings.HasPrefix(value, `\\`) || strings.HasPrefix(value, "//") { + return true + } + return strings.HasPrefix(value, `\`) +} + +func isASCIILetter(ch byte) bool { + return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') +} + +func maskPathForDryRun(path string, mask bool) string { + return maskPathForDryRunWithContext(path, resolvePathMaskContext(mask)) +} + +func maskPathForDryRunWithContext(path string, maskCtx pathMaskContext) string { + return maskHomePathWithContext(path, maskCtx) +} + +func maskHomePathWithContext(path string, maskCtx pathMaskContext) string { + if !maskCtx.enabled { + return path + } + return maskHomePathWith(path, maskCtx.home, maskCtx.windows) +} + +func maskHomePathWith(path, home string, windows bool) string { + if strings.TrimSpace(home) == "" { + return path + } + normalizedPath := normalizePathForMask(path, windows) + normalizedHome := normalizePathForMask(home, windows) + if normalizedPath == "" || normalizedHome == "" { + return path + } + + token := posixHomeToken + separator := "/" + if windows { + token = windowsHomeToken + separator = `\` + } + + if pathEqualsForMask(normalizedPath, normalizedHome, windows) { + return token + } + if !hasPathPrefixForMask(normalizedPath, normalizedHome, windows) { + return path + } + + relative := trimLeadingSeparators(normalizedPath[len(normalizedHome):], windows) + if relative == "" { + return token + } + return token + separator + relative +} + +func normalizePathForMask(path string, windows bool) string { + normalized := strings.TrimSpace(path) + if normalized == "" { + return "" + } + if windows { + normalized = strings.ReplaceAll(normalized, "/", `\`) + for strings.HasSuffix(normalized, `\`) && !isWindowsDriveRoot(normalized) { + normalized = strings.TrimSuffix(normalized, `\`) + } + return normalized + } + if normalized != "/" { + normalized = strings.TrimRight(normalized, "/") + if normalized == "" { + return "/" + } + } + return normalized +} + +func isWindowsDriveRoot(path string) bool { + return len(path) == 3 && isASCIILetter(path[0]) && path[1] == ':' && path[2] == '\\' +} + +func pathEqualsForMask(left, right string, windows bool) bool { + if windows { + return strings.EqualFold(left, right) + } + return left == right +} + +func hasPathPrefixForMask(path, prefix string, windows bool) bool { + separator := "/" + if windows { + path = strings.ToLower(path) + prefix = strings.ToLower(prefix) + separator = `\` + } + if path == prefix { + return true + } + if !strings.HasSuffix(prefix, separator) { + prefix += separator + } + return strings.HasPrefix(path, prefix) +} + +func trimLeadingSeparators(value string, windows bool) string { + if windows { + return strings.TrimLeft(value, `/\`) + } + return strings.TrimLeft(value, "/") +} diff --git a/cli/path_mask_test.go b/cli/path_mask_test.go new file mode 100644 index 0000000..9db80be --- /dev/null +++ b/cli/path_mask_test.go @@ -0,0 +1,116 @@ +package cli + +import ( + "strings" + "testing" +) + +func TestMaskHomePathWithPOSIX(t *testing.T) { + home := "/Users/alice" + tests := []struct { + name string + path string + want string + }{ + { + name: "exact home", + path: "/Users/alice", + want: "$HOME", + }, + { + name: "descendant path", + path: "/Users/alice/project", + want: "$HOME/project", + }, + { + name: "prefix safe mismatch", + path: "/Users/alice2/project", + want: "/Users/alice2/project", + }, + { + name: "outside home", + path: "/tmp/project", + want: "/tmp/project", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := maskHomePathWith(tt.path, home, false) + if got != tt.want { + t.Fatalf("maskHomePathWith(%q) = %q, want %q", tt.path, got, tt.want) + } + }) + } +} + +func TestMaskHomePathWithWindows(t *testing.T) { + home := `C:\Users\Alice` + tests := []struct { + name string + path string + want string + }{ + { + name: "exact home", + path: `C:\Users\Alice`, + want: `%USERPROFILE%`, + }, + { + name: "descendant path", + path: `C:\Users\Alice\project`, + want: `%USERPROFILE%\project`, + }, + { + name: "mixed case and separator", + path: `c:/users/alice/project`, + want: `%USERPROFILE%\project`, + }, + { + name: "prefix safe mismatch", + path: `C:\Users\Alice2\project`, + want: `C:\Users\Alice2\project`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := maskHomePathWith(tt.path, home, true) + if got != tt.want { + t.Fatalf("maskHomePathWith(%q) = %q, want %q", tt.path, got, tt.want) + } + }) + } +} + +func TestFormatGitCommandForDryRunWithContext(t *testing.T) { + args := []string{"-C", "/Users/alice/repo", "worktree", "add", "/Users/alice/repo_task", "main"} + maskCtx := pathMaskContext{ + enabled: true, + home: "/Users/alice", + windows: false, + } + + got := formatGitCommandForDryRunWithContext(args, maskCtx) + if !strings.Contains(got, "$HOME/repo") { + t.Fatalf("expected repo root to be masked, got %q", got) + } + if !strings.Contains(got, "$HOME/repo_task") { + t.Fatalf("expected worktree path to be masked, got %q", got) + } + if strings.Contains(got, "/Users/alice/repo") { + t.Fatalf("did not expect raw home path in output, got %q", got) + } +} + +func TestFormatGitCommandForDryRunWithContextDisabled(t *testing.T) { + args := []string{"-C", "/Users/alice/repo", "status"} + maskCtx := pathMaskContext{ + enabled: false, + home: "/Users/alice", + windows: false, + } + + got := formatGitCommandForDryRunWithContext(args, maskCtx) + if got != "git -C /Users/alice/repo status" { + t.Fatalf("formatGitCommandForDryRunWithContext() = %q, want %q", got, "git -C /Users/alice/repo status") + } +} diff --git a/cli/root.go b/cli/root.go index 3c3204c..95f79d2 100644 --- a/cli/root.go +++ b/cli/root.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -var Version = "0.1.2-alpha.2" +var Version = "0.1.2-alpha.3" var ( errCanceled = errors.New("git worktree task process canceled") @@ -47,12 +47,14 @@ func RootCommand() *cobra.Command { } type runState struct { - hasWarnings bool - exitOnWarning bool - noColor bool - theme string - mode string - listThemes bool + hasWarnings bool + exitOnWarning bool + noColor bool + maskSensitivePaths bool + noMaskSensitivePaths bool + theme string + mode string + listThemes bool } func gitWorkTreeCommand() (*cobra.Command, *runState) { @@ -80,6 +82,8 @@ func gitWorkTreeCommand() (*cobra.Command, *runState) { cmd.SetOut(os.Stdout) cmd.SetErr(os.Stderr) cmd.PersistentFlags().BoolVar(&state.noColor, "nocolor", false, "disable color output") + cmd.PersistentFlags().BoolVar(&state.maskSensitivePaths, "mask-sensitive-paths", true, "mask home-directory paths in --dry-run output") + cmd.PersistentFlags().BoolVar(&state.noMaskSensitivePaths, "no-mask-sensitive-paths", false, "disable home-directory path masking in --dry-run output") cmd.PersistentFlags().StringVar(&state.theme, "theme", ui.DefaultThemeName(), "color theme: "+strings.Join(ui.ThemeNames(), ", ")) cmd.PersistentFlags().StringVarP(&state.mode, "mode", "m", "classic", "execution mode: classic or codex") cmd.PersistentFlags().BoolVar(&state.listThemes, "themes", false, "print available themes and exit") @@ -103,6 +107,15 @@ func gitWorkTreeCommand() (*cobra.Command, *runState) { return err } cfg.Mode = mode + if cmd.Flags().Changed("mask-sensitive-paths") && cmd.Flags().Changed("no-mask-sensitive-paths") { + return fmt.Errorf("cannot use both --mask-sensitive-paths and --no-mask-sensitive-paths") + } + if cmd.Flags().Changed("mask-sensitive-paths") { + cfg.DryRun.MaskSensitivePaths = state.maskSensitivePaths + } + if cmd.Flags().Changed("no-mask-sensitive-paths") { + cfg.DryRun.MaskSensitivePaths = !state.noMaskSensitivePaths + } cmd.SetContext(withConfig(cmd.Context(), &cfg)) themeName := state.theme if !cmd.Flags().Changed("theme") { diff --git a/docs/plans/jobs/2026-02-07-dry-run-path-masking-implementation.md b/docs/plans/jobs/2026-02-07-dry-run-path-masking-implementation.md new file mode 100644 index 0000000..d25b039 --- /dev/null +++ b/docs/plans/jobs/2026-02-07-dry-run-path-masking-implementation.md @@ -0,0 +1,53 @@ +--- +title: "Implement dry-run home path masking with cross-platform tokens" +created-date: 2026-02-07 +modified-date: 2026-02-07 +status: completed +agent: codex +--- + +## Summary + +- Implemented configurable dry-run masking via `[dry_run].mask_sensitive_paths` with default `true`. +- Added platform-aware home-token masking: + - POSIX: `$HOME/...` + - Windows: `%USERPROFILE%\\...` +- Wired masking into all dry-run command paths in scope: + - shared `runGit` dry-run printing (covers `finish` and `cleanup`) + - `create` dry-run command output + - codex `apply`/`overwrite` dry-run plan fields, action commands, and copy/symlink action lines +- Kept non-dry-run behavior unchanged. +- Added direct overrides for masking behavior: + - CLI: `--mask-sensitive-paths[=true|false]` + - CLI: `--no-mask-sensitive-paths` + - ENV: `GWTT_DRY_RUN_MASK_SENSITIVE_PATHS` +- Follow-up lint cleanup: removed unused `maskHomePath` wrapper after golangci-lint `unused` finding. +- Updated README and config schema docs; regenerated man pages (`make man`). + +## Files Updated + +- `internal/config/config.go` +- `internal/config/config_test.go` +- `cli/path_mask.go` +- `cli/path_mask_test.go` +- `cli/git_exec.go` +- `cli/git_exec_test.go` +- `cli/root.go` +- `cli/dry_run_mask_test.go` +- `cli/create.go` +- `cli/apply_command.go` +- `cli/apply_transfer.go` +- `cli/apply_files.go` +- `cli/apply_test.go` +- `README.md` +- `docs/schemas/config-gwtt.md` + +## Verification + +- `go test ./...` +- `make man` +- `golangci-lint run` + +## Related Plans + +- `docs/plans/plan-2026-02-07-dry-run-path-masking.md` diff --git a/docs/plans/plan-2026-02-07-dry-run-path-masking.md b/docs/plans/plan-2026-02-07-dry-run-path-masking.md index 1da6aa9..2048f67 100644 --- a/docs/plans/plan-2026-02-07-dry-run-path-masking.md +++ b/docs/plans/plan-2026-02-07-dry-run-path-masking.md @@ -2,7 +2,7 @@ title: "Dry-run path masking for sensitive local paths" created-date: 2026-02-07 modified-date: 2026-02-07 -status: draft +status: completed agent: codex --- diff --git a/docs/schemas/config-gwtt.md b/docs/schemas/config-gwtt.md index 208ab32..a100a3c 100644 --- a/docs/schemas/config-gwtt.md +++ b/docs/schemas/config-gwtt.md @@ -1,7 +1,7 @@ --- title: "gwtt configuration schema" created-date: 2026-01-27 -modified-date: 2026-02-04 +modified-date: 2026-02-07 status: in-progress agent: codex --- @@ -23,6 +23,7 @@ Define the authoritative configuration schema for `gwtt`, including keys, types, - `GWTT_THEME` overrides `[theme].name`. - `GWTT_COLOR` overrides `[ui].color_enabled`. - `GWTT_MODE` overrides `mode`. +- `GWTT_DRY_RUN_MASK_SENSITIVE_PATHS` overrides `[dry_run].mask_sensitive_paths`. - `CODEX_HOME` is consumed in `mode="codex"` to locate `$CODEX_HOME/worktrees` (this is a Codex App/Codex CLI convention, not a `gwtt` config key). - Fallback: if `CODEX_HOME` is unset, `gwtt` should assume `~/.codex` (home dir + `/.codex`) to align with Codex defaults. - Note: confirm the default path against current Codex App/Codex CLI docs when implementing (and prefer matching their behavior over introducing a new `gwtt`-specific default). @@ -45,6 +46,17 @@ Define the authoritative configuration schema for `gwtt`, including keys, types, - `grid` (bool, default: `false`) +### `[dry_run]` + +- `mask_sensitive_paths` (bool, default: `true`) + - When `true`, home-prefixed paths in `--dry-run` output are masked: + - POSIX: `$HOME/...` + - Windows: `%USERPROFILE%\\...` + - When `false`, `--dry-run` output keeps raw absolute paths. + - CLI overrides: + - `--mask-sensitive-paths[=true|false]` + - `--no-mask-sensitive-paths` + ### `[create]` - `output` (string enum: `text`, `raw`; default: `text`) @@ -120,6 +132,9 @@ color_enabled = true [table] grid = false +[dry_run] +mask_sensitive_paths = true + [create] output = "text" skip_existing = false diff --git a/examples/gwtt.config.toml b/examples/gwtt.config.toml index c320016..b8e00cb 100644 --- a/examples/gwtt.config.toml +++ b/examples/gwtt.config.toml @@ -10,6 +10,9 @@ color_enabled = true [table] grid = false +[dry_run] +mask_sensitive_paths = true # set false to keep raw absolute paths in --dry-run output (CLI: --no-mask-sensitive-paths) + [create] output = "text" skip_existing = false diff --git a/internal/config/config.go b/internal/config/config.go index fe23722..d3e2c78 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,8 +10,9 @@ import ( ) const ( - envColorEnabled = "GWTT_COLOR" - envMode = "GWTT_MODE" + envColorEnabled = "GWTT_COLOR" + envMode = "GWTT_MODE" + envDryRunMaskSensitivePath = "GWTT_DRY_RUN_MASK_SENSITIVE_PATHS" ) type Config struct { @@ -19,6 +20,7 @@ type Config struct { Theme ThemeConfig UI UIConfig Table TableConfig + DryRun DryRunConfig Create CreateConfig List ListConfig Status StatusConfig @@ -38,6 +40,10 @@ type TableConfig struct { Grid bool } +type DryRunConfig struct { + MaskSensitivePaths bool +} + type CreateConfig struct { Output string SkipExisting bool @@ -93,6 +99,9 @@ func DefaultConfig() Config { Table: TableConfig{ Grid: false, }, + DryRun: DryRunConfig{ + MaskSensitivePaths: true, + }, Create: CreateConfig{ Output: "text", SkipExisting: false, @@ -137,6 +146,7 @@ type loadedConfigFile struct { Theme themeConfigFile `toml:"theme"` UI uiConfigFile `toml:"ui"` Table tableConfigFile `toml:"table"` + DryRun dryRunConfigFile `toml:"dry_run"` Create createConfigFile `toml:"create"` List listConfigFile `toml:"list"` Status statusConfigFile `toml:"status"` @@ -156,6 +166,10 @@ type tableConfigFile struct { Grid *bool `toml:"grid"` } +type dryRunConfigFile struct { + MaskSensitivePaths *bool `toml:"mask_sensitive_paths"` +} + type createConfigFile struct { Output *string `toml:"output"` SkipExisting *bool `toml:"skip_existing"` @@ -295,6 +309,11 @@ func applyEnvConfig(cfg *Config) error { } else if ok { cfg.UI.ColorEnabled = enabled } + if enabled, ok, err := envBool(envDryRunMaskSensitivePath); err != nil { + return err + } else if ok { + cfg.DryRun.MaskSensitivePaths = enabled + } return nil } @@ -342,6 +361,9 @@ func applyConfig(cfg *Config, flags *gridFlags, file loadedConfigFile) { if file.Table.Grid != nil { cfg.Table.Grid = *file.Table.Grid } + if file.DryRun.MaskSensitivePaths != nil { + cfg.DryRun.MaskSensitivePaths = *file.DryRun.MaskSensitivePaths + } if output, ok := trimString(file.Create.Output); ok { cfg.Create.Output = output } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 397f418..7fdaacf 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -6,6 +6,13 @@ import ( "testing" ) +func TestDefaultConfigDryRunMaskSensitivePaths(t *testing.T) { + cfg := DefaultConfig() + if !cfg.DryRun.MaskSensitivePaths { + t.Fatalf("DryRun.MaskSensitivePaths = false, want true") + } +} + func TestLoadConfigPrecedence(t *testing.T) { home := t.TempDir() project := t.TempDir() @@ -161,3 +168,51 @@ grid = false t.Fatalf("Status.Grid = true, want false (should cascade from table.grid)") } } + +func TestLoadConfigDryRunMaskSensitivePathsFalse(t *testing.T) { + project := t.TempDir() + writeFile(t, filepath.Join(project, projectConfigPrimary), ` +[dry_run] +mask_sensitive_paths = false +`) + + restore := chdir(t, project) + defer restore() + + cfg, err := Load() + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if cfg.DryRun.MaskSensitivePaths { + t.Fatalf("DryRun.MaskSensitivePaths = true, want false") + } +} + +func TestLoadEnvDryRunMaskSensitivePathsOverride(t *testing.T) { + project := t.TempDir() + writeFile(t, filepath.Join(project, projectConfigPrimary), ` +[dry_run] +mask_sensitive_paths = false +`) + + restore := chdir(t, project) + defer restore() + t.Setenv(envDryRunMaskSensitivePath, "true") + + cfg, err := Load() + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if !cfg.DryRun.MaskSensitivePaths { + t.Fatalf("DryRun.MaskSensitivePaths = false, want true") + } +} + +func TestLoadEnvDryRunMaskSensitivePathsInvalid(t *testing.T) { + t.Setenv(envDryRunMaskSensitivePath, "maybe") + + _, err := Load() + if err == nil { + t.Fatalf("Load() expected error") + } +} diff --git a/man/man1/git-worktree-tasks.1 b/man/man1/git-worktree-tasks.1 index df286db..938cdc6 100644 --- a/man/man1/git-worktree-tasks.1 +++ b/man/man1/git-worktree-tasks.1 @@ -17,10 +17,18 @@ Create, manage, and clean up git worktrees based on task names. \fB-h\fP, \fB--help\fP[=false] help for git-worktree-tasks +.PP +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + .PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/git-worktree-tasks_apply.1 b/man/man1/git-worktree-tasks_apply.1 index ddf4033..2590276 100644 --- a/man/man1/git-worktree-tasks_apply.1 +++ b/man/man1/git-worktree-tasks_apply.1 @@ -35,9 +35,17 @@ Apply non-destructive changes between a Codex worktree and local checkout .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/git-worktree-tasks_cleanup.1 b/man/man1/git-worktree-tasks_cleanup.1 index 9817e1f..62c3638 100644 --- a/man/man1/git-worktree-tasks_cleanup.1 +++ b/man/man1/git-worktree-tasks_cleanup.1 @@ -43,9 +43,17 @@ Remove a task worktree and/or branch .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/git-worktree-tasks_create.1 b/man/man1/git-worktree-tasks_create.1 index 5c7e029..9c27c52 100644 --- a/man/man1/git-worktree-tasks_create.1 +++ b/man/man1/git-worktree-tasks_create.1 @@ -43,9 +43,17 @@ Create a worktree and branch for a task .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/git-worktree-tasks_finish.1 b/man/man1/git-worktree-tasks_finish.1 index f960646..9416aee 100644 --- a/man/man1/git-worktree-tasks_finish.1 +++ b/man/man1/git-worktree-tasks_finish.1 @@ -59,9 +59,17 @@ Merge a task branch into a target branch .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/git-worktree-tasks_list.1 b/man/man1/git-worktree-tasks_list.1 index 23cf8ad..55cb66a 100644 --- a/man/man1/git-worktree-tasks_list.1 +++ b/man/man1/git-worktree-tasks_list.1 @@ -47,9 +47,17 @@ List task worktrees .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/git-worktree-tasks_overwrite.1 b/man/man1/git-worktree-tasks_overwrite.1 index a94888f..9fe5c86 100644 --- a/man/man1/git-worktree-tasks_overwrite.1 +++ b/man/man1/git-worktree-tasks_overwrite.1 @@ -31,9 +31,17 @@ Overwrite destination with source changes in codex mode .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/git-worktree-tasks_status.1 b/man/man1/git-worktree-tasks_status.1 index 312e12a..a542966 100644 --- a/man/man1/git-worktree-tasks_status.1 +++ b/man/man1/git-worktree-tasks_status.1 @@ -51,9 +51,17 @@ Show detailed worktree status .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/gwtt.1 b/man/man1/gwtt.1 index b543a7d..19422af 100644 --- a/man/man1/gwtt.1 +++ b/man/man1/gwtt.1 @@ -17,10 +17,18 @@ Create, manage, and clean up git worktrees based on task names. \fB-h\fP, \fB--help\fP[=false] help for gwtt +.PP +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + .PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/gwtt_apply.1 b/man/man1/gwtt_apply.1 index a96b80d..63ea8b6 100644 --- a/man/man1/gwtt_apply.1 +++ b/man/man1/gwtt_apply.1 @@ -35,9 +35,17 @@ Apply non-destructive changes between a Codex worktree and local checkout .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/gwtt_cleanup.1 b/man/man1/gwtt_cleanup.1 index d8e24a6..d394556 100644 --- a/man/man1/gwtt_cleanup.1 +++ b/man/man1/gwtt_cleanup.1 @@ -43,9 +43,17 @@ Remove a task worktree and/or branch .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/gwtt_create.1 b/man/man1/gwtt_create.1 index 1c4bdf6..5786e7f 100644 --- a/man/man1/gwtt_create.1 +++ b/man/man1/gwtt_create.1 @@ -43,9 +43,17 @@ Create a worktree and branch for a task .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/gwtt_finish.1 b/man/man1/gwtt_finish.1 index 6f7138c..9872a5b 100644 --- a/man/man1/gwtt_finish.1 +++ b/man/man1/gwtt_finish.1 @@ -59,9 +59,17 @@ Merge a task branch into a target branch .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/gwtt_list.1 b/man/man1/gwtt_list.1 index 77c2e9e..1ad660a 100644 --- a/man/man1/gwtt_list.1 +++ b/man/man1/gwtt_list.1 @@ -47,9 +47,17 @@ List task worktrees .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/gwtt_overwrite.1 b/man/man1/gwtt_overwrite.1 index 9bff581..ab52df7 100644 --- a/man/man1/gwtt_overwrite.1 +++ b/man/man1/gwtt_overwrite.1 @@ -31,9 +31,17 @@ Overwrite destination with source changes in codex mode .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output diff --git a/man/man1/gwtt_status.1 b/man/man1/gwtt_status.1 index e199029..fb6c330 100644 --- a/man/man1/gwtt_status.1 +++ b/man/man1/gwtt_status.1 @@ -51,9 +51,17 @@ Show detailed worktree status .SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--mask-sensitive-paths\fP[=true] + mask home-directory paths in --dry-run output + +.PP \fB-m\fP, \fB--mode\fP="classic" execution mode: classic or codex +.PP +\fB--no-mask-sensitive-paths\fP[=false] + disable home-directory path masking in --dry-run output + .PP \fB--nocolor\fP[=false] disable color output From 324be7fd10b4f64a8b22291b8449d5550b17f848 Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 20:15:48 +0800 Subject: [PATCH 13/15] fix: dry-run masked home path quoting --- cli/git_exec_test.go | 4 +-- cli/path_mask.go | 28 ++++++++++++++++++- cli/path_mask_test.go | 18 ++++++++++++ ...026-02-07-fix-dry-run-home-mask-quoting.md | 26 +++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 docs/plans/jobs/2026-02-07-fix-dry-run-home-mask-quoting.md diff --git a/cli/git_exec_test.go b/cli/git_exec_test.go index c42b4db..7474980 100644 --- a/cli/git_exec_test.go +++ b/cli/git_exec_test.go @@ -28,8 +28,8 @@ func TestRunGitDryRunMasksPathsByDefault(t *testing.T) { t.Fatalf("runGit() error = %v", err) } got := strings.TrimSpace(out.String()) - if got != "git -C '$HOME/repo' status" { - t.Fatalf("runGit() output = %q, want %q", got, "git -C '$HOME/repo' status") + if got != `git -C "$HOME/repo" status` { + t.Fatalf("runGit() output = %q, want %q", got, `git -C "$HOME/repo" status`) } } diff --git a/cli/path_mask.go b/cli/path_mask.go index f787ed5..b7813dc 100644 --- a/cli/path_mask.go +++ b/cli/path_mask.go @@ -50,7 +50,33 @@ func formatGitCommandForDryRunWithContext(args []string, maskCtx pathMaskContext if !maskCtx.enabled { return formatGitCommand(args) } - return "git " + formatArgs(maskGitArgs(args, maskCtx)) + return "git " + formatDryRunArgs(maskGitArgs(args, maskCtx), maskCtx) +} + +func formatDryRunArgs(args []string, maskCtx pathMaskContext) string { + parts := make([]string, 0, len(args)) + for _, arg := range args { + parts = append(parts, quoteDryRunArg(arg, maskCtx)) + } + return strings.Join(parts, " ") +} + +func quoteDryRunArg(arg string, maskCtx pathMaskContext) string { + if !maskCtx.windows && strings.HasPrefix(arg, posixHomeToken) { + return quotePosixHomeArg(arg) + } + return shellQuote(arg) +} + +func quotePosixHomeArg(path string) string { + rest := path[len(posixHomeToken):] + escapedRest := strings.NewReplacer( + `\`, `\\`, + `"`, `\"`, + "`", "\\`", + "$", `\$`, + ).Replace(rest) + return `"$HOME` + escapedRest + `"` } func maskGitArgs(args []string, maskCtx pathMaskContext) []string { diff --git a/cli/path_mask_test.go b/cli/path_mask_test.go index 9db80be..856f475 100644 --- a/cli/path_mask_test.go +++ b/cli/path_mask_test.go @@ -99,6 +99,24 @@ func TestFormatGitCommandForDryRunWithContext(t *testing.T) { if strings.Contains(got, "/Users/alice/repo") { t.Fatalf("did not expect raw home path in output, got %q", got) } + if strings.Contains(got, "'$HOME/") { + t.Fatalf("did not expect single-quoted masked path, got %q", got) + } +} + +func TestFormatGitCommandForDryRunWithContextKeepsMaskedPathsExecutable(t *testing.T) { + args := []string{"-C", "/Users/alice/repo with spaces", "status"} + maskCtx := pathMaskContext{ + enabled: true, + home: "/Users/alice", + windows: false, + } + + got := formatGitCommandForDryRunWithContext(args, maskCtx) + want := `git -C "$HOME/repo with spaces" status` + if got != want { + t.Fatalf("formatGitCommandForDryRunWithContext() = %q, want %q", got, want) + } } func TestFormatGitCommandForDryRunWithContextDisabled(t *testing.T) { diff --git a/docs/plans/jobs/2026-02-07-fix-dry-run-home-mask-quoting.md b/docs/plans/jobs/2026-02-07-fix-dry-run-home-mask-quoting.md new file mode 100644 index 0000000..d7c4ca8 --- /dev/null +++ b/docs/plans/jobs/2026-02-07-fix-dry-run-home-mask-quoting.md @@ -0,0 +1,26 @@ +--- +title: "Fix dry-run masked home path quoting" +created-date: 2026-02-07 +status: completed +agent: codex +--- + +## Summary + +Updated dry-run command formatting so masked POSIX home paths remain executable when copied from output. + +## What Changed + +- Added dry-run-specific argument formatting in `cli/path_mask.go`. +- Kept masked POSIX home paths in expandable form (`"$HOME/..."`) instead of single-quoted literals. +- Escaped unsafe characters in the suffix after `$HOME` so only the home token expands. +- Updated/added tests in `cli/git_exec_test.go` and `cli/path_mask_test.go`. + +## Why + +Single-quoting `$HOME` in dry-run output prevented shell expansion, so copy-pasted commands failed. The new formatting preserves masking while keeping commands runnable. + +## Validation + +- `GOCACHE=/tmp/gocache-gwtt go test ./cli -run 'TestRunGitDryRunMasksPathsByDefault|TestFormatGitCommandForDryRunWithContext' -v` +- `GOCACHE=/tmp/gocache-gwtt go test ./...` From e0c9ee6ffd48e646de7fe60c8b229dd2dff3f025 Mon Sep 17 00:00:00 2001 From: nakolus <44661068+dev-pi2pie@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:45:34 +0800 Subject: [PATCH 14/15] Mask symlink targets in dry-run output --- cli/apply_files.go | 2 +- .../plans/jobs/2026-02-07-mask-symlink-targets.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 docs/plans/jobs/2026-02-07-mask-symlink-targets.md diff --git a/cli/apply_files.go b/cli/apply_files.go index d8b9672..8669ab8 100644 --- a/cli/apply_files.go +++ b/cli/apply_files.go @@ -41,7 +41,7 @@ func copyFile(srcRoot, dstRoot, rel string, dryRun bool, out io.Writer, maskPath return err } if dryRun { - _, err := fmt.Fprintf(out, "symlink %s -> %s (%s)\n", maskPathForDryRun(srcPath, maskPaths), maskPathForDryRun(dstPath, maskPaths), target) + _, err := fmt.Fprintf(out, "symlink %s -> %s (%s)\n", maskPathForDryRun(srcPath, maskPaths), maskPathForDryRun(dstPath, maskPaths), maskPathForDryRun(target, maskPaths)) return err } if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil { diff --git a/docs/plans/jobs/2026-02-07-mask-symlink-targets.md b/docs/plans/jobs/2026-02-07-mask-symlink-targets.md new file mode 100644 index 0000000..e6d4a5b --- /dev/null +++ b/docs/plans/jobs/2026-02-07-mask-symlink-targets.md @@ -0,0 +1,15 @@ +--- +title: "Mask symlink targets in dry-run output" +created-date: 2026-02-07 +status: completed +agent: codex +--- + +## Summary +- masked symlink targets in dry-run output when sensitive path masking is enabled + +## Rationale +- prevent leaking sensitive symlink targets in logs while `--mask-sensitive-paths` is active + +## Result +- dry-run symlink output now uses the same masking helper for the target path From 07d5eb45ddce899b7476945f3b51e9297af32513 Mon Sep 17 00:00:00 2001 From: nakolus Date: Sat, 7 Feb 2026 20:50:24 +0800 Subject: [PATCH 15/15] version update --- cli/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/root.go b/cli/root.go index 95f79d2..4bdffe9 100644 --- a/cli/root.go +++ b/cli/root.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -var Version = "0.1.2-alpha.3" +var Version = "0.1.2" var ( errCanceled = errors.New("git worktree task process canceled")