This is the complete reference for every ds command that actually exists in the codebase today. Every command and flag below was verified directly against cli/commands/*.ts — nothing here is aspirational or planned.
The ds CLI has exactly 22 top-level commands: run, migrations, data, golden, script, stripe, g, worktree, pool, work, merge-path, verify, secrets, machine, fleet, agent, quickbooks, graph, team, stage, seed and neon. ds worktree is documented on Dockerized Worktree Development; every other command is below. There is no ds setup, ds deploy, ds db, ds local, ds api, ds integrations, ds utils, ds aws, or ds help — those command surfaces do not exist in this CLI.
Every invocation goes through the root ./ds bash script before Commander ever sees your subcommand. For an AWS-bound command it does three things, in order: an AWS SSO auth gate, a stage/region "bind" step, then execution of your actual command. ds run local, ds worktree, ds pool, ds work, ds merge-path, ds verify, ds secrets, ds machine, ds fleet, ds agent, ds quickbooks and ds graph skip the first two and run straight away — the ones that need AWS check the account themselves.
./ds [-b] [-r] [-u] <command> [subcommand] [args]
| Flag | Description |
|---|---|
-b | Force re-bind stage/region config, even if a cached .ds/bind/<stage>-<region>.env already exists |
-r | Target the remote stage — unsets IS_LOCAL, sets DEVSTRIDE_REMOTE=true |
-u | Skip the AWS SSO auth check entirely (aws sts get-caller-identity / aws sso login) |
aws sso login --profile <profile> and exits 1 instead of hanging on a device-code prompt nobody can see. A machine certificate identity is never sent to a login at all: the wrapper reports the identity as refused and exits 1. When the active profile (AWS_PROFILE, else AWS_DEFAULT_PROFILE, else default) is a role on top of another profile through source_profile, the wrapper follows that chain and logs in to the profile that actually signs in, naming both in its message. Details in The AWS SSO Gate.-b is passed, the wrapper reuses the cached .ds/bind/$DEVSTRIDE_STAGE-$DEVSTRIDE_REGION.env file if one exists, and only regenerates it (via cli/bin/store_bind.mjs) when it's missing. If you change stage/region config and the CLI doesn't seem to pick it up, run with -b.Every command in this reference is invoked as ./ds <command> <subcommand> [flags] — each <command>.ts file under cli/commands/ is its own Commander program, resolved by name from the second CLI argument.
ds runDefined in cli/commands/run.ts, delegating to cli/commands/sst/run.ts. Three subcommands: two for the AWS-bound personal stage, one for the fully-local Docker stack.
ds run backendStarts the backend: an SST live-lambda dev session against the Hono API.
ds run backend
Runs, in order: pnpm install, then deleteOrphanStacks() (cleans up orphaned CloudFormation stacks left behind by earlier sst.config.ts changes), then pnpm exec sst dev.
It refuses to start when a pool checkout is already running against the same stage through ds pool cloud — two sst dev sessions on one stage take each other's requests. Unbind that checkout first with ds pool cloud <checkout> --off.
ds run backend does not start Docker or any local services. In normal (non-test) code, DynamoDB access always points at real AWS — local Docker DynamoDB is only used when NODE_ENV=test, and only the backend test suite spins those containers up (in its own setup code, independent of ds run backend). Docker is not part of the everyday local-dev loop.ds run uiStarts the Vite frontend dev server.
ds run ui
Spawns pnpm run dev inside frontend/, injecting VITE_* environment variables derived from the bound stage (API/auth/media/importer root URLs, Pusher key/cluster, Stripe public key, GitHub app name, Giphy key, SMTP settings, plugin host allowlist, etc.). The dev server is pinned to VITE_DEV_SERVER_PORT (default 8080) so it cannot silently move off the origin registered with Cognito and API CORS.
ds run localds run local [--rebuild]
Brings up the fully-local, no-AWS Docker stack: the shared core (Postgres, DynamoDB, MinIO, cognito-local) plus the primary main instance, in the foreground. It skips the SSO gate and the stage bind entirely. --rebuild rebuilds the shared devstride-local image first (after a dependency change). Everything else about this stack is on Dockerized Worktree Development.
ds migrationsDefined in cli/commands/migrations.ts. Runs the Drizzle SQL migrations against the bound database, plus a handful of operational repair and inspection scripts that live beside them.
Migrations come in two chains, so a rolling deploy can run old and new Lambda revisions side by side: expansion migrations (additive schema changes, safe for both revisions) and contract migrations (destructive or tightening changes, safe only once every new Lambda is live). The deploy pipeline runs them as two separate steps around the code switch; locally you normally run both at once.
ds migrations runds migrations run
Runs the expansion chain, then the contract chain, bringing the bound database to the latest schema. No flags. This is the everyday local command.
ds migrations run-sqlds migrations run-sql
Same as ds migrations run — both call the same function, which runs expansion then contract. The two names are kept for compatibility; prefer run. No flags.
ds migrations run-sql-expand / run-sql-contractds migrations run-sql-expand # additive migrations only — the deploy pipeline's before_deploy step
ds migrations run-sql-contract # tightening migrations only — the deploy pipeline's after_deploy step
The two halves of run, split so Seed can apply the additive schema changes before SST switches Lambda code and the destructive ones only after every new Lambda is live (see Deployment). Locally you rarely need them separately. No flags.
ds migrations repair-time-rollupsds migrations repair-time-rollups [--org <organizationId>] [--items I123,I456] [--dry-run] [--limit <n>]
Repairs work-item time rollups that drifted from their completed time entries. It applies by default; --dry-run only reports. --org scopes to one organization, --items to a comma-separated list of item numbers, and --limit caps the rows displayed (default 50) — the repair itself covers every detected row.
ds migrations production-inventoryds migrations production-inventory [--verify] [--attest-evidence] [--expiry-days <days>]
Read-only production topology and durability inventory. By default it writes a signed, content-free manifest; --verify checks the committed manifest plus the attested manual evidence and exits 1 on any blocker; --attest-evidence is the owner step that binds the filled evidence file to the current manifest signature. --expiry-days is the manifest freshness window (default 14, the replay-horizon retention).
Four subcommands operate on the publish-intent journal — the durable record of integration events waiting to be published — for incident work. Read them as a set: two inspect, two change state.
| Command | What it does |
|---|---|
ds migrations inspect-publish-intents [--status <status>] [--limit <n>] | Read-only list of journal rows by status (pending, failed, unknown, quarantined, published; default quarantined). --limit defaults to 50 |
ds migrations stuck-fifo-groups [--min-head-age-minutes <n>] [--limit <n>] | Read-only list of FIFO groups whose head is wedged non-published and blocking successors, most-blocking first. A head older than --min-head-age-minutes (default 60) is flagged even when it is not quarantined |
ds migrations redrive-publish-intents --ids <csv> [--dry-run] [--include-unknown] [--include-permanent] | Resets quarantined intents to failed so the dispatcher sends them again, identity preserved. --include-unknown also redrives unknown rows, which is unsafe for EventBridge (they may already be delivered, and have no dedup id) — only with out-of-band reconciliation. --include-permanent also redrives permanently unrecoverable quarantines (a checksum mismatch, say), which is futile until the underlying data or code is fixed |
ds migrations skip-publish-intents --ids <csv> --yes-deployed [--dry-run] | Terminally marks quarantined intents skipped (deliberately not sending), releasing any FIFO successors they hold. --yes-deployed is required to write: it confirms the release with skipped support is fully deployed, because an older dispatcher keeps blocking behind a skipped head and cannot undo it |
ds migrations sample-legacy-sql-usage [--endpoint primary|replica]
ds migrations legacy-sql-usage-report [--days <n>]
Part of the Lane→Status and Folder→Workstream rename: sample-legacy-sql-usage samples pg_stat_statements on the bound stage's primary and replica for legacy table and column names and writes the samples to legacy_sql_usage_samples (it stores no query text; default: both endpoints when they differ). legacy-sql-usage-report prints a per-role report of those samples — first and last seen, calls, names, gaps — over a window of --days (default 7). Together they show whether anything still reads the old names before they are dropped.
ds dataDefined in cli/commands/data.ts. Bulk SQL table import/export/wipe, plus single-organization copy, against the bound database.
ds data importds data import <path> [-t <tables>]
Bulk-restores SQL tables from a directory previously produced by ds data export.
| Flag | Description |
|---|---|
<path> | (required, positional) Directory to import from |
-t, --tables <tables> | Comma-separated list of table names to import (default: all tables) |
ds data exportds data export [path] [-t <tables>]
Dumps SQL tables to disk as chunked JSON files, one directory per table.
| Flag | Description |
|---|---|
[path] | (optional, positional) Output directory. Default: .ds/data/<DEVSTRIDE_STAGE> |
-t, --tables <tables> | Comma-separated list of table names to export (default: all tables) |
Output lands under <path>/sql/<tableName>/0.json, 1.json, … (1000 rows per chunk). The workflowRuns table is always excluded.
ds data wipeds data wipe [-t <tables>]
Deletes all rows from the selected (or all) SQL tables. Temporarily sets session_replication_role = 'replica' to bypass FK constraint ordering while deleting.
| Flag | Description |
|---|---|
-t, --tables <tables> | Comma-separated list of table names to wipe (default: all tables) |
DEVSTRIDE_STAGE === "prod".ds data copyds data copy [-o <organization>] [-s <source>]
Org-scoped copy: pulls one organization's data from a source database into the bound database.
| Flag | Description |
|---|---|
-o, --organization <organization> | Organization ID to copy. Default: 970b8dea-c8b4-44d8-8aaf-febf3f25138a (the historical demo org; ds script reset-db no longer copies it — it builds the golden Acme org instead) |
-s, --source <keyOrUrl> | Source DB: either the name of an env var (e.g. GOLDEN_DB_CONNECTION_STRING) or a full connection string. Omitted in an interactive terminal, you get a picker over the *_CONNECTION_STRING vars in .env; omitted non-interactively, it defaults to SOURCE_DB_CONNECTION_STRING |
ds data copy is marker-unaware — it shares the underlying copyOrgData helper with ds golden import, but it does not reset the stage's golden_handle.anchorIso marker. If the organization ID you pass is one of the golden persona orgs, the command prints a warning and recommends ds golden import (or a follow-up ds golden reanchor --to today) instead, so a later reanchor doesn't compute its shift from a stale anchor.ds goldenDefined in cli/commands/golden.ts. Manages the golden demo/test dataset — the Acme organization — on the bound stage. Twenty subcommands, all lazy-imported so only the one you invoke loads the heavy backend graph. They fall into three groups:
| Group | Subcommands | Use it to |
|---|---|---|
| Seed a stage | build (load), push, import, refresh-stage, reanchor, aichat, cognito, enrich, reset, lifecycle, preview, status | Get Acme onto a stage, keep its dates at "today", and check its health |
| Keep the golden source current | recertify, reconcile, release <action> | Maintain the canonical source database every stage and training sandbox clones from |
| Training sandboxes | install, invite-admin, cognito-sandbox, aichat-sandbox, s3-sandbox | Mint one customer-facing sandbox organization from the source, and tear its identities down |
Every mutating subcommand is guarded by assertGoldenStageWritable: all mutating golden subcommands refuse to run against prod. On the shared dev stage only the additive verbs are allowed — import, reanchor, refresh-stage, aichat and cognito — while build, reset, enrich, lifecycle and push are refused there. push is a whole-DB replace and is refused on both prod and dev; it is only safe on a fresh, disposable stage.
acme|bright|indie|brightTrial for --org. The golden spec's ORG_IDS is Acme-only, so every other name is rejected at runtime. Acme is the whole dataset.ds golden build (alias: load)ds golden build [--no-reset] [--no-verify] [--skip-cognito] [--org <name>]
Rebuilds the golden Acme org, in the bound DB, from the generator spec (real CommandBus calls), ends with a full aggregation recalculation, runs the §16 assertion gate, then creates the Cognito logins. Scoped — only the Acme org's rows are reset/rebuilt; everything else on the stage is untouched. load is a Commander alias of build, not a separate implementation. On the fully-local Docker stack (ds worktree cli <instance> golden build) the §16 gate is skipped automatically.
| Flag | Description |
|---|---|
--no-reset | Skip the reset — only on a clean target where the Acme org is absent |
--no-verify | Skip the §16 assertion gate after building |
--skip-cognito | Skip the Cognito login reconciliation at the end (ds golden cognito runs it later) |
--org <name> | Rebuild a specific golden org in place (currently only acme) |
ds golden pushds golden push --force-full-reset [--allow-any-scale] [--skip-aichat] [--skip-cognito] [--dry-run] [--target-db <connectionString>]
Fast whole-database seed: pg_dumps the locally vitest-built golden template DB and pg_restores it straight into the bound stage's Postgres, then re-seeds the DynamoDB AI chats and Cognito logins. This is a whole-DB replace, intended only for fresh/disposable stages — it is refused by the stage guard on both prod and dev.
| Flag | Description |
|---|---|
--force-full-reset | Required to proceed — drops the stage's public + drizzle schemas and replaces all stage data with the golden template |
--allow-any-scale | Skip the full-scale floor check (allows pushing a dev-scale template) |
--skip-aichat | Skip the DynamoDB AI-chat leg |
--skip-cognito | Skip the Cognito login leg |
--dry-run | Run preflight checks (container/template/scale) and print the target + template + row counts, but drop/restore nothing |
--target-db <connectionString> | Push to an explicit Postgres connection string instead of the bound stage — used to publish the gated template into the dedicated golden source DB and for CI. Relaxes the stage journal guard and skips the AWS-bound aichat/Cognito legs |
ds golden importds golden import [--org <name>] [--skip-aichat] [--source <envVarName|connectionString>] [--dry-run]
Additive, org-scoped seed: copies the golden persona org(s) from the canonical golden source (GOLDEN_DB_CONNECTION_STRING — distinct from the generic SOURCE_DB_CONNECTION_STRING used by ds data copy) into the bound stage without wiping it. This is the safe path for shared/populated stages such as app.devstride.dev. Cognito logins are created as part of the copy.
| Flag | Description |
|---|---|
--org <name> | Import one persona org instead of all. Currently acme is the only golden persona org — the CLI's own help text still lists bright/indie/brightTrial as options, but ORG_IDS in the golden spec is Acme-only, so those other names are rejected at runtime |
--skip-aichat | Skip the DynamoDB AI-chat leg |
--source <envVarName|connectionString> | Golden source override — a connection string, or the name of an env var. Defaults to GOLDEN_DB_CONNECTION_STRING, then an interactive picker at a TTY |
--dry-run | Run the parity/provenance checks and print the planned orgs, but make no changes |
ds golden push and ds golden import refuse to run when the checkout, the golden source, and
the target stage are not on the same set of migrations — the guard that stops a golden copy
landing on a schema it was not built for. It compares journal membership, not order, so a
database that applied a migration out of band is not mistaken for drift, and when it does fire the
error names the migrations actually missing on each side rather than a position in the list.ds golden reanchords golden reanchor [--to <YYYY-MM-DD|today>] [--shift <±days>] [--org <name>] [--target-db <connectionString>] [--dry-run]
Shifts an already-built stage's entire temporal dataset (item dates, transactions, activity logs, timeboxes, roadmaps, custom-field dates, …) by a uniform delta without rebuilding — re-anchors the demo to a new "today". Transactional, with a post-shift sanity probe. Operational times are left where they are: sign-in and authorization tokens, integration leases, billing records, jobs and sweep countdowns are never shifted, so a re-date cannot strand a lease, re-fire a sweep or rewrite an audit trail.
| Flag | Description |
|---|---|
--to <YYYY-MM-DD|today> | Re-anchor to an absolute target date (delta = target − stored anchor). Default when no flag is given: today (real wall-clock UTC) |
--shift <±days> | Relative shift in whole days (mutually exclusive with --to) |
--org <name> | Reanchor one persona org instead of all. Currently acme-only in practice — see the note on ds golden import above |
--target-db <connectionString> | Shift an explicit Postgres DB (e.g. the local golden template) instead of the bound stage — for testing; skips the AWS AI-chat leg |
--dry-run | Read the stored anchor, compute the delta, print the plan + row counts, mutate nothing |
ds golden statusds golden status [--target-db <connectionString>] [--skip-source]
Read-only health report: are the persona orgs present, is the dataset anchored at today, is it in sync with the canonical source, does Acme carry the full 5-status cumulative-flow staircase. Mutates nothing; exits non-zero when missing/stale/out-of-sync.
| Flag | Description |
|---|---|
--target-db <connectionString> | Inspect an explicit Postgres connection string instead of the bound stage |
--skip-source | Skip the comparison against the canonical golden source |
ds golden refresh-stageds golden refresh-stage [--org <name>] [--skip-aichat]
One-shot recovery command: additive import → reanchor --to today → verify. Use this when a dev stage was reset and the golden orgs disappeared. Rebuilds nothing (pulls the intact gated source); safe to re-run; leaves other tenants untouched.
| Flag | Description |
|---|---|
--org <name> | Refresh one persona org instead of all. Currently acme-only in practice — see the note on ds golden import above |
--skip-aichat | Skip the DynamoDB AI-chat leg of the import |
ds golden aichatds golden aichat [--org <name>] [--force]
Seeds the golden AI-assistant chats in DynamoDB. Run after push or build if the Dynamo leg was skipped or failed.
| Flag | Description |
|---|---|
--org <name> | Only seed one org's chat. Currently acme-only in practice — see the note on ds golden import above |
--force | Delete each persona's existing chat(s) before seeding, so an updated transcript is written instead of the stale one being kept |
ds golden enrichds golden enrich [--org <name>] [--aspect <names...>]
Additively patches an existing golden org in place — descriptions, activity, and similar polish — without a rebuild. Idempotent, never truncates. Distinct from build, which rebuilds from scratch.
| Flag | Description |
|---|---|
--org <name> | The org to enrich. Default: acme (currently the only scaled org with an enrich profile) |
--aspect <names...> | Which data point(s) to patch: descriptions, archive-example, feature-planning, admin-governance, pi-objectives, comments, time-entries, activity (slow). Default: all aspects. The command's own help text lists only six of the eight; ALL_ENRICH_ASPECTS in backend/tests/golden/generator/enrich.ts is the authority |
ds golden lifecycleds golden lifecycle [--workers <n>] [--target-db <connectionString>]
Intended purpose: parallel lane walk that gives every scaled Acme item a back-dated PUBLISHED lane/status history across N worker processes. No-op at representative scale; idempotent and deterministic when it does run.
| Flag | Description |
|---|---|
--workers <n> | Number of worker processes. Default: min(8, cores) or GOLDEN_LIFECYCLE_WORKERS |
--target-db <connectionString> | Run the walk against an explicit Postgres DB instead of the bound stage — canonical use is the local Docker golden template |
ds golden resetds golden reset [--org <name>]
Deletes the golden Acme org's rows in the bound DB. Scoped — only the Acme org is removed; all other data on the stage is left intact (never a whole-DB truncate).
| Flag | Description |
|---|---|
--org <name> | Reset a specific golden org (currently only acme) |
ds golden cognitods golden cognito [--org <name>]
Seeds Cognito logins (password Demo@123) for the golden persona users. Run after build.
| Flag | Description |
|---|---|
--org <name> | Only seed one org's users. Currently acme-only in practice — see the note on ds golden import above |
ds golden previewds golden preview
Renders the golden spec to text — pure spec-to-text dump, no DB access at all. No flags. ds -u golden preview skips the SSO bind as well.
ds golden recertifyds golden recertify [--source <envVarName|connectionString> | --bound] [--dry-run]
Re-runs the §16 assertion gate against an already-built golden source and re-stamps its clone certification, without rebuilding (minutes, not hours). Certification is bound to the checkout's full migration journal, so any migration that lands decertifies the source and makes sandbox clone and reset refuse; this is the cheap fix. It genuinely re-executes every assertion and stamps only on a clean pass — a failing gate leaves the source uncertified.
| Flag | Description |
|---|---|
--source <envVarName|connectionString> | The database to re-certify. Default: GOLDEN_DB_CONNECTION_STRING, the canonical source. Certified with scope postgres-only: the Dynamo-backed AI-chat assertion is skipped because the source holds no chats |
--bound | Re-certify the bound stage's own database instead (scope full). Mutually exclusive with --source |
--dry-run | Check that the recorded gate handle is complete and the anchor resolves, then stop |
ds golden reconcileds golden reconcile [--source <envVarName|connectionString>]
Brings the canonical golden source up to this checkout's revision. A source that already matches (expansion journal, contract journal and generator digest) is not re-stamped, which preserves sandbox provenance. Journal drift — a migration landed — takes the cheap tier: run both migration chains, re-run the §16 gate, re-stamp (it delegates to recertify). Generator drift, or a source with no recorded generator provenance, exits 42 immediately: re-certification re-proves data, it does not re-run the generator, so only a full rebuild helps. The Golden Dataset — Refresh Source workflow runs this after every push to master and pages the operator on exit 42; run by hand, it re-checks production liveness only when GOLDEN_RECONCILE_LIVENESS_COMMAND is set, so a manual run must come from a checkout at production's live revision. --source defaults to GOLDEN_DB_CONNECTION_STRING and refuses the bound stage's own database. The full runbook is Workflow B in the repository's docs/golden/stages-and-secrets.md.
ds golden release <action>ds golden release <probe|prepare|check|publish> [--candidate <sha>] [--artifact-root <directory>] [--source-env <name>] [--merge-commit <sha>]
Prepares and verifies the golden source around a production release, so the source is never migrated ahead of production and no expensive build runs in cloud CI. probe reports what the candidate needs; prepare exports a current source, rehearses ordinary migrations against a local copy, and runs a full local build when the generator changed or a data check failed; check is the release's mandatory local pre-ship gate (golden-source-release-readiness in .claude/ds-config.json) and refuses missing, stale, corrupt or wrong-candidate evidence; publish runs after the matching production revision is live and healthy.
| Flag | Description |
|---|---|
--candidate <sha> | The reviewed release source commit. Default: HEAD |
--artifact-root <directory> | Private absolute archive directory. Default: GOLDEN_RELEASE_ARTIFACT_ROOT, else the checkout default |
--source-env <name> | Environment variable holding the canonical source URL. Default: GOLDEN_DB_CONNECTION_STRING |
--merge-commit <sha> | The production merge to verify before publication |
The repository's ds-golden-release skill owns the procedure; the release skill's post-deploy check (ds-post-deploy-release-check) runs publish.
ds golden installds golden install --slug <slug> [--label <label>] [--org-id <uuid>] [--day <YYYY-MM-DD>] [--source <envVarName|connectionString>] [--target-db <connectionString>] [--customer-email <email>] [--skip-cognito] [--skip-aichat] [--resume] [--dry-run]
Never-truncate sandbox provisioning: mints one new sandbox organization from the certified golden source through the additive clone engine (insert-only, the target org identity must be free, bystander tenants untouched, §16-gated), reanchored so the clone reads as "today". There is no wipe flag here by design; whole-DB replaces belong to push.
| Flag | Description |
|---|---|
--slug <slug> | Required. Becomes the org name and the acme.<slug>. user-identity namespace. acme is reserved |
--label <label> | Display label for the sandbox org. Default: ACME-<SLUG> |
--org-id <uuid> | Explicit target organization id. Default: freshly minted |
--day <YYYY-MM-DD> | The UTC day the clone should read as "today". Default: today |
--source <envVarName|connectionString> | Golden source override. Default: GOLDEN_DB_CONNECTION_STRING |
--target-db <connectionString> | Install into an explicit Postgres connection string instead of the bound stage |
--customer-email <email> | Invite this email as org Admin of the new sandbox (bound stage only; refused with --target-db). The customer completes signup with their own password |
--skip-cognito / --skip-aichat | Skip the post-install persona-login or demo-chat legs; cognito-sandbox / aichat-sandbox run them later |
--resume | Continue an install whose clone committed but whose post-clone legs failed: skips the clone and re-runs the idempotent Cognito, AI-chat and invite legs for this --slug |
--dry-run | Resolve, validate and print the install plan; connect to nothing |
ds golden invite-adminds golden invite-admin --org-id <uuid> --email <email>
Invites a customer's real email as org Admin of an already-installed sandbox: creates the invited placeholder user and Admin membership and sends the standard invitation email. The customer signs up with their own password, which mints their Cognito identity and activates the membership. Additive and idempotent: re-running resends the invite for a still-invited member and does nothing for an active one.
ds golden cognito-sandbox / aichat-sandbox / s3-sandboxds golden cognito-sandbox --org-id <uuid> [--teardown]
ds golden aichat-sandbox --org-id <uuid> [--anchor-iso <iso>] [--force]
ds golden s3-sandbox --org-id <uuid> --teardown
The per-sandbox legs, all fail-closed to sandbox orgs and the acme.<slug>. namespace, so sibling orgs and the canonical golden data are never touched:
| Command | What it does |
|---|---|
cognito-sandbox | Materializes the sandbox's persona logins in the shared per-stage pool (username = the cloned stable user id, a deterministic non-routable email, the permanent demo password). --teardown deletes only the sandbox's recorded identities and proves siblings intact |
aichat-sandbox | Seeds the canonical demo AI-chat transcript into the sandbox, owned by the clone-mapped demo persona and stamped at the sandbox's anchor (--anchor-iso overrides it). --force deletes the persona's existing sandbox chats first |
s3-sandbox | Teardown only (--teardown is required): deletes the S3 objects the sandbox's users uploaded live, under the sandbox's own org-id and persona key prefixes. There is no S3 provisioning at install — golden assets are inline data URIs |
ds scriptDefined in cli/commands/script.ts. A grab-bag of one-off scripts and everyday maintenance commands — most take no flags at all. Maintenance & Codebase Checks explains when to reach for each; this section lists what exists.
ds script find-missing-initsds script find-missing-inits
Codebase scan for Command/Query handlers that aren't wired up in their module's init file. No flags.
ds script find-missing-set-correlation-idsds script find-missing-set-correlation-ids
Codebase scan for handlers missing correlation-ID propagation. No flags.
ds script find-missing-events-registrationds script find-missing-events-registration
Codebase scan for domain/integration events that aren't registered. No flags.
ds script find-missing-events-in-stackds script find-missing-events-in-stack
Codebase scan for event handlers missing from the infrastructure stack. No flags.
ds script find-missing-eventbridge-target-dlq / find-missing-sns-subscription-dlqds script find-missing-eventbridge-target-dlq
ds script find-missing-sns-subscription-dlq
Two more wiring scans, over the infrastructure stacks rather than the module code: every EventBridge rule target, and every SNS subscription, must have a dead-letter queue, or a failed delivery is lost silently. Both print nothing when they pass. They run in the pre-commit hook beside the four scans above, and as part of the infra-synthesis pre-ship check whenever a change touches stacks/** or deploy configuration. No flags.
ds script generate-api-docsds script generate-api-docs
Generates the public-facing OpenAPI spec with embedded documentation. No flags.
ds script generate-api-clientds script generate-api-client
Generates the typed frontend TypeScript API client SDK from backend Hono routes (also writes the canonical .ds/tmp/openapi.json). No flags.
ds script generate-api-mcpds script generate-api-mcp
Orchestrates the full MCP-SDK refresh, in order: generate-api-client → generate-api-docs → regenerates packages/mcp/src/devstride/gen/* (sdk, types, schemas, operations, components) by running packages/mcp/scripts/generate.ts. Run this after any backend OpenAPI change — the generated packages/mcp files are committed to git. No flags.
ds script delete-orphaned-itemsds script delete-orphaned-items
Deletes orphaned folders, work items, activity, comments, and assets from the database. No flags.
ds script set-configds script set-config
Validates 22 unconditionally-required environment variables (DB connection strings including the read-only replica, Stripe keys, OpenAI key, Azure DevOps + Jira app credentials, all four Pusher vars, GitHub app credentials, and more), plus 5 more that are required only if SMTP is configured, then pushes them up as sst secrets set DEVSTRIDE_CONFIG (base64-encoded JSON). No flags.
ds script set-config writes your local .env values up into the SST DEVSTRIDE_CONFIG secret — it does not pull anything down. To bring values down into .env, use ds secrets pull, then run ds script set-config to push them into your stage. The running backend does not read .env directly at runtime — the CLI-side dotenv.config() only feeds the CLI/build-time process used to synth sst.config.ts. The actual Lambda / sst dev process reads secrets via the SST Config binding, which is exactly what this command populates.
The correct command name is ds script set-config. ds script set-secrets does not exist in this CLI.
ds script sync-identity-providersds script sync-identity-providers
Puts Google (federated) sign-in back on the stage's Cognito app client after a deploy. CDK pins the client's supported-provider list to [COGNITO] on every deploy, because the Google client secret lives inside the DEVSTRIDE_CONFIG secret where CloudFormation cannot read it, so federated login is unavailable until this runs. Idempotent and declarative: it does nothing on a stage with no Google keys and removes a stale provider when the keys are withdrawn; password sign-in is never affected. The deploy pipeline runs it in after_deploy, before the contract migrations. No flags.
ds script check-orphan-stacksds script check-orphan-stacks
Reports orphaned CloudFormation stacks left behind by earlier sst.config.ts changes. No flags.
ds script delete-orphan-stacksds script delete-orphan-stacks
Deletes the orphaned CloudFormation stacks found by the check above. Also invoked internally by ds run backend and by ds script reset-db on AWS-bound stages (skipped on the local Docker stack). No flags.
ds script assistantds script assistant
langchain/chat_models, langchain/agents). Not a reliable everyday tool — flagged here as likely unmaintained.No flags.
ds script get-active-emailsds script get-active-emails
Dumps active users' emails across organizations. No flags.
ds script maintenance-onds script maintenance-on
Enables maintenance mode by toggling a Lambda environment variable. No flags.
ds script maintenance-offds script maintenance-off
Disables maintenance mode. No flags.
ds script api-maintenance-on / api-maintenance-offds script api-maintenance-on
ds script api-maintenance-off
The same toggle, for the public API's Lambdas only: the app keeps working while the API is held. No flags.
ds script backfill-access-permissionsds script backfill-access-permissions
Idempotent SQL backfill of baseline module-access permission keys. No flags.
ds script backfill-description-asset-itemsds script backfill-description-asset-items
DEVSTRIDE_BACKFILL_DRY_RUN=true ds script backfill-description-asset-items # report only
The half of the description-asset repair a SQL migration cannot do: parses the assets embedded in item descriptions and unfurls their metadata into asset items. DEVSTRIDE_BACKFILL_DRY_RUN=true reports without writing. No flags.
ds script inspect-user-permissionsds script inspect-user-permissions <username>
Diagnostic dump of every organization membership for a username, including the role's permissions and is_system_owner flag — useful for telling a stale DB role apart from a stale frontend session. The username is read positionally from process.argv, not a Commander-declared argument; omitting it prints a usage message and exits non-zero. No flags.
ds script reset-dbds worktree cli <instance> script reset-db # local Docker stack
ds script reset-db # AWS-bound personal stage
Full reset of one database: terminates other DB connections, drops and recreates the public schema (and drops drizzle), deletes every Cognito user (all pages of the pool), re-runs SQL migrations, and builds the generated Golden dataset from nothing (ds golden build with a scoped reset, Cognito logins included). On an AWS-bound stage it also re-adds Stripe products and cleans up orphan stacks; on the local Docker stack (ds worktree cli …) those two steps and the §16 golden gate are skipped. No source database is read. No flags.
DEVSTRIDE_STAGE === "prod"; the golden stage guard also refuses the shared dev staging stage. A bare ds script reset-db binds to AWS — on the local stack always go through ds worktree cli <instance>, using the instance registered for the checkout you are on.ds script remove-orgds script remove-org [-o <organization>]
Deletes all rows for one organization.
| Flag | Description |
|---|---|
-o, --organization <organization> | Organization ID to remove |
Dev-only guard applies (not safe to run on prod).
ds script create-slack-workflowsds script create-slack-workflows
One-off historical migration: provisions Slack notification workflows for existing users. Never run it concurrently with the Teams backfill below or with notification-settings writes. No flags.
ds script create-teams-workflowsds script create-teams-workflows --app-origin <url> [--org <organizationId>] [--dry-run]
Reconciles existing Microsoft Teams reminder workflows for existing users. Apply only after the new settings API and membership consumer are deployed and old revisions have drained, and never alongside the Slack backfill above.
| Flag | Description |
|---|---|
--app-origin <url> | Required for a real run. Must equal the app origin the process resolves (printed first) — it is the host baked into every created link |
--org <organizationId> | Only this organization |
--dry-run | Count what would be created, write nothing |
ds script billing-create-accountds script billing-create-account --org <organizationId> [--status <status>] [--pays-by card|invoice] [--trial-days <days>]
Rehearsal only. Makes DevStride the manager of one organization's billing on a dev stage, through the real command. It refuses production by stage name and by the database it would write to.
| Flag | Description |
|---|---|
--org <organizationId> | Required. The organization to manage |
--status <status> | trial (default), active, trial_expired, suspended or closed |
--pays-by <method> | card or invoice; omit for "none chosen yet" |
--trial-days <days> | Trial length when --status is trial. Default: 14 |
ds script provision-canary-orgds script provision-canary-org --owner <account-id>
Creates the hidden canary organization and its inbound email address, once, on production — the only stage that probes it. Idempotent; a second run changes nothing. It refuses a run whose stage and database disagree in either direction. --owner (required) is the account id of the DevStride staff member who owns the canary organization; they must be active with a @devstride.com email, or the organization is billed.
ds script event-contract-testds script event-contract-test [--skip-negative] [--wait-alarm] [--allow-prod-alarms] [--timeout <seconds>]
Publishes one synthetic standard event and one synthetic FIFO event through the live EventBridge bus and SNS FIFO topic on a deployed stage and asserts they are consumed. --skip-negative runs only the delivery smoke test; --wait-alarm also waits for the sentinel alarm to reach ALARM (slow); --allow-prod-alarms permits the negative path, which fires sentinel alarms, on prod or dev; --timeout is the poll timeout in seconds (default 300). Details on Maintenance & Codebase Checks.
ds script snapshot-event-topologyds script snapshot-event-topology [--check] [--out <path>]
Writes a normalized snapshot of the live SNS, SQS and EventBridge topology to infra/event-topology/<stage>.baseline.json. --check diffs the live topology against the committed baseline and exits 1 on drift; --out overrides the baseline path.
ds script migrate-workitem-timespent-to-timeentryds script migrate-workitem-timespent-to-timeentry
One-off historical migration: converts legacy work-item time-spent data into time-entry records. No flags.
ds script add-default-sidebar-form-groupds script add-default-sidebar-form-group
One-off historical migration: backfills a default sidebar form group. No flags.
create-slack-workflows, migrate-workitem-timespent-to-timeentry, and add-default-sidebar-form-group are historical, single-use migration scripts kept for the record — not part of the regular development loop.ds stripeDefined in cli/commands/stripe.ts. Two subcommands: a read-only billing inventory export, and test-mode proofs of the Stripe behaviour billing relies on. See Stripe Integration for what each one does.
ds stripe billing-inventoryds -b stripe billing-inventory --out <directory>
READ-ONLY export of what Stripe holds for every customer beside DevStride's view. Writes inventory.csv plus raw JSON Lines files.
| Flag | Description |
|---|---|
--out <directory> | Required. Directory to write the export to. Must resolve OUTSIDE the repository — the files hold customer names, emails and negotiated prices |
Needs STRIPE_INVENTORY_KEY, a restricted read-only key (rk_…); a full sk_… secret key is refused. It reads the key from your shell, else from the agent's credentials file (~/.config/devstride/agent.env, written by ds secrets pull) — never from a checkout's .env.
ds stripe prove-invoice-flowds -b stripe prove-invoice-flow
Stripe TEST-MODE proofs: direct card charges, staff refunds, one-off invoices and subscription cancellation. No flags. Needs STRIPE_PROOF_KEY, a test-mode key (the test STRIPE_SECRET_KEY in .env will do).
ds stripe sync-products, sync-customers, check-quantities, or any Slack backfill-notifications command. add-products, create-customers and find-subscription-quantity were removed — billing-inventory replaces the last.ds gDefined in cli/commands/g.ts. CQRS codegen — scaffolds a new command or query into a backend module.
ds g command (alias: c)ds g command <name> -m <module> [-f]
Scaffolds a new CQRS command (command artifact, init, service handler, Lambda handler) in the given module.
| Flag | Description |
|---|---|
<name> | (required, positional) Command name |
-m, --module <module> | (required) Module to generate the command in — throws if omitted |
-f, --force | Overwrite existing files. Default: false |
ds g query (alias: q)ds g query <name> -m <module> [-f]
Scaffolds a new CQRS query in the given module.
| Flag | Description |
|---|---|
<name> | (required, positional) Query name |
-m, --module <module> | (required) Module to generate the query in — throws if omitted |
-f, --force | Overwrite existing files. Default: false |
ds poolDefined in cli/commands/pool.ts. Reserves, resets and hands back the machine's fixed checkouts — the main checkout, .worktrees/wt1 and .worktrees/wt2 — with a lease. It skips the wrapper's AWS gate and stage bind: everything except cloud mode touches only local Docker. See The checkout pool for how the pieces fit together.
Every subcommand except status names the session holding the lease — pass --session <name>, or set DEVSTRIDE_SESSION once in your shell. <checkout> is always main, wt1 or wt2.
The lease also records which session took it. Two sessions can share a name, so the name alone proves nothing. Each lease carries a holder: the Claude Code session id and/or the Codex thread id, which every command in that session shares. reset, client, release-client, cloud, checkin, template refresh and ds work refuse anyone but that holder, even under the same session name. From a plain terminal, which has neither id, checkout prints a one-time token — run the export DEVSTRIDE_LEASE_TOKEN=… line it prints, and every later command in that shell proves it is you. A lease written before holders were recorded is refused by all of those commands; hand it back by hand only if you are sure it is yours, otherwise it is the owner's to clear.
ds pool statusds pool status
Who holds each checkout (session, purpose and time — marked STALE once a lease is over 24 hours old; clearing it is the owner's call), what branch it is on, and what data it holds: golden, one customer's organization (flagged for review after 14 days), or bound to a cloud stage. It also reports how old the golden template is, and any containers no pool checkout owns — reported, never removed. No flags.
ds pool checkoutds pool checkout <checkout> --purpose <text> [--session <name>] [--mode client --org <id> | --mode cloud] [--i-know-main-is-free]
Takes the checkout's lease and starts its app. Prints exactly one outcome:
| Outcome | Meaning | Exit code |
|---|---|---|
LEASED | The checkout is yours. Its Docker app is started (or, with --mode cloud, its cloud app). Re-running checkout on a checkout you already hold says you already hold it; nothing was changed. If the previous holder kept a customer copy there, a NOTE names it. | 0 |
BUSY | Another session holds the lease; the line says who and why, and that you do NOT hold it — even when the other session uses your name. Message them or take the other checkout. Never delete someone else's lease. | 2 |
IN USE | No lease, but the checkout is dirty or on someone's branch — a session from before the lease rule is working there. Your lease has already been released; take the other checkout. | 2 |
| Flag | Description |
|---|---|
--purpose <text> | Required. What you are using it for, e.g. an item number |
--session <name> | Who holds it. Default: DEVSTRIDE_SESSION |
--mode <mode> | client copies one organization in (needs --org); cloud runs the checkout against this machine's own stage. Omitted: local Docker, keeping whatever data it already holds |
--org <id> | With --mode client: the organization to copy in |
--i-know-main-is-free | Required to reserve main, which is the owner's own checkout — only after they say it is free |
The lease is a file in the checkout's own git directory, so it never dirties the tree, and it is created only if it does not already exist, so exactly one session can win it.
ds pool checkinds pool checkin <checkout> [--session <name>] [--keep-client]
Hands the checkout back. Stop every process you started there first (test runs, dev servers, watchers). It refuses unless you hold the lease and the tree is clean, and refuses a detached checkout holding commits no branch contains. It then ends any cloud binding, stops the checkout's app containers (its test containers keep running), leaves it detached at a freshly fetched origin/develop, and removes the lease. Its data is kept.
It refuses while the checkout holds a customer copy (client mode), unless you pass --keep-client. Why: a customer's data left behind by accident is the one thing nobody notices. Drop it first with ds pool release-client <checkout>, or keep it on purpose with --keep-client — the next session to check the checkout out is told it is there.
ds pool template refreshds pool template refresh --from <wt1|wt2> [--session <name>]
Captures the golden template that resets clone from. Run it from a leased wt1 or wt2 that has just run ds worktree cli <checkout> golden build — never main. It refuses a checkout in client mode, one whose database holds any organization that is not golden, and one whose database holds no golden organization at all (… holds no golden organization — run its golden build first), which would capture an empty template.
ds pool resetds pool reset <checkout> [--session <name>]
Snaps a leased checkout's data back to its snapshot in seconds, instead of a full golden rebuild: the golden template, or — in client mode — that checkout's own copy of the customer's organization. A golden reset also re-creates the golden logins in the checkout's own Cognito pool.
ds pool cloud <checkout> --off.ds pool clientds pool client <checkout> --org <id> [--session <name>]
Client mode for a checkout you already hold (the same as ds pool checkout … --mode client --org <id>): copies one customer's organization into the checkout's own database, then keeps that copy as its snapshot, so every later ds pool reset snaps back to it without copying again. It reads SOURCE_DB_CONNECTION_STRING, as ds data copy does. A checkout already holding a different organization is refused, so two customers' data never share one checkout.
ds pool release-clientds pool release-client <checkout> [--session <name>]
The only way out of client mode by hand. It resets the checkout to golden first, and only once that has worked does it drop the customer snapshot — a reset that fails changes nothing. The unattended ds machine doctor also does this for a copy older than 14 days on a checkout no one holds.
ds pool cloudds pool cloud <checkout> [--off] [--session <name>]
Moves a checkout you hold from local Docker to this machine's own cloud stage — one checkout per machine at a time. Also reachable as ds pool checkout <checkout> --mode cloud.
.env — the machine bundle's DEVSTRIDE_STAGE, DEVSTRIDE_DEV_PROFILE and NEON_BRANCH, written by ds secrets pull — falling back to your shell's environment. DEVSTRIDE_REGION defaults to us-east-1.dev, staging and prod stages, a machine where sst dev is already running, and a second checkout while one is already bound.aws sso login --profile <profile> command; a refused machine identity says so.ds run backend and ds run ui in the background, with the UI on the checkout's own frontend port and logs in .ds/pool-cloud-backend.log and .ds/pool-cloud-ui.log.--off stops both, deletes that stage's .ds/bind file in the checkout (so nothing run there later is silently cloud-bound) and brings the Docker app back. ds pool checkin does the same unbinding, then hands the checkout back with its Docker app stopped. A binding whose app has stopped is reported as an error, never as "already bound" — run --off, then bind again.
ds workDefined in cli/commands/work.ts. Starts and lands one item on the branch it belongs to, inside a pool checkout you lease. It is the delivery loop's routing rule for agents and people who do not run the DevStride plugin — Codex, or work by hand — so nobody has to remember where an item lands. It skips the wrapper's AWS gate and stage bind: it reads DevStride with the agent's own key (from ~/.config/devstride/agent.env) and otherwise touches only git.
Where an item lands. The item's nearest Epic's integration branch (<user>/<YY-MM-DD>/epic-<I#####>-<slug>, created off develop when none exists); with no Epic above it, the support train, train/support, which ships with the next release. Nothing lands on develop or master directly. The plugin's routing helper (routing.py) decides the same thing, and a test runs the plugin's canonical examples through ds work, so the two cannot drift apart. See Where work lands for the why.
Every subcommand that changes git needs a checkout you hold (ds pool checkout) and a clean tree; pass --session <name> or set DEVSTRIDE_SESSION.
ds work startds work start <I#####> [--slug <kebab>] [--infra | --target <branch>] [--one-off] [--session <name>] [--dry-run]
Cuts the item's feature branch, <user>/<YY-MM-DD>/<I#####>-<slug>, from a fresh copy of its target and records the target on the branch (branch.<name>.dsTarget in local git config) for ds work land.
| Flag | Description |
|---|---|
<I#####> | Required. The item number. A container item is refused |
--slug <kebab> | The branch name after the item number. Default: the item's title |
--infra | A one-off that changes deploy configuration or migrations (stacks/, sst.config.ts, migrations): cut from develop instead of the train, to ship by its own pull request with the merge-path-override label. ds work land never lands it. Why: the train reaches production at the next release with little time between, and such a change deserves its own CI and an earlier deploy to the dev stage. Refused for Epic work, whose release pull request carries its infrastructure with the rest |
--one-off | An item filed under an Epic that is still a one-off: send it to the train, never to that Epic's branch — the same rule /devstride:create-story follows |
--target <branch> | Land on this branch instead of the one the rule picks. Needed when an Epic's integration branch predates the epic- marker, which start stops and asks about rather than cut a second one |
--dry-run | Say what would happen; change nothing |
ds work landds work land [--session <name>] [--no-verify] [--dry-run]
Run on the feature branch start created. It merges the target's newer work into the branch, runs the local gate on that combined tree — ds verify land against the target — then merges the branch --no-ff onto the target and pushes. It never pushes to develop, master or another protected branch, never forces, and leaves the feature branch and the DevStride item alone. A one-off on the train is not Done when it lands: it is Done when the release carries the train into develop.
develop and open its own pull request. Why: the check runs on the branch's own changes, so the train's other one-offs never come along with it.develop (one started with --infra) is refused: open its pull request with /devstride:pr, then add the merge-path-override label.--no-verify skips the local gate, with a warning — refused for the train, because nothing else checks that code before the train's pull request.ds work train status / ds work train rotateds work train status
ds work train rotate [--create]
status lists the one-offs on the train that have not reached develop yet (read-only). rotate fast-forwards the train to develop once a release has carried everything on it, and does nothing while it still carries unreleased work; --create creates the train at develop when it does not exist yet. The release normally does this for you.
ds work dependabot-batchds work dependabot-batch [--session <name>]
Folds every open Dependabot pull request into develop onto one new branch, dependabot/batch-<YY-MM-DD> (-2, -3… when that name is taken), and pushes it. Run it in a checkout you lease, on a clean tree. When two updates both rewrote the lockfile, it keeps either side and regenerates the lockfile once at the end; an update that conflicts anywhere else is left out and named, to batch next time or apply by hand. The batch's own commits skip the commit hooks, which would otherwise rewrite the lockfile half-way and leave the tree dirty.
Then open the branch's pull request into develop with /devstride:pr: the local-ci check runs the full local mirror on it once. Dependabot closes each original pull request itself once develop carries its update.
Why. Every pull request into develop needs a green local-ci, which is a full run of about half an hour. Checking a week of Dependabot updates one by one would cost that many runs; batched, it costs one.
ds verifyDefined in cli/commands/verify.ts. The local mirror of cloud CI, so the deep checks run on the team's own machines and paid cloud CI runs only at release. It skips the wrapper's AWS gate and stage bind and needs no login. It does need this checkout's test containers.
Both subcommands share a machine-wide lock, ~/.cache/devstride/verify.lock: one run per machine at a time. A second run waits and says whose run it is waiting on; the lock of a run that died is reclaimed. Each step prints one line, a failing step prints the end of its output, and the first failure stops the rest. The whole log and a receipt are written to .ds/verify-<land|full>.log and .ds/verify-<land|full>.json.
--base <ref> sets what the change is measured against. By default it is the target ds work start recorded for the branch, else whichever of develop, the support train and the epic integration branches is the closest ancestor. --dry-run prints the steps and stops.
ds verify landds verify land [--base <ref>] [--dry-run]
The check every Story and one-off passes before it merges onto its Epic's branch or the train. ds work land runs it, and so does the plugin's per-story gate. For the files the change touches, it:
packages/mcp changed;preShipChecks in .claude/ds-config.json).A broad change — the test harness or the lockfile, say — runs the whole non-golden backend suite instead, and says why.
Why. Before it, a logic fix with no test edit landed on the type-check alone, and its breakage was first found at the release.
ds verify fullds verify full [--post] [--adopt-release] [--base <ref>] [--dry-run]
Everything cloud CI runs on a release pull request, on this machine: a frozen-lockfile install, every type-check, eslint over the whole frontend (errors only), the MCP and frontend tests, the UI build and the portal bundle budget, a readiness probe on this checkout's test Postgres, the whole non-golden backend suite, and the pre-ship checks the change against develop triggers. It takes about half an hour. A test compares this list with the cloud workflows and fails when a cloud check has no local counterpart.
| Flag | Description |
|---|---|
--post | Record the result as the local-ci commit status on HEAD. It refuses to start on a dirty tree, posts pending while it runs, and posts success only when every step passed on that exact commit. If HEAD or the tree moves during the run, nothing is posted |
--adopt-release | With --post: when HEAD's tree is exactly the tree a merged release pull request into master already tested green in the cloud, post success citing that run instead of running everything again. This is how a master-to-develop sync gets its local-ci in seconds |
--base <ref> | What the change is measured against; it selects the path-triggered pre-ship checks |
--dry-run | Print the steps and stop |
Every pull request into develop must carry a green local-ci before it can merge. The delivery loop runs ds verify full --post --adopt-release as its local-ci pre-ship check before it marks the pull request ready; see Deployment. Posting the status needs a GitHub token that can write commit statuses on the repository; the agent's token is used when this machine holds it.
ds merge-pathDefined in cli/commands/merge-path.ts. Answers one question: may this branch be the head of a pull request into develop? It skips the wrapper's AWS gate and needs no login.
ds merge-path checkds merge-path check --head <branch> --base develop [--labels a,b] [--config <path>]
Prints one line, ALLOWED or BLOCKED with the reason, and exits 0, 1, or 2 for a usage or config error. It is the same checker the required merge-path check runs on every pull request into develop, and the one the merge guard consults before a merge. The rules are mergePath.allowedHeadPatterns in .claude/ds-config.json: an epic integration branch, the support train or the snapshot a release cuts from it, a master-into-develop sync, a release or hotfix branch, or a Dependabot update. Anything else needs the merge-path-override label — an infrastructure one-off, or an epic branch from before the epic- marker.
Why it exists. develop reaches production at the next release, so only branches that have been through a full review may merge into it. Running the check locally tells you before you open the pull request, instead of from a red check after.
ds secretsDefined in cli/commands/secrets.ts. The secrets bundles that every checkout's .env is built from: a shared bundle, one bundle per machine, and an agent bundle, all in the dev account's Secrets Manager. It skips the wrapper's AWS gate and stage bind — pull must still work from its cache when your login has lapsed — and both subcommands check themselves that the session reaches the dev account.
ds secrets pullds secrets pull
Writes .env into every pool checkout that exists (main, wt1, wt2) from the shared and machine bundles, machine winning on the same key, and writes the agent bundle to one private file outside every checkout: ~/.config/devstride/agent.env (mode 600; DEVSTRIDE_AGENT_ENV_FILE moves it). No flags.
.env. Only ds agent, ds quickbooks, ds graph, ds work and ds -b stripe billing-inventory read the agent file. Why: every ds command loads the checkout's .env, so a token kept there would travel with every command in every checkout. On a machine that pulled before this change, one pull removes the old copies from each .env; until then the file wins over them..ds/secrets-cache.json. If AWS cannot be reached, it writes from that copy with a warning naming the login command. With no cache yet, it fails with that command instead. A cache is never reused under a different machine name..env in by hand from .env.sample until then.DEVSTRIDE_MACHINE_NAME, else the hostname. With no bundle for this machine it pulls shared and agent keys only, with a warning. If the machine's bundle disappears after a previous pull used one — usually a changed hostname — it refuses rather than drop the machine's stage identity from every .env..env is kept once, as .ds/env-before-secrets-pull. After that, every pull rewrites .env completely.ds secrets setpbpaste | ds secrets set <shared|machine|agent> <KEY> [--machine <name>] [--mirror]
Sets one key in one bundle as a new version. The value is read from stdin, never the command line, so it stays out of your shell history; the command refuses to run with a terminal on stdin. One trailing newline is dropped; every other byte, including a certificate's final newline, is kept. Run ds secrets pull afterwards to update .env.
| Flag | Description |
|---|---|
<target> | shared, machine or agent. Agent service credentials always go to agent |
<key> | The environment variable name |
--machine <name> | Set another machine's bundle. Default: this machine |
--mirror | Also set the key in GitHub Actions — only for a shared key on the repository's mirror allow-list (cli/commands/secrets/mirror-allowlist.ts), which lists only keys the owner has approved for CI. Machine and agent keys are never mirrored; the command says why when it does not mirror |
ds machineDefined in cli/commands/machine.ts. It sets up and maintains the machines that work on DevStride: setup takes a bare machine to ready, ready proves it works, and doctor keeps its checkouts current. The rest give each unattended agent machine its own certificate per AWS account. A machine with nobody at the keyboard cannot finish aws sso login, so an expired session would stall it mid-task. With a certificate, IAM Roles Anywhere exchanges it for a short AWS session named after the machine. There is no login and no 8-hour expiry, and every action in CloudTrail names the machine that took it. People keep using SSO; certificates are only for machines. ds machine skips the wrapper's AWS gate: each subcommand that reaches AWS checks the account with the profile it was given.
For the whole sequence on a new machine, follow Set up your machine.
Where the certificate authority lives. Its key and its record of every certificate issued are two secrets in the management account's Secrets Manager (devstride/machine-identity/ca-key and ca-state), not on any laptop. So ca-init, sign, revoke and issued run from any machine signed in to the management account: the devstride-mgmt profile, or DEVSTRIDE_MGMT_PROFILE, or --mgmt-profile <profile>. Each checks the profile really reaches the management account before reading either secret. The key is read into memory for one operation and never written to disk. Two people signing or revoking at once cannot corrupt the record: a save that would overwrite someone else's stops with "the CA state changed while this ran". Run the command again.
./setup and ds machine setup./setup [--agent <name> | --laptop] [--dry-run] [--yes] [--skip <stages>] [--timer]
./setup, at the repository's root, is the one way to set up a machine. On a blank Mac, the one line in the README (and in Set up your machine) installs Homebrew and the GitHub CLI, signs in to GitHub, clones the repository and runs it. ./setup installs what setup itself needs to run (Homebrew when it is missing, mise, Node and pnpm at the versions mise.toml pins, and the repository's dependencies), then hands every option to ds machine setup. Running it again is always safe.
ds machine setup checks everything first, lists every change it would make, every step that waits for a person, and every change to what the agents may do (Claude Code's settings and its trust in this repository, and Codex's approval of the repository's hooks), and asks once: Proceed? [y/N]. A y covers all of it. Then it applies each change and checks it again. A step that fails is reported with its reason, and everything that does not depend on it still runs. It works through eleven stages in order:
| Stage | What it takes care of |
|---|---|
tools | The Brewfile tools (the AWS CLI, gh, git, jq, Neon's CLI, OpenSSL 3, psql from postgresql@15, linked onto the PATH only when no psql is there, and poppler), Node and pnpm at the pinned versions, the dependencies, and AWS's credential helper (aws_signing_helper, checked against its published checksum) |
containers | Docker Desktop installed (its installer accepts Docker's licence and asks for the Mac password), started, and checked for at least 4 CPUs and 8 GB |
agents | Claude Code and Codex installed and signed in; Codex's approval of the repository's hooks recorded (as its own /hooks screen would) and proven; Codex set to read all of AGENTS.md |
aws | A person's machine: a self-renewing sign-in (an sso-session profile), then one browser sign-in. An agent machine: its own certificates for dev and production, issued from one approval, after which setup removes its own sign-in |
secrets | Every checkout's .env, and the agents' credentials file, from ds secrets pull |
pool | The main checkout's app running locally; wt1 and wt2 made, recorded, with their dependencies and their own generated types |
plugin | The DevStride plugin in Claude Code and Codex, the DevStride connection in each, Claude Code's settings for DevStride work and its trust in this repository (allowed by the one Proceed, never by --yes, and never when an agent runs setup) |
machine | The GitHub sign-in, your git identity, Google Chrome with Claude in Chrome offered, the Claude and ChatGPT desktop apps, iTerm2 and Oh My Zsh (those four on a person's Mac only), the shell startup blocks (checked afterwards in a fresh login shell, and taken back out if they would change the Node or pnpm a terminal runs), ds on your PATH (~/.local/bin/ds), the docs clone, and the hourly check (agent machines, or --timer); on an agent Mac, keep-awake on mains power |
data | The demo data, built once in a free wt1 or wt2, kept as the machine's snapshot, and copied into each checkout |
stage | This machine's own AWS stage, built once with ds stage create (about 30–40 minutes, unattended), then only checked. Waits on the team's NEON_API_KEY |
proof | ds machine ready: the verdict |
| Flag | Description |
|---|---|
--agent <name> | An unattended agent machine with this name: its own certificate identity, keep-awake and the hourly check |
--laptop | A person's own machine (skips the question that asks) |
--dry-run | Report what is missing and change nothing |
--yes | Do not ask before the first change. It never allows the changes to what the agents may do (Claude Code's settings and trust, Codex's hook approval) |
--skip <stages> | Comma-separated stages to leave alone; anything that needs a skipped stage waits for a later run |
--timer | Also install the hourly check on a person's own machine |
At the end it lists what changed and, when anything is left, Needs you (a step only you can do), Waiting on the above, and When you can (nothing waits on these). A machine that is already set up reports "Nothing to change." and still runs the proof. Its log is .ds/machine-setup.log, the demo-data build's is .ds/machine-setup-data.log, and the stage's first deploy writes .ds/stage-deploy.log in the checkout it deployed from.
ds machine readyds machine ready [--quick] [--json]
Proves this machine works, and ends with READY or NOT READY. NOT READY names the first failing proof and its fix. Proofs are grouped as tools (including psql and pdftoppm), aws, github, devstride, checkouts, stage (this machine's AWS stage: recorded, stacks, config, database), landing, claude, codex, machine, agent machine, credentials and browser, and each line is one of: pass; FAIL, with its fix; held (another session is using that checkout, so it could not be proven; not a failure); you (a step only a person can take); or note (something to do when you can; never a failure). READY means no proof failed.
| Flag | Description |
|---|---|
--quick | Only the proofs that start no app and call no model, in seconds. The hourly check runs these and writes a ready: <group> result for each group to the fleet registry |
--json | Print the verdict and every proof as JSON |
The full run also reserves wt1 and wt2 one at a time under setup's own session, starts each one's app and signs in as the demo user, runs the landing check in a free checkout, and asks Codex one line. With both checkouts free it takes several minutes, and prints its progress as it goes; its log is .ds/machine-ready.log. A run stopped part-way leaves a reservation that the next full run, ./setup or the hourly check takes back.
ds machine doctords machine doctor [main|wt1|wt2] [--check-only]
ds machine doctor --install-schedule [--every <minutes>]
Checks each of this machine's checkouts (all three by default) on five points, fixes what it may, and writes the results to the fleet registry:
| Point | Healthy when | Fixed by |
|---|---|---|
| freshness | A checkout parked at develop is at the latest develop | Moving it to the latest develop. A checkout on a branch is never moved |
| install | Installed dependencies match the lockfile | pnpm install |
| data | The local database has every migration the code has, a demo-data checkout actually holds the Acme organization, and any customer copy is under 14 days old | Resetting demo (golden) data from the template, including a checkout whose database has no demo data yet; dropping an expired customer copy (below). Anything else is only reported |
| secrets | .env and the agent's credentials file came from the latest version of each bundle, and no .env still holds a copy of the agent's credentials | Nothing: it tells you to run ds secrets pull, which rewrites every checkout's .env |
| tools | Node and pnpm match mise.toml | Nothing: ds machine setup fixes it |
ds pool checkout it runs on the checkout you were just given, before its app starts, and fixes it for you.--check-only — it drops a copy older than 14 days on a checkout nobody holds and resets that checkout to golden. Inside ds pool checkout it only reports such a copy, because the session that just took the checkout decides. Why: customer data should not sit on a laptop longer than the repro needs it.DEVSTRIDE_LEASE_TOKEN=<its holder> ds pool checkin <checkout> --session machine-doctor (the token is the doctor's lease holder, since a lease answers only to the session that took it).| Flag | Description |
|---|---|
[checkout] | main, wt1 or wt2. Default: all three |
--check-only | Report everywhere and change no checkout. The fleet registry is still updated |
--install-schedule | Run it on a schedule on this machine: launchd on macOS, systemd on Linux. It runs once straight away, and keeps the PATH, AWS profile and machine name of the shell that installed it. Log: ~/.devstride/logs/machine-doctor.log. Re-running replaces the schedule |
--every <minutes> | With --install-schedule: how often, at least 15. Default: 60 |
ds machine ca-initds machine ca-init [--mgmt-profile <profile>]
Creates the certificate authority, once for the whole company: a 10-year root certificate whose key and record go straight into the two management-account secrets. It refuses once the record exists, because every enrolled machine trusts the authority that is there. If it is interrupted after storing the key, running it again finishes from that key rather than making a new one.
ds machine provisionds machine provision --account <dev|prod> [--profile <profile>] [--mgmt-profile <profile>] [--confirm-production]
Creates the trust anchor, role and Roles Anywhere profile in one account, from a person's own admin session. The management session must also be valid, because the root certificate is read from its record. It describes what exists first and creates only what is missing. It refuses, and never overwrites, a trust anchor holding a different root certificate, a role that does not trust this anchor, or a profile set up differently: replacing any of those would lock out every enrolled machine. Commit the updated cli/commands/machine/trust-anchors.json afterwards so every machine can enroll.
| Flag | Description |
|---|---|
--account <dev|prod> | Required. The account to provision |
--profile <profile> | A person's own AWS profile for that account. For prod you can set DEVSTRIDE_PROD_PROFILE instead |
--mgmt-profile <profile> | The management-account profile. Default: devstride-mgmt |
--confirm-production | Required for prod |
ds machine enrollds machine enroll <name> [--force]
ds machine enroll <name> --complete --cert-dev <file> --cert-prod <file>
Run on the agent machine, in two halves around ds machine sign. <name> is 2–41 lowercase letters, digits and hyphens.
0600, in a 0700 directory), under ~/.devstride/machine-identity/<name>/, on every platform. It prints the exact sign commands to run next. --force replaces the machine's existing identity.--complete: checks each signed certificate names this machine and account, and matches the key from step 1, then writes two AWS profiles: devstride-machine-<name>-dev and devstride-machine-<name>-prod. Pass absolute paths to --cert-dev and --cert-prod. Add --force when completing a --force re-enrollment, which is how a machine renews its certificates before their year is up.Then point this machine at it: export DEVSTRIDE_DEV_PROFILE=devstride-machine-<name>-dev. Every ./ds command and the session-start check pick it up with no other change. The plain aws and sst commands do not read that variable: pass --profile, or export AWS_PROFILE=$DEVSTRIDE_DEV_PROFILE. Production stays explicit (--profile devstride-machine-<name>-prod), as a person's production profile is.
ds machine signds machine sign <name> --account <dev|prod> --csr <path> [--out <path>] [--mgmt-profile <profile>]
Run on any machine signed in to the management account: issues one machine's certificate for one account. The certificate names the machine and account from these arguments, never from the request, so a request cannot claim another machine or account. Certificates last 365 days, so a lost machine lapses by itself. It is written to --out, or to <name>-<account>.crt.pem at the top of the checkout (./ds always runs from there, so pass absolute paths to --csr and --out), only after the record of it is saved. So nothing is ever issued that revoke cannot find. The signing machine needs OpenSSL 3 (brew install openssl@3), not macOS's built-in LibreSSL.
ds machine revokeds machine revoke <name> [--confirm-production] [--dev-profile <profile>] [--prod-profile <profile>] [--mgmt-profile <profile>] [--take-over-lock]
Run from any machine signed in to the management account when a machine is lost, stolen or retired. It revokes every certificate the authority issued that machine, saves that, then uploads the revocation list to dev. --confirm-production uploads it to prod too. Without that flag, prod is reported as skipped and the command exits non-zero, because the machine is not yet shut off everywhere. It uses a person's own session per account (DEVSTRIDE_DEV_PROFILE / DEVSTRIDE_PROD_PROFILE, or the flags), and checks each profile reaches the right account first. If an upload fails, run the same command again: it refreshes the lists and changes nothing else.
Revoking stops the machine getting new credentials; a session it already holds stays valid until it expires, up to an hour. For a stolen machine, cut that short by revoking the role's active sessions in the IAM console.
Two revokes at once. A revocation list only ever moves forward: an account that already has an equal or newer list is not overwritten, but it is still switched on if someone switched it off. Only one revoke publishes at a time. A second one meanwhile stops with "another revoke (…) is publishing … — run this again in a minute", having changed nothing. A publication a stopped run left behind is never taken over automatically, because a stalled run could wake up and upload an older list. Once you know that run has stopped, run again with --take-over-lock.
ds machine issuedds machine issued [--mgmt-profile <profile>]
Prints every certificate the authority has issued, grouped by machine and account, as JSON: each with its serial, whether it is valid, expired or revoked, when it expires, and when it was revoked.
./ds wrapper and the session-start check both say the machine identity was refused and never suggest aws sso login, because a login cannot fix a certificate. In order of likelihood: the machine was revoked, its certificate expired (re-enroll), or its key is gone.ds fleetDefined in cli/commands/fleet.ts. One listing of every agent machine and its checkouts, read from the fleet registry: a small table (devstride-fleet-registry) in the dev account that ds machine doctor writes to after every run. It skips the wrapper's AWS gate and checks itself that its profile (DEVSTRIDE_DEV_PROFILE, else the main checkout's .env, else AWS_PROFILE) reaches the dev account.
ds fleet statusds fleet status [--free]
One line per machine and checkout: its mode (golden, client or cloud data), who holds it, its branch, the machine's AWS stage (the STAGE column), its health (green, or which of the doctor's points are failing), and when it was last checked. --free lists only checkouts nobody holds. A row not checked for 7 days is flagged "machine gone?" and is never deleted automatically.
ds fleet initds fleet init
Creates the registry table, once, in the dev account. A second run finds it and changes nothing.
ds agentDefined in cli/commands/agent.ts. Wires an agent's DevStride and GitHub access from the agent bundle's credentials, which ds secrets pull writes to ~/.config/devstride/agent.env — never into .env. Needs no AWS login and no stage bind. See Agent service credentials.
ds agent mcp-configds agent mcp-config [--replace] [--codex]
Points the repository's DevStride MCP connection at the agent bundle's DEVSTRIDE_API_* key: one entry in Claude Code's ~/.claude.json, under the main checkout, which wt1 and wt2 share (Claude Code never reads an entry under a worktree's own path). An entry already there is kept unless --replace. --codex does the same for Codex's own connection instead: it is added to Codex's config.toml only when Codex has none, so a person's own DevStride sign-in in Codex is never replaced, and every change is first loaded by Codex from a scratch copy, so a broken configuration is never written. ./setup does both on a new machine. Restart Claude Code or Codex to pick up a change.
ds agent check-ghds agent check-gh
Read-only proof that gh works non-interactively from the bundle's GH_TOKEN. Prints PASS or FAIL with the fix, and exits 1 on FAIL. gh reads GH_TOKEN only from its own environment, so an agent's shell must export it from ~/.config/devstride/agent.env for gh to act as the agent. No flags.
ds quickbooksDefined in cli/commands/quickbooks.ts. An agent's read access to QuickBooks Online through its own grants — distinct from the product's QuickBooks integration.
ds quickbooks --company <sandbox|real> company-info
ds quickbooks --company <sandbox|real> search-customers <term>
| Flag | Description |
|---|---|
--company <sandbox|real> | Required on every call, with no default. sandbox is the agent-only test company; real is DevStride's own books, with full access — Intuit offers no read-only grant |
company-info prints the company's name, which proves the grant works. search-customers <term> lists the customers whose name matches — id, display name, email, and (inactive) where it applies.
Every call checks your AWS login first: Intuit may hand back a new refresh token, and that token is written straight back to the agent bundle and ~/.config/devstride/agent.env in the same run.
ds graphDefined in cli/commands/graph.ts. An agent's Microsoft Entra access through its own single-tenant app — never the Teams integration app.
ds graph read-userds graph read-user <upn>
Looks up one account by its sign-in name (UPN) or object id and prints it as JSON. Graph does not look accounts up by email, so a guest, or anyone whose email differs from their UPN, is not found.
ds graph rotate-secretds graph rotate-secret
Rotates the agent app's own client secret. It checks your AWS login first, mints a new secret, proves it can sign in before storing it anywhere, writes it to the agent bundle and ~/.config/devstride/agent.env, and retires the old one only once every place holds the new one. If a store is only partly done, both secrets stay valid and the command names where the new one landed — put it in the bundle before anyone runs ds secrets pull, then retire the old one. No flags.
ds graph retire-secretds graph retire-secret <keyId>
Retires one named client secret — the follow-up when a rotation could not retire the old one itself. It refuses to retire the secret currently in use.
ds teamDefined in cli/commands/team.ts. Grants or removes a developer's access: push access to devstride/devstride and devstride/ds-docs, and an AWS IAM Identity Center sign-in in the group devstride-developers, which holds AWSAdministratorAccess on the dev account only. It uses your own gh (an admin of the devstride organisation) and a profile that reaches the management account (DEVSTRIDE_MGMT_PROFILE, else devstride-mgmt). An agent session may only run it with --dry-run. For admins walks through both.
ds team add-developerds team add-developer --email <work email> --github <login> --name "<Given Family>" [--any-domain] [--mgmt-profile <profile>] [--dry-run]
Invites the GitHub login to both repositories with push access, creates the AWS sign-in (its user name is the email), adds it to devstride-developers, and the first time creates that group and its dev-account assignment. Prints already: for what is in place and changes only what is missing. Refuses to add anyone to the group if it has been given more than administrator access on the dev account. Ends with the admin's one console step (sending the password email, unless Identity Center sends one itself) and the steps to send the developer.
| Flag | Description |
|---|---|
--email <email> | Their work email, which becomes their AWS sign-in. Must end devstride.com or creativecapsule.com |
--github <login> | Their GitHub login |
--name "<Given Family>" | Their given and family name |
--any-domain | Allow an email outside those two domains |
--mgmt-profile <profile> | Your management-account profile |
--dry-run | Check everything and print the plan; change nothing |
ds team remove-developerds team remove-developer --email <work email> [--github <login>] [--mgmt-profile <profile>] [--dry-run]
Removes their push access to both repositories (or cancels a pending invitation), takes their sign-in out of devstride-developers and deletes it, after you retype their email. Without --github, GitHub access is left alone. Refuses a sign-in with access beyond the developers' group, an organisation member (removed at the organisation's People page instead) and the admin running it. Its notes name what is left: each of their machines' stages (ds stage remove --machine <name>) and, for an agent machine, ds machine revoke.
ds stageDefined in cli/commands/stage.ts. This machine's own AWS stage in the dev account, named after the machine and recorded in its secrets bundle. Setup's stage step runs create for you; Your AWS stage explains what it is.
ds stage createds stage create --member <wt1|wt2> [--stage <name>] [--session <name>] [--profile <profile>] [--region <region>] [--dry-run]
Makes the stage, or finishes making it: a Neon database branch stage-<stage> from golden-data (with the team's NEON_API_KEY), the stage's keys in the machine bundle, its config, its first deploy (sst dev, run from the pool checkout you hold, which must be clean), migrations, the demo logins and today's dates. Run again, it picks up where it stopped and never rebuilds a finished stage.
| Flag | Description |
|---|---|
--member <wt1|wt2> | The pool checkout you hold, where the deploy runs |
--stage <name> | The stage name. Default: the one the machine bundle records, else the machine's name. Name a stage made by hand to adopt it |
--session <name> | Who holds that checkout. Default: DEVSTRIDE_SESSION |
--profile <profile> | The dev-account profile. Default: the bundle's DEVSTRIDE_DEV_PROFILE, else this shell's |
--region <region> | Default us-east-1 |
--dry-run | Say what it would do; change nothing |
ds stage removeds stage remove [--machine <name>] [--profile <profile>] [--region <region>] [--dry-run]
Removes this machine's stage, or with --machine a retired machine's: its stacks, its config, its Neon branch and the stage's keys in that machine's bundle. Permanent: the stage's data and sign-in users go with it. A person retypes the stage name to confirm; no flag skips that, and an agent's session is refused. It also refuses while a checkout on this machine is running against the stage (ds pool cloud).
ds seedDefined in cli/commands/seed.ts. Deploys with the team's shared Seed token, SEED_TOKEN, read from the agent credentials file (~/.config/devstride/agent.env, which ds secrets pull writes). Needs no AWS login and no stage bind.
ds seed deployds seed deploy --stage <stage> --commit <sha> [--force] [--org <org>] [--app <app>]
Asks Seed to deploy one commit to one stage: the same request Seed's own seed deploy command sends. Merging is what deploys normally (a merge into develop deploys the dev stage, into master production), so this is only for a deliberate redeploy of a chosen commit.
| Flag | Description |
|---|---|
--stage <stage> | The Seed stage to deploy to |
--commit <sha> | The commit to deploy |
--force | Deploy even when Seed finds no changes |
--org <org>, --app <app> | The Seed organisation and app. Default: devstride for both |
--stage prod deploys that commit to production at once, without a release's review, CI or the owner's approval. Never run it against prod without the owner's explicit yes.Until the owner has created the token and stored it (pbpaste | ds secrets set agent SEED_TOKEN, then ds secrets pull on each machine), the command refuses and says how.
ds neonDefined in cli/commands/neon.ts.
ds neon <neonctl arguments>
ds neon api <route>
Runs Neon's command-line tool, neonctl, with the team's shared organisation key, NEON_API_KEY, from the agent credentials file, and DevStride's Neon organisation: never a person's own neonctl sign-in. The key reaches staging and production alike (owner decision, 2026-10-05). Needs no AWS login and no stage bind.
--project-id. ds neon refuses init (which always uses a person's own sign-in) and link, checkout and set-context, because every session on the machine shares ds neon and a saved project would steer them all.neonctl from a private folder of its own (~/.config/devstride/neon), so a person's saved sign-in is never touched, and anything neonctl writes (a project link, an .env with a database password) stays out of every checkout.ds neon api <route> reaches any Neon API route.Until an admin has created the key and stored it (pbpaste | ds secrets set agent NEON_API_KEY, then ds secrets pull), the command refuses and says how; ds neon --help and ds neon --version still run. Setup's stage step needs the same key. The key is never shown in the command's output or errors. Credentials and access has every key's source.
ds worktree and the checkout poolIntroduction to the ds CLI
What the ds CLI actually wraps, how ./ds resolves and runs a command, the global auth/bind flags, and the eighteen real top-level commands.
Local Development
The real day-to-day dev loop: running the backend and UI, scaffolding CQRS code, migrations, and managing local data with the ds CLI.