Vibecode GitKraken
track this build4 phases, 12 steps, beginner friendly0%GitKraken's solo core is compact: build a repository-local Git client assistant that indexes one codebase, calls one model, proposes diffs, runs tests, and records every change. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on model, context, integration.
You are building a lean indie version of GitKraken. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # GitKraken · indie build A local Git client whose hard problems are parsing real repository state without lying about filenames or signatures, drawing a commit graph from the parent list rather than from dates, and making every destructive action recoverable by something better than hope. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Engine | The installed git binary, called with plumbing commands and machine-readable output | your config, includes, hooks and credential helpers only behave correctly inside real git | | Runtime | A Node process serving a React interface, bound to the loopback address only | a program with filesystem and git access must not be reachable from the network | | Parsing | git status --porcelain=v2 -z and git log with a NUL-delimited custom format | filenames legally contain spaces, quotes and newlines and the human formats hide that | | Graph | Lanes computed from each commit's parent list over a bounded window | loading the whole history before the first paint is why these clients feel slow | | Safety | A recovery ref plus a command log written before every destructive action | the difference between a scare and a lost afternoon | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A recent git binary** · free - Why: Everything this client does is git doing it, and porcelain v2 plus the signature placeholders need a version that has them. - Get it: Your package manager, or git-scm.com. - Verify: git --version prints the version, and git status --porcelain=v2 -z runs without error - [ ] **Node.js 22 or newer** · free - Why: Runs the local process serving the interface. - Get it: nodejs.org or your version manager. - Verify: node --version prints v22 or higher - [ ] **A repository with merges, renames and a conflict** · free - Why: Lane assignment, rename detection and unmerged states are the three places this build breaks, and none of them appear in a fresh repository with four commits. - Get it: Clone something real, or generate one with a script in tests/fixtures. - Verify: It contains at least one merge with two parents and one rename git detects - [ ] **A fixture repository with awkward filenames** · free - Why: A filename containing a newline is the single fastest way to find out whether your status parser is correct, and it costs one shell script. - Get it: Create files with spaces, quotes, non-ASCII characters and a newline in the name. - Verify: git status --porcelain=v2 -z lists them and your parser agrees on the count - [ ] **Whether you will test signature states for real** · free - Why: Mapping the eight signature values is easy to get wrong and easy to test with a stub, but a real expired or revoked key is the only way to see the real output. - Get it: Decide between fixture commits with recorded output or generating a key you later revoke. - Verify: The test suite covers all eight values whichever route you take ## Quick start ```sh git status --porcelain=v2 -z --branch --untracked-files=all ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A Git reimplementation against a library binding. - Hosted issue and pull request integrations. - Shared cloud workspaces. - A built-in editor. - frontier coding model quality - large-context infrastructure - IDE-wide polish and latency - enterprise policy, telemetry, and support If one of those is essential to you, that is the reason to keep paying for GitKraken, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · GitKraken The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local Git client that reads the repository through real git and never destroys work you cannot get back to replace GitKraken. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A Git client is a program that runs destructive commands on somebody's only copy of their work. That is the whole risk profile, and it decides two rules here. First, every destructive action writes a recovery reference before it runs. Before a hard reset, a rebase, a branch deletion, a discard or a force push, create a real ref pointing at the current commit and record it with the command that is about to run. Do not lean on the reflog alone: reflog entries expire on a schedule your gc settings control, unreachable objects get pruned, and a checkout that throws away uncommitted changes was never in the reflog in the first place. Check what your own gc.reflogExpire and gc.reflogExpireUnreachable are set to before you decide how much the reflog is worth to you. Second, show the command. Every git invocation this client makes is displayed before it runs and logged after it, with its arguments and its exit code. A client that runs commands you cannot see is a client you cannot debug, and the moment something goes wrong the only useful question is what exactly was run. Uncommitted work is the case with no safety net at all, so anything that would discard it either stashes first or refuses. ### Stack (fixed, do not substitute) - Engine: The installed git binary, called with plumbing commands and machine-readable output · your config, includes, hooks and credential helpers only behave correctly inside real git - Runtime: A Node process serving a React interface, bound to the loopback address only · a program with filesystem and git access must not be reachable from the network - Parsing: git status --porcelain=v2 -z and git log with a NUL-delimited custom format · filenames legally contain spaces, quotes and newlines and the human formats hide that - Graph: Lanes computed from each commit's parent list over a bounded window · loading the whole history before the first paint is why these clients feel slow - Safety: A recovery ref plus a command log written before every destructive action · the difference between a scare and a lost afternoon ### Data model (create this before Phase 1) - repos(id, path, name, added_at, last_opened_at) · the list of folders you opened, and nothing about what is in them - settings(id, graph_window, fetch_interval_seconds, confirm_destructive, always_show_command) - recovery(id, repo_id, at, action, ref_name, head_before, argv_json, undone_at) · one row per destructive action and the ref that reverses it - command_log(id, repo_id, at, argv_json, exit_code, duration_ms, stderr_tail) · every invocation, so a surprise is explainable rather than arguable - Nothing about commits, branches, diffs or file contents is stored here. git is the store, and a cache of git is a second source of truth that will eventually disagree with the first. - The repository list holds paths. A client that copies your code into its own database has quietly become a new place your code can leak from. ### Phase 1 · Repository state, read honestly Build: Call git status --porcelain=v2 -z and parse the header lines for branch, upstream and ahead or behind counts, then the entry lines for staged, unstaged, untracked, ignored and unmerged paths. Render four groups. Refresh on a filesystem watch with a short debounce, not a timer. Trap: Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. Done when: A repository containing a file whose name has a space, a quote and a newline shows exactly three entries with correct names, tested against a fixture repository created by the test suite. Unmerged paths appear in their own group rather than as modified, tested with a conflicted merge. Branch, upstream and ahead or behind counts come from the porcelain v2 header lines and not from a second command. The view refreshes within a second of a file changing on disk, without polling git in a loop. Do not build yet: No graph, no diffs, no staging and nothing that writes to the repository yet. ### Phase 2 · A graph drawn from parents, not from dates Build: Read commits with git log in a NUL-delimited custom format carrying hash, parent hashes, ref names, author, committer date and subject, over a window of N commits with a load-more control. Assign lanes by walking the list in the order git gives you and tracking open parent slots. Trap: Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. Done when: A commit whose author date precedes its parent's still renders below that parent, tested with a repository built with forced dates. Opening a repository with a hundred thousand commits paints the first screen in under a second because only the window is read, timed. Merge commits show an edge to every parent, not just the first. Every commit row carries the ref names git reported rather than a lookup this code did separately. Do not build yet: No diff view, no staging and nothing that writes yet. ### Phase 3 · Diffs and staging, including the files that break diff views Build: Show diffs from git diff and git diff --cached with rename detection on. Stage and unstage whole files with git add and git restore --staged. Stage a selected hunk by building a patch and feeding it to git apply --cached, never by rewriting the file on disk. Trap: Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. Done when: Staging one hunk of a three-hunk file leaves the working tree file byte-identical, asserted by hashing it before and after. A file git reports as binary, or one over a configured size, is shown as a summary line and never decoded as text, tested with a large binary fixture. Renames are shown as renames rather than as a delete plus an add. Unstaging returns the index to exactly its previous state, tested by comparing git diff --cached output. Do not build yet: No commits, merges, rebases or anything that rewrites history yet. ### Phase 4 · Destructive actions with a written way back Build: Before a hard reset, a rebase, a branch delete, a discard or a force push, create a ref under your own namespace pointing at the current commit, write a recovery row with the argv, then run the command. Offer an undo that resets back to the recorded ref. Stash before a discard. Trap: Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. Done when: Every destructive action creates a ref under a configured namespace before running, asserted for each action type in the suite. Discarding uncommitted changes creates a stash and the undo restores the working tree byte-identically, tested. The exact argv is shown before execution and stored afterwards with the exit code. A destructive action whose recovery ref could not be created does not run, tested by making the ref creation fail. Do not build yet: No remote operations, signature checking or credential handling yet. ### Phase 5 · Remotes, signatures and the parts that hang Build: Fetch on a timer with cancellation and a per-command timeout. Set GIT_TERMINAL_PROMPT to zero and let the configured credential helper answer, surfacing a failure instead of asking. Read signature state with the %G? placeholder and map every value, not just the absent one. Trap: Showing a green tick whenever the signature status is anything other than N is wrong in five distinct ways, because git reports G for a good signature but also B for a bad one, U for good with unknown validity, X for good but expired, Y for good made by an expired key, R for good made by a revoked key and E for one it could not check at all; map each of the eight values to its own state, and give a tick only to G. Done when: All eight %G? values render as distinct states and only G shows as verified, tested with fixture commits or a stubbed log. The process never prompts for a password: GIT_TERMINAL_PROMPT is zero and an auth failure surfaces as an error naming the remote. A slow fetch can be cancelled and a command that exceeds its timeout is killed with the partial output kept. The repository list exports and imports as JSON so the client is replaceable without re-adding every folder. Do not build yet: Nothing after this. ### Out of scope (and why) - Reimplementing Git against a library binding, because your config includes, hooks, credential helpers and filters are part of what git does and a binding reproduces only some of them. - Hosted service integrations such as issue lists and pull request dashboards, which are a different product against APIs that change under you. - Shared cloud workspaces across many repositories, which is the paid product's actual value and needs a server, accounts and someone to run them. - An editor. Conflict resolution here opens your editor, because a half-built editor inside a Git client is the worst editor on the machine. ### README must contain - That the client shells out to your installed git and the version it was tested against - That every destructive action writes a recovery ref under a named namespace, and how to list and delete those refs - That the reflog is not the safety net here, and where to check your own gc.reflogExpire settings - That only a G signature status shows as verified, with the other seven values and what they mean - That the process binds to the loopback address and why opening it to the network would be a mistake ===== AGENTS.md ===== # Agent instructions · GitKraken indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The installed git binary, called with plumbing commands and machine-readable output, A Node process serving a React interface, bound to the loopback address only, git status --porcelain=v2 -z and git log with a NUL-delimited custom format, Lanes computed from each commit's parent list over a bounded window, A recovery ref plus a command log written before every destructive action. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. - Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. - Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. - Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. ===== BUILD_PLAN.md ===== # Build plan · GitKraken A local Git client whose hard problems are parsing real repository state without lying about filenames or signatures, drawing a commit graph from the parent list rather than from dates, and making every destructive action recoverable by something better than hope. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Repository state, read honestly The working tree view matches what git actually reports, including for files nobody expects. ### Steps 1. Parse porcelain v2 with NUL separators Split on NUL and read the record type from the first field of each entry. Files: `src/git/status.ts`, `tests/status-weird-names.test.ts` ```sh git status --porcelain=v2 -z --branch --untracked-files=all ``` 2. Build a fixture repository with hostile filenames A test that only uses readme.txt proves nothing about this code. Files: `tests/fixtures/make-repo.sh` 3. Watch the working tree instead of polling A git status every second on a large repository is a fan you can hear. Files: `src/git/watch.ts` ### Done when - [ ] A filename with a newline produces one entry, not two - [ ] Unmerged paths are shown as unmerged - [ ] Branch and ahead or behind come from the v2 header - [ ] Refresh is driven by a filesystem watch ### Watch out - Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. ## Phase 2 · A graph drawn from parents, not from dates The commit graph is correct on a repository with merges and is bounded on one with a hundred thousand commits. ### Steps 1. Read a bounded window with a NUL-delimited format One process, one parse, a known field order. Files: `src/git/log.ts` ```sh git log --max-count=500 -z --pretty=format:%H%x00%P%x00%D%x00%an%x00%cI%x00%s ``` 2. Assign lanes from the parent list in one pass Open a lane on a first parent, close it when a commit claims it. Files: `src/graph/lanes.ts`, `tests/lanes.test.ts` 3. Prove it on a repository with real merges Octopus merges and criss-cross history are where lane code falls over. Files: `tests/fixtures/merge-history.sh` ### Done when - [ ] A child with an older date still renders below its parent - [ ] First paint on a large repository is under a second - [ ] Every merge parent has a drawn edge - [ ] Ref names come from the same log call ### Watch out - Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. ## Phase 3 · Diffs and staging, including the files that break diff views Staging a hunk changes the index and nothing else, and a binary file never reaches the renderer. ### Steps 1. Stage hunks through git apply --cached The index is the only thing a staging action is allowed to change. Files: `src/git/stage.ts`, `tests/stage-hunk.test.ts` ```sh git apply --cached --unidiff-zero - ``` 2. Refuse to render binary and oversized blobs Decoding a forty megabyte binary as UTF-8 freezes the tab. Files: `src/diff/guard.ts` 3. Turn on rename detection and show it A moved file shown as a delete and an add loses its history in the eye. Files: `src/git/diff.ts` ### Done when - [ ] Hunk staging leaves the working file byte-identical - [ ] Binary and oversized files are summarised, not decoded - [ ] Renames render as renames - [ ] Unstage restores the previous index exactly ### Watch out - Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. ## Phase 4 · Destructive actions with a written way back Nothing that can lose work runs without a recovery ref and a visible command. ### Steps 1. Write the recovery ref before the command, not after If the ref write fails the action must not happen at all. Files: `src/safety/recovery.ts`, `tests/recovery.test.ts` ```sh git update-ref refs/vibe-recovery/$(date +%s) HEAD ``` 2. Stash uncommitted work before any discard It is the only case git itself cannot recover. Files: `src/safety/stash.ts` ```sh git stash push --include-untracked --message ``` 3. Show the argv, log it, and keep the exit code The first question after any surprise is what was actually run. Files: `src/git/run.ts`, `src/ui/CommandPreview.tsx` ### Done when - [ ] A recovery ref exists before every destructive command - [ ] A discard stashes and the undo restores bytes exactly - [ ] The argv is shown before and logged after - [ ] A failed ref write cancels the action ### Watch out - Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. ## Not in this build - A Git reimplementation against a library binding. - Hosted issue and pull request integrations. - Shared cloud workspaces. - A built-in editor. ## After v1, if you want it - Worktree support, which is genuinely useful and mostly a matter of teaching the repository list that one folder can have several checkouts - A commit-graph write on open for large repositories, measured before and after rather than assumed - Interactive rebase as a list you reorder, built only after the recovery refs have survived a few months of ordinary use ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The binary every command goes through. Pinned so you know what you tested. GIT_BIN=/usr/bin/git # Required. Loopback. Changing this exposes filesystem and git access to your network. BIND_ADDRESS=127.0.0.1 # Required. Repository paths, settings, recovery rows and the command log. No code. DATABASE_PATH=./data/client.db # Required. Where undo refs are written. Listed in the README so you can clean them up. RECOVERY_REF_PREFIX=refs/vibe-recovery # Required. Commits read per page. The reason a large repository opens quickly. GRAPH_WINDOW=500 # Required. Set for every child process, so git never blocks waiting for a password. GIT_TERMINAL_PROMPT=0 # Required. After this a command is killed and its partial output kept. COMMAND_TIMEOUT_MS=30000
You are building a lean indie version of GitKraken. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # GitKraken · indie build A local Git client whose hard problems are parsing real repository state without lying about filenames or signatures, drawing a commit graph from the parent list rather than from dates, and making every destructive action recoverable by something better than hope. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Engine | The installed git binary, called with plumbing commands and machine-readable output | your config, includes, hooks and credential helpers only behave correctly inside real git | | Runtime | A Node process serving a React interface, bound to the loopback address only | a program with filesystem and git access must not be reachable from the network | | Parsing | git status --porcelain=v2 -z and git log with a NUL-delimited custom format | filenames legally contain spaces, quotes and newlines and the human formats hide that | | Graph | Lanes computed from each commit's parent list over a bounded window | loading the whole history before the first paint is why these clients feel slow | | Safety | A recovery ref plus a command log written before every destructive action | the difference between a scare and a lost afternoon | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A recent git binary** · free - Why: Everything this client does is git doing it, and porcelain v2 plus the signature placeholders need a version that has them. - Get it: Your package manager, or git-scm.com. - Verify: git --version prints the version, and git status --porcelain=v2 -z runs without error - [ ] **Node.js 22 or newer** · free - Why: Runs the local process serving the interface. - Get it: nodejs.org or your version manager. - Verify: node --version prints v22 or higher - [ ] **A repository with merges, renames and a conflict** · free - Why: Lane assignment, rename detection and unmerged states are the three places this build breaks, and none of them appear in a fresh repository with four commits. - Get it: Clone something real, or generate one with a script in tests/fixtures. - Verify: It contains at least one merge with two parents and one rename git detects - [ ] **A fixture repository with awkward filenames** · free - Why: A filename containing a newline is the single fastest way to find out whether your status parser is correct, and it costs one shell script. - Get it: Create files with spaces, quotes, non-ASCII characters and a newline in the name. - Verify: git status --porcelain=v2 -z lists them and your parser agrees on the count - [ ] **Whether you will test signature states for real** · free - Why: Mapping the eight signature values is easy to get wrong and easy to test with a stub, but a real expired or revoked key is the only way to see the real output. - Get it: Decide between fixture commits with recorded output or generating a key you later revoke. - Verify: The test suite covers all eight values whichever route you take ## Quick start ```sh git status --porcelain=v2 -z --branch --untracked-files=all ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A Git reimplementation against a library binding. - Hosted issue and pull request integrations. - Shared cloud workspaces. - A built-in editor. - frontier coding model quality - large-context infrastructure - IDE-wide polish and latency - enterprise policy, telemetry, and support If one of those is essential to you, that is the reason to keep paying for GitKraken, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · GitKraken The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local Git client that reads the repository through real git and never destroys work you cannot get back to replace GitKraken. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A Git client is a program that runs destructive commands on somebody's only copy of their work. That is the whole risk profile, and it decides two rules here. First, every destructive action writes a recovery reference before it runs. Before a hard reset, a rebase, a branch deletion, a discard or a force push, create a real ref pointing at the current commit and record it with the command that is about to run. Do not lean on the reflog alone: reflog entries expire on a schedule your gc settings control, unreachable objects get pruned, and a checkout that throws away uncommitted changes was never in the reflog in the first place. Check what your own gc.reflogExpire and gc.reflogExpireUnreachable are set to before you decide how much the reflog is worth to you. Second, show the command. Every git invocation this client makes is displayed before it runs and logged after it, with its arguments and its exit code. A client that runs commands you cannot see is a client you cannot debug, and the moment something goes wrong the only useful question is what exactly was run. Uncommitted work is the case with no safety net at all, so anything that would discard it either stashes first or refuses. ### Stack (fixed, do not substitute) - Engine: The installed git binary, called with plumbing commands and machine-readable output · your config, includes, hooks and credential helpers only behave correctly inside real git - Runtime: A Node process serving a React interface, bound to the loopback address only · a program with filesystem and git access must not be reachable from the network - Parsing: git status --porcelain=v2 -z and git log with a NUL-delimited custom format · filenames legally contain spaces, quotes and newlines and the human formats hide that - Graph: Lanes computed from each commit's parent list over a bounded window · loading the whole history before the first paint is why these clients feel slow - Safety: A recovery ref plus a command log written before every destructive action · the difference between a scare and a lost afternoon ### Data model (create this before Phase 1) - repos(id, path, name, added_at, last_opened_at) · the list of folders you opened, and nothing about what is in them - settings(id, graph_window, fetch_interval_seconds, confirm_destructive, always_show_command) - recovery(id, repo_id, at, action, ref_name, head_before, argv_json, undone_at) · one row per destructive action and the ref that reverses it - command_log(id, repo_id, at, argv_json, exit_code, duration_ms, stderr_tail) · every invocation, so a surprise is explainable rather than arguable - Nothing about commits, branches, diffs or file contents is stored here. git is the store, and a cache of git is a second source of truth that will eventually disagree with the first. - The repository list holds paths. A client that copies your code into its own database has quietly become a new place your code can leak from. ### Phase 1 · Repository state, read honestly Build: Call git status --porcelain=v2 -z and parse the header lines for branch, upstream and ahead or behind counts, then the entry lines for staged, unstaged, untracked, ignored and unmerged paths. Render four groups. Refresh on a filesystem watch with a short debounce, not a timer. Trap: Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. Done when: A repository containing a file whose name has a space, a quote and a newline shows exactly three entries with correct names, tested against a fixture repository created by the test suite. Unmerged paths appear in their own group rather than as modified, tested with a conflicted merge. Branch, upstream and ahead or behind counts come from the porcelain v2 header lines and not from a second command. The view refreshes within a second of a file changing on disk, without polling git in a loop. Do not build yet: No graph, no diffs, no staging and nothing that writes to the repository yet. ### Phase 2 · A graph drawn from parents, not from dates Build: Read commits with git log in a NUL-delimited custom format carrying hash, parent hashes, ref names, author, committer date and subject, over a window of N commits with a load-more control. Assign lanes by walking the list in the order git gives you and tracking open parent slots. Trap: Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. Done when: A commit whose author date precedes its parent's still renders below that parent, tested with a repository built with forced dates. Opening a repository with a hundred thousand commits paints the first screen in under a second because only the window is read, timed. Merge commits show an edge to every parent, not just the first. Every commit row carries the ref names git reported rather than a lookup this code did separately. Do not build yet: No diff view, no staging and nothing that writes yet. ### Phase 3 · Diffs and staging, including the files that break diff views Build: Show diffs from git diff and git diff --cached with rename detection on. Stage and unstage whole files with git add and git restore --staged. Stage a selected hunk by building a patch and feeding it to git apply --cached, never by rewriting the file on disk. Trap: Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. Done when: Staging one hunk of a three-hunk file leaves the working tree file byte-identical, asserted by hashing it before and after. A file git reports as binary, or one over a configured size, is shown as a summary line and never decoded as text, tested with a large binary fixture. Renames are shown as renames rather than as a delete plus an add. Unstaging returns the index to exactly its previous state, tested by comparing git diff --cached output. Do not build yet: No commits, merges, rebases or anything that rewrites history yet. ### Phase 4 · Destructive actions with a written way back Build: Before a hard reset, a rebase, a branch delete, a discard or a force push, create a ref under your own namespace pointing at the current commit, write a recovery row with the argv, then run the command. Offer an undo that resets back to the recorded ref. Stash before a discard. Trap: Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. Done when: Every destructive action creates a ref under a configured namespace before running, asserted for each action type in the suite. Discarding uncommitted changes creates a stash and the undo restores the working tree byte-identically, tested. The exact argv is shown before execution and stored afterwards with the exit code. A destructive action whose recovery ref could not be created does not run, tested by making the ref creation fail. Do not build yet: No remote operations, signature checking or credential handling yet. ### Phase 5 · Remotes, signatures and the parts that hang Build: Fetch on a timer with cancellation and a per-command timeout. Set GIT_TERMINAL_PROMPT to zero and let the configured credential helper answer, surfacing a failure instead of asking. Read signature state with the %G? placeholder and map every value, not just the absent one. Trap: Showing a green tick whenever the signature status is anything other than N is wrong in five distinct ways, because git reports G for a good signature but also B for a bad one, U for good with unknown validity, X for good but expired, Y for good made by an expired key, R for good made by a revoked key and E for one it could not check at all; map each of the eight values to its own state, and give a tick only to G. Done when: All eight %G? values render as distinct states and only G shows as verified, tested with fixture commits or a stubbed log. The process never prompts for a password: GIT_TERMINAL_PROMPT is zero and an auth failure surfaces as an error naming the remote. A slow fetch can be cancelled and a command that exceeds its timeout is killed with the partial output kept. The repository list exports and imports as JSON so the client is replaceable without re-adding every folder. Do not build yet: Nothing after this. ### Out of scope (and why) - Reimplementing Git against a library binding, because your config includes, hooks, credential helpers and filters are part of what git does and a binding reproduces only some of them. - Hosted service integrations such as issue lists and pull request dashboards, which are a different product against APIs that change under you. - Shared cloud workspaces across many repositories, which is the paid product's actual value and needs a server, accounts and someone to run them. - An editor. Conflict resolution here opens your editor, because a half-built editor inside a Git client is the worst editor on the machine. ### README must contain - That the client shells out to your installed git and the version it was tested against - That every destructive action writes a recovery ref under a named namespace, and how to list and delete those refs - That the reflog is not the safety net here, and where to check your own gc.reflogExpire settings - That only a G signature status shows as verified, with the other seven values and what they mean - That the process binds to the loopback address and why opening it to the network would be a mistake ===== AGENTS.md ===== # Agent instructions · GitKraken indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The installed git binary, called with plumbing commands and machine-readable output, A Node process serving a React interface, bound to the loopback address only, git status --porcelain=v2 -z and git log with a NUL-delimited custom format, Lanes computed from each commit's parent list over a bounded window, A recovery ref plus a command log written before every destructive action. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. - Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. - Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. - Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. ===== BUILD_PLAN.md ===== # Build plan · GitKraken A local Git client whose hard problems are parsing real repository state without lying about filenames or signatures, drawing a commit graph from the parent list rather than from dates, and making every destructive action recoverable by something better than hope. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Repository state, read honestly The working tree view matches what git actually reports, including for files nobody expects. ### Steps 1. Parse porcelain v2 with NUL separators Split on NUL and read the record type from the first field of each entry. Files: `src/git/status.ts`, `tests/status-weird-names.test.ts` ```sh git status --porcelain=v2 -z --branch --untracked-files=all ``` 2. Build a fixture repository with hostile filenames A test that only uses readme.txt proves nothing about this code. Files: `tests/fixtures/make-repo.sh` 3. Watch the working tree instead of polling A git status every second on a large repository is a fan you can hear. Files: `src/git/watch.ts` ### Done when - [ ] A filename with a newline produces one entry, not two - [ ] Unmerged paths are shown as unmerged - [ ] Branch and ahead or behind come from the v2 header - [ ] Refresh is driven by a filesystem watch ### Watch out - Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. ## Phase 2 · A graph drawn from parents, not from dates The commit graph is correct on a repository with merges and is bounded on one with a hundred thousand commits. ### Steps 1. Read a bounded window with a NUL-delimited format One process, one parse, a known field order. Files: `src/git/log.ts` ```sh git log --max-count=500 -z --pretty=format:%H%x00%P%x00%D%x00%an%x00%cI%x00%s ``` 2. Assign lanes from the parent list in one pass Open a lane on a first parent, close it when a commit claims it. Files: `src/graph/lanes.ts`, `tests/lanes.test.ts` 3. Prove it on a repository with real merges Octopus merges and criss-cross history are where lane code falls over. Files: `tests/fixtures/merge-history.sh` ### Done when - [ ] A child with an older date still renders below its parent - [ ] First paint on a large repository is under a second - [ ] Every merge parent has a drawn edge - [ ] Ref names come from the same log call ### Watch out - Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. ## Phase 3 · Diffs and staging, including the files that break diff views Staging a hunk changes the index and nothing else, and a binary file never reaches the renderer. ### Steps 1. Stage hunks through git apply --cached The index is the only thing a staging action is allowed to change. Files: `src/git/stage.ts`, `tests/stage-hunk.test.ts` ```sh git apply --cached --unidiff-zero - ``` 2. Refuse to render binary and oversized blobs Decoding a forty megabyte binary as UTF-8 freezes the tab. Files: `src/diff/guard.ts` 3. Turn on rename detection and show it A moved file shown as a delete and an add loses its history in the eye. Files: `src/git/diff.ts` ### Done when - [ ] Hunk staging leaves the working file byte-identical - [ ] Binary and oversized files are summarised, not decoded - [ ] Renames render as renames - [ ] Unstage restores the previous index exactly ### Watch out - Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. ## Phase 4 · Destructive actions with a written way back Nothing that can lose work runs without a recovery ref and a visible command. ### Steps 1. Write the recovery ref before the command, not after If the ref write fails the action must not happen at all. Files: `src/safety/recovery.ts`, `tests/recovery.test.ts` ```sh git update-ref refs/vibe-recovery/$(date +%s) HEAD ``` 2. Stash uncommitted work before any discard It is the only case git itself cannot recover. Files: `src/safety/stash.ts` ```sh git stash push --include-untracked --message ``` 3. Show the argv, log it, and keep the exit code The first question after any surprise is what was actually run. Files: `src/git/run.ts`, `src/ui/CommandPreview.tsx` ### Done when - [ ] A recovery ref exists before every destructive command - [ ] A discard stashes and the undo restores bytes exactly - [ ] The argv is shown before and logged after - [ ] A failed ref write cancels the action ### Watch out - Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. ## Not in this build - A Git reimplementation against a library binding. - Hosted issue and pull request integrations. - Shared cloud workspaces. - A built-in editor. ## After v1, if you want it - Worktree support, which is genuinely useful and mostly a matter of teaching the repository list that one folder can have several checkouts - A commit-graph write on open for large repositories, measured before and after rather than assumed - Interactive rebase as a list you reorder, built only after the recovery refs have survived a few months of ordinary use ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The binary every command goes through. Pinned so you know what you tested. GIT_BIN=/usr/bin/git # Required. Loopback. Changing this exposes filesystem and git access to your network. BIND_ADDRESS=127.0.0.1 # Required. Repository paths, settings, recovery rows and the command log. No code. DATABASE_PATH=./data/client.db # Required. Where undo refs are written. Listed in the README so you can clean them up. RECOVERY_REF_PREFIX=refs/vibe-recovery # Required. Commits read per page. The reason a large repository opens quickly. GRAPH_WINDOW=500 # Required. Set for every child process, so git never blocks waiting for a password. GIT_TERMINAL_PROMPT=0 # Required. After this a command is killed and its partial output kept. COMMAND_TIMEOUT_MS=30000
You are building a production product version of GitKraken. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # GitKraken · product brief ## Problem GitKraken's solo core is compact: build a repository-local Git client assistant that indexes one codebase, calls one model, proposes diffs, runs tests, and records every change. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on model, context, integration. ## Product outcome A Git client that reads state the way git reports it, draws history from parents rather than dates, shows you every command before it runs, and leaves a recoverable ref behind every action that could lose work. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Python 3.12 - Git - OpenAI API key in .env ## Explicit non-goals for v1 - A Git reimplementation against a library binding. - Hosted issue and pull request integrations. - Shared cloud workspaces. - A built-in editor. - frontier coding model quality - large-context infrastructure - IDE-wide polish and latency - enterprise policy, telemetry, and support ## Success criteria - A filename containing a newline is confirmed to parse as one entry - Hunk staging is confirmed to leave the working file byte-identical - Every destructive action is confirmed to write a recovery ref first - Only the G signature status is confirmed to render as verified - No code path is confirmed to prompt for a credential ===== BRIEF.md ===== # Build brief · GitKraken The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local Git client that reads the repository through real git and never destroys work you cannot get back to replace GitKraken. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A Git client is a program that runs destructive commands on somebody's only copy of their work. That is the whole risk profile, and it decides two rules here. First, every destructive action writes a recovery reference before it runs. Before a hard reset, a rebase, a branch deletion, a discard or a force push, create a real ref pointing at the current commit and record it with the command that is about to run. Do not lean on the reflog alone: reflog entries expire on a schedule your gc settings control, unreachable objects get pruned, and a checkout that throws away uncommitted changes was never in the reflog in the first place. Check what your own gc.reflogExpire and gc.reflogExpireUnreachable are set to before you decide how much the reflog is worth to you. Second, show the command. Every git invocation this client makes is displayed before it runs and logged after it, with its arguments and its exit code. A client that runs commands you cannot see is a client you cannot debug, and the moment something goes wrong the only useful question is what exactly was run. Uncommitted work is the case with no safety net at all, so anything that would discard it either stashes first or refuses. ### Stack (fixed, do not substitute) - Engine: The installed git binary, called with plumbing commands and machine-readable output · your config, includes, hooks and credential helpers only behave correctly inside real git - Runtime: A Node process serving a React interface, bound to the loopback address only · a program with filesystem and git access must not be reachable from the network - Parsing: git status --porcelain=v2 -z and git log with a NUL-delimited custom format · filenames legally contain spaces, quotes and newlines and the human formats hide that - Graph: Lanes computed from each commit's parent list over a bounded window · loading the whole history before the first paint is why these clients feel slow - Safety: A recovery ref plus a command log written before every destructive action · the difference between a scare and a lost afternoon ### Data model (create this before Phase 1) - repos(id, path, name, added_at, last_opened_at) · the list of folders you opened, and nothing about what is in them - settings(id, graph_window, fetch_interval_seconds, confirm_destructive, always_show_command) - recovery(id, repo_id, at, action, ref_name, head_before, argv_json, undone_at) · one row per destructive action and the ref that reverses it - command_log(id, repo_id, at, argv_json, exit_code, duration_ms, stderr_tail) · every invocation, so a surprise is explainable rather than arguable - Nothing about commits, branches, diffs or file contents is stored here. git is the store, and a cache of git is a second source of truth that will eventually disagree with the first. - The repository list holds paths. A client that copies your code into its own database has quietly become a new place your code can leak from. ### Phase 1 · Repository state, read honestly Build: Call git status --porcelain=v2 -z and parse the header lines for branch, upstream and ahead or behind counts, then the entry lines for staged, unstaged, untracked, ignored and unmerged paths. Render four groups. Refresh on a filesystem watch with a short debounce, not a timer. Trap: Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. Done when: A repository containing a file whose name has a space, a quote and a newline shows exactly three entries with correct names, tested against a fixture repository created by the test suite. Unmerged paths appear in their own group rather than as modified, tested with a conflicted merge. Branch, upstream and ahead or behind counts come from the porcelain v2 header lines and not from a second command. The view refreshes within a second of a file changing on disk, without polling git in a loop. Do not build yet: No graph, no diffs, no staging and nothing that writes to the repository yet. ### Phase 2 · A graph drawn from parents, not from dates Build: Read commits with git log in a NUL-delimited custom format carrying hash, parent hashes, ref names, author, committer date and subject, over a window of N commits with a load-more control. Assign lanes by walking the list in the order git gives you and tracking open parent slots. Trap: Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. Done when: A commit whose author date precedes its parent's still renders below that parent, tested with a repository built with forced dates. Opening a repository with a hundred thousand commits paints the first screen in under a second because only the window is read, timed. Merge commits show an edge to every parent, not just the first. Every commit row carries the ref names git reported rather than a lookup this code did separately. Do not build yet: No diff view, no staging and nothing that writes yet. ### Phase 3 · Diffs and staging, including the files that break diff views Build: Show diffs from git diff and git diff --cached with rename detection on. Stage and unstage whole files with git add and git restore --staged. Stage a selected hunk by building a patch and feeding it to git apply --cached, never by rewriting the file on disk. Trap: Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. Done when: Staging one hunk of a three-hunk file leaves the working tree file byte-identical, asserted by hashing it before and after. A file git reports as binary, or one over a configured size, is shown as a summary line and never decoded as text, tested with a large binary fixture. Renames are shown as renames rather than as a delete plus an add. Unstaging returns the index to exactly its previous state, tested by comparing git diff --cached output. Do not build yet: No commits, merges, rebases or anything that rewrites history yet. ### Phase 4 · Destructive actions with a written way back Build: Before a hard reset, a rebase, a branch delete, a discard or a force push, create a ref under your own namespace pointing at the current commit, write a recovery row with the argv, then run the command. Offer an undo that resets back to the recorded ref. Stash before a discard. Trap: Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. Done when: Every destructive action creates a ref under a configured namespace before running, asserted for each action type in the suite. Discarding uncommitted changes creates a stash and the undo restores the working tree byte-identically, tested. The exact argv is shown before execution and stored afterwards with the exit code. A destructive action whose recovery ref could not be created does not run, tested by making the ref creation fail. Do not build yet: No remote operations, signature checking or credential handling yet. ### Phase 5 · Remotes, signatures and the parts that hang Build: Fetch on a timer with cancellation and a per-command timeout. Set GIT_TERMINAL_PROMPT to zero and let the configured credential helper answer, surfacing a failure instead of asking. Read signature state with the %G? placeholder and map every value, not just the absent one. Trap: Showing a green tick whenever the signature status is anything other than N is wrong in five distinct ways, because git reports G for a good signature but also B for a bad one, U for good with unknown validity, X for good but expired, Y for good made by an expired key, R for good made by a revoked key and E for one it could not check at all; map each of the eight values to its own state, and give a tick only to G. Done when: All eight %G? values render as distinct states and only G shows as verified, tested with fixture commits or a stubbed log. The process never prompts for a password: GIT_TERMINAL_PROMPT is zero and an auth failure surfaces as an error naming the remote. A slow fetch can be cancelled and a command that exceeds its timeout is killed with the partial output kept. The repository list exports and imports as JSON so the client is replaceable without re-adding every folder. Do not build yet: Nothing after this. ### Out of scope (and why) - Reimplementing Git against a library binding, because your config includes, hooks, credential helpers and filters are part of what git does and a binding reproduces only some of them. - Hosted service integrations such as issue lists and pull request dashboards, which are a different product against APIs that change under you. - Shared cloud workspaces across many repositories, which is the paid product's actual value and needs a server, accounts and someone to run them. - An editor. Conflict resolution here opens your editor, because a half-built editor inside a Git client is the worst editor on the machine. ### README must contain - That the client shells out to your installed git and the version it was tested against - That every destructive action writes a recovery ref under a named namespace, and how to list and delete those refs - That the reflog is not the safety net here, and where to check your own gc.reflogExpire settings - That only a G signature status shows as verified, with the other seven values and what they mean - That the process binds to the loopback address and why opening it to the network would be a mistake ===== ARCHITECTURE.md ===== # Architecture · GitKraken ## Stack | Part | Choice | Why | | --- | --- | --- | | Engine | The installed git binary, called with plumbing commands and machine-readable output | your config, includes, hooks and credential helpers only behave correctly inside real git | | Runtime | A Node process serving a React interface, bound to the loopback address only | a program with filesystem and git access must not be reachable from the network | | Parsing | git status --porcelain=v2 -z and git log with a NUL-delimited custom format | filenames legally contain spaces, quotes and newlines and the human formats hide that | | Graph | Lanes computed from each commit's parent list over a bounded window | loading the whole history before the first paint is why these clients feel slow | | Safety | A recovery ref plus a command log written before every destructive action | the difference between a scare and a lost afternoon | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Runner | Child process execution, timeouts, argv logging. | Any process layer, provided the argv is shown before and logged after. | | Parsers | Porcelain v2, NUL-delimited log, diff output. | Any parser, provided it reads the -z formats rather than the human ones. | | Graph | Lane assignment over a bounded window. | Any layout, provided ordering comes from git and not from dates. | | Safety | Recovery refs, stashes, the command log. | Not swappable for reliance on the reflog, which expires and misses uncommitted work. | | Interface | Rendering and keyboard handling. | Any front end, provided binary and oversized blobs never reach it. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `GIT_BIN` · required · The binary every command goes through. Pinned so you know what you tested. - `BIND_ADDRESS` · required · Loopback. Changing this exposes filesystem and git access to your network. - `DATABASE_PATH` · required · Repository paths, settings, recovery rows and the command log. No code. - `RECOVERY_REF_PREFIX` · required · Where undo refs are written. Listed in the README so you can clean them up. - `GRAPH_WINDOW` · required · Commits read per page. The reason a large repository opens quickly. - `GIT_TERMINAL_PROMPT` · required · Set for every child process, so git never blocks waiting for a password. - `COMMAND_TIMEOUT_MS` · required · After this a command is killed and its partial output kept. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · GitKraken product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: The installed git binary, called with plumbing commands and machine-readable output, A Node process serving a React interface, bound to the loopback address only, git status --porcelain=v2 -z and git log with a NUL-delimited custom format, Lanes computed from each commit's parent list over a bounded window, A recovery ref plus a command log written before every destructive action. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. - Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. - Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. - Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. - Showing a green tick whenever the signature status is anything other than N is wrong in five distinct ways, because git reports G for a good signature but also B for a bad one, U for good with unknown validity, X for good but expired, Y for good made by an expired key, R for good made by a revoked key and E for one it could not check at all; map each of the eight values to its own state, and give a tick only to G. ===== MILESTONES.md ===== # Delivery milestones · GitKraken Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Repository state, read honestly The working tree view matches what git actually reports, including for files nobody expects. ### Steps 1. Parse porcelain v2 with NUL separators Split on NUL and read the record type from the first field of each entry. Files: `src/git/status.ts`, `tests/status-weird-names.test.ts` ```sh git status --porcelain=v2 -z --branch --untracked-files=all ``` 2. Build a fixture repository with hostile filenames A test that only uses readme.txt proves nothing about this code. Files: `tests/fixtures/make-repo.sh` 3. Watch the working tree instead of polling A git status every second on a large repository is a fan you can hear. Files: `src/git/watch.ts` ### Done when - [ ] A filename with a newline produces one entry, not two - [ ] Unmerged paths are shown as unmerged - [ ] Branch and ahead or behind come from the v2 header - [ ] Refresh is driven by a filesystem watch ### Watch out - Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. ## M2 · A graph drawn from parents, not from dates The commit graph is correct on a repository with merges and is bounded on one with a hundred thousand commits. ### Steps 1. Read a bounded window with a NUL-delimited format One process, one parse, a known field order. Files: `src/git/log.ts` ```sh git log --max-count=500 -z --pretty=format:%H%x00%P%x00%D%x00%an%x00%cI%x00%s ``` 2. Assign lanes from the parent list in one pass Open a lane on a first parent, close it when a commit claims it. Files: `src/graph/lanes.ts`, `tests/lanes.test.ts` 3. Prove it on a repository with real merges Octopus merges and criss-cross history are where lane code falls over. Files: `tests/fixtures/merge-history.sh` ### Done when - [ ] A child with an older date still renders below its parent - [ ] First paint on a large repository is under a second - [ ] Every merge parent has a drawn edge - [ ] Ref names come from the same log call ### Watch out - Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. ## M3 · Diffs and staging, including the files that break diff views Staging a hunk changes the index and nothing else, and a binary file never reaches the renderer. ### Steps 1. Stage hunks through git apply --cached The index is the only thing a staging action is allowed to change. Files: `src/git/stage.ts`, `tests/stage-hunk.test.ts` ```sh git apply --cached --unidiff-zero - ``` 2. Refuse to render binary and oversized blobs Decoding a forty megabyte binary as UTF-8 freezes the tab. Files: `src/diff/guard.ts` 3. Turn on rename detection and show it A moved file shown as a delete and an add loses its history in the eye. Files: `src/git/diff.ts` ### Done when - [ ] Hunk staging leaves the working file byte-identical - [ ] Binary and oversized files are summarised, not decoded - [ ] Renames render as renames - [ ] Unstage restores the previous index exactly ### Watch out - Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. ## M4 · Destructive actions with a written way back Nothing that can lose work runs without a recovery ref and a visible command. ### Steps 1. Write the recovery ref before the command, not after If the ref write fails the action must not happen at all. Files: `src/safety/recovery.ts`, `tests/recovery.test.ts` ```sh git update-ref refs/vibe-recovery/$(date +%s) HEAD ``` 2. Stash uncommitted work before any discard It is the only case git itself cannot recover. Files: `src/safety/stash.ts` ```sh git stash push --include-untracked --message ``` 3. Show the argv, log it, and keep the exit code The first question after any surprise is what was actually run. Files: `src/git/run.ts`, `src/ui/CommandPreview.tsx` ### Done when - [ ] A recovery ref exists before every destructive command - [ ] A discard stashes and the undo restores bytes exactly - [ ] The argv is shown before and logged after - [ ] A failed ref write cancels the action ### Watch out - Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. ## M5 · Remotes, signatures and the parts that hang (production only) Fetching never blocks the interface, credentials stay with the credential helper, and a signature tick means what it says. ### Steps 1. Map all eight signature states B, U, X, Y, R and E are not the same thing as G. Files: `src/git/signatures.ts`, `tests/signatures.test.ts` ```sh git log --max-count=50 --pretty=format:%H%x00%G?%x00%GS%x00%GK ``` 2. Never prompt for credentials yourself A client that collects passwords is a password collector. Files: `src/git/run.ts`, `docs/credentials.md` 3. Timeout, cancel and keep the partial output A fetch against an unreachable remote must not freeze the window. Files: `src/git/run.ts` ### Done when - [ ] Only G renders as a verified signature - [ ] No code path prompts for a credential - [ ] Commands can be cancelled and are killed on timeout - [ ] The repository list exports and imports ### Watch out - Showing a green tick whenever the signature status is anything other than N is wrong in five distinct ways, because git reports G for a good signature but also B for a bad one, U for good with unknown validity, X for good but expired, Y for good made by an expired key, R for good made by a revoked key and E for one it could not check at all; map each of the eight values to its own state, and give a tick only to G. ===== OPERATIONS.md ===== # Operations · GitKraken ## Backup The repositories back themselves up by having remotes. What this application holds is the folder list, the settings and the command log, which is a small SQLite file worth copying but not worth worrying about. ## Restore Restore the database, confirm the repository list reappears and that recovery refs still exist in each repository. A recovery ref lives in the repository, not here, so it survives losing this database entirely. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Watch the command log for non-zero exits and for commands that hit the timeout. A rising timeout count usually means a remote is unreachable or a repository has grown past the graph window you chose. ## Incident checklist The incident in this category is lost work. Start from the command log to find what ran, then the recovery table for the ref written just before it, then git fsck for anything neither of those caught. Write this order down in the README before you need it. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A filename containing a newline is confirmed to parse as one entry - [ ] Hunk staging is confirmed to leave the working file byte-identical - [ ] Every destructive action is confirmed to write a recovery ref first - [ ] Only the G signature status is confirmed to render as verified - [ ] No code path is confirmed to prompt for a credential ## Launch constraint Do not market omitted GitKraken capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The binary every command goes through. Pinned so you know what you tested. GIT_BIN=/usr/bin/git # Required. Loopback. Changing this exposes filesystem and git access to your network. BIND_ADDRESS=127.0.0.1 # Required. Repository paths, settings, recovery rows and the command log. No code. DATABASE_PATH=./data/client.db # Required. Where undo refs are written. Listed in the README so you can clean them up. RECOVERY_REF_PREFIX=refs/vibe-recovery # Required. Commits read per page. The reason a large repository opens quickly. GRAPH_WINDOW=500 # Required. Set for every child process, so git never blocks waiting for a password. GIT_TERMINAL_PROMPT=0 # Required. After this a command is killed and its partial output kept. COMMAND_TIMEOUT_MS=30000
# GitKraken · indie build A local Git client whose hard problems are parsing real repository state without lying about filenames or signatures, drawing a commit graph from the parent list rather than from dates, and making every destructive action recoverable by something better than hope. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Engine | The installed git binary, called with plumbing commands and machine-readable output | your config, includes, hooks and credential helpers only behave correctly inside real git | | Runtime | A Node process serving a React interface, bound to the loopback address only | a program with filesystem and git access must not be reachable from the network | | Parsing | git status --porcelain=v2 -z and git log with a NUL-delimited custom format | filenames legally contain spaces, quotes and newlines and the human formats hide that | | Graph | Lanes computed from each commit's parent list over a bounded window | loading the whole history before the first paint is why these clients feel slow | | Safety | A recovery ref plus a command log written before every destructive action | the difference between a scare and a lost afternoon | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A recent git binary** · free - Why: Everything this client does is git doing it, and porcelain v2 plus the signature placeholders need a version that has them. - Get it: Your package manager, or git-scm.com. - Verify: git --version prints the version, and git status --porcelain=v2 -z runs without error - [ ] **Node.js 22 or newer** · free - Why: Runs the local process serving the interface. - Get it: nodejs.org or your version manager. - Verify: node --version prints v22 or higher - [ ] **A repository with merges, renames and a conflict** · free - Why: Lane assignment, rename detection and unmerged states are the three places this build breaks, and none of them appear in a fresh repository with four commits. - Get it: Clone something real, or generate one with a script in tests/fixtures. - Verify: It contains at least one merge with two parents and one rename git detects - [ ] **A fixture repository with awkward filenames** · free - Why: A filename containing a newline is the single fastest way to find out whether your status parser is correct, and it costs one shell script. - Get it: Create files with spaces, quotes, non-ASCII characters and a newline in the name. - Verify: git status --porcelain=v2 -z lists them and your parser agrees on the count - [ ] **Whether you will test signature states for real** · free - Why: Mapping the eight signature values is easy to get wrong and easy to test with a stub, but a real expired or revoked key is the only way to see the real output. - Get it: Decide between fixture commits with recorded output or generating a key you later revoke. - Verify: The test suite covers all eight values whichever route you take ## Quick start ```sh git status --porcelain=v2 -z --branch --untracked-files=all ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A Git reimplementation against a library binding. - Hosted issue and pull request integrations. - Shared cloud workspaces. - A built-in editor. - frontier coding model quality - large-context infrastructure - IDE-wide polish and latency - enterprise policy, telemetry, and support If one of those is essential to you, that is the reason to keep paying for GitKraken, and the README should say so rather than pretend.
# Build brief · GitKraken The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local Git client that reads the repository through real git and never destroys work you cannot get back to replace GitKraken. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A Git client is a program that runs destructive commands on somebody's only copy of their work. That is the whole risk profile, and it decides two rules here. First, every destructive action writes a recovery reference before it runs. Before a hard reset, a rebase, a branch deletion, a discard or a force push, create a real ref pointing at the current commit and record it with the command that is about to run. Do not lean on the reflog alone: reflog entries expire on a schedule your gc settings control, unreachable objects get pruned, and a checkout that throws away uncommitted changes was never in the reflog in the first place. Check what your own gc.reflogExpire and gc.reflogExpireUnreachable are set to before you decide how much the reflog is worth to you. Second, show the command. Every git invocation this client makes is displayed before it runs and logged after it, with its arguments and its exit code. A client that runs commands you cannot see is a client you cannot debug, and the moment something goes wrong the only useful question is what exactly was run. Uncommitted work is the case with no safety net at all, so anything that would discard it either stashes first or refuses. ### Stack (fixed, do not substitute) - Engine: The installed git binary, called with plumbing commands and machine-readable output · your config, includes, hooks and credential helpers only behave correctly inside real git - Runtime: A Node process serving a React interface, bound to the loopback address only · a program with filesystem and git access must not be reachable from the network - Parsing: git status --porcelain=v2 -z and git log with a NUL-delimited custom format · filenames legally contain spaces, quotes and newlines and the human formats hide that - Graph: Lanes computed from each commit's parent list over a bounded window · loading the whole history before the first paint is why these clients feel slow - Safety: A recovery ref plus a command log written before every destructive action · the difference between a scare and a lost afternoon ### Data model (create this before Phase 1) - repos(id, path, name, added_at, last_opened_at) · the list of folders you opened, and nothing about what is in them - settings(id, graph_window, fetch_interval_seconds, confirm_destructive, always_show_command) - recovery(id, repo_id, at, action, ref_name, head_before, argv_json, undone_at) · one row per destructive action and the ref that reverses it - command_log(id, repo_id, at, argv_json, exit_code, duration_ms, stderr_tail) · every invocation, so a surprise is explainable rather than arguable - Nothing about commits, branches, diffs or file contents is stored here. git is the store, and a cache of git is a second source of truth that will eventually disagree with the first. - The repository list holds paths. A client that copies your code into its own database has quietly become a new place your code can leak from. ### Phase 1 · Repository state, read honestly Build: Call git status --porcelain=v2 -z and parse the header lines for branch, upstream and ahead or behind counts, then the entry lines for staged, unstaged, untracked, ignored and unmerged paths. Render four groups. Refresh on a filesystem watch with a short debounce, not a timer. Trap: Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. Done when: A repository containing a file whose name has a space, a quote and a newline shows exactly three entries with correct names, tested against a fixture repository created by the test suite. Unmerged paths appear in their own group rather than as modified, tested with a conflicted merge. Branch, upstream and ahead or behind counts come from the porcelain v2 header lines and not from a second command. The view refreshes within a second of a file changing on disk, without polling git in a loop. Do not build yet: No graph, no diffs, no staging and nothing that writes to the repository yet. ### Phase 2 · A graph drawn from parents, not from dates Build: Read commits with git log in a NUL-delimited custom format carrying hash, parent hashes, ref names, author, committer date and subject, over a window of N commits with a load-more control. Assign lanes by walking the list in the order git gives you and tracking open parent slots. Trap: Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. Done when: A commit whose author date precedes its parent's still renders below that parent, tested with a repository built with forced dates. Opening a repository with a hundred thousand commits paints the first screen in under a second because only the window is read, timed. Merge commits show an edge to every parent, not just the first. Every commit row carries the ref names git reported rather than a lookup this code did separately. Do not build yet: No diff view, no staging and nothing that writes yet. ### Phase 3 · Diffs and staging, including the files that break diff views Build: Show diffs from git diff and git diff --cached with rename detection on. Stage and unstage whole files with git add and git restore --staged. Stage a selected hunk by building a patch and feeding it to git apply --cached, never by rewriting the file on disk. Trap: Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. Done when: Staging one hunk of a three-hunk file leaves the working tree file byte-identical, asserted by hashing it before and after. A file git reports as binary, or one over a configured size, is shown as a summary line and never decoded as text, tested with a large binary fixture. Renames are shown as renames rather than as a delete plus an add. Unstaging returns the index to exactly its previous state, tested by comparing git diff --cached output. Do not build yet: No commits, merges, rebases or anything that rewrites history yet. ### Phase 4 · Destructive actions with a written way back Build: Before a hard reset, a rebase, a branch delete, a discard or a force push, create a ref under your own namespace pointing at the current commit, write a recovery row with the argv, then run the command. Offer an undo that resets back to the recorded ref. Stash before a discard. Trap: Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. Done when: Every destructive action creates a ref under a configured namespace before running, asserted for each action type in the suite. Discarding uncommitted changes creates a stash and the undo restores the working tree byte-identically, tested. The exact argv is shown before execution and stored afterwards with the exit code. A destructive action whose recovery ref could not be created does not run, tested by making the ref creation fail. Do not build yet: No remote operations, signature checking or credential handling yet. ### Phase 5 · Remotes, signatures and the parts that hang Build: Fetch on a timer with cancellation and a per-command timeout. Set GIT_TERMINAL_PROMPT to zero and let the configured credential helper answer, surfacing a failure instead of asking. Read signature state with the %G? placeholder and map every value, not just the absent one. Trap: Showing a green tick whenever the signature status is anything other than N is wrong in five distinct ways, because git reports G for a good signature but also B for a bad one, U for good with unknown validity, X for good but expired, Y for good made by an expired key, R for good made by a revoked key and E for one it could not check at all; map each of the eight values to its own state, and give a tick only to G. Done when: All eight %G? values render as distinct states and only G shows as verified, tested with fixture commits or a stubbed log. The process never prompts for a password: GIT_TERMINAL_PROMPT is zero and an auth failure surfaces as an error naming the remote. A slow fetch can be cancelled and a command that exceeds its timeout is killed with the partial output kept. The repository list exports and imports as JSON so the client is replaceable without re-adding every folder. Do not build yet: Nothing after this. ### Out of scope (and why) - Reimplementing Git against a library binding, because your config includes, hooks, credential helpers and filters are part of what git does and a binding reproduces only some of them. - Hosted service integrations such as issue lists and pull request dashboards, which are a different product against APIs that change under you. - Shared cloud workspaces across many repositories, which is the paid product's actual value and needs a server, accounts and someone to run them. - An editor. Conflict resolution here opens your editor, because a half-built editor inside a Git client is the worst editor on the machine. ### README must contain - That the client shells out to your installed git and the version it was tested against - That every destructive action writes a recovery ref under a named namespace, and how to list and delete those refs - That the reflog is not the safety net here, and where to check your own gc.reflogExpire settings - That only a G signature status shows as verified, with the other seven values and what they mean - That the process binds to the loopback address and why opening it to the network would be a mistake
# Agent instructions · GitKraken indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The installed git binary, called with plumbing commands and machine-readable output, A Node process serving a React interface, bound to the loopback address only, git status --porcelain=v2 -z and git log with a NUL-delimited custom format, Lanes computed from each commit's parent list over a bounded window, A recovery ref plus a command log written before every destructive action. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. - Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. - Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. - Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither.
# Build plan · GitKraken A local Git client whose hard problems are parsing real repository state without lying about filenames or signatures, drawing a commit graph from the parent list rather than from dates, and making every destructive action recoverable by something better than hope. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Repository state, read honestly The working tree view matches what git actually reports, including for files nobody expects. ### Steps 1. Parse porcelain v2 with NUL separators Split on NUL and read the record type from the first field of each entry. Files: `src/git/status.ts`, `tests/status-weird-names.test.ts` ```sh git status --porcelain=v2 -z --branch --untracked-files=all ``` 2. Build a fixture repository with hostile filenames A test that only uses readme.txt proves nothing about this code. Files: `tests/fixtures/make-repo.sh` 3. Watch the working tree instead of polling A git status every second on a large repository is a fan you can hear. Files: `src/git/watch.ts` ### Done when - [ ] A filename with a newline produces one entry, not two - [ ] Unmerged paths are shown as unmerged - [ ] Branch and ahead or behind come from the v2 header - [ ] Refresh is driven by a filesystem watch ### Watch out - Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. ## Phase 2 · A graph drawn from parents, not from dates The commit graph is correct on a repository with merges and is bounded on one with a hundred thousand commits. ### Steps 1. Read a bounded window with a NUL-delimited format One process, one parse, a known field order. Files: `src/git/log.ts` ```sh git log --max-count=500 -z --pretty=format:%H%x00%P%x00%D%x00%an%x00%cI%x00%s ``` 2. Assign lanes from the parent list in one pass Open a lane on a first parent, close it when a commit claims it. Files: `src/graph/lanes.ts`, `tests/lanes.test.ts` 3. Prove it on a repository with real merges Octopus merges and criss-cross history are where lane code falls over. Files: `tests/fixtures/merge-history.sh` ### Done when - [ ] A child with an older date still renders below its parent - [ ] First paint on a large repository is under a second - [ ] Every merge parent has a drawn edge - [ ] Ref names come from the same log call ### Watch out - Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. ## Phase 3 · Diffs and staging, including the files that break diff views Staging a hunk changes the index and nothing else, and a binary file never reaches the renderer. ### Steps 1. Stage hunks through git apply --cached The index is the only thing a staging action is allowed to change. Files: `src/git/stage.ts`, `tests/stage-hunk.test.ts` ```sh git apply --cached --unidiff-zero - ``` 2. Refuse to render binary and oversized blobs Decoding a forty megabyte binary as UTF-8 freezes the tab. Files: `src/diff/guard.ts` 3. Turn on rename detection and show it A moved file shown as a delete and an add loses its history in the eye. Files: `src/git/diff.ts` ### Done when - [ ] Hunk staging leaves the working file byte-identical - [ ] Binary and oversized files are summarised, not decoded - [ ] Renames render as renames - [ ] Unstage restores the previous index exactly ### Watch out - Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. ## Phase 4 · Destructive actions with a written way back Nothing that can lose work runs without a recovery ref and a visible command. ### Steps 1. Write the recovery ref before the command, not after If the ref write fails the action must not happen at all. Files: `src/safety/recovery.ts`, `tests/recovery.test.ts` ```sh git update-ref refs/vibe-recovery/$(date +%s) HEAD ``` 2. Stash uncommitted work before any discard It is the only case git itself cannot recover. Files: `src/safety/stash.ts` ```sh git stash push --include-untracked --message ``` 3. Show the argv, log it, and keep the exit code The first question after any surprise is what was actually run. Files: `src/git/run.ts`, `src/ui/CommandPreview.tsx` ### Done when - [ ] A recovery ref exists before every destructive command - [ ] A discard stashes and the undo restores bytes exactly - [ ] The argv is shown before and logged after - [ ] A failed ref write cancels the action ### Watch out - Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. ## Not in this build - A Git reimplementation against a library binding. - Hosted issue and pull request integrations. - Shared cloud workspaces. - A built-in editor. ## After v1, if you want it - Worktree support, which is genuinely useful and mostly a matter of teaching the repository list that one folder can have several checkouts - A commit-graph write on open for large repositories, measured before and after rather than assumed - Interactive rebase as a list you reorder, built only after the recovery refs have survived a few months of ordinary use
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The binary every command goes through. Pinned so you know what you tested. GIT_BIN=/usr/bin/git # Required. Loopback. Changing this exposes filesystem and git access to your network. BIND_ADDRESS=127.0.0.1 # Required. Repository paths, settings, recovery rows and the command log. No code. DATABASE_PATH=./data/client.db # Required. Where undo refs are written. Listed in the README so you can clean them up. RECOVERY_REF_PREFIX=refs/vibe-recovery # Required. Commits read per page. The reason a large repository opens quickly. GRAPH_WINDOW=500 # Required. Set for every child process, so git never blocks waiting for a password. GIT_TERMINAL_PROMPT=0 # Required. After this a command is killed and its partial output kept. COMMAND_TIMEOUT_MS=30000
# GitKraken · product brief ## Problem GitKraken's solo core is compact: build a repository-local Git client assistant that indexes one codebase, calls one model, proposes diffs, runs tests, and records every change. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on model, context, integration. ## Product outcome A Git client that reads state the way git reports it, draws history from parents rather than dates, shows you every command before it runs, and leaves a recoverable ref behind every action that could lose work. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Python 3.12 - Git - OpenAI API key in .env ## Explicit non-goals for v1 - A Git reimplementation against a library binding. - Hosted issue and pull request integrations. - Shared cloud workspaces. - A built-in editor. - frontier coding model quality - large-context infrastructure - IDE-wide polish and latency - enterprise policy, telemetry, and support ## Success criteria - A filename containing a newline is confirmed to parse as one entry - Hunk staging is confirmed to leave the working file byte-identical - Every destructive action is confirmed to write a recovery ref first - Only the G signature status is confirmed to render as verified - No code path is confirmed to prompt for a credential
# Build brief · GitKraken The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local Git client that reads the repository through real git and never destroys work you cannot get back to replace GitKraken. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A Git client is a program that runs destructive commands on somebody's only copy of their work. That is the whole risk profile, and it decides two rules here. First, every destructive action writes a recovery reference before it runs. Before a hard reset, a rebase, a branch deletion, a discard or a force push, create a real ref pointing at the current commit and record it with the command that is about to run. Do not lean on the reflog alone: reflog entries expire on a schedule your gc settings control, unreachable objects get pruned, and a checkout that throws away uncommitted changes was never in the reflog in the first place. Check what your own gc.reflogExpire and gc.reflogExpireUnreachable are set to before you decide how much the reflog is worth to you. Second, show the command. Every git invocation this client makes is displayed before it runs and logged after it, with its arguments and its exit code. A client that runs commands you cannot see is a client you cannot debug, and the moment something goes wrong the only useful question is what exactly was run. Uncommitted work is the case with no safety net at all, so anything that would discard it either stashes first or refuses. ### Stack (fixed, do not substitute) - Engine: The installed git binary, called with plumbing commands and machine-readable output · your config, includes, hooks and credential helpers only behave correctly inside real git - Runtime: A Node process serving a React interface, bound to the loopback address only · a program with filesystem and git access must not be reachable from the network - Parsing: git status --porcelain=v2 -z and git log with a NUL-delimited custom format · filenames legally contain spaces, quotes and newlines and the human formats hide that - Graph: Lanes computed from each commit's parent list over a bounded window · loading the whole history before the first paint is why these clients feel slow - Safety: A recovery ref plus a command log written before every destructive action · the difference between a scare and a lost afternoon ### Data model (create this before Phase 1) - repos(id, path, name, added_at, last_opened_at) · the list of folders you opened, and nothing about what is in them - settings(id, graph_window, fetch_interval_seconds, confirm_destructive, always_show_command) - recovery(id, repo_id, at, action, ref_name, head_before, argv_json, undone_at) · one row per destructive action and the ref that reverses it - command_log(id, repo_id, at, argv_json, exit_code, duration_ms, stderr_tail) · every invocation, so a surprise is explainable rather than arguable - Nothing about commits, branches, diffs or file contents is stored here. git is the store, and a cache of git is a second source of truth that will eventually disagree with the first. - The repository list holds paths. A client that copies your code into its own database has quietly become a new place your code can leak from. ### Phase 1 · Repository state, read honestly Build: Call git status --porcelain=v2 -z and parse the header lines for branch, upstream and ahead or behind counts, then the entry lines for staged, unstaged, untracked, ignored and unmerged paths. Render four groups. Refresh on a filesystem watch with a short debounce, not a timer. Trap: Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. Done when: A repository containing a file whose name has a space, a quote and a newline shows exactly three entries with correct names, tested against a fixture repository created by the test suite. Unmerged paths appear in their own group rather than as modified, tested with a conflicted merge. Branch, upstream and ahead or behind counts come from the porcelain v2 header lines and not from a second command. The view refreshes within a second of a file changing on disk, without polling git in a loop. Do not build yet: No graph, no diffs, no staging and nothing that writes to the repository yet. ### Phase 2 · A graph drawn from parents, not from dates Build: Read commits with git log in a NUL-delimited custom format carrying hash, parent hashes, ref names, author, committer date and subject, over a window of N commits with a load-more control. Assign lanes by walking the list in the order git gives you and tracking open parent slots. Trap: Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. Done when: A commit whose author date precedes its parent's still renders below that parent, tested with a repository built with forced dates. Opening a repository with a hundred thousand commits paints the first screen in under a second because only the window is read, timed. Merge commits show an edge to every parent, not just the first. Every commit row carries the ref names git reported rather than a lookup this code did separately. Do not build yet: No diff view, no staging and nothing that writes yet. ### Phase 3 · Diffs and staging, including the files that break diff views Build: Show diffs from git diff and git diff --cached with rename detection on. Stage and unstage whole files with git add and git restore --staged. Stage a selected hunk by building a patch and feeding it to git apply --cached, never by rewriting the file on disk. Trap: Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. Done when: Staging one hunk of a three-hunk file leaves the working tree file byte-identical, asserted by hashing it before and after. A file git reports as binary, or one over a configured size, is shown as a summary line and never decoded as text, tested with a large binary fixture. Renames are shown as renames rather than as a delete plus an add. Unstaging returns the index to exactly its previous state, tested by comparing git diff --cached output. Do not build yet: No commits, merges, rebases or anything that rewrites history yet. ### Phase 4 · Destructive actions with a written way back Build: Before a hard reset, a rebase, a branch delete, a discard or a force push, create a ref under your own namespace pointing at the current commit, write a recovery row with the argv, then run the command. Offer an undo that resets back to the recorded ref. Stash before a discard. Trap: Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. Done when: Every destructive action creates a ref under a configured namespace before running, asserted for each action type in the suite. Discarding uncommitted changes creates a stash and the undo restores the working tree byte-identically, tested. The exact argv is shown before execution and stored afterwards with the exit code. A destructive action whose recovery ref could not be created does not run, tested by making the ref creation fail. Do not build yet: No remote operations, signature checking or credential handling yet. ### Phase 5 · Remotes, signatures and the parts that hang Build: Fetch on a timer with cancellation and a per-command timeout. Set GIT_TERMINAL_PROMPT to zero and let the configured credential helper answer, surfacing a failure instead of asking. Read signature state with the %G? placeholder and map every value, not just the absent one. Trap: Showing a green tick whenever the signature status is anything other than N is wrong in five distinct ways, because git reports G for a good signature but also B for a bad one, U for good with unknown validity, X for good but expired, Y for good made by an expired key, R for good made by a revoked key and E for one it could not check at all; map each of the eight values to its own state, and give a tick only to G. Done when: All eight %G? values render as distinct states and only G shows as verified, tested with fixture commits or a stubbed log. The process never prompts for a password: GIT_TERMINAL_PROMPT is zero and an auth failure surfaces as an error naming the remote. A slow fetch can be cancelled and a command that exceeds its timeout is killed with the partial output kept. The repository list exports and imports as JSON so the client is replaceable without re-adding every folder. Do not build yet: Nothing after this. ### Out of scope (and why) - Reimplementing Git against a library binding, because your config includes, hooks, credential helpers and filters are part of what git does and a binding reproduces only some of them. - Hosted service integrations such as issue lists and pull request dashboards, which are a different product against APIs that change under you. - Shared cloud workspaces across many repositories, which is the paid product's actual value and needs a server, accounts and someone to run them. - An editor. Conflict resolution here opens your editor, because a half-built editor inside a Git client is the worst editor on the machine. ### README must contain - That the client shells out to your installed git and the version it was tested against - That every destructive action writes a recovery ref under a named namespace, and how to list and delete those refs - That the reflog is not the safety net here, and where to check your own gc.reflogExpire settings - That only a G signature status shows as verified, with the other seven values and what they mean - That the process binds to the loopback address and why opening it to the network would be a mistake
# Architecture · GitKraken ## Stack | Part | Choice | Why | | --- | --- | --- | | Engine | The installed git binary, called with plumbing commands and machine-readable output | your config, includes, hooks and credential helpers only behave correctly inside real git | | Runtime | A Node process serving a React interface, bound to the loopback address only | a program with filesystem and git access must not be reachable from the network | | Parsing | git status --porcelain=v2 -z and git log with a NUL-delimited custom format | filenames legally contain spaces, quotes and newlines and the human formats hide that | | Graph | Lanes computed from each commit's parent list over a bounded window | loading the whole history before the first paint is why these clients feel slow | | Safety | A recovery ref plus a command log written before every destructive action | the difference between a scare and a lost afternoon | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Runner | Child process execution, timeouts, argv logging. | Any process layer, provided the argv is shown before and logged after. | | Parsers | Porcelain v2, NUL-delimited log, diff output. | Any parser, provided it reads the -z formats rather than the human ones. | | Graph | Lane assignment over a bounded window. | Any layout, provided ordering comes from git and not from dates. | | Safety | Recovery refs, stashes, the command log. | Not swappable for reliance on the reflog, which expires and misses uncommitted work. | | Interface | Rendering and keyboard handling. | Any front end, provided binary and oversized blobs never reach it. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `GIT_BIN` · required · The binary every command goes through. Pinned so you know what you tested. - `BIND_ADDRESS` · required · Loopback. Changing this exposes filesystem and git access to your network. - `DATABASE_PATH` · required · Repository paths, settings, recovery rows and the command log. No code. - `RECOVERY_REF_PREFIX` · required · Where undo refs are written. Listed in the README so you can clean them up. - `GRAPH_WINDOW` · required · Commits read per page. The reason a large repository opens quickly. - `GIT_TERMINAL_PROMPT` · required · Set for every child process, so git never blocks waiting for a password. - `COMMAND_TIMEOUT_MS` · required · After this a command is killed and its partial output kept. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · GitKraken product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: The installed git binary, called with plumbing commands and machine-readable output, A Node process serving a React interface, bound to the loopback address only, git status --porcelain=v2 -z and git log with a NUL-delimited custom format, Lanes computed from each commit's parent list over a bounded window, A recovery ref plus a command log written before every destructive action. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. - Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. - Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. - Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. - Showing a green tick whenever the signature status is anything other than N is wrong in five distinct ways, because git reports G for a good signature but also B for a bad one, U for good with unknown validity, X for good but expired, Y for good made by an expired key, R for good made by a revoked key and E for one it could not check at all; map each of the eight values to its own state, and give a tick only to G.
# Delivery milestones · GitKraken Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Repository state, read honestly The working tree view matches what git actually reports, including for files nobody expects. ### Steps 1. Parse porcelain v2 with NUL separators Split on NUL and read the record type from the first field of each entry. Files: `src/git/status.ts`, `tests/status-weird-names.test.ts` ```sh git status --porcelain=v2 -z --branch --untracked-files=all ``` 2. Build a fixture repository with hostile filenames A test that only uses readme.txt proves nothing about this code. Files: `tests/fixtures/make-repo.sh` 3. Watch the working tree instead of polling A git status every second on a large repository is a fan you can hear. Files: `src/git/watch.ts` ### Done when - [ ] A filename with a newline produces one entry, not two - [ ] Unmerged paths are shown as unmerged - [ ] Branch and ahead or behind come from the v2 header - [ ] Refresh is driven by a filesystem watch ### Watch out - Parsing git status --porcelain by splitting the output on newlines breaks on a filename containing a newline, which is legal on every filesystem you will meet, and without -z git quotes unusual pathnames the way core.quotePath describes, so you also get backslash escapes you then render as literal text; the docs say plainly that there is an alternate -z format recommended for machine parsing, so use --porcelain=v2 -z and split on NUL. ## M2 · A graph drawn from parents, not from dates The commit graph is correct on a repository with merges and is bounded on one with a hundred thousand commits. ### Steps 1. Read a bounded window with a NUL-delimited format One process, one parse, a known field order. Files: `src/git/log.ts` ```sh git log --max-count=500 -z --pretty=format:%H%x00%P%x00%D%x00%an%x00%cI%x00%s ``` 2. Assign lanes from the parent list in one pass Open a lane on a first parent, close it when a commit claims it. Files: `src/graph/lanes.ts`, `tests/lanes.test.ts` 3. Prove it on a repository with real merges Octopus merges and criss-cross history are where lane code falls over. Files: `tests/fixtures/merge-history.sh` ### Done when - [ ] A child with an older date still renders below its parent - [ ] First paint on a large repository is under a second - [ ] Every merge parent has a drawn edge - [ ] Ref names come from the same log call ### Watch out - Sorting the commit list by date to draw it produces a graph where a commit appears above its own parent, because author dates are whatever the committing machine said they were and a rebase or a wrong clock makes a child older than its parent; take the order git returns, which is already topological enough to draw, and treat dates as a label rather than as an ordering. ## M3 · Diffs and staging, including the files that break diff views Staging a hunk changes the index and nothing else, and a binary file never reaches the renderer. ### Steps 1. Stage hunks through git apply --cached The index is the only thing a staging action is allowed to change. Files: `src/git/stage.ts`, `tests/stage-hunk.test.ts` ```sh git apply --cached --unidiff-zero - ``` 2. Refuse to render binary and oversized blobs Decoding a forty megabyte binary as UTF-8 freezes the tab. Files: `src/diff/guard.ts` 3. Turn on rename detection and show it A moved file shown as a delete and an add loses its history in the eye. Files: `src/git/diff.ts` ### Done when - [ ] Hunk staging leaves the working file byte-identical - [ ] Binary and oversized files are summarised, not decoded - [ ] Renames render as renames - [ ] Unstage restores the previous index exactly ### Watch out - Applying a partially staged hunk by writing a modified copy of the file back to the working tree destroys whatever else the user had unsaved in that file and quietly changes line endings on a repository with autocrlf set; build a patch containing only the selected hunk and apply it to the index with git apply --cached, so the working tree is never touched by a staging operation. ## M4 · Destructive actions with a written way back Nothing that can lose work runs without a recovery ref and a visible command. ### Steps 1. Write the recovery ref before the command, not after If the ref write fails the action must not happen at all. Files: `src/safety/recovery.ts`, `tests/recovery.test.ts` ```sh git update-ref refs/vibe-recovery/$(date +%s) HEAD ``` 2. Stash uncommitted work before any discard It is the only case git itself cannot recover. Files: `src/safety/stash.ts` ```sh git stash push --include-untracked --message ``` 3. Show the argv, log it, and keep the exit code The first question after any surprise is what was actually run. Files: `src/git/run.ts`, `src/ui/CommandPreview.tsx` ### Done when - [ ] A recovery ref exists before every destructive command - [ ] A discard stashes and the undo restores bytes exactly - [ ] The argv is shown before and logged after - [ ] A failed ref write cancels the action ### Watch out - Treating the reflog as the undo feature means the day someone discards uncommitted changes there is nothing to recover, because those changes were never an object git knew about, and the day someone comes back a month later the reflog entry has expired under the repository's own gc settings; write your own ref for committed state, stash for uncommitted state, and never offer a destructive action that has neither. ## M5 · Remotes, signatures and the parts that hang (production only) Fetching never blocks the interface, credentials stay with the credential helper, and a signature tick means what it says. ### Steps 1. Map all eight signature states B, U, X, Y, R and E are not the same thing as G. Files: `src/git/signatures.ts`, `tests/signatures.test.ts` ```sh git log --max-count=50 --pretty=format:%H%x00%G?%x00%GS%x00%GK ``` 2. Never prompt for credentials yourself A client that collects passwords is a password collector. Files: `src/git/run.ts`, `docs/credentials.md` 3. Timeout, cancel and keep the partial output A fetch against an unreachable remote must not freeze the window. Files: `src/git/run.ts` ### Done when - [ ] Only G renders as a verified signature - [ ] No code path prompts for a credential - [ ] Commands can be cancelled and are killed on timeout - [ ] The repository list exports and imports ### Watch out - Showing a green tick whenever the signature status is anything other than N is wrong in five distinct ways, because git reports G for a good signature but also B for a bad one, U for good with unknown validity, X for good but expired, Y for good made by an expired key, R for good made by a revoked key and E for one it could not check at all; map each of the eight values to its own state, and give a tick only to G.
# Operations · GitKraken ## Backup The repositories back themselves up by having remotes. What this application holds is the folder list, the settings and the command log, which is a small SQLite file worth copying but not worth worrying about. ## Restore Restore the database, confirm the repository list reappears and that recovery refs still exist in each repository. A recovery ref lives in the repository, not here, so it survives losing this database entirely. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Watch the command log for non-zero exits and for commands that hit the timeout. A rising timeout count usually means a remote is unreachable or a repository has grown past the graph window you chose. ## Incident checklist The incident in this category is lost work. Start from the command log to find what ran, then the recovery table for the ref written just before it, then git fsck for anything neither of those caught. Write this order down in the README before you need it. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A filename containing a newline is confirmed to parse as one entry - [ ] Hunk staging is confirmed to leave the working file byte-identical - [ ] Every destructive action is confirmed to write a recovery ref first - [ ] Only the G signature status is confirmed to render as verified - [ ] No code path is confirmed to prompt for a credential ## Launch constraint Do not market omitted GitKraken capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The binary every command goes through. Pinned so you know what you tested. GIT_BIN=/usr/bin/git # Required. Loopback. Changing this exposes filesystem and git access to your network. BIND_ADDRESS=127.0.0.1 # Required. Repository paths, settings, recovery rows and the command log. No code. DATABASE_PATH=./data/client.db # Required. Where undo refs are written. Listed in the README so you can clean them up. RECOVERY_REF_PREFIX=refs/vibe-recovery # Required. Commits read per page. The reason a large repository opens quickly. GRAPH_WINDOW=500 # Required. Set for every child process, so git never blocks waiting for a password. GIT_TERMINAL_PROMPT=0 # Required. After this a command is killed and its partial output kept. COMMAND_TIMEOUT_MS=30000
$ choose a build depth, inspect the files, then open the complete pack in your agent
GitKraken: Developers pay for reliable context assembly, fast models, editor integration, evaluations, and safe handling of complex repositories.
xfrontier coding model quality
xlarge-context infrastructure
xIDE-wide polish and latency
xenterprise policy, telemetry, and support
Don't feel like building it? These folks already made it free.
all 6 free alternatives to GitKraken →· no votes, no pay-to-list · just what's real
GitKraken pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| community | $0/user | $0/user | Maximum 1 seat; local repositories and public remotes; no bundled GitKraken AI credits. |
| pro | — | $10/user | Up to 2 seats; private repositories; up to 1,000,000 AI credits/user/week. |
| advanced | — | $14/user | Up to 10 seats; 2,000,000 AI credits/user/week; 20% of each seat's weekly credits enters an organization pool. |
| business | — | $18/user | Up to 100 seats; 3,000,000 AI credits/user/week; 20% of each seat's weekly credits enters an organization pool. |
| enterprise | custom | — | 20+ seats; up to 4,000,000 AI credits/user/week; custom terms and support. |
free tier1 seat; local repositories and public remotes only; 0 bundled GitKraken AI credits
billingpaid plans are annual only; new seats are prorated to the shared annual renewal date
hidden costsAI credits reset each Sunday UTC with no rollover; optional add-on credit pricing is not publicly stated; listed prices exclude tax.
verified 2026-08-14 · source ↗
Vibecode GitKraken
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal GitKraken replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does GitKraken cost?
GitKraken's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing GitKraken?
Honestly: frontier coding model quality; large-context infrastructure; IDE-wide polish and latency; enterprise policy, telemetry, and support. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to GitKraken?
Yes: Lazygit (Git's common chores in a terminal UI; mouse polish not included.) GitHub Desktop (A calm visual Git client that stops before workspaces become a product category.) SourceGit (A small cross-platform Git GUI that mostly minds its own business.) All 6 curated free alternatives are at vibecodeit.com/gitkraken/alternatives. The prompt is for when you want it exactly your way.