Updates
What changed recently
Short progress summaries across all my projects, newest first. The detail lives in each project's articles.
Updates are bullet-point summaries generated with AI from my commits and build notes, then checked by me. Project articles and learning notes are written by me.
- Active
- The plan’s own recommendation, to take the earliest and latest four-digit year in the article, was measured against the corpus and rejected. It yields
1984-1993for a car built from 1989, because the article opens by mentioning the previous generation’s launch year. The plan had predicted that failure in the abstract; the counterexample was already sitting in the tree. - The guides’ own heading ranges were ruled out too: present in only 32 of 81 guides, and for one car the heading names the facelift years while the article is about the launch.
- What shipped instead: a range declared in the human-written source section wins outright, and the article title is never consulted at all, because the title is model-written and the input type has no field for it. Failing that, take the dominant cluster of years in the body, breaking runs at a gap of more than four years, which is what discards the orphan 1984. The guide heading may veto a derivation it disagrees with, or widen one it agrees with, capped at a ten-year generation.
- Four of five articles now derive their true generation range exactly. The thresholds are fitted to five articles, all three are asserted at their boundary, and the sixth article will be the first genuine out-of-sample test.
- The recipe bump lives on a branch rather than
main, because a version change makes every pending batch un-appliable by design. Keepingmainon the old version leaves the working cover flow working while the new one waits for its visual gate. - Model years are written to a per-candidate sidecar at generation time, keyed by the image’s own hash, and the apply step reads it rather than re-deriving. A missing record is fatal, and one naming a different article aborts the batch before any write.
- Two review passes found eight defects while the test suite was green at 581 the entire time. All four of the first pass’s findings came from reading the code against the corpus, not from running anything.
- The plan’s own recommendation, to take the earliest and latest four-digit year in the article, was measured against the corpus and rejected. It yields
- Active
- A section the reviewers will not pass used to be re-selected every night forever, because the used-set is built only from committed articles and a skip writes none. Recently-skipped sections now sort last: deprioritised, never excluded, since the trigger was a typo in a source guide and burning a section permanently would cost runway for a transient fault.
- The other half of that issue was never real. Four documents said
skippedrun records carry nosourceblock, described as the cheaper and more urgent of the two fixes. They always have carried one: it is set before all three skip paths, and every committed record proves it. The stall was visible in the records the whole time. What is missing is anything that looks at them, which turns a pipeline bug into a monitoring gap, and that is the real precondition for putting this on a timer. - The first cover generated with no human choosing the image came back as a 2001 bugeye Subaru for an article about the 2007 car. Not a bad roll. The prompt gets a title, a heading and a description, and an article identified only as “third-generation” contains no year anywhere. The tally for unsupervised covers is now 1 good, 1 wrong, and the wrong one is precisely the failure no automated check in this repository can detect.
- Ben chose to automate cover approval anyway (apply, then notify) over a pull-request model where nothing reaches the site until he merges. That decision rested on three-of-three accepted first time, and the record is no longer three of three.
- The notification design turns on one constraint: GitHub does not notify you about your own activity. The job authenticates as Ben, so any design where the job opens the issue produces silence. An Actions workflow runs as a different actor, so the bot opens the issue and mentions him.
- The guard that checks the image generator received the exact prompt cannot be a proof, and the code said it was. The CLI runs image generation in code mode, so its rollout records the program rather than the value, and there is no literal to compare against. Kept, renamed to attestation, and deliberately left out of the table of real controls.
- Two tests were vacuous and only mutation found it. One passed with its cleanup code deleted, because the failure it injected fired before anything existed to clean up. Rollback has two kinds of work, restoring what changed and removing what was created, so it needs two injection points, not one.
- Active
- The site published its first article without a human in the loop: drafted locally, passed by both reviewers, built, committed and pushed by the job itself, in 41 seconds. I offered to hand-publish an existing draft instead, to get a live page faster. Ben’s answer was “That is the whole point isn’t it?”, and he was right. The first public page was made by the machine.
- Two runs in a row skipped on the same section, and the reviewer was correct both times. The source guide calls a Honda Integra an “Integrale Type R DC5”, which is a Lancia name, and the model was reproducing it faithfully. The same guide gets it right twice elsewhere. Fixed the guide, and the next run published on the first attempt.
- Two tests went red because the work succeeded. Both asserted the live corpus still listed a particular article as missing a cover, a transient property of live content written down as an invariant. Worse, one of them is supposed to be testing command-line argument forwarding and had used cover status as its payload assertion, so a routine backfill could redden a test guarding something else entirely. Rewritten to prove forwarding in two ways that say nothing about content.
- The deploy-timing check earned its place immediately. The live site still served the old text when first queried, so all five verification assertions would have passed against the previous build and told me nothing.
- First three real covers applied. Original PNGs of 1.9-2.3 MB normalise to 59-70 KB, roughly 30x, and the generator returned two different aspect ratios, so the normaliser is load-bearing rather than incidentally correct. All three were accepted on the first candidate.
- A run aborted in 1.3 seconds because the shell carried stale environment variables pointing at the old model on the wrong port, and it committed and pushed a
failedrun record, which is exactly the documented behaviour. A precondition failure that leaves a trace is the difference between a bad night and a silent fortnight.
- Active
- Verified TIM2 counts real milliseconds: 6,212 ms on the MCU against 6,208 ms on a PC stopwatch over 63 telemetry lines (ratio 1.001). Cross-checked a second way against the sensor cadence.
- The battery ADC read between 4.96 V and 13.29 V under motor load on an 8.4 V pack, so the measurement is wrong rather than the pack. Leading suspect is ADC sampling that isn’t synchronised with the 25 kHz motor PWM. Not yet confirmed.
- The low-battery cutoff stays disabled until the reading is understood. Averaging more samples first needs a wider accumulator than
uint16_t.
- Active
- A pre-implementation review of the workspace plan found a destructible window nobody had noticed. The plan told the operator to stop with a cover applied but uncommitted, and the article pipeline’s startup reconciliation deletes untracked files from the cover directory before it checks for a dirty tree, so a nightly run in that window would delete the new WebP, abort on the modified article, and leave frontmatter pointing at a file that no longer exists. The ignored workspace is safe from this precisely because it is ignored; the applied cover is not.
- A proposed test would have exempted “explicitly labelled POSIX examples” by trusting the fence language, and the project’s own how-to-run block was fenced as bash while containing seven command forms that silently drop safety flags on Windows. The test would have exempted the exact block an operator copies, and reported green. It now requires an explicit label rather than a fence name.
- The argument-forwarding fix was asserted but never proven with a path containing a space, and this repository lives under a directory with a space in its name, so every manifest path has one. Proven properly before the plan was implemented.
- Planned a Claude-calls-Codex image integration and then deferred it, because planning it surfaced two corrections to its own premise: the cover it was meant to generate already existed and was sitting on disk, and the thing actually blocking progress was the argument-forwarding bug, not image generation. Its first step is now a cheap probe that may invalidate the whole plan.
- Adopted a standing rule after nearly losing a plan: plans live in the project’s
plans/folder, never in the editor’s scratch directory. A scratch plan gets a generated name, sits outside the repository, is unversioned and is invisible to every other session.
- Active
- Wrote a test that proves the nightly pipeline cannot reach an image generator: import closure, URL literals, declared dependencies and subprocess call sites, plus a layer-zero assertion that the walk visited real modules, because a denylist over an empty set passes happily. Then watched all six rules fail, one injected fault at a time, before committing any of it.
- That walk had a blind spot the plan had measured wrongly.
@iceniweb/ui/typesis not a third-party package, it is a tsconfig path alias pointing back intosrc/, and bucketing it with the bare specifiers would have stopped the import walk at the repository boundary while still inside the repository. It now reads the alias map from the config, so a new alias is walked the day it is added. - Recorded in the same commit what the guard does not prove: it pins static URL literals, so a URL assembled at runtime or read from an environment variable passes, and the pipeline already derives its model URL from an environment variable. Repurposing the arguments of an already-approved subprocess would pass too.
.env.exampleasserted the reverse of two facts.IMAGE_MODELandIMAGE_SIZEare read by no code anywhere in the repository, andOPENAI_API_KEYis the text reviewer’s key, not an image key. Documentation about a thing that does not exist reads exactly like documentation about a thing that does.- The first real cover attempt died twice before reading its own manifest, and the manifest was fine. PowerShell consumes the standalone
--when it invokes npm’s wrapper, so every flag after it was swallowed. Calling the nativenpm.cmdpreserves it. This is the same defect that published an article by accident five days ago, which means it applies to--dry-runand--no-pushas well as to cover flags. - Generated a first-generation Touareg cover that was accepted on the first candidate: pale concrete, VW roundel and wheel centres, no invented engine badge, and a blank number-plate-shaped panel that got called out in review and accepted deliberately.
- Active
- Deleted a permanent command I had no business building. Stage C needed four rendered prompts, once, as evidence for a visual decision, and what got built was a
covers:promptCLI with flag parsing, two modes, a lister and its own test file. Rendering four files is a tool-shaped problem for about ten minutes; after that it is a supported interface with a maintenance cost. The rendering was done by a throwaway script that imports the real function directly, andpackage.jsonwent back to byte-identical withHEAD. - Two agent sessions shared one checkout and the second inherited the first mid-flight. A plan written at 10:20 whose first item was “run the visual gate on the v1 prompts” was stale before it was read, because the other session had already run that gate, rejected the treatment and advanced to v3. Git records neither the author nor the time of an untracked file, so nothing in
git statusflags that half the pile arrived from somewhere else. - The dark workshop look was rejected in favour of pale concrete, diffused daylight and one orange accent, at a fixed eye-level three-quarter composition. Badges then became their own argument: the first badge-enabled edit invented large MR2 bumper lettering, and a generic Touareg render happily wore a
V6badge the article never claimed. The recipe now requires authentic badges only where the fenced evidence supports them. - Two guides turned out to be unusable as cover evidence, for reasons that are the corpus’s fault rather than the model’s. One description opens with a literal
<p>left over from the migration. Not a control character, not an override phrase, so the sanitiser passes it straight through. The other truncates mid-sentence before the word “SUV”, leaving “the maker of some of the world’s best sports cars” as the prompt’s last word on the subject. Evidence pointing away from the body style is worse than no evidence. - Prompt snapshots are verified by re-rendering and comparing bytes, not appearance, so a “representative example” cannot quietly drift from what the code produces.
- A gate run before the last few files are written is not a gate run on the change:
git diff --checkhad been recorded as clean, then failed on trailing whitespace in two READMEs written after it.
- Deleted a permanent command I had no business building. Stage C needed four rendered prompts, once, as evidence for a visual decision, and what got built was a
- Active
- Removed the
delay_ms(5)after each loop. It added 5 ms on top of about 3.34 ms of work, so the loop ran near 120 Hz against a 200 Hz sensor. Motor jitter dropped straight away. - The controller, encoders and telemetry now advance only on a complete fresh DMP packet, because the loop polls 625 to 770 times a second without the delay.
- Added an
IMonotonicClockinterface for the stale-IMU motor timeout. Measured cost: 616 bytes of flash and 8 bytes of BSS, under 1% of the 256 KB flash. - See Run the controller only on fresh samples and Separate the control logic from the hardware.
- Removed the
- Active
- The finding of the session: a green rollback suite that had never rolled anything back. Twenty-four passing tests, and every failure case aborted during validation, before a single byte was written. Undoing nothing always succeeds. Added a seam that injects a failure mid-batch and tests that assert every article is byte-identical afterwards, rather than merely that something threw.
- Cloud sync had renamed two tracked files, leaving them as deletions with byte-identical
(1)twins beside them. Both are named by the build or the test script, so three gates would have failed and every failure would have read as a defect in the new code I was about to commit. Confirm the tree is the tree you think it is before believing a gate failure. git diff --cached --statcaught a test fixture staged asBin 0 -> 10214 bytes: a literal NUL byte written where the escape\u0000was meant. Two other fixtures held literal zero-width characters, which are valid UTF-8 and would not have tripped the check at all, while being invisible to every reviewer. Control characters get written as escapes from now on.- Sharp refused an oversized image from inside the metadata read, which made the explicit pixel-count check unreachable and left “could not be decoded” as the only reportable cause. Judging the declared dimensions first refuses the image before a pixel is read and names the real reason. The decode limit stays as a backstop for a header that lies about its own size.
- Image shape is judged after EXIF orientation is applied, not before. A portrait photograph tagged sideways is still a portrait photograph.
- Article titles reach the image prompt, so they are treated as data: the prompt declares the fenced block non-instructional before the block appears, each fence marker occurs exactly once, and evidence containing a marker is rejected. The blocklist is documented as defence in depth behind the fence, not as the defence.
- Made “do not invent specifics” checkable instead of merely intended. The fixed half of the recipe contains no year, model, trim or place, and a test asserts every number in a finished prompt traces back to the article. Editing the style block to mention an era now fails that test.
- Active
- The first real pipeline run published a live article by accident.
npm exec tsx … --drafts --no-pushconsumed both safety flags before the generator ever saw them, so a run meant to write a local draft committed and pushed. Reverted within minutes, but the fix is the lesson: there is now one supported entry point with the--separator, and a test that proves both flags arrive at the script unchanged. - The second and third runs went as designed. One was rejected by both reviewers and committed only its run record; the next passed at 9/10 from both, with 37% overlap and a 21-word longest run, and became the first machine-written article on the site, as a draft.
- Publication dates were being computed in UTC, which puts an article on the wrong calendar day for anyone who has a calendar. They now come from the site’s declared timezone, with boundary tests.
- Replanned the cover work away from a paid image API inside the nightly job and toward a supervised backfill run from an interactive session on an existing subscription. Articles publish text-only with a placeholder until a cover catches up.
- Cover status is derived from article frontmatter and nothing else. A second, hand-maintained queue of “what still needs a cover” is exactly the file that goes stale and then gets trusted.
- The first real pipeline run published a live article by accident.