authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-05-08 11:52:19-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-08 11:52:19-07:00
log825263b6f979e24bc7cd48a8ea0bdf18c27b58fa
tree8124baa8def24f108cb59c53003169b226b434e9
parent25b02f4ba28d9c6a1adff3475625e5b3fdacce4b
signature Signed by PGP key B5690EEEBB952194

Scaffold oxlint plugin with initial rule (#2603)

Co-authored-by: codex <codex@users.noreply.github.com>

86 files changed, 1137 insertions(+), 455 deletions(-)

.oxlintrc.json+4-1
...@@ -9,6 +9,7 @@...@@ -9,6 +9,7 @@
9 "**/routeTree.gen.ts"9 "**/routeTree.gen.ts"
10 ],10 ],
11 "plugins": ["eslint", "oxc", "react", "unicorn", "typescript"],11 "plugins": ["eslint", "oxc", "react", "unicorn", "typescript"],
12 "jsPlugins": ["./oxlint-plugin-t3code/index.ts"],
12 "categories": {13 "categories": {
13 "correctness": "warn",14 "correctness": "warn",
14 "suspicious": "warn",15 "suspicious": "warn",
...@@ -17,6 +18,8 @@...@@ -17,6 +18,8 @@
17 "rules": {18 "rules": {
18 "react-in-jsx-scope": "off",19 "react-in-jsx-scope": "off",
19 "eslint/no-shadow": "off",20 "eslint/no-shadow": "off",
20 "eslint/no-await-in-loop": "off"21 "eslint/no-await-in-loop": "off",
22 "eslint/no-underscore-dangle": "off",
23 "t3code/no-inline-schema-compile": "warn"
21 }24 }
22}25}
apps/desktop/src/app/DesktopAppIdentity.ts+2-1
...@@ -16,6 +16,7 @@ const COMMIT_HASH_DISPLAY_LENGTH = 12;...@@ -16,6 +16,7 @@ const COMMIT_HASH_DISPLAY_LENGTH = 12;
16const AppPackageMetadata = Schema.Struct({16const AppPackageMetadata = Schema.Struct({
17 t3codeCommitHash: Schema.optional(Schema.String),17 t3codeCommitHash: Schema.optional(Schema.String),
18});18});
19const decodeAppPackageMetadata = Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata));
1920
20export interface DesktopAppIdentityShape {21export interface DesktopAppIdentityShape {
21 readonly resolveUserDataPath: Effect.Effect<string>;22 readonly resolveUserDataPath: Effect.Effect<string>;
...@@ -47,7 +48,7 @@ const make = Effect.gen(function* () {...@@ -47,7 +48,7 @@ const make = Effect.gen(function* () {
47 return yield* Option.match(raw, {48 return yield* Option.match(raw, {
48 onNone: () => Effect.succeed(Option.none<string>()),49 onNone: () => Effect.succeed(Option.none<string>()),
49 onSome: (value) =>50 onSome: (value) =>
50 Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata))(value).pipe(51 decodeAppPackageMetadata(value).pipe(
51 Effect.map((parsed) =>52 Effect.map((parsed) =>
52 Option.fromNullishOr(parsed.t3codeCommitHash).pipe(Option.flatMap(normalizeCommitHash)),53 Option.fromNullishOr(parsed.t3codeCommitHash).pipe(Option.flatMap(normalizeCommitHash)),
53 ),54 ),
apps/desktop/src/backend/DesktopBackendManager.test.ts+5-1
...@@ -25,6 +25,10 @@ import * as DesktopObservability from "../app/DesktopObservability.ts";...@@ -25,6 +25,10 @@ import * as DesktopObservability from "../app/DesktopObservability.ts";
25import * as DesktopState from "../app/DesktopState.ts";25import * as DesktopState from "../app/DesktopState.ts";
26import * as DesktopWindow from "../window/DesktopWindow.ts";26import * as DesktopWindow from "../window/DesktopWindow.ts";
2727
28const decodeDesktopBackendBootstrap = Schema.decodeEffect(
29 Schema.fromJsonString(DesktopBackendBootstrap),
30);
31
28const baseConfig: DesktopBackendManager.DesktopBackendStartConfig = {32const baseConfig: DesktopBackendManager.DesktopBackendStartConfig = {
29 executablePath: "/electron",33 executablePath: "/electron",
30 entryPath: "/server/bin.mjs",34 entryPath: "/server/bin.mjs",
...@@ -94,7 +98,7 @@ const healthyHttpClientLayer = httpClientLayer((request) =>...@@ -94,7 +98,7 @@ const healthyHttpClientLayer = httpClientLayer((request) =>
94);98);
9599
96function decodeBootstrap(raw: string) {100function decodeBootstrap(raw: string) {
97 return Schema.decodeEffect(Schema.fromJsonString(DesktopBackendBootstrap))(raw);101 return decodeDesktopBackendBootstrap(raw);
98}102}
99103
100function makeManagerLayer(input: {104function makeManagerLayer(input: {
apps/desktop/src/settings/DesktopClientSettings.ts+6-2
...@@ -20,10 +20,14 @@ const ClientSettingsDocumentSchema = Schema.Struct({...@@ -20,10 +20,14 @@ const ClientSettingsDocumentSchema = Schema.Struct({
2020
21const ClientSettingsJson = fromLenientJson(ClientSettingsSchema);21const ClientSettingsJson = fromLenientJson(ClientSettingsSchema);
22const LegacyClientSettingsDocumentJson = fromLenientJson(ClientSettingsDocumentSchema);22const LegacyClientSettingsDocumentJson = fromLenientJson(ClientSettingsDocumentSchema);
23const decodeLegacyClientSettingsDocumentJson = Schema.decodeEffect(
24 LegacyClientSettingsDocumentJson,
25);
26const decodeClientSettingsJsonValue = Schema.decodeEffect(ClientSettingsJson);
23const decodeClientSettingsJson = (raw: string): Effect.Effect<ClientSettings, Schema.SchemaError> =>27const decodeClientSettingsJson = (raw: string): Effect.Effect<ClientSettings, Schema.SchemaError> =>
24 Schema.decodeEffect(LegacyClientSettingsDocumentJson)(raw).pipe(28 decodeLegacyClientSettingsDocumentJson(raw).pipe(
25 Effect.map((document) => document.settings),29 Effect.map((document) => document.settings),
26 Effect.catch(() => Schema.decodeEffect(ClientSettingsJson)(raw)),30 Effect.catch(() => decodeClientSettingsJsonValue(raw)),
27 );31 );
28const encodeClientSettingsJson = Schema.encodeEffect(ClientSettingsJson);32const encodeClientSettingsJson = Schema.encodeEffect(ClientSettingsJson);
2933
apps/desktop/src/updates/DesktopUpdates.ts+7-4
...@@ -54,6 +54,9 @@ const UpdateInfo = Schema.Struct({...@@ -54,6 +54,9 @@ const UpdateInfo = Schema.Struct({
54const DownloadProgressInfo = Schema.Struct({54const DownloadProgressInfo = Schema.Struct({
55 percent: Schema.Number,55 percent: Schema.Number,
56});56});
57const decodeAppUpdateYmlConfig = Schema.decodeUnknownEffect(AppUpdateYmlConfig);
58const decodeUpdateInfo = Schema.decodeUnknownEffect(UpdateInfo);
59const decodeDownloadProgressInfo = Schema.decodeUnknownEffect(DownloadProgressInfo);
5760
58const currentIsoTimestamp = DateTime.now.pipe(Effect.map(DateTime.formatIso));61const currentIsoTimestamp = DateTime.now.pipe(Effect.map(DateTime.formatIso));
5962
...@@ -115,7 +118,7 @@ function parseAppUpdateYml(raw: string): Effect.Effect<Option.Option<AppUpdateYm...@@ -115,7 +118,7 @@ function parseAppUpdateYml(raw: string): Effect.Effect<Option.Option<AppUpdateYm
115 }118 }
116 }119 }
117120
118 return Schema.decodeUnknownEffect(AppUpdateYmlConfig)(entries).pipe(121 return decodeAppUpdateYmlConfig(entries).pipe(
119 Effect.map((config) => (config.provider ? Option.some(config) : Option.none())),122 Effect.map((config) => (config.provider ? Option.some(config) : Option.none())),
120 Effect.catch(() => Effect.succeed(Option.none<AppUpdateYmlConfig>())),123 Effect.catch(() => Effect.succeed(Option.none<AppUpdateYmlConfig>())),
121 );124 );
...@@ -408,7 +411,7 @@ const make = Effect.gen(function* () {...@@ -408,7 +411,7 @@ const make = Effect.gen(function* () {
408 const handleUpdateAvailable = Effect.fn("desktop.updates.handleUpdateAvailable")(function* (411 const handleUpdateAvailable = Effect.fn("desktop.updates.handleUpdateAvailable")(function* (
409 raw: unknown,412 raw: unknown,
410 ) {413 ) {
411 yield* Schema.decodeUnknownEffect(UpdateInfo)(raw).pipe(414 yield* decodeUpdateInfo(raw).pipe(
412 Effect.flatMap(415 Effect.flatMap(
413 Effect.fn("desktop.updates.applyUpdateAvailable")(function* (info) {416 Effect.fn("desktop.updates.applyUpdateAvailable")(function* (info) {
414 const state = yield* Ref.get(updateStateRef);417 const state = yield* Ref.get(updateStateRef);
...@@ -479,7 +482,7 @@ const make = Effect.gen(function* () {...@@ -479,7 +482,7 @@ const make = Effect.gen(function* () {
479 const handleDownloadProgress = Effect.fn("desktop.updates.handleDownloadProgress")(function* (482 const handleDownloadProgress = Effect.fn("desktop.updates.handleDownloadProgress")(function* (
480 raw: unknown,483 raw: unknown,
481 ) {484 ) {
482 yield* Schema.decodeUnknownEffect(DownloadProgressInfo)(raw).pipe(485 yield* decodeDownloadProgressInfo(raw).pipe(
483 Effect.flatMap(486 Effect.flatMap(
484 Effect.fn("desktop.updates.applyDownloadProgress")(function* (progress) {487 Effect.fn("desktop.updates.applyDownloadProgress")(function* (progress) {
485 const state = yield* Ref.get(updateStateRef);488 const state = yield* Ref.get(updateStateRef);
...@@ -506,7 +509,7 @@ const make = Effect.gen(function* () {...@@ -506,7 +509,7 @@ const make = Effect.gen(function* () {
506 const handleUpdateDownloaded = Effect.fn("desktop.updates.handleUpdateDownloaded")(function* (509 const handleUpdateDownloaded = Effect.fn("desktop.updates.handleUpdateDownloaded")(function* (
507 raw: unknown,510 raw: unknown,
508 ) {511 ) {
509 yield* Schema.decodeUnknownEffect(UpdateInfo)(raw).pipe(512 yield* decodeUpdateInfo(raw).pipe(
510 Effect.flatMap(513 Effect.flatMap(
511 Effect.fn("desktop.updates.applyUpdateDownloaded")(function* (info) {514 Effect.fn("desktop.updates.applyUpdateDownloaded")(function* (info) {
512 const state = yield* Ref.get(updateStateRef);515 const state = yield* Ref.get(updateStateRef);
apps/server/integration/OrchestrationEngineHarness.integration.ts+3-1
...@@ -79,6 +79,8 @@ import { VcsStatusBroadcaster } from "../src/vcs/VcsStatusBroadcaster.ts";...@@ -79,6 +79,8 @@ import { VcsStatusBroadcaster } from "../src/vcs/VcsStatusBroadcaster.ts";
79import { GitWorkflowService } from "../src/git/GitWorkflowService.ts";79import { GitWorkflowService } from "../src/git/GitWorkflowService.ts";
80import * as VcsProcess from "../src/vcs/VcsProcess.ts";80import * as VcsProcess from "../src/vcs/VcsProcess.ts";
8181
82const decodeCodexSettings = Schema.decodeEffect(CodexSettings);
83
82function runGit(cwd: string, args: ReadonlyArray<string>) {84function runGit(cwd: string, args: ReadonlyArray<string>) {
83 return execFileSync("git", args, {85 return execFileSync("git", args, {
84 cwd,86 cwd,
...@@ -266,7 +268,7 @@ export const makeOrchestrationIntegrationHarness = (...@@ -266,7 +268,7 @@ export const makeOrchestrationIntegrationHarness = (
266 const realCodexRegistry = Layer.effect(268 const realCodexRegistry = Layer.effect(
267 ProviderAdapterRegistry,269 ProviderAdapterRegistry,
268 Effect.gen(function* () {270 Effect.gen(function* () {
269 const codexSettings = Schema.decodeSync(CodexSettings)({});271 const codexSettings = yield* decodeCodexSettings({});
270 const codexAdapter = yield* makeCodexAdapter(codexSettings);272 const codexAdapter = yield* makeCodexAdapter(codexSettings);
271 return makeAdapterRegistryMock({273 return makeAdapterRegistryMock({
272 [ProviderDriverKind.make("codex")]: codexAdapter,274 [ProviderDriverKind.make("codex")]: codexAdapter,
apps/server/src/bootstrap.test.ts+3-6
...@@ -38,6 +38,7 @@ vi.mock("node:fs", async (importOriginal) => {...@@ -38,6 +38,7 @@ vi.mock("node:fs", async (importOriginal) => {
38});38});
3939
40const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });40const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });
41const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema));
4142
42it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {43it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
43 it.effect("uses platform-specific fd paths", () =>44 it.effect("uses platform-specific fd paths", () =>
...@@ -55,9 +56,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {...@@ -55,9 +56,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
5556
56 yield* fs.writeFileString(57 yield* fs.writeFileString(
57 filePath,58 filePath,
58 `${yield* Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema))({59 `${yield* encodeTestEnvelopeSchema({ mode: "desktop" })}\n`,
59 mode: "desktop",
60 })}\n`,
61 );60 );
6261
63 const fd = yield* Effect.acquireRelease(62 const fd = yield* Effect.acquireRelease(
...@@ -79,9 +78,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {...@@ -79,9 +78,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
7978
80 yield* fs.writeFileString(79 yield* fs.writeFileString(
81 filePath,80 filePath,
82 `${yield* Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema))({81 `${yield* encodeTestEnvelopeSchema({ mode: "desktop" })}\n`,
83 mode: "desktop",
84 })}\n`,
85 );82 );
8683
87 // Open without acquireRelease: the direct-stream fallback uses autoClose: true,84 // Open without acquireRelease: the direct-stream fallback uses autoClose: true,
apps/server/src/diagnostics/ProcessDiagnostics.ts+2-1
...@@ -50,6 +50,7 @@ class ProcessDiagnosticsError extends Schema.TaggedErrorClass<ProcessDiagnostics...@@ -50,6 +50,7 @@ class ProcessDiagnosticsError extends Schema.TaggedErrorClass<ProcessDiagnostics
50 cause: Schema.optional(Schema.Defect),50 cause: Schema.optional(Schema.Defect),
51 },51 },
52) {}52) {}
53const isProcessDiagnosticsError = Schema.is(ProcessDiagnosticsError);
5354
54function toProcessDiagnosticsError(message: string, cause?: unknown): ProcessDiagnosticsError {55function toProcessDiagnosticsError(message: string, cause?: unknown): ProcessDiagnosticsError {
55 return new ProcessDiagnosticsError({56 return new ProcessDiagnosticsError({
...@@ -316,7 +317,7 @@ const runProcess = Effect.fn("runProcess")(...@@ -316,7 +317,7 @@ const runProcess = Effect.fn("runProcess")(
316 }),317 }),
317 ),318 ),
318 Effect.mapError((cause) =>319 Effect.mapError((cause) =>
319 Schema.is(ProcessDiagnosticsError)(cause)320 isProcessDiagnosticsError(cause)
320 ? cause321 ? cause
321 : toProcessDiagnosticsError(input.errorMessage, cause),322 : toProcessDiagnosticsError(input.errorMessage, cause),
322 ),323 ),
apps/server/src/keybindings.test.ts+10-4
...@@ -23,6 +23,12 @@ import {...@@ -23,6 +23,12 @@ import {
23import { KeybindingsConfigError } from "@t3tools/contracts";23import { KeybindingsConfigError } from "@t3tools/contracts";
2424
25const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);25const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);
26const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson);
27const decodeKeybindingsConfigJson = Schema.decodeUnknownEffect(KeybindingsConfigJson);
28const encodeResolvedKeybindingFromConfig = Schema.encodeEffect(ResolvedKeybindingFromConfig);
29const decodeResolvedKeybindingFromConfigExit = Schema.decodeUnknownExit(
30 ResolvedKeybindingFromConfig,
31);
26const makeKeybindingsLayer = () => {32const makeKeybindingsLayer = () => {
27 return KeybindingsLive.pipe(33 return KeybindingsLive.pipe(
28 Layer.provideMerge(34 Layer.provideMerge(
...@@ -45,7 +51,7 @@ const writeKeybindingsConfig = (configPath: string, rules: readonly KeybindingRu...@@ -45,7 +51,7 @@ const writeKeybindingsConfig = (configPath: string, rules: readonly KeybindingRu
45 Effect.gen(function* () {51 Effect.gen(function* () {
46 const fileSystem = yield* FileSystem.FileSystem;52 const fileSystem = yield* FileSystem.FileSystem;
47 const path = yield* Path.Path;53 const path = yield* Path.Path;
48 const encoded = yield* Schema.encodeEffect(KeybindingsConfigJson)(rules);54 const encoded = yield* encodeKeybindingsConfigJson(rules);
49 yield* fileSystem.makeDirectory(path.dirname(configPath), { recursive: true });55 yield* fileSystem.makeDirectory(path.dirname(configPath), { recursive: true });
50 yield* fileSystem.writeFileString(configPath, encoded);56 yield* fileSystem.writeFileString(configPath, encoded);
51 });57 });
...@@ -54,7 +60,7 @@ const readKeybindingsConfig = (configPath: string) =>...@@ -54,7 +60,7 @@ const readKeybindingsConfig = (configPath: string) =>
54 Effect.gen(function* () {60 Effect.gen(function* () {
55 const fileSystem = yield* FileSystem.FileSystem;61 const fileSystem = yield* FileSystem.FileSystem;
56 const rawConfig = yield* fileSystem.readFileString(configPath);62 const rawConfig = yield* fileSystem.readFileString(configPath);
57 return yield* Schema.decodeUnknownEffect(KeybindingsConfigJson)(rawConfig);63 return yield* decodeKeybindingsConfigJson(rawConfig);
58 });64 });
5965
60it.layer(NodeServices.layer)("keybindings", (it) => {66it.layer(NodeServices.layer)("keybindings", (it) => {
...@@ -111,7 +117,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -111,7 +117,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
111117
112 it.effect("encodes resolved plus-key shortcuts", () =>118 it.effect("encodes resolved plus-key shortcuts", () =>
113 Effect.gen(function* () {119 Effect.gen(function* () {
114 const encoded = yield* Schema.encodeEffect(ResolvedKeybindingFromConfig)({120 const encoded = yield* encodeResolvedKeybindingFromConfig({
115 command: "terminal.toggle",121 command: "terminal.toggle",
116 shortcut: {122 shortcut: {
117 key: "+",123 key: "+",
...@@ -157,7 +163,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -157,7 +163,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
157163
158 it.effect("formats invalid resolved keybinding rules with the custom message", () =>164 it.effect("formats invalid resolved keybinding rules with the custom message", () =>
159 Effect.sync(() => {165 Effect.sync(() => {
160 const result = Schema.decodeUnknownExit(ResolvedKeybindingFromConfig)({166 const result = decodeResolvedKeybindingFromConfigExit({
161 key: "mod+shift+d+o",167 key: "mod+shift+d+o",
162 command: "terminal.new",168 command: "terminal.new",
163 });169 });
apps/server/src/keybindings.ts+10-6
...@@ -170,6 +170,10 @@ function encodeWhenAst(node: KeybindingWhenNode): string {...@@ -170,6 +170,10 @@ function encodeWhenAst(node: KeybindingWhenNode): string {
170170
171const RawKeybindingsEntries = fromLenientJson(Schema.Array(Schema.Unknown));171const RawKeybindingsEntries = fromLenientJson(Schema.Array(Schema.Unknown));
172const KeybindingsConfigPrettyJson = fromJsonStringPretty(KeybindingsConfig);172const KeybindingsConfigPrettyJson = fromJsonStringPretty(KeybindingsConfig);
173const decodeKeybindingRuleExit = Schema.decodeUnknownExit(KeybindingRule);
174const decodeResolvedKeybindingFromConfigExit = Schema.decodeExit(ResolvedKeybindingFromConfig);
175const decodeRawKeybindingsEntriesExit = Schema.decodeUnknownExit(RawKeybindingsEntries);
176const encodeKeybindingsConfigPrettyJson = Schema.encodeEffect(KeybindingsConfigPrettyJson);
173177
174export interface KeybindingsConfigState {178export interface KeybindingsConfigState {
175 readonly keybindings: ResolvedKeybindingsConfig;179 readonly keybindings: ResolvedKeybindingsConfig;
...@@ -344,7 +348,7 @@ const makeKeybindings = Effect.gen(function* () {...@@ -344,7 +348,7 @@ const makeKeybindings = Effect.gen(function* () {
344348
345 return yield* Effect.forEach(rawConfig, (entry) =>349 return yield* Effect.forEach(rawConfig, (entry) =>
346 Effect.gen(function* () {350 Effect.gen(function* () {
347 const decodedRule = Schema.decodeUnknownExit(KeybindingRule)(entry);351 const decodedRule = decodeKeybindingRuleExit(entry);
348 if (decodedRule._tag === "Failure") {352 if (decodedRule._tag === "Failure") {
349 yield* Effect.logWarning("ignoring invalid keybinding entry", {353 yield* Effect.logWarning("ignoring invalid keybinding entry", {
350 path: keybindingsConfigPath,354 path: keybindingsConfigPath,
...@@ -353,7 +357,7 @@ const makeKeybindings = Effect.gen(function* () {...@@ -353,7 +357,7 @@ const makeKeybindings = Effect.gen(function* () {
353 });357 });
354 return null;358 return null;
355 }359 }
356 const resolved = Schema.decodeExit(ResolvedKeybindingFromConfig)(decodedRule.value);360 const resolved = decodeResolvedKeybindingFromConfigExit(decodedRule.value);
357 if (resolved._tag === "Failure") {361 if (resolved._tag === "Failure") {
358 yield* Effect.logWarning("ignoring invalid keybinding entry", {362 yield* Effect.logWarning("ignoring invalid keybinding entry", {
359 path: keybindingsConfigPath,363 path: keybindingsConfigPath,
...@@ -379,7 +383,7 @@ const makeKeybindings = Effect.gen(function* () {...@@ -379,7 +383,7 @@ const makeKeybindings = Effect.gen(function* () {
379 }383 }
380384
381 const rawConfig = yield* readRawConfig;385 const rawConfig = yield* readRawConfig;
382 const decodedEntries = Schema.decodeUnknownExit(RawKeybindingsEntries)(rawConfig);386 const decodedEntries = decodeRawKeybindingsEntriesExit(rawConfig);
383 if (decodedEntries._tag === "Failure") {387 if (decodedEntries._tag === "Failure") {
384 const detail = `expected JSON array (${Cause.pretty(decodedEntries.cause)})`;388 const detail = `expected JSON array (${Cause.pretty(decodedEntries.cause)})`;
385 return {389 return {
...@@ -391,7 +395,7 @@ const makeKeybindings = Effect.gen(function* () {...@@ -391,7 +395,7 @@ const makeKeybindings = Effect.gen(function* () {
391 const keybindings: KeybindingRule[] = [];395 const keybindings: KeybindingRule[] = [];
392 const issues: ServerConfigIssue[] = [];396 const issues: ServerConfigIssue[] = [];
393 for (const [index, entry] of decodedEntries.value.entries()) {397 for (const [index, entry] of decodedEntries.value.entries()) {
394 const decodedRule = Schema.decodeUnknownExit(KeybindingRule)(entry);398 const decodedRule = decodeKeybindingRuleExit(entry);
395 if (decodedRule._tag === "Failure") {399 if (decodedRule._tag === "Failure") {
396 const detail = Cause.pretty(decodedRule.cause);400 const detail = Cause.pretty(decodedRule.cause);
397 issues.push(invalidEntryIssue(index, detail));401 issues.push(invalidEntryIssue(index, detail));
...@@ -404,7 +408,7 @@ const makeKeybindings = Effect.gen(function* () {...@@ -404,7 +408,7 @@ const makeKeybindings = Effect.gen(function* () {
404 continue;408 continue;
405 }409 }
406410
407 const resolvedRule = Schema.decodeExit(ResolvedKeybindingFromConfig)(decodedRule.value);411 const resolvedRule = decodeResolvedKeybindingFromConfigExit(decodedRule.value);
408 if (resolvedRule._tag === "Failure") {412 if (resolvedRule._tag === "Failure") {
409 const detail = Cause.pretty(resolvedRule.cause);413 const detail = Cause.pretty(resolvedRule.cause);
410 issues.push(invalidEntryIssue(index, detail));414 issues.push(invalidEntryIssue(index, detail));
...@@ -423,7 +427,7 @@ const makeKeybindings = Effect.gen(function* () {...@@ -423,7 +427,7 @@ const makeKeybindings = Effect.gen(function* () {
423 });427 });
424428
425 const writeConfigAtomically = (rules: readonly KeybindingRule[]) => {429 const writeConfigAtomically = (rules: readonly KeybindingRule[]) => {
426 return Schema.encodeEffect(KeybindingsConfigPrettyJson)(rules).pipe(430 return encodeKeybindingsConfigPrettyJson(rules).pipe(
427 Effect.map((encoded) => `${encoded}\n`),431 Effect.map((encoded) => `${encoded}\n`),
428 Effect.flatMap((encoded) =>432 Effect.flatMap((encoded) =>
429 writeFileStringAtomically({433 writeFileStringAtomically({
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+6-2
...@@ -44,6 +44,10 @@ import {...@@ -44,6 +44,10 @@ import {
44 OrchestrationEngineService,44 OrchestrationEngineService,
45 type OrchestrationEngineShape,45 type OrchestrationEngineShape,
46} from "../Services/OrchestrationEngine.ts";46} from "../Services/OrchestrationEngine.ts";
47const isOrchestrationCommandPreviouslyRejectedError = Schema.is(
48 OrchestrationCommandPreviouslyRejectedError,
49);
50const isOrchestrationCommandInvariantError = Schema.is(OrchestrationCommandInvariantError);
4751
48interface CommandEnvelope {52interface CommandEnvelope {
49 command: OrchestrationCommand;53 command: OrchestrationCommand;
...@@ -245,7 +249,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {...@@ -245,7 +249,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
245 }249 }
246250
247 const error = Cause.squash(exit.cause) as OrchestrationDispatchError;251 const error = Cause.squash(exit.cause) as OrchestrationDispatchError;
248 if (!Schema.is(OrchestrationCommandPreviouslyRejectedError)(error)) {252 if (!isOrchestrationCommandPreviouslyRejectedError(error)) {
249 yield* reconcileReadModelAfterDispatchFailure.pipe(253 yield* reconcileReadModelAfterDispatchFailure.pipe(
250 Effect.catch(() =>254 Effect.catch(() =>
251 Effect.logWarning(255 Effect.logWarning(
...@@ -259,7 +263,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {...@@ -259,7 +263,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
259 ),263 ),
260 );264 );
261265
262 if (Schema.is(OrchestrationCommandInvariantError)(error)) {266 if (isOrchestrationCommandInvariantError(error)) {
263 yield* commandReceiptRepository267 yield* commandReceiptRepository
264 .upsert({268 .upsert({
265 commandId: envelope.command.commandId,269 commandId: envelope.command.commandId,
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+5-3
...@@ -39,6 +39,8 @@ import {...@@ -39,6 +39,8 @@ import {
39import { ServerSettingsService } from "../../serverSettings.ts";39import { ServerSettingsService } from "../../serverSettings.ts";
40import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";40import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";
41import { GitWorkflowService } from "../../git/GitWorkflowService.ts";41import { GitWorkflowService } from "../../git/GitWorkflowService.ts";
42const isProviderAdapterRequestError = Schema.is(ProviderAdapterRequestError);
43const isProviderDriverKind = Schema.is(ProviderDriverKind);
4244
43type ProviderIntentEvent = Extract<45type ProviderIntentEvent = Extract<
44 OrchestrationEvent,46 OrchestrationEvent,
...@@ -118,7 +120,7 @@ function findProviderAdapterRequestError(...@@ -118,7 +120,7 @@ function findProviderAdapterRequestError(
118 cause: Cause.Cause<ProviderServiceError>,120 cause: Cause.Cause<ProviderServiceError>,
119): ProviderAdapterRequestError | undefined {121): ProviderAdapterRequestError | undefined {
120 const failReason = cause.reasons.find(Cause.isFailReason);122 const failReason = cause.reasons.find(Cause.isFailReason);
121 return Schema.is(ProviderAdapterRequestError)(failReason?.error) ? failReason.error : undefined;123 return isProviderAdapterRequestError(failReason?.error) ? failReason.error : undefined;
122}124}
123125
124function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderServiceError>): boolean {126function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderServiceError>): boolean {
...@@ -233,7 +235,7 @@ const make = Effect.gen(function* () {...@@ -233,7 +235,7 @@ const make = Effect.gen(function* () {
233235
234 const formatFailureDetail = (cause: Cause.Cause<unknown>): string => {236 const formatFailureDetail = (cause: Cause.Cause<unknown>): string => {
235 const failReason = cause.reasons.find(Cause.isFailReason);237 const failReason = cause.reasons.find(Cause.isFailReason);
236 const providerError = Schema.is(ProviderAdapterRequestError)(failReason?.error)238 const providerError = isProviderAdapterRequestError(failReason?.error)
237 ? failReason.error239 ? failReason.error
238 : undefined;240 : undefined;
239 if (providerError) {241 if (providerError) {
...@@ -361,7 +363,7 @@ const make = Effect.gen(function* () {...@@ -361,7 +363,7 @@ const make = Effect.gen(function* () {
361 ),363 ),
362 );364 );
363 const desiredDriverKind = desiredInfo.driverKind;365 const desiredDriverKind = desiredInfo.driverKind;
364 if (!Schema.is(ProviderDriverKind)(desiredDriverKind)) {366 if (!isProviderDriverKind(desiredDriverKind)) {
365 return yield* new ProviderAdapterRequestError({367 return yield* new ProviderAdapterRequestError({
366 provider: providerErrorLabel(String(desiredDriverKind)),368 provider: providerErrorLabel(String(desiredDriverKind)),
367 method: "thread.turn.start",369 method: "thread.turn.start",
apps/server/src/orchestration/projector.ts+4-5
...@@ -47,15 +47,14 @@ function updateThread(...@@ -47,15 +47,14 @@ function updateThread(
47}47}
4848
49function decodeForEvent<A>(49function decodeForEvent<A>(
50 schema: Schema.Schema<A>,50 schema: Schema.Decoder<A, never>,
51 value: unknown,51 value: unknown,
52 eventType: OrchestrationEvent["type"],52 eventType: OrchestrationEvent["type"],
53 field: string,53 field: string,
54): Effect.Effect<A, OrchestrationProjectorDecodeError> {54): Effect.Effect<A, OrchestrationProjectorDecodeError> {
55 return Effect.try({55 return Schema.decodeUnknownEffect(schema)(value).pipe(
56 try: () => Schema.decodeUnknownSync(schema as any)(value),56 Effect.mapError(toProjectorDecodeError(`${eventType}:${field}`)),
57 catch: (error) => toProjectorDecodeError(`${eventType}:${field}`)(error as Schema.SchemaError),57 );
58 });
59}58}
6059
61function retainThreadMessagesAfterRevert(60function retainThreadMessagesAfterRevert(
apps/server/src/persistence/Errors.ts+3-1
...@@ -30,6 +30,8 @@ export class PersistenceDecodeError extends Schema.TaggedErrorClass<PersistenceD...@@ -30,6 +30,8 @@ export class PersistenceDecodeError extends Schema.TaggedErrorClass<PersistenceD
30 return `Decode error in ${this.operation}: ${this.issue}`;30 return `Decode error in ${this.operation}: ${this.issue}`;
31 }31 }
32}32}
33const isPersistenceSqlError = Schema.is(PersistenceSqlError);
34const isPersistenceDecodeError = Schema.is(PersistenceDecodeError);
3335
34export function toPersistenceSqlError(operation: string) {36export function toPersistenceSqlError(operation: string) {
35 return (cause: unknown): PersistenceSqlError =>37 return (cause: unknown): PersistenceSqlError =>
...@@ -59,7 +61,7 @@ export function toPersistenceDecodeCauseError(operation: string) {...@@ -59,7 +61,7 @@ export function toPersistenceDecodeCauseError(operation: string) {
59}61}
6062
61export const isPersistenceError = (u: unknown) =>63export const isPersistenceError = (u: unknown) =>
62 Schema.is(PersistenceSqlError)(u) || Schema.is(PersistenceDecodeError)(u);64 isPersistenceSqlError(u) || isPersistenceDecodeError(u);
6365
64// ===============================66// ===============================
65// Provider Session Repository Errors67// Provider Session Repository Errors
apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts+2-1
...@@ -10,6 +10,7 @@ import { PersistenceDecodeError } from "../Errors.ts";...@@ -10,6 +10,7 @@ import { PersistenceDecodeError } from "../Errors.ts";
10import { OrchestrationEventStore } from "../Services/OrchestrationEventStore.ts";10import { OrchestrationEventStore } from "../Services/OrchestrationEventStore.ts";
11import { OrchestrationEventStoreLive } from "./OrchestrationEventStore.ts";11import { OrchestrationEventStoreLive } from "./OrchestrationEventStore.ts";
12import { SqlitePersistenceMemory } from "./Sqlite.ts";12import { SqlitePersistenceMemory } from "./Sqlite.ts";
13const isPersistenceDecodeError = Schema.is(PersistenceDecodeError);
1314
14const layer = it.layer(15const layer = it.layer(
15 OrchestrationEventStoreLive.pipe(Layer.provideMerge(SqlitePersistenceMemory)),16 OrchestrationEventStoreLive.pipe(Layer.provideMerge(SqlitePersistenceMemory)),
...@@ -110,7 +111,7 @@ layer("OrchestrationEventStore", (it) => {...@@ -110,7 +111,7 @@ layer("OrchestrationEventStore", (it) => {
110 );111 );
111 assert.equal(replayResult._tag, "Failure");112 assert.equal(replayResult._tag, "Failure");
112 if (replayResult._tag === "Failure") {113 if (replayResult._tag === "Failure") {
113 assert.ok(Schema.is(PersistenceDecodeError)(replayResult.failure));114 assert.ok(isPersistenceDecodeError(replayResult.failure));
114 assert.ok(115 assert.ok(
115 replayResult.failure.operation.includes(116 replayResult.failure.operation.includes(
116 "OrchestrationEventStore.readFromSequence:decodeRows",117 "OrchestrationEventStore.readFromSequence:decodeRows",
apps/server/src/provider/Drivers/ClaudeDriver.ts+2-1
...@@ -48,6 +48,7 @@ import {...@@ -48,6 +48,7 @@ import {
48 resolveProviderMaintenanceCapabilitiesEffect,48 resolveProviderMaintenanceCapabilitiesEffect,
49} from "../providerMaintenance.ts";49} from "../providerMaintenance.ts";
50import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts";50import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts";
51const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
5152
52const DRIVER_KIND = ProviderDriverKind.make("claudeAgent");53const DRIVER_KIND = ProviderDriverKind.make("claudeAgent");
53const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);54const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
...@@ -105,7 +106,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {...@@ -105,7 +106,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
105 supportsMultipleInstances: true,106 supportsMultipleInstances: true,
106 },107 },
107 configSchema: ClaudeSettings,108 configSchema: ClaudeSettings,
108 defaultConfig: (): ClaudeSettings => Schema.decodeSync(ClaudeSettings)({}),109 defaultConfig: (): ClaudeSettings => decodeClaudeSettings({}),
109 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>110 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
110 Effect.gen(function* () {111 Effect.gen(function* () {
111 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;112 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
apps/server/src/provider/Drivers/CodexDriver.ts+2-1
...@@ -51,6 +51,7 @@ import {...@@ -51,6 +51,7 @@ import {
51 materializeCodexShadowHome,51 materializeCodexShadowHome,
52 resolveCodexHomeLayout,52 resolveCodexHomeLayout,
53} from "./CodexHomeLayout.ts";53} from "./CodexHomeLayout.ts";
54const decodeCodexSettings = Schema.decodeSync(CodexSettings);
5455
55const DRIVER_KIND = ProviderDriverKind.make("codex");56const DRIVER_KIND = ProviderDriverKind.make("codex");
56const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);57const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
...@@ -103,7 +104,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {...@@ -103,7 +104,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
103 supportsMultipleInstances: true,104 supportsMultipleInstances: true,
104 },105 },
105 configSchema: CodexSettings,106 configSchema: CodexSettings,
106 defaultConfig: (): CodexSettings => Schema.decodeSync(CodexSettings)({}),107 defaultConfig: (): CodexSettings => decodeCodexSettings({}),
107 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>108 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
108 Effect.gen(function* () {109 Effect.gen(function* () {
109 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;110 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
apps/server/src/provider/Drivers/CodexHomeLayout.test.ts+2-1
...@@ -11,6 +11,7 @@ import {...@@ -11,6 +11,7 @@ import {
11 materializeCodexShadowHome,11 materializeCodexShadowHome,
12 resolveCodexHomeLayout,12 resolveCodexHomeLayout,
13} from "./CodexHomeLayout.ts";13} from "./CodexHomeLayout.ts";
14const decodeCodexSettingsValue = Schema.decodeSync(CodexSettings);
1415
15const decodeCodexSettings = (input: {16const decodeCodexSettings = (input: {
16 readonly enabled?: boolean;17 readonly enabled?: boolean;
...@@ -18,7 +19,7 @@ const decodeCodexSettings = (input: {...@@ -18,7 +19,7 @@ const decodeCodexSettings = (input: {
18 readonly shadowHomePath?: string;19 readonly shadowHomePath?: string;
19 readonly customModels?: readonly string[];20 readonly customModels?: readonly string[];
20 readonly binaryPath?: string;21 readonly binaryPath?: string;
21}): CodexSettings => Schema.decodeSync(CodexSettings)(input);22}): CodexSettings => decodeCodexSettingsValue(input);
2223
23const makeTempDir = Effect.fn("CodexHomeLayout.test.makeTempDir")(function* (prefix: string) {24const makeTempDir = Effect.fn("CodexHomeLayout.test.makeTempDir")(function* (prefix: string) {
24 const fileSystem = yield* FileSystem.FileSystem;25 const fileSystem = yield* FileSystem.FileSystem;
apps/server/src/provider/Drivers/CodexHomeLayout.ts+2-1
...@@ -74,6 +74,7 @@ export class CodexShadowHomeError extends Schema.TaggedErrorClass<CodexShadowHom...@@ -74,6 +74,7 @@ export class CodexShadowHomeError extends Schema.TaggedErrorClass<CodexShadowHom
74 return this.detail;74 return this.detail;
75 }75 }
76}76}
77const isCodexShadowHomeError = Schema.is(CodexShadowHomeError);
7778
78type LinkState =79type LinkState =
79 | {80 | {
...@@ -88,7 +89,7 @@ type LinkState =...@@ -88,7 +89,7 @@ type LinkState =
88 };89 };
8990
90function toShadowHomeError(cause: unknown): CodexShadowHomeError {91function toShadowHomeError(cause: unknown): CodexShadowHomeError {
91 return Schema.is(CodexShadowHomeError)(cause)92 return isCodexShadowHomeError(cause)
92 ? cause93 ? cause
93 : new CodexShadowHomeError({94 : new CodexShadowHomeError({
94 detail: "Failed to materialize Codex shadow home.",95 detail: "Failed to materialize Codex shadow home.",
apps/server/src/provider/Drivers/CursorDriver.ts+2-1
...@@ -45,6 +45,7 @@ import {...@@ -45,6 +45,7 @@ import {
45 makeStaticProviderMaintenanceResolver,45 makeStaticProviderMaintenanceResolver,
46 resolveProviderMaintenanceCapabilitiesEffect,46 resolveProviderMaintenanceCapabilitiesEffect,
47} from "../providerMaintenance.ts";47} from "../providerMaintenance.ts";
48const decodeCursorSettings = Schema.decodeSync(CursorSettings);
4849
49const DRIVER_KIND = ProviderDriverKind.make("cursor");50const DRIVER_KIND = ProviderDriverKind.make("cursor");
50const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);51const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
...@@ -89,7 +90,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {...@@ -89,7 +90,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
89 supportsMultipleInstances: true,90 supportsMultipleInstances: true,
90 },91 },
91 configSchema: CursorSettings,92 configSchema: CursorSettings,
92 defaultConfig: (): CursorSettings => Schema.decodeSync(CursorSettings)({}),93 defaultConfig: (): CursorSettings => decodeCursorSettings({}),
93 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>94 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
94 Effect.gen(function* () {95 Effect.gen(function* () {
95 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;96 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
apps/server/src/provider/Drivers/OpenCodeDriver.ts+2-1
...@@ -46,6 +46,7 @@ import {...@@ -46,6 +46,7 @@ import {
46 normalizeCommandPath,46 normalizeCommandPath,
47 resolveProviderMaintenanceCapabilitiesEffect,47 resolveProviderMaintenanceCapabilitiesEffect,
48} from "../providerMaintenance.ts";48} from "../providerMaintenance.ts";
49const decodeOpenCodeSettings = Schema.decodeSync(OpenCodeSettings);
4950
50const DRIVER_KIND = ProviderDriverKind.make("opencode");51const DRIVER_KIND = ProviderDriverKind.make("opencode");
51const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);52const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
...@@ -102,7 +103,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>...@@ -102,7 +103,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
102 supportsMultipleInstances: true,103 supportsMultipleInstances: true,
103 },104 },
104 configSchema: OpenCodeSettings,105 configSchema: OpenCodeSettings,
105 defaultConfig: (): OpenCodeSettings => Schema.decodeSync(OpenCodeSettings)({}),106 defaultConfig: (): OpenCodeSettings => decodeOpenCodeSettings({}),
106 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>107 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
107 Effect.gen(function* () {108 Effect.gen(function* () {
108 const openCodeRuntime = yield* OpenCodeRuntime;109 const openCodeRuntime = yield* OpenCodeRuntime;
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+4-3
...@@ -38,6 +38,7 @@ import { ServerSettingsService } from "../../serverSettings.ts";...@@ -38,6 +38,7 @@ import { ServerSettingsService } from "../../serverSettings.ts";
38import { ProviderAdapterValidationError } from "../Errors.ts";38import { ProviderAdapterValidationError } from "../Errors.ts";
39import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";39import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
40import { makeClaudeAdapter, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts";40import { makeClaudeAdapter, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts";
41const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
4142
42// Test-local service tag so the rest of the file can keep using `yield* ClaudeAdapter`.43// Test-local service tag so the rest of the file can keep using `yield* ClaudeAdapter`.
43class ClaudeAdapter extends Context.Service<ClaudeAdapter, ClaudeAdapterShape>()(44class ClaudeAdapter extends Context.Service<ClaudeAdapter, ClaudeAdapterShape>()(
...@@ -186,7 +187,7 @@ function makeHarness(config?: {...@@ -186,7 +187,7 @@ function makeHarness(config?: {
186 layer: Layer.effect(187 layer: Layer.effect(
187 ClaudeAdapter,188 ClaudeAdapter,
188 Effect.gen(function* () {189 Effect.gen(function* () {
189 const claudeConfig = Schema.decodeSync(ClaudeSettings)(config?.claudeConfig ?? {});190 const claudeConfig = decodeClaudeSettings(config?.claudeConfig ?? {});
190 return yield* makeClaudeAdapter(claudeConfig, adapterOptions);191 return yield* makeClaudeAdapter(claudeConfig, adapterOptions);
191 }),192 }),
192 ).pipe(193 ).pipe(
...@@ -1344,7 +1345,7 @@ describe("ClaudeAdapterLive", () => {...@@ -1344,7 +1345,7 @@ describe("ClaudeAdapterLive", () => {
1344 const layer = Layer.effect(1345 const layer = Layer.effect(
1345 ClaudeAdapter,1346 ClaudeAdapter,
1346 Effect.gen(function* () {1347 Effect.gen(function* () {
1347 const claudeConfig = Schema.decodeSync(ClaudeSettings)({});1348 const claudeConfig = decodeClaudeSettings({});
1348 return yield* makeClaudeAdapter(claudeConfig, {1349 return yield* makeClaudeAdapter(claudeConfig, {
1349 createQuery: () => {1350 createQuery: () => {
1350 const query = new FakeClaudeQuery();1351 const query = new FakeClaudeQuery();
...@@ -1427,7 +1428,7 @@ describe("ClaudeAdapterLive", () => {...@@ -1427,7 +1428,7 @@ describe("ClaudeAdapterLive", () => {
1427 const layer = Layer.effect(1428 const layer = Layer.effect(
1428 ClaudeAdapter,1429 ClaudeAdapter,
1429 Effect.gen(function* () {1430 Effect.gen(function* () {
1430 const claudeConfig = Schema.decodeSync(ClaudeSettings)({});1431 const claudeConfig = decodeClaudeSettings({});
1431 return yield* makeClaudeAdapter(claudeConfig, {1432 return yield* makeClaudeAdapter(claudeConfig, {
1432 createQuery: (input) => {1433 createQuery: (input) => {
1433 // Simulate the SDK consuming the prompt iterable1434 // Simulate the SDK consuming the prompt iterable
apps/server/src/provider/Layers/ClaudeAdapter.ts+17-18
...@@ -84,6 +84,8 @@ import {...@@ -84,6 +84,8 @@ import {
84} from "../Errors.ts";84} from "../Errors.ts";
85import { type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";85import { type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
86import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";86import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
87const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString);
88const decodeUnknownJsonStringExit = Schema.decodeUnknownExit(Schema.UnknownFromJsonString);
8789
88const PROVIDER = ProviderDriverKind.make("claudeAgent");90const PROVIDER = ProviderDriverKind.make("claudeAgent");
89type ClaudeTextStreamKind = Extract<RuntimeContentStreamKind, "assistant_text" | "reasoning_text">;91type ClaudeTextStreamKind = Extract<RuntimeContentStreamKind, "assistant_text" | "reasoning_text">;
...@@ -93,6 +95,11 @@ type ClaudeToolResultStreamKind = Extract<...@@ -93,6 +95,11 @@ type ClaudeToolResultStreamKind = Extract<
93>;95>;
94type ClaudeSdkEffort = NonNullable<ClaudeQueryOptions["effort"]>;96type ClaudeSdkEffort = NonNullable<ClaudeQueryOptions["effort"]>;
9597
98function encodeJsonStringForDiagnostics(input: unknown): string | undefined {
99 const result = encodeUnknownJsonStringExit(input);
100 return Exit.isSuccess(result) ? result.value : undefined;
101}
102
96type PromptQueueItem =103type PromptQueueItem =
97 | {104 | {
98 readonly type: "message";105 readonly type: "message";
...@@ -545,7 +552,7 @@ function summarizeToolRequest(toolName: string, input: Record<string, unknown>):...@@ -545,7 +552,7 @@ function summarizeToolRequest(toolName: string, input: Record<string, unknown>):
545 }552 }
546 }553 }
547554
548 const serialized = Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(input);555 const serialized = encodeJsonStringForDiagnostics(input) ?? "[unserializable input]";
549 if (serialized.length <= 400) {556 if (serialized.length <= 400) {
550 return `${toolName}: ${serialized}`;557 return `${toolName}: ${serialized}`;
551 }558 }
...@@ -810,22 +817,18 @@ function exitPlanCaptureKey(input: {...@@ -810,22 +817,18 @@ function exitPlanCaptureKey(input: {
810}817}
811818
812function tryParseJsonRecord(value: string): Record<string, unknown> | undefined {819function tryParseJsonRecord(value: string): Record<string, unknown> | undefined {
813 try {820 const result = decodeUnknownJsonStringExit(value);
814 const parsed = Schema.decodeUnknownSync(Schema.UnknownFromJsonString)(value);821 if (!Exit.isSuccess(result)) {
815 return parsed && typeof parsed === "object" && !Array.isArray(parsed)
816 ? (parsed as Record<string, unknown>)
817 : undefined;
818 } catch {
819 return undefined;822 return undefined;
820 }823 }
824 const parsed = result.value;
825 return parsed && typeof parsed === "object" && !Array.isArray(parsed)
826 ? (parsed as Record<string, unknown>)
827 : undefined;
821}828}
822829
823function toolInputFingerprint(input: Record<string, unknown>): string | undefined {830function toolInputFingerprint(input: Record<string, unknown>): string | undefined {
824 try {831 return encodeJsonStringForDiagnostics(input);
825 return Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(input);
826 } catch {
827 return undefined;
828 }
829}832}
830833
831function toolResultStreamKind(itemType: CanonicalItemType): ClaudeToolResultStreamKind | undefined {834function toolResultStreamKind(itemType: CanonicalItemType): ClaudeToolResultStreamKind | undefined {
...@@ -2941,12 +2944,8 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -2941,12 +2944,8 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
2941 "claude.query.include_partial_messages": true,2944 "claude.query.include_partial_messages": true,
2942 "claude.query.additional_directories": input.cwd ? [input.cwd] : [],2945 "claude.query.additional_directories": input.cwd ? [input.cwd] : [],
2943 "claude.query.setting_sources": [...CLAUDE_SETTING_SOURCES],2946 "claude.query.setting_sources": [...CLAUDE_SETTING_SOURCES],
2944 "claude.query.settings_json": Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(2947 "claude.query.settings_json": encodeJsonStringForDiagnostics(settings) ?? "",
2945 settings,2948 "claude.query.extra_args_json": encodeJsonStringForDiagnostics(extraArgs) ?? "",
2946 ),
2947 "claude.query.extra_args_json": Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(
2948 extraArgs,
2949 ),
2950 "claude.query.path_to_executable": claudeBinaryPath,2949 "claude.query.path_to_executable": claudeBinaryPath,
2951 });2950 });
29522951
apps/server/src/provider/Layers/CodexAdapter.test.ts+8-7
...@@ -46,6 +46,7 @@ import {...@@ -46,6 +46,7 @@ import {
46 type CodexThreadSnapshot,46 type CodexThreadSnapshot,
47} from "./CodexSessionRuntime.ts";47} from "./CodexSessionRuntime.ts";
48import { makeCodexAdapter } from "./CodexAdapter.ts";48import { makeCodexAdapter } from "./CodexAdapter.ts";
49const decodeCodexSettings = Schema.decodeSync(CodexSettings);
4950
50// Test-local service tag so the rest of the file can keep using `yield* CodexAdapter`.51// Test-local service tag so the rest of the file can keep using `yield* CodexAdapter`.
51class CodexAdapter extends Context.Service<CodexAdapter, CodexAdapterShape>()(52class CodexAdapter extends Context.Service<CodexAdapter, CodexAdapterShape>()(
...@@ -224,7 +225,7 @@ const validationLayer = it.layer(...@@ -224,7 +225,7 @@ const validationLayer = it.layer(
224 Layer.effect(225 Layer.effect(
225 CodexAdapter,226 CodexAdapter,
226 Effect.gen(function* () {227 Effect.gen(function* () {
227 const codexConfig = Schema.decodeSync(CodexSettings)({});228 const codexConfig = decodeCodexSettings({});
228 return yield* makeCodexAdapter(codexConfig, {229 return yield* makeCodexAdapter(codexConfig, {
229 makeRuntime: validationRuntimeFactory.factory,230 makeRuntime: validationRuntimeFactory.factory,
230 });231 });
...@@ -293,7 +294,7 @@ const sessionErrorLayer = it.layer(...@@ -293,7 +294,7 @@ const sessionErrorLayer = it.layer(
293 Layer.effect(294 Layer.effect(
294 CodexAdapter,295 CodexAdapter,
295 Effect.gen(function* () {296 Effect.gen(function* () {
296 const codexConfig = Schema.decodeSync(CodexSettings)({});297 const codexConfig = decodeCodexSettings({});
297 return yield* makeCodexAdapter(codexConfig, {298 return yield* makeCodexAdapter(codexConfig, {
298 makeRuntime: sessionRuntimeFactory.factory,299 makeRuntime: sessionRuntimeFactory.factory,
299 });300 });
...@@ -364,7 +365,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {...@@ -364,7 +365,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
364 const customLayer = Layer.effect(365 const customLayer = Layer.effect(
365 CodexAdapter,366 CodexAdapter,
366 Effect.gen(function* () {367 Effect.gen(function* () {
367 const codexConfig = Schema.decodeSync(CodexSettings)({});368 const codexConfig = decodeCodexSettings({});
368 return yield* makeCodexAdapter(codexConfig, {369 return yield* makeCodexAdapter(codexConfig, {
369 instanceId: customInstanceId,370 instanceId: customInstanceId,
370 makeRuntime: customRuntimeFactory.factory,371 makeRuntime: customRuntimeFactory.factory,
...@@ -419,7 +420,7 @@ const lifecycleLayer = it.layer(...@@ -419,7 +420,7 @@ const lifecycleLayer = it.layer(
419 Layer.effect(420 Layer.effect(
420 CodexAdapter,421 CodexAdapter,
421 Effect.gen(function* () {422 Effect.gen(function* () {
422 const codexConfig = Schema.decodeSync(CodexSettings)({});423 const codexConfig = decodeCodexSettings({});
423 return yield* makeCodexAdapter(codexConfig, {424 return yield* makeCodexAdapter(codexConfig, {
424 makeRuntime: lifecycleRuntimeFactory.factory,425 makeRuntime: lifecycleRuntimeFactory.factory,
425 });426 });
...@@ -1034,7 +1035,7 @@ const scopedLifecycleLayer = it.layer(...@@ -1034,7 +1035,7 @@ const scopedLifecycleLayer = it.layer(
1034 Layer.effect(1035 Layer.effect(
1035 CodexAdapter,1036 CodexAdapter,
1036 Effect.gen(function* () {1037 Effect.gen(function* () {
1037 const codexConfig = Schema.decodeSync(CodexSettings)({});1038 const codexConfig = decodeCodexSettings({});
1038 return yield* makeCodexAdapter(codexConfig, {1039 return yield* makeCodexAdapter(codexConfig, {
1039 makeRuntime: scopedLifecycleRuntimeFactory.factory,1040 makeRuntime: scopedLifecycleRuntimeFactory.factory,
1040 });1041 });
...@@ -1078,7 +1079,7 @@ const scopedFailureLayer = it.layer(...@@ -1078,7 +1079,7 @@ const scopedFailureLayer = it.layer(
1078 Layer.effect(1079 Layer.effect(
1079 CodexAdapter,1080 CodexAdapter,
1080 Effect.gen(function* () {1081 Effect.gen(function* () {
1081 const codexConfig = Schema.decodeSync(CodexSettings)({});1082 const codexConfig = decodeCodexSettings({});
1082 return yield* makeCodexAdapter(codexConfig, {1083 return yield* makeCodexAdapter(codexConfig, {
1083 makeRuntime: scopedFailureRuntimeFactory.factory,1084 makeRuntime: scopedFailureRuntimeFactory.factory,
1084 });1085 });
...@@ -1127,7 +1128,7 @@ it.effect("flushes managed native logs when the adapter layer shuts down", () =>...@@ -1127,7 +1128,7 @@ it.effect("flushes managed native logs when the adapter layer shuts down", () =>
1127 const layer = Layer.effect(1128 const layer = Layer.effect(
1128 CodexAdapter,1129 CodexAdapter,
1129 Effect.gen(function* () {1130 Effect.gen(function* () {
1130 const codexConfig = Schema.decodeSync(CodexSettings)({});1131 const codexConfig = decodeCodexSettings({});
1131 return yield* makeCodexAdapter(codexConfig, {1132 return yield* makeCodexAdapter(codexConfig, {
1132 makeRuntime: runtimeFactory.factory,1133 makeRuntime: runtimeFactory.factory,
1133 nativeEventLogPath: basePath,1134 nativeEventLogPath: basePath,
apps/server/src/provider/Layers/CodexAdapter.ts+11-7
...@@ -61,6 +61,12 @@ import {...@@ -61,6 +61,12 @@ import {
61 type CodexSessionRuntimeShape,61 type CodexSessionRuntimeShape,
62} from "./CodexSessionRuntime.ts";62} from "./CodexSessionRuntime.ts";
63import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";63import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
64const isCodexAppServerProcessExitedError = Schema.is(CodexErrors.CodexAppServerProcessExitedError);
65const isCodexAppServerTransportError = Schema.is(CodexErrors.CodexAppServerTransportError);
66const isCodexSessionRuntimeThreadIdMissingError = Schema.is(
67 CodexSessionRuntimeThreadIdMissingError,
68);
69const isCodexResumeCursorSchema = Schema.is(CodexResumeCursorSchema);
6470
65const PROVIDER = ProviderDriverKind.make("codex");71const PROVIDER = ProviderDriverKind.make("codex");
6672
...@@ -91,10 +97,7 @@ function mapCodexRuntimeError(...@@ -91,10 +97,7 @@ function mapCodexRuntimeError(
91 method: string,97 method: string,
92 error: CodexSessionRuntimeError,98 error: CodexSessionRuntimeError,
93): ProviderAdapterError {99): ProviderAdapterError {
94 if (100 if (isCodexAppServerProcessExitedError(error) || isCodexAppServerTransportError(error)) {
95 Schema.is(CodexErrors.CodexAppServerProcessExitedError)(error) ||
96 Schema.is(CodexErrors.CodexAppServerTransportError)(error)
97 ) {
98 return new ProviderAdapterSessionClosedError({101 return new ProviderAdapterSessionClosedError({
99 provider: PROVIDER,102 provider: PROVIDER,
100 threadId,103 threadId,
...@@ -102,7 +105,7 @@ function mapCodexRuntimeError(...@@ -102,7 +105,7 @@ function mapCodexRuntimeError(
102 });105 });
103 }106 }
104107
105 if (Schema.is(CodexSessionRuntimeThreadIdMissingError)(error)) {108 if (isCodexSessionRuntimeThreadIdMissingError(error)) {
106 return new ProviderAdapterSessionNotFoundError({109 return new ProviderAdapterSessionNotFoundError({
107 provider: PROVIDER,110 provider: PROVIDER,
108 threadId,111 threadId,
...@@ -134,7 +137,8 @@ function readPayload<A>(...@@ -134,7 +137,8 @@ function readPayload<A>(
134 schema: Schema.Schema<A>,137 schema: Schema.Schema<A>,
135 payload: ProviderEvent["payload"],138 payload: ProviderEvent["payload"],
136): A | undefined {139): A | undefined {
137 return Schema.is(schema)(payload) ? payload : undefined;140 const isPayload = Schema.is(schema);
141 return isPayload(payload) ? payload : undefined;
138}142}
139143
140function trimText(value: string | undefined | null): string | undefined {144function trimText(value: string | undefined | null): string | undefined {
...@@ -1381,7 +1385,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (...@@ -1381,7 +1385,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
1381 binaryPath: codexConfig.binaryPath,1385 binaryPath: codexConfig.binaryPath,
1382 ...(options?.environment ? { environment: options.environment } : {}),1386 ...(options?.environment ? { environment: options.environment } : {}),
1383 ...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}),1387 ...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}),
1384 ...(Schema.is(CodexResumeCursorSchema)(input.resumeCursor)1388 ...(isCodexResumeCursorSchema(input.resumeCursor)
1385 ? { resumeCursor: input.resumeCursor }1389 ? { resumeCursor: input.resumeCursor }
1386 : {}),1390 : {}),
1387 runtimeMode: input.runtimeMode,1391 runtimeMode: input.runtimeMode,
apps/server/src/provider/Layers/CodexProvider.ts+2-1
...@@ -26,6 +26,7 @@ import { buildServerProvider, type ServerProviderDraft } from "../providerSnapsh...@@ -26,6 +26,7 @@ import { buildServerProvider, type ServerProviderDraft } from "../providerSnapsh
26import { expandHomePath } from "../../pathExpansion.ts";26import { expandHomePath } from "../../pathExpansion.ts";
27import { scopedSafeTeardown } from "./scopedSafeTeardown.ts";27import { scopedSafeTeardown } from "./scopedSafeTeardown.ts";
28import packageJson from "../../../package.json" with { type: "json" };28import packageJson from "../../../package.json" with { type: "json" };
29const isCodexAppServerSpawnError = Schema.is(CodexErrors.CodexAppServerSpawnError);
2930
30const PROVIDER_PROBE_TIMEOUT_MS = 8_000;31const PROVIDER_PROBE_TIMEOUT_MS = 8_000;
31const CODEX_PRESENTATION = {32const CODEX_PRESENTATION = {
...@@ -450,7 +451,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu...@@ -450,7 +451,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
450451
451 if (Result.isFailure(probeResult)) {452 if (Result.isFailure(probeResult)) {
452 const error = probeResult.failure;453 const error = probeResult.failure;
453 const installed = !Schema.is(CodexErrors.CodexAppServerSpawnError)(error);454 const installed = !isCodexAppServerSpawnError(error);
454 return buildServerProvider({455 return buildServerProvider({
455 presentation: CODEX_PRESENTATION,456 presentation: CODEX_PRESENTATION,
456 enabled: codexSettings.enabled,457 enabled: codexSettings.enabled,
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+2-1
...@@ -16,6 +16,7 @@ import {...@@ -16,6 +16,7 @@ import {
16 isRecoverableThreadResumeError,16 isRecoverableThreadResumeError,
17 openCodexThread,17 openCodexThread,
18} from "./CodexSessionRuntime.ts";18} from "./CodexSessionRuntime.ts";
19const isCodexAppServerRequestError = Schema.is(CodexErrors.CodexAppServerRequestError);
1920
20function makeThreadOpenResponse(21function makeThreadOpenResponse(
21 threadId: string,22 threadId: string,
...@@ -269,7 +270,7 @@ describe("openCodexThread", () => {...@@ -269,7 +270,7 @@ describe("openCodexThread", () => {
269 }),270 }),
270 ),271 ),
271 (error: unknown) =>272 (error: unknown) =>
272 Schema.is(CodexErrors.CodexAppServerRequestError)(error) &&273 isCodexAppServerRequestError(error) &&
273 error.errorMessage === "timed out waiting for server",274 error.errorMessage === "timed out waiting for server",
274 );275 );
275 });276 });
apps/server/src/provider/Layers/CodexSessionRuntime.ts+10-6
...@@ -41,6 +41,7 @@ import {...@@ -41,6 +41,7 @@ import {
41 CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,41 CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
42 CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,42 CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
43} from "../CodexDeveloperInstructions.ts";43} from "../CodexDeveloperInstructions.ts";
44const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse);
4445
45const PROVIDER = ProviderDriverKind.make("codex");46const PROVIDER = ProviderDriverKind.make("codex");
4647
...@@ -66,6 +67,8 @@ export const CodexResumeCursorSchema = Schema.Struct({...@@ -66,6 +67,8 @@ export const CodexResumeCursorSchema = Schema.Struct({
66const CodexUserInputAnswerObject = Schema.Struct({67const CodexUserInputAnswerObject = Schema.Struct({
67 answers: Schema.Array(Schema.String),68 answers: Schema.Array(Schema.String),
68});69});
70const isCodexResumeCursorSchema = Schema.is(CodexResumeCursorSchema);
71const isCodexUserInputAnswerObject = Schema.is(CodexUserInputAnswerObject);
6972
70// TODO: Verify `packages/effect-codex-app-server/scripts/generate.ts` so the generated73// TODO: Verify `packages/effect-codex-app-server/scripts/generate.ts` so the generated
71// `V2TurnStartParams` schema includes `collaborationMode` directly.74// `V2TurnStartParams` schema includes `collaborationMode` directly.
...@@ -74,6 +77,9 @@ const CodexTurnStartParamsWithCollaborationMode = EffectCodexSchema.V2TurnStartP...@@ -74,6 +77,9 @@ const CodexTurnStartParamsWithCollaborationMode = EffectCodexSchema.V2TurnStartP
74 collaborationMode: Schema.optionalKey(EffectCodexSchema.V2TurnStartParams__CollaborationMode),77 collaborationMode: Schema.optionalKey(EffectCodexSchema.V2TurnStartParams__CollaborationMode),
75 }),78 }),
76);79);
80const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffect(
81 CodexTurnStartParamsWithCollaborationMode,
82);
7783
78export type CodexTurnStartParamsWithCollaborationMode =84export type CodexTurnStartParamsWithCollaborationMode =
79 typeof CodexTurnStartParamsWithCollaborationMode.Type;85 typeof CodexTurnStartParamsWithCollaborationMode.Type;
...@@ -248,7 +254,7 @@ function normalizeCodexModelSlug(...@@ -248,7 +254,7 @@ function normalizeCodexModelSlug(
248function readResumeCursorThreadId(254function readResumeCursorThreadId(
249 resumeCursor: ProviderSession["resumeCursor"],255 resumeCursor: ProviderSession["resumeCursor"],
250): string | undefined {256): string | undefined {
251 return Schema.is(CodexResumeCursorSchema)(resumeCursor) ? resumeCursor.threadId : undefined;257 return isCodexResumeCursorSchema(resumeCursor) ? resumeCursor.threadId : undefined;
252}258}
253259
254function runtimeModeToThreadConfig(input: RuntimeMode): {260function runtimeModeToThreadConfig(input: RuntimeMode): {
...@@ -367,7 +373,7 @@ export function buildTurnStartParams(input: {...@@ -367,7 +373,7 @@ export function buildTurnStartParams(input: {
367 ...(input.effort ? { effort: input.effort } : {}),373 ...(input.effort ? { effort: input.effort } : {}),
368 });374 });
369375
370 return Schema.decodeUnknownEffect(CodexTurnStartParamsWithCollaborationMode)({376 return decodeCodexTurnStartParamsWithCollaborationMode({
371 threadId: input.threadId,377 threadId: input.threadId,
372 input: turnInput,378 input: turnInput,
373 approvalPolicy: config.approvalPolicy,379 approvalPolicy: config.approvalPolicy,
...@@ -623,7 +629,7 @@ function toCodexUserInputAnswer(...@@ -623,7 +629,7 @@ function toCodexUserInputAnswer(
623 const answers = value.filter((entry): entry is string => typeof entry === "string");629 const answers = value.filter((entry): entry is string => typeof entry === "string");
624 return Effect.succeed({ answers });630 return Effect.succeed({ answers });
625 }631 }
626 if (Schema.is(CodexUserInputAnswerObject)(value)) {632 if (isCodexUserInputAnswerObject(value)) {
627 return Effect.succeed({ answers: value.answers });633 return Effect.succeed({ answers: value.answers });
628 }634 }
629 return Effect.fail(new CodexSessionRuntimeInvalidUserInputAnswersError({ questionId }));635 return Effect.fail(new CodexSessionRuntimeInvalidUserInputAnswersError({ questionId }));
...@@ -1247,9 +1253,7 @@ export const makeCodexSessionRuntime = (...@@ -1247,9 +1253,7 @@ export const makeCodexSessionRuntime = (
1247 ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}),1253 ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}),
1248 });1254 });
1249 const rawResponse = yield* client.raw.request("turn/start", params);1255 const rawResponse = yield* client.raw.request("turn/start", params);
1250 const response = yield* Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse)(1256 const response = yield* decodeV2TurnStartResponse(rawResponse).pipe(
1251 rawResponse,
1252 ).pipe(
1253 Effect.mapError((error) =>1257 Effect.mapError((error) =>
1254 toProtocolParseError("Invalid turn/start response payload", error),1258 toProtocolParseError("Invalid turn/start response payload", error),
1255 ),1259 ),
apps/server/src/provider/Layers/CursorAdapter.test.ts+4-3
...@@ -28,6 +28,7 @@ import { ServerConfig } from "../../config.ts";...@@ -28,6 +28,7 @@ import { ServerConfig } from "../../config.ts";
28import { ServerSettingsService } from "../../serverSettings.ts";28import { ServerSettingsService } from "../../serverSettings.ts";
29import type { CursorAdapterShape } from "../Services/CursorAdapter.ts";29import type { CursorAdapterShape } from "../Services/CursorAdapter.ts";
30import { makeCursorAdapter } from "./CursorAdapter.ts";30import { makeCursorAdapter } from "./CursorAdapter.ts";
31const decodeCursorSettings = Schema.decodeSync(CursorSettings);
3132
32// Test-local service tag so the rest of the file can keep using `yield* CursorAdapter`.33// Test-local service tag so the rest of the file can keep using `yield* CursorAdapter`.
33class CursorAdapter extends Context.Service<CursorAdapter, CursorAdapterShape>()(34class CursorAdapter extends Context.Service<CursorAdapter, CursorAdapterShape>()(
...@@ -132,7 +133,7 @@ const cursorAdapterTestLayer = it.layer(...@@ -132,7 +133,7 @@ const cursorAdapterTestLayer = it.layer(
132 Layer.effect(133 Layer.effect(
133 CursorAdapter,134 CursorAdapter,
134 Effect.gen(function* () {135 Effect.gen(function* () {
135 const cursorConfig = Schema.decodeSync(CursorSettings)({});136 const cursorConfig = decodeCursorSettings({});
136 const resolveSettings = yield* makeResolveCursorSettings;137 const resolveSettings = yield* makeResolveCursorSettings;
137 return yield* makeCursorAdapter(cursorConfig, { resolveSettings });138 return yield* makeCursorAdapter(cursorConfig, { resolveSettings });
138 }),139 }),
...@@ -607,7 +608,7 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => {...@@ -607,7 +608,7 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => {
607 Layer.effect(608 Layer.effect(
608 CursorAdapter,609 CursorAdapter,
609 Effect.gen(function* () {610 Effect.gen(function* () {
610 const cursorConfig = Schema.decodeSync(CursorSettings)({});611 const cursorConfig = decodeCursorSettings({});
611 const resolveSettings = yield* makeResolveCursorSettings;612 const resolveSettings = yield* makeResolveCursorSettings;
612 return yield* makeCursorAdapter(cursorConfig, { resolveSettings });613 return yield* makeCursorAdapter(cursorConfig, { resolveSettings });
613 }),614 }),
...@@ -1239,7 +1240,7 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => {...@@ -1239,7 +1240,7 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => {
1239 const customAdapterLayer = Layer.effect(1240 const customAdapterLayer = Layer.effect(
1240 CursorAdapter,1241 CursorAdapter,
1241 Effect.gen(function* () {1242 Effect.gen(function* () {
1242 const cursorConfig = Schema.decodeSync(CursorSettings)({});1243 const cursorConfig = decodeCursorSettings({});
1243 const resolveSettings = yield* makeResolveCursorSettings;1244 const resolveSettings = yield* makeResolveCursorSettings;
1244 return yield* makeCursorAdapter(cursorConfig, {1245 return yield* makeCursorAdapter(cursorConfig, {
1245 instanceId: customInstanceId,1246 instanceId: customInstanceId,
apps/server/src/provider/Layers/CursorAdapter.ts+9-2
...@@ -75,6 +75,7 @@ import {...@@ -75,6 +75,7 @@ import {
75import { type CursorAdapterShape } from "../Services/CursorAdapter.ts";75import { type CursorAdapterShape } from "../Services/CursorAdapter.ts";
76import { resolveCursorAcpBaseModelId } from "./CursorProvider.ts";76import { resolveCursorAcpBaseModelId } from "./CursorProvider.ts";
77import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";77import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
78const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString);
7879
79const PROVIDER = ProviderDriverKind.make("cursor");80const PROVIDER = ProviderDriverKind.make("cursor");
80const CURSOR_RESUME_VERSION = 1 as const;81const CURSOR_RESUME_VERSION = 1 as const;
...@@ -82,6 +83,11 @@ const ACP_PLAN_MODE_ALIASES = ["plan", "architect"];...@@ -82,6 +83,11 @@ const ACP_PLAN_MODE_ALIASES = ["plan", "architect"];
82const ACP_IMPLEMENT_MODE_ALIASES = ["code", "agent", "default", "chat", "implement"];83const ACP_IMPLEMENT_MODE_ALIASES = ["code", "agent", "default", "chat", "implement"];
83const ACP_APPROVAL_MODE_ALIASES = ["ask"];84const ACP_APPROVAL_MODE_ALIASES = ["ask"];
8485
86function encodeJsonStringForDiagnostics(input: unknown): string | undefined {
87 const result = encodeUnknownJsonStringExit(input);
88 return Exit.isSuccess(result) ? result.value : undefined;
89}
90
85export interface CursorAdapterLiveOptions {91export interface CursorAdapterLiveOptions {
86 readonly environment?: NodeJS.ProcessEnv;92 readonly environment?: NodeJS.ProcessEnv;
87 readonly nativeEventLogPath?: string;93 readonly nativeEventLogPath?: string;
...@@ -390,7 +396,7 @@ export function makeCursorAdapter(...@@ -390,7 +396,7 @@ export function makeCursorAdapter(
390 method: string,396 method: string,
391 ) =>397 ) =>
392 Effect.gen(function* () {398 Effect.gen(function* () {
393 const fingerprint = `${ctx.activeTurnId ?? "no-turn"}:${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(payload)}`;399 const fingerprint = `${ctx.activeTurnId ?? "no-turn"}:${encodeJsonStringForDiagnostics(payload) ?? "[unserializable payload]"}`;
394 if (ctx.lastPlanFingerprint === fingerprint) {400 if (ctx.lastPlanFingerprint === fingerprint) {
395 return;401 return;
396 }402 }
...@@ -640,7 +646,8 @@ export function makeCursorAdapter(...@@ -640,7 +646,8 @@ export function makeCursorAdapter(
640 permissionRequest,646 permissionRequest,
641 detail:647 detail:
642 permissionRequest.detail ??648 permissionRequest.detail ??
643 Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(params).slice(0, 2000),649 encodeJsonStringForDiagnostics(params)?.slice(0, 2000) ??
650 "[unserializable params]",
644 args: params,651 args: params,
645 source: "acp.jsonrpc",652 source: "acp.jsonrpc",
646 method: "session/request_permission",653 method: "session/request_permission",
apps/server/src/provider/Layers/OpenCodeProvider.test.ts+2-1
...@@ -16,6 +16,7 @@ import {...@@ -16,6 +16,7 @@ import {
16} from "../opencodeRuntime.ts";16} from "../opencodeRuntime.ts";
17import { checkOpenCodeProviderStatus } from "./OpenCodeProvider.ts";17import { checkOpenCodeProviderStatus } from "./OpenCodeProvider.ts";
18import type { OpenCodeInventory } from "../opencodeRuntime.ts";18import type { OpenCodeInventory } from "../opencodeRuntime.ts";
19const decodeOpenCodeSettings = Schema.decodeSync(OpenCodeSettings);
1920
20const DEFAULT_VERSION_STDOUT = "opencode 1.14.19\n";21const DEFAULT_VERSION_STDOUT = "opencode 1.14.19\n";
2122
...@@ -106,7 +107,7 @@ const testLayer = Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble).pipe...@@ -106,7 +107,7 @@ const testLayer = Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble).pipe
106);107);
107108
108const makeOpenCodeSettings = (overrides?: Partial<OpenCodeSettings>): OpenCodeSettings =>109const makeOpenCodeSettings = (overrides?: Partial<OpenCodeSettings>): OpenCodeSettings =>
109 Schema.decodeSync(OpenCodeSettings)({110 decodeOpenCodeSettings({
110 enabled: true,111 enabled: true,
111 binaryPath: "opencode",112 binaryPath: "opencode",
112 serverUrl: "",113 serverUrl: "",
apps/server/src/provider/Layers/ProviderRegistry.test.ts+6-5
...@@ -47,6 +47,7 @@ import type { ProviderInstance } from "../ProviderDriver.ts";...@@ -47,6 +47,7 @@ import type { ProviderInstance } from "../ProviderDriver.ts";
47import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";47import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";
48import { ProviderRegistry } from "../Services/ProviderRegistry.ts";48import { ProviderRegistry } from "../Services/ProviderRegistry.ts";
49import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";49import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
50const decodeServerSettings = Schema.decodeSync(ServerSettings);
5051
51const defaultClaudeSettings: ClaudeSettings = Schema.decodeSync(ClaudeSettings)({});52const defaultClaudeSettings: ClaudeSettings = Schema.decodeSync(ClaudeSettings)({});
52const defaultCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({});53const defaultCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({});
...@@ -255,7 +256,7 @@ function makeMutableServerSettingsService(...@@ -255,7 +256,7 @@ function makeMutableServerSettingsService(
255 updateSettings: (patch) =>256 updateSettings: (patch) =>
256 Effect.gen(function* () {257 Effect.gen(function* () {
257 const current = yield* Ref.get(settingsRef);258 const current = yield* Ref.get(settingsRef);
258 const next = Schema.decodeSync(ServerSettings)(deepMerge(current, patch));259 const next = decodeServerSettings(deepMerge(current, patch));
259 yield* Ref.set(settingsRef, next);260 yield* Ref.set(settingsRef, next);
260 yield* PubSub.publish(changes, next);261 yield* PubSub.publish(changes, next);
261 return next;262 return next;
...@@ -928,7 +929,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -928,7 +929,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
928 Effect.gen(function* () {929 Effect.gen(function* () {
929 const missingBinary = `t3code_codex_missing_`;930 const missingBinary = `t3code_codex_missing_`;
930 const serverSettings = yield* makeMutableServerSettingsService(931 const serverSettings = yield* makeMutableServerSettingsService(
931 Schema.decodeSync(ServerSettings)(932 decodeServerSettings(
932 deepMerge(DEFAULT_SERVER_SETTINGS, {933 deepMerge(DEFAULT_SERVER_SETTINGS, {
933 providers: {934 providers: {
934 // Disable every built-in probe that would otherwise spawn935 // Disable every built-in probe that would otherwise spawn
...@@ -1027,7 +1028,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1027,7 +1028,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1027 const firstMissing = `t3code_codex_first_`;1028 const firstMissing = `t3code_codex_first_`;
1028 const secondMissing = `t3code_codex_second_`;1029 const secondMissing = `t3code_codex_second_`;
1029 const serverSettings = yield* makeMutableServerSettingsService(1030 const serverSettings = yield* makeMutableServerSettingsService(
1030 Schema.decodeSync(ServerSettings)(1031 decodeServerSettings(
1031 deepMerge(DEFAULT_SERVER_SETTINGS, {1032 deepMerge(DEFAULT_SERVER_SETTINGS, {
1032 providers: {1033 providers: {
1033 codex: { enabled: true, binaryPath: firstMissing },1034 codex: { enabled: true, binaryPath: firstMissing },
...@@ -1122,7 +1123,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1122,7 +1123,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1122 it.effect("includes unavailable instance snapshots in getProviders", () =>1123 it.effect("includes unavailable instance snapshots in getProviders", () =>
1123 Effect.gen(function* () {1124 Effect.gen(function* () {
1124 const serverSettings = yield* makeMutableServerSettingsService(1125 const serverSettings = yield* makeMutableServerSettingsService(
1125 Schema.decodeSync(ServerSettings)(1126 decodeServerSettings(
1126 deepMerge(DEFAULT_SERVER_SETTINGS, {1127 deepMerge(DEFAULT_SERVER_SETTINGS, {
1127 providers: {1128 providers: {
1128 codex: { enabled: false },1129 codex: { enabled: false },
...@@ -1178,7 +1179,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1178,7 +1179,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1178 () =>1179 () =>
1179 Effect.gen(function* () {1180 Effect.gen(function* () {
1180 const serverSettings = yield* makeMutableServerSettingsService(1181 const serverSettings = yield* makeMutableServerSettingsService(
1181 Schema.decodeSync(ServerSettings)(1182 decodeServerSettings(
1182 deepMerge(DEFAULT_SERVER_SETTINGS, {1183 deepMerge(DEFAULT_SERVER_SETTINGS, {
1183 providers: {1184 providers: {
1184 codex: {1185 codex: {
apps/server/src/provider/Layers/ProviderService.ts+6-3
...@@ -56,6 +56,7 @@ import {...@@ -56,6 +56,7 @@ import {
56import { type EventNdjsonLogger } from "./EventNdjsonLogger.ts";56import { type EventNdjsonLogger } from "./EventNdjsonLogger.ts";
57import { ProviderEventLoggers } from "./ProviderEventLoggers.ts";57import { ProviderEventLoggers } from "./ProviderEventLoggers.ts";
58import { AnalyticsService } from "../../telemetry/Services/AnalyticsService.ts";58import { AnalyticsService } from "../../telemetry/Services/AnalyticsService.ts";
59const isModelSelection = Schema.is(ModelSelection);
5960
60/**61/**
61 * Hook for tests that want to override the canonical event logger pulled62 * Hook for tests that want to override the canonical event logger pulled
...@@ -87,8 +88,9 @@ const decodeInputOrValidationError = <S extends Schema.Top>(input: {...@@ -87,8 +88,9 @@ const decodeInputOrValidationError = <S extends Schema.Top>(input: {
87 readonly operation: string;88 readonly operation: string;
88 readonly schema: S;89 readonly schema: S;
89 readonly payload: unknown;90 readonly payload: unknown;
90}) =>91}) => {
91 Schema.decodeUnknownEffect(input.schema)(input.payload).pipe(92 const decodeProviderRequestInput = Schema.decodeUnknownEffect(input.schema);
93 return decodeProviderRequestInput(input.payload).pipe(
92 Effect.mapError(94 Effect.mapError(
93 (schemaError) =>95 (schemaError) =>
94 new ProviderValidationError({96 new ProviderValidationError({
...@@ -98,6 +100,7 @@ const decodeInputOrValidationError = <S extends Schema.Top>(input: {...@@ -98,6 +100,7 @@ const decodeInputOrValidationError = <S extends Schema.Top>(input: {
98 }),100 }),
99 ),101 ),
100 );102 );
103};
101104
102function toRuntimeStatus(session: ProviderSession): "starting" | "running" | "stopped" | "error" {105function toRuntimeStatus(session: ProviderSession): "starting" | "running" | "stopped" | "error" {
103 switch (session.status) {106 switch (session.status) {
...@@ -142,7 +145,7 @@ function readPersistedModelSelection(...@@ -142,7 +145,7 @@ function readPersistedModelSelection(
142 return undefined;145 return undefined;
143 }146 }
144 const raw = "modelSelection" in runtimePayload ? runtimePayload.modelSelection : undefined;147 const raw = "modelSelection" in runtimePayload ? runtimePayload.modelSelection : undefined;
145 return Schema.is(ModelSelection)(raw) ? raw : undefined;148 return isModelSelection(raw) ? raw : undefined;
146}149}
147150
148function readPersistedCwd(151function readPersistedCwd(
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+2-1
...@@ -14,6 +14,7 @@ import {...@@ -14,6 +14,7 @@ import {
14 type ProviderRuntimeBindingWithMetadata,14 type ProviderRuntimeBindingWithMetadata,
15 type ProviderSessionDirectoryShape,15 type ProviderSessionDirectoryShape,
16} from "../Services/ProviderSessionDirectory.ts";16} from "../Services/ProviderSessionDirectory.ts";
17const decodeProviderDriverKindValue = Schema.decodeUnknownEffect(ProviderDriverKind);
1718
18function toPersistenceError(operation: string) {19function toPersistenceError(operation: string) {
19 return (cause: unknown) =>20 return (cause: unknown) =>
...@@ -28,7 +29,7 @@ function decodeProviderDriverKind(...@@ -28,7 +29,7 @@ function decodeProviderDriverKind(
28 providerName: string,29 providerName: string,
29 operation: string,30 operation: string,
30): Effect.Effect<ProviderDriverKind, ProviderSessionDirectoryPersistenceError> {31): Effect.Effect<ProviderDriverKind, ProviderSessionDirectoryPersistenceError> {
31 return Schema.decodeUnknownEffect(ProviderDriverKind)(providerName).pipe(32 return decodeProviderDriverKindValue(providerName).pipe(
32 Effect.mapError(33 Effect.mapError(
33 (cause) =>34 (cause) =>
34 new ProviderSessionDirectoryPersistenceError({35 new ProviderSessionDirectoryPersistenceError({
apps/server/src/provider/acp/AcpAdapterSupport.ts+4-2
...@@ -11,6 +11,8 @@ import {...@@ -11,6 +11,8 @@ import {
11 ProviderAdapterSessionClosedError,11 ProviderAdapterSessionClosedError,
12 type ProviderAdapterError,12 type ProviderAdapterError,
13} from "../Errors.ts";13} from "../Errors.ts";
14const isAcpProcessExitedError = Schema.is(EffectAcpErrors.AcpProcessExitedError);
15const isAcpRequestError = Schema.is(EffectAcpErrors.AcpRequestError);
1416
15export function mapAcpToAdapterError(17export function mapAcpToAdapterError(
16 provider: ProviderDriverKind,18 provider: ProviderDriverKind,
...@@ -18,14 +20,14 @@ export function mapAcpToAdapterError(...@@ -18,14 +20,14 @@ export function mapAcpToAdapterError(
18 method: string,20 method: string,
19 error: EffectAcpErrors.AcpError,21 error: EffectAcpErrors.AcpError,
20): ProviderAdapterError {22): ProviderAdapterError {
21 if (Schema.is(EffectAcpErrors.AcpProcessExitedError)(error)) {23 if (isAcpProcessExitedError(error)) {
22 return new ProviderAdapterSessionClosedError({24 return new ProviderAdapterSessionClosedError({
23 provider,25 provider,
24 threadId,26 threadId,
25 cause: error,27 cause: error,
26 });28 });
27 }29 }
28 if (Schema.is(EffectAcpErrors.AcpRequestError)(error)) {30 if (isAcpRequestError(error)) {
29 return new ProviderAdapterRequestError({31 return new ProviderAdapterRequestError({
30 provider,32 provider,
31 method,33 method,
apps/server/src/provider/acp/AcpSessionRuntime.ts+7-4
...@@ -7,7 +7,6 @@ import * as Queue from "effect/Queue";...@@ -7,7 +7,6 @@ import * as Queue from "effect/Queue";
7import * as Ref from "effect/Ref";7import * as Ref from "effect/Ref";
8import * as Scope from "effect/Scope";8import * as Scope from "effect/Scope";
9import * as Context from "effect/Context";9import * as Context from "effect/Context";
10import * as Schema from "effect/Schema";
11import * as Stream from "effect/Stream";10import * as Stream from "effect/Stream";
12import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";11import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
13import * as EffectAcpClient from "effect-acp/client";12import * as EffectAcpClient from "effect-acp/client";
...@@ -27,6 +26,10 @@ import {...@@ -27,6 +26,10 @@ import {
27 type AcpToolCallState,26 type AcpToolCallState,
28} from "./AcpRuntimeModel.ts";27} from "./AcpRuntimeModel.ts";
2928
29function formatConfigOptionValue(value: string | boolean): string {
30 return JSON.stringify(value);
31}
32
30export interface AcpSpawnInput {33export interface AcpSpawnInput {
31 readonly command: string;34 readonly command: string;
32 readonly args: ReadonlyArray<string>;35 readonly args: ReadonlyArray<string>;
...@@ -277,7 +280,7 @@ const makeAcpSessionRuntime = (...@@ -277,7 +280,7 @@ const makeAcpSessionRuntime = (
277 }280 }
278 return yield* new EffectAcpErrors.AcpRequestError({281 return yield* new EffectAcpErrors.AcpRequestError({
279 code: -32602,282 code: -32602,
280 errorMessage: `Invalid value ${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)} for session config option "${configOption.id}": expected boolean`,283 errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected boolean`,
281 data: {284 data: {
282 configId: configOption.id,285 configId: configOption.id,
283 expectedType: "boolean",286 expectedType: "boolean",
...@@ -288,7 +291,7 @@ const makeAcpSessionRuntime = (...@@ -288,7 +291,7 @@ const makeAcpSessionRuntime = (
288 if (typeof value !== "string") {291 if (typeof value !== "string") {
289 return yield* new EffectAcpErrors.AcpRequestError({292 return yield* new EffectAcpErrors.AcpRequestError({
290 code: -32602,293 code: -32602,
291 errorMessage: `Invalid value ${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)} for session config option "${configOption.id}": expected string`,294 errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected string`,
292 data: {295 data: {
293 configId: configOption.id,296 configId: configOption.id,
294 expectedType: "string",297 expectedType: "string",
...@@ -302,7 +305,7 @@ const makeAcpSessionRuntime = (...@@ -302,7 +305,7 @@ const makeAcpSessionRuntime = (
302 }305 }
303 return yield* new EffectAcpErrors.AcpRequestError({306 return yield* new EffectAcpErrors.AcpRequestError({
304 code: -32602,307 code: -32602,
305 errorMessage: `Invalid value ${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)} for session config option "${configOption.id}": expected one of ${allowedValues.join(", ")}`,308 errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected one of ${allowedValues.join(", ")}`,
306 data: {309 data: {
307 configId: configOption.id,310 configId: configOption.id,
308 allowedValues,311 allowedValues,
apps/server/src/provider/opencodeRuntime.ts+11-5
...@@ -31,6 +31,8 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";...@@ -31,6 +31,8 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
31import { isWindowsCommandNotFound } from "../processRunner.ts";31import { isWindowsCommandNotFound } from "../processRunner.ts";
32import { collectStreamAsString } from "./providerSnapshot.ts";32import { collectStreamAsString } from "./providerSnapshot.ts";
33import * as NetService from "@t3tools/shared/Net";33import * as NetService from "@t3tools/shared/Net";
34const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString);
35const OPENCODE_EMPTY_CONFIG_CONTENT = "{}";
3436
35const OPENCODE_SERVER_READY_PREFIX = "opencode server listening";37const OPENCODE_SERVER_READY_PREFIX = "opencode server listening";
36const DEFAULT_OPENCODE_SERVER_TIMEOUT_MS = 5_000;38const DEFAULT_OPENCODE_SERVER_TIMEOUT_MS = 5_000;
...@@ -56,6 +58,11 @@ export class OpenCodeRuntimeError extends Data.TaggedError(OPENCODE_RUNTIME_ERRO...@@ -56,6 +58,11 @@ export class OpenCodeRuntimeError extends Data.TaggedError(OPENCODE_RUNTIME_ERRO
56 P.isTagged(u, OPENCODE_RUNTIME_ERROR_TAG);58 P.isTagged(u, OPENCODE_RUNTIME_ERROR_TAG);
57}59}
5860
61function encodeJsonStringForDiagnostics(input: unknown): string | undefined {
62 const result = encodeUnknownJsonStringExit(input);
63 return Exit.isSuccess(result) ? result.value : undefined;
64}
65
59export function openCodeRuntimeErrorDetail(cause: unknown): string {66export function openCodeRuntimeErrorDetail(cause: unknown): string {
60 if (OpenCodeRuntimeError.is(cause)) return cause.detail;67 if (OpenCodeRuntimeError.is(cause)) return cause.detail;
61 if (cause instanceof Error && cause.message.trim().length > 0) return cause.message.trim();68 if (cause instanceof Error && cause.message.trim().length > 0) return cause.message.trim();
...@@ -64,10 +71,9 @@ export function openCodeRuntimeErrorDetail(cause: unknown): string {...@@ -64,10 +71,9 @@ export function openCodeRuntimeErrorDetail(cause: unknown): string {
64 const anyCause = cause as Record<string, unknown>;71 const anyCause = cause as Record<string, unknown>;
65 const status = (anyCause.response as { status?: number } | undefined)?.status;72 const status = (anyCause.response as { status?: number } | undefined)?.status;
66 const body = anyCause.error ?? anyCause.data ?? anyCause.body;73 const body = anyCause.error ?? anyCause.data ?? anyCause.body;
67 try {74 const encodedBody = encodeJsonStringForDiagnostics(body ?? cause);
68 return `status=${status ?? "?"} body=${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(body ?? cause)}`;75 if (encodedBody) {
69 } catch {76 return `status=${status ?? "?"} body=${encodedBody}`;
70 /* fall through */
71 }77 }
72 }78 }
73 return String(cause);79 return String(cause);
...@@ -336,7 +342,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () {...@@ -336,7 +342,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
336 shell: process.platform === "win32",342 shell: process.platform === "win32",
337 env: {343 env: {
338 ...(input.environment ?? process.env),344 ...(input.environment ?? process.env),
339 OPENCODE_CONFIG_CONTENT: Schema.encodeUnknownSync(Schema.UnknownFromJsonString)({}),345 OPENCODE_CONFIG_CONTENT: OPENCODE_EMPTY_CONFIG_CONTENT,
340 },346 },
341 }),347 }),
342 )348 )
apps/server/src/provider/providerMaintenanceRunner.test.ts+3-2
...@@ -25,6 +25,7 @@ import {...@@ -25,6 +25,7 @@ import {
25 makeProviderMaintenanceCapabilities,25 makeProviderMaintenanceCapabilities,
26 type ProviderMaintenanceCapabilities,26 type ProviderMaintenanceCapabilities,
27} from "./providerMaintenance.ts";27} from "./providerMaintenance.ts";
28const isServerProviderUpdateError = Schema.is(ServerProviderUpdateError);
2829
29const CODEX_DRIVER = ProviderDriverKind.make("codex");30const CODEX_DRIVER = ProviderDriverKind.make("codex");
30const CURSOR_DRIVER = ProviderDriverKind.make("cursor");31const CURSOR_DRIVER = ProviderDriverKind.make("cursor");
...@@ -455,8 +456,8 @@ describe("providerMaintenanceRunner", () => {...@@ -455,8 +456,8 @@ describe("providerMaintenanceRunner", () => {
455 assert.strictEqual(Exit.isFailure(second), true);456 assert.strictEqual(Exit.isFailure(second), true);
456 if (Exit.isFailure(second)) {457 if (Exit.isFailure(second)) {
457 const error = Cause.squash(second.cause);458 const error = Cause.squash(second.cause);
458 assert.strictEqual(Schema.is(ServerProviderUpdateError)(error), true);459 assert.strictEqual(isServerProviderUpdateError(error), true);
459 if (Schema.is(ServerProviderUpdateError)(error)) {460 if (isServerProviderUpdateError(error)) {
460 assert.include(error.reason, "already running");461 assert.include(error.reason, "already running");
461 }462 }
462 }463 }
apps/server/src/provider/providerMaintenanceRunner.ts+2-1
...@@ -25,6 +25,7 @@ import { makeProviderMaintenanceCommandCoordinator } from "./providerMaintenance...@@ -25,6 +25,7 @@ import { makeProviderMaintenanceCommandCoordinator } from "./providerMaintenance
25import { enrichProviderSnapshotWithVersionAdvisory } from "./providerMaintenance.ts";25import { enrichProviderSnapshotWithVersionAdvisory } from "./providerMaintenance.ts";
26import type { ProviderMaintenanceCapabilities } from "./providerMaintenance.ts";26import type { ProviderMaintenanceCapabilities } from "./providerMaintenance.ts";
27import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts";27import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts";
28const isServerProviderUpdateError = Schema.is(ServerProviderUpdateError);
2829
29const UPDATE_TIMEOUT_MS = 5 * 60_000;30const UPDATE_TIMEOUT_MS = 5 * 60_000;
30const UPDATE_OUTPUT_MAX_BYTES = 10_000;31const UPDATE_OUTPUT_MAX_BYTES = 10_000;
...@@ -397,7 +398,7 @@ export const make = Effect.fn("ProviderMaintenanceRunner.make")(function* () {...@@ -397,7 +398,7 @@ export const make = Effect.fn("ProviderMaintenanceRunner.make")(function* () {
397 })398 })
398 .pipe(399 .pipe(
399 Effect.mapError((error) =>400 Effect.mapError((error) =>
400 Schema.is(ServerProviderUpdateError)(error)401 isServerProviderUpdateError(error)
401 ? new ServerProviderUpdateError({402 ? new ServerProviderUpdateError({
402 provider,403 provider,
403 reason: error.reason,404 reason: error.reason,
apps/server/src/serverSettings.ts+5-5
...@@ -49,6 +49,7 @@ import { fromLenientJson } from "@t3tools/shared/schemaJson";...@@ -49,6 +49,7 @@ import { fromLenientJson } from "@t3tools/shared/schemaJson";
49import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";49import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
50import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore.ts";50import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore.ts";
51import { ServerSecretStore } from "./auth/Services/ServerSecretStore.ts";51import { ServerSecretStore } from "./auth/Services/ServerSecretStore.ts";
52const decodeServerSettings = Schema.decodeEffect(ServerSettings);
5253
53const textEncoder = new TextEncoder();54const textEncoder = new TextEncoder();
54const textDecoder = new TextDecoder();55const textDecoder = new TextDecoder();
...@@ -127,9 +128,7 @@ export class ServerSettingsService extends Context.Service<...@@ -127,9 +128,7 @@ export class ServerSettingsService extends Context.Service<
127 updateSettings: (patch) =>128 updateSettings: (patch) =>
128 Ref.get(currentSettingsRef).pipe(129 Ref.get(currentSettingsRef).pipe(
129 Effect.flatMap((currentSettings) =>130 Effect.flatMap((currentSettings) =>
130 Schema.decodeEffect(ServerSettings)(131 decodeServerSettings(applyServerSettingsPatch(currentSettings, patch)).pipe(
131 applyServerSettingsPatch(currentSettings, patch),
132 ).pipe(
133 Effect.mapError(132 Effect.mapError(
134 (cause) =>133 (cause) =>
135 new ServerSettingsError({134 new ServerSettingsError({
...@@ -149,6 +148,7 @@ export class ServerSettingsService extends Context.Service<...@@ -149,6 +148,7 @@ export class ServerSettingsService extends Context.Service<
149}148}
150149
151const ServerSettingsJson = fromLenientJson(ServerSettings);150const ServerSettingsJson = fromLenientJson(ServerSettings);
151const decodeServerSettingsJsonExit = Schema.decodeUnknownExit(ServerSettingsJson);
152152
153type LegacyProviderSettings = ServerSettings["providers"][keyof ServerSettings["providers"]];153type LegacyProviderSettings = ServerSettings["providers"][keyof ServerSettings["providers"]];
154154
...@@ -280,7 +280,7 @@ const makeServerSettings = Effect.gen(function* () {...@@ -280,7 +280,7 @@ const makeServerSettings = Effect.gen(function* () {
280 }280 }
281281
282 const raw = yield* readRawConfig;282 const raw = yield* readRawConfig;
283 const decoded = Schema.decodeUnknownExit(ServerSettingsJson)(raw);283 const decoded = decodeServerSettingsJsonExit(raw);
284 if (decoded._tag === "Failure") {284 if (decoded._tag === "Failure") {
285 yield* Effect.logWarning("failed to parse settings.json, using defaults", {285 yield* Effect.logWarning("failed to parse settings.json, using defaults", {
286 path: settingsPath,286 path: settingsPath,
...@@ -533,7 +533,7 @@ const makeServerSettings = Effect.gen(function* () {...@@ -533,7 +533,7 @@ const makeServerSettings = Effect.gen(function* () {
533 current,533 current,
534 applyServerSettingsPatch(current, patch),534 applyServerSettingsPatch(current, patch),
535 );535 );
536 const next = yield* Schema.decodeEffect(ServerSettings)(nextPersisted).pipe(536 const next = yield* decodeServerSettings(nextPersisted).pipe(
537 Effect.mapError(537 Effect.mapError(
538 (cause) =>538 (cause) =>
539 new ServerSettingsError({539 new ServerSettingsError({
apps/server/src/sourceControl/BitbucketApi.ts+2-1
...@@ -44,6 +44,7 @@ export class BitbucketApiError extends Schema.TaggedErrorClass<BitbucketApiError...@@ -44,6 +44,7 @@ export class BitbucketApiError extends Schema.TaggedErrorClass<BitbucketApiError
44 return `Bitbucket API failed in ${this.operation}: ${this.detail}`;44 return `Bitbucket API failed in ${this.operation}: ${this.detail}`;
45 }45 }
46}46}
47const isBitbucketApiErrorValue = Schema.is(BitbucketApiError);
4748
48const RawBitbucketRepositorySchema = Schema.Struct({49const RawBitbucketRepositorySchema = Schema.Struct({
49 full_name: TrimmedNonEmptyString,50 full_name: TrimmedNonEmptyString,
...@@ -350,7 +351,7 @@ function requestError(operation: string, cause: unknown): BitbucketApiError {...@@ -350,7 +351,7 @@ function requestError(operation: string, cause: unknown): BitbucketApiError {
350}351}
351352
352function isBitbucketApiError(cause: unknown): cause is BitbucketApiError {353function isBitbucketApiError(cause: unknown): cause is BitbucketApiError {
353 return Schema.is(BitbucketApiError)(cause);354 return isBitbucketApiErrorValue(cause);
354}355}
355356
356function responseError(357function responseError(
apps/server/src/sourceControl/GitHubSourceControlProvider.ts+2-1
...@@ -13,6 +13,7 @@ import * as GitHubCli from "./GitHubCli.ts";...@@ -13,6 +13,7 @@ import * as GitHubCli from "./GitHubCli.ts";
13import * as GitHubPullRequests from "./gitHubPullRequests.ts";13import * as GitHubPullRequests from "./gitHubPullRequests.ts";
14import * as SourceControlProvider from "./SourceControlProvider.ts";14import * as SourceControlProvider from "./SourceControlProvider.ts";
15import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts";15import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts";
16const isSourceControlProviderError = Schema.is(SourceControlProviderError);
1617
17function providerError(18function providerError(
18 operation: string,19 operation: string,
...@@ -153,7 +154,7 @@ export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () {...@@ -153,7 +154,7 @@ export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () {
153 );154 );
154 }),155 }),
155 Effect.mapError((error) =>156 Effect.mapError((error) =>
156 Schema.is(SourceControlProviderError)(error)157 isSourceControlProviderError(error)
157 ? error158 ? error
158 : providerError("listChangeRequests", error),159 : providerError("listChangeRequests", error),
159 ),160 ),
apps/server/src/sourceControl/SourceControlRepositoryService.ts+2-1
...@@ -22,6 +22,7 @@ import {...@@ -22,6 +22,7 @@ import {
22import { ServerConfig } from "../config.ts";22import { ServerConfig } from "../config.ts";
23import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";23import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
24import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";24import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";
25const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError);
2526
26export interface SourceControlRepositoryServiceShape {27export interface SourceControlRepositoryServiceShape {
27 readonly lookupRepository: (28 readonly lookupRepository: (
...@@ -69,7 +70,7 @@ function repositoryError(input: {...@@ -69,7 +70,7 @@ function repositoryError(input: {
6970
70function mapRepositoryError(operation: string, provider: SourceControlProviderKind) {71function mapRepositoryError(operation: string, provider: SourceControlProviderKind) {
71 return Effect.mapError((cause: unknown) =>72 return Effect.mapError((cause: unknown) =>
72 Schema.is(SourceControlRepositoryError)(cause)73 isSourceControlRepositoryError(cause)
73 ? cause74 ? cause
74 : repositoryError({75 : repositoryError({
75 operation,76 operation,
apps/server/src/textGeneration/ClaudeTextGeneration.test.ts+2-1
...@@ -13,6 +13,7 @@ import { ServerConfig } from "../config.ts";...@@ -13,6 +13,7 @@ import { ServerConfig } from "../config.ts";
13import { type TextGenerationShape } from "./TextGeneration.ts";13import { type TextGenerationShape } from "./TextGeneration.ts";
14import { sanitizeThreadTitle } from "./TextGenerationUtils.ts";14import { sanitizeThreadTitle } from "./TextGenerationUtils.ts";
15import { makeClaudeTextGeneration } from "./ClaudeTextGeneration.ts";15import { makeClaudeTextGeneration } from "./ClaudeTextGeneration.ts";
16const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
1617
17const ClaudeTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), {18const ClaudeTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), {
18 prefix: "t3code-claude-text-generation-test-",19 prefix: "t3code-claude-text-generation-test-",
...@@ -182,7 +183,7 @@ function withFakeClaudeEnv<A, E, R>(...@@ -182,7 +183,7 @@ function withFakeClaudeEnv<A, E, R>(
182 }),183 }),
183 );184 );
184185
185 const config = Schema.decodeSync(ClaudeSettings)(input.claudeConfig ?? {});186 const config = decodeClaudeSettings(input.claudeConfig ?? {});
186 const textGeneration = yield* makeClaudeTextGeneration(config);187 const textGeneration = yield* makeClaudeTextGeneration(config);
187 return yield* effectFn(textGeneration);188 return yield* effectFn(textGeneration);
188 }).pipe(Effect.scoped);189 }).pipe(Effect.scoped);
apps/server/src/textGeneration/ClaudeTextGeneration.ts+38-8
...@@ -53,6 +53,9 @@ const ClaudeOutputEnvelope = Schema.Struct({...@@ -53,6 +53,9 @@ const ClaudeOutputEnvelope = Schema.Struct({
53 structured_output: Schema.Unknown,53 structured_output: Schema.Unknown,
54});54});
5555
56const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString);
57const decodeClaudeOutputEnvelope = Schema.decodeEffect(Schema.fromJsonString(ClaudeOutputEnvelope));
58
56export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(function* (59export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(function* (
57 claudeSettings: ClaudeSettings,60 claudeSettings: ClaudeSettings,
58 environment: NodeJS.ProcessEnv = process.env,61 environment: NodeJS.ProcessEnv = process.env,
...@@ -75,6 +78,26 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu...@@ -75,6 +78,26 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
75 ),78 ),
76 );79 );
7780
81 const encodeJsonForOperation = (
82 operation:
83 | "generateCommitMessage"
84 | "generatePrContent"
85 | "generateBranchName"
86 | "generateThreadTitle",
87 value: unknown,
88 detail: string,
89 ): Effect.Effect<string, TextGenerationError> =>
90 encodeJsonString(value).pipe(
91 Effect.mapError(
92 (cause) =>
93 new TextGenerationError({
94 operation,
95 detail,
96 cause,
97 }),
98 ),
99 );
100
78 /**101 /**
79 * Spawn the Claude CLI with structured JSON output and return the parsed,102 * Spawn the Claude CLI with structured JSON output and return the parsed,
80 * schema-validated result.103 * schema-validated result.
...@@ -96,8 +119,10 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu...@@ -96,8 +119,10 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
96 outputSchemaJson: S;119 outputSchemaJson: S;
97 modelSelection: ModelSelection;120 modelSelection: ModelSelection;
98 }): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> {121 }): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> {
99 const jsonSchemaStr = Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(122 const jsonSchemaStr = yield* encodeJsonForOperation(
123 operation,
100 toJsonSchemaObject(outputSchemaJson),124 toJsonSchemaObject(outputSchemaJson),
125 "Failed to encode structured output schema.",
101 );126 );
102 const caps = getClaudeModelCapabilities(modelSelection.model);127 const caps = getClaudeModelCapabilities(modelSelection.model);
103 const descriptors = getProviderOptionDescriptors({128 const descriptors = getProviderOptionDescriptors({
...@@ -118,6 +143,14 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu...@@ -118,6 +143,14 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
118 ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}),143 ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}),
119 ...(fastMode ? { fastMode: true } : {}),144 ...(fastMode ? { fastMode: true } : {}),
120 };145 };
146 const settingsJson =
147 Object.keys(settings).length > 0
148 ? yield* encodeJsonForOperation(
149 operation,
150 settings,
151 "Failed to encode Claude CLI settings.",
152 )
153 : undefined;
121154
122 const runClaudeCommand = Effect.fn("runClaudeJson.runClaudeCommand")(function* () {155 const runClaudeCommand = Effect.fn("runClaudeJson.runClaudeCommand")(function* () {
123 const command = ChildProcess.make(156 const command = ChildProcess.make(
...@@ -131,9 +164,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu...@@ -131,9 +164,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
131 "--model",164 "--model",
132 resolveClaudeApiModelId(modelSelection),165 resolveClaudeApiModelId(modelSelection),
133 ...(cliEffort ? ["--effort", cliEffort] : []),166 ...(cliEffort ? ["--effort", cliEffort] : []),
134 ...(Object.keys(settings).length > 0167 ...(settingsJson ? ["--settings", settingsJson] : []),
135 ? ["--settings", Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(settings)]
136 : []),
137 "--dangerously-skip-permissions",168 "--dangerously-skip-permissions",
138 ],169 ],
139 {170 {
...@@ -197,9 +228,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu...@@ -197,9 +228,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
197 ),228 ),
198 );229 );
199230
200 const envelope = yield* Schema.decodeEffect(Schema.fromJsonString(ClaudeOutputEnvelope))(231 const envelope = yield* decodeClaudeOutputEnvelope(rawStdout).pipe(
201 rawStdout,
202 ).pipe(
203 Effect.catchTag("SchemaError", (cause) =>232 Effect.catchTag("SchemaError", (cause) =>
204 Effect.fail(233 Effect.fail(
205 new TextGenerationError({234 new TextGenerationError({
...@@ -211,7 +240,8 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu...@@ -211,7 +240,8 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
211 ),240 ),
212 );241 );
213242
214 return yield* Schema.decodeEffect(outputSchemaJson)(envelope.structured_output).pipe(243 const decodeOutput = Schema.decodeEffect(outputSchemaJson);
244 return yield* decodeOutput(envelope.structured_output).pipe(
215 Effect.catchTag("SchemaError", (cause) =>245 Effect.catchTag("SchemaError", (cause) =>
216 Effect.fail(246 Effect.fail(
217 new TextGenerationError({247 new TextGenerationError({
apps/server/src/textGeneration/CodexTextGeneration.test.ts+2-1
...@@ -14,6 +14,7 @@ import { CodexSettings, ProviderInstanceId, TextGenerationError } from "@t3tools...@@ -14,6 +14,7 @@ import { CodexSettings, ProviderInstanceId, TextGenerationError } from "@t3tools
14import { ServerConfig } from "../config.ts";14import { ServerConfig } from "../config.ts";
15import { type TextGenerationShape } from "./TextGeneration.ts";15import { type TextGenerationShape } from "./TextGeneration.ts";
16import { makeCodexTextGeneration } from "./CodexTextGeneration.ts";16import { makeCodexTextGeneration } from "./CodexTextGeneration.ts";
17const decodeCodexSettings = Schema.decodeSync(CodexSettings);
1718
18const DEFAULT_TEST_MODEL_SELECTION = createModelSelection(19const DEFAULT_TEST_MODEL_SELECTION = createModelSelection(
19 ProviderInstanceId.make("codex"),20 ProviderInstanceId.make("codex"),
...@@ -172,7 +173,7 @@ function withFakeCodexEnv<A, E, R>(...@@ -172,7 +173,7 @@ function withFakeCodexEnv<A, E, R>(
172 const fs = yield* FileSystem.FileSystem;173 const fs = yield* FileSystem.FileSystem;
173 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-codex-text-" });174 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-codex-text-" });
174 const codexPath = yield* makeFakeCodexBinary(tempDir, input);175 const codexPath = yield* makeFakeCodexBinary(tempDir, input);
175 const config = Schema.decodeSync(CodexSettings)({ binaryPath: codexPath });176 const config = decodeCodexSettings({ binaryPath: codexPath });
176 const textGeneration = yield* makeCodexTextGeneration(config);177 const textGeneration = yield* makeCodexTextGeneration(config);
177 return yield* effectFn(textGeneration);178 return yield* effectFn(textGeneration);
178 }).pipe(Effect.scoped);179 }).pipe(Effect.scoped);
apps/server/src/textGeneration/CodexTextGeneration.ts+26-4
...@@ -40,6 +40,7 @@ import {...@@ -40,6 +40,7 @@ import {
4040
41const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low";41const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low";
42const CODEX_TIMEOUT_MS = 180_000;42const CODEX_TIMEOUT_MS = 180_000;
43const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString);
43/**44/**
44 * Build a Codex text-generation closure bound to a specific `CodexSettings`45 * Build a Codex text-generation closure bound to a specific `CodexSettings`
45 * payload. See `makeCodexAdapter` for the overall per-instance rationale.46 * payload. See `makeCodexAdapter` for the overall per-instance rationale.
...@@ -99,6 +100,25 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func...@@ -99,6 +100,25 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
99 const safeUnlink = (filePath: string): Effect.Effect<void, never> =>100 const safeUnlink = (filePath: string): Effect.Effect<void, never> =>
100 fileSystem.remove(filePath).pipe(Effect.catch(() => Effect.void));101 fileSystem.remove(filePath).pipe(Effect.catch(() => Effect.void));
101102
103 const encodeJsonForOperation = (
104 operation:
105 | "generateCommitMessage"
106 | "generatePrContent"
107 | "generateBranchName"
108 | "generateThreadTitle",
109 value: unknown,
110 ): Effect.Effect<string, TextGenerationError> =>
111 encodeJsonString(value).pipe(
112 Effect.mapError(
113 (cause) =>
114 new TextGenerationError({
115 operation,
116 detail: "Failed to encode structured output schema.",
117 cause,
118 }),
119 ),
120 );
121
102 const materializeImageAttachments = Effect.fn("materializeImageAttachments")(function* (122 const materializeImageAttachments = Effect.fn("materializeImageAttachments")(function* (
103 _operation:123 _operation:
104 | "generateCommitMessage"124 | "generateCommitMessage"
...@@ -156,11 +176,11 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func...@@ -156,11 +176,11 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
156 cleanupPaths?: ReadonlyArray<string>;176 cleanupPaths?: ReadonlyArray<string>;
157 modelSelection: ModelSelection;177 modelSelection: ModelSelection;
158 }): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> {178 }): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> {
159 const schemaPath = yield* writeTempFile(179 const schemaJson = yield* encodeJsonForOperation(
160 operation,180 operation,
161 "codex-schema",181 toJsonSchemaObject(outputSchemaJson),
162 Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(toJsonSchemaObject(outputSchemaJson)),
163 );182 );
183 const schemaPath = yield* writeTempFile(operation, "codex-schema", schemaJson);
164 const outputPath = yield* writeTempFile(operation, "codex-output", "");184 const outputPath = yield* writeTempFile(operation, "codex-output", "");
165185
166 const runCodexCommand = Effect.fn("runCodexJson.runCodexCommand")(function* () {186 const runCodexCommand = Effect.fn("runCodexJson.runCodexCommand")(function* () {
...@@ -259,6 +279,8 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func...@@ -259,6 +279,8 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
259 ),279 ),
260 );280 );
261281
282 const decodeOutput = Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson));
283
262 return yield* fileSystem.readFileString(outputPath).pipe(284 return yield* fileSystem.readFileString(outputPath).pipe(
263 Effect.mapError(285 Effect.mapError(
264 (cause) =>286 (cause) =>
...@@ -268,7 +290,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func...@@ -268,7 +290,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
268 cause,290 cause,
269 }),291 }),
270 ),292 ),
271 Effect.flatMap(Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson))),293 Effect.flatMap(decodeOutput),
272 Effect.catchTag("SchemaError", (cause) =>294 Effect.catchTag("SchemaError", (cause) =>
273 Effect.fail(295 Effect.fail(
274 new TextGenerationError({296 new TextGenerationError({
apps/server/src/textGeneration/CursorTextGeneration.test.ts+2-1
...@@ -19,6 +19,7 @@ import { CursorSettings, ProviderInstanceId } from "@t3tools/contracts";...@@ -19,6 +19,7 @@ import { CursorSettings, ProviderInstanceId } from "@t3tools/contracts";
19import { ServerConfig } from "../config.ts";19import { ServerConfig } from "../config.ts";
20import { type TextGenerationShape } from "./TextGeneration.ts";20import { type TextGenerationShape } from "./TextGeneration.ts";
21import { makeCursorTextGeneration } from "./CursorTextGeneration.ts";21import { makeCursorTextGeneration } from "./CursorTextGeneration.ts";
22const decodeCursorSettings = Schema.decodeSync(CursorSettings);
2223
23const __dirname = path.dirname(fileURLToPath(import.meta.url));24const __dirname = path.dirname(fileURLToPath(import.meta.url));
24const mockAgentPath = path.join(__dirname, "../../scripts/acp-mock-agent.ts");25const mockAgentPath = path.join(__dirname, "../../scripts/acp-mock-agent.ts");
...@@ -65,7 +66,7 @@ function withFakeAcpAgent<A, E, R>(...@@ -65,7 +66,7 @@ function withFakeAcpAgent<A, E, R>(
65 }),66 }),
66 );67 );
67 const agentPath = makeAcpAgentWrapper(tempDir, env);68 const agentPath = makeAcpAgentWrapper(tempDir, env);
68 const config = Schema.decodeSync(CursorSettings)({ binaryPath: agentPath });69 const config = decodeCursorSettings({ binaryPath: agentPath });
69 const textGeneration = yield* makeCursorTextGeneration(config);70 const textGeneration = yield* makeCursorTextGeneration(config);
70 return yield* effectFn(textGeneration);71 return yield* effectFn(textGeneration);
71 }).pipe(Effect.scoped);72 }).pipe(Effect.scoped);
apps/server/src/textGeneration/CursorTextGeneration.ts+2-3
...@@ -154,9 +154,8 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu...@@ -154,9 +154,8 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu
154 });154 });
155 }155 }
156156
157 return yield* Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson))(157 const decodeOutput = Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson));
158 extractJsonObject(rawResult),158 return yield* decodeOutput(extractJsonObject(rawResult)).pipe(
159 ).pipe(
160 Effect.catchTag("SchemaError", (cause) =>159 Effect.catchTag("SchemaError", (cause) =>
161 Effect.fail(160 Effect.fail(
162 new TextGenerationError({161 new TextGenerationError({
apps/server/src/textGeneration/OpenCodeTextGeneration.ts+2-3
...@@ -352,9 +352,8 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"...@@ -352,9 +352,8 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"
352 releaseSharedServer,352 releaseSharedServer,
353 );353 );
354354
355 return yield* Schema.decodeEffect(Schema.fromJsonString(input.outputSchemaJson))(355 const decodeOutput = Schema.decodeEffect(Schema.fromJsonString(input.outputSchemaJson));
356 extractJsonObject(rawOutput),356 return yield* decodeOutput(extractJsonObject(rawOutput)).pipe(
357 ).pipe(
358 Effect.catchTag("SchemaError", (cause) =>357 Effect.catchTag("SchemaError", (cause) =>
359 Effect.fail(358 Effect.fail(
360 new TextGenerationError({359 new TextGenerationError({
apps/server/src/textGeneration/TextGenerationUtils.ts+2-1
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import * as Schema from "effect/Schema";1import * as Schema from "effect/Schema";
22
3import { TextGenerationError } from "@t3tools/contracts";3import { TextGenerationError } from "@t3tools/contracts";
4const isTextGenerationError = Schema.is(TextGenerationError);
45
5/** Convert an Effect Schema to a flat JSON Schema object, inlining `$defs` when present. */6/** Convert an Effect Schema to a flat JSON Schema object, inlining `$defs` when present. */
6export function toJsonSchemaObject(schema: Schema.Top): unknown {7export function toJsonSchemaObject(schema: Schema.Top): unknown {
...@@ -127,7 +128,7 @@ export function normalizeCliError(...@@ -127,7 +128,7 @@ export function normalizeCliError(
127 error: unknown,128 error: unknown,
128 fallback: string,129 fallback: string,
129): TextGenerationError {130): TextGenerationError {
130 if (Schema.is(TextGenerationError)(error)) {131 if (isTextGenerationError(error)) {
131 return error;132 return error;
132 }133 }
133134
apps/server/src/vcs/GitVcsDriverCore.ts+2-1
...@@ -28,6 +28,7 @@ import {...@@ -28,6 +28,7 @@ import {
28 parseRemoteRefWithRemoteNames,28 parseRemoteRefWithRemoteNames,
29} from "../git/remoteRefs.ts";29} from "../git/remoteRefs.ts";
30import { ServerConfig } from "../config.ts";30import { ServerConfig } from "../config.ts";
31const isGitCommandError = Schema.is(GitCommandError);
3132
32const DEFAULT_TIMEOUT_MS = 30_000;33const DEFAULT_TIMEOUT_MS = 30_000;
33const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;34const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;
...@@ -327,7 +328,7 @@ function toGitCommandError(...@@ -327,7 +328,7 @@ function toGitCommandError(
327 detail: string,328 detail: string,
328) {329) {
329 return (cause: unknown) =>330 return (cause: unknown) =>
330 Schema.is(GitCommandError)(cause)331 isGitCommandError(cause)
331 ? cause332 ? cause
332 : new GitCommandError({333 : new GitCommandError({
333 operation: input.operation,334 operation: input.operation,
apps/server/src/vcs/VcsProjectConfig.ts+2-1
...@@ -15,6 +15,7 @@ const ProjectVcsConfig = Schema.Struct({...@@ -15,6 +15,7 @@ const ProjectVcsConfig = Schema.Struct({
15 ),15 ),
16 vcsKind: Schema.optional(VcsDriverKind),16 vcsKind: Schema.optional(VcsDriverKind),
17});17});
18const isProjectVcsConfig = Schema.is(ProjectVcsConfig);
1819
19interface ProjectVcsConfigFile {20interface ProjectVcsConfigFile {
20 readonly vcs?:21 readonly vcs?:
...@@ -47,7 +48,7 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto...@@ -47,7 +48,7 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto
47function parseConfig(raw: string): ProjectVcsConfigFile | null {48function parseConfig(raw: string): ProjectVcsConfigFile | null {
48 try {49 try {
49 const parsed = JSON.parse(raw) as unknown;50 const parsed = JSON.parse(raw) as unknown;
50 return Schema.is(ProjectVcsConfig)(parsed) ? parsed : null;51 return isProjectVcsConfig(parsed) ? parsed : null;
51 } catch {52 } catch {
52 return null;53 return null;
53 }54 }
apps/server/src/ws.ts+6-4
...@@ -86,6 +86,8 @@ import {...@@ -86,6 +86,8 @@ import {
86 type SessionCredentialChange,86 type SessionCredentialChange,
87} from "./auth/Services/SessionCredentialService.ts";87} from "./auth/Services/SessionCredentialService.ts";
88import { respondToAuthError } from "./auth/http.ts";88import { respondToAuthError } from "./auth/http.ts";
89const isOrchestrationDispatchCommandError = Schema.is(OrchestrationDispatchCommandError);
90const isWorkspacePathOutsideRootError = Schema.is(WorkspacePathOutsideRootError);
8991
90const nowIso = Effect.map(DateTime.now, DateTime.formatIso);92const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
9193
...@@ -216,7 +218,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>...@@ -216,7 +218,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
216 });218 });
217219
218 const toDispatchCommandError = (cause: unknown, fallbackMessage: string) =>220 const toDispatchCommandError = (cause: unknown, fallbackMessage: string) =>
219 Schema.is(OrchestrationDispatchCommandError)(cause)221 isOrchestrationDispatchCommandError(cause)
220 ? cause222 ? cause
221 : new OrchestrationDispatchCommandError({223 : new OrchestrationDispatchCommandError({
222 message: cause instanceof Error ? cause.message : fallbackMessage,224 message: cause instanceof Error ? cause.message : fallbackMessage,
...@@ -225,7 +227,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>...@@ -225,7 +227,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
225227
226 const toBootstrapDispatchCommandCauseError = (cause: Cause.Cause<unknown>) => {228 const toBootstrapDispatchCommandCauseError = (cause: Cause.Cause<unknown>) => {
227 const error = Cause.squash(cause);229 const error = Cause.squash(cause);
228 return Schema.is(OrchestrationDispatchCommandError)(error)230 return isOrchestrationDispatchCommandError(error)
229 ? error231 ? error
230 : new OrchestrationDispatchCommandError({232 : new OrchestrationDispatchCommandError({
231 message:233 message:
...@@ -637,7 +639,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>...@@ -637,7 +639,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
637 return result;639 return result;
638 }).pipe(640 }).pipe(
639 Effect.mapError((cause) =>641 Effect.mapError((cause) =>
640 Schema.is(OrchestrationDispatchCommandError)(cause)642 isOrchestrationDispatchCommandError(cause)
641 ? cause643 ? cause
642 : new OrchestrationDispatchCommandError({644 : new OrchestrationDispatchCommandError({
643 message: "Failed to dispatch orchestration command",645 message: "Failed to dispatch orchestration command",
...@@ -918,7 +920,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>...@@ -918,7 +920,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
918 WS_METHODS.projectsWriteFile,920 WS_METHODS.projectsWriteFile,
919 workspaceFileSystem.writeFile(input).pipe(921 workspaceFileSystem.writeFile(input).pipe(
920 Effect.mapError((cause) => {922 Effect.mapError((cause) => {
921 const message = Schema.is(WorkspacePathOutsideRootError)(cause)923 const message = isWorkspacePathOutsideRootError(cause)
922 ? "Workspace file path must stay within the project root."924 ? "Workspace file path must stay within the project root."
923 : "Failed to write workspace file";925 : "Failed to write workspace file";
924 return new ProjectWriteFileError({926 return new ProjectWriteFileError({
apps/web/src/components/settings/ProviderSettingsForm.tsx+4-6
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1"use client";1"use client";
22
3import { useMemo, type ReactNode } from "react";3import { useMemo, type ReactNode } from "react";
4import * as Option from "effect/Option";
4import * as Schema from "effect/Schema";5import * as Schema from "effect/Schema";
5import type {6import type {
6 ProviderSettingsFormAnnotation,7 ProviderSettingsFormAnnotation,
...@@ -63,12 +64,9 @@ function readProviderSettingsFormSchemaAnnotation(...@@ -63,12 +64,9 @@ function readProviderSettingsFormSchemaAnnotation(
63function readFieldBooleanDefault(64function readFieldBooleanDefault(
64 fieldSchema: ProviderClientDefinition["settingsSchema"]["fields"][string],65 fieldSchema: ProviderClientDefinition["settingsSchema"]["fields"][string],
65): boolean | undefined {66): boolean | undefined {
66 try {67 const decodeDefault = Schema.decodeUnknownOption(fieldSchema as Schema.Decoder<unknown>);
67 const decoded = Schema.decodeUnknownSync(fieldSchema as Schema.Decoder<unknown>)(undefined);68 const decoded = decodeDefault(undefined);
68 return typeof decoded === "boolean" ? decoded : undefined;69 return Option.isSome(decoded) && typeof decoded.value === "boolean" ? decoded.value : undefined;
69 } catch {
70 return undefined;
71 }
72}70}
7371
74export function deriveProviderSettingsFields(72export function deriveProviderSettingsFields(
apps/web/src/composerDraftStore.ts+3-2
...@@ -42,11 +42,12 @@ import { useShallow } from "zustand/react/shallow";...@@ -42,11 +42,12 @@ import { useShallow } from "zustand/react/shallow";
42import { createDebouncedStorage, createMemoryStorage } from "./lib/storage";42import { createDebouncedStorage, createMemoryStorage } from "./lib/storage";
43import { getDefaultServerModel } from "./providerModels";43import { getDefaultServerModel } from "./providerModels";
44import { UnifiedSettings } from "@t3tools/contracts/settings";44import { UnifiedSettings } from "@t3tools/contracts/settings";
45const isRuntimeMode = Schema.is(RuntimeMode);
46const isProviderDriverKind = Schema.is(ProviderDriverKind);
4547
46export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1";48export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1";
47const COMPOSER_DRAFT_STORAGE_VERSION = 6;49const COMPOSER_DRAFT_STORAGE_VERSION = 6;
48const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]);50const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]);
49const isRuntimeMode = Schema.is(RuntimeMode);
50export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type;51export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type;
5152
52export const DraftId = Schema.String.pipe(Schema.brand("DraftId"));53export const DraftId = Schema.String.pipe(Schema.brand("DraftId"));
...@@ -575,7 +576,7 @@ function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean {...@@ -575,7 +576,7 @@ function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean {
575}576}
576577
577function normalizeProviderDriverKind(value: unknown): ProviderDriverKind | null {578function normalizeProviderDriverKind(value: unknown): ProviderDriverKind | null {
578 return Schema.is(ProviderDriverKind)(value) ? value : null;579 return isProviderDriverKind(value) ? value : null;
579}580}
580581
581/**582/**
apps/web/src/hooks/useLocalStorage.ts+8-4
...@@ -19,11 +19,15 @@ const isomorphicLocalStorage: Storage =...@@ -19,11 +19,15 @@ const isomorphicLocalStorage: Storage =
19 };19 };
20 })();20 })();
2121
22const decode = <T, E>(schema: Schema.Codec<T, E>, value: string) =>22const decode = <T, E>(schema: Schema.Codec<T, E>, value: string) => {
23 Schema.decodeSync(Schema.fromJsonString(schema))(value);23 const decodeJson = Schema.decodeSync(Schema.fromJsonString(schema));
24 return decodeJson(value);
25};
2426
25const encode = <T, E>(schema: Schema.Codec<T, E>, value: T) =>27const encode = <T, E>(schema: Schema.Codec<T, E>, value: T) => {
26 Schema.encodeSync(Schema.fromJsonString(schema))(value);28 const encodeJson = Schema.encodeSync(Schema.fromJsonString(schema));
29 return encodeJson(value);
30};
2731
28export const getLocalStorageItem = <T, E>(key: string, schema: Schema.Codec<T, E>): T | null => {32export const getLocalStorageItem = <T, E>(key: string, schema: Schema.Codec<T, E>): T | null => {
29 const item = isomorphicLocalStorage.getItem(key);33 const item = isomorphicLocalStorage.getItem(key);
apps/web/src/lib/projectScriptKeybindings.ts+3-1
...@@ -8,6 +8,8 @@ import * as Schema from "effect/Schema";...@@ -8,6 +8,8 @@ import * as Schema from "effect/Schema";
88
9export const PROJECT_SCRIPT_KEYBINDING_INVALID_MESSAGE = "Invalid keybinding.";9export const PROJECT_SCRIPT_KEYBINDING_INVALID_MESSAGE = "Invalid keybinding.";
1010
11const decodeKeybindingRule = Schema.decodeUnknownOption(KeybindingRuleSchema);
12
11function normalizeProjectScriptKeybindingInput(13function normalizeProjectScriptKeybindingInput(
12 keybinding: string | null | undefined,14 keybinding: string | null | undefined,
13): string | null {15): string | null {
...@@ -22,7 +24,7 @@ export function decodeProjectScriptKeybindingRule(input: {...@@ -22,7 +24,7 @@ export function decodeProjectScriptKeybindingRule(input: {
22 const normalizedKey = normalizeProjectScriptKeybindingInput(input.keybinding);24 const normalizedKey = normalizeProjectScriptKeybindingInput(input.keybinding);
23 if (!normalizedKey) return null;25 if (!normalizedKey) return null;
2426
25 const decoded = Schema.decodeUnknownOption(KeybindingRuleSchema)({27 const decoded = decodeKeybindingRule({
26 key: normalizedKey,28 key: normalizedKey,
27 command: input.command,29 command: input.command,
28 });30 });
apps/web/src/lib/providerReactQuery.ts+5-2
...@@ -9,6 +9,9 @@ import * as Option from "effect/Option";...@@ -9,6 +9,9 @@ import * as Option from "effect/Option";
9import * as Schema from "effect/Schema";9import * as Schema from "effect/Schema";
10import { ensureEnvironmentApi } from "../environmentApi";10import { ensureEnvironmentApi } from "../environmentApi";
1111
12const decodeFullThreadDiffInput = Schema.decodeUnknownOption(OrchestrationGetFullThreadDiffInput);
13const decodeTurnDiffInput = Schema.decodeUnknownOption(OrchestrationGetTurnDiffInput);
14
12interface CheckpointDiffQueryInput {15interface CheckpointDiffQueryInput {
13 environmentId: EnvironmentId | null;16 environmentId: EnvironmentId | null;
14 threadId: ThreadId | null;17 threadId: ThreadId | null;
...@@ -36,14 +39,14 @@ export const providerQueryKeys = {...@@ -36,14 +39,14 @@ export const providerQueryKeys = {
3639
37function decodeCheckpointDiffRequest(input: CheckpointDiffQueryInput) {40function decodeCheckpointDiffRequest(input: CheckpointDiffQueryInput) {
38 if (input.fromTurnCount === 0) {41 if (input.fromTurnCount === 0) {
39 return Schema.decodeUnknownOption(OrchestrationGetFullThreadDiffInput)({42 return decodeFullThreadDiffInput({
40 threadId: input.threadId,43 threadId: input.threadId,
41 toTurnCount: input.toTurnCount,44 toTurnCount: input.toTurnCount,
42 ignoreWhitespace: input.ignoreWhitespace,45 ignoreWhitespace: input.ignoreWhitespace,
43 }).pipe(Option.map((fields) => ({ kind: "fullThreadDiff" as const, input: fields })));46 }).pipe(Option.map((fields) => ({ kind: "fullThreadDiff" as const, input: fields })));
44 }47 }
4548
46 return Schema.decodeUnknownOption(OrchestrationGetTurnDiffInput)({49 return decodeTurnDiffInput({
47 threadId: input.threadId,50 threadId: input.threadId,
48 fromTurnCount: input.fromTurnCount,51 fromTurnCount: input.fromTurnCount,
49 toTurnCount: input.toTurnCount,52 toTurnCount: input.toTurnCount,
apps/web/src/projectScripts.ts+2-1
...@@ -5,6 +5,7 @@ import {...@@ -5,6 +5,7 @@ import {
5 type ProjectScript,5 type ProjectScript,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import * as Schema from "effect/Schema";7import * as Schema from "effect/Schema";
8const isScriptRunCommand = Schema.is(SCRIPT_RUN_COMMAND_PATTERN);
89
9function normalizeScriptId(value: string): string {10function normalizeScriptId(value: string): string {
10 const cleaned = value11 const cleaned = value
...@@ -26,7 +27,7 @@ export const commandForProjectScript = (scriptId: string): KeybindingCommand =>...@@ -26,7 +27,7 @@ export const commandForProjectScript = (scriptId: string): KeybindingCommand =>
2627
27export function projectScriptIdFromCommand(command: string): string | null {28export function projectScriptIdFromCommand(command: string): string | null {
28 const trimmed = command.trim();29 const trimmed = command.trim();
29 if (!Schema.is(SCRIPT_RUN_COMMAND_PATTERN)(trimmed)) {30 if (!isScriptRunCommand(trimmed)) {
30 return null;31 return null;
31 }32 }
32 const [prefix, , suffix] = SCRIPT_RUN_COMMAND_PATTERN.parts;33 const [prefix, , suffix] = SCRIPT_RUN_COMMAND_PATTERN.parts;
apps/web/src/store.ts+2-1
...@@ -37,6 +37,7 @@ import {...@@ -37,6 +37,7 @@ import {
37import { resolveEnvironmentHttpUrl } from "./environments/runtime";37import { resolveEnvironmentHttpUrl } from "./environments/runtime";
38import { sanitizeThreadErrorMessage } from "./rpc/transportError";38import { sanitizeThreadErrorMessage } from "./rpc/transportError";
39import { getThreadFromEnvironmentState } from "./threadDerivation";39import { getThreadFromEnvironmentState } from "./threadDerivation";
40const isProviderDriverKindValue = Schema.is(ProviderDriverKind);
4041
41export interface EnvironmentState {42export interface EnvironmentState {
42 projectIds: ProjectId[];43 projectIds: ProjectId[];
...@@ -1006,7 +1007,7 @@ function toLegacySessionStatus(...@@ -1006,7 +1007,7 @@ function toLegacySessionStatus(
1006}1007}
10071008
1008function toLegacyProvider(providerName: string | null): ProviderDriverKind {1009function toLegacyProvider(providerName: string | null): ProviderDriverKind {
1009 if (Schema.is(ProviderDriverKind)(providerName)) {1010 if (isProviderDriverKindValue(providerName)) {
1010 return providerName;1011 return providerName;
1011 }1012 }
1012 return ProviderDriverKind.make("codex");1013 return ProviderDriverKind.make("codex");
apps/web/src/uiStateStore.test.ts+20-20
...@@ -445,27 +445,27 @@ describe("uiStateStore pure functions", () => {...@@ -445,27 +445,27 @@ describe("uiStateStore pure functions", () => {
445 });445 });
446});446});
447447
448describe("uiStateStore persistence round-trip", () => {448function createLocalStorageStub(): Storage {
449 function createLocalStorageStub(): Storage {449 const store = new Map<string, string>();
450 const store = new Map<string, string>();450 return {
451 return {451 clear: () => {
452 clear: () => {452 store.clear();
453 store.clear();453 },
454 },454 getItem: (key) => store.get(key) ?? null,
455 getItem: (key) => store.get(key) ?? null,455 key: (index) => [...store.keys()][index] ?? null,
456 key: (index) => [...store.keys()][index] ?? null,456 get length() {
457 get length() {457 return store.size;
458 return store.size;458 },
459 },459 removeItem: (key) => {
460 removeItem: (key) => {460 store.delete(key);
461 store.delete(key);461 },
462 },462 setItem: (key, value) => {
463 setItem: (key, value) => {463 store.set(key, value);
464 store.set(key, value);464 },
465 },465 };
466 };466}
467 }
468467
468describe("uiStateStore persistence round-trip", () => {
469 let localStorageStub: Storage;469 let localStorageStub: Storage;
470470
471 beforeEach(() => {471 beforeEach(() => {
bun.lock+41-21
...@@ -6,9 +6,10 @@...@@ -6,9 +6,10 @@
6 "name": "@t3tools/monorepo",6 "name": "@t3tools/monorepo",
7 "devDependencies": {7 "devDependencies": {
8 "@effect/language-service": "catalog:",8 "@effect/language-service": "catalog:",
9 "@oxlint/plugins": "^1.63.0",
9 "@types/node": "catalog:",10 "@types/node": "catalog:",
10 "oxfmt": "^0.40.0",11 "oxfmt": "^0.40.0",
11 "oxlint": "^1.55.0",12 "oxlint": "^1.63.0",
12 "turbo": "^2.3.3",13 "turbo": "^2.3.3",
13 "typescript": "catalog:",14 "typescript": "catalog:",
14 "vitest": "catalog:",15 "vitest": "catalog:",
...@@ -135,6 +136,21 @@...@@ -135,6 +136,21 @@
135 "vitest-browser-react": "^2.0.5",136 "vitest-browser-react": "^2.0.5",
136 },137 },
137 },138 },
139 "oxlint-plugin-t3code": {
140 "name": "@t3tools/oxlint-plugin-t3code",
141 "dependencies": {
142 "@effect/platform-node": "catalog:",
143 "@oxlint/plugins": "^1.63.0",
144 "effect": "catalog:",
145 },
146 "devDependencies": {
147 "@effect/language-service": "catalog:",
148 "@effect/vitest": "catalog:",
149 "@types/bun": "catalog:",
150 "typescript": "catalog:",
151 "vitest": "catalog:",
152 },
153 },
138 "packages/client-runtime": {154 "packages/client-runtime": {
139 "name": "@t3tools/client-runtime",155 "name": "@t3tools/client-runtime",
140 "version": "0.0.0-alpha.1",156 "version": "0.0.0-alpha.1",
...@@ -667,43 +683,45 @@...@@ -667,43 +683,45 @@
667683
668 "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/Zmj0yTYSvmha6TG1QnoLqVT7ZMRDqXvFXXBQpIjteEwx9qvUYMBH2xbiOFhDeMUJkGwC3D6fdKsFtaqUvkwNA=="],684 "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/Zmj0yTYSvmha6TG1QnoLqVT7ZMRDqXvFXXBQpIjteEwx9qvUYMBH2xbiOFhDeMUJkGwC3D6fdKsFtaqUvkwNA=="],
669685
670 "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A=="],686 "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.63.0", "", { "os": "android", "cpu": "arm" }, "sha512-A9xLtQt7i0OA1PoB/meog6kikXI9CdwEp7ZwQqmgnpKn3G3b1orvTDy8CQ6T7w1HvDrgWGB78PkFKcWgibcTCg=="],
687
688 "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.63.0", "", { "os": "android", "cpu": "arm64" }, "sha512-SQo+ZMvdR9l3CxZp5W5gFNxSiDxclY6lOzzNpKYLF8asESpm3Pwumx0gER5T7aHLF1/2BAAtLD3DiDkdgy4V1A=="],
671689
672 "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg=="],690 "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.63.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6W82XjJDTmMnjg30427l0dufpnyLoq7wEukKdM6/g2VIybRVuQiBVh43EA4b+UxZ3+tLcKm+Or/pXGNgLCEU8g=="],
673691
674 "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw=="],692 "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.63.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CnWd/YCuVG5W1BYkjJEVbJG11o526O9qAwBEQM+nh8K19CRFUkFdROXCyYkGmroHEYQe4vgQ6+lh3550Lp35Xw=="],
675693
676 "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ=="],694 "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.63.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-a4eZAqrmtajqcxfdAzC+l7g3PaE3V8hpAYqqeD3fTxLXOMFdK3eNTZrU80n4dDEVm0JXy1aL5PqvqWldBl6zYA=="],
677695
678 "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg=="],696 "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-tYUtU9TdbU3uXF5D62g5zXJ13iniFGhXQx5vp9cyEjGdbSAY3VdFBSaldYvyoDmgMZ0ZYuwQP1Y4t2Fhejwa0w=="],
679697
680 "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg=="],698 "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-I5r3twFf776UZg9dmRo2xbrKt00tTkORXEVe0ctg4vdTkQvJAjiCHxnbAU2HL1AiJ9cqADA76MAliuilsAWnvg=="],
681699
682 "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ=="],700 "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-t7ltUkg6FFh4b564QyGir8xIj/QZbXu8FlcRkcyW9+ztr/mfRHlvUOFd95pJCXi9s/L5DrUeWWgpXRS+V+6igQ=="],
683701
684 "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A=="],702 "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q5mmZy/XWjuYFUuQyYjOvZ5U/JkKEwnpir6hGxhh6HcdP0V/BKxLo8dqkfF/t7r7AguB17dfS/8+go5AQDRR6g=="],
685703
686 "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g=="],704 "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.63.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uBGtuZ0TzLB4x5wVa82HGNvYqY8buwDhyCnCP0R0gkk9szqVsP0MeTtD5HX7EsEuFIt+aYmYxuxeVxs3nTSwtQ=="],
687705
688 "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA=="],706 "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-h4s6FwxE+9MeA181o0dnDwHP32Y/bG8EiB/vrD6Ib+AMt6haigDc/0bUtI/sLmQDBMJnUfaCmtSSrEAqjtEVrA=="],
689707
690 "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg=="],708 "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-2EaNcCBR8Mcjl5ARtuN3BdEpVkX7KpjSjMGZ/mJMIeaXgTtdz5ytg2VwygMSStA/k0ixfvZFoZOfjDEcouV5vQ=="],
691709
692 "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA=="],710 "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.63.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4hlf/fd7TrYYl3QrWWD0GocqJefwMu3cHQhmi2FvEB/YOvFb5DZN3SMBaPi7B1TM5DeypkEtrVib674q1KKPg=="],
693711
694 "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ=="],712 "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Vgq9rkRVcPcjbcH+ihYTfpeR7vCXfqpd+z5ItTGc0yYUV59L5ceHYN1iV4H9bKGV7Rn5hkVc7x3mSvHegduENA=="],
695713
696 "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ=="],714 "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3/Lkq/ncooA61rorrC+ZQed1Bc4VpGj+WnGsp58zmxKgvZ2vhreu+dcVyr3mX8NUpq7mfZ4gDDTou/yrF1Pd7A=="],
697715
698 "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA=="],716 "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.63.0", "", { "os": "none", "cpu": "arm64" }, "sha512-0/EdD/6hDkx5Mfd769PTjvEM8mZ/6Dfukp1dBCL/2PjlIVGEtYdNZyok6ChqYPsT9JcFnlQnUeQzO0/1L/oC9w=="],
699717
700 "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg=="],718 "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.63.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-wb0CUkN8ngwPiRQBjD1Cj0LsHeNvm+Xt6YBHDMtj2DVQVD6Oj8Ri7g6BD+KICf6LaBqZlmzOvy6nF9E/8yyGOg=="],
701719
702 "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g=="],720 "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.63.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-BX5iq+ovdNlVYhSn5qPMUIT0uwAwt2lmEnCnzK+Gkhw4DovIvhGb96OFhV8yzQNUnQxn/xGkOR+X+BLrLDNm8w=="],
703721
704 "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A=="],722 "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.63.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QeN/WELOfsXMeYwxvfgQrl6CbVftYUCZsGXHjXQd5Trccm8+i4gmtxaOui4xbJQaiDlviF8F3yLSBloQUeFsfA=="],
705723
706 "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ=="],724 "@oxlint/plugins": ["@oxlint/plugins@1.63.0", "", {}, "sha512-vZAzaUQkwgdN62RHgPFzfCsiBI6SDJMUdUlBGpJK0V++UHCMUk7UeJseygOhE/wOUSgV3ccE4ORkgab3C3MC6g=="],
707725
708 "@pierre/diffs": ["@pierre/diffs@1.1.20", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-lLi+3sLCm3QDd5/aLO9pw+WbF6UzhrkWm2oTZ5WZJTGemOyUNRJ4DDhcEKmVusu4C4bXx9Nssh6fF+wQcapb5w=="],726 "@pierre/diffs": ["@pierre/diffs@1.1.20", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-lLi+3sLCm3QDd5/aLO9pw+WbF6UzhrkWm2oTZ5WZJTGemOyUNRJ4DDhcEKmVusu4C4bXx9Nssh6fF+wQcapb5w=="],
709727
...@@ -783,6 +801,8 @@...@@ -783,6 +801,8 @@
783801
784 "@t3tools/marketing": ["@t3tools/marketing@workspace:apps/marketing"],802 "@t3tools/marketing": ["@t3tools/marketing@workspace:apps/marketing"],
785803
804 "@t3tools/oxlint-plugin-t3code": ["@t3tools/oxlint-plugin-t3code@workspace:oxlint-plugin-t3code"],
805
786 "@t3tools/scripts": ["@t3tools/scripts@workspace:scripts"],806 "@t3tools/scripts": ["@t3tools/scripts@workspace:scripts"],
787807
788 "@t3tools/shared": ["@t3tools/shared@workspace:packages/shared"],808 "@t3tools/shared": ["@t3tools/shared@workspace:packages/shared"],
...@@ -1639,7 +1659,7 @@...@@ -1639,7 +1659,7 @@
16391659
1640 "oxfmt": ["oxfmt@0.40.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.40.0", "@oxfmt/binding-android-arm64": "0.40.0", "@oxfmt/binding-darwin-arm64": "0.40.0", "@oxfmt/binding-darwin-x64": "0.40.0", "@oxfmt/binding-freebsd-x64": "0.40.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.40.0", "@oxfmt/binding-linux-arm-musleabihf": "0.40.0", "@oxfmt/binding-linux-arm64-gnu": "0.40.0", "@oxfmt/binding-linux-arm64-musl": "0.40.0", "@oxfmt/binding-linux-ppc64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-musl": "0.40.0", "@oxfmt/binding-linux-s390x-gnu": "0.40.0", "@oxfmt/binding-linux-x64-gnu": "0.40.0", "@oxfmt/binding-linux-x64-musl": "0.40.0", "@oxfmt/binding-openharmony-arm64": "0.40.0", "@oxfmt/binding-win32-arm64-msvc": "0.40.0", "@oxfmt/binding-win32-ia32-msvc": "0.40.0", "@oxfmt/binding-win32-x64-msvc": "0.40.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ=="],1660 "oxfmt": ["oxfmt@0.40.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.40.0", "@oxfmt/binding-android-arm64": "0.40.0", "@oxfmt/binding-darwin-arm64": "0.40.0", "@oxfmt/binding-darwin-x64": "0.40.0", "@oxfmt/binding-freebsd-x64": "0.40.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.40.0", "@oxfmt/binding-linux-arm-musleabihf": "0.40.0", "@oxfmt/binding-linux-arm64-gnu": "0.40.0", "@oxfmt/binding-linux-arm64-musl": "0.40.0", "@oxfmt/binding-linux-ppc64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-musl": "0.40.0", "@oxfmt/binding-linux-s390x-gnu": "0.40.0", "@oxfmt/binding-linux-x64-gnu": "0.40.0", "@oxfmt/binding-linux-x64-musl": "0.40.0", "@oxfmt/binding-openharmony-arm64": "0.40.0", "@oxfmt/binding-win32-arm64-msvc": "0.40.0", "@oxfmt/binding-win32-ia32-msvc": "0.40.0", "@oxfmt/binding-win32-x64-msvc": "0.40.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ=="],
16411661
1642 "oxlint": ["oxlint@1.56.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.56.0", "@oxlint/binding-android-arm64": "1.56.0", "@oxlint/binding-darwin-arm64": "1.56.0", "@oxlint/binding-darwin-x64": "1.56.0", "@oxlint/binding-freebsd-x64": "1.56.0", "@oxlint/binding-linux-arm-gnueabihf": "1.56.0", "@oxlint/binding-linux-arm-musleabihf": "1.56.0", "@oxlint/binding-linux-arm64-gnu": "1.56.0", "@oxlint/binding-linux-arm64-musl": "1.56.0", "@oxlint/binding-linux-ppc64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-musl": "1.56.0", "@oxlint/binding-linux-s390x-gnu": "1.56.0", "@oxlint/binding-linux-x64-gnu": "1.56.0", "@oxlint/binding-linux-x64-musl": "1.56.0", "@oxlint/binding-openharmony-arm64": "1.56.0", "@oxlint/binding-win32-arm64-msvc": "1.56.0", "@oxlint/binding-win32-ia32-msvc": "1.56.0", "@oxlint/binding-win32-x64-msvc": "1.56.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g=="],1662 "oxlint": ["oxlint@1.63.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.63.0", "@oxlint/binding-android-arm64": "1.63.0", "@oxlint/binding-darwin-arm64": "1.63.0", "@oxlint/binding-darwin-x64": "1.63.0", "@oxlint/binding-freebsd-x64": "1.63.0", "@oxlint/binding-linux-arm-gnueabihf": "1.63.0", "@oxlint/binding-linux-arm-musleabihf": "1.63.0", "@oxlint/binding-linux-arm64-gnu": "1.63.0", "@oxlint/binding-linux-arm64-musl": "1.63.0", "@oxlint/binding-linux-ppc64-gnu": "1.63.0", "@oxlint/binding-linux-riscv64-gnu": "1.63.0", "@oxlint/binding-linux-riscv64-musl": "1.63.0", "@oxlint/binding-linux-s390x-gnu": "1.63.0", "@oxlint/binding-linux-x64-gnu": "1.63.0", "@oxlint/binding-linux-x64-musl": "1.63.0", "@oxlint/binding-openharmony-arm64": "1.63.0", "@oxlint/binding-win32-arm64-msvc": "1.63.0", "@oxlint/binding-win32-ia32-msvc": "1.63.0", "@oxlint/binding-win32-x64-msvc": "1.63.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-9TGXetdjgIHOJ9OiReomP7nnrMkV9HxC1xM2ramJSLQpzxjsAJtQwa4wqkJN2f/uCrqZuJseFuSlWDdvcruveg=="],
16431663
1644 "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],1664 "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
16451665
oxlint-plugin-t3code/index.ts created+12
...@@ -0,0 +1,12 @@
1import { definePlugin } from "@oxlint/plugins";
2
3import noInlineSchemaCompile from "./rules/no-inline-schema-compile.ts";
4
5export default definePlugin({
6 meta: {
7 name: "t3code",
8 },
9 rules: {
10 "no-inline-schema-compile": noInlineSchemaCompile,
11 },
12});
oxlint-plugin-t3code/package.json created+21
...@@ -0,0 +1,21 @@
1{
2 "name": "@t3tools/oxlint-plugin-t3code",
3 "private": true,
4 "type": "module",
5 "scripts": {
6 "typecheck": "tsc --noEmit",
7 "test": "vitest run"
8 },
9 "dependencies": {
10 "@effect/platform-node": "catalog:",
11 "@oxlint/plugins": "^1.63.0",
12 "effect": "catalog:"
13 },
14 "devDependencies": {
15 "@effect/language-service": "catalog:",
16 "@effect/vitest": "catalog:",
17 "@types/bun": "catalog:",
18 "typescript": "catalog:",
19 "vitest": "catalog:"
20 }
21}
oxlint-plugin-t3code/rules/no-inline-schema-compile.test.ts created+83
...@@ -0,0 +1,83 @@
1import { assert, describe } from "@effect/vitest";
2
3import { createOxlintRuleHarness } from "../test/utils.ts";
4
5const rule = createOxlintRuleHarness("t3code/no-inline-schema-compile");
6
7describe("t3code/no-inline-schema-compile", () => {
8 rule.valid(
9 "allows schema compilers hoisted to module scope",
10 `
11 import { Schema } from "effect";
12
13 const User = Schema.Struct({ name: Schema.String });
14 const decodeUser = Schema.decodeUnknownEffect(User);
15
16 export const parseUser = (input: unknown) => decodeUser(input);
17 `,
18 );
19
20 rule.valid(
21 "allows factory helpers that return a precompiled decoder",
22 `
23 import { Schema } from "effect";
24
25 export const makeParser = <A, I>(schema: Schema.Codec<A, I>) => {
26 const decode = Schema.decodeUnknownEffect(schema);
27 return (input: unknown) => decode(input);
28 };
29 `,
30 );
31
32 rule.valid(
33 "allows schema construction helpers that use encode transformations",
34 `
35 import { Schema } from "effect";
36
37 export const makePrettyJson = <S extends Schema.Top>(schema: S) =>
38 Schema.fromJsonString(schema).pipe(
39 Schema.encode({
40 decode: Schema.String,
41 encode: Schema.String,
42 }),
43 );
44 `,
45 );
46
47 rule.valid(
48 "allows dynamic schema parameters that cannot be hoisted to module scope",
49 `
50 import { Schema } from "effect";
51
52 export const parseWith = <A, I>(schema: Schema.Codec<A, I>, input: unknown) =>
53 Schema.decodeUnknownEffect(schema)(input);
54 `,
55 );
56
57 rule.invalid(
58 "reports schema compilers inside function bodies",
59 `
60 import { Schema } from "effect";
61
62 const User = Schema.Struct({ name: Schema.String });
63
64 export const parseUser = (input: unknown) => Schema.decodeUnknownEffect(User)(input);
65 `,
66 (output) => {
67 assert.match(output, /Hoist Schema\.decodeUnknownEffect/);
68 },
69 );
70
71 rule.invalid(
72 "reports inline schema literals as high confidence findings",
73 `
74 import { Schema } from "effect";
75
76 export const parseUser = (input: unknown) =>
77 Schema.decodeUnknownEffect(Schema.Struct({ name: Schema.String }))(input);
78 `,
79 (output) => {
80 assert.match(output, /inline schema literal and the compiled function/);
81 },
82 );
83});
oxlint-plugin-t3code/rules/no-inline-schema-compile.ts created+150
...@@ -0,0 +1,150 @@
1import { defineRule } from "@oxlint/plugins";
2import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";
4
5import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.ts";
6
7// Effect Schema decoder/encoder APIs allocate compiled functions. Keep them
8// outside function bodies so hot paths do not rebuild compilers per call.
9const COMPILER_METHODS = new Set<keyof typeof Schema>([
10 "is",
11 "asserts",
12 "decodeEffect",
13 "decodeExit",
14 "decodeOption",
15 "decodePromise",
16 "decodeSync",
17 "decodeUnknownExit",
18 "decodeUnknownEffect",
19 "decodeUnknownOption",
20 "decodeUnknownPromise",
21 "decodeUnknownSync",
22
23 "encodeExit",
24 "encodeEffect",
25 "encodeOption",
26 "encodePromise",
27 "encodeSync",
28 "encodeUnknownExit",
29 "encodeUnknownEffect",
30 "encodeUnknownOption",
31 "encodeUnknownPromise",
32 "encodeUnknownSync",
33]);
34
35const getSchemaCompilerMethod = (callee: unknown): Option.Option<string> => {
36 const expression = unwrapExpression(callee);
37 if (Option.isNone(expression) || expression.value.type !== "MemberExpression") {
38 return Option.none();
39 }
40
41 const object = unwrapExpression(expression.value.object);
42 if (!isIdentifier(object, "Schema")) return Option.none();
43
44 return Option.filter(getPropertyName(expression.value.property), (method) =>
45 COMPILER_METHODS.has(method as keyof typeof Schema),
46 );
47};
48
49const isStaticSchemaReference = (node: unknown): boolean => {
50 const expression = unwrapExpression(node);
51 if (Option.isNone(expression)) return false;
52
53 if (expression.value.type === "Identifier") {
54 const [firstChar] = expression.value.name;
55 return firstChar !== undefined && firstChar.toUpperCase() === firstChar;
56 }
57
58 return expression.value.type === "MemberExpression";
59};
60
61const isNestedStaticSchemaCall = (node: unknown): boolean => {
62 const expression = unwrapExpression(node);
63 if (Option.isNone(expression) || expression.value.type !== "CallExpression") return false;
64
65 const callee = unwrapExpression(expression.value.callee);
66 if (Option.isNone(callee) || callee.value.type !== "MemberExpression") return false;
67
68 const object = unwrapExpression(callee.value.object);
69 if (!isIdentifier(object, "Schema")) return false;
70
71 const method = getPropertyName(callee.value.property);
72 if (Option.isSome(method) && method.value === "fromJsonString") {
73 const firstArg = expression.value.arguments[0];
74 return isStaticSchemaReference(firstArg) || isNestedStaticSchemaCall(firstArg);
75 }
76
77 return true;
78};
79
80const isImmediatelyInvoked = (node: unknown): boolean => {
81 const expression = unwrapExpression(node);
82 if (Option.isNone(expression)) return false;
83
84 const parent =
85 "parent" in expression.value ? unwrapExpression(expression.value.parent) : Option.none();
86 return (
87 Option.isSome(parent) &&
88 parent.value.type === "CallExpression" &&
89 unwrapExpression(parent.value.callee).pipe(
90 Option.exists((callee) => callee === expression.value),
91 )
92 );
93};
94
95const messageHigh = (method: string) =>
96 `Hoist Schema.${method}(...) to module scope: both the inline schema literal and the compiled function are rebuilt on every call. Move the compiled function to a module-level const.`;
97
98const messageMedium = (method: string) =>
99 `Hoist Schema.${method}(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.`;
100
101export default defineRule({
102 meta: {
103 type: "problem",
104 docs: {
105 description:
106 "Disallow Schema decoder/encoder compiler calls inside function bodies; hoist them to module scope.",
107 },
108 },
109 createOnce(context) {
110 let functionDepth = 0;
111
112 const resetFunctionDepth = () => {
113 functionDepth = 0;
114 };
115
116 const enterFunction = () => {
117 functionDepth++;
118 };
119
120 const exitFunction = () => {
121 functionDepth--;
122 };
123
124 return {
125 before: resetFunctionDepth,
126 FunctionDeclaration: enterFunction,
127 "FunctionDeclaration:exit": exitFunction,
128 FunctionExpression: enterFunction,
129 "FunctionExpression:exit": exitFunction,
130 ArrowFunctionExpression: enterFunction,
131 "ArrowFunctionExpression:exit": exitFunction,
132 CallExpression(node) {
133 if (functionDepth === 0) return;
134
135 const method = getSchemaCompilerMethod(node.callee);
136 if (Option.isNone(method)) return;
137 if (!isImmediatelyInvoked(node)) return;
138
139 const firstArg = node.arguments[0];
140 const high = firstArg && isNestedStaticSchemaCall(firstArg);
141 if (!high && !isStaticSchemaReference(firstArg)) return;
142
143 context.report({
144 node: node.callee,
145 message: high ? messageHigh(method.value) : messageMedium(method.value),
146 });
147 },
148 };
149 },
150});
oxlint-plugin-t3code/test/utils.ts created+152
...@@ -0,0 +1,152 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";
3import * as Data from "effect/Data";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import * as PlatformError from "effect/PlatformError";
8import * as Predicate from "effect/Predicate";
9import * as Schema from "effect/Schema";
10import * as Stream from "effect/Stream";
11import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
12
13class OxlintFixtureFailure extends Data.TaggedError("OxlintFixtureFailure")<{
14 readonly exitCode: number;
15 readonly stdout: string;
16 readonly stderr: string;
17}> {
18 static readonly is = (u: unknown): u is OxlintFixtureFailure =>
19 Predicate.isTagged(u, "OxlintFixtureFailure");
20}
21
22class OxlintFixtureExpectedFailure extends Data.TaggedError("OxlintFixtureExpectedFailure")<{
23 readonly ruleName: string;
24}> {
25 override get message() {
26 return `Expected oxlint to report a failure for rule ${this.ruleName}, but it passed.`;
27 }
28}
29
30const encodeOxlintConfig = Schema.encodeEffect(Schema.UnknownFromJsonString);
31
32interface RuleHarness {
33 readonly run: (
34 source: string,
35 ) => Effect.Effect<
36 string,
37 OxlintFixtureFailure | PlatformError.PlatformError | Schema.SchemaError,
38 NodeServices.NodeServices
39 >;
40 readonly runAndExpectFailure: (
41 source: string,
42 ) => Effect.Effect<
43 string,
44 OxlintFixtureExpectedFailure | PlatformError.PlatformError | Schema.SchemaError,
45 NodeServices.NodeServices
46 >;
47 readonly valid: (name: string, source: string) => void;
48 readonly invalid: (name: string, source: string, assertion?: (output: string) => void) => void;
49}
50
51const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
52 stream.pipe(
53 Stream.decodeText(),
54 Stream.runFold(
55 () => "",
56 (acc, chunk) => acc + chunk,
57 ),
58 );
59
60const spawnAndCollectOutput = Effect.fnUntraced(function* (command: ChildProcess.Command) {
61 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
62 const child = yield* spawner.spawn(command);
63
64 const [stdout, stderr, exitCode] = yield* Effect.all(
65 [
66 collectStreamAsString(child.stdout),
67 collectStreamAsString(child.stderr),
68 child.exitCode.pipe(Effect.map(Number)),
69 ],
70 { concurrency: "unbounded" },
71 );
72
73 return { exitCode, stdout, stderr };
74}, Effect.scoped);
75
76export const createOxlintRuleHarness = (ruleName: string): RuleHarness => {
77 const [pluginName, shortRuleName] = ruleName.split("/");
78 const diagnosticRuleName =
79 pluginName && shortRuleName ? `${pluginName}\\(${shortRuleName}\\)` : ruleName;
80 const test = it.layer(NodeServices.layer);
81
82 const run: RuleHarness["run"] = Effect.fnUntraced(function* (source: string) {
83 const fs = yield* FileSystem.FileSystem;
84 const path = yield* Path.Path;
85 const fixtureDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-oxlint-" });
86 const configPath = path.join(fixtureDir, ".oxlintrc.json");
87 const sourcePath = path.join(fixtureDir, "fixture.ts");
88 const repoRoot = path.join(import.meta.dirname, "..", "..");
89 const oxlintBin = path.join(repoRoot, "node_modules", ".bin", "oxlint");
90 const pluginPath = path.join(repoRoot, "oxlint-plugin-t3code", "index.ts");
91
92 yield* fs.writeFileString(
93 configPath,
94 yield* encodeOxlintConfig({
95 jsPlugins: [{ name: "t3code", specifier: pluginPath }],
96 rules: { [ruleName]: "error" },
97 }),
98 );
99 yield* fs.writeFileString(sourcePath, source);
100
101 const output = yield* spawnAndCollectOutput(
102 ChildProcess.make(oxlintBin, ["--config", configPath, sourcePath], { cwd: repoRoot }),
103 );
104
105 if (output.exitCode !== 0) {
106 return yield* new OxlintFixtureFailure({
107 exitCode: output.exitCode,
108 stdout: output.stdout,
109 stderr: output.stderr,
110 });
111 }
112
113 return `${output.stdout}${output.stderr}`;
114 }, Effect.scoped);
115
116 const runAndExpectFailure: RuleHarness["runAndExpectFailure"] = (source) =>
117 run(source).pipe(
118 Effect.matchEffect({
119 onFailure: (error) =>
120 OxlintFixtureFailure.is(error)
121 ? Effect.succeed(
122 `oxlint fixture failed with exit code ${error.exitCode}\n${error.stdout}\n${error.stderr}`,
123 )
124 : Effect.fail(error),
125 onSuccess: () => Effect.fail(new OxlintFixtureExpectedFailure({ ruleName })),
126 }),
127 );
128
129 return {
130 run,
131 runAndExpectFailure,
132 valid(name, source) {
133 test(name, (it) => {
134 it.effect("passes", () => run(source));
135 });
136 },
137 invalid(name, source, assertion) {
138 test(name, (it) => {
139 it.effect("reports the rule diagnostic", () =>
140 runAndExpectFailure(source).pipe(
141 Effect.tap((output) =>
142 Effect.sync(() => {
143 assert.match(output, new RegExp(diagnosticRuleName));
144 assertion?.(output);
145 }),
146 ),
147 ),
148 );
149 });
150 },
151 };
152};
oxlint-plugin-t3code/tsconfig.json created+9
...@@ -0,0 +1,9 @@
1{
2 "extends": "../tsconfig.base.json",
3 "compilerOptions": {
4 "composite": true,
5 "types": ["bun", "node"],
6 "lib": ["ESNext", "esnext.disposable"]
7 },
8 "include": ["**/*.ts"]
9}
oxlint-plugin-t3code/utils.ts created+58
...@@ -0,0 +1,58 @@
1import type { ESTree } from "@oxlint/plugins";
2import * as Option from "effect/Option";
3
4type ExpressionWrapper =
5 | ESTree.ChainExpression
6 | ESTree.ParenthesizedExpression
7 | ESTree.TSNonNullExpression
8 | ESTree.TSAsExpression
9 | ESTree.TSTypeAssertion;
10
11type AstNode = ESTree.Node;
12
13const asAstNode = (node: unknown): Option.Option<AstNode> =>
14 typeof node === "object" && node !== null && "type" in node && typeof node.type === "string"
15 ? Option.some(node as AstNode)
16 : Option.none();
17
18const isExpressionWrapper = (node: AstNode): node is ExpressionWrapper =>
19 node.type === "ChainExpression" ||
20 node.type === "ParenthesizedExpression" ||
21 node.type === "TSNonNullExpression" ||
22 node.type === "TSAsExpression" ||
23 node.type === "TSTypeAssertion";
24
25export function unwrapExpression(node: unknown): Option.Option<AstNode> {
26 let current = asAstNode(node);
27
28 while (Option.isSome(current) && isExpressionWrapper(current.value)) {
29 current = asAstNode(current.value.expression);
30 }
31
32 return current;
33}
34
35export function getPropertyName(node: unknown): Option.Option<string> {
36 return Option.flatMap(asAstNode(node), (expression) => {
37 if (expression.type === "Identifier" && typeof expression.name === "string") {
38 return Option.some(expression.name);
39 }
40 if (expression.type === "PrivateIdentifier" && typeof expression.name === "string") {
41 return Option.some(expression.name);
42 }
43 if (expression.type === "Literal" && typeof expression.value === "string") {
44 return Option.some(expression.value);
45 }
46 return Option.none();
47 });
48}
49
50export function isIdentifier(node: Option.Option<AstNode>, name?: string): boolean {
51 if (Option.isNone(node)) return false;
52 const expression = node.value;
53 return (
54 expression.type === "Identifier" &&
55 typeof expression.name === "string" &&
56 (name === undefined || expression.name === name)
57 );
58}
package.json+3-1
...@@ -4,6 +4,7 @@...@@ -4,6 +4,7 @@
4 "workspaces": {4 "workspaces": {
5 "packages": [5 "packages": [
6 "apps/*",6 "apps/*",
7 "oxlint-plugin-t3code",
7 "packages/*",8 "packages/*",
8 "scripts"9 "scripts"
9 ],10 ],
...@@ -61,9 +62,10 @@...@@ -61,9 +62,10 @@
61 },62 },
62 "devDependencies": {63 "devDependencies": {
63 "@effect/language-service": "catalog:",64 "@effect/language-service": "catalog:",
65 "@oxlint/plugins": "^1.63.0",
64 "@types/node": "catalog:",66 "@types/node": "catalog:",
65 "oxfmt": "^0.40.0",67 "oxfmt": "^0.40.0",
66 "oxlint": "^1.55.0",68 "oxlint": "^1.63.0",
67 "turbo": "^2.3.3",69 "turbo": "^2.3.3",
68 "typescript": "catalog:",70 "typescript": "catalog:",
69 "vitest": "catalog:"71 "vitest": "catalog:"
packages/contracts/src/orchestration.test.ts+2-1
...@@ -38,6 +38,7 @@ const decodeThreadTurnStartRequestedPayload = Schema.decodeUnknownEffect(...@@ -38,6 +38,7 @@ const decodeThreadTurnStartRequestedPayload = Schema.decodeUnknownEffect(
38const decodeOrchestrationLatestTurn = Schema.decodeUnknownEffect(OrchestrationLatestTurn);38const decodeOrchestrationLatestTurn = Schema.decodeUnknownEffect(OrchestrationLatestTurn);
39const decodeOrchestrationProposedPlan = Schema.decodeUnknownEffect(OrchestrationProposedPlan);39const decodeOrchestrationProposedPlan = Schema.decodeUnknownEffect(OrchestrationProposedPlan);
40const decodeOrchestrationSession = Schema.decodeUnknownEffect(OrchestrationSession);40const decodeOrchestrationSession = Schema.decodeUnknownEffect(OrchestrationSession);
41const encodeThreadCreatedPayload = Schema.encodeEffect(ThreadCreatedPayload);
4142
42function getOptionValue(43function getOptionValue(
43 options: ReadonlyArray<{ id: string; value: unknown }> | undefined,44 options: ReadonlyArray<{ id: string; value: unknown }> | undefined,
...@@ -485,7 +486,7 @@ it.effect(...@@ -485,7 +486,7 @@ it.effect(
485 updatedAt: "2026-01-01T00:00:00.000Z",486 updatedAt: "2026-01-01T00:00:00.000Z",
486 });487 });
487488
488 const encoded = yield* Schema.encodeEffect(ThreadCreatedPayload)(decoded);489 const encoded = yield* encodeThreadCreatedPayload(decoded);
489 assert.deepStrictEqual(encoded.modelSelection.options, [{ id: "fastMode", value: true }]);490 assert.deepStrictEqual(encoded.modelSelection.options, [{ id: "fastMode", value: true }]);
490 }),491 }),
491);492);
packages/contracts/src/settings.ts+1
...@@ -279,6 +279,7 @@ export const CursorSettings = makeProviderSettingsSchema(...@@ -279,6 +279,7 @@ export const CursorSettings = makeProviderSettingsSchema(
279 },279 },
280);280);
281export type CursorSettings = typeof CursorSettings.Type;281export type CursorSettings = typeof CursorSettings.Type;
282
282export const OpenCodeSettings = makeProviderSettingsSchema(283export const OpenCodeSettings = makeProviderSettingsSchema(
283 {284 {
284 enabled: Schema.Boolean.pipe(285 enabled: Schema.Boolean.pipe(
packages/effect-acp/scripts/generate.ts+21-13
...@@ -28,15 +28,24 @@ interface GeneratedPaths {...@@ -28,15 +28,24 @@ interface GeneratedPaths {
28 readonly metaOutputPath: string;28 readonly metaOutputPath: string;
29}29}
3030
31const UpstreamJsonSchemaSchema = Schema.Struct({
32 $defs: Schema.Record(Schema.String, Schema.Json),
33});
31const MetaJsonSchema = Schema.Struct({34const MetaJsonSchema = Schema.Struct({
32 agentMethods: Schema.Record(Schema.String, Schema.String),35 agentMethods: Schema.Record(Schema.String, Schema.String),
33 clientMethods: Schema.Record(Schema.String, Schema.String),36 clientMethods: Schema.Record(Schema.String, Schema.String),
34 version: Schema.Union([Schema.Number, Schema.String]),37 version: Schema.Union([Schema.Number, Schema.String]),
35});38});
39const encodeAgentMethods = Schema.encodeEffect(
40 Schema.fromJsonString(MetaJsonSchema.fields.agentMethods),
41);
42const encodeClientMethods = Schema.encodeEffect(
43 Schema.fromJsonString(MetaJsonSchema.fields.clientMethods),
44);
45const encodeVersion = Schema.encodeEffect(Schema.fromJsonString(MetaJsonSchema.fields.version));
3646
37const UpstreamJsonSchemaSchema = Schema.Struct({47const decodeUpstreamSchema = Schema.decodeEffect(Schema.fromJsonString(UpstreamJsonSchemaSchema));
38 $defs: Schema.Record(Schema.String, Schema.Json),48const decodeMetaJson = Schema.decodeEffect(Schema.fromJsonString(MetaJsonSchema));
39});
4049
41const getGeneratedPaths = Effect.fn("getGeneratedPaths")(function* () {50const getGeneratedPaths = Effect.fn("getGeneratedPaths")(function* () {
42 const path = yield* Path.Path;51 const path = yield* Path.Path;
...@@ -86,12 +95,9 @@ const downloadSchemas = Effect.fn("downloadSchemas")(function* (tag: string) {...@@ -86,12 +95,9 @@ const downloadSchemas = Effect.fn("downloadSchemas")(function* (tag: string) {
86 );95 );
87});96});
8897
89const readJsonFile = Effect.fn("readJsonFile")(function* <98const readFileString = Effect.fn("readJsonFile")(function* (filePath: string) {
90 S extends Schema.Top & { readonly DecodingServices: never },
91>(schema: S, filePath: string) {
92 const fs = yield* FileSystem.FileSystem;99 const fs = yield* FileSystem.FileSystem;
93 const raw = yield* fs.readFileString(filePath);100 return yield* fs.readFileString(filePath);
94 return yield* Schema.decodeEffect(Schema.fromJsonString(schema))(raw);
95});101});
96102
97const writeGeneratedFiles = Effect.fn("writeGeneratedFiles")(function* (103const writeGeneratedFiles = Effect.fn("writeGeneratedFiles")(function* (
...@@ -201,8 +207,10 @@ const generateSchemas = Effect.fn("generateSchemas")(function* (skipDownload: bo...@@ -201,8 +207,10 @@ const generateSchemas = Effect.fn("generateSchemas")(function* (skipDownload: bo
201 yield* downloadSchemas(CURRENT_SCHEMA_RELEASE);207 yield* downloadSchemas(CURRENT_SCHEMA_RELEASE);
202 }208 }
203209
204 const upstreamSchema = yield* readJsonFile(UpstreamJsonSchemaSchema, upstreamSchemaPath);210 const upstreamSchema = yield* readFileString(upstreamSchemaPath).pipe(
205 const upstreamMeta = yield* readJsonFile(MetaJsonSchema, upstreamMetaPath);211 Effect.flatMap(decodeUpstreamSchema),
212 );
213 const upstreamMeta = yield* readFileString(upstreamMetaPath).pipe(Effect.flatMap(decodeMetaJson));
206 const normalizedDefinitions = Object.fromEntries(214 const normalizedDefinitions = Object.fromEntries(
207 Object.entries(upstreamSchema.$defs).map(([name, schema]) => [215 Object.entries(upstreamSchema.$defs).map(([name, schema]) => [
208 name,216 name,
...@@ -245,11 +253,11 @@ const generateSchemas = Effect.fn("generateSchemas")(function* (skipDownload: bo...@@ -245,11 +253,11 @@ const generateSchemas = Effect.fn("generateSchemas")(function* (skipDownload: bo
245253
246 const metaOutput = [254 const metaOutput = [
247 ...prelude,255 ...prelude,
248 `export const AGENT_METHODS = ${yield* Schema.encodeEffect(Schema.fromJsonString(MetaJsonSchema.fields.agentMethods))(upstreamMeta.agentMethods)} as const;`,256 `export const AGENT_METHODS = ${yield* encodeAgentMethods(upstreamMeta.agentMethods)} as const;`,
249 "",257 "",
250 `export const CLIENT_METHODS = ${yield* Schema.encodeEffect(Schema.fromJsonString(MetaJsonSchema.fields.clientMethods))(upstreamMeta.clientMethods)} as const;`,258 `export const CLIENT_METHODS = ${yield* encodeClientMethods(upstreamMeta.clientMethods)} as const;`,
251 "",259 "",
252 `export const PROTOCOL_VERSION = ${yield* Schema.encodeEffect(Schema.fromJsonString(MetaJsonSchema.fields.version))(upstreamMeta.version)} as const;`,260 `export const PROTOCOL_VERSION = ${yield* encodeVersion(upstreamMeta.version)} as const;`,
253 "",261 "",
254 ].join("\n");262 ].join("\n");
255263
packages/effect-acp/src/_internal/shared.ts+4-2
...@@ -5,6 +5,8 @@ import { RpcClientError } from "effect/unstable/rpc";...@@ -5,6 +5,8 @@ import { RpcClientError } from "effect/unstable/rpc";
55
6import * as AcpSchema from "../_generated/schema.gen.ts";6import * as AcpSchema from "../_generated/schema.gen.ts";
7import * as AcpError from "../errors.ts";7import * as AcpError from "../errors.ts";
8const isError = Schema.is(AcpSchema.Error);
9const isAcpRequestError = Schema.is(AcpError.AcpRequestError);
810
9const formatSchemaIssue = SchemaIssue.makeFormatterDefault();11const formatSchemaIssue = SchemaIssue.makeFormatterDefault();
1012
...@@ -20,7 +22,7 @@ export const callRpc = <A>(...@@ -20,7 +22,7 @@ export const callRpc = <A>(
20 }),22 }),
21 ),23 ),
22 ),24 ),
23 Effect.catchIf(Schema.is(AcpSchema.Error), (error) =>25 Effect.catchIf(isError, (error) =>
24 Effect.fail(AcpError.AcpRequestError.fromProtocolError(error)),26 Effect.fail(AcpError.AcpRequestError.fromProtocolError(error)),
25 ),27 ),
26 );28 );
...@@ -35,7 +37,7 @@ export const runHandler = Effect.fnUntraced(function* <A, B>(...@@ -35,7 +37,7 @@ export const runHandler = Effect.fnUntraced(function* <A, B>(
35 }37 }
36 return yield* handler(payload).pipe(38 return yield* handler(payload).pipe(
37 Effect.mapError((error) =>39 Effect.mapError((error) =>
38 Schema.is(AcpError.AcpRequestError)(error)40 isAcpRequestError(error)
39 ? error.toProtocolError()41 ? error.toProtocolError()
40 : AcpError.AcpRequestError.internalError(error.message).toProtocolError(),42 : AcpError.AcpRequestError.internalError(error.message).toProtocolError(),
41 ),43 ),
packages/effect-acp/src/agent.test.ts+6-6
...@@ -34,6 +34,10 @@ const SessionCancelNotification = jsonRpcNotification(...@@ -34,6 +34,10 @@ const SessionCancelNotification = jsonRpcNotification(
34const ExtPingNotification = jsonRpcNotification("x/ping", Schema.Struct({ count: Schema.Number }));34const ExtPingNotification = jsonRpcNotification("x/ping", Schema.Struct({ count: Schema.Number }));
35const ExtRequest = jsonRpcRequest("x/test", Schema.Struct({ hello: Schema.String }));35const ExtRequest = jsonRpcRequest("x/test", Schema.Struct({ hello: Schema.String }));
36const ExtResponse = jsonRpcResponse(Schema.Struct({ ok: Schema.Boolean }));36const ExtResponse = jsonRpcResponse(Schema.Struct({ ok: Schema.Boolean }));
37const decodeRequestPermissionRequest = Schema.decodeEffect(
38 Schema.fromJsonString(RequestPermissionRequest),
39);
40const decodeInitializeResponse = Schema.decodeEffect(Schema.fromJsonString(InitializeResponse));
3741
38it.effect("effect-acp agent handles core agent requests and outbound client requests", () =>42it.effect("effect-acp agent handles core agent requests and outbound client requests", () =>
39 Effect.gen(function* () {43 Effect.gen(function* () {
...@@ -83,9 +87,7 @@ it.effect("effect-acp agent handles core agent requests and outbound client requ...@@ -83,9 +87,7 @@ it.effect("effect-acp agent handles core agent requests and outbound client requ
83 })87 })
84 .pipe(Effect.forkScoped);88 .pipe(Effect.forkScoped);
8589
86 const permissionRequest = yield* Schema.decodeEffect(90 const permissionRequest = yield* decodeRequestPermissionRequest(yield* Queue.take(output));
87 Schema.fromJsonString(RequestPermissionRequest),
88 )(yield* Queue.take(output));
89 assert.equal(permissionRequest.jsonrpc, "2.0");91 assert.equal(permissionRequest.jsonrpc, "2.0");
90 assert.equal(permissionRequest.method, "session/request_permission");92 assert.equal(permissionRequest.method, "session/request_permission");
91 assert.deepEqual(permissionRequest.params, {93 assert.deepEqual(permissionRequest.params, {
...@@ -136,9 +138,7 @@ it.effect("effect-acp agent handles core agent requests and outbound client requ...@@ -136,9 +138,7 @@ it.effect("effect-acp agent handles core agent requests and outbound client requ
136 }),138 }),
137 );139 );
138140
139 const initResponse = yield* Schema.decodeEffect(Schema.fromJsonString(InitializeResponse))(141 const initResponse = yield* decodeInitializeResponse(yield* Queue.take(output));
140 yield* Queue.take(output),
141 );
142 assert.deepEqual(initResponse, {142 assert.deepEqual(initResponse, {
143 jsonrpc: "2.0",143 jsonrpc: "2.0",
144 id: 2,144 id: 2,
packages/effect-acp/src/protocol.test.ts+23-22
...@@ -41,6 +41,13 @@ const RequestPermissionRequest = jsonRpcRequest(...@@ -41,6 +41,13 @@ const RequestPermissionRequest = jsonRpcRequest(
41const RequestPermissionResponse = jsonRpcResponse(AcpSchema.RequestPermissionResponse);41const RequestPermissionResponse = jsonRpcResponse(AcpSchema.RequestPermissionResponse);
42const ExtRequest = jsonRpcRequest("x/test", Schema.Struct({ hello: Schema.String }));42const ExtRequest = jsonRpcRequest("x/test", Schema.Struct({ hello: Schema.String }));
43const ExtResponse = jsonRpcResponse(Schema.Struct({ ok: Schema.Boolean }));43const ExtResponse = jsonRpcResponse(Schema.Struct({ ok: Schema.Boolean }));
44const decodeSessionCancelNotification = Schema.decodeEffect(
45 Schema.fromJsonString(SessionCancelNotification),
46);
47const decodeExtRequest = Schema.decodeEffect(Schema.fromJsonString(ExtRequest));
48const decodeRequestPermissionResponse = Schema.decodeEffect(
49 Schema.fromJsonString(RequestPermissionResponse),
50);
44const mockPeerPath = Effect.map(Effect.service(Path.Path), (path) =>51const mockPeerPath = Effect.map(Effect.service(Path.Path), (path) =>
45 path.join(import.meta.dirname, "../test/fixtures/acp-mock-peer.ts"),52 path.join(import.meta.dirname, "../test/fixtures/acp-mock-peer.ts"),
46);53);
...@@ -79,16 +86,13 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {...@@ -79,16 +86,13 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {
7986
80 yield* transport.notify("session/cancel", { sessionId: "session-1" });87 yield* transport.notify("session/cancel", { sessionId: "session-1" });
81 const outbound = yield* Queue.take(output);88 const outbound = yield* Queue.take(output);
82 assert.deepEqual(89 assert.deepEqual(yield* decodeSessionCancelNotification(outbound), {
83 yield* Schema.decodeEffect(Schema.fromJsonString(SessionCancelNotification))(outbound),90 jsonrpc: "2.0",
84 {91 method: "session/cancel",
85 jsonrpc: "2.0",92 params: {
86 method: "session/cancel",93 sessionId: "session-1",
87 params: {
88 sessionId: "session-1",
89 },
90 },94 },
91 );95 });
9296
93 yield* Queue.offer(97 yield* Queue.offer(
94 input,98 input,
...@@ -200,7 +204,7 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {...@@ -200,7 +204,7 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {
200 .request("x/test", { hello: "world" })204 .request("x/test", { hello: "world" })
201 .pipe(Effect.forkScoped);205 .pipe(Effect.forkScoped);
202 const outbound = yield* Queue.take(output);206 const outbound = yield* Queue.take(output);
203 assert.deepEqual(yield* Schema.decodeEffect(Schema.fromJsonString(ExtRequest))(outbound), {207 assert.deepEqual(yield* decodeExtRequest(outbound), {
204 jsonrpc: "2.0",208 jsonrpc: "2.0",
205 id: 1,209 id: 1,
206 method: "x/test",210 method: "x/test",
...@@ -288,19 +292,16 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {...@@ -288,19 +292,16 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {
288 });292 });
289293
290 const outbound = yield* Queue.take(output);294 const outbound = yield* Queue.take(output);
291 assert.deepEqual(295 assert.deepEqual(yield* decodeRequestPermissionResponse(outbound), {
292 yield* Schema.decodeEffect(Schema.fromJsonString(RequestPermissionResponse))(outbound),296 jsonrpc: "2.0",
293 {297 id: 0,
294 jsonrpc: "2.0",298 result: {
295 id: 0,299 outcome: {
296 result: {300 outcome: "selected",
297 outcome: {301 optionId: "allow",
298 outcome: "selected",
299 optionId: "allow",
300 },
301 },302 },
302 },303 },
303 );304 });
304 }),305 }),
305 );306 );
306307
...@@ -321,7 +322,7 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {...@@ -321,7 +322,7 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {
321 .request("x/test", { hello: "world" })322 .request("x/test", { hello: "world" })
322 .pipe(Effect.forkScoped);323 .pipe(Effect.forkScoped);
323 const outbound = yield* Queue.take(output);324 const outbound = yield* Queue.take(output);
324 assert.deepEqual(yield* Schema.decodeEffect(Schema.fromJsonString(ExtRequest))(outbound), {325 assert.deepEqual(yield* decodeExtRequest(outbound), {
325 jsonrpc: "2.0",326 jsonrpc: "2.0",
326 id: 1,327 id: 1,
327 method: "x/test",328 method: "x/test",
packages/effect-acp/src/protocol.ts+4-4
...@@ -16,6 +16,8 @@ import * as RpcServer from "effect/unstable/rpc/RpcServer";...@@ -16,6 +16,8 @@ import * as RpcServer from "effect/unstable/rpc/RpcServer";
16import * as AcpSchema from "./_generated/schema.gen.ts";16import * as AcpSchema from "./_generated/schema.gen.ts";
17import { CLIENT_METHODS } from "./_generated/meta.gen.ts";17import { CLIENT_METHODS } from "./_generated/meta.gen.ts";
18import * as AcpError from "./errors.ts";18import * as AcpError from "./errors.ts";
19const isAcpError = Schema.is(AcpError.AcpError);
20const isAcpRequestError = Schema.is(AcpError.AcpRequestError);
1921
20export interface AcpProtocolLogEvent {22export interface AcpProtocolLogEvent {
21 readonly direction: "incoming" | "outgoing";23 readonly direction: "incoming" | "outgoing";
...@@ -408,7 +410,7 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi...@@ -408,7 +410,7 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi
408 ),410 ),
409 Effect.matchEffect({411 Effect.matchEffect({
410 onFailure: (error) => {412 onFailure: (error) => {
411 const normalized: AcpError.AcpError = Schema.is(AcpError.AcpError)(error)413 const normalized: AcpError.AcpError = isAcpError(error)
412 ? error414 ? error
413 : new AcpError.AcpTransportError({415 : new AcpError.AcpTransportError({
414 detail: error instanceof Error ? error.message : String(error),416 detail: error instanceof Error ? error.message : String(error),
...@@ -521,9 +523,7 @@ function isProtocolError(...@@ -521,9 +523,7 @@ function isProtocolError(
521}523}
522524
523function normalizeToRequestError(error: AcpError.AcpError): AcpError.AcpRequestError {525function normalizeToRequestError(error: AcpError.AcpError): AcpError.AcpRequestError {
524 return Schema.is(AcpError.AcpRequestError)(error)526 return isAcpRequestError(error) ? error : AcpError.AcpRequestError.internalError(error.message);
525 ? error
526 : AcpError.AcpRequestError.internalError(error.message);
527}527}
528528
529function toRpcClientError(error: AcpError.AcpError): RpcClientError.RpcClientError {529function toRpcClientError(error: AcpError.AcpError): RpcClientError.RpcClientError {
packages/effect-codex-app-server/scripts/generate.ts+4-2
...@@ -38,6 +38,8 @@ const JsonSchemaDocument = Schema.StructWithRest(...@@ -38,6 +38,8 @@ const JsonSchemaDocument = Schema.StructWithRest(
38 }),38 }),
39 [Schema.Record(Schema.String, Schema.Json)],39 [Schema.Record(Schema.String, Schema.Json)],
40);40);
41const decodeGithubContentEntries = Schema.decodeEffect(Schema.fromJsonString(GithubContentEntries));
42const decodeJsonSchemaDocument = Schema.decodeEffect(Schema.fromJsonString(JsonSchemaDocument));
4143
42interface GeneratedPaths {44interface GeneratedPaths {
43 readonly generatedDir: string;45 readonly generatedDir: string;
...@@ -178,7 +180,7 @@ const fetchText = Effect.fn("fetchText")(function* (url: string) {...@@ -178,7 +180,7 @@ const fetchText = Effect.fn("fetchText")(function* (url: string) {
178180
179const fetchDirectoryEntries = Effect.fn("fetchDirectoryEntries")(function* (path: string) {181const fetchDirectoryEntries = Effect.fn("fetchDirectoryEntries")(function* (path: string) {
180 const raw = yield* fetchText(`${GITHUB_API_BASE}/${path}?ref=${UPSTREAM_REF}`);182 const raw = yield* fetchText(`${GITHUB_API_BASE}/${path}?ref=${UPSTREAM_REF}`);
181 return yield* Schema.decodeEffect(Schema.fromJsonString(GithubContentEntries))(raw);183 return yield* decodeGithubContentEntries(raw);
182});184});
183185
184function collectSchemaEntries(186function collectSchemaEntries(
...@@ -545,7 +547,7 @@ const generateFiles = Effect.fn("generateFiles")(function* () {...@@ -545,7 +547,7 @@ const generateFiles = Effect.fn("generateFiles")(function* () {
545547
546 for (const file of jsonSchemaFiles) {548 for (const file of jsonSchemaFiles) {
547 const raw = yield* fetchText(file.downloadUrl);549 const raw = yield* fetchText(file.downloadUrl);
548 const parsed = yield* Schema.decodeEffect(Schema.fromJsonString(JsonSchemaDocument))(raw);550 const parsed = yield* decodeJsonSchemaDocument(raw);
549 const localDefinitionNames = new Map(551 const localDefinitionNames = new Map(
550 Object.keys(parsed.definitions ?? {}).map((definitionName) => [552 Object.keys(parsed.definitions ?? {}).map((definitionName) => [
551 definitionName,553 definitionName,
packages/effect-codex-app-server/src/errors.ts+2-1
...@@ -143,9 +143,10 @@ export const CodexAppServerError = Schema.Union([...@@ -143,9 +143,10 @@ export const CodexAppServerError = Schema.Union([
143]);143]);
144144
145export type CodexAppServerError = typeof CodexAppServerError.Type;145export type CodexAppServerError = typeof CodexAppServerError.Type;
146const isCodexAppServerRequestError = Schema.is(CodexAppServerRequestError);
146147
147export function normalizeToRequestError(error: CodexAppServerError): CodexAppServerRequestError {148export function normalizeToRequestError(error: CodexAppServerError): CodexAppServerRequestError {
148 return Schema.is(CodexAppServerRequestError)(error)149 return isCodexAppServerRequestError(error)
149 ? error150 ? error
150 : CodexAppServerRequestError.internalError(error.message);151 : CodexAppServerRequestError.internalError(error.message);
151}152}
packages/effect-codex-app-server/src/protocol.test.ts+2-2
...@@ -11,11 +11,11 @@ import { assert, it } from "@effect/vitest";...@@ -11,11 +11,11 @@ import { assert, it } from "@effect/vitest";
11import * as CodexError from "./errors.ts";11import * as CodexError from "./errors.ts";
12import * as CodexProtocol from "./protocol.ts";12import * as CodexProtocol from "./protocol.ts";
13import { makeInMemoryStdio } from "./_internal/stdio.ts";13import { makeInMemoryStdio } from "./_internal/stdio.ts";
14const encodeUnknownJsonString = Schema.encodeUnknownSync(Schema.UnknownFromJsonString);
1415
15const encoder = new TextEncoder();16const encoder = new TextEncoder();
1617
17const encodeJsonl = (value: unknown) =>18const encodeJsonl = (value: unknown) => encoder.encode(`${encodeUnknownJsonString(value)}\n`);
18 encoder.encode(`${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)}\n`);
1919
20const decodeJson = Schema.decodeEffect(Schema.UnknownFromJsonString);20const decodeJson = Schema.decodeEffect(Schema.UnknownFromJsonString);
2121
packages/effect-codex-app-server/src/protocol.ts+6-3
...@@ -10,6 +10,9 @@ import * as Stream from "effect/Stream";...@@ -10,6 +10,9 @@ import * as Stream from "effect/Stream";
1010
11import * as CodexError from "./errors.ts";11import * as CodexError from "./errors.ts";
12import { JsonRpcId, JsonRpcResponseEnvelope } from "./_internal/shared.ts";12import { JsonRpcId, JsonRpcResponseEnvelope } from "./_internal/shared.ts";
13const isJsonRpcId = Schema.is(JsonRpcId);
14const isJsonRpcResponseEnvelope = Schema.is(JsonRpcResponseEnvelope);
15const isCodexAppServerError = Schema.is(CodexError.CodexAppServerError);
1316
14export interface CodexAppServerProtocolLogEvent {17export interface CodexAppServerProtocolLogEvent {
15 readonly direction: "incoming" | "outgoing";18 readonly direction: "incoming" | "outgoing";
...@@ -72,7 +75,7 @@ function isIncomingRequest(value: unknown): value is CodexAppServerIncomingReque...@@ -72,7 +75,7 @@ function isIncomingRequest(value: unknown): value is CodexAppServerIncomingReque
72 if (!isObject(value) || typeof value.method !== "string") {75 if (!isObject(value) || typeof value.method !== "string") {
73 return false;76 return false;
74 }77 }
75 return Schema.is(JsonRpcId)(value.id);78 return isJsonRpcId(value.id);
76}79}
7780
78function isIncomingNotification(value: unknown): value is CodexAppServerIncomingNotification {81function isIncomingNotification(value: unknown): value is CodexAppServerIncomingNotification {
...@@ -80,7 +83,7 @@ function isIncomingNotification(value: unknown): value is CodexAppServerIncoming...@@ -80,7 +83,7 @@ function isIncomingNotification(value: unknown): value is CodexAppServerIncoming
80}83}
8184
82function isIncomingResponse(value: unknown): value is typeof JsonRpcResponseEnvelope.Type {85function isIncomingResponse(value: unknown): value is typeof JsonRpcResponseEnvelope.Type {
83 return Schema.is(JsonRpcResponseEnvelope)(value);86 return isJsonRpcResponseEnvelope(value);
84}87}
8588
86const encodeJsonString = Schema.encodeUnknownEffect(Schema.UnknownFromJsonString);89const encodeJsonString = Schema.encodeUnknownEffect(Schema.UnknownFromJsonString);
...@@ -114,7 +117,7 @@ const decodeWireMessage = (...@@ -114,7 +117,7 @@ const decodeWireMessage = (
114 );117 );
115118
116const normalizeIncomingError = (error: unknown, detail: string): CodexError.CodexAppServerError =>119const normalizeIncomingError = (error: unknown, detail: string): CodexError.CodexAppServerError =>
117 Schema.is(CodexError.CodexAppServerError)(error)120 isCodexAppServerError(error)
118 ? error121 ? error
119 : new CodexError.CodexAppServerTransportError({122 : new CodexError.CodexAppServerTransportError({
120 detail,123 detail,
packages/shared/src/observability.test.ts+185-180
...@@ -80,6 +80,8 @@ const makeTestLayer = (tracePath: string) =>...@@ -80,6 +80,8 @@ const makeTestLayer = (tracePath: string) =>
80 Layer.succeed(References.MinimumLogLevel, "Info"),80 Layer.succeed(References.MinimumLogLevel, "Info"),
81 );81 );
8282
83const nodeServicesIt = it.layer(NodeServices.layer);
84
83describe("observability", () => {85describe("observability", () => {
84 it("normalizes circular arrays, maps, and sets without recursing forever", () => {86 it("normalizes circular arrays, maps, and sets without recursing forever", () => {
85 const array: Array<unknown> = ["alpha"];87 const array: Array<unknown> = ["alpha"];
...@@ -118,191 +120,194 @@ describe("observability", () => {...@@ -118,191 +120,194 @@ describe("observability", () => {
118 );120 );
119 });121 });
120122
121 it.effect("flushes buffered trace records on close", () =>123 nodeServicesIt("node services", (it) => {
122 Effect.scoped(124 it.effect("flushes buffered trace records on close", () =>
123 Effect.gen(function* () {125 Effect.scoped(
124 const fileSystem = yield* FileSystem.FileSystem;126 Effect.gen(function* () {
125 const path = yield* Path.Path;127 const fileSystem = yield* FileSystem.FileSystem;
126 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });128 const path = yield* Path.Path;
127 const tracePath = path.join(tempDir, "shared.trace.ndjson");129 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });
128130 const tracePath = path.join(tempDir, "shared.trace.ndjson");
129 const sink = yield* makeTraceSink({131
130 filePath: tracePath,132 const sink = yield* makeTraceSink({
131 maxBytes: 1024,133 filePath: tracePath,
132 maxFiles: 2,134 maxBytes: 1024,
133 batchWindowMs: 10_000,135 maxFiles: 2,
134 });136 batchWindowMs: 10_000,
135137 });
136 sink.push(makeRecord("alpha"));138
137 sink.push(makeRecord("beta"));139 sink.push(makeRecord("alpha"));
138 yield* sink.close();140 sink.push(makeRecord("beta"));
139141 yield* sink.close();
140 const lines = yield* readTraceRecords(tracePath);142
141143 const lines = yield* readTraceRecords(tracePath);
142 assert.equal(lines.length, 2);144
143 assert.equal(lines[0]?.name, "alpha");145 assert.equal(lines.length, 2);
144 assert.equal(lines[1]?.name, "beta");146 assert.equal(lines[0]?.name, "alpha");
145 }).pipe(Effect.provide(NodeServices.layer)),147 assert.equal(lines[1]?.name, "beta");
146 ),148 }),
147 );149 ),
150 );
148151
149 it.effect("rotates the trace file when the configured max size is exceeded", () =>152 it.effect("rotates the trace file when the configured max size is exceeded", () =>
150 Effect.scoped(153 Effect.scoped(
151 Effect.gen(function* () {154 Effect.gen(function* () {
152 const fileSystem = yield* FileSystem.FileSystem;155 const fileSystem = yield* FileSystem.FileSystem;
153 const path = yield* Path.Path;156 const path = yield* Path.Path;
154 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });157 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });
155 const tracePath = path.join(tempDir, "shared.trace.ndjson");158 const tracePath = path.join(tempDir, "shared.trace.ndjson");
156159
157 const sink = yield* makeTraceSink({160 const sink = yield* makeTraceSink({
158 filePath: tracePath,161 filePath: tracePath,
159 maxBytes: 180,162 maxBytes: 180,
160 maxFiles: 2,163 maxFiles: 2,
161 batchWindowMs: 10_000,164 batchWindowMs: 10_000,
162 });165 });
163166
164 for (let index = 0; index < 8; index += 1) {167 for (let index = 0; index < 8; index += 1) {
165 sink.push(makeRecord("rotate", `${index}-${"x".repeat(48)}`));168 sink.push(makeRecord("rotate", `${index}-${"x".repeat(48)}`));
166 yield* sink.flush;169 yield* sink.flush;
167 }170 }
168 yield* sink.close();171 yield* sink.close();
169172
170 const matchingFiles = (yield* fileSystem.readDirectory(tempDir))173 const matchingFiles = (yield* fileSystem.readDirectory(tempDir))
171 .filter(174 .filter(
172 (entry) => entry === "shared.trace.ndjson" || entry.startsWith("shared.trace.ndjson."),175 (entry) =>
173 )176 entry === "shared.trace.ndjson" || entry.startsWith("shared.trace.ndjson."),
174 .toSorted();177 )
175178 .toSorted();
176 assert.equal(179
177 matchingFiles.some((entry) => entry === "shared.trace.ndjson.1"),180 assert.equal(
178 true,181 matchingFiles.some((entry) => entry === "shared.trace.ndjson.1"),
179 );182 true,
180 assert.equal(183 );
181 matchingFiles.some((entry) => entry === "shared.trace.ndjson.3"),184 assert.equal(
182 false,185 matchingFiles.some((entry) => entry === "shared.trace.ndjson.3"),
183 );186 false,
184 }).pipe(Effect.provide(NodeServices.layer)),187 );
185 ),188 }),
186 );189 ),
190 );
187191
188 it.effect("drops only the invalid trace record when serialization fails", () =>192 it.effect("drops only the invalid trace record when serialization fails", () =>
189 Effect.scoped(193 Effect.scoped(
190 Effect.gen(function* () {194 Effect.gen(function* () {
191 const fileSystem = yield* FileSystem.FileSystem;195 const fileSystem = yield* FileSystem.FileSystem;
192 const path = yield* Path.Path;196 const path = yield* Path.Path;
193 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });197 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });
194 const tracePath = path.join(tempDir, "shared.trace.ndjson");198 const tracePath = path.join(tempDir, "shared.trace.ndjson");
195199
196 const sink = yield* makeTraceSink({200 const sink = yield* makeTraceSink({
197 filePath: tracePath,201 filePath: tracePath,
198 maxBytes: 1024,202 maxBytes: 1024,
199 maxFiles: 2,203 maxFiles: 2,
200 batchWindowMs: 10_000,204 batchWindowMs: 10_000,
201 });205 });
202206
203 const circular: Array<unknown> = [];207 const circular: Array<unknown> = [];
204 circular.push(circular);208 circular.push(circular);
205209
206 sink.push(makeRecord("alpha"));210 sink.push(makeRecord("alpha"));
207 sink.push({211 sink.push({
208 ...makeRecord("invalid"),212 ...makeRecord("invalid"),
209 attributes: {213 attributes: {
210 circular,214 circular,
211 },215 },
212 } as TraceRecord);216 } as TraceRecord);
213 sink.push(makeRecord("beta"));217 sink.push(makeRecord("beta"));
214 yield* sink.close();218 yield* sink.close();
215219
216 const lines = yield* readTraceRecords(tracePath);220 const lines = yield* readTraceRecords(tracePath);
217221
218 assert.deepStrictEqual(222 assert.deepStrictEqual(
219 lines.map((line) => line.name),223 lines.map((line) => line.name),
220 ["alpha", "beta"],224 ["alpha", "beta"],
221 );225 );
222 }).pipe(Effect.provide(NodeServices.layer)),226 }),
223 ),227 ),
224 );228 );
225229
226 it.effect("writes nested spans to disk and captures log messages as span events", () =>230 it.effect("writes nested spans to disk and captures log messages as span events", () =>
227 Effect.scoped(231 Effect.scoped(
228 Effect.gen(function* () {232 Effect.gen(function* () {
229 const fileSystem = yield* FileSystem.FileSystem;233 const fileSystem = yield* FileSystem.FileSystem;
230 const path = yield* Path.Path;234 const path = yield* Path.Path;
231 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" });235 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" });
232 const tracePath = path.join(tempDir, "shared.trace.ndjson");236 const tracePath = path.join(tempDir, "shared.trace.ndjson");
233237
234 yield* Effect.scoped(238 yield* Effect.scoped(
235 Effect.gen(function* () {239 Effect.gen(function* () {
236 const program = Effect.gen(function* () {240 const program = Effect.gen(function* () {
237 yield* Effect.annotateCurrentSpan({
238 "demo.parent": true,
239 });
240 yield* Effect.logInfo("parent event");
241 yield* Effect.gen(function* () {
242 yield* Effect.annotateCurrentSpan({241 yield* Effect.annotateCurrentSpan({
243 "demo.child": true,242 "demo.parent": true,
244 });243 });
245 yield* Effect.logInfo("child event");244 yield* Effect.logInfo("parent event");
246 }).pipe(Effect.withSpan("child-span"));245 yield* Effect.gen(function* () {
247 }).pipe(Effect.withSpan("parent-span"));246 yield* Effect.annotateCurrentSpan({
248247 "demo.child": true,
249 yield* program.pipe(Effect.provide(makeTestLayer(tracePath)));248 });
250 }),249 yield* Effect.logInfo("child event");
251 );250 }).pipe(Effect.withSpan("child-span"));
252251 }).pipe(Effect.withSpan("parent-span"));
253 const records = yield* readTraceRecords(tracePath);252
254 assert.equal(records.length, 2);253 yield* program.pipe(Effect.provide(makeTestLayer(tracePath)));
255254 }),
256 const parent = records.find((record) => record.name === "parent-span");255 );
257 const child = records.find((record) => record.name === "child-span");256
258257 const records = yield* readTraceRecords(tracePath);
259 assert.notEqual(parent, undefined);258 assert.equal(records.length, 2);
260 assert.notEqual(child, undefined);259
261 if (!parent || !child) {260 const parent = records.find((record) => record.name === "parent-span");
262 return;261 const child = records.find((record) => record.name === "child-span");
263 }262
264263 assert.notEqual(parent, undefined);
265 assert.equal(child.parentSpanId, parent.spanId);264 assert.notEqual(child, undefined);
266 assert.equal(parent.attributes["demo.parent"], true);265 if (!parent || !child) {
267 assert.equal(child.attributes["demo.child"], true);266 return;
268 assert.equal(267 }
269 parent.events.some((event) => event.name === "parent event"),268
270 true,269 assert.equal(child.parentSpanId, parent.spanId);
271 );270 assert.equal(parent.attributes["demo.parent"], true);
272 assert.equal(271 assert.equal(child.attributes["demo.child"], true);
273 child.events.some((event) => event.name === "child event"),272 assert.equal(
274 true,273 parent.events.some((event) => event.name === "parent event"),
275 );274 true,
276 assert.equal(275 );
277 child.events.some((event) => event.attributes["effect.logLevel"] === "INFO"),276 assert.equal(
278 true,277 child.events.some((event) => event.name === "child event"),
279 );278 true,
280 }).pipe(Effect.provide(NodeServices.layer)),279 );
281 ),280 assert.equal(
282 );281 child.events.some((event) => event.attributes["effect.logLevel"] === "INFO"),
282 true,
283 );
284 }),
285 ),
286 );
283287
284 it.effect("serializes interrupted spans with an interrupted exit status", () =>288 it.effect("serializes interrupted spans with an interrupted exit status", () =>
285 Effect.scoped(289 Effect.scoped(
286 Effect.gen(function* () {290 Effect.gen(function* () {
287 const fileSystem = yield* FileSystem.FileSystem;291 const fileSystem = yield* FileSystem.FileSystem;
288 const path = yield* Path.Path;292 const path = yield* Path.Path;
289 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" });293 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" });
290 const tracePath = path.join(tempDir, "shared.trace.ndjson");294 const tracePath = path.join(tempDir, "shared.trace.ndjson");
291295
292 yield* Effect.scoped(296 yield* Effect.scoped(
293 Effect.exit(297 Effect.exit(
294 Effect.interrupt.pipe(298 Effect.interrupt.pipe(
295 Effect.withSpan("interrupt-span"),299 Effect.withSpan("interrupt-span"),
296 Effect.provide(makeTestLayer(tracePath)),300 Effect.provide(makeTestLayer(tracePath)),
301 ),
297 ),302 ),
298 ),303 );
299 );304
300305 const records = yield* readTraceRecords(tracePath);
301 const records = yield* readTraceRecords(tracePath);306 assert.equal(records.length, 1);
302 assert.equal(records.length, 1);307 assert.equal(records[0]?.name, "interrupt-span");
303 assert.equal(records[0]?.name, "interrupt-span");308 assert.equal(records[0]?.exit?._tag, "Interrupted");
304 assert.equal(records[0]?.exit?._tag, "Interrupted");309 }),
305 }).pipe(Effect.provide(NodeServices.layer)),310 ),
306 ),311 );
307 );312 });
308});313});
packages/shared/src/serverSettings.ts+6-5
...@@ -1,10 +1,12 @@...@@ -1,10 +1,12 @@
1import { ServerSettings, type ServerSettingsPatch } from "@t3tools/contracts";1import { ServerSettings, type ServerSettingsPatch } from "@t3tools/contracts";
2import * as Option from "effect/Option";
2import * as Schema from "effect/Schema";3import * as Schema from "effect/Schema";
3import { deepMerge } from "./Struct.ts";4import { deepMerge } from "./Struct.ts";
4import { fromLenientJson } from "./schemaJson.ts";5import { fromLenientJson } from "./schemaJson.ts";
5import { createModelSelection } from "./model.ts";6import { createModelSelection } from "./model.ts";
67
7const ServerSettingsJson = fromLenientJson(ServerSettings);8const ServerSettingsJson = fromLenientJson(ServerSettings);
9const decodeServerSettingsJson = Schema.decodeUnknownOption(ServerSettingsJson);
810
9export interface PersistedServerObservabilitySettings {11export interface PersistedServerObservabilitySettings {
10 readonly otlpTracesUrl: string | undefined;12 readonly otlpTracesUrl: string | undefined;
...@@ -33,12 +35,11 @@ export function extractPersistedServerObservabilitySettings(input: {...@@ -33,12 +35,11 @@ export function extractPersistedServerObservabilitySettings(input: {
33export function parsePersistedServerObservabilitySettings(35export function parsePersistedServerObservabilitySettings(
34 raw: string,36 raw: string,
35): PersistedServerObservabilitySettings {37): PersistedServerObservabilitySettings {
36 try {38 const decoded = decodeServerSettingsJson(raw);
37 const decoded = Schema.decodeUnknownSync(ServerSettingsJson)(raw);39 if (Option.isSome(decoded)) {
38 return extractPersistedServerObservabilitySettings(decoded);40 return extractPersistedServerObservabilitySettings(decoded.value);
39 } catch {
40 return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined };
41 }41 }
42 return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined };
42}43}
4344
44function shouldReplaceTextGenerationModelSelection(45function shouldReplaceTextGenerationModelSelection(
scripts/release-smoke.ts+1
...@@ -24,6 +24,7 @@ const workspaceFiles = [...@@ -24,6 +24,7 @@ const workspaceFiles = [
24 "apps/desktop/package.json",24 "apps/desktop/package.json",
25 "apps/web/package.json",25 "apps/web/package.json",
26 "apps/marketing/package.json",26 "apps/marketing/package.json",
27 "oxlint-plugin-t3code/package.json",
27 "packages/client-runtime/package.json",28 "packages/client-runtime/package.json",
28 "packages/contracts/package.json",29 "packages/contracts/package.json",
29 "packages/shared/package.json",30 "packages/shared/package.json",
scripts/update-release-package-versions.ts+3-2
...@@ -26,7 +26,7 @@ interface UpdateReleasePackageVersionsOptions {...@@ -26,7 +26,7 @@ interface UpdateReleasePackageVersionsOptions {
26const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);26const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);
27const PackageJsonPrettyJson = fromJsonStringPretty(PackageJsonSchema);27const PackageJsonPrettyJson = fromJsonStringPretty(PackageJsonSchema);
28const decodePackageJson = Schema.decodeUnknownEffect(PackageJsonPrettyJson);28const decodePackageJson = Schema.decodeUnknownEffect(PackageJsonPrettyJson);
29const encodePackageJson = Schema.encodeSync(PackageJsonPrettyJson);29const encodePackageJson = Schema.encodeEffect(PackageJsonPrettyJson);
3030
31export const updateReleasePackageVersions = Effect.fn("updateReleasePackageVersions")(function* (31export const updateReleasePackageVersions = Effect.fn("updateReleasePackageVersions")(function* (
32 version: string,32 version: string,
...@@ -44,7 +44,8 @@ export const updateReleasePackageVersions = Effect.fn("updateReleasePackageVersi...@@ -44,7 +44,8 @@ export const updateReleasePackageVersions = Effect.fn("updateReleasePackageVersi
44 continue;44 continue;
45 }45 }
4646
47 yield* fs.writeFileString(filePath, `${encodePackageJson({ ...packageJson, version })}\n`);47 const packageJsonString = yield* encodePackageJson({ ...packageJson, version });
48 yield* fs.writeFileString(filePath, `${packageJsonString}\n`);
48 changed = true;49 changed = true;
49 }50 }
5051