From bcb47be3b0cf71990fd3ee1ec562a716aefe3571 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Tue, 5 May 2026 16:38:10 +0300 Subject: [PATCH 1/8] fix(cli): preserve variant after compact --- .changeset/compact-thinking-level.md | 6 ++++++ .../src/cli/cmd/tui/component/prompt/index.tsx | 2 ++ .../opencode/src/kilocode/session/tui-sync.ts | 6 ++++++ packages/opencode/test/kilocode/tui-sync.test.ts | 16 ++++++++++++++++ 4 files changed, 30 insertions(+) create mode 100644 .changeset/compact-thinking-level.md create mode 100644 packages/opencode/src/kilocode/session/tui-sync.ts create mode 100644 packages/opencode/test/kilocode/tui-sync.test.ts diff --git a/.changeset/compact-thinking-level.md b/.changeset/compact-thinking-level.md new file mode 100644 index 00000000000..a00bd74f635 --- /dev/null +++ b/.changeset/compact-thinking-level.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Preserve the selected thinking level after compacting a session. diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 8e259ec56b2..fdf661870b9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -43,6 +43,7 @@ import { DialogSkill } from "../dialog-skill" import { DialogWorkspaceCreate, restoreWorkspaceSession } from "../dialog-workspace-create" import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable" import { useArgs } from "@tui/context/args" +import { KiloSessionTuiSync } from "@/kilocode/session/tui-sync" // kilocode_change export type PromptProps = { sessionID?: string @@ -236,6 +237,7 @@ export function Prompt(props: PromptProps) { const sessionID = props.sessionID const msg = lastUserMessage() if (!sessionID || !msg) return + if (!KiloSessionTuiSync.model(msg)) return // kilocode_change const key = [sessionID, msg.id].join(":") if (key === syncedKey) return diff --git a/packages/opencode/src/kilocode/session/tui-sync.ts b/packages/opencode/src/kilocode/session/tui-sync.ts new file mode 100644 index 00000000000..2f3375677ff --- /dev/null +++ b/packages/opencode/src/kilocode/session/tui-sync.ts @@ -0,0 +1,6 @@ +export namespace KiloSessionTuiSync { + export function model(input: { role: string; parts?: readonly { type: string }[] }) { + if (input.role !== "user") return false + return !input.parts?.some((part) => part.type === "compaction") + } +} diff --git a/packages/opencode/test/kilocode/tui-sync.test.ts b/packages/opencode/test/kilocode/tui-sync.test.ts new file mode 100644 index 00000000000..83fb4e098cd --- /dev/null +++ b/packages/opencode/test/kilocode/tui-sync.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "bun:test" +import { KiloSessionTuiSync } from "../../src/kilocode/session/tui-sync" + +describe("KiloSessionTuiSync.model", () => { + test("syncs normal user messages", () => { + expect(KiloSessionTuiSync.model({ role: "user", parts: [{ type: "text" }] })).toBe(true) + }) + + test("skips compaction marker user messages", () => { + expect(KiloSessionTuiSync.model({ role: "user", parts: [{ type: "compaction" }] })).toBe(false) + }) + + test("skips non-user messages", () => { + expect(KiloSessionTuiSync.model({ role: "assistant", parts: [{ type: "text" }] })).toBe(false) + }) +}) From 1db15a686d82d1abeb399c1a9438f6a5ea791118 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Tue, 5 May 2026 16:47:16 +0300 Subject: [PATCH 2/8] fix(cli): skip compact sync --- .../cli/cmd/tui/component/prompt/index.tsx | 3 +- .../opencode/test/kilocode/tui-sync.test.ts | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index fdf661870b9..08735632814 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -237,7 +237,8 @@ export function Prompt(props: PromptProps) { const sessionID = props.sessionID const msg = lastUserMessage() if (!sessionID || !msg) return - if (!KiloSessionTuiSync.model(msg)) return // kilocode_change + const parts = sync.data.part[msg.id] ?? [] // kilocode_change + if (!KiloSessionTuiSync.model({ role: msg.role, parts })) return // kilocode_change const key = [sessionID, msg.id].join(":") if (key === syncedKey) return diff --git a/packages/opencode/test/kilocode/tui-sync.test.ts b/packages/opencode/test/kilocode/tui-sync.test.ts index 83fb4e098cd..a28c6d041a0 100644 --- a/packages/opencode/test/kilocode/tui-sync.test.ts +++ b/packages/opencode/test/kilocode/tui-sync.test.ts @@ -1,6 +1,17 @@ import { describe, expect, test } from "bun:test" import { KiloSessionTuiSync } from "../../src/kilocode/session/tui-sync" +type Message = { + role: string + model?: { providerID: string; modelID: string; variant?: string } + parts?: readonly { type: string }[] +} + +function syncVariant(input: { current: string | undefined; message: Message }) { + if (!KiloSessionTuiSync.model(input.message)) return input.current + return input.message.model?.variant ?? "default" +} + describe("KiloSessionTuiSync.model", () => { test("syncs normal user messages", () => { expect(KiloSessionTuiSync.model({ role: "user", parts: [{ type: "text" }] })).toBe(true) @@ -10,7 +21,34 @@ describe("KiloSessionTuiSync.model", () => { expect(KiloSessionTuiSync.model({ role: "user", parts: [{ type: "compaction" }] })).toBe(false) }) + test("skips messages checked with stored parts", () => { + const msg = { role: "user" } + const parts = [{ type: "compaction" }] + + expect(KiloSessionTuiSync.model({ role: msg.role, parts })).toBe(false) + }) + test("skips non-user messages", () => { expect(KiloSessionTuiSync.model({ role: "assistant", parts: [{ type: "text" }] })).toBe(false) }) + + test("preserves thinking level after /compact", () => { + const msg = { + role: "user", + model: { providerID: "anthropic", modelID: "claude-sonnet-4-5" }, + parts: [{ type: "compaction" }], + } + + expect(syncVariant({ current: "high", message: msg })).toBe("high") + }) + + test("still updates thinking level from normal user messages", () => { + const msg = { + role: "user", + model: { providerID: "anthropic", modelID: "claude-sonnet-4-5", variant: "max" }, + parts: [{ type: "text" }], + } + + expect(syncVariant({ current: "high", message: msg })).toBe("max") + }) }) From f65abc6d8642d661f78bf94785c119d25492732d Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Tue, 5 May 2026 16:56:48 +0300 Subject: [PATCH 3/8] fix(cli): wait for message parts --- packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 08735632814..59d5bee4a56 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -237,7 +237,8 @@ export function Prompt(props: PromptProps) { const sessionID = props.sessionID const msg = lastUserMessage() if (!sessionID || !msg) return - const parts = sync.data.part[msg.id] ?? [] // kilocode_change + const parts = sync.data.part[msg.id] // kilocode_change + if (!parts) return // kilocode_change if (!KiloSessionTuiSync.model({ role: msg.role, parts })) return // kilocode_change const key = [sessionID, msg.id].join(":") From 53ecfb67c14a7ff54cb926f7ad98a48f86ace293 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Tue, 5 May 2026 17:27:23 +0300 Subject: [PATCH 4/8] fix(cli): skip model sync before parts load --- .../opencode/src/kilocode/session/tui-sync.ts | 3 +- .../opencode/test/kilocode/tui-sync.test.ts | 32 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/kilocode/session/tui-sync.ts b/packages/opencode/src/kilocode/session/tui-sync.ts index 2f3375677ff..26d71f9ba64 100644 --- a/packages/opencode/src/kilocode/session/tui-sync.ts +++ b/packages/opencode/src/kilocode/session/tui-sync.ts @@ -1,6 +1,7 @@ export namespace KiloSessionTuiSync { export function model(input: { role: string; parts?: readonly { type: string }[] }) { if (input.role !== "user") return false - return !input.parts?.some((part) => part.type === "compaction") + if (!input.parts) return false + return !input.parts.some((part) => part.type === "compaction") } } diff --git a/packages/opencode/test/kilocode/tui-sync.test.ts b/packages/opencode/test/kilocode/tui-sync.test.ts index a28c6d041a0..e7262ac74e2 100644 --- a/packages/opencode/test/kilocode/tui-sync.test.ts +++ b/packages/opencode/test/kilocode/tui-sync.test.ts @@ -7,8 +7,8 @@ type Message = { parts?: readonly { type: string }[] } -function syncVariant(input: { current: string | undefined; message: Message }) { - if (!KiloSessionTuiSync.model(input.message)) return input.current +function syncVariant(input: { current: string | undefined; message: Message; parts?: readonly { type: string }[] }) { + if (!KiloSessionTuiSync.model({ role: input.message.role, parts: input.parts })) return input.current return input.message.model?.variant ?? "default" } @@ -21,6 +21,10 @@ describe("KiloSessionTuiSync.model", () => { expect(KiloSessionTuiSync.model({ role: "user", parts: [{ type: "compaction" }] })).toBe(false) }) + test("skips messages before parts load", () => { + expect(KiloSessionTuiSync.model({ role: "user" })).toBe(false) + }) + test("skips messages checked with stored parts", () => { const msg = { role: "user" } const parts = [{ type: "compaction" }] @@ -39,7 +43,29 @@ describe("KiloSessionTuiSync.model", () => { parts: [{ type: "compaction" }], } + expect(syncVariant({ current: "high", message: msg, parts: msg.parts })).toBe("high") + }) + + test("preserves thinking level when compaction parts are stored separately", () => { + const msg = { + role: "user", + model: { providerID: "anthropic", modelID: "claude-sonnet-4-5" }, + } + const parts = [{ type: "compaction" }] + + expect(parts.some((part) => part.type === "compaction")).toBe(true) + expect(syncVariant({ current: "high", message: msg, parts })).toBe("high") + }) + + test("waits for normal user message parts before syncing", () => { + const msg = { + role: "user", + model: { providerID: "anthropic", modelID: "claude-sonnet-4-5", variant: "max" }, + } + const parts = [{ type: "text" }] + expect(syncVariant({ current: "high", message: msg })).toBe("high") + expect(syncVariant({ current: "high", message: msg, parts })).toBe("max") }) test("still updates thinking level from normal user messages", () => { @@ -49,6 +75,6 @@ describe("KiloSessionTuiSync.model", () => { parts: [{ type: "text" }], } - expect(syncVariant({ current: "high", message: msg })).toBe("max") + expect(syncVariant({ current: "high", message: msg, parts: msg.parts })).toBe("max") }) }) From 2c90fa8945c3be5b2ba442b833ff6fb3d497220d Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 16:21:27 +0000 Subject: [PATCH 5/8] fix(cli): remove redundant per-line kilocode_change markers The three lines are already inside a kilocode_change start/end block, so the individual line markers are unnecessary. --- .../opencode/src/cli/cmd/tui/component/prompt/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 59d5bee4a56..1642f63cd5e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -237,9 +237,9 @@ export function Prompt(props: PromptProps) { const sessionID = props.sessionID const msg = lastUserMessage() if (!sessionID || !msg) return - const parts = sync.data.part[msg.id] // kilocode_change - if (!parts) return // kilocode_change - if (!KiloSessionTuiSync.model({ role: msg.role, parts })) return // kilocode_change + const parts = sync.data.part[msg.id] + if (!parts) return + if (!KiloSessionTuiSync.model({ role: msg.role, parts })) return const key = [sessionID, msg.id].join(":") if (key === syncedKey) return From bd6bbdddf3566a578e81041d0b121c20b575b71d Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Wed, 6 May 2026 11:01:50 +0300 Subject: [PATCH 6/8] style(cli): use marker block --- packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 1642f63cd5e..ec5d2977f7b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -237,9 +237,11 @@ export function Prompt(props: PromptProps) { const sessionID = props.sessionID const msg = lastUserMessage() if (!sessionID || !msg) return + // kilocode_change start - skip compaction messages while syncing local agent/model const parts = sync.data.part[msg.id] if (!parts) return if (!KiloSessionTuiSync.model({ role: msg.role, parts })) return + // kilocode_change end const key = [sessionID, msg.id].join(":") if (key === syncedKey) return From c776693f3b9a6f7aae3756ff40e2cb346ae1e49f Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Wed, 6 May 2026 11:16:58 +0300 Subject: [PATCH 7/8] fix(ci): restore workflow check --- .github/workflows/compliance-close.yml | 96 ------- .github/workflows/daily-issues-recap.yml | 173 ------------ .github/workflows/daily-pr-recap.yml | 176 ------------ .github/workflows/pr-management.yml | 113 -------- .github/workflows/pr-standards.yml | 340 ----------------------- .github/workflows/storybook.yml | 39 --- script/check-workflows.ts | 81 ++++++ 7 files changed, 81 insertions(+), 937 deletions(-) delete mode 100644 .github/workflows/compliance-close.yml delete mode 100644 .github/workflows/daily-issues-recap.yml delete mode 100644 .github/workflows/daily-pr-recap.yml delete mode 100644 .github/workflows/pr-management.yml delete mode 100644 .github/workflows/pr-standards.yml delete mode 100644 .github/workflows/storybook.yml create mode 100644 script/check-workflows.ts diff --git a/.github/workflows/compliance-close.yml b/.github/workflows/compliance-close.yml deleted file mode 100644 index d56d3b4d971..00000000000 --- a/.github/workflows/compliance-close.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: compliance-close - -on: - schedule: - # Run every 30 minutes to check for expired compliance windows - - cron: "*/30 * * * *" - workflow_dispatch: - -permissions: - contents: read - issues: write - pull-requests: write - -jobs: - close-non-compliant: - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo - runs-on: ubuntu-latest - steps: - - name: Close non-compliant issues and PRs after 2 hours - uses: actions/github-script@v7 - with: - script: | - const { data: items } = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - labels: 'needs:compliance', - state: 'open', - per_page: 100, - }); - - if (items.length === 0) { - core.info('No open issues/PRs with needs:compliance label'); - return; - } - - const now = Date.now(); - const twoHours = 2 * 60 * 60 * 1000; - - for (const item of items) { - const isPR = !!item.pull_request; - const kind = isPR ? 'PR' : 'issue'; - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: item.number, - }); - - const complianceComment = comments.find(c => c.body.includes('')); - if (!complianceComment) continue; - - const commentAge = now - new Date(complianceComment.created_at).getTime(); - if (commentAge < twoHours) { - core.info(`${kind} #${item.number} still within 2-hour window (${Math.round(commentAge / 60000)}m elapsed)`); - continue; - } - - const closeMessage = isPR - ? 'This pull request has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new pull request that follows our guidelines.' - : 'This issue has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new issue that follows our issue templates.'; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: item.number, - body: closeMessage, - }); - - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: item.number, - name: 'needs:compliance', - }); - } catch (e) {} - - if (isPR) { - await github.rest.pulls.update({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: item.number, - state: 'closed', - }); - } else { - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: item.number, - state: 'closed', - state_reason: 'not_planned', - }); - } - - core.info(`Closed non-compliant ${kind} #${item.number} after 2-hour window`); - } diff --git a/.github/workflows/daily-issues-recap.yml b/.github/workflows/daily-issues-recap.yml deleted file mode 100644 index 185de2f5b3e..00000000000 --- a/.github/workflows/daily-issues-recap.yml +++ /dev/null @@ -1,173 +0,0 @@ -name: daily-issues-recap - -on: - schedule: - # Run at 6 PM EST (23:00 UTC, or 22:00 UTC during daylight saving) - - cron: "0 23 * * *" - workflow_dispatch: # Allow manual trigger for testing - -jobs: - daily-recap: - # kilocode_change start - disabled for kilo-cli fork (OpenCode Discord integration) - if: false - # kilocode_change end - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - issues: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - uses: ./.github/actions/setup-bun - - - name: Install opencode - run: curl -fsSL https://opencode.ai/install | bash - - - name: Generate daily issues recap - id: recap - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - KILO_PERMISSION: | - { - "bash": { - "*": "deny", - "gh issue*": "allow", - "gh search*": "allow" - }, - "webfetch": "deny", - "edit": "deny", - "write": "deny" - } - run: | - # Get today's date range - TODAY=$(date -u +%Y-%m-%d) - - opencode run -m kilo/anthropic/claude-sonnet-4.5 "Generate a daily issues recap for the OpenCode repository. - - TODAY'S DATE: ${TODAY} - - STEP 1: Gather today's issues - Search for all OPEN issues created today (${TODAY}) using: - gh issue list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500 - - IMPORTANT: EXCLUDE all issues authored by Anomaly team members. Filter out issues where the author login matches ANY of these: - adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr - This recap is specifically for COMMUNITY (external) issues only. - - STEP 2: Analyze and categorize - For each issue created today, categorize it: - - **Severity Assessment:** - - CRITICAL: Crashes, data loss, security issues, blocks major functionality - - HIGH: Significant bugs affecting many users, important features broken - - MEDIUM: Bugs with workarounds, minor features broken - - LOW: Minor issues, cosmetic, nice-to-haves - - **Activity Assessment:** - - Note issues with high comment counts or engagement - - Note issues from repeat reporters (check if author has filed before) - - STEP 3: Cross-reference with existing issues - For issues that seem like feature requests or recurring bugs: - - Search for similar older issues to identify patterns - - Note if this is a frequently requested feature - - Identify any issues that are duplicates of long-standing requests - - STEP 4: Generate the recap - Create a structured recap with these sections: - - ===DISCORD_START=== - **Daily Issues Recap - ${TODAY}** - - **Summary Stats** - - Total issues opened today: [count] - - By category: [bugs/features/questions] - - **Critical/High Priority Issues** - [List any CRITICAL or HIGH severity issues with brief descriptions and issue numbers] - - **Most Active/Discussed** - [Issues with significant engagement or from active community members] - - **Trending Topics** - [Patterns noticed - e.g., 'Multiple reports about X', 'Continued interest in Y feature'] - - **Duplicates & Related** - [Issues that relate to existing open issues] - ===DISCORD_END=== - - STEP 5: Format for Discord - Format the recap as a Discord-compatible message: - - Use Discord markdown (**, __, etc.) - - BE EXTREMELY CONCISE - this is an EOD summary, not a detailed report - - Use hyperlinked issue numbers with suppressed embeds: [#1234]() - - Group related issues on single lines where possible - - Add emoji sparingly for critical items only - - HARD LIMIT: Keep under 1800 characters total - - Skip sections that have nothing notable (e.g., if no critical issues, omit that section) - - Prioritize signal over completeness - only surface what matters - - OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/recap_raw.txt - - # Extract only the Discord message between markers - sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/recap_raw.txt | grep -v '===DISCORD' > /tmp/recap.txt - - echo "recap_file=/tmp/recap.txt" >> $GITHUB_OUTPUT - - - name: Post to Discord - env: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }} - run: | - if [ -z "$DISCORD_WEBHOOK_URL" ]; then - echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post" - cat /tmp/recap.txt - exit 0 - fi - - # Read the recap - RECAP_RAW=$(cat /tmp/recap.txt) - RECAP_LENGTH=${#RECAP_RAW} - - echo "Recap length: ${RECAP_LENGTH} chars" - - # Function to post a message to Discord - post_to_discord() { - local msg="$1" - local content=$(echo "$msg" | jq -Rs '.') - curl -s -H "Content-Type: application/json" \ - -X POST \ - -d "{\"content\": ${content}}" \ - "$DISCORD_WEBHOOK_URL" - sleep 1 - } - - # If under limit, send as single message - if [ "$RECAP_LENGTH" -le 1950 ]; then - post_to_discord "$RECAP_RAW" - else - echo "Splitting into multiple messages..." - remaining="$RECAP_RAW" - while [ ${#remaining} -gt 0 ]; do - if [ ${#remaining} -le 1950 ]; then - post_to_discord "$remaining" - break - else - chunk="${remaining:0:1900}" - last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1) - if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then - chunk="${remaining:0:$last_newline}" - remaining="${remaining:$((last_newline+1))}" - else - chunk="${remaining:0:1900}" - remaining="${remaining:1900}" - fi - post_to_discord "$chunk" - fi - done - fi - - echo "Posted daily recap to Discord" diff --git a/.github/workflows/daily-pr-recap.yml b/.github/workflows/daily-pr-recap.yml deleted file mode 100644 index 7049531b5dd..00000000000 --- a/.github/workflows/daily-pr-recap.yml +++ /dev/null @@ -1,176 +0,0 @@ -name: daily-pr-recap - -on: - schedule: - # Run at 5pm EST (22:00 UTC, or 21:00 UTC during daylight saving) - - cron: "0 22 * * *" - workflow_dispatch: # Allow manual trigger for testing - -jobs: - pr-recap: - # kilocode_change start - disabled for kilo-cli fork (OpenCode Discord integration) - if: false - # kilocode_change end - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - pull-requests: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - uses: ./.github/actions/setup-bun - - - name: Install opencode - run: curl -fsSL https://opencode.ai/install | bash - - - name: Generate daily PR recap - id: recap - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - KILO_PERMISSION: | - { - "bash": { - "*": "deny", - "gh pr*": "allow", - "gh search*": "allow" - }, - "webfetch": "deny", - "edit": "deny", - "write": "deny" - } - run: | - TODAY=$(date -u +%Y-%m-%d) - - opencode run -m kilo/anthropic/claude-sonnet-4.5 "Generate a daily PR activity recap for the OpenCode repository. - - TODAY'S DATE: ${TODAY} - - STEP 1: Gather PR data - Run these commands to gather PR information. ONLY include OPEN PRs created or updated TODAY (${TODAY}): - - # Open PRs created today - gh pr list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100 - - # Open PRs with activity today (updated today) - gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100 - - IMPORTANT: EXCLUDE all PRs authored by Anomaly team members. Filter out PRs where the author login matches ANY of these: - adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr - This recap is specifically for COMMUNITY (external) contributions only. - - - - STEP 2: For high-activity PRs, check comment counts - For promising PRs, run: - gh pr view [NUMBER] --repo ${{ github.repository }} --json comments --jq '[.comments[] | select(.author.login != \"copilot-pull-request-reviewer\" and .author.login != \"github-actions\")] | length' - - IMPORTANT: When counting comments/activity, EXCLUDE these bot accounts: - - copilot-pull-request-reviewer - - github-actions - - STEP 3: Identify what matters (ONLY from today's PRs) - - **Bug Fixes From Today:** - - PRs with 'fix' or 'bug' in title created/updated today - - Small bug fixes (< 100 lines changed) that are easy to review - - Bug fixes from community contributors - - **High Activity Today:** - - PRs with significant human comments today (excluding bots listed above) - - PRs with back-and-forth discussion today - - **Quick Wins:** - - Small PRs (< 50 lines) that are approved or nearly approved - - PRs that just need a final review - - STEP 4: Generate the recap - Create a structured recap: - - ===DISCORD_START=== - **Daily PR Recap - ${TODAY}** - - **New PRs Today** - [PRs opened today - group by type: bug fixes, features, etc.] - - **Active PRs Today** - [PRs with activity/updates today - significant discussion] - - **Quick Wins** - [Small PRs ready to merge] - ===DISCORD_END=== - - STEP 5: Format for Discord - - Use Discord markdown (**, __, etc.) - - BE EXTREMELY CONCISE - surface what we might miss - - Use hyperlinked PR numbers with suppressed embeds: [#1234]() - - Include PR author: [#1234]() (@author) - - For bug fixes, add brief description of what it fixes - - Show line count for quick wins: \"(+15/-3 lines)\" - - HARD LIMIT: Keep under 1800 characters total - - Skip empty sections - - Focus on PRs that need human eyes - - OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/pr_recap_raw.txt - - # Extract only the Discord message between markers - sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/pr_recap_raw.txt | grep -v '===DISCORD' > /tmp/pr_recap.txt - - echo "recap_file=/tmp/pr_recap.txt" >> $GITHUB_OUTPUT - - - name: Post to Discord - env: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }} - run: | - if [ -z "$DISCORD_WEBHOOK_URL" ]; then - echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post" - cat /tmp/pr_recap.txt - exit 0 - fi - - # Read the recap - RECAP_RAW=$(cat /tmp/pr_recap.txt) - RECAP_LENGTH=${#RECAP_RAW} - - echo "Recap length: ${RECAP_LENGTH} chars" - - # Function to post a message to Discord - post_to_discord() { - local msg="$1" - local content=$(echo "$msg" | jq -Rs '.') - curl -s -H "Content-Type: application/json" \ - -X POST \ - -d "{\"content\": ${content}}" \ - "$DISCORD_WEBHOOK_URL" - sleep 1 - } - - # If under limit, send as single message - if [ "$RECAP_LENGTH" -le 1950 ]; then - post_to_discord "$RECAP_RAW" - else - echo "Splitting into multiple messages..." - remaining="$RECAP_RAW" - while [ ${#remaining} -gt 0 ]; do - if [ ${#remaining} -le 1950 ]; then - post_to_discord "$remaining" - break - else - chunk="${remaining:0:1900}" - last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1) - if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then - chunk="${remaining:0:$last_newline}" - remaining="${remaining:$((last_newline+1))}" - else - chunk="${remaining:0:1900}" - remaining="${remaining:1900}" - fi - post_to_discord "$chunk" - fi - done - fi - - echo "Posted daily PR recap to Discord" diff --git a/.github/workflows/pr-management.yml b/.github/workflows/pr-management.yml deleted file mode 100644 index 7abe0a0ac18..00000000000 --- a/.github/workflows/pr-management.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: pr-management - -on: - pull_request_target: - types: [opened] - -jobs: - # kilocode_change start - check-author: - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo - uses: ./.github/workflows/check-org-member.yml - with: - username: ${{ github.event.pull_request.user.login }} - secrets: - kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }} - kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }} - - check-duplicates: - needs: check-author - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo - # kilocode_change end - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Check team membership - id: team-check - run: | - LOGIN="${{ github.event.pull_request.user.login }}" - if [ "$LOGIN" = "opencode-agent[bot]" ] || grep -qxF "$LOGIN" .github/TEAM_MEMBERS; then - echo "is_team=true" >> "$GITHUB_OUTPUT" - echo "Skipping: $LOGIN is a team member or bot" - else - echo "is_team=false" >> "$GITHUB_OUTPUT" - fi - - - name: Setup Bun - if: steps.team-check.outputs.is_team != 'true' - uses: ./.github/actions/setup-bun - - - name: Install dependencies - if: steps.team-check.outputs.is_team != 'true' - run: bun install - - # kilocode_change start - - name: Setup Kilo - uses: ./.github/actions/setup-kilo - # kilocode_change end - - - name: Build prompt - if: steps.team-check.outputs.is_team != 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - { - echo "Check for duplicate PRs related to this new PR:" - echo "" - echo "CURRENT_PR_NUMBER: $PR_NUMBER" - echo "" - echo "Title: $(gh pr view "$PR_NUMBER" --json title --jq .title)" - echo "" - echo "Description:" - gh pr view "$PR_NUMBER" --json body --jq .body - } > pr_info.txt - - - name: Check for duplicate PRs - if: steps.team-check.outputs.is_team != 'true' - env: - # kilocode_change start - KILO_API_KEY: ${{ secrets.KILO_API_KEY }} - KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }} - # kilocode_change end - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - COMMENT=$(bun script/duplicate-pr.ts -f pr_info.txt "Check the attached file for PR details and search for duplicates") - - if [ "$COMMENT" != "No duplicate PRs found" ]; then - gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_ - - $COMMENT" - fi - - add-contributor-label: - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo - runs-on: blacksmith-2vcpu-ubuntu-2404 # kilocode_change - permissions: - pull-requests: write - issues: write - steps: - - name: Add Contributor Label - uses: actions/github-script@v8 - with: - script: | - const isPR = !!context.payload.pull_request; - const issueNumber = isPR ? context.payload.pull_request.number : context.payload.issue.number; - const authorAssociation = isPR ? context.payload.pull_request.author_association : context.payload.issue.author_association; - - if (authorAssociation === 'CONTRIBUTOR') { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - labels: ['contributor'] - }); - } diff --git a/.github/workflows/pr-standards.yml b/.github/workflows/pr-standards.yml deleted file mode 100644 index 75fb650144d..00000000000 --- a/.github/workflows/pr-standards.yml +++ /dev/null @@ -1,340 +0,0 @@ -name: pr-standards - -on: - pull_request_target: - types: [opened, edited, synchronize] - -jobs: - # kilocode_change start - check-author: - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo - uses: ./.github/workflows/check-org-member.yml - with: - username: ${{ github.event.pull_request.user.login }} - secrets: - kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }} - kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }} - - check-standards: - needs: check-author - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo - # kilocode_change end - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - pull-requests: write - steps: - - name: Check PR standards - uses: actions/github-script@v7 - with: - script: | - const pr = context.payload.pull_request; - const title = pr.title; - - async function addLabel(label) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - labels: [label] - }); - } - - async function removeLabel(label) { - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - name: label - }); - } catch (e) { - // Label wasn't present, ignore - } - } - - async function comment(marker, body) { - const markerText = ``; - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number - }); - - const existing = comments.find(c => c.body.includes(markerText)); - if (existing) return; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body: markerText + '\n' + body - }); - } - - // Step 1: Check title format - // Matches: feat:, feat(scope):, feat (scope):, etc. - const titlePattern = /^(feat|fix|docs|chore|refactor|test)\s*(\([a-zA-Z0-9-]+\))?\s*:/; - const hasValidTitle = titlePattern.test(title); - - if (!hasValidTitle) { - await addLabel('needs:title'); - await comment('title', `Hey! Your PR title \`${title}\` doesn't follow conventional commit format. - - Please update it to start with one of: - - \`feat:\` or \`feat(scope):\` new feature - - \`fix:\` or \`fix(scope):\` bug fix - - \`docs:\` or \`docs(scope):\` documentation changes - - \`chore:\` or \`chore(scope):\` maintenance tasks - - \`refactor:\` or \`refactor(scope):\` code refactoring - - \`test:\` or \`test(scope):\` adding or updating tests - - Where \`scope\` is the package name (e.g., \`app\`, \`desktop\`, \`kilo\`). - - See [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md#pr-titles) for details.`); - return; - } - - await removeLabel('needs:title'); - - // Step 2: Check for linked issue (skip for docs/refactor/feat PRs) - const skipIssueCheck = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title); - if (skipIssueCheck) { - await removeLabel('needs:issue'); - console.log('Skipping issue check for docs/refactor/feat PR'); - return; - } - const query = ` - query($owner: String!, $repo: String!, $number: Int!) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $number) { - closingIssuesReferences(first: 1) { - totalCount - } - } - } - } - `; - - const result = await github.graphql(query, { - owner: context.repo.owner, - repo: context.repo.repo, - number: pr.number - }); - - const linkedIssues = result.repository.pullRequest.closingIssuesReferences.totalCount; - - if (linkedIssues === 0) { - await addLabel('needs:issue'); - await comment('issue', `Thanks for your contribution! - - This PR doesn't have a linked issue. All PRs must reference an existing issue. - - Please: - 1. Open an issue describing the bug/feature (if one doesn't exist) - 2. Add \`Fixes #\` or \`Closes #\` to this PR description - - See [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md#issue-first-policy) for details.`); - return; - } - - await removeLabel('needs:issue'); - console.log('PR meets all standards'); - - check-compliance: - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - name: Check PR template compliance - uses: actions/github-script@v7 - with: - script: | - const pr = context.payload.pull_request; - const login = pr.user.login; - - // Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC) - const cutoff = new Date('2026-02-19T00:00:00Z'); - const prCreated = new Date(pr.created_at); - if (prCreated < cutoff) { - console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`); - return; - } - - // Check if author is a team member or bot - if (login === 'opencode-agent[bot]') return; - const { data: file } = await github.rest.repos.getContent({ - owner: context.repo.owner, - repo: context.repo.repo, - path: '.github/TEAM_MEMBERS', - ref: 'dev' - }); - const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean); - if (members.includes(login)) { - console.log(`Skipping: ${login} is a team member`); - return; - } - - const body = pr.body || ''; - const title = pr.title; - const isDocsRefactorOrFeat = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title); - - const issues = []; - - // Check: template sections exist - const hasWhatSection = /### What does this PR do\?/.test(body); - const hasTypeSection = /### Type of change/.test(body); - const hasVerifySection = /### How did you verify your code works\?/.test(body); - const hasChecklistSection = /### Checklist/.test(body); - const hasIssueSection = /### Issue for this PR/.test(body); - - if (!hasWhatSection || !hasTypeSection || !hasVerifySection || !hasChecklistSection || !hasIssueSection) { - issues.push('PR description is missing required template sections. Please use the [PR template](../blob/dev/.github/pull_request_template.md).'); - } - - // Check: "What does this PR do?" has real content (not just placeholder text) - if (hasWhatSection) { - const whatMatch = body.match(/### What does this PR do\?\s*\n([\s\S]*?)(?=###|$)/); - const whatContent = whatMatch ? whatMatch[1].trim() : ''; - const placeholder = 'Please provide a description of the issue'; - const onlyPlaceholder = whatContent.includes(placeholder) && whatContent.replace(placeholder, '').replace(/[*\s]/g, '').length < 20; - if (!whatContent || onlyPlaceholder) { - issues.push('"What does this PR do?" section is empty or only contains placeholder text. Please describe your changes.'); - } - } - - // Check: at least one "Type of change" checkbox is checked - if (hasTypeSection) { - const typeMatch = body.match(/### Type of change\s*\n([\s\S]*?)(?=###|$)/); - const typeContent = typeMatch ? typeMatch[1] : ''; - const hasCheckedBox = /- \[x\]/i.test(typeContent); - if (!hasCheckedBox) { - issues.push('No "Type of change" checkbox is checked. Please select at least one.'); - } - } - - // Check: issue reference (skip for docs/refactor/feat) - if (!isDocsRefactorOrFeat && hasIssueSection) { - const issueMatch = body.match(/### Issue for this PR\s*\n([\s\S]*?)(?=###|$)/); - const issueContent = issueMatch ? issueMatch[1].trim() : ''; - const hasIssueRef = /(closes|fixes|resolves)\s+#\d+/i.test(issueContent) || /#\d+/.test(issueContent); - if (!hasIssueRef) { - issues.push('No issue referenced. Please add `Closes #` linking to the relevant issue.'); - } - } - - // Check: "How did you verify" has content - if (hasVerifySection) { - const verifyMatch = body.match(/### How did you verify your code works\?\s*\n([\s\S]*?)(?=###|$)/); - const verifyContent = verifyMatch ? verifyMatch[1].trim() : ''; - if (!verifyContent) { - issues.push('"How did you verify your code works?" section is empty. Please explain how you tested.'); - } - } - - // Check: checklist boxes are checked - if (hasChecklistSection) { - const checklistMatch = body.match(/### Checklist\s*\n([\s\S]*?)(?=###|$)/); - const checklistContent = checklistMatch ? checklistMatch[1] : ''; - const unchecked = (checklistContent.match(/- \[ \]/g) || []).length; - const checked = (checklistContent.match(/- \[x\]/gi) || []).length; - if (checked < 2) { - issues.push('Not all checklist items are checked. Please confirm you have tested locally and have not included unrelated changes.'); - } - } - - // Helper functions - async function addLabel(label) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - labels: [label] - }); - } - - async function removeLabel(label) { - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - name: label - }); - } catch (e) {} - } - - const hasComplianceLabel = pr.labels.some(l => l.name === 'needs:compliance'); - - if (issues.length > 0) { - // Non-compliant - if (!hasComplianceLabel) { - await addLabel('needs:compliance'); - } - - const marker = ''; - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number - }); - const existing = comments.find(c => c.body.includes(marker)); - - const body_text = `${marker} - This PR doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) and [PR template](../blob/dev/.github/pull_request_template.md). - - **What needs to be fixed:** - ${issues.map(i => `- ${i}`).join('\n')} - - Please edit this PR description to address the above within **2 hours**, or it will be automatically closed. - - If you believe this was flagged incorrectly, please let a maintainer know.`; - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body: body_text - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body: body_text - }); - } - - console.log(`PR #${pr.number} is non-compliant: ${issues.join(', ')}`); - } else if (hasComplianceLabel) { - // Was non-compliant, now fixed - await removeLabel('needs:compliance'); - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number - }); - const marker = ''; - const existing = comments.find(c => c.body.includes(marker)); - if (existing) { - await github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id - }); - } - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body: 'Thanks for updating your PR! It now meets our contributing guidelines. :+1:' - }); - - console.log(`PR #${pr.number} is now compliant, label removed`); - } else { - console.log(`PR #${pr.number} is compliant`); - } diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml deleted file mode 100644 index 9cd606324a1..00000000000 --- a/.github/workflows/storybook.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: storybook - -on: - push: - branches: [dev] - paths: - - ".github/workflows/storybook.yml" - - "package.json" - - "bun.lock" - - "packages/storybook/**" - - "packages/ui/**" - pull_request: - branches: [dev] - paths: - - ".github/workflows/storybook.yml" - - "package.json" - - "bun.lock" - - "packages/storybook/**" - - "packages/ui/**" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - if: false - name: storybook build - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Build Storybook - run: bun --cwd packages/storybook build diff --git a/script/check-workflows.ts b/script/check-workflows.ts new file mode 100644 index 00000000000..a7536cd0799 --- /dev/null +++ b/script/check-workflows.ts @@ -0,0 +1,81 @@ +#!/usr/bin/env bun +// kilocode_change - new file + +/** + * Guards against accidentally inheriting workflows from upstream opencode. + * + * We regularly merge upstream. When upstream adds a new workflow under + * `.github/workflows/`, it silently starts running in our CI unless we + * explicitly review and accept it. This check makes that decision explicit: + * the list of allowed workflows is hardcoded below, and any drift (added or + * removed file in `.github/workflows/`) fails CI until the list is updated + * deliberately. + * + * Only runnable workflows are checked (`.yml` / `.yaml`). Files under + * `.github/workflows/disabled/` are Kilo-specific and can't run, so they're + * not tracked here. + * + * To accept a new workflow: add its filename to `active`. + * To drop one: remove its filename from the list. + */ + +import { readdirSync } from "node:fs" +import path from "node:path" + +const ROOT = path.resolve(import.meta.dir, "..") +const DIR = path.join(ROOT, ".github", "workflows") + +// Workflows we have deliberately accepted into CI. Sort alphabetically. +const active = new Set([ + "auto-docs.yml", + "beta.yml", + "check-md-table-padding.yml", + "check-opencode-annotations.yml", + "check-org-member.yml", + "close-issues.yml", + "close-stale-prs.yml", + "containers.yml", + "docs-build.yml", + "docs-check-links.yml", + "duplicate-issues.yml", + "generate.yml", + "nix-eval.yml", + "nix-hashes.yml", + "publish.yml", + "smoke-test.yml", + "source-check-links.yml", + "test-vscode.yml", + "test.yml", + "triage.yml", + "typecheck.yml", + "visual-regression.yml", + "watch-opencode-releases.yml", +]) + +// GitHub picks up both .yml and .yaml in .github/workflows/. We accept both so +// an upstream `.yaml` addition also shows up as unexpected drift. +const isWorkflow = (f: string) => f.endsWith(".yml") || f.endsWith(".yaml") +const actualActive = new Set(readdirSync(DIR).filter(isWorkflow)) + +const missing = [...active].filter((f) => !actualActive.has(f)).sort() +const extra = [...actualActive].filter((f) => !active.has(f)).sort() +const errs: string[] = [] +for (const f of extra) { + errs.push(`unexpected workflow: ${f} — if this was added intentionally, add it to script/check-workflows.ts`) +} +for (const f of missing) { + errs.push( + `expected workflow not found: ${f} — if this was removed intentionally, remove it from script/check-workflows.ts`, + ) +} + +if (errs.length === 0) { + console.log(`check-workflows: ok (${actualActive.size} workflows).`) + process.exit(0) +} + +for (const e of errs) console.error(e) +console.error("") +console.error(`Found ${errs.length} workflow drift issue(s).`) +console.error("This guard prevents upstream-merged workflows from silently running in our CI.") +process.exit(1) From 2d19ec2ff0332c738e8547a88d7c941595141ec4 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Wed, 6 May 2026 12:20:57 +0300 Subject: [PATCH 8/8] fix(ci): run workflow guard --- .github/workflows/check-opencode-annotations.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/check-opencode-annotations.yml b/.github/workflows/check-opencode-annotations.yml index e1588c0fc44..c06e09a2ff8 100644 --- a/.github/workflows/check-opencode-annotations.yml +++ b/.github/workflows/check-opencode-annotations.yml @@ -28,6 +28,11 @@ jobs: - uses: oven-sh/setup-bun@v2 + # kilocode_change start - check workflow allowlist drift + - name: Check workflow allowlist + run: bun run script/check-workflows.ts + # kilocode_change end + - name: Check kilocode_change annotations in shared upstream files env: BASE_SHA: ${{ github.event.pull_request.base.sha }}