mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge branch 'main' into feat/roll-call-command
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# Changesets
|
||||
|
||||
This directory contains changeset files used to track changes for the next release.
|
||||
|
||||
## Adding a changeset
|
||||
|
||||
When making a user-facing change, run:
|
||||
|
||||
```sh
|
||||
bunx changeset add
|
||||
```
|
||||
|
||||
Or manually create a file `.changeset/<slug>.md`:
|
||||
|
||||
```md
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Short description of the change for the changelog.
|
||||
```
|
||||
|
||||
Use `patch` for bug fixes, `minor` for new features, `major` for breaking changes.
|
||||
|
||||
Changeset files are consumed at release time when the `publish.yml` workflow runs, generating changelog entries for the GitHub release notes.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep Agent Manager state hidden from Git before creating local sessions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve the JetBrains model picker with sections, favorites, and smart filtering.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep VS Code session timeline, token, context, and cost stats in sync after restoring a checkpoint.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Add a JetBrains tool window action for starting new sessions and let recent sessions reopen in the side panel without duplicate views.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Show ChatGPT sign-in again when Codex authentication expires.
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json",
|
||||
"changelog": ["../script/changelog-github.cjs", { "repo": "Kilo-Org/kilocode" }],
|
||||
"commit": false,
|
||||
"fixed": [["kilo-code", "@kilocode/cli"]],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Scale JetBrains chat transcript fonts from the editor font settings.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Keep the JetBrains model picker at the same row position when models are favorited or unfavorited.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix TUI diff rendering when header-like content lines appear inside a unified diff hunk.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show JetBrains session connection status inline above the prompt, cap expanded connection details, and keep empty sessions in the shared chat scroll area.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show detailed JetBrains session connection and configuration issues in the connection panel, and make retry recover app-side problems more reliably.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Match VS Code-style reasoning and tool run rendering in JetBrains chat.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Center description text and remove the fixed-width cap from the empty session panel so recent sessions can stretch naturally.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Open existing JetBrains sessions scrolled to the latest message after history loads.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Remember the JetBrains session header metrics expansion preference and show file names in read/write timeline tooltips.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve JetBrains history navigation with Back support and a Show History action in recent sessions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve the JetBrains history panel with native tabs, independent local and cloud search, and date-grouped cloud sessions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve the JetBrains mode picker with descriptions, deprecated badges, and native IntelliJ selection behavior.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Keep the JetBrains chat transcript pinned to bottom reliably while responses stream.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show a scroll-to-bottom button when JetBrains chat sessions are scrolled away from the latest message.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Support rebinding the JetBrains prompt Send action in the IntelliJ keymap.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show collapsible session metadata, context usage, token usage, and compaction controls in the JetBrains session header.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Add a native session history panel to the JetBrains plugin.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Control JetBrains chat transcript spacing from the session layout.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show loading immediately when opening JetBrains sessions without showing duplicate progress for new sessions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Reduce redundant JetBrains chat scroll layout work while keeping bottom-follow behavior.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show completed assistant steps in the JetBrains session timeline.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Support stopping active JetBrains sessions from the prompt action button.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Size the JetBrains model picker from its rendered model rows instead of fixed popup dimensions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Use the VS Code-native placeholder color for input fields so placeholder text is consistently distinct from values you have typed (Settings, Agent Manager, prompt input, model search, etc.).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Support reasoning effort selection and resetting JetBrains chat model overrides to the default model.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Align checked and unchecked reasoning picker rows in the JetBrains plugin.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Reset the To-dos tab when reverting a session snapshot.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Fix Mermaid diagrams rendering with empty text inside every shape by restoring the `foreignObject` HTML integration point that DOMPurify dropped in 3.1.7.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Keep the JetBrains session connection status above the prompt without shifting the chat layout.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Refine JetBrains session loading UI and settings actions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Preserve JetBrains chat scroll position when reviewing earlier session output.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"@kilocode/kilo-gateway": patch
|
||||
---
|
||||
|
||||
Prevent Kilo Gateway Responses requests from replaying transient provider item IDs when request storage is disabled.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Avoid flickering transient loading and connection error states in JetBrains sessions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep configured mode models in sync when switching modes in VS Code.
|
||||
@@ -7,3 +7,8 @@ end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
max_line_length = 80
|
||||
|
||||
[*.{kt,kts}]
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
# Visual regression baseline snapshots
|
||||
packages/kilo-ui/tests/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
packages/kilo-vscode/tests/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
packages/kilo-docs/public/img/screenshot-tests/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Hide non-English localization files by default in GitHub PR diffs.
|
||||
**/i18n/*.ts linguist-generated=true
|
||||
@@ -18,3 +19,7 @@ packages/kilo-vscode/tests/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
**/i18n/parity.test.ts linguist-generated=false
|
||||
packages/kilo-i18n/src/*.ts linguist-generated=true
|
||||
packages/kilo-i18n/src/en.ts linguist-generated=false
|
||||
|
||||
# Auto-generated CLI reference docs
|
||||
packages/kilo-docs/markdoc/partials/cli-commands-table.md linguist-generated=true
|
||||
packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md linguist-generated=true
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
name: "Setup Bun"
|
||||
description: "Setup Bun with caching and install dependencies"
|
||||
inputs:
|
||||
install-flags:
|
||||
description: "Additional flags to pass to 'bun install'"
|
||||
required: false
|
||||
default: ""
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Get baseline download URL
|
||||
id: bun-url
|
||||
shell: bash
|
||||
@@ -31,6 +28,31 @@ runs:
|
||||
bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }}
|
||||
bun-download-url: ${{ steps.bun-url.outputs.url }}
|
||||
|
||||
- name: Get cache directory
|
||||
id: cache
|
||||
shell: bash
|
||||
run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install setuptools for distutils compatibility
|
||||
run: python3 -m pip install setuptools || pip install setuptools || true
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
run: |
|
||||
# Workaround for patched peer variants
|
||||
# e.g. ./patches/ for standard-openapi
|
||||
# https://github.com/oven-sh/bun/issues/28147
|
||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||
bun install --linker hoisted ${{ inputs.install-flags }}
|
||||
else
|
||||
bun install ${{ inputs.install-flags }}
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@@ -39,6 +39,15 @@ runs:
|
||||
- name: Clear checkout auth
|
||||
run: |
|
||||
git config --local --unset-all http.https://github.com/.extraheader || true
|
||||
# kilocode_change start
|
||||
keys="$(git config --local --name-only --get-regexp '^includeIf\.gitdir:' || true)"
|
||||
for key in $keys; do
|
||||
path="$(git config --local --get "$key" || true)"
|
||||
case "$path" in
|
||||
*git-credentials-*.config) git config --local --unset-all "$key" || true ;;
|
||||
esac
|
||||
done
|
||||
# kilocode_change end
|
||||
shell: bash
|
||||
|
||||
- name: Configure git remote
|
||||
|
||||
@@ -13,8 +13,8 @@ Some description of HOW you achieved it. Perhaps give a high level description o
|
||||
## Screenshots
|
||||
|
||||
| before | after |
|
||||
| ------ | ----- |
|
||||
| | |
|
||||
|---|---|
|
||||
| | |
|
||||
|
||||
## How to Test
|
||||
|
||||
|
||||
+100
-22
@@ -1,35 +1,113 @@
|
||||
name: Trigger webhook for feat PRs
|
||||
name: Auto-docs dry-run intake
|
||||
|
||||
# Trigger on merged feature PRs, or manually with a specific PR number.
|
||||
# IMPORTANT: This workflow intentionally does NOT check out PR code and does NOT
|
||||
# execute any code from the PR branch. It only reads PR metadata and forwards it
|
||||
# to a webhook. This is required for safe use of pull_request_target on a public repo.
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: "PR number to test"
|
||||
required: true
|
||||
|
||||
# Minimal permissions: read repo contents and PR metadata only.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
# One run per PR at a time. Does not cancel in progress — let the current run
|
||||
# finish so we don't drop events when a PR is quickly closed/reopened/closed.
|
||||
concurrency:
|
||||
group: auto-docs-${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
call-webhook:
|
||||
call-valtown:
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
(startsWith(github.event.pull_request.title, 'feat:') ||
|
||||
startsWith(github.event.pull_request.title, 'feat('))
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.pull_request.merged == true &&
|
||||
(
|
||||
startsWith(github.event.pull_request.title, 'feat:') ||
|
||||
startsWith(github.event.pull_request.title, 'feat(')
|
||||
)
|
||||
)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- name: Send webhook safely
|
||||
- name: Build webhook payload
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER_FROM_EVENT: ${{ github.event.pull_request.number }}
|
||||
PR_NUMBER_FROM_INPUT: ${{ inputs.pr_number }}
|
||||
run: |
|
||||
payload=$(jq -n \
|
||||
--arg repo "${GITHUB_REPOSITORY}" \
|
||||
--arg pr_number "${{ github.event.pull_request.number }}" \
|
||||
--arg title "${{ github.event.pull_request.title }}" \
|
||||
--arg body "${{ github.event.pull_request.body }}" \
|
||||
--arg author "${{ github.event.pull_request.user.login }}" \
|
||||
--arg merged_at "${{ github.event.pull_request.merged_at }}" \
|
||||
set -euo pipefail
|
||||
|
||||
PR_NUMBER="${PR_NUMBER_FROM_EVENT:-$PR_NUMBER_FROM_INPUT}"
|
||||
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
echo "Missing PR number"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gh api "repos/$REPO/pulls/$PR_NUMBER" > pr.json
|
||||
|
||||
jq -n \
|
||||
--arg event_name "$EVENT_NAME" \
|
||||
--arg repo "$REPO" \
|
||||
--arg pr_number "$PR_NUMBER" \
|
||||
--slurpfile pr pr.json \
|
||||
'{
|
||||
event_name: $event_name,
|
||||
repo: $repo,
|
||||
pr_number: $pr_number,
|
||||
title: $title,
|
||||
description: $body,
|
||||
author: $author,
|
||||
merged_at: $merged_at
|
||||
}'
|
||||
)
|
||||
curl --fail-with-body -X POST "${{ secrets.DOC_WEBHOOK_URL }}" \
|
||||
title: $pr[0].title,
|
||||
description: ($pr[0].body // ""),
|
||||
author: $pr[0].user.login,
|
||||
merged_at: ($pr[0].merged_at // ""),
|
||||
pr_url: $pr[0].html_url,
|
||||
base_branch: $pr[0].base.ref,
|
||||
head_branch: $pr[0].head.ref
|
||||
}' > payload.json
|
||||
|
||||
echo "Payload created:"
|
||||
jq '{
|
||||
event_name,
|
||||
repo,
|
||||
pr_number,
|
||||
title,
|
||||
author,
|
||||
merged_at,
|
||||
pr_url,
|
||||
base_branch,
|
||||
head_branch
|
||||
}' payload.json
|
||||
|
||||
- name: Send webhook to Val Town
|
||||
env:
|
||||
DOC_WEBHOOK_URL: ${{ secrets.DOC_WEBHOOK_URL }}
|
||||
DOC_WEBHOOK_SECRET: ${{ secrets.DOC_WEBHOOK_SECRET }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${DOC_WEBHOOK_URL:-}" ]; then
|
||||
echo "DOC_WEBHOOK_URL secret is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${DOC_WEBHOOK_SECRET:-}" ]; then
|
||||
echo "DOC_WEBHOOK_SECRET secret is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl --fail-with-body -sS -X POST "$DOC_WEBHOOK_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-raw "$payload"
|
||||
-H "X-Docs-Webhook-Secret: $DOC_WEBHOOK_SECRET" \
|
||||
--data-binary @payload.json
|
||||
|
||||
@@ -14,8 +14,9 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
persist-credentials: false # kilocode_change
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Bun
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# kilocode_change - new file
|
||||
name: Check Kilo generated artifacts
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".gitignore"
|
||||
- ".github/workflows/check-kilo-generated-artifacts.yml"
|
||||
- ".kilo/**"
|
||||
- ".kilocode/**"
|
||||
- "script/check-kilo-generated-artifacts.ts"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check Kilo generated artifacts
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Run check
|
||||
run: bun run script/check-kilo-generated-artifacts.ts
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Check markdown table padding
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "**/*.md"
|
||||
- "script/check-md-table-padding.ts"
|
||||
- ".github/workflows/check-md-table-padding.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check markdown table padding
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Run check
|
||||
run: bun run script/check-md-table-padding.ts
|
||||
@@ -1,11 +1,18 @@
|
||||
name: Check opencode annotations
|
||||
name: Check shared upstream annotations
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/**"
|
||||
- "github/**"
|
||||
- "sdks/vscode/**"
|
||||
- "packages/extensions/**"
|
||||
- "packages/opencode/**"
|
||||
- "script/check-opencode-annotations.ts"
|
||||
- ".github/workflows/check-opencode-annotations.yml"
|
||||
- "packages/script/**"
|
||||
- "packages/shared/**"
|
||||
- "packages/storybook/**"
|
||||
- "packages/ui/**"
|
||||
- "script/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -14,14 +21,14 @@ jobs:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6 # kilocode_change
|
||||
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
|
||||
- name: Check kilocode_change annotations in shared upstream files
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
@@ -30,3 +37,8 @@ jobs:
|
||||
else
|
||||
echo "No PR base SHA available (workflow_dispatch without PR context) — skipping."
|
||||
fi
|
||||
|
||||
# kilocode_change start
|
||||
- name: Check workflow allowlist
|
||||
run: bun run script/check-workflows.ts
|
||||
# kilocode_change end
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: close-issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 2 * * *" # Daily at 2:00 AM
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
close:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6 # kilocode_change
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Close stale issues
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: bun script/github/close-issues.ts
|
||||
@@ -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('<!-- issue-compliance -->'));
|
||||
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`);
|
||||
}
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
# kilocode_change end
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6 # kilocode_change
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
|
||||
@@ -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](<https://github.com/${{ github.repository }}/issues/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"
|
||||
@@ -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](<https://github.com/${{ github.repository }}/pull/1234>)
|
||||
- Include PR author: [#1234](<url>) (@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"
|
||||
@@ -0,0 +1,95 @@
|
||||
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:
|
||||
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('<!-- issue-compliance -->'));
|
||||
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`);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
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:
|
||||
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://kilo.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 opencode/claude-sonnet-4-5 "Generate a daily issues recap for the Kilo 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](<https://github.com/${{ github.repository }}/issues/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"
|
||||
@@ -0,0 +1,173 @@
|
||||
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:
|
||||
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://kilo.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 opencode/claude-sonnet-4-5 "Generate a daily PR activity recap for the Kilo 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](<https://github.com/${{ github.repository }}/pull/1234>)
|
||||
- Include PR author: [#1234](<url>) (@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"
|
||||
@@ -0,0 +1,95 @@
|
||||
name: pr-management
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
check-duplicates:
|
||||
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
|
||||
|
||||
- name: Install opencode
|
||||
if: steps.team-check.outputs.is_team != 'true'
|
||||
run: curl -fsSL https://kilo.ai/install | bash
|
||||
|
||||
- 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:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
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']
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
name: pr-standards
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
check-standards:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check PR standards
|
||||
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 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 = `<!-- pr-standards:${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(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\`, \`opencode\`).
|
||||
|
||||
See [CONTRIBUTING.md](../blob/dev/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 #<number>\` or \`Closes #<number>\` to this PR description
|
||||
|
||||
See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#issue-first-policy) for details.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await removeLabel('needs:issue');
|
||||
console.log('PR meets all standards');
|
||||
|
||||
check-compliance:
|
||||
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 #<number>` 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 = '<!-- issue-compliance -->';
|
||||
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 = '<!-- issue-compliance -->';
|
||||
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`);
|
||||
}
|
||||
@@ -49,13 +49,13 @@ jobs:
|
||||
|
||||
- name: Check PR guidelines compliance
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
KILO_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }'
|
||||
PR_TITLE: ${{ steps.pr-details.outputs.title }}
|
||||
run: |
|
||||
PR_BODY=$(jq -r .body pr_data.json)
|
||||
opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}'
|
||||
opencode run -m opencode/gpt-5.5 --variant medium "A new pull request has been created: '${PR_TITLE}'
|
||||
|
||||
<pr-number>
|
||||
${{ steps.pr-number.outputs.number }}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
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:
|
||||
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
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6 # kilocode_change
|
||||
|
||||
- name: Link Checker
|
||||
uses: Kilo-Org/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
# kilocode_change start
|
||||
with:
|
||||
persist-credentials: false
|
||||
# kilocode_change end
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
@@ -40,8 +40,6 @@ jobs:
|
||||
|
||||
SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin"
|
||||
PACKAGES="kilo"
|
||||
# TODO: move 'desktop' to PACKAGES when #11755 is fixed
|
||||
OPTIONAL_PACKAGES="desktop"
|
||||
|
||||
echo ""
|
||||
echo "=== Evaluating packages for all systems ==="
|
||||
@@ -61,23 +59,6 @@ jobs:
|
||||
done
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Evaluating optional packages ==="
|
||||
for system in $SYSTEMS; do
|
||||
echo ""
|
||||
echo "--- $system ---"
|
||||
for pkg in $OPTIONAL_PACKAGES; do
|
||||
printf " %s: " "$pkg"
|
||||
if output=$(nix eval ".#packages.$system.$pkg.drvPath" --raw 2>&1); then
|
||||
echo "✓"
|
||||
else
|
||||
echo "✗"
|
||||
echo "::warning::Evaluation failed for packages.$system.$pkg"
|
||||
echo "$output"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Evaluating devShells for all systems ==="
|
||||
for system in $SYSTEMS; do
|
||||
|
||||
@@ -17,6 +17,10 @@ on:
|
||||
- "patches/**"
|
||||
- ".github/workflows/nix-hashes.yml"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Native runners required: bun install cross-compilation flags (--os/--cpu)
|
||||
# do not produce byte-identical node_modules as native installs.
|
||||
@@ -57,7 +61,7 @@ jobs:
|
||||
nix build ".#packages.${SYSTEM}.node_modules_updater" --no-link 2>&1 | tee "$BUILD_LOG" || true
|
||||
|
||||
# Extract hash from build log with portability
|
||||
HASH="$(grep -oE 'sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)"
|
||||
HASH="$(nix run --inputs-from . nixpkgs#gnugrep -- -oP 'got:\s*\Ksha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)"
|
||||
|
||||
if [ -z "$HASH" ]; then
|
||||
echo "::error::Failed to compute hash for ${SYSTEM}"
|
||||
@@ -69,7 +73,7 @@ jobs:
|
||||
echo "Computed hash for ${SYSTEM}: $HASH"
|
||||
|
||||
- name: Upload hash
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7 # kilocode_change
|
||||
with:
|
||||
name: hash-${{ matrix.system }}
|
||||
path: hash.txt
|
||||
@@ -82,7 +86,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -102,7 +106,7 @@ jobs:
|
||||
git pull --rebase --autostash origin "$GITHUB_REF_NAME"
|
||||
|
||||
- name: Download hash artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8 # kilocode_change
|
||||
with:
|
||||
path: hashes
|
||||
pattern: hash-*
|
||||
|
||||
@@ -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']
|
||||
});
|
||||
}
|
||||
@@ -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 = `<!-- pr-standards:${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(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 #<number>\` or \`Closes #<number>\` 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 #<number>` 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 = '<!-- issue-compliance -->';
|
||||
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 = '<!-- issue-compliance -->';
|
||||
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`);
|
||||
}
|
||||
+71
-210
@@ -22,11 +22,13 @@ on:
|
||||
description: "Override version (optional)"
|
||||
required: false
|
||||
type: string
|
||||
# kilocode_change start
|
||||
pre_release:
|
||||
description: "Publish as pre-release (VS Code marketplace)"
|
||||
description: "Publish as pre-release (VS Code marketplace + npm rc channel)"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
default: true
|
||||
# kilocode_change end
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
|
||||
|
||||
@@ -40,15 +42,20 @@ jobs:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
# kilocode_change start - install deps for version script workspace resolution
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Install Kilo
|
||||
if: inputs.bump || inputs.version
|
||||
run: bun i -g @kilocode/cli
|
||||
# kilocode_change end
|
||||
|
||||
- id: version
|
||||
run: |
|
||||
@@ -58,6 +65,7 @@ jobs:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
KILO_BUMP: ${{ inputs.bump }}
|
||||
KILO_VERSION: ${{ inputs.version }}
|
||||
KILO_PRE_RELEASE: ${{ inputs.pre_release }}
|
||||
KILO_API_KEY: ${{ secrets.KILO_API_KEY }}
|
||||
KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }}
|
||||
outputs:
|
||||
@@ -70,7 +78,7 @@ jobs:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
fetch-tags: true
|
||||
|
||||
@@ -83,13 +91,29 @@ jobs:
|
||||
env:
|
||||
KILO_VERSION: ${{ needs.version.outputs.version }}
|
||||
KILO_RELEASE: ${{ needs.version.outputs.release }}
|
||||
KILO_PRE_RELEASE: ${{ inputs.pre_release }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
# kilocode_change start - pack dist tree into a single zstd-compressed tar before upload.
|
||||
# download-artifact@v4's streaming unzip is CPU-bound and processes entries one by one;
|
||||
# collapsing ~480 files into one tar cuts build-vscode download from >30m to seconds.
|
||||
# zstd compresses the wasms (~4x) and leaves the already-compact binaries alone, for
|
||||
# ~1 GB artifact. upload-artifact@v7's `archive: false` skips the zip wrapper entirely
|
||||
# so the tarball travels as-is (download-artifact@v8 auto-detects via Content-Type).
|
||||
# With `archive: false` the `name:` input is ignored; the artifact is named after the
|
||||
# file (kilo-cli.tar.zst). zstd + GNU tar are preinstalled on both github-hosted and
|
||||
# blacksmith ubuntu-2404. See actions/upload-artifact#36, actions/toolkit#1533.
|
||||
- name: Pack CLI dist into tar.zst
|
||||
run: |
|
||||
find packages/opencode/dist -name '*.map' -delete
|
||||
tar --zstd -cf /tmp/kilo-cli.tar.zst -C packages/opencode/dist .
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: kilo-cli
|
||||
path: packages/opencode/dist
|
||||
path: /tmp/kilo-cli.tar.zst
|
||||
archive: false
|
||||
# kilocode_change end
|
||||
|
||||
outputs:
|
||||
version: ${{ needs.version.outputs.version }}
|
||||
@@ -99,11 +123,11 @@ jobs:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6 # kilocode_change
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6 # kilocode_change
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
@@ -111,11 +135,18 @@ jobs:
|
||||
- name: Install @vscode/vsce
|
||||
run: bun install -g @vscode/vsce
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
# kilocode_change start - download into /tmp and extract the tar.zst packed by build-cli
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: kilo-cli
|
||||
path: packages/opencode/dist
|
||||
name: kilo-cli.tar.zst
|
||||
path: /tmp
|
||||
skip-decompress: true
|
||||
|
||||
- name: Unpack CLI dist
|
||||
run: |
|
||||
mkdir -p packages/opencode/dist
|
||||
tar --zstd -xf /tmp/kilo-cli.tar.zst -C packages/opencode/dist
|
||||
# kilocode_change end
|
||||
- name: Build VSIX packages
|
||||
run: bun script/build.ts
|
||||
working-directory: ./packages/kilo-vscode
|
||||
@@ -125,215 +156,38 @@ jobs:
|
||||
KILO_PRE_RELEASE: ${{ inputs.pre_release }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v7 # kilocode_change
|
||||
with:
|
||||
name: kilo-vscode
|
||||
path: packages/kilo-vscode/out
|
||||
|
||||
# build-tauri:
|
||||
# needs:
|
||||
# - build-cli
|
||||
# - version
|
||||
# continue-on-error: false
|
||||
# strategy:
|
||||
# fail-fast: false
|
||||
# matrix:
|
||||
# settings:
|
||||
# - host: macos-latest
|
||||
# target: x86_64-apple-darwin
|
||||
# - host: macos-latest
|
||||
# target: aarch64-apple-darwin
|
||||
# - host: blacksmith-4vcpu-windows-2025
|
||||
# target: x86_64-pc-windows-msvc
|
||||
# - host: blacksmith-4vcpu-ubuntu-2404
|
||||
# target: x86_64-unknown-linux-gnu
|
||||
# - host: blacksmith-8vcpu-ubuntu-2404-arm
|
||||
# target: aarch64-unknown-linux-gnu
|
||||
# runs-on: ${{ matrix.settings.host }}
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
# with:
|
||||
# fetch-tags: true
|
||||
|
||||
# - uses: apple-actions/import-codesign-certs@v2
|
||||
# if: ${{ runner.os == 'macOS' }}
|
||||
# with:
|
||||
# keychain: build
|
||||
# p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
# p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
|
||||
# - name: Verify Certificate
|
||||
# if: ${{ runner.os == 'macOS' }}
|
||||
# run: |
|
||||
# CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
|
||||
# CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
# echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||
# echo "Certificate imported."
|
||||
|
||||
# - name: Setup Apple API Key
|
||||
# if: ${{ runner.os == 'macOS' }}
|
||||
# run: |
|
||||
# echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
||||
|
||||
# - uses: ./.github/actions/setup-bun
|
||||
|
||||
# - name: Cache apt packages
|
||||
# if: contains(matrix.settings.host, 'ubuntu')
|
||||
# uses: actions/cache@v4
|
||||
# with:
|
||||
# path: /var/cache/apt/archives
|
||||
# key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-${{ hashFiles('.github/workflows/publish.yml') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-${{ matrix.settings.target }}-apt-
|
||||
|
||||
# - name: install dependencies (ubuntu only)
|
||||
# if: contains(matrix.settings.host, 'ubuntu')
|
||||
# run: |
|
||||
# sudo apt-get update
|
||||
# sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
# - name: install Rust stable
|
||||
# uses: dtolnay/rust-toolchain@stable
|
||||
# with:
|
||||
# targets: ${{ matrix.settings.target }}
|
||||
|
||||
# - uses: Swatinem/rust-cache@v2
|
||||
# with:
|
||||
# workspaces: packages/desktop/src-tauri
|
||||
# shared-key: ${{ matrix.settings.target }}
|
||||
|
||||
# - name: Prepare
|
||||
# run: |
|
||||
# cd packages/desktop
|
||||
# bun ./scripts/prepare.ts
|
||||
# env:
|
||||
# KILO_VERSION: ${{ needs.version.outputs.version }}
|
||||
# GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
# RUST_TARGET: ${{ matrix.settings.target }}
|
||||
# GH_TOKEN: ${{ github.token }}
|
||||
# GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
|
||||
# # Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
||||
# - name: Install tauri-cli from portable appimage branch
|
||||
# if: contains(matrix.settings.host, 'ubuntu')
|
||||
# run: |
|
||||
# cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force
|
||||
# echo "Installed tauri-cli version:"
|
||||
# cargo tauri --version
|
||||
|
||||
# - name: Build and upload artifacts
|
||||
# uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||
# timeout-minutes: 60
|
||||
# with:
|
||||
# projectPath: packages/desktop
|
||||
# uploadWorkflowArtifacts: true
|
||||
# tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
||||
# args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
|
||||
# updaterJsonPreferNsis: true
|
||||
# releaseId: ${{ needs.version.outputs.release }}
|
||||
# tagName: ${{ needs.version.outputs.tag }}
|
||||
# releaseDraft: true
|
||||
# releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
|
||||
# TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
# TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
# APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
# APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
# APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
# APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
# APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
# APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||
|
||||
# kilocode_change start
|
||||
# Smoke test disabled as a release gate due to infrastructure issues.
|
||||
# The job is skipped via `if: false` so it no longer blocks publishing.
|
||||
# Re-enable by restoring the original `if:` condition and uncommenting
|
||||
# `- smoke-test` in the publish job's `needs` list below.
|
||||
# Run smoke tests against CLI assets uploaded to the draft GitHub release
|
||||
# before publishing the release and package artifacts.
|
||||
smoke-test:
|
||||
name: Smoke Test (pre-publish gate) [DISABLED]
|
||||
name: Smoke Test (pre-publish gate)
|
||||
needs:
|
||||
- version
|
||||
- build-cli
|
||||
if: false # was: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Trigger kilo-bench smoke test
|
||||
id: trigger
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.BENCH_GITHUB_TOKEN }}
|
||||
run: |
|
||||
BEFORE=$(date -u -d '60 seconds ago' +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
gh api repos/Kilo-Org/kilo-bench/dispatches \
|
||||
--method POST \
|
||||
-f event_type=smoke-test \
|
||||
-f 'client_payload[release_tag]=${{ needs.version.outputs.tag }}' \
|
||||
-f 'client_payload[source_run_id]=${{ github.run_id }}'
|
||||
|
||||
# Poll for the run created after our dispatch timestamp.
|
||||
# The dispatch API returns no run ID, so we query by created time
|
||||
# and pick the oldest match to avoid grabbing an unrelated run.
|
||||
echo "Waiting for smoke-test run to appear (dispatched after $BEFORE)..."
|
||||
for attempt in $(seq 1 30); do
|
||||
RUN_ID=$(gh api \
|
||||
"repos/Kilo-Org/kilo-bench/actions/workflows/smoke-test.yml/runs?event=repository_dispatch&created=>=$BEFORE" \
|
||||
--jq '.workflow_runs | sort_by(.created_at) | .[0].id // empty')
|
||||
|
||||
if [[ -n "$RUN_ID" && "$RUN_ID" != "null" ]]; then
|
||||
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::Smoke test run: https://github.com/Kilo-Org/kilo-bench/actions/runs/$RUN_ID"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo " attempt $attempt: run not yet registered, retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo "::error::Smoke test run did not appear within 5 minutes after dispatch."
|
||||
exit 1
|
||||
|
||||
- name: Wait for smoke test to complete
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.BENCH_GITHUB_TOKEN }}
|
||||
run: |
|
||||
RUN_ID="${{ steps.trigger.outputs.run_id }}"
|
||||
echo "Waiting for run $RUN_ID..."
|
||||
|
||||
for i in $(seq 1 60); do
|
||||
CONCLUSION=$(gh run view "$RUN_ID" \
|
||||
--repo Kilo-Org/kilo-bench \
|
||||
--json conclusion \
|
||||
--jq '.conclusion')
|
||||
|
||||
echo " attempt $i: $CONCLUSION"
|
||||
|
||||
if [[ "$CONCLUSION" == "success" ]]; then
|
||||
echo "::notice::Smoke test passed."
|
||||
exit 0
|
||||
elif [[ "$CONCLUSION" != "null" && "$CONCLUSION" != "" ]]; then
|
||||
echo "::error::Smoke test failed with conclusion: $CONCLUSION"
|
||||
echo "See: https://github.com/Kilo-Org/kilo-bench/actions/runs/$RUN_ID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 30
|
||||
done
|
||||
|
||||
echo "::error::Smoke test did not complete within 30 minutes."
|
||||
exit 1
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
uses: ./.github/workflows/smoke-test.yml
|
||||
with:
|
||||
cli_version: ${{ needs.version.outputs.version }}
|
||||
secrets: inherit
|
||||
# kilocode_change end
|
||||
publish:
|
||||
needs:
|
||||
- version
|
||||
- build-cli
|
||||
- build-vscode
|
||||
# - smoke-test # disabled: infrastructure issues (see smoke-test job comment)
|
||||
# - build-tauri
|
||||
- smoke-test
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6 # kilocode_change
|
||||
# kilocode_change start
|
||||
with:
|
||||
persist-credentials: false
|
||||
# kilocode_change end
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
@@ -350,7 +204,7 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6 # kilocode_change
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
@@ -367,18 +221,25 @@ jobs:
|
||||
kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }}
|
||||
# kilocode_change end
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
# kilocode_change start - download into /tmp and extract the tar.zst packed by build-cli
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: kilo-cli
|
||||
path: packages/opencode/dist
|
||||
name: kilo-cli.tar.zst
|
||||
path: /tmp
|
||||
skip-decompress: true
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- name: Unpack CLI dist
|
||||
run: |
|
||||
mkdir -p packages/opencode/dist
|
||||
tar --zstd -xf /tmp/kilo-cli.tar.zst -C packages/opencode/dist
|
||||
# kilocode_change end
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: kilo-vscode
|
||||
path: packages/kilo-vscode/out
|
||||
|
||||
- name: Cache apt packages (AUR)
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5 # kilocode_change
|
||||
with:
|
||||
path: /var/cache/apt/archives
|
||||
key: ${{ runner.os }}-apt-aur-${{ hashFiles('.github/workflows/publish.yml') }}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# kilocode_change - new file
|
||||
# Standalone smoke test — checks out kilo-bench and runs a small set of
|
||||
# Harbor eval tasks against the latest published CLI (or a specific version).
|
||||
#
|
||||
# Tasks: hello-world (~$0.01), log-summary-date-ranges (~$0.13)
|
||||
# Expected total: < $0.50, < 15 min wall clock
|
||||
#
|
||||
# Triggers:
|
||||
# - workflow_dispatch: manually from Actions tab (optionally pass a CLI version)
|
||||
# - workflow_call: from publish.yml after draft release assets are uploaded
|
||||
#
|
||||
# Required secrets:
|
||||
# KILO_API_KEY — Kilo Gateway key
|
||||
# KILO_ORG_ID — Kilo org ID
|
||||
# BENCH_GITHUB_TOKEN — PAT with contents:read on Kilo-Org/kilo-bench (private repo)
|
||||
|
||||
name: smoke-test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
cli_version:
|
||||
description: "CLI version to test (e.g. 7.0.36). Leave blank for latest npm release."
|
||||
required: false
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
cli_version:
|
||||
description: "CLI version to test from draft release assets."
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: smoke-test
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: write # needed to read draft release assets when called from publish.yml
|
||||
|
||||
jobs:
|
||||
smoke-test:
|
||||
name: Smoke Test
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
KILO_API_KEY: ${{ secrets.KILO_API_KEY }}
|
||||
KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }}
|
||||
steps:
|
||||
- name: Checkout kilo-bench
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
repository: Kilo-Org/kilo-bench
|
||||
token: ${{ secrets.BENCH_GITHUB_TOKEN }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.13
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --no-dev
|
||||
|
||||
- name: Validate API key
|
||||
run: |
|
||||
if [[ -z "$KILO_API_KEY" ]]; then
|
||||
echo "::error::KILO_API_KEY secret is not set."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Resolve CLI asset URL
|
||||
id: cli
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
VERSION="${{ inputs.cli_version }}"
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "cli_url=" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::Testing latest CLI from npm"
|
||||
exit 0
|
||||
fi
|
||||
# Resolve the release asset via the API so this works for draft
|
||||
# releases too (browser /releases/download/... URLs 404 on drafts).
|
||||
# The installer passes Accept: application/octet-stream + bearer
|
||||
# auth, which is what the API asset endpoint expects.
|
||||
# The tag_name filter yields at most one release and one asset,
|
||||
# so --jq returns a single URL without needing head/pipefail games.
|
||||
URL=$(gh api "repos/${{ github.repository }}/releases" \
|
||||
--jq ".[] | select(.tag_name == \"v${VERSION}\") | .assets[] | select(.name == \"kilo-linux-x64.tar.gz\") | .url")
|
||||
if [[ -z "$URL" ]]; then
|
||||
echo "::error::asset kilo-linux-x64.tar.gz not found for v${VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
echo "cli_url=$URL" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::Testing CLI v${VERSION} via asset API: $URL"
|
||||
|
||||
# Harbor's default agent-setup timeout is 360s. The hello-world container
|
||||
# (FROM ubuntu:24.04) needs apt-get update + apt-get install + a NodeSource
|
||||
# curl|bash + apt install nodejs before the CLI even downloads, and on
|
||||
# the Blacksmith runners that occasionally pushes past 6 min when an apt
|
||||
# mirror or NodeSource cdn is slow, killing the run with
|
||||
# AgentSetupTimeoutError. Doubling the multiplier to 2 gives enough
|
||||
# headroom for transient mirror/cdn slowness while still finishing well
|
||||
# under timeout-minutes. Note: --timeout-multiplier scales every timeout
|
||||
# uniformly (setup, agent, verifier, env build), but they're all upper
|
||||
# limits so this is harmless.
|
||||
- name: Run smoke test — hello-world
|
||||
env:
|
||||
KILO_CLI_URL: ${{ steps.cli.outputs.cli_url }}
|
||||
KILO_CLI_GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
./scripts/run_eval.sh \
|
||||
-m kilo/anthropic/claude-sonnet-4.6 \
|
||||
-d hello-world \
|
||||
--job-name smoke-test-hello-world \
|
||||
--timeout-multiplier 2
|
||||
|
||||
- name: Run smoke test — log-summary-date-ranges
|
||||
env:
|
||||
KILO_CLI_URL: ${{ steps.cli.outputs.cli_url }}
|
||||
KILO_CLI_GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
./scripts/run_eval.sh \
|
||||
-m kilo/anthropic/claude-sonnet-4.6 \
|
||||
-d terminal-bench-sample \
|
||||
-t "log-summary-date-ranges" \
|
||||
--job-name smoke-test-log-summary \
|
||||
--timeout-multiplier 2
|
||||
|
||||
- name: Validate results
|
||||
run: python3 scripts/validate_smoke_test.py jobs/smoke-test-*/
|
||||
|
||||
# Also upload the agent setup logs (stdout/stderr/return-code from the
|
||||
# CLI install script) so we can see exactly which step stalls when the
|
||||
# next AgentSetupTimeoutError happens. These come from the install
|
||||
# script in kilo-bench, which uses `set -euo pipefail` (no `set -x`)
|
||||
# and never echoes auth tokens or API keys, so the captured output is
|
||||
# safe to upload.
|
||||
- name: Upload results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7 # kilocode_change
|
||||
with:
|
||||
name: smoke-test-results
|
||||
path: |
|
||||
jobs/smoke-test-*/**/result.json
|
||||
jobs/smoke-test-*/**/trajectory.json
|
||||
jobs/smoke-test-*/**/agent/setup/*.txt
|
||||
retention-days: 30
|
||||
if-no-files-found: warn
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6 # kilocode_change
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
name: stale-issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *" # Daily at 1:30 AM
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DAYS_BEFORE_ISSUE_CLOSE: 90
|
||||
DAYS_BEFORE_PR_CLOSE: 30
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
days-before-issue-stale: ${{ env.DAYS_BEFORE_ISSUE_CLOSE }}
|
||||
days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_CLOSE }}
|
||||
days-before-issue-close: 0
|
||||
days-before-pr-close: 0
|
||||
close-issue-message: |
|
||||
Closing this one out since it's been inactive for a while — not because it's a bad issue, just that older items tend to lose context over time and we'd rather start fresh if this is still a problem.
|
||||
|
||||
Please feel free to reopen (or open a new issue) if you're still running into this. We're happy to take another look! 🙏
|
||||
close-pr-message: |
|
||||
Closing this one out since it's been inactive for a while — not because it's a bad PR, just that older items tend to lose context over time and we'd rather start fresh if this is still relevant.
|
||||
|
||||
Please feel free to reopen (or open a new PR) if you're still working on this. We're happy to take another look! 🙏
|
||||
operations-per-run: 200
|
||||
@@ -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
|
||||
@@ -3,16 +3,19 @@ name: test-vscode
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths:
|
||||
- "packages/kilo-vscode/**"
|
||||
- "packages/ui/**"
|
||||
- "packages/kilo-ui/**"
|
||||
- ".github/workflows/test-vscode.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/kilo-vscode/**"
|
||||
- "packages/ui/**"
|
||||
- "packages/kilo-ui/**"
|
||||
- ".github/workflows/test-vscode.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -24,7 +27,7 @@ jobs:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -35,6 +38,10 @@ jobs:
|
||||
working-directory: packages/kilo-vscode
|
||||
run: bun run test:unit
|
||||
|
||||
- name: Check lint (ESLint)
|
||||
working-directory: packages/kilo-vscode
|
||||
run: bun run lint
|
||||
|
||||
- name: Check formatting (prettier)
|
||||
working-directory: packages/kilo-vscode
|
||||
run: bun run format:check
|
||||
|
||||
+51
-58
@@ -6,6 +6,20 @@ on:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# Keep every run on main so cancelled checks do not pollute the default branch
|
||||
# commit history. PRs and other branches still share a group and cancel stale runs.
|
||||
group: ${{ case(github.ref == 'refs/heads/main', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
name: unit (${{ matrix.settings.name }})
|
||||
@@ -14,86 +28,65 @@ jobs:
|
||||
matrix:
|
||||
settings:
|
||||
- name: linux
|
||||
host: blacksmith-4vcpu-ubuntu-2404
|
||||
host: blacksmith-4vcpu-ubuntu-2404 # kilocode_change
|
||||
- name: windows
|
||||
host: blacksmith-4vcpu-windows-2025
|
||||
host: blacksmith-4vcpu-windows-2025 # kilocode_change
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6 # kilocode_change
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config --global user.email "bot@opencode.ai"
|
||||
git config --global user.name "opencode"
|
||||
git config --global user.email "kilo-maintainer[bot]@users.noreply.github.com"
|
||||
git config --global user.name "kilo-maintainer[bot]"
|
||||
|
||||
- name: Cache Turbo
|
||||
uses: actions/cache@v5 # kilocode_change
|
||||
with:
|
||||
path: node_modules/.cache/turbo
|
||||
key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-
|
||||
turbo-${{ runner.os }}-
|
||||
|
||||
- name: Run unit tests
|
||||
run: bun turbo test
|
||||
|
||||
e2e:
|
||||
# kilocode_change - disabled: packages/app is not actively maintained
|
||||
if: false
|
||||
name: e2e (${{ matrix.settings.name }})
|
||||
needs: unit
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- name: linux
|
||||
host: blacksmith-4vcpu-ubuntu-2404
|
||||
playwright: bunx playwright install --with-deps
|
||||
- name: windows
|
||||
host: blacksmith-4vcpu-windows-2025
|
||||
playwright: bunx playwright install
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
env:
|
||||
PLAYWRIGHT_BROWSERS_PATH: 0
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install Playwright browsers
|
||||
working-directory: packages/app
|
||||
run: ${{ matrix.settings.playwright }}
|
||||
|
||||
- name: Run app e2e tests
|
||||
run: bun --cwd packages/app test:e2e:local
|
||||
run: bun turbo test:ci
|
||||
env:
|
||||
CI: true
|
||||
# kilocode_change start
|
||||
KILO_API_KEY: ${{ secrets.KILO_API_KEY }}
|
||||
KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }}
|
||||
KILO_DISABLE_SHARE: "true"
|
||||
KILO_DISABLE_SESSION_INGEST: "true"
|
||||
# kilocode_change end
|
||||
timeout-minutes: 30
|
||||
KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
|
||||
|
||||
- name: Upload Playwright artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Publish unit reports
|
||||
if: always()
|
||||
uses: mikepenz/action-junit-report@v6
|
||||
with:
|
||||
name: playwright-${{ matrix.settings.name }}-${{ github.run_attempt }}
|
||||
report_paths: packages/*/.artifacts/unit/junit.xml
|
||||
check_name: "unit results (${{ matrix.settings.name }})"
|
||||
detailed_summary: true
|
||||
include_time_in_summary: true
|
||||
fail_on_failure: false
|
||||
|
||||
- name: Upload unit artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7 # kilocode_change
|
||||
with:
|
||||
name: unit-${{ matrix.settings.name }}-${{ github.run_attempt }}
|
||||
include-hidden-files: true
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
path: |
|
||||
packages/app/e2e/test-results
|
||||
packages/app/e2e/playwright-report
|
||||
path: packages/*/.artifacts/unit/junit.xml
|
||||
|
||||
required:
|
||||
name: test (linux)
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
matched: ${{ steps.filter.outputs.matched }}
|
||||
is_fork: ${{ steps.fork-check.outputs.is_fork }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6 # kilocode_change
|
||||
- uses: Kilo-Org/paths-filter@master
|
||||
id: filter
|
||||
with:
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- "packages/kilo-vscode/.storybook/**"
|
||||
- "packages/kilo-vscode/tests/visual-regression*"
|
||||
- "packages/kilo-vscode/tests/permission-dock-dropdown*"
|
||||
- "packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots/**"
|
||||
- "packages/kilo-docs/public/img/screenshot-tests/**"
|
||||
- ".github/workflows/visual-regression.yml"
|
||||
- name: Check if PR is from a fork
|
||||
id: fork-check
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout (internal)
|
||||
if: needs.check-paths.outputs.is_fork != 'true'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
lfs: true
|
||||
token: ${{ secrets.BOT_PAT }}
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
- name: Checkout (fork)
|
||||
if: needs.check-paths.outputs.is_fork == 'true'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
bun-version: latest
|
||||
|
||||
- name: Cache Bun modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5 # kilocode_change
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: bun-${{ hashFiles('bun.lock') }}
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
id: playwright-cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5 # kilocode_change
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ hashFiles('packages/kilo-ui/package.json') }}
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
|
||||
- name: Cache Storybook build
|
||||
id: storybook-cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5 # kilocode_change
|
||||
with:
|
||||
path: packages/kilo-ui/storybook-static
|
||||
key: storybook-${{ hashFiles('packages/kilo-ui/src/**', 'packages/kilo-ui/.storybook/**', 'packages/ui/src/**', 'packages/kilo-ui/package.json') }}
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
const idx = JSON.parse(fs.readFileSync('storybook-static/index.json', 'utf8'));
|
||||
const stories = Object.values(idx.entries || idx.stories || {}).filter(s => s.id && !s.id.endsWith('--docs'));
|
||||
const expected = new Set(stories.map(s => { const [c,v] = s.id.split('--'); return c + '/' + v + '-chromium-linux.png'; }));
|
||||
const dir = 'tests/visual-regression.spec.ts-snapshots';
|
||||
const dir = '../kilo-docs/public/img/screenshot-tests/kilo-ui/visual-regression';
|
||||
if (!fs.existsSync(dir)) process.exit(0);
|
||||
for (const component of fs.readdirSync(dir)) {
|
||||
const cdir = path.join(dir, component);
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
- name: Check for baseline changes (fork PRs)
|
||||
if: needs.check-paths.outputs.is_fork == 'true'
|
||||
run: |
|
||||
git add packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/
|
||||
git add packages/kilo-docs/public/img/screenshot-tests/kilo-ui/
|
||||
if git diff --cached --quiet; then
|
||||
echo "No visual regression detected."
|
||||
else
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
- name: Fail if baselines still changing after auto-update
|
||||
if: needs.check-paths.outputs.is_fork != 'true' && steps.check-baseline-commit.outputs.is_baseline_update == 'true'
|
||||
run: |
|
||||
git add packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/
|
||||
git add packages/kilo-docs/public/img/screenshot-tests/kilo-ui/
|
||||
if git diff --cached --quiet; then
|
||||
echo "Baselines are stable after auto-update."
|
||||
else
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/
|
||||
git add packages/kilo-docs/public/img/screenshot-tests/kilo-ui/
|
||||
if git diff --cached --quiet; then
|
||||
echo "No new baselines — nothing to commit."
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
|
||||
- name: Upload test results on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7 # kilocode_change
|
||||
with:
|
||||
name: visual-regression-results
|
||||
path: packages/kilo-ui/test-results/
|
||||
@@ -211,7 +211,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout (internal)
|
||||
if: needs.check-paths.outputs.is_fork != 'true'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
lfs: true
|
||||
token: ${{ secrets.BOT_PAT }}
|
||||
@@ -219,7 +219,7 @@ jobs:
|
||||
|
||||
- name: Checkout (fork)
|
||||
if: needs.check-paths.outputs.is_fork == 'true'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6 # kilocode_change
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
@@ -241,7 +241,7 @@ jobs:
|
||||
bun-version: latest
|
||||
|
||||
- name: Cache Bun modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5 # kilocode_change
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: bun-${{ hashFiles('bun.lock') }}
|
||||
@@ -251,7 +251,7 @@ jobs:
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
id: playwright-cache-vscode
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5 # kilocode_change
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-vscode-${{ hashFiles('packages/kilo-vscode/package.json') }}
|
||||
@@ -267,7 +267,7 @@ jobs:
|
||||
|
||||
- name: Cache Storybook build
|
||||
id: storybook-cache-vscode
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5 # kilocode_change
|
||||
with:
|
||||
path: packages/kilo-vscode/storybook-static
|
||||
key: storybook-vscode-${{ hashFiles('packages/kilo-vscode/webview-ui/src/**', 'packages/kilo-vscode/.storybook/**', 'packages/kilo-ui/src/**', 'packages/ui/src/**', 'packages/kilo-vscode/package.json') }}
|
||||
@@ -291,7 +291,7 @@ jobs:
|
||||
const idx = JSON.parse(fs.readFileSync('storybook-static/index.json', 'utf8'));
|
||||
const stories = Object.values(idx.entries || idx.stories || {}).filter(s => s.id && !s.id.endsWith('--docs'));
|
||||
const expected = new Set(stories.map(s => { const [c,v] = s.id.split('--'); return c + '/' + v + '-chromium-linux.png'; }));
|
||||
const dir = 'tests/visual-regression.spec.ts-snapshots';
|
||||
const dir = '../kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression';
|
||||
if (!fs.existsSync(dir)) process.exit(0);
|
||||
for (const component of fs.readdirSync(dir)) {
|
||||
const cdir = path.join(dir, component);
|
||||
@@ -307,7 +307,7 @@ jobs:
|
||||
- name: Remove stale baselines for permission-dock-dropdown tests
|
||||
run: |
|
||||
node -e "
|
||||
const fs = require('fs'), path = require('path');
|
||||
const fs = require('fs');
|
||||
const specPath = 'tests/permission-dock-dropdown.spec.ts';
|
||||
const expected = new Set();
|
||||
if (fs.existsSync(specPath)) {
|
||||
@@ -316,20 +316,16 @@ jobs:
|
||||
let m;
|
||||
while ((m = re.exec(spec)) !== null) {
|
||||
const parts = m[1].split(',').map(s => s.trim().replace(/[\"']/g, ''));
|
||||
expected.add(parts.join('/').replace(/\.png$/, '-chromium-linux.png'));
|
||||
const file = parts[parts.length - 1];
|
||||
if (file) expected.add(file.replace(/\.png$/, '-chromium-linux.png'));
|
||||
}
|
||||
}
|
||||
const dir = 'tests/permission-dock-dropdown.spec.ts-snapshots';
|
||||
const dir = '../kilo-docs/public/img/screenshot-tests/kilo-vscode/permission-dock-dropdown';
|
||||
if (!fs.existsSync(dir)) process.exit(0);
|
||||
for (const sub of fs.readdirSync(dir)) {
|
||||
const sdir = path.join(dir, sub);
|
||||
if (!fs.statSync(sdir).isDirectory()) continue;
|
||||
for (const file of fs.readdirSync(sdir)) {
|
||||
const rel = sub + '/' + file;
|
||||
if (file.endsWith('.png') && !expected.has(rel)) {
|
||||
console.log('Removing stale baseline:', rel);
|
||||
fs.unlinkSync(path.join(sdir, file));
|
||||
}
|
||||
for (const file of fs.readdirSync(dir)) {
|
||||
if (file.endsWith('.png') && !expected.has(file)) {
|
||||
console.log('Removing stale baseline:', file);
|
||||
fs.unlinkSync(dir + '/' + file);
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -338,8 +334,7 @@ jobs:
|
||||
- name: Check for baseline changes (fork PRs)
|
||||
if: needs.check-paths.outputs.is_fork == 'true'
|
||||
run: |
|
||||
git add packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/
|
||||
[ -d packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots ] && git add packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots/
|
||||
git add packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/
|
||||
if git diff --cached --quiet; then
|
||||
echo "No visual regression detected."
|
||||
else
|
||||
@@ -353,8 +348,7 @@ jobs:
|
||||
- name: Fail if baselines still changing after auto-update
|
||||
if: needs.check-paths.outputs.is_fork != 'true' && steps.check-baseline-commit-vscode.outputs.is_baseline_update == 'true'
|
||||
run: |
|
||||
git add packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/
|
||||
[ -d packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots ] && git add packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots/
|
||||
git add packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/
|
||||
if git diff --cached --quiet; then
|
||||
echo "Baselines are stable after auto-update."
|
||||
else
|
||||
@@ -373,8 +367,7 @@ jobs:
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/
|
||||
[ -d packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots ] && git add packages/kilo-vscode/tests/permission-dock-dropdown.spec.ts-snapshots/
|
||||
git add packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/
|
||||
if git diff --cached --quiet; then
|
||||
echo "No new baselines — nothing to commit."
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
@@ -393,7 +386,7 @@ jobs:
|
||||
|
||||
- name: Upload test results on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7 # kilocode_change
|
||||
with:
|
||||
name: visual-regression-vscode-results
|
||||
path: packages/kilo-vscode/test-results/
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
on:
|
||||
schedule:
|
||||
- cron: "7,37 * * * *" # every 30 min at :07 and :37
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: opencode-release-watch
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-release:
|
||||
if: vars.OPENCODE_WATCH_ENABLED != 'false'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- id: fetch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
data=$(gh release view --repo anomalyco/opencode --json tagName,name,url 2>/dev/null || echo '{}')
|
||||
tag=$(echo "$data" | jq -r '.tagName // empty')
|
||||
[ -z "$tag" ] && { echo "tag=" >> "$GITHUB_OUTPUT"; exit 0; }
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
echo "name=$(echo "$data" | jq -r '.name // .tagName')" >> "$GITHUB_OUTPUT"
|
||||
echo "url=$(echo "$data" | jq -r '.url')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- if: steps.fetch.outputs.tag != ''
|
||||
id: cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: .cache/last-seen
|
||||
key: last-seen-${{ steps.fetch.outputs.tag }}
|
||||
restore-keys: last-seen-
|
||||
|
||||
- if: steps.fetch.outputs.tag != '' && steps.cache.outputs.cache-hit != 'true'
|
||||
name: Trigger opencode sync
|
||||
env:
|
||||
URL: ${{ secrets.OPENCODE_WATCH_SYNC_URL }}
|
||||
SECRET: ${{ secrets.OPENCODE_WATCH_SYNC_SECRET }}
|
||||
run: |
|
||||
if [ -n "$URL" ] && [ -n "$SECRET" ]; then
|
||||
curl -sf -X POST "$URL" -H "x-sync-secret: $SECRET" -H "Content-Type: application/json" -d '{}' || true
|
||||
fi
|
||||
|
||||
- if: steps.fetch.outputs.tag != '' && steps.cache.outputs.cache-hit != 'true'
|
||||
name: Notify Slack
|
||||
uses: slackapi/slack-github-action@v2.0.0
|
||||
with:
|
||||
webhook: ${{ secrets.OPENCODE_WATCH_SLACK_WEBHOOK }}
|
||||
webhook-type: incoming-webhook
|
||||
payload: |
|
||||
name: "${{ steps.fetch.outputs.name }}"
|
||||
tag: "${{ steps.fetch.outputs.tag }}"
|
||||
url: "${{ steps.fetch.outputs.url }}"
|
||||
|
||||
- if: steps.fetch.outputs.tag != '' && steps.cache.outputs.cache-hit != 'true'
|
||||
name: Mark tag as seen
|
||||
run: mkdir -p .cache/last-seen && echo "${{ steps.fetch.outputs.tag }}" > .cache/last-seen/tag.txt
|
||||
|
||||
- if: success() && steps.fetch.outputs.tag != '' && steps.cache.outputs.cache-hit != 'true'
|
||||
name: Save seen-tag cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: .cache/last-seen
|
||||
key: last-seen-${{ steps.fetch.outputs.tag }}
|
||||
+23
-1
@@ -28,7 +28,7 @@ storybook-static
|
||||
/result
|
||||
refs
|
||||
Session.vim
|
||||
opencode.json
|
||||
/opencode.json
|
||||
a.out
|
||||
target
|
||||
.scripts
|
||||
@@ -36,9 +36,31 @@ target
|
||||
|
||||
# Local dev files
|
||||
opencode-dev
|
||||
UPCOMING_CHANGELOG.md
|
||||
logs/
|
||||
*.bun-build
|
||||
|
||||
# Telemetry ID
|
||||
telemetry-id
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# Kilo
|
||||
.kilo/.gitignore
|
||||
.kilo/package.json
|
||||
.kilo/package-lock.json
|
||||
.kilo/pnpm-lock.yaml
|
||||
.kilo/bun.lock
|
||||
.kilo/yarn.lock
|
||||
.kilo/node_modules
|
||||
.kilo/plans/*upstream-merge-report-*.md
|
||||
.kilocode/.gitignore
|
||||
.kilocode/package.json
|
||||
.kilocode/package-lock.json
|
||||
.kilocode/pnpm-lock.yaml
|
||||
.kilocode/bun.lock
|
||||
.kilocode/yarn.lock
|
||||
.kilocode/node_modules
|
||||
|
||||
# Test Artifacts
|
||||
packages/app/.artifacts/
|
||||
packages/opencode/.artifacts/
|
||||
|
||||
Generated
+12
@@ -4,12 +4,24 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<compositeConfiguration>
|
||||
<compositeBuild compositeDefinitionSource="SCRIPT">
|
||||
<builds>
|
||||
<build path="$PROJECT_DIR$/packages/kilo-jetbrains/build-tasks" name="build-tasks">
|
||||
<projects>
|
||||
<project path="$PROJECT_DIR$/packages/kilo-jetbrains/build-tasks" />
|
||||
</projects>
|
||||
</build>
|
||||
</builds>
|
||||
</compositeBuild>
|
||||
</compositeConfiguration>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/kilo-jetbrains" />
|
||||
<option name="gradleJvm" value="#JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$/packages/kilo-jetbrains" />
|
||||
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/backend" />
|
||||
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/build-tasks" />
|
||||
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/frontend" />
|
||||
<option value="$PROJECT_DIR$/packages/kilo-jetbrains/shared" />
|
||||
</set>
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
---
|
||||
description: Resolve upstream opencode merge conflicts interactively
|
||||
mode: primary
|
||||
permission:
|
||||
read: ask
|
||||
edit: ask
|
||||
webfetch: ask
|
||||
bash:
|
||||
"*": ask
|
||||
"git status *": allow
|
||||
"git log *": allow
|
||||
"git diff *": allow
|
||||
"git show *": allow
|
||||
"git ls-files *": allow
|
||||
"git ls-tree *": allow
|
||||
"git grep *": allow
|
||||
"git hash-object *": allow
|
||||
"git remote -v *": allow
|
||||
"git rev-parse *": allow
|
||||
"git merge-base *": allow
|
||||
"git show-ref *": allow
|
||||
"git worktree list": allow
|
||||
"git branch --show-current": allow
|
||||
"grep *": allow
|
||||
"rg *": allow
|
||||
"head *": allow
|
||||
"tail *": allow
|
||||
"cat *": allow
|
||||
"wc *": allow
|
||||
"ls *": allow
|
||||
"pwd *": allow
|
||||
"diff *": allow
|
||||
"gh pr view *": allow
|
||||
"gh run view *": allow
|
||||
"gh api \"repos/sst/opencode/commits/dev\" *": allow
|
||||
"axiom *": allow
|
||||
"bun test *": allow
|
||||
"bun run typecheck *": allow
|
||||
"bun run lint *": allow
|
||||
"bun run script/check-opencode-annotations.ts *": allow
|
||||
"script/upstream/find-conflict-markers.sh *": allow
|
||||
"./script/upstream/find-conflict-markers.sh *": allow
|
||||
---
|
||||
|
||||
Resolve the manual part of an upstream merge.
|
||||
|
||||
The user will provide the upstream version (for example `v1.1.50` or `1.1.50`)
|
||||
in their first message. If they don't, infer it from the current branch name,
|
||||
from `upstream-merge-report-<version>.md`, or from the newest relevant report
|
||||
file.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Inspect the current merge state
|
||||
|
||||
- `git status --short`
|
||||
- `git diff --name-only --diff-filter=U`
|
||||
- `upstream-merge-report-<version>.md` when present
|
||||
- `.worktrees/opencode-merge/auto-merge` for the automated merge snapshot when present
|
||||
|
||||
### 2. Read every conflicted file end-to-end before planning
|
||||
|
||||
Use `script/upstream/find-conflict-markers.sh <file>` to jump to each region,
|
||||
then read enough surrounding lines to understand the code — not just the
|
||||
conflict hunk. Specifically check:
|
||||
|
||||
- is this a plain 3-way on a single expression, or a structural refactor?
|
||||
- does upstream rename/move something that invalidates a HEAD-only declaration?
|
||||
- does a `kilocode_change` marker in HEAD encode a bug fix, a feature, or a
|
||||
defensive check?
|
||||
- is the conflicted block referenced by *non-conflicted* code elsewhere in the
|
||||
same file (imports, signatures, call sites) that will break if we drop it?
|
||||
|
||||
When HEAD includes a non-obvious Kilo-specific wrapper (e.g. a helper in
|
||||
`packages/opencode/src/kilocode/`), find out why it exists before deciding to
|
||||
keep or bypass it:
|
||||
|
||||
```bash
|
||||
git log --all --oneline -S "<symbol>" -- packages/opencode/src/kilocode/
|
||||
git log --all --oneline -- <kilo-file>
|
||||
```
|
||||
|
||||
Look at the commit message and any PR reference. "We wrote our own because of
|
||||
PR #NNNN" is a real constraint; "we wrote our own because of a typo" is not.
|
||||
|
||||
When upstream narrows an externally-visible compatibility list (models,
|
||||
providers, routes, config keys, file formats), verify the intent from upstream
|
||||
PRs, issues, release notes, or current docs before dropping entries. Treat
|
||||
silent list shrinkage during a refactor as suspicious until proven intentional.
|
||||
|
||||
### 3. Write a plan in chat and get approval
|
||||
|
||||
For every conflicted file (and any adjacent file the resolution forces you to
|
||||
touch — see §6) include:
|
||||
|
||||
- expected resolution kind: `hybrid`, `take-ours`, `take-theirs`, `regenerated`,
|
||||
`removed`, `renamed`, or `other`
|
||||
- risk level: `low`, `medium`, or `high`
|
||||
- one-sentence rationale (what Kilo behaviour is preserved, what upstream
|
||||
feature is adopted, what is dropped)
|
||||
- verification commands you expect to run (targeted tests, typecheck)
|
||||
|
||||
Group files by risk level. Ask the user which batch to start with. You can
|
||||
resolve an entire `low` batch in one pass if the user approves the batch, but
|
||||
resolve `medium` and `high` files one at a time.
|
||||
|
||||
**Do not resolve a file until the user has approved that file's (or batch's)
|
||||
strategy.**
|
||||
|
||||
### 4. Before every edit, explain reasoning before showing the diff
|
||||
|
||||
The user needs to review intent, not just the raw change. For each file, in
|
||||
order:
|
||||
|
||||
1. Show the conflict's surrounding context (10–30 lines around each conflict
|
||||
region, in chat).
|
||||
2. Explain what each of the three sides (HEAD, merge-base, upstream) is doing.
|
||||
3. State which Kilo behaviour must survive and why (reference PR numbers /
|
||||
`kilocode_change` comments when possible).
|
||||
4. State the resolution and why it is better than the alternatives.
|
||||
5. Then apply the edit. The tool will display the diff — the user only has to
|
||||
verify the diff matches the reasoning.
|
||||
|
||||
Do not lead with the diff. A diff without reasoning forces the user to
|
||||
reverse-engineer the decision.
|
||||
|
||||
### 5. Apply resolution rules
|
||||
|
||||
Reference worktrees when present:
|
||||
|
||||
- `.worktrees/opencode-merge/opencode` — pristine upstream tree
|
||||
- `.worktrees/opencode-merge/kilo-main` — Kilo base snapshot
|
||||
- `.worktrees/opencode-merge/auto-merge` — automated merge snapshot (original
|
||||
conflict reference)
|
||||
|
||||
Apply in order:
|
||||
|
||||
- prefer upstream code and architecture whenever compatible with Kilo behaviour
|
||||
- preserve Kilo-specific behaviour marked with `kilocode_change`
|
||||
- keep `kilocode_change` markers around Kilo-specific code in shared opencode
|
||||
files
|
||||
- when upstream refactors a region that HEAD had annotated with
|
||||
`kilocode_change`, **check whether the marker encodes a bug fix or a feature
|
||||
delta**. Bug fixes (missing `await`, defensive null-check, error capture)
|
||||
usually need to be re-applied on top of the upstream refactor. Example from
|
||||
v1.14.30: `Workspace.isSyncing` was missing an `await` — upstream's Effect
|
||||
refactor reintroduced the same bug, so we had to port the fix into the new
|
||||
`Effect.gen` block.
|
||||
- when a `take-theirs` drops a line that was the target of a Kilo pre-filter,
|
||||
the upstream line may be actively wrong for Kilo — e.g. an inner `continue`
|
||||
filter whose condition collides with an outer filter Kilo added. Re-read the
|
||||
surrounding 20 lines before committing to `take-theirs`.
|
||||
- if Kilo-specific code must be refactored to fit new upstream architecture,
|
||||
explain the refactor in the final summary
|
||||
- if upstream moved the relevant logic to another file, port the Kilo behaviour
|
||||
there and list both paths in the final summary. Verify the new file already
|
||||
carries the Kilo-renamed symbols (e.g. `x-kilo-directory`) by diffing against
|
||||
pristine upstream.
|
||||
- if upstream extracts shared policy into a helper, move Kilo-specific additions
|
||||
into the helper when possible instead of keeping a pre-check at the old call
|
||||
site. The extracted helper should stay the source of truth for all callers.
|
||||
- if upstream deleted a file, analyse whether the Kilo behaviour should be
|
||||
ported elsewhere or removed rather than restoring the deleted file
|
||||
- if tests fail only because upstream intentionally removed behaviour, remove
|
||||
or update the obsolete tests rather than adding the old file back
|
||||
- do not modify unrelated files
|
||||
|
||||
When removing code that existed in one side of a conflict, prefer
|
||||
**commenting it out with `kilocode_change` markers** over deletion when the
|
||||
surrounding structure (an `if`, a loop) still makes sense. That keeps the
|
||||
intent visible to the next merger. Example:
|
||||
|
||||
```ts
|
||||
} else if (input?.scope !== "project" && !Flag.KILO_EXPERIMENTAL_WORKSPACES) {
|
||||
// kilocode_change start - directory filtering handled by KiloSession.filters above
|
||||
// if (input?.directory) {
|
||||
// conditions.push(eq(SessionTable.directory, input.directory))
|
||||
// }
|
||||
// kilocode_change end
|
||||
}
|
||||
```
|
||||
|
||||
Use `TODO:` not `NOTE:` for follow-ups. `TODO` is searchable and implies an
|
||||
owner will act on it; `NOTE` reads as permanent commentary.
|
||||
|
||||
### 6. Look for adjacent files the conflict forces you to touch
|
||||
|
||||
Upstream restructures sometimes split one file into several (e.g. `permission.ts`
|
||||
→ `groups/permission.ts` + `handlers/permission.ts`). Only the *renamed* file
|
||||
shows up in `git diff --diff-filter=U`; the new sibling may need a Kilo feature
|
||||
ported in too. After resolving the flagged file, check:
|
||||
|
||||
- files that import from the resolved file — do they compile?
|
||||
- files at paths implied by new imports (e.g. `../middleware/*`, `./handlers/*`)
|
||||
- `kilocode_change` comments in the *auto-merge* snapshot that didn't end up in
|
||||
the working tree because the hosting file was renamed
|
||||
|
||||
Add any such files to the plan as `hybrid` or `take-ours` with the same
|
||||
approval flow.
|
||||
|
||||
### 6.5. Scan auto-merged files for latent bugs
|
||||
|
||||
Files not in `--diff-filter=U` merged without conflict markers but may still
|
||||
be broken. Check every auto-merged file for:
|
||||
|
||||
- **Duplicate declarations in the same scope.** If both sides added equivalent
|
||||
code independently, auto-merge keeps both. Grep touched functions for
|
||||
repeated identifiers before trusting the merge.
|
||||
- **Duplicate keys in config/manifest files.** If both sides added the same
|
||||
entry to a shared manifest (dependencies, scripts, workflow lists), the
|
||||
merged file may have the key twice. This often breaks install/setup before
|
||||
any test runs — a cheap early win to scan for.
|
||||
- **Orphaned imports and references.** A rename upstream may leave a Kilo
|
||||
callsite pointing at a now-missing export. Run full typecheck from the repo
|
||||
root; references that silently survived the merge surface there.
|
||||
- **Partial auto-merges.** Upstream may have refactored a region Kilo
|
||||
deliberately stubbed out (commented blocks, removed fallbacks). If the
|
||||
auto-merge pulled in references to names that only exist in the removed
|
||||
path, the file compiles upstream but breaks on Kilo.
|
||||
|
||||
### 7. Verify each resolution before moving on
|
||||
|
||||
- confirm `script/upstream/find-conflict-markers.sh <file>` prints nothing
|
||||
- read the final file region (the new shape after edit) and sanity-check imports
|
||||
- for apparently-unused symbols upstream introduced, `grep` the file and the
|
||||
rest of the package before deleting — they may be called from non-conflicted
|
||||
code elsewhere. Example: `isTheme` in `theme.tsx` looked unused at the
|
||||
resolution site but was called twice further down.
|
||||
- run the smallest relevant check (single `bun test` file, or `bun run
|
||||
typecheck` in the touched package)
|
||||
- summarise the exact resolution, tradeoff, and verification result in chat
|
||||
- ask the user to approve the resolved file before staging it or resolving the
|
||||
next one (for `medium` / `high`; `low` batches can be staged together)
|
||||
|
||||
### 8. Run the full checks once everything is resolved
|
||||
|
||||
- `git diff --name-only --diff-filter=U` returns empty
|
||||
- `bun run typecheck` from `packages/opencode/` (targeted) and from repo root
|
||||
(catches non-conflicted call-site breakage)
|
||||
- relevant targeted tests. Tests that hang or time out in an unrelated part of
|
||||
the graph may be pre-existing — note them, don't block the merge on them
|
||||
- `bun run script/check-opencode-annotations.ts` if `packages/opencode/` shared
|
||||
files changed. Note that this tool compares against the merge base via `HEAD`
|
||||
and will be silent until the merge commit lands
|
||||
- other CI guards that touched files imply (knip for `kilo-vscode/`,
|
||||
`check-kilocode-change`, source-links, visual regression)
|
||||
|
||||
### 9. Commit with the standard message
|
||||
|
||||
Per `script/upstream/README.md`:
|
||||
|
||||
```bash
|
||||
git commit -m "resolve merge conflicts"
|
||||
```
|
||||
|
||||
The default `git merge` auto-message (`Merge branch '…' into …`) is also fine,
|
||||
but `resolve merge conflicts` is the convention for these PRs.
|
||||
|
||||
### 9.5. Handle downstream API renames as separate commits
|
||||
|
||||
Upstream often renames exported APIs. The rename itself auto-merges cleanly in
|
||||
shared code, but the change cascades into Kilo-only files (kilocode tests,
|
||||
kilo-specific source, plugins) that still reference the old symbol. Those
|
||||
files don't appear in `--diff-filter=U` because their own content didn't
|
||||
conflict.
|
||||
|
||||
Keep the behavioural merge commit focused on resolution decisions. Land the
|
||||
cascade in one or more follow-up commits:
|
||||
|
||||
- after the merge commit, run full repo typecheck and collect every "cannot
|
||||
find name" / "property does not exist" error
|
||||
- bulk-rename with a mechanical transform when the rename is one-to-one
|
||||
- restructure or parameter-thread when upstream changed semantics, not just
|
||||
the name (e.g. moved a helper behind a dependency-injected surface, so
|
||||
callers now need the injected handle)
|
||||
- split large downstream refactors into their own commits with messages that
|
||||
name the rename
|
||||
|
||||
Reviewers can then skim the behavioural commit without untangling mechanical
|
||||
rename noise from merge decisions.
|
||||
|
||||
### 9.6. Handle upstream-added tests that diverge from Kilo
|
||||
|
||||
Upstream sometimes adds tests that encode design contracts Kilo intentionally
|
||||
breaks. These auto-merge cleanly and then fail. Three resolution patterns:
|
||||
|
||||
- **Rewrite the test** when the test is a contract assertion and Kilo has a
|
||||
different but equally valid contract. Invert or adjust the assertion with a
|
||||
`kilocode_change` marker explaining the divergence.
|
||||
- **Skip the test** when the test relies on patterns that Kilo has replaced
|
||||
(interception seams that are bypassed by dependency injection, fixture
|
||||
helpers bound to a removed API, assumptions about serialization shape that
|
||||
Kilo's extensions break). Mark with `kilocode_change` and a rationale
|
||||
explaining what would need to change for the test to run.
|
||||
- **Delete the test** when it covers functionality Kilo deliberately removed
|
||||
(fallback paths, deprecated endpoints, products Kilo doesn't ship). Note
|
||||
the deletion in the PR body.
|
||||
|
||||
Never silently delete; always leave a breadcrumb. A future reviewer should be
|
||||
able to understand why this one upstream test is treated differently.
|
||||
|
||||
### 10. Resync version strings in a separate commit
|
||||
|
||||
Upstream stamps its own version into shared files — notably
|
||||
`packages/extensions/zed/extension.toml` (version field + 5 Kilo-Org download
|
||||
URLs), and any `package.json` that upstream bumped in the same release window.
|
||||
After the merge this leaves parts of the tree pointing at upstream's version
|
||||
(e.g. `1.14.30`), whose release tag does not exist on Kilo's pipeline, so the
|
||||
Zed download URLs silently 404.
|
||||
|
||||
Fix this in a dedicated commit *after* `resolve merge conflicts`:
|
||||
|
||||
```bash
|
||||
bun run script/sync-versions.ts # uses root package.json version
|
||||
# or, to target an explicit version:
|
||||
bun run script/sync-versions.ts 7.2.41
|
||||
git add -A
|
||||
git commit -m "chore: resync versions after upstream merge"
|
||||
```
|
||||
|
||||
The script rewrites every top-level `"version"` in `package.json` files
|
||||
(excluding `node_modules`, hidden dirs, and `packages/kilo-jetbrains/` which
|
||||
tracks its own cadence), plus the Zed extension toml. It is idempotent — rerun
|
||||
it any time to rebase the version back onto Kilo main (useful during
|
||||
long-running upstream merges where `main` releases in the meantime).
|
||||
|
||||
Keeping this in its own commit makes reviewers' job easier: the merge commit
|
||||
only contains behavioural resolutions, and the version resync is a trivial
|
||||
diff they can skim in one glance.
|
||||
|
||||
### 11. Write the PR body
|
||||
|
||||
Structure the description so reviewers can skim:
|
||||
|
||||
- **Non-trivial merge decisions**: a short section per file (or group of
|
||||
related files) that required more than a mechanical `take-ours`/`take-theirs`.
|
||||
Focus on *what Kilo behaviour survived* and *what upstream features were
|
||||
adopted*. Link to Kilo PRs when a `kilocode_change` encodes a specific fix.
|
||||
- **Notable auto-merged changes**: new columns, new helper files, renamed
|
||||
middleware — anything reviewers should eyeball even though git didn't flag
|
||||
it.
|
||||
- **What to test**: explicit, scenario-level test steps for each non-trivial
|
||||
change. Don't list tests; list *user-visible behaviour* so a tester who
|
||||
doesn't read the diff can exercise it.
|
||||
- **CI guards to watch**: typecheck, knip, annotation check, visual regression.
|
||||
- **Follow-ups**: any `TODO:` you left in code, as a bullet list with links.
|
||||
|
||||
## User-approval checkpoints
|
||||
|
||||
Every manual merge decision requires explicit user approval **before applying**
|
||||
and **again after verification**. Be especially cautious when a decision is
|
||||
destructive, changes auth, billing, data deletion, public API compatibility,
|
||||
config schema behaviour, migrations, provider routing, or security posture.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- Auto-merged code can reference declarations that still live inside conflict
|
||||
blocks.
|
||||
- Related sibling files can need edits even when they are not listed as
|
||||
unmerged — especially after upstream structural splits.
|
||||
- `renamed` should be used only when behaviour moves to a different file.
|
||||
- Function signatures can drift across conflict boundaries (args added, return
|
||||
types widened). Grep for every call site before finalising.
|
||||
- Full-repo typecheck is the catch-all for non-conflicted call-site breakage.
|
||||
- Upstream can reintroduce bugs a Kilo `kilocode_change` had already fixed —
|
||||
during big refactors check every `kilocode_change` the refactor touched.
|
||||
- "Take-theirs" on an inner conditional is often wrong when Kilo added an outer
|
||||
pre-filter whose whole point was to widen what makes it to the inner block.
|
||||
- Apparently-unused upstream-added declarations may be called from
|
||||
non-conflicted code elsewhere. Grep before deleting.
|
||||
- Stricter DOM lib types (upstream TS upgrade) can surface latent casting
|
||||
issues around `WebSocket.send`, `Headers`, etc. — prefer narrowing the Kilo
|
||||
type over adding `any` casts.
|
||||
- Auto-merge can duplicate the same declaration twice in one scope when both
|
||||
sides added equivalent code independently. Silent for git, caught by
|
||||
typecheck. Same hazard for duplicated object keys in config/manifest files —
|
||||
those can break install before any test runs.
|
||||
- Kilo code may rely on ambient context (async-local storage, globally-set
|
||||
flags, process env) being populated at a lifecycle moment that upstream
|
||||
refactors away. If Kilo behaviour reads ambient state during init, forked
|
||||
work, or event handlers, check the refactor still establishes that state at
|
||||
the right time. Fix by restoring the ambient state, or by threading the
|
||||
needed value through explicitly.
|
||||
- Tests that intercept via process-global or module-global spies can become
|
||||
no-ops after upstream moves the intercepted code path through dependency
|
||||
injection. The production code no longer touches the spied symbol. Fixing
|
||||
the test usually means injecting a mock at the new seam rather than tweaking
|
||||
the spy.
|
||||
- When Kilo extends a shared data shape with extra optional fields, different
|
||||
serialization paths for that shape can diverge on whether missing values are
|
||||
omitted or emitted as null. Parity tests between two such paths break on
|
||||
every Kilo addition — audit the encoding assumption before adding fields.
|
||||
- Rule ordering in allowlist/permission evaluation is usually last-match-wins.
|
||||
Re-declaring a catch-all rule "for safety" in a later ruleset silently
|
||||
overrides more specific allow rules from an earlier ruleset. Treat a
|
||||
redundant catch-all as destructive, not defensive.
|
||||
- Upstream-added tests can encode a design contract Kilo deliberately breaks.
|
||||
The test passes upstream because upstream doesn't share Kilo's requirement.
|
||||
Decide between refactoring Kilo to match the upstream contract or rewriting
|
||||
the test to assert Kilo's divergent contract — with a `kilocode_change`
|
||||
marker explaining the divergence.
|
||||
- CI and local can show different test failures. Tests that read user-local
|
||||
state (home dir, global config, auth tokens) pass in one environment but
|
||||
fail in the other. A green local run does not imply green CI.
|
||||
- Dependency manifests and lockfiles move together. When the merge edits one,
|
||||
regenerate and commit the other in the same change — otherwise CI breaks on
|
||||
the follow-up setup step.
|
||||
@@ -0,0 +1 @@
|
||||
bun run extension
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: gh-issues
|
||||
description: Use when creating, triaging, or commenting on GitHub issues for the Kilo VS Code extension or JetBrains plugin via `gh`. Covers issue templates, project board assignment, title conventions, and required `gh` scopes.
|
||||
---
|
||||
|
||||
# GitHub Issues
|
||||
|
||||
Use this skill whenever you create or manage a GitHub issue with `gh` for either the VS Code extension or the JetBrains plugin.
|
||||
|
||||
## Templates
|
||||
|
||||
The repo defines issue templates in `.github/ISSUE_TEMPLATE/`. Pick the matching template instead of opening a blank issue:
|
||||
|
||||
| Template | When to use |
|
||||
|---|---|
|
||||
| `Bug report` (`bug-report.yml`) | Reproducible defects with steps, expected, and actual behavior |
|
||||
| `Feature Request` (`feature-request.yml`) | New capabilities, enhancements, or behavior changes |
|
||||
| `Question` (`question.yml`) | Usage or design questions that aren't obviously bugs or feature requests |
|
||||
|
||||
Pass the template title to `gh issue create --template`.
|
||||
|
||||
## Title Conventions
|
||||
|
||||
- Use a plain, descriptive title that reads cleanly as a standalone sentence.
|
||||
- Do not add platform-specific prefixes such as `[JetBrains]`, `[Jetbrains]`, `[JB]`, `[VS Code]`, `[VSCode]`, or similar. Routing happens through project boards, not the title.
|
||||
|
||||
## Project Boards
|
||||
|
||||
Every new issue must land on the correct project board:
|
||||
|
||||
| Surface | Project | URL |
|
||||
|---|---|---|
|
||||
| VS Code extension | `VS Code Extension` | https://github.com/orgs/Kilo-Org/projects/25 |
|
||||
| JetBrains plugin | `Jetbrains Plugin` | https://github.com/orgs/Kilo-Org/projects/39 |
|
||||
|
||||
Pass the project title to `gh issue create --project`.
|
||||
|
||||
## Recipes
|
||||
|
||||
Create a VS Code extension bug report and add it to the board:
|
||||
|
||||
```bash
|
||||
gh issue create \
|
||||
--template "Bug report" \
|
||||
--project "VS Code Extension" \
|
||||
--title "Sidebar chat fails to render after reload" \
|
||||
--body "..."
|
||||
```
|
||||
|
||||
Create a JetBrains feature request:
|
||||
|
||||
```bash
|
||||
gh issue create \
|
||||
--template "Feature Request" \
|
||||
--project "Jetbrains Plugin" \
|
||||
--title "Support Kotlin Multiplatform target detection" \
|
||||
--body "..."
|
||||
```
|
||||
|
||||
## Scope Errors
|
||||
|
||||
If `gh` reports a missing scope when assigning a project, refresh the auth token and retry:
|
||||
|
||||
```bash
|
||||
gh auth refresh -s project
|
||||
```
|
||||
|
||||
After the refresh succeeds, re-run the original `gh issue create` command. Do not fall back to creating the issue without the project — the board assignment is required.
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: kilocode-merge-minimizer
|
||||
description: Use when changing shared upstream-owned files, editing or reviewing `kilocode_change` markers, or moving additive Kilo-specific behavior into Kilo-owned code to reduce future merge conflicts.
|
||||
---
|
||||
|
||||
# Kilo Merge Minimizer
|
||||
|
||||
Use this skill whenever a normal development task touches shared upstream-owned code and includes Kilo-specific behavior, especially for marker cleanup, extraction work, or `kilocode_change` annotations.
|
||||
|
||||
Do not use this as the primary guide for upstream merge resolution. Upstream merges have their own instructions and should not duplicate that workflow here.
|
||||
|
||||
## Goal
|
||||
|
||||
Minimize Kilo's long-term diff against upstream OpenCode while preserving behavior.
|
||||
|
||||
Prefer this shape for Kilo-specific additions:
|
||||
|
||||
1. Shared upstream file contains only a minimal hook, import, call, registration, or config entry.
|
||||
2. Kilo-specific behavior lives in Kilo-owned code.
|
||||
3. Unavoidable shared-file changes have narrow `kilocode_change` markers.
|
||||
4. The annotation checker passes.
|
||||
|
||||
For changes to existing upstream behavior, prefer the smallest in-place shared-file diff with narrow markers. Do not move changed upstream logic into Kilo-owned code just to avoid textual conflicts, because that can create harder semantic merge conflicts.
|
||||
|
||||
## Core Rules
|
||||
|
||||
- Use `script/check-opencode-annotations.ts` as the source of truth for current shared scopes and exempt paths.
|
||||
- Use `script/upstream/fix-kilocode-markers.ts` for stale or broad markers, inspecting `--dry-run` output before applying changes.
|
||||
- Treat upstream-owned files as shared unless the checker or repo ownership rules exempt them.
|
||||
- Put Kilo-owned UI, CLI, runtime logic, and tests in Kilo-owned paths where practical.
|
||||
- Avoid adding Kilo business logic directly to shared files.
|
||||
- Keep shared-file edits as close as possible to upstream shape.
|
||||
- Do not change shared files unless the change is required for Kilo functionality, fixes a Kilo bug, or is a minimal targeted upstream-quality fix.
|
||||
- Do not create a large Kilo-only fork for a general upstream-quality improvement. Prefer a minimal targeted fix, or leave the broader change for upstream.
|
||||
- Do not duplicate upstream logic unless there is a concrete reason. If duplication is unavoidable, isolate the Kilo delta and keep the upstream dependency obvious.
|
||||
|
||||
## Shared File Structure
|
||||
|
||||
- Do not refactor, rename, split files, or extract helpers in shared files just to improve readability or make Kilo extraction cleaner.
|
||||
- Avoid structural changes that make upstream behavior harder to compare or hide semantic dependency on upstream code.
|
||||
|
||||
## Shared File Style
|
||||
|
||||
- Preserve upstream formatting and import style in shared files, even when it differs from Kilo style.
|
||||
- Put Kilo-only imports on separate marked lines instead of reorganizing upstream imports.
|
||||
|
||||
## Decision Rules
|
||||
|
||||
Extract Kilo logic when:
|
||||
|
||||
- The change is an additive Kilo feature or integration, not a modification of existing upstream behavior.
|
||||
- The shared-file change has meaningful Kilo-owned behavior, not just a tiny condition, import, registration, or field.
|
||||
- The code has loops, branching, error handling, async workflows, storage access, network calls, UI rendering, or telemetry.
|
||||
- The shared file can become a small orchestrator that calls Kilo helpers.
|
||||
- The Kilo code is independent enough that extraction will not hide future upstream fixes or behavior changes.
|
||||
|
||||
Keep the change inline when:
|
||||
|
||||
- The Kilo delta is a single field, import, call, simple condition, or small registry entry.
|
||||
- Extraction would reshape upstream code more than the Kilo change itself.
|
||||
- The change modifies an upstream algorithm, ordering, heuristic, control flow, or bug fix.
|
||||
- Extraction would duplicate upstream logic or hide semantic dependency on upstream behavior.
|
||||
- The Kilo helper closes over upstream-local state. Keep closure-scoped helpers inline and contiguous in one narrow marker block.
|
||||
- The shared file owns the only route table, enum, schema, switch, or registry where the hook must exist.
|
||||
- The change restores upstream shape or removes a stale Kilo divergence.
|
||||
|
||||
Always preserve upstream behavior order unless the Kilo behavior change is intentional and tested.
|
||||
|
||||
## Marker Rules
|
||||
|
||||
- Mark only Kilo-specific diff lines in shared upstream files.
|
||||
- Prefer inline markers for single-line changes: `const value = 42 // kilocode_change`.
|
||||
- Use block markers only for adjacent Kilo-specific lines:
|
||||
|
||||
```ts
|
||||
// kilocode_change start
|
||||
registerKiloFeature(app)
|
||||
// kilocode_change end
|
||||
```
|
||||
|
||||
- Use the file's native comment style, including JSX block comments inside JSX and `#` comments for YAML, TOML, and shell.
|
||||
- Do not add markers in checker-exempt Kilo-owned paths.
|
||||
- Remove stale markers when upstream already contains the behavior or when touching Kilo-owned files that still have old markers.
|
||||
- Use `// kilocode_change - new file` only for unavoidable new Kilo-specific files inside shared upstream paths.
|
||||
|
||||
## Tests
|
||||
|
||||
- Put Kilo-specific CLI/runtime tests in Kilo-owned test paths.
|
||||
- Move tests out of shared upstream test paths when the behavior under test is Kilo-specific.
|
||||
- Tests should cover the real failing path, not private or unstable APIs chosen only for convenience.
|
||||
- Do not add skip gates for required regression coverage.
|
||||
|
||||
## Verification
|
||||
|
||||
After editing shared files or marker comments, run:
|
||||
|
||||
```bash
|
||||
bun run script/check-opencode-annotations.ts
|
||||
```
|
||||
|
||||
If the PR uses a non-default comparison base, pass the correct base ref:
|
||||
|
||||
```bash
|
||||
bun run script/check-opencode-annotations.ts --base <base-ref>
|
||||
```
|
||||
|
||||
For stale or broad markers in one shared file, inspect the dry run before applying:
|
||||
|
||||
```bash
|
||||
bun run script/upstream/fix-kilocode-markers.ts <repo-relative-file> --dry-run
|
||||
```
|
||||
|
||||
Before finishing, confirm:
|
||||
|
||||
- Shared files contain minimal integration points only.
|
||||
- Kilo logic and tests live in Kilo-owned paths where practical.
|
||||
- Markers are narrow.
|
||||
- Stale markers are removed.
|
||||
- The annotation checker passed, or the reason it could not run is reported.
|
||||
@@ -21,15 +21,15 @@ The test runner at `tests/visual-regression.spec.ts` is fully automatic — it f
|
||||
|
||||
Stories live in `packages/kilo-vscode/webview-ui/src/stories/`. Existing files and their scope:
|
||||
|
||||
| File | Components covered |
|
||||
| --------------------------- | -------------------------------------------------------- |
|
||||
| `agent-manager.stories.tsx` | FileTree, DiffPanel, FullScreenDiffView, WorktreeItem |
|
||||
| `chat.stories.tsx` | ChatView, QuestionDock |
|
||||
| `composite.stories.tsx` | AssistantMessage with tool cards, permissions, questions |
|
||||
| `prompt-input.stories.tsx` | PromptInput (sidebar prompt bar) |
|
||||
| `settings.stories.tsx` | Settings panel, ProvidersTab |
|
||||
| `history.stories.tsx` | SessionList |
|
||||
| `shared.stories.tsx` | ModelSelector and shared controls |
|
||||
| File | Components covered |
|
||||
|---|---|
|
||||
| `agent-manager.stories.tsx` | FileTree, DiffPanel, FullScreenDiffView, WorktreeItem |
|
||||
| `chat.stories.tsx` | ChatView, QuestionDock |
|
||||
| `composite.stories.tsx` | AssistantMessage with tool cards, permissions, questions |
|
||||
| `prompt-input.stories.tsx` | PromptInput (sidebar prompt bar) |
|
||||
| `settings.stories.tsx` | Settings panel, ProvidersTab |
|
||||
| `history.stories.tsx` | SessionList |
|
||||
| `shared.stories.tsx` | ModelSelector and shared controls |
|
||||
|
||||
Add to an existing file if the component fits. Create a new file only for a genuinely new component area.
|
||||
|
||||
@@ -227,12 +227,12 @@ The title-slug is derived from the meta `title` (lowercased, slashes become hyph
|
||||
|
||||
Example mapping:
|
||||
|
||||
| Meta title | Export name | Snapshot path |
|
||||
| --------------------- | -------------------- | ----------------------------------------------------------- |
|
||||
| `"Chat"` | `ChatViewIdle` | `chat/chat-view-idle-chromium-linux.png` |
|
||||
| Meta title | Export name | Snapshot path |
|
||||
|---|---|---|
|
||||
| `"Chat"` | `ChatViewIdle` | `chat/chat-view-idle-chromium-linux.png` |
|
||||
| `"Composite/Webview"` | `GlobWithPermission` | `composite-webview/glob-with-permission-chromium-linux.png` |
|
||||
| `"Prompt Input"` | `Default200` | `prompt-input/default-200-chromium-linux.png` |
|
||||
| `"AgentManager"` | `WorktreeItemActive` | `agentmanager/worktree-item-active-chromium-linux.png` |
|
||||
| `"Prompt Input"` | `Default200` | `prompt-input/default-200-chromium-linux.png` |
|
||||
| `"AgentManager"` | `WorktreeItemActive` | `agentmanager/worktree-item-active-chromium-linux.png` |
|
||||
|
||||
# Reference: Playwright config
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
v1.14.33
|
||||
@@ -1,3 +1,7 @@
|
||||
plans/
|
||||
bun.lock
|
||||
node_modules
|
||||
plans
|
||||
package.json
|
||||
bun.lock
|
||||
.gitignore
|
||||
package-lock.json
|
||||
references/
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
description: ALWAYS use this when writing docs
|
||||
color: "#38A3EE"
|
||||
---
|
||||
|
||||
You are an expert technical documentation writer
|
||||
|
||||
You are not verbose
|
||||
|
||||
Use a relaxed and friendly tone
|
||||
|
||||
The title of the page should be a word or a 2-3 word phrase
|
||||
|
||||
The description should be one short line, should not start with "The", should
|
||||
avoid repeating the title of the page, should be 5-10 words long
|
||||
|
||||
Chunks of text should not be more than 2 sentences long
|
||||
|
||||
Each section is separated by a divider of 3 dashes
|
||||
|
||||
The section titles are short with only the first letter of the word capitalized
|
||||
|
||||
The section titles are in the imperative mood
|
||||
|
||||
The section titles should not repeat the term used in the page title, for
|
||||
example, if the page title is "Models", avoid using a section title like "Add
|
||||
new models". This might be unavoidable in some cases, but try to avoid it.
|
||||
|
||||
Check out the /packages/web/src/content/docs/docs/index.mdx as an example.
|
||||
|
||||
For JS or TS code snippets remove trailing semicolons and any trailing commas
|
||||
that might not be needed.
|
||||
|
||||
If you are making a commit prefix the commit message with `docs:`
|
||||
@@ -1,900 +0,0 @@
|
||||
---
|
||||
description: Translate content for a specified locale while preserving technical terms
|
||||
mode: subagent
|
||||
model: kilo/google/gemini-3.1-pro-preview
|
||||
---
|
||||
|
||||
You are a professional translator and localization specialist.
|
||||
|
||||
Translate the user's content into the requested target locale (language + region, e.g. fr-FR, de-DE).
|
||||
|
||||
Requirements:
|
||||
|
||||
- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure).
|
||||
- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks.
|
||||
- Also preserve every term listed in the Do-Not-Translate glossary below.
|
||||
- Also apply locale-specific guidance from `.opencode/glossary/<locale>.md` when available (for example, `zh-cn.md`).
|
||||
- Do not modify fenced code blocks.
|
||||
- Output ONLY the translation (no commentary).
|
||||
|
||||
If the target locale is missing, ask the user to provide it.
|
||||
If no locale-specific glossary exists, use the global glossary only.
|
||||
|
||||
---
|
||||
|
||||
# Locale-Specific Glossaries
|
||||
|
||||
When a locale glossary exists, use it to:
|
||||
|
||||
- Apply preferred wording for recurring UI/docs terms in that locale
|
||||
- Preserve locale-specific do-not-translate terms and casing decisions
|
||||
- Prefer natural phrasing over literal translation when the locale file calls it out
|
||||
- If the repo uses a locale alias slug, apply that file too (for example, `pt-BR` maps to `br.md` in this repo)
|
||||
|
||||
Locale guidance does not override code/command preservation rules or the global Do-Not-Translate glossary below.
|
||||
|
||||
---
|
||||
|
||||
# Do-Not-Translate Terms (OpenCode Docs)
|
||||
|
||||
Generated from: `packages/web/src/content/docs/*.mdx` (default English docs)
|
||||
Generated on: 2026-02-10
|
||||
|
||||
Use this as a translation QA checklist / glossary. Preserve listed terms exactly (spelling, casing, punctuation).
|
||||
|
||||
General rules (verbatim, even if not listed below):
|
||||
|
||||
- Anything inside inline code (single backticks) or fenced code blocks (triple backticks)
|
||||
- MDX/JS code in docs: `import ... from "..."`, component tags, identifiers
|
||||
- CLI commands, flags, config keys/values, file paths, URLs/domains, and env vars
|
||||
|
||||
## Proper nouns and product names
|
||||
|
||||
Additional (not reliably captured via link text):
|
||||
|
||||
```text
|
||||
Astro
|
||||
Bun
|
||||
Chocolatey
|
||||
Cursor
|
||||
Docker
|
||||
Git
|
||||
GitHub Actions
|
||||
GitLab CI
|
||||
GNOME Terminal
|
||||
Homebrew
|
||||
Mise
|
||||
Neovim
|
||||
Node.js
|
||||
npm
|
||||
Obsidian
|
||||
opencode
|
||||
opencode-ai
|
||||
Paru
|
||||
pnpm
|
||||
ripgrep
|
||||
Scoop
|
||||
SST
|
||||
Starlight
|
||||
Visual Studio Code
|
||||
VS Code
|
||||
VSCodium
|
||||
Windsurf
|
||||
Windows Terminal
|
||||
Yarn
|
||||
Zellij
|
||||
Zed
|
||||
anomalyco
|
||||
```
|
||||
|
||||
Extracted from link labels in the English docs (review and prune as desired):
|
||||
|
||||
```text
|
||||
@openspoon/subtask2
|
||||
302.AI console
|
||||
ACP progress report
|
||||
Agent Client Protocol
|
||||
Agent Skills
|
||||
Agentic
|
||||
AGENTS.md
|
||||
AI SDK
|
||||
Alacritty
|
||||
Anthropic
|
||||
Anthropic's Data Policies
|
||||
Atom One
|
||||
Avante.nvim
|
||||
Ayu
|
||||
Azure AI Foundry
|
||||
Azure portal
|
||||
Baseten
|
||||
built-in GITHUB_TOKEN
|
||||
Bun.$
|
||||
Catppuccin
|
||||
Cerebras console
|
||||
ChatGPT Plus or Pro
|
||||
Cloudflare dashboard
|
||||
CodeCompanion.nvim
|
||||
CodeNomad
|
||||
Configuring Adapters: Environment Variables
|
||||
Context7 MCP server
|
||||
Cortecs console
|
||||
Deep Infra dashboard
|
||||
DeepSeek console
|
||||
Duo Agent Platform
|
||||
Everforest
|
||||
Fireworks AI console
|
||||
Firmware dashboard
|
||||
Ghostty
|
||||
GitLab CLI agents docs
|
||||
GitLab docs
|
||||
GitLab User Settings > Access Tokens
|
||||
Granular Rules (Object Syntax)
|
||||
Grep by Vercel
|
||||
Groq console
|
||||
Gruvbox
|
||||
Helicone
|
||||
Helicone documentation
|
||||
Helicone Header Directory
|
||||
Helicone's Model Directory
|
||||
Hugging Face Inference Providers
|
||||
Hugging Face settings
|
||||
install WSL
|
||||
IO.NET console
|
||||
JetBrains IDE
|
||||
Kanagawa
|
||||
Kitty
|
||||
MiniMax API Console
|
||||
Models.dev
|
||||
Moonshot AI console
|
||||
Nebius Token Factory console
|
||||
Nord
|
||||
OAuth
|
||||
Ollama integration docs
|
||||
OpenAI's Data Policies
|
||||
OpenChamber
|
||||
OpenCode
|
||||
OpenCode config
|
||||
OpenCode Config
|
||||
OpenCode TUI with the opencode theme
|
||||
OpenCode Web - Active Session
|
||||
OpenCode Web - New Session
|
||||
OpenCode Web - See Servers
|
||||
OpenCode Zen
|
||||
OpenCode-Obsidian
|
||||
OpenRouter dashboard
|
||||
OpenWork
|
||||
OVHcloud panel
|
||||
Pro+ subscription
|
||||
SAP BTP Cockpit
|
||||
Scaleway Console IAM settings
|
||||
Scaleway Generative APIs
|
||||
SDK documentation
|
||||
Sentry MCP server
|
||||
shell API
|
||||
Together AI console
|
||||
Tokyonight
|
||||
Unified Billing
|
||||
Venice AI console
|
||||
Vercel dashboard
|
||||
WezTerm
|
||||
Windows Subsystem for Linux (WSL)
|
||||
WSL
|
||||
WSL (Windows Subsystem for Linux)
|
||||
WSL extension
|
||||
xAI console
|
||||
Z.AI API console
|
||||
Zed
|
||||
ZenMux dashboard
|
||||
Zod
|
||||
```
|
||||
|
||||
## Acronyms and initialisms
|
||||
|
||||
```text
|
||||
ACP
|
||||
AGENTS
|
||||
AI
|
||||
AI21
|
||||
ANSI
|
||||
API
|
||||
AST
|
||||
AWS
|
||||
BTP
|
||||
CD
|
||||
CDN
|
||||
CI
|
||||
CLI
|
||||
CMD
|
||||
CORS
|
||||
DEBUG
|
||||
EKS
|
||||
ERROR
|
||||
FAQ
|
||||
GLM
|
||||
GNOME
|
||||
GPT
|
||||
HTML
|
||||
HTTP
|
||||
HTTPS
|
||||
IAM
|
||||
ID
|
||||
IDE
|
||||
INFO
|
||||
IO
|
||||
IP
|
||||
IRSA
|
||||
JS
|
||||
JSON
|
||||
JSONC
|
||||
K2
|
||||
LLM
|
||||
LM
|
||||
LSP
|
||||
M2
|
||||
MCP
|
||||
MR
|
||||
NET
|
||||
NPM
|
||||
NTLM
|
||||
OIDC
|
||||
OS
|
||||
PAT
|
||||
PATH
|
||||
PHP
|
||||
PR
|
||||
PTY
|
||||
README
|
||||
RFC
|
||||
RPC
|
||||
SAP
|
||||
SDK
|
||||
SKILL
|
||||
SSE
|
||||
SSO
|
||||
TS
|
||||
TTY
|
||||
TUI
|
||||
UI
|
||||
URL
|
||||
US
|
||||
UX
|
||||
VCS
|
||||
VPC
|
||||
VPN
|
||||
VS
|
||||
WARN
|
||||
WSL
|
||||
X11
|
||||
YAML
|
||||
```
|
||||
|
||||
## Code identifiers used in prose (CamelCase, mixedCase)
|
||||
|
||||
```text
|
||||
apiKey
|
||||
AppleScript
|
||||
AssistantMessage
|
||||
baseURL
|
||||
BurntSushi
|
||||
ChatGPT
|
||||
ClangFormat
|
||||
CodeCompanion
|
||||
CodeNomad
|
||||
DeepSeek
|
||||
DefaultV2
|
||||
FileContent
|
||||
FileDiff
|
||||
FileNode
|
||||
fineGrained
|
||||
FormatterStatus
|
||||
GitHub
|
||||
GitLab
|
||||
iTerm2
|
||||
JavaScript
|
||||
JetBrains
|
||||
macOS
|
||||
mDNS
|
||||
MiniMax
|
||||
NeuralNomadsAI
|
||||
NickvanDyke
|
||||
NoeFabris
|
||||
OpenAI
|
||||
OpenAPI
|
||||
OpenChamber
|
||||
OpenCode
|
||||
OpenRouter
|
||||
OpenTUI
|
||||
OpenWork
|
||||
ownUserPermissions
|
||||
PowerShell
|
||||
ProviderAuthAuthorization
|
||||
ProviderAuthMethod
|
||||
ProviderInitError
|
||||
SessionStatus
|
||||
TabItem
|
||||
tokenType
|
||||
ToolIDs
|
||||
ToolList
|
||||
TypeScript
|
||||
typesUrl
|
||||
UserMessage
|
||||
VcsInfo
|
||||
WebView2
|
||||
WezTerm
|
||||
xAI
|
||||
ZenMux
|
||||
```
|
||||
|
||||
## OpenCode CLI commands (as shown in docs)
|
||||
|
||||
```text
|
||||
opencode
|
||||
opencode [project]
|
||||
opencode /path/to/project
|
||||
opencode acp
|
||||
opencode agent [command]
|
||||
opencode agent create
|
||||
opencode agent list
|
||||
opencode attach [url]
|
||||
opencode attach http://10.20.30.40:4096
|
||||
opencode attach http://localhost:4096
|
||||
opencode auth [command]
|
||||
opencode auth list
|
||||
opencode auth login
|
||||
opencode auth logout
|
||||
opencode auth ls
|
||||
opencode export [sessionID]
|
||||
opencode github [command]
|
||||
opencode github install
|
||||
opencode github run
|
||||
opencode import <file>
|
||||
opencode import https://opncd.ai/s/abc123
|
||||
opencode import session.json
|
||||
opencode mcp [command]
|
||||
opencode mcp add
|
||||
opencode mcp auth [name]
|
||||
opencode mcp auth list
|
||||
opencode mcp auth ls
|
||||
opencode mcp auth my-oauth-server
|
||||
opencode mcp auth sentry
|
||||
opencode mcp debug <name>
|
||||
opencode mcp debug my-oauth-server
|
||||
opencode mcp list
|
||||
opencode mcp logout [name]
|
||||
opencode mcp logout my-oauth-server
|
||||
opencode mcp ls
|
||||
opencode models --refresh
|
||||
opencode models [provider]
|
||||
opencode models anthropic
|
||||
opencode run [message..]
|
||||
opencode run Explain the use of context in Go
|
||||
opencode serve
|
||||
opencode serve --cors http://localhost:5173 --cors https://app.example.com
|
||||
opencode serve --hostname 0.0.0.0 --port 4096
|
||||
opencode serve [--port <number>] [--hostname <string>] [--cors <origin>]
|
||||
opencode session [command]
|
||||
opencode session list
|
||||
opencode session delete <sessionID>
|
||||
opencode stats
|
||||
opencode uninstall
|
||||
opencode upgrade
|
||||
opencode upgrade [target]
|
||||
opencode upgrade v0.1.48
|
||||
opencode web
|
||||
opencode web --cors https://example.com
|
||||
opencode web --hostname 0.0.0.0
|
||||
opencode web --mdns
|
||||
opencode web --mdns --mdns-domain myproject.local
|
||||
opencode web --port 4096
|
||||
opencode web --port 4096 --hostname 0.0.0.0
|
||||
opencode.server.close()
|
||||
```
|
||||
|
||||
## Slash commands and routes
|
||||
|
||||
```text
|
||||
/agent
|
||||
/auth/:id
|
||||
/clear
|
||||
/command
|
||||
/config
|
||||
/config/providers
|
||||
/connect
|
||||
/continue
|
||||
/doc
|
||||
/editor
|
||||
/event
|
||||
/experimental/tool?provider=<p>&model=<m>
|
||||
/experimental/tool/ids
|
||||
/export
|
||||
/file?path=<path>
|
||||
/file/content?path=<p>
|
||||
/file/status
|
||||
/find?pattern=<pat>
|
||||
/find/file
|
||||
/find/file?query=<q>
|
||||
/find/symbol?query=<q>
|
||||
/formatter
|
||||
/global/event
|
||||
/global/health
|
||||
/help
|
||||
/init
|
||||
/instance/dispose
|
||||
/log
|
||||
/lsp
|
||||
/mcp
|
||||
/mnt/
|
||||
/mnt/c/
|
||||
/mnt/d/
|
||||
/models
|
||||
/oc
|
||||
/opencode
|
||||
/path
|
||||
/project
|
||||
/project/current
|
||||
/provider
|
||||
/provider/{id}/oauth/authorize
|
||||
/provider/{id}/oauth/callback
|
||||
/provider/auth
|
||||
/q
|
||||
/quit
|
||||
/redo
|
||||
/resume
|
||||
/session
|
||||
/session/:id
|
||||
/session/:id/abort
|
||||
/session/:id/children
|
||||
/session/:id/command
|
||||
/session/:id/diff
|
||||
/session/:id/fork
|
||||
/session/:id/init
|
||||
/session/:id/message
|
||||
/session/:id/message/:messageID
|
||||
/session/:id/permissions/:permissionID
|
||||
/session/:id/prompt_async
|
||||
/session/:id/revert
|
||||
/session/:id/share
|
||||
/session/:id/shell
|
||||
/session/:id/summarize
|
||||
/session/:id/todo
|
||||
/session/:id/unrevert
|
||||
/session/status
|
||||
/share
|
||||
/summarize
|
||||
/theme
|
||||
/tui
|
||||
/tui/append-prompt
|
||||
/tui/clear-prompt
|
||||
/tui/control/next
|
||||
/tui/control/response
|
||||
/tui/execute-command
|
||||
/tui/open-help
|
||||
/tui/open-models
|
||||
/tui/open-sessions
|
||||
/tui/open-themes
|
||||
/tui/show-toast
|
||||
/tui/submit-prompt
|
||||
/undo
|
||||
/Users/username
|
||||
/Users/username/projects/*
|
||||
/vcs
|
||||
```
|
||||
|
||||
## CLI flags and short options
|
||||
|
||||
```text
|
||||
--agent
|
||||
--attach
|
||||
--command
|
||||
--continue
|
||||
--cors
|
||||
--cwd
|
||||
--days
|
||||
--dir
|
||||
--dry-run
|
||||
--event
|
||||
--file
|
||||
--force
|
||||
--fork
|
||||
--format
|
||||
--help
|
||||
--hostname
|
||||
--hostname 0.0.0.0
|
||||
--keep-config
|
||||
--keep-data
|
||||
--log-level
|
||||
--max-count
|
||||
--mdns
|
||||
--mdns-domain
|
||||
--method
|
||||
--model
|
||||
--models
|
||||
--port
|
||||
--print-logs
|
||||
--project
|
||||
--prompt
|
||||
--refresh
|
||||
--session
|
||||
--share
|
||||
--title
|
||||
--token
|
||||
--tools
|
||||
--verbose
|
||||
--version
|
||||
--wait
|
||||
|
||||
-c
|
||||
-d
|
||||
-f
|
||||
-h
|
||||
-m
|
||||
-n
|
||||
-s
|
||||
-v
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
```text
|
||||
AI_API_URL
|
||||
AI_FLOW_CONTEXT
|
||||
AI_FLOW_EVENT
|
||||
AI_FLOW_INPUT
|
||||
AICORE_DEPLOYMENT_ID
|
||||
AICORE_RESOURCE_GROUP
|
||||
AICORE_SERVICE_KEY
|
||||
ANTHROPIC_API_KEY
|
||||
AWS_ACCESS_KEY_ID
|
||||
AWS_BEARER_TOKEN_BEDROCK
|
||||
AWS_PROFILE
|
||||
AWS_REGION
|
||||
AWS_ROLE_ARN
|
||||
AWS_SECRET_ACCESS_KEY
|
||||
AWS_WEB_IDENTITY_TOKEN_FILE
|
||||
AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
||||
AZURE_RESOURCE_NAME
|
||||
CI_PROJECT_DIR
|
||||
CI_SERVER_FQDN
|
||||
CI_WORKLOAD_REF
|
||||
CLOUDFLARE_ACCOUNT_ID
|
||||
CLOUDFLARE_API_TOKEN
|
||||
CLOUDFLARE_GATEWAY_ID
|
||||
CONTEXT7_API_KEY
|
||||
GITHUB_TOKEN
|
||||
GITLAB_AI_GATEWAY_URL
|
||||
GITLAB_HOST
|
||||
GITLAB_INSTANCE_URL
|
||||
GITLAB_OAUTH_CLIENT_ID
|
||||
GITLAB_TOKEN
|
||||
GITLAB_TOKEN_OPENCODE
|
||||
GOOGLE_APPLICATION_CREDENTIALS
|
||||
GOOGLE_CLOUD_PROJECT
|
||||
HTTP_PROXY
|
||||
HTTPS_PROXY
|
||||
K2_
|
||||
MY_API_KEY
|
||||
MY_ENV_VAR
|
||||
MY_MCP_CLIENT_ID
|
||||
MY_MCP_CLIENT_SECRET
|
||||
NO_PROXY
|
||||
NODE_ENV
|
||||
NODE_EXTRA_CA_CERTS
|
||||
NPM_AUTH_TOKEN
|
||||
OC_ALLOW_WAYLAND
|
||||
KILO_API_KEY
|
||||
KILO_AUTH_JSON
|
||||
KILO_AUTO_SHARE
|
||||
KILO_CLIENT
|
||||
KILO_CONFIG
|
||||
KILO_CONFIG_CONTENT
|
||||
KILO_CONFIG_DIR
|
||||
KILO_DISABLE_AUTOCOMPACT
|
||||
KILO_DISABLE_AUTOUPDATE
|
||||
KILO_DISABLE_CLAUDE_CODE
|
||||
KILO_DISABLE_CLAUDE_CODE_PROMPT
|
||||
KILO_DISABLE_CLAUDE_CODE_SKILLS
|
||||
KILO_DISABLE_DEFAULT_PLUGINS
|
||||
KILO_DISABLE_FILETIME_CHECK
|
||||
KILO_DISABLE_LSP_DOWNLOAD
|
||||
KILO_DISABLE_MODELS_FETCH
|
||||
KILO_DISABLE_PRUNE
|
||||
KILO_DISABLE_TERMINAL_TITLE
|
||||
KILO_ENABLE_EXA
|
||||
KILO_ENABLE_EXPERIMENTAL_MODELS
|
||||
KILO_EXPERIMENTAL
|
||||
KILO_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS
|
||||
KILO_EXPERIMENTAL_DISABLE_COPY_ON_SELECT
|
||||
KILO_EXPERIMENTAL_DISABLE_FILEWATCHER
|
||||
KILO_EXPERIMENTAL_EXA
|
||||
KILO_EXPERIMENTAL_FILEWATCHER
|
||||
KILO_EXPERIMENTAL_ICON_DISCOVERY
|
||||
KILO_EXPERIMENTAL_LSP_TOOL
|
||||
KILO_EXPERIMENTAL_LSP_TY
|
||||
KILO_EXPERIMENTAL_MARKDOWN
|
||||
KILO_EXPERIMENTAL_OUTPUT_TOKEN_MAX
|
||||
KILO_EXPERIMENTAL_OXFMT
|
||||
KILO_EXPERIMENTAL_PLAN_MODE
|
||||
KILO_ENABLE_QUESTION_TOOL
|
||||
KILO_FAKE_VCS
|
||||
KILO_GIT_BASH_PATH
|
||||
KILO_MODEL
|
||||
KILO_MODELS_URL
|
||||
KILO_PERMISSION
|
||||
KILO_PORT
|
||||
KILO_SERVER_PASSWORD
|
||||
KILO_SERVER_USERNAME
|
||||
PROJECT_ROOT
|
||||
RESOURCE_NAME
|
||||
RUST_LOG
|
||||
VARIABLE_NAME
|
||||
VERTEX_LOCATION
|
||||
XDG_CONFIG_HOME
|
||||
```
|
||||
|
||||
## Package/module identifiers
|
||||
|
||||
```text
|
||||
../../../config.mjs
|
||||
@astrojs/starlight/components
|
||||
@opencode-ai/plugin
|
||||
@opencode-ai/sdk
|
||||
path
|
||||
shescape
|
||||
zod
|
||||
|
||||
@
|
||||
@ai-sdk/anthropic
|
||||
@ai-sdk/cerebras
|
||||
@ai-sdk/google
|
||||
@ai-sdk/openai
|
||||
@ai-sdk/openai-compatible
|
||||
@File#L37-42
|
||||
@modelcontextprotocol/server-everything
|
||||
@opencode
|
||||
```
|
||||
|
||||
## GitHub owner/repo slugs referenced in docs
|
||||
|
||||
```text
|
||||
24601/opencode-zellij-namer
|
||||
angristan/opencode-wakatime
|
||||
anomalyco/opencode
|
||||
apps/opencode-agent
|
||||
athal7/opencode-devcontainers
|
||||
awesome-opencode/awesome-opencode
|
||||
backnotprop/plannotator
|
||||
ben-vargas/ai-sdk-provider-opencode-sdk
|
||||
btriapitsyn/openchamber
|
||||
BurntSushi/ripgrep
|
||||
Cluster444/agentic
|
||||
code-yeongyu/oh-my-opencode
|
||||
darrenhinde/opencode-agents
|
||||
different-ai/opencode-scheduler
|
||||
different-ai/openwork
|
||||
features/copilot
|
||||
folke/tokyonight.nvim
|
||||
franlol/opencode-md-table-formatter
|
||||
ggml-org/llama.cpp
|
||||
ghoulr/opencode-websearch-cited.git
|
||||
H2Shami/opencode-helicone-session
|
||||
hosenur/portal
|
||||
jamesmurdza/daytona
|
||||
jenslys/opencode-gemini-auth
|
||||
JRedeker/opencode-morph-fast-apply
|
||||
JRedeker/opencode-shell-strategy
|
||||
kdcokenny/ocx
|
||||
kdcokenny/opencode-background-agents
|
||||
kdcokenny/opencode-notify
|
||||
kdcokenny/opencode-workspace
|
||||
kdcokenny/opencode-worktree
|
||||
login/device
|
||||
mohak34/opencode-notifier
|
||||
morhetz/gruvbox
|
||||
mtymek/opencode-obsidian
|
||||
NeuralNomadsAI/CodeNomad
|
||||
nick-vi/opencode-type-inject
|
||||
NickvanDyke/opencode.nvim
|
||||
NoeFabris/opencode-antigravity-auth
|
||||
nordtheme/nord
|
||||
numman-ali/opencode-openai-codex-auth
|
||||
olimorris/codecompanion.nvim
|
||||
panta82/opencode-notificator
|
||||
rebelot/kanagawa.nvim
|
||||
remorses/kimaki
|
||||
sainnhe/everforest
|
||||
shekohex/opencode-google-antigravity-auth
|
||||
shekohex/opencode-pty.git
|
||||
spoons-and-mirrors/subtask2
|
||||
sudo-tee/opencode.nvim
|
||||
supermemoryai/opencode-supermemory
|
||||
Tarquinen/opencode-dynamic-context-pruning
|
||||
Th3Whit3Wolf/one-nvim
|
||||
upstash/context7
|
||||
vtemian/micode
|
||||
vtemian/octto
|
||||
yetone/avante.nvim
|
||||
zenobi-us/opencode-plugin-template
|
||||
zenobi-us/opencode-skillful
|
||||
```
|
||||
|
||||
## Paths, filenames, globs, and URLs
|
||||
|
||||
```text
|
||||
./.opencode/themes/*.json
|
||||
./<project-slug>/storage/
|
||||
./config/#custom-directory
|
||||
./global/storage/
|
||||
.agents/skills/*/SKILL.md
|
||||
.agents/skills/<name>/SKILL.md
|
||||
.clang-format
|
||||
.claude
|
||||
.claude/skills
|
||||
.claude/skills/*/SKILL.md
|
||||
.claude/skills/<name>/SKILL.md
|
||||
.env
|
||||
.github/workflows/opencode.yml
|
||||
.gitignore
|
||||
.gitlab-ci.yml
|
||||
.ignore
|
||||
.NET SDK
|
||||
.npmrc
|
||||
.ocamlformat
|
||||
.opencode
|
||||
.opencode/
|
||||
.opencode/agents/
|
||||
.opencode/commands/
|
||||
.opencode/commands/test.md
|
||||
.opencode/modes/
|
||||
.opencode/plans/*.md
|
||||
.opencode/plugins/
|
||||
.opencode/skills/<name>/SKILL.md
|
||||
.opencode/skills/git-release/SKILL.md
|
||||
.opencode/tools/
|
||||
.well-known/opencode
|
||||
{ type: "raw" \| "patch", content: string }
|
||||
{file:path/to/file}
|
||||
**/*.js
|
||||
%USERPROFILE%/intelephense/license.txt
|
||||
%USERPROFILE%\.cache\opencode
|
||||
%USERPROFILE%\.config\opencode\opencode.jsonc
|
||||
%USERPROFILE%\.config\opencode\plugins
|
||||
%USERPROFILE%\.local\share\opencode
|
||||
%USERPROFILE%\.local\share\opencode\log
|
||||
<project-root>/.opencode/themes/*.json
|
||||
<providerId>/<modelId>
|
||||
<your-project>/.opencode/plugins/
|
||||
~
|
||||
~/...
|
||||
~/.agents/skills/*/SKILL.md
|
||||
~/.agents/skills/<name>/SKILL.md
|
||||
~/.aws/credentials
|
||||
~/.bashrc
|
||||
~/.cache/opencode
|
||||
~/.cache/opencode/node_modules/
|
||||
~/.claude/CLAUDE.md
|
||||
~/.claude/skills/
|
||||
~/.claude/skills/*/SKILL.md
|
||||
~/.claude/skills/<name>/SKILL.md
|
||||
~/.config/opencode
|
||||
~/.config/opencode/AGENTS.md
|
||||
~/.config/opencode/agents/
|
||||
~/.config/opencode/commands/
|
||||
~/.config/opencode/modes/
|
||||
~/.config/opencode/opencode.json
|
||||
~/.config/opencode/opencode.jsonc
|
||||
~/.config/opencode/plugins/
|
||||
~/.config/opencode/skills/*/SKILL.md
|
||||
~/.config/opencode/skills/<name>/SKILL.md
|
||||
~/.config/opencode/themes/*.json
|
||||
~/.config/opencode/tools/
|
||||
~/.config/zed/settings.json
|
||||
~/.local/share
|
||||
~/.local/share/opencode/
|
||||
~/.local/share/opencode/auth.json
|
||||
~/.local/share/opencode/log/
|
||||
~/.local/share/opencode/mcp-auth.json
|
||||
~/.local/share/opencode/opencode.jsonc
|
||||
~/.npmrc
|
||||
~/.zshrc
|
||||
~/code/
|
||||
~/Library/Application Support
|
||||
~/projects/*
|
||||
~/projects/personal/
|
||||
${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts
|
||||
$HOME/intelephense/license.txt
|
||||
$HOME/projects/*
|
||||
$XDG_CONFIG_HOME/opencode/themes/*.json
|
||||
agent/
|
||||
agents/
|
||||
build/
|
||||
commands/
|
||||
dist/
|
||||
http://<wsl-ip>:4096
|
||||
http://127.0.0.1:8080/callback
|
||||
http://localhost:<port>
|
||||
http://localhost:4096
|
||||
http://localhost:4096/doc
|
||||
https://app.example.com
|
||||
https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/
|
||||
https://opencode.ai/zen/v1/chat/completions
|
||||
https://opencode.ai/zen/v1/messages
|
||||
https://opencode.ai/zen/v1/models/gemini-3-flash
|
||||
https://opencode.ai/zen/v1/models/gemini-3-pro
|
||||
https://opencode.ai/zen/v1/responses
|
||||
https://RESOURCE_NAME.openai.azure.com/
|
||||
laravel/pint
|
||||
log/
|
||||
model: "anthropic/claude-sonnet-4-5"
|
||||
modes/
|
||||
node_modules/
|
||||
openai/gpt-4.1
|
||||
app.kilo.ai/config.json
|
||||
opencode/<model-id>
|
||||
opencode/gpt-5.1-codex
|
||||
opencode/gpt-5.2-codex
|
||||
opencode/kimi-k2
|
||||
openrouter/google/gemini-2.5-flash
|
||||
opncd.ai/s/<share-id>
|
||||
packages/*/AGENTS.md
|
||||
plugins/
|
||||
project/
|
||||
provider_id/model_id
|
||||
provider/model
|
||||
provider/model-id
|
||||
rm -rf ~/.cache/opencode
|
||||
skills/
|
||||
skills/*/SKILL.md
|
||||
src/**/*.ts
|
||||
themes/
|
||||
tools/
|
||||
```
|
||||
|
||||
## Keybind strings
|
||||
|
||||
```text
|
||||
alt+b
|
||||
Alt+Ctrl+K
|
||||
alt+d
|
||||
alt+f
|
||||
Cmd+Esc
|
||||
Cmd+Option+K
|
||||
Cmd+Shift+Esc
|
||||
Cmd+Shift+G
|
||||
Cmd+Shift+P
|
||||
ctrl+a
|
||||
ctrl+b
|
||||
ctrl+d
|
||||
ctrl+e
|
||||
Ctrl+Esc
|
||||
ctrl+f
|
||||
ctrl+g
|
||||
ctrl+k
|
||||
Ctrl+Shift+Esc
|
||||
Ctrl+Shift+P
|
||||
ctrl+t
|
||||
ctrl+u
|
||||
ctrl+w
|
||||
ctrl+x
|
||||
DELETE
|
||||
Shift+Enter
|
||||
WIN+R
|
||||
```
|
||||
|
||||
## Model ID strings referenced
|
||||
|
||||
```text
|
||||
{env:KILO_MODEL}
|
||||
anthropic/claude-3-5-sonnet-20241022
|
||||
anthropic/claude-haiku-4-20250514
|
||||
anthropic/claude-haiku-4-5
|
||||
anthropic/claude-sonnet-4-20250514
|
||||
anthropic/claude-sonnet-4-5
|
||||
gitlab/duo-chat-haiku-4-5
|
||||
lmstudio/google/gemma-3n-e4b
|
||||
openai/gpt-4.1
|
||||
openai/gpt-5
|
||||
opencode/gpt-5.1-codex
|
||||
opencode/gpt-5.2-codex
|
||||
opencode/kimi-k2
|
||||
openrouter/google/gemini-2.5-flash
|
||||
```
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
model: opencode/gpt-5.4
|
||||
---
|
||||
|
||||
Create `UPCOMING_CHANGELOG.md` from the structured changelog input below.
|
||||
If `UPCOMING_CHANGELOG.md` already exists, ignore its current contents completely.
|
||||
Do not preserve, merge, or reuse text from the existing file.
|
||||
|
||||
The input already contains the exact commit range since the last non-draft release.
|
||||
The commits are already filtered to the release-relevant packages and grouped into
|
||||
the release sections. Do not fetch GitHub releases, PRs, or build your own commit list.
|
||||
The input may also include a `## Community Contributors Input` section.
|
||||
|
||||
Before writing any entry you keep, inspect the real diff with
|
||||
`git show --stat --format='' <hash>` or `git show --format='' <hash>` so you can
|
||||
understand the actual code changes and not just the commit message (they may be misleading).
|
||||
Do not use `git log` or author metadata when deciding attribution.
|
||||
|
||||
Rules:
|
||||
|
||||
- Write the final file with sections in this order:
|
||||
`## Core`, `## TUI`, `## Desktop`, `## SDK`, `## Extensions`
|
||||
- Only include sections that have at least one notable entry
|
||||
- Keep one bullet per commit you keep
|
||||
- Skip commits that are entirely internal, CI, tests, refactors, or otherwise not user-facing
|
||||
- Start each bullet with a capital letter
|
||||
- Prefer what changed for users over what code changed internally
|
||||
- Do not copy raw commit prefixes like `fix:` or `feat:` or trailing PR numbers like `(#123)`
|
||||
- Community attribution is deterministic: only preserve an existing `(@username)` suffix from the changelog input
|
||||
- If an input bullet has no `(@username)` suffix, do not add one
|
||||
- Never add a new `(@username)` suffix from `git show`, commit authors, names, or email addresses
|
||||
- If no notable entries remain and there is no contributor block, write exactly `No notable changes.`
|
||||
- If no notable entries remain but there is a contributor block, omit all release sections and return only the contributor block
|
||||
- If the input contains `## Community Contributors Input`, append the block below that heading to the end of the final file verbatim
|
||||
- Do not add, remove, rewrite, or reorder contributor names or commit titles in that block
|
||||
- Do not derive the thank-you section from the main summary bullets
|
||||
- Do not include the heading `## Community Contributors Input` in the final file
|
||||
- Focus on writing the least words to get your point across - users will skim read the changelog, so we should be precise
|
||||
|
||||
**Importantly, the changelog is for users (who are at least slightly technical), they may use the TUI, Desktop, SDK, Plugins and so forth. Be thorough in understanding flow on effects may not be immediately apparent. e.g. a package upgrade looks internal but may patch a bug. Or a refactor may also stabilise some race condition that fixes bugs for users. The PR title/body + commit message will give you the authors context, usually containing the outcome not just technical detail**
|
||||
|
||||
<changelog_input>
|
||||
|
||||
!`bun script/raw-changelog.ts $ARGUMENTS`
|
||||
|
||||
</changelog_input>
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
description: translate English to other languages
|
||||
model: opencode/claude-opus-4-7
|
||||
---
|
||||
|
||||
run git diff and translate changed english doc and UI copy files to other international languages. Translate all languages in parallel to save time.
|
||||
|
||||
Requirements:
|
||||
|
||||
- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure).
|
||||
- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks.
|
||||
- Also preserve every term listed in the Do-Not-Translate glossary below.
|
||||
- Also apply locale-specific guidance from `.opencode/glossary/<locale>.md` when available (for example, `zh-cn.md`).
|
||||
- Do not modify fenced code blocks.
|
||||
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"nord0": "#2E3440",
|
||||
"nord1": "#3B4252",
|
||||
"nord2": "#434C5E",
|
||||
"nord3": "#4C566A",
|
||||
"nord4": "#D8DEE9",
|
||||
"nord5": "#E5E9F0",
|
||||
"nord6": "#ECEFF4",
|
||||
"nord7": "#8FBCBB",
|
||||
"nord8": "#88C0D0",
|
||||
"nord9": "#81A1C1",
|
||||
"nord10": "#5E81AC",
|
||||
"nord11": "#BF616A",
|
||||
"nord12": "#D08770",
|
||||
"nord13": "#EBCB8B",
|
||||
"nord14": "#A3BE8C",
|
||||
"nord15": "#B48EAD"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "nord10",
|
||||
"light": "nord9"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"error": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "nord12",
|
||||
"light": "nord12"
|
||||
},
|
||||
"success": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"info": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"text": {
|
||||
"dark": "nord6",
|
||||
"light": "nord0"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "#8B95A7",
|
||||
"light": "nord1"
|
||||
},
|
||||
"background": {
|
||||
"dark": "nord0",
|
||||
"light": "nord6"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "nord1",
|
||||
"light": "nord5"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "nord2",
|
||||
"light": "nord4"
|
||||
},
|
||||
"border": {
|
||||
"dark": "nord2",
|
||||
"light": "nord3"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "nord3",
|
||||
"light": "nord2"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "nord2",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "#8B95A7",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "#8B95A7",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#36413C",
|
||||
"light": "#E6EBE7"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#43393D",
|
||||
"light": "#ECE6E8"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "nord1",
|
||||
"light": "nord5"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "nord2",
|
||||
"light": "nord4"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#303A35",
|
||||
"light": "#DDE4DF"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3C3336",
|
||||
"light": "#E4DDE0"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "#8B95A7",
|
||||
"light": "nord3"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "nord12",
|
||||
"light": "nord12"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "nord13",
|
||||
"light": "nord13"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "#8B95A7",
|
||||
"light": "nord3"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "#8B95A7",
|
||||
"light": "nord3"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "nord8",
|
||||
"light": "nord8"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "nord15",
|
||||
"light": "nord15"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,937 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||
import { RGBA, VignetteEffect } from "@opentui/core"
|
||||
import type {
|
||||
TuiKeybindSet,
|
||||
TuiPlugin,
|
||||
TuiPluginApi,
|
||||
TuiPluginMeta,
|
||||
TuiPluginModule,
|
||||
TuiSlotPlugin,
|
||||
} from "@opencode-ai/plugin/tui"
|
||||
|
||||
const tabs = ["overview", "counter", "help"]
|
||||
const bind = {
|
||||
modal: "ctrl+shift+m",
|
||||
screen: "ctrl+shift+o",
|
||||
home: "escape,ctrl+h",
|
||||
left: "left,h",
|
||||
right: "right,l",
|
||||
up: "up,k",
|
||||
down: "down,j",
|
||||
alert: "a",
|
||||
confirm: "c",
|
||||
prompt: "p",
|
||||
select: "s",
|
||||
modal_accept: "enter,return",
|
||||
modal_close: "escape",
|
||||
dialog_close: "escape",
|
||||
local: "x",
|
||||
local_push: "enter,return",
|
||||
local_close: "q,backspace",
|
||||
host: "z",
|
||||
}
|
||||
|
||||
const pick = (value: unknown, fallback: string) => {
|
||||
if (typeof value !== "string") return fallback
|
||||
if (!value.trim()) return fallback
|
||||
return value
|
||||
}
|
||||
|
||||
const num = (value: unknown, fallback: number) => {
|
||||
if (typeof value !== "number") return fallback
|
||||
return value
|
||||
}
|
||||
|
||||
const rec = (value: unknown) => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return
|
||||
return Object.fromEntries(Object.entries(value))
|
||||
}
|
||||
|
||||
type Cfg = {
|
||||
label: string
|
||||
route: string
|
||||
vignette: number
|
||||
keybinds: Record<string, unknown> | undefined
|
||||
}
|
||||
|
||||
type Route = {
|
||||
modal: string
|
||||
screen: string
|
||||
}
|
||||
|
||||
type State = {
|
||||
tab: number
|
||||
count: number
|
||||
source: string
|
||||
note: string
|
||||
selected: string
|
||||
local: number
|
||||
}
|
||||
|
||||
const cfg = (options: Record<string, unknown> | undefined) => {
|
||||
return {
|
||||
label: pick(options?.label, "smoke"),
|
||||
route: pick(options?.route, "workspace-smoke"),
|
||||
vignette: Math.max(0, num(options?.vignette, 0.35)),
|
||||
keybinds: rec(options?.keybinds),
|
||||
}
|
||||
}
|
||||
|
||||
const names = (input: Cfg) => {
|
||||
return {
|
||||
modal: `${input.route}.modal`,
|
||||
screen: `${input.route}.screen`,
|
||||
}
|
||||
}
|
||||
|
||||
type Keys = TuiKeybindSet
|
||||
const ui = {
|
||||
panel: "#1d1d1d",
|
||||
border: "#4a4a4a",
|
||||
text: "#f0f0f0",
|
||||
muted: "#a5a5a5",
|
||||
accent: "#5f87ff",
|
||||
}
|
||||
|
||||
type Color = RGBA | string
|
||||
|
||||
const ink = (map: Record<string, unknown>, name: string, fallback: string): Color => {
|
||||
const value = map[name]
|
||||
if (typeof value === "string") return value
|
||||
if (value instanceof RGBA) return value
|
||||
return fallback
|
||||
}
|
||||
|
||||
const look = (map: Record<string, unknown>) => {
|
||||
return {
|
||||
panel: ink(map, "backgroundPanel", ui.panel),
|
||||
border: ink(map, "border", ui.border),
|
||||
text: ink(map, "text", ui.text),
|
||||
muted: ink(map, "textMuted", ui.muted),
|
||||
accent: ink(map, "primary", ui.accent),
|
||||
selected: ink(map, "selectedListItemText", ui.text),
|
||||
}
|
||||
}
|
||||
|
||||
const tone = (api: TuiPluginApi) => {
|
||||
return look(api.theme.current)
|
||||
}
|
||||
|
||||
type Skin = {
|
||||
panel: Color
|
||||
border: Color
|
||||
text: Color
|
||||
muted: Color
|
||||
accent: Color
|
||||
selected: Color
|
||||
}
|
||||
|
||||
const Btn = (props: { txt: string; run: () => void; skin: Skin; on?: boolean }) => {
|
||||
return (
|
||||
<box
|
||||
onMouseUp={() => {
|
||||
props.run()
|
||||
}}
|
||||
backgroundColor={props.on ? props.skin.accent : props.skin.border}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
>
|
||||
<text fg={props.on ? props.skin.selected : props.skin.text}>{props.txt}</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
const parse = (params: Record<string, unknown> | undefined) => {
|
||||
const tab = typeof params?.tab === "number" ? params.tab : 0
|
||||
const count = typeof params?.count === "number" ? params.count : 0
|
||||
const source = typeof params?.source === "string" ? params.source : "unknown"
|
||||
const note = typeof params?.note === "string" ? params.note : ""
|
||||
const selected = typeof params?.selected === "string" ? params.selected : ""
|
||||
const local = typeof params?.local === "number" ? params.local : 0
|
||||
return {
|
||||
tab: Math.max(0, Math.min(tab, tabs.length - 1)),
|
||||
count,
|
||||
source,
|
||||
note,
|
||||
selected,
|
||||
local: Math.max(0, local),
|
||||
}
|
||||
}
|
||||
|
||||
const current = (api: TuiPluginApi, route: Route) => {
|
||||
const value = api.route.current
|
||||
const ok = Object.values(route).includes(value.name)
|
||||
if (!ok) return parse(undefined)
|
||||
if (!("params" in value)) return parse(undefined)
|
||||
return parse(value.params)
|
||||
}
|
||||
|
||||
const opts = [
|
||||
{
|
||||
title: "Overview",
|
||||
value: 0,
|
||||
description: "Switch to overview tab",
|
||||
},
|
||||
{
|
||||
title: "Counter",
|
||||
value: 1,
|
||||
description: "Switch to counter tab",
|
||||
},
|
||||
{
|
||||
title: "Help",
|
||||
value: 2,
|
||||
description: "Switch to help tab",
|
||||
},
|
||||
]
|
||||
|
||||
const host = (api: TuiPluginApi, input: Cfg, skin: Skin) => {
|
||||
api.ui.dialog.setSize("medium")
|
||||
api.ui.dialog.replace(() => (
|
||||
<box paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1} flexDirection="column">
|
||||
<text fg={skin.text}>
|
||||
<b>{input.label} host overlay</b>
|
||||
</text>
|
||||
<text fg={skin.muted}>Using api.ui.dialog stack with built-in backdrop</text>
|
||||
<text fg={skin.muted}>esc closes · depth {api.ui.dialog.depth}</text>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<Btn txt="close" run={() => api.ui.dialog.clear()} skin={skin} on />
|
||||
</box>
|
||||
</box>
|
||||
))
|
||||
}
|
||||
|
||||
const warn = (api: TuiPluginApi, route: Route, value: State) => {
|
||||
const DialogAlert = api.ui.DialogAlert
|
||||
api.ui.dialog.setSize("medium")
|
||||
api.ui.dialog.replace(() => (
|
||||
<DialogAlert
|
||||
title="Smoke alert"
|
||||
message="Testing built-in alert dialog"
|
||||
onConfirm={() => api.route.navigate(route.screen, { ...value, source: "alert" })}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
const check = (api: TuiPluginApi, route: Route, value: State) => {
|
||||
const DialogConfirm = api.ui.DialogConfirm
|
||||
api.ui.dialog.setSize("medium")
|
||||
api.ui.dialog.replace(() => (
|
||||
<DialogConfirm
|
||||
title="Smoke confirm"
|
||||
message="Apply +1 to counter?"
|
||||
onConfirm={() => api.route.navigate(route.screen, { ...value, count: value.count + 1, source: "confirm" })}
|
||||
onCancel={() => api.route.navigate(route.screen, { ...value, source: "confirm-cancel" })}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
const entry = (api: TuiPluginApi, route: Route, value: State) => {
|
||||
const DialogPrompt = api.ui.DialogPrompt
|
||||
api.ui.dialog.setSize("medium")
|
||||
api.ui.dialog.replace(() => (
|
||||
<DialogPrompt
|
||||
title="Smoke prompt"
|
||||
value={value.note}
|
||||
onConfirm={(note) => {
|
||||
api.ui.dialog.clear()
|
||||
api.route.navigate(route.screen, { ...value, note, source: "prompt" })
|
||||
}}
|
||||
onCancel={() => {
|
||||
api.ui.dialog.clear()
|
||||
api.route.navigate(route.screen, value)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
const picker = (api: TuiPluginApi, route: Route, value: State) => {
|
||||
const DialogSelect = api.ui.DialogSelect
|
||||
api.ui.dialog.setSize("medium")
|
||||
api.ui.dialog.replace(() => (
|
||||
<DialogSelect
|
||||
title="Smoke select"
|
||||
options={opts}
|
||||
current={value.tab}
|
||||
onSelect={(item) => {
|
||||
api.ui.dialog.clear()
|
||||
api.route.navigate(route.screen, {
|
||||
...value,
|
||||
tab: typeof item.value === "number" ? item.value : value.tab,
|
||||
selected: item.title,
|
||||
source: "select",
|
||||
})
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
const Screen = (props: {
|
||||
api: TuiPluginApi
|
||||
input: Cfg
|
||||
route: Route
|
||||
keys: Keys
|
||||
meta: TuiPluginMeta
|
||||
params?: Record<string, unknown>
|
||||
}) => {
|
||||
const dim = useTerminalDimensions()
|
||||
const value = parse(props.params)
|
||||
const skin = tone(props.api)
|
||||
const set = (local: number, base?: State) => {
|
||||
const next = base ?? current(props.api, props.route)
|
||||
props.api.route.navigate(props.route.screen, { ...next, local: Math.max(0, local), source: "local" })
|
||||
}
|
||||
const push = (base?: State) => {
|
||||
const next = base ?? current(props.api, props.route)
|
||||
set(next.local + 1, next)
|
||||
}
|
||||
const open = () => {
|
||||
const next = current(props.api, props.route)
|
||||
if (next.local > 0) return
|
||||
set(1, next)
|
||||
}
|
||||
const pop = (base?: State) => {
|
||||
const next = base ?? current(props.api, props.route)
|
||||
const local = Math.max(0, next.local - 1)
|
||||
set(local, next)
|
||||
}
|
||||
const show = () => {
|
||||
setTimeout(() => {
|
||||
open()
|
||||
}, 0)
|
||||
}
|
||||
useKeyboard((evt) => {
|
||||
if (props.api.route.current.name !== props.route.screen) return
|
||||
const next = current(props.api, props.route)
|
||||
if (props.api.ui.dialog.open) {
|
||||
if (props.keys.match("dialog_close", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
props.api.ui.dialog.clear()
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (next.local > 0) {
|
||||
if (evt.name === "escape" || props.keys.match("local_close", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
pop(next)
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("local_push", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
push(next)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("home", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
props.api.route.navigate("home")
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("left", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab - 1 + tabs.length) % tabs.length })
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("right", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab + 1) % tabs.length })
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("up", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
props.api.route.navigate(props.route.screen, { ...next, count: next.count + 1 })
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("down", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
props.api.route.navigate(props.route.screen, { ...next, count: next.count - 1 })
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("modal", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
props.api.route.navigate(props.route.modal, next)
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("local", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
open()
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("host", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
host(props.api, props.input, skin)
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("alert", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
warn(props.api, props.route, next)
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("confirm", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
check(props.api, props.route, next)
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("prompt", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
entry(props.api, props.route, next)
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("select", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
picker(props.api, props.route, next)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<box width={dim().width} height={dim().height} backgroundColor={skin.panel} position="relative">
|
||||
<box
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
height="100%"
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
>
|
||||
<box flexDirection="row" justifyContent="space-between" paddingBottom={1}>
|
||||
<text fg={skin.text}>
|
||||
<b>{props.input.label} screen</b>
|
||||
<span style={{ fg: skin.muted }}> plugin route</span>
|
||||
</text>
|
||||
<text fg={skin.muted}>{props.keys.print("home")} home</text>
|
||||
</box>
|
||||
|
||||
<box flexDirection="row" gap={1} paddingBottom={1}>
|
||||
{tabs.map((item, i) => {
|
||||
const on = value.tab === i
|
||||
return (
|
||||
<Btn
|
||||
txt={item}
|
||||
run={() => props.api.route.navigate(props.route.screen, { ...value, tab: i })}
|
||||
skin={skin}
|
||||
on={on}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</box>
|
||||
|
||||
<box
|
||||
border
|
||||
borderColor={skin.border}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
flexGrow={1}
|
||||
>
|
||||
{value.tab === 0 ? (
|
||||
<box flexDirection="column" gap={1}>
|
||||
<text fg={skin.text}>Route: {props.route.screen}</text>
|
||||
<text fg={skin.muted}>plugin state: {props.meta.state}</text>
|
||||
<text fg={skin.muted}>
|
||||
first: {props.meta.state === "first" ? "yes" : "no"} · updated:{" "}
|
||||
{props.meta.state === "updated" ? "yes" : "no"} · loads: {props.meta.load_count}
|
||||
</text>
|
||||
<text fg={skin.muted}>plugin source: {props.meta.source}</text>
|
||||
<text fg={skin.muted}>source: {value.source}</text>
|
||||
<text fg={skin.muted}>note: {value.note || "(none)"}</text>
|
||||
<text fg={skin.muted}>selected: {value.selected || "(none)"}</text>
|
||||
<text fg={skin.muted}>local stack depth: {value.local}</text>
|
||||
<text fg={skin.muted}>host stack open: {props.api.ui.dialog.open ? "yes" : "no"}</text>
|
||||
</box>
|
||||
) : null}
|
||||
|
||||
{value.tab === 1 ? (
|
||||
<box flexDirection="column" gap={1}>
|
||||
<text fg={skin.text}>Counter: {value.count}</text>
|
||||
<text fg={skin.muted}>
|
||||
{props.keys.print("up")} / {props.keys.print("down")} change value
|
||||
</text>
|
||||
</box>
|
||||
) : null}
|
||||
|
||||
{value.tab === 2 ? (
|
||||
<box flexDirection="column" gap={1}>
|
||||
<text fg={skin.muted}>
|
||||
{props.keys.print("modal")} modal | {props.keys.print("alert")} alert | {props.keys.print("confirm")}{" "}
|
||||
confirm | {props.keys.print("prompt")} prompt | {props.keys.print("select")} select
|
||||
</text>
|
||||
<text fg={skin.muted}>
|
||||
{props.keys.print("local")} local stack | {props.keys.print("host")} host stack
|
||||
</text>
|
||||
<text fg={skin.muted}>
|
||||
local open: {props.keys.print("local_push")} push nested · esc or {props.keys.print("local_close")}{" "}
|
||||
close
|
||||
</text>
|
||||
<text fg={skin.muted}>{props.keys.print("home")} returns home</text>
|
||||
</box>
|
||||
) : null}
|
||||
</box>
|
||||
|
||||
<box flexDirection="row" gap={1} paddingTop={1}>
|
||||
<Btn txt="go home" run={() => props.api.route.navigate("home")} skin={skin} />
|
||||
<Btn txt="modal" run={() => props.api.route.navigate(props.route.modal, value)} skin={skin} on />
|
||||
<Btn txt="local overlay" run={show} skin={skin} />
|
||||
<Btn txt="host overlay" run={() => host(props.api, props.input, skin)} skin={skin} />
|
||||
<Btn txt="alert" run={() => warn(props.api, props.route, value)} skin={skin} />
|
||||
<Btn txt="confirm" run={() => check(props.api, props.route, value)} skin={skin} />
|
||||
<Btn txt="prompt" run={() => entry(props.api, props.route, value)} skin={skin} />
|
||||
<Btn txt="select" run={() => picker(props.api, props.route, value)} skin={skin} />
|
||||
</box>
|
||||
</box>
|
||||
|
||||
<box
|
||||
visible={value.local > 0}
|
||||
width={dim().width}
|
||||
height={dim().height}
|
||||
alignItems="center"
|
||||
position="absolute"
|
||||
zIndex={3000}
|
||||
paddingTop={dim().height / 4}
|
||||
left={0}
|
||||
top={0}
|
||||
backgroundColor={RGBA.fromInts(0, 0, 0, 160)}
|
||||
onMouseUp={() => {
|
||||
pop()
|
||||
}}
|
||||
>
|
||||
<box
|
||||
onMouseUp={(evt) => {
|
||||
evt.stopPropagation()
|
||||
}}
|
||||
width={60}
|
||||
maxWidth={dim().width - 2}
|
||||
backgroundColor={skin.panel}
|
||||
border
|
||||
borderColor={skin.border}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
gap={1}
|
||||
flexDirection="column"
|
||||
>
|
||||
<text fg={skin.text}>
|
||||
<b>{props.input.label} local overlay</b>
|
||||
</text>
|
||||
<text fg={skin.muted}>Plugin-owned stack depth: {value.local}</text>
|
||||
<text fg={skin.muted}>
|
||||
{props.keys.print("local_push")} push nested · {props.keys.print("local_close")} pop/close
|
||||
</text>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<Btn txt="push" run={push} skin={skin} on />
|
||||
<Btn txt="pop" run={pop} skin={skin} />
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
const Modal = (props: {
|
||||
api: TuiPluginApi
|
||||
input: Cfg
|
||||
route: Route
|
||||
keys: Keys
|
||||
params?: Record<string, unknown>
|
||||
}) => {
|
||||
const Dialog = props.api.ui.Dialog
|
||||
const value = parse(props.params)
|
||||
const skin = tone(props.api)
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (props.api.route.current.name !== props.route.modal) return
|
||||
|
||||
if (props.keys.match("modal_accept", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
props.api.route.navigate(props.route.screen, { ...value, source: "modal" })
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("modal_close", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
props.api.route.navigate("home")
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<box width="100%" height="100%" backgroundColor={skin.panel}>
|
||||
<Dialog onClose={() => props.api.route.navigate("home")}>
|
||||
<box paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1} flexDirection="column">
|
||||
<text fg={skin.text}>
|
||||
<b>{props.input.label} modal</b>
|
||||
</text>
|
||||
<text fg={skin.muted}>{props.keys.print("modal")} modal command</text>
|
||||
<text fg={skin.muted}>{props.keys.print("screen")} screen command</text>
|
||||
<text fg={skin.muted}>
|
||||
{props.keys.print("modal_accept")} opens screen · {props.keys.print("modal_close")} closes
|
||||
</text>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<Btn
|
||||
txt="open screen"
|
||||
run={() => props.api.route.navigate(props.route.screen, { ...value, source: "modal" })}
|
||||
skin={skin}
|
||||
on
|
||||
/>
|
||||
<Btn txt="cancel" run={() => props.api.route.navigate("home")} skin={skin} />
|
||||
</box>
|
||||
</box>
|
||||
</Dialog>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
const home = (api: TuiPluginApi, input: Cfg) => ({
|
||||
slots: {
|
||||
home_logo(ctx) {
|
||||
const map = ctx.theme.current
|
||||
const skin = look(map)
|
||||
const art = [
|
||||
" $$\\",
|
||||
" $$ |",
|
||||
" $$$$$$$\\ $$$$$$\\$$$$\\ $$$$$$\\ $$ | $$\\ $$$$$$\\",
|
||||
"$$ _____|$$ _$$ _$$\\ $$ __$$\\ $$ | $$ |$$ __$$\\",
|
||||
"\\$$$$$$\\ $$ / $$ / $$ |$$ / $$ |$$$$$$ / $$$$$$$$ |",
|
||||
" \\____$$\\ $$ | $$ | $$ |$$ | $$ |$$ _$$< $$ ____|",
|
||||
"$$$$$$$ |$$ | $$ | $$ |\\$$$$$$ |$$ | \\$$\\ \\$$$$$$$\\",
|
||||
"\\_______/ \\__| \\__| \\__| \\______/ \\__| \\__| \\_______|",
|
||||
]
|
||||
const fill = [
|
||||
skin.accent,
|
||||
skin.muted,
|
||||
ink(map, "info", ui.accent),
|
||||
skin.text,
|
||||
ink(map, "success", ui.accent),
|
||||
ink(map, "warning", ui.accent),
|
||||
ink(map, "secondary", ui.accent),
|
||||
ink(map, "error", ui.accent),
|
||||
]
|
||||
|
||||
return (
|
||||
<box flexDirection="column">
|
||||
{art.map((line, i) => (
|
||||
<text fg={fill[i]}>{line}</text>
|
||||
))}
|
||||
</box>
|
||||
)
|
||||
},
|
||||
home_prompt(ctx, value) {
|
||||
const skin = look(ctx.theme.current)
|
||||
type Prompt = (props: {
|
||||
workspaceID?: string
|
||||
visible?: boolean
|
||||
disabled?: boolean
|
||||
onSubmit?: () => void
|
||||
hint?: JSX.Element
|
||||
right?: JSX.Element
|
||||
showPlaceholder?: boolean
|
||||
placeholders?: {
|
||||
normal?: string[]
|
||||
shell?: string[]
|
||||
}
|
||||
}) => JSX.Element
|
||||
type Slot = (
|
||||
props: { name: string; mode?: unknown; children?: JSX.Element } & Record<string, unknown>,
|
||||
) => JSX.Element | null
|
||||
const ui = api.ui as TuiPluginApi["ui"] & { Prompt: Prompt; Slot: Slot }
|
||||
const Prompt = ui.Prompt
|
||||
const Slot = ui.Slot
|
||||
const normal = [
|
||||
`[SMOKE] route check for ${input.label}`,
|
||||
"[SMOKE] confirm home_prompt slot override",
|
||||
"[SMOKE] verify prompt-right slot passthrough",
|
||||
]
|
||||
const shell = ["printf '[SMOKE] home prompt\n'", "git status --short", "bun --version"]
|
||||
const hint = (
|
||||
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||
<text fg={skin.muted}>
|
||||
<span style={{ fg: skin.accent }}>•</span> smoke home prompt
|
||||
</text>
|
||||
</box>
|
||||
)
|
||||
|
||||
return (
|
||||
<Prompt
|
||||
workspaceID={value.workspace_id}
|
||||
hint={hint}
|
||||
right={
|
||||
<box flexDirection="row" gap={1}>
|
||||
<Slot name="home_prompt_right" workspace_id={value.workspace_id} />
|
||||
<Slot name="smoke_prompt_right" workspace_id={value.workspace_id} label={input.label} />
|
||||
</box>
|
||||
}
|
||||
placeholders={{ normal, shell }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
home_prompt_right(ctx, value) {
|
||||
const skin = look(ctx.theme.current)
|
||||
const id = value.workspace_id?.slice(0, 8) ?? "none"
|
||||
return (
|
||||
<text fg={skin.muted}>
|
||||
<span style={{ fg: skin.accent }}>{input.label}</span> home:{id}
|
||||
</text>
|
||||
)
|
||||
},
|
||||
session_prompt_right(ctx, value) {
|
||||
const skin = look(ctx.theme.current)
|
||||
return (
|
||||
<text fg={skin.muted}>
|
||||
<span style={{ fg: skin.accent }}>{input.label}</span> session:{value.session_id.slice(0, 8)}
|
||||
</text>
|
||||
)
|
||||
},
|
||||
smoke_prompt_right(ctx, value) {
|
||||
const skin = look(ctx.theme.current)
|
||||
const id = typeof value.workspace_id === "string" ? value.workspace_id.slice(0, 8) : "none"
|
||||
const label = typeof value.label === "string" ? value.label : input.label
|
||||
return (
|
||||
<text fg={skin.muted}>
|
||||
<span style={{ fg: skin.accent }}>{label}</span> custom:{id}
|
||||
</text>
|
||||
)
|
||||
},
|
||||
home_bottom(ctx) {
|
||||
const skin = look(ctx.theme.current)
|
||||
const text = "extra content in the unified home bottom slot"
|
||||
|
||||
return (
|
||||
<box width="100%" maxWidth={75} alignItems="center" paddingTop={1} flexShrink={0} gap={1}>
|
||||
<box
|
||||
border
|
||||
borderColor={skin.border}
|
||||
backgroundColor={skin.panel}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
width="100%"
|
||||
>
|
||||
<text fg={skin.muted}>
|
||||
<span style={{ fg: skin.accent }}>{input.label}</span> {text}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const block = (input: Cfg, order: number, title: string, text: string): TuiSlotPlugin => ({
|
||||
order,
|
||||
slots: {
|
||||
sidebar_content(ctx, value) {
|
||||
const skin = look(ctx.theme.current)
|
||||
|
||||
return (
|
||||
<box
|
||||
border
|
||||
borderColor={skin.border}
|
||||
backgroundColor={skin.panel}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
>
|
||||
<text fg={skin.accent}>
|
||||
<b>{title}</b>
|
||||
</text>
|
||||
<text fg={skin.text}>{text}</text>
|
||||
<text fg={skin.muted}>
|
||||
{input.label} order {order} · session {value.session_id.slice(0, 8)}
|
||||
</text>
|
||||
</box>
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const slot = (api: TuiPluginApi, input: Cfg): TuiSlotPlugin[] => [
|
||||
home(api, input),
|
||||
block(input, 50, "Smoke above", "renders above internal sidebar blocks"),
|
||||
block(input, 250, "Smoke between", "renders between internal sidebar blocks"),
|
||||
block(input, 650, "Smoke below", "renders below internal sidebar blocks"),
|
||||
]
|
||||
|
||||
const reg = (api: TuiPluginApi, input: Cfg, keys: Keys) => {
|
||||
const route = names(input)
|
||||
api.command.register(() => [
|
||||
{
|
||||
title: `${input.label} modal`,
|
||||
value: "plugin.smoke.modal",
|
||||
keybind: keys.get("modal"),
|
||||
category: "Plugin",
|
||||
slash: {
|
||||
name: "smoke",
|
||||
},
|
||||
onSelect: () => {
|
||||
api.route.navigate(route.modal, { source: "command" })
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${input.label} screen`,
|
||||
value: "plugin.smoke.screen",
|
||||
keybind: keys.get("screen"),
|
||||
category: "Plugin",
|
||||
slash: {
|
||||
name: "smoke-screen",
|
||||
},
|
||||
onSelect: () => {
|
||||
api.route.navigate(route.screen, { source: "command", tab: 0, count: 0 })
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${input.label} alert dialog`,
|
||||
value: "plugin.smoke.alert",
|
||||
category: "Plugin",
|
||||
slash: {
|
||||
name: "smoke-alert",
|
||||
},
|
||||
onSelect: () => {
|
||||
warn(api, route, current(api, route))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${input.label} confirm dialog`,
|
||||
value: "plugin.smoke.confirm",
|
||||
category: "Plugin",
|
||||
slash: {
|
||||
name: "smoke-confirm",
|
||||
},
|
||||
onSelect: () => {
|
||||
check(api, route, current(api, route))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${input.label} prompt dialog`,
|
||||
value: "plugin.smoke.prompt",
|
||||
category: "Plugin",
|
||||
slash: {
|
||||
name: "smoke-prompt",
|
||||
},
|
||||
onSelect: () => {
|
||||
entry(api, route, current(api, route))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${input.label} select dialog`,
|
||||
value: "plugin.smoke.select",
|
||||
category: "Plugin",
|
||||
slash: {
|
||||
name: "smoke-select",
|
||||
},
|
||||
onSelect: () => {
|
||||
picker(api, route, current(api, route))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${input.label} host overlay`,
|
||||
value: "plugin.smoke.host",
|
||||
category: "Plugin",
|
||||
slash: {
|
||||
name: "smoke-host",
|
||||
},
|
||||
onSelect: () => {
|
||||
host(api, input, tone(api))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${input.label} go home`,
|
||||
value: "plugin.smoke.home",
|
||||
category: "Plugin",
|
||||
enabled: api.route.current.name !== "home",
|
||||
onSelect: () => {
|
||||
api.route.navigate("home")
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${input.label} toast`,
|
||||
value: "plugin.smoke.toast",
|
||||
category: "Plugin",
|
||||
onSelect: () => {
|
||||
api.ui.toast({
|
||||
variant: "info",
|
||||
title: "Smoke",
|
||||
message: "Plugin toast works",
|
||||
duration: 2000,
|
||||
})
|
||||
},
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
const tui: TuiPlugin = async (api, options, meta) => {
|
||||
if (options?.enabled === false) return
|
||||
|
||||
await api.theme.install("./smoke-theme.json")
|
||||
api.theme.set("smoke-theme")
|
||||
|
||||
const value = cfg(options ?? undefined)
|
||||
const route = names(value)
|
||||
const keys = api.keybind.create(bind, value.keybinds)
|
||||
const fx = new VignetteEffect(value.vignette)
|
||||
const post = fx.apply.bind(fx)
|
||||
api.renderer.addPostProcessFn(post)
|
||||
api.lifecycle.onDispose(() => {
|
||||
api.renderer.removePostProcessFn(post)
|
||||
})
|
||||
|
||||
api.route.register([
|
||||
{
|
||||
name: route.screen,
|
||||
render: ({ params }) => <Screen api={api} input={value} route={route} keys={keys} meta={meta} params={params} />,
|
||||
},
|
||||
{
|
||||
name: route.modal,
|
||||
render: ({ params }) => <Modal api={api} input={value} route={route} keys={keys} params={params} />,
|
||||
},
|
||||
])
|
||||
|
||||
reg(api, value, keys)
|
||||
for (const item of slot(api, value)) {
|
||||
api.slots.register(item)
|
||||
}
|
||||
}
|
||||
|
||||
const plugin: TuiPluginModule & { id: string } = {
|
||||
id: "tui-smoke",
|
||||
tui,
|
||||
}
|
||||
|
||||
export default plugin
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: effect
|
||||
description: Work with Effect v4 / effect-smol TypeScript code in this repo
|
||||
---
|
||||
|
||||
# Effect
|
||||
|
||||
This codebase uses Effect for typed, composable TypeScript services, schemas, and workflows.
|
||||
|
||||
## Source Of Truth
|
||||
|
||||
Use the current Effect v4 / effect-smol source, not memory or older Effect v2/v3 examples.
|
||||
|
||||
1. If `.opencode/references/effect-smol` is missing, clone `https://github.com/Effect-TS/effect-smol` there. Do this in the project, not in the skill folder.
|
||||
2. Search `.opencode/references/effect-smol` for exact APIs, examples, tests, and naming patterns before answering or implementing Effect-specific code.
|
||||
3. Also inspect existing repo code for local house style before introducing new patterns.
|
||||
4. Prefer answers and implementations backed by specific source files or nearby repo examples.
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Prefer current Effect v4 APIs and project-local patterns over old blog posts, examples, or package-memory guesses.
|
||||
- Use `Effect.gen(function* () { ... })` for multi-step workflows.
|
||||
- Use `Effect.fn("Name")` or `Effect.fnUntraced(...)` for named effects when adding reusable service methods or important workflows.
|
||||
- Prefer Effect `Schema` for API and domain data shapes. Use branded schemas for IDs and `Schema.TaggedErrorClass` for typed domain errors when modeling new error surfaces.
|
||||
- Keep HTTP handlers thin: decode input, read request context, call services, and map transport errors. Put business rules in services.
|
||||
- In Effect service code, prefer Effect-aware platform abstractions and dependencies over ad hoc promises where the surrounding code already does so.
|
||||
- Keep layer composition explicit. Avoid broad hidden provisioning that makes missing dependencies hard to see.
|
||||
- In tests, prefer the repo's existing Effect test helpers and live tests for filesystem, git, child process, locks, or timing behavior.
|
||||
- Do not introduce `any`, non-null assertions, unchecked casts, or older Effect APIs just to satisfy types.
|
||||
- Do not answer from memory. Verify against `.opencode/references/effect-smol` or nearby code first.
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
- Use `testEffect(...)` from `packages/opencode/test/lib/effect.ts` for tests that exercise Effect services, layers, runtime context, scoped resources, or platform integrations.
|
||||
- Use `it.live(...)` for filesystem, git repositories, HTTP servers, sockets, child processes, locks, real time, and other live platform behavior.
|
||||
- Run tests from package directories such as `packages/opencode`; never run package tests from the repo root.
|
||||
- Prefer explicit test layers over ad hoc managed runtimes. Keep dependency provisioning visible in the test file.
|
||||
- Use scoped fixtures and finalizers for resources that must be cleaned up, including temporary directories, flags, databases, fibers, servers, and global state.
|
||||
@@ -0,0 +1 @@
|
||||
smoke-theme.json
|
||||
@@ -116,8 +116,8 @@
|
||||
"light": "nord5"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "nord2",
|
||||
"light": "nord4"
|
||||
"dark": "#abafb7",
|
||||
"light": "textMuted"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#3B4252",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user