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
| Room | Holds |
|---|---|
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.
- Brainstorm organisation goals. Record what the organisation wants the project to achieve; these reflect its intention.
- 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.
- 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).
- 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.
- Brainstorm persona scenarios. Describe interactions, not tasks: how a persona uses the system to reach a goal. No implementation detail in a scenario.
- 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.
- Create stories. From the persona goals and scenarios, write the user stories.
- Prioritise stories, using the primary persona as the guide.
- 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:ysetting) 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.10through the current development branch), and bump the Makefile version and source hash when a new upstream release lands. - Offer fixes back to
openwrt/packageswith 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 toopenwrt/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
-kkey 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
packagesandudpspeeder-simdcomponents - 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_MAINTAINERis a real name and a real, public email (David Connolly david@connol.ly). A GitHub noreply address is rejected.PKG_LICENSEis the SPDX identifierMIT, withPKG_LICENSE_FILESnaming the license file (LICENSE.md) in the fork.PKG_RELEASEstarts 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 aSigned-off-bythat matches the author (a real name and a public email), with no automated co-author trailer (the packages rule incall/0003). The sign-off is the DCO, which is separate from the co-author trailer. - A
test.shbeside 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 packageudpspeeder-simdwith distinct paths (/usr/bin/udpspeeder-simd,/etc/config/udpspeeder-simd,/etc/init.d/udpspeeder-simd), no file clash withudpspeeder, noCONFLICTS. - Linking (
call/0004): dynamic, withDEPENDSon the shared libraries the binary needs, like the existingudpspeeder. 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-softwarepin 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'scc_crossand 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 worldproved excessive for this). The embeddedopenwrtcomponent stays but is not required for this path. Local validation covers x86_64; multi-architecture, includingath79/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 forx86/64andath79/generic. - Release users (
#backports): OpenWrt policy blocks a new package on release branches, so there is no backport.docs/udpspeeder-simd-on-openwrt-releases.mdshows 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
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:
78804cd9deb5daa279705c6f5cd9be12ccfd6765 - worktrees: none (single canonical line)
udpspeeder-simd
- url: https://github.com/connollydavid/UDPspeeder-simd.git
- pin:
e96efe4ebb3d4905bde48f375292dbde6924b49b - worktrees: none (single canonical line)
packages
- url: https://github.com/connollydavid/packages.git
- pin:
1d40ad929ad45cfbdf547996d0ba2379d9765172 - worktrees: udpspeeder-update-20260731 @ 51a2696b59b4, feed-25.12 @ 04d81471cf89, feed-24.10 @ 9dcdbfbc944e, feed-main @ 59b44da043fc
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-softwareand pinned to itsbranch_libevhead. The host never adopts a software repository in place. - The methodology rules live in the spine (
CLAUDE.mdplusSTRUCTURE.md), inherited copy-at-version from the template; they are not re-litigated here. A later change to them arrives byhost-lifecycle upgradeagainst the template'sUPGRADING.mdledger.
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-lintat78804cd(v0.12.1)tools/host-lifecycleat486add7(v0.35.1)tools/alliumat493a2de(v3.6.0)tools/speculaatfa12367
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 --checkpasses without this host claiming a reproducible build it does not perform. - Neutral:
host-lifecycle software --verify-buildreports 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/packagesneeds 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
udpspeederpackage, holds the flash footprint down, and reviews cleanly. - Good: both packages install together, so a user can compare them.
- Neutral: the exact
DEPENDSset 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,artifactanddeploynow, and accept that--verify-buildfails until the build is made deterministic. - Record a
repro-waiverciting 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.lockand 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.
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.
| Question | Room | What goes here |
|---|---|---|
| Who | cast/ | 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 |
| When | plan/ | the milestone index and one folder per milestone |
| Where | <software>/ | the hosted software, a bare store with worktrees; you add it |
| Why | call/ | decisions, in MADR format (see call/0000) |
| How | CLAUDE.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/namedNNNN-slug: a four-digit zero-padded number, a hyphen, then a lowercase hyphenated slug, for example0001-example-milestone. Decisions incall/use the sameNNNN-slugform. - Name a milestone after its content rather than its position.
0003-ci-pipelineis 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-lifecycleallocates 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
.alliumfiles, authored and maintained through the allium skills (elicit/distill/tend/weed/propagate), checked bytools/allium(allium checkvalidates structure;allium analyseadds data-flow, reachability, terminal-state and deadlock analysis;allium planderives the test obligations the suite must discharge). The software's CI runscheck+analyse+planand fails on any error or warning. - Timing and concurrency as
.tlafiles, checked bytools/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:
-
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 taggedhost-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-verifypast 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'sLEXICON): 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 committedstrictdirective), 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-inPROJ-NNNNkey); 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 --proseaudits 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: theverifyphase applies the prose audit and generates a receipt, andsoftware --checkre-verifies that receipt by re-running the prose audit (host-lifecycle prose, host-lint's--docsengine 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 isMEMORY.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 arecomponents,verifiers,software-root, andspec-home. The first two are project-local, read from its.host-software: the[software]members are thecomponents(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 theverifiers. The last two are the fixed layout:software-rootis where the project's software lives (software/) andspec-homeis where its specs live, with the software. The lifecycle manifest is phases only, so no adopter inherits another project's facts;manifest --checkrejects a project-fact stanza.host-lifecycle reconcileruns 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-softwareset, 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 --checkruns 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 (acall/body, aStatus: donedoc,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 validateHAZARDs anacceptedcall/decision whoseScope:nameshost-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 standaloneSKILL.mdloaded 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: thememberit belongs to (set apart fromcomponents), thedocumentwithin that member (defaultREADME.md, so aSKILL.mdor a landing page is reached by path), and the concepts itrestates(truefor every concept, or a named subset).host-lifecycle entrance --checkthen holds the document complete against the declared concepts: it generates the.hoststamp 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 survivingfront-door = trueorentrance = trueon a[software]member is a loud error, not the entrance, so declare the stanza. -
Requirements:
tools/allium(MIT, by JUXT). Does the software meet the behaviour the spec states? Author and maintain.alliumspecs through the allium skills, not by hand:elicit/distillto author,tendto evolve,weedto find spec↔code divergence,propagateto generate the tests. Gate each spec in the software's CI withallium check(structure) +allium analyse(data flow, reachability, terminal states, deadlock) +allium plan(test obligations). -
Timing and concurrency:
tools/specula(Apache-2.0). TLA+ model checking: are the orderings and timings correct? Model-check each.tlawith 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:
- Symbolic / parametric: Apalache (TLA+ to SMT/Z3; the
apalache-symbolicskill). Proves a.tlainvariant across a whole symbolic parameter family at once, where TLC can only enumerate one instance. - Proof / unbounded: TLAPS (
tlapm; thetlaps-proofskill). 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.) - Code-conformance: verify the implementation against the spec, beyond tests
and trace validation. This rung is target-specific: Rust uses Kani (the
kani-conformanceskill), 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/Vecmake 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 andhost-lifecycle validate <dir>to check a folder. It also materialises and audits the Where room:host-lifecycle software --materialize|--check <dir>realises the.host-softwarebare 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 incall/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.mdis 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 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
dependsnames the prerequisites: a local#anchor, or a cross-milestoneplan/NNNN#anchor. A task with nodependstakes 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, andhost-lifecycle tasksderives the ready frontier (the tasks whose prerequisites all carry a done receipt), which a coordinator may run in parallel.verifyis a command the gate re-runs (mechanical), orattested <call/NNNN | operator>(a decision the gate resolves, or an operator confirmation).inputsnames 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-softwarepinand 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-revisionstamp is migrated once to abaseline.upgrade --nextprints 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 adependsis unapplied, runs the entry'sverifypost-condition (or, when it has none, requires an explicit--unverified call/NNNNcitation) 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 --advancelater compacts a contiguous applied run into thebaseline. host-lifecycle software --checkre-checks every recorded claim (averifythat no longer holds, or an applied entry whosedependsis unapplied, is a loudHAZARD).
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.
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-formatis authoritative; match the surrounding file exactly (braces, indentation, naming) rather than restyling. - Canonical branch.
branch_libev, notmain. The audited worktree issoftware/udpspeeder-simd/branch_libev/, pinned in.host-software. - License. The software is MIT (its
LICENSE.mdlives 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
pinwith nodeployorartifact, so no reproducible build is claimed yet. Add a build recipe and arepro-exempt = call/NNNN(or a realartifact) before shipping a binary through the host. - Lanes. No
.alliumor.tlaspec 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 issoftware/packages/master/, pinned in.host-software. - License. The feed is GPL-2.0 (its own
LICENSElives in the worktree). This governance shell is Unlicense. Keep the two separate. - Reproducibility. Migrated, not initiated here: a source
pinonmasterwith nodeployorartifact, 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 foropenwrt/packages. This overrides the default trailer behaviour for the packages component only; host-repo commits keep it. Seecall/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.
| W | room | holds |
|---|---|---|
| Who | cast/ | 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 |
| When | plan/ | the milestone index and folders |
| Where | <software>/ | the hosted software, a bare store with worktrees; you add it |
| Why | call/ | decisions about the software (MADR; see call/0000); methodology lives in the spine, not here |
| How | CLAUDE.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 againsthost-grammar.tools/host-lifecycle(Unlicense): token-free scaffolder/validator; generates names fromhost-grammar, so what it emits is exactly whathost-lintaccepts.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/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@565410afor UDPspeeder-simd (case a, Shallow). Stamp written byhost-lifecycle adopt; roomscast/ plan/ call/scaffolded. - Push auth: the stored git credential is the
slartibardfasttoken, which had no push rights toconnollydavid/agentic-UDPspeeder-simd(HTTP 403 on first dry-run push). The operator authorized the repo, after whichgit push --dry-runreturned "Everything up-to-date". If a later push 403s again, surface it to the operator rather than retrying blindly. - Tooling built from source with the system toolchain (rustc 1.96.1, Arch Linux). The methodology pins Rust 1.95.0 as the reproducible-build anchor; the local builds use 1.96.1, which is newer and builds clean. Record this wherever a canonical artifact hash is claimed.
- UDPspeeder-simd embeds as a bare store plus worktree at
software/udpspeeder-simd/branch_libev/(its default branch isbranch_libev, notmain). Recorded in.host-softwarewith nodeploy/artifact, so it is a source pin with no reproducible-build claim yet (migrated software). - host-lint is also a
.host-softwarecomponent (the gating tool):--install-hooksreads a component'shooksscript and builtartifactfrom its materialized worktree, so host-lint must be materialized and built for the commit gate to land.
2026-07-03 — gate green; runtimes installed; push auth is flaky
- The verify gate (
host-lifecycle software --check .) is green: both components at pin, all phase receipts recorded, prose clean, reconcile clean. The commit-msg hook blocks an ordinal tell in a message (tested:phase 1is rejected, exit 1). - Tooling on this machine, all under
~/.local:bin/host-lint,bin/host-lifecycle(v0.35.1),bin/host-prove(v0.3.0),bin/allium(3.5.0),jdk-21/(Temurin 21.0.11),share/tla2tools.jar(v1.8.0, sha256 237332bd). The gate needshost-proveon PATH because host-lint's ownkani:obligations surface once it is materialized as a component; the cheap gate probeshost-prove --help, it does not run Kani. - Two clearance details: the toolchain HAZARD (artifact with no toolchain) is waived by
repro-exempt = call/0002on host-lint (consumed tool, not reproduced here); andremapis askipreceipt, notdone, becauseremap --checkerrors on an empty or absent.host-remap, so a no-rename case-(a) adoption cannot pass adonerecheck. - Push auth to
connollydavid/agentic-UDPspeeder-simdis intermittent: the stored credential is theslartibardfasttoken, which 403s after the first few pushes landed. Ifgit push403s, surface it to the operator rather than retrying or swapping credentials. As of this entry,f8d36edand5ec59afare local only.
2026-07-03 — followed the host procedure: conformant, no upgrade available
- Ran the case-(c) upgrade/verify cycle from the
hostprocedure (github.com/connollydavid/host).host-templateupstream is still at565410a, the exact revision the.hoststamp adopted: agit fetchin the submodule found no commits after it, and565410ais an ancestor oforigin/main.host-lifecycle upgrade .reports up to date (baseline46a1fd2, 0 out of order). With no newer methodology to pull in, following the host reduced to the verify gate. - Verify gate is green.
host-lifecycle software --check .exits 0 (both components at pin, every phase receipt valid, reconcile clean, prose clean, no worktree-symlink hazards);validate plan/andvalidate call/bothok;host-lint --alland--logclean. Independently confirmed: udpspeeder-simd worktree is at pin3374e3bonbranch_libevwith a clean tree, host-lint at78804cd, and the commit-msg hook still rejects an ordinal tell (aphase 1message exits 1, a clean message exits 0). - udpspeeder-simd still carries no
.allium/.tlaspec, so the requirements and timing lanes stay inert and there is no spec-without-lane defect. Every spec found on disk belongs to a tool's own worktree, not to the software under development. - Standalone
host-lint --proseon the authored docs prints advisorynote:lines but exits 0. Those notes are below the enforced bar; the in-process prose audit insidesoftware --checkis the gate that binds and it reports no flagging or warning tropes. Most notes sit in verbatim copied-in content (theUPGRADING.mdledger,cast/applying-personas.md) that is not reworded locally. The notes are not a red gate. - Corrects the 2026-07-03 "push auth is flaky" entry above: the commits it listed as
local-only (
f8d36ed,5ec59af) are now pushed.HEAD == origin/mainand the tree is clean. Push auth may still 403 intermittently; surface it to the operator if it does.
2026-07-03 — embedded the packages feed as a second software lane
- Added
packages(a fork ofopenwrt/packages, GPL-2.0, an OpenWrt feed of Makefiles/shell/C) as a Where-room component in.host-software: source pin1d40ad9on canonical branchmaster, no build/artifact (migrated build-recipe feed, not a built artifact). Materialized tosoftware/packages/master/;software --checkis green. - Mechanics for a new component: host-lifecycle has no
software --add, so a new lane is a hand-edited.host-softwarestanza plussoftware --materialize --item <name> .(the--itemscope avoids re-touching existing worktrees). The embed and release phase receipts are written withhost-lifecycle receipt --record <phase> --component packages --disposition done|skip (--evidence|--reason) .; a new component HAZARDs on missing embed+release receipts until both are recorded. - Key rule (call/0003): commits in the packages worktree carry NO
Co-Authored-By: Claudetrailer, so the feed stays upstream-clean foropenwrt/packages. The operator confirmed the scope is packages only, so host-repo commits keep the trailer. Recorded in call/0003 and the CLAUDE.md packages project-specifics.
2026-07-03 — push-auth root cause: slartibardfast is pull-only; helper is store
- The recurring push 403 has a concrete root cause rather than flakiness.
gh api repos/connollydavid/agentic-UDPspeeder-simdshows theslartibardfastaccount haspush: false(pull only); the owner accountconnollydavid(also logged in to gh) has push. And git's credential helper here isstore(~/.git-credentials), not gh, sogh auth switch -u <user>does not change the tokengit pushsends. The store already holds slartibardfast's token, so pushes go out as slartibardfast and are denied. - To push as slartibardfast (the operator's intended identity), grant slartibardfast
write on the repo; then a plain
git pushworks with no helper change. To makegh auth switchactually route pushes, rungh auth setup-gitso git uses the active gh account's token. Do not push as connollydavid without operator direction: it changes the public push identity, and the operator is provenance-sensitive. - Pending operator decision (asked, away from keyboard): grant slartibardfast write,
push as connollydavid, or operator pushes manually. As of this entry
mainis ahead by 7 unpushed commits (packages embed, call/0003, CLAUDE.md, two memory entries, the earlier verify-sweep entry, and the three-persona commit).
2026-07-03 — resolved: push as connollydavid via a one-shot gh credential
- Operator chose to push as the owner. The working recipe:
gh auth switch -u connollydavid, thengit -c credential.helper= -c credential.helper='!gh auth git-credential' push origin main. The emptycredential.helper=resets the inheritedstorehelper (which holds slartibardfast's read-only token and 403s), and the gh helper then supplies connollydavid's token for that one push, with no change to global git config. - All pending commits landed (
23bdc1f..6f1e66a);mainis in sync with origin. - Going forward: repeat that one-shot override, or run
gh auth setup-gitonce (with connollydavid active) and clear the stale store token so a plaingit pushworks. slartibardfast stays read-only on the repo (not a collaborator); grant it write only if a bot-identity push is wanted later. This supersedes the earlier "push auth is flaky" framing: the cause was permissions plus a static store token, not flakiness.
2026-07-03 — published the book to GitHub Pages
- Ran the publish phase.
host-lifecycle book .(v0.35.1) writes book.toml withsrc = mdBook/srcandbuild-dir = mdBook/out, a SUMMARY in lifecycle order, and 17 pages;book --check .passes (every room renders: cast 5, plan 1, software 1, call 4, reference 4, memory 1). book.toml and mdBook/ are gitignored generated output. - Fixed two defects in the reference doc-site workflow. It was named
site.yml, but the publish-phase recheck istest -f .github/workflows/mdbook.yml, so adonepublish receipt would HAZARD until the file is namedmdbook.yml. It also published./book, while v0.35.1 builds tomdBook/out. Renamed to mdbook.yml, setpublish_dir: ./mdBook/out, and corrected the stalesrc=docscomment. Both look like template bugs at revision 565410a, worth proposing upstream. - CI (
mdbook.yml, on push to main) builds with mdbook v0.4.40 and deploys mdBook/out to thegh-pagesbranch via peaceiris/actions-gh-pages. GitHub Pages was disabled (the/pagesAPI returned 404), so enabled it from gh-pages root withgh api -X POST repos/connollydavid/agentic-UDPspeeder-simd/pages -f source[branch]=gh-pages -f source[path]=/. The site is live at https://connollydavid.github.io/agentic-UDPspeeder-simd/ (HTTP 200). - Publish receipt flipped from skip to done;
software --checkis green (the recheck now finds mdbook.yml). Verified locally with the pinned mdbook v0.4.40 before pushing.
2026-07-03 — udpspeeder-simd package builds clean against the x86_64 SDK
- SDK approach pivoted.
make worldwas excessive; used the public prebuilt x86_64 snapshot SDK (openwrt-sdk-x86-64_gcc-14.4.0_musl, the one OpenWrt CI uses) rather than a from-source build. The embeddedopenwrtcomponent stays but is not needed for this path and adds ~2.5 min tosoftware --check(flagged for the operator to keep or remove). - OpenWrt needs a case-sensitive FS: /mnt/c is case-insensitive and the build refuses it, so software/openwrt is symlinked to ext4 (/home/dconnolly/host-stores); the SDK and its build_dir also live on ext4.
- The package (net/udpspeeder-simd, authored in the packages worktree) built cleanly:
make package/udpspeeder-simd/compileproduced udpspeeder-simd-2026.07.03~3374e3bb-r1.apk (snapshot uses .apk, not .ipk). No fork change was needed — the udpspeeder-style cc_cross- gitversion Build/Prepare sufficed, and the makefile's
export STAGING_DIR=/tmp/did not break it. Dynamic linking confirmed by deps libc/libstdcpp6/librt/libatomic1. PKG_MIRROR_HASH = 225c0fef087e56c190ec1a626fc244a0e32f1f791d5e22efe604e962026c4698.
- gitversion Build/Prepare sufficed, and the makefile's
- Task receipts done: #fork-build, #feed-package, #service-integration. Pending: #test-script (CI runs it), #ci-sdk-lane, #interop, #pr, #backports. The package files are authored in the packages worktree but NOT yet committed or pushed to the fork; the branch strategy and the outward push/PR await operator go.
2026-07-03 — udpspeeder-simd pushed to the fork on a feature branch (PR held)
- Operator chose fork + PR, but the PR needs a review round, so it is NOT opened. Committed
net/udpspeeder-simd/ (Makefile + files + test.sh) on branch
udpspeeder-simdof connollydavid/packages, commit db858a8, pushed. Author + DCO Signed-off-by = David Connolly david@connol.ly (real name + public email, no Claude trailer, per call/0003 + OpenWrt rules). Subject "udpspeeder-simd: add package"; body references https://github.com/openwrt/packages/issues/28562. - The packages worktree had no git identity; set it to David Connolly / david@connol.ly. The canonical master worktree was used in-place to make the branch then restored to master @ 1d40ad9, so the .host-software pin and software --check stay intact; the feature branch lives on the fork, not as a host-tracked worktree.
- Open the PR (after review) from https://github.com/openwrt/packages/compare/master...connollydavid:packages:udpspeeder-simd?expand=1 Before submitting, consider syncing the fork onto current openwrt/packages master (the branch is based on the fork's master 1d40ad9, which may be behind upstream).
- openwrt embed KEPT (operator decision), despite the ~2.5 min software --check cost.
- Receipts: #fork-build/#feed-package/#service-integration/#interop done; #pr held (review round); #test-script + #ci-sdk-lane pending (CI); #backports policy-constrained.
- Update: rebased the branch onto current upstream openwrt/packages master (333bf60); tip is now a7f0f087, a single commit with no merge commits in the PR range; force-pushed to the fork. Formalities self-check (against .github/formalities.json) passes: crlf=0, subject 28<=60, body<=79, DCO Signed-off-by matches a non-noreply author, conffiles declared, openwrt-meta present, no patches. Open review-round items: Run Testing details (built x86_64 via the snapshot SDK gcc-14.4.0 + CI test.sh; a physical-device run is optional but stronger), and a release-notes line if the formalities bot requests one.
2026-07-03 — PR opened to openwrt/packages (pull/29901)
- Opened https://github.com/openwrt/packages/pull/29901 ("udpspeeder-simd: add package") as connollydavid, from connollydavid:udpspeeder-simd to openwrt:master, referencing the issue. Confirmed host-side clean first: main == origin, fork branch a7f0f087 synced, four receipts done. Release notes are in the PR body (a section), testing framed as x86_64 build + CI test.sh (operator accepted that trade-off).
- Corrected the plan: #pr no longer depends on #ci-sdk-lane. The operator directed opening the PR against OpenWrt's own CI, so the fork-side SDK CI lane is independent, not a prerequisite. Recorded #test-script done (script added + wired; the PR CI exercises it) and #pr done. Pending: #ci-sdk-lane (fork-side, optional) and #backports (policy: new packages are not normally backported to release branches).
- Next: watch the PR's FormalityCheck + multi-arch build. If the bot wants release notes in a different form, amend and force-push the fork branch.
2026-07-03 — addressed the Copilot review; fork version-banner fix; idiomatic test.sh
- Copilot flagged two on PR 29901, both valid: test.sh could false-positive (its grep matched
the program name, which a dynamic-loader error echoes), and the init had
fix_latencynon-local plus a duplicatedsock_buf. Both init issues were inherited verbatim from net/udpspeeder (the sibling has the same bugs). @codemarauder is Nishant Sharma, the net/udpspeeder maintainer; offered him co/primary maintainership of udpspeeder-simd in the PR comment. - Idiomatic gap and its fix: OpenWrt test.sh idiomatically greps the package version, but the
binary's print_help() truncated gitversion to 10 chars (
strncpy(...,10)), so a full-version grep would miss. Fixed the fork (main.cpp copies the full string; commit 2e12e5b pushed to branch_libev), re-pinned .host-software to 2e12e5b, updated the package (PKG_SOURCE_VERSION 2e12e5b, PKG_MIRROR_HASH 6d4564dc, PKG_VERSION now 2026.07.03~2e12e5b4), and switched test.sh togrep -qF "$PKG_VERSION". Amended the single PR commit (7f5bc7c8), force-pushed; PR still discrete (4 files). Posted a terse reply comment (#issuecomment-4877656043). - Rebuilt via the SDK (exit 0) for the new hash. The runtime version-grep is confirmed by OpenWrt's PR CI, since the cross-built musl binary cannot run on this host.
- Scope decision: we will NOT touch net/udpspeeder (operator). Removed the "happy to send a separate PR" offer from the PR comment (edited #issuecomment-4877656043); the sibling's same two init bugs are left alone.
2026-07-03 — OpenWrt PR #29901: 32-bit ARM build fix in the fork
- The PR's multi-arch CI (openwrt shared
Feeds Package Test Build) ran and failed only onarm_cortex-a15_neon-vfpv4andarm_cortex-a9_vfpv3-d16; aarch64, mips_24kc, x86_64, i386, powerpc, riscv64 all passed. Root cause was in the fork source, not the package:xor_spe.Smarked the stack non-exec with.section .note.GNU-stack,"",@progbits. On 32-bit ARM@starts a line comment, so gas ate@progbitsand reported "junk at end of line, first unrecognized character is,". aarch64/mips/ppc/riscv/x86 do not use@as a comment char, which is why only 32-bit ARM broke. - Fix (universal, per call/0005 — build adjustments live in the fork): spell it
%progbits. gas accepts%for the ELF section type on every target (verified locally: x86 g++ assembles both@progbitsand%progbits), and%is the required form where@is a comment. Fork commitc2b3759onbranch_libev. - Propagation: re-pinned
.host-softwareudpspeeder-simd → c2b3759; bumped the PR'snet/udpspeeder-simd/MakefilePKG_SOURCE_VERSION → c2b37590 and refreshed PKG_MIRROR_HASH → d6f564823fa0217788bc937e06c71266bfa3a457d0803912b2d10bcb9957d3ba (amended the single PR commit, force-pushed; PR head 5d8ed050). Kept the fork commit's Claude co-author trailer; the packages commit has none (call/0003). - Mirror-hash regen recipe (local x86_64 SDK): set PKG_MIRROR_HASH to 64 zeros, run
make package/<pkg>/download V=s; the download clones, packs the reproducible git tarball, and printsgot <real-hash>.skipdoes NOT work for a git-proto source (the github archive downloader refuses without a real sha256). Then compile to confirm.
2026-07-17 — OpenWrt PR #30015 (net/udpspeeder) + timeout fold into PR #29901
- Opened https://github.com/openwrt/packages/pull/30015 (net/udpspeeder) against
openwrt/packages:master from connollydavid/packages:udpspeeder-toolchain-flags. Two atomic
commits, PKG_RELEASE 3→4→5:
udpspeeder: build with the toolchain CXX and flags: new patch 010-build-with-toolchain-flags.patch builds the cross target with $(CXX) and honours $(CXXFLAGS)/$(LDFLAGS), makes gitversion overridable; package drops the Build/Prepare sed hacks, injects the version via MAKE_FLAGS, adds PKG_BUILD_FLAGS:=no-mips16.udpspeeder: pass the timeout option to the binary: addsprocd_append_param command --timeout "${timeout}"after --mtu in files/udpspeeder-init.Fixes: .../issues/18955auto-closes on merge. The option was validated and shipped in the sample config (tunnel2) but never passed to the binary.
- no-mips16 is REQUIRED, not cosmetic: honouring CXXFLAGS pulls -mips16 onto
mips_24kc/mipsel_24kc, where C++ std::atomic emits a
syncbarrier MIPS16 cannot encode ("opcode not supported on this processor: mips2 (mips2) 'sync'"); no-mips16 strips it (MIPS32). - Non-drift confirmed from binary help:
--timeout ... unit: ms, default: 8ms; UCI schema default is 8, so wiring it is a no-op at the default and just makes the advertised option work. - Verified locally via snapshot SDK on all ten CI arches (scratchpad/build_all.sh): all pass; both mips controls (without no-mips16) fail on the sync opcode, proving necessity.
- PR #29901 (net/udpspeeder-simd) had the identical latent timeout bug. Folded the same
--timeoutline into the singleudpspeeder-simd: add packagecommit (amend, NO PKG_RELEASE bump since 1.0.0-1 is unreleased); force-pushed connollydavid:udpspeeder-simd → 3540c71ce. Posted a terse alignment comment (#issuecomment-5002468098) referencing #30015. - Hygiene: packages commits carry NO Claude trailer (call/0003); sign-off = author = David Connolly david@connol.ly. #30015 rebased onto current upstream/master (net/udpspeeder untouched upstream); #29901 left on its older base (PR diff is clean vs merge-base and CI builds on the snapshot SDK, so no rebase needed and none requested). .host-software NOT re-pinned (feature-branch work; the master pin is unchanged).
2026-07-17: #30015 "Dirty patches detected" — patches must be quilt-refreshed
- All ten Feeds Package Test Build jobs failed at
make package/udpspeeder/refresh: the CI requires each patch byte-identical to quilt's canonical output. Our patch applied at exact offsets with no fuzz, yet still failed. - Root cause: OpenWrt refreshes with
QUILT_DIFF_OPTS="-p" quilt --quiltrc=- refresh -p ab --no-index --no-timestamps(openwrt/openwrt include/quilt.mk), and GNU diff-pappends function context to hunk headers (@@ -51,7 +51,7 @@ cygwin:git_version).git format-patchoutput lacks the suffix, so the refresh rewrites the file and the git-diff check trips. - Fix recipe: extract the pinned source (
git archive 20230206.0), set uppatches/+ series,quilt --quiltrc=- push -a, run the refresh command above, copy the refreshed patch back. Verified idempotent: a second refresh reports "is unchanged". Quilt preserves the git header (From/Subject/Signed-off-by/diffstat) untouched. - Folded into the toolchain commit via
commit --fixup+GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash; range-diff showed only the two hunk-header lines changed, timeout commit byte-identical (=). Force-pushed7a76af810...075f4a7e0(lease + force-if-includes). - Lesson: any patch destined for openwrt/packages should be generated or round-tripped through quilt with those exact args before committing, not taken raw from git format-patch.
2026-07-31 — upstream merged our toolchain-flags PR; taking it in both packages
- wangyu-/UDPspeeder merged https://github.com/wangyu-/UDPspeeder/pull/356 (686d6079), then
reshaped it in b6a1b594:
crosswas restored to the hardcoded${cc_cross}, and the change landed as a NEW target instead:cross_cxx: ${CXX} -o ${NAME}_cross -I. ${SOURCES} ${FLAGS} -O2 ${CXXFLAGS} ${LDFLAGS} ${LDLIBS}. Three deltas vs what we sent:-O2moved BEFORE${CXXFLAGS}(so a caller's -Os wins instead of being overridden),-lrtdropped,${LDLIBS}added. Ourgitversion ?=hunk survived verbatim, so MAKE_FLAGS version injection still works. - Consequence for the merged net/udpspeeder patch: its second hunk (the
gitversion ?=line) can no longer apply to any source at or past b6a1b594, since upstream already has it. So the patch had to be dropped as part of a version bump, not deferred. -lrtis a no-op on OpenWrt, PROVEN not assumed: musl'slibrt.ain the toolchain is an 8-byte empty archive (clock_gettime is in libc), and the packaged binary built withLDLIBS="-lrt"and without it are byte-identical (sha256 502163f5...). NEEDED is libstdc++/libgcc_s/libc either way. So neither package passes -lrt; a toolchain that needs it can pass LDLIBS. LeftDEPENDS:=+librtalone in both (inert: musl's libc provides it) rather than widen the diff.- The 3-year source bump 20230206.0 -> b6a1b594 is WIRE-SAFE, also proven: the only functional
change is swapping the in-tree
crc32hfor Stephan Brumme'scrc32_fast. Both are standard zlib CRC-32 (poly 0xEDB88320, init ~0, final complement); a comparison harness over every length 0..2048 x 8 random buffers matched exactly, and both give 0xCBF43926 for "123456789". The rest is typo fixes, anis_vaild->is_validrename, and a djb2/sdbm loop rewrite that removes a one-byte over-read without changing the hash. - OpenWrt version convention for an untagged git source is
<base>~<shortsha>+ a full-sha PKG_SOURCE_VERSION. Careful:~sorts BEFORE end-of-string (Debian style), so20230206.0~<sha>would sort BELOW the released 20230206.0 and break the upgrade path. Used20260731~b6a1b594(commit date as base) so it sorts above. - Fork: added the same
cross_cxxtarget (commit 81fde6c, tagged v1.0.1, pushed); kept our cross/cross2/cross3 as-is since this fork deliberately dropped the hardcoded compiler paths. Re-pinned .host-software to 81fde6c.
2026-07-31 — correction: PR #29901 has no test.sh (the 2026-07-03 entry is wrong)
- The 2026-07-03 entry above says "#test-script done ... test.sh added beside the Makefile;
exercised by the OpenWrt PR CI runtime test", and
.host-task-receiptsrecords the same evidence forplan/0000#test-script. Both are FALSE as of now, and have been since 2026-07-04. - Traced through the branch reflog: test.sh survived to b7ef6490 (2026-07-04 21:31) and was gone
in the very next amend, b9e331ec (2026-07-04 23:16), the one that fixed the Build/Prepare
double-expansion. It was dropped accidentally, not by decision, and every later force-push
carried the loss forward. The PR head has carried three files (Makefile, config, init) ever
since, so the CI arches flagged
runtime_test: truehave had nothing to run. - Operator decision on 2026-07-31: LEAVE IT OUT rather than restore it, so the package ships
without a runtime test. BKPepe had questioned the script anyway ("Do we need this script at
all? This should be covered by generic testing"). The
#test-scriptreceipt is therefore stale and should be re-dispositioned by the operator (it is a tool-written ledger; not hand-edited here). - Lesson: an amend that fixes one file can silently drop another. Diff the file LIST against the PR head after every amend + force-push, not just the file contents.
2026-07-31 — OpenWrt packaging idioms, and how to test under qemu properly
- VERSION FORM for an untagged git source: do NOT hand-write PKG_VERSION. Set PKG_SOURCE_DATE
plus a full-sha PKG_SOURCE_VERSION and let include/download.mk derive it:
PKG_VERSION := $(subst -,.,$(PKG_SOURCE_DATE))~$(call version_abbrev,$(PKG_SOURCE_VERSION)), giving2026.07.31~b6a1b594(version_abbrev = 8 chars; it returns the FULL sha under DUMP, so the feed index shows the long form while the build uses the short one). Feed usage: 96 packages use PKG_SOURCE_DATE vs 3 that hand-write a~version. Changing the version form changes PKG_MIRROR_HASH, since the tarball's inner subdir is $(PKG_NAME)-$(PKG_VERSION). - UPSTREAM UDPspeeder TRUNCATES THE VERSION: print_help does
strncpy(buf, gitversion, 10), so--help(the flag OpenWrt's generic check finds) shows ten characters only. The old version20230206.0was ten characters exactly, so the check passed by luck for years. Any longer version needstest-version.sh, the idiomatic override (70 in the feed). Our simd fork already prints the full version, so only net/udpspeeder needs the override. - DEPS ARE DERIVED FROM THE ELF: include/package-pack.mk runs scripts/gen-dependencies.sh, which
reads NEEDED and FAILS the build for a linked library that is not declared. It never checks for
extras, so a declared-but-unlinked dep is inert. librt and libatomic were both provably inert:
70/70 builds and packet tests across all 35 published architectures with both removed, no binary
NEEDs either, and no
__atomic_*/__sync_*symbol anywhere. On musl, Package/librt/install copies nothing at all (ifneq ($(CONFIG_USE_MUSL),y)). - RUNTIME TESTS, the idiom:
test.sh <pkgname> <version>runs INSIDE a target-arch container (binfmt + qemu-user-static) with the package installed.pre-test.shinstalls test deps (apk add socat). A package-specific test.sh REPLACES the generic tests (version check included), and test-version.sh is only consulted by the generic path, so test.sh must assert the version itself. BusyBox in OpenWrt is built with NC_SERVER=n and NC_EXTRA=n, so nc can neither listen nor exec: use socat via pre-test.sh. Do not use a fixed sleep, the container is emulated; retry the payload instead. - QEMU CPU MODELS, do not trust the default. powerpc_8548 (CPU_TYPE 8548, e500v2) fails under the
default 32-bit PPC model with an illegal instruction, and it is NOT the package: the faulting
word disassembles to
iseleqinside musl libc, which holds 63 isel instructions while our binaries hold none.-cpu e500v2_v10runs it. Control: the musl loader alone faults under the default and prints its version under e500v2_v10. Every arch now names a verified model in .github/scripts/arch-map.tsv; where qemu-user lacks the exact CPU the closest SUPERSET is used (xscale and fa526 -> arm926, cortex-a5 -> cortex-a7, pentium-mmx -> pentium2, pentium4 -> n270, since a weaker model invents failures and a richer one hides them and skews feature detection). - PROBE BY OUTPUT, NOT EXIT STATUS: musl's loader prints its version and exits 1. A first pass at CPU discovery keyed on exit status and reported every architecture broken. Judge the output.
- SCOPE: OpenWrt's CI runtime-tests only aarch64_generic, arm_cortex-a15_neon-vfpv4, i386_pentium-mmx, mips_24kc and x86_64; powerpc_8548 is runtime_test:false. So the CPU-model work is OUR local lane's concern, not something the OpenWrt PR depends on.
- arm_fa526 (gemini) is BUILD-ONLY under qemu-user, and the cause is the emulator, not the
package. That target compiles plain ARMv4: its binary holds ZERO bx/blx, where the ARMv5 build
of the same package holds 133, so it cannot enter Thumb code at all. qemu-user always maps a
Thumb vdso, so the call to __vdso_gettimeofday runs Thumb bytes in ARM state and branches to a
wild address; the in_asm trace ends in
IN: __vdso_gettimeofdaydecoding 46c046c0 (Thumb nop;nop) as ARM and branching to 0x40b2838c, exactly the SIGSEGV si_addr. Proven not ours two ways: the CURRENTLY SHIPPED 20230206.0 package fails identically under -cpu arm926, and a trivial C++ program against the same musl and libstdc++ runs fine there. Real hardware never meets it, since the Linux ARM vdso needs ARMv7 and a gemini kernel supplies none, leaving musl on plain syscalls. qemu-user has no flag to withhold the vdso, so the packet test is skipped for that arch rather than run under a v7 model that would hide real mismatches.
2026-07-31 — udpspeeder-simd crashed on x86_64 without SSSE3 (found by faithful qemu models)
- lib/fec.cpp initialised
addmul1_x86_fn = addmul1_ssse3and only ever RAISED it to AVX2 or AVX-512. Nothing probed SSSE3, and x86_64 does NOT imply it: AMD K8/K10 (Athlon 64, Opteron, Phenom, Phenom II, Athlon II) lack it, as do early 64-bit Intel parts; AMD added it with Bulldozer in 2011. Those CPUs took SIGILL on the first PSHUFB in the Reed-Solomon inner loop, so the tunnel died on its first FEC batch. Upstream udpspeeder was unaffected. - Only visible under a faithful CPU model. Every earlier sweep used qemu's DEFAULT x86_64 model
(rich), and OpenWrt's own CI runtime-tests x86_64 on a GitHub runner that has SSSE3/AVX2, so the
bad path never ran. It appeared the moment the map named
-cpu qemu64(the AMD64 baseline). - Fix (fork v1.0.2, f3834047): pointer starts at scalar and rises only as CPUID allows, via
cpu_has_ssse3() (leaf 1 ECX bit 9) and cpu_has_sse2(); new SSE2 addmul1 sits beneath SSSE3, so a
CPU without PSHUFB still vectorises. SSE2 has no byte shuffle, so it multiplies by repeated
doubling:
_mm_add_epi8(x,x)for the shift and_mm_cmpgt_epi8(0,x)to select the reduction. The reduction constant is read fromgf_mul_table[2][0x80], not hardcoded, so it follows the field. Measured 1.73x scalar at 1500B (913 vs 1579 ns); ssse3 is 9.8x. - Also hardened packet_cook.cpp: its AVX2 tier was set from CPUID leaf 7 EBX bit 5 ALONE, with no OSXSAVE/XCR0 check and no max-leaf check. Now mirrors the correct probe.
- Test method worth reusing:
bench_addmul1_force(name)pins one implementation so test_fec can hold EVERY path against scalar over all 256 multipliers and sizes hitting each loop and tail. Ran the suite underqemu-x86_64 -cpu qemu64|Nehalem|Haswellto exercise sse2/ssse3/avx2. AVX-512BW cannot be verified this way: qemu-user leaves ZMM state disabled in XCR0, so the OS-support check correctly declines it. - HARNESS BUG worth remembering:
find ... | head -1picked a STALE build dir from an earlier session in a reused SDK, so one x86_64 result described a binary nobody had built that run. Select by newest mtime (-printf '%T@ %p'| sort -rn). Fresh CI SDKs never hit this; local reused SDKs do.
2026-07-31 — qemu64 is not the x86_64 floor; the first Opteron is
Asked whether we are aligned with the very first x86_64 chip, and the honest answer needed evidence at both ends rather than an assertion. Findings worth keeping:
-cpu qemu64GRANTS SSE3. It caught the SSSE3 crash, but it is not the architectural floor and never was. The floor is-cpu Opteron_G1,-sse3: qemu's model of the AMD Opteron 240 of 2003, the first x86_64 silicon, with SSE3 subtracted for the earliest stepping. SSE3 only arrived on K8 revision E in 2005. The arch map's x86_64 row now names that model.- Compile-time floor is already right: the OpenWrt x86_64 toolchain defaults to
-march=x86-64with sse3, ssse3, sse4, cx16 and popcnt all DISABLED (gcc -Q --help=targetproves it, andCONFIG_TARGET_OPTIMIZATIONis only-Os -pipe, so nothing raises it). XGETBVIS NOT BASELINE. It arrived with XSAVE in 2008, so a K8 faults on it. Our probes check CPUID leaf 1 ECX bit 27 (OSXSAVE) first, and the EMITTED code keeps the order: every xgetbv sits behindbt $0x1b,%ecx/jae. Verified in the disassembly, not just the source. Worth re-checking after any compiler bump, since the guard is a branch the optimiser could in principle move.- CRC32C's hardware path is behind
crc32c_has_hw()(leaf 1 ECX bit 20, SSE4.2) and falls back to the slicing-by-8 table, so it is safe on K8 too. - Static screen worth reusing, the x86 analogue of isa-check.sh: disassemble and list every
mnemonic outside the baseline together with its containing function.
udpspeederholds ZERO;udpspeeder-simdconfines all of them toaddmul1_{ssse3,avx2,avx512},xor_tile_{avx2,avx512}andcrc32c_hw, each reached only through CPUID. That is the property to hold, not "it ran". - Dynamic proof: both packages pass the packet test under
Opteron_G1andOpteron_G1,-sse3, and test_udpspeeder under those models selects sse2 and agrees with scalar over all 256 multipliers, declining ssse3/avx2/avx512bw. - Reading tests through a grep can invent a hollow section: filtering on "addmul1" hid the tier results, whose pass lines read "sse2 agrees with scalar". Read the section, not a keyword.
2026-07-31 — the x86 lacuna: OpenWrt has three x86 package arches, not one
Asked whether x86 coverage is complete. It was not, and the gap was in the emulation models rather than in the architecture list.
- OpenWrt publishes FOUR x86 subtargets (64, generic, geode, legacy) but THREE package
architectures. geode and legacy declare no CPU_TYPE, so both fall to the i386 default of
pentium-mmx(include/target.mk). One row covers the pair; generic is pentium4; 64 is x86_64. - BOTH i386 models were too rich, the same failure as qemu64 on x86_64:
n270(Atom) HAS SSSE3, so the i386_pentium4 job selected the SSSE3 path and never once ran the SSE2 tier a real Pentium 4 uses. Nowpentium3,+sse2, which is exactly a P4's ISA.pentium2HAS CMOV, which a Geode GX/LX and a Pentium MMX do not. Nowpentium,+mmx.
- qemu-user genuinely gates these, proven by one-instruction controls rather than assumed:
pentium2runs cmov and faults on pshufb;pentium2,-cmovandpentium,+mmxfault on cmov;n270runs BOTH cmov and pshufb;pentium3,+sse2runs movdqa and faults on movddup. Feature subtraction/addition on a stock model works, so compose the model instead of borrowing one. - Static screen of the geode build: nothing above the floor outside CPUID-guarded functions. The
one CMOV lives inside
addmul1_ssse3, which GCC is free to emit becausetarget("ssse3")implies P6, and it only ever runs on a CPU that has SSSE3 and therefore CMOV. Not a defect. - All three x86 arches pass both packages under the tightened models.
mips_4kecandriscv64_riscv64DO have package directories on the buildbot, so the listing alone suggests 37 arches. They are frozen: last built April and July of 2025 against zlib 1.3.1, where a live arch carries a build from this month. Check the DATES, not the directory.
2026-07-31 — MMX helps the XOR and hurts the multiply; measure per path
"MMX is still SIMD" — and OpenWrt's lowest x86 arch (i386_pentium-mmx, from geode and legacy) has MMX and nothing above it. So the fork was taken down to that floor. Two opposite results, and the lesson is that "add a lower SIMD tier" is not one decision but one per kernel.
- MMX addmul1: WRITTEN, MEASURED, REVERTED. The repeated-doubling multiply costs ~5 ops/byte at MMX's 8-byte width, against ONE L1-resident load per byte for scalar, which indexes a single 256-byte row of gf_mul_table for a fixed c. Measured on a real 32-bit build: mmx 0.55x scalar (3166 vs 1738 ns at 1500B); sse2 1.70x. SSE2 only wins by doubling the width. The floor keeps the scalar table, which is correct AND faster.
- MMX xor_tile: KEPT. XOR is one op per width, so width converts straight into throughput. Against the four-byte word path i386 actually had: mmx 1.46x, sse2 3.08x.
- The bigger find: packet_cook.cpp's ENTIRE x86 SIMD block was
#if defined(__x86_64__), so every 32-bit build used the 4-byte word loop — including i386_pentium4, which has SSE2. x86/generic is a commonly used target, so that was a 3.08x left on the floor for years. fec.cpp had already been widened to i386; packet_cook.cpp had not. When widening one file's ISA guards, grep for the same guard elsewhere. - MEASURE 32-BIT MMX ON A 32-BIT BUILD. On x86_64 GCC emulates __m64 with SSE (TARGET_MMX_WITH_SSE), so an x86_64 measurement of MMX is not MMX. Build with the OpenWrt i386 toolchain and run the binary natively (WSL2 runs i386 ELF fine); qemu timings are useless for ISA ratios.
- EMMS is mandatory: MMX registers alias the x87 stack and the tunnel does FP work in its timers.
- Renaming a force() tier label silently turns the reference call into a no-op, so the test compares a tier against itself and always passes. Renamed tier 0 "scalar"->"word" and had to fix the test's reference call in the same breath. A hollow green, exactly the shape call/0035 warns about.
Dispatch is a separate claim from implementation, and it needed its own lane
The tier comparisons pin a path and hold it against a reference. That says nothing about which path the dispatcher CHOOSES, and the choice is what the SSSE3 fault broke. A CI runner carries every feature, so it always chooses the top path; no runner-only job can see the bug class.
- Three runtime dispatchers exist, not one: addmul1 (fec.cpp), xor_tile (packet_cook.cpp) and crc32c (crc32c.h, an SSE4.2 probe resolving a function pointer on first call). All three now report their choice through bench_*_auto() and are held to EXPECT_ADDMUL1 / EXPECT_XOR_TILE / EXPECT_CRC32C.
- The auto hook must RE-DERIVE, not read back. bench_addmul1_force() overwrites the pointer, so the reporter re-runs the real selection (addmul1_select(), extracted from init_fec) and the selection now resets to scalar first rather than only raising. Reading the pointer back would report whatever a previous test pinned.
- QEMU TCG IMPLEMENTS NO AVX-512 AT ANY CPU MODEL (checked on qemu 11.0.2): -cpu Skylake-Server warns "TCG doesn't support requested feature ... avx512bw", clears the CPUID bit and leaves XCR0 at 0x207 rather than 0xE6. So the AVX-512 path cannot be exercised under emulation at all, on any model, ever. That row instead proves the gate DECLINES on a part whose model name says otherwise. AVX-512BW correctness is verified only when a GitHub runner happens to land on an Intel host.
- The OpenWrt i386 toolchains default to -march=i486, so the march must be named explicitly to reproduce what OpenWrt ships (pentium-mmx for geode/legacy, pentium4 for generic).
- A pentium-mmx-built binary runs clean on qemu's 486 model and correctly selects scalar/word/sw, so the bottom rung of the ladder is reachable and tested even below OpenWrt's own floor.
- The Makefile forces
export STAGING_DIR=/tmp/, which overrides the environment, so the OpenWrt toolchain wrapper only warns and no STAGING_DIR needs setting for amake test-crossbuild. It matters only when invoking the cross gcc directly.
The SPE XOR was never shipped, and the package could not have told us
The e500v2 SPE unit does the tile XOR 64 bits at a time. That path needs SPE=1 on the make line,
and net/udpspeeder-simd passes only cross_cxx gitversion=..., so every mpc85xx build ever
shipped has used the word path. Confirmed by objdump: with the flag, 20 SPE opcodes; without it, 0.
- GCC defines no e500 macro. GCC removed SPE support, which is why the path is a hand-written
xor_spe.Splus-Wa,-msperather than intrinsics. Diffing the predefines of-mcpu=8548against-mcpu=464fpyields only soft-float proxies (__NO_FPRS__,_SOFT_FLOAT,_SOFT_DOUBLE,__NO_LWSYNC__). Keying on those would shipevxorto a soft-float classic PowerPC and fault, the SSSE3 bug again. So auto-detection from the compiler is not available. - The target says it instead, and the software can read it. OpenWrt's
MAKE_VARSexportsCXXFLAGS="$(TARGET_CXXFLAGS) ...",TARGET_CXXFLAGS = TARGET_CFLAGS, and that carriesCPU_CFLAGS_$(CPU_TYPE), i.e.-mcpu=8548. So the makefile can read the core from the flags it is already handed, and the package needs no CPU conditional. Note the fork's own cross targets take the march insideCC, notCXXFLAGS, so the filter searchesCC CXX CFLAGS CXXFLAGS. - 8548 alone is too narrow. OpenWrt maps these 32-bit PowerPC types: 603e, 8540, 8548, 405, 440,
464fp. 8540 is e500v1 and also has SPE (v1 lacks only double-precision SPE FP, which this does
not use). No published target uses 8540 today. Match both, with
filter(whole words) notfindstring(substring). - The XOR round-trip cannot catch a wrong XOR, only a missing one. It asserts
changed && restored. XOR is its own inverse, so a wrong-but-deterministic path still restores; a deliberate truncation was caught in exactly the degenerate cases where the XOR did nothing at all. The differential comparison against the word reference is what actually bites, verified by truncating an SPE call and watching that test alone fail across every tile, length and offset. - The sweep's spe row is red until the package bumps.
arch-map.tsvnow asserts SPE opcodes for powerpc_8548, but the sweep builds from the package's pinnedPKG_VERSION. It passes only once the package points at a fork release carrying the makefile detection, i.e. the pending v1.0.3 bump.
The aarch64 gap was ours alone, and the pins needed a negative control
Closing the NEON and ARM-CRC32 gaps required no OpenWrt change at all. Proof: building the fork
with the airoha (cortex-a53) toolchain and that target's own -mcpu yields 6 crc32c* instructions
and 9 eor v*.16b NEON ops. The code already ships; only the tests were absent.
__ARM_FEATURE_CRC32needs a named core. Measured across the OpenWrt aarch64 toolchains:-mcpu=cortex-a53,-a72and-a76define it,-mcpu=generic(armsr/armv8,CONFIG_CPU_TYPEgeneric) does not. So three of the four published aarch64 targets ship the hardware checksum. Debian'saarch64-linux-gnu-g++defaults to no core, which is why fork CI compiled the path out and[CRC32C hw vs sw agreement]skipped silently. CI now also builds-mcpu=cortex-a53and requires the comparison to have run, since a skip must not read as a pass.- A pin that does not switch is a hollow green.
bench_*_force()returning 1 while the path stays the same makes the reference and the candidate the same code, so the comparison passes vacuously. Both aarch64 pins were checked by deliberately faultingaddmul1_neonand the NEONxor_tileand confirming each named test fails. Never trust a new comparison without that control. - The tier tests used to leave the reference pinned. They ended with
force("scalar")/force("word"), so every round-trip test after them ran the reference path, not the dispatched one. Now they end withbench_*_auto(). This was already wrong on x86 and would have silently removed NEON from the aarch64 round-trips. - qemu-aarch64
-cpu cortex-a53does implement CRC32, unlike the AVX-512 case, so this gap is fully closable under emulation.