AGENTS.md and docs/specs/code-style.md expand them into concrete module boundaries, dependency rules, error semantics, and time semantics. This page is a reader’s guide to those two files, so that you know what will get sent back in review before you write any code.
Three principles
Sans-IO: the default rule
Sans-IO: the default rule
A new subsystem first defines a pure in-memory core that describes its behavior with explicit events and commands, and only then wraps adapters around it. The core only maintains the state machine, idempotency decisions, phase advancement, and result convergence; RPC, UDS, etcd, databases, clocks, logging, and metrics all stay in adapters.
WorkerCore (internal/worker/core/), CoordinatorCore (internal/coordinator/core/), and DispatcherCore (internal/worker/core/dispatcher.go) in the repository all take this shape. They are code boundaries, not new processes or roles.Scoped Resource Context: the rule for resource-governance subsystems
Scoped Resource Context: the rule for resource-governance subsystems
Modules such as IO access, which revolve around who owns a resource and when it is released, are not forced into an event/command stream. They explicitly model a Worker-level shared scope and task-level local scopes, and bind quota, cache, singleflight, deadline, and cleanup to the scope lifecycle. Scope exit is a structural cleanup boundary; it does not rely on “sending a release command later”.
internal/io/ takes this shape; its entry points are synchronous interfaces such as WorkerIOScope / TaskIOScope.Occam's Razor: constrains both spec and code
Occam's Razor: constrains both spec and code
Do not multiply entities beyond necessity. Before adding a struct, interface, error code, queue, state, or service, first ask whether an existing concept can be reused or simplified. If you cannot clearly explain why the existing
module / core / adapters / scope boundaries cannot accommodate a new entity, do not add it yet.AGENTS.md: if a behavior is lifecycle-heavy (state advancement of slots, tasks, and calls), express it with explicit states, events, and commands; if it is resource-governance-heavy (connection pools, quotas, caches), express it with explicit scope ownership, acquisition, and cleanup rules; if neither framework yields a small, enumerable set of rules, the design is still too vague.
Layering and dependency rules
Do not introduce catch-all directories such as
pkg/utils, common, or shared without clear boundaries. The existing internal/common/ in the repository only holds a few small packages with a clear purpose — argspool, clientid, types, and unsafebytes — and is not a general-purpose junk drawer.
Interaction pattern between adapters and the core
The recommended pattern is the one-way “input event → core decision → adapter executes commands”, rather than having the core hold a set of reverse callbacks. The sketch fromdocs/specs/code-style.md:
Using interfaces
- Define interfaces on the side that actually consumes them.
- When adapters call the core, there is usually no need to abstract an interface for the core; depend directly on concrete types such as
WorkerCoreandCoordinatorCore. - Only when the core genuinely needs a synchronous port, and that port can stay pure, narrow, and replaceable, define a minimal interface in
core/for adapters to implement. - Do not extract interfaces ahead of time for “possible future extension” before there is a second implementation or a clear need for a test double.
Naming and modeling
- Package names are short, stable, and domain-specific; prefer nouns.
- Use clear names for exported identifiers; avoid weak suffixes such as
Manager,Util,Data, andInfo. - States, events, and commands reflect domain semantics directly, for example
TaskPhaseFinished,PrepareTaskActivation, andReleaseSlot. - Prefer explicit structs and named enums; do not overuse
map[string]any,any, or scattered string literals. - When you need a combination of several boolean fields to understand an object’s state, switch to an explicit state enum.
- Use
NewXxxonly when it establishes invariants; a literal wrapper does not need a forced constructor.
Error and result expression
- First distinguish “admission / protocol errors” from “terminal results after execution has started”; do not mix them into one set of error returns. On the Worker, the boundary is whether the slot has been activated from
ALLOCATEDtoRUNNING: errors before activation are submission / admission-layer errors, and failures after activation all go intoTaskResult. - Stable semantics rely on error codes, enums, or named result types, not on error strings.
- When an external protocol already has stable error code conventions, the protocol fields are authoritative; do not invent parallel semantics inside adapters.
- In a Sans-IO core, model expected business outcomes as explicit results; do not overuse
error. - Boundaries such as
TaskResult,InvalidSlot,NoSlot, andUnavailablemust be consistent withdocs/specs/architecture.mdanddocs/specs/worker.md.
Time semantics
- Model time semantics centrally; do not scatter them across handlers, Plugins, and drivers, each making its own judgment.
- On the Worker side, slot TTL, task deadline, and the result retention window are concentrated in the Worker module and its timer adapter, with
docs/specs/worker.mdas the authority. - On the Coordinator side, view freshness, candidate retries, and placement-related time semantics are concentrated in the Coordinator module, with
docs/specs/task-resource-coordinator.mdas the authority. - In scope-oriented modules, the end of a lifecycle is guaranteed primarily by scope
Close(), context cancellation, and structural cleanup, supplemented by defenses against late results. - The creation, exit, and reclamation paths of goroutines must be clear; do not leave lifecycle management to process exit as a fallback.
Keeping tests and docs in sync
core/tests focus on state transitions, idempotency, TTL, deadline, and failure convergence; prefer table-driven tests.- Tests for scope-oriented modules focus on quota acquisition / release, scope closing, dropping late results, singleflight behavior, and the absence of resource leaks.
- Adapter tests focus on protocol mapping, error translation, and command execution orchestration; do not repeat the state combinations already covered by the core.
- When you change protocol or lifecycle semantics, update the tests and the corresponding spec in the same change.
Related docs
AGENTS.mdin the blockx repository: repository-level principles, directory skeleton, test baseline, commit / PR rules.docs/specs/code-style.mdin the blockx repository: the source this page expands on, including theapi/boundary and the concrete constraints of Sans-IO and Scoped Resource Context.docs/specs/io-subsystem.mdin the blockx repository: the complete definition of Scoped Resource Context.- Repository layout and code layering
- Contributing