Skip to content

feat: gr2 event system runtime (emit + outbox + cursors)#574

Merged
laynepenney merged 7 commits intosprint-21from
test/event-system-runtime
Apr 15, 2026
Merged

feat: gr2 event system runtime (emit + outbox + cursors)#574
laynepenney merged 7 commits intosprint-21from
test/event-system-runtime

Conversation

@laynepenney
Copy link
Copy Markdown
Collaborator

Summary

  • Implements emit() function: flat JSONL append to .grip/events/outbox.jsonl
  • EventType enum with all 29 event types from HOOK-EVENT-CONTRACT.md
  • Outbox rotation at 10MB threshold with outbox.{timestamp}.jsonl naming
  • Cursor-based consumer model (read_events()) with atomic cursor updates
  • 33 tests covering the full contract (sections 3-8 of the event contract)

Files

  • gr2/python_cli/events.py -- event system runtime module
  • gr2/tests/__init__.py -- test package init
  • gr2/tests/conftest.py -- shared fixtures (workspace fixture)
  • gr2/tests/test_events.py -- 33 contract tests

Test plan

  • All 33 tests pass locally (pytest gr2/tests/test_events.py -v)
  • Sentinel cross-validates event field names against QA arena scenarios
  • Atlas confirms sync.completed status field values match syncops.py

Premium boundary: core OSS (event infrastructure).

laynepenney and others added 7 commits April 15, 2026 12:13
TDD red phase for Sprint 21 event system lane. Tests define the
full contract from HOOK-EVENT-CONTRACT.md sections 3-8:

- EventType enum: all 28 event types, correct string values
- emit(): flat JSONL output, envelope fields, context fields,
  16-char hex event_id, ISO 8601 timestamps, reserved name collision
- Sequence numbers: monotonic starting at 1, unique event_ids
- Outbox rotation: 10MB threshold, timestamped archives, seq continuity
- Cursor model: read_events(), cursor advancement, independent consumers
- Error handling: emit() does not raise on write failure

All 33 tests fail with ModuleNotFoundError (events.py not yet created).
Implements HOOK-EVENT-CONTRACT.md sections 3-8:

- EventType enum with all 29 event types across 6 domains
- emit() function: flat JSONL append to .grip/events/outbox.jsonl
  - 16-char hex event_id from os.urandom(8).hex()
  - Monotonic seq numbers (line-count based, single-writer safe)
  - ISO 8601 timestamps with timezone
  - Reserved name collision check for payload keys
  - Does not raise on write failure (logs to stderr)
- Outbox rotation at 10MB with outbox.{timestamp}.jsonl naming
- Cursor-based consumer model: read_events() + atomic cursor updates
  - Independent cursors per consumer at .grip/events/cursors/
  - Cursor file uses atomic write-tmp-rename pattern

All 33 tests passing.

Premium boundary: core OSS (event infrastructure).
Events now fire at these points in app.py:
- lane create -> lane.created (with repos, branch_map, lane_type)
- lane enter  -> lane.entered (with repos, lane_type)
- lane exit   -> lane.exited  (with stashed_repos tracking)
- lease acquire -> lease.acquired (with mode, ttl_seconds)
- lease release -> lease.released

Each emit() call fires after the operation succeeds. If the
operation fails (typer.Exit), no event is emitted. stash_if_dirty
return values are now tracked to populate stashed_repos accurately.

Premium boundary: core OSS (event wiring into CLI commands).
Implements HOOK-EVENT-CONTRACT.md section 8:

- format_event(): maps event dicts to channel message strings
  - 11 mapped event types with message templates from the contract
  - hook.failed only maps when on_failure="block" (warn/skip silently dropped)
  - All unmapped event types return None (silently dropped)
- run_bridge(): cursor-based consumer using read_events()
  - Callable post_fn injection keeps MCP/recall out of the module
  - Returns count of messages posted
  - Uses "channel_bridge" cursor for independent consumption

33 channel bridge tests + 33 events tests = 66 total, all green.

Premium boundary: core OSS (channel bridge consumer).
Emits hook.started/completed/failed/skipped events from hooks.py
per HOOK-EVENT-CONTRACT.md sections 3.2 and 6.2-6.4:

- hook.started: emitted before subprocess.run with stage, hook_name,
  repo, command, cwd
- hook.completed: emitted on exit_code=0 with duration_ms
- hook.failed: emitted on non-zero exit with duration_ms, exit_code,
  on_failure policy, and stderr_tail (last 500 bytes). Emitted BEFORE
  HookRuntimeError raise on on_failure=block so the event is durable.
- hook.skipped: emitted when _should_run() returns False, no started
  event preceding it

Timing uses time.monotonic() around subprocess.run for duration_ms.
15 new tests covering all four event types, multiple hooks in
sequence, and stderr truncation. 81 total tests all green.

Premium boundary: core OSS (hook event emission).
Adds pr.py orchestration layer between CLI and PlatformAdapter:
- create_pr_group: assigns pr_group_id, calls adapter per repo, emits pr.created
- merge_pr_group: merges in repo order, emits pr.merged or pr.merge_failed
- check_pr_group_status: polls status/checks, emits pr.status_changed/checks_passed/checks_failed
- record_pr_review: push-model entry point for pr.review_submitted

17 tests covering all 7 PR event types via FakeAdapter test double.
98 total tests passing (33 events + 33 bridge + 15 hooks + 17 PR).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convergence fixes from cross-review of grip#575:
- Add fcntl.flock(LOCK_EX) around outbox write in emit() for
  concurrency safety when multiple agents emit simultaneously
- Add SYNC_CACHE_SEEDED and SYNC_CACHE_REFRESHED to EventType enum
  (7 sync types total, 31 EventType members)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@laynepenney laynepenney merged commit 0725867 into sprint-21 Apr 15, 2026
1 check passed
@laynepenney laynepenney deleted the test/event-system-runtime branch April 15, 2026 18:07
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