---
id: OBPI-0.13.0-02-persist-stage-state
parent: ADR-0.13.0-obpi-pipeline-runtime-surface
item: 2
lane: Heavy
status: Completed
---

# OBPI-0.13.0-02-persist-stage-state: Persist Pipeline Stage State

## ADR Item

- **Source ADR:** `docs/design/adr/pre-release/ADR-0.13.0-obpi-pipeline-runtime-surface/ADR-0.13.0-obpi-pipeline-runtime-surface.md`
- **Checklist Item:** #2 - "OBPI-0.13.0-02: Persist pipeline stage state in a repository-local, machine-readable form"

**Status:** Completed

## Objective

<!-- One-sentence concrete outcome. What does "done" look like? -->

Persist pipeline stage state in a repository-local, machine-readable form.

## Lane

**Heavy** - This OBPI changes a command/API/schema/runtime contract surface.

> Heavy is reserved for command/API/schema/runtime-contract changes. Process,
> documentation, and template-only work stays Lite unless it changes one of
> those external surfaces.

## Allowed Paths

<!-- What files/directories are IN SCOPE? Be explicit with paths. -->

- `src/gzkit/cli.py` - active marker payload, stage-state writer, and stage-entry metadata
- `tests/commands/test_obpi_pipeline.py` - runtime contract coverage for marker payload shape
- `tests/test_hooks.py` - compatibility checks for hook consumers reading richer markers
- `docs/user/commands/obpi-pipeline.md` - operator-facing command contract for marker state
- `docs/governance/GovZero/obpi-runtime-contract.md` - canonical machine-readable state contract
- `.gzkit/skills/gz-obpi-pipeline/SKILL.md` - wrapper skill alignment to the richer marker payload
- `.agents/skills/gz-obpi-pipeline/SKILL.md` - mirrored skill generated by control-surface sync
- `.claude/skills/gz-obpi-pipeline/SKILL.md` - mirrored skill generated by control-surface sync
- `.github/skills/gz-obpi-pipeline/SKILL.md` - mirrored skill generated by control-surface sync
- `.claude/hooks/**` - regenerated hook wrappers and README from control-surface sync
- `.claude/settings.json` - regenerated Claude hook registration surface
- `.github/copilot/hooks/ledger-writer.py` - regenerated Copilot governance hook mirror
- `.github/discovery-index.json` - regenerated discovery surface
- `.gzkit/manifest.json` - regenerated control-surface manifest metadata
- `.copilotignore` - regenerated Copilot control surface
- `AGENTS.md` - generated control surface synced from canonical skills/manifest
- `CLAUDE.md` - generated control surface synced from canonical skills/manifest
- `.github/copilot-instructions.md` - generated control surface synced from canonical skills/manifest
- `docs/design/adr/pre-release/ADR-0.13.0-obpi-pipeline-runtime-surface/ADR-0.13.0-obpi-pipeline-runtime-surface.md` - parent ADR checklist synchronization during completion accounting
- `docs/design/adr/pre-release/ADR-0.13.0-obpi-pipeline-runtime-surface/obpis/OBPI-0.13.0-02-persist-stage-state.md` - this brief and completion evidence

## Denied Paths

<!-- What files/directories are OUT OF SCOPE? Agents will not touch these. -->

- `src/gzkit/hooks/**` - hook rewiring belongs to later runtime convergence work
- manual behavioral edits to `.claude/hooks/**` outside `gz agent sync control-surfaces`
- new repo-local state files outside `.claude/plans/.pipeline-active*.json`
- structured blocker/next-command/human-action runtime outputs - deferred to `OBPI-0.13.0-03`
- new CLI flags, abort/resume subcommands, or JSON stdout contracts
- New dependencies
- CI files, lockfiles

## Requirements (FAIL-CLOSED)

<!-- Constraints that MUST hold. Numbered list. NEVER/ALWAYS language.
     These are the rules agents ground against. If not met, OBPI fails. -->

1. REQUIREMENT: Stage state MUST persist in the existing `.claude/plans/.pipeline-active-{OBPI-ID}.json` and `.claude/plans/.pipeline-active.json` files rather than in a new store.
1. REQUIREMENT: The active-state payload MUST include `obpi_id`, `parent_adr`, `lane`, `entry`, `execution_mode`, `current_stage`, `started_at`, `updated_at`, and `receipt_state`.
1. REQUIREMENT: Full launch MUST persist `entry=full` and `current_stage=implement`; `--from=verify` MUST persist `entry=verify` and `current_stage=verify`; `--from=ceremony` MUST persist `entry=ceremony` and `current_stage=ceremony`.
1. REQUIREMENT: The per-OBPI and legacy marker files MUST carry the same JSON payload while the pipeline is active.
1. REQUIREMENT: Existing consumers that only read `obpi_id` from the marker payload MUST remain compatible with the richer state document.
1. NEVER: This OBPI MUST NOT add a second durable state file, structured blocker payloads, next-command fields, required-human-action fields, or new command-line surface area.
1. ALWAYS: The marker payload contract MUST be documented as active pipeline state only, not as authoritative completion state.

> STOP-on-BLOCKERS: if prerequisites are missing, print a BLOCKERS list and halt.

## Discovery Checklist

<!-- What to read before implementation. Complete this checklist first. -->

**Governance (read once, cache):**

- [x] `.github/discovery-index.json` - repo structure
- [x] `AGENTS.md` or `CLAUDE.md` - agent operating contract
- [x] Parent ADR - understand full context

**Context:**

- [x] Parent ADR: `docs/design/adr/pre-release/ADR-0.13.0-obpi-pipeline-runtime-surface/ADR-0.13.0-obpi-pipeline-runtime-surface.md`
- [x] Related OBPIs in same ADR, especially `OBPI-0.13.0-01` and `OBPI-0.13.0-03`

**Prerequisites (check existence, STOP if missing):**

- [x] Existing `obpi pipeline` parser and marker helpers exist in `src/gzkit/cli.py`
- [x] Existing marker consumers exist under `tests/test_hooks.py`

**Existing Code (understand current state):**

- [x] Pattern to follow: `OBPI-0.13.0-01` runtime contract implementation in `src/gzkit/cli.py`
- [x] Test patterns: `tests/commands/test_obpi_pipeline.py`, `tests/test_hooks.py`

## Quality Gates

<!-- Which gates apply and how to verify them. -->

### Gate 1: ADR

- [x] Intent and scope recorded in this OBPI brief
- [x] Parent ADR checklist item quoted

### Gate 2: TDD

- [x] Tests written before/with implementation
- [x] Tests pass: `uv run gz test`
- [x] Validation commands recorded in evidence with real outputs

### Code Quality

- [x] Lint clean: `uv run gz lint`
- [x] Type check clean: `uv run gz typecheck`

<!-- Heavy lane only: -->
### Gate 3: Docs (Heavy only)

- [x] Docs build: `uv run mkdocs build --strict`
- [x] Relevant docs updated

### Gate 4: BDD (Heavy only)

- [x] Acceptance scenarios pass: `uv run -m behave features/`

### Gate 5: Human (Heavy only)

- [x] Human attestation recorded

## Verification

<!-- What commands verify this work? Use real repo commands, then paste the
     outputs into Evidence. -->

```bash
uv run python -m unittest tests.commands.test_obpi_pipeline -v
uv run python -m unittest tests.test_hooks -v
uv run gz validate --documents
uv run gz lint
uv run gz typecheck
uv run gz test
uv run mkdocs build --strict
uv run -m behave features/
```

## Acceptance Criteria

<!--
Specific, testable criteria for completion.
Each checkbox MUST carry a deterministic REQ ID:
REQ-<semver>-<obpi_item>-<criterion_index>
-->

- [x] REQ-0.13.0-02-01: `gz obpi pipeline <OBPI-ID>` writes enriched per-OBPI and legacy marker files with `entry=full`, `current_stage=implement`, and the parent/lane/receipt metadata.
- [x] REQ-0.13.0-02-02: `gz obpi pipeline <OBPI-ID> --from=verify` and `--from=ceremony` rewrite the active marker payload with the stage-specific `entry` and `current_stage` values before cleanup.
- [x] REQ-0.13.0-02-03: Existing hook consumers continue to accept the richer marker payload without behavior regressions when they only depend on `obpi_id`.
- [x] REQ-0.13.0-02-04: [doc] User and governance docs describe the richer marker payload as active pipeline state and explicitly defer structured outputs to `OBPI-0.13.0-03`.

## Completion Checklist

<!-- Verify all gates before marking OBPI accepted. -->

- [x] **Gate 1 (ADR):** Intent recorded in brief
- [x] **Gate 2 (TDD):** Tests pass, coverage maintained
- [x] **Code Quality:** Lint, format, type checks clean
- [x] **Value Narrative:** Problem-before vs capability-now is documented
- [x] **Key Proof:** One concrete usage example is included
- [x] **OBPI Acceptance:** Evidence recorded below

> For ceremony steps and lane-inheritance attestation rules, see `AGENTS.md` section `OBPI Acceptance Protocol`.

## Evidence

<!-- Record observations during/after implementation.
     Command outputs, file:line references, dates. -->

### Gate 1 (ADR)

- [x] Intent and scope recorded

### Gate 2 (TDD)

```text
$ uv run python -m unittest tests.commands.test_obpi_pipeline -v
Ran 7 tests in 0.227s
OK

$ uv run python -m unittest tests.test_hooks -v
Ran 45 tests in 0.639s
OK

$ uv run gz obpi pipeline OBPI-0.13.0-02-persist-stage-state --from=verify
PASS uv run python -m unittest tests.commands.test_obpi_pipeline -v
PASS uv run python -m unittest tests.test_hooks -v
PASS uv run gz validate --documents
PASS uv run gz lint
PASS uv run gz typecheck
PASS uv run gz test
PASS uv run mkdocs build --strict
PASS uv run -m behave features/
Verification completed.
```

### Code Quality

```text
$ uv run gz validate --documents
All validations passed.

$ uv run gz lint
Running linters...
All checks passed!

ADR path contract check passed.
Lint passed.

$ uv run gz typecheck
All checks passed!
Type check passed.

$ uv run gz format
Formatting code...
76 files left unchanged

All checks passed!
Format complete.
```

### Gate 3 (Docs)

```text
$ uv run mkdocs build --strict
PASS uv run mkdocs build --strict
```

### Gate 4 (BDD)

```text
$ uv run -m behave features/
PASS uv run -m behave features/
```

### Gate 5 (Human)

```text
Human attestation received on 2026-03-14: "Completed"

$ uv run gz git-sync --apply --lint --test
Git sync execution
  Branch: main
  Remote: origin
  ahead=0 behind=0 diverged=False dirty=True
  Actions:
    - git add -A
    - git fetch --prune origin
  Executed:
    - git add -A
    - gz lint (pre-sync)
    - gz test (pre-sync)
    - git commit
    - git push origin main
    - gz lint (post-sync)
Git sync completed.

$ uv run gz obpi emit-receipt OBPI-0.13.0-02-persist-stage-state --event completed --attestor "human:jeff" --evidence-json '{...}'
OBPI receipt emitted.
  OBPI: OBPI-0.13.0-02-persist-stage-state
  Parent ADR: ADR-0.13.0-obpi-pipeline-runtime-surface
  Event: completed
  Attestor: human:jeff

Re-baseline receipt refreshed on 2026-03-14 after the later OBPI-01 anchor
refresh advanced the synced head beyond the original OBPI-02 completion anchor.

$ uv run gz git-sync --apply --lint --test
Git sync execution
  Branch: main
  Remote: origin
  ahead=0 behind=0 diverged=False dirty=True
  Actions:
    - git add -A
    - git fetch --prune origin
  Executed:
    - git add -A
    - gz lint (pre-sync)
    - gz test (pre-sync)
    - git commit
    - git push origin main
    - gz lint (post-sync)
Git sync completed.

$ uv run gz obpi emit-receipt OBPI-0.13.0-02-persist-stage-state --event completed --attestor "human:jeff" --evidence-json '{...}'
OBPI receipt emitted.
  OBPI: OBPI-0.13.0-02-persist-stage-state
  Parent ADR: ADR-0.13.0-obpi-pipeline-runtime-surface
  Event: completed
  Attestor: human:jeff
```

### Value Narrative

<!-- What problem existed before this OBPI, and what capability exists now? -->

Before this OBPI, the pipeline only persisted a minimal active marker with
`obpi_id`, `started_at`, and `execution_mode`. That was enough to prove that a
pipeline was active, but not enough to tell which stage was currently active or
which receipt context the runtime had loaded. Now the existing active marker
files carry machine-readable stage state for the active pipeline run, including
`parent_adr`, `lane`, `entry`, `current_stage`, `updated_at`, and
`receipt_state`, while keeping the same file paths and preserving hook
compatibility.

### Key Proof

<!-- One concrete usage example, command, or before/after behavior. -->

```text
$ uv run gz obpi pipeline OBPI-0.13.0-02-persist-stage-state
OBPI pipeline: OBPI-0.13.0-02-persist-stage-state
  Parent ADR: ADR-0.13.0-obpi-pipeline-runtime-surface
  Lane: heavy
  Entry: full
  Receipt: MISSING
  Marker: .claude/plans/.pipeline-active-OBPI-0.13.0-02-persist-stage-state.json

Marker payload now includes:
- obpi_id
- parent_adr
- lane
- entry
- execution_mode
- current_stage
- started_at
- updated_at
- receipt_state
```

### Implementation Summary

- Files created/modified: `src/gzkit/cli.py`,
  `tests/commands/test_obpi_pipeline.py`, `tests/test_hooks.py`,
  `docs/user/commands/obpi-pipeline.md`,
  `docs/governance/GovZero/obpi-runtime-contract.md`,
  `.gzkit/skills/gz-obpi-pipeline/SKILL.md`, mirrored skill files, generated
  control surfaces from `gz agent sync control-surfaces`, and this brief.
- Tests added: richer marker payload coverage for full/verify/ceremony runtime
  entries plus hook compatibility cases for richer marker JSON.
- Date completed: `2026-03-14`
- Attestation status: human attestation recorded
- Re-baseline anchor refreshed: `2026-03-14` at `6bb374b` after the later
  OBPI-01 anchor refresh advanced the synced repository head.
- Re-baseline anchor refreshed: `2026-03-15` at `a587714` after completion
  receipt reconciliation cleared stale ADR-shared drift.
- Defects noted: fixed this brief's placeholder scope/requirements/template
  drift before implementation; fixed sync-time formatting drift detected by the
  first guarded `git-sync` attempt; the missing plan-audit receipt remains an
  explicit governance gap surfaced by the runtime warning.

## Tracked Defects

- GHI-11 (closed): OBPI anchor drift should ignore transient `.claude/hooks/.instruction-state.json`
- GHI-12 (closed): Recurring false OBPI anchor drift in shared-scope ADRs
- GHI-13 (closed): Status misclassifies completed OBPIs as drift when anchor freshness degrades

## Human Attestation

- Attestor: human:jeff
- Attestation: Completed
- Date: 2026-03-14

---

**Brief Status:** Completed

**Date Completed:** 2026-03-14

**Evidence Hash:** a587714
