Files
AgentCompany/builtin-skills/software-engineer/clean-architecture/references/adapt-gateway-abstraction.md
T
ZacharyZhang-NY e74dafcd36 Silence render-disposed spam, unbreak bundled skills, drop board wake
Three independently ugly bugs that all fed the same log flood:

* Electron's `WebFrameMain.send` writes every "Render frame was disposed"
  to stderr via `console.error`. When the worker fans a second
  agent-event past a reloading renderer, we produced thousands of
  identical lines and stalled the main loop on stderr I/O. Added a
  `mainFrame.detached` guard so the send is short-circuited before
  Electron's own catch block fires, and installed a targeted filter in
  `suppress-warnings.ts` (now actually imported from the entry point)
  so the specific `console.error("Error sending from webFrameMain:"…)`
  noise never reaches the terminal. Any other payload still surfaces.

* `dispatchAgentMessage` accepted the human-reader sentinel as a wake
  target when an agent replied to `__board__` — `wakeAgentIfPossible`
  then called `db.getAgent("__board__")`, threw, and the dispatcher
  kept repeating on every send. Strip `HUMAN_SENDER_ID` at
  `resolveMessageWakeTargets`, and keep a defense-in-depth guard in the
  heartbeat so a stray caller (future automation fan-out) cannot
  reintroduce the crash.

* Pi's skills loader rejects any SKILL.md whose frontmatter `name`
  diverges from its parent directory. The four bundled `my-writing-style`
  copies declared `name: writing-style-skill`, so every install emitted
  a warning and dropped the skill. They also lived as orphan gitlinks
  (mode 160000 without .gitmodules), and the global `*.md` gitignore
  hid every other SKILL.md from version control. Fixed the frontmatter
  to match the dir, converted the dangling submodules to plain trees,
  and allowlisted `builtin-skills/**/*.md` so a fresh clone carries the
  catalogue instead of silently losing it.
2026-04-20 08:13:48 -04:00

3.4 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Gateways Hide External System Details MEDIUM enables vendor switching, simplifies testing adapt, gateway, external, abstraction

Gateways Hide External System Details

Database gateways, API gateways, and service gateways are polymorphic interfaces that hide external system details. The use case layer talks to abstractions; infrastructure implements them.

Incorrect (use case knows external system details):

func (uc *ProcessRefundUseCase) Execute(orderId string) error {
    order := uc.db.QueryRow("SELECT * FROM orders WHERE id = $1", orderId)

    // Direct Stripe API knowledge in use case
    stripe.Key = os.Getenv("STRIPE_KEY")
    params := &stripe.RefundParams{
        PaymentIntent: stripe.String(order.PaymentIntentId),
        Amount:        stripe.Int64(order.Total),
    }
    _, err := refund.New(params)
    if err != nil {
        // Stripe-specific error handling
        if stripeErr, ok := err.(*stripe.Error); ok {
            if stripeErr.Code == stripe.ErrorCodeChargeAlreadyRefunded {
                return ErrAlreadyRefunded
            }
        }
        return err
    }

    uc.db.Exec("UPDATE orders SET status = 'refunded' WHERE id = $1", orderId)
    return nil
}

Correct (gateway abstracts external system):

// application/ports/PaymentGateway.go
type PaymentGateway interface {
    Refund(paymentId string, amount Money) (RefundResult, error)
}

type RefundResult struct {
    RefundId string
    Status   RefundStatus
}

// application/ports/OrderRepository.go
type OrderRepository interface {
    FindById(id OrderId) (*Order, error)
    Save(order *Order) error
}

// application/usecases/ProcessRefundUseCase.go
type ProcessRefundUseCase struct {
    orders   OrderRepository
    payments PaymentGateway
}

func (uc *ProcessRefundUseCase) Execute(orderId string) error {
    order, err := uc.orders.FindById(OrderId(orderId))
    if err != nil {
        return err
    }

    result, err := uc.payments.Refund(order.PaymentId, order.Total)
    if err != nil {
        return err  // Gateway translates Stripe errors to domain errors
    }

    order.MarkRefunded(result.RefundId)
    return uc.orders.Save(order)
}

// infrastructure/StripePaymentGateway.go
type StripePaymentGateway struct {
    client *stripe.Client
}

func (g *StripePaymentGateway) Refund(paymentId string, amount Money) (RefundResult, error) {
    params := &stripe.RefundParams{
        PaymentIntent: stripe.String(paymentId),
        Amount:        stripe.Int64(amount.Cents()),
    }

    refund, err := g.client.Refunds.New(params)
    if err != nil {
        return RefundResult{}, g.translateError(err)
    }

    return RefundResult{
        RefundId: refund.ID,
        Status:   g.translateStatus(refund.Status),
    }, nil
}

func (g *StripePaymentGateway) translateError(err error) error {
    // Convert Stripe-specific errors to domain errors
    if stripeErr, ok := err.(*stripe.Error); ok {
        switch stripeErr.Code {
        case stripe.ErrorCodeChargeAlreadyRefunded:
            return ErrAlreadyRefunded
        }
    }
    return ErrPaymentFailed
}

Benefits:

  • Switch from Stripe to Adyen without touching use cases
  • Test use cases with mock gateways
  • External API changes isolated to gateway implementation

Reference: Clean Architecture - Database Gateways