diff --git a/.changeset/opencode-v1-17-9-to-v1-17-13.md b/.changeset/opencode-v1-17-9-to-v1-17-13.md index b8a8945f18..df1deffa15 100644 --- a/.changeset/opencode-v1-17-9-to-v1-17-13.md +++ b/.changeset/opencode-v1-17-9-to-v1-17-13.md @@ -17,6 +17,7 @@ Changes from opencode v1.17.9 to v1.17.13 upstream: - Core Bugfixes: MCP tool results prefer content over structured output, and denied resource template tools stay hidden. - Core Bugfixes: Stale GitHub Copilot Responses item IDs are no longer replayed, and OpenAI reasoning variants are forced where required. - Core Bugfixes: Adaptive thinking is enabled for Claude Sonnet 5, and expired promos were removed from the zen catalog. +- Core Bugfixes: Preserve released prompt history during database replay and keep native event streams connected for all supported Kilo events. - Core Bugfixes: Remote skills refresh atomically with version pinning, and skill base directories are emitted as filesystem paths. - CLI Improvements: `kilo run --mini` provides a compact interactive mode, and ports increment from the default when busy. - CLI Improvements: Use `--auto` to start the TUI in a run-scoped auto-approve mode, and leave the mode mid-session from the command palette. diff --git a/packages/client/package.json b/packages/client/package.json index 8021d76959..26b8994d1b 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -12,6 +12,7 @@ "generate": "bun run script/build.ts", "check:generated": "bun run generate && git diff --exit-code -- src/generated src/generated-effect", "test": "bun test --timeout 5000", + "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 5000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", "typecheck": "tsgo --noEmit" }, "dependencies": { diff --git a/packages/client/src/generated/client.ts b/packages/client/src/generated/client.ts index 27ec3d81ba..5f6943c53e 100644 --- a/packages/client/src/generated/client.ts +++ b/packages/client/src/generated/client.ts @@ -250,7 +250,7 @@ export function make(options: ClientOptions) { health: { get: (requestOptions?: RequestOptions) => request( - { method: "GET", path: `/api/health`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, + { method: "GET", path: `/api/health`, successStatus: 200, declaredStatuses: [400, 401], empty: false }, requestOptions, ), }, @@ -262,7 +262,7 @@ export function make(options: ClientOptions) { path: `/api/location`, query: { location: input?.["location"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -276,7 +276,7 @@ export function make(options: ClientOptions) { path: `/api/agent`, query: { location: input?.["location"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -316,7 +316,7 @@ export function make(options: ClientOptions) { location: input?.["location"], }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -327,7 +327,7 @@ export function make(options: ClientOptions) { method: "GET", path: `/api/session/active`, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -338,7 +338,7 @@ export function make(options: ClientOptions) { method: "GET", path: `/api/session/${encodeURIComponent(input.sessionID)}`, successStatus: 200, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: false, }, requestOptions, @@ -350,7 +350,7 @@ export function make(options: ClientOptions) { path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`, body: { agent: input["agent"] }, successStatus: 204, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: true, }, requestOptions, @@ -362,7 +362,7 @@ export function make(options: ClientOptions) { path: `/api/session/${encodeURIComponent(input.sessionID)}/model`, body: { model: input["model"] }, successStatus: 204, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: true, }, requestOptions, @@ -374,7 +374,7 @@ export function make(options: ClientOptions) { path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`, body: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] }, successStatus: 200, - declaredStatuses: [409, 404, 400, 401], + declaredStatuses: [400, 401, 404, 409], empty: false, }, requestOptions, @@ -385,7 +385,7 @@ export function make(options: ClientOptions) { method: "POST", path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`, successStatus: 204, - declaredStatuses: [404, 503, 400, 401], + declaredStatuses: [400, 401, 404, 503], empty: true, }, requestOptions, @@ -396,7 +396,7 @@ export function make(options: ClientOptions) { method: "POST", path: `/api/session/${encodeURIComponent(input.sessionID)}/wait`, successStatus: 204, - declaredStatuses: [404, 503, 400, 401], + declaredStatuses: [400, 401, 404, 503], empty: true, }, requestOptions, @@ -408,7 +408,7 @@ export function make(options: ClientOptions) { path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`, body: { messageID: input["messageID"], files: input["files"] }, successStatus: 200, - declaredStatuses: [404, 500, 400, 401], + declaredStatuses: [400, 401, 404, 500], empty: false, }, requestOptions, @@ -419,7 +419,7 @@ export function make(options: ClientOptions) { method: "POST", path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`, successStatus: 204, - declaredStatuses: [404, 500, 400, 401], + declaredStatuses: [400, 401, 404, 500], empty: true, }, requestOptions, @@ -430,7 +430,7 @@ export function make(options: ClientOptions) { method: "POST", path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`, successStatus: 204, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: true, }, requestOptions, @@ -441,7 +441,7 @@ export function make(options: ClientOptions) { method: "GET", path: `/api/session/${encodeURIComponent(input.sessionID)}/context`, successStatus: 200, - declaredStatuses: [404, 500, 400, 401], + declaredStatuses: [400, 401, 404, 500], empty: false, }, requestOptions, @@ -453,7 +453,7 @@ export function make(options: ClientOptions) { path: `/api/session/${encodeURIComponent(input.sessionID)}/history`, query: { limit: input["limit"], after: input["after"] }, successStatus: 200, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: false, }, requestOptions, @@ -465,7 +465,7 @@ export function make(options: ClientOptions) { path: `/api/session/${encodeURIComponent(input.sessionID)}/event`, query: { after: input["after"] }, successStatus: 200, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: false, }, requestOptions, @@ -476,7 +476,7 @@ export function make(options: ClientOptions) { method: "POST", path: `/api/session/${encodeURIComponent(input.sessionID)}/interrupt`, successStatus: 204, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: true, }, requestOptions, @@ -487,7 +487,7 @@ export function make(options: ClientOptions) { method: "GET", path: `/api/session/${encodeURIComponent(input.sessionID)}/message/${encodeURIComponent(input.messageID)}`, successStatus: 200, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: false, }, requestOptions, @@ -501,7 +501,7 @@ export function make(options: ClientOptions) { path: `/api/session/${encodeURIComponent(input.sessionID)}/message`, query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, successStatus: 200, - declaredStatuses: [400, 404, 500, 401], + declaredStatuses: [400, 401, 404, 500], empty: false, }, requestOptions, @@ -515,7 +515,7 @@ export function make(options: ClientOptions) { path: `/api/model`, query: { location: input?.["location"] }, successStatus: 200, - declaredStatuses: [503, 401, 400], + declaredStatuses: [400, 401, 503], empty: false, }, requestOptions, @@ -529,7 +529,7 @@ export function make(options: ClientOptions) { path: `/api/provider`, query: { location: input?.["location"] }, successStatus: 200, - declaredStatuses: [503, 401, 400], + declaredStatuses: [400, 401, 503], empty: false, }, requestOptions, @@ -541,7 +541,7 @@ export function make(options: ClientOptions) { path: `/api/provider/${encodeURIComponent(input.providerID)}`, query: { location: input["location"] }, successStatus: 200, - declaredStatuses: [404, 503, 401, 400], + declaredStatuses: [400, 401, 404, 503], empty: false, }, requestOptions, @@ -555,7 +555,7 @@ export function make(options: ClientOptions) { path: `/api/integration`, query: { location: input?.["location"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -567,7 +567,7 @@ export function make(options: ClientOptions) { path: `/api/integration/${encodeURIComponent(input.integrationID)}`, query: { location: input["location"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -605,7 +605,7 @@ export function make(options: ClientOptions) { path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`, query: { location: input["location"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -630,7 +630,7 @@ export function make(options: ClientOptions) { path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`, query: { location: input["location"] }, successStatus: 204, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: true, }, requestOptions, @@ -645,7 +645,7 @@ export function make(options: ClientOptions) { query: { location: input["location"] }, body: { label: input["label"] }, successStatus: 204, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: true, }, requestOptions, @@ -657,7 +657,7 @@ export function make(options: ClientOptions) { path: `/api/credential/${encodeURIComponent(input.credentialID)}`, query: { location: input["location"] }, successStatus: 204, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: true, }, requestOptions, @@ -671,7 +671,7 @@ export function make(options: ClientOptions) { path: `/api/permission/request`, query: { location: input?.["location"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -683,7 +683,7 @@ export function make(options: ClientOptions) { path: `/api/permission/saved`, query: { projectID: input?.["projectID"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -694,7 +694,7 @@ export function make(options: ClientOptions) { method: "DELETE", path: `/api/permission/saved/${encodeURIComponent(input.id)}`, successStatus: 204, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: true, }, requestOptions, @@ -714,7 +714,7 @@ export function make(options: ClientOptions) { agent: input["agent"], }, successStatus: 200, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: false, }, requestOptions, @@ -725,7 +725,7 @@ export function make(options: ClientOptions) { method: "GET", path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`, successStatus: 200, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: false, }, requestOptions, @@ -736,7 +736,7 @@ export function make(options: ClientOptions) { method: "GET", path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}`, successStatus: 200, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: false, }, requestOptions, @@ -748,7 +748,7 @@ export function make(options: ClientOptions) { path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}/reply`, body: { reply: input["reply"], message: input["message"] }, successStatus: 204, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: true, }, requestOptions, @@ -762,7 +762,7 @@ export function make(options: ClientOptions) { path: `/api/fs/list`, query: { location: input?.["location"], path: input?.["path"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -774,7 +774,7 @@ export function make(options: ClientOptions) { path: `/api/fs/find`, query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -788,7 +788,7 @@ export function make(options: ClientOptions) { path: `/api/command`, query: { location: input?.["location"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -802,7 +802,7 @@ export function make(options: ClientOptions) { path: `/api/skill`, query: { location: input?.["location"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -811,7 +811,7 @@ export function make(options: ClientOptions) { events: { subscribe: (requestOptions?: RequestOptions): AsyncIterable => sse( - { method: "GET", path: `/api/event`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, + { method: "GET", path: `/api/event`, successStatus: 200, declaredStatuses: [400, 401], empty: false }, requestOptions, ), }, @@ -823,7 +823,7 @@ export function make(options: ClientOptions) { path: `/api/pty`, query: { location: input?.["location"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -842,7 +842,7 @@ export function make(options: ClientOptions) { env: input?.["env"], }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -854,7 +854,7 @@ export function make(options: ClientOptions) { path: `/api/pty/${encodeURIComponent(input.ptyID)}`, query: { location: input["location"] }, successStatus: 200, - declaredStatuses: [404, 401, 400], + declaredStatuses: [400, 401, 404], empty: false, }, requestOptions, @@ -867,7 +867,7 @@ export function make(options: ClientOptions) { query: { location: input["location"] }, body: { title: input["title"], size: input["size"] }, successStatus: 200, - declaredStatuses: [404, 401, 400], + declaredStatuses: [400, 401, 404], empty: false, }, requestOptions, @@ -879,7 +879,7 @@ export function make(options: ClientOptions) { path: `/api/pty/${encodeURIComponent(input.ptyID)}`, query: { location: input["location"] }, successStatus: 204, - declaredStatuses: [404, 401, 400], + declaredStatuses: [400, 401, 404], empty: true, }, requestOptions, @@ -893,7 +893,7 @@ export function make(options: ClientOptions) { path: `/api/question/request`, query: { location: input?.["location"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, @@ -904,7 +904,7 @@ export function make(options: ClientOptions) { method: "GET", path: `/api/session/${encodeURIComponent(input.sessionID)}/question`, successStatus: 200, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: false, }, requestOptions, @@ -916,7 +916,7 @@ export function make(options: ClientOptions) { path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reply`, body: { answers: input["answers"] }, successStatus: 204, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: true, }, requestOptions, @@ -927,7 +927,7 @@ export function make(options: ClientOptions) { method: "POST", path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reject`, successStatus: 204, - declaredStatuses: [404, 400, 401], + declaredStatuses: [400, 401, 404], empty: true, }, requestOptions, @@ -941,7 +941,7 @@ export function make(options: ClientOptions) { path: `/api/reference`, query: { location: input?.["location"] }, successStatus: 200, - declaredStatuses: [401, 400], + declaredStatuses: [400, 401], empty: false, }, requestOptions, diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index 8aa0751779..9d91878667 100644 --- a/packages/client/src/generated/types.ts +++ b/packages/client/src/generated/types.ts @@ -8,10 +8,6 @@ export type JsonValue = | ReadonlyArray | { readonly [key: string]: JsonValue } -export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly message: string } -export const isUnauthorizedError = (value: unknown): value is UnauthorizedError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError" - export type InvalidRequestError = { readonly _tag: "InvalidRequestError" readonly message: string @@ -21,6 +17,10 @@ export type InvalidRequestError = { export const isInvalidRequestError = (value: unknown): value is InvalidRequestError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidRequestError" +export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly message: string } +export const isUnauthorizedError = (value: unknown): value is UnauthorizedError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError" + export type InvalidCursorError = { readonly _tag: "InvalidCursorError"; readonly message: string } export const isInvalidCursorError = (value: unknown): value is InvalidCursorError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidCursorError" @@ -1133,6 +1133,33 @@ export type SessionsHistoryOutput = { readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly timestamp: number; readonly sessionID: string; readonly messageID: string } } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.prompt.promoted" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly prompt: { + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly mime: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + } + readonly timeCreated: number + } + } > readonly hasMore: boolean } @@ -1592,6 +1619,33 @@ export type SessionsEventsOutput = readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly timestamp: number; readonly sessionID: string; readonly messageID: string } } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.prompt.promoted" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly prompt: { + readonly text: string + readonly files?: ReadonlyArray<{ + readonly uri: string + readonly mime: string + readonly name?: string + readonly description?: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + readonly agents?: ReadonlyArray<{ + readonly name: string + readonly source?: { readonly start: number; readonly end: number; readonly text: string } + }> + } + readonly timeCreated: number + } + } export type SessionsInterruptInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } diff --git a/packages/core/src/kilocode/oauth/page.ts b/packages/core/src/kilocode/oauth/page.ts index a94b9b1d5f..c156691930 100644 --- a/packages/core/src/kilocode/oauth/page.ts +++ b/packages/core/src/kilocode/oauth/page.ts @@ -2,8 +2,12 @@ export * as KiloOauthCallbackPage from "./page" import { OauthCallbackPage, type CallbackPageOptions } from "../../oauth/page" +const KILO_MARK = ` + + ` + function brand(page: string) { - return page.replaceAll("OpenCode", "Kilo") + return page.replace(//, KILO_MARK).replaceAll("OpenCode", "Kilo") } export function success(options?: CallbackPageOptions) { diff --git a/packages/core/src/kilocode/session/prompt-promoted.ts b/packages/core/src/kilocode/session/prompt-promoted.ts new file mode 100644 index 0000000000..38a75b0069 --- /dev/null +++ b/packages/core/src/kilocode/session/prompt-promoted.ts @@ -0,0 +1,42 @@ +import { PromptPromoted } from "@opencode-ai/schema/kilocode/durable-event" +import { Effect } from "effect" +import { Database } from "../../database/database" +import { SessionEvent } from "../../session/event" +import { SessionInput } from "../../session/input" + +export const definition = PromptPromoted + +export function project( + db: Database.Interface["db"], + event: typeof PromptPromoted.Type, + commit: (event: typeof SessionEvent.Prompted.Type) => Effect.Effect, +) { + return Effect.gen(function* () { + if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence") + const admitted = yield* SessionInput.find(db, event.data.messageID) + if (!admitted) return yield* Effect.die(new SessionInput.LifecycleConflict({ id: event.data.messageID })) + const prompted = SessionEvent.Prompted.make({ + id: event.id, + type: SessionEvent.Prompted.type, + durable: event.durable, + location: event.location, + metadata: event.metadata, + data: { + sessionID: event.data.sessionID, + messageID: event.data.messageID, + timestamp: event.data.timeCreated, + prompt: event.data.prompt, + delivery: admitted.delivery, + }, + }) + yield* SessionInput.projectPrompted(db, { + id: prompted.data.messageID, + sessionID: prompted.data.sessionID, + prompt: prompted.data.prompt, + delivery: prompted.data.delivery, + timeCreated: prompted.data.timestamp, + promotedSeq: event.durable.seq, + }) + yield* commit(prompted) + }) +} diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 28a1269e17..cb8bd1b4ce 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -37,7 +37,7 @@ import { Snapshot } from "./snapshot" import { SessionRevert } from "./session/revert" import { Revert } from "@opencode-ai/schema/revert" import { FSUtil } from "./fs-util" -import { SessionDurable } from "@opencode-ai/schema/durable-event-manifest" +import { SessionDurable, type SessionDurableEvent } from "@opencode-ai/schema/durable-event-manifest" // kilocode_change export const RevertState = Revert.State export type RevertState = Revert.State @@ -134,12 +134,12 @@ export interface Interface { readonly events: (input: { sessionID: SessionSchema.ID after?: number - }) => Stream.Stream + }) => Stream.Stream // kilocode_change - released durable event compatibility readonly history: (input: { sessionID: SessionSchema.ID after?: number limit: number - }) => Effect.Effect<{ events: ReadonlyArray; hasMore: boolean }, NotFoundError> + }) => Effect.Effect<{ events: ReadonlyArray; hasMore: boolean }, NotFoundError> // kilocode_change readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: string }) => Effect.Effect readonly switchModel: (input: { sessionID: SessionSchema.ID @@ -193,7 +193,7 @@ const layer = Layer.effect( const store = yield* SessionStore.Service const locations = yield* LocationServiceMap.Service const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message) - const isDurableSessionEvent = Schema.is(SessionEvent.Durable) + const isDurableSessionEvent = Schema.is(SessionDurable.schema) // kilocode_change - include released storage keys const decode = (row: typeof SessionMessageTable.$inferSelect) => decodeMessage(normalize({ ...row.data, id: row.id, type: row.type })).pipe( // kilocode_change - normalize released tool content on paginated reads @@ -356,7 +356,7 @@ const layer = Layer.effect( result .get(input.sessionID) .pipe(Effect.as(events.durable({ aggregateID: input.sessionID, after: input.after }))), - ).pipe(Stream.filter((event): event is SessionEvent.DurableEvent => isDurableSessionEvent(event))), + ).pipe(Stream.filter((event): event is SessionDurableEvent => isDurableSessionEvent(event))), // kilocode_change history: Effect.fn("V2Session.history")(function* (input) { yield* result.get(input.sessionID) return yield* EventV2.readAggregate(db, { diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index 658a8489dd..1467adb5a7 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -16,6 +16,7 @@ import { WorkspaceV2 } from "../workspace" import { SessionContextEpoch } from "./context-epoch" import { MessageTable, PartTable, SessionInputTable, SessionMessageTable, SessionTable } from "./sql" import type { DeepMutable } from "../schema" +import * as PromptCompat from "../kilocode/session/prompt-promoted" // kilocode_change - released replay key type DatabaseService = Database.Interface["db"] @@ -393,6 +394,7 @@ const layer = Layer.effectDiscard( }) }), ) + yield* events.project(PromptCompat.definition, (event) => PromptCompat.project(db, event, (next) => run(db, next))) // kilocode_change - replay released two-step promotions yield* events.project(SessionEvent.ContextUpdated, (event) => run(db, event)) yield* events.project(SessionEvent.Synthetic, (event) => run(db, event)) yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event)) diff --git a/packages/core/test/kilocode/event-storage-compat.test.ts b/packages/core/test/kilocode/event-storage-compat.test.ts index 768f03543c..27746ec9e5 100644 --- a/packages/core/test/kilocode/event-storage-compat.test.ts +++ b/packages/core/test/kilocode/event-storage-compat.test.ts @@ -1,5 +1,6 @@ import { expect } from "bun:test" import { DateTime, Effect, Layer, Schema, Stream } from "effect" +import { eq } from "drizzle-orm" import { Database } from "@opencode-ai/core/database/database" import { EventV2 } from "@opencode-ai/core/event" import { SessionEvent } from "@opencode-ai/core/session/event" @@ -10,10 +11,20 @@ import { SessionMessage } from "@opencode-ai/core/session/message" import * as StoredMessage from "@opencode-ai/core/kilocode/session-message" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { SessionProjector } from "@opencode-ai/core/session/projector" +import { Project } from "@opencode-ai/core/project" +import { ProjectTable } from "@opencode-ai/core/project/sql" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { Prompt } from "@opencode-ai/core/session/prompt" +import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql" +import { SessionDurable } from "@opencode-ai/schema/durable-event-manifest" const database = Database.layerFromPath(":memory:") -const it = testEffect( - AppNodeBuilder.build(LayerNode.group([EventV2.node, Database.node]), [[Database.node, database]]), +const it = testEffect(AppNodeBuilder.build(LayerNode.group([EventV2.node, Database.node]), [[Database.node, database]])) +const replay = testEffect( + AppNodeBuilder.build(LayerNode.group([EventV2.node, Database.node, SessionProjector.node]), [ + [Database.node, database], + ]), ) it.effect("decodes legacy durable tool content without exposing it to consumers", () => @@ -98,6 +109,76 @@ it.effect("writes released durable tool and compaction shapes", () => }), ) +replay.effect("reads and replays released prompt promotion events", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const { db } = yield* Database.Service + const sessionID = SessionV2.ID.make("ses_prompt_promoted_compat") + const messageID = SessionMessage.ID.make("msg_prompt_promoted_compat") + const prompt = Prompt.make({ text: "Promoted from a released session" }) + + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "compat", + directory: "/project", + title: "compat", + version: "test", + }) + .run() + .pipe(Effect.orDie) + + yield* events.replayAll( + [ + { + id: EventV2.ID.make("evt_prompt_admitted_compat"), + type: "session.next.prompt.admitted.1", + aggregateID: sessionID, + seq: 0, + data: { timestamp: 1, sessionID, messageID, prompt, delivery: "queue" }, + }, + { + id: EventV2.ID.make("evt_prompt_promoted_compat"), + type: "session.next.prompt.promoted.1", + aggregateID: sessionID, + seq: 1, + data: { timestamp: 2, sessionID, messageID, prompt, timeCreated: 1 }, + }, + ], + { publish: true }, + ) + + const history = yield* EventV2.readAggregate(db, { + aggregateID: sessionID, + limit: 10, + manifest: SessionDurable, + }) + expect(history.events.map((event) => event.type)).toEqual([ + "session.next.prompt.admitted", + "session.next.prompt.promoted", + ]) + expect(yield* events.durable({ aggregateID: sessionID }).pipe(Stream.take(2), Stream.runCollect)).toHaveLength(2) + expect(yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, messageID)).get()).toMatchObject({ + delivery: "queue", + promoted_seq: 1, + }) + expect( + yield* db.select().from(SessionMessageTable).where(eq(SessionMessageTable.id, messageID)).get(), + ).toMatchObject({ + id: messageID, + type: "user", + seq: 1, + }) + }), +) + it.effect("stores self-contained compaction projections for released readers", () => Effect.sync(() => { const encoded = StoredMessage.encode({ diff --git a/packages/httpapi-codegen/package.json b/packages/httpapi-codegen/package.json index 1af10d6d94..aadefada25 100644 --- a/packages/httpapi-codegen/package.json +++ b/packages/httpapi-codegen/package.json @@ -8,6 +8,7 @@ }, "scripts": { "test": "bun test --timeout 5000 --only-failures", + "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 5000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", "typecheck": "tsgo --noEmit" }, "dependencies": { diff --git a/packages/httpapi-codegen/src/index.ts b/packages/httpapi-codegen/src/index.ts index 298a1211dd..34df2820ec 100644 --- a/packages/httpapi-codegen/src/index.ts +++ b/packages/httpapi-codegen/src/index.ts @@ -111,6 +111,11 @@ export function compile( const errorSchemas = Array.from(errors).flatMap(([status, schemas]) => schemas.map((schema) => ({ status, ...normalizeTransport(schema, "error", endpoint, name)! })), ) + errorSchemas.sort((a, b) => { + const left = SchemaAST.resolveIdentifier(a.schema.ast) ?? "" + const right = SchemaAST.resolveIdentifier(b.schema.ast) ?? "" + return a.status - b.status || (left < right ? -1 : left > right ? 1 : 0) + }) const inputs = [ ...inputFields(params?.schema, "params", name), ...inputFields(query?.schema, "query", name), diff --git a/packages/httpapi-codegen/test/generate.test.ts b/packages/httpapi-codegen/test/generate.test.ts index 6e076b7ef4..a5ad6f8b40 100644 --- a/packages/httpapi-codegen/test/generate.test.ts +++ b/packages/httpapi-codegen/test/generate.test.ts @@ -25,6 +25,21 @@ function compile(source: Htt } describe("HttpApiCodegen.generate", () => { + test("orders endpoint errors deterministically", () => { + class Alpha extends Schema.TaggedErrorClass()("Alpha", {}) {} + class Beta extends Schema.TaggedErrorClass()("Beta", {}) {} + const endpoint = (errors: readonly [typeof Alpha, typeof Beta] | readonly [typeof Beta, typeof Alpha]) => + HttpApiEndpoint.get("get", "/session", { + success: Schema.String, + error: errors.map((error) => error.pipe(HttpApiSchema.status(400))), + }) + + const alpha = compileContract(api(endpoint([Alpha, Beta]))) + const beta = compileContract(api(endpoint([Beta, Alpha]))) + + expect(emitPromise(alpha)).toEqual(emitPromise(beta)) + }) + test("compiles one contract for Promise and Effect emitters", () => { const contract = compileContract( api( diff --git a/packages/opencode/test/kilocode/oauth-branding.test.ts b/packages/opencode/test/kilocode/oauth-branding.test.ts index 88422afa0f..236a1bcfe2 100644 --- a/packages/opencode/test/kilocode/oauth-branding.test.ts +++ b/packages/opencode/test/kilocode/oauth-branding.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test" import path from "path" +import { KiloOauthCallbackPage } from "@opencode-ai/core/kilocode/oauth/page" const root = path.join(__dirname, "..", "..") @@ -16,13 +17,22 @@ describe("Kilo OAuth branding", () => { test("core OAuth browser flow uses Kilo branding", async () => { const src = await Bun.file(path.join(root, "..", "core", "src", "plugin", "provider", "openai.ts")).text() - const page = await Bun.file(path.join(root, "..", "core", "src", "kilocode", "oauth", "page.ts")).text() + const pages = [ + KiloOauthCallbackPage.success({ provider: "ChatGPT" }), + KiloOauthCallbackPage.error("Denied", { provider: "ChatGPT" }), + ] expect(src).toContain('originator: "kilo"') expect(src).toContain('"User-Agent": `kilo/${InstallationVersion}`') expect(src).toContain("KiloOauthCallbackPage") - expect(page).toContain('.replaceAll("OpenCode", "Kilo")') expect(src).not.toContain('originator: "opencode"') + for (const page of pages) { + expect(page).toContain("ยท Kilo") + expect(page).toContain('aria-label="Kilo Code"') + expect(page).toContain('viewBox="0 0 100 100"') + expect(page).not.toContain("OpenCode") + expect(page).not.toContain('viewBox="0 0 234 42"') + } }) test("MCP OAuth callback page uses Kilo branding", async () => { diff --git a/packages/opencode/test/kilocode/reference.test.ts b/packages/opencode/test/kilocode/reference.test.ts index 2f94a9d75b..1a14cafcce 100644 --- a/packages/opencode/test/kilocode/reference.test.ts +++ b/packages/opencode/test/kilocode/reference.test.ts @@ -105,6 +105,31 @@ describe("configured references", () => { expect(updates).toEqual(["reference.updated"]) }) + test("sync replaces stale effective references", async () => { + const cache = Layer.mock(RepositoryCache.Service, { + ensure: () => Effect.die("unexpected Git materialization"), + }) + const events = Layer.mock(EventV2.Service)({ + publish: (definition, data) => + Effect.succeed({ id: EventV2.ID.make("evt_reference_replace"), type: definition.type, data }), + }) + const layer = AppNodeBuilder.build(CoreReference.node, [ + [RepositoryCache.node, cache], + [EventV2.node, events], + ]) + + const result = await Effect.runPromise( + Effect.gen(function* () { + yield* Reference.sync({ references: { stale: "./stale" }, directory: "/workspace", worktree: "/workspace" }) + yield* Reference.sync({ references: { current: "./current" }, directory: "/workspace", worktree: "/workspace" }) + return yield* (yield* CoreReference.Service).list() + }).pipe(Effect.provide(layer), Effect.scoped), + ) + + expect(result.map((item) => item.name)).toEqual(["current"]) + expect(result[0]?.path).toBe(AbsolutePath.make(path.resolve("/workspace", "current"))) + }) + test("initializes effective references before exposing location services", async () => { await using tmp = await tmpdir({ config: { @@ -115,7 +140,10 @@ describe("configured references", () => { }, }, }) - const layer = locations.pipe(Layer.provide(AppNodeBuilder.build(Config.node)), Layer.provide(testInstanceStoreLayer)) + const layer = locations.pipe( + Layer.provide(AppNodeBuilder.build(Config.node)), + Layer.provide(testInstanceStoreLayer), + ) const result = await Effect.runPromise( Effect.gen(function* () { diff --git a/packages/opencode/test/kilocode/release-publish-scope.test.ts b/packages/opencode/test/kilocode/release-publish-scope.test.ts new file mode 100644 index 0000000000..054d8b222f --- /dev/null +++ b/packages/opencode/test/kilocode/release-publish-scope.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from "bun:test" +import path from "path" + +test("Kilo releases do not publish upstream-owned packages", async () => { + const root = path.join(import.meta.dir, "../../../..") + const src = await Bun.file(path.join(root, "script", "publish.ts")).text() + + expect(src).not.toContain("packages/ui/script/publish.ts") + expect(src).toContain("packages/opencode/script/publish.ts") + expect(src).toContain("packages/sdk/js/script/publish.ts") + expect(src).toContain("packages/plugin/script/publish.ts") +}) diff --git a/packages/opencode/test/server/httpapi-v2-location.test.ts b/packages/opencode/test/server/httpapi-v2-location.test.ts index a49b53a373..7fec737577 100644 --- a/packages/opencode/test/server/httpapi-v2-location.test.ts +++ b/packages/opencode/test/server/httpapi-v2-location.test.ts @@ -128,11 +128,23 @@ describe("v2 location HttpApi", () => { const created = await request("/session", publisher.path, { method: "POST" }) expect(created.status).toBe(200) + + // kilocode_change start - the native handler must encode Kilo events omitted from upstream's narrower manifest + const session = (await created.json()) as { id: string } expect(await readEventType(reader, "session.created")).toMatchObject({ type: "session.created", location: { directory: publisher.path }, - data: { sessionID: expect.any(String) }, + data: { sessionID: session.id }, }) + + const aborted = await request(`/session/${session.id}/abort`, publisher.path, { method: "POST" }) + expect(aborted.status).toBe(200) + expect(await readEventType(reader, "session.status")).toMatchObject({ + type: "session.status", + location: { directory: publisher.path }, + data: { sessionID: session.id, status: { type: "idle" } }, + }) + // kilocode_change end await reader.return(undefined) }) }) diff --git a/packages/protocol/src/groups/event.ts b/packages/protocol/src/groups/event.ts index a6fc5692ea..ab4ec63b9f 100644 --- a/packages/protocol/src/groups/event.ts +++ b/packages/protocol/src/groups/event.ts @@ -54,3 +54,5 @@ export const EventGroup = event.group export const OpenCodeEvent = event.schema export type OpenCodeEvent = typeof OpenCodeEvent.Type export type OpenCodeEventEncoded = typeof OpenCodeEvent.Encoded + +export const KiloEvent = schema(EventManifest.Definitions) // kilocode_change - encode the full Kilo event bus diff --git a/packages/protocol/src/groups/session.ts b/packages/protocol/src/groups/session.ts index 61e87e302a..08fd8f4628 100644 --- a/packages/protocol/src/groups/session.ts +++ b/packages/protocol/src/groups/session.ts @@ -21,6 +21,7 @@ import { Model } from "@opencode-ai/schema/model" import { Location } from "@opencode-ai/schema/location" import { Revert } from "@opencode-ai/schema/revert" import { SessionEvent } from "@opencode-ai/schema/session-event" +import { SessionDurable } from "@opencode-ai/schema/durable-event-manifest" // kilocode_change - released history keys const SessionsQueryFields = { workspace: Workspace.ID.pipe(Schema.optional), @@ -318,7 +319,7 @@ export const makeSessionGroup = < params: { sessionID: Session.ID }, query: SessionHistoryQuery, success: Schema.Struct({ - data: Schema.Array(SessionEvent.Durable), + data: Schema.Array(SessionDurable.schema), // kilocode_change hasMore: Schema.Boolean, }).annotate({ identifier: "SessionHistory" }), error: SessionNotFoundError, @@ -339,7 +340,7 @@ export const makeSessionGroup = < query: { after: Schema.NumberFromString.pipe(Schema.decodeTo(NonNegativeInt), Schema.optional), }, - success: HttpApiSchema.StreamSse({ data: SessionEvent.Durable }), + success: HttpApiSchema.StreamSse({ data: SessionDurable.schema }), // kilocode_change error: SessionNotFoundError, }) .middleware(sessionLocationMiddleware) diff --git a/packages/schema/src/durable-event-manifest.ts b/packages/schema/src/durable-event-manifest.ts index acdcb3e9d5..406ad976bf 100644 --- a/packages/schema/src/durable-event-manifest.ts +++ b/packages/schema/src/durable-event-manifest.ts @@ -1,15 +1,25 @@ export * as DurableEventManifest from "./durable-event-manifest" import { Event } from "./event" +import { Schema } from "effect" // kilocode_change import { SessionEvent } from "./session-event" import { SessionV1 } from "./session-v1" +import { PromptPromoted } from "./kilocode/durable-event" // kilocode_change - released storage key + +// kilocode_change start - retain the released prompt promotion event for history and replay +const definitions = Event.inventory(...SessionEvent.DurableDefinitions, PromptPromoted) +const schema = Schema.Union(definitions, { mode: "oneOf" }) + .pipe(Schema.toTaggedUnion("type")) + .annotate({ identifier: "SessionDurableEvent" }) +export type SessionDurableEvent = typeof schema.Type +// kilocode_change end export const SessionDurable = { - definitions: Event.durable(SessionEvent.DurableDefinitions), - schema: SessionEvent.Durable, + definitions: Event.durable(definitions), // kilocode_change + schema, // kilocode_change } as const export const Durable = Event.durable([ ...SessionV1.Event.Definitions.filter((definition) => definition.durable !== undefined), - ...SessionEvent.DurableDefinitions, + ...definitions, // kilocode_change ]) diff --git a/packages/schema/src/kilocode/durable-event.ts b/packages/schema/src/kilocode/durable-event.ts new file mode 100644 index 0000000000..a77525a787 --- /dev/null +++ b/packages/schema/src/kilocode/durable-event.ts @@ -0,0 +1,17 @@ +import { Event } from "../event" +import { Prompt } from "../prompt" +import { DateTimeUtcFromMillis } from "../schema" +import { SessionID } from "../session-id" +import { SessionMessage } from "../session-message" + +export const PromptPromoted = Event.define({ + type: "session.next.prompt.promoted", + durable: { aggregate: "sessionID", version: 1 }, + schema: { + timestamp: DateTimeUtcFromMillis, + sessionID: SessionID, + messageID: SessionMessage.ID, + prompt: Prompt, + timeCreated: DateTimeUtcFromMillis, + }, +}) diff --git a/packages/sdk-next/package.json b/packages/sdk-next/package.json index a0ca87bd36..fc40ad4448 100644 --- a/packages/sdk-next/package.json +++ b/packages/sdk-next/package.json @@ -9,6 +9,7 @@ }, "scripts": { "test": "bun test --timeout 5000", + "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 5000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", "typecheck": "tsgo --noEmit" }, "dependencies": { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 664ea83024..9e560d5001 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4531,6 +4531,7 @@ export type SessionDurableEvent = | SessionNextRevertStaged | SessionNextRevertCleared | SessionNextRevertCommitted + | SessionNextPromptPromoted export type SessionHistory = { data: Array @@ -7770,6 +7771,27 @@ export type SessionNextRevertCommitted = { } } +export type SessionNextPromptPromoted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.prompt.promoted" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + messageID: string + prompt: Prompt + timeCreated: number + } +} + export type ModelApi = | { id: string diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index fe634e4ddd..60691352a1 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -39495,6 +39495,9 @@ }, { "$ref": "#/components/schemas/SessionNextRevertCommitted" + }, + { + "$ref": "#/components/schemas/SessionNextPromptPromoted" } ] }, @@ -50022,6 +50025,67 @@ "required": ["id", "type", "data"], "additionalProperties": false }, + "SessionNextPromptPromoted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^evt_" + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": ["session.next.prompt.promoted"] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer" + }, + "version": { + "type": "integer" + } + }, + "required": ["aggregateID", "seq", "version"], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/LocationRef" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "messageID": { + "type": "string", + "pattern": "^msg_" + }, + "prompt": { + "$ref": "#/components/schemas/Prompt" + }, + "timeCreated": { + "type": "number" + } + }, + "required": ["timestamp", "sessionID", "messageID", "prompt", "timeCreated"], + "additionalProperties": false + } + }, + "required": ["id", "type", "data"], + "additionalProperties": false + }, "ModelApi": { "anyOf": [ { diff --git a/packages/server/src/handlers/event.ts b/packages/server/src/handlers/event.ts index 47e94731ee..8d89cbcd19 100644 --- a/packages/server/src/handlers/event.ts +++ b/packages/server/src/handlers/event.ts @@ -1,5 +1,5 @@ import { EventV2 } from "@opencode-ai/core/event" -import { OpenCodeEvent } from "@opencode-ai/protocol/groups/event" +import { KiloEvent } from "@opencode-ai/protocol/groups/event" // kilocode_change - encode the full Kilo event bus import { Effect, Schema, Stream } from "effect" import { HttpServerResponse } from "effect/unstable/http" import { HttpApiBuilder } from "effect/unstable/httpapi" @@ -13,7 +13,7 @@ function eventData(data: unknown): Sse.Event { _tag: "Event", event: "message", id: undefined, - data: JSON.stringify(Schema.encodeUnknownSync(OpenCodeEvent)(data)), + data: JSON.stringify(Schema.encodeUnknownSync(KiloEvent)(data)), // kilocode_change } } diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 258f7e186c..ab3353ae08 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -22,7 +22,8 @@ }, "scripts": { "typecheck": "tsgo --noEmit", - "test": "bun test src --only-failures" + "test": "bun test src --only-failures", + "test:ci": "mkdir -p .artifacts/unit && bun test src --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml" }, "devDependencies": { "@tsconfig/node22": "catalog:", diff --git a/script/publish.ts b/script/publish.ts index 90afd9c6fa..2c6604ae70 100755 --- a/script/publish.ts +++ b/script/publish.ts @@ -122,8 +122,7 @@ await import(`../packages/sdk/js/script/publish.ts`) console.log("\n=== plugin ===\n") await import(`../packages/plugin/script/publish.ts`) -console.log("\n=== ui ===\n") -await $`bun ./packages/ui/script/publish.ts` +// kilocode_change - Kilo does not publish the upstream-owned @opencode-ai/ui package // kilocode_change start console.log("\n=== vscode ===\n") diff --git a/specs/v2/config.md b/specs/v2/config.md index bd8649086d..91ddd02ea4 100644 --- a/specs/v2/config.md +++ b/specs/v2/config.md @@ -13,7 +13,7 @@ This document breaks the legacy configuration schema into small review groups. W Use one v2 config schema for now. Some fields, such as `autoupdate`, are intended for global/user configuration, but there is not yet enough benefit to enforce that with separate global and location schemas. Revisit this if more scope-sensitive fields survive the review. -V2 core discovers config documents named `opencode.json` or `opencode.jsonc` in the global config directory, ancestor project directories, and `.opencode` config directories. The legacy `config.json` filename is not supported in V2. +V2 core discovers config documents named `config.json`, `kilo.json`, `kilo.jsonc`, `opencode.json`, or `opencode.jsonc` in the global Kilo config directory, ancestor project directories, and `.kilo` or legacy `.kilocode` config directories. Kilo deliberately ignores `.opencode` directories. ## Group 1: File Metadata @@ -107,7 +107,7 @@ Plugin order remains part of the v2 configuration contract because hook registra } ``` -The configured `plugins` list represents package-loaded plugins only. Local plugin code remains discovered from plugin directories such as `.opencode/plugins/`; v2 does not port arbitrary configured local paths or file URLs into this field. +The configured `plugins` list represents package-loaded plugins only. Local plugin code remains discovered from plugin directories such as `.kilo/plugins/` and legacy `.kilocode/plugins/`; v2 does not port arbitrary configured local paths or file URLs into this field. ## Group 5: Filesystem And Tool Runtime @@ -201,7 +201,7 @@ Provider selection rules belong in `experimental.policies` rather than provider See [provider-policy.md](./provider-policy.md) for the provider policy semantics and precedence rules. -Policy evaluation will consume authored config documents in reverse order while preserving statement order inside each document. The precedence of `.opencode` policy sources remains open until `.opencode` configuration is reviewed. +Policy evaluation will consume authored config documents in reverse order while preserving statement order inside each document. The precedence of `.kilo` and legacy `.kilocode` policy sources remains open until Kilo configuration is reviewed. Provider configuration uses the plural `providers` key in v2. This intentionally differs from the legacy singular `provider` key; v2 does not add a compatibility alias while its configuration surface is still being defined.