Full Blog TOC

Full Blog Table Of Content with Keywords Available HERE

Wednesday, October 14, 2026

How I Get at Least 10x More Work Done with an AI Coding Agent



In this post we will review how I use an AI coding agent to get at least 10 times more development work done. The largest improvement comes from combining small tasks, reusable agent skills, and continuous review.

A feature rarely ends with writing a function. There are callers to update, configuration to connect, tests to adjust, deployment files to check, and sometimes a user interface to change. All these steps take time. An agent that can work inside the repository can handle much of this work while I concentrate on the decisions.

Looking back at my last two weeks of development and agent conversations, the interesting part is the variety: investigating failures, understanding unfamiliar code, comparing performance options, updating interfaces, checking deployments, and improving tests. Some conversations start with a stack trace. Others start with a selected function and a short question. The agent is part of the daily engineering work.

For my daily work, I estimate the improvement at at least 10x compared with doing the implementation manually. This is my experience, rather than a controlled benchmark. Counting commits or generated lines would not prove it. The useful result is how much working, reviewed functionality I can complete.

Delegate a Complete Task

With an agent I can delegate a complete, limited task:

"Find how this value reaches the response. Add the new field using the existing pattern, update the relevant callers, and verify the affected behavior."

The agent can search the repository, inspect the code, edit multiple files, and use tools to check its work. I review the result and decide what comes next.

This removes many small interruptions. I spend less time navigating between files and more time thinking about what the application should do.

Start with the Problem

The most expensive mistake is implementing the wrong solution quickly.

One recent investigation started with a service that kept restarting. I asked the agent to inspect the running instance and its memory usage. Then we examined the expensive components, considered alternatives, and worked through the implications before changing the code.

At one point, lazy loading looked like a possible improvement. I asked the agent to check how the loaded data was actually used. If the application eventually reads all the fields anyway, lazy loading may add complexity without removing the expensive work.

We chose a smaller change: process database rows individually instead of holding all the raw rows alongside the decoded objects. Then I asked for tracking so we could understand the remaining cost.

Even there, I challenged the proposed design. Could the existing tracking format hold the information? Did we really need another structure? Would the interface still display it correctly?

These questions are part of the work. The agent makes it much faster to investigate the answers and implement the decision.

I often start with a simple boundary:

"Check only. Do not change anything."

Once I understand the findings, I give the next instruction. This is particularly useful when investigating a running environment or code I do not yet know well.

Stop Repeating the Coding Guidelines

In my earlier post, GO Coding Guideline as Agent Skill, I explained how I converted my coding standards into a reusable skill:

GO Coding Guideline as Agent Skill

Before creating it, I kept asking for the same corrections: follow the naming conventions, reuse existing utilities, simplify the design, use the correct logging, and put the tests in the expected location.

Now these instructions live under .agents/skills/go-guidelines/.

The skill first tells the agent to inspect neighboring code. It then points to separate reference files for style, functions and types, design, errors and logging, runtime safety, and tests. The agent reads the relevant references for the task.

This is useful because the standards include much more than formatting. They cover ownership of state, error handling, configurable limits, lock cleanup, and avoiding secrets in responses or logs.

A skill does not guarantee correct code. However it reduces the amount of repeated explanation and makes the first implementation much closer to what I expect.

I've also added skills for other repeated parts of the workflow.

A Code Review Skill

The code-review skill defines how to review a named branch, from preparing the working tree to producing actionable comments.

It requires a clean working tree and updated branches before reviewing the changes against the development branch. It applies the coding guidelines, checks nearby conventions, and looks for unnecessary complexity.

One particularly useful instruction is checking everything in memory that can grow: caches, collections, buffers, queues, payloads, and concurrent work.

Notice that creating a map with an initial capacity does not limit its growth. A cache with an expiration time can also grow too large if new entries arrive faster than old entries expire. The review should check the actual limits and what happens when they are reached.

The output is deliberately short. Each finding identifies the problem, location, impact, and a concrete fix. If there are no findings, the skill does not create a report file. It also returns to the development branch when finished.

In this workflow, the review skill does not run tests; CI/CD handles them. Implementation checks are a separate part of the task. Keeping this explicit avoids confusing a code review with evidence that the application passed its tests.

A Skill for New Source Files

The add-new-source-files skill handles a small but annoying detail: new source and test files must be staged before the task is finished.

It tells the agent to stage only the source and test files it created, exclude temporary artifacts and unrelated changes, and verify the staged file list. It does not commit unless requested.

Why create a skill for something so simple?

Because a repeated omission is still expensive. A change can look complete in the editor while a required new file is missing from the staged changes. A short instruction makes this part of finishing the task.

A Cloud Credentials Skill

The use-aws-credentials skill records where the agent should load temporary cloud credentials before using the CLI or SDK. It also includes instructions to make the target context and namespace explicit for the relevant infrastructure commands.

The skill points to a local credentials file rather than embedding credential values in its instructions. This saves me from explaining the local setup in every session.

Notice that knowing how to load credentials does not establish that an infrastructure change is appropriate. I still need to understand the target environment and the operation being performed.

The broader idea is simple: write down recurring setup once, then reuse it.

Give the Agent Behavior, Not Just a Feature Name

A useful prompt describes what the user should experience and which behavior must stay consistent.

For example, when asking for multiple views of a graph, the important details include whether each view has its own selected series, whether zoom is shared, and which views can be removed. "Add another graph" leaves all these decisions to the agent.

I give those rules, let the agent implement them, then refine the interaction. Sometimes the next instruction is simply to reuse the existing controls. I do not need a new design language for one feature.

The same applies to backend work. Initializing a component without required data and failing to refresh an already working component are different situations. I ask about both paths. A retry mechanism is only useful if its behavior is correct in each one.

Use the Agent to Build the Comparison

Another useful pattern is asking the agent to create the experiment that will help me decide.

In one conversation, the question was whether an existing cache still justified its complexity. Removing it immediately would have been guessing. We worked on a comparison with a switch that preserved the current default behavior, configurable cases, and a result summary.

I kept asking practical questions: Is there enough coverage to decide? Where does the evidence come from? Can the test use the existing APIs and reduce manual setup? What does a matching result actually mean?

The agent can handle the code needed to make that comparison possible. I still need to decide whether the cases represent real usage and whether the evidence supports removing the cache. Creating a comparison is not the same as proving an optimization worked.

Verify the Running Application

A test passing locally is useful, but sometimes my next question is:

"Is it working inside the running container?"

A dependency change can affect the build, the image, startup, memory usage, and concurrency. I use the agent to check these connections instead of stopping at the updated function.

For a performance-sensitive component, I may ask it to compare alternatives, broaden the sample set, check short and long inputs, and examine behavior with multiple requests. If a test fails after the change, I ask which input failed and what happens next in the application.

A wrong intermediate result matters because of the behavior it triggers downstream. Finding that connection is more useful than changing an assertion until the test passes.

Make Tests Represent Real Behavior

I also use the agent to improve what the tests actually prove.

In one task, I asked for a black-box test that sends several realistic events through the application and examines the exported result. Rather than inventing a convenient input, I asked the agent to use a representative event from local diagnostics as an embedded test resource.

This helps exercise the mapping and processing together. It can expose assumptions that a test of one helper function would miss. Any captured input should be checked and stripped of sensitive values before it becomes a fixture.

For configured limits, I ask for an explicit scenario that reaches the limit and checks what happens next. For a bug, I ask why the existing test did not catch it. Passing tests are more useful when I understand which behavior they cover.

Use the Agent for the Work Around the Code

Development also includes explaining what happened and what someone should do next.

I use the agent to connect interface fields to the data the code actually reads, summarize an authentication failure, and prepare an explanation of the next diagnostic steps. It can turn a long investigation into a short answer grounded in the implementation.

I review these explanations just as I review code. The useful result is an accurate description with clear next steps. Collecting the relevant facts is often the slowest part, and the agent can help with that too.

Keep the Design Simple

Some of my most useful follow-up instructions are short:

"Can we reuse this?"
"This looks duplicated."
"What happens if there is no cached data?"
"KISS. What do you recommend?"

An agent can generate more code very quickly. I want it to help me maintain less code where possible. I also ask it to reuse existing libraries, keep configuration together, and avoid unrelated edits.

When the task grows, these small corrections keep it focused. They also explain why technical experience still matters: I need to recognize when a suggested solution creates more problems than it solves.

Continue in a New Chat Without Starting Over

Long investigations can accumulate many decisions. Before starting a new chat, I ask for a text summary with the information needed to continue.

The useful summary includes the objective, findings, decisions, changes, verification, and remaining questions. It should distinguish code that was edited from code that was actually deployed, and measured results from estimates.

This saves me from explaining the whole investigation again. It also gives the next conversation a clear starting point.

My Daily Agent Workflow

For most development tasks I use the following sequence:

  1. Point to the relevant code, log, screenshot, or running environment.
  2. Ask for an investigation when the cause or design is unclear.
  3. Choose a small implementation step and state the required behavior.
  4. Let the agent update the related code and configuration using the repository skills.
  5. Inspect the changes, question the edge cases, and request relevant verification.
  6. Continue with the next step or save a summary for a new chat.

I use the agent for explanations too. Understanding an unfamiliar subsystem, tracing a configuration value, or finding why a test missed a bug can save as much time as generating code.

"Done" needs evidence appropriate to the task. That might be a passing test, a benchmark comparison, or a verified interaction. If a check could not run, I want that stated clearly.

Where the 10x Gain Comes From

The gain adds up across the entire task.

The agent searches for the existing implementation, follows the pattern across multiple files, updates callers and test support, and handles repetitive edits. The skills reduce repeated instructions and correction rounds. Small tasks keep review manageable.

The savings extend beyond implementation. The same conversation can move from a failure log to the relevant code, from a proposed change to its build implications, and from a test result to the next investigation. I spend less time collecting and moving the information needed for each step.

There is still work for me: deciding whether a cache belongs in the design, whether a new dependency is justified, whether a resource limit is sufficient, and whether the result actually solves the problem.

I discussed the risks in AI Agents Coding Bad Practices:

AI Agents Coding Bad Practices

An agent can make a wrong decision and then create more code to support it. Reviewing small changes helps me catch this before a simple task becomes a complicated subsystem.

Getting Started

Start with one repeated task you understand well. Give the agent a clear expected result and an existing example to follow. Review the changes.

When you find yourself requesting the same correction repeatedly, add it to a focused skill under .agents/skills/. Include when it applies, the steps to follow, and what a completed result looks like.

Then use the saved time to tackle the next task.

My experience is that an AI coding agent becomes far more useful when the repository contains the knowledge needed to work in it. With clear tasks, practical skills, and an experienced engineer reviewing the decisions, I can complete at least 10x more work in my daily development.

Tuesday, October 6, 2026

HTTP Streaming Error Handling in GO: Why HTTP 200 Is Not Enough



In this post we will review how to handle errors in an HTTP streaming response using GO and NDJSON. This is useful for AI chat responses, generated reports, and any operation that sends results before all its work is done.

Streaming enables the user to see output without waiting for the entire operation. However it introduces a problem: what happens if the server sends the result successfully, but fails to save it?

The client already received HTTP 200 and some text. Should it display success? Should it retry? Has the result been saved?

To answer these questions, the stream needs a completion contract.

The Streaming Problem

Let's assume our application generates a report. The server performs the following steps:

  1. Start an HTTP response.
  2. Send report sections as they become available.
  3. Save the completed report.
  4. Close the response.

Everything works until step 3 fails.

For a regular HTTP request, we could return an error status. However once the streaming response has started, the headers and status have already been sent. Calling WriteHeader with HTTP 500 at this point cannot replace the original HTTP 200.

Closing the response is also not enough. The client cannot tell whether the operation completed, the database failed, or the connection was interrupted.

Notice that even a clean end of the HTTP body only tells us the transport finished. It does not tell us our application completed all its work.

Create a Completion Contract

For this example we use NDJSON, which stands for newline-delimited JSON. Each line contains a complete JSON object. This makes it possible to process records as they arrive without waiting for a full JSON array.

We use two record types: chunk and final.

A successful response looks like this:

{"type":"chunk","text":"Report section one.\n"}
{"type":"chunk","text":"Report section two.\n"}
{"type":"final","outcome":"complete","saved":true}

A response whose report was generated but not saved looks like this:

{"type":"chunk","text":"Report section one.\n"}
{"type":"chunk","text":"Report section two.\n"}
{"type":"final","outcome":"partial","saved":false,"error":"save_failed"}

The report text is still useful. The client can display it with a notice that it was not saved.

Our completion rules are simple:

  • A normally finished stream contains exactly one final record.
  • The final record is the last record.
  • Required work, including saving the report, happens before the final record.
  • A stream that ends without a final record is incomplete.

The server should attempt to send a final record for handled failures. However no protocol can guarantee delivery after the connection breaks. The client must handle that case too.

GO Server Implementation

The following example uses only the GO standard library. Save it as server.go. The save function deliberately supports a failure so we can reproduce the problem without installing a database.

package main

import (
    "encoding/json"
    "errors"
    "log"
    "net/http"
)

type streamRecord struct {
    RecordType string `json:"type"`
    TextValue  string `json:"text,omitempty"`
    Outcome    string `json:"outcome,omitempty"`
    Saved      *bool  `json:"saved,omitempty"`
    ErrorCode  string `json:"error,omitempty"`
}

type reportStream struct {
    encoder    *json.Encoder
    controller *http.ResponseController
}

func (s *reportStream) sendRecord(
    record *streamRecord,
) error {
    if err := s.encoder.Encode(record); err != nil {
        return err
    }
    return s.controller.Flush()
}

func (s *reportStream) sendFinal(
    saveError error,
) error {
    saved := saveError == nil
    record := streamRecord{
        RecordType: "final",
        Outcome:    "complete",
        Saved:      &saved,
    }
    if saveError != nil {
        record.Outcome = "partial"
        record.ErrorCode = "save_failed"
    }
    return s.sendRecord(&record)
}

func saveReport(
    failSave bool,
) error {
    if failSave {
        return errors.New("simulated storage failure")
    }
    // Replace with a database or object storage write.
    return nil
}

func (s *reportStream) writeReport(
    failSave bool,
) error {
    sections := []string{
        "Report section one.\n",
        "Report section two.\n",
    }
    for _, section := range sections {
        record := streamRecord{
            RecordType: "chunk",
            TextValue:  section,
        }
        if err := s.sendRecord(&record); err != nil {
            return err
        }
    }
    saveError := saveReport(failSave)
    if saveError != nil {
        log.Printf("report save failed: %v", saveError)
    }
    return s.sendFinal(saveError)
}

func handleReport(
    writer http.ResponseWriter,
    request *http.Request,
) {
    writer.Header().Set("Content-Type", "application/x-ndjson")
    writer.Header().Set("Cache-Control", "no-store")
    stream := reportStream{
        encoder:    json.NewEncoder(writer),
        controller: http.NewResponseController(writer),
    }
    failSave := request.URL.Query().Get("failSave") == "true"
    if err := stream.writeReport(failSave); err != nil {
        log.Printf("report stream interrupted: %v", err)
    }
}

func main() {
    http.HandleFunc("/report", handleReport)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}

The example requires GO 1.20 or later because it uses http.NewResponseController.

json.Encoder.Encode adds a newline after each JSON object. Flush requests that buffered output be sent immediately. A reverse proxy can still buffer the response, so its streaming configuration must also be checked when deploying the application.

Notice that Saved is a pointer. Chunk records omit this field, while final records include either true or false. Using a boolean with omitempty would hide false, which is an important result here.

Also notice the order: write the sections, save the report, then send the final record. Sending complete before saving would give the client a success indication that might become false a moment later.

The example logs the storage error on the server and sends a stable error code to the client. A real database error can contain internal details that should not appear in the response.

Run the Server

go run server.go

In another terminal, try both flows:

curl -N 'http://127.0.0.1:8080/report'
curl -N 'http://127.0.0.1:8080/report?failSave=true'

The -N option disables curl's output buffering. Both requests return HTTP 200, but their final records describe different application outcomes.

GO Client Implementation

Now we need a client that understands the contract. Save the following as client.go and run it separately from server.go.

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "time"
)

type streamRecord struct {
    RecordType string `json:"type"`
    TextValue  string `json:"text"`
    Outcome    string `json:"outcome"`
    Saved      *bool  `json:"saved"`
    ErrorCode  string `json:"error"`
}

func readReport(
    body io.Reader,
) error {
    decoder := json.NewDecoder(body)
    for {
        var record streamRecord
        err := decoder.Decode(&record)
        if errors.Is(err, io.EOF) {
            return errors.New("stream ended without a final record")
        }
        if err != nil {
            return fmt.Errorf("cannot read stream: %w", err)
        }
        switch record.RecordType {
        case "chunk":
            if _, err := fmt.Print(record.TextValue); err != nil {
                return err
            }
        case "final":
            return checkFinal(&record)
        default:
            return fmt.Errorf("unknown record type: %q", record.RecordType)
        }
    }
}

func checkFinal(
    record *streamRecord,
) error {
    if record.Saved == nil {
        return errors.New("final record is missing saved status")
    }
    if record.Outcome == "complete" && *record.Saved {
        return nil
    }
    return fmt.Errorf(
        "report did not fully complete: outcome=%q saved=%t error=%q",
        record.Outcome, *record.Saved, record.ErrorCode,
    )
}

func fetchReport(
    reportUrl string,
) error {
    client := http.Client{
        Timeout: 30 * time.Second,
    }
    response, err := client.Get(reportUrl)
    if err != nil {
        return err
    }
    defer response.Body.Close()
    if response.StatusCode != http.StatusOK {
        return fmt.Errorf("unexpected HTTP status: %s", response.Status)
    }
    return readReport(response.Body)
}

func main() {
    if len(os.Args) != 2 {
        log.Fatal("usage: go run client.go <report URL>")
    }
    if err := fetchReport(os.Args[1]); err != nil {
        log.Fatal(err)
    }
    fmt.Println("Report generated and saved.")
}

Try the client against both server flows:

go run client.go 'http://127.0.0.1:8080/report'
go run client.go 'http://127.0.0.1:8080/report?failSave=true'

The first prints the report and a success message. The second prints the report and then reports that it was not saved.

The decoder reads JSON records rather than network packets. This is important because one record can arrive in multiple reads, and multiple records can arrive together. A network read is not an application message boundary.

The client treats the first final record as authoritative and stops reading. The server is responsible for sending it once, at the end. If you are validating an untrusted implementation, you can additionally read to EOF and reject duplicate final records or trailing records.

This example expects a trusted server and small records. For a public API, also bound record sizes and total output. Set the request timeout according to the expected operation duration; the example's 30 seconds includes reading the response body.

What Happens When the Connection Breaks?

If the server terminates after sending the sections but before sending final, the client reports an incomplete stream. It does not display a success message just because it received the report text.

However incomplete does not necessarily mean the report was not saved. The database write might have succeeded just before the connection was lost.

For operations where this matters, assign an operation ID before streaming starts. The client can query the stored operation status after reconnecting. If retries can create duplicate side effects, use an idempotency key and store the result of the operation.

The final record tells us what the server reported. The stored operation status helps us recover when that report never reaches the client.

Testing the Failure Paths

The most useful checks are the following:

  1. Successful generation and save produce one complete final record.
  2. A save failure after output produces one partial final record.
  3. EOF before final is reported as incomplete by the client.
  4. A malformed or truncated JSON record is reported as an error.
  5. A failed network write does not lead to another attempt to write success.

A generation failure can use a final outcome of failed with a stable error code. If some useful sections were already sent, define whether the client should keep them and show them as partial output.

These checks can run with an injected storage failure and an in-memory stream. They do not require a production database or a running AI model.

Using Server-Sent Events Instead of NDJSON

The same completion contract can be used with Server-Sent Events, also called SSE. The message framing changes, but the application still needs a final outcome:

event: chunk
data: {"text":"Report section one."}

event: final
data: {"outcome":"complete","saved":true}

Notice the blank line after each event. With EventSource, the client should close the connection when it receives final; otherwise the browser may reconnect after the server closes the stream.

Choose NDJSON or SSE according to the client and API requirements. Neither format automatically defines what successful completion means.

Final Note

An HTTP streaming response needs more than chunks of output. It needs an explicit completion result that is sent after the required work finishes.

This enables the client to distinguish a completed operation, a useful result that was not saved, and an interrupted request whose outcome is still unknown.

Monday, September 7, 2026

Slack Integration in GO



In this post we will review how to integrate a GO application with Slack.

There are two flows we want to support. The first is sending a notification to a channel, for example when an operation completes. The second is receiving a message from a user and replying in a thread.

These flows use different Slack features and require different configuration. Before writing the code, let's understand what each one needs.

Notifications and Chat

First notice that an application is represented in Slack as a bot.

For notifications I've used an incoming webhook. Slack creates a URL associated with a channel, and the application sends an HTTP POST to this URL. The word incoming is from Slack's point of view: the message goes from our application into Slack.

For chat I've used the Events API to receive bot mentions, and the Web API method chat.postMessage to send replies. Slack sends an HTTP POST to our application when someone mentions the bot. Our application processes the event and uses a bot token to reply.

A webhook is enough for the notification flow. It does not subscribe the application to user messages. Chat requires a bot token, a signing secret, and a public callback URL. Both flows can use the same Slack app.

A. Slack Setup

1. Create a Slack App

Open Slack Apps, select Create New App → From scratch, and choose an app name and workspace. The following settings are all under this app.

2. Notifications: Enable Incoming Webhooks

Open Incoming Webhooks and enable Activate Incoming Webhooks. Select Add New Webhook to Workspace, choose the channel that should receive notifications, and approve access.

Copy the generated webhook URL. In the GO application this value is called WebhookUrl. The selected channel is associated with the URL, so our notification request only needs to contain the message text.

This setting is required only for webhook notifications. If all you need is sending notifications, you can skip the remaining chat setup.

3. Chat: Add Bot Permissions

Open OAuth & Permissions → Scopes → Bot Token Scopes and add:

  • app_mentions:read — receive events when someone mentions the bot.
  • chat:write — send messages using the bot token.

The first permission is used for receiving chat events. The second is used for sending the reply. Adding permissions does not enable event delivery; we will configure the event subscription separately.

4. Chat: Install the App and Copy the Bot Token

Under OAuth & Permissions, select Install to Workspace and approve the requested access. Copy the Bot User OAuth Token, which starts with xoxb-.

In the application this is BotToken. It is sent in the Authorization header when calling chat.postMessage. If you change scopes after installation, reinstall the app to approve the updated permissions.

5. Chat: Copy the Signing Secret

Open Basic Information → App Credentials → Signing Secret and copy the value. In the application this is SigningSecret.

The signing secret is used to validate requests coming from Slack. The bot token is used for requests going to Slack. These are two different credentials with different purposes.

6. Chat: Configure Event Subscriptions

Create a public TLS valid endpoint in you application. This is a public callback URL that Slack will call. The receiving service must be running and reachable over HTTPS before Slack can verify it. This is our application's incoming event URL. It is different from WebhookUrl, which belongs to Slack and receives outgoing notifications.

Open Event Subscriptions, enable Enable Events, and paste that URL into Request URL. Slack sends a verification challenge to the application. Once our application returns the challenge, Slack marks the URL as Verified.

Under Subscribe to bot events, add app_mention and save the changes. Reinstall the app if Slack asks for updated permissions.

7. Chat: Invite the Bot to a Channel

In the Slack channel, use /invite @YourBotName. Users can now address the application by mentioning the bot.

This setup receives bot mentions. Direct messages and channel messages without a mention require additional event subscriptions and permissions.

B. GO Implementation

Now that we have the Slack settings, we can connect them to the application. 

1. Save the Integration Configuration in our application

{

  "WebhookUrl": "https://hooks.slack.com/services/your-webhook",
  "BotToken": "xoxb-your-bot-token",
  "SigningSecret": "your-signing-secret"
}

Notice that the application also need to listen on the 
public TLS valid endpoint.

2. Send Notifications Using a Webhook

The webhook payload is simple:

type SlackNotification struct {
    MessageText string `json:"text"`
}

The Slack wrapper checks that the integration is enabled and that WebhookUrl is configured. It then sends the payload to that URL:

payload := &SlackNotification{MessageText: message}
var responseBody string
_, _, err := s.webClient.SendRequestWrapped(
    http.MethodPost, webhookUrl, payload, nil, &responseBody,
)
kiterr.RaiseIfError(err)

No bot token or callback URL is required for this request. The webhook URL already identifies the destination and authorizes the notification.

3. Receive Chat Events

The application listening on the public TLS valid endpoint reads the original body and headers, validates and processes the Slack request, then returns the HTTP response information.

Keeping the original request body is important. Slack signs the exact body bytes. Parsing the JSON and serializing it again could change those bytes and cause signature validation to fail.

4. Validate the Signature and Timestamp

Slack supplies X-Slack-Request-Timestamp and X-Slack-Signature. Our application requires one value for each header and checks the timestamp. This rejects requests outside the allowed time window, including timestamps too far in the future.

The signature is an HMAC-SHA256 calculated using SigningSecret and the following input:

v0:<request timestamp>:<original request body>

The relevant code is:

mac := hmac.New(sha256.New, []byte(secret))
_, err := mac.Write([]byte("v0:" + timestamp + ":"))
kiterr.RaiseIfError(err)
_, err = mac.Write(c.requestMessage.RequestBody)
kiterr.RaiseIfError(err)

expectedSignature := "v0=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expectedSignature), []byte(signature))

Invalid requests receive HTTP 401. The timestamp check limits how long a signed request can be reused; it does not detect duplicate events within that window.

5. Handle URL Verification

Slack sends an event of type url_verification when we configure the Request URL. After validating the request, our application returns its challenge value as plain text with HTTP 200.

Reply in a Thread

The current implementation replies by echoing the received prompt. If the message already belongs to a thread, we keep its thread_ts. Otherwise we use the message's ts to start a thread under it.

threadTimestamp := event.ThreadTimestamp
if threadTimestamp == "" {
    threadTimestamp = event.MessageTimestamp
}

The reply uses this structure:

type SlackReplyRequest struct {
    ChannelId       string `json:"channel"`
    ThreadTimestamp string `json:"thread_ts"`
    MessageText     string `json:"text"`
    MarkdownEnabled bool   `json:"mrkdwn"`
}

Before including the user's text in the reply, the code escapes &, <, and > so Slack does not interpret those characters as special references.

The application sends the reply to https://slack.com/api/chat.postMessage with a JSON body and these headers:

Authorization: Bearer xoxb-your-bot-token
Content-Type: application/json; charset=utf-8

Here we use BotToken, copied from Slack's OAuth & Permissions page. The channel and thread come from the event, so this flow does not use WebhookUrl.

Slack can return HTTP 200 with ok: false in the JSON response. The wrapper checks both the HTTP status and the ok field to identify a failed reply.

Acknowledge Events and Reply Asynchronously

Slack expects an event acknowledgement within three seconds. Our application starts the outgoing reply in a goroutine and returns HTTP 200 without waiting for the Web API call to finish:

kitparallel.RunAsGoRoutineSafe(func() {
    slackApi.ReplyToMessage(reply)
})

The asynchronous reply keeps the outgoing Slack call out of that path. It is not a durable queue, and the current implementation does not deduplicate Slack retries.

Final Note

We now have two Slack interaction flows. Webhook notifications need a channel webhook URL. Chat needs event subscriptions, a public callback URL, a signing secret for incoming requests, and a bot token for outgoing replies.

The current chat implementation echoes the prompt in a thread. Once the integration is in place, we can replace this reply with the application's actual processing logic.

Wednesday, September 2, 2026

Testing Time Based GO Code Without Sleeping




In this post we will review how to test time based GO code without using time.Sleep.

Many applications include logic that depends on time. For example, a cache item expires after a minute, a task runs every hour, or a request is rejected after a timeout. The simple method to test such logic is to wait for the required time. However this causes the tests to be slow, and in some cases unstable.


The Sleep Problem

Let's assume we have a token that is valid for one minute:


type Token struct {
createdTime time.Time
}
func (t *Token) Expired() bool {
return time.Since(t.createdTime) >= time.Minute
}


A test for this code can use time.Sleep:


func TestTokenExpired(t *testing.T) {
token := &Token{
createdTime: time.Now(),
}
time.Sleep(time.Minute)
if !token.Expired() {
t.Fatal("token should be expired")
}
}


The test works, but it takes at least one minute. If we have multiple tests with hours or days based logic, waiting for the real time is not an option.

Using shorter durations only for the test is also problematic. A test that sleeps for ten milliseconds might fail when the machine or CI is under load. Increasing the sleep duration makes it more stable, but also makes the test slower.


Create NowTime

The solution is to avoid reading the current time directly from the business logic. For this purpose I've created the NowTime interface:

type NowTime interface {
Now() time.Time
NowPointer() *time.Time
}


The production implementation returns the real time:

type NowTimeImpl struct {
}

func ProduceNowTimeImpl() *NowTimeImpl {
return &NowTimeImpl{}
}

func (n *NowTimeImpl) Now() time.Time {
return time.Now()
}

func (n *NowTimeImpl) NowPointer() *time.Time {
now := n.Now()
return &now
}

NowPointer is useful since the GO time package returns time.Time and in many structs we keep a *time.Time.

Use NowTime

The token receives NowTime and no longer calls time.Now() directly:

type Token struct {
createdTime *time.Time
nowTime     NowTime
}

func NewToken(
nowTime NowTime,
) *Token {
return &Token{
createdTime: nowTime.NowPointer(),
nowTime:     nowTime,
}
}

func (t *Token) Expired() bool {
return t.nowTime.Now().Sub(*t.createdTime) >= time.Minute
}


In production we create the token with the real implementation:

token := NewToken(ProduceNowTimeImpl())


The application behavior did not change, but the source of the current time can now be replaced in a test.

Create NowTime Stub

The test implementation stores a fake time that can be changed without waiting:

type NowTimeStub struct {
fakeTime *time.Time
}

func ProduceNowTimeStub() *NowTimeStub {
return &NowTimeStub{}
}

func (n *NowTimeStub) SetFakeTime(
fakeTime *time.Time,
) {
n.fakeTime = fakeTime
}

func (n *NowTimeStub) Now() time.Time {
if n.fakeTime == nil {
return time.Now()
}

return *n.fakeTime
}

func (n *NowTimeStub) NowPointer() *time.Time {
now := n.Now()
return &now
}

func (n *NowTimeStub) IncrementFakeTime(
duration time.Duration,
) {
timestamp := n.fakeTime.Add(duration)
n.fakeTime = &timestamp
}

Fake Time Test

The test uses NowTimeStub and controls the current time:

func TestTokenExpired(t *testing.T) {
startTime := time.Date(2026, time.January, 1, 12, 0, 0, 0, time.UTC)
nowTime := ProduceNowTimeStub()
nowTime.SetFakeTime(&startTime)
token := NewToken(nowTime)

if token.Expired() {
t.Fatal("new token should not be expired")
}

nowTime.IncrementFakeTime(59 * time.Second)
if token.Expired() {
t.Fatal("token should still be valid")
}

nowTime.IncrementFakeTime(time.Second)
if !token.Expired() {
t.Fatal("token should be expired")
}
}

The test moves one minute forward immediately. It does not sleep, so it runs fast and always gets the same result. The same NowTimeStub can be supplied to multiple components, so the full application moves on the same fake timeline.

Timers and Tickers

Replacing time.Now is enough for expiration and elapsed time calculations. Code that uses time.NewTimer, time.After, or time.NewTicker requires more work, since these functions also wait for the real time.
A simple approach is to keep the timer outside the business logic. The timer triggers an operation, while the operation itself is tested directly with the fake clock. If testing the scheduling code is also required, the clock abstraction can provide timer and ticker functions, or we can use an existing fake clock library.

Avoid Time Comparison Problems

Tests should use a fixed start time instead of time.Now. This makes failures reproducible and prevents the expected values from changing on each run.
It is also recommended to use UTC in tests. Local time might include daylight saving changes, where adding a day and adding 24 hours do not always provide the same result.
For duration based logic, compare durations. For calendar based logic, such as the next day or next month, use time.AddDate and test the relevant timezone explicitly.

Final Note

Sleeping in a test is sometimes required when testing integration with a real external component, but it should not be the default method for testing application logic.
By injecting the current time or a clock, we can test minutes, days, and expiration boundaries in a few milliseconds. The tests become faster, stable, and much easier to reproduce.

Sunday, August 16, 2026

AI Agents Coding Bad Practices


 


In this post we will review how NOT to use AI agents for coding. I am creating this post since I've encountered too many cases of bad code created by AI agents due to people unaware of these issues. 


We now have AI agents to due our job, all we do is create a prompt and everything is done for us by the great LLM mind and the tools supplied by the agent. 

At least, that' what most managers think, but they are wrong.

An AI agent is a powerful tool that can reduce some tasks duration by order of magnitude. However it can also create an illusion of a great progress while you're actually causing damage in the long run. 

Some people are telling: "I am not a software engineer, but I have create this project, and I can add features with Claude code". They are wrong.

Using an AI agent to create a production ready code that can be used and maintained for a long time requires a human supervisor who understand the requirements for such a solution. This might change in the future, but for now it is mandatory. The AI agent create many mistakes in the planning stage, in the architecture domain, and in the implementation phase. You need an experienced software engineer to get a good product.

In case all you want is a simple one time task done, or a proof of concept, the AI agent can do this for you. However anything else must have include experience of building a production solution that an AI agent still does not posses.


Some examples I've encountered are below.



You create a product that processes data path requests, and the AI agent is creating the code using Python. 

Really? This could never hold for heavy load. Use a production ready language such as GO...



You run your product on an EC2 instance with multiple processes, but then you discover that some of the processes are terminating due to errors, so you ask AI to handle it, and it created long bash scripts to check the health of the processes and to restart them in case of failure.

Really? Use a kubernetes deployment with liveness and readiness probes...



You use AI agent to create the unit tests for your code, and it creates 10 unit tests each checking another aspect of the feature. You end up with 10000000 (add more zeros at your will) tests each checking a small aspect of the feature, duplicating most of the logic, each using a different approach to test the code.

Really? Add a tests infrastructure and combine similar tests flows into a single test...



You create an new microservice in the kubernetes deployment, reusing the project standards. You test run it locally and everything looks great, but then in production the service causes your entire deployment to be unresponsive. You try checking the logs but they are exploded with log records from the new service and you cannot find the problem.

Really? Choose the correct algorithm to perform your task. Use configurable limits to prevent resource .exhaustion.



Given all this, how should we use an AI agent?

Considering the current LLM maturity we should treat it as both professional and stupid. The risk in using AI agent is that it might take a wrong turn and hit a wall. Then it will build a bug hammer to break the wall. Then it will build a garbage truck to clear the leftovers.

We need to identify these wrong turns as soon as they start. This is done by coding near ~100% of the tasks using the AI agent, but use a small units of work and monitoring the changes. Instead of prompting: "I have a bug in this project that requests longer than 10K are not saved to the database, fix it!" split it to multiple steps. First ask the AI to identify the bug. Then think of the solution yourself (yes, actually use the brain muscle) and instruct the agent of the solution, like "In case the requests are longer than 10K, use a secondary table to save them. Start with the database schema changes only". Notice this cannot be done by a novice software engineer which is not aware of the system implications a solution alternatives. You can consult with the LLM, but again the LLM might provide the book-solution which is over complicated for your needs and maybe does not address the real problem (wrong turn again).


To sum:

AI agent is a great tool. Nowadays I can accomplish a super high throughput of tasks. To achieve this, it is important to understand the advantages and the disadvantages of the LLM. 



Saturday, August 1, 2026

GO Libraries as Multi Repo project



 

In this post we review the steps to create GO libraries as a multi repo project. Originally we have used a mono repo for our projects. Mono repo is good and easy solution for a product that need to evolve fast, keeps changing in terms of services and architecture. However for a more mature product with a large code base, and specifically for multiple products using a shared code this causes code duplication and redundant work.

These are the steps I've made to externalize the common libraries from a project into a common repo using by multiple products.

Libraries Code Updates

Create a new repo (bitbucket in this case) and move the libraries code to it. The libraries were already setup as GO modules, so this was pretty easy. 

I've added all the libraries under the go-lib folder, for example

~GIT-ROOT/go-lib/library1

The important thing to notice is using GO standard for the package name, for example the package name

com.my-company.project1.library1

is updated to 

bitbucket.org/my-company/common/go-lib/library1


In case of cross-library usage we can use replace in the go.mod. The replace command affects only when building the actual module and not when another project that uses the library is compiled. For example:

replace bitbucket.org/my-company/common/go-lib/library2 => ../library2

Libraries CI/CD

I wanted to automated the entire versioning of the libraries, so I called a script as part of the Jenkinsfile that loops over the list of libraries:

#!/usr/bin/env bash

set -e
cd "$(dirname "$0")"
buildVersion=$1
cloudCredentials=$2
fullVersion=v1.0.${buildVersion}
mapfile -t modules < modules.txt
tags=()
for module in "${modules[@]}"; do
  tag="${module}/${fullVersion}"
  git tag "${tag}"
  tags+=("${tag}")
done
git push https://"${cloudCredentials}"@bitbucket.org/my-company/common "${tags[@]}"


This script creates a tag according to the go standard, for example:

go-lib/library1/v1.0.1234

Consumer Project

The bitbucket repo is a private repo, and the consumer project CI/CD must use credentials to access the libraries. On the other hand local build are using private key to access the bitbucket. This was a bit tricky, but eventually I've updated the docker build to dynamically decide on the method to access the private bitbucket repo.

This is the result dockerfile related section:


#================
# Stage1: compile
#================
ARG dockerCacheRepo
FROM ${dockerCacheRepo}golang:1.26.4 AS go-compiler
ARG BITBUCKET_USE_SSH=true
ARG BB_USER
ARG BB_TOKEN
ENV GOPATH=/go
ENV GOBIN=/go/bin
ENV GOPRIVATE=bitbucket.org/my-company/*

...

# Configure Bitbucket authentication
RUN if [ "$BITBUCKET_USE_SSH" = "true" ]; then \
        echo "Using SSH authentication for Bitbucket" && \
        git config --global url."git@bitbucket.org:".insteadOf "https://bitbucket.org/" && \
        mkdir -p ~/.ssh && \
        ssh-keyscan bitbucket.org >> ~/.ssh/known_hosts; \
    else \
        echo "Using HTTPS authentication for Bitbucket" && \
        git config --global credential.helper store && \
        echo "https://${BB_USER}:${BB_TOKEN}@bitbucket.org" > ~/.git-credentials; \
    fi
# Download private dependencies
RUN --mount=type=ssh \
    go mod download


...

RUN go build -o /output/___PROJECT___

The ENV GOPRIVATE instructs GO to use credentials to access libraries with a specified prefix, and we have a BITBUCKET_USE_SSH flag to decide whether to use credentials or SSH.

Local Development

One last issue is that we still need IDE development to recognize the common repo, and hence each developer need to setup this on the GO configuration:


go env -w GOPRIVATE=bitbucket.org/my-company/*
git config --global url."git@bitbucket.org:".insteadOf https://bitbucket.org/


Final Note

Once all this is done we gain a smaller git repo for each project, and a central update of the common libraries. in addition each project directly controls the each library version in it go.mod, for example:

require bitbucket.org/my-company/common/go-lib/project1 v1.0.19

This gains a great flexability.


I've also added the script to update all dependencies using GO CLI:

go get -u ./...


and alternatively upgrade only the libraries using:

    go get \
      bitbucket.org/my-company/common/go-lib/library1@latest \
      bitbucket.org/my-company/common/go-lib/library2@latest 



Thursday, July 30, 2026

GO Coding Guideline as Agent Skill



 


In this rapidly growing AI age we are using agent to do part of our work (but not all of it yet). The best and stable method for now is an agent integrated into the IDE where we ask for small or medium size tasks.

When I started using it, I found myself constantly asking for changes after the agent had done its work. Many were syntax issues, others were design problems, and some are not following the project standards.

Then, I've decided to put some effort into this, and create a skill. Well, actually I've asked my agent to convert the GO coding guidelines into a skill, and after some minor optimizations, it worked. Now I find myself asking the agent to do the work, and the code generated looks as if I've created it myself. 

I am no longer required.


Create an Agent Skill 


To apply a skill to your agent, create .agents/skills/go-guidelines/SKILL.md


---
name: go-guidelines
description: Apply repository Go standards to implementation, modification, refactoring, and review, including production code, tests, APIs, concurrency, logging, errors, and configuration.
---
# Apply CTO Go guidelines
1. Inspect neighboring code and packages for established patterns.
2. Read only applicable references:
   - [core-style.md](references/core-style.md): always, for changes or reviews.
   - [functions-and-types.md](references/functions-and-types.md): functions, signatures, structs, collections, or packages.
   - [design.md](references/design.md): new components, refactors, dependencies, configuration, or serialization.
   - [errors-and-logging.md](references/errors-and-logging.md): fallible or logging code.
   - [runtime-and-security.md](references/runtime-and-security.md): locks, persistent memory, or backend responses containing secrets.
   - [test.md](references/test.md): unit testing.
3. Apply relevant rules while preserving correctness and public contracts.
4. Treat the references as repository policy. If a rule conflicts with correctness, security, generated-code requirements, or explicit instructions, stop and report it rather than weaken either requirement.
5. In the final response, mention material exceptions and verification performed; do not enumerate rules that were followed normally.



And the create the references files under .agents/skills/go-guidelines/references/


code-style.md


# Core style
## Naming
- Use complete, descriptive words, not abbreviations. Camel-case initialisms: `clientIpAddress`, not `clientIPAddress`.
- Give exported methods and exported struct fields names of at least two words (an IDE workaround).
- Use a one-letter receiver name consistently within a type's methods.
- Export only what another package must access.
## Existing conventions
- Inspect and follow nearby naming, flag, Kafka-topic, package, and construction patterns unless they violate a guideline.
- Do not introduce a competing convention in the same area.
## Comments and cleanup
- Prefer self-explanatory code. Comment only unusual behavior, non-obvious constraints, or required documentation; never narrate obvious code.
- Do not leave TODOs.
- Remove unused variables, functions, constants, comments, commented-out code, and obsolete branches.
- Remove code duplicating Go guarantees.
- Avoid duplicated logic. Extract a shared implementation when it improves clarity and maintains sensible coupling.
## Readability
- Keep nesting to at most three indentation levels. Prefer guard clauses or small cohesive functions.
- Limit function bodies to 15 executable lines. Split by responsibility without increasing coupling or creating trivial wrappers.
- Do not retain a function that only forwards to one simple statement unless it prevents meaningful duplication or provides a real abstraction boundary.



design.md




# Design and dependencies
## Cohesion and coupling
- Minimize parameter and field coupling; keep only shared dependencies and state.
- When functions repeatedly consume the same inputs, create a cohesive object that owns them instead of threading them through every call.
- Prefer object-oriented decomposition for clear ownership, encapsulation, and behavior. Every object must have a cohesive responsibility.
- Keep private implementation details unexported.
## Repository capabilities
- Before implementing common utilities, search and prefer `kit*` packages such as `kiterr`, `kitmap`, `kittime`, and `kitstring`.
- Add reusable, missing general-purpose operations to the appropriate `kit*` package instead of duplicating them locally.
## Configuration and constants
- Do not hard-code operational configuration.
- Put per-PO configuration on the PO and non-PO environment configuration through `core/config.go`.
- Create constants only for values used more than once, not merely to rename one-use literals.
## Serialization
- Add JSON tags only for external communication or Redis persistence, never internal-only structs.



errors-and-logging.md



# Errors and logging
## Error propagation
- Follow `kiterr`'s raise/recover model; do not add errors to ordinary returns.
- Recover or handle errors only at a genuine top-level boundary, such as a new goroutine entry point.
- Never hide an unexpected error. Raise it by default.
- Suppress only explicitly expected errors that retry or recovery cannot resolve; log them at WARN or higher with context.
- Do not conceal caller bugs or invalid state with nil checks, fallbacks, or skipped paths. Surface the defect.
## Diagnostic context
- Include relevant runtime values in raised errors; stack traces identify location, not failing input.
- Never include passwords, secrets, tokens, or other sensitive values in error details or logs.
## Logging
- Use the repository logger, never direct printing.
- Use `pologger` for messages related to a PO.
- Choose verbosity by expected frequency:
  - V1: approximately once per minute.
  - V2: approximately once per second to once per minute.
  - V5: more frequently than once per second.



functions-and-types.md



# Functions and types
## Functions and control flow
- Put every parameter, even a single one, on its own line per repository formatting and nearby syntax.
- Use booleans directly: `if enabled` or `if !enabled`; never compare with `true` or `false`.
- Handle every `switch` default; raise a contextual error if it is impossible or unsupported.
- Minimize function parameters. Do not pass a value separately when it is already available through another parameter.
- Keep shared method dependencies and state in receiver fields instead of repeatedly passing them.
## Structs and packages
- Define named struct types. Do not use anonymous inline structs.
- Keep one principal class-like component per package. Follow existing layout for related helpers and types; do not harm cohesion by mechanically creating packages.
- Pass and return struct pointers, including `time.Time`, unless a required interface or established immutable-value contract dictates otherwise.
- Store struct elements in slices as pointers: `[]*Item`, not `[]Item`. This concerns elements, not pointers to slices.
- Represent empty slices as `nil` unless an external serialization contract requires `[]`.
- Replace deeply nested or otherwise complex composite types with named structs that express each layer.



runtime-and-security.md



# Runtime safety and security
## Persistent memory
- Bound every persistent in-memory collection or cache.
- Use eviction, expiration, or another limit to prevent sustained traffic exhausting pod memory.
## Backend secrets
- Never return passwords from backend APIs to the frontend.
- Exclude credentials and equivalent secrets from response models, serialization, logs, and error details.
## Locking
- Guarantee lock cleanup on every exit, including raised errors.
- Prefer a small function that acquires the lock and immediately defers its unlock before protected work.
- Minimize critical sections; avoid unknown or blocking calls while locked when possible.



test.md



# Verification
- do not create unit test in the same folder as the tested source, tests live in `images/tests/src/project/tests`.
- Follow existing test conventions.
- Prefer end-to-end simulation over testing only the updated file.



Final Note

The nice thing about this skill is that is is split to multiple sections, so first the skill is loaded only when required, and second only the relevant sub skill items are loaded, so we save tokens and optimize the agent functionality.