.claude/ds-config.jsonThis page describes the repository contract for plugin v3.9.0. The file lives in the consuming repository, and its values override the plugin’s inline fallbacks.
The easiest way to create it is:
/devstride:setup
Every setting is optional at runtime, but setup writes explicit values so important behavior is visible and reviewable. Re-run /devstride:setup validate after a hand edit.
comprehend-plan, insert-story, insert-defect, and rationalize-gantt do not read it. plan may use hierarchyRoles.releaseUnit to resolve an ambiguous release boundary and the release branch/deploy settings to shape production-safe release units. Delivery uses the rest.| Key | Shape | Setup value or absent behavior | Purpose |
|---|---|---|---|
profile | "prototype", "standard", "extended", or "enterprise" | Setup asks; absent means standard | The repository’s default delivery profile. "extended" (from 3.6.0) is standard with fewer, larger leaves: only leaf size, spec depth, understand readers and review breadth move; every safety knob and floor is standard’s. A plan root’s Delivery profile: marker overrides it for that plan, and a bare profile word in a skill’s arguments overrides both. |
profileOverrides | object of knob → value | absent | Pins individual profile knobs for every profile, for example {"maxLocalReviewRounds": 2, "reviewBreadthCeiling": "HIGH-RISK"}. Keys are the knob names from the profile contract and values use its vocabulary. verificationGrouping accepts "per-file" (the default everywhere) or "per-finding", which restores one verifier per finding at the widest review breadth; the default groups findings one verifier per file-group, and a security finding is always verified singly either way. An override cannot go below a floor; targetAdversarialCycles is a fixed two-cycle floor rather than an override key, and an attempt to set it is reported and ignored, as is an unknown knob name. |
Four profile knobs also exist as keys of their own elsewhere in this file. A key that is present is the operator’s decision and always wins; the profile fills in only when the key is absent, and setup writes each key consistent with the profile it was told so a fresh config and its profile agree.
epicIntegrationBranches.autoRelease — when absent, prototype supplies true and the other profiles false; setup writes the same.epicIntegrationBranches.fastStoryMerges.enabled — when absent or true, prototype runs fast mode whenever an integration branch exists; a present false wins and is reported. The other profiles follow the key.review.pollTimeoutMinutes — when absent, the profile supplies 5 minutes (prototype), 10 (standard and extended) or 20 (enterprise); setup writes the same.review.openPullRequestsAsDraft, review.readyForReviewReleasesCi, review.ciHeldUntilReviewSettled — these describe what your workflows support, not what a profile does with it. Review first and CI last is a floor under every profile, prototype included; setup writes all three as detected facts.review.localCommand is different: it names the local CLI reviewer, it does not schedule it. A present command puts the engine on the roster, and it reviews every release pull request and every pull-request-path item under all four profiles — never a fast-merged item with no pull request, under any profile. The profile decides only how many rounds it gets at those boundaries: one under prototype, standard and extended, two under enterprise. null removes the engine everywhere.
/devstride:doctor reports the effective profile, its source, and any key that contradicts it — for example a prototype profile beside autoRelease: false, which is honored as configured and reported.
| Key | Shape | Setup value or absent behavior | Purpose |
|---|---|---|---|
baseBranch | string | "develop" fallback | Normal feature base and merge target. |
hotfixBaseBranch | string | "master" fallback | Fresh base for production hotfixes. |
protectedBranches | string | ['develop', 'master'] fallback | Heads the loop must not rebase, force-push, delete, or write lessons onto. Setup includes the base, release source, and production branch. From 3.8.0 an entry may be a pattern such as "release/*" — see Branch patterns. |
integrationBranch | string or null | null | Explicit working-base override. Non-null takes precedence over per-release-unit derivation. |
branchNaming.pattern | string | "<prefix>/<MM-DD-YY>/<slug>" | Feature branch shape. |
branchNaming.prefixSource | string | "git user.name first name, lowercased; ask if empty or ambiguous" | How the prefix is chosen. |
branchNaming.dateFormat | string | "MM-DD-YY" | Branch date format. From 3.6.0 both branch-feature and branch-hotfix take their date from it; branch-hotfix previously always used MM-DD-YY. |
develop and master are deterministic runtime fallbacks for a repository with no config. They are not claims that every repository uses those names, and /devstride:setup does not copy them when it can inspect origin.
Setup enumerates actual remote branches and uses exact common names as role candidates:
| Role | Candidate names |
|---|---|
| Normal development and release source | develop, development, staging, stage, canary, test, testing, qa |
| Production and hotfix base | main, master, production, prod |
| Possible single trunk | trunk |
Exactly one development candidate and one production candidate produce a proposed four-key mapping. Multiple matches remain a question; setup never picks whichever name happens to appear first. A development candidate without a production candidate can suggest baseBranch and release.releaseSource, but it cannot establish the production or hotfix branch.
The detector considers only branches that exist on origin, matches whole names rather than substrings, and treats origin/HEAD as evidence for the ordinary pull-request base rather than proof of production. Existing explicit config always wins.
/devstride:doctor config resolves the configured values or inline fallbacks and checks them against the remote. If an effective fallback does not exist, doctor prints the concrete candidates and the mapping setup would propose. Doctor never writes the file.
From 3.8.0 one anchored matching rule is used for protectedBranches entries, release.releaseBranchPattern and release.releaseBranchFixExclusions: * matches one or more characters within one path segment, ** one or more across segments, a date token such as <YY-MM-DD> matches a date, a trailing [-n] allows an optional -2, -3 … suffix, and everything else is literal. A name is tested against the whole pattern, never as a substring, so release/* does not match foo/release/x and stacks/** does not match docs/stacks/x. Why: an unanchored match fails silently — it either protects nothing or refuses a harmless fix. A release branch and the support train are protected by their own keys, whether or not protectedBranches lists them.
| Key | Shape | Setup value or absent behavior | Purpose |
|---|---|---|---|
epicIntegrationBranches.enabled | boolean | true | Give each release unit its own integration branch. false sends planned items to baseBranch. |
epicIntegrationBranches.pattern | string | "<prefix>/<MM-DD-YY>/<epic-number>-<epic-title-slug>" | Integration branch pattern. The date is the branch’s creation date. An existing integration branch is found by matching this pattern, anchored, so a marker such as epic-<epic-number>-… still resolves (from 3.8.0). A branch found only under an older naming makes the loop stop and ask rather than cut a second one for the same epic (from 3.9.0, which also catches dated older names). |
epicIntegrationBranches.slugRule | string | Kebab-case, lower-case, [a-z0-9-], strip [N], about six words | Deterministic release-unit slug rule. |
epicIntegrationBranches.releaseTarget | string | "baseBranch" | Base of the completed release-unit pull request. The literal baseBranch resolves to the configured branch. |
epicIntegrationBranches.autoRelease | boolean or "ask" | false for standard, extended and enterprise; true for prototype, which setup says out loud | When true, the loop cuts and merges the release-unit pull request after the final leaf. false stops at release-ready. "ask" requests approval for each completed release unit before cutting and merging its pull request. A present value wins over the profile. |
epicIntegrationBranches.deleteBranchAfterRelease | boolean | true | Delete the release-unit branch after a successful release. Set false to retain it. |
epicIntegrationBranches.mergedStatusName | string or null | "Review" | The status a leaf moves to when it merges onto its release unit's integration branch: it has landed but is not Done until the unit's release pull request merges. Matched by name in each work type's statuses, so one value works in any organization. null, or an organization with no status of that name, keeps the leaf's status; the comment and the git history still record the merge. From 3.10.0. |
epicIntegrationBranches.fastStoryMerges.enabled | boolean | Setup writes the value consistent with the chosen profile; runtime fallback is enabled | With an integration working base, locally review and merge the leaf with no item pull request. For prototype setup writes true whenever verify.typecheck is set, since Claude’s pass is the local engine and the story gate is type-checks plus the touched suites; for the other profiles it writes true only when a local CLI engine is configured and both verify.test and verify.typecheck are set, and otherwise false, naming the missing precondition. A present value wins over the profile. |
epicIntegrationBranches.fastStoryMerges.requireLocalVerifyGreen | boolean | true | Require the configured local checks before a fast merge. |
epicIntegrationBranches.fastStoryMerges.epicReleaseIsFirstCloudPass | boolean | true | Tell the release review that it must inspect the full accumulated diff. |
If the repository is public and you would rather no tracker numbers reach branch names, use an integration-branch pattern without <epic-number>, such as "<prefix>/<MM-DD-YY>/<epic-title-slug>"; the key already tolerates it.
| Key | Shape | Absent behavior | Purpose |
|---|---|---|---|
supportTrain.branch | branch name, e.g. "train/support" | One-offs use baseBranch | From 3.8.0: one long-lived branch that one-off items — those with no release unit above them — land on instead of the base branch. It is created off the base branch on first use. |
supportTrain.fastMerges | boolean | false: a one-off on the train takes the full pull-request path, into the train | With true, a one-off merges onto the train after the local risk check and local gate, with no pull request of its own, exactly as a fast-merged story does on an epic branch. |
The train ships only when release.mergeTrainBeforeCut is true: before each cut, release opens the train's pull request into the release source, runs the full review, the pre-ship checks and CI once, merges it, and marks every one-off it carried Done. A train configured without that key would never ship, so the loop bypasses it and routes one-offs to the base branch, saying why. A one-off whose change touches release.releaseBranchFixExclusions never rides the train either. Why: every merge into the base branch costs its own CI run and often a deploy; batching one-offs gives them one full review and one CI pass per release. The train is protected by this key alone — never rebased, amended or force-pushed — and release moves it forward after every release, by fast-forward or by merging the source into it.
Two optional sibling keys found in established configs are descriptive only: epicIntegrationBranches.example shows a sample branch name, and epicIntegrationBranches.fastStoryMerges.localEngines documents the expected local roster. The skills resolve actual behavior from the active settings and probes.
| Key | Shape | Default | Purpose |
|---|---|---|---|
verify.typecheck | string | none | Commands run in order during builds and before push or merge. This is the preferred form. |
verify.typecheckCombined | string | none | One equivalent fallback command, used only when verify.typecheck is absent. |
verify.test | string | none | Standard local suite. Required for setup to enable fast item merges. |
verify.testSingle | string | none | Single-spec command containing a <path/to/test.spec.ts> placeholder. |
verify.lint | string | none | Lint command, used when the changed paths are in its scope. |
verify.testDir | string | none | Test directory used to place new specs. |
verify.skipDuringStoryBuilds | array | [] | Slow suites that run as conditional cloud CI jobs. Every entry must match a real workflow job. |
With no typecheck command, push asks instead of guessing. With no test command, setup disables fast merges; if fast merges were enabled manually anyway, the item would have review but no local test gate.
{
"name": "e2e-matrix",
"alwaysRunWhenBase": ["productionBranch"],
"manualTriggerLabel": "run-e2e",
"runWhenChangedPaths": ["apps/web/**", "packages/e2e/**"],
"ciChecksByBase": {
"baseBranch": ["E2E (PR gate)"],
"productionBranch": ["E2E (full matrix)"]
}
}
| Field | Meaning |
|---|---|
name | Display name. |
alwaysRunWhenBase | Branch roles where the suite is always required, regardless of paths. |
manualTriggerLabel | Label the loop adds when the suite is requested manually. |
runWhenChangedPaths | Globs matched against the complete final diff, including both sides of renames. |
ciChecksByBase | Required check names for each branch role. No mapping means no requirement on that base. |
An empty verify.skipDuringStoryBuilds means no slow cloud gate exists and no absent check should be awaited.
preShipChecks is an array of suites that run locally at a pull-request or production-release boundary and never appear in CI:
{
"name": "integration-suite",
"pathGlobs": ["packages/api/**", "migrations/**"],
"command": "pnpm test:integration",
"when": "perPr",
"timeoutNote": "About 25 minutes; do not run beside another test process."
}
| Field | Meaning |
|---|---|
name | Display name. |
pathGlobs | Per-PR changed-path selector. Empty means every PR. Ignored for releaseOnly. |
command | Command to run. |
when | perPr, releaseOnly, or always. |
timeoutNote | Duration and environment guidance shown before the run. |
Entries run sequentially in array order. A red per-PR check blocks the pull request. A release check can be waived only by the owner, with the waiver recorded.
Keep a suite in exactly one place: verify.skipDuringStoryBuilds for cloud CI, or preShipChecks for a local ship-boundary run.
preCommitWiringChecks is a string array of repository-specific wiring checks the build loop expects before commits. Setup leaves it [] because only the repository can name these checks.
localEnvironment describes how the repository stands up an isolated local instance, if it can. The delivery skills read it; they never guess it.
{
"localEnvironment": {
"create": "git worktree add ../<name> -b <branch> <base> && docker compose -p <name> up -d",
"recreate": "pnpm db:reset",
"recreateMode": "inPlace",
"instanceName": "cat .instance-name",
"seed": "pnpm db:seed",
"migrate": "pnpm db:migrate",
"teardown": "docker compose -p <name> down -v",
"instanceBoundTo": "directory",
"fetchOnSessionStart": false
}
}
| Field | Meaning |
|---|---|
create / seed / migrate / teardown | A command, or null when the repository has no such step. <name> (the instance), <base> (the ref to branch from) and <branch> (the new branch, in your repository's naming convention) are placeholders the tooling may need — a tool that defaults the branch to the instance name mints a nonconforming one. |
recreate | A command that genuinely returns an instance to <base>'s schema, or null. |
recreateMode | "inPlace" or "newInstance" — which of the two shapes recreate is. Required whenever recreate carries <name>. |
instanceName | A command whose output names the instance this checkout belongs to. Needed to resolve <name> in an in-place recreate; null otherwise. |
instanceBoundTo | What an instance is keyed on: directory (a second checkout gets its own database and ports, and checking out another branch inside it keeps them), branch, or none. |
fetchOnSessionStart | false by default. From 3.3.0, true has the session-start hook run a bounded, prompt-free git fetch --prune origin for the repository root and print one line only when the current branch is behind its upstream. It never blocks a session — a hung fetch is killed at its deadline and every failure stays silent — and it runs whether or not the plugin update check is enabled. Setup does not write it; turn it on by hand. It shortens the window a stale checkout can open, but the loop still fetches for itself at the start of every iteration. |
recreate is the way back, and /devstride:branch-hotfix uses it, says which path it took, and stops to ask rather than guessing when there is no safe command and the schema has diverged.recreateMode cannot be inferred from the command text — a wrapper script is opaque, and each wrong guess fails in its own way: an in-place command treated as a second instance leaves the session on the old database, while the reverse resets an instance somebody is using. instanceName matters for the same reason: nothing else in the block identifies the current instance, and guessing from a directory name resets a different one, which on a shared machine destroys someone else's data silently.
The shipped default is every command null and instanceBoundTo: "none". With that default, branch-hotfix asks before touching a database and build-item treats the current checkout as the only environment. Setup gathers candidates from compose files, devcontainers, Nix files and package scripts and asks — it never reports a command as detected, because a compose file proves a stack exists, not how your team starts it; and instanceBoundTo is always asked, because no file says whether a second checkout gets its own database.
This block tells the loop what exists — declaring an environment never makes the loop run builds concurrently. Whether two checkouts can be tested at the same time is a property of the repository, not of this block: where the test infrastructure is shared across checkouts, test execution stays serial regardless of what is configured here.
stage describes the cloud environment this checkout deploys to. The loop only ever reads it — it never creates, changes, or tears a stage down.
{
"stage": {
"resolve": "cat .sst/stage",
"productionStages": ["prod"]
}
}
| Key | Shape | Default | Purpose |
|---|---|---|---|
stage.resolve | string or null | null | A command run from the repository root whose stdout is the stage this checkout deploys to. It must print one line, and print nothing (or exit non-zero) when there is no stage; every consumer reads empty as "no stage", never as an error. Keep it cheap — it runs on a timer. |
stage.productionStages | string | [] | The stage names that are production. No name is universal — one organization's is prod, another's is live — so listing a stage here is a claim that deploying to it affects real users. |
Most repositories have no stage, and null is the honest answer rather than a gap: doctor reports the block as not applicable rather than missing, and resolve: null records that somebody looked. Setup proposes a command from the shapes it finds — an SST, Serverless, Pulumi, or Terraform layout, or a *_STAGE variable — but never reports one as detected, because finding the tooling does not tell it how your checkout picks its stage.
Three consumers use it: /devstride:release names the stage at the production gate, so the owner approves a specific consequence; /devstride:branch-hotfix says which stage the checkout points at before touching a database and stops to ask when that stage is production; and the status line renders it, in red for a production name.
localEnvironment names the local instance — this checkout's database, tables, and ports — and the loop owns its lifecycle. stage names the cloud stack this checkout deploys to; several worktrees legitimately share one, and the repository's own deploy tooling owns it.Whether the repository has a status line is not a config key: the presence of .claude/statusline.sh and a statusLine entry in .claude/settings.json is the whole state. /devstride:setup offers to write both. The optional statusLine block covers two other things.
| Key | Shape | Default | Purpose |
|---|---|---|---|
statusLine.hiddenSegments | string | [] | Segments this repository has confirmed do not apply. Use it to record a deliberate absence — not to hide a segment whose value is merely missing, which the script already omits on its own. |
statusLine.autoUpdate | boolean | true | Whether the session-start hook may refresh a copied script that still carries its managed marker. |
The line renders Model · Effort · Repo · Checkout · Branch · Stage · PR and omits any segment it has no value for — label, value, and separator together — so a repository with no stage never shows a dangling Stage:. The script is repo-agnostic and identical everywhere, reading your config at runtime, so re-copying it is always safe. Line 2 carries a # ds-statusline: managed v<x.y.z> marker; deleting that line is the supported way to take the file over, and nothing replaces it afterwards.
Because a shared project setting outranks a personal one, a repository status line wins over a statusLine in your own ~/.claude/settings.json. Override it for yourself alone in .claude/settings.local.json.
| Key | Shape | Default | Purpose |
|---|---|---|---|
generated.regenCommand | string | none | Regenerate API or other derived output after relevant source changes. |
generated.paths | string | none | Generated files excluded from hand-written-code review. |
generated.toleratedTypeErrors | {file, pattern}[] | none | Narrow generated-file errors that may be tolerated. Anything else stops the build. |
Generated files are regenerated and committed separately; they are never fixed by hand.
| Key | Shape | Default | Purpose |
|---|---|---|---|
review.localCommand | string or null | null | Optional second local review engine. null means the built-in Claude pass is the only local reviewer. Naming it does not schedule it; see Delivery profile for where and how often it runs. The template may carry <context> (the skill substitutes - and sends the scope plus the cumulative ledger on stdin) or the older <base> placeholder (the ref the engine diffs against). A template with neither keeps working — --base is appended. |
review.localAssistCommand | string or null | null | A read-only second opinion the build engine may consult once for an ambiguous design, a critical contract, or a stubborn diagnosis. It is support, not a review round: it never writes, never satisfies the merge review, and never runs on routine work. |
review.adaptiveReviewerWait | boolean | absent means true | Stop waiting on a registered cloud reviewer once the wait passes that reviewer's learned latency — its 95th percentile plus a margin, learned per machine from GitHub's own timestamps — bounded below by the registration window and above by pollTimeoutMinutes, and the full timeout while the cache is cold. false pins the fixed bound. The cache is advisory, lives under ~/.cache/devstride-plugin/, and can be deleted to start cold. Setup never writes this key. |
review.localReReviewScope | "delta" or "full" | absent means delta unless the scope rule decides otherwise | The scope every follow-up cycle reviews — Claude, local CLI, and cloud alike, despite the key's local-sounding name. A follow-up reads only the changes since the previous cycle, falling back to the whole diff by a computed rule: a file the earlier cycle never touched, more than half of its lines rewritten, or a rebase. "full" pins them all to the whole diff. Setup never writes this key. |
review.localReviewerName | string | "Codex" | The name findings are attributed to in the roster and reports. Set it to whichever engine you configured. |
review.automatedReviewers | array | [] | Cloud reviewers to request. Empty means no cloud wave and nothing to wait for. Each entry may be scoped to base branches and to a request policy — see Cloud reviewer entry. |
review.openPullRequestsAsDraft | boolean | true | Open pull requests as drafts in a draft-held repository. |
review.readyForReviewReleasesCi | boolean | true | Treat the ready flip as the action that releases CI. |
review.ciHeldUntilReviewSettled | boolean | true | Hold CI until review settles. All three ordering flags false means CI runs alongside review. |
review.pollTimeoutMinutes | number | Per profile: 5 (prototype), 10 (standard, extended) or 20 (enterprise); setup writes the profile’s value | Bound on waiting for a cloud reviewer whose request has been proven registered (a review_requested timeline event). It does not cover the two-minute registration window — after which an unregistered reviewer is dropped for the run and reported — or the CI wait. |
review.resolveAddressedThreads | boolean | true | Reply to and resolve fixed, dismissed, or captured review threads. |
review.notifyWhenSettled | boolean | absent means off | Notify after a standalone settled review. Driven review never notifies. |
review.mandatoryLenses | {name, paths, question}[] | absent or [] means none | From 3.4.0: review lenses the repository forces on its own risk surface. A matched entry adds one focused finder to the story risk check and to the merge-boundary review, on the same footing as the forced security lens. See Mandatory review lenses. Setup never writes entries. |
Mixed draft-ordering booleans are unsupported but safe: the skills use the strictest behavior and report the mix.
The adversarial fan-out ships five generic lenses and forces exactly one of them, security, on any diff touching the authentication boundary. Every repository has at least one more surface where the same reasoning holds — transactions and durable events under a rolling deploy, money-moving code, a permissions matrix — and the plugin cannot know which. From 3.4.0, review.mandatoryLenses lets the repository name that surface and the question a finder must answer over it.
{
"review": {
"mandatoryLenses": [
{
"name": "concurrency-and-rolling-deploy",
"paths": ["backend/src/**/*transaction*", "backend/src/**/events/**", "backend/migrations/**"],
"question": "For every changed hunk: what ordering or isolation assumption does it make; what happens under READ COMMITTED; what happens while old and new revisions run side by side during a rolling deploy; which claim in the change summary cannot be proven from test or command output?"
}
]
}
}
| Field | Meaning |
|---|---|
name | Kebab-case lens name. It labels the finder, its findings, and the report line. |
paths | Non-empty array of globs matched against repo-relative paths of the hand-written files in the diff under review. Globs are anchored path patterns, never bare substrings: **/events/** matches an events directory anywhere, while events alone matches nothing. |
question | What the finder must answer for every changed hunk in the matched files. It is the finder's base question, not a hint. |
When a file in the diff matches an entry, ultracode-build's story risk check and the merge-boundary adversarial review each add one focused finder for it, carrying the entry's name as its lens and its question as its base question. It sits on the same footing as the forced security lens: the breadth ceiling and the delivery profile clamp generic finder breadth and never remove it. Findings are verified like any other — a mandatory-lens finding is not automatically P1; severity comes from the verdict. When review.localCommand uses the <context> substitution, the local engine receives each matched question as an additional hypothesis, never as a finding. Each matched entry is reported as mandatory lens <name>: ran (N findings); an entry that matched nothing is not mentioned, because "did not apply" is a different fact from "ran and found nothing". A malformed entry — missing name, paths, or question, or a non-array paths — is named once and ignored. /devstride:doctor validates entry shape and warns on a glob that would match every file. Setup mentions the key and never writes entries: only the repository knows its own risk surface, and the example above is a shape, not a recommendation. The contract is skills/ultracode-build/references/mandatory-lenses.md in the plugin repository.
The local reviewer is not tied to any vendor or model. review.localCommand is a command line, and an engine qualifies when four things hold:
PATH;<context> (preferred: every cycle receives the exact scope plus the cumulative ledger) or against a diff base via <base>;Grok, Gemini, DeepSeek, and open-weight models served locally through a runner such as Ollama are all capable of this role, as is anything reachable through an OpenAI-compatible endpoint. Name your CLI when /devstride:setup asks and it is probed, validated against the contract above, and recorded for your repository. /devstride:doctor then holds it to the same contract — it never reports an engine as wrong merely for being one it does not recognize.
Two rules apply whatever the engine. Disable any agent tooling the CLI loads by default, the DevStride MCP above all: a reviewer that can reach live project data can wedge mid-review and return a clean, empty result that reads exactly like "no findings". And leave model choice to your own policy where the CLI supports it, rather than hardcoding one.
Codex is what setup offers when nothing else is chosen — a leading model whose template is verified — as a default, not a requirement:
{
"review": {
"localReviewerName": "Codex",
"localCommand": "codex exec --ephemeral --sandbox read-only -c model_reasoning_effort=\"<effort>\" -c mcp_servers.devstride.enabled=false <context>",
"localAssistCommand": "codex exec --ephemeral --sandbox read-only -c model_reasoning_effort=\"<effort>\" -c mcp_servers.devstride.enabled=false <context>"
}
}
<context> is replaced with -, and the skill sends the exact review scope plus the cumulative ledger of prior findings on stdin — which is what lets a follow-up cycle check whether a fix held instead of starting cold. <effort> is routed per task; the template deliberately omits --model, leaving model choice to your own policy. Read-only sandboxing and the disabled DevStride MCP are both load-bearing: leaving the MCP enabled can wedge the review and look like a clean, empty result.
For structured Codex review output, the catalogue also supports base mode:
"localCommand": "codex review --base <base> -c sandbox_mode=read-only -c model_reasoning_effort=\"xhigh\" -c mcp_servers.devstride.enabled=false"
<base> receives the pull request's actual base, and a template with no placeholder gets --base <value> appended. Base mode can run the first cycle, but a follow-up is skipped rather than run blind, because it has no way to receive the prior cycle's dispositions.
Setup and Doctor check both local command templates against the plugin's review-engine catalogue. A known engine missing its read-only flag fails with the required correction; a command that disables or widens the sandbox fails too. An unknown engine is reported as UNVERIFIABLE, not certified read-only. The codex review form uses -c sandbox_mode=read-only; it does not accept the codex exec flags --sandbox or --ephemeral after the subcommand. Base mode has no stdin context or cumulative ledger. Setup explains that tradeoff before proposing migration and does not propose it when only one local review round is configured.
{
"name": "Copilot",
"bot": "copilot-pull-request-reviewer[bot]",
"how": "requested_reviewer",
"value": "copilot-pull-request-reviewer[bot]",
"graphqlBotId": "BOT_kgDOCnlnWA"
}
Each reviewer needs the identifiers required by its how mechanism. The skills iterate the full array. An incomplete entry can fail silently because GitHub may accept a request without registering a review event.
Two optional fields decide when a reviewer is asked. Both exist because a cloud reviewer is often billed per review, and requesting it on every round of every pull request is what drives that bill.
| Field | Absent behavior | Purpose |
|---|---|---|
baseBranches | Requested on every pull request | From 3.7.0: a list of exact branch names. The reviewer is requested only on a pull request whose base, re-read from GitHub on every review run, is one of them — ["main"] keeps it on production releases and hotfixes while the local engines cover everything else. A reviewer left out is announced as not requested, never waited on, and never counted as a failed engine. If the local engine then fails on such a pull request, no independent engine has reviewed it, so review stops for a human review. A value that is not a list of names is treated as absent, and /devstride:doctor warns about an empty list. |
requestPolicy | "every-round": requested when the pull request opens and on every follow-up cycle | From 3.8.0: "final-head" requests the reviewer once, when every other finding is settled, on the head about to release CI — and once more only after a fix commit that answered one of its own findings. A verified P1 or serious P2 still keeps opening cycles. |
| Key | Shape | Setup value or default | Purpose |
|---|---|---|---|
prBodyTemplate.sections | {heading, guidance}[] | Four standard sections | Ordered, closed set of pull-request sections. Setup adopts an existing GitHub template when you confirm it. |
prBodyTemplate.noAiAttribution | boolean | true | Suppress AI attribution in pull-request bodies. false permits it. |
commitConventions.messageFormat | string | "<type>(<scope>): <summary> <itemTag>" | Ordinary commit subject shape. |
commitConventions.reviewFixFormat | string | "fix(<scope>): <summary> [<itemNumber> review]" | Review-fix commit shape. |
commitConventions.epicMergeFormat | string | "merge: <itemNumber> [<N>] <short scope> into <epic-slug> integration" | Fast item merge subject. |
itemTagFormat | string | "[I#####]" | Item tag shape. Added only when the commit has a verified item. |
conventionsDoc | string | "AGENTS.md" fallback | Human-owned coding rules read by the build and review skills. |
lessonsDoc | string | ".claude/ds-lessons.md" | Small review-lessons store. review is the only writer; absence is valid until the first qualifying lesson. |
The lessons file is repository data. If it is created, make sure its path is not ignored.
If the repository is public and you would rather no tracker numbers reach its commit history, set itemTagFormat to ""; no item tag is then added to any commit subject, and the key already tolerates the empty value.
| Key | Shape | Default | Purpose |
|---|---|---|---|
hierarchyRoles.releaseUnit | string or null | Resolved at runtime | Parent-item type whose completion cuts a release. |
hierarchyRoles.leaf | string or null | Resolved at runtime | Executable one-day item types. |
Setup reads the real work-type hierarchy and asks you to confirm these roles. A configured type that no longer exists stops delivery; silently falling back could route planned work directly to the base branch.
| Key | Shape | Setup value | Purpose |
|---|---|---|---|
ci.workflowGlobs | string | ['.github/workflows/*.yaml', '.github/workflows/*.yml'] | Workflows inspected for draft gating. |
ci.draftGateCondition | string | "github.event.pull_request.draft == false" | Describes the condition used in your workflows; it does not install the condition. |
ci.gateJobName | string or null | null unless detected or supplied | Cheap, path-independent job used to prove the ready flip released CI. With null, the skills look for a new run at the head commit. |
ci.freezeBaseWhileReleasePrReady | boolean | ignored | Ignored from 3.8.0, whatever its value. It used to freeze the release source while a release pull request was ready. The freeze is gone: set release.releaseBranchPattern instead, so merges into the source never touch the release under review. release mentions the key when it is still present, and setup no longer writes it. |
ci.expectedRunsPerPullRequest | integer | 1 | Executed CI runs expected per workflow on a pull request. review reports one line per workflow that executed and names a second run of the same workflow, with its cause. A full-stack pull request that runs five workflows once each is correct, not an excess. |
For draft-held ordering, workflows must also subscribe to opened, synchronize, reopened, and ready_for_review.
The draft hold makes CI run once per pull request; the rest of a CI bill comes from superseded runs, production-branch pushes re-testing a tree the base branch just tested, and release pull requests re-run by a moving base. /devstride:setup ci applies the four workflow mechanics that remove those (the draft gate, per-pull-request concurrency with cancel-in-progress, a tree-identical skip on production-branch pushes, and a draft-convention check for humans) as reviewed diffs, and /devstride:ci-audit measures the result — executed runs and minutes, never raw run counts.
| Key | Shape | Default | Purpose |
|---|---|---|---|
release.productionBranch | string | "master" | Production pull-request base. |
release.releaseSource | string | "develop" | Branch promoted to production. |
release.autoDeployOnMerge | string | none; setup asks | Plain-English description quoted at the owner merge gate. |
release.releaseBranchPattern | pattern, e.g. "release/<YY-MM-DD>[-n]" | absent: the release pull request's head is release.releaseSource | From 3.8.0: cut each production release as its own protected branch from the release source (-2, -3 … on a re-cut) and open the release pull request from it, so the source keeps receiving merges while the release is reviewed; those ship in the next release. Fixes are new commits — never a rebase, amend or force-push — and a hotfix that reaches production meanwhile is merged in and re-reviewed. After the release, release syncs the production branch back into the source by pull request and deletes the release branch once both contain it. Add a matching protectedBranches entry; doctor warns when none matches. |
release.releaseBranchFixExclusions | array of path patterns, e.g. ["infra/**", "db/migrations/**"] | absent: nothing is refused | Paths a fix commit on a release branch may not touch — deploy configuration and migrations. Such a fix is refused: merge it to the source by a normal pull request, abandon the release branch and re-cut. The same list keeps one-offs that touch those paths off the support train. |
release.mergeTrainBeforeCut | boolean | false: nothing is merged into the source before the cut | With supportTrain.branch set, every release first merges a snapshot of the train into the source through its own fully reviewed pull request. See Support train. |
release.deployVerification | string or null | null | Optional command that exits 0 only when production serves the merge commit in RELEASE_COMMIT. The release skill runs it before writing a release note; when null, it asks the owner to confirm the deploy. |
release.postDeployCheckSkill | string or null | null | From 3.3.0: the name of a local skill (.claude/skills/<name>/) that checks production health once the deploy is confirmed. null means no check is registered and the close-out reports post-deploy health: not configured. Setup does not write it. |
The plugin does not know what "healthy" means for your production system — alarms, dead-letter queues, error rates, a synthetic transaction — so, exactly as with the documentation hooks below, it holds one fact: the name of a local skill that does. The release skill invokes that skill once, after the production deploy is confirmed and before documentation, release notes, and the close-out, as check followed by a fenced JSON payload of productionBranch, mergeCommit, and deployConfirmedAt.
The skill's first line must be exactly POST-DEPLOY HEALTH: PASS, POST-DEPLOY HEALTH: FAIL, or POST-DEPLOY HEALTH: NOT RUN, followed by evidence lines that each name the command that produced them. PASS continues and carries the evidence into the close-out. FAIL stops the release and asks the owner for a rollback decision — it never continues to documentation or notes on its own. NOT RUN (missing credentials, an unreachable endpoint) is reported as this-run degradation and the owner is asked whether to proceed; it is never a substitute for FAIL. /devstride:doctor checks that a configured name resolves to an existing skill. The full contract is skills/release/references/post-deploy-check.md in the plugin repository.
| Key | Shape | Default | Purpose |
|---|---|---|---|
docs.updateSkill | string or null | null | Name of a local skill (.claude/skills/<name>/) that updates your documentation for a shipped delta. null means no documentation system; the docs phase reports itself skipped. |
docs.releaseNotesSkill | string or null | null | Name of a local skill that writes and publishes a release note. Used only when you pass --release-notes to the release skill. |
docs.updateOnEpicRelease | boolean | false | Also run the docs skill when a release-unit pull request merges, not only at the production cut. |
The plugin never edits documentation itself. /devstride:setup asks where your documentation lives, how it is updated, and how release notes are pushed, then scaffolds the two local skills from its templates and writes only their names here. Each skill accepts update (docs) or publish / draft (notes) with a structured delta payload, and a read-only check mode that setup and doctor run. Core documentation is updated by default on every production release (no docs suppresses it); release notes are written only when you pass --release-notes, and only after the production merge is confirmed and the deploy verified — nothing in the loop decides that a release deserves a note.
release.docsRepo (versions before 1.0.0) is deprecated: the release skill reports it and runs without docs; /devstride:setup docs migrates it into a local skill.
plugin controls the session-start version check.
| Key | Shape | Default | Purpose |
|---|---|---|---|
plugin.updateCheck | boolean | true | Compare the version this session is running against the newest release at every session start. Silent when current or offline; one line with the exact update commands when behind. |
plugin.autoUpdate | boolean | Setup writes true; an absent plugin block means false | Apply a newer release on disk at session start. Session start is the only moment an automatic update is applied — a mid-loop update would change skill behaviour between build steps — and it only ever touches an install scoped to this repository. A shared user-scope copy is handed to /devstride:update, an administrator-managed one is left alone, and an ambiguous one goes to doctor. |
plugin.pin | string or null | null | Hold a version deliberately. The check reports "pinned at X, newest is Y" once and never nags. |
The check reads the version the running session loaded, not the version on disk, so it reports exactly the drift claude plugin list cannot see. It never blocks a session and never fails one. To update on demand, run /devstride:update, which resolves the exact installation that loaded it and verifies the result. After a verified change, /reload-plugins is usually enough; restart only if reload is unavailable or fails. Set the environment variable DEVSTRIDE_PLUGIN_UPDATE_CHECK=0 to disable it for CI or headless runs. /devstride:doctor reports when it last ran and what it found.
After editing:
/devstride:setup validate
/devstride:doctor
Setup validates the config’s commands, branches, tools, engines, roles, lessons path, and CI consistency. Doctor checks the surrounding installation and reports exact fixes without changing anything.
| Key | Type | Default | Behavior |
|---|---|---|---|
session.jobClassGate | boolean | true | Keeps authoring commands such as setup, planning and Doctor separate from execution commands such as build, review and release in a Claude Code session. Run /clear when switching classes. |
defects.deferredContainerTitle | string | "Deferred defects" | Names the container directly under the plan root where a review finding worth filing (a P1, a security finding, or one both likely to happen and material) that is deliberately put off is filed. Any other unfixed finding is dismissed with its reason, never filed (from 3.10.0). |
The session gate allows one build loop to work through multiple stories. For an intentional exception, pass --same-session, set session.jobClassGate: false, or set DEVSTRIDE_SESSION_GATE=0. The hook fails open on errors.
Deferred defects link back to the reviewed item. They have no execution-order prefix, dependency-chain splice or delivery phase; the loop neither selects them automatically nor treats their container as a release unit. Newly discovered scope that belongs in the active plan still goes through insert-story.