dsh-embedded-workbench 0.7.1

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 (31) hide show
  1. package/LICENSE +21 -0
  2. package/README.en-US.md +262 -0
  3. package/README.md +260 -0
  4. package/cordis.patch.yml +11 -0
  5. package/lib/index.js +197 -0
  6. package/lib/types/index.d.ts +43 -0
  7. package/package.json +81 -0
  8. package/skills/c-cpp-dev/SKILL.md +121 -0
  9. package/skills/debug-methodology/SKILL.md +83 -0
  10. package/skills/debug-methodology/references/iterative-debug-case-study.md +103 -0
  11. package/skills/embedded-firmware-dev/SKILL.md +133 -0
  12. package/skills/embedded-firmware-dev/references/architecture-principles.md +204 -0
  13. package/skills/embedded-firmware-dev/references/embedded-patterns.md +95 -0
  14. package/skills/embedded-firmware-dev/references/lvgl-pitfalls.md +68 -0
  15. package/skills/embedded-workbench/SKILL.md +240 -0
  16. package/skills/embedded-workbench/references/INDEX.md +88 -0
  17. package/skills/embedded-workbench/references/audit-ledger.md +38 -0
  18. package/skills/embedded-workbench/references/contract-matrix.md +31 -0
  19. package/skills/embedded-workbench/references/decision-log.md +31 -0
  20. package/skills/embedded-workbench/references/detailed-change-plan.md +76 -0
  21. package/skills/embedded-workbench/references/durable-requirement-notes.md +27 -0
  22. package/skills/embedded-workbench/references/final-qc.md +40 -0
  23. package/skills/embedded-workbench/references/iteration-notes.md +53 -0
  24. package/skills/embedded-workbench/references/platform-tool-mapping.md +88 -0
  25. package/skills/embedded-workbench/references/result-note.md +54 -0
  26. package/skills/embedded-workbench/references/steward-memo.md +54 -0
  27. package/skills/embedded-workbench/references/task-charter.md +53 -0
  28. package/skills/hardfault-triage/SKILL.md +237 -0
  29. package/skills/keil-mdk-build/SKILL.md +237 -0
  30. package/skills/state-machine-design/SKILL.md +190 -0
  31. package/src/index.ts +209 -0
@@ -0,0 +1,237 @@
1
+ ---
2
+ name: keil-mdk-build
3
+ description: "Use when building, flashing, or packaging firmware with Keil MDK (UV4 CLI, ARMCLANG), analyzing .map files for ROM/RAM optimization and memory budget, or diagnosing Keil-specific build failures. NOT for non-Keil build systems (Makefile, CMake, IAR, GCC-only). For crash triage see hardfault-triage."
4
+ ---
5
+
6
+ <HARD-GATE>
7
+ This is a domain implementation skill. If you are planning, designing, or entering plan mode — load `Skill("embedded-workbench")` first to activate the workflow gates (Plan Verification Gate, Approval Gate, Closure Gate). Domain skills carry implementation guidance, not workflow enforcement.
8
+ </HARD-GATE>
9
+
10
+ # Keil MDK Build
11
+
12
+ Patterns for building embedded firmware with Keil MDK. Covers both ARM Compiler 5 (armcc) and ARM Compiler 6 (armclang).
13
+
14
+ ## UV4 CLI Build (Authoritative)
15
+
16
+ UV4.exe batch mode is the canonical build path. The Python CLI reimplementation is useful for CI but may miss include paths.
17
+
18
+ ```powershell
19
+ <Keil>\UV4\UV4.exe -b project.uvprojx -t TargetName -j0 -o <log_path>
20
+ ```
21
+
22
+ Replace `<Keil>` with the Keil install root. Auto-discover by checking common locations or ask the user when unknown.
23
+
24
+ **Flags**:
25
+
26
+ | Flag | Meaning |
27
+ | ---- | ------- |
28
+ | `-b` | Batch mode (no GUI) |
29
+ | `-t <target>` | Target name within the multi-target project |
30
+ | `-j0` | Auto-parallelism (use all cores) |
31
+ | `-o <path>` | Log output file |
32
+
33
+ **Exit codes**:
34
+
35
+ | Code | Meaning |
36
+ | :--: | ------- |
37
+ | 0 | Success (no errors, no warnings) |
38
+ | 1 | Warnings but no errors |
39
+ | 2 | Errors |
40
+ | 3 | Errors (when `<StopOnExitCode>3</StopOnExitCode>` is set in uvprojx) |
41
+
42
+ **Critical: Log path resolution** — The `-o` path is resolved **relative to the `.uvprojx` file's directory**, not the current working directory. Always use an absolute path or a path under a known-existing subdirectory (e.g., `objects\`) of the project directory.
43
+
44
+ **Recommended invocation** — UV4 produces no stdout; use `Start-Process` with `-Wait -PassThru` and check `ExitCode`:
45
+
46
+ ```powershell
47
+ $keil = "<Keil_install_root>" # Ask user or auto-discover
48
+ $log = Join-Path (Get-Location) "build.log"
49
+ $p = Start-Process -FilePath "$keil\UV4\UV4.exe" `
50
+ -ArgumentList "-b project.uvprojx -t Target -j0 -o $log" `
51
+ -Wait -PassThru -NoNewWindow
52
+ if ($p.ExitCode -ne 0) { throw "Build failed (exit $($p.ExitCode))" }
53
+ ```
54
+
55
+ ## Compiler Selection
56
+
57
+ Keil MDK supports two compiler generations. Identify which one the project uses before generating commands.
58
+
59
+ | Compiler | Keil Name | Binary | Install Path | Check Version |
60
+ |----------|-----------|--------|-------------|---------------|
61
+ | ARM Compiler 5 | AC5 | `armcc` | `<Keil>\ARM\ARMCC` | `armcc --vsn` |
62
+ | ARM Compiler 6 | AC6 | `armclang` | `<Keil>\ARM\ARMCLANG*` | `armclang --version` |
63
+
64
+ **How to identify**: Check the `.uvprojx` XML for `<ARMCC>` (AC5) or `<ARMCLANG>` (AC6) sections. A project can mix both — check per-file or per-group settings.
65
+
66
+ ## ARM Compiler 5 (armcc)
67
+
68
+ Legacy compiler, still common in long-lived projects. Uses its own flag syntax, incompatible with AC6.
69
+
70
+ **Detection**: Installed at `<Keil>\ARM\ARMCC`. Run `armcc --vsn` to verify.
71
+
72
+ **Optimization levels** — AC5 has two orthogonal axes: optimization level (`-On`) and optimization goal (`-Ospace` vs `-Otime`). Keil's UI combines them into a single dropdown:
73
+
74
+ | Keil Level | AC5 Flags (actual) | Effect |
75
+ | :----------: | -------------------- | -------- |
76
+ | 0 | `-O0` | Minimum optimization, best debug view |
77
+ | 1 | `-O1` | Restricted optimization, good debug view |
78
+ | 2 | `-O2` | High optimization (AC5 **default**) |
79
+ | 3 | `-O3` | Maximum optimization |
80
+ | 4 | `-O3 -Otime` | Max optimization + favor speed over size |
81
+
82
+ `-Ospace` is the default goal at levels 0-3 (favor smaller code). `-Otime` swaps to favor speed. These are separate from the `-On` level.
83
+
84
+ Sources: ARM Compiler v5.06 User Guide ([DUI0472M](https://developer.arm.com/documentation/dui0472m)), §3.154-3.159.
85
+
86
+ **Key flags**:
87
+
88
+ - `--cpu Cortex-M4` (adjust to target MCU; use `--cpu=list` to see supported targets)
89
+ - `--c99` or `--c11` (language standard; C90 is the AC5 default)
90
+ - `--gnu` (enable GNU extensions if project relies on them)
91
+ - `--apcs=/interwork` (ARM/Thumb interworking)
92
+ - `-c` (compile only, no link)
93
+ - `--split_sections` (equivalent to `-ffunction-sections -fdata-sections`)
94
+
95
+ **Warning control**: `--diag_suppress=<id>` to suppress specific warnings; `--diag_error=<id>` to promote to error.
96
+
97
+ ## ARM Compiler 6 (armclang)
98
+
99
+ LLVM-based, current generation. Installed at `<Keil>\ARM\ARMCLANG*`.
100
+
101
+ **Detection**: Search common install roots for directories matching `ARMCLANG*`. Validate with `armclang --version`. If detection fails, ask the user.
102
+
103
+ **Optimization levels**:
104
+
105
+ | Keil Level | AC6 Flag |
106
+ | :----------: | ------ |
107
+ | 0 | `-O0` |
108
+ | 1 | `-O1` |
109
+ | 2 | `-O2` |
110
+ | 3 | `-O3` |
111
+ | 4 | `-Os` |
112
+ | 5 | `-Ofast` |
113
+
114
+ **Key flags for Cortex-M**:
115
+
116
+ - `--target=arm-arm-none-eabi -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard` (adjust MCU/FPU to target)
117
+ - `-c` (compile only, no link)
118
+ - `-ffunction-sections -fdata-sections` (enable linker garbage collection)
119
+ - `-fshort-enums -fshort-wchar` (common embedded defaults)
120
+
121
+ ## AC5 to AC6 Migration Traps
122
+
123
+ Projects migrating from AC5 to AC6 commonly hit these issues. Sources: [ARM Compiler Migration Guide (DUI0742)](https://developer.arm.com/documentation/dui0742), [Arm Compiler for Embedded FuSa Migration Guide](https://developer.arm.com/documentation/109444).
124
+
125
+ | AC5 | AC6 | Trap |
126
+ | ----- | ----- | ------ |
127
+ | `__packed struct { ... }` | `struct __attribute__((packed, aligned(1))) { ... }` | AC5 keyword silently ignored by AC6; struct layout changes. `aligned(1)` ensures no implicit alignment. |
128
+ | `__irq void Handler()` | `void Handler(void) __attribute__((interrupt))` | AC5 attribute not recognized; ISR stack frame broken. Alternatively use CMSIS `IRQn_Type`. |
129
+ | `__asm { ... }` | `__asm volatile ("..." : : : )` | Inline assembly switches from armasm syntax to GAS (GNU assembler) syntax with GCC-style operand constraints. |
130
+ | `--c99` | `-std=c99` | Flag syntax differs; AC6 defaults to gnu11 |
131
+ | `--gnu` | `-fgnu89-inline` | GNU inline semantics differ between compilers |
132
+ | `--diag_suppress=<n>` | `-Wno-<name>` | Warning names differ; numbers don't map 1:1. Use AC6 `-Weverything` to list available warnings. |
133
+ | `#pragma diag_suppress` | `#pragma clang diagnostic ignored "-Wname"` | Pragma syntax differs |
134
+ | `char` unsigned by default | `-funsigned-char` | AC5 defaulted to `unsigned char`; AC6 defaults to signed. Add flag to preserve behavior. |
135
+
136
+ **Migration verification**: After switching compiler, compare `.map` file sizes and symbol lists. Unexpected size changes often indicate a packing or inlining difference.
137
+
138
+ ## Build Lifecycle
139
+
140
+ A full Keil build has 5 stages:
141
+
142
+ 1. **Pre-build** — Version header generation, manifest updates
143
+ 2. **Compile** — `.c`/`.cpp`/`.s` → `.o` via armcc (AC5) or armclang (AC6)
144
+ 3. **Link** — `armlink --via=<response>.lnp` → `.axf`
145
+ 4. **Post-build (fromelf)** — `fromelf --bin objects/app.axf → application.bin`
146
+ 5. **Post-build (merge)** — Merge application BIN + filesystem + bootloader into flash image
147
+
148
+ If bypassing UV4 (CI build), all 5 stages must be replicated. The Python CLI build tool handles this internally.
149
+
150
+ ## Merge / Packaging
151
+
152
+ Embedded firmware packages typically merge multiple components into a single flash image.
153
+
154
+ ### Bootloader Selection (Parity Rule)
155
+
156
+ A common pattern: use `version.build` parity to select development vs. production bootloader.
157
+
158
+ - Parse `version.build` as hexadecimal (not decimal)
159
+ - **Odd** → production bootloader
160
+ - **Even** → development bootloader
161
+
162
+ This is the most common source of confusion — `0x10` (hex) is even, even though "16" as a decimal number looks like it could be interpreted differently.
163
+
164
+ ### Manifest Structure
165
+
166
+ Merged firmware images include a manifest at a fixed address with:
167
+
168
+ - Version fields (major, minor, patch, build)
169
+ - Component sizes (firmware, filesystem)
170
+ - CRC32 checksums for each component
171
+ - Magic number for validation
172
+
173
+ Components are typically padded to alignment boundaries before CRC calculation.
174
+
175
+ ### Non-Standard CRC32
176
+
177
+ Embedded firmware CRC32 often differs from the standard `zlib`/`crc32` implementation:
178
+
179
+ - **Byte-swapped within each word** (MCU word order)
180
+ - No reflection (forward bit order)
181
+ - No final XOR
182
+ - Polynomial: `0x104C11DB7`
183
+
184
+ Verify the CRC implementation against a known-good reference before trusting any reimplementation.
185
+
186
+ ## Common Build Failures
187
+
188
+ | Failure | Cause | Fix |
189
+ | --------- | ------- | ----- |
190
+ | UV4 log written to wrong location | `-o` path is relative to uvprojx directory | Use absolute path |
191
+ | CLI build: missing CMSIS headers | Pack directory detection incomplete | Use UV4 CLI (`UV4.exe -b`) for authoritative builds |
192
+ | CLI build: no compile entries | `.dep` file stale or from different target | Run Keil IDE build first to regenerate |
193
+ | merge: input file not found | fromelf step didn't produce `application.bin` | Check after-build hooks; ensure fromelf completed |
194
+ | Wrong bootloader selected | `version.build` parsed as decimal instead of hex | Always parse build number as hexadecimal |
195
+ | "file not found" for OTA component | Filesystem image not generated | Build filesystem assets before merge step |
196
+ | AC6: struct layout differs from AC5 | `__packed` ignored by AC6 | Replace with `__attribute__((packed))` |
197
+ | AC6: ISR crashes after migration | `__irq` attribute not recognized | Use `__attribute__((interrupt))` or CMSIS `IRQn_Type` |
198
+ | AC6: inline asm syntax errors | AC5 `__asm { }` in sources | Rewrite as `__asm volatile ("...")` |
199
+ | Linker: "No section matches selector" | Scatter file syntax differs between AC5/AC6 | Check scatter file against compiler docs; AC5 uses different section naming |
200
+
201
+ ## MAP File Analysis
202
+
203
+ The `.map` file (at `<listings>/<target>.map`) is the linker's memory blueprint. Make it a habit to review after each build.
204
+
205
+ ### Key Sections
206
+
207
+ | Section | What It Tells You |
208
+ | --------- | ------------------- |
209
+ | **Image component sizes** | Per-file Code / RO Data / RW Data / ZI Data breakdown. Find the bloat. |
210
+ | **Memory Map of the image** | Flash and RAM layout: load regions, execution regions, stack, heap |
211
+ | **Global Symbols** | Every function/variable address and size — essential for HardFault analysis |
212
+ | **Removing unused sections** | What the linker eliminated. Check for unexpected removals. |
213
+ | **Cross References** | Which `.o` calls which. Trace startup and verify call graphs. |
214
+
215
+ ### Size Optimization Workflow
216
+
217
+ 1. Sort **Image component sizes** by Code + RO Data. Focus on the top 5 files.
218
+ 2. In **Global Symbols**, find functions with large `Size` values. Consider splitting or rewriting.
219
+ 3. Check `.constdata` / `.rodata` for debug strings — guard with `#if` or move to runtime generation.
220
+ 4. In **Memory Map**, look for `PAD` entries — these are alignment waste. Reorder struct members to minimize.
221
+ 5. Verify MicroLIB is enabled; check that no accidental `printf`/`sprintf`/`malloc` drags in heavy library code.
222
+ 6. Keep an optimization log: ROM/RAM before and after each change.
223
+
224
+ ### Memory Budget Verification
225
+
226
+ From the **Memory Map** section:
227
+
228
+ - **Load Region LR_IROM1** size = total Flash used (Code + RO + RW initial values)
229
+ - **Execution Region RW_IRAM1** = total RAM used (RW data + ZI data + Stack + Heap)
230
+ - **RW Data** consumes BOTH Flash and RAM — initial values stored in Flash, copied to RAM at startup
231
+ - **ZI Data** consumes RAM only — zero-initialized at startup
232
+
233
+ Check that Stack + Heap sizes match the worst-case call chains (from `.htm` call graph) plus margin.
234
+
235
+ ## HardFault / Exception Triage
236
+
237
+ For crash analysis — fault registers, stack-frame capture, PC-to-source resolution, root-cause classification — load `Skill("hardfault-triage")`. The `.map` file sections described above (Global Symbols, Memory Map) are the bridge between the two skills: build the `.map` here, debug the crash there.
@@ -0,0 +1,190 @@
1
+ ---
2
+ name: state-machine-design
3
+ description: "Use when reviewing or fixing async protocols, retries, ACK/NACK handling, pending flags, timeout logic, or state-machine lockups in embedded firmware. NOT for generic network protocol design (TCP/HTTP/MQTT) unless targeting embedded firmware stack."
4
+ ---
5
+
6
+ <HARD-GATE>
7
+ This is a domain implementation skill. If you are planning, designing, or entering plan mode — load `Skill("embedded-workbench")` first to activate the workflow gates (Plan Verification Gate, Approval Gate, Closure Gate). Domain skills carry implementation guidance, not workflow enforcement.
8
+ </HARD-GATE>
9
+
10
+ # State Machine Design
11
+
12
+ ## Core Rules
13
+
14
+ - Fix the state model, not the symptom. Every in-progress or pending flag must have explicit success, failure, timeout, and reset exits.
15
+ - Timeout logic must be gated on real pending work. Idle states must not trigger retry, recovery, or error transitions.
16
+ - Do not trust a low-level send return value as proof of delivery when an application-layer ACK exists. Use the protocol's completion signal.
17
+ - When adding retries, also define attempt timestamps, backoff rules, and cleanup paths so the state machine cannot lock up silently.
18
+ - If pause, stop, or reconnect can interrupt the normal flow, add an explicit recovery or re-drive branch instead of assuming the old path will naturally resume.
19
+
20
+ ## Transition Gates
21
+
22
+ - When a state transition depends on multiple preconditions, verify every one explicitly at the transition gate. Do not rely on implicit assumptions (e.g., "the timer expired, therefore everything must be healthy"). A single unchecked precondition is the most common source of silent state corruption.
23
+ - If a target state's preconditions can become false while already in that state, define a reverse transition back to the source state. One-way state latches without fallback paths will eventually leak incorrect state to downstream consumers.
24
+
25
+ ## Transient Tolerance
26
+
27
+ - Distinguish between genuine state-changing events and transient perturbations during mode switches, direction reversals, or re-initialization windows. The latter need a tolerance or grace window; only the former should advance the state machine or increment error counters.
28
+
29
+ ## Implementation Patterns
30
+
31
+ ### Pattern A: Per-State Handlers + Unified Error Gate
32
+
33
+ Each state gets its own handler function. The dispatcher is a pure `switch(state)`. A unified fault-threshold check runs **after all** state handlers — no handler triggers the error transition itself. This keeps handlers simple and fault logic centralized.
34
+
35
+ ```c
36
+ // === State enum: exactly one valid state at all times ===
37
+ typedef enum {
38
+ COMM_STATE_INIT,
39
+ COMM_STATE_IDLE,
40
+ COMM_STATE_SAMPLE_STARTING,
41
+ COMM_STATE_SAMPLING,
42
+ COMM_STATE_ERROR,
43
+ COMM_STATE_RECOVERING,
44
+ } comm_state_t;
45
+
46
+ // === Runtime context: all flags explicit in one struct ===
47
+ typedef struct {
48
+ comm_state_t state;
49
+ uint32_t command_fail_count;
50
+ bool data_ready;
51
+ bool communication_lost;
52
+ } comm_runtime_t;
53
+
54
+ // === Per-state handlers: each reads only what it needs ===
55
+ static void comm_handle_idle(comm_runtime_t *rt) {
56
+ rt->warmup_start_time = sys_tick();
57
+ comm_start_sample();
58
+ rt->state = COMM_STATE_SAMPLE_STARTING;
59
+ }
60
+
61
+ static void comm_handle_error(comm_runtime_t *rt) {
62
+ static uint32_t retry_tick = 0;
63
+ if (retry_tick == 0) {
64
+ retry_tick = sys_tick();
65
+ rt->data_ready = false;
66
+ }
67
+ comm_power_off();
68
+ if (sys_tick() - retry_tick < 500) return; // 500ms cooldown
69
+ retry_tick = 0;
70
+ rt->state = COMM_STATE_RECOVERING;
71
+ }
72
+
73
+ static void comm_handle_recovering(comm_runtime_t *rt) {
74
+ comm_handle_initializing(rt); // Recovery RE-USES init — no duplicated paths
75
+ }
76
+
77
+ // === Dispatcher: pure switch, single exit ===
78
+ static void comm_state_process(comm_runtime_t *rt) {
79
+ switch (rt->state) {
80
+ case COMM_STATE_INIT: comm_handle_initializing(rt); break;
81
+ case COMM_STATE_IDLE: comm_handle_idle(rt); break;
82
+ case COMM_STATE_SAMPLE_STARTING: comm_handle_sample_starting(rt);break;
83
+ case COMM_STATE_SAMPLING: comm_handle_sampling(rt); break;
84
+ case COMM_STATE_ERROR: comm_handle_error(rt); break;
85
+ case COMM_STATE_RECOVERING: comm_handle_recovering(rt); break;
86
+ default:
87
+ rt->state = COMM_STATE_INIT; // Unknown state → safe fallback
88
+ break;
89
+ }
90
+
91
+ // Unified error gate: checked AFTER every state, not buried inside handlers.
92
+ // A new state cannot accidentally bypass this check.
93
+ if (rt->command_fail_count >= COMM_MAX_FAILS) {
94
+ rt->state = COMM_STATE_ERROR;
95
+ rt->command_fail_count = 0;
96
+ rt->communication_lost = true;
97
+ }
98
+ }
99
+ ```
100
+
101
+ Key properties:
102
+
103
+ - **Fault logic is centralized** — the error gate runs exactly once, after every state. New states cannot bypass it.
104
+ - **Recovery reuses init** — `comm_handle_recovering()` calls `comm_handle_initializing()`. No duplicated paths to drift apart.
105
+ - **All exits are explicit** — `Error` has a cooldown period (500ms), then transitions to `Recovering`. No fall-through, no implicit assumption.
106
+ - **Unknown state → safe fallback** — the `default` case resets to `Init`.
107
+
108
+ ### Pattern B: Function-Pointer Table Dispatch
109
+
110
+ Heavier than switch-case, but useful when states are added/removed frequently or handlers need different signatures.
111
+
112
+ ```c
113
+ static const struct {
114
+ comm_state_t state;
115
+ void (*process)(void);
116
+ } comm_state_table[] = {
117
+ {COMM_STATE_INIT, comm_init_process},
118
+ {COMM_STATE_IDLE, comm_idle_process},
119
+ {COMM_STATE_CONNECTED, comm_connected_process},
120
+ {COMM_STATE_ERROR, comm_error_process},
121
+ {COMM_STATE_RECOVERING, comm_recovering_process},
122
+ };
123
+
124
+ void comm_state_dispatch(void) {
125
+ for (size_t i = 0; i < ARRAY_LEN(comm_state_table); i++) {
126
+ if (g_comm_runtime.state == comm_state_table[i].state
127
+ && comm_state_table[i].process != NULL) {
128
+ comm_state_table[i].process();
129
+ return;
130
+ }
131
+ }
132
+ // Unknown state: reset to safe default
133
+ g_comm_runtime.state = COMM_STATE_INIT;
134
+ }
135
+ ```
136
+
137
+ ### Pattern C: ACK Timeout With Explicit Retry Limit
138
+
139
+ All core rules in one function: timeout gated only when work is pending, explicit retry count, predefined max retries, all exits defined.
140
+
141
+ ```c
142
+ static void comm_ack_check(uint32_t now_sec) {
143
+ // GUARD: timeout logic only runs when there is real pending work
144
+ if (!g_comm.report_in_progress) return;
145
+
146
+ // GUARD: timeout hasn't expired yet
147
+ if (elapsed_sec(g_comm.send_time, now_sec) < COMM_ACK_TIMEOUT_S) return;
148
+
149
+ // Timeout fired. Explicit retry branch:
150
+ if (g_comm.retry_count == 0) {
151
+ g_comm.retry_count++;
152
+ g_comm.send_time = now_sec;
153
+ comm_send_report(); // One automatic retry
154
+ return;
155
+ }
156
+ // All retries exhausted → terminal exit
157
+ g_comm.report_in_progress = false;
158
+ g_comm.retry_count = 0; // Reset for next cycle
159
+ comm_report_result(false); // Notify caller: failed
160
+ }
161
+ ```
162
+
163
+ ### Anti-Patterns
164
+
165
+ ```c
166
+ // BAD: implicit state via flags — new flag creates untested state combinations
167
+ if (g_flags.busy && !g_flags.paused && g_flags.online) { ... }
168
+ // Fix: use explicit enum — exactly one valid state at all times
169
+
170
+ // BAD: idle state triggers timeout — retry fires with nothing pending
171
+ if (elapsed_ms(t0, now) > TIMEOUT) { retry(); }
172
+ // t0 is always running, even when no work is in flight
173
+
174
+ // BAD: retry loop with no exit condition
175
+ void retry_forever(void) {
176
+ while (!send_packet()) { delay(100); } // Will lock up if HW is dead
177
+ }
178
+
179
+ // BAD: recovery path duplicates init logic instead of reusing it.
180
+ // The copy drifts over time — one path gets a fix, the other doesn't.
181
+
182
+ // BAD: Error handler directly calls power_off() without cooldown period.
183
+ // Power-cycling faster than the hardware spec causes unpredictable state.
184
+ ```
185
+
186
+ ## When To Escalate
187
+
188
+ - When diagnostics point to an architecture-level or state-machine design defect, proactively offer high-level remediation focused on boundary clarity, lifecycle contracts, and reversible transitions — don't just propose ad-hoc runtime patches.
189
+
190
+ **REQUIRED SUB-SKILL:** If you find a state machine bug, also load `Skill("debug-methodology")` to apply structured root-cause analysis. If the bug involves async lifecycle flags or hardware events, load `Skill("embedded-firmware-dev")`. If the state machine lockup triggers a watchdog reset or HardFault, load `Skill("hardfault-triage")`.
package/src/index.ts ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * embedded-workbench — DeepSeek Harness native plugin for the Embedded
3
+ * Workbench toolbox. Injects the session-start gate text (1% Rule, Red
4
+ * Flags, Plan Verification Gate, skills roster) into the first model step
5
+ * of every agent session, mirroring the SessionStart hook the Claude Code
6
+ * plugin installs. The 7 skills ship in this package's `skills/` directory
7
+ * and are registered at apply time into dsh's `ctx.skills` registry through
8
+ * the standard filesystem provider, so they appear in every session catalog
9
+ * without a manual copy step.
10
+ *
11
+ * Injection listens on agent/pre-step and appends the gate to the FIRST
12
+ * model step that runs, once per session (guarded by the session's durable
13
+ * history). Session-start inbox injection was dropped: a blank-session preset
14
+ * switch (agentPreset.select -> recompose) can clear the inbox before the
15
+ * first step, losing the gate for the whole session. The pre-step decision is
16
+ * the durable path - anchored/bootstrap presets that strip first-step injected
17
+ * reminders (skill catalog, AGENTS.md, gate plugins) simply defer this message
18
+ * to the first step after their promotion, and the history guard re-injects it
19
+ * there. The default gate text is the dsh-native adaptation of
20
+ * `hooks/session-start-content.md`: behavior rules
21
+ * (1% Rule / Red Flags / Plan Verification Gate) stay in sync, while
22
+ * presentation is adapted to dsh's native skill catalog — no roster table
23
+ * (the model sees skills in its catalog) and no install instructions (those
24
+ * live in `.dsh/INSTALL.md`). Deployments override via Config.
25
+ *
26
+ * @module embedded-workbench-dsh
27
+ */
28
+
29
+ import { fileURLToPath } from 'node:url'
30
+ import type { Context } from '@deepseek-ai/cordis'
31
+ import z from '@deepseek-ai/schemastery'
32
+ import { createUserMessage } from '@deepseek-ai/dsh-llm'
33
+ import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
34
+ import type { HostCordisInspectProviderRegistration } from '@deepseek-ai/dsh-cordis-host-runner'
35
+ import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem'
36
+
37
+ export const name = 'embedded-workbench'
38
+
39
+ // Skills are contributed through the registry service, which dsh-base always
40
+ // mounts before bundle rows such as this one apply.
41
+ export const inject = ['skills']
42
+
43
+ // Absolute path of the package's shipped skills directory. `lib/index.js`
44
+ // lives one level below the package root, so `../skills` from the module URL
45
+ // lands on `<package>/skills` regardless of where the package was installed.
46
+ const SKILLS_DIR = fileURLToPath(new URL('../skills', import.meta.url))
47
+
48
+ const GATE_PLUGIN_ID = 'embedded-workbench'
49
+
50
+ const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
51
+ Plugin embedded-workbench is active. You have embedded C/C++ firmware development skills — names and "Use when" triggers are in your skill catalog; load them with the skill tool. No custom agents in dsh: use the native subagent tooling for parallel work.
52
+
53
+ **1% Rule**: If there is even a 1% chance a skill applies to your task, invoke it before responding. If the skill turns out to be wrong for the situation, discard it and move on. The cost of loading a skill is trivial compared to the cost of a preventable mistake.
54
+
55
+ **Red Flags** — if you think any of these, STOP. You are rationalizing:
56
+
57
+ | You think | Reality |
58
+ |-----------|---------|
59
+ | "This is just a quick fix" | Quick fixes break things. A 3-line design check costs 30 seconds. |
60
+ | "I already understand this code" | You are looking at one file. The blast radius may span 5 modules. |
61
+ | "The skill is overkill for this" | Simple things become complex. Check for skills. |
62
+ | "Let me explore the codebase first" | Skills tell you HOW to explore. Check first. |
63
+ | "I can just read the file directly" | Skills have patterns and pitfalls you will not discover by reading. |
64
+ | "I remember this skill content" | Skills evolve. Always load the current version. |
65
+ | "I've explored enough, time to exit plan mode" | The exit_plan_mode tool is the verification gate. Have you loaded the logicprobe skill? Every plan must pass this gate before exit. |
66
+ | "This plan is too simple for logicprobe" | The skill auto-classifies depth. You don't decide. |
67
+ | "I already read the code, I know the file paths are correct" | Load the logicprobe skill, run Phase 0, append the "## Plan Verification" block. |
68
+
69
+ **Plan Verification Gate**: Before calling exit_plan_mode (or presenting a plan for approval), either load the logicprobe skill (a separate plugin — if it is missing from your skill catalog, tell the user to install it) OR inform the user "此计划未经 logicprobe 验证,是否需要核查?" Silent skip is not an option.
70
+
71
+ To load workflows and engineering policies: load the embedded-workbench skill.
72
+
73
+ **Proactive features**: When you see state machines, protocol refactoring, behavioral claims ("always"/"never"), or multi-module tasks — suggest verification, adversarial probing, or parallel subagents BEFORE the user asks. Most users do not know these exist.
74
+ </EXTREMELY_IMPORTANT>`
75
+
76
+ export interface Config {
77
+ enabled: boolean
78
+ gateContent: string
79
+ }
80
+
81
+ export const Config = z.object({
82
+ enabled: z.boolean().default(true),
83
+ gateContent: z.string().default(DEFAULT_GATE_CONTENT),
84
+ })
85
+
86
+ function gateMessage(text: string): UserMessage {
87
+ return createUserMessage({
88
+ content: [{ type: 'text', text }],
89
+ // `form` omitted — an undeclared context is the documented default.
90
+ source: { kind: 'plugin', plugin: GATE_PLUGIN_ID },
91
+ })
92
+ }
93
+
94
+ /**
95
+ * Model-visible catalog entry (cordis_inspect_list / cordis_inspect_query):
96
+ * lets the model read this plugin's runtime status without guessing. Mirrors
97
+ * the registration pattern of the official dsh-tool-cordis host providers.
98
+ */
99
+ function inspectProvider(config: Config): HostCordisInspectProviderRegistration {
100
+ return {
101
+ manifest: {
102
+ id: 'embedded-workbench',
103
+ description: 'Session-start gate injection for the Embedded Workbench toolbox — folds the 1% Rule / Red Flags / Plan Verification Gate text into the first model step of every agent session.',
104
+ methods: [
105
+ {
106
+ name: 'status',
107
+ description: 'Read whether the gate injection is active and how large the injected gate text is.',
108
+ inputSchema: {
109
+ type: 'object',
110
+ properties: {},
111
+ additionalProperties: false,
112
+ },
113
+ outputSchema: {
114
+ type: 'object',
115
+ description: 'Gate-injection plugin status.',
116
+ properties: {
117
+ enabled: { type: 'boolean', description: 'Whether the gate folds into the first model step.' },
118
+ gateContentLength: { type: 'integer', description: 'Length in characters of the injected gate text.' },
119
+ },
120
+ required: ['enabled', 'gateContentLength'],
121
+ additionalProperties: false,
122
+ },
123
+ },
124
+ ],
125
+ },
126
+ query: async (method) => {
127
+ if (method === 'status') {
128
+ return {
129
+ enabled: config.enabled,
130
+ gateContentLength: config.gateContent.length,
131
+ }
132
+ }
133
+ return null
134
+ },
135
+ }
136
+ }
137
+
138
+ export function apply(ctx: Context, config: Config): void {
139
+ // Catalog visibility is optional: register only when the inspect registry
140
+ // service is mounted, so headless assemblies without it keep the gate
141
+ // injection working. The registry may be provided AFTER this row applies
142
+ // (base-bundle rows can mount later), so registration is retried on the
143
+ // first agent/session-start — by then the app is fully booted.
144
+ let providerRegistered = false
145
+ const registerProvider = (): void => {
146
+ if (providerRegistered) return
147
+ const inspect = ctx.get('cordisInspect')
148
+ if (inspect === undefined) return
149
+ try {
150
+ ctx.effect(() => inspect.register(inspectProvider(config)), 'embedded-workbench: inspect provider')
151
+ providerRegistered = true
152
+ } catch (err) {
153
+ console.warn('[embedded-workbench] inspect provider registration failed', err)
154
+ }
155
+ }
156
+ registerProvider()
157
+ // Ship the bundled skills through the registry: reuse the standard
158
+ // filesystem provider over this package's own `skills/` directory, so
159
+ // catalog discovery, frontmatter parsing, and SKILL.md loading behave
160
+ // exactly like project/user skills while the plugin stays self-contained.
161
+ // Registration lands in the global registry layer (this row mounts at the
162
+ // profile root), so every agent preset sees the skills. `registerProvider`
163
+ // returns the effect disposer; its teardown unregisters and invalidates.
164
+ ctx.skills.registerProvider((control) => {
165
+ return new FileSystemSkillProvider(ctx, control, {
166
+ providerName: 'embedded-workbench',
167
+ includeDefaultRoots: false,
168
+ customSkillDirs: [SKILLS_DIR],
169
+ })
170
+ })
171
+ if (!config.enabled) return
172
+ // Inject the gate once per session on the FIRST model step that runs,
173
+ // instead of at session-start: session-start injection lands in the agent's
174
+ // inbox, which a blank-session preset switch (agentPreset.select ->
175
+ // recompose) can clear before the first step - the gate would then be lost
176
+ // for the whole session. The pre-step decision is the durable path a
177
+ // first-step injection takes: the gate is appended to the first step's
178
+ // decision and enters session history there, so every later step (and a
179
+ // resume) skips it. Anchored/bootstrap presets that strip first-step
180
+ // injected reminders (skill catalog, AGENTS.md, gate plugins) simply defer
181
+ // this message to the first step after their promotion - the history guard
182
+ // re-injects it there, so the gate still lands exactly once per session.
183
+ ctx.on('agent/pre-step', async ({ agent }, next) => {
184
+ const decision = await next()
185
+ if (decision.kind === 'reject') return decision
186
+ registerProvider()
187
+ if (gateInHistory(agent.session)) return decision
188
+ return {
189
+ kind: 'enter',
190
+ messages: [...decision.messages, gateMessage(config.gateContent)],
191
+ }
192
+ })
193
+ }
194
+
195
+ /**
196
+ * Whether the gate already entered this session's durable history. The
197
+ * pre-step listener re-appends the gate until it does; once a step committed
198
+ * it, every later step (and a resume of a session that kept it) skips the
199
+ * injection. A session whose gate was dropped before any step ran (e.g. an
200
+ * inbox cleared by a blank-session preset switch) simply re-injects on the
201
+ * first step that runs.
202
+ */
203
+ function gateInHistory(session: Session): boolean {
204
+ return session.events.some((event) => {
205
+ if (event.type !== 'user/message') return false
206
+ const source = event.data.source
207
+ return source.kind === 'plugin' && source.plugin === GATE_PLUGIN_ID
208
+ })
209
+ }