Skip to content

fix(content): approve tells the truth on a diverged repo - #200

Open
ABB65 wants to merge 1 commit into
mainfrom
fix/approve-tells-the-truth
Open

fix(content): approve tells the truth on a diverged repo#200
ABB65 wants to merge 1 commit into
mainfrom
fix/approve-tells-the-truth

Conversation

@ABB65

@ABB65 ABB65 commented Aug 14, 2026

Copy link
Copy Markdown
Member

Katman 0 of the reconcile program (.internal/refactor/tasks/AI-REPO-RECONCILE.md) — the Studio half that does not wait for the ecosystem's planReconcile.

The incident this fixes

Approve on collabers → "Server Error". Retry → another. Railway logs: POST /merges → 409 ×12, then Head does not exist → unhandled 500. Repo compare: contentrain and main diverged (13/7, 26 files) because the 3.0 migration landed on main by PR — outside the content pipeline.

The content was never lost — step 1 (cr/* → contentrain) had landed it, and every reader (brain, CDN, MCP) reads contentrain. Three places turned that recoverable state into lies:

where lie
ensureContentBranch sync catch swallowed the conflict under a comment claiming the branches hold "different directories" — they share .contentrain/
finalizeContentrain only protected-branch failures became a PR; a 409 was thrown raw → 500 for a landed save
retry path step 1 had deleted the cr/* branch → second click died on "Head does not exist"

The contract

Aligned with the MCP-side R0 vocabulary (reconcile feedback, N3), so the two ecosystems name the same states the same way:

{ merged, sha, mainAdvance?: 'advanced' | 'blocked_diverged', pullRequestUrl? }
  • merged answers what the caller actually asks: did the content land on contentrain.
  • mainAdvance is the separate fact about main. A PR is an attachment, not a status — pr_opened is derivable as blocked_diverged + pullRequestUrl.

Behavior changes

  • Advance conflict → PR, same fallback as protected main. The PR body names the divergence — it is the artifact a developer resolves. 422 "already exists" is bookkeeping; any other PR failure is logged, never thrown.
  • Protected fallback now reports merged: true — step 1 had landed the content there too; the old false reported a landed save as failed. (Deliberate change to an existing behavior; the old test asserted the lie and was updated with a comment saying why.)
  • Second approve of an already-landed branch finishes the half that failed (the advance) instead of 500ing on the deleted head.
  • Real cr/* vs contentrain conflict stays merged: false — the one case where "resolve manually" is honest.
  • Sync conflict is logged loudly (with project id); writes continue — policy (b): the editor is never blocked by a divergence a developer has to resolve.
  • UI says it out loud: merged+advanced → success; merged+blocked → warning ("publishing awaits a developer review — a pull request is open"); conflict → error. All three from the dictionary; the success path was hardcoded English.
  • Turn-end finalize stops claiming divergence "self-heals on next merge" — it never did; finalize now turns it into a PR itself.

Verified

  • pnpm lint 0 errors · pnpm typecheck clean · pnpm test 151 files / 1311 tests
  • 6 engine tests map one-to-one onto the incident: the 409→PR fallback (asserting the PR body names the divergence), the 422 dedup, the head-does-not-exist retry, the real-conflict case, the protected fallback under the new contract, and the sync-conflict log with writes proceeding
  • 2 composable tests: blocked → warning not success, conflict → error with the branch kept in the list

Not verified — needs staging

  • The actual collabers flow end to end: Approve on the still-diverged repo should now show the warning toast and open/reuse one PR (contentrain → main) — this is also the fastest real-world validation, since the repo is still diverged until you resolve it
  • After you resolve the divergence (merge main into contentrain first, then advance — direction matters), Approve should go back to plain success

Next

  • Katman 1: compare-based divergence status in health + product-language surface + push-webhook trigger (separate PR)
  • When the ecosystem's planReconcile ships (mcp minor), the conflict path tries the content-aware three-way merge first; the PR remains only for surviving ConflictItems

Live incident (collabers): the editor approved a change and got "Server
Error"; retrying got another. The content had landed — every reader
reads `contentrain`, and `contentrain` had it — but the advance to main
answered 409 because main carried out-of-Studio `.contentrain/` changes
(a dependency migration PR), and three places turned that recoverable
state into lies:

- the `main → contentrain` sync swallowed the conflict under a comment
  claiming the branches held "different directories" — they share
  `.contentrain/`, and the divergence became invisible
- `finalizeContentrain` converted only protected-branch failures to a
  PR; a conflict was thrown raw, so a landed save surfaced as a 500
- the retry then died on "Head does not exist", because step 1 had
  deleted the cr/* branch the first time around

The contract, aligned with the MCP-side R0 vocabulary (reconcile
feedback, N3): `merged` answers what the caller actually asks — did the
content land on `contentrain`. The advance is a separate fact,
`mainAdvance: 'advanced' | 'blocked_diverged'`, and a PR is an
attachment (`pullRequestUrl`), not a status.

- advance conflict → PR, same fallback as protected main. The PR body
  names the divergence — it is the artifact a developer resolves.
  A 422 "already exists" is bookkeeping, not a failed merge; any other
  PR failure is logged, never thrown, because throwing would repeat the
  exact lie this removes.
- protected fallback now also reports `merged: true` — step 1 had
  landed the content there too; the old `false` was the same lie.
- a second approve of an already-landed branch finishes the half that
  failed (the advance) instead of 500ing on the deleted head.
- a real cr-vs-contentrain conflict stays `merged: false` — the one
  case where "resolve manually" is the honest answer.
- the sync conflict is logged loudly with the project id; writes
  continue (the editor is never blocked by a divergence a developer
  has to resolve).

The UI says it out loud: merged + advanced → success toast; merged +
blocked → warning that publishing awaits a developer review; conflict →
error. All three from the dictionary — the success path was hardcoded
English.

The turn-end finalize log stops claiming divergence "self-heals on next
merge" — it never did; finalize now turns it into a PR itself, so what
reaches that catch is transient.

Interim until the ecosystem's planReconcile ships (AI-REPO-RECONCILE):
then the conflict path tries a content-aware three-way merge first and
opens a PR only for what survives it.
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