DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path phases/phase-37-adapter-chaining-and-composition/phase-37-cr-adapter-chaining-and-composition-v0_2.md

Loomworks — Phase 37: Adapter chaining and composition — CR

Version. 0.2 Date. 2026-05-05 CR number. CR-2026-052 Provenance. Claude.ai CR drafting session. Operator: Marvin Percival. v0.2 incorporates CC pre-approval audit findings (5 blockers, 7 recommendations resolved). Status. Approved for execution. Strategy document. loomworks-engine-implementation-strategy-v0_2.md Section 7. This CR translates the design intent in Section 7 into Claude Code execution. Scoping note. loomworks-phase-37-scoping-note-v0_1.md — all eight scoping decisions settled and adopted. Sits alongside. Operator Layer v0.6 (the architecture this engine work serves).


1. What this builds

Today's rendering is single-step: one specialist produces one render from one shape. Phase 37 adds RenderComposition — an orchestrated sequence of adapter calls that can chain production, review, translation, and revision into a coherent lifecycle.

The canonical use case: produce a contract from a legal-document shape using a document specialist, then send it to Gemini for review, then revise the contract based on the review findings, then produce a final PDF. Today this requires four manual render triggers. Phase 37 makes it one composition.

This CR builds three pieces:

Piece 1 — RenderComposition model. A new MemoryObject type with event-log-canonical storage and a materialized state machine. Six event kinds track the lifecycle: composition_initiated, composition_step_started, composition_step_completed, composition_completed, composition_failed, composition_revision_triggered.

Piece 2 — Sequential orchestration. A background task walks the composition's steps in declared order. Each step dispatches to the appropriate specialist. Five step types map to specialist methods: produce, review, translate, transform, validate. Review steps that trigger revision are Operator-gated — the composition pauses until the Operator decides.

Piece 3 — Composition endpoints. Creation, state query, list, and the Operator decision endpoint for review-triggered revision.

This is the fourth of seven engine phases (Phases 34 through 40) the Operator Layer architecture commits the engine to. It completes Lane A (specialist plumbing). Phase 40 wraps it with the Operator-vocabulary endpoint.


2. Strategy and scoping decisions consumed

From engine implementation strategy v0.2 §7

S1 — RenderComposition as MemoryObject. Event-log canonical, materialized view. Standard pattern matching ExternalProductionRecord (Phase 34) and all prior MemoryObjects.

S2 — State lives in the database, not in process memory. Composition state survives process restart. The background task on startup reads any in-progress compositions and resumes. Same pattern as Phase 34's external-polling restart.

S3 — Specialist contract reuse, not extension. Phase 37 does not modify the RenderSpecialist base class. Producer steps call produce_render or rerender (Phase 36) through the existing dispatch wrapper. Review, translate, transform, and validate steps call specialist methods on new specialist categories — but these are separate specialist types, not modifications to the render specialist contract.

From scoping note v0.1 — eight decisions adopted

N1 — Six event kinds: composition_initiated, composition_step_started, composition_step_completed, composition_completed, composition_failed, composition_revision_triggered. Each event carries the composition ID, the step index, and the step result.

N2 — Five step types: produce, review, translate, transform, validate. Each step type determines which specialist method is called.

N3 — Dual endpoint surface. Engine endpoint at /engagements/{eid}/compositions. Phase 40 wraps it with the Operator-vocabulary endpoint at /operator/projects/{id}/render_with_review. This CR builds the engine endpoint only.

N4 — Operator-gated revision (Position B). Review step output surfaces as a needs_you item. The Operator decides whether to accept or trigger revision. The composition enters waiting_for_operator_decision state. No automatic revision. Prior position (strategy doc §7.4 allows automatic revision) set aside.

N5 — Ordering constraints enforced at creation. No reviewer step before its producer step. Invalid compositions → 422.

N6 — Sequential only. Parallel steps deferred. Prior position (strategy doc §7.5 includes parallel groups) set aside.

N7 — Process restart survival. Event-log canonical, materialized view. Background task resumes on startup.

N8 — Migration number. Next available at execution time (likely 0056).


3. Pre-flight and verification discipline

3.1 Lesson from Phases 36, 38, and 39

Three consecutive CRs went through v0.1 → amendment → v0.2 because the CRs were drafted against assumed schemas rather than verified ones. CC's pre-flight audit caught 4–8 blockers each time. v0.2 of this CR incorporates CC's pre-approval audit findings directly, reducing the amendment cycle.

This CR's discipline: Architectural decisions and step ordering are stated firmly. Substrate field names, model locations, table names, and status values that have not been verified against the live codebase are marked with [CC verifies]. The CR's job is to communicate what to build and in what order. CC's job is to verify names against the actual substrate before executing.

3.2 Step 0 — archive + pre-flight

Archive this CR to docs/phase-crs/phase-37-cr-adapter-chaining-and-composition-v0_2.md.

Pre-flight inspection — CC confirms each of these before proceeding:

  1. Phase 34 landed. ExternalProductionRecord model exists. ExternalProductionHandle and ExternalProductionFailure types exist. _run_specialist_with_external_dispatch_handling wrapper exists. ExternalPollingLoop background task exists and starts in the lifespan. [CC verifies exact module paths]
  1. Phase 36 landed. prior_render_ref and revision_strategy fields exist on RenderEvent. supports_incremental_rerender attribute exists on RenderSpecialist. rerender method exists on RenderSpecialist. StubRenderSpecialist has configurable supports_incremental_rerender. [CC verifies exact module paths and field names]
  1. Phase 36 B1 kwargs-pass design. produce_render and rerender both accept engine-determined kwargs: display_number, prior_render_ref, revision_strategy. The specialist threads these into the event payload. [CC verifies the actual signature]
  1. MemoryObject type registration pattern. How new MemoryObject types are registered — migration or code constant. [CC verifies by inspecting Phase 34's ExternalProductionRecord registration]
  1. Event kind registration pattern. How new event kinds are registered — same mechanism as above. [CC verifies by inspecting Phase 34's event kinds]
  1. BackgroundAgentRunner dispatch path. Location and signature of the function that dispatches render requests to specialists. [CC verifies exact function name and location]
  1. Lifespan background task registration pattern. How ExternalPollingLoop was registered in the FastAPI lifespan — Phase 37's composition orchestrator follows the same pattern. [CC verifies]
  1. needs_you item surfacing. Phase 39 landed — the needs_you query is a hard-coded UNION ALL over pending_shape + held_assertion in dashboard.py:_NEEDS_YOU_SQL_TEMPLATE. The schema enum reserves review_triggered_revision (item_kind). Phase 37 adds a new SELECT to this UNION ALL for compositions in waiting_for_operator_decision state. [CC verifies the exact SQL template location and enum]
  1. Phase 40 status. NOT landed — no /operator/projects or render_with_review endpoint. Step 9 is a no-op. [CC verifies]
  1. ExternalProductionRecord operational view structure. Confirm column names and the linkage to render_jobs via render_job_id. Phase 37 extends this view with nullable composition_object_id and composition_step_index. [CC verifies existing columns and constraint patterns]
  1. Baseline numbers. Tests: 1,414 passed, 2 skipped (post-Phase 39). Alembic head: 0055. Next migration: 0056 (or whatever is next available). [CC verifies]

Pre-flight divergence protocol. If any of items 1–11 diverge from this CR's assumptions, CC stops at Step 0 and produces a blocker report. The Operator decides whether to amend the CR or proceed with CC's recommended adjustments. Do not proceed through divergence.


4. RenderComposition model

4.1 MemoryObject type

RenderComposition is a new MemoryObject type. Canonical persistence is in memory_events; materialization into current_memory_objects happens through the existing projector infrastructure.

[CC verifies: the exact module where new MemoryObject types are declared, and the registration mechanism — follow the pattern established by ExternalProductionRecord in Phase 34.]

4.2 Event kinds

Six event kinds registered for the render_composition object type:

| Event kind | When written | Payload contents | |---|---|---| | composition_initiated | Composition created | Full step list, shape ref, triggered_by, trigger_reason | | composition_step_started | Orchestrator enters a step | Step index, step type, specialist ref | | composition_step_completed | A step finishes successfully | Step index, step type, result reference (the RenderEvent ref or review report ref produced by the step) | | composition_failed | A step fails or composition aborts | Step index (if step-level failure), failure_detail, error_code. Step index is null for orchestrator-level failures: specialist resolution fails on resume, dispatch precondition fails, invalid composition state encountered on restart. | | composition_completed | All steps finished | Summary: step count, final result references | | composition_revision_triggered | Operator triggers revision after review | Reviewing step index, producer step index being revised, Operator's decision rationale (if provided), review_result_ref (the review step's output RenderEvent ref) |

[CC verifies: event kind registration mechanism — follow Phase 34's pattern.]

4.3 Composition state machine

The composition's current state is materialized from its event log. States:


initiated
  → step_in_progress          (orchestrator writes composition_step_started)
  → waiting_for_operator_decision  (review step's composition_step_completed written)
  → completed                 (composition_completed written)
  → failed                    (composition_failed written)

Every state has a backing event.

Transitions:

Projector materialization rules:

4.4 Composition step definition

Each step in the composition is defined at creation time:


# Conceptual — CC uses actual base classes and patterns from the substrate
class CompositionStepDefinition:
    step_index: int                    # 0-based position
    step_type: Literal["produce", "review", "translate", "transform", "validate"]
    specialist_ref: ActorRef           # The specialist that handles this step
    config: dict                       # Step-specific configuration (opaque to the engine)

Step types are JSONB-embedded in the steps column on the operational view (§4.7). No separate DDL column for step_type. Validation of step_type values is in the endpoint logic (§6.1), not a CHECK constraint.

[CC verifies: ActorRef type location and usage pattern. For Phase 37, specialist_ref uses agent-kind ActorRefs only. Human reviewer specialists (person/contributor as specialist_ref) are future work.]

4.5 Step type dispatch mapping

Each step type maps to a specialist method:

| Step type | Specialist category | Method called | External dispatch? | |---|---|---|---| | produce | RenderSpecialist | produce_render or rerender (via existing dispatch wrapper — uses Phase 36 orchestration branch) | Yes — via existing ExternalPollingLoop through render_jobs | | review | ReviewerSpecialist (new) | review_render | Yes — via composition-linked ExternalProductionRecord (§4.8) | | translate | TranslationSpecialist (new) | translate_render | No — synchronous only in Phase 37 | | transform | TransformSpecialist (new) | transform_render | No — synchronous only in Phase 37 | | validate | ValidationSpecialist (new) | validate_render | No — synchronous only in Phase 37 |

Important: The produce step type reuses the existing render dispatch path — the same _run_specialist_with_external_dispatch_handling wrapper (or whatever it's actually called — [CC verifies]). This means produce steps within a composition use Phase 34's external-polling mechanism and Phase 36's incremental re-render lineage automatically.

Synchronous-only restriction: If a translate, transform, or validate specialist returns ExternalProductionHandle, the orchestrator writes composition_failed with error_code="external_dispatch_not_supported_for_step_type". External dispatch for these step types is future work.

4.6 New specialist categories

Phase 37 introduces four new specialist base classes alongside the existing RenderSpecialist:


class ReviewerSpecialist:
    """Takes an artifact (RenderEvent ref) and optionally the source shape,
    produces a review report as a new RenderEvent."""

    async def review_render(
        self, *,
        subject_render_event_ref: MemoryRef,    # The artifact to review
        shape_event_ref: MemoryRef | None,       # The source shape, if available
        engagement_id: UUID,
        triggered_by: ActorRef,
        trigger: str,                            # Free-text reason, from trigger_reason
        config: dict,                            # Step-specific config from composition
        db: AsyncSession,
    ) -> RenderEvent | ExternalProductionHandle:
        raise NotImplementedError


class TranslationSpecialist:
    """Takes an artifact and produces a translated version."""

    async def translate_render(
        self, *,
        subject_render_event_ref: MemoryRef,    # The artifact to translate
        engagement_id: UUID,
        triggered_by: ActorRef,
        trigger: str,
        config: dict,
        db: AsyncSession,
    ) -> RenderEvent:
        """Must complete synchronously in Phase 37."""
        raise NotImplementedError


class TransformSpecialist:
    """Takes an artifact and applies a format transformation."""

    async def transform_render(
        self, *,
        subject_render_event_ref: MemoryRef,    # The artifact to transform
        engagement_id: UUID,
        triggered_by: ActorRef,
        trigger: str,
        config: dict,
        db: AsyncSession,
    ) -> RenderEvent:
        """Must complete synchronously in Phase 37."""
        raise NotImplementedError


class ValidationSpecialist:
    """Takes an artifact and runs acceptance criteria."""

    async def validate_render(
        self, *,
        subject_render_event_ref: MemoryRef,    # The artifact to validate
        engagement_id: UUID,
        triggered_by: ActorRef,
        trigger: str,
        config: dict,
        db: AsyncSession,
    ) -> RenderEvent:
        """Must complete synchronously in Phase 37."""
        raise NotImplementedError

[CC verifies: the exact signature kwargs should match established patterns. The key design point is that ReviewerSpecialist can return ExternalProductionHandle for Phase 34 integration; the other three return RenderEvent only in Phase 37.]

Stub specialists. Phase 37 ships stub implementations for all four new categories, following the StubRenderSpecialist pattern. The stubs are deterministic and configurable for testing.

4.7 Operational view

A render_compositions_view (or equivalent — [CC verifies naming convention]) materializes the current state of each composition from its event log. Columns:

One read at restart provides everything the orchestrator needs — step list, current position, state.

[CC verifies: operational view creation pattern — follow ExternalProductionRecord's view.]

4.8 ExternalProductionRecord extension for review steps

Phase 37 extends the ExternalProductionRecord operational view with two nullable columns for composition-step linkage:

Constraint: Exactly one of render_job_id or composition_object_id is non-null. [CC verifies: whether this is a CHECK constraint on the operational view or enforced in the projector.]

When a review step's specialist returns ExternalProductionHandle, the composition orchestrator creates an ExternalProductionRecord with the composition-step linkage (instead of render_job_id). The existing ExternalPollingLoop picks it up — see §7.4 for polling loop extension.


5. Migration

5.1 Migration number

Next available at execution time — likely 0056. [CC verifies current Alembic head.]

5.2 Migration contents

  1. Register render_composition as a MemoryObject type (following the Phase 34 pattern for external_production_record).
  2. Register six event kinds: composition_initiated, composition_step_started, composition_step_completed, composition_completed, composition_failed, composition_revision_triggered.
  3. Create the operational view for RenderComposition state materialization (§4.7). The state column uses Text type, matching external_production_records_view convention.
  4. Add nullable composition_object_id (UUID) and composition_step_index (int) to ExternalProductionRecord operational view. Add CHECK constraint: exactly one of render_job_id or composition_object_id is non-null.

[CC verifies: the exact mechanism for type and event-kind registration — CHECK constraint extension, enum extension, or code constant, depending on what Phase 34 established.]

No backfill needed — no existing compositions. No backfill needed for the EPR extension — existing EPR rows all have render_job_id set and null composition columns.


6. Composition creation endpoint

6.1 POST /engagements/{eid}/compositions

Creates a new composition and initiates orchestration.

Request body:


{
    "confirmed_shape_event_ref": "<MemoryRef>",
    "steps": [
        {
            "step_type": "produce",
            "specialist_ref": "<ActorRef>",
            "config": {}
        },
        {
            "step_type": "review",
            "specialist_ref": "<ActorRef>",
            "config": {}
        }
    ],
    "trigger_reason": "Produce contract and review with Gemini"
}

Validation (synchronous, before 202):

  1. confirmed_shape_event_ref must reference a confirmed shape within the engagement.
  2. Steps must be non-empty.
  3. Ordering constraint: no review step before a produce step in the same composition. (A composition with only review steps and no produce step is invalid — there's nothing to review.)
  4. Each specialist_ref must resolve to a registered specialist of the correct category for its step type.
  5. step_type must be one of produce, review, translate, transform, validate.
  6. Validation failure → 422 with a clear message naming the constraint violated.

On success:

  1. Write composition_initiated event with the full step list and metadata.
  2. Return 202 with the composition ID and initial state.
  3. Dispatch the composition to the background orchestrator (same pattern as render dispatch — [CC verifies the exact dispatch mechanism]).

Response schema:


{
    "composition_id": "<UUID>",
    "state": "initiated",
    "steps": [...],
    "created_at": "..."
}

[CC verifies: the response schema should follow established patterns for 202 async-dispatch responses in the substrate.]

6.2 Authentication and authorization

The composition endpoint uses get_contributing_contributor (from src/loomworks/api/deps.py), which transitively uses get_current_contributor and supports both session-cookie and bearer paths. This is the same dependency as existing engagement-scoped POST endpoints. Do not use get_current_person (the dashboard pattern).


7. Sequential orchestration

7.1 Orchestrator background task

A new background task — CompositionOrchestrator or similar — runs compositions to completion. It follows the same lifespan registration pattern as ExternalPollingLoop (Phase 34). [CC verifies the exact registration pattern.]

On startup: The orchestrator reads compositions in initiated or step_in_progress state from the operational view and resumes them. Same restart-survival pattern as Phase 34.

7.2 Step execution loop

For each composition, the orchestrator walks steps sequentially:


for step_index in range(current_step_index, len(composition.steps)):
    step = composition.steps[step_index]

    # Write composition_step_started event
    # State materializes to step_in_progress via projector

    if step.step_type == "produce":
        result = dispatch_produce_step(step, composition)

    elif step.step_type == "review":
        result = dispatch_review_step(step, composition)
        # After review completes:
        # Write composition_step_completed event
        # State materializes to waiting_for_operator_decision
        # STOP — wait for Operator decision via the decision endpoint
        return  # Orchestrator exits; resumes when Operator decides

    elif step.step_type in ("translate", "transform", "validate"):
        result = dispatch_generic_step(step, composition)
        # If specialist returns ExternalProductionHandle:
        #   → write composition_failed with error_code
        #     "external_dispatch_not_supported_for_step_type"
        #   → return

    # Write composition_step_completed event with result reference
    # If this was the last step: write composition_completed event

    # If step failed: write composition_failed event, stop

7.3 Produce step dispatch

The produce step type calls the existing render dispatch path — the function that resolves a RenderSpecialist and calls produce_render or rerender via _run_specialist_with_external_dispatch_handling [CC verifies actual name].

Key integration points:

Waiting for external completion (produce steps): When a produce step dispatches externally, the ExternalPollingLoop handles it through the existing render_jobs linkage. The composition orchestrator does not busy-wait. It checks for step completion on a schedule — when the ExternalPollingLoop resolves the external production and writes the RenderEvent, the orchestrator on its next check finds the step completed and writes composition_step_completed. [CC determines the exact check mechanism — options include: periodic composition-state check in the orchestrator's main loop, or a notification from the polling loop.]

7.4 Review step dispatch and external polling

The review step calls reviewer_specialist.review_render(...) with the prior step's result (the RenderEvent ref from the produce step) as subject_render_event_ref.

Synchronous review: If the reviewer returns a RenderEvent directly, write composition_step_completed and transition to waiting_for_operator_decision.

External review (e.g., Gemini): If the reviewer returns ExternalProductionHandle:

  1. Create an ExternalProductionRecord with composition-step linkage: composition_object_id = composition ID, composition_step_index = current step index, render_job_id = null.
  2. The ExternalPollingLoop picks up this record on its next sweep.
  3. The orchestrator exits and waits.

Polling loop extension: ExternalPollingLoop._find_due_records gains a second query (or UNION) that includes composition-step-linked records alongside render-job-linked records. _poll_one dispatches identically regardless of linkage — the specialist's poll_external_work signature doesn't change.

Resolution callback for composition-linked records: When ExternalPollingLoop._mark_completed resolves a composition-linked record:

After the review completes (synchronous or external):

  1. composition_step_completed event carries the review report (a RenderEvent containing the review findings).
  2. State materializes to waiting_for_operator_decision.
  3. The composition in waiting_for_operator_decision state appears in the /me/dashboard/needs_you endpoint (§7.7).
  4. The orchestrator stops. It resumes when the Operator submits a decision via the decision endpoint (§8).

7.5 Translate, transform, validate step dispatch

These step types call their respective specialist methods with the prior step's output as subject_render_event_ref. They do not pause for Operator decisions — they either complete and advance, or fail.

Synchronous only in Phase 37. If any of these step types returns ExternalProductionHandle, the orchestrator writes composition_failed with error_code="external_dispatch_not_supported_for_step_type". External dispatch for non-produce, non-review step types is future work.

7.6 Failure handling

If any step fails:

  1. Write composition_failed event with failure_detail and error_code. step_index is present when the failure occurred during step execution. step_index is null for orchestrator-level failures: specialist resolution fails on resume, dispatch precondition fails, invalid composition state encountered on restart.
  2. The composition enters failed state.
  3. No rollback of prior steps — their results remain as produced. (Rollback is explicitly out of scope per scoping note §7.)

7.7 Step output as next step input

Each step receives the prior step's output as its input reference. The composition step result (stored in the composition_step_completed event payload) includes the RenderEvent ref produced by that step. The next step receives this ref as subject_render_event_ref.

For the first step (step 0), the input is the composition's confirmed_shape_event_ref.

7.8 needs_you SQL integration

Add a new SELECT to the _NEEDS_YOU_SQL_TEMPLATE UNION ALL in dashboard.py [CC verifies exact location]. Query render_compositions_view filtered on state='waiting_for_operator_decision'. Item kind: review_triggered_revision (already reserved in Phase 39's schema enum).


8. Operator decision endpoint

8.1 POST /engagements/{eid}/compositions/{cid}/steps/{step_index}/decide

Called when a review step has completed and the composition is in waiting_for_operator_decision state.

Request body:


{
    "decision": "accept" | "revise",
    "rationale": "Optional — Operator's reason for the decision"
}

Validation:

  1. Composition must exist and belong to the engagement.
  2. Composition must be in waiting_for_operator_decision state.
  3. step_index must reference the review step that triggered the pause.
  4. decision must be "accept" or "revise".

On "accept":

  1. The composition resumes from the step after the review step.
  2. Write composition_step_started for the next step; orchestrator picks it up.
  3. If the review step was the last step, write composition_completed.

On "revise":

  1. Write composition_revision_triggered event with the Operator's rationale and the review_result_ref (the review step's output RenderEvent ref).
  2. Identify the producer step (the most recent produce step before the review step).
  3. Re-run the producer step. The orchestrator pins prior_render_event_ref to the producer step's original result (from composition_step_completed payload). Does NOT use auto-discovery (find_most_recent_produced_render or equivalent). Calls rerender if supports_incremental_rerender is true, otherwise produce_render, with engine-determined display_number, prior_render_ref, and revision_strategy per the Phase 36 kwargs-pass design.
  4. After the re-render completes, resume from the review step (re-run the review on the new artifact).

Review report context for the specialist: The review report does not flow via a new kwarg on rerender (which would modify the Phase 36 specialist contract). Instead, the composition_revision_triggered event payload includes the review_result_ref. The specialist can query the composition's event log to retrieve the review report if it needs contextual feedback. Phase 37's stub specialists do not consume the review report — they produce canned revised renders. Real specialists (Arc 6 Phase D) will read the composition context when they arrive. No signature change to RenderSpecialist.rerender.

Revision cycles. Each revision cycle re-runs the producer step, re-runs the review step, and pauses for Operator decision. There is no hard limit on revision cycles; the Operator controls termination by accepting. [CC may choose to add a configurable max_revisions field on the composition for safety — this is a CC judgment call at implementation time, not a CR requirement.]


9. Composition state query endpoints

9.1 GET /engagements/{eid}/compositions/{cid}

Returns the full composition state:


{
    "composition_id": "<UUID>",
    "engagement_id": "<UUID>",
    "state": "step_in_progress",
    "current_step_index": 1,
    "steps": [
        {
            "step_index": 0,
            "step_type": "produce",
            "specialist_ref": "...",
            "state": "completed",
            "result_ref": "<MemoryRef>"
        },
        {
            "step_index": 1,
            "step_type": "review",
            "specialist_ref": "...",
            "state": "in_progress",
            "result_ref": null
        }
    ],
    "confirmed_shape_event_ref": "...",
    "triggered_by": "...",
    "trigger_reason": "...",
    "created_at": "...",
    "updated_at": "..."
}

9.2 GET /engagements/{eid}/compositions

Lists compositions for the engagement, ordered by created_at descending. Supports ?state= filter (e.g., ?state=waiting_for_operator_decision).


10. Process restart

10.1 Startup resume

When the application starts, the CompositionOrchestrator reads the operational view for compositions in initiated or step_in_progress state and resumes execution.

Compositions in waiting_for_operator_decision state do not need resumption — they are waiting for an HTTP request, not background execution.

10.2 Restart idempotence

On restart, the orchestrator reads compositions in step_in_progress state. For each:

  1. The last event is composition_step_started with step index N.
  2. Check whether the step produced output:
  1. If no output found: re-invoke the step. For produce steps this may cause a duplicate LLM call (the specialist was invoked but crashed before writing the event). This is the same risk Phase 34 accepts for poll_external_work — bounded by the specialist contract, acceptable for Phase 37. Tightening the idempotence contract on produce_render is future work.

11. Phase 40 stub consideration

Phase 40 (Operator-vocabulary schemas) includes a POST /operator/projects/{id}/render_with_review endpoint that wraps the composition endpoint. The strategy doc §11 says:

> Phase 37's CR replaces the stub with live composition orchestration when 37 lands.

Phase 40 has NOT landed. Step 9 is a no-op. Phase 40's CR will wire the wrapper when it lands.


12. Test surface

12.1 Composition creation tests

  1. test_composition_creation_valid_ordering — produce → review → 202.
  2. test_composition_creation_invalid_ordering_reviewer_before_producer — review → produce → 422.
  3. test_composition_creation_empty_steps — 422.
  4. test_composition_creation_review_only_no_producer — 422.
  5. test_composition_creation_invalid_specialist_ref — 422.
  6. test_composition_creation_invalid_shape_ref — 422.

12.2 Orchestration tests

  1. test_sequential_composition_produce_only — single produce step completes.
  2. test_sequential_composition_produce_then_review — produce completes, review completes, composition enters waiting_for_operator_decision.
  3. test_produce_step_calls_existing_dispatch_path — verifies produce step uses the render dispatch wrapper (Phase 36 lineage and Phase 34 external-polling apply).
  4. test_review_step_dispatches_to_reviewer_specialist — verifies review step calls review_render with subject_render_event_ref.
  5. test_step_output_passes_to_next_step — step N's result ref is available to step N+1 as subject_render_event_ref.
  6. test_composition_completes_after_last_stepcomposition_completed event written.
  7. test_composition_fails_on_step_failurecomposition_failed event with detail and step_index.
  8. test_step_started_event_written_before_dispatch — verifies composition_step_started event exists before specialist is called.

12.3 Operator decision tests

  1. test_operator_accepts_after_review — composition resumes from next step.
  2. test_operator_triggers_revisioncomposition_revision_triggered event written with review_result_ref, producer step re-runs via rerender.
  3. test_revision_produces_new_render_with_pinned_lineage — re-run produces a RenderEvent with prior_render_ref equal to the producer step's original result_ref (validates pinning, not auto-discovery).
  4. test_decision_on_wrong_state_rejected — decision on a composition not in waiting_for_operator_decision → 409.
  5. test_decision_on_wrong_step_index_rejected — decision referencing a non-review step → 422.

12.4 External specialist integration tests

  1. test_produce_step_with_external_specialist — specialist returns ExternalProductionHandle, polling resolves via render_jobs-linked ExternalProductionRecord, composition advances.
  2. test_review_step_with_external_specialist — reviewer returns ExternalProductionHandle, polling resolves via composition-linked ExternalProductionRecord, composition advances to waiting_for_operator_decision.
  3. test_external_failure_fails_composition — external specialist failure → composition_failed.
  4. test_non_produce_non_review_external_dispatch_rejected — translate step specialist returns ExternalProductionHandlecomposition_failed with external_dispatch_not_supported_for_step_type.

12.5 Process restart tests

  1. test_composition_resumes_after_restart — in-progress composition resumes from correct step.
  2. test_waiting_composition_not_resumedwaiting_for_operator_decision compositions are not spuriously resumed.
  3. test_initiated_composition_picked_up — composition that was initiated but never started gets picked up.

12.6 State query tests

  1. test_composition_state_query — GET returns correct state, steps, and results.
  2. test_composition_list_with_state_filter — list endpoint filters by state.

12.7 Event log tests

  1. test_composition_events_form_correct_sequence — initiated → step_started → step_completed × N → completed.
  2. test_revision_events_recorded — revision_triggered appears in event log at correct position.

12.8 Stub specialist tests

  1. test_stub_reviewer_specialist — deterministic review output.
  2. test_stub_translation_specialist — deterministic translation output.

12.9 needs_you integration test

  1. test_composition_waiting_for_decision_appears_in_needs_you — composition in waiting_for_operator_decision state appears in /me/dashboard/needs_you response with item_kind="review_triggered_revision".

Estimated total: ~33 new tests.


13. Step structure

Step 0 — Archive + pre-flight

Archive this CR to docs/phase-crs/phase-37-cr-adapter-chaining-and-composition-v0_2.md.

Run the full pre-flight checklist (§3.2 items 1–11). Report any divergence.

Verification: pre-flight report clean.

Step 1 — Migration

Migration [next available number, likely 0056]:

Verification: uv run pytest -v green. Migration applies and reverses cleanly.

Commit: Phase 37 step 1: migration for RenderComposition and EPR extension.

Step 2 — RenderComposition model and step definitions

Create the RenderComposition model with:

[CC determines the module location — follow established model patterns.]

Verification: uv run pytest -v green.

Commit: Phase 37 step 2: RenderComposition model and step definitions.

Step 3 — New specialist categories and stubs

Create the four new specialist base classes (§4.6): ReviewerSpecialist, TranslationSpecialist, TransformSpecialist, ValidationSpecialist.

Create stub implementations for each, following StubRenderSpecialist patterns.

Write tests 31–32 (§12.8).

Verification: uv run pytest -v green.

Commit: Phase 37 step 3: specialist categories and stubs.

Step 4 — Composition creation endpoint

Implement POST /engagements/{eid}/compositions (§6).

All validation rules (§6.1). Auth via get_contributing_contributor (§6.2). 202 response with dispatch to background orchestrator.

Write tests 1–6 (§12.1).

Verification: uv run pytest -v green.

Commit: Phase 37 step 4: composition creation endpoint.

Step 5 — Sequential orchestration

Implement the CompositionOrchestrator background task (§7.1–7.3, §7.5–7.7).

Step execution loop with dispatch to produce, review, translate, transform, validate specialist methods. composition_step_started event before each dispatch. Failure handling. Synchronous-only restriction for translate/transform/validate.

Write tests 7–14 (§12.2).

Verification: uv run pytest -v green.

Commit: Phase 37 step 5: sequential orchestration.

Step 6 — Review-triggered revision and Operator decision endpoint

Implement the decision endpoint (§8). Wire review-step completion to waiting_for_operator_decision state. Wire "revise" decision to the rerender path with pinned prior_render_event_ref (§8.1). composition_revision_triggered event carries review_result_ref.

Write tests 15–19 (§12.3).

Verification: uv run pytest -v green.

Commit: Phase 37 step 6: review-triggered revision and decision endpoint.

Step 7 — External specialist integration within compositions

Wire produce steps' external-specialist handling via existing render_jobs + ExternalPollingLoop.

Wire review steps' external-specialist handling via composition-linked ExternalProductionRecord (§4.8). Extend ExternalPollingLoop._find_due_records to include composition-linked records (§7.4).

Handle resolution callback — orchestrator finds completed review step on next check.

Write tests 20–23 (§12.4).

Verification: uv run pytest -v green.

Commit: Phase 37 step 7: external specialist integration.

Step 8 — Process restart, state query, and needs_you integration

Implement startup resume (§10). Implement GET endpoints (§9).

needs_you SQL extension: Add a new SELECT to the _NEEDS_YOU_SQL_TEMPLATE UNION ALL in dashboard.py. Query render_compositions_view filtered on state='waiting_for_operator_decision'. Item kind: review_triggered_revision.

Write tests 24–30, 33 (§12.5, §12.6, §12.7, §12.9).

Verification: uv run pytest -v green.

Commit: Phase 37 step 8: process restart, state query, and needs_you integration.

Step 9 — Phase 40 wiring (no-op)

Phase 40 has not landed. No work in this step.

Commit: no commit needed.

Checkpoint A — Operator confirms composition lifecycle

Operator verification:

  1. Create a composition with produce → review steps using stub specialists.
  2. Observe produce step completes (step_started + step_completed events), review step completes, composition enters waiting_for_operator_decision.
  3. Confirm composition appears in /me/dashboard/needs_you.
  4. Submit accept decision → composition completes.
  5. Create a second composition, trigger revision after review → producer re-runs with pinned lineage, review re-runs.
  6. Query composition state — correct state, steps, results.

On acceptance: tag the substrate repo as phase-37-adapter-chaining-and-composition. Bump the manifest.

Checkpoint B — Final

Implementation notes at docs/phase-impl-notes/phase-37-implementation-notes-v0_1.md absorb execution-time surprises and findings.


14. Acceptance gate

This CR is accepted when:

  1. Substrate: all tests pass (~1,447+, 2 skips). Baseline reconciled at Step 0.
  2. RenderComposition MemoryObject with event-log-canonical storage works correctly.
  3. Six event kinds record the composition lifecycle (including composition_step_started).
  4. Composition creation validates step ordering and specialist references.
  5. Sequential orchestration executes steps in order with step_started events before each dispatch.
  6. Produce steps use the existing render dispatch path (Phase 36 lineage and Phase 34 external-polling apply).
  7. Review steps pause the composition in waiting_for_operator_decision state.
  8. Operator decision endpoint accepts or triggers revision.
  9. Revision pins prior_render_event_ref to the producer step's recorded result (no auto-discovery) and calls rerender with engine-determined lineage kwargs.
  10. External specialist integration works for produce steps (via render_jobs) and review steps (via composition-linked ExternalProductionRecord).
  11. Translate/transform/validate steps reject ExternalProductionHandle with composition_failed.
  12. Composition state survives process restart with step-output check before re-invocation.
  13. State query and list endpoints return correct data.
  14. Compositions in waiting_for_operator_decision appear in /me/dashboard/needs_you with item_kind="review_triggered_revision".
  15. Migration round-trips cleanly.

15. Post-CR state


16. What this CR does not build


17. Kickoff prompt for the Claude Code session


Read the Change Request document at the path I supply below. This is
CR-2026-052 v0.2, the Phase 37 Change Request. You are the executing
agent named in the CR.

CR path: ~/Downloads/phase-37-cr-adapter-chaining-and-composition-v0_2.md

Phase 37 adds adapter chaining and composition — multi-step render
lifecycles where steps chain produce, review, translate, transform,
and validate operations sequentially. A new RenderComposition
MemoryObject tracks the lifecycle with event-log-canonical storage.
The orchestrator is a background task that walks steps in order and
pauses on review steps for Operator decisions.

Key points:
  - RenderComposition is a MemoryObject with SIX event kinds
    (composition_step_started added per pre-approval audit).
  - Five step types: produce, review, translate, transform, validate.
  - Produce steps reuse the existing render dispatch path (Phase 36
    lineage and Phase 34 external-polling apply automatically).
  - Review steps can dispatch externally via composition-linked
    ExternalProductionRecord (EPR extended with nullable
    composition_object_id + composition_step_index).
  - Translate/transform/validate are synchronous-only in Phase 37.
  - Review steps pause in waiting_for_operator_decision; the Operator
    accepts or triggers revision via a decision endpoint.
  - Revision pins prior_render_event_ref to producer step's result
    (no auto-discovery). Review report available on composition
    event log, not via rerender kwarg.
  - Four new specialist categories with subject_render_event_ref
    kwarg (not render_event_ref) and trigger: str kwarg.
  - Auth: get_contributing_contributor, not get_current_person.
  - needs_you SQL extension: compositions in
    waiting_for_operator_decision appear in dashboard/needs_you
    with item_kind="review_triggered_revision".
  - Process restart: orchestrator resumes initiated and
    step_in_progress compositions; checks for step output before
    re-invoking.
  - ~33 new substrate tests.

Substrate baseline: 1,414 tests, 2 skips. Alembic head: 0055.

Per Section 13, nine steps with two checkpoints. Auto-mode posture:
Steps 1–9 accept auto-mode-proceed; Checkpoint A halts until
Operator confirms. Checkpoint B (final) halts for tagging.

Pre-flight checklist (§3.2) has eleven items. Run all of them before
Step 1. Pre-flight divergence stops execution at Step 0 and drives
a blocker report. Do not proceed through divergence.

Implementation notes at Checkpoint B:
docs/phase-impl-notes/phase-37-implementation-notes-v0_1.md
absorbs execution-time surprises and findings.

DUNIN7 — Done In Seven LLC — Miami, Florida Phase 37: Adapter Chaining and Composition — CR v0.2 — 2026-05-05