* feat(opencode): add PR link detect and parse helpers
detectPrLink runs gh pr view, parsePrUrl accepts GitHub and GitLab PR
URLs, and a Storage helper holds the manual override.
* feat(opencode): add kilo pr link, unlink, and status commands
kilo pr becomes a parent command with checkout preserved. link and
unlink write the Storage override; the next heartbeat persists it.
* feat(opencode): advertise prLink on the heartbeat and ingest it
getSessions resolves the Storage override, cleared, or detected link,
puts prLink on the heartbeat, and syncs the session_pr_link item.
* fix(opencode): encode worktree in pr link override storage key
The manual override key used the raw absolute worktree path. Storage
builds the file with path.join, so a Windows drive colon made an invalid
filename and kilo pr link failed. Encode the worktree so the key is one
valid path segment on both platforms.
* test(opencode): cover kilo pr status outputs
Extract the status handler body so it is testable, then assert the four
outputs: stored link, cleared, detected, and no link. Split captured
output on os.EOL so the test passes on Windows.
* refactor(opencode): remove dead pr-link code found in simplify pass
detectPrLink now reuses parsePrUrl instead of hand-parsing the URL and
number, drop the dead github.com special case, and inline the
prLinkTripleKey helper.
* fix(opencode): clear pr-link dedupe map on session delete
* docs(kilo-docs): regenerate CLI reference for kilo pr subcommands
* chore: add session-pr-link changeset and bump facade allowlist
Main landed the same httpapi design (git-opt-in fixtures, --shard flag)
and the same permission.create readiness wait. Resolution adopts main's
implementations and keeps this branch's unique work on top: the
--shards child-process orchestrator (adapted to main's zero-based
shard indexing), scenario retry with backoff gated on state-reset,
project.update isolation, and exerciser env stripping. Removed the
superseded agentsReady/probe machinery and this branch's duplicate
round-robin filter (double-sharding silently dropped scenarios).
Validated: coverage, auth, and 4-shard effect passes all green
(315 checks, 0 failures).
Closes#12996. Part of #12986.
- websearch resolves KILO_WEBSEARCH_PROVIDER, EXA_API_KEY and
PARALLEL_API_KEY through Env.Service in the tool body and threads
them into the pure helpers; selectWebSearchProvider takes the
override as a parameter.
- mcp-websearch no longer snapshots EXA_API_KEY into a module-level
URL at import time; exaUrl(key) derives it per call, so BYOK keys
set or cleared after startup are honored.
- Tool registry provides Env.node.
- kilo-tool-process-env allowlist is now empty; check:architecture
drops from 15 to 9 classified ratchet sites.
- Transform/model tests confirmed running in-process via plain
bun test (455 tests, <1s), the other half of #12996.
warpgrep.ts, the third file in the original ratchet, was already
removed on main.
- tool-input delta/end no longer recreate a settled call as a pending part
- outputTokenMax is back in overflow accounting, with RuntimeFlags rewired
- MCP server instructions reach the system prompt again (sys.mcp call site)
- session revert decodes stored messages through the Kilo normalize boundary
- shell tool titles runs with the model description and keeps it in metadata
- subdirectory snapshots drop newly ignored files: the ignore check and the
index removal ran from the instance dir, but candidates are worktree
relative
- customize-opencode builtin stays unregistered after the boot.ts rename
- --auto is interpreted in one place; the flag beside it now covers only
--yolo and --dangerously-skip-permissions
- --cloud-fork works again, validateSession had landed before the import
- TUI block tool skips the empty title row, opencode catalog gate reads
credentials per reload, TUI worker logs crashes, Snowflake OAuth uses the
Kilo page, annotations guard covers the shared packages
- tests: startRun spawned without the solid preload so every run under it
died on the JSX runtime; two upstream tests get a permission fixture since
headless auto-rejects bash; one expected a single error record where Kilo
emits two, as it did before this merge
- changeset for the range, help snapshot, llmgateway indentation
Generation was failing. Upstream added a guard in this range that rejects
duplicate session event variants, and Kilo's tool-content codec on the shared
event schema was tripping it, so openapi.json and the SDK were stale and a temp
packages/sdk/js/openapi.json got committed by accident.
Move the codec off the event schema into core/src/kilocode/event-storage.ts,
keyed by event type and applied only at the SQL boundary. Session events are on
the wire now (SessionEvent.Durable backs /api/session/{id}/history), so a
transform there forked the generated API. Shipped readers still parse old rows
and the wire contract goes back to upstream's. Drops the consumer-side
normalizers in the TUI and sync-v2 that existed only to undo the widening.
Also: restore the Kilo HttpApi title and a few branding strings, balance five
kilocode_change markers, drop a duplicate TUI palette entry, and teach
check-model-tool-network about the LayerNode wiring that replaced
Layer.provide(ToolNetwork.httpLayer).
* fix(cli): advertise the instance from enableRemote so /remote registers as a spawn target
Enabling the remote relay from the TUI `/remote` slash command connected the
socket and mirrored sessions, but never advertised the instance, so the CLI
never appeared as a spawn target in the mobile "Run on" picker. Only the
explicit `kilo remote` command called setInstanceAdvertisement.
The advertisement now runs on every successful enableRemote() entry, before the
already-connected and coalescing early returns. That ordering matters: bootstrap
auto-enable frequently connects first, so `/remote` usually hits
`if (remote) return` and an advertisement placed in the connection-setup body
would leave the defect unfixed in the common case. `ingestDisabled` returns
before the advertisement and stays unadvertised.
The ensure helper is a no-op when an advertisement is already set, so it fires no
extra heartbeat, while explicit setInstanceAdvertisement keeps its existing
replace semantics. buildInstanceAdvertisement moves to a shared module so the
command path and the enable path derive it identically.
* fix(cli): report pending question and permission on the session heartbeat
The heartbeat built each session's status from SessionStatus.Service, whose
union is idle/retry/busy/offline and which never consults Question.Service or
Permission.Service. deriveStatus() already did consult both, but only fed the
ingest session_status sync. So a session genuinely blocked on a question was
advertised as busy on the heartbeat, and the mobile app — which takes live row
status from the heartbeat — showed no needs-input badge.
Extract the precedence (permission, then question, then SessionStatus) into a
shared helper used by both deriveStatus and the heartbeat, so the two channels
cannot drift.
The heartbeat runs on a ~10s timer across every session, and deriveStatus makes
service calls per session, so the permission and question lists are fetched once
per tick and indexed by session id rather than queried per session. A test pins
the call count.
Behaviour note beyond the strict fix: sharing the derivation also means a
SessionStatus of offline now reports as retry on the wire, matching what
deriveStatus has always sent to ingest. Nothing consumes offline from the
heartbeat — the transport forwards only idle and busy, and the mobile row treats
both as non-attention — so the effect is that the two channels now agree. The
detach fence test is parameterised accordingly; its assertion that the status
clears on detach is unchanged.
* chore(cli): widen the promise-facade allowlist for the heartbeat attention tests
The DEF-3 heartbeat tests raise and reply to real Question and Permission
requests through the global AppRuntime, which took kilo-sessions.test.ts from 4
classified references to 29 and failed the allowlist check.
Bumping the count rather than restructuring the tests is deliberate: the
heartbeat resolves attention status from the global Question.Service and
Permission.Service, so asserting it requires driving those same services.
Scoped layers cannot express that — the global-runtime coupling is the thing
under test — and it is the same integration pattern this entry already
sanctioned for the detach fence. The reason string records that.
* 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.
* refactor(cli): run remote sessions in one process with safe per-session exit
Consolidate remote session handling into a single CLI process instead of
spawning one process per remote-created session (addresses the PR review):
- restore in-process create_session (accepts an absent sessionId and targets
the connection directory); remove the session spawner, the
KILO_REMOTE_ATTACH_SESSION attach-on-boot path, the child-advertisement gate,
and their tests
- retain instance advertisement and fire one immediate out-of-band heartbeat on
(re)connect when advertising, so a headless `kilo remote` host is discoverable
without delay
Make /exit (wire command exit_cli, unchanged for compatibility) detach only the
target session instead of terminating the CLI:
- AttachedState.detach with a presence-suppression tombstone; detach also clears
the target's SessionStatus so the negative-containment heartbeat fence resolves
deterministically for busy/retry/offline sessions
- exit_cli handler verifies ownership, cancels the active prompt, detaches and
awaits the detach heartbeat, then ACKs; the interactive RemoteExit callback is
invoked only after the ACK when the last owned session exits; a headless
`kilo remote` host stays alive and advertising at zero sessions
- add an optional canExitSession boolean to the list_commands v1 catalog
(always true, independent of exitAvailable) so clients can detect safe
session-exit semantics
History and stored sessions are preserved on exit.
* fix(cli): break module-load cycle in remote session prompt-cancel
The K1 in-process exit_cli seam added a static `import { SessionPrompt }`
to kilo-sessions.ts. @/session/prompt evaluates KiloSessionPrompt at module
load, so the new static edge raced that init and left the namespace in TDZ,
crashing unrelated test files with 'undefined is not an object (evaluating
KiloSessionPrompt.shouldAskPlanFollowup)'. Defer to a dynamic import at the
single call site, mirroring remote-command.ts.
* fix(cli): correct AttachedState announce/detach concurrency and rollback
Address review findings on the shared-process session lifecycle:
- announce/detach no longer join the OPPOSITE in-flight operation. Joining
detach's negative-containment fence made announce resolve success for a
detached id (and vice versa: detach joined announce and resolved success
while still attached, which exit_cli treats as license to ACK/close). Each
path now joins only a same-kind in-flight op and, when the opposite op is
in flight, awaits it to settle and then performs the real work.
- Failed-detach rollback now releases the suppression tombstone, so a
still-attached session is not dropped by the next setPresence (the tombstone
loop would otherwise remove the still-present id and never clear).
- Both catch/rollback branches now honor the lifecycle generation guard
(mirroring the success path); a stale in-flight op that rejects after
reset() no longer mutates the new lifecycle's presence/pending/suppressed
sets (reset clears the same Set instances).
Adds regression tests for each fix, plus AC6f covering the remote-ws
detachSessionId negative-containment waiter.