feat: add harness engineering layer for agent workflows (#24791)

This PR adds an opinionated harness-engineering layer for agent-driven
workflows: a small set of agent-readable docs, mechanical structure
checks, structured CI failure summaries, an architecture-lint umbrella,
and per-worktree dev-server isolation. The goal is to make local dev,
tests, and CI mechanically inspectable by agents without changing app
runtime behavior.

## What landed

**Agent docs and navigation**
- `.claude/docs/OBSERVABILITY.md`, `.claude/docs/DEV_ISOLATION.md`,
`.claude/docs/AGENT_FAILURES.md`: task-oriented guides for logs,
tracing, Prometheus, dev-server isolation, and a seeded failure catalog.
- `AGENTS.md`: added an `Agent navigation` block, then trimmed the file
from 375 to 229 lines by migrating duplicated detail into
`WORKFLOWS.md`, `GO.md`, `TESTING.md`, and `DATABASE.md`. The
user-managed custom-instructions block is preserved.
- `.agents/docs`: symlink mirror of `.claude/docs` for agent runtimes
that look under `.agents`.

**Mechanical checks**
- `scripts/check_agents_structure.sh`: validates `@...` references in
tracked `AGENTS.md` files and warns when root grows past 600 lines.
Wired as `make lint/agents` and into `make lint`.
- `scripts/audit-agent-readiness.sh`: report-first audit of harness
readiness. Currently `10 ok, 0 warn, 0 fail`.
- `scripts/check_architecture.sh` / `make lint/architecture`: umbrella
architecture-lint target. Consolidates the existing
`check_enterprise_imports.sh` and `check_codersdk_imports.sh` so they
run exactly once via the umbrella. Slot is open for new high-confidence
rules.

**Structured CI failure summaries**
- `scripts/playwright-failure-summary.sh`: parses
`site/test-results/results.json` and writes Markdown to
`$GITHUB_STEP_SUMMARY` on failure. Wired into the `test-e2e` matrix job.
- `scripts/go-test-failure-summary.sh`: parses `go test -json`
line-delimited output the same way. Wired into `test-go-pg`,
`test-go-pg-17`, and `test-go-race-pg` by injecting `gotestsum
--jsonfile` in the workflow without touching `Makefile`. JSON also
uploaded as a CI artifact on failure.
- `site/e2e/playwright.config.ts`: enables `screenshot:
only-on-failure`, `trace: retain-on-failure`, JSON reporter, and HTML
reporter alongside existing reporters.
- `.github/workflows/ci.yaml`: failure artifact uploads for Playwright
now use `if: failure()` and predictable names
(`playwright-artifacts-<variant>-<sha>`).

**Per-worktree dev-server isolation** (`scripts/develop/main.go`)
- Deterministic FNV-64a hash of the worktree path produces a port offset
in `[0, 1000)` (50 buckets, step 20 to avoid API/proxy overlap across
adjacent buckets).
- Offset is applied only to defaults; both env vars (`CODER_DEV_PORT`,
`CODER_DEV_WEB_PORT`, `CODER_DEV_PROXY_PORT`,
`CODER_DEV_PROMETHEUS_PORT`) and CLI flags retain priority.
- Hardcoded ports `9090` (embedded Prometheus UI) and `12345` (Delve)
are unchanged by design.
- Startup banner shows each port's source: `default`, `offset`, or
`explicit`.
- Unit tests in `scripts/develop/main_test.go` cover determinism,
bounds, no-overlap across the four ports, and explicit-skip behavior.
- State (`.coderv2/`) was already worktree-isolated via `os.Getwd()`, so
no state-dir changes were needed.

## Validation

`make lint/agents`, `make lint/architecture`, `make lint/emdash`, `bash
scripts/audit-agent-readiness.sh` (10 ok, 0 warn, 0 fail), `shellcheck`
on all 5 new scripts, `go test ./scripts/develop/...`, and `js-yaml`
parse of `ci.yaml` all pass. Synthetic fixtures verify both
failure-summary scripts handle empty/missing input (silent exit 0),
ANSI-stripped output, and parent/subtest formatting.

## Known follow-ups (deferred)

- Frontend Storybook/Vitest failure summary: lowest-leverage slice of
the failure-summary work. Skipping until observed pain.
- Architecture lint currently only delegates to existing import checks;
new rules (`InTx` outer-store detection, swagger-annotation lint) plug
in as needed.
- 50 port-offset buckets means two worktree paths can occasionally
collide. The DEV_ISOLATION doc tells users to set the relevant env var
when this happens.

> Mux opened this PR on Mike's behalf.
This commit is contained in:
Michael Suchacz
2026-05-11 17:27:29 +02:00
committed by GitHub
parent 915956460a
commit 85792d08bc
20 changed files with 1561 additions and 237 deletions
+164 -27
View File
@@ -10,6 +10,7 @@ import (
"context"
"encoding/json"
"fmt"
"hash/fnv"
"net"
"net/http"
"net/url"
@@ -52,7 +53,13 @@ const (
prometheusContainerName = "coder-prometheus"
// defaultPrometheusPort avoids 2112 (agent prometheus) and
// 2113 (agent debug) already bound inside Coder workspaces.
defaultPrometheusPort = "2114"
defaultPrometheusPort = "2114"
// portOffsetBuckets keeps the offset below 1000 while leaving
// enough hash buckets for common multi-worktree use.
portOffsetBuckets = 50
// portOffsetStep avoids overlap between the default API and proxy
// ports when two worktrees land in adjacent buckets.
portOffsetStep = 20
prometheusImage = "prom/prometheus:v3.11.2"
defaultAccessURL = "http://127.0.0.1:%d"
defaultPassword = "SomeSecurePassword!"
@@ -96,6 +103,13 @@ func main() {
Description: "Prometheus metrics port. Set to 0 to disable.",
Value: serpent.Int64Of(&cfg.coderMetricsPort),
},
{
Flag: "port-offset",
Env: "CODER_DEV_PORT_OFFSET",
Default: "false",
Description: "Apply a deterministic per-worktree offset to default API, web, proxy, and Coder metrics ports. Useful when running multiple worktrees in parallel.",
Value: serpent.BoolOf(&cfg.portOffsetEnabled),
},
{
Flag: "prometheus-server",
Env: "CODER_DEV_PROMETHEUS_SERVER",
@@ -171,12 +185,13 @@ func main() {
},
Handler: func(inv *serpent.Invocation) error {
cfg.serverExtraArgs = inv.Args
cfg.portExplicit = portExplicitFromInvocation(inv)
logger := slog.Make(sloghuman.Sink(inv.Stderr))
if err := cfg.validate(); err != nil {
if err := cfg.resolveEnv(); err != nil {
return err
}
if err := cfg.resolveEnv(); err != nil {
if err := cfg.validate(); err != nil {
return err
}
return develop(inv.Context(), logger, &cfg)
@@ -191,30 +206,123 @@ func main() {
}
type devConfig struct {
apiPort int64
webPort int64
proxyPort int64
coderMetricsPort int64
prometheusServer bool
agpl bool
accessURL string
password string
useProxy bool
debug bool
skipSetup bool
multiOrg bool
starterTemplate string
dbRollback bool
dbReset bool
dbContinue bool
projectRoot string
binaryPath string
configDir string
childEnv []string
apiPort int64
webPort int64
proxyPort int64
coderMetricsPort int64
portOffsetEnabled bool
prometheusServer bool
agpl bool
accessURL string
password string
useProxy bool
debug bool
skipSetup bool
multiOrg bool
starterTemplate string
dbRollback bool
dbReset bool
dbContinue bool
projectRoot string
binaryPath string
configDir string
childEnv []string
portExplicit portExplicit
portOffset int
apiPortSource portSource
webPortSource portSource
proxyPortSource portSource
metricsPortSource portSource
// Extra args after flags forwarded to "coder server".
serverExtraArgs []string
}
type portExplicit struct {
api bool
web bool
proxy bool
metrics bool
}
type portSource string
const (
portSourceDefault portSource = "default"
portSourceExplicit portSource = "explicit"
portSourceOffset portSource = "offset"
)
func portExplicitFromInvocation(inv *serpent.Invocation) portExplicit {
return portExplicit{
api: isPortExplicit(inv, "port", "CODER_DEV_PORT"),
web: isPortExplicit(inv, "web-port", "CODER_DEV_WEB_PORT"),
proxy: isPortExplicit(inv, "proxy-port", "CODER_DEV_PROXY_PORT"),
metrics: isPortExplicit(inv, "prometheus-port", "CODER_DEV_PROMETHEUS_PORT"),
}
}
func isPortExplicit(inv *serpent.Invocation, flagName, envName string) bool {
if flag := inv.ParsedFlags().Lookup(flagName); flag != nil && flag.Changed {
return true
}
if val, ok := inv.Environ.Lookup(envName); ok && val != "" {
return true
}
for _, opt := range inv.Command.Options {
if opt.Flag == flagName {
return opt.ValueSource == serpent.ValueSourceFlag ||
opt.ValueSource == serpent.ValueSourceEnv
}
}
return false
}
// portOffset returns a deterministic offset in [0, 1000) derived from the
// worktree path. Successive callers with the same projectRoot get the same
// offset; different projectRoots get different offsets with high probability.
func portOffset(projectRoot string) int {
h := fnv.New64a()
_, _ = h.Write([]byte(projectRoot))
bucket := h.Sum64() % uint64(portOffsetBuckets)
return int(bucket) * portOffsetStep //nolint:gosec // Bucket is less than portOffsetBuckets.
}
func (c *devConfig) applyPortOffset() {
c.portOffset = 0
if !c.portOffsetEnabled {
return
}
c.portOffset = portOffset(c.projectRoot)
if c.portExplicit.api {
c.apiPortSource = portSourceExplicit
} else {
c.apiPortSource = c.applyDefaultPortOffset(&c.apiPort)
}
if c.portExplicit.web {
c.webPortSource = portSourceExplicit
} else {
c.webPortSource = c.applyDefaultPortOffset(&c.webPort)
}
if c.portExplicit.proxy {
c.proxyPortSource = portSourceExplicit
} else {
c.proxyPortSource = c.applyDefaultPortOffset(&c.proxyPort)
}
if c.portExplicit.metrics {
c.metricsPortSource = portSourceExplicit
} else {
c.metricsPortSource = c.applyDefaultPortOffset(&c.coderMetricsPort)
}
}
func (c *devConfig) applyDefaultPortOffset(port *int64) portSource {
if c.portOffset == 0 {
return portSourceDefault
}
*port += int64(c.portOffset)
return portSourceOffset
}
func (c *devConfig) validate() error {
if c.agpl && c.useProxy {
return xerrors.New("cannot use both --agpl and --use-proxy")
@@ -293,10 +401,6 @@ func (c *devConfig) validate() error {
// resolveEnv sets defaults, unsets leaked credentials, resolves
// filesystem paths, and computes the child process environment.
func (c *devConfig) resolveEnv() error {
if strings.Contains(c.accessURL, "%d") {
c.accessURL = fmt.Sprintf(c.accessURL, c.apiPort)
}
// Prevent inherited credentials from leaking into child
// processes or being picked up by config reads.
_ = os.Unsetenv("CODER_SESSION_TOKEN")
@@ -311,6 +415,11 @@ func (c *devConfig) resolveEnv() error {
fmt.Sprintf("coder_%s_%s", runtime.GOOS, runtime.GOARCH))
c.configDir = filepath.Join(c.projectRoot, ".coderv2")
c.applyPortOffset()
if strings.Contains(c.accessURL, "%d") {
c.accessURL = fmt.Sprintf(c.accessURL, c.apiPort)
}
// Compute once, reused by cmd().
c.childEnv = filterEnv(os.Environ(), "CODER_SESSION_TOKEN", "CODER_URL")
@@ -1120,6 +1229,28 @@ func prometheusBannerEntry(cfg *devConfig, prometheusServerStarted bool) (label
}
}
func portBannerLine(label string, port int64, source portSource, offset int) string {
portValue := strconv.FormatInt(port, 10)
if port == 0 {
portValue = "disabled"
}
if source == "" {
return fmt.Sprintf("%s: %s", label, portValue)
}
return fmt.Sprintf("%s: %s (%s)", label, portValue, portSourceLabel(source, offset))
}
func portSourceLabel(source portSource, offset int) string {
switch source {
case portSourceExplicit:
return fmt.Sprintf("explicit, offset +%d skipped", offset)
case portSourceOffset:
return fmt.Sprintf("offset +%d", offset)
default:
return fmt.Sprintf("default, offset +%d", offset)
}
}
func printBanner(ctx context.Context, logger slog.Logger, cfg *devConfig, prometheusServerStarted bool) {
ifaces := []string{"localhost"}
if addrs, err := net.InterfaceAddrs(); err == nil {
@@ -1153,6 +1284,12 @@ func printBanner(ctx context.Context, logger slog.Logger, cfg *devConfig, promet
"",
indent("Coder is now running in development mode."),
"",
"Effective ports:",
indent(portBannerLine("API", cfg.apiPort, cfg.apiPortSource, cfg.portOffset)),
indent(portBannerLine("Web UI", cfg.webPort, cfg.webPortSource, cfg.portOffset)),
indent(portBannerLine("Proxy", cfg.proxyPort, cfg.proxyPortSource, cfg.portOffset)),
indent(portBannerLine("Coder metrics", cfg.coderMetricsPort, cfg.metricsPortSource, cfg.portOffset)),
"",
"API:",
)