From e27fafe320b611ac21ee96a5a6e73fd0b0831904 Mon Sep 17 00:00:00 2001 From: jedarden Date: Mon, 6 Jul 2026 16:40:27 -0400 Subject: [PATCH] docs(bf-5cgc): hand off blob-creation inventory to identity-field beads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Formalize notes/bf-1q3m-consolidated.md §6 (tiered fix-target list) into a tracked site→fix→bead deliverable (notes/bf-5cgc-handoff.md): - automation main.go:2303 + engine.go:1337 → bf-5151 / bf-64h5 - explainability main.go:2206 + handler.go:95 (fold identityMap :2216) → bf-5151 - volume main.go:2326 + shape.go:1080 → bf-5151 (one-line PersonID) All three mirror the reference population pattern analytics.TrackUpdate (main.go:2267-2271): pull PersonID via identityMatcher.GetMatch(b.ID). Sites re-verified exact at HEAD 1ecc999. Includes Tier-2/3/out-of-scope (tracker wiring decision, E1 no-op, dead api.BlobPos) and a scope note flagging the Go-backend vs TS-framing mismatch on the sibling beads. Comments posted on bf-5151, bf-64h5, bf-1wvm, bf-iner, bf-4bhd. Closes bf-4bhd's "Report is ready for the next bead to use" criterion. Docs/.beads only — zero Go/JS source changed; go vet ./... passes. Co-Authored-By: Claude --- .beads/issues.jsonl | 264 +++++++++++++++++++++++------------- notes/bf-5cgc-handoff.md | 283 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 451 insertions(+), 96 deletions(-) create mode 100644 notes/bf-5cgc-handoff.md diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index bc67aaa..cdc82dd 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,96 +1,168 @@ -{"id":"bf-11dj","title":"End-to-end verify deployed /api/status reports real version","description":"Prove the fix end-to-end on a built image: GET /api/status must return the real deployed version (0.1.357+), not '1.0.0' and not 'dev'.\n\nScope:\n- After a CI image build with the VERSION build-arg wired, run the image (or query a deployed pod) and curl GET /api/status.\n- Assert the 'version' field equals the VERSION file contents for that build.\n- Record the observed version (e.g. a comment with the curl output).\n\nAcceptance criteria:\n- GET /api/status on the built image returns version == VERSION file value.\n- Observed version is recorded on this bead (not '1.0.0', not 'dev').","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T08:25:50.150628873Z","updated_at":"2026-07-04T08:26:03.611775605Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-11dj","depends_on_id":"bf-1rab","type":"blocks","created_at":"2026-07-04T08:26:03.596892561Z","created_by":"cli","thread_id":""}]} -{"id":"bf-121s","title":"Run npm ci in dashboard and capture raw output","description":"Scope: discovery only — NO source fixes. Run the install and capture the raw result.\n\nSteps:\n1. cd dashboard/ && npm ci.\n2. Capture the FULL verbatim output (stdout + stderr) and the exit code.\n\nAcceptance criteria:\n- npm ci is actually executed in dashboard/.\n- Raw output + exit code are recorded verbatim in a comment (success or failure).\n- No source changes — do NOT patch package.json, package-lock.json, or any file to fix the install. The error itself is the finding.\n- Does NOT run jest or build anything.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T11:21:59.577900469Z","updated_at":"2026-07-04T11:22:29.431359988Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-121s","depends_on_id":"bf-5wky","type":"blocks","created_at":"2026-07-04T11:22:29.425067451Z","created_by":"cli","thread_id":""}]} -{"id":"bf-1580","title":"Confirm the diff is confined to firmware/test/test_runner.c","description":"Split-child 1 of 4 for umbrella bf-tof1 (final verify bead that closes bf-22vg). Static, prerequisite-free scope check: confirm NO part of the parent's change leaked outside the single allowed file.\n\nScope (no code changes; this is read-only verification):\n- Run: git diff --stat (and git status --short) from the repo root.\n- Confirm the ONLY changed source file is firmware/test/test_runner.c.\n- Confirm there are NO changes to firmware/main/* and NO changes to firmware/CMakeLists.txt (the parent contract forbids both).\n\nPrecondition: the working tree must have the change applied (run from a state where the guard is present). If the working tree shows firmware/test/test_runner.c as unmerged (UU), that index state does not affect what git diff --stat reports for the file content — verify the file-content diff, not the index state.\n\nAcceptance criteria:\n- git diff --stat lists firmware/test/test_runner.c and nothing under firmware/main/* or firmware/CMakeLists.txt.\n- A one-line comment on this bead recording the exact git diff --stat output.\n- Closes independently once the scope boundary is confirmed.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T01:51:42.097415205Z","updated_at":"2026-07-04T02:07:42.117426679Z","closed_at":"2026-07-04T02:07:42.117426679Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"comments":[{"id":32,"issue_id":"bf-1580","author":"cli","text":"git diff --stat: .beads/issues.jsonl | 72 +++..., .needle-predispatch-sha | 2 +-, firmware/test/test_runner.c | Unmerged — only firmware path is firmware/test/test_runner.c; nothing under firmware/main/* and no firmware/CMakeLists.txt. UU is index-only artifact: working-tree blob 219bb300 == HEAD, no firmware content diff vs HEAD (firmware/main/, firmware/CMakeLists.txt empty). Scope confirmed confined to the single allowed file.","created_at":"2026-07-04T02:04:15.953102405Z"},{"id":33,"issue_id":"bf-1580","author":"cli","text":"Scope confirmed: git diff --stat lists firmware/test/test_runner.c | Unmerged (+ only .beads/issues.jsonl and .needle-predispatch-sha bookkeeping). No changes under firmware/main/* and no changes to firmware/CMakeLists.txt — diff boundary holds.","created_at":"2026-07-04T02:06:26.459930278Z"}]} -{"id":"bf-16j","title":"Add CSI binary frame serialization host test","description":"Part of split of bf-brz. Depends on the nvs test child (bf-31q).\n\nGoal: add a host-based unit test for the 24-byte binary CSI frame serialization.\n\nContext (important, corrects the parent bead): the 24-byte frame is NOT packed in csi.c. It is packed in firmware/main/websocket.c inside the websocket_send_csi() function (around lines 226-262). csi.c only collects CSI and then calls websocket_send_csi. The header layout is: bytes 0-5 node_mac, 6-11 peer_mac, 12-19 timestamp_us (little-endian via memcpy), 20 rssi, 21 noise_floor, 22 channel, 23 n_sub, then n_sub*2 bytes of I/Q payload. SPAXEL_FRAME_HEADER_SIZE is 24. This layout is mirrored by the Go parser in mothership/internal/ingestion/ -- cross-check against it.\n\nApproach:\n- websocket_send_csi() is coupled to the websocket client handle, the connection flag, and the send mutex, so it cannot be compiled on a host as-is. Extract just the header and payload packing into a pure host-compilable function (e.g. csi_frame_pack(out, node_mac, peer_mac, timestamp_us, rssi, noise_floor, channel, iq_data, n_sub) returning frame_len) with byte-identical output. Keep production websocket.c behavior identical (call the extracted packer from websocket_send_csi).\n- Add a little-endian frame round-trip test asserting every header field at its exact byte offset, including: timestamp_us round-trips correctly (multi-byte LE), rssi/noise_floor sign handling (int8 as uint8), n_sub, and the I/Q payload bytes. Where helpful, validate packed bytes against the layout the Go ingestion parser expects.\n\nAcceptance criteria:\n- A test under firmware/test/ does at least one little-endian frame round-trip including all header fields.\n- Test runs under the harness single documented command and passes.\n- firmware/main/ production behavior is unchanged (extraction is refactor-only) and the firmware build still succeeds.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T05:50:01.243531820Z","updated_at":"2026-07-03T05:50:01.243531820Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-16j","depends_on_id":"bf-31q","type":"blocks","created_at":"2026-07-03T05:50:37.923167595Z","created_by":"cli","thread_id":""}]} -{"id":"bf-17m","title":"Mark CSI recording buffer Done in PROGRESS.md","description":"Phase 2 table in PROGRESS.md shows \"CSI recording buffer\" as Pending, but it is implemented in mothership/internal/recording/buffer.go. Update the Phase 2 table row (PROGRESS.md:30) to Done, and remove it from the \"Remaining for Phase 2\" list (PROGRESS.md:95).\n\nAcceptance criteria:\n- PROGRESS.md Phase 2 table row for \"CSI recording buffer\" reads Done (not Pending).\n- The item is removed from the \"Remaining for Phase 2\" list.\n- mothership/internal/recording/buffer.go still exists and is unchanged.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T04:01:17.932376879Z","updated_at":"2026-07-04T07:52:59.308609827Z","closed_at":"2026-07-04T07:52:59.308609827Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-17m","depends_on_id":"bf-2s2","type":"blocks","created_at":"2026-07-03T04:01:17.948590766Z","created_by":"cli","thread_id":""}],"comments":[{"id":61,"issue_id":"bf-17m","author":"cli","text":"Verified all acceptance criteria already satisfied in the repo (commit 83ec5a5 'docs: mark Phase 2 CSI recording buffer + adaptive sensing rate Done'). (1) PROGRESS.md:30 Phase 2 table row reads Done for 'CSI recording buffer' -- not Pending. (2) No 'Remaining for Phase 2' list exists anywhere in PROGRESS.md (already removed; line 95 now states 'All Phase 2 items implemented'). (3) mothership/internal/recording/buffer.go exists, is git-tracked, and is unchanged (empty diff vs HEAD). No working-tree changes were needed, so no commit was made. Task premise (row shown as Pending) was stale.","created_at":"2026-07-04T07:38:30.070463773Z"},{"id":62,"issue_id":"bf-17m","author":"cli","text":"No-op close: all three acceptance criteria were already satisfied in the committed state by prior commit 83ec5a5 ('docs: mark Phase 2 CSI recording buffer + adaptive sensing rate Done'). Verified (1) PROGRESS.md:30 reads '| CSI recording buffer | **Done** | Disk-backed circular buffer — mothership/internal/recording/buffer.go |' (not Pending); (2) no 'Remaining for Phase 2' list exists — PROGRESS.md:95 states 'All Phase 2 items implemented — CSI recording buffer (mothership/internal/recording/buffer.go) ... landed after this iteration'; (3) mothership/internal/recording/buffer.go exists (13959 bytes) and is clean vs HEAD (unchanged). The bead's 'shows Pending' premise was stale (pre-83ec5a5 snapshot). No working-tree changes were needed, so nothing was committed or pushed; deliberately did NOT run git commit -am to avoid sweeping up unrelated modified/unmerged files (.beads/issues.jsonl, .needle-predispatch-sha, UU firmware/test/test_runner.c) outside this task's scope.","created_at":"2026-07-04T07:53:47.013042566Z"}]} -{"id":"bf-18x","title":"Retire GitHub Actions workflows (benchmark-ci.yml, e2e.yml) - Argo CI covers both","description":"Policy: GitHub Actions are disabled across all repos; all CI runs on Argo Workflows (iad-ci). This repo still carries two workflow files under .github/workflows/ that contradict that:\n\n1. benchmark-ci.yml - triggers on push/pull_request to main and runs the fusion-loop timing benchmark. This gate ALREADY runs in the spaxel-build Argo WorkflowTemplate (go test -bench=BenchmarkFusionLoop step), so the GH workflow is redundant dead config that suggests re-enabling Actions.\n2. e2e.yml - workflow_dispatch/repository_dispatch trigger meant to be fired by Argo; superseded by the spaxel-e2e Argo WorkflowTemplate which runs the acceptance/e2e suites directly in-cluster.\n\nSteps:\n- Delete both files under .github/workflows/ (or the whole .github/workflows dir if nothing else remains).\n- Update docs/ci-benchmark-integration.md: remove the 'GitHub Actions Integration' section (it recommends 'gh workflow run benchmark-ci.yml') and keep it Argo-only; note the benchmark step already exists in the spaxel-build template.\n- Grep the repo for other references to these workflows (README-level docs, PROGRESS.md) and clean them up.\n\nAcceptance criteria:\n- No *.yml remains under .github/workflows/ that has push/pull_request triggers\n- docs/ci-benchmark-integration.md no longer references GitHub Actions as a runnable path\n- No functional CI change needed in declarative-config (the Argo templates already cover both jobs)","design":"","acceptance_criteria":"","notes":"","status":"open","priority":3,"issue_type":"task","created_at":"2026-07-02T11:30:22.474117218Z","updated_at":"2026-07-03T17:38:41.167352165Z","source_repo":".","compaction_level":0} -{"id":"bf-1fd4","title":"Restore main per-test loop to the naive direct-call starting state","description":"Split-child 1 of 4 for parent bf-22vg. ROOT CAUSE of the repeated failures: firmware/test/test_runner.c has drifted past this bead starting point. main() per-test loop currently already contains a setjmp(g_test_jmp) guard PLUS outcome plumbing that belongs to sibling scope (bf-1na child 3): local passed/failed counters, an else branch bumping failed, a one-line summary printf, and a return based on g_failure_count. This bead contract explicitly forbids all of that here. The entanglement is why the bead keeps failing.\n\nThis child strips the drift back to the naive starting state the parent description names, so the core guard can be re-added cleanly by the next child. Scope: firmware/test/test_runner.c, main() and its immediate loop/prologue/epilogue only.\n\nDo:\n- Remove the setjmp(g_test_jmp) guard wrapping the body call, restoring the bare g_tests[i].fn() call.\n- Remove the local passed and failed counters (declarations and all uses).\n- Remove the else branch entirely.\n- Remove the run-summary printf that reads passed/failed/g_test_count.\n- Adjust main() return so it compiles with no tally in place (return 0 for this intermediate state; the real non-zero-on-failure exit code is sibling scope and lands later).\n\nDo NOT (other beads):\n- Add or modify the setjmp guard (child 2 of this split re-adds it).\n- Re-introduce any PASS/FAIL label, pass/fail tally, summary line, or non-zero exit code.\n\nAcceptance criteria:\n- main() per-test loop body is exactly: printf the RUN line, then call g_tests[i].fn() directly. No counters, no else branch, no summary printf, no setjmp.\n- File compiles cleanly with gcc -std=c11 -Wall -Wextra.\n- make -C firmware/test test runs every test and the process completes.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T00:10:57.631531567Z","updated_at":"2026-07-04T00:37:35.008624416Z","closed_at":"2026-07-04T00:37:35.008624416Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"comments":[{"id":29,"issue_id":"bf-1fd4","author":"cli","text":"Closed: main() per-test loop is at the naive direct-call baseline. The change was already present on origin/main in commit 6a2d140 ('test(runner): strip main() to naive direct-call baseline (bf-1fd4)'), merged at HEAD; bead had been left open so this run re-verified it. All four acceptance criteria pass on a clean checkout: (1) main() loop body is exactly printf(\"RUN: %s\\n\", g_tests[i].name) then g_tests[i].fn() directly (test_runner.c:228-232) — no setjmp guard, no passed/failed counters, no else branch, no summary printf, returns 0; (2) compiles cleanly under gcc -std=c11 -Wall -Wextra (and -Werror); (3) 'make -C firmware/test test' runs all 30 tests to completion, exit 0; (4) firmware/main/* and firmware/CMakeLists.txt untouched. The setjmp/longjmp guard (child 2) and PASS/FAIL tally + non-zero exit (child 3, bf-1na) are intentionally absent here per contract. No new commit needed: main is 0 ahead / 0 behind origin/main.","created_at":"2026-07-04T00:36:29.355788939Z"}]} -{"id":"bf-1ffd","title":"Build-verify a -Wclobbered-clean compile of test_runner.c at -O0..-Os and confirm the suite still runs green with no out-of-scope changes","description":"Split-child 4/4 of the bf-31rd re-split (umbrella bf-31rd, the last child). Depends on child 3 (bf-1iqs). This is the verification gate; it makes no code changes.\n\nScope (verification only across firmware/test/test_runner.c and the firmware/test build):\n- Compile firmware/test/test_runner.c with: gcc -std=c11 -Wall -Wextra -Wclobbered at -O0, -O1, -O2, and -Os. Confirm NO -Wclobbered warning AND no other setjmp-related warning at any opt level.\n- Run 'make -C firmware/test test' on the all-passing suite and confirm it completes: every registered test runs (RUN: line per test) and the process exits (no hang, no crash). This re-confirms child 3's claim of zero behavior change.\n- Confirm the change set stays in bounds: 'git diff -- firmware/main firmware/CMakeLists.txt' must be empty (no changes to firmware/main/* or firmware/CMakeLists.txt).\n\nAcceptance criteria:\n- gcc -std=c11 -Wall -Wextra -Wclobbered is silent at -O0/-O1/-O2/-Os for test_runner.c (no -Wclobbered, no other setjmp warning).\n- 'make -C firmware/test test' completes green on the all-passing suite.\n- 'git diff -- firmware/main firmware/CMakeLists.txt' is empty.\n- A comment on THIS bead records the clean-build result (per opt level) and the green make-run, satisfying umbrella bf-31rd's build/no-behavior-change acceptance.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T07:13:19.023096424Z","updated_at":"2026-07-04T07:13:58.263844963Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-1ffd","depends_on_id":"bf-1iqs","type":"blocks","created_at":"2026-07-04T07:13:58.253777156Z","created_by":"cli","thread_id":""}]} -{"id":"bf-1iqs","title":"Apply the volatile remedy on the loop index per the gcc probe and write the C11 7.13.2.1 clobber-audit comment at the setjmp call site","description":"Split-child 3/4 of the bf-31rd re-split (umbrella bf-31rd). Depends on child 2 (bf-6761). This is the ONLY child that changes production code.\n\nScope (firmware/test/test_runner.c, main()'s per-test loop ONLY):\n- Declare the loop index 'i' volatile if child 2 (bf-6761) found -Wclobbered fires on it (the expected result per the qsort-removal probe — matching the current state of the file, where 'i' is already volatile). If child 2 instead found -Wclobbered never fires under any realistic condition, leave 'i' unqualified and document why. Follow the data, do not assume.\n- At the setjmp(g_test_jmp) call site, add (or reconcile with the existing) concise comment recording the full audit so a future editor cannot regress it: the C11 7.13.2.1 reference; the list of automatic variables checked (from child 1, bf-4qk2); the per-variable verdict (i written only by for-init/increment outside the setjmp->longjmp window; no local tallies; g_failure_count/g_test_jmp are file-scope static so 7.13.2.1 does not apply); AND the gcc empirical result with the reason volatile is kept despite the standard not strictly requiring it (from child 2, bf-6761) — i.e. that -Wclobbered is heuristic and fires on this loop shape once the qsort register-bias is gone.\n- Out of bounds: do NOT add PASS/FAIL labels, pass/fail tallies, the run summary line, or non-zero exit-on-failure — that is sibling bf-1na child-3 scope, not this lineage.\n\nAcceptance criteria:\n- 'i' is volatile-qualified IFF child 2 requires it for a -Wclobbered-clean build; otherwise it is plain int and the comment says why.\n- A comment at the setjmp call site documents the 7.13.2.1 audit: variables checked, per-variable verdict, gcc empirical result, and why volatile is (or is not) kept.\n- No behavior change vs the prior child — the loop still walks i = 0..g_test_count-1 exactly once per test.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T07:12:31.660836989Z","updated_at":"2026-07-04T07:13:58.240629739Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-1iqs","depends_on_id":"bf-6761","type":"blocks","created_at":"2026-07-04T07:13:58.233994471Z","created_by":"cli","thread_id":""}]} -{"id":"bf-1na","title":"Add main() entry-point skeleton with setjmp recovery loop and run-command header","description":"Child 1 of the bf-bq9 sub-split (parent bf-bq9; grandparent bf-2i4; great-grandparent bf-56v; great-great-grandparent bf-4ne). Depends on sibling bf-3id (per-test failure recovery: g_test_jmp + g_failure_count + test_record_failure, already landed). This child is the entry-point core of the runner — the piece that makes a failing assertion non-fatal.\n\nScope (extend firmware/test/test_runner.c):\n- Add a comment block above main() naming the documented run command: `make -C firmware/test test` (per the bf-1xs header contract and the bf-56v gcc-harness decision record). make compiles every test_*.c plus this runner with plain gcc and runs the binary; main()'s exit code is what make propagates.\n- Implement int main(void): iterate the registry g_tests[] (the TEST() constructors have already fully populated it before main runs). For each test, setjmp(g_test_jmp) — the per-test target from sibling bf-3id — then call g_tests[i].fn(). On the direct setjmp return (0) the body ran clean: print \"PASS: \" and bump a local passed counter. On a longjmp return (non-zero) a failed assertion tripped test_record_failure(): print \"FAIL: \" and bump a local failed counter. Either way advance to the next test, so one test's failure never blocks the rest.\n- For now return 0 — the non-zero-on-failure exit code is a later child.\n\nAcceptance criteria:\n- firmware/test/test_runner.c has an int main(void) that drives every registered test through the per-test setjmp/longjmp recovery loop, printing one PASS/FAIL line per test.\n- A failing assertion in one test does NOT prevent later tests from running (the per-test setjmp/longjmp).\n- A comment block above main() documents the run command `make -C firmware/test test`.\n- Compiles cleanly with gcc -std=c11 -Wall -Wextra; with at least one TEST() defined it links, runs, and exits 0 when all tests pass.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T14:47:14.200520550Z","updated_at":"2026-07-03T15:22:05.907901457Z","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-1na","depends_on_id":"bf-5k8y","type":"blocks","created_at":"2026-07-03T15:20:11.583168961Z","created_by":"cli","thread_id":""}],"comments":[{"id":12,"issue_id":"bf-1na","author":"cli","text":"Auto-split into 3 children (bf-1zew -> bf-53ut -> bf-5k8y) after 3 failures. NOTE for child workers: the implementation this bead describes is ALREADY committed in 549dc1f (firmware/test/test_runner.c main() at lines 229-250: name-sort, per-test setjmp(g_test_jmp) loop, PASS/FAIL per test, summary line, non-zero exit). The children below are scoped to bf-1na's STATED scope only (run-command comment + iteration + per-test setjmp recovery + PASS/FAIL + return 0; the summary line and non-zero exit code are explicitly later siblings, out of scope). Treat each child as verify-then-close against the existing committed code rather than reimplement from scratch.","created_at":"2026-07-03T15:21:47.652360100Z"}]} -{"id":"bf-1oak","title":"Confirm the failing RUN line precedes the passing RUN line in stdout","description":"Split-child of parent bf-ba2u (auto-split). Depends on child 3. THIS IS THE LAST CHILD — parent bf-ba2u depends on this one. This child proves the CORE recovery-loop interleaving contract.\n\nGOAL: prove IN-ORDER interleaving — the failing test's neutral line appears BEFORE the passing test's neutral line in the output stream.\n\nSTEPS:\n- Read /tmp/bf38e9_test_stdout.txt (produced by child 1).\n- Locate the line number of the FAILING line:\n RUN: aaa_bf38e9_intentional_fail (assertion failed)\n- Locate the line number of the PASSING line:\n RUN: aab_bf38e9_passes_after_fail\n- Confirm failing_line_number < passing_line_number.\n\nACCEPTANCE CRITERIA:\n- Both lines exist (guaranteed by children 2 and 3).\n- The failing line's position is strictly LESS than the passing line's position — i.e. in-order interleaving holds.\n- No re-run of the suite required; no source changes.\n\nIf the PASSING line appears BEFORE the failing line, that is a REAL regression in test_runner.c's recovery loop — STOP and report it; do not edit the verify file to mask it.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T20:25:58.069325313Z","updated_at":"2026-07-03T21:14:37.890500025Z","closed_at":"2026-07-03T21:14:37.890500025Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-1oak","depends_on_id":"bf-5qsy","type":"blocks","created_at":"2026-07-03T20:26:26.931433481Z","created_by":"cli","thread_id":""}],"comments":[{"id":23,"issue_id":"bf-1oak","author":"cli","text":"Confirmed in-order interleaving of the bf-38e9 recovery-loop contract in /tmp/bf38e9_test_stdout.txt: failing test neutral marker at line 4 (RUN: aaa_bf38e9_intentional_fail (assertion failed)) strictly precedes passing test neutral marker at line 5 (RUN: aab_bf38e9_passes_after_fail) — 4 < 5. Both lines exist exactly once. Proves test_runner.c setjmp/longjmp loop recovered from the failed assertion, advanced to the next test, and ran it to completion — NOT a regression. As final child of the split, deleted throwaway firmware/test/test_bf38e9_verify.c (its header contract: deleted by final child, NOT committed); post-deletion clean rebuild green: 29 passed, 0 failed of 29 (make exit 0). Pure verification, no source changes.","created_at":"2026-07-03T21:13:46.535039105Z"}]} -{"id":"bf-1rab","title":"Verify Dockerfile and spaxel-build CI pass VERSION build-arg","description":"Confirm the version actually reaches the compiled binary in CI, not just in a local dev build. The Dockerfile already declares 'ARG VERSION=dev' and builds with -ldflags=\"-s -w -X main.version=${VERSION}\", so the remaining risk is the CI pipeline not passing --build-arg VERSION from the VERSION file.\n\nScope:\n- Confirm the Dockerfile ARG VERSION / -X main.version ldflag is intact.\n- Inspect the spaxel-build WorkflowTemplate (jedarden/declarative-config k8s/iad-ci/argo-workflows/) and confirm the docker build step passes --build-arg VERSION=$(cat VERSION) (or equivalent) from the repo VERSION file (currently 0.1.357).\n- If the build-arg is missing, add it so the image is not tagged with the fallback 'dev'.\n\nAcceptance criteria:\n- CI build passes the VERSION file contents as the VERSION build-arg.\n- A locally built image with --build-arg VERSION=0.1.357 reports version=0.1.357 at /api/status (not 'dev').","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T08:25:46.984969893Z","updated_at":"2026-07-04T08:26:03.583927346Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-1rab","depends_on_id":"bf-i90p","type":"blocks","created_at":"2026-07-04T08:26:03.576691991Z","created_by":"cli","thread_id":""}]} -{"id":"bf-1tae","title":"Verify no outcome plumbing is present in this lineage","description":"Read-only static verification of firmware/test/test_runner.c only (NO code changes). Child 4 of 4 splitting bf-2opw (confirm the per-test loop body matches parent bf-22vg contract). This child establishes the boundary that distinguishes this child-2 lineage from the outcome sibling.\n\nScope: In main(), confirm this lineage contains NONE of: PASS/FAIL outcome labels, a pass/fail tally counter, a run summary line, or non-zero exit logic. Specifically: main() returns 0 unconditionally (quote the return 0; statement), and the run-wide failure counter g_failure_count is NOT read anywhere in main() in this lineage (it is bumped by test_record_failure() but only surfaced/read in sibling bf-1na's child 3 scope, which is explicitly out of scope here).\n\nAcceptance criteria: A comment on this bead quoting main()'s exact return 0; line from firmware/test/test_runner.c, and confirming (a) no PASS/FAIL/tally/summary/non-zero-exit is present in this lineage, and (b) g_failure_count is not read in main() here. Closes independently once confirmed.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T02:42:52.882592914Z","updated_at":"2026-07-04T06:11:23.331670307Z","closed_at":"2026-07-04T06:11:23.331670307Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-1tae","depends_on_id":"bf-3yvc","type":"blocks","created_at":"2026-07-04T02:43:50.410692321Z","created_by":"cli","thread_id":""}],"comments":[{"id":52,"issue_id":"bf-1tae","author":"cli","text":"Read-only verification of firmware/test/test_runner.c complete — confirms this child-2 lineage (bf-1tae, child 4 of bf-2opw) contains NO outcome plumbing, matching parent bf-22vg contract.\n\nmain()'s exact return statement (firmware/test/test_runner.c:261):\n return 0;\n\nIt is unconditional — the only return in main() (lines 228-262), reached after the for-loop over the sorted registry with no branching on any failure state.\n\n(a) No PASS/FAIL/tally/summary/non-zero-exit present in this lineage:\n- The only per-test observable is the RUN marker printf(\"RUN: %s\\n\", name) at line 255 — never a PASS or FAIL label.\n- No pass/fail tally counter is incremented in main().\n- No run-summary line is printed after the loop.\n- No non-zero exit path: no return of a non-zero value, no exit() call. The single return is \"return 0;\".\n- test_record_failure() (lines 160-173) prints a per-assertion detail line (file:line + vfprintf message) and longjmps — assertion-level diagnostics, not a per-test PASS/FAIL outcome label, and outside main()'s scope.\n- The only other printf/fprintf in the file is test_register()'s registry-full stderr warning (line 100), also not an outcome label.\n\n(b) g_failure_count is NOT read in main():\n- grep over the file shows g_failure_count only at: line 25 (file-header comment describing the eventual contract), line 143 (declaration), line 170 (the bump inside test_record_failure()), and line 225 (a comment stating it is deliberately not yet read here).\n- None are in main()'s code (lines 228-262). The counter is bumped by test_record_failure() but never surfaced/read in main() — that surfacing is sibling bf-1na child-3 scope, explicitly out of scope here.\n\nBoundary established: every test compiles and runs through the setjmp/longjmp recovery loop (target g_test_jmp declared line 132, setjmp'd line 256), failures are caught cleanly, but nothing yet turns a passed/failed run into output or a non-zero exit code. main() returns 0 regardless.","created_at":"2026-07-04T06:08:35.828330064Z"},{"id":53,"issue_id":"bf-1tae","author":"cli","text":"Verified — read-only static check of firmware/test/test_runner.c (main(), file lines 228-262). NO code changes.\n\n(a) Outcome plumbing is ABSENT from this lineage. main() contains NONE of: PASS/FAIL labels, pass/fail tally counter, run summary line, or non-zero-exit logic. The only output in main() is the per-test RUN marker — printf(\"RUN: %s\\n\", g_tests[i].name); — printed before setjmp; no PASS/FAIL label, no tally increment, no summary after the loop. grep for PASS/FAIL across main() returns nothing.\n\n(b) g_failure_count is NOT read in main(). It is declared at file scope (line 143) and bumped only inside test_record_failure() (g_failure_count++; line 170) — outside main(). grep for g_failure_count across main()'s body (lines 228-262) returns nothing. Surfacing that counter is the sibling bf-1na child-3 scope, explicitly out of scope here. The only loop bound main() reads is g_test_count (the registry length), a distinct symbol.\n\nmain() returns 0 unconditionally — exact line (firmware/test/test_runner.c:261):\n\n return 0;\n\nThis is the sole return statement in main(); there is no return g_failure_count or any other exit-code logic. This matches the parent bf-22vg contract for this intermediate state: the per-test setjmp/longjmp recovery loop is present (setjmp(g_test_jmp) == 0 gates g_tests[i].fn()), so an assertion that fires longjmps cleanly back to the loop and advances to the next test — but the failure is not yet surfaced or counted here. Boundary between this child-2 lineage and the outcome sibling confirmed.","created_at":"2026-07-04T06:10:53.783414399Z"}]} -{"id":"bf-1xs","title":"Add firmware/test/ harness header with TEST() macro and assert macros","description":"Child 1 of the bf-56v harness split (parent umbrella bf-56v; grandparent bf-4ne). Build ONLY the public header API that later children compile against.\n\nScope: create firmware/test/test_runner.h. It defines the harness API used by every test unit and by the runner:\n- TEST(name): a macro that declares a void test function AND a GCC constructor (__attribute__((constructor))) that self-registers it via test_register(name, fn) before main() runs. This is what lets a new test_*.c be picked up with zero SOURCES edits.\n- ASSERT_* macros (at minimum ASSERT_EQ(actual, expected)) that, on mismatch, call test_record_failure(file, line, fmt...) which longjmp()s back to a per-test setjmp — so a failing assertion aborts only the current test, not the whole suite.\n- Prototypes for test_register(const char*, test_fn) and test_record_failure(...), and any jmp_buf plumbing needed (the live jmp target lives in test_runner.c, child 2).\n- A header comment block that (a) records the gcc host harness decision from bf-21t (why gcc, not ESP-IDF --target linux), and (b) names the single documented run command that child 3 will make real: 'make -C firmware/test test'.\n\nAcceptance criteria:\n- firmware/test/test_runner.h exists and is self-contained (no firmware/main/* includes; only libc).\n- TEST(name) self-registers via a constructor; a trivial includer .c using TEST(x){} compiles cleanly with 'gcc -std=c11 -Wall -Wextra -c'.\n- ASSERT_EQ exists and routes failure through test_record_failure (no direct abort/exit).\n- No main() function in the header.\n- Header comment documents the bf-21t decision and the planned single run command.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T10:47:33.030204174Z","updated_at":"2026-07-03T11:01:21.836089295Z","closed_at":"2026-07-03T11:01:21.836089295Z","close_reason":"Completed — firmware/test/test_runner.h committed (128024c) and pushed; header verified against all acceptance criteria. Summary in comments.","source_repo":".","compaction_level":0,"labels":["split-child"],"comments":[{"id":7,"issue_id":"bf-1xs","author":"cli","text":"Completed: committed ONLY the public header API, firmware/test/test_runner.h (commit 128024c, pushed to origin/main).\n\nThe header was already present in the working tree from a prior umbrella-bead run; I verified it against every acceptance criterion rather than rewrite it:\n- TEST(name) -> void body + __attribute__((constructor)) that calls test_register(name, fn) before main(). Zero SOURCES edits for new tests.\n- ASSERT_EQ / ASSERT_TRUE / ASSERT_FALSE route failures through test_record_failure(file, line, fmt...) which longjmps to the per-test setjmp (live target lives in test_runner.c, child 2). No direct abort/exit.\n- Prototypes for test_register(const char*, test_fn) and test_record_failure(...).\n- Header comment documents the bf-21t gcc-vs-ESP-IDF-linux decision and the planned single run command 'make -C firmware/test test' (child 3).\n\nVerified: only libc includes (setjmp.h, stdbool.h, stdint.h), no firmware/main/* includes, no main(). Trivial includer TEST(x){} compiles cleanly with 'gcc -std=c11 -Wall -Wextra -c' (exit 0); full harness builds/runs green as a confidence check.\n\nScope respected: test_runner.c (child 2), Makefile (child 3), test_sanity.c left untracked for sibling beads. No changes to firmware/main/* or firmware/CMakeLists.txt.","created_at":"2026-07-03T10:59:58.703466103Z"}]} -{"id":"bf-1xy","title":"Verify firmware/test/ harness extendability and firmware build non-regression","description":"Finalization gate for the firmware/test/ harness (part of split of bf-4ne; depends on child harness build-out).\n\nGoal: confirm the harness is trivially extensible for future module tests (nvs/csi/prov) and that adding firmware/test/ did not break the existing ESP-IDF firmware build.\n\nSteps:\n- Confirm adding a new test_*.c is trivial: either the Makefile wildcards test_*.c automatically, or there is a single documented SOURCES list to append. Prove it by adding a throwaway test_*.c, running the harness, then removing it (leave the tree clean).\n- Verify the existing firmware build is unchanged: firmware/CMakeLists.txt, firmware/main/CMakeLists.txt, and the firmware-builder stage of the Dockerfile still build identically (no test files leaked into the ESP-IDF component SRCS / INCLUDE_DIRS).\n- Confirm the chosen approach (from bf-21t) and the run command are documented in the test runner header comment.\n\nAcceptance criteria:\n- A new test_*.c dropped into firmware/test/ is picked up by the documented run command with no other edit (or via a single SOURCES-list append).\n- firmware/CMakeLists.txt, firmware/main/CMakeLists.txt, and the Dockerfile firmware-builder stage are unchanged by this split (git diff shows no changes to them).\n- Runner header comment documents both the chosen approach and the exact run command.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T09:32:02.860861429Z","updated_at":"2026-07-03T09:32:02.860861429Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-1xy","depends_on_id":"bf-56v","type":"blocks","created_at":"2026-07-03T09:32:17.008574348Z","created_by":"cli","thread_id":""}]} -{"id":"bf-1z44","title":"Write throwaway interleaving verify file (failing test + companion passing test)","description":"Split-child 1 of 4 for parent bf-65m5 (itself child 4 of 4 of bf-38e9). First link in the chain — creates the throwaway verification artifact the later children run against.\n\nGOAL: stand up a temporary, self-contained verify file whose TEST() ordering provably exercises the recovery loop's interleaving contract (a failing test followed by a passing test).\n\nCONTEXT (read once, then act — KEEP THIS FAST; the parent timed out 3x from over-exploration): firmware/test/test_runner.c main() sorts all registered tests by name (qsort + strcmp) and drives each through a setjmp/longjmp loop. On a failed assertion the body longjmps back and main() prints a neutral 'RUN: (assertion failed)' line, then advances to the next test. The Makefile globs every firmware/test/test_*.c automatically — NO Makefile edit needed.\n\nSTEPS:\n- Create firmware/test/test_bf38e9_verify.c (NEW file) with exactly two TEST() entries (from test_runner.h):\n 1. A FAILING test whose name sorts FIRST: TEST(aaa_bf38e9_intentional_fail) { ASSERT_EQ(1, 2); }\n 2. A companion PASSING test whose name sorts AFTER the failing one, IN THE SAME FILE (so the ordering is self-contained and deterministic regardless of other linked test files): TEST(aab_bf38e9_passes_after_fail) { ASSERT_EQ(1, 1); }\n Include test_runner.h at the top. Keep the file minimal.\n- Compile-check: 'make -C firmware/test build' must succeed with NO warnings (gcc -std=c11 -Wall -Wextra).\n\nACCEPTANCE CRITERIA:\n- firmware/test/test_bf38e9_verify.c exists with both TESTs (failing aaa_ one + passing aab_ one).\n- 'make -C firmware/test build' succeeds cleanly (no warnings, no errors).\n- No edits to firmware/main/*, firmware/CMakeLists.txt, the Makefile, or any existing test file.\n\nThis file is THROWAWAY — do NOT commit it; child 4 deletes it. Just create it and confirm it builds.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T19:37:44.261070907Z","updated_at":"2026-07-03T19:52:16.091054508Z","closed_at":"2026-07-03T19:52:16.091054508Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"comments":[{"id":18,"issue_id":"bf-1z44","author":"cli","text":"Created throwaway interleaving-verify artifact firmware/test/test_bf38e9_verify.c (NOT committed — throwaway; child 4 deletes it). Exactly two self-registering TEST()s in one self-contained file: (1) aaa_bf38e9_intentional_fail { ASSERT_EQ(1, 2); } sorts FIRST and intentionally fails; (2) aab_bf38e9_passes_after_fail { ASSERT_EQ(1, 1); } sorts immediately after and must still run+pass — pinning the recovery loop's failing-then-passing ordering deterministically regardless of other linked test files. Verified: 'make -C firmware/test build' compiles cleanly with gcc -std=c11 -Wall -Wextra (no warnings/errors). Running the suite confirms the contract: the failing test prints 'RUN: aaa_bf38e9_intentional_fail (assertion failed)' and longjmps out, then the passing test runs next (neutral RUN marker) and the rest of the suite continues. No edits to firmware/main/*, CMakeLists.txt, the Makefile, or any existing test file.","created_at":"2026-07-03T19:51:37.627799010Z"}]} -{"id":"bf-1zew","title":"Add main() skeleton with run-command comment and registry iteration","description":"Split-child 1 of 3 for parent bf-1na (Add main() entry-point skeleton with setjmp recovery loop and run-command header). bf-1na is itself child 1 of the bf-bq9 sub-split; depends on sibling bf-3id (per-test failure recovery: g_test_jmp + g_failure_count + test_record_failure, already landed). This child is the foundation slice of main(): establish the entry point, document the run command, and prove the registry iterates end to end — WITHOUT the setjmp recovery yet (a failing assertion would currently abort the process; that non-fatal isolation is child 2).\n\nScope (extend firmware/test/test_runner.c, the file holding g_tests[]/g_test_count/test_register()/test_record_failure()/g_test_jmp from bf-uvv/bf-oe1/bf-3id):\n- Add a comment block immediately above int main(void) naming the documented run command: `make -C firmware/test test` (per the bf-1xs header contract and the bf-56v gcc-harness decision record). make compiles every test_*.c plus this runner with plain gcc and runs the binary; main()'s exit code is what make propagates.\n- Implement int main(void): declare a local int passed = 0 and int failed = 0 (the failed counter is unused at this slice but reserved for child 2/3 — mark it (void)failed; or __attribute__((unused)) so -Wall -Wextra stays clean). Loop i over [0, g_test_count) and, for each registered test, call g_tests[i].fn() directly, then print one observable line per test (e.g. printf(\"RUN: %s\\n\", g_tests[i].name);) so iteration is verifiable before recovery lands. Bump passed after each call.\n- return 0; — the non-zero-on-failure exit code is explicitly a LATER sibling, out of scope for bf-1na and for this child.\n\nAcceptance criteria:\n- firmware/test/test_runner.c has an int main(void) preceded by a comment block documenting the run command `make -C firmware/test test`.\n- main() iterates g_tests[0..g_test_count) and invokes each registered g_tests[i].fn(), printing one line per test.\n- Compiles cleanly with gcc -std=c11 -Wall -Wextra; with at least one TEST() defined (e.g. the existing test_sanity.c) it links, runs, prints a line per registered test, and exits 0.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T15:19:21.947004217Z","updated_at":"2026-07-03T15:29:25.234022834Z","closed_at":"2026-07-03T15:29:25.234022834Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"comments":[{"id":13,"issue_id":"bf-1zew","author":"cli","text":"Verified and delivered main() for firmware/test/test_runner.c.\n\nThe foundation slice this bead scopes (comment block documenting the run command, iterate g_tests[0..g_test_count), invoke each g_tests[i].fn(), print one line per test, return) was already present in the committed tree as a STRICT SUPERSET: commit 549dc1f landed the full main() — name-sort via qsort, the per-test setjmp/longjmp recovery loop fed by sibling bf-3id's g_test_jmp/test_record_failure, one-line PASS/FAIL per test, run summary, and the non-zero-on-failure exit code.\n\nDid NOT regress main() down to the bare foundation slice — that would rip out the per-test failure isolation bf-3id established and that main() already wires up, leaving a broken intermediate where a failing assertion aborts the whole process. The committed superset satisfies every acceptance criterion, so churning it would only harm working code.\n\nAcceptance criteria, all verified:\n- int main(void) at test_runner.c:229, preceded by a comment block (lines 197-228) documenting 'make -C firmware/test test'.\n- main() iterates g_tests[0..g_test_count), invokes each g_tests[i].fn(), prints one line per test.\n- 'make -C firmware/test clean test' compiles cleanly with gcc -std=c11 -Wall -Wextra (zero warnings), links test_sanity.c + test_runner.c, runs, prints 'PASS: arithmetic_sanity' + '1 passed, 0 failed of 1', exits 0.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.\n\nDelivery: 549dc1f was committed locally but unpushed (origin/main was at 8325070). Pushed it — origin/main now 549dc1f. Staged Makefile/test_sanity.c/.gitignore (sibling beads bf-56v/bf-4ne) left untouched in the working tree; a plain push only delivers commits.","created_at":"2026-07-03T15:28:36.316334187Z"}]} -{"id":"bf-21t","title":"Decide firmware host test approach (ESP-IDF linux vs gcc host harness)","description":"Investigation/decision spike for the firmware/test/ host-based test harness (part of split of bf-4ne). Resolves the one open uncertainty that made the parent thrash.\n\nGoal: decide, BEFORE building the harness, whether the ESP-IDF host-based path (idf.py test --target linux / Unity host) can actually compile the firmware/main/*.c source files, given they pull in esp_wifi.h, freertos/*, driver/uart.h, nvs_flash.h, esp_log.h.\n\nSteps:\n- Attempt (or reason concretely about) compiling at least nvs_migration.c, csi.c, provision.c for the ESP-IDF linux target. Note which includes break it.\n- If the esp_* driver/freertos dependencies make the linux target impractical (the expected outcome), decide on the gcc host harness fallback: a plain gcc-compiled test runner under firmware/test/ that does NOT link firmware/main/*.c directly.\n- Carry the chosen approach + the concrete reason forward into child 2 (the harness build-out), and document it in the test runner header comment there.\n\nThis is a NO-CODE-CHANGE decision child. Do NOT build the harness here.\n\nAcceptance criteria:\n- A clear written decision exists: either \"ESP-IDF linux target\" or \"gcc host harness\", with the concrete reason (which includes/dependencies block the linux target).\n- The decision is explicitly carried into child 2.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T09:31:47.135106434Z","updated_at":"2026-07-03T10:10:55.769754443Z","closed_at":"2026-07-03T10:10:55.769754443Z","close_reason":"Completed: decision documented in docs/notes/firmware-host-test-approach.md (commit 9a3305b). gcc host harness chosen; csi.c blocked by esp_wifi.h, provision.c by driver/uart.h, main REQUIRES esp_wifi/bt/driver (no linux build). Carry-forward section added for bf-4ne. Auto-split was a false trigger — br close failed on the space-separated claimed_at format bug, not on complexity; normalized and closed.","source_repo":".","compaction_level":0,"labels":["deferred","split-child"]} -{"id":"bf-22vg","title":"Wrap each test body in a setjmp(g_test_jmp) guard so it runs only on the direct return","description":"Split-child 1 of 3 for parent bf-53ut (which is itself split-child 2 of 3 for bf-1na). This child is the actual code change that delivers the parent's core contract: drive each test through the per-test setjmp/longjmp recovery loop.\n\nStarting state (from bf-1zew, parent bf-1na's child 1): main()'s per-test loop is the naive iteration that prints the RUN line then calls the body directly:\n\n for (int i = 0; i < g_test_count; i++) {\n printf(\"RUN: %s\\n\", g_tests[i].name);\n g_tests[i].fn();\n }\n\nScope (firmware/test/test_runner.c, main()'s per-test loop only):\n- Before invoking the body, establish the per-test longjmp target by calling setjmp(g_test_jmp) — the file-scope jmp_buf added by sibling bf-3id. Gate the body call on its return: on the DIRECT call setjmp returns 0 and the body runs; replace the bare g_tests[i].fn() with `if (setjmp(g_test_jmp) == 0) { g_tests[i].fn(); }`.\n- Keep the existing RUN: printf BEFORE the setjmp (do not move or reorder it) so the one observable line per test still prints regardless of how the body ends.\n- Do NOT add outcome labels (PASS/FAIL) or any failure/pass tally counter here — those land in bf-1na's child 3, which is explicitly out of scope for this lineage. This child only wires the recovery target and gates the body on the 0 return; the non-zero path simply falls through (no else branch, or an empty/comment-only one).\n- The loop's for-increment advances i either way, so a failure in test N never blocks N+1..end.\n\nAcceptance criteria:\n- main()'s per-test loop calls g_tests[i].fn() only inside `if (setjmp(g_test_jmp) == 0)`; the RUN: line still prints before the setjmp.\n- Compiles cleanly with gcc -std=c11 -Wall -Wextra (no setjmp-related warnings such as -Wclobbered).\n- With an all-passing suite, `make -C firmware/test test` still runs every test and the process completes (behavior unchanged for passing tests).\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","created_at":"2026-07-03T23:37:18.898935971Z","updated_at":"2026-07-04T06:39:34.852082917Z","closed_at":"2026-07-04T06:39:34.852082917Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-22vg","depends_on_id":"bf-tof1","type":"blocks","created_at":"2026-07-04T00:11:21.859707997Z","created_by":"cli","thread_id":""}],"comments":[{"id":58,"issue_id":"bf-22vg","author":"cli","text":"Closing: all four acceptance criteria verified by child bf-tof1 (final, verify-and-confirm).\n- Per-test loop gates the body on if (setjmp(g_test_jmp) == 0) { g_tests[i].fn(); }; RUN: printf precedes setjmp.\n- Compiles cleanly under gcc -std=c11 -Wall -Wextra with zero warnings (no -Wclobbered) — volatile-qualified loop index (bf-2k3o) is the C11 7.13.2.1 remedy.\n- make -C firmware/test test exits 0; all 29 RUN: lines print in sorted order; process completes; behavior unchanged for the all-passing suite. Bidirectional registered-vs-RUN set diff IDENTICAL.\n- No changes to firmware/main/* or firmware/CMakeLists.txt in the lineage.\nOnly dependency (bf-tof1) is closed; parent acceptance satisfied.","created_at":"2026-07-04T06:39:34.814851561Z"}]} -{"id":"bf-25r","title":"Add serial provisioning JSON parser host test","description":"Part of split of bf-brz. Depends on the CSI frame test child. This is the LAST child; closing it unblocks the parent umbrella.\n\nGoal: add a host-based unit test for the serial provisioning JSON parse/validate logic in firmware/main/provision.c.\n\nContext: provision_listen_window() reads lines from UART and uses cJSON. The testable pure logic is the per-line decision: valid {\"provision\":{...}} -> success (writes NVS, would respond {\"ok\":true,\"mac\":...}); invalid JSON -> {\"ok\":false,\"error\":\"invalid_json\"}; valid JSON missing the provision key -> {\"ok\":false,\"error\":\"missing_provision_key\"}; NVS write failure -> {\"ok\":false,\"error\":\"nvs_write_failed\"}. cJSON itself is pure C and host-compilable.\n\nApproach:\n- Extract the line-parse decision into a pure host-compilable function (e.g. provision_parse_line(const char *line) -> result enum / error string), factored out of provision_listen_window. NVS writes can be stubbed for the host test. Keep production provision.c behavior identical.\n- Add at least one valid provisioning JSON case and at least one invalid JSON case (and ideally the missing-provision-key case).\n\nAcceptance criteria:\n- A test under firmware/test/ covers at least one valid + one invalid provisioning JSON case, asserting the ok:true / ok:false (invalid_json) decision.\n- The full firmware/test/ suite runs green under the single documented command and exits non-zero on failure.\n- firmware/main/ production behavior is unchanged (extraction is refactor-only) and the firmware build still succeeds.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T05:49:04.272518851Z","updated_at":"2026-07-03T05:49:04.272518851Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-25r","depends_on_id":"bf-16j","type":"blocks","created_at":"2026-07-03T05:50:38.038903789Z","created_by":"cli","thread_id":""}]} -{"id":"bf-26fi","title":"Build and run the firmware test suite, capturing stdout","description":"Split-child of parent bf-ba2u (auto-split: parent failed 3x on timing/out-exploration).\n\nGOAL: produce a captured, reusable stdout from the full test run so downstream children can grep it WITHOUT re-running the suite (the parent kept re-running and timing out).\n\nSTEPS:\n- Run: make -C firmware/test test and capture stdout+stderr to a file:\n make -C firmware/test test > /tmp/bf38e9_test_stdout.txt 2>&1\n (make's exit code is NONZERO because one test intentionally fails — that is EXPECTED and is NOT a failure of this child; the child succeeds as long as the suite builds and runs.)\n- Confirm /tmp/bf38e9_test_stdout.txt exists and is non-empty.\n\nACCEPTANCE CRITERIA:\n- /tmp/bf38e9_test_stdout.txt exists and is non-empty.\n- The output shows the test runner actually executing tests (not a compile/link error).\n- No source changes (this child only builds and runs).\n\nIf the suite does not build (compile/link error), that is a REAL problem — STOP and report it; do not edit source to mask it.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T20:25:29.258581134Z","updated_at":"2026-07-03T20:31:27.603984691Z","closed_at":"2026-07-03T20:31:27.603984691Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"comments":[{"id":19,"issue_id":"bf-26fi","author":"cli","text":"Ran the firmware host-test suite and captured full stdout+stderr to /tmp/bf38e9_test_stdout.txt (1554 bytes, 37 lines, ASCII). Deliverable for downstream children to grep WITHOUT re-running the suite.\n\nResult: build succeeded, runner executed all 31 tests -> '30 passed, 1 failed of 31'. Nonzero make exit (code 2) is EXPECTED and by-design: the single failure is the intentional sentinel test aaa_bf38e9_intentional_fail (test_bf38e9_verify.c ASSERT_EQ(1,2)). All real modules green: csi frame serialization (header round-trip, little-endian timestamp, signed-RSSI, I/Q payload, n_sub=0 probe, ingestion validation), nvs_migration (fresh-init v1, no-downgrade guard, multi-step dispatch, v1->v2 rename/default), and serial_prov (parser + random-byte/deep-nesting fuzz never crash).\n\nNo source changes (build-and-run only child). The captured file is the reusable artifact; sibling children can grep it instead of rebuilding.","created_at":"2026-07-03T20:30:37.572497220Z"}]} -{"id":"bf-27ud","title":"Wrap the test body call in a setjmp(g_test_jmp) guard gated on the zero return","description":"Split-child 2 of 4 for parent bf-22vg. The core code change. Once child 1 has restored the naive loop, this child wires the per-test recovery target.\n\nScope: firmware/test/test_runner.c, main() per-test loop only. Depends on sibling bf-3id (file-scope static jmp_buf g_test_jmp) and on child 1 (naive starting state).\n\nDo:\n- Before invoking the body, establish the per-test longjmp target by calling setjmp(g_test_jmp).\n- Gate the body call on the direct (zero) return: replace the bare g_tests[i].fn() with: if (setjmp(g_test_jmp) == 0) { g_tests[i].fn(); }.\n- Keep the existing RUN: printf BEFORE the setjmp (do not move or reorder it), so the one observable line per test still prints regardless of how the body ends.\n- The non-zero (longjmp) path simply falls through: no else branch, or an empty/comment-only one. Do NOT add it as executable code.\n\nDo NOT (out of scope, sibling bf-1na child 3):\n- Add PASS/FAIL labels, a pass/fail tally, a summary line, or any non-zero exit-code logic. This child only wires the recovery target and gates the body.\n\nAcceptance criteria:\n- main() per-test loop calls g_tests[i].fn() only inside if (setjmp(g_test_jmp) == 0); the RUN: line still prints before the setjmp; the non-zero path falls through with no executable else body.\n- The loop for-increment still advances i either way, so a failure in test N never blocks N+1..end.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T00:10:57.709630023Z","updated_at":"2026-07-04T00:51:21.270951097Z","closed_at":"2026-07-04T00:51:21.270951097Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-27ud","depends_on_id":"bf-1fd4","type":"blocks","created_at":"2026-07-04T00:11:20.863506419Z","created_by":"cli","thread_id":""}],"comments":[{"id":30,"issue_id":"bf-27ud","author":"cli","text":"Wrapped the per-test g_tests[i].fn() call in main()'s loop with if (setjmp(g_test_jmp) == 0), establishing the per-test longjmp recovery target (declared in bf-3id) before each body runs. RUN: printf stays before the setjmp so the per-test marker prints regardless of outcome; the non-zero (longjmp) return falls through with no else body, so the loop's i++ still advances and a failure in test N never blocks N+1..end. PASS/FAIL labels, tallies, summary line, and non-zero exit remain sibling scope (child 3, bf-1na) — main() still returns 0 unconditionally. Verified: make -C firmware/test test builds cleanly and all 30 tests run through the guard, exit code 0. Only firmware/test/test_runner.c changed; no firmware/main or CMakeLists edits. Committed b62fa3e and pushed to origin/main.","created_at":"2026-07-04T00:51:38.323161299Z"}]} -{"id":"bf-2ftl","title":"Wrap the test body call in a setjmp(g_test_jmp) == 0 guard","description":"Split-child 1 of 3 for parent bf-53ut (Wrap each test in per-test setjmp/longjmp recovery so a failing assertion is non-fatal). This child installs the structural skeleton of the per-test recovery loop: it puts a setjmp(g_test_jmp) guard in front of the existing direct body call landed by sibling bf-1zew (run-command comment + naive registry iteration calling each g_tests[i].fn() directly), so that control returns here — rather than aborting — when a failed assertion inside the body calls test_record_failure() (which longjmp(g_test_jmp, 1) — sibling bf-3id).\n\nScope (extend firmware/test/test_runner.c, main()'s per-test loop):\n- In main()'s loop, replace the bare g_tests[i].fn(); call with a guarded form: if (setjmp(g_test_jmp) == 0) { g_tests[i].fn(); }. On the DIRECT call setjmp returns 0 and the body runs as before; if the body trips test_record_failure(), the longjmp returns control to the setjmp call site with a non-zero value instead.\n- On the non-zero (longjmp) return, do NOT re-invoke the body — leave the else branch empty (a bare comment is fine) for now. Just fall through so the loop advances to i+1. (Observability on the failure path lands in child 2; outcome PASS/FAIL labels land in a later sibling — do not add either here.)\n- Either way the loop advances to the next i, so a failure in test N never blocks tests N+1..end. That per-test isolation is the whole point.\n\nImplementation note (the classic setjmp trap — C11 7.13.2.1): any automatic variable modified between setjmp and longjmp AND read after the longjmp returns must be volatile-qualified, else its value is indeterminate. The loop index i is read in the post-longjmp path; confirm it is NOT modified between the setjmp call and the longjmp (it is only incremented by the for-loop increment, which runs AFTER control returns), so no volatile is needed. Record this reasoning in a comment next to the setjmp.\n\nAcceptance criteria:\n- main()'s loop calls setjmp(g_test_jmp) before each body and invokes g_tests[i].fn() ONLY on the 0 return; on a longjmp return it advances without re-calling.\n- A comment near the setjmp records the volatile-analysis (i is not modified between setjmp and longjmp, so no volatile is needed).\n- Compiles cleanly with gcc -std=c11 -Wall -Wextra (no setjmp-related warnings).\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T16:02:04.257837384Z","updated_at":"2026-07-03T16:43:46.017591389Z","closed_at":"2026-07-03T16:43:46.017591389Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"comments":[{"id":14,"issue_id":"bf-2ftl","author":"cli","text":"Landed the per-test setjmp(g_test_jmp)==0 guard skeleton + C11 7.13.2.1 volatile-analysis comment in firmware/test/test_runner.c main() loop. The setjmp guard itself was already committed by an earlier single-pass bead (bf-bq9, in the file header), so this bead's incremental contribution is the volatile comment documenting that loop index i — read post-longjmp — is never modified between setjmp and longjmp (body only reads i; sole write is the for-loop increment after control returns), hence no volatile needed. Did NOT regress the pre-existing else-branch FAIL print, run summary, or non-zero exit code to a bare skeleton — those are committed bf-bq9 functionality the file header documents as the CI contract (make -C firmware/test test propagates non-zero on failure). Verified: clean gcc -std=c11 -Wall -Wextra compile (zero warnings); recovery loop confirmed via a temp failing+passing test pair (failure longjmp'd, next test still ran and PASSED, exit non-zero); no firmware/main or CMakeLists changes. Commit 4b0eaba pushed to origin main.","created_at":"2026-07-03T16:44:06.010419474Z"}]} -{"id":"bf-2ftq","title":"Quote the RUN-line printf preceding the setjmp guard in main()'s loop","description":"Read-only static verification of firmware/test/test_runner.c only (NO code changes). Child 2 of 4 splitting bf-3fx4.\n\nScope: Inside main()'s per-test loop (the 'for (volatile int i = 0; i < g_test_count; i++)' loop located in child 1), find the printf that emits the per-test RUN marker. Confirm it appears BEFORE the 'if (setjmp(g_test_jmp) == 0)' guard — i.e. it is a sibling statement preceding the guard, not inside it — so the RUN line prints regardless of how the test body ends.\n\nAcceptance criteria: A comment on this bead quoting the exact line 'printf(\"RUN: %s\\n\", g_tests[i].name);' from firmware/test/test_runner.c and confirming it precedes the setjmp guard. Closes independently once quoted.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T04:09:01.541868923Z","updated_at":"2026-07-04T04:37:49.604155184Z","closed_at":"2026-07-04T04:37:49.604155184Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-2ftq","depends_on_id":"bf-3ybc","type":"blocks","created_at":"2026-07-04T04:09:16.718510330Z","created_by":"cli","thread_id":""}],"comments":[{"id":44,"issue_id":"bf-2ftq","author":"cli","text":"Read-only verification of firmware/test/test_runner.c complete (no code changes — child 2 of 4 splitting bf-3fx4).\n\nInside main()'s per-test loop (line 246: 'for (volatile int i = 0; i < g_test_count; i++)'), the per-test RUN marker is emitted by the exact line:\n\n printf(\"RUN: %s\\n\", g_tests[i].name);\n\nat firmware/test/test_runner.c:255. It is a sibling statement that PRECEDES the setjmp guard at line 256:\n\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\n\nBoth the printf and the if-block are direct children of the for-loop body, with the printf first — so the RUN line prints BEFORE the guard is ever evaluated, regardless of how the test body ends (normal return or longjmp from a failed assertion). Acceptance criteria met.","created_at":"2026-07-04T04:36:46.113591252Z"}]} -{"id":"bf-2i4","title":"Implement firmware/test/ runner main(), registry, and failure handling","description":"Child 2 of the bf-56v harness split. Depends on child 1 (bf-1xs: test_runner.h API). Implement the runner that drives the self-registered tests.\n\nScope: create firmware/test/test_runner.c implementing:\n- A fixed-capacity registry (static array of {name, fn}, capacity e.g. 128) with test_register() pushing entries in construction order.\n- test_record_failure(file, line, fmt, ...): prints the assertion detail to stderr, then longjmp()s into the per-test setjmp target so the current test aborts but remaining tests still run.\n- main(): sorts the registered tests by name for deterministic order regardless of constructor/link order; for each test, setjmp() then call fn(); print PASS or FAIL; after all, print a summary and return 1 if any test failed, else 0. This non-zero-on-failure return is the contract CI relies on.\n\nAcceptance criteria:\n- firmware/test/test_runner.c implements test_register(), test_record_failure(), and main() against the test_runner.h API from child 1.\n- main() returns non-zero (1) iff at least one registered test called test_record_failure; 0 otherwise.\n- Tests run in deterministic (name-sorted) order.\n- A failing assertion in one test does not prevent later tests from running (per-test longjmp).\n- Compiles cleanly with gcc -std=c11 -Wall -Wextra against test_runner.h. (Note: nothing to link against yet — no test_*.c exists until child 4; compiling test_runner.c to an object is the bar here.)\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T10:48:03.159497099Z","updated_at":"2026-07-03T11:37:07.998211953Z","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-2i4","depends_on_id":"bf-1xs","type":"blocks","created_at":"2026-07-03T10:50:27.891881332Z","created_by":"cli","thread_id":""},{"issue_id":"bf-2i4","depends_on_id":"bf-bq9","type":"blocks","created_at":"2026-07-03T11:36:32.039716272Z","created_by":"cli","thread_id":""}]} -{"id":"bf-2k3o","title":"Make the setjmp guard compile warning-free under gcc -std=c11 -Wall -Wextra","description":"Split-child 3 of 4 for parent bf-22vg. The compile-cleanliness gate. setjmp inside a loop with automatic variables (the loop index i, and any other automatic that spans the setjmp/longjmp) routinely trips gcc -Wclobbered under -Wall -Wextra. The parent acceptance criteria require zero setjmp-related warnings, and this is the most common reason a setjmp/longjmp change fails to land. Depends on child 2 (the guard must exist first).\n\nScope: firmware/test/test_runner.c, only what is needed to clear setjmp warnings in/around main() per-test loop. No behavior change.\n\nDo:\n- Compile firmware/test/test_runner.c (and the test build) with gcc -std=c11 -Wall -Wextra and confirm there are no -Wclobbered or other setjmp-related warnings.\n- If -Wclobbered fires (typically on the loop index i or an automatic read after a possible longjmp return), fix it correctly per C11 7.13.2.1: volatile-qualify the affected automatic, or restructure so no non-volatile automatic is both modified before a possible longjmp and read after it. Do NOT silence the warning with a pragma or by downgrading flags.\n\nDo NOT (out of scope):\n- Change the guard logic itself (child 2), add outcome plumbing (sibling), or touch other modules.\n\nAcceptance criteria:\n- gcc -std=c11 -Wall -Wextra produces zero warnings (in particular no -Wclobbered) on firmware/test/test_runner.c.\n- Any volatile-qualification added is genuinely required and accompanied by a one-line rationale comment; no pragma/flag suppression.\n- make -C firmware/test test still builds and runs.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T00:11:06.676416580Z","updated_at":"2026-07-04T01:29:00.180143672Z","closed_at":"2026-07-04T01:29:00.180143672Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-2k3o","depends_on_id":"bf-27ud","type":"blocks","created_at":"2026-07-04T00:11:21.119648624Z","created_by":"cli","thread_id":""}],"comments":[{"id":31,"issue_id":"bf-2k3o","author":"cli","text":"Closed without splitting — this was a false auto-split (same pattern as bf-21t). The work was already complete and committed in ce70491 (volatile-qualified loop index i in main()'s setjmp loop, with rationale comment). Verified: gcc -std=c11 -Wall -Wextra -Wclobbered is warning-free, make -C firmware/test test builds clean and runs all tests (exit 0), only firmware/test/test_runner.c touched (no firmware/main or CMakeLists changes). The 'failure-count:3' was 3x agent timeouts (exit 124, 600s each) that occurred AFTER the fix landed but BEFORE br close ran — the worker kept timing out re-verifying already-done work. br close itself was blocked by the known claimed_at-not-RFC3339 bug (4 stale claim rows from the retries, 10min apart); normalized them to RFC3339 and closed.","created_at":"2026-07-04T01:29:35.341928538Z"}]} -{"id":"bf-2m9s","title":"Record npm ci outcome comment on parent bead","description":"Scope: discovery only — NO source fixes. Distill child 2's raw output into the deliverable the parent bead asks for.\n\nSteps:\n1. Read child 2's captured npm ci output.\n2. If success: post a comment on bf-licg recording 'success' plus node_modules/node version context.\n3. If failure: post a comment on bf-licg with the EXACT verbatim blocking error.\n\nAcceptance criteria:\n- The npm ci outcome (success, or verbatim blocking error) is documented in a comment on parent bead bf-licg.\n- No source changes (package.json / package-lock.json untouched).\n- Does NOT run jest or build the table — that is downstream of the parent.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T11:22:02.865094256Z","updated_at":"2026-07-04T11:22:29.588460722Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-2m9s","depends_on_id":"bf-121s","type":"blocks","created_at":"2026-07-04T11:22:29.577932772Z","created_by":"cli","thread_id":""}]} -{"id":"bf-2opw","title":"Confirm the per-test loop body matches the bf-22vg contract","description":"Split-child 2 of 4 for umbrella bf-tof1. Static inspection of main()'s per-test loop in firmware/test/test_runner.c: confirm the loop body matches the parent (bf-22vg) contract EXACTLY — no outcome plumbing in this lineage.\n\nScope (no code changes; read-only verification of firmware/test/test_runner.c only):\n- The RUN: printf (printf(\"RUN: %s\\n\", g_tests[i].name)) appears BEFORE the setjmp, so it prints regardless of how the body ends.\n- The body call g_tests[i].fn() is inside: if (setjmp(g_test_jmp) == 0) { ... }\n- The non-zero (longjmp) return path simply falls through to the next iteration: NO executable else branch, OR an empty/comment-only one.\n- NO PASS/FAIL outcome labels, NO pass/fail tally counter, NO run summary line, and NO non-zero exit logic are present in this lineage (main() returns 0 unconditionally; g_failure_count is not read here). These belong to sibling bf-1na's child 3 and are explicitly out of scope.\n- Note the volatile qualifier on the loop index is expected (compile-cleanliness gate for -Wclobbered) and is NOT an outcome concern.\n\nAcceptance criteria:\n- A comment on this bead quoting the exact loop body (the for(...) header through the closing brace) from firmware/test/test_runner.c.\n- The comment confirms each of: RUN precedes setjmp; body inside the if (setjmp==0); no executable else; no PASS/FAIL/tally/summary/non-zero-exit.\n- Closes independently once the contract match is confirmed.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T01:51:49.446221438Z","updated_at":"2026-07-04T06:16:57.420937601Z","closed_at":"2026-07-04T06:16:57.420937601Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-2opw","depends_on_id":"bf-1580","type":"blocks","created_at":"2026-07-04T01:52:39.534770527Z","created_by":"cli","thread_id":""},{"issue_id":"bf-2opw","depends_on_id":"bf-1tae","type":"blocks","created_at":"2026-07-04T02:43:50.432818281Z","created_by":"cli","thread_id":""}],"comments":[{"id":34,"issue_id":"bf-2opw","author":"cli","text":"Contract match CONFIRMED. Exact loop body from firmware/test/test_runner.c:246-259 (for(...) header through closing brace), executable statements:\n\n for (volatile int i = 0; i < g_test_count; i++) {\n printf(\"RUN: %s\\n\", g_tests[i].name);\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\n }\n return 0;\n\nPer-criterion confirmation:\n1. RUN precedes setjmp — CONFIRMED. printf(\"RUN: %%s\\n\", ...) is line 255, the setjmp guard is line 256, so the RUN marker prints regardless of how the body ends (it is outside the setjmp recovery scope).\n2. Body inside if (setjmp==0) — CONFIRMED. g_tests[i].fn() (line 257) is the sole statement inside the if (setjmp(g_test_jmp) == 0) { ... } block.\n3. No executable else — CONFIRMED. There is no else branch at all (not even an empty/comment-only one). On a non-zero (longjmp) return the if-body is skipped and control falls straight through to the for-loop's i++ and the next iteration.\n4. No PASS/FAIL/tally/summary/non-zero-exit — CONFIRMED. No PASS/FAIL printf, no pass/fail tally counter in main(), no run summary line. main() returns 0 unconditionally (line 261); the file-scope g_failure_count (bumped only by test_record_failure()) is NOT read in this lineage. All outcome plumbing belongs to sibling bf-1na's child 3 and is correctly absent here.\n5. volatile qualifier on the loop index (for (volatile int i = 0; ...)) is present and expected — the -Wclobbered compile-cleanliness gate, NOT an outcome concern.\n\nmain() also has a preceding qsort(g_tests, ...) for deterministic name ordering (line 230) which is orthogonal to the loop contract. The inline comment block inside the loop only restates the contract; it is not executable outcome plumbing. bf-22vg contract matches EXACTLY.","created_at":"2026-07-04T02:24:06.071422206Z"},{"id":54,"issue_id":"bf-2opw","author":"cli","text":"Contract verification (bf-22vg) — CONFIRMED.\n\nThe per-test loop body in firmware/test/test_runner.c main() (lines 246-259) matches the parent bf-22vg contract exactly. Quoted verbatim, for(...) header through the closing brace:\n\n for (volatile int i = 0; i < g_test_count; i++) {\n /*\n * The RUN line is the one observable line per test and prints BEFORE the\n * setjmp, so it appears regardless of how the body ends. The body itself\n * runs only on the direct (zero) setjmp return; a non-zero return is a\n * longjmp from a failed assertion and simply falls through to the next\n * iteration — no else body — so the loop's i++ still advances and a\n * failure in test N never blocks N+1..end.\n */\n printf(\"RUN: %s\\n\", g_tests[i].name);\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\n }\n\nConfirmation of each contract point:\n\n1. RUN precedes setjmp — CONFIRMED. printf(\"RUN: %s\\n\", g_tests[i].name) is the first statement in the loop body and is immediately followed by the setjmp guard, so the RUN line prints regardless of how the body ends.\n\n2. Body inside if (setjmp==0) — CONFIRMED. g_tests[i].fn() is the sole statement within the if (setjmp(g_test_jmp) == 0) { ... } block.\n\n3. No executable else — CONFIRMED. The if block has no else branch at all (not even an empty/comment-only one); on the non-zero (longjmp) return path control falls straight through to the for-loop closing brace and i++ advances to the next iteration.\n\n4. No outcome plumbing — CONFIRMED. There are no PASS/FAIL labels, no pass/fail tally counter, and no run summary line anywhere in main(); main() returns 0 unconditionally (line 261) with no non-zero exit logic, and g_failure_count is not read here. Those concerns belong to sibling bf-1na child 3 and are explicitly out of scope for this lineage.\n\nThe volatile qualifier on the loop index (for (volatile int i = 0; ...)) is the expected -Wclobbered compile-cleanliness gate and is not an outcome concern, as the task notes.\n\nRead-only verification — no code changes.","created_at":"2026-07-04T06:16:01.942926821Z"}]} -{"id":"bf-2s2","title":"Mark Dashboard presence indicator Done in PROGRESS.md","description":"Phase 2 table in PROGRESS.md shows \"Dashboard presence indicator\" as Pending, but it is implemented in dashboard/live.html and dashboard/js/app.js. Update the Phase 2 table row (PROGRESS.md:29) to Done, and remove it from the \"Remaining for Phase 2\" list (PROGRESS.md:94).\n\nAcceptance criteria:\n- PROGRESS.md Phase 2 table row for \"Dashboard presence indicator\" reads Done (not Pending).\n- The item is removed from the \"Remaining for Phase 2\" list.\n- dashboard/live.html and dashboard/js/app.js still contain the presence indicator code (unchanged).","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T04:00:54.961143771Z","updated_at":"2026-07-03T04:07:13.081995222Z","closed_at":"2026-07-03T04:07:13.081995222Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"comments":[{"id":1,"issue_id":"bf-2s2","author":"cli","text":"Closed: Marked 'Dashboard presence indicator' as Done in PROGRESS.md Phase 2 table (was Pending) and removed it from the 'Remaining for Phase 2' list. Presence indicator code is implemented in dashboard/live.html (#presence-indicator styling) and dashboard/js/app.js (updatePresenceIndicator, presence panel) and was left unchanged. Docs-only change; no Go code modified. Committed (9fabeb8) and pushed.","created_at":"2026-07-03T04:04:25.111190131Z"}]} -{"id":"bf-31n","title":"Add root README.md (repo standard, GitHub mirror landing page)","description":"The repo has NO README.md at the root (only dashboard/README.md exists). The workspace repo standard requires a root README.md explaining the repo's purpose and contents, and the GitHub mirror (github.com/jedarden/spaxel) renders an empty landing page without it.\n\nWrite a root README.md covering:\n- What Spaxel is (WiFi CSI-based indoor positioning for self-hosted homes; single Docker 'mothership' container + ESP32-S3 node fleet) — source the overview from docs/plan/plan.md System Overview and 'What Spaxel Can Realistically Achieve' sections\n- Repo layout: mothership/ (Go backend, go.work module), cmd/sim (CSI simulator CLI module), firmware/ (ESP-IDF C project), dashboard/ (vanilla JS + Three.js UI), test/acceptance (acceptance test module), docs/ (plan, notes, research)\n- Quickstart: docker-compose.yml usage, key env vars (SPAXEL_BIND_ADDR, SPAXEL_DATA_DIR, SPAXEL_MQTT_BROKER) per plan Deployment section\n- Pointers to docs/plan/plan.md, docs/notes/, docs/research/, dashboard/README.md\n- Realistic-capability and privacy framing (CSI-only, no camera/audio, self-hosted, no cloud)\n\nAcceptance criteria:\n- README.md exists at repo root, accurate to the CURRENT tree (go.work multi-module layout, image ronaldraygun/spaxel), no secrets, no absolute filesystem paths\n- Under ~150 lines; links resolve to files that actually exist","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-02T11:28:22.969816001Z","updated_at":"2026-07-03T04:45:29.751430671Z","closed_at":"2026-07-03T04:45:29.751430671Z","close_reason":"Completed","source_repo":".","compaction_level":0,"comments":[{"id":4,"issue_id":"bf-31n","author":"cli","text":"Added root README.md (97 lines) as the repo-standard landing page for the GitHub mirror. Covers: Spaxel overview (single Docker mothership + ESP32-S3 node fleet); realistic CSI capabilities + physics limits from docs/plan/plan.md and docs/research/06-accuracy-and-limits.md; CSI-only privacy framing (no camera/audio, self-hosted, no cloud); go.work multi-module repo layout (mothership/, cmd/sim, test/acceptance, firmware/, dashboard/); docker-compose quickstart with key env vars (SPAXEL_BIND_ADDR, SPAXEL_DATA_DIR, SPAXEL_MQTT_BROKER, TZ, SPAXEL_MDNS_ENABLED); build/dev commands; pointers to docs/plan, docs/notes, docs/research, dashboard/README.md, PROGRESS.md. Accurate to current tree (image ronaldraygun/spaxel, VERSION 0.1.357). All 17 linked paths verified to resolve. No secrets, no host filesystem paths. Committed c53bd96, pushed to origin main.","created_at":"2026-07-03T04:43:28.271553226Z"}]} -{"id":"bf-31q","title":"Add nvs schema migration host test","description":"Part of split of bf-brz. Depends on the harness from the scaffolding child.\n\nGoal: add a host-based unit test for the NVS schema migration logic in firmware/main/nvs_migration.c.\n\nContext (important): nvs_migration_run() has heavy esp_*/nvs_flash dependencies and cannot be compiled directly on a host. The testable pure logic is the migration DECISION table: reading/initializing schema_ver, the comparison against COMPILED_NVS_VERSION (already-up-to-date, newer-than-compiled/downgrade, needs-migration), and migrations[] index resolution (index = v - 1). Note COMPILED_NVS_VERSION is currently 1, and a missing schema_ver key initializes to 1, so the plan schema_ver=0-to-1 upgrade case is really the no-key-present -> initialize-to-v1 path.\n\nApproach:\n- Extract the version-compare / index-resolution / decision logic into a pure host-compilable function (e.g. takes current schema_ver + compiled_ver -> action enum), OR provide a minimal host shim/stub for the nvs_* get/set/commit calls backing nvs_migration_run(). Keep production nvs_migration.c semantics identical.\n- Add at least one schema_ver 0-to-1 migration case (missing key -> initialize to 1). Optionally also cover already-up-to-date (1==1) and the downgrade-caution branch.\n\nAcceptance criteria:\n- A test under firmware/test/ exercises the nvs migration decision logic with at least one schema_ver 0-to-1 (no-key -> init to 1) case.\n- Test runs under the harness single documented command and passes.\n- firmware/main/ production behavior is unchanged (extraction is refactor-only) and the firmware build still succeeds.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T05:48:53.058334862Z","updated_at":"2026-07-03T05:48:53.058334862Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-31q","depends_on_id":"bf-4ne","type":"blocks","created_at":"2026-07-03T05:50:34.291355239Z","created_by":"cli","thread_id":""}]} -{"id":"bf-31rd","title":"Verify no loop variable is clobbered across the setjmp/longjmp boundary and the file compiles clean","description":"Split-child 2 of 3 for parent bf-53ut. Depends on child 1 (bf-22vg, which added the `if (setjmp(g_test_jmp) == 0) { g_tests[i].fn(); }` guard). This child owns the implementation-note correctness concern for the parent: the classic setjmp volatile trap (C11 7.13.2.1).\n\nBackground: any automatic variable that is modified between setjmp and longjmp AND read after the longjmp returns has an indeterminate value unless it is volatile-qualified. Getting this wrong is silent corruption — the value of `i` or a counter read in the post-longjmp path could be garbage. This child confirms the structure child 1 wrote is safe, and locks it in with a documenting comment so a future editor cannot regress it.\n\nScope (firmware/test/test_runner.c, the per-test loop region):\n- Audit every automatic variable in scope across the setjmp/longjmp boundary in main()'s loop: the loop index `i`, and any local tallies (e.g. passed/failed if present). For each, confirm it is NOT both (a) written between the setjmp() call and a possible longjmp() fired inside the body AND (b) read in the post-longjmp path. The expected finding: `i` is written only by the for-loop increment, which runs AFTER control returns to the setjmp site (whether via normal return or longjmp), and the post-longjmp path does not read `i`; therefore no volatile qualifier is needed.\n- Add a concise comment at the setjmp call site recording this analysis (variables considered, why each is safe, the C11 7.13.2.1 reference) so the reasoning is preserved with the code.\n- Verify gcc actually agrees: build with gcc -std=c11 -Wall -Wextra and confirm there is NO -Wclobbered warning (that is the diagnostic that fires when a non-volatile automatic could be clobbered). If any -Wclobbered appears, resolve it by either restructuring so the variable is not read post-longjmp, or volatile-qualifying the variable — then re-confirm a clean build.\n\nAcceptance criteria:\n- A comment at the setjmp call site documents the volatile/clobber analysis with the C11 7.13.2.1 reference and the list of variables checked.\n- gcc -std=c11 -Wall -Wextra emits NO -Wclobbered (and no other setjmp-related) warning for test_runner.c.\n- No behavior change vs child 1 (this is analysis + comment + build verification only); `make -C firmware/test test` still completes on an all-passing suite.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T23:38:09.935937172Z","updated_at":"2026-07-04T07:13:58.287825197Z","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-31rd","depends_on_id":"bf-22vg","type":"blocks","created_at":"2026-07-03T23:39:13.032536731Z","created_by":"cli","thread_id":""},{"issue_id":"bf-31rd","depends_on_id":"bf-1ffd","type":"blocks","created_at":"2026-07-04T07:13:58.277810191Z","created_by":"cli","thread_id":""}]} -{"id":"bf-341v","title":"Wire npm test into spaxel-build CI template before a11y gate","description":"Scope: CI wiring in the OTHER repo — jedarden/declarative-config, path k8s/iad-ci/argo-workflows/, the spaxel-build WorkflowTemplate. ArgoCD app argo-workflows-ns-iad-ci syncs on push. Do NOT use GitHub Actions.\n\nSteps:\n1. Locate the dashboard test step in spaxel-build (the container/step that already runs `npm ci` + playwright `npm run test:a11y`).\n2. Add a `npm test` (jest) invocation in that same step, ordered BEFORE the playwright a11y run, so cheap unit failures fail fast.\n3. Ensure a jest non-zero exit fails the workflow (do not swallow the exit code).\n4. Commit per declarative-config repo conventions; push (ArgoCD syncs the template on push).\n\nAcceptance criteria:\n- spaxel-build WorkflowTemplate runs `npm test` before `npm run test:a11y` in the dashboard step.\n- jest failure causes the step/workflow to fail (exit code propagated).\n- Change committed and pushed to declarative-config (Forgejo origin; GitHub mirror is automatic).\n\nDepends on: the fix bead (jest suite must be green locally before it is worth wiring into CI).","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T09:52:13.619373547Z","updated_at":"2026-07-04T09:52:47.980186851Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-341v","depends_on_id":"bf-5o67","type":"blocks","created_at":"2026-07-04T09:52:47.954939999Z","created_by":"cli","thread_id":""}]} -{"id":"bf-344n","title":"Make the direct setjmp==0 path print a matching neutral observable line","description":"Split-child 2 of 4 for parent bf-38e9. Depends on child 1 (bf-52k2).\n\nGOAL: parity — exactly one observable line per test on BOTH paths, both using the SAME neutral marker family (no PASS/FAIL on either). The committed direct path currently prints an outcome label:\n\n g_tests[i].fn();\n printf(\"PASS: %s\\n\", g_tests[i].name);\n passed++;\n\nReplace \"PASS: %s\" with the matching neutral marker family chosen in child 1, e.g.:\n\n printf(\"RUN: %s\\n\", g_tests[i].name);\n\nso a passing test prints \"RUN: \" and a failing test (child 1) prints \"RUN: (assertion failed)\". Keep the internal passed++ counter — it feeds the summary line, not the per-test output, so it is not an \"observable label\".\n\nAfter this child, NEITHER branch emits PASS/FAIL; each test emits exactly one neutral line.\n\nAcceptance criteria:\n- Direct path prints exactly one neutral marker naming the test, same family as child 1's else-branch marker.\n- Neither the direct nor the else branch contains a PASS or FAIL token.\n- passed/failed counters and g_failure_count accounting are unchanged.\n- Compiles cleanly: gcc -std=c11 -Wall -Wextra.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T17:14:53.237671497Z","updated_at":"2026-07-03T18:55:06.466944008Z","closed_at":"2026-07-03T18:55:06.466944008Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-344n","depends_on_id":"bf-52k2","type":"blocks","created_at":"2026-07-03T17:16:28.393631704Z","created_by":"cli","thread_id":""}],"comments":[{"id":16,"issue_id":"bf-344n","author":"cli","text":"Completed: direct setjmp==0 path prints the neutral 'RUN: ' marker (b86cb67, child 2 of bf-38e9), matching child 1's else-branch 'RUN: (assertion failed)'. Verified acceptance: gcc -std=c11 -Wall -Wextra clean (make -C firmware/test test -> 29 passed/0 failed, exit 0, no warnings); grep confirms zero PASS/FAIL printf tokens in firmware/test/*.c; passed/failed counters + g_failure_count untouched; firmware/main + CMakeLists.txt untouched. Follow-up ca1b510 dropped a stale 'print PASS/FAIL' doc-comment reference that child 1 (bf-52k2) had left behind. Pushed to origin main.","created_at":"2026-07-03T18:55:22.116691614Z"}]} -{"id":"bf-38e9","title":"Print an observable per-test line on the longjmp failure-return path","description":"Split-child 2 of 3 for parent bf-53ut. Builds on child 1 (the setjmp(g_test_jmp) guard skeleton). This child makes the per-test recovery VISIBLE: when a test fails and longjmps back, print a per-test line so a reader can see the failure and — crucially — see that the tests AFTER it still ran (the whole point of the recovery loop). Per the parent bead's contract this is a neutral observable marker, NOT a PASS/FAIL label (outcome-based PASS/FAIL labels land in a later sibling bead — do not add them here).\n\nScope (extend firmware/test/test_runner.c, main()'s per-test loop):\n- In the non-zero (else) branch of the setjmp guard added in child 1, print one observable line for the failed test that names it, e.g. printf(\"RUN: %s (assertion failed)\\n\", g_tests[i].name); or an equivalent neutral marker.\n- Ensure exactly one observable line per test prints on BOTH paths — the direct/0 path already prints a line per the parent's child 1 (bf-1zew); make the failure path do the same — so that when test N fails, the lines for tests N+1..end still appear in order. That interleaving is how this child's acceptance is verified.\n- Do NOT introduce PASS/FAIL outcome labels here (deferred to a later sibling). Do NOT change g_failure_count accounting — test_record_failure() already bumped it before the longjmp; this branch only prints.\n\nAcceptance criteria:\n- On the longjmp (non-zero) return, main() prints a per-test observable line naming the test.\n- Both the pass-path and fail-path emit exactly one observable line per test, so a failing test followed by a passing test yields two lines in order.\n- Compiles cleanly with gcc -std=c11 -Wall -Wextra.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T16:02:09.808857448Z","updated_at":"2026-07-03T22:32:59.842534446Z","closed_at":"2026-07-03T22:32:59.842534446Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-38e9","depends_on_id":"bf-2ftl","type":"blocks","created_at":"2026-07-03T16:02:31.030559098Z","created_by":"cli","thread_id":""},{"issue_id":"bf-38e9","depends_on_id":"bf-65m5","type":"blocks","created_at":"2026-07-03T17:16:28.465494295Z","created_by":"cli","thread_id":""}],"comments":[{"id":27,"issue_id":"bf-38e9","author":"cli","text":"Completed: commit 6e45480 makes the else (non-zero) branch of main()'s per-test setjmp loop print a neutral observable marker 'RUN: (assertion failed)' instead of an outcome label. Both branches now emit exactly one line per test (pass-path 'RUN: ', fail-path 'RUN: (assertion failed)'), so a failing test followed by a passing test yields two lines in order. g_failure_count accounting unchanged (only prints). Verified: gcc -std=c11 -Wall -Wextra clean compile; standalone fail-then-pass harness through the real main() loop produced exactly 2 RUN lines in order with exit 1; no PASS/FAIL outcome tokens; no firmware/main or CMakeLists.txt changes; full suite 29 passed 0 failed. Bead had stayed open from a rejected non-fast-forward push; reconciled via clean merge of origin/main (origin changes confined to mothership/; overlapping test_runner.c comment change was byte-identical on both sides) and pushed — local and origin now both at 3d53616.","created_at":"2026-07-03T22:32:13.179287914Z"}]} -{"id":"bf-3fx4","title":"Verify test body runs only inside the setjmp==0 guard","description":"Read-only static verification of firmware/test/test_runner.c only (NO code changes). Child 2 of 4 splitting bf-2opw (confirm the per-test loop body matches parent bf-22vg contract).\n\nScope: In main()'s per-test loop, confirm the test body call g_tests[i].fn() is the sole statement inside the guard: if (setjmp(g_test_jmp) == 0) { ... }. The body must run only on the direct (zero) setjmp return path; a non-zero return is a longjmp from a failed assertion and must NOT call the body.\n\nAcceptance criteria: A comment on this bead quoting the exact if (setjmp(g_test_jmp) == 0) block (the if-line, the g_tests[i].fn(); call, and the closing brace) from firmware/test/test_runner.c, and confirming the body call is the ONLY statement inside the setjmp==0 guard. Closes independently once confirmed.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T02:41:58.719608835Z","updated_at":"2026-07-04T05:34:32.743090637Z","closed_at":"2026-07-04T05:34:32.743090637Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-3fx4","depends_on_id":"bf-3yl7","type":"blocks","created_at":"2026-07-04T02:43:50.369009071Z","created_by":"cli","thread_id":""},{"issue_id":"bf-3fx4","depends_on_id":"bf-4t1c","type":"blocks","created_at":"2026-07-04T04:09:18.226678496Z","created_by":"cli","thread_id":""}],"comments":[{"id":37,"issue_id":"bf-3fx4","author":"cli","text":"Read-only static verification of firmware/test/test_runner.c (no code changes — child 2 of bf-2opw split, confirming per-test loop matches parent bf-22vg contract).\n\nCONFIRMED. In main()'s per-test loop (lines 246-259), the setjmp==0 guard at lines 256-258 is exactly:\n\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\n\nThe body call g_tests[i].fn(); (line 257) is the SOLE statement inside the setjmp==0 guard. The only other statement in the loop body is printf(\"RUN: %s\\n\", g_tests[i].name) at line 255, which sits BEFORE the setjmp (runs unconditionally, regardless of how the body ends) — NOT inside the guard.\n\nSemantics verified: body runs only on the direct (zero) setjmp return; a non-zero return is a longjmp from a failed assertion (test_record_failure longjmp(g_test_jmp,1) at line 172) and falls straight through to the next iteration with no else body, so the loop's i++ still advances and a failure in test N never blocks N+1..end. Loop index i is volatile-qualified (line 246) because it spans the setjmp/longjmp and is read on the longjmp-return path (C11 7.13.2.1). Matches parent bf-22vg.","created_at":"2026-07-04T03:14:06.030874371Z"},{"id":38,"issue_id":"bf-3fx4","author":"cli","text":"Read-only static verification of firmware/test/test_runner.c per-test loop — CONFIRMED: the test body runs ONLY inside the setjmp==0 guard. Exact guard block (firmware/test/test_runner.c:256-258), inside the per-test for-loop at line 246: if (setjmp(g_test_jmp) == 0) { g_tests[i].fn(); } g_tests[i].fn(); (line 257) is the ONLY statement between the guard's opening brace (256) and closing brace (258): the if-line, the fn() call, and the closing brace are all that the guard encloses. The loop body's other statement — printf(\"RUN: %s\\n\", g_tests[i].name); on line 255 — is BEFORE the guard, outside it. A non-zero setjmp return (a longjmp from a failed assertion via test_record_failure) therefore never reaches g_tests[i].fn(); it falls through to the next loop iteration, matching the parent bf-22vg contract. No merge-conflict markers in the file (working tree clean despite the UU index status).","created_at":"2026-07-04T03:32:07.839439683Z"},{"id":39,"issue_id":"bf-3fx4","author":"cli","text":"Independent re-verification by current claimant (auto-split review): CONFIRMED. firmware/test/test_runner.c:256-258, inside the per-test for-loop (line 246):\n\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\n\ng_tests[i].fn(); (line 257) is the SOLE statement inside the setjmp==0 guard — the if-line, the call, and the closing brace are all the guard encloses. The loop's other statement, printf(\"RUN: %s\\n\", g_tests[i].name) at line 255, sits BEFORE the setjmp and is therefore outside the guard. A non-zero setjmp return (longjmp from test_record_failure, line 172) falls through to the next iteration without calling the body — matches parent bf-22vg contract. NOTE on the 3 failures: they are NOT task-size — the verification has now been completed 3x (comments 37, 38, this). The bead never closes because the worker model times out (exit 124, 600s cap) AFTER posting the comment but BEFORE issuing br close. Splitting would multiply this: N children each carry the same ~107k-token context, same slow model, same close step that never gets reached. Recommend closing this bead directly (AC is met) rather than splitting.","created_at":"2026-07-04T03:36:53.408392082Z"},{"id":40,"issue_id":"bf-3fx4","author":"cli","text":"Read-only static verification of firmware/test/test_runner.c — CONFIRMED.\n\nIn main()'s per-test loop, the test body call g_tests[i].fn() is the SOLE statement inside the setjmp==0 guard. Exact block (lines 256-258):\n\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\n\nThe if-line (256), the g_tests[i].fn(); call (257), and the closing brace (258) are the entire guard. The printf(\"RUN: %s\\n\", ...) that precedes it (line 255) sits OUTSIDE the guard — before the setjmp — so it fires regardless of how the body ends. There is no else body.\n\nThis matches the parent bf-22vg contract: the body runs only on the direct (zero) setjmp return path; a non-zero return is a longjmp from a failed assertion and must NOT call the body — it simply falls through to the next loop iteration (i++ advancing normally, the loop index being volatile-qualified for a -Wclobbered-clean build). No code changes were made — read-only verification only.","created_at":"2026-07-04T03:43:59.966599826Z"},{"id":41,"issue_id":"bf-3fx4","author":"cli","text":"Static verification of firmware/test/test_runner.c (read-only, no code changes). Confirmed: the per-test loop body g_tests[i].fn() is the sole statement inside the setjmp==0 guard in main()'s loop.\n\nExact block (firmware/test/test_runner.c lines 256-258):\n\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\n\nThe body call g_tests[i].fn(); is the ONLY statement between the opening brace (end of the if-line) and the closing brace — no other statements, no else branch. On the direct (zero) setjmp return path the body runs; on a non-zero return (a longjmp from a failed assertion, driven by test_record_failure()'s longjmp(g_test_jmp, 1) at line 172) control falls straight through to the next loop iteration without calling the body. Contract with parent bf-22vg confirmed.","created_at":"2026-07-04T03:57:07.969659595Z"},{"id":42,"issue_id":"bf-3fx4","author":"cli","text":"Read-only static verification of firmware/test/test_runner.c (no code changes).\n\nCONFIRMED: in main() per-test loop (lines 246-259), the test body call is the SOLE statement inside the setjmp==0 guard. The exact block at firmware/test/test_runner.c:256-258 is:\n\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\n\n- if-line (256): if (setjmp(g_test_jmp) == 0) {\n- body call (257): g_tests[i].fn();\n- closing brace (258): }\n\ng_tests[i].fn() is the ONLY statement between the opening brace of the guard and its closing brace -- nothing else is inside the setjmp==0 block. The RUN-line printf (line 255) prints BEFORE the setjmp so it runs unconditionally; the i++ advance is part of the for-loop (line 246), not the guard. Thus the test body runs ONLY on the direct (zero) setjmp return path; a non-zero return (a longjmp from a failed assertion via test_record_failure, line 172) falls straight through to the next iteration without ever calling the body. Matches parent bf-22vg contract.","created_at":"2026-07-04T04:03:41.465006811Z"},{"id":47,"issue_id":"bf-3fx4","author":"cli","text":"Read-only static verification of firmware/test/test_runner.c main()'s per-test loop — NO code changes (file untouched, confirmed via git diff).\n\nCONFIRMED: the test body call g_tests[i].fn() is the SOLE statement inside the if (setjmp(g_test_jmp) == 0) guard. Exact block (test_runner.c lines 256-258):\n\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\n\n- if-line: if (setjmp(g_test_jmp) == 0) {\n- body call: g_tests[i].fn();\n- closing brace: }\n\nThe RUN-line printf (line 255) sits BEFORE the if, not inside it — so it runs unconditionally. Nothing else lives between the { and }. On the direct (zero) setjmp return the body runs; on a non-zero (longjmp from a failed assertion) return control skips the body and falls through to the loop's i++. This matches the parent bf-22vg contract: body runs only on the zero setjmp return path; a failed-assertion longjmp must NOT call the body. Child 2 of 4 splitting bf-2opw.","created_at":"2026-07-04T05:06:06.754298931Z"},{"id":48,"issue_id":"bf-3fx4","author":"cli","text":"Verified (read-only static check of firmware/test/test_runner.c, no code changes): in main()'s per-test loop, the test body runs ONLY inside the setjmp==0 guard. The exact block (lines 256-258):\n\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\n\n- if-line: `if (setjmp(g_test_jmp) == 0) {`\n- sole statement inside the guard: `g_tests[i].fn();` (the test body call)\n- closing brace: `}`\n\nConfirmed the body call is the ONLY statement inside the setjmp==0 guard. The preceding `printf(\"RUN: %s\\n\", g_tests[i].name);` (line 255) runs BEFORE the setjmp, outside the guard. There is no else branch: a non-zero setjmp return (a longjmp from test_record_failure() on a failed assertion) falls straight through to the loop's i++ and the next iteration, so the body is NOT called on the longjmp-recovery path. Matches the parent bf-22vg contract — one test's assertion failure never blocks the remaining tests.","created_at":"2026-07-04T05:15:09.550851695Z"},{"id":49,"issue_id":"bf-3fx4","author":"cli","text":"Auto-split verification (bf-3fx4): split is ALREADY COMPLETE — no duplicate children created. Existing 4-child chain, all closed, sequentially wired:\n child 1 bf-3ybc (locate loop header) ->\n child 2 bf-2ftq (quote RUN printf, dep bf-3ybc) ->\n child 3 bf-ycik (quote if(setjmp==0) guard block, dep bf-2ftq) ->\n child 4 bf-4t1c (confirm g_tests[i].fn() is sole statement, dep bf-ycik).\nParent has umbrella label and depends on last child bf-4t1c. All 4 children CLOSED, so the original bf-3fx4 contract (body call is sole statement inside the setjmp==0 guard) is satisfied. Repeated failure-count:3 re-triggers likely stem from this umbrella parent still being in_progress despite all children closed — it is closeable.","created_at":"2026-07-04T05:25:34.679864591Z"},{"id":50,"issue_id":"bf-3fx4","author":"cli","text":"Read-only static verification of firmware/test/test_runner.c — CONFIRMED. No code changes.\n\nIn main()'s per-test loop (the for-loop at line 246), the test body is the SOLE statement inside the setjmp==0 guard. Quoting the exact block from firmware/test/test_runner.c lines 256-258:\n\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\n\n- if-line: if (setjmp(g_test_jmp) == 0) {\n- body call: g_tests[i].fn();\n- closing: }\n\nThe body call g_tests[i].fn() is the ONLY statement inside the setjmp==0 guard (no other statements, no else branch). It runs only on the direct (zero) setjmp return path. A non-zero return is a longjmp from a failed assertion (test_record_failure at line 160 -> longjmp(g_test_jmp, 1) at line 172) and falls straight through to the next loop iteration without calling the body. The RUN line (printf at line 255) prints BEFORE the setjmp and is therefore outside the guard. This matches the parent bf-22vg contract. Closes independently.","created_at":"2026-07-04T05:33:34.017058069Z"}]} -{"id":"bf-3id","title":"Implement firmware/test/ failure handling with jmp_buf and test_record_failure longjmp","description":"Sub-split child of bf-2i4 (parent bf-2i4; grandparent bf-56v; great-grandparent bf-4ne). Depends on child 1 (registry). Add the failure-recovery machinery to firmware/test/test_runner.c.\n\nScope (extend firmware/test/test_runner.c):\n- Define the per-test recovery target: a static jmp_buf at file scope (the live setjmp target the header ASSERT_* macros longjmp into). This is the shared contract with main() in child 3, which setjmp()s into it before each test.\n- Implement test_record_failure(const char *file, int line, const char *fmt, ...): format and print the assertion detail to stderr (file:line plus the fmt message via vfprintf), then longjmp() into the per-test target so the current test aborts but the runner continues.\n- Record that a failure occurred this run (a file-scope flag or counter) so main() in child 3 can return non-zero on any failure.\n\nDo NOT implement main() yet (child 3).\n\nAcceptance criteria:\n- test_record_failure prints file:line and the formatted message to stderr, then longjmp()s into the static jmp_buf.\n- The jmp_buf is declared at file scope so child 3 main() can setjmp() it.\n- A failure is recorded (flag/counter) for main() exit code use.\n- Compiles cleanly: gcc -std=c11 -Wall -Wextra -c firmware/test/test_runner.c.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T11:35:51.919421636Z","updated_at":"2026-07-03T13:53:39.314499497Z","closed_at":"2026-07-03T13:53:39.314499497Z","close_reason":"Completed: added file-scope static jmp_buf g_test_jmp, run-wide g_failure_count, and test_record_failure() (prints file:line+fmt to stderr, bumps counter, longjmp into g_test_jmp) to firmware/test/test_runner.c. Compiles clean (gcc -std=c11 -Wall -Wextra -c). main() deferred to bf-bq9. Commit 8325070.","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-3id","depends_on_id":"bf-lfz","type":"blocks","created_at":"2026-07-03T11:36:29.519920707Z","created_by":"cli","thread_id":""}]} -{"id":"bf-3itg","title":"Establish dashboard jest baseline (npm ci + smoke run)","description":"Scope: discovery only — NO source fixes. De-risk the most common catalog failure (broken test runner / bad install) and produce the file-level pass/fail table that the batch beads build on. This bead bounds the 'does the suite even run' unknown before anyone catalogs individual failures.\n\nSteps:\n1. cd dashboard/ && npm ci. If install fails, record the exact error and stop — that is the finding for this bead (do not patch source to fix it).\n2. Run npx jest --verbose (full suite). If the runner itself errors before producing per-file results, record the runner error and stop.\n3. From the jest summary, build a 13-row table: file -> PASS/FAIL (+ passing/failing test counts). Files: ambient, command-palette, explainability, fleet-page, fresnel, mobile, onboard, proactive, quick-actions, replay, sidebar-timeline, timeline, troubleshoot.\n4. Post the table as a comment on THIS bead.\n\nAcceptance criteria:\n- npm ci outcome is recorded (success, or the blocking error).\n- A 13-row file-level PASS/FAIL table is posted as a comment on this bead.\n- No source changes are made.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T10:22:24.490278802Z","updated_at":"2026-07-04T10:54:42.711078565Z","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-3itg","depends_on_id":"bf-j0lh","type":"blocks","created_at":"2026-07-04T10:54:42.704966655Z","created_by":"cli","thread_id":""}]} -{"id":"bf-3uqf","title":"Add auth token verification fuzz test","description":"Plan target #3 (docs/plan/plan.md, Testing Strategy). Fuzz test for HMAC node-token verification. NAMING DISCREPANCY: the plan calls this auth.VerifyToken, but the actual exported surface in the auth package (handler.go) is Handler.ValidateNodeToken(mac, token string) bool and Handler.DeriveNodeToken(mac) (HMAC-SHA256 over the install secret, constant-time compare). Properties: (1) ValidateNodeToken must NEVER panic on any mac/token pair (the X-Spaxel-Token header is attacker-controlled, fully untrusted); (2) roundtrip: ValidateNodeToken(mac, token) is true iff token equals DeriveNodeToken(mac). Enumerated tamper cases from the plan: wrong length, invalid hex, correct length but wrong bytes, empty, one byte short/long, uppercase variant, very long. Existing state: file token_fuzz_test.go already exists in the auth package (untracked) and currently passes seeds + the table-driven companion TestValidateNodeTokenProperty; it already documents the VerifyToken->ValidateNodeToken discrepancy. Do not redo from scratch: run go test -fuzz=FuzzValidateNodeToken -fuzztime=10s on the auth package clean (or fix any finding in the same change), then commit. AC: 10s native fuzz run produces no findings (or bugs fixed); plain go test ./... on auth package stays green and fast; file committed.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T09:18:59.792788710Z","updated_at":"2026-07-04T09:19:29.992418242Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-3uqf","depends_on_id":"bf-nzev","type":"blocks","created_at":"2026-07-04T09:19:29.980642641Z","created_by":"cli","thread_id":""}]} -{"id":"bf-3ybc","title":"Locate main()'s per-test loop header in firmware/test/test_runner.c","description":"Read-only static verification of firmware/test/test_runner.c only (NO code changes). Child 1 of 4 splitting bf-3fx4 (setjmp==0 guard verification), itself child 2 of 4 splitting bf-2opw.\n\nScope: In main() (the suite driver near the bottom of the file), locate the per-test loop that iterates the g_tests[] registry. Identify the loop header line — the for statement that walks index i from 0 to g_test_count.\n\nAcceptance criteria: A comment on this bead quoting the exact loop header line from firmware/test/test_runner.c, which must be 'for (volatile int i = 0; i < g_test_count; i++) {' (note the volatile qualifier on the loop index, required because i spans the setjmp/longjmp). Closes independently once quoted.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T04:08:49.483529497Z","updated_at":"2026-07-04T04:28:40.391245302Z","closed_at":"2026-07-04T04:28:40.391245302Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"comments":[{"id":43,"issue_id":"bf-3ybc","author":"cli","text":"Read-only static verification of firmware/test/test_runner.c. Located main()'s per-test loop header at line 246. Exact loop header line quoted from firmware/test/test_runner.c:\n\nfor (volatile int i = 0; i < g_test_count; i++) {\n\nThe loop index i carries the volatile qualifier, required because i spans the setjmp/longjmp recovery path (it is read on the longjmp-return path for the i < g_test_count test and the i++ advance, so per C11 7.13.2.1 it must be volatile to remain determinate). No code changes made — pure read-only check. Acceptance criteria met.","created_at":"2026-07-04T04:27:45.290123463Z"}]} -{"id":"bf-3yl7","title":"Verify loop header and RUN marker precede setjmp","description":"Read-only static verification of firmware/test/test_runner.c only (NO code changes). Child 1 of 4 splitting bf-2opw (confirm the per-test loop body matches parent bf-22vg contract).\n\nScope: In main(), confirm the per-test loop opens with the header: for (volatile int i = 0; i < g_test_count; i++) and that the FIRST statement inside the loop body is the printf that prints the RUN: marker with g_tests[i].name (i.e. printf(\"RUN: %s\\n\", g_tests[i].name)). It must appear BEFORE the setjmp call, so the RUN line prints regardless of how the body ends. Also confirm the loop index i carries the volatile qualifier (this is the expected compile-cleanliness gate for -Wclobbered per C11 7.13.2.1, and is explicitly NOT an outcome concern).\n\nAcceptance criteria: A comment on this bead quoting the exact for(...) header line and the exact RUN printf line from firmware/test/test_runner.c, and confirming (a) the RUN printf precedes the setjmp call, and (b) the loop index is volatile-qualified. Closes independently once confirmed.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T02:41:30.881612791Z","updated_at":"2026-07-04T03:03:47.966938100Z","closed_at":"2026-07-04T03:03:47.966938100Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"comments":[{"id":35,"issue_id":"bf-3yl7","author":"cli","text":"Read-only static verification of firmware/test/test_runner.c — no code changes.\n\nCONFIRMED against the current source (main(), lines 246-259):\n\n1. for(...) header line (line 246) — EXACT:\n for (volatile int i = 0; i < g_test_count; i++) {\n Matches parent bf-22vg contract.\n\n2. RUN printf line (line 255) — EXACT:\n printf(\"RUN: %s\\n\", g_tests[i].name);\n This is the FIRST statement inside the loop body (lines 247-254 are a comment block only — no executable statements precede it).\n\n3. (a) RUN printf PRECEDES the setjmp call — CONFIRMED.\n printf is line 255; the setjmp guard 'if (setjmp(g_test_jmp) == 0)' is line 256. The RUN line prints unconditionally before setjmp is entered, so it appears regardless of how the body ends (direct return OR longjmp from a failed assertion).\n\n4. (b) Loop index is volatile-qualified — CONFIRMED.\n 'volatile int i' on line 246 (lines 232-245 document why: i spans the setjmp/longjmp and is read on the longjmp-return path; C11 7.13.2.1 requires volatile; -Wclobbered remedy. Compile-cleanliness gate, NOT an outcome concern).\n\nPer-test loop body matches the parent bf-22vg contract. Child 1 of 4 splitting bf-2opw.","created_at":"2026-07-04T02:51:14.153177746Z"},{"id":36,"issue_id":"bf-3yl7","author":"cli","text":"Read-only verification of firmware/test/test_runner.c (Child 1 of bf-2opw). CONFIRMED against parent bf-22vg contract. Exact for(...) header line (firmware/test/test_runner.c:246): for (volatile int i = 0; i < g_test_count; i++) { Exact RUN printf line (firmware/test/test_runner.c:255): printf(\"RUN: %s\\n\", g_tests[i].name); (a) RUN printf PRECEDES setjmp: the printf(...) at line 255 is the FIRST statement inside the loop body and runs before 'if (setjmp(g_test_jmp) == 0)' at line 256, so the RUN line prints regardless of how the body ends (clean return or longjmp from a failed assertion). (b) Loop index i IS volatile-qualified: declared 'volatile int i' in the for-init (the volatile is the -Wclobbered/C11 7.13.2.1 compile-cleanliness gate; not an outcome concern). No code changes — read-only check.","created_at":"2026-07-04T03:02:43.872512941Z"}]} -{"id":"bf-3yvc","title":"Verify longjmp-return path falls through with no executable else","description":"Read-only static verification of firmware/test/test_runner.c only (NO code changes). Child 3 of 4 splitting bf-2opw (confirm the per-test loop body matches parent bf-22vg contract).\n\nScope: In main()'s per-test loop, confirm that immediately after the if (setjmp(g_test_jmp) == 0) guard there is NO executable else branch (an empty or comment-only else is acceptable; a code-bearing else is not). The non-zero (longjmp) setjmp return path must therefore simply fall through to the loop advance (the loop i++ and the next iteration), so a failed assertion in test N never blocks tests N+1..end.\n\nAcceptance criteria: A comment on this bead quoting the exact span from the setjmp guard's closing brace through the loop's own closing brace from firmware/test/test_runner.c, and confirming (a) there is no executable else branch, and (b) the non-zero return path falls straight through to the next iteration. Closes independently once confirmed.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T02:42:28.323290668Z","updated_at":"2026-07-04T05:48:41.392873027Z","closed_at":"2026-07-04T05:48:41.392873027Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-3yvc","depends_on_id":"bf-3fx4","type":"blocks","created_at":"2026-07-04T02:43:50.390547096Z","created_by":"cli","thread_id":""}],"comments":[{"id":51,"issue_id":"bf-3yvc","author":"cli","text":"Verified (read-only, no code changes) firmware/test/test_runner.c — the per-test loop in main() (lines 246-259).\n\nExact span, from the setjmp guard's closing brace through the loop's own closing brace (firmware/test/test_runner.c:256-259):\n\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n } <-- line 258: setjmp guard's closing brace\n } <-- line 259: for-loop's own closing brace\n\nThere is NOTHING between line 258 and line 259 — not a single token, not even a comment. So:\n\n(a) No executable else branch: confirmed. There is no 'else' keyword at all after the if (setjmp(g_test_jmp) == 0) guard, let alone a code-bearing one. The guard's body is only 'g_tests[i].fn();' (line 257), gated to the direct/zero setjmp return.\n\n(b) Non-zero (longjmp) return falls straight through to the next iteration: confirmed. On a non-zero setjmp return the if-body is skipped, control drops out of the if to the loop's closing brace (line 259), and the for-header's i++ then advances — so a failed assertion in test N longjmps back to the setjmp, falls through, and i advances to N+1 without obstruction. The volatile qualifier on the loop index i (line 246) keeps that post-longjmp read well-defined per C11 7.13.2.1. This matches the parent bf-22vg contract.","created_at":"2026-07-04T05:47:31.055737662Z"}]} -{"id":"bf-40j","title":"Add sanity test and verify non-zero-exit-on-failure contract","description":"Child 4 of the bf-56v harness split (the LAST child). Depends on child 3 (bf-r3v: Makefile). Add the one trivial passing test and prove the full end-to-end contract holds.\n\nScope:\n- Create firmware/test/test_sanity.c with a single self-registering test: TEST(arithmetic_sanity) { ASSERT_EQ(1 + 1, 2); }. This is the smallest possible assertion — if it fails, the harness itself is broken, not firmware logic.\n- Then verify the contract end to end with the documented single command 'make -C firmware/test test':\n 1. Green run: sanity passes, overall exit code is 0.\n 2. Flip the assertion to a guaranteed failure (e.g. ASSERT_EQ(1 + 1, 3)) and confirm 'make -C firmware/test test' now exits NON-ZERO (this is the exact step the parent kept failing on — actually run it and capture the non-zero exit, do not assume).\n 3. Revert the flip so the working tree is green again.\n- Do NOT add any module tests (nvs/csi/prov) here — those are later sibling beads.\n\nAcceptance criteria:\n- firmware/test/test_sanity.c exists with exactly the arithmetic_sanity test (1+1==2), self-registered via TEST().\n- 'make -C firmware/test test' runs it, prints 'PASS arithmetic_sanity', and exits 0.\n- With the assertion temporarily flipped, the same command exits non-zero (verified by running it, not by reasoning).\n- The flip is reverted; final 'make -C firmware/test test' is green / exit 0.\n- No nvs/csi/prov module tests added in this bead.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T10:49:28.950983312Z","updated_at":"2026-07-03T10:49:28.950983312Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-40j","depends_on_id":"bf-r3v","type":"blocks","created_at":"2026-07-03T10:50:27.925741369Z","created_by":"cli","thread_id":""}]} -{"id":"bf-489z","title":"Build 13-row jest file-level pass/fail table","description":"Prerequisite: child bf-5hyk (jest run) must have produced per-file output. If the runner errored with no per-file results, this bead is blocked.\n\nScope: discovery only — NO source fixes. Transform the jest summary from bf-5hyk into the canonical 13-row baseline table that downstream catalog beads build on.\n\nSteps:\n1. From the jest --verbose summary (captured in bf-5hyk), build a markdown table with one row per file: | File | Status | Passing | Failing |.\n2. The table MUST contain exactly these 13 rows (one per file): ambient, command-palette, explainability, fleet-page, fresnel, mobile, onboard, proactive, quick-actions, replay, sidebar-timeline, timeline, troubleshoot.\n3. Status = PASS if all tests in that file passed, FAIL otherwise. Passing/Failing = integer counts from the jest summary.\n4. If a file is missing from jest output entirely, mark it 'MISSING' (do not invent counts).\n\nAcceptance criteria:\n- A 13-row table is produced covering all 13 named files.\n- Counts come from actual jest output, not guesses; missing files are marked MISSING.\n- No source changes are made.\n- The table is posted by child bf-; this bead just produces it as a comment on itself.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T10:53:55.881361408Z","updated_at":"2026-07-04T10:54:18.213777877Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-489z","depends_on_id":"bf-5hyk","type":"blocks","created_at":"2026-07-04T10:54:18.205694254Z","created_by":"cli","thread_id":""}]} -{"id":"bf-48x","title":"Add missing fuzz/property tests: phase sanitize, replay seek, auth VerifyToken","description":"docs/plan/plan.md 'Testing Strategy > Property-Based / Fuzz Tests' lists five high-value targets. Only the two ingestion ones exist today (files frame_fuzz_test.go and json_fuzz_test.go, in the ingestion package under mothership/internal/). Missing three:\n\n1. Pipeline phase sanitization - plan wants a table-driven property test over edge I/Q pairs (all-zero, max int8, alternating sign); property: output is always finite (no NaN/Inf). Code lives in the signal package under mothership/internal/ (phase.go) - confirm exact package and exported function first.\n2. Replay seek-to-timestamp - fuzz with arbitrary int64 timestamps before/after file bounds and at wrap points; property: no panic, returned frame (if any) has recv_time_ms >= target. Code: replay package under mothership/internal/.\n3. Auth token verification - tokens with wrong length, invalid hex, correct length but wrong bytes; property: never panics. Code: auth package under mothership/internal/.\n\nConventions (match existing ingestion fuzz tests):\n- Fuzz targets in files named *_fuzz_test.go in the owning package\n- Seed corpus entries so plain 'go test ./...' exercises seeds quickly; long fuzzing only via 'go test -fuzz' (plan: excluded from regular runs to avoid indefinite CI loops)\n- The phase-sanitization one may be a deterministic table-driven property test rather than a native fuzz target, per the plan table\n\nAcceptance criteria:\n- Three new test files as above; 'go test ./...' in mothership/ stays green and fast (no fuzzing loops in default mode)\n- Each fuzz target runs at least 10s locally under 'go test -fuzz' without findings, or found bugs are fixed in the same change","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":3,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-02T11:29:58.008032950Z","updated_at":"2026-07-04T09:19:30.042243872Z","source_repo":".","compaction_level":0,"labels":["deferred","umbrella"],"dependencies":[{"issue_id":"bf-48x","depends_on_id":"bf-decb","type":"blocks","created_at":"2026-07-04T09:19:30.031590788Z","created_by":"cli","thread_id":""}]} -{"id":"bf-4er0","title":"Catalog failing dashboard jest tests batch B (files 8-13)","description":"Scope: discovery only — NO source fixes. Catalog per-test failures for the second batch of the dashboard jest suite.\n\nFiles (batch B, 8-13): proactive, quick-actions, replay, sidebar-timeline, timeline, troubleshoot.\n\nSteps:\n1. Read the file-level PASS/FAIL table from the baseline bead; only the FAIL files in batch B need per-test detail.\n2. For each failing file run: npx jest --verbose dashboard/js/.test.js — and capture every failing test.\n3. For each failing test record: file, full test name, and the first-line error/assertion cause (first-line cause is enough).\n4. Post the catalog as a comment on THIS bead.\n\nAcceptance criteria:\n- Every failing test in batch B is listed with file + full test name + first-line error.\n- PASS files in batch B are explicitly noted as passing (no per-test detail needed).\n- No source changes are made.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T10:22:30.553050100Z","updated_at":"2026-07-04T10:22:48.056125096Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-4er0","depends_on_id":"bf-4oxu","type":"blocks","created_at":"2026-07-04T10:22:48.045296038Z","created_by":"cli","thread_id":""}]} -{"id":"bf-4g7o","title":"Catalog failing dashboard jest tests (run locally, no fixes)","description":"Scope: discovery only — do NOT fix anything in this bead. The dashboard jest suite (13 *.test.js files under dashboard/js/: ambient, command-palette, explainability, fleet-page, fresnel, mobile, onboard, proactive, quick-actions, replay, sidebar-timeline, timeline, troubleshoot) is unwired from CI and likely bit-rotted. This bead bounds the unknown.\n\nSteps:\n1. From dashboard/, run `npm ci` then `npm test` (jest --verbose).\n2. Capture the full output. For each of the 13 test files, record pass/fail.\n3. For every failing test, record: file, test name, and the error/assertion that fails (first-line cause is enough).\n4. Post the catalog as a comment on this bead.\n\nAcceptance criteria:\n- A complete pass/fail catalog covering all 13 test files is posted as a comment.\n- Every failing test is listed with file + test name + error summary.\n- No source changes are made in this bead (fixes are a follow-up bead).\n\nWhy split out: the parent bead failed 3x largely because \"fix any currently-failing tests\" is unbounded. Catalog-first gives the fix bead a concrete, bounded worklist and a clear done-signal.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T09:51:42.182372433Z","updated_at":"2026-07-04T10:22:48.828083562Z","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-4g7o","depends_on_id":"bf-5ujd","type":"blocks","created_at":"2026-07-04T10:22:48.807390351Z","created_by":"cli","thread_id":""}]} -{"id":"bf-4jnd","title":"Remove throwaway verify file and confirm suite returns to green","description":"Split-child 4 of 4 for parent bf-65m5 — the LAST child; closing it unblocks bf-65m5 (and in turn bf-38e9). Depends on child 3.\n\nGOAL: tear down the throwaway verify artifact so NOTHING throwaway is committed, and confirm the suite returns to all-green.\n\nSTEPS (read once, act FAST — do not over-explore):\n- Delete the throwaway file: rm -f firmware/test/test_bf38e9_verify.c\n- Confirm it is gone: 'ls firmware/test/test_bf38e9_verify.c' should fail; 'git status' should NOT list it (it was untracked, so after removal the tree is clean of it — nothing throwaway staged or committed).\n- Re-run the suite to confirm green: make -C firmware/test test; echo \"EXIT=$?\" -> EXIT must be 0 (all real tests pass again, no leftover failure).\n\nACCEPTANCE CRITERIA:\n- firmware/test/test_bf38e9_verify.c is removed.\n- git status shows no throwaway verify file (nothing staged/committed from this split).\n- 'make -C firmware/test test' exits 0 (suite green again).\n- No changes to firmware/main/*, firmware/CMakeLists.txt, the Makefile, or any existing test file.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T19:37:49.025236944Z","updated_at":"2026-07-03T21:58:46.873546580Z","closed_at":"2026-07-03T21:58:46.873546580Z","close_reason":"Completed: throwaway verify file removed, suite green (29/29)","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-4jnd","depends_on_id":"bf-5b2a","type":"blocks","created_at":"2026-07-03T19:38:00.031338362Z","created_by":"cli","thread_id":""}],"comments":[{"id":25,"issue_id":"bf-4jnd","author":"cli","text":"Tore down the throwaway verify artifact; suite returns to all-green. firmware/test/test_bf38e9_verify.c removed (rm -f; was untracked -> removal leaves a clean tree). git status shows no throwaway file: nothing staged or committed from this split; 'git status --short firmware/' and 'git diff --stat -- firmware/' both empty. No changes to firmware/main/*, CMakeLists.txt, the Makefile, or any existing test file. make -C firmware/test test -> 29 passed, 0 failed of 29, EXIT=0. find shows zero bf38e9 references outside .git. Nothing to commit/push from this child (throwaway was untracked, so cleanup yields no diff) per acceptance criteria.","created_at":"2026-07-03T21:58:57.055479162Z"}]} -{"id":"bf-4m3","title":"Wire dashboard jest unit test suite into CI (only a11y runs today)","description":"The dashboard has a substantial jest unit test suite (10+ *.test.js files under dashboard/js/, e.g. ambient.test.js, replay.test.js, fresnel.test.js, command palette/fleet-page/explainability tests; jest config at dashboard/jest.config.js; script 'test': 'jest --verbose' in dashboard/package.json). But the CI pipeline (spaxel-build Argo WorkflowTemplate in the jedarden/declarative-config repo, path k8s/iad-ci/argo-workflows/, spaxel-build template) only runs 'npm run test:a11y' (playwright axe-core gate). The jest suite is never executed in CI, so dashboard JS regressions ship silently.\n\nSteps:\n1. Run the jest suite locally from dashboard/ ('npm ci && npm test') and fix any currently-failing tests first (the suite may have bit-rotted precisely because it is unwired).\n2. Add a 'npm test' (jest) invocation to the a11y/dashboard test step of the spaxel-build WorkflowTemplate in the declarative-config repo (same container/step that already does npm ci + playwright), ordered before the playwright a11y run so cheap unit failures fail fast.\n3. Commit the declarative-config change per that repo's conventions (ArgoCD app argo-workflows-ns-iad-ci syncs on push).\n\nAcceptance criteria:\n- 'npm test' passes locally in dashboard/\n- spaxel-build template runs jest and fails the workflow on jest failure\n- a11y gate behavior unchanged\nNote: work spans two repos (this repo for any test fixes, declarative-config for the CI wiring). GitHub Actions must NOT be used - Argo only.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":3,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-02T11:30:10.480315352Z","updated_at":"2026-07-04T09:52:48.045640793Z","source_repo":".","compaction_level":0,"labels":["deferred","umbrella"],"dependencies":[{"issue_id":"bf-4m3","depends_on_id":"bf-4xgb","type":"blocks","created_at":"2026-07-04T09:52:48.034185077Z","created_by":"cli","thread_id":""}]} -{"id":"bf-4ne","title":"Set up firmware host-based test harness under firmware/test/","description":"Part of split of bf-brz (firmware host-based unit tests). Establish the test scaffolding the other module tests build on.\n\nGoal: create firmware/test/ with a host-compilable test harness, a single documented command that compiles and runs all firmware host tests on Linux and exits non-zero on failure, and a trivial passing sanity test. Do NOT add module tests (nvs/csi/prov) here; later children add them.\n\nApproach:\n- First try the preferred path from the plan: ESP-IDF host-based testing with the Unity framework (idf.py test --target linux, or a Unity host target). Keep it only if the firmware source files can be compiled for the linux target without pulling in esp_wifi / freertos / uart driver dependencies.\n- If the ESP-IDF linux target is impractical for these heavily esp_-dependent files (the expected outcome), fall back to a plain gcc host harness under firmware/test/ (a small main() with assert macros, or Unity host).\n- Document the exact run command in a comment block at the top of the test runner (no new top-level docs).\n- Wire the harness so adding new test_*.c files later is trivial (convention-based, or a single SOURCES list).\n\nAcceptance criteria:\n- firmware/test/ exists with a build/run entrypoint that works on a Linux host with gcc (and the ESP-IDF toolchain only if that path was chosen).\n- A single documented command runs the tests and exits non-zero on failure.\n- A trivial sanity test (e.g. assert 1+1==2) passes.\n- Existing firmware build (firmware/CMakeLists.txt, Dockerfile firmware-builder stage) still builds unchanged.\n- The chosen approach and run command are documented in the test runner header comment.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T05:48:52.950375885Z","updated_at":"2026-07-03T09:32:56.718265729Z","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-4ne","depends_on_id":"bf-1xy","type":"blocks","created_at":"2026-07-03T09:32:17.597222557Z","created_by":"cli","thread_id":""}]} -{"id":"bf-4oxu","title":"Catalog failing dashboard jest tests batch A (files 1-7)","description":"Scope: discovery only — NO source fixes. Catalog per-test failures for the first batch of the dashboard jest suite.\n\nFiles (batch A, 1-7): ambient, command-palette, explainability, fleet-page, fresnel, mobile, onboard.\n\nSteps:\n1. Read the file-level PASS/FAIL table from the baseline bead; only the FAIL files in batch A need per-test detail.\n2. For each failing file run: npx jest --verbose dashboard/js/.test.js — and capture every failing test.\n3. For each failing test record: file, full test name, and the first-line error/assertion cause (first-line cause is enough).\n4. Post the catalog as a comment on THIS bead.\n\nAcceptance criteria:\n- Every failing test in batch A is listed with file + full test name + first-line error.\n- PASS files in batch A are explicitly noted as passing (no per-test detail needed).\n- No source changes are made.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T10:22:27.627713893Z","updated_at":"2026-07-04T10:22:47.808794124Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-4oxu","depends_on_id":"bf-3itg","type":"blocks","created_at":"2026-07-04T10:22:47.803263682Z","created_by":"cli","thread_id":""}]} -{"id":"bf-4qk2","title":"Enumerate automatic vars across the setjmp/longjmp boundary in main()'s per-test loop and render per-variable C11 7.13.2.1 verdicts","description":"Split-child 1/4 of the bf-31rd re-split (umbrella bf-31rd). Read-only analysis — NO production code change in this child.\n\nWhy this child exists: parent bf-31rd failed 3x partly because it bundled the standard-level analysis with the gcc empirical reality. This child isolates ONLY the C11 7.13.2.1 standard analysis; it does NOT touch the build or gcc.\n\nScope (firmware/test/test_runner.c, main()'s per-test loop region):\n- Identify EVERY automatic (stack/local) variable in scope across the setjmp(g_test_jmp) -> potential longjmp() (fired inside g_tests[i].fn()) -> longjmp-return boundary. Expected set: the loop index 'i', plus confirmation that NO local passed/failed tallies exist in main() (g_failure_count and g_test_jmp are file-scope statics, NOT automatic, so 7.13.2.1 does not apply to them at all).\n- For each automatic variable, state explicitly: (a) is it written between the setjmp() call and a longjmp() that could fire inside the body? (b) is it read in the post-longjmp path? For 'i': written ONLY by the for-init (i=0) and the for-increment (i++), both of which run OUTSIDE the setjmp->longjmp window (the increment runs AFTER control resumes at the setjmp site, whether by normal return or longjmp); the body only READS i. Therefore under the standard alone, 'i' is determinate and volatile is NOT strictly required by 7.13.2.1.\n\nAcceptance criteria:\n- A comment on THIS bead (br comment) lists every automatic variable in scope across the boundary, gives each a written/not-written and read/not-read verdict, and states the per-variable 7.13.2.1 conclusion.\n- Conclusion explicitly notes that, per the standard alone, 'i' does not require volatile — and flags that this is ONLY the standard-level finding (the gcc -Wclobbered reality is child 2's scope).\n- No change to any file under firmware/ in this child.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T07:11:10.743516523Z","updated_at":"2026-07-04T07:30:07.510152751Z","closed_at":"2026-07-04T07:30:07.510152751Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"comments":[{"id":59,"issue_id":"bf-4qk2","author":"cli","text":"## C11 7.13.2.1 audit — automatic vars across the setjmp/longjmp boundary in main()'s per-test loop\n\n**Child 1/4 of the bf-31rd re-split. Standard-level analysis ONLY.** No file under `firmware/` was modified in this child (verified: `git status` of `firmware/test/test_runner.c` is unchanged by this bead; the existing in-source audit comment at `test_runner.c:308-341` is left untouched). The gcc `-Wclobbered` empirical reality is **out of scope** here — it is child 2's scope. This comment renders only what C11 7.13.2.1 alone says.\n\n### The boundary under audit\n\n`firmware/test/test_runner.c`, `main()`, per-test loop body (lines 239-345):\n\n```c\nfor (volatile int i = 0; i < g_test_count; i++) {\n printf(\"RUN: %s\\n\", g_tests[i].name);\n if (setjmp(g_test_jmp) == 0) { // <-- setjmp() invocation\n g_tests[i].fn(); // <-- potential longjmp() fires inside here\n } // <-- post-longjmp fall-through path\n}\n```\n\nThe boundary is: the `setjmp(g_test_jmp)` call → a `longjmp()` that fires inside `g_tests[i].fn()` → the longjmp-return path at the `setjmp` call site.\n\n### Enumeration of automatic variables in scope across the boundary\n\nA walk of `main()`'s frame yields exactly **one** automatic object whose scope spans the boundary:\n\n| # | Object | Storage | Scope across boundary? |\n|---|--------|---------|------------------------|\n| 1 | `i` (`volatile int`, for-init, line 239) | **automatic** (block-scoped) | **YES** — scope is the whole loop body incl. the setjmp call, the body call, and the post-longjmp fall-through |\n\n**Confirmation: NO local passed/failed tallies exist in `main()`.** The only counters/state touched by the recovery flow live outside `main()`'s frame:\n- `g_failure_count` — file-scope `static` (line 143), bumped inside `test_record_failure()` (line 170), **not** an automatic of `main()`.\n- `g_test_jmp` — file-scope `static` (line 132), **not** an automatic of `main()`.\n- `g_tests`, `g_test_count` — file-scope `static` (lines 73-74), not automatics of `main()`.\n\nBoth statics have **static storage duration**, so 7.13.2.1 (which speaks only to objects of automatic storage duration) **does not apply to them at all** — their values are unconditionally preserved across `longjmp`. No written/read verdict is required for them under the standard; they are listed only to confirm they are **not** automatics and thus outside the clobber rule's reach. `qsort`/`printf`/`setjmp` arguments are expressions/temporaries, not named automatics, and `qsort` runs before the loop regardless.\n\n### Per-variable verdict — `i`\n\n**The three conjunctive conditions of C11 7.13.2.1p3** (an automatic is indeterminate on longjmp return only if ALL hold): (1) automatic storage duration, local to the function containing the `setjmp`; (2) non-volatile-qualified type; (3) changed between the `setjmp` invocation and the `longjmp` call.\n\n**(a) Written between the `setjmp()` call and a `longjmp()` fired inside the body? — NO.**\nThe only writes to `i` are the for-init `i = 0` (runs once at loop entry, **before** any `setjmp` call) and the for-increment `i++` (runs at the **end** of each iteration, **after** the body). Within one iteration the order is `setjmp()` → body-or-longjmp-return → end of loop body → `i++`. When a `longjmp` fires inside `g_tests[i].fn()`, control transfers **directly** to the `setjmp` return site; `i++` does **not** execute between the `setjmp()` call and the `longjmp()`. It executes only **after** control has already resumed at the `setjmp` site. Inside the body, `i` is only **read** (to index `g_tests[i]`), never written.\n\n**(b) Read in the post-longjmp path? — YES.**\nOn longjmp return, `setjmp` yields non-zero, the `if` body is skipped, control falls through to the end of the for-body, where `i++` (reads `i`, then writes) and the `i < g_test_count` re-test (reads `i`) both execute.\n\n**7.13.2.1 conclusion for `i`:** condition (3) — \"changed between the `setjmp` invocation and the `longjmp` call\" — is **not met** (no write to `i` lands in that window). The conjunctive rule therefore does not bite: **`i` is determinate on the longjmp-return path.** Condition (2) (the `volatile` qualifier on line 239) is consequently **not needed** to keep `i` determinate under the standard — the \"changed between setjmp and longjmp\" failure alone is sufficient.\n\n### Standard-level conclusion (this child's entire scope)\n\n**Per C11 7.13.2.1 alone, `i` does NOT require `volatile`.** It is determinate on the longjmp-return path purely because it is not changed between the `setjmp()` call and a `longjmp()` fired inside `g_tests[i].fn()` — the for-init runs before `setjmp` and the for-increment runs after the body resumes. No automatic in `main()`'s frame meets all three conjuncts of 7.13.2.1p3, and the only statics in play (`g_failure_count`, `g_test_jmp`) are outside the rule's scope by storage duration.\n\n⚠ **Scope flag:** the above is ONLY the standard-level finding. Whether `volatile` is nevertheless **kept** on `i` to satisfy gcc's `-Wclobbered` (a heuristic warning that does not track the \"changed between setjmp and longjmp\" distinction) is a separate, empirical/compiler question and is **deliberately deferred to child 2** of this re-split. This child asserts nothing about gcc and changes nothing under `firmware/`.","created_at":"2026-07-04T07:25:20.469651663Z"},{"id":60,"issue_id":"bf-4qk2","author":"cli","text":"## C11 7.13.2.1 enumeration — automatics across the setjmp/longjmp boundary in main()'s per-test loop\n\nScope: firmware/test/test_runner.c, main()'s per-test loop. This is split-child 1/4 of the bf-31rd re-split. READ-ONLY, standard-level only — no file under firmware/ touched, and no gcc/-Wclobbered analysis here (that is child 2's scope).\n\nThe boundary under audit:\n\n```\nfor (volatile int i = 0; i < g_test_count; i++) {\n printf(\"RUN: %s\\n\", g_tests[i].name); // before setjmp, unaffected\n if (setjmp(g_test_jmp) == 0) { // <-- setjmp() call\n g_tests[i].fn(); // <-- longjmp() can fire in here\n } // (via test_record_failure -> longjmp(g_test_jmp,1))\n} // on longjmp return: fall through if (no else),\n // then i++ and i < g_test_count re-test\n```\n\nComplete set of automatic (stack/local) variables in scope across the boundary: **{ i }** — and nothing else.\n\n### Variable: `i` — automatic, `volatile int`, loop index\n- (a) Written between the setjmp() call and a longjmp() fired inside the body? **NO — not written.** The only writes to `i` are (1) the for-init `i = 0`, which runs once before the loop and before any in-iteration setjmp, and (2) the for-increment `i++`, which runs AFTER control resumes at the setjmp call site — i.e. after the body has already returned, whether normally or via longjmp. Both writes therefore land OUTSIDE the [setjmp() call -> longjmp() firing inside g_tests[i].fn()] window. Within that window the body only READS `i` (to index `g_tests[i]`); no write to `i` occurs between the setjmp() call and a longjmp() that could fire inside `g_tests[i].fn()`.\n- (b) Read in the post-longjmp path? **YES — read.** On a longjmp return, setjmp yields non-zero, control falls through the `if` (no else body) into the loop continuation, where `i++` reads-and-writes `i` and the `i < g_test_count` condition reads `i`. These post-longjmp reads require `i` to be determinate.\n- **7.13.2.1 verdict — DETERMINATE; volatile NOT strictly required by the standard.** The standard renders an automatic object indeterminate on longjmp return ONLY if ALL THREE hold: (1) local to the function containing setjmp, (2) non-volatile, AND (3) modified between the setjmp() call and the longjmp() call. For `i`:\n - (1) local to main() — YES;\n - (2) non-volatile — NO (`i` is `volatile`-qualified, which alone exempts it from the indeterminate rule);\n - (3) modified between setjmp() and longjmp() — NO (see (a) above).\n Two of the three conditions fail, so `i` is determinate on the longjmp-return path under the standard alone. Per 7.13.2.1, `i` does NOT require `volatile`.\n\n### No other automatics in scope — confirmed\n- **No local passed/failed tallies exist in main().** main() declares no counters of its own; its body holds only the loop index `i`. The qsort() call and the setjmp()/printf()/fn() calls are expressions, not automatic declarations.\n- **`g_failure_count` is file-scope `static` (declared outside main(), bumped inside test_record_failure() before the longjmp) — NOT automatic, so 7.13.2.1 does not apply to it at all.**\n- **`g_test_jmp` is file-scope `static` — NOT automatic, 7.13.2.1 does not apply.**\n- (Likewise `g_tests[]` and `g_test_count` are file-scope statics, not in scope of the rule.)\n\n### Standard-level conclusion\nPer C11 7.13.2.1 alone, the sole automatic across the boundary — `i` — is determinate after a longjmp return (it is not modified in the setjmp->longjmp window, and it is volatile-qualified regardless), so **`volatile` is NOT strictly required by the standard.** This is ONLY the standard-level finding: the empirical gcc `-Wclobbered` reality (why `volatile` is kept on `i` anyway) is explicitly child 2's scope and is out of bounds for this child. No file under firmware/ was modified.","created_at":"2026-07-04T07:28:46.049197083Z"}]} -{"id":"bf-4t1c","title":"Confirm g_tests[i].fn() is the sole statement inside the setjmp==0 guard","description":"Read-only static verification of firmware/test/test_runner.c only (NO code changes). Child 4 of 4 splitting bf-3fx4 (the final child).\n\nScope: Building on child 3's quoted guard block, confirm the test body call 'g_tests[i].fn();' is the ONLY statement inside the 'if (setjmp(g_test_jmp) == 0) { ... }' guard. Specifically confirm: (a) no other statement appears between the opening brace and the body call, (b) no other statement appears between the body call and the closing brace, and (c) there is no else clause on the guard. This is the parent bf-3fx4 contract: the body runs only on the zero-return path and nothing else shares that path.\n\nAcceptance criteria: A comment on this bead confirming 'g_tests[i].fn();' is the sole statement inside the setjmp==0 guard (no preceding/following siblings, no else), referencing the quoted block from child 3. Closes independently once confirmed.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T04:09:03.634225295Z","updated_at":"2026-07-04T04:55:25.017901370Z","closed_at":"2026-07-04T04:55:25.017901370Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-4t1c","depends_on_id":"bf-ycik","type":"blocks","created_at":"2026-07-04T04:09:17.794994710Z","created_by":"cli","thread_id":""}],"comments":[{"id":46,"issue_id":"bf-4t1c","author":"cli","text":"Verified against firmware/test/test_runner.c (read-only, no code changes). The setjmp==0 guard at lines 256-258 is:\n\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\n\nConfirming the three criteria from child 3's quoted block:\n(a) Between the opening brace '{' (end of L256) and g_tests[i].fn(); (L257): no statement.\n(b) Between g_tests[i].fn(); (L257) and the closing brace '}' (L258): no statement.\n(c) No else clause: L258 '}' is followed immediately by L259 '}' which closes the for loop -- no else.\n\nThe printf(\"RUN: ...\") on L255 is BEFORE the guard, not inside it, so it is not a sibling on the zero-return path. CONFIRMED: g_tests[i].fn(); is the sole statement inside the setjmp==0 guard. Parent bf-3fx4 contract holds -- the body runs only on the zero-return path and nothing else shares that path.","created_at":"2026-07-04T04:53:21.274389624Z"}]} -{"id":"bf-4w29","title":"Print run summary after all tests","description":"Child 3 of the bf-bq9 sub-split (parent bf-bq9; grandparent bf-2i4). Depends on child 2 (bf-6uz: name sort).\n\nScope (extend firmware/test/test_runner.c main()):\n- After the per-test loop completes, print a one-line run summary pairing the local passed/failed counters with the total registry count, e.g. printf(\"%d passed, %d failed of %d\\n\", passed, failed, g_test_count). This pairs with the per-test PASS/FAIL lines printed inside the loop.\n\nAcceptance criteria:\n- A single summary line is printed after all tests have run, with correct passed/failed/total counts.\n- Compiles cleanly with gcc -std=c11 -Wall -Wextra; the suite still runs and exits 0 when all tests pass.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T14:48:20.423432719Z","updated_at":"2026-07-03T14:48:20.423432719Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-4w29","depends_on_id":"bf-6uz","type":"blocks","created_at":"2026-07-03T14:48:53.968751248Z","created_by":"cli","thread_id":""}]} -{"id":"bf-4xgb","title":"Verify jest CI gate runs fail-fast and a11y behavior unchanged","description":"Scope: end-to-end verification that the wired CI gate actually behaves as intended. This is distinct from wiring — it observes a real workflow run.\n\nSteps:\n1. Trigger or observe a spaxel-build workflow run (Argo Workflows, iad-ci namespace).\n2. Confirm the jest step executes BEFORE the playwright a11y step and that its output appears in the dashboard test container logs.\n3. Confirm fail-fast: a jest failure fails the workflow before a11y runs (temporarily induce a failure, or reason from exit-code propagation if induction is impractical — state which).\n4. Confirm the a11y gate still runs and behaves exactly as before (no regression in axe-core gate behavior).\n\nAcceptance criteria:\n- Observed CI run shows `npm test` running in the dashboard step before a11y.\n- jest non-zero exit fails the workflow (fail-fast demonstrated or exit-code-propagation justified).\n- a11y gate behavior unchanged from pre-change.\n- Summary comment posted with run reference + evidence.\n\nDepends on: the CI wiring bead.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T09:52:18.752539450Z","updated_at":"2026-07-04T09:52:48.016139799Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-4xgb","depends_on_id":"bf-341v","type":"blocks","created_at":"2026-07-04T09:52:48.000974055Z","created_by":"cli","thread_id":""}]} -{"id":"bf-50yh","title":"Confirm every test's RUN: line prints with per-test isolation","description":"Split-child 4 of 4 (final) for umbrella bf-tof1. Dynamic output inspection: confirm per-test isolation is observable — the one contract the loop shape exists to provide.\n\nPrecondition: a successful make -C firmware/test test run (child 3). Inspect THAT run's stdout.\n\nScope (no code changes; read-only):\n- Confirm exactly one RUN: line prints per registered test (one per test_entry_t in the registry).\n- Confirm the RUN: lines cover the WHOLE suite — count them and compare to the number of registered TEST()s; none is skipped or duplicated.\n- Confirm per-test isolation at the observable level: the loop reaches every test regardless of how the prior body ended (a body that returns normally advances to the next RUN: line). For the all-passing suite every body returns normally, so all RUN: lines should appear in sorted (qsort-by-name) order.\n- Behavior is unchanged for passing tests relative to the naive direct-call baseline (bf-1fd4): same RUN: lines, same order.\n\nAcceptance criteria:\n- A comment listing the RUN: lines captured from the run and the registered-test count, confirming they match.\n- Confirms the suite is fully driven (no test silently dropped).\n- Closes independently once per-test RUN-line isolation is confirmed — at which point umbrella bf-tof1 (and thereby parent bf-22vg) acceptance is fully satisfied.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T01:52:07.048777977Z","updated_at":"2026-07-04T06:30:13.718021317Z","closed_at":"2026-07-04T06:30:13.718021317Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-50yh","depends_on_id":"bf-6201","type":"blocks","created_at":"2026-07-04T01:52:39.588055543Z","created_by":"cli","thread_id":""}],"comments":[{"id":56,"issue_id":"bf-50yh","author":"cli","text":"Confirmed per-test RUN-line isolation by inspecting the stdout of a successful `make -C firmware/test test` run. 29 registered TEST() definitions across test_sanity(1)/test_csi_frame(7)/test_nvs_migration(9)/test_serial_prov(12); 29 RUN lines emitted by the recovery loop. Bidirectional set diff is identical — every registered test has exactly one RUN line, none skipped, none duplicated, so the suite is fully driven. Emitted order is byte-for-byte strcmp-sorted (qsort-by-name), as expected for the all-passing suite where every body returns normally and no FAIL/stderr interleaves the RUN lines. RUN-line set and order are unchanged vs the naive direct-call baseline (bf-1fd4). Recorded the captured RUN lines and the count match as a confirmation comment in firmware/test/test_runner.c next to the RUN-line printf (comment-only; no behavioral change — rebuild is warning-free and produces the identical 29 RUN lines). Satisfies umbrella bf-tof1 and thereby parent bf-22vg.","created_at":"2026-07-04T06:30:13.711306238Z"}]} -{"id":"bf-52k2","title":"Make the longjmp failure-return branch print a neutral observable line naming the test","description":"Split-child 1 of 4 for parent bf-38e9 (which is itself split-child 2 of bf-53ut).\n\nFOCUSED CORE of bf-38e9: in main()'s per-test loop in firmware/test/test_runner.c, the else branch — taken when setjmp(g_test_jmp) returns non-zero because a failed assertion longjmp'd back — must print exactly ONE observable line that names the failed test with a NEUTRAL marker.\n\nIMPORTANT context that has blocked prior attempts: the currently committed loop already prints an outcome label on this branch:\n\n } else {\n printf(\"FAIL: %s\\n\", g_tests[i].name);\n failed++;\n }\n\nThat \"FAIL:\" token is exactly what the parent bead forbids at this stage (\"Do NOT introduce PASS/FAIL outcome labels here\" — outcome labels are deferred to a later sibling of bf-53ut). Replace that printf with a neutral marker that still names the test, e.g.:\n\n printf(\"RUN: %s (assertion failed)\\n\", g_tests[i].name);\n\nor any equivalent neutral phrasing. The marker must NOT contain the tokens PASS or FAIL.\n\nDo NOT touch the failed++ counter or g_failure_count — test_record_failure() already bumped g_failure_count before the longjmp; this branch only prints. Leave the direct (setjmp==0) path for child 2.\n\nAcceptance criteria:\n- The else branch prints exactly one line naming g_tests[i].name via a marker containing NO \"PASS\"/\"FAIL\" token.\n- failed++ and g_failure_count accounting are unchanged.\n- Compiles cleanly: gcc -std=c11 -Wall -Wextra (no new warnings).\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T17:14:19.844623812Z","updated_at":"2026-07-03T17:42:25.804742858Z","closed_at":"2026-07-03T17:42:25.804742858Z","close_reason":"Completed (re-dispatch of already-shipped work — commit 6e45480)","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"comments":[{"id":15,"issue_id":"bf-52k2","author":"cli","text":"Completed — note this was a re-dispatch: the work was already implemented and shipped in commit 6e45480 ('test: print neutral RUN marker on longjmp failure-return branch'), an ancestor of origin/main, so the 'currently prints FAIL' framing in the task body is now stale.\n\nVerified state meets every acceptance criterion:\n- firmware/test/test_runner.c:261 — the else branch (setjmp non-zero / longjmp-return path) prints exactly ONE line: printf(\"RUN: %s (assertion failed)\\n\", g_tests[i].name). Names the failing test; NO uppercase PASS/FAIL token (only lowercase 'assertion failed'). Matches the task's own suggested phrasing verbatim.\n- failed++ left in place; g_failure_count NOT touched on this branch (test_record_failure() bumps it before the longjmp — single source of truth).\n- Direct (setjmp==0) path left untouched for child 2 (still 'PASS:').\n- gcc -std=c11 -Wall -Wextra on test_runner.c: zero warnings.\n- make -C firmware/test test -> 29 passed, 0 failed.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.\n\nNothing new to commit — the change is already committed and pushed, so no fabricated commit was made.","created_at":"2026-07-03T17:41:47.627630985Z"}]} -{"id":"bf-53ut","title":"Wrap each test in per-test setjmp/longjmp recovery so a failing assertion is non-fatal","description":"Split-child 2 of 3 for parent bf-1na (Add main() entry-point skeleton with setjmp recovery loop and run-command header). Builds on child 1 (bf-1zew, which added the run-command comment + the naive registry iteration that calls each g_tests[i].fn() directly). This child is the core contract of bf-1na: make a failing assertion non-fatal by driving each test through the per-test setjmp/longjmp recovery loop.\n\nScope (extend firmware/test/test_runner.c):\n- In main()'s per-test loop, before calling the body, setjmp(g_test_jmp) — the file-scope jmp_buf from sibling bf-3id. Evaluate its return: on the DIRECT call setjmp returns 0 and the body runs normally; if a failed assertion inside the body calls test_record_failure(), that longjmp(g_test_jmp, 1) returns control to the setjmp call site with a non-zero value instead.\n- On the non-zero (longjmp) return, do NOT re-invoke the body — just fall through to advance the loop. On the 0 return, call g_tests[i].fn() as before.\n- Either way the loop advances to i+1, so a failure in test N never blocks tests N+1..end. That per-test isolation is the whole point.\n- Keep printing one observable line per test BEFORE the setjmp (e.g. the RUN: line from child 1) so that, when a test fails, the lines for the tests after it still appear — which is how this child's acceptance is verified. (Outcome-based PASS/FAIL labels land in child 3; do not add them here.)\n\nImplementation note (the classic setjmp trap): any automatic variable that is modified between setjmp and longjmp AND read after the longjmp returns must be volatile-qualified, else its value is indeterminate (C11 7.13.2.1). The loop index i is read in the post-longjmp path — confirm it is not modified between the setjmp call and the longjmp (it is only incremented by the for-loop increment, which runs AFTER control returns), so no volatile is needed; double-check this holds for the structure you write.\n\nAcceptance criteria:\n- main() setjmp()s into g_test_jmp before each test and calls the body only on the 0 return; on a longjmp return it advances without re-calling.\n- A failing assertion in one test does NOT prevent later tests from running — verifiable by temporarily adding an intentionally-failing TEST() followed by a passing TEST() and confirming both tests' per-test lines print and the process completes (rather than aborting) when run via `make -C firmware/test test`.\n- Compiles cleanly with gcc -std=c11 -Wall -Wextra (no setjmp-related warnings).\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T15:20:02.394550114Z","updated_at":"2026-07-03T23:40:31.336122025Z","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-53ut","depends_on_id":"bf-1zew","type":"blocks","created_at":"2026-07-03T15:20:10.811014397Z","created_by":"cli","thread_id":""},{"issue_id":"bf-53ut","depends_on_id":"bf-vmik","type":"blocks","created_at":"2026-07-03T16:02:31.068178720Z","created_by":"cli","thread_id":""},{"issue_id":"bf-53ut","depends_on_id":"bf-yacm","type":"blocks","created_at":"2026-07-03T23:39:13.856960671Z","created_by":"cli","thread_id":""}]} -{"id":"bf-56v","title":"Build firmware/test/ gcc host harness with passing sanity test","description":"Build the actual harness scaffolding under firmware/test/ (part of split of bf-4ne; depends on child bf-21t approach decision).\n\nGoal: a host-compilable test harness with a single documented command that compiles AND runs all firmware host tests on Linux and exits non-zero on failure, plus one trivial passing sanity test. Do NOT add module tests (nvs/csi/prov) here; later siblings add them.\n\nImplementation (using the gcc host harness approach chosen in bf-21t):\n- Create firmware/test/ with a Makefile (or equivalent) that compiles test_*.c with gcc on Linux.\n- Provide a test runner main() with minimal assert macros (or Unity host).\n- Add a trivial sanity test (e.g. assert 1+1 == 2) that passes.\n- The documented run command MUST exit non-zero on any assertion failure.\n- Document the exact compile+run command in a comment block at the top of the test runner.\n- The harness is self-contained: it does NOT compile or link firmware/main/*.c (those are covered by later siblings with stubs/extraction as needed).\n\nAcceptance criteria:\n- firmware/test/ exists with a Makefile/build entrypoint that compiles with gcc on a Linux host.\n- A single documented command (shown in the runner header comment) compiles AND runs the tests.\n- Harness exits non-zero on failure — verify by temporarily flipping the sanity assertion, confirm non-zero exit, then revert.\n- The sanity test (1+1==2) passes.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","assignee":"","created_at":"2026-07-03T09:31:58.060577087Z","updated_at":"2026-07-03T10:52:23.274609792Z","source_repo":".","compaction_level":0,"labels":["deferred","failure-count:4","split-child","umbrella"],"dependencies":[{"issue_id":"bf-56v","depends_on_id":"bf-21t","type":"blocks","created_at":"2026-07-03T09:32:16.798453288Z","created_by":"cli","thread_id":""},{"issue_id":"bf-56v","depends_on_id":"bf-40j","type":"blocks","created_at":"2026-07-03T10:50:27.944098573Z","created_by":"cli","thread_id":""}]} -{"id":"bf-577f","title":"Confirm the failing test's neutral RUN line is present in stdout","description":"Split-child of parent bf-ba2u (auto-split). Depends on child 1 (bf-26fi), which captured /tmp/bf38e9_test_stdout.txt.\n\nGOAL: confirm the failing test emitted its neutral RUN line.\n\nSTEPS:\n- Read /tmp/bf38e9_test_stdout.txt (produced by child 1).\n- Confirm the literal line is present:\n RUN: aaa_bf38e9_intentional_fail (assertion failed)\n\nACCEPTANCE CRITERIA:\n- The exact string 'RUN: aaa_bf38e9_intentional_fail (assertion failed)' appears at least once in /tmp/bf38e9_test_stdout.txt.\n- No re-run of the suite required (use child 1's captured output); no source changes.\n\nIf the line is MISSING, that is a REAL regression in test_runner.c — STOP and report it; do not edit the verify file to mask it.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T20:25:52.503536859Z","updated_at":"2026-07-03T20:56:17.844115175Z","closed_at":"2026-07-03T20:56:17.844115175Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-577f","depends_on_id":"bf-26fi","type":"blocks","created_at":"2026-07-03T20:26:25.206418454Z","created_by":"cli","thread_id":""}],"comments":[{"id":20,"issue_id":"bf-577f","author":"cli","text":"Verification (child 2 of split bf-ba2u): confirmed the failing test's neutral RUN line is present in child 1's captured stdout. Checked /tmp/bf38e9_test_stdout.txt (produced by bf-26fi). The exact required line is present at line 4: 'RUN: aaa_bf38e9_intentional_fail (assertion failed)' — a literal exact match. Surrounding output confirms the recovery-loop contract holds: line 5 shows aab_bf38e9_passes_after_fail running immediately after the failure (longjmp returned control to main() and advanced to the next test), and the final tally is '30 passed, 1 failed of 31' — the intentional assertion failure did NOT abort the suite. This is NOT a regression. No source changes, no suite re-run (child 1's output is authoritative). The untracked firmware/test/test_bf38e9_verify.c is a throwaway artifact owned by sibling bead bf-1z44 (NOT committed per its header), so nothing to commit for this verification task.","created_at":"2026-07-03T20:39:13.904522007Z"},{"id":21,"issue_id":"bf-577f","author":"cli","text":"Verification-only child of bf-ba2u (split with bf-26fi). Confirmed against child 1's captured output /tmp/bf38e9_test_stdout.txt that the failing test emits its neutral RUN line: the exact string 'RUN: aaa_bf38e9_intentional_fail (assertion failed)' is present (line 4), immediately following the 'test_bf38e9_verify.c:26: ASSERT_EQ(1, 2) failed' assertion. Suite summary: '30 passed, 1 failed of 31' with all RUN markers emitted, so test_runner.c prints the neutral RUN line on the assertion-failed path — no regression in test_runner.c. No source changes (read-only verification); nothing to commit/push.","created_at":"2026-07-03T20:47:26.480384144Z"}]} -{"id":"bf-5b2a","title":"Confirm process completes non-zero with no abort or crash","description":"Split-child 3 of 4 for parent bf-65m5. Depends on child 2.\n\nGOAL: confirm the recovery loop lets the process COMPLETE normally on a failure — non-zero exit from the counted failure, NOT an abort/crash signal. (A crash here would mean the longjmp recovery is broken.)\n\nSTEPS (read once, act FAST — do not over-explore):\n- Run: make -C firmware/test test; echo \"EXIT=$?\"\n- Confirm:\n - EXIT is non-zero and specifically == 1 (test_runner.c returns 'g_failure_count > 0 ? 1 : 0'). It must NOT be a signal-based code: not 134 (SIGABRT), not 139 (SIGSEGV), not 137 (SIGKILL).\n - The run summary line printed by main() is present (matches pattern '... failed of ...') — proving the loop ran to completion rather than dying mid-iteration.\n\nACCEPTANCE CRITERIA:\n- Exit code is 1 (non-zero, and not a crash/abort signal).\n- The summary line is present in stdout.\n- No source changes (this child only runs and observes the exit code).\n\nIf the process aborts/crashes or exits with a signal code, STOP and report — that is a genuine defect, not a verification artifact.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T19:37:46.160080120Z","updated_at":"2026-07-03T21:55:08.124156221Z","closed_at":"2026-07-03T21:55:08.124156221Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-5b2a","depends_on_id":"bf-ba2u","type":"blocks","created_at":"2026-07-03T19:37:59.771471374Z","created_by":"cli","thread_id":""}],"comments":[{"id":24,"issue_id":"bf-5b2a","author":"cli","text":"VERIFIED: longjmp recovery loop lets the process COMPLETE normally on a counted failure — exit code 1, NOT an abort/crash signal. (1) Recreated child-1's throwaway fixture test_bf38e9_verify.c transiently — it had vanished as an untracked file between child 2 and this run (child 4 teardown still open, so it did not remove it); ran the real test_runner.c unchanged, then removed the fixture leaving the tree clean. (2) Binary exit code run directly = 1, exactly g_failure_count>0?1:0; NOT a signal code (not 134/137/139). (3) Summary line present: '30 passed, 1 failed of 31'. (4) Recovery loop ran to completion: failing test printed neutral '(assertion failed)' line then ALL remaining tests ran in order — loop did not die mid-iteration. (5) After removing fixture, suite green: 29 passed 0 failed. NOTE: 'make ... test; echo $?' yields EXIT=2 because GNU make exits 2 on any rule failure and does NOT propagate the recipe code (it logged 'Error 1' = binary returned 1); EXIT=2 is a make-wrapper artifact, NOT a recovery-loop defect — confirmed by running the binary directly (exit 1). No defect; longjmp recovery intact.","created_at":"2026-07-03T21:54:03.876798844Z"}]} -{"id":"bf-5ba","title":"Populate link count in calibration_complete WebSocket event","description":"mothership/cmd/mothership/main.go line 1438 sends links: 0 hardcoded in the calibration_complete WebSocket message. Should pass the actual count of active links from the ingestion server (ingestion.Server.ActiveLinkCount or similar). Affects the guided troubleshooting dashboard display after calibration.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-06-27T12:12:25.203289555Z","updated_at":"2026-07-04T08:43:28.204745873Z","closed_at":"2026-07-04T08:43:28.204745873Z","close_reason":"Completed","source_repo":".","compaction_level":0,"comments":[{"id":64,"issue_id":"bf-5ba","author":"cli","text":"Closed as complete. The calibration_complete WebSocket event in mothership/cmd/mothership/main.go (SetOnCalibrationComplete callback) previously hardcoded links: 0; it now reads the actual active link count from the ingestion server via ingestSrv.GetAllLinksInfo() (nil-guarded). The other two calibration_complete senders (dashboard/hub.go BroadcastCalibrationComplete and api/guided.go) already accept a linksCalibrated parameter and needed no change. Verified: go vet ./..., go build ./..., go test ./... all pass. Fix in commit 86b317b, pushed to origin/main.","created_at":"2026-07-04T08:44:09.874105669Z"}]} -{"id":"bf-5hr","title":"Update docs/plan/plan.md to reflect implemented reality (stale sections)","description":"docs/plan/plan.md header says 'Last updated: 2026-05-24, Status: COMPLETE' but several sections no longer match the tree (repo is now at VERSION 0.1.357 with ongoing fixes). Single consolidated plan-refresh task — do NOT split further.\n\nStale sections to fix in docs/plan/plan.md:\n1. 'Go Module Layout' section: plan shows a single root module (cmd/mothership, internal/, test/integration/). Reality: go.work at root with three modules — mothership/ (contains cmd/mothership, internal/), cmd/sim, test/acceptance. There is no test/integration/ directory; simulator-based tests live in test/acceptance/ and mothership/test/acceptance/.\n2. 'Deployment > Dockerfile' section: plan shows FROM golang:1.23 building ./cmd/mothership and image ghcr.io/spaxel/spaxel. Reality (see Dockerfile at repo root): espressif/idf:v5.2 firmware build stage + golang:1.25-bookworm, GOOS/GOARCH pinned to linux/amd64 (single-arch, deliberate), image published as ronaldraygun/spaxel via the spaxel-build Argo WorkflowTemplate.\n3. 'Quality Gates / Definition of Done' item 4 requires linux/amd64+linux/arm64 multi-arch buildx. Actual CI builds amd64 only. Update the gate to document the amd64-only decision (deployment target is amd64 k8s), or mark arm64 as future work — do not silently leave the contradiction.\n4. 'Testing Strategy > Integration Tests' says tests are 'Located in test/integration/' — correct to actual locations (test/acceptance/, mothership/test/acceptance/, tests/e2e/run.sh).\n5. 'Open Questions' has a duplicated 'Multi-installation coordination' bullet (appears twice back-to-back at the end) — deduplicate.\n6. Bump the 'Last updated' date and adjust the Status line (e.g. 'COMPLETE — maintenance mode') to reflect that beads referenced ('83 beads closed') were archived.\n\nAcceptance criteria:\n- All six items above corrected in docs/plan/plan.md; no other plan content rewritten\n- Every path named in the updated sections exists in the repo\n- Do not touch PROGRESS.md (separate bead bf-833 covers it)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-02T11:28:39.663320404Z","updated_at":"2026-07-03T05:12:09.263181100Z","closed_at":"2026-07-03T05:12:09.263181100Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred"],"comments":[{"id":5,"issue_id":"bf-5hr","author":"cli","text":"Refreshed the six stale sections of docs/plan/plan.md to match the implemented repo (VERSION 0.1.357): (1) Go Module Layout -> real go.work 3-module layout (mothership/, cmd/sim, test/acceptance); removed nonexistent test/integration/. (2) Dockerfile -> espressif/idf:v5.2 + golang:1.25-bookworm, GOOS/GOARCH pinned linux/amd64 (single-arch, deliberate), image ronaldraygun/spaxel via spaxel-build WorkflowTemplate. (3) Quality Gates item 4 -> amd64-only gate with rationale; arm64 marked future work. (4) Integration Tests -> corrected location to test/acceptance/, mothership/test/acceptance/, tests/e2e/run.sh. (5) Open Questions -> removed duplicated Multi-installation coordination bullet. (6) Bumped Last updated to 2026-07-03, Status -> maintenance mode (83 beads archived). Verified all named paths exist; go vet and go test on mothership both pass. Committed only docs/plan/plan.md (598360e), rebased onto remote and pushed. Did not touch PROGRESS.md (bf-833).","created_at":"2026-07-03T04:56:04.364469351Z"},{"id":6,"issue_id":"bf-5hr","author":"cli","text":"Verified complete: the 6 stale sections of docs/plan/plan.md were refreshed in commit 598360e (now on origin/main). (1) Go Module Layout now documents go.work stitching three modules (mothership/, cmd/sim, test/acceptance) with no test/integration/. (2) Dockerfile reflects reality: espressif/idf:v5.2 firmware stage + golang:1.25-bookworm, GOOS/GOARCH pinned linux/amd64 (single-arch by design), image ronaldraygun/spaxel via spaxel-build Argo WorkflowTemplate. (3) Quality Gates item 4 documents the amd64-only decision with arm64 marked as future work. (4) Integration Tests corrected to test/acceptance/, mothership/test/acceptance/, tests/e2e/run.sh. (5) Open Questions de-duplicated (Multi-installation coordination once). (6) Header bumped to Last updated 2026-07-03, Status 'COMPLETE - maintenance mode'. All paths named in updated sections exist; test/integration/ absent; VERSION=0.1.357 matches Status line. PROGRESS.md untouched (bf-833).","created_at":"2026-07-03T05:10:39.003850393Z"}]} -{"id":"bf-5hyk","title":"Run dashboard jest suite and confirm per-file output","description":"Prerequisite: child bf-licg (npm ci) must have succeeded. If npm ci failed, this bead is blocked — do not proceed.\n\nScope: discovery only — NO source fixes. Confirm the jest runner itself works and emits per-file results (the unknown this chain de-risks).\n\nSteps:\n1. cd dashboard/ && npx jest --verbose (full suite).\n2. If the runner errors BEFORE producing any per-file results (config error, transform error, missing binary, OOM), record the verbatim runner error and stop. Do NOT patch source/config to fix it.\n3. If the runner produces per-file PASS/FAIL lines (even if many tests fail), record that per-file output was produced and capture the raw jest summary output — child 3 turns it into the table.\n\nAcceptance criteria:\n- The jest runner status is documented in a comment on this bead: either 'per-file output produced' (with the raw summary) OR the verbatim runner-level error.\n- No source changes are made.\n- This bead does NOT build the 13-row table — that is child bf-.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T10:53:24.434940705Z","updated_at":"2026-07-04T10:53:55.837886960Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5hyk","depends_on_id":"bf-licg","type":"blocks","created_at":"2026-07-04T10:53:55.831583691Z","created_by":"cli","thread_id":""}]} -{"id":"bf-5hz","title":"Fix hardcoded version string in api/status.go","description":"mothership/internal/api/status.go line 98 returns hardcoded version '1.0.0' instead of the actual build version. Wire the VERSION file or build-time ldflags version into the StatusHandler so GET /api/status returns the real deployed version (currently 0.1.357+).","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":3,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-06-27T12:12:21.142800425Z","updated_at":"2026-07-04T08:26:03.631094489Z","source_repo":".","compaction_level":0,"labels":["deferred","umbrella"],"dependencies":[{"issue_id":"bf-5hz","depends_on_id":"bf-11dj","type":"blocks","created_at":"2026-07-04T08:26:03.621918003Z","created_by":"cli","thread_id":""}]} -{"id":"bf-5k8y","title":"Print one PASS/FAIL line per test reflecting outcome, with passed/failed counters","description":"Split-child 3 of 3 (the LAST child) for parent bf-1na (Add main() entry-point skeleton with setjmp recovery loop and run-command header). Builds on child 2 (per-test setjmp/longjmp recovery) and child 1 (run-command comment + iteration). This child finishes bf-1na's scope: replace the neutral per-test line with outcome-based PASS/FAIL reporting and wire up the passed/failed counters.\n\nScope (extend firmware/test/test_runner.c):\n- In main()'s per-test loop, branch on the setjmp(g_test_jmp) return value:\n - 0 (direct return — the body ran clean): call g_tests[i].fn(), then printf(\"PASS: %s\\n\", g_tests[i].name); and bump the local passed counter.\n - non-zero (longjmp return — a failed assertion tripped test_record_failure()): printf(\"FAIL: %s\\n\", g_tests[i].name); and bump the local failed counter. Do NOT re-invoke the body.\n- Remove the neutral RUN: line from child 1/2; the per-test output is now exactly one PASS or FAIL line, labeled by outcome.\n- return 0; — the non-zero-on-failure exit code is explicitly a LATER sibling and is OUT OF SCOPE for bf-1na. (g_failure_count already exists from bf-3id; do not read it here.) Likewise a run SUMMARY line and name-sorting are out of scope for bf-1na — do not add them.\n\nAcceptance criteria:\n- Exactly one PASS/FAIL line is printed per registered test, with the correct label for that test's outcome (PASS when its assertions all hold, FAIL when one trips test_record_failure()).\n- passed + failed == g_test_count at end of run.\n- With the existing test_sanity.c (all tests passing), `make -C firmware/test test` compiles cleanly with gcc -std=c11 -Wall -Wextra, links, runs, prints \"PASS: arithmetic_sanity\", and exits 0.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T15:20:02.447863050Z","updated_at":"2026-07-03T15:20:02.447863050Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5k8y","depends_on_id":"bf-53ut","type":"blocks","created_at":"2026-07-03T15:20:11.170911219Z","created_by":"cli","thread_id":""}]} -{"id":"bf-5o67","title":"Fix bit-rotted dashboard jest tests until npm test passes locally","description":"Scope: stabilize the dashboard jest suite so `npm test` passes cleanly in dashboard/. Consume the catalog produced by the prior bead (bf-4g7o).\n\nSteps:\n1. Read the failing-test catalog from bf-4g7o.\n2. For each failing test, determine whether the test or the source under dashboard/js/ is wrong (bit-rot = source drifted; assert against current intended behavior, do not weaken coverage to force green).\n3. Repair until `npm test` exits 0 from dashboard/.\n4. If a failure is genuinely a pre-existing product bug (not test rot), surface it — do not paper over it.\n\nAcceptance criteria:\n- `npm ci && npm test` from dashboard/ exits 0 locally.\n- Every previously-failing test is either correctly fixed or flagged as a real product bug with rationale.\n- No test is deleted or weakened merely to go green; coverage intent is preserved.\n\nDepends on: bf-4g7o (catalog).","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T09:52:09.649228351Z","updated_at":"2026-07-04T09:52:47.925832052Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5o67","depends_on_id":"bf-4g7o","type":"blocks","created_at":"2026-07-04T09:52:47.899831175Z","created_by":"cli","thread_id":""}]} -{"id":"bf-5qsy","title":"Confirm the passing test's neutral RUN line is present in stdout","description":"Split-child of parent bf-ba2u (auto-split). Depends on child 2.\n\nGOAL: confirm the passing companion test emitted its neutral RUN line (i.e. the runner kept going after the failure).\n\nSTEPS:\n- Read /tmp/bf38e9_test_stdout.txt (produced by child 1).\n- Confirm the literal line is present:\n RUN: aab_bf38e9_passes_after_fail\n\nACCEPTANCE CRITERIA:\n- The exact string 'RUN: aab_bf38e9_passes_after_fail' appears at least once in /tmp/bf38e9_test_stdout.txt.\n- No re-run of the suite required; no source changes.\n\nIf the line is MISSING, that is a REAL regression in test_runner.c's recovery loop (the passing test never ran after the failure) — STOP and report it; do not edit the verify file to mask it.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T20:25:54.828418839Z","updated_at":"2026-07-03T21:09:52.006817806Z","closed_at":"2026-07-03T21:09:52.006817806Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-5qsy","depends_on_id":"bf-577f","type":"blocks","created_at":"2026-07-03T20:26:26.866887910Z","created_by":"cli","thread_id":""}],"comments":[{"id":22,"issue_id":"bf-5qsy","author":"cli","text":"Verified /tmp/bf38e9_test_stdout.txt: the exact line 'RUN: aab_bf38e9_passes_after_fail' is present (appears once, line 5). This confirms test_runner.c's recovery loop kept going after the intentional assertion failure of 'aaa_bf38e9_intentional_fail' (line 4). Final tally: 30 passed, 1 failed of 31 -- only the intentional test failed; no real regression in the recovery loop. No source changes made (task required none).","created_at":"2026-07-03T21:08:49.838414825Z"}]} -{"id":"bf-5ujd","title":"Aggregate dashboard jest test catalog onto umbrella bead","description":"Scope: rollup only — NO source fixes, NO re-running the suite. Compile the batch catalogs into the single unified catalog the original (now umbrella) bead asked for. This is the done-signal for the umbrella.\n\nSteps:\n1. Gather the file-level PASS/FAIL table (baseline bead) + per-test catalogs (batch A + batch B beads).\n2. Produce ONE unified catalog covering all 13 files: per-file PASS/FAIL, and under each FAIL file the list of failing tests with first-line errors.\n3. Post the unified catalog as a single comment on the parent umbrella bead bf-4g7o.\n\nAcceptance criteria:\n- A single unified catalog comment covering all 13 test files is posted on bf-4g7o.\n- Every failing test across the whole suite appears with file + test name + error summary.\n- No source changes are made.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T10:22:33.365206353Z","updated_at":"2026-07-04T10:22:48.682480181Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5ujd","depends_on_id":"bf-4er0","type":"blocks","created_at":"2026-07-04T10:22:48.665550842Z","created_by":"cli","thread_id":""}]} -{"id":"bf-5wky","title":"Verify dashboard npm ci prerequisites and environment","description":"Scope: discovery only — NO source fixes. Establish that the dashboard install can even be attempted before running it.\n\nSteps:\n1. Confirm dashboard/ exists and is an npm project (has package.json).\n2. Confirm dashboard/package-lock.json exists — npm ci REQUIRES it; its absence is a common, legitimate failure worth catching in isolation.\n3. Record node version (node --version) and npm version (npm --version).\n\nAcceptance criteria:\n- node version, npm version, and presence/absence of package-lock.json are recorded in a comment.\n- No source changes (package.json / package-lock.json untouched).\n- Does NOT run npm ci — that is the next child.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T11:21:58.561167136Z","updated_at":"2026-07-04T11:37:02.999716767Z","closed_at":"2026-07-04T11:37:02.999716767Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","failure-count:1","split-child"],"comments":[{"id":67,"issue_id":"bf-5wky","author":"cli","text":"Dashboard npm ci prerequisites — discovery results:\n\n1. dashboard/ is an npm project: YES — dashboard/package.json present (name: spaxel-dashboard, private; devDependencies: jest, @playwright/test, @axe-core/playwright, http-server, jest-environment-jsdom).\n2. dashboard/package-lock.json present (npm ci REQUIRES it): YES — 164881 bytes.\n3. Toolchain: node v20.19.2, npm 9.2.0.\n\nConclusion: all npm ci prerequisites satisfied; npm ci can be attempted (next child). No source changes made; npm ci was NOT run per scope.","created_at":"2026-07-04T11:31:35.841622269Z"},{"id":68,"issue_id":"bf-5wky","author":"cli","text":"Dashboard npm prerequisites — discovery (no source changes):\n\n1. dashboard/ exists and is an npm project: package.json PRESENT (name: spaxel-dashboard, private: true; devDeps: jest, @playwright/test, @axe-core/playwright, http-server, jest-environment-jsdom).\n2. dashboard/package-lock.json: PRESENT — lockfileVersion 3, 361 package entries, requires:true. npm ci can be attempted (lockfile is the required precondition).\n3. Toolchain: node v20.19.2, npm 9.2.0.\n\npackage.json and package-lock.json left untouched. npm ci NOT run — deferred to next child.","created_at":"2026-07-04T11:35:43.788073525Z"}]} -{"id":"bf-6201","title":"Confirm the all-passing suite runs to completion under make","description":"Split-child 3 of 4 for umbrella bf-tof1. Dynamic: confirm the suite actually builds and runs end-to-end with no regression for the all-passing suite.\n\nPrecondition: the working tree must compile. If firmware/test/test_runner.c is in an unmerged (UU) index state, stage the resolved content (git add firmware/test/test_runner.c) before building so make sees a clean file; if the build fails for a reason unrelated to the guard, fix ONLY within firmware/test/test_runner.c per the parent scope.\n\nScope:\n- From the repo root run: make -C firmware/test test\n- Confirm the process COMPLETES — it does not hang, abort, crash, or wedge. (Capture wall-clock; a host unit-test run is sub-second.)\n- Confirm the build is clean: plain gcc, no setjmp-related warnings (e.g. -Wclobbered) under -Wall -Wextra.\n- Confirm exit code is 0 (main() returns 0 unconditionally for the all-passing suite — that is expected for this intermediate state, NOT a regression).\n\nAcceptance criteria:\n- A comment recording the make exit code and the tail of its output.\n- Process completed on its own (no manual kill / no timeout).\n- No new compiler warnings vs. the pre-change baseline.\n- Closes independently once a clean run-to-completion is demonstrated.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T01:52:06.988183008Z","updated_at":"2026-07-04T06:23:38.463677653Z","closed_at":"2026-07-04T06:23:38.463677653Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-6201","depends_on_id":"bf-2opw","type":"blocks","created_at":"2026-07-04T01:52:39.567482113Z","created_by":"cli","thread_id":""}],"comments":[{"id":55,"issue_id":"bf-6201","author":"cli","text":"Verification bead: confirmed the all-passing firmware host-test suite builds and runs end-to-end under make with no regression.\n\nCOMMAND: make -C firmware/test test (from repo root)\nMAKE EXIT CODE: 0\nWALL-CLOCK: ~0.42 s (clean rebuild + full run; sub-second as expected for a host unit-test run)\nPROCESS: completed on its own — no hang/abort/crash/wedge, no manual kill, no timeout. 'make: Leaving directory' printed normally.\n\nBUILD CLEANLINESS (no new warnings vs baseline):\n- Precondition: firmware/test/test_runner.c was NOT in an unmerged (UU) state (git ls-files -u empty); working tree clean, no staging needed.\n- Plain gcc (cc -> gcc 14.2.0), CFLAGS=-std=c11 -Wall -Wextra -O2 -g: stderr empty on clean rebuild; grep 'warning' -> NONE.\n- Hardened re-check: same sources under -Wall -Wextra -Werror -> rc=0 (any warning would fail it).\n- Explicit -Wclobbered pass: rc=0, NO -Wclobbered diagnostics. The setjmp loop index in main() is volatile-qualified (commit ce70491 / bf-2k3o), so the loop-index-across-setjmp shape -Wclobbered flags is gone — no pragma, no flag downgrade.\n\nSUITE: 29 tests self-registered via TEST() constructors, sorted by name, each driven through the setjmp(g_test_jmp)/longjmp recovery loop. main() returns 0 unconditionally for this all-passing intermediate state (PASS/FAIL tally + non-zero-on-failure exit are sibling scope, not regression) — exit 0 is the expected contract here.\n\nTAIL OF OUTPUT:\n RUN: serial_prov_missing_ssid_rejected\n RUN: serial_prov_port_wrong_type_ignored\n RUN: serial_prov_string_escapes_decoded\n RUN: serial_prov_top_level_array_is_missing_key\n RUN: serial_prov_valid_full_payload\n make: Leaving directory '/home/coding/spaxel/firmware/test'\n(All 29 RUN markers emitted in sorted order: arithmetic_sanity .. serial_prov_valid_full_payload; no assertions fired.)\n\nNO SOURCE CHANGES: confirmation bead — firmware/test/ was already in the clean-running committed state (the volatile-qualify it depends on is commit ce70491, already in HEAD). Nothing to commit in firmware/test/. Clean run-to-completion demonstrated; closes independently.","created_at":"2026-07-04T06:23:38.456745219Z"}]} -{"id":"bf-637g","title":"Return non-zero exit code iff any test failed","description":"Child 4 of the bf-bq9 sub-split (parent bf-bq9; grandparent bf-2i4). Depends on child 3 (bf-4w29: summary). This is the FINAL child — completing it unblocks the bf-bq9 umbrella and completes the runner.\n\nScope (extend firmware/test/test_runner.c main()):\n- Replace the placeholder return with the CI exit-code contract: return g_failure_count > 0 ? 1 : 0. g_failure_count is the run-wide counter from sibling bf-3id that test_record_failure() bumps on each failed assertion; it is monotonic across the whole run (never reset), so this returns non-zero iff at least one test failed anywhere. Do NOT re-count failures here — g_failure_count is the single source of truth for 'did anything fail'; the local passed/failed counters are only for the summary line.\n\nAcceptance criteria:\n- main() returns 1 iff at least one registered test called test_record_failure (g_failure_count > 0); 0 otherwise.\n- This non-zero-on-failure return is what `make -C firmware/test test` propagates to CI.\n- Compiles cleanly with gcc -std=c11 -Wall -Wextra; the suite links, runs, exits 0 when all tests pass and non-zero when any fail.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T14:48:53.998217295Z","updated_at":"2026-07-03T14:48:53.998217295Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-637g","depends_on_id":"bf-4w29","type":"blocks","created_at":"2026-07-03T14:49:21.777138379Z","created_by":"cli","thread_id":""}]} -{"id":"bf-65m5","title":"Verify interleaving: a failing test followed by a passing test yields two lines in order","description":"Split-child 4 of 4 for parent bf-38e9 — the LAST child; closing it unblocks the parent. Depends on child 3 (bf-it0l).\n\nGOAL: prove the recovery-loop contract end-to-end — when test N fails, the lines for tests N+1..end STILL appear in order. This interleaving is how bf-38e9's acceptance is verified.\n\nSteps:\n- Reuse the existing temporary verification pattern in firmware/test/test_bf38e9_verify.c: an intentionally-failing TEST whose name sorts BEFORE an existing passing test, e.g.:\n\n TEST(aaa_bf38e9_intentional_fail) { ASSERT_EQ(1, 2); }\n\n The leading \"aaa_\" makes qsort run it first so it fails and longjmps back, then the passing test runs.\n- Build and run: make -C firmware/test test.\n- Confirm the output shows the failing test's neutral line (e.g. \"RUN: aaa_bf38e9_intentional_fail (assertion failed)\") FOLLOWED BY the passing test's neutral line (\"RUN: \"), IN ORDER, and that the process COMPLETES (exit code 1 from the failure — NOT an abort/crash).\n- DELETE the temporary test_bf38e9_verify.c file afterward so it is NOT committed (it is a throwaway verify artifact).\n\nAcceptance criteria:\n- make -C firmware/test test prints the failing test's neutral line THEN the passing test's neutral line, in order.\n- The process completes and returns non-zero (no abort).\n- The temporary verify file is removed (git status shows it gone) — nothing throwaway is committed.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T17:15:48.943843914Z","updated_at":"2026-07-03T22:25:24.115630775Z","closed_at":"2026-07-03T22:25:24.115630775Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-65m5","depends_on_id":"bf-it0l","type":"blocks","created_at":"2026-07-03T17:16:28.440176304Z","created_by":"cli","thread_id":""},{"issue_id":"bf-65m5","depends_on_id":"bf-4jnd","type":"blocks","created_at":"2026-07-03T19:38:00.494112253Z","created_by":"cli","thread_id":""}],"comments":[{"id":26,"issue_id":"bf-65m5","author":"cli","text":"Verification bead (child 4/4 of bf-38e9): proved the host-harness recovery-loop contract end-to-end.\n\nApproach: created throwaway firmware/test/test_bf38e9_verify.c with TEST(aaa_bf38e9_intentional_fail) { ASSERT_EQ(1, 2); }. The leading 'aaa_' makes qsort run it first so it fails+longjmps back, then the passing tests run.\n\nResult of 'make -C firmware/test test' (verify file present):\n- RUN: aaa_bf38e9_intentional_fail (assertion failed) <- failing test's neutral line, FIRST\n- RUN: arithmetic_sanity <- passing test's neutral line follows, in order\n- remaining 28 tests run to completion; summary '29 passed, 1 failed of 30'\n- binary exits 1, make propagates non-zero; process COMPLETED normally (no abort/crash)\n\nThis proves: when test N fails, lines for tests N+1..end STILL appear in order (per-test setjmp/longjmp loop in test_runner.c main() recovers).\n\nCleanup: deleted test_bf38e9_verify.c (throwaway, not committed). Clean rebuild returns to green baseline (29 passed, 0 failed, exit 0). No changes to firmware/main/* or firmware/CMakeLists.txt. No spaxel code diff — by design, since the verified contract was already implemented by prior sibling beads.","created_at":"2026-07-03T22:24:48.027474939Z"}]} -{"id":"bf-6761","title":"Empirically probe whether gcc -Wclobbered fires on test_runner.c at -O0..-Os and whether dropping qsort triggers it","description":"Split-child 2/4 of the bf-31rd re-split (umbrella bf-31rd). Depends on child 1 (bf-4qk2). This is the disambiguation step that the parent conflated with the standard analysis and which caused the 3 failures: gcc's -Wclobbered is heuristic and may require volatile on 'i' EVEN THOUGH the standard (child 1) says i is determinate.\n\nScope (no production code change required in this child — use throwaway compile snippets):\n- Compile firmware/test/test_runner.c standalone with: gcc -std=c11 -Wall -Wextra -Wclobbered at -O0, -O1, -O2, and -Os. Record, per opt level, whether -Wclobbered fires on the loop index 'i' in main()'s per-test loop.\n- Probe the heuristic claim recorded in the existing in-file comment: with the preceding qsort() call present the heuristic stays silent today, but removing qsort (or otherwise un-biasing gcc's register heuristic) makes -Wclobbered fire on 'i'. Reproduce a minimal loop-across-setjmp snippet WITHOUT qsort and confirm whether -Wclobbered fires. Confirm or refute the claim empirically.\n- State the bottom line plainly: does a -Wclobbered-clean build of this TU require 'i' to be volatile-qualified, yes or no (and at which opt levels)?\n\nAcceptance criteria:\n- A comment on THIS bead records, for each of -O0/-O1/-O2/-Os, whether -Wclobbered fires on the current TU, AND records the result of the no-qsort minimal-snippet probe.\n- Bottom-line verdict stated unambiguously: volatile-required-for-clean-build or not, with the opt levels.\n- This child's finding drives child 3's code change. No change to files under firmware/ in this child (probes use throwaway snippets, not committed edits).","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T07:11:48.141113166Z","updated_at":"2026-07-04T07:13:58.223196774Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-6761","depends_on_id":"bf-4qk2","type":"blocks","created_at":"2026-07-04T07:13:58.214538137Z","created_by":"cli","thread_id":""}]} -{"id":"bf-6aj","title":"Create firmware/test/test_runner.c skeleton with libc includes","description":"Sub-split child of bf-lfz (umbrella after this split). bf-lfz is itself a sub-split child of bf-2i4; grandparent bf-56v; great-grandparent bf-4ne; header API lives in bf-1xs. bf-lfz failed 3 times in a row, so this chain decomposes its registry work into three atomic, independently-compilable pieces. This is child 1 of 3 (the skeleton).\n\nScope (child 1 of 3, the skeleton):\n- Create firmware/test/test_runner.c (new file).\n- Add a top-of-file block comment noting the file is built up incrementally across the three children of bf-lfz (registry storage arrives in child 2, test_register in child 3; failure handling and main arrive in the sibling beads bf-3id and bf-bq9). Reference test_runner.h (bf-1xs) and the gcc host harness decision (bf-21t).\n- Add the includes the whole runner needs: \"test_runner.h\", then , , , (libc only; no firmware/main/* includes).\n\nDo NOT add the registry array, the count, or test_register() yet (children 2 and 3).\n\nAcceptance criteria:\n- firmware/test/test_runner.c exists with the comment block and exactly those five includes and nothing else.\n- Compiles cleanly to an object with zero warnings: gcc -std=c11 -Wall -Wextra -c firmware/test/test_runner.c (object only; NO link step; NO main()).\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T12:07:39.666917454Z","updated_at":"2026-07-03T12:42:58.942089190Z","closed_at":"2026-07-03T12:42:58.942089190Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"comments":[{"id":8,"issue_id":"bf-6aj","author":"cli","text":"Auto-split (bf-6aj) NOT honored — this was a false auto-split. Work was already complete and committed in 191adbe (test_runner.c skeleton: comment block + exactly the 5 libc/test_runner.h includes, nothing else; compiles clean with gcc -std=c11 -Wall -Wextra -c, zero warnings; no firmware/main or CMakeLists.txt changes). failure-count:3 was the known br close claimed_at-format bug (4 stale CURRENT_TIMESTAMP claim rows for claude-code-glm-5-alpha), not a scope problem. Normalized claim rows to RFC3339 per [[br-close-claimed-at-rfc3339]] and closed. Root fix tracked in bf-2yj7.","created_at":"2026-07-03T12:43:35.607643995Z"}]} -{"id":"bf-6uz","title":"Sort registered tests by name for deterministic run order","description":"Child 2 of the bf-bq9 sub-split (parent bf-bq9; grandparent bf-2i4). Depends on child 1 (bf-1na: main skeleton + setjmp loop).\n\nScope (extend firmware/test/test_runner.c main()):\n- Add a static comparator test_entry_cmp(const void *a, const void *b) that strcmp()s two test_entry_t by their .name field.\n- At the top of main(), before the per-test loop, call qsort(g_tests, (size_t)g_test_count, sizeof(g_tests[0]), test_entry_cmp) so iteration order is deterministic by name. Rationale: the C standard does NOT guarantee constructor order across translation units (within one TU it follows definition order, but across TUs and across the Makefile's test_*.c glob — which feeds TUs in a glob-dependent order — it is implementation- and link-defined). An unsorted run would order tests however gcc happened to receive them, making a failing run's interleaved PASS/FAIL output non-reproducible. Sorting by name makes it stable, which is what CI log diffing wants.\n\nAcceptance criteria:\n- Registered tests execute in ascending name-sorted order, deterministically, independent of registration or link order.\n- Compiles cleanly with gcc -std=c11 -Wall -Wextra; the suite still runs and exits 0 when all tests pass.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T14:47:42.714379807Z","updated_at":"2026-07-03T14:47:42.714379807Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-6uz","depends_on_id":"bf-1na","type":"blocks","created_at":"2026-07-03T14:48:20.393207891Z","created_by":"cli","thread_id":""}]} -{"id":"bf-833","title":"Update PROGRESS.md: mark Phase 2 pending items as complete","description":"Phase 2 table shows three items as Pending but all are done. Presence indicator is in live.html and app.js. CSI recording buffer is in mothership/internal/recording/buffer.go. Adaptive sensing rate is in mothership/internal/ingestion/ratecontrol.go. Mark all three Done in PROGRESS.md.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-06-27T12:12:17.449862803Z","updated_at":"2026-07-03T04:38:04.438961195Z","closed_at":"2026-07-03T04:38:04.438961195Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred"],"comments":[{"id":2,"issue_id":"bf-833","author":"cli","text":"RESOLVED — work was already complete; closing instead of splitting. Verified state (2026-07-03): all three Phase 2 items already marked Done in PROGRESS.md lines 29-31 — Dashboard presence indicator (dashboard/live.html + dashboard/js/app.js), CSI recording buffer (mothership/internal/recording/buffer.go), Adaptive sensing rate (mothership/internal/ingestion/ratecontrol.go). All referenced files exist. The edits were committed in 9fabeb8 (presence indicator) and 83ec5a5 (CSI buffer + adaptive rate); PROGRESS.md has no uncommitted changes. The 203 failures were because each worker found the items already Done with no diff to produce. Splitting into child beads would have created N already-complete beads failing the same way. Note: the bead body's path 'dashboard/app.js' is slightly off — the real file is dashboard/js/app.js, which the table already records correctly. Rejecting the auto-split as a false-positive (parent is trivial + already done, not complex).","created_at":"2026-07-03T04:20:00.192559818Z"},{"id":3,"issue_id":"bf-833","author":"cli","text":"Phase 2 status update — already satisfied, no change required. All three Phase 2 items were already marked **Done** in PROGRESS.md (lines 29-31) with accurate notes pointing at the exact files named in the task: Dashboard presence indicator (dashboard/live.html + dashboard/js/app.js), CSI recording buffer (mothership/internal/recording/buffer.go), and Adaptive sensing rate (mothership/internal/ingestion/ratecontrol.go). This work landed in the two commits immediately before this bead dispatched: 9fabeb8 (presence indicator) and 83ec5a5 (CSI recording buffer + adaptive sensing rate). PROGRESS.md working tree is clean and all four referenced files exist. The task description ('shows three items as Pending') was stale — desired end-state was already the current state, so no edit/commit/push was made.","created_at":"2026-07-03T04:33:51.868086401Z"}]} -{"id":"bf-ba2u","title":"Run suite and confirm the two neutral RUN lines interleave in order","description":"Split-child 2 of 4 for parent bf-65m5. Depends on child 1 (which created firmware/test/test_bf38e9_verify.c).\n\nGOAL: prove the in-order interleaving — when test N fails, the neutral RUN line for a later (passing) test STILL appears AFTER it, in order. This is the core recovery-loop contract.\n\nSTEPS (read once, act FAST — do not over-explore; the parent timed out 3x):\n- Run: make -C firmware/test test (capture stdout)\n- In stdout, confirm BOTH neutral lines are present and IN ORDER:\n 1. 'RUN: aaa_bf38e9_intentional_fail (assertion failed)' (failing test's line)\n 2. 'RUN: aab_bf38e9_passes_after_fail' (passing test's line)\n The failing line MUST appear BEFORE the passing line in the stream.\n\nACCEPTANCE CRITERIA:\n- make -C firmware/test test output contains the failing test's neutral line.\n- It also contains the passing companion test's neutral line.\n- The failing line precedes the passing line (in-order interleaving).\n- No source changes (this child only runs and observes).\n\nIf the ordering is wrong or a line is missing, that is a REAL regression in test_runner.c's recovery loop — STOP and report it; do NOT edit the verify file to mask it.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T19:37:46.003564048Z","updated_at":"2026-07-03T21:26:29.336805481Z","closed_at":"2026-07-03T21:26:29.336805481Z","close_reason":"Completed: verified in-order interleaving. make -C firmware/test test stdout showed 'RUN: aaa_bf38e9_intentional_fail (assertion failed)' THEN 'RUN: aab_bf38e9_passes_after_fail' — failing line precedes passing line; runner completed all tests (non-zero exit from the single intentional failure, not an abort). Contract HOLDS, no test_runner.c regression. child-1 throwaway artifact was absent on disk (bf-1z44 closed but file gone); recreated exactly per spec to observe, then removed it — suite back to green (29/29, exit 0). No source changes.","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-ba2u","depends_on_id":"bf-1z44","type":"blocks","created_at":"2026-07-03T19:37:59.430294338Z","created_by":"cli","thread_id":""},{"issue_id":"bf-ba2u","depends_on_id":"bf-1oak","type":"blocks","created_at":"2026-07-03T20:26:27.009926867Z","created_by":"cli","thread_id":""}]} -{"id":"bf-bq9","title":"Implement firmware/test/ main runner with sort, setjmp loop, summary, and exit code","description":"Sub-split child of bf-2i4 (parent bf-2i4; grandparent bf-56v; great-grandparent bf-4ne). Depends on child 2 (failure handling). Add main() to firmware/test/test_runner.c. This completes the runner.\n\nScope (extend firmware/test/test_runner.c):\n- Implement main(): sort the registered tests by name (deterministic order regardless of constructor or link order); for each test, setjmp() into the per-test target (from child 2), then call fn(); on longjmp return print FAIL and count the failure, otherwise print PASS.\n- After all tests, print a summary (e.g. N passed, M failed of T).\n- Return 1 if any test called test_record_failure, else 0. This non-zero-on-failure return is the contract CI relies on.\n- A header comment block naming the documented run command: make -C firmware/test test (per the bf-1xs header contract and bf-56v harness decision).\n\nAcceptance criteria:\n- main() returns non-zero (1) iff at least one registered test called test_record_failure; 0 otherwise.\n- Tests run in deterministic name-sorted order.\n- A failing assertion in one test does not prevent later tests from running (per-test setjmp/longjmp).\n- firmware/test/test_runner.c compiles cleanly with gcc -std=c11 -Wall -Wextra; with at least one TEST() defined it links and runs, exiting 0 when all pass.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T11:35:55.598020436Z","updated_at":"2026-07-03T14:51:22.121292937Z","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-bq9","depends_on_id":"bf-3id","type":"blocks","created_at":"2026-07-03T11:36:30.172375841Z","created_by":"cli","thread_id":""},{"issue_id":"bf-bq9","depends_on_id":"bf-637g","type":"blocks","created_at":"2026-07-03T14:49:21.792550351Z","created_by":"cli","thread_id":""}]} -{"id":"bf-brz","title":"Add firmware host-based unit tests (nvs migration, CSI frame serialization, provisioning parser)","description":"docs/plan/plan.md 'Testing Strategy > Firmware Tests (host-based unit tests)' mandates host tests for three firmware modules, but firmware/ contains NO test files at all.\n\nRequired per plan:\n- nvs — NVS schema migration: simulate schema_ver=0 to 1 upgrade (source: firmware/main/nvs_migration.c/.h)\n- csi — binary frame serialization: verify frame header fields and little-endian encoding (source: firmware/main/csi.c/.h; the 24-byte binary frame format is specified in docs/plan/plan.md Component 1/2 and mirrored by the Go parser in mothership/internal/ingestion/)\n- serial_prov — provisioning JSON parser: valid JSON parsed correctly; invalid JSON returns {\"ok\":false} (source: firmware/main/provision.c/.h)\n\nImplementation guidance:\n- Preferred: ESP-IDF host-based testing (idf.py --target linux / Unity test framework) as the plan specifies. If the ESP-IDF linux target proves impractical for these files (heavy esp_* dependencies), an acceptable alternative is extracting the pure-logic parts (frame packing, JSON parse, migration decision table) into functions testable from a plain C host harness compiled with gcc, kept under firmware/test/.\n- Keep tests deterministic; no hardware or network required.\n- Document how to run them in a short section of the test file header or firmware/test/README-less comment block (do not create new top-level docs).\n\nAcceptance criteria:\n- Tests exist under firmware/ (e.g. firmware/test/) covering all three modules above with at least: one schema_ver 0-to-1 migration case, one little-endian frame round-trip case incl. header fields, one valid + one invalid provisioning JSON case\n- A single documented command runs them on a Linux host and exits non-zero on failure\n- Existing firmware build (firmware/CMakeLists.txt, Dockerfile firmware stage) still builds unchanged","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"feature","assignee":"","created_at":"2026-07-02T11:28:54.638579758Z","updated_at":"2026-07-03T05:52:52.328765219Z","source_repo":".","compaction_level":0,"labels":["deferred","failure-count:4","umbrella"],"dependencies":[{"issue_id":"bf-brz","depends_on_id":"bf-25r","type":"blocks","created_at":"2026-07-03T05:50:38.082190786Z","created_by":"cli","thread_id":""}]} -{"id":"bf-decb","title":"Add replay seek-to-timestamp fuzz test","description":"Plan target #2 (docs/plan/plan.md, Testing Strategy). Genuinely new work: NO fuzz test for replay seek exists yet. Add a native fuzz target in the replay package (file named *_fuzz_test.go, package replay). Entry points to fuzz (pick the most appropriate): Engine.Seek(id string, targetMS int64) error in engine.go; Session.SeekTo(targetMS int64) error in types.go; plus helpers ValidateRange and ClampTimestamp in session.go. Fuzz with arbitrary int64 timestamps: before session range, after range, exactly at fromMS and toMS boundaries, and at wrap points (math.MinInt64, math.MaxInt64, 0, -1, negative values). FIELD-NAME DISCREPANCY: the plan states the property as 'returned frame (if any) has recv_time_ms >= target', but there is NO recv_time_ms field. The actual frame timing field is ingestion Frame.RecvTimeNS (nanoseconds, int64) alongside RecvTime (time.Time). Adapt the property to compare in consistent units (e.g. compare against targetMS scaled to ns, or clamp targetMS into the session range first). Properties: (1) never panics on any int64 target; (2) if a frame is returned after a seek, its timestamp is >= the (clamped) target. Conventions: seed corpus entries so plain go test ./... runs seeds quickly; long fuzzing only via go test -fuzz. AC: new *_fuzz_test.go in replay package; go test ./internal/replay/... green and fast; go test -fuzz on the target runs >=10s locally with no findings (or found bugs fixed in the same change); file committed.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T09:19:14.276326735Z","updated_at":"2026-07-04T09:19:30.016507188Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-decb","depends_on_id":"bf-3uqf","type":"blocks","created_at":"2026-07-04T09:19:30.004634197Z","created_by":"cli","thread_id":""}]} -{"id":"bf-i90p","title":"Add regression test for version surfacing","description":"Add a handler-level regression test proving GET /api/status returns whatever version string was passed to NewStatusHandler (mirrors the build-time -X main.version ldflag), guarding against a future regression to a hardcoded constant.\n\nScope (mothership/internal/api/status_test.go):\n- Define a statusResponse struct mirroring the JSON written by getStatus.\n- Add a chi-mounted helper router for the handler.\n- TestStatusHandlerVersionWired: table-driven across representative version strings — \"0.1.357\", \"dev\", \"0.1.358-dirty\", and \"\" — asserting response.Version equals the input for each.\n\nAcceptance criteria:\n- Test passes for all table rows.\n- 'go test ./internal/api/...' is green.\n- Test would fail if getStatus were reverted to a hardcoded constant.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","created_at":"2026-07-04T08:25:42.539272633Z","updated_at":"2026-07-04T08:26:03.553441447Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-i90p","depends_on_id":"bf-iekf","type":"blocks","created_at":"2026-07-04T08:26:03.546408565Z","created_by":"cli","thread_id":""}]} -{"id":"bf-id5","title":"Repo hygiene: remove stray tracked artifacts and reconcile duplicate test/sim trees","description":"Several stray artifacts are tracked in git, and the test/simulator trees are duplicated in confusing ways. Careful: CI templates in the jedarden/declarative-config repo (spaxel-build and spaxel-e2e WorkflowTemplates under k8s/iad-ci/argo-workflows/) reference specific paths - map their references FIRST and only remove what nothing uses.\n\nKnown strays (tracked in git):\n- fix_ble_handlers.py at repo root - one-off fixup script, not part of the product; delete it\n- dashboard/test-results/.last-run.json - playwright run artifact; delete and add dashboard/test-results/ to .gitignore\n- mothership/cmd/_parse_check.go - stray scratch file next to real cmds; verify unused and delete\n\nDuplicated trees to reconcile (do NOT delete blindly - check CI references first):\n- cmd/sim (own go.mod, listed in go.work) vs mothership/cmd/sim - two simulator sources. spaxel-build builds ./cmd/sim from the mothership module dir; determine which is canonical, delete or clearly de-duplicate the other, update go.work if the root module goes away\n- test/acceptance (own go.mod, in go.work; files as1_setup_test.go etc.) vs mothership/test/acceptance (files as1_first_time_setup_test.go etc.) - both are exercised by spaxel-e2e template at different steps; if both are intentionally kept (module-external vs in-module suites), add a short comment/doc line in each explaining the split instead of deleting\n- tests/e2e/run.sh vs mothership/tests/e2e/e2e_test.go - same: identify which CI path uses each, document or consolidate\n\nAcceptance criteria:\n- The three stray files above are gone from git tracking; .gitignore covers playwright artifacts and local binaries (mothership/mothership, mothership/sim if not already ignored)\n- Every remaining test/sim tree is either referenced by CI or by go.work, and each carries a one-line purpose note (file comment) so the duplication is explainable\n- go build ./... and go test ./... still succeed in the mothership module; go.work remains consistent\n- No CI template edits required, OR if a path must change, the corresponding declarative-config template is updated in the same effort","design":"","acceptance_criteria":"","notes":"","status":"open","priority":3,"issue_type":"task","created_at":"2026-07-02T11:30:40.485665889Z","updated_at":"2026-07-03T17:38:41.167352165Z","source_repo":".","compaction_level":0} -{"id":"bf-iekf","title":"Wire version field through StatusHandler","description":"Replace the hardcoded \"1.0.0\" constant in mothership/internal/api/status.go getStatus() with a version sourced from the build.\n\nScope:\n- Add a 'version string' field to the StatusHandler struct.\n- Extend NewStatusHandler signature to accept version (e.g. NewStatusHandler(startTime, getNodeCount, version)).\n- In getStatus(), set the JSON \"version\" field to h.version instead of the literal \"1.0.0\".\n- Update the call site in mothership/cmd/mothership/main.go to pass the existing build-time 'version' var (the one already used by healthz and logged at startup).\n\nAcceptance criteria:\n- No literal \"1.0.0\" remains in status.go.\n- NewStatusHandler stores version and getStatus surfaces it.\n- main.go passes 'version' to NewStatusHandler.\n- Package compiles (go build ./...).","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T08:25:39.154035517Z","updated_at":"2026-07-04T08:31:45.449272038Z","closed_at":"2026-07-04T08:31:45.449272038Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"comments":[{"id":63,"issue_id":"bf-iekf","author":"cli","text":"Wired build-time version through StatusHandler. Added 'version string' field to StatusHandler, extended NewStatusHandler(startTime, getNodeCount, version), set GET /api/status 'version' to h.version (removed hardcoded 1.0.0 literal). Updated cmd/mothership/main.go call site to pass the existing main.version ldflag var (same one healthz uses and startup logs). Added table-driven handler tests (status_test.go). Verified go build/vet/test all pass; no literal 1.0.0 remains in status.go. Commit 47caab4 on main.","created_at":"2026-07-04T08:31:33.945363893Z"}]} -{"id":"bf-it0l","title":"Audit both loop paths for exactly one observable line per test","description":"Split-child 3 of 4 for parent bf-38e9. Depends on child 2 (bf-344n).\n\nGOAL: confirm the parity contract holds — a passing test emits exactly ONE observable line and a failing test emits exactly ONE observable line (no double-print, no zero-print). Verify by BOTH code inspection AND a minimal build/run:\n\n- Inspect main()'s loop: the body g_tests[i].fn() must NOT itself print a competing per-test marker on the direct path. (On failure, test bodies print failure detail to stderr via test_record_failure — that is separate and fine; it is not a per-test RUN line.)\n- Build and run the existing suite: make -C firmware/test test. Confirm each registered test yields exactly one RUN line.\n\nThis child makes NO source change unless the audit reveals a parity bug (in which case fix only the minimal cause and note it in a comment). If both paths already emit exactly one line, close as verified-no-change.\n\nAcceptance criteria:\n- A single passing TEST emits exactly one observable line; a single failing TEST emits exactly one observable line.\n- No test prints zero or two per-test lines.\n- Compiles cleanly: gcc -std=c11 -Wall -Wextra.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T17:15:22.357255240Z","updated_at":"2026-07-03T19:04:32.158506209Z","closed_at":"2026-07-03T19:04:32.158506209Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-it0l","depends_on_id":"bf-344n","type":"blocks","created_at":"2026-07-03T17:16:28.412586711Z","created_by":"cli","thread_id":""}],"comments":[{"id":17,"issue_id":"bf-it0l","author":"cli","text":"Verified-no-change: parity contract holds on both loop paths.\n\nCODE INSPECTION (test_runner.c main(), lines 257-264):\n- Direct/passing path (setjmp==0): body runs to completion, then exactly ONE printf(RUN: ). No second marker.\n- Failure path (else branch): exactly ONE printf(RUN: (assertion failed)). No second marker.\n- The body g_tests[i].fn() does NOT emit a competing per-test marker: grep of all test bodies (test_sanity/csi_frame/nvs_migration/serial_prov) shows only snprintf buffer writes, never printf/fprintf to stdout. The only stderr output is via test_record_failure (failure detail -- explicitly permitted, not a per-test RUN line).\n\nBUILD & RUN (make -C firmware/test test): 29 tests, exit 0, exactly 29 RUN lines on stdout (one per test) + summary line.\n\nFAILING-PATH VERIFICATION (throwaway /tmp harness: one passing + one deliberately-failing test, same test_runner.c): failing test emits exactly ONE stdout RUN line plus the permitted single stderr failure-detail line; exit 1. No zero-print, no double-print.\n\nCOMPILE: gcc -std=c11 -Wall -Wextra compiles the whole firmware/test glob with zero warnings.\nNO SOURCE CHANGES: firmware/main/*, firmware/CMakeLists.txt, and all firmware/ sources untouched (git status clean). Closed verified-no-change per the task's audit-only instruction.","created_at":"2026-07-03T19:03:37.987993064Z"}]} -{"id":"bf-j0lh","title":"Post jest baseline table as comment on bf-3itg","description":"Prerequisite: child bf-489z (table builder) must have produced the completed 13-row table.\n\nScope: publish the baseline so it is visible on the parent tracking bead. This is the final, externally-visible deliverable of the split chain.\n\nSteps:\n1. Take the 13-row jest file-level PASS/FAIL table produced in bf-489z.\n2. Post it as a comment on the PARENT bead bf-3itg (not on this child).\n3. Also record on this child a one-line confirmation of where/when it was posted.\n\nAcceptance criteria:\n- A comment containing the full 13-row table is posted on bf-3itg.\n- The comment includes the npm ci outcome (success or blocking error) carried forward from bf-licg, so the parent bead captures both acceptance criteria (install outcome + table) in one place.\n- No source changes are made.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T10:54:20.078035028Z","updated_at":"2026-07-04T10:54:42.545666720Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-j0lh","depends_on_id":"bf-489z","type":"blocks","created_at":"2026-07-04T10:54:42.539253902Z","created_by":"cli","thread_id":""}]} -{"id":"bf-lfz","title":"Implement firmware/test/ test registry storage and test_register","description":"Sub-split child of bf-2i4 (parent bf-2i4; grandparent bf-56v; great-grandparent bf-4ne). Build ONLY the test registry portion of firmware/test/test_runner.c. Later children add failure handling and main().\n\nScope (create firmware/test/test_runner.c):\n- Includes needed across the runner: test_runner.h, plus setjmp.h, stdio.h, stdlib.h, string.h (libc only).\n- Registry storage: a static fixed-capacity array of test entries (a struct holding const char *name and test_fn fn), capacity 128, plus a static count.\n- Implement test_register(const char *name, test_fn fn): append the entry in construction order (the order constructors fire); guard against exceeding capacity (print to stderr and skip, never overflow).\n\nDo NOT implement test_record_failure or main() yet (children 2 and 3).\n\nAcceptance criteria:\n- firmware/test/test_runner.c exists with the registry array and test_register().\n- test_register stores entries in construction order and does not overflow capacity 128.\n- Compiles cleanly to an object: gcc -std=c11 -Wall -Wextra -c firmware/test/test_runner.c (against the bf-1xs header; no link step, no main yet).\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T11:35:47.710992391Z","updated_at":"2026-07-03T13:25:16.631866727Z","closed_at":"2026-07-03T13:25:16.631866727Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-lfz","depends_on_id":"bf-oe1","type":"blocks","created_at":"2026-07-03T12:09:53.322475532Z","created_by":"cli","thread_id":""}],"comments":[{"id":11,"issue_id":"bf-lfz","author":"cli","text":"Test registry storage + test_register() for firmware/test/test_runner.c — COMPLETE and on origin.\n\nImplementation (firmware/test/test_runner.c):\n- Includes: test_runner.h + setjmp.h, stdio.h, stdlib.h, string.h (libc only)\n- Registry storage: static test_entry_t g_tests[MAX_TESTS] (MAX_TESTS=128) + static int g_test_count\n- test_register(name, fn): appends in construction order at g_tests[g_test_count]; on full registry prints to stderr (names skipped test + cap) and returns without writing — never overflows\n\nAcceptance (all pass): gcc -std=c11 -Wall -Wextra -c compiles clean (exit 0); registry symbols present in object; out-of-scope main()/test_record_failure() correctly absent; no changes to firmware/main/* or firmware/CMakeLists.txt.\n\nThis bead was a clean duplicate of work a parallel worker already pushed (origin ef1360e, byte-identical to the local attempt). Reconciled local to origin (dropped the unpushed duplicate commit, no force-push) and re-verified the full acceptance matrix against the origin-aligned tree.","created_at":"2026-07-03T13:24:09.277460777Z"}]} -{"id":"bf-licg","title":"Run dashboard npm ci and record install outcome","description":"Scope: discovery only — NO source fixes. De-risk the most common dashboard test-runner failure (broken install) before touching jest.\n\nSteps:\n1. cd dashboard/ && npm ci.\n2. If install succeeds, record 'success' plus the installed node_modules/node version context and stop — child 2 takes over from here.\n3. If npm ci fails, record the EXACT error output verbatim and stop. Do NOT patch package.json, package-lock.json, or any source to fix the install — the error itself is the finding.\n\nAcceptance criteria:\n- The npm ci outcome is documented in a comment on this bead (success, or the verbatim blocking error).\n- No source changes are made (package.json / package-lock.json untouched).\n- This bead does NOT run jest or build the table — that is downstream children.","design":"","acceptance_criteria":"","notes":"","status":"blocked","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T10:52:57.616213847Z","updated_at":"2026-07-04T11:22:30.106051480Z","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-licg","depends_on_id":"bf-2m9s","type":"blocks","created_at":"2026-07-04T11:22:30.084722556Z","created_by":"cli","thread_id":""}],"comments":[{"id":66,"issue_id":"bf-licg","author":"cli","text":"npm ci OUTCOME: SUCCESS (exit 0)\n\nRan: cd dashboard/ && npm ci\nResult: added 358 packages, audited 359 packages in 2s\n\nEnvironment:\n- node: v20.19.2\n- npm: 9.2.0\n- node_modules entries: 284\n- Top-level devDeps resolved: @axe-core/playwright@4.11.2, @playwright/test@1.59.1, http-server@14.1.1, jest@29.7.0, jest-environment-jsdom@29.7.0\n\nNon-blocking warnings (informational only, install completed):\n- 5 npm WARN deprecation notices (inflight@1.0.6, whatwg-encoding@2.0.0, abab@2.0.6, glob@7.2.3, domexception@4.0.0) — transitive deps of jest/playwright\n- 6 vulnerabilities (2 low, 2 moderate, 2 high) reported by npm audit — not addressed per task scope (discovery only)\n- NODE_TLS_REJECT_UNAUTHORIZED=0 env warning (host environment, unrelated to install)\n\nVerification:\n- package.json and package-lock.json remain UNMODIFIED (git status clean post-install)\n- No source changes made (scope is discovery only)\n- jest NOT run (downstream child)\n\nInstall is healthy and non-blocking — child 2 (jest) can proceed.","created_at":"2026-07-04T11:15:05.752269421Z"}]} -{"id":"bf-nzev","title":"Add phase sanitization property test","description":"Plan target #1 (docs/plan/plan.md, Testing Strategy). Deterministic table-driven property test for signal.PhaseSanitize (func signature: payload []int8, rssiDBm int8, nSub int, returns ProcessedCSI). Property: output is always finite (no NaN/Inf in Amplitude, ResidualPhase, STOSlope, CFOIntercept) for edge I/Q pairs (all-zero, max int8 127, min int8 -128, alternating sign, collinear); invalid inputs (short payload, nSub=0, nSub>payload) return clean errors and never panic. Per the plan table this is a deterministic property test, NOT a native fuzz target. Existing state: file phase_property_test.go already exists in the signal package (untracked) and currently passes its tests (TestPhaseSanitizeProperty, TestPhaseSanitizeInvalidInputs, TestPhaseSanitizeRegressionDenomZero). Do not redo from scratch: verify it covers all enumerated edge cases, harden any gaps, run green, commit. AC: signal package tests green and fast under plain go test ./...; no native fuzz loop in default mode; file committed.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T09:18:43.536478143Z","updated_at":"2026-07-04T09:29:04.576848932Z","closed_at":"2026-07-04T09:29:04.576848932Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"comments":[{"id":65,"issue_id":"bf-nzev","author":"cli","text":"Completed: hardened phase sanitization property test in mothership/internal/signal/phase_property_test.go.\n\nChanges:\n- Added the 'collinear I/Q' edge case to TestPhaseSanitizeProperty (samples on a single line through the origin, straddling the +-pi unwrap boundary) - the last enumerated edge input from the plan's testing strategy. All enumerated edge I/Q pairs now covered: all-zero, max int8 127, min int8 -128, alternating sign, collinear.\n- Tightened the property loop so an unexpected error on a valid edge case FAILS the test instead of being silently logged and passed, so a regression breaking the finite-output property can no longer hide behind the error branch.\n\nVerification:\n- signal package: go vet clean, go test green (property subtests ~0.004s).\n- Full mothership module: go vet ./... and go test ./... both exit 0.\n- No native fuzz target in the file; deterministic table-driven property test only, per the plan table.\n- Invalid-input coverage (short payload, nSub=0, nSub>payload) already present in TestPhaseSanitizeInvalidInputs; unchanged.\n\nCommitted as 42cd793 and pushed to origin/main.","created_at":"2026-07-04T09:28:53.533761981Z"}]} -{"id":"bf-oe1","title":"Implement firmware/test/ test_register with capacity guard","description":"Sub-split child of bf-lfz (umbrella). Depends on child 2 bf-uvv (registry storage). bf-lfz failed 3 times in a row; this chain decomposes its registry work atomically. This is child 3 of 3 (the final child); closing it unblocks the parent umbrella bf-lfz.\n\nScope (child 3 of 3, test_register) -- implement in firmware/test/test_runner.c:\n- void test_register(const char *name, test_fn fn) matching the prototype in test_runner.h (bf-1xs).\n- Append the entry at index g_test_count in construction order (the order the GCC constructors fire) and increment the count, so the list is fully populated before main() runs.\n- Capacity guard: if g_test_count >= MAX_TESTS, print a message to stderr naming the skipped test and MAX_TESTS, then return WITHOUT writing past the end (never overflow the array).\n- A comment noting that main() and test_record_failure() arrive in the sibling beads bf-3id and bf-bq9 and are intentionally absent here.\n\nDo NOT implement test_record_failure() or main() (those are bf-3id and bf-bq9, not this bead).\n\nAcceptance criteria:\n- test_register() is defined, matches the header prototype, appends entries in construction order, and does not overflow capacity 128 (logs to stderr and skips instead).\n- Compiles cleanly to an object with zero warnings: gcc -std=c11 -Wall -Wextra -c firmware/test/test_runner.c (object only; NO link step; NO main()).\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T12:08:38.275138548Z","updated_at":"2026-07-03T13:16:27.116081901Z","closed_at":"2026-07-03T13:16:27.116081901Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-oe1","depends_on_id":"bf-uvv","type":"blocks","created_at":"2026-07-03T12:09:53.311568588Z","created_by":"cli","thread_id":""}],"comments":[{"id":10,"issue_id":"bf-oe1","author":"cli","text":"Implemented test_register() in firmware/test/test_runner.c (child 3 of 3 of the bf-lfz sub-split). Appends each entry at index g_test_count in GCC-constructor order and increments the count so the registry is fully populated before main(). Capacity guard: on g_test_count >= MAX_TESTS (128) it logs to stderr naming the skipped test and the cap and returns without writing past the end (never overflows). Dropped the now-unneeded __attribute__((unused)) from g_tests/g_test_count since test_register() references them. test_record_failure() and main() intentionally absent (siblings bf-3id / bf-bq9). Verified: gcc -std=c11 -Wall -Wextra -Werror -c produces a clean object with zero warnings; test_register present as a defined T symbol, test_record_failure correctly undefined. Only firmware/test/test_runner.c touched (no firmware/main or CMakeLists changes). Committed ef1360e, pushed to main.","created_at":"2026-07-03T13:15:17.543101930Z"}]} -{"id":"bf-r3v","title":"Add firmware/test/ Makefile with documented single compile+run command","description":"Child 3 of the bf-56v harness split. Depends on child 2 (bf-2i4: test_runner.c with main()). Provide the single-command build+run entrypoint.\n\nScope: create firmware/test/Makefile such that ONE command both compiles and runs the whole suite and propagates the binary's exit code:\n- A 'test' goal = 'build run'. 'build' compiles every firmware/test/test_*.c (glob) PLUS test_runner.c with gcc (-std=c11 -Wall -Wextra -O2 -g) into a binary (e.g. build/spaxel_host_tests). 'run' executes it.\n- Because test_runner.c is matched by the test_*.c glob, filter it out of the test set and add it back exactly once — otherwise main()/registry symbols are defined twice and the link fails.\n- Link the .o files directly (not an archive) so every test_*.c's constructor is retained and runs at load (avoids needing --whole-archive).\n- make must propagate the test binary's non-zero exit (the default 'run' recipe does this; do not swallow it with a leading @ that ignores $?).\n- Header comment names the single documented command 'make -C firmware/test test' (run from repo root) — this must match the command named in test_runner.h's header comment (child 1).\n\nAcceptance criteria:\n- firmware/test/Makefile exists.\n- 'make -C firmware/test test' (the documented single command) compiles AND runs the suite.\n- make propagates the binary's non-zero exit code (verified in child 4 by a failing assertion).\n- A new firmware/test/test_*.c is compiled/linked with zero Makefile edits (wildcard + self-registering constructor).\n- test_runner.c included exactly once (no duplicate main/registry symbol link errors).\n- gcc build is -Wall -Wextra clean.\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T10:48:52.629519957Z","updated_at":"2026-07-03T10:48:52.629519957Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-r3v","depends_on_id":"bf-2i4","type":"blocks","created_at":"2026-07-03T10:50:27.908366274Z","created_by":"cli","thread_id":""}]} -{"id":"bf-tof1","title":"Verify the all-passing suite runs every test to completion and the scope boundary holds","description":"Split-child 4 of 4 (final) for parent bf-22vg. The behavior and scope verification that closes the parent. Depends on children 1-3. This is a verify-and-confirm step, not new logic.\n\nScope: run the suite and confirm scope. No code changes unless a verification step reveals a defect, in which case fix only within firmware/test/test_runner.c.\n\nDo:\n- Run make -C firmware/test test from the repo root with the all-passing suite.\n- Confirm every test RUN: line prints (per-test isolation observable) and the process completes (does not hang or abort) — behavior unchanged for passing tests.\n- Confirm the diff is confined to firmware/test/test_runner.c: no changes to firmware/main/* or firmware/CMakeLists.txt.\n- Confirm the loop body matches the parent contract: RUN line before setjmp, body inside if (setjmp(g_test_jmp) == 0), no PASS/FAIL label or pass/fail tally or summary line or non-zero exit logic present in this lineage.\n\nAcceptance criteria:\n- make -C firmware/test test runs every test and the process completes with no regression for the all-passing suite.\n- git diff --stat shows changes only in firmware/test/test_runner.c (no firmware/main/*, no firmware/CMakeLists.txt).\n- The loop body matches the parent contract exactly (guard present, RUN precedes it, no executable else/outcome plumbing).\n- The whole parent (bf-22vg) acceptance criteria are now satisfied and the parent can be closed.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T00:11:06.785654159Z","updated_at":"2026-07-04T06:38:16.287829911Z","closed_at":"2026-07-04T06:38:16.287829911Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-tof1","depends_on_id":"bf-2k3o","type":"blocks","created_at":"2026-07-04T00:11:21.560082062Z","created_by":"cli","thread_id":""},{"issue_id":"bf-tof1","depends_on_id":"bf-50yh","type":"blocks","created_at":"2026-07-04T01:52:39.608119449Z","created_by":"cli","thread_id":""}],"comments":[{"id":57,"issue_id":"bf-tof1","author":"cli","text":"Verify-and-confirm (child 4/4 of bf-22vg, final). All acceptance confirmed; no code changes (verify-only — no defect revealed).\n\nRESULTS:\n- make -C firmware/test test exits 0; process completes (no hang/abort). All 29 RUN: lines print in strcmp-sorted order, no FAIL/stderr interleaving. Behavior unchanged for the all-passing suite.\n- Per-test isolation observable: bidirectional set diff of registered TEST() names (29) vs emitted RUN: lines (29) is IDENTICAL — none skipped, none duplicated. Per-file: test_sanity 1, test_csi_frame 7, test_nvs_migration 9, test_serial_prov 12.\n- Scope boundary: NO commits in this lineage touch firmware/main/* (zero) or firmware/CMakeLists.txt (last touched f76ab62, pre-lineage). All four lineage commits (bf-1fd4, bf-27ud, bf-2k3o, bf-50yh) touch only firmware/test/test_runner.c.\n- Loop body matches parent contract exactly: RUN printf precedes setjmp; body inside if (setjmp(g_test_jmp) == 0) { g_tests[i].fn(); }; no PASS/FAIL label, no pass/fail tally, no summary line; main() never reads g_failure_count and returns 0 unconditionally (no non-zero exit logic, no else branch).\n\nParent bf-22vg acceptance satisfied — recovery-gated loop drives the whole all-passing suite to completion with the scope boundary intact; parent can be closed.","created_at":"2026-07-04T06:38:00.415273041Z"}]} -{"id":"bf-uvv","title":"Add firmware/test/ test registry storage (static array + count)","description":"Sub-split child of bf-lfz (umbrella). Depends on child 1 bf-6aj (the skeleton with includes). bf-lfz failed 3 times in a row; this chain decomposes its registry work atomically. This is child 2 of 3 (registry storage).\n\nScope (child 2 of 3, registry storage) -- add to firmware/test/test_runner.c:\n- #define MAX_TESTS 128\n- The static fixed-capacity array: static test_entry_t g_tests[MAX_TESTS]; (test_entry_t is the struct { const char *name; test_fn fn; } defined in test_runner.h / bf-1xs)\n- The static count: static int g_test_count = 0;\n- Brief comments: the static array is fine because the GCC constructors emitted by TEST() populate it before main() runs and the harness stays dependency-free; 128 is far more than the handful of pure-logic tests this harness is meant for.\n\nDo NOT implement test_register() yet (child 3).\n\nAcceptance criteria:\n- firmware/test/test_runner.c has MAX_TESTS, g_tests[], and g_test_count with the comments.\n- Still compiles cleanly to an object with zero warnings: gcc -std=c11 -Wall -Wextra -c firmware/test/test_runner.c (static file-scope symbols do not warn when unused; object only; NO link step; NO main()).\n- No changes to firmware/main/* or firmware/CMakeLists.txt.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T12:08:07.839994558Z","updated_at":"2026-07-03T13:08:42.954900562Z","closed_at":"2026-07-03T13:08:42.954900562Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-uvv","depends_on_id":"bf-6aj","type":"blocks","created_at":"2026-07-03T12:09:53.299741863Z","created_by":"cli","thread_id":""}],"comments":[{"id":9,"issue_id":"bf-uvv","author":"cli","text":"Child 2 of bf-lfz sub-split (registry storage) — already implemented in commit 18bce75, which was sitting unpushed locally (bead had failure-count:2 from a prior run that committed but failed to push+close). This run verified acceptance criteria and pushed it. firmware/test/test_runner.c now has #define MAX_TESTS 128, static test_entry_t g_tests[MAX_TESTS], static int g_test_count = 0 with comments. Statics carry __attribute__((unused)) because gcc 14 warns on unused file-scope statics under -Wall -Wextra. Verified: gcc -std=c11 -Wall -Wextra -c -> exit 0, zero warnings. No firmware/main or CMakeLists.txt changes. Pushed 191adbe..18bce75 to origin/main. test_register() deliberately NOT implemented (child 3).","created_at":"2026-07-03T13:07:52.105073844Z"}]} -{"id":"bf-vmik","title":"Verify end-to-end that a failing assertion does not block later tests","description":"Split-child 3 of 3 (final child) for parent bf-53ut. Builds on children 1 and 2 (setjmp guard + observable failure line). This child is the acceptance PROOF for the parent bead: demonstrate, by running the real harness, that a failing assertion in one test does not prevent later tests from running and does not abort the process.\n\nScope:\n- Temporarily add an intentionally-failing TEST() (one that calls an ASSERT_* macro that fails, e.g. ASSERT_EQ(1, 2)) immediately followed by a passing TEST(), in a scratch file under firmware/test/ (or inline in an existing scratch test). This probe is verification-only and MUST NOT be committed.\n- Run: make -C firmware/test test\n- Confirm all three: (a) BOTH tests' per-test observable lines print — the failing test's line AND the passing test's line, in order; (b) the process COMPLETES (returns control to the loop / exits) rather than aborting or crashing; (c) tests after the failing one are not skipped.\n- Remove the temporary failing/passing probe so the tree is left clean. When closing, capture the observed output (the two lines + confirmation the process completed) as a comment on this bead.\n\nAcceptance criteria:\n- With an intentionally-failing test placed before a passing test, make -C firmware/test test prints both tests' per-test lines and completes (exits) rather than aborting.\n- The temporary verification probe is removed; the working tree is left clean (no stray test files committed).\n- The end-to-end result is captured as a comment on this bead (the two observed lines + confirmation that the process completed).","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T16:02:16.789014583Z","updated_at":"2026-07-03T23:01:09.508150703Z","closed_at":"2026-07-03T23:01:09.508150703Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-vmik","depends_on_id":"bf-38e9","type":"blocks","created_at":"2026-07-03T16:02:31.051510109Z","created_by":"cli","thread_id":""}],"comments":[{"id":28,"issue_id":"bf-vmik","author":"cli","text":"bf-vmik (child 3/3 of bf-53ut): END-TO-END ACCEPTANCE PROOF that a failing assertion does NOT block later tests and does NOT abort the process.\n\nMethod: temporarily added test_scratch_probe.c with two adjacent TEST()s (zz_ prefix so they sort last) — zz_probe_failing (ASSERT_EQ(1,2)) immediately before zz_probe_passing (ASSERT_EQ(2+2,4)) — ran make -C firmware/test test, then removed the probe.\n\nIMPORTANT: had to run make clean first. A stale gitignored build/spaxel_host_tests binary from a prior interrupted attempt was newer than every source file, so make was re-running a probe baked into the OLD binary rather than rebuilding. A clean rebuild was required for a trustworthy result.\n\nObserved stdout (in order):\n VMIK-PROBE: entered FAILING test body (about to assert 1==2)\n RUN: zz_probe_failing (assertion failed)\n VMIK-PROBE: entered PASSING test body (later test, runs anyway)\n RUN: zz_probe_passing\n 30 passed, 1 failed of 31\nObserved stderr (the failing test diagnostic):\n test_scratch_probe.c:25: ASSERT_EQ(1, 2) failed: got 1, want 2\n\nAll three criteria confirmed:\n(a) BOTH per-test observable lines printed, in order — failing line immediately followed by passing line (adjacent on stdout).\n(b) The process COMPLETED, not crashed. Running the binary directly (bypassing make) returned exit code 1, the documented clean failure-exit (non-zero because a test failed). It was NOT 134 (SIGABRT) or 139 (SIGSEGV): the setjmp/longjmp guard caught the failed assertion and main() returned normally — no abort.\n(c) The test after the failing one was NOT skipped: its body printf genuinely printed (entered PASSING test body), and the UNREACHABLE line after the failing ASSERT_EQ did NOT print (longjmp aborted only the failing body). 30 of 31 tests passed; exactly 1 failed.\n\nProbe removed; working tree left clean — firmware/test git status empty; baseline rebuilt to 29 passed, 0 failed of 29, exit 0. No code changes in this bead (children 1 and 2 — setjmp guard + observable failure line — were already committed); this bead is the acceptance proof.","created_at":"2026-07-03T22:59:56.030805309Z"}]} -{"id":"bf-yacm","title":"Verify per-test isolation end-to-end: a failing test does not block later tests","description":"Split-child 3 of 3 (the LAST child) for parent bf-53ut. Depends on child 2 (the volatile/clobber-safety verification). This child is the parent's acceptance criterion: prove, end-to-end, that a failing assertion in one test does NOT prevent later tests from running — the whole point of the per-test setjmp/longjmp recovery loop child 1 introduced.\n\nScope (firmware/test/, temporary verification only — must be reverted):\n- Temporarily add two throwaway tests: an intentionally-failing TEST() (an ASSERT_* that is guaranteed to trip, e.g. ASSERT_EQ on unequal values) immediately followed by a passing TEST(). Register them so they run in the suite. The point is a failing test that longjmps, followed by a test that MUST still execute.\n- Run `make -C firmware/test test` and observe:\n (a) BOTH tests' per-test RUN: lines print (the failing test's AND the passing test's that follows it),\n (b) the process COMPLETES rather than aborting/crashing,\n (c) the later (passing) test actually ran — its RUN line appears after the failing one, proving the failure did not block it.\n- This intentionally-failing test exercises test_record_failure() -> longjmp(g_test_jmp, 1) -> return to main()'s setjmp with a non-zero value -> loop advances. Confirming the later test still runs is direct proof the recovery loop works.\n- After confirming, REMOVE the two temporary tests so the suite returns to its prior (all-passing) state. Do not leave the intentionally-failing test in the tree.\n- Note: do NOT add per-test PASS/FAIL outcome labels as part of this child — verifying isolation via the RUN lines is sufficient, and outcome labels are bf-1na child 3's scope.\n\nAcceptance criteria:\n- Demonstrated (paste the observed make output into a comment on this bead) that a failing TEST() followed by a passing TEST() results in both RUN: lines printing and the process completing without aborting, with the later test's line appearing after the failing one.\n- The temporary failing/passing tests are removed before close; the working tree is left clean (suite passes again).\n- No changes to firmware/main/* or firmware/CMakeLists.txt; the only file touched during verification is a throwaway test_*.c that is deleted/reverted at the end.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T23:38:10.420841941Z","updated_at":"2026-07-03T23:38:10.420841941Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-yacm","depends_on_id":"bf-31rd","type":"blocks","created_at":"2026-07-03T23:39:13.406149047Z","created_by":"cli","thread_id":""}]} -{"id":"bf-ycik","title":"Quote the if (setjmp(g_test_jmp) == 0) guard with body call and closing brace","description":"Read-only static verification of firmware/test/test_runner.c only (NO code changes). Child 3 of 4 splitting bf-3fx4.\n\nScope: Inside main()'s per-test loop, quote the complete 'if (setjmp(g_test_jmp) == 0)' guard block: the if-line, the test body call 'g_tests[i].fn();' that is its body, and the closing brace. The body must run only on the direct (zero) setjmp return path; a non-zero return is a longjmp from a failed assertion and must NOT call the body (it falls through to the next iteration with no else body).\n\nAcceptance criteria: A comment on this bead quoting the exact three lines from firmware/test/test_runner.c:\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\nCloses independently once quoted.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-04T04:09:01.810383804Z","updated_at":"2026-07-04T04:46:25.915855433Z","closed_at":"2026-07-04T04:46:25.915855433Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-ycik","depends_on_id":"bf-2ftq","type":"blocks","created_at":"2026-07-04T04:09:17.103300559Z","created_by":"cli","thread_id":""}],"comments":[{"id":45,"issue_id":"bf-ycik","author":"cli","text":"Read-only static verification of firmware/test/test_runner.c (no code changes). Confirmed the complete 'if (setjmp(g_test_jmp) == 0)' guard block exists exactly as specified at lines 256-258, inside main()'s per-test 'for (volatile int i = 0; ...)' loop (line 246), immediately after the RUN-line printf (line 255). Exact three lines quoted verbatim:\n\n if (setjmp(g_test_jmp) == 0) {\n g_tests[i].fn();\n }\n\nSemantics verified: the body call g_tests[i].fn() runs ONLY on the direct (zero) setjmp return path. A non-zero return is a longjmp from a failed assertion (test_record_failure at line 172 does 'longjmp(g_test_jmp, 1)') and must NOT call the body -- it falls straight through to the next loop iteration with no else body, so the loop's i++ still advances and a failure in test N never blocks N+1..end. The volatile qualifier on i (C11 7.13.2.1) keeps the setjmp/longjmp-clobbered loop index determinate. Matches acceptance criteria exactly.","created_at":"2026-07-04T04:45:29.102488727Z"}]} +{"id":"bf-13gwk","title":"Spatial quick actions (Component 32)","description":"## Goal\nRight-click (desktop) or long-press (mobile) anywhere in 3D view to get context-sensitive actions based on what's under cursor.\n\n## Scope\nActions per target:\n- On blob: 'Who is this?' (BLE assignment), 'Why is this here?' (explainability), 'Follow' (camera tracks), 'Create automation here', 'Mark incorrect' (thumbs-down), 'Track history'\n- On node: 'Diagnostics' (CSI plot), 'Blink LED', 'Reposition', 'Update firmware', 'Show links', 'Disable/Enable'\n- On empty floor space: 'What happened here?' (filter timeline), 'Add trigger zone', 'Add virtual node', 'Coverage quality'\n- On zone label: 'Zone history', 'Edit zone', 'Create automation', 'Crowd flow'\n- On portal: 'Crossing log', 'Edit portal', 'Reverse direction'\n- On trigger volume: 'Edit trigger', 'Test', 'View log', 'Disable/Enable'\n\n## Implementation\nThree.js Raycaster determines what's under cursor\nSingle context menu component renders appropriate options\nEach action dispatches to existing dashboard functions (no new backend endpoints needed)\n\n## Acceptance\n- Right-click/long-press on blob shows blob-specific actions\n- Right-click/long-press on node shows node-specific actions\n- Right-click/long-press on empty space shows space-specific actions\n- Actions execute correctly\n- Works on both desktop (right-click) and mobile (long-press)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:11.548191949Z","updated_at":"2026-05-06T08:32:57.207248413Z","closed_at":"2026-05-06T08:32:57.207248413Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-13s0","title":"Locate blob data structure definition","description":"## Objective\nFind where the blob/person data structure is defined in the 3D dashboard frontend.\n\n## Scope\n- Search the dashboard codebase for blob/person type definitions\n- Identify the file(s) where the blob data structure is defined\n- Document the current structure and where it's used\n\n## Acceptance Criteria\n- [ ] Located the primary blob data structure definition\n- [ ] Identified all files that create or use blob data\n- [ ] Documented the current blob structure fields\n- [ ] Know which TypeScript interface/type file to modify\n\n## Notes\nResearch-only task. No code changes yet.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T04:41:16.980053426Z","updated_at":"2026-07-06T05:01:27.282036255Z","closed_at":"2026-07-06T05:01:27.282036255Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"]} +{"id":"bf-15hy","title":"Build spaxel:0.1.354, deploy to ardenone-cluster, re-flash ESP32","description":"After bf-49fp (Dockerfile cache-bust fix) is merged:\n\n1. Bump VERSION file to 0.1.354\n2. Push to git to trigger spaxel-build CI on iad-ci\n3. Monitor workflow until ronaldraygun/spaxel:0.1.354 is pushed\n4. In ~/declarative-config, update the spaxel Deployment image tag to 0.1.354\n5. Push declarative-config, ArgoCD syncs (Recreate strategy, ~30s downtime expected)\n6. Verify mothership is healthy at spaxel.ardenone.com/healthz\n\nThen re-flash the ESP32:\n- Navigate to spaxel.ardenone.com provision page\n- Connect ESP32-S3 via USB\n- Run flash — mothership will now serve the 4MB firmware (no more crash loop)\n\nSuccess criteria: ESP32 boots, starts captive portal or connects to WiFi, no reboot loop.\nCI submit cmd: kubectl --kubeconfig=~/.kube/iad-ci.kubeconfig create -f - (spaxel-build workflow)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":0,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-06-04T03:13:21.294634213Z","updated_at":"2026-07-02T18:58:14.160082365Z","closed_at":"2026-07-02T18:58:14.160082365Z","close_reason":"Superseded","source_repo":".","compaction_level":0,"comments":[{"id":14,"issue_id":"bf-15hy","author":"cli","text":"Cannot complete as written — bead is stale. (1) VERSION REGRESSION: targets 0.1.354 but repo is at 0.1.357 and deployed mothership at spaxel.ardenone.com/healthz reports 0.1.358 (healthy, ~7.9d uptime); bumping to 0.1.354 would be a downgrade. (2) PREREQUISITE SATISFIED: bf-49fp is CLOSED, merged as commit 301a588 (cache-bust / firmware crash-loop fix) and already deployed in 0.1.358. (3) PHYSICAL STEP UNREACHABLE: ESP32 re-flash needs a human to connect the ESP32-S3 via USB and flash via the browser provision page (Web Serial). No serial device attached (/dev/ttyUSB*, /dev/ttyACM* absent, no USB-serial in lsusb), no firmware .bin in repo, esptool not installed. Deploy intent (get crash-loop fix built and live) is already done by 0.1.357/0.1.358; only the physical reflash remains, which needs a human + connected hardware. Leaving OPEN per retry guidance.","created_at":"2026-07-02T18:53:29.861919590Z"},{"id":15,"issue_id":"bf-15hy","author":"cli","text":"RETRY VERIFICATION (2026-07-02T18:56:56Z): Task is stale and structurally impossible to execute as written. (1) VERSION is 0.1.357 locally; task wants 0.1.354 = a downgrade. (2) Deployed mothership spaxel.ardenone.com/healthz = {status:ok, version:0.1.358, uptime_s:683554 (~7.9d), db:ok, shedding_level:0} -- healthy, NOT crash-looping. (3) Prereq bf-49fp (Kaniko cache-bust fix) is CLOSED, merged as 301a588, already shipped in 0.1.358. The crash-loop fix this chain exists to deliver is already built, deployed, and stable. (4) Only remaining step = physical ESP32-S3 USB re-flash via browser Web Serial, which is unreachable from a shell: no /dev/ttyUSB* or /dev/ttyACM*, no esptool installed, no release spaxel-*.bin in repo. Closing as SUPERSEDED; no code/cluster changes made. Sole outstanding item = manual physical reflash against the already-deployed 0.1.358.","created_at":"2026-07-02T18:56:56.492556342Z"}]} +{"id":"bf-16c1","title":"Strengthen TestDetectionEvents to assert at least one detection event","description":"Split from bf-5jeo (verification capstone). Depends on bf-2330 (assertion primitives). Child 2 of 4.\n\nCONTEXT: mothership/tests/e2e/e2e_test.go:679 TestDetectionEvents currently only checks events != nil. The comment says \"The endpoint must return a valid (possibly empty) events list\" and t.Logf(\"Events API functional: found %d detection events\", ...) — it explicitly ducks the count. This is one of the dodges called out in bf-5jeo.\n\nSCOPE:\n- Replace the nil-only check with a hard assertion that len(events.Events) >= 1, using the helper added in bf-2330.\n- Provide an actionable failure message (include count observed + that the fusion pipeline produced no blobs).\n- Keep the existing 4-node / 2-walker / 15s RunSimulator config and the timeout handling.\n\nACCEPTANCE CRITERIA:\n- TestDetectionEvents asserts >= 1 detection event (no longer accepts an empty list as pass).\n- Failure message names the likely upstream cause (fusion/tracking produced no blobs).\n- Test compiles (go build ./tests/e2e/...).\n- NOTE: this test is expected to be RED until upstream bf-4q5w (fusion SetNodePosition wiring) lands — that strictness is the entire point of this capstone. Do NOT weaken the assertion to make it green.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T18:51:03.114403566Z","updated_at":"2026-07-06T18:51:03.114403566Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-16c1","depends_on_id":"bf-2330","type":"blocks","created_at":"2026-07-06T18:51:13.287924396Z","created_by":"cli","thread_id":""}]} +{"id":"bf-1837","title":"CI: add idf.py test --target linux firmware gate to spaxel-build WorkflowTemplate","description":"After bf-31bp writes host-based firmware test targets (nvs/csi/serial_prov), wire them into the spaxel-build WorkflowTemplate in declarative-config as a step before the Docker build. firmware/test/ directory does not yet exist and CMakeLists has no host-test targets.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-05-31T15:43:40.781487343Z","updated_at":"2026-07-06T04:07:57.742934181Z","closed_at":"2026-07-06T04:07:57.742934181Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-18yn","title":"Seed realistic node geometry in spaxel-sim default placement","description":"Split from bf-4q5w (parent umbrella). Depends on the runtime-propagation child.\n\nEven with positions wired, the spaxel-sim default virtual-node placement leaves nodes co-located / at origin, so the Fresnel excess path length |P-T|+|P-R|-|T-R| collapses toward 0 everywhere and the accumulation grid has no meaningful peaks, producing no blobs (the core symptom in bf-4q5w).\n\nScope:\n- In the simulator virtual-node creation path (createVirtualNodes and the simulator package, e.g. internal/simulator/registry_bridge.go where PosX/PosY/PosZ are sourced from node.Position), give the default virtual nodes a realistic spread-out geometry across a default zone instead of co-located/origin positions.\n- Ensure these positions flow into both the registry and the fusion engine via the wiring from prior children.\n- Pick a sane default room/zone size consistent with roomWidth/roomDepth used in main.go for the localization system.\n\nAcceptance criteria:\n- Default sim nodes occupy a spread, non-degenerate geometry (not all at origin, not co-located).\n- A sim or host test shows the fusion accumulation grid produces non-zero peaks with the default placement.\n- Existing simulator tests still pass; go build ./... passes.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T19:22:58.964617777Z","updated_at":"2026-07-06T18:21:40.065868881Z","closed_at":"2026-07-06T18:21:40.065868881Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-18yn","depends_on_id":"bf-3p6g","type":"blocks","created_at":"2026-07-03T19:23:17.356813659Z","created_by":"cli","thread_id":""},{"issue_id":"bf-18yn","depends_on_id":"bf-1kid","type":"blocks","created_at":"2026-07-03T22:10:08.997748955Z","created_by":"cli","thread_id":""},{"issue_id":"bf-18yn","depends_on_id":"bf-4b1c","type":"blocks","created_at":"2026-07-06T04:22:45.618994878Z","created_by":"cli","thread_id":""}],"comments":[{"id":60,"issue_id":"bf-18yn","author":"cli","text":"Closed as complete — verified the full default-placement chain.\n\nbf-18yn is the umbrella; its scope was implemented by the closed children bf-xrej (impl) and bf-1kid (acceptance test), both already on origin/main.\n\nVerification (all green):\n- go build ./... clean across all 3 modules (mothership, cmd/sim, test/acceptance)\n- go test ./... — 50 packages pass\n- go vet ./... clean across all 3 modules\n- Acceptance test TestEngine_DefaultPlacementProducesPeaks passes for default placements of 2/3/4/6 nodes (uses ONLY simulator.DefaultNodePositions, no manual positioning)\n- Counter-example TestEngine_CoLocatedOriginYieldsNoPeaks confirms the assertion is non-trivial (co-located (0,0,1) nodes correctly yield 0 blobs)\n\nScope coverage:\n- Spread geometry: simulator.DefaultNodePositions (node.go) — single node at center, 2 on diagonal, 3+ on expanding grid with mixed Z. registry_bridge.effectivePositions reassigns any default-(0,0,1) origin node a distinct spread slot at sync time (SyncToRegistry/SyncOneNode/ToRegistryRecords).\n- Registry flow: effectivePositions -> SyncToRegistry writes spread positions; bf-24xp persists hello-announced position.\n- Fusion flow: main.go startup seeding loop (main.go:1054-1062) reads registry nodes and calls fusionEngine.SetNodePosition per node; bf-1tsm startup assertion guards distinct/non-(0,0,1) invariant.\n- Room size: DefaultSpace is 6x5x2.5 (plan.md canonical sim example); DefaultNodePositions is parameterized by space.Bounds() so it works for any room incl. main.go's default 10x10x2.5.\n\nNo new code committed — work was already complete and pushed by the children. Nothing left to implement.","created_at":"2026-07-03T23:39:12.940400067Z"},{"id":76,"issue_id":"bf-18yn","author":"cli","text":"Closed out: spread node geometry for spaxel-sim default placement is implemented, committed, and verified against all three acceptance criteria.\n\nImplementation (already committed by prior children of bf-4q5w, verified here):\n- internal/simulator/node.go DefaultNodePositions(space,count) (bf-3fr6): distinct, in-bounds, room-spanning points — center for 1, opposite corners for 2, an expanding ceil(sqrt(n)) grid (>=2 cols & rows, alternating low/high Z band) for >=3. No two nodes co-locate and the geometry is never all-at-origin.\n- internal/simulator/registry_bridge.go effectivePositions (bf-xrej): default-origin (0,0,1) virtual nodes are reassigned deterministic spread slots (sorted-ID order, sized to full node count, collision-free) at sync time so the registry and the fusion engine never see co-located nodes. SyncToRegistry/SyncOneNode/ToRegistryRecords all resolve through it; explicitly-placed nodes are preserved.\n- cmd/sim generateNodePositions: the spaxel-sim CLI binary likewise seeds corners/grid, never origin.\n- Default space 6x5x2.5 (sane residential default; bridge prefers the store's configured room, else DefaultSpace).\n\nAcceptance criteria verified:\n1. Spread non-degenerate geometry — DefaultNodePositions spans both X and Y for count>=2, all positions distinct. node_positions_test.go + registry_bridge_test.go pass.\n2. Host test shows non-zero fusion peaks with default placement — TestEngine_DefaultPlacementProducesPeaks (the bf-18yn acceptance test, nodes=2/3/4/6) passes; TestEngine_GeometryPlacementDrivesFusionPeaks (bf-4b1c differential) shows spread->gridMax=1.0+blobs vs co-located(0,0,1)->gridMax=0.0+no blobs.\n3. go build ./... passes across all three modules (mothership, cmd/sim, test/acceptance); go vet ./... clean; full mothership go test ./... green. Positions flow simulator->registry->fusion via the runtime wiring from bf-u7ds/bf-95tx/bf-3y9r.","created_at":"2026-07-06T18:10:44.606168746Z"},{"id":77,"issue_id":"bf-18yn","author":"cli","text":"Closed bf-18yn: default virtual nodes now receive spread, non-degenerate geometry instead of co-located/origin positions.\n\nThree creation paths covered:\n- cmd/sim createVirtualNodes -> generateNodePositions spreads nodes across corners/grid of the --space room (default 6x5x2.5).\n- simulator.DefaultNodePositions (node.go): room-spanning grid placement, distinct cells, mixed-height Z.\n- registry_bridge.effectivePositions: nodes still at the DB-default origin (0,0,1) are reassigned deterministic spread slots (DefaultNodePositions sized to the full node set) so the registry and downstream fusion engine never see co-located virtual nodes.\n- api AddNode handler (commit 669d379): assigns spread positions for origin nodes, preserving explicitly-set positions.\n\nPositions flow into the registry (SyncToRegistry/SyncOneNode) and into the fusion engine via the prior runtime-propagation child (bf-u7ds) plus main.go's startup SetNodePosition seeding loop.\n\nAcceptance verified:\n- TestEngine_DefaultPlacementProducesPeaks (fusion_test.go) is the explicit bf-18yn acceptance test: seeds nodes via DEFAULT placement only, drives a walker, asserts non-zero peaks (blobs>0 or gridMax>threshold) for node counts 2/3/4/6. Counter-example TestEngine_CoLocatedOriginYieldsNoPeaks confirms co-located nodes yield no peaks, so the assertion is non-trivial.\n- go build ./... passes for mothership, cmd/sim, test/acceptance.\n- go vet ./... clean; go test ./... green across all modules (incl. simulator, fusion, api, tests/e2e).","created_at":"2026-07-06T18:21:30.760397032Z"}]} +{"id":"bf-195o","title":"Explore and design default node geometry placement","description":"Child of bf-18yn (split).\n\nResearch the current virtual node creation path and design the approach for realistic node geometry.\n\nScope:\n- Locate createVirtualNodes and simulator package code where PosX/PosY/PosZ are sourced from node.Position\n- Identify the current default placement behavior (likely co-located/origin)\n- Research existing room/zone size constants (roomWidth, roomDepth in main.go)\n- Design a sane geometry layout (e.g., grid placement, random spread with minimum distance, etc.)\n- Document the chosen approach and constants\n\nAcceptance criteria:\n- Document the current virtual node creation flow and where positions are set\n- Identify appropriate room/zone size constants to use\n- Design approach documented (e.g., 'place nodes in a 10x10m grid with 0.5m spacing')\n- No code changes yet — research and design only","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T04:22:13.061882760Z","updated_at":"2026-07-06T04:44:35.213410347Z","closed_at":"2026-07-06T04:44:35.213410347Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"]} +{"id":"bf-1bmg","title":"Inventory JS and TS dashboard blob creation sites","description":"Scope: all blob creation on the JS/TS dashboard side.\n\nSites to document:\n- dashboard/js/state.js (~:290) — appState.blobs[id] = { id: id } then Object.assign of server payload\n- dashboard/js/websocket.js (~:167) — _blobStates.set(b.id, {x,z,vx,vz,ts}) dead-reckoning cache\n- dashboard/types/spaxel.d.ts — the canonical Blob INTERFACE declaration (personName, personId, assignedColor, identityResolved, etc.) — the single JS identity-field declaration point\n- The 4 unrelated browser new Blob() file-download sites (fleet.js:457, fleet-page.js:1034/1369, fleet.js:1997) — catalogued ONLY to be explicitly skipped (CSV/JSON downloads, not domain blobs)\n- JS test fixtures (ambient.test.js, quick-actions.test.js, replay.test.js) — list for completeness only\n\nReference: notes/bf-3ldj-findings.md sections 2.3-2.5.\n\nAcceptance Criteria:\n- [ ] Every JS blob-creation site listed with file:line + pattern\n- [ ] The spaxel.d.ts Blob interface documented as the canonical JS identity-field declaration\n- [ ] The 4 new Blob() download sites explicitly flagged as OUT OF SCOPE / unrelated browser API\n- [ ] All line numbers verified against current HEAD\n- [ ] Findings written to notes/bf--js-ts.md","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T15:03:46.854905168Z","updated_at":"2026-07-06T19:48:50.158657037Z","closed_at":"2026-07-06T19:48:50.158657037Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-1bmg","depends_on_id":"bf-4wwt","type":"blocks","created_at":"2026-07-06T15:04:21.398572664Z","created_by":"cli","thread_id":""}],"comments":[{"id":81,"issue_id":"bf-1bmg","author":"cli","text":"Inventoried every JS/TS dashboard blob-creation site, re-verified against HEAD 68bd308. Findings in notes/bf-1bmg-js-ts.md. Production domain-blob sites: state.js:290 ({id:id} + Object.assign of server payload — canonical creation) and websocket.js:167 (dead-reckoning _blobStates cache). Documented spaxel.d.ts:10-91 Blob interface as the single canonical JS identity-field declaration (personName/personId/assignedColor/identityResolved + deprecated personLabel/personColor) with Go<->JS name map. Flagged the 4 new Blob() file-download sites as OUT OF SCOPE browser API (static/js/fleet.js:457 CSV, fleet-page.js:1034 JSON & :1369 CSV, js/fleet.js:1997 JSON). Listed 12 test fixtures for completeness. Corrected two drifts vs bf-3ldj: the :457 CSV site is in static/js/fleet.js (different file from js/fleet.js), and ambient.test.js literal openings are 124/273/636/653/688/702. Committed 2b44ff6, pushed to main.","created_at":"2026-07-06T19:46:43.072917095Z"}]} +{"id":"bf-1h7h","title":"Replace blob markers with humanoid meshes in Three.js","description":"## Objective\nReplace generic 'Blob #2' sphere markers with humanoid figure meshes in the Three.js scene.\n\n## Scope\n- Locate current blob marker rendering code (likely using Three.js SphereGeometry or similar)\n- Design or select a humanoid mesh representation (simple geometric humanoid: cylinder+sphere for body+head, or loaded model)\n- Replace sphere markers with humanoid meshes for identity-resolved blobs\n- Preserve existing scale/position/rotation transforms\n- Ensure humanoid meshes work with existing animation loop\n\n## Dependencies\nDepends on: bf-iner (identity matching must provide personName/assignedColor)\n\n## Acceptance Criteria\n- [ ] Identity-resolved blobs render as humanoid figures, not spheres\n- [ ] Unresolved blobs still render as generic markers (backward compatible)\n- [ ] Humanoid meshes maintain correct position/scale in scene\n- [ ] No console errors from mesh geometry\n- [ ] Scene still renders at 60fps (performance acceptable)\n\n## Notes\nThis adds the humanoid geometry but does NOT yet add per-person colors or name labels.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T04:05:01.330670659Z","updated_at":"2026-07-06T04:05:01.330670659Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-1h7h","depends_on_id":"bf-iner","type":"blocks","created_at":"2026-07-06T04:05:04.554395165Z","created_by":"cli","thread_id":""}]} +{"id":"bf-1ih7k","title":"TX slot collision detection and adaptive re-stagger","description":"Plan (Component 5 / Fleet Manager) specifies collision monitoring: if CSI frames from two TX nodes arrive within 3ms of each other, log a 'possible slot collision' metric. If collision rate > 5% over a 60-second window, re-randomize stagger assignments (shift one node's slot by half a slot width) and push updated config messages. The fleet manager computes stagger slots but has no collision detection, no re-stagger logic, and no collision rate metric. Needs: (1) per-link-pair collision counter in ingestion/signal processing path, (2) collision rate aggregation in fleet manager, (3) adaptive re-stagger trigger.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:13.899248435Z","updated_at":"2026-05-06T12:29:01.414753570Z","closed_at":"2026-05-06T12:29:01.414753570Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-1k3zg","title":"API: GET /api/doctor — pre-flight configuration diagnostic","description":"## Goal\nAdd a GET /api/doctor endpoint that diagnoses common misconfiguration before the user concludes the system is broken. Complements /healthz (runtime state) with pre-flight checks (configuration correctness).\n\n## Endpoint\n\nGET /api/doctor\n- Requires session cookie (same as all /api/* endpoints)\n- Returns 200 with a JSON report regardless of check results (HTTP status reflects reachability, not check results)\n\n## Checks to run\n\n| Check | Pass condition | Fail message |\n|---|---|---|\n| data_dir_writable | /data is writable and has >100 MB free | 'Data directory not writable' or 'Disk space low: Nf MB free' |\n| db_integrity | PRAGMA integrity_check returns 'ok' | 'SQLite integrity check failed' |\n| firmware_dir | /firmware contains at least one *.bin file | 'No firmware binaries found — OTA updates unavailable' |\n| mdns_binding | mDNS service is registered (or SPAXEL_MDNS_ENABLED=false) | 'mDNS not advertising — nodes cannot auto-discover mothership' |\n| mqtt_reachable | If SPAXEL_MQTT_BROKER is set: TCP connect to broker succeeds within 3s | 'MQTT broker unreachable: ' |\n| ntp_reachable | UDP ping to SPAXEL_NTP_SERVER:123 resolves within 3s | 'NTP server unreachable — node clock sync may fail' |\n| install_secret | install_secret row exists in auth table | 'Installation secret missing — re-run container to regenerate' |\n| pin_configured | pin_bcrypt is non-null in auth table | 'Dashboard PIN not configured — run first-time setup' |\n| node_token_consistency | All nodes in registry have non-null node_token | 'N nodes missing auth tokens — re-provision via Web Serial' |\n\n## Response format\n\n{\n 'checks': [\n {'name': 'db_integrity', 'status': 'ok', 'message': null},\n {'name': 'mqtt_reachable', 'status': 'warn', 'message': 'MQTT broker unreachable: mqtt://ha.local:1883'},\n {'name': 'firmware_dir', 'status': 'error', 'message': 'No firmware binaries found'}\n ],\n 'overall': 'warn', // 'ok' | 'warn' | 'error' (worst of all checks)\n 'checked_at': '2024-03-15T07:00:00Z'\n}\n\nStatus levels: 'ok' (pass), 'warn' (degraded but functional), 'error' (action required).\n\n## Dashboard integration\n- Command palette: 'doctor' → calls /api/doctor, shows results inline\n- Guided troubleshooting (Component 36): 'Node offline' flow links to 'Run diagnostics' which calls /api/doctor\n- /healthz already covers runtime health; /api/doctor covers configuration health — keep them separate\n\n## Acceptance\n- GET /api/doctor returns 200 with all checks when fully configured\n- Reports 'firmware_dir: error' when /firmware is empty\n- Reports 'mqtt_reachable: warn' when MQTT broker env is set but broker is unreachable\n- Unit tests cover each check in isolation with mocked dependencies","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","created_at":"2026-05-02T12:22:51.188946318Z","updated_at":"2026-05-05T12:39:28.566216915Z","closed_at":"2026-05-05T12:39:28.566216915Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-1kid","title":"Add host test asserting default-placement sim produces non-zero fusion peaks","description":"Child of bf-18yn umbrella (final child). Depends on child 3. This is the bf-18yn acceptance-criteria test.\n\nScope:\n- Add a sim/host (or package) test that, using only the DEFAULT node placement (no manual positioning), constructs/seeds the 3D fusion engine and asserts the accumulation grid produces non-zero peaks, i.e. len(blobs) > 0 or grid max above threshold, closing the bf-4q5w symptom. Extend the bf-6s3d fleet->engine invariant test or add a sibling.\n\nAcceptance criteria:\n- Test passes with default placement, and fails (or is skipped with a clear reason) if nodes collapse to (0,0,1).\n- go build ./... passes; existing simulator and fusion tests still pass.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T22:09:38.566010752Z","updated_at":"2026-07-03T23:24:56.503387578Z","closed_at":"2026-07-03T23:24:56.503387578Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-1kid","depends_on_id":"bf-xrej","type":"blocks","created_at":"2026-07-03T22:10:08.938736179Z","created_by":"cli","thread_id":""}],"comments":[{"id":58,"issue_id":"bf-1kid","author":"cli","text":"Implemented and pushed (commit 1df0bfa, on origin/main).\n\nAdded two sibling tests to TestEngine_SeedNodePositions (bf-6s3d) in mothership/internal/fusion/fusion_test.go:\n\n1. TestEngine_DefaultPlacementProducesPeaks — the bf-18yn acceptance test / bf-4q5w fix. Seeds the 3D fusion engine using ONLY the default placement (simulator.DefaultNodePositions, the spread geometry a freshly-onboarded virtual/sim fleet gets with no manual positioning — bf-3fr6/bf-xrej), drives a synthetic walker through the room centre, and asserts the accumulation grid produces non-zero peaks (len(blobs) > 0 OR gridMax above threshold) across default placements of 2/3/4/6 nodes. assertPlacementNotCollapsed fails loudly if placement ever collapses to (0,0,1).\n\n2. TestEngine_CoLocatedOriginYieldsNoPeaks — counter-example pinning the bf-4q5w symptom: nodes left at the (0,0,1) DB default are co-located, every link is degenerate, the grid stays at zero, and Fuse emits 0 blobs. Proves the non-zero-peak assertion is meaningful, not trivially satisfiable.\n\nAcceptance criteria met: passes with default placement; co-located (0,0,1) collapse yields no peaks (documented). go build ./... / go vet ./... / go test ./... all green across mothership, cmd/sim, test/acceptance.","created_at":"2026-07-03T23:19:39.087176089Z"},{"id":59,"issue_id":"bf-1kid","author":"cli","text":"Closed bf-18yn acceptance test (commit 1df0bfa, pushed to origin/main).\n\nTestEngine_DefaultPlacementProducesPeaks: seeds the 3D fusion engine using ONLY the default node placement — simulator.DefaultNodePositions, the spread geometry a freshly-onboarded virtual/sim fleet receives with no manual positioning (bf-3fr6/bf-xrej). Runs over 2/3/4/6-node default placements, drives a synthetic walker (buildSyntheticLinks) through the room centre, and asserts len(blobs) > 0 OR gridMax above threshold. assertPlacementNotCollapsed fails loudly if default placement ever collapses to the co-located (0,0,1) DB default (the bf-4q5w root cause).\n\nTestEngine_CoLocatedOriginYieldsNoPeaks: counter-example pinning bf-4q5w — nodes left collapsed at (0,0,1) are co-located, every link is degenerate, the grid stays at zero, and Fuse emits 0 blobs. Proves the non-zero-peak assertion is meaningful, not trivially satisfiable.\n\nSibling to bf-6s3d's TestEngine_SeedNodePositions (which locks the seeding invariant; this locks its downstream consequence).\n\nAcceptance verified: go build/vet/test ./... pass across all three go.work modules (mothership incl. internal/fusion + internal/simulator, cmd/sim, test/acceptance). Targeted run: DefaultPlacement subtests nodes=2/3/4/6 PASS, CoLocatedOriginYieldsNoPeaks PASS, SeedNodePositions PASS.\n\n(implementation was complete in a prior attempt; this attempt verified correctness and closed the bead with the working comment-then-close path — the dispatch footer's --body flag is invalid for this br build)","created_at":"2026-07-03T23:24:47.063692741Z"}]} +{"id":"bf-1m2x","title":"Verify blob-creation inventory is current at HEAD","description":"## Context\nbf-4bhd's discovery objective (find all blob creation code paths) is ALREADY DELIVERED by its closed dependency subtree (bf-1q3m, bf-3ldj, bf-55rp, bf-5uzm, bf-4wwt, bf-1bmg) and consolidated in `notes/bf-1q3m-consolidated.md`, which explicitly supersedes `notes/bf-4bhd.md`. This child CLOSES the \"is the inventory actually complete and current?\" loop rather than re-doing discovery from scratch (the anti-pattern that has burned the prior 4 attempts).\n\n## Scope\n- Confirm the consolidated report's 23 production construction sites (5 primary + 16 projection + 2 JS) still match the CURRENT HEAD (report was verified at `1a26c12`).\n- Re-run the completeness greps from report section 1 against current HEAD:\n - `grep -rnE \"Blob\\{|TrackedBlob\\{\" mothership/ --include=*.go | grep -v _test.go` (expect 5 primary)\n - projection-type literals grep (expect 15 non-test projection literals)\n- Record current HEAD SHA + per-category counts; document any site added/removed since `1a26c12`.\n\n## Acceptance Criteria\n- [ ] Short verification note records current HEAD SHA + grep counts (expected: 5 primary / 15 non-test projection / 2 JS production; zero deltas vs `1a26c12`).\n- [ ] Any delta vs `1a26c12` is explicitly documented (expected none — no Go/JS code changed since 1a26c12).\n- [ ] Note cites `notes/bf-1q3m-consolidated.md` as the source of truth and does NOT re-search from scratch.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T20:13:42.925805896Z","updated_at":"2026-07-06T20:20:02.068987123Z","closed_at":"2026-07-06T20:20:02.068987123Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"comments":[{"id":83,"issue_id":"bf-1m2x","author":"cli","text":"Implemented: notes/bf-1q3m-consolidated.md inventory confirmed current at HEAD 0ae0c03. Re-ran the two completeness greps (Blob{|TrackedBlob{ -> 5 primary; BlobSnapshot{|BlobState{|... -> 15 non-test named projection) plus type-def grep (15 types) and JS sites (2) — zero deltas vs the report's verification commit 1a26c12 (no Go/JS source changed in the interval; only notes/ + .beads/). The 16-sites vs 15-literals distinction reconciled (P3 main.go:2288 is an anonymous-struct literal, absent from the named-type grep). Cited notes/bf-1q3m-consolidated.md as source of truth; did NOT re-search from scratch. Verification note at notes/bf-1m2x-verification.md (commit 64a2202, pushed). Notes-only; no source modified.","created_at":"2026-07-06T20:19:10.410924018Z"}]} +{"id":"bf-1oj7","title":"Node placement UI: TransformControls for dragging live registered nodes in 3D dashboard","description":"Plan Phase 3 item 18 requires the ability to drag actual registered (live) nodes in the 3D Three.js dashboard using TransformControls to adjust their physical placement. The pre-deployment simulator supports this, but dragging live nodes in the operational dashboard is not implemented. Add TransformControls to the live node meshes in the dashboard with position persistence via the mothership API.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-05-26T22:14:42.792062952Z","updated_at":"2026-07-06T03:47:54.761005897Z","closed_at":"2026-07-06T03:47:54.761005897Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-1q3m","title":"Consolidate and verify all blob-creation findings into single report","description":"Scope: consolidate children 1-4 into a SINGLE verified source-of-truth report and produce the tiered fix-target list the next implementation bead consumes. This is the \"report ready for the next bead\" deliverable.\n\nInputs: notes from children 1-4 (primary types bf-55rp, conversions bf-5uzm, projections bf-4wwt, JS/TS bf-1bmg).\n\nDeliverable: a single consolidated report (notes/bf--consolidated.md) that:\n- Merges the full inventory (primary types + conversions + projections + JS/TS) into one catalogue\n- Re-verifies EVERY file:line against current HEAD (prior reports drifted before — this is the whole reason the parent kept failing)\n- Flags any stale entries found in the sub-bead notes\n- Produces the tiered fix-target list for the identity-leak fix (Tier 1: identity dropped at main.go:2213 automation + :2116 explainability; Tier 2-4 as appropriate)\n- Includes the blob data-flow diagram showing where identity enters and where it leaks\n\nAcceptance Criteria:\n- [ ] Single consolidated report exists at notes/bf--consolidated.md\n- [ ] Every file:line in the catalogue re-verified against current HEAD (run the greps to confirm)\n- [ ] Stale entries from sub-bead notes explicitly flagged\n- [ ] Tiered fix-target list for the identity leak produced\n- [ ] Report explicitly states it supersedes notes/bf-3ldj-findings.md and notes/bf-4bhd.md for the inventory\n- [ ] Report is ready for the implementation bead to consume without re-searching","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T15:04:01.063024261Z","updated_at":"2026-07-06T20:07:06.993145748Z","closed_at":"2026-07-06T20:07:06.993145748Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-1q3m","depends_on_id":"bf-1bmg","type":"blocks","created_at":"2026-07-06T15:04:21.508791539Z","created_by":"cli","thread_id":""}],"comments":[{"id":82,"issue_id":"bf-1q3m","author":"cli","text":"Deliverable complete: notes/bf-1q3m-consolidated.md is the single re-verified source-of-truth report (commit f144aad, pushed to origin/main). Re-verified every file:line against current HEAD by running the greps (did not trust child notes): all 15 blob types, all 5 primary construction sites, and all 4 named leak boundaries are exact -- E2 main.go:2303 (automation.TrackedBlob, no identity fields), E3 main.go:2206 (explainability.BlobSnapshot), volume main.go:2326 (BlobPos PersonID field exists, left empty), E1 main.go:5494. Reference pattern is analytics.TrackUpdate at main.go:2271 which pulls PersonID from identityMatcher.GetMatch at :2267. Completeness sweeps confirm exactly 5 primary + 15 projection non-test literals, so no production site is missed. Stale entries flagged in section 4: bf-3ldj and bf-4bhd are SUPERSEDED (their E2 :2213 / E3 :2116 / E1 :5384 drifted to :2303 / :2206 / :5494; 2D tracking.Blob field list also drifted). Report explicitly supersedes both for the inventory. Tiered fix-target list in section 6: Tier 1 = add identity fields and populate from the ble.IdentityMatcher sidecar at main.go:2303, :2206 (fold in identityMap :2216), and :2326; Tiers 2-4 documented. Report is ready for the implementation bead to consume without re-searching. NOTE: the prior failure-count:1 was the dispatch footer's invalid 'br close --body' flag (no such flag exists) -- closing via br comments add + br close as the tool requires.","created_at":"2026-07-06T20:05:09.702563495Z"}]} +{"id":"bf-1rzd","title":"Search TypeScript files for blob-shaped object literals","description":"## Objective\nSearch all TypeScript files for blob-shaped object literals.\n\n## Scope\n- Search only .ts and .tsx files\n- Use the pattern defined in the parent bead\n- Identify all locations with blob structure\n\n## Acceptance Criteria\n- [ ] All TypeScript files searched\n- [ ] Blob literal locations identified with file paths and line numbers\n- [ ] Results saved to notes/bf-26ta-typescript-results.md\n- [ ] Each match includes context snippet showing the blob structure","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T06:01:46.201406389Z","updated_at":"2026-07-06T06:08:54.854421997Z","closed_at":"2026-07-06T06:08:54.854421997Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-1rzd","depends_on_id":"bf-3aij","type":"blocks","created_at":"2026-07-06T06:01:58.385955215Z","created_by":"cli","thread_id":""}]} +{"id":"bf-1t0kn","title":"OTA auto-update with canary strategy and quiet window","description":"Plan specifies a canary OTA strategy (Component 6): update one node first, monitor quality for 10 min, then roll out fleet-wide. Also needs a configurable quiet window (default 02:00–05:00 local) and auto-update mode toggle. Currently the fleet manager only does manual rolling OTA — no canary logic, no scheduled quiet window, no auto-update-on-firmware-detect. Implementation needed in internal/ota and/or fleet manager with a settings key for auto_update_enabled, quiet_window, canary_duration_min.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:24:59.888109951Z","updated_at":"2026-05-05T16:42:25.163065452Z","closed_at":"2026-05-05T16:42:25.163065452Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-1tsm","title":"Add startup test for fusion engine node positions and verify full build/tests pass","description":"Depends on bf-6s3d. Add verification that the fusion engine holds populated, distinct node positions after startup, then ensure the whole module builds and tests pass.\n\nTasks:\n- Add a host/unit test or a logged startup assertion using engine.NodePositions()/NodeCount() (fusion.go:140-159) confirming positions are populated for N nodes with no all-(0,0,1) collapse. The NodePositions() docstring already notes this startup-assertion use.\n- Run go build ./... and the existing test suite from mothership/.\n\nAcceptance criteria:\n- A test or startup assertion confirms the engine holds a distinct position for each of N nodes after seeding.\n- go build ./... succeeds.\n- Existing tests pass with no regressions.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T19:54:00.053417727Z","updated_at":"2026-07-03T21:03:26.394459456Z","closed_at":"2026-07-03T21:03:26.394459456Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-1tsm","depends_on_id":"bf-6s3d","type":"blocks","created_at":"2026-07-03T19:54:12.987973384Z","created_by":"cli","thread_id":""}],"comments":[{"id":49,"issue_id":"bf-1tsm","author":"cli","text":"bf-1tsm done: added the complementary logged startup assertion that the NodePositions() docstring (fusion.go:140-159) already advertises — the half not covered by bf-6s3d's TestEngine_SeedNodePositions unit test. Right after main.go's seeding loop (fusionEngine.SetNodePosition per fleet node), the engine inspects fusionEngine.NodePositions() and logs the invariant: INFO when N nodes hold distinct non-default positions; WARN 'all N collapsed to (0,0,1)' if every seeded node still sits at the nodes-table schema default pos_x=0/pos_y=0/pos_z=1 (fleet never positioned -> degenerate fusion); INFO 'no nodes positioned yet' for an empty fleet (valid per IO-1). Does not block startup; hard regression gate stays the bf-6s3d unit test. Also gofmt'd main.go (bf-3f6q's fusion import was out of alphabetical order). go build ./... / go vet ./... / go test ./... all pass; gofmt clean. Commit 3785619 pushed to origin/main.","created_at":"2026-07-03T20:56:15.016380449Z"},{"id":50,"issue_id":"bf-1tsm","author":"cli","text":"Implemented + verified (commit 3785619, on origin/main):\n\n- Added a logged startup assertion in mothership/cmd/mothership/main.go right after the fleet->engine seeding loop, using fusionEngine.NodePositions()/NodeCount(). It logs:\n * INFO 'no nodes positioned yet' when N==0 (valid; IO-1 reaches ready with no nodes)\n * WARN when all N seeded nodes collapsed to the (0,0,1) schema default (degenerate fusion)\n * INFO 'N nodes seeded, D distinct, A at (0,0,1)' otherwise\n The assertion surfaces the all-(0,0,1) collapse without blocking startup (hard gate stays TestEngine_SeedNodePositions from bf-6s3d).\n- The NodePositions() docstring (fusion.go:140-159) already advertised this startup-assertion use; this bead exercises it in the live path.\n- Drive-by gofmt of main.go imports (alphabetized 'fusion' after 'floorplan').\n\nVerification (acceptance criteria):\n- go build ./... : PASS (exit 0)\n- go vet ./... : PASS (exit 0)\n- go test ./... : PASS (all packages, incl. tests/e2e 118.8s, test/acceptance)\n- TestEngine_SeedNodePositions forced fresh (-count=1): PASS\n- gofmt -l clean on changed files","created_at":"2026-07-03T21:02:27.636610591Z"}]} +{"id":"bf-1wvm","title":"Extend blob data structure with identity fields","description":"## Objective\nAdd the actual identity fields to blob data objects in the codebase.\n\n## Scope\n- Update code that creates blob objects to include the new fields\n- Add default/undefined values for personName, assignedColor, identityResolved\n- Ensure all blob creation paths are covered\n\n## Acceptance Criteria\n- [ ] All blob creation code includes the new identity fields\n- [ ] New fields are set to undefined or appropriate defaults for existing blobs\n- [ ] No TypeScript errors from missing fields\n- [ ] Code runs without runtime errors from undefined fields\n\n## Notes\nThis adds the fields to runtime data. Previous bead added the types.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T04:41:42.917167576Z","updated_at":"2026-07-06T12:46:52.964790297Z","source_repo":".","compaction_level":0,"labels":["split-child","umbrella"],"dependencies":[{"issue_id":"bf-1wvm","depends_on_id":"bf-2186","type":"blocks","created_at":"2026-07-06T04:41:46.160682326Z","created_by":"cli","thread_id":""},{"issue_id":"bf-1wvm","depends_on_id":"bf-f841","type":"blocks","created_at":"2026-07-06T05:12:13.907081617Z","created_by":"cli","thread_id":""}],"comments":[{"id":88,"issue_id":"bf-1wvm","author":"cli","text":"bf-5cgc handoff → notes/bf-5cgc-handoff.md §3. Your dashboard-JS scope maps to consolidated-report Tier-4 #8 (dashboard/js/state.js:290 blob creation literal) — frontend identity plumbing. It only becomes useful once the GO Tier-1 sites (automation/explainability/volume in main.go) actually emit identity fields to the dashboard; those are tracked under bf-5151. No Go edit expected from this bead.","created_at":"2026-07-06T20:39:00.002448722Z"}]} +{"id":"bf-2186","title":"Add TypeScript type definitions for blob identity fields","description":"## Objective\nAdd TypeScript interface/type definitions for the new identity-related fields.\n\n## Scope\n- Extend the blob TypeScript interface with new fields:\n - personName: string (optional)\n - assignedColor: string (hex/rgb format, optional)\n - identityResolved: boolean (optional)\n- Use proper TypeScript optional types to ensure backward compatibility\n\n## Acceptance Criteria\n- [ ] Blob TypeScript interface includes personName, assignedColor, identityResolved fields\n- [ ] All new fields are typed as optional (string | undefined, boolean | undefined)\n- [ ] Interface compiles without TypeScript errors\n- [ ] JSDoc comments added explaining the new fields\n\n## Notes\nType definitions only. No runtime code changes yet.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T04:41:22.516835794Z","updated_at":"2026-07-06T05:05:06.412791638Z","closed_at":"2026-07-06T05:05:06.412791638Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-2186","depends_on_id":"bf-13s0","type":"blocks","created_at":"2026-07-06T04:41:35.808332209Z","created_by":"cli","thread_id":""}]} +{"id":"bf-21nd","title":"Install and configure axe-core with Playwright","description":"Install @axe-core/playwright package and configure it in the Playwright test suite. Set up the basic axe-core integration pattern that can be reused across tests.\n\n**Acceptance Criteria:**\n- @axe-core/playwright is installed as a dev dependency\n- A shared accessibility test helper is created (e.g., tests/accessibility/helper.ts)\n- The helper exports a function to run axe-core scans with WCAG 2.1 AA tags\n- A minimal test file demonstrates the integration (smoke test)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T03:35:44.192535701Z","updated_at":"2026-07-06T04:26:00.734281351Z","closed_at":"2026-07-06T04:26:00.734281351Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"]} +{"id":"bf-232u3","title":"GET /api/notifications/preview — rendered test thumbnail endpoint","description":"Plan (Component 30, Renderer spec) specifies a test endpoint: GET /api/notifications/preview?type=fall&person=Alice returns a rendered test image for UI development and QA. The render package (internal/render/floorplan.go) implements thumbnail generation with fogleman/gg, but the preview HTTP endpoint is never registered in main.go. Needs: handler that accepts ?type and ?person query params, calls the appropriate Generate*Thumbnail function, returns the PNG bytes with Content-Type: image/png.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:20.897907993Z","updated_at":"2026-05-05T17:25:10.053618969Z","closed_at":"2026-05-05T17:25:10.053618969Z","close_reason":"GET /api/notifications/preview endpoint already implemented and registered in commit 37571ec. The endpoint accepts ?type (fall, anomaly, zone_enter, sleep) and ?person query params, calls the appropriate Generate*Thumbnail function from internal/render/floorplan.go, and returns PNG bytes with Content-Type: image/png. All tests pass.","source_repo":".","compaction_level":0} +{"id":"bf-2330","title":"Add hard-fail blob and detection-event assertion primitives to the e2e harness","description":"Split from bf-5jeo (verification capstone). Foundation child for the e2e assertion work; the other children reuse these primitives.\n\nCONTEXT: mothership/tests/e2e/e2e_test.go captures blob/detection data but never enforces it. AssertDuringRun (line 375) logs \"No detection events during run (fusion pipeline may not have produced blobs)\" and returns nil instead of failing. WatchDashboardWS (line 326) returns blobCounts []int (line 334) that no caller asserts on. The dodge is structural.\n\nSCOPE:\n- Make AssertDuringRun return a non-nil, descriptive error when ZERO blobs AND ZERO detection events are observed across the whole run window (replace the soft log with a hard failure reporting elapsed time + counts observed). Keep the existing health==ok and nodes-online==expected assertions intact.\n- Add a reusable helper to assert max(blobCounts) >= 1 from a WatchDashboardWS result.\n- Add a reusable helper to assert len(events.Events) >= 1 from a GetEvents(ctx, detection, N) result.\n- Both helpers must be usable by TestDetectionEvents / TestFullE2EIntegration / the IO-6 hard-gate test (children 2-4).\n\nACCEPTANCE CRITERIA:\n- New/changed helpers compile (go build ./tests/e2e/... and go vet).\n- AssertDuringRun returns a descriptive error when no blob is produced (no longer silently nil).\n- The health and nodes-online checks are unchanged and still pass.\n- No weakening of any existing assertion.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T18:50:41.863718681Z","updated_at":"2026-07-06T18:50:41.863718681Z","source_repo":".","compaction_level":0,"labels":["split-child"]} +{"id":"bf-24xp","title":"Send node position in spaxel-sim hello and persist it on registration","description":"Child of bf-18yn umbrella. Depends on the geometry helper (child 1).\n\nProblem: cmd/sim/main.go and mothership/cmd/sim/main.go compute good corner geometry in generateNodePositions/createVirtualNodes, but the hello message sent on WebSocket connect (connectNodes) does NOT include position. So the mothership never learns the sim node location and the fleet registry/DB row is left at the schema default (0,0,1), co-located with every other node.\n\nScope:\n- Add the node computed position (X,Y,Z) to the hello JSON message in both cmd/sim/main.go and mothership/cmd/sim/main.go, sourced from createVirtualNodes (or child 1 helper).\n- In the mothership node-registration/hello handler, accept the position and store it via the registry so the node DB row is NOT left at (0,0,1). Ensure it flows to the fusion engine via the existing connect/register sink wired in bf-3p6g (manager.go OnNodeConnected -> nodePositionSink -> fusionEngine.SetNodePosition).\n\nAcceptance criteria:\n- A spaxel-sim node connecting with a position results in a fleet-registry/DB node row whose pos differs from (0,0,1) and matches the sent coordinates.\n- go build ./... passes; registration-path tests updated or added.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T22:09:38.428986890Z","updated_at":"2026-07-03T22:47:49.062186306Z","closed_at":"2026-07-03T22:47:49.062186306Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-24xp","depends_on_id":"bf-3fr6","type":"blocks","created_at":"2026-07-03T22:10:08.812056730Z","created_by":"cli","thread_id":""}],"comments":[{"id":56,"issue_id":"bf-24xp","author":"cli","text":"Implemented and verified: both sim CLIs (cmd/sim and mothership/cmd/sim) announce pos_x/pos_y/pos_z in the hello handshake, sourced from createVirtualNodes corner geometry. HelloMessage gained PosX/PosY/PosZ as *float64 (nil = not announced, so a real ESP32 preserves any user-placed position). The connect/register path (server.HandleNodeWS -> FleetNotifier.OnNodeConnected -> Manager) carries the announced position through; the Manager persists it via registry.SetNodePosition only when all three axes are present, then forwards it to the fusion engine through the bf-3p6g sink (GetNodePosition -> ForwardNodePosition -> fusionEngine.SetNodePosition). Acceptance criteria met: TestManagerOnNodeConnectedPersistsHelloPosition asserts a node connecting with an announced position yields a registry/DB row whose pos differs from (0,0,1) and matches the sent coords, and that the coords reach the fusion engine. go build ./..., go vet ./..., and go test ./... (50 packages) all pass. Commit d048987 pushed to origin/main.","created_at":"2026-07-03T22:47:08.588926475Z"}]} +{"id":"bf-25dmx","title":"Guided troubleshooting (Component 36)","description":"## Goal\nWhen system detects that user might be struggling or detection quality has degraded, proactively offer contextual help — but never when things are working well.\n\n## Trigger conditions and responses:\n\nDetection quality drops:\n- Condition: Zone-level detection quality below 60% for >24 hours\n- Banner in timeline and 3D view: 'Detection in the kitchen has been less reliable this week. Want me to help diagnose?'\n- Guided flow: Check node connectivity → show link health with explainability → suggest node repositioning → offer re-baseline → 'Still not right? Try adding a node here [highlighted position]'\n\nRepeated setting changes:\n- Condition: Same settings key modified 3+ times within 60-minute sliding window (qualifying keys: delta_rms_threshold, breathing_sensitivity, tau_s, fresnel_decay, n_subcarriers)\n- Tracking: per-key edit counter in memory, resets after 60 min inactivity\n- Trigger: when counter reaches 3, set hint_pending flag\n- Frontend: show non-intrusive banner: 'You've adjusted the detection threshold several times. Would you like me to show you what the system is seeing?' with [Show me] and [×] dismiss\n- [Show me]: opens time-travel to most recent detection event before first edit, with explainability overlay pre-activated\n- Cooldown: 24 hours after hint is shown\n\nNode offline:\n- Condition: Any node offline for >2 hours\n- Timeline event with expandable troubleshooting steps\n\nFirst-time feature discovery:\n- Condition: User opens feature panel for first time\n- Brief, non-intrusive tooltip (not modal): 'Draw a box around an area, then choose what happens when someone enters or leaves. [Got it]'\n- Shown once, never repeated\n\nAfter false positive feedback:\n- Inline response in timeline: 'Got it. I've slightly raised the detection threshold for the contributing links. If this keeps happening at this time of day, my hourly baseline will adapt within a few days.'\n\nAfter successful calibration:\n- Positive reinforcement: 'Re-baseline complete. Detection quality in the kitchen improved from 64% to 89%.'\n\n## Design principles\n- Reactive, not proactive: help appears only when something seems wrong or when user is clearly exploring\n- Dismissible in one tap: never blocks UI\n- Never repeats after dismissal (stored in localStorage)\n- Always explains what will happen next\n- Never condescending: assumes user is intelligent but may not know CSI physics\n\n## Acceptance\n- Detection quality drop triggers helpful banner\n- Repeated setting changes trigger hint\n- Node offline shows troubleshooting steps\n- First-time feature discovery shows tooltip once\n- Feedback responses are helpful\n- Calibration success shows positive reinforcement","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:29.724435180Z","updated_at":"2026-05-06T10:19:23.187979577Z","closed_at":"2026-05-06T10:19:23.187979577Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-26ta","title":"Search for blob-shaped object literals","description":"## Objective\nFind all locations where blob-shaped objects are created via object literals.\n\n## Scope\n- Search for object literals with blob structure (id, type, data fields)\n- Identify TypeScript and JavaScript files\n- Document each literal location\n\n## Acceptance Criteria\n- [ ] All blob-shaped object literals are identified\n- [ ] Each location documented with file path and line number\n- [ ] Blob structure pattern noted for each site","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T05:31:37.599364650Z","updated_at":"2026-07-06T13:56:30.484444172Z","closed_at":"2026-07-06T13:56:30.484444172Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-26ta","depends_on_id":"bf-5kns","type":"blocks","created_at":"2026-07-06T05:31:56.467390976Z","created_by":"cli","thread_id":""},{"issue_id":"bf-26ta","depends_on_id":"bf-3tlw","type":"blocks","created_at":"2026-07-06T06:02:20.474309914Z","created_by":"cli","thread_id":""}],"comments":[{"id":61,"issue_id":"bf-26ta","author":"cli","text":"Closing as COMPLETE — auto-dispatch re-fired the split on an already-split, already-complete bead. Findings: All 5 children closed (bf-3aij pattern, bf-1rzd TS search, bf-4ly4 JS search, bf-5kns Blob constructor calls, bf-3tlw compiled findings). Deliverables committed: notes/bf-26ta-findings.md (Go 10 types/20+ sites, JS 14 instances/5 files, TS type defs only), bf-26ta-javascript-results.md, bf-26ta-typescript-results.md. All three parent acceptance criteria met (literals identified, file:line documented, structure pattern noted). The failure-count:3 + repeated claims were a re-dispatch loop on an unclosed umbrella; closing breaks the loop and unblocks bf-67ao (blob factory search). NOT re-splitting — duplicate children would misrepresent a done task.","created_at":"2026-07-06T13:55:20.535048726Z"}]} +{"id":"bf-28dv","title":"Component 24: Fleet-level sentinel link coordination (adaptive sensing idle mode)","description":"The ratecontrol.go implements per-node adaptive rate (active: 20-50 Hz, idle: 2 Hz) but is missing the fleet-level coordination described in plan.md Component 24.\n\nThe plan specifies:\n- When ALL zones are idle, designate one sentinel link per zone at 5 Hz; all other links drop to 1 Hz\n- When activity detected in one zone, ramp that zone to full rate AND adjacent zones to 5 Hz (adjacency exists but only via OnMotionHint, not zone-level)\n- Prediction engine can preemptively ramp zones before predicted arrivals\n\nWhat exists in ratecontrol.go:\n- Per-node state machine: Active (20 Hz) / Idle (2 Hz)\n- OnMotionHint: preemptively ramps adjacent nodes to full rate\n- OnMotionState: transitions node between active/idle\n- SetAdjacentNodesFn: adjacency function for adjacent-node ramping\n\nWhat is missing:\n1. Zone-level idle detection: when all nodes in a zone have been idle, designate one sentinel link per zone at 5 Hz and drop all others to 1 Hz\n2. Fleet-wide idle state: when all zones are idle, only sentinel links at 5 Hz run\n3. Prediction-engine-driven preemptive zone ramp: RateController needs a RampZone(zoneID) method that the prediction engine can call when P(arrival in zone) > threshold\n4. The 1 Hz floor rate (current code uses 2 Hz idle; plan says 1 Hz for all-idle fleet state)\n\nScope: Add zone-aware idle coordination to RateController. Add RampZone(zoneID) interface for prediction engine wiring. Adjust idle rate to 1 Hz for fully-idle fleet.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-05-26T04:04:04.870466269Z","updated_at":"2026-07-06T03:19:06.448671235Z","closed_at":"2026-07-06T03:19:06.448671235Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-2adi","title":"Add accessibility tests for dashboard pages","description":"Create axe-core accessibility tests for all dashboard pages (ambient, live, simple, simulator). Each test should navigate to the page and scan for WCAG 2.1 AA violations.\n\n**Acceptance Criteria:**\n- Test file dashboard/tests/a11y-dashboard.spec.js exists\n- Tests cover all 4 dashboard pages: ambient, live, simple, simulator\n- Each test navigates to the page and runs an axe-core scan\n- Tests fail if any WCAG 2.1 AA violations are found\n- Tests can be run locally with 'npx playwright test a11y-dashboard'","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T03:35:55.473957414Z","updated_at":"2026-07-06T04:27:34.648883970Z","closed_at":"2026-07-06T04:27:34.648883970Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-2adi","depends_on_id":"bf-21nd","type":"blocks","created_at":"2026-07-06T03:36:48.664082960Z","created_by":"cli","thread_id":""}]} +{"id":"bf-2aqf","title":"Strengthen TestFullE2EIntegration to assert at least one blob is produced","description":"Split from bf-5jeo (verification capstone). Depends on the TestDetectionEvents child. Child 3 of 4.\n\nCONTEXT: mothership/tests/e2e/e2e_test.go:812 TestFullE2EIntegration captures blobCounts via WatchDashboardWS (line 803) and events via GetEvents (~line 843), but asserts neither count. The comment (~line 843-844) says \"we verify the API responds correctly rather than asserting a minimum count\" — the central dodge called out in bf-5jeo.\n\nSCOPE:\n- Remove the dodge. Assert max(blobCounts) >= 1 from the WatchDashboardWS result (helper from bf-2330).\n- Also assert len(events.Events) >= 1 from GetEvents(ctx, detection, 100) using the bf-2330 helper.\n- Delete/replace the \"verify the API responds correctly rather than asserting a minimum count\" comment.\n- Keep the 4-node / 2-walker / 30s RunSimulator config and AssertDuringRun call (AssertDuringRun itself is hardened by bf-2330).\n\nACCEPTANCE CRITERIA:\n- TestFullE2EIntegration asserts >= 1 blob via BOTH the dashboard WS blobCounts and the events API.\n- The minimum-count dodge comment is gone.\n- Test compiles (go build ./tests/e2e/...).\n- NOTE: expected RED until upstream bf-4q5w lands. Do NOT weaken the assertion to force green; if it cannot pass, block and feed a finding upstream per child 4.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T18:51:03.203633411Z","updated_at":"2026-07-06T18:51:03.203633411Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-2aqf","depends_on_id":"bf-16c1","type":"blocks","created_at":"2026-07-06T18:51:13.341776592Z","created_by":"cli","thread_id":""}]} +{"id":"bf-2enwo","title":"Simulator: ray-based propagation engine (internal/sim/propagation.go — direct path + first-order reflections)","description":"","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-06T00:40:16.873911203Z","updated_at":"2026-05-06T01:55:05.181196917Z","closed_at":"2026-05-06T01:55:05.181196917Z","close_reason":"Completed","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"compacted_at_commit":"","sender":""} +{"id":"bf-2fz8","title":"Investigate which fusion engine feeds the live blob loop and IO-6 gate","description":"Prerequisite investigation for the bf-3gw1 umbrella. The live 10Hz loop in mothership/cmd/mothership/main.go:1867 calls pm.GetTrackedBlobs(), but grep confirms mothership/internal/fusion.NewEngine (3D Fresnel engine, fusion.go:93) is NEVER constructed in non-test code. Determine definitively which engine produces the blobs consumed by the live loop and the IO-6 hard-gate.\n\nTasks:\n- Trace pm (processing manager) and GetTrackedBlobs() to the engine that actually emits tracked blobs in the live path.\n- Confirm whether internal/fusion.Engine is the intended blob source, or whether blobs come from a different engine (e.g. the localization tracker).\n- Find/confirm any existing non-test construction site of internal/fusion.NewEngine (expected: none).\n- Record the finding as a bead comment: engine package, construction site (or 'none - must be added'), and the exact line where the live loop reads blobs.\n\nAcceptance criteria:\n- A documented determination (bead comment) naming the blob-producing engine with file:line references.\n- States explicitly whether internal/fusion.NewEngine must be newly constructed in non-test code.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T19:53:40.123789285Z","updated_at":"2026-07-03T20:15:51.969894260Z","closed_at":"2026-07-03T20:15:51.969894260Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"comments":[{"id":44,"issue_id":"bf-2fz8","author":"cli","text":"INVESTIGATION COMPLETE (bf-2fz8). Determination of which fusion engine feeds the live blob loop + IO-6 gate:\n\nBLOB SOURCE: NONE. No engine currently produces tracked blobs for the live path.\n- pm.trackedBlobs (internal/signal/processor.go:227) is always nil: SetTrackedBlobs (processor.go:625) has ZERO callers.\n- GetTrackedBlobs() is read by: the live 10Hz loop (cmd/mothership/main.go:1866), /api/blobs (main.go:4056), anomalyPositionAdapter (main.go:4840), and API wrappers (internal/api/status.go:84, internal/api/tracks.go:99) — ALL see zero blobs. IO-6 ('walker produces a tracked blob') cannot pass.\n\nENGINE INVENTORY:\n- internal/fusion.NewEngine (3D Fresnel, internal/fusion/fusion.go:93) = the INTENDED blob producer, but NEVER constructed in non-test code. The package is imported by exactly one file, internal/localizer/fusion/timing_budget_test.go (a test); the only NewEngine call sites are that test and internal/fusion/fusion_test.go.\n- internal/localization.NewEngine (2D Fresnel, internal/localization/fusion.go:53) = IS live, via NewSelfImprovingLocalizer (main.go:1005), runs Fuse (main.go:2593), but the returned *FusionResult is DISCARDED — nothing reads .Peaks, and no signal.TrackedBlob literal exists outside test code.\n\nCONCLUSION: internal/fusion.NewEngine MUST be newly constructed in non-test code and wired (Fuse -> TrackedBlob -> SetTrackedBlobs), OR the existing localization.Engine FusionResult.Peaks must be converted to TrackedBlobs and stored via SetTrackedBlobs. Also recorded in code as a doc comment on SetTrackedBlobs (commit a7ec9d2).","created_at":"2026-07-03T20:10:24.738723116Z"},{"id":45,"issue_id":"bf-2fz8","author":"cli","text":"INVESTIGATION COMPLETE (committed a7ec9d2, on origin/main; go vet + go test green).\n\nFINDING: NO engine currently feeds the live blob loop. pm.trackedBlobs is always nil.\n\n- Blob source determination: SetTrackedBlobs (internal/signal/processor.go:631) is the ONLY writer of pm.trackedBlobs and has ZERO callers -> GetTrackedBlobs() always returns nil -> IO-6 cannot pass.\n- Exact live-loop read line: cmd/mothership/main.go:1866 (blobs := pm.GetTrackedBlobs()). Other readers: /api/blobs main.go:4056, anomalyPositionAdapter main.go:4840, internal/api/status.go:84, internal/api/tracks.go:99.\n- internal/fusion.NewEngine (3D Fresnel, internal/fusion/fusion.go:93): construction site = NONE in non-test code. The internal/fusion package is imported by ZERO non-test files; only NewEngine call sites are internal/localizer/fusion/timing_budget_test.go and internal/fusion/fusion_test.go.\n- internal/localization.NewEngine (2D Fresnel, internal/localization/fusion.go:53) IS constructed live (NewSelfImprovingLocalizer main.go:1005, Fuse main.go:2593) but the returned *FusionResult is DISCARDED -> nothing reads .Peaks; only GetLearnedWeights() consumed (main.go:2599). No signal.TrackedBlob literal outside tests (automation.TrackedBlob at main.go:2111 is a distinct type).\n\nCONCLUSION: internal/fusion.NewEngine MUST be newly constructed in non-test code and wired Fuse->TrackedBlob->SetTrackedBlobs, OR localization.Engine FusionResult.Peaks converted to TrackedBlobs via SetTrackedBlobs. Findings also in doc comments (internal/signal/processor.go:601-630).","created_at":"2026-07-03T20:15:38.634306719Z"}]} +{"id":"bf-2gmx","title":"Verify backward compatibility for blob identity fields","description":"## Objective\nEnsure existing blobs without identity info work without errors.\n\n## Scope\n- Test the dashboard with existing blob data that lacks identity fields\n- Verify no console errors from undefined personName, assignedColor, identityResolved\n- Check that rendering code handles missing identity data gracefully\n- Confirm backward compatibility is maintained\n\n## Acceptance Criteria\n- [ ] Dashboard loads with existing blob data without errors\n- [ ] No console errors related to undefined identity fields\n- [ ] Rendering code checks for undefined values before using identity fields\n- [ ] Both identity-resolved and non-identity-resolved blobs display correctly\n\n## Notes\nVerification task. Run the app and check console for errors.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T04:41:53.534620139Z","updated_at":"2026-07-06T04:41:53.534620139Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-2gmx","depends_on_id":"bf-1wvm","type":"blocks","created_at":"2026-07-06T04:41:56.600702603Z","created_by":"cli","thread_id":""}]} +{"id":"bf-2irz","title":"Verify registry bridge receives simulator positions","description":"Verify that registry_bridge.go actually receives position data from the simulator.\n\nScope:\n- Trace the data flow from simulator to registry_bridge.go\n- Add logging/debugging to verify position reception\n- Confirm positions are reaching the bridge\n\nAcceptance criteria:\n- Data flow from simulator to registry_bridge.go is traced\n- Logging confirms positions are received\n- Reception path is documented","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T05:23:44.753973594Z","updated_at":"2026-07-06T05:37:57.313816406Z","closed_at":"2026-07-06T05:37:57.313816406Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-2irz","depends_on_id":"bf-2r5o","type":"blocks","created_at":"2026-07-06T05:24:11.877031865Z","created_by":"cli","thread_id":""}]} +{"id":"bf-2jyk","title":"Dashboard: fix API path mismatch for crowd flow endpoint","description":"Crowd flow endpoint is served at /api/analytics/flow but plan spec and dashboard expect /api/flow. Fix the route definition in the Go backend or update the dashboard fetch URL to match whichever is canonical. Tracked as bf-3f16 in spaxel bead store but P3 and stale.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-05-31T15:43:40.808655211Z","updated_at":"2026-07-06T04:10:04.861491826Z","closed_at":"2026-07-06T04:10:04.861491826Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-2lfti","title":"Activity timeline (Component 27)","description":"## Goal\nImplement universal event stream timeline that serves as primary navigation for time and space.\n\n## Scope\n- Event types: detections, zone transitions, portal crossings, automation triggers, alerts (fall/anomaly/security), system events (node online/offline, OTA, baseline changes), learning milestones\n- Tap any event → 3D view jumps to that exact moment via time-travel\n- Inline actions per event: thumbs up/down (feedback), 'Why?' (explainability), create automation from event\n- Filters: By person, by zone, by event type, by time range (combinable)\n- Search: Natural language queries like 'kitchen occupied after midnight last week'\n- Scroll up = go back in time. Open dashboard after being away → scroll up to see everything that happened\n\n## Location\ndashboard/static/js/timeline.js (new module)\ninternal/api/events.go (GET /api/events endpoint already exists)\n\n## Acceptance\n- Timeline sidebar in expert mode shows all events in scrollable stream\n- Simple mode: timeline IS the main view as activity feed, with room cards above it\n- Tap event → 3D scene shows state at that moment\n- Search filters events correctly\n- FTS5 index on events table for natural language search","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.262510021Z","updated_at":"2026-05-05T22:00:56.387536287Z","closed_at":"2026-05-05T22:00:56.387536287Z","close_reason":"Component 27 - Activity Timeline already fully implemented.\n\nTimeline sidebar (expert mode): sidebar-timeline.js provides collapsible panel with scrollable events, category checkboxes, person/zone filters, date range selector, text search with fuzzy matching, cursor pagination, virtualization for 1000+ events, WebSocket real-time updates.\n\nSimple mode activity feed: simple.html imports timeline.js as ES6 module; timeline is main view below room cards.\n\nTap-to-time-travel: Both timeline.js and sidebar-timeline.js implement handleSeek() calling SpaxelReplay.jumpToTime() to jump 3D view to event timestamp.\n\nSearch: Fuzzy matching client-side + FTS5 server-side via /api/events?q= parameter.\n\nFTS5 index: events.go creates events_fts virtual table with triggers for automatic full-text indexing.\n\nAll acceptance criteria met.","source_repo":".","compaction_level":0} +{"id":"bf-2nofd","title":"Ambient dashboard mode (Component 31)","description":"## Goal\nDedicated display mode for wall-mounted tablets or always-on screens. Served at /ambient as separate lightweight route.\n\n## Scope\n- Simplified, stylized top-down floor plan — clean lines, soft rounded corners, no UI chrome\n- People appear as softly glowing colored circles (BLE-identified) or neutral dots (unknown), with names\n- Room labels show subtle occupancy: 'Kitchen · Alice' or 'Bedroom · Empty'\n- Smooth, calm animations: dots drift with interpolated positions, no jitter, no snapping\n- No toolbar, no buttons, no panels — just floor plan, people, small status line\n- Time-of-day awareness: morning (bright/cool), day (neutral), evening (warm/amber), night (very dim, minimal)\n- Adaptive behavior: house empty 30+ min → screen goes fully dark, 'All secure' in tiny text\n- Alert event: entire display transitions to alert mode with pulsing red border, large text, action buttons\n- Morning briefing integration: when first person detected, display briefly shows briefing text before fading to ambient\n\n## Implementation\n/ambient route serving lightweight HTML page\nNo Three.js — use Canvas 2D or SVG for minimal resource usage\nWebSocket receives same dashboard feed but only uses blob positions, zone counts, alerts\n<30 MB RAM, <5% CPU on 2018 iPad\n\n## Acceptance\n- Ambient mode runs unattended on wall-mounted tablet for 7+ days\n- Time-of-day palette transitions smoothly\n- Alert mode breaks the calm appropriately\n- Morning briefing displays on first detection\n- Resource usage: <30 MB RAM, <5% CPU","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:11.529140576Z","updated_at":"2026-05-06T08:26:57.944384490Z","closed_at":"2026-05-06T08:26:57.944384490Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-2po1","title":"Provision ESP32 node: set WiFi creds + mothership URL","description":"After bf-4gbf (ESP32 flashed and boots successfully):\n\nThe freshly flashed node will start a captive portal AP named spaxel-XXXX (last 4 of MAC).\n\nSteps:\n1. Connect phone or laptop to the \"spaxel-XXXX\" WiFi AP (no password)\n2. Browser redirects to 192.168.4.1 captive portal\n3. Enter WiFi SSID and password for the home network\n4. Enter mothership URL: spaxel.ardenone.com (port 443, WSS)\n The firmware will derive ws://... internally — enter just the hostname.\n5. Submit — node writes creds to NVS, reboots\n6. Node should now connect to home WiFi and then discover mothership via:\n - mDNS: _spaxel._tcp.local (disabled in prod; mothership is remote)\n - Fallback: ms_ip NVS key → use the IP/hostname provisioned in step 4\n7. Verify node appears in spaxel.ardenone.com fleet dashboard as CONNECTED\n (node sends hello: {type:hello, mac, firmware_version, capabilities})\n\nNote: SPAXEL_MIGRATION_WINDOW_HOURS=168 so the node can connect without a\nprovisioned token for 7 days. No token management needed for first node.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":0,"issue_type":"task","created_at":"2026-06-04T03:15:30.668150150Z","updated_at":"2026-07-03T00:03:28.351750089Z","source_repo":".","compaction_level":0,"dependencies":[{"issue_id":"bf-2po1","depends_on_id":"bf-4gbf","type":"blocks","created_at":"2026-07-03T00:01:33.878949841Z","created_by":"cli","thread_id":""}],"comments":[{"id":36,"issue_id":"bf-2po1","author":"cli","text":"Cannot complete via software agent — this is a hardware-validation task.\n\nREQUIREMENTS A SOFTWARE AGENT LACKS:\n- A physical ESP32-S3 dev board, freshly flashed and booting (prerequisite bf-4gbf).\n- A human's phone/laptop to join the spaxel-XXXX captive-portal AP, type the home WiFi SSID/password + hostname 'spaxel.ardenone.com' into the 192.168.4.1 form, submit, and verify the node shows CONNECTED on the dashboard.\n\nBLOCKED BY PREREQUISITE: bf-4gbf (Re-flash ESP32 via Web Serial) is still Status=pending and itself needs a human + USB-connected board. No flashed node exists yet to provision.\n\nNO CODE TO WRITE: The entire provisioning path is already implemented in firmware/:\n- Captive portal: wifi_start_captive_portal() in firmware/main/wifi.c (AP + DNS hijack to 192.168.4.1 + HTML form with ms_ip field, NVS write at wifi.c:399-400)\n- Serial provisioning JSON: firmware/main/provision.c:163-166 (writes NVS_KEY_MS_IP / NVS_KEY_MS_IP_PROV)\n- Boot fallback to provisioned mothership host: firmware/main/main.c:244-245 (ms_ip_prov)\n- Migration window (SPAXEL_MIGRATION_WINDOW_HOURS=168) allows first connect without a token.\n\nSERVER VERIFIED UP: https://spaxel.ardenone.com/healthz returns HTTP 200 (Cloudflare-fronted). Once a board is flashed + provisioned by a human, the node should appear CONNECTED.\n\nACTION: Left OPEN per task rules (do-not-close-when-incomplete). Needs a human + physical ESP32-S3 board. Same class as bf-4gbf / bf-4eal.","created_at":"2026-07-02T23:18:16.021916641Z"},{"id":37,"issue_id":"bf-2po1","author":"cli","text":"Not completable by an autonomous agent on this box — leaving OPEN per the 'do not close if you cannot complete' rule.\n\nBLOCKERS (physical, not code):\n- Prerequisite bf-4gbf is still PENDING — no physical ESP32 has been flashed yet, so there is no spaxel-XXXX captive-portal AP to join. This bead explicitly runs 'after bf-4gbf (ESP32 flashed and boots successfully)'.\n- No ESP32-S3 attached here: /dev/ttyUSB*, /dev/ttyACM* absent; no USB-serial bridge in lsusb.\n- Steps 1-5 are all physical/interactive: join the AP from a phone/laptop, browse 192.168.4.1, enter real home WiFi SSID+password + the mothership hostname.\n- Step 7 verification needs an authenticated browser: dashboard root returns 307 -> Google OAuth (accounts.google.com) — not headless-accessible.\n\nVERIFIED IN PLACE (nothing to build — the code/infra for this bead already exists and is healthy, re-confirmed 2026-07-02):\n- firmware/main/wifi.c: captive portal HTTP server @192.168.4.1:80, DNS hijack for captive detection, GET / form + POST /save handler, wifi_start_captive_portal().\n- firmware/main/provision.c: parses payload, writes wifi_ssid / wifi_pass / ms_ip / ms_ip_prov to NVS — the exact keys steps 5-6 require.\n- https://spaxel.ardenone.com/healthz -> HTTP 200 (0.11s). Mothership reachable; the ms_ip fallback target is healthy.\n\nNEEDED: a human (jedarden) with a physical ESP32-S3 + a phone. Once bf-4gbf flashes a board, the captive portal appears and steps 1-7 are manual. No token management needed (SPAXEL_MIGRATION_WINDOW_HOURS=168).","created_at":"2026-07-02T23:28:09.354382496Z"},{"id":38,"issue_id":"bf-2po1","author":"cli","text":"BLOCKED — not agent-completable (hardware-in-the-loop). This bead is a physical captive-portal provisioning step that requires: (1) a physical ESP32-S3 board connected via USB — NONE present (no /dev/ttyUSB* or /dev/ttyACM* on this host); (2) a human to join the 'spaxel-XXXX' AP from a phone/laptop and enter home WiFi creds + mothership URL at 192.168.4.1; (3) prerequisite bf-4gbf (Web Serial flash) which is still status=pending, so there is no flashed device to provision at all. The captive-portal CODE already exists (firmware/main/provision.c, wifi.c) — there is no code work in this bead. Same category as the tracked HW-validation beads (bf-4gbf, bf-4eal). Leaving OPEN for a human with the physical board; will be retried. NOT closing.","created_at":"2026-07-02T23:32:37.491825838Z"},{"id":39,"issue_id":"bf-2po1","author":"cli","text":"NOT CLOSEABLE by agent (hardware-only). Cannot provision a physical ESP32 from a server: no phone/laptop to join the spaxel-XXXX AP, no browser for the captive portal, no access to the home WiFi creds, no way to physically verify the node in the dashboard. Same category as bf-4gbf/bf-4eal (need a human + physical board). Prereq bf-4gbf (physical re-flash) is still PENDING/unassigned — no booting board exists to provision yet. SW path verified present: captive portal firmware (wifi.c wifi_start_captive_portal) implements AP spaxel-XXXX + DNS hijack to 192.168.4.1 + form (ssid/password/ms_ip) + NVS write + reboot; matches the described flow. GAP that would block step 7 even for a human: firmware hardcodes ws:// (websocket.c:107) with ms_port default 8080 (spaxel.h:13) and has NO TLS/wss support anywhere, but production endpoint per this bead is spaxel.ardenone.com:443 WSS. Provisioning that host via the captive form yields ws://spaxel.ardenone.com:8080 -> cannot reach a TLS-only :443 endpoint, and the form captures no port. Needs a firmware wss/TLS capability (root CA bundle + scheme/port handling) as its own SW bead before provisioning to a remote WSS mothership can succeed.","created_at":"2026-07-02T23:36:57.679696334Z"},{"id":40,"issue_id":"bf-2po1","author":"cli","text":"BLOCKED — not agent-completable (hardware/human step). bf-2po1 is a physical provisioning task: connect a phone to the ESP32's 'spaxel-XXXX' captive-portal AP (192.168.4.1), type the home WiFi SSID+password, enter mothership host (spaxel.ardenone.com), and verify the real node shows CONNECTED on the live dashboard. None of this is doable from the repo: it needs the physical board, the user's home WiFi credentials, and eyes on the dashboard. Additionally the predecessor bf-4gbf (re-flash ESP32 with the 4MB firmware) is still PENDING/unassigned, so the board is not even in the captive-portal state this bead assumes. Software side is already complete and verified: firmware/main/provision.c writes wifi_ssid/wifi_pass/ms_ip/ms_port to NVS and sets provisioned=1; wifi.c advertises the spaxel-%02X AP; the migration window (SPAXEL_MIGRATION_WINDOW_HOURS=168) allows tokenless connect. No code change needed — leaving open for a human with the hardware.","created_at":"2026-07-02T23:44:58.933800032Z"},{"id":41,"issue_id":"bf-2po1","author":"cli","text":"BLOCKED — not agent-completable (physical provisioning task, no code to change).\n\nThree blockers:\n1. Prerequisite bf-4gbf ('ESP32 flashed and boots successfully') is still PENDING — no node has been flashed, so there is no spaxel-XXXX captive-portal AP to provision against.\n2. No ESP32 hardware on this host (no /dev/ttyUSB* or /dev/ttyACM* devices) — this is a remote Hetzner server with no board attached.\n3. Steps 1-3 are inherently physical + require human-only data: a human must join the spaxel-XXXX WiFi AP, open the 192.168.4.1 captive portal in a browser, and type the HOME WiFi SSID/password. Those credentials are not in the repo and must not be guessed.\n\nNeeds: a human with (a) the ESP32-S3 board physically present, (b) bf-4gbf done first so the node boots into its captive portal, and (c) the home WiFi SSID/password. Leaving OPEN per the auto-retry policy — do not close until a human performs the captive-portal provisioning and the node shows CONNECTED at spaxel.ardenone.com.","created_at":"2026-07-02T23:54:25.178566299Z"},{"id":42,"issue_id":"bf-2po1","author":"cli","text":"NOT CLOSEABLE by agent (still needs human + physical ESP32-S3 + home WiFi creds). Two NEW items vs comments 36-41:\n\n(1) NETWORK CORRECTION — comment 39's 'TLS gap blocks step 7' is FALSE. Probed live: ws://spaxel.ardenone.com:8080/ws/node returns HTTP 101 (WebSocket upgrade succeeds through Cloudflare, which proxies :8080 as plain HTTP alongside :443). So the firmware's hardcoded ws://:8080 path (websocket.c:107, ms_port default 8080) works end-to-end. A provisioned node WILL send hello and show CONNECTED — NO firmware wss/TLS work is a prerequisite for physical provisioning to succeed. (The bead's 'port 443 WSS' wording is misleading; in practice Cloudflare exposes the node-facing endpoint on :8080 plain-WS.) healthz on :8080 returns the real mothership JSON: version 0.1.358, nodes_online 0, db ok.\n\n(2) ACTION to stop the retry loop — this bead was dispatched to agents 6x (comments 36-41), each reaching the same not-completable conclusion because prerequisite bf-4gbf (physically flash the board) is still pending. Added a blocking dependency: 'br dep add bf-2po1 bf-4gbf' (bf-2po1 depends-on bf-4gbf). Verified bf-2po1 is now excluded from 'br ready' while bf-4gbf is open. Once a human completes bf-4gbf (board flashed + booting into the spaxel-XXXX captive portal), bf-2po1 becomes ready and the steps are manual: join AP -> 192.168.4.1 -> enter home WiFi SSID/pass + 'spaxel.ardenone.com' -> verify CONNECTED. Left OPEN per do-not-close rule.","created_at":"2026-07-03T00:02:06.795374128Z"}]} +{"id":"bf-2povs","title":"CSI simulator (spaxel-sim CLI)","description":"## Goal\nGo CLI tool that opens WebSocket connections as virtual nodes and sends synthetic CSI binary frames for development/testing without hardware.\n\n## CLI Interface\nspaxel-sim --mothership ws://localhost:8080/ws/node --token --nodes 4 --walkers 1 --rate 20 --duration 60s --ble --seed 42 --space '6x5x2.5'\n\n## Synthetic CSI Generation\n- Each virtual node has fixed position (corners, evenly distributed)\n- Each walker follows random walk: Gaussian velocity updates (σ=0.3 m/s per axis per 50ms), reflected at walls\n- For each TX→RX link pair at each tick: compute amplitude and phase using propagation model (path-loss + wall penetration + reflection)\n- Inject Gaussian noise: amplitude_noisy[k] = amplitude × (1 + N(0, 0.05))\n- Serialize into 24-byte binary frame format with n_sub=64\n- rssi = clamp(-30 - path_loss_dB, -90, -30), noise_floor = -95\n\n## Location\ncmd/sim/main.go (new package)\n\n## Acceptance\n- Simulator exits non-zero if it receives {type:'reject'} from mothership\n- Prints per-second frame counts and blob count (from GET /api/blobs poll)\n- Integration test: run simulator for 30s, assert blob count > 0\n- --ble flag also sends simulated BLE advertisements every 5s","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.376407159Z","updated_at":"2026-05-06T04:27:27.359958280Z","closed_at":"2026-05-06T04:27:27.359958280Z","close_reason":"Implemented CSI simulator CLI tool (spaxel-sim). The simulator connects to a running mothership via WebSocket and streams synthetic CSI data for testing without hardware.\n\nKey features:\n- CLI flags: --mothership, --token, --nodes, --walkers, --rate, --duration, --ble, --seed, --space\n- Synthetic CSI generation using Fresnel zone propagation model\n- Virtual nodes at fixed positions (corners, evenly distributed)\n- Walker random walk with Gaussian velocity updates (σ=0.3 m/s per axis per 50ms)\n- Wall reflection at space boundaries\n- 24-byte binary frame format with n_sub=64\n- RSSI clamped to [-90, -30] dBm, noise_floor=-95\n- Exits non-zero on {type:'reject'} from mothership\n- Prints per-second frame counts and blob count (from GET /api/blobs poll)\n- --ble flag sends simulated BLE advertisements every 5s\n\nBinary built and copied to mothership directory for integration tests.\nAll tests pass (go test ./... && go vet ./...).","source_repo":".","compaction_level":0} +{"id":"bf-2r5o","title":"Explore simulator position data structure","description":"Understand what position data the simulator provides and how it's structured.\n\nScope:\n- Examine simulator output to identify position data fields\n- Document the current position data structure\n- Identify where position data originates in the simulator\n\nAcceptance criteria:\n- Position data structure is documented\n- Source of position data in simulator is identified\n- Field names and types are known","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T05:23:39.881193941Z","updated_at":"2026-07-06T05:35:17.257507535Z","closed_at":"2026-07-06T05:35:17.257507535Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"]} +{"id":"bf-2xhp","title":"axe-core accessibility CI gate: must pass before release","description":"Plan Quality Gates item 8 requires an axe-core accessibility check to pass before shipping. No bead (open or closed) covers this. Integrate axe-core into the Playwright E2E suite to scan the dashboard and onboarding UI for WCAG 2.1 AA violations. Wire as a CI step that blocks release.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-26T22:14:42.785566050Z","updated_at":"2026-07-06T12:46:52.964790297Z","source_repo":".","compaction_level":0,"labels":["umbrella"],"dependencies":[{"issue_id":"bf-2xhp","depends_on_id":"bf-6aif","type":"blocks","created_at":"2026-07-06T03:36:48.718374508Z","created_by":"cli","thread_id":""}]} +{"id":"bf-2xykf","title":"Time-travel debugging with parameter tuning","description":"## Goal\nImplement pause live view, scrub timeline, replay 3D scene from recorded CSI with parameter tuning overlay.\n\n## Scope\n- Dashboard toolbar: 'Pause Live' button freezes 3D view and reveals timeline scrubber\n- Scrub backward/forward through recorded history (1×, 2×, 5×, or frame-by-frame)\n- 3D scene renders blobs exactly as they were detected at scrubbed time, including trails\n- Parameter tuning overlay: sliders for detection threshold, baseline time constant, Fresnel weight decay, subcarrier selection count, breathing sensitivity\n- Adjusting slider re-runs pipeline on recorded CSI with new parameters\n- 'Apply to Live' button writes tuned parameters to running pipeline\n\n## Location\ndashboard/static/js/timetravel.js (new module)\ninternal/replay/ (package already exists)\n\n## Acceptance\n- 'Pause Live' calls POST /api/replay/start with from=now-60s\n- Scrubber seeks to timestamp via POST /api/replay/seek\n- 3D view shows replay frames with replay:true flag\n- Parameter slider changes trigger PATCH /api/replay/params\n- 3D view immediately shows how detection would have differed\n- 'Apply to Live' calls POST /api/replay/apply-params","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.329902167Z","updated_at":"2026-05-05T22:34:13.436161012Z","closed_at":"2026-05-05T22:34:13.436161012Z","close_reason":"Implemented time-travel debugging with parameter tuning:\n- Pause Live button freezes 3D view and reveals timeline scrubber\n- Scrub backward/forward through recorded history (1x, 2x, 5x)\n- 3D scene renders blobs exactly as detected at scrubbed time with trails\n- Parameter tuning overlay with sliders for all detection parameters\n- Adjusting slider re-runs pipeline on recorded CSI with new parameters\n- Apply to Live button writes tuned parameters to running pipeline","source_repo":".","compaction_level":0} +{"id":"bf-2y6u","title":"Component 5: Self-healing fleet — before/after coverage overlay in dashboard","description":"The fleet manager self-healing logic (fleet/manager.go selfHeal()) correctly re-optimizes roles and pushes new config to nodes when a node goes offline. However the dashboard visualization described in plan.md Component 12 is partially missing.\n\nWhat the plan requires:\n- Dashboard shows a before/after coverage comparison overlay when self-healing fires: Node kitchen-ceiling went offline. Coverage in kitchen corridor degraded from excellent to fair. 4 links lost, 8 remaining.\n- When node reconnects: Node kitchen-ceiling back online. Full coverage restored.\n- Affected areas highlighted in red in the 3D view (not just the node changing to red status dot)\n\nWhat exists:\n- Node status color change (green/yellow/red) on disconnect\n- selfHeal() re-optimizes roles and pushes config\n- Timeline events for node online/offline\n\nWhat is missing:\n- Before/after GDOP comparison when self-heal fires: compute coverage with N nodes vs N-1 nodes, diff\n- Overlay in 3D view highlighting zones with degraded coverage (ground plane zones turn amber/red)\n- WebSocket message type for coverage-degradation event with affected-zones payload\n- Frontend handling in viz3d.js to show the coverage degradation overlay and dismiss after recovery\n\nThis is distinct from the GDOP overlay in setup mode — it fires automatically on node loss, not on manual placement.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-05-26T04:04:17.405979993Z","updated_at":"2026-07-06T06:29:20.716432012Z","closed_at":"2026-07-06T06:29:20.716432012Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-31bp","title":"Firmware host-based unit tests: nvs/csi/serial_prov via idf.py test --target linux","description":"Testing Strategy requires 3 firmware modules tested via idf.py test --target linux (no hardware needed): (1) nvs schema migration test, (2) csi binary frame serialization roundtrip, (3) serial_prov JSON parser fuzz. No bead (open or closed) covers this. Implement the host test targets in the ESP-IDF component CMakeLists and wire them into CI.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-05-26T22:14:42.778447327Z","updated_at":"2026-07-03T17:30:32.207344691Z","closed_at":"2026-07-03T17:30:32.207344691Z","close_reason":"Completed","source_repo":".","compaction_level":0,"comments":[{"id":43,"issue_id":"bf-31bp","author":"cli","text":"Host-test intent satisfied by the committed gcc harness under firmware/test/ (build-out of decision spike bf-21t), NOT idf.py test --target linux. The idf.py --target linux mechanism named in this bead was explicitly REJECTED (decision record docs/notes/firmware-host-test-approach.md): firmware/main builds as ONE idf_component_register whose REQUIRES names esp_wifi/bt/driver -- three components with no host build -- so the whole component is unhostable; even nvs_migration.c (hostable in isolation) is blocked because main is one component. Building the idf.py path would require risky #ifdef-guarding of production firmware and create a second test mechanism (the decision record warns against exactly this). The gcc harness delivers exactly what the Testing Strategy requires, hardware-free: (1) nvs schema migration test_nvs_migration.c (fresh-install v1, no-downgrade guard, forward-migration dispatch, v1->v2 rename+default -- 8 tests); (2) csi binary frame serialization roundtrip test_csi_frame.c (24-byte header roundtrip, little-endian timestamp, signed-RSSI reinterpretation, I/Q payload, n_sub=0 probe, ingestion-side validation -- 7 tests); (3) serial_prov JSON parser fuzz test_serial_prov.c (bounded recursive-descent decoder fuzzed across random byte streams + deep-nesting cap, valid/invalid/minimal payloads -- 12 tests incl. fuzz passes). Result: 29 passed, 0 failed of 29. CI gate: Dockerfile 'RUN make -C test test' (plain gcc, no IDF toolchain; runs before the ESP-IDF build, propagates non-zero exit). Cleanup this bead: gitignored firmware/test_apps/ -- leftover CMake/ninja build output from an abandoned idf.py linux experiment (no source files; root-owned Docker artifacts so left in place, but now gitignored so it cannot be committed). Verification: make -C firmware/test test -> 29/29; cd mothership && go test ./... -> PASS; go vet ./... -> clean. Pushed 4b0eaba..fbb86fb.","created_at":"2026-07-03T17:29:35.954043239Z"}]} +{"id":"bf-33za","title":"Component 33: Interactive Onboarding — Teach by Doing (5-step CSI physics wizard)","description":"The existing onboard.js has a basic 3-phase calibration wizard (walk/still/walk_through) but is missing the full Component 33 spec from plan.md.\n\nWhat exists: 3-phase calibration (walk 30s, still 10s, walk-through 15s), live CSI waveform canvas, post-calibration card.\n\nWhat is missing — 5 teaching steps with specific narration:\n\nStep 1 (walk 30s): Copy reads I am listening to your WiFi signal. Walk across the room / See that? Your body just changed the WiFi signal — that is how I detect you. Amplitude spikes highlighted in real-time.\n\nStep 2 (still 10s): A green baseline line fades in on chart. Copy reads This is your rooms baseline — the signal when nothing is moving. Any change from this means someone is here. Baseline captured automatically here, not via separate calibrate trigger.\n\nStep 3 (walk through detection zone 15s): Fresnel zone ellipsoid between node and router lights up as translucent green in 3D scene. Copy reads Walk between your node and the router — through the green zone / That is the Fresnel zone — I am most sensitive along this path. Zone pulses brighter as user crosses.\n\nStep 4 (let me find you 15s): Copy reads Walk somewhere and stop. I will try to locate you. Humanoid figure appears at estimated position with dotted accuracy radius. Copy reads Found you! I estimate you are about here. My accuracy is plus-or-minus 1 meter with this setup.\n\nStep 5 (place your node — interactive): Coverage painting activates on ground plane inside wizard. Copy reads Now drag your node to where it actually is in the room. Watch the green coverage change. After placement shows coverage score percentage. Copy reads Nice! Your coverage score is N percent.\n\nKey constraint: zero jargon — CSI, Fresnel, deltaRMS must never appear in wizard text.\n\nImplementation: Steps 3-4 require viz3d.js integration; step 5 requires placement.js GDOP overlay inside wizard; step 4 polls GET /api/blobs for humanoid placement.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-05-26T04:03:50.425961066Z","updated_at":"2026-07-06T03:01:11.271309877Z","closed_at":"2026-07-06T03:01:11.271309877Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-3a2py","title":"Acceptance scenario integration tests (AS-1 through AS-6)","description":"## Goal\nImplement the 6 acceptance scenarios from plan §Acceptance Scenarios as verifiable integration tests in test/acceptance/.\n\n## Scenarios to implement\n\nAS-1: First-time setup in under 5 minutes\n - Start fresh mothership container, open /api/auth/setup, set PIN\n - Run spaxel-sim --nodes 1 (simulates provisioned node)\n - Assert: node appears in /api/nodes within 30s\n\nAS-2: Person detected while walking\n - spaxel-sim --nodes 2 --walkers 1 --duration 60s\n - Poll /api/blobs every second\n - Assert: blob count > 0 for >80% of the run\n\nAS-3: Fall alert fires correctly\n - spaxel-sim with a walker that drops Z rapidly (spike downward velocity, then stays at Z<0.5)\n - Assert: events table contains fall_alert within 15s of trigger\n - Assert: webhook endpoint (test HTTP server) receives POST\n\nAS-4: BLE identity resolves to person name\n - Register a BLE device as 'Alice' via POST /api/ble/devices\n - spaxel-sim --ble (sends BLE reports for that address alongside blobs)\n - Assert: /api/blobs returns at least one blob with person='Alice'\n\nAS-5: OTA update succeeds / rollback on bad firmware\n - Already partially covered by existing OTA rollback integration test\n - Extend to verify the VERIFIED badge path (valid firmware + node reconnects with new version)\n\nAS-6: Replay shows recorded history\n - Run 60s of sim data\n - POST /api/replay/start with a 30s window\n - Assert: replay blobs are returned via WebSocket with replay:true flag\n\n## Location\ntest/acceptance/*.go — one file per scenario, parallel to test/integration/\n\n## Acceptance\nAll 6 scenarios pass in the Argo CI workflow using spaxel-sim as the test harness","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"test","assignee":"claude-code-glm-4.7-golf","created_at":"2026-05-02T12:09:24.898852471Z","updated_at":"2026-05-05T11:39:18.097311991Z","closed_at":"2026-05-05T11:39:18.097311991Z","close_reason":"Implemented all 6 acceptance scenarios as verifiable integration tests in test/acceptance/\n\nAS-1: First-time setup in under 5 minutes\n- Fresh mothership container starts successfully\n- PIN setup works via /api/auth/setup\n- spaxel-sim --nodes 1 connects and appears in /api/nodes within 30s\n\nAS-2: Person detected while walking\n- spaxel-sim --nodes 2 --walkers 1 runs for 60 seconds\n- GET /api/blobs returns at least 1 blob during walk\n- Detection ratio > 80% of run duration\n\nAS-3: Fall alert fires correctly\n- spaxel-sim with fall scenario triggers rapid Z descent\n- Fall alert appears in /api/events within 30 seconds\n- Webhook endpoint receives POST with alert payload\n\nAS-4: BLE identity resolves to person name\n- BLE device registered via POST /api/ble/devices\n- spaxel-sim --ble sends BLE advertisements\n- Blob appears with person='Alice' within 15 seconds\n\nAS-5: OTA update succeeds / rollback on bad firmware\n- spaxel-sim --scenario ota simulates successful OTA\n- Node firmware version increments after update\n- Rollback scenario triggers rollback on boot failure\n\nAS-6: Replay shows recorded history\n- 60s of sim data generates CSI buffer\n- POST /api/replay/start creates replay session\n- Replay blobs returned with replay:true flag\n- Seek functionality works within 1 second target\n\nAll tests use spaxel-sim as the test harness for simulating CSI data without hardware.","source_repo":".","compaction_level":0} +{"id":"bf-3aij","title":"Define blob object literal search pattern","description":"## Objective\nDefine the search pattern for identifying blob-shaped object literals.\n\n## Scope\n- Define what constitutes a 'blob-shaped' object literal\n- Identify key characteristics: id, type, data fields\n- Create grep/search patterns for finding these literals\n\n## Acceptance Criteria\n- [ ] Blob pattern definition documented\n- [ ] Search patterns created for grep/ripgrep\n- [ ] Example matches documented\n- [ ] Search patterns saved to notes/bf-26ta-pattern.md","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T06:01:42.216826835Z","updated_at":"2026-07-06T06:06:06.848567110Z","closed_at":"2026-07-06T06:06:06.848567110Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"]} +{"id":"bf-3d55l","title":"Fuzz tests: binary frame parser and JSON protocol","description":"## Goal\nAdd property-based/fuzz tests for the two highest-impact input parsing surfaces, per plan §Testing Strategy → Property-Based / Fuzz Tests.\n\n## Targets\n\n1. FuzzParseBinaryFrame — internal/ingestion/frame_fuzz_test.go\n Seed corpus: valid frame, truncated header, n_sub mismatch, channel=0, n_sub>128\n Property: never panic; drop/parse/error all OK\n\n2. FuzzParseJSONFrame — internal/ingestion/json_fuzz_test.go\n Seed corpus: hello, health, ble, motion_hint, ota_status, unknown type\n Property: never panic; unknown types return typed error\n\n3. Phase sanitization property test — internal/pipeline/phase/phase_property_test.go\n For all valid int8 I/Q pairs, Sanitize output never contains NaN or Inf\n\n## Acceptance\n- go test -fuzz=FuzzParseBinaryFrame ./internal/ingestion/ -fuzztime=60s: no panic found\n- go test -fuzz=FuzzParseJSONFrame ./internal/ingestion/ -fuzztime=60s: no panic found\n- Phase sanitization property test passes for all int8 I/Q corner cases","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"test","assignee":"claude-code-glm-4.7-golf","created_at":"2026-05-02T12:08:47.919183889Z","updated_at":"2026-05-04T10:01:38.755721488Z","closed_at":"2026-05-04T10:01:38.755721488Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-3dip","title":"Add name labels to humanoid meshes in Three.js scene","description":"## Objective\nAdd floating name labels ('Alice is in Kitchen') above each person's humanoid mesh in the 3D scene.\n\n## Scope\n- Choose text rendering approach for Three.js (FontLoader+TextGeometry, SpriteText, HTML overlay, or CSS2DRenderer)\n- Render personName + location name (e.g., 'Alice is in Kitchen')\n- Position labels above humanoid heads (offset Y axis)\n- Ensure labels are readable (font size, contrast, background)\n- Make labels face the camera or remain screen-readable\n\n## Dependencies\nDepends on: bf-3j3s (colored humanoid meshes must exist first)\n\n## Acceptance Criteria\n- [ ] Each identity-resolved person has a name label visible in the scene\n- [ ] Label format: '{personName} is in {locationName}'\n- [ ] Labels are readable (appropriate font size, contrast)\n- [ ] Labels track their target humanoid meshes (move together)\n- [ ] Labels don't clutter the scene (reasonable size/opacity)\n- [ ] No console errors from text rendering\n\n## Notes\nThis is the final visualization piece. When this is complete, the parent bf-4zio requirement is satisfied.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T04:05:29.141912936Z","updated_at":"2026-07-06T04:05:29.141912936Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-3dip","depends_on_id":"bf-3j3s","type":"blocks","created_at":"2026-07-06T04:05:32.603344951Z","created_by":"cli","thread_id":""}]} +{"id":"bf-3f16","title":"API path mismatch: crowd flow at /api/analytics/flow vs plan spec /api/flow","description":"The plan REST API spec (plan.md Analytics section) specifies:\n GET /api/flow?period=24h&person= -> {cells:[{x,y,count,vx,vy,dwell_s}]}\n\nThe implementation serves this at:\n GET /api/analytics/flow\n\nSimilarly:\n Plan: GET /api/localization/weights and POST /api/localization/weights/reset\n Implementation: GET /api/localization/weights (OK) and POST /api/localization/reset (path differs slightly)\n\nResolution options:\n1. Add alias routes at the plan-specified paths (preferred — no breaking change, old paths still work)\n2. Document the deviation in API_IMPLEMENTATION_STATUS.md as an intentional change\n\nCheck and reconcile all API path deviations between plan.md REST API spec and actual route registration in main.go. Update API_IMPLEMENTATION_STATUS.md with any canonical path decisions.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-05-26T04:05:48.909866463Z","updated_at":"2026-07-06T06:41:05.296231176Z","closed_at":"2026-07-06T06:41:05.296231176Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-3f6q","title":"Construct blob-producing fusion engine in non-test code and integrate into live loop","description":"Depends on bf-2fz8 findings. Ensure the confirmed blob-producing fusion engine (expected: mothership/internal/fusion.Engine) is constructed in non-test code and wired into the live 10Hz loop in mothership/cmd/mothership/main.go.\n\nTasks:\n- Construct the engine at startup (fusion.NewEngine with a Config reflecting room geometry: Width/Height/Depth/Origin) if it is currently test-only.\n- Wire it so the live loop's blob source (the pm.GetTrackedBlobs() path at main.go:1867) actually derives blobs from this engine — feed it link motion via Fuse, or connect it as the tracker source, per the bf-2fz8 determination.\n- Expose the engine instance at a scope where startup seeding (next child) can call SetNodePosition on it.\n\nAcceptance criteria:\n- The fusion engine is constructed in non-test (cmd/mothership) code and reachable from the live loop.\n- The engine instance is retained at a scope where startup seeding can call SetNodePosition on it.\n- go build ./... passes.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T19:53:46.394087127Z","updated_at":"2026-07-03T20:34:54.569447625Z","closed_at":"2026-07-03T20:34:54.569447625Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-3f6q","depends_on_id":"bf-2fz8","type":"blocks","created_at":"2026-07-03T19:54:12.936675642Z","created_by":"cli","thread_id":""}],"comments":[{"id":46,"issue_id":"bf-3f6q","author":"cli","text":"Implemented (commit 30a7ea3, pushed).\n\nThe 3D Fresnel engine (internal/fusion.NewEngine) was only ever built in tests (bf-2f8), so pm.trackedBlobs was always nil and every GetTrackedBlobs() reader saw zero blobs.\n\nWiring:\n- fusion.NewEngine constructed at startup with a Config mirroring room geometry (Width/Height/Depth/Origin from the fleet room config). Declared as a function-local retained for the life of main (mirrors selfImprovingLocalizer) so the startup seeding loop and the follow-up node-seeding bead can call SetNodePosition, and the live-loop closure captures it to run Fuse each tick.\n- Node registry seeded from the fleet at startup (without positioned nodes Fuse skips every link).\n- Tracker goroutine now runs fusionEngine.Fuse(gatherFusionLinks(pm)) -> new blobTracker.track(result) -> pm.SetTrackedBlobs(blobs), replacing the dead pm.GetTrackedBlobs() read. blobTracker bridges the engine's stateless peaks to stateful sigproc.TrackedBlob IDs via greedy 1.0m nearest-neighbour association (plan.md Component 4) and derives velocity from the prior position of the same tracked ID.\n\nGates: go build ./... / go vet ./... / go test ./... all pass.\n\nInstance is reachable at function scope for the next child's SetNodePosition seeding.","created_at":"2026-07-03T20:34:05.005839683Z"}]} +{"id":"bf-3fr6","title":"Add shared default-node geometry helper to the simulator package","description":"Child of bf-18yn umbrella. Foundation for giving default spaxel-sim nodes a spread, non-degenerate geometry.\n\nContext: the prior runtime-propagation child (bf-3p6g) wires node PosX/PosY/PosZ into the 3D fusion engine (mothership/cmd/mothership/main.go seeds fusionEngine via fleetReg.GetAllNodes()), and bf-1tsm asserts the result. But every node defaults to (0,0,1) in the DB schema (mothership/internal/db/migrations.go pos_x/pos_y/pos_z), so all nodes are co-located and the Fresnel excess path |P-T|+|P-R|-|T-R| collapses toward 0, producing no blobs (core symptom in bf-4q5w).\n\nScope:\n- Add a reusable helper in mothership/internal/simulator that returns spread-out default positions for N nodes across a zone consistent with roomWidth/roomDepth/roomHeight. Reuse/extend CornerPositions/SuggestedNodes in node.go, or add e.g. DefaultNodePositions(space, count) []Point. The standalone cmd/sim/main.go generateNodePositions already does corner placement; align defaults with it.\n\nAcceptance criteria:\n- Helper returns N distinct, in-bounds positions; no two nodes co-located; positions span the room (min and max differ on X and Y).\n- Unit test asserts pairwise node distance > 0 for all pairs.\n- go build ./... and go test ./mothership/internal/simulator/... pass.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T22:09:38.340857138Z","updated_at":"2026-07-03T22:22:22.975432730Z","closed_at":"2026-07-03T22:22:22.975432730Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"comments":[{"id":54,"issue_id":"bf-3fr6","author":"cli","text":"Added DefaultNodePositions(space, count) to mothership/internal/simulator/node.go — the shared default-geometry source for spread, non-degenerate node seeding. Single node -> room center; two -> diagonal corners; >=3 -> ceil(sqrt(count)) grid fill (guarantees both X and Y span + all-distinct positions, since count>=3 touches >=2 cols and rows). Z alternates low/high bands for height diversity. Aligns with cmd/sim generateNodePositions. Added node_positions_test.go asserting: count/zero/negative handling, in-bounds (incl. offset-origin space), pairwise distance>0 for all pairs, and X/Y room spanning. go build ./... and go test ./internal/simulator/... + full mothership suite + go vet all green.","created_at":"2026-07-03T22:19:02.039634874Z"},{"id":55,"issue_id":"bf-3fr6","author":"cli","text":"Implemented DefaultNodePositions(s *Space, count int) []Point in mothership/internal/simulator/node.go — the shared default-geometry source for node seeding. Mirrors cmd/sim's generateNodePositions: 1 node at room center, 2 on a diagonal, 3+ on an expanding ceil(sqrt(count)) grid that always spans two columns and two rows (both X and Y range across the room) with alternating low/high Z for height diversity. Every grid cell is distinct so no two nodes co-locate, keeping the Fresnel excess path |P-T|+|P-R|-|T-R| non-degenerate (bf-4q5w core symptom). Added table-driven tests in node_positions_test.go covering count bounds, in-bounds across offset spaces, pairwise distance > 0, room spanning on X and Y, and the single-node center case. Verified: go build ./... ok, go vet ./... ok, go test ./... all pass. Work committed in 1f3e39a (already pushed).","created_at":"2026-07-03T22:21:32.218281224Z"}]} +{"id":"bf-3gr58","title":"Simple mode (progressive disclosure)","description":"## Goal\nCard-based mobile-first UI for household members who don't need the full 3D engineering view.\n\n## Scope\n- No 3D scene — replaces with responsive card layout\n- Room cards: one per defined zone, shows occupancy count, person names (if BLE-identified), status color\n- Activity feed: chronological list of events (from timeline), tap to expand\n- Alert banner: fall detection, anomaly alerts, system warnings\n- Quick actions: arm/disarm security mode, trigger re-baseline, silence alerts\n- Sleep summary card: morning card showing last night's sleep data\n- Mobile-first: touch-friendly, no gestures required\n- Switching: toggle button in toolbar, per-user default stored in localStorage\n- Optional: simple mode requires no auth, expert mode requires PIN\n\n## Location\ndashboard/simple.html (new route)\ndashboard/static/js/simple-mode.js (new module)\n\n## Acceptance\n- Non-technical user can check occupancy without training\n- Room cards show current status with color coding\n- Activity feed shows recent events\n- Toggle between simple/expert mode works\n- Mobile-responsive layout","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:11.506623704Z","updated_at":"2026-05-06T07:05:30.077682950Z","closed_at":"2026-05-06T07:05:30.077682950Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-3gw1","title":"Wire fusion engine node positions at mothership startup","description":"Split from bf-4q5w (parent umbrella). The live 10Hz fusion/blob loop in mothership/cmd/mothership/main.go (~line 1864 onward) produces zero blobs because node positions are never seeded into the blob-producing fusion engine.\n\nFindings:\n- Startup DOES seed positions today, but only into selfImprovingLocalizer (the internal/localization 2D engine) at main.go ~1019-1025. It does NOT seed the internal/fusion 3D Fresnel engine (internal/fusion/fusion.go:93 NewEngine; SetNodePosition at :127) that the IO-6 / blob path depends on.\n- internal/fusion.NewEngine appears to be constructed only in tests (no non-test construction found). The implementer must first confirm which engine actually feeds the live blob/detection loop and the IO-6 gate, then ensure that engine is both constructed AND seeded.\n\nScope of THIS child:\n- Determine definitively which fusion engine produces the blobs observed by tests/e2e and the IO-6 hard-gate (walker produces a tracked blob).\n- Ensure that engine is constructed and integrated into the live loop in cmd/mothership/main.go (construct it if currently test-only).\n- At startup, seed every node position into THAT engine from the fleet registry / DB nodes table (PosX/PosY/PosZ), mirroring the existing selfImprovingLocalizer seeding loop, so nodes are NOT all co-located at the DB default of (0,0,1) per internal/db/migrations.go:143-145.\n\nAcceptance criteria:\n- The blob-producing fusion engine is constructed in non-test code and integrated into the live loop.\n- At startup the engine holds a distinct, non-default position for each node (no all-(0,0,1) collapse).\n- A host/unit test or logged assertion confirms engine positions are populated after startup for N nodes.\n- go build ./... and existing tests pass.\nNote: runtime PATCH/registration propagation and sim geometry are separate children.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T19:22:44.942503675Z","updated_at":"2026-07-03T21:16:16.333632281Z","closed_at":"2026-07-03T21:16:16.333632281Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-3gw1","depends_on_id":"bf-1tsm","type":"blocks","created_at":"2026-07-03T19:54:13.010126760Z","created_by":"cli","thread_id":""}],"comments":[{"id":51,"issue_id":"bf-3gw1","author":"cli","text":"Scope already implemented and verified in the committed tree by sibling beads (bf-3f6q/bf-6s3d/bf-1tsm). No new code needed. DETERMINATION (which engine feeds live blob/IO-6 path): internal/fusion.Engine (3D Fresnel), NOT the 2D selfImprovingLocalizer. Constructed in non-test code (main.go:1028), drives the live 10Hz loop (fusionEngine.Fuse at main.go:1947), publishes peaks via pm.SetTrackedBlobs (main.go:1949). The 2D selfImprovingLocalizer.Fuse runs in a separate slower weight-learning loop (main.go:2676), not the blob path. SEEDING: main.go:1054-1062 iterates fleetReg.GetAllNodes() and calls fusionEngine.SetNodePosition(PosX/PosY/PosZ), mirroring selfImprovingLocalizer; reads actual DB columns (no all-(0,0,1) collapse). ASSERTION+TEST: logged startup assertion main.go:1075-1095 (bf-1tsm); TestEngine_SeedNodePositions internal/fusion/fusion_test.go:329 (bf-6s3d) PASS. GATES: go build ./... exit 0; go vet ./... exit 0; go test ./... 50/50 packages pass. No source changes — did not fabricate redundant seeding/assertion.","created_at":"2026-07-03T21:16:07.321863467Z"}]} +{"id":"bf-3h1hk","title":"Playwright e2e acceptance scenarios AS-1 through AS-6","description":"Open bead bf-3a2py tracks acceptance scenario integration tests but they're not implemented. The tests/e2e/run.sh exists as a bash harness skeleton. The plan implies specific acceptance scenarios covering the full happy path: (AS-1) first-node provisioning → presence detection in <30s; (AS-2) multi-node localization accuracy; (AS-3) portal crossing detection; (AS-4) fall detection alert chain; (AS-5) OTA update flow; (AS-6) security mode arm/alert/disarm. These should be Playwright tests against a running mothership+sim stack.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-02T18:26:08.138470605Z","updated_at":"2026-05-02T18:26:38.599373498Z","closed_at":"2026-05-02T18:26:38.599373498Z","close_reason":"Duplicate of existing open bead bf-3a2py which already tracks acceptance scenario integration tests AS-1 through AS-6","source_repo":".","compaction_level":0} +{"id":"bf-3h5kd","title":"golangci-lint config and CI gate for mothership","description":"The open bead bf-w15bj tracks a CI pipeline gate for golangci-lint, but no .golangci.yml exists in the repo root or mothership/ directory. The sim Makefile references golangci-lint but only for cmd/sim. The main mothership codebase (~50+ packages) has no lint config. Needed: a .golangci.yml at mothership/ level with appropriate linter set (errcheck, staticcheck, unused, govet, etc.) and a CI step in the Argo WorkflowTemplate mta-my-way-build (or a separate lint-only workflow).","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-02T18:25:46.332413911Z","updated_at":"2026-05-02T18:26:44.610380579Z","closed_at":"2026-05-02T18:26:44.610380579Z","close_reason":"Duplicate of existing open bead bf-w15bj which already tracks golangci-lint CI gate with full config specification","source_repo":".","compaction_level":0} +{"id":"bf-3ifj","title":"Trace simulator-to-registry position wiring","description":"Verify that position data from virtual node creation flows correctly from the simulator to the registry module.\n\nScope:\n- Trace the wiring from prior children that connects simulator positions to registry\n- Identify any breaks or missing connections in the simulator-to-registry path\n- Document the expected data flow\n\nAcceptance criteria:\n- Complete data flow path documented from simulator to registry\n- Any breaks or missing connections identified\n- List of files involved in the simulator-to-registry pipeline","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T04:56:13.049391510Z","updated_at":"2026-07-06T05:14:22.698532733Z","closed_at":"2026-07-06T05:14:22.698532733Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"]} +{"id":"bf-3j3s","title":"Apply per-person colors to humanoid meshes","description":"## Objective\nApply unique, consistent colors to each person's humanoid mesh based on their assigned color from the BLE identity matching.\n\n## Scope\n- Extend mesh material to use per-person colors (assignedColor field from bf-64h5)\n- Generate or assign colors if not provided (hash-based color generation from name)\n- Ensure colors are visually distinct and readable\n- Add lighting if needed to make colors visible\n\n## Dependencies\nDepends on: bf-1h7h (humanoid meshes must exist first)\n\n## Acceptance Criteria\n- [ ] Each person's humanoid mesh has a unique, consistent color\n- [ ] Alice is always the same color across sessions (consistent assignment)\n- [ ] Colors are visually distinct (no two people with same/similar colors)\n- [ ] Colors are visible in current lighting (not too dark/bright)\n- [ ] Unresolved blobs have default color (e.g., gray)\n\n## Notes\nThis adds color to the humanoid meshes. Name labels come in the next child.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T04:05:17.486156951Z","updated_at":"2026-07-06T04:05:17.486156951Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-3j3s","depends_on_id":"bf-1h7h","type":"blocks","created_at":"2026-07-06T04:05:20.560255186Z","created_by":"cli","thread_id":""}]} +{"id":"bf-3jv1x","title":"Complete portal crossings GET /api/portals/:id/crossings endpoint","description":"Plan's REST API spec defines GET /api/portals/:id/crossings with ?limit and ?before cursor pagination, returning [{timestamp, direction, person, blob_id}]. The portals package and CRUD endpoints exist (internal/api/zones.go handles portals), and portal_crossings is in the SQLite schema, but the crossings query endpoint is not implemented. This is needed for the timeline 'crossing log' quick action and for the portal detail view.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:57.312746596Z","updated_at":"2026-05-05T19:35:15.900705949Z","closed_at":"2026-05-05T19:35:15.900705949Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-3ldj","title":"Compile blob creation findings report","description":"## Objective\nAggregate all blob creation site findings into a comprehensive report.\n\n## Scope\n- Compile findings from constructor search (bf-5kns)\n- Compile findings from literal search (bf-26ta)\n- Compile findings from factory search (bf-67ao)\n- Organize by file and creation pattern\n- Identify files needing updates for blob identity fields\n\n## Acceptance Criteria\n- [ ] All blob creation sites are consolidated into single report\n- [ ] Sites organized by file and creation pattern\n- [ ] Files requiring updates are clearly listed\n- [ ] Report is ready for next bead to consume","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T05:31:45.373244704Z","updated_at":"2026-07-06T14:24:46.238699349Z","closed_at":"2026-07-06T14:24:46.238699349Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-3ldj","depends_on_id":"bf-67ao","type":"blocks","created_at":"2026-07-06T05:32:02.849482138Z","created_by":"cli","thread_id":""}],"comments":[{"id":64,"issue_id":"bf-3ldj","author":"cli","text":"Compiled consolidated report: notes/bf-3ldj-findings.md (commit e3bd5ee, pushed). Aggregates bf-5kns (new Blob constructors), bf-26ta (struct/object literals), bf-67ao (factory functions) into one catalogue organized by file (§2) and by creation pattern (§3). Re-verified every production file:line against HEAD; corrected 2 stale entries (state.js:290 is now {id} only; tracking.Blob identity fields are PersonID/PersonLabel/PersonColor not PersonName/AssignedColor). Separated the 4 unrelated browser new Blob() file-download sites (§4, skip for identity work). Deliverable for next bead (§5): identity lives in tracker.Blob/tracking.Blob/sigproc.TrackedBlob + JS spaxel.d.ts, but is DROPPED at 2 main.go conversion boundaries — :2213 (automation.TrackedBlob has no identity fields) and :2116 (explainability.BlobSnapshot drops person/color). §6 maps the full data flow with the leak points marked. All 4 acceptance criteria met. go vet/test: this is a docs-only change (no .go edits); pre-existing unrelated vet failures in simulator remain as on HEAD.","created_at":"2026-07-06T14:23:38.759407588Z"}]} +{"id":"bf-3p4bj","title":"Command palette (Component 34)","description":"## Goal\nCtrl+K (Cmd+K on Mac) opens universal search and command interface. Invisible to casual users, indispensable for power users.\n\n## Scope\nSearch:\n- 'kitchen' → Kitchen zone, kitchen nodes, kitchen automations, recent kitchen events\n- 'alice' → Alice's current location, today's timeline, sleep report, BLE devices\n- 'node 3' → Node details, diagnostics, link health\n\nNavigate time:\n- 'last night 2am' → timeline jumps there\n- 'yesterday kitchen' → filters timeline to kitchen events yesterday\n- 'this morning' → jumps to first detection today\n\nExecute commands:\n- 'update all nodes', 're-baseline kitchen', 'add node', 'arm security', 'disarm security'\n- 'dark mode'/'light mode', 'export config', 'restart node kitchen-north'\n\nGet help:\n- 'help fall detection', 'why false positive', 'troubleshoot kitchen'\n\nBehavior:\n- Fuzzy matching: 'flr pln' matches 'Floor Plan settings'\n- Recently used commands appear first\n- Results show keyboard shortcut hints where applicable\n- Escape closes, Enter executes top result\n- Works in expert mode only\n\n## Implementation\nFrontend-only component\nCommand registry maps keywords to actions\nSearch runs against: zone names, person names, node names, setting names, help topics\n\n## Acceptance\n- Ctrl+K/Cmd+K opens command palette\n- Search finds zones, people, nodes, settings, help topics\n- Commands execute correctly\n- Time navigation jumps to correct moments\n- Fuzzy matching works\n- Escape closes palette","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:11.571696739Z","updated_at":"2026-05-06T08:49:45.284413081Z","closed_at":"2026-05-06T08:49:45.284413081Z","close_reason":"Component 34: Command Palette fully implemented and tested.\n\n- Ctrl+K/Cmd+K opens universal search and command interface\n- Fuzzy matching across zones, people, nodes, settings, help topics\n- Time navigation: @3am, @-30min, @yesterday 11pm, @this morning, @last night\n- Commands execute: arm/disarm security, dark/light mode, export config, re-baseline, add node, etc.\n- Recently used commands appear first (up to 5)\n- Escape closes, Enter executes top result\n- Works in expert mode only (excluded from simple/ambient modes)\n- All 88 tests pass","source_repo":".","compaction_level":0} +{"id":"bf-3p6g","title":"Propagate runtime node position changes to the fusion engine","description":"Split from bf-4q5w (parent umbrella). Depends on the startup-seeding child.\n\nToday PATCH /api/nodes/:mac updates only the fleet registry: internal/fleet/handler.go updateNodePosition calls h.mgr.GetRegistry().SetNodePosition (~handler.go:391) and BroadcastRegistry, but never forwards the new position to the fusion engine. Likewise node connect/register events do not push positions to the engine, so a node moved at runtime stays at its stale position in the blob engine.\n\nScope:\n- On PATCH node position (handler.go updateNodePosition), after updating the registry, call the blob-producing fusion engine SetNodePosition(mac,x,y,z) for the engine wired in the prior child.\n- On node connect/register, push the node current position to the engine as well.\n- Decide where the engine reference lives so the handler can reach it (e.g. via the registry/fleet manager or a setter injected at startup), matching existing accessor patterns such as SetNodePositionAccessor in internal/diagnostics and internal/fleet/weather.go.\n\nAcceptance criteria:\n- PATCH /api/nodes/:mac and node registration both update the fusion engine position for that MAC.\n- Existing fleet handler tests (internal/fleet/handler_test.go) still pass.\n- A test asserts the position the engine stores for a MAC changes after a PATCH with new coordinates.\n- go build ./... passes.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T19:22:52.558913759Z","updated_at":"2026-07-03T21:35:49.310935619Z","closed_at":"2026-07-03T21:35:49.310935619Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-3p6g","depends_on_id":"bf-3gw1","type":"blocks","created_at":"2026-07-03T19:23:17.331023175Z","created_by":"cli","thread_id":""}],"comments":[{"id":52,"issue_id":"bf-3p6g","author":"cli","text":"Implemented bf-3p6g: runtime node positions now propagate to the fusion engine. Added Manager.nodePositionSink (write-side mirror of the read-side SetNodePositionAccessor used by diagnostics/weather.go), wired to fusionEngine.SetNodePosition in main.go. Fires from both position paths: PATCH /api/nodes/{mac}/position (handler.updateNodePosition -> ForwardNodePosition) and node connect/register (OnNodeConnected forwards the registry position). Commit a17bdd2. Tests: TestHandlerUpdateNodePositionForwardsToEngine (asserts the engine's stored position for a MAC changes after a PATCH with new coords), TestManagerOnNodeConnectedForwardsPositionToEngine (asserts connect seeds the engine position). Existing fleet handler tests still pass; go build/vet/test ./... all green.","created_at":"2026-07-03T21:31:51.895058799Z"},{"id":53,"issue_id":"bf-3p6g","author":"cli","text":"Implemented and verified (commit a17bdd2, pushed to origin/main).\n\nThe fleet Manager gains a nodePositionSink — the write-side mirror of the read-side SetNodePositionAccessor used by diagnostics/weather.go — wired at startup (main.go) to fusionEngine.SetNodePosition. It fires from both position paths:\n- PATCH /api/nodes/{mac}/position: Handler.updateNodePosition calls ForwardNodePosition after the registry accepts the new position.\n- Node connect/register: Manager.OnNodeConnected forwards the node's current registry position.\n\nTests (internal/fleet/handler_test.go):\n- TestHandlerUpdateNodePositionForwardsToEngine: asserts the engine's stored position for a MAC changes after a PATCH with new coordinates (seeds 1.5,2.5,3.5 then updates to 7,8,9).\n- TestManagerOnNodeConnectedForwardsPositionToEngine: asserts connect pushes the registry position to the engine.\n\nVerification: go build ./... OK, go vet ./... OK, go test ./... all pass (existing fleet handler tests still green).","created_at":"2026-07-03T21:34:59.582248981Z"}]} +{"id":"bf-3tlw","title":"Compile blob literal locations documentation","description":"## Objective\nCompile all findings into a comprehensive documentation.\n\n## Scope\n- Merge TypeScript and JavaScript search results\n- Create comprehensive documentation with all locations\n- Note blob structure patterns for each site\n\n## Acceptance Criteria\n- [ ] TypeScript and JavaScript results merged\n- [ ] All blob-shaped object literals documented in single file\n- [ ] Each location includes: file path, line number, blob structure pattern\n- [ ] Final documentation saved to notes/bf-26ta-findings.md","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T06:02:13.129510178Z","updated_at":"2026-07-06T06:20:54.599604029Z","closed_at":"2026-07-06T06:20:54.599604029Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-3tlw","depends_on_id":"bf-4ly4","type":"blocks","created_at":"2026-07-06T06:02:17.268268599Z","created_by":"cli","thread_id":""}]} +{"id":"bf-3ukb","title":"Fix crowd flow API path mismatch: /api/analytics/flow vs plan spec /api/flow","description":"Two open beads (bf-2jyk, bf-3f16) both describe this same bug: the crowd flow analytics endpoint is registered at /api/analytics/flow but the dashboard JS and plan spec reference /api/flow.\n\nFix:\n- Find the crowd flow endpoint registration in mothership/cmd/mothership/main.go\n- Add an alias at /api/flow that delegates to the same handler, OR\n- Update the dashboard JS to use /api/analytics/flow consistently\n- Choose the approach that aligns with the OpenAPI docs\n\nRelated open beads this resolves: bf-2jyk and bf-3f16 (same root cause).","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-06-04T03:16:28.407609539Z","updated_at":"2026-07-06T04:13:12.269404105Z","closed_at":"2026-07-06T04:13:12.269404105Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-3v39","title":"Capture baseline and verify presence detection with first node","description":"After bf-2po1 (node connected and streaming CSI):\n\nIn passive radar mode (single node, using home AP beacons as TX source):\n1. Ensure room is empty, then POST /api/baseline/capture to capture empty-room baseline\n curl -X POST https://spaxel.ardenone.com/api/baseline/capture (auth required)\n2. Wait 30s for baseline to stabilize (EMA with tau=30s)\n3. Walk through the detection area between the node and the home WiFi AP\n4. In the dashboard, observe: deltaRMS should spike from ~0.02 to ~0.10+\n5. A blob should appear in the 3D visualization at the detected position\n\nWith a single node, only presence detection is reliable (not precise localization).\nFor localization, a second node on the opposite side of the space is needed.\n\nSuccess: deltaRMS > 0.05 when person is present, blob visible in dashboard.\n\nTroubleshooting: if no signal change, check passive_bss NVS key has the AP BSSID,\nor switch node to TX_RX mode and use node-to-node CSI instead of passive radar.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":0,"issue_type":"task","assignee":"","created_at":"2026-06-04T03:15:41.581140007Z","updated_at":"2026-07-03T00:13:34.779002015Z","source_repo":".","compaction_level":0,"dependencies":[{"issue_id":"bf-3v39","depends_on_id":"bf-2po1","type":"blocks","created_at":"2026-07-03T00:12:30.706349666Z","created_by":"cli","thread_id":""}]} +{"id":"bf-3y9r","title":"Verify position propagation through registry and fusion engine","description":"Child of bf-18yn (split). Depends on bf-5yff.\n\nEnsure that the realistic node positions flow through the entire pipeline from simulator to registry to fusion engine.\n\nScope:\n- Trace the wiring from prior children that connects simulator positions to registry\n- Verify positions are correctly propagated to internal/simulator/registry_bridge.go\n- Verify positions reach the fusion engine accumulation grid\n- Add any missing glue if positions don't flow through (should be wired from prior children)\n\nAcceptance criteria:\n- Positions from virtual node creation reach the registry correctly\n- Positions from registry reach the fusion engine correctly\n- If wiring was incomplete, add the missing connections\n- Integration test or log output shows positions flowing through all stages","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T04:22:26.661147812Z","updated_at":"2026-07-06T17:40:04.865168935Z","closed_at":"2026-07-06T17:40:04.865168935Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-3y9r","depends_on_id":"bf-5yff","type":"blocks","created_at":"2026-07-06T04:22:30.785332691Z","created_by":"cli","thread_id":""},{"issue_id":"bf-3y9r","depends_on_id":"bf-95tx","type":"blocks","created_at":"2026-07-06T04:56:36.064193893Z","created_by":"cli","thread_id":""}],"comments":[{"id":72,"issue_id":"bf-3y9r","author":"cli","text":"Closed by verification. Position propagation through the full chain (simulator VirtualNodeStore -> registry_bridge -> fleet.Registry SQLite -> fusion.Engine.nodePos mirror -> accumulation grid) was already wired by prior children (bf-5lii -> bf-69ym -> bf-u7ds -> bf-95tx).\n\nThis bead's acceptance criterion -- 'positions reach the fusion engine accumulation grid and actually drive localization' -- is locked in by TestPositionPropagation_ReachesAccumulationGrid in mothership/internal/api/simulator_test.go. It posts two explicitly-positioned nodes via the simulator REST API, runs one Fuse step with a link between the two MACs the engine now holds, and asserts a blob localizes near the link midpoint (confidence 1.0, 1 active link). That peak is only reachable if BOTH endpoints' positions reached the grid lookup (AddLinkInfluence reads nodePos[mac] for both endpoints; a missing/co-located position yields no peak).\n\nVerified: go test ./... PASS (internal/api incl.), go vet ./... clean. Grid trace under -v:\n [grid] position propagation midpoint=(3.00,1.00) peak=(2.90,0.90,0.90) conf=1.000 activeLinks=1\n\nNo production wiring change needed -- prior children completed it. Committed and pushed to origin/main.","created_at":"2026-07-06T17:32:00.336376744Z"},{"id":73,"issue_id":"bf-3y9r","author":"cli","text":"Verified position propagation through simulator → registry → fusion engine. The wiring from prior children is complete and correct: a posted node position flows VirtualNodeStore → fleetRegistryBridge.SyncToRegistry (SQLite write) → fleetRegistryAdapter forwardPos hook → fleetMgr.ForwardNodePosition → nodePositionSink → fusion.Engine.SetNodePosition, and Fuse reads nodePos[mac] for both endpoints into AddLinkInfluence on the accumulation grid. No production change was needed. Lock-in: TestPositionPropagation_ReachesAccumulationGrid posts two explicitly-positioned nodes through the simulator REST API and asserts one Fuse step localizes a blob near the link midpoint (peak=(2.90,0.90,0.90), conf=1.000, activeLinks=1) — only reachable if BOTH endpoints' positions reached the grid lookup (a missing/co-located position yields no peak, cf. TestEngine_CoLocatedOriginYieldsNoPeaks). go vet ./... clean; go test ./... fully green.","created_at":"2026-07-06T17:39:55.812604919Z"}]} +{"id":"bf-49fp","title":"Dockerfile: bust Kaniko cache for firmware stage to force 4MB rebuild","description":"The 0.1.352 Docker image contains firmware compiled with CONFIG_ESPTOOLPY_FLASHSIZE_16MB (16MB header). The fix was already committed in d837598 (sdkconfig.defaults -> 4MB, partitions.csv redesigned for 4MB). But Kaniko served a cached firmware layer for the 0.1.352 build, ignoring the sdkconfig.defaults change.\n\nSymptom: ESP32-S3 (4MB flash) flashed via Web Serial crashes on every boot with:\n spi_flash: Detected size(4096k) smaller than binary image header(16384k). Probe failed.\n assert failed: do_core_init startup.c:326\n\nFix:\n1. In firmware stage of Dockerfile, add a cache-busting ARG before the COPY:\n ARG FIRMWARE_CACHE_BUST=2026-06-03\n This guarantees Kaniko treats the layer as new on the next build.\n\n2. Also add RUN rm -f sdkconfig sdkconfig.old after COPY and before idf.py set-target\n so any stale tracked sdkconfig cannot survive into the build.\n\nAfter this change, idf.py set-target will regenerate sdkconfig from sdkconfig.defaults\nwhich has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y, producing firmware with a 4MB header.\n\nReference: firmware/sdkconfig.defaults (CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y)\n firmware/partitions.csv (4MB layout: factory+ota_0 fit in 4MB)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":0,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-06-04T03:13:10.895986057Z","updated_at":"2026-07-02T18:50:13.296108997Z","closed_at":"2026-07-02T18:50:13.296108997Z","close_reason":"Fix was already committed in d837598/301a588 (Kaniko cache-bust ARG + sdkconfig cleanup in the firmware Dockerfile stage) weeks ago. Bead was stuck retrying due to a dead ANTHROPIC_BASE_URL in the claude-code-glm-5 adapter (fixed 2026-07-02); the agent correctly verified the fix was in place and closed this itself, but a missing verify-changes.sh hook on lab (also fixed 2026-07-02) reopened it every time. No further action needed.","source_repo":".","compaction_level":0,"comments":[{"id":1,"issue_id":"bf-49fp","author":"cli","text":"Already implemented in commit 301a588 (pushed to origin/main). Firmware stage of Dockerfile now contains both required changes: (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 before 'COPY firmware/ ./' to guarantee a Kaniko cache miss on the next build; (2) RUN rm -f sdkconfig sdkconfig.old after COPY and before 'idf.py set-target' so sdkconfig is regenerated from sdkconfig.defaults (CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y) every build. Verified: firmware/sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y, firmware/partitions.csv is a 4MB layout (factory+ota_0 fit), VERSION already at 0.1.357 (past the 0.1.354 bump that accompanied this fix), so a fresh CI build producing the 4MB-header firmware has already run. Resolves the ESP32-S3 boot crash (spi_flash detected 4096k < binary header 16384k).","created_at":"2026-07-02T17:19:36.504115277Z"},{"id":2,"issue_id":"bf-49fp","author":"cli","text":"Already implemented and shipped in commit 301a588 ('fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop'), on origin/main (live since v0.1.354). Verified on re-dispatch: (1) Dockerfile:31 has ARG FIRMWARE_CACHE_BUST=2026-06-03 before the firmware COPY; (2) Dockerfile:38 has 'RUN rm -f sdkconfig sdkconfig.old' after COPY and before 'idf.py set-target'. Confirmed firmware/sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y. No new commit needed - working tree had no code changes.","created_at":"2026-07-02T17:30:00.661481731Z"},{"id":3,"issue_id":"bf-49fp","author":"cli","text":"Resolved: this fix was already implemented in commit 301a588 (ancestor of HEAD, pushed to main). Verified both required lines present in current Dockerfile matching spec exactly: line 31 ARG FIRMWARE_CACHE_BUST=2026-06-03 before COPY firmware/ (line 34); line 38 RUN rm -f sdkconfig sdkconfig.old after COPY and before idf.py set-target (line 44). Dockerfile working tree clean (git diff empty) — nothing new to commit/push. go vet ./... clean, go test ./... all pass. Fix shipped in 0.1.354+ (now at 0.1.357); the 0.1.352 16MB-header crash loop is resolved. Did not bump cache-bust value: task explicitly specifies 2026-06-03 and the one-time cache invalidation already served its purpose.","created_at":"2026-07-02T17:37:28.212905891Z"},{"id":4,"issue_id":"bf-49fp","author":"cli","text":"Verified complete — both requested Dockerfile changes are already present in the firmware stage and were committed in 301a588 (pushed to origin/main). (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 before 'COPY firmware/ ./' at Dockerfile:31 busts Kaniko's firmware-layer cache. (2) 'RUN rm -f sdkconfig sdkconfig.old' after COPY and before 'idf.py set-target' at Dockerfile:38 removes stale generated sdkconfig so it regenerates from sdkconfig.defaults. Supporting state verified: sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y; partitions.csv is the 4MB layout (factory 0x10000 + ota_0 0x200000, each 0x1F0000); no stale generated sdkconfig tracked. No code change needed this session; go vet + go test ./... both pass (exit 0). Build/deploy/flash tracked in follow-up bead bf-15hy.","created_at":"2026-07-02T17:42:52.975800256Z"},{"id":5,"issue_id":"bf-49fp","author":"cli","text":"Already implemented in commit 301a588 ('fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop'), in HEAD and pushed to origin/main. Verified: (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 added in firmware stage BEFORE the COPY at Dockerfile:31 — busts Kaniko layer cache; (2) RUN rm -f sdkconfig sdkconfig.old added AFTER COPY and BEFORE idf.py set-target at Dockerfile:38 — clears stale tracked sdkconfig so set-target regenerates from sdkconfig.defaults. Confirmed sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y and partitions.csv is the 4MB layout (factory 0x1F0000 + ota_0 0x1F0000 fits 4MB). Working tree clean; no further changes needed.","created_at":"2026-07-02T17:46:59.138102056Z"},{"id":6,"issue_id":"bf-49fp","author":"cli","text":"Already implemented and committed in 301a588 (in branch history, pushed to origin). Dockerfile firmware stage has ARG FIRMWARE_CACHE_BUST=2026-06-03 before COPY firmware/ (line 31) and RUN rm -f sdkconfig sdkconfig.old after COPY and before idf.py set-target (line 38). Forces Kaniko cache miss + regenerates sdkconfig from sdkconfig.defaults (CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y), producing a 4MB-header firmware. Verified: go vet ./... clean, go test ./... all pass. No new commit needed - working tree Dockerfile is clean.","created_at":"2026-07-02T17:50:11.573756430Z"},{"id":7,"issue_id":"bf-49fp","author":"cli","text":"Implementation already present in commit 301a588 (fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop), verified in current Dockerfile on main: (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 before COPY firmware/ (Dockerfile:31); (2) RUN rm -f sdkconfig sdkconfig.old after COPY and before idf.py set-target (Dockerfile:38). Coherent with firmware/sdkconfig.defaults (CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y) and firmware/partitions.csv (4MB layout). No new code changes were needed — working tree held only dispatcher metadata and build artifacts.","created_at":"2026-07-02T17:55:44.367732542Z"},{"id":8,"issue_id":"bf-49fp","author":"cli","text":"Closed as already-complete: commit 301a588 (fix(firmware): bust Kaniko cache + force sdkconfig regen) already added both requested Dockerfile firmware-stage changes — ARG FIRMWARE_CACHE_BUST=2026-06-03 before the COPY (line 31), and RUN rm -f sdkconfig sdkconfig.old after COPY / before idf.py set-target (line 38). That commit is on origin/main; sdkconfig.defaults carries CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y, partitions.csv is the 4MB layout (factory+ota_0 fit), and VERSION has since advanced to 0.1.357 so the 4MB-header fix is baked into released images. No new commit was needed.","created_at":"2026-07-02T18:03:06.315827627Z"},{"id":9,"issue_id":"bf-49fp","author":"cli","text":"Already implemented and pushed. The exact two changes this bead requests landed in commit 301a588 ('fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop'), which is on origin/main and an ancestor of HEAD. Verified in current Dockerfile: (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 before 'COPY firmware/ ./' at line 31; (2) RUN rm -f sdkconfig sdkconfig.old after COPY and before 'idf.py set-target esp32s3' at line 38. Also confirmed firmware/sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y and firmware/partitions.csv is the 4MB layout. No working-tree changes were needed.","created_at":"2026-07-02T18:06:58.975466905Z"},{"id":10,"issue_id":"bf-49fp","author":"cli","text":"Fix already implemented and pushed (commit 301a588, present in origin/main and github/main). Verified the two required Dockerfile changes are in place in the firmware-builder stage, correctly ordered: line 31 ARG FIRMWARE_CACHE_BUST=2026-06-03 (cache-bust ARG before COPY), line 34 COPY firmware/ ./, line 38 RUN rm -f sdkconfig sdkconfig.old (stale sdkconfig removal after COPY, before set-target), line 44 idf.py set-target esp32s3. This guarantees Kaniko treats the firmware layer as new and forces set-target to regenerate sdkconfig from sdkconfig.defaults with CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y, producing firmware with a 4MB header that boots on 4MB-flash ESP32-S3 devices (fixes the spi_flash binary image header 16384k crash loop). No new commit needed; this task is a retry/duplicate of already-shipped work.","created_at":"2026-07-02T18:15:37.119821897Z"},{"id":11,"issue_id":"bf-49fp","author":"cli","text":"Already implemented in commit 301a588 (in main, pushed to origin/main). The Dockerfile firmware stage contains both required changes: (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 before COPY firmware/ (line 31) to bust Kaniko's cached 16MB firmware layer; (2) RUN rm -f sdkconfig sdkconfig.old (line 38) after COPY and before idf.py set-target, so set-target regenerates sdkconfig from sdkconfig.defaults. Verified firmware/sdkconfig.defaults sets CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y and partitions.csv uses a 4MB layout (factory+ota_0 fit in 4MB). No code changes needed - Dockerfile working tree is clean.","created_at":"2026-07-02T18:31:31.018683964Z"},{"id":12,"issue_id":"bf-49fp","author":"cli","text":"Fix already implemented in commit 301a588 (fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop), dated 2026-06-03. Verified present and correct in current Dockerfile: (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 at line 31, immediately before 'COPY firmware/ ./'; (2) RUN rm -f sdkconfig sdkconfig.old at line 38, after COPY and before 'idf.py set-target' at line 44. Reference files confirmed coherent: firmware/sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y; firmware/partitions.csv is the 4MB layout (factory 0x10000 + ota_0 0x200000 + otadata 0x3F0000, all within 4MB). No new changes required — already committed and pushed (ancestor of HEAD). No Go code touched, so go test/vet not applicable to this Dockerfile-only fix.","created_at":"2026-07-02T18:37:55.559623529Z"},{"id":13,"issue_id":"bf-49fp","author":"cli","text":"Already implemented and shipped — no change needed.\n\nBoth requested Dockerfile firmware-stage changes are present:\n - Line 31: ARG FIRMWARE_CACHE_BUST=2026-06-03 (before the COPY)\n - Line 38: RUN rm -f sdkconfig sdkconfig.old (after COPY, before idf.py set-target)\n\nIntroduced in commit 301a588 ('fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop'), already on origin/main (verified via git branch -r --contains 301a588). Dockerfile working tree matches HEAD, no uncommitted changes.\n\nEffect: the ARG forces Kaniko to treat the firmware layer as new; the rm ensures idf.py set-target regenerates sdkconfig from sdkconfig.defaults (CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y), yielding a 4MB header — fixing the spi_flash 'binary image header(16384k)' boot crash on 4MB-flash ESP32-S3. Closing as already-complete.","created_at":"2026-07-02T18:45:49.875764400Z"}]} +{"id":"bf-4b1c","title":"Add test coverage for geometry placement and fusion peaks","description":"Child of bf-18yn (split). Depends on bf-3y9r.\n\nWrite tests that verify the geometry placement produces meaningful fusion results.\n\nScope:\n- Add a sim or host test that creates virtual nodes with default placement\n- Verify the fusion accumulation grid produces non-zero peaks (not all zeros)\n- Ensure test would fail with old co-located placement but passes with new geometry\n- Run existing simulator tests to ensure no regressions\n\nAcceptance criteria:\n- New test passes with realistic geometry\n- Test verifies fusion accumulation grid has non-zero peaks\n- Existing simulator tests still pass\n- go build ./... passes","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T04:22:35.225244286Z","updated_at":"2026-07-06T17:54:27.194709041Z","closed_at":"2026-07-06T17:54:27.194709041Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-4b1c","depends_on_id":"bf-3y9r","type":"blocks","created_at":"2026-07-06T04:22:39.781625261Z","created_by":"cli","thread_id":""}],"comments":[{"id":74,"issue_id":"bf-4b1c","author":"cli","text":"Implemented (commit c4d42e8, pushed to main). Added TestEngine_GeometryPlacementDrivesFusionPeaks to mothership/internal/fusion/fusion_test.go — a differential lock-in that holds node placement as the SOLE variable (engine grid, all-pairs motion link set, and room identical across both legs) and proves the non-zero-peak condition flips with geometry: default (spread) placement accumulates a non-zero fusion grid (gridMax>0; blobs>=1 for count>=4 where crossing links form a true maximum), while the old co-located (0,0,1) collapse makes every link degenerate (<0.1m), so AddLinkInfluence early-returns and the grid stays at zero (gridMax==0, 0 blobs). The trace shows the flip: spread gridMax=1.0 blobs=2 vs colocated gridMax=0.0 blobs=0. Gates: go test ./... , go vet ./... , and go build ./... (mothership + cmd/sim + test/acceptance) all pass.","created_at":"2026-07-06T17:50:18.895413358Z"},{"id":75,"issue_id":"bf-4b1c","author":"cli","text":"Verified and closed (re-dispatch after prior run failed on 'br close --body' which is unsupported). Implementation is commit c4d42e8 on origin/main — TestEngine_GeometryPlacementDrivesFusionPeaks in mothership/internal/fusion/fusion_test.go.\n\nAll bf-4b1c acceptance criteria verified green:\n- go build ./... clean across all 3 modules (mothership, cmd/sim, test/acceptance)\n- go vet ./... clean across all 3 modules\n- go test ./... — all packages pass (mothership incl. internal/fusion + internal/simulator, test/acceptance ok)\n- New test passes with realistic geometry: spread placement accumulates a non-zero grid (gridMax=1.0) and count>=4 crossing links extract >=1 blob (nodes=4 -> blobs=2, conf=0.768)\n- Differential lock-in holds placement as the SOLE variable (identical engine grid, all-pairs motion link set, room): spread gridMax=1.0 blobs=2 vs old co-located (0,0,1) collapse gridMax=0.0 blobs=0 — the non-zero-peak condition flips with geometry, proving the test is genuinely regression-sensitive to the bf-4q5w collapse.","created_at":"2026-07-06T17:53:23.492885933Z"}]} +{"id":"bf-4bhd","title":"Find all blob creation code paths","description":"## Objective\nIdentify all locations in the codebase where blob objects are created or constructed.\n\n## Scope\n- Search for blob creation patterns (new Blob, object literals with blob shape, blob factories)\n- Document all blob creation sites\n- Identify which files need updates\n\n## Acceptance Criteria\n- [ ] All blob creation sites are identified and listed\n- [ ] Each site is documented with file path and line number\n- [ ] Creation pattern is noted (constructor, literal, factory, etc.)\n- [ ] Report is ready for the next bead to use","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T05:11:28.984747608Z","updated_at":"2026-07-06T20:14:35.102620391Z","source_repo":".","compaction_level":0,"labels":["deferred","umbrella"],"dependencies":[{"issue_id":"bf-4bhd","depends_on_id":"bf-3ldj","type":"blocks","created_at":"2026-07-06T05:32:05.819292341Z","created_by":"cli","thread_id":""},{"issue_id":"bf-4bhd","depends_on_id":"bf-1q3m","type":"blocks","created_at":"2026-07-06T15:04:21.606416708Z","created_by":"cli","thread_id":""},{"issue_id":"bf-4bhd","depends_on_id":"bf-5cgc","type":"blocks","created_at":"2026-07-06T20:13:54.885895615Z","created_by":"cli","thread_id":""}],"comments":[{"id":90,"issue_id":"bf-4bhd","author":"cli","text":"bf-5cgc handoff complete → notes/bf-5cgc-handoff.md. Your acceptance criterion 'Report is ready for the next bead to use' is now satisfied: the re-verified inventory lives in notes/bf-1q3m-consolidated.md (this file supersedes notes/bf-4bhd.md per bf-1q3m §4.1), and the site→bead handoff is formalized in notes/bf-5cgc-handoff.md with concrete owners (bf-5151 primary) + the GetMatch population pattern. bf-4bhd may be closed on this basis.","created_at":"2026-07-06T20:39:00.020455831Z"}]} +{"id":"bf-4eal","title":"Update ardenone-cluster deployment to spaxel:0.1.354 after CI","description":"After spaxel-build-manual-bj7l2 (on iad-ci) succeeds and pushes ronaldraygun/spaxel:0.1.354:\n\n1. In ~/declarative-config, open k8s/ardenone-cluster/spaxel/deployment.yml\n2. Change image tag from 0.1.352 to 0.1.354\n3. Commit and push declarative-config\n4. ArgoCD auto-syncs the Deployment (Recreate strategy, ~30s downtime)\n5. Verify: kubectl --server=http://traefik-ardenone-cluster:8001 get pods -n spaxel\n6. Hit spaxel.ardenone.com/healthz to confirm it returns 200\n\nThis gets the 4MB firmware into the running mothership so re-flashing the ESP32 will work.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":0,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-06-04T03:15:07.139700779Z","updated_at":"2026-07-02T19:37:09.459815093Z","closed_at":"2026-07-02T19:37:09.459815093Z","close_reason":"Not executed: stale/obsolete — live cluster already at 0.1.358 (newer than target 0.1.354), healthy; deploying 0.1.354 would be a downtime downgrade. See comment 18.","source_repo":".","compaction_level":0,"comments":[{"id":16,"issue_id":"bf-4eal","author":"cli","text":"NOT DEPLOYED — preconditions unmet & premise stale (agent check 2026-07-02T19:03:12Z).\n\n1) Gating CI build missing: workflow 'spaxel-build-manual-bj7l2' is NotFound on iad-ci (zero spaxel workflows present). The task's hard precondition ('After spaxel-build-manual-bj7l2 succeeds and pushes ronaldraygun/spaxel:0.1.354') has NOT occurred.\n\n2) Premise is stale: task says 'change tag from 0.1.352 to 0.1.354', but declarative-config k8s/ardenone-cluster/spaxel/deployment.yml is ALREADY at 0.1.358. Git history: 0.1.352 -> (pre-staged 0.1.354 @951d408) -> 0.1.356 -> 0.1.358.\n\n3) Deploying 0.1.354 would be a DOWNGRADE (0.1.358 -> 0.1.354) with ~30s Recreate downtime. Live pod spaxel-d76b49496-7l4mj is running 0.1.358, healthy: healthz HTTP 200, uptime ~7.9d, db ok. The stated goal (4MB firmware in the running mothership) is already satisfied by the newer 0.1.358.\n\nPer bead rules, not closing. Recommend: close as obsolete, or re-target to the current VERSION (0.1.357) if a redeploy is actually wanted. No changes made to spaxel or declarative-config.","created_at":"2026-07-02T19:03:12.407349955Z"},{"id":17,"issue_id":"bf-4eal","author":"cli","text":"OBSOLETE — do NOT deploy 0.1.354 (would be a regression). Verified 2026-07-02: production ardenone-cluster is ALREADY running ronaldraygun/spaxel:0.1.358 (1/1 Running, 26d uptime), which is 4 patch versions AHEAD of the task target. The task's stated goal (ship the 4MB firmware / fix the 16MB crash loop) is already met: that fix is commit 301a588, which was VERSION 0.1.354 and is included in the deployed 0.1.358 and all versions since. Additional blockers: (1) prerequisite CI build spaxel-build-manual-bj7l2 does not exist on iad-ci; (2) the bead's own Rules forbid working outside /home/coding/spaxel/ but the task requires editing ~/declarative-config; (3) repo HEAD is 0.1.357 and production 0.1.358 — 0.1.354 was only a transient retry version. Recommend cancelling/re-scoping this bead rather than retrying. No code changes made.","created_at":"2026-07-02T19:25:11.305734462Z"},{"id":18,"issue_id":"bf-4eal","author":"cli","text":"NOT EXECUTED — stale/obsolete, intentionally skipped to avoid a harmful production downgrade. Verified at close time: Live ardenone-cluster Deployment ALREADY running docker.io/ronaldraygun/spaxel:0.1.358 (confirmed via Deployment image AND mothership /healthz: status=ok version=0.1.358 db=ok uptime~686000s). 0.1.358 is healthy (~8d uptime, no load shedding) — no reason to roll back. Target 0.1.354 is OLDER than running 0.1.358; deploying it = downgrade with ~30s downtime (Recreate) losing fixes in 0.1.355-0.1.358 (firmware 16MB crash-loop 301a588, dashboard /fleet 500 9ac200b, provision re-broadcast 2483d36). Precondition CI run spaxel-build-manual-bj7l2 does not exist in iad-ci argo-workflows. spaxel repo VERSION=0.1.357; bead premise (0.1.352 to 0.1.354) is outdated. Bead goal (4MB firmware in mothership for ESP32 re-flash) ALREADY satisfied — 0.1.358 is newer than the firmware-fix version. No action taken; declarative-config untouched, nothing pushed. If a newer-than-0.1.358 deploy is desired, create a fresh bead.","created_at":"2026-07-02T19:36:08.475523656Z"}]} +{"id":"bf-4gbf","title":"Re-flash ESP32 via Web Serial after 4MB firmware is deployed","description":"Prerequisites: bf-4eal (deployment to 0.1.354) is complete.\n\nSteps:\n1. Open Chrome or Edge (Web Serial requires Chromium-based browser)\n2. Navigate to spaxel.ardenone.com (auth required — use existing credentials)\n3. Go to the provision / onboard page\n4. Connect the ESP32-S3 dev board via USB-C\n5. Click Flash/Provision — esptool.js will download firmware from mothership\n and write it to the device\n6. Monitor the serial output: should boot, NOT crash with flash size mismatch\n7. Expected: captive portal or WiFi connection attempt (node is unprovisioned)\n\nIf the device still shows the 16MB crash, check /api/firmware/manifest returns\na firmware binary whose image header byte[3] bits 4-7 = 0x4 (4MB), not 0x6 (16MB).\n\nThis is the validation step that confirms the 4MB fix works end-to-end.","design":"","acceptance_criteria":"","notes":"","status":"pending","priority":0,"issue_type":"task","assignee":"","created_at":"2026-06-04T03:15:17.801283106Z","updated_at":"2026-07-02T23:12:44.266969513Z","source_repo":".","compaction_level":0,"comments":[{"id":19,"issue_id":"bf-4gbf","author":"cli","text":"SOFTWARE PRE-CHECK VERIFIED (agent, no hardware): The deployed firmware binary is confirmed 4MB, not 16MB. Pulled ronaldraygun/spaxel:0.1.358 (matches running instance version per /healthz) and extracted /firmware/spaxel-firmware.bin (1.62MB merged binary). APP image header @0x10000 byte[3]=0x2F -> high nibble 0x2 = 4MB (esptool encoding). Bootloader header @0x0 byte[3]=0x2F = 4MB. An old buggy 16MB build would be 0x4F (nibble 0x4). Fix source confirmed in HEAD: commit d837598 (sdkconfig 16MB->4MB, partitions.csv 4MB redesign, onboard.js flashSize 'detect'->'4MB'); Dockerfile builds with idf.py 4MB config + esptool merge_bin --flash_size 4MB. HEADS-UP on this bead's stated nibbles: it says 'bits4-7=0x4(4MB),0x6(16MB)' but that does NOT match esptool's standard mapping (0x2=4MB,0x4=16MB,0x6=64MB). Don't check for literal 0x4 - the deployed 4MB binary is 0x2. REMAINING (needs human + ESP32-S3 + Chrome/Edge): the actual Web Serial flash + serial boot observation (steps 1-7). Agent cannot perform physical USB flashing, so bead NOT closed.","created_at":"2026-07-02T19:46:10.194617767Z"},{"id":20,"issue_id":"bf-4gbf","author":"cli","text":"RE-VERIFIED (independent, this dispatch 2026-07-02) — leaving OPEN.\n\n(1) DEPLOYED FIRMWARE BYTE-VERIFIED 4MB (independent of prior comment #19). Pulled ronaldraygun/spaxel:0.1.358 (exact version running per /healthz: status=ok, uptime~686819s/~7.95d, db=ok, shedding_level=0). Extracted /firmware/spaxel-firmware.bin (1,696,480 B / ~1.62MB, built 2026-06-27). ESP image headers:\n - Bootloader @0x0: E9 04 02 2F -> magic 0xE9 OK; byte[3]=0x2F -> flash-size nibble(bits4-7)=0x2 = 4MB\n - App @0x10000: E9 05 02 2F -> byte[3]=0x2F -> 4MB\n A buggy 16MB build would be 0x4F (nibble 0x4). Deployed binary is genuinely 4MB. CAVEAT (same as #19): the bead's stated nibbles '0x4=4MB, 0x6=16MB' do NOT match esptool's standard map (0x2=4MB, 0x4=16MB, 0x6=64MB); under the real map the deployed 4MB binary is nibble 0x2.\n\n(2) SOURCE-OF-TRUTH corroborates: firmware/sdkconfig CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y (16MB not set); partitions.csv = 4MB layout (factory 0x1F0000 + ota_0 0x1F0000 + otadata 0x3F0000); fix commit 301a588 in HEAD; deployed 0.1.358 newer than fix version 0.1.354.\n\n(3) PHYSICAL STEP STILL UNREACHABLE (re-confirmed, permanent): no /dev/ttyUSB* or /dev/ttyACM*; no serial/ESP device in lsusb; esptool not installed; no Chromium; and Web Serial is inherently a human-granted interactive browser permission over a physically-USB-connected device. No agent environment can do steps 1-7.\n\nDECISION: OPEN. All agent-verifiable criteria (the 4MB-header precondition) are satisfied and byte-confirmed, but the bead's actual purpose -- end-to-end validation by physically flashing an ESP32-S3 and observing boot -- has NOT been done by any agent or human. bf-4gbf gates the physical sub-chain (next: bf-2po1 provision the node, also hardware-only); closing on a software-only check would falsely signal 'device flashed & ready' and unblock hardware-dependent beads for a device that does not exist here. Needs a human + USB-C + ESP32-S3 + Chrome/Edge. No code changes; fix already deployed in 0.1.358.","created_at":"2026-07-02T19:54:37.887118038Z"},{"id":21,"issue_id":"bf-4gbf","author":"cli","text":"RE-VERIFIED (3rd dispatch, 2026-07-02) — leaving OPEN. Software precondition fully satisfied; physical step permanently unreachable here.\n\n(1) HARDWARE/BROWSER CONSTRAINT (re-confirmed): no /dev/ttyUSB* or /dev/ttyACM*; no serial/ESP device (lsusb none); esptool/esptool.py not installed; no Chromium/Chrome/Edge on PATH. Web Serial is an interactive, human-granted browser permission over a physically USB-connected device — no agent shell can perform steps 1-7.\n\n(2) NO DEVICE ATTACHED: live https://spaxel.ardenone.com/healthz = {status:ok, version:0.1.358, uptime_s:691176 (~8d), nodes_online:0, db:ok, shedding_level:0}. nodes_online:0 means no ESP32 is connected to the mothership — nothing to flash/observe.\n\n(3) ENTIRE 4MB CHAIN VERIFIED (independent of prior comments): firmware/sdkconfig has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y (16MB not set); partitions.csv is the 4MB layout (factory 0x1F0000 + ota_0 0x1F0000 + otadata 0x3F0000); deployed 0.1.358 firmware binary was byte-verified 4MB by prior dispatches (bootloader+app header byte[3]=0x2F -> nibble 0x2 = 4MB per esptool map; a 16MB build would be 0x4F); AND the dashboard Web Serial write path now hard-codes 4MB — dashboard/js/onboard.js:679-692 loader.writeFlash({... flashSize:'4MB', flashMode:'dio', flashFreq:'80m' ...}). The /api/firmware/manifest handler (cmd/mothership/main.go:4231) serves the expected esp-web-tools manifest (behind Google OAuth, matching 'auth required').\n\n(4) BEAD NIBBLE NOTE (carried forward): the task's stated mapping 'byte[3] bits4-7 = 0x4(4MB)/0x6(16MB)' does NOT match esptool's standard map (0x2=4MB, 0x4=16MB, 0x6=64MB). Don't check for literal 0x4 — the real 4MB value is nibble 0x2.\n\nDECISION: OPEN. This is a hardware-validation bead; closing on software-only evidence would falsely signal 'device flashed & ready' and unblock bf-2po1 (node provisioning, also hardware-only) for a device that is not present. Needs a human + USB-C ESP32-S3 + Chrome/Edge. No code changes this session; 4MB fix already shipped in 0.1.358.","created_at":"2026-07-02T21:04:18.395853935Z"},{"id":22,"issue_id":"bf-4gbf","author":"cli","text":"Autonomous agent (bf-4gbf auto-dispatch) could NOT complete this bead — it is a physical hardware validation requiring a human.\n\nWhat this step requires (none achievable by a remote software agent in a terminal):\n- Chrome/Edge Web Serial session (needs a local browser + locally-USB-connected ESP32-S3)\n- Physical ESP32-S3 dev board connected via USB-C\n- Human to click Flash/Provision and read the device serial console\nConfirmed NOT available on the agent host: no /dev/ttyUSB* or /dev/ttyACM* (no board attached), no ESP-IDF toolchain (idf.py / $IDF_PATH absent), and the live mothership's /api/firmware/manifest + /firmware/ are behind the Google OAuth proxy (307), so the deployed binary header can't be inspected remotely either.\n\nSoftware-side prerequisite VERIFIED (the 4MB fix is in place end-to-end in source + deployed image):\n- firmware/sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y, FLASHSIZE='4MB' (was 16MB before)\n- firmware/sdkconfig.defaults: CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y (comment: 'fits in 4MB flash')\n- firmware/partitions.csv fits in 4MB (factory 0x10000 1.9MB + ota_0 0x200000 1.9MB + otadata 0x3F0000)\n- Dockerfile busts Kaniko cache + forces sdkconfig regen (commit 301a588 'fix 16MB crash loop'), so deployed 0.1.357 was rebuilt from the corrected 4MB config\n- Live https://spaxel.ardenone.com/healthz returns HTTP 200\n- NOTE: stale local firmware/build/app-flash_args still says '--flash_size 16MB' but that is a gitignored pre-fix build artifact (Apr 14, pre-301a588), NOT authoritative for what CI deployed.\n\nNo code changes were needed — the fix already shipped via prerequisite bead bf-4eal. Bead left OPEN for a human with hardware to perform the actual Web Serial flash + serial-boot observation. Per dispatch rules, an un-completable bead is not closed.","created_at":"2026-07-02T21:24:09.647935343Z"},{"id":23,"issue_id":"bf-4gbf","author":"cli","text":"Agent pass (claude-code-glm-5-alpha, 2026-07-02): cannot auto-complete — this is a physical hardware validation that needs a human to connect the ESP32-S3 dev board over USB-C and click through the Web Serial / esptool.js provisioning flow in Chrome/Edge at spaxel.ardenone.com, then observe live serial output. Leaving IN_PROGRESS for a human with hardware. Prerequisites verified at source: (1) bf-4eal (deploy 0.1.354) is CLOSED; current VERSION is 0.1.357. (2) 4MB flash fix confirmed in firmware sdkconfig.defaults (CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y / ='4MB') and committed sdkconfig, plus partitions.csv is laid out for 4MB (factory 0x10000+0x1F0000, ota_0 0x200000+0x1F0000, otadata 0x3F0000). Fix commit: 301a588 'fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop'. No local spaxel.bin built (app binary is built in CI/Kaniko), so byte[3] header nibble of the deployed image can't be inspected from this checkout — confirm at runtime via /api/firmware manifest when flashing. Expected good result on re-flash: node boots cleanly (no 'flash size mismatch' / SPI-size crash) and either shows the captive portal or attempts WiFi as an unprovisioned node.","created_at":"2026-07-02T21:52:41.276935808Z"},{"id":24,"issue_id":"bf-4gbf","author":"cli","text":"Agent verification (bf-4gbf, 2026-07-02): Cannot complete the physical flash step — this is a human-in-the-loop hardware validation.\n\nConfirmed the software/firmware chain is correct end-to-end:\n- 4MB fix at source: firmware/sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y / =\"4MB\"\n- Prereq bf-4eal (deploy 0.1.354) is CLOSED; running mothership is now at 0.1.358\n- Deployed mothership healthy: https://spaxel.ardenone.com/healthz -> status:ok, version 0.1.358, db ok\n- /api/firmware and /api/firmware/manifest are behind Google OAuth (can't introspect served binary from a CLI without browser creds)\n\nCannot be done by a coding agent (requires all four):\n- A physical ESP32-S3 dev board (none available)\n- USB-C physical connection\n- Interactive Chromium browser to drive Web Serial (needs user gesture + port picker; no headless path)\n- spaxel.ardenone.com Google OAuth session\n\nLeaving bead OPEN per 'do not close if you cannot complete' rule. Needs a human with the board to: open Chrome/Edge -> spaxel.ardenone.com -> onboard -> connect ESP32-S3 via USB-C -> Flash/Provision -> confirm boot (no flash-size-mismatch crash) -> expect captive portal / WiFi connection attempt.","created_at":"2026-07-02T22:09:20.701829249Z"},{"id":25,"issue_id":"bf-4gbf","author":"cli","text":"Software-side validation (agent, 2026-07-02T22:14:07Z):\n\nCONFIRMED FIX IS DEPLOYED:\n- Source: firmware/sdkconfig.defaults = CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y; generated firmware/sdkconfig also 4MB; partitions.csv fits 4MB.\n- Cache-bust in place (commit 301a588): Kaniko FIRMWARE_CACHE_BUST ARG + 'rm -f sdkconfig sdkconfig.old' so CI regenerates from the 4MB defaults (the root cause of the original 16MB loop).\n- Deployed mothership at spaxel.ardenone.com is healthy, version 0.1.358 (uptime ~8d) -> well past the 0.1.354 fix, so the 4MB image is live.\n- Note: stale local firmware/build/bootloader.bin (dated 2026-04-14, pre-fix) still shows a 16MB header -- it predates the fix and is NOT what shipped.\n\nBLOCKED ON PHYSICAL HARDWARE (needs a human):\nThe actual acceptance of this bead -- open a Chromium browser, OAuth into spaxel.ardenone.com, connect the ESP32-S3 via USB-C, click Flash/Provision (esptool.js), and monitor serial output for a clean boot (no 'Detected size smaller than image header' crash) followed by captive portal / WiFi attempt -- requires a physical board + browser that this coding agent has no access to.\n- I also cannot fetch the deployed /firmware/spaxel-firmware.bin to read byte[3] header bits: the entire site (not just /api/*) is behind the Google OAuth proxy (every non-/healthz path returns 307 to accounts.google.com), and I have no user session.\n- Per esptool's image-header table, byte[3] bits 4-7 = 0x2 means 4MB and 0x4 means 16MB (the task's '0x4=4MB, 0x6=16MB' looks off vs esptool encoding -- whoever does the flash should compare against 0x2 for 4MB).\n\nLeaving OPEN -- the end-to-end physical flash + serial validation remains for a human with the ESP32 board.","created_at":"2026-07-02T22:14:07.684170447Z"},{"id":26,"issue_id":"bf-4gbf","author":"cli","text":"Agent verification (cannot close — needs physical hardware):\n\nBLOCKED ON: human + physical ESP32-S3 board. This Hetzner box has NO USB-serial device (no /dev/ttyUSB* or /dev/ttyACM*), no USB-serial/ESP-JTAG bridge in lsusb, and no esptool on PATH. Web Serial also requires a GUI Chromium browser, which doesn't exist here. An agent cannot perform the physical USB-C flash or observe real boot/serial output.\n\nPREREQUISITES CONFIRMED READY (everything agent/CI-doable is done):\n- bf-4eal (deployment): CLOSED. Live mothership healthy — https://spaxel.ardenone.com/healthz => status ok, version 0.1.358, nodes_online 0.\n- Source 4MB fix in place: firmware/sdkconfig + sdkconfig.defaults both set CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y; partitions.csv is a 4MB layout (factory 0x10000 size 0x1F0000, ota_0 0x200000 size 0x1F0000, otadata 0x3F0000). Commit 301a588 'fix 16MB crash loop'.\n\nACTION FOR A HUMAN: connect an ESP32-S3 via USB-C, open Chrome/Edge, go to spaxel.ardenone.com provision page, Flash/Provision, confirm it boots (no 16MB flash-size-mismatch crash) into captive portal / WiFi-connect attempt. Then close this bead.","created_at":"2026-07-02T22:18:11.875420762Z"},{"id":27,"issue_id":"bf-4gbf","author":"cli","text":"Software/deployment chain verified — only the physical flash remains (requires a human + board):\n\n[PRE-FLIGHT CHECKS — all green]\n- 4MB flash fix at source: firmware/sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y (+ CONFIG_ESPTOOLPY_FLASHSIZE='4MB'); partitions.csv sized for 4MB (factory 0x1F0000 + ota_0 0x1F0000 + otadata).\n- Prerequisite bf-4eal (deploy 0.1.354) is closed; repo VERSION=0.1.357.\n- Deployed mothership is live+healthy: GET https://spaxel.ardenone.com/healthz -> {status:ok, version:0.1.358, db:ok}. The 4MB firmware is in the running mothership, so a Web Serial flash will pull the correct binary.\n\n[BLOCKER — not agent-completable]\nThis bead's actual validation (open Chrome/Edge Web Serial, plug ESP32-S3 via USB-C, click Flash/Provision, watch serial output for boot-vs-flash-size-mismatch) requires physical hardware + a human. This server has no ESP32 attached and no browser/Web Serial. Leaving OPEN per the bead retry contract; needs a human with the dev board to perform the flash and confirm boot.\n\n[Encoding note for whoever flashes] The diagnostic hint ('byte[3] bits 4-7 = 0x4 = 4MB') does not match esptool's standard image-header encoding: high nibble 0x2 = 4MB, 0x4 = 16MB. Since FLASHSIZE_4MB is set at source, a freshly built/served binary should read high nibble 0x2 (e.g. byte[3]=0x2x). Treat a 0x4 high nibble as still-16MB, not 4MB.","created_at":"2026-07-02T22:36:42.216785943Z"},{"id":28,"issue_id":"bf-4gbf","author":"cli","text":"Autonomous-agent re-attempt (2026-07-02). Cannot complete this step — it requires a physical ESP32-S3 + USB-C and a Chromium browser driving the Web Serial API, neither of which is available on this Hetzner box (no /dev/ttyUSB* or /dev/ttyACM*, no USB-serial bridge in lsusb, no chromium/chrome/edge in PATH). Per the task rule I am leaving this open for a human (jedarden) rather than closing it.","created_at":"2026-07-02T22:39:07.125408227Z"},{"id":29,"issue_id":"bf-4gbf","author":"cli","text":"Source-level verification (2026-07-02): 4MB fix is confirmed at source. firmware/sdkconfig:482 = CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y; firmware/sdkconfig.defaults:10-11 = 4MB; firmware/partitions.csv redesigned for 4MB (factory 0x10000 size 0x1F0000 + single ota_0 0x200000, no 16MB layout; old 16MB config kept as firmware/sdkconfig.old for reference); dashboard/js/onboard.js:681 passes flashSize:'4MB' to esptool. Deploy prerequisite bf-4eal is satisfied: VERSION=0.1.357, well past the 0.1.354 this task references. Do NOT rebuild firmware locally to proxy-verify — it pulls a ~2GB ESP-IDF image and a prior run timed out (~7min) with no gain; source evidence is sufficient.","created_at":"2026-07-02T22:39:22.316277106Z"},{"id":30,"issue_id":"bf-4gbf","author":"cli","text":"IMPORTANT for whoever does the physical flash: the task's stated header test is INVERTED vs the real esptool spec. The task says 'byte[3] bits 4-7 = 0x4 (4MB), not 0x6 (16MB)'. Correct esptool encoding: 4MB -> image-header byte[3] high-nibble 0x2 (full byte 0x20); 16MB -> high-nibble 0x4 (full byte 0x40). So a correctly-built 4MB image is EXPECTED to read 0x2 in that nibble, not 0x4. Verify with: esptool.py --port /dev/ttyUSB0 image_info spaxel-*.bin and check 'Flash size: 4MB', or hexdump byte[3] of the .bin and confirm high nibble is 0x2. If the device still shows a flash-size mismatch crash after flashing, that is the real thing to investigate.","created_at":"2026-07-02T22:39:22.337314455Z"},{"id":31,"issue_id":"bf-4gbf","author":"cli","text":"BLOCKED - requires physical hardware + human, not auto-completable on this server (re-verified 2026-07-02):\n\n- No ESP32 attached: /dev/ttyUSB* and /dev/ttyACM* absent; no USB-serial bridge (CP210x/CH340/FTDI/Espressif) in lsusb.\n- No Chromium/Edge in PATH; Web Serial API cannot be driven headless from the server.\n- spaxel.ardenone.com dashboard is OAuth-gated (307 -> Google accounts), not headless-accessible.\n\n4MB fix IS confirmed correct at source: firmware/sdkconfig AND sdkconfig.defaults have CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y (16MB unset); dashboard/js/onboard.js:681 passes flashSize:'4MB'. Deploy is past the 0.1.354 the task references (repo at 0.1.357), so the bf-4eal prereq is satisfied.\n\nHEADER-CHECK CORRECTION (task spec is inverted): per vendored dashboard/js/esptool-bundle.js flash-size table (1MB:0,2MB:16,4MB:32,8MB:48,16MB:64), a valid 4MB image has header byte[3] high-nibble 0x2 (byte 0x20); 16MB is high-nibble 0x4 (byte 0x40). Expect 0x2 for 4MB, NOT 0x4 - the task's 4MB=0x4 is actually the 16MB marker.\n\nLeft open for a human (jedarden) with a Chromium browser + ESP32-S3 via USB-C. Do NOT build firmware locally to proxy-verify: it pulls a ~2GB ESP-IDF Docker image and times out (~7min, prior run killed). Source evidence is sufficient; the physical boot/captive-portal observation is the only remaining step.","created_at":"2026-07-02T22:45:19.450038533Z"},{"id":32,"issue_id":"bf-4gbf","author":"cli","text":"Verification pass (autonomous agent, 2026-07-02): 4MB fix is correct at source level and the deploy prerequisite bf-4eal is closed (4MB image is in the running mothership, past the 0.1.354 the task references). Confirmed: firmware/sdkconfig has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y (lines 482/488); firmware/partitions.csv redesigned for 4MB (factory 0x1F0000 + single ota_0 at 0x200000, no 16MB layout); dashboard/js/onboard.js:681 passes flashSize:'4MB' to esptool; the vendored dashboard/js/esptool-bundle.js FLASH_SIZES map sends '4MB'->32 (0x20). Fix landed in commit d837598. PHYSICAL STEP NOT RUN: this Hetzner box has no ESP32-S3 attached (/dev/ttyUSB* and /dev/ttyACM* absent; no CP210x/CH340/FTDI/Espressif USB-serial bridge in lsusb) and no Chromium/Edge in PATH for Web Serial, and spaxel.ardenone.com is OAuth-gated. Needs a human with a Chromium browser + a physical ESP32-S3 board over USB-C. HEAD NOTE for whoever does the physical check: the bead's stated header test is inverted vs the real esptool spec. esptool encodes 4MB as image-header byte[3] high-nibble 0x2 (full byte 0x20) and 16MB as 0x4 (0x40) -- i.e. 0x4 = 16MB, NOT 4MB. So a correct 4MB image reads 0x2 in byte[3], not 0x4. Per task rule (cannot complete -> do not close), leaving OPEN.","created_at":"2026-07-02T22:48:00.612275608Z"},{"id":33,"issue_id":"bf-4gbf","author":"cli","text":"Verification check (autonomous agent on Hetzner box) — NOT closing; this is a physical-hardware validation step that can't be done here.\n\nState confirmed:\n- 4MB fix already correct at source: firmware/sdkconfig CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y (+ FLASHSIZE=\"4MB\"); firmware/partitions.csv redesigned for 4MB (factory 0x1F0000 + single ota_0 0x1F0000, no 16MB layout); dashboard/js/onboard.js:681 passes flashSize:'4MB'.\n- Prerequisite bf-4eal closed; deployed instance live at spaxel.ardenone.com — /healthz returns 200, version 0.1.358 (past the 0.1.354 the task references). So the 4MB firmware is deployed.\n\nWhy this box can't do the physical flash step:\n- No ESP32 attached (no /dev/ttyUSB* or /dev/ttyACM*; no CP210x/CH340/FTDI/Espressif USB-serial bridge in lsusb).\n- No Chromium-based browser in PATH — Web Serial API needs Chrome/Edge.\n- /api/firmware/manifest is OAuth-gated (307 -> accounts.google.com), unreachable headlessly.\n\nHeader-byte note for whoever does the physical check: the task's stated test (byte[3] high-nibble = 0x4 for 4MB, 0x6 for 16MB) is inverted vs the esptool spec. Correct: 4MB -> high-nibble 0x2 (byte 0x20); 16MB -> 0x4 (0x40). So a good 4MB image reads 0x2, not 0x4.\n\nLeaving open for a human with a Chromium browser + physical ESP32-S3 via USB-C.","created_at":"2026-07-02T22:51:34.310104175Z"},{"id":34,"issue_id":"bf-4gbf","author":"cli","text":"Re-attempted by agent (claude-code-glm-5-alpha), still blocked: requires a human + physical ESP32-S3. No ESP32 attached (no /dev/ttyUSB*/ttyACM*, no USB-serial bridge in lsusb), no Chromium browser in PATH, and spaxel.ardenone.com is OAuth-gated — Web Serial can't be driven from this server. Source-level 4MB fix is confirmed in place: sdkconfig FLASHSIZE_4MB=y, partitions.csv redesigned for 4MB (factory 0x1F0000 + single ota_0), onboard.js:681 passes flashSize:'4MB', fix commit d837598. Prereq bf-4eal closed. NOTE for the physical check: the task's header test is inverted vs esptool spec — a 4MB image header byte[3] high-nibble reads 0x2 (byte 0x20), NOT 0x4; 16MB reads 0x4. Leaving open per task rules. See memory: hw-validation-beads-not-agent-completable.","created_at":"2026-07-02T22:57:23.907813868Z"},{"id":35,"issue_id":"bf-4gbf","author":"cli","text":"BLOCKER — requires a human + physical ESP32-S3 board; this server-based coding agent cannot perform it. Not closing per task rule.\n\nSoftware preconditions VERIFIED (no need to re-check):\n- 4MB fix at source: firmware/sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y / '4MB'; firmware/partitions.csv is a 4MB layout (factory ~1.9MB + ota_0 ~1.9MB + otadata). The 16MB flash-size-mismatch crash root cause is resolved in the build.\n- Prereq bf-4eal (deploy 0.1.354) is CLOSED. Deployed mothership healthy at v0.1.358: https://spaxel.ardenone.com/healthz -> 200 {status:ok, version:0.1.358}. 4MB firmware is live.\n- Firmware serving endpoints present in code: GET /api/firmware (manifest) + GET /firmware/ (binary), no-auth at app layer.\n\nCould NOT verify headlessly: /api/firmware is fronted by Google OAuth (auth.ardenone.com) at the reverse proxy, so the firmware binary image-header byte[3] flash-size check cannot run from this server without a browser session. A human session passes through.\n\nStill needed (HUMAN ONLY):\n1. Chrome/Edge -> spaxel.ardenone.com -> provision/onboard page.\n2. Plug ESP32-S3 dev board via USB-C.\n3. Click Flash/Provision (esptool.js downloads 4MB firmware + writes).\n4. Watch serial: must boot cleanly, NOT crash with flash-size mismatch; expect captive portal / WiFi connection attempt (node unprovisioned).","created_at":"2026-07-02T23:12:34.936822318Z"}]} +{"id":"bf-4ly4","title":"Search JavaScript files for blob-shaped object literals","description":"## Objective\nSearch all JavaScript files for blob-shaped object literals.\n\n## Scope\n- Search only .js and .jsx files\n- Use the pattern defined in the parent bead\n- Identify all locations with blob structure\n\n## Acceptance Criteria\n- [ ] All JavaScript files searched\n- [ ] Blob literal locations identified with file paths and line numbers\n- [ ] Results saved to notes/bf-26ta-javascript-results.md\n- [ ] Each match includes context snippet showing the blob structure","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T06:02:03.135320465Z","updated_at":"2026-07-06T06:18:55.359674524Z","closed_at":"2026-07-06T06:18:55.359674524Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-4ly4","depends_on_id":"bf-1rzd","type":"blocks","created_at":"2026-07-06T06:02:07.318031216Z","created_by":"cli","thread_id":""}]} +{"id":"bf-4oiz","title":"Verify and fix registry bridge position handling","description":"Ensure that positions from the simulator are correctly received and processed by internal/simulator/registry_bridge.go.\n\nScope:\n- Verify that registry_bridge.go receives position data from simulator\n- Check that positions are correctly formatted for registry consumption\n- Fix any missing or incorrect wiring\n\nAcceptance criteria:\n- registry_bridge.go receives position data from simulator\n- Positions are correctly formatted for registry\n- Any missing wiring is added\n- Positions flow from simulator to registry_bridge.go correctly","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T04:56:17.285977814Z","updated_at":"2026-07-06T16:23:13.948046549Z","closed_at":"2026-07-06T16:23:13.948046549Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-4oiz","depends_on_id":"bf-3ifj","type":"blocks","created_at":"2026-07-06T04:56:20.008223894Z","created_by":"cli","thread_id":""},{"issue_id":"bf-4oiz","depends_on_id":"bf-5dpu","type":"blocks","created_at":"2026-07-06T05:24:19.551325547Z","created_by":"cli","thread_id":""},{"issue_id":"bf-4oiz","depends_on_id":"bf-69ym","type":"blocks","created_at":"2026-07-06T13:19:39.307840995Z","created_by":"cli","thread_id":""}],"comments":[{"id":69,"issue_id":"bf-4oiz","author":"cli","text":"Verified the registry-bridge position flow end-to-end for close-out. The compile blocker flagged by bf-4pqj (*fleet.Registry not satisfying simulator.RegistryNodeAdapter) was resolved by bf-5lii's fleetRegistryAdapter + immediate SyncToRegistry on AddNode/UpdateNode; bf-69ym added two real-SQLite-registry e2e tests. Re-verified: go build ./cmd/mothership OK, go vet ./... clean, go test ./... all green (simulator + api fresh). The two virtual-node position mutation sites (simulator.go CreateVirtualNode/UpdateNodePosition) are the only writers and both trigger immediate sync; 30s ticker stays as safety net. Acceptance criteria met. Doc-only close-out commit fd7918f.","created_at":"2026-07-06T16:22:53.997318825Z"}]} +{"id":"bf-4pqj","title":"Audit and document current registry_bridge position wiring","description":"Read-only verification of the EXISTING registry_bridge position handling (split from bf-4oiz). Prior notes/bf-4oiz-findings.md claimed wiring was missing — that is now STALE: main.go already creates the VirtualNodeStore + FleetRegistryBridge and runs a 30s periodic SyncToRegistry (cmd/mothership/main.go:4269-4301).\n\nScope:\n- Confirm registry_bridge.go receives position data from the simulator via VirtualNodeStore (SyncToRegistry iterates the store's virtual nodes)\n- Confirm positions are correctly formatted for registry consumption (RegistryNodeAdapter.AddVirtualNode/SetNodePosition take x,y,z float64)\n- Update notes/bf-4oiz-findings.md to reflect the real current data flow (periodic sync IS wired)\n\nAcceptance criteria:\n- registry_bridge.go confirmed to receive position data from simulator (via store) — documented\n- Position formatting confirmed correct, backed by registry_bridge_test.go\n- notes/bf-4oiz-findings.md 'Current Data Flow' section corrected to reflect periodic sync wiring exists\n- No production code changes required (verification only)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T13:18:51.748554168Z","updated_at":"2026-07-06T15:21:01.369382114Z","closed_at":"2026-07-06T15:21:01.369382114Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"comments":[{"id":65,"issue_id":"bf-4pqj","author":"cli","text":"Re-verified (2nd dispatch) — audit deliverable in notes/bf-4oiz-findings.md (commit 1499c34) confirmed accurate against current source:\n\nACCEPTANCE CRITERIA — ALL MET:\n1. registry_bridge.go receives simulator positions via store — SyncToRegistry (registry_bridge.go:~147) calls b.store.ListNodes() -> []*VirtualNodeState{.Position}, then spreads pos.X/pos.Y/pos.Z into adapter.AddVirtualNode/SetNodePosition. Data flow store->bridge->adapter CONFIRMED.\n2. Position formatting correct + backed by tests — RegistryNodeAdapter uses float64 XYZ throughout (registry_bridge.go:113-121); fakeRegistry test double asserts exact round-trip; 'go test ./internal/simulator/' -> ok (9 tests, incl. ExplicitPositionsPreserved, SyncOneNode_ExplicitPreserved, OriginNodesGetSpreadGeometry, Idempotent, MatchesFullSync).\n3. notes/bf-4oiz-findings.md 'Current Data Flow' corrected — stale 'wiring missing' claim replaced with the real periodic-sync wiring (main.go:4195-4226: VirtualNodeStore + FleetRegistryBridge + 30s SyncToRegistry ticker).\n4. No production code changes — none made (verification only).\n\nKNOWN OUT-OF-SCOPE BLOCKER (documented, belongs to bf-5dpu / commit e92d5bc): 'go vet ./...' fails on TWO packages — cmd/mothership (main.go:4217: *fleet.Registry.GetAllNodes returns []fleet.NodeRecord, not []simulator.NodeRecord) and internal/api (simulator_test.go:19: NewSimulatorHandler signature changed to (store, bridge), test not updated). The internal/simulator package itself — the subject of THIS audit — builds and tests green. This bead cannot fix the blocker (no-prod-changes mandate); it is flagged for the wiring owner. Closing because all four acceptance criteria are satisfiable and complete; not closing would loop forever on out-of-scope breakage.","created_at":"2026-07-06T15:19:52.397270670Z"}]} +{"id":"bf-4q5w","title":"Wire fusion engine SetNodePosition — nodes never positioned, sim/IO-6 produces 0 blobs","description":"Found during investigation of bf-3v39 (presence-detection verification).\n\nSYMPTOM: E2E tests (tests/e2e TestFullE2EIntegration, TestDetectionEvents) and the IO-6 hard-gate scenario produce ZERO detection events / blobs even with 4 nodes + 2 walkers running spaxel-sim. The fusion loop runs at 10Hz but never emits a peak.\n\nROOT CAUSE: internal/fusion/fusion.go Engine.SetNodePosition(mac,x,y,z) is defined but NEVER CALLED in the non-test wiring (grep shows only the definition + unrelated SetPositionProvider helpers). Nodes default to pos=(0,0,1) in the DB (internal/db/migrations.go:143-145). With all nodes co-located, the Fresnel zone excess path length |P-T|+|P-R|-|T-R| collapses to ~0 everywhere, so the accumulation grid has no meaningful peaks and confidence is undefined -> no blobs.\n\nWHY IT MATTERS: This breaks the IO-6 release hard-gate ('walker produces a tracked blob') and means the eventual physical presence-detection validation (bf-3v39) will also show no blobs regardless of hardware, until node positions feed the fusion engine.\n\nLIKELY FIX: In cmd/mothership/main.go, call fusion Engine.SetNodePosition for each node (a) at startup from the DB nodes table, and (b) whenever a node's position changes (PATCH /api/nodes/:mac position handler) or a node connects/registers. The spaxel-sim default node placement (createVirtualNodes) and/or a default zone may also need to seed positions so the simulator path exercises real geometry. Verify by re-running TestFullE2EIntegration and asserting len(blobs) > 0 during the run (the test currently ducks this with 'we verify the API responds correctly rather than asserting a minimum count').","design":"","acceptance_criteria":"","notes":"","status":"open","priority":1,"issue_type":"task","created_at":"2026-07-03T00:11:33.217563362Z","updated_at":"2026-07-03T19:23:52.843063709Z","source_repo":".","compaction_level":0,"labels":["bug","deferred","fusion","localization","umbrella"],"dependencies":[{"issue_id":"bf-4q5w","depends_on_id":"bf-5jeo","type":"blocks","created_at":"2026-07-03T19:23:17.400573758Z","created_by":"cli","thread_id":""}]} +{"id":"bf-4qto","title":"Add default values for blob identity fields","description":"## Objective\nEnsure all blob identity fields have appropriate default or undefined values across all contexts.\n\n## Scope\n- Review all updated blob creation code\n- Add proper undefined or default values where needed\n- Ensure consistency in default value handling\n\n## Acceptance Criteria\n- [ ] All personName fields set to undefined or appropriate default\n- [ ] All assignedColor fields set to undefined or appropriate default \n- [ ] All identityResolved fields set to undefined or appropriate default\n- [ ] Default handling is consistent across codebase","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T05:11:37.452698658Z","updated_at":"2026-07-06T05:11:37.452698658Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-4qto","depends_on_id":"bf-5151","type":"blocks","created_at":"2026-07-06T05:11:57.963969215Z","created_by":"cli","thread_id":""}]} +{"id":"bf-4qwmy","title":"Simulator: GDOP overlay + shopping-list output (coverage gaps, recommended node count + positions)","description":"","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-06T00:40:16.874114137Z","updated_at":"2026-05-06T03:18:31.808932181Z","closed_at":"2026-05-06T03:18:31.808932181Z","close_reason":"Completed","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"compacted_at_commit":"","sender":""} +{"id":"bf-4truh","title":"Comprehensive notification system tests (open bead spaxel-40tl expansion)","description":"Open bead spaxel-40tl 'Write comprehensive tests for notification system' is open. The notify package (internal/notify/) has ntfy.go, pushover.go, webhook.go but tests are missing or incomplete. Needs tests covering: batching logic (30s dedup window), quiet hours gate (suppress non-critical during quiet window), morning digest aggregation, delivery retry logic, channel enable/disable, test-notification endpoint, and notification history API. The existing service_enhanced.go has complex batching logic that needs coverage.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-02T18:26:14.365679205Z","updated_at":"2026-05-02T18:26:51.067231767Z","closed_at":"2026-05-02T18:26:51.067231767Z","close_reason":"Duplicate of existing open bead spaxel-40tl which already comprehensively tracks notification system tests","source_repo":".","compaction_level":0} +{"id":"bf-4wwt","title":"Inventory blob projection and derived types with construction sites","description":"Scope: the blob-shaped PROJECTION/derived types — read-only views built FROM a tracked blob for a subsystem (not new tracked entities). Any field added to a tracked blob that a projection copies must be propagated at each construction site.\n\nProjection types to document (definition file:line + every construction boundary site):\n- explainability.BlobSnapshot, explainability.BlobExplanation\n- falldetect.BlobSnapshot (distinct type, shared name)\n- volume.BlobState, volume.BlobPos, api.BlobPos\n- tracking.BlobEvent\n- replay.BlobUpdate\n- simulator.BlobResult\n\nReference (already enumerated — RE-VERIFY, do not re-discover): notes/bf-4bhd.md \"Related Blob-shaped projection types\" table (9 types, ~15 construction sites across handler.go, detector.go, pipeline.go, simulator/engine.go, tracking/tracker.go, volume/shape.go).\n\nAcceptance Criteria:\n- [ ] Each projection type definition recorded with file:line and which tracked-blob type it is built from\n- [ ] Every projection construction boundary site listed (file:line)\n- [ ] Re-locate moved sites with: grep -rn \"BlobSnapshot{|BlobState{|BlobPos{|BlobUpdate{|BlobEvent{|BlobResult{|BlobExplanation{\" mothership/\n- [ ] All line numbers verified against current HEAD\n- [ ] Findings written to notes/bf--projections.md","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T15:03:36.904550067Z","updated_at":"2026-07-06T19:39:55.785656755Z","closed_at":"2026-07-06T19:39:55.785656755Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-4wwt","depends_on_id":"bf-5uzm","type":"blocks","created_at":"2026-07-06T15:04:21.277568858Z","created_by":"cli","thread_id":""}],"comments":[{"id":80,"issue_id":"bf-4wwt","author":"cli","text":"Re-verified all 9 blob-shaped projection/derived types and 15 non-test construction sites against current HEAD (68bd308) — every type definition (handler.go:27/95, detector.go:69, shape.go:139/1080, triggers.go:624, tracker.go:52, types.go:303, engine.go:80) and every construction site (grep -rnE of BlobSnapshot{/BlobState{/BlobPos{/BlobUpdate{/BlobEvent{/BlobResult{/BlobExplanation{) matches notes/bf-4wwt-projections.md exactly. Work committed in 68bd308 and pushed to origin/main. Findings written to notes/bf-4wwt-projections.md (companion to bf-4bhd): 9 types, 15 sites + 1 falldetect boundary (main.go:2288), with corrections noting api.BlobPos is test-only/dead, replay.BlobUpdate & simulator.BlobResult are synthetic (not blob-derived), and volume.BlobState's only blob-derived site is shape.go:575.","created_at":"2026-07-06T19:38:49.791215302Z"}]} +{"id":"bf-4zio","title":"BLE-to-blob 3D visualization: per-person colored humanoid + name labels in scene","description":"Phase 6 item 30 requires the 3D dashboard to show identified persons as colored humanoid figures with name labels ('Alice is in Kitchen') instead of generic 'Blob #2' markers. The BLE registry (spaxel-2wg) is closed but the frontend visualization using resolved identities is not implemented. Update the Three.js scene to render identity-matched blobs as named humanoid meshes.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-26T22:14:42.798449947Z","updated_at":"2026-07-06T12:46:52.964790297Z","source_repo":".","compaction_level":0,"labels":["umbrella"],"dependencies":[{"issue_id":"bf-4zio","depends_on_id":"bf-3dip","type":"blocks","created_at":"2026-07-06T04:05:42.020469537Z","created_by":"cli","thread_id":""}]} +{"id":"bf-5151","title":"Add identity fields to blob creation code","description":"## Objective\nUpdate all blob creation code to include the new identity fields (personName, assignedColor, identityResolved).\n\n## Scope\n- Use the list of blob creation sites from previous bead\n- Add the three identity fields to each blob creation\n- Set fields to undefined for existing blobs\n\n## Acceptance Criteria\n- [ ] All blob creation sites from previous bead are updated\n- [ ] personName, assignedColor, identityResolved added to each\n- [ ] Fields are set to undefined for existing code\n- [ ] No blob creation code is missed","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T05:11:33.679872377Z","updated_at":"2026-07-06T05:11:33.679872377Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5151","depends_on_id":"bf-4bhd","type":"blocks","created_at":"2026-07-06T05:11:54.079027318Z","created_by":"cli","thread_id":""}],"comments":[{"id":86,"issue_id":"bf-5151","author":"cli","text":"bf-5cgc handoff → notes/bf-5cgc-handoff.md. You are the PRIMARY owner of the three Go Tier-1 identity-leak fixes: (1) automation cmd/mothership/main.go:2303 + automation.TrackedBlob def internal/automation/engine.go:1337 — add PersonID, populate from identityMatcher.GetMatch(b.ID), engine resolves label/color via personProvider (engine.go:952); (2) explainability main.go:2206 + explainability.BlobSnapshot handler.go:95 — add identity fields, FOLD the parallel identityMap (main.go:2216) into the snapshot, drop the side-channel arg from UpdateBlobs (:2236); (3) volume main.go:2326 + volume.BlobPos shape.go:1080 — one-line PersonID populate (field already exists, consumer stubbed at shape.go:624). Mirror the reference pattern analytics.TrackUpdate (main.go:2267-2271) which pulls PersonID via GetMatch. NOTE: despite the sibling beads' TS framing, these are GO backend edits — see scope note §3.","created_at":"2026-07-06T20:38:59.973880259Z"}]} +{"id":"bf-5312","title":"Add the IO-6 hard-gate scenario test and a strict block-back triage gate","description":"Split from bf-5jeo (verification capstone). Depends on the TestFullE2EIntegration child. Child 4 of 4 (final, blocks the parent).\n\nCONTEXT: The IO-6 release hard-gate is \"walker produces a tracked blob\" with 4 nodes + 2 walkers running spaxel-sim (the config described in bf-4q5w; see also internal/signal/processor.go:607,626 which already documents that no engine feeds the live blob loop). There is currently no dedicated test that exercises this gate end-to-end.\n\nSCOPE:\n- Add a dedicated IO-6 hard-gate scenario (a tagged e2e test or runbook+test) that runs RunSimulator(ctx, 4 nodes, 2 walkers, 20 rate, duration) and asserts a walker produces a tracked blob end-to-end, reusing the assertion helpers from bf-2330.\n- CRITICAL TRIAGE GATE: if blobs are still zero after upstream bf-4q5w lands, DO NOT weaken the assertion. Instead: (a) keep this child blocked, (b) capture a finding as a comment with diagnostic evidence (fusion accumulation grid state, node positions, traces under .beads/traces/), and (c) feed that finding back to upstream bf-4q5w (fusion Engine.SetNodePosition wiring) rather than weakening the assertion.\n\nACCEPTANCE CRITERIA:\n- An IO-6 hard-gate scenario exists that asserts a tracked blob is produced with 4 nodes + 2 walkers running spaxel-sim.\n- When green: the IO-6 release hard-gate (walker produces a tracked blob) is satisfied end-to-end.\n- When zero blobs: the assertion stays strict, and a finding with trace evidence is filed back to bf-4q5w instead of weakening — never silently degrade this test.\n- bf-5jeo (this capstone umbrella) cannot close until this child is green.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T18:51:03.299000490Z","updated_at":"2026-07-06T18:51:03.299000490Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5312","depends_on_id":"bf-2aqf","type":"blocks","created_at":"2026-07-06T18:51:13.389487862Z","created_by":"cli","thread_id":""}]} +{"id":"bf-55rp","title":"Inventory primary tracked-blob Go types and direct construction sites","description":"Scope: the FIVE primary tracked-blob Go types that make up the tracked-blob lifecycle, plus the api.TrackedBlob alias. Document each type definition (file:line) and every DIRECT struct-literal construction site.\n\nTypes: tracking.Blob (2D), tracker.Blob (3D, identity-bearing), fusion.Blob (peak), automation.TrackedBlob, signal.TrackedBlob (+ api.TrackedBlob alias).\n\nThis work is mostly DONE in two existing reports — RE-VERIFY, do not re-discover:\n- notes/bf-3ldj-findings.md (sections 1-3)\n- notes/bf-4bhd.md (Blob Type Definitions + Patterns 1-2)\n\nAcceptance Criteria:\n- [ ] Each of the 5 primary types has its struct definition recorded with file:line, verified against current HEAD\n- [ ] Every direct struct-literal construction site listed (file:line + pattern: &Blob{} / Blob{} / TrackedBlob{})\n- [ ] The api.TrackedBlob alias relationship to signal.TrackedBlob is noted\n- [ ] Re-locate moved sites with: grep -rn \"Blob{|&Blob{|TrackedBlob{\" mothership/\n- [ ] Findings written to notes/bf--primary-types.md\n\nOut of scope: projection/derived types (child 3), JS/TS (child 4), test fixtures, the unrelated browser new Blob() download API.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T15:02:58.355494649Z","updated_at":"2026-07-06T18:59:02.545840787Z","closed_at":"2026-07-06T18:59:02.545840787Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"comments":[{"id":78,"issue_id":"bf-55rp","author":"cli","text":"Re-verified all 5 primary tracked-blob Go types + the api.TrackedBlob alias against HEAD c4d42e8. Findings in notes/bf-55rp-primary-types.md (committed d99f42d, pushed). Corrections vs prior reports: (1) two construction sites moved — automation.TrackedBlob main.go:2213->:2303, signal.TrackedBlob main.go:5384->:5494; (2) tracking.Blob (2D) field list drifted — PersonName/AssignedColor/IdentityResolved removed, identity fields now match tracker.Blob (3D) exactly. All 6 type definitions (5 structs + 1 pure = alias) confirmed at their reported file:line. Whole-repo sweep confirms exactly 5 production direct-literal construction sites, one per primary type (fusion:260 Blob{}, tracking:160 &Blob{}, tracker:162 &Blob{}, main.go:2303 automation.TrackedBlob{}, main.go:5494 sigproc.TrackedBlob{}). All 5 acceptance criteria met. Documentation only — no Go source changed.","created_at":"2026-07-06T18:57:23.168436965Z"}]} +{"id":"bf-55sg5","title":"Mobile-responsive expert mode","description":"## Goal\nMake expert mode fully functional on mobile devices with touch gestures.\n\n## Scope\n- Touch orbit/pan/zoom: single-finger rotate, two-finger pan, pinch to zoom (already supported by Three.js OrbitControls)\n- Hamburger menu for panels: collapsible sidebar for fleet status, settings, zones, triggers\n- Responsive layout: panels slide in from bottom on mobile, from right on desktop\n- Touch-optimized buttons: minimum 44×44px tap targets\n- No hover-dependent UI: all interactions work with tap\n- Mobile-specific shortcuts: long-press for context menu (replaces right-click)\n\n## Location\ndashboard/static/js/mobile.js (new module)\ndashboard/static/css/mobile.css (new stylesheet)\n\n## Acceptance\n- Three.js scene responds to touch gestures (orbit, pan, zoom)\n- Hamburger menu opens panel navigation\n- Panels slide in from bottom on mobile\n- All buttons are touch-friendly (≥44px)\n- No features require hover\n- Long-press context menu works on mobile","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:29.813640292Z","updated_at":"2026-05-06T11:04:43.020442678Z","closed_at":"2026-05-06T11:04:43.020442678Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-5613","title":"Component 25: Sleep monitoring — multi-person bedroom edge case (two blobs assignment)","description":"The sleep analyzer exists in internal/sleep/ with full state machine and breathing analysis. However the plan specifies a multi-person bedroom edge case that should be verified.\n\nPlan (Component 25): If two blobs are tracked in a bedroom zone simultaneously:\n- Assign sleep record to BLE-matched person if available\n- Otherwise create two separate zone-based records (one per occupant slot)\n- Breathing analysis uses blob with strongest stationary signal (lowest smooth_deltaRMS)\n\nCheck internal/sleep/analyzer.go to verify:\n1. Two-blob handling: does it create separate records per occupant or a single merged record?\n2. BLE-first assignment: when BLE identifies one or both blobs, are records attributed to persons?\n3. Lowest-deltaRMS blob selection for breathing analysis in multi-person case\n\nAlso check sleep/integration_test.go for a two-person scenario test. If missing, add a test case.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-05-26T04:05:59.392510448Z","updated_at":"2026-07-06T06:48:11.772354880Z","closed_at":"2026-07-06T06:48:11.772354880Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-56uk","title":"Verify blob identity fields TypeScript compliance","description":"## Objective\nEnsure all blob identity field additions are TypeScript-compliant with no compilation errors.\n\n## Scope\n- Run TypeScript compiler to check for errors\n- Fix any type mismatches in blob identity fields\n- Ensure all blob objects satisfy the updated type definition\n\n## Acceptance Criteria\n- [ ] TypeScript compiler runs without errors\n- [ ] All blob objects match the updated Blob interface\n- [ ] No missing field errors\n- [ ] No type mismatch errors for identity fields","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T05:11:41.148471860Z","updated_at":"2026-07-06T05:11:41.148471860Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-56uk","depends_on_id":"bf-4qto","type":"blocks","created_at":"2026-07-06T05:12:01.936893599Z","created_by":"cli","thread_id":""}]} +{"id":"bf-59me3","title":"GET /api/status and GET /api/occupancy endpoints","description":"Plan's REST API spec defines: (1) GET /api/status returning {version, nodes, blobs, uptime_s, detection_quality} and (2) GET /api/occupancy returning {zones:{:{count, people:[]}}}. Neither endpoint is registered in main.go. /api/blobs exists. These are simple read-only endpoints that dashboard and HA users would expect for quick system checks and occupancy queries without WebSocket.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:26.769707158Z","updated_at":"2026-05-05T17:40:16.692260524Z","closed_at":"2026-05-05T17:40:16.692260524Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-5cgc","title":"Hand off blob-creation inventory to identity-field implementation beads","description":"## Context\nbf-4bhd's deliverable exists to be consumed by the downstream identity-field implementation, which is tracked in separate OPEN beads: bf-5151 (add identity fields to blob creation code), bf-64h5 / bf-1wvm (extend blob data structure with identity fields), bf-iner (BLE identity matching), bf-4qto / bf-56uk / bf-f841. The site-to-fix mapping lives in `notes/bf-1q3m-consolidated.md` section 6 (tiered fix-target list) but is not yet formalized as a tracked deliverable linking specific sites to specific beads.\n\n## Scope\n- From consolidated report section 6 Tier-1, produce a concrete site -> fix-target -> bead handoff:\n - automation: `cmd/mothership/main.go:2303` + `internal/automation/engine.go:1337` (automation.TrackedBlob) -> add identity fields, populate from `identityMatcher.GetMatch(b.ID)` -> bf-5151 / bf-64h5.\n - explainability: `cmd/mothership/main.go:2206` + `internal/explainability/handler.go:95` (BlobSnapshot) -> add identity fields, fold parallel `identityMap` (`main.go:2216`) -> bf-5151.\n - volume: `cmd/mothership/main.go:2326` + `internal/volume/shape.go:1080` (volume.BlobPos) -> one-line `PersonID` populate -> bf-5151.\n- Reference population pattern: `analytics.TrackUpdate` (`cmd/mothership/main.go:2271`) pulls `PersonID` from the sidecar via `GetMatch` — mirror it.\n- Record the handoff as a note and/or comments on the linked implementation beads.\n\n## Acceptance Criteria\n- [ ] Handoff artefact (note or bead comments) maps each Tier-1 site to the responsible open implementation bead + the population pattern.\n- [ ] Tier 2 / Tier 3 / out-of-scope sites listed for completeness (tracker wiring decision, E1 no-op, dead `api.BlobPos`).\n- [ ] Closes bf-4bhd acceptance criterion: \"Report is ready for the next bead to use.\"","design":"","acceptance_criteria":"","notes":"","status":"in_progress","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T20:13:43.097215550Z","updated_at":"2026-07-06T20:34:10.204005411Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5cgc","depends_on_id":"bf-5ywk","type":"blocks","created_at":"2026-07-06T20:13:54.838823064Z","created_by":"cli","thread_id":""}]} +{"id":"bf-5dpu","title":"Fix registry bridge position wiring","description":"Fix any missing or incorrect wiring between simulator and registry_bridge.go to ensure positions flow correctly.\n\nScope:\n- Implement any missing data connections\n- Add required format transformations\n- Fix wiring issues identified in earlier beads\n- Add position routing logic if missing\n\nAcceptance criteria:\n- Positions flow from simulator to registry_bridge.go\n- Positions are correctly formatted for registry\n- All identified wiring issues are resolved\n- Data path is complete and functional","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T05:23:51.211181149Z","updated_at":"2026-07-06T05:51:38.882609058Z","closed_at":"2026-07-06T05:51:38.882609058Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5dpu","depends_on_id":"bf-5occ","type":"blocks","created_at":"2026-07-06T05:24:16.457674105Z","created_by":"cli","thread_id":""}]} +{"id":"bf-5fo3h","title":"Node disable/enable API endpoints","description":"Plan's REST API spec defines POST /api/nodes/:mac/disable (sets role to IDLE) and POST /api/nodes/:mac/enable (restores prior role). The fleet handler (internal/fleet/handler.go) has identify, reboot, OTA, position, role endpoints but no dedicated disable/enable. The quick-actions.js context menu exposes 'Disable / Enable' for nodes but there's no corresponding backend route.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:37.488896455Z","updated_at":"2026-05-05T18:13:57.459444611Z","closed_at":"2026-05-05T18:13:57.459444611Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-5jeo","title":"Assert blob production in e2e tests and the IO-6 hard-gate scenario","description":"Split from bf-4q5w (parent umbrella). Depends on the sim-geometry child. This is the verification capstone.\n\nThe e2e tests currently dodge the real assertion: tests/e2e/e2e_test.go TestFullE2EIntegration (~line 843-844) says we verify the API responds correctly rather than asserting a minimum count, and blobCount is captured but not asserted (line 364). TestDetectionEvents (line 679) is likewise weak.\n\nScope:\n- Replace the dodge with a real assertion that len(blobs) > 0 during a run (assert blobCount >= 1) in TestFullE2EIntegration.\n- Strengthen TestDetectionEvents to assert >=1 detection event.\n- Confirm the IO-6 release hard-gate scenario (walker produces a tracked blob) passes end-to-end with 4 nodes + 2 walkers running spaxel-sim, the config described in bf-4q5w.\n\nAcceptance criteria:\n- TestFullE2EIntegration asserts >=1 blob and passes.\n- TestDetectionEvents asserts >=1 detection event and passes.\n- The IO-6 hard-gate scenario is green (walker produces a tracked blob).\n- If blobs are still zero, this child blocks and feeds a finding back to the relevant upstream child rather than weakening the assertion.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-03T19:23:06.573060103Z","updated_at":"2026-07-06T18:52:33.158201458Z","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-5jeo","depends_on_id":"bf-18yn","type":"blocks","created_at":"2026-07-03T19:23:17.378033149Z","created_by":"cli","thread_id":""},{"issue_id":"bf-5jeo","depends_on_id":"bf-5312","type":"blocks","created_at":"2026-07-06T18:51:13.434632872Z","created_by":"cli","thread_id":""}]} +{"id":"bf-5kns","title":"Search for direct Blob constructor calls","description":"## Objective\nFind all locations where Blob objects are created via direct constructor calls (new Blob()).\n\n## Scope\n- Search for 'new Blob(' patterns across the codebase\n- Identify TypeScript and JavaScript files\n- Document each constructor call location\n\n## Acceptance Criteria\n- [ ] All 'new Blob()' calls are identified\n- [ ] Each location documented with file path and line number\n- [ ] Code context captured for each site","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T05:31:32.998912911Z","updated_at":"2026-07-06T05:53:06.725660865Z","closed_at":"2026-07-06T05:53:06.725660865Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"]} +{"id":"bf-5lii","title":"Wire immediate registry sync on simulator node create/update","description":"Fix the incomplete wiring in the simulator API handlers (split from bf-4oiz). AddNode and UpdateNode in mothership/internal/api/simulator.go currently only LOG 'will be synced to registry on next periodic sync' — they hold a registryBridge field but never call SyncToRegistry because the handler has no RegistryNodeAdapter to pass to it. This is the missing wiring: nodes added via the simulator API wait up to 30s (the periodic ticker) before reaching the fleet registry.\n\nScope:\n- Give SimulatorHandler access to a fleet.Registry-backed RegistryNodeAdapter (or a sync callback) so it can drive an immediate SyncToRegistry, in addition to the existing 30s periodic sync in main.go\n- AddNode: call registryBridge.SyncToRegistry() immediately after CreateVirtualNode succeeds, replacing the log-only stub\n- UpdateNode: call registryBridge.SyncToRegistry() immediately after UpdateNodePosition succeeds\n- Keep main.go wiring intact (periodic sync stays as a safety net); thread the adapter from cmd/mothership/main.go:4283/4303 into NewSimulatorHandler\n\nAcceptance criteria:\n- AddNode triggers an immediate SyncToRegistry into the fleet registry (not just a log line)\n- UpdateNode triggers an immediate SyncToRegistry on position change\n- New node appears in the fleet registry promptly after a create, without waiting for the 30s ticker\n- go vet ./internal/api/... ./internal/simulator/... passes\n- Existing simulator + registry_bridge tests still pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T13:19:02.660235909Z","updated_at":"2026-07-06T15:49:51.478283740Z","closed_at":"2026-07-06T15:49:51.478283740Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-5lii","depends_on_id":"bf-4pqj","type":"blocks","created_at":"2026-07-06T13:19:39.222892879Z","created_by":"cli","thread_id":""}],"comments":[{"id":66,"issue_id":"bf-5lii","author":"cli","text":"Implemented immediate registry sync on simulator node create/update. SimulatorHandler gains a registryAdapter (simulator.RegistryNodeAdapter) field threaded into NewSimulatorHandler. AddNode calls registryBridge.SyncToRegistry(adapter) right after CreateVirtualNode succeeds; UpdateNode does the same after UpdateNodePosition succeeds (both outside the handler lock). main.go adds fleetRegistryAdapter (adapts *fleet.Registry <-> simulator.NodeRecord), shared by the immediate syncs and the 30s periodic ticker (kept as a safety net). New table-driven TestImmediateSyncOnAddAndUpdate covers the wired (asserts AddVirtualNode + SetNodePosition driven synchronously) and nil-adapter no-op paths. go vet ./... clean; go test ./... all pass. Commit 41e8233 pushed to origin main.","created_at":"2026-07-06T15:48:19.188155951Z"}]} +{"id":"bf-5o576","title":"Fuzz tests for binary frame parser and JSON protocol","description":"Open bead bf-3d55l tracks this but has been sitting open with no implementation started. The ingestion frame parser (internal/ingestion/frame.go) and JSON message parser (internal/ingestion/message.go) parse untrusted input from ESP32 nodes. Need Go fuzz tests (testing.F) in frame_fuzz_test.go and message_fuzz_test.go covering: malformed header lengths, n_sub overflow, invalid channel values, truncated payloads, invalid JSON type discriminators, and extra fields.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-02T18:25:51.799073946Z","updated_at":"2026-05-02T18:26:32.910185523Z","closed_at":"2026-05-02T18:26:32.910185523Z","close_reason":"Duplicate of existing open bead bf-3d55l which already tracks fuzz tests for binary frame parser and JSON protocol","source_repo":".","compaction_level":0} +{"id":"bf-5occ","title":"Validate position formatting for registry","description":"Check that positions received by registry_bridge.go are correctly formatted for registry consumption.\n\nScope:\n- Examine registry's expected position format\n- Compare with actual format from simulator\n- Identify any format mismatches\n- Document required transformations\n\nAcceptance criteria:\n- Registry's expected position format is documented\n- Actual format from simulator is documented\n- Format differences are identified\n- Required transformations are documented","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T05:23:47.562893475Z","updated_at":"2026-07-06T05:48:18.742639734Z","closed_at":"2026-07-06T05:48:18.742639734Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5occ","depends_on_id":"bf-2irz","type":"blocks","created_at":"2026-07-06T05:24:16.428493592Z","created_by":"cli","thread_id":""}]} +{"id":"bf-5rre","title":"Add accessibility tests for onboarding flow","description":"Create axe-core accessibility tests for the onboarding UI flow. Tests should walk through the onboarding steps and scan for WCAG 2.1 AA violations at each step.\n\n**Acceptance Criteria:**\n- Test file dashboard/tests/a11y-onboarding.spec.js exists\n- Tests cover the main onboarding flow steps\n- Each onboarding step page is scanned for accessibility violations\n- Tests fail if any WCAG 2.1 AA violations are found\n- Tests can be run locally with 'npx playwright test a11y-onboarding'","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T03:36:03.687996283Z","updated_at":"2026-07-06T04:29:52.401195679Z","closed_at":"2026-07-06T04:29:52.401195679Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5rre","depends_on_id":"bf-2adi","type":"blocks","created_at":"2026-07-06T03:36:48.688187395Z","created_by":"cli","thread_id":""}]} +{"id":"bf-5rulx","title":"Simulator: synthetic walkers + CSI generation (user paths, random walk, propagation model integration)","description":"","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-06T00:40:16.874026391Z","updated_at":"2026-05-06T02:45:17.053495568Z","closed_at":"2026-05-06T02:45:17.053495568Z","close_reason":"Completed","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"compacted_at_commit":"","sender":""} +{"id":"bf-5txbb","title":"Fleet status page","description":"## Goal\nFull table view of all registered nodes with all metrics, bulk actions, camera fly-to on click.\n\n## Scope\nTable columns:\n- Name: user-assigned friendly name\n- MAC: hardware address\n- Role: TX/RX/TX_RX — editable dropdown\n- Position: (x, y, z) — click to highlight node in 3D view and fly camera to it\n- Firmware: version string + 'Update available' badge\n- RSSI: last reported WiFi signal strength\n- Status: ONLINE/STALE/OFFLINE with colored indicator\n- Uptime: time since last boot\n- Actions: Restart, Update, Remove, Identify (blink LED)\n\nGlobal actions:\n- Update All (rolling OTA)\n- Re-baseline All\n- Export Config\n- Import Config\n\n## Location\ndashboard/static/js/fleet.js (new module, extract from existing code)\ninternal/api/fleet.go (already exists)\n\n## Acceptance\n- Table shows all registered nodes\n- Click position → camera flies to node in 3D view\n- Role dropdown changes node role\n- Actions execute correctly\n- Bulk actions work on all nodes\n- Export/import config works","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:29.834674580Z","updated_at":"2026-05-06T11:48:29.548615767Z","closed_at":"2026-05-06T11:48:29.548615767Z","close_reason":"Implemented fleet status page with full table view and bulk actions:\n- Added dashboard/static/js/fleet.js: core fleet management module with API functions\n- Added dashboard/static/css/fleet-page.css: complete styling for fleet table\n- Table columns: Name, MAC, Status, Firmware, Uptime, Position, Role, Health, Packet Rate, Temperature, Actions\n- Bulk actions: Update All (rolling OTA), Re-baseline All, Export Config, Import Config\n- Camera fly-to on position click\n- Node actions: identify (LED blink), update firmware, remove, re-assign role\n- CSV export for reporting","source_repo":".","compaction_level":0} +{"id":"bf-5uzm","title":"Map cross-package blob conversion boundaries in main.go","description":"Scope: the cross-package conversion boundaries in cmd/mothership/main.go where a tracked blob is converted from one type to another. These are the CRITICAL identity-leak sites — the actual goal of the umbrella effort.\n\nSites to re-verify against HEAD:\n- main.go:5384 — fusion.Blob peak to sigproc.TrackedBlob (E1)\n- main.go:2213 — tracked blob to automation.TrackedBlob, IDENTITY DROPPED (E2)\n- main.go:2116 — tracked blob to explainability.BlobSnapshot, IDENTITY DROPPED (E3)\n- main.go:2236 — tracked blob to volume.BlobPos\n\nFor each: source type, target type, which fields are copied, which identity fields (if any) are dropped, and whether the target type even carries identity fields.\n\nReference: notes/bf-3ldj-findings.md section 5 (Tier 1) + section 6 (data flow), notes/bf-4bhd.md Pattern 4.\n\nAcceptance Criteria:\n- [ ] Every main.go blob-conversion loop enumerated with source type, target type, copied fields\n- [ ] The two identity-dropping boundaries (:2213 automation, :2116 explainability) explicitly flagged as the leak to fix\n- [ ] Whether the target type carries identity fields is noted for each boundary\n- [ ] All line numbers verified against current HEAD\n- [ ] Findings written to notes/bf--conversions.md","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T15:03:09.093500667Z","updated_at":"2026-07-06T19:23:33.753528151Z","closed_at":"2026-07-06T19:23:33.753528151Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-5uzm","depends_on_id":"bf-55rp","type":"blocks","created_at":"2026-07-06T15:04:21.183973883Z","created_by":"cli","thread_id":""}],"comments":[{"id":79,"issue_id":"bf-5uzm","author":"cli","text":"Implemented: findings written to notes/bf-5uzm-conversions.md and pushed (commit 585532f). Re-verified all four named conversion sites against HEAD — task line numbers had drifted +90..+110; corrected to E1 main.go:5494, E2 :2303, E3 :2206, volume :2326 (grep-confirmed against current main.go). Enumerated all 9 blob-projection loops in the live 10 Hz fusion loop with source/target types + copied fields. Flagged E2 (automation.TrackedBlob, no identity field) and E3 (explainability.BlobSnapshot, no identity field) as the two identity-leak boundaries to fix; volume.BlobPos carries PersonID but leaves it unpopulated. Noted per-target whether the type carries identity fields. Recorded a material correction: the live loop runs blobTracker (greedy associator), not tracker.TrackManager/applyIdentity — resolved identity lives in the ble.IdentityMatcher sidecar and is never written onto the blob struct, so every identity-aware boundary must pull from GetMatch(blob.ID) at the conversion site (analytics.TrackUpdate :2267 is the reference pattern). All 5 acceptance criteria met.","created_at":"2026-07-06T19:22:15.401966536Z"}]} +{"id":"bf-5vhya","title":"CI: pipeline timing benchmark gate","description":"## Goal\nAdd a benchmark that enforces the fusion loop timing budget as a CI quality gate, per plan §Quality Gates / Definition of Done (item 9).\n\n## What to build\n\nFile: internal/localizer/fusion/timing_budget_test.go\n\nRun the full fusion pipeline (phase sanitization → feature extraction → Fresnel accumulation → peak extraction → UKF update) against synthetic CSI data from spaxel-sim output.\n\nAssert:\n- Median fusion iteration < 15 ms over 600 iterations (60 seconds at 10 Hz)\n- P99 < 40 ms (hard limit)\n\n## CI integration\nAdd to Argo Workflows CI step after go test ./...:\n go test -bench=BenchmarkFusionLoop -benchtime=60s -count=1 ./internal/localizer/fusion/ | tee /tmp/bench.txt\n # fail if median exceeds 15ms threshold\n\n## Acceptance\n- Benchmark runs in the Argo CI workflow\n- Workflow fails if median latency exceeds 15 ms on the CI runner (allowance: 2x for slower hardware → 30 ms CI threshold, 15 ms production target)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","created_at":"2026-05-02T12:09:00.487025943Z","updated_at":"2026-05-04T14:25:01.352506963Z","closed_at":"2026-05-04T14:25:01.352506963Z","close_reason":"Implementation already complete in commit 7afbdc9. The timing budget benchmark:\n\n1. File: mothership/internal/localizer/fusion/timing_budget_test.go\n - Runs full fusion pipeline (phase sanitization → feature extraction → Fresnel accumulation → peak extraction → UKF update)\n - Uses synthetic CSI data simulating 4 nodes with 2 walkers\n - Runs 600 iterations (60 seconds at 10 Hz)\n\n2. Timing constraints enforced:\n - Median fusion iteration: 2.6ms (well below 15ms production target and 30ms CI threshold)\n - P99: ~10ms (well below 40ms hard limit)\n\n3. CI integration: .github/workflows/benchmark-ci.yml\n - Benchmark runs on every push/PR to main\n - Workflow fails if median exceeds 30ms (CI threshold)\n - Workflow fails if P99 exceeds 40ms (hard limit)\n\nAll acceptance criteria met.","source_repo":".","compaction_level":0} +{"id":"bf-5wb3n","title":"MQTT bidirectional commands: security_mode and rebaseline subscriptions","description":"Plan specifies that the mothership subscribes to {prefix}/command/security_mode (arm|disarm) and {prefix}/command/rebaseline (zone name or 'all') so Home Assistant automations can control these without opening the dashboard. The mqtt package has SubscribeToSystemMode but no SubscribeToRebaseline, and neither command subscription is wired in main.go to actual arm/disarm or rebaseline actions. Needs: (1) SubscribeToRebaseline in mqtt/client.go, (2) wiring both subscriptions to the relevant internal handlers in main.go when MQTT is configured.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:06.167277244Z","updated_at":"2026-05-05T16:52:27.277793127Z","closed_at":"2026-05-05T16:52:27.277793127Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-5wfsa","title":"Pre-deployment simulator (Component 17)","description":"## Goal\nBefore purchasing hardware, users can define their space, place virtual nodes, and run physics-based simulation to see expected detection quality.\n\n## Scope\n- Space definition: same 3D editor used for real setup — draw room boxes, set dimensions\n- Virtual nodes: place ghost nodes (wireframe, dashed links) that participate in GDOP computation\n- Simulation engine: simplified ray-based propagation (direct path + first-order reflections)\n- Synthetic walkers: virtual people moving along user-defined paths or random walk\n- Visualization: GDOP overlay, expected detection quality, coverage gaps highlighted\n- Outputs: minimum node count recommendation, optimal positions for N nodes, accuracy estimates, shopping list\n\n## Location\ndashboard/static/js/simulator.js (new module)\ninternal/sim/propagation.go (new package)\n\n## Acceptance\n- User draws room, places 2-4 virtual nodes\n- Click 'Simulate' → synthetic walkers generate CSI using same propagation model\n- GDOP overlay shows expected detection quality across floor\n- 'Shopping list' shows recommended node count and positions\n- 'Add another node here' highlights worst-GDOP positions","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.355796818Z","updated_at":"2026-05-06T03:35:02.980287642Z","closed_at":"2026-05-06T03:35:02.980287642Z","close_reason":"Completed","source_repo":".","compaction_level":0,"dependencies":[{"issue_id":"bf-5wfsa","depends_on_id":"bf-5xftp","type":"blocks","created_at":"2026-05-06T00:40:16.874228656Z","created_by":"batch","thread_id":""},{"issue_id":"bf-5wfsa","depends_on_id":"bf-2enwo","type":"blocks","created_at":"2026-05-06T00:40:16.875275352Z","created_by":"batch","thread_id":""},{"issue_id":"bf-5wfsa","depends_on_id":"bf-5rulx","type":"blocks","created_at":"2026-05-06T00:40:16.875320778Z","created_by":"batch","thread_id":""},{"issue_id":"bf-5wfsa","depends_on_id":"bf-4qwmy","type":"blocks","created_at":"2026-05-06T00:40:16.875374254Z","created_by":"batch","thread_id":""}]} +{"id":"bf-5xftp","title":"Simulator: space + virtual node placement (3D editor reuse, ghost wireframe nodes, dashed links)","description":"","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-06T00:40:16.873186445Z","updated_at":"2026-05-06T01:41:31.736203803Z","closed_at":"2026-05-06T01:41:31.736203803Z","close_reason":"Implemented space + virtual node placement for the simulator:\n\n1. Space key handler: Press space bar to place a new virtual node at camera target position\n2. Node placement logic: Intersection of camera ray with ground plane, clamped to room bounds\n3. Default height: 80% of room height or 1.5m minimum\n4. 3D editor reuse: Uses existing TransformControls for dragging and positioning\n5. Ghost wireframe nodes: Virtual nodes rendered as translucent wireframe octahedra (teal color)\n6. Dashed links: Links to/from virtual nodes rendered as dashed teal lines\n\nThe implementation adds placeVirtualNodeAtCameraTarget() to placement.js which:\n- Calculates camera ray intersection with ground plane (y=0)\n- Clamps position to room bounds\n- Sets reasonable default height\n- Calls addVirtualNode() to create the node via REST API\n\nThis completes the pre-deployment simulator feature, allowing users to plan\nnode placement before purchasing hardware.","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"compacted_at_commit":"","sender":""} +{"id":"bf-5y8tm","title":"Fresnel zone debug overlay","description":"## Goal\nToggle-able wireframe ellipsoids between active links in the 3D scene for debugging coverage geometry.\n\n## Scope\n- Toggle button in toolbar: 'Fresnel zones'\n- When enabled: render first Fresnel zone ellipsoids as wireframe meshes between active link pairs\n- Helps users understand coverage geometry visually\n- Shows zone 1 (most sensitive) as green wireframe\n- Multiple zones per link can be shown (zones 1-5)\n\n## Location\ndashboard/static/js/viz3d.js (extend existing 3D visualization)\n\n## Acceptance\n- Toggle button shows/hides Fresnel zone ellipsoids\n- Zones render correctly for all active TX→RX links\n- Update in real-time as nodes are moved\n- Performance: <5ms render time for 8-node fleet (28 links)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.410156795Z","updated_at":"2026-05-06T05:05:27.767847993Z","closed_at":"2026-05-06T05:05:27.767847993Z","close_reason":"Fixed Fresnel zone debug overlay mouse interaction bugs:\n- Raycast against both wireframe and fill meshes for reliable detection\n- Wireframe is always present even when fill has 0 opacity (zones 2+)\n- Sort intersections by distance to get the closest intersection\n- Fixes hover detection not working properly for multi-zone Fresnel ellipsoids","source_repo":".","compaction_level":0} +{"id":"bf-5yff","title":"Implement realistic node geometry in virtual node creation","description":"Child of bf-18yn (split). Depends on bf-195o.\n\nImplement the geometry placement design in the simulator virtual-node creation path.\n\nScope:\n- Modify the virtual node creation code to assign realistic positions instead of co-located/origin\n- Apply the design from bf-195o (e.g., grid layout with specific spacing)\n- Update internal/simulator/registry_bridge.go or wherever PosX/PosY/PosZ are sourced\n- Ensure positions are set during virtual node initialization\n\nAcceptance criteria:\n- Virtual nodes are created with spread-out positions (not all at origin, not co-located)\n- Code uses the constants/layout designed in bf-195o\n- Positions are properly assigned to PosX/PosY/PosZ fields","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T04:22:17.482482355Z","updated_at":"2026-07-06T04:46:57.705340422Z","closed_at":"2026-07-06T04:46:57.705340422Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5yff","depends_on_id":"bf-195o","type":"blocks","created_at":"2026-07-06T04:22:20.939958629Z","created_by":"cli","thread_id":""}]} +{"id":"bf-5ywk","title":"Banner superseded bf-4bhd notes, confirm single inventory source","description":"## Context\nbf-4bhd has failed 4x largely because re-dispatched agents trust the STALE `notes/bf-4bhd.md`. The bf-1q3m consolidated report section 4.1 flagged it SUPERSEDED for two material reasons: (1) 2D field-list drift (it claims `tracking.Blob` has `PersonName/AssignedColor/IdentityResolved` fields that no longer exist), and (2) +90 line drift on E1/E2/E3 boundaries. The file itself carries NO supersession banner, so it still reads as an authoritative \"Analysis Report\" with \"verified line numbers.\" This is the root-cause trap.\n\n## Scope\n- Add an explicit SUPERSEDED banner at the TOP of `notes/bf-4bhd.md` pointing to `notes/bf-1q3m-consolidated.md` as the sole trusted blob inventory. State WHY it is superseded (field-list + line drift, per section 4.1).\n- Confirm `notes/bf-1q3m-consolidated.md` is the single source of truth.\n- Verify `notes/bf-3ldj-findings.md` (also superseded per section 4.1) is clearly secondary/banned or banner it too.\n- Keep both stale files for provenance — do NOT delete.\n\n## Acceptance Criteria\n- [ ] `notes/bf-4bhd.md` top carries a visible SUPERSEDED banner naming `notes/bf-1q3m-consolidated.md` and the reason.\n- [ ] `notes/bf-1q3m-consolidated.md` confirmed as the single live inventory source.\n- [ ] No un-bannered stale inventory note remains that a future agent would mistake for authoritative.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T20:13:43.012182222Z","updated_at":"2026-07-06T20:33:37.078942894Z","closed_at":"2026-07-06T20:33:37.078942894Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5ywk","depends_on_id":"bf-1m2x","type":"blocks","created_at":"2026-07-06T20:13:54.783932850Z","created_by":"cli","thread_id":""}],"comments":[{"id":84,"issue_id":"bf-5ywk","author":"cli","text":"Closed: added SUPERSEDED banners making notes/bf-1q3m-consolidated.md the sole trusted blob inventory.\n\nVerified both staleness claims against HEAD (e698c37; recent commits are docs-only so source lines match the consolidated report's 1a26c12 verification):\n- bf-4bhd 2D field-list drift CONFIRMED: tracking.Blob has PersonID/PersonLabel/PersonColor/IdentityConfidence/IdentitySource/IdentityLastSeen -- no PersonName/AssignedColor/IdentityResolved.\n- bf-4bhd line drift CONFIRMED: E2 :2303 (was :2213, +90), E3 :2206 (was :2116, +90), E1 :5494 (was :5384, +110), volume :2326 (was :2236, +90).\n- bf-3ldj same line drift + the false 'IDENTITY ATTACHED' data-flow claim (tracker.TrackManager not wired into main.go; identity is sidecar-based via ble.IdentityMatcher).\n\nDone (commit 1ecc999, pushed):\n1. notes/bf-4bhd.md -- strong SUPERSEDED banner at top (field-list + line drift, per section 4.1), title marked SUPERSEDED.\n2. notes/bf-3ldj-findings.md -- strong SUPERSEDED banner (line + data-flow drift), title marked SUPERSEDED.\n3. notes/bf-1q3m-consolidated.md -- confirmed single source of truth (header banner + section 9 provenance already declare it); no edit needed.\n4. Proportionate 'secondary -- see consolidated' banners on the bf-3ldj-input notes (bf-26ta, bf-67ao, bf-5kns) and bf-26ta's JS/TS result dumps, so no inventory-titled note remains that a future agent could mistake for authoritative.\n\nAll stale files retained for provenance; none deleted. Docs-only -- no Go/JS source changed. go vet ./... and go test ./... both clean.\nAcceptance criteria all met.","created_at":"2026-07-06T20:29:38.374484316Z"},{"id":85,"issue_id":"bf-5ywk","author":"cli","text":"Closed bf-5ywk. Implementation was completed and pushed in commit 1ecc999 (HEAD == origin/main). Verified at close time:\n\n- notes/bf-4bhd.md: visible 🔴 SUPERSEDED banner at top naming notes/bf-1q3m-consolidated.md as sole trusted blob inventory, with the two material reasons per §4.1 (2D field-list drift: tracking.Blob's claimed PersonName/AssignedColor/IdentityResolved fields no longer exist; +90/+110 line drift on E1/E2/E3 boundaries). Original content retained below for provenance.\n- notes/bf-3ldj-findings.md: same SUPERSEDED banner (line drift + wrong 'IDENTITY ATTACHED' data-flow claim — tracker.TrackManager.UpdateWithIdentity is not wired into main.go at HEAD; identity is sidecar via ble.IdentityMatcher).\n- notes/bf-1q3m-consolidated.md: header already declares itself the single source of truth and explicitly supersedes both stale files. Confirmed unchanged.\n- Proportionate 'Secondary — see consolidated' banners added to the bf-3ldj input notes (bf-26ta, bf-26ta-js/ts, bf-67ao, bf-5kns) so no inventory-titled note reads as authoritative.\n- No stale inventory note left un-bannered. All stale files retained (none deleted) for provenance.\n- Docs-only change (7 markdown files); go vet ./... and go test ./... both clean (exit 0).","created_at":"2026-07-06T20:32:52.639968225Z"}]} +{"id":"bf-64h5","title":"Extend blob data structure with identity fields","description":"## Objective\nAdd identity-related fields to the blob/person data structure in the frontend to support name and color rendering.\n\n## Scope\n- Identify where blob data is defined in the 3D dashboard frontend\n- Add fields for: personName (string), assignedColor (hex/rgb), and identityResolved (boolean)\n- Ensure backward compatibility with existing blob data that lacks identity info\n- Add TypeScript types/interfaces if applicable\n\n## Acceptance Criteria\n- [ ] Blob data structure includes new identity fields (personName, assignedColor, identityResolved)\n- [ ] Existing code handles blobs without identity info (backward compatible)\n- [ ] TypeScript types updated if applicable\n- [ ] No console errors from undefined fields on existing blobs\n\n## Notes\nThis is a data structure-only change. No rendering work yet.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T04:04:17.436638406Z","updated_at":"2026-07-06T12:46:52.964790297Z","source_repo":".","compaction_level":0,"labels":["umbrella"],"dependencies":[{"issue_id":"bf-64h5","depends_on_id":"bf-2gmx","type":"blocks","created_at":"2026-07-06T04:41:59.854947777Z","created_by":"cli","thread_id":""}],"comments":[{"id":87,"issue_id":"bf-64h5","author":"cli","text":"bf-5cgc handoff → notes/bf-5cgc-handoff.md §0/§3. The automation Tier-1 leak (cmd/mothership/main.go:2303 + automation.TrackedBlob internal/automation/engine.go:1337) nominally lists this bead, but the fix is a GO backend edit (add PersonID to the Go struct + populate from identityMatcher.GetMatch). Your bead as written is dashboard-TS-types work (spaxel.d.ts Blob interface, per consolidated report Tier-4 #7). Recommend: keep bf-64h5 as the frontend TS track; assign the Go automation edit to bf-5151. See scope note §3 for the Go-vs-TS split.","created_at":"2026-07-06T20:38:59.992275783Z"}]} +{"id":"bf-67ao","title":"Search for blob factory functions","description":"## Objective\nFind all functions or methods that create and return blob objects.\n\n## Scope\n- Search for functions returning blob objects\n- Identify blob creation helpers/utilities\n- Document each factory location\n\n## Acceptance Criteria\n- [ ] All blob factory functions are identified\n- [ ] Each location documented with file path and line number\n- [ ] Function signature and blob creation pattern noted","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T05:31:41.102456169Z","updated_at":"2026-07-06T14:15:08.371357801Z","closed_at":"2026-07-06T14:15:08.371357801Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-67ao","depends_on_id":"bf-26ta","type":"blocks","created_at":"2026-07-06T05:31:59.643168033Z","created_by":"cli","thread_id":""}],"comments":[{"id":62,"issue_id":"bf-67ao","author":"cli","text":"Documented all 15 blob factory functions across 9 blob types in notes/bf-67ao-findings.md (commit 43d3a71). Each entry has function signature + file:line of both the func def and the struct-literal creation site + construction idiom. go vet has pre-existing failures in simulator_test.go/main.go (bf-5dpu, unrelated to this docs-only change).","created_at":"2026-07-06T14:04:25.592569220Z"},{"id":63,"issue_id":"bf-67ao","author":"cli","text":"Completed: documented all blob factory functions in notes/bf-67ao-findings.md. Verified every file:line by direct inspection (all 15 original entries exact). Completeness sweep added 2 gaps: (1) 10th blob type explainability.BlobSnapshot (handler.go:95), distinct from falldetect.BlobSnapshot despite shared name; (2) entry E3 — production conversion loop at main.go:2116 building []explainability.BlobSnapshot, sibling to E2. Also added test-only-factories caveat. Final catalogue: 16 creation sites across 10 blob types. Commits 43d3a71 + 7e89b31 pushed to main. go vet has pre-existing unrelated failures in simulator/main.go (present on HEAD, no .go files changed by this docs bead).","created_at":"2026-07-06T14:14:59.751202810Z"}]} +{"id":"bf-69ym","title":"Add end-to-end test for simulator-to-registry position flow","description":"Lock in the full data flow with an integration test (split from bf-4oiz). Today the only coverage is unit tests on registry_bridge.go in isolation; nothing proves a position travels from the simulator path all the way into the fleet registry. After the immediate-sync wiring lands (bf-5lii), add an end-to-end test.\n\nScope:\n- Add a test that exercises the complete path: node create via the simulator handler (or VirtualNodeStore.CreateVirtualNode) -> registryBridge.SyncToRegistry -> adapter.AddVirtualNode / SetNodePosition into a fleet.Registry\n- Assert the node lands in the registry with the SPREAD position assigned at origin (DefaultNodePositions), not (0,0,0)\n- Use the existing fleetRegistryAdapter pattern from cmd/mothership/main.go or a test fake implementing simulator.RegistryNodeAdapter\n\nAcceptance criteria:\n- New test covers create + update (position change) flowing into the registry\n- Test asserts the registry-side position matches the spread position assigned by DefaultNodePositions\n- Test passes: go test ./internal/simulator/... ./internal/api/...\n- No regressions in the existing test suites","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T13:19:16.172808818Z","updated_at":"2026-07-06T16:15:45.746505319Z","closed_at":"2026-07-06T16:15:45.746505319Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-69ym","depends_on_id":"bf-5lii","type":"blocks","created_at":"2026-07-06T13:19:39.265170092Z","created_by":"cli","thread_id":""}],"comments":[{"id":67,"issue_id":"bf-69ym","author":"cli","text":"Closed by end-to-end test for the simulator→fleet-registry position flow (split from bf-4oiz).\n\nAdded two tests in mothership/internal/api/simulator_test.go that wire a real SQLite-backed *fleet.Registry behind the SimulatorHandler via a test-local fleetRegistryTestAdapter (mirrors cmd/mothership/main.go's fleetRegistryAdapter, which lives in package main and isn't importable from tests):\n\n- TestSimulatorHandlerToRegistry_CreateAndUpdate: AddNode (no position) → VirtualNodeStore.CreateVirtualNode → FleetRegistryBridge.SyncToRegistry → adapter.AddVirtualNode into the live registry; asserts the node lands at exactly DefaultNodePositions(space,1) (never (0,0,0)), then UpdateNode → SetNodePosition asserting the new position flows through to the same registry row without duplicating it.\n- TestSimulatorHandlerToRegistry_OriginNodesGetSpreadGeometry: 4 nodes posted at the DB default origin {0,0,1} must land in the registry at the distinct, spread-out DefaultNodePositions(space,4) set — never co-located at the origin (the bf-18yn / bf-4q5w failure mode).\n\nThese exercise the complete path the bridge unit tests (fake adapter) and handler immediate-sync tests (recording fake, explicit positions) did not: a position round-tripped through a real fleet.Registry, with origin-default nodes confirmed spread. go test ./internal/simulator/... ./internal/api/... and full go test/vet ./... green. Commit 1456574.","created_at":"2026-07-06T16:09:52.920594256Z"},{"id":68,"issue_id":"bf-69ym","author":"cli","text":"End-to-end lock-in test added (commit 1456574, pushed to origin/main): TestSimulatorHandlerToRegistry_CreateAndUpdate drives AddNode -> VirtualNodeStore.CreateVirtualNode -> FleetRegistryBridge.SyncToRegistry -> adapter.AddVirtualNode/SetNodePosition into a REAL SQLite-backed fleet.Registry (via a test-local fleetRegistryTestAdapter mirroring cmd/mothership's fleetRegistryAdapter, since the prod adapter lives in package main and is not importable). It asserts the node lands in the live registry at the DefaultNodePositions spread slot (never the degenerate (0,0,0)/DB origin) and that a position change flows through to the same row via SetNodePosition without duplicating. A second test, TestSimulatorHandlerToRegistry_OriginNodesGetSpreadGeometry, asserts all 4 origin nodes map to exactly the DefaultNodePositions set. Verified: go test ./internal/simulator/... ./internal/api/... passes, full go test ./... green, go vet ./... clean. No regressions.","created_at":"2026-07-06T16:15:36.916712607Z"}]} +{"id":"bf-6aif","title":"Configure CI gate to block releases on accessibility violations","description":"Integrate the accessibility tests into the CI pipeline and configure them to block releases when violations are found. The axe-core tests must pass before any release can proceed.\n\n**Acceptance Criteria:**\n- Accessibility tests are added to the CI workflow (e.g., in .github/workflows or Argo Workflows)\n- The accessibility test step must pass before the release/build step can run\n- CI job clearly shows 'accessibility' or 'a11y' in the name\n- Failed accessibility tests block the release with a clear error message\n- Documentation is updated to reflect the new quality gate","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T03:36:14.607483379Z","updated_at":"2026-07-06T04:32:39.556995950Z","closed_at":"2026-07-06T04:32:39.556995950Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-6aif","depends_on_id":"bf-5rre","type":"blocks","created_at":"2026-07-06T03:36:48.703415642Z","created_by":"cli","thread_id":""}]} +{"id":"bf-6s3d","title":"Seed node positions from fleet registry into fusion engine at startup","description":"Depends on bf-3f6q. At startup, seed every node's 3D position into the blob-producing fusion engine, mirroring the existing selfImprovingLocalizer seeding loop at main.go:1024-1030.\n\nTasks:\n- After constructing the fusion engine (bf-3f6q), iterate fleetReg.GetAllNodes() and call engine.SetNodePosition(node.MAC, node.PosX, node.PosY, node.PosZ) for each node.\n- Ensure positions come from the DB PosX/PosY/PosZ columns so nodes are NOT all co-located at the DB default (0,0,1) per internal/db/migrations.go:143-145.\n- Place the seeding alongside the existing selfImprovingLocalizer seeding loop (main.go:1024-1030) for symmetry.\n\nAcceptance criteria:\n- After startup, engine.NodePositions() (fusion.go:151) returns a distinct, non-(0,0,1) position for each registered node.\n- engine.NodeCount() (fusion.go:141) equals the number of fleet nodes returned by GetAllNodes().","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T19:53:54.543046611Z","updated_at":"2026-07-03T20:47:17.785522017Z","closed_at":"2026-07-03T20:47:17.785522017Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-6s3d","depends_on_id":"bf-3f6q","type":"blocks","created_at":"2026-07-03T19:54:12.965346937Z","created_by":"cli","thread_id":""}],"comments":[{"id":47,"issue_id":"bf-6s3d","author":"cli","text":"Implemented. The 3D fusion engine's startup node seeding (iterate fleetReg.GetAllNodes() -> SetNodePosition(node.MAC, node.PosX, node.PosY, node.PosZ) per node, reading DB pos_x/pos_y/pos_z columns) was already wired into the live path by dependency bf-3f6q (main.go seeding loop at ~L1053-1062, symmetric with the selfImprovingLocalizer seeding). Verified the seed reads NodeRecord.PosX/PosY/PosZ (scanned from DB at fleet/registry.go:418), so nodes are NOT co-located at the (0,0,1) column default. This bead's contribution is a regression test, TestEngine_SeedNodePositions in internal/fusion/fusion_test.go, that locks in both acceptance criteria: NodeCount()==fleet node count, and NodePositions() holds a distinct, non-(0,0,1) position for each registered node (with exact coordinate round-trip + distinctness checks). go build/vet/test ./... all pass. Commit ce86575 pushed.","created_at":"2026-07-03T20:42:44.264248338Z"},{"id":48,"issue_id":"bf-6s3d","author":"cli","text":"Verified complete: the startup node-seeding loop at main.go:1055-1062 iterates fleetReg.GetAllNodes() and calls fusionEngine.SetNodePosition(node.MAC, node.PosX, node.PosY, node.PosZ) per node (DB pos_x/pos_y/pos_z columns), sitting symmetrically with the selfImprovingLocalizer seeding line. Acceptance criteria codified in TestEngine_SeedNodePositions (NodeCount == fleet count; NodePositions distinct + non-(0,0,1)). go build ./..., go vet ./..., go test ./... all green. Wired in bf-3f6q; regression test + acceptance lock added in ce86575 (on origin/main).","created_at":"2026-07-03T20:46:02.704219269Z"}]} +{"id":"bf-86b0","title":"Component 15: Diurnal baseline — 24-hour polar chart dashboard visualization","description":"The diurnal adaptive baseline is implemented in signal/diurnal.go with 24-slot per-hour learning. However the plan specifies a dashboard visualization that is missing.\n\nPlan requirement (Component 15):\n- A 24-hour polar chart per link showing baseline amplitude variance by hour\n- Spikes indicate noisy hours (HVAC cycling, sunlight, appliance EMI)\n- Helps users understand their environment\n- Referenced in setup/calibration panel (8c) under baseline management\n\nWhat exists:\n- diurnal.go: full 24-slot learning, crossfade logic, motion-gated updates, confidence scoring\n- diurnal_test.go: tests pass\n- diurnal-chart.js exists in dashboard/js/ — CHECK if this covers the polar chart\n\nCheck if dashboard/js/diurnal-chart.js implements the polar/radial 24-hour chart and wires it to the setup calibration panel baseline management section. If it is only a linear bar chart or stub, the polar chart with amplitude variance per hour is the gap.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","created_at":"2026-05-26T04:04:27.346995106Z","updated_at":"2026-05-26T04:04:36.960638807Z","closed_at":"2026-05-26T04:04:36.960638807Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-95tx","title":"Add integration test for position propagation","description":"Add integration test or log output that verifies positions flow through all stages: simulator → registry → fusion engine.\n\nScope:\n- Write integration test that traces position data through the full pipeline\n- Or add comprehensive logging that shows position flow at each stage\n- Ensure test/logs can be run to verify the complete data path\n\nAcceptance criteria:\n- Integration test or logging mechanism added\n- Test/logs show positions flowing from simulator to registry\n- Test/logs show positions flowing from registry to fusion engine\n- Full pipeline verification can be demonstrated","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T04:56:29.523616935Z","updated_at":"2026-07-06T17:14:54.756951528Z","closed_at":"2026-07-06T17:14:54.756951528Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-95tx","depends_on_id":"bf-u7ds","type":"blocks","created_at":"2026-07-06T04:56:33.029540394Z","created_by":"cli","thread_id":""}],"comments":[{"id":71,"issue_id":"bf-95tx","author":"cli","text":"Implemented TestPositionPropagation_FullPipeline (mothership/internal/api/simulator_test.go) — an end-to-end lock-in tracing ONE node position through all three runtime stages behind the SimulatorHandler: (1) simulator VirtualNodeStore, (2) fleet.Registry SQLite, (3) fusion.Engine nodePos mirror. Wires the production chain (handler → bridge.SyncToRegistry → adapter → Registry, with adapter forwardPos → fleet.Manager.ForwardNodePosition → sink → fusion.Engine.SetNodePosition), posts a node at AddNode and again at UpdateNode, and asserts every stage holds the same value. A pipelineStages helper logs each stage under go test -v so the full data path is observable: [create] simulator={1.5 2.5 1} registry={1.5 2.5 1} fusion={1.5 2.5 1} / [update] simulator={3.5 4.5 2} registry={3.5 4.5 2} fusion={3.5 4.5 2}. The update leg also asserts no stage retains the stale pre-update position and a single node stays single at every stage. go test ./... + go vet ./... green. Committed c69b0f7, pushed to main.","created_at":"2026-07-06T17:14:08.490298242Z"}]} +{"id":"bf-ao8eq","title":"Detection explainability (Component 28)","description":"## Goal\nImplement 'Why is this here?' on any blob/alert that shows exactly why the system made that decision.\n\n## Scope\n- X-ray overlay: non-contributing visual elements dim to 20% opacity\n- Links that contributed to detection glow, brightness proportional to deltaRMS contribution\n- Fresnel zone ellipsoids appear for active links\n- BLE match: dotted line from matched device's strongest node to blob, labeled with RSSI\n- Detail sidebar: per-link contribution table (link name, deltaRMS, threshold, Fresnel zone number, learned weight)\n- Confidence breakdown: spatial confidence + identity confidence with percentages\n\n## Location\ndashboard/static/js/explainability.js (new module)\ninternal/api/explain.go (new package)\n\n## Acceptance\n- Tap/click blob in 3D view → 'Why?' button appears\n- Tap 'Why?' → X-ray overlay activates, showing contributing links\n- Detail sidebar shows per-link breakdown\n- For alerts: specific conditions that triggered with values vs thresholds\n- Makes false positive cause obvious","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.300430327Z","updated_at":"2026-05-05T22:16:31.461661320Z","closed_at":"2026-05-05T22:16:31.461661320Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-awtza","title":"MQTT command/rebaseline and HA auto-discovery lifecycle management","description":"Plan specifies full HA auto-discovery lifecycle: configs published with retain=true on first connect AND whenever zones/persons are added or renamed; when zone or person is deleted, publish empty retained payload to remove HA entity. Also missing: rebaseline command subscription wiring. Currently mqtt/client.go publishes discovery on connect but has no mechanism to detect zone/person CRUD events and re-publish or un-publish discovery configs. Needs event bus subscription for zone/person changes.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:26:21.696828674Z","updated_at":"2026-05-06T13:53:20.089856369Z","closed_at":"2026-05-06T13:53:20.089856369Z","source_repo":".","compaction_level":0} +{"id":"bf-f841","title":"Test blob identity fields at runtime","description":"## Objective\nVerify that the blob identity fields work correctly at runtime without errors.\n\n## Scope\n- Run the application and test blob creation\n- Verify no runtime errors from undefined fields\n- Test blob access and identity field usage\n\n## Acceptance Criteria\n- [ ] Application starts without runtime errors\n- [ ] Blob objects can be created successfully\n- [ ] Identity fields can be accessed without errors\n- [ ] No undefined field access errors in console","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T05:11:44.825007401Z","updated_at":"2026-07-06T05:11:44.825007401Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-f841","depends_on_id":"bf-56uk","type":"blocks","created_at":"2026-07-06T05:12:09.557982125Z","created_by":"cli","thread_id":""}]} +{"id":"bf-iner","title":"Implement BLE identity matching logic for blobs","description":"## Objective\nImplement frontend logic to match BLE registry identities to blob IDs, so each blob gets its assigned person's name and color.\n\n## Scope\n- Identify where BLE identity data is available in the frontend (from spaxel-2wg work)\n- Write matching logic: given a blob ID, find the corresponding BLE identity and extract name/color\n- Handle unmatched blobs (no BLE identity found)\n- Store matched identity in the blob data structure (from child bf-64h5)\n\n## Dependencies\nDepends on: bf-64h5 (blob data structure must exist first)\n\n## Acceptance Criteria\n- [ ] Identity matching function exists and is callable\n- [ ] Matched blobs have personName and assignedColor populated\n- [ ] Unmatched blobs have identityResolved=false and no name/color\n- [ ] No crashes when BLE identity data is missing or malformed\n\n## Notes\nThis is data-only work. Still no rendering yet.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T04:04:22.989709866Z","updated_at":"2026-07-06T04:04:22.989709866Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-iner","depends_on_id":"bf-64h5","type":"blocks","created_at":"2026-07-06T04:04:51.962111901Z","created_by":"cli","thread_id":""}],"comments":[{"id":89,"issue_id":"bf-iner","author":"cli","text":"bf-5cgc handoff → notes/bf-5cgc-handoff.md §1.1. Server-side BLE→blob matching already EXISTS: ble.IdentityMatcher.GetMatch(blobID) (internal/ble/identity.go:609) returns *IdentityMatch{PersonID,PersonName,PersonColor,DeviceAddr,Confidence,TriangulationPos}. Your bead is the FRONTEND mirror of that match. The Go side already populates the sidecar each tick (main.go:2113 identityMatcher.UpdateBlobs); the Tier-1 fixes under bf-5151 thread that sidecar into automation/explainability/volume. Don't reimplement matching server-side — consume GetMatch.","created_at":"2026-07-06T20:39:00.011414748Z"}]} +{"id":"bf-m6f5g","title":"GET /api/baseline and POST /api/baseline/capture endpoints","description":"Plan's REST API spec defines GET /api/baseline (returns [{link_id, snapshot_time, confidence}] for all links) and POST /api/baseline/capture (optional ?links body, starts 60s quiet-room capture). The baselines SQLite table exists (from migrations.go) and the baseline system runs internally, but no HTTP endpoints expose read/capture to the dashboard. The fleet handler has /api/nodes/:mac/rebaseline and /api/nodes/rebaseline-all but no standalone baseline endpoints.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:32.710840129Z","updated_at":"2026-05-05T17:55:00.504558262Z","closed_at":"2026-05-05T17:55:00.504558262Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-qonqo","title":"GET /api/zones/:id/history occupancy history endpoint","description":"Plan's REST API spec defines GET /api/zones/:id/history?period=24h returning [{timestamp, count, people:[]}] in hourly buckets. The zones CRUD exists (internal/api/zones.go) but the history sub-endpoint is not implemented. The anomaly/pattern system stores per-zone per-hour data in anomaly_patterns, and zone occupancy is tracked in memory and SQLite. Needed for the 'Zone history' quick action and the occupancy chart in the dashboard sidebar.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:26:02.249193078Z","updated_at":"2026-05-05T21:44:59.734545703Z","closed_at":"2026-05-05T21:44:59.734545703Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-u7ds","title":"Verify and fix fusion engine position flow","description":"Ensure that positions from the registry correctly reach the fusion engine accumulation grid.\n\nScope:\n- Verify that registry sends positions to fusion engine\n- Check that fusion engine receives and accumulates positions correctly\n- Fix any missing connections in the registry-to-fusion path\n\nAcceptance criteria:\n- Registry sends positions to fusion engine\n- Fusion engine accumulation grid receives positions\n- Any missing wiring is added\n- Positions flow from registry to fusion engine correctly","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T04:56:23.503810524Z","updated_at":"2026-07-06T16:50:28.746899623Z","closed_at":"2026-07-06T16:50:28.746899623Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-u7ds","depends_on_id":"bf-4oiz","type":"blocks","created_at":"2026-07-06T04:56:26.193059903Z","created_by":"cli","thread_id":""}],"comments":[{"id":70,"issue_id":"bf-u7ds","author":"cli","text":"Verified and completed the registry→fusion engine position flow.\n\nThe fix (commit dbb5cd0) was committed locally by a prior run but never pushed to origin and the bead was never closed — that is why it re-dispatched. I pushed dbb5cd0 to origin/main and verified the complete path.\n\nAll three registry-write paths now converge on the blob-producing fusion engine's accumulation grid:\n1. REST PATCH /position → fleet.Handler.updateNodePosition → ForwardNodePosition → sink → fusion.Engine.SetNodePosition\n2. Node connect/register → fleet.Manager.OnNodeConnected → ForwardNodePosition → sink → fusion.Engine.SetNodePosition\n3. Simulator sync → fleetRegistryAdapter.forwardPos → ForwardNodePosition → sink → fusion.Engine.SetNodePosition (the path this bead closed; previously landed positions in SQLite only)\n\nPlus startup seeding (main.go reads DB → fusionEngine.SetNodePosition). The engine's Fuse() snapshots nodePos each tick and feeds grid.AddLinkInfluence — so positions reach the accumulation grid.\n\nLock-ins all green:\n- TestSimulatorHandlerToRegistry_PositionsReachFusionEngine (path 3, bf-u7ds)\n- TestHandlerUpdateNodePositionForwardsToEngine (path 1)\n- TestManagerOnNodeConnectedForwardsPositionToEngine (path 2)\n- TestEngine_SeedNodePositions / TestEngine_FourLinks_PositionAccuracy (grid consumes positions)\n\ngo vet ./... clean; go test ./... = 50 packages, exit 0.","created_at":"2026-07-06T16:49:17.319768169Z"}]} +{"id":"bf-usafo","title":"Morning briefing (Component 35)","description":"## Goal\nWhen user first opens dashboard each day, brief warm summary appears.\n\n## Content (generated from existing data):\n- Sleep summary (if available): 'You slept 7h 39m — 12 minutes more than your average. Breathing was regular.'\n- Who is home: 'Bob left at 8:15am. The house has been empty since 8:22am.'\n- Overnight anomalies: 'Last night: One unusual event at 2:34am — motion in kitchen for 30 seconds. No BLE match, low-confidence blob. Likely environmental.'\n- System health: 'System health: Excellent (94%). All 6 nodes online. Accuracy improved 2% this week thanks to your 8 corrections.'\n- Today's forecast: 'Based on your Wednesday pattern, you usually return around 5:45pm. Security mode will auto-activate when you leave.'\n\nDisplay:\n- Expert mode: card overlay on first dashboard open, dismissible with tap or 'Got it' button. Slides away after 10s if not interacted\n- Simple mode: morning card is first card in layout, stays visible until dismissed\n- Ambient mode: text fades in over ambient display when first person detected in morning, stays for 30s\n\nDelivery channels:\n- Dashboard (default)\n- Push notification at configured time (e.g., 7am)\n- Webhook to Slack/Discord\n\n## Implementation\nGo function GenerateBriefing(date string, person string) string\nAssembled in priority order: critical alerts → sleep → who's home → anomalies → system health → predictions → learning progress\nStored as daily record in briefings table\n\n## Acceptance\n- Briefing accurately summarizes overnight activity\n- Shows sleep report when available\n- Lists overnight anomalies with context\n- Shows system health\n- Shows today's predictions\n- Dismissible and non-intrusive","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:11.592787579Z","updated_at":"2026-05-06T10:09:43.916190927Z","closed_at":"2026-05-06T10:09:43.916190927Z","close_reason":"Implemented Component 35: Morning Briefing\n\n- Added briefing dashboard adapter wiring to hub for morning briefing push\n- Morning briefing provides warm summary when user first opens dashboard each day\n- Content includes: sleep summary (if available), who is home, overnight anomalies, system health, predictions, learning progress\n- Briefing generated automatically via GenerateBriefing(date, person) function\n- Stored as daily record in briefings table\n- Delivery channels: dashboard (card overlay on first open), push notification at configured time, webhook to Slack/Discord\n- Display modes: expert mode (dismissible card overlay), simple mode (first card), ambient mode (fade-in text)\n- Briefing scheduler generates daily briefings at configured time and sends push notifications\n- API endpoints: GET/POST /api/briefing/* for managing briefings\n- All tests passing (go test ./internal/briefing/...)\n","source_repo":".","compaction_level":0} +{"id":"bf-w15bj","title":"CI: golangci-lint static analysis gate","description":"## Goal\nAdd golangci-lint to the Argo CI workflow as a hard quality gate, per plan §Quality Gates / Definition of Done (item 3).\n\n## Configuration\nFile: .golangci.yml at repo root\n\nEnabled linters (minimum set):\n- errcheck: all errors must be handled or explicitly discarded with _\n- staticcheck: includes S-series (simplifications) and SA-series (bugs)\n- gosimple: simplification suggestions (SA-series overlap)\n- govet: same as go vet but integrated\n- ineffassign: catch dead assignments\n- unused: catch unused exported identifiers\n\nDisabled (too noisy for this codebase):\n- gocyclo, funlen, wsl (style preferences, not correctness)\n\n## CI integration\nAdd to the spaxel-build Argo WorkflowTemplate as a parallel step alongside go test:\n golangci-lint run --timeout 5m ./...\n\n## Acceptance\n- golangci-lint run passes on the current codebase (fix any pre-existing findings before adding the gate)\n- Argo CI fails on new lint violations\n- .golangci.yml committed to repo root","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","created_at":"2026-05-02T12:09:09.633464353Z","updated_at":"2026-05-05T06:49:00.297475830Z","closed_at":"2026-05-05T06:49:00.297475830Z","close_reason":"Completed","source_repo":".","compaction_level":0} +{"id":"bf-xrej","title":"Seed default spread geometry in the simulator registry bridge path","description":"Child of bf-18yn umbrella. Depends on child 2.\n\nProblem: the simulator FleetRegistryBridge (internal/simulator package) writes node.Position.X/Y/Z into the fleet registry via AddVirtualNode and SetNodePosition in its SyncToRegistry and SyncOneNode methods. When virtual nodes are created without an explicit position they carry (0,0,1)-style defaults, so the registry and downstream fusion engine see co-located nodes.\n\nScope:\n- In the virtual-node creation path feeding FleetRegistryBridge, and/or in SyncToRegistry/SyncOneNode when a node position is the default origin, assign the spread-out default geometry from child 1 helper (sized to roomWidth/roomDepth/roomHeight) instead of leaving co-located/origin positions.\n- Ensure those positions flow into both the registry and the fusion engine via the existing wiring.\n\nAcceptance criteria:\n- SyncToRegistry writes distinct, non-degenerate positions for default virtual nodes (no two co-located, not all at origin).\n- Existing internal/simulator tests pass; go build ./... passes.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T22:10:00.435559247Z","updated_at":"2026-07-03T23:03:03.374990382Z","closed_at":"2026-07-03T23:03:03.374990382Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-xrej","depends_on_id":"bf-24xp","type":"blocks","created_at":"2026-07-03T22:10:08.866035772Z","created_by":"cli","thread_id":""}],"comments":[{"id":57,"issue_id":"bf-xrej","author":"cli","text":"Implemented: virtual nodes still at the DB-default origin (0,0,1) are reassigned distinct, spread-out geometry via a new effectivePositions() helper in registry_bridge.go, sourced from DefaultNodePositions sized to the full node count across the store's space. Slot assignment is keyed by sorted node ID (deterministic across map iteration), and SyncOneNode resolves over the full node set so it writes the same position a full SyncToRegistry would. Explicit (non-origin) placements pass through unchanged; positions flow through SyncToRegistry, SyncOneNode, and ToRegistryRecords into the registry and the fusion-engine seeding. Added registry_bridge_test.go covering isDefaultOrigin, spread geometry for origin fleets (counts 1-9), explicit-position preservation, mixed sets, idempotent re-sync, and SyncOneNode/full-sync parity. go build ./..., go vet ./..., and go test ./... all green. Pushed as 307ab0e.","created_at":"2026-07-03T23:01:11.527240563Z"}]} +{"id":"spaxel-05a","title":"Implement calibration GET endpoint","description":"## Task\nImplement GET /api/floorplan/calibrate endpoint.\n\n## Specification\n- Return current calibration from SQLite\n- Return 404 if no calibration exists\n\n## Acceptance\n- Returns calibration data when present\n- Returns 404 when no calibration exists","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"charlie","owner":"","created_at":"2026-04-07T17:55:53.178762085Z","created_by":"coding","updated_at":"2026-04-07T18:58:38.551564957Z","closed_at":"2026-04-07T18:58:38.551463596Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["mitosis-child","mitosis-depth:1","parent-spaxel-klk"]} +{"id":"spaxel-0fm8","title":"Add quiet hours gate tests","description":"Write tests for quiet hours gate: LOW at 23:00 with 22:00-07:00 quiet hours -> queued, URGENT at 23:00 -> delivered. Acceptance Criteria: Quiet hours tests pass (queueing, bypass).","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","owner":"","created_at":"2026-04-11T08:15:07.990827798Z","created_by":"coding","updated_at":"2026-05-04T06:32:11.422038645Z","closed_at":"2026-05-04T06:32:11.422038645Z","close_reason":"Quiet hours gate tests already exist and pass:\n- TestQuietHoursGate_LowAt23pmQueued: LOW at 23:00 with 22:00-07:00 quiet hours → queued\n- TestQuietHoursGate_UrgentAt23pmDelivered: URGENT at 23:00 → delivered\nAll acceptance criteria verified.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:1","mitosis-child","mitosis-depth:1","parent-spaxel-40tl"]} +{"id":"spaxel-0ii","title":"Implement Zones CRUD REST endpoints","description":"Implement CRUD endpoints for zones: GET/POST /api/zones, PUT/DELETE /api/zones/{id}. Include OpenAPI-style godoc comments. Zone changes must reflect in live 3D view within one WebSocket cycle.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"echo","owner":"","created_at":"2026-04-07T13:56:27.275139529Z","created_by":"coding","updated_at":"2026-04-07T19:01:48.974563569Z","closed_at":"2026-04-07T19:01:48.974408083Z","close_reason":"Zones CRUD REST endpoints already fully implemented: GET/POST /api/zones, PUT/DELETE /api/zones/{id}, GET /api/zones/{id}/history, plus portals CRUD. OpenAPI godoc comments, WebSocket broadcasting for live 3D view, 31 table-driven tests. go vet and go test pass.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:1","mitosis-child","mitosis-depth:1","parent-spaxel-21n"],"dependencies":[{"issue_id":"spaxel-0ii","depends_on_id":"spaxel-3rd","type":"blocks","created_at":"2026-04-07T17:01:33.629176640Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-0ii","depends_on_id":"spaxel-5lo","type":"blocks","created_at":"2026-04-07T17:01:33.542274773Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-0j0k","title":"Implement responsive canvas resize and orientation handling","description":"Handle canvas resize on window resize and orientation change events, including iOS Safari visual viewport quirks and bottom navigation bar spacing.\n\n**Files:** dashboard/js/app.js (resize/orientationchange listeners), dashboard/css/expert.css (canvas height calculation)\n\n**Acceptance Criteria:**\n- Canvas resizes correctly on window resize event\n- Canvas resizes correctly on orientationchange event\n- renderer.setSize() called with updated dimensions\n- camera.aspect updated and camera.updateProjectionMatrix() called\n- Uses visualViewport.width/height on iOS Safari (fallback to window.innerWidth/Height)\n- Canvas height uses calc(100vh - 56px) when simple mode nav is visible","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-11T06:26:50.081049506Z","created_by":"coding","updated_at":"2026-04-11T06:51:20.327121642Z","closed_at":"2026-04-11T06:51:20.327061414Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["mitosis-child","mitosis-depth:1","parent-spaxel-kth"]} +{"id":"spaxel-0n8r","title":"Adopt single design system (Radix dark tokens) across dashboard","description":"## Goal\n\nAll dashboard pages (`index.html`, `ambient.html`, `fleet.html`, `simple.html`, `integrations.html`) share one design system per plan.md §8e.\n\n## Scope\n\nCreate `dashboard/css/tokens.css` with CSS custom properties for:\n\n- **Colors:** Radix `slate` scale (12 steps) as `--slate-1` … `--slate-12`. Accent: Radix `blue` scale. Semantic: `--ok` (green-9), `--warn` (amber-9), `--alert` (red-9).\n- **Typography:** `--font-body`, `--font-mono`. Sizes: `--text-xs` (12), `--text-sm` (14), `--text-base` (16), `--text-lg` (20), `--text-2xl` (28).\n- **Spacing:** `--space-1` (4px) through `--space-16` (64px) on the canonical scale.\n- **Radius:** `--radius-control` (6px), `--radius-card` (10px), `--radius-modal` (16px).\n\nThen refactor each existing CSS file to use these tokens instead of hard-coded values. Delete any light-theme rules; dashboard is dark-only.\n\n`integrations.html` is the reference — its current look already approximates the target. Use it as the acceptance bar for other pages.\n\n## Verification\n\n- `grep -r \"#[0-9a-fA-F]\\{3,6\\}\" dashboard/css/` returns only uses inside `tokens.css`.\n- Playwright screenshots of all five pages show one consistent theme.\n- axe-core reports zero WCAG AA contrast violations.\n\nTraceability: plan.md §8e.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"feature","assignee":"quebec","owner":"","created_at":"2026-04-24T11:13:23.515071217Z","created_by":"coding","updated_at":"2026-04-24T20:48:18.439563795Z","closed_at":"2026-04-24T20:48:18.439456031Z","close_reason":"Verified: design token migration already complete. All 25 non-token CSS files use var(--token-name) exclusively. Zero hard-coded hex/rgba outside tokens.css. Zero light-theme rules. tokens.css contains full Radix dark scale (slate 1-12, blue 1-12, semantic colors, typography, spacing, radius). Prior commits 1e8876d and 07abc03 completed this work.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:50"]} +{"id":"spaxel-0w4","title":"Fleet status page","description":"## Background\n\nThe 3D scene is great for spatial context but poor for bulk fleet management tasks. With 6+ nodes, finding a specific node in the 3D view, checking its firmware version, and triggering an update involves hunting through the scene. The fleet status page provides a flat table view of all nodes with their key metrics and inline actions — the same information you would find in a server management panel, adapted for ESP32 nodes. It complements the 3D view rather than replacing it.\n\n## Fleet Status Table\n\nNew dashboard route: /fleet\n\nThe page layout:\n- Page header: \"Fleet Status\" title, total node count, online count, \"Update All\" button, \"Download report\" button\n- Filter and sort bar (below header)\n- Fleet table (main content)\n\nTable columns:\n1. Checkbox (for multi-select)\n2. Label (editable inline on double-click)\n3. MAC address (truncated, full on hover tooltip)\n4. Status: coloured dot + text. \"Online\" (green) / \"Offline\" (red) / \"Updating\" (yellow spinner)\n5. Firmware version: current version string. If a newer version is in the firmware manifest: version displayed in amber with an \"→ {new_version}\" indicator and an \"Update\" action badge.\n6. Uptime: formatted as \"3d 4h 12m\". Only valid when online.\n7. Current role: TX / RX / TX-RX / Passive. Small badge.\n8. Signal health: composite health score for this node's links as a small colour bar (green → red)\n9. Packet rate: \"{actual} / {configured} Hz\" as a fraction. Colour-coded: > 90% = green, 70-90% = amber, < 70% = red.\n10. Temperature: from health message. Shown as \"{N}°C\" or \"--\" if not reported. Alert colour if > 75°C.\n11. Actions column: [Locate] [OTA] [...more] buttons\n\nThe table rows are clickable (full row click = fly 3D camera to that node's position). Only clicking action buttons or the checkbox should not trigger the row fly-to.\n\n## Inline Label Edit\n\nDouble-clicking the label cell makes it inline-editable:\n- Input field replaces the text\n- Enter to confirm, Escape to cancel\n- Blur (click outside) to confirm\n- On confirm: PATCH /api/nodes/{mac}/label with the new label. Update the display.\n- Validation: max 32 characters, no control characters.\n\n## Action Buttons\n\n\"Locate\" (flash LED): sends a downstream command {\"type\":\"identify\"} to the node via WebSocket. The node flashes its onboard LED rapidly for 5 seconds. The button shows a spinner while the command is in-flight, then a brief green checkmark.\n\n\"OTA\" (firmware update): available only if the node's firmware version != latest in the manifest. Clicking shows a confirmation tooltip: \"Update Node [label] from v{current} to v{latest}? [Confirm] [Cancel]\". On confirm: POST /api/nodes/{mac}/ota. The node's row shows \"Updating\" status and a progress bar (populated from ota_status WebSocket messages for this node).\n\n\"More actions\" (... button): dropdown with: \"Re-assign role\", \"View health history\", \"View event history\", \"Remove from fleet\". Each with an appropriate icon.\n\n## Bulk Actions\n\nCheckbox column allows multi-selecting rows. When any row is selected, a bulk-actions bar slides in above the table:\n- \"Update {N} selected to latest firmware\" — confirms and triggers OTA for all selected nodes in sequence (with 30s stagger)\n- \"Re-assign roles\" — opens the role optimiser with the selected nodes included\n- \"Remove {N} from fleet\" — confirmation required: lists the nodes to be removed\n\nDeselect all: \"Clear selection\" button in the bulk actions bar, or uncheck all checkboxes.\n\n## Camera Fly-To\n\nClicking a table row (non-action click) triggers a smooth camera fly-to the node's position in the expert mode 3D view. The fleet page and expert mode are on different routes, so this requires:\n1. Store the target node MAC in localStorage or URL parameter (\"?highlight={mac}\")\n2. Redirect to the expert mode route / (or open expert mode in a second tab)\n3. On load, if ?highlight={mac} parameter is present, fly camera to that node's position\n\nAlternatively: if the fleet page is opened alongside the expert mode in a split-pane layout (future enhancement), coordinate via a shared state store. For Phase 9, the redirect approach is sufficient.\n\n## Sorting and Filtering\n\nColumn header click: sort by that column. First click = ascending, second = descending. Sort state shown with a small arrow indicator.\n\nFilter row (below column headers, toggle-able with a \"Filter\" button):\n- Label / MAC: text input, filters rows containing the substring\n- Status: dropdown \"All / Online / Offline\"\n- Firmware: dropdown \"All / Outdated only\"\n- Role: multi-select dropdown \"All / TX / RX / TX-RX / Passive\"\n\nActive filters: shown as chips above the table with individual dismiss buttons. \"Clear all filters\" link.\n\n## Download Report\n\n\"Download report\" button: exports the current fleet table (including all filters) as a CSV file. Columns: MAC, label, status, firmware_version, uptime_s, role, health_score, packet_rate_hz, temperature_c, last_seen.\n\nImplemented as a client-side CSV generation from the current table data (no API call needed if data is cached in the dashboard state). Use Blob + URL.createObjectURL for download.\n\n## REST API\n\nGET /api/fleet: returns all provisioned nodes with full details (same as GET /api/nodes but with more fields: uptime, firmware_version, temperature, health_score, packet_rate).\nPATCH /api/nodes/{mac}/label: update label\nPOST /api/nodes/{mac}/locate: send identify command\nPOST /api/nodes/{mac}/role: assign new role\nDELETE /api/nodes/{mac}: remove from fleet (disconnects and archives)\n\n## Files to Create or Modify\n\n- dashboard/fleet.html: fleet page HTML shell\n- dashboard/js/fleet.js: table rendering, sorting, filtering, bulk actions, inline edit\n- dashboard/css/fleet.css: fleet page styles\n- mothership/internal/dashboard/routes.go: fleet-specific API routes, /fleet HTML route\n\n## Tests\n\n- Test fleet table renders correctly with mock data: 4 nodes, verify all columns populated\n- Test inline label edit: double-click cell, type new label, Enter -> PATCH API called with correct body\n- Test bulk selection: check 3 nodes, verify bulk actions bar appears with correct count\n- Test bulk OTA triggers OTA for all 3 selected nodes\n- Test sorting: click \"Firmware version\" header -> rows sorted ascending by version string\n- Test filter: enter \"living\" in label filter -> only rows matching \"living\" visible\n- Test camera fly-to: clicking a row stores MAC in localStorage and redirects to expert mode with ?highlight parameter\n- Test CSV download: verify blob is created with correct headers and values\n\n## Acceptance Criteria\n\n- Fleet page loads with all nodes and their current metrics\n- Inline label edit saves correctly to the API and updates the display\n- Bulk OTA fires for all selected nodes with correct stagger\n- Sorting and filtering work correctly for all columns\n- Camera fly-to positions the 3D view correctly on the selected node after redirect\n- CSV download contains correct headers and all fleet data\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"romeo","owner":"","created_at":"2026-03-28T02:06:06.562532476Z","created_by":"coding","updated_at":"2026-04-25T12:49:56.798295553Z","closed_at":"2026-04-25T12:49:56.798220896Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:33"],"dependencies":[{"issue_id":"spaxel-0w4","depends_on_id":"spaxel-sl2","type":"blocks","created_at":"2026-03-28T03:29:14.955755499Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-16e","title":"Captive portal WiFi recovery","description":"## Background\n\nIf a node loses WiFi connectivity — because a router was replaced, credentials changed, or the node was moved out of range — it currently has no recovery path without physical intervention. The firmware already implements a captive portal AP fallback in firmware/main/wifi.c: after 10 consecutive WiFi connection failures with exponential backoff (up to 30s between attempts), the firmware transitions to AP mode with SSID \"spaxel-{last4mac}\". However, the captive portal HTTP config page served in this mode is currently a stub and needs to be completed. This bead finishes that implementation and adds the mothership-side offline detection and dashboard alert.\n\n## What Already Exists (Phase 1)\n\nfirmware/main/wifi.c implements the WIFI_STATE_CAPTIVE_PORTAL state transition after failure threshold. The firmware starts a SoftAP with SSID \"spaxel-{last4mac}\" and password-free (open AP). A stub HTTP server exists but serves only a 200 OK response with no content. The DNS server for captive portal hijacking is not yet implemented.\n\n## Captive Portal HTTP Server (Firmware)\n\nUse ESP-IDF's esp_http_server component to serve a config page at http://192.168.4.1/ when in AP mode.\n\nThe config page (served as inline HTML, no external resources — it must work fully offline):\n- Shows current provisioned SSID (from NVS, read-only display)\n- Form: new SSID input, password input, optional mothership host/port\n- Submit button: \"Save and Reconnect\"\n- On form submission: firmware writes new credentials to NVS (wifi_ssid, wifi_pass, optionally mothership_host/port) and calls esp_restart()\n- Minimal HTML with inline CSS: max 4KB so it fits in IRAM. No JavaScript needed.\n- Works on iOS Safari (no JS requirement), Android Chrome, desktop browsers\n\nThe HTTP server must handle the iOS and Android captive portal probe paths:\n- GET /hotspot-detect.html (iOS)\n- GET /generate_204 (Android)\n- GET /ncsi.txt (Windows)\nRespond to all of these with a 302 redirect to http://192.168.4.1/ to trigger the OS captive portal popup.\n\n## DNS Hijacking for Captive Portal Detection\n\nModern mobile OSes detect captive portals by making a DNS lookup for a known hostname (connectivitycheck.android.com, captive.apple.com, etc.) and checking if the response is correct. To trigger the OS captive portal popup automatically (so the user does not need to manually navigate to 192.168.4.1):\n\nImplement a minimal DNS server using lwIP's UDP API. The DNS server binds to UDP port 53 on the SoftAP interface (192.168.4.1). It responds to ALL DNS queries with an A record pointing to 192.168.4.1, regardless of the queried hostname. This causes the OS to detect the captive portal and show the popup.\n\nESP-IDF does not provide a ready-made DNS hijacking component — implement using esp_event_loop and lwIP udp_new()/udp_bind()/udp_recv() APIs. Keep the implementation minimal: parse enough of the DNS query to extract the transaction ID and question name, then build a minimal A record response.\n\n## Mothership Offline Detection and Dashboard Alert\n\nThe mothership detects node disconnection via heartbeat timeout. When a node's WebSocket closes (or health messages stop for > 30s), the mothership:\n1. Sets node status to OFFLINE in the node registry\n2. Broadcasts a node_offline WebSocket message to dashboard: {\"type\":\"node_offline\",\"mac\":\"aa:bb:cc:dd:ee:ff\",\"label\":\"Living Room\",\"last_seen\":\"2026-03-27T14:23:00Z\",\"likely_cause\":\"wifi_loss\"}\n\nThe dashboard shows an offline alert card:\n- \"Node [Living Room] went offline at 14:23. It may need WiFi reconfiguration.\"\n- Shows the captive portal AP SSID to connect to: \"Connect to Wi-Fi network 'spaxel-ff01' to reconfigure\"\n- Troubleshooting steps: 1) Check power LED, 2) Check WiFi router is online, 3) Connect to captive portal if LED is blinking\n\nThe likely_cause field is set by the mothership based on the last health message before disconnect (e.g. low WiFi RSSI in last health message -> \"wifi_range\", no recent health messages at all -> \"unknown\").\n\n## Reconnection and Portal Exit\n\nWhen the user submits new credentials in the captive portal and the node reboots:\n1. The SoftAP goes down (existing connections to \"spaxel-ff01\" are dropped)\n2. The node attempts WiFi connection with new credentials\n3. On success, connects to mothership — mothership sets status to ONLINE and broadcasts node_online event\n4. Dashboard shows reconnection notification: \"Node [Living Room] reconnected successfully\"\n\nIf the new credentials also fail (e.g. user mistyped), the node re-enters captive portal mode after another 10 failures. The portal should display an error on re-entry if the previous attempt failed.\n\n## Implementation Files\n\n- firmware/main/wifi.c: complete captive portal HTTP server, add DNS hijacking task\n- firmware/main/captive_portal.c (new): DNS hijacking task + HTTP handler functions\n- mothership/internal/ingestion/server.go: heartbeat timeout detection, node_offline event\n- mothership/internal/fleet/manager.go: node status tracking (ONLINE/OFFLINE/CAPTIVE_PORTAL)\n- dashboard/js/app.js: node_offline event handler, offline alert card rendering\n\n## Testing Challenges\n\nThis feature is difficult to unit test due to hardware dependency. Recommended approaches:\n1. Firmware: Integration test in QEMU (esp32s3 target) — simulate WiFi failures by mocking the WiFi event loop, verify state machine transitions to CAPTIVE_PORTAL after failure threshold\n2. DNS server: Unit test the DNS response builder function with a fixed query buffer and verify the response parses correctly\n3. HTTP config page: Unit test the form handler that writes to NVS (mock the NVS API)\n4. Mothership: Unit test heartbeat timeout detection with a fake time source, verify node_offline event is emitted\n\n## Acceptance Criteria\n\n- After WiFi failure (10 consecutive failures), node enters AP mode with SSID \"spaxel-{last4mac}\" within 5 minutes of initial loss\n- Mobile device connecting to the captive portal AP automatically sees the OS captive portal popup (tested on iOS 16+ and Android 12+)\n- Config page is served and functional without JavaScript\n- Updating credentials causes node to reboot and reconnect to mothership within 60s\n- Dashboard shows offline node alert within 30s of disconnection\n- Reconnection notification appears in dashboard within 30s of node reconnecting\n- Captive portal DNS server responds correctly to all DNS queries with 192.168.4.1\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","owner":"","created_at":"2026-03-28T01:38:15.579673840Z","created_by":"coding","updated_at":"2026-03-28T05:36:39.291892350Z","closed_at":"2026-03-28T05:36:39.291803146Z","close_reason":"Implemented: firmware/main/wifi.c (fb69190, 89 lines added) — captive portal AP mode (spaxel-XXXX SSID), esp_http_server config page at 192.168.4.1, DNS hijacking to trigger OS captive portal popup, NVS credential update on form submission","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","dependencies":[{"issue_id":"spaxel-16e","depends_on_id":"spaxel-uc9","type":"blocks","created_at":"2026-03-28T03:29:13.901241816Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-16z3","title":"Add webhook delivery tests","description":"Write tests for webhook delivery: verifies JSON structure and base64 PNG field. Acceptance Criteria: Webhook delivery tests pass.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-11T08:15:08.130589490Z","created_by":"coding","updated_at":"2026-04-11T09:07:17.379388848Z","closed_at":"2026-04-11T09:07:17.379333570Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:1","mitosis-child","mitosis-depth:1","parent-spaxel-40tl"]} +{"id":"spaxel-17u","title":"Phase 9: UX Polish & Accessibility","description":"Goal: Accessible to every household member. Power user efficiency.\n\nDeliverables:\n- Simple mode (card-based mobile-first UI, room occupancy cards, activity feed)\n- Ambient dashboard mode (/ambient for wall tablets, simplified top-down, auto-dim)\n- Spatial quick actions (right-click context menus on 3D elements, follow camera)\n- Command palette (Ctrl+K universal search/command, fuzzy matching)\n- Morning briefing (daily summary card, push notification option)\n- Guided troubleshooting (proactive contextual help, post-feedback explanations)\n- Mobile-responsive expert mode (touch orbit/pan/zoom)\n- Fleet status page (full table, bulk actions, camera fly-to)\n\nExit criteria: Non-technical user can check occupancy without training. Ambient mode runs 7+ days.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"phase","assignee":"golf","owner":"","created_at":"2026-03-27T01:55:55.188364609Z","created_by":"coding","updated_at":"2026-04-10T09:52:30.053325636Z","closed_at":"2026-04-10T09:52:30.053221884Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:2"],"dependencies":[{"issue_id":"spaxel-17u","depends_on_id":"spaxel-2tlm","type":"blocks","created_at":"2026-04-10T02:03:09.311198456Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-2x1w","type":"blocks","created_at":"2026-04-10T02:03:09.525865595Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-7b3g","type":"blocks","created_at":"2026-04-10T02:03:09.377986112Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-bsek","type":"blocks","created_at":"2026-04-10T02:03:09.442882165Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-duvd","type":"blocks","created_at":"2026-04-10T02:03:09.577884728Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-eelr","type":"blocks","created_at":"2026-04-10T02:03:09.654765280Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-jxru","type":"blocks","created_at":"2026-04-10T02:03:09.755196510Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-sl2","type":"blocks","created_at":"2026-03-28T01:33:53.433780442Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-trsm","type":"blocks","created_at":"2026-04-10T02:03:09.707647462Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-19h","title":"fix: localization/spatial_weights.go unused variable linkID","description":"## Problem\n`internal/localization/spatial_weights.go:601` declares `linkID` but never uses it, causing build failure.\n\n## Fix\nFind line 601 in `mothership/internal/localization/spatial_weights.go` and either:\n- Remove the `linkID` variable assignment, or\n- Use the value (pass it somewhere meaningful), or\n- Replace with blank identifier: `linkID` → `_`\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./internal/localization/\n```\nMust compile with no errors.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-06T22:29:51.085342479Z","created_by":"coding","updated_at":"2026-04-06T22:35:21.502259769Z","closed_at":"2026-04-06T22:35:21.502022402Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":""} +{"id":"spaxel-1l2j","title":"Implement GET /api/events/{id} detail endpoint","description":"Implement GET /api/events/{id} for single event detail. Acceptance Criteria: Returns 404 for non-existent event IDs; Returns full event details for valid IDs.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"hotel","owner":"","created_at":"2026-04-09T18:14:45.476239078Z","created_by":"coding","updated_at":"2026-04-09T19:09:34.130476972Z","closed_at":"2026-04-09T19:09:34.130355809Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["mitosis-child","mitosis-depth:1","parent-spaxel-yeh"]} +{"id":"spaxel-1xt","title":"events: REST API GET /api/events with FTS5 search and cursor pagination","description":"## Overview\nImplement the HTTP handler for the events REST API (part 3 of spaxel-2ap split).\n\n## Endpoint\n`GET /api/events`\n\nQuery params:\n- `limit` (default 50, max 500)\n- `before` (cursor: timestamp_ms as string — keyset pagination, NOT OFFSET)\n- `after` (ISO8601 datetime)\n- `type` (filter by event type)\n- `zone` (filter by zone name)\n- `person` (filter by person label)\n- `q` (FTS5 query — prefix matching, e.g. `q=motion*`)\n\nResponse:\n```json\n{\"events\": [...], \"cursor\": \"1744000000000\", \"has_more\": true, \"total_filtered\": 42}\n```\n\n## Implementation\n- Register at `/api/events` in the existing chi router (requires spaxel-6ha is wired, but can be added independently)\n- FTS5 query via: `SELECT e.* FROM fts_events ft JOIN events e ON e.id = ft.rowid WHERE fts_events MATCH ? ORDER BY e.timestamp_ms DESC LIMIT ?`\n- Keyset: add `AND timestamp_ms < ?` when `before` cursor is provided\n- total_filtered: COUNT from same query without LIMIT\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./...\n# Run mothership and curl:\n# curl 'http://localhost:8080/api/events?limit=10&q=detection*'\n```\n\nRequires: spaxel-4u6 (schema must exist first)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"echo","owner":"","created_at":"2026-04-06T22:31:17.453879797Z","created_by":"coding","updated_at":"2026-04-07T17:27:48.091794931Z","closed_at":"2026-04-07T17:27:48.091693315Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred"],"dependencies":[{"issue_id":"spaxel-1xt","depends_on_id":"spaxel-4u6","type":"blocks","created_at":"2026-04-06T22:31:24.454688305Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-1ydd","title":"Fix viz3d.js duplicate _renderer declaration","description":"## Problem\n\n`js/viz3d.js` declares `_renderer` twice:\n\n- Line 13: `let _scene, _camera, _controls, _clock, _renderer;`\n- Line 30: `let _renderer = null;`\n\nThis throws `SyntaxError: Identifier '_renderer' has already been declared` when the dashboard loads, killing the entire Viz3D IIFE. The cascade failure is what causes the home page's overlapping-panels visual chaos (Playwright review 2026-04-24).\n\n## Fix\n\nDelete line 30 of `js/viz3d.js`. Initialize `_renderer` in the existing declaration on line 13, or leave the declaration and assign later — whichever is consistent with how other private module state is handled in that file.\n\n## Verification\n\n1. Load the dashboard home page; open DevTools console.\n2. Confirm no `SyntaxError: Identifier '_renderer' has already been declared`.\n3. Confirm `Viz3D` global is defined (`typeof Viz3D === 'object'`).\n4. Run existing unit tests: `cd dashboard && npm test` (Jest suite covers viz3d).\n\n## Why this first\n\nThis is the single highest-leverage fix in the UI remediation queue — it unblocks several other visible defects listed in plan.md §8f.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":0,"issue_type":"bug","assignee":"charlie","owner":"","created_at":"2026-04-24T11:13:01.111983612Z","created_by":"coding","updated_at":"2026-04-24T11:46:55.326179839Z","closed_at":"2026-04-24T11:46:55.326103741Z","close_reason":"Fixed duplicate _renderer declaration in dashboard/js/viz3d.js. Removed second 'let _renderer = null' on line 30 that conflicted with line 13. Commit c552ccd. Go tests pass, go vet clean.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:9"]} +{"id":"spaxel-21n","title":"Implement Zones and Portals REST endpoints","description":"Implement CRUD endpoints for zones: GET/POST /api/zones, PUT/DELETE /api/zones/{id}. Implement CRUD for portals: GET/POST /api/portals, PUT/DELETE /api/portals/{id}. Changes must reflect in live 3D view within one WebSocket cycle. Include OpenAPI-style godoc comments.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"echo","owner":"","created_at":"2026-04-06T15:31:10.270709535Z","created_by":"coding","updated_at":"2026-04-07T19:21:31.773484992Z","closed_at":"2026-04-07T19:21:31.773069063Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:1","mitosis-child","mitosis-depth:1","parent-spaxel-6ha"],"dependencies":[{"issue_id":"spaxel-21n","depends_on_id":"spaxel-0ii","type":"blocks","created_at":"2026-04-07T13:56:27.311077260Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-21n","depends_on_id":"spaxel-fi6","type":"blocks","created_at":"2026-04-07T13:56:27.361443212Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-26o","title":"Dashboard presence indicator","description":"## Background\n\nPhase 2 signal processing (phase sanitisation, baseline, motion detection) is complete in mothership/internal/signal/. The pipeline produces per-link MotionState (IsMotion bool, DeltaRMS float64, Confidence float64) via ProcessorManager.GetAllMotionStates(). This bead surfaces that data in the browser dashboard — the first human-visible output of the detection pipeline.\n\n## What to Implement\n\n1. Server-side: Add a periodic broadcast (every 500ms) of all link motion states as a JSON WebSocket message type 'presence_update'. Message schema: {type:'presence_update', links: {linkID: {is_motion: bool, delta_rms: float, confidence: float}}}. Wire into mothership/internal/dashboard/hub.go — Hub already has a Broadcast method.\n\n2. Frontend (dashboard/js/app.js): Add a 'Presence' panel distinct from the raw amplitude bar chart. Per-link rows showing: link ID (nodeMAC:peerMAC abbreviated), coloured circle indicator (green = clear, amber = motion, red = high-confidence motion), deltaRMS value. Click a link row to select it for the amplitude time series.\n\n3. Amplitude time series: Rolling 10s buffer of deltaRMS values per link. Render as a Canvas 2D line chart below the presence panel. X-axis: time (10s window), Y-axis: deltaRMS (0..0.1 typical range). Show threshold line at 0.02 (DefaultDeltaRMSThreshold).\n\n## Key Files\n\n- mothership/internal/signal/processor.go — GetAllMotionStates(), MotionState struct\n- mothership/internal/dashboard/hub.go — Hub.Broadcast(message interface{})\n- mothership/internal/dashboard/server.go — WebSocket handler, periodic broadcast setup\n- dashboard/js/app.js — existing UI code to extend\n\n## Acceptance Criteria\n\n- presence_update messages broadcast every 500ms with all active link states\n- Dashboard shows per-link coloured motion indicator updating in real-time\n- Amplitude time series shows last 10s of deltaRMS for selected link\n- Threshold line visible at 0.02\n- All existing tests pass (cargo check equivalent: go test ./...)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-03-28T03:29:50.484956631Z","created_by":"coding","updated_at":"2026-03-28T03:56:59.340530333Z","closed_at":"2026-03-28T03:56:59.340245276Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred"]} +{"id":"spaxel-288","title":"Add NTP server to provisioning payload","description":"Update mothership provisioning system:\n- Read SPAXEL_NTP_SERVER env var (default: pool.ntp.org)\n- Embed ntp_server field in provisioning payload JSON\n- Support config downstream message field ntp_server to push updated server to nodes\n\nAcceptance: NTP server is configurable via provisioning payload and can be updated via downstream config messages.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-04-07T14:37:00.339721504Z","created_by":"coding","updated_at":"2026-04-07T17:39:12.764197464Z","closed_at":"2026-04-07T17:39:12.764097159Z","close_reason":"NTP server configuration is now fully implemented:\n\n1. SPAXEL_NTP_SERVER env var read with default 'pool.ntp.org'\n2. ntp_server field embedded in provisioning payload JSON\n3. ConfigMessage supports ntp_server field for downstream updates\n4. SendNTPServerToMAC() function available to push NTP server changes to nodes\n\nThe mothership now passes the configured NTP server to nodes during provisioning and can update it at runtime via config messages.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["mitosis-child","mitosis-depth:1","parent-spaxel-u7y"]} +{"id":"spaxel-28j7","title":"Add floor-plan renderer tests","description":"Write tests for floor-plan renderer that verify: renderer produces 300x300 PNG with correct dimensions, zone boundaries appear at correct pixel coordinates, colors are correct. Acceptance Criteria: Renderer tests pass (dimensions, coordinates, colors).","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-11T08:15:07.870662320Z","created_by":"coding","updated_at":"2026-04-11T08:30:13.045511455Z","closed_at":"2026-04-11T08:30:13.045452030Z","close_reason":"Implemented comprehensive floor-plan renderer tests:\n- TestRendererDimensions: Verifies PNG output is 300x300 pixels (and custom sizes)\n- TestZoneBoundariesAtCorrectCoordinates: Verifies zone boundaries appear at correct pixel coordinates\n- TestZoneBoundaryEdges: Verifies zone edge detection by sampling pixels\n- TestPixelColors: Verifies background color, person blob colors, and brightness\n- All renderer tests pass\n\nValidates: 300x300 PNG dimensions, correct zone boundary coordinates, accurate colors","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:1","mitosis-child","mitosis-depth:1","parent-spaxel-40tl"]} +{"id":"spaxel-28k","title":"Add event messages to WebSocket feed","description":"Add 'event' message type to /ws/dashboard for presence transitions, zone entries/exits, and portal crossings. Broadcast: { type: 'event', event: { id, ts, kind, zone, blob_id, person_name } }. Handle in app.js onmessage. Events appear within 1s of zone transition.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"delta","owner":"","created_at":"2026-04-06T14:18:27.377328251Z","created_by":"coding","updated_at":"2026-04-07T15:14:57.597424097Z","closed_at":"2026-04-07T15:14:57.597088117Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:23","mitosis-child","mitosis-depth:1","parent-spaxel-9eg"]} +{"id":"spaxel-2ap","title":"Activity timeline backend: events API, FTS5 search, cursor pagination","description":"## Overview\nBackend for the unified activity timeline — event ingestion, storage, full-text search, and REST API with cursor pagination. Feeds the timeline dashboard UI (spaxel-65k).\n\n## SQLite schema\n- events table: id INTEGER PK, type TEXT, timestamp_ms INTEGER, zone TEXT, person TEXT, blob_id TEXT, detail_json TEXT, severity TEXT\n- FTS5 virtual table fts_events: content='events', content_rowid='id' — indexed on type, zone, person, detail_json\n- events_archive: same schema, auto-migrated from events when timestamp_ms < now - 90 days (nightly at 02:00 local)\n- Indexes: idx_events_ts (timestamp_ms DESC), idx_events_type, idx_events_zone, idx_events_person\n\n## Event types to ingest\ndetection, zone_entry, zone_exit, portal_crossing, trigger_fired, fall_alert, anomaly, security_alert, node_online, node_offline, ota_update, baseline_changed, system, learning_milestone\n\n## REST API\n- GET /api/events — query params: limit (default 50, max 500), before (cursor), after (ISO8601), type, zone, person, q (FTS5 query)\n- Response: {events: [...], cursor: '', has_more: bool, total_filtered: int}\n- Keyset pagination using timestamp_ms as cursor (no OFFSET)\n- FTS5 query via fts_events MATCH q; fuzzy: prefix matching via q*\n\n## Event publishing\n- All event types published to dashboard WS feed as 'event' messages (requires spaxel-9eg)\n- Event bus in Go: internal publish/subscribe so any package can emit events without direct dependency on dashboard\n\n## Acceptance\n- 1000 events query <50ms with FTS5 search\n- Cursor pagination returns consistent results across pages\n- Archive job runs at 02:00 and moves events >90 days\n- FTS5 search matches partial words (prefix mode)\n- Requires: spaxel-6ha (REST API wiring), spaxel-9eg (WS event feed)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"delta","owner":"","created_at":"2026-04-06T13:02:26.845982082Z","created_by":"coding","updated_at":"2026-04-07T19:32:10.083144442Z","closed_at":"2026-04-07T19:32:10.082803006Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["blocked","deferred"],"dependencies":[{"issue_id":"spaxel-2ap","depends_on_id":"spaxel-1xt","type":"blocks","created_at":"2026-04-06T22:31:24.411971684Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-2ap","depends_on_id":"spaxel-4u6","type":"blocks","created_at":"2026-04-06T22:31:24.308501610Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-2ap","depends_on_id":"spaxel-6n9","type":"blocks","created_at":"2026-04-06T22:31:24.359488476Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-2ea","title":"Add alert messages to WebSocket feed","description":"Add 'alert' message type to /ws/dashboard for anomaly detections and security mode triggers. Broadcast: { type: 'alert', alert: { id, ts, severity, description, acknowledged } }. Handle in app.js onmessage.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-06T14:18:27.455727878Z","created_by":"coding","updated_at":"2026-04-07T11:04:25.716894375Z","closed_at":"2026-04-07T11:04:25.716743770Z","close_reason":"Alert message type already fully implemented: BroadcastAlert() in hub.go broadcasts {type:'alert', alert:{id,ts,severity,description,acknowledged}} to /ws/dashboard clients. Called from anomaly detection, security mode changes, and trigger-disabled alerts. Frontend handleAlertMessage() in app.js routes the alert type, shows toast notifications, logs to timeline, and triggers alert banner. Table-driven tests pass (4 cases). go vet clean.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","mitosis-child","mitosis-depth:1","parent-spaxel-9eg"]} +{"id":"spaxel-2tlm","title":"Implement simple mode UI","description":"Card-based mobile-first UI with room occupancy cards and activity feed. Non-technical users should be able to check occupancy without training.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"golf","owner":"","created_at":"2026-04-10T02:03:09.271050009Z","created_by":"coding","updated_at":"2026-04-10T02:08:15.912199673Z","closed_at":"2026-04-10T02:08:15.912102801Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["mitosis-child","mitosis-depth:1","parent-spaxel-17u"]} +{"id":"spaxel-2wg","title":"BLE device registry and labelling","description":"## Background\n\nThe firmware scans BLE advertisements every 5 seconds and relays them to the mothership via the bidirectional protocol (spaxel-o4l, Phase 3). Each BLE relay message contains a list of {mac, name, rssi, manufacturer_data} tuples for all devices heard by that node in the last 5 seconds. Phase 6 turns this raw stream into a structured \"People and Devices\" registry where users can label their devices and associate them with named people. This is the identity layer that transforms anonymous CSI blobs into \"Alice\" and \"Bob\".\n\n## BLE Device Auto-Detection\n\nThe mothership can identify device types from manufacturer data embedded in BLE advertisement packets. The Bluetooth SIG assigns Company IDs to manufacturers; the first 2 bytes of manufacturer_data encode the company ID (little-endian).\n\nCompany IDs to detect:\n- 0x004C (Apple): likely iPhone, iPad, AirPods, or Apple Watch. Sub-type from manufacturer data length and flags.\n- 0x0006 (Microsoft): Windows devices\n- 0x0075 (Samsung): Samsung phones/tablets\n- 0x009E (Fitbit): Fitness trackers\n- 0x0157 (Garmin): GPS watches / fitness devices\n- 0x0059 (Nordic): Tile trackers (Nordic Semiconductor is used by many Tile-like devices)\n- 0x0499 (Ruuvi): Ruuvi temperature/humidity sensors\n- 0x00E0 (Google): Android devices (Nearby Share beacons)\nClassify all others as \"Unknown\". The device name field (if present in the advertisement) provides additional signal.\n\nWearable heuristic: RSSI typically -55 to -75 dBm across multiple nodes with relatively consistent signal (worn close to body). Static devices (speakers, tablets) show higher variance. Flags this heuristic as \"possibly wearable\" (not definitive).\n\n## BLERegistry\n\nNew package: mothership/internal/identity/ble.go\n\nBLERegistry struct: backed by SQLite table ble_devices.\n\nSQLite schema:\nCREATE TABLE ble_devices (\n mac TEXT PRIMARY KEY,\n name TEXT,\n manufacturer TEXT,\n device_type TEXT, -- apple_phone, apple_earbuds, fitbit, garmin, tile, samsung, unknown\n label TEXT, -- user-assigned label\n person_id TEXT, -- FK to people.id\n rssi_min INTEGER,\n rssi_max INTEGER,\n rssi_avg INTEGER,\n first_seen DATETIME,\n last_seen DATETIME,\n is_archived BOOLEAN DEFAULT FALSE,\n last_seen_node_mac TEXT\n);\n\nCREATE TABLE people (\n id TEXT PRIMARY KEY, -- uuid\n name TEXT NOT NULL,\n color TEXT, -- hex colour for dashboard rendering\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE person_devices (\n person_id TEXT,\n device_mac TEXT,\n PRIMARY KEY (person_id, device_mac)\n);\n\nBLERegistry methods:\n- ProcessRelayMessage(nodeMac string, devices []BLEDevice): upsert all devices, update last_seen, update RSSI stats\n- GetDevices(includeArchived bool) []BLEDeviceRecord\n- UpdateLabel(mac, label string) error\n- AssignToPerson(mac, personID string) error\n- CreatePerson(name, color string) (Person, error)\n- GetPeople() []Person\n- ArchiveStale(olderThan time.Duration): set is_archived=true for devices not seen for > olderThan\n\n## BLE MAC Randomisation Handling\n\nModern iPhones and Android phones randomise their BLE MAC address periodically (every 10-15 minutes for iPhones, similar for Android). This is a fundamental privacy feature. The implications for spaxel:\n\n1. The same physical phone appears as multiple different MAC addresses in the registry. The BLERegistry will create new entries for each rotated address.\n2. Long-term tracking of phones by MAC is unreliable. The registry will accumulate many entries for a single phone over time.\n3. Workarounds: (a) Apple uses Resolvable Private Addresses (RPA) that can be resolved with the Identity Resolving Key (IRK) — requires pairing, not available without user action. (b) Device name is sometimes consistent across rotations. (c) Wearable devices (Fitbit, Garmin, AirTag) typically do NOT rotate their MACs — they provide reliable long-term tracking.\n\nThe dashboard must clearly explain this limitation in the \"People and Devices\" panel:\n\"Your phone's Bluetooth address changes regularly for privacy reasons. For reliable person tracking, use a Fitbit, Garmin watch, or AirTag, which have stable addresses.\"\n\nGrouping heuristic: if two devices have the same manufacturer data prefix (first 6 bytes) and name, and were never seen simultaneously at high RSSI from the same node, they are likely the same device with a rotated MAC. Surface this as a \"possible duplicate\" suggestion in the UI: \"These may be the same device: [mac1] and [mac2]. Merge?\"\n\n## REST API\n\nGET /api/ble/devices: returns list of BLEDeviceRecord, optionally filtered by ?archived=true\nGET /api/ble/devices/{mac}: returns single device with full history\nPUT /api/ble/devices/{mac}: update label, device_type, or person assignment. Body: {\"label\":\"Alice's Phone\",\"device_type\":\"apple_phone\",\"person_id\":\"uuid-123\"}\nDELETE /api/ble/devices/{mac}: archive (not hard delete)\n\nGET /api/people: returns list of People with their associated devices\nPOST /api/people: create person. Body: {\"name\":\"Alice\",\"color\":\"#3b82f6\"}\nPUT /api/people/{id}: update name or color\nDELETE /api/people/{id}: soft-delete (retain historical data)\n\n## Dashboard Panel\n\n\"People and Devices\" sidebar panel showing:\n- People section: list of defined people with avatar (initials in circle with their color), device count, last seen time\n - Per person: click to expand, shows associated devices\n - \"Add person\" button opens inline form\n- All devices section (below people): list of devices not yet assigned to a person\n - Per device: device type icon (Apple logo, Fitbit icon, etc.), last seen node (abbreviated), last seen timestamp, RSSI bar\n - Inline label edit on double-click\n - Drag-and-drop to assign to a person card\n - Archive button (hides from active list, accessible via \"Show archived\" toggle)\n- Privacy notice: \"Phones may appear multiple times due to address rotation. Wearables and AirTags have stable addresses.\"\n\n## Tests\n\n- Test device auto-detection: Apple company ID 0x004C -> device_type \"apple_phone\", Fitbit 0x009E -> \"fitbit\"\n- Test that ProcessRelayMessage correctly upserts devices and updates last_seen and RSSI stats\n- Test ArchiveStale marks devices not seen for > 7 days as archived\n- Test person creation and device-to-person assignment API calls\n- Test MAC randomisation handling: two devices with same name and no simultaneous sighting are flagged as possible duplicates\n- Test that archived devices are excluded from GetDevices(false) but included in GetDevices(true)\n\n## Acceptance Criteria\n\n- Discovered BLE devices appear in the dashboard \"People and Devices\" panel within 30 seconds of first observation\n- Device type is auto-detected correctly for Apple, Fitbit, Garmin, and Samsung devices\n- User can assign labels and associate devices with named people via the dashboard UI\n- Drag-and-drop device-to-person assignment works in the UI\n- Devices not seen for > 7 days are automatically archived and hidden from the active list\n- Privacy limitation is clearly documented in the panel UI\n- Possible duplicate MAC-rotated devices are surfaced as merge suggestions\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"juliet","owner":"","created_at":"2026-03-28T01:44:02.204633291Z","created_by":"coding","updated_at":"2026-03-29T18:07:39.656772405Z","closed_at":"2026-03-29T18:07:39.656662663Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred"],"dependencies":[{"issue_id":"spaxel-2wg","depends_on_id":"spaxel-c0q","type":"blocks","created_at":"2026-03-28T03:29:14.172209347Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-2x1w","title":"Implement command palette","description":"Ctrl+K universal search/command with fuzzy matching for power user efficiency.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"quebec","owner":"","created_at":"2026-04-10T02:03:09.482985749Z","created_by":"coding","updated_at":"2026-04-24T21:38:02.866241433Z","closed_at":"2026-04-24T21:38:02.866167874Z","close_reason":"Implemented command palette (Component 34): fuzzy search, 36 commands, time navigation, entity search, recent history, expert-mode gating. 94 tests passing.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:1","mitosis-child","mitosis-depth:1","parent-spaxel-17u"]} +{"id":"spaxel-32o","title":"Link weather diagnostics and repositioning advice","description":"## Background\n\nEven with good hardware and correct placement, some links will chronically underperform. A user who placed a node on a metal shelf, behind a TV, or in a corner will see consistently poor detection without understanding why. Telling users \"your detection quality is low\" is useless without telling them what to do about it. Link weather diagnostics provide root-cause analysis and specific, actionable repositioning advice — including 3D visualisation of why a link is performing poorly and where to move a node to fix it.\n\nThe name \"link weather\" is deliberate: just as weather forecasts present complex atmospheric state in human terms (\"partly cloudy with 60% chance of rain\"), link weather presents complex RF state as: \"Node A to Node B: interference detected. Likely cause: microwave oven or 2.4GHz congestion. Try moving Node B 1.5 metres to the right.\"\n\n## DiagnosticEngine\n\nNew module: mothership/internal/diagnostics/linkweather.go\n\nDiagnosticEngine runs as a background goroutine, consuming link health history from SQLite and emitting Diagnosis structs. It runs a full diagnostic pass every 15 minutes.\n\nA Diagnosis struct contains:\n- LinkID string\n- RuleID string (identifies which rule fired)\n- Severity: INFO, WARNING, ACTIONABLE\n- Title string (human-readable headline)\n- Detail string (explanation of the diagnosis in plain language)\n- Advice string (specific actionable steps)\n- RepositioningTarget *Vec3 (3D position to move the node to, or nil if repositioning is not the solution)\n- RepositioningNodeMAC string (which node to move)\n- ConfidenceScore float64 (how confident the diagnostic engine is in this diagnosis)\n\n## Diagnostic Rules\n\nRule 1: Environmental Change\nTrigger: High baseline drift (>5% per hour) correlated across multiple links simultaneously (>50% of active links).\nTitle: \"Environmental change detected\"\nDetail: \"Multiple sensing links are showing simultaneous baseline shifts. This typically indicates a temperature change, or a large object was moved in the space. The system is adapting automatically.\"\nAdvice: \"No action needed. The baseline will re-stabilise within 30 minutes.\"\nRepositioningTarget: nil\nConfidence: 0.85 if drift is correlated across >50% of links\n\nRule 2: WiFi Congestion or Distance\nTrigger: Packet rate health < 0.8 for more than 10 minutes on a single link.\nTitle: \"Node B has low signal rate\"\nDetail: \"Node [B] is only delivering [N]% of the expected [M] packets per second. The most common causes are distance from the WiFi router or congestion from nearby networks.\"\nAdvice: \"1. Move Node [B] within 10 metres of your WiFi router. 2. If already close, check if the 2.4GHz channel is congested (3+ networks on overlapping channels). 3. ESP32-S3 supports both 2.4GHz and 5GHz — if your router supports 5GHz, update Node B's WiFi config to use the 5GHz SSID.\"\nRepositioningTarget: nil (advice is router proximity, not specific coordinates)\n\nRule 3: Near-Field Metal Interference\nTrigger: Low phase stability (< 0.4) sustained for > 30 minutes during known-quiet periods.\nTitle: \"Metal interference near Node [A]\"\nDetail: \"The sensing link [A to B] has unstable phase measurements even when no one is moving. This is typically caused by metal objects in the near field of the node's antenna (within 10cm): metal shelves, radiators, TV backs, or large appliances.\"\nAdvice: \"Check for metal objects within 10cm of Node [A]. If Node [A] is on a metal surface or shelf, mount it on a non-metal bracket or wall. Try repositioning it 20-30cm away from metal surfaces.\"\nRepositioningTarget: nil (advice is clearance from metal, not a specific position)\n\nRule 4: Fresnel Zone Blockage (Half-Room Dead Zone)\nTrigger: Consistent miss rate (>30% of test walks that should be detected are missed) in a specific area of the room, AND the missing area correlates geometrically with an obstacle in the link's Fresnel zone.\nThis rule requires the feedback loop data (Phase 7, spaxel-i28) — specifically the user-submitted false negatives with position information. If no feedback data is available, this rule can trigger heuristically when one side of the room consistently shows lower blob confidence scores.\nTitle: \"Coverage gap detected — possible obstruction\"\nDetail: \"The area near [zone description] shows lower detection coverage. An obstacle may be blocking the path between Node [A] and Node [B], interrupting their sensing zone.\"\nAdvice: \"Move Node [B] [direction] by approximately [distance] to restore coverage. The target position is marked in green in the 3D view.\"\nRepositioningTarget: computed_optimal_position (see below)\n\nRule 5: Periodic Interference Spikes\nTrigger: Periodic spikes in deltaRMS variance (3-10 events per hour, each lasting 1-3 minutes) not correlated with occupancy data (no people detected moving).\nTitle: \"Periodic interference detected\"\nDetail: \"Node [A] to Node [B] is experiencing regular interference bursts [N] times per hour. This pattern is consistent with a microwave oven, a cordless phone, or a pulsed 2.4GHz source.\"\nAdvice: \"Consider the following: 1. Is Node [A] or Node [B] near a kitchen? Microwave ovens cause strong 2.4GHz interference. 2. A cordless DECT phone or baby monitor near one of the nodes may be the source. 3. Try moving the affected node at least 2 metres from any 2.4GHz appliances.\"\nRepositioningTarget: nil (interference is appliance-specific)\n\n## Repositioning Advice in 3D\n\nFor Rule 4 (Fresnel zone blockage), compute the optimal repositioning target:\n1. Use the GDOP-based coverage optimiser from Phase 5 self-healing fleet (spaxel-jc4) to compute the position that maximises GDOP for the blocked zone while keeping all other nodes fixed.\n2. The optimal position is the computed_optimal_position Vec3.\n3. In the 3D dashboard, render a \"ghost\" node at this position: translucent version of the node mesh, with a dashed line from the current position to the ghost position.\n4. Show expected GDOP improvement: \"Moving Node B here would improve detection in the east corner from [N]% to [M]%.\"\n\n## Weekly Reliability Trends\n\nStore daily health score averages in SQLite: link_health_daily (link_id TEXT, date DATE, avg_health REAL, min_health REAL, max_health REAL, PRIMARY KEY (link_id, date)).\n\nA background job runs daily at midnight and writes the day's health averages from the link health log (link_health_log table: link_id, timestamp, composite_score).\n\nDashboard shows for each link: 7-day sparkline of daily average health score. \"Best day\" annotation (highest average) and \"worst day\" annotation (lowest average). This gives users a sense of long-term reliability.\n\n## Files to Create or Modify\n\n- mothership/internal/diagnostics/linkweather.go: DiagnosticEngine and all 5 rules\n- mothership/internal/diagnostics/reposition.go: repositioning target computation\n- mothership/internal/health/linkhealth.go: add link_health_log table writes\n- dashboard/js/linkhealth.js: link health panel, diagnostics display, ghost node rendering\n- mothership/internal/dashboard/routes.go: GET /api/links/{id}/diagnostics, GET /api/links/{id}/health-history\n\n## Tests\n\n- Test Rule 1 (environmental change): inject simultaneous high-drift events across 60% of links, verify diagnosis fires with Severity=INFO\n- Test Rule 2 (WiFi congestion): inject packet_rate=0.7 for 15 minutes, verify diagnosis fires with appropriate advice text\n- Test Rule 3 (metal interference): inject phase_stability=0.3 for 35 minutes during a quiet window, verify diagnosis fires\n- Test Rule 4 (Fresnel blockage): requires feedback data — inject synthetic false-negative feedback events clustered in one spatial zone, verify diagnosis fires and RepositioningTarget is non-nil\n- Test Rule 5 (periodic interference): inject 5 deltaRMS variance spikes per hour for 2 hours, verify diagnosis fires with correct periodicity estimate\n- Test weekly trend aggregation: inject 7 days of health scores, verify daily averages are correctly computed and stored\n- Test that repositioning target is within room bounds and improves GDOP\n\n## Acceptance Criteria\n\n- All 5 diagnostic rules fire correctly on synthetic test data that matches their trigger conditions\n- Repositioning advice for Rule 4 appears as a ghost node in the 3D dashboard view\n- Expected GDOP improvement shown alongside repositioning ghost node\n- Weekly 7-day sparkline visible in link health panel for each link\n- Diagnostics accessible via API and displayed in Link Health panel on link click\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"juliet","owner":"","created_at":"2026-03-28T01:43:13.596164634Z","created_by":"coding","updated_at":"2026-03-29T18:07:39.683230580Z","closed_at":"2026-03-29T18:07:39.683089345Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred"],"dependencies":[{"issue_id":"spaxel-32o","depends_on_id":"spaxel-axa","type":"blocks","created_at":"2026-03-28T03:29:14.023730499Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-35lb","title":"Add test-notification endpoint integration test","description":"Write integration test for test-notification endpoint fires correctly. Acceptance Criteria: Test endpoint integration test passes.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-11T08:15:08.173375326Z","created_by":"coding","updated_at":"2026-04-11T10:40:05.106805090Z","closed_at":"2026-04-11T10:40:05.106665541Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:13","mitosis-child","mitosis-depth:1","parent-spaxel-40tl"]} +{"id":"spaxel-3ca","title":"Add time-travel debugging","description":"Implement:\n- Pause live mode\n- Timeline scrubbing\n- Replay 3D from recorded CSI data\n\nAcceptance: Can replay 24 hours of historical data with full 3D visualization.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"golf","owner":"","created_at":"2026-04-09T14:54:38.737598265Z","created_by":"coding","updated_at":"2026-04-09T17:41:51.707526513Z","closed_at":"2026-04-09T17:41:51.707468066Z","close_reason":"Time-travel debugging implementation complete. All acceptance criteria met:\n- Pause live mode: Implemented in dashboard/js/replay.js with Pause Live button\n- Timeline scrubbing: Full scrubber UI with seek functionality \n- Replay 3D from recorded CSI: Viz3D integration with enterReplayMode/exitReplayMode/updateReplayBlobs\n- 24-hour replay: Recording buffer supports 48-hour retention (exceeds requirement)\n\nBackend (mothership/internal/api/replay.go, replay/worker.go):\n- REST API for session management (start, stop, seek, tune, set-speed, set-state)\n- Separate signal processing pipeline for replay\n- Blob broadcasting to dashboard\n\nFrontend (dashboard/js/replay.js):\n- Complete replay controls UI\n- Parameter tuning panel with instant preview\n- Timeline loop polling session state\n\n3D Visualization (dashboard/js/viz3d.js):\n- Stores/restores live blob states during replay transitions\n- Full 3D blob rendering from replay data\n\nVerification: Comprehensive test suite exists (replay_test.go) covering session lifecycle, multiple sessions, parameter tuning, and timestamp parsing.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:20","mitosis-child","mitosis-depth:1","parent-spaxel-sl2"]} +{"id":"spaxel-3ps","title":"Detection feedback loop and accuracy tracking","description":"## Background\n\nEvery detection algorithm produces errors. False positives (detected presence when no one is there) are annoying and erode trust. False negatives (missed detection of a real person) are dangerous for safety applications. The feedback loop gives users a direct mechanism to correct errors and the system learns from those corrections. Showing users measurable improvement over time (\"You've provided 47 corrections. Accuracy improved 12% this week\") creates a virtuous engagement loop and transforms users into active participants in improving the system.\n\n## Feedback UI Elements\n\nEvery detection event exposed to the user should have feedback affordances. Three contexts:\n\n1. Dashboard 3D view: Each active track has a small thumbs-up/down icon that appears on hover/focus. Clicking thumbs-down opens a quick inline form.\n\n2. Activity timeline (Phase 8): Every detection event entry has thumbs-up/thumbs-down at the end of the row. Space-efficient: 2 icon buttons.\n\n3. Push notifications: Fall and anomaly notifications include a quick-reply option (via ntfy actions or Pushover callbacks): \"False alarm — clear this.\"\n\n4. \"I was here and wasn't detected\" button: On the timeline panel, a button \"Report missed detection\" opens a form: \"When? [time picker, default: now]\", \"Where? [zone picker]\", \"Who? [person picker, optional]\". Submits as a FALSE_NEGATIVE feedback event with the user-provided position.\n\nFeedback form for thumbs-down:\n- \"What was wrong?\" (radio buttons):\n - \"No one was there (false alarm)\"\n - \"Someone was missed at this location\"\n - \"Wrong person identified\"\n - \"Wrong zone/location\"\n- Optional free-text \"Notes\" field\n- Submit / Cancel\n\n## Feedback Storage\n\nSQLite schema:\nCREATE TABLE detection_feedback (\n id TEXT PRIMARY KEY,\n event_id TEXT, -- references events table (activity timeline)\n event_type TEXT, -- \"blob_detection\", \"zone_transition\", \"fall_alert\", \"anomaly\"\n feedback_type TEXT, -- \"TRUE_POSITIVE\", \"FALSE_POSITIVE\", \"FALSE_NEGATIVE\", \"WRONG_IDENTITY\", \"WRONG_ZONE\"\n details_json TEXT, -- {\"zone_id\":\"...\", \"person_id\":\"...\", \"notes\":\"...\"}\n timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,\n applied BOOLEAN DEFAULT FALSE, -- set to TRUE after weight refinement processes it\n processed_at DATETIME\n);\n\nThe applied flag enables incremental processing: the weight learner (Phase 7 self-improving localisation) queries WHERE applied = FALSE, processes batches, and marks them TRUE.\n\n## Accuracy Metrics\n\nCompute precision/recall/F1 per link, per zone, and per person weekly. This requires knowing the true positives, false positives, and false negatives.\n\nGround truth sources:\n- User thumbs-up -> TRUE_POSITIVE for the corresponding detection event\n- User thumbs-down (false alarm) -> FALSE_POSITIVE for the detection event\n- User \"missed detection\" report -> FALSE_NEGATIVE for the reported time/zone\n\nNote: ground truth is sparse — users will not feedback every event. We use the feedback we have as a sample. Assume events without feedback are TRUE_POSITIVE for the purpose of precision estimates (conservative: this means precision is an upper bound, not exact).\n\nMetrics computed weekly:\n- precision = TP / (TP + FP) — of all detections, what fraction were correct\n- recall = TP / (TP + FN) — of all true presence events, what fraction were detected\n- F1 = 2 * precision * recall / (precision + recall)\n- Per-link metrics: which links have the most false positives (worst precision)\n- Per-zone metrics: which zones are most often missed (worst recall)\n\nStorage: detection_accuracy (week TEXT, scope_type TEXT, scope_id TEXT, precision REAL, recall REAL, f1 REAL, tp_count INT, fp_count INT, fn_count INT, computed_at DATETIME). Scope types: \"system\", \"link\", \"zone\", \"person\".\n\n## Accuracy Trend Display\n\nDashboard \"Accuracy\" panel (in expert mode):\n- Overall accuracy gauge: composite F1 score as a circular gauge (0-100%)\n- Week-over-week trend graph: sparkline of weekly F1 over the last 8 weeks\n- \"You've provided N corrections. Your accuracy improved X% this week.\" — motivational counter\n- Per-zone breakdown: bar chart of precision/recall per zone (click a zone bar to jump to it in 3D view)\n- Per-link breakdown: link health vs. feedback score correlation (are high-health links also high-accuracy?)\n- Feedback count: total corrections given, open corrections (not yet processed), processed corrections\n\nThe accuracy trend display intentionally shows the improvement trajectory, not just the absolute value, to reinforce that feedback has an effect.\n\n## Feedback Application\n\nProcessing happens in a background goroutine (mothership/internal/learning/feedback_processor.go) that runs every 6 hours or when triggered manually.\n\nFor FALSE_POSITIVE events with associated CSI data (in the recording buffer from Phase 2):\n- Retrieve the CSI data from the recording buffer at the event timestamp for all links\n- Add the CSI frame data to a \"known false positive\" set in SQLite: false_positive_frames (link_id, timestamp, delta_rms, context_json)\n- The weight learner (self-improving localisation bead) uses this set as negative examples\n\nFor FALSE_NEGATIVE events with user-reported position:\n- Add to \"known false negative\" set: false_negative_frames (link_id, timestamp, expected_position_xyz, context_json)\n- The weight learner uses this as a positive example at the specified position\n\nAfter processing, mark feedback.applied = TRUE.\n\n## Files to Create or Modify\n\n- mothership/internal/learning/feedback_processor.go: feedback processing pipeline\n- mothership/internal/analytics/accuracy.go: weekly metric computation\n- dashboard/js/feedback.js: thumbs-up/down UI components (reusable across 3D view and timeline)\n- dashboard/js/accuracy.js: Accuracy panel rendering\n- mothership/internal/dashboard/routes.go: POST /api/feedback, GET /api/accuracy\n\n## Tests\n\n- Test feedback storage: POST /api/feedback with each feedback_type, verify SQLite record created\n- Test accuracy metric computation with synthetic TP/FP/FN data: 8 TP, 2 FP, 1 FN -> precision=0.8, recall=0.888\n- Test weekly rollup: 7 days of daily feedback -> correctly aggregated weekly metric\n- Test that applied=false events are found and marked as applied after processor run\n- Test \"improvements\" counter: feedback_count increases on each POST /api/feedback call\n\n## Acceptance Criteria\n\n- Thumbs-up/down buttons appear on active tracks in 3D view and on all timeline events\n- \"Missed detection\" button and form available in timeline panel\n- Feedback stored in SQLite with correct feedback_type and details\n- Accuracy metrics computed weekly and stored in detection_accuracy table\n- Accuracy panel shows week-over-week trend (requires at least 2 weeks of data)\n- Feedback improvement counter shows correct counts\n- Applied flag correctly set after processor run\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"sp4","owner":"","created_at":"2026-03-28T01:49:50.419277632Z","created_by":"coding","updated_at":"2026-03-29T22:08:03.778130122Z","closed_at":"2026-03-29T22:08:03.778000167Z","close_reason":"Implementation complete: feedback storage (SQLite), accuracy computation (precision/recall/F1 weekly), feedback processor (6h interval), API endpoints (/api/learning/*), frontend feedback UI (thumbs up/down, missed detection form), accuracy panel (F1 gauge, sparkline, per-zone breakdown). All 12 tests pass.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:1"],"dependencies":[{"issue_id":"spaxel-3ps","depends_on_id":"spaxel-zvs","type":"blocks","created_at":"2026-03-28T03:29:14.442377218Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-3rd","title":"Wire WebSocket integration for zone changes","description":"Ensure zone changes from CRUD endpoints reflect in live 3D view within one WebSocket cycle. Acceptance: creating/updating/deleting a zone via REST API triggers an update broadcast through the WebSocket system.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"echo","owner":"","created_at":"2026-04-07T17:01:33.587080369Z","created_by":"coding","updated_at":"2026-04-07T18:42:55.455708044Z","closed_at":"2026-04-07T18:42:55.455446177Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","mitosis-child","mitosis-depth:1","parent-spaxel-0ii"]} +{"id":"spaxel-403","title":"Implement anomaly detection & security mode","description":"Build pattern learning and anomaly detection for security.\n\nDeliverables:\n- 7-day pattern learning algorithm\n- Anomaly scoring against learned patterns\n- Security mode integration\n\nAcceptance: System detects deviations from learned patterns; accuracy improves measurably over 4 weeks.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"golf","owner":"","created_at":"2026-03-29T19:25:04.187535979Z","created_by":"coding","updated_at":"2026-04-09T12:18:14.752621360Z","closed_at":"2026-04-09T12:18:14.752279788Z","close_reason":"Anomaly detection & security mode implementation verified complete.\n\nDeliverables implemented:\n- 7-day pattern learning algorithm with Welford's online algorithm (analytics/patterns.go)\n- Anomaly scoring against learned patterns with z-score based computation\n- Security mode integration with Armed/Disarmed/ArmedStay states\n\nAcceptance criteria met:\n- System detects deviations from learned patterns via multiple anomaly types (UnusualHour, UnknownBLE, MotionDuringAway, UnusualDwell)\n- Accuracy improves measurably through feedback loop integration with learning/feedback_store\n\nKey components:\n- PatternLearner: 7-day cold start, hourly pattern updates, per-slot readiness checking\n- Detector: Multiple anomaly types, configurable thresholds, alert chain with timers\n- Security API: /api/security/arm, /api/security/disarm, /api/security/status\n- Alert Handler: Dashboard → webhook → escalation notification chain\n- Integration: Fully wired in main.go with zones, BLE registry, dashboard, and feedback store","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:922","mitosis-child","mitosis-depth:1","parent-spaxel-i28"]} +{"id":"spaxel-40tl","title":"Write comprehensive tests for notification system","description":"Add test files for all notification components. Tests must cover: floor-plan renderer produces 300x300 PNG with correct dimensions, zone boundaries appear at correct pixel coordinates, batching behavior (3 LOW events in 10s -> 1 notification, 1 URGENT -> immediate), quiet hours gate (LOW at 23:00 with 22:00-07:00 quiet hours -> queued, URGENT at 23:00 -> delivered), morning digest delivery bundles queued events at quiet_hours_end, ntfy delivery with mock HTTP server verifies headers/body, webhook delivery verifies JSON structure and base64 PNG field, test-notification endpoint fires correctly.\n\nAcceptance Criteria:\n- All renderer tests pass (dimensions, coordinates, colors)\n- All batching tests pass (windowing, priority bypass)\n- All quiet hours tests pass (queueing, bypass, digest)\n- All delivery client tests pass with mocks\n- Test endpoint integration test passes\n- Test coverage >= 80% for notification packages","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","owner":"","created_at":"2026-04-10T12:19:08.646045806Z","created_by":"coding","updated_at":"2026-05-04T07:01:25.142023082Z","closed_at":"2026-05-04T07:01:25.142023082Z","close_reason":"Added comprehensive tests for notification system - Floor plan renderer tests - Batching behavior tests - Quiet hours tests - Delivery client tests - Test endpoint integration - Test coverage: 87.8%","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:8","mitosis-child","mitosis-depth:1","parent-spaxel-zpt"],"dependencies":[{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-0fm8","type":"blocks","created_at":"2026-04-11T08:15:08.008025729Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-16z3","type":"blocks","created_at":"2026-04-11T08:15:08.148112051Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-28j7","type":"blocks","created_at":"2026-04-11T08:15:07.907058347Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-35lb","type":"blocks","created_at":"2026-04-11T08:15:08.208324942Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-4frg","type":"blocks","created_at":"2026-04-11T08:15:08.056467899Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-k0rs","type":"blocks","created_at":"2026-04-11T08:15:07.972516975Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-wekq","type":"blocks","created_at":"2026-04-11T08:15:08.101758877Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-4fg","title":"Implement Replay/Time-Travel REST endpoints","description":"Implement GET /api/replay/sessions to list recording sessions. Add POST endpoints: /api/replay/start to start replay at timestamp, /api/replay/stop to return to live, /api/replay/seek to seek within session, /api/replay/tune to update pipeline parameters mid-replay. Include OpenAPI-style godoc comments.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-04-06T15:31:10.497876498Z","created_by":"coding","updated_at":"2026-04-07T13:20:09.903154198Z","closed_at":"2026-04-07T13:20:09.902983511Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["mitosis-child","mitosis-depth:1","parent-spaxel-6ha"]} +{"id":"spaxel-4frg","title":"Add morning digest tests","description":"Write tests for morning digest delivery: bundles queued events at quiet_hours_end. Acceptance Criteria: Morning digest tests pass.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-04-11T08:15:08.033947792Z","created_by":"coding","updated_at":"2026-04-11T09:24:34.821263381Z","closed_at":"2026-04-11T09:24:34.821202594Z","close_reason":"Morning digest tests implemented and passing. All 8 morning digest tests pass covering queuing at quiet_hours_end, disabled behavior, once-per-day, empty handling, event inclusion, queue clearing, mixed priorities, and title format. Acceptance criteria met.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:3","mitosis-child","mitosis-depth:1","parent-spaxel-40tl"]} +{"id":"spaxel-4u6","title":"events: SQLite schema, FTS5 table, indexes, and 90-day archive job","description":"## Overview\nCreate the SQLite storage layer for the unified activity timeline (part 1 of spaxel-2ap split).\n\n## Schema to create in mothership/internal/events/ (db setup or migration)\n```sql\nCREATE TABLE IF NOT EXISTS events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n type TEXT NOT NULL,\n timestamp_ms INTEGER NOT NULL,\n zone TEXT,\n person TEXT,\n blob_id TEXT,\n detail_json TEXT,\n severity TEXT\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS fts_events USING fts5(\n type, zone, person, detail_json,\n content='events', content_rowid='id'\n);\n\nCREATE INDEX IF NOT EXISTS idx_events_ts ON events(timestamp_ms DESC);\nCREATE INDEX IF NOT EXISTS idx_events_type ON events(type);\nCREATE INDEX IF NOT EXISTS idx_events_zone ON events(zone);\nCREATE INDEX IF NOT EXISTS idx_events_person ON events(person);\n\nCREATE TABLE IF NOT EXISTS events_archive (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n type TEXT NOT NULL,\n timestamp_ms INTEGER NOT NULL,\n zone TEXT, person TEXT, blob_id TEXT, detail_json TEXT, severity TEXT\n);\n```\n\n## Archive job\n- In `events` package, add `RunArchiveJob(db *sql.DB)` that runs nightly at 02:00 local time\n- Migrates rows from `events` where `timestamp_ms < now - 90 days` into `events_archive`\n- Deletes moved rows from `events`\n\n## Go types\n```go\ntype Event struct {\n ID int64\n Type string\n TimestampMs int64\n Zone string\n Person string\n BlobID string\n DetailJSON string\n Severity string\n}\n\nfunc InsertEvent(db *sql.DB, e Event) error\nfunc QueryEvents(db *sql.DB, params QueryParams) ([]Event, string, bool, error)\n```\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./internal/events/\n```","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"charlie","owner":"","created_at":"2026-04-06T22:30:57.090344045Z","created_by":"coding","updated_at":"2026-04-07T16:45:36.428356135Z","closed_at":"2026-04-07T16:45:36.428249897Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:2"]} +{"id":"spaxel-51k","title":"OTA firmware update system","description":"## Background\n\nOnce nodes are deployed in a home, they need to be updated without physical access. ESP-IDF has a mature OTA (Over-The-Air) update mechanism: two OTA flash partitions (factory, ota_0, ota_1 as defined in firmware/partitions.csv), HTTP download to the inactive partition, cryptographic verification, set boot partition, reboot. The mothership serves firmware binaries and triggers the update via a WebSocket downstream command. Phase 1 laid the groundwork in the firmware; this bead completes the mothership side.\n\n## What Already Exists\n\nfirmware/main/websocket.c has OTA command handling and an ota_download_task that handles the HTTP download to the inactive OTA partition. The partition table in firmware/partitions.csv has factory + ota_0 + ota_1 slots. The firmware parses {type:\"ota\", url:\"...\", md5:\"...\", version:\"...\"} downstream commands and initiates the download. What is missing is:\n- The mothership HTTP server for firmware binary serving\n- The REST API for triggering OTA per-node or fleet-wide\n- The firmware manifest for version management\n- The rollback detection logic on the mothership side\n- The dashboard UI for OTA management\n\n## Mothership Firmware Serving\n\nGET /firmware/latest or GET /firmware/{version}: serves the compiled .bin file from the /firmware volume mount. The response must include:\n- Content-Length header (required by ESP-IDF OTA HTTP client for progress reporting)\n- ETag header (MD5 of the binary, for caching)\n- Content-Type: application/octet-stream\n\nFirmware binaries are placed in the /firmware volume mount (configured in docker-compose.yml or k8s volume mount). The mothership reads the firmware manifest on startup and re-reads it when a new file appears (inotify watch or periodic re-scan every 60s).\n\n## Firmware Manifest\n\nFile: /firmware/manifest.json — auto-generated by the CI build process, or manually created.\nFormat: list of objects, each with version (semver string), filename (basename within /firmware/), md5 (hex string of binary MD5), size (integer bytes), build_timestamp (ISO8601).\nThe mothership's \"latest\" version is determined by sorting manifest entries by semver and taking the highest.\n\n## OTA Trigger API\n\nPOST /api/nodes/{mac}/ota — trigger OTA for a specific node\nRequest body: {\"version\": \"0.2.0\"} (optional; defaults to latest if omitted)\nResponse: {\"job_id\": \"abc123\", \"node_mac\": \"aa:bb:cc:dd:ee:ff\", \"target_version\": \"0.2.0\", \"status\": \"initiated\"}\n\nThe mothership looks up the node's active WebSocket session in the connection registry and sends the OTA command:\n{\"type\":\"ota\",\"url\":\"http://{mothership_ip}:{port}/firmware/0.2.0\",\"md5\":\"{hex_md5}\",\"version\":\"0.2.0\"}\n\nThe firmware immediately begins the download in a background task, sends ota_status messages ({\"type\":\"ota_status\",\"progress\":45,\"status\":\"downloading\"}) which the mothership logs and broadcasts to the dashboard.\n\n## Fleet Rolling Update\n\nPOST /api/ota/fleet — trigger OTA for all connected nodes\nRequest body: {\"version\": \"0.2.0\", \"stagger_seconds\": 30} (default stagger: 30s)\n\nThe rolling update coordinator in the mothership triggers OTA for the first node, waits stagger_seconds, then the next, and so on. This ensures:\n- Not all nodes reboot simultaneously (avoids a coverage gap window)\n- If a node fails OTA, the remaining nodes can be halted before more disruption\n- Fleet update progress is visible in dashboard per-node\n\nThe fleet update job is stored in SQLite (ota_jobs table) and survives mothership restarts.\n\n## Rollback Detection\n\nESP-IDF automatically rolls back to the previous firmware if the new image does not call esp_ota_mark_app_valid_cancel_rollback() within a boot window (the firmware does this on successful WebSocket connection to the mothership). The mothership detects rollback by comparing the firmware_version field in the hello message after OTA against the requested target version. If they differ, the mothership logs an OTA rollback event and updates the node's status to \"rollback\".\n\n## Dashboard OTA UI\n\nAdd an OTA panel to the dashboard settings or fleet page:\n- Per-node: current firmware version, available version (if newer), \"Update\" button\n- Fleet: \"Update All\" button with stagger slider, progress per node (with percentage from ota_status messages), last updated time per node\n- Version history: per-node firmware version history in tooltip or expandable row\n- Rollback indicator: nodes that rolled back are highlighted with a warning and the reason (if known)\n\n## Files to Create or Modify\n\n- mothership/internal/ota/server.go: firmware file serving with Content-Length and ETag\n- mothership/internal/ota/manifest.go: manifest parsing and latest-version logic\n- mothership/internal/ota/jobs.go: OTA job creation, fleet rolling update coordinator, status tracking\n- mothership/internal/dashboard/routes.go: register OTA API routes\n- dashboard/js/fleet.js or dashboard/js/ota.js: OTA UI panel\n\n## Tests\n\n- Test OTA command JSON serialisation matches firmware's expected format exactly\n- Test rolling update stagger timing with a mock time source (use a clock interface for testability)\n- Test that firmware version in hello message is parsed and stored in the node registry\n- Test manifest parsing: valid manifest, empty manifest, malformed manifest\n- Test rollback detection when hello version does not match target version\n\n## Acceptance Criteria\n\n- OTA command reaches firmware and triggers download (verified via ota_status messages)\n- Rolling update staggers correctly with the configured delay between nodes\n- After successful OTA, node reconnects with new firmware version in hello message\n- Rollback is detectable via hello version mismatch and displayed in dashboard\n- MD5 verification failure in firmware logs an error and the old firmware remains running\n- Fleet update status visible per-node in dashboard\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","owner":"","created_at":"2026-03-28T01:37:32.472078279Z","created_by":"coding","updated_at":"2026-03-28T05:36:39.250035631Z","closed_at":"2026-03-28T05:36:39.249972673Z","close_reason":"Implemented: ota/manager.go + ota/server.go (fb69190) — HTTP firmware serving from /firmware volume, WebSocket-triggered OTA command, rolling update with 30s stagger, MD5 verification, firmware manifest.json, rollback detection via hello version mismatch","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","dependencies":[{"issue_id":"spaxel-51k","depends_on_id":"spaxel-uc9","type":"blocks","created_at":"2026-03-28T03:29:13.874999678Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-54i","title":"load-shedding: add per-iteration timing and rolling avg to ProcessorManager","description":"## Task\nAdd pipeline timing and a 5-iteration rolling average to `ProcessorManager` in `mothership/internal/signal/processor.go`.\n\n## Changes to ProcessorManager struct (lines 221-228)\nAdd these fields:\n```go\ntype ProcessorManager struct {\n // ... existing fields ...\n iterDurations [5]time.Duration // ring buffer for last 5 iteration times\n iterIdx int // next write index (mod 5)\n iterCount int // how many values filled (0-5)\n shedLevel int // current load shedding level (0-3)\n steadyCount int // consecutive iters below recovery threshold\n}\n```\n\n## Changes to LinkProcessor.Process() (line 54)\nWrap the entire Process body to time it. At the START of Process():\n```go\nt0 := time.Now()\n```\nAt the END of Process(), before return, update the manager's ring buffer. BUT — Process is on LinkProcessor, not ProcessorManager. So instead, add timing to ProcessorManager.Process() (line 252):\n\n```go\nfunc (pm *ProcessorManager) Process(linkID string, ...) (*ProcessResult, error) {\n t0 := time.Now()\n // ... existing lock + delegate to processor ...\n result, err := processor.Process(payload, rssiDBm, nSub, recvTime)\n pm.mu.Unlock() // already have write lock\n elapsed := time.Since(t0)\n pm.updateShedding(elapsed)\n return result, err\n}\n```\n\n## New method updateShedding(elapsed time.Duration)\n```go\nfunc (pm *ProcessorManager) updateShedding(elapsed time.Duration) {\n pm.iterDurations[pm.iterIdx%5] = elapsed\n pm.iterIdx++\n if pm.iterCount < 5 { pm.iterCount++ }\n\n // compute rolling avg\n var sum time.Duration\n for i := 0; i < pm.iterCount; i++ {\n sum += pm.iterDurations[i]\n }\n avg := sum / time.Duration(pm.iterCount)\n\n // level up\n if avg >= 95*time.Millisecond && pm.shedLevel < 3 {\n pm.shedLevel = 3; pm.steadyCount = 0\n } else if avg >= 90*time.Millisecond && pm.shedLevel < 2 {\n pm.shedLevel = 2; pm.steadyCount = 0\n } else if avg >= 80*time.Millisecond && pm.shedLevel < 1 {\n pm.shedLevel = 1; pm.steadyCount = 0\n }\n\n // recovery: step down one level when avg < 60ms for 10 consecutive iters\n if avg < 60*time.Millisecond {\n pm.steadyCount++\n if pm.steadyCount >= 10 && pm.shedLevel > 0 {\n pm.shedLevel--\n pm.steadyCount = 0\n }\n } else {\n pm.steadyCount = 0\n }\n}\n```\n\n## New getter\n```go\nfunc (pm *ProcessorManager) GetShedLevel() int {\n pm.mu.RLock()\n defer pm.mu.RUnlock()\n return pm.shedLevel\n}\n```\n\nNote: `updateShedding` must NOT hold `pm.mu` because it's called after unlock. The iterDurations ring buffer is only written from `Process` so it is already serialized by the caller's lock sequence. Add a separate `mu` for the shed state or call updateShedding while still holding pm.mu — simplest: call it BEFORE Unlock, while still holding the write lock.\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./...\n```","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-07T06:33:03.697771676Z","created_by":"coding","updated_at":"2026-04-07T16:53:42.209613205Z","closed_at":"2026-04-07T16:53:42.209404722Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":""} +{"id":"spaxel-5a3","title":"Add Fresnel zone visualization","description":"Implement wireframe ellipsoid overlay showing Fresnel zones between active links.\n\nAcceptance: 3D visualization shows ellipsoids between communicating nodes with proper scaling.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"hotel","owner":"","created_at":"2026-04-09T14:54:38.915673399Z","created_by":"coding","updated_at":"2026-04-09T16:37:30.545014159Z","closed_at":"2026-04-09T16:37:30.544898040Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["mitosis-child","mitosis-depth:1","parent-spaxel-sl2"]} +{"id":"spaxel-5es","title":"Ambient dashboard mode","description":"## Background\n\nA wall-mounted tablet showing who is home is a genuinely useful home appliance — a digital replacement for the \"whiteboard on the fridge\" that families have used for decades. The ambient mode is designed for exactly this use case: always on, minimal information density, visually calm, and unobtrusive. It should run for weeks without user interaction, surviving screen timeouts, browser updates, and mothership restarts. When something important happens (fall, security alert), it breaks the calm decisively to get attention.\n\n## Route and Renderer\n\nNew route: /ambient (separate from /simple and expert mode)\nThe user can set a specific device as \"ambient display\" via a browser bookmark or home screen shortcut.\n\nCanvas 2D renderer (not Three.js): the ambient mode uses a dedicated Canvas 2D rendering engine rather than Three.js. Reasons:\n1. Three.js is designed for 3D; the ambient view is a 2D top-down floor plan\n2. Canvas 2D uses significantly less GPU memory and power — important for always-on tablet use\n3. Simpler code path means fewer failure modes for long-running display\n4. Lower battery drain on iPad and Android tablets\n\nRenderer: dashboard/js/ambient_renderer.js. Renders via requestAnimationFrame at 2 Hz (one frame every 500ms). The 2 Hz update rate is intentional: it's visually smooth enough for a presence display and uses minimal CPU.\n\n## Rendered Elements\n\nBackground: colour depends on time of day (see Time-of-Day Palette below).\n\nRoom outline: 2D rectangles for each zone's bounding box (projected to floor plane). White (#ffffff) with 1px stroke, no fill (transparent interior). Zone labels: zone name in white text at centroid, 14px medium font.\n\nPortal lines: thin lines (0.5px, #a855f7 purple) across doorways.\n\nNode positions: small filled circles (radius 4px, #6b7280 grey, subtle).\n\nPerson blobs: filled circles (radius 10-18px) in person.color. Name label above: first name only, 12px white. Blob radius proportional to identity confidence: full size if confident, slightly smaller if low confidence. If anonymous: a ghost/outline circle (stroke only, no fill) in light grey.\n\nSystem status indicator: top-left corner. Small circle (8px radius): green (#22c55e) if all nodes healthy, amber (#f59e0b) if any node degraded, red (#ef4444) if any node offline. No text — just the dot. Tooltip on hover (for the rare touch-and-hold interaction).\n\nTime display: top-right. Current time in large readable font. 28px, #ffffff, tabular-nums font variant for clean time display without layout shifts.\n\nPerson positions: lerp-interpolated between WebSocket updates for smooth movement at the 2 Hz render rate. On each WebSocket message received, update the target position. On each render frame, move each person blob 20% of the remaining distance to the target position (exponential approach = naturally decelerating animation).\n\n## Auto-Dim\n\nWhen no person is detected in the room where the ambient tablet is physically located (a zone that the user has configured as the \"ambient display zone\"), reduce the canvas brightness after 60 seconds of no detection:\n- After 60s: reduce canvas globalAlpha to 0.4 (40% brightness)\n- Restore immediately when presence detected in the ambient zone again\n\nImplementation: Use the CSS filter brightness() property on the canvas element, animated with a CSS transition. Set `canvas.style.filter = 'brightness(0.4)'` after timeout. This approach correctly dims including text labels.\n\nOptionally: if the device supports it (iOS WKWebView + Power Saving APIs), the ambient mode can request Screen Wake Lock API to prevent the tablet display from sleeping. window.navigator.wakeLock.request('screen'). Re-request on visibility change (the lock is released when the page is hidden).\n\n## Alert Mode\n\nWhen a fall or security alert fires (FallDetected or AnomalyDetected event received via WebSocket):\n1. The Canvas render loop detects the alert event\n2. Full canvas background fades to #dc2626 (urgent red) over 500ms\n3. Large white text in the centre: \"FALL DETECTED — Alice\" or \"ALERT — Motion while away\"\n4. Pulsing animation: canvas background alternates between #dc2626 and #991b1b at 1 Hz\n5. Acknowledge button rendered as a large white rectangle in the centre below the text\n6. Tap/click on the acknowledge button: POST /api/fall/{id}/acknowledge or /api/anomalies/{id}/acknowledge. Returns to normal ambient mode.\n\nThis alert mode must be clearly visible from across the room — the text should be at least 48px on a typical 10-inch tablet.\n\n## Morning Briefing Overlay\n\nFirst time presence is detected after 6am (configurable): the ambient display shows a brief overlay card for 15 seconds:\n- Overlaid on the normal floor plan (dark semi-transparent background)\n- Sleep summary (if available): \"Alice: 7h 23m, good sleep\"\n- Today's expected departures (from presence prediction, Phase 7): \"Alice likely leaves at 8:30am\"\n- System status: \"4 nodes healthy\"\n\nAfter 15 seconds: fade out and return to normal ambient view. Tapping the overlay dismisses it immediately.\n\n## Time-of-Day Palette\n\nThe ambient canvas background colour shifts with the time of day to be visually appropriate:\n- 06:00-12:00 (morning): light blue-grey (#f0f4f8, near white) — cool morning light feel\n- 12:00-18:00 (afternoon): neutral grey (#1e293b, dark) — reduces eye strain in bright rooms\n- 18:00-22:00 (evening): warm amber-grey (#1c1507, very dark warm) — matches evening lighting\n- 22:00-06:00 (night): near black (#040404) — OLED-friendly, minimal light\n\nTransitions: smooth CSS gradient transition over 30 minutes at each boundary (not instant). Implemented by pre-computing the target colour for the next 30 minutes and using CSS linear-gradient + keyframe animation.\n\n## Performance\n\nTarget: < 5% CPU usage on a 2016-era iPad (A9 chip). Achieved by:\n- 2 Hz render rate instead of 60 Hz (30x reduction in GPU/CPU work)\n- No Three.js, WebGL, or shader compilation overhead\n- Canvas 2D is hardware accelerated but lightweight\n- Blob count in a home is typically 1-4 — trivial to render\n\nMemory: the ambient page should have < 50MB JS heap. No large textures, no complex geometry.\n\n## Files to Create or Modify\n\n- dashboard/ambient.html: minimal HTML shell\n- dashboard/js/ambient.js: main ambient mode logic, WebSocket connection, alert handling\n- dashboard/js/ambient_renderer.js: Canvas 2D rendering engine\n- dashboard/js/ambient_briefing.js: morning briefing overlay\n- mothership/internal/dashboard/routes.go: /ambient route served statically\n\n## Tests\n\n- Test Canvas 2D renderer draws correct shapes: zone rectangle at (1,1)-(3,3) appears as a white rectangle at the correct pixel coordinates (given known canvas size and room dimensions)\n- Test auto-dim timer: mock 60s with no presence event, verify canvas brightness is reduced\n- Test auto-dim restore: presence event arrives, verify brightness returns to 100%\n- Test alert mode: inject FallDetected event, verify canvas background changes to red and text appears\n- Test acknowledge clears alert mode and returns to normal\n- Test morning briefing overlay appears only once after 6am (localStorage flag set)\n- Test lerp interpolation: person position updates from (1,1) to (3,3), after 5 render frames should be approximately (2.5, 2.5) (with 20% step lerp)\n\n## Acceptance Criteria\n\n- Ambient mode runs for 7 days without page reload (no memory leaks, no uncaught exceptions)\n- Auto-dim activates after 60 seconds of no presence in the display zone\n- Fall/anomaly alert mode clearly visible from 3 metres away on a 10-inch tablet\n- Acknowledge button works and returns to normal ambient\n- Morning briefing overlay appears once per day, dismisses after 15s\n- Canvas 2D rendering consumes < 5% CPU on a mid-range tablet\n- Time-of-day palette transitions are smooth (no hard cuts)\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-03-28T02:00:34.796733529Z","created_by":"coding","updated_at":"2026-04-11T03:17:08.016688139Z","closed_at":"2026-04-11T03:17:08.016444040Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:6"],"dependencies":[{"issue_id":"spaxel-5es","depends_on_id":"spaxel-sl2","type":"blocks","created_at":"2026-03-28T03:29:14.888731706Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-5kf8","title":"Use FXAA instead of MSAA on mobile","description":"On screens < 1024px width, use FXAA antialiasing instead of MSAA if antialiasing is needed in renderer initialization.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"quebec","owner":"","created_at":"2026-04-11T06:59:34.231285657Z","created_by":"coding","updated_at":"2026-04-24T21:41:23.760079996Z","closed_at":"2026-04-24T21:41:23.759967487Z","close_reason":"Already implemented in commit 46b3495. FXAA post-processing is used instead of MSAA on screens < 1024px width. The renderer disables MSAA on mobile, and fxaa.js initializes Three.js EffectComposer with FXAAShader pass. Tests exist in mobile.test.js.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["mitosis-child","mitosis-depth:1","parent-spaxel-gufk"]} +{"id":"spaxel-5lo","title":"Implement Zones CRUD REST endpoints with OpenAPI docs","description":"Implement CRUD endpoints for zones: GET/POST /api/zones, PUT/DELETE /api/zones/{id}. Include OpenAPI-style godoc comments. Acceptance: endpoints respond correctly to HTTP requests, godoc annotations present.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"charlie","owner":"","created_at":"2026-04-07T17:01:33.493352900Z","created_by":"coding","updated_at":"2026-04-07T18:13:38.639619498Z","closed_at":"2026-04-07T18:13:38.639505434Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:1","mitosis-child","mitosis-depth:1","parent-spaxel-0ii"]} +{"id":"spaxel-5yq","title":"load-shedding: health endpoint + dashboard WS alert integration","description":"## Task\nExpose the load shedding level in the health endpoint and send a dashboard WS alert when Level 3 is triggered. Requires spaxel-54i to be complete (GetShedLevel() must exist).\n\n## 1. Health endpoint (mothership/cmd/mothership/main.go)\nFind the `/healthz` handler (around line 218). Change the JSON to include `shedding_level`:\n```go\nfmt.Fprintf(w, `{\"status\":\"ok\",\"version\":\"%s\",\"shedding_level\":%d}`, version, pm.GetShedLevel())\n```\n\n## 2. Dashboard WS alert on Level 3\nIn the fusion loop in main.go (the goroutine that calls `pm.Process()`), track the previous shed level and broadcast an alert when it changes to 3 or recovers:\n```go\n// after pm.Process() call:\nnewLevel := pm.GetShedLevel()\nif newLevel != prevShedLevel {\n if newLevel == 3 {\n msg := map[string]interface{}{\n \"type\": \"alert\",\n \"severity\": \"warning\",\n \"description\": \"System under load — CSI rate reduced to 10 Hz\",\n }\n data, _ := json.Marshal(msg)\n dashboardHub.Broadcast(data)\n }\n prevShedLevel = newLevel\n log.Printf(\"[INFO] Load shedding level changed: %d\", newLevel)\n}\n```\nDeclare `prevShedLevel int` before the fusion goroutine.\n\n## 3. Level 3 rate reduction push (best effort — log only if push mechanism not yet available)\nWhen `newLevel == 3`, log: `log.Printf(\"[INFO] Load shed level 3 — would push 10Hz cap to nodes\")`\nWhen `newLevel` recovers from 3, log: `log.Printf(\"[INFO] Load shed recovered — restoring prior node rate\")`\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./...\n# curl http://localhost:8080/healthz should include shedding_level\n```\n\nRequires: spaxel-54i","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"charlie","owner":"","created_at":"2026-04-07T06:33:19.278442007Z","created_by":"coding","updated_at":"2026-04-07T17:56:41.358181685Z","closed_at":"2026-04-07T17:56:41.358116981Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:4"],"dependencies":[{"issue_id":"spaxel-5yq","depends_on_id":"spaxel-54i","type":"blocks","created_at":"2026-04-07T06:33:23.206754212Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-65k","title":"Dashboard: activity timeline view","description":"## Overview\n\nBuild the unified activity timeline — the primary event history UI, accessible via the #timeline route added in the dashboard framework bead.\n\n## What to build (dashboard/js/timeline.js + timeline.css)\n\n### Timeline sidebar\n- Scrollable chronological event list (newest at top)\n- Event types with distinct icons/colors:\n - Zone entry/exit (green/orange)\n - Portal crossing (blue arrow)\n - Anomaly / security alert (red pulse)\n - Learning milestone (purple star)\n - System event (grey gear)\n- Each event shows: timestamp, description, person name (if identified), zone name\n- Click event → jump to that moment in the 3D view (triggers replay seek to that timestamp)\n\n### Filter bar\n- Filter by: person, zone, event type, time range (today / last 7d / custom)\n- Search box with debounced text filter across event descriptions\n\n### Inline feedback\n- Thumbs up / thumbs down on presence detection events\n- POST /api/feedback with event_id and correct (bool)\n- System response toast: 'Thanks — threshold adjusted for kitchen link'\n\n### Data source\n- Initial load: GET /api/events?limit=200&since=24h\n- Live updates: 'event' messages from WebSocket feed (requires spaxel-9eg)\n\n## Acceptance\n\n- 200 events render within 200ms of page load\n- New events prepend without layout shift\n- Clicking an event in replay mode seeks the replay to ±5s around the event\n- Feedback buttons POST successfully and show toast confirmation","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-04-06T12:56:03.195915329Z","created_by":"coding","updated_at":"2026-04-06T16:01:48.118589901Z","closed_at":"2026-04-06T16:01:48.118470381Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:1"]} +{"id":"spaxel-6ha","title":"Complete REST API: settings, zones, portals, triggers, notifications, replay","description":"## Problem\n\nMany HTTP endpoints are stubbed or missing. The dashboard settings panel, automation builder, and replay UI all require working REST endpoints.\n\n## Endpoints to implement in mothership/\n\n### Settings\n- GET /api/settings — return all configurable settings as JSON\n- POST /api/settings — update settings (partial update, merge semantics)\n\n### Zones & Portals\n- GET /api/zones — list all zones\n- POST /api/zones — create zone\n- PUT /api/zones/{id} — update zone geometry/name\n- DELETE /api/zones/{id} — delete zone\n- GET /api/portals — list all portals\n- POST /api/portals — create portal\n- PUT /api/portals/{id} — update\n- DELETE /api/portals/{id} — delete\n\n### Automation Triggers\n- GET /api/triggers — list all triggers\n- POST /api/triggers — create trigger\n- PUT /api/triggers/{id} — update\n- DELETE /api/triggers/{id} — delete\n- POST /api/triggers/{id}/test — fire trigger once for testing\n\n### Notifications\n- GET /api/notifications/config — get delivery channel config\n- POST /api/notifications/config — set Ntfy/Pushover/webhook settings\n- POST /api/notifications/test — send a test notification\n\n### Replay / Time-Travel\n- GET /api/replay/sessions — list available recording sessions\n- POST /api/replay/start — start replay at given timestamp\n- POST /api/replay/stop — stop replay, return to live\n- POST /api/replay/seek — seek to timestamp within session\n- POST /api/replay/tune — update pipeline parameters mid-replay\n\n### BLE Devices\n- GET /api/ble/devices — list known devices\n- PUT /api/ble/devices/{mac} — set label, assign to person\n\n## Acceptance\n\n- All endpoints return JSON with appropriate status codes\n- Settings endpoint persists to SQLite across restarts\n- Zone/portal CRUD reflected in the live 3D view within one WebSocket cycle\n- OpenAPI-style godoc comment on each handler with method, path, request, response","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-anthropic-sonnet-sp-20260504031710-0","owner":"","created_at":"2026-04-06T12:55:51.683246046Z","created_by":"coding","updated_at":"2026-05-04T03:54:25.379679073Z","closed_at":"2026-05-04T03:54:25.379679073Z","close_reason":"All required REST API endpoints fully implemented and tested: Settings GET/POST with SQLite persistence, Zones/Portals CRUD with WebSocket broadcast, Triggers CRUD with test-fire, Notifications config (ntfy/pushover/gotify/webhook/mqtt), Replay sessions with seek/tune/speed, BLE device management. go test and go vet pass clean.","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:9"],"dependencies":[{"issue_id":"spaxel-6ha","depends_on_id":"spaxel-21n","type":"blocks","created_at":"2026-04-06T15:31:10.298537585Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-6ha","depends_on_id":"spaxel-4fg","type":"blocks","created_at":"2026-04-06T15:31:10.528996520Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-6ha","depends_on_id":"spaxel-kxf","type":"blocks","created_at":"2026-04-06T15:31:10.466981102Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-6ha","depends_on_id":"spaxel-mul","type":"blocks","created_at":"2026-04-06T15:31:10.407580303Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-6ha","depends_on_id":"spaxel-p5p","type":"blocks","created_at":"2026-04-06T15:31:10.594070369Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-6ha","depends_on_id":"spaxel-ubu","type":"blocks","created_at":"2026-04-06T15:31:10.240906965Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-6hd","title":"Floor plan image upload and pixel-to-meter calibration","description":"## Overview\nAllow users to upload a floor plan image (PNG/JPG) and calibrate it to real-world coordinates so the 3D scene displays nodes and blobs at accurate physical positions.\n\n## Backend (mothership/internal/ — new floorplan.go)\n- POST /api/floorplan/image — multipart form; accept PNG/JPG max 10 MB; save to /data/floorplan/image.png\n- GET /api/floorplan/image — serve the stored image (200 or 404 if none)\n- POST /api/floorplan/calibrate — accept {ax,ay,bx,by,distance_m,rotation_deg}: two pixel coordinates and their real-world distance; compute and persist pixel-to-meter transform\n- GET /api/floorplan/calibrate — return current calibration or 404 if none\n- SQLite floorplan table: image_path TEXT, cal_ax,cal_ay,cal_bx,cal_by REAL, distance_m REAL, rotation_deg REAL, updated_at INT\n\n## Dashboard (dashboard/js/floorplan-setup.js)\n- Setup panel section: 'Floor Plan' with upload button\n- On image select: POST to /api/floorplan/image; display uploaded image on ground plane in 3D scene\n- Calibration UI: click point A on image → click point B → enter real-world distance in meters → Save\n- Compute pixel-to-meter scale factor: scale = distance_m / pixel_distance(A,B)\n- Apply scale and rotation to Three.js ground plane texture on load\n\n## Acceptance\n- Uploaded image displayed as ground plane texture in 3D view\n- Calibrated coordinate system maps pixel positions to correct meter positions\n- Image persists across server restart\n- > 10 MB upload rejected with 413 error","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"golf","owner":"","created_at":"2026-04-06T16:42:49.829463356Z","created_by":"coding","updated_at":"2026-04-09T12:45:23.190650793Z","closed_at":"2026-04-09T12:45:23.190526084Z","close_reason":"Implementation complete. All acceptance criteria met:\n- Backend API with image upload (max 10 MB) and calibration endpoints\n- Dashboard UI with two-point calibration and pixel-to-meter scale computation\n- Viz3D integration with texture transformation for accurate positioning\n- SQLite persistence for image metadata and calibration data\n- Image file storage at /data/floorplan/image.png\n\nBoth dependent beads (spaxel-dbd dashboard UI, spaxel-klk backend API) were already CLOSED.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:1"],"dependencies":[{"issue_id":"spaxel-6hd","depends_on_id":"spaxel-dbd","type":"blocks","created_at":"2026-04-07T14:46:37.377627731Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-6hd","depends_on_id":"spaxel-klk","type":"blocks","created_at":"2026-04-07T14:46:37.307745453Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-6n9","title":"events: internal pub/sub event bus (decouple packages from dashboard)","description":"## Overview\nCreate a lightweight Go pub/sub event bus so any internal package can emit events without importing the dashboard package directly (part 2 of spaxel-2ap split).\n\n## Implementation in mothership/internal/eventbus/ (package already exists — extend it)\n\n```go\n// bus.go\npackage eventbus\n\ntype Event struct {\n Type string\n TimestampMs int64\n Zone string\n Person string\n BlobID string\n DetailJSON interface{}\n Severity string\n}\n\ntype Handler func(Event)\n\nvar (\n mu sync.RWMutex\n handlers []Handler\n)\n\nfunc Subscribe(h Handler)\nfunc Publish(e Event)\n```\n\n- `Publish` calls all subscribers in separate goroutines (non-blocking)\n- `Subscribe` is safe to call at any time, including after startup\n- Event types to define as constants: detection, zone_entry, zone_exit, portal_crossing, trigger_fired, fall_alert, anomaly, security_alert, node_online, node_offline, ota_update, baseline_changed, system, learning_milestone\n\n## Integration\n- Have the events package's `InsertEvent` also call `eventbus.Publish`\n- Dashboard WS handler subscribes to the bus to forward events to connected clients (wired in spaxel-9eg)\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./internal/eventbus/\nPATH=$PATH:/home/coding/go/bin go test ./internal/eventbus/\n```","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"charlie","owner":"","created_at":"2026-04-06T22:31:07.051525693Z","created_by":"coding","updated_at":"2026-04-07T16:52:34.549568384Z","closed_at":"2026-04-07T16:52:34.549293102Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":""} +{"id":"spaxel-6th","title":"Multi-link CSI fusion and localization","description":"## Background\n\nSingle-link motion detection (Phase 2) shows presence on a link axis. With 4+ links we can localise people to ±0.5–1.0m using Fresnel zone weighted localization. This is the core spatial intelligence of spaxel. The physics: WiFi CSI is most sensitive to motion within the first Fresnel zone (an ellipsoid between TX and RX). The approach: for each occupancy grid voxel, compute its weight for each link based on Fresnel zone intersection, multiply by that link's deltaRMS, sum contributions, extract blob peaks.\n\n## What to Implement\n\nNew package: mothership/internal/fusion/\n\n### OccupancyGrid\n- mothership/internal/fusion/grid.go\n- 3D float32 grid, configurable resolution (default 0.25m)\n- Dimensions from room config (width, depth, height in meters)\n- Methods: Reset(), Set(x,y,z int, val float32), Get(x,y,z int) float32, Dims() (nx,ny,nz int)\n\n### Fresnel zone geometry\n- mothership/internal/fusion/fresnel.go\n- FresnelWeight(voxelPos, txPos, rxPos vec3, wavelength float64) float64\n- For 5GHz WiFi: wavelength = 0.06m\n- A voxel is in the first Fresnel zone if: d1+d2 <= dist(tx,rx) + wavelength/2\n where d1 = dist(voxel, tx), d2 = dist(voxel, rx)\n- Weight = deltaRMS × exp(-excess_path_length² / (2×0.1²))\n where excess_path_length = (d1+d2) - dist(tx,rx)\n- Weight = 0 outside Fresnel zone\n\n### FusionEngine\n- mothership/internal/fusion/engine.go\n- Inputs: ProcessorManager (from signal package), NodeRegistry (from fleet/session)\n- Runs at 10Hz via time.Ticker\n- Each tick: reset grid, for each active link get deltaRMS from ProcessorManager, for each voxel compute FresnelWeight × deltaRMS, accumulate to grid\n- Output: call BlobExtractor.Extract(grid), broadcast via dashboard hub as 'blob_update' JSON message\n\n### BlobExtractor\n- mothership/internal/fusion/blobs.go\n- Find 3D local maxima in the grid above threshold (default 0.02)\n- Non-maximum suppression: suppress any peak within 0.5m of a higher peak\n- Output: []BlobDetection{Position vec3, Confidence float32, Radius float32}\n- Limit to max 10 blobs\n\n### Room config\n- Add to mothership config (JSON): room.width_m, room.depth_m, room.height_m (defaults: 5, 5, 2.5)\n- Node positions: initially from fleet manager, defaulting to corners if unset\n\n## Key Files\n- mothership/internal/signal/processor.go — GetAllMotionStates()\n- mothership/internal/dashboard/hub.go — Broadcast() for blob_update\n- New: mothership/internal/fusion/grid.go, fresnel.go, engine.go, blobs.go + tests\n\n## Acceptance Criteria\n- FusionEngine produces blob_update WebSocket messages at 10Hz\n- Single active link produces blob peak along the TX-RX axis\n- Two crossing links produce peak near their intersection\n- BlobExtractor correctly suppresses nearby peaks\n- FresnelWeight returns 0 for voxels clearly outside the Fresnel zone\n- go test ./internal/fusion/... passes","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","owner":"","created_at":"2026-03-28T03:30:50.362272102Z","created_by":"coding","updated_at":"2026-03-28T05:36:26.188829209Z","closed_at":"2026-03-28T05:36:26.188507646Z","close_reason":"Implemented: fusion/fusion.go + fusion/grid3d.go (9c56a37) — 3D occupancy grid 0.25m res, Fresnel zone ellipsoid weighting, FusionEngine 10Hz, BlobExtractor with NMS","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","dependencies":[{"issue_id":"spaxel-6th","depends_on_id":"spaxel-cxm","type":"blocks","created_at":"2026-03-28T03:30:50.362272102Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-70i","title":"Develop CSI simulator CLI","description":"Create Go CLI tool for:\n- Virtual node generation\n- Synthetic CSI binary frame output\n- Developer testing workflow\n\nAcceptance: CLI generates valid CSI binary frames for testing without hardware.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"hotel","owner":"","created_at":"2026-04-09T14:54:38.869607224Z","created_by":"coding","updated_at":"2026-04-09T16:31:52.344734554Z","closed_at":"2026-04-09T16:31:52.344617250Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:2","mitosis-child","mitosis-depth:1","parent-spaxel-sl2"]} diff --git a/notes/bf-5cgc-handoff.md b/notes/bf-5cgc-handoff.md new file mode 100644 index 0000000..f3ceb5f --- /dev/null +++ b/notes/bf-5cgc-handoff.md @@ -0,0 +1,283 @@ +# Blob-Creation → Identity-Field Implementation Handoff (bf-5cgc) + +> **Purpose:** formalize `notes/bf-1q3m-consolidated.md` §6 (tiered fix-target list) into a +> **tracked deliverable that links each blob-creation site to the OPEN implementation bead that +> owns it**, plus the exact population pattern to copy. This is what closes bf-4bhd's acceptance +> criterion *"Report is ready for the next bead to use"* and hands the work to bf-5151 and friends. +> +> **Consumes (do not re-search):** `notes/bf-1q3m-consolidated.md` — the sole trusted blob +> inventory. `notes/bf-4bhd.md` and `notes/bf-3ldj-findings.md` are SUPERSEDED (see bf-1q3m §4). +> +> **Verification stamp:** every `file:line` below was re-confirmed against current HEAD +> `1ecc999` (`1ecc9992d184c14b143a1ccb0d0cc932fdec388b`) on 2026-07-06 by running the greps. +> No Go/JS source moved between bf-1q3m's verification commit (`1a26c12`) and HEAD +> (`git diff --stat 1a26c12 HEAD -- mothership/ dashboard/` is empty — only `docs`/`.beads` +> commits since), so all four named Tier-1 boundaries are exact. Re-locate any moved site with: +> `grep -nE "sigproc.TrackedBlob{|automation.TrackedBlob{|explainability.BlobSnapshot{|volume.BlobPos{" mothership/cmd/mothership/main.go` + +--- + +## 0. TL;DR — the handoff in one table + +| # | Leak site (HEAD `1ecc999`) | Target type (struct def) | Fix | Owner bead(s) | +|---|---|---|---|---| +| **1** | `cmd/mothership/main.go:2303` | `automation.TrackedBlob` (`internal/automation/engine.go:1337`) | add identity fields (min `PersonID`); populate from `identityMatcher.GetMatch(b.ID)`; engine already resolves label/color via `personProvider` (`engine.go:952`) | **bf-5151** / **bf-64h5** | +| **2** | `cmd/mothership/main.go:2206` | `explainability.BlobSnapshot` (`internal/explainability/handler.go:95`) | add identity fields; populate from sidecar; **fold the parallel `identityMap`** (`main.go:2216`) so the "Why?" overlay has one identity source (then drop the side-channel arg from `UpdateBlobs` `:2236`) | **bf-5151** | +| **3** | `cmd/mothership/main.go:2326` | `volume.BlobPos` (`internal/volume/shape.go:1080`) | **one-line populate** — `PersonID` field *already exists* (left `""`); reading it is already stubbed at `shape.go:624` | **bf-5151** | + +> **Reference pattern to mirror for all three:** `analytics.TrackUpdate` +> (`cmd/mothership/main.go:2271`) pulls `PersonID` from the sidecar via +> `identityMatcher.GetMatch(blob.ID)` at the conversion site (`main.go:2267`). Copy that shape. + +--- + +## 1. The reference population pattern (copy this) + +`cmd/mothership/main.go:2264-2280` — the **only** live boundary today that correctly threads +identity from the sidecar into a target struct. Every Tier-1 fix should look like this: + +```go +// Stage 4: Update flow analytics — main.go:2262 +for _, blob := range blobs { + // Get person ID from identity matcher ← :2264 + var personID string + if identityMatcher != nil { ← :2266 + if match := identityMatcher.GetMatch(blob.ID); match != nil { ← :2267 THE CALL + personID = match.PersonID ← :2268 + } + } + flowAccumulator.UpdateTrack(analytics.TrackUpdate{ ← :2271 THE TARGET STRUCT + ID: blob.ID, + X: blob.X, + Y: blob.Y, + Z: blob.Z, + VX: blob.VX, + VY: blob.VY, + VZ: blob.VZ, + PersonID: personID, ← :2279 IDENTITY THREADED IN + }) +} +``` + +The three Tier-1 sites are all in the **same 10 Hz loop**, immediately adjacent to this block: +`explainability` (Stage 2, `:2206`), `automation` (Stage 6, `:2303`), `volume` (Stage 6, `:2326`). +The `identityMatcher` is in scope for all of them. So each fix is: declare a `personID` (and, for +automation/explainability, the other identity fields) the same way, then set them on the target +struct literal. + +### 1.1 What `GetMatch` returns (the sidecar fields available to populate from) + +`ble.IdentityMatcher.GetMatch(blobID int) *IdentityMatch` (`internal/ble/identity.go:609`). +`IdentityMatch` (`internal/ble/identity.go:34`) carries: + +| Field | Type | Use for | +|---|---|---| +| `PersonID` | `string` | the stable person key — **always populate this** (drives `personProvider.GetPerson`) | +| `PersonName` | `string` | resolved label (e.g. "Alice") | +| `PersonColor` | `string` | CSS hex color | +| `DeviceAddr` | `string` | matched BLE device address | +| `Confidence` | `float64` | match confidence [0..1] | +| `TriangulationPos` | `Position{X,Y,Z}` | BLE RSSI centroid (used by explainability overlay) | + +> Note the field-name mapping the existing code already uses (main.go:2221-2230): the +> explainability `identityMap` copies `match.PersonName → PersonLabel`. Keep that mapping +> consistent when folding #2. + +--- + +## 2. Per-site concrete fix (what the implementer edits) + +### Tier-1 #1 — automation (`main.go:2303` → bf-5151 / bf-64h5) + +**What's wrong:** `automation.TrackedBlob` (`internal/automation/engine.go:1337`) is +`{ID, X/Y/Z, VX/VY/VZ, Confidence}` — **no identity fields**. The conversion at `main.go:2303` +therefore cannot carry a person. Downstream, `automationEngine.personProvider` +(`SetPersonProvider`, `engine.go:474`) needs a `PersonID` to resolve label/color +(`engine.go:952-953`: `data.PersonName, data.PersonColor, _ = e.personProvider.GetPerson(data.PersonID)`), +but nothing supplies one → **person-aware automations ("when Alice enters…") are structurally blocked.** + +**Fix (three parts):** +1. **Add identity fields** to `automation.TrackedBlob` (`engine.go:1337`). Minimum: `PersonID string`. + Add `PersonLabel`/`PersonColor` only if the engine itself renders them (it doesn't today — it + resolves them via `personProvider` at `:952`/`:1165`, so `PersonID` alone is sufficient for + label/color; add the others only if a webhook payload or event needs them pre-resolved). +2. **Populate** at the conversion site (`main.go:2303`), mirroring `analytics.TrackUpdate`: + ```go + var personID string + if identityMatcher != nil { + if m := identityMatcher.GetMatch(b.ID); m != nil { + personID = m.PersonID + } + } + autoBlobs[i] = automation.TrackedBlob{ + ID: b.ID, X: b.X, Y: b.Y, Z: b.Z, VX: b.VX, VY: b.VY, VZ: b.VZ, + Confidence: b.Weight, + PersonID: personID, // ← new + } + ``` +3. **Verify the `PersonID` reaches `Evaluate`** (`engine.go:1346`) so `personProvider` (`:952`) + can resolve it. (The engine already does `data.PersonName == "" && data.PersonID != ""` → + `personProvider.GetPerson(data.PersonID)`; confirm the `TrackedBlob → data` copy in `Evaluate` + includes the new field.) + +### Tier-1 #2 — explainability (`main.go:2206` → bf-5151) + +**What's wrong:** `explainability.BlobSnapshot` (`internal/explainability/handler.go:95`) is +`{ID, X, Y, Z, Confidence}` — **no identity fields**. The person is currently smuggled into the +"Why?" overlay via a **parallel `identityMap`** built separately at `main.go:2216-2232` and passed +as a 4th arg to `explainabilityHandler.UpdateBlobs(blobSnapshots, linkStates, gridSnapshot, identityMap)` +(`main.go:2236`). This works today but is a **dual source of truth** (snapshot + side-channel map). + +**Fix:** +1. **Add identity fields** to `BlobSnapshot` (`handler.go:95`). Mirror the `explainability.BLEMatch` + shape already in `handler.go:53` (PersonID/PersonLabel/PersonColor/DeviceAddr/Confidence/ + MatchMethod/TriangulationPos) — or embed `*BLEMatch` directly. +2. **Populate** at `main.go:2206` from the sidecar (same `GetMatch` call the `identityMap` loop + already makes at `:2218`), so the snapshot carries identity directly. +3. **Fold** the parallel `identityMap` (`:2216`) into the snapshot, then **drop** the `identityMap` + arg from `UpdateBlobs` (`:2236`) and update `UpdateBlobs`'s signature/`BuildWebSocketSnapshot` + to read identity off the snapshot instead of the map. **Goal:** one identity source per blob. + +> This is the largest of the three (signature change to `UpdateBlobs` + handler reads). If +> bf-5151 wants to bound scope, #1+#3 (automation + volume) can ship first and #2 follow — but +> all three live in the same loop and share the same `GetMatch` call, so doing them together is +> cheapest. + +### Tier-1 #3 — volume (`main.go:2326` → bf-5151) + +**What's wrong:** `volume.BlobPos` (`internal/volume/shape.go:1080`) **already has** +`PersonID string` (line 1082) — but the conversion at `main.go:2326` leaves it `""`. The consumer +is already waiting on it: `shape.go:623-624` reads `t.ConditionParams.PersonID` and currently has +`_ = blob.PersonID // Placeholder for person filter implementation`. + +**Fix (one line):** +```go +var personID string +if identityMatcher != nil { + if m := identityMatcher.GetMatch(blob.ID); m != nil { + personID = m.PersonID + } +} +volumeBlobs[i] = volume.BlobPos{ + ID: blob.ID, X: blob.X, Y: blob.Y, Z: blob.Z, + PersonID: personID, // ← the one new line +} +``` +This **unblocks person-filtered volume triggers** (`condition_params.person`). + +--- + +## 3. Scope note ⚠️ — Go-backend fix vs. TS-framed beads + +The implementation beads as written are **TypeScript / dashboard-frontend-framed** (they mention +"frontend", "TypeScript types", "console errors", "3D dashboard frontend"): +- **bf-64h5** — *"Add identity-related fields to the blob/person data structure in the … 3D + dashboard frontend … Add TypeScript types/interfaces."* +- **bf-1wvm** — *"Update code that creates blob objects … No TypeScript errors."* +- **bf-iner** — *"frontend logic to match BLE registry identities to blob IDs."* +- **bf-4qto / bf-56uk / bf-f841** — TS defaults / TS compliance / runtime testing. + +**But the three Tier-1 leak sites above are all Go backend** (`cmd/mothership/main.go` + +`internal/{automation,explainability,volume}`). **bf-5151** (*"Add identity fields to blob +creation code … Use the list of blob creation sites from previous bead"*) is the bead that maps +to the **Go** Tier-1 work — that is where this handoff is primarily directed. + +**Mapping the consolidated report's tiers to the bead set:** + +| Consolidated-report tier | Bead(s) | Layer | +|---|---|---| +| **Tier 1** (Go leaks: automation `:2303`, explainability `:2206`, volume `:2326`) | **bf-5151** (primary; automation also bf-64h5) | Go backend | +| **Tier 4 #7** (`dashboard/types/spaxel.d.ts:10-91` — canonical JS `Blob` interface) | **bf-64h5** / **bf-1wvm** | dashboard TS types | +| **Tier 4 #8/#9** (`state.js:290` creation; `ambient_renderer.js` consumption) | **bf-1wvm** / **bf-56uk** / **bf-f841** | dashboard JS | +| **BLE matching** (the sidecar itself — already implemented server-side in `internal/ble`) | **bf-iner** (frontend mirror of the match) | dashboard JS | + +**Recommendation for whoever picks up bf-5151:** treat its scope as *"add identity fields to the +**Go** blob-creation/conversion sites in §0 above"* — i.e. this is a **backend** task despite the +sibling beads' TS framing. The frontend identity plumbing (bf-64h5/bf-1wvm/bf-iner) is a separate +concern and only becomes useful once the Go side actually emits identity fields to the dashboard +(which Tier-1 #1/#2 unblock). If the team prefers a clean split, **re-scope bf-5151 to "Go blob +identity fields"** and leave the TS bead titles as the frontend track. + +--- + +## 4. Tier 2 / Tier 3 / out-of-scope (for completeness — listed so nothing is silently dropped) + +From `notes/bf-1q3m-consolidated.md` §6. The implementer should be aware of these but they are +**not** part of the Tier-1 handoff above. + +### Tier 2 — identity machinery present but unwired (architectural decision, not a literal edit) + +| # | Site | Situation | Action | +|---|---|---|---| +| 4 | `internal/tracker/identity.go:164` (`applyIdentity`) / `:179` (`clearIdentity`) | The `tracker.Blob` (3D) identity machinery exists and is correct, but `tracker.TrackManager` is **not wired into `main.go`** (zero refs) — it never runs in the live loop | **Decision to record in the implementation bead:** (a) leave the `ble.IdentityMatcher` sidecar as the single identity source (current design — then Tier 1 is the whole job), **or** (b) wire `tracker.TrackManager` into the live loop so identity attaches to the blob struct once. **Recommendation: (a)** — the sidecar already works and `analytics.TrackUpdate` proves the pattern; wiring the 3D tracker is a larger, riskier change. | +| 5 | `internal/tracker/tracker.go:162` (A1) / `internal/tracking/tracker.go:160` (A2) | `tracker.Blob`/`tracking.Blob` identity fields exist but the spawn literals leave them zero (populated later by `applyIdentity` — which only runs if #4 is wired) | No literal change unless Tier-2 option (b) is chosen | + +### Tier 3 — identity-carrying type but built from a pre-identity source (no-op by design) + +| # | Site | Why no identity | Action | +|---|---|---|---| +| 6 | `cmd/mothership/main.go:5494` (E1) — `signal.TrackedBlob` built from a `fusion.Blob` **peak** | Peaks are pre-identity; identity would attach *after* this in a tracker-driven design | **None** under Tier-2 option (a) (sidecar design). Under option (b), identity reattaches from tracker state, not from E1. | + +### Out of scope for identity work (no identity by design — do not touch) + +- `internal/fusion/fusion.go:260` (C1 — pre-identity peak) +- `internal/simulator/engine.go:460`, `internal/replay/pipeline.go:114,132` (synthetic/demo) +- `internal/explainability/handler.go:194,255` (empty fallbacks for unknown IDs) +- `internal/falldetect/detector.go:277` (history snapshot — posture/velocity only; promote to + Tier 1 **only** if fall alerts need a person name, which per `plan.md` Component 16 they do — + **flagged for a future bead**, not this handoff) +- `internal/volume/shape.go:375,820,879` (non-blob-derived `BlobState` sites), + `internal/tracking/tracker.go:173,188` (`BlobEvent` lifecycle) +- **`api.BlobPos` (`internal/api/triggers.go:624`) — DEAD CODE.** `EvaluateTriggers` is unwired + in `main.go` (the *live* volume trigger path goes through `volumeTriggersHandler.EvaluateTriggers` + at `main.go:2333`, which takes `volume.BlobPos`, not `api.BlobPos`). `api.BlobPos` is a vestigial + duplicate of `volume.BlobPos`; **do not rely on it for field propagation.** +- All test files and all four `new Blob()` browser-API download sites (bf-1q3m §3.5) + +--- + +## 5. Field-propagation checklist (when touching `signal.TrackedBlob` or its conversions) + +If the implementer changes the field set on any tracked-blob type, these production copy sites in +the live loop must be kept in sync (re-locate with grep first — `main.go` drifts): + +1. `cmd/mothership/main.go:2206` — `explainability.BlobSnapshot` (Tier-1 #2) +2. `cmd/mothership/main.go:2288` — falldetect anon struct (+ `detector.go` anon-struct signatures) +3. `cmd/mothership/main.go:2326` — `volume.BlobPos` (Tier-1 #3) +4. `cmd/mothership/main.go:2303` — `automation.TrackedBlob` (Tier-1 #1) + struct def `engine.go:1337` +5. (Reference pattern to mirror: `cmd/mothership/main.go:2271` — `analytics.TrackUpdate`) + +--- + +## 6. Acceptance criteria status + +**bf-5cgc (this bead):** +- [x] Handoff artefact maps each Tier-1 site to the responsible open implementation bead + the + population pattern — §0 + §2 (automation→bf-5151/bf-64h5, explainability→bf-5151, + volume→bf-5151; reference pattern §1; GetMatch fields §1.1) +- [x] Tier 2 / Tier 3 / out-of-scope sites listed for completeness (tracker wiring decision, + E1 no-op, dead `api.BlobPos`) — §4 +- [x] Closes bf-4bhd acceptance criterion *"Report is ready for the next bead to use"* — this + note is the formalized, bead-linked deliverable that bf-4bhd's inventory was building toward + (bf-4bhd's own acceptance box is ticked; this handoff makes it *actionable* by naming the owners) +- [x] Comments posted on the linked implementation beads (bf-5151, bf-64h5, bf-1wvm, bf-iner, + bf-4bhd) pointing here + +**bf-4bhd (parent inventory bead):** its criterion *"Report is ready for the next bead to use"* +is satisfied by `notes/bf-1q3m-consolidated.md` (the re-verified inventory) **plus** this note +(the bead-linked handoff). bf-4bhd may be closed on that basis. + +--- + +## 7. Provenance + +| Source | Used for | Status at HEAD `1ecc999` | +|---|---|---| +| `notes/bf-1q3m-consolidated.md` §6 | tiered fix-target list (the input to this handoff) | verified exact at `1a26c12`; re-confirmed exact at `1ecc999` (no code drift) | +| `notes/bf-1q3m-consolidated.md` §5 | data-flow / leak explanation | confirmed | +| `cmd/mothership/main.go:2206,2216,2267,2271,2303,2326` | Tier-1 sites + reference pattern | all 6 exact at `1ecc999` | +| `internal/automation/engine.go:1337,952,1346` | TrackedBlob def + personProvider + Evaluate | exact | +| `internal/explainability/handler.go:53,95` | BlobSnapshot + BLEMatch defs | exact | +| `internal/volume/shape.go:624,1080` | BlobPos def + person-filter stub | exact | +| `internal/ble/identity.go:34,609` | IdentityMatch + GetMatch | exact | +| This note (bf-5cgc) | formalized site→bead handoff + population pattern | verified at `1ecc999`, 2026-07-06 |