Saphan StudioDocs
Getting started

The workspace layout

What lives where in a workspace, for one repository or several, and the operator runbook for putting a workspace into that layout.

The layout

A workspace is one directory. The layout below is the one the fleet is built around; more than one product repository is normal.

<workspace>/
  bridge/                         canon + record
  sources/<repo-1>/               every product repo the workspace builds; more than one is normal
  worktrees/<repo-1>/wt-land      the landing worktree of that repo
  worktrees/<repo-1>/wt-<slug>    one per stream, under the repo it belongs to
  tmp/                            every script, log and scratch file

<workspace> is the directory you pass as --workspace.

One repository or several

The layout does not change with the number of repositories. Each repository gets its own directory under sources/ and its own directory under worktrees/, and a stream's worktree sits under the repository it belongs to.

<workspace>/
  bridge/
  sources/api/
  sources/web/
  worktrees/api/wt-land
  worktrees/api/wt-add-pagination
  worktrees/web/wt-land
  worktrees/web/wt-add-pagination-ui
  tmp/

The names api and web are examples; a repository's name is whatever you give it with saphan workspace repo add.

The worktrees directory is worktrees under the workspace, unless SAPHAN_WORKTREES_DIR names another.

Runbook: put a workspace into the layout

These steps are for the operator's hand. The commands are described on the saphan workspace page, and the permissions step is saphan init.

A. Register a repository

  1. List what the workspace knows now and read each path's state:

    saphan workspace repo list --workspace <workspace>

    The states it can print are git, not-git, missing, alias-of=<name>, outside-workspace=<dir>, linked-worktree, not-a-directory and unresolvable.

  2. Add a repository that is already on disk. It is added to workspace.json without moving or cloning it. Without --path the repository is expected at sources/<name>; ./x and x/ are stored as x:

    saphan workspace repo add <name> --path sources/<name> --workspace <workspace>
  3. List again and confirm the new row reads git. Any other state is the reason the path cannot be used as it stands; correct the path or the directory, then list again.

B. Move an existing workspace into the layout

saphan workspace migrate moves an existing workspace into the per-repo layout, on this host only:

<workspace>/sources/<repo>       every direct-child git repo, added to the inventory
<worktrees>/<repo>/wt-<slug>     every flat worktree whose repo is established
<worktrees>/<repo>/wt-land       the landing worktree, under the repo it is a worktree of

Before

  1. Pause the fleet. The command refuses while any run of this workspace is live on this host, so let every run in flight finish and start no new one.

  2. Rehearse. Without --apply the command prints every move and changes nothing:

    saphan workspace migrate --workspace <workspace>
  3. Read the printed moves line by line. If a move is not the one you want, stop here: nothing has changed. The command also refuses when any destination already exists, and it lists, without moving, a stream whose repo cannot be established or whose readings disagree. A stream's repo is its record's repo name, else its _EXEC_ORDER.md - repo:, else the repo its git worktree belongs to. A direct child that is a symbolic link to a git repository is listed with its target and never moved.

The act

  1. Make the moves with the same command and --apply:

    saphan workspace migrate --apply --workspace <workspace>

    What --apply does besides moving: every linked worktree is re-joined to its repo with git worktree repair; the absolute repository paths the record holds (events, run records, the cost ledger) are rewritten in one transaction; signed rows (gate decisions, identity records) are counted and left as signed; and the trunk certification directory keyed by each old path is re-keyed.

After

  1. Run saphan workspace repo list --workspace <workspace> and confirm every repository reads git.

  2. Check that each stream's worktree is under <worktrees>/<repo>/ and that the repositories are under sources/.

  3. Resume the fleet.

  4. Far hosts. Every admitted transport=ssh machine with a worktrees_root is probed by the same --apply, and its legacy flat tree is moved, on that host, into the layout its row's far_layout marker composes:

    <root>/sources/<repo>               the shared clone
    <root>/worktrees/<repo>/wt-<slug>   each far worktree, by its stream's recorded repo
    <root>/mirrors/<repo>.git           the mirror the row binds

    Then git worktree repair runs there, a re-probe verifies the tree, and only then is the machine row signed with far_layout repo-scoped-v1 and its repo_dirs re-keyed. That signing is an owner act: when a far machine has work to move, --apply asks for the one-time code on your terminal once, before anything on this host or a far one moves. A machine with a live run is refused and left untouched. A machine that cannot be dialled, and any far worktree whose repo the record does not name, is listed and keeps the machine legacy.

When it refuses

  • A run of this workspace is live on this host. Wait for the run to finish, or end it, then rehearse again. Do not apply while it runs.
  • A destination already exists. The command refuses when any destination already exists; resolve that path, then rehearse again.
  • A stream is listed and not moved. Its repo could not be established, or its readings disagree. Fix the record for that stream, then rehearse again.

A run that stopped after a move is finished by the next --apply: a worktree already under <worktrees>/<repo>/ whose git link is stale is repaired there. The same holds for a run that stopped part-way on a far host.

A dirty or non-detached landing worktree is not a saphan workspace migrate rule. ⚠ This page does not say how to reverse a migration afterwards; the command's description does not cover it. Rehearse first, and keep a copy of anything you cannot recreate.

On this page