Skip to content

Lifecycle Event Contract

All coding harness connectors (claude-code, codex, opencode, pi, pi-rust, and any connector polling via agent-ctl) MUST include the normalized lifecycle payload in their events. This enables harness-agnostic routing, supervision, and automation.

Without normalization, downstream routes and SOPs must be harness-specific — different event types, different payload shapes, different terminal semantics. The lifecycle contract solves this by requiring a shared payload shape alongside any connector-specific fields.

Key principles:

  • Additive. The lifecycle payload lives inside event.payload.lifecycle and event.payload.session, alongside existing fields. Backward compatibility preserved.
  • Type-consistent. Non-terminal phases emit resource.changed. Terminal phases emit actor.stopped. This preserves OrgLoop’s 3-type event model.
  • Neutral. Like actor.stopped itself, the contract observes state — it doesn’t interpret intent. The outcome field records what happened, not what should happen next.
PhaseTerminalEvent TypeDescription
startedNoresource.changedSession launched, harness process running
activeNoresource.changedSession actively processing (tool calls, edits)
completedYesactor.stoppedSession ended normally (work finished)
failedYesactor.stoppedSession ended with error (crash, non-zero exit)
stoppedYesactor.stoppedSession ended by external action (cancel, signal)

Required when lifecycle.terminal is true:

OutcomeMeaning
successWork completed as intended (exit 0, task done)
failureWork failed (non-zero exit, crash, timeout)
cancelledSession stopped by user or system before completion
unknownTerminal state reached but cause is unclear
payload:
lifecycle:
phase: started|active|completed|failed|stopped
terminal: true|false
outcome: success|failure|cancelled|unknown # required when terminal
reason: string # optional machine reason
dedupe_key: string # stable per transition
session:
id: string # session identifier
adapter: string # adapter/harness adapter name
harness: claude-code|codex|opencode|pi|pi-rust|other
cwd: string # working directory (optional)
started_at: string # ISO 8601 (optional)
ended_at: string # ISO 8601, terminal only
exit_status: number # process exit code, terminal only
# ... connector-specific fields preserved alongside

All lifecycle events MUST include:

  • provenance.platform — connector platform (e.g., "claude-code", "agent-ctl")
  • provenance.platform_eventsession.<phase> (e.g., "session.started", "session.completed")

The dedupe_key prevents duplicate delivery of the same lifecycle transition. Format: <harness>:<session_id>:<phase>.

Examples:

  • claude-code:sess-123:started
  • claude-code:sess-123:completed
  • codex:sess-456:failed
Exit StatusPhaseOutcomeReason
0completedsuccessexit_code_0
1-127failedfailureexit_code_<N>
130 (SIGINT)stoppedcancelledsigint
137 (SIGKILL)stoppedcancelledsigkill
143 (SIGTERM)stoppedcancelledsigterm
128+N (other)stoppedcancelledsignal_<N>
agent-ctl StatusPhaseOutcomeReason
running (new)started
idle (from running)activeidle
running (from idle)activerunning
stoppedstoppedunknownsession_stopped
errorfailedfailuresession_error
(disappeared)stoppedunknownsession_stopped

Known harness identifiers: claude-code, codex, opencode, pi, pi-rust, other.

For agent-ctl, the harness is resolved from the adapter name. Unknown adapters map to other.

The contract is defined in @orgloop/sdk:

import type {
LifecyclePayload,
LifecyclePhase,
LifecycleOutcome,
LifecycleState,
SessionInfo,
HarnessType,
} from '@orgloop/sdk';
import {
validateLifecycleEvent,
validateLifecyclePayload,
assertLifecycleConformance, // test helper — throws on invalid
createLifecycleEvent, // test factory
eventTypeForPhase,
buildDedupeKey,
TERMINAL_PHASES,
NON_TERMINAL_PHASES,
} from '@orgloop/sdk';

All lifecycle connectors MUST pass the conformance assertion for every event they emit:

import { assertLifecycleConformance } from '@orgloop/sdk';
// In your connector tests:
const events = await source.poll(null);
for (const event of events.events) {
assertLifecycleConformance(event);
}

The assertion validates:

  1. payload.lifecycle shape (phase, terminal, outcome, dedupe_key)
  2. payload.session shape (id, adapter, harness)
  3. Phase/terminal consistency (terminal phases must have terminal: true)
  4. Outcome requirement (terminal events must have an outcome)
  5. Event type consistency (terminal → actor.stopped, non-terminal → resource.changed)
routes:
# Route any harness completion to a review agent
- name: harness-session-review
when:
source: my-agents
events:
- actor.stopped
transforms:
- ref: filter-lifecycle
config:
match:
payload.lifecycle.phase: completed
then:
actor: review-agent
# Route any harness failure to an escalation agent
- name: harness-failure-escalate
when:
source: my-agents
events:
- actor.stopped
transforms:
- ref: filter-lifecycle
config:
match:
payload.lifecycle.outcome: failure
then:
actor: escalation-agent

Codex uses the same exit-status-based lifecycle resolution as Claude Code:

Exit StatusPhaseOutcomeReason
0completedsuccessexit_code_0
1-127failedfailureexit_code_<N>
130 (SIGINT)stoppedcancelledsigint
137 (SIGKILL)stoppedcancelledsigkill
143 (SIGTERM)stoppedcancelledsigterm
128+N (other)stoppedcancelledsignal_<N>

OpenCode uses the same exit-status-based lifecycle resolution as Claude Code:

Exit StatusPhaseOutcomeReason
0completedsuccessexit_code_0
1-127failedfailureexit_code_<N>
130 (SIGINT)stoppedcancelledsigint
137 (SIGKILL)stoppedcancelledsigkill
143 (SIGTERM)stoppedcancelledsigterm
128+N (other)stoppedcancelledsignal_<N>

Pi uses the same exit-status-based lifecycle resolution as Claude Code:

Exit StatusPhaseOutcomeReason
0completedsuccessexit_code_0
1-127failedfailureexit_code_<N>
130 (SIGINT)stoppedcancelledsigint
137 (SIGKILL)stoppedcancelledsigkill
143 (SIGTERM)stoppedcancelledsigterm
128+N (other)stoppedcancelledsignal_<N>

Pi-rust uses the same exit-status-based lifecycle resolution as Claude Code:

Exit StatusPhaseOutcomeReason
0completedsuccessexit_code_0
1-127failedfailureexit_code_<N>
130 (SIGINT)stoppedcancelledsigint
137 (SIGKILL)stoppedcancelledsigkill
143 (SIGTERM)stoppedcancelledsigterm
128+N (other)stoppedcancelledsignal_<N>
ConnectorstartedactivecompletedfailedstoppedConformance
claude-codeYesYesYesYesTested
agent-ctlYesYesYesYesTested
codexYesYesYesYesTested (#69)
opencodeYesYesYesYesTested (#70)
piYesYesYesYesTested (#71)
pi-rustYesYesYesYesTested (#72)