git-stint 0.3.1 → 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
- | `git stint end` | Finalize session, clean up everything (deletes remote branch if merged) |
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,234 +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
- - **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.
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`)
366
258
 
367
259
  ## Development
368
260
 
@@ -375,16 +267,14 @@ npm link # Install globally for testing
375
267
 
376
268
  # Run tests
377
269
  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)
270
+ npm run test:all # Everything (build + all tests)
381
271
  ```
382
272
 
383
273
  ## Contributing
384
274
 
385
275
  See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and PR guidelines.
386
276
 
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.
277
+ This project uses a [Code of Conduct](CODE_OF_CONDUCT.md).
388
278
 
389
279
  ## License
390
280
 
@@ -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.0",
4
4
  "description": "Session-scoped change tracking for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {