Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ reviews/
evals/fixtures/logs/
node_modules/
dist/
bundled-grammars/
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,4 @@ models-list: build
pnpm run models-list

clean:
rm -rf dist/
rm -rf dist/ bundled-grammars/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.base.sha }} # trusted base; PR head is fetched as review data
fetch-depth: 0
- uses: 0xPolygon/codegenie@v0.5.0
- uses: 0xPolygon/codegenie@v0.5.1
with:
model: "anthropic/claude-opus-5:high"
llm-api-key: ${{ secrets.LLM_API_KEY }}
Expand Down
2 changes: 1 addition & 1 deletion examples/workflows/codegenie-review-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
with:
fetch-depth: 0

- uses: 0xPolygon/codegenie@v0.5.0
- uses: 0xPolygon/codegenie@v0.5.1
with:
model: "anthropic/claude-opus-5:high"
llm-api-key: ${{ secrets.LLM_API_KEY }}
Expand Down
2 changes: 1 addition & 1 deletion examples/workflows/codegenie-review-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
ref: ${{ github.event.pull_request.base.sha }}
fetch-depth: 0

- uses: 0xPolygon/codegenie@v0.5.0
- uses: 0xPolygon/codegenie@v0.5.1
with:
model: "anthropic/claude-opus-5:high"
llm-api-key: ${{ secrets.LLM_API_KEY }}
Expand Down
17 changes: 9 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@0xsequence/codegenie",
"version": "0.5.0",
"version": "0.5.1",
"description": "High-signal AI code review agent",
"type": "module",
"bin": {
Expand All @@ -9,10 +9,11 @@
"files": [
"dist",
"bundled-skills",
"bundled-grammars",
"models.md"
],
"scripts": {
"build": "tsc -p tsconfig.build.json && node scripts/write-version.mjs",
"build": "tsc -p tsconfig.build.json && node scripts/write-version.mjs && node scripts/copy-grammars.mjs",
"typecheck": "tsc -p tsconfig.json --noEmit",
"check:workflows": "bash scripts/check-workflows.sh",
"check": "pnpm run typecheck && pnpm run check:workflows",
Expand All @@ -32,19 +33,19 @@
"p-limit": "7.3.1",
"picomatch": "4.0.5",
"smol-toml": "1.7.0",
"tree-sitter-go": "0.25.0",
"tree-sitter-javascript": "0.25.0",
"tree-sitter-python": "0.25.0",
"tree-sitter-rust": "0.24.0",
"tree-sitter-solidity": "1.2.13",
"tree-sitter-typescript": "0.23.2",
"web-tree-sitter": "0.26.11",
"yaml": "2.9.0",
"zod": "4.4.3"
},
"devDependencies": {
"@types/node": "26.1.1",
"@types/picomatch": "4.0.3",
"tree-sitter-go": "0.25.0",
"tree-sitter-javascript": "0.25.0",
"tree-sitter-python": "0.25.0",
"tree-sitter-rust": "0.24.0",
"tree-sitter-solidity": "1.2.13",
"tree-sitter-typescript": "0.23.2",
"tsx": "4.23.1",
"typescript": "7.0.2",
"vitest": "4.1.10"
Expand Down
32 changes: 16 additions & 16 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ minimumReleaseAgeExclude:
- '@earendil-works/pi-ai'

# tree-sitter-solidity mis-spells its optional-peer metadata key as
# `tree_sitter`. codegenie resolves only its published WASM through
# web-tree-sitter, so installing the native tree-sitter peer is unnecessary.
# `tree_sitter`, so npm reads `tree-sitter` as a *required* peer, installs the
# native package, and compiles it from source — which fails against Node >= 23
# V8 headers. codegenie resolves only published WASM through web-tree-sitter, so
# the grammar packages are devDependencies whose WASM the build vendors into
# bundled-grammars/ (scripts/copy-grammars.mjs). An installed codegenie has no
# native dependency; these rules keep contributor installs equally native-free.
peerDependencyRules:
ignoreMissing:
- tree-sitter
Expand Down
31 changes: 31 additions & 0 deletions scripts/copy-grammars.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { copyFileSync, mkdirSync, rmSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { GRAMMAR_WASM } from "../dist/repo/tree-sitter/tree-sitter-service.js";

// codegenie parses through web-tree-sitter, so the only thing it needs from the
// tree-sitter-* grammar packages is their published WASM. Every one of those
// packages is a native-build package (`install: node-gyp-build`), and
// tree-sitter-solidity additionally misspells its optional-peer key as
// `tree_sitter` — which makes npm treat native tree-sitter as a *required* peer,
// install it, and compile it from source. Copying the WASM at build time keeps
// the grammar packages as devDependencies, so an installed codegenie has no
// native dependency and no install script for a consumer to run.
const require = createRequire(import.meta.url);
const root = process.cwd();
const outputDirectory = path.join(root, "bundled-grammars");

rmSync(outputDirectory, { recursive: true, force: true });
mkdirSync(outputDirectory, { recursive: true });

const copied = [];
for (const { package: packageName, file } of Object.values(GRAMMAR_WASM)) {
const destination = path.join(outputDirectory, file);
if (copied.includes(file)) {
continue;
}
copyFileSync(require.resolve(`${packageName}/${file}`), destination);
copied.push(file);
}

console.log(`Bundled ${copied.length} grammars: ${copied.join(", ")}`);
36 changes: 27 additions & 9 deletions src/repo/tree-sitter/tree-sitter-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { ParseInput, ParsedFile } from "../../types.js";
import type { TelemetryRecorder } from "../../telemetry/telemetry-recorder.js";
import { sha256Hex } from "../../util/hashing.js";
Expand Down Expand Up @@ -40,16 +43,31 @@ const MAX_PARSE_BYTES = 1_500_000;
const PARSE_TIMEOUT_MS = 1_000;
const MAX_CACHE_ENTRIES = 128;

const GRAMMAR_WASM: Record<GrammarId, string> = {
go: "tree-sitter-go/tree-sitter-go.wasm",
typescript: "tree-sitter-typescript/tree-sitter-typescript.wasm",
tsx: "tree-sitter-typescript/tree-sitter-tsx.wasm",
javascript: "tree-sitter-javascript/tree-sitter-javascript.wasm",
rust: "tree-sitter-rust/tree-sitter-rust.wasm",
python: "tree-sitter-python/tree-sitter-python.wasm",
solidity: "tree-sitter-solidity/tree-sitter-solidity.wasm"
// The grammar packages are devDependencies: they are native-build packages, and
// tree-sitter-solidity misspells its optional-peer key as `tree_sitter`, so npm
// treats native tree-sitter as a required peer and compiles it from source. The
// build copies their WASM into bundled-grammars/ (scripts/copy-grammars.mjs),
// which ships in the package, leaving an installed codegenie native-free.
export const GRAMMAR_WASM: Record<GrammarId, { package: string; file: string }> = {
go: { package: "tree-sitter-go", file: "tree-sitter-go.wasm" },
typescript: { package: "tree-sitter-typescript", file: "tree-sitter-typescript.wasm" },
tsx: { package: "tree-sitter-typescript", file: "tree-sitter-tsx.wasm" },
javascript: { package: "tree-sitter-javascript", file: "tree-sitter-javascript.wasm" },
rust: { package: "tree-sitter-rust", file: "tree-sitter-rust.wasm" },
python: { package: "tree-sitter-python", file: "tree-sitter-python.wasm" },
solidity: { package: "tree-sitter-solidity", file: "tree-sitter-solidity.wasm" }
};

const BUNDLED_GRAMMAR_DIRECTORY = fileURLToPath(new URL("../../../bundled-grammars/", import.meta.url));

// Prefers the bundled copy an installed package ships; falls back to the
// devDependency so a fresh checkout parses before its first build.
function resolveBundledGrammarWasm(grammarId: GrammarId): string {
const { package: packageName, file } = GRAMMAR_WASM[grammarId];
const bundled = path.join(BUNDLED_GRAMMAR_DIRECTORY, file);
return existsSync(bundled) ? bundled : require.resolve(`${packageName}/${file}`);
}

export class TreeSitterService {
private readonly telemetry: TelemetryRecorder | undefined;
private initPromise: Promise<void> | undefined;
Expand Down Expand Up @@ -178,7 +196,7 @@ export class TreeSitterService {
private async loadLanguageUncached(grammarId: GrammarId): Promise<Language | undefined> {
try {
await this.init();
const wasmPath = this.opts.resolveGrammarWasm?.(grammarId) ?? require.resolve(GRAMMAR_WASM[grammarId]);
const wasmPath = this.opts.resolveGrammarWasm?.(grammarId) ?? resolveBundledGrammarWasm(grammarId);
return await (this.opts.loadLanguage?.(wasmPath) ?? Language.load(wasmPath));
} catch (error) {
this.unavailable.add(grammarId);
Expand Down
19 changes: 19 additions & 0 deletions tests/github-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,25 @@ describe("GitHub Action and workflow contracts", () => {
expect(commentReviewStep?.with?.["trigger-phrase"]).toBe("codegenie review");
});

// The action installs the npm version read from its own package.json, so a
// documented tag that lags the package ships a codegenie nobody released.
it("pins every documented action reference to the current package version", () => {
const { version } = JSON.parse(readFileSync("package.json", "utf8")) as { version: string };
// Only real step references — the dogfood workflow mentions the tag in prose
// to explain why it builds from source instead.
const documented = [...workflowPaths, "README.md"]
.flatMap((filePath) => readFileSync(path.resolve(filePath), "utf8")
.split("\n")
.map((line) => line.trim())
.filter((line) => /^-?\s*uses:\s*0xPolygon\/codegenie@/u.test(line))
.map((line) => [filePath, line] as const));

expect(documented.length).toBeGreaterThan(0);
for (const [filePath, line] of documented) {
expect(line, filePath).toContain(`0xPolygon/codegenie@v${version}`);
}
});

it("pins pull-request jobs to the base SHA and leaves comment jobs on the default branch", () => {
const dogfood = parseYaml(readFileSync(path.resolve(workflowPaths[0] ?? ""), "utf8")) as WorkflowDocument;
const prExample = parseYaml(readFileSync(path.resolve(workflowPaths[2] ?? ""), "utf8")) as WorkflowDocument;
Expand Down
Loading
Loading