π§ Code conventions¶
The rules a contributor (human or agent) has to know that are not guessable from the code. The two-kilobyte version is AGENTS.md; this is the long form. Build, test and ownership live in CONTRIBUTING.md.
Normalize β Validate¶
Every serializable struct β config.Config, pipeline.Config, every block schema β has:
Normalize()β fill defaults, clean and canonicalize. Idempotent.Validate()β enforce rules, return an error.
Call both before persisting. pipeline.Config.Validate() rejects empty or duplicate group ids, group steps referencing unknown phases, and phases assigned to multiple groups.
A corollary that has bitten before: pipeline.Config.Normalize() merges missing default phase keys back in, so a hard delete of a phase resurrects it on the next load. GUI pipeline editors "delete" a phase by persisting when: never, enabled: false and removing it from groups and order β which is restorable, and survives normalization.
Config¶
config.Config is the single source of truth.
- Both
yaml:andjson:tags on every field. Studio's Settings page renders fromconfig schema, so an untagged field is an invisible field. ApplyEnv()handlesSLMCODE_*;ApplyPatch(Patch)handles partial updates from the API and the CLI.- Layering, lowest first: defaults β user file β project file β env β flags.
provrecords which layer supplied each key, forconfig show --origin. Rootis never persisted (yaml:"-"): an absolute path in a config file is not portable between machines or checkouts, andLoadwould honour the stale value.config_versionmarks the schema generation;migrate.gomoves older files forward.
Prompts and schemas¶
- Prompts are SLM-optimized: short, role-locked, output contract stated first.
- Every tool-using specialist inherits
agents.AntiWanderCore. It is deliberately three lines so it can prepend to any prompt without crowding the task:
ANTI-WANDER β HARD SCOPE, three rules:
SCOPE: touch only the task's focus files and same-package siblings; β¦
NOTHING EXTRA: no new helpers, files, refactors, or "nice to have" additions.
GROUNDED: reference only paths you have read; use ws_glob/ws_grep when unsure.
pkg/agents tests assert the literal strings ANTI-WANDER and HARD SCOPE, and pkg/server and pkg/orchestrator build ## Focus files (HARD SCOPE) sections that the same tests check. Do not reword these markers. - Every structured role needs a contract in pkg/schema. TestPromptContractsMatchSchema fails when a prompt promises a field the schema does not have. - Coding agents get workspace.ToolNames() + workspace.SpecialistToolNames(). - Never end a turn on a tool call β an agent must produce final JSON after tool use. - One tool call per turn. RoleSpec.SerialTools truncates an assistant message to its first tool call, so a model that ignores the instruction loses the extra calls rather than confusing the loop. - NormalizeDecoding derives a role's schema role, JSONOnly flag and stop sequences from its id, so a new role usually only declares its tools. See Constrained decoding.
Determinism¶
Two things must be byte-deterministic, and both have silently regressed before:
- Prompt assembly. Stable prefix first, volatile content last.
TaskPackrenders from explicitDocOrder/FileOrderslices, never by ranging a map β Go randomizes map iteration, so the old renderer produced a different byte sequence for identical inputs on every call, and local KV-cache prefix reuse never hit. - CI.
EngineOptions{Deterministic: true}(configdeterministic) makes the bandit greedy and disables exploration.dry_runimplies it.
Budgets¶
Everything that reaches a prompt is budgeted in tokens, and every collection is bounded.
pkg/contextderives the pack budget from the model's real window minus reserves. Never reintroduce a byte budget.- Every tool result goes through
pkg/workspace's cap; never return unbounded output. - Every memory store has a cap and a prune policy; every rendering has a token budget.
- Search tools announce their own truncation (
MaxGrepHits,MaxGlobHits).
Failure handling¶
- Fail closed at gates. Truncated reviewer JSON is a rejection. The QA gate cannot report green when tests failed. A HITL gate with a human attached blocks rather than expiring.
- Disk is authoritative. A claimed edit that is not on disk is not evidence. Repo dirt unrelated to the task is not evidence either.
- A subsystem failure must never wedge a run. Memory, evolve, repo-map and retrieval are all best-effort: a corrupt file is moved aside to
<name>.corrupt, the store starts clean, and the problem is surfaced throughWarnings()rather than an abort. - Tool failures are information, not errors. A shell timeout, a failed match and a syntax break are returned to the model as a result it can act on, with the recovery spelled out.
Runtime roles and phase gating¶
- Agent blocks are runtime roles.
agents.Factory.ExtraCustomsregisters every registry agent block (bundledgo-tester/go-worker/python-tester⦠plus project and user blocks) as a real role. On-disk.slmcode/agents/{id}.yamlwins on id clash.GET /api/agentsmerges both. execute.default_roleis consumed: tasks with an empty orimplementerrole use the pipeline'sexecute.default_role(e.g.go-worker).- Role fallbacks: a phase agent missing from the registry falls back to the default agent with a warning; unknown task roles map to generics (
go-testerβtester,python-workerβworker). The same folding gives block-defined agents their schema contract automatically. - Phase gating:
when: never/enabled: falseis honoured for the agent-driven phases (context,explore,docs,architect,clarify,plan,split,coord,execute,learn,polish,test,memory).init,skillsanddoneare engine-structural and always run. - Language pinning: the detected project language is injected into tester/worker/review/QA prompts ("Project language: Go β NEVER run pytestβ¦").
PromptTesterandPromptTaskSplitterstay language-neutral so the hint is the only source of truth.
Naming and identifiers¶
- Block ids:
^[a-z][a-z0-9_-]{1,63}$, lowercase kebab-case. - Schema role ids are output contract names, not agent ids. Several agents may share one; an agent whose id does not match a contract names it with
SchemaRole. - Block discovery order, first id wins per kind: project
.slmcode/blocks/β user~/.slmcode/blocks/or$XDG_CONFIG_HOME/slmcode/blocks/β$SLMCODE_BLOCKSand walk-upblocks/dirs β builtin (pkg/blocks/bundled/,go:embeded).
Adding things¶
| To add | Do |
|---|---|
| An agent | prompt in pkg/agents/prompts.go β RoleSpec in specs() β pkg/schema contract if it emits JSON β optional YAML block |
| A pipeline phase | pkg/pipeline/default.go Default() β orchestrator wiring β group assignment |
| A block kind | pkg/blocks/meta.go β struct in pkg/blocks/schema.go β ingest() switch in pkg/blocks/registry.go |
| A stack | stacks/<name>.yaml |
| A skill | skills/default/<name>/SKILL.md or .slmcode/skills/<name>/SKILL.md |
| A CLI command | Cobra command in cmd/slmcode/ β register in root.go under a group β honour cmd/slmcode/doc.go's non-interactive contract |
| A config field | struct field with both tags β Default() β Normalize() β ApplyPatch() β config reference |
The build¶
cmd/slmcode/ui/is ago:embed all:uidirectory whose only tracked file is.gitkeepβ it keeps the directory (and therefore the embed pattern) alive on a fresh clone.index.html,assets/andvendor/there are gitignored build output; with none of them present the server serves a placeholder page frompkg/server/placeholder.go.make bootstrapbuilds the real SPA;make ui-reactrebuilds it..slmcode/is gitignored runtime state.- Lint findings are ratcheted against a baseline in
.golangci.yml, never excluded. See CONTRIBUTING.md.