gentle-pi 1.2.0 → 2.0.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 +40 -9
- package/assets/agents/review-readability.md +0 -1
- package/assets/agents/review-reliability.md +0 -1
- package/assets/agents/review-resilience.md +0 -1
- package/assets/agents/review-risk.md +0 -1
- package/contracts/review-integration/v1/fixtures/binding-revision-conflict.fixture.json +25 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.2.fixture.json +263 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.3.fixture.json +301 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.4.fixture.json +312 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.5.fixture.json +321 -0
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +36 -0
- package/contracts/review-integration/v1/fixtures/final-verification-incident.fixture.json +1 -0
- package/contracts/review-integration/v1/fixtures/operation.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/repair-preflight.fixture.json +54 -0
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +97 -0
- package/contracts/review-integration/v1/fixtures/start.fixture.json +25 -1
- package/contracts/review-integration/v1/fixtures/status-ambiguous.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-corrupted.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-recover.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-unrelated.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-v2-ambiguous.fixture.json +110 -0
- package/contracts/review-integration/v1/fixtures/status-v2-corrupted.fixture.json +107 -0
- package/contracts/review-integration/v1/fixtures/status-v2-final-verification-retry.fixture.json +192 -0
- package/contracts/review-integration/v1/fixtures/status-v2-recover.fixture.json +130 -0
- package/contracts/review-integration/v1/fixtures/status-v2-repair.fixture.json +161 -0
- package/contracts/review-integration/v1/fixtures/status-v2-unrelated.fixture.json +103 -0
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +193 -0
- package/contracts/review-integration/v1/fixtures/status.fixture.json +19 -5
- package/contracts/review-integration/v1/fixtures/verification-evidence.fixture.json +1 -0
- package/contracts/review-integration/v1/schemas/admitted-result.schema.json +35 -0
- package/contracts/review-integration/v1/schemas/artifact-subject.schema.json +33 -0
- package/contracts/review-integration/v1/schemas/authority-repair-assessment.schema.json +247 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.2.schema.json +249 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.3.schema.json +429 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.4.schema.json +432 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.5.schema.json +154 -0
- package/contracts/review-integration/v1/schemas/consent.schema.json +86 -0
- package/contracts/review-integration/v1/schemas/failure.schema.json +54 -8
- package/contracts/review-integration/v1/schemas/final-verification-incident.schema.json +51 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +204 -31
- package/contracts/review-integration/v1/schemas/repair.schema.json +251 -0
- package/contracts/review-integration/v1/schemas/result-artifact-v2.schema.json +38 -0
- package/contracts/review-integration/v1/schemas/start-v2.schema.json +216 -0
- package/contracts/review-integration/v1/schemas/start.schema.json +79 -3
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +313 -0
- package/contracts/review-integration/v1/schemas/status.schema.json +37 -10
- package/contracts/review-integration/v1/schemas/targeted-validation-request.schema.json +46 -0
- package/contracts/review-integration/v1/schemas/verification-evidence.schema.json +64 -0
- package/contracts/review-integration/v2/fixtures/capabilities.fixture.json +111 -0
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +36 -0
- package/contracts/review-integration/v2/fixtures/start.fixture.json +93 -0
- package/contracts/review-integration/v2/fixtures/status.fixture.json +113 -0
- package/contracts/review-integration/v2/schemas/admitted-result.schema.json +14 -0
- package/contracts/review-integration/v2/schemas/artifact-subject.schema.json +36 -0
- package/contracts/review-integration/v2/schemas/capabilities.schema.json +111 -0
- package/contracts/review-integration/v2/schemas/consent.schema.json +70 -0
- package/contracts/review-integration/v2/schemas/failure.schema.json +35 -0
- package/contracts/review-integration/v2/schemas/operation.schema.json +46 -0
- package/contracts/review-integration/v2/schemas/repair.schema.json +22 -0
- package/contracts/review-integration/v2/schemas/start.schema.json +106 -0
- package/contracts/review-integration/v2/schemas/status.schema.json +104 -0
- package/docs/native-authority-architecture.md +18 -3
- package/docs/review-integration.md +175 -27
- package/extensions/gentle-ai.ts +414 -19
- package/lib/gentle-ai-binary.ts +1 -1
- package/lib/native-review-cli.ts +679 -57
- package/lib/review-candidate-view.ts +131 -1
- package/lib/review-compact-contract.ts +12 -3
- package/lib/review-consent-latch.ts +51 -0
- package/lib/review-correction-lifecycle.ts +170 -0
- package/lib/review-integration-v2.ts +1563 -0
- package/package.json +15 -11
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +663 -41
- package/runtime/review-integration-v2.mjs +1564 -0
- package/scripts/build-git-commit-transaction-runner.mjs +1 -1
- package/scripts/gentle-ai-installer.mjs +16 -11
- package/scripts/install-gentle-ai.mjs +7 -3
- package/scripts/test-packed-runner.mjs +2 -2
- package/scripts/verify-package-files.mjs +216 -64
- package/skills/release/SKILL.md +38 -11
- package/tests/devbinary/native-review-parity.devtest.ts +348 -0
- package/tests/gentle-ai-binary.test.ts +8 -1
- package/tests/gentle-ai-installer.test.ts +24 -21
- package/tests/native-binary-gate.test.ts +145 -0
- package/tests/native-review-capability-contract.test.ts +87 -0
- package/tests/native-review-cli.test.ts +44 -1
- package/tests/native-review-parity-runtime.test.ts +70 -23
- package/tests/native-review-parity.test.ts +673 -0
- package/tests/native-risk-evidence-parity.test.ts +91 -0
- package/tests/package-manifest.test.ts +72 -7
- package/tests/review-actor-tool-deny.test.ts +20 -5
- package/tests/review-candidate-view.test.ts +331 -1
- package/tests/review-compact-contract.test.ts +5 -0
- package/tests/review-consent-latch.test.ts +67 -0
- package/tests/review-controller-native-recovery.test.ts +180 -5
- package/tests/review-controller-native-routing.test.ts +385 -12
- package/tests/review-controller-workspace-root.test.ts +26 -6
- package/tests/review-correction-lifecycle.test.ts +174 -0
- package/tests/review-integration-v2.test.ts +539 -0
- package/tests/runtime-harness.mjs +187 -0
- package/tests/support/native-binary-gate.ts +86 -0
- package/tests/verify-package-files.test.ts +119 -0
- package/lib/review-integration-v1.ts +0 -795
- package/runtime/review-integration-v1.mjs +0 -796
- package/tests/native-review-integration-v1.test.ts +0 -102
- package/tests/review-integration-v1.test.ts +0 -530
package/lib/native-review-cli.ts
CHANGED
|
@@ -5,19 +5,23 @@ 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 { PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
|
|
8
|
+
import { GENTLE_AI_VERSION, PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
|
|
9
9
|
import {
|
|
10
10
|
REVIEW_INTEGRATION_CONTRACT,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
decodeReviewCapabilitiesV2,
|
|
12
|
+
decodeReviewConsentV2,
|
|
13
|
+
decodeReviewFailureV2,
|
|
14
|
+
decodeReviewOperationV2,
|
|
15
|
+
decodeReviewRepairV2,
|
|
16
|
+
decodeReviewStartV3,
|
|
17
|
+
decodeReviewStatusV3,
|
|
18
|
+
type ReviewCapabilitiesV2,
|
|
19
|
+
type ReviewConsentV2,
|
|
20
|
+
type ReviewFailureV2,
|
|
21
|
+
type ReviewRepairV2,
|
|
18
22
|
type ReviewStartState,
|
|
19
|
-
type
|
|
20
|
-
} from "./review-integration-
|
|
23
|
+
type ReviewStatusV3,
|
|
24
|
+
} from "./review-integration-v2.ts";
|
|
21
25
|
|
|
22
26
|
const execFileAsync = promisify(execFile);
|
|
23
27
|
|
|
@@ -35,6 +39,10 @@ export const NATIVE_REVIEW_OPERATION = {
|
|
|
35
39
|
QUARANTINE_LEGACY: "review/quarantine-legacy",
|
|
36
40
|
RECONCILE_AUTHORITY: "review/reconcile-authority",
|
|
37
41
|
REPAIR_LEGACY_ALIAS: "review/repair-legacy-alias",
|
|
42
|
+
MODE: "review/mode",
|
|
43
|
+
REPAIR: "review/repair",
|
|
44
|
+
CAPTURE_EVIDENCE: "review/capture-evidence",
|
|
45
|
+
CAPTURE_RESULT: "review/capture-result",
|
|
38
46
|
} as const;
|
|
39
47
|
export type NativeReviewOperation = (typeof NATIVE_REVIEW_OPERATION)[keyof typeof NATIVE_REVIEW_OPERATION];
|
|
40
48
|
|
|
@@ -52,6 +60,7 @@ export const NATIVE_REVIEW_ERROR_CODE = {
|
|
|
52
60
|
VERSION_INCOMPATIBLE: "version-incompatible",
|
|
53
61
|
CANCELLED: "cancelled",
|
|
54
62
|
PACKAGE_BINARY_MISSING: "package-local-binary-missing",
|
|
63
|
+
UNSUPPORTED_TRANSITION_OPERATION: "unsupported-transition-operation",
|
|
55
64
|
} as const;
|
|
56
65
|
export type NativeReviewErrorCode = (typeof NATIVE_REVIEW_ERROR_CODE)[keyof typeof NATIVE_REVIEW_ERROR_CODE];
|
|
57
66
|
|
|
@@ -95,14 +104,100 @@ export interface NativeReviewCli {
|
|
|
95
104
|
bindSdd(request: NativeBindSddRequest): Promise<NativeBindSddResult>;
|
|
96
105
|
sddStatus(request: NativeSddStatusRequest): Promise<NativeSddStatusResult>;
|
|
97
106
|
reviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult>;
|
|
98
|
-
capabilities?(request?: NativeCapabilitiesRequest): Promise<
|
|
99
|
-
targetStatus?(request: NativeTargetStatusRequest): Promise<
|
|
107
|
+
capabilities?(request?: NativeCapabilitiesRequest): Promise<ReviewCapabilitiesV2>;
|
|
108
|
+
targetStatus?(request: NativeTargetStatusRequest): Promise<ReviewStatusV3>;
|
|
100
109
|
reclaim?(request: NativeReviewReclaimRequest): Promise<NativeReviewRecoveryResult>;
|
|
101
110
|
recover?(request: NativeReviewRecoverRequest): Promise<NativeReviewRecoveryResult>;
|
|
102
111
|
abandon?(request: NativeReviewAbandonRequest): Promise<NativeReviewRecoveryResult>;
|
|
103
112
|
quarantineLegacy?(request: NativeReviewLegacyQuarantineRequest): Promise<NativeReviewRecoveryResult>;
|
|
104
113
|
reconcileAuthority?(request: NativeReviewReconcileAuthorityRequest): Promise<NativeReviewRecoveryResult>;
|
|
105
114
|
repairLegacyAlias?(request: NativeReviewLegacyAliasRepairRequest): Promise<NativeReviewRecoveryResult>;
|
|
115
|
+
// Net-new negotiated operations (contract v2 only): `review.repair` is
|
|
116
|
+
// advertised in the v2 operation list; `capture-evidence` is the
|
|
117
|
+
// evidence-first correction lifecycle's collection step.
|
|
118
|
+
repair?(request: NativeReviewRepairRequest): Promise<ReviewRepairV2>;
|
|
119
|
+
captureEvidence?(request: NativeReviewCaptureEvidenceRequest): Promise<NativeReviewVerificationEvidenceV2>;
|
|
120
|
+
// Dark until a negotiated version reports the `mode` capability true
|
|
121
|
+
// (Design Decision #7, organic-rdd-parity). Plain versioned CLI operation,
|
|
122
|
+
// outside the negotiated review-integration protocol — same shape as
|
|
123
|
+
// reviewStatus/sddStatus/reclaim above.
|
|
124
|
+
reviewMode?(request: NativeReviewModeRequest): Promise<NativeReviewModeResult>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const NATIVE_REVIEW_MODE_OPERATION = {
|
|
128
|
+
STATUS: "status",
|
|
129
|
+
ENABLE: "enable",
|
|
130
|
+
DISABLE: "disable",
|
|
131
|
+
} as const;
|
|
132
|
+
export type NativeReviewModeOperation = (typeof NATIVE_REVIEW_MODE_OPERATION)[keyof typeof NATIVE_REVIEW_MODE_OPERATION];
|
|
133
|
+
|
|
134
|
+
export const NATIVE_REVIEW_MODE_VALUE = {
|
|
135
|
+
UNSET: "",
|
|
136
|
+
ON: "on",
|
|
137
|
+
OFF: "off",
|
|
138
|
+
} as const;
|
|
139
|
+
export type NativeReviewModeValue = (typeof NATIVE_REVIEW_MODE_VALUE)[keyof typeof NATIVE_REVIEW_MODE_VALUE];
|
|
140
|
+
|
|
141
|
+
export const NATIVE_REVIEW_MODE_SOURCE = {
|
|
142
|
+
DEFAULT: "default",
|
|
143
|
+
GLOBAL: "global",
|
|
144
|
+
CLONE_LOCAL: "clone_local",
|
|
145
|
+
} as const;
|
|
146
|
+
export type NativeReviewModeSource = (typeof NATIVE_REVIEW_MODE_SOURCE)[keyof typeof NATIVE_REVIEW_MODE_SOURCE];
|
|
147
|
+
|
|
148
|
+
export const NATIVE_REVIEW_MODE_SCOPE = {
|
|
149
|
+
GLOBAL: "global",
|
|
150
|
+
CLONE: "clone",
|
|
151
|
+
BOTH: "both",
|
|
152
|
+
} as const;
|
|
153
|
+
export type NativeReviewModeScope = (typeof NATIVE_REVIEW_MODE_SCOPE)[keyof typeof NATIVE_REVIEW_MODE_SCOPE];
|
|
154
|
+
|
|
155
|
+
export interface NativeReviewModeRequest {
|
|
156
|
+
cwd: string;
|
|
157
|
+
operation: NativeReviewModeOperation;
|
|
158
|
+
signal?: AbortSignal;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface NativeReviewModeStatus {
|
|
162
|
+
global: NativeReviewModeValue;
|
|
163
|
+
cloneLocal: NativeReviewModeValue;
|
|
164
|
+
effective: "on" | "off";
|
|
165
|
+
source: NativeReviewModeSource;
|
|
166
|
+
revision?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface NativeReviewModeResult {
|
|
170
|
+
operation: NativeReviewModeOperation;
|
|
171
|
+
scope: NativeReviewModeScope;
|
|
172
|
+
status: NativeReviewModeStatus;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Exact-match tolerated-stderr allowlist for START only, gated on the `mode`
|
|
176
|
+
// capability being true (Design Decision #6, organic-rdd-parity). Byte-exact
|
|
177
|
+
// against gentle-ai's headless notice (internal/cli/review_mode.go
|
|
178
|
+
// reviewConsentSkippedNotice) written when the switch is on but no interactive
|
|
179
|
+
// terminal answered the one-time consent question — which is always true when
|
|
180
|
+
// Pi spawns gentle-ai without a TTY. Any other text still fails closed as
|
|
181
|
+
// UNEXPECTED_STDERR.
|
|
182
|
+
// Each entry is one whole line gentle-ai may write to the console stream while
|
|
183
|
+
// still succeeding. The provenance line rides with the skip notice whenever the
|
|
184
|
+
// resolved mode source is `default`, so a headless START legitimately emits two
|
|
185
|
+
// lines; they are separate Fprintln calls, never one joined string.
|
|
186
|
+
export const REVIEW_CONSENT_NOTICES = Object.freeze([
|
|
187
|
+
"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.",
|
|
188
|
+
"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.",
|
|
189
|
+
"Gentle AI could not read an answer, so it reviewed this change and will ask again next time.",
|
|
190
|
+
"Gentle AI did not recognize that answer, so it reviewed this change and will ask again next time.",
|
|
191
|
+
"Review skipped for this candidate at your request. It will be offered again on the next change.",
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
// Every non-empty line must be allowlisted. Matching the whole stderr blob as
|
|
195
|
+
// one string only worked while exactly one notice could appear; the moment a
|
|
196
|
+
// second legitimate line joined it, an otherwise successful START was reported
|
|
197
|
+
// as unexpected-stderr.
|
|
198
|
+
function stderrIsTolerated(stderr: string, tolerated: readonly string[]): boolean {
|
|
199
|
+
const lines = stderr.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
200
|
+
return lines.length > 0 && lines.every((line) => tolerated.includes(line));
|
|
106
201
|
}
|
|
107
202
|
|
|
108
203
|
export const NATIVE_REVIEW_RECOVER_DISPOSITION = ["scope_changed", "invalidated", "escalated"] as const;
|
|
@@ -196,9 +291,88 @@ export interface NativeReviewLegacyAliasRepairRequest {
|
|
|
196
291
|
/** Raw audited native record; Pi relays it verbatim and never reinterprets it. */
|
|
197
292
|
export interface NativeReviewRecoveryResult { record: Record<string, unknown>; }
|
|
198
293
|
|
|
199
|
-
|
|
294
|
+
// Net-new negotiated `review.repair` (contract v2). `repair(request)` always
|
|
295
|
+
// runs a `--mode preflight` first; only an eligible assessment is ever
|
|
296
|
+
// executed, using the exact provider_inputs that assessment published — Pi's
|
|
297
|
+
// own NATIVE_REVIEW_LEGACY_ALIAS_REPAIR constants are never a source, only a
|
|
298
|
+
// disagreement check (Design Decision #6, migrate-review-integration-v2).
|
|
299
|
+
export interface NativeReviewRepairRequest {
|
|
300
|
+
cwd: string;
|
|
301
|
+
actor: string;
|
|
302
|
+
reason: string;
|
|
303
|
+
maintainerAuthorization: string;
|
|
304
|
+
signal?: AbortSignal;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// `review capture-evidence` argv and response shape are pinned to a real
|
|
308
|
+
// v2.2.2 review run (lineage review-b39d803b68a90767): exactly
|
|
309
|
+
// --cwd/--lineage/--target/--expected-revision/--outcome/--input, no
|
|
310
|
+
// --contract, and the `gentle-ai.review-verification-evidence/v2` record
|
|
311
|
+
// returned directly (not wrapped in an operation/v2 envelope).
|
|
312
|
+
export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "procedural_tooling_failed"] as const;
|
|
313
|
+
export type NativeReviewCaptureOutcome = (typeof NATIVE_REVIEW_CAPTURE_OUTCOME)[number];
|
|
314
|
+
|
|
315
|
+
// `capture-result` is an additive headless command, NOT a negotiated
|
|
316
|
+
// repository operation: it accepts no --contract, and the provider's own
|
|
317
|
+
// transition tokens already carry the repository context -- it takes that or
|
|
318
|
+
// --cwd, never both. So Pi passes the tokens through verbatim and adds only
|
|
319
|
+
// --input. Reconstructing them would mean re-deriving a lineage, revision,
|
|
320
|
+
// target, lens slot, and subject hash the provider already issued.
|
|
321
|
+
// Added for the v2 finalize transport. `capturedResults` asks the provider to
|
|
322
|
+
// discover every manifest it already admitted; `resultArtifactFiles` hands them
|
|
323
|
+
// over explicitly in lens order. Both replace the retired `--result`.
|
|
324
|
+
export interface NativeReviewFinalizeCapturedResults {
|
|
325
|
+
readonly capturedResults?: boolean;
|
|
326
|
+
readonly resultArtifactFiles?: readonly string[];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export interface NativeReviewCaptureResultRequest {
|
|
330
|
+
readonly argumentTokens: readonly string[];
|
|
331
|
+
readonly resultDocument: string;
|
|
332
|
+
// Only for the compatibility path-manifest mode, when the tokens carry no
|
|
333
|
+
// repository context. Supplying both is refused by the provider.
|
|
334
|
+
readonly cwd?: string;
|
|
335
|
+
readonly signal?: AbortSignal;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export interface NativeReviewAdmittedResultManifest {
|
|
339
|
+
readonly schema: string;
|
|
340
|
+
readonly subjectHash: string;
|
|
341
|
+
readonly admissionDecision: string;
|
|
342
|
+
readonly lens?: string;
|
|
343
|
+
readonly path?: string;
|
|
344
|
+
readonly reference?: string;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface NativeReviewCaptureEvidenceRequest {
|
|
348
|
+
cwd: string;
|
|
349
|
+
lineageId: string;
|
|
350
|
+
targetIdentity: string;
|
|
351
|
+
expectedRevision: string;
|
|
352
|
+
outcome: NativeReviewCaptureOutcome;
|
|
353
|
+
evidenceDocument: string;
|
|
354
|
+
signal?: AbortSignal;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface NativeReviewVerificationEvidenceV2 {
|
|
358
|
+
schema: "gentle-ai.review-verification-evidence/v2";
|
|
359
|
+
version: 2;
|
|
360
|
+
lineageId: string;
|
|
361
|
+
authorityRevision: string;
|
|
362
|
+
targetIdentity: string;
|
|
363
|
+
candidateTree: string;
|
|
364
|
+
pathsDigest: string;
|
|
365
|
+
paths: readonly string[];
|
|
366
|
+
ledgerIds: readonly string[];
|
|
367
|
+
rawPayloadSha256: string;
|
|
368
|
+
rawPayloadBytes: number;
|
|
369
|
+
outcome: NativeReviewCaptureOutcome;
|
|
370
|
+
recordDigest: string;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export interface NativeStartRequest { cwd: string; baseRef?: string; committedOnly?: boolean; lineageId?: string; policyPath?: string; focus?: string; projection?: "workspace" | "staged"; signal?: AbortSignal; }
|
|
200
374
|
export interface NativeFinalizeLensResult { lens: string; document: unknown; }
|
|
201
|
-
export interface NativeFinalizeRequest {
|
|
375
|
+
export interface NativeFinalizeRequest extends NativeReviewFinalizeCapturedResults {
|
|
202
376
|
cwd: string;
|
|
203
377
|
lineageId?: string;
|
|
204
378
|
resultFiles?: readonly string[];
|
|
@@ -318,9 +492,9 @@ export interface NativeReviewStatusResult {
|
|
|
318
492
|
}
|
|
319
493
|
export const NATIVE_START_ACTION = { CREATED: "created", RESUMED: "resumed", REUSE_RECEIPT: "reuse-receipt", BLOCKED_SCOPE_ACTION: "blocked-scope-action" } as const;
|
|
320
494
|
export type NativeStartAction = (typeof NATIVE_START_ACTION)[keyof typeof NATIVE_START_ACTION];
|
|
321
|
-
export interface NativeStartResult { lineageId: string; state: ReviewStartState; riskLevel: string; selectedLenses: readonly string[]; changedFiles: number; changedLines: number; correctionBudget: number; action: NativeStartAction; lensesRequired: boolean; riskReasons?: readonly Record<string, unknown>[]; raw?: Readonly<Record<string, unknown>>; }
|
|
322
|
-
export interface NativeValidateResult { allowed: boolean; result: "allow" | "scope-changed" | "invalidated" | "escalated"; action: string; reason: string; gateContext: NativeGateContext; }
|
|
323
|
-
export interface NativeFinalizeResult { lineageId: string; state: string; action: string; storeRevision: string; receiptPath?: string; }
|
|
495
|
+
export interface NativeStartResult { lineageId: string; state: ReviewStartState; riskLevel: string; selectedLenses: readonly string[]; changedFiles: number; changedLines: number; correctionBudget: number; action: NativeStartAction; lensesRequired: boolean; riskReasons?: readonly Record<string, unknown>[]; raw?: Readonly<Record<string, unknown>>; riskEvidence?: readonly string[]; hint?: string; }
|
|
496
|
+
export interface NativeValidateResult { allowed: boolean; result: "allow" | "scope-changed" | "invalidated" | "escalated"; action: string; reason: string; gateContext: NativeGateContext; delivery?: "disabled/unmanaged"; }
|
|
497
|
+
export interface NativeFinalizeResult { lineageId: string; state: string; action: string; storeRevision: string; receiptPath?: string; validationRequest?: Readonly<Record<string, unknown>>; escalation?: string; }
|
|
324
498
|
export interface NativeBindSddResult {
|
|
325
499
|
revision: string;
|
|
326
500
|
change: string;
|
|
@@ -342,6 +516,76 @@ export function isCanonicalProcessString(value: unknown): value is string {
|
|
|
342
516
|
}
|
|
343
517
|
|
|
344
518
|
const NATIVE_RISK_LEVEL = ["low", "medium", "high"] as const;
|
|
519
|
+
|
|
520
|
+
// gentle-ai's negotiated `start/v2` envelope is a closed schema
|
|
521
|
+
// (`additionalProperties: false`), so the two projections its plain sibling
|
|
522
|
+
// carries cannot be added to it without a new contract version. Both are
|
|
523
|
+
// projections of facts `start/v2` already reports as required fields, so a
|
|
524
|
+
// negotiated caller reconstructs them here instead of ending up with a worse
|
|
525
|
+
// recovery story than a plain one. These are Pi-side renderings of native
|
|
526
|
+
// facts, never a claim that the CLI sent them: the `riskEvidence`/`hint`
|
|
527
|
+
// capability rows stay dark for every version whose envelope omits them.
|
|
528
|
+
//
|
|
529
|
+
// Byte-for-byte mirrors of internal/cli/review_mode.go and review_facade.go.
|
|
530
|
+
// `reviewConsentEvidencePhrases` is documented there as the single phrasing
|
|
531
|
+
// source so its surfaces cannot drift, which makes this a second surface: an
|
|
532
|
+
// unrecognized reason code therefore renders nothing rather than guessing, and
|
|
533
|
+
// nativeRiskEvidencePhrases is pinned against a gentle-ai fixture in
|
|
534
|
+
// tests/native-review-parity.test.ts so a vocabulary change fails loudly.
|
|
535
|
+
const REVIEW_EMPTY_CANDIDATE_HINT =
|
|
536
|
+
"the candidate has no pending changes; already-committed work can be reviewed by rerunning review start with --base-ref <commit> naming the base to compare against";
|
|
537
|
+
const REVIEW_MEDIUM_RISK_REASON = "this change is not purely passive documentation, so it gets one consolidated review.";
|
|
538
|
+
const REVIEW_EMPTY_CONTENT_CODE = "empty_content";
|
|
539
|
+
const REVIEW_RISK_SUBJECT_BY_CODE: Readonly<Record<string, string>> = Object.freeze({
|
|
540
|
+
service_token: "service credentials",
|
|
541
|
+
shell_source: "shell scripting",
|
|
542
|
+
process_boundary: "code that starts other processes",
|
|
543
|
+
process_scan_limit: "code that starts other processes",
|
|
544
|
+
executable_mode: "an executable permission change",
|
|
545
|
+
executable_change: "an executable change",
|
|
546
|
+
configuration_change: "a configuration change",
|
|
547
|
+
});
|
|
548
|
+
const REVIEW_RISK_SUBJECT_BY_SIGNAL: Readonly<Record<string, string>> = Object.freeze({
|
|
549
|
+
auth: "authentication",
|
|
550
|
+
update: "the update path",
|
|
551
|
+
security: "security",
|
|
552
|
+
payments: "payments",
|
|
553
|
+
data_exposure: "data exposure",
|
|
554
|
+
data_loss: "data loss",
|
|
555
|
+
permissions: "permissions",
|
|
556
|
+
shell_process: "shell or process execution",
|
|
557
|
+
});
|
|
558
|
+
// The Go signal switch has no empty default: every hot_path reason speaks, and
|
|
559
|
+
// an unmapped signal degrades to this rather than dropping the path entirely.
|
|
560
|
+
const REVIEW_RISK_UNKNOWN_SIGNAL_SUBJECT = "a sensitive area";
|
|
561
|
+
|
|
562
|
+
function nativeRiskEvidenceSubject(reason: Record<string, unknown>): string {
|
|
563
|
+
const code = typeof reason.code === "string" ? reason.code : "";
|
|
564
|
+
if (code === "hot_path") {
|
|
565
|
+
const signal = typeof reason.signal === "string" ? reason.signal : "";
|
|
566
|
+
return REVIEW_RISK_SUBJECT_BY_SIGNAL[signal] ?? REVIEW_RISK_UNKNOWN_SIGNAL_SUBJECT;
|
|
567
|
+
}
|
|
568
|
+
return REVIEW_RISK_SUBJECT_BY_CODE[code] ?? "";
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function nativeRiskEvidencePhrase(reason: Record<string, unknown>): string {
|
|
572
|
+
const path = typeof reason.path === "string" ? reason.path.trim() : "";
|
|
573
|
+
// An empty file is named first and described second. Every other subject
|
|
574
|
+
// reads "<what changed> in <path>", which for a file with no bytes would
|
|
575
|
+
// assert something about content that is not there.
|
|
576
|
+
if (reason.code === REVIEW_EMPTY_CONTENT_CODE) {
|
|
577
|
+
return path === "" ? "" : `${path}, an empty file whose type cannot be determined from its content`;
|
|
578
|
+
}
|
|
579
|
+
const subject = nativeRiskEvidenceSubject(reason);
|
|
580
|
+
if (subject === "" || path === "") return subject;
|
|
581
|
+
return `${subject} in ${path}`;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function nativeRiskEvidencePhrases(riskLevel: string, reasons: readonly Record<string, unknown>[]): readonly string[] {
|
|
585
|
+
if (riskLevel !== "high" && riskLevel !== "medium") return [];
|
|
586
|
+
const phrases = reasons.map((reason) => nativeRiskEvidencePhrase(reason)).filter((phrase) => phrase !== "");
|
|
587
|
+
return riskLevel === "medium" ? [REVIEW_MEDIUM_RISK_REASON, ...phrases] : phrases;
|
|
588
|
+
}
|
|
345
589
|
const NATIVE_REVIEW_LENS = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
|
|
346
590
|
const NATIVE_FINALIZE_STATE = ["reviewing", "correction_required", "validating", "approved", "escalated"] as const;
|
|
347
591
|
const NATIVE_START_ACTION_VALUES = Object.values(NATIVE_START_ACTION);
|
|
@@ -350,18 +594,71 @@ const NATIVE_GATE = ["post-apply", "pre-commit", "pre-push", "pre-pr", "release"
|
|
|
350
594
|
const NATIVE_SDD_NEXT_ACTION = ["apply", "verify", "remediate", "archive", "review", "resolve-review", "resolve-blockers", "sdd-new", "select-change", "propose", "spec", "design", "tasks"] as const;
|
|
351
595
|
const NATIVE_SDD_POST_REVIEW_ACTION = ["verify", "archive"] as const;
|
|
352
596
|
|
|
597
|
+
// Organic-parity columns (mode, riskEvidence, hint, delivery) stay false on
|
|
598
|
+
// every shipped row, including "2.1.11". PI-2 adds the first capability-true
|
|
599
|
+
// row (and bumps the triple pin) in one dedicated commit — never one without
|
|
600
|
+
// the other. See Design Decision #1 (organic-rdd-parity).
|
|
601
|
+
const ORGANIC_PARITY_DARK = { mode: false, riskEvidence: false, hint: false, delivery: false } as const;
|
|
602
|
+
|
|
353
603
|
export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
354
|
-
"2.1.4": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: false, inventory: false, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false }),
|
|
355
|
-
"2.1.5": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false }),
|
|
356
|
-
"2.1.6": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false }),
|
|
357
|
-
"2.1.7": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false }),
|
|
358
|
-
"2.1.8": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: false, quarantineLegacy: false, reconcileAuthority: true, repairLegacyAlias: false }),
|
|
359
|
-
"2.1.9": 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: false }),
|
|
360
|
-
"2.1.10": 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 }),
|
|
361
|
-
"2.1.11": 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 }),
|
|
604
|
+
"2.1.4": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: false, inventory: false, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
|
|
605
|
+
"2.1.5": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
|
|
606
|
+
"2.1.6": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
|
|
607
|
+
"2.1.7": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
|
|
608
|
+
"2.1.8": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: false, quarantineLegacy: false, reconcileAuthority: true, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
|
|
609
|
+
"2.1.9": 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: false, ...ORGANIC_PARITY_DARK }),
|
|
610
|
+
"2.1.10": 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, ...ORGANIC_PARITY_DARK }),
|
|
611
|
+
"2.1.11": 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, ...ORGANIC_PARITY_DARK }),
|
|
612
|
+
// First capability-true row, paired with the triple pin bump in this same
|
|
613
|
+
// commit as Design Decision #1 requires.
|
|
614
|
+
//
|
|
615
|
+
// Only two of the four organic-parity columns are lit, because a capability
|
|
616
|
+
// row is a promise and these are the two whose data was proven to reach the
|
|
617
|
+
// negotiated path Pi actually consumes:
|
|
618
|
+
//
|
|
619
|
+
// mode `gentle-ai review mode status` answers the review-mode/v1
|
|
620
|
+
// envelope directly.
|
|
621
|
+
// delivery the gate result carries `delivery` ("disabled/unmanaged" when
|
|
622
|
+
// the kill switch is off), verified against v2.2.0.
|
|
623
|
+
//
|
|
624
|
+
// riskEvidence and hint stay dark deliberately. Both exist in gentle-ai
|
|
625
|
+
// v2.2.0 but only on the PLAIN start envelope; the negotiated
|
|
626
|
+
// `review-integration.start/v2` that NativeReviewCliV216 decodes carries
|
|
627
|
+
// `risk_reasons` instead of `risk_evidence` and omits `hint` entirely.
|
|
628
|
+
// Lighting them would advertise data that cannot arrive. Closing that gap
|
|
629
|
+
// needs the negotiated start envelope extended upstream, which moves a
|
|
630
|
+
// byte-pinned fixture and therefore belongs to a gentle-ai release, not to
|
|
631
|
+
// a Pi capability flip.
|
|
632
|
+
"2.2.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 }),
|
|
633
|
+
// 2.2.1 repeats 2.2.0 because the wire did not move for the lane Pi speaks.
|
|
634
|
+
// v2.2.1 advertises capabilities/v1.5 (protocol minor 5) on
|
|
635
|
+
// review-integration/v1, but the negotiated start envelope is still the
|
|
636
|
+
// closed `start/v2`, so riskEvidence and hint stay dark for the same reason
|
|
637
|
+
// they are dark on 2.2.0. The release does publish a second contract,
|
|
638
|
+
// review-integration/v2, whose `start/v3` carries base/candidate trees --
|
|
639
|
+
// but Pi does not negotiate it yet, and a row must describe the lane in use.
|
|
640
|
+
"2.2.1": 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 }),
|
|
641
|
+
// 2.2.2 repeats 2.2.1 for the same reason, confirmed against the released
|
|
642
|
+
// v2.2.2 binary rather than assumed: on review-integration/v1 it still
|
|
643
|
+
// advertises capabilities/v1.5 and the negotiated start envelope is still
|
|
644
|
+
// the closed `start/v2`, so riskEvidence and hint still cannot arrive.
|
|
645
|
+
"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 }),
|
|
362
646
|
});
|
|
363
647
|
type NativeCliCapability = keyof (typeof NATIVE_CLI_CONTRACTS)[keyof typeof NATIVE_CLI_CONTRACTS];
|
|
364
648
|
|
|
649
|
+
// Testing-only capability overlay: lets tests exercise mode/riskEvidence/hint/
|
|
650
|
+
// delivery decode paths under a synthetic version key without ever making a
|
|
651
|
+
// shipped row (including "2.1.11") capability-true. Production code never
|
|
652
|
+
// calls the setter, so this is inert outside test files.
|
|
653
|
+
const nativeCliContractsTestingOverlay = new Map<string, Record<NativeCliCapability, boolean>>();
|
|
654
|
+
export function setNativeCliContractForTesting(version: string, contract: Record<NativeCliCapability, boolean> | undefined): void {
|
|
655
|
+
if (contract === undefined) nativeCliContractsTestingOverlay.delete(version);
|
|
656
|
+
else nativeCliContractsTestingOverlay.set(version, contract);
|
|
657
|
+
}
|
|
658
|
+
function resolvedNativeCliContract(version: string): Record<NativeCliCapability, boolean> | undefined {
|
|
659
|
+
return nativeCliContractsTestingOverlay.get(version) ?? (NATIVE_CLI_CONTRACTS as Record<string, Record<NativeCliCapability, boolean> | undefined>)[version];
|
|
660
|
+
}
|
|
661
|
+
|
|
365
662
|
export interface NativeReviewStructuredDenial {
|
|
366
663
|
schema: "gentle-ai.review-gate-result/v1";
|
|
367
664
|
result: "scope-changed" | "invalidated" | "escalated";
|
|
@@ -536,6 +833,49 @@ function decodeLegacyReconcileAudit(value: unknown): NativeReviewRecoveryResult
|
|
|
536
833
|
for (const field of ["predecessor_lineage", "successor_lineage", "outcome"]) requiredString(record[field]);
|
|
537
834
|
return { record };
|
|
538
835
|
}
|
|
836
|
+
function decodeNativeReviewVerificationEvidence(value: unknown): NativeReviewVerificationEvidenceV2 {
|
|
837
|
+
const body = exactObject(value, ["schema", "version", "lineage_id", "authority_revision", "target_identity", "candidate_tree", "paths_digest", "paths", "ledger_ids", "raw_payload_sha256", "raw_payload_bytes", "outcome", "record_digest"]);
|
|
838
|
+
if (body.schema !== "gentle-ai.review-verification-evidence/v2") throw new Error("wrong verification evidence schema");
|
|
839
|
+
return {
|
|
840
|
+
schema: "gentle-ai.review-verification-evidence/v2",
|
|
841
|
+
// verification-evidence.schema.json pins `version` to {"const": 2} -- the
|
|
842
|
+
// NUMBER two, not the string. It was written as a string from a handoff
|
|
843
|
+
// that listed the observed field NAMES without their types.
|
|
844
|
+
version: (() => { if (body.version !== 2) throw new Error(`native verification evidence version must be the number 2, received ${JSON.stringify(body.version)}`); return 2 as const; })(),
|
|
845
|
+
lineageId: requiredString(body.lineage_id),
|
|
846
|
+
authorityRevision: requiredString(body.authority_revision),
|
|
847
|
+
targetIdentity: requiredString(body.target_identity),
|
|
848
|
+
candidateTree: requiredString(body.candidate_tree),
|
|
849
|
+
pathsDigest: requiredString(body.paths_digest),
|
|
850
|
+
paths: stringArray(body.paths),
|
|
851
|
+
// The schema requires ledger_ids to be an array, but v2.2.2 emits null when
|
|
852
|
+
// there are none. Tolerated deliberately: refusing here would reject a
|
|
853
|
+
// response the provider actually sends, and the provider violating its own
|
|
854
|
+
// published schema is its defect to fix, not a reason to break the client.
|
|
855
|
+
ledgerIds: body.ledger_ids === null || body.ledger_ids === undefined ? [] : stringArray(body.ledger_ids),
|
|
856
|
+
rawPayloadSha256: requiredString(body.raw_payload_sha256),
|
|
857
|
+
rawPayloadBytes: nonNegativeInteger(body.raw_payload_bytes),
|
|
858
|
+
outcome: enumString(body.outcome, NATIVE_REVIEW_CAPTURE_OUTCOME) as NativeReviewCaptureOutcome,
|
|
859
|
+
recordDigest: requiredString(body.record_digest),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
// Unimplemented next_transition.execute.operation values must never reach
|
|
863
|
+
// argv synthesis. Checked against the raw pre-decode body so an operation
|
|
864
|
+
// gentle-pi does not implement (e.g. a future `dispose-result`) fails with a
|
|
865
|
+
// named, typed refusal instead of a generic schema-incompatible error, and
|
|
866
|
+
// before any client ever tries to build an invocation for it (Design
|
|
867
|
+
// Decision #6, migrate-review-integration-v2).
|
|
868
|
+
const NATIVE_REVIEW_SUPPORTED_TRANSITION_OPERATIONS = new Set(["review.start", "review.finalize", "review.recover", "review.repair", "review.validate"]);
|
|
869
|
+
function assertSupportedNextTransitionOperation(body: Record<string, unknown>): void {
|
|
870
|
+
const nextTransition = body.next_transition;
|
|
871
|
+
if (typeof nextTransition !== "object" || nextTransition === null || Array.isArray(nextTransition)) return;
|
|
872
|
+
const execute = (nextTransition as Record<string, unknown>).execute;
|
|
873
|
+
if (typeof execute !== "object" || execute === null || Array.isArray(execute)) return;
|
|
874
|
+
const operation = (execute as Record<string, unknown>).operation;
|
|
875
|
+
if (typeof operation === "string" && !NATIVE_REVIEW_SUPPORTED_TRANSITION_OPERATIONS.has(operation)) {
|
|
876
|
+
throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNSUPPORTED_TRANSITION_OPERATION, NATIVE_REVIEW_OPERATION.STATUS, false, `unsupported-transition-operation: gentle-pi does not implement the next_transition operation "${operation}"; refusing rather than synthesizing an invocation for it`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
539
879
|
function decode<T>(operation: NativeReviewOperation, mutating: boolean, callback: () => T, diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE)): T {
|
|
540
880
|
try { return callback(); } catch (error) { if (error instanceof NativeReviewCliError) throw error; throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE, operation, true, mutating, "native response is schema incompatible", { ...diagnostics, error_code: NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE }); }
|
|
541
881
|
}
|
|
@@ -634,6 +974,28 @@ function decodeNativeReviewStatusDiagnostic(value: unknown): NativeReviewAuthori
|
|
|
634
974
|
const diagnostic = exactObject(value, ["path", "problem"]);
|
|
635
975
|
return { path: requiredString(diagnostic.path), problem: requiredString(diagnostic.problem) };
|
|
636
976
|
}
|
|
977
|
+
function decodeNativeReviewModeStatus(value: unknown): NativeReviewModeStatus {
|
|
978
|
+
const status = exactObject(value, ["schema", "global", "clone_local", "effective", "source"], ["revision"]);
|
|
979
|
+
if (status.schema !== "gentle-ai.rdd-mode-status/v1") throw new Error("wrong review mode status schema");
|
|
980
|
+
return {
|
|
981
|
+
global: enumString(status.global, Object.values(NATIVE_REVIEW_MODE_VALUE)) as NativeReviewModeValue,
|
|
982
|
+
cloneLocal: enumString(status.clone_local, Object.values(NATIVE_REVIEW_MODE_VALUE)) as NativeReviewModeValue,
|
|
983
|
+
effective: enumString(status.effective, ["on", "off"]) as "on" | "off",
|
|
984
|
+
source: enumString(status.source, Object.values(NATIVE_REVIEW_MODE_SOURCE)) as NativeReviewModeSource,
|
|
985
|
+
...(status.revision === undefined ? {} : { revision: requiredString(status.revision) }),
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function decodeNativeReviewMode(value: unknown, expectedOperation: NativeReviewModeOperation): NativeReviewModeResult {
|
|
990
|
+
const body = exactObject(value, ["schema", "operation", "scope", "status"]);
|
|
991
|
+
if (body.schema !== "gentle-ai.review-mode/v1" || body.operation !== expectedOperation) throw new Error("wrong review mode discriminator");
|
|
992
|
+
return {
|
|
993
|
+
operation: expectedOperation,
|
|
994
|
+
scope: enumString(body.scope, Object.values(NATIVE_REVIEW_MODE_SCOPE)) as NativeReviewModeScope,
|
|
995
|
+
status: decodeNativeReviewModeStatus(body.status),
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
637
999
|
function decodeNativeReviewStatus(value: unknown): NativeReviewStatusResult {
|
|
638
1000
|
const body = exactObject(value, ["schema", "operation", "repository", "complete", "authoritative", "status", "entries", "locks", "diagnostics"]);
|
|
639
1001
|
if (body.schema !== "gentle-ai.review-authority-status/v1" || body.operation !== "review/status") throw new Error("wrong review status discriminator");
|
|
@@ -790,7 +1152,12 @@ export class NativeReviewCliV214 {
|
|
|
790
1152
|
}
|
|
791
1153
|
}
|
|
792
1154
|
|
|
793
|
-
|
|
1155
|
+
// toleratedStderr is an exact-match, operation-scoped allowlist (Design
|
|
1156
|
+
// Decision #6, organic-rdd-parity): only START passes a non-empty list, and
|
|
1157
|
+
// only when the negotiated version's `mode` capability is true. A near-miss,
|
|
1158
|
+
// prefixed, or multi-line stderr is never tolerated — only byte-exact
|
|
1159
|
+
// membership in the frozen set.
|
|
1160
|
+
private async execute(operation: NativeReviewOperation, cwd: string, arguments_: readonly string[], mutating: boolean, signal?: AbortSignal, toleratedStderr: readonly string[] = []): Promise<NativeJsonExecution> {
|
|
794
1161
|
let result: ExecFileResult;
|
|
795
1162
|
try { result = await this.adapter({ file: this.executablePath(operation, mutating), arguments: arguments_, cwd, timeoutMs: mutating ? undefined : this.timeoutMs, maxBufferBytes: this.maxBufferBytes, signal }); }
|
|
796
1163
|
catch (error) {
|
|
@@ -804,8 +1171,9 @@ export class NativeReviewCliV214 {
|
|
|
804
1171
|
if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
|
|
805
1172
|
const structuredValidateDenial = operation === NATIVE_REVIEW_OPERATION.VALIDATE && result.exitCode === 1;
|
|
806
1173
|
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;
|
|
1174
|
+
const toleratedNotice = stderrIsTolerated(result.stderr, toleratedStderr);
|
|
807
1175
|
if (result.exitCode !== 0 && !structuredValidateDenial && !maintenancePartialFailure) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native process failed", result);
|
|
808
|
-
if (result.stderr.trim().length > 0 && !structuredValidateDenial && !maintenancePartialFailure) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
|
|
1176
|
+
if (result.stderr.trim().length > 0 && !structuredValidateDenial && !maintenancePartialFailure && !toleratedNotice) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
|
|
809
1177
|
return { body: parseJson(result.stdout, operation, mutating, diagnostics), exitCode: result.exitCode, process: result };
|
|
810
1178
|
}
|
|
811
1179
|
|
|
@@ -822,8 +1190,8 @@ export class NativeReviewCliV214 {
|
|
|
822
1190
|
if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was signalled", result);
|
|
823
1191
|
if (result.exitCode !== 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.VERSION, false, "version process failed", result);
|
|
824
1192
|
const version = /^gentle-ai ([0-9]+\.[0-9]+\.[0-9]+)\n$/.exec(result.stdout.replace(/\r\n$/, "\n"))?.[1];
|
|
825
|
-
const contract = version === undefined ? undefined :
|
|
826
|
-
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,
|
|
1193
|
+
const contract = version === undefined ? undefined : resolvedNativeCliContract(version);
|
|
1194
|
+
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"}`);
|
|
827
1195
|
return version as keyof typeof NATIVE_CLI_CONTRACTS;
|
|
828
1196
|
}
|
|
829
1197
|
|
|
@@ -832,10 +1200,18 @@ export class NativeReviewCliV214 {
|
|
|
832
1200
|
if (request.committedOnly !== undefined && typeof request.committedOnly !== "boolean") throw new TypeError("Native START committedOnly must be a boolean when supplied");
|
|
833
1201
|
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
|
|
834
1202
|
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
|
|
835
|
-
await this.verifyVersion(request.cwd, request.signal, ["start"]);
|
|
836
|
-
const
|
|
1203
|
+
const version = await this.verifyVersion(request.cwd, request.signal, ["start"]);
|
|
1204
|
+
const toleratedStderr = resolvedNativeCliContract(version)?.mode === true ? REVIEW_CONSENT_NOTICES : [];
|
|
1205
|
+
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);
|
|
837
1206
|
return decode(NATIVE_REVIEW_OPERATION.START, true, () => {
|
|
838
|
-
|
|
1207
|
+
// `target_identity` and `lens_bindings` are real, unconditionally-present
|
|
1208
|
+
// fields on gentle-ai's plain (non-negotiated) `review start` JSON output
|
|
1209
|
+
// (confirmed against the pinned v2.1.11 Go source and live against a dev
|
|
1210
|
+
// build during Phase 6 dev-binary ground-truthing, organic-rdd-parity) —
|
|
1211
|
+
// tolerated here but not consumed: Pi's negotiated-contract client
|
|
1212
|
+
// (NativeReviewCliV216, the production default) carries lineage/target
|
|
1213
|
+
// identity through its own `review-integration/v1` decoder instead.
|
|
1214
|
+
const body = exactObject(result, ["operation", "lineage_id", "state", "risk_level", "selected_lenses", "changed_files", "changed_lines", "correction_budget", "action", "lenses_required", "projection"], ["risk_evidence", "hint", "target_identity", "lens_bindings"]);
|
|
839
1215
|
if (body.operation !== "review/start" || body.projection !== "workspace" || !(NATIVE_FINALIZE_STATE as readonly string[]).includes(stringValue(body.state))) throw new Error("wrong start discriminator");
|
|
840
1216
|
const lineageId = requiredString(body.lineage_id);
|
|
841
1217
|
if (request.lineageId && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start lineage mismatch");
|
|
@@ -849,7 +1225,11 @@ export class NativeReviewCliV214 {
|
|
|
849
1225
|
!hasCanonicalSelectedLenses(riskLevel, selectedLenses) ||
|
|
850
1226
|
!hasValidLensesRequired(action, body.state as string, riskLevel, lensesRequired)
|
|
851
1227
|
) throw new Error("unknown or contradictory start enum");
|
|
852
|
-
return {
|
|
1228
|
+
return {
|
|
1229
|
+
lineageId, state: body.state as NativeStartResult["state"], riskLevel, selectedLenses, changedFiles: nonNegativeInteger(body.changed_files), changedLines: nonNegativeInteger(body.changed_lines), correctionBudget: nonNegativeInteger(body.correction_budget), action, lensesRequired,
|
|
1230
|
+
...(body.risk_evidence === undefined ? {} : { riskEvidence: stringArray(body.risk_evidence) }),
|
|
1231
|
+
...(body.hint === undefined ? {} : { hint: requiredString(body.hint) }),
|
|
1232
|
+
};
|
|
853
1233
|
});
|
|
854
1234
|
}
|
|
855
1235
|
|
|
@@ -894,18 +1274,31 @@ export class NativeReviewCliV214 {
|
|
|
894
1274
|
await this.verifyVersion(request.cwd, request.signal, ["validate"]);
|
|
895
1275
|
const execution = await this.execute(NATIVE_REVIEW_OPERATION.VALIDATE, request.cwd, ["review", "validate", "--gate", request.gate, "--cwd", request.cwd, ...(request.lineageId ? ["--lineage", request.lineageId] : []), ...(request.flags ?? [])], false, request.signal);
|
|
896
1276
|
return decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => {
|
|
897
|
-
const body = exactObject(execution.body, ["schema", "result", "allowed", "action", "reason", "context"]);
|
|
1277
|
+
const body = exactObject(execution.body, ["schema", "result", "allowed", "action", "reason", "context"], ["delivery"]);
|
|
898
1278
|
const gateResult = enumString(body.result, NATIVE_GATE_RESULT) as NativeValidateResult["result"];
|
|
899
1279
|
const action = sanitizeNativeDiagnosticText(requiredString(body.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
|
|
900
1280
|
const reason = sanitizeNativeDiagnosticText(requiredString(body.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1281
|
+
// `delivery` (Design Decision #9, organic-rdd-parity) is an alternate
|
|
1282
|
+
// discriminator: when present it must be the single literal
|
|
1283
|
+
// "disabled/unmanaged" (gentle-ai's RDDDeliveryDisabledUnmanaged — the
|
|
1284
|
+
// kill switch is off and no receipt governs the candidate), paired
|
|
1285
|
+
// exactly with result:invalidated, allowed:false, action:repository-policy,
|
|
1286
|
+
// and exit 0 — never the strict continue/create-new-lineage/
|
|
1287
|
+
// explicit-maintainer-action/stop pairing used when delivery is absent.
|
|
1288
|
+
const delivery = body.delivery === undefined ? undefined : (enumString(body.delivery, ["disabled/unmanaged"]) as NativeValidateResult["delivery"]);
|
|
1289
|
+
if (body.schema !== "gentle-ai.review-gate-result/v1" || !isCanonicalProcessString(action) || !isCanonicalProcessString(reason)) throw new Error("wrong validate discriminator");
|
|
1290
|
+
if (delivery !== undefined) {
|
|
1291
|
+
if (gateResult !== "invalidated" || body.allowed !== false || action !== "repository-policy" || execution.exitCode !== 0) throw new Error("wrong validate delivery discriminator");
|
|
1292
|
+
} else {
|
|
1293
|
+
const expectedAction = { allow: "continue", "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[gateResult];
|
|
1294
|
+
const expectedExitCode = gateResult === "allow" ? 0 : 1;
|
|
1295
|
+
if (typeof body.allowed !== "boolean" || body.allowed !== (gateResult === "allow") || action !== expectedAction || execution.exitCode !== expectedExitCode) throw new Error("wrong validate discriminator");
|
|
1296
|
+
}
|
|
904
1297
|
const gateContext = decodeGateContext(body.context);
|
|
905
1298
|
const returnedGate = gateContext.raw.gate;
|
|
906
1299
|
if (returnedGate !== request.gate && (gateResult === "allow" || returnedGate !== "")) throw new Error("native gate context does not match the requested gate");
|
|
907
1300
|
if (request.lineageId && returnedGate !== "" && gateContext.lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.VALIDATE, false, "native gate lineage mismatch");
|
|
908
|
-
return { allowed: body.allowed, result: gateResult, action, reason, gateContext };
|
|
1301
|
+
return { allowed: body.allowed as boolean, result: gateResult, action, reason, gateContext, ...(delivery === undefined ? {} : { delivery }) };
|
|
909
1302
|
});
|
|
910
1303
|
}
|
|
911
1304
|
|
|
@@ -938,6 +1331,24 @@ export class NativeReviewCliV214 {
|
|
|
938
1331
|
return status;
|
|
939
1332
|
}
|
|
940
1333
|
|
|
1334
|
+
// Dark until the negotiated version reports `mode: true` (Design Decision
|
|
1335
|
+
// #7, organic-rdd-parity). `status` is read-only (no timeout suppression,
|
|
1336
|
+
// exact fixed argv per the design's data flow); `enable`/`disable` mutate
|
|
1337
|
+
// and always pass `--scope clone` so Pi's own kill-switch command surface
|
|
1338
|
+
// never mutates the operator's global gentle-ai state across other clones.
|
|
1339
|
+
async reviewMode(request: NativeReviewModeRequest): Promise<NativeReviewModeResult> {
|
|
1340
|
+
await this.verifyVersion(request.cwd, request.signal, ["mode"]);
|
|
1341
|
+
const mutating = request.operation !== NATIVE_REVIEW_MODE_OPERATION.STATUS;
|
|
1342
|
+
const { body } = await this.execute(
|
|
1343
|
+
NATIVE_REVIEW_OPERATION.MODE,
|
|
1344
|
+
request.cwd,
|
|
1345
|
+
["review", "mode", request.operation, "--cwd", request.cwd, ...(mutating ? ["--scope", "clone"] : []), "--json"],
|
|
1346
|
+
mutating,
|
|
1347
|
+
request.signal,
|
|
1348
|
+
);
|
|
1349
|
+
return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(body, request.operation));
|
|
1350
|
+
}
|
|
1351
|
+
|
|
941
1352
|
async sddStatus(request: NativeSddStatusRequest): Promise<NativeSddStatusResult> {
|
|
942
1353
|
await this.verifyVersion(request.cwd, request.signal, ["sddStatus"]);
|
|
943
1354
|
const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.SDD_STATUS, request.cwd, ["sdd-status", request.change, "--cwd", request.cwd, "--json", "--instructions"], false, request.signal);
|
|
@@ -966,9 +1377,16 @@ export class NativeReviewCliV214 {
|
|
|
966
1377
|
nonNegativeInteger(remediation.generation); nonNegativeInteger(remediation.fixBatch);
|
|
967
1378
|
if (remediation.correctionBudget !== undefined) nonNegativeInteger(remediation.correctionBudget);
|
|
968
1379
|
let reviewGateResult: string | undefined;
|
|
1380
|
+
let reviewGateDelivery: string | undefined;
|
|
969
1381
|
if (body.reviewGate !== undefined) {
|
|
970
|
-
|
|
1382
|
+
// `delivery` (present since 09e4b14c, gentle-ai v2.2.0+) is emitted
|
|
1383
|
+
// when the kill switch is off: gentle-ai's status struct carries
|
|
1384
|
+
// `Delivery` with `json:"delivery,omitempty"`, so an exact-key decode
|
|
1385
|
+
// without it rejects that payload outright. Pre-existing bug fix
|
|
1386
|
+
// (Phase 13.13, migrate-review-integration-v2).
|
|
1387
|
+
const gate = exactObject(body.reviewGate, ["result", "reason"], ["delivery"]);
|
|
971
1388
|
reviewGateResult = enumString(gate.result, NATIVE_GATE_RESULT); requiredString(gate.reason);
|
|
1389
|
+
if (gate.delivery !== undefined) reviewGateDelivery = enumString(gate.delivery, ["disabled/unmanaged"]);
|
|
972
1390
|
}
|
|
973
1391
|
if (body.reviewTransaction !== undefined) decodeReviewTransaction(body.reviewTransaction);
|
|
974
1392
|
if (body.phaseInstructions !== undefined) {
|
|
@@ -986,7 +1404,13 @@ export class NativeReviewCliV214 {
|
|
|
986
1404
|
ready:
|
|
987
1405
|
(NATIVE_SDD_POST_REVIEW_ACTION as readonly string[]).includes(nextRecommended) &&
|
|
988
1406
|
blockedReasons.length === 0 &&
|
|
989
|
-
|
|
1407
|
+
// With the kill switch off, the gate result can never become
|
|
1408
|
+
// "allow" (delivery follows ordinary repository policy instead) —
|
|
1409
|
+
// gated only on `result === "allow"` this deadlocked `ready`
|
|
1410
|
+
// forever, the same deadlock gentle-ai fixed upstream in 2c18fa10.
|
|
1411
|
+
// `disabled/unmanaged` delivery is the legitimate non-blocking
|
|
1412
|
+
// terminal state and must also unblock readiness.
|
|
1413
|
+
(reviewGateResult === "allow" || reviewGateDelivery === "disabled/unmanaged"),
|
|
990
1414
|
};
|
|
991
1415
|
});
|
|
992
1416
|
}
|
|
@@ -1179,11 +1603,11 @@ export function nativeReviewLegacyAliasRepairAuthorization(request: Pick<NativeR
|
|
|
1179
1603
|
}
|
|
1180
1604
|
|
|
1181
1605
|
export class NativeReviewIntegrationError extends Error {
|
|
1182
|
-
readonly failureEnvelope:
|
|
1183
|
-
readonly mutationOutcome:
|
|
1606
|
+
readonly failureEnvelope: ReviewFailureV2;
|
|
1607
|
+
readonly mutationOutcome: ReviewFailureV2["mutationOutcome"];
|
|
1184
1608
|
readonly nextAction: string;
|
|
1185
1609
|
readonly launchAttempted = true;
|
|
1186
|
-
constructor(failure:
|
|
1610
|
+
constructor(failure: ReviewFailureV2) {
|
|
1187
1611
|
super(failure.message);
|
|
1188
1612
|
this.name = "NativeReviewIntegrationError";
|
|
1189
1613
|
this.failureEnvelope = failure;
|
|
@@ -1192,8 +1616,22 @@ export class NativeReviewIntegrationError extends Error {
|
|
|
1192
1616
|
}
|
|
1193
1617
|
}
|
|
1194
1618
|
|
|
1619
|
+
// Raised when negotiated START answers `consent/v2` (action:
|
|
1620
|
+
// "consent_required") instead of `start/v3`. Pi stays headless and never
|
|
1621
|
+
// sends `--consent relay`, so this is a blocking question surfaced to the
|
|
1622
|
+
// caller rather than answered on its behalf.
|
|
1623
|
+
export class NativeReviewConsentRequiredError extends Error {
|
|
1624
|
+
readonly consent: ReviewConsentV2;
|
|
1625
|
+
readonly launchAttempted = true;
|
|
1626
|
+
constructor(consent: ReviewConsentV2) {
|
|
1627
|
+
super(consent.headline);
|
|
1628
|
+
this.name = "NativeReviewConsentRequiredError";
|
|
1629
|
+
this.consent = consent;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1195
1633
|
type NativeExecutableDigestResolver = (path: string) => string;
|
|
1196
|
-
const nativeCapabilitiesByDigest = new Map<string, Promise<
|
|
1634
|
+
const nativeCapabilitiesByDigest = new Map<string, Promise<ReviewCapabilitiesV2>>();
|
|
1197
1635
|
|
|
1198
1636
|
export function clearNativeReviewCapabilitiesCacheForTesting(): void {
|
|
1199
1637
|
nativeCapabilitiesByDigest.clear();
|
|
@@ -1217,6 +1655,31 @@ interface NegotiatedExecution {
|
|
|
1217
1655
|
exitCode: number;
|
|
1218
1656
|
}
|
|
1219
1657
|
|
|
1658
|
+
function decodeNativeAdmittedResultManifest(value: unknown): NativeReviewAdmittedResultManifest {
|
|
1659
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("native capture-result manifest must be an object");
|
|
1660
|
+
const body = value as Record<string, unknown>;
|
|
1661
|
+
const text = (key: string): string => {
|
|
1662
|
+
const found = body[key];
|
|
1663
|
+
if (typeof found !== "string" || found.trim() !== found || found.length === 0) throw new TypeError(`native capture-result manifest ${key} must be a non-empty trimmed string`);
|
|
1664
|
+
return found;
|
|
1665
|
+
};
|
|
1666
|
+
const schema = text("schema");
|
|
1667
|
+
if (schema !== "gentle-ai.review-result-artifact/v2") throw new TypeError(`native capture-result manifest schema must be gentle-ai.review-result-artifact/v2, received ${schema}`);
|
|
1668
|
+
const admission = text("admission_decision");
|
|
1669
|
+
if (admission !== "completed") throw new TypeError(`native capture-result manifest admission_decision must be completed, received ${admission}`);
|
|
1670
|
+
// Exactly one locator: a provider-owned path OR an opaque reference. Both or
|
|
1671
|
+
// neither means the manifest cannot be handed to FINALIZE.
|
|
1672
|
+
const hasPath = body.path !== undefined, hasReference = body.reference !== undefined;
|
|
1673
|
+
if (hasPath === hasReference) throw new TypeError("native capture-result manifest must carry exactly one of path or reference");
|
|
1674
|
+
return Object.freeze({
|
|
1675
|
+
schema,
|
|
1676
|
+
subjectHash: text("subject_hash"),
|
|
1677
|
+
admissionDecision: admission,
|
|
1678
|
+
...(body.lens === undefined ? {} : { lens: text("lens") }),
|
|
1679
|
+
...(hasPath ? { path: text("path") } : { reference: text("reference") }),
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1220
1683
|
export class NativeReviewCliV216 implements NativeReviewCli {
|
|
1221
1684
|
private readonly legacy: NativeReviewCliV214;
|
|
1222
1685
|
private readonly adapter: ExecFileAdapter;
|
|
@@ -1270,6 +1733,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1270
1733
|
mutating: boolean,
|
|
1271
1734
|
signal: AbortSignal | undefined,
|
|
1272
1735
|
path: string,
|
|
1736
|
+
toleratedStderr: readonly string[] = [],
|
|
1273
1737
|
): Promise<NegotiatedExecution> {
|
|
1274
1738
|
let result: ExecFileResult;
|
|
1275
1739
|
try {
|
|
@@ -1285,17 +1749,17 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1285
1749
|
const body = parseJson(result.stdout, operation, mutating, diagnostics);
|
|
1286
1750
|
if (result.exitCode !== 0) {
|
|
1287
1751
|
try {
|
|
1288
|
-
throw new NativeReviewIntegrationError(
|
|
1752
|
+
throw new NativeReviewIntegrationError(decodeReviewFailureV2(body));
|
|
1289
1753
|
} catch (error) {
|
|
1290
1754
|
if (error instanceof NativeReviewIntegrationError) throw error;
|
|
1291
1755
|
throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native negotiated operation failed without a valid failure envelope", result);
|
|
1292
1756
|
}
|
|
1293
1757
|
}
|
|
1294
|
-
if (result.stderr.trim().length > 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
|
|
1758
|
+
if (result.stderr.trim().length > 0 && !stderrIsTolerated(result.stderr, toleratedStderr)) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
|
|
1295
1759
|
return { body, exitCode: result.exitCode };
|
|
1296
1760
|
}
|
|
1297
1761
|
|
|
1298
|
-
async capabilities(request: NativeCapabilitiesRequest = {}): Promise<
|
|
1762
|
+
async capabilities(request: NativeCapabilitiesRequest = {}): Promise<ReviewCapabilitiesV2> {
|
|
1299
1763
|
const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.VERSION, false);
|
|
1300
1764
|
const cached = nativeCapabilitiesByDigest.get(executable.digest);
|
|
1301
1765
|
if (cached !== undefined) return cached;
|
|
@@ -1308,7 +1772,22 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1308
1772
|
request.signal,
|
|
1309
1773
|
executable.path,
|
|
1310
1774
|
);
|
|
1311
|
-
|
|
1775
|
+
// Two distinct half-upgraded-install failures (Design Decision #7,
|
|
1776
|
+
// migrate-review-integration-v2): a decode failure means the installed
|
|
1777
|
+
// runtime cannot even answer this shape (an older `.gentle-ai/` payload
|
|
1778
|
+
// that still speaks `unsupported_contract` for v2); a successful decode
|
|
1779
|
+
// whose package.version disagrees with the pin names both versions.
|
|
1780
|
+
let capabilities: ReviewCapabilitiesV2;
|
|
1781
|
+
try {
|
|
1782
|
+
capabilities = decodeReviewCapabilitiesV2(execution.body, executable.digest);
|
|
1783
|
+
} catch (error) {
|
|
1784
|
+
if (error instanceof NativeReviewCliError) throw error;
|
|
1785
|
+
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`);
|
|
1786
|
+
}
|
|
1787
|
+
if (capabilities.packageVersion !== GENTLE_AI_VERSION) {
|
|
1788
|
+
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}`);
|
|
1789
|
+
}
|
|
1790
|
+
return capabilities;
|
|
1312
1791
|
})();
|
|
1313
1792
|
nativeCapabilitiesByDigest.set(executable.digest, negotiation);
|
|
1314
1793
|
try {
|
|
@@ -1325,6 +1804,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1325
1804
|
arguments_: readonly string[],
|
|
1326
1805
|
mutating: boolean,
|
|
1327
1806
|
signal?: AbortSignal,
|
|
1807
|
+
toleratedStderr: readonly string[] = [],
|
|
1328
1808
|
): Promise<NegotiatedExecution> {
|
|
1329
1809
|
const executable = this.verifiedExecutable(operation, mutating);
|
|
1330
1810
|
await this.capabilities({ cwd, ...(signal === undefined ? {} : { signal }) });
|
|
@@ -1332,21 +1812,47 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1332
1812
|
if (afterNegotiation.path !== executable.path || afterNegotiation.digest !== executable.digest) {
|
|
1333
1813
|
throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, operation, mutating, "native executable was replaced after capability negotiation", undefined, false);
|
|
1334
1814
|
}
|
|
1335
|
-
return this.invoke(operation, cwd, arguments_, mutating, signal, executable.path);
|
|
1815
|
+
return this.invoke(operation, cwd, arguments_, mutating, signal, executable.path, toleratedStderr);
|
|
1336
1816
|
}
|
|
1337
1817
|
|
|
1338
1818
|
async start(request: NativeStartRequest): Promise<NativeStartResult> {
|
|
1339
1819
|
if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
|
|
1340
1820
|
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
|
|
1341
1821
|
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
|
|
1822
|
+
// A negotiated START requires exactly one --contract, --target, and
|
|
1823
|
+
// --projection. The target is resolved here, from the same root START is
|
|
1824
|
+
// about to run in, rather than threaded in by the caller: the caller's
|
|
1825
|
+
// root and START's root differ whenever a candidate view is in play, and
|
|
1826
|
+
// a target frozen from the wrong root would name a snapshot this START
|
|
1827
|
+
// never inspects. targetStatus is read-only, so this costs one extra
|
|
1828
|
+
// read and cannot create authority.
|
|
1829
|
+
const projection = request.projection ?? "workspace";
|
|
1830
|
+
const target = await this.targetStatus({
|
|
1831
|
+
cwd: request.cwd,
|
|
1832
|
+
projection,
|
|
1833
|
+
...(request.baseRef === undefined ? {} : { baseRef: request.baseRef }),
|
|
1834
|
+
...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }),
|
|
1835
|
+
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
1836
|
+
});
|
|
1342
1837
|
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, [
|
|
1343
1838
|
"review", "start", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
|
|
1839
|
+
"--target", target.targetIdentity, "--projection", projection,
|
|
1344
1840
|
...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
|
|
1345
1841
|
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
1346
1842
|
...(request.policyPath === undefined ? [] : ["--policy", request.policyPath]),
|
|
1347
1843
|
...(request.focus === undefined ? [] : ["--focus", request.focus]),
|
|
1348
|
-
|
|
1349
|
-
|
|
1844
|
+
// A headless START that reviews without asking still succeeds; its
|
|
1845
|
+
// consent notices are console output, never a failure signal.
|
|
1846
|
+
], true, request.signal, REVIEW_CONSENT_NOTICES);
|
|
1847
|
+
// A negotiated v2 START may answer `consent/v2` (action:
|
|
1848
|
+
// "consent_required") instead of `start/v3` when the provider needs an
|
|
1849
|
+
// explicit answer it cannot infer. Discriminated BEFORE decode: the two
|
|
1850
|
+
// envelopes share no required shape, and Pi never sends `--consent
|
|
1851
|
+
// relay`, so this is always surfaced to the caller, never answered here.
|
|
1852
|
+
if (execution.body.action === "consent_required") {
|
|
1853
|
+
throw new NativeReviewConsentRequiredError(decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewConsentV2(execution.body)));
|
|
1854
|
+
}
|
|
1855
|
+
const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
|
|
1350
1856
|
if (request.lineageId !== undefined && result.lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start lineage mismatch");
|
|
1351
1857
|
return {
|
|
1352
1858
|
lineageId: result.lineageId,
|
|
@@ -1359,6 +1865,19 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1359
1865
|
action: result.action as NativeStartAction,
|
|
1360
1866
|
lensesRequired: result.lensesRequired,
|
|
1361
1867
|
riskReasons: result.riskReasons.map((reason) => ({ ...reason })),
|
|
1868
|
+
// Derived, not received. `risk_reasons` is a required start/v2 field
|
|
1869
|
+
// already recomputed against the authoritative frozen snapshot, so
|
|
1870
|
+
// these phrases describe the same candidate the lenses will review.
|
|
1871
|
+
...(() => {
|
|
1872
|
+
const evidence = nativeRiskEvidencePhrases(result.riskLevel, result.riskReasons);
|
|
1873
|
+
return evidence.length === 0 ? {} : { riskEvidence: evidence };
|
|
1874
|
+
})(),
|
|
1875
|
+
// Only the empty-candidate recovery is reconstructed. Its sibling
|
|
1876
|
+
// tells a plain caller to rerun under the negotiated contract, which
|
|
1877
|
+
// this client already did, so relaying it would name a step that
|
|
1878
|
+
// changes nothing. A committed-only start (baseRef) is already the
|
|
1879
|
+
// recovery, and reporting zero changes there is a real answer.
|
|
1880
|
+
...(result.changedFiles === 0 && request.baseRef === undefined ? { hint: REVIEW_EMPTY_CANDIDATE_HINT } : {}),
|
|
1362
1881
|
raw: result.raw,
|
|
1363
1882
|
};
|
|
1364
1883
|
}
|
|
@@ -1390,14 +1909,20 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1390
1909
|
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
|
|
1391
1910
|
"review", "finalize", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
|
|
1392
1911
|
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
1393
|
-
|
|
1912
|
+
// `--result` is RETIRED. A reviewer result supplied that way carries
|
|
1913
|
+
// no provider-owned admission, so it cannot prove the lens inspected
|
|
1914
|
+
// the frozen candidate. Results reach authority through
|
|
1915
|
+
// `review capture-result`, and FINALIZE either discovers them
|
|
1916
|
+
// (`--captured-results`) or is handed each manifest in lens order.
|
|
1917
|
+
...(request.capturedResults === true ? ["--captured-results=true"] : []),
|
|
1918
|
+
...(request.resultArtifactFiles ?? []).flatMap((path) => ["--result-artifact-file", path]),
|
|
1394
1919
|
...(refuterFile === undefined ? [] : ["--refuter", refuterFile]),
|
|
1395
1920
|
...(request.correctionLines === undefined ? [] : ["--correction-lines", String(request.correctionLines)]),
|
|
1396
1921
|
...(validationFile === undefined ? [] : ["--validation", validationFile]),
|
|
1397
1922
|
...(evidenceFile === undefined ? [] : ["--evidence", evidenceFile]),
|
|
1398
1923
|
...(request.failed === true ? ["--failed"] : []),
|
|
1399
1924
|
], true, request.signal);
|
|
1400
|
-
const envelope = decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () =>
|
|
1925
|
+
const envelope = decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => decodeReviewOperationV2(execution.body));
|
|
1401
1926
|
if (envelope.operation !== "review.finalize") throw new Error("wrong finalize operation envelope");
|
|
1402
1927
|
const body = envelope.result;
|
|
1403
1928
|
const lineageId = requiredString(body.lineage_id);
|
|
@@ -1407,6 +1932,12 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1407
1932
|
state: requiredString(body.state),
|
|
1408
1933
|
action: requiredString(body.action),
|
|
1409
1934
|
storeRevision: requiredString(body.store_revision),
|
|
1935
|
+
// Present only when state is correction_required (validated by
|
|
1936
|
+
// decodeReviewOperationV2); carried through as the raw validated
|
|
1937
|
+
// shape rather than re-decoded, since the typed decoder for this
|
|
1938
|
+
// nested object is private to lib/review-integration-v2.ts.
|
|
1939
|
+
...(body.validation_request === undefined ? {} : { validationRequest: body.validation_request as Readonly<Record<string, unknown>> }),
|
|
1940
|
+
...(body.escalation === undefined ? {} : { escalation: requiredString(body.escalation) }),
|
|
1410
1941
|
};
|
|
1411
1942
|
} finally {
|
|
1412
1943
|
if (directory !== undefined) await this.cleanupDirectory(directory).catch(() => undefined);
|
|
@@ -1419,7 +1950,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1419
1950
|
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
1420
1951
|
...(request.flags ?? []),
|
|
1421
1952
|
], false, request.signal);
|
|
1422
|
-
const envelope = decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () =>
|
|
1953
|
+
const envelope = decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => decodeReviewOperationV2(execution.body));
|
|
1423
1954
|
if (envelope.operation !== "review.validate") throw new Error("wrong validate operation envelope");
|
|
1424
1955
|
const body = envelope.result;
|
|
1425
1956
|
const gateContext = decodeGateContext(body.context);
|
|
@@ -1429,6 +1960,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1429
1960
|
action: requiredString(body.action),
|
|
1430
1961
|
reason: requiredString(body.reason),
|
|
1431
1962
|
gateContext,
|
|
1963
|
+
...(body.delivery === undefined ? {} : { delivery: enumString(body.delivery, ["disabled/unmanaged"]) as NativeValidateResult["delivery"] }),
|
|
1432
1964
|
};
|
|
1433
1965
|
}
|
|
1434
1966
|
|
|
@@ -1438,7 +1970,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1438
1970
|
"--change", request.change, "--lineage", request.lineage,
|
|
1439
1971
|
`--expected-binding-revision=${request.expectedBindingRevision}`,
|
|
1440
1972
|
], true, request.signal);
|
|
1441
|
-
const envelope = decode(NATIVE_REVIEW_OPERATION.BIND_SDD, true, () =>
|
|
1973
|
+
const envelope = decode(NATIVE_REVIEW_OPERATION.BIND_SDD, true, () => decodeReviewOperationV2(execution.body));
|
|
1442
1974
|
if (envelope.operation !== "review.bind_sdd") throw new Error("wrong bind-sdd operation envelope");
|
|
1443
1975
|
const body = envelope.result;
|
|
1444
1976
|
const gateContext = decodeGateContext(body.gate_context);
|
|
@@ -1455,14 +1987,97 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1455
1987
|
};
|
|
1456
1988
|
}
|
|
1457
1989
|
|
|
1458
|
-
async targetStatus(request: NativeTargetStatusRequest): Promise<
|
|
1990
|
+
async targetStatus(request: NativeTargetStatusRequest): Promise<ReviewStatusV3> {
|
|
1459
1991
|
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, [
|
|
1460
1992
|
"review", "status", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
|
|
1461
1993
|
"--projection", request.projection ?? "workspace",
|
|
1462
1994
|
...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef]),
|
|
1463
1995
|
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
1996
|
+
"--next-transition",
|
|
1997
|
+
], false, request.signal);
|
|
1998
|
+
assertSupportedNextTransitionOperation(execution.body);
|
|
1999
|
+
return decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeReviewStatusV3(execution.body));
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// Net-new negotiated `review.repair`: preflight first, execute only when
|
|
2003
|
+
// the assessment is eligible, using exactly the provider_inputs that
|
|
2004
|
+
// preflight published (Design Decision #6, migrate-review-integration-v2).
|
|
2005
|
+
// Argv shape beyond --mode is inferred from provider_inputs' own field
|
|
2006
|
+
// names — no repair/v2 fixture is mirrored upstream to ground-truth it
|
|
2007
|
+
// against (design.md Open Questions); this is a documented risk.
|
|
2008
|
+
async repair(request: NativeReviewRepairRequest): Promise<ReviewRepairV2> {
|
|
2009
|
+
const preflightExecution = await this.negotiated(NATIVE_REVIEW_OPERATION.REPAIR, request.cwd, [
|
|
2010
|
+
"review", "repair", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd, "--mode", "preflight",
|
|
1464
2011
|
], false, request.signal);
|
|
1465
|
-
|
|
2012
|
+
const preflight = decode(NATIVE_REVIEW_OPERATION.REPAIR, false, () => decodeReviewRepairV2(preflightExecution.body));
|
|
2013
|
+
if (preflight.mode !== "preflight") throw new Error("wrong repair preflight discriminator");
|
|
2014
|
+
if (preflight.assessment.status !== "eligible" || preflight.providerInputs === undefined) return preflight;
|
|
2015
|
+
const providerInputs = preflight.providerInputs;
|
|
2016
|
+
const executeExecution = await this.negotiated(NATIVE_REVIEW_OPERATION.REPAIR, request.cwd, [
|
|
2017
|
+
"review", "repair", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd, "--mode", "execute",
|
|
2018
|
+
"--lineage", providerInputs.lineageId,
|
|
2019
|
+
"--expected-revision", providerInputs.expectedRevision,
|
|
2020
|
+
"--cause", providerInputs.cause,
|
|
2021
|
+
"--disposition", providerInputs.disposition,
|
|
2022
|
+
"--repository-binding", providerInputs.repositoryBinding,
|
|
2023
|
+
"--actor", request.actor,
|
|
2024
|
+
"--reason", request.reason,
|
|
2025
|
+
"--maintainer-authorization", request.maintainerAuthorization,
|
|
2026
|
+
], true, request.signal);
|
|
2027
|
+
return decode(NATIVE_REVIEW_OPERATION.REPAIR, true, () => decodeReviewRepairV2(executeExecution.body));
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// `review capture-evidence`: the evidence-first correction lifecycle's
|
|
2031
|
+
// collection step. Confirmed argv and response shape against a real v2.2.2
|
|
2032
|
+
// review run (lineage review-b39d803b68a90767): exactly
|
|
2033
|
+
// --cwd/--lineage/--target/--expected-revision/--outcome/--input, no
|
|
2034
|
+
// --contract, and the verification-evidence/v2 record returned DIRECTLY —
|
|
2035
|
+
// not wrapped in an operation/v2 envelope. Evidence is staged through the
|
|
2036
|
+
// same 0o600 tmpfile discipline FINALIZE uses.
|
|
2037
|
+
async captureResult(request: NativeReviewCaptureResultRequest): Promise<NativeReviewAdmittedResultManifest> {
|
|
2038
|
+
if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_RESULT requires the provider-issued argument tokens");
|
|
2039
|
+
if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native CAPTURE_RESULT argument tokens must all be non-empty strings");
|
|
2040
|
+
if (request.resultDocument.length === 0) throw new TypeError("Native CAPTURE_RESULT result document must contain at least one byte");
|
|
2041
|
+
const carriesContext = request.argumentTokens.some((token) => token === "--repository-context" || token.startsWith("--repository-context="));
|
|
2042
|
+
if (carriesContext && request.cwd !== undefined) throw new TypeError("Native CAPTURE_RESULT takes a repository context or --cwd, never both");
|
|
2043
|
+
const directory = await mkdtemp(join(tmpdir(), "gentle-ai-capture-result-"));
|
|
2044
|
+
try {
|
|
2045
|
+
await chmod(directory, 0o700);
|
|
2046
|
+
const resultFile = join(directory, "result.json");
|
|
2047
|
+
await writeFile(resultFile, request.resultDocument, { encoding: "utf8", mode: 0o600 });
|
|
2048
|
+
await chmod(resultFile, 0o600);
|
|
2049
|
+
const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true);
|
|
2050
|
+
const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, request.cwd ?? process.cwd(), [
|
|
2051
|
+
"review", "capture-result",
|
|
2052
|
+
...request.argumentTokens,
|
|
2053
|
+
...(carriesContext || request.cwd === undefined ? [] : ["--cwd", request.cwd]),
|
|
2054
|
+
"--input", resultFile,
|
|
2055
|
+
], true, request.signal, executable.path);
|
|
2056
|
+
return decode(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true, () => decodeNativeAdmittedResultManifest(execution.body));
|
|
2057
|
+
} finally {
|
|
2058
|
+
await this.cleanupDirectory(directory).catch(() => undefined);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
async captureEvidence(request: NativeReviewCaptureEvidenceRequest): Promise<NativeReviewVerificationEvidenceV2> {
|
|
2063
|
+
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");
|
|
2064
|
+
if (request.evidenceDocument.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE evidence must contain at least one byte");
|
|
2065
|
+
const directory = await mkdtemp(join(tmpdir(), "gentle-ai-capture-evidence-"));
|
|
2066
|
+
try {
|
|
2067
|
+
await chmod(directory, 0o700);
|
|
2068
|
+
const evidenceFile = await this.stageEvidence(directory, request.evidenceDocument);
|
|
2069
|
+
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, request.cwd, [
|
|
2070
|
+
"review", "capture-evidence", "--cwd", request.cwd,
|
|
2071
|
+
"--lineage", request.lineageId,
|
|
2072
|
+
"--target", request.targetIdentity,
|
|
2073
|
+
"--expected-revision", request.expectedRevision,
|
|
2074
|
+
"--outcome", request.outcome,
|
|
2075
|
+
"--input", evidenceFile,
|
|
2076
|
+
], true, request.signal);
|
|
2077
|
+
return decode(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, true, () => decodeNativeReviewVerificationEvidence(execution.body));
|
|
2078
|
+
} finally {
|
|
2079
|
+
await this.cleanupDirectory(directory).catch(() => undefined);
|
|
2080
|
+
}
|
|
1466
2081
|
}
|
|
1467
2082
|
|
|
1468
2083
|
reviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult> {
|
|
@@ -1473,6 +2088,13 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
1473
2088
|
return this.legacy.sddStatus(request);
|
|
1474
2089
|
}
|
|
1475
2090
|
|
|
2091
|
+
// Same plain-CLI delegation as reviewStatus/sddStatus above: `review mode`
|
|
2092
|
+
// is dark until the negotiated version reports `mode: true` and stays
|
|
2093
|
+
// outside the negotiated integration-v1 contract.
|
|
2094
|
+
reviewMode(request: NativeReviewModeRequest): Promise<NativeReviewModeResult> {
|
|
2095
|
+
return this.legacy.reviewMode(request);
|
|
2096
|
+
}
|
|
2097
|
+
|
|
1476
2098
|
// Recovery commands are version-gated plain CLI operations outside the
|
|
1477
2099
|
// negotiated integration-v1 contract, exactly like reviewStatus/sddStatus.
|
|
1478
2100
|
reclaim(request: NativeReviewReclaimRequest): Promise<NativeReviewRecoveryResult> {
|