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
package/lib/native-review-cli.ts
CHANGED
|
@@ -5,18 +5,21 @@ import { chmod, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { dirname, isAbsolute, join, posix, win32 } from "node:path";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
-
import { GENTLE_AI_VERSION, PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
|
|
8
|
+
import { GENTLE_AI_VERSION, PackageLocalGentleAiBinaryMissingError, gentleAiDevBinaryOverrideConfigured, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
|
|
9
|
+
import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.ts";
|
|
9
10
|
import {
|
|
10
11
|
REVIEW_INTEGRATION_CONTRACT,
|
|
11
12
|
decodeReviewCapabilitiesV2,
|
|
12
13
|
decodeReviewConsentV2,
|
|
14
|
+
decodeReviewConsentV3,
|
|
13
15
|
decodeReviewFailureV2,
|
|
14
16
|
decodeReviewOperationV2,
|
|
15
17
|
decodeReviewRepairV2,
|
|
18
|
+
decodeReviewResultArtifactV2,
|
|
16
19
|
decodeReviewStartV3,
|
|
17
20
|
decodeReviewStatusV3,
|
|
18
21
|
type ReviewCapabilitiesV2,
|
|
19
|
-
type
|
|
22
|
+
type ReviewConsentEnvelope,
|
|
20
23
|
type ReviewFailureV2,
|
|
21
24
|
type ReviewRepairV2,
|
|
22
25
|
type ReviewStartState,
|
|
@@ -25,6 +28,23 @@ import {
|
|
|
25
28
|
|
|
26
29
|
const execFileAsync = promisify(execFile);
|
|
27
30
|
|
|
31
|
+
// Negotiated review/status responses can carry a complete authority inventory.
|
|
32
|
+
// Keep the production default large enough for that payload while retaining a
|
|
33
|
+
// hard 64 MiB ceiling even when GENTLE_PI_REVIEW_MAX_BUFFER_BYTES is set.
|
|
34
|
+
export const NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
35
|
+
const NATIVE_REVIEW_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
36
|
+
const NATIVE_REVIEW_MAX_BUFFER_BYTES_ENV = "GENTLE_PI_REVIEW_MAX_BUFFER_BYTES";
|
|
37
|
+
const NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT = "Inspect native review state before any new START; GENTLE_PI_REVIEW_MAX_BUFFER_BYTES accepts a positive decimal up to 67108864.";
|
|
38
|
+
|
|
39
|
+
function resolveNativeReviewMaxBufferBytes(environment: NodeJS.ProcessEnv = process.env): number {
|
|
40
|
+
const value = environment[NATIVE_REVIEW_MAX_BUFFER_BYTES_ENV];
|
|
41
|
+
if (value === undefined || !/^[1-9]\d*$/.test(value)) return NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES;
|
|
42
|
+
const parsed = Number(value);
|
|
43
|
+
return Number.isSafeInteger(parsed) && parsed <= NATIVE_REVIEW_MAX_BUFFER_BYTES
|
|
44
|
+
? parsed
|
|
45
|
+
: NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES;
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
export const NATIVE_REVIEW_OPERATION = {
|
|
29
49
|
VERSION: "version",
|
|
30
50
|
START: "review/start",
|
|
@@ -43,6 +63,7 @@ export const NATIVE_REVIEW_OPERATION = {
|
|
|
43
63
|
REPAIR: "review/repair",
|
|
44
64
|
CAPTURE_EVIDENCE: "review/capture-evidence",
|
|
45
65
|
CAPTURE_RESULT: "review/capture-result",
|
|
66
|
+
CAPTURE_PROVIDER_ROLE: "review/capture-provider-role",
|
|
46
67
|
} as const;
|
|
47
68
|
export type NativeReviewOperation = (typeof NATIVE_REVIEW_OPERATION)[keyof typeof NATIVE_REVIEW_OPERATION];
|
|
48
69
|
|
|
@@ -118,6 +139,22 @@ export interface NativeReviewCli {
|
|
|
118
139
|
// evidence-first correction lifecycle's collection step.
|
|
119
140
|
repair?(request: NativeReviewRepairRequest): Promise<ReviewRepairV2>;
|
|
120
141
|
captureEvidence?(request: NativeReviewCaptureEvidenceRequest): Promise<NativeReviewVerificationEvidenceV2>;
|
|
142
|
+
// Executes one provider-rendered capture-evidence submission exactly as
|
|
143
|
+
// rendered (verbatim tokens with only the {{outcome}}/{{input}} slot
|
|
144
|
+
// substitutions); the preferred form whenever the collect slot renders one.
|
|
145
|
+
captureEvidenceSubmission?(request: NativeReviewCaptureEvidenceSubmissionRequest): Promise<NativeReviewVerificationEvidenceV2>;
|
|
146
|
+
// gentle-pi#311 P4-roles: executes one provider-rendered self-contained
|
|
147
|
+
// role capture vector exactly as rendered (verbatim tokens, foreground).
|
|
148
|
+
captureProviderRole?(request: NativeReviewProviderRoleCaptureRequest): Promise<NativeReviewProviderRoleCaptureArtifact>;
|
|
149
|
+
// gentle-pi#311 P5: executes one provider-rendered `review.finalize`
|
|
150
|
+
// transition exactly as rendered (e.g. `--lineage=<id>
|
|
151
|
+
// --captured-results=true`); the host never assembles reviewer, refuter,
|
|
152
|
+
// or validator documents for it.
|
|
153
|
+
finalizeTransition?(request: NativeReviewFinalizeTransitionRequest): Promise<NativeFinalizeResult>;
|
|
154
|
+
// Executes one provider-rendered `finalize` submission descriptor exactly
|
|
155
|
+
// as rendered, substituting only its {{value}} slot (plan line-count
|
|
156
|
+
// literal, or a staged validation artifact path).
|
|
157
|
+
finalizeSubmission?(request: NativeReviewFinalizeSubmissionRequest): Promise<NativeFinalizeResult>;
|
|
121
158
|
// Dark until a negotiated version reports the `mode` capability true
|
|
122
159
|
// (Design Decision #7, organic-rdd-parity). Plain versioned CLI operation,
|
|
123
160
|
// outside the negotiated review-integration protocol — same shape as
|
|
@@ -175,18 +212,24 @@ export interface NativeReviewModeResult {
|
|
|
175
212
|
|
|
176
213
|
// Exact-match tolerated-stderr allowlist for START only, gated on the `mode`
|
|
177
214
|
// capability being true (Design Decision #6, organic-rdd-parity). Byte-exact
|
|
178
|
-
// against gentle-ai's headless
|
|
179
|
-
// reviewConsentSkippedNotice) written when the switch is on
|
|
180
|
-
// terminal answered the one-time consent question — which
|
|
181
|
-
// Pi spawns gentle-ai without a TTY. Any other text still
|
|
182
|
-
// UNEXPECTED_STDERR.
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
215
|
+
// against gentle-ai's headless notices (internal/cli/review_mode.go
|
|
216
|
+
// reviewConsentSkippedNotice and its siblings) written when the switch is on
|
|
217
|
+
// but no interactive terminal answered the one-time consent question — which
|
|
218
|
+
// is always true when Pi spawns gentle-ai without a TTY. Any other text still
|
|
219
|
+
// fails closed as UNEXPECTED_STDERR.
|
|
220
|
+
//
|
|
221
|
+
// Each entry is one whole line the PINNED gentle-ai may write to the console
|
|
222
|
+
// stream while still succeeding. That last qualifier is what keeps this list
|
|
223
|
+
// honest: v2.4.0 deleted reviewConsentSkippedDefaultProvenance ("Reviews are
|
|
224
|
+
// on by default; this was never explicitly chosen. ..."), which used to ride
|
|
225
|
+
// with the skip notice whenever the resolved mode source was `default`. Under
|
|
226
|
+
// opt-in receipt-driven development a default-source clone is refused long
|
|
227
|
+
// before the consent ceremony runs, so the pinned binary can no longer emit
|
|
228
|
+
// that line and it is removed here rather than left as dead tolerance. Multi-
|
|
229
|
+
// line stderr is still expected in principle — these are separate Fprintln
|
|
230
|
+
// calls, never one joined string — which is why membership is per line.
|
|
187
231
|
export const REVIEW_CONSENT_NOTICES = Object.freeze([
|
|
188
232
|
"Gentle AI reviewed this change without asking, because this session has no terminal to answer on. Run 'gentle-ai review mode disable' to turn reviews off, or 'gentle-ai review mode status' to see the current setting.",
|
|
189
|
-
"Reviews are on by default; this was never explicitly chosen. Run 'gentle-ai review mode enable' to make reviews an explicit choice, or 'gentle-ai review mode disable' to turn them off.",
|
|
190
233
|
"Gentle AI could not read an answer, so it reviewed this change and will ask again next time.",
|
|
191
234
|
"Gentle AI did not recognize that answer, so it reviewed this change and will ask again next time.",
|
|
192
235
|
"Review skipped for this candidate at your request. It will be offered again on the next change.",
|
|
@@ -201,6 +244,22 @@ function stderrIsTolerated(stderr: string, tolerated: readonly string[]): boolea
|
|
|
201
244
|
return lines.length > 0 && lines.every((line) => tolerated.includes(line));
|
|
202
245
|
}
|
|
203
246
|
|
|
247
|
+
// gentle-ai main narrates the negotiated STATUS forecast head to a human on
|
|
248
|
+
// stderr while keeping the machine envelope on stdout (reviewNarrateForecast
|
|
249
|
+
// in internal/cli/review_narration.go, ground-truthed live against a main-line
|
|
250
|
+
// dev build). The pinned release does not emit it, so this narration is
|
|
251
|
+
// tolerated only for negotiated STATUS while the dev-binary override is
|
|
252
|
+
// configured; any other stderr keeps failing closed.
|
|
253
|
+
const FORECAST_NARRATION_LINES = Object.freeze([
|
|
254
|
+
/^Forecast horizon: (?:partial|terminal)$/,
|
|
255
|
+
/^step [0-9]+: (?:execute|collect|stop); reason_code=[a-z0-9_]+; description=.+$/,
|
|
256
|
+
/^Re-query STATUS after completing this partial head\.$/,
|
|
257
|
+
]);
|
|
258
|
+
function stderrIsForecastNarration(stderr: string): boolean {
|
|
259
|
+
const lines = stderr.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
260
|
+
return lines.length > 0 && lines.every((line) => FORECAST_NARRATION_LINES.some((pattern) => pattern.test(line)));
|
|
261
|
+
}
|
|
262
|
+
|
|
204
263
|
export const NATIVE_REVIEW_RECOVER_DISPOSITION = ["scope_changed", "invalidated", "escalated"] as const;
|
|
205
264
|
export type NativeReviewRecoverDisposition = (typeof NATIVE_REVIEW_RECOVER_DISPOSITION)[number];
|
|
206
265
|
|
|
@@ -244,6 +303,9 @@ export interface NativeReviewAbandonRequest {
|
|
|
244
303
|
lineage: string;
|
|
245
304
|
expectedRevision: string;
|
|
246
305
|
snapshotIdentity: string;
|
|
306
|
+
capturedLensResults: readonly string[];
|
|
307
|
+
findingsPresent: boolean;
|
|
308
|
+
evidenceRecordsPresent: boolean;
|
|
247
309
|
actor: string;
|
|
248
310
|
reason: string;
|
|
249
311
|
maintainerAuthorization: string;
|
|
@@ -345,6 +407,33 @@ export interface NativeReviewAdmittedResultManifest {
|
|
|
345
407
|
readonly reference?: string;
|
|
346
408
|
}
|
|
347
409
|
|
|
410
|
+
// gentle-pi#311 P4-roles: one Go-owned non-lens provider role capture. The
|
|
411
|
+
// provider renders a SELF-CONTAINED authority-advancing vector
|
|
412
|
+
// (`review.capture-refuter` / `review.capture-validation` with binding tokens
|
|
413
|
+
// plus `--agent=pi --execute=true`, no submission descriptor); Pi executes the
|
|
414
|
+
// exact rendered invocation verbatim, in the foreground, and Go materializes
|
|
415
|
+
// the role prompt, spawns its own locked-down pi subprocess, and admits the
|
|
416
|
+
// raw verdict. Nothing here authors, parses, or transports role output.
|
|
417
|
+
export const NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA = "gentle-ai.review-provider-role-capture/v1";
|
|
418
|
+
|
|
419
|
+
export interface NativeReviewProviderRoleCaptureRequest {
|
|
420
|
+
/** The provider-named capture operation, e.g. `review.capture-refuter`. */
|
|
421
|
+
readonly captureOperation: string;
|
|
422
|
+
/** Every provider-issued argument token, verbatim, in provider order. */
|
|
423
|
+
readonly argumentTokens: readonly string[];
|
|
424
|
+
/** Process working directory only; never rendered into the invocation. */
|
|
425
|
+
readonly cwd: string;
|
|
426
|
+
readonly signal?: AbortSignal;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export interface NativeReviewProviderRoleCaptureArtifact {
|
|
430
|
+
readonly schema: typeof NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA;
|
|
431
|
+
readonly lineageId: string;
|
|
432
|
+
readonly targetIdentity: string;
|
|
433
|
+
readonly role: string;
|
|
434
|
+
readonly captured: true;
|
|
435
|
+
}
|
|
436
|
+
|
|
348
437
|
export interface NativeReviewCaptureEvidenceRequest {
|
|
349
438
|
cwd: string;
|
|
350
439
|
lineageId: string;
|
|
@@ -355,6 +444,27 @@ export interface NativeReviewCaptureEvidenceRequest {
|
|
|
355
444
|
signal?: AbortSignal;
|
|
356
445
|
}
|
|
357
446
|
|
|
447
|
+
// Field defect (fambig, 2026-08-16): the evidence collect slot renders the
|
|
448
|
+
// exact submission tokens native admits — fix-diff `--target`,
|
|
449
|
+
// `--expected-revision`, and an opaque cwd-independent `--repository-context` —
|
|
450
|
+
// with `{{outcome}}`/`{{input}}` substitution slots. Satisfying the slot means
|
|
451
|
+
// executing those tokens verbatim with only the two slot substitutions, the
|
|
452
|
+
// same discipline captureResult uses; identities are never reconstructed from
|
|
453
|
+
// top-level status fields.
|
|
454
|
+
export interface NativeReviewCaptureEvidenceSubmissionRequest {
|
|
455
|
+
/** Process working directory; forbidden when the tokens carry --repository-context (the context is cwd-independent). */
|
|
456
|
+
cwd?: string;
|
|
457
|
+
/** Provider-rendered submission argument tokens, verbatim, in provider order. */
|
|
458
|
+
argumentTokens: readonly string[];
|
|
459
|
+
/** Index of the token carrying the {{outcome}} substitution slot. */
|
|
460
|
+
outcomeSubstitutionLocation: number;
|
|
461
|
+
/** Index of the token carrying the {{input}} substitution slot. */
|
|
462
|
+
inputSubstitutionLocation: number;
|
|
463
|
+
outcome: NativeReviewCaptureOutcome;
|
|
464
|
+
evidenceDocument: string;
|
|
465
|
+
signal?: AbortSignal;
|
|
466
|
+
}
|
|
467
|
+
|
|
358
468
|
export interface NativeReviewVerificationEvidenceV2 {
|
|
359
469
|
schema: "gentle-ai.review-verification-evidence/v2";
|
|
360
470
|
version: 2;
|
|
@@ -374,7 +484,7 @@ export interface NativeReviewVerificationEvidenceV2 {
|
|
|
374
484
|
export interface NativeStartRequest { cwd: string; baseRef?: string; committedOnly?: boolean; lineageId?: string; policyPath?: string; focus?: string; targetIdentity?: string; projection?: "workspace" | "staged"; signal?: AbortSignal; }
|
|
375
485
|
export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "declined" } as const;
|
|
376
486
|
export type NativeReviewConsentAnswer = (typeof NATIVE_REVIEW_CONSENT_ANSWER)[keyof typeof NATIVE_REVIEW_CONSENT_ANSWER];
|
|
377
|
-
export interface NativeReviewConsentAnswerRequest { cwd: string; consent:
|
|
487
|
+
export interface NativeReviewConsentAnswerRequest { cwd: string; consent: ReviewConsentEnvelope; answer: NativeReviewConsentAnswer; signal?: AbortSignal; }
|
|
378
488
|
export interface NativeReviewConsentDeclinedResult {
|
|
379
489
|
kind: "declined";
|
|
380
490
|
targetIdentity: string;
|
|
@@ -403,12 +513,56 @@ export interface NativeFinalizeRequest extends NativeReviewFinalizeCapturedResul
|
|
|
403
513
|
failed?: boolean;
|
|
404
514
|
signal?: AbortSignal;
|
|
405
515
|
}
|
|
516
|
+
// gentle-pi#311 P5: the provider-driven FINALIZE. `argumentTokens` are the
|
|
517
|
+
// exact rendered tokens of one provider-returned `review.finalize` execute
|
|
518
|
+
// transition (e.g. `--lineage=<id> --captured-results=true`), passed through
|
|
519
|
+
// verbatim and never synthesized or reordered by the host.
|
|
520
|
+
export interface NativeReviewFinalizeTransitionRequest {
|
|
521
|
+
readonly cwd: string;
|
|
522
|
+
readonly argumentTokens: readonly string[];
|
|
523
|
+
readonly signal?: AbortSignal;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// The provider-rendered `finalize` submission descriptor forms (status/v5):
|
|
527
|
+
// the correction PLAN slot substitutes a positive line-count literal into its
|
|
528
|
+
// {{value}} token; the TARGETED VALIDATION slot substitutes a staged validator
|
|
529
|
+
// artifact path. The tokens are self-contained and execute verbatim.
|
|
530
|
+
export interface NativeReviewFinalizeSubmissionRequest {
|
|
531
|
+
/** Process working directory only; the rendered tokens are self-contained. */
|
|
532
|
+
readonly cwd: string;
|
|
533
|
+
/** Provider-rendered submission argument tokens, verbatim, in provider order. */
|
|
534
|
+
readonly argumentTokens: readonly string[];
|
|
535
|
+
/** Index of the token carrying the {{value}} substitution slot. */
|
|
536
|
+
readonly valueSubstitutionLocation: number;
|
|
537
|
+
/** The literal substitution (correction_lines). Exactly one of valueLiteral/valueDocument. */
|
|
538
|
+
readonly valueLiteral?: string;
|
|
539
|
+
/** The document to stage as a 0o600 artifact whose path substitutes {{value}} (validation). */
|
|
540
|
+
readonly valueDocument?: string;
|
|
541
|
+
readonly signal?: AbortSignal;
|
|
542
|
+
}
|
|
543
|
+
|
|
406
544
|
export interface NativeValidateRequest { cwd: string; gate: string; lineageId?: string; flags?: readonly string[]; signal?: AbortSignal; }
|
|
407
545
|
export interface NativeBindSddRequest { cwd: string; change: string; lineage: string; expectedBindingRevision: string; signal?: AbortSignal; }
|
|
408
546
|
export interface NativeSddStatusRequest { cwd: string; change: string; signal?: AbortSignal; }
|
|
409
547
|
export interface NativeReviewStatusRequest { cwd: string; signal?: AbortSignal; }
|
|
410
548
|
export interface NativeCapabilitiesRequest { cwd?: string; signal?: AbortSignal; }
|
|
411
|
-
export interface NativeTargetStatusRequest {
|
|
549
|
+
export interface NativeTargetStatusRequest {
|
|
550
|
+
cwd: string;
|
|
551
|
+
lineageId?: string;
|
|
552
|
+
baseRef?: string;
|
|
553
|
+
projection?: "workspace" | "staged";
|
|
554
|
+
/**
|
|
555
|
+
* The immutable reviewer runtime this host provides. Measured against the
|
|
556
|
+
* live 2.4.0-main provider: the materialize-marked relay slot (agent=pi,
|
|
557
|
+
* materialize=true, plus the provider submission) is offered ONLY when the
|
|
558
|
+
* caller names its agent; an agent-less status returns a bare
|
|
559
|
+
* capture-result input the host relay cannot consume. Providers older than
|
|
560
|
+
* v2.4.0 do not define the flag and refuse it, so callers probe and fall
|
|
561
|
+
* back rather than version-sniff.
|
|
562
|
+
*/
|
|
563
|
+
agent?: "pi";
|
|
564
|
+
signal?: AbortSignal;
|
|
565
|
+
}
|
|
412
566
|
export interface NativeGateContext { lineageId: string; storeRevision: string; raw: Record<string, unknown>; }
|
|
413
567
|
|
|
414
568
|
export const NATIVE_REVIEW_AUTHORITY_STATUS = {
|
|
@@ -666,6 +820,20 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
666
820
|
// advertises capabilities/v1.5 and the negotiated start envelope is still
|
|
667
821
|
// the closed `start/v2`, so riskEvidence and hint still cannot arrive.
|
|
668
822
|
"2.2.2": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
823
|
+
// Ground-truthed against the released v2.2.3 binary: the v2 lane remains
|
|
824
|
+
// protocol 2.0 with the same operation set and closed START fields consumed
|
|
825
|
+
// by Pi, so the existing capability columns are unchanged.
|
|
826
|
+
"2.2.3": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
827
|
+
// Ground-truthed against the released v2.4.0 binary: the v2 lane advertises
|
|
828
|
+
// capabilities/v2.2 and answers status/v5 and consent/v3, all of which the
|
|
829
|
+
// existing decoders already read, and the START envelope Pi consumes is
|
|
830
|
+
// still `start/v3` carrying `risk_reasons` with no `risk_evidence` and no
|
|
831
|
+
// `hint`, so the existing capability columns are unchanged. v2.4.0 also
|
|
832
|
+
// made receipt-driven development opt-in, which changes what the mode
|
|
833
|
+
// envelope reports, not whether it reports it. v2.2.4 and v2.3.0 shipped
|
|
834
|
+
// while Pi stayed on 2.2.3; they were never pinned or probed, so they get
|
|
835
|
+
// no row.
|
|
836
|
+
"2.4.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
669
837
|
});
|
|
670
838
|
type NativeCliCapability = keyof (typeof NATIVE_CLI_CONTRACTS)[keyof typeof NATIVE_CLI_CONTRACTS];
|
|
671
839
|
|
|
@@ -682,6 +850,21 @@ function resolvedNativeCliContract(version: string): Record<NativeCliCapability,
|
|
|
682
850
|
return nativeCliContractsTestingOverlay.get(version) ?? (NATIVE_CLI_CONTRACTS as Record<string, Record<NativeCliCapability, boolean> | undefined>)[version];
|
|
683
851
|
}
|
|
684
852
|
|
|
853
|
+
// The latest known contract row, used as the capability floor for the
|
|
854
|
+
// explicit dev-binary override (unpinned field-test mode): a maintainer
|
|
855
|
+
// dev build is at least as capable as the newest release Pi knows. The table
|
|
856
|
+
// is declared in ascending release order, so its last key is the latest.
|
|
857
|
+
const LATEST_NATIVE_CLI_CONTRACT_VERSION = Object.keys(NATIVE_CLI_CONTRACTS).at(-1) as keyof typeof NATIVE_CLI_CONTRACTS;
|
|
858
|
+
|
|
859
|
+
// Dev-binary override version discipline: with the override configured, a
|
|
860
|
+
// version reported outside the pinned table resolves to the latest known row;
|
|
861
|
+
// with the override absent, behavior is byte-identical to the pinned gate.
|
|
862
|
+
function effectiveNativeCliContract(version: string): Record<NativeCliCapability, boolean> | undefined {
|
|
863
|
+
const pinned = resolvedNativeCliContract(version);
|
|
864
|
+
if (pinned !== undefined) return pinned;
|
|
865
|
+
return gentleAiDevBinaryOverrideConfigured() ? NATIVE_CLI_CONTRACTS[LATEST_NATIVE_CLI_CONTRACT_VERSION] : undefined;
|
|
866
|
+
}
|
|
867
|
+
|
|
685
868
|
export interface NativeReviewStructuredDenial {
|
|
686
869
|
schema: "gentle-ai.review-gate-result/v1";
|
|
687
870
|
result: "scope-changed" | "invalidated" | "escalated";
|
|
@@ -697,6 +880,8 @@ export interface NativeReviewProcessDiagnostics {
|
|
|
697
880
|
signal?: NodeJS.Signals;
|
|
698
881
|
timed_out: boolean;
|
|
699
882
|
output_limit_exceeded: boolean;
|
|
883
|
+
max_buffer_bytes?: number;
|
|
884
|
+
configuration_hint?: string;
|
|
700
885
|
stderr?: string;
|
|
701
886
|
denial?: NativeReviewStructuredDenial;
|
|
702
887
|
}
|
|
@@ -724,15 +909,25 @@ export class NativeReviewCliError extends Error {
|
|
|
724
909
|
}
|
|
725
910
|
}
|
|
726
911
|
|
|
912
|
+
// The one central runner for every gentle-ai CLI invocation the extension
|
|
913
|
+
// makes. It declares the Pi host relay handshake on each spawn: gentle-ai
|
|
914
|
+
// refuses pi admission pre-authority without it (gentle-pi#311 P4), and a
|
|
915
|
+
// single injection point keeps the declaration impossible to forget on any
|
|
916
|
+
// individual operation.
|
|
917
|
+
export function gentleAiProcessEnvironment(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
918
|
+
return { ...base, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT };
|
|
919
|
+
}
|
|
920
|
+
|
|
727
921
|
export function createNodeExecFileAdapter(): ExecFileAdapter {
|
|
728
922
|
return async (request) => {
|
|
729
923
|
try {
|
|
730
|
-
const output = await execFileAsync(request.file, [...request.arguments], { cwd: request.cwd, encoding: "utf8", shell: false, windowsHide: true, timeout: request.timeoutMs, maxBuffer: request.maxBufferBytes, signal: request.signal });
|
|
924
|
+
const output = await execFileAsync(request.file, [...request.arguments], { cwd: request.cwd, encoding: "utf8", shell: false, windowsHide: true, timeout: request.timeoutMs, maxBuffer: request.maxBufferBytes, signal: request.signal, env: gentleAiProcessEnvironment() });
|
|
731
925
|
return { stdout: output.stdout, stderr: output.stderr, exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
732
926
|
} catch (error) {
|
|
733
927
|
const detail = error as NodeJS.ErrnoException & { stdout?: string; stderr?: string; code?: string | number; signal?: NodeJS.Signals; killed?: boolean };
|
|
734
928
|
if (detail.code === "ENOENT" || detail.code === "EACCES" || detail.name === "AbortError") throw error;
|
|
735
|
-
|
|
929
|
+
const outputLimitExceeded = detail.code === "ENOBUFS" || detail.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
|
|
930
|
+
return { stdout: detail.stdout ?? "", stderr: detail.stderr ?? "", exitCode: typeof detail.code === "number" ? detail.code : 1, signal: detail.signal ?? null, timedOut: !outputLimitExceeded && detail.killed === true, outputLimitExceeded };
|
|
736
931
|
}
|
|
737
932
|
};
|
|
738
933
|
}
|
|
@@ -808,12 +1003,28 @@ function parseStructuredNativeDenial(stdout: string): NativeReviewStructuredDeni
|
|
|
808
1003
|
// Rebuild diagnostics from a duplicated module instance before facade output.
|
|
809
1004
|
export function sanitizeForeignNativeReviewDiagnostics(value: unknown): NativeReviewProcessDiagnostics | undefined {
|
|
810
1005
|
try {
|
|
811
|
-
const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "stderr", "denial"]);
|
|
1006
|
+
const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "max_buffer_bytes", "configuration_hint", "stderr", "denial"]);
|
|
812
1007
|
const operation = enumString(raw.operation, Object.values(NATIVE_REVIEW_OPERATION)) as NativeReviewOperation;
|
|
813
1008
|
const errorCode = enumString(raw.error_code, Object.values(NATIVE_REVIEW_ERROR_CODE)) as NativeReviewErrorCode;
|
|
814
1009
|
const signal = raw.signal === undefined ? undefined : requiredString(raw.signal);
|
|
815
|
-
|
|
816
|
-
|
|
1010
|
+
const maxBufferBytes = raw.max_buffer_bytes === undefined ? undefined : positiveInteger(raw.max_buffer_bytes);
|
|
1011
|
+
const configurationHint = raw.configuration_hint === undefined ? undefined : stringValue(raw.configuration_hint);
|
|
1012
|
+
if (
|
|
1013
|
+
(signal !== undefined && !/^SIG[A-Z0-9]{1,12}$/.test(signal)) ||
|
|
1014
|
+
(maxBufferBytes === undefined) !== (configurationHint === undefined) ||
|
|
1015
|
+
(configurationHint !== undefined && (errorCode !== NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT || configurationHint !== NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT))
|
|
1016
|
+
) return undefined;
|
|
1017
|
+
return {
|
|
1018
|
+
operation,
|
|
1019
|
+
error_code: errorCode,
|
|
1020
|
+
...(raw.exit_code === undefined ? {} : { exit_code: nonNegativeInteger(raw.exit_code) }),
|
|
1021
|
+
...(signal === undefined ? {} : { signal: signal as NodeJS.Signals }),
|
|
1022
|
+
timed_out: booleanValue(raw.timed_out),
|
|
1023
|
+
output_limit_exceeded: booleanValue(raw.output_limit_exceeded),
|
|
1024
|
+
...(maxBufferBytes === undefined ? {} : { max_buffer_bytes: maxBufferBytes, configuration_hint: configurationHint! }),
|
|
1025
|
+
...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr)) }),
|
|
1026
|
+
...(raw.denial === undefined ? {} : { denial: sanitizeForeignStructuredDenial(raw.denial) }),
|
|
1027
|
+
};
|
|
817
1028
|
} catch { return undefined; }
|
|
818
1029
|
}
|
|
819
1030
|
|
|
@@ -829,14 +1040,18 @@ function sanitizeForeignStructuredDenial(value: unknown): NativeReviewStructured
|
|
|
829
1040
|
return { schema: "gentle-ai.review-gate-result/v1", result, action, reason, ...(denial === undefined ? {} : { denial }) };
|
|
830
1041
|
}
|
|
831
1042
|
|
|
832
|
-
function nativeProcessDiagnostics(operation: NativeReviewOperation, code: NativeReviewErrorCode, result?: ExecFileResult): NativeReviewProcessDiagnostics {
|
|
1043
|
+
function nativeProcessDiagnostics(operation: NativeReviewOperation, code: NativeReviewErrorCode, result?: ExecFileResult, maxBufferBytes?: number): NativeReviewProcessDiagnostics {
|
|
1044
|
+
const outputLimitExceeded = result?.outputLimitExceeded === true;
|
|
833
1045
|
return {
|
|
834
1046
|
operation,
|
|
835
1047
|
error_code: code,
|
|
836
1048
|
...(result === undefined ? {} : { exit_code: result.exitCode }),
|
|
837
1049
|
...(result?.signal === null || result?.signal === undefined ? {} : { signal: result.signal }),
|
|
838
|
-
timed_out: result?.timedOut === true,
|
|
839
|
-
output_limit_exceeded:
|
|
1050
|
+
timed_out: !outputLimitExceeded && result?.timedOut === true,
|
|
1051
|
+
output_limit_exceeded: outputLimitExceeded,
|
|
1052
|
+
...(code === NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT && maxBufferBytes !== undefined
|
|
1053
|
+
? { max_buffer_bytes: maxBufferBytes, configuration_hint: NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT }
|
|
1054
|
+
: {}),
|
|
840
1055
|
...(result?.stderr.trim() ? { stderr: sanitizeNativeDiagnosticText(result.stderr) } : {}),
|
|
841
1056
|
...(result === undefined ? {} : { denial: parseStructuredNativeDenial(result.stdout) }),
|
|
842
1057
|
};
|
|
@@ -1154,8 +1369,8 @@ function hasValidLensesRequired(action: NativeStartAction, state: string, riskLe
|
|
|
1154
1369
|
return !lensesRequired;
|
|
1155
1370
|
}
|
|
1156
1371
|
|
|
1157
|
-
function nativeError(code: NativeReviewErrorCode, operation: NativeReviewOperation, mutating: boolean, message: string, result?: ExecFileResult, launchAttempted = true, auditRecord?: Record<string, unknown
|
|
1158
|
-
return new NativeReviewCliError(code, operation, launchAttempted, mutating, message, nativeProcessDiagnostics(operation, code, result), auditRecord);
|
|
1372
|
+
function nativeError(code: NativeReviewErrorCode, operation: NativeReviewOperation, mutating: boolean, message: string, result?: ExecFileResult, launchAttempted = true, auditRecord?: Record<string, unknown>, maxBufferBytes?: number): NativeReviewCliError {
|
|
1373
|
+
return new NativeReviewCliError(code, operation, launchAttempted, mutating, message, nativeProcessDiagnostics(operation, code, result, maxBufferBytes), auditRecord);
|
|
1159
1374
|
}
|
|
1160
1375
|
|
|
1161
1376
|
interface NativeJsonExecution {
|
|
@@ -1170,7 +1385,7 @@ export class NativeReviewCliV214 {
|
|
|
1170
1385
|
private readonly timeoutMs: number;
|
|
1171
1386
|
private readonly maxBufferBytes: number;
|
|
1172
1387
|
private readonly cleanupDirectory: (directory: string) => Promise<void>;
|
|
1173
|
-
constructor(adapter: ExecFileAdapter, executable: string | (() => string) = resolveGentleAiBinary, timeoutMs = 30_000, maxBufferBytes =
|
|
1388
|
+
constructor(adapter: ExecFileAdapter, executable: string | (() => string) = resolveGentleAiBinary, timeoutMs = 30_000, maxBufferBytes = resolveNativeReviewMaxBufferBytes(), cleanupDirectory = (directory: string) => rm(directory, { recursive: true, force: true })) {
|
|
1174
1389
|
if (typeof executable === "string" && (!isAbsolute(executable) || executable === "gentle-ai")) throw new TypeError("Native review requires an absolute package-local executable");
|
|
1175
1390
|
this.adapter = adapter;
|
|
1176
1391
|
this.executable = executable;
|
|
@@ -1207,8 +1422,8 @@ export class NativeReviewCliV214 {
|
|
|
1207
1422
|
throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, operation, mutating, "native process could not start");
|
|
1208
1423
|
}
|
|
1209
1424
|
const diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.NON_ZERO, result);
|
|
1425
|
+
if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result, true, undefined, this.maxBufferBytes);
|
|
1210
1426
|
if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
|
|
1211
|
-
if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result);
|
|
1212
1427
|
if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
|
|
1213
1428
|
const structuredValidateDenial = operation === NATIVE_REVIEW_OPERATION.VALIDATE && result.exitCode === 1;
|
|
1214
1429
|
const maintenancePartialFailure = [NATIVE_REVIEW_OPERATION.ABANDON, NATIVE_REVIEW_OPERATION.QUARANTINE_LEGACY, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, NATIVE_REVIEW_OPERATION.REPAIR_LEGACY_ALIAS].includes(operation) && result.exitCode !== 0;
|
|
@@ -1226,12 +1441,18 @@ export class NativeReviewCliV214 {
|
|
|
1226
1441
|
if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was cancelled");
|
|
1227
1442
|
throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, NATIVE_REVIEW_OPERATION.VERSION, false, "gentle-ai is unavailable");
|
|
1228
1443
|
}
|
|
1444
|
+
if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process output exceeded limit", result, true, undefined, this.maxBufferBytes);
|
|
1229
1445
|
if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process timed out", result);
|
|
1230
|
-
if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process output exceeded limit", result);
|
|
1231
1446
|
if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was signalled", result);
|
|
1232
1447
|
if (result.exitCode !== 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.VERSION, false, "version process failed", result);
|
|
1233
|
-
const
|
|
1234
|
-
const
|
|
1448
|
+
const normalized = result.stdout.replace(/\r\n$/, "\n");
|
|
1449
|
+
const pinnedVersion = /^gentle-ai ([0-9]+\.[0-9]+\.[0-9]+)\n$/.exec(normalized)?.[1];
|
|
1450
|
+
// Dev-binary override (unpinned field-test mode): accept the binary's
|
|
1451
|
+
// reported version even when it is not a pinned three-part release
|
|
1452
|
+
// version (a main-line dev build reports e.g. "2.4.0-rc.8+fix...").
|
|
1453
|
+
// The banner shape itself stays strict. Pinned mode is byte-identical.
|
|
1454
|
+
const version = pinnedVersion ?? (gentleAiDevBinaryOverrideConfigured() ? /^gentle-ai (\S+)\n$/.exec(normalized)?.[1] : undefined);
|
|
1455
|
+
const contract = version === undefined ? undefined : effectiveNativeCliContract(version);
|
|
1235
1456
|
if (result.stderr.trim().length > 0 || contract === undefined || capabilities.some((capability) => !contract[capability])) throw nativeError(NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.VERSION, false, `native gentle-ai lacks required capabilities: expected v${GENTLE_AI_VERSION}, found v${version ?? "unparseable"}`);
|
|
1236
1457
|
return version as keyof typeof NATIVE_CLI_CONTRACTS;
|
|
1237
1458
|
}
|
|
@@ -1242,7 +1463,7 @@ export class NativeReviewCliV214 {
|
|
|
1242
1463
|
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
|
|
1243
1464
|
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
|
|
1244
1465
|
const version = await this.verifyVersion(request.cwd, request.signal, ["start"]);
|
|
1245
|
-
const toleratedStderr =
|
|
1466
|
+
const toleratedStderr = effectiveNativeCliContract(version)?.mode === true ? REVIEW_CONSENT_NOTICES : [];
|
|
1246
1467
|
const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.START, request.cwd, ["review", "start", "--cwd", request.cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), ...(request.lineageId ? ["--lineage", request.lineageId] : []), ...(request.policyPath ? ["--policy", request.policyPath] : []), ...(request.focus ? ["--focus", request.focus] : [])], true, request.signal, toleratedStderr);
|
|
1247
1468
|
return decode(NATIVE_REVIEW_OPERATION.START, true, () => {
|
|
1248
1469
|
// `target_identity` and `lens_bindings` are real, unconditionally-present
|
|
@@ -1500,7 +1721,11 @@ export class NativeReviewCliV214 {
|
|
|
1500
1721
|
for (const [name, value] of [["lineage", request.lineage], ["expectedRevision", request.expectedRevision], ["snapshotIdentity", request.snapshotIdentity], ["actor", request.actor], ["reason", request.reason]] as const) {
|
|
1501
1722
|
if (!isCanonicalProcessString(value)) throw new TypeError(`Native ABANDON ${name} must be a non-empty, trimmed, NUL-free string`);
|
|
1502
1723
|
}
|
|
1503
|
-
if (request.
|
|
1724
|
+
if (!Array.isArray(request.capturedLensResults) || request.capturedLensResults.some((entry) => !isCanonicalProcessString(entry))) throw new TypeError("Native ABANDON capturedLensResults must be an array of non-empty, trimmed, NUL-free strings");
|
|
1725
|
+
for (const [name, value] of [["findingsPresent", request.findingsPresent], ["evidenceRecordsPresent", request.evidenceRecordsPresent]] as const) {
|
|
1726
|
+
if (typeof value !== "boolean") throw new TypeError(`Native ABANDON ${name} must be a boolean`);
|
|
1727
|
+
}
|
|
1728
|
+
if (request.maintainerAuthorization !== nativeReviewAbandonAuthorization(request)) throw new TypeError("Native ABANDON maintainerAuthorization must match the exact lineage, revision, snapshot, reason, discarded-work, and actor binding");
|
|
1504
1729
|
await this.verifyVersion(request.cwd, request.signal, ["abandon"]);
|
|
1505
1730
|
const execution = await this.execute(NATIVE_REVIEW_OPERATION.ABANDON, request.cwd, [
|
|
1506
1731
|
"review", "abandon", "--cwd", request.cwd,
|
|
@@ -1594,14 +1819,17 @@ export class NativeReviewCliV214 {
|
|
|
1594
1819
|
}
|
|
1595
1820
|
}
|
|
1596
1821
|
|
|
1597
|
-
export function nativeReviewAbandonAuthorization(request: Pick<NativeReviewAbandonRequest, "lineage" | "expectedRevision" | "snapshotIdentity" | "actor" | "reason">): string {
|
|
1822
|
+
export function nativeReviewAbandonAuthorization(request: Pick<NativeReviewAbandonRequest, "lineage" | "expectedRevision" | "snapshotIdentity" | "capturedLensResults" | "findingsPresent" | "evidenceRecordsPresent" | "actor" | "reason">): string {
|
|
1598
1823
|
return [
|
|
1599
|
-
"gentle-ai.review-abandon-authorization/
|
|
1824
|
+
"gentle-ai.review-abandon-authorization/v2",
|
|
1600
1825
|
`lineage=${request.lineage}`,
|
|
1601
1826
|
`revision=${request.expectedRevision}`,
|
|
1602
1827
|
`snapshot_identity=${request.snapshotIdentity}`,
|
|
1603
|
-
`actor=${request.actor}`,
|
|
1604
1828
|
`reason=${request.reason}`,
|
|
1829
|
+
`captured_lens_results=${request.capturedLensResults.join(",")}`,
|
|
1830
|
+
`findings_present=${request.findingsPresent}`,
|
|
1831
|
+
`evidence_records_present=${request.evidenceRecordsPresent}`,
|
|
1832
|
+
`actor=${request.actor.trim()}`,
|
|
1605
1833
|
].join("\n");
|
|
1606
1834
|
}
|
|
1607
1835
|
|
|
@@ -1618,6 +1846,32 @@ export function nativeReviewLegacyQuarantineAuthorization(request: Pick<NativeRe
|
|
|
1618
1846
|
].join("\n");
|
|
1619
1847
|
}
|
|
1620
1848
|
|
|
1849
|
+
/**
|
|
1850
|
+
* The exact native `gentle-ai.review-recovery-authorization/v1` binding for one
|
|
1851
|
+
* recovery edge.
|
|
1852
|
+
*
|
|
1853
|
+
* Native `review recover` accepts a caller-supplied authorization only when it
|
|
1854
|
+
* reproduces this binding byte for byte, because it is copied verbatim into the
|
|
1855
|
+
* recovery provenance and read afterwards as a maintainer attestation. Pi
|
|
1856
|
+
* therefore derives it from freshly read native target status and never
|
|
1857
|
+
* forwards a caller-supplied one: a wrong binding is worse than an absent one,
|
|
1858
|
+
* since an absent field cannot lie about who approved what.
|
|
1859
|
+
*
|
|
1860
|
+
* `targetIdentity` is the live target identity the provider itself publishes in
|
|
1861
|
+
* the `review.recover` eligibility binding (`status.target_identity`), which is
|
|
1862
|
+
* the identity the successor's initial snapshot takes.
|
|
1863
|
+
*/
|
|
1864
|
+
export function nativeReviewRecoverAuthorization(request: Pick<NativeReviewRecoverRequest, "predecessorLineage" | "expectedPredecessorRevision" | "actor" | "reason"> & { targetIdentity: string }): string {
|
|
1865
|
+
return [
|
|
1866
|
+
"gentle-ai.review-recovery-authorization/v1",
|
|
1867
|
+
`predecessor_lineage=${request.predecessorLineage}`,
|
|
1868
|
+
`predecessor_revision=${request.expectedPredecessorRevision}`,
|
|
1869
|
+
`target_identity=${request.targetIdentity}`,
|
|
1870
|
+
`actor=${request.actor.trim()}`,
|
|
1871
|
+
`reason=${request.reason.trim()}`,
|
|
1872
|
+
].join("\n");
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1621
1875
|
export function nativeReviewReconcileAuthorization(request: Pick<NativeReviewReconcileAuthorityRequest, "predecessorLineage" | "expectedPredecessorRevision" | "successorLineage" | "expectedSuccessorRevision" | "actor" | "reason" | "anomalies">): string {
|
|
1622
1876
|
return [
|
|
1623
1877
|
"gentle-ai.review-reconcile-authorization/v1",
|
|
@@ -1658,15 +1912,16 @@ export class NativeReviewIntegrationError extends Error {
|
|
|
1658
1912
|
}
|
|
1659
1913
|
}
|
|
1660
1914
|
|
|
1661
|
-
// Raised when negotiated START answers `consent/v2`
|
|
1915
|
+
// Raised when negotiated START answers a consent question (`consent/v2` from
|
|
1916
|
+
// the pinned line, `consent/v3` from gentle-ai >= 2.3.0; action:
|
|
1662
1917
|
// "consent_required") instead of `start/v3`. The provider has frozen no
|
|
1663
1918
|
// authority yet: Pi must relay this complete candidate-scoped question and may
|
|
1664
1919
|
// answer only through one of the exact invocations carried by the envelope.
|
|
1665
1920
|
export class NativeReviewConsentRequiredError extends Error {
|
|
1666
|
-
readonly consent:
|
|
1921
|
+
readonly consent: ReviewConsentEnvelope;
|
|
1667
1922
|
readonly launchAttempted = true;
|
|
1668
1923
|
readonly mutationOutcome = "none";
|
|
1669
|
-
constructor(consent:
|
|
1924
|
+
constructor(consent: ReviewConsentEnvelope) {
|
|
1670
1925
|
super(consent.headline);
|
|
1671
1926
|
this.name = "NativeReviewConsentRequiredError";
|
|
1672
1927
|
this.consent = consent;
|
|
@@ -1750,7 +2005,30 @@ function exactConsentOption(arguments_: readonly string[], name: string): string
|
|
|
1750
2005
|
return values[0]!;
|
|
1751
2006
|
}
|
|
1752
2007
|
|
|
1753
|
-
function
|
|
2008
|
+
function optionalConsentLineageOption(arguments_: readonly string[]): string | undefined {
|
|
2009
|
+
const values: string[] = [];
|
|
2010
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
2011
|
+
const token = arguments_[index]!;
|
|
2012
|
+
if (token === "--lineage") {
|
|
2013
|
+
const value = arguments_[index + 1];
|
|
2014
|
+
if (value === undefined || value.startsWith("--")) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", "Native consent invocation --lineage is missing its value");
|
|
2015
|
+
values.push(value);
|
|
2016
|
+
index += 1;
|
|
2017
|
+
} else if (token.startsWith("--lineage=")) values.push(token.slice("--lineage=".length));
|
|
2018
|
+
}
|
|
2019
|
+
if (values.length > 1) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", "Native consent invocation permits at most one --lineage");
|
|
2020
|
+
if (values.length === 0) return undefined;
|
|
2021
|
+
const lineageId = values[0]!;
|
|
2022
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(lineageId)) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", "Native consent invocation --lineage is malformed");
|
|
2023
|
+
return lineageId;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
interface ConsentInvocation {
|
|
2027
|
+
arguments_: readonly string[];
|
|
2028
|
+
lineageId?: string;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function consentInvocationArguments(request: NativeReviewConsentAnswerRequest): ConsentInvocation {
|
|
1754
2032
|
const choice = request.consent.choices.find((candidate) => candidate.answer === request.answer);
|
|
1755
2033
|
if (choice === undefined) throw new NativeReviewConsentBindingError("consent-answer-unknown", "Native consent answer must be granted or declined");
|
|
1756
2034
|
const words = splitNativeConsentInvocation(choice.invocation);
|
|
@@ -1760,8 +2038,9 @@ function consentInvocationArguments(request: NativeReviewConsentAnswerRequest):
|
|
|
1760
2038
|
if (exactConsentOption(arguments_, "--cwd") !== request.cwd) throw new NativeReviewConsentBindingError("consent-invocation-cwd-changed", "Native consent invocation repository binding changed");
|
|
1761
2039
|
if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new NativeReviewConsentBindingError("consent-invocation-target-changed", "Native consent invocation target binding changed");
|
|
1762
2040
|
if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new NativeReviewConsentBindingError("consent-invocation-projection-changed", "Native consent invocation projection binding changed");
|
|
2041
|
+
const lineageId = optionalConsentLineageOption(arguments_);
|
|
1763
2042
|
if (exactConsentOption(arguments_, "--consent") !== request.answer || arguments_.at(-1) !== request.answer) throw new NativeReviewConsentBindingError("consent-invocation-answer-changed", "Native consent invocation answer binding changed");
|
|
1764
|
-
return arguments_;
|
|
2043
|
+
return { arguments_, ...(lineageId === undefined ? {} : { lineageId }) };
|
|
1765
2044
|
}
|
|
1766
2045
|
|
|
1767
2046
|
function decodeDeclinedConsentStart(value: unknown, expected: NativeReviewConsentAnswerRequest): NativeReviewConsentDeclinedResult {
|
|
@@ -1810,27 +2089,46 @@ interface NegotiatedExecution {
|
|
|
1810
2089
|
}
|
|
1811
2090
|
|
|
1812
2091
|
function decodeNativeAdmittedResultManifest(value: unknown): NativeReviewAdmittedResultManifest {
|
|
1813
|
-
|
|
2092
|
+
// The admission answer routes through the exact-identity forward decoder
|
|
2093
|
+
// (decoder-freshness discipline): the complete live envelope — identity
|
|
2094
|
+
// constants, binding fields, and the exactly-one-locator rule — is
|
|
2095
|
+
// validated before anything is handed to FINALIZE, and an unknown field
|
|
2096
|
+
// grown by gentle-ai main is rejected instead of silently dropped.
|
|
2097
|
+
const artifact = decodeReviewResultArtifactV2(value);
|
|
2098
|
+
return Object.freeze({
|
|
2099
|
+
schema: artifact.schema,
|
|
2100
|
+
subjectHash: artifact.subjectHash,
|
|
2101
|
+
admissionDecision: artifact.admissionDecision,
|
|
2102
|
+
lens: artifact.lens,
|
|
2103
|
+
...(artifact.path === undefined ? {} : { path: artifact.path }),
|
|
2104
|
+
...(artifact.reference === undefined ? {} : { reference: artifact.reference }),
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// gentle-pi#311 P4-roles: the strict acknowledgement for one executed
|
|
2109
|
+
// provider role vector. The immutable verdict bytes live in the Go-owned
|
|
2110
|
+
// compact store slot; this envelope only names the binding the capture
|
|
2111
|
+
// proved, so anything beyond that exact shape is refused.
|
|
2112
|
+
function decodeNativeProviderRoleCaptureArtifact(value: unknown): NativeReviewProviderRoleCaptureArtifact {
|
|
2113
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("native provider role capture artifact must be an object");
|
|
1814
2114
|
const body = value as Record<string, unknown>;
|
|
2115
|
+
const allowed = new Set(["schema", "lineage_id", "target_identity", "role", "captured"]);
|
|
2116
|
+
for (const key of Object.keys(body)) if (!allowed.has(key)) throw new TypeError(`native provider role capture artifact carries unexpected key ${key}`);
|
|
1815
2117
|
const text = (key: string): string => {
|
|
1816
2118
|
const found = body[key];
|
|
1817
|
-
if (typeof found !== "string" || found.trim() !== found || found.length === 0) throw new TypeError(`native capture
|
|
2119
|
+
if (typeof found !== "string" || found.trim() !== found || found.length === 0) throw new TypeError(`native provider role capture artifact ${key} must be a non-empty trimmed string`);
|
|
1818
2120
|
return found;
|
|
1819
2121
|
};
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
if (
|
|
1824
|
-
// Exactly one locator: a provider-owned path OR an opaque reference. Both or
|
|
1825
|
-
// neither means the manifest cannot be handed to FINALIZE.
|
|
1826
|
-
const hasPath = body.path !== undefined, hasReference = body.reference !== undefined;
|
|
1827
|
-
if (hasPath === hasReference) throw new TypeError("native capture-result manifest must carry exactly one of path or reference");
|
|
2122
|
+
if (text("schema") !== NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA) throw new TypeError(`native provider role capture artifact schema must be ${NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA}`);
|
|
2123
|
+
const role = text("role");
|
|
2124
|
+
if (role !== "refuter" && role !== "targeted-validator") throw new TypeError(`native provider role capture artifact role must be refuter or targeted-validator, received ${role}`);
|
|
2125
|
+
if (body.captured !== true) throw new TypeError("native provider role capture artifact must report captured: true");
|
|
1828
2126
|
return Object.freeze({
|
|
1829
|
-
schema,
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2127
|
+
schema: NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA,
|
|
2128
|
+
lineageId: text("lineage_id"),
|
|
2129
|
+
targetIdentity: text("target_identity"),
|
|
2130
|
+
role,
|
|
2131
|
+
captured: true,
|
|
1834
2132
|
});
|
|
1835
2133
|
}
|
|
1836
2134
|
|
|
@@ -1846,7 +2144,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1846
2144
|
adapter: ExecFileAdapter,
|
|
1847
2145
|
executable: string | (() => string) = resolveGentleAiBinary,
|
|
1848
2146
|
timeoutMs = 30_000,
|
|
1849
|
-
maxBufferBytes =
|
|
2147
|
+
maxBufferBytes = resolveNativeReviewMaxBufferBytes(),
|
|
1850
2148
|
cleanupDirectory: (directory: string) => Promise<void> = (directory) => rm(directory, { recursive: true, force: true }),
|
|
1851
2149
|
executableDigest: NativeExecutableDigestResolver = defaultExecutableDigest,
|
|
1852
2150
|
) {
|
|
@@ -1896,8 +2194,8 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1896
2194
|
if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, operation, mutating, "native process was cancelled");
|
|
1897
2195
|
throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, operation, mutating, "native process could not start");
|
|
1898
2196
|
}
|
|
2197
|
+
if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result, true, undefined, this.maxBufferBytes);
|
|
1899
2198
|
if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
|
|
1900
|
-
if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result);
|
|
1901
2199
|
if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
|
|
1902
2200
|
const diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.NON_ZERO, result);
|
|
1903
2201
|
const body = parseJson(result.stdout, operation, mutating, diagnostics);
|
|
@@ -1909,7 +2207,8 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1909
2207
|
throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native negotiated operation failed without a valid failure envelope", result);
|
|
1910
2208
|
}
|
|
1911
2209
|
}
|
|
1912
|
-
|
|
2210
|
+
const forecastNarrationTolerated = operation === NATIVE_REVIEW_OPERATION.STATUS && gentleAiDevBinaryOverrideConfigured() && stderrIsForecastNarration(result.stderr);
|
|
2211
|
+
if (result.stderr.trim().length > 0 && !stderrIsTolerated(result.stderr, toleratedStderr) && !forecastNarrationTolerated) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
|
|
1913
2212
|
return { body, exitCode: result.exitCode };
|
|
1914
2213
|
}
|
|
1915
2214
|
|
|
@@ -1938,7 +2237,12 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1938
2237
|
if (error instanceof NativeReviewCliError) throw error;
|
|
1939
2238
|
throw nativeError(NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.VERSION, false, `expected gentle-ai v${GENTLE_AI_VERSION}; the installed runtime is incompatible — reinstall gentle-pi`);
|
|
1940
2239
|
}
|
|
1941
|
-
|
|
2240
|
+
// Dev-binary override (unpinned field-test mode): the session accepts
|
|
2241
|
+
// the binary's self-reported package version instead of the exact pin.
|
|
2242
|
+
// The full capabilities decode above still applies unchanged, so an
|
|
2243
|
+
// incompatible envelope is refused either way. Pinned mode keeps the
|
|
2244
|
+
// exact equality byte-identical.
|
|
2245
|
+
if (capabilities.packageVersion !== GENTLE_AI_VERSION && !gentleAiDevBinaryOverrideConfigured()) {
|
|
1942
2246
|
throw nativeError(NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.VERSION, false, `expected gentle-ai v${GENTLE_AI_VERSION}, provider reported v${capabilities.packageVersion}`);
|
|
1943
2247
|
}
|
|
1944
2248
|
return capabilities;
|
|
@@ -1994,12 +2298,20 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1994
2298
|
...(request.focus === undefined ? [] : ["--focus", request.focus]),
|
|
1995
2299
|
"--consent", "relay",
|
|
1996
2300
|
], true, request.signal);
|
|
1997
|
-
// A negotiated v2 START may answer
|
|
2301
|
+
// A negotiated v2 START may answer a consent question (action:
|
|
1998
2302
|
// "consent_required") instead of `start/v3` when the provider needs an
|
|
1999
2303
|
// explicit answer it cannot infer. Discriminate before decode and surface
|
|
2000
|
-
// the complete envelope; only the caller can map a human answer.
|
|
2304
|
+
// the complete envelope; only the caller can map a human answer. The
|
|
2305
|
+
// body's own schema string selects the identity-exact decoder: the
|
|
2306
|
+
// pinned 2.2.x line emits consent/v2, gentle-ai >= 2.3.0 (capabilities
|
|
2307
|
+
// v2.1+) emits consent/v3, and any other identity fails closed inside
|
|
2308
|
+
// the v3 decoder's exact identity gate.
|
|
2001
2309
|
if (execution.body.action === "consent_required") {
|
|
2002
|
-
const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () =>
|
|
2310
|
+
const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () => (
|
|
2311
|
+
execution.body.schema === "gentle-ai.review-integration.consent/v2"
|
|
2312
|
+
? decodeReviewConsentV2(execution.body)
|
|
2313
|
+
: decodeReviewConsentV3(execution.body)
|
|
2314
|
+
));
|
|
2003
2315
|
if (consent.targetIdentity !== targetIdentity || consent.projection !== projection) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent target binding mismatch");
|
|
2004
2316
|
throw new NativeReviewConsentRequiredError(consent);
|
|
2005
2317
|
}
|
|
@@ -2036,16 +2348,15 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2036
2348
|
}
|
|
2037
2349
|
|
|
2038
2350
|
async answerConsent(request: NativeReviewConsentAnswerRequest): Promise<NativeReviewConsentAnswerResult> {
|
|
2039
|
-
const
|
|
2040
|
-
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, arguments_, true, request.signal);
|
|
2351
|
+
const invocation = consentInvocationArguments(request);
|
|
2352
|
+
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, invocation.arguments_, true, request.signal);
|
|
2041
2353
|
if (request.answer === NATIVE_REVIEW_CONSENT_ANSWER.DECLINED) {
|
|
2042
2354
|
return decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeDeclinedConsentStart(execution.body, request));
|
|
2043
2355
|
}
|
|
2044
2356
|
const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
|
|
2045
2357
|
const answeredTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
|
|
2046
2358
|
if (answeredTarget !== request.consent.targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer target mismatch");
|
|
2047
|
-
|
|
2048
|
-
if (result.lineageId !== lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer lineage mismatch");
|
|
2359
|
+
if (invocation.lineageId !== undefined && result.lineageId !== invocation.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer lineage mismatch");
|
|
2049
2360
|
return { kind: "started", start: {
|
|
2050
2361
|
lineageId: result.lineageId,
|
|
2051
2362
|
state: result.state as NativeStartResult["state"],
|
|
@@ -2176,6 +2487,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2176
2487
|
"--projection", request.projection ?? "workspace",
|
|
2177
2488
|
...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef]),
|
|
2178
2489
|
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
2490
|
+
...(request.agent === undefined ? [] : ["--agent", request.agent]),
|
|
2179
2491
|
"--next-transition",
|
|
2180
2492
|
], false, request.signal);
|
|
2181
2493
|
assertSupportedNextTransitionOperation(execution.body);
|
|
@@ -2242,6 +2554,118 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2242
2554
|
}
|
|
2243
2555
|
}
|
|
2244
2556
|
|
|
2557
|
+
// gentle-pi#311 P4-roles: executes one provider-rendered self-contained
|
|
2558
|
+
// role capture vector exactly as rendered — one CLI invocation, verbatim
|
|
2559
|
+
// tokens in provider order, in the foreground. Go materializes the role
|
|
2560
|
+
// prompt, spawns its own locked-down pi subprocess, and admits the raw
|
|
2561
|
+
// verdict; Pi never adds, removes, or reorders a single token (not even
|
|
2562
|
+
// --cwd: the vector's --repository-context is authoritative and mutually
|
|
2563
|
+
// exclusive with a path). The central adapter injects the relay handshake
|
|
2564
|
+
// environment on this spawn like on every other gentle-ai invocation.
|
|
2565
|
+
async captureProviderRole(request: NativeReviewProviderRoleCaptureRequest): Promise<NativeReviewProviderRoleCaptureArtifact> {
|
|
2566
|
+
const verb = request.captureOperation.startsWith("review.") ? request.captureOperation.slice("review.".length) : "";
|
|
2567
|
+
if (verb !== "capture-refuter" && verb !== "capture-validation") throw new TypeError(`Native CAPTURE_PROVIDER_ROLE supports only review.capture-refuter and review.capture-validation, received ${JSON.stringify(request.captureOperation)}`);
|
|
2568
|
+
if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_PROVIDER_ROLE requires the provider-rendered argument tokens");
|
|
2569
|
+
if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native CAPTURE_PROVIDER_ROLE argument tokens must all be non-empty strings");
|
|
2570
|
+
const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true);
|
|
2571
|
+
const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, request.cwd, [
|
|
2572
|
+
"review", verb,
|
|
2573
|
+
...request.argumentTokens,
|
|
2574
|
+
], true, request.signal, executable.path);
|
|
2575
|
+
return decode(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true, () => decodeNativeProviderRoleCaptureArtifact(execution.body));
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// gentle-pi#311 P5: executes one provider-rendered `review.finalize`
|
|
2579
|
+
// execute transition exactly as rendered. The tokens come verbatim from
|
|
2580
|
+
// the negotiated next_transition (e.g. `--lineage=<id>
|
|
2581
|
+
// --captured-results=true`); Pi assembles no reviewer, refuter, or
|
|
2582
|
+
// validator documents for this lane — the provider discovers its own
|
|
2583
|
+
// admitted role and lens slots.
|
|
2584
|
+
async finalizeTransition(request: NativeReviewFinalizeTransitionRequest): Promise<NativeFinalizeResult> {
|
|
2585
|
+
if (request.argumentTokens.length === 0) throw new TypeError("Native FINALIZE transition requires the provider-rendered argument tokens");
|
|
2586
|
+
if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native FINALIZE transition argument tokens must all be non-empty strings");
|
|
2587
|
+
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
|
|
2588
|
+
"review", "finalize",
|
|
2589
|
+
...request.argumentTokens,
|
|
2590
|
+
], true, request.signal);
|
|
2591
|
+
return this.decodeFinalizeTransitionExecution(execution);
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// Same misbinding class as capture-evidence (live smoke, 2026-08-16): the
|
|
2595
|
+
// correction PLAN and TARGETED VALIDATION collect slots render `finalize`
|
|
2596
|
+
// submission descriptors whose tokens are self-contained (--contract,
|
|
2597
|
+
// --lineage, --expected-revision, --target, --request-hash,
|
|
2598
|
+
// --repository-context) plus exactly one {{value}} slot. Executing anything
|
|
2599
|
+
// other than those rendered tokens fails the live emitter's committed-
|
|
2600
|
+
// intent reconciliation, so the tokens pass through verbatim with only the
|
|
2601
|
+
// {{value}} substitution: a literal for correction_lines, a staged 0o600
|
|
2602
|
+
// artifact path for a validation document.
|
|
2603
|
+
async finalizeSubmission(request: NativeReviewFinalizeSubmissionRequest): Promise<NativeFinalizeResult> {
|
|
2604
|
+
if (request.argumentTokens.length === 0) throw new TypeError("Native FINALIZE submission requires the provider-rendered argument tokens");
|
|
2605
|
+
if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native FINALIZE submission argument tokens must all be non-empty strings");
|
|
2606
|
+
if ((request.valueLiteral === undefined) === (request.valueDocument === undefined)) throw new TypeError("Native FINALIZE submission takes exactly one of valueLiteral or valueDocument");
|
|
2607
|
+
const valueToken = request.argumentTokens[request.valueSubstitutionLocation];
|
|
2608
|
+
if (valueToken === undefined || !valueToken.includes("{{value}}")) throw new TypeError("Native FINALIZE submission must render exactly one {{value}} slot token");
|
|
2609
|
+
if (request.valueDocument !== undefined && request.valueDocument.length === 0) throw new TypeError("Native FINALIZE submission value document must contain at least one byte");
|
|
2610
|
+
const directory = request.valueDocument === undefined ? undefined : await mkdtemp(join(tmpdir(), "gentle-ai-finalize-submission-"));
|
|
2611
|
+
try {
|
|
2612
|
+
let substituted: string;
|
|
2613
|
+
if (directory !== undefined) {
|
|
2614
|
+
await chmod(directory, 0o700);
|
|
2615
|
+
const valueFile = join(directory, "value.json");
|
|
2616
|
+
await writeFile(valueFile, request.valueDocument!, { encoding: "utf8", mode: 0o600 });
|
|
2617
|
+
await chmod(valueFile, 0o600);
|
|
2618
|
+
substituted = valueFile;
|
|
2619
|
+
} else {
|
|
2620
|
+
substituted = request.valueLiteral!;
|
|
2621
|
+
}
|
|
2622
|
+
const resolved = request.argumentTokens.map((token, index) => index === request.valueSubstitutionLocation ? token.replaceAll("{{value}}", substituted) : token);
|
|
2623
|
+
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
|
|
2624
|
+
"review", "finalize",
|
|
2625
|
+
...resolved,
|
|
2626
|
+
], true, request.signal);
|
|
2627
|
+
return this.decodeFinalizeTransitionExecution(execution);
|
|
2628
|
+
} finally {
|
|
2629
|
+
if (directory !== undefined) await this.cleanupDirectory(directory).catch(() => undefined);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
private decodeFinalizeTransitionExecution(execution: { body: unknown }): NativeFinalizeResult {
|
|
2634
|
+
return decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => {
|
|
2635
|
+
const body = object(execution.body);
|
|
2636
|
+
// A transition rendered with `--contract` answers with the negotiated
|
|
2637
|
+
// operation envelope; one rendered without (the pi runtime form)
|
|
2638
|
+
// answers with the plain `review/finalize` shape. Both are decoded
|
|
2639
|
+
// strictly; nothing else is accepted.
|
|
2640
|
+
if (typeof body.schema === "string") {
|
|
2641
|
+
const envelope = decodeReviewOperationV2(body);
|
|
2642
|
+
if (envelope.operation !== "review.finalize") throw new Error("wrong finalize operation envelope");
|
|
2643
|
+
const result = envelope.result;
|
|
2644
|
+
return {
|
|
2645
|
+
lineageId: requiredString(result.lineage_id),
|
|
2646
|
+
state: requiredString(result.state),
|
|
2647
|
+
action: requiredString(result.action),
|
|
2648
|
+
storeRevision: requiredString(result.store_revision),
|
|
2649
|
+
...(result.validation_request === undefined ? {} : { validationRequest: result.validation_request as Readonly<Record<string, unknown>> }),
|
|
2650
|
+
...(result.escalation === undefined ? {} : { escalation: requiredString(result.escalation) }),
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
const plain = exactObject(body, ["operation", "lineage_id", "state", "action", "store_revision"], ["receipt_path", "validation_request", "escalation"]);
|
|
2654
|
+
if (plain.operation !== "review/finalize") throw new Error("wrong finalize discriminator");
|
|
2655
|
+
const state = requiredString(plain.state);
|
|
2656
|
+
if (!(NATIVE_FINALIZE_STATE as readonly string[]).includes(state)) throw new Error("unknown finalize state");
|
|
2657
|
+
return {
|
|
2658
|
+
lineageId: requiredString(plain.lineage_id),
|
|
2659
|
+
state,
|
|
2660
|
+
action: requiredString(plain.action),
|
|
2661
|
+
storeRevision: requiredString(plain.store_revision),
|
|
2662
|
+
...(plain.receipt_path === undefined ? {} : { receiptPath: requiredString(plain.receipt_path) }),
|
|
2663
|
+
...(plain.validation_request === undefined ? {} : { validationRequest: plain.validation_request as Readonly<Record<string, unknown>> }),
|
|
2664
|
+
...(plain.escalation === undefined ? {} : { escalation: requiredString(plain.escalation) }),
|
|
2665
|
+
};
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2245
2669
|
async captureEvidence(request: NativeReviewCaptureEvidenceRequest): Promise<NativeReviewVerificationEvidenceV2> {
|
|
2246
2670
|
if (!(NATIVE_REVIEW_CAPTURE_OUTCOME as readonly string[]).includes(request.outcome)) throw new TypeError("Native CAPTURE_EVIDENCE outcome must be passed, verification_failed, or procedural_tooling_failed");
|
|
2247
2671
|
if (request.evidenceDocument.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE evidence must contain at least one byte");
|
|
@@ -2263,6 +2687,45 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2263
2687
|
}
|
|
2264
2688
|
}
|
|
2265
2689
|
|
|
2690
|
+
// Field defect (fambig, 2026-08-16): at every evidence-pending sub-state
|
|
2691
|
+
// the collect slot renders the identity native demands — for a correction
|
|
2692
|
+
// that is the fix-diff `--target`, not the live workspace snapshot — so the
|
|
2693
|
+
// slot's rendered submission tokens execute verbatim, with only the
|
|
2694
|
+
// {{outcome}} and {{input}} slots substituted. Same verbatim-token
|
|
2695
|
+
// discipline as captureResult; --repository-context is authoritative and
|
|
2696
|
+
// mutually exclusive with a path.
|
|
2697
|
+
async captureEvidenceSubmission(request: NativeReviewCaptureEvidenceSubmissionRequest): Promise<NativeReviewVerificationEvidenceV2> {
|
|
2698
|
+
if (!(NATIVE_REVIEW_CAPTURE_OUTCOME as readonly string[]).includes(request.outcome)) throw new TypeError("Native CAPTURE_EVIDENCE outcome must be passed, verification_failed, or procedural_tooling_failed");
|
|
2699
|
+
if (request.evidenceDocument.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE evidence must contain at least one byte");
|
|
2700
|
+
if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE submission requires the provider-rendered argument tokens");
|
|
2701
|
+
if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native CAPTURE_EVIDENCE submission argument tokens must all be non-empty strings");
|
|
2702
|
+
const outcomeToken = request.argumentTokens[request.outcomeSubstitutionLocation];
|
|
2703
|
+
const inputToken = request.argumentTokens[request.inputSubstitutionLocation];
|
|
2704
|
+
if (request.outcomeSubstitutionLocation === request.inputSubstitutionLocation || outcomeToken === undefined || !outcomeToken.includes("{{outcome}}")) throw new TypeError("Native CAPTURE_EVIDENCE submission must render exactly one {{outcome}} slot token");
|
|
2705
|
+
if (inputToken === undefined || !inputToken.includes("{{input}}")) throw new TypeError("Native CAPTURE_EVIDENCE submission must render exactly one {{input}} slot token");
|
|
2706
|
+
const carriesContext = request.argumentTokens.some((token) => token === "--repository-context" || token.startsWith("--repository-context="));
|
|
2707
|
+
if (carriesContext && request.cwd !== undefined) throw new TypeError("Native CAPTURE_EVIDENCE submission takes a repository context or --cwd, never both");
|
|
2708
|
+
const directory = await mkdtemp(join(tmpdir(), "gentle-ai-capture-evidence-"));
|
|
2709
|
+
try {
|
|
2710
|
+
await chmod(directory, 0o700);
|
|
2711
|
+
const evidenceFile = await this.stageEvidence(directory, request.evidenceDocument);
|
|
2712
|
+
const resolved = request.argumentTokens.map((token, index) =>
|
|
2713
|
+
index === request.outcomeSubstitutionLocation
|
|
2714
|
+
? token.replaceAll("{{outcome}}", request.outcome)
|
|
2715
|
+
: index === request.inputSubstitutionLocation
|
|
2716
|
+
? token.replaceAll("{{input}}", evidenceFile)
|
|
2717
|
+
: token);
|
|
2718
|
+
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, request.cwd ?? process.cwd(), [
|
|
2719
|
+
"review", "capture-evidence",
|
|
2720
|
+
...resolved,
|
|
2721
|
+
...(carriesContext || request.cwd === undefined ? [] : ["--cwd", request.cwd]),
|
|
2722
|
+
], true, request.signal);
|
|
2723
|
+
return decode(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, true, () => decodeNativeReviewVerificationEvidence(execution.body));
|
|
2724
|
+
} finally {
|
|
2725
|
+
await this.cleanupDirectory(directory).catch(() => undefined);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2266
2729
|
reviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult> {
|
|
2267
2730
|
return this.legacy.reviewStatus(request);
|
|
2268
2731
|
}
|