git-stint 0.2.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,87 +1,109 @@
1
1
  # git-stint
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/git-stint)](https://www.npmjs.com/package/git-stint)
4
+ [![CI](https://github.com/rchaz/git-stint/actions/workflows/test.yml/badge.svg)](https://github.com/rchaz/git-stint/actions)
3
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
6
  [![Node.js: 20+](https://img.shields.io/badge/Node.js-20%2B-green.svg)](https://nodejs.org)
5
7
 
6
- Session-scoped change tracking for AI coding agents. Each session gets a real git branch and worktree — isolated by default, mergeable at the end.
8
+ **Built for AI agents to multitask safely.**
7
9
 
8
- Built to replace GitButler for AI-agent workflows. No virtual branches, no custom merge engine, no state corruption. Just git.
10
+ Run multiple AI coding agents in parallel each one gets its own branch, its own worktree, and its own lifecycle. git-stint handles the branching, tracking, checkpointing, and cleanup autonomously. You focus on the work. The grunt work is managed under the hood.
9
11
 
10
- ## Why
12
+ No cloud VMs. No Docker containers. No desktop app. No new VCS to learn. Just git — with session management that runs itself.
11
13
 
12
- AI coding agents (Claude Code, Cursor, Copilot) edit files but have no clean way to:
14
+ ```
15
+ Agent A fixes auth ──→ .stint/auth-fix/ → builds, tests, PRs independently
16
+ Agent B adds search ──→ .stint/add-search/ → builds, tests, PRs independently
17
+ Agent C writes docs ──→ .stint/update-docs/ → builds, tests, PRs independently
18
+ You keep working ──→ main branch, untouched
19
+ ```
13
20
 
14
- 1. **Track what they changed** — separate agent changes from human changes
15
- 2. **Isolate sessions** — two parallel agents editing the same repo shouldn't conflict
16
- 3. **Produce clean commits** — agent work should result in reviewable, mergeable PRs
17
- 4. **Test in isolation** — verify one session's changes without interference
21
+ ## The Problem
18
22
 
19
- git-stint solves this with ~2,000 lines of TypeScript + bash on top of standard git primitives.
23
+ When an AI coding agent works on your repo, you lose track of what it changed, your main branch accumulates half-finished work, and running two agents at once creates conflicts. There's no clean way to:
20
24
 
21
- ## Prerequisites
25
+ - **Separate agent changes from human changes** — who wrote what?
26
+ - **Run agents in parallel** — two agents on the same branch will collide
27
+ - **Get clean, reviewable PRs** — agent work is often a stream of small edits
28
+ - **Undo an agent's work** — reverting scattered changes across files is painful
29
+ - **Prevent accidental damage** — agents write to `main` unless something stops them
22
30
 
23
- - [Node.js](https://nodejs.org) 20+
24
- - [git](https://git-scm.com) 2.20+ (worktree support)
25
- - [`gh` CLI](https://cli.github.com) (optional, for PR creation)
31
+ ## How git-stint Solves This
26
32
 
27
- ## Install
33
+ Each agent session gets a **real git branch** and a **worktree** — not virtual branches, not overlays, not a custom VCS. Standard git, fully compatible with every tool you already use.
28
34
 
29
- ### With Claude Code (recommended)
35
+ Every step of the session lifecycle is autonomous:
30
36
 
31
- Tell Claude Code:
37
+ | What happens | What git-stint does | You do anything? |
38
+ |---|---|---|
39
+ | Agent writes its first file | Auto-creates a session branch + worktree | No |
40
+ | Agent keeps writing files | Hooks track every change automatically | No |
41
+ | Agent builds or runs tests | Runs in the isolated worktree — can't break main | No |
42
+ | Conversation ends unexpectedly | Auto-commits a WIP checkpoint — nothing lost | No |
43
+ | Work is ready to ship | `git stint squash` + `git stint pr` = clean PR | One command |
44
+ | Session is done | `git stint end` cleans up branch, worktree, and remote | One command |
32
45
 
33
- > Install git-stint globally (`npm install -g git-stint`), set up hooks for this repo, and create a .stint.json
46
+ ## Key Features
34
47
 
35
- Claude Code will:
36
- 1. Run `npm install -g git-stint`
37
- 2. Run `git stint install-hooks` (writes to `.claude/settings.json`)
38
- 3. Create a `.stint.json` with your preferred `main_branch_policy`
48
+ **Fully autonomous** — Install once, then forget about it. Hooks intercept file writes, create sessions, track changes, and checkpoint work without any manual intervention. The agent works on its stint. git-stint manages everything else.
39
49
 
40
- ### Manual install
50
+ **Parallel agents, zero conflicts** — Each agent instance gets its own session via process ID. Three Claude Code windows? Three isolated branches. Each can build, test, commit, and create PRs independently — no coordination required between them.
41
51
 
42
- ```bash
43
- # 1. Install
44
- npm install -g git-stint # from npm
45
- # — or from source —
46
- git clone https://github.com/rchaz/git-stint.git
47
- cd git-stint && npm install && npm run build && npm link
52
+ **Safe builds and tests** — Every session runs in its own worktree. `npm test`, `cargo build`, `go test` — all execute against that session's files only. One agent's broken build can't block another. `git stint test --combine A B` lets you verify multiple sessions work together before merging any of them.
48
53
 
49
- # 2. Set up hooks in your project
50
- cd /path/to/your/repo
51
- git stint install-hooks
54
+ **No work lost — ever** — Conversation timeout? Crash? User closes the window? The stop hook auto-commits pending changes as a WIP checkpoint. Come back later, the work is still there.
52
55
 
53
- # 3. Configure (optional)
54
- cat > .stint.json << 'EOF'
55
- {
56
- "shared_dirs": [],
57
- "main_branch_policy": "prompt"
58
- }
59
- EOF
60
- ```
56
+ **Clean PRs from messy work** — An agent's stream of incremental edits becomes a single, reviewable commit with `git stint squash`. Create a PR with auto-generated descriptions in one command.
57
+
58
+ **Conflict detection before merge** — `git stint conflicts` checks file overlap across all active sessions. Know which agents are touching the same files *before* you try to merge.
59
+
60
+ **Shared directories** — Large caches (`node_modules`, `venv`, build outputs) are symlinked into worktrees instead of duplicated. Agents get fast startup, you save disk space.
61
+
62
+ **Zero dependencies** — Pure Node.js built-ins. No native modules, no runtime deps, no surprises. Installs in seconds.
63
+
64
+ **Just git underneath** — Real branches, real worktrees, real commits. `git log`, `git diff`, lazygit, VS Code, GitHub — every tool you already use works because there's nothing custom underneath.
61
65
 
62
66
  ## Quick Start
63
67
 
64
68
  ```bash
65
- # Start a session (creates branch + worktree)
69
+ # Install
70
+ npm install -g git-stint
71
+
72
+ # Set up hooks in your repo
73
+ cd /path/to/your/repo
74
+ git stint install-hooks
75
+
76
+ # Start a session
66
77
  git stint start auth-fix
67
78
  cd .stint/auth-fix/
68
79
 
69
- # Work normally — make changes, edit files...
70
-
71
- # Commit progress (advances baseline)
80
+ # Make changes, then commit
72
81
  git stint commit -m "Fix token refresh logic"
73
82
 
74
- # More changes...
75
- git stint commit -m "Add refresh token tests"
76
-
77
- # Squash into a single clean commit
83
+ # Squash into a clean commit and create a PR
78
84
  git stint squash -m "Fix auth token refresh"
79
-
80
- # Create PR and clean up
81
85
  git stint pr --title "Fix auth bug"
86
+
87
+ # Clean up
82
88
  git stint end
83
89
  ```
84
90
 
91
+ ### With Claude Code
92
+
93
+ Tell Claude Code:
94
+
95
+ > Install git-stint globally (`npm install -g git-stint`), set up hooks for this repo, and create a .stint.json
96
+
97
+ Or manually:
98
+
99
+ ```bash
100
+ npm install -g git-stint
101
+ cd /path/to/your/repo
102
+ git stint install-hooks # Writes to .claude/settings.json
103
+ ```
104
+
105
+ Once hooks are installed, Claude Code automatically works in sessions. When `main_branch_policy` is `"block"`, the first file write auto-creates a session and redirects the agent — zero manual steps.
106
+
85
107
  ## Commands
86
108
 
87
109
  | Command | Description |
@@ -89,45 +111,44 @@ git stint end
89
111
  | `git stint start [name]` | Create a new session (branch + worktree) |
90
112
  | `git stint list` | List all active sessions |
91
113
  | `git stint status` | Show current session state |
92
- | `git stint track <file...>` | Add files to the pending list |
93
114
  | `git stint diff` | Show uncommitted changes in worktree |
94
115
  | `git stint commit -m "msg"` | Commit changes, advance baseline |
95
116
  | `git stint log` | Show session commit history |
96
117
  | `git stint squash -m "msg"` | Collapse all commits into one |
97
- | `git stint merge` | Merge session into current branch (no PR) |
118
+ | `git stint merge` | Merge session into current branch |
98
119
  | `git stint pr [--title "..."]` | Push branch and create GitHub PR |
99
120
  | `git stint end` | Finalize session, clean up everything |
100
121
  | `git stint abort` | Discard session — delete all changes |
101
122
  | `git stint undo` | Revert last commit, changes become pending |
123
+ | `git stint which [--worktree]` | Print resolved session name (or worktree path) |
102
124
  | `git stint conflicts` | Check file overlap with other sessions |
103
125
  | `git stint test [-- cmd]` | Run tests in the session worktree |
104
126
  | `git stint test --combine A B` | Test multiple sessions merged together |
127
+ | `git stint track <file...>` | Add files to the pending list |
105
128
  | `git stint prune` | Clean up orphaned worktrees/branches |
106
- | `git stint allow-main [--client-id <PID>]` | Allow writes to main branch (scoped to one process/session) |
129
+ | `git stint allow-main` | Allow writes to main (scoped to one process) |
107
130
  | `git stint install-hooks` | Install Claude Code hooks |
108
131
  | `git stint uninstall-hooks` | Remove Claude Code hooks |
109
132
 
110
133
  ### Options
111
134
 
112
- - `--session <name>` Specify which session (auto-detected from CWD)
113
- - `--client-id <id>` — Client identifier (used by hooks). For `start`, tags the session. For `allow-main`, scopes the flag to that client.
114
- - `--adopt` / `--no-adopt` Override `adopt_changes` config for this start
115
- - `-m "message"` Commit or squash message
116
- - `--title "title"` PR title
117
- - `--version` Show version number
135
+ | Flag | Description |
136
+ |------|-------------|
137
+ | `--session <name>` | Target a specific session (auto-detected from CWD) |
138
+ | `--client-id <id>` | Client identifier for multi-instance affinity |
139
+ | `--adopt` / `--no-adopt` | Control whether uncommitted changes carry into a new session |
140
+ | `-m "message"` | Commit or squash message |
141
+ | `--title "title"` | PR title |
142
+ | `--version` | Show version number |
118
143
 
119
144
  ## Configuration — `.stint.json`
120
145
 
121
- Create a `.stint.json` file in your repo root to configure git-stint behavior:
146
+ Create a `.stint.json` in your repo root:
122
147
 
123
148
  ```json
124
149
  {
125
- "shared_dirs": [
126
- "backend/data",
127
- "backend/results",
128
- "backend/logs"
129
- ],
130
- "main_branch_policy": "prompt",
150
+ "shared_dirs": ["node_modules", ".venv", "dist"],
151
+ "main_branch_policy": "block",
131
152
  "force_cleanup": "prompt",
132
153
  "adopt_changes": "always"
133
154
  }
@@ -135,233 +156,105 @@ Create a `.stint.json` file in your repo root to configure git-stint behavior:
135
156
 
136
157
  | Field | Values | Default | Description |
137
158
  |-------|--------|---------|-------------|
138
- | `shared_dirs` | `string[]` | `[]` | Directories to symlink from worktree to main repo on `start`. Use for gitignored data dirs (caches, build outputs, logs) that shouldn't be duplicated per session. |
139
- | `main_branch_policy` | `"prompt"` / `"allow"` / `"block"` | `"prompt"` | What happens when writing to main with hooks enabled. `"block"` auto-creates a session. `"allow"` passes through. `"prompt"` blocks with instructions to run `git stint allow-main` or `git stint start`. |
140
- | `force_cleanup` | `"prompt"` / `"force"` / `"fail"` | `"prompt"` | What happens when non-force worktree removal fails. `"force"` retries with `--force`. `"fail"` throws an error. `"prompt"` retries with force (default, same as previous behavior). |
141
- | `adopt_changes` | `"always"` / `"never"` / `"prompt"` | `"always"` | What happens when `git stint start` is called with uncommitted changes on main. `"always"` stashes and moves them into the new worktree. `"never"` leaves them on main. `"prompt"` warns and suggests `--adopt` or `--no-adopt`. |
142
-
143
- ### Shared Directories
144
-
145
- When a worktree is created, gitignored directories (caches, build outputs, data) don't exist in it. Without `shared_dirs`, you'd need to manually symlink or recreate them.
146
-
147
- With `shared_dirs` configured, `git stint start` automatically:
148
- 1. Creates symlinks from the worktree to the main repo for each listed directory
149
- 2. On `git stint end` / `abort`, removes the symlinks before deleting the worktree — so linked data is never lost
150
-
151
- ```
152
- # Main repo # Worktree (.stint/my-session/)
153
- backend/data/ (200MB cache) ←── backend/data → symlink to main
154
- backend/results/ ←── backend/results → symlink to main
155
- ```
156
-
157
- The directories listed in `shared_dirs` should typically be gitignored, since they contain large or generated data that shouldn't be committed.
159
+ | `shared_dirs` | `string[]` | `[]` | Directories to symlink from worktree to main repo. Use for gitignored dirs (caches, build outputs) that shouldn't be duplicated per session. |
160
+ | `main_branch_policy` | `"block"` / `"prompt"` / `"allow"` | `"prompt"` | What happens when an agent writes to main. `"block"` auto-creates a session. `"prompt"` blocks with instructions. `"allow"` passes through. |
161
+ | `force_cleanup` | `"force"` / `"prompt"` / `"fail"` | `"prompt"` | Behavior when worktree removal fails. |
162
+ | `adopt_changes` | `"always"` / `"never"` / `"prompt"` | `"always"` | Whether uncommitted changes on main carry into new sessions. |
158
163
 
159
164
  ### Main Branch Policy
160
165
 
161
- Controls what happens when Claude Code (or another agent) tries to write directly to the main branch while hooks are installed:
166
+ The hook intercepts every file write from the AI agent:
162
167
 
163
- - **`"block"`** — Auto-creates a session and blocks the write, forcing the agent to work in the worktree. This is the most protective mode.
164
- - **`"prompt"`** (default) — Blocks with a message that includes the exact command to unblock, e.g. `git stint allow-main --client-id 56193`. Lets you choose per-situation.
165
- - **`"allow"`** — Passes through silently. Hooks still track files in existing worktrees, but don't enforce session usage.
168
+ - **`"block"`** — Auto-creates a session on the first write. The agent is seamlessly redirected to the worktree. Most protective mode.
169
+ - **`"prompt"`** (default) — Blocks with a message showing the exact command to unblock (`git stint allow-main --client-id <PID>`). Lets you decide per-situation.
170
+ - **`"allow"`** — Passes through. Hooks still track files in existing sessions but don't enforce session usage.
166
171
 
167
- The `allow-main` flag is scoped per-process. When the hook blocks a write, it prints the exact command with the correct `--client-id` (the Claude Code instance's PID). Running that command creates `.git/stint-main-allowed-<PID>`, which only unblocks that specific instance. Other Claude Code sessions remain blocked.
168
-
169
- **Important:** Always use the `--client-id` value from the hook's block message. Running `allow-main` without `--client-id` from a separate terminal will NOT unblock Claude Code (different process tree). The intended flow is:
170
-
171
- 1. Hook blocks Claude's write and prints: `git stint allow-main --client-id <PID>`
172
- 2. Claude (or the user telling Claude) runs that exact command
173
- 3. Claude retries the write — it succeeds
174
-
175
- Stale flags from dead processes are cleaned up by `git stint prune`.
172
+ The `allow-main` flag is scoped per-process. When the hook blocks a write, it prints the exact command with the correct `--client-id`. Only that specific agent instance is unblocked other instances stay protected. Stale flags from dead processes are cleaned by `git stint prune`.
176
173
 
177
174
  ### Adopting Uncommitted Changes
178
175
 
179
- When you run `git stint start` with uncommitted changes on main, behavior depends on `adopt_changes`:
176
+ When you run `git stint start` with uncommitted changes on main:
180
177
 
181
- - **`"always"`** (default) — Stashes changes (staged + unstaged + untracked), pops them into the new worktree, leaves main clean. Your work carries over seamlessly.
182
- - **`"never"`** — Leaves uncommitted changes on main. The new worktree starts clean.
183
- - **`"prompt"`** — Warns about uncommitted changes and suggests using `--adopt` or `--no-adopt`.
184
-
185
- CLI flags override the config for a single invocation:
178
+ - **`"always"`** (default) — Stashes changes, pops them into the new worktree. Your work carries over seamlessly.
179
+ - **`"never"`** — Leaves uncommitted changes on main. New worktree starts clean.
180
+ - **`"prompt"`** — Warns and suggests `--adopt` or `--no-adopt`.
186
181
 
187
182
  ```bash
188
- git stint start my-feature --adopt # Force adopt (overrides "never")
189
- git stint start my-feature --no-adopt # Force skip (overrides "always")
183
+ git stint start my-feature --adopt # Force adopt regardless of config
184
+ git stint start my-feature --no-adopt # Skip regardless of config
190
185
  ```
191
186
 
192
- ## Claude Code Integration
193
-
194
- git-stint includes hooks that make it work seamlessly with [Claude Code](https://docs.anthropic.com/en/docs/claude-code):
195
-
196
- - **PreToolUse hook**: When Claude writes/edits a file inside a session worktree, the file is automatically tracked. If Claude tries to write to the main repo, behavior depends on `main_branch_policy` in `.stint.json`. Writes to gitignored files (e.g. `node_modules/`, `dist/`, `.env`) are always allowed through — they can't be committed, so they don't need branch isolation.
197
- - **Stop hook**: When a Claude Code conversation ends, pending changes are auto-committed as a WIP checkpoint.
198
- - **Session affinity**: Each Claude Code instance is mapped to its own session via `clientId` (process ID). Multiple Claude instances can work in parallel without hijacking each other's sessions.
199
-
200
- ### Setup for Claude Code
201
-
202
- ```bash
203
- # 1. Install git-stint globally
204
- npm install -g git-stint
205
-
206
- # 2. Navigate to your project
207
- cd /path/to/your/repo
187
+ ## How It Works
208
188
 
209
- # 3. Install hooks (writes to .claude/settings.json)
210
- git stint install-hooks
189
+ ### Session Model
211
190
 
212
- # 4. (Optional) Configure shared dirs and branch policy
213
- cat > .stint.json << 'EOF'
214
- {
215
- "shared_dirs": [],
216
- "main_branch_policy": "prompt"
217
- }
218
- EOF
191
+ Each session creates three things:
219
192
 
220
- # 5. Done — Claude Code will now auto-track files in sessions
221
193
  ```
222
-
223
- To install hooks globally (all repos):
224
-
225
- ```bash
226
- git stint install-hooks --user
194
+ Branch: stint/auth-fix (real git branch, forked from HEAD)
195
+ Worktree: .stint/auth-fix/ (isolated working directory)
196
+ Manifest: .git/sessions/auth-fix.json (session state, disposable)
227
197
  ```
228
198
 
229
- ### Workflow with Claude Code
199
+ The **baseline cursor** advances on each commit. `git diff baseline..HEAD` always shows exactly the uncommitted work — no guesswork about what changed.
230
200
 
231
- **Option A: Session-based (isolated branch)**
201
+ ### Session Resolution
232
202
 
233
- ```bash
234
- # Start a session before asking Claude to work
235
- git stint start my-feature
236
- cd .stint/my-feature/
203
+ You rarely need `--session`. git-stint resolves the active session automatically:
237
204
 
238
- # Tell Claude to work on things — hooks handle tracking automatically
205
+ 1. Explicit `--session <name>` flag
206
+ 2. CWD inside a `.stint/<name>/` worktree
207
+ 3. Process-based affinity (agent's PID maps to its session)
208
+ 4. Single session fallback (if only one session exists)
239
209
 
240
- # When done, squash and PR
241
- git stint squash -m "Implement feature X"
242
- git stint pr
243
- git stint end
244
- ```
245
-
246
- Or let the hooks auto-create sessions — just start coding with Claude and the hook will create a session on the first write (when `main_branch_policy` is `"block"`).
247
-
248
- **Option B: Quick edits on main (allow-main)**
210
+ ### Parallel Sessions
249
211
 
250
- For small changes that don't need a branch, the hook blocks and tells Claude exactly what to run:
212
+ Each session is fully isolated separate branch, separate worktree, separate filesystem. Agents can safely do everything in parallel:
251
213
 
252
214
  ```
253
- BLOCKED: Writing to main branch.
254
- To allow, run: git stint allow-main --client-id 56193
255
- To create a session instead, run: git stint start <name>
215
+ Session A: .stint/auth-fix/ ──→ edit, build, test, commit, PR
216
+ Session B: .stint/add-tests/ ──→ edit, build, test, commit, PR
217
+ Session C: .stint/refactor/ ──→ edit, build, test, commit, PR
218
+ └── all running simultaneously
256
219
  ```
257
220
 
258
- Claude runs the printed command, then retries the write it succeeds. The flag only applies to that specific Claude Code session. Other instances stay blocked.
259
-
260
- ## How It Works
261
-
262
- ### Session Model
221
+ One agent's `npm install` doesn't interfere with another's build. One agent's failing test doesn't block another's PR. Overlapping file edits are caught early by `git stint conflicts`, and final integration uses git's standard merge machinery.
263
222
 
264
- Each session creates:
265
- - A **git branch** (`stint/<name>`) forked from HEAD
266
- - A **worktree** (`.stint/<name>/`) for isolated file access
267
- - A **manifest** (`.git/sessions/<name>.json`) tracking state
223
+ ### Claude Code Hooks
268
224
 
269
- ```
270
- Session starts at HEAD = abc123
271
- |
272
- Edit config.ts, server.ts
273
- |
274
- "commit" -> changeset 1 (baseline advances to new SHA)
275
- |
276
- Edit server.ts (again), test.ts
277
- |
278
- "commit" -> changeset 2 (only NEW changes since last commit)
279
- ```
225
+ Two hooks make git-stint work automatically with Claude Code:
280
226
 
281
- The **baseline cursor** advances on each commit. `git diff baseline..HEAD` always gives exactly the uncommitted work. No virtual branches, no custom merge engine.
227
+ **PreToolUse** Intercepts every `Write`, `Edit`, and `NotebookEdit` tool call. If the file is inside a session worktree, it's tracked. If it's on main, the hook enforces the configured policy (block, prompt, or allow). Writes to gitignored files always pass through.
282
228
 
283
- ### Parallel Sessions
229
+ **Stop** When a conversation ends, commits all pending changes as a WIP checkpoint. No work is ever lost to timeouts or closed windows.
284
230
 
285
- Multiple sessions run simultaneously with full isolation:
231
+ To install hooks globally (all repos):
286
232
 
287
- ```
288
- Session A: edits config.ts, server.ts -> .stint/session-a/
289
- Session B: edits server.ts, constants.ts -> .stint/session-b/
290
- ^ overlap detected by `git stint conflicts`
233
+ ```bash
234
+ git stint install-hooks --user
291
235
  ```
292
236
 
293
- Each session has its own worktree — no interference. Conflicts resolve at PR merge time, using git's standard merge machinery.
237
+ ### Smart Cleanup
294
238
 
295
- ### Testing
239
+ `git stint end` handles everything:
296
240
 
297
- ```bash
298
- # Test a single session in its worktree
299
- git stint test -- npm test
300
-
301
- # Test multiple sessions merged together
302
- git stint test --combine auth-fix perf-update -- npm test
303
- ```
241
+ - Removes symlinks before deleting worktrees (linked data is never lost)
242
+ - Deletes the remote branch **only** when changes are verified merged on the remote — checks against `origin/main`, not local branches
243
+ - Two-tier merge verification: commit ancestry for regular merges, content diff for squash/rebase merges
244
+ - Network errors never block cleanup — unmerged branches are preserved with a warning
304
245
 
305
- Combined testing creates a temporary octopus merge of the specified sessions, runs the test command, then cleans up. No permanent state changes.
246
+ ### Safety
306
247
 
307
- ## Architecture
248
+ - All git commands use `execFileSync` with array arguments — no shell injection
249
+ - Session names are validated (alphanumeric, hyphens, dots, underscores — no path traversal)
250
+ - Manifest writes are atomic (temp file + rename) — crash-safe
251
+ - `.stint/` is excluded via `.git/info/exclude` — never pollutes `.gitignore`
308
252
 
309
- ```
310
- +----------------+
311
- | .stint.json |
312
- | (config) |
313
- +-------+--------+
314
- |
315
- +-----------+ +-----------+ v +----------------+
316
- | CLI | | Session |------->| Config |
317
- | (cli.ts) |--->|(session.ts)| | (config.ts) |
318
- | arg parse | | commands |---+ +----------------+
319
- +-----------+ +-----+-----+ |
320
- | +--->+----------------+
321
- +------v------+ | Manifest |
322
- | Git | | (manifest.ts) |
323
- | (git.ts) | | JSON state |
324
- | plumbing | +----------------+
325
- +-------------+
326
- ```
253
+ ## Prerequisites
327
254
 
328
- | File | Purpose | Lines |
329
- |------|---------|-------|
330
- | `src/git.ts` | Git command wrapper (`execFileSync`) | ~180 |
331
- | `src/manifest.ts` | Session state CRUD in `.git/sessions/` | ~200 |
332
- | `src/session.ts` | Core commands (start, commit, squash, pr, end...) | ~830 |
333
- | `src/config.ts` | `.stint.json` loading and validation | ~60 |
334
- | `src/conflicts.ts` | Cross-session file overlap detection | ~55 |
335
- | `src/test-session.ts` | Worktree-based testing + combined testing | ~140 |
336
- | `src/cli.ts` | Entry point, argument parsing | ~300 |
337
- | `src/install-hooks.ts` | Claude Code hook installation/removal | ~170 |
338
- | `adapters/claude-code/hooks/` | Bash hooks (PreToolUse + Stop) | ~220 |
339
-
340
- ### Design Decisions
341
-
342
- - **Real git branches** — not virtual branches. Every git tool works: `git log`, `git diff`, lazygit, tig, VS Code.
343
- - **Worktrees for isolation** — the default state is isolated. No unapply/apply dance.
344
- - **JSON manifests** — stored in `.git/sessions/`. Disposable. Worst case: delete and start over.
345
- - **No custom merge engine** — git's built-in merge handles everything. Source of most GitButler complexity eliminated.
346
- - **`execFileSync` everywhere** — array arguments prevent shell injection. No `execSync` with string interpolation.
347
- - **Atomic manifest writes** — write to `.tmp`, then `rename()`. Crash-safe.
348
- - **Symlinks for shared data** — gitignored dirs (caches, data) symlink into worktrees instead of being copied or lost.
349
- - **Zero runtime dependencies** — only Node.js built-ins. Dev deps are TypeScript and @types/node.
350
-
351
- ## git-stint vs GitButler
352
-
353
- | Aspect | git-stint | GitButler |
354
- |--------|-----------|-----------|
355
- | Isolation | Default (each branch IS isolated) | Opt-in (unapply other branches) |
356
- | Branch storage | Real git branches | Virtual branches (TOML + SQLite) |
357
- | Working dir | One branch per worktree | Permanent octopus merge overlay |
358
- | Merge engine | Git's built-in | Custom hunk-level engine |
359
- | Git compatibility | Full — all git tools work | Partial — writes break state |
360
- | State | JSON manifests (disposable) | SQLite + TOML (can corrupt) |
361
- | Code size | ~2,000 lines TypeScript + bash | ~100k+ lines Rust |
362
- | Dependencies | git, gh (optional) | Tauri desktop app |
363
-
364
- git-stint is designed for AI agent workflows where sessions are independent and short-lived. GitButler is a full-featured branch management GUI for teams.
255
+ - [Node.js](https://nodejs.org) 20+
256
+ - [git](https://git-scm.com) 2.20+ (worktree support)
257
+ - [`gh` CLI](https://cli.github.com) (optional, for `git stint pr`)
365
258
 
366
259
  ## Development
367
260
 
@@ -374,16 +267,14 @@ npm link # Install globally for testing
374
267
 
375
268
  # Run tests
376
269
  npm test # Unit tests
377
- npm run test:security # Security tests
378
- npm run test:integration # Integration tests
379
- npm run test:all # Everything (build + all tests)
270
+ npm run test:all # Everything (build + all tests)
380
271
  ```
381
272
 
382
273
  ## Contributing
383
274
 
384
275
  See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and PR guidelines.
385
276
 
386
- Please note that this project is released with a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you agree to abide by its terms.
277
+ This project uses a [Code of Conduct](CODE_OF_CONDUCT.md).
387
278
 
388
279
  ## License
389
280
 
@@ -152,7 +152,8 @@ if [ "$MAIN_BRANCH_POLICY" = "prompt" ]; then
152
152
  fi
153
153
  echo "BLOCKED: Writing to main branch." >&2
154
154
  echo "To allow, run: git stint allow-main --client-id $CLIENT_ID" >&2
155
- echo "To create a session instead, run: git stint start <name>" >&2
155
+ echo "To create a session instead, run: git stint start <descriptive-name>" >&2
156
+ echo " (pick a short name that describes your task, e.g. fix-auth-refresh, add-user-search)" >&2
156
157
  exit 2
157
158
  fi
158
159
 
@@ -163,7 +164,9 @@ if [ -n "$CLIENT_ID" ]; then
163
164
  CLIENT_FLAG="--client-id $CLIENT_ID"
164
165
  fi
165
166
 
166
- START_OUTPUT=$(git-stint start "$SESSION_NAME" $CLIENT_FLAG 2>&1) || {
167
+ # Always adopt uncommitted changes when auto-creating files may have been
168
+ # written to main via Bash (bypassing hooks) before this session was created.
169
+ START_OUTPUT=$(git-stint start "$SESSION_NAME" --adopt $CLIENT_FLAG 2>&1) || {
167
170
  # If start fails, allow the write
168
171
  exit 0
169
172
  }
package/dist/cli.js CHANGED
@@ -166,6 +166,10 @@ try {
166
166
  }
167
167
  break;
168
168
  }
169
+ case "which": {
170
+ session.which(getFlag("--session"), args.includes("--worktree"));
171
+ break;
172
+ }
169
173
  case "list": {
170
174
  if (args.includes("--json")) {
171
175
  session.listJson();
@@ -253,6 +257,7 @@ Commands:
253
257
  end Finalize session, clean up everything
254
258
  abort Discard session — delete all changes
255
259
  undo Revert last commit, changes become pending
260
+ which [--worktree] Print resolved session name or worktree path
256
261
  conflicts Check file overlap with other sessions
257
262
  test [-- <cmd>] Run tests in the session worktree
258
263
  test --combine A B Test multiple sessions merged together
package/dist/git.d.ts CHANGED
@@ -20,6 +20,16 @@ export declare function deleteBranch(name: string): void;
20
20
  */
21
21
  export declare function remoteBranchExists(name: string): boolean;
22
22
  export declare function deleteRemoteBranch(name: string): void;
23
+ /**
24
+ * Check if all changes from `branch` are present in `into`.
25
+ * Handles regular merges, squash merges, and rebase merges.
26
+ *
27
+ * 1. Fast path: branch tip is an ancestor of `into` (regular merge / ff).
28
+ * 2. Content check: every file the branch changed has identical content in
29
+ * `into`. This catches squash and rebase merges where SHAs differ but
30
+ * the content is the same.
31
+ */
32
+ export declare function isBranchMergedInto(branch: string, into: string): boolean;
23
33
  export declare function addWorktree(path: string, branch: string): void;
24
34
  /** Create a worktree in detached HEAD mode at a given ref. */
25
35
  export declare function addWorktreeDetached(path: string, ref: string): void;
package/dist/git.js CHANGED
@@ -78,6 +78,39 @@ export function remoteBranchExists(name) {
78
78
  export function deleteRemoteBranch(name) {
79
79
  git("push", "origin", "--delete", name);
80
80
  }
81
+ /**
82
+ * Check if all changes from `branch` are present in `into`.
83
+ * Handles regular merges, squash merges, and rebase merges.
84
+ *
85
+ * 1. Fast path: branch tip is an ancestor of `into` (regular merge / ff).
86
+ * 2. Content check: every file the branch changed has identical content in
87
+ * `into`. This catches squash and rebase merges where SHAs differ but
88
+ * the content is the same.
89
+ */
90
+ export function isBranchMergedInto(branch, into) {
91
+ // Fast path — works for regular merges and fast-forwards
92
+ try {
93
+ git("merge-base", "--is-ancestor", branch, into);
94
+ return true;
95
+ }
96
+ catch {
97
+ // Not an ancestor — may still be squash/rebase merged
98
+ }
99
+ // Content check — compare files the branch changed against `into`
100
+ try {
101
+ const mergeBase = git("merge-base", into, branch);
102
+ const changedOutput = git("diff", "--name-only", mergeBase, branch);
103
+ if (!changedOutput)
104
+ return true; // branch has no file changes
105
+ const files = changedOutput.split("\n");
106
+ const contentDiff = git("diff", into, branch, "--", ...files);
107
+ return contentDiff.length === 0;
108
+ }
109
+ catch {
110
+ // If anything fails (e.g. branch already deleted), assume not merged
111
+ return false;
112
+ }
113
+ }
81
114
  export function addWorktree(path, branch) {
82
115
  git("worktree", "add", path, branch);
83
116
  }
@@ -93,9 +93,19 @@ const RULES_CONTENT = `# Git Stint Workflow
93
93
  All file edits are intercepted by git-stint hooks and redirected to isolated
94
94
  worktrees. One stint session = one branch = one PR.
95
95
 
96
+ ## Session Naming
97
+
98
+ When creating a session, pick a short descriptive name that captures the task:
99
+ - Good: \`fix-auth-refresh\`, \`add-user-search\`, \`refactor-db-queries\`
100
+ - Bad: \`session-1\`, \`changes\`, \`test\`, \`update\`
101
+
102
+ The name becomes the branch (\`stint/<name>\`) and the PR title context.
103
+
96
104
  ## Session Lifecycle
97
105
 
98
- - Session auto-creates on first Write/Edit (via PreToolUse hook).
106
+ - If the hook blocks a write, create a session: \`git stint start <descriptive-name>\`
107
+ - Any uncommitted files on main are automatically carried into the new session.
108
+ Do NOT redo work that was already written — it is adopted into the worktree.
99
109
  - All edits redirect to \`.stint/<session>/\` worktree.
100
110
  - \`git stint commit -m "msg"\` to commit logical units of work.
101
111
  - \`git stint pr\` to push and create PR.
@@ -103,6 +113,10 @@ worktrees. One stint session = one branch = one PR.
103
113
 
104
114
  ## Rules
105
115
 
116
+ - **NEVER end or delete a stint session you didn't create.** Other sessions
117
+ belong to other conversations or agents. Only operate on your own session
118
+ (the one auto-created by the hook for your edits). Use \`git stint list\` to
119
+ see all sessions — leave others alone.
106
120
  - Do NOT call \`git stint end\` until all changes are committed (code, tests,
107
121
  config updates, follow-up tasks). Premature \`end\` kills the session; the
108
122
  next edit auto-creates a NEW session, fragmenting work across multiple PRs.
package/dist/manifest.js CHANGED
@@ -128,6 +128,11 @@ export function resolveSession(explicit) {
128
128
  return manifests[0];
129
129
  if (manifests.length === 0)
130
130
  throw new Error("No active sessions. Run `git stint start <name>` to create one.");
131
+ // PPID-based clientId affinity
132
+ const ppid = String(process.ppid);
133
+ const clientMatch = manifests.filter(m => m.clientId === ppid);
134
+ if (clientMatch.length === 1)
135
+ return clientMatch[0];
131
136
  const names = manifests.map((m) => m.name).join(", ");
132
137
  throw new Error(`Multiple active sessions: ${names}.\n` +
133
138
  `Use --session <name> to specify, or cd into a worktree.\n` +
package/dist/session.d.ts CHANGED
@@ -13,6 +13,7 @@ export declare function end(sessionName?: string): void;
13
13
  export declare function abort(sessionName?: string): void;
14
14
  /** Revert last commit, keeping changes as unstaged files. */
15
15
  export declare function undo(sessionName?: string): void;
16
+ export declare function which(sessionName?: string, showWorktree?: boolean): void;
16
17
  export declare function list(): void;
17
18
  export declare function listJson(): void;
18
19
  /** Clean up orphaned worktrees, manifests, and branches. */
package/dist/session.js CHANGED
@@ -536,6 +536,15 @@ export function undo(sessionName) {
536
536
  console.log(`Undid commit: ${last.sha.slice(0, 8)} ${last.message}`);
537
537
  console.log(`${last.files.length} file(s) back to pending.`);
538
538
  }
539
+ export function which(sessionName, showWorktree) {
540
+ const manifest = resolveSession(sessionName);
541
+ if (showWorktree) {
542
+ console.log(getWorktreePath(manifest));
543
+ }
544
+ else {
545
+ console.log(manifest.name);
546
+ }
547
+ }
539
548
  export function list() {
540
549
  const manifests = listManifests();
541
550
  if (manifests.length === 0) {
@@ -696,15 +705,58 @@ function cleanup(manifest, force = false) {
696
705
  }
697
706
  }
698
707
  }
708
+ // Check if remote branch should be cleaned up (before deleting local branch,
709
+ // since we need it for the merge check).
710
+ //
711
+ // IMPORTANT: We check against REMOTE tracking refs (origin/main), not local
712
+ // branches. A local `git stint merge` merges into local main, but the user
713
+ // may not have pushed yet. Deleting the remote session branch before the
714
+ // remote main has the changes would destroy the only remote copy of the work.
715
+ // By checking origin/main, we only delete when the remote already has the
716
+ // changes — matching how GitHub/GitLab auto-delete works.
717
+ let shouldDeleteRemote = false;
718
+ if (git.remoteBranchExists(manifest.branch)) {
719
+ // Build list of remote tracking refs to check against.
720
+ const targets = new Set();
721
+ const defaultBranch = git.getDefaultBranch();
722
+ targets.add(`origin/${defaultBranch}`);
723
+ try {
724
+ const current = git.currentBranch(topLevel);
725
+ if (current !== manifest.branch && git.remoteBranchExists(current)) {
726
+ targets.add(`origin/${current}`);
727
+ }
728
+ }
729
+ catch { /* detached HEAD — skip */ }
730
+ for (const target of targets) {
731
+ if (git.isBranchMergedInto(manifest.branch, target)) {
732
+ shouldDeleteRemote = true;
733
+ break;
734
+ }
735
+ }
736
+ if (!shouldDeleteRemote) {
737
+ console.log(`Remote branch 'origin/${manifest.branch}' was NOT deleted (has unmerged changes).\n` +
738
+ ` To delete manually: git push origin --delete ${manifest.branch}`);
739
+ }
740
+ }
699
741
  // Delete local branch
700
742
  try {
701
743
  git.deleteBranch(manifest.branch);
702
744
  }
703
745
  catch { /* branch may already be deleted */ }
704
- // Delete remote tracking ref if it exists (no network call — just local ref).
705
- // We intentionally do NOT delete the remote branch itself:
706
- // the user may have an open PR. They can clean up with `git push origin --delete`.
707
- // The local tracking ref is cleaned up by `git branch -D` above.
746
+ // Delete remote branch if all changes are merged
747
+ if (shouldDeleteRemote) {
748
+ try {
749
+ git.deleteRemoteBranch(manifest.branch);
750
+ console.log(`Deleted remote branch 'origin/${manifest.branch}'.`);
751
+ }
752
+ catch {
753
+ // Branch may already be deleted on the remote (e.g., GitHub auto-delete
754
+ // after PR merge) or the network may be down. Either way, not critical.
755
+ console.log(`Could not delete remote branch 'origin/${manifest.branch}'.\n` +
756
+ ` It may have already been deleted (e.g., by GitHub after PR merge).\n` +
757
+ ` To delete manually: git push origin --delete ${manifest.branch}`);
758
+ }
759
+ }
708
760
  // Delete manifest last — if anything above fails, manifest persists for prune
709
761
  deleteManifest(manifest.name);
710
762
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-stint",
3
- "version": "0.2.4",
3
+ "version": "0.4.0",
4
4
  "description": "Session-scoped change tracking for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {