Skip to content

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
kalwei merged 6 commits into
mainfrom
fix/first-run-on-windows
Oct 14, 2026
Merged

kalwei merged 6 commits into
mainfrom
fix/first-run-on-windows

Conversation

@kalwei

@kalwei kalwei commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

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. fsmes was not recognized after py -m pip install. On Windows, pip puts
fsmes.exe in a Scripts folder a default Python install does not put on PATH.
There is now src/fsmes/__main__.py, so py -m fsmes demo bottling does
everything 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 PATH
is broken is exactly the machine the children are launched on. python -m fsmes --version is checked on both runners.

2. status said two processes were alive and nothing answered, and it did not
say what that meant or what to type.
A plant now records what each of its
processes is for, in a <plant>.procs file written in the same breath as the
pid file, and status reads it back:

  bottling is not answering on http://127.0.0.1:8010, and 4 of its 5 processes are still alive:
      still alive: the OPC server the machines answer on (pid 1294967)
      still alive: the reader that writes the machines into the MES (pid 1294972)
      still alive: the simulated floor (pid 1294974)
      still alive: the log sink (pid 1294966)
      gone: the dashboard and the API (pid 1294973) - its log is /tmp/fsmes-by-hand/home/logs/bottling/api.jsonl
  that is a plant that is running and cannot be talked to. This is the way out:
      fsmes demo bottling stop      (stops what is left and clears the record)
      fsmes demo bottling           (starts it again)

The roles come from the file start wrote, never from the position of a pid in
the 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.
--force lives on fsmes fleet stop, and the first answer given on the day was
a flag that does not exist. Worse, the pair was a closed loop: the gate permits
start on a silent plant, but plant.start then refuses because the pid file
names live processes — so neither command got him out. fsmes demo <plant> stop
now recovers it with no flag and says what it found and did:

  bottling was not answering; 4 of its 5 processes were still alive (1294967, 1294972, 1294974, 1294966); stopping them.
      bottling is not answering; stopping it anyway because this installation created it and the instance id in /tmp/fsmes-by-hand/home/data still matches.
      bottling stopped (4 process(es)).
  `fsmes demo bottling` starts it again.

fsmes fleet stop keeps its --force exactly as it was. The reasoning is in
standalone.stop's docstring: the gate's safety property is ownership, not
liveness, and on the demo path the person typing stop owns the plant they made
on 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 ports
before it launches anything, so starting on top of a silent plant no longer fires
five processes that fail at bind one by one:

  not starting bottling: it is not answering, but 4 of its 5 processes from the last start are still alive:
      ...
  they are holding its ports, so starting it again would launch processes that die one by one. This is the way out:
      fsmes demo bottling stop      (stops what is left and clears the record)
      fsmes demo bottling           (starts it again)

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 hour
happens: 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, and first-plant.md's install step.

How it is verified

  • A new CI job, half-dead-plant, on ubuntu-latest and windows-latest
    (.github/scripts/recover_a_half_dead_plant.py, beside wheel-plant): from
    the wheel in a fresh venv, python -m fsmes --version, start the shipped
    bottling plant, kill the API process only, prove the OPC port is still
    alive, assert status names the gone process and its log and that the log it
    names is a real file on disk, assert stop exits 0 and says what it found,
    assert both ports come back, and start the plant again. Words and exit codes,
    no timing numbers.
  • 28 new unit tests across three files, named as prose, plus the layering
    ratchet and a ratchet asserting the CI job keeps both runners.
  • Run by hand on Linux from a fresh venv of this branch, start to recovery to
    second start — the blocks above are that run's own output, pasted.
  • ruff check clean; the default tier and the slow tier green locally.
  • fsmes.firstrun:answers opens a TCP port, so it has an entry in
    shadow.REGISTER (allowed, nothing leaves the box), and the register's total
    goes 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.

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>
@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.

@kalwei
kalwei merged commit 00ee8b6 into main Oct 14, 2026
24 checks passed
@kalwei
kalwei deleted the fix/first-run-on-windows branch October 14, 2026 10:52
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>
@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