Webページの移動やフォーム入力、クリックをエージェントに任せ、情報抽出や画面保存、操作前後の比較まで行います。ブラウザ作業の自動化やWebアプリの確認に使えるスキル。
- フォーム入力やクリックを自動化したいとき
- Webページのテキスト抽出
- ページのスクリーンショット・PDF保存
既存のoRPC処理を一件ずつEffectへ移行し、ルートや応答形式、認可条件を保ちながら、DB操作・エラー処理・ログ・ルーター接続を置き換えるスキル。
Migrate a single oRPC procedure from classic async handler to Effect-backed implementation. Use when converting procedures one at a time to maintain incremental, reviewable changes.
実行スコア 未検証
インストール方法を見る既存のoRPCのAPI処理を一件ずつ、非同期処理やエラーを組み立てるライブラリEffectを使う実装へ移行します。ルート、入出力の仕様、認可条件を保ちながら、サービスの取得、DBクエリ、トランザクション、並列処理、ログを置き換えます。新しい.effect.tsファイルの作成からルーターの差し替え、検証後の元ファイル削除まで扱います。
API全体を段階的に移行し、一件の処理ごとにコミットやPRを分けてレビューしたい開発者に向いています。元の課金ミドルウェアやエラー応答も確認するため、既存の振る舞いを保つ移行の手順として使えます。
対象の依存サービスがAppRuntimeServicesで提供されている必要があります。未対応のサービスを使う処理は対象外です。Stripeでは公開されたStripeServiceを使い、effect-program-designに従う指定があります。検証にはBunを使い、Docker環境が必要なテストはCIで実行する前提です。
この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。
インストールする前に、エージェントに与えられる指示の中身を確認できます。
Converts a single classic oRPC procedure to an Effect-backed implementation while preserving route shape, response contract, and authorization semantics.
from 'effect' import)IMPORTANT: Always check the source of truth for available services:
# Check AppRuntimeServices type for available Effect services
cat apps/riptide-api/src/effects/app-runtime.ts | head -50
Only migrate procedures that use services available in appRuntime. The AppRuntimeServices type in apps/riptide-api/src/effects/app-runtime.ts is the authoritative list.
Key services to look for:
PostgresDb - for database access (replaces context.services.db)S3 - for S3 operations (replaces context.services.s3)StripeService - for Stripe billing operations (replaces eligible context.services.stripe usage)effects/services/ directory for other available servicesIf a procedure uses a classic service not in AppRuntimeServices, do NOT migrate it.
Before starting, verify the target procedure is eligible:
# 1. Check what services the procedure uses
rg 'services\.' <file>
# 2. Check what Effect services are available in the runtime
cat apps/riptide-api/src/effects/app-runtime.ts | grep -A20 'AppRuntimeServices'
# 3. Confirm procedure hasn't already been migrated
rg "from 'effect'" <file>
# If matches, procedure is already migrated
If the procedure uses any service from context.services that doesn't have a corresponding Effect service in AppRuntimeServices, do NOT migrate it.
Stripe procedures are eligible only when they can call the public Effect StripeService from
apps/riptide-api/src/effects/services/stripe/stripe.service.ts. Do not call the Stripe TypeScript
SDK directly from a migrated oRPC procedure. Load and follow the effect-program-design skill for
Stripe migrations. STRIPE_SECRET_KEY may only be consumed through Config.redacted in the Stripe
HTTP adapter and must never be printed, inspected, or threaded through procedure code. kv remains
unavailable; there is no generic KvService for migrated procedures.
Read the source file and understand:
apiBaseOrgRequired, daemonBase, apiBaseForOrganizationAdmin, etc.)comments.delete, settings.get)Map the classic base to its Effect equivalent:
| Classic Base | Effect Base |
|---|---|
apiBaseOrgRequired | effectOrpcBaseOrganizationRequired |
apiBasePublic | effectOrpcBasePublic |
apiBaseForOrganizationAdmin | effectOrpcBaseForOrganizationAdmin |
daemonBase | effectOrpcDaemonBase |
All Effect bases are exported from apps/riptide-api/src/orpc/effect-base.ts.
Create a new file with .effect.ts suffix alongside the original:
apps/riptide-api/src/orpc/api/comments/delete.ts # original
apps/riptide-api/src/orpc/api/comments/delete.effect.ts # new Effect version
Follow the golden pattern structure:
import { /* drizzle tables */ } from '@humanlayer/db/tables/...'
import { /* drizzle operators */ } from 'drizzle-orm'
import { Effect } from 'effect'
import { mapPostgresErrorToResponse } from '../../../effects/services/postgres/error-mapping'
import { getTransactionId } from '../../../effects/services/postgres/helpers'
import { PostgresDb } from '../../../effects/services/postgres/postgres-db.service'
import { Sentry } from '../../../instrument'
import { effectOrpcBaseOrganizationRequired } from '../../effect-base'
export const deleteCommentEffectProcedure = effectOrpcBaseOrganizationRequired.comments.delete
.effect(function* ({ input, context, errors }) {
// 1. Yield services from runtime
const postgres = yield* PostgresDb
// 2. Extract auth context
const { userId, organizationId } = context.auth
// 3. Implement logic using Effect patterns
const result = yield* postgres.transaction((tx) =>
Effect.gen(function* () {
// Query using tx.query() wrapper
const [comment] = yield* tx.query(
tx.client
.select()
.from(artifactComments)
.where(eq(artifactComments.id, input.commentId))
.limit(1)
)
// Fail with oRPC errors using Effect.fail
if (!comment) {
return yield* Effect.fail(
errors.NOT_FOUND({ data: { message: 'Comment not found' } })
)
}
// Continue with business logic...
const transactionId = yield* getTransactionId(tx)
return { success: true, transactionId }
})
).pipe(
// Log errors for observability
Effect.tapError((e) => Effect.logError(e)),
// Capture to Sentry
Effect.tapError((e) => Effect.sync(() => Sentry.captureException(e))),
// Map tagged Postgres errors to oRPC responses
Effect.catchTags({
PostgresDbQueryError: (error) => mapPostgresErrorToResponse(error, errors),
PostgresDbTransactionError: (error) => mapPostgresErrorToResponse(error, errors),
})
)
// 4. Log success with structured context
yield* Effect.logInfo('Comment deleted').pipe(
Effect.annotateLogs({ commentId: input.commentId, userId })
)
return result
})
Before (classic):
const { db } = context.services
After (Effect):
const postgres = yield* PostgresDb
Before:
const rows = await db.select().from(table).where(condition)
After:
const rows = yield* postgres.query(
postgres.client.select().from(table).where(condition)
)
Before:
return await db.transaction(async (tx) => {
const [row] = await tx.select()...
await tx.update()...
return result
})
After:
return yield* postgres.transaction((tx) =>
Effect.gen(function* () {
const [row] = yield* tx.query(tx.client.select()...)
yield* tx.query(tx.client.update()...)
return result
})
)
Before:
if (!found) {
throw errors.NOT_FOUND({ data: { message: '...' } })
}
After:
if (!found) {
return yield* Effect.fail(
errors.NOT_FOUND({ data: { message: '...' } })
)
}
Before:
const [a, b, c] = await Promise.all([
db.select().from(tableA)...,
db.select().from(tableB)...,
db.select().from(tableC)...,
])
After:
const [a, b, c] = yield* Effect.all([
postgres.query(postgres.client.select().from(tableA)...),
postgres.query(postgres.client.select().from(tableB)...),
postgres.query(postgres.client.select().from(tableC)...),
], { concurrency: 'unbounded' })
Before:
try {
// logic
} catch (error) {
if (error instanceof ORPCError) throw error
logger.error(error, 'Failed')
throw errors.INTERNAL_SERVER_ERROR()
}
After (use .pipe() with error handling chain):
return yield* someEffect.pipe(
// Log errors before mapping
Effect.tapError((e) => Effect.logError(e)),
// Capture to Sentry
Effect.tapError((e) => Effect.sync(() => Sentry.captureException(e))),
// Map to oRPC errors
Effect.mapError((error) =>
error.retryable
? errors.SERVICE_UNAVAILABLE()
: errors.INTERNAL_SERVER_ERROR()
)
)
Replace classic logger calls with Effect logging:
Before:
logger.info({ taskId, userId }, 'Task created')
logger.error(error, 'Failed to create task')
After:
// Info logging with annotations
yield* Effect.logInfo('Task created').pipe(
Effect.annotateLogs({ taskId, userId })
)
// Error logging (usually via tapError in a pipe)
Effect.tapError((e) => Effect.logError(e))
// Warning logging
yield* Effect.logWarning('Deprecated endpoint called').pipe(
Effect.annotateLogs({ endpoint: 'old-api' })
)
Effect logs are automatically sent to Sentry via SentryEffectLogger configured in appRuntime.
If the original procedure uses billing middleware:
Before:
export const proc = apiBaseOrgRequired.route.path
.use(requiresBillingMiddleware)
.handler(async ({ ... }) => { ... })
After:
export const procEffect = effectOrpcBaseOrganizationRequired.route.path
.use(requiresBillingEffectMiddleware)
.effect(function* ({ ... }) { ... })
In apps/riptide-api/src/orpc/router.ts, swap the procedure import:
Before:
import { deleteCommentProcedure } from './api/comments/delete'
// ...
comments: {
delete: deleteCommentProcedure,
}
After:
import { deleteCommentEffectProcedure } from './api/comments/delete.effect'
// ...
comments: {
delete: deleteCommentEffectProcedure,
}
Once the Effect procedure is wired in and tests pass, delete the original .ts file.
Run validation commands (typecheck and lint only - tests require Docker containers and run in CI):
# Package typecheck
bun --bun run typecheck --filter @humanlayer/riptide-api
# Linting and formatting
bun --bun run quality
Do NOT run tests locally - the riptide-api tests require Docker containers (Postgres, Electric SQL) that are set up by CI. The CI workflow will validate the migration.
This list may be out of date. Always check the source of truth:
# Authoritative list of available services
cat apps/riptide-api/src/effects/app-runtime.ts | grep -A20 'AppRuntimeServices'
# Browse available service implementations
ls apps/riptide-api/src/effects/services/
Common services (verify availability before using):
| Service | Import Path | Usage |
|---|---|---|
PostgresDb | effects/services/postgres/postgres-db.service | Database queries/transactions |
S3 | effects/services/s3/s3.service | Presigned URLs |
StripeService | effects/services/stripe/stripe.service | Stripe billing operations through the public Effect surface |
Check the effects/services/ directory for other available services and their implementations.
Common helpers in effects/services/postgres/helpers.ts:
getTransactionId(tx) - Get Postgres transaction IDrequireTaskReadPermission(tx, { taskId, userId, organizationId }) - Check task read accessrequireTaskEditPermission(tx, { taskId, userId, organizationId }) - Check task edit accessError mapping in effects/services/postgres/error-mapping.ts:
mapPostgresErrorToResponse(error, errors) - Convert Postgres errors to oRPC errorsBefore committing:
.effect.ts suffixyield* from runtime, not context.servicespostgres.query() or postgres.transaction()Effect.fail(), not throwEffect.tapError((e) => Effect.logError(e))Effect.tapError((e) => Effect.sync(() => Sentry.captureException(e)))Effect.catchTags or mapPostgresErrorToResponseEffect.logInfo() with Effect.annotateLogs() for contextcontext.services.db directly (use PostgresDb from runtime)throw for errors inside Effect generators (use Effect.fail)postgres.query()async/await with Effect generatorsappRuntime)StripeService insteadEffect.tapError before mapping errorslogger.info/error instead of Effect.logInfo/logErrorSentry.captureException in error handling chainsSee these files for reference implementations:
apps/riptide-api/src/orpc/api/hosts/alias-list.effect.tsapps/riptide-api/src/orpc/api/comments/update.effect.tsapps/riptide-api/src/orpc/api/uploads/create.effect.tsapps/riptide-api/src/orpc/api/uploads/download-url.effect.tsThe standard error handling pattern for transactions:
yield* postgres.transaction((tx) =>
Effect.gen(function* () {
// ... transaction logic
})
).pipe(
Effect.tapError((e) => Effect.logError(e)),
Effect.tapError((e) => Effect.sync(() => Sentry.captureException(e))),
Effect.catchTags({
TaskNotFoundError: () => Effect.fail(errors.NOT_FOUND()),
TaskPermissionDeniedError: () => Effect.fail(errors.FORBIDDEN()),
PostgresDbQueryError: (error) => mapPostgresErrorToResponse(error, errors),
PostgresDbTransactionError: (error) => mapPostgresErrorToResponse(error, errors),
})
)
Tests for Effect procedures use @effect/vitest and require Docker containers (Postgres, Electric SQL).
Tests run in CI only - do not attempt to run locally without the test infrastructure.
it.effectFor testing Effect services in isolation:
import { describe, it } from '@effect/vitest'
import { Effect, Layer } from 'effect'
import { expect } from 'vitest'
describe('MyService', () => {
it.effect('does something', () =>
Effect.gen(function* () {
const service = yield* MyService
const result = yield* service.doSomething()
expect(result).toBe('expected')
}).pipe(
Effect.provide(makeTestLayer())
)
)
it.effect('handles errors', ({ expect }) =>
Effect.gen(function* () {
const service = yield* MyService
const result = yield* Effect.result(service.failingOperation())
expect(Result.isFailure(result)).toBe(true)
}).pipe(
Effect.provide(makeTestLayer())
)
)
})
For testing Effect procedures against a real database:
import { call } from '@orpc/server'
import { beforeAll, afterAll, describe, test, expect } from 'vitest'
import { myEffectProcedure as liveProcedure } from '../../src/orpc/api/my/procedure.effect'
import { bindProcedureToRuntime, createRuntimeWithTestPostgresLayer } from '../utils/effect-procedure-test-runtime'
import { createTestDb } from '../utils/test-db'
describe('my effect procedure', async () => {
let db: ReturnType<typeof drizzle>
let effectRuntime: ReturnType<typeof createRuntimeWithTestPostgresLayer>
let myProcedure: typeof liveProcedure
beforeAll(async () => {
const testDb = await createTestDb('my_procedure_test')
db = testDb.db
effectRuntime = createRuntimeWithTestPostgresLayer(db)
myProcedure = bindProcedureToRuntime(liveProcedure, effectRuntime)
})
afterAll(async () => {
await effectRuntime?.dispose()
})
test('does the thing', async () => {
const result = await call(myProcedure, { input }, { context: makeContext() })
expect(result).toEqual({ expected: 'value' })
})
})
See apps/riptide-api/test/procedures/list-effect-procedures.test.ts for a complete example.
まだレビューはありません。使ってみた感想をお寄せください。
ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。
まだコメントはありません。最初のコメントを書いてみませんか。
Webページの移動やフォーム入力、クリックをエージェントに任せ、情報抽出や画面保存、操作前後の比較まで行います。ブラウザ作業の自動化やWebアプリの確認に使えるスキル。
曖昧で読みにくい英文を、語の意味をそろえた短い能動態の文へ書き直すスキル。指示文やツール説明を見直し、修正理由と変更前後を表で確認できます。
GitHubのPRの状態、レビューの会話、CI失敗のログを専用スクリプトで収集し、変更の確認状況や未解決の指摘、失敗したジョブとステップを調べるスキル。
リリース時に対象パッケージの基準バージョンを更新し、関連ファイルの同期と生成結果を検証して、コミットからPRブランチへのプッシュまで進めるスキル。
Git のコミット間や作業中のコードで、関数の呼び出し構造の違いを確認します。呼び出しツリーの表示や、指定した関数へ至る経路の調査にも使えます。
ターミナルからブラウザのクリックや入力、ページ移動を自動化し、通信やエラー、表示速度も調べられる、Chrome DevToolsのCLI操作を支援するスキル。