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 +150 -256
- package/adapters/claude-code/hooks/git-stint-hook-pre-tool +3 -1
- package/dist/cli.js +5 -0
- package/dist/install-hooks.js +2 -0
- package/dist/manifest.js +5 -0
- package/dist/session.d.ts +1 -0
- package/dist/session.js +9 -0
- package/package.json +1 -1
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
|
+
[](https://www.npmjs.com/package/git-stint)
|
|
8
|
+
[](https://github.com/rchaz/git-stint/actions)
|
|
3
9
|
[](LICENSE)
|
|
4
10
|
[](https://nodejs.org)
|
|
5
11
|
|
|
6
|
-
|
|
12
|
+
**Built for AI agents to multitask without collisions.**
|
|
7
13
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
+
Every step of the session lifecycle is autonomous:
|
|
30
40
|
|
|
31
|
-
|
|
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
|
-
|
|
50
|
+
## Key Features
|
|
34
51
|
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
70
|
-
|
|
71
|
-
# Commit progress (advances baseline)
|
|
84
|
+
# Make changes, then commit
|
|
72
85
|
git stint commit -m "Fix token refresh logic"
|
|
73
86
|
|
|
74
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
-
|
|
116
|
-
|
|
117
|
-
|
|
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`
|
|
150
|
+
Create a `.stint.json` in your repo root:
|
|
122
151
|
|
|
123
152
|
```json
|
|
124
153
|
{
|
|
125
|
-
"shared_dirs": [
|
|
126
|
-
|
|
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
|
|
139
|
-
| `main_branch_policy` | `"
|
|
140
|
-
| `force_cleanup` | `"
|
|
141
|
-
| `adopt_changes` | `"always"` / `"never"` / `"prompt"` | `"always"` |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
189
|
-
git stint start my-feature --no-adopt #
|
|
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
|
-
##
|
|
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
|
-
|
|
210
|
-
git stint install-hooks
|
|
193
|
+
### Session Model
|
|
211
194
|
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
+
### Parallel Sessions
|
|
249
215
|
|
|
250
|
-
|
|
216
|
+
Each session is fully isolated — separate branch, separate worktree, separate filesystem. Agents can safely do everything in parallel:
|
|
251
217
|
|
|
252
218
|
```
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
241
|
+
### Smart Cleanup
|
|
296
242
|
|
|
297
|
-
|
|
298
|
-
# Test a single session in its worktree
|
|
299
|
-
git stint test -- npm test
|
|
243
|
+
`git stint end` handles everything:
|
|
300
244
|
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
250
|
+
### Safety
|
|
306
251
|
|
|
307
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/install-hooks.js
CHANGED
|
@@ -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) {
|