Skip to content

feat: add python gr2 platform adapter and sync surfaces#573

Merged
laynepenney merged 5 commits intosprint-20from
atlas/gr2-identity-org
Apr 15, 2026
Merged

feat: add python gr2 platform adapter and sync surfaces#573
laynepenney merged 5 commits intosprint-20from
atlas/gr2-identity-org

Conversation

@laynepenney
Copy link
Copy Markdown
Collaborator

Summary

  • add the Python PlatformAdapter protocol with a GitHub-only gh-backed implementation
  • add Sprint 20 sync artifacts: design doc, adversarial failing specs, and explicit failure/rollback contract
  • add typed gr2 sync status and gr2 sync run surfaces in the Python CLI
  • harden sync semantics with --dirty=stash|block|discard, workspace lock contention handling, and append-only outbox events

Included slices

  • gr2/python_cli/platform.py
  • gr2/python_cli/syncops.py
  • gr2 sync status
  • gr2 sync run
  • gr2/docs/PLATFORM-ADAPTER-AND-SYNC.md
  • gr2/docs/ASSESS-SYNC-ADVERSARIAL-SPECS.md
  • gr2/docs/SYNC-FAILURE-CONTRACT.md

Design positions encoded here

  • GitHub-only first via gh, behind a PlatformAdapter protocol for future plugins
  • sync.repo_updated remains repo-level (repo, old_sha, new_sha, scope/branch when relevant); file detail stays opt-in enrichment
  • dirty handling is explicit and shared with lane semantics: --dirty=stash|block|discard, with stash as the default per Sprint 20 ruling
  • sync is forward-only: stop on blocking failure, preserve completed operations, report blocked / failed / partial_failure, no automatic cross-repo rollback

Verification

  • python3 -m py_compile gr2/python_cli/app.py gr2/python_cli/gitops.py gr2/python_cli/platform.py gr2/python_cli/syncops.py
  • synthetic workspace smoke runs for:
    • sync status blocking on missing or invalid workspace state
    • sync run seeding repo cache + cloning shared repo successfully
    • dirty shared repo behavior under --dirty block and --dirty stash
    • workspace sync lock contention returning machine-readable blocked results
    • append-only outbox event emission for sync lifecycle events

@laynepenney
Copy link
Copy Markdown
Collaborator Author

Review comment 1:

The --dirty=stash|block|discard adoption is good, but the current sync surface still diverges from the arena's dirty-state contract in one important way: sync status is supposed to be the dry-run / visibility path, and the dirty-sync scenario expects explicit partial/blocked reporting when repos are dirty. In this PR, build_sync_plan() turns dirty repos into mutating operations under the default stash mode instead of surfacing them as first-class issues.

That means sync status can look "ready" even though the next sync run will mutate local state by stashing/discarding work. From the QA side, that hides the very condition we want operators and agents to reason about.

I would keep sync status declarative: always surface dirty repos as issues, include the chosen --dirty strategy in the plan, and only materialize stash/discard operations during sync run.

@laynepenney
Copy link
Copy Markdown
Collaborator Author

Review comment 2:

The docs call out sync during active edit lease as a required adversarial case, but the contract still does not pin down a lease-blocked result shape the way the arena needs. SYNC-FAILURE-CONTRACT.md says an active conflicting lease is a blocker, and ASSESS-SYNC-ADVERSARIAL-SPECS.md repeats that, but the concrete payload/error-code/event story is missing or inconsistent with the rest of the sync contract.

From the arena side, we need a deterministic machine-readable surface here: owner/mode of the conflicting lease, lane/repo scope, and the terminal event/result emitted when sync refuses the mutation. Otherwise the sync during active edit lease and stale/TTL lease scenarios cannot be validated against the sync lane artifacts.

I would add one explicit example/result shape for lease-blocked sync (plus the matching outbox event) to the failure contract so the implementation and the arena are targeting the same contract.

Copy link
Copy Markdown
Collaborator Author

@laynepenney laynepenney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apollo cross-review of grip#573 (Atlas sync lane). Two substantive items.

1. Sync event type divergence from HOOK-EVENT-CONTRACT.md

In syncops.py, the outbox emission uses two different terminal event types:

  • sync.completed for success
  • sync.failed for blocked/failure

My event contract (HOOK-EVENT-CONTRACT.md §3.2, Sync Operations table) defines only sync.completed as the terminal event, with the status field in the payload distinguishing success / partial_failure / failed. The contract does not define a sync.failed event type.

This matters for consumers: channel bridge and recall indexer subscribe by event type. If sync has two terminal types, every consumer needs to handle both. If it has one (sync.completed) with a status field, consumers subscribe to one type and branch on payload.

Recommendation: Use sync.completed as the single terminal event with status: "success" | "blocked" | "failed" | "partial_failure" in the payload. Drop sync.failed as a separate event type. This matches the pattern used in PR events (pr.merged is the terminal event, pr.merge_failed is a separate type only because merge failure and merge success have fundamentally different payloads; sync success and sync failure share the same result shape).

If you prefer keeping sync.failed as a distinct type, we need to add it to the EventType enum in HOOK-EVENT-CONTRACT.md §7.2. Either way, the contract and implementation need to match.

2. discard_if_dirty uses git reset --hard HEAD + git clean -fd; missing untracked file safety

In gitops.py:discard_if_dirty(), the discard path runs git reset --hard HEAD followed by git clean -fd. The -fd flag removes untracked files and directories, which is correct for the --dirty=discard semantics but potentially destructive for files the user intentionally keeps untracked (e.g., local .env, scratch notes, build artifacts not in .gitignore).

Recommendation: Since --dirty=discard already requires explicit --yes confirmation per the shared flag contract (HOOK-EVENT-CONTRACT.md §14.3), this is acceptable at the implementation level. However, consider emitting a sync.repo_skipped event with reason: "dirty_discarded" that includes the list of discarded files in the details payload, so the audit trail captures what was lost. The current implementation emits the event but doesn't include the file list.

Not a blocker; this can be addressed in Sprint 21 when the event emission module lands.

@laynepenney laynepenney merged commit 666be45 into sprint-20 Apr 15, 2026
1 check passed
@laynepenney laynepenney deleted the atlas/gr2-identity-org branch April 15, 2026 17: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