multiples · working protocols

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.

The name behind everything

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:

NamePoints atHow it is found
DATA_ROOTyour working data$MULTIPLES_DATA
GLOBAL_DATAthe canonical panelsderived: DATA_ROOT's sibling
WORK_ROOTyour code and resultsderived from the script's own location
REPO_ROOTthe repoderived 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"
The gate does not carry over

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.

What it cannot see

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

HookFiresDoes
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.

Where these came from

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.

Turn track changes off

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

  1. 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.
  2. git pull, then claim the round.
  3. Read project/global/state.md and your session.md.
  4. Work. Run scripts through tools/runlog.py. Check --script before touching anything upstream.
  5. 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.
  6. On agreement: update state.md and its changelog, refresh session.md, commit, push, release the lock.

Traps

  • MULTIPLES_DATA unset 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.md is 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.md conflicts are resolved by hand, on the one file that defines what the paper is.