gentle-pi 2.2.0 → 2.3.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 +51 -80
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +54 -248
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +22 -38
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/docs/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +1778 -3673
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +372 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +210 -68
- package/lib/review-integration-v2.ts +839 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +177 -66
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +322 -890
- package/runtime/review-integration-v2.mjs +789 -257
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +12 -13
- package/skills/_shared/review-ledger-contract.md +8 -14
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +28 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +37 -35
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +117 -82
- package/tests/review-host-relay.test.ts +242 -8
- package/tests/review-integration-v2-forward.test.ts +576 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +361 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -530
package/lib/native-review-cli.ts
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { readFileSync, statSync } from "node:fs";
|
|
4
2
|
import { chmod, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
|
5
3
|
import { tmpdir } from "node:os";
|
|
6
4
|
import { dirname, isAbsolute, join, posix, win32 } from "node:path";
|
|
7
5
|
import { promisify } from "node:util";
|
|
8
|
-
import {
|
|
6
|
+
import { PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
|
|
9
7
|
import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.ts";
|
|
10
8
|
import {
|
|
11
9
|
REVIEW_INTEGRATION_CONTRACT,
|
|
12
|
-
|
|
10
|
+
decodeReviewAcknowledgedV1,
|
|
13
11
|
decodeReviewConsentV2,
|
|
14
12
|
decodeReviewConsentV3,
|
|
15
13
|
decodeReviewFailureV2,
|
|
16
|
-
|
|
14
|
+
decodeReviewLastEventClosureV1,
|
|
17
15
|
decodeReviewRepairV2,
|
|
18
16
|
decodeReviewResultArtifactV2,
|
|
19
17
|
decodeReviewStartV3,
|
|
18
|
+
decodeReviewStartV4,
|
|
20
19
|
decodeReviewStatusV3,
|
|
21
|
-
type
|
|
20
|
+
type ReviewAcknowledgedExpectationV1,
|
|
21
|
+
type ReviewAcknowledgedV1,
|
|
22
22
|
type ReviewConsentEnvelope,
|
|
23
23
|
type ReviewFailureV2,
|
|
24
24
|
type ReviewRepairV2,
|
|
25
|
+
type ReviewNextTransitionV3,
|
|
25
26
|
type ReviewStartState,
|
|
27
|
+
type ReviewStartV3,
|
|
28
|
+
type ReviewStartV4,
|
|
26
29
|
type ReviewStatusV3,
|
|
30
|
+
type ReviewLastEventClosureV1,
|
|
27
31
|
} from "./review-integration-v2.ts";
|
|
28
32
|
|
|
29
33
|
const execFileAsync = promisify(execFile);
|
|
@@ -46,12 +50,7 @@ function resolveNativeReviewMaxBufferBytes(environment: NodeJS.ProcessEnv = proc
|
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
export const NATIVE_REVIEW_OPERATION = {
|
|
49
|
-
VERSION: "version",
|
|
50
53
|
START: "review/start",
|
|
51
|
-
FINALIZE: "review/finalize",
|
|
52
|
-
VALIDATE: "review/validate",
|
|
53
|
-
BIND_SDD: "review/bind-sdd",
|
|
54
|
-
SDD_STATUS: "sdd-status",
|
|
55
54
|
STATUS: "review/status",
|
|
56
55
|
RECLAIM: "review/reclaim",
|
|
57
56
|
RECOVER: "review/recover",
|
|
@@ -61,9 +60,10 @@ export const NATIVE_REVIEW_OPERATION = {
|
|
|
61
60
|
REPAIR_LEGACY_ALIAS: "review/repair-legacy-alias",
|
|
62
61
|
MODE: "review/mode",
|
|
63
62
|
REPAIR: "review/repair",
|
|
64
|
-
CAPTURE_EVIDENCE: "review/capture-evidence",
|
|
65
63
|
CAPTURE_RESULT: "review/capture-result",
|
|
64
|
+
CAPTURE_CORRECTION_PLAN: "review/capture-correction-plan",
|
|
66
65
|
CAPTURE_PROVIDER_ROLE: "review/capture-provider-role",
|
|
66
|
+
ACKNOWLEDGE_APPROVED: "review/acknowledge-approved",
|
|
67
67
|
} as const;
|
|
68
68
|
export type NativeReviewOperation = (typeof NATIVE_REVIEW_OPERATION)[keyof typeof NATIVE_REVIEW_OPERATION];
|
|
69
69
|
|
|
@@ -89,43 +89,9 @@ export interface ExecFileRequest { file: string; arguments: readonly string[]; c
|
|
|
89
89
|
export interface ExecFileResult { stdout: string; stderr: string; exitCode: number; signal: NodeJS.Signals | null; timedOut: boolean; outputLimitExceeded: boolean; }
|
|
90
90
|
export type ExecFileAdapter = (request: ExecFileRequest) => Promise<ExecFileResult>;
|
|
91
91
|
|
|
92
|
-
export const NATIVE_SDD_ARTIFACT_STORE = {
|
|
93
|
-
OPENSPEC: "openspec",
|
|
94
|
-
ENGRAM: "engram",
|
|
95
|
-
NONE: "none",
|
|
96
|
-
} as const;
|
|
97
|
-
export type NativeSddArtifactStore = (typeof NATIVE_SDD_ARTIFACT_STORE)[keyof typeof NATIVE_SDD_ARTIFACT_STORE];
|
|
98
|
-
|
|
99
|
-
export const NATIVE_SDD_ARTIFACT_STATE = {
|
|
100
|
-
MISSING: "missing",
|
|
101
|
-
DONE: "done",
|
|
102
|
-
PARTIAL: "partial",
|
|
103
|
-
} as const;
|
|
104
|
-
export type NativeSddArtifactState = (typeof NATIVE_SDD_ARTIFACT_STATE)[keyof typeof NATIVE_SDD_ARTIFACT_STATE];
|
|
105
|
-
|
|
106
|
-
export interface NativeSddArtifactStates {
|
|
107
|
-
proposal: NativeSddArtifactState;
|
|
108
|
-
specs: NativeSddArtifactState;
|
|
109
|
-
design: NativeSddArtifactState;
|
|
110
|
-
tasks: NativeSddArtifactState;
|
|
111
|
-
applyProgress: NativeSddArtifactState;
|
|
112
|
-
verifyReport: NativeSddArtifactState;
|
|
113
|
-
reviewPolicy?: NativeSddArtifactState;
|
|
114
|
-
reviewLedger: NativeSddArtifactState;
|
|
115
|
-
reviewReceipt: NativeSddArtifactState;
|
|
116
|
-
reviewBundle: NativeSddArtifactState;
|
|
117
|
-
reviewContext: NativeSddArtifactState;
|
|
118
|
-
reviewState: NativeSddArtifactState;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
92
|
export interface NativeReviewCli {
|
|
122
93
|
start(request: NativeStartRequest): Promise<NativeStartResult>;
|
|
123
|
-
finalize(request: NativeFinalizeRequest): Promise<NativeFinalizeResult>;
|
|
124
|
-
validate(request: NativeValidateRequest): Promise<NativeValidateResult>;
|
|
125
|
-
bindSdd(request: NativeBindSddRequest): Promise<NativeBindSddResult>;
|
|
126
|
-
sddStatus(request: NativeSddStatusRequest): Promise<NativeSddStatusResult>;
|
|
127
94
|
reviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult>;
|
|
128
|
-
capabilities?(request?: NativeCapabilitiesRequest): Promise<ReviewCapabilitiesV2>;
|
|
129
95
|
targetStatus?(request: NativeTargetStatusRequest): Promise<ReviewStatusV3>;
|
|
130
96
|
answerConsent?(request: NativeReviewConsentAnswerRequest): Promise<NativeReviewConsentAnswerResult>;
|
|
131
97
|
reclaim?(request: NativeReviewReclaimRequest): Promise<NativeReviewRecoveryResult>;
|
|
@@ -134,31 +100,14 @@ export interface NativeReviewCli {
|
|
|
134
100
|
quarantineLegacy?(request: NativeReviewLegacyQuarantineRequest): Promise<NativeReviewRecoveryResult>;
|
|
135
101
|
reconcileAuthority?(request: NativeReviewReconcileAuthorityRequest): Promise<NativeReviewRecoveryResult>;
|
|
136
102
|
repairLegacyAlias?(request: NativeReviewLegacyAliasRepairRequest): Promise<NativeReviewRecoveryResult>;
|
|
137
|
-
// Net-new negotiated operations (contract v2 only): `review.repair` is
|
|
138
|
-
// advertised in the v2 operation list; `capture-evidence` is the
|
|
139
|
-
// evidence-first correction lifecycle's collection step.
|
|
140
103
|
repair?(request: NativeReviewRepairRequest): Promise<ReviewRepairV2>;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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>;
|
|
104
|
+
captureResult?(request: NativeReviewCaptureResultRequest): Promise<NativeReviewCaptureResultOutcome>;
|
|
105
|
+
captureCorrectionPlan?(request: NativeReviewCorrectionPlanCaptureRequest): Promise<ReviewLastEventClosureV1>;
|
|
106
|
+
captureProviderRole?(request: NativeReviewProviderRoleCaptureRequest): Promise<NativeReviewProviderRoleCaptureOutcome>;
|
|
158
107
|
// Dark until a negotiated version reports the `mode` capability true
|
|
159
108
|
// (Design Decision #7, organic-rdd-parity). Plain versioned CLI operation,
|
|
160
109
|
// outside the negotiated review-integration protocol — same shape as
|
|
161
|
-
// reviewStatus/
|
|
110
|
+
// reviewStatus/reclaim above.
|
|
162
111
|
reviewMode?(request: NativeReviewModeRequest): Promise<NativeReviewModeResult>;
|
|
163
112
|
}
|
|
164
113
|
|
|
@@ -183,6 +132,12 @@ export const NATIVE_REVIEW_MODE_SOURCE = {
|
|
|
183
132
|
} as const;
|
|
184
133
|
export type NativeReviewModeSource = (typeof NATIVE_REVIEW_MODE_SOURCE)[keyof typeof NATIVE_REVIEW_MODE_SOURCE];
|
|
185
134
|
|
|
135
|
+
export const NATIVE_REVIEW_MODE_REACH = {
|
|
136
|
+
MACHINE: "machine",
|
|
137
|
+
THIS_BUILD: "this_build",
|
|
138
|
+
} as const;
|
|
139
|
+
export type NativeReviewModeReach = (typeof NATIVE_REVIEW_MODE_REACH)[keyof typeof NATIVE_REVIEW_MODE_REACH];
|
|
140
|
+
|
|
186
141
|
export const NATIVE_REVIEW_MODE_SCOPE = {
|
|
187
142
|
GLOBAL: "global",
|
|
188
143
|
CLONE: "clone",
|
|
@@ -202,6 +157,7 @@ export interface NativeReviewModeStatus {
|
|
|
202
157
|
effective: "on" | "off";
|
|
203
158
|
source: NativeReviewModeSource;
|
|
204
159
|
revision?: string;
|
|
160
|
+
reach?: NativeReviewModeReach;
|
|
205
161
|
}
|
|
206
162
|
|
|
207
163
|
export interface NativeReviewModeResult {
|
|
@@ -367,28 +323,12 @@ export interface NativeReviewRepairRequest {
|
|
|
367
323
|
signal?: AbortSignal;
|
|
368
324
|
}
|
|
369
325
|
|
|
370
|
-
// `review capture-evidence` argv and response shape are pinned to a real
|
|
371
|
-
// v2.2.2 review run (lineage review-b39d803b68a90767): exactly
|
|
372
|
-
// --cwd/--lineage/--target/--expected-revision/--outcome/--input, no
|
|
373
|
-
// --contract, and the `gentle-ai.review-verification-evidence/v2` record
|
|
374
|
-
// returned directly (not wrapped in an operation/v2 envelope).
|
|
375
|
-
export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "procedural_tooling_failed"] as const;
|
|
376
|
-
export type NativeReviewCaptureOutcome = (typeof NATIVE_REVIEW_CAPTURE_OUTCOME)[number];
|
|
377
|
-
|
|
378
326
|
// `capture-result` is an additive headless command, NOT a negotiated
|
|
379
327
|
// repository operation: it accepts no --contract, and the provider's own
|
|
380
328
|
// transition tokens already carry the repository context -- it takes that or
|
|
381
329
|
// --cwd, never both. So Pi passes the tokens through verbatim and adds only
|
|
382
330
|
// --input. Reconstructing them would mean re-deriving a lineage, revision,
|
|
383
331
|
// target, lens slot, and subject hash the provider already issued.
|
|
384
|
-
// Added for the v2 finalize transport. `capturedResults` asks the provider to
|
|
385
|
-
// discover every manifest it already admitted; `resultArtifactFiles` hands them
|
|
386
|
-
// over explicitly in lens order. Both replace the retired `--result`.
|
|
387
|
-
export interface NativeReviewFinalizeCapturedResults {
|
|
388
|
-
readonly capturedResults?: boolean;
|
|
389
|
-
readonly resultArtifactFiles?: readonly string[];
|
|
390
|
-
}
|
|
391
|
-
|
|
392
332
|
export interface NativeReviewCaptureResultRequest {
|
|
393
333
|
readonly argumentTokens: readonly string[];
|
|
394
334
|
readonly resultDocument: string;
|
|
@@ -407,6 +347,33 @@ export interface NativeReviewAdmittedResultManifest {
|
|
|
407
347
|
readonly reference?: string;
|
|
408
348
|
}
|
|
409
349
|
|
|
350
|
+
/** A non-final reviewer capture acknowledges an admitted artifact; final captures close natively. */
|
|
351
|
+
export type NativeReviewCaptureResultOutcome = NativeReviewAdmittedResultManifest | ReviewLastEventClosureV1;
|
|
352
|
+
|
|
353
|
+
// The one continuation that burns approved authority. Its tokens are rendered
|
|
354
|
+
// by the provider in a closed order and are relayed verbatim: Pi never builds,
|
|
355
|
+
// reorders, or substitutes one, because a synthesized acknowledgement would be
|
|
356
|
+
// Pi deciding that a review is over. `binding` is the lineage, target, and
|
|
357
|
+
// revision the caller already holds from STATUS: when the provider answers the
|
|
358
|
+
// burn with a review-acknowledged/v1 envelope (gentle-ai #3947), that envelope
|
|
359
|
+
// must name exactly this burn.
|
|
360
|
+
export interface NativeReviewAcknowledgeApprovedRequest {
|
|
361
|
+
readonly argumentTokens: readonly string[];
|
|
362
|
+
readonly cwd: string;
|
|
363
|
+
readonly binding?: ReviewAcknowledgedExpectationV1;
|
|
364
|
+
readonly signal?: AbortSignal;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** `undefined` is the pinned silent burn (every release up to v2.5.0-rc.3); an envelope is the #3947 typed burn. */
|
|
368
|
+
export type NativeReviewAcknowledgeApprovedOutcome = ReviewAcknowledgedV1 | undefined;
|
|
369
|
+
|
|
370
|
+
export interface NativeReviewCorrectionPlanCaptureRequest {
|
|
371
|
+
readonly argumentTokens: readonly string[];
|
|
372
|
+
readonly correctionLines: number;
|
|
373
|
+
readonly cwd: string;
|
|
374
|
+
readonly signal?: AbortSignal;
|
|
375
|
+
}
|
|
376
|
+
|
|
410
377
|
// gentle-pi#311 P4-roles: one Go-owned non-lens provider role capture. The
|
|
411
378
|
// provider renders a SELF-CONTAINED authority-advancing vector
|
|
412
379
|
// (`review.capture-refuter` / `review.capture-validation` with binding tokens
|
|
@@ -416,6 +383,19 @@ export interface NativeReviewAdmittedResultManifest {
|
|
|
416
383
|
// raw verdict. Nothing here authors, parses, or transports role output.
|
|
417
384
|
export const NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA = "gentle-ai.review-provider-role-capture/v1";
|
|
418
385
|
|
|
386
|
+
// Capture operations and their terminal closure operations are independently
|
|
387
|
+
// named upstream. Keep this closed mapping explicit: capture-validation is the
|
|
388
|
+
// intentionally nonuniform `review/capture-validation` closure operation.
|
|
389
|
+
export const NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_CLOSURE_OPERATION = {
|
|
390
|
+
"review.capture-refuter": "review.capture-refuter",
|
|
391
|
+
"review.capture-validation": "review/capture-validation",
|
|
392
|
+
} as const;
|
|
393
|
+
type NativeReviewProviderRoleCaptureOperation = keyof typeof NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_CLOSURE_OPERATION;
|
|
394
|
+
|
|
395
|
+
function isNativeReviewProviderRoleCaptureOperation(operation: string): operation is NativeReviewProviderRoleCaptureOperation {
|
|
396
|
+
return Object.hasOwn(NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_CLOSURE_OPERATION, operation);
|
|
397
|
+
}
|
|
398
|
+
|
|
419
399
|
export interface NativeReviewProviderRoleCaptureRequest {
|
|
420
400
|
/** The provider-named capture operation, e.g. `review.capture-refuter`. */
|
|
421
401
|
readonly captureOperation: string;
|
|
@@ -434,54 +414,37 @@ export interface NativeReviewProviderRoleCaptureArtifact {
|
|
|
434
414
|
readonly captured: true;
|
|
435
415
|
}
|
|
436
416
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
lineageId: string;
|
|
440
|
-
targetIdentity: string;
|
|
441
|
-
expectedRevision: string;
|
|
442
|
-
outcome: NativeReviewCaptureOutcome;
|
|
443
|
-
evidenceDocument: string;
|
|
444
|
-
signal?: AbortSignal;
|
|
445
|
-
}
|
|
417
|
+
/** Refuter and validator captures close only when they are the native last event. */
|
|
418
|
+
export type NativeReviewProviderRoleCaptureOutcome = NativeReviewProviderRoleCaptureArtifact | ReviewLastEventClosureV1;
|
|
446
419
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
}
|
|
420
|
+
export const NATIVE_UNTRACKED_SCOPE = {
|
|
421
|
+
EXCLUDE: "exclude",
|
|
422
|
+
SELECT: "select",
|
|
423
|
+
} as const;
|
|
424
|
+
export type NativeUntrackedScope = (typeof NATIVE_UNTRACKED_SCOPE)[keyof typeof NATIVE_UNTRACKED_SCOPE];
|
|
467
425
|
|
|
468
|
-
export interface
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
ledgerIds: readonly string[];
|
|
478
|
-
rawPayloadSha256: string;
|
|
479
|
-
rawPayloadBytes: number;
|
|
480
|
-
outcome: NativeReviewCaptureOutcome;
|
|
481
|
-
recordDigest: string;
|
|
426
|
+
export interface NativeIntendedUntrackedSelectionSubmission {
|
|
427
|
+
readonly argumentTokens: readonly string[];
|
|
428
|
+
readonly value: string;
|
|
429
|
+
}
|
|
430
|
+
interface NativeUntrackedSelectionRequest {
|
|
431
|
+
untrackedScope?: NativeUntrackedScope;
|
|
432
|
+
expectedUntrackedInventory?: string;
|
|
433
|
+
intendedUntracked?: readonly string[];
|
|
434
|
+
intendedUntrackedSelection?: NativeIntendedUntrackedSelectionSubmission;
|
|
482
435
|
}
|
|
483
436
|
|
|
484
|
-
export interface NativeStartRequest
|
|
437
|
+
export interface NativeStartRequest extends NativeUntrackedSelectionRequest {
|
|
438
|
+
cwd: string;
|
|
439
|
+
baseRef?: string;
|
|
440
|
+
committedOnly?: boolean;
|
|
441
|
+
lineageId?: string;
|
|
442
|
+
policyPath?: string;
|
|
443
|
+
focus?: string;
|
|
444
|
+
targetIdentity?: string;
|
|
445
|
+
projection?: "workspace" | "staged";
|
|
446
|
+
signal?: AbortSignal;
|
|
447
|
+
}
|
|
485
448
|
export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "declined" } as const;
|
|
486
449
|
export type NativeReviewConsentAnswer = (typeof NATIVE_REVIEW_CONSENT_ANSWER)[keyof typeof NATIVE_REVIEW_CONSENT_ANSWER];
|
|
487
450
|
export interface NativeReviewConsentAnswerRequest { cwd: string; consent: ReviewConsentEnvelope; answer: NativeReviewConsentAnswer; signal?: AbortSignal; }
|
|
@@ -497,59 +460,12 @@ export interface NativeReviewConsentDeclinedResult {
|
|
|
497
460
|
}
|
|
498
461
|
export interface NativeReviewConsentStartedResult { kind: "started"; start: NativeStartResult; }
|
|
499
462
|
export type NativeReviewConsentAnswerResult = NativeReviewConsentStartedResult | NativeReviewConsentDeclinedResult;
|
|
500
|
-
export interface NativeFinalizeLensResult { lens: string; document: unknown; }
|
|
501
|
-
export interface NativeFinalizeRequest extends NativeReviewFinalizeCapturedResults {
|
|
502
|
-
cwd: string;
|
|
503
|
-
lineageId?: string;
|
|
504
|
-
resultFiles?: readonly string[];
|
|
505
|
-
lensResults?: readonly NativeFinalizeLensResult[];
|
|
506
|
-
refuterFile?: string;
|
|
507
|
-
refuterDocument?: unknown;
|
|
508
|
-
correctionLines?: number;
|
|
509
|
-
validationFile?: string;
|
|
510
|
-
validationDocument?: unknown;
|
|
511
|
-
evidenceFile?: string;
|
|
512
|
-
evidenceDocument?: string;
|
|
513
|
-
failed?: boolean;
|
|
514
|
-
signal?: AbortSignal;
|
|
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
|
-
|
|
544
|
-
export interface NativeValidateRequest { cwd: string; gate: string; lineageId?: string; flags?: readonly string[]; signal?: AbortSignal; }
|
|
545
|
-
export interface NativeBindSddRequest { cwd: string; change: string; lineage: string; expectedBindingRevision: string; signal?: AbortSignal; }
|
|
546
|
-
export interface NativeSddStatusRequest { cwd: string; change: string; signal?: AbortSignal; }
|
|
547
463
|
export interface NativeReviewStatusRequest { cwd: string; signal?: AbortSignal; }
|
|
548
|
-
export interface
|
|
549
|
-
export interface NativeTargetStatusRequest {
|
|
464
|
+
export interface NativeTargetStatusRequest extends NativeUntrackedSelectionRequest {
|
|
550
465
|
cwd: string;
|
|
551
466
|
lineageId?: string;
|
|
552
467
|
baseRef?: string;
|
|
468
|
+
committedOnly?: boolean;
|
|
553
469
|
projection?: "workspace" | "staged";
|
|
554
470
|
/**
|
|
555
471
|
* The immutable reviewer runtime this host provides. Measured against the
|
|
@@ -563,8 +479,6 @@ export interface NativeTargetStatusRequest {
|
|
|
563
479
|
agent?: "pi";
|
|
564
480
|
signal?: AbortSignal;
|
|
565
481
|
}
|
|
566
|
-
export interface NativeGateContext { lineageId: string; storeRevision: string; raw: Record<string, unknown>; }
|
|
567
|
-
|
|
568
482
|
export const NATIVE_REVIEW_AUTHORITY_STATUS = {
|
|
569
483
|
CLEAN: "clean",
|
|
570
484
|
ACTIVE: "active",
|
|
@@ -661,29 +575,60 @@ export interface NativeReviewStatusResult {
|
|
|
661
575
|
diagnostics: readonly NativeReviewAuthorityDiagnostic[];
|
|
662
576
|
raw: Record<string, unknown>;
|
|
663
577
|
}
|
|
664
|
-
export const NATIVE_START_ACTION = { CREATED: "created", RESUMED: "resumed",
|
|
578
|
+
export const NATIVE_START_ACTION = { CREATED: "created", RESUMED: "resumed", REPLAYED: "replayed", CLOSED: "closed", BLOCKED_SCOPE_ACTION: "blocked-scope-action" } as const;
|
|
665
579
|
export type NativeStartAction = (typeof NATIVE_START_ACTION)[keyof typeof NATIVE_START_ACTION];
|
|
666
|
-
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; }
|
|
667
|
-
export
|
|
668
|
-
|
|
669
|
-
export interface NativeBindSddResult {
|
|
670
|
-
revision: string;
|
|
671
|
-
change: string;
|
|
672
|
-
lineage: string;
|
|
673
|
-
authorityRevision: string;
|
|
674
|
-
receiptHash: string;
|
|
675
|
-
gateContext: NativeGateContext;
|
|
580
|
+
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>[]; nextTransition?: ReviewNextTransitionV3; raw?: Readonly<Record<string, unknown>>; riskEvidence?: readonly string[]; hint?: string; }
|
|
581
|
+
export function isCanonicalProcessString(value: unknown): value is string {
|
|
582
|
+
return typeof value === "string" && value.length > 0 && value.trim() === value && !/[\u0000-\u001f\u007f]/.test(value);
|
|
676
583
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
[key: string]: unknown;
|
|
584
|
+
|
|
585
|
+
interface NativeUntrackedSelection {
|
|
586
|
+
untrackedScope?: NativeUntrackedScope;
|
|
587
|
+
expectedUntrackedInventory?: string;
|
|
588
|
+
intendedUntracked?: readonly string[];
|
|
683
589
|
}
|
|
684
590
|
|
|
685
|
-
|
|
686
|
-
return
|
|
591
|
+
function isNativeUntrackedPath(value: unknown): value is string {
|
|
592
|
+
return isCanonicalProcessString(value)
|
|
593
|
+
&& !posix.isAbsolute(value)
|
|
594
|
+
&& !win32.isAbsolute(value)
|
|
595
|
+
&& !value.includes("\\")
|
|
596
|
+
&& value.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function nativeUntrackedSelection(request: NativeUntrackedSelectionRequest): NativeUntrackedSelection {
|
|
600
|
+
const { untrackedScope, expectedUntrackedInventory, intendedUntracked } = request;
|
|
601
|
+
const declared = untrackedScope !== undefined || expectedUntrackedInventory !== undefined || intendedUntracked !== undefined;
|
|
602
|
+
if (!declared) return {};
|
|
603
|
+
if (
|
|
604
|
+
(untrackedScope !== NATIVE_UNTRACKED_SCOPE.EXCLUDE && untrackedScope !== NATIVE_UNTRACKED_SCOPE.SELECT) ||
|
|
605
|
+
!isCanonicalProcessString(expectedUntrackedInventory) ||
|
|
606
|
+
(intendedUntracked !== undefined && (!Array.isArray(intendedUntracked) || intendedUntracked.some((path) => !isNativeUntrackedPath(path) || intendedUntracked.indexOf(path) !== intendedUntracked.lastIndexOf(path))))
|
|
607
|
+
) {
|
|
608
|
+
throw new TypeError("Native untracked selection must declare one scope, one inventory digest, and unique repository-relative paths");
|
|
609
|
+
}
|
|
610
|
+
if (untrackedScope === NATIVE_UNTRACKED_SCOPE.EXCLUDE && (intendedUntracked?.length ?? 0) > 0) {
|
|
611
|
+
throw new TypeError("Native exclude untracked selection cannot include paths");
|
|
612
|
+
}
|
|
613
|
+
if (untrackedScope === NATIVE_UNTRACKED_SCOPE.SELECT && (intendedUntracked?.length ?? 0) === 0) {
|
|
614
|
+
throw new TypeError("Native select untracked selection requires at least one path");
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
untrackedScope,
|
|
618
|
+
expectedUntrackedInventory,
|
|
619
|
+
intendedUntracked: intendedUntracked === undefined ? undefined : [...intendedUntracked],
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function nativeUntrackedSelectionArguments(selection: NativeUntrackedSelection): readonly string[] {
|
|
624
|
+
if (selection.untrackedScope === undefined) return [];
|
|
625
|
+
return [
|
|
626
|
+
`--untracked-scope=${selection.untrackedScope}`,
|
|
627
|
+
`--expected-untracked-inventory=${selection.expectedUntrackedInventory!}`,
|
|
628
|
+
...(selection.untrackedScope === NATIVE_UNTRACKED_SCOPE.SELECT
|
|
629
|
+
? selection.intendedUntracked!.map((path) => `--intended-untracked=${path}`)
|
|
630
|
+
: []),
|
|
631
|
+
];
|
|
687
632
|
}
|
|
688
633
|
|
|
689
634
|
const NATIVE_RISK_LEVEL = ["low", "medium", "high"] as const;
|
|
@@ -764,28 +709,21 @@ export function nativeRiskEvidencePhrases(riskLevel: string, reasons: readonly N
|
|
|
764
709
|
return riskLevel === "medium" ? [REVIEW_MEDIUM_RISK_REASON, ...phrases] : phrases;
|
|
765
710
|
}
|
|
766
711
|
const NATIVE_REVIEW_LENS = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
|
|
767
|
-
const NATIVE_FINALIZE_STATE = ["reviewing", "correction_required", "validating", "approved", "escalated"] as const;
|
|
768
712
|
const NATIVE_START_ACTION_VALUES = Object.values(NATIVE_START_ACTION);
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const NATIVE_SDD_POST_REVIEW_ACTION = ["verify", "archive"] as const;
|
|
773
|
-
|
|
774
|
-
// Organic-parity columns (mode, riskEvidence, hint, delivery) stay false on
|
|
775
|
-
// every shipped row, including "2.1.11". PI-2 adds the first capability-true
|
|
776
|
-
// row (and bumps the triple pin) in one dedicated commit — never one without
|
|
777
|
-
// the other. See Design Decision #1 (organic-rdd-parity).
|
|
713
|
+
// The pin table is intentionally preserved unchanged while #3587 is
|
|
714
|
+
// unpublished. Last-event capture support is exercised through the explicit
|
|
715
|
+
// dev-binary override; no source-level pin or contract-row mutation is allowed.
|
|
778
716
|
const ORGANIC_PARITY_DARK = { mode: false, riskEvidence: false, hint: false, delivery: false } as const;
|
|
779
717
|
|
|
780
718
|
export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
781
|
-
"2.1.4": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true,
|
|
782
|
-
"2.1.5": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true,
|
|
783
|
-
"2.1.6": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true,
|
|
784
|
-
"2.1.7": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true,
|
|
785
|
-
"2.1.8": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true,
|
|
786
|
-
"2.1.9": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true,
|
|
787
|
-
"2.1.10": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true,
|
|
788
|
-
"2.1.11": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true,
|
|
719
|
+
"2.1.4": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: false, inventory: false, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
|
|
720
|
+
"2.1.5": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
|
|
721
|
+
"2.1.6": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
|
|
722
|
+
"2.1.7": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
|
|
723
|
+
"2.1.8": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: false, quarantineLegacy: false, reconcileAuthority: true, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
|
|
724
|
+
"2.1.9": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
|
|
725
|
+
"2.1.10": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, ...ORGANIC_PARITY_DARK }),
|
|
726
|
+
"2.1.11": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, ...ORGANIC_PARITY_DARK }),
|
|
789
727
|
// First capability-true row, paired with the triple pin bump in this same
|
|
790
728
|
// commit as Design Decision #1 requires.
|
|
791
729
|
//
|
|
@@ -806,7 +744,7 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
806
744
|
// needs the negotiated start envelope extended upstream, which moves a
|
|
807
745
|
// byte-pinned fixture and therefore belongs to a gentle-ai release, not to
|
|
808
746
|
// a Pi capability flip.
|
|
809
|
-
"2.2.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true,
|
|
747
|
+
"2.2.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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 }),
|
|
810
748
|
// 2.2.1 repeats 2.2.0 because the wire did not move for the lane Pi speaks.
|
|
811
749
|
// v2.2.1 advertises capabilities/v1.5 (protocol minor 5) on
|
|
812
750
|
// review-integration/v1, but the negotiated start envelope is still the
|
|
@@ -814,16 +752,16 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
814
752
|
// they are dark on 2.2.0. The release does publish a second contract,
|
|
815
753
|
// review-integration/v2, whose `start/v3` carries base/candidate trees --
|
|
816
754
|
// but Pi does not negotiate it yet, and a row must describe the lane in use.
|
|
817
|
-
"2.2.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true,
|
|
755
|
+
"2.2.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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 }),
|
|
818
756
|
// 2.2.2 repeats 2.2.1 for the same reason, confirmed against the released
|
|
819
757
|
// v2.2.2 binary rather than assumed: on review-integration/v1 it still
|
|
820
758
|
// advertises capabilities/v1.5 and the negotiated start envelope is still
|
|
821
759
|
// the closed `start/v2`, so riskEvidence and hint still cannot arrive.
|
|
822
|
-
"2.2.2": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true,
|
|
760
|
+
"2.2.2": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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
761
|
// Ground-truthed against the released v2.2.3 binary: the v2 lane remains
|
|
824
762
|
// protocol 2.0 with the same operation set and closed START fields consumed
|
|
825
763
|
// by Pi, so the existing capability columns are unchanged.
|
|
826
|
-
"2.2.3": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true,
|
|
764
|
+
"2.2.3": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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
765
|
// Ground-truthed against the released v2.4.0 binary: the v2 lane advertises
|
|
828
766
|
// capabilities/v2.2 and answers status/v5 and consent/v3, all of which the
|
|
829
767
|
// existing decoders already read, and the START envelope Pi consumes is
|
|
@@ -833,45 +771,25 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
833
771
|
// envelope reports, not whether it reports it. v2.2.4 and v2.3.0 shipped
|
|
834
772
|
// while Pi stayed on 2.2.3; they were never pinned or probed, so they get
|
|
835
773
|
// no row.
|
|
836
|
-
"2.4.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true,
|
|
774
|
+
"2.4.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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 }),
|
|
775
|
+
// Ground-truthed by driving the exact v2.5.0-rc.3 tagged build through the
|
|
776
|
+
// gentle-ai-bench driven journey corpus (exit 0), which exercises the
|
|
777
|
+
// start/status/capture/validate/mode/delivery lifecycles Pi consumes. The
|
|
778
|
+
// v2 lane advertises capabilities/v2.3 and its reviewing START is the
|
|
779
|
+
// `start/v4` continuation envelope that #499 already decodes; the closed
|
|
780
|
+
// fields Pi consumes are unchanged, so the columns match the 2.4.0 row.
|
|
781
|
+
// riskEvidence and hint stay dark: still not proven to reach the
|
|
782
|
+
// negotiated path Pi reads.
|
|
783
|
+
"2.5.0-rc.3": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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 }),
|
|
784
|
+
// Ground-truthed against the published v2.5.0 binary installed from its
|
|
785
|
+
// signed archive: `review capabilities` on the v2 lane advertises
|
|
786
|
+
// capabilities/v2.4 (protocol minor 4) and answers status/v6, consent/v3,
|
|
787
|
+
// and the `start/v4` continuation, all of which the decoders already read.
|
|
788
|
+
// The closed fields Pi consumes did not change between rc.3 and stable, so
|
|
789
|
+
// the columns match the 2.5.0-rc.3 row. riskEvidence and hint stay dark:
|
|
790
|
+
// still not proven to reach the negotiated START path Pi reads.
|
|
791
|
+
"2.5.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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 }),
|
|
837
792
|
});
|
|
838
|
-
type NativeCliCapability = keyof (typeof NATIVE_CLI_CONTRACTS)[keyof typeof NATIVE_CLI_CONTRACTS];
|
|
839
|
-
|
|
840
|
-
// Testing-only capability overlay: lets tests exercise mode/riskEvidence/hint/
|
|
841
|
-
// delivery decode paths under a synthetic version key without ever making a
|
|
842
|
-
// shipped row (including "2.1.11") capability-true. Production code never
|
|
843
|
-
// calls the setter, so this is inert outside test files.
|
|
844
|
-
const nativeCliContractsTestingOverlay = new Map<string, Record<NativeCliCapability, boolean>>();
|
|
845
|
-
export function setNativeCliContractForTesting(version: string, contract: Record<NativeCliCapability, boolean> | undefined): void {
|
|
846
|
-
if (contract === undefined) nativeCliContractsTestingOverlay.delete(version);
|
|
847
|
-
else nativeCliContractsTestingOverlay.set(version, contract);
|
|
848
|
-
}
|
|
849
|
-
function resolvedNativeCliContract(version: string): Record<NativeCliCapability, boolean> | undefined {
|
|
850
|
-
return nativeCliContractsTestingOverlay.get(version) ?? (NATIVE_CLI_CONTRACTS as Record<string, Record<NativeCliCapability, boolean> | undefined>)[version];
|
|
851
|
-
}
|
|
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
|
-
|
|
868
|
-
export interface NativeReviewStructuredDenial {
|
|
869
|
-
schema: "gentle-ai.review-gate-result/v1";
|
|
870
|
-
result: "scope-changed" | "invalidated" | "escalated";
|
|
871
|
-
action: string;
|
|
872
|
-
reason: string;
|
|
873
|
-
denial?: { stage: string; code: string };
|
|
874
|
-
}
|
|
875
793
|
|
|
876
794
|
export interface NativeReviewProcessDiagnostics {
|
|
877
795
|
operation: NativeReviewOperation;
|
|
@@ -883,7 +801,6 @@ export interface NativeReviewProcessDiagnostics {
|
|
|
883
801
|
max_buffer_bytes?: number;
|
|
884
802
|
configuration_hint?: string;
|
|
885
803
|
stderr?: string;
|
|
886
|
-
denial?: NativeReviewStructuredDenial;
|
|
887
804
|
}
|
|
888
805
|
|
|
889
806
|
export class NativeReviewCliError extends Error {
|
|
@@ -955,7 +872,6 @@ function decodeSelectedLenses(value: unknown, riskLevel: string, lensesRequired:
|
|
|
955
872
|
}
|
|
956
873
|
function enumString(value: unknown, allowed: readonly string[]): string { const parsed = stringValue(value); if (!allowed.includes(parsed)) throw new Error("unsupported enum"); return parsed; }
|
|
957
874
|
const NATIVE_DIAGNOSTIC_TEXT_LIMIT = 4_096;
|
|
958
|
-
const NATIVE_REVIEW_DENIAL_TEXT_LIMIT = 1_024;
|
|
959
875
|
|
|
960
876
|
function sanitizeNativeDiagnosticText(value: string, limit = NATIVE_DIAGNOSTIC_TEXT_LIMIT): string {
|
|
961
877
|
const normalized = value
|
|
@@ -970,40 +886,10 @@ function sanitizeNativeDiagnosticText(value: string, limit = NATIVE_DIAGNOSTIC_T
|
|
|
970
886
|
return normalized.length <= limit ? normalized : `${normalized.slice(0, limit - 14)}…[truncated]`;
|
|
971
887
|
}
|
|
972
888
|
|
|
973
|
-
function parseStructuredNativeDenial(stdout: string): NativeReviewStructuredDenial | undefined {
|
|
974
|
-
if (Buffer.byteLength(stdout, "utf8") > NATIVE_DIAGNOSTIC_TEXT_LIMIT * 4) return undefined;
|
|
975
|
-
try {
|
|
976
|
-
const value = exactObject(JSON.parse(stdout), ["schema", "result", "allowed", "action", "reason", "context"]);
|
|
977
|
-
const result = enumString(value.result, ["scope-changed", "invalidated", "escalated"] as const) as NativeReviewStructuredDenial["result"];
|
|
978
|
-
const action = sanitizeNativeDiagnosticText(requiredString(value.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
|
|
979
|
-
const reason = sanitizeNativeDiagnosticText(requiredString(value.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
|
|
980
|
-
const expectedAction = { "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[result];
|
|
981
|
-
if (
|
|
982
|
-
value.schema !== "gentle-ai.review-gate-result/v1" ||
|
|
983
|
-
value.allowed !== false ||
|
|
984
|
-
action !== expectedAction ||
|
|
985
|
-
!isCanonicalProcessString(action) ||
|
|
986
|
-
!isCanonicalProcessString(reason)
|
|
987
|
-
) return undefined;
|
|
988
|
-
const context = decodeGateContext(value.context).raw;
|
|
989
|
-
const rawDenial = context.denial;
|
|
990
|
-
const denial = rawDenial === undefined
|
|
991
|
-
? undefined
|
|
992
|
-
: (() => {
|
|
993
|
-
const parsed = exactObject(rawDenial, ["stage", "code"]);
|
|
994
|
-
const stage = sanitizeNativeDiagnosticText(requiredString(parsed.stage), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
|
|
995
|
-
const code = sanitizeNativeDiagnosticText(requiredString(parsed.code), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
|
|
996
|
-
if (!isCanonicalProcessString(stage) || !isCanonicalProcessString(code)) throw new Error("non-canonical denial evidence");
|
|
997
|
-
return { stage, code };
|
|
998
|
-
})();
|
|
999
|
-
return { schema: "gentle-ai.review-gate-result/v1", result, action, reason, ...(denial === undefined ? {} : { denial }) };
|
|
1000
|
-
} catch { return undefined; }
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
889
|
// Rebuild diagnostics from a duplicated module instance before facade output.
|
|
1004
890
|
export function sanitizeForeignNativeReviewDiagnostics(value: unknown): NativeReviewProcessDiagnostics | undefined {
|
|
1005
891
|
try {
|
|
1006
|
-
const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "max_buffer_bytes", "configuration_hint", "stderr"
|
|
892
|
+
const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "max_buffer_bytes", "configuration_hint", "stderr"]);
|
|
1007
893
|
const operation = enumString(raw.operation, Object.values(NATIVE_REVIEW_OPERATION)) as NativeReviewOperation;
|
|
1008
894
|
const errorCode = enumString(raw.error_code, Object.values(NATIVE_REVIEW_ERROR_CODE)) as NativeReviewErrorCode;
|
|
1009
895
|
const signal = raw.signal === undefined ? undefined : requiredString(raw.signal);
|
|
@@ -1023,23 +909,10 @@ export function sanitizeForeignNativeReviewDiagnostics(value: unknown): NativeRe
|
|
|
1023
909
|
output_limit_exceeded: booleanValue(raw.output_limit_exceeded),
|
|
1024
910
|
...(maxBufferBytes === undefined ? {} : { max_buffer_bytes: maxBufferBytes, configuration_hint: configurationHint! }),
|
|
1025
911
|
...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr)) }),
|
|
1026
|
-
...(raw.denial === undefined ? {} : { denial: sanitizeForeignStructuredDenial(raw.denial) }),
|
|
1027
912
|
};
|
|
1028
913
|
} catch { return undefined; }
|
|
1029
914
|
}
|
|
1030
915
|
|
|
1031
|
-
function sanitizeForeignStructuredDenial(value: unknown): NativeReviewStructuredDenial {
|
|
1032
|
-
const parsed = exactObject(value, ["schema", "result", "action", "reason"], ["denial"]);
|
|
1033
|
-
if (parsed.schema !== "gentle-ai.review-gate-result/v1") throw new Error("invalid denial schema");
|
|
1034
|
-
const result = enumString(parsed.result, ["scope-changed", "invalidated", "escalated"] as const) as NativeReviewStructuredDenial["result"];
|
|
1035
|
-
const action = sanitizeNativeDiagnosticText(requiredString(parsed.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
|
|
1036
|
-
const reason = sanitizeNativeDiagnosticText(requiredString(parsed.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
|
|
1037
|
-
const expectedAction = { "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[result];
|
|
1038
|
-
if (action !== expectedAction || !isCanonicalProcessString(action) || !isCanonicalProcessString(reason)) throw new Error("non-canonical denial evidence");
|
|
1039
|
-
const denial = parsed.denial === undefined ? undefined : (() => { const nested = exactObject(parsed.denial, ["stage", "code"]); const stage = sanitizeNativeDiagnosticText(requiredString(nested.stage), NATIVE_REVIEW_DENIAL_TEXT_LIMIT); const code = sanitizeNativeDiagnosticText(requiredString(nested.code), NATIVE_REVIEW_DENIAL_TEXT_LIMIT); if (!isCanonicalProcessString(stage) || !isCanonicalProcessString(code)) throw new Error("non-canonical denial evidence"); return { stage, code }; })();
|
|
1040
|
-
return { schema: "gentle-ai.review-gate-result/v1", result, action, reason, ...(denial === undefined ? {} : { denial }) };
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
916
|
function nativeProcessDiagnostics(operation: NativeReviewOperation, code: NativeReviewErrorCode, result?: ExecFileResult, maxBufferBytes?: number): NativeReviewProcessDiagnostics {
|
|
1044
917
|
const outputLimitExceeded = result?.outputLimitExceeded === true;
|
|
1045
918
|
return {
|
|
@@ -1053,7 +926,6 @@ function nativeProcessDiagnostics(operation: NativeReviewOperation, code: Native
|
|
|
1053
926
|
? { max_buffer_bytes: maxBufferBytes, configuration_hint: NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT }
|
|
1054
927
|
: {}),
|
|
1055
928
|
...(result?.stderr.trim() ? { stderr: sanitizeNativeDiagnosticText(result.stderr) } : {}),
|
|
1056
|
-
...(result === undefined ? {} : { denial: parseStructuredNativeDenial(result.stdout) }),
|
|
1057
929
|
};
|
|
1058
930
|
}
|
|
1059
931
|
|
|
@@ -1072,39 +944,13 @@ function decodeLegacyReconcileAudit(value: unknown): NativeReviewRecoveryResult
|
|
|
1072
944
|
for (const field of ["predecessor_lineage", "successor_lineage", "outcome"]) requiredString(record[field]);
|
|
1073
945
|
return { record };
|
|
1074
946
|
}
|
|
1075
|
-
function decodeNativeReviewVerificationEvidence(value: unknown): NativeReviewVerificationEvidenceV2 {
|
|
1076
|
-
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"]);
|
|
1077
|
-
if (body.schema !== "gentle-ai.review-verification-evidence/v2") throw new Error("wrong verification evidence schema");
|
|
1078
|
-
return {
|
|
1079
|
-
schema: "gentle-ai.review-verification-evidence/v2",
|
|
1080
|
-
// verification-evidence.schema.json pins `version` to {"const": 2} -- the
|
|
1081
|
-
// NUMBER two, not the string. It was written as a string from a handoff
|
|
1082
|
-
// that listed the observed field NAMES without their types.
|
|
1083
|
-
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; })(),
|
|
1084
|
-
lineageId: requiredString(body.lineage_id),
|
|
1085
|
-
authorityRevision: requiredString(body.authority_revision),
|
|
1086
|
-
targetIdentity: requiredString(body.target_identity),
|
|
1087
|
-
candidateTree: requiredString(body.candidate_tree),
|
|
1088
|
-
pathsDigest: requiredString(body.paths_digest),
|
|
1089
|
-
paths: stringArray(body.paths),
|
|
1090
|
-
// The schema requires ledger_ids to be an array, but v2.2.2 emits null when
|
|
1091
|
-
// there are none. Tolerated deliberately: refusing here would reject a
|
|
1092
|
-
// response the provider actually sends, and the provider violating its own
|
|
1093
|
-
// published schema is its defect to fix, not a reason to break the client.
|
|
1094
|
-
ledgerIds: body.ledger_ids === null || body.ledger_ids === undefined ? [] : stringArray(body.ledger_ids),
|
|
1095
|
-
rawPayloadSha256: requiredString(body.raw_payload_sha256),
|
|
1096
|
-
rawPayloadBytes: nonNegativeInteger(body.raw_payload_bytes),
|
|
1097
|
-
outcome: enumString(body.outcome, NATIVE_REVIEW_CAPTURE_OUTCOME) as NativeReviewCaptureOutcome,
|
|
1098
|
-
recordDigest: requiredString(body.record_digest),
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
947
|
// Unimplemented next_transition.execute.operation values must never reach
|
|
1102
948
|
// argv synthesis. Checked against the raw pre-decode body so an operation
|
|
1103
949
|
// gentle-pi does not implement (e.g. a future `dispose-result`) fails with a
|
|
1104
950
|
// named, typed refusal instead of a generic schema-incompatible error, and
|
|
1105
951
|
// before any client ever tries to build an invocation for it (Design
|
|
1106
952
|
// Decision #6, migrate-review-integration-v2).
|
|
1107
|
-
const NATIVE_REVIEW_SUPPORTED_TRANSITION_OPERATIONS = new Set(["review.start", "review.
|
|
953
|
+
const NATIVE_REVIEW_SUPPORTED_TRANSITION_OPERATIONS = new Set(["review.start", "review.recover", "review.repair", "review.acknowledge-approved"]);
|
|
1108
954
|
function assertSupportedNextTransitionOperation(body: Record<string, unknown>): void {
|
|
1109
955
|
const nextTransition = body.next_transition;
|
|
1110
956
|
if (typeof nextTransition !== "object" || nextTransition === null || Array.isArray(nextTransition)) return;
|
|
@@ -1118,11 +964,23 @@ function assertSupportedNextTransitionOperation(body: Record<string, unknown>):
|
|
|
1118
964
|
function decode<T>(operation: NativeReviewOperation, mutating: boolean, callback: () => T, diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE)): T {
|
|
1119
965
|
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 }); }
|
|
1120
966
|
}
|
|
967
|
+
function decodeReviewStartResponse(value: unknown): ReviewStartV3 | ReviewStartV4 {
|
|
968
|
+
const body = object(value);
|
|
969
|
+
return body.schema === "gentle-ai.review-integration.start/v4"
|
|
970
|
+
? decodeReviewStartV4(body)
|
|
971
|
+
: decodeReviewStartV3(body);
|
|
972
|
+
}
|
|
1121
973
|
function decodeReleaseEvidence(value: unknown): void {
|
|
1122
974
|
const release = exactObject(value, ["release_tree", "configuration_hash", "generated_artifact_hash", "provenance_hash", "publication_boundary_hash", "publication_state", "evidence_freshness_hash", "evidence_freshness_state"]);
|
|
1123
975
|
for (const field of ["release_tree", "configuration_hash", "generated_artifact_hash", "provenance_hash", "publication_boundary_hash", "evidence_freshness_hash"]) requiredString(release[field]);
|
|
1124
976
|
if (release.publication_state !== "sealed" || release.evidence_freshness_state !== "current") throw new Error("invalid release evidence");
|
|
1125
977
|
}
|
|
978
|
+
function decodeNonDecidingGateContext(value: unknown, expectedGate: string): NativeGateContext {
|
|
979
|
+
const context = exactObject(value, ["gate"]);
|
|
980
|
+
const gate = enumString(context.gate, NATIVE_GATE);
|
|
981
|
+
if (gate !== expectedGate) throw new Error("native non-deciding gate context does not match the requested gate");
|
|
982
|
+
return { lineageId: "", storeRevision: "", raw: context };
|
|
983
|
+
}
|
|
1126
984
|
function decodeGateContext(value: unknown): NativeGateContext {
|
|
1127
985
|
const context = exactObject(
|
|
1128
986
|
value,
|
|
@@ -1215,7 +1073,7 @@ function decodeNativeReviewStatusDiagnostic(value: unknown): NativeReviewAuthori
|
|
|
1215
1073
|
return { path: requiredString(diagnostic.path), problem: requiredString(diagnostic.problem) };
|
|
1216
1074
|
}
|
|
1217
1075
|
function decodeNativeReviewModeStatus(value: unknown): NativeReviewModeStatus {
|
|
1218
|
-
const status = exactObject(value, ["schema", "global", "clone_local", "effective", "source"], ["revision"]);
|
|
1076
|
+
const status = exactObject(value, ["schema", "global", "clone_local", "effective", "source"], ["revision", "reach"]);
|
|
1219
1077
|
if (status.schema !== "gentle-ai.rdd-mode-status/v1") throw new Error("wrong review mode status schema");
|
|
1220
1078
|
return {
|
|
1221
1079
|
global: enumString(status.global, Object.values(NATIVE_REVIEW_MODE_VALUE)) as NativeReviewModeValue,
|
|
@@ -1223,6 +1081,7 @@ function decodeNativeReviewModeStatus(value: unknown): NativeReviewModeStatus {
|
|
|
1223
1081
|
effective: enumString(status.effective, ["on", "off"]) as "on" | "off",
|
|
1224
1082
|
source: enumString(status.source, Object.values(NATIVE_REVIEW_MODE_SOURCE)) as NativeReviewModeSource,
|
|
1225
1083
|
...(status.revision === undefined ? {} : { revision: requiredString(status.revision) }),
|
|
1084
|
+
...(status.reach === undefined ? {} : { reach: enumString(status.reach, Object.values(NATIVE_REVIEW_MODE_REACH)) as NativeReviewModeReach }),
|
|
1226
1085
|
};
|
|
1227
1086
|
}
|
|
1228
1087
|
|
|
@@ -1365,7 +1224,6 @@ function hasValidLensesRequired(action: NativeStartAction, state: string, riskLe
|
|
|
1365
1224
|
if (riskLevel === "low") return !lensesRequired;
|
|
1366
1225
|
if (action === NATIVE_START_ACTION.CREATED) return state === "reviewing" && lensesRequired;
|
|
1367
1226
|
if (action === NATIVE_START_ACTION.RESUMED) return !lensesRequired || state === "reviewing";
|
|
1368
|
-
if (action === NATIVE_START_ACTION.REUSE_RECEIPT) return state === "approved" && !lensesRequired;
|
|
1369
1227
|
return !lensesRequired;
|
|
1370
1228
|
}
|
|
1371
1229
|
|
|
@@ -1379,7 +1237,7 @@ interface NativeJsonExecution {
|
|
|
1379
1237
|
process: ExecFileResult;
|
|
1380
1238
|
}
|
|
1381
1239
|
|
|
1382
|
-
|
|
1240
|
+
class NativeReviewPlainCli {
|
|
1383
1241
|
private readonly adapter: ExecFileAdapter;
|
|
1384
1242
|
private readonly executable: string | (() => string);
|
|
1385
1243
|
private readonly timeoutMs: number;
|
|
@@ -1425,168 +1283,14 @@ export class NativeReviewCliV214 {
|
|
|
1425
1283
|
if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result, true, undefined, this.maxBufferBytes);
|
|
1426
1284
|
if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
|
|
1427
1285
|
if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
|
|
1428
|
-
const structuredValidateDenial = operation === NATIVE_REVIEW_OPERATION.VALIDATE && result.exitCode === 1;
|
|
1429
1286
|
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;
|
|
1430
1287
|
const toleratedNotice = stderrIsTolerated(result.stderr, toleratedStderr);
|
|
1431
|
-
if (result.exitCode !== 0 && !
|
|
1432
|
-
if (result.stderr.trim().length > 0 && !
|
|
1288
|
+
if (result.exitCode !== 0 && !maintenancePartialFailure) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native process failed", result);
|
|
1289
|
+
if (result.stderr.trim().length > 0 && !maintenancePartialFailure && !toleratedNotice) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
|
|
1433
1290
|
return { body: parseJson(result.stdout, operation, mutating, diagnostics), exitCode: result.exitCode, process: result };
|
|
1434
1291
|
}
|
|
1435
1292
|
|
|
1436
|
-
private async verifyVersion(cwd: string, signal: AbortSignal | undefined, capabilities: readonly NativeCliCapability[]): Promise<keyof typeof NATIVE_CLI_CONTRACTS> {
|
|
1437
|
-
let result: ExecFileResult;
|
|
1438
|
-
try { result = await this.adapter({ file: this.executablePath(NATIVE_REVIEW_OPERATION.VERSION, false), arguments: ["version"], cwd, timeoutMs: this.timeoutMs, maxBufferBytes: this.maxBufferBytes, signal }); }
|
|
1439
|
-
catch (error) {
|
|
1440
|
-
if (error instanceof NativeReviewCliError) throw error;
|
|
1441
|
-
if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was cancelled");
|
|
1442
|
-
throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, NATIVE_REVIEW_OPERATION.VERSION, false, "gentle-ai is unavailable");
|
|
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);
|
|
1445
|
-
if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process timed out", result);
|
|
1446
|
-
if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was signalled", result);
|
|
1447
|
-
if (result.exitCode !== 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.VERSION, false, "version process failed", result);
|
|
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);
|
|
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"}`);
|
|
1457
|
-
return version as keyof typeof NATIVE_CLI_CONTRACTS;
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
async start(request: NativeStartRequest): Promise<NativeStartResult> {
|
|
1461
|
-
if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
|
|
1462
|
-
if (request.committedOnly !== undefined && typeof request.committedOnly !== "boolean") throw new TypeError("Native START committedOnly must be a boolean when supplied");
|
|
1463
|
-
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
|
|
1464
|
-
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
|
|
1465
|
-
const version = await this.verifyVersion(request.cwd, request.signal, ["start"]);
|
|
1466
|
-
const toleratedStderr = effectiveNativeCliContract(version)?.mode === true ? REVIEW_CONSENT_NOTICES : [];
|
|
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);
|
|
1468
|
-
return decode(NATIVE_REVIEW_OPERATION.START, true, () => {
|
|
1469
|
-
// `target_identity` and `lens_bindings` are real, unconditionally-present
|
|
1470
|
-
// fields on gentle-ai's plain (non-negotiated) `review start` JSON output
|
|
1471
|
-
// (confirmed against the pinned v2.1.11 Go source and live against a dev
|
|
1472
|
-
// build during Phase 6 dev-binary ground-truthing, organic-rdd-parity) —
|
|
1473
|
-
// tolerated here but not consumed: Pi's negotiated-contract client
|
|
1474
|
-
// (NativeReviewCliV216, the production default) carries lineage/target
|
|
1475
|
-
// identity through its own `review-integration/v1` decoder instead.
|
|
1476
|
-
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"]);
|
|
1477
|
-
if (body.operation !== "review/start" || body.projection !== "workspace" || !(NATIVE_FINALIZE_STATE as readonly string[]).includes(stringValue(body.state))) throw new Error("wrong start discriminator");
|
|
1478
|
-
const lineageId = requiredString(body.lineage_id);
|
|
1479
|
-
if (request.lineageId && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start lineage mismatch");
|
|
1480
|
-
const riskLevel = requiredString(body.risk_level);
|
|
1481
|
-
const action = enumString(body.action, NATIVE_START_ACTION_VALUES) as NativeStartAction;
|
|
1482
|
-
const lensesRequired = booleanValue(body.lenses_required);
|
|
1483
|
-
const selectedLenses = decodeSelectedLenses(body.selected_lenses, riskLevel, lensesRequired);
|
|
1484
|
-
if (
|
|
1485
|
-
!(NATIVE_RISK_LEVEL as readonly string[]).includes(riskLevel) ||
|
|
1486
|
-
selectedLenses.some((lens) => !(NATIVE_REVIEW_LENS as readonly string[]).includes(lens)) ||
|
|
1487
|
-
!hasCanonicalSelectedLenses(riskLevel, selectedLenses) ||
|
|
1488
|
-
!hasValidLensesRequired(action, body.state as string, riskLevel, lensesRequired)
|
|
1489
|
-
) throw new Error("unknown or contradictory start enum");
|
|
1490
|
-
return {
|
|
1491
|
-
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,
|
|
1492
|
-
...(body.risk_evidence === undefined ? {} : { riskEvidence: stringArray(body.risk_evidence) }),
|
|
1493
|
-
...(body.hint === undefined ? {} : { hint: requiredString(body.hint) }),
|
|
1494
|
-
};
|
|
1495
|
-
});
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
private async stageDocument(directory: string, name: string, document: unknown): Promise<string> {
|
|
1499
|
-
const path = join(directory, `${name}.json`);
|
|
1500
|
-
await writeFile(path, JSON.stringify(document), { encoding: "utf8", mode: 0o600 });
|
|
1501
|
-
await chmod(path, 0o600);
|
|
1502
|
-
return path;
|
|
1503
|
-
}
|
|
1504
|
-
private async stageEvidence(directory: string, evidence: string): Promise<string> {
|
|
1505
|
-
const path = join(directory, "evidence.txt");
|
|
1506
|
-
await writeFile(path, evidence, { encoding: "utf8", mode: 0o600 });
|
|
1507
|
-
await chmod(path, 0o600);
|
|
1508
|
-
return path;
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
async finalize(request: NativeFinalizeRequest): Promise<NativeFinalizeResult> {
|
|
1512
|
-
if (request.evidenceDocument !== undefined && (typeof request.evidenceDocument !== "string" || request.evidenceDocument.length === 0)) throw new TypeError("Native FINALIZE evidence must contain at least one byte");
|
|
1513
|
-
await this.verifyVersion(request.cwd, request.signal, ["finalize"]);
|
|
1514
|
-
const needsStaging = request.lensResults !== undefined || request.refuterDocument !== undefined || request.validationDocument !== undefined || request.evidenceDocument !== undefined;
|
|
1515
|
-
const directory = needsStaging ? await mkdtemp(join(tmpdir(), "gentle-ai-finalize-")) : undefined;
|
|
1516
|
-
try {
|
|
1517
|
-
if (directory) await chmod(directory, 0o700);
|
|
1518
|
-
const resultFiles = directory && request.lensResults ? await Promise.all(request.lensResults.map((entry, index) => this.stageDocument(directory, `result-${index}`, entry.document))) : request.resultFiles ?? [];
|
|
1519
|
-
const refuterFile = directory && request.refuterDocument !== undefined ? await this.stageDocument(directory, "refuter", request.refuterDocument) : request.refuterFile;
|
|
1520
|
-
const validationFile = directory && request.validationDocument !== undefined ? await this.stageDocument(directory, "validation", request.validationDocument) : request.validationFile;
|
|
1521
|
-
const evidenceFile = directory && request.evidenceDocument !== undefined ? await this.stageEvidence(directory, request.evidenceDocument) : request.evidenceFile;
|
|
1522
|
-
const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, ["review", "finalize", "--cwd", request.cwd, ...(request.lineageId ? ["--lineage", request.lineageId] : []), ...resultFiles.flatMap((path) => ["--result", path]), ...(refuterFile ? ["--refuter", refuterFile] : []), ...(request.correctionLines === undefined ? [] : ["--correction-lines", String(request.correctionLines)]), ...(validationFile ? ["--validation", validationFile] : []), ...(evidenceFile ? ["--evidence", evidenceFile] : []), ...(request.failed ? ["--failed"] : [])], true, request.signal);
|
|
1523
|
-
return decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => {
|
|
1524
|
-
const body = exactObject(result, ["operation", "lineage_id", "state", "action", "store_revision"], ["receipt_path"]);
|
|
1525
|
-
if (body.operation !== "review/finalize") throw new Error("wrong finalize discriminator");
|
|
1526
|
-
const lineageId = requiredString(body.lineage_id);
|
|
1527
|
-
if (request.lineageId && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.FINALIZE, true, "native finalize lineage mismatch");
|
|
1528
|
-
const state = requiredString(body.state);
|
|
1529
|
-
if (!(NATIVE_FINALIZE_STATE as readonly string[]).includes(state)) throw new Error("unknown finalize state");
|
|
1530
|
-
return { lineageId, state, action: requiredString(body.action), storeRevision: requiredString(body.store_revision), ...(body.receipt_path === undefined ? {} : { receiptPath: requiredString(body.receipt_path) }) };
|
|
1531
|
-
});
|
|
1532
|
-
} finally { if (directory) await this.cleanupDirectory(directory).catch(() => undefined); }
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
async validate(request: NativeValidateRequest): Promise<NativeValidateResult> {
|
|
1536
|
-
await this.verifyVersion(request.cwd, request.signal, ["validate"]);
|
|
1537
|
-
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);
|
|
1538
|
-
return decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => {
|
|
1539
|
-
const body = exactObject(execution.body, ["schema", "result", "allowed", "action", "reason", "context"], ["delivery"]);
|
|
1540
|
-
const gateResult = enumString(body.result, NATIVE_GATE_RESULT) as NativeValidateResult["result"];
|
|
1541
|
-
const action = sanitizeNativeDiagnosticText(requiredString(body.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
|
|
1542
|
-
const reason = sanitizeNativeDiagnosticText(requiredString(body.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
|
|
1543
|
-
// `delivery` (Design Decision #9, organic-rdd-parity) is an alternate
|
|
1544
|
-
// discriminator: when present it must be the single literal
|
|
1545
|
-
// "disabled/unmanaged" (gentle-ai's RDDDeliveryDisabledUnmanaged — the
|
|
1546
|
-
// kill switch is off and no receipt governs the candidate), paired
|
|
1547
|
-
// exactly with result:invalidated, allowed:false, action:repository-policy,
|
|
1548
|
-
// and exit 0 — never the strict continue/create-new-lineage/
|
|
1549
|
-
// explicit-maintainer-action/stop pairing used when delivery is absent.
|
|
1550
|
-
const delivery = body.delivery === undefined ? undefined : (enumString(body.delivery, ["disabled/unmanaged"]) as NativeValidateResult["delivery"]);
|
|
1551
|
-
if (body.schema !== "gentle-ai.review-gate-result/v1" || !isCanonicalProcessString(action) || !isCanonicalProcessString(reason)) throw new Error("wrong validate discriminator");
|
|
1552
|
-
if (delivery !== undefined) {
|
|
1553
|
-
if (gateResult !== "invalidated" || body.allowed !== false || action !== "repository-policy" || execution.exitCode !== 0) throw new Error("wrong validate delivery discriminator");
|
|
1554
|
-
} else {
|
|
1555
|
-
const expectedAction = { allow: "continue", "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[gateResult];
|
|
1556
|
-
const expectedExitCode = gateResult === "allow" ? 0 : 1;
|
|
1557
|
-
if (typeof body.allowed !== "boolean" || body.allowed !== (gateResult === "allow") || action !== expectedAction || execution.exitCode !== expectedExitCode) throw new Error("wrong validate discriminator");
|
|
1558
|
-
}
|
|
1559
|
-
const gateContext = decodeGateContext(body.context);
|
|
1560
|
-
const returnedGate = gateContext.raw.gate;
|
|
1561
|
-
if (returnedGate !== request.gate && (gateResult === "allow" || returnedGate !== "")) throw new Error("native gate context does not match the requested gate");
|
|
1562
|
-
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");
|
|
1563
|
-
return { allowed: body.allowed as boolean, result: gateResult, action, reason, gateContext, ...(delivery === undefined ? {} : { delivery }) };
|
|
1564
|
-
});
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
async bindSdd(request: NativeBindSddRequest): Promise<NativeBindSddResult> {
|
|
1568
|
-
await this.verifyVersion(request.cwd, request.signal, ["bindSdd"]);
|
|
1569
|
-
const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.BIND_SDD, request.cwd, ["review", "bind-sdd", "--cwd", request.cwd, "--change", request.change, "--lineage", request.lineage, `--expected-binding-revision=${request.expectedBindingRevision}`], true, request.signal);
|
|
1570
|
-
return decode(NATIVE_REVIEW_OPERATION.BIND_SDD, true, () => {
|
|
1571
|
-
const body = exactObject(result, ["schema", "revision", "change", "lineage", "authority_revision", "receipt_hash", "gate_context"]);
|
|
1572
|
-
if (body.schema !== "gentle-ai.sdd-review-binding/v1" || body.change !== request.change || body.lineage !== request.lineage) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.BIND_SDD, true, "native binding identity mismatch");
|
|
1573
|
-
const receiptHash = requiredString(body.receipt_hash);
|
|
1574
|
-
const gateContext = decodeGateContext(body.gate_context);
|
|
1575
|
-
const authorityRevision = requiredString(body.authority_revision);
|
|
1576
|
-
if (gateContext.lineageId !== request.lineage || gateContext.storeRevision !== authorityRevision || gateContext.raw.gate !== "post-apply") throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.BIND_SDD, true, "native binding gate mismatch");
|
|
1577
|
-
return {
|
|
1578
|
-
revision: requiredString(body.revision),
|
|
1579
|
-
change: requiredString(body.change),
|
|
1580
|
-
lineage: requiredString(body.lineage),
|
|
1581
|
-
authorityRevision,
|
|
1582
|
-
receiptHash,
|
|
1583
|
-
gateContext,
|
|
1584
|
-
};
|
|
1585
|
-
});
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
1293
|
async reviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult> {
|
|
1589
|
-
await this.verifyVersion(request.cwd, request.signal, ["status", "inventory"]);
|
|
1590
1294
|
const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, ["review", "status", "--cwd", request.cwd], false, request.signal);
|
|
1591
1295
|
const status = decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeNativeReviewStatus(result));
|
|
1592
1296
|
if (!await repositoriesMatch(request.cwd, status.repository)) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.STATUS, false, "native review status repository mismatch");
|
|
@@ -1600,7 +1304,6 @@ export class NativeReviewCliV214 {
|
|
|
1600
1304
|
// never mutates the operator's global gentle-ai state across other clones.
|
|
1601
1305
|
async reviewMode(request: NativeReviewModeRequest): Promise<NativeReviewModeResult> {
|
|
1602
1306
|
const cwd = await canonicalNativeReviewCwd(request.cwd);
|
|
1603
|
-
await this.verifyVersion(cwd, request.signal, ["mode"]);
|
|
1604
1307
|
const mutating = request.operation !== NATIVE_REVIEW_MODE_OPERATION.STATUS;
|
|
1605
1308
|
const { body } = await this.execute(
|
|
1606
1309
|
NATIVE_REVIEW_OPERATION.MODE,
|
|
@@ -1612,77 +1315,10 @@ export class NativeReviewCliV214 {
|
|
|
1612
1315
|
return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(body, request.operation));
|
|
1613
1316
|
}
|
|
1614
1317
|
|
|
1615
|
-
async sddStatus(request: NativeSddStatusRequest): Promise<NativeSddStatusResult> {
|
|
1616
|
-
await this.verifyVersion(request.cwd, request.signal, ["sddStatus"]);
|
|
1617
|
-
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);
|
|
1618
|
-
return decode(NATIVE_REVIEW_OPERATION.SDD_STATUS, false, () => {
|
|
1619
|
-
const body = exactObject(result, ["schemaName", "schemaVersion", "changeName", "artifactStore", "planningHome", "changeRoot", "artifactPaths", "contextFiles", "artifacts", "taskProgress", "dependencies", "applyState", "actionContext", "relationships", "remediationState", "nextRecommended", "blockedReasons"], ["reviewGate", "reviewTransaction", "phaseInstructions"]);
|
|
1620
|
-
if (body.schemaName !== "gentle-ai.sdd-status" || body.schemaVersion !== 1 || body.changeName !== request.change || !["openspec", "engram", "none"].includes(body.artifactStore as string) || !["blocked", "all_done", "ready"].includes(body.applyState as string)) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.SDD_STATUS, false, "native status identity mismatch");
|
|
1621
|
-
const paths = ["proposal", "specs", "design", "tasks", "applyProgress", "verifyReport", "reviewPolicy", "reviewLedger", "reviewReceipt", "reviewBundle", "reviewContext", "reviewState"];
|
|
1622
|
-
const pathMap = (value: unknown) => { const parsed = exactObject(value, paths); for (const path of paths) stringArray(parsed[path]); };
|
|
1623
|
-
const planningHome = exactObject(body.planningHome, ["mode", "path"]);
|
|
1624
|
-
if (planningHome.mode !== "repo-local") throw new Error("invalid planning home");
|
|
1625
|
-
requiredString(planningHome.path); requiredString(body.changeRoot); pathMap(body.artifactPaths); pathMap(body.contextFiles);
|
|
1626
|
-
const artifactStates = paths.filter((path) => path !== "reviewPolicy" || body.artifactStore === NATIVE_SDD_ARTIFACT_STORE.ENGRAM);
|
|
1627
|
-
const artifacts = exactObject(body.artifacts, artifactStates);
|
|
1628
|
-
for (const path of artifactStates) if (!Object.values(NATIVE_SDD_ARTIFACT_STATE).includes(artifacts[path] as NativeSddArtifactState)) throw new Error("invalid artifact state");
|
|
1629
|
-
const taskProgress = exactObject(body.taskProgress, ["total", "completed", "pending", "allComplete"]);
|
|
1630
|
-
const total = nonNegativeInteger(taskProgress.total), completed = nonNegativeInteger(taskProgress.completed), pending = nonNegativeInteger(taskProgress.pending);
|
|
1631
|
-
if (typeof taskProgress.allComplete !== "boolean" || completed + pending !== total || taskProgress.allComplete !== (pending === 0)) throw new Error("invalid task progress");
|
|
1632
|
-
const dependencies = exactObject(body.dependencies, ["proposal", "specs", "design", "tasks", "apply", "verify", "archive"]);
|
|
1633
|
-
for (const phase of ["proposal", "specs", "design", "tasks", "apply", "verify", "archive"]) if (!["blocked", "ready", "all_done"].includes(dependencies[phase] as string)) throw new Error("invalid dependency state");
|
|
1634
|
-
const actionContext = exactObject(body.actionContext, ["mode", "workspaceRoot", "allowedEditRoots"]);
|
|
1635
|
-
if (actionContext.mode !== "repo-local" || requiredString(actionContext.workspaceRoot).length === 0 || stringArray(actionContext.allowedEditRoots).length === 0) throw new Error("invalid action context");
|
|
1636
|
-
const relationships = exactObject(body.relationships, ["dependsOn", "supersedes", "amends", "conflictsWith", "sameDomainActiveChanges"]);
|
|
1637
|
-
for (const field of ["dependsOn", "supersedes", "amends", "conflictsWith", "sameDomainActiveChanges"]) stringArray(relationships[field]);
|
|
1638
|
-
const remediation = exactObject(body.remediationState, ["required", "complete", "failedEvidenceRevision", "lineageId", "generation", "fixBatch", "reason"], ["correctionBudget"]);
|
|
1639
|
-
if (typeof remediation.required !== "boolean" || typeof remediation.complete !== "boolean" || ["failedEvidenceRevision", "lineageId", "reason"].some((field) => typeof remediation[field] !== "string")) throw new Error("invalid remediation state");
|
|
1640
|
-
nonNegativeInteger(remediation.generation); nonNegativeInteger(remediation.fixBatch);
|
|
1641
|
-
if (remediation.correctionBudget !== undefined) nonNegativeInteger(remediation.correctionBudget);
|
|
1642
|
-
let reviewGateResult: string | undefined;
|
|
1643
|
-
let reviewGateDelivery: string | undefined;
|
|
1644
|
-
if (body.reviewGate !== undefined) {
|
|
1645
|
-
// `delivery` (present since 09e4b14c, gentle-ai v2.2.0+) is emitted
|
|
1646
|
-
// when the kill switch is off: gentle-ai's status struct carries
|
|
1647
|
-
// `Delivery` with `json:"delivery,omitempty"`, so an exact-key decode
|
|
1648
|
-
// without it rejects that payload outright. Pre-existing bug fix
|
|
1649
|
-
// (Phase 13.13, migrate-review-integration-v2).
|
|
1650
|
-
const gate = exactObject(body.reviewGate, ["result", "reason"], ["delivery"]);
|
|
1651
|
-
reviewGateResult = enumString(gate.result, NATIVE_GATE_RESULT); requiredString(gate.reason);
|
|
1652
|
-
if (gate.delivery !== undefined) reviewGateDelivery = enumString(gate.delivery, ["disabled/unmanaged"]);
|
|
1653
|
-
}
|
|
1654
|
-
if (body.reviewTransaction !== undefined) decodeReviewTransaction(body.reviewTransaction);
|
|
1655
|
-
if (body.phaseInstructions !== undefined) {
|
|
1656
|
-
const instructions = exactObject(body.phaseInstructions, ["apply", "verify", "remediate", "archive"]);
|
|
1657
|
-
for (const phase of ["apply", "verify", "remediate", "archive"]) stringArray(instructions[phase]);
|
|
1658
|
-
}
|
|
1659
|
-
const nextRecommended = requiredString(body.nextRecommended);
|
|
1660
|
-
if (!(NATIVE_SDD_NEXT_ACTION as readonly string[]).includes(nextRecommended)) throw new Error("unknown SDD next action");
|
|
1661
|
-
const blockedReasons = stringArray(body.blockedReasons);
|
|
1662
|
-
return {
|
|
1663
|
-
...body,
|
|
1664
|
-
artifactStore: body.artifactStore as NativeSddArtifactStore,
|
|
1665
|
-
artifacts: artifacts as unknown as NativeSddArtifactStates,
|
|
1666
|
-
nextRecommended,
|
|
1667
|
-
ready:
|
|
1668
|
-
(NATIVE_SDD_POST_REVIEW_ACTION as readonly string[]).includes(nextRecommended) &&
|
|
1669
|
-
blockedReasons.length === 0 &&
|
|
1670
|
-
// With the kill switch off, the gate result can never become
|
|
1671
|
-
// "allow" (delivery follows ordinary repository policy instead) —
|
|
1672
|
-
// gated only on `result === "allow"` this deadlocked `ready`
|
|
1673
|
-
// forever, the same deadlock gentle-ai fixed upstream in 2c18fa10.
|
|
1674
|
-
// `disabled/unmanaged` delivery is the legitimate non-blocking
|
|
1675
|
-
// terminal state and must also unblock readiness.
|
|
1676
|
-
(reviewGateResult === "allow" || reviewGateDelivery === "disabled/unmanaged"),
|
|
1677
|
-
};
|
|
1678
|
-
});
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
1318
|
async reclaim(request: NativeReviewReclaimRequest): Promise<NativeReviewRecoveryResult> {
|
|
1682
1319
|
for (const [name, value] of [["lineage", request.lineage], ["actor", request.actor], ["reason", request.reason]] as const) {
|
|
1683
1320
|
if (!isCanonicalProcessString(value)) throw new TypeError(`Native RECLAIM ${name} must be a non-empty, trimmed, NUL-free string`);
|
|
1684
1321
|
}
|
|
1685
|
-
await this.verifyVersion(request.cwd, request.signal, ["reclaim"]);
|
|
1686
1322
|
const { body } = await this.execute(NATIVE_REVIEW_OPERATION.RECLAIM, request.cwd, ["review", "reclaim", "--cwd", request.cwd, "--lineage", request.lineage, "--actor", request.actor, "--reason", request.reason], true, request.signal);
|
|
1687
1323
|
return { record: body };
|
|
1688
1324
|
}
|
|
@@ -1703,7 +1339,6 @@ export class NativeReviewCliV214 {
|
|
|
1703
1339
|
throw new TypeError("Native RECOVER maintainerAuthorization must be a non-empty LF-only binding");
|
|
1704
1340
|
}
|
|
1705
1341
|
if (!(NATIVE_REVIEW_RECOVER_DISPOSITION as readonly string[]).includes(request.disposition)) throw new TypeError("Native RECOVER disposition must be scope_changed, invalidated, or escalated");
|
|
1706
|
-
await this.verifyVersion(request.cwd, request.signal, ["recover"]);
|
|
1707
1342
|
const { body } = await this.execute(NATIVE_REVIEW_OPERATION.RECOVER, request.cwd, [
|
|
1708
1343
|
"review", "recover", "--cwd", request.cwd,
|
|
1709
1344
|
"--predecessor-lineage", request.predecessorLineage,
|
|
@@ -1726,7 +1361,6 @@ export class NativeReviewCliV214 {
|
|
|
1726
1361
|
if (typeof value !== "boolean") throw new TypeError(`Native ABANDON ${name} must be a boolean`);
|
|
1727
1362
|
}
|
|
1728
1363
|
if (request.maintainerAuthorization !== nativeReviewAbandonAuthorization(request)) throw new TypeError("Native ABANDON maintainerAuthorization must match the exact lineage, revision, snapshot, reason, discarded-work, and actor binding");
|
|
1729
|
-
await this.verifyVersion(request.cwd, request.signal, ["abandon"]);
|
|
1730
1364
|
const execution = await this.execute(NATIVE_REVIEW_OPERATION.ABANDON, request.cwd, [
|
|
1731
1365
|
"review", "abandon", "--cwd", request.cwd,
|
|
1732
1366
|
"--lineage", request.lineage,
|
|
@@ -1746,7 +1380,6 @@ export class NativeReviewCliV214 {
|
|
|
1746
1380
|
}
|
|
1747
1381
|
if (request.diagnostic !== NATIVE_REVIEW_LEGACY_QUARANTINE.DIAGNOSTIC || request.disposition !== NATIVE_REVIEW_LEGACY_QUARANTINE.DISPOSITION) throw new TypeError("Native QUARANTINE_LEGACY supports only the published malformed freeze-findings diagnostic and disposition");
|
|
1748
1382
|
if (request.maintainerAuthorization !== nativeReviewLegacyQuarantineAuthorization(request)) throw new TypeError("Native QUARANTINE_LEGACY maintainerAuthorization must match the exact repository, lineage, revision, diagnostic, disposition, actor, and reason binding");
|
|
1749
|
-
await this.verifyVersion(request.cwd, request.signal, ["quarantineLegacy"]);
|
|
1750
1383
|
const execution = await this.execute(NATIVE_REVIEW_OPERATION.QUARANTINE_LEGACY, request.cwd, [
|
|
1751
1384
|
"review", "quarantine-legacy", "--cwd", request.cwd,
|
|
1752
1385
|
"--lineage", request.lineage,
|
|
@@ -1778,7 +1411,6 @@ export class NativeReviewCliV214 {
|
|
|
1778
1411
|
if (request.maintainerAuthorization !== expectedAuthorization) {
|
|
1779
1412
|
throw new TypeError("Native RECONCILE_AUTHORITY maintainerAuthorization must match the exact target and revision binding");
|
|
1780
1413
|
}
|
|
1781
|
-
const version = await this.verifyVersion(request.cwd, request.signal, ["reconcileAuthority"]);
|
|
1782
1414
|
const execution = await this.execute(NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, request.cwd, [
|
|
1783
1415
|
"review", "reconcile-authority", "--cwd", request.cwd,
|
|
1784
1416
|
"--predecessor-lineage", request.predecessorLineage,
|
|
@@ -1789,9 +1421,7 @@ export class NativeReviewCliV214 {
|
|
|
1789
1421
|
"--reason", request.reason,
|
|
1790
1422
|
"--maintainer-authorization", request.maintainerAuthorization,
|
|
1791
1423
|
], true, request.signal);
|
|
1792
|
-
const result = decode(NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, true, () =>
|
|
1793
|
-
? decodeLegacyReconcileAudit(execution.body)
|
|
1794
|
-
: decodeNativeMaintenanceResult(execution.body, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY));
|
|
1424
|
+
const result = decode(NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, true, () => decodeNativeMaintenanceResult(execution.body, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY));
|
|
1795
1425
|
if (execution.exitCode !== 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, true, "native authority reconciliation partially failed", execution.process, true, result.record);
|
|
1796
1426
|
return result;
|
|
1797
1427
|
}
|
|
@@ -1802,7 +1432,6 @@ export class NativeReviewCliV214 {
|
|
|
1802
1432
|
}
|
|
1803
1433
|
if (request.diagnostic !== NATIVE_REVIEW_LEGACY_ALIAS_REPAIR.DIAGNOSTIC || request.disposition !== NATIVE_REVIEW_LEGACY_ALIAS_REPAIR.DISPOSITION) throw new TypeError("Native REPAIR_LEGACY_ALIAS supports only the published historical alias diagnostic and disposition");
|
|
1804
1434
|
if (request.maintainerAuthorization !== nativeReviewLegacyAliasRepairAuthorization(request)) throw new TypeError("Native REPAIR_LEGACY_ALIAS maintainerAuthorization must match the exact repository, lineage, revision, diagnostic, disposition, actor, and reason binding");
|
|
1805
|
-
await this.verifyVersion(request.cwd, request.signal, ["repairLegacyAlias"]);
|
|
1806
1435
|
const execution = await this.execute(NATIVE_REVIEW_OPERATION.REPAIR_LEGACY_ALIAS, request.cwd, [
|
|
1807
1436
|
"review", "repair-legacy-alias", "--cwd", request.cwd,
|
|
1808
1437
|
"--lineage", request.lineage,
|
|
@@ -2005,6 +1634,19 @@ function exactConsentOption(arguments_: readonly string[], name: string): string
|
|
|
2005
1634
|
return values[0]!;
|
|
2006
1635
|
}
|
|
2007
1636
|
|
|
1637
|
+
// A Pi consent envelope is bound to Pi only when every exact provider replay
|
|
1638
|
+
// path carries exactly one `--agent pi` option. The outer envelope agent alone
|
|
1639
|
+
// cannot bind an omitted, embedded, duplicated, or conflicting invocation.
|
|
1640
|
+
function validatePiConsentChoiceAgentBindings(consent: ReviewConsentEnvelope): void {
|
|
1641
|
+
if (!("agent" in consent) || consent.agent !== "pi") return;
|
|
1642
|
+
for (const choice of consent.choices) {
|
|
1643
|
+
const arguments_ = splitNativeConsentInvocation(choice.invocation).slice(1);
|
|
1644
|
+
if (exactConsentOption(arguments_, "--agent") !== "pi") {
|
|
1645
|
+
throw new NativeReviewConsentBindingError("consent-invocation-agent-changed", "Native Pi consent invocation agent binding changed");
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
2008
1650
|
function optionalConsentLineageOption(arguments_: readonly string[]): string | undefined {
|
|
2009
1651
|
const values: string[] = [];
|
|
2010
1652
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
@@ -2029,6 +1671,7 @@ interface ConsentInvocation {
|
|
|
2029
1671
|
}
|
|
2030
1672
|
|
|
2031
1673
|
function consentInvocationArguments(request: NativeReviewConsentAnswerRequest): ConsentInvocation {
|
|
1674
|
+
validatePiConsentChoiceAgentBindings(request.consent);
|
|
2032
1675
|
const choice = request.consent.choices.find((candidate) => candidate.answer === request.answer);
|
|
2033
1676
|
if (choice === undefined) throw new NativeReviewConsentBindingError("consent-answer-unknown", "Native consent answer must be granted or declined");
|
|
2034
1677
|
const words = splitNativeConsentInvocation(choice.invocation);
|
|
@@ -2063,29 +1706,12 @@ function decodeDeclinedConsentStart(value: unknown, expected: NativeReviewConsen
|
|
|
2063
1706
|
};
|
|
2064
1707
|
}
|
|
2065
1708
|
|
|
2066
|
-
type NativeExecutableDigestResolver = (path: string) => string;
|
|
2067
|
-
const nativeCapabilitiesByDigest = new Map<string, Promise<ReviewCapabilitiesV2>>();
|
|
2068
|
-
|
|
2069
|
-
export function clearNativeReviewCapabilitiesCacheForTesting(): void {
|
|
2070
|
-
nativeCapabilitiesByDigest.clear();
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
function defaultExecutableDigest(path: string): string {
|
|
2074
|
-
const before = statSync(path);
|
|
2075
|
-
const digest = createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
2076
|
-
const after = statSync(path);
|
|
2077
|
-
if (
|
|
2078
|
-
before.dev !== after.dev ||
|
|
2079
|
-
before.ino !== after.ino ||
|
|
2080
|
-
before.size !== after.size ||
|
|
2081
|
-
before.mtimeMs !== after.mtimeMs
|
|
2082
|
-
) throw new Error("native review executable changed during capability verification");
|
|
2083
|
-
return digest;
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
1709
|
interface NegotiatedExecution {
|
|
2087
1710
|
body: Record<string, unknown>;
|
|
2088
1711
|
exitCode: number;
|
|
1712
|
+
// True only for a zero-exit bodyless operation that printed nothing: the
|
|
1713
|
+
// pinned silent acknowledgement burn. A body is never synthesized for it.
|
|
1714
|
+
silent?: true;
|
|
2089
1715
|
}
|
|
2090
1716
|
|
|
2091
1717
|
function decodeNativeAdmittedResultManifest(value: unknown): NativeReviewAdmittedResultManifest {
|
|
@@ -2133,20 +1759,18 @@ function decodeNativeProviderRoleCaptureArtifact(value: unknown): NativeReviewPr
|
|
|
2133
1759
|
}
|
|
2134
1760
|
|
|
2135
1761
|
export class NativeReviewCliV216 implements NativeReviewCli {
|
|
2136
|
-
private readonly
|
|
1762
|
+
private readonly plain: NativeReviewPlainCli;
|
|
2137
1763
|
private readonly adapter: ExecFileAdapter;
|
|
2138
1764
|
private readonly executable: string | (() => string);
|
|
2139
1765
|
private readonly timeoutMs: number;
|
|
2140
1766
|
private readonly maxBufferBytes: number;
|
|
2141
1767
|
private readonly cleanupDirectory: (directory: string) => Promise<void>;
|
|
2142
|
-
private readonly executableDigest: NativeExecutableDigestResolver;
|
|
2143
1768
|
constructor(
|
|
2144
1769
|
adapter: ExecFileAdapter,
|
|
2145
1770
|
executable: string | (() => string) = resolveGentleAiBinary,
|
|
2146
1771
|
timeoutMs = 30_000,
|
|
2147
1772
|
maxBufferBytes = resolveNativeReviewMaxBufferBytes(),
|
|
2148
1773
|
cleanupDirectory: (directory: string) => Promise<void> = (directory) => rm(directory, { recursive: true, force: true }),
|
|
2149
|
-
executableDigest: NativeExecutableDigestResolver = defaultExecutableDigest,
|
|
2150
1774
|
) {
|
|
2151
1775
|
if (typeof executable === "string" && (!isAbsolute(executable) || executable === "gentle-ai")) throw new TypeError("Native review requires an absolute package-local executable");
|
|
2152
1776
|
this.adapter = adapter;
|
|
@@ -2154,8 +1778,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2154
1778
|
this.timeoutMs = timeoutMs;
|
|
2155
1779
|
this.maxBufferBytes = maxBufferBytes;
|
|
2156
1780
|
this.cleanupDirectory = cleanupDirectory;
|
|
2157
|
-
this.
|
|
2158
|
-
this.legacy = new NativeReviewCliV214(adapter, executable, timeoutMs, maxBufferBytes, cleanupDirectory);
|
|
1781
|
+
this.plain = new NativeReviewPlainCli(adapter, executable, timeoutMs, maxBufferBytes, cleanupDirectory);
|
|
2159
1782
|
}
|
|
2160
1783
|
|
|
2161
1784
|
private executablePath(operation: NativeReviewOperation, mutating: boolean): string {
|
|
@@ -2169,15 +1792,6 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2169
1792
|
}
|
|
2170
1793
|
}
|
|
2171
1794
|
|
|
2172
|
-
private verifiedExecutable(operation: NativeReviewOperation, mutating: boolean): { path: string; digest: string } {
|
|
2173
|
-
const path = this.executablePath(operation, mutating);
|
|
2174
|
-
try {
|
|
2175
|
-
return { path, digest: this.executableDigest(path) };
|
|
2176
|
-
} catch {
|
|
2177
|
-
throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, operation, mutating, "package-local native executable identity could not be verified", undefined, false);
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
1795
|
private async invoke(
|
|
2182
1796
|
operation: NativeReviewOperation,
|
|
2183
1797
|
cwd: string,
|
|
@@ -2186,6 +1800,14 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2186
1800
|
signal: AbortSignal | undefined,
|
|
2187
1801
|
path: string,
|
|
2188
1802
|
toleratedStderr: readonly string[] = [],
|
|
1803
|
+
// A successful acknowledgement burns its authority and, on every
|
|
1804
|
+
// published release up to v2.5.0-rc.3, prints nothing: there is no
|
|
1805
|
+
// receipt left to describe. Only that shape opts out of the body, and
|
|
1806
|
+
// only for a zero exit. When such an operation does print, the bytes
|
|
1807
|
+
// are handed back as a body for the caller to decode against the one
|
|
1808
|
+
// identity it accepts (gentle-ai #3947); a non-zero exit still has to
|
|
1809
|
+
// produce its typed failure envelope like every other operation.
|
|
1810
|
+
expectsBody = true,
|
|
2189
1811
|
): Promise<NegotiatedExecution> {
|
|
2190
1812
|
let result: ExecFileResult;
|
|
2191
1813
|
try {
|
|
@@ -2198,6 +1820,10 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2198
1820
|
if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
|
|
2199
1821
|
if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
|
|
2200
1822
|
const diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.NON_ZERO, result);
|
|
1823
|
+
if (!expectsBody && result.exitCode === 0 && result.stdout.trim().length === 0) {
|
|
1824
|
+
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);
|
|
1825
|
+
return { body: {}, exitCode: result.exitCode, silent: true };
|
|
1826
|
+
}
|
|
2201
1827
|
const body = parseJson(result.stdout, operation, mutating, diagnostics);
|
|
2202
1828
|
if (result.exitCode !== 0) {
|
|
2203
1829
|
try {
|
|
@@ -2207,55 +1833,11 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2207
1833
|
throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native negotiated operation failed without a valid failure envelope", result);
|
|
2208
1834
|
}
|
|
2209
1835
|
}
|
|
2210
|
-
const forecastNarrationTolerated = operation === NATIVE_REVIEW_OPERATION.STATUS &&
|
|
1836
|
+
const forecastNarrationTolerated = operation === NATIVE_REVIEW_OPERATION.STATUS && stderrIsForecastNarration(result.stderr);
|
|
2211
1837
|
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);
|
|
2212
1838
|
return { body, exitCode: result.exitCode };
|
|
2213
1839
|
}
|
|
2214
1840
|
|
|
2215
|
-
async capabilities(request: NativeCapabilitiesRequest = {}): Promise<ReviewCapabilitiesV2> {
|
|
2216
|
-
const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.VERSION, false);
|
|
2217
|
-
const cached = nativeCapabilitiesByDigest.get(executable.digest);
|
|
2218
|
-
if (cached !== undefined) return cached;
|
|
2219
|
-
const negotiation = (async () => {
|
|
2220
|
-
const execution = await this.invoke(
|
|
2221
|
-
NATIVE_REVIEW_OPERATION.VERSION,
|
|
2222
|
-
request.cwd ?? dirname(executable.path),
|
|
2223
|
-
["review", "capabilities", "--contract", REVIEW_INTEGRATION_CONTRACT],
|
|
2224
|
-
false,
|
|
2225
|
-
request.signal,
|
|
2226
|
-
executable.path,
|
|
2227
|
-
);
|
|
2228
|
-
// Two distinct half-upgraded-install failures (Design Decision #7,
|
|
2229
|
-
// migrate-review-integration-v2): a decode failure means the installed
|
|
2230
|
-
// runtime cannot even answer this shape (an older `.gentle-ai/` payload
|
|
2231
|
-
// that still speaks `unsupported_contract` for v2); a successful decode
|
|
2232
|
-
// whose package.version disagrees with the pin names both versions.
|
|
2233
|
-
let capabilities: ReviewCapabilitiesV2;
|
|
2234
|
-
try {
|
|
2235
|
-
capabilities = decodeReviewCapabilitiesV2(execution.body, executable.digest);
|
|
2236
|
-
} catch (error) {
|
|
2237
|
-
if (error instanceof NativeReviewCliError) throw error;
|
|
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`);
|
|
2239
|
-
}
|
|
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()) {
|
|
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}`);
|
|
2247
|
-
}
|
|
2248
|
-
return capabilities;
|
|
2249
|
-
})();
|
|
2250
|
-
nativeCapabilitiesByDigest.set(executable.digest, negotiation);
|
|
2251
|
-
try {
|
|
2252
|
-
return await negotiation;
|
|
2253
|
-
} catch (error) {
|
|
2254
|
-
nativeCapabilitiesByDigest.delete(executable.digest);
|
|
2255
|
-
throw error;
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
1841
|
private async negotiated(
|
|
2260
1842
|
operation: NativeReviewOperation,
|
|
2261
1843
|
cwd: string,
|
|
@@ -2264,13 +1846,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2264
1846
|
signal?: AbortSignal,
|
|
2265
1847
|
toleratedStderr: readonly string[] = [],
|
|
2266
1848
|
): Promise<NegotiatedExecution> {
|
|
2267
|
-
|
|
2268
|
-
await this.capabilities({ cwd, ...(signal === undefined ? {} : { signal }) });
|
|
2269
|
-
const afterNegotiation = this.verifiedExecutable(operation, mutating);
|
|
2270
|
-
if (afterNegotiation.path !== executable.path || afterNegotiation.digest !== executable.digest) {
|
|
2271
|
-
throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, operation, mutating, "native executable was replaced after capability negotiation", undefined, false);
|
|
2272
|
-
}
|
|
2273
|
-
return this.invoke(operation, cwd, arguments_, mutating, signal, executable.path, toleratedStderr);
|
|
1849
|
+
return this.invoke(operation, cwd, arguments_, mutating, signal, this.executablePath(operation, mutating), toleratedStderr);
|
|
2274
1850
|
}
|
|
2275
1851
|
|
|
2276
1852
|
async start(request: NativeStartRequest): Promise<NativeStartResult> {
|
|
@@ -2278,26 +1854,35 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2278
1854
|
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
|
|
2279
1855
|
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
|
|
2280
1856
|
if (request.targetIdentity !== undefined && !/^sha256:[0-9a-f]{64}$/.test(request.targetIdentity)) throw new TypeError("Native START targetIdentity must be a canonical sha256 identity");
|
|
2281
|
-
//
|
|
2282
|
-
//
|
|
2283
|
-
//
|
|
1857
|
+
// STATUS owns the candidate binding and renders the only executable START
|
|
1858
|
+
// vector. Callers may supply a previously observed identity only to detect
|
|
1859
|
+
// drift; Pi never rebuilds that vector from request fields.
|
|
2284
1860
|
const projection = request.projection ?? "workspace";
|
|
2285
|
-
const
|
|
1861
|
+
const selection = nativeUntrackedSelection(request);
|
|
1862
|
+
const status = await this.targetStatus({
|
|
2286
1863
|
cwd: request.cwd,
|
|
2287
1864
|
projection,
|
|
2288
|
-
...(request.baseRef === undefined ? {} : { baseRef: request.baseRef }),
|
|
1865
|
+
...(request.baseRef === undefined ? {} : { baseRef: request.baseRef, committedOnly: true }),
|
|
2289
1866
|
...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }),
|
|
1867
|
+
...selection,
|
|
1868
|
+
...(request.intendedUntrackedSelection === undefined ? {} : { intendedUntrackedSelection: request.intendedUntrackedSelection }),
|
|
1869
|
+
agent: "pi",
|
|
2290
1870
|
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
2291
|
-
})
|
|
2292
|
-
const
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
1871
|
+
});
|
|
1872
|
+
const transition = status.nextTransition?.kind === "execute" && status.nextTransition.execute?.operation === "review.start"
|
|
1873
|
+
? status.nextTransition.execute
|
|
1874
|
+
: undefined;
|
|
1875
|
+
if (transition === undefined) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.START, false, "native STATUS did not offer an executable review.start transition", undefined, false);
|
|
1876
|
+
if (status.projection.projection !== projection || transition.binding.targetIdentity !== status.targetIdentity || (request.targetIdentity !== undefined && request.targetIdentity !== status.targetIdentity)) {
|
|
1877
|
+
throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, false, "native START transition target binding mismatch", undefined, false);
|
|
1878
|
+
}
|
|
1879
|
+
if (request.lineageId !== undefined && transition.binding.lineageId !== undefined && transition.binding.lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, false, "native START transition lineage binding mismatch", undefined, false);
|
|
1880
|
+
const transitionTokens = transition.arguments.map((argument) => {
|
|
1881
|
+
if (argument.token === undefined) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.START, false, "native START transition omitted an ordered argument token", undefined, false);
|
|
1882
|
+
return argument.token;
|
|
1883
|
+
});
|
|
1884
|
+
const targetIdentity = status.targetIdentity;
|
|
1885
|
+
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, ["review", "start", ...transitionTokens], true, request.signal);
|
|
2301
1886
|
// A negotiated v2 START may answer a consent question (action:
|
|
2302
1887
|
// "consent_required") instead of `start/v3` when the provider needs an
|
|
2303
1888
|
// explicit answer it cannot infer. Discriminate before decode and surface
|
|
@@ -2307,15 +1892,16 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2307
1892
|
// v2.1+) emits consent/v3, and any other identity fails closed inside
|
|
2308
1893
|
// the v3 decoder's exact identity gate.
|
|
2309
1894
|
if (execution.body.action === "consent_required") {
|
|
2310
|
-
const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () =>
|
|
2311
|
-
execution.body.schema === "gentle-ai.review-integration.consent/v2"
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
1895
|
+
const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () => {
|
|
1896
|
+
if (execution.body.schema === "gentle-ai.review-integration.consent/v2") return decodeReviewConsentV2(execution.body);
|
|
1897
|
+
const decoded = decodeReviewConsentV3(execution.body, "pi");
|
|
1898
|
+
validatePiConsentChoiceAgentBindings(decoded);
|
|
1899
|
+
return decoded;
|
|
1900
|
+
});
|
|
2315
1901
|
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");
|
|
2316
1902
|
throw new NativeReviewConsentRequiredError(consent);
|
|
2317
1903
|
}
|
|
2318
|
-
const result = decode(NATIVE_REVIEW_OPERATION.START, true, () =>
|
|
1904
|
+
const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartResponse(execution.body));
|
|
2319
1905
|
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");
|
|
2320
1906
|
const resultTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
|
|
2321
1907
|
if (resultTarget !== undefined && resultTarget !== targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start target mismatch");
|
|
@@ -2330,6 +1916,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2330
1916
|
action: result.action as NativeStartAction,
|
|
2331
1917
|
lensesRequired: result.lensesRequired,
|
|
2332
1918
|
riskReasons: result.riskReasons.map((reason) => ({ ...reason })),
|
|
1919
|
+
...("nextTransition" in result && result.nextTransition !== undefined ? { nextTransition: result.nextTransition } : {}),
|
|
2333
1920
|
// Derived, not received. `risk_reasons` is a required start/v2 field
|
|
2334
1921
|
// already recomputed against the authoritative frozen snapshot, so
|
|
2335
1922
|
// these phrases describe the same candidate the lenses will review.
|
|
@@ -2353,7 +1940,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2353
1940
|
if (request.answer === NATIVE_REVIEW_CONSENT_ANSWER.DECLINED) {
|
|
2354
1941
|
return decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeDeclinedConsentStart(execution.body, request));
|
|
2355
1942
|
}
|
|
2356
|
-
const result = decode(NATIVE_REVIEW_OPERATION.START, true, () =>
|
|
1943
|
+
const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartResponse(execution.body));
|
|
2357
1944
|
const answeredTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
|
|
2358
1945
|
if (answeredTarget !== request.consent.targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer target mismatch");
|
|
2359
1946
|
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");
|
|
@@ -2368,6 +1955,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2368
1955
|
action: result.action as NativeStartAction,
|
|
2369
1956
|
lensesRequired: result.lensesRequired,
|
|
2370
1957
|
riskReasons: result.riskReasons.map((reason) => ({ ...reason })),
|
|
1958
|
+
...("nextTransition" in result && result.nextTransition !== undefined ? { nextTransition: result.nextTransition } : {}),
|
|
2371
1959
|
...(() => {
|
|
2372
1960
|
const evidence = nativeRiskEvidencePhrases(result.riskLevel, result.riskReasons);
|
|
2373
1961
|
return evidence.length === 0 ? {} : { riskEvidence: evidence };
|
|
@@ -2376,120 +1964,23 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2376
1964
|
} };
|
|
2377
1965
|
}
|
|
2378
1966
|
|
|
2379
|
-
private async stageDocument(directory: string, name: string, document: unknown): Promise<string> {
|
|
2380
|
-
const path = join(directory, `${name}.json`);
|
|
2381
|
-
await writeFile(path, JSON.stringify(document), { encoding: "utf8", mode: 0o600 });
|
|
2382
|
-
await chmod(path, 0o600);
|
|
2383
|
-
return path;
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
private async stageEvidence(directory: string, evidence: string): Promise<string> {
|
|
2387
|
-
const path = join(directory, "evidence.txt");
|
|
2388
|
-
await writeFile(path, evidence, { encoding: "utf8", mode: 0o600 });
|
|
2389
|
-
await chmod(path, 0o600);
|
|
2390
|
-
return path;
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
async finalize(request: NativeFinalizeRequest): Promise<NativeFinalizeResult> {
|
|
2394
|
-
if (request.evidenceDocument !== undefined && request.evidenceDocument.length === 0) throw new TypeError("Native FINALIZE evidence must contain at least one byte");
|
|
2395
|
-
const needsStaging = request.lensResults !== undefined || request.refuterDocument !== undefined || request.validationDocument !== undefined || request.evidenceDocument !== undefined;
|
|
2396
|
-
const directory = needsStaging ? await mkdtemp(join(tmpdir(), "gentle-ai-finalize-")) : undefined;
|
|
2397
|
-
try {
|
|
2398
|
-
if (directory !== undefined) await chmod(directory, 0o700);
|
|
2399
|
-
const resultFiles = directory !== undefined && request.lensResults !== undefined ? await Promise.all(request.lensResults.map((entry, index) => this.stageDocument(directory, `result-${index}`, entry.document))) : request.resultFiles ?? [];
|
|
2400
|
-
const refuterFile = directory !== undefined && request.refuterDocument !== undefined ? await this.stageDocument(directory, "refuter", request.refuterDocument) : request.refuterFile;
|
|
2401
|
-
const validationFile = directory !== undefined && request.validationDocument !== undefined ? await this.stageDocument(directory, "validation", request.validationDocument) : request.validationFile;
|
|
2402
|
-
const evidenceFile = directory !== undefined && request.evidenceDocument !== undefined ? await this.stageEvidence(directory, request.evidenceDocument) : request.evidenceFile;
|
|
2403
|
-
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
|
|
2404
|
-
"review", "finalize", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
|
|
2405
|
-
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
2406
|
-
// `--result` is RETIRED. A reviewer result supplied that way carries
|
|
2407
|
-
// no provider-owned admission, so it cannot prove the lens inspected
|
|
2408
|
-
// the frozen candidate. Results reach authority through
|
|
2409
|
-
// `review capture-result`, and FINALIZE either discovers them
|
|
2410
|
-
// (`--captured-results`) or is handed each manifest in lens order.
|
|
2411
|
-
...(request.capturedResults === true ? ["--captured-results=true"] : []),
|
|
2412
|
-
...(request.resultArtifactFiles ?? []).flatMap((path) => ["--result-artifact-file", path]),
|
|
2413
|
-
...(refuterFile === undefined ? [] : ["--refuter", refuterFile]),
|
|
2414
|
-
...(request.correctionLines === undefined ? [] : ["--correction-lines", String(request.correctionLines)]),
|
|
2415
|
-
...(validationFile === undefined ? [] : ["--validation", validationFile]),
|
|
2416
|
-
...(evidenceFile === undefined ? [] : ["--evidence", evidenceFile]),
|
|
2417
|
-
...(request.failed === true ? ["--failed"] : []),
|
|
2418
|
-
], true, request.signal);
|
|
2419
|
-
const envelope = decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => decodeReviewOperationV2(execution.body));
|
|
2420
|
-
if (envelope.operation !== "review.finalize") throw new Error("wrong finalize operation envelope");
|
|
2421
|
-
const body = envelope.result;
|
|
2422
|
-
const lineageId = requiredString(body.lineage_id);
|
|
2423
|
-
if (request.lineageId !== undefined && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.FINALIZE, true, "native finalize lineage mismatch");
|
|
2424
|
-
return {
|
|
2425
|
-
lineageId,
|
|
2426
|
-
state: requiredString(body.state),
|
|
2427
|
-
action: requiredString(body.action),
|
|
2428
|
-
storeRevision: requiredString(body.store_revision),
|
|
2429
|
-
// Present only when state is correction_required (validated by
|
|
2430
|
-
// decodeReviewOperationV2); carried through as the raw validated
|
|
2431
|
-
// shape rather than re-decoded, since the typed decoder for this
|
|
2432
|
-
// nested object is private to lib/review-integration-v2.ts.
|
|
2433
|
-
...(body.validation_request === undefined ? {} : { validationRequest: body.validation_request as Readonly<Record<string, unknown>> }),
|
|
2434
|
-
...(body.escalation === undefined ? {} : { escalation: requiredString(body.escalation) }),
|
|
2435
|
-
};
|
|
2436
|
-
} finally {
|
|
2437
|
-
if (directory !== undefined) await this.cleanupDirectory(directory).catch(() => undefined);
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
async validate(request: NativeValidateRequest): Promise<NativeValidateResult> {
|
|
2442
|
-
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.VALIDATE, request.cwd, [
|
|
2443
|
-
"review", "validate", "--contract", REVIEW_INTEGRATION_CONTRACT, "--gate", request.gate, "--cwd", request.cwd,
|
|
2444
|
-
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
2445
|
-
...(request.flags ?? []),
|
|
2446
|
-
], false, request.signal);
|
|
2447
|
-
const envelope = decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => decodeReviewOperationV2(execution.body));
|
|
2448
|
-
if (envelope.operation !== "review.validate") throw new Error("wrong validate operation envelope");
|
|
2449
|
-
const body = envelope.result;
|
|
2450
|
-
const gateContext = decodeGateContext(body.context);
|
|
2451
|
-
return {
|
|
2452
|
-
allowed: booleanValue(body.allowed),
|
|
2453
|
-
result: enumString(body.result, NATIVE_GATE_RESULT) as NativeValidateResult["result"],
|
|
2454
|
-
action: requiredString(body.action),
|
|
2455
|
-
reason: requiredString(body.reason),
|
|
2456
|
-
gateContext,
|
|
2457
|
-
...(body.delivery === undefined ? {} : { delivery: enumString(body.delivery, ["disabled/unmanaged"]) as NativeValidateResult["delivery"] }),
|
|
2458
|
-
};
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
|
-
async bindSdd(request: NativeBindSddRequest): Promise<NativeBindSddResult> {
|
|
2462
|
-
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.BIND_SDD, request.cwd, [
|
|
2463
|
-
"review", "bind-sdd", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
|
|
2464
|
-
"--change", request.change, "--lineage", request.lineage,
|
|
2465
|
-
`--expected-binding-revision=${request.expectedBindingRevision}`,
|
|
2466
|
-
], true, request.signal);
|
|
2467
|
-
const envelope = decode(NATIVE_REVIEW_OPERATION.BIND_SDD, true, () => decodeReviewOperationV2(execution.body));
|
|
2468
|
-
if (envelope.operation !== "review.bind_sdd") throw new Error("wrong bind-sdd operation envelope");
|
|
2469
|
-
const body = envelope.result;
|
|
2470
|
-
const gateContext = decodeGateContext(body.gate_context);
|
|
2471
|
-
const lineage = requiredString(body.lineage);
|
|
2472
|
-
const change = requiredString(body.change);
|
|
2473
|
-
if (lineage !== request.lineage || change !== request.change || gateContext.raw.gate !== "post-apply") throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.BIND_SDD, true, "native binding identity mismatch");
|
|
2474
|
-
return {
|
|
2475
|
-
revision: requiredString(body.revision),
|
|
2476
|
-
change,
|
|
2477
|
-
lineage,
|
|
2478
|
-
authorityRevision: requiredString(body.authority_revision),
|
|
2479
|
-
receiptHash: requiredString(body.receipt_hash),
|
|
2480
|
-
gateContext,
|
|
2481
|
-
};
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
1967
|
async targetStatus(request: NativeTargetStatusRequest): Promise<ReviewStatusV3> {
|
|
2485
|
-
|
|
1968
|
+
if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native STATUS baseRef must be a non-empty, trimmed, NUL-free string");
|
|
1969
|
+
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native STATUS baseRef requires explicit committedOnly acknowledgement");
|
|
1970
|
+
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native STATUS committedOnly requires an explicit baseRef");
|
|
1971
|
+
const selection = nativeUntrackedSelection(request);
|
|
1972
|
+
const submitted = request.intendedUntrackedSelection;
|
|
1973
|
+
if (submitted !== undefined && submitted.argumentTokens.reduce((count, token) => count + token.split("{{value}}").length - 1, 0) !== 1) throw new TypeError("Native intended-untracked selection requires exactly one provider-issued {{value}} token");
|
|
1974
|
+
const statusArguments = submitted === undefined ? [
|
|
2486
1975
|
"review", "status", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
|
|
2487
1976
|
"--projection", request.projection ?? "workspace",
|
|
2488
|
-
...(
|
|
1977
|
+
...nativeUntrackedSelectionArguments(selection),
|
|
1978
|
+
...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
|
|
2489
1979
|
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
2490
1980
|
...(request.agent === undefined ? [] : ["--agent", request.agent]),
|
|
2491
1981
|
"--next-transition",
|
|
2492
|
-
],
|
|
1982
|
+
] : ["review", "status", "--cwd", request.cwd, ...submitted.argumentTokens.map((token) => token.replaceAll("{{value}}", submitted.value))];
|
|
1983
|
+
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, statusArguments, false, request.signal);
|
|
2493
1984
|
assertSupportedNextTransitionOperation(execution.body);
|
|
2494
1985
|
return decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeReviewStatusV3(execution.body));
|
|
2495
1986
|
}
|
|
@@ -2529,7 +2020,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2529
2020
|
// --contract, and the verification-evidence/v2 record returned DIRECTLY —
|
|
2530
2021
|
// not wrapped in an operation/v2 envelope. Evidence is staged through the
|
|
2531
2022
|
// same 0o600 tmpfile discipline FINALIZE uses.
|
|
2532
|
-
async captureResult(request: NativeReviewCaptureResultRequest): Promise<
|
|
2023
|
+
async captureResult(request: NativeReviewCaptureResultRequest): Promise<NativeReviewCaptureResultOutcome> {
|
|
2533
2024
|
if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_RESULT requires the provider-issued argument tokens");
|
|
2534
2025
|
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");
|
|
2535
2026
|
if (request.resultDocument.length === 0) throw new TypeError("Native CAPTURE_RESULT result document must contain at least one byte");
|
|
@@ -2541,19 +2032,67 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2541
2032
|
const resultFile = join(directory, "result.json");
|
|
2542
2033
|
await writeFile(resultFile, request.resultDocument, { encoding: "utf8", mode: 0o600 });
|
|
2543
2034
|
await chmod(resultFile, 0o600);
|
|
2544
|
-
const executable = this.
|
|
2035
|
+
const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true);
|
|
2545
2036
|
const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, request.cwd ?? process.cwd(), [
|
|
2546
2037
|
"review", "capture-result",
|
|
2547
2038
|
...request.argumentTokens,
|
|
2548
2039
|
...(carriesContext || request.cwd === undefined ? [] : ["--cwd", request.cwd]),
|
|
2549
2040
|
"--input", resultFile,
|
|
2550
|
-
], true, request.signal, executable
|
|
2551
|
-
return decode(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true, () =>
|
|
2041
|
+
], true, request.signal, executable);
|
|
2042
|
+
return decode(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true, () => {
|
|
2043
|
+
const body = object(execution.body);
|
|
2044
|
+
if (body.schema === "gentle-ai.review-last-event-closure/v1") {
|
|
2045
|
+
const closure = decodeReviewLastEventClosureV1(body);
|
|
2046
|
+
if (closure.operation !== "review/capture-result") throw new TypeError("capture-result returned a closure for another operation");
|
|
2047
|
+
return closure;
|
|
2048
|
+
}
|
|
2049
|
+
return decodeNativeAdmittedResultManifest(body);
|
|
2050
|
+
});
|
|
2552
2051
|
} finally {
|
|
2553
2052
|
await this.cleanupDirectory(directory).catch(() => undefined);
|
|
2554
2053
|
}
|
|
2555
2054
|
}
|
|
2556
2055
|
|
|
2056
|
+
// Relays the provider-issued acknowledgement exactly as rendered. A success
|
|
2057
|
+
// burns the authority and its artifacts; a refusal still arrives as the
|
|
2058
|
+
// ordinary typed failure envelope. Two success shapes exist and both are
|
|
2059
|
+
// the same burn: every published release up to v2.5.0-rc.3 prints nothing
|
|
2060
|
+
// (resolved as `undefined`, byte-for-byte the pinned behaviour), and
|
|
2061
|
+
// gentle-ai #3947 prints one `gentle-ai.review-acknowledged/v1` envelope
|
|
2062
|
+
// naming the burned lineage, target, and consumed revision. Any other
|
|
2063
|
+
// output on a zero exit is schema-incompatible with the mutation outcome
|
|
2064
|
+
// unknown, exactly like a malformed terminal closure: the caller
|
|
2065
|
+
// reconciles through STATUS and never infers the burn from prose.
|
|
2066
|
+
async acknowledgeApproved(request: NativeReviewAcknowledgeApprovedRequest): Promise<NativeReviewAcknowledgeApprovedOutcome> {
|
|
2067
|
+
if (request.argumentTokens.length === 0) throw new TypeError("Native ACKNOWLEDGE_APPROVED requires the provider-issued argument tokens");
|
|
2068
|
+
if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native ACKNOWLEDGE_APPROVED argument tokens must all be non-empty strings");
|
|
2069
|
+
if (request.argumentTokens.some((token) => token.includes("{{value}}"))) throw new TypeError("Native ACKNOWLEDGE_APPROVED argument tokens carry no caller-substituted value");
|
|
2070
|
+
const executable = this.executablePath(NATIVE_REVIEW_OPERATION.ACKNOWLEDGE_APPROVED, true);
|
|
2071
|
+
const execution = await this.invoke(NATIVE_REVIEW_OPERATION.ACKNOWLEDGE_APPROVED, request.cwd, [
|
|
2072
|
+
"review", "acknowledge-approved", ...request.argumentTokens,
|
|
2073
|
+
], true, request.signal, executable, [], false);
|
|
2074
|
+
if (execution.silent) return undefined;
|
|
2075
|
+
return decode(NATIVE_REVIEW_OPERATION.ACKNOWLEDGE_APPROVED, true, () => decodeReviewAcknowledgedV1(execution.body, request.binding ?? {}));
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
async captureCorrectionPlan(request: NativeReviewCorrectionPlanCaptureRequest): Promise<ReviewLastEventClosureV1> {
|
|
2079
|
+
if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_CORRECTION_PLAN requires the provider-issued argument tokens");
|
|
2080
|
+
if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native CAPTURE_CORRECTION_PLAN argument tokens must all be non-empty strings");
|
|
2081
|
+
if (!Number.isSafeInteger(request.correctionLines) || request.correctionLines < 1 || request.correctionLines > 200) throw new TypeError("Native CAPTURE_CORRECTION_PLAN correction lines must be a positive integer up to 200");
|
|
2082
|
+
const valueIndex = request.argumentTokens.findIndex((token) => token.includes("{{value}}"));
|
|
2083
|
+
if (valueIndex < 0 || request.argumentTokens.filter((token) => token.includes("{{value}}")).length !== 1) throw new TypeError("Native CAPTURE_CORRECTION_PLAN requires exactly one provider-issued {{value}} token");
|
|
2084
|
+
const argumentTokens = request.argumentTokens.map((token, index) => index === valueIndex ? token.replaceAll("{{value}}", String(request.correctionLines)) : token);
|
|
2085
|
+
const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_CORRECTION_PLAN, true);
|
|
2086
|
+
const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_CORRECTION_PLAN, request.cwd, [
|
|
2087
|
+
"review", "capture-correction-plan", ...argumentTokens,
|
|
2088
|
+
], true, request.signal, executable);
|
|
2089
|
+
return decode(NATIVE_REVIEW_OPERATION.CAPTURE_CORRECTION_PLAN, true, () => {
|
|
2090
|
+
const closure = decodeReviewLastEventClosureV1(execution.body);
|
|
2091
|
+
if (closure.operation !== "review.capture-correction-plan") throw new TypeError("capture-correction-plan returned a closure for another operation");
|
|
2092
|
+
return closure;
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2557
2096
|
// gentle-pi#311 P4-roles: executes one provider-rendered self-contained
|
|
2558
2097
|
// role capture vector exactly as rendered — one CLI invocation, verbatim
|
|
2559
2098
|
// tokens in provider order, in the foreground. Go materializes the role
|
|
@@ -2562,17 +2101,26 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2562
2101
|
// --cwd: the vector's --repository-context is authoritative and mutually
|
|
2563
2102
|
// exclusive with a path). The central adapter injects the relay handshake
|
|
2564
2103
|
// environment on this spawn like on every other gentle-ai invocation.
|
|
2565
|
-
async captureProviderRole(request: NativeReviewProviderRoleCaptureRequest): Promise<
|
|
2566
|
-
|
|
2567
|
-
|
|
2104
|
+
async captureProviderRole(request: NativeReviewProviderRoleCaptureRequest): Promise<NativeReviewProviderRoleCaptureOutcome> {
|
|
2105
|
+
if (!isNativeReviewProviderRoleCaptureOperation(request.captureOperation)) throw new TypeError(`Native CAPTURE_PROVIDER_ROLE supports only review.capture-refuter and review.capture-validation, received ${JSON.stringify(request.captureOperation)}`);
|
|
2106
|
+
const verb = request.captureOperation.slice("review.".length);
|
|
2107
|
+
const expectedClosureOperation = NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_CLOSURE_OPERATION[request.captureOperation];
|
|
2568
2108
|
if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_PROVIDER_ROLE requires the provider-rendered argument tokens");
|
|
2569
2109
|
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.
|
|
2110
|
+
const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true);
|
|
2571
2111
|
const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, request.cwd, [
|
|
2572
2112
|
"review", verb,
|
|
2573
2113
|
...request.argumentTokens,
|
|
2574
|
-
], true, request.signal, executable
|
|
2575
|
-
return decode(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true, () =>
|
|
2114
|
+
], true, request.signal, executable);
|
|
2115
|
+
return decode(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true, () => {
|
|
2116
|
+
const body = object(execution.body);
|
|
2117
|
+
if (body.schema === "gentle-ai.review-last-event-closure/v1") {
|
|
2118
|
+
const closure = decodeReviewLastEventClosureV1(body);
|
|
2119
|
+
if (closure.operation !== expectedClosureOperation) throw new TypeError("provider role capture returned a closure for another operation");
|
|
2120
|
+
return closure;
|
|
2121
|
+
}
|
|
2122
|
+
return decodeNativeProviderRoleCaptureArtifact(body);
|
|
2123
|
+
});
|
|
2576
2124
|
}
|
|
2577
2125
|
|
|
2578
2126
|
// gentle-pi#311 P5: executes one provider-rendered `review.finalize`
|
|
@@ -2581,15 +2129,6 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2581
2129
|
// --captured-results=true`); Pi assembles no reviewer, refuter, or
|
|
2582
2130
|
// validator documents for this lane — the provider discovers its own
|
|
2583
2131
|
// 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
2132
|
|
|
2594
2133
|
// Same misbinding class as capture-evidence (live smoke, 2026-08-16): the
|
|
2595
2134
|
// correction PLAN and TARGETED VALIDATION collect slots render `finalize`
|
|
@@ -2600,92 +2139,8 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2600
2139
|
// intent reconciliation, so the tokens pass through verbatim with only the
|
|
2601
2140
|
// {{value}} substitution: a literal for correction_lines, a staged 0o600
|
|
2602
2141
|
// 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
2142
|
|
|
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
2143
|
|
|
2669
|
-
async captureEvidence(request: NativeReviewCaptureEvidenceRequest): Promise<NativeReviewVerificationEvidenceV2> {
|
|
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");
|
|
2671
|
-
if (request.evidenceDocument.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE evidence must contain at least one byte");
|
|
2672
|
-
const directory = await mkdtemp(join(tmpdir(), "gentle-ai-capture-evidence-"));
|
|
2673
|
-
try {
|
|
2674
|
-
await chmod(directory, 0o700);
|
|
2675
|
-
const evidenceFile = await this.stageEvidence(directory, request.evidenceDocument);
|
|
2676
|
-
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, request.cwd, [
|
|
2677
|
-
"review", "capture-evidence", "--cwd", request.cwd,
|
|
2678
|
-
"--lineage", request.lineageId,
|
|
2679
|
-
"--target", request.targetIdentity,
|
|
2680
|
-
"--expected-revision", request.expectedRevision,
|
|
2681
|
-
"--outcome", request.outcome,
|
|
2682
|
-
"--input", evidenceFile,
|
|
2683
|
-
], true, request.signal);
|
|
2684
|
-
return decode(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, true, () => decodeNativeReviewVerificationEvidence(execution.body));
|
|
2685
|
-
} finally {
|
|
2686
|
-
await this.cleanupDirectory(directory).catch(() => undefined);
|
|
2687
|
-
}
|
|
2688
|
-
}
|
|
2689
2144
|
|
|
2690
2145
|
// Field defect (fambig, 2026-08-16): at every evidence-pending sub-state
|
|
2691
2146
|
// the collect slot renders the identity native demands — for a correction
|
|
@@ -2694,85 +2149,62 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2694
2149
|
// {{outcome}} and {{input}} slots substituted. Same verbatim-token
|
|
2695
2150
|
// discipline as captureResult; --repository-context is authoritative and
|
|
2696
2151
|
// 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
2152
|
|
|
2729
|
-
reviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult> {
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2153
|
+
async reviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult> {
|
|
2154
|
+
const execution = await this.invoke(
|
|
2155
|
+
NATIVE_REVIEW_OPERATION.STATUS,
|
|
2156
|
+
request.cwd,
|
|
2157
|
+
["review", "status", "--cwd", request.cwd],
|
|
2158
|
+
false,
|
|
2159
|
+
request.signal,
|
|
2160
|
+
this.executablePath(NATIVE_REVIEW_OPERATION.STATUS, false),
|
|
2161
|
+
);
|
|
2162
|
+
const status = decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeNativeReviewStatus(execution.body));
|
|
2163
|
+
if (!await repositoriesMatch(request.cwd, status.repository)) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.STATUS, false, "native review status repository mismatch");
|
|
2164
|
+
return status;
|
|
2735
2165
|
}
|
|
2736
2166
|
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2167
|
+
async reviewMode(request: NativeReviewModeRequest): Promise<NativeReviewModeResult> {
|
|
2168
|
+
const cwd = await canonicalNativeReviewCwd(request.cwd);
|
|
2169
|
+
const mutating = request.operation !== NATIVE_REVIEW_MODE_OPERATION.STATUS;
|
|
2170
|
+
const execution = await this.invoke(
|
|
2171
|
+
NATIVE_REVIEW_OPERATION.MODE,
|
|
2172
|
+
cwd,
|
|
2173
|
+
["review", "mode", request.operation, "--cwd", cwd, ...(mutating ? ["--scope", "clone"] : []), "--json"],
|
|
2174
|
+
mutating,
|
|
2175
|
+
request.signal,
|
|
2176
|
+
this.executablePath(NATIVE_REVIEW_OPERATION.MODE, mutating),
|
|
2177
|
+
);
|
|
2178
|
+
return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(execution.body, request.operation));
|
|
2742
2179
|
}
|
|
2743
2180
|
|
|
2744
2181
|
// Recovery commands are version-gated plain CLI operations outside the
|
|
2745
|
-
// negotiated integration-v1 contract, exactly like reviewStatus
|
|
2182
|
+
// negotiated integration-v1 contract, exactly like reviewStatus.
|
|
2746
2183
|
reclaim(request: NativeReviewReclaimRequest): Promise<NativeReviewRecoveryResult> {
|
|
2747
|
-
return this.
|
|
2184
|
+
return this.plain.reclaim(request);
|
|
2748
2185
|
}
|
|
2749
2186
|
|
|
2750
2187
|
recover(request: NativeReviewRecoverRequest): Promise<NativeReviewRecoveryResult> {
|
|
2751
|
-
return this.
|
|
2188
|
+
return this.plain.recover(request);
|
|
2752
2189
|
}
|
|
2753
2190
|
|
|
2754
2191
|
abandon(request: NativeReviewAbandonRequest): Promise<NativeReviewRecoveryResult> {
|
|
2755
|
-
return this.
|
|
2192
|
+
return this.plain.abandon(request);
|
|
2756
2193
|
}
|
|
2757
2194
|
|
|
2758
2195
|
quarantineLegacy(request: NativeReviewLegacyQuarantineRequest): Promise<NativeReviewRecoveryResult> {
|
|
2759
|
-
return this.
|
|
2196
|
+
return this.plain.quarantineLegacy(request);
|
|
2760
2197
|
}
|
|
2761
2198
|
|
|
2762
2199
|
reconcileAuthority(request: NativeReviewReconcileAuthorityRequest): Promise<NativeReviewRecoveryResult> {
|
|
2763
|
-
return this.
|
|
2200
|
+
return this.plain.reconcileAuthority(request);
|
|
2764
2201
|
}
|
|
2765
2202
|
|
|
2766
2203
|
repairLegacyAlias(request: NativeReviewLegacyAliasRepairRequest): Promise<NativeReviewRecoveryResult> {
|
|
2767
|
-
return this.
|
|
2204
|
+
return this.plain.repairLegacyAlias(request);
|
|
2768
2205
|
}
|
|
2769
2206
|
}
|
|
2770
2207
|
|
|
2771
2208
|
export function createNativeReviewCli(adapter?: ExecFileAdapter, executable: string | (() => string) = resolveGentleAiBinary): NativeReviewCli {
|
|
2772
|
-
|
|
2773
|
-
return new NativeReviewCliV216(createNodeExecFileAdapter(), executable);
|
|
2209
|
+
return new NativeReviewCliV216(adapter ?? createNodeExecFileAdapter(), executable);
|
|
2774
2210
|
}
|
|
2775
|
-
|
|
2776
|
-
// Response-schema fixtures remain v2.1.3 historical contracts; the production
|
|
2777
|
-
// client above accepts only the v2.1.4 runtime release.
|
|
2778
|
-
export { NativeReviewCliV214 as NativeReviewCliV213 };
|