Skip to content
parley.

Guide

One fair decision for a group, when nobody wants to say where their limit is.

Picking a date, splitting a bill, choosing a flat, sharing out the work: the argument usually stalls on a limit somebody has but would rather not explain. This page walks one such decision through the repository's real-decision flow. Each person states their red lines and preferences alone at the keyboard, the engine drops every option that crosses anyone's red line, picks the option whose least-happy person is happiest, and writes a receipt that each person can check against their own rules and whose arithmetic anyone can re-check.

The run on this page was made with version 0.2.0: theparley-consensus wheel from PyPI, plus the v0.2.0 checkout of the repository for the case files under examples/cases/, the flow script examples/real_decision.py and the verifier, none of which are in the wheel. Companion guides: fair multi-agent negotiation, red lines for LLM agents, running Parley as an MCP tool and verifying a decision receipt.

What fair means here

Three rules, each one enforced in parley/consensus.py rather than left to anyone's judgement:

The three rules the engine applies to a group decision
A limit is a vetoA red line is a rule about one attribute, such as cost_each <= 250. An option that breaks anyone's red line is out, whatever everyone else thinks of it. It is never traded against a preference. It is also self-reported: the engine enforces what you type and cannot tell a real limit from a tactical one.
Lift the least-happy personAmong the options nobody vetoed, each person's preferences give a score from 0 to 1. The winner is the option whose lowest score is highest. Ties go to the higher total. This is not a vote and not a sum.
No option means no decisionIf every option breaks someone's red line, the result is a deadlock, written into the record as such. The engine never picks a least-bad option over somebody's limit.

1. Agree the options in public

The option list is the one thing everyone must see identically, so it comes from a file the group looks at together. The repository ships four ready-made ones; each option is a flat set of attributes that somebody might refuse over.

The four case files under examples/cases/ and the argument each encodes
FileThe argument it encodes
pick-a-date.jsonWhich weekend to go: weeks away, cost each, days off needed, weather risk, who misses out.
split-the-bill.jsonHow to split a shared cost when people used it unevenly: each share, admin effort, whether receipts are needed.
flatshare.jsonWhich flat to take: rent per person, commute, bedrooms, outdoor space, pets, furnished.
who-does-what.jsonWho takes which part of the work: hours per person, who is on call, whether swaps are allowed, extra cost.

This page uses pick-a-date.json, five weekends for a trip:

The five options in examples/cases/pick-a-date.json with their attributes
idweeks_awaycost_eachdays_off_neededweather_risksomeone_misses
oct-third32101lownone
oct-fourth41801lowc
nov-second71400mediumnone
dec-first122602highnone
next-week13202lowb

The command, from a checkout of the repository, with three names and manual entry (no model, no network):

python examples/real_decision.py --options examples/cases/pick-a-date.json \
    --participants Ana,Bob,Cara --manual --receipt receipt.json

2. Each person states limits and preferences alone

The flow prints blank lines and calls one person at a time. A red line is typed asattribute operator value; a preference asattribute value weight. A person's score for an option is the weight of the preferences it matches divided by the weight of all their preferences, so it sits between 0 and 1. Nothing typed here is written into the record, and the flow does not show it to the next person; the terminal's scrollback can, so clear it or use a fresh terminal between people. The engine takes each rule as given: it cannot tell a real limit from a tactical one.

The three positions below were typed for this page by one person at one keyboard, so they can be shown. No real group has used Parley this way yet. In a real run each person sees only their own.

Ana
  red line >  cost_each <= 250
  prefer >    someone_misses none 1
  prefer >    cost_each 140 1

Bob
  red line >  someone_misses != b
  red line >  days_off_needed <= 1
  prefer >    someone_misses none 1
  prefer >    days_off_needed 0 1

Cara
  red line >  someone_misses != c
  prefer >    weather_risk low 3
  prefer >    weeks_away 7 1

Ana will not pay over 250. Bob will not be the one who misses the trip and cannot take two days off. Cara will not be the one who misses it either. Everything else is a preference, and a preference only ranks the options that survive everyone's red lines.

3. What the record holds, and what wins

The engine asks each person's sheet about each option and records only the answer: acceptable or not, the score, and the word ok orred-line. These are the five entries from the receipt written by the command above. The scores are exact; none were rounded.

oct-third    (weeks_away 3, cost_each 210, days_off_needed 1, weather_risk low, someone_misses none)
  Ana   acceptable=true   reason="ok"        score=0.50
  Bob   acceptable=true   reason="ok"        score=0.50
  Cara  acceptable=true   reason="ok"        score=0.75

oct-fourth   (weeks_away 4, cost_each 180, days_off_needed 1, weather_risk low, someone_misses c)
  Ana   acceptable=true   reason="ok"        score=0.00
  Bob   acceptable=true   reason="ok"        score=0.00
  Cara  acceptable=false  reason="red-line"  score=0.75

nov-second   (weeks_away 7, cost_each 140, days_off_needed 0, weather_risk medium, someone_misses none)
  Ana   acceptable=true   reason="ok"        score=1.00
  Bob   acceptable=true   reason="ok"        score=1.00
  Cara  acceptable=true   reason="ok"        score=0.25

dec-first    (weeks_away 12, cost_each 260, days_off_needed 2, weather_risk high, someone_misses none)
  Ana   acceptable=false  reason="red-line"  score=0.50
  Bob   acceptable=false  reason="red-line"  score=0.50
  Cara  acceptable=true   reason="ok"        score=0.00

next-week    (weeks_away 1, cost_each 320, days_off_needed 2, weather_risk low, someone_misses b)
  Ana   acceptable=false  reason="red-line"  score=0.00
  Bob   acceptable=false  reason="red-line"  score=0.00
  Cara  acceptable=true   reason="ok"        score=0.75

Three options are out: oct-fourth crosses one of Cara's lines, dec-first and next-weekcross lines of Ana's and Bob's. The record says red-lineand does not name the rule. It does show who vetoed what, and the scores next to the vetoes: with five flat options, someone reading that Cara rejected the only option on which she misses out can guess her rule, and SECURITY.md says so (scores are unmasked, leaking preference ordering and the feasible region).

Two options survive. nov-second has the higher total (2.25 against 1.75) and is the favourite of two people out of three. It loses. Its lowest score is Cara's 0.25; the lowest score onoct-third is 0.50. The option whose least-happy person is happiest is oct-third, and that is the decision. A vote or a sum would have picked the other one.

4. Each person checks, then accepts

Before anything is final, the flow again calls each person alone, shows them the decision and the record hash, and replays the decision against their own sheet on their side. The line that says whether a red line was crossed is shown only to that person. The code refuses an acceptance when the sheet it was given rejects the decision. This is what Ana saw, exactly as printed:

--- Ana, review the outcome privately. ---

Decision: {"cost_each": 210, "days_off_needed": 1, "id": "oct-third", "someone_misses": "none", "weather_risk": "low", "weeks_away": 3}
Record hash: 6b7aaf34e4b5b0cef24b1849348f19d5c89eab63f0d59aa0b3d9079d91741877

Replayed against your own sheet: no red line of yours was crossed.

Do you accept this outcome? yes / no >

All three answered yes. The closing report and the receipt:

==================================================================
  Which weekend do we actually go?
==================================================================
Decision: {"cost_each": 210, "days_off_needed": 1, "id": "oct-third", "someone_misses": "none", "weather_risk": "low", "weeks_away": 3}

Record hash          6b7aaf34e4b5b0cef24b1849348f19d5c89eab63f0d59aa0b3d9079d91741877
Max-min honest       yes   (the announced option really is the max-min one)
Unanimously accepted yes   (every participant accepted this exact record)

  Ana              accepted
  Bob              accepted
  Cara             accepted

Receipt written to receipt.json

The replay checks the sheet the person submits, nothing more: a rule they left out is not checked, and the acceptance records the owner, the decision, the hash and a boolean, not why the rules held.

When nothing clears everyone's limits

The same case was run a second time with two extra red lines: Bob addedcost_each <= 200, which removesoct-third, and Cara addedweeks_away <= 4, which removesnov-second. Every option now crosses somebody's line. The flow skips ratification, since there is nothing to accept, and prints this:

==================================================================
  Which weekend do we actually go?
==================================================================
DEADLOCK. No option cleared every participant's red lines.
This is an honest outcome, not a failure: forcing an agreement that crosses
somebody's red line is exactly what this tool exists to prevent.

Record hash          28685bcd5bc067ca005db387f858ee548af9857be63df377510d43fb79d2949d
Max-min honest       yes   (the announced option really is the max-min one)
Unanimously accepted no   (every participant accepted this exact record)

Receipt written to receipt-deadlock.json

The deadlock receipt carries "status": "deadlock","decision": null, every verdict, and no acceptances. The verifier passes it, because a deadlock over a record in which no option is acceptable to everyone is the honest result. The receipt records, per option and per person, whether that person's red lines allowed it, so everyone can see who blocked what. It does not say which rule did it. The way forward is a new option list, not a different rule.

The receipt anyone can re-check

The receipt holds the title, status, decision, a SHA-256 hash over the record, the roster, every acceptance and the full masked record. The verifier in the repository rebuilds the hash, recomputes the winner from the recorded verdicts and checks that every acceptance is bound to exactly this hash and this decision. Its output on the first run. The script ends every run with a list of what it checked and what it does not prove; that list is cut here to two of its lines:

$ python scripts/verify-receipt.py receipt.json
Parley receipt check: Which weekend do we actually go?
  status agreed   decision {"cost_each": 210, "days_off_needed": 1, "id": "oct-third", "someone_misses": "none", "weather_risk": "low", "weeks_away": 3}

Record
  transcript hash   claimed    6b7aaf34e4b5b0cef24b1849348f19d5c89eab63f0d59aa0b3d9079d91741877
                    recomputed 6b7aaf34e4b5b0cef24b1849348f19d5c89eab63f0d59aa0b3d9079d91741877   OK
  status/decision   match the finalized record   OK
  max-min honest    yes (recomputed from the recorded verdicts)   OK
  roster            Ana, Bob, Cara (record: Ana, Bob, Cara)   OK
  verdict sigs      none

Acceptances
  Ana              accepted  bound to this record      signature unsigned   OK
  Bob              accepted  bound to this record      signature unsigned   OK
  Cara             accepted  bound to this record      signature unsigned   OK
  unanimous         yes (receipt says yes; tamper-evidence only: unsigned)   OK

Result: VERIFIED. Every claim in this receipt re-derives from its own record.

What this does NOT prove
  ...
  - that the roster is the right one: the participants list is itself coordinator-written.
  ...
  - that nobody's red line was crossed: no private sheet is in the receipt or read here.
  ...

Someone who was not in the room can run this and learn that the announced weekend really is the max-min pick over the recorded verdicts and that all three acceptances are bound to this exact record, but not that Ana, Bob and Cara gave them: every field of an unsigned acceptance is public, so that tier authenticates nobody. What each check proves and what it does not is the subject of the verification guide.

What stays private, and what does not

  • The rules stay out of the record. No red line and no preference is written anywhere. A verdict saysred-line, never which line, and the acceptance says yes or no, never why.
  • The scores and the vetoes are public. SECURITY.md states it as a known gap: "Scores are unmasked. The soft cardinal score is public, leaking preference ordering / feasible region". The record shows that Cara scored oct-third at 0.75 andnov-second at 0.25, and that she alone rejectedoct-fourth. The rule is not named, but with a few flat options the pattern of vetoes and scores lets the others guess it.
  • Self-reported. The engine enforces what each person types. It cannot tell a real limit from a tactical one, and a person who overstates a red line to steer the outcome is not caught. Strategic misreporting is out of scope (SECURITY.md lists collusion and strategic-misreport resistance as a research track).
  • The terminal keeps scrollback. Between people the flow prints forty blank lines; it does not clear the screen. Scrolling up can still show the previous person's input. Use a fresh terminal per person, or clear the scrollback before handing the keyboard on.
  • One keyboard means one process. In the pass-the-keyboard flow all three sheets live in the same process on the same machine. The privacy is from the other people and from the record, not from whoever runs that machine. Separate processes per owner exist in the repository as examples/run_env.py, one HTTP server per party.
  • The receipt is unsigned by default. Verdicts and acceptances carry sig: null. The hash makes a later edit visible to anyone who kept the hash from the ratification screen; it does not say who accepted. The roster is written by the same process that writes the record. --sign adds Ed25519 signatures with keys made by that same process, which proves the file was not altered afterwards, not who signed.
  • The replay checks what it is given.A person's acceptance is checked against the sheet they submitted. A rule they did not type, or typed wrong, is not checked. The flow validates the operator and the shape of each rule as it is typed; it cannot know what the person meant.

Questions

How do we pick a fair option when everyone has private limits?

Agree the list of options in public, then let each person state two things in private: red lines, which are rules an option must satisfy, and preferences, which only rank the options that survive. The engine drops every option that breaks anyone's red line and, among the rest, picks the option whose least-happy person is happiest. The record shows each person's masked verdict per option (acceptable or not, a score, and the word ok or red-line) and never which rule was behind it. Each person then replays the winner against their own rules on their own side before accepting. The limits are self-reported: the engine enforces what each person types and cannot tell a real limit from a tactical one.

How is this different from a vote or from adding up everyone's scores?

A vote lets a majority pick an option that crosses one person's limit. A sum lets two very happy people outweigh one very unhappy one. Here a red line removes an option before any score is weighed, and among the survivors the option with the highest minimum score wins, with the total only breaking ties. In the run on this page the option with the highest total (2.25 against 1.75) lost because its lowest score was 0.25 against 0.5.

What stays private, and from whom?

The record never holds a red line or a preference, only verdicts. What leaves a person's side is, per option, acceptable or not, a score between 0 and 1, and the word ok or red-line. The rule is not named, but SECURITY.md is plain about the rest: scores are unmasked, the soft cardinal score is public, leaking preference ordering and the feasible region. With a few flat options the pattern of a person's vetoes and scores lets the others guess the rule behind it. In the pass-the-keyboard flow all three positions live in one process on one machine, so this is privacy from the other people and from the record, not from whoever operates that machine. For separate processes per owner, the repository's examples/run_env.py runs each party as its own HTTP server.

What do we do when the result is a deadlock?

A deadlock means no option on the table satisfied everyone's red lines. The receipt still records every verdict and the deadlock itself, and the verifier confirms the deadlock is honest. The way out is a new option list: add an option, change the numbers, or let a person loosen a rule they set, then run again. The engine will not pick an option over someone's red line to avoid the deadlock.

Put your own argument in a file and run it.

The demo replays pre-computed runs of the same engine, no signup. The repository is Apache-2.0; copy one of the four case files, change the numbers to your own, and run examples/real_decision.py with the people in the room.

Back to the overview · Next: what a red line is, exactly · Then: verify the receipt afterwards