The agent's own append-only working memory. Excluded from the naming and prose
audits via .host-lintignore; never rewrite an old entry (correct in place with a
dated pointer). A fresh session reads plan/, call/, and this file to continue.
2026-07-02 — host adopted
- Adopted
host-template@565410afor UDPspeeder-simd (case a, Shallow). Stamp written byhost-lifecycle adopt; roomscast/ plan/ call/scaffolded. - Push auth: the stored git credential is the
slartibardfasttoken, which had no push rights toconnollydavid/agentic-UDPspeeder-simd(HTTP 403 on first dry-run push). The operator authorized the repo, after whichgit push --dry-runreturned "Everything up-to-date". If a later push 403s again, surface it to the operator rather than retrying blindly. - Tooling built from source with the system toolchain (rustc 1.96.1, Arch Linux). The methodology pins Rust 1.95.0 as the reproducible-build anchor; the local builds use 1.96.1, which is newer and builds clean. Record this wherever a canonical artifact hash is claimed.
- UDPspeeder-simd embeds as a bare store plus worktree at
software/udpspeeder-simd/branch_libev/(its default branch isbranch_libev, notmain). Recorded in.host-softwarewith nodeploy/artifact, so it is a source pin with no reproducible-build claim yet (migrated software). - host-lint is also a
.host-softwarecomponent (the gating tool):--install-hooksreads a component'shooksscript and builtartifactfrom its materialized worktree, so host-lint must be materialized and built for the commit gate to land.
2026-07-03 — gate green; runtimes installed; push auth is flaky
- The verify gate (
host-lifecycle software --check .) is green: both components at pin, all phase receipts recorded, prose clean, reconcile clean. The commit-msg hook blocks an ordinal tell in a message (tested:phase 1is rejected, exit 1). - Tooling on this machine, all under
~/.local:bin/host-lint,bin/host-lifecycle(v0.35.1),bin/host-prove(v0.3.0),bin/allium(3.5.0),jdk-21/(Temurin 21.0.11),share/tla2tools.jar(v1.8.0, sha256 237332bd). The gate needshost-proveon PATH because host-lint's ownkani:obligations surface once it is materialized as a component; the cheap gate probeshost-prove --help, it does not run Kani. - Two clearance details: the toolchain HAZARD (artifact with no toolchain) is waived by
repro-exempt = call/0002on host-lint (consumed tool, not reproduced here); andremapis askipreceipt, notdone, becauseremap --checkerrors on an empty or absent.host-remap, so a no-rename case-(a) adoption cannot pass adonerecheck. - Push auth to
connollydavid/agentic-UDPspeeder-simdis intermittent: the stored credential is theslartibardfasttoken, which 403s after the first few pushes landed. Ifgit push403s, surface it to the operator rather than retrying or swapping credentials. As of this entry,f8d36edand5ec59afare local only.
2026-07-03 — followed the host procedure: conformant, no upgrade available
- Ran the case-(c) upgrade/verify cycle from the
hostprocedure (github.com/connollydavid/host).host-templateupstream is still at565410a, the exact revision the.hoststamp adopted: agit fetchin the submodule found no commits after it, and565410ais an ancestor oforigin/main.host-lifecycle upgrade .reports up to date (baseline46a1fd2, 0 out of order). With no newer methodology to pull in, following the host reduced to the verify gate. - Verify gate is green.
host-lifecycle software --check .exits 0 (both components at pin, every phase receipt valid, reconcile clean, prose clean, no worktree-symlink hazards);validate plan/andvalidate call/bothok;host-lint --alland--logclean. Independently confirmed: udpspeeder-simd worktree is at pin3374e3bonbranch_libevwith a clean tree, host-lint at78804cd, and the commit-msg hook still rejects an ordinal tell (aphase 1message exits 1, a clean message exits 0). - udpspeeder-simd still carries no
.allium/.tlaspec, so the requirements and timing lanes stay inert and there is no spec-without-lane defect. Every spec found on disk belongs to a tool's own worktree, not to the software under development. - Standalone
host-lint --proseon the authored docs prints advisorynote:lines but exits 0. Those notes are below the enforced bar; the in-process prose audit insidesoftware --checkis the gate that binds and it reports no flagging or warning tropes. Most notes sit in verbatim copied-in content (theUPGRADING.mdledger,cast/applying-personas.md) that is not reworded locally. The notes are not a red gate. - Corrects the 2026-07-03 "push auth is flaky" entry above: the commits it listed as
local-only (
f8d36ed,5ec59af) are now pushed.HEAD == origin/mainand the tree is clean. Push auth may still 403 intermittently; surface it to the operator if it does.
2026-07-03 — embedded the packages feed as a second software lane
- Added
packages(a fork ofopenwrt/packages, GPL-2.0, an OpenWrt feed of Makefiles/shell/C) as a Where-room component in.host-software: source pin1d40ad9on canonical branchmaster, no build/artifact (migrated build-recipe feed, not a built artifact). Materialized tosoftware/packages/master/;software --checkis green. - Mechanics for a new component: host-lifecycle has no
software --add, so a new lane is a hand-edited.host-softwarestanza plussoftware --materialize --item <name> .(the--itemscope avoids re-touching existing worktrees). The embed and release phase receipts are written withhost-lifecycle receipt --record <phase> --component packages --disposition done|skip (--evidence|--reason) .; a new component HAZARDs on missing embed+release receipts until both are recorded. - Key rule (call/0003): commits in the packages worktree carry NO
Co-Authored-By: Claudetrailer, so the feed stays upstream-clean foropenwrt/packages. The operator confirmed the scope is packages only, so host-repo commits keep the trailer. Recorded in call/0003 and the CLAUDE.md packages project-specifics.
2026-07-03 — push-auth root cause: slartibardfast is pull-only; helper is store
- The recurring push 403 has a concrete root cause rather than flakiness.
gh api repos/connollydavid/agentic-UDPspeeder-simdshows theslartibardfastaccount haspush: false(pull only); the owner accountconnollydavid(also logged in to gh) has push. And git's credential helper here isstore(~/.git-credentials), not gh, sogh auth switch -u <user>does not change the tokengit pushsends. The store already holds slartibardfast's token, so pushes go out as slartibardfast and are denied. - To push as slartibardfast (the operator's intended identity), grant slartibardfast
write on the repo; then a plain
git pushworks with no helper change. To makegh auth switchactually route pushes, rungh auth setup-gitso git uses the active gh account's token. Do not push as connollydavid without operator direction: it changes the public push identity, and the operator is provenance-sensitive. - Pending operator decision (asked, away from keyboard): grant slartibardfast write,
push as connollydavid, or operator pushes manually. As of this entry
mainis ahead by 7 unpushed commits (packages embed, call/0003, CLAUDE.md, two memory entries, the earlier verify-sweep entry, and the three-persona commit).
2026-07-03 — resolved: push as connollydavid via a one-shot gh credential
- Operator chose to push as the owner. The working recipe:
gh auth switch -u connollydavid, thengit -c credential.helper= -c credential.helper='!gh auth git-credential' push origin main. The emptycredential.helper=resets the inheritedstorehelper (which holds slartibardfast's read-only token and 403s), and the gh helper then supplies connollydavid's token for that one push, with no change to global git config. - All pending commits landed (
23bdc1f..6f1e66a);mainis in sync with origin. - Going forward: repeat that one-shot override, or run
gh auth setup-gitonce (with connollydavid active) and clear the stale store token so a plaingit pushworks. slartibardfast stays read-only on the repo (not a collaborator); grant it write only if a bot-identity push is wanted later. This supersedes the earlier "push auth is flaky" framing: the cause was permissions plus a static store token, not flakiness.
2026-07-03 — published the book to GitHub Pages
- Ran the publish phase.
host-lifecycle book .(v0.35.1) writes book.toml withsrc = mdBook/srcandbuild-dir = mdBook/out, a SUMMARY in lifecycle order, and 17 pages;book --check .passes (every room renders: cast 5, plan 1, software 1, call 4, reference 4, memory 1). book.toml and mdBook/ are gitignored generated output. - Fixed two defects in the reference doc-site workflow. It was named
site.yml, but the publish-phase recheck istest -f .github/workflows/mdbook.yml, so adonepublish receipt would HAZARD until the file is namedmdbook.yml. It also published./book, while v0.35.1 builds tomdBook/out. Renamed to mdbook.yml, setpublish_dir: ./mdBook/out, and corrected the stalesrc=docscomment. Both look like template bugs at revision 565410a, worth proposing upstream. - CI (
mdbook.yml, on push to main) builds with mdbook v0.4.40 and deploys mdBook/out to thegh-pagesbranch via peaceiris/actions-gh-pages. GitHub Pages was disabled (the/pagesAPI returned 404), so enabled it from gh-pages root withgh api -X POST repos/connollydavid/agentic-UDPspeeder-simd/pages -f source[branch]=gh-pages -f source[path]=/. The site is live at https://connollydavid.github.io/agentic-UDPspeeder-simd/ (HTTP 200). - Publish receipt flipped from skip to done;
software --checkis green (the recheck now finds mdbook.yml). Verified locally with the pinned mdbook v0.4.40 before pushing.
2026-07-03 — udpspeeder-simd package builds clean against the x86_64 SDK
- SDK approach pivoted.
make worldwas excessive; used the public prebuilt x86_64 snapshot SDK (openwrt-sdk-x86-64_gcc-14.4.0_musl, the one OpenWrt CI uses) rather than a from-source build. The embeddedopenwrtcomponent stays but is not needed for this path and adds ~2.5 min tosoftware --check(flagged for the operator to keep or remove). - OpenWrt needs a case-sensitive FS: /mnt/c is case-insensitive and the build refuses it, so software/openwrt is symlinked to ext4 (/home/dconnolly/host-stores); the SDK and its build_dir also live on ext4.
- The package (net/udpspeeder-simd, authored in the packages worktree) built cleanly:
make package/udpspeeder-simd/compileproduced udpspeeder-simd-2026.07.03~3374e3bb-r1.apk (snapshot uses .apk, not .ipk). No fork change was needed — the udpspeeder-style cc_cross- gitversion Build/Prepare sufficed, and the makefile's
export STAGING_DIR=/tmp/did not break it. Dynamic linking confirmed by deps libc/libstdcpp6/librt/libatomic1. PKG_MIRROR_HASH = 225c0fef087e56c190ec1a626fc244a0e32f1f791d5e22efe604e962026c4698.
- gitversion Build/Prepare sufficed, and the makefile's
- Task receipts done: #fork-build, #feed-package, #service-integration. Pending: #test-script (CI runs it), #ci-sdk-lane, #interop, #pr, #backports. The package files are authored in the packages worktree but NOT yet committed or pushed to the fork; the branch strategy and the outward push/PR await operator go.
2026-07-03 — udpspeeder-simd pushed to the fork on a feature branch (PR held)
- Operator chose fork + PR, but the PR needs a review round, so it is NOT opened. Committed
net/udpspeeder-simd/ (Makefile + files + test.sh) on branch
udpspeeder-simdof connollydavid/packages, commit db858a8, pushed. Author + DCO Signed-off-by = David Connolly david@connol.ly (real name + public email, no Claude trailer, per call/0003 + OpenWrt rules). Subject "udpspeeder-simd: add package"; body references https://github.com/openwrt/packages/issues/28562. - The packages worktree had no git identity; set it to David Connolly / david@connol.ly. The canonical master worktree was used in-place to make the branch then restored to master @ 1d40ad9, so the .host-software pin and software --check stay intact; the feature branch lives on the fork, not as a host-tracked worktree.
- Open the PR (after review) from https://github.com/openwrt/packages/compare/master...connollydavid:packages:udpspeeder-simd?expand=1 Before submitting, consider syncing the fork onto current openwrt/packages master (the branch is based on the fork's master 1d40ad9, which may be behind upstream).
- openwrt embed KEPT (operator decision), despite the ~2.5 min software --check cost.
- Receipts: #fork-build/#feed-package/#service-integration/#interop done; #pr held (review round); #test-script + #ci-sdk-lane pending (CI); #backports policy-constrained.
- Update: rebased the branch onto current upstream openwrt/packages master (333bf60); tip is now a7f0f087, a single commit with no merge commits in the PR range; force-pushed to the fork. Formalities self-check (against .github/formalities.json) passes: crlf=0, subject 28<=60, body<=79, DCO Signed-off-by matches a non-noreply author, conffiles declared, openwrt-meta present, no patches. Open review-round items: Run Testing details (built x86_64 via the snapshot SDK gcc-14.4.0 + CI test.sh; a physical-device run is optional but stronger), and a release-notes line if the formalities bot requests one.
2026-07-03 — PR opened to openwrt/packages (pull/29901)
- Opened https://github.com/openwrt/packages/pull/29901 ("udpspeeder-simd: add package") as connollydavid, from connollydavid:udpspeeder-simd to openwrt:master, referencing the issue. Confirmed host-side clean first: main == origin, fork branch a7f0f087 synced, four receipts done. Release notes are in the PR body (a section), testing framed as x86_64 build + CI test.sh (operator accepted that trade-off).
- Corrected the plan: #pr no longer depends on #ci-sdk-lane. The operator directed opening the PR against OpenWrt's own CI, so the fork-side SDK CI lane is independent, not a prerequisite. Recorded #test-script done (script added + wired; the PR CI exercises it) and #pr done. Pending: #ci-sdk-lane (fork-side, optional) and #backports (policy: new packages are not normally backported to release branches).
- Next: watch the PR's FormalityCheck + multi-arch build. If the bot wants release notes in a different form, amend and force-push the fork branch.
2026-07-03 — addressed the Copilot review; fork version-banner fix; idiomatic test.sh
- Copilot flagged two on PR 29901, both valid: test.sh could false-positive (its grep matched
the program name, which a dynamic-loader error echoes), and the init had
fix_latencynon-local plus a duplicatedsock_buf. Both init issues were inherited verbatim from net/udpspeeder (the sibling has the same bugs). @codemarauder is Nishant Sharma, the net/udpspeeder maintainer; offered him co/primary maintainership of udpspeeder-simd in the PR comment. - Idiomatic gap and its fix: OpenWrt test.sh idiomatically greps the package version, but the
binary's print_help() truncated gitversion to 10 chars (
strncpy(...,10)), so a full-version grep would miss. Fixed the fork (main.cpp copies the full string; commit 2e12e5b pushed to branch_libev), re-pinned .host-software to 2e12e5b, updated the package (PKG_SOURCE_VERSION 2e12e5b, PKG_MIRROR_HASH 6d4564dc, PKG_VERSION now 2026.07.03~2e12e5b4), and switched test.sh togrep -qF "$PKG_VERSION". Amended the single PR commit (7f5bc7c8), force-pushed; PR still discrete (4 files). Posted a terse reply comment (#issuecomment-4877656043). - Rebuilt via the SDK (exit 0) for the new hash. The runtime version-grep is confirmed by OpenWrt's PR CI, since the cross-built musl binary cannot run on this host.
- Scope decision: we will NOT touch net/udpspeeder (operator). Removed the "happy to send a separate PR" offer from the PR comment (edited #issuecomment-4877656043); the sibling's same two init bugs are left alone.
2026-07-03 — OpenWrt PR #29901: 32-bit ARM build fix in the fork
- The PR's multi-arch CI (openwrt shared
Feeds Package Test Build) ran and failed only onarm_cortex-a15_neon-vfpv4andarm_cortex-a9_vfpv3-d16; aarch64, mips_24kc, x86_64, i386, powerpc, riscv64 all passed. Root cause was in the fork source, not the package:xor_spe.Smarked the stack non-exec with.section .note.GNU-stack,"",@progbits. On 32-bit ARM@starts a line comment, so gas ate@progbitsand reported "junk at end of line, first unrecognized character is,". aarch64/mips/ppc/riscv/x86 do not use@as a comment char, which is why only 32-bit ARM broke. - Fix (universal, per call/0005 — build adjustments live in the fork): spell it
%progbits. gas accepts%for the ELF section type on every target (verified locally: x86 g++ assembles both@progbitsand%progbits), and%is the required form where@is a comment. Fork commitc2b3759onbranch_libev. - Propagation: re-pinned
.host-softwareudpspeeder-simd → c2b3759; bumped the PR'snet/udpspeeder-simd/MakefilePKG_SOURCE_VERSION → c2b37590 and refreshed PKG_MIRROR_HASH → d6f564823fa0217788bc937e06c71266bfa3a457d0803912b2d10bcb9957d3ba (amended the single PR commit, force-pushed; PR head 5d8ed050). Kept the fork commit's Claude co-author trailer; the packages commit has none (call/0003). - Mirror-hash regen recipe (local x86_64 SDK): set PKG_MIRROR_HASH to 64 zeros, run
make package/<pkg>/download V=s; the download clones, packs the reproducible git tarball, and printsgot <real-hash>.skipdoes NOT work for a git-proto source (the github archive downloader refuses without a real sha256). Then compile to confirm.
2026-07-17 — OpenWrt PR #30015 (net/udpspeeder) + timeout fold into PR #29901
- Opened https://github.com/openwrt/packages/pull/30015 (net/udpspeeder) against
openwrt/packages:master from connollydavid/packages:udpspeeder-toolchain-flags. Two atomic
commits, PKG_RELEASE 3→4→5:
udpspeeder: build with the toolchain CXX and flags: new patch 010-build-with-toolchain-flags.patch builds the cross target with $(CXX) and honours $(CXXFLAGS)/$(LDFLAGS), makes gitversion overridable; package drops the Build/Prepare sed hacks, injects the version via MAKE_FLAGS, adds PKG_BUILD_FLAGS:=no-mips16.udpspeeder: pass the timeout option to the binary: addsprocd_append_param command --timeout "${timeout}"after --mtu in files/udpspeeder-init.Fixes: .../issues/18955auto-closes on merge. The option was validated and shipped in the sample config (tunnel2) but never passed to the binary.
- no-mips16 is REQUIRED, not cosmetic: honouring CXXFLAGS pulls -mips16 onto
mips_24kc/mipsel_24kc, where C++ std::atomic emits a
syncbarrier MIPS16 cannot encode ("opcode not supported on this processor: mips2 (mips2) 'sync'"); no-mips16 strips it (MIPS32). - Non-drift confirmed from binary help:
--timeout ... unit: ms, default: 8ms; UCI schema default is 8, so wiring it is a no-op at the default and just makes the advertised option work. - Verified locally via snapshot SDK on all ten CI arches (scratchpad/build_all.sh): all pass; both mips controls (without no-mips16) fail on the sync opcode, proving necessity.
- PR #29901 (net/udpspeeder-simd) had the identical latent timeout bug. Folded the same
--timeoutline into the singleudpspeeder-simd: add packagecommit (amend, NO PKG_RELEASE bump since 1.0.0-1 is unreleased); force-pushed connollydavid:udpspeeder-simd → 3540c71ce. Posted a terse alignment comment (#issuecomment-5002468098) referencing #30015. - Hygiene: packages commits carry NO Claude trailer (call/0003); sign-off = author = David Connolly david@connol.ly. #30015 rebased onto current upstream/master (net/udpspeeder untouched upstream); #29901 left on its older base (PR diff is clean vs merge-base and CI builds on the snapshot SDK, so no rebase needed and none requested). .host-software NOT re-pinned (feature-branch work; the master pin is unchanged).
2026-07-17: #30015 "Dirty patches detected" — patches must be quilt-refreshed
- All ten Feeds Package Test Build jobs failed at
make package/udpspeeder/refresh: the CI requires each patch byte-identical to quilt's canonical output. Our patch applied at exact offsets with no fuzz, yet still failed. - Root cause: OpenWrt refreshes with
QUILT_DIFF_OPTS="-p" quilt --quiltrc=- refresh -p ab --no-index --no-timestamps(openwrt/openwrt include/quilt.mk), and GNU diff-pappends function context to hunk headers (@@ -51,7 +51,7 @@ cygwin:git_version).git format-patchoutput lacks the suffix, so the refresh rewrites the file and the git-diff check trips. - Fix recipe: extract the pinned source (
git archive 20230206.0), set uppatches/+ series,quilt --quiltrc=- push -a, run the refresh command above, copy the refreshed patch back. Verified idempotent: a second refresh reports "is unchanged". Quilt preserves the git header (From/Subject/Signed-off-by/diffstat) untouched. - Folded into the toolchain commit via
commit --fixup+GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash; range-diff showed only the two hunk-header lines changed, timeout commit byte-identical (=). Force-pushed7a76af810...075f4a7e0(lease + force-if-includes). - Lesson: any patch destined for openwrt/packages should be generated or round-tripped through quilt with those exact args before committing, not taken raw from git format-patch.
2026-07-31 — upstream merged our toolchain-flags PR; taking it in both packages
- wangyu-/UDPspeeder merged https://github.com/wangyu-/UDPspeeder/pull/356 (686d6079), then
reshaped it in b6a1b594:
crosswas restored to the hardcoded${cc_cross}, and the change landed as a NEW target instead:cross_cxx: ${CXX} -o ${NAME}_cross -I. ${SOURCES} ${FLAGS} -O2 ${CXXFLAGS} ${LDFLAGS} ${LDLIBS}. Three deltas vs what we sent:-O2moved BEFORE${CXXFLAGS}(so a caller's -Os wins instead of being overridden),-lrtdropped,${LDLIBS}added. Ourgitversion ?=hunk survived verbatim, so MAKE_FLAGS version injection still works. - Consequence for the merged net/udpspeeder patch: its second hunk (the
gitversion ?=line) can no longer apply to any source at or past b6a1b594, since upstream already has it. So the patch had to be dropped as part of a version bump, not deferred. -lrtis a no-op on OpenWrt, PROVEN not assumed: musl'slibrt.ain the toolchain is an 8-byte empty archive (clock_gettime is in libc), and the packaged binary built withLDLIBS="-lrt"and without it are byte-identical (sha256 502163f5...). NEEDED is libstdc++/libgcc_s/libc either way. So neither package passes -lrt; a toolchain that needs it can pass LDLIBS. LeftDEPENDS:=+librtalone in both (inert: musl's libc provides it) rather than widen the diff.- The 3-year source bump 20230206.0 -> b6a1b594 is WIRE-SAFE, also proven: the only functional
change is swapping the in-tree
crc32hfor Stephan Brumme'scrc32_fast. Both are standard zlib CRC-32 (poly 0xEDB88320, init ~0, final complement); a comparison harness over every length 0..2048 x 8 random buffers matched exactly, and both give 0xCBF43926 for "123456789". The rest is typo fixes, anis_vaild->is_validrename, and a djb2/sdbm loop rewrite that removes a one-byte over-read without changing the hash. - OpenWrt version convention for an untagged git source is
<base>~<shortsha>+ a full-sha PKG_SOURCE_VERSION. Careful:~sorts BEFORE end-of-string (Debian style), so20230206.0~<sha>would sort BELOW the released 20230206.0 and break the upgrade path. Used20260731~b6a1b594(commit date as base) so it sorts above. - Fork: added the same
cross_cxxtarget (commit 81fde6c, tagged v1.0.1, pushed); kept our cross/cross2/cross3 as-is since this fork deliberately dropped the hardcoded compiler paths. Re-pinned .host-software to 81fde6c.
2026-07-31 — correction: PR #29901 has no test.sh (the 2026-07-03 entry is wrong)
- The 2026-07-03 entry above says "#test-script done ... test.sh added beside the Makefile;
exercised by the OpenWrt PR CI runtime test", and
.host-task-receiptsrecords the same evidence forplan/0000#test-script. Both are FALSE as of now, and have been since 2026-07-04. - Traced through the branch reflog: test.sh survived to b7ef6490 (2026-07-04 21:31) and was gone
in the very next amend, b9e331ec (2026-07-04 23:16), the one that fixed the Build/Prepare
double-expansion. It was dropped accidentally, not by decision, and every later force-push
carried the loss forward. The PR head has carried three files (Makefile, config, init) ever
since, so the CI arches flagged
runtime_test: truehave had nothing to run. - Operator decision on 2026-07-31: LEAVE IT OUT rather than restore it, so the package ships
without a runtime test. BKPepe had questioned the script anyway ("Do we need this script at
all? This should be covered by generic testing"). The
#test-scriptreceipt is therefore stale and should be re-dispositioned by the operator (it is a tool-written ledger; not hand-edited here). - Lesson: an amend that fixes one file can silently drop another. Diff the file LIST against the PR head after every amend + force-push, not just the file contents.
2026-07-31 — OpenWrt packaging idioms, and how to test under qemu properly
- VERSION FORM for an untagged git source: do NOT hand-write PKG_VERSION. Set PKG_SOURCE_DATE
plus a full-sha PKG_SOURCE_VERSION and let include/download.mk derive it:
PKG_VERSION := $(subst -,.,$(PKG_SOURCE_DATE))~$(call version_abbrev,$(PKG_SOURCE_VERSION)), giving2026.07.31~b6a1b594(version_abbrev = 8 chars; it returns the FULL sha under DUMP, so the feed index shows the long form while the build uses the short one). Feed usage: 96 packages use PKG_SOURCE_DATE vs 3 that hand-write a~version. Changing the version form changes PKG_MIRROR_HASH, since the tarball's inner subdir is $(PKG_NAME)-$(PKG_VERSION). - UPSTREAM UDPspeeder TRUNCATES THE VERSION: print_help does
strncpy(buf, gitversion, 10), so--help(the flag OpenWrt's generic check finds) shows ten characters only. The old version20230206.0was ten characters exactly, so the check passed by luck for years. Any longer version needstest-version.sh, the idiomatic override (70 in the feed). Our simd fork already prints the full version, so only net/udpspeeder needs the override. - DEPS ARE DERIVED FROM THE ELF: include/package-pack.mk runs scripts/gen-dependencies.sh, which
reads NEEDED and FAILS the build for a linked library that is not declared. It never checks for
extras, so a declared-but-unlinked dep is inert. librt and libatomic were both provably inert:
70/70 builds and packet tests across all 35 published architectures with both removed, no binary
NEEDs either, and no
__atomic_*/__sync_*symbol anywhere. On musl, Package/librt/install copies nothing at all (ifneq ($(CONFIG_USE_MUSL),y)). - RUNTIME TESTS, the idiom:
test.sh <pkgname> <version>runs INSIDE a target-arch container (binfmt + qemu-user-static) with the package installed.pre-test.shinstalls test deps (apk add socat). A package-specific test.sh REPLACES the generic tests (version check included), and test-version.sh is only consulted by the generic path, so test.sh must assert the version itself. BusyBox in OpenWrt is built with NC_SERVER=n and NC_EXTRA=n, so nc can neither listen nor exec: use socat via pre-test.sh. Do not use a fixed sleep, the container is emulated; retry the payload instead. - QEMU CPU MODELS, do not trust the default. powerpc_8548 (CPU_TYPE 8548, e500v2) fails under the
default 32-bit PPC model with an illegal instruction, and it is NOT the package: the faulting
word disassembles to
iseleqinside musl libc, which holds 63 isel instructions while our binaries hold none.-cpu e500v2_v10runs it. Control: the musl loader alone faults under the default and prints its version under e500v2_v10. Every arch now names a verified model in .github/scripts/arch-map.tsv; where qemu-user lacks the exact CPU the closest SUPERSET is used (xscale and fa526 -> arm926, cortex-a5 -> cortex-a7, pentium-mmx -> pentium2, pentium4 -> n270, since a weaker model invents failures and a richer one hides them and skews feature detection). - PROBE BY OUTPUT, NOT EXIT STATUS: musl's loader prints its version and exits 1. A first pass at CPU discovery keyed on exit status and reported every architecture broken. Judge the output.
- SCOPE: OpenWrt's CI runtime-tests only aarch64_generic, arm_cortex-a15_neon-vfpv4, i386_pentium-mmx, mips_24kc and x86_64; powerpc_8548 is runtime_test:false. So the CPU-model work is OUR local lane's concern, not something the OpenWrt PR depends on.
- arm_fa526 (gemini) is BUILD-ONLY under qemu-user, and the cause is the emulator, not the
package. That target compiles plain ARMv4: its binary holds ZERO bx/blx, where the ARMv5 build
of the same package holds 133, so it cannot enter Thumb code at all. qemu-user always maps a
Thumb vdso, so the call to __vdso_gettimeofday runs Thumb bytes in ARM state and branches to a
wild address; the in_asm trace ends in
IN: __vdso_gettimeofdaydecoding 46c046c0 (Thumb nop;nop) as ARM and branching to 0x40b2838c, exactly the SIGSEGV si_addr. Proven not ours two ways: the CURRENTLY SHIPPED 20230206.0 package fails identically under -cpu arm926, and a trivial C++ program against the same musl and libstdc++ runs fine there. Real hardware never meets it, since the Linux ARM vdso needs ARMv7 and a gemini kernel supplies none, leaving musl on plain syscalls. qemu-user has no flag to withhold the vdso, so the packet test is skipped for that arch rather than run under a v7 model that would hide real mismatches.
2026-07-31 — udpspeeder-simd crashed on x86_64 without SSSE3 (found by faithful qemu models)
- lib/fec.cpp initialised
addmul1_x86_fn = addmul1_ssse3and only ever RAISED it to AVX2 or AVX-512. Nothing probed SSSE3, and x86_64 does NOT imply it: AMD K8/K10 (Athlon 64, Opteron, Phenom, Phenom II, Athlon II) lack it, as do early 64-bit Intel parts; AMD added it with Bulldozer in 2011. Those CPUs took SIGILL on the first PSHUFB in the Reed-Solomon inner loop, so the tunnel died on its first FEC batch. Upstream udpspeeder was unaffected. - Only visible under a faithful CPU model. Every earlier sweep used qemu's DEFAULT x86_64 model
(rich), and OpenWrt's own CI runtime-tests x86_64 on a GitHub runner that has SSSE3/AVX2, so the
bad path never ran. It appeared the moment the map named
-cpu qemu64(the AMD64 baseline). - Fix (fork v1.0.2, f3834047): pointer starts at scalar and rises only as CPUID allows, via
cpu_has_ssse3() (leaf 1 ECX bit 9) and cpu_has_sse2(); new SSE2 addmul1 sits beneath SSSE3, so a
CPU without PSHUFB still vectorises. SSE2 has no byte shuffle, so it multiplies by repeated
doubling:
_mm_add_epi8(x,x)for the shift and_mm_cmpgt_epi8(0,x)to select the reduction. The reduction constant is read fromgf_mul_table[2][0x80], not hardcoded, so it follows the field. Measured 1.73x scalar at 1500B (913 vs 1579 ns); ssse3 is 9.8x. - Also hardened packet_cook.cpp: its AVX2 tier was set from CPUID leaf 7 EBX bit 5 ALONE, with no OSXSAVE/XCR0 check and no max-leaf check. Now mirrors the correct probe.
- Test method worth reusing:
bench_addmul1_force(name)pins one implementation so test_fec can hold EVERY path against scalar over all 256 multipliers and sizes hitting each loop and tail. Ran the suite underqemu-x86_64 -cpu qemu64|Nehalem|Haswellto exercise sse2/ssse3/avx2. AVX-512BW cannot be verified this way: qemu-user leaves ZMM state disabled in XCR0, so the OS-support check correctly declines it. - HARNESS BUG worth remembering:
find ... | head -1picked a STALE build dir from an earlier session in a reused SDK, so one x86_64 result described a binary nobody had built that run. Select by newest mtime (-printf '%T@ %p'| sort -rn). Fresh CI SDKs never hit this; local reused SDKs do.
2026-07-31 — qemu64 is not the x86_64 floor; the first Opteron is
Asked whether we are aligned with the very first x86_64 chip, and the honest answer needed evidence at both ends rather than an assertion. Findings worth keeping:
-cpu qemu64GRANTS SSE3. It caught the SSSE3 crash, but it is not the architectural floor and never was. The floor is-cpu Opteron_G1,-sse3: qemu's model of the AMD Opteron 240 of 2003, the first x86_64 silicon, with SSE3 subtracted for the earliest stepping. SSE3 only arrived on K8 revision E in 2005. The arch map's x86_64 row now names that model.- Compile-time floor is already right: the OpenWrt x86_64 toolchain defaults to
-march=x86-64with sse3, ssse3, sse4, cx16 and popcnt all DISABLED (gcc -Q --help=targetproves it, andCONFIG_TARGET_OPTIMIZATIONis only-Os -pipe, so nothing raises it). XGETBVIS NOT BASELINE. It arrived with XSAVE in 2008, so a K8 faults on it. Our probes check CPUID leaf 1 ECX bit 27 (OSXSAVE) first, and the EMITTED code keeps the order: every xgetbv sits behindbt $0x1b,%ecx/jae. Verified in the disassembly, not just the source. Worth re-checking after any compiler bump, since the guard is a branch the optimiser could in principle move.- CRC32C's hardware path is behind
crc32c_has_hw()(leaf 1 ECX bit 20, SSE4.2) and falls back to the slicing-by-8 table, so it is safe on K8 too. - Static screen worth reusing, the x86 analogue of isa-check.sh: disassemble and list every
mnemonic outside the baseline together with its containing function.
udpspeederholds ZERO;udpspeeder-simdconfines all of them toaddmul1_{ssse3,avx2,avx512},xor_tile_{avx2,avx512}andcrc32c_hw, each reached only through CPUID. That is the property to hold, not "it ran". - Dynamic proof: both packages pass the packet test under
Opteron_G1andOpteron_G1,-sse3, and test_udpspeeder under those models selects sse2 and agrees with scalar over all 256 multipliers, declining ssse3/avx2/avx512bw. - Reading tests through a grep can invent a hollow section: filtering on "addmul1" hid the tier results, whose pass lines read "sse2 agrees with scalar". Read the section, not a keyword.
2026-07-31 — the x86 lacuna: OpenWrt has three x86 package arches, not one
Asked whether x86 coverage is complete. It was not, and the gap was in the emulation models rather than in the architecture list.
- OpenWrt publishes FOUR x86 subtargets (64, generic, geode, legacy) but THREE package
architectures. geode and legacy declare no CPU_TYPE, so both fall to the i386 default of
pentium-mmx(include/target.mk). One row covers the pair; generic is pentium4; 64 is x86_64. - BOTH i386 models were too rich, the same failure as qemu64 on x86_64:
n270(Atom) HAS SSSE3, so the i386_pentium4 job selected the SSSE3 path and never once ran the SSE2 tier a real Pentium 4 uses. Nowpentium3,+sse2, which is exactly a P4's ISA.pentium2HAS CMOV, which a Geode GX/LX and a Pentium MMX do not. Nowpentium,+mmx.
- qemu-user genuinely gates these, proven by one-instruction controls rather than assumed:
pentium2runs cmov and faults on pshufb;pentium2,-cmovandpentium,+mmxfault on cmov;n270runs BOTH cmov and pshufb;pentium3,+sse2runs movdqa and faults on movddup. Feature subtraction/addition on a stock model works, so compose the model instead of borrowing one. - Static screen of the geode build: nothing above the floor outside CPUID-guarded functions. The
one CMOV lives inside
addmul1_ssse3, which GCC is free to emit becausetarget("ssse3")implies P6, and it only ever runs on a CPU that has SSSE3 and therefore CMOV. Not a defect. - All three x86 arches pass both packages under the tightened models.
mips_4kecandriscv64_riscv64DO have package directories on the buildbot, so the listing alone suggests 37 arches. They are frozen: last built April and July of 2025 against zlib 1.3.1, where a live arch carries a build from this month. Check the DATES, not the directory.
2026-07-31 — MMX helps the XOR and hurts the multiply; measure per path
"MMX is still SIMD" — and OpenWrt's lowest x86 arch (i386_pentium-mmx, from geode and legacy) has MMX and nothing above it. So the fork was taken down to that floor. Two opposite results, and the lesson is that "add a lower SIMD tier" is not one decision but one per kernel.
- MMX addmul1: WRITTEN, MEASURED, REVERTED. The repeated-doubling multiply costs ~5 ops/byte at MMX's 8-byte width, against ONE L1-resident load per byte for scalar, which indexes a single 256-byte row of gf_mul_table for a fixed c. Measured on a real 32-bit build: mmx 0.55x scalar (3166 vs 1738 ns at 1500B); sse2 1.70x. SSE2 only wins by doubling the width. The floor keeps the scalar table, which is correct AND faster.
- MMX xor_tile: KEPT. XOR is one op per width, so width converts straight into throughput. Against the four-byte word path i386 actually had: mmx 1.46x, sse2 3.08x.
- The bigger find: packet_cook.cpp's ENTIRE x86 SIMD block was
#if defined(__x86_64__), so every 32-bit build used the 4-byte word loop — including i386_pentium4, which has SSE2. x86/generic is a commonly used target, so that was a 3.08x left on the floor for years. fec.cpp had already been widened to i386; packet_cook.cpp had not. When widening one file's ISA guards, grep for the same guard elsewhere. - MEASURE 32-BIT MMX ON A 32-BIT BUILD. On x86_64 GCC emulates __m64 with SSE (TARGET_MMX_WITH_SSE), so an x86_64 measurement of MMX is not MMX. Build with the OpenWrt i386 toolchain and run the binary natively (WSL2 runs i386 ELF fine); qemu timings are useless for ISA ratios.
- EMMS is mandatory: MMX registers alias the x87 stack and the tunnel does FP work in its timers.
- Renaming a force() tier label silently turns the reference call into a no-op, so the test compares a tier against itself and always passes. Renamed tier 0 "scalar"->"word" and had to fix the test's reference call in the same breath. A hollow green, exactly the shape call/0035 warns about.
Dispatch is a separate claim from implementation, and it needed its own lane
The tier comparisons pin a path and hold it against a reference. That says nothing about which path the dispatcher CHOOSES, and the choice is what the SSSE3 fault broke. A CI runner carries every feature, so it always chooses the top path; no runner-only job can see the bug class.
- Three runtime dispatchers exist, not one: addmul1 (fec.cpp), xor_tile (packet_cook.cpp) and crc32c (crc32c.h, an SSE4.2 probe resolving a function pointer on first call). All three now report their choice through bench_*_auto() and are held to EXPECT_ADDMUL1 / EXPECT_XOR_TILE / EXPECT_CRC32C.
- The auto hook must RE-DERIVE, not read back. bench_addmul1_force() overwrites the pointer, so the reporter re-runs the real selection (addmul1_select(), extracted from init_fec) and the selection now resets to scalar first rather than only raising. Reading the pointer back would report whatever a previous test pinned.
- QEMU TCG IMPLEMENTS NO AVX-512 AT ANY CPU MODEL (checked on qemu 11.0.2): -cpu Skylake-Server warns "TCG doesn't support requested feature ... avx512bw", clears the CPUID bit and leaves XCR0 at 0x207 rather than 0xE6. So the AVX-512 path cannot be exercised under emulation at all, on any model, ever. That row instead proves the gate DECLINES on a part whose model name says otherwise. AVX-512BW correctness is verified only when a GitHub runner happens to land on an Intel host.
- The OpenWrt i386 toolchains default to -march=i486, so the march must be named explicitly to reproduce what OpenWrt ships (pentium-mmx for geode/legacy, pentium4 for generic).
- A pentium-mmx-built binary runs clean on qemu's 486 model and correctly selects scalar/word/sw, so the bottom rung of the ladder is reachable and tested even below OpenWrt's own floor.
- The Makefile forces
export STAGING_DIR=/tmp/, which overrides the environment, so the OpenWrt toolchain wrapper only warns and no STAGING_DIR needs setting for amake test-crossbuild. It matters only when invoking the cross gcc directly.
The SPE XOR was never shipped, and the package could not have told us
The e500v2 SPE unit does the tile XOR 64 bits at a time. That path needs SPE=1 on the make line,
and net/udpspeeder-simd passes only cross_cxx gitversion=..., so every mpc85xx build ever
shipped has used the word path. Confirmed by objdump: with the flag, 20 SPE opcodes; without it, 0.
- GCC defines no e500 macro. GCC removed SPE support, which is why the path is a hand-written
xor_spe.Splus-Wa,-msperather than intrinsics. Diffing the predefines of-mcpu=8548against-mcpu=464fpyields only soft-float proxies (__NO_FPRS__,_SOFT_FLOAT,_SOFT_DOUBLE,__NO_LWSYNC__). Keying on those would shipevxorto a soft-float classic PowerPC and fault, the SSSE3 bug again. So auto-detection from the compiler is not available. - The target says it instead, and the software can read it. OpenWrt's
MAKE_VARSexportsCXXFLAGS="$(TARGET_CXXFLAGS) ...",TARGET_CXXFLAGS = TARGET_CFLAGS, and that carriesCPU_CFLAGS_$(CPU_TYPE), i.e.-mcpu=8548. So the makefile can read the core from the flags it is already handed, and the package needs no CPU conditional. Note the fork's own cross targets take the march insideCC, notCXXFLAGS, so the filter searchesCC CXX CFLAGS CXXFLAGS. - 8548 alone is too narrow. OpenWrt maps these 32-bit PowerPC types: 603e, 8540, 8548, 405, 440,
464fp. 8540 is e500v1 and also has SPE (v1 lacks only double-precision SPE FP, which this does
not use). No published target uses 8540 today. Match both, with
filter(whole words) notfindstring(substring). - The XOR round-trip cannot catch a wrong XOR, only a missing one. It asserts
changed && restored. XOR is its own inverse, so a wrong-but-deterministic path still restores; a deliberate truncation was caught in exactly the degenerate cases where the XOR did nothing at all. The differential comparison against the word reference is what actually bites, verified by truncating an SPE call and watching that test alone fail across every tile, length and offset. - The sweep's spe row is red until the package bumps.
arch-map.tsvnow asserts SPE opcodes for powerpc_8548, but the sweep builds from the package's pinnedPKG_VERSION. It passes only once the package points at a fork release carrying the makefile detection, i.e. the pending v1.0.3 bump.
The aarch64 gap was ours alone, and the pins needed a negative control
Closing the NEON and ARM-CRC32 gaps required no OpenWrt change at all. Proof: building the fork
with the airoha (cortex-a53) toolchain and that target's own -mcpu yields 6 crc32c* instructions
and 9 eor v*.16b NEON ops. The code already ships; only the tests were absent.
__ARM_FEATURE_CRC32needs a named core. Measured across the OpenWrt aarch64 toolchains:-mcpu=cortex-a53,-a72and-a76define it,-mcpu=generic(armsr/armv8,CONFIG_CPU_TYPEgeneric) does not. So three of the four published aarch64 targets ship the hardware checksum. Debian'saarch64-linux-gnu-g++defaults to no core, which is why fork CI compiled the path out and[CRC32C hw vs sw agreement]skipped silently. CI now also builds-mcpu=cortex-a53and requires the comparison to have run, since a skip must not read as a pass.- A pin that does not switch is a hollow green.
bench_*_force()returning 1 while the path stays the same makes the reference and the candidate the same code, so the comparison passes vacuously. Both aarch64 pins were checked by deliberately faultingaddmul1_neonand the NEONxor_tileand confirming each named test fails. Never trust a new comparison without that control. - The tier tests used to leave the reference pinned. They ended with
force("scalar")/force("word"), so every round-trip test after them ran the reference path, not the dispatched one. Now they end withbench_*_auto(). This was already wrong on x86 and would have silently removed NEON from the aarch64 round-trips. - qemu-aarch64
-cpu cortex-a53does implement CRC32, unlike the AVX-512 case, so this gap is fully closable under emulation.
What the next feed build owes
Deferred on 2026-08-07, after the publish run went green: 111 jobs, no failures, two packages across 35 architectures and three release lines, each installed from the signed feed in a stock rootfs and refused without the published key.
- The banner's build date is the SDK's date, not the build's.
target/sdk/MakefilebakesSOURCE_DATE_EPOCHinto the SDK'sinclude/version.mkandinclude/toplevel.mkexports it; GCC honours that variable and expands__DATE__and__TIME__from it. So the published binaries report the date the SDK was cut:Jun 29 2026 12:59:20on 25.12 and main,Jul 24 2026 07:21:50on 24.10. One source reports two dates, and both read as a stale binary to someone filing a bug. Feeding the line a real timestamp is not the fix, since the determinism is OpenWrt's own reproducibility measure. Drop the line or relabel it;source commitand the version already answer what it is asked. - That edit is not local. The fork's copy sits in
main.cpp, and CI ties the tag toPROGRAM_VERSION, so touching the banner means a new tag, which means bothudpspeeder-simd-snapshotMakefiles take a newPKG_VERSIONand a newPKG_MIRROR_HASH(the tarball name follows the version) on all three feed branches. The stock package's line is upstream's own, reached only through010-note-snapshot-build.patch, so it is dropped there or not at all. - The pins are stale by one commit on each of four entries:
feed-maina22b5740,feed-25.12ebc5ab6c,feed-24.10e646bb33, the fork 0ba7c9f5. The.host-softwarecomment above the feed worktrees still says the feed "is not published at present and its workflow is disabled", which stopped being true when it went live. Correct it in the commit that re-pins. - No SDK is pinned, and the workflow runs itself weekly. Each build job resolves the newest
point release of its line at run time and verifies the SDK against that release's own
sha256sums; the schedule fires every Monday at 08:00 UTC. A cron run can therefore republish different binaries, from a different toolchain and carrying a different baked date, with no recipe change and no host commit. The checksum is provenance, not a pin. Decide whether the feed wants an SDK pinned per line, or whether floating with the release is what a snapshot feed should do. - The feed retires once openwrt/packages #29901 and the net/udpspeeder version bump offered upstream both land, since the official feed is then the better answer for either package and should not be left running beside it. That condition lived in the workflow's header comment until the comments were stripped, and this entry is now the only record of it.
The flashprog package, and why its programmer list is spelled out
Written 2026-08-08 for openwrt/packages issue #29591, which asks for flashprog beside the
flashrom package that #29679 had just updated to 1.7.0. One package, utils/flashprog,
targeting v1.5 (released 2026-02-13; v1.6-rc1 was tagged the day before this work and is a
release candidate). Branch flashprog in the packages fork, commit 5ca6daa.
- Selecting a programmer group also enables the ones upstream disables. flashprog offers
group_pci,group_usb,group_ftdi,group_serial,group_jlinkandgroup_internalas meson choices, which reads like the tidy way to express an OpenWrt config menu. It is not. Inmeson.buildthe selection test isgroups.contains(true) or 'all' in programmer or 'auto' in programmer and default, andandbinds tighter thanor, so group membership alone selects a programmer and its owndefault: falseis never consulted. Passinggroup_pcitherefore buildsatahpt("not yet working"),atapromiseandnicnatsemi("not complete nor tested"), all three of which upstream's Makefile sets tono. The package names all 32 programmers individually instead, which reproduces upstream's default set exactly and is what the long list in the recipe is buying. - Naming a programmer is a hard selection, and unavailable is a hard error. A named
programmer that meson cannot build on the target calls
error()and fails configure; a group-selected one merely reports "Not available on platform". So spelling the list out moves the architecture question from a silent skip to something the recipe must get right.cpus_raw_memcovers x86, mips, ppc, arm, aarch64, sparc, arc and e2k but not riscv64 or loongarch64, both of which OpenWrt publishes, andcpus_port_iois x86 only. The recipe gates on$(ARCH)accordingly. Checked againstinclude/meson.mk: OpenWrt maps i386 to x86, powerpc to ppc, mipsel to mips, mips64el to mips64 and armeb to arm, so those two are the only OpenWrt architectures the raw-memory list omits. - The dependency is
libpci, notpciutils.utils/pciutilsbuilds both; the flashrom package depends onpciutils, which drags in thelspcibinary and thepciidsdatabase for a package that only links the library. Also, flashprog defaultsuse_internal_dmito true, so unlike flashrom it needs nodmidecodeon x86. - libjaylink 0.3.1 in the feed is enough. Every one of the 30 functions and 9 constants
jlink_spi.cuses is in 0.3.1, and neither flashprog's meson nor its Makefile states a minimum. Upstream is at 0.4.0, whose NEWS lists meson support, more USB product IDs and a udevuaccesstag, and no new API. So the J-Link programmer costs one feed dependency and nothing else. The flashrom package offers no J-Link programmer at all. linux_gpio_spiis unreachable and was left out. Its group,group_gpiod, is read inmeson.buildbut is absent from theprogrammeroption'schoices, so it cannot be selected by name. It would also drag in libgpiod, which carriesDEPENDS:=@GPIO_SUPPORTand would make the whole package unbuildable on targets without GPIO support.- The license is GPL-2.0-only, not the
-or-laterthe flashrom package claims. 85 files carry "either version 2 or (at your option) any later version"; 46 carry version 2 with no later clause, includingspi.c,spi25.c,layout.c,linux_spi.c,linux_mtd.canddummyflasher.c. Upstream's ownmeson.builddeclaresGPL-2.0. - Verified on 124 jobs, all green (run 31266763904): 35 architectures across 24.10, 25.12
and snapshot, each asserting the exact programmer set for its architecture class plus the
package's file list and dependency list; five architectures repeated with all four config
symbols off, asserting libpci, libftdi1 and libjaylink absent and only
libcleft; and an install into a stock rootfs on each line running a dummy write, verify, read,cmpand erase. The assertions were proved against negative controls first: claiming aarch64 for an x86 build reportsrayer_spi,nic3com,nicrealtekandsatamvas wrongly built, and claiming the minimal config for a default build reports 13 failures. - Two CI failures that were not the package, both worth keeping in mind. The d1/generic
snapshot SDK was rebuilt between reading
sha256sumsand downloading the tarball, so the fetch now retries three times; a snapshot directory moves under you. And the branch-trackingopenwrt/rootfs:x86-64-openwrt-24.10image lists a kmods feed whose kernel hash has moved, soopkg updatereturns non-zero on one of six feeds. Rather than silence it, the job now reads the package's ownDepends:and requires each one to be present in the feed, which is the claim the job exists to make. - A worktree on the Windows drive cannot record the executable bit.
chmod +xfollowed bygit addleft the CI scripts at mode 100644, and all 123 jobs died at their first step with exit 126.git update-index --chmod=+xis the fix, and the general rule is that a mode change made under/mnt/chas to be set in the index, not the filesystem.
Correcting the flashprog entry: the design it describes is gone
The entry above, "The flashprog package, and why its programmer list is spelled out", described commit 5ca6daa and is now wrong in three places. It is left standing because this log is append-only; read this entry instead.
- There are no config symbols and no
Config.in. That design put fourdefault ybools in a menu, which meant the buildbot shipped the full package to everyone and the saving existed only for people already running menuconfig. The operator called it a mistake, correctly: OpenWrt is size-focused and the feed download is what most people install. - There are two packages, not one.
flashprogis the default variant carrying everything the architecture supports;flashprog-spicarries only the programmers that need no library. Both own/usr/bin/flashprog, so they conflict:flashprogdeclaresCONFLICTS,flashprog-spideclaresPROVIDES:=flashprog, the shapenet/chronyandlibs/libwebsocketsuse. Renaming the binary per variant, which is whatutils/flashromdoes, is wrong here because flashprog dispatches subcommands and its man pages carry its own name. - The programmer list is no longer spelled out, and the riscv64 gate is gone with it. The recipe
selects upstream's
group_serial,group_usb,group_ftdi,group_jlinkand, on x86,group_internal, naming onlydummy,linux_spiandlinux_mtd, which belong to no group we select. Measured against the hand-written list this costs 8 kB and three obsolete x86 programmers (atahpt,atapromise,nicnatsemi) and dropsrayer_spi, a parallel-port cable programmer no modern board can use. It buys a recipe a third the size that cannot break when upstream renames or narrows a programmer, because group selection is soft where a named programmer is a harderror(). The old entry's riscv64 and loongarch64 special case dissolved: the raw-memory programmers are now only ever added inside the x86 branch.
Two findings from that work are worth keeping.
- A group's membership is the real argument against groups, and it is about
DEPENDS.group_usbgainedch347_spianddirtyjtag_spiacross releases. A version bump can therefore add a programmer whose library the recipe never declared, and soft selection readsstaging_dir, so it activates only when something else happened to stage that library.ft4222_spialready shows the shape: it is ingroup_usbandgroup_ftdibut needs libusb, so selectinggroup_ftdialone links a library the package would not have declared. Declaring all four libraries unconditionally is what makes groups safe here. - flashprog needs
libpci, neverpciids. It calls only config-space, enumeration and filter functions, neverpci_lookup_name, and matches devices numerically against its own table. Proven three ways: no such call in the source, both PCI programmers reaching their expected errors in a rootfs with no/usr/share/hwdata/pci.ids, and pciutils' own documentation calling the database a name lookup table. Depending onlibpcirather thanpciutilstherefore avoidspciidsat 1,651,592 bytes installed, pluslibkmodand thelspcibinary.
Correcting the flashprog entry again: the two packages are co-installable, and why
The entry above still describes flashprog declaring CONFLICTS and flashprog-spi declaring
PROVIDES:=flashprog. That design is gone. It also called flashrom's rename-the-binary approach
"wrong here"; that judgement is reversed, and this entry says why.
- A versioned
PROVIDESmakes the name substitutable, which is not what it looks like. The operator ranapk add flashprog-snapshoton a real router and apk installedflashprog-spi-snapshotinstead. This is documented behaviour, not a bug: apk-package.5 says a versioned provide makes apk "treat it as-if a real package with the provided name is installed", so the provider satisfies a request for the provided name outright.provider-prioritycannot arbitrate, because it applies only to non-versioned provides. All three orientations of the provide were tried; none gives exclusion without also giving substitution. - OpenWrt's
CONFLICTSnever reaches apk.package-pack.mk:417writes it only into the opkg control file, and OpenWrt passes twelve--infokeys toapk mkpkg, none of them a conflict. apk expresses a conflict as a negative dependency (depends: !name), which OpenWrt never emits. So under apk the conflict half was silently absent while the provide half was fully active. - The fix is distinct install paths, so there is nothing to arbitrate.
flashprog-spiinstalls the same ELF as/usr/bin/flashprog-spi. The two packages now share no path, are co-installable, and carry noPROVIDESand noCONFLICTSat all. This is whatutils/flashromdoes with its five variants, and the earlier entry's argument against it (that flashprog dispatches subcommands and its man pages carry its own name) does not survive contact: the package ships no man pages, and argv[0] changes nothing about subcommand dispatch. - Local-file installs never exercise provider resolution. Every container test installed by
filename, which names an exact package, so the substitution could not appear. Only the operator's
apk add <name>against a published feed reached the resolver. A package carryingPROVIDEShas to be tested by name from a repository index, or it is not tested at all.
Three smaller findings from settling the recipe.
group_pciadds no programmer; it makeslibpcirequired. Upstream haslibpci = dependency('libpci', required : group_pci), so selecting the group turns a missing libpci from a silent programmer drop into a hard configure error. That is the whole reason to select it, and the recipe comment now says so rather than describing the off-x86 case.- The x86 gate is
CONFIG_TARGET_x86, not$(ARCH).target/linux/uml/MakefilesetsARCH:=x86_64without settingCONFIG_TARGET_x86, so the two predicates diverge. Gating the programmer groups on one andDEPENDSon the other selectedgroup_pciwhere libpci was never declared, and uml failed to configure. Both gates now readCONFIG_TARGET_x86. - Off x86,
internaldegrades tolinux_mtdwithout touching/dev/mem.internal_init()callstry_mtd()beforeprocessor_flash_enable()and before the#if defined(__i386__) || defined(__x86_64__)physmap block, so on a non-x86 build withLINUX_MTD_AS_INTERNALthe MTD path is reached first and succeeds on its own. This is what lets the commit message say the group is redundant off x86 rather than merely unsupported.
2026-08-10 — the tools this host runs are now pinned like the software it builds
Every tool reached through whatever the machine carried: node and allium from
~/.local, java from /usr/sbin, tla2tools.jar from a home share. A generated
pre-push hook had $HOME/.local/bin/node baked in, so it worked here and nowhere
else, and a stale target/release/host-lint six minor versions behind the binary
on PATH turned a green integration suite red until the cause was found.
.env now names each tool's version and in-tree path; tools/install-tools.sh
fetches them into .host-tools/ and verifies each sha256 before unpacking.
Recorded as call/0007. The general form belongs upstream in the template.
Two findings worth keeping. allium-cli does not install from crates.io at 3.4.2
or 3.5.0 without --locked, because it resolves a newer allium-parser whose
analyze_with_cross_module grew arguments. And host-lint before v0.13.0 failed
closed on any submodule gitlink, so no git submodule add could land in a gated
host; upstream had fixed it a month before this host noticed, which is the
argument for comparing a pin against origin/main before writing a patch.
2026-08-10 — this host must not reference an upstream tracker, and a rebase obliges a re-pin
A commit message pushed from this public host that carries owner/repo#N or a
github.com/<owner>/<repo>/(issues|pull|commit)/ URL makes GitHub post a
referenced timeline event on the target. Two host commits had already done it,
one through each form, so this governance repo surfaced inside an upstream thread
about the package.
The event cannot be retracted. Issue events are read-only in the REST API, and a
force push only makes the commit unreachable while GitHub keeps it addressable by
SHA. After the offending commit was rewritten out of this history, the API still
resolved it and the event still rendered. The remaining levers are a support
request, a delete-and-recreate of the repo, or making it private. Prevention is
therefore the whole remedy, so tools/crossref-check.sh installs as the
pre-push hook and refuses either form. It cannot gate an issue or pull request
body posted through gh, which stays a matter of care.
The same rewrite taught the second rule. A rebase strands every reference to what
it rewrote. Six pins in .host-software named commits that later force pushes
had made unreachable, and a fresh clone fetches reachable objects alone, so
software --materialize could not check them out and exited 2. The
reproducible-build lane had been red for a day, and it died two steps before the
build it exists to verify, so it never built anything at all. Reachability is the
test: the API resolves an unreachable object and proves nothing, while git ls-remote --heads lists what a clone will actually get. Both rules are now
priority rules in CLAUDE.md.
One more thing surfaced and is not yet fixed. Four host-lifecycle versions read
this project: the tools/host-lifecycle submodule at 0.35.1, which the mdBook
and reproducible-build workflows build from, .env at v0.50.0 for local use, and
a git rev in prose.yml. Version 0.35.1 parses only repro-exempt, so it reads
the repro-waiver lines an applied ledger entry introduced as absent and reports
host-lint as DRIFT, and it has no book --print-mount, which is the mdBook
failure. A bump of the submodule to the v0.50.0 commit clears both. Note what the
lane proves once it goes green: three components record no artifact and host-lint
is waived, so every line is a skip or an exemption and no reproducibility is
established yet.
Correcting the skew entry: the fix was .env, not a submodule bump
The entry above proposed bumping the tools/host-lifecycle submodule. What
landed is narrower and treats the cause instead of the symptom. All three
workflows now install the .env pin and source .env for the path, so one
binary answers for CI and for the local shell. Prose and mdBook went green at
once, and the reproducible lane reports WAIVED host-lint repro-waiver (call/0002) with the other three skipped.
How the pins stayed stale is the part worth keeping. The submodule pointer has
exactly one commit in its whole history, 72a8e2e, which wired it at v0.35.1,
and nothing ever moved it, because software --check audits the components in
.host-software and not the tools under tools/. prose.yml carried its own
inline rev at v0.30.1. Every command run by hand used the newest binary
available, so CI alone ever saw the old ones. A second source of truth is not a
pin; it is a way for two answers to disagree without anyone noticing.
One skew survives on purpose. The submodule still supplies the lifecycle skills
that .claude/skills/ links to, so those remain v0.35.1's while the binary is
v0.50.0, and the release phase has no skill there at all. Bumping the
submodule is what closes that, and it is a ledger-ordered action rather than a
free edit.
Read the green with care. v0.50.0 prints 0 builds verified here ... nothing to attest, which is the honest summary: no component reproduces yet.
Correcting the upstream-reference entry: the closer belongs in the pull request body, not the commit
The scoping bullet the entry above carried into CLAUDE.md said a commit in the
packages worktree keeps its Closes: trailer because upstream requires it.
Checked against openwrt/packages on 2026-08-10, that claim is wrong. Nothing
requires it, and the tree's convention is the closing keyword in the pull
request body, where it fires once at merge, rather than in the commit, where
every push of an amended branch re-posts a reference on the issue. Four of the
five most recently merged pull requests that closed an issue put the keyword in
the body; the one exception (fluent-bit) used a commit trailer. openwrt/packages
PR 30186 is the direct precedent: a new package, merged the same day, that
closed its request issue from the body with no issue reference in any of its
commits. The openwrt-package-commit-style manual in host-lint-openwrt
states the rule and carries the corpus measurement behind it.
CLAUDE.md's scoping bullet is corrected to match. The flashprog commit on openwrt/packages PR 30228 drops the trailer, and at push time the body's "Requested in #29591" becomes "Closes #29591", so the merge still closes the issue.
Host-repo commits carry the DeepSeek co-author trailer
Operator direction on 2026-08-10: the host repo's co-author trailer is
Co-Authored-By: DeepSeek V4 Flash 0731 <noreply@www.deepseek.com>, not the
Claude one. The packages worktree still carries no co-author trailer at all,
per call/0003; only the name the host trailer carries has moved.
2026-08-10 — v1.1.0 released: the DNS lease manager, the first minor bump
The first minor version bump of udpspeeder-simd shipped as v1.1.0, carrying the
DNS lease manager (plan/0001). The client's -r now accepts a hostname: a
single-header, allocation-free, nonblocking DNS Locator-Hint Cache
(dns_lease_mgr.h) resolves it, leases the candidate IPs for an effective TTL,
refreshes before expiry, serves the last-known candidates while a refresh fails
(the stale window, default 1 hour, 0 = never expire), falls back to TCP on
truncation, and re-points the tunnel with a second connect() on the same fd
so the io_uring multishot and ev_io watcher stay valid. ECONNREFUSED on the
remote recv path force-refreshes; a FIFO dns-refresh command does it by hand.
Server mode rejects hostnames in -r.
Two decisions were recorded: call/0008 (versioning: minor = feature, patch =
fix, the tag is the release, the banner must equal the tag) and call/0009 (the
header is C++-compatible — every negative constraint of the C11 spec preserved,
dialect shifted to the CXX-only build). The release process ran as the house
process: fork commit 36641bc tagged v1.1.0, CI (now including the new allium
and tlc spec lanes, which gate the release job) published seven static
binaries, .host-software re-pinned, and the release receipt flipped from a
stale skip to done (authorization plan/0001).
The spec lanes are the notable new machinery in the fork's CI:
.github/workflows/ci.yml gained an allium job (allium-cli 3.5.0 +
host-lifecycle pinned, check/analyse/plan + obligations --strict-discharge)
and a tlc job (tla2tools.jar v1.7.4, hash-pinned like bench/sde.lock). The
obligations checker resolves test:<name> by fn <name>(, a Rust convention;
the C++ bench tests carry a /* fn <name>(...) */ marker comment before each
test function so the checker brace-matches the real body. TLC writes
spec/DNSLease_TTrace_* and a states/ dir on runs; both are gitignored.
2026-08-11 — the DNS lease manager verification gaps are closed
A review of the v1.1.0 verification found five gaps; all are now closed. The
DNS response parser was never fuzzed (the fuzz target only covered the packet
decoder), so bench/fuzz_dns_lease.cpp now feeds mutated responses to
dns_lease_parse_response under ASan/UBSan — a bounded random driver and a
libFuzzer target (make fuzz-dns / fuzz-dns-libfuzzer), wired into CI. The
stale_max_ms == 0 sentinel (serve stale indefinitely) was untested on both
lanes: a C++ case now advances a stale lease ten days and asserts it never
gives up, and the TLA+ StaleGiveUp gained the StaleMax > 0 guard the header
already had, with a second model instance (MC-stale0.cfg).
The live runtime paths were never exercised end to end: the create-on-first-
lease socket, the re-point on a candidate change, and the TCP fallback on a
truncated answer. A python DNS stub (bench/dns_stub.py) on port 53 plus a
driver (bench/dns-live-test.sh) now run the real client against real servers
in CI, and the Windows build is exercised under Wine (make test-mingw +
wine64), compiling the header's Windows PAL for the first time.
Two pre-existing fork bugs surfaced: lib/fec.cpp cast a pointer to long
(32-bit on Windows x64 -> precision loss), fixed with (intptr_t); and
xor_spe.S kept its comment outside the HAVE_PPC_SPE guard, so the MinGW
assembler choked on it — the guard now covers the whole file. The live lane
also needed its resolv.conf written via a bind mount (a runner's
/etc/resolv.conf is systemd-resolved's and not writable even as root). The
obligations checker's fn <name>( marker convention (Rust-oriented) applies to
the new test functions too.
2026-08-11 — branch_libev split: udpspeeder-only vs the feed, and the persona-in-commit rule
The fork's branch_libev carried two streams interleaved in one linear chain:
39 commits of OpenWrt feed-publishing work (the d0e497a..03b8369 span) sat
between the pre-feed udpspeeder work and the v1.1.0 DNS-lease run. The branch
was split by rebuilding both streams on the pre-feed tip ef92d69:
branch_libevis now udpspeeder-only: pre-feed history + the 4 banner/ version commits (2f8ad9d..b4ef0d9, tagged v1.0.4/5/6) + the 5 DNS-lease commits, re-rooted ontoef92d69. New tipe33aa71. The feed files (.github/scripts/*,package-feed.yml) are gone from the branch.feed-snapshotis the new feed branch:ef92d69+ the 29 feed-machinery commits only (no banner, no DNS). Tip7acdf4e. No tags live on it.
The re-root rewrote the DNS commits, so the v1.1.0 version-bump commit is now
ba6e4ab (was 36641bc). The four v1.* tags were NOT moved: the fork has a
tag-protection rule blocking in-place tag updates (deletion and creation both
pass; only update is blocked — GH013 "Cannot update this protected ref"). The
tags stay at their old commits, which remain reachable through the tag objects
and feed-snapshot's history. The release receipt's evidence (tag v1.1.0 at 36641bc) is still literally true because the tag never moved. Re-pinning the
host to the new tip was done in the same session; the tag/pin pair is now
forked (tag at the old commit, pin at the new tip) by the protection rule's
choice, not by oversight.
The rule that fired here: a commit message is the record of who did what; a
persona is a planning fiction, not a doer. Commit 8b67771 opened its body
"Samira's lane is now full" — a persona named as the author of verification
work. Rewritten as 7da6e1f ("The DNS response parser gets the fuzz
treatment...") with the trailer kept. The cast-room commits that introduce or
revise a persona may name it (that is their subject); a software-work commit
may not attribute the work to one.
2026-08-11 — canonical branch renamed branch_libev → main
The udpspeeder-simd fork's canonical branch moved from branch_libev to main
(the rename the split had been leading to). The fork's main is at 845df57
(the ci.yml rename commit), the GitHub default branch is main, and remote
branch_libev is deleted. feed-snapshot keeps its name.
The rename surface, all moved in the same pass: the 5 branch_libev
references in .github/workflows/ci.yml (the push/pull_request trigger filters
and the benchmark-storage conditions) — a rename without them silently stops CI
on pushes to the new branch — plus the host's .host-software branch = main
(pin 845df57) and the CLAUDE.md canonical-branch fact. Call/0000's mention of
the adoption-time branch_libev is left as the historical record it is.
The worktree was re-registered at software/udpspeeder-simd/main/ from the
bare store. A git worktree move to the new name left a phantom main_old/
directory entry on the WSL /mnt/c filesystem (stat/find ENOENT yet the listing
shows it; rmdir "not empty") — a DrvFS cache artifact of the interrupted
rename, invisible to the host gate. A wsl --shutdown or a C: remount clears
it; it is cosmetic.
Release health after the split + rename: the release job is tag-triggered
(tags: ['v*'] + a ref-starts-with test) and branch-agnostic — a future tag
cut on main publishes normally, and the eight verification jobs it needs all
run on the new branch. The v1.1.0 release's own tag was never moved (tag
protection blocks in-place updates), so its evidence stays true.
2026-08-11 — feed branches rewritten to a single compliant commit; v1.1.0 in the feed
The packages repo's three feed branches (feed-main, feed-25.12, feed-24.10) carried ~24 commits each that failed the repo's formality-check hook (missing Signed-off-by, bad subjects, empty bodies, patch files without git headers) and were a newer-upstream sync plus the snapshot packages, only the snapshot dirs of which the feed workflow actually uses. Each branch was rebuilt as a single compliant commit on master (1d40ad9) carrying only the three snapshot package dirs (udpspeeder-snapshot, udpspeeder-simd-snapshot at v1.1.0, flashprog-snapshot) with git-header'd patches and a Signed-off-by. All three are now identical at 387ae05; the formality-check passes with errors=0.
The fork's feed-snapshot branch was reduced in the same pass to an orphan stub
(e2f9a85) carrying only the feed workflow + scripts, no udpspeeder source. A
dispatch from it built and published 111 jobs green, confirming the stub. The
v1.1.0 bump (PKG_VERSION 1.0.6 to 1.1.0, PKG_MIRROR_HASH recomputed via the
OpenWrt dl_tar_pack method: git archive, then tar --sort=name --owner=0
--group=0 --mode=a-s --mtime=@
2026-08-12 — release assets are immutable; v1.0.0 left as an accepted rebuild
Release assets are treated as immutable. Deleting or restoring a v* tag re-triggers the tag-driven release workflow, which rebuilds and replaces the published binaries; the build is non-reproducible (call/0006, the runner's g++ varies), so a rebuild changes the bytes even from the same source. A tag-protection probe during the split work deleted and restored v1.0.0, which re-published its two binaries on 2026-08-11 (built from d266e37, the same source as the original 2026-08-02 release, but different bytes). The v1.0.0 assets are left as that rebuild; v1.0.1 through v1.0.6 are untouched at their original dates. Going forward no v* tag is deleted or restored: the assets it points at are immutable. Test tag protection with a scratch tag name, never an existing release tag.
2026-08-14 — flashprog ships the variant pattern under a non-canonical provide
The entry "Correcting the flashprog entry again: the two packages are
co-installable, and why" describes the distinct-install-paths design:
flashprog-spi installing /usr/bin/flashprog-spi, no PROVIDES, no
CONFLICTS. That design is gone. The package now ships four variants that all
install /usr/bin/flashprog and all carry PROVIDES:=flashprog-bin from
Package/flashprog/Default, with DEFAULT_VARIANT:=1 on the full variant.
- Reviewer efahl asked for the variant pattern
dnsmasquses. In that shape the narrow variants provide the canonical name, whichpackage-pack.mkemits as a versioned provide. That reintroduces the substitution the entry above records, in its quietest form: with a narrow variant already installed,apk add flashprogis satisfied by the provide and installs nothing at all, reporting success. A silent no-op, not an error. - The deviation is the provide's name. All four variants provide
flashprog-bin, a name no package is called. The versioned provide then buys mutual exclusion between the variants whileflashprogstays the full package's own real name, never a virtual any variant can satisfy.apk add flashproginstalls the full package or fails. - Measured on the router's exact resolver. apk 3.0.5 inside an
openwrt/rootfs:x86-64-25.12.4container. Clean,apk add flashprogselects the full package. Withflashprog-spiinstalled it errors on theflashprog-binconflict instead of no-opping. The documented swap,apk del flashprog-spi && apk add flashprog, works. - The measurement used synthetic apks, not real builds. Twelve packages
(each candidate shape crossed with the four variants) built by
apk mkpkgwith a dummy payload and only the metadata under test, indexed byapk mkndx, and installed by name from that feed. The resolver reads index metadata alone, so the payload is irrelevant and a shape settles in minutes rather than a cross-compile apiece. Installing by name is the load-bearing half; a local filename names an exact package and never runs the resolver. - The dependency lists came out of the descriptions. efahl's closing note
asked for it:
DEPENDSis the source of truth,apk querysurfaces it, and a duplicated list is a second place to edit. All fourRequires ...sentences were dropped,flashprog-spi'sRequires no libraries.among them, since an empty depends list carries that already.
The PR head is 82e211de, pinned in .host-software. BKPepe's CHANGES_REQUESTED
(the sizes request that the commit message's table answers) is still the
standing review state; efahl's "looks good to me" arrived as a comment rather
than an approving review, so the gate does not flip on it.