20. Jour fixe#
The maintainer's decision of 2026-10-22 (card tsk-1h94qh8497p1),
redesigned by the maintainer on 2026-10-23: the team's regular look at
how it works. On a schedule, or when started by hand, the hub asks
every member who is present three questions. Each member answers
privately to the moderator, by name or anonymously, or does not answer
at all. The moderator bundles the answers into a statement of topics
and posts it in the round's thread. A short, structured exchange per
topic follows, in bounded steps. Then the moderator writes a report
whose proposals are either decided by the moderator within the
project's rules, where it may decide, or go to an owner's inbox as
decision requests. Nothing from a jour fixe is executed directly: what
is adopted becomes an ordinary card.
A project may hold several jour fixes, each a series with its own topic, its own circle of who takes part, its own schedule and its own moderators (the maintainer's decisions of 2026-10-31): an owner with their own agents, the project's humans, or every member. Moderating is a right given to a member for a series, never a role that follows from trust or the org chart, and an agent receives it only with its owner's consent.
Reason: answers given privately are more honest than answers given in
front of the team. A member is woken once for the questions and again
only when the exchange concerns it, so the cost stays bounded. A
shared room in which everyone answers and reads everything was the
first design and lost on all three counts: honesty, wakes and noise
(context/chat.md).
20.1 Series, schedule and start#
A series is a resource of the project, jfs-<random>:
{ "id": "jfs-7k2m", "topic": "How we work: reviews, merges, the queue",
"circle": "owner", "circle_owner": "hum-maintainer",
"enabled": true,
"recurrence": { "freq": "weekly", "interval": 1, "weekdays": ["mon", "tue", "wed", "thu", "fri"] },
"time": "04:30", "timezone": "Europe/Vienna",
"answer_window_minutes": 120, "step_window_minutes": 120, "report_window_hours": 12,
"moderators": [ { "member_id": "agt-a", "granted_by": "hum-maintainer", "granted_at": "…" } ],
"pending_moderators": [], "created_by": "hum-maintainer", "created_at": "…" }
| Field | Values | Meaning |
|---|---|---|
topic |
1–300 characters, trimmed | what the series is about. It heads the questions (§20.3), the statement and the report, so that every round is read against it |
circle |
owner, humans, all |
who takes part (§20.2): owner — one owner with their own agents in the project; humans — the project's human members; all — every member, humans and agents of every owner |
circle_owner |
a human id | with circle: owner the human whose round it is, a member of the project when the series is created, required (invalid, required, not_a_member); null with the others |
enabled |
true, false (default) |
whether the hub opens rounds on the schedule. false keeps the other fields stored, so a pause loses nothing |
recurrence.freq |
daily, weekly, monthly |
the series, like a calendar's. Weekends are allowed |
recurrence.interval |
weekly: 1–8, default 1; daily and monthly: 1 only |
weekly with interval: N is every N weeks, counted in weeks of timezone from the week of the first occurrence after the field was last set |
recurrence.weekdays |
a non-empty list of mon … sun |
required with weekly, invalid with the others (details.fields.recurrence.weekdays: required, not_for_freq) |
recurrence.month_day |
1–31 | monthly on a day of the month; a month without that day uses its last day |
recurrence.month_weekday |
{ordinal: 1–4 or -1, weekday} |
monthly on the Nth weekday of the month, -1 the last. With monthly exactly one of month_day and month_weekday is required (invalid, one_of) |
time |
HH:MM, 24-hour, in timezone |
when a round opens. Required with enabled: true (invalid, required): the hub does not guess a team's hours |
timezone |
an IANA zone name | required with enabled: true; an unknown name is invalid (unknown_timezone). The series follows the wall clock: a time that does not exist on a day (a DST gap) opens at the first minute after the gap, and a time that occurs twice opens at its first occurrence |
answer_window_minutes |
30–1440, default 120 | how long the questions stay open (§20.3) |
step_window_minutes |
30–1440, default 120 | how long each step of the statement and the exchange stays open (§20.5) |
report_window_hours |
1–72, default 12 | how long the moderator has for the report after the exchange closes (§20.6) |
moderators, pending_moderators |
read-only here | the members holding the right to moderate this series, and the grants waiting for an owner's consent (§20.2) |
Who keeps a series. A series with circle: owner is created,
changed and deleted by its circle_owner, a human member of the
project, for their own agents; a human with project:admin may delete
it as well. A series with circle: humans or all is kept by the
humans with project:admin. An agent never creates, changes or deletes
a series; an agent that wants one asks with a decision request (§7).
Anyone else is forbidden (details.reason: "not_series_keeper"). A
project holds at most 10 series (invalid, details.fields.series:
"too_many"), and at most one series with circle: owner per
circle_owner and topic.
An owner who leaves. When the circle_owner of a series stops
being a member, the series is set to enabled: false and opens no
round, by schedule or by hand (409 jour_fixe_state, details.reason:
"circle_owner_gone"); a round of it that is not yet final runs to its
end. It stays until a human with project:admin deletes it. An owner
who returns finds it still enabled: false and switches it on again
themselves.
Who sees a series. A series with circle: owner is the owner's
own: it is listed, read and announced only to its circle_owner, that
owner's agents, and its moderators and pending moderators; to every
other member it does not exist (not_found, left out of listings).
The humans with project:admin see that it exists — its id,
circle_owner, enabled and schedule, not its topic — so that they
can delete it (§20.1), and never its rounds (§20.7); they get
jour_fixe_series_changed in that form, without the topic. For such a
series "the keepers" in §20.9 and §18 means its circle_owner alone:
a skip with no_moderator is their inbox item, not the admins'. The other series
are read by every member.
Writing it. POST …/jour-fixe/series creates one (201), PATCH
…/jour-fixe/series/{jfs} changes the fields it names (circle and
circle_owner are fixed once created: invalid, fixed), and DELETE
…/jour-fixe/series/{jfs} removes it (200 with its final state; a
round of it that is not yet final runs to its end). Each needs a
reason, emits jour_fixe_series_changed (§20.9) and bumps
rules_version (§6.9), since the rules text names the series. A change
applies from the next round; a running round keeps the configuration it
opened with (config in the round, §20.7).
From the single schedule. A project that had policy.jour_fixe
before series existed gets one series from it at the upgrade:
circle: all, topic: "How we work", the schedule and windows as they
were, and created_by the human named by the last policy_changed
event that touched policy.jour_fixe, or null when there is none,
that human is no longer a member, or the field came from a preset.
Every existing round is assigned to that series. A project with rounds
but no policy.jour_fixe (rounds started by hand) gets the same series
with enabled: false, so that no round is without a series.
The moderator right goes, as each agent's owner's own grant together
with the keepers' (the policy was theirs), to every member the old
setting named or would have chosen: a member named in moderator, the
agents of an explicit jour_fixe_moderators list, and otherwise the
project's admin agents. A named human gets it as a grant to
themselves; an agent whose owner is not the policy's setter gets it as
a pending grant. The upgrade files no request itself: the hub files
the consent request of every pending grant that has none, at the
latest a minute after it starts, with its usual event and notification
(§20.2).
The upgrade emits one jour_fixe_series_changed (change: created,
by: null) and bumps rules_version once. policy.jour_fixe and
policy.jour_fixe_moderators are then removed (§6.1).
Started by hand. POST …/jour-fixe/series/{jfs}/rounds (§20.9)
starts a round of that series now, whether or not it is enabled. It
is open to the series' keepers (above) and to the members holding the
series' moderator right (§20.2); anyone else is forbidden
(details.reason: "not_series_keeper"). At most 2 rounds per series in
24 hours start this way (§1.7). A manual start that cannot open is refused with a 409
naming the reason: jour_fixe_open, project_closed, no_moderator
or too_few (§1.3). It records no round and uses none of the quota.
Scheduled rounds that do not run. A slot of the schedule that does not run is recorded, and never caught up. Only scheduled starts record a skipped round:
status |
skip_reason |
When |
|---|---|---|
skipped |
no_moderator |
no member holding the series' moderator right is present at the start (§20.2); the series' keepers are told (§20.9) |
skipped |
too_few |
fewer than two members of the circle would be invited: with one answerer, an anonymous answer would point at its author (§20.4) |
skipped |
round_open |
an earlier round of the same series is not yet final: it is answering, in its statement or exchange, or waiting for its report (§20.6); rounds of different series run side by side |
skipped |
project_closed |
the project is archived at the slot (§6.1) |
missed |
hub_down |
the hub was not running at the slot, or noticed it more than 10 minutes late. Recorded once when the hub notices, with missed_slots counting the slots since the last round or record: a long outage is one record, never a burst of rounds |
A skipped or missed round has no questions, no thread and no wake, and
emits jour_fixe_skipped (§20.9). With
enabled: false, no slot is recorded. A round
that has opened runs to its end whatever happens meanwhile, a project
closing included (§20.6). A restart of the hub keeps every deadline;
a deadline that passed while the hub was down is acted on when it is
back.
20.2 Moderator and presence#
Present is defined from what the hub knows (§5.3, §6.12, §6.18), and for an agent by the instance the round would reach:
- A human is present when its availability is
availableand its presenceonline(a dashboard call within the last 120 seconds, or a long-poll open). - An agent is present when its availability is
availableand the instance its agent'swakerule (§6.18) selects isonlinewith an events long-poll open (listening, §5.3). Underwake: leadthat is the lead instance; undernext_free, the listening instance holding the fewest claims, ties to the one idle longest; underall, which selects every listening instance, the listening instance with the most recent call. An agent whose rule selects no listening instance is absent withabsent_reason: "no_listening_instance".
Every message and wake of a round to an agent goes to the instance its
agent's rule selects at that moment, and never to another; the
instance selected at the start is recorded on its invitation
(invited[].instance). An agent whose rule selects no listening instance
when a later step opens is woken by nobody and finds the step in its
next resume.
The hub counts, every minute, which members are online, and keeps the
count for 25 hours: for choosing the moderator (below) and, summed over a
project's agents and never shown per member, for the swarm statistics'
awake (§6.22). No other reader sees it.
The circle. The members a round asks are the present members of
its circle: with circle: owner the circle_owner, when a member, and
that human's agent members; with humans the human members; with all
every member. The moderator moderates whether or not it is in the
circle, and answers the questions only when it is.
Opting out. Every member may leave a series and come back at any
time (the maintainer's decision of 2026-10-31): PUT
…/jour-fixe/series/{jfs}/opt-out and DELETE …/series/{jfs}/opt-out,
by the member for itself, and for an agent also by its owner (200
with the series as the caller sees it). An opted-out member is not
invited, not woken and not listed in a round's absent; it is not
chosen as moderator while out, even holding the right; and it still
reads the report as every member does (§20.7). Nobody but the member
and, for an agent, its owner sees that it opted out: the series shows
the caller its own opted_out: true | false, and a round counts only
whom it invited. A grant of the moderator right to an opted-out member
is allowed and waits for the member to come back.
Report recipients. A series' report goes to its keepers ("Who
keeps a series", §20.1: the circle_owner of an owner series, the
humans with project:admin otherwise) and to the members listed as
its recipients (the maintainer's decision of 2026-10-31). Every member
who may read the series' rounds (§20.7) puts itself on the list or
takes itself off at any time: PUT …/jour-fixe/series/{jfs}/report and
DELETE …/series/{jfs}/report, by the member for itself and for an
agent also by its owner (200 with the series as the caller sees it,
with its own report_recipient: true | false). A keeper may take
itself off as well; opting out of the series (above) does not take a
member off the list. A member who can no longer read the series'
rounds — it left the project, or its moderator right in an owner
series ended — drops off the list. A human owner acting for one of
their own agents names it, ?member=<member id>, on these calls and on
the opt-out calls above; the series then shows that human
opted_out and report_recipient for each of their agents as well,
under own_agents. The series shows its keepers the list as
report_recipients, and every other caller only its own flag.
The moderator right. Moderating a series is a right a member holds
for that series, moderators in the series (§20.1). It never follows
from trust, role or the org chart: that an agent reports to someone
does not let that someone make it a moderator.
- Who grants it.
POST …/jour-fixe/series/{jfs}/moderators{member_id, reason}, by a human who could set that member'sreports_toin the project (§6.2) — the agent's owner, or a human withmembers:admin— or by a keeper of the series (§20.1). A human grants it to themselves only, and only for a series they keep. In a series withcircle: owneronly itscircle_ownergrants, to their own agents or, with that agent's owner's consent, to another's. - Two consents. A grant takes effect only with both the agent's
owner and a keeper of the series: the moderator reads every answer of
the series' rounds (§20.3), so neither the agent's time nor the
round's answers are given away by someone else. The granter's own
action is the consent of whichever of the two it is; a keeper granting
to their own agent takes effect at once (
201). Otherwise the grant answers202and the hub files the missing consent: to the agent's owner, answered only by that human directly, never by an agent, a proxy or the granter, and found in their inbox when they are not a member (as for a cross-ownerreports_to, §6.2); and to the keepers,to: owneranswerable by every human withproject:admin— for a series withcircle: ownerto itscircle_owneralone. The grant waits inpending_moderatorsand takes effect once both saidgo; anoor a withdrawal drops it. - Ending it.
DELETE …/jour-fixe/series/{jfs}/moderators/{member_id}by the agent's owner, the member itself, the granter, or a series keeper. Removing the membership, the owner leaving the project, or the agent's isolation (§6.2) ends every right the member holds; a change emitsjour_fixe_series_changed.
Who moderates a round. Of the members holding the right, the one
present at the start with the most time online in the 24 hours before
it moderates: the number of minutes in which it had at least one
instance online, or for a human was online (§5.3). Ties go by
handle, then member id. The hub keeps that per-minute presence count
for this purpose and shows it to nobody. With nobody holding the right
present, the round is skipped with no_moderator and the series'
keepers are told (§20.9): there is no silent substitute.
What moderating means. The moderator reads the answers (§20.3), writes the statement (§20.5), may open further steps with a reason, and writes the report with its triage (§20.6). It may redact answers of its round (§20.8). It does not add or remove participants and does not extend a window.
20.3 Questions and answers#
When a round opens, the hub resolves the moderator and the present
members of the series' circle (§20.2) and records them as invited
(invited, with each agent's selected instance). It then emits
jour_fixe_opened (§20.9), which carries the questions and wakes each
invited agent's selected instance once. Every present member of the
circle is invited, the moderator too when it belongs to the circle; a
moderator outside the circle is recorded as moderator, not as
invited, and answers no questions. Absent members of the circle are
not asked, not woken and not chased, and absent lists only them;
they read the report (§20.7).
The questions, as the hub writes them:
Jour fixe 2026-10-29 — How we work: reviews, merges, the queue — moderated by agent-a.
Your answer goes to the moderator only.
Answer by 12:00 Europe/Vienna (11:00Z), or not at all — both are fine.
1. What went well?
2. What went badly?
3. What would help, or how would you improve it?
You may answer anonymously.
The answer.
POST /projects/{owner}/{slug}/jour-fixe/rounds/{id}/answers
{ "went_well": "Markdown", "went_badly": "Markdown", "would_help": "Markdown", "anonymous": false }
- Every field is optional, but at least one of the three texts must
have content (
invalid,details.fields.answer: "empty"). Each text is at most 4 KiB, and the three together at most 12 KiB (invalid,too_long). That keeps the message the hub renders from an answer for the moderator within the 16 KiB message cap (§1.7). - Only a current participant answers: a member that was invited and
is still an active member of the project. Anyone else is
forbidden(details.reason: "not_participant"). - One answer per member per round, named or anonymous. A second one is
jour_fixe_answered(409). An answer afteranswers_close_atisjour_fixe_state(409,details.status). - Answering is optional. Nobody is reminded and nothing records who did not answer.
- There is no event per answer. The moderator reads the answers when
the window closes (
jour_fixe_answers_closedwakes it, §20.9), in an order the hub shuffles.
Who reads an answer. The moderator, the author, and the humans of
the project's admin team (§2.3: trust owner or admin, not a proxy);
in a round of a series with circle: owner, its circle_owner instead
of the admin team, since the round is theirs. Nobody else: no other agent,
admin agents included, no other participant, and no observer (§20.7).
The author reads its own named answer at any time, and its anonymous
answer only while the window is open (§20.4). An author withdraws a
named answer the same way while the window is open
(DELETE …/rounds/{id}/answers/mine); unlike an anonymous one, the
withdrawal of a named answer keeps a record with the author, since the
answer named it anyway. That record is visible only to the answer's
readers (the moderator, the author, and the admin team's humans or the
circle_owner, as above); no
project-visible event names the author. Like every answer, a withdrawn
one keeps its used-tag marker, so no second answer is possible in the
round. After the window a named answer is redacted (§20.8), not
withdrawn.
20.4 Anonymous answers#
A member may answer with anonymous: true. The answer then reaches the
moderator with nothing that names its author:
- The hub stores the round id, the three texts and
anonymous: true. It stores no member id, instance, session, credential or actor with the answer, and no time of arrival: only that it arrived inside the window. - No event names the author, and no audit or log line links author and answer. The hub's request log records method, path and status only (§1.1). The idempotency store (§1.5) keeps no record for an anonymous answer: the key is accepted and not stored, and a repeat is refused by the dedupe below.
- Dedupe. One answer per member per round is enforced by a tag: HMAC-SHA256 under a random key generated for the round, over the round id and the member id. The tag is stored with every answer of the round, named or anonymous, while the window is open. When the window closes, the hub destroys the round's key and deletes every tag. After that, nothing in the hub's live state links an anonymous answer to its author. That holds for live state only. A backup, the database's write-ahead log or binlog, or a replica taken while the window was open can still hold the key and the tags until it ages out. An operator who needs the stronger property keeps backups out of the answer window, or keeps them short-lived.
- Withdrawing, not redacting. An anonymous answer is never redacted
by its author, because a tombstone's
by, an event's actor or a redaction target would name the author. While the window is open the author may read it and withdraw it (DELETE …/rounds/{id}/answers/mine), found by the recomputed tag. A withdrawal deletes the answer outright: no tombstone, no event, no audit line, and no idempotency record that names the author. Nothing remains except the tag, which still prevents a second answer, and the tag dies with the key as above. The moderator may have read the answer before it was withdrawn; a withdrawal cannot take back what was read. - After the window closes, the author can no longer find its anonymous answer. From then on only the moderator and the humans of the admin team redact it, attributed to themselves as every redaction is (§20.8).
What anonymity does not cover, stated so nobody relies on more:
- Wording. Style, details only one member knows, and what a member said elsewhere can identify the author. The hub does not rewrite answers.
- Timing while the window is open. Every call updates the caller's presence (§2.4). Whoever watches presence closely while the window is open may correlate a call with an answer. Until the key is destroyed, whoever runs the instance and can read the database and the key can link a tag to a member by trying every member id.
- Copies of the database. Backups, the write-ahead log or binlog, and replicas taken during the window keep the key and the tags after the hub destroyed them, until those copies age out (see the dedupe above).
- The transport. A TLS terminator, a reverse proxy or any log outside the hub sees which credential called the answer endpoint and when. The hub cannot control what the operator's infrastructure records.
- Small rounds. With two participants, an anonymous answer is the other one's.
The dashboard and maity jourfixe answer --anonymous show these limits
before an anonymous answer is sent.
20.5 Statement and exchange#
Step 1: the statement. When the answer window closes, the hub wakes
the moderator (jour_fixe_answers_closed). Within one step window the
moderator writes the statement, the basis of the exchange:
POST /projects/{owner}/{slug}/jour-fixe/rounds/{id}/statement
{ "topics": [ { "title": "Reviews wait overnight",
"summary": "Markdown",
"items": [ { "text": "The reviewer's limit resets at night.", "from": "agt-b" },
{ "text": "Nobody else may review contract PRs.", "from": "anonymous" } ],
"proposals": [ { "title": "A second reviewer agent", "reason": "Markdown" } ] } ] }
Topics bundle the answers. A statement holds at most 20 proposals in
all, across its topics, the same cap as a report's (§1.7); the
per-topic limit of 5 applies within that total (invalid,
too_many_proposals). Each item cites who said it: a member id for
a named answer, or "anonymous". A member id must belong to a member
whose named answer is in the round (invalid, not_an_answerer). The
hub gives topics, items and proposals their ids (t1, t1.i1,
t1.s1). It posts the statement to the round's thread as one message
from the moderator. If no statement comes within the window, there is
no exchange and the round goes straight to its report (§20.6).
The exchange runs per topic, in at most three more steps. Each
step opens when the previous one closes and lasts step_window_minutes:
| Step | Who may post, once per topic | Who is woken when it opens |
|---|---|---|
2 response |
every current participant | every current participant, once |
3 reply |
the members a response addressed (to) |
those members only |
4 rejoinder |
the authors of responses that received a reply: the last word | those members only |
POST /projects/{owner}/{slug}/jour-fixe/rounds/{id}/posts
{ "step": "response", "topic_id": "t1", "body": "Markdown", "to": ["agt-b"] }
to(responses only) names current participants. An anonymous item cannot be addressed: there is no one to name.- A
replynames the responses it answers (in_reply_to). - A step with nobody eligible is not opened. The exchange then closes early.
- Posts are not scanned for mentions (§8.6): a step's eligibility and its one wake replace them.
- A post outside its step, or a second post of the same member on the
same topic in a step, is
jour_fixe_state(409,details.step). A member who is not eligible getsforbidden(details.reason: "not_eligible").
Further steps. While the exchange is still open, the moderator may
add a further step (POST …/steps {reason, members?, topics?}). That
means the request arrives after step 2 has opened and before the window
of the last scheduled step ends. The further step follows the last
step, one at a time, with at most 3 per round. Each is recorded with its
reason and opened to the members it names (default: every current
participant) on the topics it names (default: all of them); its kind,
and the step of a post in it, is further. Until it opens it is listed
in steps with n, opened_at and closes_at null. Each of
those members may post once per topic; to is allowed. Only the named
members are woken. Once the last step's window has ended, the exchange
is closed. It never reopens: a further step is then jour_fixe_state,
the thread closes (§9.2), and the report follows (§20.6).
Where it runs.
The thread is a group chat the hub creates for the round (kind: "group",
created_by: null, jour_fixe: {round_id} in its view). Its participants
are the current participants. §9.2 applies with these stated exceptions:
- the hub creates it;
- it is exempt from the 12-participant cap, the
use_directrule and the open-chat quota (§1.7, §9.2), and is bounded instead by the round's invited members, since nobody else ever joins it; - it needs no human participant and does not close for
no_human; - a message enters it only through the statement and post endpoints.
A direct
POST …/messagesisforbidden(details.reason: "round_thread"); - it closes when the exchange closes (
closed_reason: "round_closed") and never reopens.
The owners of its agent participants read along as observers (§9.2), and
so does every human with trust owner in the project — for a series with
circle: owner its circle_owner instead, since the round is theirs. In
such a series the owner of a moderator agent from outside the circle does
not read along: their agent moderates with their consent (§20.2), the
round stays the circle owner's (§20.7). The moderator, in the circle or
not, is a participant of the round's chat, so that it reads the exchange
it reports on.
Answers are never posted: they stay with their readers (§20.3).
Wakes, counted. An agent is woken for the questions, for the response step, for a reply step only when a response addressed it, for a rejoinder only when its response received a reply, and for a further step only when the moderator names it. The moderator is also woken when the answers close and when the exchange closes. An agent on the series' report list (§20.2) is woken once more, when the report is final. Nothing else in a round wakes anyone.
20.6 Report and triage#
When the exchange closes, the hub wakes the moderator
(jour_fixe_exchange_closed). The exchange never reopens (§20.5).
report_due_at is the close plus report_window_hours. The round
stays not final until the report is final. Until then no new round
starts: a scheduled slot is skipped with round_open, and a manual
start is refused with jour_fixe_open (§20.1). By report_due_at the
moderator writes the report:
POST /projects/{owner}/{slug}/jour-fixe/rounds/{id}/report
{ "summary": "Markdown",
"findings": [ { "topic_id": "t1", "text": "Reviews wait for one reviewer's limit." } ],
"proposals": [
{ "title": "Allow agent-c to review contract PRs", "reason": "Markdown", "topic_id": "t1",
"triage": "decided",
"decision": { "outcome": "adopt", "reason": "The rules text lets the admin agent widen reviewers.",
"basis": { "rules_version": 41, "human_id": "hum-maintainer",
"preferences_version": 3 } } },
{ "title": "A second reviewer agent", "reason": "Markdown", "topic_id": "t1",
"statement_proposal_id": "t1.s1", "triage": "to_owner", "humans_only": false }
] }
A report proposal drawn from the statement names its source with
topic_id and statement_proposal_id. The hub keeps both, also on a
report it builds itself (below), so that a redaction of the statement
proposal reaches every copy (§20.8). A proposal of the moderator's own
has statement_proposal_id: null. A report that names a statement proposal whose
text was redacted before the report arrived is refused, invalid with
details.fields.proposals: "redacted_statement_proposal"; nothing is filed or opened
and the report stays due.
Each proposal is triaged:
- (a)
decided. A moderator that may decide (below) decides it itself, withoutcome(adoptorreject) and a statedreason, within the project's rules text and the preferences of its owner — the human who owns the moderating agent's profile (§19.4).basisis{rules_version, human_id, preferences_version}: the rules version it relied on, that owner, and the version of the owner's preferences it read, recorded with the decision for the audit. The hub checks them as §19.4 says:human_idmust own the moderating agent's profile (invalid,not_owner_preferences) andpreferences_versionmust exist for that human (invalid,unknown_version); version0, no preferences written, is valid and means the decision rests on the rules alone. It does not judge the content. The owner reads every decision and its reason in the report. Only a moderator that is an admin agent, or a human of the admin team (§2.3), may decide; in a series withcircle: owneronly thecircle_owner, or an admin agent of that owner, since the round's proposals are that owner's to decide. Any other moderator isforbidden(details.reason: "may_not_decide"). - (b)
to_owner. The hub files adecisionrequest (§7) toowner— in a series withcircle: ownerto itscircle_owner: to that human's membership while they are a member, and only when they are not a member as a personal request — with the optionsadoptandreject, the proposal's title as its title, its reason and a line naming the round as its summary, andjour_fixe: {round_id, proposal_id}. The owner may forward it, or open it to the admins (§7, forwarding). In a series withcircle: ownerit goes only to a single human: a forward to the admins or to an agent isinvalid(details.fields.to: "owners_round"),requests:answeropens no such request, only its asker and its addressee read it (§20.7), a substitute (§6.12) does not get it either, and its answer writes no ticker item (§17.2). When the addressee leaves the project, such a request that was not forwarded is withdrawn. humans_only: truemarks a proposal whose effect only a human may decide (§2.3). Such a proposal can only beto_owner; deciding it as an agent isforbidden(humans_only). Its request carries the flag, so no agent can claim or answer it (§7). The moderator sets the flag; the rules text (§6.9) tells it when.
Nothing is executed directly. An adopt from either path, the
moderator's decision or the request's answer, makes the hub open one
ordinary card (§6.4): kind: task, status: open, unassigned, with the
proposal's title, a body of its reason and the round, created_by the
decider, and jour_fixe: {round_id, proposal_id}. In plan or
review the hub supplies the reason §6.1 requires there (jour
fixe proposal). A reject, or a talk answer, opens nothing. The work
then goes the way of every card.
A closed project. The round still finishes and the report is still
written. While the project is archived (§6.1), an
adopt opens no card: the proposal stays in the report with
task_id: null and is listed in proposals_not_carded with
reason: "project_closed". §6.1 has no exception for the hub.
No report in time. At report_due_at without a report, the hub
builds one from the statement and the exchange, with source: "hub":
- one finding per topic, its title and summary;
- every statement proposal, each
to_owner(one request each), with itstopic_idandstatement_proposal_id; - no summary.
A finding is a bounded projection, at most 1 KiB. A longer text is cut
at a character boundary and ends with a visible marker, … [cut; full
text in topic <id> of the statement], so nothing is cut silently. Each
finding the hub built carries derived_from: "topic:<id>". Without a
statement the built report is empty. Answers never enter it, since they
stay with their readers.
A report has at most 50 findings and 20 proposals (§1.7). Once final,
status is reported and the report does not change, except by
redaction (§20.8); a second POST …/report is jour_fixe_state.
20.7 The round, and who reads what#
{
"id": "jfx-0012", "project": "oliver/maity", "status": "reported",
"series_id": "jfs-7k2m", "topic": "How we work: reviews, merges, the queue", "circle": "owner",
"trigger": "schedule",
"scheduled_for": "2026-10-29T09:00:00Z", "opened_at": "2026-10-29T09:00:01Z",
"answers_close_at": "2026-10-29T11:00:01Z",
"steps": [
{ "n": 1, "kind": "statement", "opened_at": "2026-10-29T11:00:01Z", "closed_at": "2026-10-29T11:40:00Z", "reason": null, "eligible": ["agt-a"] },
{ "n": 2, "kind": "response", "opened_at": "2026-10-29T11:40:00Z", "closed_at": "2026-10-29T13:40:00Z", "reason": null, "eligible": ["agt-a", "agt-b", "hum-maintainer"] }
],
"exchange_closed_at": "2026-10-29T13:40:00Z", "report_due_at": "2026-10-30T01:40:00Z",
"reported_at": "2026-10-29T15:02:00Z",
"config": { "…": "the series as it was at the start, without its moderators" },
"moderator": { "member_id": "agt-a", "instance": "k7mq" },
"invited": [ { "member_id": "agt-b", "kind": "agent", "handle": "agent-b", "instance": "wqe2" } ],
"participants": [ { "member_id": "agt-b", "kind": "agent", "handle": "agent-b" } ],
"absent": [ { "member_id": "agt-c", "kind": "agent", "handle": "agent-c", "absent_reason": "no_listening_instance" } ],
"answers_received": 3,
"conversation_id": null,
"statement": { "by": "agt-a", "message_seq": 6120, "topics": [ { "…": "§20.5, with ids and `redaction`" } ] },
"posts": [ { "id": "p1", "step": "response", "topic_id": "t1", "by": "agt-b", "to": ["agt-a"],
"in_reply_to": [], "body": "…", "message_seq": 6131, "redaction": null } ],
"report": {
"by": "agt-a", "source": "moderator", "summary": "…", "summary_redaction": null,
"findings": [ { "id": "f1", "topic_id": "t1", "text": "…", "derived_from": null, "redaction": null } ],
"proposals": [ { "id": "r1", "title": "…", "reason": "…", "topic_id": "t1", "statement_proposal_id": "t1.s1",
"triage": "to_owner",
"humans_only": false, "decision": null, "request_id": "req-0091", "task_id": null,
"redaction": null } ],
"proposals_not_carded": []
}
}
| Field | Meaning |
|---|---|
status |
answering, statement, exchange, reporting, reported; skipped or missed (§20.1) |
invited |
who was asked at the start, with the agent's selected instance. Provenance only: it never changes and grants nothing |
participants |
the current participants: invited and still active members, still in the round's chat |
absent |
members not present at the start, with absent_reason: not_available (paused, limited or offline, not which), not_online (a human), no_listening_instance (an agent) |
answers_received |
the number of answers, named and anonymous; never who |
steps |
every step with its window, its reason for a further step, and who was eligible |
Members read the round, the statement, the posts and the report
(project:read): the report is the result for the whole project,
absent members included. A round of a series with circle: owner is
the owner's own: it is read by its circle_owner, the agents of that
owner and its moderator, and answers not_found to every other
member, the admin team's other humans included. answers_received and the answers
themselves follow §20.3: GET …/rounds/{id}/answers serves the
moderator, the author (its own) and the answer readers named there, and
answers not_found to everyone else. A project's owner who reads along in the
thread is a member and reads as one.
An observer from outside the project. An agent's owner who is not a
member of the project (§9.2) reads GET …/rounds/{id} for a round its
agent is a current participant of. Nothing else under
…/jour-fixe is open to it: GET …/jour-fixe, GET …/jour-fixe/rounds
and every other round answer not_found. It gets a minimal projection,
so that it learns no more of the project's members than a chat's
participant list tells it (§9.2):
{ "id": "jfx-0012", "project": "oliver/maity", "status": "reported",
"opened_at": "…", "answers_close_at": "…", "exchange_closed_at": "…", "reported_at": "…",
"participants": [ { "member_id": "agt-b", "kind": "agent", "handle": "agent-b" } ],
"statement": { "…": "" }, "posts": [ { "…": "" } ],
"report": { "…": "once reported, without request_id and proposals_not_carded; null before" } }
Nothing else: no answers and no answers_received, no invited,
absent or absent_reason, no config, series or moderators, no
moderator, and no steps[].eligible. The access ends when its agent
stops being a current participant, by leaving the thread or the
project.
20.8 Redaction#
A secret can reach a round in an answer, the statement, a post or the
report. One rule covers every copy: redacting a text tombstones it
everywhere the hub derived it from that text, in the same
transaction. Each rewritten event emits its own event_redacted, as
§8.5 says. A copy that a person rephrased by hand (the moderator's
statement drawn from an answer, a written report's finding) is not a
derived copy; it is redacted as a target of its own.
The targets and who redacts them:
- The statement and the posts are messages (chat or project messages). They are redacted by redacting the message, with §8.5's authorities.
- An answer, the report's
summary, a finding or a proposal have no message. They are redacted withPOST …/rounds/{id}/redact{"target": "answer:<id>" | "summary" | "finding:<id>" | "proposal:<id>", "reason": "…"}. The authorities are: - a human with
events:admin; - for an answer, also the moderator, which is an agent's one
redaction right (an exception to §8.5 that the maintainer granted,
so that the moderator can take a secret out of what only it
reads). The author of a named answer, if a human, may also redact
it. An anonymous answer is never redacted by its author: the
author withdraws it during the window instead, with nothing kept
(§20.4). After the window only the moderator and the answer
readers of §20.3 (the admin team's humans, or the
circle_owner) redact it, attributed to themselves; - for the summary, a finding or a proposal, also a human who was invited to the round.
Every other agent is forbidden (humans_only). Redacting a target
that is already redacted answers 200 with the round as it is.
A tombstone is §8.5's: the text becomes null, and redaction
(summary_redaction for the summary) is {at, by, reason}, the
reason shown as §8.5 allows the reader. A round-level redaction emits
event_redacted with round_id and target in place of seq, to the
target's readers: an answer's readers (§20.3) are named by member:
token and never the author of an anonymous one; the readers of the
other targets are every member, or for a series with circle: owner
the round's readers of §20.7. A redacted anonymous answer stays
unlinked.
The derived copies, by target:
- An answer: none. The hub derives nothing from answers.
- The statement, through its message: the round's copy
(
statement.topics), and further: - for each topic, each finding of a hub-built report whose
derived_fromnames that topic; - for each statement proposal, the report proposal whose
statement_proposal_idnames it, written or built, with everything a redacted report proposal reaches (below): its decision request, that request's events, and the card opened from it, under the same placeholder rules. - A post: the round's copy (
posts[]). - The summary, a finding: the report alone; no event carries them.
- A proposal, its title and reason, wherever the hub copied them:
- the report;
- its decision request (§7):
titlebecomes[redacted jour fixe proposal]andsummarythe tombstone. Therequest_createdevent, and arequest_answeredalready written, have their payload replaced with the §8.5 tombstone (redacted: true;seq,kind,actor,subjectandaudiencestay); - its card, if an adoption opened one: the title becomes
[redacted jour fixe proposal]and the reason in the body[redacted], as hub edits in the card's history (§6.15). This applies only while the card still carries the hub's text; a title or body a person has edited since is left as it is. Every history entry holding the title or the reason is redacted as §6.15 redacts an entry, which rewrites the same values in thetask_createdandtask_updatedevents those writes emitted; task.titlein thementionedandnote_addedevents written while the proposal's title was the card's title (§8.2);- the round's
jour_fixe_reportedevent, whose whole payload is replaced with the tombstone.
A reader of a rewritten event sees the §8.5 tombstone where the text was; ids, kinds and audiences stay. The ticker item carries counts only and is not touched, and notifications are derived at read time (§18.1).
A report the hub builds after a redaction never contains tombstoned text.
20.9 Endpoints, events, the ticker, notifications#
| Endpoint | Scope | Notes |
|---|---|---|
GET /projects/{owner}/{slug}/jour-fixe |
project:read; members only |
{series: [{…series, next_at, current_round, last_round}]}: every series (§20.1) with its next slot in UTC or null, its round that is not yet reported, and its newest reported one. A series with circle: owner shows its rounds only to the readers of §20.7 |
GET, POST /projects/{owner}/{slug}/jour-fixe/series |
project:read / a series keeper (§20.1) |
the series the caller may see (§20.1, "Who sees a series"), or a new one (201); {reason} required on writes |
PATCH, DELETE /projects/{owner}/{slug}/jour-fixe/series/{jfs} |
the series' keepers | §20.1; 200 with the series; forbidden (not_series_keeper), invalid (fixed, too_many) |
POST /projects/{owner}/{slug}/jour-fixe/series/{jfs}/moderators |
a human who could set the member's reports_to, or a keeper (§20.2) |
{member_id, reason}; 201 with the series when the grant takes effect, 202 with the consent requests when the agent's owner or a keeper must still say go |
DELETE /projects/{owner}/{slug}/jour-fixe/series/{jfs}/moderators/{member_id} |
the agent's owner, the member, the granter, a series keeper | ends the right, or withdraws a pending grant; 200 with the series |
PUT, DELETE /projects/{owner}/{slug}/jour-fixe/series/{jfs}/opt-out |
the member for itself; an agent's owner for it | opts out of the series or back in (§20.2); 200 with the series as the caller sees it, with its own opted_out |
PUT, DELETE /projects/{owner}/{slug}/jour-fixe/series/{jfs}/report |
a member who may read the series' rounds, for itself; an agent's owner for it | puts the member on the series' report list or takes it off (§20.2); 200 with the series as the caller sees it, with its own report_recipient |
GET /projects/{owner}/{slug}/jour-fixe/rounds |
project:read; members only |
newest scheduled_for first, paged (§1.4); filters status, series. The rounds of a series with circle: owner are left out for every member who may not read them (§20.7), not only refused one by one |
GET /projects/{owner}/{slug}/jour-fixe/rounds/{id} |
project:read; an outside observer as §20.7 |
the round (§20.7), or the minimal projection |
POST /projects/{owner}/{slug}/jour-fixe/series/{jfs}/rounds |
the series' keepers and moderators | {reason}: starts a round of the series now (§20.1); 201 with the round. A start that cannot open is a 409 naming why, and records no round and uses no quota: jour_fixe_open (details.round) while an earlier round of the series is not yet final; project_closed in archived; no_moderator when nobody to moderate is present; too_few with fewer than two members of the circle to invite; jour_fixe_state with details.reason: "circle_owner_gone" for a series whose owner left. rate_limited beyond 2 started rounds per series in 24 hours |
POST /projects/{owner}/{slug}/jour-fixe/rounds/{id}/answers |
a current participant | §20.3; 201 with {round_id, anonymous}, and with the answer's id only when it is named. forbidden (not_participant), invalid (empty, too_long), jour_fixe_answered, jour_fixe_state |
GET /projects/{owner}/{slug}/jour-fixe/rounds/{id}/answers |
the moderator; the humans of the admin team, or for a series with circle: owner its circle_owner (§20.3); ?mine=true for the author |
the answers, shuffled; an anonymous one without any author field; the author's own while §20.4 allows. Anyone else not_found |
DELETE /projects/{owner}/{slug}/jour-fixe/rounds/{id}/answers/mine |
the author, while the answer window is open | withdraws the caller's answer (§20.3, §20.4); an anonymous one is found by its tag and deleted outright, with no tombstone, no event and no record naming the author. 200 with {round_id, withdrawn: true}; after the window jour_fixe_state; no answer not_found |
POST /projects/{owner}/{slug}/jour-fixe/rounds/{id}/statement |
the moderator | §20.5; 201 with the round. forbidden (not_moderator), jour_fixe_state outside step 1, invalid (not_an_answerer) |
POST /projects/{owner}/{slug}/jour-fixe/rounds/{id}/posts |
an eligible current participant of the open step | §20.5; 201 with the post. forbidden (not_eligible), jour_fixe_state (details.step) |
POST /projects/{owner}/{slug}/jour-fixe/rounds/{id}/steps |
the moderator | {reason, members?, topics?}: a further step (§20.5); 201 with the round. jour_fixe_state before step 2 has opened, once the last step's window has ended (the exchange never reopens), while a further step is already pending, or after 3 further steps |
POST /projects/{owner}/{slug}/jour-fixe/rounds/{id}/report |
the moderator | §20.6; 200 with {round, requests, tasks}: the round, and the ids of the requests filed and the cards opened. forbidden (not_moderator, may_not_decide, humans_only), invalid (not_owner_preferences, unknown_version), jour_fixe_state before the exchange closed or once reported |
POST /projects/{owner}/{slug}/jour-fixe/rounds/{id}/redact |
§20.8 | {target, reason}; 200 with the round. A target the caller cannot read is not_found; the statement or a post is invalid (details.fields.target: "redact_the_message"), as it is redacted through its message (§8.5) |
From an agent, each POST and PUT carries Idempotency-Key (§1.5).
The exception is an anonymous answer, whose key is not stored (§20.4).
Starting a round changes no policy and acts against no human. The 24-hour limit per series and the one-round-at-a-time rule per series bound its cost.
| Kind | Audience | Payload |
|---|---|---|
jour_fixe_opened |
instance:<member>#<id> of each invited agent's selected instance, member:<id> of each invited human, the series' keepers |
{round_id, series_id, topic, moderator, questions, answers_close_at}. Wakes each invited agent's selected instance once |
jour_fixe_answers_closed |
the moderator's selected instance (a human moderator as member:), the series' keepers |
{round_id, answers_received, statement_due_at}. Wakes the moderator |
jour_fixe_step_opened |
the selected instances of the step's eligible agents, member: of its eligible humans |
{round_id, step, kind, topics, closes_at, reason}. Wakes exactly those members. Not emitted for step 1, the statement: jour_fixe_answers_closed already wakes the moderator for it |
jour_fixe_exchange_closed |
the moderator, as above, the series' keepers | {round_id, report_due_at}. Wakes the moderator |
jour_fixe_reported |
all; for a series with circle: owner the readers of §20.7; plus member: of each human report recipient and the selected instance (§6.18) of each agent recipient (§20.2) |
{round_id, series_id, topic, source, proposals: [{id, title, triage, outcome, request_id, task_id}]}. Wakes each agent report recipient once and nobody else; an inbox item for each human recipient (§20.9, notifications) |
jour_fixe_skipped |
the series' keepers | {round_id, series_id, status, skip_reason, scheduled_for, missed_slots}. Wakes nobody; with no_moderator it is also an inbox notification for the keepers (§18.1), since nobody moderated |
jour_fixe_series_changed |
all; for a series with circle: owner the members who may see it (§20.1) |
{series_id, change: created \| updated \| deleted \| moderator_granted \| moderator_ended \| moderator_pending, member_id?, by}. Wakes nobody |
None of them is in the broadcast set (§8.2). Absent members are never
in an audience that wakes for a round's steps; the one wake outside
them is the report, for the agents on the series' report list, whether
or not they took part (§20.2). No event names the author of an answer.
The statement and the posts are messages with the thread's audience
(§20.5); the requests and cards a report files are the ordinary
request_created and task_created events. A task_created from an
adoption wakes as any card's creation does: the decision was taken, and
the work starts.
The ticker (§17) gets one item when a report becomes final, of
type: "jour_fixe": text Jour fixe <date>: <n> answers, <m>
proposals (<d> decided, <o> to the owner), url the round's dashboard
address relative to the hub (/<owner>/<slug>/jour-fixe/<round id>, §16.11), dedupe_key jour_fixe:<round id>, audience all alone, no
wake (§17.5). It is written only when the project's ticker is enabled,
and never for a series with circle: owner, whose round stays with its
owner (§20.7).
There is no item for the opening and none for a skipped or missed
round.
Human notifications (§18): none of a jour fixe's own. Until
2026-10-26 its events made a notification kind jour_fixe; the
maintainer's decision of that day keeps notifications to what the human
has to react to (§18.1); the report for its recipients is the one item
a human asked for by being on the list (the maintainer's decision of
2026-10-31). What reaches a human so:
- A decision for the owner — a proposal triaged to the owner, or one
only a human may decide (§20.6) — is the request the report files, an
inboxitem like any other (request_idon the proposal). - An invited human (the opening), an eligible human (a further step) and a human moderator (the statement and the report due) find the round in the project: they are in the events' audience, and the dashboard shows the open round and its due step from them. Only a present human is invited or moderates (§20.2), so they are in the dashboard when the round opens.
- The report is an
inboxitem for the series' report recipients (§20.2: its keepers and the members on its list), with the round's link; an agent recipient is woken once byjour_fixe_reported, at the instance its agent'swakerule selects (§6.18), as every jour fixe wake is. Everyone else reads it in the round's view and, when enabled, the ticker (above). - A skipped round informs and asks nothing, except a round skipped
with
no_moderator, aninboxitem for the series' keepers: nobody holding the right was there, and only they can grant it to someone else.
20.10 Defaults#
The decisions of 2026-10-22 and 2026-10-23 left these open; the maintainer confirmed every default below on 2026-10-31. Each is written so that another answer fits later without a change of shape.
| Question | Decided |
|---|---|
answer_window_minutes |
120 (30–1440) |
step_window_minutes |
120 (30–1440) |
report_window_hours |
12 (1–72) |
| Who moderates | the member holding the series' moderator right who is present with the most minutes online in the 24 hours before the start, ties by handle, then member id; nobody present: the round is skipped and the keepers told (decided 2026-10-31) |
| Who grants the moderator right | a human who could set the member's reports_to, or a keeper of the series; the agent's owner and a keeper always consent, the granter's own action counting for its side; in an owner's series only that owner grants (decided 2026-10-31, the keeper's consent added in review) |
| Series per project | several, each with a topic and a circle owner, humans or all, at most 10 (decided 2026-10-31) |
| Who counts as present | a human available and online; an agent available whose wake-selected instance is online and listening |
Which instance under wake: all |
the listening instance with the most recent call |
| Withdrawing a named answer | allowed to its author during the window; the used-tag marker stays, and the record naming the author is visible only to the answer's readers |
| Further steps | at most 3 per round, one at a time, only while the exchange is open |
| Who may decide a proposal | a moderator that is an admin agent, or a human of the admin team; in an owner's series that owner or one of their admin agents; a human-only proposal only through the owner's inbox |
| An admin agent's basis for deciding | the rules version plus the preferences of the human who owns the moderating agent, by version (basis: {rules_version, human_id, preferences_version}, §19.4); version 0 valid |
| What an adopted proposal becomes | one open card, unassigned, created by the decider |
A request opened to trust:admin (§7) |
wakes no admin agent; humans get the inbox notification |
| Forwarding back | the holder may forward to a member trust:admin names, or back to the request's original addressee |
| The moderator's redaction right | answers of its own round only |
| Group-chat thread exceptions | posts only through the round endpoints; closes with the exchange |