Audited 2026-08-23. A standing reference: what is structurally weak about this project and its
GitHub setup, with the evidence for each claim. Not a bug list — docs/AUDIT_*.md hold those. This
is about the conditions that let those bugs exist and survive.
Everything below was measured against the tree at c028a37 unless stated. Where a claim is
inference rather than measurement, it says so.
This project is unusually strong in one dimension and unusually weak in another, and they are the same fact seen twice. The reasoning behind the code is documented to a standard most commercial projects never reach — and almost none of it is enforced by anything a machine checks. Part E lists what is genuinely good, and it is not a courtesy section: the strengths are what make the weaknesses survivable.
2026-09-14 — the table below is out of date in the project's favour. CI has since gained a
-fsyntax-only -Werrorpass over every C/C++ file, nine C++ self-tests, 41 harness scenarios that bootmain.cppagainst fake services, the launcher recovery matrix and pinnedshellcheck;PLAN_2026-09-14.md§A1 has today's coverage. What is still true, stated plainly: nothing in CI links an ARM build, and nothing can.cinder-homelinks against a dozen of Sony's own libraries from the firmware, which cannot be put on a public runner. The link gate istools/release.sh, which rebuilds the payload from source before every tag and refuses to tag unless the committed bytes match (D4). What CI does check is the shape of what ships —tools/check_arm_payload.sh: ARM, glibc ≤ 2.23, only libraries the firmware has, static setuid helpers.
| Surface | Lines | Compiled by CI? | Tested by CI? |
|---|---|---|---|
Rust (player/, installer/) |
35,921 | yes | yes (404 tests) |
C / C++ (cinder-home/src, cinder-audio, ldac-bridge) |
19,435 | no | no |
| Shell (33 scripts) | 5,288 | n/a | no |
| Never touched by CI | 24,723 (41%) |
That would be defensible if the uncovered 41% were peripheral. It is the opposite. The uncovered half is the code that:
- runs as root — eight setuid-root helpers, each installed by a
chmod 4755ininstall_cinderhome.sh(this said "twelve" until 2026-09-14, counting installs across scripts rather than binaries); - owns the boot path — the launcher, the crash supervisor, the bad-boot counter, the auto-revert ladder, i.e. every mechanism standing between a bad build and a brick;
- drives closed Sony services over hand-recovered vtable offsets, where a wrong argument shape
reaches
operator new[]inside a core service (this rebooted the device twice on 2026-08-11); - performs the USB-MSC handoff, where an ordering mistake corrupts the user's music volume.
The well-covered half — cinder-ui, with 311 tests — is pure drawing and navigation logic that
cannot brick anything, cannot corrupt anything, and cannot escalate privilege.
The testing effort is inversely proportional to the blast radius. This is the single most important structural finding in this document.
2,500 lines of C++ shim — player_shim.cpp, effect_shim.cpp, tuner_shim.cpp,
analyzer_shim.cpp, power_shim.cpp — is the entire IPC surface to PlayerService, EffectCtrlDmp,
the FM tuner and the power manager. It has no tests of any kind and is not compiled by CI.
Its correctness rests on hand-recovered ABI declarations in effect_abi.hpp /
playerservice_abi.hpp, where being wrong does not produce a compile error — it produces a
mis-marshalled call into a closed service. This is precisely the failure mode the project has
already been bitten by, and it is the one part of the tree with no automated check at all.
2026-09-01 — first crack, and a real defect with it.
cinder-audionow has a test:eqrange_selftest(in CI's self-test list and inbuild.sh), over the rule extracted tocinder-audio/src/eq_range.h. It is not a token test — writing it found a live bug.
SetEq10BandValuetakes half-dB units and a value outside ±20 does not clamp inside the service: it ZEROES the band.effect_shim.cppforwarded whatever it was handed. The UI could never produce an out-of-range gain — every site in the EQ screen clamps — but the settings loader could, and does: it parsesi8, which accepts −128..127, from a file on/contents, which is vfat and writable by any PC the player is plugged into. SECURITY.md already treats that volume as untrusted everywhere else. One corrupted or hand-edited line silently flattened a band, drew its knob outside the EQ field, and was written straight back out on the next save.Clamped now in both places on purpose: the Rust loader protects Cinder's own model, and the shim protects the service, which cannot defend itself and whose failure is silent. A third defect fell out of the same read —
cinder_get_eq_bandsreturned early when the renderer was not up, leaving the caller'ssigned char bands[10]uninitialised and marshalling ten bytes of stack into the DSP. Unreachable today, which is exactly how it would have stayed until it wasn't.What this does NOT close. The IPC surface is still untested and mostly untestable off-device: the vtable offsets, the argument shapes, the object sizes. What it shows is that the shims are not purely IPC — there is validation logic in there, it is the last line before a closed service, and it was absent. The rest of
player_shim.cppandtuner_shim.cppdeserve the same read. A2 stays open, one rung less high.
install_cinderhome.sh is 757 lines and contains the crash supervisor, the bad-boot counter, the
escape ladder and the kill switch. cinder-msc.c's helper scripts perform the mount/unmount
ordering that the file's own header warns will "eat the user's library" if reversed.
No shellcheck, no bash -n syntax gate, no test harness. tools/test_launcher.sh exists and
covers a 44-case recovery matrix — and nothing automatic runs it.
Both halves closed 2026-08-24.
tools/shell_check.sh(pinnedshellcheck+bash -nover all 36 scripts) and the launcher matrix now both run in thenativeCI job. The matrix also stopped lying: one case makes/data/cinderunwritable withchmod, which does not bind uid 0, so run as root it reported a failure about the tester rather than the launcher. It skips itself there now, with a root-proof variant covering the same rule — 55 cases as a normal user, zero failures either way. The launcher itself was already correct; the guard it needs was added after the 2026-07-26 brick and is proven by write-then-read-back rather than[ -w ].
cinder-home/build.sh runs six C++ self-tests (guard recovery, volume ramp, BT edge, jack edge,
BT switch reconcile, DB signature), the GLIBC ≤2.23 ceiling gate, and the qemu construction
preflight.
build.sh is invoked from exactly one place: tools/release.sh — a manual, local, opt-in
script a maintainer chooses to run. No workflow calls it. So on any given commit those gates are
worth exactly as much as someone remembering.
This is recursive: the self-tests added during the 2026-08-23 audits are subject to the same problem the moment they were written.
By design — and correctly — most of what matters can only be settled on hardware. But the consequence is not managed:
STATUS.mdcarries a standing list of device-unverified claims that only grows between hardware sessions.- The project has twice discovered that a successful write is not evidence a feature works (high gain; and DSEE AI is currently in the same unresolved position — see A7).
- There is no staging environment.
cinder-probeis the mitigation and a good one, but it still requires the device.
Partly addressed 2026-08-24.
cinder-home/harness/boots the realmain.cppoff-device against fake services, so bring-up ORDER, service-availability behaviour and polling RATE can now be observed without hardware. It does not reduce what the device must settle — ABI shapes, whether a write did anything, audio itself — but it moves the "did the app do the right thing given that answer" half off the critical path, and that half is where the fourteen findings of 2026-08-23 came from.
There is no tracked list of "claims awaiting hardware confirmation" separate from the feature matrix, so unverified claims and verified ones sit in the same tables and are distinguished only by prose.
Four of the fourteen defects fixed in the 2026-08-23 audits are the same bug, and it has a name
worth adopting: an assertion about somebody else's state, written as if it were our own
preference. A fifth instance — SetSelectUsingEq, uncalled for months — was found earlier and is
listed with them because it is the clearest example of the class.
| Defect | What was assumed | Reality |
|---|---|---|
BT switch (cinder_set_bt_on) |
one boot read is the answer forever | the radio answers -1/0 while coming up; nothing ever re-asked |
| BT pairing table | populated when a screen needs it | bt_reconnect_tick needs it at boot, when nothing had read it |
| BT connect-wait cache | starts false |
the state is sticky in the service and outlives the process |
| DSP boot reconcile | "no settings file → nothing to push" | the DSP holds what the stock player left |
SetSelectUsingEq |
not called at all, for months | the device defaults to a tone system Cinder does not expose |
Each was written by someone who had reasoned carefully about the local case. The shared blind spot is in the heading: Cinder is a guest process on a device whose services outlive it, boot before it, and are also written to by stock software — but the code repeatedly assumes that what it learned at startup stays true, and that what it never set is unset.
No convention exists to catch this. bt_service_retry reconciles against a getter and explains
why in a comment; the function immediately below it does not. The correct pattern is present in the
codebase, adjacent to violations of it, and nothing propagates it.
2026-08-24 — now at least detectable. A convention is still the right fix (Part F item 6), but the class is no longer invisible to automation:
cinder-home/harness/boots the realmain.cppagainst fake Sony services and asserts on the call trace, and every row in the table above is a question about that trace. Thebt-late-servicescenario makes the factory fail four times and then work — the exact shape of the first two rows — and fails if the app stops asking. The catch is that a scenario has to be written: the harness proves a known assumption is still handled, it does not find the next unexamined one.
The project learned this expensively with high gain — the mixer control accepted the write, read
back 1, persisted across reboots, and the codec ignored it because the A50 output stage lacks the
hardware. STATUS.md records the lesson in capitals.
DSEE AI is in exactly that position right now, and shipped for months drawn identically to the
toggles that work, despite analysis/RE_dsp_effects_surface.md and cinder_effects.h both saying
"UNVERIFIED — treat like high gain until heard". The 2026-08-23 audit changed the label; it did not
settle the question, which needs an ear test.
There is no marker convention (a type, a naming rule, a lint) distinguishing verified-audible from the-call-returns-0. The distinction lives only in prose that the UI layer does not read.
Excluding the design handoff dump, the repo carries ~14.5k lines of Markdown against ~55k lines of code — a ~26% doc-to-code ratio, which is exceptional. The problem is not volume, it is decay without a decay process:
cinder-home/STATUS.mdis 1,628 lines and opens by saying its own matrix "was last re-audited 2026-07-30 and several of its entries are now stale; the ones proven wrong have been struck through in place."CLAUDE.md(582 lines) opens with a banner explaining that its own premise is historical.- Four
AUDIT_*.mdfiles, twoPLAN_*.md,ROADMAP.md,PRODUCTION_READINESS.md,COMPARISON_*.md— each a point-in-time snapshot, none with an expiry or an owner.
At least four files claim to be the single source of truth for overlapping questions
(STATUS.md "the single source of truth" for feature state; ROADMAP.md for the plan;
AUDIT_2026-08-16.md "the current gap list"; PRODUCTION_READINESS.md for what is missing). A
reader cannot know which is current without reading all of them and comparing dates.
This is the sharpest contrast in the project. In-code comments routinely explain the measurement, the wrong hypothesis, the date it was disproved and the log line that settled it. The commit log for the same work reads:
bff3172 Update binary files for cinder-home and cinder-probe, and enhance playback sequence handling
8eeaf4a Update binary files for cinder-home and cinder-probe
9499e94 Unfinished patch.
78acf3f Refactor code structure for improved readability and maintainability
git log and git blame — the tools built for exactly the archaeology this project does
constantly — are close to useless here. The knowledge is real and durable; it is simply in the one
place that cannot be queried by "when did this change and why".
Code and docs cite task #21, task #26 (×4), task #31, task #40, task #46, task #56,
task #59, (#1), (#4), (#25), (#55), (#58), (#63).
The repository has zero issues, open or closed. Every one of those references is unresolvable by anyone reading the repo — including its author in six months.
Partly closed, 2026-08-26. The six references in code — the ones a developer actually trips over — now point at documents that exist: every
task #26was the same thing (the render-loop poll storm behind the audio stutter) and now citesdocs/DEVICE_TESTS.md§7;task #59cites B2 above, which is precisely what it was warning about;task #21cites the Bluetooth RE findings.The remaining references live in historical write-ups (
analysis/*/RE_findings.md,docs/AUDIT_*,cinder-home/STATUS.md) and are deliberately left alone. Those files are a record of what was known on a given day; editing them to tidy a citation would falsify the record for no reader's benefit. They are dated, and the numbering is a maintainer's own task list that was never mirrored to GitHub Issues — which is the thing worth knowing, and is now written down here.
.github/workflows/ci.yml runs, on every push and PR:
cargo test --release+cargo build --releaseforplayer/andinstaller/- a payload-existence check on
cinder-home/dist/stable/* - a
file(1)check that the committed binaries are ARM
It does not run: any C/C++ compile, cinder-home/build.sh (and therefore none of the six C++
self-tests, the GLIBC ceiling gate, or the qemu preflight), any shell lint, the 44-case launcher
recovery matrix, cargo clippy, or cargo fmt --check.
The ARM cross-build being absent is documented and defensible — it needs a glibc-2.23 +
libc++-3.9.0 toolchain that would be real machinery to maintain on a hosted runner. But the C++
self-tests do not need that toolchain. They are host-compiled with plain cc and run in
milliseconds. They are skipped not by necessity but because they live inside a script CI never
calls.
No rustfmt.toml, no clippy.toml, no .editorconfig, and neither tool runs in CI. On a codebase
this comment-dense, formatting drift is a live merge-conflict source.
- 58 commits on
main; 2 are merge commits. ~56 changes went straight tomain. - Three PRs have ever existed. PR #2 was created at
20:56:52and merged at20:57:03— eleven seconds, which is less than the CI run it was supposed to gate. - No PR template, no
CODEOWNERS, no review requirement in evidence.
The direct-push pattern is conclusive evidence that main accepts pushes without a PR. (Whether
branch protection is configured and simply permits it, I could not read — that needs repo admin
access.)
This matters more than usual here, because a bad main is not an inconvenience: main is what
tools/release.sh tags, and a release flashes a device with no public recovery path.
tools/release.sh does the right thing thoroughly: refuses a dirty tree, checks the installer
version against the tag, rebuilds the ARM payload from source and refuses to tag unless every
committed byte matches. This is a genuinely well-designed guard against the project's scariest
silent failure — shipping stale device binaries.
And nothing requires its use. release.yml triggers on push: tags: ["v*"]. A plain
git tag v0.1.4 && git push --tags bypasses every check above and publishes. The workflow's own
payload step verifies only that the files exist and are ARM — never that they match the source.
release.yml's header states the hazard plainly ("BUILD AND COMMIT dist/ BEFORE TAGGING, or the
release ships whatever was last committed"). It is a known, documented, unenforced risk. Four
releases have shipped under it.
Closed by a manifest, which is the shape the 2026-08-26 note worked out. The runner still cannot re-run the comparison — it has no glibc-2.23 cross toolchain, which is the entire reason
dist/is committed — so insteadtools/release.shnow records what it verified: the sha256 of every payload file, plus the tag it was verified for.release.ymlchecks that record still describes the tree before anything is published.The tag line is the part that closes the hole. A release cut with
git tag && git push --tags, skipping the script entirely, finds a manifest naming the previous version and fails by name. A payload edited after verification fails on its hash.Via
tools/verify_payload_manifest.sh, run by both CI and a contributor — the rule this repo already learned when an inlined linter step went red on its first run. Tested against all five ways it should fail: wrong tag, tampered payload, missing manifest, manifest with no hashes, and the happy path.It is not a signature and does not pretend to be — anyone who can push can rewrite it. It closes the accident: the forgotten step, the stale payload, which is the failure this project has actually had. Provenance proper is D7.
| File | Size | Commits touching it |
|---|---|---|
cinder-home/cinder-home.unstripped |
6.1 MB | 36 |
cinder-home/cinder-probe.unstripped |
6.1 MB | ~36 |
dist/stable/cinder-home |
3.5 MB | 17 |
dist/stable/cinder-probe |
3.6 MB | 17 |
.git is 83 MB for a 1,706-file source repository. Committing dist/ is a deliberate,
reasoned choice (the cross toolchain is not reproducible on a runner) and is defensible. Committing
the 6 MB .unstripped debug artifacts 36 times over is not — they are build byproducts, not
deliverables, and they are the largest single contributor.
This is unfixable without history rewriting, which is why it belongs in a permanent-record document rather than a to-do list.
2026-09-01 — the numbers were badly out of date, and the bleeding was worse than recorded.
.gitis 1.3 GB, not 83 MB.cinder-home/dist/alone accounts for 863 MB of it — 66% of the repository — across 117 revisions of ~3.5 MB stripped ARM ELFs, which share no deltas, so every build rewrites each one in full. Agit cloneof a ~60,000-line project transfers 1.3 GB, and that is the first thing anyone evaluating this repository experiences.
dist/dev/is now untracked (git rm --cached, files left on disk, gitignored). That is 74 of the 117 revisions and it is not what anyone installs — build it withbuild.sh devwhen needed, and an installer compiled without it names the two commands that rebuild it. The growth stops there; the existing 863 MB does not come back without a rewrite, and the judgement in "Deliberately not recommended" below still stands, but it was made against 83 MB and should be re-made against 1.3 GB by whoever owns the repo. SeeAUDIT_2026-09-01.md§D1 for the three options.2026-09-11 — and that 1.3 GB was a local
.gitfull of unpacked objects. A clone from GitHub is about 124 MB. The rewrite is now prepared and rehearsed:HISTORY_REWRITE.md.
Absent: CONTRIBUTING.md, CODEOWNERS, SECURITY.md, issue templates, PR template,
dependabot.yml, .editorconfig.
2026-09-01 — mostly closed. GitHub's own community profile read 71%. Added:
CODE_OF_CONDUCT.md, three issue templates, a PR template with a blast-radius section separating "UI, cannot brick anything" from "runs as root / boot path / Sony IPC / USB-MSC ordering",dependabot.yml,CHANGELOG.md, anddocs/README.mdas an index that says which documents are history rather than current state (which is C1's real fix).One of the three issue templates is not standard furniture and is the point of the exercise:
device_report.yml, for reporting aDEVICE_CHECKLIST.mditem run on real hardware, pass or fail. A5 is the bottleneck this whole document keeps returning to — verification needs a device and there is one — and there was no route for a stranger who owns an A50 to contribute one.CODEOWNERSand.editorconfigremain absent; both are low value for a single maintainer.
SECURITY.md is not box-ticking here. This project ships eight setuid-root binaries to a
device, distributes an unsigned Windows executable that drives a firmware flasher, and has no
stated way to report a vulnerability in any of it.
The release attaches SHA256SUMS and the body explains how to check it — good, and better than
most hobby projects manage. But:
- the installer is unsigned, so SmartScreen warns and users are conditioned to click through;
- there is no build provenance/attestation, so the checksum proves the download matches what the workflow produced, not that the workflow built the source it claims;
- the ARM payload inside is committed blobs, so even a perfect installer build says nothing about whether those bytes came from this source tree (see D4).
For software that flashes a device with no recovery path, that is the weakest link in the chain.
2026-09-01 — the third bullet is closed, the first two are not. D4's manifest means the committed ARM payload is now tied to a verified rebuild and to the tag it was verified for, so a release does say something about where those bytes came from. Separately, nothing had ever looked at the dependency tree — 122 crates reaching the binary that runs as the device's Home app — so
ci.ymlgained acargo auditjob with a weekly cron (0 vulnerabilities today).Signing and build attestation are still absent, and they are the two that would actually make the installer trustworthy to someone who does not know the maintainer. Both cost money or a non-trivial workflow change, so they stay recorded rather than pretended-at.
2026-09-14 — the second bullet is closed, in the workflow; unproven until a tag runs it.
release.yml's release job now runsactions/attest-build-provenanceoverSHA256SUMS, so every published file (both installers and both.UPGs) gets a Sigstore-signed provenance statement naming the workflow, the repository and the commit.gh attestation verify <file> -R superwilso/Cinderchecks it;SECURITY.mdand the release notes say how. It is free for a public repository, which is why it came before signing. Signing is still open, and so is the part no attestation can close: the ARM payload is committed, so provenance for those bytes still rests on D4's manifest.
Stated plainly, because the weaknesses above are only survivable because of these.
- The comments are the best artifact in the project. They record the wrong hypothesis, the
measurement that killed it, and the date.
cinder-msc.c's header — explaining that MSC "never was a race" and naming both root-only steps — would have saved weeks if written earlier, and will save them for the next reader. - Safety engineering is taken seriously and is layered. Bad-boot counter → auto-revert → crash
supervisor → kill switch → wbrt restore, each depending on strictly less than the layer above.
RECOVERY.mdexists and is honest about there being no DFU path. run_guarded. Every Sony IPC call runs behind crash+hang recovery, so a bad service degrades a feature instead of bricking the boot. This is the single best design decision in the codebase.- Pure-logic extraction for testability.
bt_edge.h,jack_edge.h,vol_ramp.h,db_sig.h,bt_switch.h,sound::signal_path— small rules pulled out where a host test can reach them. The pattern is right; it needs a CI hook (D1) and wider application (A2). - The UI overflow matrix. 22 screens × 2 content sets × 2 themes × 7 UI scales, which found five
real defects that had no visible symptom because
Canvasclips silently. - Honesty about negative results. High gain was removed when measurement disproved it, and the removal is documented so it is not re-added. That is rarer and more valuable than most features.
Items 1, 2 and 3 are done; 9 is half done.
ci.ymlgained anativejob runningtools/host_syntax_check.sh(18 C/C++ files), the six C++ self-tests,bash -nandshellcheck -S warningover all 33 scripts — plus a clippy gate scoped tocorrectness+suspiciouson the Rust jobs. That closes A3, A4 and D2, and closes A1/D1 apart from the ARM link, the GLIBC ceiling gate and the qemu preflight, which genuinely need the cross toolchain.The same change removed a duplicate trigger (
push: ["**"]andpull_requestboth fired on every PR branch), so CI now covers ~24,700 more lines while running fewer jobs than before — 8 per push down to 5.Two things worth recording from doing it: the syntax check found a real latent bug on its first run (
probe.cppusinguintptr_tin seven places with no<cstdint>), and the four shellcheck findings were fixed rather than silenced — one of which, a hardcoded personal Windows path inflash.sh, had no business in a public repo.
cargo fmt --check(the other half of 9) was deliberately not added:fmtfails on both workspaces today, so the gate would be red on arrival. It needs a formatting commit first, and that is a separate decision on a comment-dense tree.The gate went red on its own first run, and the reason is worth keeping. The steps were inlined in
ci.ymland verified locally by hand; the local shellcheck was 0.11.0 and the runner's was older, and the two disagree about#!/system/xbin/busybox sh. So the fix was not just the finding — CI and a contributor now run the same script (tools/shell_check.sh,tools/host_syntax_check.sh) against a pinned linter. A gate whose version floats is a gate that can turn red with no source change: the same fragility class as the<cstdint>bug above.2026-08-24 — the gate got stricter.
tools/host_syntax_check.shnow runs-Wall -Wextra -Werror, in both build channels. The C/C++ had never been compiled with warnings on; the first run produced seven across 19,435 lines and all seven were fixed rather than tolerated. The gate then immediately caught two more that a manual sweep had missed. Deep sweep and its nine clean areas:AUDIT_2026-08-24_deep_sweep.md.2026-08-24 — the app now BOOTS in CI. The deep sweep ended by saying static analysis had reached its limit and that the remaining defect class needed a fake-service harness. That harness exists:
cinder-home/harness/links the realmain.cppagainst faked Sony service clients, a faked easel framework and a virtual clock, runs the appmgr lifecycle, and asserts on the resulting call trace. Five scenarios, twenty assertions, about two seconds including the build — because sleeping advances a counter instead of waiting, so two virtual minutes of device time costs milliseconds.This is the first automated check that can see B1 at all. A test of
bt_switch.hsays the reconcile rule is right; the harness says the app runs it, during boot, and keeps running it when the service was not there the first time. Three of the five scenarios are direct regression tests for defects already shipped and fixed.It also found a real error immediately — in itself. The hand-written slot map had
AddListener's two indices swapped between the two Bluetooth clients, so a bring-up step that worked was reported as missing. That map is now generated frommain.cpp's own call sites: a harness has to be harder to be wrong about than the thing it checks.What it still does not cover: the ABI (the fakes agree with the RE notes, so where those are wrong it is wrong with them), the ARM link and GLIBC ceiling, UI input (touch comes from
/dev/input, which does not exist off-device), anddlopened services, which take their degraded branch. A2 is untouched —cinder-audio's shims are behind the stub boundary, so the harness exercises the app's use of them, not the shims themselves.2026-08-24, end of day — where the harness got to. Twenty scenarios, 74 assertions, nine seconds including the build, and it now also runs from
build.sh, so it gates a flash and not just a push. Beyond the bring-up and pacing work above it grew a fake device filesystem (fopen,openandstatserved from a private tree, with files that can CHANGE part way through a run) and fake input (/dev/input/event*as real FIFOs, so touch and buttons reach the app the way the driver delivers them). That closed the last two "nothing checks this at all" surfaces:
- hardware edges — headphones out mid-track pauses within 496 ms; a PC appearing hands the volume over once and takes it back when the cable goes; auto power-off fires when idle and does not fire while playing or on a charger;
- input — a dark panel wakes on touch without also pressing what was under the finger; a tap is a tap and a drag is a drag; raw evdev codes decode to the right buttons; the volume rocker accelerates, stops on release, and gives up on a stuck key.
Seven defects in total, all one shape — work on a timer, for a condition that cannot change, that nobody stops — and the pacing rule now lives in one place (
retry_log) rather than seven.What it still cannot see is written down in
cinder-home/harness/README.mdand again inDEVICE_CHECKLIST.md: the ABI, the ARM link and GLIBC ceiling,alarm()and the guard budgets,dlopened services, the navigator's own decisions, A2'scinder-audioshims, and — the one worth repeating —system/popenare recording stubs, so every setuid helper "fails" there and the SUCCESS paths of MSC and power-off have no coverage at all.Still open: 4, 5, 6, 7, 8, 10.
4 and 10 are done; 5 went further; D3, D6 and half of D7 came with them. Details per item in the table below, but the shape of the day is worth recording, because two of the four things fixed were caused by the other two.
Item 10 turned out to be the load-bearing one, and not for the reason it was listed. It sat last as "worth doing when a second contributor appears". Then
mainwent red twice in one day —15b0fb8pushed straight tomain, and a dependabot PR merged over a failing check — and the second one left the tree not compiling for an hour. CI ran both times, was correct both times, and was not required. Branch protection now requires all six checks.The wider lesson is in
AUDIT_2026-09-01.md§A5 and it generalises past this repo: an automated gate that proposes changes must be scoped to what the automated gates can check. The dependabot config added that morning had no version limits, so its first run proposed three semver-major bumps straight through the device's decode path — includingrusqlite, whose bundled SQLite C is the one thingbuild.sh's glibc-2.23 ceiling exists to police and which no runner can check. It is scoped now: majors are ignored for every crate the device links.Item 4 needed the 2026-08-26 note's design, not a tweak — a manifest written by
release.shand verified byrelease.yml, so a bypassedgit tagfails on a stale tag line. Done and tested against all five failure modes.§A2 got its first test ever, and it found a live defect (see the note under A2): the EQ shim forwarded out-of-range band gains that the DSP zeroes rather than clamps, reachable from a settings file on a PC-writable vfat volume.
Still open: 6◐, 8◐, 9◐, and A2's IPC surface. 9 is still blocked on the same thing it always was —
cargo fmtfails on both workspaces, so the gate would be red on arrival, and a formatting commit across a comment-dense tree is a decision rather than a chore.
| # | Action | Cost | Why this order |
|---|---|---|---|
| 1 | ✅ Call cinder-home/build.sh's self-tests from CI. Extract the six cc-compiled self-tests into a selftests job (or a build.sh --host-tests-only flag). No cross toolchain needed. |
~1 h | Turns six existing, written, passing gates from opt-in into enforced. Highest ratio in the table. |
| 2 | ✅ Add a bash -n + shellcheck job over the 33 scripts. |
~1 h | 5,288 lines of root-privileged, boot-path shell currently has no syntax gate at all. |
| 3 | ✅ Compile-check the C++ on the host — cinder-audio + cinder-home/src against stub headers, -fsyntax-only if linking is impractical. |
~half day | Would have caught this session's C++ edits, which shipped uncompiled. Closes the worst of A1. |
| 4 | ✅ Make release.sh the only way to release — done 2026-09-01 via the manifest below. Original note: — NOTE 2026-08-26: a runner cannot re-run the real comparison, because it has no glibc-2.23 cross toolchain. The workable shape is a manifest (payload hashes) written by release.sh and verified by release.yml, so a bypassed git tag leaves a stale manifest and fails. That is a design change, not a tweak.: have release.yml re-run the payload-vs-source comparison rather than an existence check. |
~2 h | The guard already exists and is correct; it is simply bypassable. Protects the flash path. |
| 5 | ✅ Stop committing *.unstripped. Done 2026-08-26: gitignored and git rm --cacheded. The local copies stay, so addr2line on a crash address still works. History untouched, deliberately. |
10 min | Stops the bleeding on D5. Does not fix history, and should not try to. |
| 6 | ◐ Adopt a "service state" convention for B1 — 2026-08-26: the convention is now written down in CONTRIBUTING.md (apply_ = push intent, refresh_ = read fact, reconcile_ = assert intent still holds, idempotent). The tree already follows the first two; nothing has been retrofitted to reconcile_, deliberately — a 30-function rename is churn, and new code is where the rule pays. — a naming rule or a helper (reconcile_* vs apply_*) that makes "assertion about a service" visually distinct from "push a preference". |
~half day | Five defects in one audit came from this. A convention is cheaper than finding the sixth. |
| 7 | ✅ Add SECURITY.md + CONTRIBUTING.md. Done 2026-08-26. Scope, out-of-scope, a private-advisory route, and the four rules-from-incidents; CONTRIBUTING covers the local gates, the harness and its limits, and the device rules. |
~1 h | Twelve setuid binaries and an unsigned flasher, with no disclosure route. |
| 8 | ◐ Either use the issue tracker or stop citing it. 2026-08-26: the six code references now cite real documents; the rest are in dated historical write-ups and are left as record (see C3). | ~2 h | Cheap, and it makes the excellent comments navigable. |
| 9 | ◐ Add cargo clippy (done, scoped to correctness + suspicious) + cargo fmt --check (NOT done — fmt fails on both workspaces today, so the gate would be red on arrival; it needs a formatting commit first). |
~30 min | Low value against the rest, listed for completeness. |
| 10 | ✅ Require PRs on main. Done 2026-09-01 — branch protection requires all six ci checks, force-pushes and deletions off; enforce_admins and required reviews deliberately left off, because this should stop a RED merge, not require a second person who does not exist. |
~10 min | main went red twice in one day: one push straight to main, one PR merged over a failing check. The reason to require checks was never a second contributor; it is that a gate nobody is obliged to obey protects nothing. |
- Rewriting history to purge the blobs. 83 MB is annoying, not harmful, and a rewrite breaks every existing clone and release reference.
- Reproducing the ARM cross toolchain in CI. The existing reasoning is sound; item 3 gets most of the benefit for a fraction of the cost.
- Consolidating the Markdown into one file. The sprawl is a symptom of a real method (write down what was measured, when). A dated index with an owner per document fixes C1 better than a merge would.