From 1c07546e386b6c9fabb9bc787b457a94d932a269 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Fri, 21 Aug 2026 05:54:54 +0100 Subject: [PATCH 1/2] Commit only a minimal review attestation; keep full receipt evidence local and make the review skill report-only --- .agents/skills/self-review-solve/SKILL.md | 2 +- .agents/skills/self-review/SKILL.md | 2 +- .claude/skills/self-review-solve/SKILL.md | 2 +- .claude/skills/self-review/SKILL.md | 2 +- .cursor/skills/self-review-solve/SKILL.md | 2 +- .cursor/skills/self-review/SKILL.md | 2 +- .../supervisory-review-program.receipt.json | 276 ------------------ boatstack/cmd/boatstack-reviewer/main.go | 24 +- .../cmd/boatstack-reviewer/reviewer_test.go | 153 +++++++--- boatstack/cmd/boatstack-reviewer/seal.go | 167 +++++++++-- docs/self-review.md | 60 ++-- .../2026-08-21-minimal-review-attestation.md | 3 + skills/self-review-solve/SKILL.md | 5 +- skills/self-review-solve/main.go | 11 +- skills/self-review/SKILL.md | 11 +- skills/self-review/main.go | 25 +- 16 files changed, 357 insertions(+), 390 deletions(-) delete mode 100644 .github/reviews/supervisory-review-program.receipt.json create mode 100644 release-notes/2026-08-21-minimal-review-attestation.md diff --git a/.agents/skills/self-review-solve/SKILL.md b/.agents/skills/self-review-solve/SKILL.md index 51168f8..a86bd2b 100644 --- a/.agents/skills/self-review-solve/SKILL.md +++ b/.agents/skills/self-review-solve/SKILL.md @@ -3,7 +3,7 @@ name: self-review-solve description: "Resolve the Boatstack self-review: fix open findings or run a fresh review, converge the loop, and seal the receipt." --- - + This adapter exposes the canonical Yield workflow at `skills/self-review-solve`. Read its SKILL.md, then run from the repository root: diff --git a/.agents/skills/self-review/SKILL.md b/.agents/skills/self-review/SKILL.md index c73c32c..b710019 100644 --- a/.agents/skills/self-review/SKILL.md +++ b/.agents/skills/self-review/SKILL.md @@ -3,7 +3,7 @@ name: self-review description: "Run the Boatstack supervisory-control self-review for the current branch and report the verdict without changing code." --- - + This adapter exposes the canonical Yield workflow at `skills/self-review`. Read its SKILL.md, then run from the repository root: diff --git a/.claude/skills/self-review-solve/SKILL.md b/.claude/skills/self-review-solve/SKILL.md index 51168f8..a86bd2b 100644 --- a/.claude/skills/self-review-solve/SKILL.md +++ b/.claude/skills/self-review-solve/SKILL.md @@ -3,7 +3,7 @@ name: self-review-solve description: "Resolve the Boatstack self-review: fix open findings or run a fresh review, converge the loop, and seal the receipt." --- - + This adapter exposes the canonical Yield workflow at `skills/self-review-solve`. Read its SKILL.md, then run from the repository root: diff --git a/.claude/skills/self-review/SKILL.md b/.claude/skills/self-review/SKILL.md index c73c32c..b710019 100644 --- a/.claude/skills/self-review/SKILL.md +++ b/.claude/skills/self-review/SKILL.md @@ -3,7 +3,7 @@ name: self-review description: "Run the Boatstack supervisory-control self-review for the current branch and report the verdict without changing code." --- - + This adapter exposes the canonical Yield workflow at `skills/self-review`. Read its SKILL.md, then run from the repository root: diff --git a/.cursor/skills/self-review-solve/SKILL.md b/.cursor/skills/self-review-solve/SKILL.md index 51168f8..a86bd2b 100644 --- a/.cursor/skills/self-review-solve/SKILL.md +++ b/.cursor/skills/self-review-solve/SKILL.md @@ -3,7 +3,7 @@ name: self-review-solve description: "Resolve the Boatstack self-review: fix open findings or run a fresh review, converge the loop, and seal the receipt." --- - + This adapter exposes the canonical Yield workflow at `skills/self-review-solve`. Read its SKILL.md, then run from the repository root: diff --git a/.cursor/skills/self-review/SKILL.md b/.cursor/skills/self-review/SKILL.md index c73c32c..b710019 100644 --- a/.cursor/skills/self-review/SKILL.md +++ b/.cursor/skills/self-review/SKILL.md @@ -3,7 +3,7 @@ name: self-review description: "Run the Boatstack supervisory-control self-review for the current branch and report the verdict without changing code." --- - + This adapter exposes the canonical Yield workflow at `skills/self-review`. Read its SKILL.md, then run from the repository root: diff --git a/.github/reviews/supervisory-review-program.receipt.json b/.github/reviews/supervisory-review-program.receipt.json deleted file mode 100644 index feb4e05..0000000 --- a/.github/reviews/supervisory-review-program.receipt.json +++ /dev/null @@ -1,276 +0,0 @@ -{ - "schema_version": 1, - "instance": "supervisory-review-program", - "program": { - "id": "boatstack-reviewer", - "version": "1", - "fingerprint": "76680abd47f9e920ebfe807d3b3f226cbb48bd1a531b0a0a068cd27fd3e6f6e6" - }, - "policy": { - "prompt_path": ".github/codex/review-prompt.md", - "prompt_sha256": "66c4c7111f200de489f4fed0852b261e36cbd1e962c61320c1e4ea2dd2641986", - "schema_path": ".github/codex/review-output-schema.json", - "schema_sha256": "113b02c5cca93156692031c3dedd038e5c2b80cbc4a56337ce06e536110e3e6a", - "max_rounds": 16, - "stall_window": 3, - "weights": [ - 1000, - 100, - 10, - 1 - ] - }, - "base_ref": "origin/main", - "merge_base": "fffbab0e806fe87cd16323a708bf696c0cfceca3", - "head_commit": "fec374ddf36c0a1e2375c5e1f31eba88d3bfb4b1", - "reviewed_tree": "3aa9458d3bdfd48e74b70fc111883c2582489c31", - "generation": 3, - "rounds": [ - { - "generation": 3, - "index": 1, - "candidate_fingerprint": "66de8c7b85be8ff74b99df18518ee01dd9a9320c656b35f7bc62ddee7e44ebfc", - "reviewed_tree": "3aa9458d3bdfd48e74b70fc111883c2582489c31", - "head_commit": "fec374ddf36c0a1e2375c5e1f31eba88d3bfb4b1", - "merge_base": "fffbab0e806fe87cd16323a708bf696c0cfceca3", - "verdict": "patch is correct", - "measure": 0, - "finding_count": 0, - "priorities": [ - 0, - 0, - 0, - 0 - ], - "transition": "review.converge" - } - ], - "final_review": { - "findings": [], - "overall_correctness": "patch is correct", - "overall_explanation": "Round over fffbab0e..fec374dd. The delta since the last converged round is the sealed generation-2 receipt plus a .gitattributes pin declaring .github/codex/** and .github/reviews/*.receipt.json as text eol=lf. The pin closes a real cross-platform admission defect surfaced by CI: on Windows runners (core.autocrlf=true) the checkout materialized the policy assets with CRLF, so the seal-time worktree policy hashes (092a1752/d68222f2, verified equal to the CRLF variants) could never match the base-revision admitted blobs (66c4c711/113b02c5, the LF bytes), failing verification in TestLoopConvergesSealsAndVerifies and TestVerificationRejectsTamperingForgeryAndBypass. Adjacent-case pass: every committed blob for the pinned paths is already LF, so adding the attribute triggers no renormalization and changes no committed hash on any platform; the scratch-repo fixtures copy the checked-out assets, which now materialize as LF everywhere, and scratch git-show reads raw blobs, so worktree and revision admission agree; sealed receipts are written with LF and the receipt directory remains excluded from the reviewed tree, so the pin cannot invalidate an existing receipt. No new defect introduced by this delta was established. Model-level verification before merge: not recommended.", - "overall_confidence_score": 0.85 - }, - "control_state": { - "mode": "converged", - "revision": 15 - }, - "kernel_receipts": [ - { - "schema_version": 3, - "id": "rcp-e6d32467705e31011c25a975b966aafaa14dc02300ed96bccfe443edf50efb42", - "instance_id": "supervisory-review-program", - "prescription_id": "prx-b0bd59f4bd3768e4fe65ebf2c0b92280a8577f25c9617a7b4e6d7bd129e4422a", - "program": { - "id": "boatstack-reviewer", - "version": "1", - "fingerprint": "76680abd47f9e920ebfe807d3b3f226cbb48bd1a531b0a0a068cd27fd3e6f6e6" - }, - "transition_id": "review.findings.record", - "prior_state_revision": 1, - "attempt_state_revision": 2, - "result_state_revision": 3, - "authority_fingerprint": "e1f9d0143447daa7e9a3d11d7497659642b964a06018d671855c4496afbb30f2", - "capabilities": [ - "review.submit" - ], - "effects": [ - { - "facet": "review.round", - "operation": "review.findings.record", - "fingerprint": "c1e46f7e821c4a362181525ddc7cb015741867a179ad372b574c916cc7029476" - } - ], - "prior_observation": "9552cd99ee5c3a388a72f838b89a33a20d740c4089ee9051bb0ff8c9e3f71e92", - "result_observation": "459317767812b8de53fd8871cc1d4dea51ce827874deb4ff3edcef6c4ad71691", - "verification": "satisfied", - "committed_at": "2026-08-21T01:43:08.248706Z" - }, - { - "schema_version": 3, - "id": "rcp-ce98819998897d509f34460ab279d8f0890e0874bc4bab0db6be3f964fdf723d", - "instance_id": "supervisory-review-program", - "prescription_id": "prx-81b9b269e4dbd8656e7865f8990a90b81fc561e0544b818e7c16fe9830fc1edc", - "program": { - "id": "boatstack-reviewer", - "version": "1", - "fingerprint": "76680abd47f9e920ebfe807d3b3f226cbb48bd1a531b0a0a068cd27fd3e6f6e6" - }, - "transition_id": "review.converge", - "prior_state_revision": 3, - "attempt_state_revision": 4, - "result_state_revision": 5, - "authority_fingerprint": "2c120eaf9382cfe382abda5b83c3b5b95548e844c3e404b9905976dc35bdeeb5", - "capabilities": [ - "review.submit" - ], - "effects": [ - { - "facet": "review.round", - "operation": "review.converge", - "fingerprint": "816ff42b6b75b61238802e9a3da8fc3419c2c462b7c8f7d681b7b09015dbd343" - } - ], - "prior_observation": "d7eefc02a8a8f45cfc5583c1eebcc64adce7ff20894ed21d45edb92dfc9a1aae", - "result_observation": "a8c8d1352c96583f79679f05fc543f621d09a99b4c05b8e5758f2ce6d8198f03", - "verification": "satisfied", - "committed_at": "2026-08-21T01:48:51.282881Z" - }, - { - "schema_version": 3, - "id": "rcp-6dac967d6831833dfe32701c08f4558c36124a7ba01677edfd831b28bb6ef707", - "instance_id": "supervisory-review-program", - "prescription_id": "prx-4c03d610c22c85aa11095cb0e77f42670e1054cb49bece7eb58629a3a4be4405", - "program": { - "id": "boatstack-reviewer", - "version": "1", - "fingerprint": "76680abd47f9e920ebfe807d3b3f226cbb48bd1a531b0a0a068cd27fd3e6f6e6" - }, - "transition_id": "review.reopen", - "prior_state_revision": 5, - "attempt_state_revision": 6, - "result_state_revision": 7, - "authority_fingerprint": "612af2b28e108bf69b10cec77dd688adac1ade9e73143b6ee5be1fe88c9d3d0b", - "capabilities": [ - "review.human" - ], - "effects": [ - { - "facet": "review.round", - "operation": "review.reopen", - "fingerprint": "generation-2" - } - ], - "prior_observation": "9787090456e6836381b1d3702f630a88506c4e1ac42f658d4d5674de43b8058a", - "result_observation": "c546633650586d5243b531805e3d7460b2e7501dfbe60e2606a452dbd143786a", - "verification": "satisfied", - "committed_at": "2026-08-21T02:13:18.819099Z" - }, - { - "schema_version": 3, - "id": "rcp-0cb388ab91385fcff8a34f017a83e9c8c64a26ede241bf2479be5abf1b9d2e4b", - "instance_id": "supervisory-review-program", - "prescription_id": "prx-0ae09a9dc531642f0a52d09df7f116c328b2df61d2a6d353bb3e24f8894dfce2", - "program": { - "id": "boatstack-reviewer", - "version": "1", - "fingerprint": "76680abd47f9e920ebfe807d3b3f226cbb48bd1a531b0a0a068cd27fd3e6f6e6" - }, - "transition_id": "review.findings.record", - "prior_state_revision": 7, - "attempt_state_revision": 8, - "result_state_revision": 9, - "authority_fingerprint": "75ada7d57808df1c3a9341cf60d064cd0f82c613cd1903de417751c6b2cc144a", - "capabilities": [ - "review.submit" - ], - "effects": [ - { - "facet": "review.round", - "operation": "review.findings.record", - "fingerprint": "c4ae2b6387fe9b5e382ac9fe68d2c780f49d613323a838a20c2784895e60820a" - } - ], - "prior_observation": "410089929bd05ea5e6a61cf670e4bf7b51b912332d9312ff6a06b233b5a42622", - "result_observation": "a9ad87e432d5cfbfb439bc9635f4906058f8dd37599dbcbb0e70083f0c2a5a08", - "verification": "satisfied", - "committed_at": "2026-08-21T02:16:25.169665Z" - }, - { - "schema_version": 3, - "id": "rcp-80a66988028811f154353a6e07af1eb12cf2f58a8eebf849a848896f354e64d0", - "instance_id": "supervisory-review-program", - "prescription_id": "prx-1bdc304995ab4531cffdfc27d2cc94382086ac7bc932b86574214d3a72cab41a", - "program": { - "id": "boatstack-reviewer", - "version": "1", - "fingerprint": "76680abd47f9e920ebfe807d3b3f226cbb48bd1a531b0a0a068cd27fd3e6f6e6" - }, - "transition_id": "review.converge", - "prior_state_revision": 9, - "attempt_state_revision": 10, - "result_state_revision": 11, - "authority_fingerprint": "1f9c39e910112b5074d0169b3d1ce7d59d97eaade92e8226dd62822180ee2136", - "capabilities": [ - "review.submit" - ], - "effects": [ - { - "facet": "review.round", - "operation": "review.converge", - "fingerprint": "a8887afe41be3cf56685b18373174189c28dc22380da0152aaf8af46f59e7894" - } - ], - "prior_observation": "545b8012b511db8b58ed6f3fe5806dbd74400a2d1dde7a5388a95581c35a6e60", - "result_observation": "868a1e74eab2952c67ec0592973cb556fa36134849b09839fcc4ebc860b40a0a", - "verification": "satisfied", - "committed_at": "2026-08-21T02:18:35.533334Z" - }, - { - "schema_version": 3, - "id": "rcp-a00414357805e89f1ca93b9596d35045a8a5af3a937cd574294c2d5d4f1d2b6a", - "instance_id": "supervisory-review-program", - "prescription_id": "prx-240e898ff3b3813b88cffeb0e0eb43b17316ece34ceb691463ec4e9b2826ee8e", - "program": { - "id": "boatstack-reviewer", - "version": "1", - "fingerprint": "76680abd47f9e920ebfe807d3b3f226cbb48bd1a531b0a0a068cd27fd3e6f6e6" - }, - "transition_id": "review.reopen", - "prior_state_revision": 11, - "attempt_state_revision": 12, - "result_state_revision": 13, - "authority_fingerprint": "e958cbf68de68708cad034d304915c9a3dd2d0d3a08936b6d1974ff314cd246e", - "capabilities": [ - "review.human" - ], - "effects": [ - { - "facet": "review.round", - "operation": "review.reopen", - "fingerprint": "generation-3" - } - ], - "prior_observation": "12894eb1320889e6bcc88c1e7cbc09edfbf6d4773e861df554f09f2fb4e50fe7", - "result_observation": "b6bf0cef3a39cb57b52bd57699e2f3febe206815462027078b62d69de3b9f19b", - "verification": "satisfied", - "committed_at": "2026-08-21T02:26:32.023904Z" - }, - { - "schema_version": 3, - "id": "rcp-c01fc323bd5431330d47451fb4e1db82a715bcb371310312b80b97e02b0de091", - "instance_id": "supervisory-review-program", - "prescription_id": "prx-0ebcfcfb545ca0fc73fe7f4cdf7e1b9444165a5a64b8b31a5181106015d6a80a", - "program": { - "id": "boatstack-reviewer", - "version": "1", - "fingerprint": "76680abd47f9e920ebfe807d3b3f226cbb48bd1a531b0a0a068cd27fd3e6f6e6" - }, - "transition_id": "review.converge", - "prior_state_revision": 13, - "attempt_state_revision": 14, - "result_state_revision": 15, - "authority_fingerprint": "99aaf65d7e429d765b1b6bae85b3158a23eb427b0bcbad2d8cec87b9583cc448", - "capabilities": [ - "review.submit" - ], - "effects": [ - { - "facet": "review.round", - "operation": "review.converge", - "fingerprint": "66de8c7b85be8ff74b99df18518ee01dd9a9320c656b35f7bc62ddee7e44ebfc" - } - ], - "prior_observation": "383f96bd9dd9b8a59f450d5f804db9ce18cc6246a7bd2cfcf92e6737b98fb815", - "result_observation": "219dea3026ab1d69c00f36b14392796f5a42ee9da7c248c0cd63d6994879c8e8", - "verification": "satisfied", - "committed_at": "2026-08-21T02:27:03.244993Z" - } - ], - "honesty": { - "semantic_correctness": "not-evaluated", - "origin_authenticity": "not-proven" - }, - "sealed_at": "2026-08-21T02:27:03.663388Z", - "fingerprint": "e6ec3a381e204e7574611a9308743d1a575978e1ead795cf1271529931dcb4b9" -} diff --git a/boatstack/cmd/boatstack-reviewer/main.go b/boatstack/cmd/boatstack-reviewer/main.go index 334ca46..ece02dc 100644 --- a/boatstack/cmd/boatstack-reviewer/main.go +++ b/boatstack/cmd/boatstack-reviewer/main.go @@ -461,19 +461,31 @@ func commandSeal(arguments []string) error { if err != nil { return err } + // The full receipt is verified here, at seal time, and archived in the + // local store. Only the minimal attestation enters the commit. + report := verifyFullReceipt(loop.repo, receipt, "", loop.baseRef, "HEAD") + if !report.Verified { + return fmt.Errorf("seal refused: the full receipt does not verify: %s", strings.Join(report.Failures, "; ")) + } + archive := filepath.Join(loop.store.dir, "sealed-receipt.json") + if err := writeSealedReceipt(archive, receipt); err != nil { + return err + } path := *output if path == "" { path = filepath.Join(loop.repo.receiptDirectoryPath(), loop.instance+".receipt.json") } - if err := writeSealedReceipt(path, receipt); err != nil { + if err := writeAttestation(path, attestationOf(receipt)); err != nil { return err } return printJSON(struct { Sealed string `json:"sealed"` Fingerprint string `json:"fingerprint"` ReviewedTree string `json:"reviewed_tree"` + Archive string `json:"full_receipt"` Guidance string `json:"guidance"` - }{path, receipt.Fingerprint, receipt.ReviewedTree, "commit this file with the pull request; CI verifies it deterministically"}) + }{path, receipt.Program.Fingerprint, receipt.ReviewedTree, archive, + "commit the attestation with the pull request; CI verifies it deterministically — pushing can wait until you are ready"}) } func commandVerify(arguments []string) error { @@ -493,22 +505,22 @@ func commandVerify(arguments []string) error { if err != nil { return err } - var receipt SealedReceipt + var attestation committedAttestation path := *receiptPath if path != "" { - receipt, err = readSealedReceipt(path) + attestation, err = readAttestation(path) } else { scanDir := *directory if !filepath.IsAbs(scanDir) { scanDir = filepath.Join(repo.Root, filepath.FromSlash(scanDir)) } - receipt, path, err = findReceiptForHead(repo, scanDir, *headRevision) + attestation, path, err = findReceiptForHead(repo, scanDir, *headRevision) } if err != nil { printJSON(verificationReport{Failures: []string{err.Error()}, Checks: []string{}, Warnings: []string{}}) return err } - report := verifySealedReceipt(repo, receipt, path, *baseRevision, *headRevision) + report := verifyAttestation(repo, attestation, path, *baseRevision, *headRevision) if err := printJSON(report); err != nil { return err } diff --git a/boatstack/cmd/boatstack-reviewer/reviewer_test.go b/boatstack/cmd/boatstack-reviewer/reviewer_test.go index 7634948..b8c97fc 100644 --- a/boatstack/cmd/boatstack-reviewer/reviewer_test.go +++ b/boatstack/cmd/boatstack-reviewer/reviewer_test.go @@ -371,29 +371,46 @@ func TestLoopConvergesSealsAndVerifies(t *testing.T) { t.Fatalf("mode is %q after convergence", mode(t, loop)) } - // Seal, commit the receipt, and verify: committing the sealed receipt - // must not invalidate its own tree binding. + // Seal: the full receipt verifies at seal time; only the minimal + // attestation is committed, and committing it must not invalidate its + // own tree binding. sealed, err := buildSealedReceipt(scratch.repo, loop.store, policy, loop.program, "main", time.Now()) if err != nil { t.Fatal(err) } + if report := verifyFullReceipt(scratch.repo, sealed, "", "main", "HEAD"); !report.Verified { + t.Fatalf("full receipt did not verify at seal time: %v", report.Failures) + } receiptPath := filepath.Join(scratch.repo.receiptDirectoryPath(), "feature.receipt.json") - if err := writeSealedReceipt(receiptPath, sealed); err != nil { + if err := writeAttestation(receiptPath, attestationOf(sealed)); err != nil { t.Fatal(err) } - scratch.commitAll("sealed review receipt") - report := verifySealedReceipt(scratch.repo, sealed, receiptPath, "main", "HEAD") + scratch.commitAll("sealed review attestation") + report := verifyAttestation(scratch.repo, attestationOf(sealed), receiptPath, "main", "HEAD") if !report.Verified { - t.Fatalf("converged receipt did not verify: %v", report.Failures) + t.Fatalf("converged attestation did not verify: %v", report.Failures) + } + + // The committed artifact carries exactly the two admitted facts. + var raw map[string]json.RawMessage + value, err := os.ReadFile(receiptPath) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(value, &raw); err != nil { + t.Fatal(err) + } + if len(raw) != 2 { + t.Fatalf("the committed attestation carries %d fields, not the minimal 2: %v", len(raw), raw) } - // Directory scan finds the same receipt for the head tree. + // Directory scan finds the same attestation for the head tree. found, foundPath, err := findReceiptForHead(scratch.repo, scratch.repo.receiptDirectoryPath(), "HEAD") if err != nil { t.Fatal(err) } - if found.Fingerprint != sealed.Fingerprint || foundPath != receiptPath { - t.Fatal("directory scan did not find the sealed receipt for the head tree") + if found != attestationOf(sealed) || foundPath != receiptPath { + t.Fatal("directory scan did not find the attestation for the head tree") } } @@ -516,7 +533,7 @@ func TestStallEscalatesAndHumanReopens(t *testing.T) { } // converge drives a scratch repository to a sealed, committed, verified -// receipt and returns it with its path. +// attestation and returns the full receipt with the attestation path. func converge(t *testing.T, scratch *scratchRepo, loop *loopContext) (SealedReceipt, string) { t.Helper() if _, _, err := submit(t, loop, correctReview()); err != nil { @@ -526,13 +543,16 @@ func converge(t *testing.T, scratch *scratchRepo, loop *loopContext) (SealedRece if err != nil { t.Fatal(err) } + if report := verifyFullReceipt(scratch.repo, sealed, "", "main", "HEAD"); !report.Verified { + t.Fatalf("baseline full receipt did not verify at seal time: %v", report.Failures) + } path := filepath.Join(scratch.repo.receiptDirectoryPath(), "feature.receipt.json") - if err := writeSealedReceipt(path, sealed); err != nil { + if err := writeAttestation(path, attestationOf(sealed)); err != nil { t.Fatal(err) } - scratch.commitAll("sealed review receipt") - if report := verifySealedReceipt(scratch.repo, sealed, path, "main", "HEAD"); !report.Verified { - t.Fatalf("baseline receipt did not verify: %v", report.Failures) + scratch.commitAll("sealed review attestation") + if report := verifyAttestation(scratch.repo, attestationOf(sealed), path, "main", "HEAD"); !report.Verified { + t.Fatalf("baseline attestation did not verify: %v", report.Failures) } return sealed, path } @@ -543,17 +563,18 @@ func TestVerificationRejectsTamperingForgeryAndBypass(t *testing.T) { loop := newTestLoop(t, scratch, policy) sealed, path := converge(t, scratch, loop) - // Tampered content: any edit breaks the content identity. + // Seal-time law — tampered content: any edit breaks the content + // identity of the full receipt. tampered := sealed tampered.Rounds = append([]journalRound(nil), sealed.Rounds...) tampered.Rounds[0].Verdict = verdictIncorrect - if report := verifySealedReceipt(scratch.repo, tampered, path, "main", "HEAD"); report.Verified { + if report := verifyFullReceipt(scratch.repo, tampered, path, "main", "HEAD"); report.Verified { t.Fatal("a tampered receipt verified") } - // Forged verdict: rewriting the final review and re-fingerprinting the - // envelope still fails, because the kernel receipt chain committed the - // original candidate fingerprint. + // Seal-time law — forged verdict: rewriting the final review and + // re-fingerprinting the envelope still fails, because the kernel + // receipt chain committed the original candidate fingerprint. forged := sealed forged.FinalReview = json.RawMessage(strings.Replace( string(sealed.FinalReview), "patch is correct", "patch is incorrect", 1)) @@ -562,35 +583,78 @@ func TestVerificationRejectsTamperingForgeryAndBypass(t *testing.T) { t.Fatal(err) } forged.Fingerprint = refingered - report := verifySealedReceipt(scratch.repo, forged, path, "main", "HEAD") - if report.Verified { + if report := verifyFullReceipt(scratch.repo, forged, path, "main", "HEAD"); report.Verified { t.Fatal("a forged final review verified") } - // Bypass: new commits after convergence leave the receipt bound to the - // old tree, so the new head is not review-verified. + // CI law — attestation tampering: a mutated tree or fingerprint fails. + wrongTree := attestationOf(sealed) + wrongTree.ReviewedTree = strings.Repeat("0", 40) + if report := verifyAttestation(scratch.repo, wrongTree, path, "main", "HEAD"); report.Verified { + t.Fatal("an attestation for a different tree verified") + } + wrongProgram := attestationOf(sealed) + wrongProgram.ProgramFingerprint = strings.Repeat("0", 64) + if report := verifyAttestation(scratch.repo, wrongProgram, path, "main", "HEAD"); report.Verified { + t.Fatal("an attestation under an unadmitted program verified") + } + + // CI law — strictness: unknown fields, missing facts, and receipts of + // the superseded full format are not the admitted artifact. + padded := filepath.Join(scratch.repo.receiptDirectoryPath(), "padded.receipt.json") + if err := os.WriteFile(padded, []byte(`{"reviewed_tree":"`+sealed.ReviewedTree+`","program_fingerprint":"`+sealed.Program.Fingerprint+`","note":"extra"}`), 0o644); err != nil { + t.Fatal(err) + } + if _, err := readAttestation(padded); err == nil { + t.Fatal("an attestation with unknown fields decoded") + } + if err := os.WriteFile(padded, []byte(`{"reviewed_tree":"`+sealed.ReviewedTree+`"}`), 0o644); err != nil { + t.Fatal(err) + } + if _, err := readAttestation(padded); err == nil { + t.Fatal("an attestation missing the program fingerprint decoded") + } + fullFormat := filepath.Join(scratch.repo.receiptDirectoryPath(), "old-format.receipt.json") + if err := writeSealedReceipt(fullFormat, sealed); err != nil { + t.Fatal(err) + } + if _, err := readAttestation(fullFormat); err == nil { + t.Fatal("a superseded full-format receipt decoded as an attestation") + } + if err := os.Remove(padded); err != nil { + t.Fatal(err) + } + if err := os.Remove(fullFormat); err != nil { + t.Fatal(err) + } + + // Bypass: new commits after convergence leave the attestation bound to + // the old tree, so the new head is not review-verified. scratch.writeFile("subject.go", "package subject\n\nfunc Value() int { return 99 }\n") scratch.commitAll("unreviewed change") - if report := verifySealedReceipt(scratch.repo, sealed, path, "main", "HEAD"); report.Verified { - t.Fatal("a receipt for an older tree verified a new head") + if report := verifyAttestation(scratch.repo, attestationOf(sealed), path, "main", "HEAD"); report.Verified { + t.Fatal("an attestation for an older tree verified a new head") } if _, _, err := findReceiptForHead(scratch.repo, scratch.repo.receiptDirectoryPath(), "HEAD"); err == nil { - t.Fatal("directory scan bound an unreviewed head to an old receipt") + t.Fatal("directory scan bound an unreviewed head to an old attestation") } - // Policy drift: a base whose admitted prompt differs refuses admission. + // Policy drift: a base whose admitted prompt differs recompiles to a + // different program, so the attested fingerprint refuses. scratch.git("checkout", "-q", "main") scratch.writeFile(policyPromptPath, "entirely different review policy\n") scratch.commitAll("policy change on main") scratch.git("checkout", "-q", "feature") - if report := verifySealedReceipt(scratch.repo, sealed, path, "main", "HEAD"); report.Verified { - t.Fatal("a receipt sealed under a superseded policy verified") + if report := verifyAttestation(scratch.repo, attestationOf(sealed), path, "main", "HEAD"); report.Verified { + t.Fatal("an attestation sealed under a superseded policy verified") } } func TestVerificationRejectsReceiptDeclaredBoundsDrift(t *testing.T) { // Regression for round 1, finding 1: a receipt sealed under weakened - // convergence bounds must not verify against the admitted policy. + // convergence bounds must not verify against the admitted policy — + // neither at seal time (bounds check) nor in CI, where the weakened + // bounds change the program fingerprint the attestation carries. scratch := newScratchRepo(t) weakened := testPolicy(t, scratch) weakened.MaxRounds = 1000 @@ -604,22 +668,37 @@ func TestVerificationRejectsReceiptDeclaredBoundsDrift(t *testing.T) { t.Fatal(err) } path := filepath.Join(scratch.repo.receiptDirectoryPath(), "feature.receipt.json") - if err := writeSealedReceipt(path, sealed); err != nil { + if err := writeAttestation(path, attestationOf(sealed)); err != nil { t.Fatal(err) } scratch.commitAll("sealed under weakened bounds") - report := verifySealedReceipt(scratch.repo, sealed, path, "main", "HEAD") - if report.Verified { - t.Fatal("a receipt sealed under weakened convergence bounds verified") + + fullReport := verifyFullReceipt(scratch.repo, sealed, "", "main", "HEAD") + if fullReport.Verified { + t.Fatal("a full receipt sealed under weakened convergence bounds verified at seal time") } boundsNamed := false - for _, failure := range report.Failures { + for _, failure := range fullReport.Failures { if strings.Contains(failure, "convergence bounds") { boundsNamed = true } } if !boundsNamed { - t.Fatalf("the failure does not name the bounds drift: %v", report.Failures) + t.Fatalf("the seal-time failure does not name the bounds drift: %v", fullReport.Failures) + } + + report := verifyAttestation(scratch.repo, attestationOf(sealed), path, "main", "HEAD") + if report.Verified { + t.Fatal("an attestation sealed under weakened convergence bounds verified") + } + programNamed := false + for _, failure := range report.Failures { + if strings.Contains(failure, "does not match the program recompiled") { + programNamed = true + } + } + if !programNamed { + t.Fatalf("the CI failure does not name the program mismatch: %v", report.Failures) } } @@ -779,7 +858,7 @@ func TestCLIVerifyCommandExitsNonZeroWithoutReceipt(t *testing.T) { if err == nil { t.Fatal("verify succeeded with no sealed receipt present") } - if !strings.Contains(err.Error(), "no sealed review receipt") { + if !strings.Contains(err.Error(), "no review attestation") { t.Fatalf("failure does not tell the operator what to do: %v", err) } } diff --git a/boatstack/cmd/boatstack-reviewer/seal.go b/boatstack/cmd/boatstack-reviewer/seal.go index 9784a09..1cfbc0c 100644 --- a/boatstack/cmd/boatstack-reviewer/seal.go +++ b/boatstack/cmd/boatstack-reviewer/seal.go @@ -14,11 +14,25 @@ import ( const sealedReceiptSchemaVersion = 1 -// SealedReceipt is the one artifact that travels with a pull request. It -// binds the exact reviewed (receipt-excluded) tree, the exact admitted -// policy, the exact program identity, the round trajectory, the final -// review bytes, and the complete kernel receipt chain. CI verifies it -// deterministically; no reviewer runs there. +// committedAttestation is the one artifact that travels with a pull request, +// reduced to the minimal facts CI cannot re-derive: which receipt-excluded +// tree the converged review bound, and under which program it converged. +// Everything else — policy hashes, convergence bounds, weights, transition +// law — is already inside the program fingerprint, which the verifier +// recompiles from the base-revision admitted policy. The full sealed +// receipt (round trajectory, kernel receipt chain, final review bytes) +// stays in the local review store and never enters a commit. +type committedAttestation struct { + ReviewedTree string `json:"reviewed_tree"` + ProgramFingerprint string `json:"program_fingerprint"` +} + +// SealedReceipt is the full local sealing artifact. It binds the exact +// reviewed (receipt-excluded) tree, the exact admitted policy, the exact +// program identity, the round trajectory, the final review bytes, and the +// complete kernel receipt chain. It is verified in full at seal time and +// archived in the local store; only the committedAttestation travels with +// the pull request. type SealedReceipt struct { SchemaVersion int `json:"schema_version"` Instance string `json:"instance"` @@ -150,6 +164,44 @@ func readSealedReceipt(path string) (SealedReceipt, error) { return receipt, nil } +func attestationOf(receipt SealedReceipt) committedAttestation { + return committedAttestation{ + ReviewedTree: receipt.ReviewedTree, + ProgramFingerprint: receipt.Program.Fingerprint, + } +} + +func writeAttestation(path string, attestation committedAttestation) error { + encoded, err := json.MarshalIndent(attestation, "", " ") + if err != nil { + return err + } + return writeFileAtomic(path, append(encoded, '\n')) +} + +// readAttestation decodes strictly: an attestation with unknown fields or +// missing facts is not the admitted artifact and must not verify. +func readAttestation(path string) (committedAttestation, error) { + value, err := os.ReadFile(path) + if err != nil { + return committedAttestation{}, err + } + decoder := json.NewDecoder(strings.NewReader(string(value))) + decoder.DisallowUnknownFields() + var attestation committedAttestation + if err := decoder.Decode(&attestation); err != nil { + return committedAttestation{}, fmt.Errorf("review attestation %s does not decode: %w", path, err) + } + var trailing json.RawMessage + if err := decoder.Decode(&trailing); err == nil { + return committedAttestation{}, fmt.Errorf("review attestation %s carries trailing content", path) + } + if attestation.ReviewedTree == "" || attestation.ProgramFingerprint == "" { + return committedAttestation{}, fmt.Errorf("review attestation %s is missing required facts", path) + } + return attestation, nil +} + // verificationReport is the deterministic CI answer. type verificationReport struct { Verified bool `json:"verified"` @@ -161,13 +213,82 @@ type verificationReport struct { Warnings []string `json:"warnings"` } -// verifySealedReceipt is the CI-side control law: +// verifyAttestation is the CI-side control law: +// +// a pull request head is review-verified only when a committed +// attestation binds its exact receipt-excluded tree under the review +// program recompiled from the policy admitted at the pull request base +// revision. +// +// The attestation carries nothing else, so there is nothing else to check +// here: the program fingerprint already hashes the policy assets, the +// convergence bounds, the weights, and the transition law. The round +// trajectory, kernel receipt chain, and final review bytes were verified in +// full at seal time and stay in the local review store. +func verifyAttestation(repo *gitRepo, attestation committedAttestation, receiptPath, baseRevision, headRevision string) verificationReport { + report := verificationReport{ + ReceiptPath: receiptPath, + Program: attestation.ProgramFingerprint, + Checks: []string{}, + Failures: []string{}, + Warnings: []string{}, + } + pass := func(check string) { report.Checks = append(report.Checks, check) } + fail := func(format string, args ...any) { + report.Failures = append(report.Failures, fmt.Sprintf(format, args...)) + } + + // Program identity: recompile the review program from the policy + // admitted at the pull request base revision under this binary's + // transition law. The attestation's fingerprint is never trusted as a + // description of anything; it must equal the recompiled identity. + basePolicy, err := loadRevisionPolicy(repo, baseRevision) + if err != nil { + fail("base revision policy admission failed: %v", err) + return report + } + program, err := compileReviewProgram(basePolicy) + if err != nil { + fail("admitted review program does not compile: %v", err) + return report + } + if program.Fingerprint != attestation.ProgramFingerprint { + fail("attested program fingerprint %s does not match the program recompiled from the base-revision admitted policy %s", attestation.ProgramFingerprint, program.Fingerprint) + } else { + pass("attested program matches the base-revision admitted policy and transition law") + } + + // Tree binding: the pull request head, receipts excluded, must be the + // exact tree the converged review bound. + head, err := repo.revParse(headRevision) + if err != nil { + fail("head revision %s is unavailable: %v", headRevision, err) + return report + } + reviewedTree, err := repo.reviewedTree(head) + if err != nil { + fail("reviewed tree of %s is unavailable: %v", head, err) + return report + } + if reviewedTree != attestation.ReviewedTree { + fail("attestation binds tree %s but the head reviewed tree is %s; the reviewed content changed after convergence", attestation.ReviewedTree, reviewedTree) + } else { + pass("attestation binds the exact head reviewed tree") + } + + report.Verified = len(report.Failures) == 0 + return report +} + +// verifyFullReceipt is the seal-time control law over the full local +// receipt: // -// a pull request head is review-verified only when a sealed receipt -// binds its exact receipt-excluded tree, under the review program -// whose policy assets are admitted at the pull request base revision, -// with an integral kernel receipt chain ending in convergence. -func verifySealedReceipt(repo *gitRepo, receipt SealedReceipt, receiptPath, baseRevision, headRevision string) verificationReport { +// a converged loop seals only when the receipt binds the exact +// receipt-excluded head tree, under the review program whose policy +// assets are admitted at the base revision, with an integral kernel +// receipt chain ending in convergence and final review bytes that +// revalidate. +func verifyFullReceipt(repo *gitRepo, receipt SealedReceipt, receiptPath, baseRevision, headRevision string) verificationReport { report := verificationReport{ ReceiptPath: receiptPath, Instance: receipt.Instance, @@ -357,23 +478,25 @@ func verifySealedReceipt(repo *gitRepo, receipt SealedReceipt, receiptPath, base return report } -// findReceiptForHead scans a receipt directory for the sealed receipt that -// binds the exact head reviewed tree. -func findReceiptForHead(repo *gitRepo, directory, headRevision string) (SealedReceipt, string, error) { +// findReceiptForHead scans a receipt directory for the committed +// attestation that binds the exact head reviewed tree. Files that are not +// strict attestations (including receipts of the superseded full format) +// are skipped: they are not the admitted artifact. +func findReceiptForHead(repo *gitRepo, directory, headRevision string) (committedAttestation, string, error) { head, err := repo.revParse(headRevision) if err != nil { - return SealedReceipt{}, "", err + return committedAttestation{}, "", err } reviewedTree, err := repo.reviewedTree(head) if err != nil { - return SealedReceipt{}, "", err + return committedAttestation{}, "", err } entries, err := os.ReadDir(directory) if err != nil { if os.IsNotExist(err) { - return SealedReceipt{}, "", fmt.Errorf("no sealed review receipt binds reviewed tree %s: %s does not exist; run the local review loop and commit the sealed receipt", reviewedTree, directory) + return committedAttestation{}, "", fmt.Errorf("no review attestation binds reviewed tree %s: %s does not exist; run the local review loop and commit the sealed attestation", reviewedTree, directory) } - return SealedReceipt{}, "", err + return committedAttestation{}, "", err } var names []string for _, entry := range entries { @@ -384,13 +507,13 @@ func findReceiptForHead(repo *gitRepo, directory, headRevision string) (SealedRe sort.Strings(names) for _, name := range names { path := filepath.Join(directory, name) - receipt, err := readSealedReceipt(path) + attestation, err := readAttestation(path) if err != nil { continue } - if receipt.ReviewedTree == reviewedTree { - return receipt, path, nil + if attestation.ReviewedTree == reviewedTree { + return attestation, path, nil } } - return SealedReceipt{}, "", fmt.Errorf("no sealed review receipt in %s binds reviewed tree %s; run the local review loop to convergence, seal, and commit the receipt", directory, reviewedTree) + return committedAttestation{}, "", fmt.Errorf("no review attestation in %s binds reviewed tree %s; run the local review loop to convergence, seal, and commit the attestation", directory, reviewedTree) } diff --git a/docs/self-review.md b/docs/self-review.md index e0f96f6..be3406e 100644 --- a/docs/self-review.md +++ b/docs/self-review.md @@ -53,10 +53,14 @@ Work on a branch, commit your change, then: 3. Fix the recorded findings, commit, and repeat. The measure must trend down; convergence requires a fresh review of the fixed tree whose verdict is `patch is correct`. -4. `boatstack-reviewer seal` — writes - `.github/reviews/.receipt.json`. Commit that file with the pull - request. The receipt directory is excluded from the tree binding, so - committing the receipt does not invalidate it. +4. `boatstack-reviewer seal` — verifies the full receipt (round trajectory, + kernel receipt chain, final review bytes) locally, archives it in the + local store, and writes only a minimal attestation to + `.github/reviews/.receipt.json`: the reviewed tree and the + program fingerprint — the two facts CI cannot re-derive. Commit that file + with the pull request; pushing can wait until you are ready. The receipt + directory is excluded from the tree binding, so committing the + attestation does not invalidate it. 5. `boatstack-reviewer show` prints a recorded review itself — the exact archived findings of the latest round (`--round ` for earlier ones) and any staged, not-yet-submitted candidate — without resolving or changing @@ -71,13 +75,16 @@ so any registered coding agent drives the loop through recorded, resumable operations instead of remembering the command order (adapters are registered for Cursor, Codex, and Claude Code under their skill directories): -- `skills/self-review` — one review round, no code changes: builds the - reviewer from the current tree, resolves the control state, has the agent - review exactly the admitted range under the admitted schema, submits, and - reports the recorded verdict. It verifies afterwards that no file changed. +- `skills/self-review` — report-only: builds the reviewer from the current + tree, resolves the control state, has the agent review exactly the + admitted range under the admitted schema, submits, and reports the + recorded verdict in the conversation. It changes no code, verifies + afterwards that no tracked file changed, and never seals, commits, or + pushes. - `skills/self-review-solve` — drives to convergence: fixes open findings in code (committed by the agent), re-reviews the fixed tree, repeats within a - bounded attempt budget, then seals the receipt and commits it. An escalated + bounded attempt budget, then seals and commits the minimal attestation + locally. It never pushes; pushing is the user's decision. An escalated loop asks the human before reopening. Run either with `.yield/bin/yskill run 'skills/'` from the repository @@ -88,18 +95,23 @@ touches real review state. CI (`.github/workflows/review-verified.yml`) rebuilds the verifier and runs `boatstack-reviewer verify --dir .github/reviews --base --head `, which re-admits the policy from the base revision, recompiles the program -fingerprint, recomputes the receipt-excluded head tree, and checks the kernel -receipt chain and the final review bytes. - -## What the receipt does and does not prove - -Like work-package verification, the sealed receipt is honest about its -boundary: it proves the declared review program ran to convergence over the -exact bound tree under the admitted policy. It does not prove the review was -semantically right, and it does not prove who performed it — the receipt -records `semantic_correctness: not-evaluated` and `origin_authenticity: -not-proven`. Branch protection and human judgment remain the authority for -merging. - -Local review state lives under `.git/boatstack-review//` and never -enters a commit. +fingerprint, recomputes the receipt-excluded head tree, and checks that the +committed attestation names exactly those two facts. Nothing else travels +with the pull request: the attestation is deliberately minimal because the +program fingerprint already hashes the prompt bytes, the schema bytes, the +round bound, the stall window, and the priority weights. + +## What the attestation does and does not prove + +Like work-package verification, the committed attestation is honest about +its boundary: it proves a review program with the base-admitted identity +sealed a convergence over the exact bound tree. The full evidence — round +trajectory, kernel receipt chain, final review bytes, and the honesty +markers `semantic_correctness: not-evaluated` and `origin_authenticity: +not-proven` — is verified in full at seal time and archived in the local +store, not in the commit. The attestation does not prove the review was +semantically right, and it does not prove who performed it. Branch +protection and human judgment remain the authority for merging. + +Local review state, including the archived full receipt, lives under +`.git/boatstack-review//` and never enters a commit. diff --git a/release-notes/2026-08-21-minimal-review-attestation.md b/release-notes/2026-08-21-minimal-review-attestation.md new file mode 100644 index 0000000..82588f8 --- /dev/null +++ b/release-notes/2026-08-21-minimal-review-attestation.md @@ -0,0 +1,3 @@ +### Minimal review attestation; report-only review skill + +The artifact a pull request carries for review verification shrinks to the minimal facts CI cannot re-derive: the reviewed (receipt-excluded) tree and the program fingerprint. The full sealed receipt — round trajectory, kernel receipt chain, final review bytes, honesty markers — is now verified in full at seal time and archived in the local review store instead of being committed. CI verification reduces to recompiling the program from the base-admitted policy and recomputing the head tree; the fingerprint already binds the policy assets, convergence bounds, and weights, so tampering and bounds drift still refuse. The self-review Yield skill is now strictly report-only (it records the round and reports the verdict; it never seals, commits, or pushes), and self-review-solve seals and commits the minimal attestation locally without pushing. diff --git a/skills/self-review-solve/SKILL.md b/skills/self-review-solve/SKILL.md index 7a5a843..3bd0f05 100644 --- a/skills/self-review-solve/SKILL.md +++ b/skills/self-review-solve/SKILL.md @@ -7,7 +7,10 @@ Drive the supervisory-control self-review of the current branch to convergence. The workflow decides from the committed control state what is needed: open findings are fixed in code and committed, an unreviewed tree gets a fresh review, an escalated loop asks before reopening, and a -converged loop is sealed and the receipt committed. +converged loop is sealed — the minimal attestation (reviewed tree + program +fingerprint) is committed locally. This skill never pushes: pushing the +branch is the user's decision, and CI verifies the attestation whenever the +push happens. Run from the repository root: diff --git a/skills/self-review-solve/main.go b/skills/self-review-solve/main.go index 47fa366..9dbff36 100644 --- a/skills/self-review-solve/main.go +++ b/skills/self-review-solve/main.go @@ -198,24 +198,25 @@ func main() { return yield.Outcome{}, err } + // The attestation is committed locally so it rides with the branch; + // pushing is the user's decision and never happens here. commit := ctx.RunCommand("commit-receipt", prelude+`if [ -n "$(git -C "$repo" status --porcelain .github/reviews)" ]; then git -C "$repo" add .github/reviews - git -C "$repo" commit -qm "Seal converged self-review receipt" + git -C "$repo" commit -qm "Seal converged self-review attestation" echo committed else echo unchanged fi`, 60) - ctx.Require(commit.ExitCode == 0, "the sealed receipt is committed with the change", commit) + ctx.Require(commit.ExitCode == 0, "the sealed attestation is committed with the change", commit) return ctx.Complete(map[string]any{ "mode": "converged", "receipt": sealed.Sealed, "fingerprint": sealed.Fingerprint, "reviewed_tree": sealed.ReviewedTree, - "measures": measures, - "receipt_state": commit.Stdout, - "guidance": "push the branch; the review-verified CI job verifies the receipt deterministically", + "guidance": "the minimal attestation is committed locally and is NOT pushed; " + + "push whenever ready — the review-verified CI job verifies it deterministically", }) }) } diff --git a/skills/self-review/SKILL.md b/skills/self-review/SKILL.md index 1371d81..ef952ce 100644 --- a/skills/self-review/SKILL.md +++ b/skills/self-review/SKILL.md @@ -3,10 +3,13 @@ name: self-review description: "Run the Boatstack supervisory-control self-review for the current branch and report the verdict without changing code." --- -Run one review round for the current branch against `origin/main` and show -the recorded verdict. This skill never edits code: the review is performed -read-only, `boatstack-reviewer` admits or refuses the candidate, and the -round is recorded in the repository's local review store. +Run one review round for the current branch against `origin/main` and report +the recorded verdict — nothing else. This skill is report-only: the review is +performed read-only, `boatstack-reviewer` admits or refuses the candidate, +and the round is recorded in the repository's local review store. It never +seals a receipt, never commits, and never pushes; when the run completes, +report the verdict in the conversation and stop. Sealing and committing +belong to the `self-review-solve` skill or an explicit user request. Run from the repository root: diff --git a/skills/self-review/main.go b/skills/self-review/main.go index 8581ec4..ef13f39 100644 --- a/skills/self-review/main.go +++ b/skills/self-review/main.go @@ -29,6 +29,12 @@ reviewer="$tmp/boatstack-reviewer" const actor = "yield-self-review" +// reportOnlyContract is the skill's whole scope: it records the round and +// reports the verdict. Sealing, committing, and pushing are separate +// decisions that belong to the user (or the self-review-solve skill). +const reportOnlyContract = "report this verdict in the conversation and stop; " + + "do not seal, commit, or push anything unless the user asks" + type resolveOutput struct { Instance string `json:"instance"` State struct { @@ -118,7 +124,7 @@ func main() { "instance": resolved.Instance, "mode": resolved.State.Mode, "verdict": "patch is correct", - "guidance": "already converged for this tree; run boatstack-reviewer seal, or reopen to re-review", + "action": reportOnlyContract, }) } // The instance converged for an older tree; new commits need a @@ -183,19 +189,20 @@ func main() { "before": before.Stdout, "after": after.Stdout, }) - return ctx.Complete(map[string]any{ + result := map[string]any{ "instance": resolved.Instance, "mode": shown.Mode, "round": shown.Round.Index, "verdict": shown.Round.Verdict, "measure": shown.Round.Measure, "finding_count": shown.Round.FindingCount, - "review": json.RawMessage(shown.Review), - "guidance": map[string]string{ - "converged": "run boatstack-reviewer seal and commit the sealed receipt", - "findings-open": "fix the findings (or run the self-review-solve skill), commit, and review again", - "escalated": "the loop escalated; a human must decide, then boatstack-reviewer reopen", - }[shown.Mode], - }) + "action": reportOnlyContract, + } + // The findings are the actionable content of an incorrect verdict; + // a correct verdict needs nothing beyond itself. + if shown.Round.Verdict != "patch is correct" { + result["review"] = json.RawMessage(shown.Review) + } + return ctx.Complete(result) }) } From 02a18c5f899ccb9514e94a0f381180ab4a8b3ca1 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Fri, 21 Aug 2026 05:56:14 +0100 Subject: [PATCH 2/2] Seal converged self-review attestation --- .github/reviews/minimal-review-receipt.receipt.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/reviews/minimal-review-receipt.receipt.json diff --git a/.github/reviews/minimal-review-receipt.receipt.json b/.github/reviews/minimal-review-receipt.receipt.json new file mode 100644 index 0000000..18ef982 --- /dev/null +++ b/.github/reviews/minimal-review-receipt.receipt.json @@ -0,0 +1,4 @@ +{ + "reviewed_tree": "387d5503cc1582f89a1832f18f7d5c860b297940", + "program_fingerprint": "76680abd47f9e920ebfe807d3b3f226cbb48bd1a531b0a0a068cd27fd3e6f6e6" +}