gentle-pi 2.3.0 → 2.5.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.
Files changed (153) hide show
  1. package/README.md +195 -11
  2. package/assets/agents/gentle-ai-worker.md +9 -0
  3. package/assets/agents/sdd-explore.md +1 -0
  4. package/assets/orchestrator-delegation.md +21 -10
  5. package/assets/orchestrator.md +8 -12
  6. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  7. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
  8. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
  9. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
  10. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
  11. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
  12. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
  13. package/docs/delegated-verification.md +25 -0
  14. package/docs/review-integration.md +1 -1
  15. package/docs/telemetry.md +38 -0
  16. package/extensions/ask-user-choice.ts +26 -20
  17. package/extensions/codegraph-tools.ts +94 -5
  18. package/extensions/gentle-agents.ts +588 -0
  19. package/extensions/gentle-ai.ts +1421 -143
  20. package/extensions/gentle-shell.ts +547 -0
  21. package/extensions/gentle-todo.ts +199 -0
  22. package/extensions/quiet-tools.ts +1 -1
  23. package/lib/agent-home.ts +8 -0
  24. package/lib/agents-config.ts +318 -0
  25. package/lib/agents-history.ts +80 -0
  26. package/lib/agents-protocol.ts +429 -0
  27. package/lib/agents-runner.ts +490 -0
  28. package/lib/agents-transcript.ts +87 -0
  29. package/lib/agents-view.ts +557 -0
  30. package/lib/agents-widget.ts +222 -0
  31. package/lib/gentle-ai-renderer.ts +142 -26
  32. package/lib/native-choice-list.ts +194 -0
  33. package/lib/native-fullscreen-interaction.ts +47 -0
  34. package/lib/native-pointer-region.ts +164 -0
  35. package/lib/native-review-cli.ts +103 -12
  36. package/lib/provider-contract-bundle.ts +88 -6
  37. package/lib/review-candidate-view-owner.ts +177 -0
  38. package/lib/review-candidate-view.ts +127 -35
  39. package/lib/review-consent-ui.ts +65 -0
  40. package/lib/review-host-relay.ts +146 -60
  41. package/lib/review-integration-v2.ts +92 -13
  42. package/lib/review-last-event-controller.ts +1 -0
  43. package/lib/review-relay-contract.ts +11 -0
  44. package/lib/review-repository.ts +2 -2
  45. package/lib/review-risk-assessment.ts +339 -0
  46. package/lib/review-session-standing-permission-ipc.ts +309 -0
  47. package/lib/review-session-standing-permission.ts +219 -0
  48. package/lib/sdd-preflight.ts +2 -2
  49. package/lib/shell-bar.ts +138 -0
  50. package/lib/shell-card.ts +136 -0
  51. package/lib/shell-changes-view.ts +205 -0
  52. package/lib/shell-changes.ts +210 -0
  53. package/lib/shell-gauge.ts +40 -0
  54. package/lib/shell-prompt.ts +119 -0
  55. package/lib/shell-todo.ts +280 -0
  56. package/lib/shell-usage-view.ts +76 -0
  57. package/lib/shell-usage.ts +246 -0
  58. package/lib/telemetry-trigger.ts +151 -0
  59. package/package.json +4 -4
  60. package/runtime/native-review-cli.mjs +102 -11
  61. package/runtime/review-integration-v2.mjs +92 -13
  62. package/runtime/review-relay-contract.mjs +11 -0
  63. package/runtime/review-risk-assessment.mjs +340 -0
  64. package/runtime/telemetry-trigger.mjs +152 -0
  65. package/scripts/build-runtime-modules.mjs +2 -0
  66. package/scripts/gentle-ai-installer.mjs +10 -10
  67. package/scripts/test-packed-runner.mjs +22 -0
  68. package/scripts/verify-package-files.mjs +18 -13
  69. package/skills/_shared/review-ledger-contract.md +9 -1
  70. package/skills/issue-creation/SKILL.md +53 -93
  71. package/tests/agents-config.test.ts +143 -0
  72. package/tests/agents-fake-child.ts +52 -0
  73. package/tests/agents-history.test.ts +54 -0
  74. package/tests/agents-protocol.test.ts +153 -0
  75. package/tests/agents-runner-process.test.ts +111 -0
  76. package/tests/agents-runner.test.ts +402 -0
  77. package/tests/agents-transcript.test.ts +30 -0
  78. package/tests/agents-view.test.ts +274 -0
  79. package/tests/agents-widget.test.ts +111 -0
  80. package/tests/ask-user-choice.test.ts +157 -3
  81. package/tests/codegraph-tools.test.ts +110 -1
  82. package/tests/devbinary/native-review-parity.devtest.ts +108 -0
  83. package/tests/fixtures/agents-process-child.mjs +23 -0
  84. package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
  85. package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
  86. package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
  87. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
  88. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
  89. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
  90. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
  91. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
  92. package/tests/gentle-agents.test.ts +741 -0
  93. package/tests/gentle-ai-binary.test.ts +1 -1
  94. package/tests/gentle-ai-installer.test.ts +47 -47
  95. package/tests/gentle-ai-renderer.test.ts +65 -0
  96. package/tests/gentle-ai.test.ts +31 -14
  97. package/tests/gentle-card-text.ts +35 -0
  98. package/tests/gentle-shell.test.ts +527 -0
  99. package/tests/gentle-todo.test.ts +182 -0
  100. package/tests/issue-creation-skill.test.ts +103 -0
  101. package/tests/native-choice-list.test.ts +202 -0
  102. package/tests/native-fullscreen-interaction.test.ts +125 -0
  103. package/tests/native-pointer-region.test.ts +245 -0
  104. package/tests/native-review-capability-contract.test.ts +33 -1
  105. package/tests/native-review-cli.test.ts +40 -0
  106. package/tests/native-review-consent.test.ts +91 -0
  107. package/tests/native-review-parity-runtime.test.ts +8 -2
  108. package/tests/native-review-parity.test.ts +29 -22
  109. package/tests/orchestrator-budget.test.ts +71 -2
  110. package/tests/orchestrator-rdd-ownership.test.ts +10 -1
  111. package/tests/package-manifest.test.ts +134 -9
  112. package/tests/provider-contract-bundle.test.ts +76 -0
  113. package/tests/provider-contract-mirror.test.ts +19 -0
  114. package/tests/quiet-tool-rendering.test.ts +96 -37
  115. package/tests/rdd-aware-verification-contract.test.ts +216 -0
  116. package/tests/rdd-status-line.test.ts +286 -0
  117. package/tests/review-agent-end-preflight.test.ts +408 -0
  118. package/tests/review-candidate-view.test.ts +452 -6
  119. package/tests/review-contract-prompt.test.ts +142 -0
  120. package/tests/review-controller-native-recovery.test.ts +29 -4
  121. package/tests/review-controller-native-routing.test.ts +321 -4
  122. package/tests/review-controller-workspace-root.test.ts +45 -2
  123. package/tests/review-controller.test.ts +26 -1
  124. package/tests/review-host-relay-routing.test.ts +229 -11
  125. package/tests/review-host-relay.test.ts +195 -7
  126. package/tests/review-integration-v2-forward.test.ts +47 -0
  127. package/tests/review-integration-v2.test.ts +112 -0
  128. package/tests/review-last-event-closure.test.ts +7 -2
  129. package/tests/review-ledger-contract.test.ts +1 -1
  130. package/tests/review-relay-contract.test.ts +26 -0
  131. package/tests/review-repository.test.ts +28 -1
  132. package/tests/review-risk-assessment.test.ts +626 -0
  133. package/tests/review-session-standing-permission-controller.test.ts +608 -0
  134. package/tests/review-session-standing-permission-ipc.test.ts +233 -0
  135. package/tests/review-session-standing-permission-runtime.test.ts +212 -0
  136. package/tests/review-session-standing-permission.test.ts +126 -0
  137. package/tests/runtime-harness.mjs +1 -0
  138. package/tests/shell-bar.test.ts +176 -0
  139. package/tests/shell-card.test.ts +118 -0
  140. package/tests/shell-changes-view.test.ts +146 -0
  141. package/tests/shell-changes.test.ts +182 -0
  142. package/tests/shell-prompt.test.ts +118 -0
  143. package/tests/shell-todo.test.ts +170 -0
  144. package/tests/shell-usage-view.test.ts +62 -0
  145. package/tests/shell-usage.test.ts +197 -0
  146. package/tests/telemetry-trigger.test.ts +349 -0
  147. package/tests/writer-edit-surface-scope.test.ts +153 -17
  148. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
  149. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
  150. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
  151. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
  152. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
  153. /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
@@ -284,7 +284,19 @@ export interface ReviewHostRelayResult {
284
284
  readonly submission: string;
285
285
  }
286
286
 
287
+ /** Opaque materialize-and-review result, not yet submitted to the provider. */
288
+ export interface ReviewHostRelayPreparedResult {
289
+ /** Copy-safe request snapshot captured before materialization starts. */
290
+ readonly request: ReviewHostRelayRequest;
291
+ readonly promptByteLength: number;
292
+ readonly resultByteLength: number;
293
+ }
294
+
295
+ const preparedResultBytes = new WeakMap<ReviewHostRelayPreparedResult, Buffer>();
296
+
287
297
  export type ReviewHostRelayRunner = (request: ReviewHostRelayRequest) => Promise<ReviewHostRelayResult>;
298
+ export type ReviewHostRelayPreparationRunner = (request: ReviewHostRelayRequest) => Promise<ReviewHostRelayPreparedResult>;
299
+ export type ReviewHostRelaySubmissionRunner = (prepared: ReviewHostRelayPreparedResult) => Promise<ReviewHostRelayResult>;
288
300
 
289
301
  const DEFAULT_GENTLE_AI_TIMEOUT_MS = 120_000;
290
302
 
@@ -442,38 +454,54 @@ function assertTokens(name: string, tokens: readonly string[]): void {
442
454
  }
443
455
  }
444
456
 
457
+ function snapshotReviewHostRelayRequest(request: ReviewHostRelayRequest): ReviewHostRelayRequest {
458
+ assertTokens("capture", request.captureArgumentTokens);
459
+ // The completing form is validated before any process launches: a materialize
460
+ // slot without a provider-owned submission is a typed contract mismatch,
461
+ // never a synthesized invocation.
462
+ resolveReviewHostRelaySubmission(request.submission);
463
+ const gentleAiExecutable = request.gentleAiExecutable ?? resolveGentleAiBinary();
464
+ if (!isAbsolute(gentleAiExecutable)) throw new TypeError("Pi host relay requires an absolute gentle-ai executable path");
465
+ const environment = Object.freeze({ ...(request.environment ?? process.env) }) as NodeJS.ProcessEnv;
466
+ const submission = request.submission === undefined ? undefined : Object.freeze({
467
+ operationToken: request.submission.operationToken,
468
+ argumentTokens: Object.freeze([...request.submission.argumentTokens]),
469
+ values: Object.freeze(request.submission.values.map((value) => Object.freeze({ ...value }))),
470
+ });
471
+ return Object.freeze({
472
+ ...request,
473
+ captureArgumentTokens: Object.freeze([...request.captureArgumentTokens]),
474
+ ...(submission === undefined ? {} : { submission }),
475
+ gentleAiExecutable,
476
+ environment,
477
+ gentleAiTimeoutMs: request.gentleAiTimeoutMs ?? DEFAULT_GENTLE_AI_TIMEOUT_MS,
478
+ targetCwd: request.targetCwd ?? process.cwd(),
479
+ });
480
+ }
481
+
445
482
  /**
446
- * Runs one complete host-relay capture for one provider-bound slot:
447
- * materialize → opaque Pi adapter → submit. Throws a typed
448
- * {@link ReviewHostRelayError} on every failure leg and submits nothing after
449
- * a failure; the caller re-queries negotiated STATUS instead of retrying.
483
+ * Materializes one provider-bound reviewer prompt and runs its opaque Pi
484
+ * subprocess. It does not submit anything, so independent reviewer work can
485
+ * finish before the caller performs provider-ordered admission.
450
486
  */
451
- export async function runReviewHostRelaySlot(request: ReviewHostRelayRequest): Promise<ReviewHostRelayResult> {
452
- assertTokens("capture", request.captureArgumentTokens);
453
- // The completing form is validated before any process launches: a
454
- // materialize slot without a provider-owned submission is a typed
455
- // contract mismatch, never a synthesized invocation.
456
- const submissionBinding = resolveReviewHostRelaySubmission(request.submission);
457
- const gentleAi = request.gentleAiExecutable ?? resolveGentleAiBinary();
458
- if (!isAbsolute(gentleAi)) throw new TypeError("Pi host relay requires an absolute gentle-ai executable path");
459
- const baseEnvironment = request.environment ?? process.env;
460
- // Every gentle-ai invocation the relay makes carries the handshake; the
461
- // pi subprocess environment stays exactly as the user configured it.
462
- const gentleAiEnvironment = { ...baseEnvironment, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT };
463
- const gentleAiTimeoutMs = request.gentleAiTimeoutMs ?? DEFAULT_GENTLE_AI_TIMEOUT_MS;
464
- const targetCwd = request.targetCwd ?? process.cwd();
465
-
466
- // (a) Materialize the Go-issued opaque prompt. This invocation is also the
467
- // capability detection: an old binary's unknown-flag refusal proves the
468
- // relay surface is absent, and the provider's handshake refusal surfaces
469
- // verbatim. No version sniffing.
487
+ export async function prepareReviewHostRelaySlot(
488
+ request: ReviewHostRelayRequest,
489
+ reviewer: typeof runOpaquePiReviewer = runOpaquePiReviewer,
490
+ ): Promise<ReviewHostRelayPreparedResult> {
491
+ // Copy mutable transport configuration before the first async boundary. The
492
+ // supplied AbortSignal intentionally stays live across materialize, reviewer,
493
+ // and submit, preserving the established cancellation behavior.
494
+ const preparedRequest = snapshotReviewHostRelayRequest(request);
495
+
496
+ // The provider materializes the opaque prompt and detects whether this relay
497
+ // surface is available. No version sniffing or prompt reconstruction occurs.
470
498
  let materialized: ProcessCapture;
471
499
  try {
472
- materialized = await collectGentleAiProcess(gentleAi, ["review", "capture-result", ...request.captureArgumentTokens], {
473
- cwd: targetCwd,
474
- env: gentleAiEnvironment,
475
- timeoutMs: gentleAiTimeoutMs,
476
- ...(request.signal === undefined ? {} : { signal: request.signal }),
500
+ materialized = await collectGentleAiProcess(preparedRequest.gentleAiExecutable!, ["review", "capture-result", ...preparedRequest.captureArgumentTokens], {
501
+ cwd: preparedRequest.targetCwd!,
502
+ env: { ...preparedRequest.environment!, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT },
503
+ timeoutMs: preparedRequest.gentleAiTimeoutMs!,
504
+ ...(preparedRequest.signal === undefined ? {} : { signal: preparedRequest.signal }),
477
505
  });
478
506
  } catch (error) {
479
507
  throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.MATERIALIZE_FAILED, "materialize", `gentle-ai prompt materialization could not start: ${error instanceof Error ? error.message : String(error)}`);
@@ -481,45 +509,91 @@ export async function runReviewHostRelaySlot(request: ReviewHostRelayRequest): P
481
509
  if (materialized.exitCode !== 0 || materialized.timedOut) {
482
510
  const stderr = materialized.stderr.toString("utf8");
483
511
  const refusal = classifyReviewHostRelayRefusal(stderr);
484
- const timing = { elapsedMs: materialized.elapsedMs, timeoutMs: gentleAiTimeoutMs };
512
+ const timing = { elapsedMs: materialized.elapsedMs, timeoutMs: preparedRequest.gentleAiTimeoutMs! };
485
513
  if (refusal === "unknown-flag") {
486
- throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE, "materialize", REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, { exitCode: materialized.exitCode, stderr, timedOut: materialized.timedOut, ...timing });
514
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE, "materialize", REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, {
515
+ exitCode: materialized.exitCode,
516
+ stderr,
517
+ timedOut: materialized.timedOut,
518
+ ...timing,
519
+ });
487
520
  }
488
521
  if (refusal === "handshake") {
489
- throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, "materialize", stderr, { exitCode: materialized.exitCode, stderr, timedOut: materialized.timedOut, ...timing });
522
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, "materialize", stderr, {
523
+ exitCode: materialized.exitCode,
524
+ stderr,
525
+ timedOut: materialized.timedOut,
526
+ ...timing,
527
+ });
490
528
  }
491
529
  throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.MATERIALIZE_FAILED, "materialize", materialized.timedOut
492
- ? `gentle-ai prompt materialization exceeded its ${gentleAiTimeoutMs}ms bound after ${materialized.elapsedMs}ms`
530
+ ? `gentle-ai prompt materialization exceeded its ${preparedRequest.gentleAiTimeoutMs!}ms bound after ${materialized.elapsedMs}ms`
493
531
  : "gentle-ai prompt materialization failed", { exitCode: materialized.exitCode, stderr, timedOut: materialized.timedOut, ...timing });
494
532
  }
495
533
  const promptBytes = materialized.stdout;
496
534
  if (promptBytes.length === 0) {
497
- throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.EMPTY_PROMPT, "materialize", "gentle-ai prompt materialization produced no bytes", { exitCode: 0, stderr: materialized.stderr.toString("utf8"), elapsedMs: materialized.elapsedMs, timeoutMs: gentleAiTimeoutMs });
535
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.EMPTY_PROMPT, "materialize", "gentle-ai prompt materialization produced no bytes", {
536
+ exitCode: 0,
537
+ stderr: materialized.stderr.toString("utf8"),
538
+ elapsedMs: materialized.elapsedMs,
539
+ timeoutMs: preparedRequest.gentleAiTimeoutMs!,
540
+ });
498
541
  }
499
542
  // The reviewer bound is derived from the prompt the provider actually
500
- // materialized, so it can only be resolved here. An explicit request
501
- // timeout (the test seam) still wins over both the override and the scale.
502
- const piTimeoutMs = request.piTimeoutMs ?? resolveReviewHostRelayPiTimeoutMs(promptBytes.length, baseEnvironment);
543
+ // materialized. The explicit request timeout is a test seam that wins over
544
+ // both the user-owned environment override and the scale-derived bound.
545
+ const piTimeoutMs = preparedRequest.piTimeoutMs ?? resolveReviewHostRelayPiTimeoutMs(promptBytes.length, preparedRequest.environment);
503
546
 
504
- // (b) The pure adapter owns the fresh isolated Pi process. Its input and
505
- // output are opaque bytes; this coordinator only maps transport failures to
506
- // the established relay boundary.
547
+ // The pure adapter owns the fresh isolated Pi process. Its input and output
548
+ // are opaque bytes; this coordinator only maps transport failures.
507
549
  let piResult: OpaquePiReviewerResult;
508
550
  try {
509
- piResult = await runOpaquePiReviewer(promptBytes, {
510
- ...(request.piExecutable === undefined ? {} : { piExecutable: request.piExecutable }),
511
- environment: baseEnvironment,
551
+ piResult = await reviewer(promptBytes, {
552
+ ...(preparedRequest.piExecutable === undefined ? {} : { piExecutable: preparedRequest.piExecutable }),
553
+ environment: preparedRequest.environment,
512
554
  timeoutMs: piTimeoutMs,
513
- ...(request.signal === undefined ? {} : { signal: request.signal }),
555
+ ...(preparedRequest.signal === undefined ? {} : { signal: preparedRequest.signal }),
514
556
  });
515
557
  } catch (error) {
516
558
  throw relayPiTransportError(error, promptBytes.length, piTimeoutMs);
517
559
  }
518
- const resultBytes = piResult.stdout;
560
+ const prepared = Object.freeze({
561
+ request: preparedRequest,
562
+ promptByteLength: promptBytes.length,
563
+ resultByteLength: piResult.stdoutByteLength,
564
+ });
565
+ preparedResultBytes.set(prepared, Buffer.from(piResult.stdout));
566
+ return prepared;
567
+ }
568
+
569
+ /**
570
+ * Starts every reviewer before awaiting any result. If one or more reviewers
571
+ * fail, it rejects only after every started transport has settled and reports
572
+ * the earliest failed request in provider order.
573
+ */
574
+ export async function runReviewHostRelayReviewerGroup(
575
+ requests: readonly ReviewHostRelayRequest[],
576
+ prepare: ReviewHostRelayPreparationRunner = prepareReviewHostRelaySlot,
577
+ ): Promise<readonly ReviewHostRelayPreparedResult[]> {
578
+ if (requests.length === 0) {
579
+ throw new TypeError("Pi host relay reviewer group requires at least one provider-bound request");
580
+ }
581
+ const settled = await Promise.allSettled(requests.map(async (request) => await prepare(request)));
582
+ const failed = settled.find((result) => result.status === "rejected");
583
+ if (failed?.status === "rejected") throw failed.reason;
584
+ return settled.map((result) => (result as PromiseFulfilledResult<ReviewHostRelayPreparedResult>).value);
585
+ }
519
586
 
520
- // (c) Submit the raw final bytes untouched through the provider-owned
521
- // completing form: its exact operation and argument tokens, with only the
522
- // artifact path substituted into the declared {{value}} slot.
587
+ /**
588
+ * Submits one already-reviewed opaque result through the exact provider-owned
589
+ * completing form. Only the provider-declared artifact slot is substituted.
590
+ */
591
+ export async function submitReviewHostRelayPreparedResult(prepared: ReviewHostRelayPreparedResult): Promise<ReviewHostRelayResult> {
592
+ const resultBytes = preparedResultBytes.get(prepared);
593
+ if (resultBytes === undefined) throw new TypeError("Pi host relay requires a recognized prepared result");
594
+ const { request } = prepared;
595
+ assertTokens("capture", request.captureArgumentTokens);
596
+ const submissionBinding = resolveReviewHostRelaySubmission(request.submission);
523
597
  const stagingDirectory = await mkdtemp(join(tmpdir(), "gentle-pi-host-relay-result-"));
524
598
  let primaryFailure = false;
525
599
  try {
@@ -528,14 +602,16 @@ export async function runReviewHostRelaySlot(request: ReviewHostRelayRequest): P
528
602
  await writeFile(resultFile, resultBytes, { mode: 0o600 });
529
603
  await chmod(resultFile, 0o600);
530
604
  const submitTokens = submissionBinding.argumentTokens.map((token, index) =>
531
- index === submissionBinding.substitutionLocation ? token.split(REVIEW_HOST_RELAY_SUBMISSION_VALUE_SLOT).join(resultFile) : token,
605
+ index === submissionBinding.substitutionLocation
606
+ ? token.split(REVIEW_HOST_RELAY_SUBMISSION_VALUE_SLOT).join(resultFile)
607
+ : token,
532
608
  );
533
609
  let submission: ProcessCapture;
534
610
  try {
535
- submission = await collectGentleAiProcess(gentleAi, ["review", submissionBinding.operationToken, ...submitTokens], {
536
- cwd: targetCwd,
537
- env: gentleAiEnvironment,
538
- timeoutMs: gentleAiTimeoutMs,
611
+ submission = await collectGentleAiProcess(request.gentleAiExecutable!, ["review", submissionBinding.operationToken, ...submitTokens], {
612
+ cwd: request.targetCwd!,
613
+ env: { ...request.environment!, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT },
614
+ timeoutMs: request.gentleAiTimeoutMs!,
539
615
  ...(request.signal === undefined ? {} : { signal: request.signal }),
540
616
  });
541
617
  } catch (error) {
@@ -543,20 +619,22 @@ export async function runReviewHostRelaySlot(request: ReviewHostRelayRequest): P
543
619
  }
544
620
  if (submission.exitCode !== 0 || submission.timedOut || submission.stdout.length === 0) {
545
621
  const stderr = submission.stderr.toString("utf8");
546
- const details = { exitCode: submission.exitCode, stderr, timedOut: submission.timedOut, elapsedMs: submission.elapsedMs, timeoutMs: gentleAiTimeoutMs };
547
- // A typed admission refusal is a proven non-mutation whose reason is
548
- // the refusal text itself; everything else that launched (timeout,
549
- // signal, untyped exit) stays unknown pending STATUS.
622
+ const details = { exitCode: submission.exitCode, stderr, timedOut: submission.timedOut, elapsedMs: submission.elapsedMs, timeoutMs: request.gentleAiTimeoutMs! };
623
+ // A typed admission refusal proves the provider consumed no slot.
624
+ // Every other launched submission stays unknown pending fresh STATUS.
550
625
  if (isReviewHostRelayAdmissionRefusal(submission, stderr)) {
551
- throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", stderr.trim(), { ...details, mutationOutcome: "none" });
626
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", stderr.trim(), {
627
+ ...details,
628
+ mutationOutcome: "none",
629
+ });
552
630
  }
553
631
  throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", submission.timedOut
554
- ? `gentle-ai capture submission exceeded its ${gentleAiTimeoutMs}ms bound after ${submission.elapsedMs}ms`
632
+ ? `gentle-ai capture submission exceeded its ${request.gentleAiTimeoutMs!}ms bound after ${submission.elapsedMs}ms`
555
633
  : "gentle-ai refused the relayed capture submission", details);
556
634
  }
557
635
  return {
558
- promptByteLength: promptBytes.length,
559
- resultByteLength: piResult.stdoutByteLength,
636
+ promptByteLength: prepared.promptByteLength,
637
+ resultByteLength: prepared.resultByteLength,
560
638
  submission: submission.stdout.toString("utf8"),
561
639
  };
562
640
  } catch (error) {
@@ -576,3 +654,11 @@ export async function runReviewHostRelaySlot(request: ReviewHostRelayRequest): P
576
654
  }
577
655
  }
578
656
  }
657
+
658
+ /**
659
+ * Compatibility one-binding path: materialize → opaque Pi adapter → submit.
660
+ * It preserves the established API and its typed failure behavior exactly.
661
+ */
662
+ export async function runReviewHostRelaySlot(request: ReviewHostRelayRequest): Promise<ReviewHostRelayResult> {
663
+ return await submitReviewHostRelayPreparedResult(await prepareReviewHostRelaySlot(request));
664
+ }
@@ -79,7 +79,10 @@ const RISK_LEVELS = ["low", "medium", "high"] as const;
79
79
  const REVIEW_LENSES = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
80
80
  const RISK_REASON_CODES = ["configuration_change", "empty_content", "executable_change", "executable_mode", "hot_path", "large_change", "non_executable_only", "process_boundary", "process_scan_limit", "service_token", "shell_source"] as const;
81
81
  const RISK_SIGNALS = ["auth", "update", "security", "payments", "permissions", "shell_process"] as const;
82
- const STATUS_ACTIONS = ["start", "recover", "maintainer_action", "select_lineage", "repair_authority", "stop"] as const;
82
+ // "collect" and "execute" are the v2 envelope's projection of a live
83
+ // transaction whose next_transition is mandatory: the root action names the
84
+ // transition kind instead of reading as a terminal stop.
85
+ const STATUS_ACTIONS = ["start", "recover", "maintainer_action", "select_lineage", "repair_authority", "stop", "collect", "execute"] as const;
83
86
  const RECEIPT_STATUSES = ["expected_missing", "present", "publication_pending", "not_applicable"] as const;
84
87
  type ReviewReceiptStatus = (typeof RECEIPT_STATUSES)[number];
85
88
  export const REVIEW_STATUS_ACTION_DISPOSITION = {
@@ -183,6 +186,19 @@ const CAPABILITIES_SCHEMA_IDENTITIES: Readonly<Record<string, { protocolMinor: n
183
186
  requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
184
187
  optionalFeatureFloor: 14,
185
188
  }),
189
+ // Ground-truthed against the published v2.6.0 binary: capabilities/v2.5
190
+ // advertises the same required surface as v2.4 (status/v6 is still
191
+ // advertised for compatibility) plus the new status/v7 schema, which is a
192
+ // superset-checked addition, not a requirement -- decodeReviewStatusV3
193
+ // accepts v7 as an additive extension of v6, so the required-schema floor
194
+ // stays unchanged. The v2.6.0 binary advertised 15 optional features
195
+ // (floor stays at 14, its established minimum).
196
+ "gentle-ai.review-integration.capabilities/v2.5": Object.freeze({
197
+ protocolMinor: 5,
198
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON_V23, "gentle-ai.review-integration.capabilities/v2.5", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v4", "gentle-ai.review-integration.status/v6", "gentle-ai.review-intended-untracked-selection/v1"]),
199
+ requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
200
+ optionalFeatureFloor: 14,
201
+ }),
186
202
  });
187
203
  const OPTIONAL_FEATURE_NAMES = Object.freeze([
188
204
  "base_ref_workspace_overlay",
@@ -372,6 +388,8 @@ export interface ReviewStatusFrozenV1 {
372
388
  tier: RiskLevel;
373
389
  originalChangedLines: number;
374
390
  correctionBudget: number;
391
+ /** Digest of the frozen changed-path manifest; binds manifest-less capture inputs (gentle-ai#3922). */
392
+ changedPathManifestSha256?: string;
375
393
  }
376
394
 
377
395
  export interface ReviewStatusReconciliationV1 {
@@ -532,6 +550,16 @@ export interface ReviewCollectInputV3 {
532
550
  validationRequest?: ReviewTargetedValidationRequestV1;
533
551
  }
534
552
 
553
+ // gentle-pi#627: the exact `gentle-ai sync` invocation that resolves a stale
554
+ // managed-asset set (STATUS next_transition stop, and START's preflight
555
+ // failure envelope both carry this same shape).
556
+ export interface ReviewManagedAssetsContinuationV1 {
557
+ operation: "sync";
558
+ command: string;
559
+ agent?: string;
560
+ staleAssets?: readonly string[];
561
+ }
562
+
535
563
  export interface ReviewNextTransitionV3 {
536
564
  kind: "execute" | "collect" | "stop";
537
565
  reasonCode: string;
@@ -539,6 +567,8 @@ export interface ReviewNextTransitionV3 {
539
567
  collect?: { inputs: readonly ReviewCollectInputV3[] };
540
568
  /** status/v5 only: the bounded correction plan request. */
541
569
  correctionRequest?: ReviewCorrectionPlanRequestV1;
570
+ /** stop only, reason_code managed_assets_outdated: the sync continuation. */
571
+ continuation?: ReviewManagedAssetsContinuationV1;
542
572
  }
543
573
 
544
574
  export interface ReviewStatusV3 {
@@ -569,6 +599,8 @@ export interface ReviewStatusV3 {
569
599
  repositoryContext?: ReviewRepositoryContextV2;
570
600
  /** status/v5 only: the provider-owned request mirrored by the targeted-validator collect input. */
571
601
  validationRequest?: ReviewTargetedValidationRequestV1;
602
+ /** status/v7 only: the eligible untracked-path inventory digest, absent on the `staged` projection. */
603
+ eligibleUntrackedInventory?: string;
572
604
  raw: Readonly<Record<string, unknown>>;
573
605
  }
574
606
 
@@ -690,6 +722,8 @@ export interface ReviewFailureV2 {
690
722
  causeCategory?: ReviewFailureCauseCategoryV2;
691
723
  cause?: string;
692
724
  context?: ReviewFailureContextV2;
725
+ /** code=managed_assets_outdated only: the sync continuation (gentle-pi#627). */
726
+ continuation?: ReviewManagedAssetsContinuationV1;
693
727
  raw: Readonly<Record<string, unknown>>;
694
728
  }
695
729
 
@@ -1624,8 +1658,11 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean, v6: bool
1624
1658
  }
1625
1659
 
1626
1660
  if (captureOperation === "review.capture-result") {
1627
- if (input.artifact_subject === undefined || input.base_tree === undefined || input.candidate_tree === undefined || input.changed_path_manifest === undefined) {
1628
- throw new TypeError(`${label} requires artifact_subject, base_tree, candidate_tree, and changed_path_manifest`);
1661
+ // changed_path_manifest is optional here: the artifact subject's
1662
+ // changed_path_manifest_sha256 already binds the frozen manifest, and
1663
+ // gentle-ai stops inlining one copy per lens (gentle-ai#3922).
1664
+ if (input.artifact_subject === undefined || input.base_tree === undefined || input.candidate_tree === undefined) {
1665
+ throw new TypeError(`${label} requires artifact_subject, base_tree, and candidate_tree`);
1629
1666
  }
1630
1667
  if (schema !== "https://gentle-ai.dev/schema/review/reviewer/v1") throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/reviewer/v1`);
1631
1668
  } else if (input.artifact_subject !== undefined || input.base_tree !== undefined || input.candidate_tree !== undefined || input.changed_path_manifest !== undefined) {
@@ -1658,12 +1695,32 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean, v6: bool
1658
1695
  };
1659
1696
  }
1660
1697
 
1698
+ // gentle-pi#627: gentle-ai's managed-assets continuation (STATUS next_transition
1699
+ // stop reason_code=managed_assets_outdated, and START's preflight failure
1700
+ // envelope) names the exact `gentle-ai sync` invocation that resolves a stale
1701
+ // managed-asset set without abandoning the frozen candidate. Decoded verbatim
1702
+ // from internal/cli/review_operation_contract.go's ReviewManagedAssetsContinuation.
1703
+ export function decodeReviewManagedAssetsContinuationV1(value: unknown, label: string): ReviewManagedAssetsContinuationV1 {
1704
+ const body = exactRecord(value, label, ["operation", "command"], ["agent", "stale_assets"]);
1705
+ const operation = enumeration(body.operation, ["sync"] as const, `${label}.operation`);
1706
+ const command = nonempty(body.command, `${label}.command`);
1707
+ const agent = body.agent === undefined ? undefined : nonempty(body.agent, `${label}.agent`);
1708
+ const staleAssets = body.stale_assets === undefined ? undefined : stringArray(body.stale_assets, `${label}.stale_assets`, { minimum: 1 });
1709
+ return { operation, command, ...(agent === undefined ? {} : { agent }), ...(staleAssets === undefined ? {} : { staleAssets }) };
1710
+ }
1711
+
1661
1712
  export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boolean; v6?: boolean } = {}): ReviewNextTransitionV3 {
1662
1713
  const v6 = options.v6 === true;
1663
1714
  const v5 = options.v5 === true || v6;
1664
- const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : [])]);
1715
+ const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : []), "continuation"]);
1665
1716
  const kind = enumeration(transition.kind, ["execute", "collect", "stop"] as const, "next_transition.kind");
1666
1717
  const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1718
+ const continuation = transition.continuation === undefined ? undefined : decodeReviewManagedAssetsContinuationV1(transition.continuation, "next_transition.continuation");
1719
+ // gentle-pi#627: continuation is optional on every status version an older
1720
+ // gentle-ai may emit a managed_assets_outdated stop with no continuation at
1721
+ // all, and decode must not refuse the whole envelope for that. When present
1722
+ // it is only valid on a stop with this exact reason_code.
1723
+ if (continuation !== undefined && !(kind === "stop" && reasonCode === "managed_assets_outdated")) throw new TypeError("next_transition.continuation is only valid for a stop transition with reason_code managed_assets_outdated");
1667
1724
 
1668
1725
  // status/v5: the bounded correction plan request rides exactly its two
1669
1726
  // reason codes and never any other (vendored status-v5.schema.json).
@@ -1713,7 +1770,7 @@ export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boo
1713
1770
  return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1714
1771
  }
1715
1772
  if (transition.execute !== undefined || transition.collect !== undefined) throw new TypeError("next_transition stop cannot carry a transition");
1716
- return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1773
+ return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }), ...(continuation === undefined ? {} : { continuation }) };
1717
1774
  }
1718
1775
 
1719
1776
  // ---------------------------------------------------------------------------
@@ -1761,15 +1818,19 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1761
1818
  // Additive forward acceptance: status/v5 (gentle-ai main; ground-truthed
1762
1819
  // against a live capture and the vendored status-v5.schema.json) is the v3
1763
1820
  // key set plus the optional forecast and the v5-only next_transition
1764
- // surfaces. status/v6 adds the intended-untracked selection; v3 keeps
1765
- // rejecting every v5/v6-only field.
1821
+ // surfaces. status/v6 adds the intended-untracked selection; status/v7
1822
+ // (gentle-ai v2.6.0, advertised through capabilities/v2.5 alongside v6)
1823
+ // adds only the top-level optional `eligible_untracked_inventory` digest,
1824
+ // so it is decoded on the v6 surface. v3 keeps rejecting every v5/v6/v7-only
1825
+ // field.
1766
1826
  const schema = typeof value === "object" && value !== null ? (value as Record<string, unknown>).schema : undefined;
1767
- const v6 = schema === "gentle-ai.review-integration.status/v6";
1827
+ const v7 = schema === "gentle-ai.review-integration.status/v7";
1828
+ const v6 = v7 || schema === "gentle-ai.review-integration.status/v6";
1768
1829
  const v5 = v6 || schema === "gentle-ai.review-integration.status/v5";
1769
1830
  const body = exactRecord(value, "status", [
1770
1831
  "schema", "contract", "operation", "applicability", "action", "replayability", "target_identity", "projection", "repair", "candidates",
1771
- ], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : [])]);
1772
- requireIdentity(body, v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
1832
+ ], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : []), ...(v7 ? ["eligible_untracked_inventory"] : [])]);
1833
+ requireIdentity(body, v7 ? "gentle-ai.review-integration.status/v7" : v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
1773
1834
 
1774
1835
  const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] as const, "status.applicability");
1775
1836
  let receipt: ReviewStatusReceiptV1 | undefined;
@@ -1798,11 +1859,12 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1798
1859
 
1799
1860
  let frozen: ReviewStatusFrozenV1 | undefined;
1800
1861
  if (body.frozen !== undefined) {
1801
- const source = exactRecord(body.frozen, "status.frozen", ["tier", "original_changed_lines", "correction_budget"]);
1862
+ const source = exactRecord(body.frozen, "status.frozen", ["tier", "original_changed_lines", "correction_budget"], ["changed_path_manifest_sha256"]);
1802
1863
  frozen = {
1803
1864
  tier: enumeration(source.tier, RISK_LEVELS, "status.frozen.tier"),
1804
1865
  originalChangedLines: integer(source.original_changed_lines, "status.frozen.original_changed_lines"),
1805
1866
  correctionBudget: integer(source.correction_budget, "status.frozen.correction_budget", 0, 200),
1867
+ ...(source.changed_path_manifest_sha256 === undefined ? {} : { changedPathManifestSha256: sha256(source.changed_path_manifest_sha256, "status.frozen.changed_path_manifest_sha256") }),
1806
1868
  };
1807
1869
  }
1808
1870
  if (authority?.version === REVIEW_AUTHORITY_VERSION.COMPACT_V2 && frozen === undefined) throw new TypeError("compact-v2 status requires frozen metadata");
@@ -1851,6 +1913,15 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1851
1913
  };
1852
1914
  }
1853
1915
 
1916
+ // status/v7 top-level optional digest (gentle-ai v2.6.0): resolves #4066's
1917
+ // closed loop where `sdd-attempt finish` named a digest status never
1918
+ // published. Absent on the `staged` projection, which never resolves an
1919
+ // inventory, so it stays structurally optional rather than a required v7
1920
+ // field.
1921
+ const eligibleUntrackedInventory = v7 && body.eligible_untracked_inventory !== undefined
1922
+ ? sha256(body.eligible_untracked_inventory, "status.eligible_untracked_inventory")
1923
+ : undefined;
1924
+
1854
1925
  return {
1855
1926
  contract: REVIEW_INTEGRATION_CONTRACT,
1856
1927
  applicability,
@@ -1869,6 +1940,7 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1869
1940
  ...(forecast === undefined ? {} : { forecast }),
1870
1941
  ...(repositoryContext === undefined ? {} : { repositoryContext }),
1871
1942
  ...(validationRequest === undefined ? {} : { validationRequest }),
1943
+ ...(eligibleUntrackedInventory === undefined ? {} : { eligibleUntrackedInventory }),
1872
1944
  raw: body,
1873
1945
  };
1874
1946
  }
@@ -2019,9 +2091,10 @@ function decodeFailureContext(value: unknown, label: string): ReviewFailureConte
2019
2091
  export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
2020
2092
  const body = exactRecord(value, "failure", [
2021
2093
  "schema", "contract", "operation", "phase", "code", "message", "mutation_outcome", "authority_applicability", "retry_safe", "replayability", "required_inputs", "next_action",
2022
- ], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context"]);
2094
+ ], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context", "continuation"]);
2023
2095
  requireIdentity(body, "gentle-ai.review-integration.failure/v2");
2024
2096
  const operation = enumeration(body.operation, FAILURE_OPERATIONS, "failure.operation");
2097
+ const code = text(body.code, "failure.code", { pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ });
2025
2098
 
2026
2099
  if (body.progress_identity !== undefined) {
2027
2100
  if (body.lineage_id === undefined || body.request_digest === undefined) throw new TypeError("failure.progress_identity requires lineage_id and request_digest");
@@ -2030,13 +2103,18 @@ export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
2030
2103
  if (body.request_digest !== undefined && operation === REVIEW_INTEGRATION_OPERATION.REPAIR && body.progress_identity === undefined) {
2031
2104
  throw new TypeError("failure.request_digest with review.repair requires progress_identity");
2032
2105
  }
2106
+ // gentle-pi#627: continuation is optional on every version — an older
2107
+ // gentle-ai may emit managed_assets_outdated with no continuation, and
2108
+ // decode must not refuse the whole envelope for that. When present it is
2109
+ // only valid on this exact code.
2110
+ if (body.continuation !== undefined && code !== "managed_assets_outdated") throw new TypeError("failure.continuation is only valid for code managed_assets_outdated");
2033
2111
 
2034
2112
  return {
2035
2113
  schema: "gentle-ai.review-integration.failure/v2",
2036
2114
  contract: REVIEW_INTEGRATION_CONTRACT,
2037
2115
  operation,
2038
2116
  phase: enumeration(body.phase, ["preflight", "pre_native", "native_running", "native_committed", "reconciliation"] as const, "failure.phase"),
2039
- code: text(body.code, "failure.code", { pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ }),
2117
+ code,
2040
2118
  message: text(body.message, "failure.message", { minimum: 1, maximum: 240, pattern: /^[^\r\n]+$/ }),
2041
2119
  mutationOutcome: enumeration(body.mutation_outcome, Object.values(REVIEW_MUTATION_OUTCOME), "failure.mutation_outcome"),
2042
2120
  authorityApplicability: enumeration(body.authority_applicability, Object.values(REVIEW_AUTHORITY_APPLICABILITY), "failure.authority_applicability"),
@@ -2050,6 +2128,7 @@ export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
2050
2128
  ...(body.cause_category === undefined ? {} : { causeCategory: text(body.cause_category, "failure.cause_category", { minimum: 1, pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ }) }),
2051
2129
  ...(body.cause === undefined ? {} : { cause: text(body.cause, "failure.cause", { minimum: 1, maximum: 4000, pattern: /^[^\r\n]+$/ }) }),
2052
2130
  ...(body.context === undefined ? {} : { context: decodeFailureContext(body.context, "failure.context") }),
2131
+ ...(body.continuation === undefined ? {} : { continuation: decodeReviewManagedAssetsContinuationV1(body.continuation, "failure.continuation") }),
2053
2132
  raw: body,
2054
2133
  };
2055
2134
  }
@@ -4,6 +4,7 @@ import type { ReviewLastEventClosureBinding, ReviewStatusV3 } from "./review-int
4
4
  export interface ReviewLastEventCaptureSelector {
5
5
  readonly baseRef?: string;
6
6
  readonly committedOnly?: true;
7
+ readonly agent?: "pi";
7
8
  }
8
9
 
9
10
  /**
@@ -14,3 +14,14 @@
14
14
 
15
15
  export const GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV = "GENTLE_PI_REVIEW_RELAY_CONTRACT";
16
16
  export const GENTLE_PI_REVIEW_RELAY_CONTRACT = "gentle-pi.review-relay/v1";
17
+
18
+ // The host is this extension: every process the session starts (pi's shell
19
+ // tools, Gentle Agents children) runs under it, so the handshake belongs in
20
+ // the session environment too, not only in the extension's own invocations.
21
+ // Without it the first `gentle-ai review status --agent pi` typed in the
22
+ // session shell fails closed (gentle-pi#550). Returns whether anything changed.
23
+ export function declareReviewRelayHandshake(env: NodeJS.ProcessEnv): boolean {
24
+ if (env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV] === GENTLE_PI_REVIEW_RELAY_CONTRACT) return false;
25
+ env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV] = GENTLE_PI_REVIEW_RELAY_CONTRACT;
26
+ return true;
27
+ }
@@ -55,8 +55,8 @@ export function reviewGitEnvironment(): NodeJS.ProcessEnv {
55
55
  const environment: NodeJS.ProcessEnv = {};
56
56
  for (const [key, value] of Object.entries(process.env)) if (!key.startsWith("GIT_")) environment[key] = value;
57
57
  environment.GIT_CONFIG_NOSYSTEM = "1";
58
- environment.GIT_CONFIG_GLOBAL = process.platform === "win32" ? "NUL" : "/dev/null";
59
- environment.GIT_CONFIG_SYSTEM = process.platform === "win32" ? "NUL" : "/dev/null";
58
+ environment.GIT_CONFIG_GLOBAL = "/dev/null";
59
+ environment.GIT_CONFIG_SYSTEM = "/dev/null";
60
60
  environment.GIT_OPTIONAL_LOCKS = "0";
61
61
  environment.LC_ALL = "C";
62
62
  environment.LANG = "C";