@polderlabs/bizar 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -0
  3. package/cli/audit.mjs +144 -0
  4. package/cli/banner.mjs +41 -0
  5. package/cli/bin.mjs +186 -0
  6. package/cli/copy.mjs +508 -0
  7. package/cli/export.mjs +87 -0
  8. package/cli/init.mjs +147 -0
  9. package/cli/install.mjs +390 -0
  10. package/cli/plan-templates.mjs +523 -0
  11. package/cli/plan.mjs +2087 -0
  12. package/cli/prompts.mjs +163 -0
  13. package/cli/update.mjs +273 -0
  14. package/cli/utils.mjs +153 -0
  15. package/config/AGENTS.md +282 -0
  16. package/config/agents/baldr.md +148 -0
  17. package/config/agents/forseti.md +112 -0
  18. package/config/agents/frigg.md +101 -0
  19. package/config/agents/heimdall.md +157 -0
  20. package/config/agents/hermod.md +144 -0
  21. package/config/agents/mimir.md +115 -0
  22. package/config/agents/odin.md +309 -0
  23. package/config/agents/quick.md +78 -0
  24. package/config/agents/semble-search.md +44 -0
  25. package/config/agents/thor.md +97 -0
  26. package/config/agents/tyr.md +96 -0
  27. package/config/agents/vidarr.md +100 -0
  28. package/config/agents/vor.md +140 -0
  29. package/config/commands/audit.md +1 -0
  30. package/config/commands/explain.md +1 -0
  31. package/config/commands/init.md +1 -0
  32. package/config/commands/learn.md +1 -0
  33. package/config/commands/pr-review.md +1 -0
  34. package/config/commands/tailscale-serve.md +96 -0
  35. package/config/hooks/README.md +29 -0
  36. package/config/hooks/post-tool-use.md +16 -0
  37. package/config/hooks/pre-tool-use.md +16 -0
  38. package/config/opencode.json +52 -0
  39. package/config/opencode.json.template +52 -0
  40. package/config/rules/general.md +8 -0
  41. package/config/rules/git.md +11 -0
  42. package/config/rules/javascript.md +10 -0
  43. package/config/rules/python.md +10 -0
  44. package/config/rules/testing.md +10 -0
  45. package/config/skills/bizar/README.md +9 -0
  46. package/config/skills/bizar/SKILL.md +187 -0
  47. package/config/skills/cpp-coding-standards/README.md +28 -0
  48. package/config/skills/cpp-coding-standards/SKILL.md +634 -0
  49. package/config/skills/cpp-coding-standards/agents/openai.yaml +4 -0
  50. package/config/skills/cpp-coding-standards/references/concurrency.md +320 -0
  51. package/config/skills/cpp-coding-standards/references/error-handling.md +229 -0
  52. package/config/skills/cpp-coding-standards/references/memory-safety.md +216 -0
  53. package/config/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
  54. package/config/skills/cpp-coding-standards/references/review-checklist.md +96 -0
  55. package/config/skills/cpp-testing/README.md +28 -0
  56. package/config/skills/cpp-testing/SKILL.md +304 -0
  57. package/config/skills/cpp-testing/agents/openai.yaml +4 -0
  58. package/config/skills/cpp-testing/references/coverage.md +370 -0
  59. package/config/skills/cpp-testing/references/framework-compare.md +175 -0
  60. package/config/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
  61. package/config/skills/cpp-testing/references/mocking.md +364 -0
  62. package/config/skills/cpp-testing/references/tdd-workflow.md +308 -0
  63. package/config/skills/embedded-esp-idf/README.md +41 -0
  64. package/config/skills/embedded-esp-idf/SKILL.md +439 -0
  65. package/config/skills/embedded-esp-idf/agents/openai.yaml +4 -0
  66. package/config/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
  67. package/config/skills/embedded-esp-idf/references/host-tests.md +164 -0
  68. package/config/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
  69. package/config/skills/embedded-esp-idf/references/kconfig.md +159 -0
  70. package/config/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
  71. package/config/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
  72. package/config/skills/embedded-esp-idf/references/nvs.md +121 -0
  73. package/config/skills/embedded-esp-idf/references/packed-structs.md +192 -0
  74. package/config/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
  75. package/config/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
  76. package/config/skills/self-improvement/SKILL.md +64 -0
  77. package/package.json +47 -0
  78. package/templates/plan/htmx.min.js +1 -0
  79. package/templates/plan/library/bug-investigation.mdx +79 -0
  80. package/templates/plan/library/decision-record.mdx +71 -0
  81. package/templates/plan/library/feature-design.mdx +92 -0
  82. package/templates/plan/meta.json.template +8 -0
  83. package/templates/plan/plan.canvas.template +1711 -0
  84. package/templates/plan/plan.html.template +937 -0
  85. package/templates/plan/plan.mdx.template +46 -0
@@ -0,0 +1,137 @@
1
+ # Memory model and IRAM
2
+
3
+ ESP32 (Xtensa LX6/LX7, or RISC-V on S3/C3) has distinct address spaces. Code and data are placed into them by the linker based on attributes; getting this wrong is the difference between a working firmware and one that crashes on the first SPI-DMA transfer.
4
+
5
+ ## Address spaces at a glance
6
+
7
+ | Space | Size (classic ESP32) | What lives here |
8
+ |-----------|-----------------------------|--------------------------------------------------------------|
9
+ | IRAM | ~128 KB (some chips 256 KB) | ISR handlers, `IRAM_ATTR` functions, flash-cache-disabled |
10
+ | DRAM | ~120 KB | `.data`, `.bss`, heap, stacks, default-code-section RAM |
11
+ | Flash | 4–16 MB | Program text (XIP), read-only data, filesystems |
12
+ | PSRAM | 0–8 MB optional | Large buffers via `MALLOC_CAP_SPIRAM` |
13
+
14
+ ## IRAM (instruction RAM)
15
+
16
+ IRAM is the tightest budget on most ESP32 firmware because it has to be physically present on-chip and is not expandable. Code in IRAM can run without flash cache, which is required during:
17
+
18
+ - **ISR handlers** — the cache may be disabled.
19
+ - **SPI / DMA completion paths** — flash-cache-disabled periods when the SPI peripheral is busy.
20
+ - **Time-critical inner loops** — a few hot loops that would be slowed by XIP cache misses.
21
+
22
+ Mark a function IRAM-resident with the attribute:
23
+
24
+ ```cpp
25
+ #include "esp_attr.h"
26
+
27
+ void IRAM_ATTR spi_complete_isr(spi_transaction_t *trans) {
28
+ // runs from IRAM, must be small and avoid non-IRAM APIs
29
+ }
30
+ ```
31
+
32
+ `(AMS7)` Keep IRAM remaining ≥ 20 KiB on AMS7. Below 24 KiB is a warning. Do not add `IRAM_ATTR` unless the function is genuinely on a cache-disabled path. See `docs/firmware/memory_budget.md`.
33
+
34
+ ### What blows the IRAM budget
35
+
36
+ - **Every `IRAM_ATTR` function** — even a 200-byte routine pulls in 200 bytes of IRAM that flash would otherwise have held.
37
+ - **Logging inside ISRs** — `ESP_LOG*` is not `IRAM_ATTR` by default; if you call it from an ISR, the linker pulls the entire logging chain into IRAM. Use direct `uart_tx` or a deferred task instead.
38
+ - **Format strings in IRAM** — `printf`-style code in IRAM is expensive. Move formatted output to a task.
39
+ - **Per-frame work in IRAM** — even non-IRAM helpers reached only from IRAM functions can end up in IRAM if the linker decides so (`-ffunction-sections` + linker `--gc-sections` reduces but does not eliminate this).
40
+ - **Inlining** — `static inline` in a header used by an IRAM file ends up in IRAM for every consumer.
41
+
42
+ ### Diagnosing IRAM growth
43
+
44
+ ```bash
45
+ idf.py size --format json --output-file size.json
46
+ python3 -c "import json; s=json.load(open('size.json')); print(s['iram_size'], s['dram_size'])"
47
+ ```
48
+
49
+ For per-symbol breakdown:
50
+
51
+ ```bash
52
+ $IDF_PATH/tools/uf2/elf2image.py build/<project>.elf # if needed
53
+ xtensa-esp32-elf-objdump -d build/<project>.elf | grep '<.*>:' | head
54
+ ```
55
+
56
+ The AMS7-specific gate `tools/check_idf_size_budget.py` (wrapped by `scripts/size_check.sh`) reads the JSON and fails on hard thresholds.
57
+
58
+ ## DRAM (data RAM)
59
+
60
+ DRAM holds:
61
+
62
+ - `.data` — initialized globals (`int x = 5;`).
63
+ - `.bss` — zero-initialized globals (`int x;`).
64
+ - **Heap** — `malloc` / `new` returns here by default.
65
+ - **Stacks** — one stack per FreeRTOS task; `app_main`'s stack is fixed.
66
+
67
+ ### Prefer static
68
+
69
+ Heap fragmentation is severe on ESP32 — there is no `mmap` or `sbrk`, just a fixed free list. Long-running firmware that `malloc`s in a loop will eventually fail with `ESP_ERR_NO_MEM`.
70
+
71
+ Rules:
72
+
73
+ - `static const` arrays for lookup tables.
74
+ - `static` (file-scope) for buffers reused across calls.
75
+ - `std::array<T, N>` or `std::span` over `std::vector`.
76
+ - Pool allocators if you really need dynamic.
77
+
78
+ When you must malloc, prefer `heap_caps_malloc(size, MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL)` and free in matching scope. Always check the returned pointer.
79
+
80
+ ### Stacks
81
+
82
+ FreeRTOS stacks grow downward; overflow corrupts whatever is below them. Tuning:
83
+
84
+ - Stack depth is in **words** (4 bytes on Xtensa, 4 bytes on RISC-V too). `4096` = 16 KiB.
85
+ - Over-budget: enable `CONFIG_COMPILER_STACK_CHECK_MODE_NORM` (compiler-instrumented) or `CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK` (hardware watchpoint, only one task at a time).
86
+ - Use `uxTaskGetStackHighWaterMark(handle)` to measure headroom at runtime.
87
+
88
+ `(AMS7)` AMS7 acquisition stacks are 8–16 KB; command-handler stack on the PRO CPU is 16 KB (`CONFIG_MAIN_TASK_STACK_SIZE=16384`).
89
+
90
+ ### Internal vs PSRAM (DMA-capable)
91
+
92
+ `MALLOC_CAP_INTERNAL` requests memory in DRAM. `MALLOC_CAP_SPIRAM` requests external PSRAM (slow, no DMA). Combine as needed:
93
+
94
+ ```cpp
95
+ // 8-bit accessible, internal (DMA-capable) — for SPI/I2S buffers
96
+ uint8_t *dma_buf = (uint8_t *)heap_caps_malloc(4096, MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
97
+
98
+ // large, no DMA requirement — JSON caches, log buffers
99
+ char *big_buf = (char *)heap_caps_malloc(64 * 1024, MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
100
+ ```
101
+
102
+ Always check the return value — `MALLOC_CAP_SPIRAM` may fail if PSRAM is not configured, or return NULL if the chip has none.
103
+
104
+ ## Flash
105
+
106
+ Most code lives in flash and is executed in place (XIP). The flash cache is on-chip and small (~32 KB on classic ESP32). Functions that are not in cache take a flash-read penalty.
107
+
108
+ To check cache hit rate at runtime:
109
+
110
+ ```cpp
111
+ #include "esp_heap_caps.h"
112
+ ESP_LOGI(TAG, "free 8-bit internal: %u", heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL));
113
+ ```
114
+
115
+ For static asserts on flash size, see `idf.py size` output. The relevant numbers are:
116
+
117
+ - **Flash app binary** — total image size.
118
+ - **`bytes free in the smallest app partition`** — partition headroom; matters for OTA.
119
+
120
+ ## `.iram0.lit`, `.iram0.text`, and `.dram0.*`
121
+
122
+ The default linker script places:
123
+
124
+ - `.iram0.lit` — literal pools (constants referenced from IRAM).
125
+ - `.iram0.text` — `IRAM_ATTR` functions.
126
+ - `.dram0.data` / `.dram0.bss` — initialized / zero-initialized data.
127
+ - `.flash.text` — all other code.
128
+
129
+ `idf.py size` aggregates these. Per-section breakdown requires `xtensa-esp32-elf-size -A build/<project>.elf`.
130
+
131
+ ## Common pitfalls
132
+
133
+ - **Calling a non-IRAM function from an ISR.** Symptom: intermittent crash in the ISR. Fix: mark the called function `IRAM_ATTR` or move the work to a task.
134
+ - **Using `printf` / `ESP_LOG*` from an ISR.** Symptom: large IRAM growth. Fix: use `xQueueSendFromISR` to defer the log to a task.
135
+ - **Malloc with `MALLOC_CAP_SPIRAM` on a chip without PSRAM.** Symptom: NULL return, hard fault on deref. Fix: fall back to internal with a smaller size, or fail loudly.
136
+ - **Stack overflow from deep recursion.** Symptom: hard fault or corruption deep in the call stack. Fix: convert to iteration, or bump the task's stack.
137
+ - **Static buffers in headers.** A `static constexpr` buffer in a header included by many `.cpp` files duplicates per translation unit, then collides at link. Use `inline constexpr` (C++17) or move to a single `.cpp`.
@@ -0,0 +1,121 @@
1
+ # NVS (Non-Volatile Storage) on ESP-IDF
2
+
3
+ NVS stores small key-value pairs in flash and survives reboot. ESP-IDF provides `nvs_flash.h` (low-level) and `nvs.h` (typed handle API). Most firmware code uses the typed handle API.
4
+
5
+ ## Initialization — once at boot
6
+
7
+ `nvs_flash_init()` must be called **exactly once at application startup**, not on every operation. Calling it in a getter/setter is an anti-pattern.
8
+
9
+ ```cpp
10
+ // GOOD — in app_main()
11
+ extern "C" void app_main() {
12
+ esp_err_t ret = nvs_flash_init();
13
+ if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
14
+ ESP_ERROR_CHECK(nvs_flash_erase());
15
+ ret = nvs_flash_init();
16
+ }
17
+ ESP_ERROR_CHECK(ret);
18
+ // ... start tasks, services, etc.
19
+ }
20
+
21
+ // BAD — calling nvs_flash_init() in every Get/Set is wasteful
22
+ esp_err_t Configuration::GetInt(const char* key, int32_t* out) {
23
+ nvs_flash_init(); // <-- wrong
24
+ nvs_handle_t h;
25
+ nvs_open("ns", NVS_READONLY, &h);
26
+ // ...
27
+ }
28
+ ```
29
+
30
+ For partition-encrypted or large NVS partitions, also handle `ESP_ERR_NVS_NO_FREE_PAGES` and `ESP_ERR_NVS_NEW_VERSION_FOUND` by erase + retry. See `examples/storage/nvs_value_iterator` in ESP-IDF.
31
+
32
+ ## Namespaces and handles
33
+
34
+ Open a handle per operation or per long-lived component:
35
+
36
+ ```cpp
37
+ // Per-operation (simple, slightly more overhead)
38
+ nvs_handle_t h;
39
+ ESP_ERROR_CHECK(nvs_open("ams7cfg", NVS_READWRITE, &h));
40
+ int32_t val = 42;
41
+ ESP_ERROR_CHECK(nvs_set_i32(h, "ble_adv", val));
42
+ ESP_ERROR_CHECK(nvs_commit(h));
43
+ nvs_close(h);
44
+
45
+ // Per-component (preferred for hot paths)
46
+ class ConfigStore {
47
+ public:
48
+ esp_err_t Init() {
49
+ esp_err_t err = nvs_open("ams7cfg", NVS_READWRITE, &handle_);
50
+ if (err != ESP_OK) return err;
51
+ // optional: pre-load known keys
52
+ return ESP_OK;
53
+ }
54
+ ~ConfigStore() { nvs_close(handle_); }
55
+ private:
56
+ nvs_handle_t handle_{};
57
+ };
58
+ ```
59
+
60
+ `nvs_commit()` is **required** for write durability — `nvs_set_*` only updates the in-memory cache.
61
+
62
+ ## Typed vs blob storage
63
+
64
+ | Type | Use for |
65
+ |---|---|
66
+ | `nvs_set_i8/u8/i16/u16/i32/u64` | Counters, flags, IDs, durations |
67
+ | `nvs_set_str` | Variable-length strings (max 4000 bytes per key, ~1984 bytes safe) |
68
+ | `nvs_set_blob` | Packed structs, calibration data, JSON payloads |
69
+ | `nvs_set_str` with fixed keys | Persistent default values (e.g., `feature_flag_default` = "on") |
70
+
71
+ Use **typed keys** for atomic per-field updates; use **blob** only when the data is always written and read as a unit (otherwise partial writes corrupt the format).
72
+
73
+ ## Error handling
74
+
75
+ `ESP_ERROR_CHECK_WITHOUT_ABORT(nvs_set_*(...))` silently swallows NVS failures. The caller has no way to know the persist operation succeeded. Two patterns:
76
+
77
+ ```cpp
78
+ // 1. Propagate the error to the caller
79
+ esp_err_t ConfigStore::SetInt(const char* key, int32_t val) {
80
+ esp_err_t err = nvs_set_i32(handle_, key, val);
81
+ if (err != ESP_OK) return err;
82
+ return nvs_commit(handle_);
83
+ }
84
+
85
+ // 2. Log and degrade gracefully
86
+ esp_err_t err = nvs_set_i32(handle_, key, val);
87
+ if (err != ESP_OK) {
88
+ ESP_LOGE(TAG, "nvs_set_i32(%s) failed: %s", key, esp_err_to_name(err));
89
+ return; // caller decides what to do
90
+ }
91
+ ```
92
+
93
+ Avoid bare `ESP_ERROR_CHECK_WITHOUT_ABORT` followed by an assumed-success return value.
94
+
95
+ ## Common pitfalls
96
+
97
+ - **Forgetting `nvs_commit()`** — `nvs_set_*` is buffered; without commit, values are lost on power cycle
98
+ - **Using the same handle from two tasks** — `nvs_handle_t` is not thread-safe; serialize access or use one handle per task
99
+ - **Hitting the 4-6 KB per-namespace limit** — large blobs need a dedicated namespace; check `nvs_get_stats()` first
100
+ - **Storing `std::string` directly** — NVS stores C strings, convert with `.c_str()` for write and `nvs_get_str` + sized buffer for read
101
+ - **Initializing on every getter** — see "Initialization" above
102
+ - **Hardcoded partition not present** — confirm `partitions.csv` includes an `nvs` entry; default is usually fine but custom layouts can break
103
+
104
+ ## Kconfig gating for NVS debug
105
+
106
+ Expose NVS internals under a debug option so production builds stay quiet:
107
+
108
+ ```kconfig
109
+ config AMS7_NVS_DEBUG_ENABLE
110
+ bool "Enable verbose NVS logging"
111
+ default n
112
+ help
113
+ When enabled, every NVS get/set logs a line. Disable in production
114
+ to avoid per-write console output.
115
+ ```
116
+
117
+ ## AMS7-specific patterns
118
+
119
+ `(AMS7)` The AMS7 firmware uses the namespace `"ams7cfg"` for runtime feature flags and study metadata. The NVS handle is owned by `Configuration` in `main/ams7conf.cpp`. The boot path in `main/ams7_esp32.cpp` calls `nvs_flash_init()` once before any `Configuration` access.
120
+
121
+ `(AMS7)` Persisted feature-flag defaults are stored as NVS strings (e.g., `ble_adv_snapshot_default = "on"`) — the runtime override is in RAM only. This lets `_SAVE` commands persist while plain `!FEATURE=true` commands stay ephemeral.
@@ -0,0 +1,192 @@
1
+ # Packed binary protocols
2
+
3
+ Wire-format frames over ESP-NOW, BLE characteristic writes, or any byte-oriented transport must have deterministic layout. ESP32 is little-endian natively, but `__attribute__((packed))` structs are still required to lock field offsets across compilers and configurations.
4
+
5
+ ## The canonical AMS7 frame shape
6
+
7
+ Every AMS7 wire frame lives in `main/connectivity/espnow_*_frame.hpp` and follows the same pattern. The IMU frame is the cleanest example:
8
+
9
+ ```cpp
10
+ // main/connectivity/espnow_imu_frame.hpp
11
+ #pragma once
12
+
13
+ #include <stdint.h>
14
+
15
+ #ifdef __cplusplus
16
+ extern "C" {
17
+ #endif
18
+
19
+ #define CORE_ESPNOW_IMU_MAGIC 0x494dU // ASCII 'I','M'
20
+ #define CORE_ESPNOW_IMU_VERSION 1U
21
+ #define CORE_ESPNOW_FRAME_IMU_RAW 1U
22
+
23
+ typedef struct __attribute__((packed)) {
24
+ uint16_t magic; // identifies the frame family on the wire
25
+ uint8_t version; // protocol version of this struct
26
+ uint8_t frame_type; // sub-type within the family
27
+ uint16_t id; // sender / subject id
28
+ uint16_t seq; // monotonic sequence
29
+ uint16_t age_ms; // capture age in ms (sender-stamped)
30
+ int16_t accel_x, accel_y, accel_z;
31
+ int16_t gyro_x, gyro_y, gyro_z;
32
+ } core_espnow_imu_frame_v1_t;
33
+
34
+ void espnow_imu_frame_init(core_espnow_imu_frame_v1_t *frame,
35
+ uint16_t id,
36
+ uint16_t seq,
37
+ uint16_t age_ms,
38
+ int16_t accel_x,
39
+ int16_t accel_y,
40
+ int16_t accel_z,
41
+ int16_t gyro_x,
42
+ int16_t gyro_y,
43
+ int16_t gyro_z);
44
+
45
+ #ifdef __cplusplus
46
+ }
47
+ static_assert(sizeof(core_espnow_imu_frame_v1_t) == 22,
48
+ "core_espnow_imu_frame_v1_t must be 22 bytes");
49
+ #else
50
+ _Static_assert(sizeof(core_espnow_imu_frame_v1_t) == 22,
51
+ "core_espnow_imu_frame_v1_t must be 22 bytes");
52
+ #endif
53
+ ```
54
+
55
+ This pattern is **load-bearing** for the project:
56
+
57
+ - `magic` is the first 16 bits so the receiver can reject foreign frames in O(1) before parsing.
58
+ - `version` distinguishes `_v1_t` from `_v2_t` so the receiver can dispatch to the right decoder.
59
+ - `frame_type` distinguishes sub-types within the same magic family (e.g., `IMU_RAW` vs a future `IMU_QUAT`).
60
+ - A trailing `static_assert` locks the on-wire size — adding a field is a build error, not a silent wire-format drift.
61
+
62
+ `(AMS7)` Naming convention is `core_<transport>_<family>_frame_v<N>_t`. Static asserts are mandatory on every wire struct.
63
+
64
+ ## The universal five rules
65
+
66
+ 1. **`__attribute__((packed))`** on every wire struct. Never rely on natural alignment.
67
+ 2. **`static_assert(sizeof(T) == N)`** for every wire struct. The assertion fires at compile time, not over the air.
68
+ 3. **Fixed-width types** only: `uint16_t`, `int16_t`, `uint32_t`, `int32_t`. Never `int`, `long`, `short`.
69
+ 4. **Magic byte first**, then version, then frame_type. Let the receiver reject foreign frames cheaply.
70
+ 5. **Length-prefixed framing** on the wire: 2 bytes little-endian length, then that many bytes of payload. No delimiter scanning, no escape bytes.
71
+
72
+ ## Byte order
73
+
74
+ ESP32 is little-endian natively; a `uint16_t` written via `memcpy` lands as low-byte-then-high-byte. When porting to a big-endian host (or a future big-endian SoC), use explicit byte-swap helpers instead of leaving `<<` chains:
75
+
76
+ ```cpp
77
+ // Bad: silent byte-order dependence
78
+ uint32_t be = (raw[0] << 24) | (raw[1] << 16) | (raw[2] << 8) | raw[3];
79
+
80
+ // Good: explicit and obvious
81
+ #include <endian.h>
82
+ uint32_t value = le32toh(*reinterpret_cast<const uint32_t *>(raw));
83
+ ```
84
+
85
+ Most ESP-IDF code uses the native LE order and stays put — the explicit byte swap is only needed when a host tool expects BE or when you genuinely don't know the SoC.
86
+
87
+ ## Versioning and capability bits
88
+
89
+ When you need to evolve a frame without breaking old receivers, **bump the version** and add a `_v2_t`. Old receivers see the new magic and ignore it; new receivers see the version byte and dispatch:
90
+
91
+ ```cpp
92
+ typedef struct __attribute__((packed)) {
93
+ uint16_t magic;
94
+ uint8_t version; // 2
95
+ uint8_t frame_type;
96
+ uint16_t id;
97
+ uint16_t seq;
98
+ uint32_t timestamp_us; // new in v2
99
+ /* ... existing fields ... */
100
+ } core_espnow_imu_frame_v2_t;
101
+
102
+ static_assert(sizeof(core_espnow_imu_frame_v2_t) == 30,
103
+ "core_espnow_imu_frame_v2_t must be 30 bytes");
104
+ ```
105
+
106
+ For optional fields, use a `capability_bits` bitmask after the header:
107
+
108
+ ```cpp
109
+ typedef struct __attribute__((packed)) {
110
+ uint16_t magic;
111
+ uint8_t version;
112
+ uint8_t frame_type;
113
+ uint16_t id;
114
+ uint16_t seq;
115
+ uint32_t cap_bits; // bit 0 = has_quaternion, bit 1 = has_temperature, ...
116
+ /* ... variable ... */
117
+ } core_espnow_capability_frame_v1_t;
118
+ ```
119
+
120
+ Receivers mask-and-test `cap_bits` to know which optional fields are present.
121
+
122
+ ## Wire framing (length-prefixed)
123
+
124
+ ESP-NOW itself has a 250-byte MTU per packet and 6-byte receiver MAC; AMS7 adds a 2-byte length prefix and a magic-byte envelope:
125
+
126
+ ```
127
+ +--------+--------+--------------+----------+
128
+ | length | magic | payload | (rounded |
129
+ | (u16) | (u16) | (frame_t) | to MTU) |
130
+ +--------+--------+--------------+----------+
131
+ ```
132
+
133
+ The receiver reads `length`, then reads exactly `length` bytes. If `length` exceeds the packet, the packet is dropped. If `magic` does not match a known family, drop.
134
+
135
+ For ESP-NOW specifically, AMS7 frames use a per-frame structure with sender-stamped `age_ms` so receivers can detect stale samples without keeping state.
136
+
137
+ ## Init functions
138
+
139
+ Always provide an `_init` function that fills the struct from named arguments. This is the only thing that touches the wire struct's bytes, so the layout can change without callers changing:
140
+
141
+ ```cpp
142
+ void espnow_imu_frame_init(core_espnow_imu_frame_v1_t *frame,
143
+ uint16_t id, uint16_t seq, uint16_t age_ms,
144
+ int16_t ax, int16_t ay, int16_t az,
145
+ int16_t gx, int16_t gy, int16_t gz) {
146
+ frame->magic = CORE_ESPNOW_IMU_MAGIC;
147
+ frame->version = CORE_ESPNOW_IMU_VERSION;
148
+ frame->frame_type = CORE_ESPNOW_FRAME_IMU_RAW;
149
+ frame->id = id;
150
+ frame->seq = seq;
151
+ frame->age_ms = age_ms;
152
+ frame->accel_x = ax; frame->accel_y = ay; frame->accel_z = az;
153
+ frame->gyro_x = gx; frame->gyro_y = gy; frame->gyro_z = gz;
154
+ }
155
+ ```
156
+
157
+ The init function is the seam — add a field, the init function gets a new argument, every caller updates. The wire size assertion catches a missed caller at compile time.
158
+
159
+ ## Sending and receiving
160
+
161
+ ```cpp
162
+ // Send: copy struct bytes into a flat buffer, prefix with length, hand to ESP-NOW.
163
+ core_espnow_imu_frame_v1_t f{};
164
+ espnow_imu_frame_init(&f, id, seq, age_ms, ax, ay, az, gx, gy, gz);
165
+ uint8_t wire[2 + sizeof(f)];
166
+ wire[0] = sizeof(f) & 0xff;
167
+ wire[1] = (sizeof(f) >> 8) & 0xff;
168
+ memcpy(wire + 2, &f, sizeof(f));
169
+ esp_now_send(peer, wire, sizeof(wire));
170
+ ```
171
+
172
+ ```cpp
173
+ // Receive: validate length, magic, version; then consume fields.
174
+ void on_recv(const uint8_t *data, size_t len) {
175
+ if (len < 2 + sizeof(core_espnow_imu_frame_v1_t)) return;
176
+ uint16_t want = (uint16_t)data[0] | ((uint16_t)data[1] << 8);
177
+ if (want != sizeof(core_espnow_imu_frame_v1_t)) return;
178
+ core_espnow_imu_frame_v1_t f;
179
+ memcpy(&f, data + 2, sizeof(f));
180
+ if (f.magic != CORE_ESPNOW_IMU_MAGIC || f.version != CORE_ESPNOW_IMU_VERSION) return;
181
+ handle_imu(&f);
182
+ }
183
+ ```
184
+
185
+ ## Common pitfalls
186
+
187
+ - **No `static_assert`.** A field added in one header silently breaks every receiver. Always lock `sizeof`.
188
+ - **`int` or `long` fields.** Their size varies across compilers and platforms; never use them on the wire.
189
+ - **Natural alignment assumed.** Without `__attribute__((packed))`, the compiler may insert padding that you cannot see at the source level.
190
+ - **Magic-byte collision.** Two frame families accidentally using the same 2-byte magic. Pick from a sparse range; document in `docs/transport_payload_reference.md`.
191
+ - **Sending the struct directly via ESP-NOW.** ESP-NOW needs a flat byte buffer and a length prefix; sending `&frame, sizeof(frame)` skips the length prefix and confuses the receiver's parser.
192
+ - **Endianness assumptions on the host.** If a host-side tool (Rust gateway, Python decoder) reads the bytes, it must apply the same byte order. Document in `docs/transport_payload_reference.md`.
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bash
2
+ # idf_env.sh — print the source line for the ESP-IDF environment and verify idf.py is reachable.
3
+ # Run as: bash scripts/idf_env.sh
4
+ # Exits 0 if `idf.py` is on PATH after a (dry-run) env probe; 1 otherwise.
5
+
6
+ set -euo pipefail
7
+
8
+ # Find the repo root from the skill directory. We resolve the script's real
9
+ # path so it works whether invoked from the skill dir or anywhere else.
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+
12
+ # Heuristic: the project root is two levels up from scripts/ inside the skill.
13
+ # Skill layout: <repo>/scripts/<this>.sh → PROJECT_ROOT is SCRIPT_DIR's parent.
14
+ PROJECT_ROOT_DEFAULT="$(cd "${SCRIPT_DIR}/.." && pwd)"
15
+
16
+ PROJECT_ROOT="${PROJECT_ROOT:-${PROJECT_ROOT_DEFAULT}}"
17
+
18
+ # The vendored ESP-IDF is conventionally at <repo>/esp/esp-idf.
19
+ IDF_DIR_CANDIDATE="${PROJECT_ROOT}/esp/esp-idf"
20
+ EXPORT_LINE="source ${IDF_DIR_CANDIDATE}/export.sh"
21
+
22
+ if [[ ! -d "${IDF_DIR_CANDIDATE}" ]]; then
23
+ echo "Run: ${EXPORT_LINE} # (note: esp-idf not found at ${IDF_DIR_CANDIDATE})" >&2
24
+ echo "info: set PROJECT_ROOT to override the discovery path" >&2
25
+ echo "info: set IDF_PATH to a different ESP-IDF checkout" >&2
26
+ exit 1
27
+ fi
28
+
29
+ if [[ ! -x "${IDF_DIR_CANDIDATE}/tools/idf.py" ]] && [[ ! -f "${IDF_DIR_CANDIDATE}/tools/idf.py" ]]; then
30
+ echo "Run: ${EXPORT_LINE} # (note: idf.py not present at expected path)" >&2
31
+ exit 1
32
+ fi
33
+
34
+ # Probe: source the env in a subshell, then check whether idf.py is on PATH.
35
+ # We avoid printing the env's own banner by sending output to /dev/null.
36
+ if (
37
+ set +e
38
+ # shellcheck disable=SC1091
39
+ source "${IDF_DIR_CANDIDATE}/export.sh" >/dev/null 2>&1
40
+ command -v idf.py >/dev/null 2>&1
41
+ ) ; then
42
+ echo "Run: ${EXPORT_LINE}"
43
+ exit 0
44
+ else
45
+ echo "Run: ${EXPORT_LINE} # (env probe failed; check IDF_PATH and Python)" >&2
46
+ exit 1
47
+ fi
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ # size_check.sh — run a project size-budget check.
3
+ # Prefers the AMS7-specific `tools/check_idf_size_budget.py` if present, else falls back to `idf.py size`.
4
+ # Exits 0 on pass, 1 on size violation (AMS7 budget tool) or build failure (fallback).
5
+ # Usage: bash scripts/size_check.sh [--project-dir DIR] [--min-iram-remain BYTES] [--min-dram-remain BYTES]
6
+
7
+ set -euo pipefail
8
+
9
+ PROJECT_DIR="."
10
+ MIN_IRAM_REMAIN=""
11
+ MIN_DRAM_REMAIN=""
12
+
13
+ while [[ $# -gt 0 ]]; do
14
+ case "$1" in
15
+ --project-dir)
16
+ PROJECT_DIR="${2:?--project-dir requires a value}"
17
+ shift 2
18
+ ;;
19
+ --min-iram-remain)
20
+ MIN_IRAM_REMAIN="${2:?--min-iram-remain requires a value}"
21
+ shift 2
22
+ ;;
23
+ --min-dram-remain)
24
+ MIN_DRAM_REMAIN="${2:?--min-dram-remain requires a value}"
25
+ shift 2
26
+ ;;
27
+ -h|--help)
28
+ sed -n '2,6p' "$0" | sed 's/^# \?//'
29
+ exit 0
30
+ ;;
31
+ *)
32
+ echo "size_check.sh: unknown argument: $1" >&2
33
+ exit 2
34
+ ;;
35
+ esac
36
+ done
37
+
38
+ # Resolve absolute path so the AMS7 budget tool's argparse sees a clean --project-dir.
39
+ if [[ -d "${PROJECT_DIR}" ]]; then
40
+ PROJECT_DIR="$(cd "${PROJECT_DIR}" && pwd)"
41
+ fi
42
+
43
+ AMS7_BUDGET="${PROJECT_DIR}/tools/check_idf_size_budget.py"
44
+
45
+ if [[ -f "${AMS7_BUDGET}" ]]; then
46
+ # AMS7-specific path: uses hard thresholds from tools/check_idf_size_budget.py.
47
+ cmd=(python3 "${AMS7_BUDGET}" --project-dir "${PROJECT_DIR}")
48
+ if [[ -n "${MIN_IRAM_REMAIN}" ]]; then
49
+ cmd+=(--min-iram-remain "${MIN_IRAM_REMAIN}")
50
+ fi
51
+ if [[ -n "${MIN_DRAM_REMAIN}" ]]; then
52
+ cmd+=(--min-dram-remain "${MIN_DRAM_REMAIN}")
53
+ fi
54
+ echo "size_check: running AMS7 budget tool: ${cmd[*]}"
55
+ if "${cmd[@]}"; then
56
+ exit 0
57
+ else
58
+ echo "size_check: AMS7 size budget failed" >&2
59
+ exit 1
60
+ fi
61
+ fi
62
+
63
+ # Fallback: plain idf.py size. This prints IRAM/DRAM/flash but does NOT enforce
64
+ # a hard threshold — exit code is whatever idf.py returns.
65
+ if ! command -v idf.py >/dev/null 2>&1; then
66
+ echo "size_check: idf.py not on PATH and no AMS7 budget tool found." >&2
67
+ echo "size_check: source esp/esp-idf/export.sh first." >&2
68
+ exit 1
69
+ fi
70
+
71
+ echo "size_check: no AMS7 budget tool; running 'idf.py size' as a fallback."
72
+ if idf.py -C "${PROJECT_DIR}" size; then
73
+ exit 0
74
+ else
75
+ echo "size_check: idf.py size failed" >&2
76
+ exit 1
77
+ fi
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: self-improvement
3
+ description: Use when setting up, configuring, or debugging the project-level self-improvement system. Every task records lessons learned to .bizar/AGENTS_SELF_IMPROVEMENT.md for agent behavior improvement across sessions.
4
+ ---
5
+
6
+ # Self Improvement
7
+
8
+ Project-level learning system. Every task records what worked, what didn't, and what patterns to follow next time — stored in `.bizar/AGENTS_SELF_IMPROVEMENT.md` at the project root.
9
+
10
+ ## How It Works
11
+
12
+ 1. **Session start**: Odin reads `.bizar/AGENTS_SELF_IMPROVEMENT.md` from project root and factors active rules into routing
13
+ 2. **During work**: Agents follow documented patterns and avoid previously-caught mistakes
14
+ 3. **Task completion**: Odin dispatches @heimdall to append a structured entry to the file
15
+ 4. **Next session**: The cycle repeats — agents get smarter over time
16
+
17
+ ## File Format
18
+
19
+ The file lives at `<project-root>/.bizar/AGENTS_SELF_IMPROVEMENT.md`. Structure:
20
+
21
+ ```markdown
22
+ # Self Improvement
23
+
24
+ ## Active Rules
25
+ <!-- Keep top 5-10 actionable patterns here. Extract from recent entries. -->
26
+
27
+ ## Log
28
+
29
+ ### 2026-06-16: Brief descriptive title
30
+ - **Context**: What was the task
31
+ - **Lesson**: What we learned
32
+ - **Pattern**: What to do next time
33
+ - **Files**: src/foo.ts, src/bar.ts
34
+ - **Agent**: thor, tyr
35
+ ```
36
+
37
+ ## Entry Rules for Agents
38
+
39
+ When writing an entry:
40
+
41
+ - **File per project** — `.bizar/AGENTS_SELF_IMPROVEMENT.md` at the root of whatever project you're working in
42
+ - **If file doesn't exist**, create it with the header template
43
+ - **Entry format**: H3 date header, bullet list with Context, Lesson, Pattern, Files, Agent
44
+ - **Active Rules**: At the top, keep 5-10 distilled patterns from recent entries. If adding a new entry makes it necessary, add a rule too or promote a pattern from an entry.
45
+ - **Deduplicate**: Don't repeat the same lesson. If the same lesson comes up again, update the existing entry's date instead.
46
+ - **Be specific**: "Always use strictNullChecks" not "TypeScript is good"
47
+ - **Agent tag**: Use the subagent name (thor, tyr, heimdall, mimir, etc.)
48
+
49
+ ## Setup
50
+
51
+ For a new project where no such file exists yet, create the initial file:
52
+
53
+ ```
54
+ # Self Improvement
55
+
56
+ Template for project-specific agent learning. Entries are auto-appended by Odin
57
+ at task completion and read at session start.
58
+
59
+ ## Active Rules
60
+
61
+ <!-- Top 5-10 actionable patterns extracted from recent entries -->
62
+
63
+ ## Log
64
+ ```