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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .codex/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@ extends = ":workspace"
[permissions.local-web-dev-broad.network]
enabled = true
allow_local_binding = true

[mcp_servers.gopls]
command = ".bin/gopls"
args = ["mcp"]
cwd = "."
enabled = true
required = false
default_tools_approval_mode = "writes"
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.4"
go-version: "1.26.5"

- name: Run unit CI
run: make ci
36 changes: 36 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Fuzz

on:
workflow_dispatch:
schedule:
- cron: "0 3 * * 1"

permissions:
contents: read

jobs:
fuzz:
name: Native Go fuzzing
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.5"

- name: Fuzz untrusted input boundaries
run: make fuzz FUZZ_TIME=1m

- name: Upload crash corpus
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crash-corpus
path: |
internal/markdown/testdata/fuzz
internal/llm/openresponses/testdata/fuzz
if-no-files-found: ignore
2 changes: 1 addition & 1 deletion .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.4"
go-version: "1.26.5"

- name: Run container smoke test
run: make container-smoke
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ repos:
entry: make tidy-check
language: system
pass_filenames: false
- id: mod-verify
name: Go module cache verification
entry: make mod-verify
language: system
pass_filenames: false
- id: modernize-check
name: Go modernizer check
entry: make modernize-check
language: system
pass_filenames: false
- id: lint-fast
name: fast Go lint
entry: make lint-fast
Expand Down
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ Reference repositories live under `.agent/skills/references/*/repo` as skill-bac
- Keep generated artifacts, coverage files, binaries, local tools, and temporary files out of Git.
- Preserve clear package boundaries. Avoid adding abstractions before there is concrete duplication or complexity to remove.

## Go Agent Workflow

- Run `make tools` after cloning so the pinned Go development tools, including `gopls`, are available under `.bin/`.
- Prefer the project-scoped `gopls` MCP server for semantic navigation, references, diagnostics, and refactors. It runs in detached mode and sees saved files only.
- Treat `gopls`, compiler, and modernizer output as evidence to review, not as a substitute for tests or architectural judgment.
- Run `make modernize-check` after Go changes. If it reports a useful change, inspect the diff before running `make modernize`.
- During development, run the narrowest relevant Go test first. Before committing, format with `gofmt` and `goimports` and run `make all-tests`.

## Testing Standards

- Use red/green TDD for all core functionality
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.26.4-alpine AS builder
FROM golang:1.26.5-alpine AS builder

WORKDIR /src
COPY go.mod go.sum ./
Expand Down
34 changes: 31 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,17 @@ GOLANGCI_LINT_VERSION := v2.12.2
GOVULNCHECK_VERSION := v1.3.0
GOSEC_VERSION := v2.26.1
DEADCODE_VERSION := v0.45.0
GOPLS_VERSION := v0.23.0

GOIMPORTS := $(BIN_DIR)/goimports
GOLANGCI_LINT := $(BIN_DIR)/golangci-lint
GOVULNCHECK := $(BIN_DIR)/govulncheck
GOSEC := $(BIN_DIR)/gosec
DEADCODE := $(BIN_DIR)/deadcode
GOPLS := $(BIN_DIR)/gopls
COVERAGE_PROFILE ?= coverage.out
COVERAGE_MIN ?= 95.0
FUZZ_TIME ?= 30s

GO_FILES := $(shell find . \( -path './.agent/skills/references/*/repo' -o -path './third_party' -o -path './.cache' -o -path './.bin' -o -path './bin' \) -prune -o -name '*.go' -print)

Expand All @@ -54,7 +57,7 @@ COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || printf unknown)
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -X 'example.com/llm-chat-web/internal/buildinfo.Version=$(VERSION)' -X 'example.com/llm-chat-web/internal/buildinfo.Commit=$(COMMIT)' -X 'example.com/llm-chat-web/internal/buildinfo.Date=$(DATE)'

.PHONY: help cache-dirs fmt fmt-check imports imports-check tidy tidy-check test test-race coverage lint lint-fast vet vuln security deadcode build build-fake-responses image container-smoke serve-start serve-stop serve-status serve-restart serve-fake-start serve-fake-stop clean pre-commit all-tests ci tools
.PHONY: help cache-dirs fmt fmt-check imports imports-check tidy tidy-check mod-verify modernize modernize-check test test-race coverage fuzz lint lint-fast vet vuln security deadcode gopls build build-fake-responses image container-smoke serve-start serve-stop serve-status serve-restart serve-fake-start serve-fake-stop clean pre-commit all-tests ci tools

help:
@printf '%s\n' \
Expand All @@ -65,15 +68,20 @@ help:
' imports-check Check goimports formatting.' \
' tidy Run go mod tidy.' \
' tidy-check Check go.mod/go.sum tidiness.' \
' mod-verify Download and verify module cache contents.' \
' modernize Apply Go modernizers with go fix.' \
' modernize-check Check whether go fix would change Go files.' \
' test Run unit tests.' \
' test-race Run unit tests with the race detector.' \
' coverage Generate coverage.out, print summary, and enforce minimum coverage.' \
' fuzz Fuzz untrusted input boundaries (FUZZ_TIME per target).' \
' lint Run golangci-lint.' \
' lint-fast Run fast local lint checks.' \
' vet Run go vet.' \
' vuln Run govulncheck.' \
' security Run gosec.' \
' deadcode Run deadcode as an advisory check.' \
' gopls Install the pinned gopls language server.' \
' build Build bin/pyttechat.' \
' build-fake-responses Build bin/fake-responses.' \
' image Build the single-container image.' \
Expand Down Expand Up @@ -110,7 +118,12 @@ $(GOSEC): | $(BIN_DIR) cache-dirs
$(DEADCODE): | $(BIN_DIR) cache-dirs
GOBIN=$(BIN_DIR) $(GO) install golang.org/x/tools/cmd/deadcode@$(DEADCODE_VERSION)

tools: $(GOIMPORTS) $(GOLANGCI_LINT) $(GOVULNCHECK) $(GOSEC) $(DEADCODE)
$(GOPLS): | $(BIN_DIR) cache-dirs
GOBIN=$(BIN_DIR) $(GO) install golang.org/x/tools/gopls@$(GOPLS_VERSION)

tools: $(GOIMPORTS) $(GOLANGCI_LINT) $(GOVULNCHECK) $(GOSEC) $(DEADCODE) $(GOPLS)

gopls: $(GOPLS)

fmt:
$(GOFMT) -w $(GO_FILES)
Expand Down Expand Up @@ -139,6 +152,16 @@ tidy-check: cache-dirs
rm -rf "$$tmp_dir"; \
exit $$status

mod-verify: cache-dirs
$(GO) mod download
$(GO) mod verify

modernize: cache-dirs
$(GO) fix ./...

modernize-check: cache-dirs
$(GO) fix -diff ./...

test: cache-dirs
$(GO) test ./...

Expand All @@ -162,6 +185,11 @@ coverage: cache-dirs
printf "coverage %.1f%% meets required %.1f%%\n", total, minimum; \
}'

fuzz: cache-dirs
$(GO) test ./internal/markdown -run=^$$ -fuzz=^FuzzRendererRender$$ -fuzztime=$(FUZZ_TIME)
$(GO) test ./internal/markdown -run=^$$ -fuzz=^FuzzBlockStreamerChunking$$ -fuzztime=$(FUZZ_TIME)
$(GO) test ./internal/llm/openresponses -run=^$$ -fuzz=^FuzzOpenResponsesStream$$ -fuzztime=$(FUZZ_TIME)

lint: cache-dirs $(GOLANGCI_LINT)
$(GOLANGCI_LINT) run ./...

Expand Down Expand Up @@ -410,6 +438,6 @@ clean:
pre-commit: cache-dirs
pre-commit run --all-files

all-tests: fmt-check imports-check tidy-check vet lint-fast test test-race coverage build
all-tests: fmt-check imports-check tidy-check mod-verify modernize-check vet lint-fast test test-race coverage build

ci: all-tests lint vuln security deadcode
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,34 @@ curl -N http://localhost:8080/v1/responses \

Expected: an SSE stream with typed response lifecycle events, text deltas, `response.completed`, and final `data: [DONE]`.

## Agentic Go Tooling

Install the repository's pinned Go tools after cloning:

```sh
make tools
```

This includes `gopls`, which the trusted project configuration exposes to Codex as a non-required, approval-gated MCP server. Restart Codex after installing the tools, then use `codex mcp list` to confirm that `gopls` is available. The detached server sees saved files only; compiler and test results remain the source of truth.

Use the Go 1.26 modernizers and module-integrity checks directly when iterating:

```sh
make modernize-check
make mod-verify
```

Review the proposed modernization diff before applying it with `make modernize`.

Native fuzz targets cover untrusted Markdown rendering, streamed Markdown chunking, and OpenResponses SSE parsing. Run each target for 30 seconds by default, or choose a duration per target:

```sh
make fuzz
make fuzz FUZZ_TIME=10s
```

Regular unit tests run each fuzz seed deterministically. A separate scheduled workflow performs open-ended input generation without making pull-request checks nondeterministic.

## Pre-Commit

Install the hooks after cloning:
Expand All @@ -152,7 +180,7 @@ Run the same hooks manually:
make pre-commit
```

The pre-commit checks are intentionally fast: formatting, module tidiness, fast linting, and unit tests.
The pre-commit checks are intentionally fast: formatting, module tidiness and integrity, modernization, fast linting, and unit tests.

## Local CI

Expand All @@ -168,15 +196,15 @@ Run the broader PR-quality checks locally:
make ci
```

The Makefile installs pinned Go tools into `.bin/` when needed.
The Makefile installs pinned Go tools into `.bin/` when needed. `make all-tests` also rejects pending `go fix` modernizations and modified module-cache contents.

`make coverage` enforces 95.0% Go statement coverage. Container-level smoke coverage is kept separate from `all-tests`/`ci` because it requires Docker:

```sh
make container-smoke
```

The CI workflow in `.github/workflows/ci.yml` runs `make ci`, and the smoke workflow in `.github/workflows/smoke.yml` runs container smoke as its own CI job.
The CI workflow in `.github/workflows/ci.yml` runs `make ci`, the smoke workflow in `.github/workflows/smoke.yml` runs container smoke as its own CI job, and `.github/workflows/fuzz.yml` performs weekly and manually dispatched native fuzzing.

## Reference Skills

Expand Down
12 changes: 11 additions & 1 deletion docs/QUALITY_GATES.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Quality Gates

The current scaffold has a tiny deterministic unit test and local build/test tooling. Future application work should expand coverage before adding production behavior.
The repository uses deterministic commit gates, broader security checks, container smoke coverage, and separately scheduled native fuzzing.

## Current Checks

- `make fmt-check`
- `make imports-check`
- `make tidy-check`
- `make mod-verify` downloads the module graph and verifies cached contents against recorded checksums.
- `make modernize-check` fails when Go's modernizers would change source files.
- `make vet`
- `make lint-fast`
- `make test`
Expand All @@ -30,6 +32,14 @@ Smoke/integration coverage is intentionally separate from the normal Go quality

The smoke workflow builds the container image, starts the fake Responses API, registers a browser user through the server-rendered app, submits a chat turn, consumes the SSE stream, and verifies conversation persistence across a container restart.

Coverage-guided fuzzing is also separate so pull-request checks remain deterministic:

- `make fuzz` runs each target for `FUZZ_TIME`, defaulting to 30 seconds per target.
- `.github/workflows/fuzz.yml` runs weekly and on manual dispatch with one minute per target.
- Fuzz targets cover Markdown rendering and sanitization, streaming chunk boundaries, and upstream OpenResponses SSE parsing.

Every fuzz seed still runs during ordinary `go test`. If fuzzing finds a failure, fix the behavior and retain the minimized input under the generated `testdata/fuzz` corpus as a deterministic regression.

`make deadcode` is advisory during the scaffold phase because placeholder code can produce noisy findings. It should become strict once the app has a stable shape.

Project quality gates are scoped to this module's own Go packages. The reference submodules under `.agent/skills/references/*/repo` are read-only design references and are not part of lint, security, vulnerability, or deadcode enforcement.
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ module example.com/llm-chat-web

go 1.26

toolchain go1.26.5

require (
github.com/alecthomas/chroma/v2 v2.2.0
github.com/microcosm-cc/bluemonday v1.0.27
Expand Down Expand Up @@ -45,7 +47,7 @@ require (
golang.org/x/text v0.37.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.81.1 // indirect
google.golang.org/grpc v1.82.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1480,10 +1480,10 @@ func servedSSEData(t *testing.T, body, eventName string) []string {
t.Helper()

var matches []string
for _, raw := range strings.Split(strings.TrimSpace(body), "\n\n") {
for raw := range strings.SplitSeq(strings.TrimSpace(body), "\n\n") {
var event string
var data strings.Builder
for _, line := range strings.Split(raw, "\n") {
for line := range strings.SplitSeq(raw, "\n") {
switch {
case strings.HasPrefix(line, "event: "):
event = strings.TrimPrefix(line, "event: ")
Expand Down
7 changes: 4 additions & 3 deletions internal/llm/llm.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package llm

import (
"context"
"strings"
"time"
)

Expand Down Expand Up @@ -63,13 +64,13 @@ func NewTextMessage(role Role, text string) Message {
}

func (m Message) Text() string {
var text string
var text strings.Builder
for _, part := range m.Parts {
if part.Type == PartText {
text += part.Text
text.WriteString(part.Text)
}
}
return text
return text.String()
}

func (m Message) Clone() Message {
Expand Down
Loading
Loading