Repo-specific notes for AI agents. README.md is the product-facing source of truth; this file is the "things you would otherwise get wrong" layer.
When docs disagree, trust in this order:
src/**(executable)README.md(kept current — last updated same day as code)integrations/**(current, shipped)INTEGRATION-MCP-RESERVED.md— archival only. MCP was removed from the shipped surface in 0.1.1 and moved toreserved/mcp.ts. The doc still says default backend iscodexand references./dist/mcp.js— both are wrong now. Do not follow its install instructions.
- Git is on
mainbranch with GitHub remote atdevonestar/janus. CI runs on push/PR via.github/workflows/ci.yml(build + test + dist/ freshness check across Node 18/20/22). Dependabot is configured for npm + GitHub Actions weekly updates. - No linter, no formatter. Only mechanical gate beyond
tscisnpm test→ mock-backend smoke onfixtures/smoke.md. It validates output-schema stability, not logic inparser/,sampling/, orloop/. dist/is tracked (intentionally —binresolves to./dist/index.js). After editingsrc/**, runnpm run buildand commit the resultingdist/in the same change.prompts/is still empty.fixtures/smoke.mdis the mock-smoke input — keep it passingrecommend/medium(needs explicit## Goal,## Constraints,## NFRs,## Options,## Assumptionssections persrc/backend/mock.tskeyword detection)..janus-self-modifying-pathsis the machine-readable source of truth for paths that must bypass automatic Janus approval. If a gate-critical file changes and this file is not updated, the workflow drifts silently.- Janus is CLI-first.
marketplace/andintegrations/are adapter layers, not the product definition. When public docs get fuzzy, bias back toward “thejanusbinary is the product; plugins/skills/hooks are optional”. docs/paths inREADME.mdare example paths in CLI usage snippets, not actual directories in the repo. Do not create them or cite them as if they ship.
npm install
npm run build # tsc, outputs ./dist (also the only typecheck you get)
npm run dev <args> # tsx src/index.ts (skip build)
npm link # exposes `janus` globally; uses ./dist/index.js
npm run check:intake -- <file.md> # Stage 1 markdown precheck for Context/Problem, Goal, Constraints, Options/Planned Change, Unknowns, Decision requested
npm run self-dev -- <file.md> [backend] # internal Janus self-dev flow: intake -> eval -> loop -> matching dogfood validator
./dist/index.js eval fixtures/foo.md # direct invocation, no link- ES module project:
"type": "module"+NodeNext. All intra-src/imports must use explicit.jsextensions even for.tsfiles (NodeNext convention). tsconfig.jsonincludeissrc/**/*only.reserved/mcp.tsis not compiled —npm run builddoes not producedist/mcp.js. If MCP is reinstated, addreserved/toincludeor move the file.- Node: README says 18+, the archival MCP doc says 20+. No
enginesfield. If in doubt, test on the higher.
src/index.ts:3doesdelete process.env["CI"]at startup. This is load-bearing for theclaudeheadless backend — do not remove.- Default backend is
claude(notcodex).src/backend/claude.tsspawnsclaude -p --output-format json --model sonnet --no-session-persistence --disable-slash-commandsand parses the envelope'sresultfield for an inner JSON blob. - Default Claude timeout is 240s (bumped from 120s in 0.2.1 to survive
--samples Non large docs). - "Compact" system prompt is only used when backend is
claude | codex | opencode(src/index.ts:61,221). API backends (openai-api,anthropic-api) get the non-compact prompt. janus gatereturns FAIL whendecision_status === "recommend"butbest_path.robustness_score === "low"(src/index.ts:183). Not just a pass-through of exit codes.- Exit codes:
0=recommend,1=conditional,2=blocked,3=error(src/types.ts—EXIT_*). --samplesis clamped to[1,5]; values>3print a "NFR ≤3× baseline" warning but proceed.- Partial variance run on
fixtures/smoke.md(7 runs ofjanus eval --samples 3) showed stabledecision_status+best_pathbut unstablerejected_pathsnaming. Treatrejected_paths[].nameas presentation text, not a stable key. For reviewer automation, compare byviolated_principleand/or canonicalized option labels (Option A/B/C), not exact strings.
src/
index.ts CLI entry (commander); exit-code + format logic lives here
backend/ one file per backend, all implement JanusBackend
interface.ts JanusBackend + createBackend() switch
claude.ts default; shells out to `claude` CLI
codex.ts, opencode.ts, openai-api.ts, anthropic-api.ts, mock.ts
prompt/
system.ts full system prompt
system-compact.ts compact variant (CLI backends only)
output-schema.ts JSON schema embedded into the prompt
builder.ts buildEvalRequest / buildCompareRequest
parser/output.ts validateOutput + formatOutput (json|markdown|yaml)
sampling/aggregator.ts --samples N consensus (majority vote, conservative tie-break, union of rejections/unknowns)
loop/engine.ts `janus loop` Generate→Evaluate→Eliminate→Refine
types.ts shared types + EXIT_* constants
specs/ internal design specs, dogfood docs, roadmap contracts (not user-facing)
integrations/ optional integration layers for the CLI
skill/SKILL.md frontmatter-triggered skill for Claude Code & OpenCode
codex/ copy-paste for Codex AGENTS.md
git-hook/pre-commit bash hook; defaults to backend=codex/model=gpt-5.4 (env-overridable)
marketplace/ Claude Code plugin (`claude plugin marketplace add ./marketplace`)
reserved/mcp.ts archived MCP server; NOT in tsconfig include
The parsed response shape (see types.ts and README.md "What Janus Produces") is load-bearing. Every backend must return an object with at least decision_status; parsers reject missing/extra required fields via validateOutput. When touching prompts or schemas, regenerate fixtures by hand — there are no golden-file tests.
- Strict TS, no
anyin new code (the codebase usesunknown+ narrowing for errors — follow that pattern). - Error handling in the CLI writes human text to
stderrand exits with a typed code. Don'tthrowout of command handlers past the top-leveltry/catch. - Keep principle references P1–P7 exactly as named in
README.mdandSKILL.md; the skill'sdescriptionfrontmatter is what triggers auto-invocation in Claude Code / OpenCode — don't edit it casually.
Janus has a built-in feature development loop: npm run self-dev -- <spec.md> [backend].
Stage 1: intake → check-intake.mjs (markdown structure gate)
Stage 2: eval → janus eval (single-shot evaluation)
Stage 3: loop → janus loop (Generate→Evaluate→Eliminate→Refine, max 3 iterations)
Stage 4: validator → pattern-matched (spec-specific dogfood validation)
The pipeline auto-selects a validator based on spec filename:
| Filename pattern | Validator script | What it checks |
|---|---|---|
/canonical/i |
scripts/validate-canonical-identity.mjs |
archetype_slug / canonical_key / violated_principle stability across repeated evals on 2 fixtures |
/explicit-alternatives|candidate-path/i |
scripts/validate-candidate-paths.mjs |
candidate_paths presence, cap (≤3), dedup, fit_summary length, no best_path overlap; runs eval + compare + loop |
| no match | — | prints "no validator configured for this spec yet" |
- New spec/roadmap document: always run
npm run self-dev -- <spec.md> claudebefore committing - Spec edits: re-run self-dev after significant changes
- Adding a new validator: add an entry to
validatorRegistryinscripts/self-dev.mjswith a filename regex and command array
scripts/variance.sh [N] [fixture] # default: N=10, fixture=fixtures/smoke.mdRuns janus eval --samples 3 N times on identical input. Outputs JSONL log + jitter summary to .janus-variance/. Gate criterion: >1 disagreement dimension per 10 runs → pin --samples 1 or --backend mock.
npm run check:intake -- <file.md> requires these markdown headings (case-insensitive):
## Contextor## Problem## Goal## Constraints## Optionsor## Planned Change## Unknowns## Decision requested
Missing any heading → exit 1. Stage 2 (eval) does not start until intake passes.
Per the dogfood ledger in README.md, spec-level changes (anything under a hypothetical docs/specs/, or large roadmap edits to README.md) are expected to be evaluated with janus eval or janus loop before merging. Code-only changes in src/ are not gated.