0070 Participation Pipeline And Rules Schema
ADR-0070: Condition-driven participation — a recruit/collaborate/execute/enforce pipeline and a registry-declared participationRules schema
- Date: 2026-07-09
- Status: proposed
- Deciders: Gerald Dagher (owner)
- Supersedes: the recruitment scope of construct-ca4's watch-condition mechanism (
WATCHERS/evaluateWatchConditionsinlib/orchestration/routing-tables.mjs:29-39,145-159) — the ownership-lookup responsibilities of that same file (ownerForEvent,ownerForDoc,teamForSpecialist,specialistsInTeam,getTeamById) are unaffected and untouched by this ADR - Relates to:
docs/notes/research/2026-07-09-cdsp-substrate-reconciliation-audit.md(construct-pteo2.1, the substrate this ADR builds on), ADR-0025 (explicit activation model — the "aware, never ambient" precedent this pipeline's advisory default extends to sign-off), ADR-0033 (platform capability registry — the "agnostic, no hard-coding per platform" principle this schema's shape must not violate),schemas/scope.schema.json(existingteam.decisionRightsshape this ADR's enforcement opt-in reuses rather than duplicates)
Problem
Construct's specialist roster is fixed at 12 roles (specialists/org/specialists/*.json), but which of those 12 actually participate in a given piece of work is decided in at least three different, disconnected places today, none of which is condition-driven in the sense the construct-pteo2 epic requires ("best players on the field, in and out, configurable"). routeRequest (lib/orchestration/flow-selection.mjs:280-378) resolves a specialist list from intent/risk/workCategory classification plus a fixed set of 5 hardcoded watch-condition predicates (WATCHERS in lib/orchestration/routing-tables.mjs:29-39); invokeWorkflow (lib/embedded-contract/workflow-invoke.mjs:204-229) independently selects roles from a workflow definition's static chain unless the caller explicitly overrides it; runConstructArtifactLoop (lib/artifact-loop-core.mjs:311-348) calls invokeWorkflow without ever passing roleStrategy or requestedRoles, so an artifact-loop run always gets whatever def.chain says regardless of the artifact's actual risk surface. None of these three call sites shares a rule format: a specialist or team cannot be declared, once, in the registry, as "recruit me when X, in this role, with this sign-off weight" and have all three entry points honor it consistently. The result is exactly the gap construct-pteo2's epic description names: a roster role that should be pulled in for a legal/compliance-flagged change, or excluded from a narrow low-risk change, has no single declarative place to say so — each entry point would need its own bespoke condition logic, and today only one of the three (routeRequest) has any condition logic at all.
Context
construct-ca4 (closed, P1) shipped a partial version of this idea: WATCHERS is a fixed map of 5 named predicates (high-ambiguity-deep-work, visual-or-ui-risk, auth-payments-non-narrow, architecture-without-metric, wide-blast-radius) over the requestSignals shape, and a registry entry can reference one by name in a watchConditions array to claim ownership of it (apply() in lib/orchestration/routing-tables.mjs:83-113); evaluateWatchConditions(signals) (lib/orchestration/routing-tables.mjs:145-159) returns {specialist, reason, watcher} triples, which routeRequest folds into its triggers/dispatchReasons output (lib/orchestration/flow-selection.mjs:297-302). This is a real, working condition -> specialist mechanism, confirmed on staging by the cdsp.01 substrate audit (docs/notes/research/2026-07-09-cdsp-substrate-reconciliation-audit.md, row 1). But it has three structural gaps this ADR's schema must close, not duplicate: (1) adding a new watch condition requires a code change to the WATCHERS map — a registry entry can only re-bind an existing watcher to a different owner, never define a new condition; (2) a triggered watcher has no role (author/reviewer/advisor) or gate (advisory/enforced) — it is a flat specialist-gets-added signal with no sign-off semantics; (3) only routeRequest consults it — invokeWorkflow and runConstructArtifactLoop have no equivalent hook, confirmed by reading both files directly (lib/embedded-contract/workflow-invoke.mjs:204-229 selects roles from def.chain and roleStrategy alone; lib/artifact-loop-core.mjs:343-348 calls invokeWorkflow with no role arguments at all).
construct-lyxx (closed, P1) is the other half of the substrate: it retired hardcoded EVENT_OWNERSHIP/DOC_OWNERSHIP constants and made lib/orchestration/routing-tables.mjs resolve everything from loadRegistry() (specialists/org plus .cx/specialists/ project overlays), confirmed ON_STAGING in the same audit (row 2). This is the mechanism this ADR's schema extends: participationRules is registry-declared data in exactly lyxx's sense — it lives on a specialist or team's registry entry (or a project overlay), resolved at the same loadRegistry() boundary, not a new hardcoded constant.
The org hierarchy this pipeline recruits into already exists as registry data, not as something this ADR invents: specialists/org/groups/engineering-group.json carries "kind": "group", a decisionRights array, and a squads array; specialists/org/teams/design-team.json carries "kind": "squad", its own decisionRights/forbiddenDecisions, and a collaborators array; individual specialists execute under a team. schemas/scope.schema.json:47-56 already types a team's decisionRights as a plain string array with no semantics attached beyond naming a decision the team is authorized to make — this ADR is the first place that array gets read as an enforcement switch rather than as descriptive metadata, and registry:validate --unified already flags (as warnings, not errors) decisionRights/forbiddenDecisions entries with no matching policy id, which is the exact validation surface a future implementation of enforcementScope would extend.
Agnosticism and modularity invariants. ADR-0033 states this project's recurring principle plainly: capability/routing facts must not scatter as "inline branches and duplicated literals" per consumer, because that "contradicts the project's stated 'agnostic, no hard-coding per platform' principle" — ADR-0033's own fix was a single registry read by every host surface instead of six host-specific branches. This ADR's schema follows the identical discipline for a different axis: participationRules is host-agnostic (nothing in the schema or the three insertion points names a Claude Code, Codex, Cursor, or any other platform-specific concept — requestSignals, WATCHERS, and loadRegistry() are all already host-neutral, confirmed by reading each) and profile-agnostic (the schema has no field naming a specific specialists/org/scopes/ profile; a rule recruits by roster role or team id, which every profile shares, never by a profile-specific concept). It is also modular in the same sense specialists/org's existing per-file layout is: a participationRules array is data attached to one registry entry's own file (or a .cx/specialists/ overlay), so adding or changing a rule never requires touching a shared, monolithic file — the same modularity property construct-lyxx's registry resolver already established for events/docs/watchers.
Two user decisions bound this design and are not re-litigated here: (1) the sign-off gate defaults to advisory; enforcement is opt-in per team, expressed through that team's own decisionRights config — never a global switch and never the schema's default. (2) Legal/compliance is a first-class recruitment dimension, not a 13th role: this repo's root CLAUDE.md states a profile "selects flows + skill emphasis over the fixed 12-role roster — it does not invent new roles or departments," and the roster is confirmed fixed at 12 by specialists/org/specialists/*.json (architect, data-analyst, debugger, designer, engineer, operations, orchestrator, product-manager, qa, researcher, reviewer, security). A legal/compliance concern recruits cx-security, the existing role whose charter already covers policy/compliance surface area, rather than adding a cx-legal or cx-compliance specialist.
flowchart LR
A[Registry-declared participationRules] --> B[Recruit: when(signals) matches]
B --> C[Collaborate: squad reviews within group decisionRights]
C --> D[Execute: specialist runs, skill/flavor adapts]
D --> E{gate}
E -->|advisory| F[Sign-off recorded, never blocks]
E -->|enforced| G[Sign-off required per team decisionRights]
H[ca4 WATCHERS - 5 fixed predicates] -.superseded by.-> A
Decision
Define, as a registry-declared shape (schemas/participation-rules.schema.json), a participationRules array attachable to a specialist or team registry entry (or a .cx/specialists/ project overlay, following lyxx's existing overlay precedent), and the four-stage pipeline that consumes it:
- Recruit. A rule's
whencondition is evaluated againstrequestSignals— either a namedwatchCondition(one of ca4's existing 5, or one added under construct-pteo2.3's signal-set expansion) or asignalExpr(the generalized form ca4's fixedWATCHERSmap cannot express without a code change). A matching rule recruits itsrecruit.specialistsand/orrecruit.teamsin the statedrole(author | reviewer | advisor). - Collaborate. A recruited team collaborates at the squad level: it can review, advise, and escalate through its own
escalationPath(already present on every team/group registry entry), but a squad does not itself hold sign-off authority — that authority is a group-leveldecisionRightsgrant, per the hierarchy this ADR formalizes below. - Execute. The recruited specialist executes; a skill or role-flavor (
classifyRoleFlavors/roleFlavorsinlib/orchestration/flow-selection.mjs:260-278, already live) adapts how it executes without changing who is recruited or what role they hold. - Enforce. A rule's
gateisadvisoryby default: recruitment and any sign-off are recorded (trace/telemetry) but never block release. A rule may setgate: enforcedonly together with anenforcementScope: {team, decisionRight}naming adecisionRightthe recruiting team's own registry entry actually lists in itsdecisionRightsarray — enforcement is opt-in, per team, expressed through data that team already owns, never a schema-level or global default.schemas/participation-rules.schema.json'sallOf/if/thenmakesenforcementScopea hard requirement whenevergate: enforcedis set, so this is a structural constraint on the rule itself, not a convention a future implementer could skip.
Hierarchy semantics (already the registry's on-disk shape, formalized here as the semantics this pipeline recruits into): a group (specialists/org/groups/*.json, kind: "group") owns decision rights and lists the squads (kind: "squad", e.g. specialists/org/teams/design-team.json) under it; a squad collaborates — it reviews, advises, and can escalate through escalationPath, but does not unilaterally hold enforcement authority beyond what its owning group grants; a specialist (one of the fixed 12 roster roles) executes the work; a skill or role-flavor adapts execution style without changing who is recruited or what role/gate applies. This ADR does not change the group/squad/specialist registry shape — it only defines the rule format that recruits across it.
Legal/compliance binding. A participationRules entry may set dimension: "legal-compliance". The schema's second conditional requires such a rule's recruit.specialists to contain cx-security — a rule author cannot declare a legal/compliance dimension and recruit a specialist other than (or in addition to, but never instead of) cx-security for it. This is the schema enforcing the no-13th-role decision structurally: rosterSpecialist is a closed 12-value enum, so there is no roster value a rule could name to invent a cx-legal/cx-compliance specialist even if an author tried.
This dimension is not purely prospective: it already has a working trigger and a working contract, confirmed by direct read, that predate this ADR and that a dimension: "legal-compliance" rule formalizes rather than invents. detectRiskFlags (lib/orchestration/classification.mjs:205,233,280-283) sets a compliance flag from enterprise/procurement/audit language, a privacy flag from PII/retention/consent/data-export language, and folds legal review/compliance review/regulatory/gdpr/ccpa/hipaa/license-compliance/data-residency language into risk detection more broadly — compliance, privacy, and licensing are the three riskFlag names this dimension binds to, not new flags this ADR introduces. specialists/org/contracts/architect-to-legal-compliance.json already declares "trigger": {"riskFlags": ["compliance", "privacy", "licensing"]}, routes producer cx-architect to consumer cx-security, and requires a compliance-memo output (mustContain: risks, severity, remediation, plus two executable postconditions requiring Regulatory Citations and Remediation Plan sections); specialists/org/contracts/legal-compliance-to-release-manager.json chains that memo forward from cx-security to cx-operations as a release gate. Both contracts are correct as written and need no schema change — what they lack, and what this ADR's pipeline closes, is a guarantee that the pass actually runs: today a contract's trigger.riskFlags match is not evaluated by any of the three insertion points named below, so a compliance/privacy/licensing-flagged request can silently skip the cx-security compliance-memo pass entirely. A participationRules entry with dimension: "legal-compliance" and a signalExpr over these same three riskFlags is the condition-driven recruit step that closes that gap once a follow-on bead wires the evaluator into routeRequest/invokeWorkflow/runConstructArtifactLoop; until then, architect-to-legal-compliance.json's trigger.riskFlags remains advisory metadata, consistent with this ADR's advisory-default decision, not an enforced guarantee (construct-95phc.1).
Insertion points (naming exact locations; no execution logic changes in this ADR — these are the points a follow-on implementation bead wires against):
routeRequest—lib/orchestration/flow-selection.mjs:280. Already computessignalsandtriggersat lines 297-302 and folds them intoaugmentOpts/specialistsat lines 304-322; aparticipationRulesevaluation would extendproactiveTriggers/evaluateWatchConditions's role in this exact spot, producing role/gate-aware triggers instead of today's flat specialist-add.runConstructArtifactLoop—lib/artifact-loop-core.mjs:311. CallsinvokeWorkflowat lines 343-348 with noroleStrategy/requestedRoles, so the artifact's actual risk/signal surface never reaches role selection today; this is the insertion point construct-pteo2.8 (cdsp.30, "author_artifactrecruits") targets.invokeWorkflow—lib/embedded-contract/workflow-invoke.mjs:204. Selects roles viaselectRoles({ strategy, defChain: def.chain, requestedRoles, map, warnings })at line 229 — a staticdef.chainfloor; construct-pteo2.9 (cdsp.31, "roleChainis a floor, not a ceiling") is the bead scoped to let a matchedparticipationRulesentry add roles beyonddef.chainat this call.
Rationale
Reusing schemas/scope.schema.json's existing team.decisionRights array as the enforcement-opt-in mechanism (rather than inventing a parallel enforcement-config shape) means enforcement state lives in exactly one place per team, and registry:validate --unified's existing decision-no-policy/forbidden-decision-invalid warnings already audit that array's consistency — this ADR's enforcementScope gets that validation surface for free rather than needing a second one. Making watchCondition one valid form of when (rather than replacing ca4's mechanism outright) means the 5 shipped watchers keep working unmodified through the recruit stage; nothing in construct-ca4's contract needs to be torn out for this schema to land, which matters because evaluateWatchConditions is called from routeRequest today and any breaking change there would need its own migration, not a documentation ADR. Closing rosterSpecialist as a 12-value enum (rather than an open string) is the only way a JSON Schema can make "no 13th role" a validation failure instead of a reviewer's manual check — the alternative (an open string plus a written rule) is exactly the kind of guidance-only constraint ADR-0067 already rejected for its own fan-out restriction, for the same reason: a validation-time restriction is strictly more reliable than a convention a future author could miss under pressure.
Rejected alternatives
- Give legal/compliance its own 13th specialist role (
cx-legalorcx-compliance). Rejected by explicit user decision, and independently foreclosed by this repo'sCLAUDE.md, which fixes the roster at 12 and states a profile "does not invent new roles or departments" — a 13th role would require reopening that constraint for a single dimension, when the existingcx-securitycharter already covers compliance/policy surface area. - Make
enforcedthe schema default, with teams opting out. Rejected by explicit user decision: an opt-out default means every existing team registry entry silently gains a blocking gate the moment this schema is adopted, with no team having affirmatively asked for it — the opposite of the "opt-in per team" instruction, and a much larger blast radius for a documentation-stage ADR to impose on every in-flight run. - Replace
construct-ca4'sWATCHERS/evaluateWatchConditionsoutright instead of treating it as one form ofwhen. Rejected: ca4 is confirmedON_STAGINGand actively consulted byrouteRequesttoday (docs/notes/research/2026-07-09-cdsp-substrate-reconciliation-audit.mdrow 1); a hard replacement would be a breaking runtime change bundled into what this bead scopes as a documentation-only ADR, and would discard 5 working predicates for no capability gain —participationRulesis a strict superset (adds role/gate/dimension the ca4 shape has no field for), not a competing mechanism, so keepingwatchConditionas a validwhenform costs nothing and preserves continuity. - Attach
participationRulesonly to teams, never to individual specialists. Rejected:construct-pteo2.6(cdsp.21, "coverage: every specialist/team is recruitable") requires any roster member to be independently recruitable, and several existingwatchConditionsalready bind to a single specialist rather than a whole team (apply()inlib/orchestration/routing-tables.mjs:101-112resolves a watcher to onecxId, not a team) — restricting the new schema to teams-only would be a capability regression from the mechanism it supersedes. - Define the rule format inline in each of the three insertion points (
routeRequest,invokeWorkflow,runConstructArtifactLoop) rather than as one registry-declared schema. Rejected: this is exactly the fragmentation this ADR's Problem section describes as the current state, and would leave a specialist/team recruitment declared three different ways depending on which entry point read it — the entire point of a registry-declared schema (per lyxx's precedent) is that one rule, read once atloadRegistry(), governs all three consumers identically.
Consequences
A follow-on implementation bead (tracked by the construct-pteo2 epic's children: cdsp.10/.11 signal-set expansion, cdsp.30/.31 wiring into runConstructArtifactLoop/invokeWorkflow, cdsp.60/.61 Org Studio and CLI/MCP authoring surfaces) must write the actual evaluator that reads participationRules from the assembled registry and calls it from the three named insertion points — this ADR defines the shape and the pipeline stages, not the evaluator code. construct-ca4's WATCHERS/evaluateWatchConditions continues to run unmodified until a future bead explicitly migrates a given watcher into a participationRules entry (mirroring ADR-0067's own migration posture for its superseded mechanism); there is no forced cutover. Any team that wants enforcement must explicitly add the relevant decisionRight to its own registry entry — silence stays advisory, so adopting this schema changes no existing run's blocking behavior until a team opts in. registry:validate --unified's existing decision-rights warnings become load-bearing the moment a real enforcementScope references a decisionRight — a future implementation should consider promoting the relevant decision-no-policy case from warning to error for rules with gate: enforced, though this ADR does not itself change that validator's severity.
Reversibility
Two-way door at the schema level: schemas/participation-rules.schema.json is a freestanding reference schema (matching schemas/execution-policy.schema.json's existing pattern — not wired into registry:validate --unified's invariants), so adopting it costs nothing to undo; no registry entry is required to declare a participationRules array, and none does yet. Once a future bead wires an evaluator into routeRequest, invokeWorkflow, or runConstructArtifactLoop and teams begin setting gate: enforced in production, reversing becomes a real migration — every enforcementScope binding would need to be audited before the mechanism could be retired, the same one-way cost ADR-0067 describes for a ported-and-deleted flow chain. This ADR's own scope (schema + pipeline definition, no wiring) stays fully reversible.
References
docs/notes/research/2026-07-09-cdsp-substrate-reconciliation-audit.md(construct-pteo2.1 — substrate matrix this ADR's supersession and insertion-point claims are grounded in)lib/orchestration/routing-tables.mjs:29-39,83-113,145-159(construct-ca4'sWATCHERS/apply/evaluateWatchConditions— the mechanism this schema is a superset of)lib/orchestration/flow-selection.mjs:260-278,280-378(routeRequest, role-flavor adaptation — one of three named insertion points)lib/artifact-loop-core.mjs:311-348(runConstructArtifactLoop— second named insertion point)lib/embedded-contract/workflow-invoke.mjs:204-229(invokeWorkflow— third named insertion point)
The next two references ground the "Legal/compliance binding" section's claim that the dimension already has a working trigger and contract this ADR formalizes, not invents.
specialists/org/contracts/architect-to-legal-compliance.json,specialists/org/contracts/legal-compliance-to-release-manager.json(existingcompliance/privacy/licensingriskFlag trigger and compliance-memo handoff the "Legal/compliance binding" section formalizes, not invents; annotated with this ADR per construct-95phc.1)lib/orchestration/classification.mjs:205,233,280-283(detectRiskFlags— source of thecompliance,privacy, andlicensingriskFlag names the legal-compliance dimension binds to)
The remaining references ground the hierarchy semantics, the enforcement-opt-in reuse, and the prior-art posture this ADR follows rather than invents.
specialists/org/groups/engineering-group.json,specialists/org/teams/design-team.json(existing group/squaddecisionRights/kindshape this ADR's hierarchy semantics formalizes, not invents)schemas/scope.schema.json:47-56(existingteam.decisionRightsarray this ADR'senforcementScopereuses)- ADR-0025 (explicit activation model — "aware, never ambient" precedent for the advisory default)
- ADR-0033 (platform capability registry — "agnostic, no hard-coding per platform" principle this schema's host/profile-neutral shape follows)
- ADR-0067 (deterministic flow engine — precedent for superseding-without-deleting a working mechanism, and for validation-time restrictions over guidance-only ones)
- JSON Schema 2020-12 core specification, <https://json-schema.org/draft/2020-12/schema> (the
$schemadialectschemas/participation-rules.schema.jsonand its siblings declare)