Changelog¶
v1.1 — Seneca Polycentric + Pollution Monitoring (in progress)¶
Polycentric governance under calibrated Seneca dynamics. Research
question: does a meta-agent that observes the hidden pollution
coordinate \(P\) avert the cascade collapse that \(P\)-blind player
agents cannot diagnose? Branch v1.1-dev opened 2026-05-28 from
e4d658a (= v1.0 tag). Scope and plan:
V1.1_PLAN.md.
A — Foundations¶
- A1 —
RCPAgent.baseFractionviastrategyParams(d5f0445). Exposes the base harvest fraction as a tunable parameter instead of the hard-coded 0.4 (calibrated for logistic, wrong for Seneca). - A2 — Seneca calibration document (
fc4af66).docs/experiments/seneca_calibration.mdrecords the S0 sweep and cross-referencesRCPAgentcomments.
S — Seneca experiments¶
- S0 — Seneca calibration sweep (
6f096c4).baseFraction = 0.035for 5 RCP × R₀=1.0 canonical Seneca (seed 17, 29 turns in band). - S1 — Seneca standalone pilot (
8bb3d2b). Mule late entry halves the cliff (24→12); pact without enforcer logs 2 violations but does not save. Mule wins in wealth. Includes E1 (setPollution) and L3 (pollution link). - S2 — Polycentric pollution cascade (
646c27e). Pollution from arena A strangles arenas B/C: −76% capital, collapse 16 turns earlier, P 7.1× baseline. First consumer of L3 + E1. - S3 — Governance P-aware (
2617eca). Three levers (cap/levy/fine) × two triggers (pollution/capital). Capital-triggered cap is the win-win: prevents cascade AND saves the emitter. OFAT robustness (9880847): claims A/C 20/20, claim B 5/5 on k1/bf/linkRate, 3/5 on dt (cedes at dt ≥ 3.5; baseline dt=3.0 is the edge).
E — Engine extensions¶
- E1 —
SenecaEngine.setPollution(p)(62495ff). Mirrors the existingsetStocksetter. - E2 —
setCapital+capital_shock(in2617eca). New perturbation type for the levy instrument (direct action on the capital ODE variable \(C\)).
L — Link extensions¶
- L3 —
Link.type='pollution'(da42aee). Directional pollution cascade in_applyLinks: emitter's \(P\) is injected into receiver arenas' Seneca state each world round.
P — Publications (tracked on dev branch)¶
Papers 01–03 reworked, rendered, and tracked on v1.1-dev:
- paper_01 — signal-adaptive rework, V5/V6 appendix, 2 figures.
- paper_02 — Cell C decomposition, 2×2 factorial, Bayesian CI.
- paper_03 — Seneca polycentric (full draft, §1–§6, 21 BibTeX, alive-lineage Bardi, OFAT robustness figures, code appendix).
v1.0 — Nested Arenas / Polycentric (tagged 2026-05-28)¶
Cross-arena federation. 27 commits, tag v1.0 on e4d658a,
merged to main. Research question: does Vasconcelos' polycentric
governance result survive in BDPD when commons are coupled by
resource flow and governed by a meta-agent? Scope and plan:
V1.0_PLAN.md.
W — World scaffold¶
- W1 —
platform/world.js(53f1ca7).World,Link,Treatyclasses. A World holds N arenas + links + treaties + a global clock. - W2 — Super-scheduler (
4e55e59).world.tick()+runRounds() - aggregated rollup across arenas.
- W3 — Storage paths (
9b57f97). Arena data lands under realworldId;defaultbucket for back-compatibility. - W4 — World API (
39510ca)./api/v1/worlds/:widREST surface.
L — Link types¶
- L1 —
Link.type='resource'(a5792be). End-of-round resource transfer between linked arenas (constantrate).
C — Governance agents¶
- C3.a — Arena-level
governance_agent(a63b732).role: 'meta'agent type: non-harvesting, observes state each turn, returns perturbation specs. Registry:noop,sanctioner. - C3.b — World-level
governance_agent(2bb12bf).WorldSanctionerAgentcovers all arenas in a federation.
B — Federation features¶
- B1 — Agent migration (
6ceaf29). Move an agent between arenas within a world (wealth preserved). - B2 — Cross-arena messaging (
18051ed). Federatedsend_privateacross arenas within a world.
D — Science¶
- D1 — Cliff polycentric pilot (
b269956). Functional verification of the W+L+C stack on the canonical cliff setup. - D2 — Mule polycentric pilot (
4d46b22). L1 + sanctions stack; Mule relocation dynamics. - D3 — Yoon-style voluntary sanctioner (
f7ee90d). Discretionary sanctioning withtriggerStockRatio+repeatThreshold. - D4 — Treaty enforcer (
ef46d0e). Cross-arena treaty withharvest_cap_per_arena_per_roundenforcement. - D5 — Multi-source exclusion (
fbde331,6c8be98).excludeperturbation + cluster-of-aggressors sweep (junk purity 1.00).
Docs¶
- Standard light/dark theme toggle replaces custom BDPD palette
(
1a4a91a).
v0.9 — Governance (tagged 2026-05-27)¶
Communication and sanctioning surface for BDPD. Closes the three
"Not directly implemented" rows of the Ostrom table from Paper 0
(§4): pact tracking, cheap-talk surface, sanctioning perturbation.
Opened 2026-05-22 on branch v0.9-dev from d63436d. Tag v0.9 on
commit 4322aa7. Scope and plan: V0.9_PLAN.md.
MVP Paper 1 critical path completed: A1 + A3 + B1 + B3 + C1 + D1. The branch is citable. P1/P2 items (A2 done; C2/C3/D2/D3/D4 deferred to post-v1.0) enrich but do not block submission.
Headline empirical finding (D1)¶
Pure cheap-talk does not solve the cliff. In a 6-agent canonical
cliff setup (5 conservative + 1 aggressive, K=150, regen=0.12,
threshold=10, 30-turn max), enabling cheap-talk for all six LLM
agents (broadcast, send_private, announce_intended_harvest,
pact_propose, pact_accept) results in earlier collapse (T19,
commons → 0) than the builtin baseline (T24, commons 4.4/150). The
aggressor wins in wealth (105 vs mean 64) even with cheap-talk on,
announcements are honoured (lie_score ≈ 0.001) but communication
decays from 8 → 1 messages/turn after T7. This is the Farrell-Rabin
negative result that motivates the Phase B → C transition in
Paper 1: sanctioning is required.
A — State foundations¶
- A1 — PactRegistry (
platform/pacts.js). Per-arena pact lifecycle: propose / accept / decline / cancel, term typesharvest_cap/commons_floor/reciprocity, automatic violation tracking viacheckViolations()called by arena.tick. - A2 — ReputationRegistry (P1, included). EMA per-player
cooperation score, exposed via
query_reputationtool. Mirrored in middle-man for prompt observation. - A3 — Governance metrics in METRIC_REGISTRY (
engine/metrics.js). Five entries:cooperation_index,lie_score,sanction_rate,announce_frequency,silent_defection. All populated per-turn inturnRecord.governancebygovernanceSnapshot.
B — Communication (cheap talk)¶
- B1a — MessageBus (
platform/messages.js). Append-only, recipient-filtered cheap-talk channel with broadcast / private / announcement scopes. - B1b — Arena action protocol + SDK cheap-talk tools. Agents
return
actions[]alongsideharvest. SDK exposesbroadcast,send_private; inbox surfaced inbuildObservationwith one-turn lag (matchesmessages.jsfilter). - B1c — Pact actions on both sides. Arena consumes
pact_propose/pact_accept/pact_decline/pact_cancel; SDK exposes the same tool surface. Active pacts in the observation. - B1d —
announce_intended_harvestas both a tool and a JSON field on the structured response (fallback path: empirically agents prefer the JSON field over the tool when both exist). - B3 — Lie score + announce_frequency + silent_defection. Bundle: gap, signal frequency, and the "silent over-harvester" pattern that catches the aggressive archetype (lie_score is dead at honour=100%; silent_defection is the diagnostic for D1).
C — Mechanisms (sanctioning)¶
- C1 —
sanctionperturbation +pact_violationtrigger (platform/perturbation.js). New perturbation type (amount + factorpenalty model) and trigger kind that fires on each violation recorded byPactRegistry. Smoke + DeepSeek pilot confirm 1:1 violation→sanction wiring in vivo.
D — Science¶
- D1 — Cliff under active cheap talk (
scripts/pilot_d1.mjs). Side-by-side 2-cell mini-pilot at the canonical cliff. Output:data/pilot/d1/{cell_A_off,cell_B_on,comparison}.json+ headline figureexperiments/figures/pilot/d1_headline__*.pngrendered in all three themes byscripts/plot_d1.py.
Smoke coverage¶
15 smokes green pre-pilot: 11 Node (smoke_governance_arena,
smoke_messages, smoke_pact_actions, smoke_pacts,
smoke_reputation, smoke_action_protocol, smoke_b1d1,
smoke_b3, smoke_c1, smoke_world_prefix, smoke_b1b2) +
4 Python (smoke_b1b2, smoke_b1c2, smoke_b1d2, smoke_b1d3).
Plus 3 optional DeepSeek pilots (pilot_c1, pilot_d1, smoke_b1d_pilot).
Post-tag fixes folded in (originally v0.8.1)¶
The two fixes below shipped on the dev branch after the v0.8
annotated tag was published. Originally seeded as a v0.8.1 patch;
folded into v0.9 (no intermediate patch release cut).
- fix — §5 #8 storage path world-prefix (
36c914c). Arena files now write todata/worlds/default/arenas/<id>/instead ofdata/arenas/<id>/, honoring BDPD_ultimate_edition.md §5 decision #8 (nested-safe storage).defaultis the implicitworld_id; v1.0 nested-arena dispatch will populate other worlds without code changes. platform/registry.js:DATA_DIRflipped to nested; deadWORLDS_DATA_DIR/worldsDataDiraccessor (zero callers) removed.platform/reporter.js: same flip + updated path comments.scripts/migrate_storage_world_prefix.mjs: idempotent migration with--dry-run; removes empty legacy dir post-move.scripts/smoke_world_prefix.mjs: registry-level smoke (no LLM, no network) verifying writes land at the nested path.-
Regression:
hardin_1968replica re-run unchanged.REPRODUCIBILITY.mdpins lockfile sha256s only (not arena outputs), so no manifest updates needed. -
perf — raise
max_historydefault 6/8 → 32 (61d7d62). Empirical pilot finding 2026-05-20: DeepSeek prompt-cache hit was ~89% T1-T9 but dropped to ~27% from T10 onwards becausedeque(maxlen=max_history*2)evicted earlier turns and changed the cache-stable prefix. Default32covers 30-turn BDPD games without eviction, keeping the hit rate steady across the full sweep (~3-4× cheaper input tokens for full-game runs). Bumped in three places:agents/bdpd_sdk/agent.py(SDK default), and both middle-man fallbacks plus the--max-historyargparse default inagents/bdpd_agent.py. Games > 32 turns reintroduce the eviction; token-budget guard / summarisation-based history (candidato 4 options B/C) is the v0.9+ path.
v0.8 (2026-05-20)¶
Agent SDK. The release that turns BDPD into a citable testbed for
LLM cooperation experiments. Single LLM provider supported and
tested: DeepSeek, via its OpenAI-compatible API
(deepseek-v4-flash thinking-off as the operational default,
deepseek-v4-pro available for thinking-on workloads). See
Cloud APIs for the rationale.
bdpd_sdk Python package — the v0.8 deliverable surface¶
- New:
agents/bdpd_sdk/— a six-module package that gives BDPD's middle-man (and any notebook user) a stable LLM-agent primitive. client.py—make_client()factory pointed at DeepSeek (DEFAULT_BASE_URL,DEFAULT_MODEL = "deepseek-v4-flash"). ReadsDEEPSEEK_API_KEY/DEEPSEEK_BASE_URLwith a fall-back toOPENAI_API_KEYso the same env works against any OpenAI-compatible endpoint a contributor might want to target.agent.py—BDPDAgentclass: system prompt + per-instance history +chat(user_message, *, memory=None, response_format=None). Rolling history bounded viadeque(maxlen=max_history*2). Exposeslast_reasoning_contentandlast_usageper call.memory.py—PlayerMemorydataclass with typed fields (beliefsAboutOthers,plannedDefectionTurn,observedAggressorIntensity,notes). Three writers: deterministicupdate_from_observation(EMA aggressor intensity), LLM-sideapply_updates(partial merge with strict semantics: absent = untouched, null = clear, wrong type = ignored,notescapped at 5 oldest-drop), and direct caller-side mutation.trace.py—TraceWriterappends one JSONL entry per/decideturn with a fixed schema (ts, player_id, model, observation, prompt, response, reasoning_content, harvest, scratchpad_changes, tool_calls, elapsed_seconds, usage). Suitable for post-hoc replay and per-call cache-rate analysis.tools.py—@tooldecorator with auto-schema frominspect.signature+get_type_hints, supporting primitives,list[primitive], and free-formdict. Internal tool-call loop (max_tool_iterations=4) with optionalon_tool_callhook.parallel_tool_calls=Falseenforced.agents/bdpd_tools.py— two engine-agnostic starter tools wired by the middle-man as closures over per-playerTURN_LOG:query_history(turn_start, turn_end)andcompute_trend(window). A logistic-onlysimulate_futurewas prototyped and dropped — the platform stays engine-agnostic; dynamics belong in the JS engine registry, not in Python tools.
Middle-man (bdpd_agent.py) refactor¶
- Refactor:
bdpd_agent.pynow delegates/decideand/resetto per-playerBDPDAgentinstances (AGENTSdict). The Flask routes stay thin; all LLM logic lives in the SDK. - New:
--archetype {aggressive, conservative, adaptive, random}flag injects a behavioural nudge into the system prompt;--trace-fileenablesTraceWriterJSONL output;--no-memoryfalls back to bare-number responses. - JSON mode: the model is asked to respond
{"harvest": N, "scratchpad": {...}}viaresponse_format={"type":"json_object"}(disabled in--no-memorymode and on the tool-emitting iterations of the tool loop, where DeepSeek rejects JSON mode together with tools). - Parser robustness (commit
76bac3a): v4-flash narrates first and emits JSON last in ~100% of /decide calls. The legacy parser failedjson.loadson the prefix prose and fell back to a bare-number regex that grabbed stock percentages — a silent bug found by the eval-harness pilot.parse_responsenow uses a three-step ladder: full-textjson.loads→JSONDecoder.raw_decodescan for embedded JSON → targeted"harvest": Nregex. Pinned byexamples/agents/parse_response_smoke.py(14 cases including the exact bug patterns).
DeepSeek v4 migration¶
- Migration (commit
517ec00):DEFAULT_MODEL→deepseek-v4-flash; allchat.completions.createcalls passextra_body={"thinking":{"type":"disabled"}}because v4 defaults thinking ON and the tool-call loop requiresreasoning_contentreplay acrosstool_callboundaries that we do not yet plumb. Probing test (examples/agents/probing_test.py) showed thinking is marginal for BDPD's calc-heavy observations anyway — full rationale inmemory/project_v08_thinking_utility. - Cleanup (commit
8f08756): all docs and the cards code path migrate from legacydeepseek-chat/deepseek-reasoneraliases (deprecated by DeepSeek 2026-07-24) to the explicit v4 names.
Prompt-cache optimisation¶
- Perf (commit
08cbd6d): PlayerMemory snap is now baked into the user message instead of being sent as a separate system message between the system prompt and the rolling history. Reason: the snap changes every turn (EMA refresh), and DeepSeek caches on the longest stable prefix. With the snap inline, the prefix[system, *history]stays bit-stable and cache hit rate climbs monotonically with turn count — measured ~89% across turns 1–9 of the pilot (vs ~50% pre-fix). After turn 9 the rolling-history FIFO eviction invalidates the prefix again; a fix for that is registered as a v0.8.1 candidate. - Telemetry (commit
c66bf91):TraceWriterandBDPDAgent.last_usagenow capture the fullusageblock of every call, including DeepSeek-specificprompt_cache_hit_tokens/prompt_cache_miss_tokens(viausage.model_extra). Enables empirical verification of the cache-prefix fix above.
Platform integration¶
- New (commit
07b0558):arena.start()fires aPOST /resetat every http-callback agent at kickoff (fire-and-forget, 5 s timeout, errors swallowed asagent_reset_failedevents). Reset URL is derived by replacing the last path segment ofcallbackUrl(/decide→/reset, etc.). This guarantees the middle-man's per-player state is purged between arenas without operator intervention. Smoke:examples/agents/smoke_arena_reset.mjs.
Eval harness¶
- New (commit
3852dd3):examples/pilot/run_pilot.py— Python orchestrator for v0.8 eval. For each(scenario, mix)cell it spawns 3bdpd_agent.pymiddle-man processes (one per archetype in the mix), wires them as http agents in a generated sweep.json, runsnode experiments/experiment.js, and saves per-cell summary. Scenarios shipped:hardin_3p(symmetric 30/30/30) andolson_3p(asymmetric 50/10/10). Mixes shipped:balanced(a+c+adp),lone-defender(2a+c),lone-defector(a+2c). Output lands indata/pilot/<cell>/. - New (commit
c252d8a):--n-runs Non the runner + acompare_runs.pydiff tool that reports per-cell determinism at three levels (outcome / harvest-sequence / response-text). Backs the v0.8 deliverable #6 finding: attemperature=0.4without an explicitseedparam, DeepSeek v4-flash is fully non-deterministic — 0/4 cells have identical outcomes between paired runs sharing the same arena seed. Documented inmemory/project_v08_seed_determinism. - New (commit
3df2a19):examples/agents/probing_test.py— 10-probe × 3-config aptitude test (flash-noThink, flash-think, pro-think). Empirical finding (memoryproject_v08_thinking_utility): thinking helps only on metacognitive probes (recognising underdetermination); on calc-heavy BDPD-style probes it's neutral or worse (truncation). Default v0.8 = flash-noThink.
Pilot findings (preliminary, n=1)¶
Three behavioural patterns observed in the v0.8 pilot, recorded in
memory/project_v08_pilot_findings for promotion to a proper
examples/llm_findings/ showcase in v0.8.1:
- Cooperation does not scale from n=1. In the balanced mix (1 conservative among 3) the conservative restraint collapses after ~7 turns; in lone-defector (2 conservatives + 1 aggressive) the conservatives hold a steady restraint for all 30 turns. Empirical replica of Janssen & Ostrom (2006) small-group robustness.
- Aggressive self-modulates for survival. Under the
"harvest near full capacity" nudge, the agent ramps up linearly
for ~14 turns and then drops to ~50% when the stock plateaus —
v4-flash-noThink has enough strategic cognition to read
stock_direction = stableand infer "commons in danger" without thinking mode. - Safe betrayal under inequality. In
olson_3p__lone-defector(1 rich aggressive + 2 poor conservatives), the rich captures 6× more wealth than the two poor combined while the commons survives (ratio 0.34). The rich's restraint at T24-25 keeps the system alive; the poor pay the restraint cost for 23 turns and finish marginal winners only of survival, not of wealth. This is the workshop-citable case for the AI safety angle the v0.8 charter targets.
Provider scope¶
- Decision (2026-05-20): v0.8 is built and tested against
DeepSeek only. The underlying
openaiSDK is generic (any OpenAI-compatible endpoint will physically respond), but pricing tables, model recommendations, regression suite, and examples target DeepSeek exclusively. Multi-provider support is out of scope for v0.8 and not on any roadmap before v1.x.
Polish carried over from v0.7¶
requirements.txt→pyproject.toml [project.dependencies]migration and a realuv.lock(commit52bb891) — closes the reproducibility polish flagged in v0.7.
v0.7 (2026-05-19)¶
Replication Library. The release that turns BDPD from "interesting in the abstract" into "practically useful for anyone arriving with a commons-literature bibliography in hand". Four annotated paper replicas cover the four classical axes of the empirical commons literature; the engine, the docs, and the toolchain are pinned so the same plots will re-render five years from now.
Replication Library¶
- New:
examples/scenarios/is a Quarto book with four end-to-end paper replicas, each runnable in under 30 seconds:- Hardin (1968) — Tragedy of the Commons (full): strategy contrast on a homogeneous 6-agent population, commons-ratio gap 0.000 vs 0.361.
- Olson (1965) — Logic of Collective Action (partial): privileged-member restraint delays collapse 36% and lifts welfare 27%, but tragedy still wins without reciprocation.
- Janssen & Ostrom (2006) — Empirically Based ABMs (full): group-size effect at fixed strategy, N=12 collapses 2.8× faster than N=4 (turn 5 vs 14).
- Dayton-Johnson (2002) — Inequality and Conservation (partial): inequality monotonically extends commons life (turns 11 → 16) and reduces welfare equity (57 → 33); no U-shape on the BDPD built-ins.
- New:
examples/scenarios/REPRODUCIBILITY.md(in the repo, outside the mkdocs nav since it ships next to the Quarto book it documents) is the single source of truth for the canonical toolchain (Node 20, Python 3.12,uv, Quarto ≥ 1.5), the locked dependency files (package-lock.json,requirements.txt) with SHA-256 hashes recorded at release time, and the snapshot tag policy. Single tagv0.7covers all four scenarios as a unit; per-scenario frozen tags were sketched in the design doc but rejected on the basis that the four scenarios share the same engine and freezing them as a unit is honest about the coupling. - New:
.tool-versionsat the repo root (asdf/mise format) declaresnodejs 20+python 3.12. Hosts using either tool pick the versions up automatically. - New:
package.jsondeclaresengines: { node: ">=20", npm: ">=10" }, matching the Docker base image (node:20-bookworm-slim). - Refactor: the four scenarios are organized as a Quarto book
(
examples/scenarios/_quarto.yml) with docked sidebar (left), shared style file (_style.html), and per-chapter TOC (right). Each scenario directory keeps its standalonenotebook.qmdso rendering a single chapter still works (quarto render path/to/notebook.qmd).
Plugin API¶
- v1.0 formalized.
docs/dev/plugin-api-contract.mdis no longer a stub: the contract declares Plugin API version 1.0, bound to the BDPD core version (no separate npm package in v0.7), with a formal semver policy, a deprecation policy (survival ≥ 1 major), an explicit native-plugin trust model ("trusted only, admin-loaded"), and recommended optional manifest fields (version,bdpd_plugin_api,requires, …). The contract supersedes the v0.6.5 stub and resolves the three open questions noted there (engine registry exists, manifestrequiresis optional/informational in v1.0, native plugin signing deferred to v0.8+). - Stub corrections. Two inaccuracies in the v0.6.5 stub fixed
retroactively while formalizing:
- Plugin files export
defaultan object{ type, name, ...required }, not aregister(registry)function. The loader readsmod.defaultand dispatches ontype. - The engines registry does exist (
engine/engine-registry.jswithENGINE_REGISTRY+buildEngine()). The plugin loader installs plugin engines viafactory: (modelSpec) => BaseEnginekeyed byname.
- Plugin files export
Plugin API > Deprecations¶
(none in v1.0)
Docker¶
- New:
HEALTHCHECKclause inDockerfileprobes/api/healthevery 30 s with a 20 s start-period (covers cold Node boot + plugin load). In practice the container transitionsstarting → healthywithin ~8 s on a warm host. - New:
docs/getting-started/installation-docker.mdis a quickstart additive to the from-sourceinstallation.md, not a replacement. Covers prerequisites (with a Manjaro/Arch note on the separatedocker-composeplugin package),clone → build → wait healthy → run sweep → down, devcontainer pointer for VS Code users, and explicit non-goals (multi-stage, registry push, signed plugins, GPU passthrough — all deferred to v0.8+). - Cross-link:
installation.mdnow links to the Docker quickstart at the top so users discover both paths.
Scope decisions (explicit)¶
Three originally-planned v0.7 deliverables were dropped with
explicit rationale (full discussion in the project design doc
BDPD_ultimate_edition.md, kept outside the repo by design — see
§15 entry "v0.7-dev: scope cut e chiusura imminente"):
- Colab badges → deferred to v1.1 only if requested. The
Docker quickstart now covers "click-to-run without a native
toolchain" with a comparable user experience; Colab would have
required a GitHub mirror, four
.ipynbwrappers per scenario, and a 3–5 min bootstrap cell per Colab session, with the audience-mismatch that v0.7 targets commons researchers (R / Python / NetLogo natives), not the AI/ML audience for whom Colab is the default. - Scenarios 5–7 (Vasconcelos full, Yoon, Perolat) → deferred to their gating-feature release (v1.0 cross-arena observability, v0.9 governance plugin, runner per-turn timeline export respectively). Anticipating any of these with workarounds would produce a partial replica that weakens the library's promise.
- P1–P11 / CT1–CT6 BDPD sweeps as scenarios → dropped. The
canonical sweeps already exist under
experiments/, are pinned by manifest, and are in the strict regression suite (5/5 PASS in v0.6.5 carried forward). Reformatting them asexamples/scenarios/entries would be cosmetic.
v0.6.5 (2026-05-19)¶
Provenance & manifests — the "experiment + figure + paper" packaging
gap that ate hours during paper review in v0.6 is closed. Manifest
infrastructure was bootstrapped in v0.6 (manifest MVP, PNG tEXt
embedding); v0.6.5 ships the full quartet of CLI tools that make it
usable end-to-end: register-figure (auto-wired in sweep.py),
list-orphans (technical-debt detector), audit-paper (verify a
.qmd against its declared manifests), snapshot (Zenodo/HAL
deposit bundles). One observability refactor (pipeline-based
transform dispatch) and Docker container support also land here.
Provenance tooling — the quartet¶
- New:
tools/manifest_helper.py register-figureis now invoked automatically byexperiments/sweep.pyfor every plot generated. Embedsbdpd_run_id+bdpd_manifest_sha256+bdpd_manifest_pathinto the PNGtEXtchunk, and adds the figure as anartifactentry in the parent results manifest. The--no-registerflag opts out (rarely needed). - New:
tools/manifest_helper.py list-orphansscansexperiments/results/andexperiments/figures/for files without manifest provenance or with stale manifest pointers. Six classes:result_no_manifest,manifest_unreachable_commit,manifest_missing_results,manifest_artifact_outside_repo,figure_no_provenance,figure_dangling_manifest. Exit 0 always — never auto-deletes; reports only. Git reachability check is cached per-SHA and fail-open ifgitis unavailable. - New:
tools/manifest_helper.py audit-paper <paper.qmd>walks.qmdfiles for Pandoc attributes{data-manifest="sha256:..."}on![]()figure refs, verifies each manifest exists, that the referenced figure is present in the manifest's artifact list, and that the SHA matches. Seven statuses includingFAIL_MANIFEST_SHA_STALE(a diagnostic that distinguishes "manifest doesn't exist" from "manifest was re-signed after the.qmdwas written"). Skips_*.qmdpartials by default (--include-underscoreto override). - New:
tools/manifest_helper.py snapshot --out bundle.tar.gzproduces a self-contained.tar.gzfor Zenodo/HAL deposit: manifests + result JSON + figures + sweep definitions +docs/releases/*.qmd(optional), plus a top-levelsnapshot.jsonmeta-manifest and an auto-generatedREADME.md. Refuses to bundle artifacts pointing outside the repo (warning - exclusion); refuses to overwrite without
--force.
Observability refactor¶
- Changed:
platform/observability.jsnow dispatches all observation transforms throughOBSERVATION_TRANSFORM_REGISTRYvia two helpers,compileTransforms(varSpec)andapplyPipeline(value, pipeline, ctx). The registry was public since v0.6 but the call sites still routed everything through hardcodedif (noise > 0) ... if (resolution !== 'exact') ...branches; v0.6.5 makes the registry the actual dispatch. New transforms (e.g.,quantize,delay) can now be wired in without touchingbuildObservation().commonsStockandnPlayerspaths split the pipeline into a noise filter + a rest filter to preserve the pre/post-resolution intermediates (commons ratio derivation;Math.round(Math.max(1, ...))). - Verified: 5/5 strict regression sweeps bit-identical (P1, P8,
P9, P10, P11). P9 (
observability_noise) exercises the noise pipeline heavily — the bit-identity confirms the refactor preserves seeding semantics.
Docker compose + devcontainer¶
- New:
Dockerfile— single-stagenode:20-bookworm-slimwithuv-managed Python 3.12. Three cache-friendly layers: Node deps (npm ci), Python deps (uv pip install), source.VIRTUAL_ENV PATHset sopythonresolves inside the venv without manual activation.- New:
docker-compose.yml— single servicearenaon port 3000 (overrideBDPD_PORT). Bind-mountsexperiments/results,experiments/figures,data,logso sweep outputs land on the host filesystem and survive container restarts. - New:
.devcontainer/devcontainer.json— reuses the same Dockerfile,forwardPorts: [3000]withopenBrowserOnce,postCreateCommandre-aligns deps. VS Code extensions: Python, Ruff, ESLint, Quarto. - New:
.dockerignore— excludesnode_modules, venvs, regenerable experiment outputs, build artifacts,.git, secrets.
Scope deliberately minimal — Docker is addition, not
replacement: scripts/setup.sh + uv venv remains the
primary onboarding path. A Docker-as-golden-path rewrite of
the getting-started docs is a v0.7 candidate.
Bug fixes during dogfooding¶
- Fixed:
docs/releases/v0.6.qmdfigure#fig-p1referenced a stale manifest SHA (sha256:e4e4388a...) — the.qmdwas written beforeregister-figurere-signed the P1 manifest, leaving the document pointing at a pre-figure manifest. Caught by the first run ofaudit-paper; corrected to the current SHAsha256:ed824f45.... This was the exact class of bug the quartet was designed to catch.
Smoke tests¶
- Verified: end-to-end quartet round-trip on a fresh sweep:
sweep.py→list-orphans→audit-paper→snapshot→ extract →manifest.mjs verifyon extracted manifest →audit-paperon extracted bundle. All steps PASS, including 5/5 PASS on the audit-paper round-trip from the deposit bundle. - Verified: Docker stack —
docker compose build(16 steps OK) →up -d→curl /api/health200 → 6 plugins loaded →python 3.12.13 + numpy/matplotlib/pandasinside container → Node sweep inside container writes manifest visible on host via bind mount →downclean.
v0.6 (2026-05-19)¶
Open Platform release. The four-week core platform delivery: every
behaviour-bearing component is now plugin-shaped (registry +
factory + conformance check), the regression set is bit-identical
across 5 canonical sweeps including the seeded-noise one, and the
provenance manifest infrastructure (per-run manifest sidecar, PNG
tEXt chunk embedding) is bootstrapped — full provenance tooling
ships in v0.6.5.
Plugin-shaped registries¶
- New:
platform/perturbation.js—PERTURBATION_REGISTRY+buildPerturbation(). Registry-based dispatch replaces the prior if-else chain. Built-in perturbations (regen_shock,regen_set,commons_shock,capacity_shock,threshold_shift,agent_lockout,agent_restore) registered on import; third-party perturbations register viaplugins/perturbations/<name>.js. - New:
platform/observability.js—OBSERVATION_TRANSFORM_REGISTRYmarker (full dispatch in v0.6.5). The two built-in transforms (gaussian_noise,resolution) registered on import. - New:
platform/metric.js/platform/victory.js—METRIC_REGISTRY+VICTORY_REGISTRYwith enginecontextinjected at evaluation time. Plug victory conditions can readcontext.engine.toJSON()to compose decisions. - Pre-existing:
platform/scheduler.jsSCHEDULER_REGISTRY(already shipped in v0.5.0) — left untouched. Note: the non-defaultSequentialRandomandWealthWeightedschedulers use unseededMath.random(); in regression they stay outside the strict bit-identity set until they get a seeded RNG (deferred to v0.7).
Provenance manifest (MVP)¶
- New:
tools/manifest.mjs— library + CLI (generate,verify). ~250 lines, zero deps beyond Node built-ins. Schema 1.0:run_id(uuid v4),executed_at/started_at,code(git_commit, branch, dirty, node_version),definition(path,content_sha256,human_description),seed,artifacts(each withcontent_sha256),manifest_sha256as canonical-JSON signature. - New:
experiments/experiment.js— every run now emits a<sweep>_<ts>.manifest.jsonsibling next to the results JSON. Side-by-side; no schema change to the results JSON itself. - New: PNG
tEXtchunk embedding viatools/manifest_helper.py(Python —PngImagePlugin). Three text keys per figure:bdpd_run_id,bdpd_manifest_sha256,bdpd_manifest_path. Recoverable from any lost PNG viaidentify -verbose foo.png | grep bdpd. - New: Quarto/Pandoc convention for citing figures with
provenance —
{#fig-id data-manifest="sha256:..."}. Used indocs/releases/v0.6.qmdfor all 5 platform figures.
Strict regression¶
- New:
tools/check_regression.sh— bit-identical diff of 5 canonical sweeps (P1, P8, P9, P10, P11) against a frozen baseline atexperiments/results_paper/. Runs in ~3 seconds. Sidecar.manifest.jsonfiles excluded from the diff (fix4518230). - New:
platform/observability.jsgaussianNoise(rng)accepts a seedable RNG instance. P9 (observability_noise) — which previously could not be in the strict regression set because of freeMath.random()— now reproduces bit-identically.
Plugin loader + conformance¶
- New:
tools/plugin-loader.mjs— scansplugins/andexamples/plugins/at startup; calls each file'sregister()with the relevant registry handle. Six categories: engine, perturbation, scheduler, metric, victory, observation_transform. Error reporting vialoaded+errorsarrays. - New:
tools/plugin-check.mjs— runs each plugin through a minimal conformance harness (registry contract + smoke run) and reports per-plugin pass/fail. Used by the third quickstart. - New:
examples/plugins/— one example plugin per category, intentionally simple, all 6 passplugin-check.
Documentation¶
- New:
docs/getting-started/first-experiment.mdrevised — the v0.5 single-arena walkthrough now points at the v0.6 sweep CLI for end-to-end reproducibility (Path A). - New:
docs/getting-started/extend-the-engine.md— second quickstart, walks through registering a custom engine via the plugin loader (Path B). - New:
docs/getting-started/design-an-experiment.md— third quickstart, walks through building a new sweep definition and running it (Path C). - New:
docs/releases/v0.6.qmd— first release notebook in the Quarto format that v0.6.5'saudit-paperwas designed for. Cites all 5 platform figures by manifest SHA.
Bug fixes¶
- Fixed:
docs/getting-started/extend-the-engine.md— 5 MkDocs--strictwarnings (broken anchors, missing nav entry). - Fixed:
docs/getting-started/design-an-experiment.md— Path C copy-paste fragility (two commands embedded a path that diverged from the canonical layout).
v0.5.1 (2026-05-17)¶
Pre-live audit + first packaged release. Adds the HTTP server (previously
living outside migration/), fixes one critical regression in the
perturbation engine, and consolidates terminology across paper + code.
Critical fix — perturbation engine reconnected¶
- Fixed:
regen_shock,regen_set,commons_shock,capacity_shock, andthreshold_shiftwere silently no-ops against the engine since the v0.5.0BaseEnginerefactor. The perturbation engine mutated the cachedarena.commons.*but not the engine's internal_regen/_stock/_capacity/_threshold. The frozen paper P-sweep results were unaffected (produced on 9 May, pre-regression) but live reproduction from v0.5.0 was broken. Live verification with the canonical P10 config now reproduces the documented 26-turn game length under shock ×0.10. - New:
BaseEnginesetter contract:setStock,setCapacity,setRegenRate,setThreshold. - Changed:
LogisticEngineandSenecaEngineimplement the setters with defensive clamps;platform/perturbation.jsinvokes them.
HTTP server + web dashboard packaged¶
- New:
main.js— Node.js entry point, port 3000 (env:PORT). - New:
api/server.js— Express setup, staticweb/mount. - New:
api/routes/arenas.js— full REST/SSE surface for arenas (create, join, start, tick, stream, summary, perturb, history replay, delete). Fixes a pre-existingrequire/DATA_DIRReferenceErrorin the history-replay endpoint. - New:
web/— static dashboard (arena.html,history.html,index.html,join.html, supporting CSS/JS). - New:
ArenaRegistry.deleteArena()+ read-onlydataDiraccessor. - Dropped: legacy
/api/gameand/api/agentsroutes (tied to the removedengine/game.js; never exercised by the v0.5 dashboard).
Engine + numerical hygiene¶
- Fixed:
engine/commons.js—commonsStepandcommonsStepWithReservenow clampnewStockto[0, K]viaMath.max(0, Math.min(K, ...)). Matches the paper formula verbatim and provides defence in depth against extremecommonsShockperturbations that could push the residual below zero. - Fixed:
platform/observability.js— bucket-resolution branch producedNaNforobs.commonsStockwhenresolution: 'bucket'was configured (latent: never exercised by P-sweeps).
Renames + terminology unification¶
- Renamed:
SenecaAwareAgent→RCPAgent(class), display name'SenecaAware'→'RCP'. Registry key'rcp'unchanged. Legacy identifiers retained as historical pointer indocs/platform/agents.md. - Renamed: the adaptive < conservative effect is now uniformly
reactive < conservative throughout the paper and codebase. The
AdaptiveAgentclass name in the codebase is retained (config.strategyParams.reductionFactoretc. unchanged). - Docs: companion clarifications in
docs/platform/api.md,docs/platform/architecture.md,docs/platform/agents.md.
Removed orphan files¶
- Removed:
engine/game.js(323 LOC, standalone game runner superseded byplatform/arena.js, zero importers). - Removed:
config/game-defaults_00.js(72 LOC, pre-v0.4 config snapshot, zero importers).
Paper alignment¶
- §2.1 Gate+Rank Victory now explicitly states the configurable threshold default (\(S_\text{final} > 10\), ≈ 6 % of \(K\)), removing the prior \(S > 0\) overstatement.
- §2.1 Hidden Reserve and the Forest Die marked as optional engine
layer; clarified that the P-sweeps run with
useHiddenReserve: false(deterministic), consistent with the B5 variance audit. - §A-B
B3regen-extended-sweep heading layout: added LaTeX\clearpagebefore the heading to prevent an overflow on the rendered PDF.
Smoke tests¶
- New:
tmp/smoke_test_rcp.mjs— 21-assertion regression suite covering the RCP class identity, registry mapping,decide()paths (logistic obs / Seneca obs), and the canonical Bardi signature emerging from a 3000-turn pure-ODE run.
v0.5.0 (2026-05-13)¶
Multi-engine plugin registry¶
- New:
engine/base-engine.js—BaseEngineinterface contract (step,stepOne,applyRegen,extraObservations,checkEnd,toJSON) - New:
engine/logistic.js—LogisticEnginewraps existingcommonsStep/commonsStepWithReserve; zero numerical change from v0.4.x - New:
engine/seneca.js—SenecaEnginewrapssenecaStep/buildSenecaState; exposescapitalStockandpollutionLevelviaextraObservations() - New:
engine/engine-registry.js—buildEngine(modelSpec)dispatch;ENGINE_REGISTRY = { logistic, seneca }; extensible by adding one file and one line - Changed:
platform/arena.js— replaced hard-wiredcommonsStepcalls withthis._engine.step/stepOne/applyRegen/checkEnd;commonsCapacitydefault is now engine-aware (Seneca defaults tocommonsInitial) - Changed:
platform/observability.js—buildObservationaddscapitalStockandpollutionLevel(null-safe; alwaysnullfor logistic);describeObservability(obsConfig, engineMode)filters engine-specific fields - Changed:
platform/registry.js—createArenareturnswarnings[]whenengineMode='seneca'andcommonsCapacity≠commonsInitial - Docs: new
docs/platform/engines.md— engine reference + developer guide for adding new ODE models
API key & tooling fixes¶
- New:
scripts/_env.sh—resolve_api_key(provider)with provider-specific precedence andBDPD_API_KEYgeneric fallback; sets_RESOLVED_KEY_VAR - Changed:
scripts/run_arena.sh— sources_env.sh; removed silentexport OPENAI_API_KEY="$API_KEY"re-export that broke non-OpenAI providers - Changed:
agents/bdpd_agent.py—--api-key-envCLI argument; addsAuthorization: Bearer <key>header to LLM calls when configured - Changed:
requirements.txt— addedpandas,statsmodels,tabulate(required for B5 statistical inference) - Changed:
experiments/run_all.sh—BOOTSTRAP_NandPERMUTATION_Nare now env-overridable; under--fastthey default to 1000 instead of 10000 - New:
.env.sandbox— template environment file for sandbox validation runs
v0.4.1¶
- Added supplement experiments B1–B5 (aggressor intensity fine-sweep, reductionFactor generalisation, extended regen rescue boundary, pure Seneca shock isolation, cross-sweep statistical inference)
- Added
rcpbuilt-in agent strategy — explicitly models Bardi pollution to anticipate Seneca cliff - Added
strategyParams.intensity(AggressiveAgent, default 0.5) andstrategyParams.reductionFactor(AdaptiveAgent, default 1.0) for parameterised heuristic experiments - Added
regen_setperturbation type — sets regen rate to an exact value - Added statistical inference module: bootstrap CIs, logistic regression, permutation tests
- Added experiment summary generator (
generate_experiment_summaries.py) - Added
sweep_supplement.py— Python runner for B1–B5 supplement sweeps - Added
A-B-supplement.qmdappendix to the Quarto paper - Card template updated to v5: 16:9 art box, art-clip path, dynamic badge width
CITATION.cffversion bumped to 0.4.1- All 17 experiment definitions committed under
experiments/definitions/
v0.4.0¶
- Added hidden reserve + stochastic collapse (Forest Die,
useHiddenReserve,collapseDie) - Added Seneca engine mode — Bardi (2011) three-variable R/C/P ODE model alongside logistic
- Added perturbation engine: 8 types (regen_shock, regen_set, commons_shock, capacity_shock, wealth_shock, strategy_override, observability_flip, threshold_shift)
- Added 3 pluggable schedulers: simultaneous, sequential_random, wealth_weighted
- Added 3 observability resolution modes: exact, bucket, sign
- Added code sandbox agent type — user-submitted JS with 50ms timeout and persistent private memory
- Added SSE streaming — real-time event broadcast to web clients
- Added Arena Registry & Persistence — multi-tenant arena management, JSONL logging, meta.json/summary.json on close
- Added automated report generation (
platform/reporter.js) — narrative report.md + figures - Added Stranger-King archetype (The Mule) — dual-mode cooperative/aggressive defection
- Added LLM agent middleman (
agents/bdpd_agent.py) — Flask server bridging Node.js platform to any LLM - Added LLM card game tournament runner (
agents/cards_ai_play.py, 2143 lines) — full simulator with heuristic and LLM modes - Added Card Forest Die and Seneca Clip as physical mechanics in the card game
- Added card generator tool — SVG template → PNG pipeline (
tools/card_generator/) - Added mkdocs documentation site scaffold (Material theme, 23 pages)
- Added CITATION.cff
- Gate threshold removed in card game (v0.3) — collapse only via stochastic Forest Die
v0.3.0¶
- Initial platform: logistic commons, 4 built-in agents (aggressive, conservative, adaptive, random)
- Card game v0.1: 4 archetype decks, simulation engine
- Gate+Rank victory model
- Wealth-scaled harvest capacity