Smart Contract Security Audit You are the orchestrator of a parallelized smart contract security audit. Mode Selection Exclude pattern: skip dependency and build directories — , , , , , , , — and non-production code — , , and files matching , or . Deploy scripts stay in scope. , and are audited like any other code: a deploy script sets constructor arguments, hands over ownership and seeds state, and it carries real bugs. Excluding a dependency is an argument about code you did not write; a deploy script is code you did write. - Default (no arguments): scan all files using the exclude pattern.…

\\t', and a\n# tab is IFS *whitespace*, so bash collapses `\\t\\t` into one delimiter and every later field\n# shifts left by one. That is how a lead's memory tag ended up printed as its location.\n: > \"$work/index.tsv\"\nfor f in ${runs[@]+\"${runs[@]}\"}; do\n awk -v F=\"$f\" '\n /^\u003c!--RUN / { line=$0; sub(/[[:space:]]*-->[[:space:]]*$/, \"\", line)\n n=split(line, a, /[[:space:]]+/)\n for (i=2; i\u003c=n; i++) { eq=index(a[i],\"=\"); if (substr(a[i],1,eq-1)==\"pass\") pass=substr(a[i],eq+1) } }\n /^\u003c!--F / {\n key=\"\"; conf=\"\"; kind=\"\"; agents=\"\"\n line = $0\n sub(/[[:space:]]*-->[[:space:]]*$/, \"\", line) # terminator off BEFORE splitting\n n = split(line, a, /[[:space:]]+/)\n for (i = 2; i \u003c= n; i++) {\n eq = index(a[i], \"=\"); nm = substr(a[i], 1, eq-1); v = substr(a[i], eq+1)\n if (nm == \"key\") key = v; else if (nm == \"conf\") conf = v\n else if (nm == \"kind\") kind = v; else if (nm == \"agents\") agents = v\n }\n if (conf == \"\") conf = \"-\" # a LEAD is not scored\n start = NR; title = \"\"; loc = \"\"; lbody = \"\"\n next\n }\n kind != \"LEAD\" && start && NR == start + 2 { t = $0; sub(/^\\[[0-9]+\\] /, \"\", t); sub(/^\\*\\*/, \"\", t); sub(/\\*\\*$/, \"\", t); title = t }\n kind != \"LEAD\" && start && NR == start + 4 { l = $0; if (match(l, /`[^`]*`/)) loc = substr(l, RSTART+1, RLENGTH-2) }\n\n # A lead is one line. Split it here, once, so the emitter formats leads and findings with\n # the same fields instead of re-printing a line that is already formatted.\n # `match` + RSTART/RLENGTH, never index()+a literal width: the separator is an em dash,\n # 3 bytes and 1 character, and the two disagree across awks and locales.\n kind == \"LEAD\" && start && NR == start + 2 && /^- \\*\\*/ {\n l = $0\n sub(/^- \\*\\*/, \"\", l)\n if (match(l, /\\*\\* — /)) { title = substr(l, 1, RSTART-1); l = substr(l, RSTART+RLENGTH) }\n else { title = l; l = \"\" }\n if (match(l, /^`[^`]*`/)) { loc = substr(l, RSTART+1, RLENGTH-2); l = substr(l, RSTART+RLENGTH) }\n # What is left is \" · NEW — body\" or \" — body\". Either way the body starts after the\n # first \" — \". The inline tag is DROPPED: the assembler derives the memory tag itself\n # from memory-before.tsv, and two sources for one fact is how they come to disagree.\n if (match(l, / — /)) l = substr(l, RSTART+RLENGTH); else l = \"\"\n lbody = l\n }\n /^\u003c!--\\/F-->/ && start {\n if (title == \"\") title = \"-\"\n if (loc == \"\") loc = \"-\"\n if (lbody == \"\") lbody = \"-\"\n print key \"\\t\" conf \"\\t\" kind \"\\t\" agents \"\\t\" F \"\\t\" pass \"\\t\" start \"\\t\" NR \"\\t\" (start+6) \"\\t\" (NR-1) \"\\t\" title \"\\t\" loc \"\\t\" lbody\n start = 0; kind = \"\"\n }\n ' \"$f\" >> \"$work/index.tsv\"\ndone\n\n# ---------------------------------------------------------------- the structure check\n# The model typed these markers. When the counts disagree, a finding was raised and cannot be\n# read — say so IN the report. The report may print less than the scan found; it may never\n# claim to print more.\n#\n# No run file at all is the same rule at its limit: pass 1 produced nothing, so the scan\n# reviewed nothing. That is a REPORT, not an exit — SKILL.md Turn 3b routes a real scan down\n# here, and a shell error in place of a report is the one outcome this skill must never have.\n# The row is necessary because the Passes row is suppressed on a 1-pass scan, so without it\n# nothing in the report would say the pass died.\nbroken=\nif [ \"$N\" = 0 ]; then\n broken=\"⚠️ No pass produced a run file. This scan reviewed nothing.\"\n echo \"assemble.sh: NO RUN FILES — $broken\" >&2\nelse\n opens=$(cat \"${runs[@]}\" | grep -c '^\u003c!--F ' || :)\n closes=$(cat \"${runs[@]}\" | grep -c '^\u003c!--/F-->' || :)\n indexed=$(wc -l \u003c \"$work/index.tsv\" | tr -d ' ')\n if [ \"$opens\" != \"$closes\" ] || [ \"$opens\" != \"$indexed\" ]; then\n broken=\"⚠️ $opens findings marked, $indexed readable. $(( opens - indexed )) could not be read and are missing below. See the run files.\"\n echo \"assemble.sh: STRUCTURE BROKEN — $broken\" >&2\n fi\nfi\n\n# ---------------------------------------------------------------- dedup across runs\n# One winner per key: strongest kind, then highest confidence, then the LATER pass — a later\n# pass read the ledger every earlier pass wrote, so it is the better-informed write-up.\n# k counts every run that raised the key, in either section.\nsort -t

Smart Contract Security Audit You are the orchestrator of a parallelized smart contract security audit. Mode Selection Exclude pattern: skip dependency and build directories — , , , , , , , — and non-production code — , , and files matching , or . Deploy scripts stay in scope. , and are audited like any other code: a deploy script sets constructor arguments, hands over ownership and seeds state, and it carries real bugs. Excluding a dependency is an argument about code you did not write; a deploy script is code you did write. - Default (no arguments): scan all files using the exclude pattern.…

\\t' -k1,1 \"$work/index.tsv\" | awk -F'\\t' '\n function rank(k) { return (k == \"FINDING\") ? 1 : 0 }\n {\n if (!(($5 SUBSEP $1) in seenrun)) { seenrun[$5 SUBSEP $1]=1; k[$1]++ }\n c = ($2 == \"-\") ? -1 : $2 + 0\n if (!($1 in best) || rank($3) > br[$1] || (rank($3) == br[$1] && c > bc[$1]) \\\n || (rank($3) == br[$1] && c == bc[$1] && $6 + 0 >= bp[$1])) {\n best[$1] = $0; br[$1] = rank($3); bc[$1] = c; bp[$1] = $6 + 0\n }\n }\n END { for (key in best) print k[key] \"\\t\" best[key] }\n' > \"$work/merged.tsv\"\n\n# ---------------------------------------------------------------- the memory tag\n# Derived here from memory-before.tsv, not carried in the marker. One source: the report and\n# the ledger cannot disagree because both read the same photocopy.\n# KNOWN (n scans) — the STORED count plus one; this scan has just found it again.\nif [ \"$memory_on\" = 1 ]; then\n awk -F'\\t' -v OFS='\\t' '\n FILENAME==ARGV[1] { if (FNR>1) sc[$1]=$3; next }\n { print $0, ($2 in sc) ? \"KNOWN (\" sc[$2]+1 \" scans)\" : \"NEW\" }\n ' \"$dir/memory-before.tsv\" \"$work/merged.tsv\" > \"$work/merged.tagged.tsv\"\nelse\n awk -F'\\t' -v OFS='\\t' '{ print $0, \"\" }' \"$work/merged.tsv\" > \"$work/merged.tagged.tsv\"\nfi\n\n# columns now: 1 k · 2 key · 3 conf · 4 kind · 5 agents · 6 file · 7 pass · 8 start · 9 end\n# 10 bodystart · 11 bodyend · 12 title · 13 loc · 14 leadbody · 15 memtag\n# 12, 13 and 14 are never empty — `-` stands in, because IFS=

Smart Contract Security Audit You are the orchestrator of a parallelized smart contract security audit. Mode Selection Exclude pattern: skip dependency and build directories — , , , , , , , — and non-production code — , , and files matching , or . Deploy scripts stay in scope. , and are audited like any other code: a deploy script sets constructor arguments, hands over ownership and seeds state, and it carries real bugs. Excluding a dependency is an argument about code you did not write; a deploy script is code you did write. - Default (no arguments): scan all files using the exclude pattern.…

\\t' collapses adjacent tabs.\nsort -t

Smart Contract Security Audit You are the orchestrator of a parallelized smart contract security audit. Mode Selection Exclude pattern: skip dependency and build directories — , , , , , , , — and non-production code — , , and files matching , or . Deploy scripts stay in scope. , and are audited like any other code: a deploy script sets constructor arguments, hands over ownership and seeds state, and it carries real bugs. Excluding a dependency is an argument about code you did not write; a deploy script is code you did write. - Default (no arguments): scan all files using the exclude pattern.…

\\t' -k4,4 -k3,3nr -k2,2 \"$work/merged.tagged.tsv\" > \"$work/sorted.tsv\"\nawk -F'\\t' '$4==\"FINDING\"' \"$work/sorted.tsv\" > \"$work/findings.tsv\"\nawk -F'\\t' '$4==\"LEAD\"' \"$work/sorted.tsv\" > \"$work/leads.tsv\"\n\nnfind=$(wc -l \u003c \"$work/findings.tsv\" | tr -d ' ')\nnlead=$(wc -l \u003c \"$work/leads.tsv\" | tr -d ' ')\n\n# ---------------------------------------------------------------- emit\nR=\"$work/report.md\"\n: > \"$R\"\nsay() { printf '%s\\n' \"$1\" >> \"$R\"; }\nrule() { say \"\"; say \"---\"; say \"\"; }\nrow() { printf '| %s | %s |\\n' \"$1\" \"$2\" >> \"$R\"; }\n\nsay \"# 🔐 Security Review — $name\"\nrule\nsay \"## Scope\"\nsay \"\"\nrow \"\" \"\"\nrow \"---\" \"---\"\nrow \"**Mode**\" \"$mode\"\n\n# Files reviewed — 3 per line, in source.md order, joined with `\u003cbr>`. One `files` key holds\n# them space separated; shell does the wrapping.\nfiles=$(scope files | tr ' ' '\\n' | awk 'NF { printf \"`%s`\", $0; if (++n % 3 == 0) printf \"\u003cbr>\"; else printf \" · \" }' \\\n | sed -e 's/ · $//' -e 's/\u003cbr>$//')\nrow \"**Files reviewed**\" \"$files\"\nrow \"**Confidence threshold (1-100)**\" \"$THRESHOLD\"\n\n# The Passes cell is COMPOSED from facts, never written as prose by a model.\nif [ -n \"$passes_planned\" ] && [ \"$passes_planned\" -gt 1 ] 2>/dev/null; then\n short=\"\"\n for k in $(seq 1 \"$passes_planned\"); do\n a=$(scope \"pass_${k}_agents\"); fail=$(scope \"pass_${k}_failed\")\n if [ -n \"$fail\" ]; then short=\"${short:+$short, }pass $k failed\"\n elif [ -n \"$a\" ] && [ \"$a\" != \"12/12\" ]; then short=\"${short:+$short, }pass $k ran $a agents\"; fi\n done\n if [ \"$N\" -lt \"$passes_planned\" ]; then cell=\"$N of $passes_planned\"; else cell=\"$passes_planned\"; fi\n [ -n \"$short\" ] && cell=\"$cell ($short)\"\n row \"**Passes**\" \"$cell\"\nfi\n[ \"$memory_on\" = 1 ] && row \"**Memory**\" \"$mem_before records before this scan · $mem_after after · \\`$mem_sha\\`\"\n[ -n \"$broken\" ] && row \"**Run files**\" \"$broken\"\nrule\n\nsay \"## Findings\"\nsay \"\"\n\nemit_body() { # file bodystart bodyend — copied through, never re-worded\n awk -v s=\"$2\" -v e=\"$3\" 'NR>=s && NR\u003c=e' \"$1\" \\\n | awk '{ lines[++n] = $0 } END {\n b=1; while (b\u003c=n && lines[b]==\"\") b++\n while (n>=b && lines[n]==\"\") n--\n for (i=b; i\u003c=n; i++) print lines[i] }'\n}\n\ni=0\nwhile IFS=

Smart Contract Security Audit You are the orchestrator of a parallelized smart contract security audit. Mode Selection Exclude pattern: skip dependency and build directories — , , , , , , , — and non-production code — , , and files matching , or . Deploy scripts stay in scope. , and are audited like any other code: a deploy script sets constructor arguments, hands over ownership and seeds state, and it carries real bugs. Excluding a dependency is an argument about code you did not write; a deploy script is code you did write. - Default (no arguments): scan all files using the exclude pattern.…

\\t' read -r k key conf kind agents file pass start end bstart bend title loc lbody memtag; do\n i=$((i+1))\n [ \"$title\" = \"-\" ] && title=\"**Title missing** — the block's title line could not be read\"\n if [ \"$loc\" = \"-\" ]; then meta=\"**location missing**\"; else meta=\"\\`$loc\\`\"; fi\n say \"[$conf] **$i. $title**\"\n say \"\"\n meta=\"$meta · Confidence: $conf\"\n [ \"$N\" -gt 1 ] && meta=\"$meta · seen in $k/$N runs\"\n [ -n \"$memtag\" ] && meta=\"$meta · $memtag\"\n say \"$meta\"\n say \"\"\n body=$(emit_body \"$file\" \"$bstart\" \"$bend\")\n if [ -z \"$body\" ]; then\n say \"**Body missing** — pass $pass raised this finding and wrote no description.\"\n else\n printf '%s\\n' \"$body\" >> \"$R\"\n fi\n rule\ndone \u003c \"$work/findings.tsv\"\n\nif [ \"$nfind\" = 0 ]; then\n # An empty table with a header row and no rows under it is not a report of nothing, it is a\n # report that looks broken. Say it in words instead.\n say \"_None — this scan raised no findings._\"\n rule\nelse\nsay \"Findings List\"\nsay \"\"\nsay \"| # | Confidence | Title |\"\nsay \"|---|---|---|\"\ni=0; sep=0\nwhile IFS=

Smart Contract Security Audit You are the orchestrator of a parallelized smart contract security audit. Mode Selection Exclude pattern: skip dependency and build directories — , , , , , , , — and non-production code — , , and files matching , or . Deploy scripts stay in scope. , and are audited like any other code: a deploy script sets constructor arguments, hands over ownership and seeds state, and it carries real bugs. Excluding a dependency is an argument about code you did not write; a deploy script is code you did write. - Default (no arguments): scan all files using the exclude pattern.…

\\t' read -r k key conf kind agents file pass start end bstart bend title loc lbody memtag; do\n i=$((i+1))\n if [ \"$sep\" = 0 ] && [ \"$conf\" -lt \"$THRESHOLD\" ]; then\n say \"| | | **Below Confidence Threshold** |\"; sep=1\n fi\n say \"| $i | [$conf] | $title |\"\ndone \u003c \"$work/findings.tsv\"\nrule\nfi\n\nsay \"## Leads\"\nsay \"\"\nsay \"_Vulnerability trails with concrete code smells where the full exploit path could not be completed in one analysis pass. These are not false positives — they are high-signal leads for manual review. Not scored._\"\nsay \"\"\n[ \"$nlead\" = 0 ] && say \"_None._\"\nwhile IFS=

Smart Contract Security Audit You are the orchestrator of a parallelized smart contract security audit. Mode Selection Exclude pattern: skip dependency and build directories — , , , , , , , — and non-production code — , , and files matching , or . Deploy scripts stay in scope. , and are audited like any other code: a deploy script sets constructor arguments, hands over ownership and seeds state, and it carries real bugs. Excluding a dependency is an argument about code you did not write; a deploy script is code you did write. - Default (no arguments): scan all files using the exclude pattern.…

\\t' read -r k key conf kind agents file pass start end bstart bend title loc lbody memtag; do\n seg=\"\"\n [ \"$N\" -gt 1 ] && seg=\"$seg · seen in $k/$N runs\"\n [ -n \"$memtag\" ] && seg=\"$seg · $memtag\"\n # The body was parsed out of the lead's own line by the indexer. Do NOT read it back out of\n # the run file here: a lead has no body region, so emit_body returned nothing and every lead\n # got the \"Body missing\" line whether or not it had one.\n [ \"$title\" = \"-\" ] && title=\"**Title missing** — the lead's line could not be read\"\n if [ \"$loc\" = \"-\" ]; then locmd=\"**location missing**\"; else locmd=\"\\`$loc\\`\"; fi\n [ \"$lbody\" = \"-\" ] && lbody=\"**Body missing** — pass $pass raised this lead and wrote no description.\"\n say \"- **$title** — $locmd$seg — $lbody\"\ndone \u003c \"$work/leads.tsv\"\n\n# ---------------------------------------------------------------- Known from earlier scans\n# The keys this scan raised come from the index — the run files are the record, so no separate\n# run-keys.tsv or scan-rows.tsv has to be carried in.\nif [ \"$memory_on\" = 1 ]; then\n held=$(( $(wc -l \u003c \"$dir/memory-before.tsv\") - 1 ))\n if [ \"$held\" -gt 0 ]; then\n tail -n +2 \"$dir/memory-before.tsv\" | cut -f1 | sort -u > \"$work/keys-before.txt\"\n cut -f1 \"$work/index.tsv\" | sort -u > \"$work/keys-scan.txt\"\n comm -23 \"$work/keys-before.txt\" \"$work/keys-scan.txt\" > \"$work/keys-left.txt\"\n rule\n say \"## Known from earlier scans\"\n say \"\"\n say \"_Recorded by earlier scans of this repo, not raised again by this one. Not re-checked — a record here may be fixed, or may still be live and missed. \\`.solidity-auditor/memory.tsv\\`._\"\n say \"\"\n if [ -s \"$work/keys-left.txt\" ]; then\n say \"| Scans | Kind | Location | Title |\"\n say \"|---|---|---|---|\"\n awk -F'\\t' '\n FILENAME==ARGV[1] { nm[$1]=$2; next }\n FILENAME==ARGV[2] { if (FNR>1) { sc[$1]=$3; ti[$1]=$5; ki[$1]=$6 } ; next }\n {\n split($1, p, \"|\")\n c = (p[1] in nm) ? nm[p[1]] : p[1] # print-as-stored when the segment is not in the map\n f = (p[2] in nm) ? nm[p[2]] : p[2]\n print \"| \" sc[$1] \" | \" ki[$1] \" | `\" c \".\" f \"` | \" ti[$1] \" |\"\n }\n ' \"$dir/source-names.tsv\" \"$dir/memory-before.tsv\" \"$work/keys-left.txt\" >> \"$R\"\n else\n say \"_None — every record in the ledger was raised again by this scan._\"\n fi\n fi\nfi\n\nrule\nsay \"> ⚠️ This review was performed by an AI assistant. AI analysis can never verify the complete absence of vulnerabilities and no guarantee of security is given. Team security reviews, bug bounty programs, and on-chain monitoring are strongly recommended. For a consultation regarding your projects' security, visit [https://www.pashov.com](https://www.pashov.com)\"\n\ncp \"$R\" \"$out.part\"\nmv \"$out.part\" \"$out\"\necho \"assembled $out — $nfind findings, $nlead leads, $N run files\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":16718,"content_sha256":"f81c647ea530f0d0827e67b8a822673ebd559e07194f934f3096dc68a30675b7"},{"filename":"references/dedup-and-assembly.md","content":"# Dedup, record and assembly — Turn 4 and Turn 5\n\nThis file holds the two turns that run **after the 12 agents return**: Turn 4\ndeduplicates, gates, tags and records each pass, and Turn 5 assembles and prints\nthe one report. `SKILL.md` keeps the name of each turn and the condition that\ndecides whether it runs; the procedure is here.\n\nThe orchestrator reads this file **once, in Turn 3b**, while it waits for the\nagents. It stays in context for the later passes of a loop.\n\nBoth turns use `{stamp}`, `{bundle_dir}`, `{passes}`, `{resolved_path}` and\n`{project-name}` exactly as `SKILL.md` set them.\n\n---\n\n## Turn 4\n\n**Turn 4 — Deduplicate, validate & record.** Runs **every pass**, and it is the end of the loop body: deduplicate this pass's agent results, gate-evaluate, tag, record the pass, write the ledger. **It prints no report** — there is exactly one report per scan and Turn 5 prints it. Do NOT print an intermediate dedup list.\n\n> **This turn used to print the report and clean up, and both moved.** The report moved to Turn 5 so that a 1-pass scan and an N-pass scan take the **same** path through this turn — the \"1 pass is byte-identical to today\" constraint is easier to hold with one report writer than with a last pass that doubles as one. The auto-clean moved to Turn 5 because, left here, pass 1 deletes what pass 2 needs: `source.md`, the ledger photocopy and the accumulating rows file. A later editor must not move either back.\n\nThen, after the loop body has run `{passes}` times (or stopped early), go to Turn 5 once.\n\n1. **Dedup.** Parse every FINDING and LEAD from the 12 agents. Group by `group_key` (Contract | function | bug-class). Exact-match first; merge synonymous bug_class within same (Contract, function). Keep best per group, number sequentially, annotate `[agents: N]`.\n\n **MANDATORY — Canonicalise the bug-class label (HARD GATE).** Before grouping, for every (Contract, function) whose agents used **more than one** bug-class label, choose **one** label and rewrite every one of those findings to carry it. Then group.\n\n Choose in this order, and stop at the first that applies:\n 1. A label already listed for that same contract and function in `{bundle_dir}/known-findings.md` — the repository's own word wins, always.\n 2. Otherwise the label that most agents used.\n 3. Otherwise the shortest label that names the **defect**, not its consequence. `zero-abort-address` over `abort-address-zero-burns-funds`.\n\n Merging under one label is only correct when the labels name the **same class of bug**. Two genuinely different bugs in one function keep two labels and stay two records — function isolation is about functions, and this rule is about words.\n\n **Why this is a gate and not a nicety.** The per-function vocabulary that keeps a key stable only constrains what an **earlier scan** wrote. A class discovered mid-scan is constrained by nothing, and a real scan proved what follows: three agents in one pass named one bug `zero-abort-address`, `unregistered-abort-address` and `abort-address-zero-burns-funds`, and the over-approval class drew three labels across three passes. Unfixed, that is three ledger records for one bug, three lines in every future `known-findings.md`, and one bug that is never recognised again. This step is the only thing standing between the fork and the ledger. Canonicalise here and the next pass inherits the canonical label, so the fork closes after one scan.\n\n **MANDATORY — Wide-description (group_key).** Merged group with distinct mechanisms (different `fix:`, code-level cause, or attack path) MUST list every mechanism. No dropping. Same function can have multiple coexisting bugs at the same group_key — all MUST appear.\n\n **MANDATORY — Function-level second pass (after group_key dedup).** Run at (Contract, function), ignoring bug_class. Agents often label coexisting bugs with different bug_class tags but reference multiple mechanisms in the body. For every (Contract, function) with multiple final findings: scan body (description, path, proof, fix) of every constituent for distinct mechanisms across bug_class boundaries. Every mechanism in any constituent body MUST appear in ≥1 final finding.\n\n **MANDATORY — Function isolation (HARD).** NEVER merge across different `function:` fields. Dedup only within (Contract, function). Different function = different bug. Second pass above stays WITHIN (Contract, function), never across.\n\n **MANDATORY — Fix preservation (HARD GATE).** Before writing merged `fix:` on a multi-finding (Contract, function):\n 1. Collect every raw `fix:` from agents flagging the tuple.\n 2. Group by ADD-lines (`+` lines, or equivalent require/assignment).\n 3. Distinct if ADD-lines differ in: called function/expression (e.g., `require(msg.value == amount)` vs `require(zrc20 != _ETH_ADDRESS_)`), check direction (validate/restrict/ban), or checked parameter.\n 4. ≥2 distinct → present as Option A, B, … — one block per distinct fix, verbatim from agent text (no paraphrase).\n 5. Label intuitively: validate / restrict / allow-and-handle / ban-path.\n\n **Output format when 2+ distinct fixes exist:**\n\n ```\n **Fix (Option A — \u003clabel>)**:\n\n ```diff\n \u003cverbatim diff from raw agent N1's fix>\n ```\n\n **Fix (Option B — \u003clabel>)**:\n\n ```diff\n \u003cverbatim diff from raw agent N2's fix>\n ```\n ```\n\n **Inline check before printing**: count distinct fixes from raw for this (Contract, function). ≥2 distinct but merged shows 1 → violation, add alternatives.\n\n **MANDATORY — Completeness (HARD GATE).** Before print: list every unique (Contract, function, bug-class) in any raw FINDING/LEAD across the 12 agents. Every unique (Contract, function) MUST have ≥1 item in final. Zero = silent drop, fix it. Multiple bug-class within same (Contract, function) MAY collapse to one item (wide-description), but the (Contract, function) MUST survive. Print inline before report: `Completeness: N unique (Contract, function) in raw, N covered in final.`\n\n Composite chains: if A's output feeds B's precondition AND combined impact > either alone, add `Chain: [A] + [B]` at conf = min(A, B). Most audits: 0–2.\n\n2. **Gate.** Run each deduped finding through the four gates in `judging.md` (no skip, no reorder, no revisit after verdict).\n\n **Single-pass:** every relevant code path ONCE in fixed order (constructor → setters → swap → mint → burn → liquidate). One-line verdict: `BLOCKS` / `ALLOWS` / `IRRELEVANT` / `UNCERTAIN`. `UNCERTAIN = ALLOWS`. Commit, no re-examination.\n\n3. **Lead promotion / rejection.**\n - LEAD → FINDING (conf 75) if: full exploit chain in source, OR `[agents: 2+]` demoted (not rejected) same issue.\n - `[agents: 2+]` does NOT override a code path that interrupts attack before harm — demote to LEAD if execution uncertain.\n - No deployer-intent reasoning — what code allows, not how deployer might use it.\n\n4. **Memory tag.** **SKIP this step entirely when memory is off.** It comes before the report, not after it, because the report prints what it decides. It is a **lookup, never a judgment** — no finding is re-argued here, and no verdict from the gate is revisited.\n\n a. **Build the key** for every gated FINDING and every gated LEAD — the three segments of Turn 4's `group_key`, each normalised **on its own** (lower case, every run of non-alphanumeric characters to one hyphen), then joined with `|`: `vault|withdraw|reentrancy`. Normalise the segments separately, never the joined string, or the separators become hyphens too.\n\n **The bug-class segment is where memory succeeds or fails.** Before writing a key, read the per-function vocabulary back from `{bundle_dir}/known-findings.md` — the same file the agents were given: *if this finding is one of the bug classes already listed for this same contract and function, write that exact label. Invent a new label only when none of them is the same class of bug.* This is the only judgment in the whole memory path, and it happens when the label is **written** — never at merge time. The keys built here are the keys step 6 writes; build them once.\n\n b. **Tag each finding and each lead.** Look its key up in the pre-scan key list:\n\n ```bash\n tail -n +2 {bundle_dir}/memory-before.tsv | cut -f1,3 | sort > {bundle_dir}/known-keys.tsv\n ```\n\n Key present → **KNOWN**, printed as `KNOWN (n scans)` where **n is column 2 plus one**. The scan that is printing has found it again, so the count the report shows is the count the ledger will hold when step 6 has written it — report and file say the same number. Key absent → **NEW**. Nothing else decides this tag.\n\n The lookup is against the **pre-scan photocopy**, in every pass of a loop. `KNOWN` means \"an earlier **scan** recorded this\"; a finding first raised by pass 1 of this scan is still `NEW` in pass 3, and how many passes saw it is what `seen in k/N runs` says. Tagging against the live ledger would make pass 2 call this scan's own work old news.\n\n c. **Write this run's keys** to `{bundle_dir}/run-keys.tsv`, one per line, from step 4a.\n\n The \"Known from earlier scans\" rows are **not** built here. They are report work, and a pass that built them could not know what a later pass is about to raise. They are built once, in Turn 5.\n\n5. **Record the pass.** Do NOT print the report.\n\n a. **Write the run file** to `.solidity-auditor/runs/{stamp}/run-K.md`, where `K` is the pass number. **Every pass of every scan writes one**, at any pass count, `{passes}` of 1 included. This is the only place this pass's work is recorded, and Turn 5 assembles the report out of these files — a pass that writes no run file has produced nothing the report can print.\n\n One directory per scan, one file per run; `ls` is history in order. The git SHA goes **inside** the file, not in the directory name, so two scans of the same commit sit side by side instead of colliding. Nothing deletes these files, and `--file-output` does not govern them — they are memory, not reports.\n\n **The file, exactly:**\n\n ````markdown\n # Run K — solidity-auditor\n\n \u003c!--RUN pass=K of=N stamp={stamp} sha=abc1234 agents=11/12-->\n\n Pass K of N · \u003cdate> · `abc1234` · 11/12 agents returned — the access-control agent died.\n\n ## Findings\n\n \u003cfinding blocks, kind=FINDING>\n\n ## Leads\n\n \u003cfinding blocks, kind=LEAD>\n ````\n\n The sentence is for the human; the `\u003c!--RUN-->` marker is what shell reads. `agents=11/12` is where the `Passes` row gets its degradation text, so a lost agent is on disk and not only in a printed line that scrolls away — and the dead agent's **name** stays here, in the sentence, and never enters the Scope table.\n\n **These two headings and no others, in every run.** No `## New findings` in a later run — newness is already carried by the `NEW` / `KNOWN` tag and by `seen in k/N runs`, and a heading saying it again is a second source of truth that can disagree with the first. **No \"verified clean\", \"checked\" or \"sound\" section, under any name.** A path a pass did not raise is not a path a pass cleared, and a real scan invented such a section unasked. Nothing ever reads it back, and it is the same overstated-coverage defect this whole design exists to kill.\n\n **The finding block, exactly:**\n\n ````markdown\n \u003c!--F key=contract|function|bug-class conf=95 kind=FINDING agents=8-->\n\n [95] **\u003cTitle>**\n\n `ContractName.functionName` · Confidence: 95\n\n **Description**\n \u003cthe vulnerable pattern and why it is exploitable, 1 short sentence>\n\n **Fix**\n\n ```diff\n - vulnerable line(s)\n + fixed line(s)\n ```\n\n \u003c!--/F-->\n ````\n\n - **The markers bound the block.** `\u003c!--F ` opens it, `\u003c!--/F-->` closes it, each on its own line at the start of the line. **Never `---`**: today's separator also appears inside a unified diff (`--- a/Vault.sol`), so it cannot cut a block that now carries one. The HTML comment renders invisible, so the file still reads as human memory.\n - **`key`** — exactly the key step 4a built, unchanged. Never re-derived here. The same key the ledger stores, so the run file and `memory.tsv` agree by construction.\n - **`conf`** — an integer, on `kind=FINDING` blocks only. **A `kind=LEAD` block carries no `conf` attribute at all** — a lead is not scored, and writing `0` would sort it against real numbers.\n - **`kind`** — `FINDING` or `LEAD`.\n - **`agents`** — carried from `[agents: 8]`. Informational; nothing parses it.\n\n **Write the markers exactly as printed.** They are the only thing standing between a finding and a silent loss: the assembler counts open markers against close markers against readable blocks, and a disagreement becomes a visible `**Run files**` row in the report saying how many findings could not be read. The report may print **less** than the scan found; it may never claim to print **more**. A fudged marker costs a visible warning, not a hidden hole — but it still costs the finding.\n\n **Above and below the threshold, the block shape is identical.** A block whose `conf` is below 75 simply has no `**Fix**` section. The run file does **not** mark which side of the line a finding sits on; the assembler re-derives that from `conf`. One source of truth.\n\n **Write the Description and the diff Fix block here, in full.** This is the expensive per-finding writing, and it is paid **now**, in the pass that found the bug, while that pass still has context to spare. Turn 5 re-words nothing — it pastes these bytes into the report. An orchestrator drafting seventy findings at the end of a long scan is the failure this ordering exists to prevent.\n\n **The title and the Description are written in Simplified Technical English**, per `{resolved_path}/report-language.md`, which Turn 2 read. Because Turn 5 re-words nothing, this is the **only** step where the report's sentences can be made readable — there is no later cleanup pass and there must never be one. Four pieces of text obey that file: the title, the Description, the Lead description, and the Lead's code-smell list.\n\n Three things it does **not** reach, and a later editor must not extend it to them: the **diff** inside the Fix block, which is pasted verbatim from the agent, the **bug-class label** in the `key` attribute, which is a memory key and is chosen by step 1 and step 4a, and the **identifiers** on the location line, which are spelled as the source spells them. Softening a label to read better writes a second ledger record for one bug.\n\n Where the agent handed up a sentence that already meets the rules, keep the agent's words. Where it did not — a metaphor, an `-ing` clause, forty words, `catastrophic` — rewrite the sentence and keep the claim. Rewriting is about the wording only: the mechanism, the actor and the effect the agent proved are not up for revision here, and the gate has already run.\n\n **Then record the agent count**, once per pass, always, `12/12` included:\n\n ```bash\n printf '%s\\t%s\\n' pass_{K}_agents \"11/12\" >> .solidity-auditor/runs/{stamp}/scope.tsv\n ```\n\n Written only when a pass ran short, a missing key would mean two different things — a whole pass and a lost pass — and the assembler could not tell them apart.\n\n b. **Print one summary line**, only when `{passes}` is above 1, and nothing else — the last thing the pass does, after step 6 has written the ledger. It is the only thing the runner sees for minutes at a time, so it answers two questions: how far along, and is the loop still learning?\n\n ```\n Pass 2/3 — 17 findings (3 new) · 6 leads (2 new) · ledger 41 records\n ```\n\n - The counts are post-dedup and post-gate — the numbers `run-2.md` holds, not raw agent output.\n - **new** = the key was not in the ledger before this pass. A lookup, not a judgment.\n - `ledger N records` is the row count of `.solidity-auditor/memory.tsv` after step 6 has written it, so print this line after step 6.\n - The parentheses are printed **only when the ledger was non-empty when the scan started**. On a first-ever scan every key is new and they say nothing: `Pass 1/3 — 17 findings · 6 leads · ledger 17 records`.\n - When a pass lost an agent, the line gains a clause: `Pass 2/3 — 15 findings (2 new) · 6 leads (0 new) · ledger 39 records · 11/12 agents`.\n - When **both** new-counts are zero, the line ends with `· no new ground`. That is the loop's most informative outcome — the passes have converged — and it must not have to be inferred from two zeros:\n\n ```\n Pass 3/3 — 17 findings (0 new) · 6 leads (0 new) · ledger 41 records · no new ground\n ```\n\n Elapsed time is deliberately not printed: it needs a `date` call either side of every pass, and the runner's terminal already shows wall-clock.\n\n > **On a 1-pass scan this whole step prints nothing.** Step 5a still writes `run-1.md` — silently. `Pass 1/1 — …` is a progress line for a loop that is not running, and the plain path prints the bytes it printed before loop mode existed. Writing a file is not printing; the freeze covers printed output.\n\n6. **Memory write.** **SKIP this step entirely when memory is off.** It is a step of Turn 4 and not a turn of its own because it can only be built from this turn's gated findings — the rows do not exist until the gate has run.\n\n a. **Build this run's rows** from the keys step 4 already built. One row per gated FINDING and per gated LEAD, four tab-separated columns:\n\n ```\n key\tsha\ttitle\tkind\n ```\n\n - `key` — exactly the key step 4a built and step 4b tagged. Never rebuilt here: a key that differs between the tag and the row would report `NEW` and store a duplicate.\n - `sha` — `git rev-parse --short HEAD`. Not a git repo, or a repo with no commits: write `none` and carry on. The SHA is never part of the match, so a missing one costs nothing.\n - `title` — the finding title, tabs and newlines replaced by a single space, cut to 80 characters. Bars are safe: only the key is ever split on bars.\n - `kind` — `FINDING` or `LEAD`.\n\n Append the rows to the accumulating file, do not overwrite it:\n\n ```bash\n cat >> {bundle_dir}/scan-rows.tsv\n ```\n\n b. **Merge.** One `awk` pass over the photocopy and this scan's accumulated rows. `sort` + `join` is not used: `join` returns only matching rows, so keeping old-only and new-only rows would need three invocations and identical sort order across locales.\n\n ```bash\n awk -F'\\t' -v OFS='\\t' '\n FILENAME==ARGV[1] {\n if (FNR==1) next\n o_st[$1]=$2; o_sc[$1]=$3; o_sha[$1]=$4; o_ti[$1]=$5; o_ki[$1]=$6\n if (!($1 in o_seen)) { o_seen[$1]=1; oord[++on]=$1 }\n next\n }\n {\n if (!($1 in n_seen)) { n_seen[$1]=1; nord[++nn]=$1 }\n n_sha[$1]=$2; n_ti[$1]=$3; n_ki[$1]=$4\n }\n END {\n print \"#solidity-auditor-memory v1\", \"key\", \"status\", \"scans\", \"sha\", \"title\", \"kind\"\n for (i=1; i\u003c=on; i++) {\n k = oord[i]\n if (k in n_seen) print k, \"KNOWN\", o_sc[k]+1, n_sha[k], n_ti[k], n_ki[k]\n else print k, o_st[k], o_sc[k], o_sha[k], o_ti[k], o_ki[k]\n }\n for (i=1; i\u003c=nn; i++) {\n k = nord[i]\n if (!(k in o_seen)) print k, \"NEW\", 1, n_sha[k], n_ti[k], n_ki[k]\n }\n }\n ' {bundle_dir}/memory-before.tsv {bundle_dir}/scan-rows.tsv > .solidity-auditor/memory.tsv.tmp\n ```\n\n The three cases, all visible in one command:\n\n | Case | Result |\n |---|---|\n | Old row re-found | `status` `KNOWN`, `scans` + 1, `sha` / `title` / `kind` taken from **this scan's** row |\n | Old row not re-found | carried over completely unchanged |\n | No old row | `status` `NEW`, `scans` 1 |\n\n Two rules the merge carries, and a later editor must not simplify away:\n\n - **`kind` is not part of the key.** A Lead an earlier scan recorded and this scan proved is the *same bug*: its `kind` flips to `FINDING` in place, `scans` keeps counting, `status` stays `KNOWN`. Demotion is the same rule running backwards and needs no special case. Putting `kind` in the key would remember one bug twice and list it twice.\n - **The merge is idempotent.** It always reads the pre-scan photocopy plus everything this scan has found so far, so `scans` rises by exactly 1 however many times it runs inside one scan. \"Have I already counted this key?\" is a question that cannot arise. Where one key appears more than once in `scan-rows.tsv`, the last row wins — which is what makes a Lead proven by a later pass land as `FINDING`.\n\n c. **Move it into place.**\n\n ```bash\n mv .solidity-auditor/memory.tsv.tmp .solidity-auditor/memory.tsv\n ```\n\n The temporary file sits beside the real one so the move stays on one disk. **The new file is always written before the old one is replaced** — an interrupted scan leaves one whole ledger, never half of one. Never write `memory.tsv` directly.\n\n `mkdir -p .solidity-auditor` before the first write. The user is expected to gitignore that directory; the ledger is not shared between machines or people.\n\n **Then write the other two `mem_` keys**, every pass, immediately after the move:\n\n ```bash\n printf '%s\\t%s\\n' mem_after \"$(( $(wc -l \u003c .solidity-auditor/memory.tsv) - 1 ))\" \\\n >> .solidity-auditor/runs/{stamp}/scope.tsv\n printf '%s\\t%s\\n' mem_sha \"$(git rev-parse --short HEAD 2>/dev/null || echo none)\" \\\n >> .solidity-auditor/runs/{stamp}/scope.tsv\n ```\n\n Both are re-written on every pass and **last wins**, so the `Memory` row ends up describing the ledger as the scan left it, with no delete step and no \"have I written this already?\" flag. `mem_before` is not touched here — it was written once, in Turn 2 step 2c, and the row's arithmetic depends on it staying put.\n\n d. **In a loop, this step runs after every pass, and it rebuilds rather than appends.** Pass K+1 learns what pass K found by reading the merged ledger, so the write cannot wait for the end. But re-running the merge N times must not raise `scans` by N, because `scans` counts scans. The command above already holds this: it always reads the **pre-scan photocopy** plus **everything this scan has accumulated so far**, so `scans` rises by exactly 1 however many passes ran. Nothing new is invented per pass, and no \"have I counted this key already?\" flag exists to get wrong. It also keeps the interrupt rule for the whole loop: a loop killed at pass 3 keeps what passes 1 and 2 learned, written atomically.\n\n Then the next pass starts at **Turn 2 step 2c**, which rebuilds `known-findings.md` from the freshly merged `.solidity-auditor/memory.tsv` — not from the photocopy, or pass K+1 would never see pass K's findings — and re-cats the twelve bundles at Turn 2 step 3.\n\n## Turn 5\n\n**Turn 5 — Assemble, print, clean.** Runs **once per scan**, at any pass count, including a 1-pass scan and including a loop that stopped early. There is no path where a scan ends without this turn.\n\n> **This turn no longer composes a report, and must never be re-taught to.** It used to combine the runs, dedup them, build the \"Known from earlier scans\" rows and format the whole thing — all of it model work, all of it at the moment of least remaining context. On a real 3-pass scan that produced 71 findings, it printed 14 of them in full, collapsed the rest into two lines, and **claimed a coverage it did not have**. Nothing was lost from disk; the report lied about what it was showing. A prose instruction cannot fix that, because the orchestrator under context pressure is the thing that failed.\n>\n> So the step that must not lose a finding holds **no model judgment**. The dedup, the `seen in k/N runs` counts, the strongest-kind rule, the \"Known from earlier scans\" rows and the whole report body are `assemble.sh`'s work now, and it is shell. This turn copies two files, runs it, counts, and prints. If you are reading this turn and it looks too thin to be a reporting step — that is the point.\n\n1. **Copy the memory inputs in.** **SKIP when memory is off.** The assembler reads one directory, so anything it needs that lives in `{bundle_dir}` has to be in the runs directory before it runs:\n\n ```bash\n cp {bundle_dir}/memory-before.tsv {bundle_dir}/source-names.tsv .solidity-auditor/runs/{stamp}/\n ```\n\n `memory-before.tsv` is the **pruned** photocopy — the \"Known from earlier scans\" section is built from it, so a record the prune dropped must not reappear here. `source-names.tsv` turns a normalised key back into the spelling the source uses, so no reader is ever shown `gatewaytransfernative.withdrawtonativechain`.\n\n Only these two. `run-keys.tsv` and `scan-rows.tsv` are **not** copied: the keys this scan raised come from the assembler's own index of the run files, which is the same data with no copy step.\n\n2. **Assemble the report.**\n\n ```bash\n bash {resolved_path}/assemble.sh --dir .solidity-auditor/runs/{stamp}\n ```\n\n It writes `.solidity-auditor/runs/{stamp}/full-report.md` — the whole report, banner line down to the disclaimer, ready to print word for word. It writes to a `.part` file and moves it into place, so a half-report never exists.\n\n **This is the only producer of a report in this skill.** Do not re-word its output, do not re-order it, do not add to it and do not summarise it in your own words. Do not re-read source to \"verify the most critical claim\" either — the agents did that and the gate filtered it; re-verification costs about five minutes and rarely changes a verdict.\n\n If it exits non-zero, print its stderr and stop. It has told you it could not read its own inputs, and there is nothing for a model to salvage by hand.\n\n3. **Count the findings, then branch.** One `awk` over the assembled file, no model arithmetic anywhere:\n\n ```bash\n awk '\n /^## Leads$/ { sec=\"leads\"; next }\n /^## / { sec=\"\"; next }\n /^\\[[0-9]+\\]/ { F++; next }\n sec==\"leads\" && /^- \\*\\*/ { L++ }\n /Confidence: [0-9]+/ && /^`/ {\n match($0, /Confidence: [0-9]+/)\n c = substr($0, RSTART+12, RLENGTH-12) + 0\n if (c >= 90) A++; else if (c >= 75) B++; else C++\n }\n END { printf \"%d %d %d %d %d\\n\", F, L, A, B, C }\n ' .solidity-auditor/runs/{stamp}/full-report.md\n ```\n\n `F` is findings, `L` leads, and `A` / `B` / `C` the confidence buckets `100-90`, `89-75` and `below 75`.\n\n **Only `F` is printed** — it is the trigger and it is the total in `top 3 of {F}`. `L`, `A`, `B` and `C` are computed anyway and kept as a **self-check**, not as output.\n\n > **`A + B + C` must equal `F`.** The buckets are the whole set, in every mode. If they do not sum, the assembled file is malformed — a finding block the counter could not read is a finding block a reader may not get either. Say so and print the full report rather than a three-row slice of a file you cannot stand behind.\n\n **The trigger is more than 20 findings** — `F` alone. Leads are excluded, so a scan with 18 findings and 40 leads still prints in full.\n\n a. **`F` is 20 or fewer — print `full-report.md` word for word.** Every byte, nothing added, nothing cut, no preface. This is the path a plain scan of a small contract takes, and its printed bytes are exactly what this skill printed before any of this existed.\n\n b. **`F` is above 20 — print this instead**, and nothing else:\n\n ````markdown\n # 🔐 Security Review — {project-name}\n\n ---\n\n ## Scope\n\n {the Scope table from full-report.md, copied unchanged}\n\n ---\n\n Findings List — top 3 of {F}\n\n | # | Confidence | Title | Location | Seen |\n |---|---|---|---|---|\n {the three rows the extractor below printed}\n\n **→ Full findings list, every description and every fix:** {path}\n\n ---\n\n > ⚠️ {the disclaimer line from full-report.md, copied unchanged}\n ````\n\n - **`{path}`** — `.solidity-auditor/runs/{stamp}/full-report.md`, or, when `--file-output` was passed, the **copy** and only the copy: `{project-name}-pashov-ai-audit-report-{stamp}.md`. Never both. Two paths to identical bytes answers a question the runner did not ask at the moment they want one filename. This line is the only part of the block a flag changes.\n - **The Scope table's `Files reviewed` row is copied unchanged**, full paths and all. It is the one row that proves *which* `SafeMath.sol` was read, and a terminal that shortens it can no longer be diffed against the file it claims to summarise.\n - **No counts summary.** The `{F} findings, {L} leads` line and the `100-90 / 89-75 / below 75` split are **not** printed. `top 3 of {F}` already carries the only total a reader needs at this moment, and the bucket split answered a question nobody asked while standing between them and the path.\n\n > **The `{F}` in `top 3 of {F}` is what makes the slice safe, and it is not optional.** The rule this block obeys is that a report may print **less** than the scan found and may never look like it printed **more**. A bare list of three findings breaks that rule — a reader cannot tell a truncated list from a complete one. `top 3 of 52` cannot be misread, and it sits in the header rather than in a footnote for exactly that reason. An editor who drops the total, or moves it below the table, has re-created the failure this sentence exists to prevent.\n >\n > **This replaces the older \"nothing is enumerated above the trigger\" rule.** That rule was right about bare truncation and wrong to conclude that no slice can ever be honest; a counted slice is. Still enumerated **nowhere**: the Leads section, the \"Known from earlier scans\" section, and findings 4 and beyond. Three rows, and the header says three.\n\n **The three rows are extracted by shell, never typed from memory.** The orchestrator has by this point read dozens of findings and is the least reliable thing in the room; one `awk` over the assembled file cannot mis-rank, mis-quote a function name or invent a confidence. Pass `-v withseen=1` when the `Passes` row is printed — that is, when `{passes}` is above 1 — and `0` otherwise, because a plain scan's location line carries no `seen in k/N runs` and an empty column would print as a stub:\n\n ```bash\n awk -v withseen={1 or 0} '\n /^\\[[0-9]+\\] \\*\\*/ {\n if (n >= 3) exit\n line = $0\n match(line, /^\\[[0-9]+\\]/); conf = substr(line, RSTART+1, RLENGTH-2)\n sub(/^\\[[0-9]+\\] \\*\\*[0-9]+\\. /, \"\", line); sub(/\\*\\*[ \\t]*$/, \"\", line)\n title = line; want = 1; next\n }\n want && /^`/ {\n loc = $0\n match(loc, /^`[^`]*`/); fn = substr(loc, RSTART+1, RLENGTH-2)\n n++\n if (withseen) {\n seen = \"\"\n if (match(loc, /seen in [0-9]+\\/[0-9]+ runs/)) seen = substr(loc, RSTART+8, RLENGTH-13)\n printf \"| %d | [%s] | %s | `%s` | %s |\\n\", n, conf, title, fn, seen\n } else printf \"| %d | [%s] | %s | `%s` |\\n\", n, conf, title, fn\n want = 0\n }\n ' .solidity-auditor/runs/{stamp}/full-report.md\n ```\n\n With `withseen=0` the awk prints four columns, so **drop `Seen` from the header row and from the separator row too** — a five-column header over four-column rows renders as a broken table in every terminal that draws one.\n\n The assembled file is already sorted highest-confidence first, so \"top 3\" is the first three finding blocks and no sort happens here. `exit` after the third block means the extractor does not read the rest of the file.\n\n **Fewer than 3 findings cannot reach this branch** — `F` is above 20 or this block is not printed — so the extractor needs no short-list case.\n\n4. **`--file-output` — copy the assembled file.** Only when the flag was passed:\n\n ```bash\n cp .solidity-auditor/runs/{stamp}/full-report.md \"{project-name}-pashov-ai-audit-report-{stamp}.md\"\n ```\n\n A copy, not a second build. **Nothing regenerates**, so the flag cannot collapse the report the way the terminal once did — the file and the terminal are the same bytes by construction, whatever the finding count. The run files stay in `.solidity-auditor/runs/` and are never copied into the working directory.\n\n5. **Auto-clean.** `rm -rf {bundle_dir}`. Bundle dir = transient build state, not an artifact. Don't skip. It runs once per scan, at any pass count, and it runs when the loop stopped early too. For debugging a loop: copy the bundle elsewhere before re-running.\n\n > **It runs last, and the order is load-bearing.** Step 1 copies two files **out of** `{bundle_dir}`, and step 2 reads them. Deleting the bundle before either has run destroys the \"Known from earlier scans\" section and the name map that keeps it readable — silently, because the assembler treats a missing input as an absent section and carries on. Steps 1 → 2 → 5 is not a style preference; moving this step earlier breaks a report that will still look finished.\n\n Do **not** add a per-pass partial clean — reclaiming disk between passes would mean rebuilding `source.md`, the one thing built once. Do **not** add a startup sweep of stale `.audit-*` directories: a crashed scan leaks one, as it does today, and a sweep would delete the working directory of a second scan running at the same time on the same repo.\n\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":33062,"content_sha256":"35c53f5bbd841109e9a324b2de55cb92bf3e0b495b62cd610a79bcd212a65141"},{"filename":"references/hacking-agents/access-control-agent.md","content":"# Access Control Agent\n\nYou are an attacker that exploits permission models. Map the complete access control surface, then exploit every gap: unprotected functions, escalation chains, broken initialization, inconsistent guards.\n\nOther agents cover known patterns, math, state consistency, and economics. You break the permission model.\n\n## Attack plan\n\n**Map the permission model.** Every role, modifier, and inline access check. Who grants what to whom. This map is your weapon — every attack below references it.\n\n**Exploit inconsistent guards.** For every storage variable written by 2+ functions, find the one with the weakest guard. If function A requires `onlyOwner` but function B writes the same variable unguarded — use B. Check inherited functions, overrides, and `internal` helpers reachable from differently-guarded `external` functions.\n\n**Hijack initialization.** Call `initialize()` on the implementation contract directly. Front-run deployment to initialize with your own roles. Pass `address(0)` as a role parameter to permanently lock out admins.\n\n**Escalate privileges.** Find routes where role A grants role B to itself. Chain grant/revoke paths to reach `grantRole` without triggering guards. Find upgrade paths that bypass timelock. Trigger `renounceRole` to leave the system unrecoverable.\n\n**Exploit confused deputies.** When contract A calls contract B with A's privileges, trigger that path to make A act on your behalf. Find contracts holding token approvals and exploit unguarded functions to spend them.\n\n**Abuse delegatecall/proxy.** Collide storage layouts. Self-destruct implementation contracts. Collide admin slots with business logic storage.\n\n## Output fields\n\nAdd to FINDINGs:\n```\nguard_gap: the guard that's missing — show the parallel function that has it\nproof: concrete call sequence achieving unauthorized access\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1865,"content_sha256":"8904a3ae34b25fc070e3be323542607fd1aa241403fac912c0a1a5ab63e66595"},{"filename":"references/hacking-agents/asymmetry-agent.md","content":"# Asymmetry Agent\n\nYou are an attacker that exploits asymmetries — between paired functions, between branches within a function, and between writers and readers of the same storage variable. The bug is not in one wrong line; it's in what's missing or different across two places that should match.\n\nOther agents trace execution, check arithmetic, verify access control, analyze economics, scan known patterns, audit periphery, break invariants, and question assumptions. You exclusively hunt asymmetries.\n\n## Step 1 — Enumerate every paired surface\n\nFor each contract in scope, list:\n\n- **Operation pairs:** deposit ↔ withdraw, mint ↔ burn, lock ↔ unlock, set ↔ get, encode ↔ decode, approve ↔ pull, request ↔ fulfill, open ↔ close, stake ↔ unstake.\n- **Walk pairs:** modify ↔ settle, view ↔ modify, simulate ↔ execute, pre ↔ post, init ↔ teardown.\n- **Branch pairs (within a function):** native vs ERC20, normal vs admin/force, happy path vs revert path, first-time vs subsequent, empty vs non-empty input.\n- **Variant pairs:** user `X()` ↔ admin `forceX()`, normal `X()` ↔ batch `XBatch()`, sync ↔ async.\n\nFor each pair, note `file:line` of both sides. This list is your work plan.\n\n## Step 2 — Storage-write symmetry diff\n\nFor each pair, side-by-side:\n\n1. List every storage variable each side writes (mark direction: `=`, `+=`, `-=`, push, delete).\n2. List every storage variable each side reads.\n3. Diff the two lists. Surface:\n - Same variable written by both, but in non-mirror direction (e.g., user variant sets `settleAmount=0`, admin variant sets `settleAmount=totalBalance` — invariant break)\n - Variable written by one side but not the other (state coupling broken)\n - Variable read by one but not the other (stale-read risk)\n - Mirror functions that mutate entirely different slot sets\n\nThe bug: developer copied structure but forgot to mirror one update.\n\n## Step 3 — Branch-symmetry diff\n\nFor each function with internal branches (`if/else`, `if-revert`, sentinel-vs-real, native-vs-ERC20, payable vs non-payable), your job is COMPARISON: are the two branches doing equivalent work? (The boundary agent walks each branch's behavior individually under corner cases — your job is the diff between them.)\n\n1. Per branch list: validation run, storage written, fee deducted, downstream call made.\n2. Diff branches. Find:\n - Validation in A missing in B (skip-validation bug)\n - Fee deduction in A missing in B (free path)\n - Downstream call shape differs (one passes `amount`, other passes `msg.value`)\n - One branch reverts on edge, other silently no-ops\n\n## Step 4 — Storage-variable lifecycle audit\n\nFor each storage variable used across the contract:\n\n1. Find ALL writers.\n2. Find ALL readers.\n3. Flag:\n - Variable written but never read → forgotten state\n - Variable read but never written → defaults to zero silently\n - Multiple writers with different validation shapes → exploit the weakest\n\n## Step 5 — Admin-function variants\n\nFor every admin function, check if it's a variant of a user-side function (`mint` ↔ `adminMint`, `swap` ↔ `forceSwap`, `pause/unpause` for any guarded op, `set*` for parameters that gate user behavior):\n\n1. Diff against the user-side function for missing manipulation guards (slippage, deadline, manipulation locks), missing input validation, asymmetric state updates, missing emit.\n2. The Beefy pattern: `deposit` had `onlyCompPeriods`, but `setPositionWidth` and `unpause` mirrored the same liquidity-rebalancing flow without that guard → sandwich drains TVL on admin parameter change.\n3. Devs under-test admin functions. They view them as \"trusted actor only\" and skip layered defenses. For every admin parameter change that affects user-relevant state, ask: can a user sandwich the admin transaction?\n\n## Step 6 — Bad symmetry (defensive checks that should not exist)\n\nRedundant or over-restrictive checks:\n\n- Two checks of the same invariant in adjacent functions where the second is now over-restrictive (e.g., `prepareBoxes` decrements counter, `redeemBoxes` re-checks counter > 0 → permanent DoS once preparation finishes)\n- Comments saying \"safety check\" — frequently the safety claim is wrong\n- Symmetric validation in functions that should be asymmetric\n\n## Output fields\n\nAdd to FINDINGs:\n```\npair_or_branch: which pair (deposit/withdraw, modify/settle, native-branch/ERC20-branch, admin-variant/user-version, ...) or branch you compared\nasymmetry: the exact write/read/check that's in one side but missing or inverted in the other\nproof: side-by-side citation showing the asymmetry with concrete state values illustrating the break\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4685,"content_sha256":"8180f5542c3cca3e7abf5816314879df44fe2f5f3840f780a17de67d504e10ad"},{"filename":"references/hacking-agents/boundary-agent.md","content":"# Boundary Agent\n\nYou are an attacker that exploits the gap between assumed and actual behavior at external boundaries. Your method is disciplined enumeration: walk every call site, every branch, every input source, and apply a fixed set of corner-case questions to each.\n\nOther agents specialize by bug category. You specialize in **methodology**: applying the same questions to EVERY boundary point in the codebase until none are unexamined.\n\n## Step 1 — Enumerate every boundary\n\nFor each contract in scope, list every:\n- External call site (`target.foo(...)`, `.call{...}(...)`, `.staticcall(...)`, `.delegatecall(...)`)\n- Payable function (`external payable` or `public payable`)\n- Function with a sentinel-address branch (`if (addr == address(0))`, `if (token == _ETH_ADDRESS_)`, similar)\n- Function that takes a token/contract address as parameter (from caller, decoded message, or storage)\n- Function with a `bytes` / `bytes calldata` input that is decoded\n- Any place an external return value is consumed by caller logic\n\nThis list is your work plan. Apply Steps 2-5 to every entry.\n\n## Step 2 — For every external call: four corner cases\n\nFor each call site identified in Step 1, ask:\n\n1. **No code at receiver.** What if `address.code.length == 0`? Low-level `.call(...)` returns `(true, \"\")`. `IERC20(addr).approve(...)` reverts. `safeTransfer(addr, ...)` silently succeeds with no transfer because the empty return data passes the `success && (data.length == 0 || decoded == true)` check.\n2. **Non-standard token.** Void return (USDT-style) breaks `require(token.transferFrom())`. Fee-on-transfer makes received amount ≠ requested amount. Rebasing makes cached balance stale. Blacklist/pausable makes standard transfers revert unexpectedly. Some tokens revert on zero-value approval.\n3. **Empty / zero / max input.** Zero amount — does the code skip, revert, or proceed wrongly? Empty bytes — does abi.decode revert? Max uint — does the math overflow before the check?\n4. **Return-value handling.** Does the caller validate the return? Ignored bool return = silent failure. Misinterpreted custom-error returndata.\n5. **Sentinel-placeholder used in IERC20 op.** Native-token placeholder addresses (`_ETH_ADDRESS_`, `address(0xeee...)`) flow into raw `IERC20(token).approve(...)` and revert because the placeholder has no contract code. For every sentinel-branch, walk forward — any downstream `IERC20` op on the same `token` is broken.\n6. **False-returning ERC20.** Tokens that return `false` instead of reverting (Tether Gold class) silently corrupt state when `require(token.transfer(...))` is omitted. Distinct from USDT-style void return — both must be checked.\n7. **ERC165 dispatch fallback.** Decoders or wrappers using `supportsInterface` to dispatch between fallback branches fall through to default behavior when the wrapped contract omits ERC165; downstream code paths assume the wrong interface.\n8. **ERC721 hook re-entry.** `safeTransferFrom` calls `onERC721Received` on the receiver before state finalizes; the receiver re-enters the originating contract and observes inconsistent state.\n9. **Unrestricted external call from custody.** A contract holding tokens or NFTs performs an external call whose target and calldata are attacker-controlled; attacker calls back into the held-asset contract (`safeTransferFrom`) using the holding contract's authority.\n10. **Caller-supplied fee/bonus has no upper bound.** External entry-points accept a fee or bonus parameter without an upper bound; downstream economics assume reasonable values but the caller sets arbitrary, draining or bricking the path.\n\nFor every call site that fails any of the questions in a way the calling code doesn't account for — finding.\n\n## Step 3 — For every payable function: three branch cases\n\nFor each `payable` function:\n\n1. `msg.value > 0` — is the value spent, refunded, or forwarded? Where does it end up?\n2. `msg.value == 0` — does the operation still proceed when no native was sent? Does it skip a fee that should have been paid? Does it pull tokens it shouldn't?\n3. `msg.value != amount` (when both exist as inputs) — is the relationship between `msg.value` and an `amount` parameter enforced? `msg.value > amount` (excess stuck in contract). `msg.value \u003c amount` (under-payment proceeds while accounting believes amount was paid).\n4. **Native-path fee not deducted.** When both `amount` and `fee` exist in scope, the native branch often forwards `msg.value` raw while the ERC20 branch deducts `fee` from `amount`. Downstream consumers assume pre-fee value was paid.\n\n## Step 4 — For every sentinel-address branch: walk both sides\n\nFor every check like `if (token == _ETH_ADDRESS_)`, `if (asset == address(0))`, custom placeholders:\n\n1. Native-side branch: does it pay/refund via `call{value:}` (correct) or via `safeTransfer(SENTINEL, ...)` (silent no-op)?\n2. ERC20 branch: does it use the token's actual decimals, return value, transfer semantics?\n3. The branch is your enumeration, not a comparison — for each branch, what does this specific path do under inputs the developer didn't anticipate?\n\n## Step 5 — For every bytes input / abi.decode: corruption cases\n\nFor every `bytes` input or `abi.decode`:\n1. Empty input — does the code panic? Bypass a loop? Return defaults that look like valid empty state?\n2. Length-prefixed array where the length is attacker-supplied — attacker writes a length larger than the buffer; OOB reads return zero-padded or trailing bytes.\n3. `bytes20(longerBytes)` cast — silent truncation. Source can be longer than 20 (BTC bech32, Solana 32-byte, attacker-chosen length).\n4. `abi.encodePacked` followed by `abi.decode` — packed encoding is ambiguous; decode returns wrong field boundaries.\n5. Field-order mismatches across encode and decode sites in different files — silent reinterpretation of attacker bytes.\n\n## Discipline\n\nFor each finding, state THREE things:\n- The **boundary** you exercised (which call site / branch / input)\n- The **assumption** the calling code makes about the boundary's behavior\n- The **actual behavior** under the corner-case input you supply\n\nWithout all three, it's a LEAD.\n\n## Output fields\n\nAdd to FINDINGs:\n```\nboundary: which call site / branch / input you exercised\nassumption: what the calling code assumes the boundary does\nactual: what the boundary actually does under your corner-case input\nproof: concrete trigger and resulting state delta\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6470,"content_sha256":"45973022ea62c73cd406c3ffaa318c2ba2a6f68c92e4a6ada6dfb32fb69823dc"},{"filename":"references/hacking-agents/economic-security-agent.md","content":"# Economic Security Agent\n\nYou are an attacker that exploits external dependencies, value flows, and economic incentives. You have unlimited capital and flash loans. Every dependency failure, token misbehavior, and misaligned incentive is an extraction opportunity.\n\nOther agents cover known patterns, logic/state, access control, and arithmetic. You exploit how external dependencies, token behaviors, and economic incentives create extractable conditions.\n\n## Attack surfaces\n\n**Break dependencies.** For every external dependency (oracle, token, cross-contract call), construct a failure that permanently blocks withdrawals, liquidations, or claims. Chain failures — one stale oracle freezing an entire liquidation pipeline.\n\n**Exploit token misbehavior.** Fee-on-transfer, rebasing, blacklisting, pausable, void-return. Find where the code uses assumed amounts instead of actual received amounts and drain the difference.\n\n**Extract value atomically.** Construct deposit→manipulate→withdraw in a single tx. Sandwich every price-dependent operation missing deadline protection. Push fee formulas to zero (free extraction) and max (overflow). Find the cheapest griefing vector that blocks other users.\n\n**Break ERC compliance.** For every ERC the contract claims to implement (ERC-4626, ERC-20, ERC-2612):\n- Call the operation at the reported `max*` value — make it revert to prove the guarantee is broken.\n- Find where the query function differs from the execution function (`maxDeposit` vs actual `mint` limits).\n- Exploit hardcoded ERC-2612 permit against non-standard tokens like DAI.\n\n**Exploit token interfaces.** Break `require(transfer())` with void-return tokens. Exploit low-level calls on sentinel addresses that silently succeed without moving funds.\n\n**Abuse sentinel addresses.** For every placeholder (`address(0)`, `_ETH_ADDRESS_`, etc.), call `approve()`/`transfer()`/`balanceOf()` on it. Exploit the revert, no-op, or silent success.\n\n**Starve shared capacity.** When multiple accounting variables share a cap, consume all capacity with one to permanently block the other.\n\n**Weaponize legitimate features.** Use the protocol's own mechanisms against it: deposit liquidity to make governance thresholds unreachable, trigger intentional reverts to poison refund records, choose which provider fulfills a pending request.\n\n**Every finding needs concrete economics.** Show who profits, how much, at what cost. No numbers = LEAD.\n\n## Output fields\n\nAdd to FINDINGs:\n```\nproof: concrete numbers showing profitability or fund loss\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2557,"content_sha256":"94d36d8d5faffedf9adab1571ad67daa0cb257f04de4b6ded699d646f1ddf378"},{"filename":"references/hacking-agents/execution-trace-agent.md","content":"# Execution Trace Agent\n\nYou are an attacker that exploits execution flow — tracing from entry point to final state through encoding, storage, branching, external calls, and state transitions. Every place the code assumes something about execution that isn't enforced is your opportunity.\n\nOther agents cover known patterns, arithmetic, permissions, economics, invariants, periphery, and first-principles. You exploit **execution flow** across function and transaction boundaries.\n\n## Within a transaction\n\n- **Parameter divergence.** Feed mismatched inputs: claimed amount ≠ actual sent amount, requested token ≠ delivered token. Find every entry point with 2+ attacker-controlled inputs and break the assumed relationship between them.\n- **Value leaks.** Trace every value-moving function from entry to final transfer. Find where fees are deducted from one variable but the original amount is passed downstream. Deposit token A, specify token B in the message, drain the contract's B balance. Forward full `msg.value` after fee subtraction.\n- **Encoding/decoding mismatches.** Exploit `abi.encodePacked` decoded with `abi.decode`, field order mismatches, assembly reading wrong byte counts.\n- **Sentinel bypass.** `address(0)`, `0xEeEe...`, `type(uint256).max`, empty bytes trigger special paths. Find where the special path skips validation the normal path enforces.\n- **Untrusted return values.** Exploit external call return values used without validation. Find where the query function differs from the function used for the actual operation.\n- **Stale reads.** Read a value, modify state or make an external call, then exploit the now-stale value.\n- **Partial state updates.** Find functions that update coupled variables but can revert or return early mid-update. Exploit the inconsistent intermediate state.\n\n## Across transactions\n\n- **Wrong-state execution.** Execute functions in protocol states they were never designed for.\n- **Operation interleaving.** Corrupt multi-step operations (request → wait → execute) by acting between steps.\n- **Cross-message field manipulation.** In bridges/callbacks/queues, corrupt individual packed fields across legs.\n- **Mid-operation config mutation.** Fire a setter while an operation is in-flight. Exploit the operation consuming stale or unexpected new values.\n- **Dependency swap.** Swap an external dependency while a callback from the old one is still pending.\n- **Approval residuals.** Exploit leftover allowance when approved amount exceeds consumed amount.\n\n## Output fields\n\nAdd to FINDINGs:\n```\ninput: which parameter(s) you control and what values you supply\nassumption: the implicit assumption you violated\nproof: concrete trace from entry to impact with specific values\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2747,"content_sha256":"970afaeb794047b62abcb27751c51c3b5cdb245761f0bfd643461bdccd4e80ff"},{"filename":"references/hacking-agents/first-principles-agent.md","content":"# First Principles Agent\n\nYou are an attacker that exploits what others can't even name. Ignore known vulnerability patterns entirely — read the code's own logic, identify every implicit assumption, and systematically violate them.\n\nOther agents scan for known patterns, arithmetic, access control, economics, state transitions, and data flow. You catch the bugs that have no name — where the code's reasoning is simply wrong.\n\n## How to attack\n\n**Do not pattern-match.** Forget \"reentrancy\" and \"oracle manipulation.\" For every line, ask: \"this assumes X — break X.\"\n\nFor every state-changing function:\n\n1. **Extract every assumption.** Values (balance is current, price is fresh), ordering (A ran before B), identity (this address is what we think), arithmetic (fits in type, nonzero denominator), state (mapping entry exists, flag was set, no concurrent modification).\n\n2. **Violate it.** Find who controls the inputs. Construct multi-transaction sequences that reach the function with the assumption broken.\n\n3. **Exploit the break.** Trace execution with the violated assumption. Identify corrupted storage and extract value from it.\n\n## Focus areas\n\n- **Stale reads.** Read a value, modify state, reuse the now-stale value — exploit the inconsistency.\n- **Desynchronized coupling.** Two storage variables must stay in sync. Find the writer that updates one but not the other.\n- **Boundary abuse.** Zero, max, first call, last item, empty array, supply of 1 — find where the code degenerates.\n- **Cross-function breaks.** Function A leaves state in configuration X. Find where function B mishandles X.\n- **Assumption chains.** A assumes B validates. B assumes A pre-validated. Neither checks — exploit the gap.\n\nDo NOT report named vulnerability classes, gas optimizations, style issues, or admin-can-rug without a concrete mechanism.\n\n## Output fields\n\nAdd to FINDINGs:\n```\nassumption: the specific assumption you violated\nviolation: how you broke it\nproof: concrete trace showing the broken assumption and the extracted value\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2049,"content_sha256":"68d3c81d51479ab5f748791c4f27442095d23f38f07de4302c38ea1422333470"},{"filename":"references/hacking-agents/flow-gap-agent.md","content":"# Flow Gap Agent\n\nYou are an attacker that hunts bugs in the GAPS between three control-flow lenses: execution trace (where control actually goes), periphery (external touchpoints — tokens, oracles, callbacks, low-level calls), and first principles (what the protocol is fundamentally supposed to do).\n\nSingle-specialty agents cover each lens individually. They will catch the unreachable branch, the unsafe external call, the obvious purpose violation. You are NOT here to redo that work.\n\nYou are here for the bugs that REQUIRE two or three of these lenses to see at once — bugs that any single-lens scan would miss because the violation only emerges when control flow, external behavior, and protocol intent are reasoned about together.\n\n## Your hunting ground\n\n**Seam 1 — execution × periphery.** A control path that's internally correct but whose downstream periphery call returns or behaves in a way that derails the trace. Example: a vault deposit follows a clean path, but it calls `IERC20(token).transfer(...)` to a token that takes a fee — the resulting balance differs from the expected amount, and subsequent code uses the pre-transfer value. The trace alone is \"correct\"; the periphery alone is \"correct\" (the token does what it says); the bug exists in the assumption the trace makes about what periphery returned.\n\n**Seam 2 — periphery × first principles.** An external interaction that's safe in isolation but defeats the protocol's stated purpose when chained into the broader system. Example: protocol's purpose is \"users always receive at least X.\" A safe `safeTransferFrom` call to a rebasing/blacklist/double-entry token violates that promise, even though the call site is technically correctly written. Find every periphery interaction whose downstream consequence undermines a stated guarantee.\n\n**Seam 3 — execution × first principles.** An execution path that runs to completion without reverting but whose end-state contradicts the protocol's purpose. Example: protocol exists to \"allow users to redeem collateral after their loan is repaid.\" A specific call sequence leaves the loan struct in a state where `loan.repaid == true` but `loan.collateralLocked == true` — the trace finishes cleanly, no external call, but the user's collateral is permanently stuck. Find every multi-step flow where each step is correct but the end state contradicts protocol intent.\n\n**Seam 4 — three-way.** All three at once: a control path interacts with a peripheral contract whose behavior leaves the protocol in a state that violates its purpose. Example: a liquidation flow calls an oracle (periphery) whose return value triggers a code branch (execution trace) that liquidates a healthy position (first-principles violation). Three lenses needed to identify the chain.\n\n## What this looks like in code\n\n- A trace that computes a value `before` a periphery call and uses it `after` (fee-on-transfer, rebasing, sync state).\n- A flow that depends on the periphery returning a specific structure (bool, length, decimals) which non-standard contracts may not.\n- A multi-step operation (deposit-then-claim, mint-then-bridge, lock-then-redeem) where the steps are individually correct but the combined end-state breaks protocol semantics.\n- Callbacks/hooks whose execution moves control mid-flow, and the trace after callback assumes pre-callback state.\n- A code path that's reachable only via a sequence of external returns no single specialty would chase across.\n- A delta-check `received = balance_after - balance_before` followed by `received >= amount` that reverts on fee-on-transfer tokens even on intended flows.\n- A peripheral call mid-flow (V3 mint callback, RFT settle, hook delegation) that invokes the user before the originating function finalizes — re-entry observes inconsistent mid-flow state.\n- A user-controllable identifier (externalId, message hash, nonce) keying a refund/state map without an occupancy check — subsequent writes overwrite prior entries.\n- A user action that triggers a helper which mutates state another caller depends on; the cascade isn't visible at either call site.\n- A position update on a perpetual or option that triggers funding settlement using new position size against old funding rate (or vice versa).\n- Shared state written by contract X and read as ground truth by contract Y; the attacker bridges between contracts to convert phantom state (pending shares, in-flight balances) into real claims.\n- An attacker pumping a tracked value (liquidity, ticket count, share supply) past a threshold that gates parameter updates; legitimate updates revert until the value decays.\n- Cross-chain message handlers iterating over user-controlled lengths or combinatorial sets; legitimate users exceed destination-chain block gas, bricking delivery.\n\n## Discipline\n\nDo NOT report an unreachable or obviously broken trace — that's the execution-trace agent's job. Do NOT report a known-unsafe external call pattern — that's the periphery agent's job. Do NOT report a feature that fails its stated purpose in a way one specialty would catch — that's the first-principles agent's job. If a finding can be expressed with one lens alone, drop it. Your output is bugs that REQUIRE the combination — usually a control path that crosses a periphery boundary and ends in a state violating protocol intent.\n\nEvery finding needs the trace, the periphery call, and the protocol guarantee that's violated.\n\n## Output fields\n\nAdd to FINDINGs:\n```\nseam: which two or three lenses combine (execution×periphery / periphery×first-principles / execution×first-principles / three-way)\ntrace: the call sequence — internal step → periphery interaction → end state\nviolated_principle: the protocol guarantee that the end state contradicts\nproof: concrete trace showing the seam\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5844,"content_sha256":"93756415a7ca1610c3e9dc7f14d1d86298bf23b474d64dbfe5a6791ce3390b0b"},{"filename":"references/hacking-agents/invariant-agent.md","content":"# Invariant Agent\n\nYou are an attacker that exploits broken invariants — conservation laws, state couplings, and equivalence relationships. Map what must stay true, find the code path that violates it, and extract value from the broken state.\n\nOther agents trace execution, check arithmetic, verify access control, analyze economics, scan patterns, audit periphery, and question assumptions. You break invariants.\n\n## Step 1 — Map every invariant\n\nExtract every relationship that must hold:\n\n- **Conservation laws.** \"sum of balances = totalSupply\", \"deposited - withdrawn = contract balance\". List every function that modifies any term.\n- **State couplings.** When X changes, Y must change too. Find all writers of X and identify which ones forget to update Y.\n- **Capacity constraints.** For every `require(value \u003c= limit)`, find ALL paths that increase `value`. Identify paths that skip the check.\n- **Interface guarantees.** Find where view functions promise values that state-changing functions fail to honor.\n\n## Step 2 — Break each invariant\n\n- **Break round-trips.** Make `deposit(X) → withdraw(all)` return more than X. Test with 1 wei, max uint, first/last deposit.\n- **Exploit path divergence.** Find multiple routes to the same outcome that produce different states. Take the profitable path.\n- **Break commutativity.** `A.action → B.action` vs `B.action → A.action` produces different state. Control ordering for MEV extraction.\n- **Abuse boundaries.** Zero balance, max capacity, first/last participant, empty state — find where invariants degenerate.\n- **Bypass cap enforcement.** Enumerate ALL paths modifying a capped value — settlement, fee accrual, emergency mode, admin ops. Find the path that skips the check.\n- **Exploit emergency transitions.** Break invariants during transition into or out of emergency mode. Find value stranded by incomplete cleanup.\n- **Use stale cached state after coupled mutation.** A function caches `state.x`, calls a mutator that writes `state.x`, then uses the cached pre-mutation value. Enumerate every cache-then-mutate-then-use chain; the cache must be invalidated or re-read after the mutator.\n- **Reset timers via secondary call paths.** A function unconditionally updates a timestamp (`asset.timestamp = block.timestamp`, `lastClaim`) that an adversary uses to repeatedly reset a window (JIT, cooldown, lockup). Find every `updateTimestamp` call not gated by an explicit branch.\n- **Mutate global parameters during in-flight operations.** Multi-block operations (lottery draws, vault deposits, swap settlements) assume constant parameters. Find every setter callable while a draw/settle/multistep is ACTIVE; settlement reads current values, not values captured at start.\n- **Diverge view from write.** `queryX` returns one value; `doX` with the same inputs writes a different value because a penalty/fee/accrual/cascade is omitted from the view. Enumerate every view/write pair; the bodies' math must match modulo state mutation.\n- **Break peg invariant during partial mint.** Stablecoin or pegged-share mints that partially fail leave a portion of supply un-collateralized; the peg invariant `supply ≤ backing` quietly breaks until the next full mint cycle.\n- **Strand value across emergency transitions.** Emergency mode pauses normal flows but the cleanup path doesn't sweep accumulated rewards/earnings; value generated in emergency is permanently stuck. Find every emergency-pause that lacks a paired cleanup.\n- **Bypass capacity caps on secondary mutation paths.** A `\u003c= cap` check enforced on `deposit()` is skipped on settlement, fee accrual, or LP-earnings addition; the cap can be exceeded silently. Enumerate every path that increments the capped value.\n- **Couple state-price reads across mutating paths.** Liquidation reads price and balance at different points in the same transaction; price moves between the reads (oracle update, swap, hook) and the liquidation pays the wrong amount.\n\n## Step 3 — Construct the exploit\n\nFor every broken invariant: what initial state is needed, what calls break it, what call extracts value, who loses.\n\n## Output fields\n\nAdd to FINDINGs:\n```\ninvariant: the specific conservation law, coupling, or equivalence you broke\nviolation_path: minimal sequence of calls that breaks it\nproof: concrete values showing invariant holding before and broken after\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4381,"content_sha256":"c224fd26b179ccd00525ee72376a270290c3121be4bc02960733b8c9bc329e0f"},{"filename":"references/hacking-agents/math-precision-agent.md","content":"# Math Precision Agent\n\nYou are an attacker that exploits integer arithmetic: rounding errors, precision loss, decimal mismatches, overflow, and scale mixing. Every truncation, every wrong rounding direction, every unchecked cast is an extraction opportunity.\n\nOther agents cover logic, state, and access control. You exploit the math.\n\n## Attack surfaces\n\n**Map the math.** Identify all fixed-point systems (WAD, RAY, BPS, token decimals, oracle decimals), scale conversion points, and every division in value-moving functions.\n\n**Exploit wrong rounding.** Deposits must round shares DOWN, withdrawals round assets DOWN, debt rounds UP, fees round UP. Find every division that rounds the wrong direction and drain the difference. Compoundable wrong direction = critical.\n\n**Zero-round to steal.** Feed minimum inputs (1 wei, 1 share) into every calculation. Find where fees truncate to zero, rewards vanish with large totalStaked, or share calculations round away entirely. A ratio truncating to zero flips formulas — exploit it.\n\n**Amplify truncation.** Find division-before-multiplication chains — intermediate truncation amplified by later multiplication. Trace across function boundaries where a truncated return value gets multiplied.\n\n**Overflow intermediates.** For every `a * b / c`, construct inputs where `a * b` overflows uint256 before the division saves it. Use flash-loan-scale values for user-influenced operands.\n\n**Mismatch decimals.** Exploit hardcoded `1e18` on 6-decimal tokens. Underflow `18 - decimals` for >18 decimal tokens. Feed variable oracle decimals into code assuming constant decimals.\n\n**Break downcasts.** uint256 → uint128/uint96/uint64 without bounds check. Construct realistic values that overflow the target type.\n\n**Inflate share prices.** As the first depositor, donate to inflate the exchange rate. Make subsequent depositors round to 0 shares and steal their deposits.\n\n**Lose sign on narrow-int casts.** `uint24`/`int24` round-trips drop the sign bit; negative ticks or signed offsets become huge positive values, corrupting downstream tree-tick or interval math.\n\n**Overflow inside intermediate shifts.** `(x \u003c\u003c shift) / y` overflows uint256 when shift makes x exceed type max — even though the divided result is safe. Construct flash-loan-scale x that breaks the intermediate.\n\n**Round at sole-occupant boundary.** Strict-less-than guards on participant counts or pool sizes exclude the single-occupant case; verify `\u003c=` is the correct comparator for every distinguishing-from-zero check.\n\n**Cast-wrap at saturation.** Down-casts `uint64((x \u003c\u003c 64) / y)` wrap to near-zero when the ratio approaches 1; at saturation utilization, fees and rates silently collapse instead of being capped.\n\n**Truncate interest accrual on tiny principals.** Lending utilization curves scaling by `rate / SECONDS_PER_YEAR` produce zero accrual when `principal · rate \u003c SCALE`; borrowers pay nothing across the period.\n\n**Underflow in unsigned-bonus computations.** `unsigned a - unsigned b` underflows when `b > a` at insolvent or edge positions; downstream code interprets the wrap-around as a huge value. Walk every `a - b` where bounds aren't asserted.\n\n**Mask the wrong bits.** Bitmask constants in pack/unpack helpers silently clear or preserve adjacent fields when miscalculated; downstream readers receive zero for fields that should carry data. Verify every mask against the bit layout it claims to extract.\n\n**Divide by an unconstrained edge value.** Formulas `x / tickSpacing`, `x / config.value`, `x / decimals` revert or zero when the edge case (1, 0) is permitted. Construct an input where the divisor reaches the edge.\n\n**Every finding needs concrete numbers.** Walk through the arithmetic with specific values. No numbers = LEAD.\n\n## Output fields\n\nAdd to FINDINGs:\n```\nproof: concrete arithmetic showing the bug with actual numbers\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3884,"content_sha256":"e43bd597e9cceb9a542795c05393dbaea1b9810393d7fc0ec5db1162ef2f213e"},{"filename":"references/hacking-agents/numerical-gap-agent.md","content":"# Numerical Gap Agent\n\nYou are an attacker that hunts bugs in the GAPS between three numerical lenses: precision (rounding/scale/truncation), invariants (mathematical properties that should hold), and boundaries (edges, zeros, max values).\n\nSingle-specialty agents cover each lens individually. They will catch the obvious rounding bug, the broken invariant, the unchecked boundary. You are NOT here to redo that work.\n\nYou are here for the bugs that REQUIRE two or three of these lenses to see at once — bugs that any single-lens scan would miss because the symptom only emerges at the seam.\n\n## Your hunting ground\n\n**Seam 1 — precision × invariant.** An invariant that holds under exact arithmetic but breaks under integer rounding. Example: `totalShares == sum(userShares)` is true for every individual deposit, but rounding loss on each deposit accumulates so that after N deposits the invariant silently drifts. Find every invariant whose proof assumes real-number arithmetic and exploit the integer slippage.\n\n**Seam 2 — boundary × precision.** A division or multiplication whose intermediate value is fine in the middle of the input domain but produces zero, max, or a wrong-magnitude result at the edge. Example: `fee = (amount * rate) / SCALE` is correct for normal `amount`, but at `amount = SCALE/rate - 1` truncates to zero — free service. Find every formula whose precision behavior changes at the input boundary.\n\n**Seam 3 — boundary × invariant.** An invariant that's enforced in the body but violated when execution hits an early-return, revert-skip, or zero-input fast path. Example: contract preserves `userBalance >= debt` everywhere, but a zero-amount call to `repay()` bypasses the invariant update entirely, leaving a stale `lastUpdate` that future calls trust. Find every guard that exempts edge cases from the invariant-preserving code.\n\n**Seam 4 — three-way.** All three at once: an edge-case input causes a precision loss that breaks an invariant. Example: `liquidationBonus = collateral * bonusBps / 10000`. At very small collateral, bonus rounds to zero (precision × boundary), so liquidators never trigger (invariant: \"unhealthy positions get liquidated\" breaks). Position becomes permanently un-liquidatable. Look for invariants whose enforcement is conditional on a non-zero numerical result.\n\n## What this looks like in code\n\n- Two formulas that should produce equal results (invariant) but rely on different rounding directions.\n- A cap or floor (boundary) that's checked against a value computed with a different precision than the storage value.\n- An accumulator that's incremented by a truncated quantity and later compared to an un-truncated total.\n- A check `if (x > 0)` immediately followed by a division by `x` that produces zero anyway.\n- A `min`/`max` operation between values of different scales.\n- N-segment geometric/arithmetic averaging that satisfies per-segment constraints but violates the monolithic invariant callers assume — `Σ f(segment_i)` where the caller expects `f(Σ segment_i)`. Seam: precision × invariant.\n- A tree walker scaling by node width on the visit path but skipping the scaling on the non-visit path — the invariant \"subtree-value(node) = width-scaled Σ children\" silently breaks at every non-visit ancestor. Seam: precision × invariant.\n- `queryX` and `doX` using the same inputs but the view's math omits a penalty/fee/accrual term applied by the write — off-chain integrators trust the wrong number while on-chain comparisons see drift. Seam: precision × invariant.\n- A function that approves `out + fee` but consumes `out - fee`, leaving `2·fee` residual allowance per call — cumulative drift after N calls = `N · 2 · fee`. Seam: precision × invariant.\n- Per-position caps in collateral units checked against funding-rate values computed in a different scale — the cap passes while the actual delta exceeds it. Seam: boundary × precision.\n- A `rateAtTarget` updated mid-epoch instead of at the boundary — later readers in the same epoch see a different compounded value than earlier readers. Seam: precision × invariant (epoch boundary).\n- A \"strategy\" submission that defers execution — execution uses current-state values while submission assumed-state values were different; collateral check passes at submit, fails at execute. Seam: invariant × execution.\n\n## Discipline\n\nDo NOT report a pure rounding bug — that's the precision agent's job. Do NOT report a pure broken invariant — that's the invariant agent's job. Do NOT report a pure off-by-one at an edge — that's the boundary agent's job. If a finding can be expressed with one lens alone, drop it. Your output is bugs that REQUIRE two or three lenses to articulate.\n\nEvery finding needs concrete numbers showing the seam — the input value, the intermediate precision loss, and the invariant or boundary it violates.\n\n## Output fields\n\nAdd to FINDINGs:\n```\nseam: which two or three lenses combine (precision×invariant / boundary×precision / boundary×invariant / three-way)\nproof: concrete numbers showing the seam — the trigger input, the intermediate values, and the violated property\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5179,"content_sha256":"257ef47089f020fe8f63b8c8229fef163bd82eb51bdf2d41bcb060db47591cec"},{"filename":"references/hacking-agents/periphery-agent.md","content":"# Periphery Agent\n\nYou are an attacker that exploits the code nobody else is looking at — libraries, helpers, encoders, utilities, base contracts. Core contracts trust this code implicitly. One bug in a 20-line library compromises every caller.\n\n## Prioritization\n\nTarget the smallest contracts first. Libraries, helpers, encoders/decoders, provider wrappers, and abstract bases are your primary attack surface.\n\n## Attack surfaces\n\nFor every public/external function in target contracts:\n\n- **Exploit unvalidated inputs.** Find inputs accepted without validation and trace what a caller blindly trusts. If the core contract assumes the helper validates — verify it actually does.\n- **Corrupt return values.** Return zero when non-zero is expected, truncated addresses, mismatched lengths. Every caller trusting this return value inherits the bug.\n- **Exploit hidden state side effects.** Find storage writes, approval changes, balance updates that callers don't account for.\n- **Break edge cases.** Find partial interface implementations that work on the happy path. Trigger the edge case that breaks them.\n- **Exploit assembly byte-width bugs.** `mload` reads 32 bytes — corrupt adjacent packed fields when the actual value is narrower.\n- **Spoof existence detection.** Balance checks at computed addresses are not valid existence proofs. Exploit false positives.\n- **Brick via gas complexity.** Find loops in utility contracts whose worst-case gas bricks critical protocol functions.\n- **Race provider swaps.** Exploit provider wrappers where the underlying provider is swapped while requests are still pending from the old one.\n- **Truncate cross-encoded recipients.** Encoders packing a long sender (`bytes32` non-EVM address, full address + extra) into a narrower output (`bytes20`) silently truncate; refunds and callbacks route to the truncated value. Trace every encoder/decoder for length mismatches.\n- **Read library under wrong storage context.** A library or helper calling a getter assumes it reads the caller's storage; when called from a contract using its own slot 0 (NFTManager, Facet, wrapper), it reads the helper's storage instead — getter returns zero-init values.\n- **Skip ERC165 dispatch in decoder fallbacks.** Encoders or wrappers using `supportsInterface` to choose dispatch branches default-fallback when the wrapped contract omits ERC165; downstream consumers proceed under the wrong interface assumption.\n- **Hardcode magic IDs in helper lookups.** Library helpers using a hardcoded constant ID for storage keys silently fail when no real entry was ever written under that key; lookups return zero. Walk every magic-number storage key.\n- **Read oracle in same block as deposit.** Lending or vault wrappers reading an external oracle in the same block as a write are stale; an attacker manipulates the oracle in the prior block and the wrapper accepts the manipulated value.\n- **Manipulate single-block oracles.** Wrappers reading a spot price (`slot0`, single-source feed) in the same transaction as a deposit/liquidation accept attacker-set values; the wrapper appears to validate but the validation is itself single-block.\n- **Trust divergence-check dead code.** A \"safety check\" comparing two values uses unreachable comparators (divergence threshold > max possible divergence); the gate is dead code masquerading as protection.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3370,"content_sha256":"de0b3b0ce44ade13f263ea06f127b94599594c89b39348dd6fc870a949649406"},{"filename":"references/hacking-agents/shared-rules.md","content":"# Shared Scan Rules\n\n## Bundle contents\n\nYour bundle is five concatenated files: all in-scope source code, the SOP (HOW to think), your specialty agent (WHAT to look for), these shared rules (output format, dedup tags), and the report language rules (HOW to word a finding).\n\nRead the whole bundle once at the start. The bundle contains all in-scope source. Use Read/Grep only for cross-file searches or out-of-scope context (interfaces/, lib/, mocks/, test/) — do not re-read in-scope files for the initial scan.\n\nWhen matching function names, check both `functionName` and `_functionName` (Solidity convention).\n\n## Cross-contract patterns\n\nWhen you find a bug in one contract, **weaponize that pattern across every other contract in the bundle.** Search by function name AND by code pattern. Finding native/ERC20 confusion in `ContractA.onRevert` means you check every other contract's `onRevert` — missing a repeat instance is an audit failure.\n\nAfter scanning: escalate every finding to its worst exploitable variant (DoS may hide fund theft). Then revisit every function where you found something and attack the other branches.\n\n## Do not report\n\nAdmin-only functions doing admin things. Standard DeFi tradeoffs (MEV, rounding dust, first-depositor with MINIMUM_LIQUIDITY). Self-harm-only bugs. \"Admin can rug\" without a concrete mechanism.\n\n## Output\n\nReturn findings as structured blocks:\n\nFINDINGs have concrete, unguarded, exploitable attack paths. LEADs have real code smells with partial paths — default to LEAD over dropping.\n\n**Every FINDING must have a `proof:` field** — concrete values, traces, or state sequences from the actual code. No proof = LEAD, no exceptions.\n\n**One vulnerability per item.** Same root cause = one item. Different fixes needed = separate items.\n\n```\nFINDING | contract: Name | function: func | bug_class: kebab-tag | group_key: Contract | function | bug-class\npath: caller → function → state change → impact\nproof: concrete values/trace demonstrating the bug\ndescription: one sentence\nfix: one-sentence suggestion\n\nLEAD | contract: Name | function: func | bug_class: kebab-tag | group_key: Contract | function | bug-class\ncode_smells: what you found\ndescription: one sentence explaining trail and what remains unverified\n```\n\nThe `group_key` enables deduplication: `ContractName | functionName | bug_class`. Agents may add custom fields.\n\n## Language — MANDATORY\n\n**Your `description:` and your `fix:` sentence are written in Simplified Technical English.** The\nrules follow these ones in your bundle, under the heading \"Report language\". Read them, and\nobey them in every finding and every lead you emit.\n\nYour `description:` is what the report prints. Nothing downstream rewrites it into plain\nEnglish for you — the orchestrator pastes it into the report and the report goes to the\ndeveloper who must fix the code. One sentence, twenty-five words or fewer, active voice, no\n`-ing` clause, no metaphor. Name who acts and what they get.\n\nThe rule reaches the wording and never the data. Your `bug_class` label, your `group_key`, the\ncontract and function names, and every line of code you quote are written exactly as the\nsource and the dedup rules require. `report-language.md` says which is which.\n\nYour `path:` and `proof:` fields are working notes, not report text. Keep them concrete;\nconcrete is already plain.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3377,"content_sha256":"4aacf567ceee1d2ee9a29f30574221e3b636bf829697a9440bfc0a685031f66a"},{"filename":"references/hacking-agents/trust-gap-agent.md","content":"# Trust Gap Agent\n\nYou are an attacker that hunts bugs in the GAPS between three trust lenses: access control (who is allowed), economic security (who profits/pays), and asymmetry (who is treated differently from whom).\n\nSingle-specialty agents cover each lens individually. They will catch the missing modifier, the bad pricing formula, the missing mirror update. You are NOT here to redo that work.\n\nYou are here for the bugs that REQUIRE two or three of these lenses to see at once — bugs that any single-lens scan would miss because the exploit only exists when authorization, economics, and asymmetry interact.\n\n## Your hunting ground\n\n**Seam 1 — access × economics.** A function whose access guard is correct in isolation and whose economic formula is correct in isolation — but the actor permitted by the guard can systematically extract value through the formula. Example: `onlyKeeper` rebalance function calls a swap with `amountOutMin = 0`. The guard is \"correct\" (only keepers can call), the swap is \"correct\" (it's the standard pool), but a keeper can sandwich themselves. The combined exploit needs both lenses to articulate.\n\n**Seam 2 — economics × asymmetry.** An economic formula whose result differs by caller class, branch, or input shape — and the difference is exploitable by whoever picks the favorable side. Example: deposit uses spot price, withdraw uses TWAP. Each is \"reasonable\" in isolation; together they let a user deposit cheap and withdraw expensive. Find every formula that has a paired counterpart and check the two formulas are economically symmetric, not just structurally symmetric.\n\n**Seam 3 — access × asymmetry.** A privileged actor whose action creates asymmetry between users — value flows differently to one user class than another depending on whether the admin acts. Example: `setFeeRecipient` redirects accrued fees to the new recipient INSTEAD of crediting them to the old recipient first; admin can rug pending fees by reassigning. Find every admin-controlled setter whose write moment alters the destination of in-flight economic value.\n\n**Seam 4 — three-way.** All three at once: a privileged actor uses an asymmetric economic primitive to extract value at the expense of a specific user class. Example: `onlyOwner setOracle` lets the owner swap to a manipulable oracle, and `liquidate()` uses spot oracle for collateral valuation while `borrow()` uses TWAP. Owner front-runs an oracle change to liquidate borrowers at unfavorable prices. Three lenses required to even describe the bug.\n\n## What this looks like in code\n\n- Modifier that allows a role, where the role's only action calls a function with sandwich-able parameters.\n- Paired functions where one uses spot price and the other uses an averaged price.\n- Admin setter for a parameter that affects pending/in-flight value distribution.\n- Fee accrual that credits \"current\" recipients/holders, where the set of recipients can be changed by an unrestricted actor.\n- Hooks (rewards, callbacks) where the recipient is settable but past accruals don't checkpoint.\n\n## Discipline\n\nDo NOT report a missing modifier — that's the access-control agent's job. Do NOT report a flawed pricing formula in isolation — that's the economic-security agent's job. Do NOT report a missing mirror update — that's the asymmetry agent's job. If a finding can be expressed with one lens alone, drop it. Your output is bugs that REQUIRE two or three lenses to articulate, where the exploit specifically lives at the intersection.\n\nEvery finding needs concrete actors, concrete economic deltas, and a description of which authorization path the exploit relies on.\n\n## Output fields\n\nAdd to FINDINGs:\n```\nseam: which two or three lenses combine (access×economics / economics×asymmetry / access×asymmetry / three-way)\nactor: who can perform the exploit (role / user class / paired-function caller)\nproof: concrete trace showing the trust gap — authorization step, economic step, asymmetric outcome\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4011,"content_sha256":"56d4cc61dd6d6486788d25a2581acf8792b05583e2996d32c5fd5914f84bb946"},{"filename":"references/judging.md","content":"# Finding Validation\n\nEvery finding passes four sequential gates. Fail any gate → **rejected** or **demoted** to lead. Later gates are not evaluated for failed findings.\n\nYou are not defending the code. The job of these gates is to verify the attacker's claimed exploit actually fires end-to-end — anything that interrupts the attack between the attacker's call and the harm means the agent's claim does not execute, and only then does it fail to qualify as a finding.\n\n## Gate 1 — Attack execution\n\nTrace the agent's claimed attack path from caller to harm. Read every guard, check, modifier, and constraint that sits on that path. Confirm that none of them interrupts the attack before the exploit step fires.\n- A specific guard / check / modifier on the attack path interrupts the claimed exploit step before harm occurs (quote the exact line and trace it) → **REJECTED** (or **DEMOTE** if a related code smell remains)\n- The supposed interruption is speculative (\"probably wouldn't happen\", \"the caller would notice\", \"the deployer would set X\") → **clears**, continue\n\n## Gate 2 — Reachability\n\nProve the vulnerable state exists in a live deployment.\n\n- Structurally impossible (enforced invariant prevents it) → **REJECTED**\n- Requires privileged actions outside normal operation → **DEMOTE**\n- Achievable through normal usage or common token behaviors → **clears**, continue\n\n## Gate 3 — Trigger\n\nProve an unprivileged actor executes the attack.\n\n- Only trusted roles can trigger → **DEMOTE**\n- Unprivileged actor triggers profitably → **clears**, continue\n\n**Admin-action findings — reject unless an unprivileged amplifier is named.** This applies ONLY to actions performed by admin/owner, NOT to unprivileged attacker actions. If the harm requires the admin acting maliciously or against documented intent, **REJECT** — do not even emit as a LEAD (stricter than the DEMOTE above). The finding clears only when the body names a concrete unprivileged amplifier:\n\n- **race** — admin sets X mid-flow; an unprivileged user exploits the window before the update propagates.\n- **retroactive sweep** — an admin update rewrites a pending value already credited.\n- **asymmetric formula** — admin output chains into a formula an unprivileged actor profits from.\n- **access gap** — missing guard, tautological auth, or missing init guard (the access mechanism itself is the bug).\n\nNo amplifier named → **REJECTED**. Amplifier named → judge it on that unprivileged path.\n\n## Gate 4 — Impact\n\nProve material harm to an identifiable victim.\n\n- Self-harm only → **REJECTED**\n- Dust-level, no compounding → **DEMOTE**\n- Material loss to identifiable victim → **CONFIRMED**\n\n## Confidence\n\nStart at **100**, deduct: partial attack path **-20**, bounded non-compounding impact **-15**, requires specific (but achievable) state **-10**. Confidence ≥ 75 gets description + fix. Below 75 gets description only.\n\n**The threshold is 75, and it is set here.** `report-formatting.md` reads it from this line and states it nowhere else. It is 75 and not 80 because the three lead-promotion rules below all land a promoted lead at exactly 75: at a threshold of 80 every cross-contract echo, every multi-agent convergence and every completed partial path would be promoted to a finding and then printed with no **Fix** block. Moving this number means moving those three, or the promotions stop being worth making.\n\n## Safe patterns (do not flag)\n\n- `unchecked` in 0.8+ (but verify the reasoning is correct)\n- Explicit narrowing casts in 0.8+ (reverts on overflow)\n- MINIMUM_LIQUIDITY burn on first deposit\n- SafeERC20 (`safeTransfer`/`safeTransferFrom`)\n- `nonReentrant` (only flag cross-contract attacks)\n- Two-step admin transfer\n- Consistent protocol-favoring rounding unless compounding or zero-rounding\n\n## Lead promotion\n\nBefore finalizing leads, promote where warranted:\n\n- **Cross-contract echo.** Same root cause confirmed as FINDING in one contract → promote in every contract where the identical pattern appears.\n- **Multi-agent convergence.** 2+ agents flagged same area, lead was demoted (not rejected) → promote to FINDING at confidence 75.\n- **Partial-path completion.** Only weakness is incomplete trace but path is reachable and unguarded → promote to FINDING at confidence 75, **description only — a deliberate exception to the threshold**. 75 clears the line, so this finding would otherwise carry a **Fix** block; it does not, because the trace it would fix was never completed. The other two promotions above take their **Fix** block normally.\n\n## Leads\n\nHigh-signal trails for manual investigation. No confidence score, no fix — title, code smells, and what remains unverified.\n\n## Do Not Report\n\nLinter/compiler issues, gas micro-opts, naming, NatSpec. Admin privileges by design. Missing events. Centralization without exploit path. Implausible preconditions (but fee-on-transfer, rebasing, blacklisting ARE plausible for contracts accepting arbitrary tokens).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5025,"content_sha256":"c49b0f60777f5fae71c7b79ea41d36f0e38e114b476529a998c01133c157b43d"},{"filename":"references/report-formatting.md","content":"# Report Formatting\n\n## The one rule\n\n**A report must not overstate its own coverage.** Every other rule in this file serves that\none. The report may print **less** than the scan found — a mistyped marker, a dead agent, a\npass that never ran — and when it does it says so in words. It may never claim to print\n**more**. A report that shows 14 findings and reads as if it showed all of them is the defect\nthis whole design exists to kill.\n\n## Who reads this file\n\nNothing composes a report from this template any more. There are two readers:\n\n- **SKILL.md Turn 4 step 5a** — the pass writing its findings into `run-K.md`. It uses the\n **finding block** shape below: title line, location line, `**Description**`, diff `**Fix**`\n block. Those bytes are pasted straight through into the report, so they are written in the\n report's own shape while the pass still has context to spare.\n- **`references/assemble.sh`** — the assembler. Everything below the finding block — the\n banner, the Scope table, the ordering, the Findings List, the Leads section, \"Known from\n earlier scans\", the disclaimer — is what the assembler emits.\n\n**This file settles the shape. `report-language.md` settles the words.** Every sentence a\nhuman reads in the report — the finding title, the Description, the Lead description, the\nLead's code smells — is written in Simplified Technical English, and that file holds the rules.\nIt is read by the same Turn 4 step 5a that writes the block, and it is in all twelve agent\nbundles. It never reaches the diff in a **Fix** block, the bug-class label inside a key, or an\nidentifier on a location line: those three are data, and it says so.\n\n**The template is the contract the assembler emits, not a shape a model imitates.** A later\neditor must not re-teach the orchestrator to write it. The orchestrator under context pressure\nis the thing that failed; the step that must not lose a finding holds no model judgment.\n\n## Report Path\n\n**Every scan assembles `.solidity-auditor/runs/{stamp}/full-report.md`** — the whole report,\nbanner line down to the disclaimer — at any pass count, with or without flags. That file is\nthe report. There is exactly one producer of it, and it is the assembler.\n\n`--file-output` makes a **copy** of that file into the current working directory as\n`{project-name}-pashov-ai-audit-report-{stamp}.md`, where `{project-name}` is the repo root\nbasename and `{stamp}` is `YYYYMMDD-HHMMSS` at scan time. A copy, not a second build —\nnothing regenerates, so the flag cannot collapse the report the way the terminal once did. The\nflag never causes a report to be produced; it only decides whether a copy lands where the\nrunner can see it.\n\nThe `run-K.md` files stay in `.solidity-auditor/runs/{stamp}/` under the same stamp, so a\nreport and the runs that produced it are tied together by eye. They are never copied into the\nworking directory — they are memory, not reports.\n\n## Output Format\n\nThis is what the assembler emits.\n\n````\n# 🔐 Security Review — \u003cContractName or repo name>\n\n---\n\n## Scope\n\n| | |\n| --- | --- |\n| **Mode** | ALL / default / filename |\n| **Files reviewed** | `File1.sol` · `File2.sol`\u003cbr>`File3.sol` · `File4.sol` | \u003c!-- every file, 3 per line -->\n| **Confidence threshold (1-100)** | N |\n| **Passes** | 3 | \u003c!-- only when passes > 1 -->\n| **Memory** | 12 records before this scan · 14 after · `a1b2c3d` | \u003c!-- only when memory is on -->\n| **Run files** | ⚠️ 2 findings marked, 1 readable. 1 could not be read. | \u003c!-- only when the structure check disagrees -->\n\n---\n\n## Findings\n\n[95] **1. \u003cTitle>**\n\n`ContractName.functionName` · Confidence: 95 · seen in 2/3 runs · KNOWN (4 scans) \u003c!-- runs segment only when passes > 1; memory segment only when memory is on -->\n\n**Description**\n\u003cThe vulnerable code pattern and why it is exploitable, in 1 short sentence>\n\n**Fix**\n\n```diff\n- vulnerable line(s)\n+ fixed line(s)\n```\n\n---\n\n[82] **2. \u003cTitle>**\n\n`ContractName.functionName` · Confidence: 82\n\n**Description**\n\u003cThe vulnerable code pattern and why it is exploitable, in 1 short sentence>\n\n**Fix**\n\n```diff\n- vulnerable line(s)\n+ fixed line(s)\n```\n\n---\n\n\u003c ... all above-threshold findings >\n\n---\n\n[60] **3. \u003cTitle>**\n\n`ContractName.functionName` · Confidence: 60\n\n**Description**\n\u003cThe vulnerable code pattern and why it is exploitable, in 1 short sentence>\n\n---\n\n\u003c ... all below-threshold findings (description only, no Fix block) >\n\n---\n\nFindings List\n\n| # | Confidence | Title |\n|---|---|---|\n| 1 | [95] | \u003ctitle> |\n| 2 | [82] | \u003ctitle> |\n| 3 | [75] | \u003ctitle> |\n| | | **Below Confidence Threshold** |\n| 4 | [60] | \u003ctitle> |\n\n---\n\n## Leads\n\n_Vulnerability trails with concrete code smells where the full exploit path could not be completed in one analysis pass. These are not false positives — they are high-signal leads for manual review. Not scored._\n\n- **\u003cTitle>** — `Contract.function` · NEW — Code smells: \u003cmissing guard, unsafe arithmetic, etc.> — \u003c1-2 sentence description of the trail and what remains unverified> \u003c!-- memory segment only when memory is on -->\n- **\u003cTitle>** — `Contract.function` · seen in 2/3 runs · KNOWN (2 scans) — Code smells: \u003c...> — \u003c1-2 sentence description> \u003c!-- runs segment only when passes > 1 -->\n\n---\n\n## Known from earlier scans\n\n\u003c!-- only when memory is on AND the ledger held at least one record — see Memory in the report -->\n\n_Recorded by earlier scans of this repo, not raised again by this one. Not re-checked — a record here may be fixed, or may still be live and missed. `.solidity-auditor/memory.tsv`._\n\n| Scans | Kind | Location | Title |\n|---|---|---|---|\n| 4 | FINDING | `Router.swap` | Swap trusts a spot price as an oracle |\n| 2 | LEAD | `Vault.sweep` | Sweep lets the owner take user deposits |\n\n---\n\n> ⚠️ This review was performed by an AI assistant. AI analysis can never verify the complete absence of vulnerabilities and no guarantee of security is given. Team security reviews, bug bounty programs, and on-chain monitoring are strongly recommended. For a consultation regarding your projects' security, visit [https://www.pashov.com](https://www.pashov.com)\n\n````\n\n**Rules describing the output**, all of them held by the assembler and none of them addressed\nto a model:\n\n- Findings are sorted by confidence, highest first.\n- A finding at or above the threshold carries a **Description** and a diff **Fix** block. One\n below carries the **Description** only. Nothing in the run file marks which side of the line\n a finding sits on — it is re-derived from the confidence, so there is one source of truth.\n- The **Below Confidence Threshold** separator row is printed whenever at least one finding\n falls below the threshold, and left out when none does. Without it the table runs 1, 2, 3\n with nothing to mark the boundary, so a reader of the table alone cannot tell which findings\n carry no **Fix** block.\n- **Empty cases say so in words, never with an empty table**: `_None — this scan raised no\n findings._` in place of the Findings List, `_None._` under Leads, `**Body missing** — pass K\n raised this finding and wrote no description.` in place of a body.\n- **A `Run files` row appears only when the structure check disagrees.** The model types the\n markers that bound each finding in a run file, so it can mistype one. The assembler counts\n open markers against close markers against readable blocks, and prints the disagreement as a\n Scope row naming how many findings could not be read. Everything readable is still printed\n and the assembler never stops. That row is the one rule at the top made mechanical.\n- **The Scope table's column padding is not part of the shape.** Markdown renders\n `| **Mode** | default |` and a space-padded form identically, and the padding above is for\n reading this file. Content and order are the contract; character widths are not.\n\n**The threshold is 75**, set in `judging.md` and printed in the Scope row above. It is named\nin `judging.md` and nowhere else; this file reads it from there. A finding at 75 or above gets\na description and a **Fix** block, one below 75 gets the description only. A promoted lead\nlands at exactly 75, so it clears — that is why the number is 75.\n\n## The size trigger\n\n**More than 20 findings.** At 20 the terminal prints the assembled file word for word; at 21\nit stops printing findings. Findings only — above and below the threshold together. **Leads\nare excluded from the trigger count**, so a scan with 18 findings and 40 leads prints in full.\n\nThe trigger is on **size, not on flags**. A plain scan of a small contract is unchanged; a\nplain scan of a big one gets the new shape.\n\n**Above the trigger the terminal prints four things:** the banner, the Scope table unchanged,\na **Findings List holding the top 3 findings**, and the path to the full report — then the\ndisclaimer. About fifteen lines.\n\nThe full report is unaffected. Above the trigger, as below it, `full-report.md` holds every\nfinding in the shape above — only the terminal changes.\n\n**The slice is honest because it is counted.** The table's header reads\n`Findings List — top 3 of {F}`, and that `{F}` is the whole rule:\n\n- **A report may print less than the scan found. It may never look like it printed more.**\n A bare list of three findings breaks that — a reader cannot tell a truncated list from a\n complete one. `top 3 of 52` cannot be misread.\n- **The total goes in the header, not a footnote.** A reader who sees the table sees the\n total in the same glance. This is not a style choice; a footnote can be scrolled past.\n- **Three rows, and nothing else is enumerated.** No Leads, no \"Known from earlier scans\",\n no findings past the third.\n\n> **This replaces the older \"nothing is enumerated above the trigger\" rule**, which forbade\n> every top-N slice. That rule was right that bare truncation lies and wrong to conclude no\n> slice can be honest; a counted one is. A later editor must not restore the blanket ban by\n> citing the half of the argument that survived.\n\n**The rows are extracted by shell, never typed.** One `awk` over the assembled file — which is\nalready sorted highest-confidence first, so no sort happens in the terminal step. The\norchestrator has read dozens of findings by then and is the least reliable thing in the room;\nthe extractor cannot mis-rank, mis-quote a function name or invent a confidence. The command\nis in `dedup-and-assembly.md` Turn 5 step 3b.\n\n**The `Seen` column appears only when the `Passes` row does** — that is, when the pass count is\nabove 1. A plain scan's location line carries no `seen in k/N runs`, so the column is dropped\nfrom the header, the separator and the rows together. A five-column header over four-column\nrows draws as a broken table.\n\n**There is no counts summary.** The `{F} findings, {L} leads` line and the\n`100-90 / 89-75 / below 75` split are not printed. `top 3 of {F}` carries the only total the\nmoment needs, and the bucket split stood between the reader and the path. The buckets are still\n**computed** as a self-check — `A + B + C` must equal `F`, in every mode, or the assembled file\nis malformed and the scan says so instead of printing a slice of it.\n\nThe path line names `.solidity-auditor/runs/{stamp}/full-report.md`, or — when `--file-output`\nwas passed — the **copy** and only the copy. Never both: two paths to identical bytes answer a\nquestion the runner did not ask at the moment they want one filename. That line is the only\npart of the block a flag changes.\n\nThe exact wording, spacing and order of the block are in `dedup-and-assembly.md` Turn 5 step\n3b, which is where the terminal is printed.\n\n## The freeze\n\n**A small scan with no flags prints what it printed before any of this existed.** Below the\ntrigger the terminal prints `full-report.md` word for word — every byte, nothing added,\nnothing cut, no preface — so the freeze is a property of the assembled file.\n\nThe freeze is a **diff against `golden/plain-full-report.md`**, the frozen plain-path report,\nand it covers the report's **content and order, not its column padding** and not what lands on\ndisk. Byte-identity against \"today\" was tried and is unmeetable: no captured report exists\nanywhere, and today's bytes were composed by a model from a template that contradicted itself.\nThe golden file is what replaced it.\n\nWhat the freeze does **not** cover: writing files. Every scan now writes a runs directory and\nassembles a report there, at any pass count. Writing a file is not printing, and the freeze is\non printed output.\n\n## Memory in the report\n\nEverything in this section is printed **only when memory is on** (`--memory`, or a pass count\nabove 1). With memory off the assembled file carries no extra rows, no extra segments, no\nextra sections and no dangling `·`, and below the trigger it still diffs clean against the\ngolden file.\n\n**Segment order on the meta line.** One dot-separated chain, in a fixed order:\n\n```\n`ContractName.functionName` · Confidence: 95 · seen in 2/3 runs · KNOWN (4 scans)\n```\n\nlocation · confidence · runs · memory. **A segment appears only when it carries information** —\n`seen in k/N runs` needs more than one run, `KNOWN (n scans)` / `NEW` needs memory on. So:\n\n| Scan | Meta line |\n|---|---|\n| 1 pass, no flags | `` `Vault.withdraw` · Confidence: 95 `` |\n| 1 pass, `--memory` | `` … · Confidence: 95 · KNOWN (4 scans) `` |\n| 3 passes | `` … · Confidence: 95 · seen in 2/3 runs · KNOWN (4 scans) `` |\n\n`KNOWN (4 scans)` and `NEW` are the ledger's own two words, so the report and the file say the\nsame thing. **`n` counts this scan.** The tag is derived by the assembler from\n`memory-before.tsv` — the pre-scan photocopy — and `n` is that row's `scans` **plus one**: the\nscan that is printing has just found it again, and the ledger already holds that same number.\nPrinting the stored count instead would show a report one behind its own ledger. `NEW` carries\nno count. The tag is **not** carried in the run-file marker: one source, so the report and the\nledger cannot disagree.\n\nThe `Scans` column of \"Known from earlier scans\" is the opposite case and is printed **as\nstored**: this scan did not raise those records, so nothing about them counts up. The\n**Findings List** table is unchanged — it stays `# | Confidence | Title`.\n\nLeads carry the same segments as findings, on their own line, because a Lead is a full ledger\nrecord.\n\n**The `Memory` row in Scope.** `\u003crecords in the ledger before this scan> records before this\nscan · \u003crecords after the merge> after · `\u003csha>``. The SHA is the one every row of this scan\nwrote; `none` when the repo has no commits. The row states the whole ledger state in one line,\nso the two counts show at a glance what the scan added.\n\n**The \"before\" count is the count after the prune** — the row count of `memory-before.tsv` as\nthe merge reads it, not the row count of the file the scan opened. On a scan where the prune\ndrops records the two differ, and using the pre-prune number makes the row's own arithmetic\nwrong: a scan that opened 4 records, pruned 1 and added 12 would read `4 records before this\nscan · 15 after`, and a reader counting 4 + 12 gets 16. The prune is already reported on its\nown line (`Pruned N records …`), so the Scope row does not restate it and must not contradict\nit.\n\n**Building \"Known from earlier scans\".** These are the records the ledger already held that\nthis scan did **not** raise again. It is **not** a filter on the `status` column — `status`\nstays `KNOWN` once set and never marks the not-found-again case.\n\n**The `comm` block that used to sit here is now in `assemble.sh`**, under\n`# Known from earlier scans`. It reads two inputs Turn 5 copies into the runs directory:\n`memory-before.tsv`, the pruned photocopy, and `source-names.tsv`. The keys this scan raised\ncome from the assembler's own index of the run files, so no `run-keys.tsv` or `scan-rows.tsv`\nis carried in — the run files are the record. It is computed once per scan, scoped to the\nwhole scan and not to the last run, so a bug found in run 1 but not in run 3 does not appear\nhere.\n\nEach key `comm` returns is one row: `Scans`, `Kind` and `Title` come straight from its row in\n`memory-before.tsv`. `Location` is rebuilt from the key's first two segments, which are stored\nnormalised (lower case, hyphenated) and cannot be printed as they are — each segment is looked\nup in `source-names.tsv` and printed as the source spells it, or printed as stored when it is\nnot in the map, which happens on a named-file scan where the contract was never assembled into\n`source.md`.\n\nThree states, and no fourth:\n\n- The ledger held records this scan did not raise → the table above.\n- The ledger held records and this scan raised **every one** → `_None — every record in the\n ledger was raised again by this scan._`\n- Memory is off, or the ledger is empty (first-ever scan) → **no section at all.** A plain\n scan grows no empty headings.\n\nThe italic line under the heading is load-bearing and is printed verbatim: these records are\n**not re-checked**. A reader must not take the section as \"still open\", and must not take it\nas \"fixed\".\n\n## Loop mode in the report\n\nEverything in this section is printed **only when the pass count is above 1**. One pass prints\nnone of it.\n\n**One report per scan.** However many passes ran, the runner sees one combined list. The\nper-pass output is one summary line each (SKILL.md Turn 4 step 5b) and one `run-K.md` in\n`.solidity-auditor/runs/{stamp}/` — neither is a report.\n\n**`seen in k/N runs`.** `k` is how many of this scan's runs raised that group key; `N` is how\nmany passes produced a run file. A pass that collapsed produced none, so it is not in `N` — a\nreport must not claim a run that never happened. The segment sits between confidence and the\nmemory tag, and it is printed on Leads exactly as it is on findings, because a Lead is a full\nledger record.\n\n**`seen in k/N runs` and `KNOWN (n scans)` count different things and must never be mixed.**\n`k/N` counts runs inside **this** scan and is never written to the ledger. `n` counts scans and\ncomes from the ledger. A finding first raised by pass 1 of this scan is `NEW` in pass 3 — it is\nthis scan's work, however many of its runs saw it.\n\n**Which write-up survives the merge.** One winner per key: strongest kind, then highest\nconfidence, then the later pass — a later pass read the ledger every earlier pass wrote, so it\nis the better-informed write-up. **Confidence is untouched by the merge.** A finding three runs\nsaw keeps the number the runs gave it; repetition is reported on the line, never scored, and\n`seen in 3/3 runs` is there for the reader to weigh.\n\n**The `Passes` row in Scope** is composed by the assembler from recorded facts, never written\nas prose. It carries degradation text whenever the scan did not run whole — a report claiming\n3 passes when one of them ran three-quarters of its agents overstates its own coverage:\n\n```\n| **Passes** | 3 |\n| **Passes** | 3 (pass 2 ran 11/12 agents) |\n| **Passes** | 2 of 3 (pass 3 failed) |\n| **Passes** | 2 of 3 (pass 1 ran 11/12 agents, pass 3 failed) |\n```\n\nThe `R of P` prefix comes from counting run files. The dead agent's **name** never appears\nhere — it stays in the run file, in the sentence under the `\u003c!--RUN-->` marker.\n\nOn a 1-pass scan this row is suppressed, so a pass 1 that produced nothing is reported by the\n`Run files` row instead: `⚠️ No pass produced a run file. This scan reviewed nothing.`\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":19772,"content_sha256":"2597b23f9e216df7b4c7877a9cb07b44d2cea2f8ef3fb8526881b6589016f349"},{"filename":"references/report-language.md","content":"# Report language — Simplified Technical English\n\nEvery sentence a human reads in the report obeys this file. The rules are ASD-STE100\n(Simplified Technical English), reduced to the part an audit finding needs.\n\n## Why\n\nA report is read by the developer who must fix the code, often at speed, often not in their\nfirst language. A sentence they must decode twice costs the fix, and the bug stays in the\ncode. The scan is only as good as the sentence that delivers it.\n\nShort words do not make a finding less rigorous. The proof is the diff in the **Fix** block\nand the location on the meta line. The sentence exists to say what breaks and who breaks it.\n\n## What this covers\n\nFour pieces of text, and no others:\n\n- the finding **title**\n- the **Description** sentence\n- the **Lead** description\n- the **code smells** list on a Lead\n\n## What it must never touch\n\nThese are data, not prose. Rewriting one of them for readability breaks the scan:\n\n- **Solidity identifiers** — contract, function, variable and event names. They are printed as\n the source spells them. `withdrawToNativeChain` is never softened to `withdraw to chain`.\n- **The diff inside a Fix block.** It is code. It is pasted verbatim from the agent that wrote\n it, and `judging.md` and `dedup-and-assembly.md` both forbid paraphrase there.\n- **The bug-class label** (`zero-abort-address`). It is the third segment of a memory key.\n The same bug under a plainer word is a second ledger record, so memory remembers it twice\n and recognises it never. Labels are chosen by the rules in `dedup-and-assembly.md` step 1,\n never by this file.\n- **Fixed strings the assembler prints** — the banner, the Scope table headers, the italic\n lines under Leads and \"Known from earlier scans\", the disclaimer. They are in\n `assemble.sh` and they are already written.\n\n## The rules\n\n1. **One sentence, one idea.** A Description is one sentence. A Lead description is one or\n two. Never three.\n2. **Twenty-five words is the ceiling.** Count them. Over the ceiling means two ideas in one\n sentence — cut it or split it.\n3. **Active voice, and name the actor.** Who does this? Write that word first.\n - No: `Funds can be drained due to a missing check.`\n - Yes: `Any caller can take all the tokens, because the function checks no caller.`\n4. **Simple present tense.** The code does this today. Not `would be able to`, not\n `could potentially`. If the path is uncertain it is a Lead, and the Lead says which step\n is unproven.\n5. **No `-ing` clauses.** They hide the actor and the order of events. Use `that`, `so`, or a\n second sentence.\n - No: `Missing access control allowing anyone to drain the vault.`\n - Yes: `The function has no access control, so any caller can take all the tokens.`\n6. **Keep the articles.** `the`, `a`, `an`. Telegraphic style — `Function reverts on zero\n amount` — is not shorter to read, only shorter to type.\n7. **Three nouns in a row is the limit.** Break a longer stack with `of`, `for` or `in`.\n - No: `cross-chain message replay protection gap`\n - Yes: `the contract does not stop a replay of a cross-chain message`\n8. **One word for one thing, every time.** Pick `token`, or pick `asset`, and use that word\n in the title, the Description and the fix. A synonym reads as a second thing.\n9. **No metaphor, no idiom, no slang.** See the table below. A metaphor is a word the reader\n must translate before they can act.\n10. **Say what happens, not how bad it is.** `catastrophic`, `critical`, `severe` and\n `trivially` carry no information — the confidence number and the Fix block carry it. Write\n the effect: `the caller keeps the tokens and the pool keeps the debt`.\n11. **Positive statements. No double negatives.** `The check is absent` beats `the check is\n not present`.\n12. **Event order is sentence order.** Cause, then effect. `The price comes from the pool\n balance, so an attacker who moves the balance moves the price.`\n13. **A title is one clause, twelve words or fewer,** present tense, active, and it names the\n defect or its effect. `Swap trusts a spot price as an oracle.`\n Not `Oracle manipulation vulnerability in swap functionality`.\n\n## Words to replace\n\nLeft column: the wording to keep out of the report. Right column: what the report prints.\n\n| Do not write | Write |\n| --- | --- |\n| burns the funds | sends the tokens to an address that cannot spend them |\n| bricks the contract | makes the function fail for every caller, forever |\n| rug / rugpull | the owner takes the user deposits |\n| griefing | an attacker makes the function fail for other users |\n| footgun | (delete the word and name the defect) |\n| silently swallows the error | ignores the error |\n| DoS / denial of service | blocks every caller |\n| attack surface / attack vector | path |\n| malicious actor / bad actor | an attacker |\n| leverages / utilizes | uses |\n| is able to | can |\n| in order to | to |\n| arbitrary value | any value the caller chooses |\n| stale price | old price |\n| sanity check | check |\n| happy path | the normal path |\n| atomically | in one transaction |\n| trustlessly / permissionlessly | (delete, or name who may call) |\n| non-zero | more than zero |\n| cascading failures | (name each failure) |\n| exponentially / trivially / catastrophically | (delete) |\n| edge case | (name the input or the state) |\n\n## Technical names stay\n\nSTE-100 keeps technical names, and a security report cannot work without them:\n`reentrancy`, `slippage`, `oracle`, `revert`, `overflow`, `rounding`, `allowance`, `nonce`,\n`slot`, `delegatecall`. Use them. They are precise and the reader who fixes Solidity knows\nthem.\n\nThe rule is what goes **around** them: name the mechanism once with its technical name, then\nsay in plain words what the attacker gets.\n\n- No: `Classic CEI violation enabling reentrant drainage of the vault.`\n- Yes: `The function sends ETH before it lowers the balance, so a reentrant call withdraws\n the same deposit twice.`\n\n## Worked examples\n\nBefore, and after. The code the sentence describes did not change.\n\n**One**\n\n- Before: `Improper validation of the abort address results in a catastrophic loss of user\n funds via burning to the zero address.`\n- After: `The function accepts a zero abort address, so a failed transfer sends the tokens to\n an address that nobody controls.`\n\n**Two**\n\n- Before: `Lack of slippage protection in the swap path exposes users to sandwich attacks,\n potentially leading to significant value extraction by MEV searchers.`\n- After: `The swap sets the minimum output to zero, so an attacker who trades before and\n after the victim keeps most of the output.`\n\n**Three — a Lead**\n\n- Before: `Potential accounting inconsistency stemming from asymmetric fee handling on\n deposit/withdraw flows warranting further investigation.`\n- After: `The deposit takes the fee from the amount, and the withdrawal takes it from the\n balance. We did not find the input that makes the two disagree.`\n\n## Self-check before you write the block\n\nFour questions. Any `no` means rewrite the sentence, not the code:\n\n- Does the sentence name **who** acts and **what** they get?\n- Is it one sentence, twenty-five words or fewer, with no `-ing` clause?\n- Is every word in it a word the reader does not have to translate?\n- Are the identifiers, the label and the diff exactly as the source and the agent wrote them?\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7384,"content_sha256":"20e162ff6dd57e1b267594f4f543183f6043dbd913556fe34b4bea47296f3a2d"},{"filename":"references/senior-auditor-sop.md","content":"# Senior Auditor's Mindset\n\nThis is how a senior auditor thinks. Pattern-matching catches the obvious bugs — your specialty file teaches that. The high-value bugs, the ones everyone else misses, come from HOW you reason about code, not from WHAT bugs you know.\n\nThe senior auditor's edge is not \"knowing more bug patterns\" — it is having internalized mental tools they reach for instinctively when something feels off, when a path seems clean, or when a conclusion comes too quickly.\n\nThis file gives you three tools. They are not steps. You reach for the right one the moment the trigger fires. They are how you think, not text you write: do not put them in your reply, which holds only your FINDING and LEAD blocks. Use them. Trust your discomfort.\n\nA finding is not real until you've traced the attack with concrete values. You are an attacker, not a defender — when you find a bug, deepen the attack; never argue yourself out of one.\n\n---\n\n## 1. The Feynman test (FIRST — use it before anything else)\n\n**This is the first tool. Apply it the moment you open any new function or contract — before you reason about anything else.** Code you have not Feynman'd is code you have not actually understood.\n\nWhen you read code, STOP and ask: \"Can I explain what this function does to someone who doesn't know Solidity?\"\n\nTry it. In plain words. The places where your explanation gets fuzzy — where you reach for Solidity jargon instead of plain meaning — are where you're papering over an assumption. That's where bugs hide.\n\nExample: you read `_handleFeeTransfer(zrc20, fee)` and your explanation comes out as \"it transfers the fee.\" That's not Feynman. Feynman is: \"it picks up the protocol's commission off the user's payment and moves it to the treasury wallet.\" Now keep going: what if the payment is in ETH and the function uses an ERC20 method? Your plain-English explanation breaks. Bug.\n\nA senior auditor doesn't trust their understanding until they can explain it without the safety net of technical vocabulary.\n\n---\n\n## 2. Socratic questioning\n\nFor every line of code, ask: why is this here? What does it assume? What happens if the assumption breaks?\n\nDon't accept \"because that's how it's written\" as an answer. Don't accept \"the function name says so\" as an answer. Drill until you reach the implicit belief the code rests on. The first answer is usually a restatement. The actual assumption is two or three \"whys\" deeper.\n\nExample: `if (zrc20 != _ETH_ADDRESS_) IERC20(zrc20).transferFrom(msg.sender, address(this), amount);`\n- Why is `zrc20 != _ETH_ADDRESS_` checked? → because ETH isn't transferable via transferFrom.\n- Why is there no else branch? → because the developer assumed ETH arrives via `msg.value`.\n- Where is `msg.value` enforced to equal `amount` for the ETH path? → **nowhere**. Bug.\n\nA senior auditor accepts no \"because\" without examining it.\n\n---\n\n## 3. Inversion\n\nEvery clean path gets a backward pass. After you understand what the code IS supposed to do, ask: how would I make it NOT do that?\n\nSame code, attacker's eye instead of developer's eye. The developer asks \"does this work?\" The attacker asks \"how do I break this?\" Read every check and ask \"what value slips past it?\" Read every state update and ask \"what state am I in just before this?\"\n\nA senior auditor never reads code only forward.\n\n---\n\n## When to reach for which tool\n\nYou don't apply these in order — except Feynman, which is always first. You reach for what the moment calls for:\n\n- Opening any new function or contract → **Feynman** (always — before anything else)\n- Trying to understand a line you don't yet → **Socratic**\n- Something looks too clean → **Inversion**\n- You reached a \"bug\" conclusion → amplify the attack (chain it, find more victims, lower the precondition cost — do NOT refute it)\n\nThe tools are how you keep yourself honest. Without them, you fall into the trap of every junior auditor: trusting your first read, accepting code that \"looks right,\" moving on when something feels off.\n\nTrust your discomfort. Reach for the tool. Don't stop until the discomfort has a name.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4130,"content_sha256":"44a265f9af0068a2eb14d285320285f7907fde7eed27b30d61aee5b56052b063"},{"filename":"VERSION","content":"4.1\n","content_type":"text/plain; charset=utf-8","language":null,"size":4,"content_sha256":"ccc8c8540a4da7ae55474bbcafcf335a2dde73265b3bbd29ffa047c08777f307"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Smart Contract Security Audit","type":"text"}]},{"type":"paragraph","content":[{"text":"You are the orchestrator of a parallelized smart contract security audit.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Mode Selection","type":"text"}]},{"type":"paragraph","content":[{"text":"Exclude pattern:","type":"text","marks":[{"type":"strong"}]},{"text":" skip ","type":"text"},{"text":"dependency and build directories","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"node_modules/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"lib/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"artifacts/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"cache/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"out/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"broadcast/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"coverage/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"typechain*/","type":"text","marks":[{"type":"code_inline"}]},{"text":" — and ","type":"text"},{"text":"non-production code","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"interfaces/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"mocks/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"test/","type":"text","marks":[{"type":"code_inline"}]},{"text":" and files matching ","type":"text"},{"text":"*.t.sol","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"*Test*.sol","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"*Mock*.sol","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Deploy scripts stay in scope.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"script/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"deploy/","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"*.s.sol","type":"text","marks":[{"type":"code_inline"}]},{"text":" are audited like any other code: a deploy script sets constructor arguments, hands over ownership and seeds state, and it carries real bugs. Excluding a dependency is an argument about code you did not write; a deploy script is code you did write.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Default","type":"text","marks":[{"type":"strong"}]},{"text":" (no arguments): scan all ","type":"text"},{"text":".sol","type":"text","marks":[{"type":"code_inline"}]},{"text":" files using the exclude pattern. Use Bash ","type":"text"},{"text":"find","type":"text","marks":[{"type":"code_inline"}]},{"text":" (not Glob), exactly this command:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"find . -type f -name '*.sol' \\\n -not -path '*/node_modules/*' -not -path '*/lib/*' -not -path '*/artifacts/*' \\\n -not -path '*/cache/*' -not -path '*/out/*' -not -path '*/broadcast/*' \\\n -not -path '*/coverage/*' -not -path '*/typechain*/*' \\\n -not -path '*/interfaces/*' -not -path '*/mocks/*' -not -path '*/test/*' \\\n -not -name '*.t.sol' -not -name '*Test*.sol' -not -name '*Mock*.sol'","type":"text"}]},{"type":"paragraph","content":[{"text":"-type f","type":"text","marks":[{"type":"code_inline"}]},{"text":" is ","type":"text"},{"text":"required, not tidiness","type":"text","marks":[{"type":"strong"}]},{"text":": Hardhat writes each build artifact into a ","type":"text"},{"text":"directory","type":"text","marks":[{"type":"em"}]},{"text":" named ","type":"text"},{"text":"GatewayCrossChain.sol/","type":"text","marks":[{"type":"code_inline"}]},{"text":", so a ","type":"text"},{"text":"find","type":"text","marks":[{"type":"code_inline"}]},{"text":" without it matches directories as if they were source files and hands them to ","type":"text"},{"text":"cat","type":"text","marks":[{"type":"code_inline"}]},{"text":". Do not drop it, and do not re-derive this command — paste it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"$filename ...","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":": scan the specified file(s) only. ","type":"text"},{"text":"The exclude pattern does not apply here.","type":"text","marks":[{"type":"strong"}]},{"text":" A file named on the command line is always scanned, wherever it lives — naming ","type":"text"},{"text":"node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol","type":"text","marks":[{"type":"code_inline"}]},{"text":" scans that file. The pattern chooses what a ","type":"text"},{"text":"default","type":"text","marks":[{"type":"em"}]},{"text":" scan discovers; it never overrides an explicit request.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Flags:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--file-output","type":"text","marks":[{"type":"code_inline"}]},{"text":" (off by default): ","type":"text"},{"text":"copy","type":"text","marks":[{"type":"strong"}]},{"text":" the assembled report into the working directory (name per ","type":"text"},{"text":"{resolved_path}/report-formatting.md","type":"text","marks":[{"type":"code_inline"}]},{"text":"). The flag never causes a report to be produced — every scan assembles ","type":"text"},{"text":".solidity-auditor/runs/{stamp}/full-report.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" whether it is passed or not. It only decides whether a copy lands where the runner can see it.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Line 33 used to read \"Never write a report file unless explicitly passed\", and that is now false.","type":"text","marks":[{"type":"strong"}]},{"text":" The rule was written to protect the runner's ","type":"text"},{"text":"working directory","type":"text","marks":[{"type":"strong"}]},{"text":", and that protection is unchanged: without the flag, nothing is written outside ","type":"text"},{"text":".solidity-auditor/","type":"text","marks":[{"type":"code_inline"}]},{"text":". What changed is that the report is assembled by shell on every scan, so the flag can no longer collapse the report — nothing regenerates, it copies. A later editor must not read this as a mistake and revert it.","type":"text"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--memory","type":"text","marks":[{"type":"code_inline"}]},{"text":" (off by default): remember findings between scans in a ledger at ","type":"text"},{"text":".solidity-auditor/memory.tsv","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the audited repo. Any pass count above 1 turns memory on by itself, whether or not the flag was passed.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--loop [N]","type":"text","marks":[{"type":"code_inline"}]},{"text":" (off by default): run N passes in one scan, each pass told what the earlier ones found, ending in one combined report. ","type":"text"},{"text":"--loop","type":"text","marks":[{"type":"code_inline"}]},{"text":" with no number is ","type":"text"},{"text":"3","type":"text","marks":[{"type":"strong"}]},{"text":" passes. ","type":"text"},{"text":"--loop 1","type":"text","marks":[{"type":"code_inline"}]},{"text":" is a 1-pass scan. The flag exists for runners who prefer flags; when it is passed the picker in Turn 1b does not ask, it obeys.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Vocabulary (used throughout this file):","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"run","type":"text","marks":[{"type":"strong"}]},{"text":" — one pass of the 12 agents.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scan","type":"text","marks":[{"type":"strong"}]},{"text":" — one invocation of this skill. A scan holds 1 or more runs.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The ledger's ","type":"text"},{"text":"scans","type":"text","marks":[{"type":"code_inline"}]},{"text":" column counts scans. The report's ","type":"text"},{"text":"seen in k/N runs","type":"text","marks":[{"type":"code_inline"}]},{"text":" counts runs inside one scan and is never written to the ledger. Two numbers, two names — do not mix them.","type":"text"}]},{"type":"paragraph","content":[{"text":"The pass count the scan runs is ","type":"text"},{"text":"{passes}","type":"text","marks":[{"type":"code_inline"}]},{"text":" — settled in Turn 1b, 1 or more. The ","type":"text"},{"text":"loop body","type":"text","marks":[{"type":"strong"}]},{"text":" is Turn 2 step 2c, Turn 2 step 3, Turn 3a, Turn 3b and Turn 4, and it runs once per pass. Everything before it runs once per scan, and Turn 5 closes the scan once, whatever ","type":"text"},{"text":"{passes}","type":"text","marks":[{"type":"code_inline"}]},{"text":" is.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"HARD RULE — the plain path must stay plain.","type":"text","marks":[{"type":"strong"}]},{"text":" A 1-pass scan with no flags reaches ","type":"text"},{"text":"none","type":"text","marks":[{"type":"strong"}]},{"text":" of the memory steps: not Turn 1c, not Turn 2 step 2, not Turn 4 step 4, not Turn 4 step 6. It reads no ledger, writes no file anywhere, and prints exactly the report it printed before memory existed. Memory is on only when ","type":"text"},{"text":"--memory","type":"text","marks":[{"type":"code_inline"}]},{"text":" was passed or the pass count is above 1. A later editor who is tempted to make any memory step unconditional is breaking this on purpose, not tidying up.","type":"text"}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"A 1-pass answer reaches none of the loop or memory machinery.","type":"text","marks":[{"type":"strong"}]},{"text":" No ledger is read or written. No ","type":"text"},{"text":"Passes","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"Memory","type":"text","marks":[{"type":"code_inline"}]},{"text":" row in the Scope table. No ","type":"text"},{"text":"seen in k/N runs","type":"text","marks":[{"type":"code_inline"}]},{"text":". No ","type":"text"},{"text":"KNOWN","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"NEW","type":"text","marks":[{"type":"code_inline"}]},{"text":" tag. No per-pass summary lines. ","type":"text"},{"text":"The printed report is the report this skill printed before loop mode existed.","type":"text","marks":[{"type":"strong"}]},{"text":" The only trace of the picker is the question itself.","type":"text"}]},{"type":"paragraph","content":[{"text":"What it does write.","type":"text","marks":[{"type":"strong"}]},{"text":" Every scan writes ","type":"text"},{"text":".solidity-auditor/runs/{stamp}/","type":"text","marks":[{"type":"code_inline"}]},{"text":" — one ","type":"text"},{"text":"run-1.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", one ","type":"text"},{"text":"scope.tsv","type":"text","marks":[{"type":"code_inline"}]},{"text":", one ","type":"text"},{"text":"full-report.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — because the report is assembled from those files at every pass count, and ","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"mode","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"files","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"threshold","type":"text","marks":[{"type":"code_inline"}]},{"text":" are needed by every report. This paragraph used to say a 1-pass answer creates no ","type":"text"},{"text":".solidity-auditor/","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory and no ","type":"text"},{"text":"runs/","type":"text","marks":[{"type":"code_inline"}]},{"text":" files; that is now false, and it was never what the rule was protecting.","type":"text"}]},{"type":"paragraph","content":[{"text":"What the rule protects, stated exactly:","type":"text","marks":[{"type":"strong"}]},{"text":" on the plain path the scan reads no ledger, writes no ","type":"text"},{"text":"mem_","type":"text","marks":[{"type":"code_inline"}]},{"text":" key, and prints a Scope table of exactly ","type":"text"},{"text":"three","type":"text","marks":[{"type":"strong"}]},{"text":" rows — ","type":"text"},{"text":"Mode","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Files reviewed","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Confidence threshold (1-100)","type":"text","marks":[{"type":"code_inline"}]},{"text":" — and no ","type":"text"},{"text":"Passes","type":"text","marks":[{"type":"code_inline"}]},{"text":" row, no ","type":"text"},{"text":"Memory","type":"text","marks":[{"type":"code_inline"}]},{"text":" row. Disk is not printed output. A later editor who makes a ","type":"text"},{"text":"memory","type":"text","marks":[{"type":"strong"}]},{"text":" step unconditional is breaking this on purpose; writing the runs directory is not one of those steps.","type":"text"}]},{"type":"paragraph","content":[{"text":"--memory","type":"text","marks":[{"type":"code_inline"}]},{"text":" on a 1-pass run is the single exception: memory turns on, the loop machinery stays off.","type":"text"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Orchestration","type":"text"}]},{"type":"paragraph","content":[{"text":"Turn 1 — Discover.","type":"text","marks":[{"type":"strong"}]},{"text":" Print the banner, then make these parallel tool calls in one message:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"lower-alpha"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Bash ","type":"text"},{"text":"find","type":"text","marks":[{"type":"code_inline"}]},{"text":" for in-scope ","type":"text"},{"text":".sol","type":"text","marks":[{"type":"code_inline"}]},{"text":" files per mode selection","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Glob for ","type":"text"},{"text":"**/references/hacking-agents/shared-rules.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — extract the ","type":"text"},{"text":"references/","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory (two levels up) as ","type":"text"},{"text":"{resolved_path}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ToolSearch ","type":"text"},{"text":"select:Agent","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read the local ","type":"text"},{"text":"VERSION","type":"text","marks":[{"type":"code_inline"}]},{"text":" file from the same directory as this skill","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Bash ","type":"text"},{"text":"curl -sf https://raw.githubusercontent.com/pashov/skills/main/solidity-auditor/VERSION","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Bash ","type":"text"},{"text":"mktemp -d ./.audit-XXXXXX","type":"text","marks":[{"type":"code_inline"}]},{"text":" → store as ","type":"text"},{"text":"{bundle_dir}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Bash ","type":"text"},{"text":"date +%Y%m%d-%H%M%S","type":"text","marks":[{"type":"code_inline"}]},{"text":" → store as ","type":"text"},{"text":"{stamp}","type":"text","marks":[{"type":"code_inline"}]},{"text":", the scan-time stamp. ","type":"text"},{"text":"One stamp per scan, computed once, here.","type":"text","marks":[{"type":"strong"}]},{"text":" The runs directory, the run files and any ","type":"text"},{"text":"--file-output","type":"text","marks":[{"type":"code_inline"}]},{"text":" copy all carry it, so a report and the runs that produced it are tied together by eye.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Turn 1a — Open the scan directory.","type":"text","marks":[{"type":"strong"}]},{"text":" After the ","type":"text"},{"text":"find","type":"text","marks":[{"type":"code_inline"}]},{"text":" returns, in one Bash command:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mkdir -p .solidity-auditor/runs/{stamp}\n: > .solidity-auditor/runs/{stamp}/scope.tsv","type":"text"}]},{"type":"paragraph","content":[{"text":"Then write the three scope keys this turn knows. ","type":"text"},{"text":"A scope key is written with one ","type":"text","marks":[{"type":"strong"}]},{"text":"printf","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" and never any other way:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"printf '%s\\t%s\\n' name \"{project-name}\" >> .solidity-auditor/runs/{stamp}/scope.tsv\nprintf '%s\\t%s\\n' mode \"{mode}\" >> .solidity-auditor/runs/{stamp}/scope.tsv\nprintf '%s\\t%s\\n' files \"{file list}\" >> .solidity-auditor/runs/{stamp}/scope.tsv","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"{project-name}","type":"text","marks":[{"type":"code_inline"}]},{"text":" — the repo root basename, the same one ","type":"text"},{"text":"report-formatting.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" names.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"{mode}","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"default","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"filename","type":"text","marks":[{"type":"code_inline"}]},{"text":", as Mode Selection settled it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"{file list}","type":"text","marks":[{"type":"code_inline"}]},{"text":" — every in-scope path the ","type":"text"},{"text":"find","type":"text","marks":[{"type":"code_inline"}]},{"text":" returned, ","type":"text"},{"text":"space separated on one line","type":"text","marks":[{"type":"strong"}]},{"text":", in ","type":"text"},{"text":"find","type":"text","marks":[{"type":"code_inline"}]},{"text":" order. The assembler wraps them 3 per row; the order it prints is the order written here.","type":"text"}]}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"scope.tsv","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" is ","type":"text","marks":[{"type":"strong"}]},{"text":"key\u003cTAB>value","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":", append-only, last line per key wins.","type":"text","marks":[{"type":"strong"}]},{"text":" An absent key gives an absent table row — that is what keeps the plain scan's Scope table at three rows with no special case. A tab or a newline in a value breaks the row, so values are ","type":"text"},{"text":"stripped","type":"text","marks":[{"type":"strong"}]},{"text":", not escaped: no key here has any use for either character. Six writers across four turns append to this one file, and none of them ever rewrites or deletes a line.","type":"text"}]},{"type":"paragraph","content":[{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"mode","type":"text","marks":[{"type":"code_inline"}]},{"text":" are the ","type":"text"},{"text":"only two keys a model types","type":"text","marks":[{"type":"strong"}]},{"text":". Everything else is either shell knowledge or read by the assembler for itself: the threshold from the constant, ","type":"text"},{"text":"N","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":"seen in k/N runs","type":"text","marks":[{"type":"code_inline"}]},{"text":" from counting run files, the stamp from the directory's own name.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"If the remote VERSION fetch succeeds, compare the two as ","type":"text"},{"text":"numbers","type":"text","marks":[{"type":"strong"}]},{"text":" and warn ","type":"text"},{"text":"only when the local one is lower","type":"text","marks":[{"type":"strong"}]},{"text":": print ","type":"text"},{"text":"⚠️ You are not using the latest version. Please upgrade for best security coverage. See https://github.com/pashov/skills","type":"text","marks":[{"type":"code_inline"}]},{"text":". If it fails, skip silently.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Lower, not different.","type":"text","marks":[{"type":"strong"}]},{"text":" A plain \"differs\" test warns the wrong person: somebody working on an unreleased version has a local ","type":"text"},{"text":"VERSION","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"above","type":"text","marks":[{"type":"strong"}]},{"text":" the published one, and gets told to upgrade to the version they are writing. Local equal to remote, or local above it, prints nothing.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Turn 1b — Model and pass count.","type":"text","marks":[{"type":"strong"}]},{"text":" This turn asks ","type":"text"},{"text":"two questions in one ","type":"text","marks":[{"type":"strong"}]},{"text":"AskUserQuestion","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" call","type":"text","marks":[{"type":"strong"}]},{"text":": which model the 12 agents use, and how many passes the scan runs. The runner is interrupted once, before any work starts.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"The two questions do not fail the same way.","type":"text","marks":[{"type":"strong"}]},{"text":" On a runtime without ","type":"text"},{"text":"AskUserQuestion","type":"text","marks":[{"type":"code_inline"}]},{"text":" and an ","type":"text"},{"text":"Agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" tool that takes a ","type":"text"},{"text":"model","type":"text","marks":[{"type":"code_inline"}]},{"text":" parameter — Codex, Gemini, Cursor's native agent — the ","type":"text"},{"text":"model","type":"text","marks":[{"type":"strong"}]},{"text":" question is skipped silently, ","type":"text"},{"text":"{agent_model}","type":"text","marks":[{"type":"code_inline"}]},{"text":" is left unset, and no prose replaces it. The ","type":"text"},{"text":"pass","type":"text","marks":[{"type":"strong"}]},{"text":" question is not skipped: it falls through to the printed block in Turn 1b-ii, which stops and waits. This turn as a whole is never skipped. A later editor must not restore a blanket \"SKIP this turn entirely\" rule: it was true when this turn asked one question, and it is false now.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Turn 1b-i — the ","type":"text","marks":[{"type":"strong"}]},{"text":"AskUserQuestion","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" call (Claude Code).","type":"text","marks":[{"type":"strong"}]},{"text":" Ask both questions in one call. Where the ","type":"text"},{"text":"Agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" tool takes no ","type":"text"},{"text":"model","type":"text","marks":[{"type":"code_inline"}]},{"text":" parameter, ask the pass question alone.","type":"text"}]},{"type":"paragraph","content":[{"text":"Question 1 — model:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read your system prompt to detect your own model ","type":"text"},{"text":"family","type":"text","marks":[{"type":"strong"}]},{"text":" (Opus, Sonnet, or Haiku). Ignore the version digits — the Agent tool's ","type":"text"},{"text":"model","type":"text","marks":[{"type":"code_inline"}]},{"text":" parameter takes the family name (","type":"text"},{"text":"\"opus\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"\"sonnet\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"\"haiku\"","type":"text","marks":[{"type":"code_inline"}]},{"text":"), and the runtime resolves to the latest version in that family.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Put this question in the call:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Question: ","type":"text"},{"text":"\"Which Claude model should the 12 audit agents use?\"","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Three single-select options. Mark the orchestrator's own family as ","type":"text"},{"text":"(Recommended)","type":"text","marks":[{"type":"code_inline"}]},{"text":" and place it first.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"On each option, set the ","type":"text"},{"text":"description","type":"text","marks":[{"type":"code_inline"}]},{"text":" field to ","type":"text"},{"text":"latest","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"On each option, set the ","type":"text"},{"text":"preview","type":"text","marks":[{"type":"code_inline"}]},{"text":" field verbatim (preserve all whitespace exactly — the box widths must stay equal across all three):","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Opus preview:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"┌──────────────────────────────────────────────────────────┐\n│ opus · highest reasoning · most expensive │\n└──────────────────────────────────────────────────────────┘","type":"text"}]},{"type":"paragraph","content":[{"text":"Sonnet preview:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"┌──────────────────────────────────────────────────────────┐\n│ sonnet · balanced reasoning · mid cost │\n└──────────────────────────────────────────────────────────┘","type":"text"}]},{"type":"paragraph","content":[{"text":"Haiku preview:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"┌──────────────────────────────────────────────────────────┐\n│ haiku · lowest reasoning · cheapest │\n└──────────────────────────────────────────────────────────┘","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Store the runner's choice as ","type":"text"},{"text":"{agent_model}","type":"text","marks":[{"type":"code_inline"}]},{"text":". If no answer, default to the orchestrator's own model.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Question 2 — pass count. It goes in the ","type":"text"},{"text":"same call","type":"text","marks":[{"type":"strong"}]},{"text":", second:","type":"text"}]},{"type":"ordered_list","attrs":{"order":4,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Question: ","type":"text"},{"text":"\"How many passes should this audit run? Each pass is a full 12-agent audit, and every pass after the first is told what the earlier ones found, so it hunts new ground. You get one combined report at the end.\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"Three single-select options, ","type":"text"},{"text":"3 passes","type":"text","marks":[{"type":"code_inline"}]},{"text":" first and marked ","type":"text"},{"text":"(Recommended)","type":"text","marks":[{"type":"code_inline"}]},{"text":". Each carries a ","type":"text"},{"text":"preview","type":"text","marks":[{"type":"code_inline"}]},{"text":" box in this turn's style — the boxes are ","type":"text"},{"text":"60 characters wide, equal to the model picker's","type":"text","marks":[{"type":"strong"}]},{"text":", so two questions in one prompt look like one thing. Set ","type":"text"},{"text":"preview","type":"text","marks":[{"type":"code_inline"}]},{"text":" verbatim, whitespace preserved:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Label","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"description","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3 passes (Recommended)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~45 min","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1 pass","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~15 min","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5 passes","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~75 min","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"3 passes preview:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"┌──────────────────────────────────────────────────────────┐\n│ 3 passes · each pass hunts new ground · ~45 min │\n└──────────────────────────────────────────────────────────┘","type":"text"}]},{"type":"paragraph","content":[{"text":"1 pass preview:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"┌──────────────────────────────────────────────────────────┐\n│ 1 pass · today's audit · ~15 min, nothing written │\n└──────────────────────────────────────────────────────────┘","type":"text"}]},{"type":"paragraph","content":[{"text":"5 passes preview:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"┌──────────────────────────────────────────────────────────┐\n│ 5 passes · deepest sweep · ~75 min │\n└──────────────────────────────────────────────────────────┘","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"These are measured, not guessed — and they are a floor.","type":"text","marks":[{"type":"strong"}]},{"text":" A real 3-pass scan of 2,228 lines of Solidity across 10 files, 12 agents per pass on Opus, took ","type":"text"},{"text":"43 minutes","type":"text","marks":[{"type":"strong"}]},{"text":" wall clock: 11 minutes for pass 1 and about 16 for each of passes 2 and 3, which run slower because the growing ","type":"text"},{"text":"known-findings.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" is appended to all twelve bundles. Individual agents ran 3.5–11 minutes. A larger codebase takes longer; a smaller model is faster. Quote minutes rather than multipliers — \"~3x time\" told the runner nothing about whether to wait or come back after lunch. If these numbers are ever re-measured, correct them here rather than adding a second estimate somewhere else.","type":"text"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Any other number needs no option of its own.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"AskUserQuestion","type":"text","marks":[{"type":"code_inline"}]},{"text":" always adds an ","type":"text"},{"text":"Other","type":"text","marks":[{"type":"strong"}]},{"text":" choice with a free-text box, and the runner types their number there. Do NOT add a fourth option reading \"your own number\" — options are fixed choices, so it could not collect the number and would dead-end.","type":"text"}]},{"type":"paragraph","content":[{"text":"Parse the Other answer for the first integer. Below 1 or above 10 → ask once more. A second unusable answer → ","type":"text"},{"text":"1 pass","type":"text","marks":[{"type":"strong"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Store the answer as ","type":"text"},{"text":"{passes}","type":"text","marks":[{"type":"code_inline"}]},{"text":". No answer at all → 1 pass.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Turn 1b-ii — the printed fallback (every runtime without ","type":"text","marks":[{"type":"strong"}]},{"text":"AskUserQuestion","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":").","type":"text","marks":[{"type":"strong"}]},{"text":" Print this exactly:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"How many passes should this audit run?\n\nEach pass is a full 12-agent audit. Every pass after the first is told what the\nearlier passes found, so it hunts new ground. You get one combined report at the end.\n\n 1) 1 pass — today's audit, about 15 minutes. Nothing is written to disk.\n 2) 3 passes — recommended. About 45 minutes.\n 3) 5 passes — deepest sweep. About 75 minutes.\n\nAnswer with 1, 2, 3, or any pass count you want.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"STOP here and wait for the runner's answer.","type":"text","marks":[{"type":"strong"}]},{"text":" Do NOT choose for them. Do NOT continue to Turn 2 with an assumed pass count. Do NOT start the scan and ask later.","type":"text"}]},{"type":"paragraph","content":[{"text":"This is the one place in this skill where a question is emitted as prose. Turn 1b forbids prose questions because the model picker has a safe default — the orchestrator's own model. A pass count has no safe default: 1 and 5 differ by 5x in time and in cost, and that is the runner's money. A later editor must not \"fix\" this by deleting the prose block or by picking a default. If you are reading this and it looks like an inconsistency, it is deliberate.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Answers ","type":"text"},{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"2","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"3","type":"text","marks":[{"type":"code_inline"}]},{"text":" are the three listed choices; any other integer is that many passes. Apply the same bounds as Turn 1b-i step 5 — below 1 or above 10, ask once more, then 1 pass.","type":"text"}]},{"type":"paragraph","content":[{"text":"Turn 1b-iii — when the runner already said.","type":"text","marks":[{"type":"strong"}]},{"text":" Ask nothing that has already been answered:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"What arrived","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"What the picker does","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--loop 5","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Skip the pass question, silently. 5 passes.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--loop","type":"text","marks":[{"type":"code_inline"}]},{"text":" with no number","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Skip the pass question, silently. 3 passes.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"run 4 passes\", \"audit this four times\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Skip the pass question, silently. 4 passes.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"loop mode\", \"run it a few times\" — a request with no number","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ask.","type":"text","marks":[{"type":"strong"}]},{"text":" They asked for the feature, not for a count.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Nothing","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ask.","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Skipping is silent in the first three rows — printing ","type":"text"},{"text":"using 5 passes","type":"text","marks":[{"type":"code_inline"}]},{"text":" back at somebody who just typed ","type":"text"},{"text":"--loop 5","type":"text","marks":[{"type":"code_inline"}]},{"text":" is noise. Skipping the pass question never skips the model question, and the reverse holds too.","type":"text"}]},{"type":"paragraph","content":[{"text":"Turn 1b-iv — record the pass count.","type":"text","marks":[{"type":"strong"}]},{"text":" However ","type":"text"},{"text":"{passes}","type":"text","marks":[{"type":"code_inline"}]},{"text":" was settled — asked, typed in the prose fallback, or read off a flag — write it once, here:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"printf '%s\\t%s\\n' passes_planned \"{passes}\" >> .solidity-auditor/runs/{stamp}/scope.tsv","type":"text"}]},{"type":"paragraph","content":[{"text":"This is the ","type":"text"},{"text":"P","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the Scope table's ","type":"text"},{"text":"Passes","type":"text","marks":[{"type":"code_inline"}]},{"text":" row. The ","type":"text"},{"text":"R","type":"text","marks":[{"type":"code_inline"}]},{"text":" — how many passes actually produced a run file — is counted by the assembler from the run files themselves, so a pass that dies before it can record anything still lowers the count. The row is printed only when ","type":"text"},{"text":"P","type":"text","marks":[{"type":"code_inline"}]},{"text":" is above 1, so writing the key on a 1-pass scan is harmless: ","type":"text"},{"text":"passes_planned","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":" prints no row.","type":"text"}]},{"type":"paragraph","content":[{"text":"Turn 1c — Memory read.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"SKIP this turn entirely when memory is off.","type":"text","marks":[{"type":"strong"}]},{"text":" It is a turn of its own, and not a step of Turn 2, because two of its outcomes stop or downgrade the whole scan — they have to be reached before any expensive work starts.","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Shell check.","type":"text","marks":[{"type":"strong"}]},{"text":" Memory is merged by ","type":"text"},{"text":"awk","type":"text","marks":[{"type":"code_inline"}]},{"text":". Run ","type":"text"},{"text":"command -v awk >/dev/null","type":"text","marks":[{"type":"code_inline"}]},{"text":" once. If it fails, print ","type":"text"},{"text":"memory needs a bash shell — on Windows install Git for Windows","type":"text","marks":[{"type":"code_inline"}]},{"text":", turn memory ","type":"text"},{"text":"off","type":"text","marks":[{"type":"strong"}]},{"text":" for this scan, scan as a plain 1-run scan, and touch no file. There is no second implementation of the merge; the rule that must never break lives in one language only.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stale temporary file.","type":"text","marks":[{"type":"strong"}]},{"text":" If ","type":"text"},{"text":".solidity-auditor/memory.tsv.tmp","type":"text","marks":[{"type":"code_inline"}]},{"text":" exists, a previous scan did not finish. Print ","type":"text"},{"text":"warning: .solidity-auditor/memory.tsv.tmp left by an unfinished scan — overwriting","type":"text","marks":[{"type":"code_inline"}]},{"text":", then carry on. It is overwritten by this scan's merge.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read the ledger.","type":"text","marks":[{"type":"strong"}]},{"text":" If ","type":"text"},{"text":".solidity-auditor/memory.tsv","type":"text","marks":[{"type":"code_inline"}]},{"text":" does not exist, this is a first-ever scan: memory is empty, every finding is ","type":"text"},{"text":"NEW","type":"text","marks":[{"type":"code_inline"}]},{"text":", the file is written at the end. Otherwise read it and ","type":"text"},{"text":"validate it before using it","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"the first line's first tab-separated field is exactly ","type":"text"},{"text":"#solidity-auditor-memory v1","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"every later line has exactly ","type":"text"},{"text":"6","type":"text","marks":[{"type":"strong"}]},{"text":" tab-separated fields","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"On any failure print the path and the problem and ","type":"text"},{"text":"STOP the scan","type":"text","marks":[{"type":"strong"}]},{"text":". No agents, no report, no write. The user fixes or deletes the file. Do NOT start fresh and do NOT continue with a warning — a single bad row must never destroy real memory.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Photocopy it.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"mkdir -p {bundle_dir}","type":"text","marks":[{"type":"code_inline"}]},{"text":" is already done; copy the ledger as it was ","type":"text"},{"text":"before this scan started","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"cp .solidity-auditor/memory.tsv {bundle_dir}/memory-before.tsv 2>/dev/null || : > {bundle_dir}/memory-before.tsv","type":"text"}]},{"type":"paragraph","content":[{"text":"Every merge this scan performs reads this photocopy, never the live file. Create it empty when no ledger exists, so the merge command below needs no special case.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create the scan's row file","type":"text","marks":[{"type":"strong"}]},{"text":", empty: ","type":"text"},{"text":": > {bundle_dir}/scan-rows.tsv","type":"text","marks":[{"type":"code_inline"}]},{"text":". Each run appends its gated rows to it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hold the key list","type":"text","marks":[{"type":"strong"}]},{"text":" — column 1 of every row of the photocopy (after the prune in Turn 2, if one runs).","type":"text"}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"per-function bug-class vocabulary","type":"text","marks":[{"type":"strong"}]},{"text":" — for each ","type":"text"},{"text":"contract|function","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefix, the bug-class labels the ledger already holds — is not held in the orchestrator's head. Turn 2 step 2c writes it to ","type":"text"},{"text":"{bundle_dir}/known-findings.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", where the agents read it inside their bundles and Turn 4 reads it back from disk. It is a file and not a memory because the two readers are a long scan apart, and because the same labels must reach both.","type":"text"}]},{"type":"paragraph","content":[{"text":"The vocabulary is what keeps exact key matching honest: a bug class is a label written in words, so the same bug re-labelled is a second record and memory fails silently. Handing the existing labels back is how the same bug keeps the same key.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Turn 2 — Prepare.","type":"text","marks":[{"type":"strong"}]},{"text":" In one message, make parallel tool calls: (a) Read ","type":"text"},{"text":"{resolved_path}/report-formatting.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", (b) Read ","type":"text"},{"text":"{resolved_path}/judging.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", (c) Read ","type":"text"},{"text":"{resolved_path}/agent-prompts.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", (d) Read ","type":"text"},{"text":"{resolved_path}/report-language.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Why ","type":"text","marks":[{"type":"strong"}]},{"text":"report-formatting.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" is still read, now that nothing here composes a report.","type":"text","marks":[{"type":"strong"}]},{"text":" It is the shape Turn 4 step 5a writes each finding in — title line, location line, Description, diff Fix block — and the assembler pastes those bytes straight through. Read it as the contract the finding blocks meet, not as a template to imitate at the end.","type":"text"}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"report-language.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" is read for the same reason, one level down.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"report-formatting.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" settles the ","type":"text"},{"text":"shape","type":"text","marks":[{"type":"strong"}]},{"text":" of a finding block; ","type":"text"},{"text":"report-language.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" settles the ","type":"text"},{"text":"words inside it","type":"text","marks":[{"type":"strong"}]},{"text":". Turn 4 step 5a writes the title and the Description, the assembler pastes them through unchanged, so those bytes are the last chance to write a sentence a developer can act on. It is Simplified Technical English (ASD-STE100), and it is not optional styling: a finding the developer cannot read is a finding they do not fix. The same file is in every agent bundle, so the sentence the pass writes and the sentence the agent handed it obey one rule.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Then build ","type":"text"},{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", run the memory step, and only then cat the bundles — in that order, because the bundles carry a file the memory step writes:","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Turn 2 is split across the loop.","type":"text","marks":[{"type":"strong"}]},{"text":" Step 1 runs ","type":"text"},{"text":"once per scan","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" provably cannot change between passes — one git SHA for the whole loop, no pruning between them — and it is the expensive half of the build. Step 2c and step 3 run ","type":"text"},{"text":"once per pass","type":"text","marks":[{"type":"strong"}]},{"text":", because only they carry the knowns, and re-catting twelve bundles from files already on disk is one Bash command. Steps 2a and 2b run once per scan with step 1, since both read that frozen source.","type":"text"}]},{"type":"paragraph","content":[{"text":"One ","type":"text","marks":[{"type":"strong"}]},{"text":"{bundle_dir}","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":", reused, everything overwritten.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" is written once and never touched again; ","type":"text"},{"text":"known-findings.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" and the twelve ","type":"text"},{"text":"agent-N-bundle.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" files are overwritten each pass. Disk stays flat whether the runner picked 1 pass or 10 — a directory per pass would hold 12 × N copies of the whole repo. This is safe because Turn 3b is a hard barrier: no pass-K agent is still reading a bundle when pass K+1 overwrites it. The cost, accepted: after the loop you cannot see what pass 2 told its agents. The durable record is the run files and the ledger.","type":"text"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Once per scan.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"{bundle_dir}/source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ALL in-scope ","type":"text"},{"text":".sol","type":"text","marks":[{"type":"code_inline"}]},{"text":" files, each with a ","type":"text"},{"text":"### path","type":"text","marks":[{"type":"code_inline"}]},{"text":" header and fenced code block.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Turn 2 step 2 — Name map, prune and known findings","type":"text","marks":[{"type":"strong"}]},{"text":" (below). SKIPPED whole when memory is off. Parts a and b run once per scan; part c runs ","type":"text"},{"text":"every pass","type":"text","marks":[{"type":"strong"}]},{"text":", because the ledger it reads grows as the loop learns.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Every pass.","type":"text","marks":[{"type":"strong"}]},{"text":" Agent bundles, in a single Bash command using ","type":"text"},{"text":"cat","type":"text","marks":[{"type":"code_inline"}]},{"text":" (not shell variables or heredocs) = ","type":"text"},{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + agent-specific files:","type":"text"}]}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bundle","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Appended files (relative to ","type":"text"},{"text":"{resolved_path}","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent-1-bundle.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"senior-auditor-sop.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/math-precision-agent.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/shared-rules.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent-2-bundle.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"senior-auditor-sop.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/access-control-agent.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/shared-rules.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent-3-bundle.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"senior-auditor-sop.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/economic-security-agent.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/shared-rules.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent-4-bundle.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"senior-auditor-sop.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/execution-trace-agent.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/shared-rules.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent-5-bundle.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"senior-auditor-sop.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/invariant-agent.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/shared-rules.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent-6-bundle.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"senior-auditor-sop.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/periphery-agent.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/shared-rules.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent-7-bundle.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"senior-auditor-sop.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/first-principles-agent.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/shared-rules.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent-8-bundle.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"senior-auditor-sop.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/asymmetry-agent.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/shared-rules.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent-9-bundle.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"senior-auditor-sop.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/boundary-agent.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/shared-rules.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent-10-bundle.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"senior-auditor-sop.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/numerical-gap-agent.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/shared-rules.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent-11-bundle.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"senior-auditor-sop.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/trust-gap-agent.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/shared-rules.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent-12-bundle.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"senior-auditor-sop.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/flow-gap-agent.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"hacking-agents/shared-rules.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"every one of the 12","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"+ ","type":"text","marks":[{"type":"strong"}]},{"text":"report-language.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":", appended after ","type":"text","marks":[{"type":"strong"}]},{"text":"shared-rules.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — unconditional, every mode, every pass. The agent's ","type":"text"},{"text":"description:","type":"text","marks":[{"type":"code_inline"}]},{"text":" is the seed of the report's Description, so the language rule has to reach the writer and not only the editor.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"every one of the 12","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"+ ","type":"text","marks":[{"type":"strong"}]},{"text":"{bundle_dir}/known-findings.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":", appended last","type":"text","marks":[{"type":"strong"}]},{"text":" — only when memory is on ","type":"text"},{"text":"and","type":"text","marks":[{"type":"strong"}]},{"text":" step 2 wrote that file. Never appended on a plain scan, and never appended when the ledger holds no record.","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Each bundle = source.md + SOP + specialty + shared-rules + report-language (+ known findings, when there are any). Agents read the bundle; no Read/Grep needed for the initial scan. Targeted Read/Grep allowed for cross-file investigation.","type":"text"}]},{"type":"paragraph","content":[{"text":"Turn 2 step 2 — Name map, prune and known findings.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"SKIP this step entirely when memory is off.","type":"text","marks":[{"type":"strong"}]},{"text":" It lives in Turn 2 and not in a turn of its own because all three parts read ","type":"text"},{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", which Turn 2 has just built — a separate turn would only carry that file across a boundary. It is numbered ","type":"text"},{"text":"2","type":"text","marks":[{"type":"strong"}]},{"text":", ahead of the bundle cat, because part c writes a file every bundle carries; a pruned record must never reach an agent.","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"lower-alpha"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Build the name map, always","type":"text","marks":[{"type":"strong"}]},{"text":" — once per scan, with step 1 (both the prune and the report need it, and the source it reads is frozen for the whole scan):","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"grep -ohE '(contract|library|interface)[[:space:]]+[A-Za-z0-9_]+|function[[:space:]]+[A-Za-z0-9_]+' {bundle_dir}/source.md \\\n | awk '{ n=$2; k=tolower(n); gsub(/[^a-z0-9]+/, \"-\", k); print k \"\\t\" n }' \\\n | sort -u > {bundle_dir}/source-names.tsv","type":"text"}]},{"type":"paragraph","content":[{"text":"Column 1 is the identifier normalised the same way a key segment is (lower case, every run of non-alphanumeric characters to one hyphen); column 2 is how it is spelled in the source. Normalising ","type":"text"},{"text":"both sides the same way","type":"text","marks":[{"type":"strong"}]},{"text":" is what makes the comparison exact — a key segment can never be matched against raw source text, because ","type":"text"},{"text":"withdrawAll","type":"text","marks":[{"type":"code_inline"}]},{"text":" is stored as ","type":"text"},{"text":"withdrawall","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"ordered_list","attrs":{"order":2,"listStyle":"lower-alpha"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prune — once per scan, and only after a whole-repo scan.","type":"text","marks":[{"type":"strong"}]},{"text":" Run it ","type":"text"},{"text":"only","type":"text","marks":[{"type":"strong"}]},{"text":" in default mode. Never on a named-file scan (a file that was not scanned proves nothing about the records it holds), and never a second time inside one scan — the loop's later passes read the same frozen source, so there is nothing new for a second prune to learn.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The prune edits the ","type":"text"},{"text":"photocopy","type":"text","marks":[{"type":"strong"}]},{"text":", not the live ledger. The live file inherits the prune when the first merge writes it. Pruning the live file instead would be undone by the next rebuild.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"awk -F'\\t' -v OFS='\\t' '\nFILENAME==ARGV[1] { names[$1]=1; next }\nFNR==1 { print; next }\n{\n split($1, p, \"|\")\n keep = (p[1] in names) && (p[2] in names)\n if (p[2]==\"constructor\" || p[2]==\"receive\" || p[2]==\"fallback\") keep = (p[1] in names)\n if (keep) print; else print \"pruned: \" $6 \"\\t\" $1 \"\\t\" $5 > \"/dev/stderr\"\n}\n' {bundle_dir}/source-names.tsv {bundle_dir}/memory-before.tsv > {bundle_dir}/memory-before.tsv.tmp \\\n && mv {bundle_dir}/memory-before.tsv.tmp {bundle_dir}/memory-before.tsv","type":"text"}]},{"type":"paragraph","content":[{"text":"A record is dropped when its contract or its function can no longer be found in the assembled source. ","type":"text"},{"text":"constructor","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"receive","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"fallback","type":"text","marks":[{"type":"code_inline"}]},{"text":" are never written as ","type":"text"},{"text":"function \u003cname>","type":"text","marks":[{"type":"code_inline"}]},{"text":", so a record on one of them is kept whenever its contract survives.","type":"text"}]},{"type":"paragraph","content":[{"text":"The prune deletes Leads as well as findings.","type":"text","marks":[{"type":"strong"}]},{"text":" A Lead is a full ledger record, so the same rule reaches it — and a pruned Lead leaves no trace anywhere else in the output. That is why the command prints every dropped row to the runner: ","type":"text"},{"text":"kind","type":"text","marks":[{"type":"code_inline"}]},{"text":", key and title, one line each. Print them under ","type":"text"},{"text":"Pruned N records (contract or function no longer in scope):","type":"text","marks":[{"type":"code_inline"}]},{"text":". Never drop a record silently.","type":"text"}]},{"type":"paragraph","content":[{"text":"After the prune, re-read the photocopy for the key list of Turn 1c step 6. A pruned record must not reach the agents, the merge, or the \"Known from earlier scans\" section.","type":"text"}]},{"type":"ordered_list","attrs":{"order":3,"listStyle":"lower-alpha"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Write ","type":"text","marks":[{"type":"strong"}]},{"text":"mem_before","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":", then build ","type":"text","marks":[{"type":"strong"}]},{"text":"{bundle_dir}/known-findings.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":".","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"paragraph","content":[{"text":"mem_before","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" first","type":"text","marks":[{"type":"strong"}]},{"text":" — the row count of the photocopy ","type":"text"},{"text":"as the merge will read it","type":"text","marks":[{"type":"strong"}]},{"text":", which is the count ","type":"text"},{"text":"after","type":"text","marks":[{"type":"em"}]},{"text":" the prune. It is written here and nowhere else, and it is written in every mode memory is on, whether or not part b's prune ran (it does not run on a named-file scan):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"printf '%s\\t%s\\n' mem_before \"$(( $(wc -l \u003c {bundle_dir}/memory-before.tsv) - 1 ))\" \\\n >> .solidity-auditor/runs/{stamp}/scope.tsv","type":"text"}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"- 1","type":"text","marks":[{"type":"code_inline"}]},{"text":" drops the header line. An empty photocopy has no header and would give ","type":"text"},{"text":"-1","type":"text","marks":[{"type":"code_inline"}]},{"text":"; write ","type":"text"},{"text":"0","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead. The ","type":"text"},{"text":"pre","type":"text","marks":[{"type":"strong"}]},{"text":"-prune number would make the ","type":"text"},{"text":"Memory","type":"text","marks":[{"type":"code_inline"}]},{"text":" row's own arithmetic wrong — ","type":"text"},{"text":"12 records before this scan · 14 after","type":"text","marks":[{"type":"code_inline"}]},{"text":" has to describe the same set the merge worked on.","type":"text"}]},{"type":"paragraph","content":[{"text":"This key is the memory flag the assembler reads.","type":"text","marks":[{"type":"strong"}]},{"text":" Its presence is what makes the ","type":"text"},{"text":"Memory","type":"text","marks":[{"type":"code_inline"}]},{"text":" row appear and the \"Known from earlier scans\" section exist — the presence of ","type":"text"},{"text":"memory-before.tsv","type":"text","marks":[{"type":"code_inline"}]},{"text":" is not. Nothing writes a ","type":"text"},{"text":"mem_","type":"text","marks":[{"type":"code_inline"}]},{"text":" key when memory is off, and this whole step is skipped when it is, so the flag cannot get out of step with the scan.","type":"text"}]},{"type":"paragraph","content":[{"text":"It is written before the early exit below, so an empty ledger still gets a ","type":"text"},{"text":"Memory","type":"text","marks":[{"type":"code_inline"}]},{"text":" row reading ","type":"text"},{"text":"0 records before this scan","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Then ","type":"text","marks":[{"type":"strong"}]},{"text":"{bundle_dir}/known-findings.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — the ledger as the agents and Turn 4 read it. It is built from the ","type":"text"},{"text":"pruned","type":"text","marks":[{"type":"strong"}]},{"text":" photocopy and from ","type":"text"},{"text":"source-names.tsv","type":"text","marks":[{"type":"code_inline"}]},{"text":", so it never names a record the prune has just dropped and never prints a normalised key at a human.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"awk -F'\\t' '\nFILENAME==ARGV[1] { nm[$1]=$2; next }\nFNR==1 { next }\n{\n split($1, p, \"|\")\n c = (p[1] in nm) ? nm[p[1]] : p[1]\n f = (p[2] in nm) ? nm[p[2]] : p[2]\n h = c \".\" f\n if (!(h in seen)) { seen[h]=1; ord[++n]=h }\n body[h] = body[h] \"- `\" p[3] \"` — \" $6 \", seen in \" $3 ($3==1 ? \" scan\" : \" scans\") \" — \" $5 \"\\n\"\n}\nEND { for (i=1; i\u003c=n; i++) printf \"## %s\\n\\n%s\\n\", ord[i], body[ord[i]] }\n' {bundle_dir}/source-names.tsv {bundle_dir}/memory-before.tsv > {bundle_dir}/known-findings.body.md","type":"text"}]},{"type":"paragraph","content":[{"text":"If that file is empty, stop here","type":"text","marks":[{"type":"strong"}]},{"text":" — delete it, write no ","type":"text"},{"text":"known-findings.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", and append nothing to the bundles. An empty ledger (a first-ever scan, or a scan whose every record the prune removed) must not hand twelve agents an empty heading.","type":"text"}]},{"type":"paragraph","content":[{"text":"Otherwise write ","type":"text"},{"text":"{bundle_dir}/known-findings.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" as this exact prose followed by the body, unchanged:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"markdown"},"content":[{"text":"# Known findings — ground already walked\n\nEarlier scans of this repository recorded the findings below, grouped by the contract and\nfunction they sit in. Each line is `bug class` — kind, scans, title.\n\n**They are not false positives, and they are not off limits.** Put your **effort** into new\nground: functions, flows and mechanisms this list does not name. That is a rule about where\nyour reading time goes. It is **not** a rule about what you report.\n\n**Report every bug you find in full, listed or not.** A listed bug you reach again is a bug\nthat is still there, and the report has to say so. Write it up exactly as you would write up\nanything new — the same path, proof and fix — whether you reached it by the mechanism it is\nlisted with or by a different one. Silence is read as \"nobody found this\": a finding no agent\nraises drops out of the report into a table of records nobody re-checked, so a repository\nscanned twice would show fewer bugs than the same repository scanned once.\n\n**Reuse the bug-class label.** The backticked label on each line is the word this repository\nalready uses for that class of bug in that function. When you report a finding or a lead\nwhose bug class is one of the classes listed for that same contract and function, write\n**that exact label**. Invent a new label only when none of them is the same class of bug.\nMemory matches these labels as plain text, so the same bug under a new word is remembered\ntwice and recognised never.\n\n\u003cbody — one `## Contract.function` section per function, as generated above>","type":"text"}]},{"type":"paragraph","content":[{"text":"The label-reuse rule is written here, once, for ","type":"text"},{"text":"both","type":"text","marks":[{"type":"strong"}]},{"text":" readers of this file: the 12 agents, who write the bug class in a finding, and Turn 4 step 4, which writes it into a key.","type":"text"}]},{"type":"paragraph","content":[{"text":"The builder takes any 6-column ledger file.","type":"text","marks":[{"type":"strong"}]},{"text":" On a single-run scan that file is the pruned photocopy, as above. A multi-pass scan rebuilds ","type":"text"},{"text":"known-findings.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" from the freshly merged ","type":"text"},{"text":".solidity-auditor/memory.tsv","type":"text","marks":[{"type":"code_inline"}]},{"text":" after every pass, before it re-cats the bundles, so the agents of pass K+1 read what pass K found as ground already walked. That is the whole point of passing memory down a loop; the command does not change, only the file it is pointed at.","type":"text"}]},{"type":"paragraph","content":[{"text":"Print line counts for every bundle and ","type":"text"},{"text":"source.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". Do NOT inline source code into the Agent call prompt itself.","type":"text"}]},{"type":"paragraph","content":[{"text":"Turn 3a — Spawn all 12 agents.","type":"text","marks":[{"type":"strong"}]},{"text":" Runs ","type":"text"},{"text":"every pass","type":"text","marks":[{"type":"strong"}]},{"text":". In one message, spawn all 12 agents as ","type":"text"},{"text":"parallel BACKGROUND Agent calls","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"run_in_background=true","type":"text","marks":[{"type":"code_inline"}]},{"text":"). If Turn 1b set ","type":"text"},{"text":"{agent_model}","type":"text","marks":[{"type":"code_inline"}]},{"text":", pass ","type":"text"},{"text":"model={agent_model}","type":"text","marks":[{"type":"code_inline"}]},{"text":" on every Agent call. If ","type":"text"},{"text":"{agent_model}","type":"text","marks":[{"type":"code_inline"}]},{"text":" is unset (Turn 1b skipped — Codex, Gemini, others), omit the ","type":"text"},{"text":"model","type":"text","marks":[{"type":"code_inline"}]},{"text":" parameter entirely — do NOT substitute any default. The orchestrator will receive a notification when each agent completes — do NOT poll or sleep. Single phase, no later spawns. Proceed to Turn 3b only after all 12 have notified completion.","type":"text"}]},{"type":"paragraph","content":[{"text":"Agents 1–9 use the ","type":"text"},{"text":"single-specialty prompt","type":"text","marks":[{"type":"strong"}]},{"text":" (Turn 3a-i). Agents 10–12 use the ","type":"text"},{"text":"gap-hunter prompt","type":"text","marks":[{"type":"strong"}]},{"text":" (Turn 3a-ii).","type":"text"}]},{"type":"paragraph","content":[{"text":"Turn 3a-i — Single-specialty prompt (agents 1–9).","type":"text","marks":[{"type":"strong"}]},{"text":" Use the template under \"Single-specialty prompt\" in ","type":"text"},{"text":"{resolved_path}/agent-prompts.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", substituting ","type":"text"},{"text":"{bundle_dir}","type":"text","marks":[{"type":"code_inline"}]},{"text":", the agent number and the bundle line count.","type":"text"}]},{"type":"paragraph","content":[{"text":"Turn 3a-ii — Gap-hunter prompt (agents 10–12).","type":"text","marks":[{"type":"strong"}]},{"text":" Use the template under \"Gap-hunter prompt\" in the same file.","type":"text"}]},{"type":"paragraph","content":[{"text":"Two rules that file carries, repeated here because they are conditions and not text:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"\"Known findings\"","type":"text","marks":[{"type":"strong"}]},{"text":" paragraph is included ","type":"text"},{"text":"only when memory is on and ","type":"text","marks":[{"type":"strong"}]},{"text":"known-findings.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" was appended","type":"text","marks":[{"type":"strong"}]},{"text":". On a plain scan the prompt is byte-identical to the one it has always been — a paragraph about a section that is not there would send agents hunting for it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"READ-ONLY","type":"text","marks":[{"type":"strong"}]},{"text":" paragraph is ","type":"text"},{"text":"unconditional","type":"text","marks":[{"type":"strong"}]},{"text":" — every agent, every mode, every pass. A real scan proved it necessary: an agent built Foundry proof-of-concept files inside the audited repository and deleted them afterwards. It left the tree clean and the stored SHA honest, and it was still wrong. A later editor must not make it conditional, and must not soften it into a preference.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Turn 3b — Wait for all 12 agents to complete.","type":"text","marks":[{"type":"strong"}]},{"text":" Runs ","type":"text"},{"text":"every pass","type":"text","marks":[{"type":"strong"}]},{"text":". Once every one of the 12 spawned agents has notified completion, proceed to Turn 4. Do NOT proceed to dedup until every agent has finished — let them run to natural completion. Do NOT poll or sleep; act only on completion notifications.","type":"text"}]},{"type":"paragraph","content":[{"text":"While you wait, on the first pass only, Read ","type":"text","marks":[{"type":"strong"}]},{"text":"{resolved_path}/dedup-and-assembly.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":".","type":"text","marks":[{"type":"strong"}]},{"text":" It holds the whole of Turn 4 and Turn 5. This turn is the one point in the scan where the orchestrator has nothing else to do, so the read costs no wall-clock; and having it in hand before Turn 4 starts is what keeps Turn 4 from improvising. Later passes already hold it.","type":"text"}]},{"type":"paragraph","content":[{"text":"When an agent dies.","type":"text","marks":[{"type":"strong"}]},{"text":" Continue the pass with the eleven that came back. ","type":"text"},{"text":"Never respawn it, in any mode.","type":"text","marks":[{"type":"strong"}]},{"text":" A retry costs an unbounded wait for one twelfth of the coverage, and a loop covers it for free — the next pass runs the same twelve specialties again, knowing what this one found. Record the loss in all three places, or it is a silent coverage loss: the pass summary line (Turn 4 step 5), the ","type":"text"},{"text":"run-K.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" header, and the report's ","type":"text"},{"text":"Passes","type":"text","marks":[{"type":"code_inline"}]},{"text":" row.","type":"text"}]},{"type":"paragraph","content":[{"text":"When a whole pass produces nothing","type":"text","marks":[{"type":"strong"}]},{"text":" — the bundle build failed, or all twelve died:","type":"text"}]},{"type":"paragraph","content":[{"text":"Record the failure before doing either.","type":"text","marks":[{"type":"strong"}]},{"text":" A pass that produces nothing writes no run file, so nothing else on disk knows it was ever planned:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"printf '%s\\t%s\\n' pass_{K}_failed 1 >> .solidity-auditor/runs/{stamp}/scope.tsv","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pass 1 produces nothing:","type":"text","marks":[{"type":"strong"}]},{"text":" this is not a loop failure, it is today's failure. No ledger write. Go to Turn 5, which assembles a report with no findings in it — ","type":"text"},{"text":"_None — this scan raised no findings._","type":"text","marks":[{"type":"code_inline"}]},{"text":" — rather than printing nothing at all. The ","type":"text"},{"text":"Passes","type":"text","marks":[{"type":"code_inline"}]},{"text":" row is suppressed on a 1-pass scan, so the assembler says it in a ","type":"text"},{"text":"Run files","type":"text","marks":[{"type":"strong"}]},{"text":" row instead — ","type":"text"},{"text":"⚠️ No pass produced a run file. This scan reviewed nothing.","type":"text","marks":[{"type":"code_inline"}]},{"text":" The runs directory stays; it holds the ","type":"text"},{"text":"scope.tsv","type":"text","marks":[{"type":"code_inline"}]},{"text":" that says what was attempted.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A later pass produces nothing:","type":"text","marks":[{"type":"strong"}]},{"text":" stop the loop and go straight to Turn 5. The report is assembled from the passes that finished and its ","type":"text"},{"text":"Passes","type":"text","marks":[{"type":"code_inline"}]},{"text":" row says which one failed. Nothing is lost — the ledger was rebuilt after every pass. Grinding on to the next pass after the machinery has broken burns the runner's money.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Turn 4 — Deduplicate, validate & record.","type":"text","marks":[{"type":"strong"}]},{"text":" Runs ","type":"text"},{"text":"every pass","type":"text","marks":[{"type":"strong"}]},{"text":", and it is the end of the loop body: deduplicate this pass's agent results, gate-evaluate, tag, record the pass, write the ledger. ","type":"text"},{"text":"It prints no report","type":"text","marks":[{"type":"strong"}]},{"text":" — there is exactly one report per scan and Turn 5 prints it. Do NOT print an intermediate dedup list.","type":"text"}]},{"type":"paragraph","content":[{"text":"Steps 4 and 6 are SKIPPED entirely when memory is off.","type":"text","marks":[{"type":"strong"}]},{"text":" Every other step runs at any pass count.","type":"text"}]},{"type":"paragraph","content":[{"text":"Follow ","type":"text"},{"text":"{resolved_path}/dedup-and-assembly.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", section ","type":"text"},{"text":"\"Turn 4\"","type":"text","marks":[{"type":"strong"}]},{"text":", step by step.","type":"text"}]},{"type":"paragraph","content":[{"text":"Then, after the loop body has run ","type":"text"},{"text":"{passes}","type":"text","marks":[{"type":"code_inline"}]},{"text":" times (or stopped early), go to Turn 5 once.","type":"text"}]},{"type":"paragraph","content":[{"text":"Turn 5 — Assemble, print, clean.","type":"text","marks":[{"type":"strong"}]},{"text":" Runs ","type":"text"},{"text":"once per scan","type":"text","marks":[{"type":"strong"}]},{"text":", at any pass count, including a 1-pass scan and including a loop that stopped early. There is no path where a scan ends without this turn.","type":"text"}]},{"type":"paragraph","content":[{"text":"This turn holds no model judgment.","type":"text","marks":[{"type":"strong"}]},{"text":" It copies two files, runs ","type":"text"},{"text":"assemble.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":", counts with ","type":"text"},{"text":"awk","type":"text","marks":[{"type":"code_inline"}]},{"text":", and prints. ","type":"text"},{"text":"assemble.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" is the only producer of a report in this skill: do not re-word its output, do not re-order it, do not add to it and do not summarise it in your own words. If this turn looks too thin to be a reporting step — that is the point, and ","type":"text"},{"text":"dedup-and-assembly.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" records why.","type":"text"}]},{"type":"paragraph","content":[{"text":"Follow ","type":"text"},{"text":"{resolved_path}/dedup-and-assembly.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", section ","type":"text"},{"text":"\"Turn 5\"","type":"text","marks":[{"type":"strong"}]},{"text":", step by step. Its step 1 is SKIPPED when memory is off; its step 4 runs only when ","type":"text"},{"text":"--file-output","type":"text","marks":[{"type":"code_inline"}]},{"text":" was passed.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Banner","type":"text"}]},{"type":"paragraph","content":[{"text":"Before doing anything else, print this exactly:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"\n██████╗ █████╗ ███████╗██╗ ██╗ ██████╗ ██╗ ██╗ ███████╗██╗ ██╗██╗██╗ ██╗ ███████╗\n██╔══██╗██╔══██╗██╔════╝██║ ██║██╔═══██╗██║ ██║ ██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝\n██████╔╝███████║███████╗███████║██║ ██║██║ ██║ ███████╗█████╔╝ ██║██║ ██║ ███████╗\n██╔═══╝ ██╔══██║╚════██║██╔══██║██║ ██║╚██╗ ██╔╝ ╚════██║██╔═██╗ ██║██║ ██║ ╚════██║\n██║ ██║ ██║███████║██║ ██║╚██████╔╝ ╚████╔╝ ███████║██║ ██╗██║███████╗███████╗███████║\n╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝\n","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"solidity-auditor","author":"@skillopedia","source":{"stars":1297,"repo_name":"skills","origin_url":"https://github.com/pashov/skills/blob/HEAD/solidity-auditor/SKILL.md","repo_owner":"pashov","body_sha256":"4e3fa4b23b62fa5e32cbc2ec041ac3143ddf89f721434c59e2c10da06ad637a3","cluster_key":"30ffcd90d23ea3fbead211916230a99f261e736c3f854380fc257f10927ae341","clean_bundle":{"format":"clean-skill-bundle-v1","source":"pashov/skills/solidity-auditor/SKILL.md","text_bytes":143762,"source_commit":"eacfc09b365d2697b83355909d0300607e596ffc","skills_sh_rank":14323,"attachment_count":22,"text_attachments":22,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"solidity-auditor/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"security","import_tag":"skills-sh-top20k-2026-11","description":"Security audit of Solidity code while you develop. Trigger on \"audit\", \"check this contract\", \"review for security\", \"loop mode\", \"run the auditor in loop mode\", \"run 3 passes\". Modes - default (full repo) or a specific filename. Loop mode runs several passes in one scan, each pass told what the earlier ones found, and ends in one combined report; it remembers findings between scans in a ledger."}},"renderedAt":1793671428921}

Smart Contract Security Audit You are the orchestrator of a parallelized smart contract security audit. Mode Selection Exclude pattern: skip dependency and build directories — , , , , , , , — and non-production code — , , and files matching , or . Deploy scripts stay in scope. , and are audited like any other code: a deploy script sets constructor arguments, hands over ownership and seeds state, and it carries real bugs. Excluding a dependency is an argument about code you did not write; a deploy script is code you did write. - Default (no arguments): scan all files using the exclude pattern.…