From f838f238abb9217a84145c9ed33b3e09747723b0 Mon Sep 17 00:00:00 2001 From: cairn-reviewer-scratch Date: Wed, 19 Aug 2026 23:33:18 -0700 Subject: [PATCH 1/4] =?UTF-8?q?test(feat):=20red=20=E2=80=94=20Investigate?= =?UTF-8?q?=20title/summary=20reindex-freshness=20asymmetry=20(needs=20liv?= =?UTF-8?q?e=20repro=20first)=20(refs=20crn-rott9.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestStatusServesStaleTitleSummaryAfterFileOnlyWriteback reproduces the asymmetry live: WriteBackBackfill (writeBackPatched) is pure filesystem I/O with no git operation, so it never advances the store's HEAD. ensureFresh's staleness check compares only the index watermark against HEAD, so it never notices the edit -- Status (prime/list) keeps serving the pre-edit title/summary from stale SQL columns, while Find (recall/get) already re-reads the body fresh off disk and is unaffected. --- internal/cairn/index_test.go | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/internal/cairn/index_test.go b/internal/cairn/index_test.go index 5f49c2e4..b7bf5f9c 100644 --- a/internal/cairn/index_test.go +++ b/internal/cairn/index_test.go @@ -526,6 +526,62 @@ func TestEnsureFreshNoopOnNonGitStore(t *testing.T) { assert.Equal(t, 0, calls, "non-git store has no HEAD to drift from; an already-built index must not be reindexed") } +// TestStatusServesStaleTitleSummaryAfterFileOnlyWriteback pins crn-rott9.3: the +// title/summary reindex-freshness asymmetry flagged by crn-rott9's design doc +// (fix 4). WriteBackBackfill (like its sibling WriteBackRetrievalMetadata) goes +// through writeBackPatched, which is pure filesystem I/O -- os.ReadFile plus +// os.WriteFile, no git operation at all -- so patching an entry's title/summary +// this way never advances the store's checked-out HEAD. ensureFresh's +// staleness check (indexStale) compares only the index's stored watermark +// against HEAD, so it has no way to notice this edit: the watermark still +// matches HEAD, the index is judged fresh, and no reindex ever runs on its +// own. +// +// Find (recall/get) is unaffected by this -- it re-reads the body file +// directly off disk on every call, regardless of index freshness. But Status +// (prime/list) reads title/summary from the SQL index columns alone, so today +// it keeps serving the pre-edit values until something calls Reindex +// explicitly. +func TestStatusServesStaleTitleSummaryAfterFileOnlyWriteback(t *testing.T) { + ctx := t.Context() + store := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(store, "global"), 0o750)) + body := "+++\nid = \"a\"\ntitle = \"Old title\"\nsummary = \"Old summary\"\ntype = \"knowledge\"\n+++\nbody\n" + require.NoError(t, os.WriteFile(filepath.Join(store, "global", "a.md"), []byte(body), 0o600)) + gitInit(t, store) + gitCommitAll(t, store, "init") + + _, err := Reindex(ctx, store) + require.NoError(t, err) + + beforeHead, ok, err := git(ctx, store, "rev-parse", "HEAD") + require.NoError(t, err) + require.True(t, ok) + + e, err := Find(ctx, store, "a") + require.NoError(t, err) + e.Title, e.Summary = "New title", "New summary" + require.NoError(t, e.WriteBackBackfill(true)) + + afterHead, ok, err := git(ctx, store, "rev-parse", "HEAD") + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, beforeHead, afterHead, + "WriteBackBackfill is a pure filesystem patch and must not itself advance HEAD -- this is the trigger the bug depends on") + + visible, err := Status(ctx, store) + require.NoError(t, err) + require.Len(t, visible, 1) + assert.Equal(t, "New title", visible[0].Title, + "prime/list (Status) must reflect a title/summary writeback without requiring an explicit cairn reindex") + assert.Equal(t, "New summary", visible[0].Summary) + + fresh, err := Find(ctx, store, "a") + require.NoError(t, err) + assert.Equal(t, "New title", fresh.Title, + "recall/get (Find) already re-reads the body fresh off disk regardless of index freshness -- no regression expected here") +} + func TestEnsureFreshWithLogsIndexDriftWhenStale(t *testing.T) { store := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(store, "global"), 0o750)) From 077b4261f2b7f0dac3c5b5597e76ebef2b15140d Mon Sep 17 00:00:00 2001 From: cairn-reviewer-scratch Date: Wed, 19 Aug 2026 23:47:10 -0700 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20green=20=E2=80=94=20Investigate=20t?= =?UTF-8?q?itle/summary=20reindex-freshness=20asymmetry=20(needs=20live=20?= =?UTF-8?q?repro=20first)=20(refs=20crn-rott9.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WriteBackRetrievalMetadata and WriteBackBackfill now sync the index's title/summary/title_source/summary_source columns directly via SQL right after a successful file patch, mirroring Find's existing hit_count/ last_recalled_at direct-write pattern. Find now stamps the resolved Entry with the store root it came from so this sync has a target; an Entry built via a bare ParseEntry (no store) is a no-op and falls back to today's behavior (correct again after the next reindex). This closes the gap ensureFresh's HEAD-watermark staleness check can't see: WriteBackBackfill/WriteBackRetrievalMetadata are pure filesystem I/O with no git operation, so patching title/summary through them never advances HEAD, and indexStale never fires. Status (prime/list) now reflects the edit immediately instead of waiting for an unrelated reindex to happen to run. Scoped to title/summary only, per this bead's exit contract -- type and the other writeBackPatched-based fields (anchor, recurrence_count, promoted_bead_id, review_status) are untouched. --- internal/cairn/entry.go | 62 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/internal/cairn/entry.go b/internal/cairn/entry.go index 9112d6c4..61e845e4 100644 --- a/internal/cairn/entry.go +++ b/internal/cairn/entry.go @@ -89,6 +89,14 @@ type Entry struct { BodyPath string `toml:"-"` Body string `toml:"-"` + + // store is the index root this Entry was resolved against, set only by + // Find. It exists purely so a writeBackPatched-based writeback that + // touches title/summary can sync the index directly (see + // syncIndexRetrievalMetadata) instead of leaving Status stale until the + // next HEAD-triggered reindex (crn-rott9.3). Empty for an Entry built via + // a bare ParseEntry -- those calls fall back to today's behavior. + store string `toml:"-"` } var scopeDirs = []string{"global", "rig", "role", "agent"} @@ -172,14 +180,17 @@ func (e *Entry) WriteBackAnchor() error { // WriteBackRetrievalMetadata surgically replaces title/summary and records // both as authored. The body and unrelated frontmatter remain byte-identical. func (e *Entry) WriteBackRetrievalMetadata() error { - return e.writeBackPatched(func(front string) (string, error) { + if err := e.writeBackPatched(func(front string) (string, error) { lines := strings.Split(front, "\n") lines = patchTopLevelScalar(lines, "title", strconv.Quote(e.Title)) lines = patchTopLevelScalar(lines, "title_source", strconv.Quote(MetadataSourceAuthored)) lines = patchTopLevelScalar(lines, "summary", strconv.Quote(e.Summary)) lines = patchTopLevelScalar(lines, "summary_source", strconv.Quote(MetadataSourceAuthored)) return strings.Join(lines, "\n"), nil - }) + }); err != nil { + return err + } + return e.syncIndexRetrievalMetadata(context.Background(), e.Title, MetadataSourceAuthored, e.Summary, MetadataSourceAuthored) } // WriteBackBackfill atomically patches a legacy entry's classification and, @@ -195,7 +206,7 @@ func (e *Entry) WriteBackBackfill(updateRetrievalMetadata bool) error { if e.Kind == EntryTypeRemediation && e.Type != EntryTypeRemediation { return fmt.Errorf("type %q contradicts legacy kind %q", e.Type, e.Kind) } - return e.writeBackPatched(func(front string) (string, error) { + if err := e.writeBackPatched(func(front string) (string, error) { lines := strings.Split(front, "\n") lines = patchTopLevelScalar(lines, "type", strconv.Quote(e.Type)) if updateRetrievalMetadata { @@ -205,7 +216,13 @@ func (e *Entry) WriteBackBackfill(updateRetrievalMetadata bool) error { lines = patchTopLevelScalar(lines, "summary_source", strconv.Quote(MetadataSourceAuthored)) } return strings.Join(lines, "\n"), nil - }) + }); err != nil { + return err + } + if !updateRetrievalMetadata { + return nil + } + return e.syncIndexRetrievalMetadata(context.Background(), e.Title, MetadataSourceAuthored, e.Summary, MetadataSourceAuthored) } func patchTopLevelScalar(lines []string, key, value string) []string { @@ -350,6 +367,42 @@ func (e *Entry) applyPatchedFrontmatter(raw []byte, patch func(front string) (st return os.WriteFile(e.BodyPath, []byte(sb.String()), 0o600) } +// syncIndexRetrievalMetadata directly patches the index's title/summary +// (and their source) columns for e.ID, without a full Reindex. +// WriteBackRetrievalMetadata and WriteBackBackfill both go through +// writeBackPatched, which is pure filesystem I/O -- no git operation -- so +// patching title/summary this way never advances the store's checked-out +// HEAD. ensureFresh's staleness check compares only the index's watermark +// against HEAD, so it has no way to notice the edit: left alone, Status +// (prime/list) would keep serving the pre-edit title/summary from the index +// until an unrelated reindex happened to run (crn-rott9.3). This mirrors +// Find's own hit_count/last_recalled_at update above: state written +// directly via SQL rather than waiting on the next full reindex. +// +// A no-op (nil error) when e carries no store -- an Entry built directly via +// ParseEntry rather than Find (as in WriteBackRetrievalMetadata's own unit +// test) has no index to sync against and falls back to today's behavior: +// correct again after the next reindex. Likewise a no-op, after the UPDATE +// runs, if id isn't indexed yet -- the next Find/Reindex picks it up fresh +// from the body, same as any other not-yet-indexed entry. +func (e *Entry) syncIndexRetrievalMetadata(ctx context.Context, title, titleSource, summary, summarySource string) error { + if e.store == "" { + return nil + } + db, err := openDB(e.store) + if err != nil { + return err + } + defer func() { _ = db.Close() }() + return retryOnBusy(ctx, func() error { + _, err := db.ExecContext(ctx, + `UPDATE entries SET title = ?, title_source = ?, summary = ?, summary_source = ? WHERE id = ?`, + title, titleSource, summary, summarySource, e.ID, + ) + return err + }) +} + // patchVerification patches verified_at (top-level) and anchor.fingerprint // (inside the [anchor] table) into front, a splitFrontmatter frontmatter // blob, in place -- every other line, including field order, indentation, @@ -857,6 +910,7 @@ func Find(ctx context.Context, store, id string) (*Entry, error) { if err != nil { return nil, err } + e.store = store // hit_count and last_recalled_at are index-only state (crn-6az.6.1.1, // crn-28ge.1.1): the freshly-parsed body's values are stale-by-construction, From a6173d9cc6d729748da07cf93bfb705b14045f2f Mon Sep 17 00:00:00 2001 From: cairn-reviewer-scratch Date: Wed, 19 Aug 2026 23:55:33 -0700 Subject: [PATCH 3/4] fix: thread ctx through WriteBackRetrievalMetadata/WriteBackBackfill (refs crn-rott9.3) golangci-lint's contextcheck flagged cmd/backfill.go:335 after the GREEN commit: WriteBackBackfill now reaches syncIndexRetrievalMetadata, which called context.Background() internally even though applyBackfillRecord already had a ctx in scope. Thread ctx through both public methods instead of manufacturing a fresh background context inside the writeback path; call sites (the one production caller plus both tests) now pass their own in-scope context. No behavior change -- same index sync, just wired to the real context. --- cmd/backfill.go | 2 +- cmd/backfill_test.go | 2 +- internal/cairn/entry.go | 8 ++++---- internal/cairn/index_test.go | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/backfill.go b/cmd/backfill.go index 62c838fd..51a16212 100644 --- a/cmd/backfill.go +++ b/cmd/backfill.go @@ -332,7 +332,7 @@ func applyBackfillRecord(ctx context.Context, store string, r backfillRecord, wr e.Title = r.ProposedTitle e.Summary = r.ProposedSummary } - if err = e.WriteBackBackfill(metadataProposed); err != nil { + if err = e.WriteBackBackfill(ctx, metadataProposed); err != nil { return applyResult{ID: r.ID, Status: "failed", Detail: err.Error()}, err } return applyResult{ID: r.ID, Status: "applied"}, nil diff --git a/cmd/backfill_test.go b/cmd/backfill_test.go index 30f0cc88..5793b2eb 100644 --- a/cmd/backfill_test.go +++ b/cmd/backfill_test.go @@ -104,7 +104,7 @@ func TestWriteBackRetrievalMetadataPreservesBody(t *testing.T) { e, err := cairn.ParseEntry(path) require.NoError(t, err) e.Title, e.Summary = "New claim", "New retrieval summary" - require.NoError(t, e.WriteBackRetrievalMetadata()) + require.NoError(t, e.WriteBackRetrievalMetadata(context.Background())) got, err := cairn.ParseEntry(path) require.NoError(t, err) assert.Equal(t, body, got.Body) diff --git a/internal/cairn/entry.go b/internal/cairn/entry.go index 61e845e4..f9745fd4 100644 --- a/internal/cairn/entry.go +++ b/internal/cairn/entry.go @@ -179,7 +179,7 @@ func (e *Entry) WriteBackAnchor() error { // WriteBackRetrievalMetadata surgically replaces title/summary and records // both as authored. The body and unrelated frontmatter remain byte-identical. -func (e *Entry) WriteBackRetrievalMetadata() error { +func (e *Entry) WriteBackRetrievalMetadata(ctx context.Context) error { if err := e.writeBackPatched(func(front string) (string, error) { lines := strings.Split(front, "\n") lines = patchTopLevelScalar(lines, "title", strconv.Quote(e.Title)) @@ -190,14 +190,14 @@ func (e *Entry) WriteBackRetrievalMetadata() error { }); err != nil { return err } - return e.syncIndexRetrievalMetadata(context.Background(), e.Title, MetadataSourceAuthored, e.Summary, MetadataSourceAuthored) + return e.syncIndexRetrievalMetadata(ctx, e.Title, MetadataSourceAuthored, e.Summary, MetadataSourceAuthored) } // WriteBackBackfill atomically patches a legacy entry's classification and, // when requested, its retrieval metadata. It exists for the migration path: // new-entry validation intentionally rejects policy, while a migration may // classify an existing policy entry so a librarian can query and remove it. -func (e *Entry) WriteBackBackfill(updateRetrievalMetadata bool) error { +func (e *Entry) WriteBackBackfill(ctx context.Context, updateRetrievalMetadata bool) error { switch e.Type { case EntryTypeKnowledge, EntryTypeRemediation, EntryTypePolicy: default: @@ -222,7 +222,7 @@ func (e *Entry) WriteBackBackfill(updateRetrievalMetadata bool) error { if !updateRetrievalMetadata { return nil } - return e.syncIndexRetrievalMetadata(context.Background(), e.Title, MetadataSourceAuthored, e.Summary, MetadataSourceAuthored) + return e.syncIndexRetrievalMetadata(ctx, e.Title, MetadataSourceAuthored, e.Summary, MetadataSourceAuthored) } func patchTopLevelScalar(lines []string, key, value string) []string { diff --git a/internal/cairn/index_test.go b/internal/cairn/index_test.go index b7bf5f9c..586522ab 100644 --- a/internal/cairn/index_test.go +++ b/internal/cairn/index_test.go @@ -561,7 +561,7 @@ func TestStatusServesStaleTitleSummaryAfterFileOnlyWriteback(t *testing.T) { e, err := Find(ctx, store, "a") require.NoError(t, err) e.Title, e.Summary = "New title", "New summary" - require.NoError(t, e.WriteBackBackfill(true)) + require.NoError(t, e.WriteBackBackfill(ctx, true)) afterHead, ok, err := git(ctx, store, "rev-parse", "HEAD") require.NoError(t, err) From 49f4a8f541d6102e7c35062b03ff4709242743cf Mon Sep 17 00:00:00 2001 From: cairn-reviewer-scratch Date: Thu, 20 Aug 2026 10:00:31 -0700 Subject: [PATCH 4/4] chore: release gate PASS for crn-bmy54 (writeback title/summary reindex-freshness fix) --- release-gates/crn-bmy54-gate.md | 106 ++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 release-gates/crn-bmy54-gate.md diff --git a/release-gates/crn-bmy54-gate.md b/release-gates/crn-bmy54-gate.md new file mode 100644 index 00000000..3519f66d --- /dev/null +++ b/release-gates/crn-bmy54-gate.md @@ -0,0 +1,106 @@ +# Release Gate: crn-bmy54 + +**Bead:** crn-bmy54 (deploy) — source: crn-x81gk (review), build: crn-rott9.3 (architect: crn-rott9, child F) +**Reviewed commit:** `d5d7b1e1fb05d32a24e44b166b2252f8b6c363e2` (matches crn-x81gk's verdict and the deploy bead's `metadata.gc.deploy_commit` — see criterion 1) +**Deploy commit:** `a6173d9cc6d729748da07cf93bfb705b14045f2f` — a bounded self-rebase of the reviewed commit onto `origin/main`'s current tip (see criterion 6), cut onto `deploy/crn-bmy54-gate` +**Date:** 2026-08-20 + +## Background + +cairn's `WriteBackBackfill` and `WriteBackRetrievalMetadata` patch an entry's +frontmatter file directly (`writeBackPatched` — pure `os.ReadFile`/ +`os.WriteFile`, zero git ops), without ever advancing the store's checked-out +HEAD. Because `ensureFresh`'s staleness check is HEAD-watermark-based, it +never noticed the file-only edit, so `Status` (`cairn prime`/`list`) kept +serving stale title/summary indefinitely until an unrelated `Reindex` +happened to run. + +The fix adds an `Entry.store` field (set by `Find`) and a +`syncIndexRetrievalMetadata` method that directly `UPDATE`s the entries +table's `title`/`summary` columns after a successful file patch, called from +both `WriteBackRetrievalMetadata` (always) and `WriteBackBackfill` (when +`updateRetrievalMetadata=true`). `Find`/`recall`/`get` were never affected — +they always re-read the body file fresh — this bug was specific to the +cached index columns `Status` reads from. + +## Criteria + +| # | Criterion | Result | Evidence | +|---|-----------|--------|----------| +| 1 | Review PASS for exact deployed commit | PASS | crn-x81gk's verdict cites `d5d7b1e1fb05d32a24e44b166b2252f8b6c363e2`, identical to the deploy bead's `metadata.gc.deploy_commit` — exact SHA match (D = R). Independently re-resolved both sides this session via `git rev-parse --verify --quiet "^{commit}"`, not trusted as transcribed strings. | +| 2 | Acceptance criteria met | PASS | Verified directly against crn-rott9.3's full `exit_contract` (5 bullets: live-repro, check-evw98.2-first, close-if-resolved-by-dependency, implement-if-not, record-evidence-either-way). Builder's own notes confirm `evw98.2` was not merged and is mechanistically unrelated, so the "implement fix" branch correctly applied, with live-repro evidence recorded. | +| 3 | Tests pass | PASS | Independently re-run twice: first against the exact reviewed commit `d5d7b1e1` (detached checkout), then again against the actual deploy commit `a6173d9c` after the self-rebase (criterion 6) — not trusting the reviewer's report alone. Both: `go build ./...` exit 0; `go test ./... -race -count=1` — **7/7 packages ok** (`.`, `cmd`, `formulas`, `internal/cairn`, `internal/critic`, `internal/obslog`, `scripts`). Diff-owned tests re-run individually by exact name at `d5d7b1e1`: `TestStatusServesStaleTitleSummaryAfterFileOnlyWriteback` (new) and `TestWriteBackRetrievalMetadataPreservesBody` (modified — trivial ctx-param addition) — both PASS. 3a (attribute failing tests) not applicable, zero failures. 3b (policy/lint lane, per `.github/workflows/ci.yml`'s `lint` job), re-run at both SHAs with an isolated `GOLANGCI_LINT_CACHE` (avoiding known cross-session cache contamination on this machine): `golangci-lint run ./...` — 0 issues; `go vet ./...` exit 0; `golangci-lint fmt -d ./...` exit 0, no diff. | +| 4 | No open HIGH findings | PASS | crn-x81gk records exactly 1 finding, `[low, non-blocking]`. No open HIGH/blocking finding anywhere in the chain. | +| 5 | Clean tree | PASS | Verification performed via direct checkout of both `d5d7b1e1fb05d32a24e44b166b2252f8b6c363e2^{commit}` and, after the self-rebase, `a6173d9cc6d729748da07cf93bfb705b14045f2f^{commit}`; `git status --porcelain` empty throughout both. | +| 6 | Clean divergence from main | PASS | `git merge-tree --write-tree origin/main d5d7b1e1fb05d32a24e44b166b2252f8b6c363e2` → clean merge tree `68068d4e84afd929622533a2b8604e4204afb92d`, exit 0, zero conflicts. `d5d7b1e1`'s own history predates several now-merged `origin/main` commits (#125–#130), so per this formula's push-and-pr step a **bounded self-rebase onto `origin/main`** was performed, producing AFTER_SHA `a6173d9cc6d729748da07cf93bfb705b14045f2f`. Verified byte-identical in content to the precomputed clean-merge tree: `git rev-parse a6173d9c^{tree}` → `68068d4e84afd929622533a2b8604e4204afb92d`, exact match — confirms the rebase applied with zero fuzzy/conflicted resolution (rebase and merge onto the same target necessarily agree when conflict-free). Also confirmed `origin/main` is a strict ancestor of `a6173d9c` (no gap) via `git merge-base --is-ancestor origin/main a6173d9c`. See Process note 4 for how this was rediscovered and re-verified this session. | +| 7 | Single feature theme | PASS | `git diff --name-only origin/main a6173d9cc6d729748da07cf93bfb705b14045f2f` (post-rebase) → exactly the same 4 tightly-related files as pre-rebase (`cmd/backfill.go`, `cmd/backfill_test.go`, `internal/cairn/entry.go`, `internal/cairn/index_test.go`), +118/-8 lines — 2 source + 2 test, no unrelated changes. `assert_deploy_ancestry_scope crn-rott9.3 main ` exit 0 — both commits in range cite the build bead `crn-rott9.3`, no out-of-scope path. | + +## Verdict: PASS — proceeding to PR. + +## Process notes + +1. **Merge authority — self-merge authorized under the reinstated standing + authorization.** crn-bmy54's own notes carry the same generic "PR-based, + mayor-routed ... route the merge request to mayor" boilerplate seen on the + three recently-flagged reviewer-authored beads (`crn-e6pc7`/PR#118, + `crn-daxbq`/PR#119, `crn-y0caj`/PR#120). That recurrence pattern triggered + a temporary SUPERSEDED state on the fleet's + `cairn-auto-merge-requires-explicit-strategy` standing authorization on + 2026-08-19 (see `release-gates/crn-y0caj-gate.md`, which paused and routed + to mayor under that state) — but a fresh, explicit mayor ruling later the + same day (msg `gm-wisp-2yhv7u`, 2026-08-19 03:01:50) **REINSTATED** + deployer self-merge for quad341/cairn, naming and clearing all three PRs + directly. Root cause per mayor: quad341/cairn is not covered by `mpr` + (maintainer-pr-review) at all — the reviewer-authored "route through mpr" + boilerplate is a structural miscitation for this repo; the real choices + here were always deployer self-merge / mayor / operator. Mayor's ruling + explicitly instructs future sessions **not** to re-pause on a bare repeat + of this same boilerplate — only on a genuine, bead-specific objection + distinct from the recycled clause. crn-bmy54's notes raise nothing beyond + the recycled clause (the "do NOT push to `builder/crn-rott9.3` directly" + instruction is branch-target guidance, addressed in note 2 below, not a + merge-authority objection). Proceeding under the STANDING AUTHORIZATION + per mayor's explicit instruction, subject to the reinstatement's four + conditions: (1) gate 7/7 PASS — this table; (2) PR state confirmed via a + direct, fresh `gh pr view --json` read at merge time, not reused/stale + data; (3) CLEAN/MERGEABLE with both required checks (`build-test`, + `lint`) COMPLETED/SUCCESS; (4) no `--auto` arm — plain + `gh pr merge --squash` only, then independently verified via a fresh + re-read (`state=MERGED`, `mergedAt` non-null) per the FR-03 discipline, + never trusting a bare exit 0. + +2. **Branch target:** `builder/crn-rott9.3` is provenance-only per the + deploy bead's own instruction — a possibly shared builder branch, not a + push target. `deploy/crn-bmy54-gate` was cut fresh from the exact + reviewed SHA (`d5d7b1e1fb05d32a24e44b166b2252f8b6c363e2`). + +3. **SHA integrity:** the deploy commit was independently re-resolved via + `git rev-parse --verify --quiet "^{commit}"` on both the D (deploy + bead metadata) and R (review bead metadata) sides before comparison, per + the sha-integrity discipline — never trusted as an eyeballed or + transcribed string. + +4. **Branch-anomaly investigation.** Before pushing, `git ls-remote origin + refs/heads/deploy/crn-bmy54-gate` unexpectedly returned `a6173d9c`, not the + locally-expected `d5d7b1e1`, with no ancestor relationship either + direction — raising the possibility of foreign/clobbered state per the + "investigate before overwriting" principle, so no push was attempted until + this was fully characterized. Root cause, confirmed via `git reflog`: this + same local worktree had already, earlier this session (07:14:10), run the + bounded self-rebase referenced in criterion 6 (`rebase (start): checkout + origin/main` → picked the 3 crn-rott9.3 commits → `rebase (finish): + returning to refs/heads/deploy/crn-bmy54-gate`, landing on `a6173d9c`) and + pushed it, before switching to a detached checkout of the pristine + `d5d7b1e1` (09:35:42) to gather the criterion-3 evidence above — i.e. this + was this session's own completed work-in-progress, interrupted before `gh + pr create`, not foreign or unrelated content. Confirmed via three + independent checks before relying on it: (a) `a6173d9c`'s tree hash + exactly matches the precomputed clean-merge tree (criterion 6); (b) + `origin/main` is a strict ancestor of `a6173d9c`, and `a6173d9c`'s diff + against `origin/main` touches exactly the same 4 files as the reviewed + commit, same diffstat; (c) `assert_deploy_ancestry_scope` passes clean + against it. `gh pr list --head deploy/crn-bmy54-gate --state all` returned + `[]` throughout — no PR had been opened from either SHA. Proceeded by + resetting the local branch to the already-pushed `a6173d9c` (via + `resolve_deploy_branch_target`) and building this commit on top of it, + rather than re-doing the rebase or force-pushing over it.