Repository navigation
The first run on a Windows PC: a half-dead plant says what it is and gets out of it, py -m fsmes, and one page - #172
Merged
Merged
Conversation
The first person to install 0.3.0 on Windows could not run it. `py -m pip install` put `fsmes.exe` in a user Scripts folder that was not on PATH - the same reason pip itself had to be reached as `py -m pip` - and PowerShell answered "fsmes is not recognized as an internal or external command". There was no second way in: the package had no `__main__.py`, so `py -m fsmes` printed nothing either, and he could not tell "not installed" from "installed, not on PATH". `src/fsmes/__main__.py` calls `fsmes.cli.run`, the same function the console script points at, so the Windows console's encoding is retuned the same way and a misconfiguration still arrives as one sentence. A plant started that way must also be able to start its own processes, and every one of them is started by naming fsmes. `plant.fsmes_command()` prefers the console script, as before, and falls back to `[sys.executable, "-m", "fsmes"]` when there is none to find - otherwise `py -m fsmes demo bottling` would get as far as its first child and then say it cannot find the fsmes executable, on the machine where the whole point is that it is not findable. Signed-off-by: scottkalw <scottkalwei@gmail.com>
"Says 2 PID are alive, 7964 and 3940. But it can't talk to them." That is the whole of what the product could tell the first Windows user about a plant that had half died, because the only record of a plant's processes was the order of the pids in its pid file - and order is what a half-dead plant has lost. `plant.processes()` is now one table of what a plant is made of: the arguments that start each process, what it is for in words, and the log it writes. `start` starts them from that table and writes the roles into `<name>.procs` beside the pid file in the same breath; `stop` removes both. `processes_in()` reads them back as alive-or-gone with the log path of each, which is what the next commit prints. A plant left running by an earlier release has a pid file and no roles file. Every one of its pids is still reported, each saying its role was not recorded: unknown is not zero, and a status line that names the wrong process is worse than one that admits it cannot name it. Signed-off-by: scottkalw <scottkalwei@gmail.com>
Three answers `fsmes demo <plant>` did not have on 2026-10-13. `status` on a plant that is alive and silent now names which of its processes are still alive and which are gone, by role, gives the log path of each one that went, and ends with the two commands to type. On a healthy plant it prints the address to open. On a plant whose processes have all gone it says so and how to start it again. Before this it relayed the fleet's three states and stopped, and the whole of what the first Windows user had on screen was two pid numbers. `stop` recovers that state by itself, with no flag. It says what it found first - "bottling was not answering; 2 of its 5 processes were still alive (7964, 3940); stopping them." - because afterwards the pids are gone and nobody can check the sentence. `fsmes fleet stop` keeps its `--force` exactly as it was: there a name can mean a plant on another host that somebody else is watching, while `fsmes demo bottling stop` can only ever mean the one plant this installation made in this home on this machine. The flag gives up liveness, never ownership, and a plant this installation did not create is refused on this path exactly as before. `start` looks before it launches. A port somebody else holds does not stop a start - it kills the process that wanted it a second later, in a log file nobody is reading yet - so the ports the pack asks for are probed first, and the refusal says which port, what it is for, and whether another plant of ours holds it or another program does. Where this installation's own silent plant is holding them, it points at the `stop` above. The words live in `fsmes/firstrun.py` and are printed by the `demo` wrappers only. `fsmes fleet` and its gate are untouched. Signed-off-by: scottkalw <scottkalwei@gmail.com>
`.github/scripts/recover_a_half_dead_plant.py` makes the state the first Windows user was stuck in - start the plant from a built wheel, then kill the dashboard and API process and nothing else - and asserts the way out: `status` names the process that is gone by what it is for and prints the log that will say why, the log it names is really a file, the OPC server is still alive while that is true, `stop` recovers it with no flag and says what it found, and `fsmes demo bottling` brings the plant back up. It also runs `python -m fsmes --version` in that venv, which is his first question in one line. On ubuntu-latest and windows-latest, beside `wheel-plant`, because all of this is processes, pids and console output and that is exactly where Windows differs. It reuses `plant_from_wheel.py`'s install and poll helpers rather than keeping a second copy that would drift, and it asserts words and exit codes - never seconds. Signed-off-by: scottkalw <scottkalwei@gmail.com>
Scott installed 0.3.0 on his Windows PC on 2026-10-13 and hit three things in three hours: `fsmes` was not recognized after a `py -m pip install`, the plant stopped answering, and `status` said two of its processes were still alive. The product fixes are in the commits before this one; this is the page that says the same things in words, for the next person, before they ask. One screen, in the order a first hour happens: install on Windows and the two ways out when the command is not found; is it still running, and what takes a demo down on a laptop (it is not a service, so nothing restarts it); the alive-but-silent state, what `status` prints and the two commands out of it; what is on this one box and what needs a key or a local Ollama; and the four things people trip on going from a shape file to a plant of their own. It says which version first carries the fixes (0.3.1, not yet released) and gives the 0.3.0 way out beside the new one, so a reader on today's release knows which half of the page is already true for them. Linked from the README's Windows paragraph, docs/index.md, the Operate index and first-plant.md's install step, and in the Operate nav. Signed-off-by: scottkalw <scottkalwei@gmail.com>
`firstrun.answers` opens a TCP connection to 127.0.0.1 and closes it, which is an outbound primitive, and the register is the one place every one of them is written down - the suite scans the source and fails on a call site no entry covers. It is `allowed`: nothing about a plant leaves the box by it and nothing outside this MES changes. The total in the register's own docstring goes from 35 to 36, and so does the line in the harness design page that quotes it. Signed-off-by: scottkalw <scottkalwei@gmail.com>
|
Thanks for the first pull request. Checks: every commit signed off ( |
kalwei
added a commit
that referenced
this pull request
Oct 15, 2026
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
added a commit
that referenced
this pull request
Oct 15, 2026
…w mode, and an honest page on migration (#173) ## 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. --------- Signed-off-by: scottkalw <scottkalwei@gmail.com>
Merged
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three things stopped a first Windows run on 2026-10-13, inside three hours. All
three are fixed here, and the words they print now are below.
1.
fsmeswas not recognized afterpy -m pip install. On Windows, pip putsfsmes.exein a Scripts folder a default Python install does not put on PATH.There is now
src/fsmes/__main__.py, sopy -m fsmes demo bottlingdoeseverything the command does — and a plant started that way starts its own child
processes the same way (
plant.fsmes_command()falls back to[sys.executable, "-m", "fsmes"]), which matters because the machine where PATHis broken is exactly the machine the children are launched on.
python -m fsmes --versionis checked on both runners.2.
statussaid two processes were alive and nothing answered, and it did notsay what that meant or what to type. A plant now records what each of its
processes is for, in a
<plant>.procsfile written in the same breath as thepid file, and
statusreads it back:The roles come from the file
startwrote, never from the position of a pid inthe pid file — order is the one thing a half-dead plant has lost. A plant started
by an older release has no such file and says so rather than guessing: unknown is
not zero. A healthy plant ends with
bottling is up: http://127.0.0.1:8010/dashboard.3. The easy stop could not recover that state, and there is no flag on it.
--forcelives onfsmes fleet stop, and the first answer given on the day wasa flag that does not exist. Worse, the pair was a closed loop: the gate permits
starton a silent plant, butplant.startthen refuses because the pid filenames live processes — so neither command got him out.
fsmes demo <plant> stopnow recovers it with no flag and says what it found and did:
fsmes fleet stopkeeps its--forceexactly as it was. The reasoning is instandalone.stop's docstring: the gate's safety property is ownership, notliveness, and on the demo path the person typing
stopowns the plant they madeon this PC five minutes ago. A remote or not-owned plant is refused as before.
And one that follows from them:
fsmes demo <plant>now looks at the portsbefore it launches anything, so starting on top of a silent plant no longer fires
five processes that fail at bind one by one:
A port held by something else says so instead: which host and port, what needs
it, and what is on it — this installation's own named plant, or "another program
on this PC".
The page
docs/operate/run-it-on-your-own-box.md, one screen, in the order a first hourhappens: the Windows install and the two ways out when the command is not found;
is it still running, and what takes a demo down on a laptop (it is not a service,
so nothing restarts it); the alive-but-silent state and the two commands out of
it; what is on this one box and what needs a key (~$0.12 a question) or a local
Ollama; and the four things people trip on going from a shape file to a plant of
their own. It says the fixes first ship in 0.3.1, not yet released, and gives
the 0.3.0 way out beside the new one. Linked from the README's Windows paragraph,
docs/index.md, the Operate index and nav, andfirst-plant.md's install step.How it is verified
half-dead-plant, on ubuntu-latest and windows-latest(
.github/scripts/recover_a_half_dead_plant.py, besidewheel-plant): fromthe wheel in a fresh venv,
python -m fsmes --version, start the shippedbottling plant, kill the API process only, prove the OPC port is still
alive, assert
statusnames the gone process and its log and that the log itnames is a real file on disk, assert
stopexits 0 and says what it found,assert both ports come back, and start the plant again. Words and exit codes,
no timing numbers.
ratchet and a ratchet asserting the CI job keeps both runners.
second start — the blocks above are that run's own output, pasted.
ruff checkclean; the default tier and the slow tier green locally.fsmes.firstrun:answersopens a TCP port, so it has an entry inshadow.REGISTER(allowed, nothing leaves the box), and the register's totalgoes 35 → 36.
Not here
A 0.3.1 release — that is the maintainer's call. A Windows service or
auto-restart. Anything about why that particular plant half-died on that
particular PC: the page now says where the log is.