Skip to content
Draft
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
158 changes: 158 additions & 0 deletions .claude/skills/coding-standards/build-reviewer-agent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env node
/**
* build-reviewer-agent.mjs
*
* Compiles the coding-standards rule files into the code-inline-reviewer
* subagent definition, so the full ruleset lives in the subagent's SYSTEM
* PROMPT (a cacheable, byte-stable prefix) instead of being pulled in at
* runtime via Glob + ~28 Read tool calls (which land in the volatile message
* layer and cost orchestration turns every run).
*
* Design requirements:
* - DETERMINISTIC output: rules sorted by filename, no timestamps, no random
* ids. Same inputs => byte-identical output => stable cache prefix across
* CI runs. This is the whole point — a single non-deterministic byte breaks
* the prompt cache.
* - Emits a checksum of the inputs so cache invalidation on a rules change is
* observable (and intentional).
*
* Usage:
* node build-reviewer-agent.mjs \
* --rules <dir> (default: .claude/skills/coding-standards/rules) \
* --template <file> (default: .claude/skills/coding-standards/code-inline-reviewer.template.md) \
* --out <file> (default: .claude/agents/code-inline-reviewer.md) \
* --check (CI mode: fail if --out is stale instead of writing)
*/

import {readFileSync, writeFileSync, readdirSync, existsSync} from 'node:fs';
import {createHash} from 'node:crypto';
import path from 'node:path';

const args = parseArgs(process.argv.slice(2));
const RULES_DIR = args.rules ?? '.claude/skills/coding-standards/rules';
const TEMPLATE = args.template ?? '.claude/skills/coding-standards/code-inline-reviewer.template.md';
const OUT = args.out ?? '.claude/agents/code-inline-reviewer.md';
const CHECK = Boolean(args.check);

Check failure on line 35 in .claude/skills/coding-standards/build-reviewer-agent.mjs

View workflow job for this annotation

GitHub Actions / ESLint check

Use !! instead of Boolean()

Check failure on line 35 in .claude/skills/coding-standards/build-reviewer-agent.mjs

View workflow job for this annotation

GitHub Actions / ESLint check

Use !! instead of Boolean()
const DOCS_BASE =
'https://github.com/Expensify/App/blob/main/.claude/skills/coding-standards/rules';
const INJECT_MARKER = '<!-- BUILD:INJECT_RULES -->';

function parseArgs(argv) {
const out = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (!a.startsWith('--')) continue;

Check failure on line 44 in .claude/skills/coding-standards/build-reviewer-agent.mjs

View workflow job for this annotation

GitHub Actions / ESLint check

Expected { after 'if' condition

Check failure on line 44 in .claude/skills/coding-standards/build-reviewer-agent.mjs

View workflow job for this annotation

GitHub Actions / ESLint check

Expected { after 'if' condition
const key = a.slice(2);
const next = argv[i + 1];
if (!next || next.startsWith('--')) {
out[key] = true;
} else {
out[key] = next;
i++;
}
}
return out;
}

function parseFrontmatter(src, filename) {
const m = src.match(/^---\n([\s\S]*?)\n---\n?/);
const meta = {};
if (m) {
for (const line of m[1].split('\n')) {
const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
if (kv) meta[kv[1]] = kv[2].replace(/^["']|["']$/g, '').trim();

Check failure on line 63 in .claude/skills/coding-standards/build-reviewer-agent.mjs

View workflow job for this annotation

GitHub Actions / ESLint check

Prefer `String#replaceAll()` over `String#replace()`

Check failure on line 63 in .claude/skills/coding-standards/build-reviewer-agent.mjs

View workflow job for this annotation

GitHub Actions / ESLint check

Expected { after 'if' condition

Check failure on line 63 in .claude/skills/coding-standards/build-reviewer-agent.mjs

View workflow job for this annotation

GitHub Actions / ESLint check

Prefer `String#replaceAll()` over `String#replace()`

Check failure on line 63 in .claude/skills/coding-standards/build-reviewer-agent.mjs

View workflow job for this annotation

GitHub Actions / ESLint check

Expected { after 'if' condition
}
}
if (!meta.ruleId) {
throw new Error(`Rule file ${filename} is missing 'ruleId' in frontmatter`);
}
if (!meta.title) {
throw new Error(`Rule file ${filename} is missing 'title' in frontmatter`);
}
return meta;
}

// 1. Read rules, sorted by filename for determinism.
const ruleFiles = readdirSync(RULES_DIR)
.filter((f) => f.endsWith('.md'))
.sort();

if (ruleFiles.length === 0) {
throw new Error(`No rule files found in ${RULES_DIR}`);
}

const rules = ruleFiles.map((filename) => {
const src = readFileSync(path.join(RULES_DIR, filename), 'utf8');
const meta = parseFrontmatter(src, filename);
return {
filename,
slug: filename.replace(/\.md$/, ''),
ruleId: meta.ruleId,
title: meta.title,
body: src.trimEnd(),
};
});

// 2. Build the deterministic ruleId -> docs-link index table.
const indexRows = rules
.map((r) => `| \`${r.ruleId}\` | ${r.title} | [${r.slug}](${DOCS_BASE}/${r.slug}.md) |`)
.join('\n');

// 3. Concatenate full rule bodies with stable separators.
const ruleBlocks = rules
.map((r) => `<!-- rule:${r.ruleId} file:${r.filename} -->\n${r.body}`)
.join('\n\n---\n\n');

// 4. Checksum the *inputs* (filenames + contents) so a rules change is a
// visible, intentional cache-buster — and nothing else moves the bytes.
const checksum = createHash('sha256')
.update(rules.map((r) => `${r.filename}\n${r.body}`).join('\n\n'))
.digest('hex')
.slice(0, 16);

const injected = [
`## Coding Standards (embedded — generated by build-reviewer-agent.mjs)`,
``,
`<coding-standards rule-count="${rules.length}" checksum="${checksum}">`,
``,
`### Rule index (ruleId → title → docs link)`,
``,
`| ruleId | title | docs |`,
`| --- | --- | --- |`,
indexRows,
``,
`### Full rules`,
``,
ruleBlocks,
``,
`</coding-standards>`,
].join('\n');

// 5. Inject into the template at the marker.
const template = readFileSync(TEMPLATE, 'utf8');
if (!template.includes(INJECT_MARKER)) {
throw new Error(`Template ${TEMPLATE} is missing marker ${INJECT_MARKER}`);
}
const output = template.replace(INJECT_MARKER, injected) + '\n';

Check failure on line 136 in .claude/skills/coding-standards/build-reviewer-agent.mjs

View workflow job for this annotation

GitHub Actions / ESLint check

Unexpected string concatenation

Check failure on line 136 in .claude/skills/coding-standards/build-reviewer-agent.mjs

View workflow job for this annotation

GitHub Actions / ESLint check

Unexpected string concatenation

const approxTokens = Math.round(output.length / 4);

// 6. Write or check.
if (CHECK) {
const current = existsSync(OUT) ? readFileSync(OUT, 'utf8') : '';
if (current !== output) {
console.error(
`[build-reviewer-agent] ${OUT} is STALE. Run: node build-reviewer-agent.mjs`,
);
process.exit(1);
}
console.error(`[build-reviewer-agent] ${OUT} is up to date (checksum ${checksum}).`);
} else {
writeFileSync(OUT, output);
console.error(
`[build-reviewer-agent] wrote ${OUT}\n` +
` rules: ${rules.length}\n` +
` checksum: ${checksum}\n` +
` bytes: ${output.length} (~${approxTokens} tokens)`,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ Your job is to scan through changed files and create **inline comments** for spe

## Rules

Coding standards are defined as individual rule files in `.claude/skills/coding-standards/rules/`.
The **complete coding-standards ruleset is embedded in this prompt** under the
`<coding-standards>` section below. You do NOT need to Glob or Read any rule
files — every rule, with its reasoning, examples, and Review Metadata (search
patterns, "DO NOT flag" exceptions), is already in your context.

**Always use the `coding-standards` skill to review changed files.**

Each rule file contains:
Each embedded rule contains:

- **YAML frontmatter**: `ruleId`, `title`
- **Reasoning**: Technical explanation of why the rule is important
Expand All @@ -27,11 +28,10 @@ Each rule file contains:

## Instructions

1. **Load all rules:**
- Use Glob to list all `.md` files in `.claude/skills/coding-standards/rules/`
- Read ALL rule files
- Build an explicit checklist of all rules (ruleId + title) from the YAML frontmatter
- Build a ruleId-to-filename mapping for creating docs links in comments
1. **Build your rule checklist from the embedded `<coding-standards>` section.**
- The checklist (ruleId + title) and the ruleId-to-docs-link mapping are
pre-built for you in the `### Rule index` table at the top of that section.
- Do NOT spend turns listing or reading rule files — they are already inlined.
2. **Get the list of changed files and their diffs:**
- Use `gh pr diff` to see what actually changed in the PR
- Focus ONLY on the changed lines, not the entire file
Expand Down Expand Up @@ -68,3 +68,5 @@ Use this format for the `body` field of each violation:

<Suggested, specific fix preferably with a code snippet>
```

<!-- BUILD:INJECT_RULES -->
12 changes: 12 additions & 0 deletions .github/workflows/claude-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ jobs:
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true'
uses: ./.github/actions/composite/setupNode

# Compile the coding-standards ruleset into the reviewer subagent's
# system prompt. Deterministic output (sorted, no timestamps) => the
# cached prompt prefix stays byte-stable across runs. Generated on the
# fly and gitignored, so the ruleset is never duplicated in source.
- name: Generate reviewer agent
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true'
run: node .claude/skills/coding-standards/build-reviewer-agent.mjs

- name: Filter paths
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true'
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
Expand Down Expand Up @@ -90,6 +98,10 @@ jobs:
claude_args: |
--model claude-opus-4-6
--allowedTools "Task,Glob,Grep,Read,Bash(gh pr diff:*),Bash(gh pr view:*),Bash(check-compiler.sh:*)" --json-schema '${{ steps.toolkit.outputs.schema_json }}'
--exclude-dynamic-system-prompt-sections
# Optional, measure-first: 1h cache TTL. Only net-positive if PR cadence
# keeps reviews inside the 1h window; on cold runs it costs 2x write vs 1.25x.
# settings: '{"env":{"ENABLE_PROMPT_CACHING_1H":"1"}}'

- name: Post code review results
if: |
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,6 @@ server/victory-chart-renderer/.dev/

# Bun lockfiles (VCR uses npm workspaces + package-lock.json; bun is runtime-only)
bun.lock

# Generated by .claude/skills/coding-standards/build-reviewer-agent.mjs (compiled on the fly in CI)
.claude/agents/code-inline-reviewer.md
Loading