2026-09-22 · 22 min · explainer · agents · llm · evaluation · calibration · open-source
Every use of a decision model I have written about on this site puts it inside the loop. Stanley asks Jev which workflow a request belongs to, then runs the workflow. The xArm7 harness asks it for four signs, then moves the arm. jev-linkmap asks it whether a link belongs on a phrase, then places the link. In all three the model is in the critical path, deciding the next action while the agent is still running.
Asymptote-Labs/agent-beacon puts it after the loop. Beacon captures agent sessions across Claude Code, Cursor, Codex, OpenCode and two dozen more harnesses, normalizes them into one event stream, and then — only when you type the command — sends a finished trace to a decision model and asks whether that run contained anything worth teaching to the next agent. High scores become review candidates. Approved candidates become Agent Skills on disk.
That is a genuinely different problem, and on this site's own framework it should be an easier one. Three tiers argued the decision tier's two real advantages are latency and cost, and its two real costs are that it cannot see across its options and cannot explain itself. Judging a finished trace is offline, so latency does not matter at all. There is no next step to get wrong. The option space is a fixed rubric rather than a menu that changes per step. Half the reasons to be careful with a decision model do not apply here.
The other half is worth stating up front too, because it is where the interesting
question is. Beacon prices a trace at DefaultCostPerTrace = 0.00035 and spends exactly
one call on it, so a thousand traces is 35 cents. At that scale cost is not the binding
constraint either, and neither is latency. Which leaves precisely one reason to reach for
this tier instead of an LLM: the bounded answer with a calibrated number on it. Hold onto
that. It does not survive the next function.
So I cloned it at commit 63d43f4 and read the loop.
The capture layer is excellent and I will get to it. The learning layer is 1,509 lines of Go written in a single day, and three of its choices do not survive reading.
The rubric, in full
jev-linkmap established that with these models the rubric is
the product — the yes/no question and the numbers you compare its answer to are where
all the behaviour lives. So the first thing to find is the literal question text. It is
not in the README, and the docs paraphrase it. It is in
cli/beacon/internal/learning/evaluator.go, as a package-level var.
instructions: “Did the trace complete the user's engineering task successfully?”
criteria.true: “The trace satisfies this criterion.”
criteria.false: “The trace does not satisfy this criterion.”
the trace records every command's exit_code; the projection does not carry it
instructions: “Does the trace contain a correction or debugging pattern that future agents should reuse?”
criteria.true: “The trace satisfies this criterion.”
criteria.false: “The trace does not satisfy this criterion.”
the only question about reuse, and the gate gives it no floor of its own
instructions: “Is the reusable lesson supported by concrete events in the trace?”
criteria.true: “The trace satisfies this criterion.”
criteria.false: “The trace does not satisfy this criterion.”
“the reusable lesson” is question two's answer, which this question cannot see
candidate = score >= 0.60
That is the whole rubric. Six lines of criteria text, two of which are the same sentence with a not in it, repeated three times — so every bit of instruction the model receives is in the three prompts. Compare rubrics/v3.json in jev-linkmap, which carries per-question instructions, worked examples and a per-answer confidence floor, and which a $15.51 loop spent two rewrites tuning. Beacon's rubric is a Go var, hashed into every stored evaluation, and has never been revised.
Three questions, one request, noul type — Jev's yes/no with a calibrated probability.
No examples, no confidence floor, and no per-question criteria: the two criteria strings
are one sentence and its negation, sent identically with all three questions, which means
every bit of instruction the model receives is the one-line prompt.
Two things about the third question. It asks whether the reusable lesson is supported by evidence — definite article, referring to the lesson the second question is being asked to decide exists. That is a chain. And the one rule the Jev Engineering guide states about batching, quoted in the three-tiers piece, is that questions cannot read one another's answers. The rubric is written as a conjunction and answered as three independent judgements, which is exactly the relational limit the middle tier has. It is not fatal here — "is this trace well evidenced" is a sensible question on its own — but the rubric is not asking what its wording implies.
The third thing is the aggregation, and it is the one that matters.
An average has no veto
evaluationScore returns the unweighted mean of whatever came back.
CandidateFromEvaluation compares that mean to CandidateScoreThreshold = 0.60 and
either mints a review candidate or does nothing. That is the entire application policy:
one constant, one comparison, used in exactly one place in the repository.
There is no third band. A trace either becomes a candidate awaiting review or it becomes
nothing — the promote/review/discard triple is really score, queue, and silence. The
one thing the discard path does right is that the evaluation itself is persisted either
way, so beacon memory evaluations list shows you the sub-threshold scores and the
discard is at least auditable. From there a person moves each candidate to approved,
rejected or superseded; those four states are the whole lifecycle. (The docs tell you
to filter the queue with --state pending, which matches no state the code defines. The
one you want is --state candidate.)
Mean ≥ 0.60 over three answers is the same test as sum ≥ 1.80, which makes the trades between the three visible.
Row four is the one to look at. The model has answered reusable_correction: 0.00 — there is nothing here worth keeping — and the trace becomes a review candidate anyway, because a successful, well-evidenced run carries 1.81 on its own. The three questions read as a conjunction and are scored as an average, and an average has no veto in it. A one-line fix exists: require reusable_correction to clear its own floor before the mean is consulted. Probabilities here are illustrative — no scored trace is published anywhere in the repository — but the threshold and the arithmetic are the shipped ones.
reusable_correction is the only question in the rubric about reuse. It is the whole
point of the product. And it has no floor of its own, so a successful, well-evidenced run
carries 1.81 on its own and clears the gate with the reuse answer at zero — the model
putting the probability that anything here is worth reusing at nothing at all, and the
trace becoming a review candidate anyway.
I want to be fair about what this costs in practice. Everything past the gate is a human
review queue, so a bad candidate wastes a person's attention, not a skill file. But the
gate exists to make that queue worth reading, and a gate that promotes a certain "no" on
its central question is not filtering on the thing it is named after. The fix is one
line: require reusable_correction to clear its own floor before the mean is consulted.
Where the constant came from
Nothing says. 0.60 appears once, with no comment, in the commit that added the review
lifecycle. It is a Go const, so it is not settable by flag, by environment variable or
by config file — beacon memory evaluations run has ten flags of its own, including one
for the estimated cost per trace, and none of them is this one.
That matters more than it looks, because of where the comparison sits. CandidateFromEvaluation
is called inside the run loop, immediately after each model call. The evaluation is
persisted with its score, but candidate creation is not a separate pass over stored
evaluations. So moving the threshold is: edit the source, rebuild the binary, and re-run
the scoring, paying for every trace again. In jev-linkmap a threshold sweep was free —
the verdicts were on disk — and it was worth 19 of the 20 recall points a $15.51 rubric
rewrite was credited with. Here the cheapest knob in the system is the one you cannot
turn.
The number the model returns, and the number nobody reads
Jev's answer carries a probability and a confidence. Beacon parses the confidence,
stores it, prints it in beacon memory evaluations show, and reads it in no policy
anywhere in the repository. I grepped: two occurrences, one write and one Printf.
Compare Stanley's router, which this site read line by line: three tests on the shape of
the returned distribution — minConfidence: 0.6, minProbability: 0.55, and a
minMargin: 0.15 between the top two options — because the distribution is the only
thing a decision model gives you. Beacon takes three calibrated numbers, averages them,
throws the confidence away and compares the result to a constant. Three tiers argued you
reach for this tier because you want a number attached. One function later, the number
is a boolean.
What the model is actually shown
Now the part I did not expect.
Beacon's event model is good. Every adapter normalizes into one TraceEventV1 carrying
a typed action, an actor, a command with its exit code, a file with its operation and
diff, an approval with its decision, token usage, and a fidelity marker saying
whether the action was observed from a vendor hook or inferred by a pattern match over
prose. That last field is unusually careful engineering; most telemetry products do not
distinguish a report from a reading.
BuildProjection keeps six fields.
what every adapter produces
- number
- type
- action
- title
- summary
- content.text
- id
- timestamp
- category
- fidelityobserved, or inferred by a pattern match
- actoruser or agent — the correction signal
- trace.span_id
- tool_call_id
- tool.{name,args,result}
- command.exit_codedid the test pass
- command.output
- command.duration_ms
- file.{path,operation,diff}
- mcp.{server,tool,method}
- approval.decisionthe only human decision in the trace
- model
- usage.{tokens,cost_usd}
what the model is shown
- number
- type
- action
- title
- summary
- content.text
Strings only, each truncated to 1,200 characters and passed through the secret redactor. For a command.executed event, content.text is the command line — Beacon never fills it with the command's output, so the projection falls back to the command itself.
16 fields dropped, 4 of them the ones the rubric is asking about.
The first rubric question is did the trace complete the task successfully. Beacon recorded the exit code of every command in the run and does not send it. The second is does it contain a correction. Beacon recorded who spoke and does not send that either. Both questions have exact answers sitting in the same struct, one field away from the projection that replaces them with a guess.
Read that against the rubric. The first question is did the trace complete the task
successfully. command.exit_code is in the struct. It is not in the projection. The
second question is does the trace contain a correction — the announcement's phrase for
this is "human correction signals" — and actor is in the struct, and not in the
projection, and neither is approval.decision, the only decision in the whole trace that
a person definitely made.
So the model is asked to infer, from prose summaries, two facts the record already holds
exactly. A regex over event.action == "command.executed" && exit_code == 0 is not an
approximation of the first question. It is the first question.
The figure below is the project's own dashboard, and it is why I trust the capture layer:
1,025 events off one machine, with approval.allowed and command.executed counted as
first-class actions.

The truncation bug proves the point
Until the day I read it, BuildProjection did this:
// cli/beacon/internal/learning/evaluator.go, before 2791dcd
events := show.Events
if len(events) > maxProjectionEvents { // 80
events = events[:maxProjectionEvents]
}The first 80 events. In a long session the fix, the passing test and the correction are
all at the end, so task_success was being scored on a trace that stopped before the
outcome. Issue #615 reported it on 2026-09-22; commit 2791dcd fixed it the same day by
keeping 40 from the head and 40 from the tail with an explicit omitted marker in
between, and added a regression test. The fix is right and the turnaround is good.
What is worth noticing is how it was found: by someone reading the function. Not by a score moving. A judge scoring the wrong half of every long trace produces probabilities that look exactly like probabilities, and there is nothing in this repository that would have noticed.
What gets promoted
Say a trace clears the gate. What lands on disk?
beacon memory candidates approve mints a memory record; beacon memory skills install
renders it to .agents/skills/<slug>/SKILL.md. Here is that file, for the trace visible
in the project's own log-search screenshot — a Factory Droid session whose first user
message was tell me about htis repo, typo included.
| --- | |
| first prompt | name: beacon-correction-tell-me-about-htis-repo |
| first prompt | description: "correction: tell me about htis repo" |
| hash | beacon_memory_id: "memory_9f3c…" |
| hash | beacon_candidate_id: "candidate_41ab…" |
| tags: | |
| template | - "beacon" |
| keyword switch | - "correction" |
| template | - "factory" |
| --- | |
| first prompt | # correction: tell me about htis repo |
| template | Use this skill When a future agent in this project hits a similar workflow, regardless of harness. |
| ## Guidance | |
| template | Reusable lesson extracted from a reviewed Beacon trace. |
| hash | Trace: 0475ffdc-003d-4617-afdb-6b77e777c1ef |
| template | Harness: factory |
| first prompt | Observed workflow: tell me about htis repo |
| template | Evaluation signals: |
| model | - task_success: 0.91 |
| model | - reusable_correction: 0.44 |
| model | - evidence_supported: 0.68 |
| ## Beacon Evidence | |
| first prompt | - Trace `0475ffdc…`: tell me about htis repo |
The three red lines are the model's entire contribution to the artifact, and they are the numbers that got the trace here, reprinted. Everything else is a string template, a substring of the first thing the user typed, or a hash. There is no lesson in the file because no step in the pipeline writes one: the rubric asks whether a reusable lesson exists, and nothing then asks what it is. description is what a future agent reads when deciding whether to load the skill, and here it is a truncated prompt with a typo in it. Hashes are abbreviated and the three probabilities are illustrative; every other character is what RenderSkill emits for this trace.
There is no lesson in it. Not a badly-written lesson — none. candidateBody is a
five-line template with the trace id, the harness name, the session title and the three
probabilities printed to two decimals. The rubric asks whether a reusable lesson exists
and nothing afterwards ever asks what it is. The promoted artifact is a pointer to a
trace, plus the score that made it a pointer.
Two derivations under that are worth stating on their own:
- The memory kind is a keyword switch over the first thing the user typed.
candidateKindruns fourstrings.Containscases over the trace title —convention|standard,gotcha|pitfall,workflow|process,fix|debug|fail— and falls through tocorrection. The trace title is the firstuser_messagetruncated to 80 characters. So a session that opens "fix the flaky retry test" is filed as a debugging pattern and one that opens "the retry test is flaky" is filed as a correction, and the model had no say in either. - The skill's
descriptionis that same 80-character prompt fragment, anddescriptionis the field a future agent reads when deciding whether to load the skill. Retrieval through MCP is a case-insensitive substring match over title, body and kind, capped at five results, ordered by recency. There is no embedding anywhere.

Two things this gets right that the last one got wrong
I have been hard on the learning layer, so let me be precise about what it does not do, because both are failure modes this site has documented in other projects and Beacon avoids them.
The reviewer is a person. When I wrote up jev-linkmap I first described its editor
pass as a human gate. It was claude-opus-5. So I checked this one the same way. Every
write to the memory store in the entire repository happens in cli/beacon/cmd/memory.go,
and every one of them is reached from a cobra command a person types. The MCP
server asserts its own tool list at startup and all three memory tools are reads. The
dashboard's /api/memory returns 405 to anything but GET. No workflow file, no hook
and no CI job calls the evaluator or the approver. The docs state the boundary and the
code keeps it.
The honest qualifier: this is a gate made of absence, not of enforcement.
beacon memory candidates approve is a shell command, and a coding agent with a shell
can type it. Of the ten commits that built this package, five are co-authored by Cursor
and one by Claude Opus 5 — so in this repository, "a human runs the command" is a
convention rather than an invariant. Stanley's equivalent — an agent writes a workflow, a
human runs --promote-candidate — has the same shape and the same hole.
The loop does not grade its own exam. This is the failure mode jev-linkmap actually
exhibited: its System 2 loop rewrote the rubric, the referee's system prompt was built
from that same rubric, and agreement improved by construction. Beacon cannot do that.
RubricQuestions is a compile-time var; nothing writes to it; there is no rubric file,
no rubric endpoint, no rubric rewriter. Better, every stored evaluation carries
RubricHash() — a SHA-256 over the question set — and the evaluations table has a unique
index on (project_id, trace_id, rubric_hash). Change one word of one prompt and every
prior evaluation is automatically a different row rather than silently comparable. That
is the discipline jev-linkmap needed and did not have, shipped here on day one.
What promoted skills feed is the agents' context, not the rubric. The loop is: skill
lands on disk, a future agent loads it, that session becomes a trace, the same fixed
rubric scores it. task_success should go up if the skill helps. That is the intended
mechanism and it is not circular.
It is also unmeasured. Nothing compares scores before and after a skill is installed,
and nothing stops the same lesson being re-promoted from every session that used it. A
smaller version of the same gap: CandidateID hashes the candidate's body, and the body
contains the three probabilities at two decimals — so re-scoring the same trace and
getting 0.82 where you got 0.81 mints a second candidate for the same lesson,
next to the first one you already rejected.
The capture layer is the product
"20+ harnesses" is a strong claim and I expected to find it thin. It is not. Every row of the README's runtime table has real code behind it, and the mechanisms are wildly different from each other, which is the interesting part.
Beacon's headline is "20+ other harnesses", and the count holds: every row of the README's runtime table has real code behind it. What the count hides is how unlike each other the adapters are — a settings-file hook, a SQLite reader and a monkeypatched window.fetch are three different pieces of engineering, and Beacon normalises all of them into one typed event. Then the learning layer, which is the part the announcement is about, flattens that typed event down to six string fields and sends at most 80 of them to a decision model. Line counts are non-test Go (non-test TypeScript for the extension) at commit 63d43f4.
| capture mechanism | harnesses | what it actually reads | lines |
|---|---|---|---|
| Hook installers | 22 | the harness's own settings file, to register beacon-hooks with it | 7,439 |
| Hook event translators | 33 cmds | the typed payload each vendor's hook hands it on stdin | 13,382 |
| Session-log pollers | 14 + 1 cloud | files the harness wrote for itself: ~/.claude/projects/**/*.jsonl, Cursor's state.vscdb SQLite and agent-transcripts/ | 21,201 |
| OTLP configuration | 7 | the harness's own OpenTelemetry exporter, pointed at a local collector | 2,127 |
| Browser extension | 2 sites | window.fetch, monkeypatched in the page, teeing SSE chunks | 1,573 |
| rules/ threat detection | 75 rules | the full typed event — event.action, file.path, command, approval — with 509 labelled test cases | 5,598 |
| internal/learning | all of them | 6 string fields per event, at most 80 events, no exit code, no actor, no approval decision — with 0 labelled cases | 1,509 |
The two rows worth reading against each other are the last two. The rules engine and the learning rubric ask questions of the same event stream. One is a declarative expression over typed fields, shipped with 509 labelled cases that run in CI on three operating systems. The other is a hosted model call over prose, shipped with none.
A hook installer writes Beacon into ~/.claude/settings.json. The Cursor poller opens
state.vscdb, the SQLite database Cursor keeps in its VS Code global storage, and walks
~/.cursor/projects/*/agent-transcripts. The browser extension monkeypatches
window.fetch in the page's own JS context and tees SSE chunks to a local collector.
Those are three different pieces of engineering wearing the same word. Normalizing them
into one typed event is hard, and Beacon does it well enough to mark each event
observed or inferred so a downstream rule can insist on the former.
Then the learning layer flattens all of it to six strings.
The baseline is in the next directory
Here is the question I came to answer: is the decision model doing work a cheap heuristic could not?
The repository contains the cheap heuristic. Not as a suggestion — as a shipped,
tested subsystem. rules/ holds 75 detection rules, each a declarative expression over
the same normalized event:
# rules/credential-access/password-manager-db-read.rule.yaml — abridged
match: >
e.event.action == "file.read" && (
e.file.path.matches("(?i)(^|/)[^/]+\.kdbx$") ||
e.file.path.matches("(?i)(^|/)\.password-store/") || ...
)
tests:
- name: read_kdbx
verdict: match
events:
- event: { action: file.read }
file: { path: "/home/u/Documents/vault.kdbx" }
- name: read_unrelated_sqlite # eight cases in this file,
verdict: no_match # three of them negatives
events:
- event: { action: file.read }
file: { path: "/repo/app/data/cache.sqlite" }Every rule carries its own labelled cases, including negatives, in the same file. Across the 75 rules there are 509 of them, and the conformance suite runs in CI on macOS, Linux and Windows.
Against that, the learning rubric has zero. The learning package has 19 tests and
they are all plumbing — dry run does not need an endpoint, the projection redacts
secrets, the store scopes by project, the skill renders its provenance. The one test that
touches the score assigns it by hand:
// cli/beacon/internal/learning/candidate_test.go
eval.Score = 0.3
if _, ok := CandidateFromEvaluation(eval); ok { t.Fatal("...") }
eval.Score = 0.9No fixture trace. No committed evaluation. No runs/ directory, no out/. jev-linkmap,
whatever else I took issue with, committed its run files, which is the only reason I was
able to recompute its numbers and disagree with three of them. Beacon publishes the loop
and none of its output, so nothing here is recomputable, by me or by its authors.
Which is the finding, and it is not a gotcha. The only defensible reason to pay a model for this is the part a regex cannot do: telling a reusable lesson from a task-specific fix. That distinction is real, it is genuinely hard, and it is plausibly what Jev is good at. Nothing in the repository measures whether it does it. And the two questions either side of it — did the task succeed, was there a correction — have exact answers in fields the projection deletes on the way out. So the model is being asked three questions: two it should never have been asked, and one nobody has checked it can answer.
What I would change
All four of these are cheap, and none of them needs a rubric rewrite.
- Put the structured fields back in the projection.
command.exit_code,actor,approval.decisionandfidelityare four more fields per event. Better: computetask_successdeterministically from the last command's exit code and stop asking. Two of three questions go away and the third gets the whole budget. - Give
reusable_correctionits own floor. An unweighted mean over a conjunction lets any one answer be zero. Oneif. - Make the threshold a flag, and re-derive candidates from stored evaluations.
The scores are already in SQLite. A
beacon memory candidates rebuild --min-scorethat re-runs the gate over persisted rows makes the cheapest experiment in the system free, which is exactly the lesson jev-linkmap paid $15.51 to learn. - Extract the lesson. The rubric asks whether one exists; the pipeline never asks what it is. This is the one place a reasoning model belongs — after the gate, on the small set of traces that got through, where the cost argument for the decision tier has already done its work.
And one for the reader rather than the maintainers: the capture layer is worth installing on its own. It is 44,000 lines of careful normalization across 29 runtimes, it is MIT, it runs local-first, and it does not need the memory feature to be useful. The memory feature landed on 2026-09-21. Read it before you believe its loop.
What would change my mind
6 claims above, and what would falsify each
A run the model scores 0.00 on reusable_correction can still become a review candidate.
This is arithmetic over
evaluationScoreandCandidateScoreThreshold = 0.60at commit63d43f4: the mean of(1.00, 0.00, 0.81)is 0.603. It is falsified by a gate I misread — if the hosted TypeSafe endpoint returns something other than the threenoulanswers Beacon asks for, or returns a top-levelscorefield,Evaluateoverwrites the computed mean with*resp.Scoreand the aggregation I am describing never runs. I have no API key and no captured response body to check that against. Anyone with a key can settle it in one call: score a trace, runbeacon memory evaluations show --json, and see whetherscoreequals the mean of the three probabilities.The decision model is asked two questions the event schema already answers exactly.
TraceEventV1carriescommand.exit_codeandactor;learning.ProjectedEventcarries neither. The claim fails if those fields are mostly empty in practice — a harness that reports commands without exit codes, or an adapter that never sets an actor, makes the deterministic answer unavailable and the model's guess the only option going. The test is a census, not an argument: runbeacon traces --jsonover a few hundred real sessions per harness and count the fraction ofcommand.executedevents carrying a non-nullexit_codeand the fraction of events carrying an actor. If either is low across the harnesses people actually use, the projection is dropping fields that were not there anyway.Nothing in the repository measures whether the rubric distinguishes a reusable lesson from a task-specific fix.
Read against the whole tree at
63d43f4: 19 tests ininternal/learning, all plumbing, the only score-touching one assigningeval.Scoreby hand; no fixture trace, no committed evaluation, no run artifacts. Falsified by an artifact I did not find — a held-out set, an agreement study, anything with a denominator. It is also the falsifier I most want someone to close, because the interesting claim is the one nobody has tested: label 200 traces by hand for "contains a lesson a future agent should have", score them, and publish precision and recall against a regex baseline that fires on a user prompt following a tool call. If the model beats the regex by a wide margin, the whole design is vindicated and my objection collapses to the projection.The promoted skill contains no lesson — only a template, a prompt fragment and three probabilities.
Traced through
candidateBody,RenderSkillandInstallSkill. The escape hatch is editing: a memory record is stored as JSON and a person reviewing a candidate could rewritebodybefore approving. Nothing in the CLI offers that today — there is no--body, no--edit, andApproveCandidatecopies the candidate's body verbatim — but a maintainer could add it in an afternoon, and if they do, the artifact stops being a pointer and this criticism becomes a criticism of a version that was current for a week.Beacon's loop is not circular, unlike jev-linkmap's.
RubricQuestionsis a compile-timevarwith no writer, andRubricHash()is stored on every evaluation. The second-order path is still open, though, and I have not measured it: a promoted skill changes how future agents behave, those sessions become traces, and the same rubric scores them. If skills systematically raisetask_successwithout raising the quality of what is learned, the loop starts promoting the traces of agents that already read its own output — circular one level up. Install a skill, then compare score distributions on sessions before and after across the same task mix. A rise intask_successwith no rise inreusable_correctionis what that failure looks like.Every harness in the '20+' claim has real code behind it, and the mechanisms are heterogeneous.
Counted by walking the tree: 22 hook installers, 33 hook translators, 14 session pollers plus one cloud poller, 7 OTLP configurations, two browser adapters. What a file census cannot tell you is whether any of it works against the current version of each vendor's format — a session-log parser is a standing bet on someone else's private file layout, and it rots silently. The repository's
beacon-sandboxruns 16 end-to-end scenarios, which is a long way short of 29 runtimes. The falsifier is operational: install it, use four different harnesses for a day, and checkbeacon tracesagainst what you remember doing.
Read at commit 63d43f4 (2026-09-22) of Asymptote-Labs/agent-beacon, MIT. The two dashboard screenshots are the project's own, reproduced for commentary with the licence and a notice committed beside them in public/articles/agent-beacon/; every diagram is mine. The probabilities in the gate and skill diagrams are worked illustrations — the repository publishes no scored trace — while the thresholds, the arithmetic and the generated file's structure are read from the source.