git-stint 0.3.1 → 0.4.1

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,113 @@
1
1
  # git-stint
2
2
 
3
+ <p align="center">
4
+ <img src="assets/banner.svg" alt="git-stint — Autonomous git infrastructure for multi-agent engineering" width="100%">
5
+ </p>
6
+
7
+ [![npm](https://img.shields.io/npm/v/git-stint)](https://www.npmjs.com/package/git-stint)
8
+ [![CI](https://github.com/rchaz/git-stint/actions/workflows/ci.yml/badge.svg)](https://github.com/rchaz/git-stint/actions)
3
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
10
  [![Node.js: 20+](https://img.shields.io/badge/Node.js-20%2B-green.svg)](https://nodejs.org)
5
11
 
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.
12
+ **Built for AI agents to multitask without collisions.**
7
13
 
8
- Built to replace GitButler for AI-agent workflows. No virtual branches, no custom merge engine, no state corruption. Just git.
14
+ 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
15
 
10
- ## Why
16
+ No cloud VMs. No Docker containers. No desktop app. No new VCS to learn. Just git — with session management that runs itself.
11
17
 
12
- AI coding agents (Claude Code, Cursor, Copilot) edit files but have no clean way to:
18
+ ```
19
+ Agent A fixes auth ──→ .stint/auth-fix/ → builds, tests, PRs independently
20
+ Agent B adds search ──→ .stint/add-search/ → builds, tests, PRs independently
21
+ Agent C writes docs ──→ .stint/update-docs/ → builds, tests, PRs independently
22
+ You keep working ──→ main branch, untouched
23
+ ```
13
24
 
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
25
+ ## The Problem
18
26
 
19
- git-stint solves this with ~2,000 lines of TypeScript + bash on top of standard git primitives.
27
+ 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
28
 
21
- ## Prerequisites
29
+ - **Separate agent changes from human changes** — who wrote what?
30
+ - **Run agents in parallel** — two agents on the same branch will collide
31
+ - **Get clean, reviewable PRs** — agent work is often a stream of small edits
32
+ - **Undo an agent's work** — reverting scattered changes across files is painful
33
+ - **Prevent accidental damage** — agents write to `main` unless something stops them
22
34
 
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)
35
+ ## How git-stint Solves This
26
36
 
27
- ## Install
37
+ 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
38
 
29
- ### With Claude Code (recommended)
39
+ Every step of the session lifecycle is autonomous:
30
40
 
31
- Tell Claude Code:
41
+ | What happens | What git-stint does | You do anything? |
42
+ |---|---|---|
43
+ | Agent writes its first file | Auto-creates a session branch + worktree | No |
44
+ | Agent keeps writing files | Hooks track every change automatically | No |
45
+ | Agent builds or runs tests | Runs in the isolated worktree — can't break main | No |
46
+ | Conversation ends unexpectedly | Auto-commits a WIP checkpoint — nothing lost | No |
47
+ | Work is ready to ship | `git stint squash` + `git stint pr` = clean PR | One command |
48
+ | Session is done | `git stint end` cleans up branch, worktree, and remote | One command |
32
49
 
33
- > Install git-stint globally (`npm install -g git-stint`), set up hooks for this repo, and create a .stint.json
50
+ ## Key Features
34
51
 
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`
52
+ **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
53
 
40
- ### Manual install
54
+ **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
55
 
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
56
+ **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
57
 
49
- # 2. Set up hooks in your project
50
- cd /path/to/your/repo
51
- git stint install-hooks
58
+ **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
59
 
53
- # 3. Configure (optional)
54
- cat > .stint.json << 'EOF'
55
- {
56
- "shared_dirs": [],
57
- "main_branch_policy": "prompt"
58
- }
59
- EOF
60
- ```
60
+ **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.
61
+
62
+ **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.
63
+
64
+ **Shared directories** — Large caches (`node_modules`, `venv`, build outputs) are symlinked into worktrees instead of duplicated. Agents get fast startup, you save disk space.
65
+
66
+ **Zero dependencies** — Pure Node.js built-ins. No native modules, no runtime deps, no surprises. Installs in seconds.
67
+
68
+ **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
69
 
62
70
  ## Quick Start
63
71
 
64
72
  ```bash
65
- # Start a session (creates branch + worktree)
73
+ # Install
74
+ npm install -g git-stint
75
+
76
+ # Set up hooks in your repo
77
+ cd /path/to/your/repo
78
+ git stint install-hooks
79
+
80
+ # Start a session
66
81
  git stint start auth-fix
67
82
  cd .stint/auth-fix/
68
83
 
69
- # Work normally — make changes, edit files...
70
-
71
- # Commit progress (advances baseline)
84
+ # Make changes, then commit
72
85
  git stint commit -m "Fix token refresh logic"
73
86
 
74
- # More changes...
75
- git stint commit -m "Add refresh token tests"
76
-
77
- # Squash into a single clean commit
87
+ # Squash into a clean commit and create a PR
78
88
  git stint squash -m "Fix auth token refresh"
79
-
80
- # Create PR and clean up
81
89
  git stint pr --title "Fix auth bug"
90
+
91
+ # Clean up
82
92
  git stint end
83
93
  ```
84
94
 
95
+ ### With Claude Code
96
+
97
+ Tell Claude Code:
98
+
99
+ > Install git-stint globally (`npm install -g git-stint`), set up hooks for this repo, and create a .stint.json
100
+
101
+ Or manually:
102
+
103
+ ```bash
104
+ npm install -g git-stint
105
+ cd /path/to/your/repo
106
+ git stint install-hooks # Writes to .claude/settings.json
107
+ ```
108
+
109
+ 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.
110
+
85
111
  ## Commands
86
112
 
87
113
  | Command | Description |
@@ -89,45 +115,44 @@ git stint end
89
115
  | `git stint start [name]` | Create a new session (branch + worktree) |
90
116
  | `git stint list` | List all active sessions |
91
117
  | `git stint status` | Show current session state |
92
- | `git stint track <file...>` | Add files to the pending list |
93
118
  | `git stint diff` | Show uncommitted changes in worktree |
94
119
  | `git stint commit -m "msg"` | Commit changes, advance baseline |
95
120
  | `git stint log` | Show session commit history |
96
121
  | `git stint squash -m "msg"` | Collapse all commits into one |
97
- | `git stint merge` | Merge session into current branch (no PR) |
122
+ | `git stint merge` | Merge session into current branch |
98
123
  | `git stint pr [--title "..."]` | Push branch and create GitHub PR |
99
- | `git stint end` | Finalize session, clean up everything (deletes remote branch if merged) |
124
+ | `git stint end` | Finalize session, clean up everything |
100
125
  | `git stint abort` | Discard session — delete all changes |
101
126
  | `git stint undo` | Revert last commit, changes become pending |
127
+ | `git stint which [--worktree]` | Print resolved session name (or worktree path) |
102
128
  | `git stint conflicts` | Check file overlap with other sessions |
103
129
  | `git stint test [-- cmd]` | Run tests in the session worktree |
104
130
  | `git stint test --combine A B` | Test multiple sessions merged together |
131
+ | `git stint track <file...>` | Add files to the pending list |
105
132
  | `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) |
133
+ | `git stint allow-main` | Allow writes to main (scoped to one process) |
107
134
  | `git stint install-hooks` | Install Claude Code hooks |
108
135
  | `git stint uninstall-hooks` | Remove Claude Code hooks |
109
136
 
110
137
  ### Options
111
138
 
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
139
+ | Flag | Description |
140
+ |------|-------------|
141
+ | `--session <name>` | Target a specific session (auto-detected from CWD) |
142
+ | `--client-id <id>` | Client identifier for multi-instance affinity |
143
+ | `--adopt` / `--no-adopt` | Control whether uncommitted changes carry into a new session |
144
+ | `-m "message"` | Commit or squash message |
145
+ | `--title "title"` | PR title |
146
+ | `--version` | Show version number |
118
147
 
119
148
  ## Configuration — `.stint.json`
120
149
 
121
- Create a `.stint.json` file in your repo root to configure git-stint behavior:
150
+ Create a `.stint.json` in your repo root:
122
151
 
123
152
  ```json
124
153
  {
125
- "shared_dirs": [
126
- "backend/data",
127
- "backend/results",
128
- "backend/logs"
129
- ],
130
- "main_branch_policy": "prompt",
154
+ "shared_dirs": ["node_modules", ".venv", "dist"],
155
+ "main_branch_policy": "block",
131
156
  "force_cleanup": "prompt",
132
157
  "adopt_changes": "always"
133
158
  }
@@ -135,234 +160,105 @@ Create a `.stint.json` file in your repo root to configure git-stint behavior:
135
160
 
136
161
  | Field | Values | Default | Description |
137
162
  |-------|--------|---------|-------------|
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.
163
+ | `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. |
164
+ | `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. |
165
+ | `force_cleanup` | `"force"` / `"prompt"` / `"fail"` | `"prompt"` | Behavior when worktree removal fails. |
166
+ | `adopt_changes` | `"always"` / `"never"` / `"prompt"` | `"always"` | Whether uncommitted changes on main carry into new sessions. |
158
167
 
159
168
  ### Main Branch Policy
160
169
 
161
- Controls what happens when Claude Code (or another agent) tries to write directly to the main branch while hooks are installed:
162
-
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.
166
-
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.
170
+ The hook intercepts every file write from the AI agent:
168
171
 
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:
172
+ - **`"block"`** Auto-creates a session on the first write. The agent is seamlessly redirected to the worktree. Most protective mode.
173
+ - **`"prompt"`** (default) — Blocks with a message showing the exact command to unblock (`git stint allow-main --client-id <PID>`). Lets you decide per-situation.
174
+ - **`"allow"`** — Passes through. Hooks still track files in existing sessions but don't enforce session usage.
170
175
 
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`.
176
+ 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
177
 
177
178
  ### Adopting Uncommitted Changes
178
179
 
179
- When you run `git stint start` with uncommitted changes on main, behavior depends on `adopt_changes`:
180
-
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`.
180
+ When you run `git stint start` with uncommitted changes on main:
184
181
 
185
- CLI flags override the config for a single invocation:
182
+ - **`"always"`** (default) — Stashes changes, pops them into the new worktree. Your work carries over seamlessly.
183
+ - **`"never"`** — Leaves uncommitted changes on main. New worktree starts clean.
184
+ - **`"prompt"`** — Warns and suggests `--adopt` or `--no-adopt`.
186
185
 
187
186
  ```bash
188
- git stint start my-feature --adopt # Force adopt (overrides "never")
189
- git stint start my-feature --no-adopt # Force skip (overrides "always")
187
+ git stint start my-feature --adopt # Force adopt regardless of config
188
+ git stint start my-feature --no-adopt # Skip regardless of config
190
189
  ```
191
190
 
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
191
+ ## How It Works
208
192
 
209
- # 3. Install hooks (writes to .claude/settings.json)
210
- git stint install-hooks
193
+ ### Session Model
211
194
 
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
195
+ Each session creates three things:
219
196
 
220
- # 5. Done — Claude Code will now auto-track files in sessions
221
197
  ```
222
-
223
- To install hooks globally (all repos):
224
-
225
- ```bash
226
- git stint install-hooks --user
198
+ Branch: stint/auth-fix (real git branch, forked from HEAD)
199
+ Worktree: .stint/auth-fix/ (isolated working directory)
200
+ Manifest: .git/sessions/auth-fix.json (session state, disposable)
227
201
  ```
228
202
 
229
- ### Workflow with Claude Code
230
-
231
- **Option A: Session-based (isolated branch)**
203
+ The **baseline cursor** advances on each commit. `git diff baseline..HEAD` always shows exactly the uncommitted work — no guesswork about what changed.
232
204
 
233
- ```bash
234
- # Start a session before asking Claude to work
235
- git stint start my-feature
236
- cd .stint/my-feature/
237
-
238
- # Tell Claude to work on things — hooks handle tracking automatically
205
+ ### Session Resolution
239
206
 
240
- # When done, squash and PR
241
- git stint squash -m "Implement feature X"
242
- git stint pr
243
- git stint end
244
- ```
207
+ You rarely need `--session`. git-stint resolves the active session automatically:
245
208
 
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"`).
209
+ 1. Explicit `--session <name>` flag
210
+ 2. CWD inside a `.stint/<name>/` worktree
211
+ 3. Process-based affinity (agent's PID maps to its session)
212
+ 4. Single session fallback (if only one session exists)
247
213
 
248
- **Option B: Quick edits on main (allow-main)**
214
+ ### Parallel Sessions
249
215
 
250
- For small changes that don't need a branch, the hook blocks and tells Claude exactly what to run:
216
+ Each session is fully isolated separate branch, separate worktree, separate filesystem. Agents can safely do everything in parallel:
251
217
 
252
218
  ```
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>
219
+ Session A: .stint/auth-fix/ ──→ edit, build, test, commit, PR
220
+ Session B: .stint/add-tests/ ──→ edit, build, test, commit, PR
221
+ Session C: .stint/refactor/ ──→ edit, build, test, commit, PR
222
+ └── all running simultaneously
256
223
  ```
257
224
 
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
225
+ 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
226
 
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
227
+ ### Claude Code Hooks
268
228
 
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
- ```
229
+ Two hooks make git-stint work automatically with Claude Code:
280
230
 
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.
231
+ **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
232
 
283
- ### Parallel Sessions
233
+ **Stop** When a conversation ends, commits all pending changes as a WIP checkpoint. No work is ever lost to timeouts or closed windows.
284
234
 
285
- Multiple sessions run simultaneously with full isolation:
235
+ To install hooks globally (all repos):
286
236
 
237
+ ```bash
238
+ git stint install-hooks --user
287
239
  ```
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`
291
- ```
292
-
293
- Each session has its own worktree — no interference. Conflicts resolve at PR merge time, using git's standard merge machinery.
294
240
 
295
- ### Testing
241
+ ### Smart Cleanup
296
242
 
297
- ```bash
298
- # Test a single session in its worktree
299
- git stint test -- npm test
243
+ `git stint end` handles everything:
300
244
 
301
- # Test multiple sessions merged together
302
- git stint test --combine auth-fix perf-update -- npm test
303
- ```
245
+ - Removes symlinks before deleting worktrees (linked data is never lost)
246
+ - Deletes the remote branch **only** when changes are verified merged on the remote — checks against `origin/main`, not local branches
247
+ - Two-tier merge verification: commit ancestry for regular merges, content diff for squash/rebase merges
248
+ - Network errors never block cleanup — unmerged branches are preserved with a warning
304
249
 
305
- Combined testing creates a temporary octopus merge of the specified sessions, runs the test command, then cleans up. No permanent state changes.
250
+ ### Safety
306
251
 
307
- ## Architecture
252
+ - All git commands use `execFileSync` with array arguments — no shell injection
253
+ - Session names are validated (alphanumeric, hyphens, dots, underscores — no path traversal)
254
+ - Manifest writes are atomic (temp file + rename) — crash-safe
255
+ - `.stint/` is excluded via `.git/info/exclude` — never pollutes `.gitignore`
308
256
 
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
- ```
257
+ ## Prerequisites
327
258
 
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
- - **Safe remote branch cleanup** — on `end`/`merge`, deletes the remote branch only when all changes are verified merged **on the remote**. Checks against remote tracking refs (`origin/main`), not local branches — so a local-only merge won't delete the remote branch until you push. Uses a two-tier check (commit ancestry + content diff) that handles regular merges, squash merges, and rebase merges. Unmerged branches are always preserved with a warning.
350
- - **Zero runtime dependencies** — only Node.js built-ins. Dev deps are TypeScript and @types/node.
351
-
352
- ## git-stint vs GitButler
353
-
354
- | Aspect | git-stint | GitButler |
355
- |--------|-----------|-----------|
356
- | Isolation | Default (each branch IS isolated) | Opt-in (unapply other branches) |
357
- | Branch storage | Real git branches | Virtual branches (TOML + SQLite) |
358
- | Working dir | One branch per worktree | Permanent octopus merge overlay |
359
- | Merge engine | Git's built-in | Custom hunk-level engine |
360
- | Git compatibility | Full — all git tools work | Partial — writes break state |
361
- | State | JSON manifests (disposable) | SQLite + TOML (can corrupt) |
362
- | Code size | ~2,000 lines TypeScript + bash | ~100k+ lines Rust |
363
- | Dependencies | git, gh (optional) | Tauri desktop app |
364
-
365
- 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.
259
+ - [Node.js](https://nodejs.org) 20+
260
+ - [git](https://git-scm.com) 2.20+ (worktree support)
261
+ - [`gh` CLI](https://cli.github.com) (optional, for `git stint pr`)
366
262
 
367
263
  ## Development
368
264
 
@@ -375,16 +271,14 @@ npm link # Install globally for testing
375
271
 
376
272
  # Run tests
377
273
  npm test # Unit tests
378
- npm run test:security # Security tests
379
- npm run test:integration # Integration tests
380
- npm run test:all # Everything (build + all tests)
274
+ npm run test:all # Everything (build + all tests)
381
275
  ```
382
276
 
383
277
  ## Contributing
384
278
 
385
279
  See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and PR guidelines.
386
280
 
387
- 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.
281
+ This project uses a [Code of Conduct](CODE_OF_CONDUCT.md).
388
282
 
389
283
  ## License
390
284
 
@@ -164,7 +164,9 @@ if [ -n "$CLIENT_ID" ]; then
164
164
  CLIENT_FLAG="--client-id $CLIENT_ID"
165
165
  fi
166
166
 
167
- 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) || {
168
170
  # If start fails, allow the write
169
171
  exit 0
170
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
@@ -104,6 +104,8 @@ The name becomes the branch (\`stint/<name>\`) and the PR title context.
104
104
  ## Session Lifecycle
105
105
 
106
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.
107
109
  - All edits redirect to \`.stint/<session>/\` worktree.
108
110
  - \`git stint commit -m "msg"\` to commit logical units of work.
109
111
  - \`git stint pr\` to push and create PR.
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-stint",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Session-scoped change tracking for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {