The skill is tested TDD-style (RED → GREEN → REFACTOR) against OpenZeppelin, a quality bar the user trusts. OZ is the reference answer; it is not treated as 100% ideal — where our output diverges, the diff is discussed and may favor ours.
- Clone OZ
v5.6.1into.baseline/openzeppelin-v5.6.1/(gitignored). - Pick a file. Read its existing comments + code and distill an implementation-context
note (
work/<File>.context.md) — the factual knowledge the author has, NOT the polished comments to copy. This mimics a real session where the agent knows the code it wrote. - Copy the file to
work/<File>.stripped.soland remove all comments (keep SPDX + code). - A sub-agent re-writes the comments from the stripped file + context note.
- RED: sub-agent runs without the skill (it still inherits the user's global CLAUDE.md —
this is the realistic "current behavior" baseline). Output →
work/<File>.red.sol. - GREEN: sub-agent runs with the skill. Output →
work/<File>.green.sol.
- RED: sub-agent runs without the skill (it still inherits the user's global CLAUDE.md —
this is the realistic "current behavior" baseline). Output →
- Compare each output to the OZ original. Record meaningful diffs below.
Context: work/MerkleTree.context.md. Stripped: work/MerkleTree.stripped.sol.
Reproduced the user's complaints. Dominant failure = inline over-expansion; line count was close to the original (267 → 275) but the distribution was wrong.
| # | Failure | OZ original | RED baseline |
|---|---|---|---|
| 1 | Inline over-expansion (main pain) | terse step labels: // Cache read, // Get leaf index, // Check if tree is full., // Rebuild branch from leaf to root |
1–3 sentence paragraphs per step; commented the update loop body that OZ leaves mostly bare |
| 2 | Invented / wrong metadata | // ... (last updated v5.5.0) ... |
re-added a version-stamp line and guessed it wrong (v5.1.0) |
| 3 | One-line doc inflated to a block | /// @dev Error emitted when trying to update a leaf that was not previously pushed. |
expanded into a multi-line /** */ block with extra prose |
| 4 | Restructure + duplicate IMPORTANT | compact NatSpec | added a redundant "called only once" IMPORTANT duplicating the adjacent reset note |
| 5 | Lost load-bearing doc | struct NOTE (root/history not stored) + WARNING |
dropped/merged some of it |
Correctly preserved (skill must NOT break these): the guard comments
// Workaround stack too deep and // ... cannot overflow because: 0 <= index < length.
Conclusion → the GREEN skill must target: terse inline labels (not paragraphs); never invent
version/metadata stamps; /// for single-line docs; don't restructure or pad existing
completeness; keep load-bearing guard comments.
Big improvement; the dominant failure is gone. Metrics (lower = terser):
| metric | OZ original | RED (no skill) | GREEN (skill) |
|---|---|---|---|
| total lines | 267 | 275 | 221 |
| comment lines | 127 | 134 | 81 |
inline-body // |
18 | 31 | 13 |
What the skill fixed vs RED:
- Inline terse — GREEN reproduced OZ's short labels verbatim (
// Cache read,// Get leaf index,// Check if tree is full.,// Rebuild branch from leaf to root) and kept both guard comments. RED's paragraphs are gone. - No invented metadata — GREEN omitted the version stamp RED hallucinated.
- Form — one-line errors stayed
///; multi-line docs stayed/** */. - API docs complete-but-terse — documented the surprising reverts, the same-
fnHashprecondition, the front-running WARNING, and the caller'soldRootduty.
GREEN vs OZ (OZ not treated as ideal):
- GREEN is terser than OZ and still conveys enough — matches the user's terse-adequate target.
- GREEN documents
push's full-treePanic(RESOURCE_ERROR)revert, which the OZ NatSpec omits — arguably better (a surprising revert worth stating). - Borderline: one 2-sentence inline in
update(frontier sibling) where the skill says "a second sentence is a red flag". It explains a genuinely non-obvious step OZ left bare — keep but consider allowing a single compact sentence for a real non-obvious why. - Minor nit: GREEN invented an xref anchor format
{setup-Bytes32PushTree-uint8-bytes32-...}(non-standard). Candidate hygiene rule: don't guess doc-tooling anchor syntax; use a plain{symbol}or prose when unsure.
Verdict: skill works on run 1. Candidate REFACTOR tweaks above are minor.
Added three rules from user feedback and re-ran the same file. All three landed:
- Lists over prose — the header now uses OZ's
* Depth: … * Zero value: … * Hashing function: …list instead of the v1 prose paragraph. - Backtick references, no invented anchors —
`push()`/`update()`; overloads use the signature form`setup(Bytes32PushTree storage, uint8, bytes32, function)`. The hallucinated{setup-Type-uint8-...}anchor is gone. - Plain language — the
updateinline no longer says "frontier": "When this node is the last filled one at its level, its sibling is the stored side, not a proof element: …". Clear, if still ~3 lines (acceptable — a genuine non-obvious why OZ left bare).
All v1 wins retained (terse inline labels, guards, no metadata, /// one-liners, complete-but-terse
API docs incl. the push full-tree revert OZ omits). Run 1 passes; skill is in good shape.
Later finding (scope): green2's update carried WARNING: This is front-running sensitive ...
— a bare security label with no mechanism (the user couldn't tell the author understood Solidity's
single-thread model; fnHash is view). The front-running risk is real (mempool tx-ordering),
but coining/elaborating security warnings is not the authoring skill's job — it belongs to the
review skill's vulnerability track, which must explain the mechanism, not just label it. Added a
"Don't invent security analysis" rule to SKILL.md Hygiene; noted for the review session's MEMORY.
Hardening on a different profile: header with usage examples + WARNING, an interface-implementing
surface, "Requirements/emits" API blocks. Skill run only (RED skipped — failure modes already
documented in run 1). Context: work/AccessControl.context.md. Output: work/AccessControl.green.sol.
| metric | OZ original | GREEN (skill) |
|---|---|---|
| total lines | 207 | 162 |
| comment lines | 121 | 76 |
@inheritdoc |
1 | 6 |
- Header reproduced faithfully — usage examples, the role-id
keccak256snippet, and theDEFAULT_ADMIN_ROLEWARNING all kept. No invented metadata. - Internal functions (
_checkRole,_setRoleAdmin,_grantRole,_revokeRole) documented terse, like OZ (they are not on the interface). - Deliberate divergence (arguably better than OZ): the 5 external functions use
@inheritdoc IAccessControlinstead of OZ's re-documented "Requirements/emits" blocks. Verified lossless —IAccessControlfully documents those functions (Requirements + emits), so@inheritdocdrops nothing and removes the duplication/drift risk. Leaner by 45 lines. - Surfaced caveat → skill updated:
@inheritdocis only safe over a complete base doc; over a thin interface it silently drops caller info. Added that caveat tocomment-types.md. - Minor: reference style was
{Symbol}here (matches OZ docgen house style) vs backticks in run 1 — the agent followed each file's apparent convention, but the flip was memory-driven. The skill rule (follow the project's docgen convention, else backticks) is right; detection should come from surrounding files and stay consistent within a project.
Re-ran MerkleTree.sol and AccessControl.sol with the refined skill.
Outputs: work/MerkleTree.green3.sol, work/AccessControl.green2.sol (initial), then
work/AccessControl.green3.sol (after the @inheritdoc fix below).
MerkleTree (green3) — clean, plus the front-running fix. The bare
WARNING: front-running sensitive label is gone; replaced by a factual precondition + concrete
mechanism: "The caller is responsible for verifying that oldRoot equals the last known root …
Because the proof must match the tree's current state, any push/update that changes the root
invalidates an in-flight update proof." The "Don't invent security analysis" rule worked. All
other properties retained (list header, backtick refs, /// one-liners, guards, terse inline).
AccessControl (green2) — surfaced a defect → skill fix. The agent produced an
@inheritdoc IAccessControl + appended @dev Requirements: hybrid on grantRole/revokeRole/
renounceRole — tool-dependent, and it re-duplicated the requirement + emitted-event that the
interface already documents. Root cause: the old "inherit only what's complete" caveat pushed the
agent to verify the interface and patch the gap inline.
Fixes applied:
comment-types.md—@inheritdocis all-or-nothing; in passive mode do not audit the base's completeness (avoids cross-file churn). Base has a doc → inherit; none → write own; thin → leave for review.active-mode.md— review is where@inheritdoccompleteness is checked; the fix goes into the interface/base, never an@inheritdoc+@devhybrid.SKILL.md— "Blank lines aren't free either" (default fewer; keep the ones the project's doc tooling needs, e.g. Markdown's blank-line-before-a-list).- Review-session MEMORY: added
inherited-doc-completion-is-review-work.
AccessControl (green3) — verified. Re-ran after the fix: pure @inheritdoc IAccessControl on
all three external functions, no hybrid, no duplication (agent stated it chose to inherit rather
than re-document). Internal functions documented; header + WARNING intact; no invented metadata.
Skill is stable across both files. Candidate future test: an interface with thin docs, to confirm passive mode inherits-as-is and leaves completion to review.
User feedback: "all-or-nothing" is too rigid. A shared interface used by several contracts can't
hold per-implementation differences (e.g. one contract accepts a narrower input range than a
sibling), so those must be documented on the implementation. New rule (comment-types.md):
- Don't re-state what the base already says (the duplication we hit on
AccessControl.grantRole). - Do add a delta that is specific to this implementation and cannot live in the shared base —
as
@inheritdoc+ a short@devnote (just the specific part) or a standalone doc, per project. - Two gaps, two homes: applies-to-all → review fixes the interface; per-implementation → document the delta on the impl now.
Candidate test (folds in the thin-interface one): a shared interface with two implementations whose input constraints differ — confirm the agent inherits the common doc and documents only each impl's delta, without duplicating the shared parts.
Constructed (work/inheritdoc-test/): a documented IVault interface (deposit/withdraw, both
require amount > 0, each emits its event) and two implementations — BasicVault (no extra
constraints) and CappedVault (a global deposit cap; deposit additionally reverts CapExceeded).
Agent given the interface + context + stripped impls. Outputs: work/inheritdoc-test/*.green.sol.
Result matched the intended matrix exactly:
| function | expected | got |
|---|---|---|
BasicVault.deposit |
pure @inheritdoc |
✅ |
BasicVault.withdraw |
pure @inheritdoc |
✅ |
CappedVault.withdraw |
pure @inheritdoc (no delta) |
✅ |
CappedVault.deposit |
@inheritdoc + cap delta only |
✅ @dev Additionally reverts with CapExceeded if totalDeposited + amount would exceed cap. |
Notable judgment (stated by the agent): it did not restate the interface's amount > 0 / event;
it treated the over-withdraw underflow as already implied by the interface's "must have at least
amount" requirement; and it left CappedVault.withdraw a pure inherit because the totalDeposited
decrement is internal bookkeeping, not caller-facing. The softened rule (forbid duplication, allow a
per-implementation delta) works as intended. Declaration docs (/// on state vars, error, ctor
@param) are terse and correct.