
The boundary: what belongs to the model
An LLM is very good at judgement and very bad at counting. Ask it to explain what a 2-bit signal with no value table probably means and you get a useful answer; ask it to verify that no two signals in a 256-bit frame overlap, across 379 signals, and you get something that looks like an answer.
That is the whole design premise. Engineering work splits into two categories, and mixing them degrades both:
| Belongs to the model | Belongs to code |
|---|---|
| Deciding which of several checks matters here | Whether two signals occupy the same bit |
| Translating a finding into the symptom it causes | Walking 379 signals and comparing bit ranges |
| Spotting that a changelog disagrees with the file | Extracting a CRC data id out of a comment |
| Choosing the order to run things in | Getting the answer right every single time |
A skill is the seam between the two. It has to carry enough structure for the model to know what to do, and enough determinism that the numbers never depend on the model. Getting that seam right is the actual engineering work, and almost everything below is about it.
Anything that must be reproducible goes into code. Anything that requires context or judgement stays with the model. A skill that asks the model to do arithmetic is not a skill, it is a hope.
Domain knowledge becomes deterministic code
The first decision was not 'write a parser' but 'which data does the audit actually need'. That question determines the architecture, and the answer ruled out the obvious off-the-shelf option.
cantools is a good library and I use it elsewhere. But it is built for decoding, and it discards exactly the four things an audit needs:
| Data the audit needs | cantools .messages | This tool |
|---|---|---|
Cycle time GenMsgCycleTime | ✗ | ✓ |
Value tables VAL_ | partial | ✓ |
Comments CM_ | ✗ | ✓ |
Attributes BA_ | ✗ | ✓ |
Losing those is not a detail. Without a value table, a 2-bit signal reads as the number 2 and you cannot tell whether that means 'On' or 'Fault'. Without a cycle time, a frame cannot be checked against a timing budget. In an audit, the metadata is the payload.
So the parser is hand-written: standard library only, every field the format defines preserved.
Encoding the part of the spec everyone gets wrong
DBC looks line-oriented and is not. The detail that matters most for correctness is Motorola bit numbering: 39|8@0+ does not mean 'start at bit 39 and read forward'. The start bit names the most significant bit, and the cursor walks toward the LSB.
This is where domain experience pays off. It is not knowledge an LLM lacks exactly — it is knowledge you cannot afford to have re-derived, differently, on every run.
Skill shape: writing for the reader that loads it
The tool worked; then came a design question that had nothing to do with code. A skill file is not documentation. Its reader is a model, and the model loads it at a specific moment, under a specific budget.
The core is Step 0 — mode detection: a table mapping the user's wording to a subcommand, with an explicit instruction not to ask a clarifying question. It is the highest-leverage paragraph in the file, because it removes a round-trip on every single use.
# user wording -> subcommand
# ─────────────────────────────────────────────────────────────────────
"audit / check / anything wrong?" -> lint
"compare / what changed / two revisions" -> diff
"decode these bytes / what is this value" -> decode
"garbled / GBK / question marks" -> fix-encoding
# both "check" and "compare" -> run diff FIRST
# a change to something shipped outranks a flaw in something not yet shipped
The second design move is the section most skills omit: what the tool cannot see. A node declared in the header that transmits nothing. A changelog promising frames the file does not contain. An attribute filled with one value across the board. A linter can never report these — and if the file does not say so, a clean run reads as 'the matrix is fine'.
A skill's value is not only what it automates, but how precisely it tells the model where automation stops. That boundary is the difference between a tool and a crutch.
Capability design: what a naive diff cannot express
The second capability is revision comparison. The obvious implementation keys frames by CAN id and walks both maps — and it works, in the sense that it produces output. The design question is whether that output can express the changes that actually break a vehicle network.
So the comparison runs two passes: pair by name across ids, emit one DIF-ID, and suppress the misleading add/remove pair. The same reasoning produces signal-rename detection keyed on bit geometry — because when a name changes, bit geometry is the only thing left to anchor on.
The finding that lives inside prose
A checksum's data id is not a structured field. It sits inside the signal comment as free text:
CM_ SG_ 466 VIU_Info_Checksum "CRC16 checksum (DataID: 0x61D2)";
A structural comparison sees a comment that changed and files it as info. But a receiver validates the CRC with that id: change it and every frame is rejected. The symptom is not an error, it is silence — the module simply never responds, and the sender gets investigated for a day. So the tool extracts the id with a regex and promotes a change to error.
RE_DATAID = re.compile(r"data\s*id\s*[::]?\s*(0x[0-9A-Fa-f]+)", re.I)
# DataID present on both sides and different -> not DIF-CMT, raise DIF-CRC (error)
if old_id and new_id and old_id != new_id:
return DiffFinding("DIF-CRC", "error", ...)
Both cases are the same insight: some of the most dangerous information in an engineering artefact is not in its schema. It is in a comment, a string, a naming convention. A tool that reads only the schema reports success on a broken file.
Quality system: making claims checkable
A tool whose output nobody can verify is not trustworthy, it is just confident. Three mechanisms, each aimed at a different failure:
| Failure mode | Mechanism |
|---|---|
| Code produces wrong numbers | Unit tests on fixtures with known-correct answers, not on 'it ran' |
| Documentation drifts from code | Bidirectional assertion: documented-but-missing fails, implemented-but-undocumented fails |
| Model uses it wrong | Isolated install, clean session, natural-language prompt, inspect the commands it actually ran |
Assertions instead of promises
Documentation drifts silently. A table that says '12 kinds of checks' when the code emits 11 is worse than no table, because it makes the report look complete. So the count is asserted, in both directions:
documented = set(re.findall(r"`([A-Z]{3}-\d{3})`", skill_md))
implemented = set(re.findall(r'code="([A-Z]{3}-\d{3})"', source))
assert documented - implemented == set() # doc ahead of code -> fail
assert implemented - documented == set() # doc behind code -> fail
The same rule covers the numbers in the docs about the bundled examples: which severity split, how many defect classes. If a check is added and the fixture starts reporting differently, CI fails instead of the documentation quietly becoming wrong.
def test_broken_covers_ten_codes(self):
_, findings = self._lint(EXAMPLES / "demo_broken.dbc")
codes = {f["code"] for f in findings}
self.assertEqual(len(codes), 10) # a wrong count fails the build
self.assertEqual(codes, EXPECTED_SET) # the sets must match exactly
Fixtures that double as an environment check
The two bundled DBC files are shipped inside the skill, so an agent can verify the install before touching a real matrix:
python3 <skill>/scripts/dbc_analyze.py lint <skill>/examples/demo.dbc # expect 0 error 0 warn
python3 <skill>/scripts/dbc_analyze.py lint <skill>/examples/demo_broken.dbc # expect all 10 defect codes
A clean fixture whose bit assignments are computed by the generator rather than typed by hand, and a broken one that deliberately trips every check family. If the first one stops being clean or the second stops being fully detected, something regressed — no human review needed.
Acceptance: letting the model drive
Everything above still only proves that the script runs. It does not prove the skill works — because the skill is not the script, it is the script plus its instructions plus the model's interpretation of them.
So acceptance runs in an isolated profile, with the session cleared, driven by natural language, and then inspects the commands the agent actually executed:
# isolated profile: state goes to ~/.openclaw-skilltest, own workspace too
openclaw --profile skilltest skills install <skill-dir>
# a real user sentence, not a command
openclaw --profile skilltest agent --local --agent main \
-m "check the hood_v14.dbc communication matrix for problems"
| Prompt | Commands the agent chose |
|---|---|
| "check this matrix for problems" | info → fix-encoding → lint → messages → signals --frame |
| "what is different between these two" | transcodes both, then diff old new with the baseline in the right position |
The second row is the interesting one. The skill's Step 2 says 'encoding before anything else'; the agent applied that principle to both files on its own, which the file never spells out. That is the difference between an agent following a list and an agent following a rule.
Installing next to the repository proves nothing: a relative path can resolve by accident. The only meaningful test is the one a future user will run — a clean environment, where the repository is not nearby.
Results and reusable method
Applied to two production matrices from the same vehicle network, one revision apart: 10 errors, 49 warnings, 42 infos — and the reverse direction reports the same ten errors, which is what says the comparison is symmetric rather than one-sided.
$ dbc_analyze.py diff EHX_Car_FLM_v1.0.dbc EHX_VIU1_HOOD_v1.4.dbc
old 21 messages / 379 signals
new 26 messages / 297 signals
[E] DIF-CRC VIU_Info_Checksum: checksum data id changed (0x1D2)
0x61D2 -> 0x41D2
[E] DIF-ID NM_Autosar_FLM_R: CAN id changed (0x457 -> 0x46B)
[E] DIF-ID NM_Autosar_FLM_L: CAN id changed (0x45C -> 0x46A)
[E] DIF-REN signal renamed VIU1_SourceNodeIdentifier -> VIU1_NodeAddress
summary: 10 error, 49 warn, 42 info, 101 total
The three most dangerous findings are all invisible in a normal diff: a CRC data id that moved, and two network-management frames that changed address while keeping their names. Every receiver would have to be rebuilt. That is the payoff of designing for the domain instead of for the format.
The method, in five lines
| Step | Question |
|---|---|
| 1 · Split | Which parts must be reproducible, and which need judgement? |
| 2 · Encode | What does the domain know that a model would re-derive differently each run? |
| 3 · Shape | Step 0 first. What does the model need in order not to ask? |
| 4 · Bound | What can this never see, and who must check it? |
| 5 · Prove | Is every claim in the docs asserted by a test, and does a model in a clean environment pick the right commands? |
None of those five steps is about writing more code. Four of them are about deciding what not to automate, and how to make the remaining claims verifiable. That is the part that transfers to the next skill.
Code, tests, and the design in full
Standard library only, MIT licensed. Fixtures are synthetic — no real vehicle, supplier, or personal data from any customer matrix.
github.com/autosar-skills/autosar-agent-skillsCommon questions
What separates a skill from a prompt?
A prompt persuades; a skill constrains. The skill has a Step 0 that decides which branch applies before anything runs, an execution order whose sequence has a stated cost if reversed, and output that is either asserted by a test or explicitly listed as unverifiable. If removing the model would not change what happens, it was a tool, not a prompt — put it in code.
Why not just let the model write the parser each time?
Because it re-derives the same domain facts differently every run. Bit geometry, the PduR routing chain, which vendor uses which container name — these are facts, not judgement. Encoding them once means the model cannot get them wrong on a Tuesday, and a wrong answer becomes a failing test instead of a plausible sentence.
How do you test a skill that is mostly instructions?
Assert the parts that are mechanically checkable and be explicit about the rest. Documented check codes must equal the codes the code can emit, in both directions — that caught a code written in the docs but never implemented. Everything a test cannot reach gets listed as 'must be verified by a human', which is the section most skill documents omit.
Why does the skill document read like a runbook instead of a README?
Because its reader is a model, not a person. The model does not need to be told why the design is good; it needs to know which branch to take and in what order. Explanatory material moves to README.md or references/, where it is loaded only when needed. A quick check: if SKILL.md contains no Step 0, it was probably written for humans.