> ## Documentation Index
> Fetch the complete documentation index at: https://f4c7a9e2d8b1-docs.tenzo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Module and stage-orchestrator contract

> One global hop executor, one work runner per module, and one composition coordinator

<Warning>
  **Restricted Access**: This documentation is only accessible to @tenzo.ai and @salv.ai email addresses.
</Warning>

## Decision

**One global hop executor. One work runner per module. One composition coordinator.**

[Chosen shape](#chosen-shape) is that split, drawn against the rejected shape. [Why the split](#why-the-split) is why a third module is a definition, a work runner, and a link. [Endings and custom pathways](#endings-and-custom-pathways) and [Conditionals](#conditionals) say where a continue goes.

This page is the Slice 0 contract. There is one module type and one hop compile. It does not cut over runtime, invert coordinator handoff, restyle Admin → Modules, or change the Modules Demo walkthrough. Those wait on later internals tickets and [TEN-1333](https://linear.app/tenzo-ai/issue/TEN-1333/starting-ui-modules-bank-and-job-journey-picker-for-25-sep).

## Chosen shape

One global hop executor applies every hop. One work runner per module does that module's work. One composition coordinator reads the job's compiled links and activates the next instance, or Stops.

The rejected shape is a Module class that owns a private stage orchestrator and starts the next module. The class carries its own hop table and names the module that runs next.

```mermaid theme={null}
flowchart LR
  subgraph rejected ["Rejected shape"]
    Module["Module class"] --> Private["Private stage orchestrator"]
    Module --> StartsNext["Starts the next module"]
  end

  subgraph chosen ["Chosen shape"]
    Work["Work runner\none per module"] -->|"continue(output_id) or hold"| Coord["Composition coordinator\none"]
    Coord -->|"next instance or Stop"| Next["Next work runner"]
    Work -->|"one hop"| Hop["Hop executor, one global\nfrom-stage, to-stage, action profile\nstage write, history, actions"]
    Next -->|"one hop"| Hop
  end

  style rejected fill:#fdecea,stroke:#d9534f
  style chosen fill:#f0f8e8,stroke:#5cb85c
```

The diagrams on this page are the contract. Runtime still applies a hop through `TenzoStageOrchestrator` ([TEN-1316](https://linear.app/tenzo-ai/issue/TEN-1316/runtime-executes-compiled-transition-plans)). Resume Review `continue_handoff` still activates Interview ([TEN-1315](https://linear.app/tenzo-ai/issue/TEN-1315/coordinator-executes-composition-modules-do-not-start-each-other)).

## Why the split

A hop is one operation. The compiled plan names from-stage, to-stage, and action profile. The hop executor writes the stage, records history, and runs that plan's actions. The operation is the same for every module.

Work differs per module. Resume Review scores a resume. AI Interview runs outreach. That procedure stays in the module's work runner.

A third module (Scheduling, ID Verification, or a test double) is a definition, a work runner, and a link. The definition lists sit-able stages, inner transitions, and outputs. The work runner reports `continue(output_id)` or `hold`. The link names the next instance or Stop. The module does not copy the hop table and does not name the next module. [Adding a module](#adding-a-module) is that boundary.

## A pass

On a pass, the work runner reports `continue(output_id)`. The coordinator follows the job's compiled link for that output. The next work runner activates. The one hop executor writes the stage.

Hold does not follow a link. The candidate stays with that work runner. A missing link also leaves the candidate in place ([Endings and custom pathways](#endings-and-custom-pathways)).

```mermaid theme={null}
sequenceDiagram
    participant Work as Work runner
    participant Coord as Composition coordinator
    participant Next as Next work runner
    participant Hop as Hop executor

    Work->>Coord: continue(output_id)
    Coord->>Coord: Follow the compiled link
    Coord->>Next: Activate the next instance
    Next->>Hop: Hop from-stage, to-stage, action profile
    Hop->>Hop: Stage write, history, actions
```

## Endings and custom pathways

Endings and custom pathways are composition data.

The work runner reports `continue(output_id)` or `hold`. The coordinator reads the job's compiled link for that output: the next instance, or Stop.

A missing link leaves the candidate in place.

Changing an ending edits the link. The work runner, the hop executor, and the module definition stay as they are.

Two jobs can share one output and end differently. One job's link starts the next module. The other job's link Stops. [Same output, different endings](#same-output-different-endings) draws that, and a conditional beside it.

## Conditionals

A conditional is composition data on the same coordinator. Either the work runner reports a richer output id, and each id has its own link, or the link carries a predicate and the one coordinator matches it.

The conditional is not code inside the module, and it is not a second hop list.

The recruiter editor and the predicate matcher are not runtime yet. This page does not add either one. The named follow-ons remain [TEN-1315](https://linear.app/tenzo-ai/issue/TEN-1315/coordinator-executes-composition-modules-do-not-start-each-other), [TEN-1316](https://linear.app/tenzo-ai/issue/TEN-1316/runtime-executes-compiled-transition-plans), and [TEN-1333](https://linear.app/tenzo-ai/issue/TEN-1333/starting-ui-modules-bank-and-job-journey-picker-for-25-sep).

## Same output, different endings

```mermaid theme={null}
flowchart TB
  subgraph jobs ["Two jobs, same output"]
    Passed["continue(passed)"] --> JobA["Job A link"]
    Passed --> JobB["Job B link"]
    JobA --> NextMod["Next module instance"]
    JobB --> StopJob["Stop"]
  end

  subgraph either ["Conditional"]
    High["continue(richer output id)"] --> LinkHigh["Link to next instance"]
    Low["continue(other output id)"] --> LinkLow["Link to Stop"]
    One["continue(passed), predicate on the link"] --> Match["Coordinator matches the predicate"]
    Match --> PredNext["Next instance"]
    Match --> PredStop["Stop"]
  end

  style jobs fill:#e8f4f8,stroke:#169fff
  style either fill:#fef8e8,stroke:#f0ad4e
```

The top is two jobs and one output. The bottom is a conditional: two links from richer output ids, or a predicate on the link. The recruiter editor and the predicate matcher are not runtime yet ([Conditionals](#conditionals)).

## Who owns what

| Concern                                                                  | Owner                       |
| ------------------------------------------------------------------------ | --------------------------- |
| Sit-able stages, inner transitions, outputs                              | Module definition (data)    |
| Work: scoring, outreach, overrides, retries, terminal result             | One work runner per module  |
| Applying a hop: critical actions, CAS, history, post-commit, best-effort | One global hop executor     |
| Gates and next-module / Stop                                             | One composition coordinator |
| Recruiter `TenzoStage`                                                   | Compatibility projection    |

Sit-able stages are the module stages on the compiled `ModuleDefinition`. The module-run FSM (`pending` / `evaluating` / …) is lease and work progress, not a second stage language. Recruiter-visible names on the click-in stay as they are until a UI ticket.

Outputs are keys on the definition (`passed`, `resume_rejected`). They do not name the next module. Composition links map an output to a module instance start or to Stop.

## Adding a module

The boundary is [Why the split](#why-the-split).

**May change:** one module definition, that module's work handlers, tests, composition links.

**Must not change:** the global hop executor, the pipeline compiler, the composition coordinator.

The hop executor may record which module owns a hop for history. It must not `switch` on module identity. The coordinator must not grow a `START_<MODULE>` action. It activates the instance the composition link names, or it Stops.

Today's `JobWorkflowCoordinator.START_AI_INTERVIEW` is the legacy V1 adapter. [TEN-1315](https://linear.app/tenzo-ai/issue/TEN-1315/coordinator-executes-composition-modules-do-not-start-each-other) replaces it with this contract's activate / Stop / hold / close / no-op.

## Layers

```text theme={null}
Module definition (stages, transitions, outputs, default actions)
        │
        ▼
Compiler ──► TransitionPlan + output destinations
        │
        ├─ hop executor (one, global) ──► CAS stage + history + actions
        ├─ work runner (one per module) ──► continue(output_id) or hold
        └─ composition coordinator (one) ──► activate linked instance | Stop | hold | close | stay
```

Resume Review scoring and Interview outreach stay module-owned **work**. They are not a second stage writer. "Resume Review orchestrator" and "Interview orchestrator" mean that work runner implementing the shared activate, claim, transition, and terminal types. They do not mean a private hop engine inside the module.

The click-in already reads the compiled inner graph. Allowed module hops come from that compile. Runtime still applies a hop through `TenzoStageOrchestrator`. [TEN-1316](https://linear.app/tenzo-ai/issue/TEN-1316/runtime-executes-compiled-transition-plans) points that executor at compiled plans. Extra actions on the click-in still do not persist or run.

## Handoff rules

* A module reports continue or hold. Continue carries an `output_id` only. The module does not report close / Stop.
* The coordinator applies global gates (canceled, terminal application decision, global hold) before resolving a hop.
* Continue always goes through the **compiled** composition index (`CompiledPipeline.output_destinations`). First matching link wins: activate that instance, or Stop.
* If none match, the candidate stays. Outreach is not invented.
* Authored closing outputs (`resume_rejected`, `fraud_rejected`) are ordinary outputs. Composition can Stop or send them onward; the work runner cannot skip that lookup.
* Resume Review omitted Interview means the continuing resume output links to Stop. No pending call.
* Modules do not call the next module's activate. Resume Review `continue_handoff` still does that today; [TEN-1315](https://linear.app/tenzo-ai/issue/TEN-1315/coordinator-executes-composition-modules-do-not-start-each-other) inverts it.
* Hops resolve through `TransitionLookupKey` (`from`, `to`, `action_profile`). Same stage pair with a different profile is a different compiled plan. [TEN-1316](https://linear.app/tenzo-ai/issue/TEN-1316/runtime-executes-compiled-transition-plans) executes that plan; it does not take a private action list.

## Canonical types

| Role              | Type                                                                                     |
| ----------------- | ---------------------------------------------------------------------------------------- |
| Module type       | `application_stage.pipeline.types.ModuleDefinition` (the only module type)               |
| Hop plan          | `application_stage.pipeline.types.TransitionPlan` (compile is the hop table)             |
| Job sequence view | Ordered `JobModuleStep` list built from that same `ModuleDefinition`                     |
| Contract resolver | `workflow_modules.orchestrator_contract.resolve_composition_handoff` (wired by TEN-1315) |

The compiler already accepts a third definition without product branches. The contract resolver does the same for handoff.

## Shared skeleton

Resume Review and AI Interview implement one activate, claim, transition, and terminal-result shape. A further module implements that shape and is registered by its module id. The composition coordinator does not gain a start action for it, and `JobWorkflowCoordinator` is unchanged.

A terminal result is continue, with an output id, or hold. Continue does not name the next module. In-progress work is not a terminal. Interview reports continue only when its run is completed and the candidate sits on one of that module's output stages. A passed or bypassed Resume Review stays a pass if a later stage is a rejection output.

Transition asks the existing Tenzo stage writer to apply the hop. It does not add pairs and does not filter pairs. Who can move where stays on that writer.

## Candidate position

A candidate's place is the module instance they are in, the sit-able stage they sit on, and any overlay. The instance id is not the module kind, so the same kind can appear twice. The recruiter-facing stage is derived from that place: an overlay hides the sit-able stage, and with no overlay the sit-able stage is the view.

A local move is decided from the sit-able stage on that instance. The derived recruiter stage is not an input. Overlay enter and release are not local moves. The decision only accepts a transition the module definition already has between two of its sit-able stages. Applying a compatibility hop is still the existing stage writer, which keeps today's allowed moves.

## Out of scope here

* Cutover, snapshot of existing jobs, Progress to Interview as composition data
* Admin Modules chrome, extra-action picker, Modules Demo walkthrough ([TEN-1333](https://linear.app/tenzo-ai/issue/TEN-1333/starting-ui-modules-bank-and-job-journey-picker-for-25-sep))

See [Module-owned stage orchestration](/internal/modules/overview) for V1 runtime as it runs today, and [Job module composition](/internal/modules/composition) for snapshots and the bank.
