Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: lint

on:
push:
branches: [dev]
pull_request:
workflow_dispatch:

jobs:
lint:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Setup Bun
uses: ./.github/actions/setup-bun

- name: Test lint rules
run: bun lint:test

- name: Run lint
run: bun lint --quiet
4 changes: 3 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
{
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json",
"jsPlugins": ["./script/lint/opencode.mjs"],
"options": {
"typeAware": true
},
"categories": {
"suspicious": "warn"
},
"rules": {
"opencode/tagged-error-message": "error",
"typescript/no-base-to-string": "warn",
// Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield
"require-yield": "off",
Expand Down Expand Up @@ -47,5 +49,5 @@
"options": {
"typeAware": true
},
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts", "**/sdk.gen.ts"]
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/.lint-tmp-*", "**/*.d.ts", "**/sdk.gen.ts"]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
"dev:storybook": "bun --cwd packages/storybook storybook",
"lint": "oxlint",
"lint:test": "bun script/lint/test.ts",
"typecheck": "bun turbo typecheck",
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
"postinstall": "bun run --cwd packages/core fix-node-pty",
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/aisdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ function prepareOptions(model: ModelV2.Info, pkg: string) {
export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", {
providerID: ProviderV2.ID,
cause: Schema.Defect(),
}) {}
}) {
override get message() {
return `Failed to initialize AI SDK provider: ${this.providerID}`
}
}

function initError(providerID: ProviderV2.ID) {
return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) })))
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/control-plane/move-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ export class DestinationProjectMismatchError extends Schema.TaggedErrorClass<Des
expected: ProjectV2.ID,
actual: ProjectV2.ID,
},
) {}
) {
override get message() {
return `Destination project ${this.actual} does not match session project ${this.expected}`
}
}

export class ApplyChangesError extends Schema.TaggedErrorClass<ApplyChangesError>()("MoveSession.ApplyChangesError", {
message: Schema.String,
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/file-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,19 @@ export interface RemoveInput {

export class StaleContentError extends Schema.TaggedErrorClass<StaleContentError>()("FileMutation.StaleContentError", {
path: Schema.String,
}) {}
}) {
override get message() {
return `File changed since it was read: ${this.path}`
}
}

export class TargetExistsError extends Schema.TaggedErrorClass<TargetExistsError>()("FileMutation.TargetExistsError", {
path: Schema.String,
}) {}
}) {
override get message() {
return `File already exists: ${this.path}`
}
}

export interface WriteResult {
readonly operation: "write"
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import { FileSystem } from "./filesystem"
export class ResizerUnavailableError extends Schema.TaggedErrorClass<ResizerUnavailableError>()(
"Image.ResizerUnavailableError",
{},
) {}
) {
override get message() {
return "Image resizer is unavailable"
}
}

export class DecodeError extends Schema.TaggedErrorClass<DecodeError>()("Image.DecodeError", {
resource: Schema.String,
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,19 @@ export type AttemptStatus = typeof AttemptStatus.Type

export class CodeRequiredError extends Schema.TaggedErrorClass<CodeRequiredError>()("Integration.CodeRequired", {
attemptID: AttemptID,
}) {}
}) {
override get message() {
return `Authorization code required for OAuth attempt ${this.attemptID}`
}
}

export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationError>()("Integration.Authorization", {
cause: Schema.Defect(),
}) {}
}) {
override get message() {
return "Integration authorization failed"
}
}

export type Error = CodeRequiredError | AuthorizationError

Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/location-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ export type ResolveInput = typeof ResolveInput.Type
export class PathError extends Schema.TaggedErrorClass<PathError>()("LocationMutation.PathError", {
path: Schema.String,
reason: Schema.Literals(["relative_escape", "location_escape", "non_directory_ancestor"]),
}) {}
}) {
override get message() {
if (this.reason === "relative_escape") return `Relative path escapes the location: ${this.path}`
if (this.reason === "location_escape") return `Path resolves outside the location: ${this.path}`
return `Path has a non-directory ancestor: ${this.path}`
}
}

export interface ExternalDirectoryAuthorization {
readonly action: "external_directory"
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedErr
add: Schema.Array(Schema.String).pipe(Schema.optional),
dir: Schema.String,
cause: Schema.optional(Schema.Defect()),
}) {}
}) {
override get message() {
return `Failed to install dependencies in ${this.dir}`
}
}

export interface EntryPoint {
readonly directory: string
Expand Down
25 changes: 21 additions & 4 deletions packages/core/src/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,36 @@ export const Event = {
}),
}

export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {}) {}
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {}) {
override get message() {
return "The user rejected this permission request"
}
}

export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("PermissionV2.CorrectedError", {
feedback: Schema.String,
}) {}
}) {
override get message() {
return `The user rejected this permission request with feedback: ${this.feedback}`
}
}

export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("PermissionV2.DeniedError", {
rules: PermissionSchema.Ruleset,
}) {}
}) {
override get message() {
if (this.rules.length === 0) return "Permission denied by configured rules"
return `Permission denied by configured rules: ${this.rules.map((rule) => `${rule.action} ${rule.resource}`).join(", ")}`
}
}

export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("PermissionV2.NotFoundError", {
requestID: ID,
}) {}
}) {
override get message() {
return `Permission request not found: ${this.requestID}`
}
}

export type Error = DeniedError | RejectedError | CorrectedError

Expand Down
36 changes: 30 additions & 6 deletions packages/core/src/project/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,32 +58,56 @@ export type ListEntry = typeof ListEntry.Type
export class SourceDirectoryNotFoundError extends Schema.TaggedErrorClass<SourceDirectoryNotFoundError>()(
"ProjectCopy.SourceDirectoryNotFoundError",
{ directory: AbsolutePath },
) {}
) {
override get message() {
return `Project copy source directory not found: ${this.directory}`
}
}

export class DestinationExistsError extends Schema.TaggedErrorClass<DestinationExistsError>()(
"ProjectCopy.DestinationExistsError",
{ directory: AbsolutePath },
) {}
) {
override get message() {
return `Project copy destination already exists: ${this.directory}`
}
}

export class DirectoryUnavailableError extends Schema.TaggedErrorClass<DirectoryUnavailableError>()(
"ProjectCopy.DirectoryUnavailableError",
{ directory: AbsolutePath },
) {}
) {
override get message() {
return `Project copy directory is unavailable: ${this.directory}`
}
}

export class InvalidDirectoryError extends Schema.TaggedErrorClass<InvalidDirectoryError>()(
"ProjectCopy.InvalidDirectoryError",
{ directory: AbsolutePath },
) {}
) {
override get message() {
return `Invalid project copy directory: ${this.directory}`
}
}

export class StrategyUnavailableError extends Schema.TaggedErrorClass<StrategyUnavailableError>()(
"ProjectCopy.StrategyUnavailableError",
{ strategy: StrategyID },
) {}
) {
override get message() {
return `Project copy strategy is unavailable: ${this.strategy}`
}
}

export class DuplicateStrategyError extends Schema.TaggedErrorClass<DuplicateStrategyError>()(
"ProjectCopy.DuplicateStrategyError",
{ strategy: StrategyID },
) {}
) {
override get message() {
return `Project copy strategy is already registered: ${this.strategy}`
}
}

export type Error =
| SourceDirectoryNotFoundError
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/pty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,19 @@ export type Attachment = {

export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Pty.NotFoundError", {
ptyID: PtyID,
}) {}
}) {
override get message() {
return `PTY session not found: ${this.ptyID}`
}
}

export class ExitedError extends Schema.TaggedErrorClass<ExitedError>()("Pty.ExitedError", {
ptyID: PtyID,
}) {}
}) {
override get message() {
return `PTY session has exited: ${this.ptyID}`
}
}

export const Event = {
Created: EventV2.define({ type: "pty.created", schema: { info: Info } }),
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("Que

export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("QuestionV2.NotFoundError", {
requestID: ID,
}) {}
}) {
override get message() {
return `Question request not found: ${this.requestID}`
}
}

export interface AskInput {
readonly sessionID: SessionSchema.ID
Expand Down
18 changes: 15 additions & 3 deletions packages/core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,33 @@ type CompactInput = {

export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Session.NotFoundError", {
sessionID: SessionSchema.ID,
}) {}
}) {
override get message() {
return `Session not found: ${this.sessionID}`
}
}

export class OperationUnavailableError extends Schema.TaggedErrorClass<OperationUnavailableError>()(
"Session.OperationUnavailableError",
{
operation: Schema.Literals(["move", "shell", "skill", "switchAgent", "compact", "wait"]),
},
) {}
) {
override get message() {
return `Session ${this.operation} is not available yet`
}
}

export { ContextSnapshotDecodeError, MessageDecodeError } from "./session/error"

export class PromptConflictError extends Schema.TaggedErrorClass<PromptConflictError>()("Session.PromptConflictError", {
sessionID: SessionSchema.ID,
messageID: SessionMessage.ID,
}) {}
}) {
override get message() {
return `Prompt message ${this.messageID} conflicts with an existing durable record in session ${this.sessionID}`
}
}

export type Error = NotFoundError | MessageDecodeError | OperationUnavailableError | PromptConflictError

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/session/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const find = Effect.fn("SessionInput.find")(function* (db: DatabaseServic
return row === undefined ? undefined : fromRow(row)
})

// oxlint-disable-next-line opencode/tagged-error-message -- internal defect sentinel for inconsistent projections
export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict>()("SessionInput.LifecycleConflict", {
id: SessionMessage.ID,
}) {}
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/util/effect-flock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@ export namespace EffectFlock {

export class LockTimeoutError extends Schema.TaggedErrorClass<LockTimeoutError>()("LockTimeoutError", {
key: Schema.String,
}) {}
}) {
override get message() {
return `Timed out acquiring lock: ${this.key}`
}
}

export class LockCompromisedError extends Schema.TaggedErrorClass<LockCompromisedError>()("LockCompromisedError", {
detail: Schema.String,
}) {}
}) {
override get message() {
return `Lock was compromised: ${this.detail}`
}
}

class ReleaseError extends Schema.TaggedErrorClass<ReleaseError>()("ReleaseError", {
detail: Schema.String,
Expand All @@ -32,6 +40,7 @@ export namespace EffectFlock {
}

/** Internal: signals "lock is held, retry later". Never leaks to callers. */
// oxlint-disable-next-line opencode/tagged-error-message -- internal retry sentinel for lock contention
class NotAcquired extends Schema.TaggedErrorClass<NotAcquired>()("NotAcquired", {}) {}

export type LockError = LockTimeoutError | LockCompromisedError
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/v1/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("Permiss

export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Permission.NotFoundError", {
requestID: ID,
}) {}
}) {
override get message() {
return `Permission request not found: ${this.requestID}`
}
}

export type Error = DeniedError | RejectedError | CorrectedError
Loading
Loading