API reference
CLI command reference
Every command in the CLI's real command registry (packages/cli/construct.mjs's dispatch, packages/core/cli.mjs, packages/core/repl.mjs): flags and one example each.
CLI command reference, next.
Every command construct (and construct repl) actually dispatches, generated at build time from the real command registry: bin/construct.mjs's dispatch table, src/cli.mjs's command functions, and src/repl.mjs's HELP_TOPICS — the exact text construct repl's own help/help <topic> prints, not a hand-copied duplicate.
Overview#
Construct REPL — type a command without the leading "construct". help this list help <topic> detailed help for one topic, e.g. "help refactor" cd <path> change the working directory (affects every command after, instead of --dir) pwd show the current working directory exit / quit leave the REPL Capabilities: create ... scaffold a feature, a layer, or a whole vertical slice refactor ... mechanical, LLM-free moves/renames within the architecture research ... read-only: summarize a feature, explain its workflows, compute a change's impact, or check environment/tooling import ... scaffold layers for an existing, non-Construct file + a breadcrumb to it Note: "construct import --route <path>" (the interactive, whole-feature wizard) is a standalone command — run it directly from your shell, not from inside this REPL. See "help import" for why. Flat commands (unchanged): init, feature, generate, sync, validate, summarize, doctor Topics: create, refactor, research, import, init, feature, generate, sync, validate, summarize, doctor, dir, import-001
construct create#
create — scaffold a feature, a layer, or a whole vertical slice (deciding what to create is always deterministic; filling one is opt-in) create feature <name> [--dir <path>] Scaffold a new feature: all 7 layer folders + types.ts + index.ts. Example: create feature cpo-v2 create layer <name> --feature <feature> --layers <l1,l2,...> [--llm <provider>] [--dir <path>] Scaffold one logical unit across several layers in a single command, always generated in dependency order (domain -> service -> workflow -> hook -> component -> page -> controller) regardless of the order you list --layers in. Example: create layer CpoAccess --feature cpo-v2 --layers domain,hook create <layer> <name> --feature <feature> [--llm <provider>] [--dir <path>] Scaffold a single file in one layer. <layer> is one of: domain, service, workflow, hook, component, page, controller. Example: create domain CpoAccess --feature cpo-v2 Every create command only ever writes a short, self-contained stub by default — filling in the real logic is a separate, deliberate step (by you, or by an LLM you choose). Pass --llm <provider> (same providers as "import", currently: claude, ollama) to have that provider write a real implementation into each generated file instead — one call per file, scoped strictly to that one file's own body. Which layers/files get created is always decided deterministically either way; --llm only changes what ends up inside them. "create feature" has nothing fillable (just types.ts/index.ts boilerplate), so --llm has no effect there. Example: create domain CpoAccess --feature cpo-v2 --llm claude
Source: packages/core/repl.mjs.
construct refactor#
refactor — mechanical, LLM-free moves/renames within the architecture refactor move <name> --feature <feature> --from <layer> --to <layer> [--dir <path>] Relocate a file from one layer to another, keeping its base name. Rewrites every other file's import of it, AND the moved file's own same-layer relative imports (layer folders are siblings, so those would otherwise point at the wrong directory after the move). Example: refactor move CpoAccess --feature cpo-v2 --from domain --to service refactor rename <name> <newName> --feature <feature> --layer <layer> [--dir <path>] Rename a file within the same layer. Same import-rewriting as move. Example: refactor rename Foo Bar --feature cpo-v2 --layer domain Neither command touches a file's own content or its exported identifier — only its location/name and every other file's import path. Whether the result is valid in its new home (naming convention, purity, etc.) is reported immediately afterward via "validate", not decided here. There's no persistent log: the one line each command prints IS the record.
Source: packages/core/repl.mjs.
construct research#
research — read-only: summarize a feature, explain its workflows in English, compute a change's impact, or check environment/tooling research summarize [--feature <name>] [--format json|md|compact|prose] [--since <ref>] [--dir <path>] English or JSON/Markdown summary of a feature's structure and exports. Example: research summarize --feature cpo-v2 --format prose research workflow <feature> [<file>] [--format prose|md|json|scenarios] [--dir <path>] Explains the state machines in features/<feature>/workflows/ in plain English: what each state does, every scenario from start to end (happy path first), and health findings (unreachable states, dead ends, missing fallbacks). Derived from the source every time — no LLM. Formats: prose (default), md, json, scenarios (Given/When/Then only). Example: research workflow checkout CheckoutWorkflow.ts --format scenarios research impact <unit-ref>... [--files a,b] [--since <ref>] [--ticket <text>] [--ticket-file <path>] [--depth N] [--max-files N] [--format json|markdown] [--dir <path>] The blast radius of a change: which features and layers it touches, why each file is implicated, which files are shared across features, and what your rules already say about them. Deterministic and LLM-free. Seeds are unit refs (feature:login, a file path, /login, rule:PAGE-003), changed files (--files, --since <ref>) or a ticket in English (--ticket). Every entry is marked "derived" (computed from the graph) or "inferred" (reached only from a seed guessed from ticket text). Depth defaults to 2 importer hops; what lies past it is counted, not dropped. Example: research impact feature:login --depth 3 --format markdown research spec <file> [--generate [--feature <name>] | --read-back | --coverage [--feature <name>]] [--format json|text] [--dir <path>] Checks a machine-spec.v1 file: an English requirement (sentences with ids) broken down into states, events, guarded transitions and typed functions, every item linked back to its sentence by "req". Refuses the spec with a SPEC-* code, the path and the reason: unreachable state, unknown state/event, untyped function or undeclared type, a sentence neither covered nor marked out of scope. Deterministic, no LLM; exit 1 on any failure. Schema and worked example: docs/machine-spec.md. --generate: on an accepted spec, writes the workflow (typed state union, typed event payloads, named guard stubs, each a TODO returning false), the declared "types" into the feature's types.ts, one defineService(...) stub per function (throws, precondition/ postcondition and req text as comments) and the machine's locked every-path unit test -- never overwrites an existing file. --feature is used only when the spec has no "feature" field; exit 2 when neither is given. --read-back: on an accepted spec, prints it in plain English per requirement sentence (the states, events, transitions and functions that point at it; sentences out of scope listed as such), in the workflow narrator's words. --format json: the same as a fixed-shape list. Writes nothing; exclusive with --generate. --coverage: sentence -> functions -> the files --generate writes, and functions -> sentences (an orphan cites none); sentences out of scope listed as such. Exit 1 only when a sentence is uncovered and not out of scope. --format json: a fixed-shape result. Writes nothing; exclusive with --generate and --read-back; needs a feature name. Example: research spec specs/sign-in.machine-spec.json --generate --feature auth research doctor [--dir <path>] Environment sanity check: node/npm versions, architecture.yml presence, which enforcer modules are available.
Source: packages/core/repl.mjs.
construct import#
import — the tool-only half of migrating an existing, non-Construct file import <name> --feature <feature> --layers <l1,l2,...> --from <path> [--dir <path>] Scaffolds --layers exactly like "create layer" (same dependency-order guarantee), then prepends a "TODO(import): port ... from <path>" comment to each generated file. Never reads <path>'s content — only confirms it exists. Example: import CpoAccess --feature cpo-v2 --layers domain,hook --from ../src/old/CpoGate.tsx import --plan <path> [--dir <path>] The whole-feature form: runs the same scaffold-and-breadcrumb step once per unit listed in a plan file, in one command. The plan itself — which old files map to which logical units/layers — is judgment, not something construct can produce; have whichever LLM you're using analyze the old feature and propose the plan, review/approve it yourself, then execute it here. Plan shape: { "feature": "cpo-v2", "units": [ { "name": "CpoAccess", "layers": ["domain","hook"], "from": "../old/CpoGate.ts" }, ... ] } Either form does everything a tool safely can (locate the source(s), scaffold structure, wire the breadcrumb(s)) on its own, and by default stops there: reading the old file(s) and writing the real ported logic is left for you or whichever LLM you choose, as an explicit next step. Add --llm <provider> to have import do that step for you instead: it calls the provider once per generated file (never once for the whole batch), each time with that file's own layer constraints and the old source, and writes the returned code directly, replacing the TODO breadcrumb. construct only ever calls an LLM when you pass this flag (here, or the same flag on "create"/"generate" — see "help create") — everything else (locating files, scaffolding, --plan's batching, deciding what to create) stays exactly as deterministic either way. Supported providers: claude (shells out to the "claude" CLI; must be installed and authenticated on your machine), ollama (calls a local Ollama server's HTTP API; model configurable, defaults to qwen2.5-coder:7b against http://localhost:11434) Example: import CpoAccess --feature cpo-v2 --layers domain,hook --from ../src/old/CpoGate.tsx --llm claude Example: import --plan plan.json --llm claude Always review LLM-written output (diff against the source) before trusting it — run "validate" right after either way. import --route <path> [standalone command — run directly, not in repl] The guided, whole-feature form. <path> is a real router route — a URL like /v2/home, or the folder that owns its page.tsx — and seeds the first one; it then loops asking for as many more as the old feature actually spans (e.g. /v2/home and /v2/home/details — one combined analysis call covers all of them together, not one call each). Each route is traced through its real import graph (page -> Client -> hooks/components, stopping at known external boundaries) rather than reading a hand-picked directory; a URL-style route needs to know where your app/ directory is, asked once and reused for the rest of the session. It auto-creates the destination feature if it doesn't exist yet (never touches it if it already does), shows you the proposed plan, and only scaffolds anything once you approve it. After building, it immediately runs validate and tells you exactly what's left — a TODO(import) count to fill in, or a reminder to review LLM-written output. It manages its own input, so run "construct import --route <path>" directly from your shell — not typed inside this REPL, which is already reading its own input. Example: construct import --route /v2/home Example: construct import --route ../src/old-feature/routes/home
Source: packages/core/repl.mjs.
construct init#
init [dir] Bootstrap a brand-new Construct project at <dir> (default: cwd): architecture.yml, AGENTS.md, a starter "core" feature, and app/page.tsx. Works inside a subdirectory of a larger, unrelated project — nothing required at that project's own root. Example: init construct
Source: packages/core/repl.mjs.
construct feature#
feature create <name> [--dir <path>] The flat form of "create feature". Scaffolds all 7 layer folders plus types.ts/index.ts for a new feature.
Source: packages/core/repl.mjs.
construct generate#
generate <layer> <name> --feature <feature> [--llm <provider>] [--dir <path>] generate layer <name> --feature <feature> --layers <l1,l2,...> [--llm <provider>] [--dir <path>] The flat form of "create <layer>" / "create layer" — see "help create".
Source: packages/core/repl.mjs.
construct sync#
sync [--dir <path>] Regenerate .dependency-cruiser.cjs and each feature's index.ts public API from its actual exports (controllers + hooks, by default).
Source: packages/core/repl.mjs.
construct validate#
validate [--format json] [--dir <path>] Run every enforcer (architecture boundaries, separation of concerns, readability, public-API drift) and report violations.
Source: packages/core/repl.mjs.
construct summarize#
summarize [--feature <name>] [--format json|md|compact|prose] [--since <ref>] [--dir <path>] The flat form of "research summarize". --since <ref> scopes the report to only the features touched since that git ref. summarize --backend [<dir>] [--format json] Read-only, deterministic summary of a Node.js / Express backend, no model and no rules: the route map (method, full path with router mounts resolved, handler, middleware, file:line), each file's role (route, store, service, auth, job, util, config, test, other) with the reason, the import graph and its cycles, process.env names (never values) and effects (fs, child_process, network, timers). Without <dir> it reads backend.dir from architecture.yml, else the project root. Roles are overridable through a backend: section in architecture.yml. Express and plain http.createServer only; whatever it cannot read is listed as "not detected". --format json prints backend-summary.v1 (schemas/backend-summary.v1.json). Example: summarize --backend ui/server/src --format json
Source: packages/core/repl.mjs.
construct doctor#
doctor [--format json] [--dir <path>] Check this machine, read-only, no network, no model: Node and npm, the project's architecture.yml, and what the machine can run. It reports the tier (Lite, Cockpit use or Contributor) with the reason, what is switched on and off and why, and the exact command that fixes each missing optional item (ffmpeg, a Playwright browser, Ollama, python3, voice models). Exits 0, or 1 for a Node older than 20 or a machine it cannot read. The flat form of "research doctor".
Source: packages/core/repl.mjs.
construct review#
construct review <base> <head> [--plan <file>] [--features a,b] [--no-merge-base] [--format json|markdown] [--dir <path>] | construct review --usage. Read-only PR health: the deterministic indicators for the change from to (any two refs; local branches need no GitHub login): declared-vs-actual scope (only with --plan/--features), unexplained changes, rule regressions, public surface, and a flow diff. Findings are split into mechanical (a Construct block can fix them) and conversation (a human decides). No LLM, and the working tree, index and branches are never touched. Returns true when it printed only JSON.
Usage:
construct review <base> <head> [--plan <file>] [--features a,b] [--no-merge-base] [--format json|markdown] [--dir <path>]
Source: packages/core/cli.mjs.
construct test#
construct test run <feature> [--name <file> --area generated|yours] [--base-url <url>] [--format json|text] [--dir <path>] | construct test proof <feature> | construct test types [--feature <f>] and construct test build. Runs a feature's Playwright tests against the project's own running app and says, for each failure, whether the harness or the app is at fault; proof runs the render proof of a shaped screen offline; types type-checks the project with its own TypeScript and build runs its build script (bounded), each printing a classified result, never a raw log. Read-only, deterministic, no LLM. Exit code 1 when any test failed or a check found a problem.
Usage:
construct test run <feature> [--name <file> --area generated|yours] [--base-url <url>] [--format json|text] [--dir <path>]\n construct test proof <feature> [--name <file>] [--format json|text] [--dir <path>]\n construct test types [--feature <feature>] [--format json|text] [--dir <path>]\n construct test build [--format json|text] [--dir <path>]
Source: packages/core/cli.mjs.
construct process#
construct process gc [--dry-run] [--older-than <days>]: sweep the process-runtime debris a restart or an abandoned review leaves behind -- dead-owner bot worktrees, orphaned construct/bot/* branches -- and flag (never delete) process records older than the threshold that still have an approval pending. Deterministic, JSON in/out (packages/engine/processGc.mjs); no model is ever involved.
Example:
processCommand(['gc', '--dry-run']);
/
Source: packages/core/cli.mjs.
construct template#
construct template list|show|instantiate. Read-only, JSON in/out, no LLM. construct template list [--templates ] construct template show [--templates ] construct template instantiate [--templates ] [--param key=value]... [--params-json '{"k":"v"}'] A template is a named, reusable, parameterised plan; instantiate prints an ordinary plan.v1 (feed it to the runner or construct review --plan). The mechanism is open; a small starter set of generic feature-shape templates ships with it (DEFAULT_TEMPLATES_DIR). --templates <dir> (or CONSTRUCT_TEMPLATES_DIR) points at a project's own curated set instead; nothing here runs a plan.
Usage:
construct template list|show <name>|instantiate <name> [--param key=value]... [--params-json <json>] [--templates <dir>] (or CONSTRUCT_TEMPLATES_DIR)
Source: packages/core/cli.mjs.
construct pipeline#
Usage:
construct traces list [--chooser <id>] [--limit <n>] [--json] | stats [--chooser <id>] [--json] | replay (--provider <name> [--plugin <file.mjs>] | --model <name> [--all]) [--chooser <id>] [--min-traces <n>] [--baseline <name>] [--json] | export --out <dir> [--since <date>] [--chooser <id>] [--yes] [--json] [--dir <path>]
Source: packages/core/cli.mjs.
construct traces#
construct traces list|stats|replay|export: read-only (export writes only with --yes, and only the bundle folder), deterministic, no model and no network. The traces are the decision-trace.v1 records the chain wrote to this project's state directory (docs/DECISION-TRACES.md). list [--chooser ] [--limit ] the recorded decisions stats [--chooser ] counts per chooser, acceptance rate of suggestions, per provider replay --provider [--chooser ] [--min-traces ] [--baseline ] [--plugin <file.mjs>] score a provider on the recorded summaries against the rules baseline replay --model [--all] [--chooser ] [--min-traces ] [--baseline ] the same, for a model registered by construct model import (a features.json or a prototypes.json model), ENABLED OR NOT: scoring a model offline never enables it. It scores only decisions the model has not seen (the held-out records of its export and everything recorded after it); --all scores every recorded decision, including the ones it was built from export --out [--since ] [--chooser ] [--yes] a dataset bundle for training the decision model ELSEWHERE: a PREVIEW of exactly what would be included (records per chooser, the fields, the date range, hashed project keys, no path or secret) unless --yes writes dataset.jsonl, schema.json, manifest.json and README.md into ; traces: off projects contribute nothing; docs/TRAIN-ELSEWHERE.md --json (or --format json) prints one JSON document. --plugin <file.mjs> imports a module that registers a decision provider (it calls registerDecisionProvider, or default-exports { name, suggest }); code you name yourself, run locally, so it is as trusted as any script you run. A provider that is not registered is a usage error (exit 2).
Example:
await traces(['replay', '--provider', 'rules', '--json']);
/
Source: packages/core/cli.mjs.
construct decide#
construct decide --summary <file|-> [--provider <name>] | construct decide --requirement "<sentence>" [--provider <name>]: ask the project's decision provider which option to take next. Read-only, writes nothing, and a suggestion never applies itself. This is the tool an LLM calls: a chooser or open-question summary (JSON, from a file or stdin) in, one suggestion out. --requirement reads a sentence with parseRequirement and placeCard and prints, for each open question and offer, what the provider suggests and why. No model is called unless the project's architecture.yml names a plugin (decision: { provider, plugin }); the built-in rules provider (the default) and off need none. A plugin that fails or is slow is replaced by the rules provider and a line on stderr says so. --format json prints one document, { ok, provider, requested, fellBackFrom, notes, suggestion } (or questions), and an error as { ok: false, error: { code, message } } with exit code 2 for a usage error.
Example:
await decide(['--summary', 'chooser-summary.json', '--format', 'json']);
/
Source: packages/core/cli.mjs.
construct model#
construct model list|import|remove|enable|disable: a decision model trained on ANOTHER machine comes back as a folder of plain data and is verified here. Nothing trains here and nothing in the folder is ever executed. import [--yes] [--min-traces ] verify the bundle (allowed files only: manifest.json, checksums.txt, eval-report.json, MODEL_CARD.md, features.json, prototypes.json, model.onnx; no script, pickle, link or path; size caps; sha256 of every file; the dataset hash must be one THIS project exported), replay the held-out test records against the rules baseline (a features.json model; an .onnx file is verified and stored, not loaded) and print the comparison; with --yes register it in the state directory DISABLED. architecture.yml is never edited. list the registered models remove delete a registered model (state directory only) enable | disable the owner's explicit act: one flag in the state directory (docs/TRAIN-ELSEWHERE.md) --json prints one JSON document; a refusal is { ok: false, error: { code, message } } and exit code 1 (2 for a usage error), never a stack trace.
Example:
await model(['import', 'model-out', '--yes']);
/
Source: packages/core/cli.mjs.
construct add-rule#
construct add-rule no-console --layers domain,service: scaffold the rule file and its fixture pair. /
Source: packages/core/cli.mjs.
construct export#
construct export ci --target <dependency-cruiser|eslint-boundaries> [--out <path>] [--dir <path>]: writes (or, without --out, prints to stdout) the same CI-tool config sync writes for dependency-cruiser, or the eslint-boundaries equivalent. Read-only otherwise -- unlike sync, this never touches feature public APIs. /
Source: packages/core/cli.mjs.
construct check-change#
construct check-change --file <path> [--ref <ref>]: a deterministic "did this staged edit change behaviour" indicator, meant to be run during a session before saving, not only at PR/CI time. JSON out always -- see check-change.mjs. /
Source: packages/core/cli.mjs.
construct mutation-check#
construct mutation-check <file...> [--test-command <cmd>]: an on-demand, Stryker-backed test-strength signal scoped to the given files -- never triggered by check-change's live path, only by an explicit call. JSON out always -- see mutation-check.mjs. /
Usage:
construct rules list [--json] [--dir <path>]
Source: packages/core/cli.mjs.
construct rules#
construct rules list [--json] [--dir <path>]: every rule construct validate can report -- id, owning module, layer(s), scope (buffer needs only the one file, project needs the whole graph), default and effective severity (--dir's architecture.yml override, if any), why the rule exists, what a passing file looks like, and a fix hint. Read-only, deterministic, packages/core/rules-catalog.mjs's listRules (also GET /api/rules, ui/server/src/rulesApi.mjs).
Example:
rulesCommand(['list', '--json']);
/
Source: packages/core/cli.mjs.
construct rename#
construct rename <id|unit> <newName> --feature <feature> [--dry-run] (LIN-152): cascade a rename across every layer of one unit (page/viewmodel/controller/adapter/... — whichever layers it has) — a map edit (LIN-154's unit/name/path attributes) plus a regenerate, never a file-by-file hand edit. Business logic (a member's slot body) is byte-identical after a rename; only generated wiring moves. --dry-run lists every file that would be written, renamed or deleted, before touching any of them; a collision or an existing target file refuses and writes nothing.
Example:
await rename(['refund-request', 'reimbursement', '--feature', 'billing']);
/
Source: packages/core/cli.mjs.
construct repl#
Interactive shell over the exact same functions a one-shot invocation dispatches to — no new command logic. Type any command above without the leading "construct", plus "help"/"help ", "cd " and "pwd".
Example:
construct repl
Source: packages/core/repl.mjs.