# Workflow Runtime Decoupling Plan

## Goal

Split Harness into two explicit layers:

- workflow orchestration: state, events, decisions, commands, artifacts, leases, and recovery
- runtime execution: Codex, Claude Code, Anthropic API, or remote workers that execute accepted jobs

The workflow layer decides what should happen through agent-produced structured decisions.
The runtime layer only executes approved jobs and returns structured activity results.

## Current Implementation Status

This branch is an isolated phase-2 worktree based on `origin/main`.

Implemented now:

- generic workflow/runtime contract types in `harness-workflow::runtime`
- Postgres-backed `WorkflowRuntimeStore`
- transition validator and deterministic plan-issue policy adapter
- optional server startup wiring for `{workflow_namespace}_runtime`
- `PLAN_ISSUE` durable event/decision/command recording
- `ReplanCompleted` event recording after a successful replan
- PR detection durable binding through `PrDetected` / `bind_pr`
- PR feedback decisions for waiting, addressing feedback, and ready-to-merge
- repo backlog decisions for issue discovery, merged PR reconciliation, and stale workflow recovery
- generic runtime worker boundary for claiming jobs and recording structured activity results
- workflow-first runtime tree API and dashboard panel for instances, commands, runtime jobs, and
  rejected decisions
- runtime tree API and dashboard runtime job rows include runtime event counts, latest event type,
  retry cooldown, and prompt packet digest summaries
- workflow command outbox dispatcher that converts pending runtime commands into runtime jobs
- default-enabled server background loop for workflow command dispatch
- default-enabled server background runtime worker that executes runtime jobs through registered agents
- runtime job completion writes back command status and workflow completion events
- runtime completion reducer advances known workflow states from activity output
- runtime completion reducer returns repo backlog workflows to `idle` after successful dispatch or
  reconciliation activities
- runtime completion reducer can retry failed activities when the workflow instance declares a
  bounded retry policy
- runtime completion reducer can apply validated `workflow_decision` artifacts returned by
  activity results before falling back to built-in reducer mappings
- `WORKFLOW.md` can define a global failed activity retry budget and activity-specific retry
  overrides that are copied into workflow runtime instance data
- runtime command dispatch can select runtime profiles per workflow/activity pair, then activity,
  then workflow
- repo backlog lifecycle activities are handled as server-owned runtime worker operations for child
  workflow creation, merged-PR completion, and stale workflow recovery
- failed runtime activity retries can declare cooldown metadata and runtime jobs are not claimed
  before their `not_before` timestamp
- runtime profiles carry model and execution metadata into runtime jobs
- server runtime workers pass runtime profile model overrides into agent turns
- server turn lifecycle enforces runtime profile `timeout_secs` as an execution deadline
- server runtime workers pass profile reasoning effort and sandbox overrides into agent requests
- workflow runtime workers enforce profile `max_turns` as a workflow-instance runtime turn budget
- server runtime workers pass Codex runtime profile `approval_policy` into Codex exec and
  app-server requests
- server runtime workers build a structured prompt packet for agent jobs and record
  `RuntimePromptPrepared` with a SHA-256 packet digest before the agent turn starts
- prompt packets include a workflow/activity-specific `activity_result_schema` that tells the
  runtime agent which summary fields and reducer transitions apply to the current activity
- server runtime workers can parse a final fenced `harness-activity-result` JSON block from
  completed agent turns when the structured activity name matches the runtime job activity
- prompt packets include the current workflow decision contract with validator-derived allowed
  next states and command types for structured `workflow_decision` artifacts
- `quality_gate` is registered as a first-class runtime workflow contract with a run decision
  helper, transition validator, completion reducer, retry support, and activity result schema
- issue `POST /tasks` submissions now write `github_issue_pr` workflow runtime state with
  `IssueSubmitted`, a validated `submit_issue` decision, and a pending `implement_issue`
  command that is dispatched to runtime jobs instead of the legacy task runner
- prompt-only `POST /tasks` and batch task submissions now write `prompt_task` workflow runtime
  state with `PromptSubmitted`, a validated `submit_prompt` decision, and a pending
  `implement_prompt` command instead of registering legacy task runner rows
- dashboard ready-to-merge issue cards can submit runtime merge approvals through the workflow
  runtime API; the accepted `approve_merge` decision records `MergeApproved`, enqueues `mark_done`,
  and moves the runtime workflow to `done`
- runtime issue workflows with bound PRs now request `sweep_pr_feedback` through the workflow
  command outbox; runtime agents inspect GitHub and return structured `workflow_decision`
  artifacts or reducer-visible feedback signals
- runtime issue workflows now start a `pr_feedback` child workflow for PR feedback inspection; the
  child owns the `inspect_pr_feedback` runtime job and propagates structured results back to the
  parent issue workflow
- repo backlog polling now writes `RepoBacklogPollRequested`, a validated `poll_repo_backlog`
  decision, and a pending runtime activity command; runtime agents inspect GitHub and return
  `IssueDiscovered` signals or validated `workflow_decision` artifacts
- repo backlog scans that discover candidate issues now transition to `planning_batch` and enqueue
  a runtime-owned `plan_repo_sprint` activity before issue child workflows are started
- runtime sprint plans return `SprintTaskSelected` signals or a `sprint_plan` artifact; the reducer
  starts selected child workflows with dependency metadata instead of using the legacy sprint
  planner task
- GitHub issue intake is delegated to the `repo_backlog` workflow; the server no longer registers
  a legacy GitHub poller fallback
- legacy issue-workflow feedback candidates are no longer swept or adopted; only runtime
  `github_issue_pr` workflows can request the runtime `pr_feedback` child workflow
- direct `pr:N` submissions require an existing runtime issue workflow with a bound PR and request
  the runtime feedback sweep instead of creating an independent legacy PR task
- missing workflow runtime storage or disabled runtime dispatch/worker policy is a hard error for
  runtime-owned issue, prompt, GitHub intake, and PR feedback paths

Still intentionally not moved yet:

- non-runtime dashboard task actions still use existing task routes

## Non-Goals

- Do not remove existing Codex, Claude, workspace, queue, dashboard, or recovery code.
- Do not introduce a YAML workflow DSL before the runtime contract is proven.
- Do not make Rust code interpret GitHub PR feedback as product policy.

## Target Architecture

```text
Source adapters
  -> WorkflowEvent
  -> WorkflowController
  -> WorkflowPolicyAgent
  -> WorkflowDecision
  -> TransitionValidator
  -> WorkflowCommand
  -> RuntimeJob
  -> RuntimeWorker
  -> RuntimeEvent
  -> ActivityResult
  -> WorkflowEvent
```

## Layer Responsibilities

### Workflow Definition

Owns:

- state names and allowed transitions
- source event vocabulary
- activity names
- prompt policy
- structured decision schema
- retry, replan, feedback, and quality-gate policy
- acceptance and validation requirements

Does not own:

- Codex or Claude CLI argument construction
- runtime process management
- direct GitHub or git subprocess execution

### Workflow Runtime

Owns:

- workflow instances
- workflow events
- accepted and rejected decisions
- command outbox
- artifacts
- workflow leases
- idempotency keys
- transition validation
- resource budget checks

Does not own:

- product judgment about PR feedback
- deciding whether a plan concern is valid
- interpreting CI output beyond structured runtime result schemas

### Agent Runtime

Owns:

- claiming runtime jobs
- executing Codex, Claude Code, Anthropic API, or a remote worker
- streaming runtime events
- writing activity results
- surfacing runtime errors

Does not own:

- mutating workflow state directly
- deciding parent workflow transitions outside its structured output

## Initial Workflow Set

### `repo_backlog`

Purpose:

- discover issues and PRs for a repo
- reconcile GitHub state with local workflow state
- start or wake issue workflows

Main outputs:

- `IssueDiscovered`
- `IssueUpdated`
- `PrUpdated`
- `PrReviewUpdated`
- `RecoveryRequested`

### `github_issue_pr`

Purpose:

- own a single issue lifecycle
- bind PRs to issues
- coordinate implementation, replan, feedback, and readiness

Main states:

- `discovered`
- `scheduled`
- `planning`
- `implementing`
- `replanning`
- `pr_open`
- `awaiting_feedback`
- `addressing_feedback`
- `ready_to_merge`
- `blocked`
- `done`
- `failed`
- `cancelled`

### `pr_feedback`

Purpose:

- inspect PR comments, review state, checks, and mergeability
- produce a structured feedback report

Main outputs:

- `FeedbackFound`
- `NoFeedbackFound`
- `ChangesRequested`
- `Approved`
- `ChecksFailed`
- `ChecksPassed`
- `Mergeable`

### `quality_gate`

Purpose:

- verify configured validation commands
- convert runtime validation into structured artifacts

Status:

- first-class definition id: `quality_gate`
- first-class runtime activity: `run_quality_gate`
- validator states: `pending -> checking -> passed`, plus `blocked`, `failed`, and `cancelled`
- successful `run_quality_gate` runtime completion reduces `checking` to `passed`
- failed `run_quality_gate` can use the generic bounded runtime retry policy before terminal failure
- prompt packets expose the quality-gate transition and summary contract to the runtime agent

Main outputs:

- `QualityPassed`
- `QualityFailed`
- `QualityBlocked`

### `prompt_task`

Purpose:

- own a manually submitted prompt-only task
- preserve prompt text, dependency metadata, source, and external correlation ids in workflow
  runtime state
- run implementation through runtime command dispatch instead of legacy task rows

Main states:

- `submitted`
- `awaiting_dependencies`
- `implementing`
- `done`
- `blocked`
- `failed`
- `cancelled`

Main outputs:

- `PromptSubmitted`
- `PromptSubmissionCancelled`
- `PromptImplemented`

## Data Model

The durable bus uses Postgres tables:

- `workflow_definitions`
- `workflow_instances`
- `workflow_events`
- `workflow_decisions`
- `workflow_commands`
- `runtime_jobs`
- `runtime_events`
- `workflow_artifacts`

The current implementation creates these tables in the optional
`{workflow_namespace}_runtime` schema. If the store cannot initialize, existing task execution
continues and the server reports the optional subsystem as degraded.

## Runtime Profiles

Runtime jobs reference profiles by name.
Runtime-specific details stay outside workflow definitions.

Initial runtime kinds:

- `codex_exec`
- `codex_jsonrpc`
- `claude_code`
- `anthropic_api`
- `remote_host`

Runtime profile fields:

- runtime kind
- model
- reasoning effort
- sandbox
- approval policy
- max turns
- timeout
- environment policy
- working directory
- tool allowlist

## Prompt Packet Contract

Every runtime job receives a complete prompt packet:

- workflow definition name and version
- workflow instance id and current state
- triggering source event
- allowed decisions or activity result schema
- project root and repository identity
- subject key, such as `issue:123` or `pr:456`
- recent workflow event history
- parent and child workflow links
- previous artifacts
- resource budget summary
- hard constraints
- acceptance criteria
- validation requirements
- required structured output schema

The runtime should persist a prompt digest or redacted prompt packet for audit and recovery.

Implemented now:

- server-owned runtime jobs build `harness.runtime.prompt_packet.v1`
- the generated prompt is derived from that packet instead of separate ad hoc prompt sections
- prompt packets include `harness.runtime.activity_result.v1` schema guidance for known
  workflow/activity pairs such as `implement_issue`, `replan_issue`, and `address_pr_feedback`
- agent prompts instruct runtimes to return optional structured activity output in a final fenced
  `harness-activity-result` JSON block, and mismatched activity names are ignored
- prompt packets include validator-derived `workflow_decision_contract` data when a workflow
  instance is available, so agents can propose only visible next states and command types
- runtime events persist `RuntimePromptPrepared` with `prompt_packet_digest` and the redacted packet
- agent-backed activity results include a `runtime_prompt_packet` artifact that references the
  digest used for the turn

## Migration Plan

### Phase 0: Core Contract

Status: implemented in `harness_workflow::runtime`.

Added a new module under `harness-workflow` with:

- workflow definitions
- workflow instances
- workflow events
- workflow decisions
- workflow commands
- runtime jobs
- runtime events
- activity results
- transition validator

Tests:

- serde round trips for every public contract type
- valid replan decision is accepted
- invalid state mismatch is rejected
- unallowed command is rejected
- duplicate dedupe key is rejected
- terminal reopen is rejected by default
- runtime job and activity result round trips are stable

### Phase 1: Shadow Bus

Status: partially superseded by phase 2.

Keep existing task-first execution.
Write workflow events and decisions in parallel while the existing task runner remains the
execution path.

Tests:

- existing task creation still works
- issue task completion writes workflow events
- PR URL detection writes a PR binding artifact

### Phase 2: Replan Through Workflow Decision

Status: implemented for `PLAN_ISSUE`.

Move `PLAN_ISSUE` handling out of executor-local failure handling.

Flow:

```text
implementation runtime job emits PlanIssueRaised
  -> workflow policy decision
  -> validator accepts RunReplan
  -> command outbox records replan_issue
  -> existing executor runs replan during this transition period
  -> ReplanCompleted event
  -> implementation runtime job resumes with revised plan
```

Tests:

- `PLAN_ISSUE` does not directly mark the issue workflow failed
- force-execute records the concern and continues
- repeated replan hits the configured loop limit and blocks

### Phase 3: PR Feedback Workflow

Status: implemented for runtime-owned feedback inspection.

The current branch records PR feedback decisions into the workflow runtime and routes runtime-owned
feedback inspection through a dedicated child workflow.

Implemented now:

- `PrDetected` events bind issue workflows to PRs through a validated `bind_pr` decision
- review waits record `NoFeedbackFound` / `wait_for_pr_feedback`
- actionable review or validation failures record `FeedbackFound` / `address_pr_feedback`
- approved reviews and graduated low-risk exits record `PrReadyToMerge` / `mark_ready_to_merge`
- runtime `github_issue_pr` workflows with attached PRs are swept by a server background loop that
  writes a validated `sweep_pr_feedback` decision and starts a child `pr_feedback` workflow instead
  of enqueueing a legacy `pr:N` task
- legacy issue-workflow feedback candidates are no longer adopted; runtime `github_issue_pr`
  workflows are the authoritative source for PR feedback sweeps
- runtime feedback storage, dispatch, and worker policy are required; the sweeper marks the project
  degraded instead of falling back to an independent PR task
- child `pr_feedback` workflows enqueue `inspect_pr_feedback` runtime jobs with PR metadata
- `inspect_pr_feedback` runtime completions update the child workflow and propagate the same
  structured activity result to the parent issue workflow
- propagated feedback completions can advance the parent workflow through structured
  `workflow_decision` artifacts or explicit feedback signals

Tests:

- feedback report with blocking items moves parent workflow to `addressing_feedback`
- no actionable feedback keeps parent workflow in `awaiting_feedback`
- approved and checks-passed events can move parent workflow to `ready_to_merge`
- runtime PR feedback sweep requests start `pr_feedback` child workflows, enqueue
  `inspect_pr_feedback`, and suppress duplicate active feedback inspection work
- `inspect_pr_feedback` child completions update both child and parent workflow state
- legacy issue-workflow feedback sweeper is removed, so stale legacy candidates do not create
  runtime commands

### Phase 4: Repo Backlog Workflow

Status: implemented for workflow-owned GitHub polling and runtime-owned sprint planning.

The current branch records repo backlog decisions into the workflow runtime. GitHub backlog poll
requests are now workflow commands when runtime dispatch and workers are enabled. Candidate issues
from a scan flow through a runtime-owned sprint planning activity before child issue workflows are
started.

Implemented now:

- configured GitHub repos emit `RepoBacklogPollRequested` and a validated `poll_repo_backlog`
  activity command instead of registering a legacy GitHub poller fallback
- runtime agents receive a `poll_repo_backlog` activity result contract and return
  `IssueDiscovered`, `IssueSkipped`, or `NoOpenIssueFound` signals
- `IssueDiscovered` signals from a successful repo backlog scan reduce to a validated
  `plan_repo_sprint` activity command, so dependency planning runs as a workflow runtime job
- `SprintTaskSelected` signals or a `sprint_plan` artifact from a successful runtime sprint plan
  reduce to validated `start_child_workflow` commands; those child workflow commands auto-submit
  the issue through the existing `submit_issue` decision so implementation continues through
  runtime jobs
- selected sprint dependencies are copied into child issue submissions so dependent issues wait in
  `awaiting_dependencies` until the dependency release tick unblocks them
- empty scans return the repo backlog workflow to `idle`
- open GitHub issues without an existing issue workflow record `IssueDiscovered` and emit a
  validated `start_issue_workflow` child workflow command
- externally merged PRs record `PrMerged`, emit `mark_bound_issue_done`, and update the bound
  issue workflow to `done`
- startup recovery of stale active issue workflows records `RecoveryRequested` and emits
  `recover_issue_workflow`

Tests:

- repo backlog poll decision starts runtime scan
- runtime repo backlog poll tick enqueues a runtime command
- runtime completion reducer turns `IssueDiscovered` signals into runtime sprint planning
- runtime completion reducer dispatches `SprintTaskSelected` signals
- runtime completion reducer idles after an empty repo backlog scan
- runtime completion reducer idles after an empty runtime sprint plan
- activity result schema describes the repo backlog poll contract
- activity result schema describes the runtime sprint plan contract
- open issue without workflow emits start command
- merged PR updates the bound issue workflow
- stale active workflow emits recovery event
- server runtime worker executes repo backlog lifecycle commands without sending an agent turn

### Phase 5: Runtime Worker Abstraction

Status: implemented.

The current branch adds the generic worker boundary while leaving the existing Codex and Claude task
execution paths in place.

Implemented now:

- `RuntimeJobExecutor` trait for runtime-specific execution adapters
- `RuntimeWorker` that claims one pending runtime job, records claim/result events, executes the
  adapter, and completes the durable job
- `ActivityResult::cancelled` so cancellation can be represented as a structured runtime result
- command outbox rows are automatically converted into runtime jobs by the server dispatch loop
- server-owned runtime workers claim pending runtime jobs through registered agent runtimes

Tests:

- runtime worker claims one job once
- runtime events stream in sequence
- failed runtime job records structured error
- cancelled runtime job releases lease

### Phase 6: Workflow-First UI

Status: partially implemented.

Keep task drill-down, but make workflow tree the primary dashboard surface.

Implemented now:

- `GET /api/workflows/runtime/tree` returns workflow instances as a parent/child tree
- each node includes events, decisions, command outbox rows, and runtime jobs
- the dashboard active view shows a compact workflow runtime panel above the task kanban
- rejected decisions display operator-readable reasons
- runtime issue rows in `GET /tasks` expose a workflow summary so dashboard cards can group by
  runtime workflow state
- ready-to-merge runtime issue cards call `POST /api/workflows/runtime/merge`, which records a
  validated `approve_merge` decision instead of relying on a legacy task row
- non-terminal runtime issue and prompt workflows can be cancelled directly from the runtime tree;
  the server records a validated cancellation decision and cancels unfinished commands/jobs

Still intentionally not moved yet:

- non-runtime task cards still use existing task endpoints

Tests:

- workflow tree displays parent and child workflows
- runtime jobs are visible under workflow activities
- runtime issue rows expose workflow state in the task list
- runtime dashboard merge approval records `MergeApproved`, `approve_merge`, and `mark_done`
- runtime dashboard cancellation records a workflow runtime cancellation and refreshes the runtime
  tree
- rejected decisions show operator-readable reasons

### Phase 7: Command Outbox Dispatch

Status: implemented.

Convert accepted workflow commands into durable runtime jobs without making the existing task runner
depend on the new path yet.

Implemented now:

- `RuntimeCommandDispatcher` scans pending command outbox rows
- commands that require runtime execution enqueue `runtime_jobs`
- non-runtime commands are marked `skipped` so they do not stay pending forever
- repeated dispatch is idempotent when a runtime job already exists for the command
- command outbox dispatch uses durable dispatcher ownership and lease expiry so multiple server
  dispatch loops can claim commands without materializing duplicate runtime jobs
- expired dispatch leases can be reclaimed by another dispatcher

Tests:

- activity commands enqueue runtime jobs with prompt input context
- non-runtime commands are skipped without creating jobs
- command claim leases hide active dispatch work from other dispatchers and allow stale work to be
  reclaimed

### Phase 8: Server Dispatch Loop

Status: implemented.

Run the command outbox dispatcher from the server as a policy-controlled background loop.

Implemented now:

- `WORKFLOW.md` supports `runtime_dispatch` policy fields
- the server spawns a weak-reference runtime command dispatcher loop when the workflow runtime store
  is available
- the loop is enabled by default and can be disabled explicitly in `WORKFLOW.md`
- each tick converts pending command rows into runtime jobs using the configured runtime profile
- each dispatch tick first claims command rows with a durable dispatch lease before materializing
  runtime jobs, so concurrent loops use the same database protocol

Tests:

- workflow config parses dispatch policy fields
- server dispatch tick creates runtime jobs and marks commands dispatched

### Phase 9: Server Runtime Worker Loop

Status: partially implemented.

Run claimed runtime jobs through the existing agent turn lifecycle under workflow policy control.

Implemented now:

- `WORKFLOW.md` supports `runtime_worker` policy fields
- the server can claim pending runtime jobs on a configurable worker tick
- runtime jobs create normal Harness threads and turns, so agent output remains visible in the
  existing conversation/runtime log path
- completed turns are written back as structured `ActivityResult` payloads
- completed turns can provide a dedicated `harness-activity-result` JSON block so runtime agents
  can return reducer-visible artifacts and validation records without server text inference
- repo backlog lifecycle activities are handled as server-owned commands, creating or reusing child
  issue workflows, marking merged bound issues done, and recovering stale active issue workflows
  without asking an agent to mutate workflow tables
- `remote_host` jobs remain reserved for external runtime hosts and are not executed locally

Still intentionally not moved yet:

- non-Codex approval policy remains metadata until those runtimes define compatible contracts
- workflow completion reducer coverage is intentionally limited to known activity/state pairs
- runtime workers are process-local; distributed worker leasing beyond the job claim is not added yet

Tests:

- workflow config parses worker policy fields
- server worker tick claims a job, runs a registered agent, and records a succeeded runtime job
- server worker starts child workflows, marks bound issues done, and recovers stale issue workflows
  directly, then feeds the completion back through the runtime reducer

### Phase 10: Runtime Completion Feedback

Status: partially implemented.

Feed runtime job completion back into the workflow event stream.

Implemented now:

- runtime workers update the originating command status to `completed`, `failed`, or `cancelled`
- runtime workers append `RuntimeJobCompleted` events to the parent workflow when the command row
  is known, including the original workflow command payload for retry/reducer policy
- direct runtime jobs without a workflow command row still complete normally
- workflow runtime instance data carries configured `runtime_retry_policy` values from
  `WORKFLOW.md` when policy is present
- runtime jobs can carry `not_before` cooldown timestamps and the worker claim query leaves future
  retry jobs pending until the cooldown expires
- runtime prompt packets include workflow/activity-specific activity result schemas for known
  GitHub issue PR and repo backlog activities
- activity result schemas expose `error_kind`, allowing agents to mark fatal or configuration
  failures as non-retryable
- timeout failures are represented as `error_kind = timeout` so runtime completion and retry policy
  can distinguish lifecycle timeouts from generic agent failures

Still intentionally not moved yet:

- reducer coverage is limited to known activity/state pairs
- retry cooldown scheduling is best-effort local timing; distributed lease ownership remains a
  separate runtime-host concern

Tests:

- runtime worker completion updates command status and appends a workflow event
- existing runtime worker tests still cover direct jobs without command rows
- runtime worker skips pending runtime jobs whose `not_before` timestamp is still in the future

### Phase 11: Runtime Completion Reducer

Status: partially implemented.

Consume `RuntimeJobCompleted` events and advance workflow state for known activity results.

Implemented now:

- `replan_issue` completion moves GitHub issue workflows from `replanning` to `implementing`
- `address_pr_feedback` completion moves GitHub issue workflows from `addressing_feedback` to
  `awaiting_feedback`
- successful `implement_issue` completion can bind a PR and move GitHub issue workflows from
  `implementing` to `pr_open` when the activity result includes a structured `pull_request`
  artifact
- successful activity results can return a structured `workflow_decision` artifact; the runtime
  worker records it only after the normal transition validator accepts it
- successful repo backlog dispatch and reconciliation activity completion moves `dispatching` and
  `reconciling` backlog workflows back to `idle`
- failed, blocked, and cancelled activity results transition workflows to `failed`, `blocked`, or
  `cancelled` through validated workflow decisions
- failed activity results retry the same activity before failing when
  `runtime_retry_policy.max_failed_activity_retries` is present on the workflow instance data
- activity-specific retry budgets under `runtime_retry_policy.activity_retries` override the global
  failed activity retry budget
- retry policy supports `retry_delay_secs` and `max_retry_delay_secs`, including per-activity
  overrides, and writes `retry_not_before` metadata into retry commands
- failed command retries preserve the original workflow command type and payload before adding
  retry metadata
- failed activity results can carry `error_kind`; `fatal` and `configuration` failures bypass retry
  policy and fail the workflow immediately
- failed timeout activity results carry `error_kind = timeout`, preserve that taxonomy in retry
  commands, and can retry under the workflow retry budget
- unknown successful activity/state pairs are preserved as completion events without unsafe state
  changes

Still intentionally not moved yet:

- implementation completion does not infer PR state from free-form agent text; only structured
  `pull_request` artifacts drive this reducer path
- retry policy does not yet model provider-specific quota, billing, or rate-limit taxonomies beyond
  the portable activity error kinds

Tests:

- reducer resumes implementation after replan completion
- reducer binds PR metadata from structured `pull_request` artifacts on implementation completion
- reducer returns structured `workflow_decision` artifacts so normal validation can accept or reject
  agent-proposed workflow transitions
- reducer idles repo backlog workflows after dispatch and reconciliation completion
- reducer ignores unmapped successful activity completions
- reducer retries a failed activity until the configured retry budget is exhausted
- reducer honors activity-specific retry budget overrides
- reducer adds cooldown metadata to retry commands when retry delay policy is configured
- reducer preserves `StartChildWorkflow` command type when retrying failed child workflow dispatch
- reducer skips retries for non-retryable `fatal` and `configuration` activity errors
- reducer treats timeout activity failures as retryable and preserves the timeout taxonomy in retry
  command metadata
- runtime worker applies the reducer, records decisions, and updates workflow state

### Phase 12: Workflow Runtime Profile Selection

Status: implemented.

Route runtime command dispatch through a workflow-aware profile selector instead of a single global
runtime profile.

Implemented now:

- `WORKFLOW.md` front matter can define `runtime_dispatch.workflow_profiles` overrides keyed by
  workflow definition id, `runtime_dispatch.activity_profiles` overrides keyed by activity name,
  and `runtime_dispatch.workflow_activity_profiles` overrides keyed first by workflow definition id
  and then by activity name
- command dispatch loads the command's workflow instance and activity name, then selects the most
  specific matching runtime profile when available
- commands without an explicit activity use their command type as the runtime activity key, so
  child workflow dispatch can be targeted as `start_child_workflow`
- runtime job input persists the resolved runtime activity key for external workers and server
  worker prompts
- selection precedence is workflow/activity pair, global activity, workflow, then configured default
  runtime profile
- server background dispatch builds the same profile selector from project workflow config
- server background dispatch persists a project-scoped `runtime_profile_manifest` workflow
  definition artifact with source path, content hash, resolved default profile, overrides, and
  precedence metadata before dispatching runtime jobs

Tests:

- workflow config parses per-workflow, per-activity, and workflow/activity runtime profile overrides
- dispatcher uses workflow/activity overrides first, then activity overrides, then workflow
  definition overrides, then the default profile
- dispatcher selects activity overrides for child workflow dispatch through the
  `start_child_workflow` activity key
- server dispatch config conversion preserves default, workflow override, activity override, and
  workflow/activity override behavior
- server dispatch tick persists the runtime profile manifest definition before materializing runtime
  jobs

### Phase 13: Runtime Profile Metadata Application

Status: implemented.

Carry runtime profile metadata beyond profile names and apply safe fields during server-owned
runtime execution.

Implemented now:

- `runtime_dispatch`, `runtime_dispatch.workflow_profiles`,
  `runtime_dispatch.activity_profiles`, and `runtime_dispatch.workflow_activity_profiles` can
  configure `model`, `reasoning_effort`, `sandbox`, `approval_policy`, `max_turns`, and
  `timeout_secs`
- profile overrides inherit default metadata for fields they do not set
- dispatcher persists the full selected `RuntimeProfile` into runtime job input
- runtime worker includes the parsed profile metadata in the prompt packet
- runtime worker passes `model` and `timeout_secs` into the turn lifecycle request

Still intentionally not moved yet:

- non-Codex approval policy still comes from registered agent/server configuration
- timeout is not a workflow reducer retry budget yet

Tests:

- workflow config parses runtime profile metadata fields
- dispatch profile selector inherits and overrides metadata correctly
- dispatcher preserves full profile metadata in runtime job input
- server worker passes a runtime profile model override to the agent request

### Phase 14: Runtime Profile Execution Timeout

Status: partially implemented.

Apply `timeout_secs` as a server lifecycle deadline so runtime profiles constrain ordinary
`CodeAgent` streaming execution as well as adapter-based turns.

Implemented now:

- turn lifecycle builds a hard execution deadline from `TurnLifecycleOptions::timeout_secs`
- deadline expiry marks the agent turn failed with a timeout-specific error item
- runtime worker completion converts the failed turn into a failed `ActivityResult`
- runtime job output records the timeout reason as structured activity error data
- runtime worker completion classifies lifecycle timeout errors as `ActivityErrorKind::Timeout`
- timeout activity failures flow through the same workflow retry reducer budget as other retryable
  runtime failures

Tests:

- runtime worker fails a blocked registered agent when profile `timeout_secs` expires
- runtime worker classifies timeout turn failures with `error_kind = timeout`
- runtime reducer retries timeout activity failures when retry policy allows it

### Phase 15: Runtime Profile Execution Metadata

Status: partially implemented.

Apply runtime profile metadata that already has a concrete execution-layer contract without moving
workflow policy into Rust code.

Implemented now:

- `AgentRequest` and adapter `TurnRequest` carry optional `reasoning_effort` and `sandbox_mode`
- runtime worker parses profile `sandbox` values and passes request metadata into turn lifecycle
- Codex exec uses request `reasoning_effort` for `model_reasoning_effort` and request sandbox for
  both CLI `-s` and the OS sandbox wrapper
- Codex app-server requests include profile `effort`, thread `sandbox`, and turn
  `sandboxPolicy` metadata
- Claude CodeAgent uses request `reasoning_effort` when no execution phase already defines effort
- Claude CodeAgent applies request sandbox to the OS sandbox wrapper

Still intentionally not moved yet:

- `approval_policy` is not applied to non-Codex runtime profiles because those adapters do not yet
  expose a matching approval contract
- Claude adapter sandbox wrapping is still unchanged because that path does not currently own a
  sandbox wrapper configuration

Tests:

- Codex exec base arguments honor request reasoning effort and sandbox overrides
- Codex app-server thread and turn payloads include runtime profile overrides
- Claude CodeAgent base arguments honor request reasoning effort without overriding execution phase
- server runtime worker passes profile reasoning effort and sandbox overrides to the agent request

### Phase 16: Runtime Profile Turn Budget

Status: partially implemented.

Apply `max_turns` as a workflow-runtime budget without changing agent CLI behavior. In the current
runtime contract each `RuntimeJob` maps to one agent turn, so the budget is enforced before a
claimed runtime job starts execution.

Implemented now:

- workflow runtime store can count started runtime turns for a workflow instance
- runtime worker checks the selected profile `max_turns` after claiming a job and before invoking
  the runtime executor
- exhausted budgets complete the job with a structured blocked `ActivityResult`
- blocked budget results mark the workflow command as `blocked` and flow through the existing
  runtime completion reducer
- currently running jobs from the same workflow count toward the budget, while the claimed current
  job is excluded so `max_turns: 1` allows the first turn and blocks the second

Still intentionally not moved yet:

- `max_turns` is not a multi-turn conversation budget inside a single runtime job because runtime
  jobs currently execute one lifecycle turn
- `approval_policy` is only applied to Codex runtime profiles

Tests:

- runtime worker executes the first job under `max_turns: 1`
- runtime worker blocks the second job for the same workflow without invoking the executor

### Phase 17: Runtime Profile Approval Policy

Status: partially implemented.

Apply `approval_policy` only where the execution layer has a concrete native contract. This keeps
workflow policy declarative while avoiding silent no-ops on adapters that do not yet expose approval
mode selection.

Implemented now:

- runtime worker validates profile `approval_policy` values against Codex native modes:
  `untrusted`, `on-failure`, `on-request`, and `never`
- runtime worker rejects `approval_policy` on non-Codex runtime kinds until those adapters define
  compatible semantics
- `AgentRequest` and adapter `TurnRequest` carry optional `approval_policy`
- Codex exec passes request approval policy via CLI `-a`
- Codex app-server passes request approval policy via `approvalPolicy` on both `thread/start` and
  `turn/start`
- Codex app-server turn sandbox metadata now uses the current `sandboxPolicy` object shape while
  thread startup keeps the native `sandbox` mode string

Still intentionally not moved yet:

- Claude approval behavior remains controlled by its existing permission/tooling path
- remote host runtimes must define their own runtime-host approval contract before using this field

Tests:

- Codex exec base arguments honor request approval overrides and keep the prompt as the final token
- Codex app-server thread and turn payloads include `approvalPolicy`
- runtime worker passes profile approval policy into the agent request
- runtime worker rejects unknown approval policies and non-Codex approval policy usage

### Phase 18: Workflow-First Task Submission

Status: implemented for GitHub issue and prompt-only submissions.

Route operator GitHub issue submissions and prompt-only task submissions through the workflow
runtime instead of the legacy task runner.

Implemented now:

- issue `POST /tasks` submissions write an `IssueSubmitted` event to the matching
  `github_issue_pr` workflow instance
- the workflow policy records a validated `submit_issue` decision from `discovered`,
  `scheduled`, `failed`, or `cancelled` to `scheduled`
- the accepted decision writes an `implement_issue` command with task-scoped dedupe so explicit
  resubmissions create a new auditable workflow command
- the command is left `pending` so the runtime dispatcher can materialize it into a runtime job
- prompt-only `POST /tasks` submissions write a `PromptSubmitted` event to a `prompt_task`
  workflow instance
- prompt submission records a validated `submit_prompt` decision and an `implement_prompt` command
  when dependencies are ready
- prompt submissions with pending dependencies stay in `awaiting_dependencies` until the dependency
  release tick records a new `submit_prompt` decision and enqueues `implement_prompt`
- prompt runtime workflows are visible through `GET /tasks` and `GET /tasks/{id}` even though no
  legacy task row exists
- issue and prompt submissions require `WorkflowRuntimeStore`; missing runtime storage is a hard
  server error for runtime-owned submissions
- issue and prompt submissions no longer register legacy task rows or call the legacy task runner
- legacy issue-workflow PR feedback candidates are no longer routed or adopted; the legacy PR task
  fallback and legacy issue-workflow sweeper are removed
- direct PR submissions are accepted only when the PR is already bound to a runtime issue workflow;
  the submission requests the runtime `pr_feedback` child workflow instead of creating a standalone
  `pr:N` lifecycle
- issue and prompt submission responses include the runtime `workflow_id`, so dashboard actions can
  address the workflow runtime API directly instead of treating the returned `task_id` as a legacy
  task-row handle
- the submit-success cancel action calls `POST /api/workflows/runtime/cancel` when the submission
  response includes a runtime workflow handle
- worktree cards carry runtime workflow handles from `GET /tasks` and cancel through
  `POST /api/workflows/runtime/cancel` for runtime-backed issue and prompt submissions

Still intentionally not moved yet:

- the `task_id` returned for issue and prompt submissions remains a workflow submission correlation
  id for stream/detail compatibility, not a legacy task row id

Tests:

- issue submission decisions validate in the workflow contract layer
- failed issue workflows can be explicitly reopened by operator submission
- server submission tracking records `IssueSubmitted`, `implementing` state, and a pending
  `implement_issue` command without registering a legacy task row
- prompt submission decisions validate in the workflow contract layer
- prompt submission tracking records `PromptSubmitted`, `implementing` state, and a pending
  `implement_prompt` command without registering a legacy task row
- prompt submissions wait for dependencies and release to `implementing` once dependencies are done
- task creation, task listing, task detail, and task batch APIs expose runtime prompt submissions
  with `execution_path = workflow_runtime`
- task creation and task batch APIs expose runtime `workflow_id`
- submit-success cancellation uses the runtime workflow endpoint when `workflow_id` is available
- runtime prompt task summaries expose workflow metadata for dashboard/worktree runtime actions
- worktree cancellation uses the runtime workflow endpoint for runtime-backed cards
- PR feedback submissions fail closed when workflow runtime storage, dispatch, or worker policy is
  unavailable

## Test Strategy

Run after each implementation step:

- `cargo fmt --all`
- `cargo check -p harness-workflow`
- `cargo test -p harness-workflow`

Before any PR:

- `RUSTFLAGS="-Dwarnings" cargo check --workspace --all-targets`

Expensive Postgres-heavy workspace tests may be skipped during local design iteration, but the
contract module must have full non-database unit coverage.
