Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"gentle-ai.review-integration.repair/v1",
"gentle-ai.review-integration.start/v2",
"gentle-ai.review-integration.status/v2",
"gentle-ai.review-verification-evidence/v1",
"gentle-ai.review-receipt/v1",
"gentle-ai.review-receipt/v2",
"gentle-ai.review-result-artifact/v2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@
"selected_order": 3
}
],
"reviewer_task_bindings": [
"GENTLE_AI_REVIEW_BINDING {\"lineage\":\"review-start-fixture\",\"target\":\"sha256:136dc6556e3bac8c2e7f7af7cc5ec361f449e383a997638128729059fefa06a5\",\"lens\":\"review-risk\",\"order\":0,\"revision\":\"sha256:3411d049f39e891b50adfc76473199b473927d10245a227a3bc967eae0caf68c\",\"repository_context\":\"rctx1_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"subject_hash\":\"sha256:38a55988f6b3ae6f520690d52fd29255f425a4048b0f8694de3a80c5daf265cd\"}",
"GENTLE_AI_REVIEW_BINDING {\"lineage\":\"review-start-fixture\",\"target\":\"sha256:136dc6556e3bac8c2e7f7af7cc5ec361f449e383a997638128729059fefa06a5\",\"lens\":\"review-resilience\",\"order\":1,\"revision\":\"sha256:3411d049f39e891b50adfc76473199b473927d10245a227a3bc967eae0caf68c\",\"repository_context\":\"rctx1_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"subject_hash\":\"sha256:06d86f981a8c0b16c2179696d467c8157580214ad72c952adee423fe596d2c21\"}",
"GENTLE_AI_REVIEW_BINDING {\"lineage\":\"review-start-fixture\",\"target\":\"sha256:136dc6556e3bac8c2e7f7af7cc5ec361f449e383a997638128729059fefa06a5\",\"lens\":\"review-readability\",\"order\":2,\"revision\":\"sha256:3411d049f39e891b50adfc76473199b473927d10245a227a3bc967eae0caf68c\",\"repository_context\":\"rctx1_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"subject_hash\":\"sha256:beacbf1eed52620ef7686087f8944cbc2304d71abf13281267efe9e4de20fb1e\"}",
"GENTLE_AI_REVIEW_BINDING {\"lineage\":\"review-start-fixture\",\"target\":\"sha256:136dc6556e3bac8c2e7f7af7cc5ec361f449e383a997638128729059fefa06a5\",\"lens\":\"review-reliability\",\"order\":3,\"revision\":\"sha256:3411d049f39e891b50adfc76473199b473927d10245a227a3bc967eae0caf68c\",\"repository_context\":\"rctx1_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"subject_hash\":\"sha256:61a10b133e3f7a8ccad1d0f1d58989df75d2330ad530690137de625bfd10eec8\"}"
],
"candidate_diff": {
"encoding": "base64",
"data": "ZGlmZiAtLWdpdCBhL3NjcmlwdHMvZGVwbG95LnNoIGIvc2NyaXB0cy9kZXBsb3kuc2gKbmV3IGZpbGUgbW9kZSAxMDA2NDQKaW5kZXggMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMC4uMjhiM2FjMTI4ZmJiZWFmZmY0OWEzMjYxMDVmOGVhYzg1YzZjNzdhMQotLS0gL2Rldi9udWxsCisrKyBiL3NjcmlwdHMvZGVwbG95LnNoCkBAIC0wLDAgKzEgQEAKK2VjaG8gZGVwbG95Cg==",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
"value": "0"
}
],
"reviewer_task_binding": "GENTLE_AI_REVIEW_BINDING {\"lineage\":\"review-status-fixture\",\"target\":\"sha256:e6faad1cb9ceb2db170e0ec9ea5394b44c3991dff6cbd02fb6787539715968f6\",\"lens\":\"review-reliability\",\"order\":0,\"revision\":\"sha256:81cdad8f3182d4919147d3c4fe8f614814068e5ef68417e3dcbd5e326dd3e390\",\"repository_context\":\"rctx1_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"subject_hash\":\"sha256:fe57db2ea45a33c7e852d039e8af3291c3bc31911f0c92a710b5fef547b1999d\"}",
"artifact_subject": {
"schema": "gentle-ai.review-artifact-subject/v1",
"subject_hash": "sha256:fe57db2ea45a33c7e852d039e8af3291c3bc31911f0c92a710b5fef547b1999d",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"schema": "gentle-ai.review-verification-evidence/v1",
"outcome": "passed",
"checks": [
{
"name": "focused tests",
"status": "passed",
"command": "go test ./internal/cli",
"evidence": ["ok github.com/gentleman-programming/gentle-ai/internal/cli"]
}
]
}
5 changes: 5 additions & 0 deletions contracts/review-integration/v1/schemas/start-v2.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@
"maxItems": 4,
"items": {"$ref": "#/$defs/artifact_subject"}
},
"reviewer_task_bindings": {
"type": "array",
"maxItems": 4,
"items": {"type": "string", "pattern": "^GENTLE_AI_REVIEW_BINDING \\{"}
},
"candidate_diff": {"$ref": "#/$defs/frozen_candidate_diff"},
"changed_path_manifest": {
"type": "array",
Expand Down
6 changes: 6 additions & 0 deletions contracts/review-integration/v1/schemas/status-v2.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
"properties": {
"name": { "type": "string", "pattern": "^[a-z0-9_]+$" }, "schema": { "type": "string", "minLength": 1 }, "capture_operation": { "type": "string", "minLength": 1 },
"arguments": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/transition_argument" } },
"reviewer_task_binding": { "type": "string", "pattern": "^GENTLE_AI_REVIEW_BINDING \\{" },
"artifact_subject": { "$ref": "artifact-subject.schema.json" },
"candidate_diff": { "$ref": "start-v2.schema.json#/$defs/frozen_candidate_diff" },
"changed_path_manifest": { "type": "array", "uniqueItems": true, "items": { "$ref": "start-v2.schema.json#/$defs/changed_path" } },
Expand All @@ -159,10 +160,15 @@
"else": {
"allOf": [
{ "not": { "required": ["artifact_subject"] } },
{ "not": { "required": ["reviewer_task_binding"] } },
{ "not": { "required": ["candidate_diff"] } },
{ "not": { "required": ["changed_path_manifest"] } }
]
}
},
{
"if": { "properties": { "capture_operation": { "const": "review.capture-evidence" } }, "required": ["capture_operation"] },
"then": { "properties": { "schema": { "const": "gentle-ai.review-verification-evidence/v1" } } }
}
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://gentle-ai.dev/contracts/review-integration/v1/schemas/verification-evidence.schema.json",
"title": "Gentle AI final verification evidence",
"type": "object",
"additionalProperties": false,
"required": ["schema", "outcome", "checks"],
"properties": {
"schema": { "const": "gentle-ai.review-verification-evidence/v1" },
"outcome": { "enum": ["passed", "failed"] },
"checks": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["name", "status", "evidence"],
"properties": {
"name": { "type": "string", "pattern": "\\S" },
"status": { "enum": ["passed", "failed"] },
"command": { "type": "string", "pattern": "\\S" },
"evidence": {
"type": "array",
"minItems": 1,
"items": { "type": "string", "pattern": "\\S" }
}
}
}
}
},
"examples": [
{
"schema": "gentle-ai.review-verification-evidence/v1",
"outcome": "passed",
"checks": [
{
"name": "focused tests",
"status": "passed",
"command": "go test ./internal/cli",
"evidence": ["ok github.com/gentleman-programming/gentle-ai/internal/cli"]
}
]
}
]
}
2 changes: 1 addition & 1 deletion docs/review-authority-threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Frozen intended-untracked membership proves what entered the reviewed candidate

## Review Input Schemas

Run `gentle-ai review schema reviewer`, `gentle-ai review schema refuter`, `gentle-ai review schema validator`, or `gentle-ai review schema final-verification-incident` to print a versioned JSON Schema. Final verification evidence remains arbitrary non-empty bytes and therefore has no invented JSON contract. The incident schema is different: its native parser accepts only compact canonical JSON plus one LF, rejects unknown fields, and admits only `procedural_tooling_failure`. Unknown JSON fields and semantic violations remain rejected before authority changes.
Run `gentle-ai review schema reviewer`, `gentle-ai review schema refuter`, `gentle-ai review schema validator`, `gentle-ai review schema verification-evidence`, or `gentle-ai review schema final-verification-incident` to print a versioned JSON Schema. Provider-owned final verification evidence uses `gentle-ai.review-verification-evidence/v1`: the provider records an overall `passed` or `failed` outcome and one or more named checks with explicit evidence, while the native capture operation supplies lineage, target, revision, canonical bytes, and hashes. Pass the payload as a file to the `review.capture-evidence` transition emitted by `review status --next-transition`; once captured, lineage-only `review finalize` discovers it and preserves exact authority bindings. The incident schema is different: its native parser accepts only compact canonical JSON plus one LF, rejects unknown fields, and admits only `procedural_tooling_failure`. Unknown JSON fields and semantic violations remain rejected before authority changes.

An ordinary bounded lineage permits exactly one changed-target correction attempt. The initial lenses and frozen finding IDs execute once, while that correction records its snapshot, validation checks, and changed-line charge without expanding immutable genesis paths or the frozen budget. Consuming the attempt exhausts ordinary correction even when its measured delta is zero; a later change requires an authorized successor rather than another fix transition. Historical multi-attempt records remain readable, but cannot append another attempt.

Expand Down
10 changes: 8 additions & 2 deletions docs/review-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,12 @@ When a release merge retains an approved `current-changes` candidate but expands

Malformed reviewer JSON, missing required reviewer arrays, canonicalization failures, and selected-lens mismatches are deterministic preflight failures. Negotiated finalize reports `invalid_request`, `mutation_outcome: not_started`, `retry_safe: true`, `replayability: not_replayable`, and `next_action: correct_request`, while preserving a valid requested lineage for target-scoped recovery. Correct the payload before retrying; do not run authority repair.

### Complete final verification from validating

When negotiated status emits `collect / verification_evidence_required`, use its exact `review.capture-evidence` operation and lineage, target, and expected-revision arguments. Obtain the public payload contract with `gentle-ai review schema verification-evidence`, write the provider-owned `gentle-ai.review-verification-evidence/v1` payload to a file, and pass that file with `--input`. The provider records check names, statuses, optional commands, and concrete result evidence; native capture canonicalizes the JSON and derives all bindings and hashes.

After capture, status emits `execute / captured_verification_evidence_ready`. Lineage-only `review finalize` also discovers the same canonical evidence after restart, derives pass or failure from structured evidence, and publishes or discovers the terminal receipt idempotently. Do not replay `--captured-results` after authority leaves `reviewing`: negotiated finalize rejects that stale selector without mutation and directs the caller to `review.status` for the current transition.

### Reopen unusable validating results without another budget

`gentle-ai review reopen-results` is a bounded maintenance operation for an uncorrected validating authority whose historical reviewer artifact was unadmitted or whose preserved evidence says candidate inspection was unavailable. It never starts a lineage or recalculates target, tier, lenses, changed-line count, or correction budget.
Expand Down Expand Up @@ -304,8 +310,8 @@ Pi adoption, fallback retirement, package pinning, and Pi release sequencing are

Each release archive contains:

- `contracts/review-integration/v1/schemas/` — twenty strict JSON Schemas, including preserved capability protocols v1.0–v1.3, current v1.4, versioned START/status/result-artifact contracts, final-verification incident, classified repair, provider subject/admission, and targeted validation.
- `contracts/review-integration/v1/fixtures/` — twenty-four deterministic conformance fixtures, including all five capability minors, preserved v1 plus current v2 START/status examples, the final-verification incident and retry projection, classified repair preflight, and typed failure envelopes.
- `contracts/review-integration/v1/schemas/` — twenty-one strict JSON Schemas, including preserved capability protocols v1.0–v1.3, current v1.4, versioned START/status/result-artifact contracts, final-verification evidence and incident, classified repair, provider subject/admission, and targeted validation.
- `contracts/review-integration/v1/fixtures/` — twenty-five deterministic conformance fixtures, including all five capability minors, preserved v1 plus current v2 START/status examples, final-verification evidence, the final-verification incident and retry projection, classified repair preflight, and typed failure envelopes.
- `docs/review-integration.md` — this ownership and consumption guide.

Repository maintainers can verify source inventory or a complete GoReleaser snapshot:
Expand Down
5 changes: 4 additions & 1 deletion internal/assets/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,10 @@ func TestReviewResultArtifactsPluginContract(t *testing.T) {
`artifact_subject`,
`candidate_diff`,
`changed_path_manifest`,
`output.args.prompt = await injectReviewerContext(`,
`const injected = await injectReviewerContext(`,
`retainedPreflight.set(bindingKey(parseBinding(output.args.prompt, output.args.subagent_type)), injected.preflight)`,
`result = enrichedReviewerResult(binding, result, preflight)`,
`inspection: { status: "completed", paths }`,
`"--lineage", binding.lineage`,
`"--target", binding.target`,
`"--lens", binding.lens`,
Expand Down
75 changes: 68 additions & 7 deletions internal/assets/opencode/plugins/review-result-artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ interface ReviewCapturePreflight {
changed_path_manifest: Array<Record<string, unknown>>
}

function validManifestPath(path: unknown): path is string {
return typeof path === "string" && path !== "" && !path.startsWith("/") && !path.includes("\\") &&
path.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..")
}

function bindingKey(binding: ReviewBinding): string {
return JSON.stringify([
binding.lineage, binding.target, binding.lens, binding.order,
binding.revision ?? "", binding.repository_context ?? "",
])
}

function parseBinding(prompt: unknown, lens: string): ReviewBinding {
const match = BINDING.exec(typeof prompt === "string" ? prompt : "")
if (!match) throw new Error("review task is missing GENTLE_AI_REVIEW_BINDING")
Expand Down Expand Up @@ -139,9 +151,11 @@ async function preflightCapture(cwd: string, binding: ReviewBinding): Promise<Re
}
const value = parsed as Record<string, unknown>
const subject = value.artifact_subject as Record<string, unknown> | undefined
const manifest = value.changed_path_manifest as Array<Record<string, unknown>> | undefined
if (!subject || typeof subject.subject_hash !== "string" || !/^sha256:[a-f0-9]{64}$/.test(subject.subject_hash) ||
!value.candidate_diff || typeof value.candidate_diff !== "object" || Array.isArray(value.candidate_diff) ||
!Array.isArray(value.changed_path_manifest) || value.changed_path_manifest.some((entry) => !entry || typeof entry !== "object" || Array.isArray(entry))) {
!Array.isArray(manifest) || manifest.some((entry) => !entry || typeof entry !== "object" || Array.isArray(entry) || !validManifestPath(entry.path)) ||
new Set(manifest.map((entry) => entry.path)).size !== manifest.length) {
throw new Error("review capture preflight returned incomplete frozen candidate context")
}
if (binding.subject_hash && subject.subject_hash !== binding.subject_hash) {
Expand Down Expand Up @@ -169,18 +183,54 @@ async function preflightCapture(cwd: string, binding: ReviewBinding): Promise<Re
}
}

async function injectReviewerContext(prompt: string, lens: string, cwd: string): Promise<string> {
async function injectReviewerContext(prompt: string, lens: string, cwd: string): Promise<{prompt: string, preflight?: ReviewCapturePreflight}> {
const binding = parseBinding(prompt, lens)
const preflight = await preflightCapture(cwd, binding)
if (!preflight) return prompt
if (!preflight) {
if (binding.subject_hash) throw new Error("provider-bound review task requires capture preflight")
return { prompt }
}
const injectedBinding = { ...binding, subject_hash: preflight.artifact_subject.subject_hash }
const boundPrompt = prompt.replace(BINDING, `GENTLE_AI_REVIEW_BINDING ${JSON.stringify(injectedBinding)}\n`)
const frozen = JSON.stringify({
artifact_subject: preflight.artifact_subject,
candidate_diff: preflight.candidate_diff,
changed_path_manifest: preflight.changed_path_manifest,
})
return `${boundPrompt.trimEnd()}\n${FROZEN_CONTEXT}${frozen}`
return { prompt: `${boundPrompt.trimEnd()}\n${FROZEN_CONTEXT}${frozen}`, preflight }
}

function enrichedReviewerResult(binding: ReviewBinding, result: string, preflight: ReviewCapturePreflight | undefined): string {
if (!preflight) {
if (binding.subject_hash) throw new Error("review task is missing retained provider context")
return result
}
if (binding.subject_hash !== preflight.artifact_subject.subject_hash) {
throw new Error("review task binding does not match retained provider context")
}
const paths = preflight.changed_path_manifest.map((entry) => entry.path)
if (paths.some((path) => typeof path !== "string" || path === "")) {
throw new Error("retained provider context contains a malformed changed-path manifest")
}
let parsed: unknown
try {
parsed = JSON.parse(result)
} catch {
throw new Error("reviewer result is not strict JSON")
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("reviewer result must be an object")
}
const value = parsed as Record<string, unknown>
if (Object.keys(value).sort().join(",") !== "evidence,findings" || !Array.isArray(value.findings) || !Array.isArray(value.evidence)) {
throw new Error("reviewer result must contain only findings and evidence")
}
return JSON.stringify({
subject_hash: preflight.artifact_subject.subject_hash,
inspection: { status: "completed", paths },
findings: value.findings,
evidence: value.evidence,
})
}

function preserveResult(cwd: string, binding: ReviewBinding, raw: string, cls?: string): Promise<string> {
Expand Down Expand Up @@ -250,18 +300,24 @@ async function preservedCaptureFailure(cwd: string, binding: ReviewBinding, raw:
}
}

const ReviewResultArtifactsPlugin: Plugin = async ({ directory, worktree }) => ({
const ReviewResultArtifactsPlugin: Plugin = async ({ directory, worktree }) => {
const retainedPreflight = new Map<string, ReviewCapturePreflight>()
return {
"tool.execute.before": async (input, output) => {
if (input.tool !== "task" || typeof output.args?.subagent_type !== "string" ||
!REVIEW_AGENTS.has(output.args.subagent_type) || !BINDING.test(output.args.prompt)) return
if (output.args.background === true) {
throw new Error("bound review tasks must run in the foreground for native result capture")
}
output.args.prompt = await injectReviewerContext(
const injected = await injectReviewerContext(
output.args.prompt,
output.args.subagent_type,
captureCwd(worktree, directory),
)
output.args.prompt = injected.prompt
if (injected.preflight) {
retainedPreflight.set(bindingKey(parseBinding(output.args.prompt, output.args.subagent_type)), injected.preflight)
}
},
"tool.execute.after": async (input, output) => {
if (input.tool !== "task" || typeof input.args?.subagent_type !== "string" || !REVIEW_AGENTS.has(input.args.subagent_type)) return
Expand All @@ -283,11 +339,16 @@ const ReviewResultArtifactsPlugin: Plugin = async ({ directory, worktree }) => (
throw await preservedCaptureFailure(cwd, binding, output.output, cause)
}
try {
const key = bindingKey(binding)
const preflight = retainedPreflight.get(key)
retainedPreflight.delete(key)
result = enrichedReviewerResult(binding, result, preflight)
output.output = await captureResult(cwd, binding, result)
} catch (cause) {
throw await preservedCaptureFailure(cwd, binding, result, cause)
}
},
})
}
}

export default ReviewResultArtifactsPlugin
Loading