Email me
Updates
← Automating Garage Dreams

Updates

Automating Garage Dreams

Every update on this project, newest first.

28 updates

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.

  1. AI summary

    • 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-1993 for 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. Keeping main on 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.
  2. AI summary

    • 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 skipped run records carry no source block, 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.
  3. AI summary

    • 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 failed run 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.
  4. AI summary

    • 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.
  5. AI summary

    • 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/types is not a third-party package, it is a tsconfig path alias pointing back into src/, 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.example asserted the reverse of two facts. IMAGE_MODEL and IMAGE_SIZE are read by no code anywhere in the repository, and OPENAI_API_KEY is 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 native npm.cmd preserves it. This is the same defect that published an article by accident five days ago, which means it applies to --dry-run and --no-push as 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.
  6. AI summary

    • 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:prompt CLI 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, and package.json went back to byte-identical with HEAD.
    • 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 status flags 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 V6 badge 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 --check had been recorded as clean, then failed on trailing whitespace in two READMEs written after it.
  7. AI summary

    • 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 --stat caught a test fixture staged as Bin 0 -> 10214 bytes: a literal NUL byte written where the escape \u0000 was 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.
  8. AI summary

    • The first real pipeline run published a live article by accident. npm exec tsx … --drafts --no-push consumed 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.
  9. AI summary

    • The site I was verifying was not the site that is deployed. garagedreams.net is still parked at the registrar, answering 410 from someone else’s server with the registrar’s nameservers; the Pages project deploys to garagedreams.pages.dev and always had. Three documents said “Cloudflare is wired”, which is true of the project and false of the domain.
    • A draft: true post 404s in production, and drafts are absent from the sitemap and the feed. The whole burn-in safety argument rests on that and it had never been checked. The first pass reported a false positive: /<slug>/ 404s for everything, live guides included, because the route is /blog/<slug>/. A 404 test with no live control proves nothing.
    • public/robots.txt had been pointing at jdmgems.com/sitemap-index.xml since the fork, so the real sitemap was never advertised. A static asset has no schema and no test, so the build gate cannot catch this class of bug at all. Only looking at the deployed site finds it.
    • --dry-run returns before the build gate, and the harvest driver passes --dry-run on every run. So astro build had never once run against a model-generated article, across five batches and sixty-plus drafts. Every “published” count in the evidence that set the originality thresholds is an upper bound.
    • Re-analysed all five archived batches directly instead of trusting their summaries. Claude has passed 58 of 58 Gemma 4 articles. On the shipping model its verdict has never changed an outcome, and the two-reviewer gate is functionally “Codex must pass”. It is not a rubber stamp in general: it rejected 9 of 14 drafts from the weaker model.
    • Codex’s mean score is higher than Claude’s while Codex is the one demanding revisions. A threshold fitted to the numeric score would have been fitted to the wrong signal entirely.
    • The code’s own docstring denied a real bug. A failed push leaves the commit in place, so the tree is clean and every precondition passes, which is how a job stacks unpushed commits night after night while looking healthy. The comment claimed the next run’s clean-tree check catches it. Fixed with an explicit upstream comparison, made deliberately asymmetric so that being merely behind the other peer does not abort the night.
  10. AI summary

    • Built the originality gate in three deliberate stages: record the draft on every outcome, then measure and enforce nothing for three batches, then arm it. Shipping a threshold before there were numbers to set it from would have repeated the dead-knob bug from the 12th.
    • The measurement could not have been taken at all. The run log only stored prose on rejection, so the batch where the 26B published 20 of 20 left twenty-one records containing no article text whatsoever. The one batch that most needed examining had thrown away its evidence.
    • Three models, 60 drafts, the same 20 sections. Gemma 4 26B copies steadily: median 29% five-gram share, worst case 72%, and that worst case is a straight reflow of a bulleted list into paragraphs that both reviewers passed, because every claim in it is perfectly supported. Gemma 3 4B is bimodal: best median at 15%, and two runs that echoed the source almost whole, 95% and 99%, one with a 577-word unbroken verbatim run.
    • Those two catastrophic runs were caught only because a long source pushed them over the word limit. A shorter section would have produced a verbatim copy inside the cap and both reviewers would have passed it. The only thing between total transcription and publication was a rule that happened to correlate.
    • The plan predicted the small model would be unpublishable, and it inverted. Gemma 4 E4B, the only one of the three that fits an 8 GB Pi, copies less than the 26B, has no catastrophic tail at all, and runs three times faster. Copying is closer to what a capable model does when told to stay faithful to a source than to what a weak one does.
    • Armed at a 30-word verbatim run and a 45% five-gram share, plus a separate fatal rule at 90% that skips rather than revising. Overlap fired in 3 of 20 runs and revision cleared it 3 for 3. The live risk was a fourth rule the model could never satisfy, and that is now retired with evidence rather than hope.