Intro

Command Reference

Exhaustive, flag-by-flag reference for every real ds CLI command, verified directly against cli/commands/*.ts.

Command Reference

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.

The Global Wrapper

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]
FlagDescription
-bForce re-bind stage/region config, even if a cached .ds/bind/<stage>-<region>.env already exists
-rTarget the remote stage — unsets IS_LOCAL, sets DEVSTRIDE_REMOTE=true
-uSkip the AWS SSO auth check entirely (aws sts get-caller-identity / aws sso login)

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 run

Defined 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 backend

Starts 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 ui

Starts 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 local

ds 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 migrations

Defined 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 run

ds 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-sql

ds 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-contract

ds 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-rollups

ds 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-inventory

ds 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).

Publish-intent journal operations

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.

CommandWhat 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

Legacy SQL usage sampling

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 data

Defined in cli/commands/data.ts. Bulk SQL table import/export/wipe, plus single-organization copy, against the bound database.

ds data import

ds data import <path> [-t <tables>]

Bulk-restores SQL tables from a directory previously produced by ds data export.

FlagDescription
<path>(required, positional) Directory to import from
-t, --tables <tables>Comma-separated list of table names to import (default: all tables)

ds data export

ds data export [path] [-t <tables>]

Dumps SQL tables to disk as chunked JSON files, one directory per table.

FlagDescription
[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 wipe

ds 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.

FlagDescription
-t, --tables <tables>Comma-separated list of table names to wipe (default: all tables)

ds data copy

ds data copy [-o <organization>] [-s <source>]

Org-scoped copy: pulls one organization's data from a source database into the bound database.

FlagDescription
-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 golden

Defined 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:

GroupSubcommandsUse it to
Seed a stagebuild (load), push, import, refresh-stage, reanchor, aichat, cognito, enrich, reset, lifecycle, preview, statusGet Acme onto a stage, keep its dates at "today", and check its health
Keep the golden source currentrecertify, reconcile, release <action>Maintain the canonical source database every stage and training sandbox clones from
Training sandboxesinstall, invite-admin, cognito-sandbox, aichat-sandbox, s3-sandboxMint 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.

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.

FlagDescription
--no-resetSkip the reset — only on a clean target where the Acme org is absent
--no-verifySkip the §16 assertion gate after building
--skip-cognitoSkip 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 push

ds 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.

FlagDescription
--force-full-resetRequired to proceed — drops the stage's public + drizzle schemas and replaces all stage data with the golden template
--allow-any-scaleSkip the full-scale floor check (allows pushing a dev-scale template)
--skip-aichatSkip the DynamoDB AI-chat leg
--skip-cognitoSkip the Cognito login leg
--dry-runRun 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 import

ds 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.

FlagDescription
--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-aichatSkip 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-runRun the parity/provenance checks and print the planned orgs, but make no changes

ds golden reanchor

ds 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.

FlagDescription
--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-runRead the stored anchor, compute the delta, print the plan + row counts, mutate nothing

ds golden status

ds 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.

FlagDescription
--target-db <connectionString>Inspect an explicit Postgres connection string instead of the bound stage
--skip-sourceSkip the comparison against the canonical golden source

ds golden refresh-stage

ds 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.

FlagDescription
--org <name>Refresh one persona org instead of all. Currently acme-only in practice — see the note on ds golden import above
--skip-aichatSkip the DynamoDB AI-chat leg of the import

ds golden aichat

ds 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.

FlagDescription
--org <name>Only seed one org's chat. Currently acme-only in practice — see the note on ds golden import above
--forceDelete each persona's existing chat(s) before seeding, so an updated transcript is written instead of the stale one being kept

ds golden enrich

ds 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.

FlagDescription
--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 lifecycle

ds 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.

FlagDescription
--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 reset

ds 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).

FlagDescription
--org <name>Reset a specific golden org (currently only acme)

ds golden cognito

ds golden cognito [--org <name>]

Seeds Cognito logins (password Demo@123) for the golden persona users. Run after build.

FlagDescription
--org <name>Only seed one org's users. Currently acme-only in practice — see the note on ds golden import above

ds golden preview

ds 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 recertify

ds 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.

FlagDescription
--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
--boundRe-certify the bound stage's own database instead (scope full). Mutually exclusive with --source
--dry-runCheck that the recorded gate handle is complete and the anchor resolves, then stop

ds golden reconcile

ds 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.

FlagDescription
--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 install

ds 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.

FlagDescription
--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-aichatSkip the post-install persona-login or demo-chat legs; cognito-sandbox / aichat-sandbox run them later
--resumeContinue 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-runResolve, validate and print the install plan; connect to nothing

ds golden invite-admin

ds 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-sandbox

ds 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:

CommandWhat it does
cognito-sandboxMaterializes 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-sandboxSeeds 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-sandboxTeardown 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 script

Defined 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-inits

ds 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-ids

ds script find-missing-set-correlation-ids

Codebase scan for handlers missing correlation-ID propagation. No flags.

ds script find-missing-events-registration

ds script find-missing-events-registration

Codebase scan for domain/integration events that aren't registered. No flags.

ds script find-missing-events-in-stack

ds 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-dlq

ds 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-docs

ds script generate-api-docs

Generates the public-facing OpenAPI spec with embedded documentation. No flags.

ds script generate-api-client

ds 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-mcp

ds 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-items

ds script delete-orphaned-items

Deletes orphaned folders, work items, activity, comments, and assets from the database. No flags.

ds script set-config

ds 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 sync-identity-providers

ds 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-stacks

ds script check-orphan-stacks

Reports orphaned CloudFormation stacks left behind by earlier sst.config.ts changes. No flags.

ds script delete-orphan-stacks

ds 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 assistant

ds script assistant

No flags.

ds script get-active-emails

ds script get-active-emails

Dumps active users' emails across organizations. No flags.

ds script maintenance-on

ds script maintenance-on

Enables maintenance mode by toggling a Lambda environment variable. No flags.

ds script maintenance-off

ds script maintenance-off

Disables maintenance mode. No flags.

ds script api-maintenance-on / api-maintenance-off

ds 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-permissions

ds script backfill-access-permissions

Idempotent SQL backfill of baseline module-access permission keys. No flags.

ds script backfill-description-asset-items

ds 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-permissions

ds 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-db

ds 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.

ds script remove-org

ds script remove-org [-o <organization>]

Deletes all rows for one organization.

FlagDescription
-o, --organization <organization>Organization ID to remove

Dev-only guard applies (not safe to run on prod).

ds script create-slack-workflows

ds 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-workflows

ds 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.

FlagDescription
--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-runCount what would be created, write nothing

ds script billing-create-account

ds 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.

FlagDescription
--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-org

ds 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-test

ds 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-topology

ds 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-timeentry

ds 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-group

ds script add-default-sidebar-form-group

One-off historical migration: backfills a default sidebar form group. No flags.


ds stripe

Defined 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-inventory

ds -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.

FlagDescription
--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-flow

ds -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 g

Defined 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.

FlagDescription
<name>(required, positional) Command name
-m, --module <module>(required) Module to generate the command in — throws if omitted
-f, --forceOverwrite 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.

FlagDescription
<name>(required, positional) Query name
-m, --module <module>(required) Module to generate the query in — throws if omitted
-f, --forceOverwrite existing files. Default: false

ds pool

Defined 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 status

ds 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 checkout

ds 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:

OutcomeMeaningExit code
LEASEDThe 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
BUSYAnother 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 USENo 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
FlagDescription
--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-freeRequired 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 checkin

ds 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 refresh

ds 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 reset

ds 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.

  • Your branch is ahead of the snapshot: it clones, runs your branch's migrations, and checks that every one of them actually landed.
  • Your branch is behind the snapshot: refused. A clone would be ahead of your code and nothing downgrades it — rebase, or refresh the template from a checkout at your branch's migrations.
  • The checkout is cloud-bound: refused until ds pool cloud <checkout> --off.

ds pool client

ds 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-client

ds 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 cloud

ds 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.

  • The stage comes from the checkout's .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.
  • It refuses the shared dev, staging and prod stages, a machine where sst dev is already running, and a second checkout while one is already bound.
  • It checks the AWS identity first and never opens a login. A lapsed login gets the exact aws sso login --profile <profile> command; a refused machine identity says so.
  • It stops the checkout's Docker app and starts 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 work

Defined 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 start

ds 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.

FlagDescription
<I#####>Required. The item number. A container item is refused
--slug <kebab>The branch name after the item number. Default: the item's title
--infraA 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-offAn 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-runSay what would happen; change nothing

ds work land

ds 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.

  • An infrastructure change on a train branch is refused before anything merges, with the exact commands to move the branch onto 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.
  • A branch targeting 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 rotate

ds 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-batch

ds 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 verify

Defined 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 land

ds 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:

  1. installs dependencies from the frozen lockfile, as CI does, so a dependency bump is tested with its new versions;
  2. runs every type-check;
  3. runs eslint on the changed frontend files — errors fail the run, and formatting is fixed in place, so commit what it changes;
  4. runs every backend and frontend test whose imports reach a changed file. A file no import can reach (a hook script, a config file, a workflow) finds the backend specs that name it;
  5. runs the MCP server's checks when packages/mcp changed;
  6. runs the pre-ship checks the changed paths trigger (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 full

ds 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.

FlagDescription
--postRecord 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-releaseWith --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-runPrint 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-path

Defined 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 check

ds 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 secrets

Defined 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 pull

ds 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.

  • Agent credentials never enter a .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.
  • Offline fallback: it keeps a copy in the main checkout's .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.
  • Before the bundles are seeded: with no shared bundle it refuses and changes nothing — fill .env in by hand from .env.sample until then.
  • Machine name: 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.
  • A hand-made .env is kept once, as .ds/env-before-secrets-pull. After that, every pull rewrites .env completely.

ds secrets set

pbpaste | 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.

FlagDescription
<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
--mirrorAlso 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 machine

Defined 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:

StageWhat it takes care of
toolsThe 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)
containersDocker 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
agentsClaude 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
awsA 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
secretsEvery checkout's .env, and the agents' credentials file, from ds secrets pull
poolThe main checkout's app running locally; wt1 and wt2 made, recorded, with their dependencies and their own generated types
pluginThe 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)
machineThe 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
dataThe demo data, built once in a free wt1 or wt2, kept as the machine's snapshot, and copied into each checkout
stageThis 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
proofds machine ready: the verdict
FlagDescription
--agent <name>An unattended agent machine with this name: its own certificate identity, keep-awake and the hourly check
--laptopA person's own machine (skips the question that asks)
--dry-runReport what is missing and change nothing
--yesDo 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
--timerAlso 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 ready

ds 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.

FlagDescription
--quickOnly 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
--jsonPrint 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 doctor

ds 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:

PointHealthy whenFixed by
freshnessA checkout parked at develop is at the latest developMoving it to the latest develop. A checkout on a branch is never moved
installInstalled dependencies match the lockfilepnpm install
dataThe 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 oldResetting 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 credentialsNothing: it tells you to run ds secrets pull, which rewrites every checkout's .env
toolsNode and pnpm match mise.tomlNothing: ds machine setup fixes it
  • Inside ds pool checkout it runs on the checkout you were just given, before its app starts, and fixes it for you.
  • Run by hand or on the schedule, it fixes only a checkout it can take over itself. It leaves alone the main checkout, a checkout on a branch, one with its app or another process running in it, and one someone holds. Those are only reported. It holds the checkout while it works and hands it back after.
  • It never changes the main checkout's database, data it cannot identify, or a database ahead of the code. A customer copy is left alone too, with one exception: run unattended — on the schedule, or by hand without --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.
  • If a run is stopped part-way it keeps the checkout held, because a reset can still be finishing inside Docker. The next scheduled run takes it back, first stopping any app the stopped run left running. To free it sooner, run the command the stopped run printed: 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).
FlagDescription
[checkout]main, wt1 or wt2. Default: all three
--check-onlyReport everywhere and change no checkout. The fleet registry is still updated
--install-scheduleRun 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-init

ds 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 provision

ds 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.

FlagDescription
--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-productionRequired for prod

ds machine enroll

ds 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.

  1. First run: makes one private key and one certificate request per account. Each key is a file only the machine's user can read (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.
  2. --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 sign

ds 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 revoke

ds 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 issued

ds 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 fleet

Defined 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 status

ds 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 init

ds fleet init

Creates the registry table, once, in the dev account. A second run finds it and changes nothing.


ds agent

Defined 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-config

ds 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-gh

ds 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 quickbooks

Defined 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>
FlagDescription
--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 graph

Defined 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-user

ds 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-secret

ds 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-secret

ds 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 team

Defined 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-developer

ds 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.

FlagDescription
--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-domainAllow an email outside those two domains
--mgmt-profile <profile>Your management-account profile
--dry-runCheck everything and print the plan; change nothing

ds team remove-developer

ds 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 stage

Defined 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 create

ds 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.

FlagDescription
--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-runSay what it would do; change nothing

ds stage remove

ds 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 seed

Defined 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 deploy

ds 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.

FlagDescription
--stage <stage>The Seed stage to deploy to
--commit <sha>The commit to deploy
--forceDeploy even when Seed finds no changes
--org <org>, --app <app>The Seed organisation and app. Default: devstride for both

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 neon

Defined 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.

  • Name the project on every command with --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.
  • It runs 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.

Next Steps