A local-first macOS command center for running and coordinating multiple Grok Build sessions across your projects.
Open a project folder, spread several independent grok terminals across a
pane grid, and hand work to them from a Kanban board. Agents are teammates, not
tools: they plan, code, and review while you stay in the loop.
Everything runs on your machine. There is no mandatory cloud dependency and no
telemetry; workspace state lives in ~/.grokspace. Host and renderer errors are
appended to ~/.grokspace/logs/grokspace.log on this machine only. Permission
answers are appended to ~/.grokspace/ledgers/<project-id>.jsonl (Settings
replays the last 20). Nothing is uploaded.
Status: Phase 5 done. Projects, a multi-pane terminal grid, a live graph per session, a task board that hands work to an agent, agents driven over ACP that report what they are doing (including a transcript of what they say), a project memory every session reads, role presets that can be launched as a swarm, a command palette, settings, a diff panel, and per-agent git worktrees that merge back into the project. What remains of Phase 4 is the signed, notarized release — see Roadmap.
This is an early public, v0.1-track macOS app: build it from source. There is no signed or notarized download yet, and it is not an App Store build. A few checks still need a Mac with a licensed
grok— see issue #8.
GrokSpace is a local-first macOS command center for running and coordinating
several Grok Build coding agents across your projects: a grid of real
terminals, a Kanban board that hands work to ACP-driven agents, per-agent git
worktrees that merge back through the app, a permission gate that never
silently becomes always-approve, and a shared project memory. Tauri 2 + Rust +
React. No telemetry; everything stays in ~/.grokspace.
The longer documents — file contracts, the IPC inventory, the grok CLI
surface, releasing — are indexed in docs/README.md, which
also has a one-page map of the host / renderer / ACP boundary.
- Project management — open any folder as a project, switch between them, rename them, and remove them from the workspace. Removing a project only forgets it; nothing on disk is touched.
- Terminal grid — run several independent sessions side by side in a
1x1,2x1,2x2, or3x2grid, or expand one to fill the window. The layout is remembered per project. - Real terminals — each pane is a genuine pty, so full-screen TUIs work:
Grok Build's own interface, but equally
vim,htop, or anything else. Panes start in the project folder and reflow when the window resizes. - Session lifecycle — start a Grok agent or a plain shell in any pane, then stop, restart, rename, clear, or close it. Sessions left running when the app quits come back marked as stopped, ready to restart.
- Agents that report themselves — a session can be a Grok agent driven over
ACP instead of a terminal. It holds no pane, and in exchange it says whether it
is
running,idle, orneeds_input; a permission it is blocked on appears on the card of the task it concerns. Under the chips, the session title, thedoingstep, the worktree path, and overlapping diff paths are listed when those are already known — missing pieces are omitted, not invented. Allow isallow_onceonly;allow_alwaysandreject_alwaysare separate chips using the name the agent sent, so Allow can never silently become always-approve. After Allow, Also this session:Edit src/**opts into matching file-tool prompts in that session, still answered as Allow once.*and any Bash are refused. The lease dies on Stop or Restart (a new id) and is never persisted asallow_always. A pane chip plan | ask | acceptEdits records host intent: plan is Spec, ask is today's chips, acceptEdits auto-grants that edit-class lease (stillallow_once). There is no yolo chip, and the mode is not passed tosession/neworgrok agent --permission-mode. A named permission policy file of user-authored globs (~/.grokspace/permission-policy.json, and optionally<project>/.grokspace/permission-policy.json) can auto-Deny or allow-once-similar before chips appear. Deny wins; allow-once-similar cannot widen a deny and never becomes Always. A bad glob is skipped, not treated as Always. Deny is a reject-once chip. Its words, thoughts, tools, and plans surface as a transcript on the Graph tab and the Tasks rail, with Cancel while it is working and a follow-up field while it is idle. A long transcript keeps the latest rows on screen; earlier lines unmount until you ask for them. Five identical tool calls in a row (same text — different args do not count) raise a host Pause / Continue strip: Pause cancels the turn, Continue dismisses the strip and resets the count. A terminal can only ever reportrunningorstopped, because a pty carries pixels. - Live graphs, one per session — every session has its own graph file, and
the panel redraws the moment an agent writes to it. Watch a plan from the Graph
tab, or flip a single pane from
termtographorstepsand keep working in the others. The workspace Tasks tab is the Kanban board; the pane face is this session's step list. - Task board — a Kanban board per project, in the columns
backlog,in_progress,review, anddone. Cards can be dragged between columns or moved with the arrows on the card, and each card can be handed to an agent: one already running, a fresh terminal in a free pane, or a new ACP agent, which needs no pane at all. Dispatching records which session took the task. A Coder-ish dispatch (Coder, or no role) while the Diff isolation map lists overlapping paths offers Scout, Spec, or Dispatch anyway; a clean tree does not. Double-click a description to edit it. - Session steps — each Grok or agent session can propose a short working list beside the board. Build (Approve) locks the step titles and the graph node titles the panel is showing; completing steps does not move the Kanban card. The rail warns when graph node labels or ids disagree with step titles (layout is ignored; both empty is not drift; comparison stops at 20).
- Shared project memory — one memory per project, in the columns
context,decisions,notes, andartifacts. It is projected into a Markdown file every session is told to read, so what you would otherwise repeat to each agent gets said once. A bundled skill is what teaches agents to read it before they plan. - Roles, and swarms of them — a session can be started as a Planner, Coder,
Reviewer, Tester, or Scout. The role is remembered, titles the session, is exported
as
GROKSPACE_SESSION_ROLE, and is what the agent is told first. One click starts one agent per chosen role, each briefed for its job; a role that will not start is named rather than losing the rest. Palette Hand to Coder / Hand to Reviewer starts that role (or prompts an idle one) with the source graph path (read-only), approved step titles, and a ≤2 KiB transcript excerpt; the source is left idle. Palette Continue this job is Restart-shaped (new GrokSpace id, same worktree, new ACP handshake, never--resume) with a host brief from the previous graph path, step titles, and that same excerpt cap. Each named role has a closed capability profile (Planner/Scout: read plus graph, steps, and memory paths; Coder: worktree write; Reviewer/Tester: read and tests, no merge or project-tree write). The brief states it. A matching permission shows a Deny suggestion; the host does not auto-Deny an ask already on screen. - A command palette —
⌘Kreaches the tabs, layouts, session starts, swarm launches, Hand to Coder/Reviewer, Continue this job for a stopped agent, Export pack for a session, project switches, skill installs, Merge for a stopped isolated agent, Allow first wait, and Jump to first Needs you from one place, and it opens over a focused terminal rather than being swallowed by it.⌘O/Ctrl+Oopens a project folder. The palette lists each bundled skill as installed or missing and can refresh that list; there is no remote catalog. Palette Save last Planner graph recipe (!name) writes$GROKSPACE_*prose to~/.grokspace/skills/(see How user skills work). Palette Save successful run as playbook (!name) and Dispatch playbook !name snapshot and replay a session shape (see How playbooks work). "Install or refresh GrokSpace skills" writes graph, memory, and steps in that order; the Graph empty-state button still installs graph alone. Mid-session install is invisible until the next grok start. Search matches a subsequence, sosgrfinds "Start Grok in the first free pane". - Settings — a default pane layout for projects that have never chosen one, which panel the workspace opens on, which new session a dispatch reaches for first, an inbox-zero gate on dispatch (off by default), and a named permission-policy file of user globs. Shared by every project, and refused rather than stored when a value is not one this build knows. The dispatch preference reorders what is offered and never picks a target: a setting that chose for you would be one that sends work somewhere nobody looked. Inbox-zero stays off until you turn it on; the escape is typing dispatch anyway, not a checkbox. Policy Deny wins over ask and over allow-once-similar; Allow is never inferred as Always.
- A diff panel — what changed, read out of
git. The default view is the project's tree. An ACP agent that isolated into a worktree appears as a chip, and picking it reads that checkout. Modified, new, deleted and renamed files, with each file's diff againstHEAD; a file git has never seen is shown as all additions rather than skipped. Paths another worktree (or the project) also touched are marked; lockfiles and migrations get a louder hotspot strip. That strip warns — it does not lock Merge. On an isolated worktree that overlaps, Walk (on by default) lists hotspots, then other overlaps, then the rest; turn it off for git's order. A stopped agent's Merge refusal (dirty project, nothing to merge, missing git) is shown before you click. The checkpoint is that worktree'sHEAD; Discard reverts it so Close can proceed. Merge, on a stopped agent, lands that branch on the project. There is no shadow-git. - Per-agent worktrees — an ACP agent starts in a clean checkout of
HEADat<project>/.grokspace/worktrees/<session-id>/, on a branch namedgrokspace/<short-id>. Grok panes and shells stay on the project folder. Graphs, steps, and memory still live under the project, via absolute environment variables. If isolation is skipped, a dialog asks before start (Cancel / Start on the project tree); after confirm, a banner says the agent is on the project tree. Close refuses while that checkpoint is dirty; Discard reverts it (force-remove). Merge, on a stopped agent, commits leftover files and lands the branch on the project. After a successfulgit worktree add, paths listed in<project>/.grokspace/worktreeinclude(one relative path per line) are copied from the project into the new tree. Missing paths are skipped; start still succeeds.**,.., absolute paths, and anything outside the project are refused. Nothing is copied by default — not.env. Restart reuses the tree and does not copy again. If Settings → Worktree setup is on (off by default) and the folder is trusted, GrokSpace then runs<project>/.grokspace/worktree-setup, orsetupin.grokspace/worktrees.json, in that tree. First open asks Deny / Trust once / Trust this folder. Untrusted folders keep setup and project hooks off. Trust is stored by canonical path in~/.grokspace; forgetting a project does not forget it. Settings → Project hooks shows that host gate, plus any.grok/hooksor.cursor/hooks.jsonfiles, and whether grok's~/.grok/trusted_folders.tomlalready lists the folder. GrokSpace does not spawngrokor run/hooks-trust. Use a grok pane if you want grok itself to load project hooks. HTTP hooks and a host hook that can block tools are out of scope. Settings → MCP lists servers from grok's local config (~/.grok/config.toml,~/.claude.json, project.mcp.json/.cursor/mcp.json/.grok/config.toml). MCP is loaded by grok, not GrokSpace. An untrusted folder keeps project MCP off. GrokSpace does not spawn MCP servers, fetch a marketplace, or send its ownmcpServers— ACPsession/newstill passes[]. - Attention inbox — a strip above the workspace lists Needs you (an ACP
agent on
needs_inputor a pending permission), Review (idle, with approved steps or an isolated worktree), and Merge (stopped, isolated, and Merge would not refuse). Clicking jumps to the task card, the Diff chip, or the graph. With the inbox or a permission chip focused (not a terminal),Aallows once,Ddenies,Oopens the pane or card, andGshows the graph. Snooze 1h / Tomorrow hide a Needs you item without answering the agent; the dock badge still counts the wait, and the item returns when the timer ends (or at next launch if it already ended). The palette has Allow first wait and Jump to first Needs you. Dispatch is not gated unless Settings → Inbox-zero gate is on (off by default). Then a waiting Needs you disables Dispatch until you type dispatch anyway. - Dock attention — an unfocused window with an ACP agent waiting on
needs_inputshows a badge count and one Informational bounce. A focused window already has Allow/Deny, so it does not bounce. A second permission on the same session does not bounce again until that session has leftneeds_input. - Local persistence — projects, sessions, tasks, memory, and settings live in
SQLite at
~/.grokspace/grokspace.db. Inbox snooze timestamps live in~/.grokspace/inbox-snooze.json.
| Layer | Choice |
|---|---|
| Shell | Tauri 2 |
| Frontend | React 19, TypeScript, Vite 8, Tailwind CSS 4 |
| Terminals | xterm.js 6 with portable-pty |
| State | Zustand |
| Database | SQLite via rusqlite (bundled) |
Prebuilt .dmg files are on the Releases page; they are unsigned, so macOS refuses a double-click — right-click the app and choose Open, or run xattr -d com.apple.quarantine /Applications/GrokSpace.app.
Requirements
- Node.js 20.19+ or 22.12+ and npm (
package.jsonenginesmatches Vite) - Rust 1.88+ (
rustup toolchain install 1.88;rust-toolchain.tomlpins this channel) - On macOS: Xcode Command Line Tools (
xcode-select --install) - The
grokCLI — needed to drive agents and Grok panes, not to compile or run the CI checks - Optional:
XAI_API_KEYin the process environment if you want ACP to authenticate with an API key instead ofgrok login. A repo-root.envis not loaded. Export it in the same shell asnpm run tauri:dev. A packaged.appneeds the key in the user environment, or a priorgrok login.
# optional — only if you are not using `grok login`
export XAI_API_KEY=…
npm install
npm run tauri:devThe first run compiles the Rust backend, which takes a few minutes; later runs
are incremental. npm run tauri:build produces a release build, and a .dmg
on macOS. That local .dmg is unsigned; macOS will ask you to right-click and
Open it. "private": true in package.json only means the package is not
published to npm — it says nothing about the repository.
npm run build # type-check the frontend and build it
npm test # frontend store tests (Vitest)
npm run test:e2e # TEST-002 host smoke (skips WebView if none)
cd src-tauri
cargo test # backend tests, against in-memory SQLite
cargo clippy --all-targets -- -D warnings
cargo fmtThose are the same checks .github/workflows/ci.yml
runs on every pull request, so a green run locally is a green run there. CI adds
cargo fmt --check in place of cargo fmt, and installs the WebKitGTK packages
Tauri needs to compile on Linux.
npm run test:e2e is one smoke, not a harness: it opens a fixture folder, starts
a shell pane, runs graph:demo in that pane, and asserts the Graph snapshot
the panel would draw. A Playwright drive of the Tauri window is skipped when
there is no display (Linux CI has WebKitGTK to link, not a usable WebView).
On a Mac you can still watch the same path by hand with npm run tauri:dev
and npm run graph:demo -- "$GROKSPACE_GRAPH_FILE".
This tree is the map; docs/README.md is the index of the
longer documents, with a reading order and a one-page summary of how the host,
the renderer, and an ACP agent talk to each other.
.github/ The CI workflow: the same checks, on every pull request
src/
components/ TitleBar, ProjectSidebar, WorkspaceShell, EmptyState, PaneGrid,
TerminalPane, GraphVisualizer, TaskBoard, MemoryPanel,
CommandPalette, SettingsPanel, DiffPanel, AgentTranscript,
SessionSteps, ErrorBoundary, PermissionActions, graph/
stores/ Zustand stores (projectStore, sessionStore, graphStore,
taskStore, memoryStore, settingsStore, diffStore, stepStore,
uiStore)
lib/ Typed `invoke` wrappers (api.ts), the terminal registry,
the graph document parser, the role presets, the shortcut
table, the palette's commands, the dispatch targets, the
theme reader, dock attention, worktree overlap, permission
chips, session steps, hunk prompts, window drag, home-relative
paths, the ACP transcript folder, and graph ask/artifact helpers
styles.css Every colour the app draws, including the ANSI palette
types.ts Mirrors the Rust structs, which serialize as camelCase
generated/ Domain enum lists emitted from src-tauri/src/domain.rs
scripts/ Development helpers; demo-graph.mjs writes a moving graph,
e2e-smoke.mjs is the TEST-002 host smoke, and the release-*.sh
pair holds the release workflow's decisions so they can be tested
src-tauri/
migrations/ Append-only SQL migrations
skills/ The three Grok skills GrokSpace installs on request (see below)
src/
db.rs Database location, pragmas, migration runner
project.rs Project model, queries, and Tauri commands
pty.rs Pseudo-terminal plumbing; no database, no Tauri
session/ Session model, start path, and worktree commands
db.rs Session row, permissions, status, reconcile
start.rs Resolve program, isolate, spawn pty or ACP
worktree_cmds.rs Close, discard, merge, merge-readiness, orphan GC
program.rs Finding `grok` and `git` when PATH is not enough
acp/ Agent Client Protocol: status from JSON-RPC, no Tauri, no database
protocol.rs Classify lines, visible updates, permission replies
process.rs Child, handshake, status tracker
task.rs Task model, the board's queries, and dispatch
memory.rs Shared project memory, and the file agents read it from
skill.rs Installing the skills GrokSpace bundles into ~/.grok/skills
user_skill.rs User SKILL.md library in ~/.grokspace/skills; not ~/.grok/skills
graph.rs Graph file locations, reads, and the change watcher
permission_heat.rs Host `<id>.permissions.json` sidecar; Graph count badge
edges.rs Project `.grokspace/edges.json`; Graph tab overlay, not a merge
steps/ Session steps: watch, ingest, approve
store.rs Types, paths, SQLite CRUD
ingest.rs Parse and fold the agent's file
watch.rs Directory watcher and watch command
worktree.rs Git worktrees for ACP agents; no Tauri needed to test
diff.rs What changed, read out of git, optionally in one session's tree
settings.rs App preferences: key-value in SQLite, typed on the way out
policy.rs Named permission-policy globs; deny wins; never Always
ledger.rs Append-only permission answers; ~/.grokspace/ledgers/; no upload
domain.rs Shared enum lists; emits src/generated/domain.ts
error.rs Error type; serializes to a plain string for the frontend
e2e_smoke.rs TEST-002 host smoke (compiled only for tests)
icons/source/ Icon artwork and how to regenerate it
Three bundled skills. Source directory, install directory, and frontmatter
name are not always the same word — the memory skill is the one that differs:
| Skill | Source | Installs to | Frontmatter name |
|---|---|---|---|
| Graph | src-tauri/skills/grokspace-graph/ |
~/.grok/skills/grokspace-graph/ |
grokspace-graph |
| Memory | src-tauri/skills/project-memory/ |
~/.grok/skills/grokspace-memory/ |
grokspace-memory |
| Steps | src-tauri/skills/grokspace-steps/ |
~/.grok/skills/grokspace-steps/ |
grokspace-steps |
The frontend never spells out raw command names: every backend call goes
through a typed wrapper in src/lib/api.ts. The command and
event inventory — and the spawn environment — is
docs/ipc.md.
Colour works the same way. Every value the app draws is declared in
src/styles.css, including the sixteen ANSI colours a terminal
paints with. Two places cannot use Tailwind classes — xterm takes a theme object and
React Flow takes props — so src/lib/theme.ts reads the tokens
back out of the stylesheet rather than keeping a second copy. ErrorBoundary is the
exception: it paints inline from a fallback map in that file so a missing stylesheet
still matches the palette, and a test locks the map to @theme. Adding a light theme
is a second block in one file.
Three decisions here are not obvious, and undoing any of them breaks something subtle:
- Output travels over a Tauri channel, not the event system. Tauri's own
docs call the event system unsuitable for throughput and point at channels for
child-process output. Chunks are sent as raw bytes and written to xterm as a
Uint8Array; decoding UTF-8 in Rust would corrupt any sequence that straddles a read boundary. Exits, being rare, do use an event. - The pty's slave handle is dropped immediately after spawning. Holding it
keeps the pty open, and the reader thread then waits for an EOF that never
comes. EOF also arrives as
Ok(0)rather than as an error. - xterm instances live in a registry outside React
(
src/lib/terminals.ts), each owning a detached container that is moved between hosts. React remounts panes on every layout change, and StrictMode remounts them in development; without the registry that would duplicate input handlers and discard scrollback.
src-tauri/src/pty.rs depends on neither Tauri nor the
database, so its tests drive real pseudo-terminals headlessly.
A graph belongs to a session, not to the project:
<project>/.grokspace/graphs/<session-id>.json
Naming the file after the session id means nothing has to be stored to remember
whose graph is whose, two agents in one project never overwrite each other's
plan, and a restart — which mints a new session id — starts from no graph instead
of inheriting the plan of the run it replaced. A project folder that cannot be
written to falls back to ~/.grokspace/graphs/.
Every session is spawned knowing where its graph belongs, through
GROKSPACE_GRAPH_FILE (absolute, so a worktree does not change the answer),
GROKSPACE_GRAPH_DIR, GROKSPACE_SESSION_ID, GROKSPACE_PROJECT_DIR,
GROKSPACE_STEPS_DIR, GROKSPACE_STEPS_FILE, GROKSPACE_MEMORY_FILE, and —
when the session has a role or isolated — GROKSPACE_SESSION_ROLE and
GROKSPACE_WORKTREE. The full table is in
docs/graph-engineering.md.
src-tauri/src/graph.rs watches those directories and
reports which session's file moved; the panel re-reads that one file, which is
what makes the graphs live rather than a snapshot.
What makes grok write one is a bundled skill, installed to ~/.grok/skills/
from the button in the Graph panel's empty state, or together with memory and
steps from the palette. That empty state also names the directory installing
writes to, and can ask a running agent for a graph directly.
The skill is three files: a short runbook, a catalogue of topologies for deciding whether the work deserves a graph at all, and the file contract. It carries no node positions on purpose — the panel lays a graph out by longest-path layering, which can see how many nodes landed in each column when a formula in a prompt cannot.
docs/graph-engineering.md has the file contract,
the reasoning behind the watcher's filters, and how to test the panel by hand —
including npm run graph:demo, which steps a graph through a run so the panel can
be watched updating without an agent.
Hand-offs live in <project>/.grokspace/edges.json as
{ fromSession, fromNode, toSession, kind } (delegates | blocks | reviews).
The Graph tab overlays rows that touch the selected session. Per-session graph
files stay one plan each; they are not merged.
Answered permission chips are also written to
<project>/.grokspace/graphs/<session-id>.permissions.json (requestId,
summary, chip, optional stepId). The Graph tab shows a count badge, not a
trust colour. The agent's $GROKSPACE_GRAPH_FILE is never rewritten.
A step list belongs to a session, not to the project board:
<project>/.grokspace/steps/<session-id>.json
The agent writes the file; GrokSpace folds it into SQLite and draws the rail.
The phase on the session row is none, proposed, or approved. Build
(Approve) locks the titles and sends them back as a prompt; completing a step
does not move a Kanban card. After Build the graph panel keeps those node
titles (status may still move); new titles are a revise-plan — reopen Spec. The
Tasks rail warns when node labels or ids and step titles disagree; positions
do not count, and two empty lists are not drift. A project folder that cannot
be written to falls back to ~/.grokspace/steps/.
What makes grok write one is a bundled skill, installed to
~/.grok/skills/grokspace-steps/ from the Tasks rail empty state or the
palette's "install GrokSpace skills".
docs/session-steps.md has the file contract, the
phase machine, and how to test the rail by hand.
A playbook is a session shape, not a skill. Palette !name saves a successful
run (graph stub, steps stub, role list, memory excerpt) to
~/.grokspace/playbooks/<name>/, or you can drop the same folder under the
project's .grokspace/playbooks/. Dispatch starts those roles and briefs them
with $GROKSPACE_GRAPH_FILE / $GROKSPACE_STEPS_FILE / $GROKSPACE_MEMORY_FILE.
The snapshot does not include the transcript. There is no remote catalog.
A user skill is prose + pointers, not a playbook session shape. Palette
Save last Planner graph recipe (!name) writes ~/.grokspace/skills/<name>/SKILL.md
from the newest Planner graph using $GROKSPACE_*, never a session UUID. Grok
does not load that folder; copy into ~/.grok/skills/ yourself. The transcript
shows a display-only skill: grokspace-graph chip when a line names that skill.
The database is the memory; the file is a projection of it:
<project>/.grokspace/memory.md
The first write under .grokspace/ also plants .grokspace/.gitignore with *,
so git add . does not commit memory, graphs, or worktrees. This repository
lists .grokspace/ in its own .gitignore for the same reason; opening a
different folder did not, until that first write.
Agents read files rather than SQLite, so every write rebuilds that file from the
whole table and every session is spawned knowing its path through
GROKSPACE_MEMORY_FILE. It is written even when the memory is empty, since a file
saying there is nothing to know is friendlier than one that is missing.
Three decisions shape the rest:
- The key is half the primary key, so writing is an upsert. Memory is a set of things that are true about the project, not a log of things that were said, and writing the same key twice is a correction.
- One direction only. GrokSpace writes and the agent reads. A file the agent
also wrote would need merging against the table on every change, and a merge that
guesses wrong loses something a person typed. The skill therefore asks the agent to
name what belongs in memory in its reply, rather than to write it. The
transcript offers Add to Memory when a message names a key, or on the last
line by hand. Default type is
note(context/decision if you switch); the 32k cap still refuses, and the renderer does not write the Markdown file. - It is capped, at 32k characters. Every session reads all of it, so an unbounded panel would quietly make each session more expensive and less attentive. Rewriting an existing entry replaces its own size, so a correction is never refused for being long.
.grok/rules/ would have needed no skill at all, since Grok loads every .md
under it automatically. It is not used, for a reason recorded in
docs/grok-cli-integration.md: Grok skips files
that .gitignore ignores, so the file would either land in the user's git history
or be silently ignored.
An ACP agent writes in its own checkout so its diff is its own. Grok panes and shells stay on the project folder — that is the tree a person is looking at.
<project>/.grokspace/worktrees/<session-id>/
The tree is a clean checkout of HEAD on a branch named grokspace/<short-id>.
The agent does not see uncommitted files on the project tree; that is the point.
Walk-up from the worktree still finds the project's AGENTS.md and .grok.
Putting trees under ~/.grokspace would not.
To seed deps or local config the agent would otherwise miss, list relative
paths in <project>/.grokspace/worktreeinclude, one per line. After a
successful git worktree add, those paths are copied from the project into
the new tree. Blank lines and # comments are ignored. A missing source is
skipped with a reason; isolation still succeeds. **, .., absolute paths,
and anything that resolves outside the project are refused. There is no
default copy — .env and node_modules stay off the tree unless listed.
Restart reuses the checkout and does not copy again.
To install (not just copy), add <project>/.grokspace/worktree-setup or a
setup string in .grokspace/worktrees.json. GrokSpace runs it only
when Settings → Worktree setup is on and the folder is trusted.
That toggle is off by default. First open asks Deny / Trust once /
Trust this folder (not Allow forever). Deny and Trust once last until quit;
Trust this folder is stored by path in ~/.grokspace. Forgetting a project
does not drop trust. Untrusted folders keep setup and project hooks off.
Settings → Project hooks shows the same host trust (and grok's toml, read-only).
GrokSpace does not spawn grok or write ~/.grok/trusted_folders.toml.
Settings → MCP is the same gate for project .mcp.json: a read-only list.
GrokSpace does not start those servers.
The command is logged (and named in the skip reason). $GROKSPACE_WORKTREE is
the dest; a worktree-setup file also gets dest as $1. Timeout is two
minutes. Fail or timeout skips isolation rather than leaving a silent tree.
Restart reuses the checkout and does not run the script again.
GrokSpace calls git worktree add itself. It does not pass grok --worktree:
every extra flag is a way for a session to fail to start, which is the same
reason graphs, memory, and roles stay out of flags. Missing git, a folder that
is not a repository, or a failed worktree add refuse to start unless you
confirm Start on the project tree. Cancel leaves the agent unstarted. The
first start never sends allowUnisolated. After confirm, the isolation banner
(and the session chip) still say the agent is on the project tree. A swarm
asks once, never starts unisolated in silence.
GROKSPACE_PROJECT_DIR, GROKSPACE_GRAPH_FILE, GROKSPACE_STEPS_FILE, and
GROKSPACE_MEMORY_FILE stay pointed at the project. GROKSPACE_WORKTREE is
set only when a tree exists. Process cwd (and ACP session/new cwd) is the
worktree.
Stop keeps the tree so the Diff panel can still read it. Close runs
git worktree remove without --force and refuses while the tree is dirty.
The checkpoint is this worktree's HEAD. Discard, on a stopped session only,
reverts it (force-remove). There is no second snapshot store. Merge, also stopped-only,
commits leftover files on the session branch (a dirty tree cannot be merged
otherwise) and git merges that branch into the project. Uncommitted files on
the project block the merge; .grokspace/ does not, because that folder is
GrokSpace's own. Conflicts abort. Restart mints a new session id but
reuses the directory, so uncommitted files survive. Palette Continue this
job is the same start (new id, same tree, no --resume) plus a host brief
from the previous graph path, step titles, and a ≤2 KiB transcript excerpt.
Forgetting a project
force-removes every leftover tree so git is not left with registered worktrees
for a folder the sidebar no longer knows. Settings can dry-run leftover trees
under .grokspace/worktrees/ that have no session row, with sizes. Removing
them keeps dirty and unmerged trees and never runs on its own.
An assigned card moves from in_progress to review when the agent goes idle,
except while its step list is still proposed — that idle is the Approve gate,
not the end of the work. Comments on a scoped diff (path, hunk, and a note)
stay in the panel until they are sent as one follow-up to an idle agent. A
graph node's file artifactPath opens that path in the Diff panel, scoped to
the session. Fork from here starts a new isolated agent at project HEAD
(not the parent's dirty files) with a reminted graph (that node running,
descendants pending) and steps as proposed. The parent is unchanged; trees are
never shared. The node inspector and Memory Artifacts column list claimed file
paths with Open in Finder or Diff. Markdown previews as text and images as
images; agent HTML is never rendered.
State lives in ~/.grokspace/grokspace.db rather than the platform app-data
directory, so it is easy to inspect and back up. Folder trust is the
trusted_folders table, keyed by canonical path — forgetting a project does
not drop it.
sqlite3 ~/.grokspace/grokspace.db '.tables'
tail -n 50 ~/.grokspace/logs/grokspace.log
tail -n 20 ~/.grokspace/ledgers/<project-id>.jsonlCommand failures, ErrorBoundary crashes, store banners, and failed pane attach/resize/emit land in that log. Typing into a session that has already exited does not. Nothing is uploaded.
Migrations are an append-only list in
src-tauri/src/db.rs, tracked with SQLite's
user_version. To change the schema, add a new file under
src-tauri/migrations/ and append it to MIGRATIONS — never edit a migration
that has already shipped.
There are six shipped migrations. 0001 creates projects, tasks, sessions,
and memory_entries. It reserves sessions.role and sessions.worktree_path.
It does not create sessions.kind — that arrives in 0002. The 0001 file
header still says later phases "only add rows and queries"; that is stale.
| File | What it added |
|---|---|
0001_initial.sql |
projects, tasks, sessions, memory_entries |
0002_session_command.sql |
sessions.kind (default grok) and sessions.exit_code. The filename says "command"; the SQL does not add a command column. The name is left as-is so the include_str! path in db.rs stays stable. |
0003_app_settings.sql |
app_settings key-value table. Preferences belong to the app, not a project; a new preference is a new key, and the typed surface lives in Rust. |
0004_session_permissions.sql |
session_permissions, so a pending ACP prompt survives a webview reload |
0005_session_steps.sql |
session_steps and sessions.steps_phase (none / proposed / approved) |
0006_permission_options.sql |
options JSON on session_permissions, so Allow can stay allow_once and Always-allow is a named chip |
sessions.worktree_path was reserved in 0001 and is written when an ACP agent
isolates. Filling a nullable column that already exists needed no new migration.
- Phase 0 — Foundation. Scaffold, window shell, SQLite, project CRUD. Done.
- Phase 1 — Terminal core. Pty-backed sessions, xterm.js panes, the grid layout, the session lifecycle, and a live graph per session. Done.
- Phase 2 — Kanban and dispatch. The board, dispatch, and ACP-driven agents
whose status is richer than running/stopped. Done. Leftover real-
grokACP checks (permission option ids, a swarm) stay on issue #8.docs/grok-cli-integration.mdrecords the documented surface. - Phase 3 — Memory and roles. Shared project memory, role presets, and swarm launches. Done. A swarm is made of ACP sessions, so it inherits the same leftovers.
- Phase 4 — Polish and distribution. A command palette, a diff panel, settings,
and a notarized
.dmg. The first three are done. The release pipeline is written but unproven: there are nov*tags, signing and notarizing need Apple Developer credentials and a macOS runner, and repository Actions secrets may still be empty. Also on issue #8: HTML5 drag in macOS WKWebView, and optional column reorder. Inline description edit shipped in #23. - Phase 5 — Isolation and review. ACP agents start in their own git
worktree, the diff panel can read that tree, Close refuses to eat dirty work,
Discard reverts that checkpoint, Merge commits leftover files and lands the branch on
the project. An assigned card moves to
reviewwhen the agent goes idle, except while the step list is stillproposed(the Approve gate). Comments on hunks can be bundled and sent back as one prompt, and a graph node's file path opens in the Diff panel. The node inspector and Memory Artifacts column list claimed paths with Finder or Diff. Done. The same Mac/grokleftovers as Phase 2 still apply.
docs/skill-merge.md records how the bundled graph skill was
merged with a hand-written one, every conflict, and which side won.
docs/session-steps.md is the step-list contract (path,
JSON, phase, skill), the same shape as
docs/graph-engineering.md for graphs.
docs/grok-cli-integration.md records the
verified grok CLI surface the app builds on, and
docs/releasing.md covers signing, notarizing and what to check
the first time a real release runs.
GrokSpace targets macOS. The window is configured with
titleBarStyle: "Overlay" and a hidden native title, so the traffic lights
float over the in-app title bar. Moving the window is started from that bar
(data-tauri-drag-region plus core:window:allow-start-dragging); without
the permission the Overlay chrome cannot be dragged.
It also builds and runs on Linux, which is useful for CI. Those two options are macOS-only, so on Linux you get the ordinary window decorations in addition to the in-app title bar, and the space reserved for the traffic lights is empty.
- Contributing — how to run the checks CI runs, and the hard
rules around
grokand ACP. - Code of Conduct — what is expected in issues, pull requests, and reviews.
- Security policy — report a vulnerability privately through GitHub Security Advisories, never in a public issue.
- Issues — bugs, questions, and
ideas.
help wantedmarks places to start; the checks that still need a Mac with a licensedgrokare on issue #8.
Released under the MIT License.