gentle-pi 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -12
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -1
- package/assets/agents/review-reliability.md +4 -1
- package/assets/agents/review-resilience.md +4 -1
- package/assets/agents/review-risk.md +4 -1
- package/assets/agents/sdd-apply.md +6 -1
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +6 -1
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +6 -1
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +6 -1
- package/assets/agents/sdd-verify.md +6 -1
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +246 -67
- package/assets/orchestrator.md +7 -14
- package/assets/sdd-orchestrator-workflow.md +154 -9
- package/assets/support/sdd-status-contract.md +19 -1
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +5 -5
- package/docs/review-integration.md +22 -2
- package/extensions/gentle-ai.ts +1595 -201
- package/extensions/sdd-init.ts +19 -6
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/git-commit-transaction.ts +77 -17
- package/lib/native-review-cli.ts +528 -65
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +527 -18
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +436 -0
- package/lib/review-integration-v2.ts +537 -36
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +53 -1
- package/package.json +5 -2
- package/runtime/gentle-ai-binary.mjs +173 -2
- package/runtime/git-commit-transaction.mjs +75 -15
- package/runtime/native-review-cli.mjs +524 -61
- package/runtime/review-integration-v2.mjs +536 -35
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/build-git-commit-transaction-runner.mjs +1 -0
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +23 -13
- package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/verify-package-files.mjs +110 -33
- package/skills/_shared/review-ledger-contract.md +4 -6
- package/skills/gentle-ai/SKILL.md +4 -4
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +7 -1
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/crosslane/cross-lane.mjs +1169 -0
- package/tests/delegated-key-learnings-contract.test.ts +238 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +336 -0
- package/tests/gentle-ai-installer.test.ts +46 -46
- package/tests/git-commit-transaction.test.ts +229 -1
- package/tests/maintainer/provider-relay.maintest.ts +265 -0
- package/tests/native-review-capability-contract.test.ts +48 -2
- package/tests/native-review-cli.test.ts +56 -0
- package/tests/native-review-consent.test.ts +164 -3
- package/tests/native-review-parity-runtime.test.ts +37 -0
- package/tests/native-review-parity.test.ts +218 -15
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/orchestrator-budget.test.ts +30 -5
- package/tests/package-manifest.test.ts +98 -72
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +355 -0
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-candidate-view.test.ts +489 -9
- package/tests/review-compact-contract.test.ts +52 -119
- package/tests/review-controller-native-recovery.test.ts +643 -47
- package/tests/review-controller-native-routing.test.ts +1667 -222
- package/tests/review-controller-workspace-root.test.ts +17 -2
- package/tests/review-corrected-finalize-binding.test.ts +175 -0
- package/tests/review-dispatch-hydration-gap.test.ts +197 -0
- package/tests/review-host-relay-routing.test.ts +317 -0
- package/tests/review-host-relay.test.ts +520 -0
- package/tests/review-integration-v2-forward.test.ts +631 -0
- package/tests/review-integration-v2.test.ts +114 -0
- package/tests/review-ledger-contract.test.ts +12 -28
- package/tests/review-recovered-lineage-routing.test.ts +246 -0
- package/tests/review-relay-transport-agent.test.ts +249 -0
- package/tests/runtime-harness.mjs +242 -14
- package/tests/sdd-agent-tools.test.ts +18 -33
- package/tests/skill-collision-prefixes.test.ts +1 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -6,14 +6,17 @@ 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 { GENTLE_AI_VERSION, PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.mjs";
|
|
9
|
+
import { GENTLE_AI_VERSION, PackageLocalGentleAiBinaryMissingError, gentleAiDevBinaryOverrideConfigured, resolveGentleAiBinary } from "./gentle-ai-binary.mjs";
|
|
10
|
+
import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.mjs";
|
|
10
11
|
import {
|
|
11
12
|
REVIEW_INTEGRATION_CONTRACT,
|
|
12
13
|
decodeReviewCapabilitiesV2,
|
|
13
14
|
decodeReviewConsentV2,
|
|
15
|
+
decodeReviewConsentV3,
|
|
14
16
|
decodeReviewFailureV2,
|
|
15
17
|
decodeReviewOperationV2,
|
|
16
18
|
decodeReviewRepairV2,
|
|
19
|
+
decodeReviewResultArtifactV2,
|
|
17
20
|
decodeReviewStartV3,
|
|
18
21
|
decodeReviewStatusV3,
|
|
19
22
|
|
|
@@ -26,6 +29,23 @@ import {
|
|
|
26
29
|
|
|
27
30
|
const execFileAsync = promisify(execFile);
|
|
28
31
|
|
|
32
|
+
// Negotiated review/status responses can carry a complete authority inventory.
|
|
33
|
+
// Keep the production default large enough for that payload while retaining a
|
|
34
|
+
// hard 64 MiB ceiling even when GENTLE_PI_REVIEW_MAX_BUFFER_BYTES is set.
|
|
35
|
+
export const NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
36
|
+
const NATIVE_REVIEW_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
37
|
+
const NATIVE_REVIEW_MAX_BUFFER_BYTES_ENV = "GENTLE_PI_REVIEW_MAX_BUFFER_BYTES";
|
|
38
|
+
const NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT = "Inspect native review state before any new START; GENTLE_PI_REVIEW_MAX_BUFFER_BYTES accepts a positive decimal up to 67108864.";
|
|
39
|
+
|
|
40
|
+
function resolveNativeReviewMaxBufferBytes(environment = process.env) {
|
|
41
|
+
const value = environment[NATIVE_REVIEW_MAX_BUFFER_BYTES_ENV];
|
|
42
|
+
if (value === undefined || !/^[1-9]\d*$/.test(value)) return NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES;
|
|
43
|
+
const parsed = Number(value);
|
|
44
|
+
return Number.isSafeInteger(parsed) && parsed <= NATIVE_REVIEW_MAX_BUFFER_BYTES
|
|
45
|
+
? parsed
|
|
46
|
+
: NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES;
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
export const NATIVE_REVIEW_OPERATION = {
|
|
30
50
|
VERSION: "version",
|
|
31
51
|
START: "review/start",
|
|
@@ -44,6 +64,7 @@ export const NATIVE_REVIEW_OPERATION = {
|
|
|
44
64
|
REPAIR: "review/repair",
|
|
45
65
|
CAPTURE_EVIDENCE: "review/capture-evidence",
|
|
46
66
|
CAPTURE_RESULT: "review/capture-result",
|
|
67
|
+
CAPTURE_PROVIDER_ROLE: "review/capture-provider-role",
|
|
47
68
|
} ;
|
|
48
69
|
|
|
49
70
|
|
|
@@ -104,6 +125,22 @@ export const NATIVE_SDD_ARTIFACT_STATE = {
|
|
|
104
125
|
|
|
105
126
|
|
|
106
127
|
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
107
144
|
|
|
108
145
|
|
|
109
146
|
|
|
@@ -176,18 +213,24 @@ export const NATIVE_REVIEW_MODE_SCOPE = {
|
|
|
176
213
|
|
|
177
214
|
// Exact-match tolerated-stderr allowlist for START only, gated on the `mode`
|
|
178
215
|
// capability being true (Design Decision #6, organic-rdd-parity). Byte-exact
|
|
179
|
-
// against gentle-ai's headless
|
|
180
|
-
// reviewConsentSkippedNotice) written when the switch is on
|
|
181
|
-
// terminal answered the one-time consent question — which
|
|
182
|
-
// Pi spawns gentle-ai without a TTY. Any other text still
|
|
183
|
-
// UNEXPECTED_STDERR.
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
216
|
+
// against gentle-ai's headless notices (internal/cli/review_mode.go
|
|
217
|
+
// reviewConsentSkippedNotice and its siblings) written when the switch is on
|
|
218
|
+
// but no interactive terminal answered the one-time consent question — which
|
|
219
|
+
// is always true when Pi spawns gentle-ai without a TTY. Any other text still
|
|
220
|
+
// fails closed as UNEXPECTED_STDERR.
|
|
221
|
+
//
|
|
222
|
+
// Each entry is one whole line the PINNED gentle-ai may write to the console
|
|
223
|
+
// stream while still succeeding. That last qualifier is what keeps this list
|
|
224
|
+
// honest: v2.4.0 deleted reviewConsentSkippedDefaultProvenance ("Reviews are
|
|
225
|
+
// on by default; this was never explicitly chosen. ..."), which used to ride
|
|
226
|
+
// with the skip notice whenever the resolved mode source was `default`. Under
|
|
227
|
+
// opt-in receipt-driven development a default-source clone is refused long
|
|
228
|
+
// before the consent ceremony runs, so the pinned binary can no longer emit
|
|
229
|
+
// that line and it is removed here rather than left as dead tolerance. Multi-
|
|
230
|
+
// line stderr is still expected in principle — these are separate Fprintln
|
|
231
|
+
// calls, never one joined string — which is why membership is per line.
|
|
188
232
|
export const REVIEW_CONSENT_NOTICES = Object.freeze([
|
|
189
233
|
"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.",
|
|
190
|
-
"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.",
|
|
191
234
|
"Gentle AI could not read an answer, so it reviewed this change and will ask again next time.",
|
|
192
235
|
"Gentle AI did not recognize that answer, so it reviewed this change and will ask again next time.",
|
|
193
236
|
"Review skipped for this candidate at your request. It will be offered again on the next change.",
|
|
@@ -202,6 +245,22 @@ function stderrIsTolerated(stderr , tolerated )
|
|
|
202
245
|
return lines.length > 0 && lines.every((line) => tolerated.includes(line));
|
|
203
246
|
}
|
|
204
247
|
|
|
248
|
+
// gentle-ai main narrates the negotiated STATUS forecast head to a human on
|
|
249
|
+
// stderr while keeping the machine envelope on stdout (reviewNarrateForecast
|
|
250
|
+
// in internal/cli/review_narration.go, ground-truthed live against a main-line
|
|
251
|
+
// dev build). The pinned release does not emit it, so this narration is
|
|
252
|
+
// tolerated only for negotiated STATUS while the dev-binary override is
|
|
253
|
+
// configured; any other stderr keeps failing closed.
|
|
254
|
+
const FORECAST_NARRATION_LINES = Object.freeze([
|
|
255
|
+
/^Forecast horizon: (?:partial|terminal)$/,
|
|
256
|
+
/^step [0-9]+: (?:execute|collect|stop); reason_code=[a-z0-9_]+; description=.+$/,
|
|
257
|
+
/^Re-query STATUS after completing this partial head\.$/,
|
|
258
|
+
]);
|
|
259
|
+
function stderrIsForecastNarration(stderr ) {
|
|
260
|
+
const lines = stderr.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
261
|
+
return lines.length > 0 && lines.every((line) => FORECAST_NARRATION_LINES.some((pattern) => pattern.test(line)));
|
|
262
|
+
}
|
|
263
|
+
|
|
205
264
|
export const NATIVE_REVIEW_RECOVER_DISPOSITION = ["scope_changed", "invalidated", "escalated"] ;
|
|
206
265
|
|
|
207
266
|
|
|
@@ -286,6 +345,9 @@ export const NATIVE_REVIEW_LEGACY_ALIAS_REPAIR = {
|
|
|
286
345
|
|
|
287
346
|
|
|
288
347
|
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
|
|
289
351
|
|
|
290
352
|
|
|
291
353
|
|
|
@@ -346,6 +408,54 @@ export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "
|
|
|
346
408
|
|
|
347
409
|
|
|
348
410
|
|
|
411
|
+
// gentle-pi#311 P4-roles: one Go-owned non-lens provider role capture. The
|
|
412
|
+
// provider renders a SELF-CONTAINED authority-advancing vector
|
|
413
|
+
// (`review.capture-refuter` / `review.capture-validation` with binding tokens
|
|
414
|
+
// plus `--agent=pi --execute=true`, no submission descriptor); Pi executes the
|
|
415
|
+
// exact rendered invocation verbatim, in the foreground, and Go materializes
|
|
416
|
+
// the role prompt, spawns its own locked-down pi subprocess, and admits the
|
|
417
|
+
// raw verdict. Nothing here authors, parses, or transports role output.
|
|
418
|
+
export const NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA = "gentle-ai.review-provider-role-capture/v1";
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
// Field defect (fambig, 2026-08-16): the evidence collect slot renders the
|
|
449
|
+
// exact submission tokens native admits — fix-diff `--target`,
|
|
450
|
+
// `--expected-revision`, and an opaque cwd-independent `--repository-context` —
|
|
451
|
+
// with `{{outcome}}`/`{{input}}` substitution slots. Satisfying the slot means
|
|
452
|
+
// executing those tokens verbatim with only the two slot substitutions, the
|
|
453
|
+
// same discipline captureResult uses; identities are never reconstructed from
|
|
454
|
+
// top-level status fields.
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
|
|
349
459
|
|
|
350
460
|
|
|
351
461
|
|
|
@@ -394,6 +504,50 @@ export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "dec
|
|
|
394
504
|
|
|
395
505
|
|
|
396
506
|
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
// gentle-pi#311 P5: the provider-driven FINALIZE. `argumentTokens` are the
|
|
518
|
+
// exact rendered tokens of one provider-returned `review.finalize` execute
|
|
519
|
+
// transition (e.g. `--lineage=<id> --captured-results=true`), passed through
|
|
520
|
+
// verbatim and never synthesized or reordered by the host.
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
// The provider-rendered `finalize` submission descriptor forms (status/v5):
|
|
528
|
+
// the correction PLAN slot substitutes a positive line-count literal into its
|
|
529
|
+
// {{value}} token; the TARGETED VALIDATION slot substitutes a staged validator
|
|
530
|
+
// artifact path. The tokens are self-contained and execute verbatim.
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
|
|
397
551
|
|
|
398
552
|
|
|
399
553
|
|
|
@@ -667,6 +821,20 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
667
821
|
// advertises capabilities/v1.5 and the negotiated start envelope is still
|
|
668
822
|
// the closed `start/v2`, so riskEvidence and hint still cannot arrive.
|
|
669
823
|
"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 }),
|
|
824
|
+
// Ground-truthed against the released v2.2.3 binary: the v2 lane remains
|
|
825
|
+
// protocol 2.0 with the same operation set and closed START fields consumed
|
|
826
|
+
// by Pi, so the existing capability columns are unchanged.
|
|
827
|
+
"2.2.3": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
828
|
+
// Ground-truthed against the released v2.4.0 binary: the v2 lane advertises
|
|
829
|
+
// capabilities/v2.2 and answers status/v5 and consent/v3, all of which the
|
|
830
|
+
// existing decoders already read, and the START envelope Pi consumes is
|
|
831
|
+
// still `start/v3` carrying `risk_reasons` with no `risk_evidence` and no
|
|
832
|
+
// `hint`, so the existing capability columns are unchanged. v2.4.0 also
|
|
833
|
+
// made receipt-driven development opt-in, which changes what the mode
|
|
834
|
+
// envelope reports, not whether it reports it. v2.2.4 and v2.3.0 shipped
|
|
835
|
+
// while Pi stayed on 2.2.3; they were never pinned or probed, so they get
|
|
836
|
+
// no row.
|
|
837
|
+
"2.4.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
670
838
|
});
|
|
671
839
|
|
|
672
840
|
|
|
@@ -683,6 +851,23 @@ function resolvedNativeCliContract(version )
|
|
|
683
851
|
return nativeCliContractsTestingOverlay.get(version) ?? (NATIVE_CLI_CONTRACTS )[version];
|
|
684
852
|
}
|
|
685
853
|
|
|
854
|
+
// The latest known contract row, used as the capability floor for the
|
|
855
|
+
// explicit dev-binary override (unpinned field-test mode): a maintainer
|
|
856
|
+
// dev build is at least as capable as the newest release Pi knows. The table
|
|
857
|
+
// is declared in ascending release order, so its last key is the latest.
|
|
858
|
+
const LATEST_NATIVE_CLI_CONTRACT_VERSION = Object.keys(NATIVE_CLI_CONTRACTS).at(-1) ;
|
|
859
|
+
|
|
860
|
+
// Dev-binary override version discipline: with the override configured, a
|
|
861
|
+
// version reported outside the pinned table resolves to the latest known row;
|
|
862
|
+
// with the override absent, behavior is byte-identical to the pinned gate.
|
|
863
|
+
function effectiveNativeCliContract(version ) {
|
|
864
|
+
const pinned = resolvedNativeCliContract(version);
|
|
865
|
+
if (pinned !== undefined) return pinned;
|
|
866
|
+
return gentleAiDevBinaryOverrideConfigured() ? NATIVE_CLI_CONTRACTS[LATEST_NATIVE_CLI_CONTRACT_VERSION] : undefined;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
|
|
686
871
|
|
|
687
872
|
|
|
688
873
|
|
|
@@ -725,15 +910,25 @@ export class NativeReviewCliError extends Error {
|
|
|
725
910
|
}
|
|
726
911
|
}
|
|
727
912
|
|
|
913
|
+
// The one central runner for every gentle-ai CLI invocation the extension
|
|
914
|
+
// makes. It declares the Pi host relay handshake on each spawn: gentle-ai
|
|
915
|
+
// refuses pi admission pre-authority without it (gentle-pi#311 P4), and a
|
|
916
|
+
// single injection point keeps the declaration impossible to forget on any
|
|
917
|
+
// individual operation.
|
|
918
|
+
export function gentleAiProcessEnvironment(base = process.env) {
|
|
919
|
+
return { ...base, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT };
|
|
920
|
+
}
|
|
921
|
+
|
|
728
922
|
export function createNodeExecFileAdapter() {
|
|
729
923
|
return async (request) => {
|
|
730
924
|
try {
|
|
731
|
-
const output = await execFileAsync(request.file, [...request.arguments], { cwd: request.cwd, encoding: "utf8", shell: false, windowsHide: true, timeout: request.timeoutMs, maxBuffer: request.maxBufferBytes, signal: request.signal });
|
|
925
|
+
const output = await execFileAsync(request.file, [...request.arguments], { cwd: request.cwd, encoding: "utf8", shell: false, windowsHide: true, timeout: request.timeoutMs, maxBuffer: request.maxBufferBytes, signal: request.signal, env: gentleAiProcessEnvironment() });
|
|
732
926
|
return { stdout: output.stdout, stderr: output.stderr, exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
733
927
|
} catch (error) {
|
|
734
928
|
const detail = error ;
|
|
735
929
|
if (detail.code === "ENOENT" || detail.code === "EACCES" || detail.name === "AbortError") throw error;
|
|
736
|
-
|
|
930
|
+
const outputLimitExceeded = detail.code === "ENOBUFS" || detail.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
|
|
931
|
+
return { stdout: detail.stdout ?? "", stderr: detail.stderr ?? "", exitCode: typeof detail.code === "number" ? detail.code : 1, signal: detail.signal ?? null, timedOut: !outputLimitExceeded && detail.killed === true, outputLimitExceeded };
|
|
737
932
|
}
|
|
738
933
|
};
|
|
739
934
|
}
|
|
@@ -809,12 +1004,28 @@ function parseStructuredNativeDenial(stdout )
|
|
|
809
1004
|
// Rebuild diagnostics from a duplicated module instance before facade output.
|
|
810
1005
|
export function sanitizeForeignNativeReviewDiagnostics(value ) {
|
|
811
1006
|
try {
|
|
812
|
-
const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "stderr", "denial"]);
|
|
1007
|
+
const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "max_buffer_bytes", "configuration_hint", "stderr", "denial"]);
|
|
813
1008
|
const operation = enumString(raw.operation, Object.values(NATIVE_REVIEW_OPERATION)) ;
|
|
814
1009
|
const errorCode = enumString(raw.error_code, Object.values(NATIVE_REVIEW_ERROR_CODE)) ;
|
|
815
1010
|
const signal = raw.signal === undefined ? undefined : requiredString(raw.signal);
|
|
816
|
-
|
|
817
|
-
|
|
1011
|
+
const maxBufferBytes = raw.max_buffer_bytes === undefined ? undefined : positiveInteger(raw.max_buffer_bytes);
|
|
1012
|
+
const configurationHint = raw.configuration_hint === undefined ? undefined : stringValue(raw.configuration_hint);
|
|
1013
|
+
if (
|
|
1014
|
+
(signal !== undefined && !/^SIG[A-Z0-9]{1,12}$/.test(signal)) ||
|
|
1015
|
+
(maxBufferBytes === undefined) !== (configurationHint === undefined) ||
|
|
1016
|
+
(configurationHint !== undefined && (errorCode !== NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT || configurationHint !== NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT))
|
|
1017
|
+
) return undefined;
|
|
1018
|
+
return {
|
|
1019
|
+
operation,
|
|
1020
|
+
error_code: errorCode,
|
|
1021
|
+
...(raw.exit_code === undefined ? {} : { exit_code: nonNegativeInteger(raw.exit_code) }),
|
|
1022
|
+
...(signal === undefined ? {} : { signal: signal }),
|
|
1023
|
+
timed_out: booleanValue(raw.timed_out),
|
|
1024
|
+
output_limit_exceeded: booleanValue(raw.output_limit_exceeded),
|
|
1025
|
+
...(maxBufferBytes === undefined ? {} : { max_buffer_bytes: maxBufferBytes, configuration_hint: configurationHint }),
|
|
1026
|
+
...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr)) }),
|
|
1027
|
+
...(raw.denial === undefined ? {} : { denial: sanitizeForeignStructuredDenial(raw.denial) }),
|
|
1028
|
+
};
|
|
818
1029
|
} catch { return undefined; }
|
|
819
1030
|
}
|
|
820
1031
|
|
|
@@ -830,14 +1041,18 @@ function sanitizeForeignStructuredDenial(value )
|
|
|
830
1041
|
return { schema: "gentle-ai.review-gate-result/v1", result, action, reason, ...(denial === undefined ? {} : { denial }) };
|
|
831
1042
|
}
|
|
832
1043
|
|
|
833
|
-
function nativeProcessDiagnostics(operation , code , result ) {
|
|
1044
|
+
function nativeProcessDiagnostics(operation , code , result , maxBufferBytes ) {
|
|
1045
|
+
const outputLimitExceeded = result?.outputLimitExceeded === true;
|
|
834
1046
|
return {
|
|
835
1047
|
operation,
|
|
836
1048
|
error_code: code,
|
|
837
1049
|
...(result === undefined ? {} : { exit_code: result.exitCode }),
|
|
838
1050
|
...(result?.signal === null || result?.signal === undefined ? {} : { signal: result.signal }),
|
|
839
|
-
timed_out: result?.timedOut === true,
|
|
840
|
-
output_limit_exceeded:
|
|
1051
|
+
timed_out: !outputLimitExceeded && result?.timedOut === true,
|
|
1052
|
+
output_limit_exceeded: outputLimitExceeded,
|
|
1053
|
+
...(code === NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT && maxBufferBytes !== undefined
|
|
1054
|
+
? { max_buffer_bytes: maxBufferBytes, configuration_hint: NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT }
|
|
1055
|
+
: {}),
|
|
841
1056
|
...(result?.stderr.trim() ? { stderr: sanitizeNativeDiagnosticText(result.stderr) } : {}),
|
|
842
1057
|
...(result === undefined ? {} : { denial: parseStructuredNativeDenial(result.stdout) }),
|
|
843
1058
|
};
|
|
@@ -1155,8 +1370,8 @@ function hasValidLensesRequired(action , state , riskLe
|
|
|
1155
1370
|
return !lensesRequired;
|
|
1156
1371
|
}
|
|
1157
1372
|
|
|
1158
|
-
function nativeError(code , operation , mutating , message , result , launchAttempted = true, auditRecord ) {
|
|
1159
|
-
return new NativeReviewCliError(code, operation, launchAttempted, mutating, message, nativeProcessDiagnostics(operation, code, result), auditRecord);
|
|
1373
|
+
function nativeError(code , operation , mutating , message , result , launchAttempted = true, auditRecord , maxBufferBytes ) {
|
|
1374
|
+
return new NativeReviewCliError(code, operation, launchAttempted, mutating, message, nativeProcessDiagnostics(operation, code, result, maxBufferBytes), auditRecord);
|
|
1160
1375
|
}
|
|
1161
1376
|
|
|
1162
1377
|
|
|
@@ -1171,7 +1386,7 @@ export class NativeReviewCliV214 {
|
|
|
1171
1386
|
timeoutMs ;
|
|
1172
1387
|
maxBufferBytes ;
|
|
1173
1388
|
cleanupDirectory ;
|
|
1174
|
-
constructor(adapter , executable = resolveGentleAiBinary, timeoutMs = 30_000, maxBufferBytes =
|
|
1389
|
+
constructor(adapter , executable = resolveGentleAiBinary, timeoutMs = 30_000, maxBufferBytes = resolveNativeReviewMaxBufferBytes(), cleanupDirectory = (directory ) => rm(directory, { recursive: true, force: true })) {
|
|
1175
1390
|
if (typeof executable === "string" && (!isAbsolute(executable) || executable === "gentle-ai")) throw new TypeError("Native review requires an absolute package-local executable");
|
|
1176
1391
|
this.adapter = adapter;
|
|
1177
1392
|
this.executable = executable;
|
|
@@ -1208,8 +1423,8 @@ export class NativeReviewCliV214 {
|
|
|
1208
1423
|
throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, operation, mutating, "native process could not start");
|
|
1209
1424
|
}
|
|
1210
1425
|
const diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.NON_ZERO, result);
|
|
1426
|
+
if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result, true, undefined, this.maxBufferBytes);
|
|
1211
1427
|
if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
|
|
1212
|
-
if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result);
|
|
1213
1428
|
if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
|
|
1214
1429
|
const structuredValidateDenial = operation === NATIVE_REVIEW_OPERATION.VALIDATE && result.exitCode === 1;
|
|
1215
1430
|
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;
|
|
@@ -1227,12 +1442,18 @@ export class NativeReviewCliV214 {
|
|
|
1227
1442
|
if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was cancelled");
|
|
1228
1443
|
throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, NATIVE_REVIEW_OPERATION.VERSION, false, "gentle-ai is unavailable");
|
|
1229
1444
|
}
|
|
1445
|
+
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);
|
|
1230
1446
|
if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process timed out", result);
|
|
1231
|
-
if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process output exceeded limit", result);
|
|
1232
1447
|
if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was signalled", result);
|
|
1233
1448
|
if (result.exitCode !== 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.VERSION, false, "version process failed", result);
|
|
1234
|
-
const
|
|
1235
|
-
const
|
|
1449
|
+
const normalized = result.stdout.replace(/\r\n$/, "\n");
|
|
1450
|
+
const pinnedVersion = /^gentle-ai ([0-9]+\.[0-9]+\.[0-9]+)\n$/.exec(normalized)?.[1];
|
|
1451
|
+
// Dev-binary override (unpinned field-test mode): accept the binary's
|
|
1452
|
+
// reported version even when it is not a pinned three-part release
|
|
1453
|
+
// version (a main-line dev build reports e.g. "2.4.0-rc.8+fix...").
|
|
1454
|
+
// The banner shape itself stays strict. Pinned mode is byte-identical.
|
|
1455
|
+
const version = pinnedVersion ?? (gentleAiDevBinaryOverrideConfigured() ? /^gentle-ai (\S+)\n$/.exec(normalized)?.[1] : undefined);
|
|
1456
|
+
const contract = version === undefined ? undefined : effectiveNativeCliContract(version);
|
|
1236
1457
|
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"}`);
|
|
1237
1458
|
return version ;
|
|
1238
1459
|
}
|
|
@@ -1243,7 +1464,7 @@ export class NativeReviewCliV214 {
|
|
|
1243
1464
|
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
|
|
1244
1465
|
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
|
|
1245
1466
|
const version = await this.verifyVersion(request.cwd, request.signal, ["start"]);
|
|
1246
|
-
const toleratedStderr =
|
|
1467
|
+
const toleratedStderr = effectiveNativeCliContract(version)?.mode === true ? REVIEW_CONSENT_NOTICES : [];
|
|
1247
1468
|
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);
|
|
1248
1469
|
return decode(NATIVE_REVIEW_OPERATION.START, true, () => {
|
|
1249
1470
|
// `target_identity` and `lens_bindings` are real, unconditionally-present
|
|
@@ -1501,7 +1722,11 @@ export class NativeReviewCliV214 {
|
|
|
1501
1722
|
for (const [name, value] of [["lineage", request.lineage], ["expectedRevision", request.expectedRevision], ["snapshotIdentity", request.snapshotIdentity], ["actor", request.actor], ["reason", request.reason]] ) {
|
|
1502
1723
|
if (!isCanonicalProcessString(value)) throw new TypeError(`Native ABANDON ${name} must be a non-empty, trimmed, NUL-free string`);
|
|
1503
1724
|
}
|
|
1504
|
-
if (request.
|
|
1725
|
+
if (!Array.isArray(request.capturedLensResults) || request.capturedLensResults.some((entry) => !isCanonicalProcessString(entry))) throw new TypeError("Native ABANDON capturedLensResults must be an array of non-empty, trimmed, NUL-free strings");
|
|
1726
|
+
for (const [name, value] of [["findingsPresent", request.findingsPresent], ["evidenceRecordsPresent", request.evidenceRecordsPresent]] ) {
|
|
1727
|
+
if (typeof value !== "boolean") throw new TypeError(`Native ABANDON ${name} must be a boolean`);
|
|
1728
|
+
}
|
|
1729
|
+
if (request.maintainerAuthorization !== nativeReviewAbandonAuthorization(request)) throw new TypeError("Native ABANDON maintainerAuthorization must match the exact lineage, revision, snapshot, reason, discarded-work, and actor binding");
|
|
1505
1730
|
await this.verifyVersion(request.cwd, request.signal, ["abandon"]);
|
|
1506
1731
|
const execution = await this.execute(NATIVE_REVIEW_OPERATION.ABANDON, request.cwd, [
|
|
1507
1732
|
"review", "abandon", "--cwd", request.cwd,
|
|
@@ -1595,14 +1820,17 @@ export class NativeReviewCliV214 {
|
|
|
1595
1820
|
}
|
|
1596
1821
|
}
|
|
1597
1822
|
|
|
1598
|
-
export function nativeReviewAbandonAuthorization(request
|
|
1823
|
+
export function nativeReviewAbandonAuthorization(request ) {
|
|
1599
1824
|
return [
|
|
1600
|
-
"gentle-ai.review-abandon-authorization/
|
|
1825
|
+
"gentle-ai.review-abandon-authorization/v2",
|
|
1601
1826
|
`lineage=${request.lineage}`,
|
|
1602
1827
|
`revision=${request.expectedRevision}`,
|
|
1603
1828
|
`snapshot_identity=${request.snapshotIdentity}`,
|
|
1604
|
-
`actor=${request.actor}`,
|
|
1605
1829
|
`reason=${request.reason}`,
|
|
1830
|
+
`captured_lens_results=${request.capturedLensResults.join(",")}`,
|
|
1831
|
+
`findings_present=${request.findingsPresent}`,
|
|
1832
|
+
`evidence_records_present=${request.evidenceRecordsPresent}`,
|
|
1833
|
+
`actor=${request.actor.trim()}`,
|
|
1606
1834
|
].join("\n");
|
|
1607
1835
|
}
|
|
1608
1836
|
|
|
@@ -1619,6 +1847,32 @@ export function nativeReviewLegacyQuarantineAuthorization(request
|
|
|
1619
1847
|
].join("\n");
|
|
1620
1848
|
}
|
|
1621
1849
|
|
|
1850
|
+
/**
|
|
1851
|
+
* The exact native `gentle-ai.review-recovery-authorization/v1` binding for one
|
|
1852
|
+
* recovery edge.
|
|
1853
|
+
*
|
|
1854
|
+
* Native `review recover` accepts a caller-supplied authorization only when it
|
|
1855
|
+
* reproduces this binding byte for byte, because it is copied verbatim into the
|
|
1856
|
+
* recovery provenance and read afterwards as a maintainer attestation. Pi
|
|
1857
|
+
* therefore derives it from freshly read native target status and never
|
|
1858
|
+
* forwards a caller-supplied one: a wrong binding is worse than an absent one,
|
|
1859
|
+
* since an absent field cannot lie about who approved what.
|
|
1860
|
+
*
|
|
1861
|
+
* `targetIdentity` is the live target identity the provider itself publishes in
|
|
1862
|
+
* the `review.recover` eligibility binding (`status.target_identity`), which is
|
|
1863
|
+
* the identity the successor's initial snapshot takes.
|
|
1864
|
+
*/
|
|
1865
|
+
export function nativeReviewRecoverAuthorization(request ) {
|
|
1866
|
+
return [
|
|
1867
|
+
"gentle-ai.review-recovery-authorization/v1",
|
|
1868
|
+
`predecessor_lineage=${request.predecessorLineage}`,
|
|
1869
|
+
`predecessor_revision=${request.expectedPredecessorRevision}`,
|
|
1870
|
+
`target_identity=${request.targetIdentity}`,
|
|
1871
|
+
`actor=${request.actor.trim()}`,
|
|
1872
|
+
`reason=${request.reason.trim()}`,
|
|
1873
|
+
].join("\n");
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1622
1876
|
export function nativeReviewReconcileAuthorization(request ) {
|
|
1623
1877
|
return [
|
|
1624
1878
|
"gentle-ai.review-reconcile-authorization/v1",
|
|
@@ -1659,15 +1913,16 @@ export class NativeReviewIntegrationError extends Error {
|
|
|
1659
1913
|
}
|
|
1660
1914
|
}
|
|
1661
1915
|
|
|
1662
|
-
// Raised when negotiated START answers `consent/v2`
|
|
1916
|
+
// Raised when negotiated START answers a consent question (`consent/v2` from
|
|
1917
|
+
// the pinned line, `consent/v3` from gentle-ai >= 2.3.0; action:
|
|
1663
1918
|
// "consent_required") instead of `start/v3`. The provider has frozen no
|
|
1664
1919
|
// authority yet: Pi must relay this complete candidate-scoped question and may
|
|
1665
1920
|
// answer only through one of the exact invocations carried by the envelope.
|
|
1666
1921
|
export class NativeReviewConsentRequiredError extends Error {
|
|
1667
|
-
consent
|
|
1922
|
+
consent ;
|
|
1668
1923
|
launchAttempted = true;
|
|
1669
1924
|
mutationOutcome = "none";
|
|
1670
|
-
constructor(consent
|
|
1925
|
+
constructor(consent ) {
|
|
1671
1926
|
super(consent.headline);
|
|
1672
1927
|
this.name = "NativeReviewConsentRequiredError";
|
|
1673
1928
|
this.consent = consent;
|
|
@@ -1751,6 +2006,29 @@ function exactConsentOption(arguments_ , name )
|
|
|
1751
2006
|
return values[0] ;
|
|
1752
2007
|
}
|
|
1753
2008
|
|
|
2009
|
+
function optionalConsentLineageOption(arguments_ ) {
|
|
2010
|
+
const values = [];
|
|
2011
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
2012
|
+
const token = arguments_[index] ;
|
|
2013
|
+
if (token === "--lineage") {
|
|
2014
|
+
const value = arguments_[index + 1];
|
|
2015
|
+
if (value === undefined || value.startsWith("--")) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", "Native consent invocation --lineage is missing its value");
|
|
2016
|
+
values.push(value);
|
|
2017
|
+
index += 1;
|
|
2018
|
+
} else if (token.startsWith("--lineage=")) values.push(token.slice("--lineage=".length));
|
|
2019
|
+
}
|
|
2020
|
+
if (values.length > 1) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", "Native consent invocation permits at most one --lineage");
|
|
2021
|
+
if (values.length === 0) return undefined;
|
|
2022
|
+
const lineageId = values[0] ;
|
|
2023
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(lineageId)) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", "Native consent invocation --lineage is malformed");
|
|
2024
|
+
return lineageId;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
|
|
2028
|
+
|
|
2029
|
+
|
|
2030
|
+
|
|
2031
|
+
|
|
1754
2032
|
function consentInvocationArguments(request ) {
|
|
1755
2033
|
const choice = request.consent.choices.find((candidate) => candidate.answer === request.answer);
|
|
1756
2034
|
if (choice === undefined) throw new NativeReviewConsentBindingError("consent-answer-unknown", "Native consent answer must be granted or declined");
|
|
@@ -1761,8 +2039,9 @@ function consentInvocationArguments(request )
|
|
|
1761
2039
|
if (exactConsentOption(arguments_, "--cwd") !== request.cwd) throw new NativeReviewConsentBindingError("consent-invocation-cwd-changed", "Native consent invocation repository binding changed");
|
|
1762
2040
|
if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new NativeReviewConsentBindingError("consent-invocation-target-changed", "Native consent invocation target binding changed");
|
|
1763
2041
|
if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new NativeReviewConsentBindingError("consent-invocation-projection-changed", "Native consent invocation projection binding changed");
|
|
2042
|
+
const lineageId = optionalConsentLineageOption(arguments_);
|
|
1764
2043
|
if (exactConsentOption(arguments_, "--consent") !== request.answer || arguments_.at(-1) !== request.answer) throw new NativeReviewConsentBindingError("consent-invocation-answer-changed", "Native consent invocation answer binding changed");
|
|
1765
|
-
return arguments_;
|
|
2044
|
+
return { arguments_, ...(lineageId === undefined ? {} : { lineageId }) };
|
|
1766
2045
|
}
|
|
1767
2046
|
|
|
1768
2047
|
function decodeDeclinedConsentStart(value , expected ) {
|
|
@@ -1811,27 +2090,46 @@ function defaultExecutableDigest(path ) {
|
|
|
1811
2090
|
|
|
1812
2091
|
|
|
1813
2092
|
function decodeNativeAdmittedResultManifest(value ) {
|
|
1814
|
-
|
|
2093
|
+
// The admission answer routes through the exact-identity forward decoder
|
|
2094
|
+
// (decoder-freshness discipline): the complete live envelope — identity
|
|
2095
|
+
// constants, binding fields, and the exactly-one-locator rule — is
|
|
2096
|
+
// validated before anything is handed to FINALIZE, and an unknown field
|
|
2097
|
+
// grown by gentle-ai main is rejected instead of silently dropped.
|
|
2098
|
+
const artifact = decodeReviewResultArtifactV2(value);
|
|
2099
|
+
return Object.freeze({
|
|
2100
|
+
schema: artifact.schema,
|
|
2101
|
+
subjectHash: artifact.subjectHash,
|
|
2102
|
+
admissionDecision: artifact.admissionDecision,
|
|
2103
|
+
lens: artifact.lens,
|
|
2104
|
+
...(artifact.path === undefined ? {} : { path: artifact.path }),
|
|
2105
|
+
...(artifact.reference === undefined ? {} : { reference: artifact.reference }),
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// gentle-pi#311 P4-roles: the strict acknowledgement for one executed
|
|
2110
|
+
// provider role vector. The immutable verdict bytes live in the Go-owned
|
|
2111
|
+
// compact store slot; this envelope only names the binding the capture
|
|
2112
|
+
// proved, so anything beyond that exact shape is refused.
|
|
2113
|
+
function decodeNativeProviderRoleCaptureArtifact(value ) {
|
|
2114
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("native provider role capture artifact must be an object");
|
|
1815
2115
|
const body = value ;
|
|
2116
|
+
const allowed = new Set(["schema", "lineage_id", "target_identity", "role", "captured"]);
|
|
2117
|
+
for (const key of Object.keys(body)) if (!allowed.has(key)) throw new TypeError(`native provider role capture artifact carries unexpected key ${key}`);
|
|
1816
2118
|
const text = (key ) => {
|
|
1817
2119
|
const found = body[key];
|
|
1818
|
-
if (typeof found !== "string" || found.trim() !== found || found.length === 0) throw new TypeError(`native capture
|
|
2120
|
+
if (typeof found !== "string" || found.trim() !== found || found.length === 0) throw new TypeError(`native provider role capture artifact ${key} must be a non-empty trimmed string`);
|
|
1819
2121
|
return found;
|
|
1820
2122
|
};
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
if (
|
|
1825
|
-
// Exactly one locator: a provider-owned path OR an opaque reference. Both or
|
|
1826
|
-
// neither means the manifest cannot be handed to FINALIZE.
|
|
1827
|
-
const hasPath = body.path !== undefined, hasReference = body.reference !== undefined;
|
|
1828
|
-
if (hasPath === hasReference) throw new TypeError("native capture-result manifest must carry exactly one of path or reference");
|
|
2123
|
+
if (text("schema") !== NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA) throw new TypeError(`native provider role capture artifact schema must be ${NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA}`);
|
|
2124
|
+
const role = text("role");
|
|
2125
|
+
if (role !== "refuter" && role !== "targeted-validator") throw new TypeError(`native provider role capture artifact role must be refuter or targeted-validator, received ${role}`);
|
|
2126
|
+
if (body.captured !== true) throw new TypeError("native provider role capture artifact must report captured: true");
|
|
1829
2127
|
return Object.freeze({
|
|
1830
|
-
schema,
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2128
|
+
schema: NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA,
|
|
2129
|
+
lineageId: text("lineage_id"),
|
|
2130
|
+
targetIdentity: text("target_identity"),
|
|
2131
|
+
role,
|
|
2132
|
+
captured: true,
|
|
1835
2133
|
});
|
|
1836
2134
|
}
|
|
1837
2135
|
|
|
@@ -1847,7 +2145,7 @@ export class NativeReviewCliV216 {
|
|
|
1847
2145
|
adapter ,
|
|
1848
2146
|
executable = resolveGentleAiBinary,
|
|
1849
2147
|
timeoutMs = 30_000,
|
|
1850
|
-
maxBufferBytes =
|
|
2148
|
+
maxBufferBytes = resolveNativeReviewMaxBufferBytes(),
|
|
1851
2149
|
cleanupDirectory = (directory) => rm(directory, { recursive: true, force: true }),
|
|
1852
2150
|
executableDigest = defaultExecutableDigest,
|
|
1853
2151
|
) {
|
|
@@ -1897,8 +2195,8 @@ export class NativeReviewCliV216 {
|
|
|
1897
2195
|
if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, operation, mutating, "native process was cancelled");
|
|
1898
2196
|
throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, operation, mutating, "native process could not start");
|
|
1899
2197
|
}
|
|
2198
|
+
if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result, true, undefined, this.maxBufferBytes);
|
|
1900
2199
|
if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
|
|
1901
|
-
if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result);
|
|
1902
2200
|
if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
|
|
1903
2201
|
const diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.NON_ZERO, result);
|
|
1904
2202
|
const body = parseJson(result.stdout, operation, mutating, diagnostics);
|
|
@@ -1910,7 +2208,8 @@ export class NativeReviewCliV216 {
|
|
|
1910
2208
|
throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native negotiated operation failed without a valid failure envelope", result);
|
|
1911
2209
|
}
|
|
1912
2210
|
}
|
|
1913
|
-
|
|
2211
|
+
const forecastNarrationTolerated = operation === NATIVE_REVIEW_OPERATION.STATUS && gentleAiDevBinaryOverrideConfigured() && stderrIsForecastNarration(result.stderr);
|
|
2212
|
+
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);
|
|
1914
2213
|
return { body, exitCode: result.exitCode };
|
|
1915
2214
|
}
|
|
1916
2215
|
|
|
@@ -1939,7 +2238,12 @@ export class NativeReviewCliV216 {
|
|
|
1939
2238
|
if (error instanceof NativeReviewCliError) throw error;
|
|
1940
2239
|
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`);
|
|
1941
2240
|
}
|
|
1942
|
-
|
|
2241
|
+
// Dev-binary override (unpinned field-test mode): the session accepts
|
|
2242
|
+
// the binary's self-reported package version instead of the exact pin.
|
|
2243
|
+
// The full capabilities decode above still applies unchanged, so an
|
|
2244
|
+
// incompatible envelope is refused either way. Pinned mode keeps the
|
|
2245
|
+
// exact equality byte-identical.
|
|
2246
|
+
if (capabilities.packageVersion !== GENTLE_AI_VERSION && !gentleAiDevBinaryOverrideConfigured()) {
|
|
1943
2247
|
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}`);
|
|
1944
2248
|
}
|
|
1945
2249
|
return capabilities;
|
|
@@ -1995,12 +2299,20 @@ export class NativeReviewCliV216 {
|
|
|
1995
2299
|
...(request.focus === undefined ? [] : ["--focus", request.focus]),
|
|
1996
2300
|
"--consent", "relay",
|
|
1997
2301
|
], true, request.signal);
|
|
1998
|
-
// A negotiated v2 START may answer
|
|
2302
|
+
// A negotiated v2 START may answer a consent question (action:
|
|
1999
2303
|
// "consent_required") instead of `start/v3` when the provider needs an
|
|
2000
2304
|
// explicit answer it cannot infer. Discriminate before decode and surface
|
|
2001
|
-
// the complete envelope; only the caller can map a human answer.
|
|
2305
|
+
// the complete envelope; only the caller can map a human answer. The
|
|
2306
|
+
// body's own schema string selects the identity-exact decoder: the
|
|
2307
|
+
// pinned 2.2.x line emits consent/v2, gentle-ai >= 2.3.0 (capabilities
|
|
2308
|
+
// v2.1+) emits consent/v3, and any other identity fails closed inside
|
|
2309
|
+
// the v3 decoder's exact identity gate.
|
|
2002
2310
|
if (execution.body.action === "consent_required") {
|
|
2003
|
-
const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () =>
|
|
2311
|
+
const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () => (
|
|
2312
|
+
execution.body.schema === "gentle-ai.review-integration.consent/v2"
|
|
2313
|
+
? decodeReviewConsentV2(execution.body)
|
|
2314
|
+
: decodeReviewConsentV3(execution.body)
|
|
2315
|
+
));
|
|
2004
2316
|
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");
|
|
2005
2317
|
throw new NativeReviewConsentRequiredError(consent);
|
|
2006
2318
|
}
|
|
@@ -2037,16 +2349,15 @@ export class NativeReviewCliV216 {
|
|
|
2037
2349
|
}
|
|
2038
2350
|
|
|
2039
2351
|
async answerConsent(request ) {
|
|
2040
|
-
const
|
|
2041
|
-
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, arguments_, true, request.signal);
|
|
2352
|
+
const invocation = consentInvocationArguments(request);
|
|
2353
|
+
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, invocation.arguments_, true, request.signal);
|
|
2042
2354
|
if (request.answer === NATIVE_REVIEW_CONSENT_ANSWER.DECLINED) {
|
|
2043
2355
|
return decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeDeclinedConsentStart(execution.body, request));
|
|
2044
2356
|
}
|
|
2045
2357
|
const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
|
|
2046
2358
|
const answeredTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
|
|
2047
2359
|
if (answeredTarget !== request.consent.targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer target mismatch");
|
|
2048
|
-
|
|
2049
|
-
if (result.lineageId !== lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer lineage mismatch");
|
|
2360
|
+
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");
|
|
2050
2361
|
return { kind: "started", start: {
|
|
2051
2362
|
lineageId: result.lineageId,
|
|
2052
2363
|
state: result.state ,
|
|
@@ -2177,6 +2488,7 @@ export class NativeReviewCliV216 {
|
|
|
2177
2488
|
"--projection", request.projection ?? "workspace",
|
|
2178
2489
|
...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef]),
|
|
2179
2490
|
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
2491
|
+
...(request.agent === undefined ? [] : ["--agent", request.agent]),
|
|
2180
2492
|
"--next-transition",
|
|
2181
2493
|
], false, request.signal);
|
|
2182
2494
|
assertSupportedNextTransitionOperation(execution.body);
|
|
@@ -2243,6 +2555,118 @@ export class NativeReviewCliV216 {
|
|
|
2243
2555
|
}
|
|
2244
2556
|
}
|
|
2245
2557
|
|
|
2558
|
+
// gentle-pi#311 P4-roles: executes one provider-rendered self-contained
|
|
2559
|
+
// role capture vector exactly as rendered — one CLI invocation, verbatim
|
|
2560
|
+
// tokens in provider order, in the foreground. Go materializes the role
|
|
2561
|
+
// prompt, spawns its own locked-down pi subprocess, and admits the raw
|
|
2562
|
+
// verdict; Pi never adds, removes, or reorders a single token (not even
|
|
2563
|
+
// --cwd: the vector's --repository-context is authoritative and mutually
|
|
2564
|
+
// exclusive with a path). The central adapter injects the relay handshake
|
|
2565
|
+
// environment on this spawn like on every other gentle-ai invocation.
|
|
2566
|
+
async captureProviderRole(request ) {
|
|
2567
|
+
const verb = request.captureOperation.startsWith("review.") ? request.captureOperation.slice("review.".length) : "";
|
|
2568
|
+
if (verb !== "capture-refuter" && verb !== "capture-validation") throw new TypeError(`Native CAPTURE_PROVIDER_ROLE supports only review.capture-refuter and review.capture-validation, received ${JSON.stringify(request.captureOperation)}`);
|
|
2569
|
+
if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_PROVIDER_ROLE requires the provider-rendered argument tokens");
|
|
2570
|
+
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");
|
|
2571
|
+
const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true);
|
|
2572
|
+
const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, request.cwd, [
|
|
2573
|
+
"review", verb,
|
|
2574
|
+
...request.argumentTokens,
|
|
2575
|
+
], true, request.signal, executable.path);
|
|
2576
|
+
return decode(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true, () => decodeNativeProviderRoleCaptureArtifact(execution.body));
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
// gentle-pi#311 P5: executes one provider-rendered `review.finalize`
|
|
2580
|
+
// execute transition exactly as rendered. The tokens come verbatim from
|
|
2581
|
+
// the negotiated next_transition (e.g. `--lineage=<id>
|
|
2582
|
+
// --captured-results=true`); Pi assembles no reviewer, refuter, or
|
|
2583
|
+
// validator documents for this lane — the provider discovers its own
|
|
2584
|
+
// admitted role and lens slots.
|
|
2585
|
+
async finalizeTransition(request ) {
|
|
2586
|
+
if (request.argumentTokens.length === 0) throw new TypeError("Native FINALIZE transition requires the provider-rendered argument tokens");
|
|
2587
|
+
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");
|
|
2588
|
+
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
|
|
2589
|
+
"review", "finalize",
|
|
2590
|
+
...request.argumentTokens,
|
|
2591
|
+
], true, request.signal);
|
|
2592
|
+
return this.decodeFinalizeTransitionExecution(execution);
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// Same misbinding class as capture-evidence (live smoke, 2026-08-16): the
|
|
2596
|
+
// correction PLAN and TARGETED VALIDATION collect slots render `finalize`
|
|
2597
|
+
// submission descriptors whose tokens are self-contained (--contract,
|
|
2598
|
+
// --lineage, --expected-revision, --target, --request-hash,
|
|
2599
|
+
// --repository-context) plus exactly one {{value}} slot. Executing anything
|
|
2600
|
+
// other than those rendered tokens fails the live emitter's committed-
|
|
2601
|
+
// intent reconciliation, so the tokens pass through verbatim with only the
|
|
2602
|
+
// {{value}} substitution: a literal for correction_lines, a staged 0o600
|
|
2603
|
+
// artifact path for a validation document.
|
|
2604
|
+
async finalizeSubmission(request ) {
|
|
2605
|
+
if (request.argumentTokens.length === 0) throw new TypeError("Native FINALIZE submission requires the provider-rendered argument tokens");
|
|
2606
|
+
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");
|
|
2607
|
+
if ((request.valueLiteral === undefined) === (request.valueDocument === undefined)) throw new TypeError("Native FINALIZE submission takes exactly one of valueLiteral or valueDocument");
|
|
2608
|
+
const valueToken = request.argumentTokens[request.valueSubstitutionLocation];
|
|
2609
|
+
if (valueToken === undefined || !valueToken.includes("{{value}}")) throw new TypeError("Native FINALIZE submission must render exactly one {{value}} slot token");
|
|
2610
|
+
if (request.valueDocument !== undefined && request.valueDocument.length === 0) throw new TypeError("Native FINALIZE submission value document must contain at least one byte");
|
|
2611
|
+
const directory = request.valueDocument === undefined ? undefined : await mkdtemp(join(tmpdir(), "gentle-ai-finalize-submission-"));
|
|
2612
|
+
try {
|
|
2613
|
+
let substituted ;
|
|
2614
|
+
if (directory !== undefined) {
|
|
2615
|
+
await chmod(directory, 0o700);
|
|
2616
|
+
const valueFile = join(directory, "value.json");
|
|
2617
|
+
await writeFile(valueFile, request.valueDocument , { encoding: "utf8", mode: 0o600 });
|
|
2618
|
+
await chmod(valueFile, 0o600);
|
|
2619
|
+
substituted = valueFile;
|
|
2620
|
+
} else {
|
|
2621
|
+
substituted = request.valueLiteral ;
|
|
2622
|
+
}
|
|
2623
|
+
const resolved = request.argumentTokens.map((token, index) => index === request.valueSubstitutionLocation ? token.replaceAll("{{value}}", substituted) : token);
|
|
2624
|
+
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
|
|
2625
|
+
"review", "finalize",
|
|
2626
|
+
...resolved,
|
|
2627
|
+
], true, request.signal);
|
|
2628
|
+
return this.decodeFinalizeTransitionExecution(execution);
|
|
2629
|
+
} finally {
|
|
2630
|
+
if (directory !== undefined) await this.cleanupDirectory(directory).catch(() => undefined);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
decodeFinalizeTransitionExecution(execution ) {
|
|
2635
|
+
return decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => {
|
|
2636
|
+
const body = object(execution.body);
|
|
2637
|
+
// A transition rendered with `--contract` answers with the negotiated
|
|
2638
|
+
// operation envelope; one rendered without (the pi runtime form)
|
|
2639
|
+
// answers with the plain `review/finalize` shape. Both are decoded
|
|
2640
|
+
// strictly; nothing else is accepted.
|
|
2641
|
+
if (typeof body.schema === "string") {
|
|
2642
|
+
const envelope = decodeReviewOperationV2(body);
|
|
2643
|
+
if (envelope.operation !== "review.finalize") throw new Error("wrong finalize operation envelope");
|
|
2644
|
+
const result = envelope.result;
|
|
2645
|
+
return {
|
|
2646
|
+
lineageId: requiredString(result.lineage_id),
|
|
2647
|
+
state: requiredString(result.state),
|
|
2648
|
+
action: requiredString(result.action),
|
|
2649
|
+
storeRevision: requiredString(result.store_revision),
|
|
2650
|
+
...(result.validation_request === undefined ? {} : { validationRequest: result.validation_request }),
|
|
2651
|
+
...(result.escalation === undefined ? {} : { escalation: requiredString(result.escalation) }),
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
const plain = exactObject(body, ["operation", "lineage_id", "state", "action", "store_revision"], ["receipt_path", "validation_request", "escalation"]);
|
|
2655
|
+
if (plain.operation !== "review/finalize") throw new Error("wrong finalize discriminator");
|
|
2656
|
+
const state = requiredString(plain.state);
|
|
2657
|
+
if (!(NATIVE_FINALIZE_STATE ).includes(state)) throw new Error("unknown finalize state");
|
|
2658
|
+
return {
|
|
2659
|
+
lineageId: requiredString(plain.lineage_id),
|
|
2660
|
+
state,
|
|
2661
|
+
action: requiredString(plain.action),
|
|
2662
|
+
storeRevision: requiredString(plain.store_revision),
|
|
2663
|
+
...(plain.receipt_path === undefined ? {} : { receiptPath: requiredString(plain.receipt_path) }),
|
|
2664
|
+
...(plain.validation_request === undefined ? {} : { validationRequest: plain.validation_request }),
|
|
2665
|
+
...(plain.escalation === undefined ? {} : { escalation: requiredString(plain.escalation) }),
|
|
2666
|
+
};
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2246
2670
|
async captureEvidence(request ) {
|
|
2247
2671
|
if (!(NATIVE_REVIEW_CAPTURE_OUTCOME ).includes(request.outcome)) throw new TypeError("Native CAPTURE_EVIDENCE outcome must be passed, verification_failed, or procedural_tooling_failed");
|
|
2248
2672
|
if (request.evidenceDocument.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE evidence must contain at least one byte");
|
|
@@ -2264,6 +2688,45 @@ export class NativeReviewCliV216 {
|
|
|
2264
2688
|
}
|
|
2265
2689
|
}
|
|
2266
2690
|
|
|
2691
|
+
// Field defect (fambig, 2026-08-16): at every evidence-pending sub-state
|
|
2692
|
+
// the collect slot renders the identity native demands — for a correction
|
|
2693
|
+
// that is the fix-diff `--target`, not the live workspace snapshot — so the
|
|
2694
|
+
// slot's rendered submission tokens execute verbatim, with only the
|
|
2695
|
+
// {{outcome}} and {{input}} slots substituted. Same verbatim-token
|
|
2696
|
+
// discipline as captureResult; --repository-context is authoritative and
|
|
2697
|
+
// mutually exclusive with a path.
|
|
2698
|
+
async captureEvidenceSubmission(request ) {
|
|
2699
|
+
if (!(NATIVE_REVIEW_CAPTURE_OUTCOME ).includes(request.outcome)) throw new TypeError("Native CAPTURE_EVIDENCE outcome must be passed, verification_failed, or procedural_tooling_failed");
|
|
2700
|
+
if (request.evidenceDocument.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE evidence must contain at least one byte");
|
|
2701
|
+
if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE submission requires the provider-rendered argument tokens");
|
|
2702
|
+
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");
|
|
2703
|
+
const outcomeToken = request.argumentTokens[request.outcomeSubstitutionLocation];
|
|
2704
|
+
const inputToken = request.argumentTokens[request.inputSubstitutionLocation];
|
|
2705
|
+
if (request.outcomeSubstitutionLocation === request.inputSubstitutionLocation || outcomeToken === undefined || !outcomeToken.includes("{{outcome}}")) throw new TypeError("Native CAPTURE_EVIDENCE submission must render exactly one {{outcome}} slot token");
|
|
2706
|
+
if (inputToken === undefined || !inputToken.includes("{{input}}")) throw new TypeError("Native CAPTURE_EVIDENCE submission must render exactly one {{input}} slot token");
|
|
2707
|
+
const carriesContext = request.argumentTokens.some((token) => token === "--repository-context" || token.startsWith("--repository-context="));
|
|
2708
|
+
if (carriesContext && request.cwd !== undefined) throw new TypeError("Native CAPTURE_EVIDENCE submission takes a repository context or --cwd, never both");
|
|
2709
|
+
const directory = await mkdtemp(join(tmpdir(), "gentle-ai-capture-evidence-"));
|
|
2710
|
+
try {
|
|
2711
|
+
await chmod(directory, 0o700);
|
|
2712
|
+
const evidenceFile = await this.stageEvidence(directory, request.evidenceDocument);
|
|
2713
|
+
const resolved = request.argumentTokens.map((token, index) =>
|
|
2714
|
+
index === request.outcomeSubstitutionLocation
|
|
2715
|
+
? token.replaceAll("{{outcome}}", request.outcome)
|
|
2716
|
+
: index === request.inputSubstitutionLocation
|
|
2717
|
+
? token.replaceAll("{{input}}", evidenceFile)
|
|
2718
|
+
: token);
|
|
2719
|
+
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, request.cwd ?? process.cwd(), [
|
|
2720
|
+
"review", "capture-evidence",
|
|
2721
|
+
...resolved,
|
|
2722
|
+
...(carriesContext || request.cwd === undefined ? [] : ["--cwd", request.cwd]),
|
|
2723
|
+
], true, request.signal);
|
|
2724
|
+
return decode(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, true, () => decodeNativeReviewVerificationEvidence(execution.body));
|
|
2725
|
+
} finally {
|
|
2726
|
+
await this.cleanupDirectory(directory).catch(() => undefined);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2267
2730
|
reviewStatus(request ) {
|
|
2268
2731
|
return this.legacy.reviewStatus(request);
|
|
2269
2732
|
}
|