Skip to content

feat(commit): let a GitTarget frame a save request's message - #364

Merged
sunib merged 3 commits into
fix/watch-reconnect-severityfrom
feat/commit-request-message-composition
Sep 15, 2026
Merged

sunib merged 3 commits into
fix/watch-reconnect-severityfrom
feat/commit-request-message-composition

Conversation

@sunib

@sunib sunib commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Stacked on #363. Base is fix/watch-reconnect-severity; review this PR's own commit only. Merge #363 first and this retargets to main cleanly.

Came out of the question "does CommitRequest.spec.message support templating, and should it?" — no, and no. But the question points at a real gap, and the fix runs the other way round.

The gap

A save request's message replaces the commit template rather than feeding it, so supplying one costs the resource body liveTemplate would have produced. "Why I saved" and "what was saved" are mutually exclusive today: the commit states the reason and stops naming the resources.

Why not just template the request

Three reasons, strongest last.

Wrong trust boundary. liveTemplate/reconcileTemplate live on the GitTarget, which an operator owns, and describe house style for the org's Git history. A CommitRequest is created by whoever holds create on it in a namespace — the save-button population, deliberately much wider. That set supplying message content is the design; that set defining formatting policy is not.

It breaks the literal contract, which exists for auditability. The spec is immutable (self == oldSelf), validation never truncates, surrounding spaces are preserved. Every one of those says the same thing: what you typed is what lands in Git, so the object and the commit agree byte for byte.

Decisively: it could not be validated. Commit templates run with missingkey=error, and ValidateCommitConfig exists to render every template against sample data at GitTarget admission precisely because:

these templates run with missingkey=error, so {{.Labels.team}} fails for a resource that does not carry "team", and failing at admission is the difference between a rejected GitTarget and a commit that dies mid-window months later.

A CommitRequest cannot be given that. Syntax could be checked at admission, but a runtime fault surfaces at finalize — and there the blast radius is not the request, it is the whole window, including other authors' retained events. The spec is immutable, so nobody can repair the request that killed it.

The inversion

Don't template the request. Let the operator's template see the request's message.

spec:
  commit:
    message:
      requestTemplate: |-
        {{.RequestMessage}}

        {{range .Resources -}}
        - [{{.Operation}}] {{.APIVersion}}/{{.Resource}}/{{.Namespace}}/{{.Name}}
        {{end -}}
Input, in precedence order Message source
Request message and requestTemplate set requestTemplate, with .RequestMessage
Request message, no requestTemplate Exact supplied text (unchanged)
Live window of any size liveTemplate
Atomic snapshot or resync reconcileTemplate

Fully backward compatible: omit requestTemplate and nothing moves.

Why an explicit field rather than just exposing .RequestMessage

The cheaper-looking alternative is to add .RequestMessage to the live context, add no API field, and let people write {{if .RequestMessage}}…{{end}} inside liveTemplate.

It doesn't work. Today a request message skips the template entirely, so making liveTemplate see it means no longer skipping — and then any existing target whose liveTemplate doesn't mention .RequestMessage would silently drop the user's save message. That is a worse regression than the gap being closed. requestTemplate being set is the opt-in signal, and it is self-documenting.

A requestTemplate that drops the message is rejected

Without this rule the design reintroduces the exact fault the alternative above was rejected for, moved one level up: a target could set requestTemplate: "chore: sync {{.Count}} resources" and every save message would silently vanish while the commit still counted as request-sourced.

The check renders the template and looks for a probe in the output, rather than scanning the source. A scan for the literal {{.RequestMessage}} would reject {{.RequestMessage | printf "%s"}}, a variable assignment, and every other legitimate spelling; the probe accepts all of them for the right reason — the message reached the commit. Covered in both directions by tests.

Residual, stated plainly: sample execution cannot prove every branch, so a template that drops the message only under {{if eq .Count 1}} still passes. That is the caveat docs/configuration.md already documents for the existing templates, now extended to this one.

Both live templates share one sample set

liveTemplate and requestTemplate are validated against the same window shapes.

This was not the first implementation, and the tests caught it: validating requestTemplate against a single sample was not enough, because sampleLabeledObject() carries a team label. A template reading .Labels.team passed admission and would have fallen back at runtime for every resource lacking it — exactly the failure admission exists to prevent. They face identical windows in production; checking one more thoroughly than the other only changes which template fails late.

Render failure falls back, and is counted

A requestTemplate that fails at finalize commits the request's message verbatim rather than losing the window. This deviates from liveTemplate, where a render failure fails the write, and the deviation is the point: a correct, non-lossy answer is always in hand here because the literal already passed ValidateLiteralCommitMessage at admission. Failing would discard the requester's save and everyone else's retained events to punish a formatting mistake.

framedRequestMessage returns no error at all, and cannot — "this failed" is not an outcome it can report. (unparam pointed this out; the signature now states it rather than the linter being suppressed.)

A fallback is a successful commit, so nothing is refused and no condition moves. Without a counter, "the template works" and "the template silently stopped applying" read identically. So git_commits_total gains two message_source values:

Value Meaning
commit_request Verbatim, no requestTemplate configured. Unchanged, so existing dashboards keep their meaning.
commit_request_framed requestTemplate rendered.
commit_request_fallback requestTemplate failed; the literal was committed.

rate(…fallback) > 0 is the alert and framed / (framed + fallback) the health ratio, both in docs/interpreting-metrics.md.

This splits from commit_request even though the text originated with the request either way. The existing messageResolution.label() comment groups by how text is produced (which is why resync and atomic share reconcile) — but a fallback is not a production detail, it is the operator's configured policy not having applied, which is a different question and one an operator actually asks.

The source is stamped onto the write at commit time the same way CommitSHA already is, because publishCommitsForPush runs after the push and recomputing the source from the write cannot know a render failed earlier.

Considered and not done: a GitTarget condition

The seam exists (commitFailureRefused is surfaced as a condition). Left out on proportionality: the marker check plus the shared sample renders now catch syntax faults, missingkey faults, and the drop-the-message mistake at admission, where the operator is already looking. What survives to runtime is a narrow residual, mirroring is unaffected, and commits keep flowing. If commit_request_fallback ever shows real traffic, that is the evidence for promoting it — noted in the code rather than left as a decision made once and forgotten.

Incidental finding

CommitMessageSpec's doc comment claims the "identically-shaped GitProvider.spec.commit.message is retained only to reject a manifest that still sets it there". There is no such fieldCommitMessageSpec is embedded only by GitTargetCommitSpec, and GitProvider.spec.commit carries just Committer and Signing. The generated CRD diff confirms it: requestTemplate lands on gittargets only. Left alone as out of scope; flagged so the next reader doesn't trust the comment.

Testing

internal/git/request_template_test.go, plus signature updates to existing callers:

  • Composition renders reason + resource body.
  • Backward compatibility: no requestTemplate commits the literal byte for byte, leading and trailing spaces included — the test that guards the CommitRequest contract.
  • Render failure falls back, and reports commit_request_fallback.
  • messageSource keeps plain / framed / fallback distinct, including the commit-time stamp.
  • Precedence: requestTemplate outranks the verbatim arm; a window with no request message is untouched.
  • Rejection of a message-dropping template, of an unparseable one, and of one reading a label not every resource carries.
  • Acceptance of every honest spelling — direct, piped, via variable, guarded, prefixed — which is what proves the probe mechanism was the right choice over a source scan.
  • requestTemplate has no built-in default, and the overlay trims like the others.

Full gate green: fmt / generate / manifests / vet / lint / test (coverage holds at the 78.0% baseline) / test-e2e (85 passed, 0 failed).

Flake found and fixed along the way

The first local e2e run failed Manager Unsupported Folder Refusal at unsupported_folder_e2e_test.go:108, inside the shared seedRenderedFolderIntoRepo helper:

git push origin HEAD:main: remote: error: cannot lock ref 'refs/heads/main':
  is at 80cae2ab... but expected ea92102...
 ! [remote rejected] HEAD -> main (incorrect old value provided)

That is the test's own push, rejected by compare-and-swap. The spec brings the GitTarget to healthy and actively mirroring, then pushes straight into the same branch via a helper that does fetchreset --hardcommitpush with no retry. Any controller commit landing in that window rejects the seed push.

Confirmed pre-existing, not caused by this change, by CI rather than by argument: the full-manager leg — which runs label: "manager", exactly this spec's label — passed on d6d84aa8, the feature commit without the race fix. Same spec, same code, clean cluster. The supporting reasoning also holds: requestTemplate appears nowhere in the e2e suite or fixtures, so resolveMessage() takes an identical arm for every e2e commit and messages are byte-identical.

Fixed rather than left, because the helper is shared by six spec files, so one unretried push was a latent flake source for all of them. Each attempt now rebuilds the commit on the new tip. Two details:

  • The attempt returns errors rather than asserting. An Expect inside Eventually would fail the spec on the first lost race, which is precisely what the retry exists to survive.
  • A retry landing on a tip that already carries identical content stages nothing, and git commit with nothing staged is an error. That case is success: the fixture is in Git, which is all the caller asked for.

Review round (second commit)

  • The framed arm now validates the literal message. It previously went straight to the render, skipping ValidateLiteralCommitMessage. The literal reaches Git on both outcomes of that arm — framed on success, bare on the fallback — so framing must not become a route that smuggles a message past the check the verbatim arm enforces. Not reachable through the normal flow (the controller validates earlier); it is the invariant that stops holding once a second producer of PendingWrite exists. Tested on both arms together, so framing cannot change what is accepted.
  • The CRD description is back to contract shape. Rationale moved above the doc comment, separated by the blank line that keeps controller-gen from putting it in the schema, per AGENTS.md. Verified as that section prescribes — regenerated to a scratch dir and compared with every description stripped: zero structural diffs, so no +kubebuilder: marker was displaced.
  • RequestMessage removed from the liveTemplate field list. It is always empty there, because liveTemplate only runs for windows with no request message. The docs now say so, so nobody reaches for {{if .RequestMessage}} and waits for it to fire.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added optional request-message templates for framing save commit messages with resource data.
    • Request messages remain literal and are preserved verbatim when no template is configured.
    • Templates must include the request message; rendering failures fall back to the original message.
  • Metrics

    • Added message-source metrics distinguishing framed messages and fallback commits.
  • Documentation

    • Documented configuration, validation, fallback behavior, and monitoring guidance.
  • Bug Fixes

    • Improved end-to-end repository seeding by retrying pushes affected by concurrent commits.

@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 6e1ece2b-b78b-4bfe-acf1-147cd844bc1f

📥 Commits

Reviewing files that changed from the base of the PR and between 7970f7a and 21b4a81.

📒 Files selected for processing (17)
  • api/v1alpha3/gitprovider_types.go
  • config/crd/bases/configbutler.ai_gittargets.yaml
  • docs/UPGRADING.md
  • docs/configuration.md
  • docs/interpreting-metrics.md
  • docs/spec/commitrequest-design.md
  • internal/git/commit.go
  • internal/git/commit_executor.go
  • internal/git/commit_executor_test.go
  • internal/git/commit_metadata_fields_test.go
  • internal/git/literal_message_test.go
  • internal/git/open_window.go
  • internal/git/pending_writes.go
  • internal/git/request_template_test.go
  • internal/git/resync_flush.go
  • internal/git/types.go
  • test/e2e/inplace_edit_e2e_test.go

📝 Walkthrough

Walkthrough

The change adds optional requestTemplate support for framing literal CommitRequest messages, validates and renders the template, records framed and fallback message sources, updates documentation and tests, and makes e2e fixture pushes retry concurrent updates.

Changes

Request message framing

Layer / File(s) Summary
Message contract and data flow
api/v1alpha3/gitprovider_types.go, config/crd/bases/configbutler.ai_gittargets.yaml, internal/git/types.go, internal/git/open_window.go
Adds requestTemplate to the API and CRD. Carries the unchanged request message as RequestMessage.
Template validation and rendering
internal/git/commit.go
Validates request templates with generated samples and fresh probes. Renders request messages through the configured template.
Commit resolution and observability
internal/git/commit_executor.go, internal/git/pending_writes.go, internal/git/resync_flush.go, internal/git/*_test.go
Tracks framed and fallback resolutions, preserves literal validation, records commit-time sources, and tests request-template behavior and existing write paths.
Configuration and upgrade documentation
docs/configuration.md, docs/UPGRADING.md, docs/interpreting-metrics.md, docs/spec/commitrequest-design.md
Documents requestTemplate behavior, validation, fallback handling, and new message_source labels.

E2E fixture push retry

Layer / File(s) Summary
Fixture push retry
test/e2e/inplace_edit_e2e_test.go
Retries fixture pushes after refetching and resetting to the current remote branch. Empty staged changes complete successfully without a commit.

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant CommitRequest
  participant BranchWorker
  participant requestTemplate
  participant GitCommit
  CommitRequest->>BranchWorker: provide literal message
  BranchWorker->>requestTemplate: render with RequestMessage and live data
  requestTemplate->>BranchWorker: return framed message or render error
  BranchWorker->>GitCommit: commit framed message or verbatim fallback
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/commit-request-message-composition

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.13514% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/git/commit_executor.go 70.9% 9 Missing ⚠️
internal/git/commit.go 94.1% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

sunib and others added 2 commits September 15, 2026 08:33
A CommitRequest's message REPLACED the commit template rather than feeding it,
so supplying one cost the resource body liveTemplate would have produced. "Why I
saved" and "what was saved" were mutually exclusive: the commit said the reason
and stopped naming the resources.

The obvious fix is to template `CommitRequest.spec.message`, and it is the wrong
one. That spec is immutable, its bytes are committed verbatim, and nothing parses
them, which together are what make a request auditable against the commit it
produced. Templating it would also cross a trust boundary: a GitTarget is
operator-owned, while a CommitRequest is created by whoever holds create on it in
a namespace.

Decisively, it could not be validated. Commit templates run with
missingkey=error, and ValidateCommitConfig exists to render every one of them
against sample data at GitTarget admission precisely because "failing at
admission is the difference between a rejected GitTarget and a commit that dies
mid-window months later". A CommitRequest gets no such protection: a runtime
fault surfaces at finalize, where it costs the whole window (other authors'
retained events included), on an immutable object nobody can repair.

So it is inverted. The operator's template sees the request's message instead.
`GitTarget.spec.commit.message.requestTemplate` renders for a window a request
attached to, with the message arriving as `.RequestMessage` and committed
unaltered. Omit it and behaviour is byte-for-byte what it was.

Three properties make it safe rather than merely convenient:

  - The request is still never parsed. Nothing a requester writes is executed;
    they own the content, the operator owns the wording around it.

  - A requestTemplate that never renders `.RequestMessage` is REJECTED. Without
    this the feature reintroduces the exact fault it was designed around — a
    target could frame the reason away entirely and the commit would still count
    as request-sourced. The check renders the template and looks for a probe in
    the OUTPUT rather than scanning the source, so `{{.RequestMessage}}`, a
    pipeline, and a variable all pass for the right reason: the message reached
    the commit.

  - Both live templates are now validated against ONE shared set of window
    shapes. Validating requestTemplate against a single sample was not enough and
    the tests caught it: the labelled sample carries `team`, so a template
    reading that label passed admission and would have fallen back at runtime for
    every resource without it. They face identical windows in production;
    checking one more thoroughly than the other only moves which template fails
    late.

A requestTemplate that fails to render at finalize commits the request's message
verbatim rather than losing the window. That deviates from liveTemplate, where a
render failure fails the write, and the deviation is the point: here a correct,
non-lossy answer is always in hand because the literal already passed admission,
so failing would discard the requester's save and everyone else's retained events
to punish a formatting mistake. framedRequestMessage returns no error at all, and
cannot — "this failed" is not an outcome it can report.

That fallback is a SUCCESSFUL commit, so nothing is refused and no condition
moves, which would leave "the template works" and "the template silently stopped
applying" indistinguishable. git_commits_total therefore gains
`commit_request_framed` and `commit_request_fallback`; `commit_request` keeps its
meaning, so existing dashboards are unaffected. The source is stamped onto the
write at commit time the way CommitSHA already is, because publishCommitsForPush
runs after the push and recomputing it from the write cannot know a render
failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… framing

Review follow-ups, plus the flake the first e2e run exposed.

seedRenderedFolderIntoRepo pushed straight into a branch the controller is
already mirroring, with no retry. It reads the remote tip, builds a commit on it
and pushes under the remote's compare-and-swap, so a controller commit landing in
that window rejects it:

    ! [remote rejected] HEAD -> main (incorrect old value provided)

That is the test's own push failing, not the controller, and losing the race says
nothing about the behaviour under test. Each attempt now rebuilds the commit on
the new tip. The helper is shared by six spec files, so one unretried push was a
flake source for all of them; it surfaced as the unsupported-folder spec failing
locally while the same spec passed on CI's full-manager leg for the same commit.

The attempt returns errors rather than asserting: an Expect inside Eventually
would fail the spec on the first lost race, which is exactly what the retry is
there to survive. A retry landing on a tip that already carries the identical
content stages nothing, and `git commit` with nothing staged is an error — that
case is success, because the fixture is in Git, which is all the caller asked.

Three review points on the framing change itself:

  - The requestTemplate arm now validates the literal message too, instead of
    going straight to the render. The literal reaches Git on BOTH outcomes of
    that arm (framed on success, bare on the fallback), so framing must not
    become a route that smuggles a message past the check the verbatim arm
    enforces. The controller validates earlier, so this is not reachable through
    the normal flow today — it is the kind of invariant that stops holding the
    moment a second producer of PendingWrite appears. Tested on both arms
    together, so framing cannot change what is accepted.

  - The CRD field description is back to the contract: when it renders, what
    fields it gets, that the message is literal, and what omitting or dropping it
    does. The rationale moves to a block above the doc comment, separated by the
    blank line that keeps controller-gen from putting it in the schema
    (AGENTS.md). Verified the way that section prescribes: regenerated to a
    scratch dir and compared with every description stripped — zero structural
    diffs, so no kubebuilder marker was displaced.

  - docs/configuration.md listed RequestMessage among liveTemplate's fields,
    which is misleading: it is always empty there, because liveTemplate only runs
    for windows that have no request message. Moved to requestTemplate, with the
    reason stated so nobody reaches for {{if .RequestMessage}} and waits for it
    to fire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sunib
sunib force-pushed the feat/commit-request-message-composition branch from ae2252f to bc88b0b Compare September 15, 2026 08:33
@sunib

sunib commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Auto-review was skipped here because this PR is stacked on fix/watch-reconnect-severity (#363) rather than main. Requesting one explicitly.

@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== PR metadata =='
gh pr view 364 --repo ConfigButler/gitops-reverser --json number,title,baseRefName,headRefName,commits,files --jq '{number,title,baseRefName,headRefName,commits: [.commits[] | {oid,messageHeadline}], files: [.files[] | {path,additions,deletions}]}'

echo '== Diff against PR base branch =='
git diff --stat origin/fix/watch-reconnect-severity...HEAD
git diff --name-only origin/fix/watch-reconnect-severity...HEAD

echo '== Changed Go symbols =='
fd -e go . | xargs -r ast-grep outline --items all --type function,method,struct,interface 2>/dev/null | head -n 400

echo '== Diff =='
git diff --find-renames --find-copies --unified=80 origin/fix/watch-reconnect-severity...HEAD

Length of output: 3022


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="ConfigButler/gitops-reverser"
base_sha=$(gh pr view 364 --repo "$repo" --json baseRefOid --jq .baseRefOid)
head_sha=$(gh pr view 364 --repo "$repo" --json headRefOid --jq .headRefOid)
echo "base_sha=$base_sha"
echo "head_sha=$head_sha"

echo '== API comparison file list =='
gh api "repos/$repo/compare/$base_sha...$head_sha" --paginate --jq '.files[] | [.filename, .status, .additions, .deletions] | `@tsv`'

echo '== API comparison patch =='
gh api "repos/$repo/compare/$base_sha...$head_sha" --paginate --jq '.files[] | "\n===== \(.filename) =====\n\(.patch // "(binary or no patch)")"'

echo '== Go source outlines for changed implementation files =='
for f in api/v1alpha3/gitprovider_types.go internal/git/commit.go internal/git/commit_executor.go internal/git/open_window.go internal/git/pending_writes.go internal/git/resync_flush.go internal/git/types.go internal/git/request_template_test.go test/e2e/inplace_edit_e2e_test.go; do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    ast-grep outline "$f" --items all --type function,method,struct,interface || true
  fi
done

Length of output: 71139


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Request-template implementation =='
sed -n '100,245p' internal/git/commit.go
sed -n '60,285p' internal/git/commit_executor.go
sed -n '760,825p' internal/git/types.go
sed -n '1,115p' internal/git/pending_writes.go

echo '== Validation call sites and CommitMessage producers =='
rg -n -C 4 --type go 'ValidateCommitConfig\s*\(|CommitMessage:' api internal test -g '!**/*_test.go'
rg -n -C 4 --type go '\.CommitMessage\s*=|CommitMessage\s*:' internal -g '!**/*_test.go'

echo '== Commit publication and message source =='
rg -n -C 8 --type go '\bpublishCommitsForPush\s*\(|\bmessageSource\s*\(' internal/git

echo '== Relevant API validation =='
rg -n -C 8 --type go 'CommitMessageSpec|requestTemplate|ValidateCommit' api internal/controller* internal -g '!**/*_test.go' | head -n 500

Length of output: 86264


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Commit modes and WriteRequest producers =='
rg -n -C 6 --type go 'CommitMode(?:\s*:|\s*=)|WriteRequest\s*\{' . -g '!**/*_test.go'

echo '== Atomic pending-write and enqueue routing =='
rg -n -C 10 --type go '\bbuildAtomicPendingWrite\s*\(|\bbuildGroupedPendingWrite\s*\(|\bEnqueueRequest\s*\(|\benqueueRequest\s*\(' internal -g '!**/*_test.go'

echo '== CommitRequest attachment and effective message =='
sed -n '1030,1120p' internal/git/branch_worker.go
rg -n -C 8 --type go 'effectiveMessage|pendingMessage|CommitRequest' internal/git/branch_worker.go internal/controller -g '!**/*_test.go'

echo '== Existing tests with atomic message overrides =='
rg -n -C 6 --type go 'PendingWriteAtomic|CommitModeAtomic|CommitMessage:' internal -g '*_test.go' | head -n 800

Length of output: 143363


@sunib I reviewed the changes relative to #363.

I found two blocking issues:

  1. requestTemplate validation can be bypassed by a template that emits the fixed requestTemplateProbe literal without rendering .RequestMessage. Use a fresh unpredictable probe for each validation, and add a rejection test for a hard-coded probe.
  2. resolveMessage() applies requestTemplate to atomic writes with WriteRequest.CommitMessage. This changes the documented atomic snapshot path from reconcileTemplate to requestTemplate. Restrict the framed path to grouped CommitRequest writes.
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Two holes in the requestTemplate work, both reported by CodeRabbit on #364.

The validation probe was a package constant, so the check could be satisfied
without doing the thing it verifies: a template emitting that literal string
would pass while never referencing .RequestMessage. Nobody writes that on
purpose, but the whole argument for rejecting a message-dropping template was
that the guarantee must hold by construction rather than by good intentions, and
a spoofable sentinel does not carry that. The probe is now minted per validation
from crypto/rand, so rendering the message is the only way to pass.

Framing was bound to "carries a message" rather than to the kind. A CommitRequest
attaches its message to an open live window, which finalizes as
PendingWriteCommit — but an atomic snapshot can carry a message too
(WriteRequest.CommitMessage), and it was being framed with a live context written
for save requests, rerouting the documented atomic path through the wrong
template. Not reachable in production today, since the only WriteRequest producer
hardcodes an empty message and EnqueueRequest has no production callers; that is
exactly the kind of invariant that stops holding the moment a second producer
appears, which is the same reason the literal validator was added to this arm
earlier. An atomic write with a message now keeps the behaviour it had before
requestTemplate existed: committed verbatim.

Tests cover both: a template faking either probe shape is rejected, successive
probes differ, and atomic/resync writes resolve exactly as they did before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sunib
sunib added this pull request to stack #365 September 15, 2026 09:38
@sunib
sunib merged commit 65abf54 into main Sep 15, 2026
19 of 20 checks passed
@sunib
sunib deleted the feat/commit-request-message-composition branch September 15, 2026 09:38
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