Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,28 @@ naming constraints before applying schema changes.
per field, validates each JSON result, aggregates the decisions, and applies
safe changes.

Schemator can also use another agent runtime:

```bash
schemator run --strategy pi --reviewer-model claude-bridge/claude-sonnet-4-6 --source schema.md --context project-context.md --out .schemator
schemator run --strategy pi --reviewer-model openai/gpt-5.1 --reviewer-arg=--thinking --reviewer-arg off --source schema.md --out .schemator
```

Use `command` for any CLI that reads the field-review prompt from stdin and
prints one field-review JSON object to stdout:

```bash
schemator review --strategy command --reviewer-command ./review-field --graph .schemator/graph.iteration-1.json --out .schemator/reviews.iteration-1
```

Shared reviewer options:

- `--reviewer-command <path>`: executable for `codex`, `pi`, or `command`.
- `--reviewer-model <name>`: provider/model passed to the reviewer when supported.
- `--reviewer-timeout-ms <n>`: per-field timeout.
- `--reviewer-concurrency <n>`: maximum concurrent external reviewers.
- `--reviewer-arg <arg>`: extra reviewer argument; repeat for multiple args. Use `--reviewer-arg=--flag` when the value itself starts with `--`.

Use local mode only for smoke tests:

```bash
Expand Down
89 changes: 55 additions & 34 deletions skills/schemator/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,71 +1,92 @@
---
name: schemator
description: Use when running Schemator to extract, review, simplify, converge, diff, or report on data models, JSON schemas, API payloads, SQL tables, migrations, TypeScript interfaces, YAML resources, or other field-based schemas.
description: "Use when running Schemator to extract, review, simplify, converge, diff, or report on database schemas, ORM models, migrations, API payloads, JSON Schema, TypeScript interfaces, YAML resources, or other field-based data models."
---

# Schemator

Use this skill when a task asks to run or interpret Schemator, improve a schema
with Schemator, inspect Schemator artifacts, or prepare a Schemator-backed
schema report.
Use Schemator as a schema-design reviewer: it extracts fields from an existing
schema/proposal, asks reviewers to challenge each field, applies safe reductions,
and reports the resulting graph. It is a design aid, not product truth.

## Workflow

1. Start from a real draft schema or proposal. Schemator reviews existing model
shapes; it does not invent the first draft.
2. Write or locate project/task context before review. Context should explain
the product goal, naming conventions, borrowed vocabulary, user-facing
constraints, and what should remain stable.
3. Run real Codex review for semantic decisions. Use the local strategy only
for smoke tests and plumbing checks.
4. Inspect generated prompts under `jobs.iteration-N/` when decisions look
wrong. Verify the expected context is actually injected.
5. Read reduction artifacts, not just aggregate totals. Applied changes,
skipped proposals, manual proposals, and consistency warnings have different
meanings.
6. Treat a converged run as a candidate, not automatic product truth. Do a
manual naming and product-semantics pass before accepting the final schema.
7. For published or handoff reports, use the `final-report` skill too.
1. Start from a real draft schema, model, migration, payload, or proposal.
Schemator reviews existing shapes; it does not invent the first draft.
2. Locate the source shape and context:
- DB / ORM: migrations, SQL DDL, Prisma/Drizzle/TypeORM/SQLAlchemy/Rails/Laravel models.
- API / contract: OpenAPI snippets, JSON Schema, GraphQL types, protobuf, API payload examples.
- App model: TypeScript interfaces/types, Zod schemas, YAML/JSON resources, Markdown proposals.
3. Write or locate project/task context before review. Good context explains the
product goal, naming conventions, borrowed vocabulary, user-facing fields,
compatibility constraints, and what must remain stable.
4. Run a real reviewer for semantic decisions. Prefer `--strategy pi` in Luke's
Pi runtime, or `--strategy codex` when you specifically want Codex. Use
`--strategy local` only for smoke tests and plumbing checks.
5. Inspect generated prompts under `jobs.iteration-N/` when decisions look wrong.
Verify the expected schema source and context are injected.
6. Read reduction artifacts, not just aggregate totals. Applied changes, skipped
proposals, manual proposals, and consistency warnings mean different things.
7. Treat convergence as a candidate schema. Do a manual naming, product-semantics,
and backwards-compatibility pass before accepting changes.
8. For published or handoff reports, use the `schemator-final-report` skill too if installed.

## Commands

End-to-end review:
Pi runtime / provider-model selected from CLI:

```bash
npm run dev -- run --source schema.md --context project-context.md --out .schemator
schemator run --strategy pi --reviewer-model claude-bridge/claude-sonnet-4-6 --source schema.md --context project-context.md --out .schemator
schemator run --strategy pi --reviewer-model openai/gpt-5.1 --reviewer-arg=--thinking --reviewer-arg off --source schema.md --out .schemator
```

Write prompts without reviewing:
Default Codex strategy:

```bash
npm run dev -- create-jobs --graph .schemator/graph.iteration-1.json --context project-context.md --out .schemator/jobs.iteration-1
schemator run --source schema.md --context project-context.md --out .schemator
```

Any command that reads the prompt from stdin and prints one field-review JSON object:

```bash
schemator review --strategy command --reviewer-command ./review-field --graph .schemator/graph.iteration-1.json --out .schemator/reviews.iteration-1
```

Generate report and graph diff:

```bash
npm run dev -- report --run .schemator --out .schemator/final-report.md
npm run dev -- diff --run .schemator --out .schemator/graph-diff.md
schemator report --run .schemator --out .schemator/final-report.md
schemator diff --run .schemator --out .schemator/graph-diff.md
```

Smoke test only:

```bash
npm run dev -- run --strategy local --source schema.md --out .schemator-smoke
schemator run --strategy local --source schema.md --out .schemator-smoke
```

Reviewer knobs:

- `--reviewer-command <path>` sets the executable for `codex`, `pi`, or `command`.
- `--reviewer-model <name>` passes a provider/model to the reviewer when supported.
- `--reviewer-timeout-ms <n>` sets the per-field timeout.
- `--reviewer-concurrency <n>` sets max concurrent external reviewers.
- `--reviewer-arg <arg>` adds extra reviewer args; use `--reviewer-arg=--flag` for flag-looking values.

## Review Rules

- Do not add local field-specific keep, rename, remove, merge, derive, or move
rules unless the user explicitly asks for that exact field outcome.
- Use project context to explain naming intent instead of hardcoding outcomes.
- Preserve intentional declarative/configuration vocabulary when the context
says it is borrowed or meaningful.
- Prefer short clear names. Do not accept longer explicit names unless they
prevent a real ambiguity.
- Use context to explain naming/product intent instead of hardcoding outcomes.
- Preserve intentional domain vocabulary when context says it is borrowed or stable.
- Prefer short clear names. Accept longer explicit names only when they prevent
a real ambiguity.
- Watch for renamed fields that became more verbose without improving the model.
- For DB/ORM sources, flag compatibility risks separately: migrations, data
backfills, API clients, analytics, imports/exports, and generated code may all
depend on field names.
- Check whether removals are missing because the run was partial, because the
field had a current use case, or because the reviewer lacked enough context.
field has a current use case, or because reviewers lacked enough context.

## Report Checklist

Expand All @@ -76,5 +97,5 @@ Before calling a Schemator result final:
- Include the project context and command lines used.
- Include applied, skipped, and manual decisions separately.
- Include the initial-vs-final graph diff.
- State manual corrections or naming overrides clearly.
- Link the pushed artifact or PR.
- State manual corrections, naming overrides, and compatibility concerns clearly.
- Link the pushed artifact or PR when relevant.
117 changes: 82 additions & 35 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join } from "node:path";
import { Command } from "commander";
import { findSkillsRoot, maybeHandleSkillflag } from "skillflag";
import { renderPatchPlan } from "./apply.js";
import { writeCodexReviews } from "./codex-review.js";
import { writeReviewerReviews, type ReviewerStrategy } from "./codex-review.js";
import { aggregateFromFiles, combineAggregates, deriveFinalGraph, runConvergence } from "./convergence.js";
import { diffGraphs, renderGraphDiff } from "./diff.js";
import { extractGraph } from "./extract/index.js";
Expand Down Expand Up @@ -61,26 +61,32 @@ program
.requiredOption("--out <dir>", "review output directory")
.option("--context <path>", "project/task context Markdown")
.option("--jobs <dir>", "also write independent field-review prompts")
.option("--strategy <name>", "review strategy", "codex")
.option("--codex-command <path>", "Codex executable for --strategy codex", "codex")
.option("--codex-model <name>", "Codex model for --strategy codex")
.option("--codex-timeout-ms <n>", "per-field Codex timeout in milliseconds", "120000")
.option("--codex-concurrency <n>", "maximum concurrent Codex reviewers", "4")
.option("--strategy <name>", "review strategy: codex, pi, command, or local", "codex")
.option("--reviewer-command <path>", "reviewer executable for codex, pi, or command strategy")
.option("--reviewer-model <name>", "provider/model or model name passed to the reviewer")
.option("--reviewer-timeout-ms <n>", "per-field reviewer timeout in milliseconds", "120000")
.option("--reviewer-concurrency <n>", "maximum concurrent external reviewers", "4")
.option("--reviewer-arg <arg>", "extra reviewer argument; repeat for multiple args", collectOption, [])
.option("--codex-command <path>", "deprecated alias for --reviewer-command with --strategy codex")
.option("--codex-model <name>", "deprecated alias for --reviewer-model with --strategy codex")
.option("--codex-timeout-ms <n>", "deprecated alias for --reviewer-timeout-ms")
.option("--codex-concurrency <n>", "deprecated alias for --reviewer-concurrency")
.action(async (options: ReviewCommandOptions) => {
await runCommand(async () => {
const graph = assertModelGraph(await readJson(resolvePath(options.graph)));
const projectContext = await readProjectContext(options.context);
if (options.jobs) {
await writeReviewJobs(graph, resolvePath(options.jobs), reviewContextOptions(projectContext));
}
const reviews = options.strategy === "codex"
? await writeCodexReviews(graph, resolvePath(options.out), {
...codexOptions(options),
const reviews = options.strategy === "local"
? await writeDeterministicReviews(graph, resolvePath(options.out), {
strategy: "local",
...reviewContextOptions(projectContext),
})
: options.strategy === "local"
? await writeDeterministicReviews(graph, resolvePath(options.out), {
strategy: "local",
: isReviewerStrategy(options.strategy)
? await writeReviewerReviews(graph, resolvePath(options.out), {
...reviewerOptions(options),
strategy: options.strategy,
...reviewContextOptions(projectContext),
})
: unsupportedStrategy(options.strategy);
Expand Down Expand Up @@ -202,11 +208,16 @@ program
.requiredOption("--out <dir>", "run output directory")
.option("--context <path>", "project/task context Markdown")
.option("--max-iterations <n>", "maximum simplification iterations", "4")
.option("--strategy <name>", "review strategy", "codex")
.option("--codex-command <path>", "Codex executable for --strategy codex", "codex")
.option("--codex-model <name>", "Codex model for --strategy codex")
.option("--codex-timeout-ms <n>", "per-field Codex timeout in milliseconds", "120000")
.option("--codex-concurrency <n>", "maximum concurrent Codex reviewers", "4")
.option("--strategy <name>", "review strategy: codex, pi, command, or local", "codex")
.option("--reviewer-command <path>", "reviewer executable for codex, pi, or command strategy")
.option("--reviewer-model <name>", "provider/model or model name passed to the reviewer")
.option("--reviewer-timeout-ms <n>", "per-field reviewer timeout in milliseconds", "120000")
.option("--reviewer-concurrency <n>", "maximum concurrent external reviewers", "4")
.option("--reviewer-arg <arg>", "extra reviewer argument; repeat for multiple args", collectOption, [])
.option("--codex-command <path>", "deprecated alias for --reviewer-command with --strategy codex")
.option("--codex-model <name>", "deprecated alias for --reviewer-model with --strategy codex")
.option("--codex-timeout-ms <n>", "deprecated alias for --reviewer-timeout-ms")
.option("--codex-concurrency <n>", "deprecated alias for --reviewer-concurrency")
.action(async (options: RunCommandOptions) => {
await runCommand(async () => {
const source = resolvePath(options.source);
Expand All @@ -216,7 +227,7 @@ program
throw new Error("--max-iterations must be a positive integer");
}
const projectContext = await readProjectContext(options.context);
if (options.strategy !== "codex" && options.strategy !== "local") {
if (options.strategy !== "local" && !isReviewerStrategy(options.strategy)) {
unsupportedStrategy(options.strategy);
}
await runConvergence({
Expand All @@ -225,7 +236,7 @@ program
maxIterations,
strategy: options.strategy,
...(projectContext === undefined ? {} : { projectContext }),
codex: codexOptions(options),
reviewer: reviewerOptions(options),
});
});
});
Expand All @@ -238,10 +249,15 @@ type ReviewCommandOptions = {
context?: string;
jobs?: string;
strategy: string;
codexCommand: string;
reviewerCommand?: string;
reviewerModel?: string;
reviewerTimeoutMs?: string;
reviewerConcurrency?: string;
reviewerArg?: string[];
codexCommand?: string;
codexModel?: string;
codexTimeoutMs: string;
codexConcurrency: string;
codexTimeoutMs?: string;
codexConcurrency?: string;
};

type RunCommandOptions = {
Expand All @@ -250,36 +266,67 @@ type RunCommandOptions = {
context?: string;
maxIterations: string;
strategy: string;
codexCommand: string;
reviewerCommand?: string;
reviewerModel?: string;
reviewerTimeoutMs?: string;
reviewerConcurrency?: string;
reviewerArg?: string[];
codexCommand?: string;
codexModel?: string;
codexTimeoutMs: string;
codexConcurrency: string;
codexTimeoutMs?: string;
codexConcurrency?: string;
};

function codexOptions(
options: Pick<ReviewCommandOptions, "codexCommand" | "codexModel" | "codexTimeoutMs" | "codexConcurrency">,
): {
command: string;
type ReviewerCommandOptions = Pick<
ReviewCommandOptions,
| "strategy"
| "reviewerCommand"
| "reviewerModel"
| "reviewerTimeoutMs"
| "reviewerConcurrency"
| "reviewerArg"
| "codexCommand"
| "codexModel"
| "codexTimeoutMs"
| "codexConcurrency"
>;

function reviewerOptions(options: ReviewerCommandOptions): {
command?: string;
args?: string[];
model?: string;
timeoutMs: number;
concurrency: number;
} {
const timeoutMs = Number.parseInt(options.codexTimeoutMs, 10);
const timeoutRaw = options.reviewerTimeoutMs ?? options.codexTimeoutMs ?? "120000";
const timeoutMs = Number.parseInt(timeoutRaw, 10);
if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
throw new Error("--codex-timeout-ms must be a positive integer");
throw new Error("--reviewer-timeout-ms must be a positive integer");
}
const concurrency = Number.parseInt(options.codexConcurrency, 10);
const concurrencyRaw = options.reviewerConcurrency ?? options.codexConcurrency ?? "4";
const concurrency = Number.parseInt(concurrencyRaw, 10);
if (!Number.isInteger(concurrency) || concurrency < 1) {
throw new Error("--codex-concurrency must be a positive integer");
throw new Error("--reviewer-concurrency must be a positive integer");
}
const command = options.reviewerCommand ?? options.codexCommand;
const model = options.reviewerModel ?? options.codexModel;
return {
command: options.codexCommand,
...(options.codexModel ? { model: options.codexModel } : {}),
...(command ? { command } : {}),
...(options.reviewerArg && options.reviewerArg.length > 0 ? { args: options.reviewerArg } : {}),
...(model ? { model } : {}),
timeoutMs,
concurrency,
};
}

function collectOption(value: string, previous: string[]): string[] {
return [...previous, value];
}

function isReviewerStrategy(strategy: string): strategy is ReviewerStrategy {
return strategy === "codex" || strategy === "pi" || strategy === "command";
}

function unsupportedStrategy(strategy: string): never {
throw new Error(`unsupported review strategy: ${strategy}`);
}
Expand Down
Loading