A local-first CLI for competitive wargaming. It tests a specific strategic decision against grounded actors with isolated, committed moves.
Early implementation: the CLI runs from initialization through 1–3 adjudicated rounds, criterion evaluation, prediction registration, and final JSON/HTML reports. Sourced lookback outcomes and calibration scoring are also supported. The full method is in SPEC.md; unfinished capabilities are listed in ROADMAP.md.
Start with the narrative field guide: a sourced Starbucks strategy case, detailed player dossiers, downloadable setup files, and a guide to move elicitation, neutral adjudication and player rebuttals. Public company facts and hypothetical pilot assumptions are labeled separately throughout. The guide includes three completed live EDSL rounds with 30 accepted responses, original Jobs/Results packages, and a downloadable audit. The simulation supplies hypotheses and arguments, not actual pilot measurements.
Python 3.11+ on macOS or Linux:
uv venv
uv pip install -e '.[dev,edsl]'
source .venv/bin/activate
perla agent-startEDSL is optional for setup and dossier review, and required for job generation,
Results ingestion, and package audits. The EDSL extra follows the neighboring
Expected Parrot CLIs' GitHub main dependency. perla never runs inference,
chooses models, sends survey links, or manages credentials.
After initializing a game, remember it for your current directory and its children:
perla project set /path/to/game
perla project show
perla status
perla project unsetThe selection lives in .perla-project.json in the directory where you ran set.
An explicit --project overrides PERLA_PROJECT, which overrides directory
selection. Without those overrides, Perla walks up from the current directory,
using the nearest actual project or selection. A real project in the working
directory takes precedence over a selection inherited from an ancestor.
A missing selected project is an error. unset removes only the selection in
the current directory; the game is preserved. init uses its explicit path or
the current directory, not an inherited selection. The pre-initialization game
commands use the same rule.
Rebuild the standalone narrative guide after editing its source or adding live execution artifacts:
uv pip install -e '.[docs]'
python scripts/build_docs.pyTo author the experiment yourself, follow Build the game through commands.
game decision set, game criterion add and game board set build a local draft;
game show reports what is missing. Once ready, perla init registers that draft
without an input file. The guide then constructs all four dossiers with actor add
and the dossier commands, and sets review policy through named CLI options.
Initialization fixes the experiment design; further changes require a fresh game.
From an initialized game directory containing a copy of the tutorial case:
perla actor load --input case/players.json
perla dossier show home
perla statusThe readable manifest registers the cast and its full sourced dossiers in one transaction. Repeating the same load is harmless; conflicting edits are preserved by refusing replacement. This removes the need for shell loops or copying every statement. Individual authoring commands remain available for inspecting and changing the assumptions that matter.
The tutorial teaches the strategic argument and one complete round. Execution commands cover all three rounds and recovery; the reference retains the full players, prompts and transcripts.
Add facts and strategy statements directly; Perla records the edits automatically:
perla actor add home --name Starbucks --role home
perla dossier fact home h1 'Assumed pilot limit: 50 cafés.' \
--source 'Scenario design; not an announced company policy'
perla dossier incentive home 'Improve café service without losing mobile purchases (h1).'
perla dossier constraint home 'Keep the pilot within 50 cafés (h1).'
perla dossier capability home 'Test pickup windows within the pilot (h1).'
perla dossier red-line home 'Do not claim that a national rollout is authorized (h1).'
perla dossier show homeThis small example illustrates the interface; the guide builds all four complete
Starbucks-case dossiers with sourced history, financials, budgets and information
limits. Each fact needs an ID and at least one --source; repeat that flag for
multiple sources. --replace explicitly revises an existing fact. Identical
facts and statements are not duplicated. Unfinished dossiers remain drafts and
cannot enter play. A complete dossier still needs approval unless review was
explicitly delegated. Edits record the local OS account, timestamp and before/after
contents, without claiming human review. Dossiers freeze at the first move export.
The existing whole-file dossier edit interface remains available for imports.
Run from your game directory. Export a batch directly to files, choose models with ep, and import the completed Results without extracting IDs from JSON:
perla job generate moves --move 1 --output round-1/moves
ep models create --model gpt-4.1 --service openai \
--temperature 0.5 --max-tokens 16000 --output home-model.ep
ep run --jobs round-1/moves/home.jobs.ep --model_list home-model.ep \
--background --wait --timeout 900 --remote_inference_results_visibility private \
--output round-1/moves/home.results.ep
# Run the other exported actor files with their chosen models, then:
perla ingest moves --from round-1/moves--output DIRECTORY also works with adjudicate open, adjudicate resolve,
and the other job generate kinds. It creates ACTOR.jobs.ep and batch.json.
Save each ep result as ACTOR.results.ep in that directory. Paths are relative
to the shell's working directory, including when Perla uses a selected project.
ingest KIND --from DIRECTORY checks the receipt against registered jobs and
validates all pending Results in one transaction. A missing or invalid result
commits none of that batch. Repeating import skips previously accepted jobs.
Re-exporting the same batch preserves Results; a different batch or altered
package is refused. The existing --job ID --source FILE interface remains
available for individual imports, including retries with separately named files.
Use ep's job UUID with ep jobs wait or ep jobs results to resume a remote
execution after a timeout. No inference runs inside Perla.
Run from this checkout. The examples use fictional teaching data, not researched claims about real companies.
perla --project /tmp/perla-example init --input examples/marketplace-outcomes/game.json
perla --project /tmp/perla-example actor add home --name 'Home marketplace' --role home
perla --project /tmp/perla-example actor add rival --name 'Incumbent marketplace' --role competitor
perla --project /tmp/perla-example actor add startup --name 'Venture-backed entrant' --role competitor
perla --project /tmp/perla-example dossier edit home --input examples/marketplace-outcomes/home.json --by facilitator --note 'Imported fictional teaching data'
perla --project /tmp/perla-example dossier edit rival --input examples/marketplace-outcomes/rival.json --by facilitator --note 'Imported fictional teaching data'
perla --project /tmp/perla-example dossier edit startup --input examples/marketplace-outcomes/startup.json --by facilitator --note 'Imported fictional teaching data'Review the dossiers using dossier show ACTOR. After the facilitator actually
approves them, record that review:
perla --project /tmp/perla-example workflow approve dossiers --by facilitator --note 'Reviewed distinct incentives, constraints, and source grounding'
perla --project /tmp/perla-example job generate moves --move 1
perla --project /tmp/perla-example workflow validateEach generated job has a job_id, actor id, and absolute .jobs.ep package
path in the JSON response. Inspect the packages with ep inspect PATH. Execute
each externally with ep run PATH --model MODEL --output RESULTS.ep, choosing
different models for the actors. Then ingest each result:
perla --project /tmp/perla-example ingest moves --job JOB_ID --source RESULTS.epIngestion checks the project, actor, round, job id, and embedded context, requires
exactly one structured answer, and stores its model provenance. It rejects
replacement submissions. The next phase is ready_for_adjudication.
After all actors commit, optionally add scoped, attributed human testimony using
testimony add --input testimony.json --by RECORDER. The example
testimony file shows the format.
Testimony is immutable, audited, and remains separate from simulation provenance.
Supply it before opening control; the control snapshot freezes its evidence.
perla --project /tmp/perla-example adjudicate open --move 1
# Inspect the emitted control package and execute it externally, then:
perla --project /tmp/perla-example ingest rulings --job CONTROL_JOB_ID --source CONTROL_RESULTS.ep
perla --project /tmp/perla-example adjudicate show --move 1
perla --project /tmp/perla-example workflow validateOpening fails with E_UNCOMMITTED_MOVES until every expected actor has a valid
commitment. It emits one model-free control job containing all moves, action ids,
dossiers, criteria, the initial board, and testimony. Reopening returns the same
package. Choose a control model independently of the actors. Use --mode human
at opening for human control, then ingest a participant-attributed JSON submission
with kind: rulings, actor_id: control, and answer: {"rulings": [...]}.
The ruling example contains a complete
answer. The exported control prompt also includes its JSON schema. Each ruling
must name the committed action ids it resolves (m1.home.a1 refers to the home
actor's first action), all affected actors, a causal chain, a primary basis,
rationale, outcome description, confidence, and proposed board_delta. Across
the batch, rulings must address every committed action, including no-effect
outcomes. Unknown references, duplicate ids, missing evidence, and invalid
probabilities reject the entire batch without partial ingestion.
Supported evidence bases are:
dossier_fact: qualified citations such asd.home.h1must exist in the snapshot.testimony: citations such ast.procurementmust identify registered testimony.reference_class: at least two distinct named cases, each with sources, a mapping to this game, and known disanalogies. Perla checks structure, not the truth of supplied historical claims.judgment: explicit residual judgment, flagged as an escalation candidate.
These checks apply to both the primary basis and each causal link. model and
panel bases are rejected until deterministic recomputation and panel aggregation
are implemented. Resolution provenance comes from the ingested artifact; an agent
cannot label its own answer as a human panel ruling.
Optional branches contain uncertainty, at least two options (each with id,
description, probability, and board_delta), and the played option id.
Probabilities must sum to one and the chosen delta must match the ruling outcome.
Criterion links, judgment links, and branches are flagged for later escalation.
Accepted initial rulings are immutable proposals. The workflow becomes
adjudication_routing; the following sequence resolves and closes the round.
perla adjudicate route --move 1
perla job generate rebuttals --move 1
# Execute each actor package externally, then ingest each response:
perla ingest rebuttals --job ACTOR_JOB_ID --source ACTOR_RESULTS.ep
perla adjudicate resolve --move 1
# Execute the new control package externally:
perla ingest resolutions --job RESOLUTION_JOB_ID --source CONTROL_RESULTS.ep
perla adjudicate show --move 1route snapshots the escalation policy for the round. Defaults are a human-review
budget of 5, automatic eligibility at confidence 0.7, and a judgment-share warning
threshold of 0.4. Configure future unrouted rounds using
adjudicate policy set --input policy.json:
{"human_budget": 5, "auto_confidence": 0.7, "judgment_share_threshold": 0.4}Criterion-linked candidates rank first, then board impact, then ruling id.
Default impact is the number of board fields changed, a coarse proxy. An optional
route --input routing.json can supply {"flagged": ["r001"], "impact_scores": {"r001": 10.0}} to flag rulings and specify impact scores. Policy and routing
options freeze when the round is first routed. Routes are recalculated after
control resolutions so revised rulings become escalation candidates.
Selected criterion-linked rulings block closure until a human assesses them. Other escalation candidates are provisional; over-budget candidates are also provisional and explicitly marked as over budget. Unassessed candidates become recorded escalation debt when the round closes. Automatic eligibility skips escalation, but never skips rebuttals or the final approval checkpoint.
Each actor receives only its own dossier, the public decision and board, and the initial rulings affecting it. Its single response must accept or challenge every assigned ruling. A challenge names one mechanism link and cites the actor's own dossier facts. Example answer for a home-actor rebuttal job:
{"rebuttals": [{"ruling_id": "r001", "stance": "challenge", "link": "L1",
"argument": "The capacity constraint supports a narrower conclusion", "cite": ["d.home.h1"]}]}An acceptance uses stance: accept, a reason in argument, link: null, and
cite: []. Duplicate submissions and missing assigned rulings are rejected.
Control receives all rebuttals after all affected actors respond. It must affirm or revise every ruling, explain the decision, and respond to each affected actor. Example answer:
{"resolutions": [{"ruling_id": "r001", "decision": "affirm", "reason": "The original mechanism remains supported",
"responses": [{"actor_id": "home", "reason": "The narrower interpretation does not change the outcome"},
{"actor_id": "rival", "reason": "Acceptance recorded"}, {"actor_id": "startup", "reason": "Acceptance recorded"}],
"revision": null}]}A revision supplies the full replacement ruling in revision. It must preserve
the ruling id, resolved actions, affected actors, and existing criterion links.
Initial rulings and their provenance stay intact; final rulings, control reasons,
rebuttals, assessments, and workflow events are stored in .perla/review/move-N.json.
After actual human review, record any required individual assessments and the round's final approval:
perla adjudicate assess --move 1 --ruling r001 --by REVIEWER --note 'Reviewed the revised mechanism against the evidence'
perla workflow approve rulings --move 1 --by FACILITATOR --note 'Approved the final rulings, proposed board, and escalation debt'
perla adjudicate close --move 1
perla job generate moves --move 2An assessment confirms the current final ruling and is labeled
individual_review; it does not become panel evidence or overwrite simulation
provenance. Hosted panels and voting aggregation remain pending. Direct human
assessment is the currently implemented way to satisfy blocking escalations.
Board deltas replace top-level fields. If rulings propose different values for
one field, approval and closure refuse to choose silently. Inspect
adjudicate show, then use adjudicate reconcile --move 1 --by REVIEWER --input choices.json with a ruling and rationale for every conflicting field:
{"choices": {"home_qualified_teams": {"ruling_id": "r002", "reason": "This mechanism determines final capacity"}}}Approval binds to the exact final rulings, routing/assessments, reconciliations, and proposed board. Later assessment or reconciliation changes invalidate it. Closure atomically locks the review, versions the board, and advances the game; the next actor jobs receive only that public board and their own dossier. Closed rounds cannot be altered. Testimony may be added for subsequent rounds without changing older control snapshots. Asynchronous reopening and propagation of later panel outcomes are not yet implemented.
init --delegate rulings explicitly waives the round approval checkpoint and is
recorded at closure. It does not waive blocking human assessments. Once all
registered rounds close, the phase is ready_for_endgame.
Optionally generate an isolated novelty baseline, then evaluate the completed game through another EDSL package:
perla job generate novelty
# Execute externally and ingest its findings:
perla ingest novelty --job BASELINE_JOB_ID --source BASELINE_RESULTS.ep
perla job generate endgame
# Execute externally and ingest its assessment:
perla ingest endgame --job EVALUATION_JOB_ID --source EVALUATION_RESULTS.ep
perla endgame show
perla endgame
perla report context
perla report html
perla export uncertaintiesFinalization requires every criterion to have a cited verdict, every insight
to have an owner and due date, and every committed move and final ruling to have
a dated prediction with explicit resolution criteria. Human corrections use
endgame revise; unsupported criteria remain unevaluated and require explicit,
attributed gap acceptance before reports render. Missing insight assignments can
also be waived explicitly. No baseline means novelty stays unassessed.
See the endgame guide for schemas, revisions, waivers,
provenance, and report contracts. Exports default to .perla/reports/ and can be
regenerated deterministically from canonical state.
After real outcomes become observable, record sourced evidence and score the probabilities registered at endgame:
perla lookback open --by reviewer --note "Scheduled follow-up"
perla lookback show
perla lookback record --input outcomes.json --by reviewer --note "Reviewed dated evidence"
perla lookback score --as-of 2028-01-01
perla report lookback
perla report htmlOutcome corrections retain history and invalidate the current score. The original registry stays frozen. Reports include Brier scores, calibration plots by population and basis, shared-event comparisons, and explicit missing-evidence counts. Probabilities are never inferred from control confidence. See the lookback guide for the input schema and scoring contract.
From the installed checkout, use a new output directory:
python examples/marketplace-outcomes/demo.py --project /tmp/perla-control-demo
perla --project /tmp/perla-control-demo adjudicate show --move 1This explicitly fictional three-round demo generates genuine EDSL Jobs and synthetic Results
using the test model identifier, without executing inference. It records the
dossier and ruling approval delegations, a zero human-review budget, scripted
testimony, move commitments, rebuttals, control resolutions, and closed boards.
Escalation debt remains recorded. The demo also runs a baseline and evaluation,
explicitly accepts the replication and margin measurement gaps, registers 12
predictions with teaching probabilities, and records scripted future observations
from reality.json. It scores ten predictions, preserves two unresolved or
unresolvable outcomes, and writes final JSON/HTML reports with calibration plots.
It ends at complete and audits the whole project; no real-world findings are implied.
Supply actual researched facts as {"facts": [{"id": "f1", "claim": "...", "sources": ["source URL or documentary reference"]}]}:
perla job generate dossiers --actor rival --facts facts.json
# Execute the emitted package externally, then:
perla ingest dossiers --job JOB_ID --source RESULTS.epThe job supplies incentives, constraints, capabilities, and red lines. Ingestion
preserves the original supplied facts and sources. A human can supply or revise
a complete dossier through dossier edit with author and rationale; edits have
before/after audit records and invalidate the previous review approval.
Set actor set-mode ACTOR human before move generation. This milestone exports
the structured EDSL survey job but does not yet generate hosted survey links.
Use ingest ... --human-input to ingest a local JSON answer:
{
"project_id": "PROJECT_UUID",
"job_id": "job-JOB_HEX",
"actor_id": "home",
"kind": "moves",
"move": 1,
"participant_id": "facilitator-alice",
"answer": {
"actions": ["Launch a bounded pilot"],
"rationale": "Limit exposure while measuring competitor response",
"resource_commitments": ["Assign two delivery teams for six months"],
"expected_responses": ["The incumbent discounts comparable contracts"]
}
}Human provenance remains labeled separately from simulation results. Recording names and approvals is a facilitator assertion, not identity authentication.
- Every invocation emits one versioned JSON envelope; errors exit nonzero and
include
code,message, andremediation.--humanrenders it with Rich. Put global flags (--project,--human) before the command. .perla/stores canonical JSON. Writes are serialized with a file lock; a write-ahead journal recovers interrupted multi-file commits on next access.- Decision and criteria lock at initialization (the stricter draft-spec rule). Input validation requires explicit offer, segment, pricing, timing, scope, and observable criterion thresholds; it cannot prove semantic testability.
- Review requires one home and two to five external actors, all with sourced dossiers. Identical incentive/constraint sets are rejected. Deeper mirror-imaging checks remain the facilitator's responsibility.
init --delegate dossiersexplicitly records a review waiver. Without it, approval binds to the exact actor/dossier snapshot.- Actor roster, modes, and dossiers freeze at the first move export. Repeating generation returns existing packages. Each move package contains only its own dossier, public decision, and previous public board, plus routing metadata.
workflow validatechecks actual exported package hashes as well as manifests, registration integrity, committed content hashes, lineage, testimony, frozen control snapshots, review records, approvals, deterministic board outcomes, evaluation snapshots, and finalized insight/prediction records. Same-model simulations warn;--strictmakes warnings errors.- Local files remain inspectable by the facilitator. Isolation applies to generated actor jobs; hashes detect accidental edits, not an adversary able to rewrite both records and hashes. Keep private state out of actor access.
pytest -q
ruff check .The tests include actual EDSL package round trips without model calls. EDSL integration tests skip explicitly if the optional dependency is absent.
