gentle-pi 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -12
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -1
- package/assets/agents/review-reliability.md +4 -1
- package/assets/agents/review-resilience.md +4 -1
- package/assets/agents/review-risk.md +4 -1
- package/assets/agents/sdd-apply.md +6 -1
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +6 -1
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +6 -1
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +6 -1
- package/assets/agents/sdd-verify.md +6 -1
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +246 -67
- package/assets/orchestrator.md +7 -14
- package/assets/sdd-orchestrator-workflow.md +154 -9
- package/assets/support/sdd-status-contract.md +19 -1
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +5 -5
- package/docs/review-integration.md +22 -2
- package/extensions/gentle-ai.ts +1595 -201
- package/extensions/sdd-init.ts +19 -6
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/git-commit-transaction.ts +77 -17
- package/lib/native-review-cli.ts +528 -65
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +527 -18
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +436 -0
- package/lib/review-integration-v2.ts +537 -36
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +53 -1
- package/package.json +5 -2
- package/runtime/gentle-ai-binary.mjs +173 -2
- package/runtime/git-commit-transaction.mjs +75 -15
- package/runtime/native-review-cli.mjs +524 -61
- package/runtime/review-integration-v2.mjs +536 -35
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/build-git-commit-transaction-runner.mjs +1 -0
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +23 -13
- package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/verify-package-files.mjs +110 -33
- package/skills/_shared/review-ledger-contract.md +4 -6
- package/skills/gentle-ai/SKILL.md +4 -4
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +7 -1
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/crosslane/cross-lane.mjs +1169 -0
- package/tests/delegated-key-learnings-contract.test.ts +238 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +336 -0
- package/tests/gentle-ai-installer.test.ts +46 -46
- package/tests/git-commit-transaction.test.ts +229 -1
- package/tests/maintainer/provider-relay.maintest.ts +265 -0
- package/tests/native-review-capability-contract.test.ts +48 -2
- package/tests/native-review-cli.test.ts +56 -0
- package/tests/native-review-consent.test.ts +164 -3
- package/tests/native-review-parity-runtime.test.ts +37 -0
- package/tests/native-review-parity.test.ts +218 -15
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/orchestrator-budget.test.ts +30 -5
- package/tests/package-manifest.test.ts +98 -72
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +355 -0
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-candidate-view.test.ts +489 -9
- package/tests/review-compact-contract.test.ts +52 -119
- package/tests/review-controller-native-recovery.test.ts +643 -47
- package/tests/review-controller-native-routing.test.ts +1667 -222
- package/tests/review-controller-workspace-root.test.ts +17 -2
- package/tests/review-corrected-finalize-binding.test.ts +175 -0
- package/tests/review-dispatch-hydration-gap.test.ts +197 -0
- package/tests/review-host-relay-routing.test.ts +317 -0
- package/tests/review-host-relay.test.ts +520 -0
- package/tests/review-integration-v2-forward.test.ts +631 -0
- package/tests/review-integration-v2.test.ts +114 -0
- package/tests/review-ledger-contract.test.ts +12 -28
- package/tests/review-recovered-lineage-routing.test.ts +246 -0
- package/tests/review-relay-transport-agent.test.ts +249 -0
- package/tests/runtime-harness.mjs +242 -14
- package/tests/sdd-agent-tools.test.ts +18 -33
- package/tests/skill-collision-prefixes.test.ts +1 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -93,23 +93,38 @@ export const REVIEW_STATUS_ACTION_DISPOSITION = {
|
|
|
93
93
|
export type ReviewStatusActionDisposition = (typeof REVIEW_STATUS_ACTION_DISPOSITION)[keyof typeof REVIEW_STATUS_ACTION_DISPOSITION];
|
|
94
94
|
const RECEIPT_STATUSES = ["expected_missing", "present", "publication_pending", "not_applicable"] as const;
|
|
95
95
|
const REQUIRED_OPERATIONS = Object.freeze(Object.values(REVIEW_INTEGRATION_OPERATION));
|
|
96
|
+
// failure/v2 operations: the capability floor plus the four collect-capture
|
|
97
|
+
// verbs that emit typed refusals on the gentle-ai main line (commit a2d57117,
|
|
98
|
+
// fix/capture-evidence-typed-refusal; exact strings from that branch's
|
|
99
|
+
// published failure.schema.json 12-value operation enum). Additive forward
|
|
100
|
+
// surface: every operation outside the published enum still rejects.
|
|
101
|
+
const FAILURE_OPERATIONS = Object.freeze([
|
|
102
|
+
...REQUIRED_OPERATIONS,
|
|
103
|
+
"review.capture-result",
|
|
104
|
+
"review.capture-evidence",
|
|
105
|
+
"review.capture-refuter",
|
|
106
|
+
"review.capture-validation",
|
|
107
|
+
] as const);
|
|
96
108
|
const REQUIRED_GATES = Object.freeze(["post-apply", "pre-commit", "pre-push", "pre-pr", "release"] as const);
|
|
97
109
|
const REQUIRED_PROJECTIONS = Object.freeze(Object.values(REVIEW_PROJECTION));
|
|
98
|
-
|
|
110
|
+
// The schema floor shared by every accepted capabilities identity. Each minor
|
|
111
|
+
// then adds its own capabilities/consent/status identities below: the provider
|
|
112
|
+
// swaps those three advertisements as the protocol minor advances (v2.1 moved
|
|
113
|
+
// consent to v3; v2.2 moved status to v5), ground-truthed against the vendored
|
|
114
|
+
// capabilities[-v2.1|-v2.2].schema.json contracts on gentle-ai main and a live
|
|
115
|
+
// v2.2 capture from a main-line dev build.
|
|
116
|
+
const REQUIRED_SCHEMAS_COMMON = Object.freeze([
|
|
99
117
|
"gentle-ai.review-admitted-result/v2",
|
|
100
118
|
"gentle-ai.review-artifact-subject/v2",
|
|
101
119
|
"gentle-ai.review-authority-repair-assessment/v1",
|
|
102
120
|
"gentle-ai.review-authority-status/v1",
|
|
103
121
|
"gentle-ai.review-gate-request/v1",
|
|
104
|
-
"gentle-ai.review-integration.capabilities/v2",
|
|
105
|
-
"gentle-ai.review-integration.consent/v2",
|
|
106
122
|
"gentle-ai.review-integration.failure/v2",
|
|
107
123
|
"gentle-ai.review-final-verification-incident/v1",
|
|
108
124
|
"gentle-ai.review-integration.operation/v2",
|
|
109
125
|
"gentle-ai.review-integration.projection/v1",
|
|
110
126
|
"gentle-ai.review-integration.repair/v2",
|
|
111
127
|
"gentle-ai.review-integration.start/v3",
|
|
112
|
-
"gentle-ai.review-integration.status/v3",
|
|
113
128
|
"gentle-ai.review-receipt/v1",
|
|
114
129
|
"gentle-ai.review-receipt/v2",
|
|
115
130
|
"gentle-ai.review-result-artifact/v2",
|
|
@@ -119,6 +134,20 @@ const REQUIRED_SCHEMAS = Object.freeze([
|
|
|
119
134
|
"https://gentle-ai.dev/schema/review/reviewer/v1",
|
|
120
135
|
"https://gentle-ai.dev/schema/review/validator/v1",
|
|
121
136
|
] as const);
|
|
137
|
+
const CAPABILITIES_SCHEMA_IDENTITIES: Readonly<Record<string, { protocolMinor: number; requiredSchemas: readonly string[] }>> = Object.freeze({
|
|
138
|
+
"gentle-ai.review-integration.capabilities/v2": Object.freeze({
|
|
139
|
+
protocolMinor: 0,
|
|
140
|
+
requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2", "gentle-ai.review-integration.consent/v2", "gentle-ai.review-integration.status/v3"]),
|
|
141
|
+
}),
|
|
142
|
+
"gentle-ai.review-integration.capabilities/v2.1": Object.freeze({
|
|
143
|
+
protocolMinor: 1,
|
|
144
|
+
requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.1", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.status/v3"]),
|
|
145
|
+
}),
|
|
146
|
+
"gentle-ai.review-integration.capabilities/v2.2": Object.freeze({
|
|
147
|
+
protocolMinor: 2,
|
|
148
|
+
requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.2", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.status/v5"]),
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
122
151
|
const OPTIONAL_FEATURE_NAMES = Object.freeze([
|
|
123
152
|
"base_ref_workspace_overlay",
|
|
124
153
|
"bounded_process_waits",
|
|
@@ -235,8 +264,19 @@ export interface ReviewRepositoryContextV2 {
|
|
|
235
264
|
handle: string;
|
|
236
265
|
revision: string;
|
|
237
266
|
targetIdentity: string;
|
|
267
|
+
// Optional additive members on the same start/v3 identity (gentle-ai main,
|
|
268
|
+
// Go `ReviewRepositoryContextReference` with `omitempty`): the compact
|
|
269
|
+
// effects event this context applied and its recorded outcome. Ground
|
|
270
|
+
// truth is the captured granted start from a 2.4.0-main binary
|
|
271
|
+
// (tests/fixtures/devbinary/start-v3-consent-granted.captured.json); the
|
|
272
|
+
// published start.schema.json is narrower than the emitter here.
|
|
273
|
+
eventId?: string;
|
|
274
|
+
outcome?: ReviewRepositoryContextOutcomeV1;
|
|
238
275
|
}
|
|
239
276
|
|
|
277
|
+
const REPOSITORY_CONTEXT_OUTCOMES = ["applied", "pending", "blocked_conflict", "durability_limited"] as const;
|
|
278
|
+
export type ReviewRepositoryContextOutcomeV1 = (typeof REPOSITORY_CONTEXT_OUTCOMES)[number];
|
|
279
|
+
|
|
240
280
|
export interface ReviewStartV3 {
|
|
241
281
|
contract: typeof REVIEW_INTEGRATION_CONTRACT;
|
|
242
282
|
action: StartAction;
|
|
@@ -312,6 +352,109 @@ export interface ReviewNextTransitionExecuteV3 {
|
|
|
312
352
|
command?: string;
|
|
313
353
|
}
|
|
314
354
|
|
|
355
|
+
// Provider-owned completing form for a host-mediated capture slot
|
|
356
|
+
// (gentle-pi#311 P4). The provider issues the exact operation and argument
|
|
357
|
+
// tokens that submit the captured bytes; the host substitutes only the
|
|
358
|
+
// artifact location into the declared {{value}} slot and never synthesizes
|
|
359
|
+
// or filters the form itself.
|
|
360
|
+
export interface ReviewCaptureSubmissionValueV1 {
|
|
361
|
+
slot: string;
|
|
362
|
+
domain: string;
|
|
363
|
+
substitutionLocation: number;
|
|
364
|
+
/**
|
|
365
|
+
* status/v5 only: the artifact schema the substituted value must satisfy.
|
|
366
|
+
* NEW optional member carried by the singular wire `value` form the live
|
|
367
|
+
* 2.4.0-main binary emits for the materialize capture-result slot
|
|
368
|
+
* (captured 2026-08-16 from 2.4.0-main.b1afef46); the legacy `values`
|
|
369
|
+
* array rows never carry it and existing consumers stay untouched.
|
|
370
|
+
*/
|
|
371
|
+
schema?: string;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export interface ReviewCaptureSubmissionV1 {
|
|
375
|
+
operationToken: string;
|
|
376
|
+
argumentTokens: readonly string[];
|
|
377
|
+
values: readonly ReviewCaptureSubmissionValueV1[];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// The two Go-owned non-lens provider role capture operations (gentle-pi#311
|
|
381
|
+
// P4-roles; provider side gentle-ai#3264). Their collect inputs are
|
|
382
|
+
// self-contained authority-advancing vectors: binding tokens plus
|
|
383
|
+
// `--agent=pi --execute=true`, with NO submission descriptor. The known set
|
|
384
|
+
// is closed — an unknown role capture operation is never executed.
|
|
385
|
+
export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION = {
|
|
386
|
+
CAPTURE_REFUTER: "review.capture-refuter",
|
|
387
|
+
CAPTURE_VALIDATION: "review.capture-validation",
|
|
388
|
+
} as const;
|
|
389
|
+
export type ReviewProviderRoleCaptureOperation = (typeof REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION)[keyof typeof REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION];
|
|
390
|
+
export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS = Object.freeze(Object.values(REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION));
|
|
391
|
+
|
|
392
|
+
// status/v5 structural surfaces. Pi has no consumer for these yet; they are
|
|
393
|
+
// decoded strictly (exact key sets per the vendored status-v5.schema.json and
|
|
394
|
+
// correction-plan-request.schema.json on gentle-ai main) and carried through
|
|
395
|
+
// so a v5 provider payload is never rejected for being newer than v3.
|
|
396
|
+
export interface ReviewForecastStepV1 {
|
|
397
|
+
step: 1;
|
|
398
|
+
kind: "execute" | "collect" | "stop";
|
|
399
|
+
reasonCode: string;
|
|
400
|
+
description: string;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export interface ReviewForecastV1 {
|
|
404
|
+
horizon: "partial" | "terminal";
|
|
405
|
+
steps: readonly ReviewForecastStepV1[];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export interface ReviewProviderTaskV1 {
|
|
409
|
+
agent: "review-refuter" | "review-validator";
|
|
410
|
+
role: "refuter" | "targeted-validator";
|
|
411
|
+
prompt: string;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export interface ReviewSubmissionDescriptorValueV5 {
|
|
415
|
+
slot: string;
|
|
416
|
+
domain: string;
|
|
417
|
+
substitutionLocation: number;
|
|
418
|
+
schema?: string;
|
|
419
|
+
minimum?: number;
|
|
420
|
+
maximum?: number;
|
|
421
|
+
allowedValues?: readonly string[];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// The two v5 Go-owned submission descriptor forms (finalize carries one
|
|
425
|
+
// singular `value`; capture-evidence carries an outcome/input `values` pair).
|
|
426
|
+
// Distinct from the host-relay ReviewCaptureSubmissionV1 form, which stays
|
|
427
|
+
// exactly as it is.
|
|
428
|
+
export interface ReviewSubmissionDescriptorV5 {
|
|
429
|
+
operationToken: "finalize" | "capture-evidence";
|
|
430
|
+
argumentTokens: readonly string[];
|
|
431
|
+
value?: ReviewSubmissionDescriptorValueV5;
|
|
432
|
+
values?: readonly ReviewSubmissionDescriptorValueV5[];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export interface ReviewCorrectionPlanFindingV1 {
|
|
436
|
+
id: string;
|
|
437
|
+
lens: "risk" | "resilience" | "readability" | "reliability";
|
|
438
|
+
location: string;
|
|
439
|
+
severity: "BLOCKER" | "CRITICAL";
|
|
440
|
+
claim: string;
|
|
441
|
+
proofRefs: readonly string[];
|
|
442
|
+
evidence: string;
|
|
443
|
+
evidenceClass: "deterministic" | "inferential";
|
|
444
|
+
causalDisposition: "introduced" | "behavior-activated" | "worsened";
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export interface ReviewCorrectionPlanRequestV1 {
|
|
448
|
+
schema: "gentle-ai.review-correction-plan-request/v1";
|
|
449
|
+
requestHash: string;
|
|
450
|
+
lineageId: string;
|
|
451
|
+
expectedRevision: string;
|
|
452
|
+
targetIdentity: string;
|
|
453
|
+
correctionBudget: number;
|
|
454
|
+
fixFindingIds: readonly string[];
|
|
455
|
+
findings: readonly ReviewCorrectionPlanFindingV1[];
|
|
456
|
+
}
|
|
457
|
+
|
|
315
458
|
export interface ReviewCollectInputV3 {
|
|
316
459
|
name: string;
|
|
317
460
|
schema: string;
|
|
@@ -322,6 +465,11 @@ export interface ReviewCollectInputV3 {
|
|
|
322
465
|
candidateTree?: string;
|
|
323
466
|
changedPathManifest?: readonly ChangedPathEntry[];
|
|
324
467
|
validationRequest?: ReviewTargetedValidationRequestV1;
|
|
468
|
+
submission?: ReviewCaptureSubmissionV1;
|
|
469
|
+
/** status/v5 only: the finalize/capture-evidence descriptor forms. */
|
|
470
|
+
submissionDescriptor?: ReviewSubmissionDescriptorV5;
|
|
471
|
+
/** status/v5 only: the self-contained external.run_provider_role task. */
|
|
472
|
+
providerTask?: ReviewProviderTaskV1;
|
|
325
473
|
}
|
|
326
474
|
|
|
327
475
|
export interface ReviewNextTransitionV3 {
|
|
@@ -329,6 +477,8 @@ export interface ReviewNextTransitionV3 {
|
|
|
329
477
|
reasonCode: string;
|
|
330
478
|
execute?: ReviewNextTransitionExecuteV3;
|
|
331
479
|
collect?: { inputs: readonly ReviewCollectInputV3[] };
|
|
480
|
+
/** status/v5 only: the bounded correction plan request. */
|
|
481
|
+
correctionRequest?: ReviewCorrectionPlanRequestV1;
|
|
332
482
|
}
|
|
333
483
|
|
|
334
484
|
export interface ReviewTargetedValidationRequestV1 {
|
|
@@ -373,6 +523,15 @@ export interface ReviewStatusV3 {
|
|
|
373
523
|
nextTransition?: ReviewNextTransitionV3;
|
|
374
524
|
validationRequest?: ReviewTargetedValidationRequestV1;
|
|
375
525
|
finalVerificationRetry?: ReviewFinalVerificationRetryV1;
|
|
526
|
+
/** status/v5 only: the descriptive, non-routing transition preview. */
|
|
527
|
+
forecast?: ReviewForecastV1;
|
|
528
|
+
/**
|
|
529
|
+
* status/v5 only: the opaque repository-context reference the live
|
|
530
|
+
* 2.4.0-main binary publishes once a reviewing lineage has bound one.
|
|
531
|
+
* NEW struct member — captured 2026-08-16 from 2.4.0-main.b1afef46; the
|
|
532
|
+
* published status-v5.schema.json omits it (capture is authoritative).
|
|
533
|
+
*/
|
|
534
|
+
repositoryContext?: ReviewRepositoryContextV2;
|
|
376
535
|
raw: Readonly<Record<string, unknown>>;
|
|
377
536
|
}
|
|
378
537
|
|
|
@@ -403,6 +562,19 @@ export interface ReviewConsentV2 {
|
|
|
403
562
|
raw: Readonly<Record<string, unknown>>;
|
|
404
563
|
}
|
|
405
564
|
|
|
565
|
+
// consent/v3 (gentle-ai >= 2.3.0, capabilities/v2.1+): the same per-candidate
|
|
566
|
+
// blocking question with one net-new required member, the provider-fixed
|
|
567
|
+
// `agent` runtime binding. Everything shared with v2 keeps its exact shape so
|
|
568
|
+
// consumers of either identity read one structural surface.
|
|
569
|
+
export interface ReviewConsentV3 extends Omit<ReviewConsentV2, "schema"> {
|
|
570
|
+
schema: "gentle-ai.review-integration.consent/v3";
|
|
571
|
+
agent: "claude-code";
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Either accepted consent identity. Consumers that relay the envelope (rather
|
|
575
|
+
// than decode it) accept both; each decoder still admits exactly one schema.
|
|
576
|
+
export type ReviewConsentEnvelope = ReviewConsentV2 | ReviewConsentV3;
|
|
577
|
+
|
|
406
578
|
const FAILURE_REQUIRED_INPUTS = [
|
|
407
579
|
"lineage_id",
|
|
408
580
|
"change",
|
|
@@ -453,10 +625,12 @@ export interface ReviewFailureContextV2 {
|
|
|
453
625
|
bindingRevision?: ReviewFailureBindingRevisionV1;
|
|
454
626
|
}
|
|
455
627
|
|
|
628
|
+
export type ReviewFailureOperation = ReviewIntegrationOperation | "review.capture-result" | "review.capture-evidence" | "review.capture-refuter" | "review.capture-validation";
|
|
629
|
+
|
|
456
630
|
export interface ReviewFailureV2 {
|
|
457
631
|
schema: "gentle-ai.review-integration.failure/v2";
|
|
458
632
|
contract: typeof REVIEW_INTEGRATION_CONTRACT;
|
|
459
|
-
operation:
|
|
633
|
+
operation: ReviewFailureOperation;
|
|
460
634
|
phase: "preflight" | "pre_native" | "native_running" | "native_committed" | "reconciliation";
|
|
461
635
|
code: string;
|
|
462
636
|
message: string;
|
|
@@ -689,10 +863,15 @@ function decodeOptionalFeature(value: unknown, label: string): { name: string; s
|
|
|
689
863
|
export function decodeReviewCapabilitiesV2(value: unknown, verifiedExecutableDigest: string): ReviewCapabilitiesV2 {
|
|
690
864
|
const requiredFields = ["schema", "contract", "protocol", "package", "build", "executable", "operations", "gates", "projections", "schemas", "features", "compatibility"] as const;
|
|
691
865
|
const body = exactRecord(value, "capabilities", requiredFields, ["bootstrap"]);
|
|
692
|
-
|
|
866
|
+
// Additive minor acceptance: each accepted schema identity keeps its own
|
|
867
|
+
// exact protocol minor and required-schema floor; v2 stays exactly as it
|
|
868
|
+
// was, and an unknown identity is rejected before anything else.
|
|
869
|
+
const identity = CAPABILITIES_SCHEMA_IDENTITIES[typeof body.schema === "string" ? body.schema : ""];
|
|
870
|
+
if (identity === undefined) throw new TypeError(`schema must be one of ${Object.keys(CAPABILITIES_SCHEMA_IDENTITIES).join(", ")}`);
|
|
871
|
+
requireIdentity(body, body.schema as string);
|
|
693
872
|
|
|
694
873
|
const protocol = exactRecord(body.protocol, "capabilities.protocol", ["major", "minor"]);
|
|
695
|
-
if (protocol.major !== 2 || protocol.minor !==
|
|
874
|
+
if (protocol.major !== 2 || protocol.minor !== identity.protocolMinor) throw new TypeError("incompatible review integration protocol");
|
|
696
875
|
|
|
697
876
|
const packageIdentity = exactRecord(body.package, "capabilities.package", ["name", "version", "release_channel"]);
|
|
698
877
|
if (packageIdentity.name !== "gentle-ai") throw new TypeError("capabilities package identity mismatch");
|
|
@@ -712,13 +891,18 @@ export function decodeReviewCapabilitiesV2(value: unknown, verifiedExecutableDig
|
|
|
712
891
|
if (selfReportedDigest !== normalizedVerifiedDigest) throw new TypeError("review provider executable digest mismatch");
|
|
713
892
|
|
|
714
893
|
const advertisedOperations = stringArray(body.operations, "capabilities.operations", { minimum: REQUIRED_OPERATIONS.length, unique: true });
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
894
|
+
// Gates and projections are, like operations and schemas, a superset promise
|
|
895
|
+
// rather than an exact manifest: a compatible provider release may advertise
|
|
896
|
+
// an additional gate or projection name beyond the required floor. Decode as
|
|
897
|
+
// a plain string array (not `enumArray` against the known enum) so an
|
|
898
|
+
// unknown addition is not rejected before assertSupersetOf can even run.
|
|
899
|
+
const advertisedGates = stringArray(body.gates, "capabilities.gates", { minimum: REQUIRED_GATES.length, unique: true });
|
|
900
|
+
const advertisedProjections = stringArray(body.projections, "capabilities.projections", { minimum: REQUIRED_PROJECTIONS.length, unique: true });
|
|
901
|
+
const advertisedSchemas = stringArray(body.schemas, "capabilities.schemas", { minimum: identity.requiredSchemas.length, unique: true });
|
|
718
902
|
assertSupersetOf(advertisedOperations, REQUIRED_OPERATIONS, "capabilities operations");
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
assertSupersetOf(advertisedSchemas,
|
|
903
|
+
assertSupersetOf(advertisedGates, REQUIRED_GATES, "capabilities gates");
|
|
904
|
+
assertSupersetOf(advertisedProjections, REQUIRED_PROJECTIONS, "capabilities projections");
|
|
905
|
+
assertSupersetOf(advertisedSchemas, identity.requiredSchemas, "capabilities schemas");
|
|
722
906
|
|
|
723
907
|
const features = exactRecord(body.features, "capabilities.features", ["mandatory", "optional"]);
|
|
724
908
|
const mandatory = array(features.mandatory, "capabilities.features.mandatory", (entry, label) => decodeFeature(entry, label), { minimum: 10 });
|
|
@@ -764,9 +948,9 @@ export function decodeReviewCapabilitiesV2(value: unknown, verifiedExecutableDig
|
|
|
764
948
|
buildId,
|
|
765
949
|
executableDigest: selfReportedDigest,
|
|
766
950
|
operations: new Set(REQUIRED_OPERATIONS),
|
|
767
|
-
gates: new Set(
|
|
768
|
-
projections: new Set(
|
|
769
|
-
schemas: new Set(
|
|
951
|
+
gates: new Set(REQUIRED_GATES),
|
|
952
|
+
projections: new Set(REQUIRED_PROJECTIONS),
|
|
953
|
+
schemas: new Set(identity.requiredSchemas),
|
|
770
954
|
mandatoryFeatures: new Set(mandatoryNames),
|
|
771
955
|
optionalFeatures: new Set(optional.filter((feature) => feature.supported && (FEATURE_NAMES as readonly string[]).includes(feature.name)).map((feature) => feature.name)),
|
|
772
956
|
raw: body,
|
|
@@ -857,13 +1041,15 @@ export function decodeReviewStartV3(value: unknown): ReviewStartV3 {
|
|
|
857
1041
|
|
|
858
1042
|
let repositoryContext: ReviewRepositoryContextV2 | undefined;
|
|
859
1043
|
if (body.repository_context !== undefined) {
|
|
860
|
-
const source = exactRecord(body.repository_context, "start.repository_context", ["capability", "handle", "revision", "target_identity"]);
|
|
1044
|
+
const source = exactRecord(body.repository_context, "start.repository_context", ["capability", "handle", "revision", "target_identity"], ["event_id", "outcome"]);
|
|
861
1045
|
if (source.capability !== "review.opaque_repository_context") throw new TypeError("start.repository_context.capability is unsupported");
|
|
862
1046
|
repositoryContext = {
|
|
863
1047
|
capability: "review.opaque_repository_context",
|
|
864
1048
|
handle: text(source.handle, "start.repository_context.handle", { pattern: /^rctx1_[0-9a-f]{64}$/ }),
|
|
865
1049
|
revision: sha256(source.revision, "start.repository_context.revision"),
|
|
866
1050
|
targetIdentity: sha256(source.target_identity, "start.repository_context.target_identity"),
|
|
1051
|
+
...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "start.repository_context.event_id") }),
|
|
1052
|
+
...(source.outcome === undefined ? {} : { outcome: enumeration(source.outcome, REPOSITORY_CONTEXT_OUTCOMES, "start.repository_context.outcome") }),
|
|
867
1053
|
};
|
|
868
1054
|
}
|
|
869
1055
|
|
|
@@ -1042,8 +1228,146 @@ function decodeTransitionArguments(value: unknown, label: string): readonly Revi
|
|
|
1042
1228
|
});
|
|
1043
1229
|
}
|
|
1044
1230
|
|
|
1045
|
-
function
|
|
1046
|
-
|
|
1231
|
+
function decodeCaptureSubmission(value: unknown, label: string, v5: boolean): ReviewCaptureSubmissionV1 {
|
|
1232
|
+
// status/v5: the live 2.4.0-main binary emits the materialize
|
|
1233
|
+
// capture-result submission as a SINGULAR `value` object carrying a
|
|
1234
|
+
// `schema` key (captured 2026-08-16 from 2.4.0-main.b1afef46; the
|
|
1235
|
+
// emitter has been singular since gentle-ai f1a95179). The form is
|
|
1236
|
+
// closed to that exact captured shape and normalizes into the typed
|
|
1237
|
+
// one-entry values array the host relay already consumes. A payload
|
|
1238
|
+
// carrying both wire forms at once matches no captured shape and falls
|
|
1239
|
+
// through to the legacy decoder, which rejects the unknown `value` key.
|
|
1240
|
+
if (v5 && typeof value === "object" && value !== null && "value" in value && !("values" in value)) {
|
|
1241
|
+
const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
|
|
1242
|
+
if (submission.operation_token !== "capture-result") throw new TypeError(`${label}.operation_token must be capture-result for the singular value form`);
|
|
1243
|
+
const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
|
|
1244
|
+
const row = exactRecord(submission.value, `${label}.value`, ["slot", "domain", "schema", "substitution_location"]);
|
|
1245
|
+
return {
|
|
1246
|
+
operationToken: "capture-result",
|
|
1247
|
+
argumentTokens,
|
|
1248
|
+
values: [{
|
|
1249
|
+
slot: enumeration(row.slot, ["reviewer_result"] as const, `${label}.value.slot`),
|
|
1250
|
+
domain: nonempty(row.domain, `${label}.value.domain`),
|
|
1251
|
+
schema: nonempty(row.schema, `${label}.value.schema`),
|
|
1252
|
+
substitutionLocation: integer(row.substitution_location, `${label}.value.substitution_location`, 0, argumentTokens.length - 1),
|
|
1253
|
+
}],
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "values"]);
|
|
1257
|
+
const operationToken = text(submission.operation_token, `${label}.operation_token`, { minimum: 1, pattern: /^[a-z0-9-]+$/ });
|
|
1258
|
+
const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
|
|
1259
|
+
const values = array(submission.values, `${label}.values`, (entry, entryLabel) => {
|
|
1260
|
+
const row = exactRecord(entry, entryLabel, ["slot", "domain", "substitution_location"]);
|
|
1261
|
+
return {
|
|
1262
|
+
slot: nonempty(row.slot, `${entryLabel}.slot`),
|
|
1263
|
+
domain: nonempty(row.domain, `${entryLabel}.domain`),
|
|
1264
|
+
substitutionLocation: integer(row.substitution_location, `${entryLabel}.substitution_location`, 0, argumentTokens.length - 1),
|
|
1265
|
+
};
|
|
1266
|
+
}, { minimum: 1 });
|
|
1267
|
+
return { operationToken, argumentTokens, values };
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// v5-only structural decoders, exact to the vendored status-v5.schema.json
|
|
1271
|
+
// and correction-plan-request.schema.json on gentle-ai main.
|
|
1272
|
+
|
|
1273
|
+
function decodeProviderTask(value: unknown, label: string): ReviewProviderTaskV1 {
|
|
1274
|
+
const task = exactRecord(value, label, ["agent", "role", "prompt"]);
|
|
1275
|
+
return {
|
|
1276
|
+
agent: enumeration(task.agent, ["review-refuter", "review-validator"] as const, `${label}.agent`),
|
|
1277
|
+
role: enumeration(task.role, ["refuter", "targeted-validator"] as const, `${label}.role`),
|
|
1278
|
+
prompt: nonempty(task.prompt, `${label}.prompt`),
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function decodeSubmissionDescriptorV5(value: unknown, label: string, operationToken: "finalize" | "capture-evidence"): ReviewSubmissionDescriptorV5 {
|
|
1283
|
+
if (operationToken === "finalize") {
|
|
1284
|
+
const descriptor = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
|
|
1285
|
+
const argumentTokens = stringArray(descriptor.argument_tokens, `${label}.argument_tokens`, { minimum: 7 });
|
|
1286
|
+
const source = exactRecord(descriptor.value, `${label}.value`, ["slot", "domain", "substitution_location"], ["schema", "minimum", "maximum"]);
|
|
1287
|
+
const slot = enumeration(source.slot, ["correction_lines", "validation"] as const, `${label}.value.slot`);
|
|
1288
|
+
return {
|
|
1289
|
+
operationToken,
|
|
1290
|
+
argumentTokens,
|
|
1291
|
+
value: {
|
|
1292
|
+
slot,
|
|
1293
|
+
domain: nonempty(source.domain, `${label}.value.domain`),
|
|
1294
|
+
substitutionLocation: integer(source.substitution_location, `${label}.value.substitution_location`, 0, argumentTokens.length - 1),
|
|
1295
|
+
...(source.schema === undefined ? {} : { schema: nonempty(source.schema, `${label}.value.schema`) }),
|
|
1296
|
+
...(source.minimum === undefined ? {} : { minimum: integer(source.minimum, `${label}.value.minimum`, 1, 200) }),
|
|
1297
|
+
...(source.maximum === undefined ? {} : { maximum: integer(source.maximum, `${label}.value.maximum`, 1, 200) }),
|
|
1298
|
+
},
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
const descriptor = exactRecord(value, label, ["operation_token", "argument_tokens", "values"]);
|
|
1302
|
+
const argumentTokens = stringArray(descriptor.argument_tokens, `${label}.argument_tokens`, { minimum: 6, maximum: 6 });
|
|
1303
|
+
const values = array(descriptor.values, `${label}.values`, (entry, entryLabel) => {
|
|
1304
|
+
const source = exactRecord(entry, entryLabel, ["slot", "domain", "substitution_location"], ["schema", "allowed_values"]);
|
|
1305
|
+
return {
|
|
1306
|
+
slot: nonempty(source.slot, `${entryLabel}.slot`),
|
|
1307
|
+
domain: nonempty(source.domain, `${entryLabel}.domain`),
|
|
1308
|
+
substitutionLocation: integer(source.substitution_location, `${entryLabel}.substitution_location`, 0, argumentTokens.length - 1),
|
|
1309
|
+
...(source.schema === undefined ? {} : { schema: nonempty(source.schema, `${entryLabel}.schema`) }),
|
|
1310
|
+
...(source.allowed_values === undefined ? {} : { allowedValues: stringArray(source.allowed_values, `${entryLabel}.allowed_values`, { minimum: 1, unique: true }) }),
|
|
1311
|
+
};
|
|
1312
|
+
}, { minimum: 2, maximum: 2 });
|
|
1313
|
+
return { operationToken, argumentTokens, values };
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function decodeCorrectionPlanRequestV1(value: unknown, label: string): ReviewCorrectionPlanRequestV1 {
|
|
1317
|
+
const body = exactRecord(value, label, ["schema", "request_hash", "lineage_id", "expected_revision", "target_identity", "correction_budget", "fix_finding_ids", "findings"]);
|
|
1318
|
+
if (body.schema !== "gentle-ai.review-correction-plan-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-correction-plan-request/v1`);
|
|
1319
|
+
return {
|
|
1320
|
+
schema: "gentle-ai.review-correction-plan-request/v1",
|
|
1321
|
+
requestHash: sha256(body.request_hash, `${label}.request_hash`),
|
|
1322
|
+
lineageId: lineage(body.lineage_id, `${label}.lineage_id`),
|
|
1323
|
+
expectedRevision: sha256(body.expected_revision, `${label}.expected_revision`),
|
|
1324
|
+
targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
|
|
1325
|
+
correctionBudget: integer(body.correction_budget, `${label}.correction_budget`, 1, 200),
|
|
1326
|
+
fixFindingIds: stringArray(body.fix_finding_ids, `${label}.fix_finding_ids`, { minimum: 1, unique: true }),
|
|
1327
|
+
findings: array(body.findings, `${label}.findings`, (entry, entryLabel) => {
|
|
1328
|
+
const finding = exactRecord(entry, entryLabel, ["id", "lens", "location", "severity", "claim", "proof_refs", "evidence", "evidence_class", "causal_disposition"]);
|
|
1329
|
+
return {
|
|
1330
|
+
id: nonempty(finding.id, `${entryLabel}.id`),
|
|
1331
|
+
lens: enumeration(finding.lens, ["risk", "resilience", "readability", "reliability"] as const, `${entryLabel}.lens`),
|
|
1332
|
+
location: nonempty(finding.location, `${entryLabel}.location`),
|
|
1333
|
+
severity: enumeration(finding.severity, ["BLOCKER", "CRITICAL"] as const, `${entryLabel}.severity`),
|
|
1334
|
+
claim: nonempty(finding.claim, `${entryLabel}.claim`),
|
|
1335
|
+
proofRefs: stringArray(finding.proof_refs, `${entryLabel}.proof_refs`, { minimum: 1 }),
|
|
1336
|
+
evidence: nonempty(finding.evidence, `${entryLabel}.evidence`),
|
|
1337
|
+
evidenceClass: enumeration(finding.evidence_class, ["deterministic", "inferential"] as const, `${entryLabel}.evidence_class`),
|
|
1338
|
+
causalDisposition: enumeration(finding.causal_disposition, ["introduced", "behavior-activated", "worsened"] as const, `${entryLabel}.causal_disposition`),
|
|
1339
|
+
};
|
|
1340
|
+
}, { minimum: 1 }),
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
export function decodeReviewForecastV1(value: unknown, label = "status.forecast"): ReviewForecastV1 {
|
|
1345
|
+
const body = exactRecord(value, label, ["horizon", "steps"]);
|
|
1346
|
+
const horizon = enumeration(body.horizon, ["partial", "terminal"] as const, `${label}.horizon`);
|
|
1347
|
+
const steps = array(body.steps, `${label}.steps`, (entry, entryLabel) => {
|
|
1348
|
+
const step = exactRecord(entry, entryLabel, ["step", "kind", "reason_code", "description"]);
|
|
1349
|
+
if (step.step !== 1) throw new TypeError(`${entryLabel}.step must be 1`);
|
|
1350
|
+
return {
|
|
1351
|
+
step: 1 as const,
|
|
1352
|
+
kind: enumeration(step.kind, ["execute", "collect", "stop"] as const, `${entryLabel}.kind`),
|
|
1353
|
+
reasonCode: text(step.reason_code, `${entryLabel}.reason_code`, { minimum: 1, pattern: /^[a-z0-9_]+$/ }),
|
|
1354
|
+
description: nonempty(step.description, `${entryLabel}.description`),
|
|
1355
|
+
};
|
|
1356
|
+
}, { minimum: 1, maximum: 1 });
|
|
1357
|
+
// The published horizon-to-step invariant: a stop head is terminal, any
|
|
1358
|
+
// other head is partial.
|
|
1359
|
+
if ((horizon === "terminal") !== (steps[0]!.kind === "stop")) throw new TypeError(`${label}.horizon does not match its step kind`);
|
|
1360
|
+
return { horizon, steps };
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// The two v5 reason codes whose transitions must carry the correction plan
|
|
1364
|
+
// request; every other reason code must not.
|
|
1365
|
+
const CORRECTION_REQUEST_REASON_CODES = Object.freeze(["correction_plan_required", "corrected_candidate_unavailable"] as const);
|
|
1366
|
+
// v5 capture operations that must carry a submission descriptor.
|
|
1367
|
+
const V5_SUBMISSION_CAPTURE_OPERATIONS = Object.freeze(["external.plan_correction", "external.run_targeted_validation", "review.capture-evidence"] as const);
|
|
1368
|
+
|
|
1369
|
+
function decodeCollectInput(value: unknown, label: string, v5: boolean): ReviewCollectInputV3 {
|
|
1370
|
+
const input = exactRecord(value, label, ["name", "schema", "capture_operation", "arguments"], ["artifact_subject", "base_tree", "candidate_tree", "changed_path_manifest", "validation_request", "submission", ...(v5 ? ["provider_task"] : [])]);
|
|
1047
1371
|
const name = text(input.name, `${label}.name`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
|
|
1048
1372
|
const schema = nonempty(input.schema, `${label}.schema`);
|
|
1049
1373
|
const captureOperation = nonempty(input.capture_operation, `${label}.capture_operation`);
|
|
@@ -1052,8 +1376,43 @@ function decodeCollectInput(value: unknown, label: string): ReviewCollectInputV3
|
|
|
1052
1376
|
if (captureOperation === "external.run_targeted_validation") {
|
|
1053
1377
|
if (input.validation_request === undefined) throw new TypeError(`${label}.validation_request is required`);
|
|
1054
1378
|
if (schema !== "gentle-ai.review-targeted-validation-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-targeted-validation-request/v1`);
|
|
1379
|
+
} else if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION) {
|
|
1380
|
+
if (input.validation_request === undefined) throw new TypeError(`${label}.validation_request is required`);
|
|
1055
1381
|
} else if (input.validation_request !== undefined) {
|
|
1056
|
-
throw new TypeError(`${label}.validation_request is only valid for external.run_targeted_validation`);
|
|
1382
|
+
throw new TypeError(`${label}.validation_request is only valid for external.run_targeted_validation or review.capture-validation`);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// v5-only surfaces (vendored status-v5.schema.json): the provider role task
|
|
1386
|
+
// rides exactly the external.run_provider_role vector, and the finalize/
|
|
1387
|
+
// capture-evidence submission descriptor forms ride their own vectors.
|
|
1388
|
+
if (v5) {
|
|
1389
|
+
if (captureOperation === "external.run_provider_role") {
|
|
1390
|
+
if (input.provider_task === undefined) throw new TypeError(`${label}.provider_task is required for external.run_provider_role`);
|
|
1391
|
+
} else if (input.provider_task !== undefined) {
|
|
1392
|
+
throw new TypeError(`${label}.provider_task is only valid for external.run_provider_role`);
|
|
1393
|
+
}
|
|
1394
|
+
if ((V5_SUBMISSION_CAPTURE_OPERATIONS as readonly string[]).includes(captureOperation)) {
|
|
1395
|
+
if (input.submission === undefined) throw new TypeError(`${label}.submission is required for ${captureOperation}`);
|
|
1396
|
+
if (captureOperation === "external.plan_correction" && schema !== "gentle-ai.review-correction-plan/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-correction-plan/v1`);
|
|
1397
|
+
if (captureOperation === "review.capture-evidence" && schema !== "https://gentle-ai.dev/schema/review/verification-evidence/v1") throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/verification-evidence/v1`);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// gentle-pi#311 P4-roles: the two Go-owned non-lens provider role capture
|
|
1402
|
+
// operations render SELF-CONTAINED authority-advancing vectors. Executing
|
|
1403
|
+
// the exact rendered tokens makes Go materialize the role prompt, run its
|
|
1404
|
+
// own locked-down pi subprocess, and admit the raw verdict — so a
|
|
1405
|
+
// submission descriptor (the host-mediated completing form) on one of
|
|
1406
|
+
// these inputs would hand the caller a way to author the verdict and is
|
|
1407
|
+
// rejected as a provider contract violation.
|
|
1408
|
+
if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_REFUTER && schema !== "https://gentle-ai.dev/schema/review/refuter/v1") {
|
|
1409
|
+
throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/refuter/v1`);
|
|
1410
|
+
}
|
|
1411
|
+
if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION && schema !== "https://gentle-ai.dev/schema/review/validator/v1") {
|
|
1412
|
+
throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/validator/v1`);
|
|
1413
|
+
}
|
|
1414
|
+
if (input.submission !== undefined && (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS as readonly string[]).includes(captureOperation)) {
|
|
1415
|
+
throw new TypeError(`${label}.submission is not allowed on the self-contained ${captureOperation} vector`);
|
|
1057
1416
|
}
|
|
1058
1417
|
|
|
1059
1418
|
if (captureOperation === "review.capture-result") {
|
|
@@ -1064,6 +1423,23 @@ function decodeCollectInput(value: unknown, label: string): ReviewCollectInputV3
|
|
|
1064
1423
|
} else if (input.artifact_subject !== undefined || input.base_tree !== undefined || input.candidate_tree !== undefined || input.changed_path_manifest !== undefined) {
|
|
1065
1424
|
throw new TypeError(`${label} carries capture-result fields without review.capture-result`);
|
|
1066
1425
|
}
|
|
1426
|
+
const submissionOperations: readonly string[] = v5 ? ["review.capture-result", ...V5_SUBMISSION_CAPTURE_OPERATIONS] : ["review.capture-result"];
|
|
1427
|
+
if (input.submission !== undefined && !submissionOperations.includes(captureOperation)) {
|
|
1428
|
+
throw new TypeError(v5 ? `${label}.submission is only valid for ${submissionOperations.join(", ")}` : `${label}.submission is only valid for review.capture-result`);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// The v5 descriptor forms are discriminated by their closed operation
|
|
1432
|
+
// tokens; every other submission stays the host-relay completing form.
|
|
1433
|
+
let submission: ReviewCaptureSubmissionV1 | undefined;
|
|
1434
|
+
let submissionDescriptor: ReviewSubmissionDescriptorV5 | undefined;
|
|
1435
|
+
if (input.submission !== undefined) {
|
|
1436
|
+
const operationToken = (input.submission as Record<string, unknown> | null)?.operation_token;
|
|
1437
|
+
if (v5 && (operationToken === "finalize" || operationToken === "capture-evidence")) {
|
|
1438
|
+
submissionDescriptor = decodeSubmissionDescriptorV5(input.submission, `${label}.submission`, operationToken);
|
|
1439
|
+
} else {
|
|
1440
|
+
submission = decodeCaptureSubmission(input.submission, `${label}.submission`, v5);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1067
1443
|
|
|
1068
1444
|
return {
|
|
1069
1445
|
name,
|
|
@@ -1075,14 +1451,28 @@ function decodeCollectInput(value: unknown, label: string): ReviewCollectInputV3
|
|
|
1075
1451
|
...(input.candidate_tree === undefined ? {} : { candidateTree: gitTree(input.candidate_tree, `${label}.candidate_tree`) }),
|
|
1076
1452
|
...(input.changed_path_manifest === undefined ? {} : { changedPathManifest: array(input.changed_path_manifest, `${label}.changed_path_manifest`, decodeChangedPathEntry, { unique: true }) }),
|
|
1077
1453
|
...(input.validation_request === undefined ? {} : { validationRequest: decodeTargetedValidationRequestV1(input.validation_request, `${label}.validation_request`) }),
|
|
1454
|
+
...(submission === undefined ? {} : { submission }),
|
|
1455
|
+
...(submissionDescriptor === undefined ? {} : { submissionDescriptor }),
|
|
1456
|
+
...(v5 && input.provider_task !== undefined ? { providerTask: decodeProviderTask(input.provider_task, `${label}.provider_task`) } : {}),
|
|
1078
1457
|
};
|
|
1079
1458
|
}
|
|
1080
1459
|
|
|
1081
|
-
export function decodeReviewNextTransitionV3(value: unknown): ReviewNextTransitionV3 {
|
|
1082
|
-
const
|
|
1460
|
+
export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boolean } = {}): ReviewNextTransitionV3 {
|
|
1461
|
+
const v5 = options.v5 === true;
|
|
1462
|
+
const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : [])]);
|
|
1083
1463
|
const kind = enumeration(transition.kind, ["execute", "collect", "stop"] as const, "next_transition.kind");
|
|
1084
1464
|
const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
|
|
1085
1465
|
|
|
1466
|
+
// status/v5: the bounded correction plan request rides exactly its two
|
|
1467
|
+
// reason codes and never any other (vendored status-v5.schema.json).
|
|
1468
|
+
let correctionRequest: ReviewCorrectionPlanRequestV1 | undefined;
|
|
1469
|
+
if (v5) {
|
|
1470
|
+
const required = (CORRECTION_REQUEST_REASON_CODES as readonly string[]).includes(reasonCode);
|
|
1471
|
+
if (required && transition.correction_request === undefined) throw new TypeError(`next_transition.correction_request is required for ${reasonCode}`);
|
|
1472
|
+
if (!required && transition.correction_request !== undefined) throw new TypeError(`next_transition.correction_request is only valid for ${CORRECTION_REQUEST_REASON_CODES.join(", ")}`);
|
|
1473
|
+
if (transition.correction_request !== undefined) correctionRequest = decodeCorrectionPlanRequestV1(transition.correction_request, "next_transition.correction_request");
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1086
1476
|
if (kind === "execute") {
|
|
1087
1477
|
// `command` is an optional, ready-to-paste rendering of `arguments` (the
|
|
1088
1478
|
// exact same binding as a single shell-ready string) — observed live
|
|
@@ -1109,16 +1499,16 @@ export function decodeReviewNextTransitionV3(value: unknown): ReviewNextTransiti
|
|
|
1109
1499
|
const revision = binding.revision === undefined ? undefined : sha256(binding.revision, "next_transition.execute.binding.revision");
|
|
1110
1500
|
const command = execute.command === undefined ? undefined : nonempty(execute.command, "next_transition.execute.command");
|
|
1111
1501
|
if (transition.collect !== undefined) throw new TypeError("next_transition.collect is incompatible with execute");
|
|
1112
|
-
return { kind, reasonCode, execute: { operation, arguments: argumentsList, preconditions, binding: { targetIdentity, ...(lineageId === undefined ? {} : { lineageId }), ...(revision === undefined ? {} : { revision }) }, ...(command === undefined ? {} : { command }) } };
|
|
1502
|
+
return { kind, reasonCode, execute: { operation, arguments: argumentsList, preconditions, binding: { targetIdentity, ...(lineageId === undefined ? {} : { lineageId }), ...(revision === undefined ? {} : { revision }) }, ...(command === undefined ? {} : { command }) }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1113
1503
|
}
|
|
1114
1504
|
if (kind === "collect") {
|
|
1115
1505
|
const collect = exactRecord(transition.collect, "next_transition.collect", ["inputs"]);
|
|
1116
|
-
const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label), { minimum: 1 });
|
|
1506
|
+
const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5), { minimum: 1 });
|
|
1117
1507
|
if (transition.execute !== undefined) throw new TypeError("next_transition.execute is incompatible with collect");
|
|
1118
|
-
return { kind, reasonCode, collect: { inputs } };
|
|
1508
|
+
return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1119
1509
|
}
|
|
1120
1510
|
if (transition.execute !== undefined || transition.collect !== undefined) throw new TypeError("next_transition stop cannot carry a transition");
|
|
1121
|
-
return { kind, reasonCode };
|
|
1511
|
+
return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1122
1512
|
}
|
|
1123
1513
|
|
|
1124
1514
|
// ---------------------------------------------------------------------------
|
|
@@ -1178,10 +1568,15 @@ function decodeFinalVerificationRetry(value: unknown, label: string): ReviewFina
|
|
|
1178
1568
|
// ---------------------------------------------------------------------------
|
|
1179
1569
|
|
|
1180
1570
|
export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
1571
|
+
// Additive forward acceptance: status/v5 (gentle-ai main; ground-truthed
|
|
1572
|
+
// against a live capture and the vendored status-v5.schema.json) is the v3
|
|
1573
|
+
// key set plus the optional forecast and the v5-only next_transition
|
|
1574
|
+
// surfaces. The v3 identity keeps rejecting every v5-only field.
|
|
1575
|
+
const v5 = typeof value === "object" && value !== null && (value as Record<string, unknown>).schema === "gentle-ai.review-integration.status/v5";
|
|
1181
1576
|
const body = exactRecord(value, "status", [
|
|
1182
1577
|
"schema", "contract", "operation", "applicability", "receipt", "action", "replayability", "target_identity", "projection", "repair", "candidates",
|
|
1183
|
-
], ["authority", "frozen", "reconciliation", "action_disposition", "eligibility", "next_transition", "authority_target_identity", "validation_request", "final_verification_retry"]);
|
|
1184
|
-
requireIdentity(body, "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
|
|
1578
|
+
], ["authority", "frozen", "reconciliation", "action_disposition", "eligibility", "next_transition", "authority_target_identity", "validation_request", "final_verification_retry", ...(v5 ? ["forecast", "repository_context"] : [])]);
|
|
1579
|
+
requireIdentity(body, v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
|
|
1185
1580
|
|
|
1186
1581
|
const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] as const, "status.applicability");
|
|
1187
1582
|
const receiptBody = exactRecord(body.receipt, "status.receipt", ["status"], ["identity"]);
|
|
@@ -1222,7 +1617,11 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1222
1617
|
if ((action === "recover" || action === "retry_final_verification") && actionDisposition === undefined) throw new TypeError(`${action} status requires action_disposition`);
|
|
1223
1618
|
if (action !== "recover" && action !== "retry_final_verification" && actionDisposition !== undefined) throw new TypeError("status.action_disposition is only valid for the recover or retry_final_verification action");
|
|
1224
1619
|
if (body.eligibility !== undefined) decodeEligibility(body.eligibility, "status.eligibility");
|
|
1225
|
-
const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition);
|
|
1620
|
+
const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5 });
|
|
1621
|
+
// status/v5 dependentRequired: a forecast previews the transition head, so
|
|
1622
|
+
// it can only accompany an actual next_transition.
|
|
1623
|
+
const forecast = v5 && body.forecast !== undefined ? decodeReviewForecastV1(body.forecast) : undefined;
|
|
1624
|
+
if (forecast !== undefined && nextTransition === undefined) throw new TypeError("status.forecast requires status.next_transition");
|
|
1226
1625
|
const replayability = enumeration(body.replayability, Object.values(REVIEW_REPLAYABILITY), "status.replayability");
|
|
1227
1626
|
|
|
1228
1627
|
let reconciliation: ReviewStatusReconciliationV1 | undefined;
|
|
@@ -1257,6 +1656,24 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1257
1656
|
throw new TypeError("status.final_verification_retry is only valid for the retry_final_verification action");
|
|
1258
1657
|
}
|
|
1259
1658
|
|
|
1659
|
+
// status/v5 additive top-level repository context reference (captured
|
|
1660
|
+
// 2026-08-16 from 2.4.0-main.b1afef46; missing from the published
|
|
1661
|
+
// status-v5.schema.json, so the live capture is authoritative). Same
|
|
1662
|
+
// reference shape as start/v3's repository_context.
|
|
1663
|
+
let repositoryContext: ReviewRepositoryContextV2 | undefined;
|
|
1664
|
+
if (body.repository_context !== undefined) {
|
|
1665
|
+
const source = exactRecord(body.repository_context, "status.repository_context", ["capability", "handle", "revision", "target_identity"], ["event_id", "outcome"]);
|
|
1666
|
+
if (source.capability !== "review.opaque_repository_context") throw new TypeError("status.repository_context.capability is unsupported");
|
|
1667
|
+
repositoryContext = {
|
|
1668
|
+
capability: "review.opaque_repository_context",
|
|
1669
|
+
handle: text(source.handle, "status.repository_context.handle", { pattern: /^rctx1_[0-9a-f]{64}$/ }),
|
|
1670
|
+
revision: sha256(source.revision, "status.repository_context.revision"),
|
|
1671
|
+
targetIdentity: sha256(source.target_identity, "status.repository_context.target_identity"),
|
|
1672
|
+
...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "status.repository_context.event_id") }),
|
|
1673
|
+
...(source.outcome === undefined ? {} : { outcome: enumeration(source.outcome, REPOSITORY_CONTEXT_OUTCOMES, "status.repository_context.outcome") }),
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1260
1677
|
return {
|
|
1261
1678
|
contract: REVIEW_INTEGRATION_CONTRACT,
|
|
1262
1679
|
applicability,
|
|
@@ -1275,6 +1692,8 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
|
|
|
1275
1692
|
...(nextTransition === undefined ? {} : { nextTransition }),
|
|
1276
1693
|
...(validationRequest === undefined ? {} : { validationRequest }),
|
|
1277
1694
|
...(finalVerificationRetry === undefined ? {} : { finalVerificationRetry }),
|
|
1695
|
+
...(forecast === undefined ? {} : { forecast }),
|
|
1696
|
+
...(repositoryContext === undefined ? {} : { repositoryContext }),
|
|
1278
1697
|
raw: body,
|
|
1279
1698
|
};
|
|
1280
1699
|
}
|
|
@@ -1295,11 +1714,10 @@ function decodeConsentChoice(value: unknown, label: string, answer: "granted" |
|
|
|
1295
1714
|
};
|
|
1296
1715
|
}
|
|
1297
1716
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
requireIdentity(body, "gentle-ai.review-integration.consent/v2", "review.start");
|
|
1717
|
+
// The semantic surface shared verbatim by both accepted consent identities.
|
|
1718
|
+
// Every label and guard predates consent/v3, so the v2 decode stays
|
|
1719
|
+
// byte-identical (test-locked) while v3 adds only its own identity gate.
|
|
1720
|
+
function decodeConsentSemantics(body: Record<string, unknown>): Omit<ReviewConsentV2, "schema" | "raw"> {
|
|
1303
1721
|
if (body.action !== "consent_required") throw new TypeError("consent.action must be consent_required");
|
|
1304
1722
|
if (body.blocking !== true) throw new TypeError("consent.blocking must be true");
|
|
1305
1723
|
|
|
@@ -1316,7 +1734,6 @@ export function decodeReviewConsentV2(value: unknown): ReviewConsentV2 {
|
|
|
1316
1734
|
if (offPathSource.command !== "gentle-ai review mode disable") throw new TypeError("consent.off_path.command is unsupported");
|
|
1317
1735
|
|
|
1318
1736
|
return {
|
|
1319
|
-
schema: "gentle-ai.review-integration.consent/v2",
|
|
1320
1737
|
contract: REVIEW_INTEGRATION_CONTRACT,
|
|
1321
1738
|
operation: "review.start",
|
|
1322
1739
|
action: "consent_required",
|
|
@@ -1332,6 +1749,39 @@ export function decodeReviewConsentV2(value: unknown): ReviewConsentV2 {
|
|
|
1332
1749
|
riskEvidence: stringArray(body.risk_evidence, "consent.risk_evidence"),
|
|
1333
1750
|
choices: [granted, declined],
|
|
1334
1751
|
offPath: { note: nonempty(offPathSource.note, "consent.off_path.note"), command: "gentle-ai review mode disable" },
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
const CONSENT_KEYS_V2 = Object.freeze([
|
|
1756
|
+
"schema", "contract", "operation", "action", "blocking", "target_identity", "projection", "risk_level", "changed_files", "changed_lines", "headline", "reason", "value", "risk_evidence", "choices", "off_path",
|
|
1757
|
+
] as const);
|
|
1758
|
+
|
|
1759
|
+
export function decodeReviewConsentV2(value: unknown): ReviewConsentV2 {
|
|
1760
|
+
const body = exactRecord(value, "consent", CONSENT_KEYS_V2);
|
|
1761
|
+
requireIdentity(body, "gentle-ai.review-integration.consent/v2", "review.start");
|
|
1762
|
+
return {
|
|
1763
|
+
schema: "gentle-ai.review-integration.consent/v2",
|
|
1764
|
+
...decodeConsentSemantics(body),
|
|
1765
|
+
raw: body,
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// consent/v3 (gentle-ai >= 2.3.0): the v2 surface plus the required, fixed
|
|
1770
|
+
// `agent` runtime binding. Ground truth is the captured envelope from a
|
|
1771
|
+
// 2.4.0-main binary (tests/fixtures/devbinary/consent-v3.captured.json) plus
|
|
1772
|
+
// gentle-ai main contracts/review-integration/v2/schemas/consent-v3.schema.json.
|
|
1773
|
+
// The choice-invocation shape deliberately stays the shared v2 pattern: the
|
|
1774
|
+
// published v3 schema demands an `--agent claude-code` token that the live
|
|
1775
|
+
// emitter omits when the caller declared no --agent, so the capture is
|
|
1776
|
+
// authoritative and Pi replays whichever provider-owned invocation arrived.
|
|
1777
|
+
export function decodeReviewConsentV3(value: unknown): ReviewConsentV3 {
|
|
1778
|
+
const body = exactRecord(value, "consent", [...CONSENT_KEYS_V2, "agent"]);
|
|
1779
|
+
requireIdentity(body, "gentle-ai.review-integration.consent/v3", "review.start");
|
|
1780
|
+
if (body.agent !== "claude-code") throw new TypeError("consent.agent must be claude-code");
|
|
1781
|
+
return {
|
|
1782
|
+
schema: "gentle-ai.review-integration.consent/v3",
|
|
1783
|
+
agent: "claude-code",
|
|
1784
|
+
...decodeConsentSemantics(body),
|
|
1335
1785
|
raw: body,
|
|
1336
1786
|
};
|
|
1337
1787
|
}
|
|
@@ -1395,7 +1845,7 @@ export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
|
|
|
1395
1845
|
"schema", "contract", "operation", "phase", "code", "message", "mutation_outcome", "authority_applicability", "retry_safe", "replayability", "required_inputs", "next_action",
|
|
1396
1846
|
], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context"]);
|
|
1397
1847
|
requireIdentity(body, "gentle-ai.review-integration.failure/v2");
|
|
1398
|
-
const operation = enumeration(body.operation,
|
|
1848
|
+
const operation = enumeration(body.operation, FAILURE_OPERATIONS, "failure.operation");
|
|
1399
1849
|
|
|
1400
1850
|
if (body.progress_identity !== undefined) {
|
|
1401
1851
|
if (body.lineage_id === undefined || body.request_digest === undefined) throw new TypeError("failure.progress_identity requires lineage_id and request_digest");
|
|
@@ -1565,3 +2015,54 @@ export function decodeReviewRepairV2(value: unknown): ReviewRepairV2 {
|
|
|
1565
2015
|
raw: body,
|
|
1566
2016
|
};
|
|
1567
2017
|
}
|
|
2018
|
+
|
|
2019
|
+
// ---------------------------------------------------------------------------
|
|
2020
|
+
// result-artifact/v2 — the `review capture-result` admission answer
|
|
2021
|
+
// ---------------------------------------------------------------------------
|
|
2022
|
+
|
|
2023
|
+
// The provider's admitted-reviewer-result envelope, printed by `review
|
|
2024
|
+
// capture-result` when a reviewer result is admitted and re-discovered by
|
|
2025
|
+
// STATUS artifact discovery. Unlike the negotiated envelopes above it carries
|
|
2026
|
+
// no `contract`/`operation` identity pair — the schema constant plus the
|
|
2027
|
+
// capability constant are its complete identity (vendored ground truth:
|
|
2028
|
+
// contracts/review-integration/v1/schemas/result-artifact-v2.schema.json,
|
|
2029
|
+
// confirmed against a live 2.4.0-main capture; the v2.2.3 pinned emitter
|
|
2030
|
+
// shares the exact same struct). Exactly one locator is present: a
|
|
2031
|
+
// provider-owned store `path`, or the opaque `rart1_` `reference` minted for
|
|
2032
|
+
// repository-context captures.
|
|
2033
|
+
export interface ReviewResultArtifactV2 {
|
|
2034
|
+
schema: "gentle-ai.review-result-artifact/v2";
|
|
2035
|
+
capability: "review.native_result_artifact";
|
|
2036
|
+
sha256: string;
|
|
2037
|
+
lineageId: string;
|
|
2038
|
+
targetIdentity: string;
|
|
2039
|
+
lens: (typeof REVIEW_LENSES)[number];
|
|
2040
|
+
selectedOrder: number;
|
|
2041
|
+
subjectHash: string;
|
|
2042
|
+
admissionDecision: "completed";
|
|
2043
|
+
path?: string;
|
|
2044
|
+
reference?: string;
|
|
2045
|
+
raw: Record<string, unknown>;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
export function decodeReviewResultArtifactV2(value: unknown): ReviewResultArtifactV2 {
|
|
2049
|
+
const body = exactRecord(value, "result_artifact", ["schema", "capability", "sha256", "lineage_id", "target_identity", "lens", "selected_order", "subject_hash", "admission_decision"], ["path", "reference"]);
|
|
2050
|
+
if (body.schema !== "gentle-ai.review-result-artifact/v2") throw new TypeError("result_artifact.schema must be gentle-ai.review-result-artifact/v2");
|
|
2051
|
+
if (body.capability !== "review.native_result_artifact") throw new TypeError("result_artifact.capability must be review.native_result_artifact");
|
|
2052
|
+
if (body.admission_decision !== "completed") throw new TypeError("result_artifact.admission_decision must be completed");
|
|
2053
|
+
if ((body.path === undefined) === (body.reference === undefined)) throw new TypeError("result_artifact must carry exactly one of path or reference");
|
|
2054
|
+
return {
|
|
2055
|
+
schema: "gentle-ai.review-result-artifact/v2",
|
|
2056
|
+
capability: "review.native_result_artifact",
|
|
2057
|
+
sha256: sha256(body.sha256, "result_artifact.sha256"),
|
|
2058
|
+
lineageId: lineage(body.lineage_id, "result_artifact.lineage_id"),
|
|
2059
|
+
targetIdentity: sha256(body.target_identity, "result_artifact.target_identity"),
|
|
2060
|
+
lens: enumeration(body.lens, REVIEW_LENSES, "result_artifact.lens"),
|
|
2061
|
+
selectedOrder: integer(body.selected_order, "result_artifact.selected_order", 0, 3),
|
|
2062
|
+
subjectHash: sha256(body.subject_hash, "result_artifact.subject_hash"),
|
|
2063
|
+
admissionDecision: "completed",
|
|
2064
|
+
...(body.path === undefined ? {} : { path: nonempty(body.path, "result_artifact.path") }),
|
|
2065
|
+
...(body.reference === undefined ? {} : { reference: text(body.reference, "result_artifact.reference", { pattern: /^rart1_[0-9a-f]{64}$/ }) }),
|
|
2066
|
+
raw: body,
|
|
2067
|
+
};
|
|
2068
|
+
}
|