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 repositoryinit 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:
- Deterministic —
audit,standardandmcpnever touch a model. Same input, same output, every time; this is what the CI gate stands on. - Agentic —
planandreviewneed a credential and refuse without one, since their entire output is a model's opinion, not an enrichment of something else.sprintandinitare 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 worksA 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 tasteProfiles
--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 eachA 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 runOne 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.jsonis genuinely free-text, so generating it requires--agentic-editexplicitly:kreogen plan "a booking product for clinics, Stripe, no blog" kreogen init --recipe kreogen.recipe.json --agentic-editWithout
--agentic-edit,initnever imports the editing capability and never reaches a provider for this purpose, even if a credential is present and even with--yes—--agentic-editis a separate, independent flag from--yes, never folded into it. A CI script already passing--yesto skip prompts must not silently start making model calls and writing files the moment editing is also unlocked.--agentic-editwithout a TTY and without--yesfails immediately with a usage error rather than hanging on a confirmation nobody can answer; with--yes, the generated diff is applied without asking, same asupdate'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 as | Becomes |
|---|---|
templates/project.gitlab-ci.yml | .gitlab-ci.yml |
templates/project.README.md | README.md |
templates/project.NOTICE | NOTICE |
templates/project.CLAUDE.md | CLAUDE.md |
templates/project.AGENTS.md | AGENTS.md |
templates/project.commitlint.config.js | commitlint.config.js |
templates/project.renovate.json | renovate.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/kreogensrc/ 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.