The context file¶
Every stack agent-smith works in has a .agentsmith/contexts/<name>/context.yaml.
It is pasted verbatim into every master prompt, so the model is its reader — and a
field that is wrong is not merely dead weight, it is acted on.
One test decides whether a field belongs¶
| Class | The test | Examples |
|---|---|---|
| Judgement | Somebody DECIDED it, and no file in the repository states it. | meta.purpose, quality.limits, behavior.*, integrations, data, state, decisions |
| Mechanism | The orchestrator ACTS on it — a run behaves differently because of its value. | meta.workdir, stack.lang, stack.image, stack.resources, prerequisites, verify, probe, methodology, registry_auth |
| Reading | A copy of something the repository still states for itself. | stack.frameworks, stack.sdks, stack.testing, stack.ci, meta.version, arch.style, quality.principles |
A reading is worthless on the day it is written and wrong soon after. sdks names a
package at a version the manifest is one install away from disagreeing with, and
nothing can tell which of the two is true. Deleting it removes no knowledge — it
removes a second copy that can only diverge.
A guessed LABEL is the same defect wearing a taxonomy. "We build in layers" would be
a judgement if anybody had decided it; what actually happens is that an agent skims
the tree once and picks a word from a list. That is why arch goes: not because
nothing reads it, but because a reader can act on it, and acting on a guess is worse
than acting on a silence.
meta.purpose is the counter-example that survives everything. What a module is FOR
appears in no file, and the ticket-to-repo classifier reasons from it before a
checkout exists — the most human field in the file and one of the few the
orchestrator reads.
state is the other one. Every entry there records a decision and what came of it,
so the file teaches a reader how the program got its shape. It is long because
entries have been wordy, which the 400-character index-line cap attacks directly —
not a reason to remove the one section written by whoever made the call.
verify is what proves a change¶
The verify block names the commands that turn an edit into a verdict, in order.
A run executes exactly those, ahead of anything the analyzer infers and ahead of
the .NET entry-point discovery — a repository that states its gates is the
authority on them.
Take the commands from the pipeline the repository already has. In an established estate the truth about "green" is already written down, and a second copy invented here can only disagree with it.
verify:
- label: build
command: dotnet build MySolution.sln
- label: test
command: dotnet test MySolution.sln
- label: lint
command: npm run lint
when_present: package.json
A stage carries a label, one shell command, and optionally the path it needs
present. Where that path is absent the stage is skipped and says so, so one
declaration serves repositories of different shapes. A command that cannot fail
— echo, true — is refused at resolution rather than run: a declared gate
that cannot go red is not a gate.
A repository that declares nothing keeps working exactly as before.
verify_derived_from says where the commands came from¶
In an established estate the truth about "green" is already written down. A real reference estate keeps twenty-three CI files; the pipeline pins the Python version, installs a JRE so PySpark runs, and executes the unit tests against a live cluster. Nobody could have guessed any of that — and nobody had to, because the repository states it.
So the block above is DERIVED, and verify_derived_from records what from:
The files are paths relative to this context's workdir, named by whoever derived the stages. The hash is the framework's, stamped by the write path — a model never supplies one, because a hash it invented would report drift on the very next run.
Every later run re-reads those few files and compares. That is a filesystem read in the sandbox: no model call, no re-derivation. When they no longer match, the run says once that the declaration may be stale, next to the verdict — and still runs exactly what is declared. Whether to re-derive is the operator's call.
What the hash cannot catch. It sees the pipeline FILE move, not the TARGET move. A cluster id, a schema name, a service connection can all change under a byte-identical file. Nothing in a hash sees that; the stage does, when it runs and goes red.
A repository with no pipeline gets no stages and no verify_derived_from. An invented
gate can only disagree with the one the estate actually runs.
probe is whether the target answers¶
verify proves the change; probe proves the environment the change depends on is
reachable and willing. It is one command, run through a shell at this context's
workdir after the prerequisites and before the coding agent starts.
A CLI that resolves authentication before it does anything else reds on a clean tree without credentials. That is a fact about the measurement environment, not about the command: with the credential it is the cheapest true statement about the estate a run can buy, and buying it first turns a wrong or absent credential into infrastructure instead of into a coding agent that cannot build.
Reference an injected credential by NAME — $SF_USERNAME — never by value; the shell
expands it inside the sandbox.
A refusal fails the run naming the target, the command and the exit code, and carries no captured output. Output travels into the failure reason, the per-repository result document and a comment on the ticket, and the masker only knows values the framework holds — it never holds an injected credential, so a value it does not know is one it cannot replace. The output tail goes to the run log alone.
The record tells three states apart. The target answered; the target refused; no probe was declared. Only the first is silent. A sandbox backend that injects no credentials — docker, in-process — does not ask at all and says so, because a refusal there would be a fact about the backend rather than about the target.
state.done is written by the run¶
When a phase finishes in a target repository, agent-smith writes the executed spec to
.agentsmith/phases/done/{id}-{slug}.yaml and the state.done line that names it,
in the context whose sandbox carried the change. Every repository that gets the record
file gets the line.
The line is an index entry, not an essay: the goal, cut at a word boundary, then
-> .agentsmith/phases/done/…. It is composed to FIT the cap rather than refused for
exceeding it — the step runs after the work is committed, so a refusal would fail a run
nobody could go back and shorten. The reasoning belongs in the spec the pointer names.
The entry is keyed by phase id and upserted: a re-run replaces its own line rather than adding a second one under the same key, which would make the file unparseable. New entries go first, so the section reads newest to oldest.
The edit is a splice. Everything else in the file — the schema header, your comments, your flow style — is left exactly as it was.
Where the classification lives¶
In the schema. Every field in .agentsmith/context.schema.json carries a $comment
beginning with JUDGEMENT, MECHANISM or READING and one clause saying why, and a
test asserts that no field is missing one. A field added without a classification
fails the build, so the next one is argued rather than assumed.
Deprecated, not deleted¶
The schema root is additionalProperties: false. Removing a reading from
properties would not stop the file asking for it — it would start REFUSING it, and
every context written before would fail on its first run. So the readings stay
declared and stay accepted:
- an existing
context.yamlkeeps its readings on disk and still validates; - a model that still offers one is accepted, not refused — the tool discards it;
- the tool description, the template and the shipped samples no longer ask.
Nothing is migrated and no installation is invalidated. A context loses its readings the next time it is written, and not before.