From f0b34e4129812e37af24bf468dd3ef6538d9eca9 Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 7 Apr 2026 15:25:10 -0400 Subject: [PATCH 1/3] fix(cli): use bun ./script path to fix module resolution in CI --- .../workflows/check-opencode-annotations.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/check-opencode-annotations.yml diff --git a/.github/workflows/check-opencode-annotations.yml b/.github/workflows/check-opencode-annotations.yml new file mode 100644 index 00000000000..625eea153b3 --- /dev/null +++ b/.github/workflows/check-opencode-annotations.yml @@ -0,0 +1,32 @@ +name: Check opencode annotations + +on: + pull_request: + paths: + - "packages/opencode/**" + - "script/check-opencode-annotations.ts" + - ".github/workflows/check-opencode-annotations.yml" + workflow_dispatch: + +jobs: + check-annotations: + name: Check kilocode_change annotations + if: github.repository == 'Kilo-Org/kilocode' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 + + - uses: oven-sh/setup-bun@v2 + + - name: Check kilocode_change annotations in shared opencode files + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + if [ -n "$BASE_SHA" ]; then + bun ./script/check-opencode-annotations.ts --base "$BASE_SHA" + else + echo "No PR base SHA available (workflow_dispatch without PR context) — skipping." + fi From 2828b7e0002565b63874c97534cad28a7d64c0c3 Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 7 Apr 2026 15:39:04 -0400 Subject: [PATCH 2/3] feat(cli): add annotation check script and AGENTS.md docs --- AGENTS.md | 1 + script/check-opencode-annotations.ts | 168 +++++++++++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 script/check-opencode-annotations.ts diff --git a/AGENTS.md b/AGENTS.md index a8323860822..1d4c3f77eb2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang - **Knip** (unused exports): `bun run knip` from `packages/kilo-vscode/`. CI runs this — all exported types/functions must be imported somewhere. Remove or unexport unused exports before pushing. - **Source links**: After adding or changing URLs in `packages/kilo-vscode/`, `packages/kilo-vscode/webview-ui/`, or `packages/opencode/src/`, run `bun run script/extract-source-links.ts` from the repo root and commit the updated `packages/kilo-docs/source-links.md`. CI runs this check — the build fails if the file is stale. - **kilocode_change check**: `bun run check-kilocode-change` from `packages/kilo-vscode/`. CI runs this — `kilocode_change` is a marker for upstream merge conflicts and must not appear in `packages/kilo-vscode/` or `packages/kilo-ui/` (these are entirely Kilo Code additions). Remove the markers before pushing. +- **opencode annotation check**: `bun run script/check-opencode-annotations.ts` from repo root. CI runs this on PRs touching `packages/opencode/` — every Kilo-specific change in shared opencode files must be annotated with `kilocode_change` markers. Exempt paths (no markers needed): `packages/opencode/src/kilocode/`, `packages/opencode/test/kilocode/`, and any path containing `kilocode` in the name. ## Products diff --git a/script/check-opencode-annotations.ts b/script/check-opencode-annotations.ts new file mode 100644 index 00000000000..314b399d460 --- /dev/null +++ b/script/check-opencode-annotations.ts @@ -0,0 +1,168 @@ +#!/usr/bin/env bun + +/** + * Verifies that every Kilo-specific change in shared packages/opencode/ files + * is annotated with a kilocode_change marker. + * + * Usage: + * bun run script/check-opencode-annotations.ts # diff against origin/main + * bun run script/check-opencode-annotations.ts --base # diff against + * + * A line is "covered" if it: + * - contains // kilocode_change (inline annotation) + * - falls inside a // kilocode_change start/end block (block annotation) + * - is in a file whose first non-empty line is (whole-file annotation) + * // kilocode_change - new file + * - is empty / whitespace-only (skipped) + * - is itself a marker line (auto-covered) + * + * Exempt paths (no markers needed — entirely Kilo-specific): + * - packages/opencode/src/kilocode/** + * - packages/opencode/test/kilocode/** + * - Any path containing "kilocode" in directory or filename + */ + +import { spawnSync } from "node:child_process" +import { readFileSync } from "node:fs" +import path from "node:path" + +const ROOT = path.resolve(import.meta.dir, "..") +const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx"]) + +const args = process.argv.slice(2) +const baseIdx = args.indexOf("--base") +const base = baseIdx !== -1 ? args[baseIdx + 1] : "origin/main" + +function run(cmd: string, args: string[]) { + const result = spawnSync(cmd, args, { cwd: ROOT, encoding: "utf8" }) + return result.stdout?.trim() ?? "" +} + +function changedFiles() { + const out = run("git", ["diff", "--name-only", "--diff-filter=AMRT", `${base}...HEAD`, "--", "packages/opencode"]) + return out ? out.split("\n").filter(Boolean) : [] +} + +function isExempt(file: string) { + const norm = file.replaceAll("\\", "/").toLowerCase() + return norm.split("/").some((part) => part.includes("kilocode")) +} + +function isSource(file: string) { + return SOURCE_EXTS.has(path.extname(file)) +} + +function addedLines(file: string): Set { + const diff = run("git", ["diff", "--unified=0", "--diff-filter=AMRT", `${base}...HEAD`, "--", file]) + const out = new Set() + for (const line of diff.split("\n")) { + const m = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/) + if (!m) continue + const start = Number(m[1]) + const count = m[2] !== undefined ? Number(m[2]) : 1 + for (let i = 0; i < count; i++) out.add(start + i) + } + return out +} + +function coveredLines(text: string): { lines: string[]; covered: Set } { + const lines = text.split(/\r?\n/) + const covered = new Set() + + // Whole-file annotation: first non-empty line is "// kilocode_change - new file" + const first = lines.find((x) => x.trim() !== "") + if (first?.match(/\/\/\s*kilocode_change\s*-\s*new\s*file\b/)) { + for (let i = 1; i <= lines.length; i++) covered.add(i) + return { lines, covered } + } + + let block = false + for (let i = 0; i < lines.length; i++) { + const n = i + 1 + const line = lines[i] ?? "" + + if (line.match(/\/\/\s*kilocode_change\s+start\b/)) { + block = true + covered.add(n) + continue + } + + if (line.match(/\/\/\s*kilocode_change\s+end\b/)) { + covered.add(n) + block = false + continue + } + + if (block) { + covered.add(n) + continue + } + + if (line.match(/\/\/\s*kilocode_change\b/)) covered.add(n) + } + + return { lines, covered } +} + +// --- main --- + +const files = changedFiles().filter((f) => !isExempt(f) && isSource(f)) + +if (files.length === 0) { + console.log("No shared opencode source files changed — nothing to check.") + process.exit(0) +} + +const violations: string[] = [] + +for (const file of files) { + const nums = addedLines(file) + if (nums.size === 0) continue + + const abs = path.join(ROOT, file) + const text = readFileSync(abs, "utf8") + const { lines, covered } = coveredLines(text) + + for (const n of nums) { + const line = lines[n - 1] ?? "" + const trim = line.trim() + if (!trim) continue + if (trim.match(/\/\/\s*kilocode_change\b/)) continue + if (!covered.has(n)) violations.push(` ${file}:${n}: ${trim}`) + } +} + +if (violations.length === 0) { + console.log("All shared opencode changes are annotated with kilocode_change markers.") + process.exit(0) +} + +console.error( + [ + "Unannotated Kilo changes found in shared opencode files:", + "", + ...violations, + "", + "Every Kilo-specific change in packages/opencode/ must be annotated.", + "", + "Inline (single line):", + " const url = Flag.KILO_MODELS_URL || 'https://models.dev' // kilocode_change", + "", + "Block (multiple lines):", + " // kilocode_change start", + " ...", + " // kilocode_change end", + "", + "New file:", + " // kilocode_change - new file", + "", + "Exempt paths (no markers needed):", + " - packages/opencode/src/kilocode/**", + " - packages/opencode/test/kilocode/**", + " - Any path containing 'kilocode' in the directory or filename", + "", + "See AGENTS.md for details.", + ].join("\n"), +) + +process.exit(1) From 2371b7478434c3bc435e1acee40c7b9db7c246bf Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 7 Apr 2026 15:41:29 -0400 Subject: [PATCH 3/3] revert: restore bun run script/ invocation (consistent with source-check-links) --- .github/workflows/check-opencode-annotations.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-opencode-annotations.yml b/.github/workflows/check-opencode-annotations.yml index 625eea153b3..b2fc705daea 100644 --- a/.github/workflows/check-opencode-annotations.yml +++ b/.github/workflows/check-opencode-annotations.yml @@ -26,7 +26,7 @@ jobs: BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | if [ -n "$BASE_SHA" ]; then - bun ./script/check-opencode-annotations.ts --base "$BASE_SHA" + bun run script/check-opencode-annotations.ts --base "$BASE_SHA" else echo "No PR base SHA available (workflow_dispatch without PR context) — skipping." fi