Protocols
How we work in this repository: where things live, how the two
builds stay independent, what gets logged, and the rules that are not negotiable.
Nothing here is about the paper itself — for that read
project/global/state.md.
Your git config user.name, lower-cased and hyphenated, is the name of
every folder you own — code, data, notes, logs: kerry-back,
kevin-crotty, bob-dittmar. It is set once and then never
thought about again, but if it is ever wrong, work lands in a folder nobody reads and
nothing errors.
Where things live
Three roots, and one rule: shared things are in
global; everything else is named after its owner.
multiples/ the git repo — code, drafts, memory
├── CLAUDE.md the operating manual (Claude reads this)
├── protocols.html this page
├── writing-guide.md the prose standard for anything in draft/
├── draft/ SHARED — the paper
│ ├── main.tex ia.tex
│ ├── references.bib every entry has a verified DOI
│ ├── figures/ tables/
├── project/ memory
│ ├── global/ SHARED — what we all agree on
│ │ ├── state.md thesis, settled facts, killed ideas
│ │ ├── method_spec.md the frozen spec for the current round
│ │ ├── verifier.md settled facts about the literature
│ │ ├── data_manifest.md what is canonical, and why
│ │ └── config.toml the debate roster
│ ├── kerry-back/ ONE PER PERSON — nobody else writes here
│ ├── kevin-crotty/
│ └── bob-dittmar/
│ ├── session.md your handoff between sessions
│ ├── analyst.md replicator.md
│ └── logs/ run records, briefs, event log
├── workspaces/
│ ├── global/params.py SHARED — the frozen spec, as code
│ ├── kerry-back/ bob-dittmar/ ONE PER PERSON
│ └── kevin-crotty/
│ ├── analyst/ your primary build
│ ├── replicator/ your independent re-build
│ ├── archive/ superseded work, kept for the record
│ └── dag/ provenance diagrams (generated)
├── tools/ the machinery, vendored — nothing to install
└── .claude/
├── commands/ /refresh, /round, /report
└── skills/ econ-write, econ-review (Lu Han)
~/Dropbox/multiples/ NOT in git
├── data/
│ ├── global/ SHARED, read-only — the canonical panels
│ ├── kerry-back/ bob-dittmar/ ONE PER PERSON — working extracts
│ └── kevin-crotty/
└── literature/ SHARED — every paper, named by citekey
If you have the litdb plugin installed, that literature folder is
also indexed and searchable by meaning, so “what does anything we hold say about
peer selection” is a query rather than a recollection. Search it before any web
source. Two things do not change: references.bib is still maintained by
hand, because the citekeys are load-bearing in main.tex and
ia.tex, and the PDFs still live in Dropbox — litdb indexes them rather
than replacing them, and it is each person’s own local database.
Code is in git; data is in Dropbox. The repo carries what rebuilds a number, not the pull. Scripts never write an absolute path — they use three names set at the top of each file:
| Name | Points at | How it is found |
|---|---|---|
DATA_ROOT | your working data | $MULTIPLES_DATA |
GLOBAL_DATA | the canonical panels | derived: DATA_ROOT's sibling |
WORK_ROOT | your code and results | derived from the script's own location |
REPO_ROOT | the repo | derived by walking up to CLAUDE.md |
Analyst & Replicator
The paper's central methodological claim is that two independent builds of the same frozen spec converge. That only means something if they really are independent.
You run both. Your analyst/ tree builds the sample and runs the
estimation. Your replicator/ tree re-implements the same spec
in its own code and runs the robustness probes. They are separate directories
precisely so the boundary is hard to cross by accident.
The protocol, in short: two independent builds → reconcile counts and summary statistics → confirm they agree (a hard gate, before any estimation) → collapse to one panel → both estimators run on it. Never average two disagreeing builds. A gap is a bug or a spec hole; disagreement that survives a correct, identical spec is itself the finding.
The repo can check the boundary for you:
python3 -m tools.provenance --independence
It lists every edge where a replicator script reads an analyst artifact or the reverse. Reads of the canonical panel are excluded — sharing that is the design. A crossing is either a deliberate cross-test or an accidental coupling, and the tool cannot tell which: say which, in the script's docstring.
Data & the canonical panel
You will not rebuild the panel. You will start from the confirmed one.
~/Dropbox/multiples/data/global/ holds the core panels, read-only, and
every author estimates on them. project/global/data_manifest.md records for
each file its hash, its shape, who promoted it, and the reconciliation that justifies
trusting it.
Build in your own tree; promote deliberately, and only after the two-build gate:
python3 tools/promote.py <your-built-file> --note "how it was reconciled"
Starting from the confirmed panel means you inherit assurance from the original reconciliation and add none of your own. That is a fair trade while the panel is unchanged. If it is rebuilt, extended, or has a column redefined, it is a new artifact and needs the two-build gate again.
The spec's numeric parameters — sample years, peer rule, feature list, tuning grids,
learner hyperparameters, seeds — live in workspaces/global/params.py. Import
them rather than retyping them. Changing that file is a change to the method spec and
goes through the same gate.
The state file
project/global/state.md is the single source of truth for
the paper's direction: thesis, open questions, settled facts, killed ideas with the
reason they were killed.
It is shared, and it is long prose that gets rewritten at every gate — which git cannot merge. So it has a changelog at the top: one line per substantive change, newest first, with your slug and the date. Append; never rewrite or tidy someone else's entry. That is how the other two see what moved without reading 750 lines.
One round at a time
Because state.md cannot be merged, exactly one round runs
across the whole project at a time. Git enforces it — a push is atomic, so exactly one
claim can win.
python3 -m tools.debate.lock claim "peer rule for microcaps" # before you start
python3 -m tools.debate.lock status # who holds it
python3 -m tools.debate.lock release # after the gate
If your claim is refused you are told who holds the round and what they are working on. Release even if the round was killed — an unreleased lock blocks everyone. If a lock looks abandoned, ask before clearing it; releasing a live one discards their gate.
/round start and /round wrap these, and are what you should
normally use — the command also writes the changelog entry, which the raw lock commands
do not.
The debate team
A proposer and two adversaries, each a different model family, called through OpenRouter. They are stateless: one call each, charter plus brief in, JSON out. The brief you write is their context.
You need your own OpenRouter key — openrouter.ai,
then export OPENROUTER_API_KEY=.... Calls are billed to whoever runs them.
The roster is project/global/config.toml, shared and committed so all three of us —
Kerry, Kevin and Bob — debate against the same panel; the charters are in
tools/debate/charters/.
python3 -m tools.debate.debate --seats proposer \
--brief-file <path> --project . --round <n>
python3 -m tools.debate.debate --seats adversary,adversary_2 \
--brief-file <path> --project . --round <n> # concurrent
A seat that errors returns {"error": ...} — note it and carry on. One
bad voice never sinks a round.
Logging
Everything a run produces is stamped
<author>-<date>-<time> and written into your own folder. No
configuration; the author comes from your git name.
Run analysis scripts through the logger, not bare python:
python3 tools/runlog.py workspaces/kevin-crotty/analyst/logmult/code/04_eval.py 2001 2025 mytag
It runs the script normally and records what it actually opened — an audit
hook below pandas and pyarrow, so paths built at runtime are caught. One JSON line per
run is appended to project/<you>/logs/runs.jsonl, committed: script,
arguments, git commit, duration, status, inputs, outputs. Failed runs are recorded too,
which is exactly what you want when a file is half-written.
python3 -m tools.provenance --runs # read them back
Two other things get logged automatically. Every tool call Claude makes lands in
project/<you>/logs/log-<stamp>.jsonl via a PostToolUse hook,
secrets redacted. And every brief you send a debate seat should be written to
project/<you>/logs/ rather than a temp file — briefs are the actual prompts the
debate answered, and the only part of a round the event log does not already hold.
Logs are gitignored; runs.jsonl is not. The event log grows without
bound — one project's is already 53 MB.
The DAG
Which script produced each file, what it depended on, and what breaks if you change it. Read out of the code — nothing to declare.
python3 -m tools.provenance --file logmult/data/preds.parquet # where did this come from
python3 -m tools.provenance --script extend2000/code/01_extract.py # what must be re-run
python3 -m tools.provenance --mermaid analyst/logmult # draw it
python3 -m tools.provenance --orphans # inputs nothing produces
Run --script before changing anything upstream, and
report the blast radius with the change. That list is what had to be worked out by hand
after a market-cap bug invalidated every downstream number — which is how a stale figure
survives into a draft.
The pictures are Mermaid, in workspaces/<you>/dag/*.md. GitHub
renders them inline, so you can just click the file — nothing to install. They are
regenerated by the pre-commit hook, so a diagram cannot lag the code it describes.
A path assembled at runtime resolves to a wildcard, and a genuinely dynamic one may
not appear at all — --orphans lists the residue. The static graph is a
strong first draft of provenance; runs.jsonl is the record of what
actually happened.
Hooks
| Hook | Fires | Does |
|---|---|---|
tools/githooks/pre-commit |
every commit touching workspaces/**/*.py |
regenerates your DAG diagrams and stages them into the same commit |
tools/debate/log_event.py |
after every Claude tool call | appends it to your run log, secrets redacted |
Both are turned on during setup — the first needs
core.hooksPath pointed at tools/githooks once per clone, the
second is wired in .claude/settings.json and works as soon as you clone.
Skip the pre-commit hook for a single commit with
git commit --no-verify.
The commands
All of them come with the clone. Nothing to install, and the same behaviour on all three machines.
/round — finish a round properly
Writes the round's changelog entry, promotes whatever settled, and releases the lock. It starts by checking whether the changelog has fallen behind, and it insists on an entry even when the round settled nothing — a round that took something up and dropped it leaves no diff and no artifact, so if the entry is not written the exploration is gone. That entry is what answers did we ever try X a year later.
/round start "what this is" claims the lock first. Worth doing here:
with three of us, claiming up front surfaces a collision in the first minute instead
of after two people have spent an afternoon on the same question.
/round status says who holds it and whether the changelog is behind.
/report — the project on one page
Writes an HTML report to the repo root: executive summary, what is settled, open
issues, and a chronology. It reads only committed records, so there is no accumulation
step to remember. This replaces the hand-built project-to-date.html.
Underneath it, tools/chronology.py merges the state.md
changelog, git log, and the run records into one timeline. It is also the
quickest answer to a question asked in passing:
python3 -m tools.chronology --author kevin-crotty --since "last week" --human
python3 -m tools.chronology --kinds changelog --human decisions only
python3 -m tools.chronology --gap --human commits with no reason recorded
/refresh — before the context runs out
When a session gets long, this checkpoints memory to disk. It promotes what has
settled into state.md with your changelog line, rewrites your
session.md so a cold session could resume from it alone, freshens the
subagent notes, and releases the round lock if you are done with it. Then it stops and
tells you to run /clear — it cannot clear its own context. Start a new
session and say resume.
Refreshing between rounds costs nothing; mid-round it costs a re-read. Claude will suggest it when a round has run long.
econ-write — automatic, whenever the draft is touched
You do not invoke this one. Claude reads it before writing or revising any
prose in draft/ — the paper, the internet appendix, an abstract, a referee
response. That is a rule in CLAUDE.md, not a suggestion, because the point
of having it is that Claude's default register is not what a finance journal wants.
It is Lu Han's synthesis of writing guidance from Cochrane, McCloskey, Shapiro, Head,
Bellemare, Goldin, Glaeser and others. It does not apply to state.md,
commit messages, or notes between us — only to what an editor will read.
/econ-review — a referee pass, on demand
A full review of the draft: identification, claim consistency, exhibits, literature, and writing, fanned out across reviewers and adjudicated. Use it when a draft is close to circulating, not on every edit — it is slow and thorough by design.
It needs its own Python dependencies for reading PDFs, deliberately kept out of the
project's requirements.txt so a large dependency tree is not in everyone's
venv for a tool most sessions never touch. Claude installs them the first time you run
a review; see .claude/skills/ATTRIBUTION.md.
Both skills are by Lu Han, vendored into
.claude/skills/ so we all get the same version from a clone.
econ-review is licensed PolyForm Noncommercial — academic use is
permitted and the licence travels with it. econ-write ships without a
licence file, so use it here but do not redistribute it further, including in a
replication package, without asking the author.
Overleaf
For whoever prefers writing in Overleaf. If that is not you, nothing here changes anything.
There is an Overleaf project that mirrors draft/ — the paper
(main.tex), the internet appendix (ia.tex), the bibliography,
and the figures. Nothing else from the repo is there: no tools/, no
workspaces/, no project/. Ask Kerry for an invitation.
Edit there as you normally would. The two sides are kept in step by Claude, and the
procedure is written down in CLAUDE.md — so you do not need to know the git
commands, and neither does anyone else. Ask it to sync the draft and it will pull the
Overleaf edits down, push ours up, and tell you if the two collided. GitHub stays the
source of truth; Overleaf is a window onto one folder of it.
This is the one that costs you work. Overleaf documents that mixing tracked changes
with git pushes loses data — not a merge conflict you can see and resolve, but edits
that quietly disappear. Leave track changes off on main.tex and
ia.tex. If you have already turned it on, accept or reject everything
before asking for a sync.
Two smaller things that follow from how the bridge works:
- Comments do not survive a rename. The bridge sends a renamed file as a delete plus a create, so anything anchored to it is lost. Rename before the comments accumulate, not after.
- Figures and tables travel one way only, repo to Overleaf. They are built by code here and stamped with what produced them, so an edit made to one on the Overleaf side is a number nobody can trace. If a figure is wrong, fix the script.
- Old drafts live in
draft/archive/— dated PDFs (draft_YYYYMMDD.pdf, the date the version was last current), snapshotted before each major edit so any prior milestone opens without git. They ride along to Overleaf.
A working day
- Open Claude Code. It checks your setup before anything else; if something has drifted — a new machine, a shell without the export — it fixes what it can and tells you what it cannot.
git pull, then claim the round.- Read
project/global/state.mdand yoursession.md. - Work. Run scripts through
tools/runlog.py. Check--scriptbefore touching anything upstream. - Present the result to your coauthors — with the full empirical design, not just the headline number. Every filter, definition, lag, and standard-error treatment. A number whose construction cannot be seen cannot be judged.
- On agreement: update
state.mdand its changelog, refreshsession.md, commit, push, release the lock.
Traps
MULTIPLES_DATAunset in a new shell. Scripts raise immediately — that is deliberate, and the fix is a new terminal, not a hardcoded path. The alternative to a crash is silently reading the wrong panel.- Editing the canonical panel. It is read-only. If you need it
changed, that is a two-build gate, not a
chmod. - Reading the other role's working notes. A replicator that has
seen the analyst's
analyst.mdis no longer independent — that file contains the answers. - Installing
openassetpricing. It downgrades pandas 3.0 to 2.2 and breaks the pipeline. - Running two rounds at once. The lock exists because
state.mdconflicts are resolved by hand, on the one file that defines what the paper is.