.gitattributes sets text=auto with eol=lf, so every text file is
stored and checked out with LF, and a patch keeps its bytes. The files
that carried CRLF from a Windows editor are renormalized. No content
changes.
The sheet handed the bench actuator every press after the operator's
presence press, and the actuator pressed every time - into nothing. On
the bench reference all six cards were pressed before the card asked:
the frame by 8 s, the focus by 33, the floor by 10, the dose curve by 8,
the corner by 9, and the flow-load card by 69. The operator then pressed
all six himself, which is the opposite of what the ready gate promises.
arm_press() waits for hw.button_lit(), which is true when any button LED
is on, and burn() started that wait before run_check had even started the
wizard. The button is lit through parts of a card that are not the arm -
the lens reference, the program on its way to the controller - so the
wait ended at once, the press landed before the job waited for it, and
the thread was gone by the time the real cue came. forgectrl uses the
same predicate but only inside the job's own sample callback, with the
tube still dark, where a lit button does mean the arm.
The machine already says when it wants the press: a live check opens a
`press` wait prompt at that moment, and run_check sees every prompt. It
now presses there, through a new Ctx.press_now() - no LED read, no
waiting thread, no timing guess. That retires the per-card lit-timeout
column of CARDS, which existed only to give the flow-load card's coolant
settle enough room for a wait that was reading the wrong thing.
The four grbl-driven arm_press() callers in laser.py and cooling.py are
left as they are: they call it with the machine idle and its LEDs dark,
so the level read is the edge they mean. The same shape would bite them
if that ever stopped being true.
Pin: forgectrl 0.1.15 (6e69479, the flow-load tail ending at the coolant
peak and the way out of a finished setup page).
Host-proven: 357 forgetest unit tests, including two new ones - the
actuator presses on the prompt with button_lit stubbed false throughout,
so the LED is provably not consulted, and the press falls to the operator
without a takeover.
meta-forgefirm: the forgefirm-users init replays the account at boot;
sshd refuses root and empty passwords and runs only while the panel
turns it on; the release image keeps an empty root password for the
console; the console banner; avahi announces forgefirm.local; https in
libmicrohttpd and ulfius; the panel on 80 and 443; the license bundle on
the rootfs; release.sh checks the root policy on the built rootfs.
forgetest: the commission suites (commission, commission_dark,
commission_sheet: 23 cases); the runner turns cloud mode on with the
typed phrase for a test that declares it; the baseline's motor_lock is
0; the log-export test checks the bundle for the camera key; the record
helpers write bytes as given and join the daemon's paths as POSIX. The
stream harness gains rule 24: a hold verdict is held again after a
resume. Bench drills: lens_travel.py and lens_stop_accel.py.
Docs: BRINGUP carries the present state; CAMPAIGN-LOG carries the dated
record.
The ready gate lives inside the arm-and-fire helper, and the kill drill
calls that helper twice, once for the expected stop and once for the
SIGKILL. So the presence gate asked the operator for a second press part
way through a test they had already proved themselves present for, with
the actuator standing by holding the presses. It is the only test in the
catalog with two ready gates.
The second gate now returns at once. Its setup line still goes up,
because the second half may want the scrap moved, but there is no press
to make.
A live test asked a person to click Ready on a page and then make
timing-critical presses in the middle of a burning cut. That is how
tonight's pause test became unanswerable: the actuator had dropped off
the network, the harness fell back to the operator without saying so,
and afterwards nobody could tell a second press from the machine
resuming on its own.
Where an actuator is up and wired to the button, the ready gate now
takes a press on the machine's own button as the presence check, and the
actuator performs every press in that test. The button does nothing at
Idle, so the press is only a presence check, and the gate waits for the
release so it is never read as the arm press. With no actuator the
operator does the presses and answers on the page, as before.
An actuator lost after that takeover is now said out loud, in the log
and in the evidence, instead of quietly becoming a person's press.
The live-fire cue was four lines of machine-shaped prose. It is now what
a person needs: protection, exhaust, extinguisher, scrap, lid.
update.slots-and-signature's apply section required 200 from
POST /update/apply, where the daemon answers 202 with started, like
every job endpoint, so its first bench run on image 20260902144848
ended before the job did; the cleanup then deleted the staged archive
under the running job. The drill requires 202 and started, and looks
for the daemon's refusal ("archive is not signed with the ForgeFIRM
release key"). Bench: the apply started, the job ended with that
refusal, PASS.
A queue started 4 s after a forgetest restart ran 7 tests instead of
10. The bench page's /state poll had a fixture probe in flight (an mDNS
answer), probe_fixture stamped its time at its start, and the queue
start read the stale fixture, none, so the three operator tests the
fixture runs in the unattended queue were routed to nobody. The probe
now runs under a lock and is stamped when it completes: a caller that
arrives during a probe waits for its answer. tests/test_fixture.py
holds the race with a slow scripted probe; it fails on the old code.
Catalog consequence: the update implementation hash moves; the runner
change is dev-only.
The first campaign with the bench actuator wired failed
motion.button-hold-resume on the tool, not the machine: the second press
was asked while the first 200 ms pulse was still on, the fixture answered
409, the runner handed the step to an operator who was not in the room,
and the post pass could not jog a controller left in Hold.
- fixture.py: a press waits for the last pulse to end (the fixture's
pulse_ms) plus a 300 ms release, so the controller sees the edge; a
409 for a pulse in progress is waited out against button_pulsing and
retried once.
- runner.py: in an unattended run a fixture refusal ends the test at
once as ERROR naming the refusal; the operator fallback stays for
attended runs.
- baseline.py: a controller in Hold or Door gets a soft reset before the
return jog, position kept.
- tests: the fake fixture refuses a press while one is in progress and
reports button_pulsing; FakeGrbl records ^X and can land a reset in a
chosen state; five new tests.
- docs: ACCEPTANCE.md fixture rules, fixture/README.md tool's side.
No catalog consequence: tool-side change, no covers map moves.
Bench: campaign c-20260824174545-0bdc 25/25 with every action by the
fixture; the hold reset proven by a dry drill.
fixture.py: the bench's /data/forgetest/fixture.json (hostname, key,
optional ip, the channels wired, arm_press), a resolver for
<hostname>.local asked of the network directly (the image has no mDNS
resolver), and the client. The runner probes it before every run and
at most every 30 s otherwise; ctx.act asks it for a channel it covers
and still waits for the machine's own reading, falling back to the
operator's notice when the box fails. A test declares with hands=(...)
what it asks of a person beyond its typed actions; an operator test
with none, whose actions the fixture covers, is routed into the
unattended queue, its Ready gates pass, and a prompt it raises anyway
is a FAIL naming the undeclared step. Live tests never move; their arm
press stays a person's unless the bench opted in, in which case the
fixture presses when the button lights. Whatever the box still holds
after a run is released before the baseline's post pass and recorded.
The page shows what the fixture covers. Contract in ACCEPTANCE.md; the
wiring facts, with the interlock connector left to the bench to settle
(SAFETY.md and the sister map differ), in BRINGUP.
Catalog unchanged in its definitions; the cloud and laser suites'
shared code moved, so their implementation hashes move with it.