Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/instructions/testing.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ Run after core or adapter changes; all manual (no automated tests).
# New branch creation
./bin/gtr new brand-new-feature # creates branch + worktree

# Machine-readable creation (stdout: path, branch, hook_status records)
./bin/gtr new agent-feature --from HEAD --no-fetch --porcelain

Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Force multiple worktrees same branch
./bin/gtr new test-feature --force --name backend # test-feature-backend

Expand Down Expand Up @@ -87,6 +90,7 @@ echo "DEBUG worktree_path=$worktree_path" >&2 # variable inspection
- All commands exit 0 (except intentional failures) and produce expected side-effects.
- No unquoted path errors; spaces handled.
- Hooks run only once per creation/removal.
- `new --porcelain` emits only stable records on stdout; progress and hook output use stderr.
- `list --porcelain` stable for scripting.

## When Adding Features
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this

## [Unreleased]

### Added

- `git gtr new --porcelain` emits stable worktree path, branch, and hook-status records for shell automation and coding agents, with incidental output isolated on stderr.
- Agent integration guidance documents the machine-readable contract and recommended trust and cleanup boundaries.

## [2.9.0] - 2026-08-12

### Added
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [Features](#features)
- [Requirements](#requirements)
- [Commands](#commands)
- [AI Agent Usage](#ai-agent-usage)
- [Configuration](#configuration)
- [Shell Completions](#shell-completions-optional)
- [Platform Support](#platform-support)
Expand Down Expand Up @@ -169,6 +170,7 @@ git gtr new feature/auth
git gtr new feature/implement-user-authentication-with-oauth2-integration --folder auth # Custom folder name
git gtr new feature-auth --name backend --force # Same branch, custom name
git gtr new my-feature --name descriptive-variant # Optional: custom name without --force
git gtr new agent-task --porcelain # Stable output for automation
```

**Options:**
Expand All @@ -186,6 +188,13 @@ git gtr new my-feature --name descriptive-variant
- `--editor`, `-e`: Open in editor after creation
- `--ai`, `-a`: Start AI tool after creation
- `--yes`: Non-interactive mode
- `--porcelain`: Emit stable `key<TAB>value` records (implies `--yes`; incompatible with `--editor` and `--ai`)

On success, `--porcelain` writes exactly three records to stdout: `path`,
`branch`, and `hook_status`. Progress messages and hook output are written to
stderr. If a post-create hook fails, the command exits non-zero and emits no
success records. See [AI agent usage](docs/agent-usage.md) for the format and
recommended safety boundaries.

### `git gtr pr <number|url|branch> [options]`

Expand Down Expand Up @@ -496,6 +505,15 @@ Requires Git 2.17+ and Bash 3.2+.

> For troubleshooting, platform-specific notes, and architecture details, see [docs/troubleshooting.md](docs/troubleshooting.md)

## AI Agent Usage

Shell-capable coding agents can use `git gtr` directly; a separate MCP server is
not required. Use `git gtr new <branch> --porcelain` to create a worktree and
reliably capture its path, then run the agent inside that directory.

See [docs/agent-usage.md](docs/agent-usage.md) for a copy-paste `AGENTS.md`
policy, output contract, and safe lifecycle examples.

## Advanced Usage

For advanced workflows including:
Expand Down
1 change: 1 addition & 0 deletions completions/_git-gtr
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ _git-gtr() {
'--name[Custom folder name suffix]:name:' \
'--folder[Custom folder name (replaces default)]:folder:' \
'--yes[Non-interactive mode]' \
'--porcelain[Machine-readable output]' \
'--editor[Open in editor after creation]' \
'-e[Open in editor after creation]' \
'--ai[Start AI tool after creation]' \
Expand Down
1 change: 1 addition & 0 deletions completions/git-gtr.fish
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ complete -c git -n '__fish_git_gtr_using_command new' -l force -d 'Allow same br
complete -c git -n '__fish_git_gtr_using_command new' -l name -d 'Custom folder name suffix' -r
complete -c git -n '__fish_git_gtr_using_command new' -l folder -d 'Custom folder name (replaces default)' -r
complete -c git -n '__fish_git_gtr_using_command new' -l yes -d 'Non-interactive mode'
complete -c git -n '__fish_git_gtr_using_command new' -l porcelain -d 'Machine-readable output'
complete -c git -n '__fish_git_gtr_using_command new' -s e -l editor -d 'Open in editor after creation'
complete -c git -n '__fish_git_gtr_using_command new' -s a -l ai -d 'Start AI tool after creation'

Expand Down
2 changes: 1 addition & 1 deletion completions/gtr.bash
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ _git_gtr() {
new)
# Complete flags
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "--from --from-current --remote --track --no-copy --no-fetch --no-hooks --sparse --no-sparse --force --name --folder --yes --editor -e --ai -a" -- "$cur"))
COMPREPLY=($(compgen -W "--from --from-current --remote --track --no-copy --no-fetch --no-hooks --sparse --no-sparse --force --name --folder --yes --porcelain --editor -e --ai -a" -- "$cur"))
elif [ "$prev" = "--track" ]; then
COMPREPLY=($(compgen -W "auto remote local none" -- "$cur"))
fi
Expand Down
66 changes: 66 additions & 0 deletions docs/agent-usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# AI Agent Usage

Shell-capable coding agents can call `git gtr` directly. A separate MCP server
is not necessary for creating, locating, using, and removing worktrees.

## Machine-readable creation

Create a worktree with stable output:

```bash
git gtr new agent/my-task --porcelain
```

The command writes exactly three tab-separated records to stdout:

```text
path /absolute/path/to/repo-worktrees/agent-my-task
branch agent/my-task
hook_status ran
```

Values escape backslashes, tabs, and newlines as `\\`, `\t`, and `\n`.
Progress messages, warnings, and hook output go to stderr. `--porcelain` implies
`--yes` and cannot be combined with `--editor` or `--ai`.

`hook_status` is one of:

| Value | Meaning |
| --- | --- |
| `disabled` | Hooks were disabled with `--no-hooks`. |
| `none` | No post-create hooks were configured. |
| `ran` | All configured post-create hooks were trusted and ran successfully. |
| `skipped-untrusted` | Only untrusted `.gtrconfig` hooks were configured, so none ran. |
| `partial` | Trusted hooks ran, while untrusted `.gtrconfig` hooks were skipped. |

A non-zero exit means creation or a post-create hook failed. No success records
are emitted in that case.

## Recommended agent lifecycle

1. Inspect existing worktrees with `git gtr list --porcelain`.
2. Create an isolated worktree with `git gtr new <branch> --porcelain`.
3. Parse the `path` record and perform all task work inside that directory.
4. Before handing off, run `git status --short --branch` in the worktree and
report its branch, changes, and validation results.
5. Remove the worktree only when the user explicitly asks for cleanup.

Agents should not run `git gtr trust`. Trusting committed `.gtrconfig` commands
authorizes code execution and requires human review. Agents should also avoid
`git gtr rm --force`, `--delete-branch`, and `git gtr clean` unless the user has
explicitly authorized the destructive scope.

## `AGENTS.md` example

```markdown
## Worktree policy

- Use `git gtr list --porcelain` to inspect worktrees.
- For implementation tasks, create an isolated worktree with
`git gtr new <branch> --porcelain` and work only in the returned `path`.
- Treat a non-zero exit as failure; do not infer success from human-readable logs.
- If `hook_status` is `skipped-untrusted` or `partial`, report it. Never run
`git gtr trust` on the user's behalf.
- Do not remove worktrees, force cleanup, or delete branches without explicit
user authorization.
```
56 changes: 53 additions & 3 deletions lib/commands/create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,31 @@ _create_resolve_from_ref() {

printf "%s" "$from_ref"
}

# Print a stable, escaped record stream for scripting and agent integrations.
# Format: key<tab>value, one record per line.
_create_print_porcelain() {
local worktree_path="$1" branch_name="$2" hook_status="$3"
printf "path\t%s\n" "$(_tsv_escape_field "$worktree_path")"
printf "branch\t%s\n" "$(_tsv_escape_field "$branch_name")"
printf "hook_status\t%s\n" "$hook_status"
}

# Detect machine mode before parsing so all incidental stdout, including hook
# output, can be redirected away from the stable record stream.
_create_wants_porcelain() {
local arg
for arg in "$@"; do
case "$arg" in
--porcelain) return 0 ;;
--) return 1 ;;
esac
done
return 1
}

# shellcheck disable=SC2154 # _arg_* _pa_* set by parse_args, _ctx_* set by resolve_*
cmd_create() {
_cmd_create_impl() {
local _spec
_spec="--from: value
--from-current
Expand All @@ -100,6 +123,7 @@ cmd_create() {
--force
--name: value
--folder: value
--porcelain
--editor|-e
--ai|-a"
parse_args "$_spec" "$@"
Expand All @@ -118,9 +142,18 @@ cmd_create() {
local force="${_arg_force:-0}"
local custom_name="${_arg_name:-}"
local folder_override="${_arg_folder:-}"
local porcelain="${_arg_porcelain:-0}"
local open_editor="${_arg_editor:-0}"
local start_ai="${_arg_ai:-0}"

if [ "$porcelain" -eq 1 ]; then
yes_mode=1
if [ "$open_editor" -eq 1 ] || [ "$start_ai" -eq 1 ]; then
log_error "--porcelain cannot be combined with --editor or --ai"
exit 1
fi
fi

# Validate flag combinations
if [ -n "$folder_override" ] && [ -n "$custom_name" ]; then
log_error "--folder and --name cannot be used together"
Expand Down Expand Up @@ -232,20 +265,37 @@ cmd_create() {
fi

# Run post-create hooks (unless --no-hooks)
local hook_status="disabled"
if [ "$skip_hooks" -eq 0 ]; then
run_hooks_in postCreate "$worktree_path" \
hook_status=$(_hooks_phase_status postCreate)
if ! run_hooks_in postCreate "$worktree_path" \
REPO_ROOT="$repo_root" \
WORKTREE_PATH="$worktree_path" \
BRANCH="$branch_name"
BRANCH="$branch_name"; then
exit 1
fi
fi

echo ""
log_info "Worktree created: $worktree_path"

if [ "$porcelain" -eq 1 ]; then
_create_print_porcelain "$worktree_path" "$branch_name" "$hook_status" >&3
return 0
fi

# Auto-launch editor/AI or show next steps
[ "$open_editor" -eq 1 ] && { _auto_launch_editor "$worktree_path" || true; }
[ "$start_ai" -eq 1 ] && { _auto_launch_ai "$worktree_path" "$repo_root" "$branch_name" || true; }
if [ "$open_editor" -eq 0 ] && [ "$start_ai" -eq 0 ]; then
_post_create_next_steps "$branch_name" "$folder_name" "$folder_override" "$repo_root" "$base_dir" "$prefix"
fi
}

cmd_create() {
if _create_wants_porcelain "$@"; then
_cmd_create_impl "$@" 3>&1 1>&2
else
_cmd_create_impl "$@"
fi
}
4 changes: 4 additions & 0 deletions lib/commands/help.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@ Options:
--name <suffix> Custom folder name suffix (appended after branch name)
--folder <name> Custom folder name (replaces default entirely)
--yes Non-interactive mode (skip prompts)
--porcelain Machine-readable key<TAB>value output (implies --yes)
Cannot be combined with --editor or --ai
-e, --editor Open in editor after creation
-a, --ai Start AI tool after creation

Examples:
git gtr new feature/user-auth # Folder: feature-user-auth
git gtr new hotfix --from v2.0.0 # Branch from tag
git gtr new my-feature --from-current # Branch from current HEAD
git gtr new agent-task --porcelain # Stable output for automation
git gtr new feature -e -a # Create, open editor + AI
git gtr new feature --force --name backend # Second worktree for same branch
git gtr new feature --folder my-dir # Custom folder name
Expand Down Expand Up @@ -561,6 +564,7 @@ CORE COMMANDS (daily workflow):
--name <suffix>: custom folder name suffix (e.g., backend, frontend)
--folder <name>: custom folder name (replaces default, useful for long branches)
--yes: non-interactive mode
--porcelain: machine-readable key<TAB>value output (implies --yes)
-e, --editor: open in editor after creation
-a, --ai: start AI tool after creation

Expand Down
34 changes: 34 additions & 0 deletions lib/hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,40 @@ _hooks_get_trusted() {
} | awk '!seen[$0]++'
}

# Report the outcome a successful hook phase will have without executing it.
# Usage: _hooks_phase_status <phase>
# Prints one of: none, ran, skipped-untrusted, partial
_hooks_phase_status() {
local phase="$1"
local git_hooks="" config_file="" file_hooks=""
local has_runnable=0 has_untrusted=0

git_hooks=$(git config --get-all "gtr.hook.$phase" 2>/dev/null) || true
[ -n "$git_hooks" ] && has_runnable=1

config_file=$(_gtrconfig_path) || true
if [ -n "$config_file" ] && [ -f "$config_file" ]; then
file_hooks=$(git config -f "$config_file" --get-all "hooks.$phase" 2>/dev/null) || true
if [ -n "$file_hooks" ]; then
if _hooks_are_trusted "$config_file"; then
has_runnable=1
else
has_untrusted=1
fi
fi
fi

if [ "$has_runnable" -eq 1 ] && [ "$has_untrusted" -eq 1 ]; then
printf "partial"
elif [ "$has_runnable" -eq 1 ]; then
printf "ran"
elif [ "$has_untrusted" -eq 1 ]; then
printf "skipped-untrusted"
else
printf "none"
fi
}

# Run hooks for a specific phase
# Usage: run_hooks phase [env_vars...]
# Example: run_hooks postCreate REPO_ROOT="$root" WORKTREE_PATH="$path"
Expand Down
4 changes: 3 additions & 1 deletion scripts/generate-completions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ MIDDLE1
new)
# Complete flags
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "--from --from-current --remote --track --no-copy --no-fetch --no-hooks --sparse --no-sparse --force --name --folder --yes --editor -e --ai -a" -- "$cur"))
COMPREPLY=($(compgen -W "--from --from-current --remote --track --no-copy --no-fetch --no-hooks --sparse --no-sparse --force --name --folder --yes --porcelain --editor -e --ai -a" -- "$cur"))
elif [ "$prev" = "--track" ]; then
COMPREPLY=($(compgen -W "auto remote local none" -- "$cur"))
fi
Expand Down Expand Up @@ -338,6 +338,7 @@ _git-gtr() {
'--name[Custom folder name suffix]:name:' \
'--folder[Custom folder name (replaces default)]:folder:' \
'--yes[Non-interactive mode]' \
'--porcelain[Machine-readable output]' \
'--editor[Open in editor after creation]' \
'-e[Open in editor after creation]' \
'--ai[Start AI tool after creation]' \
Expand Down Expand Up @@ -582,6 +583,7 @@ complete -c git -n '__fish_git_gtr_using_command new' -l force -d 'Allow same br
complete -c git -n '__fish_git_gtr_using_command new' -l name -d 'Custom folder name suffix' -r
complete -c git -n '__fish_git_gtr_using_command new' -l folder -d 'Custom folder name (replaces default)' -r
complete -c git -n '__fish_git_gtr_using_command new' -l yes -d 'Non-interactive mode'
complete -c git -n '__fish_git_gtr_using_command new' -l porcelain -d 'Machine-readable output'
complete -c git -n '__fish_git_gtr_using_command new' -s e -l editor -d 'Open in editor after creation'
complete -c git -n '__fish_git_gtr_using_command new' -s a -l ai -d 'Start AI tool after creation'

Expand Down
Loading
Loading