Policies

Configure what the bundled SDLC flow's policies check, override individual values, or write your own flow with custom policies.

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.

FilePurposeEdited by
.xcoder/guidelines.yamlExplicit overrides. Hand-edit to pin values you care about.you
.xcoder/guidelines.cache.jsonInferred from the codebase. Regenerate with xc flow guidelines refresh.xc
.xcoder/flow.jsOptional custom flow. Replaces the bundled SDLC when present.you

To see what is currently in effect — including which file each value came from — run:

bash
xc flow guidelines show          # human-readable, with source attribution
xc flow guidelines show --json   # JSON
xc flow status                   # current phase + last acceptance result

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

LayerWhat it controlsWhere
GuidelinesWhat each policy checks against — branch prefix, integration branch name, typecheck command, whether specs are required, etc..xcoder/guidelines.yaml
Flow declarationWhich 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:

  1. .xcoder/guidelines.yaml (or .yml) — explicit config. Always wins.
  2. .xcoder/guidelines.cache.json — inferred from the codebase by xc flow guidelines refresh.
  3. Built-in defaults shipped with @xcoder/flow-engine.

Full shape

yaml
# .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            # informational

Inspect the resolved view

bash
xc flow guidelines show          # human-readable
xc flow guidelines show --json   # JSON

xc flow guidelines refresh       # re-infer from the codebase

The 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

PropertyValue
MatchesEdit | Write | MultiEdit | NotebookEdit
Blocks whenHEAD is on guidelines.branched.off
Bypass envXCODER_ALLOW_INTEGRATION_EDIT=1

branch-prefix-must-match

PropertyValue
Matchesgit checkout -b ... | git switch -c ...
Blocks whenslug 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

PropertyValue
Matchesgit commit ...
Blocks whenguidelines.specify.specRequired is true and the message lacks #N (or fixes/closes/resolves #N, issue 42, issues/42)
Skips whencommand has --no-verify, --amend, --allow-empty, or no -m (interactive editor mode)
Bypass envXCODER_SKIP_ISSUE_REF=1

typecheck-must-pass-before-commit

PropertyValue
Matchesgit commit ...
Blocks whenguidelines.qa.typecheck is set and .xcoder/verdicts/typecheck.jsonis missing, stale (>5min), or failing
Skips whenguidelines.qa.typecheck is unset, or command has --no-verify / --amend
Bypass envXCODER_SKIP_QA_GATE=1
How to satisfyrun 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.

SeverityBehavior
blockDefault. Hook exits 2; reason is fed back to the agent as an error. Tool call does not run.
warnHook 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.
offPolicy is filtered out before its guard runs. Override-only — flow declarations cannot ship `off`.

Per-repo override

yaml
# .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: warn

Or use the CLI to write the override and emit an audit event:

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

ts
// .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 flow

Then attach with --flow:

bash
xc flow attach claude-code --flow .xcoder/flow.js

xc 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; guard runs pre-tool, verify runs post-tool.
  • policy({id, on, only?, guard}) always-on guard (no transition); optional only limits to specific states.
  • state({id, entry?, exit?, contextOnEntry?}) → state lifecycle hooks + context injection.

See Flow engine for the full type signatures.

Next