Constructdocs
section · decisions

0070 Participation Pipeline And Rules Schema

14 min read·decisions / adr / 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 / evaluateWatchConditions in lib/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 (existing team.decisionRights shape 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:

  1. Recruit. A rule's when condition is evaluated against requestSignals — either a named watchCondition (one of ca4's existing 5, or one added under construct-pteo2.3's signal-set expansion) or a signalExpr (the generalized form ca4's fixed WATCHERS map cannot express without a code change). A matching rule recruits its recruit.specialists and/or recruit.teams in the stated role (author | reviewer | advisor).
  2. 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-level decisionRights grant, per the hierarchy this ADR formalizes below.
  3. Execute. The recruited specialist executes; a skill or role-flavor (classifyRoleFlavors/roleFlavors in lib/orchestration/flow-selection.mjs:260-278, already live) adapts how it executes without changing who is recruited or what role they hold.
  4. Enforce. A rule's gate is advisory by default: recruitment and any sign-off are recorded (trace/telemetry) but never block release. A rule may set gate: enforced only together with an enforcementScope: {team, decisionRight} naming a decisionRight the recruiting team's own registry entry actually lists in its decisionRights array — 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's allOf/if/then makes enforcementScope a hard requirement whenever gate: enforced is 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):

  • routeRequestlib/orchestration/flow-selection.mjs:280. Already computes signals and triggers at lines 297-302 and folds them into augmentOpts/specialists at lines 304-322; a participationRules evaluation would extend proactiveTriggers/evaluateWatchConditions's role in this exact spot, producing role/gate-aware triggers instead of today's flat specialist-add.
  • runConstructArtifactLooplib/artifact-loop-core.mjs:311. Calls invokeWorkflow at lines 343-348 with no roleStrategy/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_artifact recruits") targets.
  • invokeWorkflowlib/embedded-contract/workflow-invoke.mjs:204. Selects roles via selectRoles({ strategy, defChain: def.chain, requestedRoles, map, warnings }) at line 229 — a static def.chain floor; construct-pteo2.9 (cdsp.31, "roleChain is a floor, not a ceiling") is the bead scoped to let a matched participationRules entry add roles beyond def.chain at 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-legal or cx-compliance). Rejected by explicit user decision, and independently foreclosed by this repo's CLAUDE.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 existing cx-security charter already covers compliance/policy surface area.
  • Make enforced the 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's WATCHERS/evaluateWatchConditions outright instead of treating it as one form of when. Rejected: ca4 is confirmed ON_STAGING and actively consulted by routeRequest today (docs/notes/research/2026-07-09-cdsp-substrate-reconciliation-audit.md row 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 — participationRules is a strict superset (adds role/gate/dimension the ca4 shape has no field for), not a competing mechanism, so keeping watchCondition as a valid when form costs nothing and preserves continuity.
  • Attach participationRules only 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 existing watchConditions already bind to a single specialist rather than a whole team (apply() in lib/orchestration/routing-tables.mjs:101-112 resolves a watcher to one cxId, 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 at loadRegistry(), 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's WATCHERS/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 (existing compliance/privacy/licensing riskFlag 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 the compliance, privacy, and licensing riskFlag 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/squad decisionRights/kind shape this ADR's hierarchy semantics formalizes, not invents)
  • schemas/scope.schema.json:47-56 (existing team.decisionRights array this ADR's enforcementScope reuses)
  • 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 $schema dialect schemas/participation-rules.schema.json and its siblings declare)