Where settings live
All policy settings live under .xcoder/ at the repo root. The files below are commit-tracked — they are the team baseline, not per-contributor state.
| File | Purpose | Edited by |
|---|---|---|
.xcoder/guidelines.yaml | Explicit overrides. Hand-edit to pin values you care about. | you |
.xcoder/guidelines.cache.json | Inferred from the codebase. Regenerate with xc flow guidelines refresh. | xc |
.xcoder/flow.js | Optional custom flow. Replaces the bundled SDLC when present. | you |
To see what is currently in effect — including which file each value came from — run:
xc flow guidelines show # human-readable, with source attribution
xc flow guidelines show --json # JSON
xc flow status # current phase + last acceptance resultCommit the cache too
guidelines.cache.json is commit-tracked alongside the yaml. New contributors get the same resolved view without running refreshfirst, and a diff on this file is a reviewable signal that the codebase's inferred conventions changed.
The two layers
Policy customization happens at two layers:
| Layer | What it controls | Where |
|---|---|---|
| Guidelines | What each policy checks against — branch prefix, integration branch name, typecheck command, whether specs are required, etc. | .xcoder/guidelines.yaml |
| Flow declaration | Which policies exist, what they match, and how they decide. | .xcoder/flow.js |
For most users, the guidelines layer is enough — keep the bundled SDLC flow and pin the values you care about. Power users write their own flow.
Layer 1: guidelines
Three sources, in priority order:
.xcoder/guidelines.yaml(or.yml) — explicit config. Always wins..xcoder/guidelines.cache.json— inferred from the codebase byxc flow guidelines refresh.- Built-in defaults shipped with
@xcoder/flow-engine.
Full shape
# .xcoder/guidelines.yaml — every key optional
specify:
specRequired: true # commit messages must reference an issue
specPath: docs/specs/ # where spec files live
prioritize:
source: github-issues # github-issues | linear | jira | roadmap-md | milestone-md | unset
query: 'is:open is:issue label:"ready"'
branched:
prefix: ['feat/', 'fix/', 'docs/', 'test/', 'chore/']
off: main # the integration branch (no edits allowed)
qa:
typecheck: pnpm run typecheck # used by xc qa typecheck
test: pnpm run test # used by xc qa test
lint: pnpm run lint # used by xc qa lint
coverageMin: 60 # informational; not gated yet
maintain:
refactorLocThreshold: 1000 # informational
depFreshnessDays: 90 # informationalInspect the resolved view
xc flow guidelines show # human-readable
xc flow guidelines show --json # JSON
xc flow guidelines refresh # re-infer from the codebaseThe resolved view shows the merged result and which source each value came from.
Layer 2: the bundled SDLC policies
The default flow (sdlc.simple.v1) ships four policies. Each one reads from the resolved guidelines and decides whether to allow or block the matched action.
no-edit-on-integration
| Property | Value |
|---|---|
| Matches | Edit | Write | MultiEdit | NotebookEdit |
| Blocks when | HEAD is on guidelines.branched.off |
| Bypass env | XCODER_ALLOW_INTEGRATION_EDIT=1 |
branch-prefix-must-match
| Property | Value |
|---|---|
| Matches | git checkout -b ... | git switch -c ... |
| Blocks when | slug doesn't start with any of guidelines.branched.prefix |
| Bypass env | — (no env override; failing this policy means the slug is genuinely off-convention) |
commit-must-reference-issue
| Property | Value |
|---|---|
| Matches | git commit ... |
| Blocks when | guidelines.specify.specRequired is true and the message lacks #N (or fixes/closes/resolves #N, issue 42, issues/42) |
| Skips when | command has --no-verify, --amend, --allow-empty, or no -m (interactive editor mode) |
| Bypass env | XCODER_SKIP_ISSUE_REF=1 |
typecheck-must-pass-before-commit
| Property | Value |
|---|---|
| Matches | git commit ... |
| Blocks when | guidelines.qa.typecheck is set and .xcoder/verdicts/typecheck.jsonis missing, stale (>5min), or failing |
| Skips when | guidelines.qa.typecheck is unset, or command has --no-verify / --amend |
| Bypass env | XCODER_SKIP_QA_GATE=1 |
| How to satisfy | run xc qa typecheck — writes a fresh verdict the policy reads |
Verdict freshness
Verdicts go stale after 5 minutes by design — to catch the case where you fix code, type-check, then keep editing without re-checking. Treat xc qa typecheck as a pre-commit ritual, not a one-time setup.
Severity (block | warn | off)
Each policy has an eslint-style severity. The flow declares the default; .xcoder/guidelines.yaml can override it per-repo without forking the flow.
| Severity | Behavior |
|---|---|
block | Default. Hook exits 2; reason is fed back to the agent as an error. Tool call does not run. |
warn | Hook exits 0 but writes a `[xcoder warn]` line to stderr and emits a hookSpecificOutput JSON so the agent sees the nudge on the next turn. Tool call proceeds. |
off | Policy is filtered out before its guard runs. Override-only — flow declarations cannot ship `off`. |
Per-repo override
# .xcoder/guidelines.yaml
policies:
no-edit-on-integration: block # keep strict
branch-prefix-must-match: warn # nudge, don't block
commit-must-reference-issue: warn
typecheck-must-pass-before-commit: warnOr use the CLI to write the override and emit an audit event:
xc flow policy branch-prefix-must-match warn --reason "casual repo, just nudge me"
xc flow show # confirms — each policy is tagged [block] / [warn (override)] / [off (override)]Severity does not affect transition guards
The policies: map only overrides items declared as policy(…). Transition guards (the guard on a transition(…)) always block on failure — they protect the state machine's correctness. To weaken those, edit the flow.
Layer 2 (advanced): write your own flow
For policies the bundled SDLC doesn't cover, write .xcoder/flow.js (or .mjs) that exports a Flow:
// .xcoder/flow.js
import { defineFlow, policy, transition } from '@xcoder/flow-engine'
export const flow = defineFlow({
id: 'my-team.flow.v1',
states: ['idle', 'branched', 'qa'],
initial: 'idle',
transitions: [
transition({
from: 'idle',
to: 'branched',
on: { tool: /^Bash$/, command: /^git checkout -b / },
}),
],
policies: [
policy({
id: 'no-secrets-in-edits',
on: { tool: /^(Edit|Write|MultiEdit)$/ },
guard: ({ input }) => {
const text = String(input.new_string ?? '')
return /(sk-[a-zA-Z0-9]{30,}|AKIA[0-9A-Z]{16})/.test(text)
? { ok: false, reason: 'edit appears to contain a secret' }
: { ok: true }
},
}),
],
})
export default flowThen attach with --flow:
xc flow attach claude-code --flow .xcoder/flow.jsxc flow attach auto-discovers .xcoder/flow.js / .xcoder/flow.mjs / .xcoder/flow.cjs if --flowisn't passed; falls back to the bundled SDLC otherwise.
The DSL primitives
defineFlow({id, states, initial, transitions, policies?, stateDefs?})→ validates and packages a Flow.transition({from, to, on, guard?, verify?})→ state transition triggered by a tool action;guardruns pre-tool,verifyruns post-tool.policy({id, on, only?, guard})→ always-on guard (no transition); optionalonlylimits to specific states.state({id, entry?, exit?, contextOnEntry?})→ state lifecycle hooks + context injection.
See Flow engine for the full type signatures.