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