Skip to content

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 :

  1. 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".
  2. 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.
  3. 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".
  4. 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 in committee/sessions/<id>_artifact.md.
  5. Forcing function — 2026-05-05 : dd248118 plan_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 via scripts/op_save_committee_as_meeting.py after-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 :

  1. Operator runs python scripts/expert_committee.py --artifact ... --question ... --session-type ... --issue ... per ADR-0068 — produces committee/sessions/<session_id>_committee.json + committee/sessions/<session_id>_artifact.md.
  2. 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 User per committee expert (idempotent : looked up by login=<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#
  3. 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 for plan_review + pr_review + experiment_review sessions. Only --session-type general may omit the link (rare). The link surfaces as an agenda item of item_type=work_package so the Meeting appears on the WP's "Meetings" tab.
  • State must be closed at creation : OP's Meeting.states enum is {open:0, draft:1, in_progress:3, cancelled:4, closed:5}. The MeetingsController#load_query hides draft from the project meetings index by default. Committee sessions are concluded events ; state=closed is 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 as invited+attended.
  • Experts as locked Users, not PlaceholderUsers : MeetingParticipant.belongs_to :user restricts the polymorphic association to the User class — PlaceholderUser instances raise Validation failed: User can't be blank. Experts are persisted as User.status="locked" records with login=<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 Attachment records on the Meeting (via POST /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-wp makes 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 exec access 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 User records per committee session type (one per expert). Idempotent by login so 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) :

  1. 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.
  2. 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 :

Meeting.where(project_id: 3).where("title LIKE 'Committee %'").destroy_all

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)