ADR-0082 — Every committee session MUST be logged as an OpenProject Meeting¶
Status: active
Date: 2026-05-05
Introduced by: CVN-N001-EE-S14 / PR (TBD)
Supersedes: amends ADR-0068 (committee invocation traceability) ; complements ADR-0052 (committee session JSON audit trail) ; reinforces ADR-0076 (OpenProject as SSoT for project memory).
Context¶
ADR-0068 made the Expert Committee the default channel for plan + PR reviews and required that committee verdicts are the system of record (not chat transcripts, not Slack threads). The verdict + opinions are persisted as JSON under committee/sessions/<session_id>_committee.json per ADR-0052.
In practice this dual-system of record (file-system JSON + linked from PR/Story body) has been brittle :
- Discoverability gap : finding past committee verdicts requires
grep-ing the repo or knowing the session id. The OP Story body links the session JSON path, but operators reading the OP roadmap UI don't see the verdict — they have to switch tools. This is the same anti-pattern ADR-0076 called out for project memory : "discrepancy → OP wins". - Cross-Story aggregation fails : "show me all committee verdicts in the last 30 days" requires walking the repo's session/ directory. OP has a built-in calendar + "Past meetings" view that natively answers this question — for any other meeting type but committees.
- Expert participation invisible : the committee runs 5 expert personas (data-scientist, ops, ml-engineer, architect, crypto-trader) plus a consolidator. Their opinions are in the session JSON but invisible from OP. There's no way to filter "Stories where expert-X dissented" or "experts most consulted this sprint".
- Reviewed artefacts not native : the dossier under
documentation/reviews/is referenced from the Story but not bundled with the verdict in OP. If the dossier gets reorganised on disk, the link rots ; the committee's frozen-in-time view of the artefact is only incommittee/sessions/<id>_artifact.md. - Forcing function — 2026-05-05 :
dd248118plan_review of wp#103 shipped a 14-recommendation verdict that needed to be referenced from the Story, the GH issue, the impl PR, and the operator's "what did the committee say last week?" weekly review. The 4 references were OP comment + PR description + GH issue body + operator notebook — all stale within hours. The native OP Meeting object (created viascripts/op_save_committee_as_meeting.pyafter-the-fact) collapsed all 4 into one queryable record.
OpenProject's Meeting module exposes : title, project, start_time, location, state (open/closed/cancelled), participants (locked Users for the experts), agenda items (verdict + agreement + dissent + recommendations + blockers + linked WP), and attachments (the session JSON + reviewed artefact). This is a 1-to-1 fit for a committee session's audit envelope.
Decision¶
Every successful committee session — plan_review, pr_review, experiment_review, general — MUST be logged as an OpenProject Meeting, linked to its Story (or PR) work package, with the source artefacts attached.
The canonical ritual is :
- Operator runs
python scripts/expert_committee.py --artifact ... --question ... --session-type ... --issue ...per ADR-0068 — producescommittee/sessions/<session_id>_committee.json+committee/sessions/<session_id>_artifact.md. - Operator immediately runs
python scripts/op_save_committee_as_meeting.py --session committee/sessions/<session_id>_committee.json --linked-wp <wp_id>— creates an OP Meeting with :- Title :
Committee {session_type} {issue_ref} ({session_id}) — {verdict.status} - State :
closed(committee already concluded ; OP UI hides drafts from the project meetings index, so this is mandatory for visibility) - Participants : the Story's operator (admin user) + one locked
Userper committee expert (idempotent : looked up bylogin=<expert_id>) - Attachments :
<session_id>_committee.json+<session_id>_artifact.md(OP REST API multipart upload, idempotent : skipped if already attached) - Agenda items (in section position 1) : Verdict + summary, Areas of agreement, Areas of dissent, Recommendations, Blockers, Artefacts + audit trail, Linked Story wp#
- Title :
- Operator pastes the OP Meeting URL alongside the session JSON path in the Story body / PR description (both references stay — JSON is source-of-truth for content, Meeting URL is the queryable surface).
The script is idempotent : re-running on the same session reports EXISTING_MEETING_ID=<n> and skips re-attaching duplicates. Operators can re-run after touching the dossier without polluting OP.
Failure to log : if the committee session ran but the OP Meeting was not created within 24h of the session timestamp, the verdict is treated as non-compliant per ADR-0068 + ADR-0076 (project state lives in OP, not in the file system alone). The Story / PR review evidence reverts to "informal" until the meeting is created retroactively.
Invariants¶
- One Meeting per session : the OP Meeting title MUST contain the verbatim
session_id(8-character hex prefix). Duplicate sessions on the same wp create separate Meetings — the script's idempotency check prevents accidental double-creation but does not prevent intentional re-runs. - Meeting linked to a WP :
--linked-wp <id>is REQUIRED forplan_review+pr_review+experiment_reviewsessions. Only--session-type generalmay omit the link (rare). The link surfaces as an agenda item ofitem_type=work_packageso the Meeting appears on the WP's "Meetings" tab. - State must be closed at creation : OP's
Meeting.statesenum is{open:0, draft:1, in_progress:3, cancelled:4, closed:5}. TheMeetingsController#load_queryhidesdraftfrom the project meetings index by default. Committee sessions are concluded events ;state=closedis the only correct value. - Author MUST be a participant : OP applies a default index filter
where("invited_user_id", "=", [User.current.id]). A meeting with 0 participants is invisible from the project + global meetings lists (only the direct/meetings/<id>URL renders). The author is auto-added asinvited+attended. - Experts as locked Users, not PlaceholderUsers :
MeetingParticipant.belongs_to :userrestricts the polymorphic association to theUserclass —PlaceholderUserinstances raiseValidation failed: User can't be blank. Experts are persisted asUser.status="locked"records withlogin=<expert_id>, so they appear as proper participants without ever being able to log in. - Source artefacts attached, not just referenced : the session JSON + reviewed artefact MUST be uploaded as actual
Attachmentrecords on the Meeting (viaPOST /api/v3/meetings/<id>/attachments). A Story body link is insufficient — the Meeting must be self-contained for the audit trail. - Verdict surfaced in agenda items : the consolidated verdict (
status,code,consensus_strength,reason) lands in the first agenda item ("Verdict + summary") ; areas of agreement, dissent, recommendations, blockers each land in their own agenda item. Operators reading the Meeting page see the full picture without opening the JSON. - Idempotent re-run : the script's
EXISTING_MEETING_ID=<n>short-circuit MUST be preserved. Operators may re-run the script after the session JSON evolves (rare — typically only the artefact path drifts) ; the existing Meeting is left untouched and only new attachments / agenda items would be added.
Alternatives rejected¶
- Keep the JSON-only audit trail (status quo before this ADR) : the JSON files in
committee/sessions/are correct + machine-readable, but invisible from the OP roadmap UI. Operators forget to grep them ; cross-Story aggregation requires a custom CLI. The OP UI provides the queryability for free. - Embed the verdict as a long OP Story comment : easier (no script needed), but loses the structured fields (participants, attachments, dedicated section per agenda item) and dilutes the Story comment thread with a 5-section verdict every time a committee runs. OP Meetings are the right object type for a meeting.
- Use OP Wiki pages instead of Meetings : Wiki pages are project-scoped and not natively listed under "what happened in this period?". Meetings have the calendar + past/upcoming filter that match the operator's "show me last week's reviews" use case.
- Use a custom WP type "Committee Session" : would create another WP visible in the roadmap, polluting the Story-vs-Epic-vs-Need taxonomy. Meetings are deliberately a separate object type that doesn't compete with WPs in the roadmap.
- Skip the Meeting and rely on the auto-syncer to post the verdict on the Story : already done (committee output is posted as an OP Story comment). Doesn't solve discoverability across Stories ; doesn't surface participants ; doesn't bundle attachments.
Consequences¶
Positive :
- Single queryable surface for committee history : OP "Past meetings" filter natively answers "all committee sessions in the last 30 days" without leaving the UI.
- Expert participation visible : OP search "meetings I participated in" works (with the locked-User trick) ; aggregating "experts most consulted" is a simple JOIN.
- Source artefacts bundled with verdict : the session JSON + reviewed dossier travel with the Meeting record ; reorganising files on disk doesn't break the audit trail.
- Forcing operators to follow ADR-0076 : OP becomes the SSoT for committee history alongside Story status. Memory files / chat transcripts no longer compete with OP for the verdict.
- WP "Meetings" tab populated :
--linked-wpmakes the meeting appear under the Story's Meetings tab, so reviewers landing on the Story see the committee context one click away.
Negative :
- Operator overhead : one extra command per committee session (
python scripts/op_save_committee_as_meeting.py ...). Mitigated by the script being idempotent + scriptable + ~10s wall-clock (Rails-runner phase + 2 multipart uploads). - kubectl dependency : the script requires
kubectl execaccess to the OP pod (Meetings POST is not exposed via the public OP REST API in OP 17.3.1). This couples the committee log step to cluster access. If kubectl access is lost, fall back to logging the verdict as an OP Story comment per ADR-0076. - Locked Users proliferate : 5 new
Userrecords per committee session type (one per expert). Idempotent byloginso capped at the number of unique expert ids over the project's lifetime — currently 5. Acceptable. - OP Meeting count grows : ~1 Meeting per Story ceremony (plan_review at start, pr_review at PR-open). For a 30-Story sprint that's ~60 Meetings. OP handles this without performance issues at the expected order of magnitude.
Neutral :
- The committee JSON stays the source of truth for content : the OP Meeting is the queryable surface, but if a discrepancy appears between the JSON and the Meeting agenda items, the JSON wins. Per ADR-0076, OP is SSoT for project state ; per this ADR, the JSON is SSoT for verdict content. The script's job is to keep them in sync at creation time ; subsequent edits to the Meeting agenda items are operator-discretionary annotations, not authoritative restatements.
- Auto-syncer scope unchanged : the existing OP auto-syncer (Story status transitions on PR merge events) is untouched by this ADR. The Meeting log is a separate object class that doesn't interact with Story state.
Rollback¶
If the OP Meeting log proves more friction than value (operator skips the step routinely → Meetings missing → audit trail incomplete) :
- Soft rollback : drop the "MUST" requirement to "SHOULD" in this ADR ; keep the script available for operators who want it but don't gate review evidence on it.
- Hard rollback : delete this ADR ; remove the script ; revert OPERATIONS §15 changes ; revert to the JSON-only audit trail per ADR-0052 + ADR-0068.
The Meetings already created in OP can stay (no harm) or be bulk-deleted via a one-shot Rails-runner snippet :
The locked Users created for experts can stay (no harm) or be bulk-deleted similarly.
References¶
- Parent need :
CVN-N001-EE(committee operational maturity) - Story that introduced this :
CVN-N001-EE-S14(Track 1 leakage investigation — first session for which the script was end-to-end tested) - Related ADRs : ADR-0052 (committee sessions JSON audit trail), ADR-0068 (committee as default review channel), ADR-0076 (OP as project memory SSoT)
- Script :
scripts/op_save_committee_as_meeting.py(file outside the docs site tree → absolute GH URL to keep the link clickable in MkDocs strict mode) - First Meeting created under this ADR : https://openproject.cvntrade.eu/meetings/2 — committee session
dd248118(plan_review on wp#103) - Operator runbook : OPERATIONS.md §15 (Expert Committee invocation flow)