Skip to content

One zip any agent can read: install, a plant shaped like yours, shadow mode, and an honest page on migration - #173

Merged
kalwei merged 6 commits into
mainfrom
feat/fsmes-assistant
Oct 15, 2026
Merged

kalwei merged 6 commits into
mainfrom
feat/fsmes-assistant

Conversation

@kalwei

@kalwei kalwei commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What this is

One download, fsmes-assistant.zip, that a person hands to whatever AI
assistant they already use — Claude, ChatGPT, Copilot, a local model, in a chat
window in the cloud or on the box — and says "help me install
FactorySemantics MES"
. It is plain markdown and nothing else. The agent needs
no access to the machine: the person runs every command and pastes back what it
printed.

Four stages, one at a time:

  1. Install — Windows words first, because that is where the first person
    was — until fsmes demo bottling answers on 127.0.0.1:8010.
  2. A plant shaped like yours — the existing plant-from-your-mes folder,
    nested inside this one as stage 2, ending at fsmes demo from-shape.
  3. Shadow mode beside your MES — docs/operate/first-plant.md turned into
    an agent's script, ending at the shadow scorecard.
  4. Migration — what you have when the scorecard matches, what is not
    built
    , and how to tell us. It stops there. No commands on that page.

It comes out of an installation with no download and no internet:

fsmes skills unpack assistant ./assistant

The boring piece first: the folder cannot drift from the CLI

tests/test_every_command_the_assistant_folder_tells_you_to_run_exists.py
pulls every fsmes … command out of the markdown — inline code spans and
shell-ish fenced blocks, \ continuations folded, py -m fsmes and
.venv\Scripts\fsmes spellings included — and resolves each one against the
typer app: sub-command by sub-command, then the options, then the number of
arguments. 59 commands across the pages today (23 of them in 1-install.md), and 4-migration.md deliberately carries none.

Proof it goes red, from breaking one command in 1-install.md:

$ sed -i 's/fsmes demo bottling/fsmes demmo bottling/' skills/assistant/1-install.md
$ pytest 'tests/test_every_command_the_assistant_folder_tells_you_to_run_exists.py::\
test_every_fsmes_command_on_this_page_resolves_against_the_cli[1-install.md]'

E  AssertionError: 1-install.md: `fsmes demmo bottling status` — `fsmes` has no
E  sub-command 'demmo'. It has: add-user, agent-eval, ai, ai-status, assist,
E  autoloop, backup, config-audit, db-status, demo, design-backlog, design-log…

$ sed -i 's/--buffer 20/--bufer 20/' skills/assistant/2-your-plant.md

E  AssertionError: 2-your-plant.md: `fsmes shape plant plant-shape.toml --out
E  ./acme --bufer 20` — `fsmes shape plant` has no option '--bufer'. It has:
E  --buffer, --force, --help, --hours, --name, --out, --seed

What it cannot catch is written down in the test itself
(test_what_this_check_cannot_catch_is_written_down): an argument's value is
not a CLI name, so fsmes demo bottling stattus is well-formed as far as click
is concerned. A page quoting computer output must label its fence text, or the
check reads the output as input — also written down, and
test_output_quoted_on_a_page_is_not_read_as_a_command holds it.

One copy in git, nested at build time

Stage 2 is plant-from-your-mes, so that folder has to be inside the one a
person downloads. It is not a second copy in git and it is not a symlink — a
zipped symlink arrives on Windows as a text file with a path in it. The
relationship is one line of registry, fsmes.skills.INCLUDES, which
build_zip and unpack both read, so the nesting is made from the single copy
in skills/plant-from-your-mes/. Two tests hold that: the folder has no
skills/assistant/plant-from-your-mes in it, and nothing anywhere in it is a
symlink.

The two names. The skill is called assistant — that is what a person
types after fsmes skills unpack. The download and the top-level folder inside
the zip are called fsmes-assistant, because a zip lands in a folder beside
other people's downloads and has to say whose it is. One dict,
fsmes.skills.RELEASED, says which skills a release attaches and under what
name, and the release workflow loops that instead of SKILLS.
plant-from-your-mes.zip is retired from the upload list — it is inside this
one now; two downloads of the same files would be a question about which to
take. The skill itself stays in SKILLS: still checked, still unpackable by
name, and fsmes shape plant's hint text still names it.

Checked

  • .github/scripts/assistant_folder_from_the_wheel.py, a first step in the
    existing wheel-plant-from-shape job so no new Windows leg is paid for: a
    fresh venv with the built wheel, the network off, fsmes skills list names
    assistant, fsmes skills unpack assistant ./x writes 48 files with
    plant-from-your-mes/ among them, every one byte-for-byte the checkout's,
    no symlink anywhere. Ran green here on Linux; Windows is this PR's job.
  • fsmes skills-zip --check now covers every skill and each skill's includes;
    CI runs it bare.
  • The zip's contents, the page lengths (each stage page under 200 lines, the
    folder outside plant-from-your-mes/ 824 lines), and the page-one rules
    being the first thing in SKILL.md all have tests.
  • ruff check . clean; the default pytest tier green.

Honest caveats

  • Nobody has run shadow mode on a real plant. first-plant.md is written
    and untried, and stage 3 says so in its own footer rather than only here.
  • What the agent will get wrong. It will be confident about the stage it is
    reading and vague about the rest; it cannot see the person's screen, so a
    command that half-worked reads to it as a command that worked unless the
    person pastes the whole output; it will not know which of the five rules it
    is breaking if its own system prompt tells it to be helpful by running
    things. The rules are first in the file for that reason, and 0-how-to-help.md
    tells it to name the page it is reading from when it is unsure.
  • What the folder cannot know. Anything about their plant: their OPC
    security policy, whether their IT department will trust a certificate, what
    their MES calls its tables, whether their 0.3.0 install is the one the page
    describes. Every "what the output means" section is the three or four things
    that actually went wrong for the one person who has done this, plus what the
    code prints today.
  • Stage 4 says no. Asked "can I switch my old MES off now?", the folder's
    answer is no, with the list of what is missing — cutover, history migration,
    PLC write-back beyond the recommendation queue, confirmations to an ERP in
    charge, and the plain fact that none of this has run in production.
  • Not done here, and the handoff says it is the steward's: the live run with a
    fresh assistant, the zip uploaded, somebody playing the first user on
    Windows. That is the real test of a folder like this and it has not happened
    yet.

No release in this PR. CHANGELOG.md ## [Unreleased] carries the line.

After the steward's live run

The steward ran this before merge the way it is meant to be used: a fresh
no-tools assistant, the seven pages pasted in as the zip, him playing the first
user on Windows 11. It passed — one command per turn, the paste asked for, the
production data pasted "in case it helps" refused, taskkill for the half-dead
plant, and a plain no to "can we switch the old MES off". It found one real
gap, now fixed in 1-install.md and 0-how-to-help.md: the cheapest way out
of not recognized is py -m fsmes, which arrived in #172 and ships in 0.3.1,
so on 0.3.0 — the only release anybody has — it prints No module named fsmes.__main__. The page says that now, and the two places that used
fsmes --version to settle "is it installed at all" use
py -m pip show factorysemantics-mes, which answers with PATH broken or not.

The folder the next commits add tells a person what to type. A page that
says `fsmes demo bottlingg` wastes somebody's afternoon and makes the
project look careless, and markdown does not compile, so this is the
compiler: every `fsmes ...` in the folder, out of the inline code spans and
the shell-ish fenced blocks, resolved against the typer app sub-command by
sub-command, then its options, then how many arguments it will take.

Written first, before a word of the folder, because it is the piece that
keeps the folder true after everyone has forgotten it exists.

Two things it cannot do are written down in the test itself rather than
left for somebody to discover. An argument's *value* is not a CLI name, so
`fsmes demo bottling stattus` is well formed as far as click is concerned.
And a page that quotes what the computer printed must label the fence
`text`, or the check reads the output as input.

Typer vendors its own click core, so `isinstance(x, click.Group)` is False
for every group in this app and a resolver written that way silently
descends into nothing and passes everything. It duck-types instead: a
command with a dict of `commands` is a group, and a param whose `opts` all
start with a dash is an option.

Signed-off-by: scottkalw <scottkalwei@gmail.com>
Plain markdown a person hands to whatever AI assistant they already use —
in a chat window is fine, it needs no access to their machine — and says
"help me install FactorySemantics MES". They run every command and paste
back what it printed.

`SKILL.md` opens with five rules, above the stage table, because an agent
reads the top of a file and then starts being helpful: it runs nothing, it
never asks for data, nothing leaves the building because of the folder, it
says what is not built, and it works one stage at a time.

Then the four stages, each with the same four parts — what you need before
you start, the commands in order with what to paste back, what the output
means, and done with this stage when:

1. Install, Windows words first because that is where the first person
   was: the PATH warning pip prints verbatim, `py -m fsmes` as the way
   out, the half-dead plant and the 0.3.0 line that `stop` cannot fix it
   and Task Manager is the way.
2. A plant shaped like theirs, over two machines because the MES box
   often has no Python. Stage 2 is the `plant-from-your-mes` folder, and
   the page hands over to it rather than summarising it.
3. Shadow mode beside their own MES: what to ask IT and Engineering for,
   how to tell shadow mode is really on, orders in by file, and the
   scorecard. Its footer says `first-plant.md` is written and untried,
   because it is.
4. Migration, with no commands on it at all: what they have when the
   scorecard matches, what is built, what is not built, and three
   questions to send us. Asked whether they can switch the old MES off,
   the answer on that page is no.

Signed-off-by: scottkalw <scottkalwei@gmail.com>
…es in it

Stage 2 of the assistant *is* `plant-from-your-mes`, so that folder has to
be inside the one a person downloads. Two things it must not be: a second
copy of those forty-one files in git, which would drift, and a symlink,
which a zip delivers to Windows as a text file with a path in it.

So the relationship is a line of registry. `fsmes.skills.INCLUDES` says
the assistant carries that folder; `build_zip` and `unpack` both read it
and nest it from the one copy in `skills/plant-from-your-mes/`. A test
asserts `skills/assistant/plant-from-your-mes` does not exist and that
nothing in the folder is a link.

`RELEASED` is the other new line: which skills a release attaches, and
under what name. The skill is `assistant`, which is what a person types
after `fsmes skills unpack`; the download and the top folder inside the zip
are `fsmes-assistant`, because a zip lands beside other people's downloads
and has to say whose it is. The release workflow loops `RELEASED` instead
of `SKILLS`, so one zip is attached and `plant-from-your-mes.zip` is
retired — it is inside this one now, and two downloads of the same files
would be a question about which to take. The skill stays in `SKILLS`:
still checked, still unpackable by name, still what `fsmes shape plant`'s
hint names.

`fsmes skills-zip` with no name now checks every skill and each skill's
includes, which is what CI runs; `--out` refuses when it would stand for
more than one zip and says which names it means.

Signed-off-by: scottkalw <scottkalwei@gmail.com>
The first line of this handoff's definition of done, checked the way a
person checks it: a fresh virtual environment with the built wheel in it,
the network off afterwards, `fsmes skills list` naming `assistant` and
saying the folder came out of the installation rather than out of a
checkout lying above the working directory, then `fsmes skills unpack
assistant ./x` writing forty-eight files with `plant-from-your-mes/`
among them, every one byte-for-byte the checkout's, and no symlink
anywhere.

A first step in `wheel-plant-from-shape` rather than a job of its own: the
wheel is already built on both runners there, and a Windows round costs a
day. Cheap and first, so a missing `force-include` line fails in a minute
instead of after the half-hour shape run.

The venv and network-off helpers are imported from `plant_from_shape.py`
beside it rather than copied, for the same reason the skill folder has one
copy.

Signed-off-by: scottkalw <scottkalwei@gmail.com>
`plant-from-your-mes.zip` is not attached to a release any more, so the
page about that folder says where it is instead — inside
`fsmes-assistant.zip`, or out of an installation with `fsmes skills
unpack`, which needs no download at all. `first-plant.md` gains a short
paragraph saying that it is stage 3 of a folder you can hand to an
assistant, for people who would rather be walked through it than read an
afternoon's guide. The changelog's Unreleased section carries the line.

Signed-off-by: scottkalw <scottkalwei@gmail.com>
@github-actions

Copy link
Copy Markdown

Thanks for the first pull request. Checks: every commit signed off (git commit -s), ruff and pytest green, and the house-rules checklist in the template. The maintainer reviews within a few days.

The steward's live run (a fresh no-tools assistant, the pages pasted in,
him playing the first user on Windows) found the page's cheapest fix is
the one release nobody has yet. `py -m fsmes` arrived in #172 and ships in
0.3.1; on 0.3.0 it prints `No module named fsmes.__main__; 'fsmes' is a
package and cannot be directly executed`. The assistant in that run said
"that error isn't on the page's list" and moved on, which is the right
instinct and a worse afternoon than the page saying it.

So way 1 now carries the 0.3.0 line, the same way the `stop` section
already does, and the two places that reached for `py -m fsmes --version`
or `fsmes --version` to settle "is it installed at all" reach for
`py -m pip show factorysemantics-mes` instead: it answers on every
release, with PATH broken or not, and prints the version anyway.

Signed-off-by: scottkalw <scottkalwei@gmail.com>
@kalwei
kalwei merged commit 9699dd9 into main Oct 15, 2026
23 checks passed
@kalwei
kalwei deleted the feat/fsmes-assistant branch October 15, 2026 01:59
@kalwei kalwei mentioned this pull request Oct 15, 2026
kalwei added a commit that referenced this pull request Oct 15, 2026
The first-run fixes (#172), the shape file that starts a plant (#170), the
assistant folder (#173) and `fsmes doctor` (#174), cut as 0.3.0 was cut.

`src/fsmes/__init__.py` is the one place the version is written and hatchling
reads it, so the wheel, the sdist and the container tag follow. `CITATION.cff`
is the one a person types, and the release workflow refuses a tag whose
citation disagrees.

`## [Unreleased]` becomes `## [0.3.1] — 2026-10-15` with a fresh empty
Unreleased above it, and the compare link at the bottom moves to v0.3.1.

One entry is new rather than rolled: **#170**, the shape file that starts a
plant, landed the morning after 0.3.0 was tagged and wrote no changelog entry
at all. It is in this release whether or not anything says so, so it is
written down — with what it has never done (a real SQL Server) beside what it
does.

`docs/releases/0.3.1.md` is the body of the GitHub Release, composed by
`.github/scripts/release_notes.sh`: 119 lines, 5,744 of the 125,000 characters
the API allows, led by what a Windows user sees differently. `docs/operate/
upgrade.md` gets a `## 0.3.0 to 0.3.1` section that says there are no
migrations — `d4e8a1c93b70` is head on both versions — and the releases index
gets a row. The 0.3.0 row said "not yet tagged" beside it, three days after
the tag; it now says 2026-10-12.

The date stamp is the UTC date, which is what the Release body will carry:
`date -u` fills it in when the tag is pushed.

Signed-off-by: scottkalw <scottkalwei@gmail.com>
kalwei added a commit that referenced this pull request Oct 15, 2026
The release half of 0.3.1. It changes the version, the dates, the
changelog,
the release page, the upgrade page and the README's status line, and
nothing
else. **Stacked on #175** (the GIF and the assistant pages): until #175
merges,
this PR's diff shows its two commits too. Merge #175 first and this one
becomes
the eight files below.

## What is in 0.3.1

Four pull requests since 0.3.0: #170 (a plant shaped like your own, from
the
shape file), #172 (the first run on a Windows PC recovers itself), #173
(the
assistant folder) and #174 (`fsmes doctor`).

## The files

| File | What changes |
|---|---|
| `src/fsmes/__init__.py` | `0.3.0` → `0.3.1`. Hatchling reads this
line, so the wheel, the sdist and the container tag follow |
| `CITATION.cff` | `version: 0.3.1`, `date-released: "2026-10-15"`. The
release workflow refuses a tag whose citation disagrees |
| `CHANGELOG.md` | `## [Unreleased]` → `## [0.3.1] — 2026-10-15`, a
fresh empty Unreleased above it, the compare link moved to `v0.3.1`, and
**one new entry** (below) |
| `docs/releases/0.3.1.md` | new — the body of the GitHub Release |
| `docs/releases/index.md` | a row for 0.3.1; 0.3.0's row said "not yet
tagged" three days after its tag and now says 2026-10-12 |
| `docs/operate/upgrade.md` | a `## 0.3.0 to 0.3.1` section |
| `README.md` | the status line: 0.3.1, the date, and one sentence of
what is new |
| `mkdocs.yml` | 0.3.1 in the Releases nav |

## One changelog entry that is new rather than rolled

**#170 wrote no changelog entry at all.** It merged the morning after
0.3.0 was
tagged, so the `[Unreleased]` section covered #172, #173 and #174 and
said
nothing about the command that turns a shape file into a running plant —
which
is the largest thing in this release. It is written down now, in the
0.3.1
section, with what it has never done (a real SQL Server) beside what it
does.
Nothing else in the section was edited; the rest is the Unreleased text
as it
stood on `main`.

## The release page

`docs/releases/0.3.1.md` is **119 lines**, and
`.github/scripts/release_notes.sh 0.3.1` composes a body of **5,744 of
the
125,000 characters** the API allows. It is led by what a Windows user
sees
differently — `py -m fsmes`, a half-dead plant named process by process
by
`status`, `stop` with no flag — then the shape file that starts a plant,
then
the assistant folder, then `fsmes doctor`, then what 0.3.1 still cannot
do.

**No migrations.** `d4e8a1c93b70` is head on 0.3.0 and on 0.3.1, so the
upgrade
section is a `pipx upgrade` and a version check, and there is no Honesty
entry
to read first.

## How it is verified

Run against this branch, before the tag:

- `python -m build --wheel` →
**`factorysemantics_mes-0.3.1-py3-none-any.whl`,
  2,337,001 bytes (2.23 MB)**, under the 15 MB line.
- That wheel installed into an **empty venv** (Python 3.13, nothing else
in it):
  `fsmes --version` says `fsmes 0.3.1`.
- From that venv **with the network cut** (`unshare -rn`):
`fsmes skills unpack assistant ./assistant` writes the folder — 48
files,
  including `plant-from-your-mes/`.
- From that venv **on a home with no plant in it**: `fsmes doctor --out
  doctor.md` writes the file — 48 lines, 0 plants, `leak check: passed`.
- `python -m ruff check .` clean; `mkdocs build --strict` clean; the
nine tests
in `tests/test_the_release_body_github_will_accept.py` green, which is
what
asserts the body's length in the `build` job that a rehearsal also runs.
- The default suite is green apart from

`test_the_calibration_the_supervisor_records_is_on_the_gauges_own_register`,
which compares a UTC date against local `date.today()` and so fails on
this
machine between 19:00 and midnight CDT. It is red on `main` at this hour
too
  and green on CI, which runs in UTC.

## The date

`2026-10-15` is the UTC date, and `release_notes.sh` fills the published
body's
"Released:" line from `date -u` — so a tag pushed before 2026-10-15
23:59 UTC
makes the changelog, the index row, the README and the Release body
agree. A tag
after that needs the three stamps moved on; they are one `sed` each and
the
handoff's `## Result` says where.

## Not done here, on purpose

No tag, no release, no PyPI, no workflow run: the `workflow_dispatch`
rehearsal
of `release.yml` on this branch is the maintainer's to trigger.

Signed-off-by: scottkalw <scottkalwei@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant