agentic-UDPspeeder-simd

The governance shell for UDPspeeder-simd, a Forward Error Correction UDP tunnel (C++, forked from UDPspeeder). This repository holds the thought: the plans, decisions, personas, and the operating manual. The software itself, the action, lives beneath it as a bare store with a worktree, kept separate and independently versioned.

Read CLAUDE.md first; it is the operating manual and works literally. STRUCTURE.md is the one-page map. The process that built this host lives in the host repository.

Layout

RoomHolds
cast/personas (the Who); examples pending replacement
plan/the milestone index
call/decisions, in MADR format
software/the hosted software, materialized locally (gitignored)
tools/the verification tools, referenced submodules

The software is pinned in .host-software and materialized with host-lifecycle software --materialize . into software/udpspeeder-simd/branch_libev/.

Tooling

host-lifecycle and host-lint drive the mechanical work. Build them from the tools/ submodules, then generate the skill symlinks and install the commit gate:

git submodule update --init --recursive
./link-skills.sh
host-lifecycle software --materialize .
host-lifecycle software --install-hooks .

License

This governance shell is released into the public domain (Unlicense). The hosted software, UDPspeeder-simd, carries its own MIT license in its worktree.

Cast: the project's Who

Personas: hypothetical archetypal actors, grounded in research, that keep the work anchored in who it serves. The process comes from Powell, Keenan and McDaid (2007), which builds on the personas in Cooper and Reimann's Interaction Design.

Mara and Wren are examples: the operators of any agentic project (e.g. agentic-acme), the human who develops software and the agentic LLM who develops software. They illustrate the method, not your project.

Each project builds at least one persona of its own, by discussion: the operator and the agent elicit the actual users of the software under development (allium's elicit). Add them here. See applying-personas.md for the process.

(If your software under development is itself an agentic project, Mara and Wren may double as your real cast; the example sits at the meta level it was drawn from.)

Applying personas during planning

The XP-Persona process (Powell, Keenan & McDaid 2007) runs as a workshop with all interested parties (developers, customer representatives, management) before development begins. Steps one to six come from Interaction Design (persona development); steps seven to nine from XP story development. The aim is the minimum beneficial investigation before development starts, after which requirements and plans evolve.

  1. Brainstorm organisation goals. Record what the organisation wants the project to achieve; these reflect its intention.
  2. Brainstorm possible personas. Start broad, then combine through discussion to between two and twelve personas. Give each a name; the team refers to personas by name when making decisions, which keeps the view on the user rather than the system. Unless user research is available, the customer has the final say.
  3. Brainstorm persona goals. Set between two and six goals per persona; each goal states what the persona wants to achieve through the system. "A persona exists to achieve his goals, and the goals exist to give meaning to a persona" (Cooper 2004).
  4. Brainstorm persona personalization. Give each persona a personality and a technical ability relative to the system; a short paragraph or bullets of personal detail and nuance.
  5. Brainstorm persona scenarios. Describe interactions, not tasks: how a persona uses the system to reach a goal. No implementation detail in a scenario.
  6. Select the primary persona. This is the one who must be satisfied but cannot be satisfied by an interface designed for any other persona (Cooper 2004). It takes highest priority and feeds story prioritisation. More than one primary persona implies a separate interface for each.
  7. Create stories. From the persona goals and scenarios, write the user stories.
  8. Prioritise stories, using the primary persona as the guide.
  9. Estimate stories, estimated and agreed by customer and developers.

In our pipeline: steps one to six are the cast/ work (built by discussion); steps seven to nine become plan/ (milestones are the stories, prioritised by the primary persona, estimated); persona goals and scenarios feed spec/, where a scenario becomes acceptance criteria becomes a behavioural spec.

Complementary practice reinforces the same steps (departing only on strong later evidence): Cohn's "As a <persona>, I want <goal> so that <reason>" template reinforces the named-persona stories of steps two and seven; Gothelf's proto-personas reinforce the assumption-first workshop of steps two to five; Patton's story mapping reinforces the create-and-prioritise of steps seven and eight; behaviour-driven scenarios reinforce step five becoming executable acceptance criteria.

Sources. Primary: Powell, S., Keenan, F. & McDaid, K. (2007), "Enhancing Agile Requirements Elicitation with Personas," IADIS Int'l Journal on Computer Science and Information Systems, vol. 2(1), pp. 82-95, ISSN 1646-3692. Complementary: Cohn, User Stories Applied (2004); Patton, User Story Mapping (2014); Gothelf, Lean UX (proto-personas); Jeffries, "The Three C's of User Stories" (2001); Cooper & Reimann, About Face (2003); Agile Alliance on BDD.

Diego (router administrator)

Diego runs the gateway for a remote site on a high-latency, lossy WAN link. He puts UDPspeeder-simd on that gateway, with a matching server endpoint he controls on the far side, to make the link usable for real-time traffic.

Goals

  • Keep a lossy link usable for voice, video, and remote access by tunnelling the affected traffic through the client-and-server pair.
  • Tune the redundancy ratio (the -f x:y setting) so he recovers lost packets without spending more upstream bandwidth than the link can spare.
  • Manage the tunnel through OpenWrt's service and configuration tools, without editing or building C++.
  • Run the tunnel inside the CPU and memory limits of modest router hardware, where a feature such as io_uring may be missing from the kernel.
  • See whether the tunnel is healthy and how much loss it is recovering, so he can adjust as the link changes.

Personalization and technical ability

A competent network administrator for a small operator or a remote installation. He is comfortable on the OpenWrt command line and with UCI configuration, reads logs, and scripts routine tasks. He is not a C++ developer, and he treats the tunnel as an appliance that has to stay up while he is not watching it.

Scenarios

  • Diego installs the package on a satellite-backed site, points the client at his server endpoint, sets the redundancy ratio, and confirms that call quality holds up under measurable packet loss.
  • A firmware upgrade replaces the device configuration. Diego re-applies his tunnel settings from a saved snapshot and verifies the link recovers without hand-tuning.

Priority

Primary end-user persona for UDPspeeder-simd, and the anchor for prioritising the tunnel's own stories. The tunnel exists to serve his link, so his constraints (appliance-grade stability on modest hardware, with no source edits) drive the software's defaults and ergonomics. He depends on Ingrid's package for the service integration he configures.

Grounded in the role and the UDPspeeder-simd source. Refine further by discussion with the operator (see applying-personas.md).

Ingrid (OpenWrt feed maintainer)

Ingrid maintains the packages feed and packages UDPspeeder-simd and its dependencies for OpenWrt, so router users install the tunnel with opkg.

Goals

  • Package UDPspeeder-simd cleanly for the feed, with correct dependencies such as libev, so it installs on a stock OpenWrt device.
  • Build the tunnel across the OpenWrt target architectures the feed supports. UDPspeeder-simd ships hand-written SIMD assembly (xor_spe.S) and CPU-specific optimisation, so she confirms the package builds on MIPS, ARM, and x86, or falls back to a portable path where the SIMD one is unavailable.
  • Provide the OpenWrt service glue that upstream lacks. Upstream ships a raw command-line binary, so she adds an init script and a UCI configuration wrapper that let an administrator run the tunnel as a managed service.
  • Keep the feed building across the supported OpenWrt release branches (for example openwrt-24.10 through the current development branch), and bump the Makefile version and source hash when a new upstream release lands.
  • Offer fixes back to openwrt/packages with no rework, so authorship and commit style stay clean for the community feed.

Personalization and technical ability

An experienced package maintainer, fluent in the OpenWrt build system, Makefile feeds, and the feed layout. She reviews a steady stream of contributions and holds them to reproducible, hermetic builds and correct license metadata. She works several release branches at once, and she reads a diff for upstream-cleanliness before she forwards it.

Scenarios

  • A new UDPspeeder-simd release lands. Ingrid bumps the feed Makefile version and source hash, builds the package for a MIPS target and an ARM target, and confirms it installs and runs on a test device.
  • A contributor opens a change to the feed. Ingrid checks it applies cleanly and carries no automated co-author trailer (see call/0003) before she offers it to openwrt/packages.

Priority

Primary persona for the packages lane. The feed exists to let her ship the software to OpenWrt users, so her packaging interface (the Makefiles and the service glue) and her upstream-clean workflow drive the lane's work.

Grounded in the role and the UDPspeeder-simd source. Refine further by discussion with the operator (see applying-personas.md).

Samira (security engineer)

Samira reviews UDPspeeder-simd as a network service that parses untrusted input. The tunnel reads UDP packets from the open network and decodes them in C++, so she treats every packet as potentially hostile.

Goals

  • Assure that malformed or crafted packets cannot corrupt memory or crash the tunnel, since the receive path in C++ handles attacker-controlled bytes.
  • Confirm the Reed-Solomon decode path enforces its buffer bounds under adversarial input, and that a crafted stream cannot exhaust the decode buffer (--decode-buf) or turn the server into a reflection or amplification vector.
  • Set expectations for the -k key correctly. It applies a simple XOR, which gives no cryptographic confidentiality, so she checks that the defaults and the documentation state the safe pattern plainly: run UDPspeeder under a real VPN such as OpenVPN when the traffic needs protecting.
  • Tie each security claim to a rung of the verification ladder, so a claim rests on a check rather than an assertion.

Personalization and technical ability

A senior security engineer who reads C++ fluently, threat-models network services, and runs fuzzers and static analysis. She distrusts undocumented invariants and asks for the check that establishes one. She prefers a bound proven for all inputs over a bound sampled by tests.

Scenarios

  • Samira fuzzes the packet-receive path with malformed frames under address and undefined-behaviour sanitizers, reviews how the decode bounds are enforced, and records the invariants she expects as spec obligations for the requirements lane.
  • Reviewing a change to the FEC buffer handling, she asks for the decode bound to be proven for all inputs where the C++ code admits a bounded model check, and exhaustively fuzzed under sanitizers where it does not, before the change ships.

Priority

Secondary persona, cross-cutting the tunnel and the feed. She is a served reviewer whose obligations feed the spec lanes, so her concerns become behavioural specs, timing checks, and code-conformance proofs rather than a separate interface. On the feed she also watches the supply chain, so a Makefile fetches only pinned, hash-verified sources.

Grounded in the role and the UDPspeeder-simd source. Refine further by discussion with the operator (see applying-personas.md).

Plan: what & when

Milestones in this project.

Add an OpenWrt package for udpspeeder-simd

  • Status: planned
  • Persona: Ingrid (OpenWrt feed maintainer)
  • Serves: https://github.com/openwrt/packages/issues/28562
  • Scope: the packages and udpspeeder-simd components
  • Package maintainer: David Connolly david@connol.ly
  • Target: OpenWrt master for the first pull request; a release-branch backport is constrained by policy (see the rules below)
  • Decisions: call/0004 (coexist, dynamic linking), call/0005 (build adjustments in the fork)

Why

The issue at https://github.com/openwrt/packages/issues/28562 asks for the performance-optimised UDPspeeder on low-end devices; the reporter's target is ath79/generic, a MIPS 24Kc core. UDPspeeder-simd is that optimised fork. Rather than repoint the existing net/udpspeeder package at a fork, this milestone adds a separate net/udpspeeder-simd package that coexists with udpspeeder.

On ath79 the gain comes from the portable word-width XOR and the portable Reed-Solomon speedups, not from SIMD. The source carries no MIPS SIMD path, and none is worthwhile on the 24Kc core (MSA is absent on that core, and its optional DSP ASE targets fixed-point math rather than vector XOR).

OpenWrt maintainer rules this milestone follows

From the feed's CONTRIBUTING.md (pinned in the packages worktree) and https://openwrt.org/docs/guide-developer/packages :

  • Pin the source to a fixed commit or tag with a PKG_MIRROR_HASH, never a branch head or a "latest" archive. This is also what the issue requests.
  • PKG_MAINTAINER is a real name and a real, public email (David Connolly david@connol.ly). A GitHub noreply address is rejected.
  • PKG_LICENSE is the SPDX identifier MIT, with PKG_LICENSE_FILES naming the license file (LICENSE.md) in the fork.
  • PKG_RELEASE starts at 1.
  • No dependencies outside the OpenWrt core packages or this feed.
  • Each pull-request commit subject is prefixed with the package name (udpspeeder-simd: add package) and carries a Signed-off-by that matches the author (a real name and a public email), with no automated co-author trailer (the packages rule in call/0003). The sign-off is the DCO, which is separate from the co-author trailer.
  • A test.sh beside the Makefile lets the OpenWrt CI runtime-test the package on its supported architectures.
  • Release branches (openwrt-XX.YY) take only security and bug fixes. Adding a new package to a release branch is against policy, so a backport is not a routine cherry-pick (see #backports).

Design decisions

  • Interop (call/0004): a distinct package udpspeeder-simd with distinct paths (/usr/bin/udpspeeder-simd, /etc/config/udpspeeder-simd, /etc/init.d/udpspeeder-simd), no file clash with udpspeeder, no CONFLICTS.
  • Linking (call/0004): dynamic, with DEPENDS on the shared libraries the binary needs, like the existing udpspeeder. No static build. A static C++ binary is larger on flash, which works against the low-end devices this issue targets, and static linking runs against OpenWrt's shared-library model.
  • Build adjustments (call/0005): they live in the fork's makefile, after which the .host-software pin is updated. The fork commits keep the normal sign-off and co-author trailer; only the packages repo drops the trailer. In practice no fork change was needed: the feed's cc_cross and gitversion Build/Prepare sufficed.
  • SDK for local validation: the public prebuilt x86_64 snapshot SDK (the one OpenWrt CI uses), not a from-source build (make world proved excessive for this). The embedded openwrt component stays but is not required for this path. Local validation covers x86_64; multi-architecture, including ath79/generic, runs in the CI lane. Snapshot OpenWrt packages are .apk, not .ipk.
  • CI validation (#ci-sdk-lane): lives in the host repo (.github/workflows/udpspeeder-simd-sdk.yml), never in the openwrt PR, so the PR stays a discrete package addition. It builds the fork's package branch against the current snapshot SDK for x86/64 and ath79/generic.
  • Release users (#backports): OpenWrt policy blocks a new package on release branches, so there is no backport. docs/udpspeeder-simd-on-openwrt-releases.md shows a developer or power user how to self-apply it with a release SDK or buildroot.

Build sequence

Make the fork build cleanly across architectures

  • verify: attested operator

Author the udpspeeder-simd feed package Makefile

  • depends: #fork-build
  • verify: attested operator

Author the service integration

  • depends: #feed-package
  • verify: attested operator

Add the runtime test script

  • depends: #feed-package
  • verify: attested operator

Add the OpenWrt SDK build lane to CI

  • depends: #feed-package
  • verify: attested operator

Confirm polite interop with the udpspeeder package

  • depends: #service-integration
  • verify: attested operator

Open the pull request to OpenWrt master

  • depends: #interop, #test-script
  • verify: attested operator

Evaluate a release-branch backport under policy

  • depends: #pr
  • verify: attested operator

Take upstream's cross_cxx merge into both packages

  • depends: #pr
  • verify: attested operator

DNS lease manager for udpspeeder-simd (v1.1.0)

  • Status: planned
  • Persona: Diego (router administrator); Samira cross-cuts (spec lanes)
  • Serves: Diego points the client at a hostname endpoint and the tunnel keeps itself alive across IP changes, TTL expiry, and tunnel collapse, without him watching
  • Scope: the udpspeeder-simd component
  • Decisions: call/0008 (versioning and release policy), call/0009 (the C++-compatible single header)

Why

The client's -r option requires an IP literal today: address_t::from_str (common.cpp) splits host:port and demands inet_pton succeed, so a hostname calls myexit(-1). Diego's gateway points at a server endpoint he controls on the far side; when that endpoint moves (dynamic DNS, provider IP churn), he must find the new address and hand-edit his configuration. A tunnel meant to be an appliance he does not babysit should follow the endpoint itself.

This milestone adds a DNS lease manager: a single-header, allocation-free, nonblocking, TTL-aware DNS Locator-Hint Cache. The client resolves -r hostname:port to candidate IPs, leases them for an effective TTL, refreshes before expiry, and re-points the tunnel when the resolved IP changes. It treats DNS as an untrusted, mutable locator-hint source (the tunnel test the candidates via the data plane), not as a trust anchor. This is the first minor version bump (v1.0.6v1.1.0); every release so far has been a patch.

The OpenWrt feed package is out of scope here; the fork release is a prerequisite for a later package bump.

Design decisions

  • The header is written as valid C++11 (static inline throughout) rather than strict C11, because the fork builds C++-only and the negative constraints the spec imposes (no allocation, mem* only, no OS resolver, no threads/atomics/signals, no recursion/longjmp, no float, no unaligned access, no global mutable state, no stdio) carry over unchanged. See call/0009.
  • The state machine adds a STALE state to the spec's seven: on a failed refresh the last-known candidates stay servable until stale_max_ms elapses (default 1 hour; 0 means serve stale indefinitely). This is the keep-last-known-IP policy, made explicit, observable, and TLC-testable.
  • TCP fallback on truncation (TC=1) is included, per RFC 1035 and the imported spec: the header gains UDP-pending / TCP-connecting / TCP-receiving transport states and a 4096-byte TCP receive buffer.
  • Client mode only. In server mode the -r value names the client endpoint and stays IP-only; a hostname there is rejected at parse time with a clear error.
  • Verification carries both spec lanes: a .allium requirements spec (allium-cli check/analyse/plan in the fork CI, obligations discharged by tests) and a .tla state-machine/timing spec (TLC model check in the fork CI). Specs live with the software, per the methodology.
  • Release mechanics follow the house rule: the tag is the release; the banner PROGRAM_VERSION and the tag must agree; the fork CI publishes the per-architecture binaries; the host re-pins .host-software and records the release receipt. Versioning policy is recorded in call/0008.

Build sequence

Author the DNS lease manager header

  • verify: attested operator

Author the allium requirements spec and wire its lane

  • depends: #dns-header
  • verify: attested operator

Author the tla state-machine spec and wire TLC

  • depends: #dns-header
  • verify: attested operator

Integrate the client

  • depends: #dns-header
  • verify: attested operator

Discharge the spec obligations with tests

  • depends: #allium-spec, #tla-spec, #integrate
  • verify: attested operator

Bump to v1.1.0 and release

  • depends: #tests
  • verify: attested operator

Re-pin the host and record the release

  • depends: #release
  • verify: attested operator

Software: where

The action this project produces. Each component is a bare object store with worktrees, not committed into this repo; the recipe below is the reproducibility anchor. Materialize the worktrees locally with:

host-lifecycle software --materialize .

host-lint

  • url: https://github.com/connollydavid/host-lint.git
  • pin: 966259fb2001d0a4f6bdf3ae608ddd1c36c309d0
  • worktrees: none (single canonical line)

udpspeeder-simd

  • url: https://github.com/connollydavid/UDPspeeder-simd.git
  • pin: 4370bac
  • worktrees: none (single canonical line)

packages

  • url: https://github.com/connollydavid/packages.git
  • pin: 1d40ad929ad45cfbdf547996d0ba2379d9765172
  • worktrees: udpspeeder-update-20260731 @ d9ef24250436, feed-25.12 @ 387ae05fc4da, feed-24.10 @ 387ae05fc4da, feed-main @ 387ae05fc4da, flashprog @ 08e328a04a2a, ci-flashprog @ 75ff54251411

openwrt

  • url: https://github.com/openwrt/openwrt.git
  • pin: 62e7565f0e1e88e5fc8d038f09cb38ffd583bbed
  • worktrees: none (single canonical line)

Govern UDPspeeder-simd under the agentic-host methodology

  • Status: accepted
  • Scope: udpspeeder-simd
  • Date: 2026-07-02

Context and Problem Statement

UDPspeeder-simd is a pre-existing C++ project (a Forward Error Correction UDP tunnel, forked from UDPspeeder) developed on a bare commit history. The reasoning behind its choices lives only in commit messages and is lost across sessions, with no durable record of decisions or specs.

Decision

Govern development of UDPspeeder-simd under the agentic-host methodology, adopted from the host-template repository at revision 565410a (recorded in .host).

  • The host repository (agentic-UDPspeeder-simd) holds the thought: plans (plan/), decisions (call/), personas (cast/), and the operating manual (CLAUDE.md).
  • The software itself stays separate as the Where room, a bare store with a worktree under software/udpspeeder-simd/branch_libev/, recorded in .host-software and pinned to its branch_libev head. The host never adopts a software repository in place.
  • The methodology rules live in the spine (CLAUDE.md plus STRUCTURE.md), inherited copy-at-version from the template; they are not re-litigated here. A later change to them arrives by host-lifecycle upgrade against the template's UPGRADING.md ledger.

Consequences

  • Good: decisions, plans, and specs gain a durable, citable home; the software pin is a real audit anchor; tooling upgrades are mechanical and token-free.
  • Neutral: this is one of two numbered registers (milestones and decisions), kept apart by home. The software keeps its own MIT license; this governance shell is released into the public domain (Unlicense).

Pin the verification tools to current releases, not the template gitlinks

  • Status: accepted
  • Scope: udpspeeder-simd
  • Date: 2026-07-02

Context and Problem Statement

The adopt guidance says to wire each tools/ submodule "to the commit the template references at this revision." The template at 565410a records these gitlink SHAs in its .gitmodules: host-lint 2ef5399 (v0.2.0), host-lifecycle 2a24deb (v0.15.1), allium 82da292, specula 38e9d6e. Those gitlinks are stale: host-lint v0.2.0 predates the --docs engine, and host-lifecycle v0.15.1 predates book, prose, software --install-hooks, and reconcile, which the methodology's own workflows call. The template's prose.yml pins host-lifecycle at 46d481cd (a far newer revision than its gitlink), confirming the gitlinks lag the working set.

Decision

Pin the tools/ submodules to current, working releases instead of the stale gitlinks, so the methodology's CI lanes and the commit gate actually run:

  • tools/host-lint at 78804cd (v0.12.1)
  • tools/host-lifecycle at 486add7 (v0.35.1)
  • tools/allium at 493a2de (v3.6.0)
  • tools/specula at fa12367

Consequences

  • Good: the commit gate (host-lint --stdin), the prose lane (host-lifecycle prose), the site publisher (host-lifecycle book), and the embed hook installer all run against tool versions that actually implement them.
  • Neutral: the submodules drift ahead of the template's recorded gitlinks. A later template bump that refreshes its gitlinks reconciles this; until then the pins live here in .gitmodules, the source of truth for this host.

host-lint is a consumed tool, not reproduced by this host

  • Status: accepted
  • Scope: host-lint
  • Date: 2026-07-02

Context and Problem Statement

host-lint is registered in .host-software so host-lifecycle software --install-hooks can install its pre-commit hook and built binary as this host's commit gate. The software check requires a component that records an artifact hash to also record a toolchain pin, so the artifact is reproducibly verifiable. This host does not build host-lint reproducibly. It pins an upstream release, then builds the binary locally so the commit gate can install it. No reproducible build of that binary is claimed here.

Decision

Carry repro-exempt = call/0002 on the host-lint component and record no build or toolchain recipe. The pinned source SHA (in .host-software) plus the recorded artifact hash (a local-build anchor that attests the installed binary came from that source) are the interim provenance. host-lint's own reproducibility is the upstream maintainer's concern, attested by its released tags.

Consequences

  • Good: the commit gate installs and runs; the cheap software --check passes without this host claiming a reproducible build it does not perform.
  • Neutral: host-lifecycle software --verify-build reports host-lint as exempt and skips the rebuild comparison. Retire this exemption only if this host begins to build host-lint itself, which is not the case for a consumer.

No Claude co-author trailers on packages commits

  • Status: accepted
  • Scope: packages
  • Date: 2026-07-03

Context and Problem Statement

The packages component is a fork of the OpenWrt packages feed (openwrt/packages, GPL-2.0). Work done here may be offered back to that upstream, whose commit history carries no AI-assistant co-author trailers and whose authorship attribution stays clean for a community feed. This host's default agent behaviour appends a Co-Authored-By: Claude ... trailer to commit messages. That trailer is unwanted on any commit in the packages worktree.

Decision

Every commit created in the packages worktree (software/packages/<branch>/) omits the Co-Authored-By: Claude ... trailer. An agent committing under software/packages/ suppresses the trailer its default behaviour would add. The prohibition is scoped to the packages component; the operator confirmed the scope is packages only, so commits in this host repository keep the trailer as before.

Consequences

  • Good: packages commits stay upstream-clean, so a change offered to openwrt/packages needs no trailer scrubbing.
  • Neutral: the rule is a commit-message convention rather than a hook the host enforces, because the packages worktree is a separate repository with its own hooks. An agent honours it by reading this decision and the CLAUDE.md project-specifics note that points here.
  • Follow-up: if upstream provenance later needs a DCO sign-off or another trailer policy, record it as a sibling decision under the same scope.

The udpspeeder-simd package coexists with udpspeeder and links dynamically

  • Status: accepted
  • Scope: packages
  • Date: 2026-07-03

Context and Problem Statement

The issue at https://github.com/openwrt/packages/issues/28562 asks for the performance-optimised UDPspeeder on low-end OpenWrt devices. The feed already carries net/udpspeeder (the upstream UDPspeeder). A udpspeeder-simd package has to avoid clashing with that package, and it has to choose how the binary links. The operator first asked for a static build.

Decision

Ship a distinct package udpspeeder-simd with distinct paths (/usr/bin/udpspeeder-simd, /etc/config/udpspeeder-simd, /etc/init.d/udpspeeder-simd), so it installs alongside udpspeeder with no file clash and no CONFLICTS.

Link the binary dynamically, with DEPENDS on the shared libraries it needs (the udpspeeder package depends on libstdcpp, librt, and libatomic). Do not build statically. A static C++ binary is larger on flash, which works against the low-end devices the issue targets, and static linking runs against OpenWrt's shared-library model. The operator settled this: no static builds for OpenWrt.

Consequences

  • Good: the package matches OpenWrt's conventions and the sibling udpspeeder package, holds the flash footprint down, and reviews cleanly.
  • Good: both packages install together, so a user can compare them.
  • Neutral: the exact DEPENDS set is confirmed when the package first builds against the SDK.

Cross-build adjustments for udpspeeder-simd live in the fork

  • Status: accepted
  • Scope: udpspeeder-simd
  • Date: 2026-07-03

Context and Problem Statement

The OpenWrt package builds udpspeeder-simd from the fork's source. Any change the cross build needs (for example gating the io_uring path on a toolchain that lacks the syscall numbers, or a clean cross entry point) can live either in the fork's own makefile or as patches applied from the feed's package Makefile. The existing udpspeeder package patches upstream's makefile from the feed with sed.

Decision

Make the build adjustments in the fork's makefile, then update the .host-software pin, rather than carry feed-side patches. connollydavid owns the fork, so a fix there is upstreamable and keeps the feed Makefile thin. The fork's cross target already links dynamically, so the adjustments are expected to be small.

Consequences

  • Good: the feed Makefile stays close to a stock build, which reviews more easily.
  • Good: the fix benefits anyone who builds the fork, not only the OpenWrt package.
  • Neutral: each fork build change is a fork commit (with the normal sign-off and co-author trailer) followed by a re-pin, per the software discipline.

udpspeeder-simd release binaries are not yet reproducible

  • Status: accepted
  • Scope: udpspeeder-simd
  • Date: 2026-08-05

Context and Problem Statement

On 2026-08-03 this project began publishing binaries. The v1.0.1 and v1.0.2 releases each carry five static executables, one per architecture, and a tooling-sde-10.13.1 release mirrors a third-party emulator.

The methodology is explicit that this brings an obligation: a component shipping static or self-contained release binaries must be able to reproduce them offline from pinned inputs, and must record which line ships (deploy) together with the artifact's expected hash (artifact = <path> <sha256>). The udpspeeder-simd stanza in .host-software records none of that. It carries a source pin and nothing else, so the gate has no build to check and no hash to compare.

We cannot honestly record an artifact today. The binaries were built by GitHub Actions on ubuntu-latest using whatever g++ that image carried on the day, which is not a pinned toolchain, and nothing in the build fixes timestamps, build paths or -frecord-gcc-switches output. A second run on a later image would very likely differ. Recording a hash for a build that cannot be reproduced would state a guarantee we have not established, which is the fault this project spent the week removing from its verification lanes rather than one to introduce here.

Considered Options

  • Record build, toolchain, artifact and deploy now, and accept that --verify-build fails until the build is made deterministic.
  • Record a repro-waiver citing this decision, and state the route out.
  • Stop publishing binaries until the build is reproducible.

Decision

Record repro-waiver = call/0006 on the udpspeeder-simd stanza.

The escape clause fits this component exactly. udpspeeder-simd is pre-existing software brought under the methodology rather than initiated by it, so the reproducibility obligation is one it converges on rather than one it was born with. The waiver is the honest record of that, and it keeps software --check able to distinguish "no claim made" from "claim made and unmet".

Publishing continues. The interim provenance is not nothing:

  • Every release binary is built by a workflow run in a public repository, from a tagged commit, and the run that produced it is linkable.
  • The four cross toolchains are now hash-pinned in bench/arch-map.tsv, and the download step fails on a mismatch, so the cross binaries are built by a compiler we have named rather than whatever a URL returned.
  • The mirrored emulator is pinned by sha256 in bench/sde.lock and carries a detached Intel signature, verified before the hash was recorded.

Consequences

The gate stops being silent about this component. software --check requires the citation to resolve, and --verify-build warns and skips rather than reporting a reproducibility it never tested.

What remains owed, and what retires this waiver: a pinned toolchain for the native x86_64 build, so every line has a named compiler rather than the runner's default; a deterministic build, which for this makefile means at least SOURCE_DATE_EPOCH, -ffile-prefix-map and a fixed link order; and then build, toolchain, artifact and deploy recorded per line, with --verify-build green. At that point this decision is superseded rather than deleted, and the waiver comes off the stanza.

The tooling-sde-10.13.1 release is outside all of this. It republishes Intel's tarball byte for byte and builds nothing, so there is no artifact of ours to reproduce; its provenance is the recorded sha256 and Intel's own signature.

The tools this host runs are pinned and hash-verified

  • Status: accepted
  • Scope: agentic-UDPspeeder-simd
  • Date: 2026-08-10

Context and Problem Statement

The methodology pins the software a host builds. A component records a source pin, a build recipe, an artifact hash, and may record a deps-bundle so the build reproduces offline from inputs fixed in advance rather than from whatever a network fetch returns that day.

It says nothing about the tools the host itself runs. Those were reached through whatever this machine happened to carry. On 2026-08-10 that meant node and allium from ~/.local, java from /usr/sbin, and tla2tools.jar from a home share. Nothing recorded which versions those were, and nothing would have noticed had they drifted.

The failure this invites is the one the OpenWrt work already taught. A package build that fetches its dependencies at build time is not reproducible, however carefully its source is pinned, because the fetch is the unpinned input. The same holds one level up: a gate is only as trustworthy as the binary that runs it, and an unpinned binary makes the gate's verdict a property of the machine rather than of the change under it.

Two concrete symptoms had already appeared. A generated pre-push hook carried $HOME/.local/bin/node, so it worked here and nowhere else. And a stale target/release/host-lint sat six minor versions behind the binary on PATH, which turned a green suite red and cost an hour before the cause was found.

Decision

Every tool this host runs installs to a project-local path, at a version and a sha256 recorded in the repository.

.env names each tool's version and its in-tree path. tools/install-tools.sh fetches each one into .host-tools/ and verifies its sha256 before unpacking it, refusing on a mismatch. .host-tools/ is gitignored: the recipe is committed, the binaries are not. Scripts read .env rather than probing PATH, so a hook or a lane names the tool it means.

Where a publisher ships its own checksum file, the script verifies against that rather than a hash we copied: node against the published SHASUMS256.txt, the JDK against the Adoptium API's checksum. Where none exists, the recorded hash is the one this project pinned, and a change to it is a visible diff.

Two mechanisms exist, and each tool uses exactly one. host-lint is a Where-room component: .host-software already carries its pin, its artifact sha256, and the hooks it installs, and software --install-hooks verifies the binary against that hash. It is not repeated in .env, because a hash recorded twice is a hash that can disagree with itself. Everything else, node, the JDK, tla2tools.jar, host-lifecycle and allium, is pinned in .env and fetched by the script.

Consequences

A fresh clone gets the same tool versions as this one, and a drifted download fails loudly at the point of installation rather than quietly at the point of use.

The cost is that a tool upgrade is now a commit: the version and the hash both move in .env, and that is the intent.

Two limits are worth stating plainly. The install verifies provenance, not behaviour, so a tool that is authentic and wrong still passes. And .host-tools/ holds around 550 MB, mostly the JDK, which is a real cost on a machine that already carries one.

This decision is scoped to this host. The general form, a pinned and hash-verified tool set recorded in .host-software and checked by software --verify-setup, is a change to the shared methodology and belongs upstream in the template, proposed there rather than settled here.

Notes

allium is pinned to 3.5.0 rather than the template's 3.4.2. Neither version installs from crates.io without --locked: allium-cli resolves a newer allium-parser whose analyze_with_cross_module takes more arguments than the call site supplies. 3.5.0 is the version tools/allium carries.

udpspeeder-simd versioning and releases

  • Status: accepted
  • Scope: udpspeeder-simd
  • Date: 2026-08-10

Context and Problem Statement

The fork has released v1.0.0 through v1.0.6 with no recorded versioning policy. Every bump so far has been a patch (a fix layered on the same feature surface), cut by tagging the fork and letting CI build the binaries. The milestone currently planned (plan/0001) is the first feature addition since the fork diverged, which makes it the first minor bump. The room has no decision that states what a minor bump means, what the tag carries, or how the release is recorded, so the v1.1.0 release would otherwise be cut the way the v1.0.x line was: by habit, with the reasoning living only in the tag.

call/0006 governs reproducibility (the binaries are not yet byte-reproducible, and the stanza carries a waiver). It does not govern versioning or cadence.

Decision

The fork follows semantic versioning on its tags, with the release process the v1.0.x line already established:

  • A minor bump (vX.Y.<anything>vX.<Y+1>.0) carries a user-visible feature; a patch bump carries a fix. The first minor bump is v1.1.0, cut for the DNS lease manager feature in plan/0001.
  • The tag is the release, per the spine rule: a version bump MUST be accompanied by a matching annotated tag vX.Y.Z at the release commit, pushed alongside it. The tag-triggered CI job builds the artifacts from it.
  • The banner PROGRAM_VERSION in main.cpp must equal the tag; the fork CI enforces the match on tag pushes.
  • The host records each release by re-pinning the .host-software udpspeeder-simd stanza to the tagged commit and recording the release phase receipt for the component.

Consequences

  • Good: a future release is cut the same way, with the meaning of the bump decided before the tag, not after; the v1.1.0 bump has a citable decision behind it.
  • Good: the release receipt ledger (currently a stale skip for this component) gains a real done entry once plan/0001 ships.
  • Neutral: the OpenWrt feed package bump is a separate milestone; the fork release is its prerequisite, not its mechanism.
  • Follow-up: call/0006 still governs reproducibility; when the build becomes deterministic, that waiver is retired and this decision's release-mechanics half stays in force.

The DNS lease manager ships as a C++-compatible single header

  • Status: accepted
  • Scope: udpspeeder-simd
  • Date: 2026-08-10

Context and Problem Statement

The DNS lease manager for plan/0001 was specified as a strict C11 single-header library: allocation-free, mem*-only string handling, no OS resolver APIs, no threads, no recursion, no floating point, no unaligned access, no global mutable state, no stdio in the protocol path, all functions static inline.

The fork builds C++-only: the flat makefile compiles every translation unit with g++ -std=c++11, there is no C compiler in the build, and adding one (a .c translation unit, a C compiler target, cross-compiler coverage for every OpenWrt target) would be a build-system change purely to satisfy a language dialect. A C11 header's negative constraints are not C-specific; they are properties of how the library must behave, and C++ can hold them all.

Decision

Write the DNS lease manager as a single header compiled by the existing C++ build, preserving every negative constraint from the C11 specification unchanged: no dynamic allocation, memcpy/memmove/memcmp/memset only, no getaddrinfo or any OS resolver API, no threads/mutexes/atomics/signals, no recursion or longjmp/setjmp, no floating-point math, no unaligned memory access or struct casting for network parsing (manual byte shifting), no global mutable state (all state in the caller-provided dns_lease_ctx), and no stdio in the protocol path (resolv.conf discovery uses open/read/close; logging is a caller-supplied callback). All functions remain static inline.

The only change is the dialect: the header is valid C++11 so it compiles under the fork's existing -std=c++11 with zero build-system change.

Consequences

  • Good: the fork's build and its cross-compiler matrix are untouched; the header compiles on every target the fork already builds.
  • Good: the constraints the spec was really about (no allocation, no OS resolver, parser bounds safety, no ambient state) are preserved and enforced by the same review and test gate a C11 header would face.
  • Neutral: the header is not valid C compilation, so it cannot be reused by a C consumer without a translation; the project is C++-only, so no such consumer exists today.
  • Follow-up: if upstream or a future consumer needs a C11 build, the header is already written to C11-compatible idioms where C++ and C differ, so the remaining work is a dialect pass, not a rewrite.

CLAUDE.md: operating manual for an agentic project

This file tells you, the agent, how to work in this repository. Follow it exactly. The rules are written to be literal: when one says "do X", do X. Do not look for a cleverer path. When something is unclear or could mean two things, stop and ask the human before you act. Clarity beats cleverness here.

What this repository is

This repository is an agentic project (e.g. agentic-acme). It is the externalized thought about a piece of software: the plans, the decisions, the specifications, the people it serves, and the rules you work under. The software itself (the action) lives beneath the project as a bare store with worktrees (the Where room). You write thought in the project and action in the worktree. Keep them separate.

You are working in a template. A real project replaces the example personas with its own and adds its software as the hosted bare store with worktrees. The structure below stays the same.

The five rooms

The host has five rooms, one for each question you ask about any piece of work. Put each kind of file in its room. Do not invent new top-level folders.

QuestionRoomWhat goes here
Whocast/personas: the people (human or agent) the software serves
What<software>/ (with the code)specifications: behaviour (.allium), timing (.tla), verified in the software's own CI
Whenplan/the milestone index and one folder per milestone
Where<software>/the hosted software, a bare store with worktrees; you add it
Whycall/decisions, in MADR format (see call/0000)
HowCLAUDE.md + tools/this manual, and the verification tools

STRUCTURE.md is the short map of the same thing. Read it once.

How you work: four principles

These four principles govern every change. They exist because language models make predictable mistakes; each principle blocks one.

1. Think before coding

  • State your assumptions in plain text before you write code.
  • If the request could mean more than one thing, list the meanings and ask which one. Do not pick one silently.
  • If a simpler approach exists than the one you first reached for, say so.
  • If any part of the request is unclear, stop and name the unclear thing. Ask. Do not guess.

The goal: the human never reads your output and says "that is not what I meant."

2. Keep it simple

  • Write the least code that solves the stated problem. Nothing speculative.
  • Do not add features that were not asked for.
  • Do not add an abstraction (a base class, an interface, a wrapper) for something used in exactly one place. Write the concrete thing.
  • Do not add configuration for a value that has exactly one setting. Hardcode it.
  • Do not handle errors that cannot happen given the current inputs.
  • If your code is long and the same result fits in a fraction of the lines, rewrite it short before you show it.

3. Make surgical changes

  • Touch only the lines the request needs. Clean up only the mess you made.
  • Do not "improve" nearby code, comments, names, or whitespace that the request did not ask about.
  • Do not refactor working code that is not part of the request.
  • Match the existing style exactly: tabs or spaces, snake_case or camelCase, whatever the file already uses.
  • If your change leaves an import or variable unused, remove it in the same commit. If you spot unrelated dead code or a bug, mention it to the human; do not silently fix or delete it.

Check: every line you changed must trace to the request. If a line does not, revert it.

4. Drive to a verifiable goal

Turn every task into a goal you can check, then loop until the check passes.

  • "Add validation" becomes: write tests for the invalid inputs, then write code until those tests pass.
  • "Fix the bug" becomes: write a test that reproduces the bug, then change code until that test passes.
  • "Refactor X" becomes: confirm the tests pass, refactor, confirm they still pass.

For any task with more than one step, write a short numbered plan first, and give each step a check:

<what you will do> -> verify by: <how you will confirm it worked>

If the success check is weak ("make it work"), ask the human to make it concrete before you start.

Names, numbers, and milestones

Numbers are identity. Slugs are content. Ordering lives in the index, never in the name.

  • A milestone is a folder in plan/ named NNNN-slug: a four-digit zero-padded number, a hyphen, then a lowercase hyphenated slug, for example 0001-example-milestone. Decisions in call/ use the same NNNN-slug form.
  • Name a milestone after its content rather than its position. 0003-ci-pipeline is good. Do not name things by ordinal position (phase-one, M2) or by a bare numeral (a header that is just "3" or "5.5"). Positions move when a plan is re-cut; content names stay attached to their content.
  • The number is assigned when the work is accepted, and it never changes. To read sequence, read the index, not the filenames.
  • tools/host-lifecycle allocates these numbers and checks the names for you. Use it (see below) instead of numbering by hand.

Decisions: call/

When you make a choice that someone later will ask "why?", record it as a decision in call/, in MADR (Markdown Any Decision Record) format. The bootstrap decision call/0000 explains the format and links to the MADR spec. One file per decision, NNNN-slug.md, number assigned when the decision is accepted.

call/ records decisions about the software under development, why your software is built as it is. It does not hold methodology decisions. The methodology is settled in this spine (CLAUDE.md + STRUCTURE.md), inherited by copy-at-version; a change to it is made in the template and propagated by host-lifecycle upgrade, never re-litigated as a project call/.

Anti-ouroboros. A project must avoid feeding on its own methodological tail. If a call/ decision restates a methodology rule that is then settled or changed upstream in the spine, retire it the MADR way: set Status: superseded by the spine in place (records are immutable: you change status, never delete). The live (accepted) Why room then holds only decisions still in force. host-lifecycle validate fails an accepted decision that is missing a Scope: header or declares Scope: methodology. The template ships call/0000 only as a worked example; replace it with your software's own decisions.

Inherit from the source, not from a host. You inherit the methodology from this template (the versioned source you copy-at-version) alone. A host or management repo's top-level instance contents (its call/, plan/, MEMORY.md, the project-specific parts of its CLAUDE.md) are that project's own rooms and bind no adopter. Do not read them as normative.

Specs, with the software they constrain

A spec states what the software must do; a tool turns it into a check. Specs live with the software they describe (in the software repo, beside the code, and verified by that repo's CI), the way tests live next to code, so a spec and the code it constrains move, version, and break together:

  • Behaviour and requirements as .allium files, authored and maintained through the allium skills (elicit/distill/tend/weed/propagate), checked by tools/allium (allium check validates structure; allium analyse adds data-flow, reachability, terminal-state and deadlock analysis; allium plan derives the test obligations the suite must discharge). The software's CI runs check + analyse + plan and fails on any error or warning.
  • Timing and concurrency as .tla files, checked by tools/specula (TLA+/TLC), run by the software's CI.

A present spec carries its full lane (see "Mandatory when used", below). Wire the tool and its skills before authoring.

The host's plan/<milestone>/ references a spec (by path and the software pin); it does not contain it. Do not place specs in the host's plan/ tree. Quarantining a spec from its software is a bad smell (the spec drifts from the code). A spec that ended up under plan/*/spec/ is relocated into the software repo.

Personas: cast/

cast/ holds the personas: short profiles of the people the software serves. A persona is a hypothetical archetypal user, not a real individual. The examples here (mara.md, a human operator, and wren.md, an agentic LLM) show the two modalities; replace them with your project's own.

Build at least one persona by discussion with the human before planning the work it serves. cast/applying-personas.md gives the cited process for doing this. Follow it.

The verification ladder

Different kinds of property need different checkers, and different strengths of checker. Route each claim to the lane, and to the rung, that can prove it. The base lanes:

  1. Hygiene: tools/host-lint. Catches naming tells: ordinal labels and bare numerals leaking into commit messages, headers, and comments. Runs as a git hook. Self-referential software is excluded, not bypassed. Software that detects tells (a linter, parser, validator, grammar tool) must embed them in test fixtures, docs, and sometimes source, legitimate self-reference the hook would otherwise flag. Exclude that corpus through the tool's ignore mechanism (.host-lintignore), which the per-file hook scan MUST honor; validate each excluded file line-by-line so no real tell hides among the examples, and keep ordinary source scanned (reword an example comment rather than mute the file). When a tracked document must instead reproduce a tell verbatim (an old-name remap table, or a frozen dated review citing another document's numbered steps), it is boxed in the file, not path-excluded: wrap it in a fenced code block tagged host-lint:ignore (markdown only), and the naming scan skips that block while the rest of the file stays linted. A regular code block and inline backticks stay scanned, so a tell cannot be laundered by quoting it. The choice is three-way: reword a pedagogical example or a document's own ordinal label into content; box an irreducible literal citation; and path-exclude only the immutable record (the append-only memory log, dated review artifacts) and the self-referential corpus above. Never --no-verify past the gate to land a fixture. That silently defeats the lane, the same "let red hide" failure as a CI matrix that fail-fast-cancels its own jobs. (A fix to either, once found, is a behaviour change: ship it as a patch release with a matching tag.) Legitimate tell-shaped tokens are declared, not silenced. A version string, product name, or cited tracker reference that merely looks like a tell is declared in a provenance-checked allowlist (host-lint's LEXICON): each entry the full contextual phrase, masked before detection; a tracker reference carries its backing URL. Because a sound, declarable escape then exists, the identifier/reference tier MAY escalate an advisory warn into a blocking flag (the committed strict directive), so an undeclared tell-shaped token becomes a hard signal. The allowlist is provenance, not a mute button: the tool refuses a bare master key, a phrase that is itself a tell (rename it, do not declare it), and an un-cited tracker reference (#N, owner/repo#N, or an opted-in PROJ-NNNN key); URL liveness is re-derived by a network-having lane, never the offline hook.

    Prose hygiene is the same lane, applied continuously. Beyond naming tells, host-lint --prose audits authored docs for the LLM-slop prose tropes that tropes.fyi names (decoration dashes and arrows, tricolons, hypophora, and so on). The bar matches the naming audit: authored docs carry zero prose tropes, as an ongoing rule, not a one-time migration. It is wired into the receipts mechanism: the verify phase applies the prose audit and generates a receipt, and software --check re-verifies that receipt by re-running the prose audit (host-lifecycle prose, host-lint's --docs engine run in-process, so the gate needs no host-lint on PATH), so a doc that regresses to slop re-opens it as a HAZARD. The one exception is MEMORY.md, the agent's own append-only working memory: it is excluded from both the naming and prose audits via .host-lintignore, never rewritten.

    The methodology grows by reflective practice, because an agent is blind to its own drift. An agent perceives neither the register it emits nor the restatements its own change stales, so both are re-examined on purpose, prompted at the trust boundaries (the verify gate, adoption), mechanical-first, and operator-validated. Two arms run under this one principle. The first, gather, looks forward and grows the living grammar: the lane enforces a shared corpus of tells, and that corpus is incomplete by nature, so new tell-shapes that emerge in practice are swept up rather than waited for. Discovery is mechanical-first: sweep the history and recent work for a recurring shape the lane does not catch. An agent seldom perceives its own register as a tell, so this reflection is prompted, at the verify gate before a milestone closes and at adoption, and the agent assists rather than leads. The operator validates whether a surfaced shape is a genuine tell or legitimate domain vocabulary, by one test: is the shape a property of how models segment work, or of this project's own domain? Authority over a grammar change rests with the operator; an agent proposes a change and never approves one.

    A confirmed tell graduates into the shared grammar; legitimacy stays local. The two run in opposite directions because a tell is a property of machine register that recurs across projects, while a legitimate token (a version string, a product identifier, a cited tracker reference) belongs to one project. So a confirmed tell is proposed upstream to the shared grammar the lane consumes, where every project gains it on the next bump; the project that finds it does not edit that shared source itself. A legitimate tell-shaped token stays in the per-project LEXICON. The host's own maintainer validates universality and releases the graduation.

    Growth never inverts the disposition order, and it looks forward. Reword a positional or ordinal reference into content by default, since a content name is almost always available; box or declare an irreducible citation; declare the numeral-free contextual prefix when a shape is a genuine quantity; and reserve a graduation for the residue that still recurs once rewording is impossible. Harvesting at adoption proposes what to catch from then on, and leaves the immutable past alone, which migration disposes of by renaming live files and boxing frozen records. When a later grammar bump flags an existing live doc, reword it; a frozen record is boxed. A graduation that proves to over-flag is narrowed by a later grammar release, the same as any behaviour fix.

    The second arm, reconcile, looks backward at the project's own restatements. Copy-at-version keeps the verbatim spine current, but a project also restates methodology in its own prose: its room map, its components, its verifiers, its recorded layout. When a spine change moves a concept, that restatement silently drifts, because the upgrade propagates the spine yet never re-reads the paraphrase. Prefer pointing over paraphrasing: define each methodology concept once, at a stable {#id} anchor on a heading in an authored doc (its home), and point at it from elsewhere with a [text](FILE#id) link rather than restating it. For example, a home is the heading ## Components {#components} (the {#id} sits at the end of the heading) and a pointer is [components](STRUCTURE.md#components). The concepts are components, verifiers, software-root, and spec-home. The first two are project-local, read from its .host-software: the [software] members are the components (a project with a single-file entrance sets it apart with an [entrance] stanza naming that member; most have none), and the [verification] drivers are the verifiers. The last two are the fixed layout: software-root is where the project's software lives (software/) and spec-home is where its specs live, with the software. The lifecycle manifest is phases only, so no adopter inherits another project's facts; manifest --check rejects a project-fact stanza. host-lifecycle reconcile runs three checks over the tracked docs, operable at the weak-agent bar: link-integrity (every concept link resolves to its home), declared-anchor (the link names a real concept), and coverage (each project-local home names its full .host-software set, so a dropped tool fails by absence, the bite). Coverage guards the home and a pointer cannot drift; an enumeration left un-pointed is the author's choice and is not guarded. The earlier inline <!-- host-reconcile: KIND --> annotation is deprecated: it is kept checking during the transition and a surviving annotation is warned, and the form retires a spine revision later, never silently inert. The trigger is conditional and host-aware. Adoption runs the full reconcile once; for a development host that authors its spine changes, the verify gate is the binding trigger: software --check runs reconcile in its recheck. Disposition is three-way, as for a flagged tell: convert a live restatement to a pointer, box a frozen citation, forward-correct an immutable record (a call/ body, a Status: done doc, MEMORY.md). A reconcile fix stays local and never propagates, the mirror of a gathered tell graduating upstream. A sibling check closes decision-status drift: host-lifecycle validate HAZARDs an accepted call/ decision whose Scope: names host-template, since its rule is now spine-resident and belongs superseded there. The entrance check is reconcile's standalone sibling. A document read out of context cannot point at a definition, so a single-file entry restates the spine and stales, whether a front-door README or a standalone SKILL.md loaded on its own. The entrance check holds such a document by coverage and generation, the way reconcile holds a linkable one by pointers. A project declares one entrance, a global singleton, in an [entrance] stanza in .host-software: the member it belongs to (set apart from components), the document within that member (default README.md, so a SKILL.md or a landing page is reached by path), and the concepts it restates (true for every concept, or a named subset). host-lifecycle entrance --check then holds the document complete against the declared concepts: it generates the .host stamp and covers the rest. A document that restates only home-less doctrine declares no checkable concept, and the tool says so rather than claim a coverage it cannot deliver. The legacy per-member marker is retired: a surviving front-door = true or entrance = true on a [software] member is a loud error, not the entrance, so declare the stanza.

  2. Requirements: tools/allium (MIT, by JUXT). Does the software meet the behaviour the spec states? Author and maintain .allium specs through the allium skills, not by hand: elicit/distill to author, tend to evolve, weed to find spec↔code divergence, propagate to generate the tests. Gate each spec in the software's CI with allium check (structure) + allium analyse (data flow, reachability, terminal states, deadlock) + allium plan (test obligations).

  3. Timing and concurrency: tools/specula (Apache-2.0). TLA+ model checking: are the orderings and timings correct? Model-check each .tla with TLC in the software's CI.

Deeper rungs, tools/host-prove (our tooling, Unlicense). The base lanes are bounded: TLC checks one finite instance, allium's tests sample inputs. When a claim must hold for all parameter values or all inputs, host-prove drives three heavier verifiers as agentic skills, each turning the tool's output into one machine-readable verdict so the rung runs down to a small model:

  1. Symbolic / parametric: Apalache (TLA+ to SMT/Z3; the apalache-symbolic skill). Proves a .tla invariant across a whole symbolic parameter family at once, where TLC can only enumerate one instance.
  2. Proof / unbounded: TLAPS (tlapm; the tlaps-proof skill). A deductive, machine-checked proof that a property holds for all states, the must-hold-for-all claims bounded and symbolic checking cannot close. (Authoring a proof needs a strong model; the skill scopes a weak model to running and maintaining existing proofs.)
  3. Code-conformance: verify the implementation against the spec, beyond tests and trace validation. This rung is target-specific: Rust uses Kani (the kani-conformance skill), C uses CBMC, and so on. The methodology prescribes the obligation; the project picks the verifier its language supports. (Prefer byte/char-level targets: str::split/Vec make CBMC-style checkers blow up.)

Read the ladder as: hygiene, then requirements, then bounded timing (TLC), then symbolic (Apalache), then proof (TLAPS), with code-conformance (Kani et al.) the orthogonal rung that ties a proven spec to the running code. The deeper rungs are opt-in and inert (see below): nothing installs or runs until a project declares one.

Mandatory when used (RFC-2119). Adopting a lane is optional (not every project needs TLA+); once a spec of a kind exists, its tool, skills, and CI lane are required. A component carrying any .allium spec MUST wire tools/allium and its skills and run check + analyse + plan in that repo's CI, and the plan obligations MUST be discharged by the software's tests. A component carrying any .tla spec MUST wire tools/specula and TLC-check it in that repo's CI. A spec present without its full lane is a defect, not a choice. The lanes are not reference decoration. The tools are referenced submodules; their skills are generated, gitignored symlinks (link-skills.sh), wired before you author a spec. This is enforced, not only stated: host-lifecycle software --check raises a HAZARD when a materialized component carries a .allium with no allium check + allium analyse CI workflow, or a .tla with no TLC lane.

The deeper rungs work the same way, one level up: they are opt-in and inert until a project declares a rung by dispositioning an obligation kani:/apalache:/tlaps: (below). A declaration then obliges that rung's CI lane and a re-deriver that runs. software --check HAZARDs an obligation that declares kani: with no cargo kani lane, apalache: with no apalache-mc lane, or tlaps: with no tlapm lane, and it HAZARDs a declared rung whose shared re-deriver, host-prove, does not run, since a re-derivation that cannot run leaves the rung undischarged however complete the CI config reads. So install the re-deriver where the gate runs, the same way the verifier itself is installed. The digest a rung records is earned through obligations --rederive --record-digests, which re-runs the proof and records only on a pass, in place of a hand edit, so a fresh digest stands for a passing re-derivation on the current inputs. The mere presence of a .tla or a crate never activates a rung; only the declaration does, so a project pays for a heavier verifier exactly when, and only when, it chooses to.

Obligations are discharged, not just emitted. allium plan derives a test obligation for every config default, entity, enum, invariant, rule and transition. Each obligation MUST be dispositioned in a sibling <spec>.obligations manifest (the remap-dictionary discipline applied to tests) as test:<name> (a named test discharges it), structural (the spec's own check/analyse lane covers it), waived: <reason> (an honest, recorded gap), or a deeper-rung proof: kani:<harness>, apalache:<inv>, or tlaps:<theorem> (a host-prove rung discharges it, stronger than a test, for all inputs/parameters). host-lifecycle obligations <spec> --tests <dir> [--prove <dir>] fails on any undispositioned obligation, any stale disposition, any test:<name> absent from the test sources, and any rung proof name absent from the --prove sources; the software's CI runs it, and software --check HAZARDs a .allium that has no .obligations manifest. An obligation left undispositioned is a defect. Discharge is total, per component.

A verification lane reports clean only after it performs its check (no-hollow-green, call/0035). A test: disposition is name-presence until it carries an exercises=<symbol> link to a function the discharging test drives. host-lifecycle obligations --strict-discharge then HAZARDs a test: whose named test does not reference that symbol, an #[ignore]'d discharging test, a behavioural obligation relabelled structural, or a waived: lacking a reason. The link is an auditable heuristic, weaker than a proof: the claim that a test truly drives the rule is the opt-in re-derivation below, and what the tool cannot mechanically establish is labelled attested rather than asserted as proved. The strict check is staged warn-then-retire, so bumping the tool never reddens a green ladder. The build lane obeys the same rule: software --verify-build prints its clean line only when a build was verified and none was left unrun, and it exits non-zero on an in-scope build it could not rebuild, so it never attests a guarantee it did not establish.

A rung is discharged by re-derivation, not by name-presence (call/0018). That a proof exists is not that it passes. --prove only lints that the rung's named target is present (AVAILABLE ≠ DISCHARGED). The real discharge is host-lifecycle obligations <spec> --rederive <dir>, which re-runs each rung's verifier through host-prove in its recorded pinned toolchain and requires a PASS at the declared bound=, checkable anywhere, with no keys and no dependence on a specific CI; it generalizes the reproducible-build re-derivation (which reproduces a recorded artifact hash) so the same re-derivation applies to proofs as well as artifacts. The cheap offline signal is input-digest staleness: a rung may declare inputs=<files>, --rederive --record-digests fingerprints them with git hash-object into a committed <manifest>.digests ledger, and a later offline run reports the proof STALE if those inputs drifted without a fresh re-derivation. Enforcement is project-pluggable: a required check, any CI, a pre-push hook, or the operator running the verify phase; the methodology prescribes the re-derivation and ships the re-deriver, and never bakes in a CI.

Two rules govern the tools:

  • Reference, don't vendor. Each tool is a git submodule pinned to a commit. Keep its code out of this repository. This governs an adopter consuming the tools. A development host that authors a host-* tool is the exception: it develops that tool as a Where-room software component of its own (materialized and released through the lifecycle like any software it builds) and consumes the built result (binary + worktree-sourced skills) from that worktree, rather than referencing its own source as a foreign submodule. Reference is for the consumer; the producer of a tool embeds it.
  • Instruct, don't patch. Drive the tools through this manual and their own interfaces. Do not edit a tool's source to make it fit. If a tool needs a change, raise it upstream.

The output a tool produces about your project (a report, a counterexample, a generated check) belongs to your project, not to the tool's license, the same way a compiler's license does not cover the program it compiles.

The host-* tools

Three of the tools are ours, released into the public domain (Unlicense):

  • host-grammar: the shared rules for valid names and numbers. A library, not a command. Both tools below depend on it.
  • host-lint: the checker. It reads text and flags naming tells.
  • host-lifecycle: the generator. It allocates numbers and scaffolds milestones, decisions, and personas without spending model tokens on mechanical work. Run host-lifecycle next <dir> for the next number and host-lifecycle validate <dir> to check a folder. It also materialises and audits the Where room: host-lifecycle software --materialize|--check <dir> realises the .host-software bare store + worktrees and verifies each is at its pin.

Because the generator and the checker share host-grammar, what host-lifecycle emits is exactly what host-lint accepts. Trust that symmetry; do not number by hand.

The lifecycle phases, every phase emits a receipt

host-lifecycle ships one Claude skill per lifecycle phase, generated into .claude/skills/ by link-skills.sh exactly as the allium/specula skills are: classify (preview + case), adopt (governance + rooms + stamp), embed (the software as a bare store with worktrees), remap (the dictionary rename), verify (the gate sweep: validate, software --check, obligations, book --check), publish (the doc site), upgrade (the ledger), and release (the strict, tool-carried release: verify, build in the recorded toolchain, re-derive the artifact hash, re-pin, tag, receipt). Each owns the judgment around its mechanical command.

The phases (their order, modality, command, and the evidence each carries) are not re-typed in prose; they live once in the tool-readable lifecycle.manifest (one [phase "<name>"] stanza each), which host-lifecycle reads at the project's adopted .host revision for --next, the book order, and the receipt gate. Read the whole lifecycle at a glance with host-lifecycle manifest <path>.

Unlike a verification lane, which is conditional on a spec existing, the lifecycle is driven by the tool, never hand-operated, and the rule is every phase emits a receipt, not "every phase runs". Modality is first-class: a phase may be conditional (embed and release apply only with a Where room) or recurring (once per software component), so it can legitimately not run; even then it records a receipt written by the tool, in .host-receipts for a methodology-version phase (adopt, upgrade) and .host-lifecycle-receipts for an operational one: done with re-derivable evidence, skip with a cited reason, or tool-computed n-a. host-lifecycle software --check re-verifies each done by the manifest's closed recheck = and HAZARDs a phase with no receipt, the one defect the gate needs; a protected core (verify, skippable = false) refuses a skip outright. Operating a phase ad-hoc (hand-scaffolding rooms, hand-renaming files, hand-rolling the site or a release) leaves no receipt, and so is a defect by construction.

Never adopt a software repository in place

A host is a separate meta-repo; the software it governs lives beneath it as the Where room (a bare store with worktrees recorded in .host-software). The two stay separable and independently versioned. That is the whole point of "keep them separate". So at first adoption (no .host stamp yet), if the target directory is itself a software repository (it carries a root build manifest (Cargo.toml, package.json, go.mod, pyproject.toml, …) and is not already managing software via .host-software), you MUST refuse to continue. host-lifecycle classify <dir> enforces this: it prints the refusal and exits non-zero instead of a case letter, rather than letting you turn the code repo into the host.

Refusing is not the end of the task. Embed the software the right way instead: create or choose an empty host repository (e.g. agentic-<name>), adopt it there, then add the software as the Where room with a [software "<name>"] stanza in the host's .host-software (the repo's URL, a pinned SHA, the worktree set) and software --materialize. The classify refusal prints these exact steps.

Audited plans and append-only memory

Two disciplines keep the host trustworthy across sessions.

  • Audited plans. Every change to plan/ (the milestone index or any milestone document) and every decision in call/ is committed and pushed immediately, in its own commit, not batched with code. After you finish a step in the software, update the plan to say what you actually did, in a separate commit.
  • Append-only memory. MEMORY.md is a running log of decisions, discovered constraints, and lessons. Add a short entry whenever you finish something significant, hit a non-obvious bug, or find an unexpected constraint, as you go, not at the end. Commit it on its own and push it. Never rewrite or delete an old entry; if one was wrong, add a new entry that corrects it and points back.

The test for both: a new session with no memory of this conversation should be able to read plan/, call/, and MEMORY.md and continue without repeating a past mistake.

The two-tier memory store and the dream audit

Memory has two tiers with opposite rules, and one audit that holds both.

  • The repo tier: MEMORY.md, append-only. The project memory log above is the shared, committed, audited tier. Never rewrite or delete an old entry; a correction is a new entry that points back.
  • The per-user tier: the host- store, editable.* An operator may carry a private, local, uncommitted store at ~/.host-memory/<project>/, where <project> is the project's working directory with each / replaced by -. One markdown file per entry: YAML frontmatter (description:, the one line recall keys on; type:, one of feedback, fact, workaround, state; created: and last_edited: dates; superseded_by:, a slug or empty) plus a free-form body with optional [[slug]] cross-references, and a MEMORY.md index carrying one bullet per entry that mirrors its description: line. This tier is editable in place: entries may be rewritten, pruned, and relinked as understanding improves.

The asymmetry is load-bearing: the repo log is the project's immutable record, the per-user store is the operator's working memory. `host-lifecycle dream

` is the advisory audit over both. It flags staleness, contradictions, drift, dangling links, and append-only violations on the repo tier, then routes each finding by store: a repo-tier finding suggests an append (the correction text and the forward link to add, printed verbatim); a per-user finding suggests an in-place edit. `dream` writes nothing in the memory stores; the tier-marker file below is its sole repo-side write surface. `--fix` refuses the repo store outright, and on the per-user store it applies only mechanical, structure-only classes (index/description reconciliation, dangling-link repair), a set that grows one class at a time, each class landing only with cast-review sign-off; at this revision no class is auto-applied yet. The detectors are heuristic and advisory by design: a finding is a prompt for operator judgment, not a proof of staleness.

The tier marker and finding confidence. The per-user tier's in-use status is declared state: a tracked .host-memory-tier marker, written by dream when it first observes an initialized store on a machine (commit the stamp; it carries a date-and-author provenance line), retired only by the operator with dream --retire-marker and recorded as an appended MEMORY.md correction. The marker never flips on one machine's absence evidence, and a store observed after retirement is a contradiction finding, never a silent re-stamp. [[links]] resolve against the union of both tiers. With no marker, an unresolved link is a confirmed finding whose remedy leads with the operator's initialization fork; with the marker and no store on the machine at hand it is advisory (leave the link in place; never drop it on that machine's evidence); with marker and store present it is advisory create-or-correct; with the marker retired, unresolved links are confirmed again (retirement is the pressure valve), and re-opting in rides an appended correction plus marker removal so the next run re-stamps, never a hand-edit to stamped. Every finding carries its confidence in prose, confirmed or review-prompt, and dream exits 0 clean, 3 advisory-only, 1 on any confirmed finding, 2 on input errors; per-tier coverage lines state what ran, what was inapplicable and why, and the marker's provenance.

Cadence. Run dream at the start of a session that will rely on recall, and again after a session that superseded a decision. It is advisory, never a gate: findings inform, they do not block.

Boundary. dream audits memory content only. It never proposes a methodology-version migration (that is the upgrade ledger's job), and a finding that touches a call/ or plan/ record routes to the MADR path by naming the room and the record, never by editing them.

Scope. The host-* per-user store is the reference tier; vendor harness memory stores are not read. An operator on a harness without the store gets the repo tier alone. The store is per-machine: it does not sync across machines, and moving or renaming the project directory starts a fresh store, since the encoded path changes. The memory tools ride the host-lifecycle mcp stdio server as memory_list, memory_read, memory_write, and memory_consolidate; memory_write writes only to the per-user store, never the repo tier.

The task graph: in-plan tasks are receipted nodes

A milestone's ## Build sequence is not loose prose. Each step is a task: an anchored ### heading under that section, ending in {#anchor} (the placement stock mdBook honors), for example ### Gather the data {#gather-data}. Its identity is global, plan/NNNN#anchor, so a receipt and a dependency hang on a stable anchor, never a position that renumbers when a plan is re-cut. An anchored ### heading belongs only under ## Build sequence, and a build-sequence ### without an anchor is refused, so a task is never confused with an ordinary subsection.

A few bullets under each task heading carry its fields:

## Build sequence

### Gather the data {#gather-data}

- verify: cargo test gather
- inputs: src/gather.rs

### Ship it {#ship-it}

- depends: #gather-data
- verify: attested call/0007
  • depends names the prerequisites: a local #anchor, or a cross-milestone plan/NNNN#anchor. A task with no depends takes the previous task in the section (the linear default), and the first task is a root. The project's tasks form one graph across milestones, and host-lifecycle tasks derives the ready frontier (the tasks whose prerequisites all carry a done receipt), which a coordinator may run in parallel.
  • verify is a command the gate re-runs (mechanical), or attested <call/NNNN | operator> (a decision the gate resolves, or an operator confirmation).
  • inputs names the files a mechanical verify covers, fingerprinted so the gate flags a done as stale once they drift (call/0018's input-digest staleness, one level down).

Declare prerequisites; the tool derives parallelism. State for each task only what must finish before it. The needs-question is local and conservative, and a missing edge merely over-serializes (safe), while a guessed "independent" edge races two workers (a corruption), so the author never answers "what can run at once?" A coordinator fans a frontier out to parallel workers only when they are resource-isolated (separate worktrees), since depends orders work, it does not lock a shared resource.

Every task emits a receipt, and the gate is mandatory. host-lifecycle tasks --record writes a receipt into .host-task-receipts (it reads the task's own verify/inputs, so you never re-type them), a tool-written ledger you never hand-edit. software --check HAZARDs a task with no receipt, a done whose mechanical inputs drifted or whose citation does not resolve, a skip without a resolvable call/NNNN, and an orphan receipt whose task was renamed or removed. A done is re-derivable, never self-asserted: the cheap gate checks the input digest, and tasks --rederive re-runs the command and refreshes it. These task receipts are a third receipt kind beside the methodology-version and operational ledgers.

Software and submodule discipline

The tools are submodules; the software is a bare store with worktrees. Both follow a commit-upstream-first rule:

  • A tool submodule: commit and push inside it (on main) first, then commit the updated submodule pointer in the host and push.
  • The software: commit and push inside the canonical worktree first, then record the new SHA as the .host-software pin and push that host commit. The recorded pin is the audit anchor a gitlink used to be.

Never push a host commit whose tool pointer or software pin is not yet pushed. If a push fails (no network, no auth), stop, tell the human which commits are unpushed, and do not start work that depends on them.

Tag every release. A version bump (a change to the version in a tool's or the software's manifest (Cargo.toml, …)) MUST be accompanied by a matching annotated git tag vX.Y.Z at the release commit, pushed alongside it (git tag -a vX.Y.Z -m "<name> vX.Y.Z" && git push origin vX.Y.Z). The tag is the release: a tag-triggered CI job builds the artifacts from it (e.g. a v* release workflow). An untagged version bump is an unreleased version, a defect; back-fill the tag at its bump commit. Do not re-pin .host-software (or a tool pointer) to a version-bumped commit that carries no matching tag.

Worktree-absence coherence. A separately-materialized path (the software worktree, or a tool submodule) is absent (or empty) until materialized; a fresh clone, CI, and a partial submodule init do not have it. So do not git-track an artifact that depends on such a path existing (a skill symlink into <software>/ or into tools/<tool>/skills/): gitignore it and generate it after materialization: link-skills.sh produces .claude/skills/* for the tools present, and the software's links are recreated after software --materialize. Where an automated context genuinely needs the path, it materializes first; otherwise it must tolerate the absence. host-lifecycle software --check flags any tracked symlink whose target is not itself tracked here as a HAZARD. And: an un-materialized CI job must exercise each runtime-critical artifact. "done" means the whole CI sweep is green, not one artifact built.

Worktrees live under software/. Every materialized Where-room worktree MUST surface at software/<name>/<branch>/ under the host root, never a bare external path disjoint from the tree. The rule an agent relies on is: if you build it, its files live under the host root, so an edit through the default-cwd path lands in the tree under test. When a backing store genuinely must live elsewhere (another filesystem or platform (e.g. a native-Windows build that cannot sit on a WSL share)), record it on the parallel-worktree line as store=<path> (and optionally host=<os>, the OS that materializes the store, i.e. where you run host-lifecycle, not the build platform; for a Windows Dev Drive reached from WSL that is linux, even though the build's own attest-host is windows). Off-platform, host= makes --materialize/--check skip the line rather than fail. software --materialize then realises the store at that path and the in-tree software/<name>/<branch>/ as a symlink / directory junction / bind-mount to it. host-lifecycle software --check HAZARDs any recorded worktree path that escapes the host root, and any store= line whose in-tree handle is missing or does not resolve to the store. A disjoint external worktree with no in-structure handle is the wrong-tree footgun (edits silently land in a tree not under test) and is a defect, not a layout choice.

Reproducible builds, the production anchor. Software initiated under the methodology has reproducible builds: its deployed artifact MUST be byte-reproducible from the pinned source plus a recorded build recipe (a pinned toolchain and build command in .host-software). That is what makes the pin a true production anchor (a clean rebuild from the pin equals what is deployed) rather than just a source pin. Record per component which line ships (deploy) and the artifact's expected hash (artifact = <path> <sha256>); host-lifecycle software --check attests these cheaply (a present artifact that matches is verified; one built by a local toolchain that differs from the canonical hash is noted, not failed, the same reasoning --install-hooks uses, since the recorded hash is the pinned build host's output), and a CI job runs host-lifecycle software --verify-build to rebuild from the pin and fail unless the artifact reproduces. --verify-build, not --check, is the reproducibility proof. For greenfield software, non-reproducibility is a defect designed out from the start.

Hermetic builds, the dependency bundle. Reproducibility and hermeticity are the same lane: a component that ships static or self-contained release binaries MUST be able to reproduce them offline from pinned inputs, never from whatever a network fetch returns at build time. The recommended mechanism is a reusable, versioned, hash-pinned dependency bundle: vendor the dependency layer once, publish it as a downloadable release (the pattern pgs-release uses for its prebuilt sysroot), and have every build download and verify that bundle and build with no network. Record it per component as deps-bundle = <url> <sha256> in .host-software. host-lifecycle software --verify-build and release then perform the one controlled, pinned download, verify the sha (the provenance half of the gate), stage the vendored sources, and build under --network none (the egress half). The gate invariant is specific and enforceable: a component recording a deps-bundle MUST build offline, its staged bundle sha MUST match the recorded one, and software --check HAZARDs a deps-bundle pin that has drifted from the producer's committed deps-bundle.lock. A component that genuinely cannot vendor offline, such as one with a network-fetching build.rs or a non-Rust toolchain, may carry hermetic-exempt = call/NNNN citing a software-scoped case decision, the same escape shape as repro-exempt; the exemption is never available where offline vendoring is feasible.

Multi-platform builds. A component whose one source pin ships on several platforms records one [build "<name>" "<platform>"] subsection per platform under its [software "<name>"] stanza, each carrying its own build/toolchain/artifact/deploy (and optional repro-exempt) plus an attest-host naming the OS (linux, windows, macos) that reproduces it. --check and --verify-build iterate the builds and attest each only on its attest-host. A build whose host is not the current one is skipped, and the run does not fail (a Linux runner cannot reproduce the Windows artifact, and is not asked to). The flat single-build fields remain the form for a single-platform component.

Escape clause (migrated software only). Pre-existing software brought under the methodology may not be reproducible yet. It may carry repro-exempt = call/NNNN citing a recorded case decision, a software-scoped call/ decision documenting why it is not yet reproducible and the interim provenance. --verify-build then warns and skips the rebuild comparison; --check still requires the citation to resolve. The exemption is meant to be retired as the component converges on reproducibility, and is never available to greenfield software.

Upgrading

Adopting is one event; the template moves on. The .host stamp records the baseline ledger entry (every entry at or before its position in UPGRADING.md counts as applied). The applied set of out-of-order entries lives in .host-receipts (the methodology-version trail); host-lifecycle migrate-receipts moves it there from a legacy .host. UPGRADING.md is the ledger of actions, one [upgrade "<revision>"] stanza each, ordered by file position; a stanza may declare independent or depends = <id> … (logical prerequisites, distinct from the requires tool-version floor) and a verify post-condition.

To upgrade, fetch the template to the target revision, then:

  • host-lifecycle upgrade <dir> lists every ledger entry not yet applied, by ledger position, never git ancestry (ledger SHAs are a linear-commit artifact and some are orphaned from HEAD). A legacy single-revision stamp is migrated once to a baseline. upgrade --next prints the single next safe action.
  • Apply an entry, then record it with host-lifecycle upgrade --record <id> (an id, an unambiguous prefix, or a ledger ordinal). The tool validates the id, refuses if a depends is unapplied, runs the entry's verify post-condition (or, when it has none, requires an explicit --unverified call/NNNN citation) and appends an append-only claim. You never hand-edit the stamp.
  • A late independent entry may be cherry-applied without an earlier unrelated one: the deferred entries stay pending and re-list. A forgotten or premature record can never silently hide owed work (fail-safe). upgrade --advance later compacts a contiguous applied run into the baseline.
  • host-lifecycle software --check re-checks every recorded claim (a verify that no longer holds, or an applied entry whose depends is unapplied, is a loud HAZARD).

The tool carries the process. Even a low-capability agent upgrades by reading one line and running one command, never editing the stamp by hand. Re-stamp is the tool's job, not yours.

Provenance

The four working principles are rewritten, in our own words, from observations by Andrej Karpathy on where LLM coding goes wrong, by way of Jiayuan Zhang (@forrestchang) and the andrej-karpathy-skills contributors. The persona process in cast/applying-personas.md follows Powell, Keenan and McDaid (2007) on personas in XP, with later agile work cited alongside it. This manual is released into the public domain (Unlicense); the credit here is acknowledgement, not a license obligation.

Priority rule: this host never references an upstream tracker

This host repository is public. GitHub mints a timeline event on the target whenever a pushed commit message carries a cross-repository autolink, so a reference written here surfaces inside the upstream thread and pulls the governance repo into a conversation that is about the package. Two host commits did this before the rule existed, one through each form.

  • Never write a linking form in a commit message pushed from an agentic-* host. The forms that link are owner/repo#N and any github.com/<owner>/<repo>/issues|pull|commit/... URL. Name the work without the link instead: openwrt/packages PR 30228.
  • The same rule binds an issue or pull request body or comment posted from here, which no hook can gate. Write the plain form there too.
  • tools/crossref-check.sh enforces the commit half. It installs as this repository's pre-push hook (crossref-check.sh --install) and refuses a push whose range carries either form.
  • The rule is scoped to the host, and the packages worktree follows the upstream tree's own convention. A commit in the packages worktree carries no issue closer: upstream requires no Closes: trailer, and the tree's convention is the closing keyword in the pull request body, where it fires once at merge instead of re-posting a reference on every push of an amended branch. Write the plain same-repo form there (Closes #N), never a linking form. The openwrt-package-commit-style manual in host-lint-openwrt states the rule and holds the measurement it rests on.
  • A file is not a reference. Only a commit message or a posted body mints one, so a URL already inside MEMORY.md, plan/ or call/ stays as written.
  • A minted event cannot be retracted. No API deletes it, and a force push only makes the commit unreachable. Prevention is the whole remedy.

Priority rule: a rebase updates every reference to what it rewrote

A rebase, an amend or a force push makes the old commit unreachable. GitHub keeps the object addressable, so a stale reference still answers and fails only where it counts: a fresh clone fetches reachable objects alone, so software --materialize cannot check out a pin that was rebased away, and the gate dies two steps before the build it exists to verify. Six pins drifted this way and the reproducible-build lane stayed red for a day.

  • Re-pin in the same session as the rebase. A force push to any branch recorded in .host-software obliges an update to that pin or worktree line. Push it with the rebase rather than leaving it for later.
  • Reachability is the test, not resolvability. The API resolves an unreachable object, so a successful lookup proves nothing. Compare against git ls-remote --heads, which lists only what a clone will fetch.
  • A record that cannot be re-pinned is superseded, not edited. A call/ decision or a MEMORY.md entry naming a rewritten SHA earns a new entry that points back, under the append-only rule.

Project specifics: UDPspeeder-simd

The rules above are the inherited spine. The facts below are this project's own, recorded so an agent does not have to re-derive them from the code.

  • What it is. A Forward Error Correction UDP tunnel that improves a high-latency lossy link with Reed-Solomon redundancy. SIMD-accelerated. Forked from UDPspeeder.
  • Language and build. C++. Build it with make (the flat makefile) or with CMake (cmake -B build && cmake --build build). The software is C++ only.
  • Style. .clang-format is authoritative; match the surrounding file exactly (braces, indentation, naming) rather than restyling.
  • Canonical branch. main. The audited worktree is software/udpspeeder-simd/main/, pinned in .host-software.
  • License. The software is MIT (its LICENSE.md lives in the worktree). This governance shell is Unlicense. Keep the two separate.
  • Reproducibility. Brought under the methodology, not initiated by it: the stanza records a source pin with no deploy or artifact, so no reproducible build is claimed yet. Add a build recipe and a repro-exempt = call/NNNN (or a real artifact) before shipping a binary through the host.
  • Lanes. No .allium or .tla spec exists yet, so the requirements and timing lanes are inert. Author a spec through its skills and wire its CI lane before committing it; a present spec without its full lane is a defect.

Project specifics: the packages lane

A second software component, packages, is embedded as a Where-room lane alongside UDPspeeder-simd, recorded in .host-software.

  • What it is. A fork of the OpenWrt packages feed (openwrt/packages): build recipes (Makefiles), patches, and options for OpenWrt applications and libraries. Makefile and shell, with some C. Layered on an OpenWrt buildroot via the feed system.
  • Canonical branch. master. The audited worktree is software/packages/master/, pinned in .host-software.
  • License. The feed is GPL-2.0 (its own LICENSE lives in the worktree). This governance shell is Unlicense. Keep the two separate.
  • Reproducibility. Migrated, not initiated here: a source pin on master with no deploy or artifact, so no reproducible build is claimed. It is a build-recipe feed, not a built artifact.
  • No Claude co-author trailers. Every commit made in the packages worktree omits the Co-Authored-By: Claude ... trailer, so the feed stays upstream-clean for openwrt/packages. This overrides the default trailer behaviour for the packages component only; host-repo commits keep it. See call/0003.

MIGRATION.md: the process moved to the host repo

How to bring an existing repo under this methodology, and how to upgrade one that adopted an earlier revision, now lives as a single instruction file in its own repo:

https://github.com/connollydavid/host

Point an agent or a patient human at that URL and follow it. The host file owns the adopt / migrate / upgrade procedure.

This template owns the techniques that procedure installs (the CLAUDE.md spine, the room scaffold, the tool pins) and the revision-keyed upgrade ledger, UPGRADING.md. The procedure is stable and lives in one place; the ledger changes with the techniques that generate it, so it stays here with them.

Structure

An agentic project (e.g. agentic-acme) is the externalized thought about the work; the software under development is the action, hosted beneath it. Its rooms map to the five W's.

Wroomholds
Whocast/personas (Powell, Keenan & McDaid 2007), examples Mara + Wren
What<software>/ (with the code)behavioural (.allium) + temporal (.tla) specs, verified in the software's CI; the host plan/ references them
Whenplan/the milestone index and folders
Where<software>/the hosted software, a bare store with worktrees; you add it
Whycall/decisions about the software (MADR; see call/0000); methodology lives in the spine, not here
HowCLAUDE.md + tools/the verification lanes

tools/ are referenced submodules, each under its own license; we orchestrate and wrap, never patch:

  • tools/host-lint (Unlicense): hygiene / anti-slop; checks names against host-grammar.
  • tools/host-lifecycle (Unlicense): token-free scaffolder/validator; generates names from host-grammar, so what it emits is exactly what host-lint accepts.
  • tools/allium (MIT, JUXT): requirements + property-based testing.
  • tools/specula (Apache-2.0): timing and concurrency via TLA+.

(host-grammar, the shared naming/numbering rules crate, is a build dependency of host-lint and host-lifecycle, not a host submodule.)

A lane is mandatory once a spec of its kind exists (RFC-2119 MUST; see CLAUDE.md): a .allium requires tools/allium + its skills + a check/analyse/plan CI lane with the obligations discharged by tests; a .tla requires tools/specula + a TLC lane. Adopting a lane is optional; ignoring a present spec's lane is a defect.

.claude/skills/ are symlinks into those submodules' skills (reference, not copy). They are generated, not tracked: link-skills.sh creates a link for each materialized tool (skipping uninitialized submodules), because a tracked symlink into an uninitialized tool dangles and trips any tree-walking tool. Run it after git submodule update --init. Tool outputs are project-owned. host-lifecycle ships one skill per lifecycle phase (classify, adopt, embed, remap, verify, publish, upgrade, release), enumerated once in the tool-readable lifecycle.manifest; unlike a lane, the lifecycle is driven by the tool, and the rule is every phase emits a receipt (done/skip/n-a in .host-receipts), not "every phase runs". A phase with no receipt is a software --check HAZARD (CLAUDE.md).

The Where room is the software under test, one or more components, each embedded as a bare store with worktrees under software/<name>/: the shared object store is software/<name>/.git, and each worktree is keyed by branch at software/<name>/<branch>/ (the branch keeps its slashes, so feature/login nests). The canonical worktree (the audited state, where CI runs) is the component's recorded branch (default main) checked out at the pin; the rest are parallel lines, one per agent or live release branch. These trees are local and gitignored (a single /software/ entry); the host commits a recipe (.host-software) with one [software "<name>"] stanza per component (mirroring .gitmodules), each recording the source URL, the pinned canonical SHA, the canonical branch, and any parallel-line branches, which host-lifecycle software --materialize realises and --check audits. Operate on a single component, or one branch worktree of it, with --item <name>[@<branch>]. The recorded pin replaces a submodule gitlink as the reproducibility anchor, so several branches stay materialized at once where a single submodule tree could not. Software initiated under the methodology has reproducible builds: the stanza also records the build/toolchain recipe and the deployed artifact hash, so host-lifecycle software --verify-build can rebuild from the pin and prove the deployed binary; migrated software not yet reproducible carries a repro-exempt = call/NNNN case decision (see CLAUDE.md). A component that ships static or self-contained binaries also records a deps-bundle = <url> <sha256>, a pinned vendored-dependency bundle it downloads, verifies, and builds offline against under --network none, so the build is hermetic (see CLAUDE.md). A component that ships on several platforms records one [build "<name>" "<platform>"] subsection per platform (each a distinct toolchain/artifact of the same pin, with an attest-host naming the OS that reproduces it); the flat single-build form stays valid for the single-platform case.

To instantiate: clone, git submodule update --init (the tools), run ./link-skills.sh (regenerate the skill symlinks for the tools you initialized), replace the cast/ examples with your own personas, and set up your software as a bare store with worktrees (above). For bringing an existing repo under the methodology instead, follow the host repo (github.com/connollydavid/host).

To publish docs with mdBook, run host-lifecycle book ., the canonical publisher, so you do not hand-roll a generator that drops a room or re-derives the src-scoping wrong. It writes a book.toml at the repo root scoped to a generated mdBook/src/ (never src = ".", which would walk the tool submodules and the un-materialized software worktree and trip over whatever is not present), with the built HTML in mdBook/out/, and a SUMMARY.md in lifecycle order: Cast (Who), then Plan (When), then Software/Where (the What specs live with the code, read as a stub from .host-software), then Call (Why), then Reference/CLAUDE (How), then Memory. Then host-lifecycle book --check . fails the build unless every room with source renders a page, so a half-room site cannot ship. book.toml and the whole mdBook/ tree are generated output (gitignored), so a project keeps docs/ for its own authored documentation; the reference Site workflow under .github/workflows/ runs both before mdbook build, which still runs from the repo root.

A migrated or instantiated repo carries a .host stamp at its root recording the template revision it adopted (template/revision/adopted), written by host-lifecycle adopt. It is what a later upgrade diffs from. An optional name line pins the published book's title, so host-lifecycle book does not derive it from the checkout directory.

The methodology lives in CLAUDE.md. Read it first. The whole template is released into the public domain (Unlicense); see README.md for provenance.

UPGRADING.md: the version-to-version upgrade ledger

Adopting the methodology records a template revision in .host. The template then moves on, and an adopted repo must upgrade across the revision span. The upgrade re-applies spine doc changes and the structural migrations the span introduced (a re-embedding of the software, a tool bump). A git diff of the template shows the prose; it does not say "convert the submodule" or "untrack a symlink". This ledger does.

Apply every entry below whose revision is newer than your stamp. host-lifecycle upgrade <dir> prints exactly those, decided by git ancestry against this template, so same-day revisions order correctly. Fetch the template to the target revision first; then apply the list and re-stamp. Each entry is keyed by the template revision at which its action became required.

[upgrade "8c28e33"] title = Software is a bare store with worktrees (call/0004) action = Convert the embedded gitlink submodule to a bare store + worktrees (see the host repo, converting an existing submodule): preserve the pin, de-register the gitlink, write .host-software, gitignore the trees, then host-lifecycle software --materialize. requires = host-lifecycle v0.3.0

[upgrade "325f2cf"] title = Worktree-absence coherence (call/0005) action = Untrack artifacts that depend on the worktree (skill symlinks into the software path); gitignore them and recreate after materialize. Keep an un-materialized CI job green. host-lifecycle software --check must report no HAZARD. requires = host-lifecycle v0.3.1

[upgrade "71d12a8"] title = Coherence generalized to tool submodules (call/0005) action = Re-run host-lifecycle software --check (now flags any tracked symlink whose target is not tracked here, not just software-worktree paths). Untrack any flagged tool-skill symlinks and generate them instead (the link-skills.sh pattern); keep them gitignored. requires = host-lifecycle v0.4.1

[upgrade "bbbfdc3"] title = Reserve agentic-host for the meta repo; stamp is .host, scaffold is host-template action = Rename your .agentic-host stamp to .host. Re-point your template-agentic-host submodule and its URL to host-template. A repo that adopts the methodology is "an agentic project" (e.g. agentic-acme); agentic-host now names only the meta repo. requires = host-lifecycle v0.5.0

[upgrade "7ae93cd"] title = Publish via host-lifecycle book (the canonical doc-site publisher) action = Replace any hand-rolled mdBook generator/SUMMARY with host-lifecycle book .; it writes book.toml (src = "docs", never ".") + docs/ in lifecycle order, renders specs, and emits a Where stub from .host-software. Run host-lifecycle book . && host-lifecycle book --check . before mdbook build (see .github/workflows/site.yml); gitignore the generated book.toml/docs/. requires = host-lifecycle v0.6.1

[upgrade "6db01f3"] title = Anti-ouroboros: call/ is for the software, methodology lives in the spine action = The methodology is owned by the spine (copy-at-version), not your call/. In a single dedicated commit, set Status: superseded by the spine on every accepted call/ decision that merely restates a methodology rule now settled upstream (leave the file in place, since MADR records are immutable). Add a Scope: header to each remaining accepted decision. Do not treat any host/management repo's top-level instance rooms as normative. host-lifecycle validate <call-dir> now fails an accepted decision missing Scope: or declaring Scope: methodology. requires = host-lifecycle v0.7.0

[upgrade "07025a7"] title = book publisher v0.7.1: nested specs + retired-decision Archive action = Bump your pinned host-lifecycle book revision to v0.7.1. Two rendering changes follow on the next build: specs nested in spec/<topic>/ now render (previously dropped), and decisions whose MADR Status: is superseded/deprecated/rejected move into a trailing "Archive / Record" section (banner + nav-label suffix) instead of shipping as current chapters. The record signal is Status: only; .host-lintignore does not affect the book. requires = host-lifecycle v0.7.1

[upgrade "e3b174d"] title = Reproducible builds are the production anchor action = Record build provenance in .host-software per component: build + toolchain (the deterministic recipe), deploy (which line ships), and artifact = <worktree-path> <sha256>. Add a CI job that runs host-lifecycle software --verify-build (rebuild from the pin; fail unless the artifact reproduces); see the reference .github/workflows/reproducible-build.yml. Software initiated under the methodology must reproduce. Pre-existing software not yet reproducible may carry repro-exempt = call/NNNN citing a software-scoped case decision; the exemption is retired as it converges and is never available to greenfield software. requires = host-lifecycle v0.8.0

[upgrade "c137567"] title = Per-platform builds for multi-platform software action = For a component whose one source pin ships on several platforms, replace the flat single-build fields with one [build "<name>" "<platform>"] subsection per platform under its [software "<name>"] stanza, each with its own build/toolchain/artifact/deploy (and optional repro-exempt) plus an attest-host naming the OS (linux/windows/macos) that reproduces it. host-lifecycle software --check/--verify-build then attest each build only on its attest-host, skipping foreign-host builds rather than failing. Single-platform components need no change: the flat form stays valid. requires = host-lifecycle v0.9.0 independent = true

[upgrade "d3dc5ed"] title = --check artifact mismatch is a note, not a failure action = Bump your pinned host-lifecycle revision to v0.9.1. software --check no longer hard-fails when a present artifact's hash differs from the recorded canonical hash (a local-toolchain build legitimately differs); it prints a note instead, the way --install-hooks already does. A match still reports verified; --verify-build (the container/CI lane) remains the reproducibility proof. No recipe change is required; this only removes a dev-box false failure. requires = host-lifecycle v0.9.1 independent = true

[upgrade "b6232a5"] title = Specs live with the software, not the host plan tree action = Move any behaviour (.allium) or timing (.tla) specs out of the host's plan/<milestone>/spec/ and into the software repo, beside the code they constrain. Add the verification lane to the software's CI, not the host's: allium gated with both allium check and allium analyse (install allium-cli@3.4.2; both exit non-zero on any error or warning), and specula/TLC (tla2tools v1.8.0 on a Temurin 21 JDK). Remove the host's spec copies and its specula workflow; leave a forward note in the affected milestone. The host plan/<milestone>/ now references a spec by path and software pin rather than containing it. requires = host-lifecycle v0.9.1

[upgrade "c771d60"] title = Verification lanes are mandatory when a spec of their kind exists action = Wire the lane tools you use as submodules and generate their skills (gitignored) with link-skills.sh: tools/allium (+ its elicit/distill/tend/weed/propagate skills) for any .allium, tools/specula for any .tla. Author and maintain .allium specs THROUGH the skills, not by hand. In the software repo's CI, gate each .allium with allium check + allium analyse + allium plan (install allium-cli@3.4.2; both check and analyse exit non-zero on any error or warning) and discharge the plan test obligations with the suite; TLC-check each .tla (tla2tools v1.8.0 on Temurin 21). The lane is conditional (TLA+ stays optional until a .tla exists, allium until a .allium exists), but a spec present without its full lane is a defect, not a choice. Do not treat the lanes as reference decoration. requires = host-lifecycle v0.9.1 depends = b6232a5

[upgrade "b8c54fc"] title = The spec-lane MUST is enforced by software --check action = Bump your pinned host-lifecycle to v0.10.0. host-lifecycle software --check now raises a HAZARD when a materialized component carries a .allium spec with no CI workflow running allium check + allium analyse, or a .tla with no TLC lane (tlc2.TLC/tla2tools). Wire the missing lane (per the previous entry) so --check is clean; an un-materialized worktree is skipped. requires = host-lifecycle v0.10.0 depends = c771d60

[upgrade "821a216"] title = Allium plan obligations must be dispositioned per component action = For each .allium, add a sibling <spec>.obligations manifest that dispositions every obligation allium plan derives, one of test:<name> (a named test discharges it), structural (the check/analyse lane covers it), or waived: <reason>. Run host-lifecycle obligations <spec> --tests <dir> in the software's CI (it fails on any undispositioned/stale obligation or absent test ref). Bump host-lifecycle to v0.11.1, where software --check also HAZARDs a .allium with no .obligations manifest. requires = host-lifecycle v0.11.1 depends = b6232a5 c771d60

[upgrade "f62d766"] title = Lifecycle phases are an unconditional MUST, driven by host-lifecycle skills action = Bump tools/host-lifecycle and run link-skills.sh so the phase skills (classify, adopt, embed, remap, verify, publish, upgrade) appear under .claude/skills/ alongside the allium/specula skills. Operate the methodology through these phases and their commands, never ad-hoc; the phases cover scaffolding, embedding, migration, verification, publishing, and upgrading. Unlike a verification lane, this is unconditional: there is no opt-out, and hand-operating any phase is a defect. requires = host-lifecycle v0.11.1 independent = true

[upgrade "0e83e3f"] title = Tag every release (a version bump MUST carry a matching vX.Y.Z tag) action = Audit your tools and software for version bumps that were never tagged (git tag vs the manifest version); back-fill an annotated vX.Y.Z tag at each bump commit and push it. From now on, every version bump is committed with its matching tag, pushed alongside; the tag is the release a v* CI workflow builds. Do not re-pin .host-software or a tool pointer to a version-bumped commit that has no matching tag. requires = host-lifecycle v0.11.1 independent = true

[upgrade "ae1e688"] title = Never adopt a software repository in place action = Bump your pinned host-lifecycle to v0.12.0 and re-run the classify phase as the first step of any adoption. host-lifecycle classify <dir> now refuses (exits non-zero, prints the embedding steps) when the target is itself a software repository (a root build manifest (Cargo.toml, package.json, go.mod, pyproject.toml, …) with no .host stamp and no .host-software recipe) instead of printing a case letter. A host is a separate meta-repo; the software is embedded as its Where room. If you previously adopted a methodology host directly on top of a software repo, split them: move the code into a bare store with worktrees recorded in the host's .host-software, so the host root carries only the rooms and config. requires = host-lifecycle v0.12.0 independent = true

[upgrade "7de7cb1"] title = Materialized worktrees must live under the host root action = Bump your pinned host-lifecycle to v0.13.0. Audit .host-software for any worktree whose path escapes the host root (an absolute or ..-climbing worktrees/worktree entry, or a Where-room worktree materialized at a disjoint external path with no in-structure handle). Bring each under the root: for a store that must live off-tree (another filesystem or platform), record it on the parallel line as worktree = <dir> <branch> <pin> store=<path> [host=<os>] and re-run software --materialize, which realises the store at <path> and the in-tree <dir> as a symlink/junction to it. software --check now HAZARDs an escaping path and a store= line whose in-tree handle is missing or does not resolve to the store; host= gates a line to one OS so a foreign-OS CI skips it rather than failing. requires = host-lifecycle v0.13.0 independent = true

[upgrade "3f7c065"] title = Honest partial upgrades: the applied-set stamp model action = Bump your pinned host-lifecycle to v0.14.0. The .host stamp now records what is applied as a baseline ledger entry (every entry at or before its position in this file counts applied) plus an optional applied set of out-of-order entries, keyed by ledger position, never git ancestry. Your first host-lifecycle upgrade migrates a legacy single-revision stamp once (derives the baseline; no manual edit). Thereafter: upgrade lists pending by position, upgrade --next prints the single next safe action, and you record an applied entry with host-lifecycle upgrade --record <id> (id, unambiguous prefix, or ledger ordinal), which validates the id, refuses if a depends is unapplied, runs the entry's verify post-condition or requires --unverified call/NNNN, and appends an append-only claim. Never hand-edit the stamp. A late independent entry may be cherry-applied without an earlier unrelated one (deferred entries stay pending and re-list, so a forgotten or premature record can never hide owed work); upgrade --advance compacts a contiguous applied run into the baseline; software --check re-checks every recorded claim. requires = host-lifecycle v0.14.0 independent = true

[upgrade "4a98d92"] title = Deeper verification rungs: Apalache (symbolic), TLAPS (proof), Kani (code-conformance) action = Bump your pinned host-lifecycle to v0.15.0. The verification ladder gains opt-in deeper rungs above the bounded lanes, driven by a referenced tools/host-prove submodule (wire it and run link-skills.sh only if you use a rung; its apalache-symbolic, tlaps-proof and kani-conformance skills turn each verifier into a one-line verdict). An obligation may now be dispositioned kani:<harness>, apalache:<inv> or tlaps:<theorem> instead of test:/structural/waived:, since a proof discharges it for all inputs/parameters; host-lifecycle obligations <spec> --prove <dir> validates that the named proof exists. Declaring a rung obliges its CI lane: software --check now HAZARDs an obligation that declares kani: with no cargo kani lane, apalache: with no apalache-mc lane, or tlaps: with no tlapm lane. Nothing is required until you declare a rung, and bare .tla/crate presence never activates one. Route a parametric or unbounded .tla invariant to Apalache/TLAPS, and a code-conformance claim to your language's verifier (Rust uses Kani); prefer byte/char-level Kani targets (str::split/Vec blow CBMC up). Verifiers install from official prebuilt binaries pinned by version + SHA256; no Docker. requires = host-lifecycle v0.15.0 depends = b6232a5 c771d60 verify = host-lifecycle obligations 2>&1 | grep -q -- --prove

[upgrade "a22704e"] title = Self-referential software is excluded from the hygiene lane, not bypassed action = Bump your pinned host-lifecycle to v0.15.1 (it fixes the first-upgrade breakage: adopt now registers the host-template submodule, the missing-template error remediates, and the upgrade skill matches the applied-set model). If your software detects the patterns the hygiene lane flags (a linter, parser, validator, grammar tool), it embeds them in test fixtures, docs and sometimes source, the legitimate self-reference the git hook would block. Exclude that corpus through .host-lintignore (host-lint v0.4.1+ honors it in the per-file hook scan, not only the --all walk), validated file-by-file so no real tell hides among the examples; keep ordinary source scanned (reword an example comment rather than mute the file). Never --no-verify past the gate to land a fixture; that silently defeats the lane. Finally, keep the template's own tools/host-lifecycle pin at or above the maximum requires in this ledger, so the pinned tool can actually run the ledger it ships. requires = host-lifecycle v0.15.1 independent = true verify = grep -rqs "Self-referential software is excluded" host-template/CLAUDE.md

[upgrade "897ce0d"] title = Sound discharge by re-derivation (call/0018) + the LEXICON provenance allowlist action = Bump your pinned host-lifecycle to v0.18.1, host-lint to v0.6.0, and (if you run a deeper rung) host-prove to v0.2.0. A rung obligation is now discharged by RE-DERIVATION, not name-presence: host-lifecycle obligations <spec> --rederive <dir> re-runs each kani:/apalache:/tlaps: rung through host-prove in its recorded pinned toolchain and requires a PASS at the declared bound (AVAILABLE != DISCHARGED). The offline --prove check stays, honestly a name-presence lint. Add the cheap offline staleness signal by declaring inputs=<files> on a rung and recording <manifest>.digests with --rederive --record-digests; a later offline run reports STALE if the proven inputs drift without a fresh re-derivation. Enforcement is project-pluggable (a required check, any CI, a pre-push hook, or the operator's verify phase): no keys, no CI lock-in; it generalizes the reproducible-build re-derivation so that proofs are re-derived the way artifacts are. For the hygiene lane, declare legitimate tell-shaped tokens in a LEXICON (each the full contextual phrase, masked; a tracker reference carries its URL); host-lifecycle adopt seeds a comment-only scaffold. Because a sound escape now exists, the identifier/reference tier may escalate warn to flag via a committed host-lint: strict directive, and host-lint: jira-key PROJ opts a project key into citation-gating. Curate with host-lint lexicon add (it refuses a master key, a laundered tell, or an un-cited reference); host-lint lexicon --check-urls re-derives URL liveness in a network lane. requires = host-lifecycle v0.18.1 depends = 4a98d92 verify = grep -rqs "discharged by re-derivation" host-template/CLAUDE.md

[upgrade "da000aa"] title = Box irreducible literal citations in a host-lint:ignore fence action = Bump your pinned host-lint to v0.7.0. The hygiene lane gains a per-block escape for a document that must reproduce a tell verbatim: a fenced code block tagged host-lint:ignore (markdown only). Its lines are skipped by the naming scan while the rest of the file stays linted, and a regular code block or inline backticks stay scanned so a tell cannot be laundered by quoting it. Reserve it for an irreducible literal citation (an old-name remap table, a frozen dated review citing another document's numbered steps); reword a pedagogical example or a document's own ordinal label into content, and path-exclude (.host-lintignore) only the immutable record (the append-only memory log, dated review artifacts) and the self-referential corpus. This loosens, never widens, the gate: it replaces blanket path-exclusion of editable docs with in-file, per-block, still-audited boxing. requires = host-lifecycle v0.18.1 independent = true verify = grep -rqs "boxed in the file, not path-excluded" host-template/CLAUDE.md

[upgrade "617e420"] title = Lifecycle manifest, and every phase emits a receipt action = Bump your pinned host-lifecycle to the v0.18.1 build that ships manifest/receipt/release. The lifecycle phases (their order, modality, command and the evidence each carries) now live once in a tool-readable lifecycle.manifest at the template root (the CLAUDE.md/STRUCTURE.md prose point at it instead of re-typing the order), and a release phase is added (the strict, tool-carried release: verify, build in the recorded toolchain, re-derive the artifact hash, re-pin, tag, receipt). The rule changes from "every phase runs, no opt-out" to every phase emits a receipt: host-lifecycle receipt --record <phase> [--component <c>] --disposition done|skip writes an append-only .host-receipts, and host-lifecycle software --check now HAZARDs any manifest phase with no receipt, re-verifying each done by the manifest's closed recheck = (never the receipt's own say-so). Modality is first-class: a conditional-on-Where phase is tool-computed n-a where the project has no Where room, a recurring-per-component phase (embed, release) is receipted once per component, and a protected core (verify, skippable = false) refuses a skip. Back-fill a receipt for each phase your project has already done; host-lifecycle manifest <path> prints the whole lifecycle at a glance. requires = host-lifecycle v0.18.1 independent = true verify = grep -rqs "every phase emits a receipt" host-template/CLAUDE.md

[upgrade "0cd6b0a"] title = The Where room uses the nested software// layout action = Bump your pinned host-lifecycle to v0.19.0. The Where room now materializes under software/<name>/: the bare store at software/<name>/.git and each worktree keyed by branch at software/<name>/<branch>/ (the canonical worktree is the recorded branch, default main, checked out at the pin), replacing the old root-scattered <name>/, <name>.git/, <name>.<line>/. Migrate, in order: (1) replace the per-component .gitignore triplets with a single /software/ entry; (2) remove the old root-scattered worktrees and bare stores (commit or stash any uncommitted parallel-worktree work first, since re-materialize is destructive); (3) run host-lifecycle software --materialize . to realise the new tree; (4) re-run link-skills.sh and re-point any software skill link at software/<name>/<branch>/; then host-lifecycle software --check . must be clean. The worktree = recipe line drops its leading <dir> token (the path now derives from the branch: worktree = <branch> <pin> [store=<path>] [host=<os>]), worktrees = is a branch list, and an optional branch = sets a non-main canonical branch. Operate on one component (or one branch worktree) with software --item <name>[@<branch>]. software --check also HAZARDs a dangling generated skill link now. A development host that authors a host-* tool embeds it as its own Where component rather than referencing its own source as a submodule (the reference-don't-vendor carve-out). requires = host-lifecycle v0.19.0 independent = true verify = grep -rqs "the producer of a tool embeds it" host-template/CLAUDE.md

[upgrade "950fbd6"] title = Prose hygiene is an ongoing hygiene-lane rule with a receipt action = Treat the prose audit as a STANDING part of the hygiene lane, not a one-time pass. host-lint --prose (host-lint v0.7.0+) flags the LLM-slop prose tropes that tropes.fyi names (decoration dashes and arrows, tricolons, hypophora, and the rest) in authored docs. Clean every authored doc to ZERO prose tropes (reword to plain prose, the same bar as naming tells), then wire it into the receipts mechanism: the verify phase applies host-lint --prose and generates a receipt, and host-lifecycle software --check re-verifies that receipt by re-running --prose (add it to the verify phase recheck = in lifecycle.manifest), re-opening any regressed doc as a HAZARD. MEMORY.md is the agent's append-only working memory and is excepted from both the naming and prose audits via .host-lintignore, never rewritten. requires = host-lifecycle v0.19.0 independent = true verify = grep -rqs "Prose hygiene is the same lane" host-template/CLAUDE.md

[upgrade "ecce498"] title = Hermetic builds via a pinned dependency bundle action = Bump your pinned host-lifecycle to the v0.20.0 build that ships the deps-bundle recipe field and the offline --network none build. A component that ships static or self-contained release binaries MUST be able to reproduce them offline from pinned inputs, never from a network fetch at build time. Vendor the dependency layer once and publish it as a reusable, versioned, hash-pinned downloadable release bundle (the pattern pgs-release uses for its prebuilt sysroot), then record it per component as deps-bundle = <url> <sha256> in .host-software. host-lifecycle software --verify-build and release perform the one controlled, pinned download, verify the sha (provenance), stage the vendored sources, and build under --network none (egress). The producer commits a deps-bundle.lock as the single source of truth, and software --check HAZARDs a deps-bundle pin that has drifted from it. A component that genuinely cannot vendor offline (a network-fetching build.rs, a non-Rust toolchain) may carry hermetic-exempt = call/NNNN citing a software-scoped case decision, the same escape shape as repro-exempt; the exemption is never available where offline vendoring is feasible. requires = host-lifecycle v0.20.0 independent = true verify = grep -rqs "reproduce them offline from pinned inputs" host-template/CLAUDE.md

[upgrade "27d815b"] title = The hygiene grammar grows by reflective practice action = Bump your pinned host-lint to v0.9.0, which flags a positional reference to a milestone checklist item (the "box N" / "boxes N-M" / "steps N-M" shape), the same ordinal-by-position tell as a numbered name. Adopt the living-grammar doctrine: the shared tell corpus is incomplete and grows by reflective practice. Discovery is mechanical-first, a sweep of history and recent work for a recurring shape the lane misses, prompted at the verify gate before a milestone closes and at adoption, with the agent assisting rather than leading. The operator validates whether a surfaced shape is a genuine tell or legitimate domain vocabulary, by one test: is the shape a property of how models segment work, or of this project's own domain? A confirmed tell is proposed upstream to the shared grammar the lane consumes (a naming tell to host-lint, a prose trope to host-grammar), gained on the next bump; a legitimate tell-shaped token stays in the per-project LEXICON; the project that finds a tell does not edit the shared source itself. Reword by default; box or declare an irreducible citation; declare the numeral-free contextual prefix for a genuine quantity; reserve a graduation for the residue. A later grammar bump that flags a live doc is reworded, and a frozen record is boxed. requires = host-lifecycle v0.20.0 independent = true verify = grep -rqs "grows by reflective practice" host-template/CLAUDE.md

[upgrade "e280a8d"] title = The verify recheck re-runs the prose audit, so the gate enforces prose hygiene action = Bump your pinned host-lifecycle to v0.22.0, which adds host-lifecycle prose <dir>, host-lint's --docs prose audit run in-process so the gate needs no host-lint on PATH (a development host whose host-lint is embedded Where software still runs it). The verify phase recheck = in lifecycle.manifest now chains host-lifecycle prose . after validate, so software --check re-verifies the verify receipt by re-running the prose audit and re-opens any doc that regressed to slop as a HAZARD. This enforces the standing prose-hygiene rule the spine already states; before it the recheck ran only validate, so a prose regression slipped past the gate. requires = host-lifecycle v0.22.0 independent = true verify = grep -rqs "host-lifecycle prose" host-template/lifecycle.manifest

[upgrade "ac32d1c"] title = Re-home the receipts family, tool-driven action = Bump your pinned host-lifecycle to v0.23.0 and run host-lifecycle migrate-receipts . (one command, never hand-edited). It moves the applied-set out of .host into .host-receipts and splits the operational receipts (classify, embed, remap, verify, publish, release) out to .host-lifecycle-receipts; adopt and upgrade stay in .host-receipts. The binary auto-migrates on read, so the gate stays green across the boundary and an un-migrated project is still read correctly. A receipt is methodology-version (.host-receipts) only for adopt and upgrade; every other phase host-lifecycle runs is operational (.host-lifecycle-receipts). The old-layout reader is permanent backward compatibility, not a one-time shim. requires = host-lifecycle v0.23.0 independent = true verify = test -f .host-lifecycle-receipts

[upgrade "d5a0034"] title = Reflective practice grows in two arms: gather forward, reconcile backward action = Bump your pinned host-lifecycle to v0.24.2, which adds host-lifecycle reconcile <dir> and wires the reconcile step into the upgrade/adopt/verify skills. Adopt the widened doctrine: an agent perceives neither the register it emits nor the restatements its own change stales, so reflective practice runs in two arms under one principle. The first arm, gather (unchanged), is forward: emergent tells in the corpus, a confirmed tell graduates upstream, cadence-driven. The second arm, reconcile (new), is backward: the project's own restatements of methodology (its room map, tool family, verification model, recorded Where layout), a confirmed drift fixed locally and never propagated. Prefer pointing at the spine over restating it; a restatement that must stay carries an inline <!-- host-reconcile: KIND --> annotation declaring an assertion host-lifecycle reconcile checks against a source of truth (the host-* family list and the verification-ladder drivers the lifecycle manifest records, the fixed software/ and plan/ layout), with kinds family, verification, where-root, spec-path. The trigger is conditional and host-aware: a restates = field on an UPGRADING entry marks a drift-capable move so the upgrade skill re-reads the named kinds, adoption runs the full reconcile once, and for a development host that authors its own spine changes the verify gate is the binding trigger (the verify recheck now chains host-lifecycle reconcile .). Disposition is three-way (reword a live restatement, box a frozen citation, forward-correct an immutable record). A sibling check: host-lifecycle validate now HAZARDs an accepted call/ decision whose Scope: names host-template, since its rule is spine-resident and belongs superseded there. requires = host-lifecycle v0.24.2 independent = true verify = grep -rqs "Two arms run under this one principle" host-template/CLAUDE.md

[upgrade "7be692f"] title = Reconcile evolves to concept-as-URI; the lifecycle manifest is phases-only action = Bump your pinned host-lifecycle to v0.25.0 and move to this host-template revision together (a partial bump leaves the old manifest stanzas under the hardened manifest --check). The lifecycle manifest is now phases only; a project's own facts live in its .host-software. Define each methodology concept once at a {#id} anchor on a heading (for example ## Components {#components}) in a doc of your choice, and point at it elsewhere with [text](FILE#id); reconcile runs link-integrity, declared-anchor, and coverage over your docs. The concepts: components (your host-* tools, the [software] members of .host-software minus a single-file front door marked front-door = true if you have one), verifiers (your verification drivers, named on a drivers = ... line under a [verification] stanza you add to .host-software, for example a [verification] stanza whose line is drivers = host-lint allium specula host-prove), software-root (where your software lives, software/), and spec-home (where your specs live, with the software). Each home that lists a set must name the whole set; coverage flags an omission. Convert each surviving inline <!-- host-reconcile: KIND --> annotation by replacing its enumeration and comment with a single such link. You may migrate gradually: the inline annotations keep working, warned, until a later revision retires them. See the reconcile section in CLAUDE.md for the full rules. requires = host-lifecycle v0.25.0 independent = true verify = grep -rqs "rejects a project-fact stanza" host-template/CLAUDE.md

[upgrade "2229dbb"] title = In-plan tasks are a receipted dependency graph action = Bump your pinned host-lifecycle to v0.26.0 and move to this host-template revision together. Each entry under a milestone's ## Build sequence becomes a task: an anchored ### heading ending in {#anchor}, keyed plan/NNNN#anchor. A task declares its prerequisites with - depends: #anchor (or a cross-milestone plan/NNNN#anchor; omit the line for the previous task, the linear default), its check with - verify: <command> (mechanical) or - verify: attested <call/NNNN | operator>, and the files a mechanical verify covers with - inputs: <paths>. Record a receipt for each task with host-lifecycle tasks --record <plan/NNNN#anchor> --disposition done|skip (the tool reads the task's own verify/inputs, so you never re-type them); software --check then HAZARDs a task with no receipt, a done whose inputs drifted or whose citation does not resolve, a skip without a resolvable call/NNNN, and an orphan receipt whose task was renamed or removed. Declare only what must finish first; the tool derives what runs in parallel, and a coordinator parallelizes only resource-isolated workers. Migrate gradually: a plan with no anchored ### task is untouched. See the task-graph section in CLAUDE.md. requires = host-lifecycle v0.26.0 independent = true verify = grep -rqs "in-plan tasks are receipted nodes" host-template/CLAUDE.md

[upgrade "ba86125"] title = The entrance check holds a self-contained document to the spine action = Bump your pinned host-lifecycle to v0.29.0 and move to this host-template revision together. A document read out of context cannot point at a definition, so the entrance check holds it by coverage and generation, reconcile's standalone sibling. Declare one entrance, a global singleton, in an [entrance] stanza in .host-software naming the member it belongs to (set apart from components), the document within that member (default README.md), and the concepts it restates (true for every concept, or a named subset of phases, tools, stamp). host-lifecycle entrance --check then holds that document complete against the declared concepts. If you marked a single-file front door with the legacy front-door = true or entrance = true on its [software] member, replace it with an [entrance] stanza naming that member; the legacy marker is still accepted, warned, until a later revision retires it. requires = host-lifecycle v0.29.0

verify   = grep -rqs "entrance check is reconcile's standalone sibling" host-template/CLAUDE.md

[upgrade "de8a517"] title = The legacy per-member entrance marker is retired action = Bump your pinned host-lifecycle to v0.30.0 and move to this host-template revision together. The legacy per-member entrance marker is retired: a front-door = true or entrance = true on a [software] member is now a loud error, not silently accepted. Replace any such marker with an [entrance] stanza naming that member (the member, an optional document with default README.md, and restates); see the entrance section in CLAUDE.md. A project with no single-file entrance carries no such marker and needs no change. requires = host-lifecycle v0.30.0

verify   = grep -rqs "The legacy per-member marker is retired" host-template/CLAUDE.md

[upgrade "e068828"] title = The generated book moves under mdBook/, freeing docs/ action = Bump your pinned host-lifecycle to v0.30.2 and move to this host-template revision together. host-lifecycle book now writes its generated mdBook source to mdBook/src/ and the built HTML to mdBook/out/, with book.toml kept at the repo root, so mdbook build still runs from the root. This frees docs/ for your own authored documentation. Update your .gitignore: replace the /docs/ and /book/ lines with a single /mdBook/ line, keeping /book.toml. Regenerate the site with host-lifecycle book ., then remove the stale generated docs/ and book/ trees, which held only generated output under the old layout (git clean -fd docs book reaches them once they are no longer ignored). If your .github/workflows/ publishes the site, point the publish step at mdBook/out in place of book. A project that has not yet published a site carries no generated docs/, so it needs only the gitignore and workflow change before it first runs book. requires = host-lifecycle v0.30.2 independent = true verify = grep -rqs "mdBook/src" host-template/STRUCTURE.md

[upgrade "6174996"] title = A declared rung obliges a runnable re-deriver, not just a present CI lane action = Bump your pinned host-lifecycle to v0.31.0 and move to this host-template revision together. software --check now HAZARDs a declared deeper rung (kani:/apalache:/tlaps:) whose shared re-deriver, host-prove, does not run, beside the existing no-CI-lane HAZARD, because a re-derivation that cannot run leaves the rung undischarged however complete the CI config reads. So install host-prove on PATH wherever the gate runs: your local setup and the CI that runs software --check (cargo install --git https://github.com/connollydavid/host-prove --rev <its pin>). A project that declares no deeper rung carries no host-prove requirement and needs no change. And earn the digest a rung records through host-lifecycle obligations <spec> --rederive <crate-root> --record-digests, which re-runs the proof and records only on a pass, in place of a hand edit, so a fresh digest stands for a passing re-derivation on the current inputs. requires = host-lifecycle v0.31.0 independent = true verify = grep -rqs "a re-deriver that runs" host-template/CLAUDE.md

[upgrade "46a1fd2"] title = A verification lane reports clean only after it performs its check action = Bump your pinned host-lifecycle to v0.32.0 and move to this host-template revision together. The obligation-discharge check now verifies an exercises=<symbol> link rather than only that a test: name resolves. Add to each behavioural obligation a link naming a function the discharging test drives, and run host-lifecycle obligations <spec> --tests <dir> --strict-discharge where the gate runs. The strict mode is opt-in, so a tool bump leaves your ladder green; the reject names the test and the symbol it is missing. Do not relabel a behavioural obligation structural to dodge it, and give every waived: a real reason. Separately, software --verify-build is now three-state: it prints its clean line only when a build was verified and none was UNVERIFIABLE, and it exits non-zero on an in-scope build it could not run (no container runtime, or no toolchain pin), so a runtime-less CI that relied on its exit 0 must gain a runtime or scope that build out. A project whose obligations are all structural carries no exercises= link and needs only the verify-build change. requires = host-lifecycle v0.32.0 independent = true verify = grep -rqs "reports clean only after it performs its check" host-template/CLAUDE.md

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 @ 565410a for UDPspeeder-simd (case a, Shallow). Stamp written by host-lifecycle adopt; rooms cast/ plan/ call/ scaffolded.
  • Push auth: the stored git credential is the slartibardfast token, which had no push rights to connollydavid/agentic-UDPspeeder-simd (HTTP 403 on first dry-run push). The operator authorized the repo, after which git push --dry-run returned "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 is branch_libev, not main). Recorded in .host-software with no deploy/artifact, so it is a source pin with no reproducible-build claim yet (migrated software).
  • host-lint is also a .host-software component (the gating tool): --install-hooks reads a component's hooks script and built artifact from 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 1 is 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 needs host-prove on PATH because host-lint's own kani: obligations surface once it is materialized as a component; the cheap gate probes host-prove --help, it does not run Kani.
  • Two clearance details: the toolchain HAZARD (artifact with no toolchain) is waived by repro-exempt = call/0002 on host-lint (consumed tool, not reproduced here); and remap is a skip receipt, not done, because remap --check errors on an empty or absent .host-remap, so a no-rename case-(a) adoption cannot pass a done recheck.
  • Push auth to connollydavid/agentic-UDPspeeder-simd is intermittent: the stored credential is the slartibardfast token, which 403s after the first few pushes landed. If git push 403s, surface it to the operator rather than retrying or swapping credentials. As of this entry, f8d36ed and 5ec59af are local only.

2026-07-03 — followed the host procedure: conformant, no upgrade available

  • Ran the case-(c) upgrade/verify cycle from the host procedure (github.com/connollydavid/host). host-template upstream is still at 565410a, the exact revision the .host stamp adopted: a git fetch in the submodule found no commits after it, and 565410a is an ancestor of origin/main. host-lifecycle upgrade . reports up to date (baseline 46a1fd2, 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/ and validate call/ both ok; host-lint --all and --log clean. Independently confirmed: udpspeeder-simd worktree is at pin 3374e3b on branch_libev with a clean tree, host-lint at 78804cd, and the commit-msg hook still rejects an ordinal tell (a phase 1 message exits 1, a clean message exits 0).
  • udpspeeder-simd still carries no .allium/.tla spec, 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 --prose on the authored docs prints advisory note: lines but exits 0. Those notes are below the enforced bar; the in-process prose audit inside software --check is the gate that binds and it reports no flagging or warning tropes. Most notes sit in verbatim copied-in content (the UPGRADING.md ledger, 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/main and 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 of openwrt/packages, GPL-2.0, an OpenWrt feed of Makefiles/shell/C) as a Where-room component in .host-software: source pin 1d40ad9 on canonical branch master, no build/artifact (migrated build-recipe feed, not a built artifact). Materialized to software/packages/master/; software --check is green.
  • Mechanics for a new component: host-lifecycle has no software --add, so a new lane is a hand-edited .host-software stanza plus software --materialize --item <name> . (the --item scope avoids re-touching existing worktrees). The embed and release phase receipts are written with host-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: Claude trailer, so the feed stays upstream-clean for openwrt/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-simd shows the slartibardfast account has push: false (pull only); the owner account connollydavid (also logged in to gh) has push. And git's credential helper here is store (~/.git-credentials), not gh, so gh auth switch -u <user> does not change the token git push sends. 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 push works with no helper change. To make gh auth switch actually route pushes, run gh auth setup-git so 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 main is 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, then git -c credential.helper= -c credential.helper='!gh auth git-credential' push origin main. The empty credential.helper= resets the inherited store helper (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); main is in sync with origin.
  • Going forward: repeat that one-shot override, or run gh auth setup-git once (with connollydavid active) and clear the stale store token so a plain git push works. 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 with src = mdBook/src and build-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 is test -f .github/workflows/mdbook.yml, so a done publish receipt would HAZARD until the file is named mdbook.yml. It also published ./book, while v0.35.1 builds to mdBook/out. Renamed to mdbook.yml, set publish_dir: ./mdBook/out, and corrected the stale src=docs comment. 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 the gh-pages branch via peaceiris/actions-gh-pages. GitHub Pages was disabled (the /pages API returned 404), so enabled it from gh-pages root with gh 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 --check is 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 world was 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 embedded openwrt component stays but is not needed for this path and adds ~2.5 min to software --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/compile produced 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.
  • 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-simd of 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_latency non-local plus a duplicated sock_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 to grep -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 on arm_cortex-a15_neon-vfpv4 and arm_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.S marked the stack non-exec with .section .note.GNU-stack,"",@progbits. On 32-bit ARM @ starts a line comment, so gas ate @progbits and 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 @progbits and %progbits), and % is the required form where @ is a comment. Fork commit c2b3759 on branch_libev.
  • Propagation: re-pinned .host-software udpspeeder-simd → c2b3759; bumped the PR's net/udpspeeder-simd/Makefile PKG_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 prints got <real-hash>. skip does 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: adds procd_append_param command --timeout "${timeout}" after --mtu in files/udpspeeder-init. Fixes: .../issues/18955 auto-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 sync barrier 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 --timeout line into the single udpspeeder-simd: add package commit (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 -p appends function context to hunk headers (@@ -51,7 +51,7 @@ cygwin:git_version). git format-patch output 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 up patches/ + 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-pushed 7a76af810...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: cross was 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: -O2 moved BEFORE ${CXXFLAGS} (so a caller's -Os wins instead of being overridden), -lrt dropped, ${LDLIBS} added. Our gitversion ?= 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.
  • -lrt is a no-op on OpenWrt, PROVEN not assumed: musl's librt.a in the toolchain is an 8-byte empty archive (clock_gettime is in libc), and the packaged binary built with LDLIBS="-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. Left DEPENDS:=+librt alone 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 crc32h for Stephan Brumme's crc32_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, an is_vaild->is_valid rename, 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), so 20230206.0~<sha> would sort BELOW the released 20230206.0 and break the upgrade path. Used 20260731~b6a1b594 (commit date as base) so it sorts above.
  • Fork: added the same cross_cxx target (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-receipts records the same evidence for plan/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: true have 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-script receipt 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)), giving 2026.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 version 20230206.0 was ten characters exactly, so the check passed by luck for years. Any longer version needs test-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.sh installs 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 iseleq inside musl libc, which holds 63 isel instructions while our binaries hold none. -cpu e500v2_v10 runs 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_gettimeofday decoding 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_ssse3 and 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 from gf_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 under qemu-x86_64 -cpu qemu64|Nehalem|Haswell to 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 -1 picked 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 qemu64 GRANTS 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-64 with sse3, ssse3, sse4, cx16 and popcnt all DISABLED (gcc -Q --help=target proves it, and CONFIG_TARGET_OPTIMIZATION is only -Os -pipe, so nothing raises it).
  • XGETBV IS 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 behind bt $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. udpspeeder holds ZERO; udpspeeder-simd confines all of them to addmul1_{ssse3,avx2,avx512}, xor_tile_{avx2,avx512} and crc32c_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_G1 and Opteron_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. Now pentium3,+sse2, which is exactly a P4's ISA.
    • pentium2 HAS CMOV, which a Geode GX/LX and a Pentium MMX do not. Now pentium,+mmx.
  • qemu-user genuinely gates these, proven by one-instruction controls rather than assumed: pentium2 runs cmov and faults on pshufb; pentium2,-cmov and pentium,+mmx fault on cmov; n270 runs BOTH cmov and pshufb; pentium3,+sse2 runs 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 because target("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_4kec and riscv64_riscv64 DO 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 a make test-cross build. 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.S plus -Wa,-mspe rather than intrinsics. Diffing the predefines of -mcpu=8548 against -mcpu=464fp yields only soft-float proxies (__NO_FPRS__, _SOFT_FLOAT, _SOFT_DOUBLE, __NO_LWSYNC__). Keying on those would ship evxor to 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_VARS exports CXXFLAGS="$(TARGET_CXXFLAGS) ...", TARGET_CXXFLAGS = TARGET_CFLAGS, and that carries CPU_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 inside CC, not CXXFLAGS, so the filter searches CC 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) not findstring (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.tsv now asserts SPE opcodes for powerpc_8548, but the sweep builds from the package's pinned PKG_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_CRC32 needs a named core. Measured across the OpenWrt aarch64 toolchains: -mcpu=cortex-a53, -a72 and -a76 define it, -mcpu=generic (armsr/armv8, CONFIG_CPU_TYPE generic) does not. So three of the four published aarch64 targets ship the hardware checksum. Debian's aarch64-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-a53 and 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 faulting addmul1_neon and the NEON xor_tile and 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 with bench_*_auto(). This was already wrong on x86 and would have silently removed NEON from the aarch64 round-trips.
  • qemu-aarch64 -cpu cortex-a53 does 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/Makefile bakes SOURCE_DATE_EPOCH into the SDK's include/version.mk and include/toplevel.mk exports 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:20 on 25.12 and main, Jul 24 2026 07:21:50 on 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 commit and 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 to PROGRAM_VERSION, so touching the banner means a new tag, which means both udpspeeder-simd-snapshot Makefiles take a new PKG_VERSION and a new PKG_MIRROR_HASH (the tarball name follows the version) on all three feed branches. The stock package's line is upstream's own, reached only through 010-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-main a22b5740, feed-25.12 ebc5ab6c, feed-24.10 e646bb33, the fork 0ba7c9f5. The .host-software comment 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_jlink and group_internal as meson choices, which reads like the tidy way to express an OpenWrt config menu. It is not. In meson.build the selection test is groups.contains(true) or 'all' in programmer or 'auto' in programmer and default, and and binds tighter than or, so group membership alone selects a programmer and its own default: false is never consulted. Passing group_pci therefore builds atahpt ("not yet working"), atapromise and nicnatsemi ("not complete nor tested"), all three of which upstream's Makefile sets to no. 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_mem covers x86, mips, ppc, arm, aarch64, sparc, arc and e2k but not riscv64 or loongarch64, both of which OpenWrt publishes, and cpus_port_io is x86 only. The recipe gates on $(ARCH) accordingly. Checked against include/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, not pciutils. utils/pciutils builds both; the flashrom package depends on pciutils, which drags in the lspci binary and the pciids database for a package that only links the library. Also, flashprog defaults use_internal_dmi to true, so unlike flashrom it needs no dmidecode on x86.
  • libjaylink 0.3.1 in the feed is enough. Every one of the 30 functions and 9 constants jlink_spi.c uses 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 udev uaccess tag, 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_spi is unreachable and was left out. Its group, group_gpiod, is read in meson.build but is absent from the programmer option's choices, so it cannot be selected by name. It would also drag in libgpiod, which carries DEPENDS:=@GPIO_SUPPORT and would make the whole package unbuildable on targets without GPIO support.
  • The license is GPL-2.0-only, not the -or-later the flashrom package claims. 85 files carry "either version 2 or (at your option) any later version"; 46 carry version 2 with no later clause, including spi.c, spi25.c, layout.c, linux_spi.c, linux_mtd.c and dummyflasher.c. Upstream's own meson.build declares GPL-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 libc left; and an install into a stock rootfs on each line running a dummy write, verify, read, cmp and erase. The assertions were proved against negative controls first: claiming aarch64 for an x86 build reports rayer_spi, nic3com, nicrealtek and satamv as 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 sha256sums and downloading the tarball, so the fetch now retries three times; a snapshot directory moves under you. And the branch-tracking openwrt/rootfs:x86-64-openwrt-24.10 image lists a kmods feed whose kernel hash has moved, so opkg update returns non-zero on one of six feeds. Rather than silence it, the job now reads the package's own Depends: 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 +x followed by git add left the CI scripts at mode 100644, and all 123 jobs died at their first step with exit 126. git update-index --chmod=+x is the fix, and the general rule is that a mode change made under /mnt/c has 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 four default y bools 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. flashprog is the default variant carrying everything the architecture supports; flashprog-spi carries only the programmers that need no library. Both own /usr/bin/flashprog, so they conflict: flashprog declares CONFLICTS, flashprog-spi declares PROVIDES:=flashprog, the shape net/chrony and libs/libwebsockets use. Renaming the binary per variant, which is what utils/flashrom does, 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_jlink and, on x86, group_internal, naming only dummy, linux_spi and linux_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 drops rayer_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 hard error(). 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_usb gained ch347_spi and dirtyjtag_spi across releases. A version bump can therefore add a programmer whose library the recipe never declared, and soft selection reads staging_dir, so it activates only when something else happened to stage that library. ft4222_spi already shows the shape: it is in group_usb and group_ftdi but needs libusb, so selecting group_ftdi alone links a library the package would not have declared. Declaring all four libraries unconditionally is what makes groups safe here.
  • flashprog needs libpci, never pciids. It calls only config-space, enumeration and filter functions, never pci_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 on libpci rather than pciutils therefore avoids pciids at 1,651,592 bytes installed, plus libkmod and the lspci binary.

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 PROVIDES makes the name substitutable, which is not what it looks like. The operator ran apk add flashprog-snapshot on a real router and apk installed flashprog-spi-snapshot instead. 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-priority cannot 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 CONFLICTS never reaches apk. package-pack.mk:417 writes it only into the opkg control file, and OpenWrt passes twelve --info keys to apk 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-spi installs the same ELF as /usr/bin/flashprog-spi. The two packages now share no path, are co-installable, and carry no PROVIDES and no CONFLICTS at all. This is what utils/flashrom does 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 carrying PROVIDES has to be tested by name from a repository index, or it is not tested at all.

Three smaller findings from settling the recipe.

  • group_pci adds no programmer; it makes libpci required. Upstream has libpci = 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/Makefile sets ARCH:=x86_64 without setting CONFIG_TARGET_x86, so the two predicates diverge. Gating the programmer groups on one and DEPENDS on the other selected group_pci where libpci was never declared, and uml failed to configure. Both gates now read CONFIG_TARGET_x86.
  • Off x86, internal degrades to linux_mtd without touching /dev/mem. internal_init() calls try_mtd() before processor_flash_enable() and before the #if defined(__i386__) || defined(__x86_64__) physmap block, so on a non-x86 build with LINUX_MTD_AS_INTERNAL the 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_libev is 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 onto ef92d69. New tip e33aa71. The feed files (.github/scripts/*, package-feed.yml) are gone from the branch.
  • feed-snapshot is the new feed branch: ef92d69 + the 29 feed-machinery commits only (no banner, no DNS). Tip 7acdf4e. 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=@, then zstd -T0 --ultra -20) ships through the feed once a dispatch runs against the rewritten branches.

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 dnsmasq uses. In that shape the narrow variants provide the canonical name, which package-pack.mk emits as a versioned provide. That reintroduces the substitution the entry above records, in its quietest form: with a narrow variant already installed, apk add flashprog is 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 while flashprog stays the full package's own real name, never a virtual any variant can satisfy. apk add flashprog installs the full package or fails.
  • Measured on the router's exact resolver. apk 3.0.5 inside an openwrt/rootfs:x86-64-25.12.4 container. Clean, apk add flashprog selects the full package. With flashprog-spi installed it errors on the flashprog-bin conflict 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 mkpkg with a dummy payload and only the metadata under test, indexed by apk 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: DEPENDS is the source of truth, apk query surfaces it, and a duplicated list is a second place to edit. All four Requires ... sentences were dropped, flashprog-spi's Requires 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.