CLI

The published tool that creates and updates projects.

packages/cli is published to npm as @kreotic/kreogen:

npx @kreotic/kreogen@latest init      # create a project
npx @kreotic/kreogen@latest update    # apply template changes
npx @kreotic/kreogen@latest audit     # measure a repository against the standard
npx @kreotic/kreogen@latest sprint    # emit a handoff pack from that audit
npx @kreotic/kreogen@latest plan      # interview a brief into a recipe init can build
npx @kreotic/kreogen@latest doctor    # check what the optional parts can do
npx @kreotic/kreogen@latest standard  # print the conventions the audit enforces
npx @kreotic/kreogen@latest mcp       # serve the audit as MCP tools over stdio
npx @kreotic/kreogen@latest review    # open-ended agentic review of a repository

init is covered by the quickstart; update has its own page. This page is about the package itself — read it if you are changing the template rather than using it.

The CLI is template-only. init strips packages/cli from generated projects, so it does not appear in a project built from kreogen.

Two engines

Every command runs on one of two engines, decided by a single call site (engine/select.ts's selectEngine) rather than each command detecting a credential its own way:

  • Deterministicaudit, standard and mcp never touch a model. Same input, same output, every time; this is what the CI gate stands on.
  • Agenticplan and review need a credential and refuse without one, since their entire output is a model's opinion, not an enrichment of something else. sprint and init are blended: a complete, correct run with no model at all, plus optional prose or capabilities layered on top when a credential happens to be available.
kreogen doctor          # node, git, package managers, disk space, both engines, ...
kreogen doctor --probe  # also spend one request proving the credential actually works

A provider is detected from whichever of these credentials is set, in this order: OPENROUTER_API_KEY, AI_GATEWAY_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, then OLLAMA_HOST for a local runtime. --model provider:model and KREOGEN_AI_MODEL / KREOGEN_AI_PROVIDER both override detection, in that order.

--model and --max-cost are defined once, in cli/program.ts's withModelOptions, and applied to every command that can reach a provider -- init, sprint, plan and review -- so their help text cannot drift apart per command. The order that change has to happen in is worth knowing before a fifth command joins them: the command's own options type has to carry model and pass it to selectEngine() first, because registering the flag alone parses it and throws it away, which is a worse surface than not having it. On init both flags bound the marketing-copy pass and nothing else; without --agentic-edit it reaches no provider at all.

--max-cost <usd> is honest rather than universal: only OpenRouter and the Vercel AI Gateway bill per request and report what they billed, so only against those two is a ceiling actually enforced. Against any other provider the meter counts tokens and says so — a ceiling that silently did nothing would be worse than no ceiling at all, since someone would set it and believe it. sprint --apply, plan and init --agentic-edit all warn on stderr when --max-cost was set against a provider that cannot honor it.

review --max-tokens <n> is the ceiling that is universal. Token counts come back on every step from every provider, so it binds where --max-cost cannot — and it binds between steps as well as between units, not only at a unit boundary. That distinction is the whole reason it exists: one 20-step review consumed 622,824 input tokens under a --max-cost that could never be reached, and a decomposed run in this sprint consumed 1,990,168.

KREOGEN_AI_REPLAY=1 is how every AI-touching test in this repository runs: it disables outbound requests entirely and serves only what is already cached, degrading (never failing) on a cache miss. It exists so the test suite needs neither a live credential nor a mock of the whole AI SDK, and it is safe to set the same way in any CI job that should never make a live model call by accident.

stdout is the machine channel

Every command's --json document is the only thing on its stdout. Progress lines, warnings, the intro and outro, the live panel and the spend summary all go to stderr. A terminal interleaves the two, so an interactive run looks exactly as it did.

This was a contract in intent and not in fact until this release. cli/tui/components/status-line.tsx wrote to stdout, because it was built as a drop-in for @clack/prompts and that is where @clack defaulted — so kreogen review --json > out.json produced a file with three human lines ahead of the document and a spend line after it, and did not parse. audit --json looked clean only because it returns before any of those calls are reached.

If you were scraping human output off stdout — kreogen sprint | grep, kreogen init | tee — it is now on stderr. Redirect with 2>&1.

packages/cli/__tests__/cli/json-output.test.ts is the gate: one row per command exposing --json, each asserting that the whole of stdout parses as a single JSON document. Adding a --json command means adding a row.

A command whose entire output is a document keeps that document on stdout without --json too — kreogen standard prints its listing there, so kreogen standard > standard.txt works. What moved is everything a human reads around a result, never the result.

Auditing a repository

audit, sprint, standard and mcp all sit on top of the same deterministic core: kreogen audit . fingerprints a Next.js repository (no model, no network) and evaluates it against the hand-authored rule set, printing a score, a tier and a gate verdict. kreogen standard prints the rules themselves — kreogen standard lists all of them, kreogen standard explain <rule-id> prints one in full. The same rule set, rendered, is the standard page.

kreogen audit .                        # score, tier, gate verdict
kreogen audit . --json                 # the report as JSON, nothing else
kreogen audit . --only data,delivery   # just these rule ids or categories
kreogen audit . --fail-on blocker      # exit 3 only at or above this severity
kreogen audit . --profile kreogen      # add the rules that are kreogen's taste

Profiles

--profile general is the default, and it is the set of rules that hold in any Next.js repository. --profile kreogen is a superset: it adds the rules that encode a house preference rather than a general convention.

Exactly one rule is in the kreogen profile today, tooling/formatter-is-ultracite. Ultracite is a Biome preset, so a repository already running Biome has made the choice that rule argues for and differs only in which preset it extends — charging a stranger's repository for that spends the audit's credibility on nothing. The mechanism exists so the next such rule has somewhere to go other than out.

kreogen sprint takes the same flag and the same default, because a handoff pack is a charge against a repository in exactly the way a report is -- more so, since --apply acts on it. The MCP server's audit tools are pinned to general and take no flag.

--only naming a rule outside the active profile is a usage error rather than an empty report and exit 0. --skip is checked against the whole standard, since skipping an already-excluded rule is a no-op. kreogen standard lists every rule regardless of profile — it is what generates the standard page — and takes the same --profile flag to filter.

sprint turns a report into a Claude Code handoff pack — one document per finding, grouped into workstreams, written to <path>/.kreogen by default:

kreogen sprint .              # write the pack
kreogen sprint . --check      # exit 5 if regenerating it in memory would change anything
kreogen sprint . --apply      # run the eligible fixes, one commit each

A finding is eligible for --apply only when its rule declares a single command that produces the target state, declares that command idempotent, and declares interactive: false — its own assertion that the command completes without asking a question. Silence is read as "do not run": a rule that says nothing about prompting has asserted nothing, and ultracite init under an explicit --yes is what taught that, stopping on "Which linter do you want to use?". Everything --apply declines to touch is listed with the reason.

--check is the CI gate: it regenerates the pack in memory and diffs it against what is on disk, so a handoff pack going stale is a build failure rather than something someone notices weeks later. That comparison has to be stable run to run with an unchanged repository, which is why the model layer below is deliberately not part of it by default.

kreogen mcp --root <dir> serves the same audit as MCP tools over stdio, for an agent (a connected Claude Code session, for instance) already working inside the repository being audited: kreogen_audit, kreogen_fingerprint, kreogen_standard, kreogen_read_file, kreogen_grep and kreogen_explain (which cross-references a file against every rule whose evidence names it, in one call, instead of three). Every tool is read-only and jailed to the root the server was started with — realpath-checked on both ends, so a symlink inside the root that points outside it cannot be used to escape it.

sprint's narration can read the repository

sprint's model prose (the paragraph under each finding explaining what it looks like in this repository) is not handed a pre-serialized blob and nothing else — it also has read_file, list_directory, grep and git_log tools, jailed to the repository being audited, to verify a specific claim before writing about it. The loop is bounded on two independent axes so neither a cost surprise nor a runaway trajectory is possible: a hard step ceiling, and a live budget check between steps once --max-cost is set. A trajectory that used a tool is not written to the model-response cache, since the next run may see a genuinely different repository; a tool-free response is cached exactly as before. None of this changes what a model is allowed to say — parseSynthesis still drops anything naming a finding the audit did not raise, and the pack renders identically whether or not a tool ever fired.

kreogen review

The rule set can only ever encode patterns someone thought to write ahead of time. kreogen review is a different, additive kind of value: an open-ended agentic pass with the same explore tools as sprint's narration and no findings to narrate — just the repository, and instructions to find things a fixed rule set would not catch (a subtle bug, an over-engineered abstraction, a misleading name).

kreogen review .                          # needs --max-cost or -y; there is no free-running default
kreogen review . --max-cost 2 --json
kreogen review . -y --max-tokens 500000
kreogen review . --scope packages/auth -y # one workspace, in full
kreogen review . -y --no-decompose        # one pass over everything, as it used to run

One pass per workspace

The default is one bounded pass per workspace, merged. That is not a performance tweak — it is the difference between the command working and not. The same model, the same prompt and the same planted x-forwarded-for off-by-one: found and explained when the run was scoped to the one package, and findings: [] when it was pointed at the monorepo that package lives in. Breadth was the binding constraint, not the prompt and not the model.

A review unit is a workspace, because a workspace is the only decomposition the repository itself declares. Each pass is jailed to that directory and seeded with that directory's own file inventory, and it is told which workspace it is in and that the others are reviewed separately — a model handed one package's file list without being told so reports its unreachable dependencies as findings.

--max-steps is a total across units, not a per-unit ceiling. The default is 12 steps per unit, so it scales with the repository. A total that divides to fewer than 5 steps each is refused with a usage error naming the arithmetic and both escape hatches, rather than running a dozen passes too short to read a file.

A unit whose pass fails — a provider error, an unparseable response, a content filter — degrades that unit and the merge proceeds. The rendered document carries a ## Coverage section naming what was and was not reviewed, because a run that covered 22 of 25 workspaces reads exactly like one that covered 25 and found nothing unless it says so. Findings are merged and de-duplicated on path and title, each keeping its unit as provenance.

--scope <dir> narrows to one workspace and --no-decompose forces the old single pass. Both are escape hatches, not modes.

Its findings use a deliberately different vocabulary from an audit finding — worth-fixing / worth-discussing / minor, not blocker/high/medium/low/info — so the two can never be mistaken for each other. They are written to <out>/review/findings.md and findings.json, outside sprint's pack entirely: review never affects sprint's score, gate or exit code, and its own exit code never depends on what it found — only on whether it produced something usable at all. The rendered document opens by saying so.

Run it from a real terminal and each step updates a live panel in place — current step, the tool call it's making, running spend, and how much of --max-cost is left. Piped or redirected output (a CI log, > out.txt) gets the same plain step N/max: toolName line this command has always written, so tailing a long run in CI still shows it making progress rather than nothing until the very end.

kreogen plan and agentic branding

kreogen plan "<a sentence or two>" interviews a brief into two files — kreogen.recipe.json and PROJECT-BRIEF.md — and writes nothing else; it never calls init, and no model runs after the recipe is written. That is the safety argument for the whole command: a model's opinion becomes a JSON file someone can read and edit, and the step that actually generates a repository (init --recipe kreogen.recipe.json) takes that file and no model at all.

The interview can propose more than scope and capabilities — a branding object (product name, description, legal name, support email, url, social links) if the brief gives it enough to work with. init applies that proposal in two different ways, chosen deliberately per field:

  • Identity is deterministic. packages/branding/index.ts — the single source every generated project's footer, header and page metadata read from — is rewritten by a targeted, template-fill pass, not a model call. No brief means no rewrite: the template's placeholder identity ships unchanged, exactly as it always has.

  • Marketing copy is agentic, and opt-in. The hero and footer prose in packages/internationalization/dictionaries/en.json is genuinely free-text, so generating it requires --agentic-edit explicitly:

    kreogen plan "a booking product for clinics, Stripe, no blog"
    kreogen init --recipe kreogen.recipe.json --agentic-edit

    Without --agentic-edit, init never imports the editing capability and never reaches a provider for this purpose, even if a credential is present and even with --yes--agentic-edit is a separate, independent flag from --yes, never folded into it. A CI script already passing --yes to skip prompts must not silently start making model calls and writing files the moment editing is also unlocked. --agentic-edit without a TTY and without --yes fails immediately with a usage error rather than hanging on a confirmation nobody can answer; with --yes, the generated diff is applied without asking, same as update's own -y.

    Like review, a failed or empty agentic-edit run degrades: project generation still finishes, and a one-line warning explains that the copy step produced nothing.

Agentic file editing

init --agentic-edit is powered by a small, general editing capability (engine/agentic/capabilities/edit.ts) built on two tools — write_file (full create/replace) and apply_patch (a unified diff, applied through git apply) — bounded exactly like review's loop (a hard step count plus a live budget check, a write consuming a step the same as a read).

The loop never touches the real project directory while it runs. It operates against a filtered, disposable shadow copy; the caller only applies the resulting changes to the real directory after computing a diff per file and getting it approved (a prompt, or --yes) — the same dry-run-then-confirm shape kreogen update already uses for template updates. Every write is also jailed to its root the same way the read-only explore tools are: .git and node_modules are refused outright, and a write over the size cap is rejected rather than silently truncated.

What counts as template-only

bootstrap/constants.ts is the list, and it is the file to edit when adding tooling that belongs to kreogen rather than to projects:

export const INTERNAL_DIRS = [
  '.gitlab', '.claude', 'ci', 'apps/docs', 'packages/cli', '.turbo',
];

export const INTERNAL_FILES = [
  '.gitlab-ci.yml', 'release.config.js', 'commitlint.config.js',
  'renovate.json', 'CHANGELOG.md', 'CODEOWNERS', 'CONTRIBUTING.md',
  'SECURITY.md', 'LICENSE', 'CLAUDE.md',
];

Add template-only tooling here, or strip.test.ts and the cli:smoke CI job fail. That is deliberate — the test is what stops kreogen's own pipeline and documentation from shipping inside a client project.

LICENSE is stripped because a generated project picks its own. The upstream grants it must still carry travel in LICENSES/ and the swapped-in NOTICE, so do not add LICENSES/ to that list.

Template swaps

Some files must exist in a generated project but with different content. Those ship under templates/ so the template's own copy can be stripped, then are renamed into place:

Shipped asBecomes
templates/project.gitlab-ci.yml.gitlab-ci.yml
templates/project.README.mdREADME.md
templates/project.NOTICENOTICE
templates/project.CLAUDE.mdCLAUDE.md
templates/project.AGENTS.mdAGENTS.md
templates/project.commitlint.config.jscommitlint.config.js
templates/project.renovate.jsonrenovate.json

Without this, every client project would inherit kreogen's CI pipeline, its README, and a CLAUDE.md describing a repository the project does not have — which taught every coding agent in a client project about the wrong codebase.

The last two rows are the ones that cost most when they are missing. Stripping commitlint.config.js without replacing it is worse than shipping kreogen's: lefthook still runs commitlint on commit-msg, and commitlint with no config exits 9 on empty-rules, so init could not make the project's own first commit. And without renovate.json a generated project has no dependency updates at all — which the audit reports as a finding kreogen itself does not have, and which nobody notices until the first CVE.

update derives its never-touch list from this table rather than restating it, so a swap added later is protected without anyone remembering to.

Package managers

bun, pnpm and npm. yarn is deliberately unsupported: Classic 1.22 is end-of-life and has no real workspace-protocol support.

Choosing anything but bun triggers a conversion — workspace:* ranges rewritten, bun-specific scripts rewritten, bun.lock removed. pnpm additionally gets a pnpm-workspace.yaml with onlyBuiltDependencies, because pnpm blocks lifecycle scripts by default and without it prisma generate and sharp silently never run.

The pinned versions live in bootstrap/constants.ts and are kept current by a Renovate custom manager, since no dependency manager would look for a version inside a TypeScript file.

Versioning

packages/cli/package.json deliberately carries no version field in git. It is written during release.

bun.lock records versions for workspace packages but not for the root, so bumping the root version is lockfile-safe while bumping the CLI's would dirty the lockfile and break the next --frozen-lockfile install.

The published build reads its own version from package.json at startup rather than having it baked in by the bundler, so kreogen --version always reports what npm actually installed.

Developing on it

bun run cli:build          # tsup
bun test --filter @kreotic/kreogen

src/ is organized around the two-engine split above: cli/ (Commander wiring, errors, exit codes), engine/deterministic/ (fingerprint, rules, evaluate, the handoff pack), engine/agentic/ (provider resolution, budget, the bounded tool loop, and one file per capability — narrate, review, interview, edit), bootstrap/ (everything project-generation, engine-agnostic), mcp/ (the MCP server), and infra/ (filesystem jail, subprocess spawning, diffing — no CLI or engine concept). __tests__/ mirrors that tree 1:1.

The tests are the interesting part: strip.test.ts asserts the internal content list against the real repository, update.test.ts covers the diff parser — including the cases that broke it, such as a filename containing a space and core.quotepath escaping non-ASCII into C-style octal — and engine/agentic/runtime/loop.test.ts proves the shared bounded tool loop's step and budget ceilings against a scripted mock model rather than merely asserting them.