The MES as an agentic harness¶
Design. Written 2026-09-27 against main at f9705990, before anything is
built. Scott asked, that morning, how the AI tab becomes "the dashboard of an
MES agentic harness" — every person with their own agent, functional agents for
deep work, a continuous-improvement team on a cadence, and "this whole crew
setup, basically running in Bottling. Not exactly." This page answers that with
the code cited and proposes a boundary for what an agent may put in force
without a signature.
Revised 2026-09-28 against main at ad5219d3, to what Scott decided that
morning. His ten answers are quoted in full in Decided,
which also says, section by section, which recommendation was accepted, which was
accepted with a rider, and which was replaced. Two answers changed the shape of
the design: the harness changes the product's own code, UI first, and the
harness is the engine of all product improvement rather than one more
feature. §9 is now a four-milestone build plan sized as handoffs.
Still nothing is built from this page. The decision it turns on is
0038,
accepted 2026-09-28 with those amendments.
Decided (2026-09-28)¶
Scott answered all ten questions of §10 on 2026-09-28 at about 06:20. Eight answers accept this page's recommendation. Two change its shape, and this revision is what they change. His words are below in full, unedited, because a paraphrase of a decision is not the decision.
1. Is the line right? Seems right to me but it should be configurable with the same agentic access to change.
2. "UI updates 100% automated" turned out to mean 15 specific numbers (the [screens] settings) — is that what you meant, or something bigger? I was thinking a person should literally be able to change the code to improve the UI. Move cards, change graphs, etc. This should be as maliable as possible.
3. Want a new permission, screens.define, off by default, so a plant turns this on deliberately rather than getting it automatically? Configurable with MCP tool.
4. Start with the CI crew that only proposes fixes (never applies them itself), plus the analysis agent's read-only tools? Or build the analysis agent alone first? Analysis first with very good graphing capabilities.
5. Should it run warn-only for a release first — the crew proposes, nothing auto-applies, until you've seen a month of what it would have done? Yes.
6. Should there be a new "AI" section in Configuration for agent budgets/schedules, rather than burying them in Setup? Yes.
7. Per-person memory: start with none — the agent just stops forgetting mid-conversation, but doesn't remember facts about you — until there's a real design for memory you can read and delete? I'd envision a fully customizable agent with full memory and potentially even a skill set later. I could envision each person eventually getting their own fully embodied agent that works on their behalf. But simple is best for now and I'd defer to a simpler proof of concept, with just retaining memory for now.
8. Right now nobody can read their own past AI conversations, and it's not private from supervisors either. Add a "my agent" view, and say that out loud in the UI? Yes.
9. If the analysis agent runs in shadow/offline mode: turn it off and say so, or let it answer worse with a local model? (recommends: off) Off.
10. The honest case against doing any of this is in §9 — this ranks 5th of 5 on your own priority list, nobody outside this machine asked for it. Is it worth an evening a week for a month, against the first-real-machine work you've called the real bottleneck? I feel this should be a tool for driving product improvement and thus should accelerate solutions for all other areas. That's the goal for this harness. To make this product self evolve on its own. In that regard, this should become the bottle neck for driving all other improvements on that list. For example, if I wanted to integrate with SAP or fix any ERPNext connection, then it should be this harness that allows be to fix or build it.
What each section now says¶
| § | The recommendation it made | Ruling |
|---|---|---|
| 2 — the map: crew → plant | the crew's five ideas, ported; the steward's safety model replaced by the approvals queue | replaced by answer 10. The map is not an analogy. The crew's roles run inside the product, against the product's own repository, and §2a below says what runs where |
| 3 — one agent per person | a durable transcript; the trace unchanged; no memory at first | accepted, except memory, replaced by answer 7: a simple proof of concept that retains memory. §3 defines what that is and what it is not |
3, 8 — a "my agent" view behind plant.read, and the panel saying out loud that a supervisor can read what you type |
accepted (answer 8) | |
| 4 — functional agents as six fields | an agent kind is an account, a tool set, a cadence, a budget, a data class and one place its work lands | accepted, and answer 10 adds a seventh thing a code-editing kind needs: a checkout. §2a |
| 5 — the signature boundary | five clauses; the automatable set is fifteen screen numbers | replaced by answer 2. Fifteen numbers is a floor, not the thing. §5 is now three tiers, and the middle one is the harness changing the product's own code. The five clauses survive as tier (a)'s test |
| 5 — the automation line itself | a judgement written into the product | accepted with the rider (answer 1): the line is a Configuration setting the agent may read and propose changes to through the settings tools, gated on *.approve |
5 — screens.define, off agent by default |
a new capability an administrator grants deliberately | accepted with the rider (answer 3): granted and revoked through the same settings tools, not only by hand |
| 5 — warn-only for a release first | the crew proposes; automation is switched on by a plant that has read a month of proposals | accepted (answer 5), and it now applies to tier (b) as well as tier (a) |
| 6 — bare minimum by default | a default screen answers a question somebody has while standing up | accepted, not asked about, unchanged |
7 — budgets, cadences, and a fifth ai Configuration domain inside the AI nav group |
accepted (answer 6) | |
| 8 — the AI tab as the dashboard | what the AI did, what it costs, what it is waiting on; at most one number per agent | accepted, and it is where M1's charts render |
| 9 — start with the CI crew (option A), taking the analysis agent's read tools along | replaced by answer 4: the analysis agent first, "with very good graphing capabilities". §9 is now a four-milestone build plan in the order the answers imply | |
| 9 — the analysis agent in shadow mode | off, and saying so | accepted (answer 9) |
| 9 — the case for building none of it yet | this ranks fifth of five on the project's own strategy pages | kept, and answered by answer 10 in §9. The case is not withdrawn; it is overruled with a reason, and the reason is testable |
The two amendments, stated once¶
Amendment A (answer 2) — the harness changes the product's code. "I was
thinking a person should literally be able to change the code to improve the UI.
Move cards, change graphs, etc. This should be as maliable as possible." The
automated tier this page drew — fifteen [screens] numbers a plant runs at — is
a floor. The thing is an agent that edits web/*.html, web/*.js and
web/*.css, runs the test tiers the repository already has (the browser tier
included), and lands the change through the same review the maintainer's own crew
uses: a branch, a pull request, CI, and a person's merge. Decision
0035's
rule that the agent never approves is kept exactly, and the mechanism that keeps
it is that the merge is the signature. §5 is rewritten around that.
Amendment B (answer 10) — the harness is the engine, not a feature. "This should become the bottle neck for driving all other improvements on that list. For example, if I wanted to integrate with SAP or fix any ERPNext connection, then it should be this harness that allows be to fix or build it." So §2's crew → plant table stops being an analogy: the roles the maintainer runs on his own machine — a steward that keeps a board, executors that take a handoff to a pull request, a triage that wakes on a delta, an evolver that changes the machinery — are what runs inside the product, against the product's own repository, budgeted and scheduled by the plant, with a person's merge as the signature. §2a says what runs where and what it honestly cannot do.
One thing this page still refuses to say. Nothing here claims the harness
will make the product self-evolve. That is the stated goal, in Scott's words, and
§5 says tier by tier what would have to be true for a change to land without a
person: a test that fails on the bug, a browser tier that runs it, a diff
confined to a declared file set, a warn-only release whose proposals a person
reviewed, and an automation line the plant wrote down. Until those hold, every
code change is a pull request a person merges, and the page says so in every
section that touches code.
1. What the harness is for¶
Scott's sentence is the specification: "ultimately the goal of this MES is to make better manufacturing decisions." So the first thing to pin down is what a decision is in this product, because the harness is worth exactly as much as the decisions it improves and nothing more.
A decision here is a write with a name on it¶
This MES has 68 write routes; 66 of them a screen calls
(docs/operate/assistant-coverage.md, generated 2026-09-27). Almost all of
them record something that happened. A much smaller set decides something —
a person commits the plant to a state it was not in, and the record says who:
| The decision | The route | Who may |
|---|---|---|
| Put a work instruction in force | POST /documents/{code}/approve/{revision} |
documents.approve |
| Put a trigger in force — logic against a live plant | POST /triggers/{code}/approve |
triggers.approve |
| Write a setpoint to a PLC | POST /adjustments/{code}/approve |
adjustments.approve |
| Name what every stop from now on is called | POST /equipment/downtime-reasons/{code}/approve/{revision} |
process.approve |
| Grade what every non-conformance is graded at | POST /quality/severities/{code}/approve/{revision} |
quality.approve |
| Disposition a non-conformance | POST /quality/nonconformances/{code}/disposition |
quality.close_nc |
| Close an order short, or over | POST /workorders/{code}/close |
orders.close |
| Change what a role grants | PUT /admin/roles/{code} |
users.manage |
Five of those eight are signatures on a draft somebody else wrote, and that
is the shape this product already has: a draft is authored by the role that owns
the domain and signed by the role that owns the consequence
(0035).
It is also the shape the agent role is built around — it holds the drafting
half of every pair and never the approving half
(src/fsmes/services/capabilities.py:130-142).
A better decision, then, is one of three things: a draft that arrives with the evidence for it, a signature that happens sooner because somebody was told a draft was waiting, or a signature that does not happen because the evidence said not to. Everything the harness does has to land on that list. A screen that makes a number prettier is not on it.
"Actionable to the company" is a proposal, a walk, or a signature¶
Scott's other phrase — "making it actionable to the company" — has a test in this repository already. Three things count as actionable output:
- a proposal with its evidence, which is what
recommended_adjustmentsalready is:rationaleis required and refused empty ("a recommendation needs a rationale - what was seen, and why this value"),evidenceis the JSON of what the proposer looked at, and the queue is "the only way anything writes to a PLC" (src/fsmes/domain/adjustments.py); - a walk, which is what the assistant already puts on a screen: 38 authored
surfaces (
services/assistant.py,SURFACES), each one landing on the real control with the real values in it; - a signed change, which is a row in
audit_logwithactor,on_behalf_of,beforeandafter.
Nothing else counts, and in particular a dashboard does not. That is not a style preference; it is what the product has already decided twice. Decision 0023 rejected Grafana for the fleet view with one line that applies to every chart this harness might draw: it puts the view "outside the product, where the honesty rules do not reach". And the same record rejected a single fleet OEE because "a fleet OEE is a lie unless every plant is the same shape, and no two plants are" — rule 1 at company scale.
What the harness inherits, and cannot argue with¶
Everything below is already accepted and binds every part of this page.
| Rule | Where | What it forbids the harness |
|---|---|---|
| Never invent production; unknown is not zero | house rules 1–2, CONTRIBUTING.md; 0019, 0030 |
drawing a gap as a zero, or a missing edge as no edge |
| Every figure carries how much of the window was watched | 0033 | an agent's number without its coverage beside it |
| What is withheld is the figure, never the evidence | 0033 | an agent that hides a ledger it could not summarise |
| The honesty rules must reach the view | 0023 | rendering plant numbers anywhere the product's rules do not apply |
| A judgment is a proposal, and never an input to a graded number | 0031 | a model's answer reaching OEE, a booked count, a yield or CI |
| A question declares the class of state it sends; shadow refuses by class | 0032 | any agent that has not said what leaves the box |
| The agent drafts; a person signs | 0035 | an agent holding any *.approve |
| Every list states its total | house rule; STYLE.md rule 4 |
a graph, a roster or a proposals queue that reads complete when it is not |
| One database per plant | 0021 | a harness that keeps its state anywhere but the plant's own database |
One consequence is worth stating early because it cuts against the most
exciting-sounding version of this idea. An agent in this harness may not make
the plant's numbers better. It may not label a stop, assign an orphan count,
or fill a coverage gap: 0031 puts a model's answer beside the fact, never in
place of it, and 0033's withheld figures stay withheld. What it may do is read
those refusals — the unlabelled bucket, the four not_observed causes, the
unassigned_production list — and propose the change that would stop them
recurring. The harness's job is to act on the product's honesty, not to
paper over it.
2. The map: crew → plant¶
Scott: "make this whole crew setup I have here, basically run in Bottling. Not exactly, I don't want my crew session there, but something like that."
The "not exactly" is load-bearing, and it is worth saying why before the table.
The crew is six role files, three shell scripts and a timer, driving Claude Code
sessions in terminal panes. Most of the machinery is repair, not
orchestration. Of the 200 lines of bin/crew-up, roughly 60 orchestrate and
roughly 80 repair the runtime: agents restored without their role after a
snapshot restore, a phone channel that went dead, a session that acquired a
/loop and ran 48 times a day for four days, orphan panes, sessions that drift
from their role after two weeks, and a screen-scraper that sends arrow keys at a
"trust this folder" dialog so a headless agent is not blocked for ever. None of
that is an idea. It is scar tissue from running an interactive CLI as a daemon,
and in a server process the whole category disappears — which is most of
what "not exactly" means.
What is left, after the scar tissue, is five ideas and one posture.
The table¶
| In the crew | What it actually is | In the plant | Why |
|---|---|---|---|
| steward | the only role in --permission-mode default: the risky calls become a dialog on Scott's phone |
no agent. It is the approvals queue plus the person who signs | The steward is not a worker, it is the human-in-the-loop mechanism. An agent called "steward" would be an agent holding *.approve, which 0035 forbids. The plant already has the queue: GET /dashboard/pending-approvals |
| executor | one handoff → one pull request → stop | one proposal. A draft with its evidence, landing in the queue, and the agent stops | Already the shape of recommended_adjustments and of every draft revision. The discipline that ports is the write order: the status flip is the last write, because a watcher fires on it |
| triage | woken by a no-model delta against a watermark, never by a clock; the model advances the watermark itself | the continuous-improvement crew's trigger | The best idea in the crew and the cheapest to copy. 131 script passes a week, 22 with news, a handful of model wakes — against the 48 model passes a day of the /loop it replaced |
| evolver | changes the crew and never the product; only on surplus; each change states the effect expected in next week's report; a smoke test gates it; rollback to a blessed tag |
the part of the CI crew that may act without a signature (§5) | Its three disciplines are exactly the ones §5 needs: spend only surplus, state the expected effect, and keep a one-step undo |
| scout | reads the internet weekly, writes one report, proposes at most three drafts | no plant analogue, deliberately | A scout is outbound by construction, and 0032 is about what may leave the box. The nearest plant-side thing is the fleet console, and 0023 says it observes |
| explainer | answers questions, never does work, files each one | the per-person agent (§3) | Near-identical already. Its failure is the warning: it was told to file a question as documentation and filed none in nineteen days, because nothing enforced it |
THROTTLE |
one integer, four legal values, resolving to (which roles stand, how many run at once) | one plant setting for how much AI this plant buys | Four values rather than a continuum, so it is explainable to an administrator. The crew's own mistake to avoid: the cap is written twice (crew-lib.sh and the guard hook). One table, two readers |
| the meter and the line | line = throttle × elapsed ÷ window, with a five-point margin and an end-of-window sweep-up |
spend against plan, per agent (§7) | Four lines of arithmetic over (share, elapsed, reset). It answers "may I spend now" with no coordinator. But note: the crew's meter is scraped out of a terminal statusline. The plant's is already a real number — usd on every ai_turns row |
| standing orders | prose Scott edits and agents may not, enforced by a hook | the six house rules and the accepted decisions | Already stronger: they ship in the wheel, and the capability checks and shadow.REGISTER enforce them in code rather than in prose |
BOARD.md |
the plan, who holds what, what is waiting | no analogue, and none wanted | The board exists because the crew has no database. It is edited by one guarded script (bin/board-edit) that refuses a write shrinking the file by more than 10 %, because an agent once truncated it and nobody noticed for fifteen hours. The plant has a database and 0021 |
handoffs/*.md |
116 files, 100 done; Goal / Why / Constraints / Definition of done / Out of scope / Result |
the draft rows that already exist — a document revision, a trigger, an adjustment, a reason, a severity | Why cites its provenance; Result carries Verified and Left out, and why. A proposal's rationale and evidence are the same two fields |
journal/ |
479 stanzas, append-only, several concurrent writers, no locking — timestamps run backwards and three files carry a duplicate heading | ai_turns and audit_log |
Already there and already better: indexed, pruned to a stated horizon, gated on audit.read, and one writer per row |
reports/fitness-*.md |
the crew measuring itself weekly | the eval suite and fsmes assist coverage |
Both exist. Neither is on the AI tab yet (§8) |
bin/crew-guard |
seven mechanical refusals | the capability checks, and shadow.REGISTER's 36 entries |
The lesson, not the mechanism: every rule in the guard exists because prose failed first. The role file said "you do not loop" and the agent looped for four days |
panes, sessions, --resume, permission modes, clear_trust_dialog |
~80 of crew-up's 200 lines |
nothing | This is the "not exactly". A server process has one long-lived loop already (api/app.py's lifespan) and needs none of it |
The five ideas worth taking, in order¶
- Wake on a delta, not on a clock — and let the expensive thing advance the watermark, so a wake that did nothing is visible rather than silent.
- A budget is arithmetic, not a limit — spend-so-far against share-times- elapsed, with a margin. It tells an agent whether it may run without asking anybody.
- One ordinal knob resolving to which agents stand and how much runs at once, held in one place.
- A role is a row: prompt, model, tool subset, cadence, budget, and where its work lands. And the distinction that matters — a standing agent resumes, a one-shot agent starts fresh, because "a two-week session drifts from its role".
- The cheap tier runs first. The crew has a local index that searches, answers and first-pass classifies for free, and the rule is to use it before reading whole files. The plant has qwen on the box for exactly this.
And the one posture: the risky thing becomes a question, and the agent waits. In the crew that is a terminal dialog on Scott's phone. In a plant it cannot be, because the person who must answer is on a shift and may never open a terminal. So the whole of the crew's safety model has to be replaced by the approvals queue, and that is the single most important thing this design has to get right. It is also the thing the market is asking for: ABI Research's 2026-07-30 note names the leaders for aligning with "repeatability, transparency, and human oversight, rather than promising fully autonomous execution on the factory floor".
2a. Amended by answer 10: the map is not an analogy¶
Scott, 2026-09-28: "This should become the bottle neck for driving all other improvements on that list. For example, if I wanted to integrate with SAP or fix any ERPNext connection, then it should be this harness that allows be to fix or build it."
So the table above is not a set of metaphors. The roles are what runs inside the product, against the product's own repository. A steward that keeps a board of what is being worked on; executors that take one piece of work from open to a pull request and stop; a triage woken by a delta rather than a clock; an evolver that changes the machinery and never the product in the same pass. The plant schedules them and pays for them; a person merges.
That is a bigger claim than anything else on this page, so it is worth being exact about what it requires, because three of the four requirements do not exist and one of them cannot exist in some plants.
What runs where¶
The plant box has the wheel, not the repository. A plant runs fsmes from an
installed wheel (PyPI, or the container image) with its own database beside it
(0021). There is no git checkout
on it, no compiler, no test runner and no network path to GitHub, and none of
those is an oversight: 0005 is
"plain HTML, CSS and JS, no build step" precisely so a plant PC needs no
toolchain for years.
So a harness that edits code needs three things the plant does not have, and they have to be somewhere:
| What it needs | Where it can honestly be | What it is today |
|---|---|---|
| A checkout of the product's source | not the plant. A machine the maintainer controls — his own box, a runner, or a container the project publishes — holding a clone and a branch per proposal | the maintainer's own crew already is this, exactly: worktrees under ~/.herdr/worktrees/, a branch per handoff |
| A CI that can fail the change | GitHub Actions, already: test, postgres, browser, lab, lockfile, wheel-demo, plus the DCO sign-off check |
exists and is required on pull requests |
| A way for the result to reach a running plant | a release. A merged change reaches a plant when the plant upgrades its wheel or its image and runs its migrations — which ship in the wheel since #25 | exists, and is the only path |
The consequence, stated plainly: a code change made by the harness does not reach a plant until the plant upgrades. There is no hot patch, no code pushed down a wire into a running plant, and this page does not propose one. A plant that pulled executable code from the internet at run time would be a plant that fails its own change management, and the field's most disciplined comparable project refuses even to run resident on an OT network for the same reason (§9). The loop is: the plant's records produce the finding → a checkout somewhere else produces the change → CI judges it → a person merges it → the release carries it back. The plant is where the evidence and the goal come from; it is not where the compiler runs.
What a plant with no internet can do¶
An air-gapped or shadow-mode plant cannot participate in the code half at all, and it should be told so rather than shown a dead button. What it can still do is everything the harness does with configuration and evidence:
- Produce the finding. The crawl over its own refusals — unlabelled seconds, withheld stations, orphan counts, drafts nobody signed — runs on the local model or on no model at all, and its output is a proposal in the plant's own queue.
- Export the finding, not the data. A proposal is a sentence, an evidence
payload and a diff.
fsmes shadow's register already names, per class, what may leave the box (0032); a finding written for a maintainer iscatalogueorconfigurationclass, and a plant that will not export even that is a plant that contributes nothing upstream and keeps working. - Receive the fix as a release, like every other plant.
What it cannot do is run the cloud brain, and the register already refuses that with its own sentence: "it carries this plant's numbers off the box, and a plant lending us its data to watch did not agree to that."
What the maintainer's own crew keeps doing¶
Governance of the public repository stays a person's, and stays outside the product. Merging, releasing, tagging, answering an issue, writing on the website, posting anywhere — the crew's standing orders already reserve every one of those for Scott, and nothing in this design moves them into a plant. A plant's harness may open a pull request; it may not merge one, may not tag a release, and has no account on GitHub of its own that a maintainer did not deliberately give it.
That division is also the answer to "I don't want my crew session there" from
2026-09-27: the crew's shape ports; the crew's session does not. About 80 of
bin/crew-up's 200 lines are repair of an interactive CLI run as a daemon, and
in a server process that whole category disappears.
What answer 10 does to the priority list¶
It reverses it, and that is the whole of the argument in §9. Every other item on the project's roadmap costs an evening per improvement. If SAP and ERPNext work is to go through the harness, then the harness is not competing with the first-real-machine work for evenings — it is the thing that is supposed to make each of those evenings produce more than one change. That is a claim that can be wrong, and §9 says what would show it wrong.
3. One agent per person¶
Scott: "each user gets their own agent (this tab manages context/history)."
What a person has today, and how long they have it¶
Three stores, and only one of them survives anything.
| Store | Where | Lives for | Holds |
|---|---|---|---|
| The conversation the model actually sees | agent._sessions, a dict in the API process (services/agent.py:686) |
until 30 minutes of silence ([admin] agent_session_ttl_seconds), a restart, or a second worker taking the next request |
the whole message history, the open proposals, the tool results |
| What the panel showed | the browser's sessionStorage, capped at [screens] assistant_log_entries (60) |
until the tab closes | the rendered lines |
| The plant's record | ai_turns, in the plant's own database |
[admin] ai_trace_days, ninety days by default; 0 keeps everything |
one row per turn: asked, said, tool summaries, proposals and outcomes, tokens, usd |
So the honest answer to "each user gets their own agent" today is: for thirty minutes, in one process's memory, and it forgets everything on restart. After that the person starts cold. An open proposal card is not even confirmable — the confirm endpoint answers "that conversation has expired - ask again".
The handoff for this page assumed ai_turns is already the history. It is
not, and this is the most important correction on the page. ai_turns is a
record for people: tool payloads are deliberately excluded, summaries are cut
at 160 characters, it is pruned, and nothing anywhere reads it back into a
session. It cannot be replayed to a model, and it was never meant to be
(docs/operate/ai.md; domain/ai_turns.py).
There is a second problem in the same place. The AI tab is behind
audit.read, which an operator does not hold. So today a person cannot read
their own past conversations; a supervisor can read everybody's. That is right
for an audit screen and wrong for "each user gets their own agent", and the two
are different screens wanting different gates.
What to build, in three pieces that are deliberately separate¶
- A transcript the person owns. Durable, keyed to the person, replayable to
a model — the
tool_use/tool_resultblocks the Messages API needs, whichai_turnsdeliberately does not keep. It is the person's own, readable by them withoutaudit.read, and it is what makes "pick up where we left off" true. Recommendation: a table of its own in the plant's database (0021), with its own horizon, defaulting shorter than the trace's ninety days — a transcript is a convenience and the trace is the record. - The trace. Exists. Unchanged. Behind
audit.read, because it is the record of what was done in this plant and by whom. - Memory, which is not history. This page recommended nothing at first. Answer 7 replaced that: "I'd envision a fully customizable agent with full memory and potentially even a skill set later … But simple is best for now and I'd defer to a simpler proof of concept, with just retaining memory for now."
So memory is in, at proof-of-concept size, and the size has to be written down or it will grow. "Retaining memory", for M2, is exactly two things:
- the durable transcript of piece 1 above — the agent picks up where the
person left off, across a restart and across a week, because the
tool_use/tool_resultblocks were kept; and - a short remembered-facts list, plain sentences, which the person can
read and delete on the
my agentview, with a stated maximum number of entries and its total shown beside it like every other list.
And nothing else. Not a skill set, not a profile the agent infers, not a
vector store, not a memory shared between people, and never a plant
number — a remembered number is a plant fact cached outside the table that
owns it, which is the thing ai_turns already refuses to do ("a screen gated
on audit.read that carried raw tool results would be a way around the
capabilities those rows are behind"). The agent re-reads numbers every turn
because the system prompt already tells it to, and a remembered "OEE on MIX01
is 62 %" would be wrong by the next shift and convincing anyway.
Two consequences that must be built with it rather than after it. A remembered fact is written only when the person's own turn put it there, so the list is readable as things I told it about me rather than things it decided about me. And a remembered fact is not private from a supervisor, like everything else in the trace, so the same sentence that says so on the panel says so on the memory list. The fully embodied agent with a skill set is a later design, and this page does not pretend to be it.
What is private, and what the plant may see¶
Say this plainly on the page and on the screen, because it is not what people
assume: nothing a person types to the assistant is private. It is in
ai_turns and every holder of audit.read can read it. That is the correct
answer for a regulated MES — the trace exists because on 2026-09-26 a
conversation broke and nothing on the machine could say what it had been asked —
but it means a per-person agent is not a private assistant, and the tab should
say so where a person types.
Purging is already honest and stays: rows past the horizon are deleted as new ones are written, so the screen shows what the plant has rather than what a cleanup job got round to.
What it costs¶
Today, one number for everything: MES_AGENT_MONTHLY_USD, default $10,
summed from ~/.local/share/fsmes/agent-usage.jsonl. Three things about it are
wrong for a harness and §7 fixes them: it is an environment variable, not a
plant setting; it is per box, not per plant (that file holds every plant
on the machine); and it is one pool, so a person's conversation and a
nightly crawl spend the same money with no way to say which may have it.
What it must never do — the current design, kept, with one hole named¶
An agent acts within the capabilities of the person it acts for. That is
enforced in catalogue() and withheld(): the model is never shown a tool the
person may not use, and the list of what they may not do is in the cached system
prompt, each entry with the capability and who holds it. Keep all of it. One
thing is not true yet and a harness will make it worse:
on_behalf_ofis attribution, not authorisation. The API authorises against theAGENTaccount's own role — 14 capabilities, no*.approve— and the person's capabilities are enforced only insideagent.py, on this side of the API. That is deliberate and it works, but it means every new agent kind either gets its own account and role or silently inherits AGENT's fourteen. §4 makes that a requirement.
The second hole this page found has been closed since it was written, and the
numbers in it have moved with the fix. Nine write tools — add_person,
register_gauge, calibrate_gauge, issue_certificate,
issue_pallet_certificate, produce_batch, pack_unit, set_unit_status and
erp_retry — were in no NEEDS entry and no SURFACES entry, so catalogue()
offered all nine to anybody holding plant.read, and only the API's own gate
against the AGENT account refused them. PR #119 closed it on 2026-09-27.
Measured against main at ad5219d3, which this revision is written against:
NEEDS ∪ PER_CALL_NEEDS is now 47 of the 47 write tools rather than 38, and
the catalogue offered on plant.read alone is 51 — every read tool that
takes a plant argument, and no write tool at all — rather than 60.
assistant-coverage.md is accurate for those nine rows again. It is named here
rather than deleted because it is the reason §4's first field is the account:
the hole existed for months and nothing exploited it only because the floor
assistant never writes unattended.
The tab is where a person manages their own agent¶
Which, concretely, means the AI tab needs a second view that is the person's
own: my conversations, what my agent spent, what it has proposed for me and
what became of each, and a way to start again. plant.read is the right gate
for that view; audit.read stays the gate for everybody's.
4. Functional agents¶
Scott: "there should be functional agents, like data analysis with full MCP for extremely advanced analytics if necessary … Along with a continuous improvement team that crawls this data at some configured time span to look for improvements."
What a functional agent is — six fields, and it is a row¶
Recommendation: an agent kind is a row in the plant's database with six fields, and nothing more. This is the crew's "a role is a row", with the crew's scar tissue left out.
| Field | What it is | Already exists as |
|---|---|---|
| account and role | its own Person row and a role that is a bundle of capabilities |
AGENT is already exactly this (plant.py:84, role agent, 14 capabilities) |
| tool set | which of the 100 MCP tools it may call | catalogue(capabilities) already computes it; a kind names a further subset |
| cadence | on demand, or a named interval | nothing. The one precedent is the hourly retention task in api/app.py's lifespan |
| budget | what it may spend in a month, and what one run may spend | MES_AGENT_MONTHLY_USD for the whole box; [admin] agent_max_rounds per conversation |
| data class | what state its questions carry off the box | 0032's four classes: catalogue, configuration, observation, production |
| where its work lands | the table its output is a row in | recommended_adjustments, the draft revisions, and the approvals queue |
A kind declares all six or it does not exist. The fifth is the one nobody would think to add: an agent that has not said what leaves the box cannot be switched on in a plant that cares, and 0032 already made every question say it.
Amendment A adds a seventh field, and only for the kinds that have it: a checkout. A kind that edits the product's own code needs a clone of the repository, a branch per proposal, and a CI that can fail it — and §2a says why none of those is on the plant box and what that means for a plant with no internet. The six fields above are unchanged for every kind that does not edit code, which is all of them until M4.
(a) The analysis agent¶
What it is. On demand, from the AI tab, for somebody who has sat down with a question. It reads deeply and writes nothing.
"Full MCP" means every read tool and no write tool. Concretely, today: 53
read-only tools of the 100 — and catalogue()'s own filter drops two that
take no plant argument, so the surface a model is actually offered is 51,
plus the two walk tools. A write tool is recognised by having a dry_run
parameter (agent.py:544), and assist_coverage.py derives the same 47 names
independently from which helper each tool's body calls. Two independent
definitions that agree is what makes "no write tool" checkable rather than
asserted, and the test should be written that way: the analysis kind's
catalogue is asserted to contain no tool with dry_run.
The gap to close first. The four analyses this product already computes —
oee_breakdown, state_timeline, downtime_pareto, tag_trend — have no
MCP tool at all. They are HTTP-only (api/routers/analysis.py), and the
analysis module carries no tools=. So an "extremely advanced analytics"
agent today cannot reach the plant's own analytics; it would recompute them
from machines, downtime and tag_trend, and a recomputed OEE is the thing
0031 and
0033 exist
to prevent. Four read tools over the existing service is the whole of the
analysis agent's first requirement, and they must return the payload the
screens get — coverage, coverage_note, the full ledger, unknown_seconds,
labelled_by, every total — so the agent inherits the honesty rather than
being told about it.
What it may render. Explorations, in the AI tab or beside the conversation, for the person who asked. Never a default screen (§6). Every number it shows carries its coverage, because the payload carries it; a station whose figures are withheld is reported withheld with its ledger, not omitted and not averaged away.
What it must never do. Compute a KPI itself. Answer a question the ledger withheld. Reach a graded number. All three are 0031 and 0033 already.
(b) The continuous-improvement crew¶
What it is. A scheduled crawl over records the plant already keeps, producing proposals and nothing else.
What it reads. The trace (ai_turns), the audit trail, the four analyses, the
coverage ledger, the eval results, and the plant's own refusals — the
unlabelled bucket, the four not_observed causes, unassigned_production,
the withheld stations, the drafts that have been waiting.
Where its work lands — and this is the important part. Into the flows that
already exist, never a third mechanism. There are exactly five draft-and-sign
pairs in this product and the agent role already holds the drafting half of
every one:
| What it finds | What it drafts | Who signs |
|---|---|---|
| 40 % of stops typed four ways | a downtime reason (draft_downtime_reason, process.define) |
process.approve |
| non-conformances graded in free text | a severity (draft_nc_severity, quality.define) |
quality.approve |
| a stop pattern a condition would catch | a trigger (draft_trigger, triggers.write) |
triggers.approve |
| scrap tracking a setpoint over 40 hours | an adjustment with rationale and evidence (propose_adjustment) |
adjustments.approve |
| a procedure the floor works around | an instruction revision (revise_document, documents.write) |
documents.approve |
And it lands where an approver will see it: GET /dashboard/pending-approvals,
which already answers "what is waiting that this caller may act on, counted,
with its total" from the caller's live capabilities, and which already refuses
to list a kind it cannot show the substance of. Two kinds are wired today
(review.KINDS: downtime reason, NC severity); work instructions, triggers and
adjustments are named in the code as the follow-up, "an entry in that registry,
not a new mechanism here". Wiring the remaining three is a prerequisite for
the CI crew, and it is worth doing whether or not any of this is built.
And its role is not AGENT's. The five drafting capabilities it needs are the
five in the table and nothing else. It must not simply sign in as AGENT and
inherit that role's whole bundle, for a reason §5 sets out and which is the single
strongest finding on this page: AGENT holds process.define and
quality.define, and twenty-four live settings behind those two take effect the
moment they are saved.
Its cadence. One precedent, and it is the right one: the tag-retention sweep
in api/app.py's lifespan, whose own comment states the pattern — "Retention
runs in the API process because it is the one long-lived process every
deployment has. Hourly, in batches, and it says so on Ops." A nightly crawl is
that task with a different interval and a plant setting behind it.
Its trigger, better than a clock. Take the crew's best idea. A no-model delta first: has anything the crawl is about changed since the last pass — new unlabelled seconds, a new withheld station, a new open non-conformance, a draft that has crossed a waiting threshold? If nothing changed, no model runs, and the pass is recorded as a pass that found nothing. The agent itself advances the watermark, so a wake that did nothing is visible. The crew replaced 48 model passes a day with a handful this way.
What "found nothing" must never look like. ai_status's rule already:
unknown is not zero and stale is not dead — "a night the loop did not run
is unknown in the morning, never a quiet 'clean'."
5. What needs a signature, and what does not¶
Scott, 2026-09-27: "Maybe even making things like UI updates 100 % automated whereas other improvements need ADMIN user approval."
Scott, 2026-09-28, answer 2, which replaced this section's shape: "I was thinking a person should literally be able to change the code to improve the UI. Move cards, change graphs, etc. This should be as maliable as possible."
So there are three tiers, not one line. The fifteen numbers this page found are the floor — tier (a). The thing Scott means is tier (b), the harness changing the product's own code. Tier (c) is everything else, and it never automates. Each tier below has its test — what has to pass before the change counts as safe — and its signature — who or what stands behind it.
Where the line is today¶
There is no automated tier by design, and the reason is not caution — it is
that the settings Scott means are behind the administrator's own capability.
There is one by accident, and it is the subsection after this. The
numbers the screens run at are the [screens] pack table: fifteen keys across
six Configuration sections (how often a screen re-reads the plant, how many
machine cards and orders a page shows, how many rows Admin lists, the whole-list
ceiling, how long a confirmation lingers and how long typing settles, and the
three numbers behind the assistant's own walks). Every one of the six sections
carries define="users.manage" (modules.py:331-434). The agent role
deliberately does not hold users.manage, so an agent cannot write one of
them today, and write_plant_setting is refused with the capability named.
And there is no code tier at all. No agent in this product has ever written a line of the product, there is no checkout on a plant box (§2a), and the only thing that changes a plant's code is an upgrade.
There is also a precedent for the opposite answer, and it is worth reading
before proposing anything: signals.define went to admin and explicitly not
to agent, with the reason in the source —
"an agent may draft a vocabulary for somebody to approve, and retuning how hard the OPC agent retries a booking is not drafting — there is nobody in the loop after it."
That sentence is the real boundary, and it survives amendment A intact. Not "is it the UI"; is there anybody in the loop after it. What answer 2 changes is the answer for code, not the question: for a code change there is somebody in the loop after it, and it is whoever merges the pull request.
An automated tier already exists, by accident, and it is twenty-four keys¶
This is the most important thing this page found in the code, and nothing in the amendments touches it.
A live Configuration section is written through
PATCH /dashboard/config/{domain}/settings/{key}, and "the gate is the
section's, not this endpoint's" — the capability is looked up per key from the
section that owns it
(api/routers/dashboard.py:609-614). A section with approve=None takes effect
when it is saved, which is rule three of
0035:
a number with no pending state has nothing to sign.
Twenty-two of the 56 sections — twenty-four keys — have approve=None and a
define the agent role already holds (process.define or
quality.define). Among them:
[quality] hold_rulesand[quality] major_rules— which SPC rules raise a hold, the whole subject of 0036;[quality] cpk_capableand[quality] cpk_marginal— the bars a process is judged capable against;[oee] min_observed_seconds— how little observed time is too little to divide by, which is the gate on whether availability is a number at all;[quality] nc_code_prefix,[quality] serial_digits— what records are named;[process] default_report_hours,report_windows,gantt_screenful— the presentation numbers, and the only three of the twenty-four that tier (a) below would admit.
Today nothing exploits this, for one reason: the floor assistant never writes
unattended. Every write is proposed, the person presses "Do it", and the
confirm runs as AGENT on their behalf. There is a person in the loop on every
one.
A scheduled agent has nobody in front of it, and if it signs in as AGENT it
inherits all twenty-four. So the first consequence of building a functional
agent is not a new capability — it is that a functional agent must not run as
the AGENT account. It needs a role of its own with a bundle chosen for its
job, which is exactly what §4's first field says and why that field is first.
The second consequence is that the honest version of tier (a) is a narrowing, not a widening: the plant declares a small list of keys an unattended agent may put in force, and every other live setting stays a proposal even though the account could technically write it.
Tier (a) — the numbers a screen runs at¶
The change. A value in the [screens] table, plus [process] gantt_screenful
(see below): how often a screen re-reads the plant, how many cards a page shows,
how long a confirmation lingers, how many times a walk retries an anchor.
The test. Five clauses, all five of which must hold:
- it changes a number a screen runs at, not a number the plant is judged
by. The test is what the number does, not which pack table it sits in —
[screens]is the table where these live, and it is not quite the whole set (seegantt_screenfulbelow); - it changes no record's meaning — not what a stop is called, not what a non-conformance is graded at, not what a figure counts, and not how long a record is kept;
- it changes nobody's capability, and nothing a role grants;
- nothing a PLC reads changes, directly or through a cadence that a write depends on;
- it is undone from the screen that shows it, in one step, by the same path that made it.
The signature. Warn-only for the first release (answer 5): the crew
proposes the number like anything else, nothing applies itself, and a person
reads a month of what it would have done. After that release, and only in a plant
whose administrator has granted screens.define, the change applies itself and
writes its own audit row.
Why this tier is small and stays small. In 0035's vocabulary it is tier three's presentation edge — a number whose only consequence is what a person sees on the next page load. It is not a new tier; it is the smallest part of the existing one. Anything failing one of the five clauses is tier (c).
Tier (b) — the product's own UI code¶
This is what answer 2 asked for, and it is the new thing on this page.
The change. A diff to the operator UI's own files — moving a card, changing a chart, adding a column, fixing a layout that is wrong on a phone — proposed by the harness as a branch and a pull request, and merged by a person. Not applied to a running plant; see §2a. It reaches plants in the next release, like every other change.
The test. The repository's existing gates, unchanged and unweakened, all of
them required on the pull request: test, postgres, browser, lab,
lockfile, wheel-demo, and the DCO sign-off check. The browser tier is the
one that matters here — it exists because until 2026-09-25 the Chromium tests
were slow-marked and nothing ran them, and a test_ui_nav assertion had been
red on main for a day of green checks. A UI change with no browser test is a UI
change nobody checked.
Beyond the gates, three things a code proposal must carry or it is not reviewable, taken from the crew's own handoff shape: the finding it came from, with the record that produced it; a test that fails before the change and passes after it, named as prose; and one sentence saying what it did not do and why.
The signature. A person's merge, for a release — and that is not a temporary
measure, it is the mechanism. Decision 0035 says the agent never approves.
Here the approval is the merge button on a pull request, held by a person with
commit rights on a repository the plant does not control. That is a stronger
signature than any *.approve capability in the product, because it is outside
the plant the agent is running in.
What could later automate, and what could not. After the warn-only release
(answer 5), the candidate for automation is narrow and it has to be written down
before it is granted, because "touch only web/" sounds obvious and is not.
Included — the per-screen files, where a diff confined to one screen's own
web/<screen>.html, web/<screen>.js and web/<screen>.css changes what that
one screen looks like and nothing else. There are 27 such HTML files, 32 JS and
16 CSS.
Excluded, by name, even though they are under web/:
| File | Why it is not presentation |
|---|---|
common.js (778 lines) |
the nav and the one api(). It exists because "the nav was pasted into eight HTML files - which is how one page grew a duplicate link and seven lost their current-page marker". A change here changes every screen at once, and test_ui_nav.py checks the nav against the module registry |
assist.js (952) and assist-record.js (552) |
the walk engine and the recorder. Walks are how approvals are signed — "approvals are walks to the signature" (#113) — and the 2026-09-25 anchor race was a bug in this file that made the product look permission-broken on Scott's phone. Not layout |
kit.js (255) |
the one set of coverage charts, so that "the Floor tile and the machine page can never state coverage differently". A change here changes how a coverage figure reads on four pages at once. That is honesty rendering, and honesty rendering is tier (c) |
styles.css (586), themes.css, themes.js |
the palette and the four themes — STYLE.md rules 2 and 3. All four themes are first-class, and a change to the palette is a change to every screen in four looks |
web/vendor/ |
three.js, vendored with its licences and SHA-256s under the standing terms "these files are read, never built" |
web/line/, line3d.html |
the 3D line view, which is the one place the no-build-step rule is met by a vendored ES module rather than by hand-drawn SVG |
And three exclusions that are about the diff rather than the file, because a change inside an included file can still be any of them:
- Removing or renaming a
data-assistanchor. Guides point at anchors, andtests/page_anchors.pywithtest_assistant.pyandtest_agent.pyfail when a guide step names an anchor its page does not have. The test catches it; the point is that it is not a presentation change, it is a change to how somebody is walked to a signature. - Moving a computation into the browser. A screen draws what the API
measured (0031,
0033, and
kit.js's own "nothing here computes a number"). A diff that starts calculating a figure in JavaScript has changed what a number means, whatever directory it is in. - Fetching anything from outside the box — a script tag, a font, a CDN.
STYLE.mdrule 9 and 0005.
One honest hole in automating this tier, and it has no answer yet.
fsmes ui-check crawls every screen in every theme and judges it against
accepted baselines in tests/ui/baselines/ — and ui-check --accept makes the
current look the accepted look. An agent that may run --accept can erase the
only check that would have said the look changed. So either the baselines are
re-accepted by a person as part of the merge, or tier (b) never automates at all.
This page recommends the first and names the second as the fallback; it does not
pretend the problem is solved.
Tier (c) — everything else, always a person's merge¶
Everything not in (a) or (b): the kernel, the API, the domain services, the capability model, migrations, the packs, the connectors, the docs that state a rule. A harness may propose any of it — that is the whole of answer 10, and it is how SAP or an ERPNext fix would arrive — and none of it ever applies itself. There is no release after which tier (c) automates, and no setting that turns it on.
Three things are not automatable at any tier, and the product already refuses
them: a person's capability, a password, and the deletion of a record. The
assistant has no tool for DELETE /admin/roles/{code}, for any /auth/*
password route, or for any *.approve route, each with a written reason.
The line itself is a setting, not a constant (answer 1, and answer 3)¶
Scott, answer 1: "Seems right to me but it should be configurable with the same
agentic access to change." Answer 3, on screens.define: "Configurable with
MCP tool."
So the boundary above is not a constant in the wheel. It is this plant's
configuration, in the [ai] domain of §7, and it says: which tier (a) keys
this plant will let an agent write; whether tier (b) is warn-only or automated;
and which file set tier (b) counts as presentation. Three consequences follow
from putting it there, and all three are already the product's own rules:
- The agent can read it and propose changes to it through the settings tools
that already exist (
read_plant_setting,write_plant_setting,propose_plant_setting, §11 of configuration assistance), which is exactly what Scott asked for. It reads it the way it reads any other setting, and it proposes a change to it the way it proposes any other change. - It is gated on
*.approve, not on adefine. This is the one setting in the product that must not be aapprove=Nonesection, because a section that takes effect when saved would let an agent widen its own boundary and then use the widened one. The section that holds the automation line is signed, by a person, every time it moves — and the pending-approval row says which clause moved and in whose favour. screens.defineis granted and revoked through the same tools, on the same gate: an agent may propose that a plant grant it, with its evidence; an administrator signs. It is off by default and a plant that never grants it never has an automated change, and the harness still works — the proposals simply wait.
The mechanism, per tier¶
Common to all three. A role of its own for every functional agent, so no
scheduled agent inherits AGENT's process.define and quality.define and the
twenty-four keys behind them. This one is not optional and does not depend on the
rest.
Tier (a). One new capability, screens.define, gating the six [screens]
sections instead of users.manage, described as "Set the numbers this plant's
screens run at: refresh rates, page sizes, how long a confirmation stays". No
screens.approve beside it — there is nothing to sign, and 0035's rule three
plus signals.approve's own absence say a capability that gates nothing is a
role saying something untrue about itself. [process] gantt_screenful moves
under it, or is named as an exception on the screen; either way the set is
written down in one place, in the [ai] domain, where it is readable.
Through the API, never through a pack: 0035 records that fsmes pack apply
writes no audit row, and an automated change that left no audit row would be the
one change in this product nobody can find, so the path is the same PATCH a
person's Save uses.
Tier (b). A checkout, a branch per proposal, a pull request, CI, a merge — §2a says where each of those lives and why none of them is on the plant. Three things it needs that the maintainer's own crew already has working and the product does not: a worktree per piece of work, a handoff shape that carries the finding and the evidence into the pull request body, and a sign-off on every commit. The DCO check already enforces the third.
Tier (c). Nothing new. It is a proposal like any other, and the merge is the signature.
The boundary applied to ten things the harness might actually find¶
| # | What it found | What it would do | Verdict |
|---|---|---|---|
| 1 | The floor page shows 24 machine cards; this plant has 9 and half the page is empty | [screens] floor_machine_page 24 → 12 |
tier (a) — presentation, reversible, nothing judged |
| 2 | Walks time out on this plant: 20 attempts at 150 ms is 3 s and the config page renders in 4 | [screens] assistant_fill_attempts 20 → 40 |
tier (a) — this is the 2026-09-25 bug as a setting |
| 3 | Operators miss confirmations; the toast is gone before they look up | [screens] toast_ms 3500 → 6000 |
tier (a) — an accessibility answer a plant gives for its own people |
| 4 | The state timeline shows 12 machines of 60 and people never scroll | [process] gantt_screenful 12 → 20 |
tier (a), and the clause-1 exception — read the note below |
| 5 | On a phone the order card's three buttons wrap and the middle one falls under the fold | a diff to orders.html and orders.css, with a browser test that fails at 390 px before it |
tier (b) — a pull request, CI, a person's merge. This is answer 2's "move cards" exactly |
| 6 | The OEE waterfall's bars are unreadable in the high-contrast theme | a diff to analysis.css — and not to styles.css, which would be tier (c) |
tier (b), if it stays out of the palette; tier (c) the moment it touches a theme variable |
| 7 | Forty per cent of stops are four spellings of one changeover | draft a downtime reason changeover |
process.approve — it names what every stop from now on is called. Tier (c) |
| 8 | Brix has gone over specification eleven times without an NC being raised | draft a trigger: brix > 11.5 sustained 60 s → open NC | triggers.approve — logic against a live plant, and the record says so |
| 9 | Scrap tracks MIX01's speed setpoint across 40 hours | propose an adjustment 1.33 → 1.25, with the evidence | adjustments.approve — the human in the loop before a PLC write |
| 10 | The ERPNext connector drops a confirmation when the Job Card is closed between read and write | a diff to the connector, a test that reproduces it, a pull request | tier (c) — and it is answer 10's own example. The harness may find it, write it and prove it; a person merges it |
Two more that show the edges, both real and both from this repository's history:
half the stations reporting on under 30 % coverage would want [oee]
coverage_floor, which has no Configuration section at all — it changes only
when the pack is applied and the plant restarts, and fsmes pack apply writes no
audit row, so an agent could not leave a record of having done it; the proposal
there is a sentence to a person. And "operators cannot read their own AI
conversations" wants a capability grant, which is never automatable at any
tier.
Number 4 is the one to read twice, because it is tier (a)'s clause-1
exception and it is a real finding. gantt_screenful is a number a screen runs
at — how many machines the state timeline draws before it stops and says so — and
it lives in the [process] pack table rather than [screens], because that is
where the analysis service's numbers live (config-assistance.md §13). So the
boundary cannot be "the [screens] table" and has to be the five clauses; a
plant's answer is a list of keys the product declares automatable, and
[process] gantt_screenful is on it while every other [process] key is not.
Which is one more argument for the line being a setting a person signs: the set
it gates is a judgement somebody made once, and it should be readable on a
screen.
Every automated change is still a record¶
- An audit row, written through the same
audit.record()every other write uses, withactor = "AGENT"andon_behalf_ofnull — and that is a new case worth naming, because the column's own comment reads "Null for a person acting as themselves." An agent acting on its own standing is exactly that and has never happened before; the screens must render it as the agent, not as a blank. - A trace row, so the change sits beside the reasoning that produced it — which is the pairing nothing in the researched field has: the 2026-09-25 sweep found no product keeping a record of model turns beside its business audit trail, and the closest thing (a hash-chained audit of device writes) has no orders to sit beside.
- Undone by the same path.
setting_changesalready answers "what was it before"; the undo is writing that value back, by the samePATCH, and it is one row in the same trail. - Counted. The AI tab says how many changes this agent made without a signature this month, beside its spend. A number nobody can see is a number nobody governs.
And for tier (b), a code change's record is the pull request — the branch, the diff, the CI run, the sign-off and the merge commit, all of them outside the plant and all of them permanent. That is a better record than anything the plant could keep about its own code, and it is the reason the code half lives where it does. What the plant keeps is the other end: the finding that produced the change, and — once the release carries it back — the version it arrived in.
6. Bare minimum by default; depth in the agent¶
Scott: "Most other MES systems seem to have a ton of analytics but it's too much. Ours should only default to showing bare minimum with somewhat limited functionality. Whereas the analytics agent could do extremely deep and interactive data explorations."
What the default screens already are¶
29 addressable dashboard URLs over 26 templates (the gap is the four
Configuration workspaces sharing one page), across 23 modules of which 9 are
kernel. Four of the 29 carry the full coverage treatment — the floor tile, the
machine page, the line page and the analysis page, sharing one kit.js so that
"the Floor tile and the machine page can never state coverage differently".
One page is the analytics page, and the registry already says why it is
separate:
"Shift analysis: OEE losses, the state timeline, downtime pareto and tag trends. Its own page because these are questions you sit down with, not things you watch." (
modules.py:1098)
That sentence is already the rule Scott is asking for. It should be promoted from a comment to the design.
The rule for a default screen¶
Recommendation, one sentence: a default screen answers a question somebody has while standing up. State, queue, what is waiting on me, what is alarming, what I must book. Anything you sit down with — a waterfall, a pareto, a correlation, a window you choose — belongs behind a deliberate act: the analysis page for the five things a plant looks at every week, and the analysis agent for everything else.
The corollary matters more than the rule: the way to keep the default screens bare is to stop adding settings and charts to them, not to hide things. The 2026-09-21 audit is the inventory to hold this against — 419 raw candidates in 235 files, 75 curated, by scope 9 general, 54 plant, 12 object. Its own verdict is that it "does not turn seventy-five candidates into seventy-five questions"; sixty-six are routed rather than asked. A harness that proposes one new screen control per finding would undo that in a month.
The rule for what the analysis agent renders¶
- In the AI tab or beside the conversation, for the person who asked, and gone when they are done. An exploration is an answer, not a screen.
- Never promoted to a default screen without a person's decision — and when
one is, it joins the system:
STYLE.mdrule 12, the palette, four themes, adata-assistanchor, andui-checkcrawling it from the day it exists. - Every figure carries its coverage, because the payload it came from does.
A withheld station is reported withheld with its ledger, exactly as
analysis.jsalready does: "Watched 24 % — below this plant's floor, so the figures are unknown." - Every list states its total, including a list of things the agent chose not to draw.
The network visualisation¶
Scott: "With lots of nodes, cool network visualizations could come in very handy."
What it would show. This plant is already a graph and the product already
holds every edge: equipment in an ISA-95 hierarchy; materials joined by a bill
of material; routings joining operations to work centres; lots consumed into
lots; units packed into pallets; tags bound to machines; orders moving across
stations. GET /serialization/unit/{serial}/trace and where_used are a
genealogy graph with no picture, and /dashboard/trace is the screen that
already asks the question in a table.
What makes one honest. Four rules, all of them existing rules applied to a picture:
- An edge nobody observed is not an edge. A material that should be consumed into a lot is not drawn as consumed. A machine with no connection fact is drawn as unknown, never as connected — that is 0030, and its own line is "inferring a lost connection from silence invents an outage".
- Unknown is drawn as unknown, not as absence and not as zero.
STYLE.mdrule 8 on a graph means an unknown edge has a rendering of its own, and a node with no data is visibly a node with no data. - The graph states its total. "Showing 60 of 340 nodes; 12 edges not drawn." A layout that silently drops what will not fit is the same failure as a list that reads complete — rule 4, and the failure that produced "no workspace holds such a setting" on a truncated list in September.
- Coverage on the edges that carry a number. If an edge is labelled with
seconds or units, it carries how much of the window was watched, or it
carries nothing. Watched is the coverage ledger's own
observed_secondsfor that machine over that window, not the window's length: on 2026-09-29 the first graph drawn for a person claimed 604,800 watched seconds where the ledger said 37,303, because it took the window less the recorded disconnections and the plant had recorded none.
What it must not claim. It is a picture of what the records say, not a model of the plant. It cannot be asked to find a root cause, and a thicker line is not a stronger relationship — it is more seconds, or more units, or whatever the legend says. Rule 6 of the house rules applies with full force here, because a rendered graph is the most convincing wrong thing this product could draw: "a rendered chart can be completely convincing and completely wrong … look at it with real data before you call it done, then pin what you saw with a test."
How it is built, with no build step. STYLE.md rule 9 is "Plain HTML/JS/CSS,
hand-drawn SVG for charts, no bundler, no framework, no font or script fetched
from outside the box." Two sanctioned routes exist:
- hand-drawn SVG, in the style of
web/kit.js, whose own docstring is the brief: "Hand-drawn SVG, no library — a plant PC renders this for years without a toolchain. Nothing here computes a number: a chart draws what the API measured, and draws 'unknown' when it did not." - a vendored ES module, which is what
line3d.htmlalready does: three.js r185 committed underweb/vendor/, loaded by an import map, with a table giving each file's npm version, licence, SHA-256 and the date it was downloaded, and the standing terms — "these files are read, never built."
Recommendation: hand-drawn SVG, and one graph. The three.js precedent was accepted because WebGL cannot be hand-drawn; a node-link layout can, and a force-directed layout on 60 nodes is a hundred lines. The smallest version worth building is genealogy for one lot or one serial — the data is already there, the screen that wants it already exists, the honesty rules have obvious renderings (a consumed-from edge is a fact; an unknown origin is a visible unknown), and it answers a question a person actually has during a recall.
And not the harness's own graph, first. A picture of agents, budgets and proposals flowing to signatures is a diagram for this design page (§8 has it), not a screen. Three agents and a queue is a table.
7. Levels of investment and times¶
Scott: "ensuring the AI, in whatever multiple forms, are always working for it at their specified levels of investments and times."
Budgets¶
What exists: one environment variable, $10 a month, per box, over one pooled
usage file, plus three per-conversation numbers on Setup
([admin] agent_max_rounds 12, agent_session_ttl_seconds 1800,
agent_result_limit 6000) that a conversation reads once and keeps.
Recommendation, four changes and no new machinery:
- The plant's AI budget is a plant setting, not an environment variable, so it is on a Configuration page, in the pack, in the audit trail when it changes, and per plant rather than per box.
- Each agent kind has a monthly share of it, and the shares are checked
against the plant's total when they are saved — the way
writealready refuses raisingcpk_marginalovercpk_capable, judging the difference between the table before and after rather than each number alone. - Spend is already recorded per turn (
usdon everyai_turnsrow), so spend per agent needs no new column — only the agent's name on the row. - The estimate says it is an estimate.
BUDGET.md's line stays: "The Console is the bill."
Times¶
Recommendation: cadence is a plant setting per agent kind, and "on demand" is one of its values. Concretely: the analysis agent is on demand; the CI crew has an interval with a delta check in front of it; a per-person agent has neither, because a person asking is its cadence.
Where these rows live: a fifth Configuration domain, not Setup¶
§2a of configuration assistance is "a domain gets one
Configuration entry in its nav group; every configurable section of that domain
lives inside it" — and "a domain does not have to invent a nav group", because
Supply chain's entry sits in Orders.
AI already has its own nav group. web/common.js:78-90 puts it there
deliberately: "AI is its own workspace beside Setup, not a chip per brain: one
entry with Conversations, Status and Settings inside it, the same shape §2a gives
a Configuration workspace."
So: a fifth ConfigDomain, ai, whose Configuration entry sits in the AI nav
group. Not Setup. Setup already carries 20 sections and 45 keys, and putting
the agents' budgets and cadences there would separate them from the tab that
shows what they did — the same mistake §2a was written to prevent, in the other
direction. The four Setup sections that already hold the assistant's own numbers
— agent_budget, assistant_context, ai_rollup_stale and ai_trace_days —
move with them, or stay and are linked from the new domain, which is a judgement
Scott can make in one sentence.
What happens at the cap¶
- The agent stops, says which cap, and the plant carries on.
available()already returns the sentence — "this month's budget is spent ($8.41 of $10.00)" — and the panel already falls back to the local model. A scheduled agent that is out of budget records a pass it could not make, which is unknown, not clean. - Nothing is left half-done, because nothing a functional agent does is a multi-step commit: it drafts, and a draft that was never finished is a draft nobody signs.
What runs in shadow mode¶
The cloud brain is refused in shadow mode, in two places — available() checks
it before anything else, and _call_model calls shadow.guard() before it
imports the SDK — and the register's reason is the one that matters here: "It
changes nothing in the plant, but it carries this plant's numbers off the box,
and a plant lending us its data to watch did not agree to that." The local
model, the drafting path and the entire MCP surface stay allowed, and the trace
writes rows like any other plant.
So, per agent kind:
| Kind | In shadow mode | Why |
|---|---|---|
| Per-person agent | the local model answers; the tab says the cloud brain is off and why | already how it behaves |
| Analysis agent | off, and says so | deep exploration is what the cloud brain is for; a degraded version that quietly answered worse would be the failure 0032 warns about |
| CI crew | local model only, or off, and the plant chooses | its inputs are configuration and observation class, both refused off-box in shadow. A local-model crawl over the plant's own refusals is genuinely useful and carries nothing away |
And every kind declares its class, so fsmes shadow prints what would and would
not leave in the vocabulary a plant asks the question in — which is 0032's own
reason for having classes at all.
Against the crew's throttle, line and meter¶
The crew's arithmetic ports almost unchanged and is worth copying: line =
share × elapsed ÷ window, with a margin, and surplus is what an agent may spend
now. One difference in the plant's favour: the crew's meter is scraped out of a
terminal statusline; the plant's is a real number it wrote itself. One
difference against: the crew has a human who changes the throttle three times in
nineteen days, and a plant has an administrator who will set it once. So the
default matters more than the knob, and the default should be the one that costs
nothing surprising: the CI crew off, the analysis agent on demand, and the
per-person agent as it is today.
8. The AI tab as the harness's dashboard¶
What the tab is today¶
Three tabs behind audit.read, added 2026-09-26 (#112):
- Conversations — one row per conversation with who, when, turns, what was proposed and what became of each, how many turns failed, and what it cost; open one and get the turns in order, with the audit row a confirmed proposal wrote. Complete, and the best thing on it.
- Status — which brains are on and why the rest are off. Honest but
machine-shaped rather than plant-shaped: its rows come from
services/ai_status.py, which reads files under~/.local/share/fsmes/— the results store, the rollup notes, the design database, the usage file — because one GPU serves every plant on the box. This page found that the cloud brain's spend was a note inside a local-AI payload, so the whole tab blanked whenMES_LOCAL_AI=0, cloud budget included; PR #122 closed that on 2026-09-27 and the tab now callsGET /assist/agent/statusfor spend, cap and tokens whatever the local layer is doing. What remains true is the shape: the rows are still the box's rather than the plant's, and §7 is where that is fixed. - Settings — exactly one editable number,
ai_trace_days, plus links to three Setup sections.
What it gains, and where each piece already is¶
| Panel | What it shows | Status |
|---|---|---|
| Agents | every agent on this plant — the functional kinds and, counted, the people who have one — with role, tool count, budget, spend this month, last run, next run, and state | new. Needs a plant-scoped roster; ai_status.consumers() is the shape ({name, trigger, output, state, last, note}) and its five states are the right ones: ok, idle, stale, down and unknown |
| Proposals | what is waiting for a signature, whose it is, how long it has waited, and the diff behind each | mostly exists. GET /dashboard/pending-approvals is per-caller, counted, with its total, and /pending-approvals/{kind}/{code}/{revision} already returns the diff, how much recorded history carries the code, who drafted it and on whose behalf, the one-click revert, and a generated walk to the signature. Two of the five kinds are wired |
| The trace | every conversation, every turn | exists |
| Faithfulness | whether the assistant does what people ask, per role | exists as a document, not a screen. fsmes assist eval scripted runs on every pull request; the first live admin-role run scored 20 of 39 on 2026-09-26 and the fixture gaps behind most of those nineteen were fixed in #115. docs/ai/ASSIST-EVAL.md holds it. A number on the tab needs the run to be a record the plant keeps, not a file in the repository |
| Coverage | what share of the actions a person can take the assistant can do for them | exists as a generated page (docs/operate/assistant-coverage.md: 66 screen actions, 51 proposable as admin, 42 with a walk) |
| Network | the plant as a graph | new, and §6 says start with genealogy |
| My agent | this person's own conversations and spend, behind plant.read |
new, and §3 says why it cannot be the audit view |
The diagram¶
flowchart TD
subgraph people [People]
OP[operator]
SUP[supervisor]
ADM[administrator]
end
subgraph harness [The harness, inside the plant]
MINE[each person's agent<br/>their capabilities · on demand<br/>budget: a share of the plant's]
ANA[analysis agent<br/>every read tool, no write tool<br/>on demand · deep · renders in the tab]
CI[continuous improvement<br/>woken by a delta, not a clock<br/>cadence + monthly budget]
end
subgraph records [What the plant already keeps]
TRACE[(ai_turns<br/>the trace)]
AUDIT[(audit_log<br/>actor · on_behalf_of)]
LEDGER[(coverage ledger<br/>+ the four analyses)]
end
subgraph queue [Waiting for a signature]
Q[pending-approvals<br/>counted, with its total<br/>+ the diff and the walk]
end
OP --> MINE
SUP --> MINE
ADM --> MINE
SUP --> ANA
ADM --> ANA
LEDGER --> ANA
TRACE --> CI
AUDIT --> CI
LEDGER --> CI
MINE -->|proposes| Q
ANA -->|proposes nothing<br/>it holds no write tool| TRACE
CI -->|drafts: reason · severity · trigger<br/>adjustment · instruction| Q
CI -.->|the fifteen screen numbers only,<br/>if the plant granted screens.define| AUDIT
Q -->|*.approve| SUP
Q -->|*.approve| ADM
SUP -->|signs| AUDIT
ADM -->|signs| AUDIT
MINE --> TRACE
ANA --> TRACE
CI --> TRACE
Read it as one claim: everything an agent produces goes to the queue or to the trace, and the only arrow that reaches the plant without passing a person is the dotted one, which is fifteen numbers and off by default.
What it must not become¶
Scott's own complaint about the field is the specification for what to refuse. So, plainly: the AI tab is not where the analytics go. It shows what the AI did, what it costs, what it is waiting on and what it is asking for. It shows at most one number per agent. The moment it carries a second chart nobody asked for, it has become the thing he is describing — and the precedent is on the record: 0023 refused a single fleet OEE because "an executive asks for it" is not a reason when the number would be a lie.
And the distinction M1 has to hold, because it puts charts on this tab. An exploration the analysis agent drew for the person who asked, beside their conversation, gone when they are done is not a dashboard panel; it is an answer. The rule is the one already in the registry — "its own page because these are questions you sit down with, not things you watch" — so an exploration lives where the question was asked, and a chart becomes part of the tab only by somebody deciding it should be, at which point it joins the system like any other screen (§6). If a chart is on this tab for everybody and nobody asked it a question, it has crossed the line.
9. The build plan¶
This section asked which of three candidates to start with and recommended the continuous-improvement crew. Answer 4 replaced that: "Analysis first with very good graphing capabilities." Answer 5 set the release discipline — warn-only first. Answer 7 put a simple memory in. Answer 2 put code changes in. Answer 10 said what the whole thing is for.
So this is now four milestones in the order those answers imply, each sized as handoffs an executor can take one at a time. Nothing here is built, and this plan is not a schedule. Each milestone states what it proves, and — more usefully — what it still does not.
Two prerequisites from this page's own findings are already done and are not
milestones: the nine ungated write tools were closed by #119, and the AI
tab's Status blanking without a local model was closed by #122, both on
2026-09-27. The prerequisite that remains is inside M1: a functional agent gets
an account and a role of its own rather than AGENT's, because AGENT holds
process.define and quality.define and the twenty-four settings behind them
(§5).
M1 — the analysis agent, with graphing (answer 4)¶
What it is. An agent, on demand from the AI tab, that holds every read tool and no write tool, can reach the plant's own four analyses, and draws what it finds. It writes nothing anywhere, so the worst case is a wrong picture rather than a wrong plant.
Its read tools. The four analyses the product computes and no agent can reach
(oee_breakdown, state_timeline, downtime_pareto, tag_trend), plus what is
already reachable: the trace (ai_turns), the audit trail, the coverage ledger,
and the eval results. The four must return the payload the screens get —
coverage, coverage_note, the full ledger, unknown_seconds, labelled_by,
every total — so the agent inherits the honesty rather than being told about
it.
The chart contract. This is the "very good graphing" half of the answer, and it is where the milestone can go wrong, because a rendered chart is the most convincing wrong thing this product can draw (house rule 6). Six rules, all of them existing rules applied to a picture:
- A chart draws what the API measured. It computes nothing —
kit.js's own line, and 0031 and 0033 behind it. - Every figure carries its coverage, because the envelope it came from does. A station whose figures are withheld is drawn as withheld, with its ledger, never omitted and never averaged away.
- Unknown is drawn as unknown — a rendering of its own, not a gap, not a
zero, not a smooth line through it (
STYLE.mdrule 8, 0030). - Honest axes. A y-axis that does not start at zero says so on itself; a truncated window says what it truncated; a rate has its denominator in the label. A chart whose shape depends on a choice states the choice.
- Every chart states its total — "showing 60 of 340" — including the
things the agent chose not to draw (
STYLE.mdrule 4). - The palette, and all four themes.
STYLE.mdrules 2 and 3: colour comes from the palette and nowhere else, and control-room, daylight, high-contrast and night-shift are all first-class. A chart that is only legible in one theme is not finished.
And the charting library, which is the decision inside this milestone.
0005 is "no build step", and
STYLE.md rule 9 is "plain HTML/JS/CSS, hand-drawn SVG for charts, no bundler,
no framework, no font or script fetched from outside the box." So "no build
step" means, concretely: no npm, no bundler, nothing fetched at run time, and
any library at all must be vendored — committed under web/vendor/ with its
npm version, licence, SHA-256 and the date it was downloaded, under the standing
terms "these files are read, never built", which is exactly what three.js
already has there. Recommendation: hand-drawn SVG through kit.js, extended
rather than duplicated, because kit.js exists precisely so that "the Floor
tile and the machine page can never state coverage differently" and a second
chart engine would make that drift permanent. A vendored library is the fallback
if a chart the agent needs genuinely cannot be hand-drawn — the three.js
precedent was accepted because WebGL cannot be — and it is a decision record when
it happens, not a quiet addition.
Where it renders. The AI tab (§8), beside the conversation, for the person who
asked. Never promoted to a default screen without a person's decision (§6), and
when one is, it joins the system: same header and nav, the palette, four themes,
a data-assist anchor, and ui-check crawling it from the day it exists.
In shadow mode: off, and saying so (answer 9).
| Handoff | One line |
|---|---|
analysis-mcp-tools |
The four analyses as read tools, returning the screens' own envelope with coverage, the ledger and every total |
analysis-agent-kind |
The kind: its own account and role, no write tool, asserted by a test that its catalogue contains nothing carrying dry_run; off in shadow mode and saying which brain and why |
analysis-chart-kit |
kit.js extended to the chart shapes an exploration needs, against the six rules above, in four themes, with no build step and nothing fetched from outside the box |
analysis-in-the-ai-tab |
Where an exploration renders, what it costs, and what it does when the ledger withheld the answer |
What M1 proves. An account and role of its own, a tool set defined by what it may not call, a per-conversation budget, and a place on the tab. What it does not. Anything about a schedule, a monthly budget, a proposal, a signature, or a line of the product's own code.
M2 — the [ai] domain, the person's own agent, and memory (answers 6, 7, 8)¶
What it is. The plumbing every later milestone needs, plus the two things Scott asked for by name: a my agent view, and an agent that stops forgetting.
The [ai] Configuration domain (answer 6): a fifth ConfigDomain whose
Configuration entry sits in the AI nav group, not Setup — §7 says why, and
§2a of configuration assistance is the rule it follows.
It holds each kind's budget and cadence, the plant's AI budget as a plant setting
rather than an environment variable, and — from §5 — the automation line
itself, in a section gated on *.approve rather than one that takes effect when
saved.
The my agent view (answer 8): behind plant.read, not audit.read, showing
my conversations, what my agent spent, what it proposed for me and what became of
each. And the panel says out loud, where a person types, that nothing they type
is private — it is in ai_turns and every holder of audit.read can read it.
That sentence is the deliverable, not a footnote to it.
Retained memory (answer 7), at the size §3 fixes: the durable transcript, plus a short remembered-facts list the person can read and delete on that view, with its maximum and its total stated. No skill set, no inferred profile, no shared memory, and never a plant number.
Per-person budgets: shares of the plant's budget, checked against the total
when they are saved the way cpk_marginal is already checked against
cpk_capable. Spend per agent needs no new column — usd is on every ai_turns
row — only the agent's name on the row.
| Handoff | One line |
|---|---|
ai-config-domain |
The fifth Configuration domain, its entry in the AI nav group, the plant's AI budget as a setting, and the automation line in a section a person signs |
my-agent-view |
The person's own view behind plant.read, and the panel saying out loud that a supervisor can read what you type |
durable-transcript |
A transcript that replays: the tool_use/tool_result blocks ai_turns deliberately does not keep, its own horizon, the person's to read |
remembered-facts |
The short list, written only from the person's own turns, readable and deletable on the my agent view, with its maximum and its total |
per-agent-budgets |
Each kind's monthly share, checked against the plant's total when saved; the agent's name on the trace row |
What M2 proves. A budget that is the plant's, a cadence field with somewhere to live, and the tab as a person's own place rather than an auditor's. What it does not. Anything on a schedule, and anything that proposes.
M3 — the improvement crew, warn-only (answer 5)¶
What it is. A scheduled, budgeted crawl over the records the plant already keeps, whose entire output is proposals, in the flows that already exist. Nothing applies itself in this milestone — that is what warn-only means, and it runs that way for a release so Scott can read a month of what it would have done before any of it is switched on.
Its trigger. A no-model delta first: has anything the crawl is about changed — new unlabelled seconds, a new withheld station, a new open non-conformance, a draft that has crossed a waiting threshold? If nothing changed, no model runs, and the pass is recorded as a pass that found nothing. The agent advances its own watermark, so a wake that did nothing is visible rather than silent. This is the crew's best idea and its cheapest: 131 script passes a week, 22 with news, a handful of model wakes, against the 48 model passes a day it replaced.
Its cadence. The one precedent in the product is the hourly tag-retention sweep
in api/app.py's lifespan, whose own comment states the pattern: "Retention
runs in the API process because it is the one long-lived process every deployment
has."
Where its work lands. The five draft-and-sign pairs and the approvals queue,
never a third mechanism. Two of the five kinds are wired into review.KINDS
today; wiring the other three — trigger, instruction, adjustment — is a
prerequisite and is worth doing whether or not any of this is built, because
"a lifecycle whose last step is that somebody remembers to look" is the failure
0035 was aimed at.
A pass that did not happen is unknown, never clean. ai_status's rule already:
"a night the loop did not run is unknown in the morning, never a quiet
'clean'."
| Handoff | One line |
|---|---|
review-kinds-unwired |
The three unwired review.KINDS entries — trigger, instruction, adjustment — so a drafted one reaches an approver |
crew-cadence-and-delta |
The cadence in the API lifespan, the no-model delta in front of it, the watermark the pass advances itself, and a pass that found nothing recorded as one |
crew-proposals-warn-only |
The crawl's findings as drafts in the flows that exist; nothing applies itself; the roster row on the AI tab, honest about a pass that did not run |
screens-define-capability |
screens.define, off agent by default, granted and revoked through the settings tools on a signature (answers 1 and 3) |
What M3 proves. All six fields of §4's row, against a real plant, with the three hardest — a cadence inside a long-lived process, a budget that is a plant setting, and a roster honest about a pass that did not happen. What it does not. Touch a line of the product's code.
M4 — the harness as the engine (answer 10, and answer 2)¶
What it is. The milestone Scott's tenth answer describes: the crew's roles running inside the product, against the product's own repository, so that "if I wanted to integrate with SAP or fix any ERPNext connection, then it should be this harness that allows be to fix or build it."
What it needs, and §2a is the honest version. A checkout somewhere that is not the plant; the CI the repository already has; and a release as the only path back to a running plant. No code is pushed into a running plant, ever, and an air-gapped plant participates by exporting a finding and receiving a release.
What it produces. Tier (b) of §5 first, because it is the narrowest and it is the one Scott asked for: a UI change as a branch, a pull request, a test that fails before it, and a person's merge. Then, on the same machinery, tier (c) — a connector fix, an ERPNext bug, an SAP adapter — which is the same shape and never automates.
And the automation question for tier (b), which this milestone must answer
rather than assume. After the warn-only release: the declared file set (§5 names
the inclusions, the six named exclusions and the three diff-shaped ones), and the
ui-check baseline problem — ui-check --accept makes the current look the
accepted look, so an agent that may run it can erase the check that would have
caught it. Either a person re-accepts the baselines as part of the merge, or
tier (b) never automates. Both are acceptable outcomes; pretending the question
does not exist is not.
| Handoff | One line |
|---|---|
harness-checkout-and-ci |
Where the checkout lives, a branch per proposal, and a pull-request body that carries the finding, its evidence and what it did not do |
ui-code-proposals |
Tier (b): the harness opens a draft pull request against web/, with a browser test that fails before the change and passes after it |
harness-roles-in-product |
The crew's roles as agent kinds inside the product — a board, a handoff, an executor that stops at a pull request, a triage on a delta |
tier-b-automation-line |
After the warn-only release: the declared file set as a setting, and the ui-check baseline question answered one way or the other |
What M4 proves. Answer 10, or disproves it — see the next section. What it does not. Make the product self-evolve. §5 says what would have to be true, tier by tier, for a change to land without a person, and tier (c) never does.
The case for building none of it yet — kept, and answered¶
This page made the case against itself, because the project's own strategy says so in three places, and the case is not withdrawn by being answered.
- Growth engine (2026-09-05) ranks the agent story lever five of six by leverage per evening and names the bottleneck elsewhere: "the single most important growth work is the first-real-machine experience … Not more features."
- Why open source, and what winning looks like (2026-09-05) puts "agents operate it in someone else's plant" fifth of five on what winning is, and says what winning is not: "ship the 20 % of each module that covers 80 % of a small-to-mid plant."
- Costs and sustainability (2026-09-05): "The scarce resource is not money; it is evenings." And "the bus factor is one." A harness with cadences and a budget is, by construction, a thing that produces work on a schedule — and a proposals queue only reduces bus factor if a second person can sign.
Two external facts sharpen it. Nobody has asked for this: the fitness reports for the weeks ending 2026-09-17 and 2026-09-25 both record issues, PRs or discussions from someone other than the maintainer: 0, while the one thing that did earn forks and stars in the researched field earned them by answering a question in public where the people asking it were already standing. And the field's most disciplined comparable project refuses to be resident at all: "there is deliberately no run-forever mode: a resident process on an OT network needs change management, a laptop running for a week does not."
Scott's answer, 2026-09-28, in his own words:
"I feel this should be a tool for driving product improvement and thus should accelerate solutions for all other areas. That's the goal for this harness. To make this product self evolve on its own. In that regard, this should become the bottle neck for driving all other improvements on that list. For example, if I wanted to integrate with SAP or fix any ERPNext connection, then it should be this harness that allows be to fix or build it."
Read against the three strategy pages, that is not a disagreement about the ranking. It is a claim that the ranking is the wrong shape: the harness is not the fifth item competing with the other four for evenings, it is the thing that is supposed to change the price of the other four. The scarce resource is still evenings, and the answer to a scarce resource is not to spend it on the fifth item — it is to spend it on the thing that makes each evening produce more than one change.
Two of the three objections survive that answer intact, and are not answered here. Nobody outside this machine has asked for it, and the field's most disciplined comparable project refuses to run resident on an OT network — which §2a takes seriously by putting the checkout, the CI and the compiler anywhere but the plant. The third objection, the ranking, is overruled with a reason.
And the reason is testable, so this page says what would show it wrong. After M3's warn-only release and M4's first tier (b) proposals, three numbers exist that do not exist today:
- How many of the harness's proposals a person signed, against how many were declined and how many were ignored. 0038 already says to revisit on exactly this number and not before.
- How many evenings a merged harness proposal cost, end to end, against what the same change would have cost by hand. If a proposal costs an evening to review, the harness has moved the work rather than reduced it.
- Whether a second person could sign. The bus factor objection is answered only if somebody other than the maintainer ever merges one.
If, after a month, the first number is small, the second is not better than one-for-one, and the third is zero, then the ranking was right and this is lever five of six. That is the honest test, it is a month away, and it is the reason M3 is warn-only and M4 is last.
Where this is, and is not, ground nobody holds¶
Honest, with the caveats the research itself carries.
- Table stakes, already shipped by somebody: an MCP server over your product's data; governed writes with dry-run defaults and MOC gating at the device layer; blanket human approval of agent-sourced changes enforced in code; per-consumer token budgets at the gateway layer; and observed coverage printed beside a KPI — which the 2026-09-25 sweep records as a correction to this project's own earlier novelty claim.
- Empty ground, per the same reports: no MCP server anywhere exposes ISA-95 Level 3 objects — orders, routings, operations, genealogy. Everything agentic in manufacturing is at the tag layer or in an ERP with no machine underneath, and "nobody has both ends." A per-person agent holding Level 3 tools is unoccupied ground.
- The tiered boundary, with the caveat stated: the 2026-09-25 report's own words are that this project's "tiered draft → validated → signed-off → live loop with cheap undo (decision 0035) is as far as I can tell unmatched in any shipped product, though I cannot verify the insides of Opcenter or Critical Manufacturing and say so." Carry that caveat. The nearest commercial thing is a "Modeling Agent" for MES configuration guidance announced in April 2026, whose write-up names the risk — "a false sense of understanding" — and describes no guardrail.
- Where the research is silent, and this page must not claim: the word "faithfulness" appears nowhere in either landscape sweep or anywhere in the project's own research vault. Neither pass looked. So "nobody ships a per-role faithfulness score" is not a finding and must not be written as one. What is supportable is narrower and still worth something: the research found no product keeping a record of model turns beside its business audit trail, and none describing a scheduled, budgeted, multi-role agent crew inside an MES.
10. The ten questions, and what each answer changed¶
These were the questions this page ended with on 2026-09-27. Scott answered all ten on 2026-09-28 at about 06:20; his words are quoted in full in Decided and are not paraphrased here. This table is the index: the question, the answer in a word, and where the page changed.
| # | The question | Answer | Where it landed |
|---|---|---|---|
| 1 | Is the five-clause line right? | yes, with a rider — make it configurable, with agentic access to change it | §5, The line itself is a setting: the line lives in the [ai] domain, in a section gated on *.approve, and the agent reads and proposes changes to it through the settings tools |
| 2 | "UI updates 100 % automated" turned out to be fifteen numbers — is that what you meant? | no — something bigger: a person should be able to change the code | §5 rewritten as three tiers; tier (b) is the harness editing web/, proving it with the browser tier, and landing it as a pull request a person merges. Amendment A |
| 3 | A new capability screens.define, off by default? |
yes, and configurable with an MCP tool | §5, The mechanism, per tier, and the screens-define-capability handoff in M3: granted and revoked through the same settings tools, on a signature |
| 4 | The CI crew first, or the analysis agent alone? | analysis first, with very good graphing | §9 is now M1–M4, and M1 is the analysis agent with the six-rule chart contract and the no-build-step question answered |
| 5 | Warn-only for a release first? | yes | §5, tier (a) and tier (b) both; M3 is the warn-only milestone and M4's automation waits on its release |
| 6 | A fifth ai Configuration domain rather than Setup? |
yes | §7 unchanged, and it is the ai-config-domain handoff in M2 |
| 7 | Per-person memory: none at first? | no — retain memory, in a simple proof of concept | §3, piece 3, rewritten: the durable transcript plus a short remembered-facts list the person can read and delete, with its total. No skill set, no inferred profile, never a plant number |
| 8 | A "my agent" view, and say out loud that it is not private? | yes | §3 and §8, and the my-agent-view handoff in M2 |
| 9 | The analysis agent in shadow mode: off, or worse answers? | off | §7's shadow table, unchanged, and M1 builds it that way |
| 10 | Is this worth an evening a week, against the first-real-machine work? | it is the engine, not the fifth item | §2a — the crew's roles inside the product, what runs where, and what an air-gapped plant can do; §9's case-against, kept and answered, with three numbers that would show the answer wrong. Amendment B |
What is still open, and is Scott's to say when it arrives. Three things this revision names as decisions rather than deciding:
- Whether tier (b) ever automates at all, which turns on the
ui-checkbaseline question in §5 — an agent that may runui-check --acceptcan erase the check that would have caught its own change. M4 answers it; either answer is honest. - Whether a chart the analysis agent needs justifies a vendored library.
M1 recommends hand-drawn SVG through
kit.js; a vendored library would be a decision record, not a quiet addition. - Whether the four Setup sections that already hold the assistant's numbers
move into the
[ai]domain or stay and are linked (§7). One sentence.
What this page does not do¶
- It does not build anything. The design is accepted; §9's four milestones are a plan, not a schedule, and no handoff named in it has been opened.
- It does not claim the harness will make the product self-evolve. That is the stated goal, in Scott's words. What the page says is what would have to be true, tier by tier, for a change to land without a person — and that tier (c), which is most of the product, never does.
- It does not put a compiler on a plant. §2a says where the checkout, the CI and the release live, and that a code change reaches a plant only when the plant upgrades.
- It does not reopen the questions in kernel and modules — whether an agent kind is a module is that page's question, and the six-field row in §4 is deliberately written so that either answer works.
- It does not design the fully embodied agent Scott described in answer 7. §3 defines memory at proof-of-concept size and says so; a skill set is a later design.
- It does not choose the plant's numbers. Every budget, cadence and floor in it is a setting with a default, and the default is the one that costs nothing surprising.
- It does not claim to be first at anything the research could not verify. §9 says where the ground is empty, where it is contested, and where nobody looked.