Commit Graph
23 Commits
Author SHA1 Message Date
BFE Engineering 249c311953 ci: self-hosted code quality grade and badge
A new quality job runs the linter battery (clippy, cppcheck, shellcheck,
ruff, lizard, jscpd, cargo-audit per crate, scc for the LOC denominator)
and feeds tools/quality/score.py: findings convert to SQALE remediation
minutes, debt ratio grades A-F on SonarQube's published grid, and a
separate worst-of security axis can only drag the overall grade down.
The job uploads the full quality.json breakdown and fails when the
security grade is worse than C.

The badges job now renders all four SVGs in-runner with anybadge
(shields hex palette); the previous img.shields.io curls were the one
external-service dependency left in the pipeline. quality.svg is seeded
at the current locally-computed grade (A, debt ratio 0.42%).

Also fixes the two ruff findings the battery surfaced in flowgen.py.
2026-08-31 16:18:38 -06:00
BFE Engineering 53bc57fbf8 ci: unbreak workflow YAML (colon in unquoted step name)
The style pass rewrote an em dash as a colon inside an unquoted name:
scalar, which YAML reads as a nested mapping. Comma instead; file
re-validated with a YAML parse.
2026-08-31 13:09:42 -06:00
BFE Engineering 42fb386f60 docs: ASCII typography and style normalization across all repo text
Replace every em dash with real punctuation (rewrites, not hyphen swaps)
in docs, code comments, scripts, configs, and the port records; convert
en dashes, curly quotes, ellipsis glyphs, arrows, and section signs to
ASCII; drop machine-writing tell phrases from living docs. ADR titles
now use a colon. The M2 bring-up DTS model string carried an em dash
into the patch series and its record echoes; fixed at both, and the full
series re-verified to apply cleanly onto pristine 6.18.46. One comment
in freshness.h deliberately names the em dash glyph the UI renders as
the unknown mark; that is data, kept as prose naming it.

Verified: cargo tests (sim, config-lint, rs485-bridge), shellcheck,
both driver MC/DC harnesses, patches-apply.
2026-08-31 12:31:27 -06:00
BFE Engineering ed62a7d099 Merge remote-tracking branch 'origin/main' into docs-reposition 2026-08-31 08:00:33 -06:00
noah fbd672c591 Merge pull request #7 from blueflare-energy/btlpm-618-modules
Matched 6.18 module set from the hermetic build (issue #4)
2026-08-31 09:58:57 -04:00
BFE Engineering d465d97b99 docs: repo renamed to bfe-core1106-sdk
The last product-first artifact after ADR-0008 was the repo name itself.
Current and forward-looking references updated; period-bound history in
the ADRs keeps the old name, with the rename recorded in ADR-0008.
Crate names, WARDEN_* env vars, and binaries are unchanged.
2026-08-31 07:17:00 -06:00
BFE EngineeringandClaude Fable 5 c258a2da68 build: produce the matched 6.18 module set (issue #4 root cause)
The hermetic build only ever made zImage+dtb — no 6.18 .ko set existed,
so the panel's only aic8800_btlpm.ko was the stale 5.10 build, which
fails the struct-module ABI check at load. New WARDEN_MODULES_COLLECT
env runs the full `make modules` (required: zImage alone emits no
Module.symvers, so per-directory M= builds cannot link) and collects the
listed dirs' modules fail-closed; CI's kernel-build collects
drivers/net/wireless/aic8800 and ships the .ko files in the artifact.

Verified: all three aic8800 modules (bsp 326K, fdrv 1.5M, btlpm 36K)
compile clean against 6.18, and bsp+btlpm INSMOD WITH RC=0 on the 6.18
kernel in the qemu/ device sim — no linkonce/this_module ABI error;
btlpm's rfkill init runs and only hardware power-up fails (no AIC silicon
on virt, expected). On-panel BT bring-up remains bench-gated (c8a3
currently dark).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HUayid7W5w7jBdb9Rrj1K
2026-08-30 09:56:43 -06:00
BFE EngineeringandClaude Fable 5 1d7dec167e clock diagnostics for issue #3: probe, VM sanity scenario, fix plan
The qemu/ device sim answered the first question off-board: the same
kernel family under -M virt gives musl vDSO rate ratio 0.99963 — the
generic 6.18 armv7 vDSO is correct, so the board symptom is RV1106
register state (CNTFRQ/CNTVOFF, firmware-owned, secure-world boot chain).

- qemu/tests/clockprobe: interval-based musl probe separating RATE error
  (CNTFRQ) from boot OFFSET (CNTVOFF) — the original single absolute
  sample cannot distinguish them.
- qemu/tests/clock-sanity.sh: VM regression guard asserting the vDSO rate
  within 1% (PASSES: 1.00026); cross-builds the probe and stages it as
  payload itself.
- kernel/rv1106-enablement/timer/PLAN.md: the DT fix
  (arm,cpu-registers-not-fw-configured + measured clock-frequency on the
  board dts) gated on the two bench measurements; c8a3 is currently
  physically dark, needs hands at the bench.
- clockprobe joins the CI test loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HUayid7W5w7jBdb9Rrj1K
2026-08-30 09:49:50 -06:00
BFE EngineeringandClaude Fable 5 973a414f07 review: iteration-2 fixes (fragment assertion, evidence paths, hardening)
- build-kernel.sh fragment assertion: survives a missing trailing newline
  (the read footgun, reproduced) and treats an absent symbol on a disable
  line as FATAL, symmetric with the enable arm.
- fetch-kernel-tarball.sh checks the pin before downloading; both fetchers
  add --retry-connrefused.
- mkimage rejects '.'/'..' state keys.
- ui-shot: VM liveness checked before every QMP call, console.log preserved
  as evidence on every failure path, repaint deadline widened to 90s with
  the contended-runner rationale documented.
- rs485-bridge: overflow discards back off one gap and rate-limit their log
  line, mirroring the accept-loop fix; clippy nit fixed.
- .gitignore ignores *.elf/*.map so the untracked artifacts cannot silently
  return; CI shellcheck glob now covers build/ and the rootfs boot scripts
  (directives added for the deliberate in-guest source paths).
- Docs: NPU parity row matches its sibling verification docs; line-pinned
  audit cross-references unpinned; CROSS_COMPILE documented in the build
  header; payload README lists warden-ui; ci-cd tense settled.

Verified: guards negative-tested (bad state keys, no-newline fragment);
boot smoke, portal scenario, ui-shot all PASS; 53 tests green; shellcheck
clean across the widened glob; clippy zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HUayid7W5w7jBdb9Rrj1K
2026-08-30 08:34:47 -06:00
BFE EngineeringandClaude Fable 5 2756de0b46 review: iteration-1 fixes across CI, bridge, VM harness, and docs
CI/pipeline:
- KERNEL_TARBALL passed as a YAML env literal '~' was never tilde-expanded
  and would have failed every hosted kernel-build dispatch; the path is now
  exported from the shell. Verified reproducible before the fix.
- Every job gets timeout-minutes; boot smoke uses timeout -k so a wedged
  qemu is SIGKILLed instead of holding the job.
- Tarball fetch + fail-closed sha256 verification deduplicated into
  build/fetch-kernel-tarball.sh (with curl retries), used by build-kernel.sh
  and both CI jobs. busybox fetch gains retries too.
- ccache layer for kernel-build (cache keyed on defconfig+patches) recovers
  the incremental-compile speed the ephemeral-runner move cost.
- build-kernel.sh now asserts every fragment option survived olddefconfig —
  merge_config -m pastes text and Kconfig silently drops unmet symbols.

rs485-bridge:
- pending-buffer cap (2x max RTU ADU) instead of unbounded growth;
  explicit accept-loop error handling with backoff instead of .flatten();
  per-arm inline bounds instead of the string-keyed lookup whose default
  would have mis-bounded a future get-input; control-socket cleanup errors
  surfaced; flag-shaped values rejected in arg parsing; doc example uses a
  private mktemp dir. Test timing margins widened for contended runners
  (gap 25->120ms, 60x margin on the split-frame test).

VM harness:
- stage-1/stage-2 boot scripts share one validated slot parser and one
  by-name populator (qemu/rootfs/etc/warden-lib.sh) — the duplicated
  parser had already diverged on validation; userdata/oem mount failures
  now fail fast with a greppable sentinel; udhcpc fallback keys off the
  interface actually having an address; switch_root applet guarded.
- boot-smoke delegates the qemu invocation to run.sh (machine shape lives
  in ONE place); run.sh port 0 disables a hostfwd.
- mkimage: unknown partition names fail at build time; DISK_END is a max,
  not last-entry; --state keys validated as filenames.
- portal-scenario: mock readiness is asserted (no silent fall-through),
  hostfwd port collisions retried, mount-failure sentinel fails fast.
- ui-shot: fixed sleeps replaced with bounded screendump polling; the
  repaint assertion is real and documented as such. qmp.py loses its
  module-global and gains argv validation.

Docs/scrub: bench-host paths and the site AP name removed from six more
port docs and two evidence tables; path-bearing build artifacts (.elf,
.map) untracked (the 154-byte firmware .bin is path-free and stays);
ADR-0003 marked visibility-superseded by ADR-0007; stale section
cross-reference fixed; flare-edge noted as private for outside readers;
stale root-level review report removed per the new workspace rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HUayid7W5w7jBdb9Rrj1K
2026-08-30 08:19:17 -06:00
BFE EngineeringandClaude Fable 5 5155224594 public-readiness: scrub internal details, split licensing, hosted kernel-build
Preparing the repo to go public (ADR-0007):

- CI: kernel-build moves from the self-hosted runner to ubuntu-latest
  (installs its own cross toolchain + qemu, caches the pristine tarball).
  On a public repo a registered self-hosted runner is reachable from
  approved fork-PR workflows — i.e. arbitrary code on private
  infrastructure — and the build never actually needed the SDK host.
  ADR-0004 marked superseded-in-part; docs/ci-cd.md rewritten (site
  specifics now live only in the private deployment log).
- Licensing: LICENSE gains the GPL-2.0 carve-out for patches/ and the
  kernel source excerpts (Linux derivatives; per-driver provenance was
  already tracked in PROVENANCE.md); patches/README.md states it too.
- Scrubbed from the tip: bench-unit dev credentials and its gadget IP
  (m2-boot notes), the site AP SSID+BSSID and a neighboring AP's BSSID
  and the device WLAN MAC (wifi bring-up evidence — BSSIDs are
  geolocatable), the runner mesh IP. NOTE: these remain in git history;
  decision on a pre-publication history rewrite is separate.
- Emoji cleanup across 21 tracked files (kernel port docs, review report,
  enforce-mcdc.sh) per repo text conventions: status marks became
  [x]/[wip]/[ ]/OK plain text.
- "[maintainer]-gated" process phrasing normalized to "maintainer-gated"
  (attributions in dated evidence docs kept).

Verified: zero emojis tracked; scrub grep clean; patches carry no internal
references; ci.yml parses; shellcheck unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HUayid7W5w7jBdb9Rrj1K
2026-08-30 07:25:57 -06:00
BFE EngineeringandClaude Fable 5 c7e06514ad qemu: RS485 bridge to sim, portal E2E scenario, CI wiring
Phase 3 of the device sim, all verified on QEMU 10.0.11:

- qemu/rs485-bridge/: std-only crate bridging a QEMU serial chardev (unix
  socket) to warden_sim::ModbusSlave — gap-based RTU framing (CRC failures
  degrade to real-slave silence), line-protocol control socket for register
  seeding and fault injection (drop/exception/clear), bounds-checked so a
  scenario typo answers err instead of panicking the bus. 7 unit tests,
  bench in the sim_bench pattern. Verified end-to-end: guest master frame
  on /dev/ttyS4 (pci-serial) answered from the sim slave, CRC-correct.
- virt machine gains highmem=off: the 32-bit non-LPAE kernel cannot reach
  virt's default 40-bit PCIe ECAM (pci-host-generic EOVERFLOW); with it the
  full PCI set probes (16550A ttyS0, i6300esb).
- Watchdog scenario verified: guest arms /dev/watchdog, no petting, i6300esb
  resets the VM ~30s later (first environment where this arm is testable).
- qemu/tests/portal-scenario.sh: the real static-musl warden-flared inside
  the VM against flare-edge's mock portal on the host — authenticated
  check-in, firmware desired-state pull, and download of a real signed
  tier-1 .wfw offer, asserted from the portal log. Found and filed
  flare-edge#106 (fatal SIGBUS in the HPMCU boot-loaded probe on
  non-RV1106 memory maps); runs against a flared built from the
  qemu-vm-support fix branch.
- stage-2 init: WARDEN_FLARE_INSECURE=1 + WARDEN_HPMCU=0 (documented VM
  deviations), firmware-version stamp, newline-terminated state seeds.
- CI: rs485-bridge joins the test loop and bench job; new qemu-tools job
  (shellcheck + initramfs + disk image on hosted runners); kernel-build
  gains a fail-closed qemu boot-smoke step. All qemu scripts shellcheck-clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HUayid7W5w7jBdb9Rrj1K
2026-08-29 19:59:44 -06:00
BFE Engineering 0833d61bfa ci: automatic artifact GC for kernel builds (account-wide storage)
Mirror flare-edge's artifact GC so the shared Actions storage quota can't fill
from either repo. A new prune-artifacts job (ubuntu, gh + built-in token,
actions:write) runs before the kernel-build upload and deletes older
kernel-rv1106 artifacts beyond the newest 3; kernel-build needs it. retention-days
14 -> 5 as the backstop. Best-effort so it never blocks a build. Completes the
account-wide auto-clear (an over-quota once blocked ALL runs, not just uploads).
2026-08-26 10:44:48 -06:00
BFE EngineeringandClaude Opus 4.8 a804a35e08 ci: kernel-build artifact upload is best-effort (storage-quota resilience)
The kernel build itself is the gate; uploading the zImage/dtb can fail on an
account-wide GitHub artifact-storage-quota hit unrelated to the build. Mark the
upload continue-on-error so a good build isn't red-X'd by it. Outputs also remain
on the self-hosted runner host.

Verified: kernel-build compiled the RV1106 6.18.46 kernel end-to-end on
bfe-mpc-0640 (zImage 8.25 MB + rv1106-warden.dtb 37 KB); the patch series applied
(issue #1 fix confirmed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
2026-08-25 17:16:36 -06:00
BFE EngineeringandClaude Opus 4.8 5e1b02ba60 kernel-build: apply patches outside the checkout + fail-loud sentinel (#1)
The kernel-build job set WORK inside the repo checkout, so build-kernel.sh's
`git apply` ran from a subdirectory of the warden-sdk git repo — where git apply
silently ignores out-of-subdir paths and exits 0 without applying anything. The
build then failed at the (never-created) rv1106-warden.dtb target, and the
unconditional `echo "applied"` had masked it.

- ci.yml: WORK -> ${{ runner.temp }}/kbuild-out (outside the checkout); artifact
  paths follow.
- build-kernel.sh: the patch loop now fails loudly instead of echoing on silent
  failure, and a post-apply sentinel assert (rv1106-warden.dts must exist) makes a
  silent no-op impossible to ship — the regression guard for this class.

Refs #1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
2026-08-25 16:55:46 -06:00
BFE EngineeringandClaude Opus 4.8 5a9432b8bc ci: self-provision python + cross toolchain in kernel-build job
Make the self-hosted kernel-build runnable with zero manual toolchain/python setup
on the runner host (docs/ci-cd.md steps 3-4 move into the workflow):

- build/build-kernel.sh: honor a caller-provided CROSS_COMPILE (default stays the
  Luckfox uclibc prefix). The kernel is freestanding, so a generic arm cross
  compiler links it.
- kernel-build job: set CROSS_COMPILE=arm-linux-gnueabihf- (Debian
  gcc-arm-linux-gnueabihf, already on 0640) instead of depending on the ephemeral
  SDK checkout path; symlink python->python3 into $RUNNER_TEMP/bin on $GITHUB_PATH
  for the bare-`python` SDK quirk.
- docs/ci-cd.md: only steps 1-2 (systemd service + cgroup cap) still need 0640 sudo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
2026-08-25 16:27:21 -06:00
BFE EngineeringandClaude Opus 4.8 4f060db1b1 ci: make main the canonical branch (badges guard, docs)
Point the badges job's branch guard at refs/heads/main and update the README /
ci-cd docs to name main as the canonical branch, ahead of establishing main from
bringup as the SDK's first main commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
2026-08-25 16:18:36 -06:00
BFE EngineeringandClaude Opus 4.8 4ee4dfcf88 review: fix review findings (correctness, hardening, doc-accuracy)
First recursive code-review-harness pass over the authored SDK code (sim/,
config-lint, flowgen, drivers/{relays,freshness}, build/, ci.yml). Four parallel
dimension reviewers; all findings at every severity corrected per workspace rule.

Correctness / reliability:
- freshness.c min_budget_ms: use a `seen` flag, not `best==0`, as the empty
  sentinel — a zero-tolerance (max_stale_ms==0) binding was silently widened to a
  looser neighbour's budget. Regression test added; still 66/66 MC/DC.
- config-lint parse_reserved_ranges: match `reg` as a whole property token (ident
  boundary before, `=` after) so `reg-names` / a `region-*` label no longer
  mis-parses into a bogus reserved range.
- config-lint loader check: fail closed — flag any loader with a LOAD_ADDR that is
  not a known-safe boot component, instead of only known MCU names, so a future
  coprocessor ("Rtos"/"Bl32") can't slip past the 0x40000-brick gate.
- build-kernel.sh: sha256 verification is now mandatory (refuse to build if the pin
  is missing) and the mktemp scratch tree is removed on exit (trap), while a
  caller-provided WORK is left intact for CI artifact upload.

Test quality:
- freshness: added the age==max_stale boundary case and a clock-wraparound
  (now < last_ok) fail-safe-to-UNKNOWN test.
- relays: unsetenv(WARDEN_GPIO_ROOT) at main() so the NULL-env arm is hermetic.

Security / CI:
- ci.yml: top-level `permissions: contents: read` (badges overrides to write);
  pin taiki-e/install-action to commit SHA (v2.86.7).

Maintainability / docs:
- drivers/enforce-mcdc.sh: one shared, name-derived gate replaces the two
  copy-pasted per-driver scripts; Makefiles call ../../enforce-mcdc.sh.
- docs/architecture.md: §3/§4/§6/§7 rewritten to match reality — NPU/RGA models,
  config-lint, and the relays+freshness MC/DC harnesses are done; kernel §6 now
  reflects the 5.10->6.18.46 forward-port (ADR-0001), not the superseded plan44/6.6.
- README: status blurb + layout table corrected (kernel/, .github/; stale ci/ and
  patches/ descriptions fixed). hpmcu "8 tests" -> 7 in docs.
- freshness.{c,h}: ADR reference points at flare-edge ADR-0004 (warden-sdk's
  ADR-0004 is the CI runner — number collision).
- normalize rustfmt drift across sim/ + config-lint.

All green: sim 37 tests, config-lint 8 tests, both drivers 100% MC/DC (relays
40/40, freshness 66/66), clippy clean under -D warnings, gitleaks clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
2026-08-25 15:48:52 -06:00
BFE EngineeringandClaude Opus 4.8 289ce7f42f ci: self-hosted warden-sdk runner registered + docs/ci-cd.md (P5)
Registered a 3rd repo-scoped runner on bfe-mpc-0640 (label warden-sdk,
bfe-mpc-0640-warden-sdk) per ADR-0004. It is offline pending the sudo-gated steps
(service install + CPUQuota/MemoryMax cgroup cap + toolchain/python provisioning),
documented in docs/ci-cd.md as a [maintainer] handoff. kernel-build stays dispatch-gated
and passes JOBS=4 as a belt-and-braces resource bound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
2026-08-25 14:31:29 -06:00
BFE EngineeringandClaude Opus 4.8 f2a9c0a32c kernel: RV1106 6.18.46 patch series + hermetic build wrapper (P1)
- patches/: the pristine-6.18.46 -> WardenOS delta as a 13-patch, subsystem-split
  series (223 files, ~136K lines: clk, pinctrl, DTs/mach, usb-phy, VOP/panel/rgb,
  mailbox, pvtm, rknpu, rga, aic8800 wifi, audio codec, thermal/rtc/adc/gmac/touch).
  Verified: every patch applies cleanly onto pristine (git apply --check), the full
  series reproduces the hardware-verified tree, and the applied source configures +
  builds the warden dtb + rockchip DRM drivers (rc=0).
- build/build-kernel.sh: fetch+verify pristine (sha256-pinned) -> apply series ->
  warden_defconfig -> zImage + rv1106-warden.dtb. build/warden_defconfig captured.
- CI: `patches-apply` (GitHub-hosted, cached tarball) enforces the series applies;
  `kernel-build` (self-hosted warden-sdk runner, dispatch-gated until registered)
  runs the full build and uploads the image.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
2026-08-25 14:26:04 -06:00
BFE EngineeringandClaude Opus 4.8 6fcfb80e5a bench: dependency-free sim micro-benchmarks + CI bench job (P4)
benches/sim_bench.rs (harness=false, zero deps): fixed-iteration ns/op timing for
hpmcu_tick, cru_poll, modbus_read_holding, rga_improcess, membus_poke_peek. Human
timings to stdout, one JSON trend line per bench to stderr for CI capture. New CI
`bench` job smoke-runs them. Regression-vs-history gating is future work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
2026-08-25 14:15:22 -06:00
BFE EngineeringandClaude Opus 4.8 1752dea7b6 drivers/relays: hardened + 100% MC/DC (Tier-1, first driver)
Bring relays.c into warden-sdk/drivers/ behind a relay_io seam (ADR-0005): the
sysfs plumbing is an injectable backend, the gpio root is $WARDEN_GPIO_ROOT-
overridable. Host harness (drivers/relays/test/) reaches 100% MC/DC (40/40
conditions, 100% lines) by combining fake-io unit tests (logic branches incl.
export->node-appears) with real-sysfs integration tests over a scratch tree
(backend fopen/stat branches). CI `mcdc` job enforces 100% across drivers/*/test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
2026-08-25 14:01:29 -06:00
BFE EngineeringandClaude Opus 4.8 1d3298a408 ci: GitHub Actions pipeline — cargo test + llvm-cov + badges (P5 host-testable jobs)
test job runs cargo test across sim + config-lint and cargo-llvm-cov line coverage
on the sim crate; badges job renders loc/tests/coverage shields on push to bringup
([skip ci] + paths-ignore loop guard). mcdc/bench/kernel-build jobs land with
P2/P4/P1. README carries the CI + badge row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
2026-08-25 13:48:22 -06:00