Skip to content

feat(provider): normalize Claude and Codex run events (provider-contract Phases 2–3) - #110

Merged
suprie merged 3 commits into
mainfrom
run/provider-contract-normalize-claude-plus3-1aa529a93261
Oct 28, 2026
Merged

suprie merged 3 commits into
mainfrom
run/provider-contract-normalize-claude-plus3-1aa529a93261

Conversation

@suprie

@suprie suprie commented Oct 28, 2026

Copy link
Copy Markdown
Owner

Summary

Implements provider-contract Phases 2–3 from execution-foundations: Claude
and Codex output is converted at the adapter boundary into the versioned,
provider-neutral RunEvent envelope, dual-emitted alongside the unchanged
legacy ClaudeEvent channel, mechanically bound to TypeScript, consumed by one
migrated frontend consumer, and covered by fixture + reducer contract tests.

This is an additive migration. No legacy event payload changed, no permission
policy changed, and no provider launch/billing behavior changed.

What changed

  • Claude normalization (src-tauri/src/provider_normalize.rs,
    claude_session.rs): stream-json lines map to
    started/content/tool/decision/usage/terminal. Control requests emit a pending
    decision; the existing answer paths emit the resolved decision. User
    interrupts map to interrupted; duplicated terminals and synthesized
    interrupt terminals are gated so a segment has exactly one terminal.
  • Codex normalization (provider_normalize.rs, codex_session.rs):
    app-server notifications map to the same payload table, completed agent
    messages are suppressed when deltas already carried the text, server requests
    emit pending/resolved decisions, and capability gaps (plan approval, dynamic
    tools, unknown requests) become explicit unsupported events.
  • Redaction: tool inputs, decision summaries/details, and terminal messages
    pass through secret_scan::redact_secrets (the pre-push pattern set) and a
    4 KB truncation.
  • Auto-allow provenance: policy-approved actions are
    DecisionState::Allowed with the stable auto-allow: summary prefix — no
    enum change.
  • Decision render payload: additive detail_redacted (question specs,
    plan markdown, or {tool_name, input}) so approval/question cards are not
    lost during migration.
  • Shared types: ts-rs derives generate src/types/generated/*.ts;
    src/types/runEvent.ts is the hand-written barrel; CI regenerates and fails
    on git diff --exit-code.
  • Migrated consumer: useRunQueueEvents listens to the additive run-event
    topic and interprets it through the pure runEventProjection reducer. The
    legacy run-queue-event topic continues emitting unchanged.
  • Tests: provider_contract_tests.rs (fixture parity, one terminal,
    duplicated terminal, payload preservation, capabilities, auto-allow) and
    tests/frontend/runEventProjection.test.mjs (34 total frontend tests pass).

PRD

docs/epics/execution-foundations/prd-provider-contract.md — Phase 2
(normalize-claude, normalize-codex, share-types) and Phase 3
(test-contract). All four checkboxes are checked in this branch.

Acceptance criteria addressed:

  • equivalent product-level variants across providers, with differences explicit
    in capability metadata;
  • fixtures cover success, error, and interrupt for both providers and prove one
    terminal per segment;
  • a CI check detects Rust/TypeScript contract drift.

Decisions

Recorded in .loopdeck/decisions.md:

  • dual-emit normalized events during migration; retries are new segments;
  • ts-rs owns the TypeScript bindings; token counts/sequence stay number;
  • auto-allow stays allowed with summary provenance and decision render
    detail is additive.

Handoff artifacts

  • .loopdeck/handoff-artifacts/provider-contract-normalize-claude.md
  • .loopdeck/handoff-artifacts/provider-contract-normalize-codex.md
  • .loopdeck/handoff-artifacts/provider-contract-share-types.md
  • .loopdeck/handoff-artifacts/provider-contract-test-contract.md

Test plan

Command Result
cd src-tauri && cargo fmt --check clean
cargo clippy -- -D warnings clean
cargo test 727 passed, 0 failed, 9 ignored
cargo test export_bindings (run twice) deterministic output
npm run build built successfully
npm run test:frontend 34 passed, 0 failed
cargo run -- secret-scan --path . (staged diff) clean (exit 0)

DOM render note: the repo has no DOM/render runner, so per the pre-run decision
the migrated consumer's "render test" is covered at the pure projection seam
with node:test plus tsc against the generated bindings. The gap is recorded
in provider-contract-test-contract.md (Q1) rather than silently treated as a
rendered-UI assertion.

Verify Verdict

# Criterion Verdict Evidence
1 Claude output is converted at its adapter boundary while the legacy channel payload stays unchanged PASS src-tauri/src/provider_normalize.rs:278; src-tauri/src/claude_session.rs:1382,1671,1825
2 Codex output normalizes to the same semantic events while the legacy channel payload stays unchanged PASS src-tauri/src/provider_normalize.rs:383; src-tauri/src/codex_session.rs:351,555
3 Same product-level variants across providers; differences explicit in capability metadata PASS src-tauri/src/provider_event.rs (CapabilitySet); src-tauri/src/provider_contract_tests.rs:478
4 Fixtures cover success, error, and interrupt for both providers and prove exactly one terminal per segment PASS src-tauri/tests/fixtures/provider-events/manifest.json; src-tauri/src/provider_contract_tests.rs:391,425
5 Approval and question payloads are preserved (no lost approval payload) PASS src-tauri/src/provider_contract_tests.rs:263,311
6 Automatic approval maps to allowed with automatic provenance in the summary PASS src-tauri/src/provider_normalize.rs:45,555; src-tauri/src/provider_contract_tests.rs:509
7 TypeScript bindings are generated and a CI check detects Rust/TypeScript drift PASS src-tauri/src/provider_event.rs (ts-rs derives); src/types/generated/; .github/workflows/ci.yml:79-88
8 One consumer is migrated and a frontend type/projection test covers it; duplicate terminals are idempotent PASS src/hooks/useRunQueueEvents.ts:35; src/lib/runEventProjection.ts; tests/frontend/runEventProjection.test.mjs

Verdict: PASS

Run metadata

  • Phases:
    • provider-contract/normalize-claude — "Convert Claude output at its adapter boundary while preserving the current channel behavior." (execution-foundations / prd-provider-contract / Phase 2 — Provider normalization)
    • provider-contract/normalize-codex — "Convert Codex output to the same semantic events while preserving the current channel behavior." (execution-foundations / prd-provider-contract / Phase 2 — Provider normalization)
    • provider-contract/share-types — "Generate or mechanically validate TypeScript event bindings from Rust and migrate one consumer to them." (execution-foundations / prd-provider-contract / Phase 2 — Provider normalization)
    • provider-contract/test-contract — "Run fixture-based contract tests for both providers and a frontend type/render test for the migrated consumer; verify no duplicate terminal event or lost approval payload." (execution-foundations / prd-provider-contract / Phase 3 — Verification)
  • Budgets: 2000000 tokens total · 21600s wall-clock cap for this turn · 28800s total-run wall-clock cap

Notes for review

  • Draft PR only; do not mark ready or merge in this run.
  • Interactive streaming commands and multi-agent workers do not attach the
    normalized stream yet; the detached run queue is the migrated producer.
    Extending them is replay-PRD work.
  • raw_ref remains empty until a payload digest dependency/event store exists.

suprie and others added 3 commits October 28, 2026 17:01
- Dual-emit a provider-neutral RunEvent stream from both adapters while
  leaving legacy Channel<ClaudeEvent> payloads unchanged.
- Add a per-segment terminal gate, secret-pattern redaction, explicit
  capability gaps, and auto-allow provenance in the decision summary.
- Generate TypeScript bindings with ts-rs and fail CI on Rust/TS drift.
- Migrate the run-queue consumer to the normalized stream through a pure,
  node:test-covered projection reducer.
- Add fixture parity tests proving one terminal per segment and preserved
  approval/question payloads.

Rubric: PASS — 8/8 criteria

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@suprie
suprie marked this pull request as ready for review October 28, 2026 10:07
@suprie
suprie merged commit 5ace1a0 into main Oct 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant