Agent-first Workflow
Agent-first Workflow
Section titled “Agent-first Workflow”Plumego is not only a Go HTTP toolkit. It has a machine-readable control plane that tells both humans and AI coding assistants how to classify work before editing code.
Read Repository Boundaries first if you are still learning how ownership is classified. Use this page when the question is specifically how AI agents interact with the repository.
The problem this solves
Section titled “The problem this solves”Most AI coding assistants write code that compiles and passes tests but violates architectural conventions — not because the model is wrong, but because the conventions live in human knowledge and team memory rather than in a form the model can read.
Plumego externalizes these conventions:
| Convention | Where it lives | Machine-readable? |
|---|---|---|
| Which module owns this work type | specs/task-routing.yaml | Yes |
| Which imports are allowed between layers | specs/dependency-rules.yaml | Yes |
| What a correct change sequence looks like | specs/change-recipes/ | Yes |
| What a module promises locally | <module>/module.yaml | Yes |
| What gates must pass before a change lands | internal/checks/* | Yes — enforced in CI |
Control-plane map from an agent perspective
Section titled “Control-plane map from an agent perspective”specs/task-routing.yaml → route work to owning module before writing codespecs/dependency-rules.yaml → verify import boundaries are not violatedspecs/change-recipes/ → follow a defined sequence for known change types<module>/module.yaml → read local scope, risk, and validation rulesinternal/checks/* → run boundary and manifest checkers before commitmake gates → run the full CI-equivalent gate locallyWhat an agent reads before editing
Section titled “What an agent reads before editing”| Step | What to read | File |
|---|---|---|
| 1 | Which module owns this work type? | specs/task-routing.yaml |
| 2 | Which imports are allowed? | specs/dependency-rules.yaml |
| 3 | Is there a predefined recipe? | specs/change-recipes/<type>.yaml |
| 4 | What does the module promise locally? | <module>/module.yaml |
| 5 | Which checkers apply? | internal/checks/ |
Reading in this order means the agent classifies ownership before it opens any Go file. The classification step is intentionally front-loaded so that boundary violations appear in the plan, not in the diff.
Work routing via task-routing.yaml
Section titled “Work routing via task-routing.yaml”specs/task-routing.yaml maps work descriptions to owning modules. An agent evaluating “add rate limiting to the inbound greet endpoint” can read the routing table to confirm this belongs in middleware (transport-layer rate limiting) rather than x/resilience (outbound circuit protection).
This is the same classification humans do in PR review — Plumego just makes the rule explicit enough that an agent can apply it before writing code.
Routing in practice — a worked example
Section titled “Routing in practice — a worked example”Task description: “add rate limiting to the inbound greet endpoint”
Step 1 — read the routing rules from specs/task-routing.yaml:
# specs/task-routing.yaml (excerpt)routing_rules: stable_root_work: intent: Change kernel, lifecycle, route structure, transport contracts, transport middleware, auth primitives, or storage primitives. destination: stable packages: - middleware # ← transport-layer rate limiting lives here - core - router - contract - security - store - health - log - metrics
extension_work: intent: Change product capability, business feature, protocol adaptation, or extension behavior. destination: extension primary_families: - x/resilience # ← outbound circuit protection lives here - x/tenant - x/gateway # ...Step 2 — classify:
| Signal in the task | Routing match | Owner |
|---|---|---|
| ”inbound” | Transport concern, not product/business logic | stable_root_work |
| ”rate limiting” on the inbound path | Transport middleware, not circuit breaker | middleware |
| ”greet endpoint” | Route registration in app-local wiring | app_wiring → consult reference/standard-service |
Step 3 — confirm the owning module task entry:
tasks: middleware: start_with: - middleware/module.yaml - docs/modules/middleware/README.md - docs/reference/canonical-style-guide.md avoid: - core - contractResult: the change belongs in middleware/, starting from middleware/module.yaml. x/resilience is for outbound circuit protection — a different classification. Opening x/resilience first would be an ownership error the routing table prevents.
The routing table does not replace reading code. It narrows the search space before the agent opens any Go file.
Try it — select a scenario to see the routing decision:
Given a task, which module does the routing table assign?
Select a scenario to see the classification:
Add rate limiting to an inbound HTTP endpoint (e.g. the greet route).
Start with
middleware/module.yamldocs/modules/middleware/README.mddocs/reference/canonical-style-guide.md
Avoid
corecontract
Why this routing
"Inbound" maps to transport layer. Rate limiting at the request boundary is middleware work — not outbound circuit protection (x/resilience). Destination: middleware/.
Fix a defect where an HTTP handler returns the wrong status or malformed response body.
Start with
AGENTS.mddocs/reference/canonical-style-guide.mdspecs/change-recipes/http-endpoint-bugfix.yamlreference/standard-service/internal/app/routes.go- +2 more
Avoid
middlewarex/gateway
Why this routing
Endpoint defect → explicit http_endpoint_bugfix recipe. Starts from contract + router; avoids middleware to keep transport wiring clean.
Add per-tenant JWT validation, quota enforcement, or policy evaluation.
Start with
AGENTS.mdspecs/change-recipes/tenant-policy-change.yamlx/tenant/module.yamldocs/concepts/x-tenant-blueprint.md- +1 more
Avoid
middlewarestorecore
Why this routing
Tenant identity and policy live in x/tenant, not generic auth middleware. Stable roots carry the HTTP baseline; x/tenant carries the tenant layer.
Add an endpoint that streams responses from an AI provider (e.g. OpenAI-compatible SSE).
Start with
x/ai/module.yamldocs/modules/x-ai/README.mdx/ai/entrypoints.go
Avoid
coreroutermiddleware
Why this routing
AI capability is product work, not transport infrastructure. extension_work → x/ai. Stable roots remain compatible as AI API shapes evolve.
Add WebSocket upgrade handling to an existing HTTP server.
Start with
x/websocket/module.yamldocs/modules/x/websocket/README.mdx/websocket/websocket.go
Avoid
corerouter
Why this routing
Protocol adaptation (HTTP → WS) is extension work. x/websocket coexists with standard HTTP routes using the same middleware chain.
Add reverse proxy, upstream routing, or load balancing at the edge.
Start with
x/gateway/module.yamlx/gateway/entrypoints.gospecs/extension-taxonomy.yaml
Avoid
x/restreference/standard-service
Why this routing
Edge transport is gateway_edge_transport task → x/gateway. Does not change the stable routing model used by upstream services.
Add multipart file upload and download handling.
Start with
x/fileapi/module.yamldocs/modules/x/fileapi/README.mdx/fileapi/handler.go
Avoid
store/filex/data/file
Why this routing
File upload is product capability, not a storage primitive. file_api task → x/fileapi. Avoids store/file and x/data/file to keep primitives separate.
Add or restructure routes and handler wiring in an existing service.
Start with
reference/standard-service/main.goreference/standard-service/internal/app/app.goreference/standard-service/internal/app/routes.go
Avoid
x/restx/gatewayx/webhook
Why this routing
Route registration is app-local wiring, not a stable root change. app_wiring → reference/standard-service internal/app/routes.go.
Update the allowed import directions between layers in specs/dependency-rules.yaml.
Start with
specs/dependency-rules.yamlspecs/repo.yamlAGENTS.md
Avoid
reference/standard-servicecmd/plumego
Why this routing
Architecture rule changes belong in the control plane. repo_rules → specs/. Do not edit Go packages for this — the rule is machine-readable only.
Evaluate or add a new package to the stable roots (e.g. a new transport primitive).
Start with
AGENTS.mdspecs/change-recipes/stable-root-boundary-review.yamlspecs/repo.yamlspecs/dependency-rules.yaml- +9 more
Avoid
reference/with-messagingreference/with-gateway
Why this routing
Stable root expansion requires a boundary review recipe before coding. stable_root_boundary_review → reads all nine existing stable root manifests.
Boundary enforcement
Section titled “Boundary enforcement”specs/dependency-rules.yaml defines the allowed import directions between layers:
- Stable roots may not import from
x/* x/*families may import from stable roots- App-local code may import from both
internal/checks/dependency-rules enforces this rule mechanically. If an agent adds an import that violates the boundary, the gate fails:
go run ./internal/checks/dependency-rulesThis check runs as part of make gates and is required before any change lands. It does not rely on reviewer memory.
Change recipes
Section titled “Change recipes”specs/change-recipes/ contains YAML files that define the correct execution sequence for known change types. Each recipe specifies: the scope, ordered steps, and stop conditions (things the recipe must not do).
Full recipe index
Section titled “Full recipe index”| Recipe | Use when |
|---|---|
add-http-endpoint.yaml | Adding a new route and handler in app-local code |
add-middleware.yaml | Adding transport-only middleware to the middleware package |
fix-bug.yaml | Localizing and fixing a defect with regression tests |
http-endpoint-bugfix.yaml | Fixing a defect in HTTP handlers, route wiring, or transport contracts |
symbol-change.yaml | Renaming, removing, or changing the behavior of an exported symbol |
new-extension-module.yaml | Creating a new x/* capability family |
new-stable-module.yaml | Adding a new package to the stable roots |
add-websocket-room.yaml | Adding a new room type or connection policy to x/websocket |
add-grpc-method.yaml | Adding a gRPC method alongside an existing HTTP surface via x/rpc |
add-ai-tool.yaml | Registering a new tool in x/ai/tool or wiring it into a session |
add-acceptance-tests.yaml | Writing pre-failing acceptance tests that define a task card’s done condition |
tenant-policy-change.yaml | Changing tenant resolution, policy evaluation, quota, or rate-limit behavior |
stable-root-boundary-review.yaml | Review-only safety check for stable-root boundary changes |
analysis-only.yaml | Research and planning tasks — no file edits allowed |
review-only.yaml | Code review tasks — findings only, no patch |
Worked example: adding a new HTTP endpoint
Section titled “Worked example: adding a new HTTP endpoint”Task: “Add a GET /users/:id handler that returns user details.”
Step 1 — classify using specs/task-routing.yaml. The task is app-local HTTP feature work → routing entry is app_wiring, recipe is add-http-endpoint.
Step 2 — read the recipe:
# specs/change-recipes/add-http-endpoint.yaml (excerpt)name: add-http-endpointscope: app-local HTTP feature worksteps: - Read docs/reference/canonical-style-guide.md. - Read specs/repo.yaml and the target module manifest. - Open reference/standard-service/internal/app/routes.go. - Add one explicit route registration line. - Add or update a handler with net/http shape. - Use contract.WriteError for structured error responses. - Add or update focused tests near the changed handler or module. - Run module tests, then repository quality gates if the change is cross-cutting.stop_conditions: - Do not add bootstrap behavior in x/*. - Do not hide dependencies in request context.Step 3 — follow the steps in order:
mux.Get("/users/:id", handlers.GetUser)
// reference/standard-service/internal/handlers/users.gofunc GetUser(w http.ResponseWriter, r *http.Request) { id := router.Param(r, "id") user, err := svc.FindUser(r.Context(), id) if err != nil { _ = contract.WriteError(w, r, contract.NewErrorBuilder(). WithType(contract.TypeNotFound). WithMessage("user not found"). Build()) return } _ = contract.WriteResponse(w, r, http.StatusOK, user, nil)}Step 4 — run validation per the recipe:
go test -race ./internal/...go run ./internal/checks/dependency-rulesResult: the recipe removes ambiguity about where the handler lives, what response helpers to use, and which tests to run — without requiring reviewer memory for any of it.
An agent following a recipe reads the defined steps, completes each one, then runs the checkers specified in the recipe before considering the change done.
Running gates after agent-generated changes
Section titled “Running gates after agent-generated changes”After any agent-generated change, run the full gate before pushing:
make gatesmake gates mirrors CI. It includes:
gofmt -l .— format checkgo vet ./...— static analysisgo test ./...— test suitego run ./internal/checks/dependency-rules— boundary checkgo run ./internal/checks/agent-workflow— workflow compliancego run ./internal/checks/module-manifests— manifest consistencygo run ./internal/checks/reference-layout— reference app shape
If any check fails, fix the specific failure before re-running. Do not bypass with --no-verify or skip individual checkers.
When an agent change crosses a boundary
Section titled “When an agent change crosses a boundary”If an agent’s change spans a stable root and an x/* extension — for example, a change that modifies middleware and x/resilience together — treat these as separate changes and classify each independently:
- Read
specs/task-routing.yamlfor each work type in the change - Confirm the owning module for each
- If they belong to different owners, split the change before editing
Crossing a boundary in a single commit is not automatically wrong, but it must be deliberate: the PR description should explain why the change spans layers and which recipe governs each part.
Read next
Section titled “Read next”| Next question | Page |
|---|---|
| How is work classified in general? | Repository Boundaries |
Which stable root or x/* family should I open? | Modules Overview |
| What does release posture mean for adoption? | Release Posture |