Purpose: prove that an agent wired to a reproducible backtest harness can run scored forecasting experiments unattended, that an adversary agent catches leakage, and that the loop produces a candidate that beats a baseline on a frozen holdout. Public data only. This is a mechanics test, not an accuracy claim.
Hand this file to Claude Code in an empty directory and say: "Read SPEC.md and execute Phase 0 through Phase 3 in order. Stop at the end of each phase and report against the acceptance criteria before continuing."
- GitHub CLI installed and authenticated:
gh auth statusreturns logged in. - Kaggle account, competition rules accepted for "M5 Forecasting - Accuracy" at https://www.kaggle.com/competitions/m5-forecasting-accuracy/rules
- Kaggle API token placed at
~/.kaggle/kaggle.jsonwith permissions 600. The agent must never read, print, or move this file. - Python 3.11+ and
uvorpipavailable. 8 GB RAM is sufficient for the subset defined below.
The agent does not create accounts, does not enter credentials, and does not modify
anything under ~/.kaggle.
- Create a private GitHub repository named
keplerunder the authenticated user withgh repo create kepler --private --clone. - Inside it, create the layout below. Commit as
chore: scaffold.
kepler/
SPEC.md (this file)
CLAUDE.md (researcher instructions, Phase 2)
adversary/CLAUDE.md (adversary instructions, Phase 3)
Makefile
pyproject.toml
src/
data.py (download, subset, snapshot)
features.py (feature construction; the researcher edits this)
model.py (model definition; the researcher edits this)
backtest.py (rolling-origin folds; the researcher edits this only to add models, never the fold logic)
scorer.py (metrics; FROZEN, never edited by any agent)
report.py (aggregates runs.csv; FROZEN)
data/
raw/ (gitignored)
snapshot/ (parquet subset, committed via git-lfs or regenerated by make data)
holdout/ (FROZEN, gitignored, created once by make holdout)
runs/
runs.csv (append-only experiment log)
findings/ (one markdown file per experiment)
hypotheses/ (human-written hypotheses for debate mode)
.gitignore
.gitignoremust includedata/raw/,holdout/,.env,*.kaggle*.- Python dependencies: pandas, numpy, pyarrow, lightgbm, scikit-learn, statsmodels.
Pin versions in
pyproject.toml. - Add a
CODEOWNERSfile assigningsrc/scorer.py,src/report.py, andsrc/backtest.pyfold logic to the human.
gh repo viewshows the private repo.make envinstalls dependencies andpython -c "import lightgbm"succeeds.
- Download M5 with the Kaggle CLI:
kaggle competitions download -c m5-forecasting-accuracy -p data/rawand unzip. - Build the subset: store
CA_1, departmentFOODS_3only. Roughly 800 series, 1,913 days. Write todata/snapshot/sales.parquet,data/snapshot/calendar.parquet,data/snapshot/prices.parquet. - Record a SHA-256 of each snapshot file in
data/snapshot/MANIFEST.txt. Every backtest verifies the manifest before running and aborts on mismatch.
- Cut the final 28 days of the snapshot into
holdout/. Remove those days from the snapshot the agents see. The agents never readholdout/. Scoring on holdout happens only viamake score-holdout, which the human runs.
- Rolling-origin evaluation. Four folds, each forecasting a 28-day horizon, fold origins spaced 28 days apart, ending at the last day of the agent-visible snapshot.
- Training data for each fold is strictly prior to the fold origin. Features may not use any information after the origin. Feature construction receives the origin date explicitly and must respect it.
- Outputs per fold and aggregated: WRMSSE (the M5 competition metric, computed at the item-store level with the standard weights), WAPE, and bias (sum of forecast minus actual, divided by sum of actual). Also report WAPE by horizon bucket (days 1-7, 8-14, 15-28).
- Runtime target: under 10 minutes per full backtest on a laptop. If a model exceeds
20 minutes, the run is marked
timeoutand discarded.
Columns, exactly:
run_id, timestamp, git_commit, model_name, config_hash, fold_count,
wrmsse, wape, bias, wape_h1_7, wape_h8_14, wape_h15_28,
seconds, status, findings_file, author
author is human, researcher, or adversary.
status is ok, timeout, error, or rejected.
seasonal_naive: value from 7 days prior. Log it.lgbm_baseline: LightGBM with lags 7, 14, 28; rolling means over 7 and 28; day-of-week; month; SNAP flag; event flag; sell price. One model across all series, item and store as categorical. Log it.- Reproduce
lgbm_baselinetwice with a fixed seed. Metrics must match to four decimals. If they do not, fix determinism before proceeding.
make backtest MODEL=lgbm_baselineruns twice with identical logged metrics.runs.csvhas three rows: seasonal_naive, lgbm_baseline, lgbm_baseline (repeat).make reportprints a table of all runs sorted by WRMSSE.- Commit as
feat: harness and baseline. Tagv0-harness.
Write CLAUDE.md at repo root (content in the spec handed to the operator).
- Start a Claude Code session in the repo root and say: "Read CLAUDE.md and run the loop for two hours."
- Human records wall-clock start and end.
- At least 10 scored runs logged with author=researcher in the two-hour window.
- Every logged run has a matching findings file and, if kept, a branch.
- Zero edits to frozen files
(verify with
git diff v0-harness -- src/scorer.py src/report.py). - The session report names best WRMSSE, baseline WRMSSE, and runs per hour.
Write adversary/CLAUDE.md (content in the spec handed to the operator).
- Human creates a branch
exp/planted-leakthat adds a feature using the rolling 7-day mean centered on the forecast date (which reaches into the future). Do not tell the adversary which branch is planted. - Run the adversary on all
exp/*branches.
- The adversary FAILs
exp/planted-leakon checklist item 1 with the specific feature named. - The adversary produces a review for every researcher branch.
- At least one researcher branch receives PASS.
make score-holdout MODEL=lgbm_baselineandmake score-holdout MODEL=<best PASS branch>.- Record both in
runs/holdout.csv. This file is written by the human only. - Do not iterate on holdout. One scoring per candidate. If the candidate loses, the loop resumes on the backtest and a new candidate is scored later.
Record in RESULTS.md at the end:
- Researcher runs per hour.
- Compute seconds per run and, if using API billing, cost per run.
- Did the adversary catch the planted leak (yes/no) and how many real branches did it reject.
- Holdout WRMSSE: baseline versus best PASS candidate.
- The single hypothesis the researcher flagged as most promising and unexplored.
These five numbers are the input to the compute test and the team conversation. Nothing else from this prototype is a claim about PepsiCo.
- Any PepsiCo data, code, or credentials.
- Hierarchical reconciliation beyond the item-store level.
- Cold-start or new-product forecasting. That is a separate harness with a different fold design.
- Scheduling the loop overnight. Run it interactively first; add a cron job only after Phase 3 passes.