Skip to content

Latest commit

 

History

History
121 lines (106 loc) · 6.86 KB

File metadata and controls

121 lines (106 loc) · 6.86 KB

claude-workspaces (Go)

CLI tool (workspace) managing isolated dev workspaces for parallel Claude Code sessions: a git worktree per project, an allocated port block, generated env, and daemons — an environment engine. Clean-room Go rewrite (v1.1.x, complete, daily-driven) of an earlier personal Ruby tool, never publicly released and since retired; never consult or port Ruby code — the spec and this repo are the only sources.

Module path is github.com/Phaengris/claude-workspaces — the public home. MIT-licensed.

Commands

CGO_ENABLED=0 go build -o ./workspace ./cmd/workspace     # build (binary is gitignored)
go test ./...                                             # full suite (~11s; real processes, real git)
go test -race ./internal/proc ./internal/wsp ./internal/cli
gofmt -l . && go vet ./...                                # both must be clean before commit
# landing a change: commit, push master to BOTH remotes (origin = private mirror, github = public),
#   add its CHANGELOG entry under "Unreleased". Do NOT tag per change.
# daily-driver install between releases: build with
#   -ldflags "-X github.com/Phaengris/claude-workspaces/internal/cli.version=$(git describe --tags --always)"
#   then ./workspace install — the user runs master, tags are for the world.
# release (end of a day with user-visible changes, or on explicit need):
#   retitle Unreleased to X.Y.Z in CHANGELOG.md, build with version=X.Y.Z,
#   ./workspace install, tag vX.Y.Z, push master+tag to both remotes.

Conventional commits (feat(cli): …). The user's real install lives at ~/.local/bin/workspace — after user-facing fixes, rebuild with a bumped version and run ./workspace install (idempotent, manifest-driven).

Architecture (dependency flow strictly downward)

cmd/workspace → internal/cli (one file per command; cobra)
  → internal/wsp    workspace domain: identity/Resolve, ensure-chain, writers,
                    daemon model, ResolveTargets grammar, CommandEnv (SOLE
                    spawn-env composer), runtime ${} substitution
  → internal/config strict YAML (goccy), template expansion on the raw tree,
                    validation (errors.Join, all-at-once)
  → internal/alloc  .allocations.json registry: flock + atomic write, index
                    gap-filling, values math (alloc.Block is the ONE formula home)
  → internal/proc   LEAF. $SHELL -lc spawn, daemons (Setpgid), pid+starttime
                    liveness, TERM→KILL StopGroup (leader-only)
  → internal/gitx   LEAF. argv-form git only; GIT_DIR-family env neutralized
  → internal/envx   LEAF. curated env: allowlist + sanitized PATH + env_allow
  → internal/xerr   exit-code kinds: 0/1/2 usage/3 not found/4 config; ExitError
                    carries a child's code verbatim (checked before kinds)
internal/assets ← assets/ (//go:embed carrier; skill, hook, wrappers, config stub)

Load-bearing doctrines (violating these is a bug, not a style choice)

  • Derive, don't record: the registry holds only allocations; everything else (checked out, setup-current, running) is derived live. No status field. Every op is an idempotent ensure; re-run converges. new is the one transaction (LIFO undo; halts at a failed worktree undo).
  • Two-tier env: user commands (setup/start/stop/teardown/exec) get the CURATED env — wsp.CommandEnvenvx.Curated, exact-name allowlist + env_allow + sanitized PATH (concrete /versions|installs/.../bin stripped, shims kept). Claude sessions get sanitized-INHERITED env + workspace overlay. proc.Run/StartDaemon are the only user-command spawn paths; nil env means empty, never parent.
  • Destruction safety, layered: config rejects escaping path:; every WorktreeRemove/RemoveAll is containment-gated (isAncestorOrSame, root Abs'd); adopted dirs are never deleted; teardown/stop failures abort before removal; gc -d's five gates (tool-created, ≥1 checked out, all merged via refs/heads-qualified IsMerged, no live pids-dir record, clean incl. Err). Doubt always reads as "keep".
  • pids directory is truth for "what runs": down/restart(no-target)/destroy/ gc/release/doctor enumerate wsp.PidFileKeys, not config keys. Liveness = kill-0 AND starttime match (zombies = dead). StopGroup refuses non-leaders; "stopped (TERM)" promises leader death only.
  • Completions never break the shell: any error → nil + NoFileComp.
  • Uninstall removes EXACTLY the manifest (survivors manifest on failures); install never edits settings.json/rc files — prints snippets.

Testing conventions

  • TDD; table tests for pure logic; testscript txtar for command flows (internal/cli/testdata/). Shared txtar helpers in cli_test.go: wsenv, workroot (WORKROOT/WORKDIR substitution + ! grep WORKROOT guard).
  • Real processes (bounded sleep 30, explicit down epilogues, one 5s KILL-escalation case per package) and real git repos (hermetic: GIT_CONFIG_GLOBAL/SYSTEM=/dev/null + author env). PATH shims for git/claude/ xdg-open — a real claude must never be invocable from tests.
  • Culture: mutation-check load-bearing pins (state it in the commit/PR); exact exit codes pinned in Go tests (txtar ! exec is only "non-zero").
  • testscript gotcha: $VAR expands in UNQUOTED chunks only — absolute-path asserts need '…'$WORK'…'.

Known sharp edges (documented, don't rediscover)

  • goccy rejects unquoted ${…} inside flow-style YAML collections — block style in all examples.
  • Claude project-dir encoding: EVERY non-alphanumeric byte → - (empirical; probe in claude.go). ${WORKSPACE} = task id, NOT the dir name.
  • -- in session commands is the sniff-suppressor ONLY at the slot after the workspace; later -- reaches the child verbatim.
  • The public docs never mention v1/the Ruby predecessor beyond one line in README's "How this was built" — readers have no v1. Behavior details that used to live in the v1-divergences appendix are plain facts in docs/reference.md now (biggest: stop: runs AFTER daemons stop).

Doc map

  • docs/superpowers/specs/2026-07-30-claude-workspaces-go-design.md — THE spec; decided behaviors live here and in each milestone plan's Decided-behaviors table (docs/superpowers/plans/*-m[0-5]-*.md).
  • Ideas/backlog now live on the user's Fizzy board named claude-workspaces (internal tracker; find it via fizzy board list — never put its URL in this public repo). The md backlog below is historical.
  • docs/superpowers/plans/2026-08-10-m5-deferred-items.md — the post-v1.0 backlog + standing decisions (do not re-litigate without cause).
  • README.md — the front door: short sentences, primary info only, links out. docs/reference.md — the deep contracts behind it. BOTH are accuracy-audited against the code; if behavior changes, whichever documents it changes in the same commit (README for headline behavior, reference for details).