CDO learns coding style from one or more JavaScript/TypeScript repositories and turns that into reusable artifacts for:
- humans (
STYLEGUIDE.cdo.md) - tooling (
biome.json+ Grit assets) - coding agents via root
AGENTS.md(agents.md)
The goal is simple: infer how an author actually writes code, then reuse that style safely in other repos.
CDO is pure ESM JavaScript ("type": "module") with JSDoc + // @ts-check and no runtime TypeScript compilation requirement.
- What CDO Produces
- Style Signals CDO Detects
- How Inference Works
- How Enforcement Works (Biome + Grit)
- Requirements
- Install
- Quick Start
- CLI Reference
- Node API Reference
- MCP Server Reference
- MCP Setup (Codex, Cursor, Claude)
- LLM Augmentation Contract
- Fixture Validation Loop (Your 3 Repos)
- Testing and Coverage
- Repository Layout
- mcp-layer Integration
- Troubleshooting and Debugging
- Publish Readiness
- Limitations
- License
A standard run produces:
cdo-profile.json
- schema-versioned machine profile (
schemaVersion: "1.0.0") - inferred rules, confidence, evidence, provenance
STYLEGUIDE.cdo.md
- human-readable guide derived from
cdo-profile.json
.cdo/generated tooling artifacts
biome.jsonbiome/plugins/*.grit(only when profile has unsupported enforced preferences)grit/cdo.gritgrit/README.mdgrit/recipes.jsonoxfmt.json(optional compatibility artifact)AGENTS.md(root-level, agents.md format)
- apply/report artifacts
cdo-apply-report.jsoncdo-iteration-report.json
CDO v1 infers these rule families:
- Comments
- line comment spacing:
//commentvs// comment - function JSDoc tendency
- comment block framing: framed (
//blank top/bottom) vs plain - trailing inline comment alignment (
//column behavior on assignments/object members/array entries/function lines)
- Naming
- function name word count tendency: single-word vs multi-word
- function expression naming tendency: named vs anonymous
- Control flow
- one-statement
ifstyle: braces omitted vs required - guard-clause tendency at function start
- Syntax
- quote style: single vs double
- semicolon usage: always vs never
- trailing comma usage: multiline vs never
- variable declaration comma placement: leading vs trailing
- yoda conditions:
if (3 === value)vsif (value === 3) - multiline ternary operator placement: leading (
?/:at line start) vs trailing - preferred JS line width (for formatter wrapping behavior)
- Whitespace/layout
- indentation kind: spaces vs tabs
- indentation size (space-based projects)
switch/caseindentation tendencyswitchbreakindentation tendency: match-case vs indented- member-expression continuation indentation tendency
- multiline call-argument layout tendency: compact first-line vs expanded
- blank-line density: compact vs spacious
- blank-line-before-
returntendency - blank-line-before-
iftendency
- Imports
- ordering tendency for ESM imports and CJS requires: alphabetical vs none
Important extraction notes:
- directive/separator comments are excluded from comment-spacing inference (
//# sourceMappingURL,// eslint-disable..., divider lines) - trailing inline comment alignment detection is gap-tolerant within the same block to preserve aligned comment columns
- one-line
ifbrace rule only considers true one-statement single-lineifforms switchbreakindentation only counts directbreak;statements incaseblocks- member-expression indentation uses dot-leading continuation lines and file-level majority voting
- multiline call layout only evaluates call expressions that span multiple lines
- import ordering is weighted by group size to avoid tiny groups dominating results
Inference pipeline:
- discover tracked JS/TS files from each repo
- optionally filter by author email(s)
- recency-sort and sample files per repo (
maxFilesPerRepo) - parse AST and extract raw signals
- aggregate signals across all sampled repos
- infer winner per rule with threshold gating
A rule is enforced only when both thresholds pass:
evidenceCount >= minEvidenceconfidence >= minConfidence
Otherwise it is marked undetermined.
Default thresholds:
minEvidence = 30minConfidence = 0.75
CDO is Biome-only for apply. There is no ESLint/native apply engine.
Enforcement model:
- Biome formatter/linter/assist (
biome.json)
- strongest coverage for formatting-level rules
- applied by
cdo applythroughbiome check --write
- Biome Grit plugins (
.cdo/biome/plugins/*.grit)
- query diagnostics for profile preferences not fully expressed in base Biome rules
- generated only when needed
- Grit pack (
.cdo/grit/*)
- structural/query recipes for manual review or extra automation outside Biome apply flow
cdo apply defaults to dry-run. Use --write to mutate files.
--safe-only suppresses rules not marked autoFixSafe before generating Biome config.
In safe-only mode, CDO also disables Biome formatter rewrites to keep diffs low-noise.
- Node.js
>=20 gitavailable onPATH- local git repositories as input
Supported source extensions:
.js,.jsx,.mjs,.cjs,.ts,.tsx,.mts,.cts
Default ignored directories (any depth):
node_modules,dist,coverage,vendor
From npm:
npm install cdoLocal development:
npm installcdo learn \
--repos ./repo-a,./repo-b,./repo-c \
--author me@example.com \
--out cdo-profile.jsoncdo guide --profile cdo-profile.json --out STYLEGUIDE.cdo.mdcdo config --profile cdo-profile.json --out-dir .cdocdo apply \
--profile cdo-profile.json \
--repos ./target-repo \
--engine biome \
--safe-only \
--report cdo-apply-report.jsoncdo report \
--profile cdo-profile.json \
--apply-report cdo-apply-report.json \
--out cdo-iteration-report.jsonWhen diff noise is acceptable, enable write mode:
cdo apply \
--profile cdo-profile.json \
--repos ./target-repo \
--engine biome \
--write \
--report cdo-apply-write-report.jsoncdo learn --repos <a,b,...> [--author <email>] [--out cdo-profile.json]Flags:
--repos(required): comma-separated repo paths--author: repeatable or comma-separated author emails--out: output file (defaultcdo-profile.json)--max-files: max sampled files per repo (default400)--min-evidence: evidence threshold (default30)--min-confidence: confidence threshold[0,1](default0.75)--inference:deterministic|llm-mcp(defaultdeterministic)--llm-augmenter-cmd: external augmenter command (only used inllm-mcp)--llm-sample:compact|fullpayload mode (defaultcompact)CDO_LLM_AUGMENTER_CMD: env alternative to--llm-augmenter-cmd
cdo guide --profile <path> [--out STYLEGUIDE.cdo.md]cdo config --profile <path> [--out-dir .cdo] [--no-oxc]cdo apply --profile <path> --repos <a,b,...> [--engine biome] [--safe-only] [--write] [--report report.json]Notes:
- engine is Biome-only (
biome) - dry-run is default (omit
--write) --safe-onlyexcludes non-safe rules
cdo report --profile <path> --apply-report <path> [--previous-profile <path>] [--out iteration-report.json]cdo mcpSelf-test tool listing:
cdo mcp --self-testcdo --help
cdo --versionimport {
learnStyle,
generateGuide,
generateConfigs,
applyStyle,
generateIterationReport,
startMcpServer
} from 'cdo';Input:
repoPaths: string[](required)authorEmails?: string[]maxFilesPerRepo?: numberminEvidence?: numberminConfidence?: numberinferenceMode?: 'deterministic' | 'llm-mcp'llmAugmenterCommand?: stringllmSamplingMode?: 'compact' | 'full'llmAugmenter?: (input) => { rules: Record<string, { value, confidence?, evidenceCount? }> }
Returns: Promise<CdoProfileV1>
Returns markdown string.
Options:
outDir?: stringincludeOxc?: boolean
Returns output paths for Biome/Grit/agent artifacts.
Input:
profile: CdoProfileV1 | stringrepoPaths: string[]engine?: 'biome'write?: booleansafeOnly?: booleanreportPath?: string
Returns: Promise<ApplyReport>
Returns confidence deltas, changed categories, and repo-level diff stats.
Starts stdio MCP server.
Tools exposed:
cdo.learn_stylecdo.generate_style_guidecdo.generate_agent_templatescdo.generate_iteration_reportcdo.apply_style
cdo.learn_style supports:
maxFilesPerReposampleSize(alias ofmaxFilesPerRepo)authorEmailsinferenceMode: deterministic|llm-mcpllmSamplingMode: compact|fullsampleContent(alias ofllmSamplingMode)llmAugmenterCommand
Sampling behavior:
- tracked JS/TS files are discovered
- author filter (if provided)
- files are recency-sorted by commit date
- top
maxFilesPerReposelected per repo
Example tool payload:
{
"repoPaths": ["/path/repo-a", "/path/repo-b"],
"authorEmails": ["you@example.com"],
"sampleSize": 200,
"inferenceMode": "llm-mcp",
"llmSamplingMode": "full",
"llmAugmenterCommand": "node /abs/path/augmenter.mjs",
"minEvidence": 20,
"minConfidence": 0.7
}cdo mcp --self-testYou should see all cdo.* tools listed.
[mcp_servers.cdo]
command = "cdo"
args = ["mcp"]Local repo checkout alternative:
[mcp_servers.cdo]
command = "node"
args = ["/absolute/path/to/cdo/src/cli.js", "mcp"]{
"mcpServers": {
"cdo": {
"command": "cdo",
"args": ["mcp"]
}
}
}{
"mcpServers": {
"cdo": {
"command": "cdo",
"args": ["mcp"]
}
}
}Host-side validation flow:
- list tools
- call
cdo.generate_style_guidefirst - call
cdo.apply_stylewithwrite: falsebefore any write mode
llm-mcp mode allows an external command/function to suggest rule values for low-confidence areas.
Command contract:
- stdin: JSON
{ profile, sampledFiles } - stdout: JSON response with
rules - non-zero exit code: treated as error
Response format:
{
"rules": {
"syntax.yodaConditions": {
"value": "always",
"confidence": 0.87,
"evidenceCount": 18
},
"comments.commentBlockFraming": {
"value": "framed",
"confidence": 0.82,
"evidenceCount": 12
}
}
}Behavior rules:
- only known rule paths are accepted
- invalid values are ignored
- deterministic strong enforced rules are not replaced unless LLM suggestion is materially stronger
- applied LLM rules are marked with
provenance: "llm_augmented" - compact sampling defaults to up to
12files with5000chars per file
Built-in fixture loop targets:
https://github.com/unshiftio/liferaft.githttps://github.com/unshiftio/url-parse.githttps://github.com/unshiftio/recovery.git
npm run fixtures:setupClones/updates into .fixtures/style-fixtures/ (gitignored).
npm run validate:fixturesThis runs:
learnguideconfigapply --engine biome --safe-only(dry-run)report
npm run validate:fixtures:strictCurrent strict thresholds:
max-changed-files = 6max-changed-lines = 80
fixtures:setup supports:
--root <path>orCDO_FIXTURE_ROOT--repos <url1,url2,...>orCDO_FIXTURE_REPOS
validate-fixtures.js supports:
--root <path>orCDO_FIXTURE_ROOT--repos <name1,name2,...>orCDO_FIXTURE_REPO_NAMES--author <email>--engine <biome>--safe-onlyorCDO_FIXTURE_SAFE_ONLY--min-confidence <0-1>orCDO_MIN_CONFIDENCE--min-evidence <n>orCDO_MIN_EVIDENCE--max-changed-files <n>orCDO_MAX_CHANGED_FILES--max-changed-lines <n>orCDO_MAX_CHANGED_LINES--summary-out <path>orCDO_FIXTURE_SUMMARY_OUT
npm test is the full confidence gate.
npm testIt runs:
- coverage-gated test suite (
npm run coverage) - type checking (
npm run typecheck) - strict fixture validation against the 3 fixture repos (
npm run validate:fixtures:strict)
Coverage thresholds:
- statements:
>= 90% - lines:
>= 90% - branches:
>= 70%
Why there are multiple test scripts:
test:run: executes alltest/*.test.js(unit + integration + snapshot)coverage: wrapstest:runinc8with thresholdstypecheck: runs static type checks on the JSDoc-typed codebasetest: single full gate (coverage + typecheck + strict fixture loop)snapshots:test: only snapshot suitesnapshots:update: regenerate golden snapshots after intentional behavior changes
Key directories:
src/runtime implementationtest/all teststest/fixtures/fixture inputs + golden snapshotstest/support/test helpersscripts/developer scripts (fixtures, release smoke, snapshot updater).fixtures/external cloned repos for validation (gitignored)
There is one canonical test tree (test/). Snapshot fixtures are intentionally inside test/fixtures/ so they are versioned with tests.
CDO and mcp-layer are complementary:
- CDO provides domain logic (learn/apply/report)
mcp-layerprovides generic MCP interface surfaces (CLI/REST/GraphQL/OpenAPI)
Example config in this repo:
examples/mcp-layer/cdo-mcp.json
List CDO tools through mcp-layer:
npx -y @mcp-layer/cli@1.2.0 tools list --config ./examples/mcp-layer/cdo-mcp.json --server cdo --format json --no-spinnerCall a tool through mcp-layer:
npx -y @mcp-layer/cli@1.2.0 tools cdo.learn_style \
--config ./examples/mcp-layer/cdo-mcp.json \
--server cdo \
--json '{"repoPaths":["/path/to/repo"],"authorEmails":["you@example.com"]}' \
--rawSymptom: Repository path is not a git repository
Check:
git -C /path/to/repo rev-parse --is-inside-work-treeFix:
- pass valid local git repos to
--repos
Symptom: No parsable source files were found after sampling
Likely causes:
- author filter does not match commit history
- unsupported or excluded file layout
- repos are empty or have no tracked JS/TS files
Actions:
- retry without
--author - inspect commit emails (
git log --format='%ae' | sort | uniq) - raise
--max-files
Symptom: Profile schema validation failed
Actions:
- regenerate profile with
cdo learn - verify
schemaVersionis1.0.0 - avoid manual edits to required fields
Symptom: Unknown apply engine
Fix:
- use
--engine biome
Symptom: Biome check failed: ... loading of plugins
Likely cause:
- invalid generated plugin file or stale generated artifacts
Actions:
- regenerate config artifacts:
cdo config --profile cdo-profile.json --out-dir .cdo - inspect generated plugin files under
.cdo/biome/plugins/ - run biome directly for details:
node $(node -e "console.log(require.resolve('@biomejs/biome/bin/biome'))") check --config-path ./.cdo/biome.json /path/to/file.jsSymptom: filesChanged is always 0
Interpretation:
- target already matches enforced rules, or
- too many rules are
undetermined
Actions:
- inspect
cdo-profile.jsonenforced rules - lower thresholds for exploration
- add more representative source repos
Symptom: dry-run diffs are too noisy
Actions:
- tighten author filter
- increase
--min-evidence/--min-confidence - keep
--safe-onlyenabled - inspect
cdo-iteration-report.jsontop changed categories
Symptom: missing fixture repo error
Fix:
npm run fixtures:setup
npm run validate:fixturesChecks:
cdo mcp --self-test
node ./src/cli.js mcp --self-testIf tools are missing in host app:
- confirm host points to correct
cdobinary/path - restart/reload MCP host
- verify no older global install shadows current package
Quality gates:
npm run publish:readyRuns:
npm testnpm run release:checknpm run release:smoke
Manual publish checklist:
npm whoaminpm run publish:readynpm publish
Current v1 boundaries:
- input sources are local repo paths
- extraction targets JS/TS codebases
- structural rewrites remain conservative
- auto-renaming APIs is intentionally out of scope
MIT