replicate-paper
Use when a published empirical finance or accounting result has to be reproduced from data — 'replicate Table 2 of X', 'does this paper replicate', 'reproduce their main result', 'can we get their coe
SKILL.mdskills/replicate-paper/SKILL.md
GitHubreplicate-paper — locked reproduction, then a fenced, disclosed in-design search
What this skill carries — grep references/ for any subject the names below miss:
!d=${CLAUDE_SKILL_DIR}; command -v skill-toc >/dev/null 2>&1 && exec skill-toc "$d"; s=$HOME/.claude/skills/plugin-utils/bin/skill-toc; [ -x "$s" ] && exec "$s" "$d"; echo "(skill-toc unavailable: references and scripts are NOT listed here — install the plugin-utils plugin, or start a new session so its bin/ reaches PATH)"
Templates: ${CLAUDE_SKILL_DIR}/templates/check.sh, ${CLAUDE_SKILL_DIR}/templates/PROMPT.md,
${CLAUDE_SKILL_DIR}/templates/ledger.csv. Worked example (ADKO not reproduced, Antoniou fragile hit):
${CLAUDE_SKILL_DIR}/references/worked-example.md. Scoreboard rows and the appendix paragraph:
${CLAUDE_SKILL_DIR}/references/closing-and-disclosure.md.
A replication is a locked reproduction of the paper's printed cells on the paper's own sample. When it fails, the user may authorise an in-design search (the AGK rule): deviations only inside the paper's own design, every specification priced and ledgered, the count disclosed beside any hit. A hit found that way is a searched specification, never a locked result.
fetch paper ─► cite cells & definitions (page/line) ─► lock predictions + bar (user) ─► md5 the lock
─► reproduce on the paper's own window ──pass──► extension rows ─► done
│ fail
▼
AskUserQuestion: in-design search? ──no──► record the non-replication ─► done
│ yes
▼
scaffold grind_<id>/ (check.sh, frozen.md5, ledger, PROMPT.md) ─► check.sh at baseline exits 1
─► grind.sh run --gate <leads_exhausted key> --wait-alert 1
├─ check exits 0 ─► verify hit vs printed table ─► user: accept as fragile? ─► robustness rows ─► disclose
│ └─ second gated cell owed? ─► phase 2 (PROMPT2, new journal)
└─ leads_exhausted filed ─► grind.sh stop ─► record NOT reproduced, with the count
<EXTREMELY-IMPORTANT>
## IRON LAW: NO ESTIMATE BEFORE THE LOCK, AND NO BAR CHANGE AFTER AN ESTIMATE
The bar — which cells, sign, |t|, CI, window — is written, user-approved and md5-frozen before the
first coefficient is computed. After that, only the user changes it, and their words go verbatim into
check.sh's header. A bar chosen after seeing estimates is a specification search reported as a
replication: it hands the user a result that the next referee takes apart, which is the opposite of
help. If an estimate makes you want to move the bar, that wish is the finding — report it.
</EXTREMELY-IMPORTANT>
1. Obtain the paper and cite it
- Get the version of record through the
fetch-paperskill;pdftotext -layoutit to a.txtbeside the PDF so every cite is a line number. Only a working paper on disk → say so incheck.sh's header ("WP values may differ"). Prevents: a build from memory of the paper, and a fence nobody can adjudicate. - Every cell, filter, variable definition, window and winsorizing rule the build uses is cited by page or
.txtline. Prevents: an "allowed reading" that the paper never offered.
2. Lock predictions and the pass bar
- Write
prediction_locked.md(bar cell(s), published sign, p threshold, window(s), class/measure rule, pre-specified robustness, any predictions) and get it approved through AskUserQuestion. md5 it intofrozen.md5before estimating. Prevents: the bar drifting toward the estimate. - Derive the published SE:
se = |b|/|t|; when only p is printed,se = |b|/zwithz = Φ⁻¹(1 − p/2); 95% CI =b ± 1.96·se. State the derivation and the rounding of the printed coefficient in the header. Stars only → the CI is undetermined; ask the user, do not invent one. - Gate only cells significant in print. A published null is scored by the matched-null rule (ours insignificant at 5% passes), never as a hit. Prevents: AGK 2019 Table 4 — a "done" inside a null's CI, rejected by the user because a null's CI admits almost anything.
- Joint rule over every printed-significant cell the user gates. One step must carry them all IN;
hits on different steps never combine. Ask which cells are gated before the loop starts.
Prevents: CHL
g_34, DROA IN with BHAR OUT on the same row.
3. Reproduce on the paper's own sample and window first
- The locked reproduction runs on the paper's sample and window before any extension; extension windows are separate rows with their own predictions. Data that do not reach the paper's window → the first job is the pull, not a different gated window. Prevents: gating a window the paper never estimated; the Antoniou hit came from restoring 2003-2016, not from any construction switch.
- WRDS through the
wrdsskill: SAS on the SGE grid via qsub only, never Postgres; a one-year test job, inspected, then one pull per source over the whole window, split by year locally. Prevents: 18 serial per-year queries plus count probes, which the user stopped by hand.
4. The in-design grind
Only after the user picks "in-design search" through AskUserQuestion. One loop per paper, under
scratch/grind_<id>/, ledger at data/output/<id>_grind.csv.
| Piece | Rule | Prevents |
|---|---|---|
check.sh (from the template) |
Exit code is the verdict: 0 hit, 1 not yet, 2 integrity. Per cell: published sign, |t| ≥ 1.96 on our coef/se, coef inside the published CI. Joint over the gated cells. Prints BEST/MOST_CELLS_IN progress lines. Run it at baseline; it must exit 1 on the committed run, not 0. |
a loop that grades itself; a check that passes on nothing |
| Integrity → exit 2 | frozen files changed, ledger header changed, a (step, cell, window) priced twice, non-numeric coef/se, a gated cell that is a published null |
silent edits to the locked build; re-pricing until it hits |
frozen.md5 |
md5 of the committed run's outputs, scripts, tests, note, the lock file and check.sh itself |
an iteration editing the baseline or the gate |
| Ledger | Append-only CSV, step,cell,window,class_rule,coef,se,t,p,n,first_stage_F,switches,sample_n; g_NN_<slug>; a corrected build gets a new number |
a deleted row the count no longer discloses |
| Declared switches | Every switch vs the committed run, ;-joined key=value, declared in the log before estimating, with the fence reason and the paper's page/line |
a hit whose deviations nobody can list |
inference_only=1 |
Set in switches for a step that changes only SEs/clustering; check.sh reads the field, never the slug; such a row is priced but never ranks or hits |
clustering passed off as a replication |
| No threshold tuning | Median splits, winsor percentiles, bandwidths, size cuts stay at the paper's values; an alternative enters only as a named reading of the text, before its row is priced; N and treated shares are reported, never targeted | tuning a cutoff on the outcome |
| Fence | Allowed: choices the paper leaves open or defensible readings of its text. Forbidden: controls/estimators the paper lacks, selecting on the outcome or residuals, raw-value edits, windows outside the paper's | a "replication" of a model the paper did not run |
| Rejected steps | A step the user later rules out of fence stays in the ledger and the count, and is listed verbatim in check.sh's REJECTED set so it cannot hit |
BY17 g_13, controls in a column that prints none |
| User decisions | Quoted verbatim, dated, in check.sh's header |
an operator's paraphrase widening the bar |
PROMPT.md (from the template) |
One g_ row per iteration; ranked leads, each citing the paper's page/line and the code line it would change; stack only switches named before pricing |
an iteration re-diagnosing from scratch, or stacking on t |
Launch through the grind skill, detached, with the gate on the FILED key:
G=/abs/scratch/grind_<id>; J=$G/<id>.jsonl
setsid nohup bash ${CLAUDE_PLUGIN_ROOT}/skills/grind/scripts/grind.sh run \
--journal "$J" --prompt-file "$G/PROMPT.md" --check "bash $G/check.sh" \
--gate "jq -R 'fromjson? | select((.kind==\"floor\" or .kind==\"note\") and .key==\"leads_exhausted\")' $J | grep -q . && { echo 'leads_exhausted filed: operator stop due'; exit 1; } || exit 0" \
--wait-alert 1 --stall-after 8 --max-iters 40 >"$G/grind.log" 2>&1 </dev/null &
The first wait after the floor is filed alerts you; answer it with grind.sh stop, never by waiting
for the stall count. Prevents: 3-8 empty iterations after leads_exhausted (BKL 10 of 20); and a
grep -c leads_exhausted that counted prose mentions as filings.
Phase 2 re-arm — a first hit accepted, another gated cell owed, or the user adds a gate: copy
PROMPT.md to PROMPT1.md; write PROMPT2.md = a # PHASE 2 header (what phase 1 found, next step
number, the new ranked leads, what not to re-price) + PROMPT1.md; a new check2.sh when the bar
changes (user's words verbatim); launch with a new journal <id>_p2.jsonl, same ledger. The phase-1 journal, PROMPT1.md and the
superseded check are retired: add them to frozen.md5, never resume them, one live journal per loop.
Prevents: phase-1 floors and the stall count carrying into a different objective.
5. Closing
- Verify a hit against the printed table yourself — re-read the published cells, rerun the hit step
from its script, confirm
check.shexit 0 and the ledger row match. Prevents: relaying an iteration's claim. - Ask the user (AskUserQuestion) to accept it as fragile, or reject it — fragile hits are the norm. Record the answer verbatim. Prevents: a search hit promoted to a result by the operator.
- Price robustness rows the user asks for on the hit build (another window, alternative
construction, singletons): disclosed, never counted as a second hit even when
check.shprints HIT. - Disclose every searched step: the
g_count, pulls, rejected and inference-only rows, the switches the hit stacks, and what it does not survive. - Scoreboard: hit rows are appended after the main rows, tagged
in-design search hit:, withlock_statussearchedandn_specs_searched; the locked verdicts and the main tally do not move. Details inreferences/closing-and-disclosure.md. Prevents: a search silently rewriting the bar's pass count. - Not reproduced is a result: record the best row per cell, why the fence was exhausted, and the count.
6. Composition — use these, do not restate them
| Need | Skill |
|---|---|
| the PDF | fetch-paper |
| the loop, journal, floors, stop/status | grind |
| SAS/SGE pulls, CCM/link tables | wrds |
| delegating a heavy pricing or build step | farm-out — the result-deciding fix as its own short row, cleanup as separate rows |
| the estimation code itself | ds conventions |
Red flags — STOP
| About to | Why wrong | Do instead |
|---|---|---|
Compute a coefficient with no prediction_locked.md in frozen.md5 |
every later bar is post hoc | lock, get approval, md5, then estimate |
| Gate a cell the paper prints as insignificant | a null's CI admits almost anything | matched-null rule; gate printed-significant cells |
| Count two cells IN on different steps as a joint hit | the paper's result is one coding | joint rule: one step, all gated cells |
Edit check.sh, a threshold or the window after seeing estimates |
the gate moves to the estimate | ask the user; new check2.sh, their words verbatim |
| Re-price a ledger row or delete one | the disclosed count lies | new step number stating the correction |
Name a row _cl/_hc and let the slug decide |
the check never sees it | inference_only=1 in switches |
Gate the loop on a prose grep for leads_exhausted |
it matches notes about the key | jq on .key=="leads_exhausted" |
Let the loop run after leads_exhausted is filed |
each pass is an empty model call | grind.sh stop on the first alert |
| Arm a session hold beside the grind on the same objective | each waits on the other | one driver: the grind |
| Loop a WRDS pull by year, or query Postgres | the user stopped exactly this | one SGE pull per source, one-year test first |
Report a hit without the g_ count and the stacked switches |
a searched row read as a replication | disclose the count beside the hit |
| Move a main-row verdict because a search hit landed | the locked bar stops meaning anything | append a tagged hit row; main tally unchanged |