Version. 0.2 Date. 2026-05-03 Provenance. Claude.ai design session, continued from the Operator Layer v0.6 and the v0.1 of this document. Operator: Marvin Percival. Status. Working draft. The companion document to the Operator Layer v0.6. Where v0.6 names what the engine must support, this document names what changes inside the engine to support it, structured as a phased plan against the engine's actual current state at Phase 33. Audience. The Operator and Claude. Translates the Operator Layer architecture into specific engine work. Sits alongside. The Operator Layer v0.6 (which carries the architecture and the Operator-Layer-side development plan). This document covers the engine work; v0.6 covers the Operator Layer work; together they cover the build. Supersedes. v0.1 (same date) — three decisions left open in v0.1 Section 13 are now resolved. Phase 38's grammar set expands from five to six (scene-specification added). Phase 40's composition endpoint behavior is specified explicitly as a stub-then-wire pattern. Section 13 reframed from "open questions" to "decisions resolved." When the engine work moves into execution. This document holds the design intent for each engine phase. Individual phase CRs translate the design intent into Claude Code execution; the CRs derive from this document the same way the Operator Layer arc CRs will derive from v0.6 Part II.
This document is. A phased engine implementation strategy for the work the Operator Layer architecture commits the engine to. It identifies what changes in the engine, the data model, the test surface, and the dependencies between phases.
This document is not. A redesign of the engine. The engine works. 1,274 substrate tests pass. 33 phases of disciplined construction sit behind it. This document specifies extensions, not corrections — the engine's existing patterns are the foundation, not something to be replaced.
This document is also not. A set of CRs. CRs come later, when each engine phase enters active build. CRs include the order-of-operations details, the kickoff prompts, the auto-mode posture, the per-step test verifications. This document holds the design intent that CRs will execute against.
The Operator Layer v0.6 Section 13 named five engine prerequisites at architectural-reasoning level. Inspection of the engine's actual state at Phase 33 reveals that some of those prerequisites are partially in place, some are differently shaped than v0.6 named, and at least two prerequisites that the architecture genuinely needs were not in v0.6's list. This section reframes v0.6's commitments against what the engine actually has today.
v0.6 13.1 — Async submit-and-poll specialist production. v0.6 named this as a missing capability. The engine actually has substantial async machinery already: BackgroundAgentRunner exists from Phase 3; shaping_jobs and render_jobs operational tables exist with the queued → dispatched → completed/failed status pattern from Phase 9 and Phase 10; render dispatch is non-blocking (HTTP 202, caller polls for completion). The specific gap is narrower than v0.6 framed it: the current pattern assumes the specialist completes synchronously inside the BackgroundAgentRunner's task. For external long-running services (Claude Code Dispatch, 3D printing services, video generators), the specialist itself dispatches to an external system, returns a handle, and the engine polls the external system for completion. This is the missing piece — external-service polling, not asynchronous dispatch in general. Reframed prerequisite name: External-service polling for specialists.
v0.6 13.2 — Binary and reference render content support. v0.6 named this as a missing capability. The engine actually has the materializer registry pattern from the May 2 render-download work, which produces bytes from JSONB render_content. What's actually missing is upstream of materialization: RenderEvent carries no content-type discriminator and no way to point at non-dict content. Storing a generated STL file or a deployment URL today requires shoving it into the JSONB field, which is structurally wrong. The prerequisite's specific shape is: extend RenderEvent with a content-kind discriminator (inline_dict, binary_blob, external_reference) and add storage paths for binary content (file-system pattern from Phase 16) and reference URIs. Reframed prerequisite name: Render content kind and external reference support.
v0.6 13.3 — Re-render contracts with diff and migration semantics. v0.6 named this. The engine genuinely lacks it. Today's re-render is fresh production with no relationship to prior renders. The shape lineage exists (confirmed_shape_event_ref on RenderEvent), but the render side has no prior_render_ref and no way for a specialist to declare whether it supports incremental update. Confirmed prerequisite. Renamed for clarity: Incremental re-render with prior-render lineage.
v0.6 13.4 — Adapter chaining and composition orchestration. v0.6 named this. The engine genuinely lacks it. There is no multi-step render lifecycle support. No way to declare "produce-then-review" or "produce-then-translate" as a chained operation. Confirmed prerequisite. Renamed: Adapter chaining and composition.
v0.6 13.5 — Specification grammar declaration on shape types. v0.6 named this. The engine partly has it: DeclaredShapeType.shaping_instructions_ref carries the implicit grammar via the shaping instructions. What's missing is explicit declaration of structural elements, completeness criteria, and testability criteria that the engine could evaluate without running a full shaping pass. Confirmed prerequisite. Renamed for precision: Specification grammar declaration on DeclaredShapeType.
Cross-engagement aggregation endpoints. The Operator Layer Dashboard zone aggregates state across all the Operator's projects (active jobs, items needing decision, recently finished). Today's endpoints are all scoped to a single engagement. The person layer (Phase 14) provides GET /me/memberships, but there is no /me/dashboard or /me/projects/active that aggregates state across engagements. This is genuinely missing and is a substantial new endpoint surface. New prerequisite: Cross-engagement aggregation endpoints.
Operator-vocabulary response schemas. The Operator Layer architecture commits to "Operator vocabulary in API responses" enforced at the Orchestration API. Today's API responses use engine vocabulary throughout — engagement_id, assertion, shape_event, render_event. The Orchestration API needs to do real translation, which means new response schemas, not thin wrappers. This is more work than v0.6's "translation layer" framing suggested. New prerequisite: Operator-vocabulary response schemas.
Replacing v0.6 Section 13's five with seven, after the inspection:
BackgroundAgentRunner and operational tables exist. What needs to be added: a polling mechanism for specialists that dispatch to external long-running services and return handles rather than completing in-process.RenderEvent with a content-kind discriminator. Add binary blob storage and external reference URI handling. The materializer registry already exists; this prerequisite supplies what it materializes from.prior_render_ref to RenderEvent. Add supports_incremental_rerender declaration to render specialists. Add the engine orchestration that calls the right method based on the declaration.RenderComposition MemoryObject that holds multi-step lifecycles. Engine orchestration that holds state between adapter calls. Conditional revision triggers for review-driven re-renders.DeclaredShapeType. Define a small grammar-of-grammars. Existing shape types declare their grammar; new shape types declare at creation./me/... that aggregate state across the requesting person's engagements. Active jobs across projects, items needing decision across projects, recently finished artifacts across projects.These seven prerequisites are the engine work the Operator Layer architecture depends on. Sections 4 through 10 specify each prerequisite as an engine phase.
The engine build is currently at Phase 33 (the most recent tagged work being engagement activity observability). Operator Layer engine work continues the numbering. The seven engine prerequisites become Phases 34 through 40, with sequencing determined by their dependencies.
Sequencing. Phases that have no dependency on other Operator Layer engine work can run in parallel against engine capacity. Phases that depend on others must wait. The dependency graph is:
Phase 34 (External-service polling) ─────────┐
├─→ Phase 37 (Adapter chaining)
Phase 35 (Render content kinds) ──────────────┤
└─→ Phase 36 (Incremental re-render) ──────┘
Phase 38 (Specification grammar declaration) — independent
Phase 39 (Cross-engagement aggregation) — independent
Phase 40 (Operator-vocabulary schemas) — depends on Phase 39 conceptually
but can be designed in parallel
Parallelization. Phases 34, 35, 38, and 39 can all start immediately. Phase 36 starts after Phase 35 lands. Phase 37 starts after Phase 34 and Phase 36 land. Phase 40 starts after Phase 39's endpoint shapes are stable.
Operator Layer arc dependencies. The seven phases unblock different Operator Layer arcs:
Estimated effort. Each phase is medium-sized — roughly the scope of Phase 17 (Redirect and Display Numbers) or Phase 28 (Expandable Explanations). Phase 39 (cross-engagement aggregation) is larger because it adds a substantial new endpoint surface. Phase 37 (composition) is larger because it touches orchestration directly. The other five phases are typical CR-sized.
Enable specialists to dispatch to external long-running services (Claude Code Dispatch, 3D printing service APIs, video generation services), return a handle, and have the engine poll the external service for completion rather than the specialist blocking until production finishes.
BackgroundAgentRunner (Phase 3) dispatches agent work as background asyncio tasks. Tasks complete in-process.shaping_jobs (Phase 9) and render_jobs (Phase 10) operational tables track job status with queued → dispatched → completed/failed transitions.RenderSpecialist.produce_render(...) returns a RenderEvent directly. The specialist's job completes when the method returns.GET /engagements/{eid}/render-jobs/{jobid} for completion.The specialist contract assumes synchronous completion within the BackgroundAgentRunner's task. There is no mechanism for a specialist to:
RenderEvent then.For Phase B's Claude Code adapter, this matters concretely: building a website takes minutes to hours. The adapter cannot block in the BackgroundAgentRunner for that long — the FastAPI lifespan would not survive a deploy or restart, and the polling pattern would not surface meaningful progress.
Specialist contract extension. RenderSpecialist gains a second return mode. Today the specialist returns RenderEvent directly. Phase 34 adds:
@dataclass
class ExternalProductionHandle:
"""Returned by specialists that dispatch to external services.
Indicates the engine should poll for completion."""
external_job_id: str # Specialist-defined identifier for the external work
polling_interval_seconds: int # How often to poll
progress_hint: str | None # Optional human-readable status
# Specialists may now return either RenderEvent or ExternalProductionHandle:
async def produce_render(self, ...) -> RenderEvent | ExternalProductionHandle:
...
# When ExternalProductionHandle is returned, the specialist also implements:
async def poll_external_work(
self, *, external_job_id: str, db: AsyncSession
) -> RenderEvent | ExternalProductionHandle | ExternalProductionFailure:
"""Called by the engine at the polling interval. Returns either:
- RenderEvent: production completed successfully
- ExternalProductionHandle: still in progress (may update progress_hint)
- ExternalProductionFailure: external service reported failure
"""
...
New MemoryObject: ExternalProductionRecord. Tracks in-flight external work tied to a render_jobs row.
class ExternalProductionRecord(MemoryObject):
object_type: Literal["external_production_record"] = "external_production_record"
render_job_id: UUID # The render_jobs row this tracks
specialist_ref: ActorRef # The specialist that dispatched the work
external_job_id: str # Specialist-defined external identifier
polling_interval_seconds: int # Current polling interval
progress_hint: str | None # Last reported progress
last_polled_at: datetime
state: Literal["polling", "completed", "failed"]
New render_jobs status: awaiting_external. Between dispatched and completed, a job that returns an ExternalProductionHandle transitions to awaiting_external. The engine's polling loop reads awaiting_external jobs and calls the specialist's poll_external_work.
Engine polling loop. A background task started by the FastAPI lifespan that walks awaiting_external rows on a schedule, calls each specialist's poll_external_work, and acts on the response:
RenderEvent returned: write the event to memory_events, transition the render_jobs row to completed.ExternalProductionHandle returned: update progress_hint and last_polled_at on the ExternalProductionRecord. Repeat at next interval.ExternalProductionFailure returned: transition to failed. Surface as Inbox item.
Restart safety. The polling loop must survive process restarts. State lives in external_production_records; the loop on startup reads any awaiting_external rows and resumes polling. No work is lost across restarts.
ExternalProductionHandle causes job to transition to awaiting_external.poll_external_work at the declared interval.ExternalProductionRecord rows track the in-flight work.RenderEvent and transitions the job to completed.failed with the failure detail captured.awaiting_external jobs.RenderEvent directly, never enter awaiting_external).0046_phase_34_external_production_records.py — creates the external_production_records table with foreign key to render_jobs.render_external_dispatched, render_external_completed, render_external_failed) — pre-flight confirms.
Extend RenderEvent to support content kinds beyond inline_dict. Specifically: binary blobs (STL files, generated images, audio) and external references (deployment URLs, cloud storage URIs). The materializer registry already exists; Phase 35 supplies what it materializes from.
RenderEvent.render_content is JSONB, holding dict-shaped content.data/files/{engagement_id}/{file_id}{ext}.RenderEvent carries render_format (MIME-like identifier) but no discriminator for what kind of content storage backs it.
RenderEvent cannot represent:
Today, all three would have to be shoved into the JSONB render_content field, which conflates content with metadata and breaks the materializer registry's separation of concerns.
RenderEvent extension. Add content_kind discriminator and content storage fields:
class RenderEvent(MemoryObject):
# ... existing fields unchanged ...
# New in Phase 35:
content_kind: Literal["inline_dict", "binary_blob", "external_reference", "multi_file"]
# When content_kind = "inline_dict": render_content carries the dict (existing behavior)
# When content_kind = "binary_blob": render_content carries metadata; storage_path points at bytes
# When content_kind = "external_reference": render_content carries metadata; reference_uri is the link
# When content_kind = "multi_file": render_content carries the manifest; multi_file_manifest holds file refs
storage_path: str | None = None # For binary_blob: path under data/renders/{engagement_id}/
reference_uri: str | None = None # For external_reference: the URL or URI
reference_metadata: dict | None = None # For external_reference: deployment timestamp, version, etc.
multi_file_manifest: list[dict] | None = None # For multi_file: list of {filename, content_kind, storage_path/uri, content_type}
Storage convention for binary blobs. Following Phase 16's pattern: data/renders/{engagement_id}/{render_event_object_id}{ext}. Binary content is written to disk; the RenderEvent carries the path. Binary content survives database backups but the path-relative-to-data-directory is what's stored, so backup-and-restore works correctly.
Multi-file renders. A multi-file render carries a manifest listing files. Each file has its own kind (binary_blob or external_reference), so a multi-file render can mix bytes-on-disk with cloud URIs. The download pathway materializes a multi-file render as a zip archive; individual files can be downloaded separately.
Materializer registry extension. Today the registry maps render_format to materializer functions that take dict and return bytes. Phase 35 extends this:
# Materializers now take RenderEvent (not just render_content) so they can dispatch by content_kind:
MaterializerFn = Callable # async (render_event, engagement_title) -> MaterializedFile
async def materialize_pdf(render_event, engagement_title):
if render_event.content_kind == "inline_dict":
# Existing behavior
...
elif render_event.content_kind == "binary_blob":
# Read from storage_path, possibly wrap in PDF if format declared
...
elif render_event.content_kind == "external_reference":
# Generate a PDF that names the URL — for cases where the artifact is the URL itself
...
Most materializers will keep behaving as today (dict-driven). New materializers handle binary blobs and external references as needed.
API extensions.
GET /engagements/{eid}/renders/{rid}/content — already exists for inline_dict content. Extended to handle binary_blob (returns the file bytes with the correct content type) and external_reference (returns the reference URI in a small JSON envelope).GET /engagements/{eid}/renders/{rid}/files/{filename} — new. For multi_file renders, retrieves a specific file from the manifest.GET /engagements/{eid}/renders/{rid}/download — already exists. Extended to handle binary_blob (returns the file directly) and multi_file (returns a zip).RenderEvent with content_kind="binary_blob" and a populated storage_path is stored correctly.content_kind="external_reference" stores the URI and metadata; download endpoint serves the URI.content_kind.content_kind for legacy data is inline_dict).RenderEvent is retired or invalidated, what happens to the binary blob? Decision: bytes are kept for audit (a retired render's content is still referenceable). Hard-delete pathway for binary blobs is future work.0047_phase_35_render_event_content_kind.py — adds content_kind, storage_path, reference_uri, reference_metadata, multi_file_manifest columns to the render-event storage location (event-log canonical, materialized into current_memory_objects and render_events_view).content_kind="inline_dict" (the value implicit in their current shape).When a shape's specification changes, a re-render may be incremental rather than fresh: a website re-render preserves URLs, a contract re-render preserves formatting, a 3D model re-render preserves printer settings. Phase 36 adds the engine support for this lineage and the specialist declaration of incremental-rerender capability.
RenderEvent references the confirmed_shape_event_ref it derived from.
RenderEvent has no prior_render_ref. Specialists have no way to declare whether they support incremental re-render. The engine has no orchestration that calls a specialist's incremental method when prior-render lineage exists.
RenderEvent extension.
class RenderEvent(MemoryObject):
# ... existing fields unchanged ...
# New in Phase 36:
prior_render_ref: MemoryRef | None = None # Set when this render is an incremental update
revision_strategy: Literal["fresh", "incremental"] = "fresh"
Specialist contract extension.
class RenderSpecialist:
# New in Phase 36:
supports_incremental_rerender: bool = False # Declared by the specialist class
async def rerender(
self, *,
prior_render_event_ref: MemoryRef,
revised_shape_event_ref: MemoryRef,
...,
) -> RenderEvent | ExternalProductionHandle:
"""Called when prior render exists and specialist supports incremental.
Default implementation raises NotImplementedError; specialists that
support incremental override this method.
Returns a RenderEvent with content reflecting the incremental update,
or an ExternalProductionHandle for async incremental work.
"""
raise NotImplementedError("Specialist does not support incremental rerender")
Engine orchestration. The render dispatch logic (Phase 10) gains a branch:
# Pseudocode for the dispatch path:
if revised_shape_event has a prior render in the same declared_render_type:
if specialist.supports_incremental_rerender:
result = await specialist.rerender(
prior_render_event_ref=prior_render_ref,
revised_shape_event_ref=revised_shape_ref,
...
)
new_render_event.prior_render_ref = prior_render_ref
new_render_event.revision_strategy = "incremental"
else:
result = await specialist.produce_render(...)
new_render_event.revision_strategy = "fresh"
else:
# First render for this declared_render_type — always fresh
result = await specialist.produce_render(...)
new_render_event.revision_strategy = "fresh"
Lineage chain. Each RenderEvent may reference its prior_render_ref. The chain forms a linked list of revisions. The Library can show the lineage as a version history; the Operator can navigate older versions and revert if needed. This lineage discipline parallels the assertion revision chain established in Phase 3 (wasRevisionOf).
supports_incremental_rerender=True is called via rerender when prior render exists.supports_incremental_rerender=False falls back to produce_render even when prior render exists.RenderEvent correctly records prior_render_ref and revision_strategy.prior_render_ref to get the version history).prior_render_ref and revision_strategy="fresh".0048_phase_36_render_event_prior_render_lineage.py — adds prior_render_ref and revision_strategy columns. Backfill: existing renders get prior_render_ref=NULL and revision_strategy="fresh".Support multi-step render lifecycles where one adapter's output becomes another adapter's input. Cross-model review (produce-then-review) is the immediate use case; produce-then-translate, produce-then-validate, parallel reviewers, and other compositions are general patterns the same support enables.
Single-specialist render lifecycles: shape goes in to a specialist, render comes out. There is no engine support for chaining.
New MemoryObject: RenderComposition. Represents a multi-step render lifecycle.
class RenderComposition(MemoryObject):
object_type: Literal["render_composition"] = "render_composition"
# The chain definition
steps: list[CompositionStep] # Ordered or parallel-grouped steps
# Lineage
confirmed_shape_event_ref: MemoryRef # The shape that initiates the composition
triggered_by: ActorRef
trigger_reason: str
# Execution state
state: Literal["pending", "running", "completed", "failed", "stopped"]
current_step_index: int # For ordered execution
step_results: list[dict] # Results from each completed step
failure_detail: str | None
# Conditional triggers
revision_threshold: dict | None # Optional: when to trigger producer re-render
# e.g., {"reviewer_specialist": "...", "max_severity": "blocking"}
class CompositionStep(BaseModel):
"""A single step in a composition."""
step_id: str # Identifier within the composition
step_kind: Literal["produce", "review", "transform", "validate"]
specialist_ref: ActorRef # The specialist that handles this step
input_refs: list[MemoryRef] # References to inputs (shape, prior step output, etc.)
config: dict # Step-specific configuration
parallel_group: str | None = None # If set, all steps in the same group run in parallel
Engine orchestration. The composition orchestrator:
RenderComposition and walks its steps in declared order.step_results.revision_threshold and a review step's findings exceed the threshold, the composition re-runs the producer step with the findings as additional context (bounded re-run, not infinite loop).completed.Specialist contract. Producer specialists already exist. Phase 37 adds two new specialist categories:
class ReviewerSpecialist(RegisteredAgent):
"""Takes an artifact (and optionally the original spec) and produces a review."""
async def review(
self, *,
artifact_ref: MemoryRef, # The render to review
shape_ref: MemoryRef | None, # The original spec (may be None for some review types)
review_criteria: dict, # What to review for
db: AsyncSession,
) -> ReviewReport: ...
class TransformerSpecialist(RegisteredAgent):
"""Takes an artifact and produces a derivative artifact."""
async def transform(
self, *,
source_artifact_ref: MemoryRef,
transformation_config: dict,
db: AsyncSession,
) -> RenderEvent: ... # Returns a new RenderEvent for the transformed artifact
Review reports as engagement artifacts. A ReviewReport is itself a RenderEvent (a render type whose shape is "review of an artifact"). The RenderEvent carries prior_render_ref pointing at the artifact reviewed, plus structured findings in the content. The Library presents review reports alongside the artifacts they review with visible lineage.
Composition endpoint. New API endpoint:
POST /engagements/{eid}/compositions
Body: {
"confirmed_shape_event_ref": MemoryRef,
"steps": [...], # The chain definition
"revision_threshold": {...} # Optional
}
Response: 202 with composition_id
The composition runs asynchronously. Status is polled via GET /engagements/{eid}/compositions/{cid}.
failed with the failure detail captured.RenderEvents with the correct prior_render_ref.0049_phase_37_render_compositions.py — registers the render_composition MemoryObject type and any new event kinds (composition_initiated, composition_step_completed, composition_completed, composition_failed, composition_revision_triggered). Composition data lives event-log canonical with the standard current_memory_objects materialization.Phase 37 depends on Phase 34 (most reviewer specialists will be external services like Gemini, requiring external-service polling) and Phase 36 (revision triggers re-run the producer adapter, requiring the incremental re-render lineage).
Make the engine grammar-indifferent in a structurally defensible way. Today, the grammar a shape type produces is implicit in the shaping instructions. Phase 38 adds explicit declaration of the grammar's structural elements, completeness criteria, and testability criteria — what the engine can evaluate without running a full shaping pass.
DeclaredShapeType.shaping_instructions_ref carries the implicit grammar via the shaping instructions text.The engine cannot:
DeclaredShapeType extension. Add explicit grammar declaration:
class DeclaredShapeType(MemoryObject):
# ... existing fields unchanged ...
# New in Phase 38:
specification_grammar: SpecificationGrammar
class SpecificationGrammar(BaseModel):
"""Explicit declaration of what a shape type's content looks like."""
grammar_name: str # e.g. "req-table", "narrative-storybook-pages",
# "procedural-3d", "legal-document"
structural_elements: list[StructuralElement] # Required and optional sections/fields
completeness_criteria: list[CompletenessCriterion] # What must be present for the shape to be confirmable
testability_criteria: list[TestabilityCriterion] # How to evaluate a produced render against the spec
class StructuralElement(BaseModel):
name: str # e.g. "title", "abstract", "requirements_table"
required: bool
repeats: bool = False # Whether multiple instances are allowed
description: str | None = None
class CompletenessCriterion(BaseModel):
name: str # e.g. "all_requirements_have_actors"
description: str
evaluator: str | None = None # Optional: a small DSL or script identifier
class TestabilityCriterion(BaseModel):
name: str # e.g. "every_clause_has_jurisdiction_marker"
description: str
evaluator: str | None = None
Grammar registry. A small registry of well-known grammar shapes. Phase 38 ships six grammars in the initial registry: REQ table (for application engagements), narrative-storybook-pages (for the Goosey engagement and similar), legal-document (for the Phase A contract template adapter), procedural-3d (for the Phase C 3D printing adapter), application-specification (for the Phase B application-rendering adapter), and scene-specification (for future video adapters; shipped now because the grammar declaration is cheap and pre-positioning it means a future video adapter attaches without re-opening the registry).
Each registered grammar carries a default SpecificationGrammar that shape types can declare against. New grammars (component-specification for design systems, protocol-structure for clinical work, regulatory-submission variants of legal-document) are added when an engagement requires them — the engine itself does not enumerate grammars beyond the initial six.
# src/loomworks/grammars/registry.py
GRAMMAR_REGISTRY: dict[str, SpecificationGrammar] = {}
def register_grammar(grammar: SpecificationGrammar) -> None:
GRAMMAR_REGISTRY[grammar.grammar_name] = grammar
def get_grammar(name: str) -> SpecificationGrammar | None:
return GRAMMAR_REGISTRY.get(name)
Existing shape types declare their grammar. As part of Phase 38, the existing five DeclaredShapeTypes in the Loomworks engagement and any others in active engagements declare their grammars. A migration backfills specification_grammar on existing rows.
Specialist matching. Render specialists declare what grammars they accept. The dispatch logic checks grammar compatibility:
class RenderSpecialist:
accepts_grammars: list[str] = [] # e.g. ["legal-document", "req-table"]
# Dispatch logic gains a check:
if shape.declared_shape_type.specification_grammar.grammar_name not in specialist.accepts_grammars:
raise GrammarMismatchError(
f"Specialist {specialist.id} does not accept grammar "
f"{shape.declared_shape_type.specification_grammar.grammar_name}"
)
Completeness evaluation at confirmation time. The shape confirmation flow gains a grammar-check step: before a shape can be confirmed, its content must satisfy the grammar's completeness criteria. The check runs the criterion evaluators (where defined) and reports any failures to the Operator. The Operator can override the check (with a deliberate exception) but must do so explicitly.
GrammarMismatchError when grammars don't match.0050_phase_38_specification_grammar.py — adds specification_grammar (JSONB) to the shape-type storage. Backfill: existing shape types get the grammar inferred from their shaping instructions, with the inference run as a script that the Operator reviews.None. Phase 38 is independent; it can land at any point relative to Phases 34–37.
Provide endpoints under /me/... that aggregate state across all the requesting person's engagements. The Operator Layer Dashboard shows active jobs across projects, items needing decision across projects, and recently finished artifacts across projects. Today's endpoints are all single-engagement-scoped; aggregating in the frontend by N concurrent calls per page load is wrong.
GET /me/memberships (Phase 14) returns the person's engagement memberships.GET /engagements/{eid}/activity-summary once per engagement to assemble a cross-engagement dashboard.Aggregation endpoints that:
Three new endpoints under /me/...:
GET /me/dashboard/active — Active jobs across all the requesting person's engagements.
class DashboardActiveResponse(BaseModel):
items: list[DashboardActiveItem]
total_count: int
class DashboardActiveItem(BaseModel):
engagement_id: UUID
engagement_name: str
item_kind: Literal["shape_production", "render_production", "composition", "external_polling"]
item_id: UUID # The job or composition id
item_label: str # Human-readable label
started_at: datetime
progress_hint: str | None
GET /me/dashboard/needs_you — Items requiring the person's decision across engagements.
class DashboardNeedsYouResponse(BaseModel):
items: list[DashboardNeedsYouItem]
total_count: int
class DashboardNeedsYouItem(BaseModel):
engagement_id: UUID
engagement_name: str
item_kind: Literal["pending_shape", "held_assertion", "produced_render_for_review",
"review_report", "external_failure", "downstream_specification"]
item_id: UUID
item_label: str
detail: str # Brief context
created_at: datetime
GET /me/dashboard/recent — Recently finished artifacts across engagements.
class DashboardRecentResponse(BaseModel):
items: list[DashboardRecentItem]
total_count: int
class DashboardRecentItem(BaseModel):
engagement_id: UUID
engagement_name: str
artifact_kind: Literal["render", "review_report", "transformed_artifact"]
artifact_id: UUID
artifact_label: str # The render's title
completed_at: datetime
download_url: str | None # If downloadable
Authorization. All three endpoints require an authenticated person session (Phase 14). The aggregation is bounded to the engagements the person has membership on. Engagements where the person has only read access (per the membership designation) include only items the person can act on.
Performance. Each endpoint runs a single SQL query with engagement-scoped UNIONs or joins via the membership table. Expected latency: under 200ms for a person with 10 engagements. If this becomes a hotspot, materialized cross-engagement views are a future optimization.
Pagination. Each endpoint accepts ?limit and ?cursor for pagination. Default limit 50.
No new migrations — these endpoints query existing tables. New SQL queries; no schema changes.
None. Phase 39 is independent; it can run in parallel with Phases 34–38.
Implement the vocabulary wall (Operator Layer v0.6 Section 9) at the Orchestration API layer. Today's API responses use engine vocabulary throughout. The Orchestration API layer needs response schemas that translate engine vocabulary into Operator vocabulary, with the wall enforced architecturally.
engagement_id, assertion, shape_event, render_event, confirmed, pending, held, etc.src/loomworks/api/routers/... is the only API surface./operator/... endpoint surface exists.A separate endpoint surface (the Orchestration API per v0.6 Section 10) that:
/operator/... paths./operator/... cannot accidentally see engine vocabulary, because the schemas don't carry it.
New module: src/loomworks/orchestration/. Houses the Orchestration API. Imports engine functions directly (same process), translates responses.
Project structure:
src/loomworks/orchestration/
__init__.py
schemas.py # Operator-vocabulary Pydantic schemas
translators.py # Functions that translate engine objects to Operator objects
routers/
converse.py # POST /operator/converse — the Companion brain entry
dashboard.py # GET /operator/dashboard — the home aggregation
inbox.py # GET /operator/inbox — items needing decision
library.py # GET /operator/library — finished artifacts
narrative.py # GET /operator/projects/{id}/story — project narrative
composition.py # POST /operator/projects/{id}/render_with_review
downstream.py # GET /operator/projects/{id}/implied_specifications
Operator-vocabulary schemas. Each engine concept gets an Operator-vocabulary equivalent:
# Engine vocabulary (existing):
class AssertionResponse(BaseModel):
assertion_id: UUID
engagement_id: UUID
state: Literal["held", "committed", "retracted", "redirected"]
content: str
contributor: ContributorRef
# ...
# Operator vocabulary (new in Phase 40):
class NoteResponse(BaseModel):
note_id: UUID
project_id: UUID
status: Literal["waiting", "saved", "discarded", "moved_to_other_project"]
text: str
contributor_name: str
# ...
The translation discipline is one-way: Operator vocabulary in, engine functions called underneath, engine objects translated back to Operator vocabulary on the way out. The translators handle the mapping in both directions.
Vocabulary-wall enforcement test. A new test class scans all /operator/... response schemas and verifies they contain no engine-vocabulary terms. The test maintains a list of forbidden terms (engagement, assertion, shape_event, render_event, etc.) and fails if any appear in /operator/... schema field names or default literal values.
Endpoint mapping. Each endpoint in v0.6 Section 10 gets implemented:
POST /operator/converse — wraps the Phase 31 conversation engine, extending intent classification to all the v0.6 intents.GET /operator/dashboard — wraps the Phase 39 cross-engagement aggregation endpoints.GET /operator/inbox and POST /operator/inbox/{item_id}/respond — translates Phase 39 needs-you items into Inbox items with verbs.GET /operator/library — translates renders, review reports, transformed artifacts into Library entries.GET /operator/library/{artifact_id}/download — wraps the existing render download endpoint.GET /operator/projects/{id}/story — new endpoint computing the project narrative.GET /operator/projects/{id}/implied_specifications — new endpoint surfacing downstream specifications.POST /operator/projects/{id}/render_with_review — wraps the Phase 37 composition endpoint. Ships in Phase 40 as a stub returning HTTP 501 Not Implemented with a structured response indicating the capability is pending. The endpoint shape (request schema, response schema, status semantics) is the contract that Arc 2's companion and Arc 3's frontend build against. When Phase 37 lands, that phase's CR includes the wiring step that replaces the 501 stub with the live composition orchestration. Callers don't change between stub and live — only the response status transitions from 501 to 202. This stub-then-wire pattern follows the precedent set in Phase 3, where BackgroundAgentRunner's async dispatch seam shipped before being used asynchronously.
The engine API stays. The existing /engagements/... and /me/... endpoints continue to exist. The Workshop (existing four-room frontend) and any other consumers using engine vocabulary continue to work. The Orchestration API is additive, not a replacement.
No new migrations — the Orchestration API is a translation layer over existing engine state. New schemas; no schema changes.
Phase 40 depends on Phase 39 (the dashboard endpoint wraps Phase 39's cross-engagement aggregation) and on the existing Phase 31 conversation engine being extended to all intents (which is Operator Layer Arc 2 work, not engine work, but Phase 40's /operator/converse endpoint requires Arc 2's intent classification to actually function). Phase 40 can be substrate-built ahead of Arc 2's completion using the existing Phase 31 engagement-creation intent only, with the other intents added incrementally as Arc 2 lands them.
Phase 40 does not depend on Phase 37. The composition endpoint at Phase 40 ships as a stub returning HTTP 501; Phase 37's CR includes the step that replaces the stub with live composition orchestration. This stub-then-wire pattern lets Phase 40 ship its full endpoint surface as a stable contract before all the engine support exists, so Arc 2 and Arc 3 can build against the contract from the start.
The seven phases organize into three swim lanes that can run in parallel, with one cross-lane dependency.
Lane A — Specialist plumbing.
Lane B — Grammar.
Lane C — Operator-Layer-facing endpoints.
Cross-lane. Phase 37 (Lane A) and Phase 40 (Lane C) interact, but Phase 40 ships its composition endpoint as a stub returning HTTP 501. Phase 37's CR replaces the stub with live composition orchestration when 37 lands. This means Phase 40 can ship in full once Phase 39 lands, without waiting on Lane A.
Operator Layer arc unblocking.
Recommended kickoff order. Five engine phases can start immediately: 34, 35, 38, 39 in parallel against engine capacity. Phase 36 starts when 35 lands. Phase 37 starts when 34 and 36 land. Phase 40 starts when 39 lands.
Once Phase 38 (grammar declaration) and Phase 39+40 (cross-engagement plus Operator vocabulary) are landed, Operator Layer Arc 1 can begin. Once Arc 1 reaches enough completeness, Arc 3 can begin. Arc 6 Phase A (contract template adapter) is gated only on Phase 38, so it can begin in parallel with Arc 1's later sub-phases.
This document covers the engine prerequisites for the Operator Layer architecture. It does not cover:
DUNIN7/loomworks-operator repo creation, scaffold, deployment configuration. This is Arc 3 work.Three decisions left open in v0.1 of this document have been resolved by the Operator. The rationale is recorded here so future readers can reconstruct why each path was chosen.
Decision. Arc 1 sub-phases (1A, 1B, 1C, 1D) ship as their dependencies land rather than as one monolithic Arc 1 release.
Rationale. The engine build itself runs phase-by-phase rather than as one big release; the Operator Layer arcs should not break that pattern. More specifically: Arc 1A (the conversational endpoint) depends on Arc 2's intent classification reaching maturity, which is Operator Layer work, not engine work. If Arc 1 shipped monolithically, sub-phases 1B, 1C, and 1D would sit unshipped waiting for 1A — the opposite of how the dependency graph should drive sequencing. The dependency graph says 1B–1D ship when Phases 39 and 40 land; 1A ships later when Arc 2 catches up. The incremental approach honors the actual dependency graph.
Alternative considered and rejected. Monolithic Arc 1 release. Rejected because it would force fast-ready sub-phases to wait for slower-ready sub-phases, slowing observable progress without compensating benefit.
Implication for build sequence. Arc 1B (Dashboard endpoint), Arc 1C (Library and project narrative endpoints), and the Inbox endpoint can ship as soon as Phases 39 and 40 land. Arc 1A (converse endpoint) and Arc 1D (composition endpoint) ship later. This shows up in the Operator-Layer-side development plan in v0.6 Part II as sub-phase-specific dependencies.
Decision. Phase 40's POST /operator/projects/{id}/render_with_review endpoint ships in Phase 40 as a stub returning HTTP 501 Not Implemented with a structured response indicating the capability is pending. Phase 37's CR includes the wiring step that replaces the stub with live composition orchestration when Phase 37 lands.
Rationale. The endpoint shape is the contract that the rest of the system builds against, and the contract should be locked in early. If Arc 2's companion offers "I can have Gemini review this" and Arc 3's frontend shows a "Run review" button before the endpoint exists, they'll either invent placeholder shapes that turn out to be wrong, or they'll hold off on the parts that depend on the composition endpoint. Both are worse than building against a stub that returns 501. When 501 turns into real responses, no caller has to change.
Precedent. Phase 3's BackgroundAgentRunner shipped with the seam in place but used synchronously, and later phases filled in the async behavior without callers having to change. Phase 40's stub composition endpoint follows the same pattern.
Alternative considered and rejected. Wait for Phase 37 to land before Phase 40 ships the composition endpoint, in a follow-on phase. Rejected because it delays the contract that Arc 2 and Arc 3 build against, increasing the risk that those arcs build against speculative shapes that turn out wrong.
Implication for build sequence. Phase 40 can ship in full once Phase 39 lands, without waiting on Phase 37. Phase 37's CR includes the stub-replacement step.
Decision. Phase 38's grammar registry ships with six grammars: REQ table, narrative-storybook-pages, legal-document, procedural-3d, application-specification, scene-specification.
Rationale. The first five cover the existing engagements (REQ table for the application engagements; narrative-storybook-pages for Goosey) and the Phase A through Phase C adapters (legal-document for Phase A; application-specification for Phase B; procedural-3d for Phase C). That's the immediate must-have set.
The sixth — scene-specification — is added because video and animation are named in v0.6 Section 15.6 as deferred adapter categories that are likely to arrive eventually. The grammar declaration costs almost nothing to ship (it's a small data structure with structural elements, completeness criteria, testability criteria), and shipping it now means a future video adapter attaches without re-opening the registry. Not shipping it would mean a future Phase 38b. The grammar declaration is cheap; the future re-opening is not.
Grammars not shipped in Phase 38, and the reason. Component-specification (design systems), protocol-structure (clinical work), and regulatory-submission variants of legal-document are deferred. Unlike video, these don't have any current trajectory — no existing engagement uses them, no near-term adapter requires them. The deferral discipline says we add grammars when an engagement requires them, and these don't yet have requiring engagements.
Alternative considered and rejected. Ship only the five immediately-required grammars (no scene-specification). Rejected because the cost of pre-positioning scene-specification is small and the cost of re-opening the registry later is larger.
Implication for build sequence. Phase 38's CR will register six grammars at shipment. New shape types declared after Phase 38 must declare against one of these six (or a newly-registered grammar; the registry is extensible).
Seven engine phases. Three lanes. Most run in parallel. The longest dependency chain is Phase 35 → Phase 36 → Phase 37 (specialist plumbing, three phases serial). The other lanes are independent and can start immediately.
Each phase is medium-sized — comparable to the engine phases that came before. Each has a clear data model change, a clear test surface, and clear dependencies. None of them require an engine redesign; all of them are extensions of existing patterns that the engine has been hardening across 33 phases.
With the three decisions in Section 13 resolved (Arc 1 incremental, Phase 40 composition endpoint as stub, Phase 38 ships six grammars), the strategy is ready to drive CR drafting. Each phase has a stable design intent; the next move for any phase is a CR drafted from the relevant section in this document, adding the order-of-operations details, the kickoff prompt, the auto-mode posture, and the per-step test verifications.
The Operator Layer architecture is buildable. The engine work it requires is shaped to fit the engine that exists rather than the engine that would be most convenient. The dependencies are clear. The decisions are made. The work can begin.
DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Engine Implementation Strategy — v0.2 — 2026-05-03