Skip to content

fix(ci): docs-sync bot passes --auto, drains its backlog, and reports readable causes; fix(cli): honest exit codes for headless runs - #12605

Open
iscekic wants to merge 9 commits into
mainfrom
docs-sync-cli-47f4
Open

fix(ci): docs-sync bot passes --auto, drains its backlog, and reports readable causes; fix(cli): honest exit codes for headless runs#12605
iscekic wants to merge 9 commits into
mainfrom
docs-sync-cli-47f4

Conversation

@iscekic

@iscekic iscekic commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Issue

Closes the CLI-side follow-up that #12580 deliberately deferred. No separate issue exists — #12580 made the docs-sync bot survive broken CLI calls (pending rows, held-back watermark, stderr capture on failure); this PR fixes the causes.

Context

The daily docs-sync bot drives the Kilo CLI headless to update packages/kilo-docs. Its CLI calls were doing no work whenever the agent reached for a shell command, and nothing could see it:

  1. Headless kilo run auto-rejects every permission ask it receives. Without --auto, a plain kilo run replies reject to any bash permission ask (run.ts). A GitHub runner has no user-level config granting bash: allow, so ordinary read-only commands (sed -n …, git log … -- <path>, find …) are auto-rejected and the run ends without writing its summary. All three docs-sync call sites omitted --auto. In CI run 30306629290 this fired 9 times (auto-rejectingError: The user rejected permission…), all 11 edit batches failed across 3 attempts, 0 summary files. The defect is intermittent by model behaviour — read/edit/glob/grep are allowed outright, so a run whose model never shells out succeeds anyway — which is exactly what made it hard to see.

  2. A crippled run was indistinguishable from a healthy one. Non-interactive kilo run exited 0 for runs that accomplished nothing: the permission-rejected case above reached idle normally and exited 0, and the bot also discarded the child's stderr on the success path, so a "successful" call that was auto-rejected left no trace anywhere.

  3. Throughput, not correctness, is what defers most PRs. --auto fixes the batches the bot attempted; it does not fix the ones it never started. In the same CI run 30306629290 (254 PRs collected, 51 docs-worthy), the wall-clock budgets deferred 54 PRs untriaged (triage got 8 of 11 chunks in 35 min) and 31 unedited (edit got 4 of 11 batches in 50 min) with no attempt made. That is 45 of the 60 pending rows on rolling PR docs: auto-sync with merged PRs (through 2026-07-27) #12521; the auto-reject defect accounts for the other 15. Fixing only the first two defects would leave the bot delivering ~20 PRs/run against an 11-day backlog.

  4. Pending causes are unreadable. runKilo's stderrTail feeds the rolling PR's pending table, and kilo renders its TUI to stderr, so every "Why" cell reads ^[[0m→ ^[[0mRead packages/... instead of the diagnostic.

Implementation

Two commits, two halves of the fix:

fix(ci) — the bot's side (takes effect on merge):

  • Pass --auto at all three docs-sync CLI call sites (triage.mjs, edit.mjs, the Fix verify failures step in docs-sync.yml), immediately after "run", with a comment explaining why. --auto is the documented flag for autonomous/pipeline usage; the bot is scheduled-only on main, runs on a trusted runner, never executes PR-branch code, and upsert-pr.mjs stages only packages/kilo-docs — which bounds what can be committed, not what the agent can execute (see the residual-risk note below).
  • runKilo now always persists the child's output to docs-sync-out/ (full stderr to kilo-stderr-<label>.log, 8 MB cap, warn-and-continue on write failure) — previously stderr surfaced only on failure, so a run that succeeded while being auto-rejected left no trace. This is what makes the rejection check observable at all. Because these logs land in uploaded CI artifacts — raw files on a public repo, and GitHub masks secrets in log streams only — all persisted output (stderr and stdout, including the triage-raw-*.txt snapshots) is redacted at capture with redactEnvSecrets, and the Fix verify failures step streams through redact-stream.mjs before tee reaches edit-log.txt: exact values of env vars named KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|ORG_ID|_PAT (length ≥ 8), longest-first so a shorter secret that prefixes a longer one cannot leak a remainder (selftest cases 2d–2g, proven red-first).
  • selftest.mjs asserts all of this non-vacuously: region-scoped matchers (the args: [ … ] array slice; the workflow step's joined run: | block) so a comment can't satisfy them, plus a stub-invocation case that requires callCount > 0 and --auto in every recorded argv, and two stub cases proving stderr is persisted on both the failing and the summary-writing success path.

fix(cli) — honest exit codes (reaches the bot when @kilocode/cli is next published):

  • Deliberate contract change: a plain headless run (neither --auto nor --dangerously-skip-permissions) in which the CLI auto-rejected any permission ask now exits 1 with run ended with an auto-rejected permission; pass --auto for autonomous use on stderr — even if the agent recovered via allowlisted tools afterwards. The blunter rule was chosen over "non-zero only when nothing was accomplished" because "accomplished" has no crisp definition, and that fuzziness is what let this failure hide for days. Correction to an earlier claim in this description: this was previously stated as "no known caller branches on this exit code". One does, indirectly — the Fix verify failures step runs under set -o pipefail and the default bash -e, so a nonzero kilo run would abort the block before the rebuild, leaving verify2.log unwritten and Re-verify status reporting VERIFIED=false even when the docs build fine. Fixed here by guarding that pipeline with || echo "::warning::…" so the rebuild stays authoritative (selftest case 2b, shown red with the guard removed). The remaining callers do not branch on it: docs-sync decides on the summary file / parseable stdout, and the kilo workflow's dispatchers read sentinel lines. This replaces a test that locked the old exit-0 behaviour in on purpose; the replacement records what it used to lock in and why the contract changed. The flag is set in both the root and the subagent auto-reject branches (same defect class; the subagent branch has no harness fixture — recorded gap, no test).
  • A run whose session errors mid-stream already exited 1, but the stderr diagnostic was silently skipped under --format json by an if (emit(...)) continue early-out — stderr is now written before the event emit in both formats.
  • Diagnosis-before-fix was enforced: the third hypothesized mechanism (event stream ending without idle) proved unreachable in the non-interactive flow and was dropped with evidence rather than faked. The session.error-id-mismatch widening was likewise not applied — the diagnosis showed the error event carries the run's own session id.
  • Interactive mode, --attach, the network-retry block, and what the permission branches approve or reject are untouched — only the exit code changes.

fix(ci) — throughput (takes effect on merge):

  • TRIAGE_BUDGET_MINUTES: "90" and EDIT_BUDGET_MINUTES: "120" on their workflow steps (both already Number(env) || default in triage.mjs / edit.mjs, so this is env-only), with timeout-minutes 120 → 240. These are ceilings, not costs: a caught-up run needs ~2 chunks and ~1 batch and finishes in ~25 min, so raising them spends nothing on a normal day and drains the backlog on a bad one. 90/120 covers 20 chunks and 14 batches — 500 triaged and 70 edited PRs — against a ~5 docs-worthy/day inflow, inside the 240-minute job timeout (228 planned, 12-minute reserve).
  • The 90 for triage carries deliberate headroom: chunks averaged 3.3 min while their agent was being rejected out of shell commands, and under --auto they do real work. 90 still covers 254 PRs even if chunk time doubles.
  • Deliberately not done: edit.mjs:77,141 reserves a full EDIT_BATCH_TIMEOUT_MINUTES (15) before starting a batch even though a healthy batch is ~8 min — that is what turned "590s left" into 31 deferred PRs. It is a real flaw, but at a 120-minute budget the gate stops at batch 14 and only 11 are needed, so fixing it buys nothing today. Same for parallelising batches (they share one worktree) and alarming on the 14-day lookback cap (unreachable once the watermark catches up).
  • ANSI CSI sequences are stripped in tailText — the shared path both triage.mjs and edit.mjs route their pending causes through, rather than at each call site. Stripped before the line/char slice so escapes do not consume the tail budget. The persisted docs-sync-out/kilo-stderr-*.log stays raw; it is the debugging record.
  • selftest.mjs case 2h asserts the pending reason in .docs-sync-summary.json is escape-free and still carries the diagnostic text (non-vacuous both ways), and that the artifact log stays raw. Case 2i asserts each budget is set and fits at least two units, and that the job timeout outlasts both — the invariant that matters, since a budget below one unit timeout silently runs zero units. Both shown failing on the unmodified code first (EDIT_BUDGET_MINUTES=10 → red; env removed → red).

Publish-lag split, stated plainly: docs-sync.yml installs the published CLI (npm install -g @kilocode/cli; 7.4.16 in the failing runs). The fix(ci) commit changes the bot's own scripts, so it fixes production on merge. The fix(cli) commit changes CLI source, so the bot gets honest exit codes only after the next @kilocode/cli release. The bot does not "now report failures honestly" before that.

Residual risk, stated plainly (deferred hardening): with --auto, an agent steered by injected PR content (merged-PR titles/bodies/diffs from a public repo, outside contributors included) can run arbitrary shell in the job. KILO_API_KEY/KILO_ORG_ID are job-level env, so the LLM steps necessarily hold them; GH_TOKEN is step-scoped to the non-LLM steps only, but actions/checkout@v6 defaults to persist-credentials: true, leaving a pushable credential in .git/config for the whole job. The exfiltration channel this PR does close: every persisted/streamed kilo log is redacted at capture. Deliberately not in this PR: (a) a scoped permission.bash map via KILO_CONFIG_CONTENT — the right hardening, but the agent's shell patterns are unpredictable, and on the published CLI (7.4.16) a too-narrow map silently re-creates the original no-work defect, whose honest exit codes only ship with the next release; (b) persist-credentials: false — the rolling-branch push in upsert-pr.mjs relies on the stored credential and that path is only verifiable on a live scheduled run. Tracked in the deliberately unresolved triage.mjs review thread for the human reviewer.

Screenshots / Video

N/A — CI scripts and CLI process behaviour, no visual changes.

How to Test

Manual/local verification (all executed by the agent)

  • r0 repro gate (pre-fix, on the unmodified baseline): the deterministic local repro produced the verbatim signature permission requested: bash (sed -n 1,5p README.md); auto-rejecting, no completed answer, exit 0; the --auto control ran clean (zero rejections, command executed, answer produced). This pair is reproduced post-fix as AC1a below.

  • node .github/docs-sync/selftest.mjs — all cases pass, including the new --auto and unconditional-stderr assertions (each shown failing on unmodified code first).

  • oxlint .github/docs-sync — 0 errors; bun run script/check-workflows.ts — ok (29 workflows).

  • From packages/opencode/: bun run typecheck clean; bun test ./test/cli/run/run-process.test.ts8 pass / 0 fail, including the new non-zero-exit cases and the unchanged happy-path / --format json guards.

  • bun run script/check-opencode-annotations.ts --worktree — clean; every shared-file edit is wrapped in kilocode_change markers.

  • AC1a (deterministic two-way proof, post-fix): PASS. Same argv shape edit.mjs uses, XDG_CONFIG_HOME isolation, a prompt that must shell out (sed -n 1,5p README.md), published CLI 7.4.16, kimi-k3 --variant high. Without --auto: exactly one permission requested: bash (sed -n 1,5p README.md); auto-rejecting, the README lines never appear. With --auto: zero auto-rejecting lines and the sed output is present. (One pre-agent startup crash from a local credential-store write race on the shared verification machine — classified a test-environment failure, retried clean; recorded in the workflow learnings.)

  • AC1b (batch integration regression, post-fix): PASS. Real .github/docs-sync/edit.mjs over the CI-run-30306629290 fixtures (5 PRs = one batch), clean start enforced. docs-sync-out/edit-summary-0.json produced by this run with one entry per batch-0 PR (3 updated, 2 skipped; console edit pass complete: 3 changed, 2 skipped, 0 pending, ~6 min). The new unconditional stderr log kilo-stderr-edit-batch-0-attempt-1.log, ANSI-stripped: 0 auto-rejecting lines, 0 permission requested: bash / ✗ … failed markers, 14 successful $ bash renders — the agent shelled out and bash ran under --auto. AC1b alone proves only that the added argv token did not break the invocation (the baseline passes without --auto when the model stays on allowed tools); AC1a is the proof the defect is fixed.

  • node .github/docs-sync/selftest.mjs — all cases pass including 2h/2i; each shown red first on unmodified code.

  • oxlint .github/docs-sync — 0 errors, 2 warnings, both pre-existing on the branch (verified by stashing).

  • bun run script/check-workflows.ts — ok (29 workflows); bun run script/check-opencode-annotations.ts --worktree — clean.

  • Budget arithmetic is checked by case 2i rather than asserted in prose: gates are remaining >= unit timeout, so 90/10 → 20 chunks and 120/15 → 14 batches.

Reviewer test steps

  1. env XDG_CONFIG_HOME=$(mktemp -d) kilo run 'Run exactly this shell command and show its output: sed -n 1,5p README.md' -m kilo/moonshotai/kimi-k3 --dir "$PWD" → observe auto-rejecting and no command output (the isolation matters: a dev machine with permission.bash: "allow" in its user config hides the defect).
  2. Same command with --auto → the command runs.
  3. git show the two commits; run the selftest and the run-process tests as above.

Blocked checks and substitute verification

  • The CLI-side exit-code change cannot be exercised through the published kilo binary (7.4.16 predates it) — substitute verification is the run-process test harness running the worktree source, per the publish-lag split above.

What to watch after merge

The first scheduled docs-sync run after merge (07:00 UTC daily) is the real-world confirmation: batches should now produce summary files and their docs-sync-out/kilo-stderr-*.log artifacts will show zero auto-rejecting lines. One measurement that must not be undone: a healthy --auto batch takes ~8 minutes (7m45s measured at the r0 gate) because the agent now really runs its shell commands — production's 15-minute EDIT_BATCH_TIMEOUT_MINUTES default is correct and must not be tightened.

The throughput change has its own signal: that run should defer zero PRs for "budget exhausted" and drain #12521's backlog in one pass, advancing the watermark ~11 days in a single run instead of the ~1 hour it has been managing. If chunk or batch times come in far above the ~3.3 min / ~8 min this sizing assumes, the budgets are the knob to turn, not the batch size. Note the daily runs of 07-26 → 07-28 died before the edit pass on the git-identity fault that #12580 fixed at 08:40 UTC on 07-28; the 07-29 run is the first with that fix, so it is also the first meaningful production sample.

Checklist

  • Issue linked above, or exception explained (no dedicated issue; closes fix(ci): docs-sync bot — no errors, no timeouts, no lost PRs #12580's deferred CLI follow-up)
  • Tests/verification described
  • Screenshots/video included for visual changes, or marked N/A
  • Changeset included (.changeset/headless-run-honest-exit.md, @kilocode/cli patch) for the CLI exit-code contract change; no user-facing UI changes
  • I personally reviewed the diff and can explain the changes, including any AI-assisted work.

iscekic added 2 commits July 28, 2026 15:12
…logs

Headless kilo run auto-rejects every permission ask it receives, and the
GitHub runner has no user config granting bash — so without --auto the
docs-sync bot's triage, edit and verify-fix calls were silently crippled
whenever the agent reached for a non-allowlisted shell command (CI run
30306629290: 9 rejections, all 11 edit batches failed, exit 0).

- Pass --auto immediately after "run" at all three call sites
  (triage.mjs, edit.mjs, docs-sync.yml Fix verify failures step)
- runKilo now always writes the child's full stderr to
  docs-sync-out/kilo-stderr-<label>.log, on success as well as failure —
  the blindness that hid the defect
- selftest asserts --auto non-vacuously (region-scoped source match +
  a stub invocation that records argv) and proves the stderr log is
  written on both the failure and the summary-writing success path
…n errors

Non-interactive kilo run reported success for runs that accomplished
nothing — a caller cannot distinguish success from a dead run, which is
why the docs-sync bot had to stop trusting exit codes entirely.

- Plain headless run (neither --auto nor --dangerously-skip-permissions)
  in which the CLI auto-rejected at least one permission ask now exits
  non-zero, even when the session still reaches idle afterwards, with
  the diagnostic: run ended with an auto-rejected permission; pass
  --auto for autonomous use. Deliberate contract change: any
  auto-rejected ask means the run was crippled, not successful.
- A mid-stream session error now writes its diagnostic to stderr before
  emitting the json error event, so the cause is visible under
  --format json as well (the emit-first shape skipped the stderr write).
- The old exit-0 contract lock-in test is replaced: its llm.fail fixture
  never published a consumable session.error, so it locked in a false
  premise. New tests cover both scenarios in both output formats;
  happy-path and --format json runs still exit 0 unchanged.
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs (docs-staging) Ready Ready Preview, Comment Jul 28, 2026 7:46pm

Request Review

Comment thread .github/docs-sync/lib.mjs
Comment thread .github/docs-sync/triage.mjs
@kilo-code-bot

kilo-code-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
.github/docs-sync/triage.mjs 78 --auto grants the agent unattended command execution while processing untrusted PR content, on a runner holding real secrets (author acknowledged, declined to change — rationale on the thread)

Resolved since last review:

  • .github/docs-sync/lib.mjs — the unredacted-stderr-in-artifact WARNING is fixed: 090dfd0411 adds redactEnvSecrets(), applied to both the persisted docs-sync-out/kilo-stderr-*.log artifact and the console tail, with a new selftest case (2d) that proves a KILO_API_KEY-shaped secret is stripped from both, via a real subprocess run rather than mocks.
  • CLI exit-code changeset SUGGESTION is fixed: .changeset/headless-run-honest-exit.md was added, describing the behavior change from a user's perspective.

The rest of the incremental diff (i18n files, Agent Manager terminal work, nix/flake updates, etc.) came in via the merge commit from main and is not new work introduced by this PR — out of scope for this review.

Fix these issues in Kilo Cloud

Files Reviewed (3 files)
  • .changeset/headless-run-honest-exit.md - 0 issues
  • .github/docs-sync/lib.mjs - 0 issues (previous WARNING resolved)
  • .github/docs-sync/selftest.mjs - 0 issues
Previous Review Summary (commit 20b058b)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 20b058b)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
.github/docs-sync/lib.mjs 218 Full, unredacted kilo run stderr is now captured unconditionally and shipped in a CI artifact (always() upload, 14-day retention)
.github/docs-sync/triage.mjs 78 --auto now approves all permission asks (incl. bash) while the agent processes untrusted PR content, on a runner holding real secrets

SUGGESTION

  • No changeset found for the CLI exit-code contract change in packages/opencode/src/cli/cmd/run.ts (a plain headless kilo run now exits 1 instead of 0 when a permission ask was auto-rejected). This is a user-visible behavior change for anyone scripting kilo run and is worth a .changeset/*.md entry per repo convention.

The CLI-side logic in run.ts (the autoRejected flag, moving UI.error before emit so the diagnostic survives --format json, and the replaced mid-stream-error test) was traced through and looks correct — the removed continue doesn't skip any now-relevant branches, and UI.error/println write to stderr so --format json stdout stays clean JSON. The new run-process.test.ts cases exercise real CLI behavior via the existing llm.error/llm.tool test harness, not mocks.

Fix these issues in Kilo Cloud

Files Reviewed (7 files)
  • .github/docs-sync/edit.mjs - 0 issues
  • .github/docs-sync/lib.mjs - 1 issue
  • .github/docs-sync/selftest.mjs - 0 issues
  • .github/docs-sync/triage.mjs - 1 issue
  • .github/workflows/docs-sync.yml - 0 issues
  • packages/opencode/src/cli/cmd/run.ts - 0 issues (missing changeset noted separately)
  • packages/opencode/test/cli/run/run-process.test.ts - 0 issues

Reviewed by claude-sonnet-5 · Input: 44 · Output: 8K · Cached: 1.4M

Review guidance: REVIEW.md from base branch main

The full-stderr capture added for observability lands in uploaded CI
artifacts, which are raw files (GitHub masks secrets in log streams
only) on a public repo, and the runner env holds a long-lived
KILO_API_KEY. Redact exact values of KEY|TOKEN|SECRET-named env vars
(len >= 8) once at capture, so both the console tail and the artifact
file are safe. Adds the changeset for the headless exit-code contract
change.
@iscekic

iscekic commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

(bot) @kilocode-bot please review the latest head (090dfd0). Changes since the last review: merged origin/main, redacted KEY|TOKEN|SECRET env values from the persisted kilo stderr logs (addresses the lib.mjs thread), added the exit-code-contract changeset.

@kilo-code-bot

kilo-code-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Incremental review from 3b32193fd1b to 424fdec052e (2 files, 23 insertions/1 deletion): .github/docs-sync/selftest.mjs, .github/workflows/docs-sync.yml. This commit guards the Fix verify failures step's kilo run | redact-stream | tee pipeline with || echo "::warning::..." so set -o pipefail + bash -e cannot abort the block (and skip writing verify2.log) once a headless kilo run starts exiting nonzero on an auto-rejected permission or mid-stream session error, per the CLI-side commit in this same PR. Re-verify status already keys off steps.fix.outcome/steps.verify.outcome and the rebuild's own exit code, so the guard correctly defers the pass/fail decision to the rebuild rather than the agent's exit code. selftest.mjs case 2b was extended to assert the pipeline is guarded (\|\|\s*(echo|true)) and that verify2.log is still written, scoped to the region between the tee and the next bun run call so a stray comment elsewhere can't satisfy it. No logic defects found in the changed lines.

The two previously open findings (.github/docs-sync/lib.mjs:256, .github/docs-sync/triage.mjs:80) are in files untouched by this commit and are not re-raised: the lib.mjs finding was fixed and author-confirmed in 090dfd0411; the triage.mjs --auto unattended-execution finding was explicitly accepted by the human reviewer, who decided to merge with --auto as-is.

Files Reviewed (incremental: 2 files across 1 new commit)
  • .github/docs-sync/selftest.mjs - new pipefail-guard assertion in case 2b, 0 issues
  • .github/workflows/docs-sync.yml - || echo "::warning::..." guard on the fix-pass pipeline, 0 issues
Previous Review Summaries (4 snapshots, latest commit 3b32193)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 3b32193)

Status: No Issues Found | Recommendation: Merge

Incremental review from 88b2442fde5 to 3b32193fd1b (1 new commit, 3 files: .github/docs-sync/lib.mjs, .github/docs-sync/selftest.mjs, .github/workflows/docs-sync.yml). This commit raises the docs-sync workflow's throughput budgets (TRIAGE_BUDGET_MINUTES 35→90, EDIT_BUDGET_MINUTES 50→120, job timeout-minutes 120→240 — 228 min of budgeted work with a 12-minute reserve, matching the PR description's arithmetic) and strips ANSI CSI escape sequences in tailText so pending-cause diagnostics surfacing in the rolling PR are human-readable instead of raw terminal escapes. Two new selftest cases (2h, 2i) exercise both changes against the real scripts/workflow file rather than duplicating the logic in the test, and are stated to fail on the unmodified code first. No logic defects found in the changed lines.

The two previously open findings are unaffected by this commit and were already addressed on their own threads: the lib.mjs full-stderr-capture WARNING was fixed by the redaction work in 090dfd0411 (author-confirmed), and the triage.mjs --auto unattended-execution WARNING was explicitly acknowledged and deliberately deferred by the author, with the residual risk and the deferred permission.bash/KILO_CONFIG_CONTENT hardening plan now documented directly in code comments. Neither is re-raised here.

Files Reviewed (incremental: 3 files across 1 new commit)
  • .github/docs-sync/lib.mjs - ANSI CSI stripping in tailText, 0 issues
  • .github/docs-sync/selftest.mjs - new cases 2h/2i plus formatting-only reflow, 0 issues
  • .github/workflows/docs-sync.yml - budget/timeout env changes, 0 issues

Previous review (commit 88b2442)

Status: No Issues Found | Recommendation: Merge

Incremental review from 988a1d8b07 to 88b2442fde5 (1 new commit, 2 files, comment-only changes). This commit expands the existing rationale comment in triage.mjs and edit.mjs to explicitly document the --auto security trade-off (unattended bash execution while fed untrusted PR content) and the deferred hardening plan (a scoped permission.bash map via KILO_CONFIG_CONTENT). No code paths, arguments, or logic changed — only comment text.

This directly addresses the previously open WARNING on triage.mjs:80 by making the accepted residual risk and its rationale explicit in the code, matching the acknowledgment already given on that review thread. The other previously open WARNING (lib.mjs:246, unconditional stderr capture) was already fixed in an earlier commit and is unaffected by this diff. No new issues found in the incremental diff.

Files Reviewed (incremental: 2 files across 1 new commit)
  • .github/docs-sync/edit.mjs - comment-only change, 0 issues
  • .github/docs-sync/triage.mjs - comment-only change, 0 issues

Previous review (commit 988a1d8)

Status: No Issues Found | Recommendation: Merge

Incremental review from 090dfd0411 to 988a1d8b07 (2 new commits). Both new commits are a direct, verified fix for every finding from the previous review round:

  • WARNING (stdout not redacted, lib.mjs): fixed — runKilo now redacts captured stdout with redactEnvSecrets, and a new redact-stream.mjs line-wise filter is piped into the Fix verify failures workflow step before tee -a docs-sync-out/edit-log.txt. Verified real (non-mocked) execution in selftest cases 2f/2g: 2f spawns triage.mjs for real and asserts the secret is absent from triage-raw-*.txt; 2g spawns redact-stream.mjs as a real child process via spawnSync and asserts exact redacted output, including the no-trailing-newline partial-line case.
  • SUGGESTION (prefix-secret leak): fixed — candidates are now collected and sorted by value.length descending before substitution, so a shorter secret that prefixes a longer one can no longer leave a remainder. Selftest case 2e reproduces the exact A_KEY/B_TOKEN prefix scenario from the prior review and asserts no leaked remainder.
  • SUGGESTION (narrow name allowlist): fixed — pattern widened to KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|ORG_ID|_PAT$ (covers KILO_ORG_ID, *_PAT, etc.); the $ is correctly scoped to the _PAT branch only, avoiding an over-broad match on names like *_PATH.
  • SUGGESTION (exact-substring limitation undocumented): fixed — the function's doc comment now states the exact-substring/case-sensitive limitation and that this is defense-in-depth, matching GitHub's own log-masking caveat.
  • SUGGESTION (changeset stdout-shape claim): fixed — the changeset now correctly calls out the new error event added to the --format json stream on the auto-reject path, and clarifies the exit-1 rule also applies to a plain --attach run.

No new issues found in the incremental diff. The two previously-open WARNING threads (unconditional stderr capture blast radius; --auto unattended execution risk) are addressed/acknowledged in-place on their existing threads and not re-raised.

Files Reviewed (incremental: 4 files across 2 new commits)
  • .github/docs-sync/lib.mjs - fixes verified, 0 new issues
  • .github/docs-sync/redact-stream.mjs - new file, 0 issues
  • .github/docs-sync/selftest.mjs - new non-vacuous cases 2e/2f/2g verified genuine (real spawned child processes, no mocking)
  • .github/workflows/docs-sync.yml - new pipe stage, 0 issues
  • .changeset/headless-run-honest-exit.md - text corrected, 0 issues

Previous review (commit 090dfd0)

Status: 1 Issue Found (incremental review of 090dfd0411) | Recommendation: Non-blocking; address the artifact-coverage gap if convenient

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 4
Issue Details (click to expand)

WARNING

File Line Issue
.github/docs-sync/triage.mjs 85 Redaction is stderr-only, but raw kilo stdout is persisted to the same uploaded artifact: triage-raw-<i>.txt here, and docs-sync-out/edit-log.txt via tee -a in the workflow Fix verify failures step. Both bypass redactEnvSecrets(), so the threat model the new function states ("artifact files are raw; GitHub masks log streams only") is only half covered. Cheap fix: run stdout through the same helper at the persist sites. Not a proven leak — model stdout is unlikely to echo KILO_API_KEY — but it is the same channel.

SUGGESTION

File Line Issue
.github/docs-sync/lib.mjs 182 Iteration order = process.env insertion order, so a shorter secret that is a prefix of a longer one leaks the remainder. Verified locally: with A_KEY=abcdefgh before B_TOKEN=abcdefghIJKL-tail, redactEnvSecrets("leak: abcdefghIJKL-tail end")"leak: ***IJKL-tail end". Collect candidates first and sort by value.length descending.
.github/docs-sync/lib.mjs 183 Name allowlist `KEY
.github/docs-sync/lib.mjs 182 Exact-substring matching only: a value that is JSON-escaped, base64-encoded, ANSI/line-wrapped in the child's stderr render, or shorter than 8 chars survives. Same limitation as GitHub's own masking, so fine as defense-in-depth — worth one comment line saying so, so a future reader does not treat the log as guaranteed-clean.
.changeset/headless-run-honest-exit.md 9 "the --format json stdout shape is unchanged" is not quite right: the auto-reject path emits a new {"type":"error", ...} event, and its error is a plain string, whereas the existing session.error emit passes the structured props.error object (run.ts 819 vs 927). Either mention the new event or align the payload shape. Also, the PR body says --attach is untouched, but a plain --attach run that auto-rejects an ask now exits 1 too.

Verification of the three new changes:

  • redactEnvSecrets() correctness: uses split(value).join("***"), so no regex-escaping hazard for values containing ./+/$ — the right choice. Applied to stderrSafe before both tailText() and the 8 MB slice, so truncation cannot reintroduce a value, and the redacted tail is what flows into formatCause() / pending rows / console output. Case-sensitive exact matching on values (name match is case-insensitive) is correct for API keys. Over-redaction of non-secret *KEY* vars (e.g. NEXT_PUBLIC_POSTHOG_KEY) is cosmetic only. No log-injection concern: the replacement is a fixed literal.
  • Selftest case 2d is real and non-vacuous: runNodeScript spawns the actual edit.mjs with a stub kilo on PATH (mode: "stderr-exit0") writing the secret to real stderr, with KILO_API_KEY=<secret> in the child env — no mocking of runKilo. It asserts the secret is absent from the on-disk kilo-stderr-edit-batch-0-attempt-1.log, that the redaction marker is present (so an empty log cannot pass), and that the stderr tail: console region is clean. Removing the redactEnvSecrets call makes it fail. Minor nit: the includes("***") guard could be satisfied by an unrelated env var in the runner's environment; asserting the exact leak before *** after line would pin it.
  • Changeset: package/bump format matches the repo convention and the described contract (exit 1 on any auto-rejected ask in a plain headless run; stderr diagnostic before the JSON emit; exit 0 unchanged on clean runs) matches run.ts as changed. patch is defensible; minor would signal a deliberate exit-code contract change more loudly.

Still open (informational, unchanged):

  • .github/docs-sync/triage.mjs:78 — the previously flagged --auto WARNING (unattended command execution while processing untrusted PR content on a secret-holding runner) is unchanged; author acknowledged and declined, rationale on the thread. Not re-counted above as a new issue.

CI note: the only red job is unit (windows, 4/4), failing on an unrelated flaky timeout (session HttpApi > returns false when an active prompt wins the deletion race, 1 flaky / 154 passed); selftest, typecheck, and the annotation/workflow guards pass.

Files Reviewed (3 files + ancestry)
  • .changeset/headless-run-honest-exit.md - 1 suggestion
  • .github/docs-sync/lib.mjs - 3 suggestions
  • .github/docs-sync/selftest.mjs - 0 issues (case 2d verified genuine)
  • Merge commit b34e6ae1af from main brings unrelated work (i18n, Agent Manager terminal, nix) — out of scope.

Reviewed by claude-sonnet-5 · Input: 36 · Output: 7.7K · Cached: 855.2K

Review guidance: REVIEW.md from base branch main

@kilo-code-bot

kilo-code-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

The Cloud Agent already posted the review to the PR: #12605 (comment)

Summary: 0 CRITICAL, 1 WARNING (stdout redaction gap in triage.mjs and the edit-log.txt tee not covered by redactEnvSecrets()), 4 non-blocking suggestions (secret-ordering substring leak, allowlist gaps, exact-substring-only limits, one selftest nit), plus two changeset accuracy nits. CI is green aside from an unrelated flaky Windows unit test.

No further action needed unless you want me to spawn a follow-up agent to fix the stdout redaction gap.

iscekic added 2 commits July 28, 2026 18:28
The docs-sync workflow uploads docs-sync-out/ as a public 14-day artifact,
and kilo stdout was persisted raw there in two more places: triage-raw-*.txt
and the edit-log.txt tee. Redact at capture in runKilo for stdout as already
done for stderr, and pipe the verify-fix step's kilo stdout through a new
line-wise redact-stream.mjs filter before tee.

Also harden redactEnvSecrets: widen the name pattern to
KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|ORG_ID|_PAT (KILO_ORG_ID is a repo
secret), replace longer values first so a short secret that prefixes a
longer one cannot leak the remainder, and document the exact-substring
limitation. Selftest gains cases 2e (prefix ordering), 2f (stdout capture),
2g (stream filter) and a non-vacuous exact-line assertion in 2d.
The auto-reject path adds a new error event to the --format json stream;
only the existing event shapes are unchanged. Also state that the exit-1
rule covers a plain non-interactive --attach run that auto-rejects an ask.
Update comments in triage.mjs and edit.mjs to accurately describe the
security implications of --auto (unrestricted bash for an agent steered
by external PR content) and note that a scoped permission.bash map via
KILO_CONFIG_CONTENT is the intended hardening, deferred until required
shell patterns are stable.
@iscekic

iscekic commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

(bot) CI triage on the latest head, for reviewers:

Green: every check except the two below — selftest, typecheck, Check kilocode_change annotations, docs-sync, the full unit matrix, and Kilo Code Review (summary: "No Issues Found | Recommendation: Merge").

unit (windows, 4/4) — flake, resolved by rerun. It failed once on session HttpApi > returns false when an active prompt wins the deletion race, which hit its explicit 10 s it.live timeout (10 130 ms) on a runner that had just needed a bun-install retry; the rerun passed. The test file imports nothing this PR touches.

HttpApi exerciser — red, but provably not from this PR. It has hit the job's 15-minute timeout-minutes on all four executions across the last three heads (exactly 15m16s every time), while on main the same job passes in 6m23s. Reproduced locally on pure main (3075d35f13, and even on 77812dbd1a — the last sha whose CI passed it): bun run script/httpapi-exercise.ts --mode auth prints summary pass=297 fail=0 and then the process never exits; --mode coverage (which mocks the TestLLMServer layer) exits cleanly. The hang is therefore in the Effect-scoped teardown/finalizer path of test/server/httpapi-exercise/index.ts with a live TestLLMServer, not in any scenario — a latent main-side race that started firing this afternoon. This PR's only runtime delta (src/cli/cmd/run.ts) is not imported by the exerciser harness, CI installs with --frozen-lockfile, and the local bun.lock drift is cosmetic (version marker + ordering). It cannot be fixed from this PR; once the harness teardown is fixed on main and merged in here, this check should go green.

@iscekic iscekic added the human-ready PR is ready for a human review. label Jul 28, 2026
@iscekic
iscekic requested a review from pandemicsyn July 28, 2026 18:28
--auto fixes the batches the bot attempted; it does not fix the ones it
never started. In run 30306629290 (254 PRs collected, 51 docs-worthy),
the wall-clock budgets deferred 54 PRs untriaged and 31 unedited without
an attempt — 45 of the 60 pending rows on the rolling PR. Triage got 8 of
11 chunks in 35 min; edit got 4 of 11 batches in 50 min.

Both are ceilings, not costs. A caught-up run needs ~2 chunks and ~1
batch and finishes in ~25 min, so raising them spends nothing on a normal
day and drains the backlog on a bad one. 90/120 covers 20 chunks and 14
batches — 500 triaged and 70 edited PRs against a ~5 docs-worthy/day
inflow — inside a 240-minute job timeout.

Also strip ANSI CSI sequences in tailText, the shared path both triage
and edit route their pending causes through. kilo renders its TUI to
stderr, so every "Why" cell on the rolling PR currently reads
"^[[0m→ ^[[0mRead packages/..." instead of the diagnostic. The persisted
docs-sync-out/kilo-stderr-*.log stays raw as the debugging record.

selftest case 2h asserts the pending reason is escape-free and still
carries the diagnostic text; case 2i asserts each budget fits at least
two units and the job timeout outlasts both, so a future edit cannot
silently restore a budget too small to run anything. Both shown failing
on the unmodified code first.
@iscekic iscekic changed the title fix(ci): docs-sync bot passes --auto to kilo runs; fix(cli): honest exit codes for headless runs fix(ci): docs-sync bot passes --auto, drains its backlog, and reports readable causes; fix(cli): honest exit codes for headless runs Jul 28, 2026
The "Fix verify failures" step runs under `set -o pipefail` and the
default `bash -e`. Once the CLI half of this PR ships, `kilo run` exits 1
on a mid-stream session error, which aborts the block before the rebuild
runs: verify2.log is never written and `Re-verify status` reports
VERIFIED=false even when the docs build fine. A transient provider error
would flip every rolling docs PR to "Verification: failing".

The agent's exit code was never the signal for this step — the rebuild
is. Guard the pipeline with `|| echo ::warning::` so a nonzero kilo run
is surfaced but the rebuild still decides the outcome.

Extends selftest case 2b (which already parses this step) rather than
adding a case: the guard must appear between the pipeline's tee and the
rebuild, so a comment elsewhere in the block cannot satisfy it. Shown
failing with the guard removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-ready PR is ready for a human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants