create-agent-skill-fallback

Create an Agent SKILL.md fallback for an existing find-XXXX finder that relies on a fragile discovery foundation — above all LLM_DECOMPILE (patterns C/D/E), which matches the decompiled shape of a pre

SKILL.md

GitHub

Create an Agent SKILL.md Fallback for a Finder

Given a target find-XXXX finder whose preprocessor uses a fragile foundation (most often LLM_DECOMPILE), author .claude/skills/find-XXXX/SKILL.md — the Agent fallback that ida_analyze_bin.py runs only when the preprocessor returns failure. The preprocessor stays as-is; this skill adds a durable backstop beside it.

This is the robustness-oriented sibling of /convert-finder-skill-to-preprocessor-scripts (that skill turns a SKILL.md into a preprocessor; this one gives a preprocessor a SKILL.md fallback).

When to Use

  • A find-XXXX finder failed on a new game version because a member/vfunc/function was inlined or de-inlined relative to what its LLM_DECOMPILE reference expects (classic symptom: one target in a multi-target -decompiles skill can no longer be found, aborting the module).
  • You want to pre-emptively backstop an LLM_DECOMPILE-based finder (patterns C/D/E) whose correctness hinges on a predecessor keeping a fixed decompiled shape.
  • The recipe also applies to xref-string / found_call / index-based finders — the foundation differs, but the same "self-contained, skip-existing, anchor semantically, follow the callee" method holds.

Do not use this to replace a working preprocessor. The fallback is a safety net; the preprocessor remains the fast path.

The one constraint that drives the whole design

When the preprocessor fails, agent_runner.run_skill launches the agent with a prompt of only /{skill_name} (see _build_claude_command, profile sig-finder). The skill's expected_yaml_paths is used only for post-run missing-file verification (_missing_expected_outputs / _result_failure_reason) — it is never injected into the prompt, and the missing list is not fed back to the agent on retry.

Consequence: the fallback SKILL.md you generate MUST be fully self-contained. It must enumerate every output, gate each by platform, and tell the agent to skip outputs whose YAML already exists (the preprocessor may have written most of them before failing; earlier fallback skills may have written others).

Inputs to gather (read these before writing anything)

For target finder find-XXXX in module <module> (server, engine, networksystem, …):

  1. Preprocessor ida_preprocessor_scripts/find-XXXX.py — the source of truth for what to find:
    • TARGET_FUNCTION_NAMES, TARGET_STRUCT_MEMBER_NAMES, TARGET_GLOBALVAR_NAMES and any *_WINDOWS / *_LINUX variants → the output symbols and their platform gating.
    • LLM_DECOMPILE (and _WINDOWS/_LINUX) → the predecessor reference each target is mined from (references/<module>/<predecessor>.{platform}.yaml).
    • FUNC_VTABLE_RELATIONS → which targets are vtable-related (vtable_name).
    • GENERATE_YAML_DESIRED_FIELDS → the exact fields and kind for each target (this tells you whether a target is a struct member, an indirect-vcall vfunc, a real vfunc, a regular func, or a global var — see the kind table below).
    • any FUNC_XREFS (string/gv anchors) — extra fingerprints you can reuse.
  2. configs/<GAMEVER>.yaml skill entry — expected_output / expected_output_windows / expected_output_linux (authoritative output list per platform), expected_input (the predecessor YAML), platform, prerequisite.
  3. Reference YAMLs ida_preprocessor_scripts/references/<module>/<predecessor>.{platform}.yaml — the disasm_code + procedure carry the annotations ; 0xNN = Class::member and ; 0xNN = Class::vfunc / // NNNN = 0xNN = … at each access/call site. These annotations are the semantic fingerprints you translate into the fallback's anchors. Also collect any real-world helper or alternate inline/de-inline reference YAML that materially helps locate the targets.
  4. Source-owned output YAMLs bin_artifacts/<gamever>/<module>/<target>.{platform}.yaml — the authoritative offsets, vfunc indices, and signature styles the finder currently produces. Mine these for the reference values in the inventory table. Resolve an exact configured <gamever>; these files are tracked Git truth. Document their values as update- and platform-specific references, never as fixed answers.

Cross-check every value across the reference annotation AND the ground-truth YAML; where they disagree, trust the ground-truth YAML and note the discrepancy (references occasionally mis-annotate — see the decoy caution).

Workflow

Step 1 — Build the output inventory

From the preprocessor .py + config entry, list every output as (symbol, kind, platform, predecessor, desired-fields). Determine kind from GENERATE_YAML_DESIRED_FIELDS using this table:

Kind Tell-tale desired fields Sig-gen skill Writer skill
struct member struct_name, member_name, offset, offset_sig[, size, offset_sig_disp] /generate-signature-for-structoffset /write-structoffset-as-yaml
indirect vcall (call [reg+disp], no body) vfunc_sig, vfunc_offset, vfunc_index, vtable_name and no func_va/func_sig /generate-signature-for-vfuncoffset /write-vfunc-as-yaml (func_addr=None, func_sig=None)
real vtable vfunc (has a body) func_va/func_sig and vtable_name/vfunc_offset/vfunc_index /generate-signature-for-function /write-vfunc-as-yaml (+ vtable fields)
regular function func_name, func_sig, func_va, func_rva, func_size (no vtable fields) /generate-signature-for-function /write-func-as-yaml
global variable gv_name, gv_va, gv_sig, gv_inst_* /generate-signature-for-globalvar /write-globalvar-as-yaml

The indirect-vcall kind is easy to miss: its YAML has no func_va and its vfunc_sig is the signature of the call instruction itself (e.g. FF 90 A0 00 00 00 = call qword ptr [rax+0A0h]), not of any target function body. Treat it as such — do not try to resolve a concrete implementation address.

Step 2 — Confirm platform gating and the predecessor

From the config entry, record which outputs are cross-platform vs expected_output_windows / expected_output_linux, and the predecessor(s) from expected_input. A symbol that is de-inlined into a separate function on one platform but inlined on the other is common (that asymmetry is often why the finder is fragile).

Step 3 — Extract per-target fingerprints

For each target, read its access/call site in the reference disasm_code + procedure and write down:

  • the semantic anchor: the nearest stable landmark — a string literal, a magic constant, a named global/interface call, a distinctive helper — that identifies the site independent of address;
  • the this-relative offset (members) or call displacement (indirect vcalls) or vtable index;
  • the reference value from the source-owned bin_artifacts/ YAML (both platforms where they differ).

Step 4 — Write the fallback SKILL.md

Create .claude/skills/find-XXXX/SKILL.md from the template below. Fill every placeholder; keep only the target kinds that actually occur. The output filename MUST equal the finder's skill name so agent_runner.run_skill finds it.

The generated fallback MUST contain ## Realworld Function References near the top, before the background. List one exact repo-relative YAML path per bullet for every platform-relevant predecessor and useful inline/de-inline helper or variant. Spell out .windows.yaml and .linux.yaml paths separately; do not use {platform} shorthand in this section, because the agent must be able to open each reference directly. State that addresses and offsets are reference-build values that still require verification against the current binary.

Step 5 — Validate

  • uv run python -m unittest discover -s tests -b (guard; a pure-doc addition should not affect tests).

  • Re-read the generated SKILL.md against the inventory: is every output listed, platform-gated, and mapped to a sig-gen + writer skill with correct params?

  • Run the fallback itself in the real IDA/MCP environment, bypassing both old-version reuse and preprocessing:

    uv run ida_analyze_bin.py -gamever <gamever> -oldgamever none -modules=<module> -debug -skip_pp -skill=<skill_name>
    

    The selected game version must contain the module binary and every configured expected_input. Seed a checkout-external scratch artifact root from bin_artifacts/<gamever>/, remove the target outputs only in that scratch copy, and pass it with -artifactdir (plus tracked bin_artifacts as -oldartifactdir). Never delete or rewrite tracked expected artifacts merely to force the test. A run that says all outputs already exist and skips the skill is not a valid Agent-Skill-only test.

  • Require the log to show Agent Skill only mode: enabled (-skip_pp), Skipping preprocess: <skill_name> (-skip_pp), and Starting agent skill: <skill_name>, followed by Success and summary Failed: 0. Verify that every expected YAML for the tested platform was produced and parses as a non-empty mapping.

Do not report the fallback as complete unless this end-to-end Agent-Skill-only test passes. If the real IDA/MCP environment is unavailable, report the validation as blocked rather than substituting a preprocessor run or a ground-truth-only review.

Step 6 — Commit Changes to dev

After the end-to-end fallback test has produced and verified its YAMLs, copy the validated canonical target outputs from the isolated root into bin_artifacts/<gamever>/<module>/, include their computed downstream closure, and run the repository artifact contract. Record the required one-line memory pointer per the repo workflow. Ensure the delivery branch is dev; never commit directly to main. If the local dev branch exists, switch to it. Otherwise, switch to main first and create dev from main:

if git show-ref --verify --quiet refs/heads/dev; then
  git switch dev
else
  git switch main
  git switch -c dev
fi

If any branch switch fails, stop and report the error. Review git status --short, then explicitly stage the new fallback skill, source-owned artifact closure, and that memory update; never use git add -A:

git add -- .claude/skills/find-XXXX/SKILL.md bin_artifacts/<gamever>/<module>/<target-yamls> <memory-pointer-path>
git diff --cached --name-only

Stop if the staged-path list contains anything unrelated to this task. Commit only the staged task changes using the repository commit format:

git commit -m "feat(skills): add find-XXXX fallback" -m "Co-Authored-By: Codex <codex@openai.com>"

Do not call /create-pr, push the branch, or open a pull request unless the user separately requests it. Finish by reporting the commit hash, tested game version, unittest result, and Agent-Skill-only result.


Fallback SKILL.md template

Fill placeholders <...>; drop sections for kinds that do not apply.

---
name: find-XXXX
description: |
  Final-guarantee fallback for the find-XXXX preprocessor. Recovers <one-line summary of the targets> in CS2
  <module binaries> by decompiling <PREDECESSOR> and following de-inlined callees when a target is no longer
  accessed directly. Use when the deterministic/LLM preprocessor (ida_preprocessor_scripts/find-XXXX.py) could
  not resolve every target because a symbol was inlined or de-inlined in a way the LLM_DECOMPILE references do
  not cover.
  Trigger: <every target symbol, comma-separated>
disable-model-invocation: true
---

# Find XXXX (final-guarantee fallback)

Recover every symbol the find-XXXX preprocessor produces, in CS2 `<binary.dll>` / `<libbinary.so>`, using IDA
Pro MCP tools. This is the Agent fallback: it runs only when the preprocessor returned failure — which almost
always means a target's access pattern **moved** across the inline/de-inline boundary.

## Realworld Function References

Read the platform-relevant real-world YAMLs before searching in IDA. Treat their addresses and offsets as
reference-build values only; verify every result against the current binary.

- `ida_preprocessor_scripts/references/<module>/<predecessor>.windows.yaml`
- `ida_preprocessor_scripts/references/<module>/<predecessor>.linux.yaml`
- `ida_preprocessor_scripts/references/<module>/<relevant-helper-or-variant>.windows.yaml`
- `ida_preprocessor_scripts/references/<module>/<relevant-helper-or-variant>.linux.yaml`

Spell out each existing path literally and drop non-applicable placeholders or platforms.

## Background — <PREDECESSOR> and what it wires up

<2–5 sentences: what the predecessor does and which targets it touches, in source terms. Note that all member
accesses are relative to `this` (arg1: rcx/rsi on Windows, rdi/rbx on Linux) — the key to robustness.>

## Robustness principle — follow the de-inline boundary

For every target: (1) look for its access pattern inside `<PREDECESSOR>`; (2) if absent, it was de-inlined —
enumerate the functions `<PREDECESSOR>` calls, decompile the plausible ones, and search there (the helper
receives `this` as its first argument, so the same `this + offset` reappears; recurse a level or two);
(3) conversely a target the reference expected in a separate function may have been inlined back into
`<PREDECESSOR>`. Anchor each target by its semantic fingerprint (string / constant / neighboring call), never by
a fixed address or containing function.

## Output inventory

`struct_name` is `<STRUCT>` where applicable. Offsets/indices are **reference values from build <gamever> —
verify against the binary, do not assume**.

| # | Output symbol | Kind | Windows | Linux | Writer skill |
|---|---------------|------|---------|-------|--------------|
| 1 | `<symbol>` | <kind> | `<value or "inlined — skip">` | `<value or "inlined — skip">` | `/write-...-as-yaml` |
| … | | | | | |

Platform gating: <list cross-platform vs windows-only / linux-only outputs>.

## Step 0. Skip targets already produced

For each output, if `<name>.<platform>.yaml` already exists in the active artifact module directory and parses to a
non-empty mapping, skip it — the preprocessor or an earlier fallback wrote it. Use the analyzer-reported artifact
module directory; never derive the artifact path from the binary directory.

`/get-func-from-yaml` also reports existence for functions/vfuncs.

## Step 1. Load and decompile the predecessor

**ALWAYS** Use SKILL `/get-func-from-yaml` with `func_name=<PREDECESSOR>` to get its `func_va`. If it errors,
**STOP** and report to user. Then:

```
mcp__ida-pro-mcp__decompile addr="<PREDECESSOR.func_va>"
```

Note the `this` register and keep the list of called functions for the de-inline search.

## Step 2…N. Resolve each target

<One subsection per target (or per cluster sharing a call site). For each: the semantic anchor, the
this-relative offset / call displacement / vtable index, the reference value, and how to handle de-inline. Note
any decoys.>

## Signatures and YAML output

<Per kind, the sig-gen skill + writer skill + exact params — copy from the kind table. E.g. struct members →
/generate-signature-for-structoffset → /write-structoffset-as-yaml with struct_name/member_name/offset/size=None/
offset_sig/offset_sig_disp; indirect vcalls → /generate-signature-for-vfuncoffset → /write-vfunc-as-yaml with
func_addr=None, func_sig=None, vfunc_sig, vtable_name, vfunc_offset, vfunc_index=offset/8.>

## Failure handling

- Predecessor YAML missing → **STOP** and report.
- A required target unresolved even after following callees → resolve the rest, then **STOP** and report exactly
  which output(s) failed so the user can extend the references.
- Never emit a platform-gated symbol on the wrong platform.

## Output YAML filenames

Written under the active artifact module directory, one per symbol: `<symbol>.windows.yaml` / `<symbol>.linux.yaml`.

Robustness principles (the heart of a good fallback)

  1. Anchor semantically, not positionally. A fixed address or "it's in function F" breaks on the next update. A string literal, a magic constant (e.g. an FNV seed 0x811C9DC5), a named interface call, or a distinctive helper survives.
  2. this + offset is stable across the inline boundary. Struct offsets are relative to the class pointer (arg1). Whether the access is inlined in the predecessor or de-inlined into a helper that receives this, the same this + offset appears — so members are recoverable either way.
  3. Follow the callee (the core move). If a target isn't in the predecessor, enumerate the predecessor's calls, decompile them, and recurse. This is what makes the fallback a guarantee rather than a re-run of the fragile reference match. Cover the inline-back case too.
  4. Know the indirect-vcall shape. For call qword ptr [reg + disp] on an interface pointer, the output is vtable_name + vfunc_offset = disp + vfunc_index = disp/8 + a vfunc_sig pinning the call instruction; there is no func_va. Resolve the interface from the receiver global's type.
  5. Watch for decoys. References sometimes annotate two nearby offsets with the same member name. Trust the source-owned bin_artifacts/ YAML. (Real example: CEntitySystem::m_eNetworkSerializationMode is the DWORD at 0xBBC, set from the mode param and re-read at the SetNetworkSerializationContextData call — not the byte flag at 0xBDA, even though the reference labels both.)
  6. The offset is the must-have; the signature is best-effort. For struct members, offset is the required output; offset_sig is for relocation and may legitimately be omitted (write offset only) when a unique signature can't be found — especially for members whose access spans a function boundary.

Checklist

  • Read the target preprocessor .py, its config entry, its reference YAMLs, and its bin_artifacts/ source-owned output YAMLs.
  • Output inventory lists every symbol with kind + platform + predecessor + reference value.
  • Fallback SKILL.md filename equals the finder's skill name.
  • ## Realworld Function References lists exact, directly openable repo-relative YAML paths for each relevant platform and inline/de-inline helper or variant; it contains no {platform} shorthand.
  • SKILL.md is self-contained: enumerates all outputs, gates by platform, has the Step-0 skip-existing step.
  • Each target has a semantic anchor and the follow-the-callee instruction; decoys are called out.
  • Each kind is mapped to the correct sig-gen + writer skill with correct params (indirect vcalls use func_addr=None/func_sig=None + vfunc_sig).
  • Failure handling + output-filename sections present.
  • unittest discover -s tests -b passes.
  • Values cross-checked against bin_artifacts/ Git truth.
  • Real Agent-Skill-only test passed with uv run ida_analyze_bin.py -gamever <gamever> -oldgamever none -modules=<module> -debug -skip_pp -skill=<skill_name>; the log proves preprocessing was skipped, the Agent Skill actually started, all expected YAMLs were produced, and the summary reports Failed: 0.
  • The current branch is dev (created from main when it did not already exist).
  • New SKILL.md, computed bin_artifacts closure, and memory pointer are explicitly staged and committed.
  • /create-pr was not called; no push or PR was performed without a separate user request.

Worked example — find-CEntitySystem_Init-decompiles

The canonical output of this workflow lives at .claude/skills/find-CEntitySystem_Init-decompiles/SKILL.md (commit a31fdf4). Study it as the reference.

  • Fragile foundation: ida_preprocessor_scripts/find-CEntitySystem_Init-decompiles.py mines 11 targets by LLM_DECOMPILE off one predecessor, CEntitySystem_Init (reference references/server/CEntitySystem_Init.{platform}.yaml).
  • What broke: on Windows 14168, CEntitySystem_InitEntityMaterialAttributes was de-inlined out of CEntitySystem_Init, so the m_EntityMaterialAttributes access left the predecessor and the LLM match failed, aborting the module.
  • What the fallback covers: all 11 targets — 7 cross-platform struct members, 2 indirect-vcall vtable offsets (INetworkMessages_SetNetworkSerializationContextData @0xA0/idx20, IFlattenedSerializers_CreateFieldChangedEventQueue @0x118/idx35), Linux-only CEntitySystem_ProcessEntityRegistration, and Windows-only m_EntityMaterialAttributes (@0x2070) — each anchored by a fingerprint (the "string_t_table" call, the CUtlScratchMemoryPool::Init(_, 0x400, …) call, the FNV material-hash loop, …) and recoverable whether inlined or de-inlined.
  • It demonstrates every section of the template: directly openable real-world function references, the inventory table, Step-0 skip, follow-the-callee, the indirect-vcall shape, the 0xBBC-vs-0xBDA decoy note, and the offset-is-must-have caveat for the field-change trio.