mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-31 01:37:28 +08:00
1346963e59
* feat: daily docs-sync bot workflow (Kilo CLI) Adds a scheduled workflow that keeps packages/kilo-docs in sync with PRs merged to Kilo-Org/cloud and Kilo-Org/kilocode: - watermark.mjs derives the processing window from the bot's own PR body marker (self-healing, no external state; 72h fallback, 14d cap) - collect.mjs queries merged PRs via the GitHub API and applies a deterministic pre-filter (bots, chores, docs-only PRs) - triage.mjs classifies PRs in chunks of 25 with kilo run; failed chunks degrade to unclassified instead of failing the run - edit.mjs updates docs in batches of 5 PRs with kilo run, bounded per batch; failures surface as skipped entries in the PR body - verify runs the kilo-docs build + test suite; one LLM fix pass on failure; still-red becomes a draft PR - upsert-pr.mjs maintains one rolling auto-docs PR (appends while open, fresh branch after merge), with a 15-file draft cap and a machine-readable processed-through watermark Also adds docs-sync.yml to the workflow allowlist in script/check-workflows.ts. * fix: correct kilo run invocation and auth - message positional must come before flags: --file is multi-value and consumes a trailing message as a file path (File not found) - authenticate via the existing KILO_API_KEY repo secret (the kilo provider reads it natively); drop the DOCS_SYNC_KILO_CONFIG config secret requirement - fix default model IDs: gateway provider id is kilo/, not kilocode/ - include stderr tail in triage/edit failure logs * fix: handle kilo run double-printed assistant output kilo run prints the assistant message twice (streaming render + final summary), so stdout can contain the same JSON array back-to-back. Parse the largest valid trailing array instead of slicing first-to-last bracket. Verified against real chunked triage output. * fix: reviewer-pass robustness fixes - edit.mjs: unambiguous summary file path in the batch prompt and a fallback read when the agent drops the docs-sync-out/ prefix, so real edits never report as skipped - prepare-branch.mjs: use the open auto-docs PR's actual head.ref instead of assuming docs/auto-sync - upsert-pr.mjs: compute the 15-file draft cap on the cumulative PR diff (origin/main...HEAD), not just the latest commit * fix: address Kilobot review findings Security: - sanitize HTML-comment sequences out of agent-generated PR body values so a crafted value cannot forge section markers or the watermark - draft any PR whose diff touches non-content files in packages/kilo-docs (outside pages/ and lib/nav/) — build-executable changes force human review before merge - on merge conflict, keep the conflicted rolling branch untouched (preserving human commits) and continue on a fresh dated branch that links the old PR Resilience: - retry GitHub API calls on network errors and 5xx, not just 403 rate limits - isolate per-PR collect failures instead of aborting the run - trust watermark markers only on bot-authored PRs and clamp future dates loudly - validate chunk triage entries belong to their chunk before the shared dedupe - use changed_files for files_total and skip docs-only classification on truncated (300+) file lists - pipe stderr in the edit pass so failure warnings carry the real CLI error * fix: address second Kilobot review round - escape pipe characters in changeRow actions (same as skippedRow) - sanitize agent-chosen file paths before they land in draftReasons and the PR body (residual marker-forgery path via filenames) - log expected fetch misses in prepare-branch instead of silent catches * feat: keep bot-authored PRs in the docs-sync digest Release and dependency bots ship user-facing changes (e.g. JetBrains release PRs from kilo-maintainer[bot]). The auto-docs label check and docs-only path filter remain as the loop guards.
123 lines
5.0 KiB
JavaScript
123 lines
5.0 KiB
JavaScript
// kilocode_change - new file
|
|
|
|
/**
|
|
* Runs the LLM edit pass over docs-sync-out/worthy.json in batches.
|
|
*
|
|
* Batching bounds each `kilo run` context (a replay window can yield dozens
|
|
* of docs-worthy PRs with large diffs). Each batch gets its own CLI session
|
|
* and writes its own summary file; results are merged into
|
|
* docs-sync-out/edit-summary.json. A batch that fails is skipped with a
|
|
* warning — its PRs show up in the rolling PR body as skipped, so nothing
|
|
* fails silently.
|
|
*
|
|
* Env: EDIT_MODEL (provider/model), KILO_API_KEY (set by workflow; read natively by the kilo provider).
|
|
*/
|
|
|
|
import { execFileSync } from "node:child_process"
|
|
import fs from "node:fs"
|
|
import path from "node:path"
|
|
import { fileURLToPath } from "node:url"
|
|
|
|
const BATCH_SIZE = 5
|
|
const ATTEMPTS = 2
|
|
const OUT_DIR = "docs-sync-out"
|
|
export const SUMMARY_FILE = ".docs-sync-summary.json"
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
const basePrompt = fs.readFileSync(path.join(HERE, "edit-prompt.md"), "utf8")
|
|
const model = process.env.EDIT_MODEL
|
|
if (!model) throw new Error("EDIT_MODEL is required")
|
|
|
|
const worthy = JSON.parse(fs.readFileSync(`${OUT_DIR}/worthy.json`, "utf8"))
|
|
const triage = JSON.parse(fs.readFileSync(`${OUT_DIR}/triage.json`, "utf8"))
|
|
const priority = new Map(triage.map((e) => [e.url, e]))
|
|
const ordered = [...worthy].sort((a, b) => {
|
|
const rank = { high: 0, medium: 1, low: 2 }
|
|
return (rank[priority.get(a.url)?.priority] ?? 1) - (rank[priority.get(b.url)?.priority] ?? 1)
|
|
})
|
|
|
|
function editBatch(batch, index) {
|
|
const batchFile = `${OUT_DIR}/edit-batch-${index}.json`
|
|
const triageFile = `${OUT_DIR}/edit-batch-triage-${index}.json`
|
|
const summaryFile = `${OUT_DIR}/edit-summary-${index}.json`
|
|
fs.writeFileSync(batchFile, JSON.stringify(batch, null, 2))
|
|
fs.writeFileSync(
|
|
triageFile,
|
|
JSON.stringify(
|
|
batch.map((d) => priority.get(d.url)).filter(Boolean),
|
|
null,
|
|
2,
|
|
),
|
|
)
|
|
|
|
const prompt = `${basePrompt}
|
|
|
|
Batch specifics for this run: the PRs to handle are in the attached ${batchFile} (full details) and ${triageFile} (triage verdicts). Handle ONLY the PRs in these batch files. When finished, write your per-PR results in the summary JSON format described above to the file \`${summaryFile}\` (path relative to the repository root).`
|
|
|
|
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
|
try {
|
|
// Message positional first: --file is multi-value and would otherwise
|
|
// consume a trailing message as a file path ("File not found").
|
|
execFileSync(
|
|
"kilo",
|
|
["run", prompt, "-m", model, "--variant", "high", "--dir", process.cwd(), "-f", batchFile, "-f", triageFile],
|
|
// stdout streams live to the Actions log; stderr is piped so failure
|
|
// warnings can include the tail of the actual CLI error.
|
|
{ encoding: "utf8", maxBuffer: 32 * 1024 * 1024, timeout: 25 * 60 * 1000, stdio: ["ignore", "inherit", "pipe"] },
|
|
)
|
|
if (fs.existsSync(summaryFile)) return true
|
|
// Tolerate the agent dropping the docs-sync-out/ prefix.
|
|
const alt = path.basename(summaryFile)
|
|
if (fs.existsSync(alt)) {
|
|
fs.renameSync(alt, summaryFile)
|
|
return true
|
|
}
|
|
console.warn(`batch ${index} attempt ${attempt}: summary file ${summaryFile} not produced`)
|
|
} catch (err) {
|
|
const stderr = String(err.stderr ?? "").trim().split("\n").slice(-5).join("\n")
|
|
console.warn(`batch ${index} attempt ${attempt}: kilo run failed: ${stderr || err.message}`)
|
|
}
|
|
}
|
|
console.warn(`::warning::edit batch ${index} failed after ${ATTEMPTS} attempts; ${batch.length} PRs skipped`)
|
|
return false
|
|
}
|
|
|
|
const batches = []
|
|
for (let i = 0; i < ordered.length; i += BATCH_SIZE) {
|
|
batches.push(ordered.slice(i, i + BATCH_SIZE))
|
|
}
|
|
console.log(`editing docs for ${ordered.length} PRs in ${batches.length} batches of up to ${BATCH_SIZE}`)
|
|
|
|
for (let i = 0; i < batches.length; i++) {
|
|
editBatch(batches[i], i)
|
|
}
|
|
|
|
// Merge batch summaries. Coverage: every worthy PR gets an entry so the PR
|
|
// body accounts for it; failed batches show up as skipped.
|
|
const merged = []
|
|
const seen = new Set()
|
|
for (let i = 0; i < batches.length; i++) {
|
|
const file = `${OUT_DIR}/edit-summary-${i}.json`
|
|
let entries = []
|
|
try {
|
|
entries = JSON.parse(fs.readFileSync(file, "utf8"))
|
|
} catch {
|
|
continue
|
|
}
|
|
for (const e of entries) {
|
|
const url = String(e?.url ?? "")
|
|
if (!url.startsWith("http") || seen.has(url)) continue
|
|
seen.add(url)
|
|
merged.push({ pr: Number(e.pr) || 0, url, action: String(e.action ?? "skipped"), reason: String(e.reason ?? "") })
|
|
}
|
|
}
|
|
for (const d of ordered) {
|
|
if (seen.has(d.url)) continue
|
|
merged.push({ pr: d.number, url: d.url, action: "skipped", reason: "edit pass failed or timed out for this PR" })
|
|
}
|
|
|
|
// upsert-pr.mjs consumes the merged summary from the repo root; the file is
|
|
// removed there before committing so it never lands in the docs PR.
|
|
fs.writeFileSync(SUMMARY_FILE, JSON.stringify(merged, null, 2))
|
|
console.log(`edit pass complete: ${merged.filter((e) => e.action !== "skipped").length} changed, ${merged.filter((e) => e.action === "skipped").length} skipped`)
|