Working with agents
Give coding agents Rogen's rules, and check the files they change
Updated
A coding agent doesn't know that Rogen generates the Rojo project file, so Rojo and the editor can miss the files it adds. A short block of rules in AGENTS.md tells it to check its changes.
Add the Rules
rogen init adds the block below to AGENTS.md, and imports it into the CLAUDE.md and GEMINI.md that Claude Code and Gemini CLI read instead. --no-agent-rules leaves it out. In a project that already has a config, run rogen init --agent-rules, which adds only the agent files and changes no config, or paste the block yourself, markers included:
<!-- rogen -->
## Rogen
This repo uses Rogen. Each `*.rogen.json` generates the Rojo project file beside it (`game.rogen.json` → `game.project.json`) from folder and file names, so a file's path on disk is not its path in the game, and adding, moving or deleting a file can leave the project file out of date.
After you add, move, rename or delete files, or edit a `*.rogen.json`, run `rogen check <paths>` from the repo root. Fix each line it prints, or tell the user why it stays. When it says to run `rogen write`, run it.
### Placing code
- Name new files like their neighbours: same folders, `@` suffixes and casing. Run `rogen where <path>` to see where a file lands and how to require it.
- A folder or `@` suffix named after a key in `routes` routes its contents: `Shop/Server/`, `Save@Server.luau`.
- The outermost route wins, so `Shared/Server/Save.luau` ships to clients. Keep server-only code under a server route.
- In roblox-ts, import by relative path.
### Changing the setup
- Edit the `*.rogen.json`, never the generated `*.project.json`.
- Put what names can't place, such as packages, in the `tree`; Rogen adds the routed files to it.
- Configs share nothing: apply a change to `routes`, `sourcePaths` or `tree` to every `*.rogen.json` it concerns.
- Read `rogen doc routing` before adding a route or a new naming pattern.
### Commands
- In roblox-ts, check `projects/common` through a place that uses it: `rogen check projects/game`.
- Run other commands in the folder of the `*.rogen.json`, or name it: `rogen write projects/game`.
- To sync to Studio, run `rogen serve --json` in the background and leave it running. `rogen help serve` explains its output.
- `rogen doc <topic>` covers `routing`, `config`, `layout` and `output`; `rogen help <command> --json` lists a command's flags and JSON output.
<!-- /rogen -->Keep the Rules Current
rogen write and rogen serve replace an outdated block with the installed version's, and keep your text around it. Pin one Rogen version for the team, as Installation does, so everyone writes the same block. To stop the updates, delete the marker lines to keep the rules as your own, or delete the block.
Check Changed Files Automatically
An agent can forget to run rogen check. This hook runs it on the files changed in git when the agent ends its turn, and sends each new warning back as the agent's next prompt.
// Agent Stop hook: report new Rogen diagnostics about changed files before the agent ends its turn.
// Pass "cursor" as the first argument for Cursor, which replies with followup_message.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
const git = (...args) => {
const run = spawnSync("git", args, { encoding: "utf8" });
return run.status === 0 ? run.stdout : "";
};
let payload = {};
try {
payload = JSON.parse(fs.readFileSync(0, "utf8"));
} catch {}
const session = String(
payload.session_id ??
payload.sessionId ??
payload.conversation_id ??
"agent",
).replace(/[^\w.-]/g, "_");
try {
process.chdir(process.env.CLAUDE_PROJECT_DIR || payload.cwd || ".");
} catch {
process.exit(0);
}
const changed = [
...git(
"diff",
"-z",
"--name-only",
"--relative",
"--diff-filter=d",
"HEAD",
).split("\0"),
...git("ls-files", "-z", "--others", "--exclude-standard").split("\0"),
].filter(Boolean);
if (!changed.length) process.exit(0);
const seenFile = path.resolve(
git("rev-parse", "--git-dir").trim(),
`rogen-hook-${session}`,
);
const seen = fs.existsSync(seenFile)
? fs.readFileSync(seenFile, "utf8").split("\n")
: [];
const windows = process.platform === "win32";
const names = windows ? ["rogen.cmd", "rogen.exe"] : ["rogen"];
const locate = (dirs) =>
dirs
.flatMap((dir) => names.map((name) => path.join(dir, name)))
.find((file) => fs.existsSync(file));
const rogen = locate([
path.resolve("node_modules/.bin"),
path.join(git("rev-parse", "--show-toplevel").trim(), "node_modules/.bin"),
...(process.env.PATH ?? "").split(path.delimiter).filter(Boolean),
]);
if (!rogen) {
const NONE = "# found no rogen";
if (!seen.includes(NONE)) {
console.error(
"Rogen's hook found no rogen; install it in the project or globally",
);
fs.appendFileSync(seenFile, `${NONE}\n`);
}
process.exit(0);
}
const run = (command, args) =>
windows && command.endsWith(".cmd")
? spawnSync([command, ...args].map((part) => `"${part}"`).join(" "), {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
shell: true,
})
: spawnSync(command, args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
const check = run(rogen, ["check", ...changed]);
const report = (check.stdout ?? "")
.split(/\r?\n/)
.filter((line) => line && !seen.includes(line));
if (!report.length) process.exit(0);
const message = `Rogen warns about files you changed. Fix each, or tell the user why it stays:\n${report.join("\n")}`;
fs.appendFileSync(seenFile, `${report.join("\n")}\n`);
console.log(
JSON.stringify(
process.argv[2] === "cursor"
? { followup_message: message }
: { decision: "block", reason: message },
),
);rogen init adds the hook only when asked: rogen init --agent-hook, or Customise in a terminal. It writes the script and registers it with each agent it finds:
| Agent | Hook file | Event | Entry |
|---|---|---|---|
| Claude Code | .claude/settings.json | Stop | node "$CLAUDE_PROJECT_DIR"/.agents/hooks/rogen-check.mjs |
| Codex | .codex/hooks.json | Stop | node "$(git rev-parse --show-toplevel)"/.agents/hooks/rogen-check.mjs |
| Gemini CLI | .gemini/settings.json | AfterAgent | node "$GEMINI_PROJECT_DIR"/.agents/hooks/rogen-check.mjs |
| Cursor | .cursor/hooks.json | stop | node .agents/hooks/rogen-check.mjs cursor |
| Copilot | .github/hooks/rogen.json | agentStop | node "$(git rev-parse --show-toplevel)"/.agents/hooks/rogen-check.mjs |
To register it by hand, put the script at .agents/hooks/rogen-check.mjs and add the entry under its event. Codex runs a project hook only after you trust it in /hooks. The hook also reports warnings in files you changed yourself, once per session.
In a project that already has a config, --agent-rules and --agent-hook add only the agent files and change no config. They take --skip-git, -y, -n and --json, but a flag that shapes a config, such as --language, is an error.
Install the Skill
The rogen skill sets a repo up through rogen init, and can reorganise a Rojo layout into feature folders:
npx skills add LDGerrits/rogenThe skill runs rogen init --dry-run --json and reads its decided list, each choice init made with the flag that changes it. {"question": "sample", "value": true, "flag": "--no-sample"} means init writes the Greeter sample, and --no-sample leaves it out. It also reads warnings, such as code that no source path holds, and settles each one with you.
The rules stay in AGENTS.md because an agent loads a skill only when a task looks like it needs one, and adding a Luau file doesn't.