Version: 0.1
Status: Normative
Package: @peac/cli, @peac/schema
Extension URI: org.peacprotocol/lifecycle-observation
Record Type URIs: 9 (one per event kind; see §3)
Depends on: Evidence Carrier Contract (DD-124), Wire 0.2 Interaction Record (interaction-record+jwt), Opaque-Reference Schema (@peac/schema)
This document specifies how PEAC records observations of lifecycle events emitted by external systems (orchestrators, workflow engines, evaluation systems, approval systems, agent runtimes). The caller observed the event; the CLI issues a record using the caller-provided issuer key. The caller's issuer is the signer-of-record. PEAC provides the record format, validation, and signing path. PEAC does not capture, observe, decide, evaluate, score, transition, or vouch for the truth of the lifecycle event.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals, as shown here.
PEAC lifecycle observation records are an EXPORT-ONLY surface. The caller is the observer of the event; the CLI is the issuance path. PEAC does not run the workflow, score the evaluation, run the experiment, schedule the work, or assign the approval. The wrapper MUST NOT fetch upstream artifacts, validate orchestrator state, or compute any decision derived from the recorded event.
Lifecycle observation records are NOT:
- a workflow engine
- an orchestrator
- an approval-routing system
- an evaluation harness
- an experimentation platform
- a step scheduler
- a policy decision point
- a runtime supervisor
- a verifier of the event's truth
Operators retain all responsibility for the workflow itself, the approval decision, the evaluation outcome, the experiment configuration, and the truth of every recorded fact.
Builds a lifecycle observation record from caller-supplied flags and emits a Wire 0.2 compact JWS (typ: interaction-record+jwt). The CLI issues the record using the caller-provided issuer key; the caller's issuer is the signer-of-record. PEAC provides the record format, validation, and signing path.
The Wire 0.2 envelope MUST carry:
payload.issset from--issuer-idpayload.kind = "evidence"payload.typeequal to one of the 9 lifecycle observation type URIs (see §3)payload.extensions["org.peacprotocol/lifecycle-observation"]containing the observation object defined in §5
A lifecycle observation record MUST carry exactly one of the following type URIs:
| Type URI | event_kind discriminator |
Required additional fields |
|---|---|---|
org.peacprotocol/lifecycle-approval-requested |
lifecycle-approval-requested |
approval_ref, approver_ref |
org.peacprotocol/lifecycle-approval-granted |
lifecycle-approval-granted |
approval_ref, approver_ref |
org.peacprotocol/lifecycle-approval-denied |
lifecycle-approval-denied |
approval_ref, approver_ref |
org.peacprotocol/lifecycle-evaluation-started |
lifecycle-evaluation-started |
(none) |
org.peacprotocol/lifecycle-evaluation-completed |
lifecycle-evaluation-completed |
result_ref |
org.peacprotocol/lifecycle-experiment-assigned |
lifecycle-experiment-assigned |
experiment_ref |
org.peacprotocol/lifecycle-experiment-result |
lifecycle-experiment-result |
experiment_ref, result_ref |
org.peacprotocol/lifecycle-workflow-transition |
lifecycle-workflow-transition |
from_state, to_state |
org.peacprotocol/lifecycle-mode-observed |
lifecycle-mode-observed |
observed_mode |
The discriminator event_kind value equals the type URI with the org.peacprotocol/ prefix removed.
The CLI MUST issue records using the caller-provided issuer key. The caller's issuer is the signer-of-record. PEAC does not vouch for the truth of the lifecycle event. A lifecycle-approval-granted record does not imply PEAC granted; it implies an external system reported that an external approver granted, and the caller-provided issuer signed a record of that report.
The signing flow MUST follow the canonical PEAC issuer-key convention:
--issuer-key env:VAR_NAME— load Ed25519 JWK from the named environment variable--issuer-key file:/path/to/jwk.json— load Ed25519 JWK from the named file--issuer-id <url>— REQUIRED canonical issuer URL recorded aspayload.iss--unsafe-ephemeral-key— generate an ephemeral local signing key whose public key is not published through normal issuer-key discovery; for local development and tests only
The observation object is the value of payload.extensions["org.peacprotocol/lifecycle-observation"]. It MUST validate against LifecycleObservationSchema from @peac/schema.
Every event kind carries:
| Field | Type | Requirement | Notes |
|---|---|---|---|
event_kind |
enum | REQUIRED | One of the 9 discriminator values in §3 |
subject_ref |
OpaqueRef | REQUIRED | Opaque reference to the subject of the observation (task / approval / evaluation / experiment / etc.) |
observed_at |
RFC 3339 string | REQUIRED | The wall-clock time at which the EXTERNAL system observed the event; never the wrapper's invocation time |
parent_ref |
OpaqueRef | OPTIONAL | Opaque reference to a parent observation |
upstream_artifact_ref |
OpaqueRef | OPTIONAL | Opaque reference to an upstream artifact (raw event blob, source receipt, etc.) |
upstream_artifact_digest |
sha256 string | OPTIONAL | Canonical digest of the upstream artifact |
policy_ref |
OpaqueRef | OPTIONAL | Opaque reference to a policy document the upstream system applied |
policy_digest |
sha256 string | OPTIONAL | Canonical digest of the policy document |
rubric_ref |
OpaqueRef | OPTIONAL | Opaque reference to an evaluation rubric or scoring criteria document |
score_ref |
OpaqueRef | OPTIONAL | Opaque reference to a stored score artifact (score values are NEVER inlined; see §6) |
result_digest |
sha256 string | OPTIONAL | Canonical digest of the result artifact |
event_kind |
Additional REQUIRED fields |
|---|---|
lifecycle-approval-requested |
approval_ref (OpaqueRef), approver_ref (OpaqueRef) |
lifecycle-approval-granted |
approval_ref, approver_ref |
lifecycle-approval-denied |
approval_ref, approver_ref |
lifecycle-evaluation-started |
(none) |
lifecycle-evaluation-completed |
result_ref (OpaqueRef) |
lifecycle-experiment-assigned |
experiment_ref (OpaqueRef); cohort_ref/variant_ref OPTIONAL |
lifecycle-experiment-result |
experiment_ref, result_ref; cohort_ref/variant_ref OPTIONAL |
lifecycle-workflow-transition |
from_state (string, 1..128 chars), to_state (same) |
lifecycle-mode-observed |
observed_mode (enum: deterministic_script / templated_flow / agent_loop / human_step / hybrid) |
observed_mode MAY appear OPTIONALLY on any event kind. On lifecycle-mode-observed it is REQUIRED.
Every *_ref field on a lifecycle observation record MUST validate against the canonical PEAC OpaqueRefSchema grammar (packages/schema/src/opaque-ref.ts):
- string, max 256 UTF-8 bytes
- MUST NOT contain whitespace
- MUST NOT contain
@ - MUST NOT begin with a JSON-structural character (
{,[,") - MUST start with one of the recognized prefixes:
ref:,urn:,did:,sha256:,peac:,https:// sha256:requires exactly 64 lowercase hex charactershttps://requires a non-empty path/host suffix
Integrators MUST treat *_ref values as opaque pointers. PEAC does NOT dereference, fetch, or validate the resolved content. Downstream verifiers MAY dereference at their own discretion.
The lifecycle-observation extension is observational; it MUST NOT carry inlined verdicts, scores, decisions, or judgment values at the extension top level. Validators MUST REJECT any of the 20 forbidden top-level keys with the stable error code lifecycle.inline_value_blocked:
decision, verdict, score, result, passed, failed,
policy_result, approval_result, outcome, judgment,
rating, grade, pass, fail, allow, deny,
authorized, denied, granted, rejected_reason
This rule is grammar-based, not heuristic-based. The list is a closed enum; changes to this list require an explicit schema and conformance update.
The event_kind enum value 'lifecycle-approval-granted' is REQUIRED on the event_kind field. An extension top-level field literally named granted: true is FORBIDDEN. The no-inline-value check inspects the extension top level only, never the event_kind field's enum-literal value.
| Trigger | Stable error code |
|---|---|
| Forbidden top-level key present at extension top level | lifecycle.inline_value_blocked |
Conditional-required field absent (per event_kind); includes missing subject_ref / observed_at |
lifecycle.missing_required_field |
event_kind value is not in the 9-literal enum |
lifecycle.event_kind_unknown |
observed_at present but is not a valid RFC 3339 timestamp |
lifecycle.invalid_observed_at |
*_ref value is non-string (number, object, array, boolean, null) |
lifecycle.ref_must_be_string |
approver_ref value is a string containing @ |
lifecycle.approver_ref_pii_blocked |
*_ref value is a string that fails the opaque-reference grammar (whitespace, leading JSON char, no recognized prefix, bad sha256 length, empty https suffix; numeric strings like "0.92" reject here) |
lifecycle.opaque_ref_grammar_violation |
The approver_ref priority order is: non-string → ref_must_be_string; contains @ → approver_ref_pii_blocked (more specific subclass; checked before general grammar); otherwise → opaque_ref_grammar_violation. No generic Zod string error MAY leak as a public diagnostic.
Numeric-string *_ref values reject through the opaque-reference grammar (no recognized prefix), NOT through the inline-value-blocked path. There are NO numeric-specific or language-specific heuristics.
peac.record.ref (dotted form) is a PEAC custom attribute name. The underscore form peac.record_ref MUST NOT appear in normative spec text. The legacy attribute name peac.receipt_ref is permitted only where existing receipt/header terminology is frozen (the v0.11.x receipt_url / PEAC-Receipt header surfaces); new emitters MUST use peac.record.ref.
PEAC does not claim ownership over OpenTelemetry semantic-convention namespaces. OpenTelemetry's GenAI and MCP semantic conventions are owned by the OpenTelemetry Specification authors; peac.record.ref is not an OpenTelemetry semantic convention unless and until OpenTelemetry adopts it.
PEAC ships no OpenTelemetry SDK dependency, exporter, collector, or semantic-convention package.
Implementations can emit peac.record.ref as an OpenTelemetry span attribute alongside other PEAC and OTel attributes when useful, while preserving applicable OTel semantic conventions for the underlying span. This section uses no RFC-style normative words; the normative behavior lives in §7.1.
PEAC may record lifecycle transitions emitted by orchestrators, workflow engines, evaluation systems, approval systems, or agent runtimes. PEAC does not assign work, run agents, schedule tasks, manage issue trackers, route approvals, decide step ordering, or enforce workflow policy. The orchestrator (or workflow engine, evaluation system, approval system, or agent runtime) is the subject of the observation, never an output of PEAC.
Specific external projects MAY appear in informative material accompanying this profile, never in normative spec text.
Conformance vectors live at specs/conformance/parity-corpus/lifecycle-observation/vectors.json. The corpus contains 11 vectors: 9 positive (one per event_kind) plus 2 negative (lo-neg-001-inline-score-blocked, lo-neg-002-numeric-result-ref-grammar-violation). Every positive vector is internally valid against LifecycleObservationSchema.
Section 30 conformance requirements (LIFE-OBS-001..010) live at specs/conformance/requirement-ids.json. All requirements have enforcement_class: hard_fail.
{
"event_kind": "lifecycle-approval-granted",
"subject_ref": "urn:peac:task:approval-002",
"observed_at": "2026-05-12T10:05:00Z",
"approval_ref": "urn:peac:approval:req-001",
"approver_ref": "did:example:approver-002",
"policy_ref": "urn:peac:policy:approvals-v1"
}{
"event_kind": "lifecycle-evaluation-completed",
"subject_ref": "urn:peac:eval:run-004",
"observed_at": "2026-05-12T11:10:00Z",
"result_ref": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"result_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111"
}{
"event_kind": "lifecycle-workflow-transition",
"subject_ref": "urn:peac:task:wf-008",
"observed_at": "2026-05-12T13:00:00Z",
"from_state": "pending",
"to_state": "running"
}{
"event_kind": "lifecycle-mode-observed",
"subject_ref": "urn:peac:run:mode-009",
"observed_at": "2026-05-12T14:00:00Z",
"observed_mode": "agent_loop"
}