Skip to content

Commit 30b6899

Browse files
authored
feat: add machine-readable worktree creation for agents (#192)
Adds stable porcelain output and hook disposition reporting for shell-native agent integrations, with tests and documentation.
1 parent 214e49f commit 30b6899

13 files changed

Lines changed: 294 additions & 5 deletions

File tree

.github/instructions/testing.instructions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ Run after core or adapter changes; all manual (no automated tests).
2323
# New branch creation
2424
./bin/gtr new brand-new-feature # creates branch + worktree
2525

26+
# Machine-readable creation (stdout: path, branch, hook_status records)
27+
./bin/gtr new agent-feature --from HEAD --no-fetch --porcelain
28+
2629
# Force multiple worktrees same branch
2730
./bin/gtr new test-feature --force --name backend # test-feature-backend
2831

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

9296
## When Adding Features

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- `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.
12+
- Agent integration guidance documents the machine-readable contract and recommended trust and cleanup boundaries.
13+
914
## [2.9.0] - 2026-08-12
1015

1116
### Added

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- [Features](#features)
1818
- [Requirements](#requirements)
1919
- [Commands](#commands)
20+
- [AI Agent Usage](#ai-agent-usage)
2021
- [Configuration](#configuration)
2122
- [Shell Completions](#shell-completions-optional)
2223
- [Platform Support](#platform-support)
@@ -169,6 +170,7 @@ git gtr new feature/auth
169170
git gtr new feature/implement-user-authentication-with-oauth2-integration --folder auth # Custom folder name
170171
git gtr new feature-auth --name backend --force # Same branch, custom name
171172
git gtr new my-feature --name descriptive-variant # Optional: custom name without --force
173+
git gtr new agent-task --porcelain # Stable output for automation
172174
```
173175

174176
**Options:**
@@ -186,6 +188,13 @@ git gtr new my-feature --name descriptive-variant
186188
- `--editor`, `-e`: Open in editor after creation
187189
- `--ai`, `-a`: Start AI tool after creation
188190
- `--yes`: Non-interactive mode
191+
- `--porcelain`: Emit stable `key<TAB>value` records (implies `--yes`; incompatible with `--editor` and `--ai`)
192+
193+
On success, `--porcelain` writes exactly three records to stdout: `path`,
194+
`branch`, and `hook_status`. Progress messages and hook output are written to
195+
stderr. If a post-create hook fails, the command exits non-zero and emits no
196+
success records. See [AI agent usage](docs/agent-usage.md) for the format and
197+
recommended safety boundaries.
189198

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

@@ -496,6 +505,15 @@ Requires Git 2.17+ and Bash 3.2+.
496505

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

508+
## AI Agent Usage
509+
510+
Shell-capable coding agents can use `git gtr` directly; a separate MCP server is
511+
not required. Use `git gtr new <branch> --porcelain` to create a worktree and
512+
reliably capture its path, then run the agent inside that directory.
513+
514+
See [docs/agent-usage.md](docs/agent-usage.md) for a copy-paste `AGENTS.md`
515+
policy, output contract, and safe lifecycle examples.
516+
499517
## Advanced Usage
500518

501519
For advanced workflows including:

completions/_git-gtr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ _git-gtr() {
7474
'--name[Custom folder name suffix]:name:' \
7575
'--folder[Custom folder name (replaces default)]:folder:' \
7676
'--yes[Non-interactive mode]' \
77+
'--porcelain[Machine-readable output]' \
7778
'--editor[Open in editor after creation]' \
7879
'-e[Open in editor after creation]' \
7980
'--ai[Start AI tool after creation]' \

completions/git-gtr.fish

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ complete -c git -n '__fish_git_gtr_using_command new' -l force -d 'Allow same br
7373
complete -c git -n '__fish_git_gtr_using_command new' -l name -d 'Custom folder name suffix' -r
7474
complete -c git -n '__fish_git_gtr_using_command new' -l folder -d 'Custom folder name (replaces default)' -r
7575
complete -c git -n '__fish_git_gtr_using_command new' -l yes -d 'Non-interactive mode'
76+
complete -c git -n '__fish_git_gtr_using_command new' -l porcelain -d 'Machine-readable output'
7677
complete -c git -n '__fish_git_gtr_using_command new' -s e -l editor -d 'Open in editor after creation'
7778
complete -c git -n '__fish_git_gtr_using_command new' -s a -l ai -d 'Start AI tool after creation'
7879

completions/gtr.bash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ _git_gtr() {
9999
new)
100100
# Complete flags
101101
if [[ "$cur" == -* ]]; then
102-
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"))
102+
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"))
103103
elif [ "$prev" = "--track" ]; then
104104
COMPREPLY=($(compgen -W "auto remote local none" -- "$cur"))
105105
fi

docs/agent-usage.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# AI Agent Usage
2+
3+
Shell-capable coding agents can call `git gtr` directly. A separate MCP server
4+
is not necessary for creating, locating, using, and removing worktrees.
5+
6+
## Machine-readable creation
7+
8+
Create a worktree with stable output:
9+
10+
```bash
11+
git gtr new agent/my-task --porcelain
12+
```
13+
14+
The command writes exactly three tab-separated records to stdout:
15+
16+
```text
17+
path /absolute/path/to/repo-worktrees/agent-my-task
18+
branch agent/my-task
19+
hook_status ran
20+
```
21+
22+
Values escape backslashes, tabs, and newlines as `\\`, `\t`, and `\n`.
23+
Progress messages, warnings, and hook output go to stderr. `--porcelain` implies
24+
`--yes` and cannot be combined with `--editor` or `--ai`.
25+
26+
`hook_status` is one of:
27+
28+
| Value | Meaning |
29+
| --- | --- |
30+
| `disabled` | Hooks were disabled with `--no-hooks`. |
31+
| `none` | No post-create hooks were configured. |
32+
| `ran` | All configured post-create hooks were trusted and ran successfully. |
33+
| `skipped-untrusted` | Only untrusted `.gtrconfig` hooks were configured, so none ran. |
34+
| `partial` | Trusted hooks ran, while untrusted `.gtrconfig` hooks were skipped. |
35+
36+
A non-zero exit means creation or a post-create hook failed. No success records
37+
are emitted in that case.
38+
39+
## Recommended agent lifecycle
40+
41+
1. Inspect existing worktrees with `git gtr list --porcelain`.
42+
2. Create an isolated worktree with `git gtr new <branch> --porcelain`.
43+
3. Parse the `path` record and perform all task work inside that directory.
44+
4. Before handing off, run `git status --short --branch` in the worktree and
45+
report its branch, changes, and validation results.
46+
5. Remove the worktree only when the user explicitly asks for cleanup.
47+
48+
Agents should not run `git gtr trust`. Trusting committed `.gtrconfig` commands
49+
authorizes code execution and requires human review. Agents should also avoid
50+
`git gtr rm --force`, `--delete-branch`, and `git gtr clean` unless the user has
51+
explicitly authorized the destructive scope.
52+
53+
## `AGENTS.md` example
54+
55+
```markdown
56+
## Worktree policy
57+
58+
- Use `git gtr list --porcelain` to inspect worktrees.
59+
- For implementation tasks, create an isolated worktree with
60+
`git gtr new <branch> --porcelain` and work only in the returned `path`.
61+
- Treat a non-zero exit as failure; do not infer success from human-readable logs.
62+
- If `hook_status` is `skipped-untrusted` or `partial`, report it. Never run
63+
`git gtr trust` on the user's behalf.
64+
- Do not remove worktrees, force cleanup, or delete branches without explicit
65+
user authorization.
66+
```

lib/commands/create.sh

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,31 @@ _create_resolve_from_ref() {
8484

8585
printf "%s" "$from_ref"
8686
}
87+
88+
# Print a stable, escaped record stream for scripting and agent integrations.
89+
# Format: key<tab>value, one record per line.
90+
_create_print_porcelain() {
91+
local worktree_path="$1" branch_name="$2" hook_status="$3"
92+
printf "path\t%s\n" "$(_tsv_escape_field "$worktree_path")"
93+
printf "branch\t%s\n" "$(_tsv_escape_field "$branch_name")"
94+
printf "hook_status\t%s\n" "$hook_status"
95+
}
96+
97+
# Detect machine mode before parsing so all incidental stdout, including hook
98+
# output, can be redirected away from the stable record stream.
99+
_create_wants_porcelain() {
100+
local arg
101+
for arg in "$@"; do
102+
case "$arg" in
103+
--porcelain) return 0 ;;
104+
--) return 1 ;;
105+
esac
106+
done
107+
return 1
108+
}
109+
87110
# shellcheck disable=SC2154 # _arg_* _pa_* set by parse_args, _ctx_* set by resolve_*
88-
cmd_create() {
111+
_cmd_create_impl() {
89112
local _spec
90113
_spec="--from: value
91114
--from-current
@@ -100,6 +123,7 @@ cmd_create() {
100123
--force
101124
--name: value
102125
--folder: value
126+
--porcelain
103127
--editor|-e
104128
--ai|-a"
105129
parse_args "$_spec" "$@"
@@ -118,9 +142,18 @@ cmd_create() {
118142
local force="${_arg_force:-0}"
119143
local custom_name="${_arg_name:-}"
120144
local folder_override="${_arg_folder:-}"
145+
local porcelain="${_arg_porcelain:-0}"
121146
local open_editor="${_arg_editor:-0}"
122147
local start_ai="${_arg_ai:-0}"
123148

149+
if [ "$porcelain" -eq 1 ]; then
150+
yes_mode=1
151+
if [ "$open_editor" -eq 1 ] || [ "$start_ai" -eq 1 ]; then
152+
log_error "--porcelain cannot be combined with --editor or --ai"
153+
exit 1
154+
fi
155+
fi
156+
124157
# Validate flag combinations
125158
if [ -n "$folder_override" ] && [ -n "$custom_name" ]; then
126159
log_error "--folder and --name cannot be used together"
@@ -232,20 +265,37 @@ cmd_create() {
232265
fi
233266

234267
# Run post-create hooks (unless --no-hooks)
268+
local hook_status="disabled"
235269
if [ "$skip_hooks" -eq 0 ]; then
236-
run_hooks_in postCreate "$worktree_path" \
270+
hook_status=$(_hooks_phase_status postCreate)
271+
if ! run_hooks_in postCreate "$worktree_path" \
237272
REPO_ROOT="$repo_root" \
238273
WORKTREE_PATH="$worktree_path" \
239-
BRANCH="$branch_name"
274+
BRANCH="$branch_name"; then
275+
exit 1
276+
fi
240277
fi
241278

242279
echo ""
243280
log_info "Worktree created: $worktree_path"
244281

282+
if [ "$porcelain" -eq 1 ]; then
283+
_create_print_porcelain "$worktree_path" "$branch_name" "$hook_status" >&3
284+
return 0
285+
fi
286+
245287
# Auto-launch editor/AI or show next steps
246288
[ "$open_editor" -eq 1 ] && { _auto_launch_editor "$worktree_path" || true; }
247289
[ "$start_ai" -eq 1 ] && { _auto_launch_ai "$worktree_path" "$repo_root" "$branch_name" || true; }
248290
if [ "$open_editor" -eq 0 ] && [ "$start_ai" -eq 0 ]; then
249291
_post_create_next_steps "$branch_name" "$folder_name" "$folder_override" "$repo_root" "$base_dir" "$prefix"
250292
fi
251293
}
294+
295+
cmd_create() {
296+
if _create_wants_porcelain "$@"; then
297+
_cmd_create_impl "$@" 3>&1 1>&2
298+
else
299+
_cmd_create_impl "$@"
300+
fi
301+
}

lib/commands/help.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,16 @@ Options:
3030
--name <suffix> Custom folder name suffix (appended after branch name)
3131
--folder <name> Custom folder name (replaces default entirely)
3232
--yes Non-interactive mode (skip prompts)
33+
--porcelain Machine-readable key<TAB>value output (implies --yes)
34+
Cannot be combined with --editor or --ai
3335
-e, --editor Open in editor after creation
3436
-a, --ai Start AI tool after creation
3537
3638
Examples:
3739
git gtr new feature/user-auth # Folder: feature-user-auth
3840
git gtr new hotfix --from v2.0.0 # Branch from tag
3941
git gtr new my-feature --from-current # Branch from current HEAD
42+
git gtr new agent-task --porcelain # Stable output for automation
4043
git gtr new feature -e -a # Create, open editor + AI
4144
git gtr new feature --force --name backend # Second worktree for same branch
4245
git gtr new feature --folder my-dir # Custom folder name
@@ -561,6 +564,7 @@ CORE COMMANDS (daily workflow):
561564
--name <suffix>: custom folder name suffix (e.g., backend, frontend)
562565
--folder <name>: custom folder name (replaces default, useful for long branches)
563566
--yes: non-interactive mode
567+
--porcelain: machine-readable key<TAB>value output (implies --yes)
564568
-e, --editor: open in editor after creation
565569
-a, --ai: start AI tool after creation
566570

lib/hooks.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,40 @@ _hooks_get_trusted() {
212212
} | awk '!seen[$0]++'
213213
}
214214

215+
# Report the outcome a successful hook phase will have without executing it.
216+
# Usage: _hooks_phase_status <phase>
217+
# Prints one of: none, ran, skipped-untrusted, partial
218+
_hooks_phase_status() {
219+
local phase="$1"
220+
local git_hooks="" config_file="" file_hooks=""
221+
local has_runnable=0 has_untrusted=0
222+
223+
git_hooks=$(git config --get-all "gtr.hook.$phase" 2>/dev/null) || true
224+
[ -n "$git_hooks" ] && has_runnable=1
225+
226+
config_file=$(_gtrconfig_path) || true
227+
if [ -n "$config_file" ] && [ -f "$config_file" ]; then
228+
file_hooks=$(git config -f "$config_file" --get-all "hooks.$phase" 2>/dev/null) || true
229+
if [ -n "$file_hooks" ]; then
230+
if _hooks_are_trusted "$config_file"; then
231+
has_runnable=1
232+
else
233+
has_untrusted=1
234+
fi
235+
fi
236+
fi
237+
238+
if [ "$has_runnable" -eq 1 ] && [ "$has_untrusted" -eq 1 ]; then
239+
printf "partial"
240+
elif [ "$has_runnable" -eq 1 ]; then
241+
printf "ran"
242+
elif [ "$has_untrusted" -eq 1 ]; then
243+
printf "skipped-untrusted"
244+
else
245+
printf "none"
246+
fi
247+
}
248+
215249
# Run hooks for a specific phase
216250
# Usage: run_hooks phase [env_vars...]
217251
# Example: run_hooks postCreate REPO_ROOT="$root" WORKTREE_PATH="$path"

0 commit comments

Comments
 (0)