Goal: implement the approved spec at docs/phase-568-slice-3-run-projector.md by adding one pure projector module (src/brigade/run_projector.py), its 20 tests, and two golden fixtures, with zero changes to any existing writer, reader, or CLI path.
Architecture: project_run_snapshot(base_snapshot, events, *, journal_present) re-validates every envelope through run_events.validate_event, re-verifies the chain (contiguous sequence from 1, digest linkage, single run_id), then derives exactly five fields (status, projector_version, journal_present, journal_last_sequence, journal_last_event_digest) while deep-copying the 44 preserved fields verbatim from the base. Output bytes come from the single encoding path (json.dumps(snapshot, indent=2, sort_keys=True) + "\n").encode("utf-8"), matching aboyeur._write_json byte for byte. The module performs no I/O, reads no clock or environment, and nothing in the runtime calls it in this slice.
The spec is the authority. This plan quotes the spec's API and the real source contracts in src/brigade/run_events.py, src/brigade/run_journal.py, src/brigade/run_lifecycle.py, src/brigade/aboyeur.py, src/brigade/run_resume.py, and src/brigade/runguard.py as of the base commit. If any quoted contract disagrees with the checked-out code, stop and re-read the code. Do not improvise.
| Path | Action |
|---|---|
tests/test_run_projector.py |
Create first (RED). 20 test functions, listed below |
src/brigade/run_projector.py |
Create. The entire production surface of this slice |
tests/fixtures/run-lifecycle/golden-projection.base.json |
Create. Hand-authored base snapshot carrying all 44 preserved fields |
tests/fixtures/run-lifecycle/golden-projection.expected.json |
Create. Generated once by the implementation, reviewed by hand before commit |
Do not edit src/brigade/aboyeur.py, src/brigade/run_lifecycle.py, src/brigade/run_journal.py, src/brigade/run_events.py, any CLI module, pyproject.toml, or any other existing file. The existing fixture tests/fixtures/run-lifecycle/golden-lifecycle.jsonl is read, never modified.
- Tests run with
python3 -m pytestfrom the repo root.tests/conftest.pyinsertssrc/onsys.path, so no install step is needed. Ad-hoc scripts outside pytest needPYTHONPATH=src. - Focused runs name the test file:
python3 -m pytest tests/test_run_projector.py -q. - Lint and type gates:
python3 -m ruff check <paths>(line length 120, rules B, E4, E7, E9, F) andpython3 -m mypy src/brigade/run_projector.py(new modules are checked from day one. Only legacy modules carry overrides inpyproject.toml). - Fixtures for this work live under
tests/fixtures/run-lifecycle/, besidegolden-lifecycle.jsonl(6 events, run_id20260727-153045-a1b2c3d4, final eventrun.completedwithpayload.status == "ok", finalevent_digest176259cd00408b637f1c13496fcdfafea7dd96753fb8a9e76dbea58817442fc7).
- Create
tests/test_run_projector.pywith the approved RED coverage:
"""Tests for brigade.run_projector pure run snapshot projector (issue #568 slice 3).
Covers the slice-3 contract: golden replay against the committed lifecycle
journal, determinism, empty-sequence journal presence semantics, the
event-to-status mapping with its payload status rules, closed field ownership
over run.json, fail-closed chain verification, preservation deep-equality,
re-projection idempotence, encoding parity with aboyeur._write_json, and
bounded typed errors for every failure mode.
"""
from __future__ import annotations
import dataclasses
import json
from pathlib import Path
import pytest
from brigade import run_events, run_journal, run_projector
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "run-lifecycle"
GOLDEN_LIFECYCLE_PATH = FIXTURES / "golden-lifecycle.jsonl"
GOLDEN_BASE_PATH = FIXTURES / "golden-projection.base.json"
GOLDEN_EXPECTED_PATH = FIXTURES / "golden-projection.expected.json"
RUN_ID = "20260727-153045-a1b2c3d4"
RECORDED_AT = "2026-07-27T15:30:45.123456Z"
GOLDEN_FINAL_DIGEST = "176259cd00408b637f1c13496fcdfafea7dd96753fb8a9e76dbea58817442fc7"
def _load_golden_base() -> dict:
if not GOLDEN_BASE_PATH.is_file():
pytest.fail(f"missing golden fixture: {GOLDEN_BASE_PATH}")
return json.loads(GOLDEN_BASE_PATH.read_text(encoding="utf-8"))
def _read_golden_events() -> list[run_journal.RunEvent]:
if not GOLDEN_LIFECYCLE_PATH.is_file():
pytest.fail(f"missing golden fixture: {GOLDEN_LIFECYCLE_PATH}")
report = run_journal.read_journal(GOLDEN_LIFECYCLE_PATH)
assert report.partial_tail is None
assert report.chain_errors == []
return report.events
def _minimal_base() -> dict:
return {
"schema": "brigade.run.v1",
"schema_version": 1,
"task": "project the lifecycle journal",
"orchestrator": "chef",
"dry_run": False,
"read_only": False,
"status": "started",
"started_at": "2026-07-27T15:30:45.123456+00:00",
"status_started_at": "2026-07-27T15:30:45.123456+00:00",
"suspected_noop": False,
"code_graph_brief": {"attached": False, "bytes": 0},
"drift_impact_brief": {"attached": False, "bytes": 0, "pending_count": 0},
"evidence_brief": {"attached": False, "bytes": 0},
"brief_budget": {"bytes": 8192, "attached": []},
}
def _build_chain(
steps: list[tuple[str, dict]],
*,
run_id: str = RUN_ID,
start_sequence: int = 1,
previous_digest: str | None = None,
) -> list[dict]:
envelopes = []
sequence = start_sequence
for event_type, payload in steps:
env = run_events.build_event(
run_id=run_id,
sequence=sequence,
event_type=event_type,
payload=payload,
idempotency_key=f"test-{run_id}-{sequence}",
recorded_at=RECORDED_AT,
previous_digest=previous_digest,
)
envelopes.append(env)
previous_digest = env["event_digest"]
sequence += 1
return envelopes
def _failed_chain(status_payload: dict) -> list[dict]:
return _build_chain(
[
("run.created", {"status": "started"}),
("run.failed", status_payload),
]
)
def test_golden_replay_matches_expected_bytes():
if not GOLDEN_EXPECTED_PATH.is_file():
pytest.fail(f"missing golden fixture: {GOLDEN_EXPECTED_PATH}")
projection = run_projector.project_run_snapshot(
_load_golden_base(), _read_golden_events(), journal_present=True
)
assert projection.to_bytes() == GOLDEN_EXPECTED_PATH.read_bytes()
def test_golden_projection_is_byte_deterministic():
base = _load_golden_base()
events = _read_golden_events()
first = run_projector.project_run_snapshot(base, events, journal_present=True)
second = run_projector.project_run_snapshot(base, events, journal_present=True)
assert first.to_bytes() == second.to_bytes()
def test_empty_sequence_without_journal_preserves_base():
base = _minimal_base()
projection = run_projector.project_run_snapshot(base, [], journal_present=False)
assert projection.status == "started"
assert projection.journal_present is False
assert projection.last_sequence == 0
assert projection.last_event_digest is None
snapshot = projection.snapshot
assert snapshot["status"] == "started"
assert snapshot["projector_version"] == 1
assert snapshot["journal_present"] is False
assert snapshot["journal_last_sequence"] == 0
assert snapshot["journal_last_event_digest"] is None
for key, value in base.items():
assert snapshot[key] == value
assert snapshot["code_graph_brief"] is not base["code_graph_brief"]
def test_empty_created_journal_projects_presence_without_facts():
projection = run_projector.project_run_snapshot(_minimal_base(), [], journal_present=True)
assert projection.journal_present is True
assert projection.last_sequence == 0
assert projection.last_event_digest is None
assert projection.status == "started"
assert projection.snapshot["journal_present"] is True
assert projection.snapshot["journal_last_sequence"] == 0
assert projection.snapshot["journal_last_event_digest"] is None
def test_full_golden_sequence_derives_terminal_facts():
projection = run_projector.project_run_snapshot(
_load_golden_base(), _read_golden_events(), journal_present=True
)
assert projection.status == "ok"
assert projection.last_sequence == 6
assert projection.last_event_digest == GOLDEN_FINAL_DIGEST
def test_run_failed_timeout_payload_derives_timeout():
chain = _failed_chain({"status": "timeout", "detail": "bounded"})
projection = run_projector.project_run_snapshot(_minimal_base(), chain, journal_present=True)
assert projection.status == "timeout"
assert projection.last_sequence == 2
def test_run_failed_failed_payload_derives_failed():
chain = _failed_chain({"status": "failed", "detail": "bounded"})
projection = run_projector.project_run_snapshot(_minimal_base(), chain, journal_present=True)
assert projection.status == "failed"
assert projection.last_sequence == 2
@pytest.mark.parametrize("payload", [{"status": "ok"}, {"detail": "bounded"}])
def test_run_failed_bad_or_missing_payload_status_raises(payload):
chain = _failed_chain(payload)
with pytest.raises(run_projector.EventPayloadError) as excinfo:
run_projector.project_run_snapshot(_minimal_base(), chain, journal_present=True)
assert len(excinfo.value.diagnostic) <= run_events.MAX_DIAGNOSTIC_LEN
assert "run.failed" in excinfo.value.diagnostic
def test_run_interrupted_derives_canceled_and_wrong_status_raises():
chain = _build_chain(
[
("run.created", {"status": "started"}),
("run.interrupted", {"status": "canceled"}),
]
)
projection = run_projector.project_run_snapshot(_minimal_base(), chain, journal_present=True)
assert projection.status == "canceled"
bad = _build_chain(
[
("run.created", {"status": "started"}),
("run.interrupted", {"status": "failed"}),
]
)
with pytest.raises(run_projector.EventPayloadError) as excinfo:
run_projector.project_run_snapshot(_minimal_base(), bad, journal_present=True)
assert len(excinfo.value.diagnostic) <= run_events.MAX_DIAGNOSTIC_LEN
def test_unknown_base_key_raises_unknown_snapshot_field():
base = _minimal_base()
base["mystery_key"] = 1
with pytest.raises(run_projector.UnknownSnapshotFieldError) as excinfo:
run_projector.project_run_snapshot(base, [], journal_present=False)
assert "mystery_key" in excinfo.value.diagnostic
assert len(excinfo.value.diagnostic) <= run_events.MAX_DIAGNOSTIC_LEN
def test_registered_unmapped_event_type_raises():
chain = _build_chain(
[
("run.created", {"status": "started"}),
("run.paused", {"approval_id": "ap-1", "reason": "review"}),
]
)
with pytest.raises(run_projector.UnmappedEventTypeError) as excinfo:
run_projector.project_run_snapshot(_minimal_base(), chain, journal_present=True)
assert "run.paused" in excinfo.value.diagnostic
assert len(excinfo.value.diagnostic) <= run_events.MAX_DIAGNOSTIC_LEN
def test_chain_breaks_raise_event_chain_error():
base = _minimal_base()
first = _build_chain([("run.created", {"status": "started"})])
gap = first + _build_chain(
[("run.planning.started", {"detail": "planning"})],
start_sequence=3,
previous_digest=first[0]["event_digest"],
)
with pytest.raises(run_projector.EventChainError):
run_projector.project_run_snapshot(base, gap, journal_present=True)
duplicate = first + first
with pytest.raises(run_projector.EventChainError):
run_projector.project_run_snapshot(base, duplicate, journal_present=True)
broken = first + _build_chain(
[("run.planning.started", {"detail": "planning"})],
start_sequence=2,
previous_digest="0" * 64,
)
with pytest.raises(run_projector.EventChainError):
run_projector.project_run_snapshot(base, broken, journal_present=True)
mixed = first + _build_chain(
[("run.planning.started", {"detail": "planning"})],
run_id="20260727-153045-b2c3d4e5",
start_sequence=2,
previous_digest=first[0]["event_digest"],
)
with pytest.raises(run_projector.EventChainError):
run_projector.project_run_snapshot(base, mixed, journal_present=True)
def test_invalid_envelope_mapping_raises_event_chain_error():
env = dict(_build_chain([("run.created", {"status": "started"})])[0])
env["event_type"] = "run.unknown"
with pytest.raises(run_projector.EventChainError):
run_projector.project_run_snapshot(_minimal_base(), [env], journal_present=True)
def test_invalid_envelope_diagnostic_excludes_rejected_value():
private_marker = "private-marker-568"
env = dict(_build_chain([("run.created", {"status": "started"})])[0])
env["recorded_at"] = private_marker
with pytest.raises(run_projector.EventChainError) as excinfo:
run_projector.project_run_snapshot(_minimal_base(), [env], journal_present=True)
assert private_marker not in excinfo.value.diagnostic
def test_mutated_typed_run_event_raises_event_chain_error():
env = _build_chain([("run.created", {"status": "started"})])[0]
event = run_journal.RunEvent(**env)
mutated = dataclasses.replace(event, payload={"status": "ok"})
with pytest.raises(run_projector.EventChainError):
run_projector.project_run_snapshot(_minimal_base(), [mutated], journal_present=True)
def test_full_coverage_base_preservation_deep_equals():
base = _load_golden_base()
assert run_projector.PRESERVED_FIELDS <= set(base.keys())
projection = run_projector.project_run_snapshot(base, [], journal_present=False)
snapshot = projection.snapshot
for key in run_projector.PRESERVED_FIELDS:
assert snapshot[key] == base[key]
preserved_in_output = set(snapshot.keys()) & run_projector.PRESERVED_FIELDS
assert preserved_in_output == set(base.keys()) & run_projector.PRESERVED_FIELDS
assert snapshot["failure"] is not base["failure"]
assert snapshot["recovery_history"] is not base["recovery_history"]
assert snapshot["active_seats"] is not base["active_seats"]
def test_reprojection_is_idempotent():
events = _read_golden_events()
first = run_projector.project_run_snapshot(_load_golden_base(), events, journal_present=True)
second = run_projector.project_run_snapshot(first.snapshot, events, journal_present=True)
assert first.to_bytes() == second.to_bytes()
def test_to_bytes_matches_run_json_encoding():
projection = run_projector.project_run_snapshot(_minimal_base(), [], journal_present=False)
expected = (json.dumps(projection.snapshot, indent=2, sort_keys=True) + "\n").encode("utf-8")
assert projection.to_bytes() == expected
assert run_projector.encode_snapshot_bytes(projection.snapshot) == expected
def test_unencodable_preserved_value_raises_snapshot_encoding_error():
base = _minimal_base()
base["failure"] = {"phase": "dispatch", "detail": object()}
with pytest.raises(run_projector.SnapshotEncodingError) as excinfo:
run_projector.project_run_snapshot(base, [], journal_present=False)
assert len(excinfo.value.diagnostic) <= run_events.MAX_DIAGNOSTIC_LEN
circular = _minimal_base()
loop: dict = {}
loop["self"] = loop
circular["failure"] = loop
with pytest.raises(run_projector.SnapshotEncodingError) as excinfo:
run_projector.project_run_snapshot(circular, [], journal_present=False)
assert len(excinfo.value.diagnostic) <= run_events.MAX_DIAGNOSTIC_LEN
assert "Circular" not in excinfo.value.diagnostic
@pytest.mark.parametrize("value", ["yes", 1, None])
def test_non_boolean_journal_present_raises_projection_input_error(value):
with pytest.raises(run_projector.ProjectionInputError):
run_projector.project_run_snapshot(_minimal_base(), [], journal_present=value)- Run the RED command and confirm the expected failure:
python3 -m pytest tests/test_run_projector.py -qExpected: collection fails with ModuleNotFoundError: No module named 'brigade.run_projector'. Any other failure means the test file has a syntax or import error. Fix the test file before continuing.
- Create
src/brigade/run_projector.pywith exactly this content:
"""Pure run snapshot projector (issue #568, slice 3).
Derives a complete ``run.json`` snapshot from a base snapshot plus a verified
lifecycle event sequence. The projector owns exactly five derived fields
(``status``, ``projector_version``, ``journal_present``,
``journal_last_sequence``, ``journal_last_event_digest``) and deep-copies the
44 preserved fields of the current ``run.json`` contract verbatim from the
base. Every envelope is re-validated through ``run_events.validate_event``
and the chain is re-verified (contiguous sequence from 1, previous-digest
linkage, single run_id) even when the caller already read the events through
``run_journal.read_journal``. Any deviation fails closed with a bounded typed
error; there is no partial projection.
The projector performs no I/O, reads no clock, reads no environment, and holds
no mutable module state. Nothing in the runtime calls it in this slice.
Standard library only. Brigade is zero-runtime-dependency.
"""
from __future__ import annotations
import json
from collections.abc import Mapping, Sequence
from copy import deepcopy
from dataclasses import dataclass
from typing import Any
from brigade import run_events, run_journal
PROJECTOR_VERSION: int = 1
# Field ownership over the run.json contract. Every current run.json key is
# in exactly one of these two sets; any other base key stops projection.
DERIVED_FIELDS: frozenset[str] = frozenset(
{
"status",
"projector_version",
"journal_present",
"journal_last_sequence",
"journal_last_event_digest",
}
)
PRESERVED_FIELDS: frozenset[str] = frozenset(
{
# Identity and schema
"schema",
"schema_version",
"task",
"orchestrator",
"roster",
"worker",
"scheduler",
# Run configuration
"dry_run",
"read_only",
"cwd",
"lock_workspace",
"codex_transport",
"lifecycle_journal_requested",
# Timing
"started_at",
"status_started_at",
"finished_at",
"duration_seconds",
"resumed_at",
# Recovery
"recovery_history",
"recovery_preserved_artifact",
# Briefs and routing telemetry
"code_graph_brief",
"drift_impact_brief",
"evidence_brief",
"brief_budget",
"route",
"skill_route_policy",
"pre_run_snapshot",
"git",
"code_graph_delta",
"context_eval",
"suspected_noop",
# Control transport
"control_transport",
"control_socket",
# Live progress (record_dispatch_stage, record_result_processing)
"active_stage",
"active_seats",
"phase_owner",
# Outcome and failure
"error",
"failure_phase",
"failure_kind",
"failure",
"transport_warning",
"artifact_collection",
# Artifact references
"artifacts",
"handoff",
}
)
OWNED_FIELDS: frozenset[str] = DERIVED_FIELDS | PRESERVED_FIELDS
# Payload-independent rows of the event-to-status mapping. The remaining four
# mapped event types carry their run.json status in the payload and are
# handled by _EVENT_PAYLOAD_STATUS.
EVENT_STATUS: dict[str, str] = {
"run.planning.started": "planning",
"run.dispatch.requested": "dispatching",
"run.dispatch.completed": "result-processing",
"run.synthesis.started": "synthesizing",
"run.synthesis.completed": "handoff",
}
# Payload-dependent rows: event_type -> allowed payload status values. The
# derived status is the payload value itself after the membership check.
_EVENT_PAYLOAD_STATUS: dict[str, frozenset[str]] = {
"run.created": frozenset({"started"}),
"run.completed": frozenset({"ok"}),
"run.failed": frozenset({"failed", "timeout"}),
"run.interrupted": frozenset({"canceled"}),
}
_ENCODING_CATEGORY = "snapshot cannot be encoded under the run.json JSON contract"
class ProjectionError(RuntimeError):
"""Base class for projection failures. Carries a bounded ``diagnostic``."""
def __init__(self, diagnostic: str) -> None:
super().__init__(diagnostic)
self.diagnostic = diagnostic
class UnknownSnapshotFieldError(ProjectionError):
"""The base snapshot carried a key outside ``OWNED_FIELDS``."""
class UnmappedEventTypeError(ProjectionError):
"""A validated event's type has no status mapping."""
class EventChainError(ProjectionError):
"""Envelope validation or chain verification failed."""
class EventPayloadError(ProjectionError):
"""A payload status rule failed for a mapped event type."""
class ProjectionInputError(ProjectionError):
"""An input argument violated the projector contract."""
class SnapshotEncodingError(ProjectionError):
"""Projected values cannot be encoded by the run.json JSON contract."""
@dataclass(frozen=True)
class RunProjection:
"""Result of a successful projection."""
snapshot: dict[str, Any]
status: str
journal_present: bool
last_sequence: int
last_event_digest: str | None
def to_bytes(self) -> bytes:
return encode_snapshot_bytes(self.snapshot)
def encode_snapshot_bytes(snapshot: Mapping[str, Any]) -> bytes:
"""Encode a snapshot with the existing run.json byte contract.
Sorted keys, two-space indent, trailing newline, UTF-8, matching
``aboyeur._write_json`` byte for byte. Encoding failures (non-JSON values,
circular references, recursion or overflow) surface as a bounded
category-only SnapshotEncodingError, never as a raw serializer exception.
"""
try:
return (json.dumps(snapshot, indent=2, sort_keys=True) + "\n").encode("utf-8")
except (TypeError, ValueError, OverflowError, RecursionError) as exc:
raise SnapshotEncodingError(_ENCODING_CATEGORY) from exc
def _validated_envelopes(
events: Sequence[run_journal.RunEvent | Mapping[str, Any]],
) -> list[Mapping[str, Any]]:
"""Normalize every item to an envelope mapping and re-validate it."""
if isinstance(events, (str, bytes)) or not isinstance(events, Sequence):
raise ProjectionInputError("events must be a sequence of envelopes")
envelopes: list[Mapping[str, Any]] = []
for index, item in enumerate(events, start=1):
env = item.to_dict() if isinstance(item, run_journal.RunEvent) else item
errors = run_events.validate_event(env)
if errors:
raise EventChainError(run_events._bound(f"event envelope invalid at sequence {index}"))
envelopes.append(env)
return envelopes
def _event_status(env: Mapping[str, Any]) -> str:
"""Map one validated envelope to its run.json status, enforcing payload rules."""
event_type = env["event_type"]
static = EVENT_STATUS.get(event_type)
if static is not None:
return static
expected = _EVENT_PAYLOAD_STATUS.get(event_type)
if expected is None:
raise UnmappedEventTypeError(
run_events._bound(f"event type {event_type} has no status mapping at sequence {env['sequence']}")
)
payload = env["payload"]
value = payload.get("status")
if not isinstance(value, str) or value not in expected:
wanted = ", ".join(sorted(expected))
raise EventPayloadError(
run_events._bound(f"event type {event_type} requires payload status in {{{wanted}}}")
)
return value
def project_run_snapshot(
base_snapshot: Mapping[str, Any],
events: Sequence[run_journal.RunEvent | Mapping[str, Any]],
*,
journal_present: bool,
) -> RunProjection:
"""Project a complete run.json snapshot from a base plus a verified event sequence.
Fail closed: unknown base keys, invalid envelopes, chain breaks, mixed
run_id values, unmapped event types, payload rule violations, and
unencodable preserved values each raise a typed ProjectionError subclass
with a bounded diagnostic. An empty event sequence is valid and preserves
status from the base.
"""
if not isinstance(journal_present, bool):
raise ProjectionInputError("journal_present must be a real boolean")
if not isinstance(base_snapshot, Mapping):
raise ProjectionInputError("base snapshot must be a mapping")
unknown = sorted(set(base_snapshot.keys()) - OWNED_FIELDS)
if unknown:
raise UnknownSnapshotFieldError(
run_events._bound("base snapshot carries unmapped fields: " + ", ".join(unknown[:8]))
)
envelopes = _validated_envelopes(events)
last_sequence = 0
last_event_digest: str | None = None
run_id: str | None = None
status: str | None = None
for env in envelopes:
sequence = env["sequence"]
if sequence != last_sequence + 1:
raise EventChainError(run_events._bound(f"event chain sequence break at sequence {sequence}"))
previous_digest = env["previous_digest"]
if sequence == 1:
if previous_digest is not None:
raise EventChainError("event chain sequence 1 previous_digest must be null")
elif previous_digest != last_event_digest:
raise EventChainError(run_events._bound(f"event chain digest link break at sequence {sequence}"))
if run_id is None:
run_id = env["run_id"]
elif env["run_id"] != run_id:
raise EventChainError(run_events._bound(f"event chain mixes run_id at sequence {sequence}"))
status = _event_status(env)
last_sequence = sequence
last_event_digest = env["event_digest"]
if status is None:
base_status = base_snapshot.get("status")
if not isinstance(base_status, str):
raise ProjectionInputError("base snapshot must carry a string status when events is empty")
status = base_status
snapshot: dict[str, Any] = {}
for key in PRESERVED_FIELDS:
if key in base_snapshot:
snapshot[key] = deepcopy(base_snapshot[key])
snapshot["status"] = status
snapshot["projector_version"] = PROJECTOR_VERSION
snapshot["journal_present"] = journal_present
snapshot["journal_last_sequence"] = last_sequence
snapshot["journal_last_event_digest"] = last_event_digest
# Fail closed at projection time on preserved values the run.json JSON
# contract cannot encode; to_bytes() re-encodes deterministically.
encode_snapshot_bytes(snapshot)
return RunProjection(
snapshot=snapshot,
status=status,
journal_present=journal_present,
last_sequence=last_sequence,
last_event_digest=last_event_digest,
)
__all__ = [
"DERIVED_FIELDS",
"EVENT_STATUS",
"OWNED_FIELDS",
"PRESERVED_FIELDS",
"PROJECTOR_VERSION",
"EventChainError",
"EventPayloadError",
"ProjectionError",
"ProjectionInputError",
"RunProjection",
"SnapshotEncodingError",
"UnknownSnapshotFieldError",
"UnmappedEventTypeError",
"encode_snapshot_bytes",
"project_run_snapshot",
]Implementation notes that resolve the spec's implicit corners (these are decisions, not open questions):
-
The empty-sequence rule preserves
statusfrom the base. Because the invariants require all five derived fields in every output and every failure to be aProjectionErrorsubclass, a base without a stringstatuson an empty sequence raisesProjectionInputError(category-only diagnostic). Every real base fromaboyeur._run_payloadcarries a string status, so this only fires on synthetic input. -
Every event in the sequence must map to a status. The mapping walk computes each event's status (raising
UnmappedEventTypeErrororEventPayloadErroron the offending event) and keeps the final one, matching the spec's "nothing is skipped silently" rule. -
Diagnostics follow the spec's bounded-error table: categories plus field names, event types, sequences, and expected value sets only. Raw payload values, envelope bytes, paths, and snapshot contents never appear. Bounding reuses
run_events._bound(240 chars, ellipsis), the same cross-module reuserun_lifecycle.pyalready makes. -
Run the focused suite and confirm the expected intermediate state:
python3 -m pytest tests/test_run_projector.py -qObserved: 22 passed, 1 failed. The only failure was Failed: missing golden base fixture: .../golden-projection.base.json from the golden replay test. Any other failure is a real bug in the module.
- Create
tests/fixtures/run-lifecycle/golden-projection.base.jsonwith exactly this content (all 44 preserved fields, nested objects, lists, and optional fields, plus the derivedstatuskey the live writer always carries):
{
"schema": "brigade.run.v1",
"schema_version": 1,
"task": "Project the lifecycle journal",
"orchestrator": "chef",
"roster": {
"path": ".brigade/roster.toml",
"source": "project",
"shadowed": []
},
"worker": "coder",
"scheduler": {
"requested": "auto",
"used": "serial"
},
"dry_run": false,
"read_only": false,
"cwd": "/work/repo",
"lock_workspace": "/work/repo",
"codex_transport": "app-server",
"lifecycle_journal_requested": true,
"started_at": "2026-07-27T15:30:45.123456+00:00",
"status_started_at": "2026-07-27T15:30:49.000000+00:00",
"finished_at": "2026-07-27T15:30:50.000000+00:00",
"duration_seconds": 4.877,
"resumed_at": [
"2026-07-27T15:31:00.000000+00:00",
"2026-07-27T15:32:00.000000+00:00"
],
"recovery_history": [
{
"kind": "worker-error",
"at": "2026-07-27T15:31:30.000000+00:00",
"seat": "coder"
}
],
"recovery_preserved_artifact": "/work/repo/.brigade/runs/20260727-153045-a1b2c3d4/run.json.corrupt",
"code_graph_brief": {
"attached": true,
"bytes": 2048
},
"drift_impact_brief": {
"attached": false,
"bytes": 0,
"pending_count": 0
},
"evidence_brief": {
"attached": true,
"bytes": 512
},
"brief_budget": {
"bytes": 8192,
"attached": [
"code-graph"
]
},
"route": {
"skill": "brigade-work",
"matched": [
"run"
],
"score": 3
},
"skill_route_policy": {
"policy_applied": true,
"extensions": [
"force-verify"
]
},
"pre_run_snapshot": {
"head": "abc123",
"dirty": false
},
"git": {
"head": "abc123",
"branch": "main",
"dirty": false
},
"code_graph_delta": {
"summary": "no drift",
"changed": 0
},
"context_eval": {
"verdict": "pass",
"score": 0.9
},
"suspected_noop": false,
"control_transport": {
"kind": "unix",
"path": "/run/brigade/ctl.sock"
},
"control_socket": "/run/brigade/ctl.sock",
"active_stage": 2,
"active_seats": [
"coder",
"reviewer"
],
"phase_owner": "chef",
"error": null,
"failure_phase": null,
"failure_kind": null,
"failure": {
"phase": "dispatch",
"kind": "worker-error",
"detail": "bounded detail",
"seat": "coder"
},
"transport_warning": {
"kind": "retry",
"attempts": 1
},
"artifact_collection": {
"status": "ok"
},
"artifacts": "/work/repo/.brigade/runs/20260727-153045-a1b2c3d4",
"handoff": "/work/repo/.brigade/runs/20260727-153045-a1b2c3d4/handoff.md",
"status": "handoff"
}- Confirm the fixture carries every preserved field and nothing outside the ownership map:
PYTHONPATH=src python3 - <<'PY'
import json
from pathlib import Path
from brigade import run_projector
base = json.loads(Path("tests/fixtures/run-lifecycle/golden-projection.base.json").read_text(encoding="utf-8"))
missing = run_projector.PRESERVED_FIELDS - set(base.keys())
extra = set(base.keys()) - run_projector.OWNED_FIELDS
assert not missing, f"fixture missing preserved fields: {sorted(missing)}"
assert not extra, f"fixture carries unmapped fields: {sorted(extra)}"
print(f"base fixture covers all {len(run_projector.PRESERVED_FIELDS)} preserved fields")
PYExpected output: base fixture covers all 44 preserved fields.
- Run the focused suite and confirm the expected intermediate state:
python3 -m pytest tests/test_run_projector.py -qObserved: 22 passed, 1 failed. The one failure was Failed: missing golden expected fixture: .../golden-projection.expected.json from test_golden_replay_matches_expected_bytes.
The convention from golden-lifecycle.jsonl applies: the implementation generates the expected bytes once, a human reviews them, and only then are they committed.
- Generate
tests/fixtures/run-lifecycle/golden-projection.expected.json:
PYTHONPATH=src python3 - <<'PY'
import json
from pathlib import Path
from brigade import run_journal, run_projector
fixtures = Path("tests/fixtures/run-lifecycle")
base = json.loads((fixtures / "golden-projection.base.json").read_text(encoding="utf-8"))
report = run_journal.read_journal(fixtures / "golden-lifecycle.jsonl")
assert report.partial_tail is None and report.chain_errors == []
projection = run_projector.project_run_snapshot(base, report.events, journal_present=True)
(fixtures / "golden-projection.expected.json").write_bytes(projection.to_bytes())
print("wrote", fixtures / "golden-projection.expected.json")
PY- Hand-review the generated bytes against this checklist. The review is the point of the golden convention, so do not skip it:
PYTHONPATH=src python3 - <<'PY'
import json
from pathlib import Path
raw = Path("tests/fixtures/run-lifecycle/golden-projection.expected.json").read_bytes()
assert raw.endswith(b"\n") and not raw.endswith(b"\n\n"), "exactly one trailing newline"
data = json.loads(raw)
assert len(data) == 49, f"expected 49 owned keys, got {len(data)}"
assert list(data) == sorted(data), "keys must be sorted"
assert data["status"] == "ok"
assert data["projector_version"] == 1
assert data["journal_present"] is True
assert data["journal_last_sequence"] == 6
assert data["journal_last_event_digest"] == "176259cd00408b637f1c13496fcdfafea7dd96753fb8a9e76dbea58817442fc7"
base = json.loads(Path("tests/fixtures/run-lifecycle/golden-projection.base.json").read_text(encoding="utf-8"))
for key, value in base.items():
if key != "status":
assert data[key] == value, f"preserved field drifted: {key}"
print("golden expected fixture reviewed ok")
PYAlso read the file top to bottom and confirm by eye: two-space indent, preserved nested values (failure, code_graph_brief, control_transport, active_seats) match the base verbatim, and no key outside the 49 owned fields appears. Expected output: golden expected fixture reviewed ok.
- Run the focused suite to green:
python3 -m pytest tests/test_run_projector.py -qObserved: 23 passed (20 test functions, one parametrized over four cases).
- Focused suite plus the neighboring journal and lifecycle suites:
python3 -m pytest tests/test_run_projector.py tests/test_run_events.py tests/test_run_journal.py tests/test_run_lifecycle.py -qObserved: 132 passed.
- Full repository suite:
python3 -m pytest -qObserved through ./scripts/verify: 4864 passed, 3 skipped.
- Lint and type gates:
python3 -m ruff check src/brigade/run_projector.py tests/test_run_projector.py
python3 -m mypy src/brigade/run_projector.pyObserved through ./scripts/verify: ruff lint and format checks passed, and mypy reported Success: no issues found in 331 source files.
- Diff review: confirm the slice adds only its module, tests, fixtures, specification, and plan:
git status --porcelainObserved: exactly six new paths, the four implementation paths above plus this plan and docs/phase-568-slice-3-run-projector.md. No pre-existing path is modified relative to the slice branch point.
- Commit each completed task checkpoint with a conventional message.
The slice is split into specification, contract-test, projector, and fixture commits. This is an in-house repo, so each implementation commit carries a Co-authored-by trailer only for a tool that did substantial work on that commit.
Per the mandatory Brigade work loop and Definition of Done in AGENTS.md,
the final result must run the complete repository gate through Brigade:
brigade work verify run --target . --command "./scripts/verify" --capture brigade-workExpected: the receipt records ruff lint, ruff format check, version sync,
mypy, and the full pytest suite with its coverage floor. Atomic
--capture brigade-work records the outcome once. Do not run a second
brigade outcome capture for the same receipt.
- Receipt
20260729-024813-work-verify-0a6ae6completed after the review fix: ruff lint and format passed, version0.25.1was synchronized across 13 locations, mypy passed 331 source files, and pytest finished with 4864 passed, 3 skipped, and 82.95% coverage.