Developer Guide

Develop

From a READY machine to a story merged onto its target: take a checkout, start the item on the right branch, build and try it, check it locally, and land it.

Develop

On this page you take one item from the plan to a merge. You reserve a checkout, start the item on the branch it belongs to, build and try it against demo data, check it on your own machine, and land it.

It assumes two things: Set up your machine ended with the one-word verdict READY, and the item already exists in DevStride (Plan work covers making it). Item numbers below are written I#####: use the real number from DevStride.

There are two ways to drive the work, and both follow the same rules:

  • An agent does it. A Claude Code session runs the DevStride plugin's /devstride:build-item. It picks the item, builds it, reviews it, checks it and merges it.
  • You do it, or Codex does. You write the change yourself, and two commands, ds work start and ds work land, put it on the right branch and check it before it merges. Codex (OpenAI's command-line coding agent) uses the same two commands.

How work flows, in one minute

  • develop is effectively production. It is released to customers several times a day, so nothing merges into it until it is finished and fully reviewed.
  • So work collects somewhere else first. A Story under an Epic (the group of Stories that is released together) lands on that Epic's own branch, where unfinished work is allowed. A one-off item (one with no Epic above it) lands on a shared branch called the support train. The whole Epic, or the whole train, later goes to develop in one reviewed pull request.
  • A Story gets no pull request of its own, and neither does a one-off on the train. Each is checked on your machine instead, then merged onto its branch. This is called fast develop mode. The full review and the cloud checks (CI) run once, on the Epic's pull request, or the train's.
  • Review comes before CI, and CI runs once, on the code that was actually reviewed.

The rest of this page walks those rules in order. How DevStride Is Built, and Why explains the reasoning behind each one.

1. Take a checkout

Each machine has exactly three copies of the repository, called checkouts. Each has its own copy of the app and its own database in Docker, so several sessions can work side by side.

CheckoutFolderWho works there
main~/dev/devstrideThe person at the machine
wt1~/dev/devstride/.worktrees/wt1Agent sessions
wt2~/dev/devstride/.worktrees/wt2Agent sessions

Never make another copy (git worktree add, ds worktree create) unless the owner asks for one. When all three are busy, wait, or ask the session that holds one. Extra copies were made "to be safe" in the past, and nobody ever cleaned them up.

You take a checkout with a lease. A lease is a small file in the checkout's own git folder that records who holds it and why. It never shows up as a change in your work. You hold it until you hand the checkout back.

Why a lease rather than a quick look: a clean folder does not mean nobody is using it. A release can sit in one for hours, another session may be running tests there, and two sessions can both see "clean" before either one switches branch. Only one session can create the lease file, so only one can win.

  1. Name your session once per terminal (or add --session <name> to every ds pool and ds work command):
    export DEVSTRIDE_SESSION="<your name>"
    
  2. See who holds what:
    ds pool status
    

    It shows each checkout's holder, purpose and start time (a lease more than 24 hours old is marked STALE; clearing one is the owner's call), the branch it is on, and what data it holds.
  3. Take a checkout. A person takes main. The extra flag is there to keep agents out of main, which belongs to the person at the machine:
    ds pool checkout main --purpose I##### --i-know-main-is-free
    

    You can edit files in main without a lease. The lease matters for ds work, which only runs in a checkout you hold. Take it while main is clean and on develop, before you start the item (see IN USE below).
    An agent session takes wt1 or wt2 itself, from inside the session (AGENTS.md, the rules every agent reads, tells it to):
    ds pool checkout wt1 --purpose I#####
    

    Let the agent take its own lease. A lease belongs to the session that took it, so one you take from your terminal is not the agent's.

ds pool checkout prints exactly one outcome:

OutcomeWhat it meansWhat to do
LEASEDThe checkout is yours.Carry on. Running checkout again on a checkout you already hold changes nothing and says so.
BUSYAnother session holds it. The line names that session and its purpose, and says you do not hold it, even when the other session uses your name.Take another checkout, or ask that session. Never delete someone else's lease.
IN USENobody holds a lease, but the checkout has uncommitted changes or is on someone's branch, so a session from before leases existed may be working there. Your lease has already been released.Take another checkout.

BUSY and IN USE exit with code 2. A checkout must be clean, and on develop or detached (on no branch at all), before you can lease it. That is the state ds pool checkin leaves it in.

From a plain terminal, one that is not a Claude Code or Codex session, checkout also prints an export DEVSTRIDE_LEASE_TOKEN=… line. Run it in that terminal. Later ds pool and ds work commands use the token to prove they come from the session that took the lease; without it they are refused as someone else. Claude Code and Codex sessions need no token: each command in an agent session carries that session's own id.

Before it hands the checkout over, ds pool checkout brings it up to date. It moves a parked checkout to the latest develop, reinstalls dependencies, and brings its demo data up to date. It reports anything it may not fix itself, such as newer secrets (run ds secrets pull for those). Then it starts the checkout's app.

While you hold it, switch branches freely. The lease, not the branch, makes the checkout yours.

The checkout pool covers the rest: customer copies, cloud mode and every flag.

2. Know where the work lands

You never work out the target branch by hand: /devstride:build-item and ds work start both compute it from the item's place in DevStride, and a test holds the two to the same answer. This is the rule they follow:

  1. The item has an Epic above it. It lands on the nearest Epic's integration branch (the branch the Epic's Stories collect on). That branch is named <you>/<YY-MM-DD>/epic-<I#####>-<slug>, and is created from develop the first time one of the Epic's Stories starts.
  2. No Epic above it (a one-off). It lands on the support train, train/support. Every release first merges the train into develop through its own reviewed pull request.
  3. A one-off that changes deploy configuration or database migrations (anything under stacks/, sst.config.ts, or the migrations folder) never rides the train. It goes to develop by its own pull request instead. Why: the train reaches production soon after it merges, and such a change deserves its own cloud checks and an earlier deploy to the shared development environment.

Nothing lands on develop or master directly.

Branch names. A feature branch is <you>/<YY-MM-DD>/<I#####>-<slug>, for example jane/26-10-06/I#####-retry-failed-export. <you> is the first word of your git config user.name, lowercased. The date puts the year first so branches sort by date in any plain listing. Some older branches put the month first; they are mistakes, not a pattern to copy. The tools name branches for you.

Where work lands has the full rule.

3. Start the item

With an agent

Open a Claude Code session in wt1 or wt2 (for example cd ~/dev/devstride/.worktrees/wt1 && claude). The session takes that checkout's lease before it builds. Build one named item:

/devstride:build-item I#####

Or pass a plan's top item, and it builds the next unblocked item under it (the highest-priority one whose blockers are all Done):

/devstride:build-item I#####

Once you trust one pass, let it walk the whole plan, one item after another:

/loop /devstride:build-item I#####

build-item fetches the latest code, picks the item, moves it to In Progress, branches, builds, runs its risk check, runs the local checks, merges, and records on the item what actually shipped. Its DevStride changes are live: status changes, comments and dates reach your real organization straight away. If this session was just used for planning, run /clear first: the plugin keeps planning and building in separate sessions. The Delivery Loop describes every step.

By hand, or in Codex

In the checkout you leased, with a clean tree:

ds work start I#####

It reads the item and everything above it from DevStride, picks the target by the rule in step 2, creates the Epic's branch on GitHub if it does not exist yet, and cuts your feature branch from a fresh copy of the target. It prints the branch it made, the target it chose and why. It needs no AWS sign-in: it reads DevStride with the agents' own key.

OptionUse it when
--dry-runYou want to see the branch and target first. Nothing changes.
--slug <kebab-case>You want a different name after the item number. By default it comes from the item's title.
--one-offThe item sits under an Epic but is really a one-off (an inbound request filed there). It goes to the support train, never to that Epic's branch.
--infraThe one-off changes deploy configuration or migrations. The branch is cut from develop, to ship by its own pull request (step 6).
--target <branch>start stopped and asked: for example, the Epic already has an integration branch from before the epic- naming.

It starts only a Story or a Defect, one at a time, and refuses a checkout you have not leased.

ds work never changes the DevStride item. Move it to In Progress yourself when you start, and record on it what you shipped.

4. Build and try it

The app. Each checkout's app runs that checkout's code in Docker. ds worktree ls prints each checkout's address; main is at http://localhost:8080. No AWS sign-in is needed for any of it. After the Mac restarts, start Docker Desktop, then ds worktree up main starts main's app again.

Docker is the local path. When you need the real AWS services (queues, events, email, file storage), run a checkout against your machine's own AWS stage, which setup built: ds pool cloud wt1 runs wt1's code against it, and ds pool cloud wt1 --off goes back to Docker. One checkout per machine at a time. Your AWS stage explains it.

Demo data. Every checkout's database holds the Acme demo organization (the golden dataset), which setup built once and copied into each checkout. Sign in with the username acme.devstride (a username, not an email) and the password Demo@123. The same password works for the other demo people, such as alex.rte.

Reset the demo data in seconds. In a checkout you hold:

ds pool reset wt1

It copies the machine's demo-data snapshot over the checkout's database and re-creates the demo logins. If your branch adds migrations the snapshot lacks, it runs them and checks each one landed. If your branch is behind the snapshot, it refuses, because nothing moves a database backwards. Why: a full rebuild of the demo data takes minutes; a reset takes seconds. If the demo data itself is wrong, fix the generator rather than the rows; see Golden Dataset.

Database changes. After you add a migration, apply it to your checkout's own database with ds worktree cli <checkout> migrations run (for example ds worktree cli wt1 migrations run). Every migration must be safe to run twice and safe while old and new code run side by side; AGENTS.md, "Migration Safety", has the rules.

Generated types. The backend type-check needs the types that SST (the framework that deploys the backend to AWS) generates, which live in .sst/types and are never committed. Each checkout makes its own: ds verify land, ds verify full, the pre-push hook and setup all run cli/ensure_sst_types.sh first, which regenerates them in about five seconds, with no AWS sign-in, whenever they are missing or were made from different stack files. After you change stacks/ or sst.config.ts, the next of those runs catches up; to do it now, run sh cli/ensure_sst_types.sh.

Run one test with Vitest, the test runner, from the repository's top folder:

pnpm --dir backend exec vitest run tests/suits/path/to/test.spec.ts

Tests run against this checkout's own test databases in Docker. Never run two test runs against the same checkout at once: they corrupt each other's databases, so the second one is stopped.

Changed an API route? Regenerate the API client and the MCP files on the host, never through ds worktree cli, and check the generated files rather than the exit code. API Development explains why; the repository's ds-api-regen skill has the commands.

5. Check it locally

Before anything lands, it passes the landing check:

ds verify land

For the files your change touches, it:

  1. installs dependencies exactly as the lockfile pins them;
  2. makes the generated types, then runs every type-check;
  3. runs eslint (the frontend's linter) on the frontend files you changed. Errors fail the run; formatting is fixed in place, so commit what it changes;
  4. runs every backend and frontend test whose imports reach a file you changed. A change to something broad, such as the test harness or the lockfile, runs the whole backend suite instead and says why;
  5. runs the checks of the MCP server (DevStride's connection for AI agents) when packages/mcp changed, and the extra checks a changed path triggers (for example the golden-dataset assertions when the generator changes).

It prints one PASS or FAIL line per step and stops at the first failure. Its whole output goes to .ds/verify-land.log in the checkout, and a receipt to .ds/verify-land.json. ds verify land --dry-run lists the steps without running them.

What it needs, and what it does not:

  • This checkout's test databases in Docker, so Docker must be running. No AWS sign-in.
  • One deep check per machine at a time. A second run waits, and says whose run it is waiting on. Why: two full test runs on one machine starve each other of CPU, and their timeouts look exactly like broken code. A run that died leaves a lock the next run reclaims by itself.
  • It leaves four CPU cores free for the rest of the machine. When the only failures are up to five test files that timed out while setting up, with no test failing, it runs just those files once more and says so.

By default it compares your branch with the target ds work start recorded, otherwise with whichever of develop, the support train and the Epic branches your branch is closest to. --base <ref> sets it explicitly.

ds verify full is the bigger check: on this machine, everything cloud CI runs on a release, including the whole backend suite and the UI build. It takes about half an hour. With --post it records its result as the local-ci status on the exact commit it checked, and develop will not accept a pull request without a green one. The loop runs it on each pull request into develop before that pull request is marked ready; you rarely run it for a single Story. See local-ci.

6. Land it

A Story on an Epic branch, or a one-off on the support train, gets no pull request of its own. Once its checks pass, it is merged onto its target on your machine and pushed. The full review and CI are not skipped: they move to the release unit's pull request (the Epic's pull request into develop, or the train's at the next release), where they run once over the whole batch. This is fast develop mode.

With an agent

build-item lands the item itself. Before it merges one, it needs:

  • a completed risk check of the change by Claude;
  • an extra, focused check whenever the change touches sign-in or permissions, a database migration, anything that cannot be undone, or a contract a deployed service depends on. That one is never skipped;
  • a green landing check (ds verify land).

Then it merges the item's branch onto the target with a merge commit, pushes, and deletes the item's branch.

By hand, or in Codex

On your feature branch, with everything committed:

ds work land

It:

  1. brings the target's newer work into your branch, so the check covers what will actually land;
  2. runs ds verify land against the target. Its output goes to .ds/work-land.log; a failure lands nothing;
  3. merges your branch onto the target and pushes. If another session landed first, it tries once more.

It never pushes to develop, master or any other protected branch, and never forces a push. When it finishes, it says when the item ships:

  • A Story ships when its Epic merges to develop, and is Done then.
  • A one-off on the train ships with the next release, and is Done only when the release carries the train into develop. The release marks it Done.

Two refusals to know about:

  • A one-off that touches deploy configuration or migrations is refused before anything merges onto the train. The message gives the exact commands to move your branch onto develop and open its own pull request. Next time, start such a change with ds work start <I#####> --infra.
  • A branch started with --infra is never landed by ds work land. Open its pull request with /devstride:pr in Claude Code (it opens a draft and runs the full review), then add the label that lets it into develop: gh pr edit <number> --add-label merge-path-override. ds merge-path check --head <branch> --base develop tells you beforehand whether a branch may open a pull request into develop.

--no-verify skips the landing check, with a warning. It is refused for the support train, because nothing else checks that code before the train's pull request.

7. Hand the checkout back

When your work has landed or been handed over, stop every process you started in the checkout (test runs, dev servers, watchers), then:

ds pool checkin wt1

It refuses unless you hold the lease and the tree is clean. It stops the checkout's app (its test databases keep running), leaves it detached at a freshly fetched develop, and removes the lease. Its data is kept. Handing main back stops main's app too; ds worktree up main starts it again.

After you land: review, then CI, once

When an Epic's last Story has landed, the Epic is release-ready. In this repository its pull request into develop waits for the owner to cut it; Release covers that step and the release to production. What happens on every pull request is the same:

  1. It opens as a draft, and CI does not run on drafts. /devstride:pr opens it that way. A pull request a person opens straight to ready fails the ci-policy check (the check that enforces drafts first).
  2. Reviewers run first. Two run on every pull request, at full depth: an adversarial Claude pass and Codex's command-line reviewer. GitHub Copilot joins only on pull requests into master (the release, or a hotfix). It is asked once, on the final version, and again only if a fix answers one of its own findings.
  3. Every finding is fixed, answered or dismissed with a written reason. Two rounds is the normal target; a confirmed serious problem keeps opening rounds until one finds nothing.
  4. The pull request is marked ready, which starts CI once, on exactly the code that was reviewed. Pushing or editing it after that starts CI again, so finish the title and description first.
  5. Two checks do the real work on a pull request into develop. merge-path admits only an Epic branch, the train, a release or hotfix branch, a sync from master or a Dependabot update, unless the pull request carries merge-path-override. local-ci is the full local check from step 5. The heaviest cloud checks (the backend suite, the linters and the UI tests) report skipped there and run once per release, on the pull request into master.

Because fast develop mode skips the per-Story pull request, the Epic's pull request is the first time any outside reviewer or CI sees that code, so it is reviewed in full, every Story included. The Delivery Loop has the mechanics.

The merge guard

Every Claude Code and Codex session in the repository runs a small check before each shell command. It refuses the commands that would skip the flow above:

  • a push to develop or master, in any form;
  • a forced or deleting push to a protected branch, such as a release/* branch;
  • git merge while you are on develop or master;
  • gh pr merge into develop unless the merge-path rule admits the branch, and into master unless it is a release or hotfix branch;
  • gh api writes to GitHub's branch rules.

When a session starts, the same check prints a short banner: the checkout you hold, where your branch lands, and any open release.

It is a speed bump, not a lock. It fails open: when it cannot run (no Python, a command it cannot read), the command goes through and the reason is written to ~/.cache/devstride/merge-guard.log. It only sees the command line, so a script or GitHub's web page gets past it. The real boundary is on GitHub: the required checks and branch rules.

What you should see

  • ds pool checkout printed LEASED, and ds pool status shows the checkout as yours.
  • ds work start (or build-item) named your feature branch and its target: the Epic's branch or train/support.
  • ds verify land printed PASS for every step.
  • ds work land (or build-item) said the item landed, and when it ships.
  • ds pool checkin handed the checkout back, clean and parked at develop.

When something goes wrong

For everything else, start at Troubleshooting.

Reference

Next: Release