High-level architecture map for commands, workflows, services, and JS integration.
This page describes how the WSM codebase is organized. It is aimed at contributors who want to understand where behavior lives, how layers connect, and where to add new functionality.
WSM follows a strict four-layer architecture. Each layer has a clear responsibility, and dependencies flow downward:
┌──────────────────────────────────────────────────┐
│ cmd/wsm/cmds/ CLI adapters │
│ registry/ workspace/ git/ js/ │
├──────────────────────────────────────────────────┤
│ pkg/wsm/workflows/ Orchestration layer │
│ create, status, commit, rebase, fork, ... │
├──────────────────────────────────────────────────┤
│ pkg/wsm/ Core domain services │
│ workspace, discovery, git_operations, branch/ │
├──────────────────────────────────────────────────┤
│ pkg/wsm/gitclient/ Git abstraction │
│ cli backend (system git) │
└──────────────────────────────────────────────────┘
cmd/wsm/cmds/)Each Cobra command lives in a subpackage. Command files are thin: they decode flags into a settings struct, call a workflow, and format output. Business logic does not belong here.
Subpackages:
registry/ -- discover, list repos, list workspacesworkspace/ -- create, info, status, add, remove, fork, merge, deletegit/ -- commit, diff, log, branch (create/switch/list), rebase (start/status/continue/abort)js/ -- runnerShared infrastructure lives in cmd/wsm/cmds/common/:
build.go -- Glazed command description builder with standard sectionsruntime.go -- --output-mode resolution and structured row emissionpkg/wsm/workflows/)Workflows are the orchestration layer. Each workflow encapsulates a complete user-facing operation and defines its own request/result types:
| Workflow | Purpose |
|---|---|
DiscoverWorkflow | Scan directories, update registry |
CreateWorkflow | Resolve branches, create worktrees, write workspace config |
StatusWorkflow | Gather git status across repos (parallelized with --jobs) |
CommitWorkflow | Two-phase: Prepare() then Execute() for multi-repo commits |
RebaseWorkflow | Rebase/status/continue/abort with conflict detection |
ListWorkflow | List repositories and workspaces |
InfoWorkflow | Resolve workspace, extract field values |
ForkWorkflow | Two-phase: Plan() then Fork() |
DeleteWorkflow | Preview then delete with worktree cleanup |
MergeWorkflow | Merge branches back and optionally clean up |
Workflows own the sequencing of operations. They call into core domain services but never call CLI commands.
pkg/wsm/)The domain layer contains the foundational types and services:
types.go -- Repository, Workspace, RepositoryStatus, WorkspaceStatus, WorkspaceConfigworkspace.go -- WorkspaceManager with create, load, delete, detect operationsworkspace_context.go -- Workspace detection from working directorydiscovery.go -- Repository scanning logicstatus.go -- StatusChecker for per-repo git statusgit_operations.go -- Diff and other git operationshistory_operations.go -- Log and commit history queriesbranch_operations.go -- Create, switch, list branches across reposrebase_operations.go -- Low-level rebase: start, continue, abort, detect state, list conflictspkg/wsm/gitclient/)The git client provides an abstraction over git operations. WSM uses the system git CLI backend:
client.go -- Interface definitioncli_client.go -- Git operations via git CLIworktree_cli.go -- Worktree operations via git worktreeBranch policy is centralized in pkg/wsm/branch/ to avoid duplicating branch
decisions across commands. The system has two parts: a creation/checkout
resolver, and a status base-ref resolver.
The original resolution system uses typed enums:
ResolutionMode -- what operation triggered the branch decision:
CreateWorktree -- creating a new workspace worktreeAddRepository -- adding a repo to an existing workspaceSync -- syncing branches across a workspaceResolutionStrategy -- how to obtain the target branch:
UseLocal -- use the existing local branch as-isTrackRemote -- create a local tracking branch from remoteCreateFromBase -- create a new branch from the specified baseCreateFromHead -- create a new branch from HEADRemoteRefKind -- how to interpret remote references:
None -- no remote referenceRemoteTrackingBranch -- reference is remote/branch formatA BranchResolutionRequest goes in, a BranchResolutionPlan comes out. The
plan is deterministic: same inputs always produce the same strategy. Commands
and workflows use the branch service rather than implementing their own logic.
pkg/wsm/branch/status_resolve.go)wsm status compares each repo's HEAD against a base branch to report
is_merged/needs_rebase. The base is resolved through a single precedence
function, ResolveBaseBranchForRepo, so no call site can forget a layer:
RepositoryMetadata.BaseBranch in .wsm/wsm.json, set by wsm set-base)Repository.BaseBranch, set by wsm set-base --global)Workspace.BaseBranch)Repository.DefaultBaseBranch, from git symbolic-ref refs/remotes/origin/HEAD)WSM_BASE_BRANCH envmainThe chosen branch is then turned into a concrete git ref by ResolveBaseRef,
which prefers the remote-tracking ref (<remote>/<base>) and falls back to the
local branch (<base>) -- the latter is what makes forked workspaces work when
the base branch was never pushed. The outcome is a BaseComparison carrying
ResolvedRef, RefSource, Status (resolved/unknown/error), Reason, and
the IsMerged/NeedsRebase values. The status table renders this in a BASE
column; MERGED/REBASE glyphs are honest: ? for unknown, ! for error.
See wsm help wsm-persistence-and-state for the two stores and wsm set-base
for the override command.
The JavaScript layer lives in pkg/wsmjs/ and mirrors the CLI/workflow
architecture:
pkg/wsmjs/
├── service/ Manager facade wrapping workflows
├── module/ goja native module (require("wsm"))
└── runner/ Script file loader and executor
service/manager.go -- The Manager struct wraps workflow instances
and provides a flat Go API: Discover, CreateWorkspace, Status,
ListWorkspaces, ListRepositories.module/module.go -- Registers a goja native module that maps JS calls
to service methods. Handles type conversion between Go and JS via JSON
round-trip.runner/runner.go -- RunFile() bootstraps a goja runtime, registers
the wsm module, loads and executes a script file, and returns its result.The key design decision is that the JS API calls the same workflow layer as the
CLI. There are no separate implementations -- require("wsm").createWorkspace()
and wsm create both go through CreateWorkflow.
All commands use the shared output mode system defined in
cmd/wsm/cmds/common/runtime.go:
ShouldOutputHuman(mode) -- true for human and both modesShouldOutputData(mode) -- true for data and both modesEmitRows(ctx, vals, rows) -- emits structured data through GlazedCommands typically have two output blocks: one for human-readable text (headers,
tables, status messages) and one for structured rows. The both mode runs them
sequentially.
cmd/wsm/cmds/ subpackage.pkg/wsm/workflows/ with request/result types.pkg/wsm/, not implement git logic directly.cmd/wsm/root.go.pkg/wsmjs/service/manager.go and expose it in pkg/wsmjs/module/module.go.| Problem | Cause | Solution |
|---|---|---|
| New command has lots of business logic | Logic added in cmd/ instead of workflow | Move orchestration into pkg/wsm/workflows/ |
| Duplicate branch decisions | Branch policy bypassed | Route through pkg/wsm/branch/ service |
| JS API diverges from CLI behavior | Separate code paths | Both must call the same workflow |
| Rebase state detection fails | Missing .git/rebase-merge check | See rebase_operations.go for state detection logic |
wsm status shows ? in MERGED/REBASE | Base branch could not be resolved (no remote-tracking and no local ref) | Set a base with wsm set-base <repo> --branch <branch>, or check the BASE column reason |
wsm status shows ! in MERGED/REBASE | git itself failed during the comparison | Inspect the base_reason JSON field / BASE column; the captured stderr explains the git failure |
wsm help wsm-command-referencewsm help wsm-persistence-and-statewsm help wsm-js-api-and-runner